ci: label allowlisted fork pull requests (#2380)
This commit is contained in:
@@ -9,6 +9,7 @@ import unittest
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
WORKFLOW = REPO_ROOT / ".github" / "workflows" / "claude-code-review.yml"
|
||||
MENTION_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "claude.yml"
|
||||
LINT_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "lint-action-pinning.yml"
|
||||
ALLOWED_USERS_INPUT = "allowed_non_write_users: ${{ github.event.pull_request.user.login }}"
|
||||
|
||||
|
||||
@@ -136,6 +137,43 @@ class ClaudeCodeReviewWorkflowContractTest(unittest.TestCase):
|
||||
"to Claude's allowed_non_write_users input",
|
||||
)
|
||||
|
||||
def test_allowlisted_fork_label_job_adds_both_approval_labels(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]
|
||||
|
||||
self.assertTrue(separator, "Allowlisted fork label job is missing")
|
||||
self.assertIn("github.event_name == 'pull_request_target'", label_job)
|
||||
self.assertIn("github.event.action == 'opened'", label_job)
|
||||
self.assertIn(
|
||||
"github.event.pull_request.head.repo.full_name != github.repository",
|
||||
label_job,
|
||||
)
|
||||
self.assertIn("vars.CLAUDE_REVIEW_ALLOWLIST != ''", label_job)
|
||||
self.assertIn(
|
||||
"contains(fromJSON(vars.CLAUDE_REVIEW_ALLOWLIST), github.event.pull_request.user.login)",
|
||||
label_job,
|
||||
)
|
||||
self.assertIn("issues: write", label_job)
|
||||
self.assertIn("github.rest.issues.addLabels", label_job)
|
||||
self.assertIn(
|
||||
"const labels = ['safe-to-review', 'safe-to-test'];",
|
||||
label_job,
|
||||
)
|
||||
self.assertNotIn("actions/checkout", label_job)
|
||||
|
||||
def test_lint_workflow_runs_preview_contract_test(self) -> None:
|
||||
workflow = LINT_WORKFLOW.read_text(encoding="utf-8")
|
||||
|
||||
self.assertIn(
|
||||
".github/scripts/preview-env-workflow-contract_test.py",
|
||||
workflow,
|
||||
)
|
||||
self.assertIn(
|
||||
"python3 .github/scripts/preview-env-workflow-contract_test.py",
|
||||
workflow,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Contract tests for the fork preview authorization workflow."""
|
||||
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
WORKFLOW = REPO_ROOT / ".github" / "workflows" / "preview-env.yml"
|
||||
|
||||
|
||||
class PreviewEnvironmentWorkflowContractTest(unittest.TestCase):
|
||||
def test_claude_allowlist_authorizes_fork_preview_deployment(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("github.event_name == 'pull_request_target'", deploy_job)
|
||||
self.assertIn(
|
||||
"github.event.pull_request.head.repo.full_name != github.repository",
|
||||
deploy_job,
|
||||
)
|
||||
self.assertIn(
|
||||
"contains(github.event.pull_request.labels.*.name, 'safe-to-test')",
|
||||
deploy_job,
|
||||
)
|
||||
self.assertIn("vars.PREVIEW_ENV_ALLOWLIST != ''", deploy_job)
|
||||
self.assertIn("vars.CLAUDE_REVIEW_ALLOWLIST != ''", deploy_job)
|
||||
self.assertIn(
|
||||
"contains(fromJSON(vars.CLAUDE_REVIEW_ALLOWLIST), github.event.pull_request.user.login)",
|
||||
deploy_job,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -73,6 +73,33 @@ 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.
|
||||
label-allowlisted-fork:
|
||||
if: >
|
||||
github.event_name == 'pull_request_target' &&
|
||||
github.event.action == 'opened' &&
|
||||
github.event.pull_request.head.repo.full_name != github.repository &&
|
||||
vars.CLAUDE_REVIEW_ALLOWLIST != '' &&
|
||||
contains(fromJSON(vars.CLAUDE_REVIEW_ALLOWLIST), github.event.pull_request.user.login)
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Add automatic fork approval labels
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
|
||||
with:
|
||||
script: |
|
||||
const labels = ['safe-to-review', 'safe-to-test'];
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
labels,
|
||||
});
|
||||
|
||||
claude-review-fork:
|
||||
# Fork PRs only, via pull_request_target so secrets are available.
|
||||
# Two initial-review approval paths:
|
||||
|
||||
@@ -8,6 +8,7 @@ on:
|
||||
- ".github/scripts/lint-action-pinning.py"
|
||||
- ".github/scripts/lint-action-pinning_test.py"
|
||||
- ".github/scripts/claude-code-review-workflow-contract_test.py"
|
||||
- ".github/scripts/preview-env-workflow-contract_test.py"
|
||||
- ".github/scripts/collect-macos-desktop-diagnostics.sh"
|
||||
- ".github/scripts/release-workflow-contract_test.py"
|
||||
- ".github/release-signing-key.asc"
|
||||
@@ -34,6 +35,7 @@ on:
|
||||
- ".github/scripts/lint-action-pinning.py"
|
||||
- ".github/scripts/lint-action-pinning_test.py"
|
||||
- ".github/scripts/claude-code-review-workflow-contract_test.py"
|
||||
- ".github/scripts/preview-env-workflow-contract_test.py"
|
||||
- ".github/scripts/collect-macos-desktop-diagnostics.sh"
|
||||
- ".github/scripts/release-workflow-contract_test.py"
|
||||
- ".github/release-signing-key.asc"
|
||||
@@ -79,6 +81,9 @@ jobs:
|
||||
- name: Test Claude Code review workflow contract
|
||||
run: python3 .github/scripts/claude-code-review-workflow-contract_test.py
|
||||
|
||||
- name: Test preview environment workflow contract
|
||||
run: python3 .github/scripts/preview-env-workflow-contract_test.py
|
||||
|
||||
- name: Test release workflow contract
|
||||
run: python3 .github/scripts/release-workflow-contract_test.py
|
||||
|
||||
|
||||
@@ -68,9 +68,11 @@ jobs:
|
||||
# -----------------------------------------------------------------------
|
||||
# Deploy / update preview (fork PRs, gated by allowlist)
|
||||
# Uses pull_request_target so secrets are available.
|
||||
# Two approval paths:
|
||||
# Three approval paths:
|
||||
# - PREVIEW_ENV_ALLOWLIST repo variable (trusted login list, all events;
|
||||
# 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, per-commit). Explicitly
|
||||
# blocked on `synchronize` so a contributor's push cannot re-use a
|
||||
# stale label — the maintainer must re-apply it after each push for
|
||||
@@ -87,7 +89,8 @@ jobs:
|
||||
github.event_name == 'pull_request_target' &&
|
||||
github.event.pull_request.head.repo.full_name != github.repository &&
|
||||
((github.event.action != 'synchronize' && contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
|
||||
(vars.PREVIEW_ENV_ALLOWLIST != '' && contains(fromJSON(vars.PREVIEW_ENV_ALLOWLIST), github.event.pull_request.user.login)))
|
||||
(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
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -98,9 +101,11 @@ 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-test, 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 must be re-applied after each new push (see strip-safe-to-test job).
|
||||
# The safe-to-test label must be re-applied after each new push for
|
||||
# the label-only path (see strip-safe-to-test job).
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user