feat: add skill to author and maintain Compass manifests for agentic packs #111
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Skill Code Review Workflow | |
| # | |
| # Automated code review using Google Gemini, validating PRs against project | |
| # rules (CLAUDE.md, SKILL_DESIGN_PRINCIPLES.md, pack-level AGENTS.md). | |
| # | |
| # Trigger methods: | |
| # 1. PR comment: Maintainer comments "/skill-code-review" | |
| # 2. Manual dispatch: Triggered from Actions tab or `gh workflow run` | |
| # | |
| # Security model: | |
| # - issue_comment runs from main branch (fork code never executes with secrets) | |
| # - Only maintainers listed in MAINTAINERS file can trigger via PR comments | |
| name: Skill Code Review | |
| on: | |
| issue_comment: | |
| types: [created] | |
| workflow_dispatch: | |
| inputs: | |
| pr_number: | |
| description: 'PR number to review' | |
| required: true | |
| type: number | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| concurrency: | |
| group: skill-code-review-${{ github.event.issue.number || inputs.pr_number }} | |
| cancel-in-progress: true | |
| # --------------------------------------------------------------------------- | |
| # Gate job: validates the /skill-code-review command and authorizes the user. | |
| # Only runs for issue_comment events. | |
| # --------------------------------------------------------------------------- | |
| jobs: | |
| gate: | |
| if: > | |
| github.event_name == 'issue_comment' && | |
| github.event.issue.pull_request && | |
| contains(github.event.comment.body, '/skill-code-review') | |
| runs-on: ubuntu-latest | |
| outputs: | |
| authorized: ${{ steps.auth.outputs.authorized }} | |
| pr_number: ${{ steps.pr.outputs.number }} | |
| head_sha: ${{ steps.pr.outputs.head_sha }} | |
| steps: | |
| # Checkout only the MAINTAINERS file from main — never from the PR branch | |
| - name: Checkout MAINTAINERS from main | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.event.repository.default_branch }} | |
| sparse-checkout: MAINTAINERS | |
| sparse-checkout-cone-mode: false | |
| - name: Check authorization | |
| id: auth | |
| env: | |
| COMMENT_AUTHOR: ${{ github.event.comment.user.login }} | |
| run: | | |
| if ! [ -f MAINTAINERS ]; then | |
| echo "::error::MAINTAINERS file not found in default branch" | |
| echo "authorized=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if grep -v '^#' MAINTAINERS | grep -v '^$' | grep -qx "$COMMENT_AUTHOR"; then | |
| echo "authorized=true" >> "$GITHUB_OUTPUT" | |
| echo "✅ $COMMENT_AUTHOR is authorized" | |
| else | |
| echo "authorized=false" >> "$GITHUB_OUTPUT" | |
| echo "❌ $COMMENT_AUTHOR is not in MAINTAINERS" | |
| fi | |
| - name: Reject unauthorized user | |
| if: steps.auth.outputs.authorized == 'false' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| gh api --method POST \ | |
| "/repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions" \ | |
| -f content="-1" | |
| gh pr comment "${{ github.event.issue.number }}" \ | |
| --repo "${{ github.repository }}" \ | |
| --body "❌ @${{ github.event.comment.user.login }} is not authorized to trigger code reviews. Only maintainers listed in \`MAINTAINERS\` can use \`/skill-code-review\`." | |
| - name: React with eyes | |
| if: steps.auth.outputs.authorized == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| gh api --method POST \ | |
| "/repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions" \ | |
| -f content="eyes" | |
| - name: Get PR info | |
| if: steps.auth.outputs.authorized == 'true' | |
| id: pr | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| PR_NUMBER="${{ github.event.issue.number }}" | |
| echo "number=$PR_NUMBER" >> "$GITHUB_OUTPUT" | |
| HEAD_SHA=$(gh api "/repos/${{ github.repository }}/pulls/$PR_NUMBER" --jq '.head.sha') | |
| echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT" | |
| echo "PR #$PR_NUMBER — HEAD: $HEAD_SHA" | |
| # --------------------------------------------------------------------------- | |
| # Review job: fetches the PR diff, builds context, and sends to Gemini. | |
| # --------------------------------------------------------------------------- | |
| review: | |
| needs: gate | |
| if: | | |
| always() && ( | |
| (needs.gate.result == 'success' && needs.gate.outputs.authorized == 'true') || | |
| github.event_name == 'workflow_dispatch' | |
| ) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Resolve inputs | |
| id: inputs | |
| run: | | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| echo "pr_number=${{ inputs.pr_number }}" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "pr_number=${{ needs.gate.outputs.pr_number }}" >> "$GITHUB_OUTPUT" | |
| echo "head_sha=${{ needs.gate.outputs.head_sha }}" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Checkout PR head | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ steps.inputs.outputs.head_sha || '' }} | |
| fetch-depth: 1 | |
| - name: Get PR diff | |
| id: diff | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| gh api \ | |
| -H "Accept: application/vnd.github.v3.diff" \ | |
| "/repos/${{ github.repository }}/pulls/${{ steps.inputs.outputs.pr_number }}" \ | |
| > /tmp/pr.diff | |
| DIFF_SIZE=$(wc -c < /tmp/pr.diff) | |
| echo "diff_size=$DIFF_SIZE" >> "$GITHUB_OUTPUT" | |
| if [ "$DIFF_SIZE" -gt 200000 ]; then | |
| head -c 200000 /tmp/pr.diff > /tmp/pr_truncated.diff | |
| echo "truncated=true" >> "$GITHUB_OUTPUT" | |
| else | |
| cp /tmp/pr.diff /tmp/pr_truncated.diff | |
| echo "truncated=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Skip if empty diff | |
| if: steps.diff.outputs.diff_size == '0' | |
| run: echo "No diff found, skipping review." | |
| - name: Build review context | |
| if: steps.diff.outputs.diff_size != '0' | |
| run: | | |
| set -euo pipefail | |
| { | |
| echo "=== PROJECT RULES: CLAUDE.md ===" | |
| cat CLAUDE.md 2>/dev/null || echo "(not found)" | |
| echo "" | |
| echo "=== PROJECT RULES: SKILL_DESIGN_PRINCIPLES.md ===" | |
| cat SKILL_DESIGN_PRINCIPLES.md 2>/dev/null || echo "(not found)" | |
| } > /tmp/project_rules.txt | |
| CHANGED_PACKS=$(grep -oP '^diff --git a/([^/]+)/' /tmp/pr.diff | sort -u | sed 's|diff --git a/||;s|/||') | |
| for pack in $CHANGED_PACKS; do | |
| if [ -f "$pack/AGENTS.md" ]; then | |
| { | |
| echo "" | |
| echo "=== PACK RULES: $pack/AGENTS.md ===" | |
| cat "$pack/AGENTS.md" | |
| } >> /tmp/project_rules.txt | |
| fi | |
| done | |
| head -c 50000 /tmp/project_rules.txt > /tmp/project_rules_truncated.txt | |
| - name: Build system instruction | |
| if: steps.diff.outputs.diff_size != '0' | |
| run: | | |
| set -euo pipefail | |
| cp .github/gemini-review-prompt.md /tmp/system_instruction.txt | |
| if [ "${{ steps.diff.outputs.truncated }}" = "true" ]; then | |
| printf '\n\nNote: This diff was truncated due to size. Review covers the first ~200KB only.' \ | |
| >> /tmp/system_instruction.txt | |
| fi | |
| { | |
| cat /tmp/system_instruction.txt | |
| printf '\n\n---\n\nPROJECT RULES REFERENCE:\n\n' | |
| cat /tmp/project_rules_truncated.txt | |
| } > /tmp/system_full.txt | |
| # Call the Gemini API with retry for transient errors | |
| - name: Review with Gemini | |
| id: gemini | |
| if: steps.diff.outputs.diff_size != '0' | |
| env: | |
| GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} | |
| CODE_REVIEW_LLM_MODEL: ${{ vars.CODE_REVIEW_LLM_MODEL }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "$GEMINI_API_KEY" ]; then | |
| echo "::error::GEMINI_API_KEY secret is not configured" | |
| echo "skipped=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| MODEL="${CODE_REVIEW_LLM_MODEL:-gemini-3.1-pro-preview}" | |
| echo "Using model: $MODEL" | |
| jq -n \ | |
| --rawfile system /tmp/system_full.txt \ | |
| --rawfile diff /tmp/pr_truncated.diff \ | |
| '{ | |
| "systemInstruction": { | |
| "parts": [{ "text": $system }] | |
| }, | |
| "contents": [{ | |
| "parts": [{ | |
| "text": ("Review the following pull request diff:\n\n" + $diff) | |
| }] | |
| }], | |
| "generationConfig": { | |
| "temperature": 0.2, | |
| "maxOutputTokens": 8192 | |
| } | |
| }' > /tmp/request.json | |
| MAX_ATTEMPTS=5 | |
| RETRY_DELAY=60 | |
| TRANSIENT_CODES="429 500 502 503 504" | |
| for attempt in $(seq 1 $MAX_ATTEMPTS); do | |
| HTTP_CODE=$(curl -s -o /tmp/response.json -w "%{http_code}" -X POST \ | |
| "https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent" \ | |
| -H "Content-Type: application/json" \ | |
| -H "x-goog-api-key: ${GEMINI_API_KEY}" \ | |
| -d @/tmp/request.json) | |
| if [ "$HTTP_CODE" -eq 200 ]; then | |
| break | |
| fi | |
| if echo "$TRANSIENT_CODES" | grep -qw "$HTTP_CODE"; then | |
| if [ "$attempt" -lt "$MAX_ATTEMPTS" ]; then | |
| echo "::warning::Gemini API returned HTTP $HTTP_CODE (attempt $attempt/$MAX_ATTEMPTS), retrying in ${RETRY_DELAY}s..." | |
| sleep $RETRY_DELAY | |
| continue | |
| fi | |
| fi | |
| echo "::error::Gemini API returned HTTP $HTTP_CODE after $attempt attempt(s)" | |
| echo "failed=true" >> "$GITHUB_OUTPUT" | |
| echo "skipped=false" >> "$GITHUB_OUTPUT" | |
| exit 1 | |
| done | |
| REVIEW=$(jq -r '.candidates[0].content.parts[0].text // empty' /tmp/response.json) | |
| if [ -z "$REVIEW" ]; then | |
| echo "::error::Gemini returned empty review" | |
| echo "failed=true" >> "$GITHUB_OUTPUT" | |
| echo "skipped=false" >> "$GITHUB_OUTPUT" | |
| exit 1 | |
| fi | |
| echo "$REVIEW" > /tmp/review.md | |
| echo "failed=false" >> "$GITHUB_OUTPUT" | |
| echo "skipped=false" >> "$GITHUB_OUTPUT" | |
| - name: Post or update review comment | |
| if: steps.diff.outputs.diff_size != '0' && always() | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PR_NUMBER: ${{ steps.inputs.outputs.pr_number }} | |
| GEMINI_SKIPPED: ${{ steps.gemini.outputs.skipped }} | |
| GEMINI_FAILED: ${{ steps.gemini.outputs.failed }} | |
| run: | | |
| set -euo pipefail | |
| MARKER="<!-- gemini-code-review -->" | |
| WORKFLOW_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| TIMESTAMP=$(date -u '+%Y-%m-%d %H:%M UTC') | |
| EXISTING_COMMENT_ID=$(gh api --paginate \ | |
| "/repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \ | |
| --jq ".[] | select(.body | startswith(\"$MARKER\")) | .id" \ | |
| | head -1) | |
| EXISTING_BODY="" | |
| RUN_NUMBER=1 | |
| if [ -n "$EXISTING_COMMENT_ID" ]; then | |
| EXISTING_BODY=$(gh api \ | |
| "/repos/${{ github.repository }}/issues/comments/$EXISTING_COMMENT_ID" \ | |
| --jq '.body') | |
| RUN_NUMBER=$(( $(echo "$EXISTING_BODY" | grep -c '### 🔄 Run #' || true) + 1 )) | |
| fi | |
| { | |
| if [ "$RUN_NUMBER" -eq 1 ]; then | |
| echo "$MARKER" | |
| fi | |
| echo "### 🔄 Run #${RUN_NUMBER} — ${TIMESTAMP}" | |
| echo "" | |
| echo "## Gemini Code Review" | |
| echo "" | |
| if [ -f "/tmp/review.md" ]; then | |
| echo "<details>" | |
| echo "<summary>📝 Review details</summary>" | |
| echo "" | |
| cat /tmp/review.md | |
| echo "" | |
| echo "</details>" | |
| elif [ "$GEMINI_SKIPPED" = "true" ]; then | |
| echo "⚠️ Skipped — \`GEMINI_API_KEY\` secret is not configured." | |
| else | |
| echo "⚠️ Code review unavailable — check the [workflow run]($WORKFLOW_URL) for details." | |
| fi | |
| echo "" | |
| echo "> [Workflow run]($WORKFLOW_URL)" | |
| } > /tmp/new_report.md | |
| if [ -n "$EXISTING_COMMENT_ID" ]; then | |
| { | |
| echo "$EXISTING_BODY" | |
| echo "" | |
| echo "---" | |
| echo "" | |
| cat /tmp/new_report.md | |
| } > /tmp/comment.md | |
| gh api \ | |
| --method PATCH \ | |
| "/repos/${{ github.repository }}/issues/comments/$EXISTING_COMMENT_ID" \ | |
| -f body="$(cat /tmp/comment.md)" | |
| else | |
| gh pr comment "$PR_NUMBER" \ | |
| --repo "${{ github.repository }}" \ | |
| --body-file /tmp/new_report.md | |
| fi | |
| - name: React to PR | |
| if: always() | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const result = '${{ job.status }}'; | |
| const prNumber = Number('${{ steps.inputs.outputs.pr_number }}'); | |
| await github.rest.reactions.createForIssue({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| content: result === 'success' ? '+1' : '-1' | |
| }); | |