[AIROCMLIR-597][CI] Extract Jenkinsfile helpers into helpers/*.groovy and load them in a Bootstrap stage #313
Workflow file for this run
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: Claude Auto Review (perimeter-banner) | |
| # Companion to claude_auto_review.yml -- the Layer-2 automation (doc §6, §9). | |
| # On every PR open/synchronize/reopen, if the PR's diff vs the default branch | |
| # touches the security perimeter (.github/workflows/, .github/scripts/, | |
| # .claude/), it ensures + applies the `modifies-ci-paths` label and posts a | |
| # one-time banner (deduped by a hidden marker + author filter) warning | |
| # maintainers to audit before applying `claude-review`. Removes the label if a | |
| # later push drops the perimeter changes. | |
| # | |
| # Safe under pull_request_target (same pattern as fork_notify): NO | |
| # actions/checkout (PR code never runs; we only fetch refs over HTTPS into a | |
| # temp dir and read them with `git diff --name-only` -- no PR-controlled code | |
| # path executes), read-only API queries + label management on a hardcoded name | |
| # + a fixed-template comment; PR-author strings are interpolated as text, | |
| # never re-evaluated as shell; default token is permissions:{}, all calls use | |
| # the in-step App token. | |
| on: | |
| pull_request_target: | |
| types: [opened, synchronize, reopened] | |
| # Default-deny at workflow level. | |
| permissions: {} | |
| # Only the latest head's perimeter status matters; cancel stale runs. | |
| concurrency: | |
| group: claude-perimeter-banner-${{ github.event.pull_request.number }} | |
| cancel-in-progress: true | |
| jobs: | |
| detect: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| # Default-deny: all calls use the in-step App token. | |
| permissions: {} | |
| steps: | |
| # App token so the banner posts under the unique bot identity, which the | |
| # dedup filter (EXPECTED_AUTHOR) keys on and a PR commenter can't | |
| # impersonate. Request only the scopes the steps below exercise: | |
| # - contents:read git fetch of the default branch + PR head. | |
| # - issues:write create the repository label (gh label create) | |
| # and use the Issues REST label/comment endpoints. | |
| # - pull-requests:write apply/remove the label and post the banner on a | |
| # PR-backed issue: GitHub enforces Pull requests | |
| # write for these operations even though the URL | |
| # is under /issues (issues:write alone returns | |
| # 403 "Resource not accessible by integration"). | |
| # See doc §9, §11. | |
| - name: Generate App token | |
| id: app-token | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| client-id: ${{ vars.ROCMLIR_PR_REVIEWER_APP_ID }} | |
| private-key: ${{ secrets.ROCMLIR_PR_REVIEWER_PRIVATE_KEY }} | |
| permission-contents: read | |
| permission-issues: write | |
| permission-pull-requests: write | |
| - name: Detect perimeter modifications, label, banner-comment | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| REPO: ${{ github.repository }} | |
| # Hidden marker for the one-time-banner dedup. Bump the suffix to | |
| # force a re-post. | |
| BANNER_MARKER: '<!-- claude-perimeter-banner:v1 -->' | |
| # MUST stay in sync with the Layer-3 perimeter block in | |
| # claude_auto_review.yml -- same perimeter definition. See doc §15. | |
| PERIMETER_REGEX: '^(\.github/workflows/|\.github/scripts/|\.claude/)' | |
| # Compare baseline -- same as Layer 3's, so the two agree regardless | |
| # of the PR's base branch. See doc §9, §15. | |
| DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} | |
| # Server-determined PR head SHA (40-char hex; shape comes from | |
| # the GitHub event, not from PR-controlled content). Used only | |
| # as a quoted argv to `git fetch / merge-base / diff` below; | |
| # never interpolated into a URL, never passed to `eval`, never | |
| # concatenated into a shell command. The `git` argv path is | |
| # exec-style (no shell re-parse), so the SHA's bytes are | |
| # opaque to the shell -- no injection surface even if the | |
| # SHA contained shell metacharacters (it can't, but the | |
| # safety doesn't depend on that). | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| # Bot identity for the dedup filter so a PR author can't suppress the | |
| # banner by pasting the marker (user.login can't be impersonated). | |
| # Mirror of BOT_LOGIN in claude_auto_review.yml. See doc §11, §15. | |
| EXPECTED_AUTHOR: 'rocmlir-pr-reviewer[bot]' | |
| run: | | |
| set -euo pipefail | |
| # ---- 1. Get the changed-file list with `git diff` (NOT the Compare API) ---- | |
| # No actions/checkout -- PR code never runs. Fetch the two refs we | |
| # need into a temp dir, compute the diff locally. The Compare API | |
| # caps `.files` at 300 (>300-file PRs could hide a perimeter file | |
| # past entry 300). `git diff` has no cap and uses the same | |
| # baseline, three-dot range, and `--no-renames` / | |
| # `-c core.quotePath=false` flags as Layer 3 in | |
| # claude_auto_review.yml -- only mechanical differences are | |
| # `git -C "$tmp"` (no workspace checkout under | |
| # pull_request_target) and `${HEAD_SHA}` vs `HEAD`. Compare vs | |
| # DEFAULT branch (not PR base) so the result agrees with Layer 3 | |
| # regardless of PR base. Flags rationale: doc §15.1. | |
| tmp=$(mktemp -d) | |
| # Clean up the tempdir on any exit path (success, failure, or | |
| # signal). The tempdir holds no secrets (the auth header is | |
| # scoped to each `git_fetch_with_auth` call via env vars and | |
| # never lands in .git/config -- see auth block below), but on | |
| # self-hosted runners /tmp persists across jobs, so an explicit | |
| # cleanup prevents unbounded .git/objects accumulation across | |
| # repeated banner runs. | |
| trap 'rm -rf "$tmp"' EXIT | |
| git -C "$tmp" init --quiet | |
| # Match actions/checkout's Basic auth scheme exactly | |
| # (base64("x-access-token:<TOKEN>")) -- the canonical, most | |
| # reliably supported form of git-over-HTTPS auth for GitHub | |
| # App installation tokens. Scope the header to each `git fetch` | |
| # via per-invocation GIT_CONFIG_* env vars (an env-var | |
| # equivalent of `git -c key=val` -- both produce one-shot | |
| # config that lives only for that git invocation, but env | |
| # vars survive the shell-function boundary that prepended | |
| # `git -c` would lose) instead of persisting it in | |
| # .git/config -- the token never lands on disk, so a missed | |
| # cleanup trap can't leak it. Mirrors rocmlirTriton's auth | |
| # block for cross-repo parity. | |
| if ! auth_header=$(printf 'x-access-token:%s' "$GH_TOKEN" | base64 | tr -d '\n'); then | |
| echo "::error::Failed to encode git fetch auth header" | |
| exit 1 | |
| fi | |
| if [ -z "$auth_header" ]; then | |
| echo "::error::Encoded git fetch auth header is empty" | |
| exit 1 | |
| fi | |
| # Register the base64-encoded credential as a workflow secret | |
| # so it never appears in plaintext in run logs. GH_TOKEN | |
| # itself is already masked (it's a registered secret from | |
| # steps.token.outputs.token), but the runner's secret-tracking | |
| # does NOT follow values through pipes -- the base64 derivative | |
| # is a fresh string from the runner's perspective. actions/ | |
| # checkout addresses this with `core.setSecret(basicCredential)`; | |
| # `::add-mask::` is the shell-step equivalent. Defense-in-depth | |
| # against any future debugging that turns on `set -x` (the | |
| # runner intercepts the `::add-mask::` workflow command BEFORE | |
| # logging the line, so this echo itself is safe). | |
| echo "::add-mask::${auth_header}" | |
| # Keep GH_TOKEN available in the step env for the gh CLI calls | |
| # below; this step also posts labels and comments after | |
| # computing the diff. | |
| git_fetch_with_auth() { | |
| GIT_CONFIG_COUNT=1 \ | |
| GIT_CONFIG_KEY_0='http.https://github.com/.extraheader' \ | |
| GIT_CONFIG_VALUE_0="AUTHORIZATION: basic ${auth_header}" \ | |
| git -C "$tmp" fetch "$@" | |
| } | |
| git -C "$tmp" remote add origin "https://github.com/${REPO}.git" | |
| # Shallow fetch + bounded deepen loop until the merge-base is | |
| # reachable (`git diff A...B` requires it). Mirrors the same idiom | |
| # in claude_auto_review.yml's `deepen` step. HEAD_SHA is in the | |
| # refspec so the grafted root has a known parent for merge-base. | |
| git_fetch_with_auth --no-tags --depth=50 origin \ | |
| "${DEFAULT_BRANCH}" "${HEAD_SHA}" | |
| for _ in 1 2 3 4 5 6 7 8 9 10; do | |
| if git -C "$tmp" merge-base \ | |
| "origin/${DEFAULT_BRANCH}" "${HEAD_SHA}" >/dev/null 2>&1; then | |
| break | |
| fi | |
| git_fetch_with_auth --no-tags --deepen=500 origin \ | |
| "${DEFAULT_BRANCH}" "${HEAD_SHA}" | |
| done | |
| # Hard fail if merge-base still unreachable, so we don't silently | |
| # fall back to a wrong baseline. | |
| git -C "$tmp" merge-base \ | |
| "origin/${DEFAULT_BRANCH}" "${HEAD_SHA}" >/dev/null | |
| changed=$(git -C "$tmp" -c core.quotePath=false diff --name-only --no-renames \ | |
| "origin/${DEFAULT_BRANCH}...${HEAD_SHA}") | |
| perimeter=$(printf '%s\n' "$changed" \ | |
| | grep -E "$PERIMETER_REGEX" \ | |
| || true) | |
| # ---- 2. Branch on perimeter touched / not touched ---- | |
| if [ -z "$perimeter" ]; then | |
| # No longer touches perimeter: remove the stale label. REST DELETE | |
| # (not `gh pr edit --remove-label`) gives a clean 404 for the | |
| # already-absent case; any other error fails the workflow. | |
| if output=$(gh api --method DELETE \ | |
| "/repos/${REPO}/issues/${PR_NUMBER}/labels/modifies-ci-paths" \ | |
| 2>&1); then | |
| echo "Removed stale modifies-ci-paths label from PR #${PR_NUMBER}." | |
| elif grep -qE 'HTTP 404|Not Found' <<<"$output"; then | |
| echo "PR #${PR_NUMBER} does not touch the CI security perimeter and the modifies-ci-paths label is already absent; nothing to do." | |
| else | |
| echo "::error::Failed to remove stale modifies-ci-paths label from PR #${PR_NUMBER}" | |
| printf '%s\n' "$output" >&2 | |
| exit 1 | |
| fi | |
| exit 0 | |
| fi | |
| # ---- 3. Perimeter touched: ensure label exists ---- | |
| # `--force` is upsert (create or update, both 2xx); any error fails | |
| # the step (no `|| true` fail-open). | |
| if ! out=$(gh label create modifies-ci-paths --repo "${REPO}" \ | |
| --color D93F0B \ | |
| --description "PR modifies the Claude review CI security perimeter; audit before applying claude-review" \ | |
| --force 2>&1); then | |
| echo "::error::Failed to ensure modifies-ci-paths label exists on ${REPO}" | |
| printf '%s\n' "$out" >&2 | |
| exit 1 | |
| fi | |
| # ---- 4. Apply the label to the PR issue (idempotent) ---- | |
| gh api --method POST \ | |
| "/repos/${REPO}/issues/${PR_NUMBER}/labels" \ | |
| -f labels[]=modifies-ci-paths >/dev/null | |
| echo "Applied modifies-ci-paths label to PR #${PR_NUMBER}." | |
| # ---- 5. Check if banner comment already exists ---- | |
| # Filter on BOTH author == bot AND body contains marker -- the | |
| # author check stops a PR commenter from suppressing the banner by | |
| # pasting the marker. `gh api --paginate` walks the Link header | |
| # until exhausted, so a busy PR with >100 comments can't hide an | |
| # already-posted banner past page 1. `--jq` is applied per page; | |
| # we collect every matching id (rather than the prior `first(...)` | |
| # / fixed-page approach) and pick the first via bash parameter | |
| # expansion -- NOT `head -n1`, which would SIGPIPE under pipefail | |
| # on multi-match (same idiom as the perimeter awk loop below). | |
| # See doc §9. | |
| all_existing=$(MARKER="${BANNER_MARKER}" \ | |
| AUTHOR="${EXPECTED_AUTHOR}" \ | |
| gh api --paginate \ | |
| "repos/${REPO}/issues/${PR_NUMBER}/comments?per_page=100" \ | |
| --jq '.[] | |
| | select(.user.login == env.AUTHOR | |
| and (.body | contains(env.MARKER))) | |
| | .id') | |
| existing="${all_existing%%$'\n'*}" | |
| if [ -n "$existing" ]; then | |
| echo "Banner already posted on PR #${PR_NUMBER} (comment id=${existing}); skipping." | |
| exit 0 | |
| fi | |
| # ---- 6. Post the banner comment ---- | |
| # PR-controlled paths are interpolated as heredoc TEXT (never | |
| # re-evaluated as shell). To stop markdown injection, render each as | |
| # backtick-stripped inline-code (not a fenced block) and cap the list | |
| # at 50 entries. awk (not `head -n 50`) avoids a SIGPIPE failure under | |
| # pipefail. See doc §9. | |
| perimeter_total=$(printf '%s\n' "$perimeter" | grep -c .) | |
| perimeter_md=$(printf '%s\n' "$perimeter" \ | |
| | awk 'length($0) > 0 { n++; if (n <= 50) { gsub(/`/, ""); print "- `" $0 "`" } }') | |
| perimeter_overflow="" | |
| if [ "$perimeter_total" -gt 50 ]; then | |
| perimeter_overflow=$'\n\n_...and '"$((perimeter_total - 50))"' more perimeter file(s) not shown._' | |
| fi | |
| # --raw-field (NOT --field): the body is always a string, and | |
| # --raw-field disables gh's magic value interpretation (leading `@` | |
| # would read a file, bare true/false/null/ints would be type-coerced) | |
| # -- defensive since the body embeds PR-controlled perimeter paths. | |
| gh api --method POST \ | |
| "/repos/${REPO}/issues/${PR_NUMBER}/comments" \ | |
| --raw-field body="$(cat <<EOF | |
| ${BANNER_MARKER} | |
| ### :warning: This PR modifies the Claude-review CI security perimeter | |
| The following files in this PR control whether and how the | |
| \`claude-review\` workflow protects its LLM Gateway secrets at | |
| runtime (see \`.github/workflows/CLAUDE_AUTO_REVIEW.md\`): | |
| $perimeter_md$perimeter_overflow | |
| **Before applying the \`claude-review\` label on this PR**, please: | |
| 1. Audit the diff in these paths line-by-line. A malicious or | |
| accidental change could disable the \`--allowedTools\` | |
| restriction, the overlay step, the sanitizer, or the | |
| review/post job split -- any of which would expose the | |
| secrets in env to the PR-modified workflow. | |
| 2. If the changes are legitimate and you want a Claude review, | |
| do NOT apply the \`claude-review\` label. Instead, run via | |
| **Actions → Claude Auto Review → Run workflow** and enter | |
| this PR's number. The dispatch path runs from the trusted, | |
| code-owner-approved version of the workflow on | |
| \`${DEFAULT_BRANCH:-develop}\`, so a malicious PR-side | |
| modification cannot affect the run. | |
| (This banner is automated. The \`modifies-ci-paths\` label | |
| will be removed automatically if a future push removes the | |
| perimeter modifications. The Layer-3 in-workflow guard will | |
| additionally fail the \`claude-review\` label-triggered run | |
| on this PR if the label is applied while perimeter changes | |
| are present.) | |
| EOF | |
| )" >/dev/null | |
| echo "Posted perimeter banner comment on PR #${PR_NUMBER}." |