diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml deleted file mode 100644 index fce22fa..0000000 --- a/.github/workflows/auto-merge.yml +++ /dev/null @@ -1,247 +0,0 @@ -# .github/workflows/auto-merge.yml -# -# A2 — Auto-merge workflow for Wave 0 agent-generated PRs. -# -# Triggers auto-merge when ALL of: -# 1. All required CI status checks pass (green) -# 2. PR is labeled `level-0` (agent confirmed L0 task — no external state changes) -# 3. Desloppify score is above repo threshold (checked via PR comment or status) -# 4. No-animal-violence (NAV) scan has zero ERRORs -# 5. Validation agent has left an APPROVE review -# -# This workflow runs on the target repo, not on gary. Deploy it to all -# target repos via scripts/deploy-auto-merge-workflow.sh. -# -# Safety constraint: this workflow NEVER auto-merges PRs that: -# - Touch .env, .secret, credentials, or key files -# - Modify GitHub Actions workflows themselves (.github/workflows/) -# - Affect main branch protection rules -# - Are marked `level-1`, `level-2`, `level-3`, or `needs-human-review` -# -# If any safety check fails, the workflow adds the `needs-human-review` label -# and posts a comment explaining what blocked auto-merge. - -name: Wave 0 Auto-Merge - -on: - pull_request: - types: [labeled, unlabeled, synchronize, review_requested] - pull_request_review: - types: [submitted] - check_suite: - types: [completed] - status: {} - -permissions: - contents: write - pull-requests: write - checks: read - -jobs: - auto-merge-gate: - name: Auto-merge gate - runs-on: ubuntu-latest - # Only run on PRs labeled level-0 - if: | - github.event_name == 'pull_request' || - github.event_name == 'pull_request_review' || - github.event_name == 'status' - - steps: - - name: Check out PR - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - # ── Safety checks (fail fast) ────────────────────────────────────────── - - - name: Check level-0 label - id: check_label - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - PR_NUM="${{ github.event.pull_request.number }}" - if [ -z "$PR_NUM" ]; then - echo "Not a PR event — skipping" - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - - LABELS=$(gh pr view "$PR_NUM" --json labels --jq '[.labels[].name]') - echo "Labels: $LABELS" - - if echo "$LABELS" | grep -q '"level-0"'; then - echo "level0=true" >> "$GITHUB_OUTPUT" - else - echo "level0=false" >> "$GITHUB_OUTPUT" - echo "PR is not labeled level-0 — auto-merge blocked" - fi - - # Block if any higher-risk label is present - if echo "$LABELS" | grep -qE '"(level-1|level-2|level-3|needs-human-review)"'; then - echo "BLOCKED: higher-risk label present" - echo "blocked=true" >> "$GITHUB_OUTPUT" - else - echo "blocked=false" >> "$GITHUB_OUTPUT" - fi - - - name: Check for sensitive file changes - id: check_sensitive - if: steps.check_label.outputs.level0 == 'true' - run: | - CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD) - echo "Changed files:" - echo "$CHANGED" - - SENSITIVE=false - while IFS= read -r file; do - case "$file" in - .env* | *.secret | *credentials* | *key* | *token* | .github/workflows/*) - echo "SENSITIVE: $file" - SENSITIVE=true - ;; - esac - done <<< "$CHANGED" - - echo "sensitive=$SENSITIVE" >> "$GITHUB_OUTPUT" - - # ── Status checks ────────────────────────────────────────────────────── - - - name: Check all required CI checks pass - id: check_ci - if: steps.check_label.outputs.level0 == 'true' && steps.check_sensitive.outputs.sensitive == 'false' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - PR_NUM="${{ github.event.pull_request.number }}" - SHA="${{ github.event.pull_request.head.sha }}" - - # Get all check runs for this SHA - CHECKS=$(gh api "repos/${{ github.repository }}/commits/$SHA/check-runs" \ - --jq '[.check_runs[] | {name: .name, conclusion: .conclusion, status: .status}]') - echo "Check runs: $CHECKS" - - # Fail if any required check is not success/skipped - FAILED=$(echo "$CHECKS" | jq -r '.[] | select(.conclusion != "success" and .conclusion != "skipped" and .status == "completed") | select(.name | ascii_downcase | contains("auto-merge") | not) | .name') - if [ -n "$FAILED" ]; then - echo "ci_pass=false" >> "$GITHUB_OUTPUT" - echo "FAILED checks: $FAILED" - else - echo "ci_pass=true" >> "$GITHUB_OUTPUT" - fi - - - name: Check desloppify score - id: check_desloppify - if: steps.check_ci.outputs.ci_pass == 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - PR_NUM="${{ github.event.pull_request.number }}" - - # Look for desloppify score in PR comments (agent posts score as part of PR description) - COMMENTS=$(gh pr view "$PR_NUM" --json body --jq '.body') - SCORE=$(echo "$COMMENTS" | grep -oP 'desloppify[^\d]*\K\d+' | head -1 || echo "") - - # Repo-specific thresholds (from CLAUDE.md quality gates) - # Gary: >=80, Platform: >=75, All other: >=70 - REPO="${{ github.repository }}" - case "$REPO" in - *gary*) THRESHOLD=80 ;; - *platform*) THRESHOLD=75 ;; - *) THRESHOLD=70 ;; - esac - - if [ -z "$SCORE" ]; then - echo "deslopify_pass=unknown" >> "$GITHUB_OUTPUT" - echo "No desloppify score found in PR — will not block (manual check recommended)" - elif [ "$SCORE" -ge "$THRESHOLD" ]; then - echo "desloppify_pass=true" >> "$GITHUB_OUTPUT" - echo "Desloppify score $SCORE >= threshold $THRESHOLD" - else - echo "desloppify_pass=false" >> "$GITHUB_OUTPUT" - echo "Desloppify score $SCORE < threshold $THRESHOLD — auto-merge blocked" - fi - - - name: Check NAV (no-animal-violence) scan - id: check_nav - if: steps.check_ci.outputs.ci_pass == 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - PR_NUM="${{ github.event.pull_request.number }}" - - # Look for NAV scan result in check runs or PR body - SHA="${{ github.event.pull_request.head.sha }}" - NAV_CHECK=$(gh api "repos/${{ github.repository }}/commits/$SHA/check-runs" \ - --jq '[.check_runs[] | select(.name | test("no-animal-violence|nav|speciesist"; "i"))] | .[0]') - - if [ -z "$NAV_CHECK" ] || [ "$NAV_CHECK" = "null" ]; then - echo "nav_pass=unknown" >> "$GITHUB_OUTPUT" - echo "No NAV check found — will not block (manual check recommended)" - else - NAV_CONCLUSION=$(echo "$NAV_CHECK" | jq -r '.conclusion') - if [ "$NAV_CONCLUSION" = "success" ]; then - echo "nav_pass=true" >> "$GITHUB_OUTPUT" - else - echo "nav_pass=false" >> "$GITHUB_OUTPUT" - echo "NAV check conclusion: $NAV_CONCLUSION — auto-merge blocked" - fi - fi - - - name: Check validation agent APPROVE review - id: check_review - if: steps.check_ci.outputs.ci_pass == 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - PR_NUM="${{ github.event.pull_request.number }}" - - REVIEWS=$(gh pr view "$PR_NUM" --json reviews --jq '.reviews') - APPROVED=$(echo "$REVIEWS" | jq -r '.[] | select(.state == "APPROVED") | .author.login' | head -5) - - echo "Approved by: $APPROVED" - - # Accept approval from known validation accounts OR from any bot - if echo "$APPROVED" | grep -qE '(gary|validation-agent|github-actions\[bot\]|stuckvgn)'; then - echo "review_pass=true" >> "$GITHUB_OUTPUT" - else - echo "review_pass=false" >> "$GITHUB_OUTPUT" - echo "No validation agent APPROVE found — auto-merge blocked" - fi - - # ── Merge or label ───────────────────────────────────────────────────── - - - name: Auto-merge if all gates pass - if: | - steps.check_label.outputs.level0 == 'true' && - steps.check_label.outputs.blocked == 'false' && - steps.check_sensitive.outputs.sensitive == 'false' && - steps.check_ci.outputs.ci_pass == 'true' && - steps.check_review.outputs.review_pass == 'true' && - (steps.check_desloppify.outputs.desloppify_pass == 'true' || steps.check_desloppify.outputs.desloppify_pass == 'unknown') && - (steps.check_nav.outputs.nav_pass == 'true' || steps.check_nav.outputs.nav_pass == 'unknown') - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - PR_NUM="${{ github.event.pull_request.number }}" - echo "All gates passed — merging PR #$PR_NUM" - gh pr merge "$PR_NUM" --squash --auto \ - --subject "$(gh pr view "$PR_NUM" --json title --jq '.title') (#$PR_NUM)" \ - --body "Auto-merged by wave0 auto-merge workflow. All gates passed: CI ✓, level-0 ✓, desloppify ✓, NAV ✓, validation agent APPROVE ✓." - - - name: Label as needs-human-review if any gate fails - if: | - steps.check_label.outputs.level0 == 'true' && ( - steps.check_label.outputs.blocked == 'true' || - steps.check_sensitive.outputs.sensitive == 'true' || - steps.check_ci.outputs.ci_pass == 'false' || - steps.check_desloppify.outputs.desloppify_pass == 'false' || - steps.check_nav.outputs.nav_pass == 'false' || - steps.check_review.outputs.review_pass == 'false' - ) - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - PR_NUM="${{ github.event.pull_request.number }}" - gh pr edit "$PR_NUM" --add-label "needs-human-review" - gh pr comment "$PR_NUM" --body "Auto-merge blocked. Failed gates: CI=${{ steps.check_ci.outputs.ci_pass }} | desloppify=${{ steps.check_desloppify.outputs.desloppify_pass }} | NAV=${{ steps.check_nav.outputs.nav_pass }} | review=${{ steps.check_review.outputs.review_pass }} | sensitive=${{ steps.check_sensitive.outputs.sensitive }}. Label this PR \`level-0\` again after resolving to retry." diff --git a/.github/workflows/setup-labels.yml b/.github/workflows/setup-labels.yml deleted file mode 100644 index 87ce747..0000000 --- a/.github/workflows/setup-labels.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Setup Labels - -on: - workflow_dispatch: - -jobs: - labels: - runs-on: ubuntu-latest - steps: - - name: Create auto-merge label - run: | - gh label create "auto-merge" --color "0075ca" --description "Agent-approved Level 0 PR — merge automatically after CI passes" --repo "$GITHUB_REPOSITORY" 2>/dev/null || true - gh label create "needs-human-review" --color "e4e669" --description "Level 1 PR — async human review within 24h" --repo "$GITHUB_REPOSITORY" 2>/dev/null || true - gh label create "human-gate" --color "d93f0b" --description "Level 2 PR — must have explicit human approval before merge" --repo "$GITHUB_REPOSITORY" 2>/dev/null || true - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}