Merge pull request #175 from HereLiesAz/claude/pages-host-docs #345
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
| name: "📐 Enforce Contribution Guidelines" | ||
|
Check failure on line 1 in .github/workflows/pr-contribution-guidelines-review.yml
|
||
| # Allow the Gemini CLI to run in CI's non-interactive checkout. Without this it treats the | ||
| # workspace as "untrusted", refuses to start, and exits 1 (failing the check). See | ||
| # https://geminicli.com/docs/cli/trusted-folders/#headless-and-automated-environments | ||
| env: | ||
| GEMINI_CLI_TRUST_WORKSPACE: 'true' | ||
| on: | ||
| pull_request: | ||
| types: [opened, synchronize, edited, reopened, ready_for_review] | ||
| pull_request_review_comment: | ||
| types: [created] | ||
| issue_comment: | ||
| types: [created] | ||
| permissions: | ||
| contents: read | ||
| pull-requests: write | ||
| issues: write | ||
| jobs: | ||
| validate-pr: | ||
| if: | | ||
| github.event_name == 'pull_request' || | ||
| ( | ||
| github.event_name == 'issue_comment' && | ||
| github.event.issue.pull_request && | ||
| contains(github.event.comment.body, '/validate-contribution') && | ||
| contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) | ||
| ) || | ||
| ( | ||
| github.event_name == 'pull_request_review_comment' && | ||
| contains(github.event.comment.body, '/validate-contribution') && | ||
| contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) | ||
| ) | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v4 | ||
| # Default behavior handles fork PRs correctly (checking out the merge commit) | ||
| # whereas explicit ref: head_ref fails for forks if the branch isn't on origin. | ||
| - name: Generate GitHub App Token (fork-safe, optional) | ||
| id: generate_token | ||
| if: ${{ vars.APP_ID }} | ||
| uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # ratchet:actions/create-github-app-token@v2 | ||
| with: | ||
| app-id: ${{ vars.APP_ID }} | ||
| private-key: ${{ secrets.APP_PRIVATE_KEY }} | ||
| - name: Get PR number | ||
| id: get_pr | ||
| run: | | ||
| if [ "${{ github.event_name }}" == "pull_request" ]; then | ||
| echo "number=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT" | ||
| elif [ "${{ github.event_name }}" == "issue_comment" ]; then | ||
| echo "number=${{ github.event.issue.number }}" >> "$GITHUB_OUTPUT" | ||
| elif [ "${{ github.event_name }}" == "pull_request_review_comment" ]; then | ||
| echo "number=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT" | ||
| fi | ||
| - name: Collect PR metadata and diff | ||
| id: pr | ||
| env: | ||
| GITHUB_TOKEN: ${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }} | ||
| REPOSITORY: ${{ github.repository }} | ||
| PR_NUMBER: ${{ steps.get_pr.outputs.number }} | ||
| run: | | ||
| set -euo pipefail | ||
| echo "number=${PR_NUMBER}" >> "$GITHUB_OUTPUT" | ||
| # Get PR details using gh CLI | ||
| PR_DATA=$(gh pr view "${PR_NUMBER}" --repo "${REPOSITORY}" --json title,body) | ||
| TITLE=$(echo "$PR_DATA" | jq -r '.title') | ||
| BODY=$(echo "$PR_DATA" | jq -r '.body // ""') | ||
| # Safely write PR title (handles quotes and newlines) | ||
| { | ||
| echo "title<<EOF" | ||
| printf "%s\n" "$TITLE" | ||
| echo "EOF" | ||
| } >> "$GITHUB_OUTPUT" | ||
| # Safely write PR body (handles quotes and newlines) | ||
| { | ||
| echo "body<<EOF" | ||
| printf "%s\n" "$BODY" | ||
| echo "EOF" | ||
| } >> "$GITHUB_OUTPUT" | ||
| # Safely capture changed files (one per line) | ||
| { | ||
| echo "changed_files<<EOF" | ||
| gh pr diff "${PR_NUMBER}" --name-only --repo "${REPOSITORY}" | ||
| echo "EOF" | ||
| } >> "$GITHUB_OUTPUT" | ||
| # Safely capture full diff | ||
| { | ||
| echo "diff<<EOF" | ||
| gh pr diff "${PR_NUMBER}" --repo "${REPOSITORY}" | ||
| echo "EOF" | ||
| } >> "$GITHUB_OUTPUT" | ||
| - name: Validate PR against CONTRIBUTING.md | ||
| id: gemini | ||
| if: ${{ secrets.GEMINI_API_KEY != '' }} | ||
| uses: google-github-actions/run-gemini-cli@v0 | ||
| env: | ||
| # gh CLI inside Gemini | ||
| GH_TOKEN: ${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }} | ||
| GITHUB_TOKEN: ${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }} | ||
| PR_NUMBER: ${{ steps.pr.outputs.number }} | ||
| REPOSITORY: ${{ github.repository }} | ||
| with: | ||
| gemini_api_key: ${{ secrets.GEMINI_API_KEY }} | ||
| gemini_cli_version: 'latest' | ||
| settings: | | ||
| { | ||
| "coreTools": [ | ||
| "run_shell_command(cat)", | ||
| "run_shell_command(printf)", | ||
| "run_shell_command(gh pr view)", | ||
| "run_shell_command(gh pr diff)", | ||
| "run_shell_command(gh pr comment)", | ||
| "run_shell_command(gh api)" | ||
| ], | ||
| "telemetry": { "enabled": false, "target": "gcp" } | ||
| } | ||
| # Prompt is now a positional argument in the CLI, but actions usage maps this input to it. | ||
| # The key fix is ensuring the prompt logic handles missing files gracefully. | ||
| prompt: | | ||
| You are a repository assistant ensuring pull requests follow the repository's contribution guidelines. | ||
| Inputs: | ||
| - Repository: ${REPOSITORY} | ||
| - PR Number: ${PR_NUMBER} | ||
| - Title: ${{ steps.pr.outputs.title }} | ||
| - Body: ${{ steps.pr.outputs.body }} | ||
| - Changed files: | ||
| ${{ steps.pr.outputs.changed_files }} | ||
| - Diff: | ||
| ```diff | ||
| ${{ steps.pr.outputs.diff }} | ||
| ``` | ||
| Required behavior: | ||
| 1. Check if CONTRIBUTING.md exists. You can try `cat CONTRIBUTING.md` or `ls CONTRIBUTING.md`. | ||
| - If the command fails or file is missing, set STATUS="PASS" with note "No CONTRIBUTING.md found", and SKIP to step 4. | ||
| 2. If found, Read the contribution guidelines. | ||
| 3. Compare the PR against the guidelines. Consider (at minimum): | ||
| - PR title/body formatting and required sections | ||
| - Branch naming or target branch requirements | ||
| - Tests, docs, or changelog requirements | ||
| - Commit message conventions (if specified) | ||
| - Any checklists or templates to be followed | ||
| Decide STATUS: | ||
| - If any requirement is not met: STATUS="FAIL" and prepare a concise checklist of actionable items with links to CONTRIBUTING.md anchors. | ||
| - Else: STATUS="PASS" and prepare a brief confirmation referencing key checks. | ||
| 4. Build exactly one Markdown comment with this marker and structure: | ||
| <!-- guidelines-check: ${STATUS} --> | ||
| ## 📐 Contribution Guidelines: ${STATUS} | ||
| <short summary, 1–2 sentences> | ||
| If STATUS=="FAIL": include a task list: | ||
| - [ ] <actionable item 1> (see CONTRIBUTING.md#<anchor>) | ||
| - [ ] <actionable item 2> (see CONTRIBUTING.md#<anchor>) | ||
| If STATUS=="PASS": include: | ||
| - All required guideline items appear satisfied based on the PR title/body/diff. | ||
| - Save the content to /tmp/guidelines_comment.md using a heredoc, then run: | ||
| cat > /tmp/guidelines_comment.md <<'EOF' | ||
| <the markdown from above> | ||
| EOF | ||
| gh pr comment "${PR_NUMBER}" --repo "${REPOSITORY}" --body-file /tmp/guidelines_comment.md | ||
| Constraints: | ||
| - Reference shell variables as "${VAR}". | ||
| - Keep the comment under ~6,500 characters. | ||
| - Do not expose secrets. | ||
| - Post or update exactly one comment per run. | ||
| - name: Detect guideline status from comment | ||
| id: detect | ||
| uses: actions/github-script@v7 | ||
| env: | ||
| PR_NUMBER: ${{ steps.pr.outputs.number }} | ||
| with: | ||
| github-token: ${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }} | ||
| script: | | ||
| const { data } = await github.rest.issues.listComments({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: Number(process.env.PR_NUMBER), | ||
| per_page: 100 | ||
| }); | ||
| const marker = /<!--\s*guidelines-check:\s*(PASS|FAIL)\s*-->/i; | ||
| const found = [...data].reverse().find(c => marker.test(c.body || '')); | ||
| if (!found) { | ||
| core.warning('No guidelines-check marker found in PR comments.'); | ||
| core.setOutput('status', 'UNKNOWN'); | ||
| } else { | ||
| const m = found.body.match(marker); | ||
| core.setOutput('status', m[1].toUpperCase()); | ||
| } | ||
| - name: Fail workflow on violations (optional) | ||
| if: steps.detect.outputs.status == 'FAIL' && vars.FAIL_ON_GUIDELINE_VIOLATIONS == 'true' | ||
| run: | | ||
| echo "Guideline violations found. Set repository variable FAIL_ON_GUIDELINE_VIOLATIONS=false to disable enforcement." >&2 | ||
| exit 1 | ||