@@ -41,8 +41,8 @@ permissions:
4141
4242jobs :
4343 # Decide whether this is a fork PR that needs e2e, resolve its PR number and
44- # head SHA, and open the pending "E2E Tests" check-run so the PR is not
45- # mergeable until we conclude it. Every decision is derived from data the fork
44+ # head SHA, and post the pending "E2E Tests" commit status so the PR is not
45+ # mergeable until `report` concludes it. Every decision is derived from data the fork
4646 # cannot forge — the workflow_run conclusion, the base-repo PR list, and the
4747 # base-repo PR file list — NOT from the presence/absence of an artifact the
4848 # fork's own build controls. Fail-closed if the fork build failed or the
@@ -60,23 +60,24 @@ jobs:
6060 contents : read
6161 actions : read
6262 pull-requests : read
63+ statuses : write
6364 outputs :
6465 should_run : ${{ steps.r.outputs.should_run }}
6566 pr_number : ${{ steps.r.outputs.pr_number }}
6667 head_sha : ${{ steps.r.outputs.head_sha }}
6768 run_id : ${{ steps.r.outputs.run_id }}
68- check_run_id : ${{ steps.open.outputs.check_run_id }}
6969 # TIA inputs derived from TRUSTED base-repo data (the PR file list and
7070 # labels), NOT the fork-controlled merge-ref diff — so suite selection
7171 # keeps the same anti-forgery property as the docs-only decision (#3257).
7272 changed_b64 : ${{ steps.r.outputs.changed_b64 }}
7373 full_e2e : ${{ steps.r.outputs.full_e2e }}
7474 steps :
75- # Decide the verdict using this step's default (workflow_run) token, which
76- # already has the actions:read / pull-requests:read scopes these lookups
77- # need. It creates NO check-run — the next step publishes the verdict as
78- # the COZYSTACK_CI app so the app token stays scoped to checks:write only.
79- - name : Resolve fork PR and decide verdict
75+ # Decide the verdict and post the "E2E Tests" commit status directly, with
76+ # this job's default (workflow_run) token — it already has the actions:read
77+ # / pull-requests:read scopes the lookups need, plus statuses:write from the
78+ # job permissions. A commit status (not a check-run) is the merge gate; see
79+ # pull-requests.yaml's e2e-report and docs/agents/e2e-testing.md §10.
80+ - name : Resolve fork PR and post "E2E Tests" status
8081 id : r
8182 uses : actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
8283 env :
@@ -87,26 +88,29 @@ jobs:
8788 script : |
8889 const { HEAD_SHA, RUN_ID, CONCLUSION } = process.env;
8990 const { owner, repo } = context.repo;
90- // Validate the SHA before it flows into a check-run / image tag.
91+ // Validate the SHA before it flows into a status target / image tag.
9192 if (!/^[0-9a-f]{40}$/.test(HEAD_SHA)) {
9293 core.setFailed(`unexpected head_sha '${HEAD_SHA}'`);
9394 return;
9495 }
9596 core.setOutput('head_sha', HEAD_SHA);
9697 core.setOutput('run_id', String(RUN_ID));
9798
98- // Record a terminal verdict for the `open` step to publish as a
99- // completed check-run. This step never creates a check itself.
100- const decide = (conclusion, summary) => {
101- core.setOutput('verdict_status', 'completed');
102- core.setOutput('verdict_conclusion', conclusion);
103- core.setOutput('verdict_summary', summary);
99+ const target_url = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
100+ const setStatus = (state, description) => github.rest.repos.createCommitStatus({
101+ owner, repo, sha: HEAD_SHA, state, context: 'E2E Tests', description, target_url,
102+ });
103+ // Post a TERMINAL status (docs-only success / fail-closed failure) and
104+ // stop — no privileged run needed. `report` never runs for these
105+ // (guarded on should_run).
106+ const decide = async (state, description) => {
107+ await setStatus(state, description);
104108 core.setOutput('should_run', 'false');
105109 };
106110
107- // Fork build did not succeed → fail the required check (fail-closed).
111+ // Fork build did not succeed → fail the required status (fail-closed).
108112 if (CONCLUSION !== 'success') {
109- decide('failure', `Pull Request workflow concluded '${CONCLUSION}'; e2e not run`);
113+ await decide('failure', `Pull Request workflow concluded '${CONCLUSION}'; e2e not run`);
110114 return;
111115 }
112116
@@ -121,7 +125,7 @@ jobs:
121125 const pr = assoc.data.find(p => p.state === 'open');
122126 const prNumber = pr ? String(pr.number) : '';
123127 if (!/^[0-9]+$/.test(prNumber)) {
124- decide('failure', `no open PR for ${HEAD_SHA}`);
128+ await decide('failure', `no open PR for ${HEAD_SHA}`);
125129 return;
126130 }
127131 core.setOutput('pr_number', prNumber);
@@ -136,7 +140,7 @@ jobs:
136140 });
137141 const hasNonDocs = files.some(f => !f.filename.startsWith('docs/'));
138142 if (!hasNonDocs) {
139- decide('success', 'docs-only PR (verified via PR file list) — e2e not required');
143+ await decide('success', 'docs-only PR (verified via PR file list) — e2e not required');
140144 return;
141145 }
142146
@@ -148,7 +152,7 @@ jobs:
148152 { owner, repo, run_id: RUN_ID, per_page: 100 },
149153 );
150154 if (!arts.some(a => a.name === 'pr-patch')) {
151- decide('failure', 'expected pr-patch artifact is missing — the fork build did not complete');
155+ await decide('failure', 'expected pr-patch artifact is missing — the fork build did not complete');
152156 return;
153157 }
154158
@@ -165,51 +169,11 @@ jobs:
165169 const fullE2e = (pr.labels || []).some(l => l.name === 'full-e2e');
166170 core.setOutput('full_e2e', fullE2e ? 'true' : 'false');
167171
168- // Real run: the `open` step opens an in-progress check-run; `report`
169- // concludes it.
170- core.setOutput('verdict_status', 'in_progress');
171- core.setOutput('verdict_summary', 'Running e2e in the privileged fork workflow…');
172+ // Real run: post a pending status now; `report` flips it to the
173+ // terminal state after publish + e2e.
174+ await setStatus('pending', 'Running e2e in the privileged fork workflow…');
172175 core.setOutput('should_run', 'true');
173176
174- # Skipped when `resolve` set-failed on a bad SHA (fail-closed: no check →
175- # "E2E Tests" stays unreported → PR not mergeable).
176- - name : Generate GitHub App token
177- id : app-token
178- uses : actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
179- with :
180- app-id : ${{ secrets.COZYSTACK_CI_APP_ID }}
181- private-key : ${{ secrets.COZYSTACK_CI_PRIVATE_KEY }}
182- owner : cozystack
183- repositories : ${{ github.event.repository.name }}
184- permission-checks : write
185- # Publish the verdict as the COZYSTACK_CI app so the check-run lands in the
186- # app's own check-suite — matching the same-repo e2e-report path and the
187- # branch-protection pin (see pull-requests.yaml e2e-report comment and
188- # docs/agents/e2e-testing.md §10). Creating it with the default token would
189- # file it under an arbitrary GitHub Actions suite (a labeler).
190- - name : Open "E2E Tests" check-run (as app)
191- id : open
192- uses : actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
193- env :
194- STATUS : ${{ steps.r.outputs.verdict_status }}
195- CONCLUSION : ${{ steps.r.outputs.verdict_conclusion }}
196- SUMMARY : ${{ steps.r.outputs.verdict_summary }}
197- HEAD_SHA : ${{ steps.r.outputs.head_sha }}
198- with :
199- github-token : ${{ steps.app-token.outputs.token }}
200- script : |
201- const { STATUS, CONCLUSION, SUMMARY, HEAD_SHA } = process.env;
202- const { owner, repo } = context.repo;
203- const params = {
204- owner, repo, name: 'E2E Tests', head_sha: HEAD_SHA,
205- status: STATUS,
206- output: { title: 'E2E Tests', summary: SUMMARY },
207- };
208- if (STATUS === 'completed') params.conclusion = CONCLUSION;
209- const cr = await github.rest.checks.create(params);
210- // Only the real (in-progress) run is handed to `report` to conclude.
211- if (STATUS === 'in_progress') core.setOutput('check_run_id', String(cr.data.id));
212-
213177 # Push the fork-built image archives to OCIR by digest and flux-push the
214178 # digest-pinned packages tree. Only trusted binaries touch the fork's content
215179 # while the OCIR token is live — no `make`/fork script runs here.
@@ -549,62 +513,42 @@ jobs:
549513 if : always()
550514 run : rm -rf "/tmp/$SANDBOX_NAME"
551515
552- # Conclude the required "E2E Tests" check-run opened by `resolve`. Always runs
553- # (so a failed publish/e2e still turns the check red rather than leaving it
554- # stuck in-progress ). success only when the e2e job passed.
516+ # Flip the pending "E2E Tests" commit status posted by `resolve` to its
517+ # terminal state. Always runs (so a failed publish/e2e turns the status red
518+ # rather than leaving it stuck pending ). success only when the e2e job passed.
555519 report :
556520 name : Report fork E2E status
557521 needs : [resolve, publish, e2e]
558522 if : ${{ always() && needs.resolve.outputs.should_run == 'true' }}
559523 runs-on : ubuntu-latest
560524 permissions :
561525 contents : read
526+ statuses : write
562527 steps :
563- # The pending check-run was opened by the COZYSTACK_CI app in `resolve`;
564- # checks.update must run as the same app, so mint its token here too.
565- - name : Generate GitHub App token
566- id : app-token
567- uses : actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
568- with :
569- app-id : ${{ secrets.COZYSTACK_CI_APP_ID }}
570- private-key : ${{ secrets.COZYSTACK_CI_PRIVATE_KEY }}
571- owner : cozystack
572- repositories : ${{ github.event.repository.name }}
573- permission-checks : write
574- - name : Conclude "E2E Tests" check-run
528+ - name : Report "E2E Tests" commit status
575529 uses : actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
576530 env :
577- CHECK_RUN_ID : ${{ needs.resolve.outputs.check_run_id }}
578531 PUBLISH_RESULT : ${{ needs.publish.result }}
579532 E2E_RESULT : ${{ needs.e2e.result }}
580533 HEAD_SHA : ${{ needs.resolve.outputs.head_sha }}
534+ RUN_URL : ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
581535 with :
582- github-token : ${{ steps.app-token.outputs.token }}
583536 script : |
584- const { CHECK_RUN_ID, PUBLISH_RESULT, E2E_RESULT, HEAD_SHA } = process.env;
537+ const { PUBLISH_RESULT, E2E_RESULT, HEAD_SHA, RUN_URL } = process.env;
585538 const { owner, repo } = context.repo;
586- let conclusion, summary ;
539+ let state, description ;
587540 if (E2E_RESULT === 'success') {
588- conclusion = 'success';
589- summary = 'fork e2e passed (privileged run)';
541+ state = 'success';
542+ description = 'fork e2e passed (privileged run)';
590543 } else if (PUBLISH_RESULT !== 'success') {
591- conclusion = 'failure';
592- summary = `image/artifact publish did not succeed (result: ${PUBLISH_RESULT})`;
544+ state = 'failure';
545+ description = `image/artifact publish did not succeed (result: ${PUBLISH_RESULT})`;
593546 } else {
594- conclusion = 'failure';
595- summary = `fork e2e did not pass (result: ${E2E_RESULT})`;
547+ state = 'failure';
548+ description = `fork e2e did not pass (result: ${E2E_RESULT})`;
596549 }
597- if (CHECK_RUN_ID) {
598- await github.rest.checks.update({
599- owner, repo, check_run_id: Number(CHECK_RUN_ID),
600- status: 'completed', conclusion,
601- output: { title: 'E2E Tests', summary },
602- });
603- } else {
604- await github.rest.checks.create({
605- owner, repo, name: 'E2E Tests', head_sha: HEAD_SHA,
606- status: 'completed', conclusion,
607- output: { title: 'E2E Tests', summary },
608- });
609- }
610- core.info(`E2E Tests → ${conclusion}: ${summary}`);
550+ await github.rest.repos.createCommitStatus({
551+ owner, repo, sha: HEAD_SHA, state, context: 'E2E Tests',
552+ description, target_url: RUN_URL,
553+ });
554+ core.info(`E2E Tests → ${state}: ${description}`);
0 commit comments