Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions .asf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,16 @@ github:
required_approving_review_count: 1
required_status_checks:
strict: false
# test is the single unconditional job in .github/workflows/ci.yml. It
# runs the install-free contract checks on every change and installs the
# toolchain only for the validation its own planning step selects, so a
# documentation-only change still reports without paying for a build.
# Renaming the job there, adding a paths filter that stops ci.yml from
# running, or splitting the work back across jobs so this context comes
# from an aggregator that can be skipped, freezes every pull request:
# the check never reports and no committer can override it.
# test is the single required-check authority in
# .github/workflows/ci.yml. It runs for every new or updated pull request
# targeting main, including a pull request retargeted there. Title- and
# body-only edits create a differently named skipped job, so they neither
# cancel nor satisfy this context. The job runs install-free contract
# checks before installing the toolchain only for the validation its own
# planning step selects, so a documentation-only change still reports
# without paying for a build. Renaming the required job, adding a paths
# filter that stops ci.yml from running, or splitting this authority back
# across jobs can leave the check unreported and freeze pull requests.
# A required context must report on every pull request, so a lane
# behind a paths filter cannot be listed here: the filter would keep
# the workflow from starting and the check would stay pending forever.
Expand Down
9 changes: 8 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@ name: CI
on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened, edited]
push:
branches: [main]
workflow_dispatch:

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
# `edited` also covers title and body changes. Isolate those no-op runs so
# they cannot cancel the check for the current pull request revision.
group: ci-${{ github.workflow }}-${{ github.ref }}${{ github.event.action == 'edited' && github.event.changes.base.ref.from == '' && format('-ignored-{0}', github.run_id) || '' }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

permissions:
Expand All @@ -42,6 +45,10 @@ jobs:
# Renaming it would leave that check unreported on every open pull request
# until the rename merged, and nothing could merge while it was unreported.
test:
# A base-ref edit keeps the protected `CI / test` name. Other edits create
# only a differently named skipped check, so they cannot satisfy it.
name: ${{ github.event_name == 'pull_request' && github.event.action == 'edited' && github.event.changes.base.ref.from == '' && 'ignored-edit' || 'test' }}
if: ${{ github.event_name != 'pull_request' || github.event.action != 'edited' || github.event.changes.base.ref.from != '' }}
# Pinned, not `ubuntu-latest`. The two resolve to the same image, but only
# the alias makes this required context wait at the tail, and the steps
# below already assume this image. `ci-workflow-policy.test.mjs` holds the
Expand Down
27 changes: 23 additions & 4 deletions scripts/ci-workflow-policy.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,19 @@ test('GitHub output matches the selections consumed by CI', () => {
assert.deepEqual(outputKeys, consumedKeys);
});

test('one unconditional job carries the required context on every pull request', () => {
test('one job remains the only required-check authority', () => {
const workflow = readWorkflow('ci.yml');

// `.asf.yaml` requires `test`. A paths filter would stop the workflow and
// leave that check pending forever, and a second job would make the same
// pull request queue for a scarce runner twice to reach one verdict.
// leave that check pending forever, and a second job would create another
// authority. Metadata-only edits may skip this job under a different name;
// the retarget contract below proves that exception cannot impersonate it.
assert.doesNotMatch(triggerBlock('ci.yml'), /\bpaths(-ignore)?:/u);

const jobsBlock = workflow.slice(workflow.indexOf('\njobs:'));
const jobs = [...jobsBlock.matchAll(/^ {2}([a-z0-9_-]+):$/gmu)].map((match) => match[1]);
assert.deepEqual(jobs, ['test']);
assert.doesNotMatch(jobsBlock, /^ {4}needs:/mu);
assert.doesNotMatch(jobsBlock, /^ {4}if:/mu);
});

test('planning runs first and every later step gates on its outputs', () => {
Expand Down Expand Up @@ -101,6 +101,25 @@ test('core CI validates pull requests and the resulting main branch state', () =
assert.match(workflow, /\[\[ "\$BASE_SHA" =~ \^0\+\$ \]\]/u);
});

test('core CI runs on base retargets without letting metadata edits replace the required check', () => {
const workflow = readWorkflow('ci.yml');

assert.match(workflow, /types: \[opened, synchronize, reopened, edited\]/u);
assert.match(
workflow,
/group: ci-\$\{\{ github\.workflow \}\}-\$\{\{ github\.ref \}\}\$\{\{ github\.event\.action == 'edited' && github\.event\.changes\.base\.ref\.from == '' && format\('-ignored-\{0\}', github\.run_id\) \|\| '' \}\}/u,
);
assert.match(workflow, /cancel-in-progress: \$\{\{ github\.event_name == 'pull_request' \}\}/u);
assert.match(
workflow,
/name: \$\{\{ github\.event_name == 'pull_request' && github\.event\.action == 'edited' && github\.event\.changes\.base\.ref\.from == '' && 'ignored-edit' \|\| 'test' \}\}/u,
);
assert.match(
workflow,
/if: \$\{\{ github\.event_name != 'pull_request' \|\| github\.event\.action != 'edited' \|\| github\.event\.changes\.base\.ref\.from != '' \}\}/u,
);
});

test('core CI uses the Windows inventory package-script authority', () => {
const workflow = readWorkflow('ci.yml');

Expand Down