mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-09 14:02:56 +09:00
fix(ci): publish "E2E Tests" check-run as the COZYSTACK_CI app
The synthetic "E2E Tests" required check-run was created with the default GITHUB_TOKEN (the "GitHub Actions" app). A token-created check-run has no owning job, so GitHub files it under an arbitrary GitHub Actions check-suite for the head SHA — in practice the earliest one, the PR-size labeler — so "E2E Tests" rendered under "PR size label". Publish it as the COZYSTACK_CI GitHub App instead (create-github-app-token, scoped to permission-checks: write) so the check lands in the app own dedicated suite. This is also stricter: only the CI app can satisfy the gate. - pull-requests.yaml (e2e-report): mint the app token, pass it to checks.create. - e2e-fork.yaml: split resolve into a verdict decision on the default workflow_run token (keeps the actions:read / pull-requests:read lookups) and an "open" step that publishes the check as the app; report re-mints the token so checks.update runs as the same app that opened the pending run. - docs/agents/e2e-testing.md: document the change and the branch-protection requirement. Requires main branch protection to require "E2E Tests" from the COZYSTACK_CI app id (not app_id 15368). Assisted-By: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
This commit is contained in:
1 parent
06d734cf53
commit
9cddb532bf
2 files changed
+94
-27
No files matched your search
@@ -58,7 +58,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write
|
||||
actions: read
|
||||
pull-requests: read
|
||||
outputs:
|
||||
@@ -66,9 +65,13 @@ jobs:
|
||||
pr_number: ${{ steps.r.outputs.pr_number }}
|
||||
head_sha: ${{ steps.r.outputs.head_sha }}
|
||||
run_id: ${{ steps.r.outputs.run_id }}
|
||||
check_run_id: ${{ steps.r.outputs.check_run_id }}
|
||||
check_run_id: ${{ steps.open.outputs.check_run_id }}
|
||||
steps:
|
||||
- name: Resolve and open check-run
|
||||
# Decide the verdict using this step's default (workflow_run) token, which
|
||||
# already has the actions:read / pull-requests:read scopes these lookups
|
||||
# need. It creates NO check-run — the next step publishes the verdict as
|
||||
# the COZYSTACK_CI app so the app token stays scoped to checks:write only.
|
||||
- name: Resolve fork PR and decide verdict
|
||||
id: r
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
|
||||
env:
|
||||
@@ -87,19 +90,18 @@ jobs:
|
||||
core.setOutput('head_sha', HEAD_SHA);
|
||||
core.setOutput('run_id', String(RUN_ID));
|
||||
|
||||
const conclude = async (conclusion, summary) => {
|
||||
const cr = await github.rest.checks.create({
|
||||
owner, repo, name: 'E2E Tests', head_sha: HEAD_SHA,
|
||||
status: 'completed', conclusion,
|
||||
output: { title: 'E2E Tests', summary },
|
||||
});
|
||||
return cr.data.id;
|
||||
// Record a terminal verdict for the `open` step to publish as a
|
||||
// completed check-run. This step never creates a check itself.
|
||||
const decide = (conclusion, summary) => {
|
||||
core.setOutput('verdict_status', 'completed');
|
||||
core.setOutput('verdict_conclusion', conclusion);
|
||||
core.setOutput('verdict_summary', summary);
|
||||
core.setOutput('should_run', 'false');
|
||||
};
|
||||
|
||||
// Fork build did not succeed → fail the required check (fail-closed).
|
||||
if (CONCLUSION !== 'success') {
|
||||
await conclude('failure', `Pull Request workflow concluded '${CONCLUSION}'; e2e not run`);
|
||||
core.setOutput('should_run', 'false');
|
||||
decide('failure', `Pull Request workflow concluded '${CONCLUSION}'; e2e not run`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -114,8 +116,7 @@ jobs:
|
||||
const pr = assoc.data.find(p => p.state === 'open');
|
||||
const prNumber = pr ? String(pr.number) : '';
|
||||
if (!/^[0-9]+$/.test(prNumber)) {
|
||||
await conclude('failure', `no open PR for ${HEAD_SHA}`);
|
||||
core.setOutput('should_run', 'false');
|
||||
decide('failure', `no open PR for ${HEAD_SHA}`);
|
||||
return;
|
||||
}
|
||||
core.setOutput('pr_number', prNumber);
|
||||
@@ -130,8 +131,7 @@ jobs:
|
||||
});
|
||||
const hasNonDocs = files.some(f => !f.filename.startsWith('docs/'));
|
||||
if (!hasNonDocs) {
|
||||
await conclude('success', 'docs-only PR (verified via PR file list) — e2e not required');
|
||||
core.setOutput('should_run', 'false');
|
||||
decide('success', 'docs-only PR (verified via PR file list) — e2e not required');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -143,20 +143,55 @@ jobs:
|
||||
{ owner, repo, run_id: RUN_ID, per_page: 100 },
|
||||
);
|
||||
if (!arts.some(a => a.name === 'pr-patch')) {
|
||||
await conclude('failure', 'expected pr-patch artifact is missing — the fork build did not complete');
|
||||
core.setOutput('should_run', 'false');
|
||||
decide('failure', 'expected pr-patch artifact is missing — the fork build did not complete');
|
||||
return;
|
||||
}
|
||||
|
||||
// Real run: open an in-progress check-run; `report` concludes it.
|
||||
const cr = await github.rest.checks.create({
|
||||
owner, repo, name: 'E2E Tests', head_sha: HEAD_SHA,
|
||||
status: 'in_progress',
|
||||
output: { title: 'E2E Tests', summary: 'Running e2e in the privileged fork workflow…' },
|
||||
});
|
||||
core.setOutput('check_run_id', String(cr.data.id));
|
||||
// Real run: the `open` step opens an in-progress check-run; `report`
|
||||
// concludes it.
|
||||
core.setOutput('verdict_status', 'in_progress');
|
||||
core.setOutput('verdict_summary', 'Running e2e in the privileged fork workflow…');
|
||||
core.setOutput('should_run', 'true');
|
||||
|
||||
# Skipped when `resolve` set-failed on a bad SHA (fail-closed: no check →
|
||||
# "E2E Tests" stays unreported → PR not mergeable).
|
||||
- name: Generate GitHub App token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
|
||||
with:
|
||||
app-id: ${{ secrets.COZYSTACK_CI_APP_ID }}
|
||||
private-key: ${{ secrets.COZYSTACK_CI_PRIVATE_KEY }}
|
||||
owner: cozystack
|
||||
repositories: ${{ github.event.repository.name }}
|
||||
permission-checks: write
|
||||
# Publish the verdict as the COZYSTACK_CI app so the check-run lands in the
|
||||
# app's own check-suite — matching the same-repo e2e-report path and the
|
||||
# branch-protection pin (see pull-requests.yaml e2e-report comment and
|
||||
# docs/agents/e2e-testing.md §10). Creating it with the default token would
|
||||
# file it under an arbitrary GitHub Actions suite (a labeler).
|
||||
- name: Open "E2E Tests" check-run (as app)
|
||||
id: open
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
|
||||
env:
|
||||
STATUS: ${{ steps.r.outputs.verdict_status }}
|
||||
CONCLUSION: ${{ steps.r.outputs.verdict_conclusion }}
|
||||
SUMMARY: ${{ steps.r.outputs.verdict_summary }}
|
||||
HEAD_SHA: ${{ steps.r.outputs.head_sha }}
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
const { STATUS, CONCLUSION, SUMMARY, HEAD_SHA } = process.env;
|
||||
const { owner, repo } = context.repo;
|
||||
const params = {
|
||||
owner, repo, name: 'E2E Tests', head_sha: HEAD_SHA,
|
||||
status: STATUS,
|
||||
output: { title: 'E2E Tests', summary: SUMMARY },
|
||||
};
|
||||
if (STATUS === 'completed') params.conclusion = CONCLUSION;
|
||||
const cr = await github.rest.checks.create(params);
|
||||
// Only the real (in-progress) run is handed to `report` to conclude.
|
||||
if (STATUS === 'in_progress') core.setOutput('check_run_id', String(cr.data.id));
|
||||
|
||||
# Push the fork-built image archives to OCIR by digest and flux-push the
|
||||
# digest-pinned packages tree. Only trusted binaries touch the fork's content
|
||||
# while the OCIR token is live — no `make`/fork script runs here.
|
||||
@@ -406,8 +441,19 @@ jobs:
|
||||
if: ${{ always() && needs.resolve.outputs.should_run == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
checks: write
|
||||
contents: read
|
||||
steps:
|
||||
# The pending check-run was opened by the COZYSTACK_CI app in `resolve`;
|
||||
# checks.update must run as the same app, so mint its token here too.
|
||||
- name: Generate GitHub App token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
|
||||
with:
|
||||
app-id: ${{ secrets.COZYSTACK_CI_APP_ID }}
|
||||
private-key: ${{ secrets.COZYSTACK_CI_PRIVATE_KEY }}
|
||||
owner: cozystack
|
||||
repositories: ${{ github.event.repository.name }}
|
||||
permission-checks: write
|
||||
- name: Conclude "E2E Tests" check-run
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
|
||||
env:
|
||||
@@ -416,6 +462,7 @@ jobs:
|
||||
E2E_RESULT: ${{ needs.e2e.result }}
|
||||
HEAD_SHA: ${{ needs.resolve.outputs.head_sha }}
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
const { CHECK_RUN_ID, PUBLISH_RESULT, E2E_RESULT, HEAD_SHA } = process.env;
|
||||
const { owner, repo } = context.repo;
|
||||
|
||||
@@ -884,6 +884,17 @@ jobs:
|
||||
# • otherwise (skipped/failed/cancelled with code changes) → failure
|
||||
# Fork PRs skip this job; e2e-fork.yaml concludes "E2E Tests" for them, so a
|
||||
# fork PR stays "Expected"/not-mergeable until the privileged run reports.
|
||||
#
|
||||
# The check-run is published with the COZYSTACK_CI GitHub App token, NOT the
|
||||
# default GITHUB_TOKEN. A token-created check-run is owned by the app that made
|
||||
# it and GitHub files it under that app's check-suite; with the default token
|
||||
# (the "GitHub Actions" app) a floating check-run attaches to an arbitrary
|
||||
# GitHub Actions suite for the SHA — in practice the earliest one, a tiny
|
||||
# labeler workflow — so "E2E Tests" rendered under "PR size label". Publishing
|
||||
# as the app gives the check its own dedicated suite (shown under the app's
|
||||
# name) and is stricter: only the CI app can satisfy the gate, not any Actions
|
||||
# workflow. REQUIRES main branch protection to require "E2E Tests" from this
|
||||
# app's id (not app_id 15368) — see docs/agents/e2e-testing.md §10.
|
||||
e2e-report:
|
||||
name: "Report E2E status"
|
||||
needs: ["plan", "e2e"]
|
||||
@@ -891,8 +902,16 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write
|
||||
steps:
|
||||
- name: Generate GitHub App token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
|
||||
with:
|
||||
app-id: ${{ secrets.COZYSTACK_CI_APP_ID }}
|
||||
private-key: ${{ secrets.COZYSTACK_CI_PRIVATE_KEY }}
|
||||
owner: cozystack
|
||||
repositories: ${{ github.event.repository.name }}
|
||||
permission-checks: write
|
||||
- name: Conclude "E2E Tests" check-run
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
|
||||
env:
|
||||
@@ -901,6 +920,7 @@ jobs:
|
||||
E2E_RESULT: ${{ needs.e2e.result }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
const { PLAN_RESULT, CODE, E2E_RESULT, HEAD_SHA } = process.env;
|
||||
let conclusion, summary;
|
||||
|
||||
Reference in new issue
Block a user