From 8ba2e5163ca84872be07dd9458407a35a991c128 Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Fri, 22 May 2026 13:42:51 +0200 Subject: [PATCH 1/3] workflows: merge update_actions + update_composite_action to fix race The two workflows ran in separate concurrency groups and could overlap on a push that changed both actions.yml and the dependabot composite action.yml, silently clobbering one direction's edit. The new update_allowlist.yml runs both directions in order with a single concurrency group, so neither edit is lost. Fixes #866 Generated-by: Claude Opus 4.7 (1M context) --- .github/workflows/update_actions.yml | 91 ---------- .github/workflows/update_allowlist.yml | 167 ++++++++++++++++++ .github/workflows/update_composite_action.yml | 90 ---------- README.md | 24 +-- 4 files changed, 179 insertions(+), 193 deletions(-) delete mode 100644 .github/workflows/update_actions.yml create mode 100644 .github/workflows/update_allowlist.yml delete mode 100644 .github/workflows/update_composite_action.yml diff --git a/.github/workflows/update_actions.yml b/.github/workflows/update_actions.yml deleted file mode 100644 index 54baa5d0..00000000 --- a/.github/workflows/update_actions.yml +++ /dev/null @@ -1,91 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# -name: Handle Dependabot update -on: - workflow_dispatch: - push: - branches: - - main - paths: - - ".github/actions/for-dependabot-triggered-reviews/action.yml" - pull_request: - paths: - - ".github/workflows/update_actions.yml" - - ".github/actions/for-dependabot-triggered-reviews/action.yml" - - gateway/* - -permissions: - contents: read - -# We want workflows on main to run in order to avoid losing data through race conditions -concurrency: "${{ github.ref }}-${{ github.workflow }}" - -jobs: - update_actions: - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: true - token: ${{ secrets.ALLOWLIST_WORKFLOW_TOKEN || github.token }} # zizmor: ignore[secrets-outside-env] - - - name: Print token details - if: ${{ github.event_name != 'pull_request' }} - env: - GH_TOKEN: ${{ secrets.ALLOWLIST_WORKFLOW_TOKEN }} # zizmor: ignore[secrets-outside-env] - run: | - echo "::group::Token details" - echo "Token user and permissions:" - gh api /user --jq '"Login: \(.login)\nName: \(.name)\nEmail: \(.email)"' - echo "" - echo "Token expiration:" - gh api /installation/token --jq '.expires_at' 2>/dev/null || echo "Token expiration not available (likely a PAT, not an installation token)" - echo "" - echo "Token scopes:" - curl -sS -H "Authorization: token ${GH_TOKEN}" -I https://api.github.com/ 2>/dev/null | grep -i 'x-oauth-scopes' || echo "No OAuth scopes header (fine-grained or app token)" - echo "::endgroup::" - - - run: pipx install uv - - - name: Update actions.yml - run: | - uv run python << 'PYEOF' - import sys - sys.path.append("./gateway/") - - import gateway as g - g.update_actions(".github/actions/for-dependabot-triggered-reviews/action.yml", "actions.yml") - g.update_patterns("approved_patterns.yml", "actions.yml") - PYEOF - - - name: Commit and push changes - if: ${{ github.event_name != 'pull_request' }} - env: - GH_TOKEN: ${{ secrets.ALLOWLIST_WORKFLOW_TOKEN || github.token }} # zizmor: ignore[secrets-outside-env] - run: | - AUTHOR_NAME=$(gh api /user --jq '.login' 2>/dev/null || echo "asfgit") - AUTHOR_EMAIL=$(gh api /user --jq '.email // "\(.login)@users.noreply.github.com"' 2>/dev/null || echo "asfgit@users.noreply.github.com") - git config --local user.name "${AUTHOR_NAME}" - git config --local user.email "${AUTHOR_EMAIL}" - git add -f actions.yml approved_patterns.yml - git commit -m "Update actions.yml and approved_patterns.yml based on .github/actions/for-dependabot-triggered-reviews/action.yml" -m "Generated by .github/workflows/update_actions.yml" || echo "No changes" - git push origin diff --git a/.github/workflows/update_allowlist.yml b/.github/workflows/update_allowlist.yml new file mode 100644 index 00000000..6d590fc2 --- /dev/null +++ b/.github/workflows/update_allowlist.yml @@ -0,0 +1,167 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +# Replaces the former pair `update_actions.yml` (composite -> actions.yml) +# and `update_composite_action.yml` (actions.yml -> composite). Those ran +# in separate concurrency groups and could race when both files changed in +# overlapping pushes, silently overwriting one direction's edit. This +# single workflow always runs both directions in order, so neither edit +# is lost regardless of which file triggered the run. See #866. +name: Update Allowlist +on: + workflow_dispatch: + push: + branches: + - main + paths: + - "actions.yml" + - ".github/actions/for-dependabot-triggered-reviews/action.yml" + pull_request: + paths: + - ".github/workflows/update_allowlist.yml" + - ".github/actions/for-dependabot-triggered-reviews/action.yml" + - "actions.yml" + - "gateway/*" + +permissions: + contents: read + +# Single group across both inputs so no two updates touch the allowlist +# files in parallel. Don't cancel — every queued run must finish so we +# don't drop a dependabot bump or a manual actions.yml edit. +concurrency: + group: "${{ github.ref }}-update-allowlist" + cancel-in-progress: false + +jobs: + update: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: true + token: ${{ secrets.ALLOWLIST_WORKFLOW_TOKEN || github.token }} # zizmor: ignore[secrets-outside-env] + # On push/dispatch, check out the current tip of main rather + # than the trigger SHA. A queued run that started on an older + # SHA would otherwise regenerate from stale inputs and either + # undo the prior run's commit or fail to push. + ref: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && 'main' || '' }} + + - name: Print token details + if: ${{ github.event_name != 'pull_request' }} + env: + GH_TOKEN: ${{ secrets.ALLOWLIST_WORKFLOW_TOKEN }} # zizmor: ignore[secrets-outside-env] + run: | + echo "::group::Token details" + echo "Token user and permissions:" + gh api /user --jq '"Login: \(.login)\nName: \(.name)\nEmail: \(.email)"' + echo "" + echo "Token expiration:" + gh api /installation/token --jq '.expires_at' 2>/dev/null || echo "Token expiration not available (likely a PAT, not an installation token)" + echo "" + echo "Token scopes:" + curl -sS -H "Authorization: token ${GH_TOKEN}" -I https://api.github.com/ 2>/dev/null | grep -i 'x-oauth-scopes' || echo "No OAuth scopes header (fine-grained or app token)" + echo "::endgroup::" + + - run: pipx install uv + + - name: Sync composite action, actions.yml, and approved_patterns.yml + run: | + uv run python << 'PYEOF' + import sys + sys.path.append("./gateway/") + + import gateway as g + + composite = ".github/actions/for-dependabot-triggered-reviews/action.yml" + actions = "actions.yml" + patterns = "approved_patterns.yml" + + # Always run both directions. The previous two-workflow split + # raced on overlapping pushes; running both here means the + # outcome is the same regardless of which file triggered us. + # + # 1. Pull any new refs from the composite (e.g. dependabot + # bumps) into actions.yml. Additive: existing entries stay + # and get their expiry refreshed. + g.update_actions(composite, actions) + + # 2. Regenerate the composite from the (now-merged) + # actions.yml so a manual actions.yml edit is reflected. + g.update_workflow(composite, actions) + + # 3. Regenerate the approved patterns from actions.yml. + g.update_patterns(patterns, actions) + PYEOF + + - name: Commit and push changes + if: ${{ github.event_name != 'pull_request' }} + env: + GH_TOKEN: ${{ secrets.ALLOWLIST_WORKFLOW_TOKEN || github.token }} # zizmor: ignore[secrets-outside-env] + run: | + AUTHOR_NAME=$(gh api /user --jq '.login' 2>/dev/null || echo "asfgit") + AUTHOR_EMAIL=$(gh api /user --jq '.email // "\(.login)@users.noreply.github.com"' 2>/dev/null || echo "asfgit@users.noreply.github.com") + git config --local user.name "${AUTHOR_NAME}" + git config --local user.email "${AUTHOR_EMAIL}" + + composite=".github/actions/for-dependabot-triggered-reviews/action.yml" + if git diff --quiet -- actions.yml approved_patterns.yml "${composite}"; then + echo "No changes" + exit 0 + fi + + git add -f actions.yml approved_patterns.yml "${composite}" + git commit \ + -m "Sync actions.yml, composite action, and approved_patterns.yml" \ + -m "Generated by .github/workflows/update_allowlist.yml" + + # If a concurrent push (e.g. remove_expired.yml) advanced main + # while we were computing, rebase and retry. The sync script is + # idempotent so re-running on the rebased tree is safe. + for attempt in 1 2 3; do + if git push origin HEAD:main; then + exit 0 + fi + echo "Push rejected on attempt ${attempt}; rebasing and re-running sync" + git fetch origin main + git reset --hard origin/main + uv run python << 'PYEOF' + import sys + sys.path.append("./gateway/") + import gateway as g + composite = ".github/actions/for-dependabot-triggered-reviews/action.yml" + actions = "actions.yml" + patterns = "approved_patterns.yml" + g.update_actions(composite, actions) + g.update_workflow(composite, actions) + g.update_patterns(patterns, actions) + PYEOF + if git diff --quiet -- actions.yml approved_patterns.yml "${composite}"; then + echo "Already in sync after rebase; nothing to push" + exit 0 + fi + git add -f actions.yml approved_patterns.yml "${composite}" + git commit \ + -m "Sync actions.yml, composite action, and approved_patterns.yml" \ + -m "Generated by .github/workflows/update_allowlist.yml" + done + echo "Failed to push after 3 attempts" + exit 1 diff --git a/.github/workflows/update_composite_action.yml b/.github/workflows/update_composite_action.yml deleted file mode 100644 index 2b3d58d2..00000000 --- a/.github/workflows/update_composite_action.yml +++ /dev/null @@ -1,90 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# -name: Update Approved Patterns and Composite Action -on: - workflow_dispatch: - push: - branches: - - main - paths: - - "actions.yml" - pull_request: - paths: - - ".github/workflows/update_composite_action.yml" - - "actions.yml" - - gateway/* - -permissions: {} - -# We want workflows on main to run in order to avoid losing data through race conditions -concurrency: "${{ github.ref }}-${{ github.workflow }}" - -jobs: - update: - name: Update Workflow - runs-on: ubuntu-latest - steps: - - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: true - # We have to use a PAT to commit the workflow file - token: ${{ secrets.ALLOWLIST_WORKFLOW_TOKEN || github.token }} # zizmor: ignore[secrets-outside-env] - - - name: Print token details - if: ${{ github.event_name != 'pull_request' }} - env: - GH_TOKEN: ${{ secrets.ALLOWLIST_WORKFLOW_TOKEN }} # zizmor: ignore[secrets-outside-env] - run: | - echo "::group::Token details" - echo "Token user and permissions:" - gh api /user --jq '"Login: \(.login)\nName: \(.name)\nEmail: \(.email)"' - echo "" - echo "Token expiration:" - gh api /installation/token --jq '.expires_at' 2>/dev/null || echo "Token expiration not available (likely a PAT, not an installation token)" - echo "" - echo "Token scopes:" - curl -sS -H "Authorization: token ${GH_TOKEN}" -I https://api.github.com/ 2>/dev/null | grep -i 'x-oauth-scopes' || echo "No OAuth scopes header (fine-grained or app token)" - echo "::endgroup::" - - - run: pipx install uv - - - name: Update Workflow - run: | - uv run python << 'PYEOF' - import sys - sys.path.append("./gateway/") - - import gateway as g - g.update_workflow(".github/actions/for-dependabot-triggered-reviews/action.yml", "actions.yml") - g.update_patterns("approved_patterns.yml", "actions.yml") - PYEOF - - - name: Commit and push changes - if: ${{ github.event_name != 'pull_request' }} - env: - GH_TOKEN: ${{ secrets.ALLOWLIST_WORKFLOW_TOKEN || github.token }} # zizmor: ignore[secrets-outside-env] - run: | - AUTHOR_NAME=$(gh api /user --jq '.login' 2>/dev/null || echo "asfgit") - AUTHOR_EMAIL=$(gh api /user --jq '.email // "\(.login)@users.noreply.github.com"' 2>/dev/null || echo "asfgit@users.noreply.github.com") - git config --local user.name "${AUTHOR_NAME}" - git config --local user.email "${AUTHOR_EMAIL}" - git add -f .github/actions/for-dependabot-triggered-reviews/action.yml approved_patterns.yml - git commit -m "Update approved_patterns.yml and .github/actions/for-dependabot-triggered-reviews/action.yml based on actions.yml" -m "Generated by .github/workflows/update_composite_action.yml" || echo "No changes" - git push origin diff --git a/README.md b/README.md index 27e74991..b702db81 100644 --- a/README.md +++ b/README.md @@ -121,11 +121,11 @@ graph LR dependabot-->composite dependabot-.verified by.-verify["verify_dependabot_action.yml
(rebuild & diff)"] - composite=="update_actions.yml
(on merge)"==>actions + composite=="update_allowlist.yml
(on merge)"==>actions cron=="remove_expired.yml"==>actions - actions=="update_composite_action.yml"==>composite - actions=="update_composite_action.yml"==>approved + actions=="update_allowlist.yml"==>composite + actions=="update_allowlist.yml"==>approved guard["check_approved_limit.yml
(fails at 800 / 1000)"]-.monitors.-approved @@ -149,11 +149,11 @@ Solid arrows (`==>`) are workflow regeneration edges — the "source → generat ```mermaid graph TD; manual["manual PR"]--new entry-->actions.yml - actions.yml--"update_composite_action.yml"-->composite[".github/actions/for-dependabot-triggered-reviews/action.yml"] - actions.yml--"update_composite_action.yml"-->approved["approved_patterns.yml"] + actions.yml--"update_allowlist.yml"-->composite[".github/actions/for-dependabot-triggered-reviews/action.yml"] + actions.yml--"update_allowlist.yml"-->approved["approved_patterns.yml"] ``` -A human-authored PR edits `actions.yml` directly. Once it merges to `main`, the **`update_composite_action.yml`** workflow regenerates both `.github/actions/for-dependabot-triggered-reviews/action.yml` and `approved_patterns.yml` from the new entries, so contributors never have to hand-edit the generated files. +A human-authored PR edits `actions.yml` directly. Once it merges to `main`, the **`update_allowlist.yml`** workflow regenerates both `.github/actions/for-dependabot-triggered-reviews/action.yml` and `approved_patterns.yml` from the new entries, so contributors never have to hand-edit the generated files. To request addition of an action to the allow list: @@ -185,14 +185,14 @@ The infrastructure team will review your request and either approve, request cha graph TD; dependabot--"PR updates"-->composite[".github/actions/for-dependabot-triggered-reviews/action.yml"] dependabot-.verified by.-verify["verify_dependabot_action.yml"] - composite--"update_actions.yml (on merge)"-->actions.yml - actions.yml--"update_actions.yml"-->approved["approved_patterns.yml"] + composite--"update_allowlist.yml (on merge)"-->actions.yml + actions.yml--"update_allowlist.yml"-->approved["approved_patterns.yml"] ``` In most cases, new versions are automatically added through Dependabot: - Dependabot opens PRs against `.github/actions/for-dependabot-triggered-reviews/action.yml` to update actions to the newest releases - **`verify_dependabot_action.yml`** runs on each such PR, rebuilds the action's compiled JavaScript in Docker, and diffs it against the published version (see [Automated Verification in CI](#automated-verification-in-ci)) -- Once a reviewer merges the PR, **`update_actions.yml`** reflects the new commit SHAs back into `actions.yml` and regenerates `approved_patterns.yml` +- Once a reviewer merges the PR, **`update_allowlist.yml`** reflects the new commit SHAs back into `actions.yml` and regenerates `approved_patterns.yml` - The previously approved version is marked with an `expires_at` date 3 months out, giving projects a grace period to update their workflows; see [Automatic Expiration of Old Versions](#automatic-expiration-of-old-versions) for how the cleanup runs Projects are encouraged to help review updates to actions they use. Please have a look at the diff and mention in your approval what you have checked and why you think the action is safe. @@ -356,15 +356,15 @@ If you add older version of the action and want to set an expiration date for it ```mermaid graph TD; entry["actions.yml entry
with expires_at"]--"remove_expired.yml (daily, 02:04 UTC)"-->actions.yml - actions.yml--"update_composite_action.yml"-->composite[".github/actions/for-dependabot-triggered-reviews/action.yml"] - actions.yml--"update_composite_action.yml"-->approved["approved_patterns.yml"] + actions.yml--"update_allowlist.yml"-->composite[".github/actions/for-dependabot-triggered-reviews/action.yml"] + actions.yml--"update_allowlist.yml"-->approved["approved_patterns.yml"] ``` Routine cleanup of superseded versions is automated: - Any entry in `actions.yml` with an `expires_at: YYYY-MM-DD` field is a candidate for removal. - Dependabot-driven updates (see [Updating Version of Already Approved Action](#updating-version-of-already-approved-action)) set `expires_at` to **3 months out** on the previously approved version. For manually added older versions, set `expires_at` explicitly (see [Manual Addition of Specific Versions](#manual-addition-of-specific-versions)). -- The **`remove_expired.yml`** workflow runs daily at **02:04 UTC**. Every entry whose `expires_at` date has passed is deleted from `actions.yml`; the workflow then commits the change and lets `update_composite_action.yml` regenerate `approved_patterns.yml` and the dependabot composite. +- The **`remove_expired.yml`** workflow runs daily at **02:04 UTC**. Every entry whose `expires_at` date has passed is deleted from `actions.yml`; the workflow then commits the change and lets `update_allowlist.yml` regenerate `approved_patterns.yml` and the dependabot composite. - Entries without `expires_at` (for example, `keep: true` wildcards and the current approved version) are never auto-removed — removal of those requires a manual PR. No human action is required for the routine case: projects get a 3-month grace window after a version bump, and the old entry disappears on its own afterwards. From d63ca67d624ad20c8e30afdf8d27b58332419e3a Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Mon, 25 May 2026 17:13:04 +0200 Subject: [PATCH 2/3] docs: explain why update_allowlist.yml needs a PAT instead of GITHUB_TOKEN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Piotr's review on #876 flagged the two PAT references in update_allowlist.yml as needing an explanation. The rationale: when this workflow's final step pushes the regenerated actions.yml / approved_patterns.yml / composite back to main, the push needs to re-trigger downstream push-triggered workflows (e.g. check_approved_limit.yml watching approved_patterns.yml). GitHub deliberately suppresses that recursive trigger when the push is authenticated with the default GITHUB_TOKEN — see https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow#triggering-a-workflow-from-a-workflow * README "Updating version of already approved action" gets a NOTE callout citing the doc and explaining the fallback to github.token in forks. * Both PAT use sites in the workflow get an inline comment pointing at the same docs URL, so a reviewer doesn't need to grep the README to understand the choice. Generated-by: Claude Code (Claude Opus 4.7) --- .github/workflows/update_allowlist.yml | 11 +++++++++++ README.md | 3 +++ 2 files changed, 14 insertions(+) diff --git a/.github/workflows/update_allowlist.yml b/.github/workflows/update_allowlist.yml index 6d590fc2..f2177797 100644 --- a/.github/workflows/update_allowlist.yml +++ b/.github/workflows/update_allowlist.yml @@ -58,6 +58,13 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: true + # PAT (or GitHub App token) so the push at the end of this + # workflow re-triggers downstream push-triggered workflows + # (e.g. check_approved_limit.yml on approved_patterns.yml). + # GITHUB_TOKEN-authored pushes don't create new workflow runs, + # see https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow#triggering-a-workflow-from-a-workflow. + # Falls back to github.token in forks (downstream chain won't + # fire there — safe, but a manual re-run is needed). token: ${{ secrets.ALLOWLIST_WORKFLOW_TOKEN || github.token }} # zizmor: ignore[secrets-outside-env] # On push/dispatch, check out the current tip of main rather # than the trigger SHA. A queued run that started on an older @@ -115,6 +122,10 @@ jobs: - name: Commit and push changes if: ${{ github.event_name != 'pull_request' }} env: + # Same PAT/fallback as the checkout step: the push needs to + # re-trigger downstream push-triggered workflows, which a + # GITHUB_TOKEN push wouldn't. See the comment on the + # `actions/checkout` step above. GH_TOKEN: ${{ secrets.ALLOWLIST_WORKFLOW_TOKEN || github.token }} # zizmor: ignore[secrets-outside-env] run: | AUTHOR_NAME=$(gh api /user --jq '.login' 2>/dev/null || echo "asfgit") diff --git a/README.md b/README.md index b702db81..ed8518c9 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,9 @@ In most cases, new versions are automatically added through Dependabot: - Once a reviewer merges the PR, **`update_allowlist.yml`** reflects the new commit SHAs back into `actions.yml` and regenerates `approved_patterns.yml` - The previously approved version is marked with an `expires_at` date 3 months out, giving projects a grace period to update their workflows; see [Automatic Expiration of Old Versions](#automatic-expiration-of-old-versions) for how the cleanup runs +> [!NOTE] +> **Why `update_allowlist.yml` uses a PAT (`ALLOWLIST_WORKFLOW_TOKEN`) instead of `GITHUB_TOKEN`.** When the workflow commits the regenerated `actions.yml` / `approved_patterns.yml` / composite back to `main`, that push needs to re-trigger the downstream workflows that watch those files (e.g. `check_approved_limit.yml`, which runs on push to `approved_patterns.yml`, and any future audit workflow). GitHub deliberately suppresses the recursive trigger when the push is authenticated with the default `GITHUB_TOKEN`: per the [GitHub Actions docs](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow#triggering-a-workflow-from-a-workflow), *"when you use the repository's `GITHUB_TOKEN` to perform tasks, events triggered by the `GITHUB_TOKEN` will not create a new workflow run"*. Using a fine-grained PAT (or GitHub App installation token) stored in `ALLOWLIST_WORKFLOW_TOKEN` makes the push look like a regular commit to GitHub, so the downstream push-triggered workflows do run. The workflow falls back to `github.token` when the secret is absent (e.g. in forks), in which case the downstream chain simply won't fire — that's safe but means a manual re-run is needed. + Projects are encouraged to help review updates to actions they use. Please have a look at the diff and mention in your approval what you have checked and why you think the action is safe. #### Verifying Compiled JavaScript From 0dca159b7f0a9613762e1369e3fa80f26fe735f5 Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Mon, 25 May 2026 17:28:54 +0200 Subject: [PATCH 3/3] workflows: inline approved-cap check, drop PAT, consolidate check_approved_limit.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up from Piotr's review on #876: rather than document why the PAT is needed, eliminate the need for it. The PAT existed for exactly one downstream workflow: check_approved_limit.yml, which fires on push to approved_patterns.yml to enforce the 800/1000-entry cap. By inlining that check as a step inside the update job (running after regeneration, before commit/push) the cap is enforced before any over-cap state can land on main, and no downstream push-triggered workflow needs to fire — so the workflow can authenticate with the default GITHUB_TOKEN. Changes: * update_allowlist.yml: new "Check approved actions count" step; drops both ALLOWLIST_WORKFLOW_TOKEN references (checkout + push); drops the now-pointless "Print token details" diagnostic step. * check_approved_limit.yml: deleted (consolidated into the update job above). * README: removes the PAT NOTE callout; replaces the cap guard NOTE with an explanation of the inlined step; rewrites all four mermaid diagrams to label edges with job names (update / verify / clean_up) rather than workflow filenames, with workflow filenames mentioned in the prose for grep-ability. Generated-by: Claude Code (Claude Opus 4.7) --- .github/workflows/check_approved_limit.yml | 54 ---------------------- .github/workflows/update_allowlist.yml | 52 +++++++++------------ README.md | 53 ++++++++++----------- 3 files changed, 47 insertions(+), 112 deletions(-) delete mode 100644 .github/workflows/check_approved_limit.yml diff --git a/.github/workflows/check_approved_limit.yml b/.github/workflows/check_approved_limit.yml deleted file mode 100644 index b6aed9af..00000000 --- a/.github/workflows/check_approved_limit.yml +++ /dev/null @@ -1,54 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# -name: Check Approved Actions Limit - -on: - push: - branches: - - main - paths: - - approved_patterns.yml - pull_request: - paths: - - approved_patterns.yml - - .github/workflows/check_approved_limit.yml - -permissions: - contents: read - -jobs: - check-limit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Check approved actions count - run: | - # The org-wide approved_patterns list has a hard limit of 1000 entries. - # We fail at 800 to give ourselves room to act before hitting the wall. - # See https://github.com/apache/infrastructure-actions/issues/602 - LIMIT=800 - COUNT=$(grep -c '^- ' approved_patterns.yml) - echo "Approved actions count: $COUNT / 1000 (warning threshold: $LIMIT)" - if [ "$COUNT" -ge "$LIMIT" ]; then - echo "::error::Approved actions count ($COUNT) has reached the warning threshold of $LIMIT. The org-wide limit is 1000. Time to clean up unused actions or explore workarounds. See https://github.com/apache/infrastructure-actions/issues/602" - exit 1 - fi diff --git a/.github/workflows/update_allowlist.yml b/.github/workflows/update_allowlist.yml index f2177797..70121163 100644 --- a/.github/workflows/update_allowlist.yml +++ b/.github/workflows/update_allowlist.yml @@ -22,6 +22,12 @@ # overlapping pushes, silently overwriting one direction's edit. This # single workflow always runs both directions in order, so neither edit # is lost regardless of which file triggered the run. See #866. +# +# Also subsumes the former `check_approved_limit.yml`: the 800/1000-entry +# cap on approved_patterns.yml is now enforced inline (see the "Check +# approved actions count" step). Folding it in here means no downstream +# push-triggered workflow needs to fire after this workflow commits, so +# the workflow can authenticate with the default GITHUB_TOKEN — no PAT. name: Update Allowlist on: workflow_dispatch: @@ -58,36 +64,12 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: true - # PAT (or GitHub App token) so the push at the end of this - # workflow re-triggers downstream push-triggered workflows - # (e.g. check_approved_limit.yml on approved_patterns.yml). - # GITHUB_TOKEN-authored pushes don't create new workflow runs, - # see https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow#triggering-a-workflow-from-a-workflow. - # Falls back to github.token in forks (downstream chain won't - # fire there — safe, but a manual re-run is needed). - token: ${{ secrets.ALLOWLIST_WORKFLOW_TOKEN || github.token }} # zizmor: ignore[secrets-outside-env] # On push/dispatch, check out the current tip of main rather # than the trigger SHA. A queued run that started on an older # SHA would otherwise regenerate from stale inputs and either # undo the prior run's commit or fail to push. ref: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && 'main' || '' }} - - name: Print token details - if: ${{ github.event_name != 'pull_request' }} - env: - GH_TOKEN: ${{ secrets.ALLOWLIST_WORKFLOW_TOKEN }} # zizmor: ignore[secrets-outside-env] - run: | - echo "::group::Token details" - echo "Token user and permissions:" - gh api /user --jq '"Login: \(.login)\nName: \(.name)\nEmail: \(.email)"' - echo "" - echo "Token expiration:" - gh api /installation/token --jq '.expires_at' 2>/dev/null || echo "Token expiration not available (likely a PAT, not an installation token)" - echo "" - echo "Token scopes:" - curl -sS -H "Authorization: token ${GH_TOKEN}" -I https://api.github.com/ 2>/dev/null | grep -i 'x-oauth-scopes' || echo "No OAuth scopes header (fine-grained or app token)" - echo "::endgroup::" - - run: pipx install uv - name: Sync composite action, actions.yml, and approved_patterns.yml @@ -119,14 +101,26 @@ jobs: g.update_patterns(patterns, actions) PYEOF + - name: Check approved actions count + # Inlined from the former check_approved_limit.yml. The org-wide + # approved_patterns list has a hard limit of 1000 entries; we + # fail at 800 to give maintainers room to act before hitting the + # wall. Runs after regeneration and before commit/push, so an + # over-cap state never lands on main. See + # https://github.com/apache/infrastructure-actions/issues/602 + run: | + LIMIT=800 + COUNT=$(grep -c '^- ' approved_patterns.yml) + echo "Approved actions count: $COUNT / 1000 (warning threshold: $LIMIT)" + if [ "$COUNT" -ge "$LIMIT" ]; then + echo "::error::Approved actions count ($COUNT) has reached the warning threshold of $LIMIT. The org-wide limit is 1000. Time to clean up unused actions or explore workarounds. See https://github.com/apache/infrastructure-actions/issues/602" + exit 1 + fi + - name: Commit and push changes if: ${{ github.event_name != 'pull_request' }} env: - # Same PAT/fallback as the checkout step: the push needs to - # re-trigger downstream push-triggered workflows, which a - # GITHUB_TOKEN push wouldn't. See the comment on the - # `actions/checkout` step above. - GH_TOKEN: ${{ secrets.ALLOWLIST_WORKFLOW_TOKEN || github.token }} # zizmor: ignore[secrets-outside-env] + GH_TOKEN: ${{ github.token }} run: | AUTHOR_NAME=$(gh api /user --jq '.login' 2>/dev/null || echo "asfgit") AUTHOR_EMAIL=$(gh api /user --jq '.email // "\(.login)@users.noreply.github.com"' 2>/dev/null || echo "asfgit@users.noreply.github.com") diff --git a/README.md b/README.md index ed8518c9..9cf5c050 100644 --- a/README.md +++ b/README.md @@ -119,41 +119,39 @@ graph LR human-->actions dependabot-->composite - dependabot-.verified by.-verify["verify_dependabot_action.yml
(rebuild & diff)"] + dependabot-.verified by.-verify["verify job
(rebuild & diff)"] - composite=="update_allowlist.yml
(on merge)"==>actions - cron=="remove_expired.yml"==>actions + composite=="update job
(on merge)"==>actions + cron=="clean_up job"==>actions - actions=="update_allowlist.yml"==>composite - actions=="update_allowlist.yml"==>approved - - guard["check_approved_limit.yml
(fails at 800 / 1000)"]-.monitors.-approved + actions=="update job"==>composite + actions=="update job
(cap check + regen)"==>approved classDef source fill:#fff3b0,stroke:#8a6d0b,color:#333 classDef generated fill:#e0f0ff,stroke:#2563a6,color:#333 classDef trigger fill:#f3e0ff,stroke:#6a1b9a,color:#333 - classDef workflow fill:#e6ffe6,stroke:#1b5e20,color:#333 + classDef job fill:#e6ffe6,stroke:#1b5e20,color:#333 class actions source class composite,approved generated class human,dependabot,cron trigger - class verify,guard workflow + class verify job ``` -Solid arrows (`==>`) are workflow regeneration edges — the "source → generated" flows that keep `actions.yml`, `approved_patterns.yml` and the dependabot composite in sync. Thin arrows feed the pipeline with new content (human or Dependabot PRs, cron), and dotted arrows are observer workflows that verify or guard rather than mutate. +Solid arrows (`==>`) are regeneration edges — the "source → generated" flows that keep `actions.yml`, `approved_patterns.yml` and the dependabot composite in sync. Thin arrows feed the pipeline with new content (human or Dependabot PRs, cron), and dotted arrows are observer jobs that verify rather than mutate. Bold labels are job names (rather than workflow filenames) — `update` lives in `update_allowlist.yml`, `verify` in `verify_dependabot_action.yml` / `verify_manual_action.yml`, `clean_up` in `remove_expired.yml`. > [!NOTE] -> `check_approved_limit.yml` guards the whole pipeline: the org-wide allow list has a hard cap of 1000 entries, and this workflow fails at 800 to give maintainers room to clean up before hitting the wall. +> The 800/1000-entry cap on `approved_patterns.yml` is enforced as a step inside the `update` job (formerly the separate `check_approved_limit.yml`). It runs after regeneration and before commit/push, so an over-cap state never lands on `main`. Folding the check in here also lets the workflow push with the default `GITHUB_TOKEN` — no PAT, no downstream-trigger requirement. ### Adding a New Action to the Allow List ```mermaid graph TD; manual["manual PR"]--new entry-->actions.yml - actions.yml--"update_allowlist.yml"-->composite[".github/actions/for-dependabot-triggered-reviews/action.yml"] - actions.yml--"update_allowlist.yml"-->approved["approved_patterns.yml"] + actions.yml--"update job"-->composite[".github/actions/for-dependabot-triggered-reviews/action.yml"] + actions.yml--"update job"-->approved["approved_patterns.yml"] ``` -A human-authored PR edits `actions.yml` directly. Once it merges to `main`, the **`update_allowlist.yml`** workflow regenerates both `.github/actions/for-dependabot-triggered-reviews/action.yml` and `approved_patterns.yml` from the new entries, so contributors never have to hand-edit the generated files. +A human-authored PR edits `actions.yml` directly. Once it merges to `main`, the **`update`** job (in `update_allowlist.yml`) regenerates both `.github/actions/for-dependabot-triggered-reviews/action.yml` and `approved_patterns.yml` from the new entries, so contributors never have to hand-edit the generated files. To request addition of an action to the allow list: @@ -184,20 +182,17 @@ The infrastructure team will review your request and either approve, request cha ```mermaid graph TD; dependabot--"PR updates"-->composite[".github/actions/for-dependabot-triggered-reviews/action.yml"] - dependabot-.verified by.-verify["verify_dependabot_action.yml"] - composite--"update_allowlist.yml (on merge)"-->actions.yml - actions.yml--"update_allowlist.yml"-->approved["approved_patterns.yml"] + dependabot-.verified by.-verify["verify job"] + composite--"update job (on merge)"-->actions.yml + actions.yml--"update job"-->approved["approved_patterns.yml"] ``` In most cases, new versions are automatically added through Dependabot: - Dependabot opens PRs against `.github/actions/for-dependabot-triggered-reviews/action.yml` to update actions to the newest releases -- **`verify_dependabot_action.yml`** runs on each such PR, rebuilds the action's compiled JavaScript in Docker, and diffs it against the published version (see [Automated Verification in CI](#automated-verification-in-ci)) -- Once a reviewer merges the PR, **`update_allowlist.yml`** reflects the new commit SHAs back into `actions.yml` and regenerates `approved_patterns.yml` +- The **`verify`** job (in `verify_dependabot_action.yml`) runs on each such PR, rebuilds the action's compiled JavaScript in Docker, and diffs it against the published version (see [Automated Verification in CI](#automated-verification-in-ci)) +- Once a reviewer merges the PR, the **`update`** job (in `update_allowlist.yml`) reflects the new commit SHAs back into `actions.yml`, regenerates `approved_patterns.yml`, and enforces the 800/1000-entry cap inline before pushing - The previously approved version is marked with an `expires_at` date 3 months out, giving projects a grace period to update their workflows; see [Automatic Expiration of Old Versions](#automatic-expiration-of-old-versions) for how the cleanup runs -> [!NOTE] -> **Why `update_allowlist.yml` uses a PAT (`ALLOWLIST_WORKFLOW_TOKEN`) instead of `GITHUB_TOKEN`.** When the workflow commits the regenerated `actions.yml` / `approved_patterns.yml` / composite back to `main`, that push needs to re-trigger the downstream workflows that watch those files (e.g. `check_approved_limit.yml`, which runs on push to `approved_patterns.yml`, and any future audit workflow). GitHub deliberately suppresses the recursive trigger when the push is authenticated with the default `GITHUB_TOKEN`: per the [GitHub Actions docs](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow#triggering-a-workflow-from-a-workflow), *"when you use the repository's `GITHUB_TOKEN` to perform tasks, events triggered by the `GITHUB_TOKEN` will not create a new workflow run"*. Using a fine-grained PAT (or GitHub App installation token) stored in `ALLOWLIST_WORKFLOW_TOKEN` makes the push look like a regular commit to GitHub, so the downstream push-triggered workflows do run. The workflow falls back to `github.token` when the secret is absent (e.g. in forks), in which case the downstream chain simply won't fire — that's safe but means a manual re-run is needed. - Projects are encouraged to help review updates to actions they use. Please have a look at the diff and mention in your approval what you have checked and why you think the action is safe. #### Verifying Compiled JavaScript @@ -276,8 +271,8 @@ The `--no-gh` mode supports all the same features as the default `gh`-based mode Two workflows in `.github/workflows/` run `verify-action-build` on PRs that touch the allow list, so the verification status is visible on every PR as a required-candidate status check: -- **`verify_dependabot_action.yml`** — triggers on Dependabot PRs that modify `.github/actions/for-dependabot-triggered-reviews/action.yml`. Extracts the action reference from the PR, rebuilds the compiled JavaScript in Docker, and compares it against the published version. -- **`verify_manual_action.yml`** — triggers on human-authored PRs that modify `actions.yml` or `approved_patterns.yml` (i.e. manual allow-list additions / version bumps). Dependabot-authored PRs are skipped, since they are already covered by the workflow above. +- **`verify` job in `verify_dependabot_action.yml`** — triggers on Dependabot PRs that modify `.github/actions/for-dependabot-triggered-reviews/action.yml`. Extracts the action reference from the PR, rebuilds the compiled JavaScript in Docker, and compares it against the published version. +- **`verify` job in `verify_manual_action.yml`** — triggers on human-authored PRs that modify `actions.yml` or `approved_patterns.yml` (i.e. manual allow-list additions / version bumps). Dependabot-authored PRs are skipped, since they are already covered by the workflow above. Both workflows use a regular `pull_request` trigger with read-only permissions and no PR comments — pass/fail is surfaced through the status check. Neither workflow auto-approves or merges; a human reviewer must still approve. @@ -358,23 +353,23 @@ If you add older version of the action and want to set an expiration date for it ```mermaid graph TD; - entry["actions.yml entry
with expires_at"]--"remove_expired.yml (daily, 02:04 UTC)"-->actions.yml - actions.yml--"update_allowlist.yml"-->composite[".github/actions/for-dependabot-triggered-reviews/action.yml"] - actions.yml--"update_allowlist.yml"-->approved["approved_patterns.yml"] + entry["actions.yml entry
with expires_at"]--"clean_up job (daily, 02:04 UTC)"-->actions.yml + actions.yml--"update job"-->composite[".github/actions/for-dependabot-triggered-reviews/action.yml"] + actions.yml--"update job"-->approved["approved_patterns.yml"] ``` Routine cleanup of superseded versions is automated: - Any entry in `actions.yml` with an `expires_at: YYYY-MM-DD` field is a candidate for removal. - Dependabot-driven updates (see [Updating Version of Already Approved Action](#updating-version-of-already-approved-action)) set `expires_at` to **3 months out** on the previously approved version. For manually added older versions, set `expires_at` explicitly (see [Manual Addition of Specific Versions](#manual-addition-of-specific-versions)). -- The **`remove_expired.yml`** workflow runs daily at **02:04 UTC**. Every entry whose `expires_at` date has passed is deleted from `actions.yml`; the workflow then commits the change and lets `update_allowlist.yml` regenerate `approved_patterns.yml` and the dependabot composite. +- The **`clean_up`** job (in `remove_expired.yml`) runs daily at **02:04 UTC**. Every entry whose `expires_at` date has passed is deleted from `actions.yml`; the job then commits the change and lets the `update` job in `update_allowlist.yml` regenerate `approved_patterns.yml` and the dependabot composite. - Entries without `expires_at` (for example, `keep: true` wildcards and the current approved version) are never auto-removed — removal of those requires a manual PR. No human action is required for the routine case: projects get a 3-month grace window after a version bump, and the old entry disappears on its own afterwards. ### Removing a version manually -Routine removal is already automated: set `expires_at` on the entry and the daily `remove_expired.yml` workflow will delete it once the date passes. Use the manual process below only when you need an immediate removal that can't wait for the entry to expire. +Routine removal is already automated: set `expires_at` on the entry and the daily `clean_up` job (in `remove_expired.yml`) will delete it once the date passes. Use the manual process below only when you need an immediate removal that can't wait for the entry to expire. > [!IMPORTANT] > If a version or entire action needs to be removed immediately due to a security vulnerability: