ci: unify trusted contributor PR automation (#2996)

This commit is contained in:
Carlos Florêncio
2026-08-24 19:11:29 +00:00
committed by GitHub
parent c325169d8d
commit 727d59ce71
20 changed files with 863 additions and 62 deletions
@@ -41,6 +41,7 @@ class ClaudeCodeReviewWorkflowContractTest(unittest.TestCase):
activity_types(workflow, "pull_request_target"),
)
self.assertNotIn(" strip-safe-to-review:", workflow)
self.assertEqual(workflow.count("persist-credentials: false"), 2)
def test_fork_review_uses_only_open_or_safe_to_review_label(self) -> None:
workflow = WORKFLOW.read_text(encoding="utf-8")
@@ -137,7 +138,7 @@ class ClaudeCodeReviewWorkflowContractTest(unittest.TestCase):
"to Claude's allowed_non_write_users input",
)
def test_allowlisted_fork_label_job_adds_both_approval_labels(self) -> None:
def test_allowlisted_fork_label_job_adds_one_approval_label(self) -> None:
workflow = WORKFLOW.read_text(encoding="utf-8")
_, separator, label_job = workflow.partition(" label-allowlisted-fork:")
label_job = label_job.partition("\n claude-review-fork:")[0]
@@ -158,9 +159,10 @@ class ClaudeCodeReviewWorkflowContractTest(unittest.TestCase):
self.assertIn("pull-requests: write", label_job)
self.assertIn("github.rest.issues.addLabels", label_job)
self.assertIn(
"const labels = ['safe-to-review', 'safe-to-test'];",
"const labels = ['safe-to-review'];",
label_job,
)
self.assertNotIn("safe-to-test", label_job)
self.assertNotIn("actions/checkout", label_job)
def test_lint_workflow_runs_preview_contract_test(self) -> None:
@@ -35,6 +35,7 @@ class OpenCodeCodeReviewWorkflowContractTest(unittest.TestCase):
workflow = WORKFLOW.read_text(encoding="utf-8")
self.assertNotIn(" strip-safe-to-review:", workflow)
self.assertNotIn("safe-to-test", workflow)
self.assertNotIn("github.rest.issues.removeLabel", workflow)
def test_lint_workflow_runs_opencode_contract_test(self) -> None:
@@ -51,7 +51,7 @@ class PRWalkthroughWorkflowContractTest(unittest.TestCase):
self.assertIn("vars.PR_WALKTHROUGH_ENABLED == 'true'", self.publication)
self.assertNotIn("OPENCODE_REVIEW_ENABLED", self.workflow)
def test_generation_is_limited_to_authorized_same_repository_events(self) -> None:
def test_generation_is_limited_to_authorized_repository_events(self) -> None:
self.assertIn("pull_request_target:", self.workflow)
self.assertIn(
"types: [opened, ready_for_review, reopened, synchronize, labeled]",
@@ -61,13 +61,20 @@ class PRWalkthroughWorkflowContractTest(unittest.TestCase):
"github.event_name == 'pull_request_target'",
"github.event.pull_request.draft == false",
"github.event.pull_request.head.repo.full_name == github.repository",
"github.event.pull_request.head.repo.full_name != github.repository",
"github.event.action == 'opened'",
"github.event.action == 'reopened'",
"github.event.action == 'ready_for_review'",
"github.event.action == 'synchronize'",
"github.event.action == 'labeled' && github.event.label.name == 'generate-pr-walkthrough'",
"github.event.label.name == 'safe-to-review'",
"github.event.action != 'labeled'",
"contains(github.event.pull_request.labels.*.name, 'safe-to-review')",
"vars.CLAUDE_REVIEW_ALLOWLIST != ''",
"contains(fromJSON(vars.CLAUDE_REVIEW_ALLOWLIST), github.event.pull_request.user.login)",
):
self.assertIn(condition, self.generation)
self.assertNotIn("safe-to-test", self.generation)
def test_skill_explains_trusted_context_without_provider_names(self) -> None:
self.assertNotIn("Kandev", self.skill)
@@ -94,7 +101,11 @@ class PRWalkthroughWorkflowContractTest(unittest.TestCase):
self.assertIn('test "$(git rev-parse HEAD)" = "$TRUSTED_SHA"', self.generation)
self.assertIn(
'git fetch --no-tags --filter=blob:none '
'--negotiation-tip="$TRUSTED_SHA" origin "$HEAD_SHA"',
'--negotiation-tip="$TRUSTED_SHA" origin "refs/pull/${PR_NUMBER}/head"',
self.generation,
)
self.assertIn(
'PR_NUMBER: ${{ github.event.pull_request.number }}',
self.generation,
)
self.assertNotIn("--depth=1", self.generation)
@@ -109,7 +120,7 @@ class PRWalkthroughWorkflowContractTest(unittest.TestCase):
self.assertNotIn("github.event.pull_request.base.sha", job)
for value in (
'git fetch --no-tags --filter=blob:none --negotiation-tip="$TRUSTED_SHA" origin "$HEAD_SHA"',
'git fetch --no-tags --filter=blob:none --negotiation-tip="$TRUSTED_SHA" origin "refs/pull/${PR_NUMBER}/head"',
'git merge-base "$TRUSTED_SHA" "$HEAD_SHA"',
'git archive "$TRUSTED_SHA" .agents/skills/pr-walkthrough | tar -x',
'--base-sha "$TRUSTED_SHA"',
@@ -10,7 +10,7 @@ WORKFLOW = REPO_ROOT / ".github" / "workflows" / "preview-env.yml"
class PreviewEnvironmentWorkflowContractTest(unittest.TestCase):
def test_claude_allowlist_authorizes_fork_preview_deployment(self) -> None:
def test_safe_to_review_and_allowlists_authorize_fork_preview_deployment(self) -> None:
workflow = WORKFLOW.read_text(encoding="utf-8")
_, separator, deploy_job = workflow.partition(" deploy-fork:")
@@ -21,7 +21,7 @@ class PreviewEnvironmentWorkflowContractTest(unittest.TestCase):
deploy_job,
)
self.assertIn(
"contains(github.event.pull_request.labels.*.name, 'safe-to-test')",
"contains(github.event.pull_request.labels.*.name, 'safe-to-review')",
deploy_job,
)
self.assertIn("vars.PREVIEW_ENV_ALLOWLIST != ''", deploy_job)
@@ -30,16 +30,18 @@ class PreviewEnvironmentWorkflowContractTest(unittest.TestCase):
"contains(fromJSON(vars.CLAUDE_REVIEW_ALLOWLIST), github.event.pull_request.user.login)",
deploy_job,
)
self.assertEqual(workflow.count("persist-credentials: false"), 3)
def test_safe_to_test_approval_survives_follow_up_pushes(self) -> None:
def test_safe_to_review_approval_survives_follow_up_pushes(self) -> None:
workflow = WORKFLOW.read_text(encoding="utf-8")
_, separator, deploy_job = workflow.partition(" deploy-fork:")
self.assertTrue(separator, "Fork preview deploy job is missing")
self.assertIn(
"((contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||",
"((contains(github.event.pull_request.labels.*.name, 'safe-to-review')) ||",
deploy_job,
)
self.assertNotIn("safe-to-test", deploy_job)
self.assertNotIn(" strip-safe-to-test:", workflow)
self.assertNotIn("github.rest.issues.removeLabel", workflow)
+8 -5
View File
@@ -43,6 +43,7 @@ jobs:
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 1
persist-credentials: false
- name: Run Claude Code Review (App token, inline comments)
id: claude-review
@@ -73,9 +74,10 @@ jobs:
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"
--disallowedTools "Edit,Write,MultiEdit,NotebookEdit,WebFetch,WebSearch,Bash(git add:*),Bash(git commit:*),Bash(git push:*),Bash(git rebase:*),Bash(git reset:*),Bash(gh pr edit:*),Bash(curl:*),Bash(wget:*)"
# Mark allowlisted fork PRs with the approval labels used by the review and
# preview workflows. The direct allowlist gates remain necessary because
# labels written with GITHUB_TOKEN do not start another workflow run.
# Mark allowlisted fork PRs with the single approval label used by the
# review, preview, and walkthrough workflows. The direct allowlist gates
# remain necessary because labels written with GITHUB_TOKEN do not start
# another workflow run.
label-allowlisted-fork:
if: >
github.event_name == 'pull_request_target' &&
@@ -92,11 +94,11 @@ jobs:
pull-requests: write
steps:
- name: Add automatic fork approval labels
- name: Add automatic fork approval label
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const labels = ['safe-to-review', 'safe-to-test'];
const labels = ['safe-to-review'];
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
@@ -139,6 +141,7 @@ jobs:
# claude-code-action only reads files and posts comments.
ref: refs/pull/${{ github.event.pull_request.number }}/head
fetch-depth: 1
persist-credentials: false
- name: Run Claude Code Review (GITHUB_TOKEN fallback)
id: claude-review
+24 -7
View File
@@ -21,12 +21,28 @@ jobs:
vars.PR_WALKTHROUGH_ENABLED == 'true' &&
github.event_name == 'pull_request_target' &&
github.event.pull_request.draft == false &&
github.event.pull_request.head.repo.full_name == github.repository &&
(github.event.action == 'opened' ||
github.event.action == 'reopened' ||
github.event.action == 'ready_for_review' ||
github.event.action == 'synchronize' ||
(github.event.action == 'labeled' && github.event.label.name == 'generate-pr-walkthrough'))
(
(
github.event.pull_request.head.repo.full_name == github.repository &&
(github.event.action == 'opened' ||
github.event.action == 'reopened' ||
github.event.action == 'ready_for_review' ||
github.event.action == 'synchronize' ||
(github.event.action == 'labeled' && github.event.label.name == 'generate-pr-walkthrough'))
) ||
(
github.event.pull_request.head.repo.full_name != github.repository &&
(
(github.event.action == 'labeled' && github.event.label.name == 'safe-to-review') ||
(github.event.action != 'labeled' &&
(
contains(github.event.pull_request.labels.*.name, 'safe-to-review') ||
(vars.CLAUDE_REVIEW_ALLOWLIST != '' &&
contains(fromJSON(vars.CLAUDE_REVIEW_ALLOWLIST), github.event.pull_request.user.login))
))
)
)
)
runs-on: ubuntu-latest
timeout-minutes: 15
environment: opencode-review-trusted
@@ -46,10 +62,11 @@ jobs:
env:
TRUSTED_SHA: ${{ github.workflow_sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$TRUSTED_SHA"
git fetch --no-tags --filter=blob:none --negotiation-tip="$TRUSTED_SHA" origin "$HEAD_SHA"
git fetch --no-tags --filter=blob:none --negotiation-tip="$TRUSTED_SHA" origin "refs/pull/${PR_NUMBER}/head"
test "$(git rev-parse FETCH_HEAD)" = "$HEAD_SHA"
MERGE_BASE="$(git merge-base "$TRUSTED_SHA" "$HEAD_SHA")"
test -n "$MERGE_BASE"
+9 -5
View File
@@ -8,7 +8,7 @@ on:
pull_request:
types: [opened, synchronize, reopened, closed]
pull_request_target:
# `labeled` is needed so a maintainer applying `safe-to-test` to a fork PR
# `labeled` is needed so a maintainer applying `safe-to-review` to a fork PR
# triggers a deploy against the current head SHA. Subsequent fork pushes
# (synchronize) re-use the maintainer's approval for the new head.
types: [opened, synchronize, reopened, closed, labeled]
@@ -37,6 +37,7 @@ jobs:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
@@ -72,7 +73,7 @@ jobs:
# JSON array of logins)
# - CLAUDE_REVIEW_ALLOWLIST repo variable (trusted login list, all events;
# the Claude workflow also adds the approval labels on open)
# - `safe-to-test` label (maintainer opt-in, durable until removed). It
# - `safe-to-review` label (maintainer opt-in, durable until removed). It
# authorizes the current and subsequent fork heads. This matters more
# here than for claude-code-review.yml because deploy actually runs the
# fork's `cmd/preview` code with SPRITES_API_TOKEN.
@@ -85,7 +86,7 @@ jobs:
github.event.action != 'closed' &&
github.event_name == 'pull_request_target' &&
github.event.pull_request.head.repo.full_name != github.repository &&
((contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
((contains(github.event.pull_request.labels.*.name, 'safe-to-review')) ||
(vars.PREVIEW_ENV_ALLOWLIST != '' && contains(fromJSON(vars.PREVIEW_ENV_ALLOWLIST), github.event.pull_request.user.login)) ||
(vars.CLAUDE_REVIEW_ALLOWLIST != '' && contains(fromJSON(vars.CLAUDE_REVIEW_ALLOWLIST), github.event.pull_request.user.login)))
runs-on: ubuntu-latest
@@ -98,13 +99,14 @@ jobs:
with:
# pull_request_target checks out base by default; explicitly use PR head.
# Safe: the preview CLI only reads files to build artifacts and posts comments.
# NOTE: The allowlist gate (safe-to-test, PREVIEW_ENV_ALLOWLIST,
# NOTE: The allowlist gate (safe-to-review, PREVIEW_ENV_ALLOWLIST,
# CLAUDE_REVIEW_ALLOWLIST)
# authorizes running the fork's cmd/preview code with SPRITES_API_TOKEN and GH_TOKEN.
# The safe-to-test label remains an explicit maintainer approval for
# The safe-to-review label remains an explicit maintainer approval for
# follow-up pushes until the maintainer removes it.
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
@@ -151,6 +153,8 @@ jobs:
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with: