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
115 changes: 67 additions & 48 deletions .github/workflows/required-changes-from-trunk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,87 +2,106 @@ name: Required changes from trunk

on:
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review, edited]
types: [opened, synchronize, reopened, ready_for_review, edited, closed]
branches: [trunk]
push:
branches: [trunk]
workflow_dispatch:
inputs:
move-baseline:
description: 'Move the baseline tag to current trunk HEAD and flip stale statuses'
type: boolean
default: false
mode:
description: 'auto moves the baseline only if a labeled merge earned it, then refreshes; move-baseline forces a move and then refreshes; refresh-pr-statuses only refreshes open PR statuses'
type: choice
default: auto
options: [auto, move-baseline, refresh-pr-statuses]

# Disable permissions for all available scopes by default.
# Any needed permissions should be configured at the job level.
permissions: {}

concurrency:
# Serialize per PR, and serialize all baseline moves in one group. Replaced
# pending runs are safe: labeled merges are scanned cumulatively from the
# tag, so the next run in the group recovers them.
group: required-trunk-changes-${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || 'baseline' }}
cancel-in-progress: false
# The default queue keeps a single pending run, so a trunk push could
# replace (cancel) a pending forced dispatch; queue: max keeps them all.
queue: max
env:
# One source of truth for both jobs. No markers: the baseline moves only
# through a labeled merge or a dispatch, never because a path changed.
PR_BASELINES: '[{"name":"required-trunk-changes","label":"Require PR update"}]'
STATUS_CONTEXT: Required changes from trunk
DESCRIPTION_PASS: Includes required {base} changes.
DESCRIPTION_FAIL: Merge or rebase {base} to include required changes.

jobs:
check-pr:
name: Check required changes from trunk
if: github.repository == 'WordPress/gutenberg' && github.event_name == 'pull_request_target'
name: Report required trunk changes status
if: >
github.repository == 'WordPress/gutenberg' &&
github.event_name == 'pull_request_target' && github.event.action != 'closed'
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 10
permissions:
contents: read
statuses: write
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
concurrency:
group: required-trunk-changes-status-${{ github.event.pull_request.number }}
cancel-in-progress: false
steps:
# Trunk only. PR code is never checked out, built, or executed.
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# The head SHA comes from the event. PR code is never checked out,
# built, or executed.
- name: Report the status for this pull request
uses: mawesomedev/pr-baseline-action@8c21a09691ccfdf5ea118cb60cee8ec8b99231a1 # v0.1.1
with:
ref: trunk
persist-credentials: false
base: trunk
baselines: ${{ env.PR_BASELINES }}
status-context: ${{ env.STATUS_CONTEXT }}
description-pass: ${{ env.DESCRIPTION_PASS }}
description-fail: ${{ env.DESCRIPTION_FAIL }}

- name: Setup Node.js and install dependencies
uses: ./.github/setup-node

- name: Check required changes from trunk and post status
run: npm run --workspace @wordpress/validation-tools required-trunk-changes -- check --head-sha "$HEAD_SHA"

move-baseline:
name: Move required trunk baseline
# The baseline moves only through explicit intent: a dispatch with
# move-baseline, or a merged PR carrying the required-update label
# (detected cumulatively on every trunk push). Re-dispatching when the
# tag is already current re-runs the fan-out, which is the retry path
# after an interrupted sweep.
refresh-pr-statuses:
name: Move the required trunk baseline and refresh statuses
# The baseline moves only through explicit intent: a merged pull request
# carrying the label, or a dispatch with mode move-baseline. Every trunk
# push recovers a move whose own run was lost, and continues a refresh
# that stopped on its write budget.
if: >
github.repository == 'WordPress/gutenberg' && (
github.event_name == 'push' ||
(github.event_name == 'workflow_dispatch' && inputs.move-baseline)
(github.event_name == 'pull_request_target' && github.event.action == 'closed' && github.event.pull_request.merged) ||
(github.event_name == 'push' && github.ref_name == 'trunk') ||
github.event_name == 'workflow_dispatch'
)
Comment on lines 61 to 66

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rollout instructions seed the new ref only after merge, but this job runs on that merge's trunk push while the ref is still absent. An absent baseline is a pass, so the first refresh can overwrite existing failing statuses with green ones. Could we pre-seed refs/baselines/required-trunk-changes at the old tag's commit before merging and update the rollout steps accordingly?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I am thinking about the same. Seed the ref and hit merge.

