Skip to content

Commit a519120

Browse files
feat(claude-review): review against configured team-standards context
Optional two-job flow: a context job fetches configured read-only sources with a short-lived app token and hands them to the review as an artifact; the review job (no fetch credentials) injects the standards into CLAUDE.md. Sources are supplied via a TEAM_CONTEXT_SOURCES secret, so no source repos or paths live in this public repo. Backward compatible — without the inputs the review runs as before.
1 parent 00ffd9b commit a519120

4 files changed

Lines changed: 201 additions & 4 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Fetch review context
2+
description: Fetch read-only files from configured source repositories with a short-lived app token, for use as review context.
3+
inputs:
4+
app-id:
5+
description: GitHub App ID.
6+
required: true
7+
private-key:
8+
description: GitHub App private key.
9+
required: true
10+
sources:
11+
description: >
12+
Newline list of `repo:path[,path...]` to fetch from the 0xPolygon org
13+
(paths ending in `/` are directories).
14+
required: true
15+
output-dir:
16+
description: Directory to write the fetched files into.
17+
required: false
18+
default: review-context
19+
runs:
20+
using: composite
21+
steps:
22+
# The dedicated app is installed only on the source repos with contents:read,
23+
# so the minted token is already least-privilege — no extra scoping needed.
24+
- id: app-token
25+
uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
26+
with:
27+
app-id: ${{ inputs.app-id }}
28+
private-key: ${{ inputs.private-key }}
29+
owner: 0xPolygon
30+
permission-contents: read
31+
- name: Fetch
32+
shell: bash
33+
env:
34+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
35+
SOURCES: ${{ inputs.sources }}
36+
OUTPUT_DIR: ${{ inputs.output-dir }}
37+
ACTION_PATH: ${{ github.action_path }}
38+
run: bash "$ACTION_PATH/fetch.sh"
39+
- name: Revoke token
40+
if: always()
41+
shell: bash
42+
env:
43+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
44+
run: gh api --method DELETE /installation/token || true
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Fetch the files listed in $SOURCES into $OUTPUT_DIR. Each source line is
4+
# `repo:path[,path...]`; a path ending in `/` is a directory (cone
5+
# sparse-checkout, which excludes sibling subdirectories), otherwise a single
6+
# file. Only file contents are copied out — never a clone's .git dir, so the
7+
# token in the clone URL is not carried into the output. Logs counts, not names.
8+
set -euo pipefail
9+
10+
: "${GH_TOKEN:?GH_TOKEN required}"
11+
: "${OUTPUT_DIR:?OUTPUT_DIR required}"
12+
: "${SOURCES:?SOURCES required}"
13+
14+
owner=0xPolygon
15+
out="$(mkdir -p "$OUTPUT_DIR" && cd "$OUTPUT_DIR" && pwd)"
16+
count=0
17+
18+
fetch_file() { # repo path
19+
local repo="$1" path="$2"
20+
mkdir -p "$out/$repo/$(dirname "$path")"
21+
if gh api "repos/$owner/$repo/contents/$path" \
22+
-H "Accept: application/vnd.github.raw" > "$out/$repo/$path" 2>/dev/null; then
23+
count=$((count + 1))
24+
else
25+
echo "::warning::could not fetch a configured file"
26+
rm -f "$out/$repo/$path"
27+
fi
28+
}
29+
30+
fetch_dir() { # repo dir (no trailing slash)
31+
local repo="$1" dir="$2" tmp
32+
tmp="$(mktemp -d)"
33+
git clone --no-checkout --depth 1 --filter=blob:none \
34+
"https://x-access-token:${GH_TOKEN}@github.com/${owner}/${repo}.git" "$tmp" >/dev/null 2>&1
35+
git -C "$tmp" sparse-checkout set "$dir" >/dev/null
36+
git -C "$tmp" checkout >/dev/null 2>&1
37+
if [[ -d "$tmp/$dir" ]]; then
38+
mkdir -p "$out/$repo/$(dirname "$dir")"
39+
cp -R "$tmp/$dir" "$out/$repo/$(dirname "$dir")/" # content only — never $tmp/.git
40+
count=$((count + $(find "$tmp/$dir" -type f | wc -l)))
41+
else
42+
echo "::warning::could not fetch a configured directory"
43+
fi
44+
rm -rf "$tmp"
45+
}
46+
47+
while IFS= read -r line; do
48+
[[ -z "${line// /}" ]] && continue
49+
repo="${line%%:*}"
50+
paths="${line#*:}"
51+
IFS=',' read -ra parts <<< "$paths"
52+
for p in "${parts[@]}"; do
53+
p="$(echo "$p" | xargs)" # trim whitespace
54+
[[ -z "$p" ]] && continue
55+
if [[ "$p" == */ ]]; then fetch_dir "$repo" "${p%/}"; else fetch_file "$repo" "$p"; fi
56+
done
57+
done <<< "$SOURCES"
58+
59+
echo "Fetched $count file(s) into $OUTPUT_DIR"

.github/workflows/apps-claude-code-review-trigger.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,9 @@ jobs:
1616
uses: ./.github/workflows/apps-claude-code-review.yml
1717
secrets:
1818
CLAUDE_API_KEY: ${{ secrets.CLAUDE_API_KEY }}
19+
# Optional: lets the review fetch team-standards context. A dedicated
20+
# read-only app (contents:read on the source repos only) plus an
21+
# APPS_TEAM_CONTEXT_SOURCES secret listing the `repo:path` sources.
22+
TEAM_CONTEXT_APP_ID: ${{ secrets.APPS_TEAM_CONTEXT_APP_ID }}
23+
TEAM_CONTEXT_APP_PRIVATE_KEY: ${{ secrets.APPS_TEAM_CONTEXT_APP_PRIVATE_KEY }}
24+
TEAM_CONTEXT_SOURCES: ${{ secrets.APPS_TEAM_CONTEXT_SOURCES }}

