diff --git a/.github/scripts/artifact-workflow-filter.test.js b/.github/scripts/artifact-workflow-filter.test.js new file mode 100644 index 00000000000..0214907fa24 --- /dev/null +++ b/.github/scripts/artifact-workflow-filter.test.js @@ -0,0 +1,58 @@ +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); +const assert = require('node:assert/strict'); +const { test } = require('node:test'); + +function conditionFor(file, job) { + const source = fs.readFileSync(path.join(__dirname, '../workflows', file), 'utf8').replace(/\r\n/g, '\n'); + const jobBody = source.split(`\n ${job}:\n`)[1]?.split(/\n [\w-]+:\n/)[0]; + const expression = jobBody?.match(/\n if: >\n((?: .*\n)+)/)?.[1].trim(); + assert.ok(expression, `Missing condition for ${file}:${job}`); + // The conditions use only member access, == and &&, so they run as JavaScript. + // GitHub's == ignores case and a missing object yields null; JS models neither. + return new Function('github', `return Boolean(${expression});`); +} + +function event(overrides = {}) { + return { + workflow: { path: '.github/workflows/ci.yml' }, + workflow_run: { name: 'Build firmware', event: 'pull_request', conclusion: 'success' }, + ...overrides, + }; +} + +for (const [file, job] of [['pr-test-builds.yml', 'publish'], ['ci-size-report.yml', 'pr-comment']]) { + const accepts = conditionFor(file, job); + test(`${file}: accepts firmware without workflow_run.path`, () => { + assert.equal(accepts({ event: event() }), true); + }); + test(`${file}: rejects same-name non-code workflow`, () => { + assert.equal(accepts({ event: event({ workflow: { path: '.github/workflows/non-code-change.yaml' } }) }), false); + }); + test(`${file}: ignores a misleading run path`, () => { + const payload = event({ workflow: { path: '.github/workflows/non-code-change.yaml' } }); + payload.workflow_run.path = '.github/workflows/ci.yml'; + assert.equal(accepts({ event: payload }), false); + }); + test(`${file}: rejects unsuccessful and non-PR runs`, () => { + for (const conclusion of ['failure', 'cancelled', 'skipped', 'action_required', null]) { + const payload = event(); + payload.workflow_run.conclusion = conclusion; + assert.equal(accepts({ event: payload }), false); + } + const payload = event(); + payload.workflow_run.event = 'push'; + assert.equal(accepts({ event: payload }), false); + }); + test(`${file}: rejects missing or unknown workflow paths`, () => { + for (const workflow of [{}, { path: '.github/workflows/other.yml' }]) { + assert.equal(accepts({ event: event({ workflow }) }), false); + } + }); +} + +test('firmware workflow path exists', () => { + assert.ok(fs.existsSync(path.join(__dirname, '../..', event().workflow.path))); +}); diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 7e28f7cad01..8b469ad949c 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -121,6 +121,13 @@ as `pr-test-builds.yml`** (secrets available even for fork PRs). **Triggers:** Pull requests **Purpose:** Detects PRs with only documentation/formatting changes +It shares the name `Build firmware` with `ci.yml` but produces none of its +artifacts. The `workflow_run` consumers `pr-test-builds.yml` and +`ci-size-report.yml` therefore also require +`github.event.workflow.path == '.github/workflows/ci.yml'` (a path, not a +workflow ID, so forks behave the same). Check both conditions with +`node --test .github/scripts/artifact-workflow-filter.test.js`. + ## Configuration Files - `../.github/stale.yml` - Stale issue/PR management diff --git a/.github/workflows/ci-size-report.yml b/.github/workflows/ci-size-report.yml index f095124a0b9..d5d6e7a3772 100644 --- a/.github/workflows/ci-size-report.yml +++ b/.github/workflows/ci-size-report.yml @@ -34,8 +34,9 @@ name: CI Size Report # of the two fires per PR and either satisfies the same required-status- # check name. `workflow_run` can't tell the two apart by name (see # github-actions-workflows.md), and the stub's only job is `test` — no -# `upload-artifacts`, none of the artifacts this job needs — so a guard -# step checks for that job directly before downloading anything. +# `upload-artifacts`, none of the artifacts this job needs — so pr-comment +# filters on the workflow path, and a guard step still checks that +# `upload-artifacts` succeeded before downloading anything. # # Requires the same repository secret PR_BUILDS_TOKEN as pr-test-builds.yml # (Contents: write access to iNavFlight/pr-test-builds). @@ -137,6 +138,7 @@ jobs: runs-on: ubuntu-latest if: > github.event.workflow_run.name == 'Build firmware' && + github.event.workflow.path == '.github/workflows/ci.yml' && github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' concurrency: @@ -154,12 +156,10 @@ jobs: # untrusted code is never checked out in this privileged context. - uses: actions/checkout@v4 - # ci.yml's "Build firmware" name is shared with the non-code-change.yaml - # stub (see comment at the top of this file), whose only job is `test` - # — no upload-artifacts, none of the artifacts downloaded below. Check - # for that job directly rather than trusting the aggregate - # workflow_run.conclusion, which the job-level `if:` above already - # confirmed is 'success' for either workflow. + # The job-level `if:` already excludes the non-code-change.yaml stub by + # path (see comment at the top of this file). Still check + # upload-artifacts directly, so a renamed or skipped job skips the + # comment instead of failing on the downloads below. - name: Check upload-artifacts job succeeded id: check env: diff --git a/.github/workflows/pr-test-builds.yml b/.github/workflows/pr-test-builds.yml index 19867077aa5..8ead14bd46c 100644 --- a/.github/workflows/pr-test-builds.yml +++ b/.github/workflows/pr-test-builds.yml @@ -13,9 +13,11 @@ on: jobs: publish: runs-on: ubuntu-latest - # Only act on pull_request-triggered runs that succeeded. + # Only act on successful pull_request runs of ci.yml. non-code-change.yaml + # shares the name "Build firmware" but uploads no artifacts. if: > github.event.workflow_run.event == 'pull_request' && + github.event.workflow.path == '.github/workflows/ci.yml' && github.event.workflow_run.conclusion == 'success' # Prevent concurrent runs for the same PR branch racing on the # release delete/create cycle.