runs-on: ubuntu-latest
timeout-minutes: 30
timeout-minutes: 60
permissions:
contents: write
statuses: write
pull-requests: read
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FORCE: ${{ github.event_name == 'workflow_dispatch' && inputs.move-baseline }}
concurrency:
group: required-trunk-changes-refresh
cancel-in-progress: false
# The default queue keeps a single pending run, so a trunk push could
# replace (cancel) a pending forced dispatch; queue: max keeps them all.
queue: max
steps:
# Treeless but complete history: ancestry needs commits, not trees,
# and reading it from the clone costs no API quota.
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: trunk
fetch-depth: 0
filter: tree:0
persist-credentials: false

- name: Setup Node.js and install dependencies
uses: ./.github/setup-node
- name: Move the baseline and refresh open pull requests
id: pr-baseline
uses: mawesomedev/pr-baseline-action@8c21a09691ccfdf5ea118cb60cee8ec8b99231a1 # v0.1.1
with:
base: trunk
baselines: ${{ env.PR_BASELINES }}
status-context: ${{ env.STATUS_CONTEXT }}
description-pass: ${{ env.DESCRIPTION_PASS }}
description-fail: ${{ env.DESCRIPTION_FAIL }}
mode: ${{ inputs.mode || 'auto' }}
force: ${{ inputs.mode == 'move-baseline' }}

