From 971edc0451dc735a2e8b8a58daeb84554311e220 Mon Sep 17 00:00:00 2001 From: Farhan Date: Wed, 19 Aug 2026 21:51:25 +0500 Subject: [PATCH 1/3] ci(changelog): re-run the check when PR labels change --- .github/workflows/changelog.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index dde678d9645..604d701ef87 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -10,6 +10,10 @@ concurrency: on: pull_request: branches: ["main"] + # labeled/unlabeled: the skip-changelog and changelog-version-edit gates + # read labels from the event payload, so toggling a label must start a + # fresh run (re-running an old run replays the stale payload). + types: [opened, synchronize, reopened, labeled, unlabeled] jobs: changelog: From ebc0d571f4317c7cf82f99f4d8c527f812cf5073 Mon Sep 17 00:00:00 2001 From: Farhan Date: Sat, 22 Aug 2026 00:39:54 +0500 Subject: [PATCH 2/3] ci(changelog): read PR labels from the API, not the event payload The skip-changelog and changelog-version-edit gates read github.event.pull_request.labels, which is a snapshot taken when the run was queued. A label applied after the last push is invisible to the run, and "Re-run failed jobs" replays that same stale payload, so the check keeps failing until someone pushes again. Read the labels from the API at job time instead, so a re-run picks up the current label set. This removes the need for the labeled/unlabeled trigger types added earlier on this branch, which fired for every label on every PR and cancelled in-progress runs for no change in outcome. Same approach aiohttp uses for its CHANGES-fragment check. --- .github/workflows/changelog.yml | 35 ++++++++++++++++----------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 604d701ef87..e0c4cd2a91e 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -2,6 +2,7 @@ name: changelog permissions: contents: read + pull-requests: read concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.id }} @@ -10,28 +11,26 @@ concurrency: on: pull_request: branches: ["main"] - # labeled/unlabeled: the skip-changelog and changelog-version-edit gates - # read labels from the event payload, so toggling a label must start a - # fresh run (re-running an old run replays the stale payload). - types: [opened, synchronize, reopened, labeled, unlabeled] jobs: changelog: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - name: Check for skip-changelog label - id: skip + # Read labels from the API so re-runs do not use a stale event payload. + - name: Read PR labels + id: labels shell: bash env: - SKIP_CHANGELOG: ${{ contains(github.event.pull_request.labels.*.name, 'skip-changelog') }} + GH_TOKEN: ${{ github.token }} + PR_URL: ${{ github.event.pull_request.url }} run: | - if [ "$SKIP_CHANGELOG" = "true" ]; then - echo "skip=true" >> "$GITHUB_OUTPUT" - echo "PR has 'skip-changelog' label; bypassing changelog check." - else - echo "skip=false" >> "$GITHUB_OUTPUT" - fi + set -euo pipefail + gh api "$PR_URL" --jq ' + [.labels[].name] | + "skip=\(index("skip-changelog") != null)", + "version_edit=\(index("changelog-version-edit") != null)" + ' >> "$GITHUB_OUTPUT" # The heading guard runs even for skip-changelog PRs: that label only # waives the news-fragment requirement, not the publish-trigger guard. - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -44,19 +43,19 @@ jobs: # from, so the guard and the publisher cannot disagree. Escape hatch # for deliberate restructuring: the 'changelog-version-edit' label. - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - if: ${{ !contains(github.event.pull_request.labels.*.name, 'changelog-version-edit') && !startsWith(github.head_ref, 'release/') }} + if: steps.labels.outputs.version_edit != 'true' && !startsWith(github.head_ref, 'release/') - name: Reject manual changelog version headings - if: ${{ !contains(github.event.pull_request.labels.*.name, 'changelog-version-edit') && !startsWith(github.head_ref, 'release/') }} + if: steps.labels.outputs.version_edit != 'true' && !startsWith(github.head_ref, 'release/') env: BASE_REF: origin/${{ github.base_ref }} run: uv run --no-config --locked --script scripts/release.py check-headings - uses: ./.github/actions/setup_build_env - if: steps.skip.outputs.skip != 'true' + if: steps.labels.outputs.skip != 'true' with: python-version: "3.14" run-uv-sync: true - name: Determine affected packages - if: steps.skip.outputs.skip != 'true' + if: steps.labels.outputs.skip != 'true' id: affected shell: bash run: | @@ -79,7 +78,7 @@ jobs: echo "Affected packages:" cat affected.txt - name: Check for news fragments - if: steps.skip.outputs.skip != 'true' + if: steps.labels.outputs.skip != 'true' shell: bash run: | set -euo pipefail From 105cefecc15283b6f8eaed69ff856cbd524c380b Mon Sep 17 00:00:00 2001 From: Farhan Date: Sat, 22 Aug 2026 00:58:04 +0500 Subject: [PATCH 3/3] ci(changelog): restore the label triggers alongside the API read Dropping labeled/unlabeled left the opposite bug: removing skip-changelog produced no run at all, so the green computed while the label was applied stayed as the check's verdict and the gate silently stayed open. The verdict is only valid for the label set it was computed under, so a label change has to start a run. Keep the API read too: it is what makes "Re-run failed jobs" pick up the current labels instead of replaying the queued payload. --- .github/workflows/changelog.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index e0c4cd2a91e..a19ec4b7f12 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -11,6 +11,16 @@ concurrency: on: pull_request: branches: ["main"] + # labeled/unlabeled: skip-changelog and changelog-version-edit waive parts + # of this check, so the verdict is only valid for the label set it was + # computed under. Without these types, removing a label leaves the green + # run that the label produced standing, and the gate silently stays open. + # + # The job cannot cheaply no-op on an unrelated label: a job skipped by `if` + # reports its check as skipped, which branch protection counts as passing, + # so it would overwrite a real failure with a green. Every run therefore + # computes the real verdict. + types: [opened, synchronize, reopened, labeled, unlabeled] jobs: changelog: