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
35 changes: 35 additions & 0 deletions .asf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,41 @@ github:
required_status_checks:
# strict means "Require branches to be up to date before merging".
strict: false
# The checks every PR must pass to merge. A ruleset rather than
# protected_branches, because only a ruleset can name who may bypass it.
rulesets:
- name: "Required checks"
target: branch
enforcement: active
conditions:
ref_name:
include:
- "~DEFAULT_BRANCH"
exclude: []
bypass_actors:
# Repository admins, always: update.yml pushes its sync commit to
# main directly as asfgit, which is an admin.
- actor_id: 5
actor_type: RepositoryRole
bypass_mode: always
# Write access, on pull requests only: a maintainer can choose to
# merge past the checks, never push past them.
- actor_id: 4
actor_type: RepositoryRole
bypass_mode: pull_request
rules:
- type: required_status_checks
parameters:
strict_required_status_checks_policy: false
required_status_checks:
# allowlist_approval.yml: an allow-list PR needs a maintainer's
# approval; other PRs pass it at once.
- context: approval
integration_id: -1
# verify_*_action.yml: rebuilds an allow-list PR's action and
# compares it with the published one; other PRs pass it at once.
- context: verify
integration_id: -1

notifications:
commits: commits@infra.apache.org
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/allowlist_approval.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
#
# Fails PRs that touch the allow list (actions.yml, approved_patterns.yml or
# the generated Dependabot composite action) until at least one maintainer —
# a reviewer with write access — has approved. Other PRs pass immediately,
# so the check can be required on every PR.
# a reviewer with write access — has approved. Other PRs pass
# immediately, so the check is required on every PR (.asf.yaml).
#
# Re-runs on review events so the status flips as soon as an approval lands
# or is dismissed.
Expand Down
44 changes: 42 additions & 2 deletions .github/workflows/verify_dependabot_action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ name: Verify Dependabot Action Build

on:
pull_request:
paths:
- .github/actions/for-dependabot-triggered-reviews/action.yml

permissions:
contents: read
Expand All @@ -32,13 +30,55 @@ jobs:
if: github.event.pull_request.user.login == 'dependabot[bot]'
runs-on: ubuntu-26.04
steps:
# Required on every PR (.asf.yaml), so it runs on every PR, but only an
# allow-list PR is verified: for any other PR this step finds nothing to
# do and every step after it is skipped. Deciding here, not in a job
# this one needs, keeps it fail-closed: a failed lookup fails `verify`,
# where a failed upstream job would only skip it, and a skipped job
# passes a required check. A file list the API may have cut short (it
# stops at 3000) is verified, not skipped.
- name: Does this PR touch the allow list?
id: scope
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
# The whole allow list, as check_allowlist_approval.py has it, in
# both verify workflows: each job runs for one kind of author, so
# the one that runs must watch every allow-list file.
WATCHED: |
actions.yml
approved_patterns.yml
.github/actions/for-dependabot-triggered-reviews/action.yml
run: |
# Names compared as JSON strings, one per line, so a file name with
# a newline in it can neither hide a match nor fake one. Entries
# are counted, not lines, for the same reason.
files="$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" --paginate \
--jq '.[] | (.filename, (.previous_filename // empty)) | @json')"
listed="$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" --paginate --jq '.[] | 1' | wc -l)"
changed="$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.changed_files')"
watched="$(printf '%s' "${WATCHED}" | sed '/^$/d' | jq -R '.')"
if [ "${listed}" -lt "${changed}" ] || [ "${changed}" -ge 3000 ]; then
echo "The file list may be incomplete (${listed} of ${changed}); verifying to be safe."
echo "run=true" >> "$GITHUB_OUTPUT"
elif grep -Fxq -f <(printf '%s\n' "${watched}") <<< "${files}"; then
echo "run=true" >> "$GITHUB_OUTPUT"
else
echo "Not an allow-list PR; nothing to verify."
echo "run=false" >> "$GITHUB_OUTPUT"
fi

- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
if: steps.scope.outputs.run == 'true'
with:
persist-credentials: false

- run: pipx install uv
if: steps.scope.outputs.run == 'true'

- name: Verify action build
if: steps.scope.outputs.run == 'true'
run: |
# Disable workflow command processing so that strings like
# "##[add-matcher]" or "::error::" in action diffs are not
Expand Down
45 changes: 42 additions & 3 deletions .github/workflows/verify_manual_action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@ name: Verify Manual Allowlist PR

on:
pull_request:
paths:
- actions.yml
- approved_patterns.yml

