@@ -36,6 +36,8 @@ const CAST_BRANCH = 'squad/bootstrap-cast';
3636const CAST_PR_TITLE = '[squad] Cast your Squad' ;
3737const RESEARCH_ISSUE_TITLE = '[Research Proposals] Agent-discovered repo opportunities' ;
3838const RESEARCH_MARKER = '<!-- squad:bootstrap-opportunities schema=1 -->' ;
39+ const PROVENANCE_MARKER_PATTERN = / ^ < ! - - s q u a d : b o o t s t r a p - p r o v e n a n c e ( \{ [ ^ \r \n ] + \} ) - - > $ / gm;
40+ const ACTIONS_BOT_LOGIN = 'github-actions[bot]' ;
3941const SAFE_CHILD_ENV = Object . freeze ( [
4042 'CI' ,
4143 'GH_CONFIG_DIR' ,
@@ -135,7 +137,7 @@ const githubAdapter = Object.freeze({
135137 listBranches ( target ) {
136138 return ghJson ( [
137139 'api' , '--paginate' , '--slurp' , `repos/${ target } /branches?per_page=100` ,
138- ] ) . flat ( ) . map ( ( { name } ) => ( { name } ) ) ;
140+ ] ) . flat ( ) . map ( ( branch ) => ( { name : branch . name , sha : branch . commit . sha } ) ) ;
139141 } ,
140142 listPullRequests ( target ) {
141143 return ghJson ( [
@@ -148,7 +150,15 @@ const githubAdapter = Object.freeze({
148150 createdAt : pullRequest . created_at ,
149151 isDraft : pullRequest . draft ,
150152 headRefName : pullRequest . head . ref ,
153+ headRepository : pullRequest . head . repo ?. full_name ,
154+ headSha : pullRequest . head . sha ,
151155 baseRefName : pullRequest . base . ref ,
156+ baseSha : pullRequest . base . sha ,
157+ author : {
158+ login : pullRequest . user . login ,
159+ id : pullRequest . user . id ,
160+ type : pullRequest . user . type ,
161+ } ,
152162 state : pullRequest . state ,
153163 } ) ) ;
154164 } ,
@@ -161,9 +171,20 @@ const githubAdapter = Object.freeze({
161171 title : issue . title ,
162172 body : issue . body ,
163173 createdAt : issue . created_at ,
174+ author : {
175+ login : issue . user . login ,
176+ id : issue . user . id ,
177+ type : issue . user . type ,
178+ } ,
164179 state : issue . state ,
165180 } ) ) ;
166181 } ,
182+ getUser ( login ) {
183+ return ghJson ( [
184+ 'api' , `users/${ login } ` ,
185+ '--jq' , '{login,id,type}' ,
186+ ] ) ;
187+ } ,
167188} ) ;
168189
169190function sourceGhJson ( args ) {
@@ -409,12 +430,55 @@ function waitForBootstrapRun(target, headSha, startedAt, evidence) {
409430}
410431
411432function bootstrapOutputs ( target , github = githubAdapter ) {
433+ const expectedAuthor = github . getUser ( ACTIONS_BOT_LOGIN ) ;
434+ if ( expectedAuthor . login !== ACTIONS_BOT_LOGIN
435+ || expectedAuthor . type !== 'Bot'
436+ || ! Number . isSafeInteger ( expectedAuthor . id ) ) {
437+ throw new Error ( 'GitHub Actions bot identity is unavailable or invalid.' ) ;
438+ }
439+ const castBranch = github . listBranches ( target ) . find ( ( { name } ) => name === CAST_BRANCH ) ;
412440 return {
441+ target,
442+ expectedAuthor,
443+ castBranchSha : castBranch ?. sha ,
413444 castPrs : github . listPullRequests ( target ) ,
414445 issues : github . listIssues ( target ) ,
415446 } ;
416447}
417448
449+ function parseBootstrapProvenance ( body , label ) {
450+ const matches = [ ...String ( body ?? '' ) . matchAll ( PROVENANCE_MARKER_PATTERN ) ] ;
451+ if ( matches . length !== 1 ) {
452+ throw new Error ( `${ label } must contain exactly one bootstrap provenance marker.` ) ;
453+ }
454+ let marker ;
455+ try {
456+ marker = JSON . parse ( matches [ 0 ] [ 1 ] ) ;
457+ } catch {
458+ throw new Error ( `${ label } bootstrap provenance marker is malformed.` ) ;
459+ }
460+ const keys = Object . keys ( marker ) . sort ( ) ;
461+ const expectedKeys = [ 'cast_sha' , 'install_sha' , 'repository' , 'run_id' , 'schema' ] ;
462+ if ( JSON . stringify ( keys ) !== JSON . stringify ( expectedKeys )
463+ || marker . schema !== 1
464+ || typeof marker . repository !== 'string'
465+ || typeof marker . run_id !== 'string'
466+ || typeof marker . install_sha !== 'string'
467+ || typeof marker . cast_sha !== 'string'
468+ || ! SHA_PATTERN . test ( marker . install_sha )
469+ || ! SHA_PATTERN . test ( marker . cast_sha )
470+ || ! / ^ [ 1 - 9 ] [ 0 - 9 ] * $ / . test ( marker . run_id ) ) {
471+ throw new Error ( `${ label } bootstrap provenance marker is malformed.` ) ;
472+ }
473+ return marker ;
474+ }
475+
476+ function sameAuthor ( actual , expected ) {
477+ return actual ?. login === expected . login
478+ && actual ?. id === expected . id
479+ && actual ?. type === expected . type ;
480+ }
481+
418482export function selectBootstrapOutputs ( outputs , baseline , installation , bootstrapRun ) {
419483 const cutoff = Math . max (
420484 Date . parse ( baseline . capturedAt ) ,
@@ -424,24 +488,51 @@ export function selectBootstrapOutputs(outputs, baseline, installation, bootstra
424488 const castPrs = outputs . castPrs . filter ( ( pr ) => (
425489 pr . number > baseline . maximumPullRequestNumber
426490 && Date . parse ( pr . createdAt ) >= cutoff
427- && pr . headRefName === CAST_BRANCH
428- && pr . baseRefName === baseline . defaultBranch
429491 && pr . title === CAST_PR_TITLE
430- && pr . isDraft === true
431492 ) ) ;
432493 const issues = outputs . issues . filter ( ( issue ) => (
433494 issue . number > baseline . maximumIssueNumber
434495 && Date . parse ( issue . createdAt ) >= cutoff
435496 && issue . title === RESEARCH_ISSUE_TITLE
436- && issue . body ?. startsWith ( RESEARCH_MARKER )
437497 ) ) ;
438498 if ( castPrs . length > 1 || issues . length > 1 ) {
439499 throw new Error (
440500 `Ambiguous current bootstrap outputs: ${ castPrs . length } Cast PRs and ${ issues . length } research issues.` ,
441501 ) ;
442502 }
443503 if ( castPrs . length === 1 && issues . length === 1 ) {
444- return { castPr : castPrs [ 0 ] , researchIssue : issues [ 0 ] } ;
504+ const [ castPr ] = castPrs ;
505+ const [ researchIssue ] = issues ;
506+ const castMarker = parseBootstrapProvenance ( castPr . body , 'Cast PR' ) ;
507+ const issueMarker = parseBootstrapProvenance ( researchIssue . body , 'Research issue' ) ;
508+ const expectedMarker = {
509+ schema : 1 ,
510+ repository : outputs . target ,
511+ run_id : String ( bootstrapRun . databaseId ) ,
512+ install_sha : installation . mergeCommit . oid ,
513+ cast_sha : castPr . headSha ,
514+ } ;
515+ if ( JSON . stringify ( castMarker ) !== JSON . stringify ( expectedMarker )
516+ || JSON . stringify ( issueMarker ) !== JSON . stringify ( expectedMarker ) ) {
517+ throw new Error ( 'Bootstrap outputs do not share the expected current-run provenance.' ) ;
518+ }
519+ if ( ! researchIssue . body . startsWith ( `${ RESEARCH_MARKER } \n` ) ) {
520+ throw new Error ( 'Research issue is missing the canonical bootstrap marker.' ) ;
521+ }
522+ if ( ! sameAuthor ( castPr . author , outputs . expectedAuthor )
523+ || ! sameAuthor ( researchIssue . author , outputs . expectedAuthor ) ) {
524+ throw new Error ( 'Bootstrap outputs were not authored by the expected GitHub Actions bot.' ) ;
525+ }
526+ if ( castPr . headRepository !== outputs . target
527+ || castPr . headRefName !== CAST_BRANCH
528+ || castPr . baseRefName !== baseline . defaultBranch
529+ || castPr . baseSha !== installation . mergeCommit . oid
530+ || castPr . headSha !== outputs . castBranchSha
531+ || ! SHA_PATTERN . test ( castPr . headSha )
532+ || castPr . isDraft !== true ) {
533+ throw new Error ( 'Bootstrap Cast PR repository, branch, SHA, base, or draft state is invalid.' ) ;
534+ }
535+ return { castPr, researchIssue } ;
445536 }
446537 return null ;
447538}
0 commit comments