@@ -281,6 +281,8 @@ export class ReleasePullRequest {
281281 * This allows the caller to update the branch ref atomically, avoiding
282282 * the race condition where resetting the branch first causes GitHub
283283 * to auto-close the PR (0 commits = closed).
284+ *
285+ * Implements lockstep versioning: all workspace packages get the same version.
284286 */
285287 private async createReleaseCommitAndGetSha (
286288 bump : VersionBump ,
@@ -301,23 +303,38 @@ export class ReleasePullRequest {
301303
302304 // Get current deno.json content
303305 let denoJsonContent : string ;
306+ let workspaceMembers : string [ ] = [ ] ;
304307 try {
305308 const file = await this . request < { content : string } > (
306309 `/repos/${ this . owner } /${ this . repo } /contents/deno.json?ref=${ this . baseBranch } ` ,
307310 ) ;
308311 denoJsonContent = atob ( file . content . replace ( / \n / g, '' ) ) ;
312+ const parsed = JSON . parse ( denoJsonContent ) ;
313+ workspaceMembers = parsed . workspace || [ ] ;
309314 } catch {
310315 denoJsonContent = '{}' ;
311316 }
312317
313- // Update version in deno.json
318+ // Update version in root deno.json
314319 const denoJson = JSON . parse ( denoJsonContent ) ;
315320 denoJson . version = bump . to ;
316321 const newDenoJson = JSON . stringify ( denoJson , null , 2 ) + '\n' ;
317322
323+ // Collect tree entries for all files to update
324+ const treeEntries : Array < { path : string ; mode : string ; type : string ; sha : string } > = [ ] ;
325+
326+ // Create blob for root deno.json
327+ const rootDenoBlob = await this . request < { sha : string } > (
328+ `/repos/${ this . owner } /${ this . repo } /git/blobs` ,
329+ {
330+ method : 'POST' ,
331+ body : JSON . stringify ( { content : newDenoJson , encoding : 'utf-8' } ) ,
332+ } ,
333+ ) ;
334+ treeEntries . push ( { path : 'deno.json' , mode : '100644' , type : 'blob' , sha : rootDenoBlob . sha } ) ;
335+
318336 // Get current .pls/versions.json content (or create new)
319337 // Note: Don't preserve SHA - it becomes stale after squash/rebase merge
320- // SHA is only set from GitHub releases API after merge
321338 let versionsContent : Record < string , string > ;
322339 try {
323340 const file = await this . request < { content : string } > (
@@ -334,53 +351,74 @@ export class ReleasePullRequest {
334351 } catch {
335352 versionsContent = { } ;
336353 }
354+
355+ // Set root version
337356 versionsContent [ '.' ] = bump . to ;
338- const newVersionsJson = JSON . stringify ( versionsContent , null , 2 ) + '\n' ;
339357
340- // Create blobs for both files
341- const denoBlob = await this . request < { sha : string } > (
342- `/repos/${ this . owner } /${ this . repo } /git/blobs` ,
343- {
344- method : 'POST' ,
345- body : JSON . stringify ( {
346- content : newDenoJson ,
347- encoding : 'utf-8' ,
348- } ) ,
349- } ,
350- ) ;
358+ // Update workspace member deno.json files (lockstep versioning)
359+ for ( const pattern of workspaceMembers ) {
360+ // Skip glob patterns - only handle direct paths
361+ if ( pattern . includes ( '*' ) ) continue ;
351362
363+ const memberPath = pattern . replace ( / ^ \. \/ / , '' ) ;
364+
365+ try {
366+ const file = await this . request < { content : string } > (
367+ `/repos/${ this . owner } /${ this . repo } /contents/${ memberPath } /deno.json?ref=${ this . baseBranch } ` ,
368+ ) ;
369+ const memberContent = atob ( file . content . replace ( / \n / g, '' ) ) ;
370+ const memberJson = JSON . parse ( memberContent ) ;
371+
372+ // Update version to match root (lockstep)
373+ memberJson . version = bump . to ;
374+ const newMemberJson = JSON . stringify ( memberJson , null , 2 ) + '\n' ;
375+
376+ // Create blob for member deno.json
377+ const memberBlob = await this . request < { sha : string } > (
378+ `/repos/${ this . owner } /${ this . repo } /git/blobs` ,
379+ {
380+ method : 'POST' ,
381+ body : JSON . stringify ( { content : newMemberJson , encoding : 'utf-8' } ) ,
382+ } ,
383+ ) ;
384+ treeEntries . push ( {
385+ path : `${ memberPath } /deno.json` ,
386+ mode : '100644' ,
387+ type : 'blob' ,
388+ sha : memberBlob . sha ,
389+ } ) ;
390+
391+ // Add to versions manifest
392+ versionsContent [ memberPath ] = bump . to ;
393+ } catch {
394+ // Member doesn't have deno.json, skip
395+ }
396+ }
397+
398+ // Create blob for versions.json
399+ const newVersionsJson = JSON . stringify ( versionsContent , null , 2 ) + '\n' ;
352400 const versionsBlob = await this . request < { sha : string } > (
353401 `/repos/${ this . owner } /${ this . repo } /git/blobs` ,
354402 {
355403 method : 'POST' ,
356- body : JSON . stringify ( {
357- content : newVersionsJson ,
358- encoding : 'utf-8' ,
359- } ) ,
404+ body : JSON . stringify ( { content : newVersionsJson , encoding : 'utf-8' } ) ,
360405 } ,
361406 ) ;
407+ treeEntries . push ( {
408+ path : '.pls/versions.json' ,
409+ mode : '100644' ,
410+ type : 'blob' ,
411+ sha : versionsBlob . sha ,
412+ } ) ;
362413
363- // Create tree with updated files
414+ // Create tree with all updated files
364415 const tree = await this . request < { sha : string } > (
365416 `/repos/${ this . owner } /${ this . repo } /git/trees` ,
366417 {
367418 method : 'POST' ,
368419 body : JSON . stringify ( {
369420 base_tree : baseCommit . tree . sha ,
370- tree : [
371- {
372- path : 'deno.json' ,
373- mode : '100644' ,
374- type : 'blob' ,
375- sha : denoBlob . sha ,
376- } ,
377- {
378- path : '.pls/versions.json' ,
379- mode : '100644' ,
380- type : 'blob' ,
381- sha : versionsBlob . sha ,
382- } ,
383- ] ,
421+ tree : treeEntries ,
384422 } ) ,
385423 } ,
386424 ) ;
@@ -494,6 +532,8 @@ ${optionsBlock}
494532 /**
495533 * Sync PR branch with the selected version.
496534 * Resets branch to base, creates fresh commit with new version, force pushes.
535+ *
536+ * Implements lockstep versioning: all workspace packages get the same version.
497537 */
498538 async syncBranch (
499539 prNumber : number ,
@@ -516,22 +556,38 @@ ${optionsBlock}
516556 `/repos/${ this . owner } /${ this . repo } /git/commits/${ baseSha } ` ,
517557 ) ;
518558
519- // Get current deno.json content
559+ // Get current deno.json content and workspace members
520560 let denoJsonContent : string ;
561+ let workspaceMembers : string [ ] = [ ] ;
521562 try {
522563 const file = await this . request < { content : string } > (
523564 `/repos/${ this . owner } /${ this . repo } /contents/deno.json?ref=${ baseBranch } ` ,
524565 ) ;
525566 denoJsonContent = atob ( file . content . replace ( / \n / g, '' ) ) ;
567+ const parsed = JSON . parse ( denoJsonContent ) ;
568+ workspaceMembers = parsed . workspace || [ ] ;
526569 } catch {
527570 denoJsonContent = '{}' ;
528571 }
529572
530- // Update version in deno.json
573+ // Update version in root deno.json
531574 const denoJson = JSON . parse ( denoJsonContent ) ;
532575 denoJson . version = selectedVersion ;
533576 const newDenoJson = JSON . stringify ( denoJson , null , 2 ) + '\n' ;
534577
578+ // Collect tree entries for all files to update
579+ const treeEntries : Array < { path : string ; mode : string ; type : string ; sha : string } > = [ ] ;
580+
581+ // Create blob for root deno.json
582+ const rootDenoBlob = await this . request < { sha : string } > (
583+ `/repos/${ this . owner } /${ this . repo } /git/blobs` ,
584+ {
585+ method : 'POST' ,
586+ body : JSON . stringify ( { content : newDenoJson , encoding : 'utf-8' } ) ,
587+ } ,
588+ ) ;
589+ treeEntries . push ( { path : 'deno.json' , mode : '100644' , type : 'blob' , sha : rootDenoBlob . sha } ) ;
590+
535591 // Get/create .pls/versions.json content
536592 // Note: Don't preserve SHA - it becomes stale after squash/rebase merge
537593 let versionsContent : Record < string , string > ;
@@ -550,37 +606,74 @@ ${optionsBlock}
550606 } catch {
551607 versionsContent = { } ;
552608 }
609+
610+ // Set root version
553611 versionsContent [ '.' ] = selectedVersion ;
554- const newVersionsJson = JSON . stringify ( versionsContent , null , 2 ) + '\n' ;
555612
556- // Create blobs
557- const denoBlob = await this . request < { sha : string } > (
558- `/repos/${ this . owner } /${ this . repo } /git/blobs` ,
559- {
560- method : 'POST' ,
561- body : JSON . stringify ( { content : newDenoJson , encoding : 'utf-8' } ) ,
562- } ,
563- ) ;
613+ // Update workspace member deno.json files (lockstep versioning)
614+ for ( const pattern of workspaceMembers ) {
615+ // Skip glob patterns - only handle direct paths
616+ if ( pattern . includes ( '*' ) ) continue ;
617+
618+ const memberPath = pattern . replace ( / ^ \. \/ / , '' ) ;
564619
620+ try {
621+ const file = await this . request < { content : string } > (
622+ `/repos/${ this . owner } /${ this . repo } /contents/${ memberPath } /deno.json?ref=${ baseBranch } ` ,
623+ ) ;
624+ const memberContent = atob ( file . content . replace ( / \n / g, '' ) ) ;
625+ const memberJson = JSON . parse ( memberContent ) ;
626+
627+ // Update version to match root (lockstep)
628+ memberJson . version = selectedVersion ;
629+ const newMemberJson = JSON . stringify ( memberJson , null , 2 ) + '\n' ;
630+
631+ // Create blob for member deno.json
632+ const memberBlob = await this . request < { sha : string } > (
633+ `/repos/${ this . owner } /${ this . repo } /git/blobs` ,
634+ {
635+ method : 'POST' ,
636+ body : JSON . stringify ( { content : newMemberJson , encoding : 'utf-8' } ) ,
637+ } ,
638+ ) ;
639+ treeEntries . push ( {
640+ path : `${ memberPath } /deno.json` ,
641+ mode : '100644' ,
642+ type : 'blob' ,
643+ sha : memberBlob . sha ,
644+ } ) ;
645+
646+ // Add to versions manifest
647+ versionsContent [ memberPath ] = selectedVersion ;
648+ } catch {
649+ // Member doesn't have deno.json, skip
650+ }
651+ }
652+
653+ // Create blob for versions.json
654+ const newVersionsJson = JSON . stringify ( versionsContent , null , 2 ) + '\n' ;
565655 const versionsBlob = await this . request < { sha : string } > (
566656 `/repos/${ this . owner } /${ this . repo } /git/blobs` ,
567657 {
568658 method : 'POST' ,
569659 body : JSON . stringify ( { content : newVersionsJson , encoding : 'utf-8' } ) ,
570660 } ,
571661 ) ;
662+ treeEntries . push ( {
663+ path : '.pls/versions.json' ,
664+ mode : '100644' ,
665+ type : 'blob' ,
666+ sha : versionsBlob . sha ,
667+ } ) ;
572668
573- // Create tree
669+ // Create tree with all updated files
574670 const tree = await this . request < { sha : string } > (
575671 `/repos/${ this . owner } /${ this . repo } /git/trees` ,
576672 {
577673 method : 'POST' ,
578674 body : JSON . stringify ( {
579675 base_tree : baseCommit . tree . sha ,
580- tree : [
581- { path : 'deno.json' , mode : '100644' , type : 'blob' , sha : denoBlob . sha } ,
582- { path : '.pls/versions.json' , mode : '100644' , type : 'blob' , sha : versionsBlob . sha } ,
583- ] ,
676+ tree : treeEntries ,
584677 } ) ,
585678 } ,
586679 ) ;
0 commit comments