permissions:
contents: read
Expand All @@ -39,13 +36,55 @@ jobs:
if: github.event.pull_request.user.login != 'dependabot[bot]'
runs-on: ubuntu-26.04
steps:
# Required on every PR (.asf.yaml), so it runs on every PR, but only an
# allow-list PR is verified: for any other PR this step finds nothing to
# do and every step after it is skipped. Deciding here, not in a job
# this one needs, keeps it fail-closed: a failed lookup fails `verify`,
# where a failed upstream job would only skip it, and a skipped job
# passes a required check. A file list the API may have cut short (it
# stops at 3000) is verified, not skipped.
- name: Does this PR touch the allow list?
id: scope
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
# The whole allow list, as check_allowlist_approval.py has it, in
# both verify workflows: each job runs for one kind of author, so
# the one that runs must watch every allow-list file.
WATCHED: |
actions.yml
approved_patterns.yml
.github/actions/for-dependabot-triggered-reviews/action.yml
run: |
# Names compared as JSON strings, one per line, so a file name with
# a newline in it can neither hide a match nor fake one. Entries
# are counted, not lines, for the same reason.
files="$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" --paginate \
--jq '.[] | (.filename, (.previous_filename // empty)) | @json')"
listed="$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" --paginate --jq '.[] | 1' | wc -l)"
changed="$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.changed_files')"
watched="$(printf '%s' "${WATCHED}" | sed '/^$/d' | jq -R '.')"
if [ "${listed}" -lt "${changed}" ] || [ "${changed}" -ge 3000 ]; then
echo "The file list may be incomplete (${listed} of ${changed}); verifying to be safe."
echo "run=true" >> "$GITHUB_OUTPUT"
elif grep -Fxq -f <(printf '%s\n' "${watched}") <<< "${files}"; then
echo "run=true" >> "$GITHUB_OUTPUT"
else
echo "Not an allow-list PR; nothing to verify."
echo "run=false" >> "$GITHUB_OUTPUT"
fi

- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
if: steps.scope.outputs.run == 'true'
with:
persist-credentials: false

- run: pipx install uv
if: steps.scope.outputs.run == 'true'

- name: Verify action build
if: steps.scope.outputs.run == 'true'
run: |
# Disable workflow command processing so that strings like
# "##[add-matcher]" or "::error::" in action diffs are not
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,9 +353,11 @@ The workflows below run on PRs that touch the allow list, so the verification an

- **`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 `verify` jobs start on every PR, because `verify` is required on `main` by the `Required checks` ruleset in `.asf.yaml`, and a check that never starts would block a PR forever. Their first step asks the API for the PR's changed files; a PR that touches none of the three allow-list files (`actions.yml`, `approved_patterns.yml`, the Dependabot composite action) passes at once, with no checkout or build. Both jobs watch all three, so whichever runs for the PR's author covers every allow-list file. The decision is made inside the job, so a failed lookup fails `verify` rather than skipping it, and a file list the API may have cut short is verified rather than skipped.
- **`check_action_tags` job in `check_action_tags.yml`** — triggers when `actions.yml`, `approved_patterns.yml`, the generated Dependabot composite action, the update workflow, or gateway verification code changes. It verifies that configured action SHAs exist and, when a `tag` is recorded, that the SHA is reachable from that Git tag or branch.

- **`approval` job in `allowlist_approval.yml`** — runs on every PR and on every review submitted, edited or dismissed. If the PR touches `actions.yml`, `approved_patterns.yml` or the generated Dependabot composite action, it fails until at least one maintainer (a reviewer with write access to this repository) has approved; only each reviewer's latest approve / request-changes / dismissal counts. Other PRs pass immediately. The logic lives in `scripts/check_allowlist_approval.py`, which the job runs from the PR's base commit so a PR cannot rewrite its own gate.
- **`approval` job in `allowlist_approval.yml`** — runs on every PR and on every review submitted, edited or dismissed. If the PR touches `actions.yml`, `approved_patterns.yml` or the generated Dependabot composite action, it fails until at least one maintainer (a reviewer with write access to this repository) has approved; only each reviewer's latest approve / request-changes / dismissal counts. Other PRs pass immediately. The check is required on `main` by the `Required checks` ruleset in `.asf.yaml`, so an allow-list PR cannot be merged unapproved. People with write access can deliberately merge past it ("bypass rules" on the PR), but cannot push past it; repository admins bypass it outright, which is how `update.yml`'s sync commit, pushed as the admin `asfgit`, still lands. The logic lives in `scripts/check_allowlist_approval.py`, which the job runs from the PR's base commit so a PR cannot rewrite its own gate.

These workflows use regular `pull_request` (and `pull_request_review`) triggers with read-only permissions and no PR comments — pass/fail is surfaced through the status check. They do not auto-approve or merge; a human reviewer must still approve.

Expand Down