.github/workflows/apps-claude-code-review.yml

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,78 @@ on:
55
secrets:
66
CLAUDE_API_KEY:
77
required: true
8+
# Optional team-context credentials. If absent, the review still runs,
9+
# just without injected standards (backward compatible during rollout).
10+
TEAM_CONTEXT_APP_ID:
11+
required: false
12+
TEAM_CONTEXT_APP_PRIVATE_KEY:
13+
required: false
14+
# Newline list of `repo:path[,path...]` to fetch — kept in a secret so the
15+
# source repos/paths don't live in this public repo.
16+
TEAM_CONTEXT_SOURCES:
17+
required: false
818

919
permissions: {}
1020

21+
# Run conditions (claude-review inherits via `needs`): same-repo PRs only, skip
22+
# changeset-release branches, skip the disable-claude-code-review label.
23+
1124
jobs:
12-
claude-review:
13-
# Only run on PRs from the same repository (not forks) to avoid API costs on external contributions.
14-
# Skip changeset-release branches — those are automated version-bump PRs, not code to review.
15-
# Skip PRs labelled disable-claude-code-review.
25+
# Job 1 — fetches review context with a short-lived, read-only token and hands
26+
# it to job 2 as an artifact. No PR content is checked out here, and the token
27+
# is never passed to the review job.
28+
fetch-context:
1629
if: >-
1730
github.event.pull_request.head.repo.full_name == github.repository &&
1831
!startsWith(github.head_ref, 'changeset-release/') &&
1932
!contains(github.event.pull_request.labels.*.name, 'disable-claude-code-review')
2033
runs-on: ubuntu-latest
34+
permissions: {}
35+
steps:
36+
- id: guard
37+
name: Detect whether team-context inputs were provided
38+
shell: bash
39+
env:
40+
APP_ID: ${{ secrets.TEAM_CONTEXT_APP_ID }}
41+
SOURCES: ${{ secrets.TEAM_CONTEXT_SOURCES }}
42+
run: |
43+
if [[ -n "$APP_ID" && -n "$SOURCES" ]]; then
44+
echo "enabled=true" >> "$GITHUB_OUTPUT"
45+
else
46+
echo "enabled=false" >> "$GITHUB_OUTPUT"
47+
echo "::notice::Team context not configured — review will run without injected standards."
48+
fi
49+
- name: Prepare context dir
50+
shell: bash
51+
# Always produce an artifact (even when the fetch is skipped) so job 2's
52+
# download never fails. The marker must be non-hidden — upload-artifact
53+
# excludes dotfiles by default, so a `.placeholder` would yield an empty
54+
# (uncreated) artifact and break the download.
55+
run: |
56+
mkdir -p review-context
57+
touch review-context/placeholder
58+
- name: Fetch review context
59+
if: steps.guard.outputs.enabled == 'true'
60+
# TODO(before merge): switch @feat/... back to @main. The branch ref lets
61+
# this PR self-test the action before it exists on main; reusable
62+
# workflows resolve composite actions by full path, not local ./.
63+
uses: 0xPolygon/pipelines/.github/actions/fetch-team-context@feat/claude-review-team-context
64+
with:
65+
app-id: ${{ secrets.TEAM_CONTEXT_APP_ID }}
66+
private-key: ${{ secrets.TEAM_CONTEXT_APP_PRIVATE_KEY }}
67+
sources: ${{ secrets.TEAM_CONTEXT_SOURCES }}
68+
output-dir: review-context
69+
- name: Upload context artifact
70+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
71+
with:
72+
name: review-context
73+
path: review-context
74+
retention-days: 1
75+
76+
# Job 2 — the review. Separate runner; not given the team-context credentials.
77+
claude-review:
78+
needs: fetch-context
79+
runs-on: ubuntu-latest
2180
permissions:
2281
contents: read
2382
pull-requests: write
@@ -29,6 +88,35 @@ jobs:
2988
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
3089
with:
3190
fetch-depth: 1
91+
- name: Download review context
92+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
93+
with:
94+
name: review-context
95+
path: .review-context
96+
- name: Inject team standards into CLAUDE.md
97+
shell: bash
98+
# Load the standards natively via CLAUDE.md so they're guaranteed in the
99+
# review's context; any other fetched docs stay on disk for on-demand reads.
100+
run: |
101+
std="$(find .review-context -type f -name team-standards.md | head -1)"
102+
if [[ -z "$std" ]]; then
103+
echo "::warning::No team standards in context — reviewing without them."
104+
exit 0
105+
fi
106+
{
107+
echo ""
108+
echo "<!-- BEGIN injected team standards (CI review only; not committed) -->"
109+
echo "# Team standards (injected for this review)"
110+
echo ""
111+
echo "These team standards govern this review. Additional reference material"
112+
echo "(rationale docs and a service dependency graph for cross-repo impact)"
113+
echo "is available on disk under \`.review-context/\` — consult it as needed."
114+
echo ""
115+
cat "$std"
116+
echo ""
117+
echo "<!-- END injected team standards -->"
118+
} >> CLAUDE.md
119+
echo "Injected $(wc -l < "$std") lines of team standards into CLAUDE.md"
32120
- name: Run Claude Code Review
33121
id: claude-review
34122
uses: anthropics/claude-code-action@df37d2f0760a4b5683a6e617c9325bc1a36443f6 # v1

0 commit comments

Comments
 (0)