diff --git a/.github/workflows/auto-approve-bridge.yml b/.github/workflows/auto-approve-bridge.yml index deb9b8e..2948255 100644 --- a/.github/workflows/auto-approve-bridge.yml +++ b/.github/workflows/auto-approve-bridge.yml @@ -1,15 +1,19 @@ -# Auto-approve bridge caller stub for CheckPointSW/terraform-azure-cloudguard-network-security (DOPS-12542). -# Must live on the repo's DEFAULT branch — workflow_dispatch only fires from there. -# The auto-approve Lambda dispatches this; it calls the central reusable bridge, which -# (as github-actions[bot], write access -> counts) approves after re-verifying the head -# sha + the pinned validator-App success check, then enables auto-merge. +# Auto-approve bridge for CheckPointSW/terraform-azure-cloudguard-network-security (DOPS-12542). +# Self-contained (inlined) bridge — must live on the repo's DEFAULT branch; workflow_dispatch only +# fires from there. The auto-approve Lambda dispatches this. Runs as github-actions[bot] (write +# access -> its approval COUNTS toward branch protection): re-verifies the head sha + the pinned +# validator-App success check, enables auto-merge (queue), then approves. +# +# Inlined, NOT a reusable `uses:` call — GitHub forbids a PUBLIC repo from calling a reusable +# workflow in a PRIVATE/INTERNAL repo (CheckPointSW/.github is internal). Keep this body in sync +# with the onboarding skill (.claude/skills/onboard-public-auto-approve) when it changes. name: auto-approve-bridge on: workflow_dispatch: inputs: - pr_number: { required: true, type: string } - head_sha: { required: true, type: string } - check_name: { required: true, type: string } + pr_number: { description: 'PR number to approve', required: true, type: string } + head_sha: { description: 'Expected HEAD SHA (must match)', required: true, type: string } + check_name: { description: 'Lambda check run name to verify', required: true, type: string } permissions: checks: read @@ -17,15 +21,66 @@ permissions: contents: read jobs: - call: - uses: CheckPointSW/.github/.github/workflows/auto-approve-bridge.yml@main + bridge: + # Only run trusted code from the repo's own default branch (never a feature-branch copy). + # Generic across repos: e2e uses `main`, cgns mirror repos use `master`. + if: github.ref_name == github.event.repository.default_branch + runs-on: checkpointsw-scaleset permissions: checks: read pull-requests: write contents: read - # Pass the org secret AUTO_APPROVER_APP_ID (the pinned validator App id) to the reusable bridge. - secrets: inherit - with: - pr_number: ${{ inputs.pr_number }} - head_sha: ${{ inputs.head_sha }} - check_name: ${{ inputs.check_name }} + steps: + - name: Verify, enable auto-merge, and approve + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ inputs.pr_number }} + EXPECTED_SHA: ${{ inputs.head_sha }} + CHECK_NAME: ${{ inputs.check_name }} + REPO: ${{ github.repository }} + # SECURITY: pin the approver App id so a caller-supplied check_name cannot be satisfied by + # an unrelated green check. Must match the Lambda's APPROVE_APP_ID. This is a public + # identifier (not a secret), so it is a plain literal — no org secret/variable needed. + APPROVER_APP_ID: "4287270" + run: | + set -euo pipefail + if ! echo "$PR_NUMBER" | grep -qE '^[0-9]+$'; then + echo "::error::Invalid pr_number input: $PR_NUMBER"; exit 1 + fi + if ! echo "$EXPECTED_SHA" | grep -qiE '^[0-9a-f]{40}$'; then + echo "::error::Invalid head_sha input — expected 40-char hex"; exit 1 + fi + EXPECTED_SHA=$(echo "$EXPECTED_SHA" | tr 'A-F' 'a-f') + if ! echo "$CHECK_NAME" | grep -qE '^[A-Za-z0-9 ._:/-]+$'; then + echo "::error::Invalid check_name input"; exit 1 + fi + # With `set -e`, a bare `gh api` failure exits the step silently; guard each call with + # an explicit ::error so failures are visible in the log. + CURRENT_SHA=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.head.sha') || { + echo "::error::Failed to read PR #${PR_NUMBER} head from ${REPO}"; exit 1; } + if [ "$CURRENT_SHA" != "$EXPECTED_SHA" ]; then + echo "::error::SHA mismatch: PR head is ${CURRENT_SHA:0:7}, expected ${EXPECTED_SHA:0:7}. Skipping approval."; exit 1 + fi + echo "✓ SHA match confirmed: ${EXPECTED_SHA:0:7}" + PASSED=$(gh api "repos/${REPO}/commits/${EXPECTED_SHA}/check-runs?per_page=100" \ + | jq --arg name "$CHECK_NAME" --argjson appid "$APPROVER_APP_ID" \ + '[.check_runs[] | select(.name == $name and .conclusion == "success" and .app.id == $appid)] | length') || { + echo "::error::Failed to fetch/parse check-runs for ${EXPECTED_SHA:0:7}"; exit 1; } + if [ "$PASSED" -eq 0 ]; then + echo "::error::No successful '${CHECK_NAME}' check run from approver App ${APPROVER_APP_ID} on ${EXPECTED_SHA:0:7}. Cannot approve."; exit 1 + fi + echo "✓ Check run '${CHECK_NAME}' passed on ${EXPECTED_SHA:0:7}" + # Enable native auto-merge BEFORE approving. Order matters for SoD: while our approval is + # still pending, `--auto` only QUEUES the merge (enablePullRequestAutoMerge, a + # pull-requests:write action) and GitHub merges once the approval satisfies branch + # protection. Enabling it AFTER approving would make the PR already-mergeable and gh + # would attempt a DIRECT merge (mergePullRequest), needing contents:write — which the + # bridge must never have. Requires "Allow auto-merge" on the repo. + gh pr merge "$PR_NUMBER" --repo "$REPO" --auto --squash || { + echo "::error::Failed to enable auto-merge on PR #${PR_NUMBER}"; exit 1; } + echo "✓ auto-merge queued on PR #${PR_NUMBER}" + gh api "repos/${REPO}/pulls/${PR_NUMBER}/reviews" \ + --method POST --field event="APPROVE" \ + --field body="✅ Auto-Approve: validation passed (validated commit ${EXPECTED_SHA:0:7})." || { + echo "::error::Failed to submit APPROVE review on PR #${PR_NUMBER}"; exit 1; } + echo "✓ PR #${PR_NUMBER} approved — GitHub will auto-merge once requirements are met"