- name: Move the baseline tag and fan out
run: npm run --workspace @wordpress/validation-tools required-trunk-changes -- move-baseline --force="$FORCE" --then-fanout
- name: Upload the per-pull-request results
if: ${{ always() && steps.pr-baseline.outputs.results-file != '' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: required-trunk-changes-refresh
path: ${{ steps.pr-baseline.outputs.results-file }}
38 changes: 19 additions & 19 deletions docs/contributors/code/required-changes-from-trunk.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,22 @@ The `Required changes from trunk` status check ensures open pull requests contai

## How it works

A movable lightweight git tag, `required-trunk-baseline`, points at the last `trunk` commit that every open pull request must contain. The check passes when the tagged commit is an ancestor of the pull request head, and fails otherwise.
A movable git ref, `refs/baselines/required-trunk-changes`, points at the last `trunk` commit that every open pull request must contain. The check passes when that commit is an ancestor of the pull request head, and fails otherwise. While the ref does not exist, nothing is required and every pull request passes.

The tag moves only through explicit maintainer intent, in one of two ways:
The baseline moves only through explicit maintainer intent, in one of two ways:

- Dispatching the `Required changes from trunk` workflow with `move-baseline: true`, or
- Merging a pull request that carries the `Require PR update` label.
- Merging a pull request that carries the `Require PR update` label, or
- Dispatching the `Required changes from trunk` workflow with `mode: move-baseline`.

When the tag moves, open pull requests holding a now-outdated passing status are switched to failing, subject to the limits below.
When the baseline moves, every open pull request is restamped with the verdict it now warrants.

## Limits
The ref lives outside `refs/heads/` and `refs/tags/` on purpose: nothing fetches it unless asked, so a move never disturbs a `git pull`, starts no workflow, and no branch cleanup tool removes it. `git ls-remote origin 'refs/baselines/*'` shows where it points.

The sweep that follows a baseline move is intentionally bounded:
## Limits

- Draft pull requests are skipped; they cannot merge anyway and are re-checked when marked ready for review.
- Each run writes at most 400 statuses to stay inside API rate limits. A run that hits the cap (or any write failure) exits nonzero; dispatch the workflow again with `move-baseline: true` until a run finishes cleanly.
- Pull requests that never received a status show as "Expected", which also blocks merging; they get a real status on their next update.
- A pull request checked at the exact moment the baseline moves can briefly keep an outdated status; it converges on the pull request's next event or the next dispatch.
- Each run writes at most 450 statuses. A run that stops on that budget, or on a rate limit, exits nonzero and says so; the next `trunk` push, or a dispatch, continues where it left off.
- A pull request that never received a status shows as "Expected", which also blocks merging. The next refresh or its own next update gives it a real one.
- A pull request whose head moves while a refresh is running is deferred to the next one rather than stamped at a stale head.

## My pull request has a red "Required changes from trunk" status. What do I do?

Expand All @@ -29,25 +28,26 @@ Update your branch past the current baseline, using either method:
- Merge the latest `trunk` into your branch, or
- Rebase your branch onto the latest `trunk`.

Pushing the update re-runs the check and turns it green.
Pushing the update re-runs the check and turns it green. The failing status links to the compare view of exactly what your branch is missing.

## The `Require PR update` label

Committers apply the `Require PR update` label to a pull request whose change invalidates all open pull requests, such as a toolchain bump or a repo-wide formatting change. When such a pull request merges, the baseline moves and all open pull requests must update. The label is detected cumulatively on every `trunk` push, so it still takes effect if workflow runs are coalesced.
Committers apply the `Require PR update` label to a pull request whose change invalidates all open pull requests, such as a toolchain bump or a repo-wide formatting change. When such a pull request merges, the baseline moves and all open pull requests must update.

## Maintainer runbook

The `Required changes from trunk` workflow exposes one `workflow_dispatch` switch, `move-baseline`: it moves the baseline tag to the current `trunk` HEAD and flips stale passing statuses. Re-dispatching when the baseline is already current re-runs only the flip sweep, which is the retry path after an interrupted run.
The `Required changes from trunk` workflow exposes a `mode` dispatch input: `auto` behaves like a `trunk` push, moving the baseline only if a labeled merge earned it and then refreshing, `move-baseline` forces a move to the current `trunk` HEAD and then refreshes, and `refresh-pr-statuses` only refreshes open pull request statuses, which is the retry path after an interrupted run.

### Initial setup

The workflow blocks nothing until these one-time steps are completed, in order:

1. Create the `Require PR update` label.
2. Seed the baseline: dispatch the workflow with `move-baseline: true`.
3. Let the check run unenforced for a while; active pull requests get stamped by their own events.
4. Ask a repository admin to add `Required changes from trunk` (source: GitHub Actions) to the trunk ruleset's required status checks, and to add a tag ruleset for `required-trunk-baseline` restricting creation, update, and deletion to the GitHub Actions app.
2. Let a refresh run with no baseline ref. Every open pull request receives a pass, which proves the token and permissions before anything can block.
3. Seed the baseline: dispatch the workflow with `mode: move-baseline`, which seeds it at the current `trunk` HEAD. To seed at an earlier commit, push that commit to the ref directly: `git push origin <sha>:refs/baselines/required-trunk-changes`.
4. Re-dispatch with `mode: refresh-pr-statuses` until a run reports no writes, so that no pull request is left unstamped.
5. Ask a repository admin to add the `Required changes from trunk` commit status (source: GitHub Actions) to the trunk ruleset's required status checks.

To roll the check back, remove `Required changes from trunk` from the required status checks; existing statuses become informational immediately. The tag can stay for a later re-enable.
Rulesets cannot cover `refs/baselines/`, so anyone with write access can move or delete the ref; the tooling itself only ever fast-forwards it. To roll the check back, remove `Required changes from trunk` from the required status checks; existing statuses become informational immediately.

The implementation lives in `tools/validation/required-trunk-changes/` and `.github/workflows/required-changes-from-trunk.yml`.
The workflow lives in `.github/workflows/required-changes-from-trunk.yml` and runs the [`pr-baseline` action](https://github.com/marketplace/actions/pr-baseline), whose [documentation](https://github.com/mawesomedev/mawesome/tree/main/packages/pr-baseline/docs) covers permissions, rate limits and edge cases in full.
1 change: 0 additions & 1 deletion tools/validation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
"dependency-audit": "dependency-audit --collapse-root-cause \"../../packages/*\"",
"check-local-changes": "node ./check-local-changes.cjs",
"check-root-dependencies": "node ./check-root-dependencies.mjs",
"required-trunk-changes": "node ./required-trunk-changes/index.mjs",
"test-create-block": "node ./test-create-block.mjs",
"validate-package-lock": "node ./validate-package-lock.cjs",
"validate-tsconfig": "node ./validate-tsconfig.mjs"
Expand Down
52 changes: 0 additions & 52 deletions tools/validation/required-trunk-changes/check.mjs

This file was deleted.

Loading
Loading