Skip to content

Commit 55070d1

Browse files
authored
Add starter centralized workflows (poutine + reusable PR review) (#5)
1 parent 6dc71d8 commit 55070d1

5 files changed

Lines changed: 664 additions & 4 deletions

File tree

.github/workflows/enforce-owner-prs.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
name: Enforce Owner PRs
22

33
on:
4+
# zizmor: ignore[dangerous-triggers] This workflow does not checkout or execute PR code; it only closes unauthorized PRs via metadata.
45
pull_request_target:
56
types:
67
- opened
78
- reopened
89
- synchronize
910

10-
permissions:
11-
pull-requests: write
12-
contents: read
11+
permissions: {}
1312

1413
jobs:
1514
owner_gate:
1615
if: ${{ github.event.pull_request.user.login != 'Mehdi-Bl' }}
1716
runs-on: ubuntu-latest
17+
permissions:
18+
pull-requests: write
19+
contents: read
1820
steps:
1921
- name: Close unauthorized PR
20-
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
22+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
2123
with:
2224
script: |
2325
const number = context.payload.pull_request.number;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Required Workflow - Poutine
2+
3+
on:
4+
pull_request:
5+
merge_group:
6+
workflow_call:
7+
8+
permissions: {}
9+
10+
jobs:
11+
poutine:
12+
name: poutine
13+
runs-on: ubuntu-latest
14+
permissions:
15+
contents: read
16+
security-events: write
17+
18+
steps:
19+
- name: Checkout caller repository
20+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
21+
with:
22+
persist-credentials: false
23+
24+
- name: Run poutine scan
25+
uses: boostsecurityio/poutine-action@84c0a0d32e8d57ae12651222be1eb15351429228 # v0.15.2
26+
with:
27+
format: sarif
28+
output: results.sarif
29+
30+
- name: Normalize poutine SARIF for GitHub upload
31+
run: |
32+
jq 'del(.runs[]?.tool.driver.supportedTaxonomies)' results.sarif > results.cleaned.sarif
33+
mv results.cleaned.sarif results.sarif
34+
35+
- name: Upload poutine SARIF
36+
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
37+
uses: github/codeql-action/upload-sarif@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
38+
with:
39+
sarif_file: results.sarif
40+
category: /tool:poutine
41+
42+
- name: Upload poutine artifact
43+
if: always()
44+
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
45+
with:
46+
name: poutine-sarif
47+
path: results.sarif
48+
if-no-files-found: error
49+
retention-days: 14
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
name: Reusable Workflow - Claude Manual PR Review
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
pr_number:
7+
description: Pull request number to review.
8+
required: true
9+
type: number
10+
force_review:
11+
description: Run review even when the PR is below size thresholds.
12+
required: false
13+
default: false
14+
type: boolean
15+
allowed_actors:
16+
description: Comma-separated dispatcher allowlist.
17+
required: true
18+
type: string
19+
azure_client_id:
20+
description: Azure OIDC application client ID.
21+
required: true
22+
type: string
23+
azure_tenant_id:
24+
description: Azure tenant ID.
25+
required: true
26+
type: string
27+
azure_subscription_id:
28+
description: Azure subscription ID.
29+
required: true
30+
type: string
31+
azure_key_vault_name:
32+
description: Azure Key Vault name.
33+
required: true
34+
type: string
35+
claude_secret_name:
36+
description: Key Vault secret name that stores Claude OAuth token.
37+
required: true
38+
type: string
39+
min_changed_files:
40+
description: Minimum changed files threshold before auto-skip.
41+
required: false
42+
default: 5
43+
type: number
44+
min_total_changes:
45+
description: Minimum total additions+deletions threshold before auto-skip.
46+
required: false
47+
default: 20
48+
type: number
49+
50+
permissions:
51+
contents: read
52+
53+
concurrency:
54+
group: claude-manual-review-${{ inputs.pr_number }}
55+
cancel-in-progress: true
56+
57+
jobs:
58+
claude-review:
59+
name: Claude Manual Review
60+
runs-on: ubuntu-latest
61+
permissions:
62+
contents: read
63+
pull-requests: write
64+
issues: write
65+
id-token: write
66+
67+
steps:
68+
- name: Enforce default branch dispatch
69+
env:
70+
REF_NAME: ${{ github.ref_name }}
71+
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
72+
run: |
73+
set -euo pipefail
74+
if [ "${REF_NAME}" != "${DEFAULT_BRANCH}" ]; then
75+
echo "Manual reviews are only allowed from ${DEFAULT_BRANCH}. Current ref: ${REF_NAME}"
76+
exit 1
77+
fi
78+
79+
- name: Authorize dispatcher (allowlist)
80+
env:
81+
ACTOR: ${{ github.actor }}
82+
ALLOWED_ACTORS: ${{ inputs.allowed_actors }}
83+
run: |
84+
set -euo pipefail
85+
86+
if [ -z "${ALLOWED_ACTORS}" ]; then
87+
echo "Missing required allowlist input: allowed_actors"
88+
exit 1
89+
fi
90+
91+
allowed="false"
92+
IFS=',' read -r -a actors <<< "${ALLOWED_ACTORS}"
93+
for raw_actor in "${actors[@]}"; do
94+
candidate="$(echo "${raw_actor}" | xargs)"
95+
if [ -n "${candidate}" ] && [ "${candidate}" = "${ACTOR}" ]; then
96+
allowed="true"
97+
break
98+
fi
99+
done
100+
101+
if [ "${allowed}" != "true" ]; then
102+
echo "Actor '${ACTOR}' is not authorized to run this workflow."
103+
exit 1
104+
fi
105+
106+
- name: Login to Azure with OIDC
107+
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
108+
with:
109+
client-id: ${{ inputs.azure_client_id }}
110+
tenant-id: ${{ inputs.azure_tenant_id }}
111+
subscription-id: ${{ inputs.azure_subscription_id }}
112+
113+
- name: Fetch Claude OAuth token from Azure Key Vault
114+
id: keyvault
115+
env:
116+
AZURE_KEY_VAULT_NAME: ${{ inputs.azure_key_vault_name }}
117+
CLAUDE_SECRET_NAME: ${{ inputs.claude_secret_name }}
118+
run: |
119+
set -euo pipefail
120+
121+
claude_token="$(az keyvault secret show --vault-name "${AZURE_KEY_VAULT_NAME}" --name "${CLAUDE_SECRET_NAME}" --query value -o tsv)"
122+
if [ -z "${claude_token}" ]; then
123+
echo "Failed to read Claude token from Azure Key Vault secret '${CLAUDE_SECRET_NAME}'."
124+
exit 1
125+
fi
126+
127+
echo "::add-mask::${claude_token}"
128+
echo "claude_code_oauth_token=${claude_token}" >> "${GITHUB_OUTPUT}"
129+
130+
- name: Resolve pull request metadata
131+
id: pr
132+
env:
133+
GH_TOKEN: ${{ github.token }}
134+
PR_NUMBER: ${{ inputs.pr_number }}
135+
run: |
136+
set -euo pipefail
137+
138+
if ! [[ "${PR_NUMBER}" =~ ^[0-9]+$ ]]; then
139+
echo "Invalid pr_number input: '${PR_NUMBER}'. Expected a numeric pull request number."
140+
exit 1
141+
fi
142+
143+
pr_json="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}")"
144+
head_sha="$(echo "${pr_json}" | jq -r '.head.sha')"
145+
title="$(echo "${pr_json}" | jq -r '.title')"
146+
body="$(echo "${pr_json}" | jq -r '.body // ""')"
147+
changed_files="$(echo "${pr_json}" | jq -r '.changed_files')"
148+
additions="$(echo "${pr_json}" | jq -r '.additions')"
149+
deletions="$(echo "${pr_json}" | jq -r '.deletions')"
150+
total_changes="$((additions + deletions))"
151+
152+
echo "head_sha=${head_sha}" >> "${GITHUB_OUTPUT}"
153+
echo "changed_files=${changed_files}" >> "${GITHUB_OUTPUT}"
154+
echo "total_changes=${total_changes}" >> "${GITHUB_OUTPUT}"
155+
156+
title_delim="TITLE_$(cat /proc/sys/kernel/random/uuid)"
157+
{
158+
echo "title<<${title_delim}"
159+
echo "${title}"
160+
echo "${title_delim}"
161+
} >> "${GITHUB_OUTPUT}"
162+
163+
body_delim="BODY_$(cat /proc/sys/kernel/random/uuid)"
164+
{
165+
echo "body<<${body_delim}"
166+
echo "${body}"
167+
echo "${body_delim}"
168+
} >> "${GITHUB_OUTPUT}"
169+
170+
- name: Skip tiny pull requests unless forced
171+
if: ${{ !inputs.force_review && fromJSON(steps.pr.outputs.changed_files) < inputs.min_changed_files && fromJSON(steps.pr.outputs.total_changes) < inputs.min_total_changes }}
172+
run: |
173+
echo "Skipping Claude review because PR is below size thresholds."
174+
echo "Re-run with force_review=true to review this PR."
175+
176+
- name: Checkout pull request head
177+
if: ${{ inputs.force_review || fromJSON(steps.pr.outputs.changed_files) >= inputs.min_changed_files || fromJSON(steps.pr.outputs.total_changes) >= inputs.min_total_changes }}
178+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
179+
with:
180+
ref: ${{ steps.pr.outputs.head_sha }}
181+
fetch-depth: 1
182+
persist-credentials: false
183+
184+
- name: Run Claude manual pull request review
185+
id: claude_review
186+
if: ${{ inputs.force_review || fromJSON(steps.pr.outputs.changed_files) >= inputs.min_changed_files || fromJSON(steps.pr.outputs.total_changes) >= inputs.min_total_changes }}
187+
continue-on-error: true
188+
uses: anthropics/claude-code-action@c22f7c3f9dbdf2faa98a4c3139f7ec9eb5a691dc
189+
with:
190+
claude_code_oauth_token: ${{ steps.keyvault.outputs.claude_code_oauth_token }}
191+
github_token: ${{ github.token }}
192+
allowed_non_write_users: ${{ github.actor }}
193+
prompt: |
194+
REPO: ${{ github.repository }}
195+
PR NUMBER: ${{ inputs.pr_number }}
196+
PR TITLE: ${{ steps.pr.outputs.title }}
197+
PR BODY:
198+
${{ steps.pr.outputs.body }}
199+
200+
You are performing a strict pull request review. Review only changed files in this PR.
201+
Post exactly one consolidated `gh pr comment`.
202+
203+
Required comment format:
204+
- first line: `Model used: anthropics/claude-code-action`
205+
- section `### Executive Summary`
206+
- section `### Findings`
207+
- section `### Test Coverage Gaps`
208+
- section `### Final Verdict`
209+
210+
Findings requirements:
211+
- prioritize by severity: Critical, High, Medium, Low
212+
- include concrete evidence with `path:line` references from the diff
213+
- classify each finding as one of: Bug, Security, Performance, Testing, Maintainability
214+
- explain why it matters and provide a concrete fix
215+
- avoid speculative findings without evidence
216+
217+
If no material issues exist, state exactly `No material issues found` and list any residual risks.
218+
Review only; do not modify files, push commits, or open additional PRs.
219+
Keep comments factual, specific, and action-oriented.
220+
221+
- name: Warn when Claude review fails (non-blocking)
222+
if: ${{ always() && (inputs.force_review || fromJSON(steps.pr.outputs.changed_files) >= inputs.min_changed_files || fromJSON(steps.pr.outputs.total_changes) >= inputs.min_total_changes) && steps.claude_review.outcome == 'failure' }}
223+
env:
224+
PR_NUMBER: ${{ inputs.pr_number }}
225+
run: |
226+
echo "::warning::Claude manual review failed but is configured as non-blocking. Check the previous step logs."
227+
{
228+
echo "### Claude Manual Review Warning"
229+
echo "- PR: #${PR_NUMBER}"
230+
echo "- Model action: anthropics/claude-code-action"
231+
echo "- Status: failed (non-blocking via continue-on-error)"
232+
} >> "${GITHUB_STEP_SUMMARY}"

0 commit comments

Comments
 (0)