diff --git a/.asf.yaml b/.asf.yaml index f6264b23..bc83dfbf 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -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 diff --git a/.github/workflows/allowlist_approval.yml b/.github/workflows/allowlist_approval.yml index b472bf7c..b9bc13b0 100644 --- a/.github/workflows/allowlist_approval.yml +++ b/.github/workflows/allowlist_approval.yml @@ -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. diff --git a/.github/workflows/verify_dependabot_action.yml b/.github/workflows/verify_dependabot_action.yml index 8bc101c5..1046259e 100644 --- a/.github/workflows/verify_dependabot_action.yml +++ b/.github/workflows/verify_dependabot_action.yml @@ -20,8 +20,6 @@ name: Verify Dependabot Action Build on: pull_request: - paths: - - .github/actions/for-dependabot-triggered-reviews/action.yml permissions: contents: read @@ -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 diff --git a/.github/workflows/verify_manual_action.yml b/.github/workflows/verify_manual_action.yml index e719552a..280e5546 100644 --- a/.github/workflows/verify_manual_action.yml +++ b/.github/workflows/verify_manual_action.yml @@ -26,9 +26,6 @@ name: Verify Manual Allowlist PR on: pull_request: - paths: - - actions.yml - - approved_patterns.yml permissions: contents: read @@ -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 diff --git a/README.md b/README.md index b8b76e56..47b3f103 100644 --- a/README.md +++ b/README.md @@ -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.