@@ -30,6 +30,12 @@ const TRUSTED_SOURCE = Object.freeze({
3030 ownerId : 41929050 ,
3131} ) ;
3232const SHA_PATTERN = / ^ [ 0 - 9 a - f ] { 40 } $ / ;
33+ const INSTALL_PR_TITLE = 'ci: install Squad agentic workflows' ;
34+ const PROBE_PR_TITLE = 'test: add Squad bootstrap trigger probe' ;
35+ const CAST_BRANCH = 'squad/bootstrap-cast' ;
36+ const CAST_PR_TITLE = '[squad] Cast your Squad' ;
37+ const RESEARCH_ISSUE_TITLE = '[Research Proposals] Agent-discovered repo opportunities' ;
38+ const RESEARCH_MARKER = '<!-- squad:bootstrap-opportunities schema=1 -->' ;
3339const SAFE_CHILD_ENV = Object . freeze ( [
3440 'CI' ,
3541 'GH_CONFIG_DIR' ,
@@ -113,6 +119,53 @@ function ghJson(args, options = {}) {
113119 return output ? JSON . parse ( output ) : null ;
114120}
115121
122+ const githubAdapter = Object . freeze ( {
123+ getDefaultBranchSha ( target , branch ) {
124+ return ghJson ( [
125+ 'api' , `repos/${ target } /branches/${ branch } ` ,
126+ '--jq' , '{sha:.commit.sha}' ,
127+ ] ) . sha ;
128+ } ,
129+ getTree ( target , sha ) {
130+ return ghJson ( [
131+ 'api' , `repos/${ target } /git/trees/${ sha } ?recursive=1` ,
132+ '--jq' , '{truncated,paths:[.tree[] | select(.type == "blob") | .path]}' ,
133+ ] ) ;
134+ } ,
135+ listBranches ( target ) {
136+ return ghJson ( [
137+ 'api' , '--paginate' , '--slurp' , `repos/${ target } /branches?per_page=100` ,
138+ ] ) . flat ( ) . map ( ( { name } ) => ( { name } ) ) ;
139+ } ,
140+ listPullRequests ( target ) {
141+ return ghJson ( [
142+ 'api' , '--paginate' , '--slurp' , `repos/${ target } /pulls?state=all&per_page=100` ,
143+ ] ) . flat ( ) . map ( ( pullRequest ) => ( {
144+ number : pullRequest . number ,
145+ url : pullRequest . html_url ,
146+ title : pullRequest . title ,
147+ body : pullRequest . body ,
148+ createdAt : pullRequest . created_at ,
149+ isDraft : pullRequest . draft ,
150+ headRefName : pullRequest . head . ref ,
151+ baseRefName : pullRequest . base . ref ,
152+ state : pullRequest . state ,
153+ } ) ) ;
154+ } ,
155+ listIssues ( target ) {
156+ return ghJson ( [
157+ 'api' , '--paginate' , '--slurp' , `repos/${ target } /issues?state=all&per_page=100` ,
158+ ] ) . flat ( ) . filter ( ( issue ) => ! issue . pull_request ) . map ( ( issue ) => ( {
159+ number : issue . number ,
160+ url : issue . html_url ,
161+ title : issue . title ,
162+ body : issue . body ,
163+ createdAt : issue . created_at ,
164+ state : issue . state ,
165+ } ) ) ;
166+ } ,
167+ } ) ;
168+
116169function sourceGhJson ( args ) {
117170 const token = process . env . SOURCE_READ_TOKEN ?? process . env . GH_TOKEN ;
118171 if ( ! token ) throw new Error ( 'A source repository read token is required.' ) ;
@@ -196,7 +249,103 @@ function assertExactStagedDiff(cwd, expected) {
196249 if ( deleted ) throw new Error ( `Staged deletions are forbidden: ${ deleted } ` ) ;
197250}
198251
199- export function authorizeTarget ( args , sourceInfo ) {
252+ function packageArtifactCategories ( contract ) {
253+ return {
254+ workflowDestinations : contract . workflows . map ( ( { destination } ) => destination ) ,
255+ workflowLocks : contract . workflows . map ( ( { lock } ) => lock ) ,
256+ runtimePackageDestinations : contract . runtime . map ( ( { packageDestination } ) => packageDestination ) ,
257+ runtimeDestinations : contract . runtime . map ( ( { destination } ) => destination ) ,
258+ manifest : [ CONTRACT_DESTINATION ] ,
259+ ownership : [ OWNERSHIP_DESTINATION ] ,
260+ skill : contract . skills . map ( ( { destination } ) => destination ) ,
261+ triggerSentinel : [ TRIGGER_PROBE_DESTINATION ] ,
262+ } ;
263+ }
264+
265+ function matchingPriorRunArtifacts ( branches , pullRequests , issues ) {
266+ return {
267+ branches : branches . filter ( ( { name } ) => name . startsWith ( 'squad-e2e/install-' )
268+ || name . startsWith ( 'squad-e2e/probe-' )
269+ || name === CAST_BRANCH ) ,
270+ pullRequests : pullRequests . filter ( ( { headRefName, title } ) => (
271+ headRefName . startsWith ( 'squad-e2e/install-' )
272+ || headRefName . startsWith ( 'squad-e2e/probe-' )
273+ || headRefName === CAST_BRANCH
274+ || title === INSTALL_PR_TITLE
275+ || title === PROBE_PR_TITLE
276+ || title === CAST_PR_TITLE
277+ ) ) ,
278+ issues : issues . filter ( ( { title, body } ) => (
279+ title === RESEARCH_ISSUE_TITLE || ( body ?? '' ) . includes ( RESEARCH_MARKER )
280+ ) ) ,
281+ } ;
282+ }
283+
284+ export function assertPristineTarget ( target , defaultBranch , contract , github = githubAdapter , now = Date . now ) {
285+ if ( contract . workflows . length !== 7 || contract . runtime . length !== 15 || contract . skills . length !== 1 ) {
286+ throw new Error ( 'Trusted package topology must be exactly 7 workflows, 15 runtime resources, and 1 skill.' ) ;
287+ }
288+ const defaultBranchSha = github . getDefaultBranchSha ( target , defaultBranch ) ;
289+ assertSha ( defaultBranchSha , 'Target default-branch SHA' ) ;
290+ const tree = github . getTree ( target , defaultBranchSha ) ;
291+ if ( tree . truncated ) throw new Error ( 'Target default-branch tree response was truncated.' ) ;
292+ const paths = new Set ( tree . paths ) ;
293+ const categories = packageArtifactCategories ( contract ) ;
294+ const existingPaths = Object . entries ( categories ) . flatMap ( ( [ category , candidates ] ) => (
295+ candidates . filter ( ( path ) => paths . has ( path ) ) . map ( ( path ) => ( { category, path } ) )
296+ ) ) ;
297+ const packageNamespace = [ ...paths ] . filter ( ( path ) => path . startsWith ( '.github/aw/squad/' ) ) ;
298+ const branches = github . listBranches ( target ) ;
299+ const pullRequests = github . listPullRequests ( target ) ;
300+ const issues = github . listIssues ( target ) ;
301+ const prior = matchingPriorRunArtifacts ( branches , pullRequests , issues ) ;
302+ if ( existingPaths . length > 0 || packageNamespace . length > 0
303+ || prior . branches . length > 0 || prior . pullRequests . length > 0 || prior . issues . length > 0 ) {
304+ throw new Error ( `Target is not pristine: ${ JSON . stringify ( {
305+ packagePaths : existingPaths ,
306+ packageNamespace,
307+ priorRunBranches : prior . branches . map ( ( { name } ) => name ) ,
308+ priorRunPullRequests : prior . pullRequests . map ( ( { number } ) => number ) ,
309+ priorRunIssues : prior . issues . map ( ( { number } ) => number ) ,
310+ } ) } `) ;
311+ }
312+ return {
313+ capturedAt : new Date ( now ( ) ) . toISOString ( ) ,
314+ defaultBranch,
315+ defaultBranchSha,
316+ maximumPullRequestNumber : Math . max ( 0 , ...pullRequests . map ( ( { number } ) => number ) ) ,
317+ maximumIssueNumber : Math . max ( 0 , ...issues . map ( ( { number } ) => number ) ) ,
318+ } ;
319+ }
320+
321+ export function assertTargetDefaultBranch (
322+ target ,
323+ defaultBranch ,
324+ expectedSha ,
325+ phase ,
326+ github = githubAdapter ,
327+ ) {
328+ const actualSha = github . getDefaultBranchSha ( target , defaultBranch ) ;
329+ if ( actualSha !== expectedSha ) {
330+ throw new Error (
331+ `Target default branch moved before ${ phase } : expected ${ expectedSha } , found ${ actualSha } .` ,
332+ ) ;
333+ }
334+ }
335+
336+ export function guardedTargetMutation ( {
337+ target,
338+ defaultBranch,
339+ expectedSha,
340+ phase,
341+ mutate,
342+ github = githubAdapter ,
343+ } ) {
344+ assertTargetDefaultBranch ( target , defaultBranch , expectedSha , phase , github ) ;
345+ return mutate ( ) ;
346+ }
347+
348+ export function authorizeTarget ( args , sourceInfo , contract , github = githubAdapter ) {
200349 const target = requireArg ( args , 'target' ) ;
201350 const expectedRepositoryId = Number ( requireArg ( args , 'target_repository_id' ) ) ;
202351 const expectedOwnerId = Number ( requireArg ( args , 'target_owner_id' ) ) ;
@@ -233,7 +382,8 @@ export function authorizeTarget(args, sourceInfo) {
233382 || workflowPermissions . can_approve_pull_request_reviews !== true ) {
234383 throw new Error ( 'Target Actions permissions do not match the supported secure configuration.' ) ;
235384 }
236- return { target, info, workflowPermissions, actor } ;
385+ const baseline = assertPristineTarget ( target , info . default_branch , contract , github ) ;
386+ return { target, info, workflowPermissions, actor, baseline } ;
237387}
238388
239389function waitForBootstrapRun ( target , headSha , startedAt , evidence ) {
@@ -258,45 +408,122 @@ function waitForBootstrapRun(target, headSha, startedAt, evidence) {
258408 throw new Error ( `Timed out waiting for bootstrap run at ${ headSha } .` ) ;
259409}
260410
261- function bootstrapOutputs ( target ) {
262- const castPrs = ghJson ( [
263- 'pr' , 'list' , '--repo' , target , '--state' , 'all' , '--head' , 'squad/bootstrap-cast' ,
264- '--json' , 'number,url,isDraft,title,headRefName,baseRefName,state' ,
265- ] ) ;
266- const issues = ghJson ( [
267- 'issue' , 'list' , '--repo' , target , '--state' , 'all' , '--limit' , '100' ,
268- '--search' , '"[Research Proposals] Agent-discovered repo opportunities" in:title' ,
269- '--json' , 'number,url,title,body,state' ,
270- ] ) . filter ( ( issue ) => issue . title === '[Research Proposals] Agent-discovered repo opportunities'
271- && issue . body . includes ( '<!-- squad:bootstrap-opportunities schema=1 -->' ) ) ;
272- return { castPrs, issues } ;
411+ function bootstrapOutputs ( target , github = githubAdapter ) {
412+ return {
413+ castPrs : github . listPullRequests ( target ) ,
414+ issues : github . listIssues ( target ) ,
415+ } ;
273416}
274417
275- function waitForBootstrapOutputs ( target ) {
276- const deadline = Date . now ( ) + 5 * 60 * 1000 ;
277- while ( Date . now ( ) < deadline ) {
278- const outputs = bootstrapOutputs ( target ) ;
279- if ( outputs . castPrs . length === 1 && outputs . castPrs [ 0 ] . isDraft && outputs . issues . length === 1 ) {
280- return { castPr : outputs . castPrs [ 0 ] , researchIssue : outputs . issues [ 0 ] } ;
281- }
282- sleep ( 10_000 ) ;
418+ export function selectBootstrapOutputs ( outputs , baseline , installation , bootstrapRun ) {
419+ const cutoff = Math . max (
420+ Date . parse ( baseline . capturedAt ) ,
421+ Date . parse ( installation . mergedAt ) ,
422+ Date . parse ( bootstrapRun . createdAt ) ,
423+ ) ;
424+ const castPrs = outputs . castPrs . filter ( ( pr ) => (
425+ pr . number > baseline . maximumPullRequestNumber
426+ && Date . parse ( pr . createdAt ) >= cutoff
427+ && pr . headRefName === CAST_BRANCH
428+ && pr . baseRefName === baseline . defaultBranch
429+ && pr . title === CAST_PR_TITLE
430+ && pr . isDraft === true
431+ ) ) ;
432+ const issues = outputs . issues . filter ( ( issue ) => (
433+ issue . number > baseline . maximumIssueNumber
434+ && Date . parse ( issue . createdAt ) >= cutoff
435+ && issue . title === RESEARCH_ISSUE_TITLE
436+ && issue . body ?. startsWith ( RESEARCH_MARKER )
437+ ) ) ;
438+ if ( castPrs . length > 1 || issues . length > 1 ) {
439+ throw new Error (
440+ `Ambiguous current bootstrap outputs: ${ castPrs . length } Cast PRs and ${ issues . length } research issues.` ,
441+ ) ;
442+ }
443+ if ( castPrs . length === 1 && issues . length === 1 ) {
444+ return { castPr : castPrs [ 0 ] , researchIssue : issues [ 0 ] } ;
445+ }
446+ return null ;
447+ }
448+
449+ export function waitForBootstrapOutputs (
450+ target ,
451+ baseline ,
452+ installation ,
453+ bootstrapRun ,
454+ {
455+ github = githubAdapter ,
456+ now = Date . now ,
457+ pause = sleep ,
458+ timeoutMs = 5 * 60 * 1000 ,
459+ pollMs = 10_000 ,
460+ } = { } ,
461+ ) {
462+ const deadline = now ( ) + timeoutMs ;
463+ while ( now ( ) < deadline ) {
464+ const selected = selectBootstrapOutputs (
465+ bootstrapOutputs ( target , github ) ,
466+ baseline ,
467+ installation ,
468+ bootstrapRun ,
469+ ) ;
470+ if ( selected ) return selected ;
471+ pause ( pollMs ) ;
283472 }
284473 throw new Error ( 'Timed out waiting for the draft Cast PR and bootstrap research issue.' ) ;
285474}
286475
287- function createAndMergePr ( { cwd, target, defaultBranch, branch, title, body, evidence } ) {
476+ function createAndMergePr ( {
477+ cwd,
478+ target,
479+ defaultBranch,
480+ expectedBaseSha,
481+ branch,
482+ title,
483+ body,
484+ evidence,
485+ github = githubAdapter ,
486+ } ) {
288487 privileged ( 'gh' , [ 'auth' , 'setup-git' ] ) ;
289- privileged ( 'git' , [ 'push' , '--set-upstream' , 'origin' , branch ] , { cwd, capture : false } ) ;
290- const prUrl = privileged ( 'gh' , [
291- 'pr' , 'create' , '--repo' , target , '--base' , defaultBranch , '--head' , branch ,
292- '--title' , title , '--body' , body ,
293- ] , { cwd } ) ;
488+ guardedTargetMutation ( {
489+ target,
490+ defaultBranch,
491+ expectedSha : expectedBaseSha ,
492+ phase : `${ title } push` ,
493+ github,
494+ mutate : ( ) => privileged ( 'git' , [ 'push' , '--set-upstream' , 'origin' , branch ] , {
495+ cwd,
496+ capture : false ,
497+ } ) ,
498+ } ) ;
499+ const prUrl = guardedTargetMutation ( {
500+ target,
501+ defaultBranch,
502+ expectedSha : expectedBaseSha ,
503+ phase : `${ title } PR creation` ,
504+ github,
505+ mutate : ( ) => privileged ( 'gh' , [
506+ 'pr' , 'create' , '--repo' , target , '--base' , defaultBranch , '--head' , branch ,
507+ '--title' , title , '--body' , body ,
508+ ] , { cwd } ) ,
509+ } ) ;
294510 const pr = ghJson ( [ 'pr' , 'view' , prUrl , '--repo' , target , '--json' , 'number,url,state,headRefName,baseRefName' ] ) ;
295511 writeJson ( resolve ( evidence , `pr-${ pr . number } -created.json` ) , pr ) ;
296- privileged ( 'gh' , [ 'pr' , 'merge' , String ( pr . number ) , '--repo' , target , '--merge' , '--delete-branch' ] , {
297- cwd,
298- capture : false ,
299- timeout : 300_000 ,
512+ guardedTargetMutation ( {
513+ target,
514+ defaultBranch,
515+ expectedSha : expectedBaseSha ,
516+ phase : `${ title } merge` ,
517+ github,
518+ mutate : ( ) => privileged (
519+ 'gh' ,
520+ [ 'pr' , 'merge' , String ( pr . number ) , '--repo' , target , '--merge' , '--delete-branch' ] ,
521+ {
522+ cwd,
523+ capture : false ,
524+ timeout : 300_000 ,
525+ } ,
526+ ) ,
300527 } ) ;
301528 const merged = ghJson ( [
302529 'pr' , 'view' , String ( pr . number ) , '--repo' , target ,
@@ -328,7 +555,7 @@ function hosted(args, repositoryRoot) {
328555 if ( contract . workflows . length !== 7 || contract . runtime . length !== 15 || contract . skills . length !== 1 ) {
329556 throw new Error ( 'Trusted package topology must be exactly 7 workflows, 15 runtime resources, and 1 skill.' ) ;
330557 }
331- const targetState = authorizeTarget ( args , sourceInfo ) ;
558+ const targetState = authorizeTarget ( args , sourceInfo , contract ) ;
332559 mkdirSync ( evidence , { recursive : true } ) ;
333560 writeJson ( resolve ( evidence , 'preflight.json' ) , targetState ) ;
334561
@@ -341,6 +568,12 @@ function hosted(args, repositoryRoot) {
341568 capture : false ,
342569 timeout : 300_000 ,
343570 } ) ;
571+ const clonedHead = runChild ( 'git' , [ 'rev-parse' , 'HEAD' ] , { cwd : checkout } ) ;
572+ if ( clonedHead !== targetState . baseline . defaultBranchSha ) {
573+ throw new Error (
574+ `Cloned target HEAD does not match pristine baseline: expected ${ targetState . baseline . defaultBranchSha } , found ${ clonedHead } .` ,
575+ ) ;
576+ }
344577 runChild ( 'git' , [ 'config' , 'user.name' , 'Squad hosted E2E' ] , { cwd : checkout } ) ;
345578 runChild ( 'git' , [ 'config' , 'user.email' , 'github-actions[bot]@users.noreply.github.com' ] , { cwd : checkout } ) ;
346579 const installBranch = `squad-e2e/install-${ process . env . GITHUB_RUN_ID ?? sourceSha . slice ( 0 , 12 ) } ` ;
@@ -384,8 +617,9 @@ function hosted(args, repositoryRoot) {
384617 cwd : checkout ,
385618 target : targetState . target ,
386619 defaultBranch : targetState . info . default_branch ,
620+ expectedBaseSha : targetState . baseline . defaultBranchSha ,
387621 branch : installBranch ,
388- title : 'ci: install Squad agentic workflows' ,
622+ title : INSTALL_PR_TITLE ,
389623 body : 'One-shot hosted E2E installation. Generated Squad work remains human-reviewed and is not merged.' ,
390624 evidence,
391625 } ) ;
@@ -396,7 +630,17 @@ function hosted(args, repositoryRoot) {
396630 installStartedAt ,
397631 evidence ,
398632 ) ;
399- const outputs = waitForBootstrapOutputs ( targetState . target ) ;
633+ writeJson ( resolve ( evidence , 'installation-identity.json' ) , {
634+ baseline : targetState . baseline ,
635+ installation,
636+ bootstrapRun : installRun ,
637+ } ) ;
638+ const outputs = waitForBootstrapOutputs (
639+ targetState . target ,
640+ targetState . baseline ,
641+ installation ,
642+ installRun ,
643+ ) ;
400644
401645 runChild ( 'git' , [ 'fetch' , 'origin' , targetState . info . default_branch ] , { cwd : checkout } ) ;
402646 const probeBranch = `squad-e2e/probe-${ process . env . GITHUB_RUN_ID ?? sourceSha . slice ( 0 , 12 ) } ` ;
@@ -421,8 +665,9 @@ function hosted(args, repositoryRoot) {
421665 cwd : checkout ,
422666 target : targetState . target ,
423667 defaultBranch : targetState . info . default_branch ,
668+ expectedBaseSha : installation . mergeCommit . oid ,
424669 branch : probeBranch ,
425- title : 'test: add Squad bootstrap trigger probe' ,
670+ title : PROBE_PR_TITLE ,
426671 body : 'One-shot non-executable sentinel proving the bootstrap path trigger.' ,
427672 evidence,
428673 } ) ;
0 commit comments