Skip to content
Open
58 changes: 58 additions & 0 deletions .github/scripts/artifact-workflow-filter.test.js
Original file line number Diff line number Diff line change
@@ -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)));
});
7 changes: 7 additions & 0 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 8 additions & 8 deletions .github/workflows/ci-size-report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/pr-test-builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading