Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .github/scripts/tests/verify-pull-request-policy.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ test("blocking steps have ids, continue on error and guarded applicability", ()
}
assert.match(step("intent"), /inputs.release-intent && github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'main'/);
assert.match(step("notes"), /github.event_name == 'pull_request'/);
// #93: an unlabelled opened run waits in progress so the labeled run cancels it, and only then reads labels.
assert.match(step("label-grace"), /inputs.release-intent && github.event_name == 'pull_request' && github.event.action == 'opened'/);
assert.match(step("label-grace"), /toJSON\(github.event.pull_request.labels\) == '\[\]'/);
assert.match(step("label-grace"), /user.login != 'dependabot\[bot\]'/);
assert.match(step("label-grace"), /\n {8}run: sleep 20$/);
assert(steps.indexOf(step("label-grace")) < steps.indexOf(step("pull")), "waits before the live read");
assert.match(step("pull"), /github.event_name == 'pull_request'/);
assert.match(step("checkout"), /persist-credentials: false/);
assert.doesNotMatch(step("checkout"), /\n\s+ref:/);
Expand Down
10 changes: 10 additions & 0 deletions .github/scripts/tests/verify-release-intent.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,16 @@ test("a Dependabot pull request passes with a notice when the token cannot write
assert.equal(run(INTENT, { live: pull(["major", "patch"], "someone"), readOnly: true }).status, 1);
});

test("an unlabelled opened pull request waits before the gate reads labels, so a labeled run supersedes it", () => {
const grace = INTENT.indexOf("Wait briefly for labels added right after opening");
const gate = INTENT.indexOf("Require exactly one release-intent label");
assert(grace > 0 && grace < gate);
const block = INTENT.slice(grace, gate);
assert.match(block, /github.event.action == 'opened' && toJSON\(github.event.pull_request.labels\) == '\[\]'/);
assert.match(block, /user.login != 'dependabot\[bot\]'/);
assert.match(block, /run: sleep 20\n/);
});

test("the gate sets no permissions of its own, so the caller's pull-requests: read reaches it", () => {
assert.equal(/^\s*permissions:/m.test(INTENT), false);
assert(INTENT.includes("GH_TOKEN: ${{ github.token }}"));
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/verify-pull-request-policy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# A pull request opened without labels is often labelled seconds later (gh pr create --label, or a click
# right after opening). Waiting here keeps this run in progress, so the labeled run cancels it through the
# caller's concurrency group instead of leaving a stale failing release-intent check (Cratis/Workflows#93).
- name: Wait briefly for labels added right after opening
id: label-grace
if: ${{ !cancelled() && inputs.release-intent && github.event_name == 'pull_request' && github.event.action == 'opened' && github.event.pull_request.base.ref == 'main' && toJSON(github.event.pull_request.labels) == '[]' && github.event.pull_request.user.login != 'dependabot[bot]' }}
run: sleep 20
- name: Read the pull request as it is now
id: pull
if: ${{ !cancelled() && github.event_name == 'pull_request' && steps.tools.outcome == 'success' }}
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/verify-release-intent.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ jobs:
timeout-minutes: 15

steps:
# A pull request opened without labels is often labelled seconds later (gh pr create --label, or a click
# right after opening). Waiting keeps this run in progress, so the labeled run cancels it through the
# caller's concurrency group instead of leaving a stale failing check (Cratis/Workflows#93).
- name: Wait briefly for labels added right after opening
if: ${{ github.event.action == 'opened' && toJSON(github.event.pull_request.labels) == '[]' && github.event.pull_request.user.login != 'dependabot[bot]' }}
run: sleep 20
- name: Require exactly one release-intent label
env:
GH_TOKEN: ${{ github.token }}
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,9 @@ The caller runs on pull-request events `opened`, `edited`, `reopened`, `synchron
`labeled`, `unlabeled`, `ready_for_review`, and pushes to `main`, with one cancelling
concurrency group per PR/ref. Push events run only the work-record policy. Intent
applies only to PRs targeting `main` and can be disabled with `release-intent: false`.
When such a PR is opened without labels, the intent check waits 20 seconds before reading
them, so a label added right after opening cancels that run instead of leaving a stale
failure; `verify-release-intent.yml` does the same.
Notes retain the default-branch, release-label and Dependabot rules below. The
reusable declares no permissions; callers grant `contents: read` and
`pull-requests: read`. Releasing repositories also have a separate
Expand Down
Loading