Skip to content

fix(view-runtime): print the per-user medium clause only for plugins that ship a medium option #14442

fix(view-runtime): print the per-user medium clause only for plugins that ship a medium option

fix(view-runtime): print the per-user medium clause only for plugins that ship a medium option #14442

name: pr-review-security-hosted
# Locally owned: the `claude-security-review-hosted-caller` component of
# melodic-software/standards
# (components/claude-lanes-hosted/claude-security-review.yml) is
# `locally-owned` for this repository in distribution/sync-manifest.yml, so
# this copy can add the resolve-model and kill-switch jobs below. It differs
# from the component in those jobs and these comments.
#
# Thin caller for the org's pr-review-cli reusable, lane `security` (advisory
# verdict, fail-closed execution check). The reusable owns the launcher, the
# credential order (the reviews role's API key, then its subscription token
# after a handoff) and the secret-handling model; per its header, this caller
# owns the triggers, concurrency, and the GITHUB_TOKEN permission grant.
#
# There is no workflow-level path gating. The `docs-only-paths` passed below
# is empty, so a documentation-only pull request is reviewed too. A
# `.github/claude-security-paths` file is no longer read.
#
# NEVER add workflow-level `paths:`/`paths-ignore:` to this caller: a check
# whose workflow never triggers is missing, not green, so a required check
# would stay Pending forever, and the babysit merge gate, which waits for
# `pr-review-security / security-review`, never arms.
#
# The reusable skips fork PRs (they receive no secrets and a read-only token),
# Dependabot, and every `[bot]` actor whose sender id is not in
# `allowed-bot-sender-ids` (below); a required check does NOT prove a fork PR
# was reviewed (see the reusable's header). On a public repository the lane
# reviews only pull requests from branches of this repository: fork and
# Dependabot pull requests get no secrets.
#
# RUNNER PLACEMENT: every job of the reusable runs on ubuntu-24.04; it takes
# no runner input.
on:
pull_request:
# `synchronize`, matching the code-review lane: every push is re-reviewed,
# including the push that resolves a merge conflict (GitHub runs no
# pull_request workflow while a PR conflicts with its base), narrowed by
# the reusable to the files changed since the last completed review. No
# ruleset requires the check; ci-status, the one required check, needs its
# success on the head and fails while the hold label is on (github-iac
# ADR 0017).
#
# NO `merge_group:`: the reusable runs only on `pull_request`; see
# melodic-software/ci-workflows#272 before enabling a merge queue.
types: [opened, synchronize, ready_for_review, reopened]
permissions:
contents: read
# Per-PR group: a newer event for the same PR waits for the running review
# instead of cancelling it, because the guardrails merge guard reads a
# cancelled run as red. The group is the `${{ github.workflow }}`-prefixed
# canonical form the standards `concurrency-policy` component requires.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: false
jobs:
# The review's model, turns and effort: the review-security route of the
# models block in docs/conventions/pr-pipeline.yaml, read from the default
# branch, never the PR head, so a PR cannot choose the model through its
# own pr-pipeline.yaml (an edit to this file still shows in its diff). When
# this job fails, the review still runs on the reusable's own
# defaults, so the security-review check keeps reporting.
resolve-model:
if: github.event.pull_request.draft == false
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: read # check out the default branch for the models block
outputs:
claude-args: ${{ steps.models.outputs.claude-args }}
env:
API_BUDGET_TOKEN: github-token
steps:
- name: Resolve main's tip
id: tip
env:
TIP_BRANCH: main
GIT_TERMINAL_PROMPT: "0"
run: |
sha=$(timeout 60 git -C "$RUNNER_TEMP" ls-remote "https://github.com/$GITHUB_REPOSITORY.git" "refs/heads/$TIP_BRANCH" |
awk -v ref="refs/heads/$TIP_BRANCH" '$2 == ref { print $1 }')
[[ $sha =~ ^[0-9a-f]{40}$ ]] || { echo "::error::the tip of $TIP_BRANCH did not resolve"; exit 1; }
echo "sha=$sha" >>"$GITHUB_OUTPUT"
- name: Check out the default branch
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ steps.tip.outputs.sha }}
path: .base
persist-credentials: false
# resolve-config imports the two check-* action modules.
sparse-checkout: |
/.github/actions/check-kill-switch/
/.github/actions/check-trusted-trigger/
/.github/actions/log-api-budget/
/.github/actions/resolve-config/
/.github/actions/setup-pinned-node/
/.github/CODEOWNERS
/.github/standards/github-actions-conventions/vocabulary.json
/.node-version
/docs/conventions/pr-pipeline.yaml
/docs/conventions/pr-pipeline/
sparse-checkout-cone-mode: false
- name: Set up Node
uses: ./.base/.github/actions/setup-pinned-node # zizmor: ignore[self-repository]
with:
node-version-file: .base/.node-version
- name: Resolve the review model
id: models
uses: ./.base/.github/actions/resolve-config # zizmor: ignore[self-repository]
with:
model-route: review-security
output-path: ${{ runner.temp }}/review-models.json
# The reusable takes Claude args only; a subagent model would be dropped.
- name: Refuse a subagent model
env:
SETTINGS_JSON: ${{ steps.models.outputs.settings-json }}
run: |
if [ -n "$SETTINGS_JSON" ]; then
echo "::error::the review-security profile sets subagent-model, which pr-review-cli.yml does not support"
exit 1
fi
- name: Log the API budget at the end
if: always()
continue-on-error: true
timeout-minutes: 1
uses: ./.base/.github/actions/log-api-budget # zizmor: ignore[self-repository]
with:
tokens: github-token
# The reusable reads no kill switch, so this caller does. The org
# and repository switches fail closed (check-kill-switch); the lane's own
# CLAUDE_SECURITY_REVIEW_DISABLED stops it only at `true`, as github-iac's
# README documents for the review-lane switches. A failed job leaves proceed
# empty, which also stops the review.
kill-switch:
if: github.event.pull_request.draft == false
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: read # check out the default branch for the kill-switch action
outputs:
proceed: ${{ steps.kill.outputs.proceed == 'true' && steps.upstream.outputs.degraded != 'true' && vars.CLAUDE_SECURITY_REVIEW_DISABLED != 'true' && 'true' || 'false' }}
env:
API_BUDGET_TOKEN: github-token
steps:
- name: Resolve main's tip
id: tip
env:
TIP_BRANCH: main
GIT_TERMINAL_PROMPT: "0"
run: |
sha=$(timeout 60 git -C "$RUNNER_TEMP" ls-remote "https://github.com/$GITHUB_REPOSITORY.git" "refs/heads/$TIP_BRANCH" |
awk -v ref="refs/heads/$TIP_BRANCH" '$2 == ref { print $1 }')
[[ $sha =~ ^[0-9a-f]{40}$ ]] || { echo "::error::the tip of $TIP_BRANCH did not resolve"; exit 1; }
echo "sha=$sha" >>"$GITHUB_OUTPUT"
- name: Check out the default branch
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ steps.tip.outputs.sha }}
path: .base
persist-credentials: false
sparse-checkout: |
/.github/actions/check-kill-switch/
/.github/actions/check-upstream-status/
/.github/scripts/maintenance-reconcile-lost-runs/component-status.mjs
/.github/actions/setup-pinned-node/
/.node-version
/.github/actions/log-api-budget/
sparse-checkout-cone-mode: false
- name: Set up Node
uses: ./.base/.github/actions/setup-pinned-node # zizmor: ignore[self-repository]
with:
node-version-file: .base/.node-version
- name: Check the kill switch
id: kill
uses: ./.base/.github/actions/check-kill-switch # zizmor: ignore[self-repository]
with:
value: ${{ vars.CLAUDE_LANES_DISABLED }}
repo-value: ${{ vars.CLAUDE_LANES_REPO_DISABLED }}
# Skips the run while Actions or npm installs list an outage, with a
# logged reason; fails open (.github/actions/check-upstream-status).
- name: Check upstream status
id: upstream
if: steps.kill.outputs.proceed == 'true'
continue-on-error: true
timeout-minutes: 2
uses: ./.base/.github/actions/check-upstream-status # zizmor: ignore[self-repository]
- name: Report the kill switch
if: steps.kill.outputs.proceed != 'true' || vars.CLAUDE_SECURITY_REVIEW_DISABLED == 'true'
env:
REASON: ${{ steps.kill.outputs.reason }}
LANE_SWITCH: ${{ vars.CLAUDE_SECURITY_REVIEW_DISABLED }}
run: |
echo "::notice::a kill switch stops this security review (lanes: ${REASON:-unreadable}, CLAUDE_SECURITY_REVIEW_DISABLED: ${LANE_SWITCH:-unset})"
# A skip fails this job, so the run is a failed first attempt: the
# lost-run reconciler re-runs it once GitHub Actions is operational
# (reconcile.mjs `upstream`), the merge gate binds that re-run on this
# error (babysit_review_evidence UPSTREAM_STOP_RE), and ci-status names
# the stop. The reason is the gate's own one-line output.
- name: Stop on upstream status
if: steps.kill.outputs.proceed == 'true' && steps.upstream.outputs.degraded == 'true' && vars.CLAUDE_SECURITY_REVIEW_DISABLED != 'true'
env:
REASON: ${{ steps.upstream.outputs.reason }}
run: |
echo "::error::an upstream outage stops this security review (${REASON:-unknown}); the lost-run reconciler re-runs it once GitHub Actions is operational"
exit 1
- name: Log the API budget at the end
if: always()
continue-on-error: true
timeout-minutes: 1
uses: ./.base/.github/actions/log-api-budget # zizmor: ignore[self-repository]
with:
tokens: github-token
pr-review-security:
needs: [resolve-model, kill-switch]
# A draft is work in progress and does not spend a review; the
# ready_for_review trigger above is what delivers one when it is ready.
if: ${{ !cancelled() && github.event.pull_request.draft == false && needs.kill-switch.outputs.proceed == 'true' }}
# The model jobs narrow this to contents: read and pull-requests: read;
# only the reusable's status job (the security-review check) writes.
permissions:
actions: read # the reusable's check-key-credit job reads this
# workflow's runs for the key's recent credit stop,
# and its status job reads them for cost decisions
checks: read # the reusable's status job reads its earlier
# runs' cost notices
contents: read
pull-requests: write # the review, its inline comments and the lane's
# reviewed-head marker comment
uses: melodic-software/ci-workflows/.github/workflows/pr-review-cli.yml@main
with:
lane: security
publish: comments
lane-transcript-public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }}
# Empty unless resolve-model succeeded, so a failed job's partial output
# never reaches the review.
model-args: ${{ needs.resolve-model.result == 'success' && needs.resolve-model.outputs.claude-args || '' }}
# Bot senders this lane reviews, by numeric user id: the
# automation-lanes App, so a commit it pushes to a pull request is
# reviewed. Where the repository does not set this variable the value is
# empty and the reusable reviews no bot.
allowed-bot-sender-ids: ${{ vars.AUTOMATION_LANES_APP_SENDER_ID }}
# Empty: the security lane reviews every change, documentation included.
# A skipped review binds no evidence to the head, so pr-merge scored a
# docs-only pull request medium and held it for a person; a review with
# no findings binds to the head and scores low.
docs-only-paths: ''
# Copies scripts/sync-shared-copies.sh generates, read at the merge base:
# identical changes to copies of one source are read once, hooks included.
generated-copies: scripts/shared-copies.txt
# Generated data files counted as read, read at the merge base.
generated-data: scripts/review-generated-data.txt
# A hold adds this label and a `claude-lane-hold` marker comment; ci-status
# fails while the label is on. The review holds only for a real-charge
# cost or a CRITICAL security finding; hold-paths names the files the
# prompt sends to extra security scrutiny and never holds by itself.
# scripts/sync-hold-floor.py writes the list from the floor,
# the same in both callers; ci-status's exempt-author hold_patterns name
# the same paths less the security review's own files, which are
# scrutiny only.
hold-label: needs-human
# BEGIN hold-floor hold-paths: generated by scripts/sync-hold-floor.py
hold-paths: |
.github/standards/trusted-actors/**
**/CODEOWNERS
docs/conventions/pr-pipeline.yaml
**/.gitmodules
plugins/source-control/skills/babysit-prs/scripts/babysit_risk.py
plugins/source-control/skills/babysit-prs/scripts/babysit_check_inputs.py
plugins/source-control/skills/babysit-prs/scripts/babysit_checks.py
plugins/source-control/skills/babysit-prs/scripts/babysit_classify.py
plugins/source-control/skills/babysit-prs/scripts/babysit_dependabot.py
plugins/source-control/skills/babysit-prs/scripts/babysit_feedback.py
plugins/source-control/skills/babysit-prs/scripts/babysit_gh.py
plugins/source-control/skills/babysit-prs/scripts/babysit_merge.py
plugins/source-control/skills/babysit-prs/scripts/babysit_merge_policy.py
plugins/source-control/skills/babysit-prs/scripts/babysit_postmerge.py
plugins/source-control/skills/babysit-prs/scripts/babysit_repo_config.py
plugins/source-control/skills/babysit-prs/scripts/babysit_review_evidence.py
plugins/source-control/skills/babysit-prs/scripts/babysit_review_trigger.py
plugins/source-control/skills/babysit-prs/scripts/babysit_run_cache.py
plugins/source-control/skills/babysit-prs/scripts/babysit_state.py
plugins/source-control/skills/babysit-prs/scripts/babysit_tracker_binding.py
plugins/source-control/skills/babysit-prs/scripts/babysit_util.py
plugins/source-control/skills/babysit-prs/scripts/babysit_verify_evidence.py
plugins/source-control/skills/babysit-prs/scripts/babysit_whole_review.py
**/.claude/**
docs/conventions/source-control.yaml
**/.claude-lane/**
**/.mcp.json
**/CLAUDE.md
**/CLAUDE.local.md
**/AGENTS.md
REVIEW.md
.github/actions/check-trusted-trigger/**
.github/actions/resolve-config/**
.github/actions/check-kill-switch/**
.github/actions/request-lane-token/**
.github/actions/log-api-budget/rate-guard.mjs
.github/actions/log-api-budget/api-budget.bash
.github/actions/log-api-budget/rate-limits.json
.github/scripts/pr-merge/merge-holds.mjs
.github/scripts/pr-merge/enable-auto-merge.mjs
.github/scripts/pr-merge/superseded.mjs
.github/scripts/pr-merge/clear-escalations.mjs
.github/scripts/pr-merge/carry-run-cache.mjs
.github/scripts/pr-merge/gate-check.mjs
.github/scripts/pr-merge/head-ref-map.mjs
.github/scripts/pr-merge/lane-records.mjs
.github/scripts/pr-merge/last-report.mjs
.github/scripts/pr-merge/rate-stop.bash
.github/scripts/pr-merge/retry-call.bash
.github/scripts/pr-merge/refire-refused.mjs
.github/scripts/pr-merge/rotation.mjs
.github/scripts/pr-merge/sweep-summary.mjs
.github/scripts/pr-merge/who.mjs
.github/scripts/pr-merge/post-unarmed-status.mjs
.github/scripts/pr-merge/verify-answers.mjs
.github/actions/report-check-run/posting-run.mjs
scripts/sync-hold-floor.py
.github/scripts/pr-unblock/address-feedback.mjs
.github/scripts/pr-unblock/escalation-routes.mjs
.github/scripts/pr-unblock/escalation-routes.json
.github/scripts/pr-unblock/gate-source.mjs
.github/scripts/pr-unblock/thread-scope.mjs
.github/scripts/pr-unblock/retry-fetch.mjs
.github/scripts/work-resolve-issue/report-outcome.mjs
.github/scripts/work-resolve-issue/limits.mjs
.github/scripts/work-resolve-issue/retry-fetch.mjs
.github/scripts/pr-unblock/report-escalation.sh
.github/scripts/pr-unblock/adjudicate-feedback.mjs
.github/scripts/pr-unblock/publish-feedback.mjs
.github/scripts/pr-unblock/resolve-threads.mjs
.github/scripts/pr-unblock/request-budget.mjs
.github/scripts/pr-verify/resolve-pr.mjs
.github/scripts/pr-verify/write-verdict.mjs
.github/scripts/intake-triage/admit.mjs
.github/actions/select-trusted-text/select-trusted-text.mjs
lib/publish-gate.mjs
.github/recurring-schedule.json
plugins/work-items/scripts/charge-words.json
.github/scripts/pr-unblock/dependency-classify.mjs
.github/scripts/apply-patch/apply-patch.mjs
.github/scripts/apply-patch/predicates.mjs
.github/scripts/apply-patch/protected-paths.mjs
.github/scripts/apply-patch/retry-fetch.mjs
.github/scripts/pr-unblock/fix-ci-context.mjs
.github/scripts/pr-unblock/feedback-dispatch.mjs
.github/scripts/pr-unblock/verify-notes.mjs
.github/scripts/pr-unblock/earlier-fail-notes.mjs
.github/scripts/pr-unblock/limit-stop.mjs
.github/scripts/lib/lane-outcome.mjs
.github/scripts/pr-unblock/dispatch-record.mjs
.github/scripts/pr-unblock/verify-cap.mjs
.github/scripts/maintenance-reconcile-lost-runs/infra-failure.mjs
.github/scripts/work-resolve-issue/stop-routes.json
.github/scripts/work-resolve-issue/stop-routes.mjs
.github/scripts/work-resolve-issue/known-names.json
.github/scripts/lib/stop-reason.mjs
.github/scripts/lib/admission-comment.mjs
.github/scripts/lib/done-evidence.mjs
.github/scripts/pr-review-main/**
docs/conventions/instruction-exception-register/**
docs/conventions/untrusted-content/**
docs/standards/**
docs/review/**
.github/workflows/dependabot-fragments.yml
.github/workflows/eval-browser-dispatch.yml
.github/workflows/intake-reenter.yml
.github/workflows/intake-triage.yml
.github/workflows/maintenance-audit-lane-runs.yml
.github/workflows/maintenance-reconcile-lost-runs.yml
.github/workflows/maintenance-tidy-code-upkeep.yml
.github/workflows/post-merge-demotion-alert.yml
.github/workflows/post-merge-test-mutation.yml
.github/workflows/pr-feedback-dispatch.yml
.github/workflows/pr-merge.yml
.github/workflows/pr-refine.yml
.github/workflows/pr-require-checks.yml
.github/workflows/pr-review-approved.yml
.github/workflows/pr-review-comment-relay.yml
.github/workflows/pr-review-hosted.yml
.github/workflows/pr-review-main.yml
.github/workflows/pr-review-security-hosted.yml
.github/workflows/pr-run-activity-read.yml
.github/workflows/pr-run-activity-write.yml
.github/workflows/pr-unblock-dispatch.yml
.github/workflows/pr-unblock.yml
.github/workflows/pr-verify-capture.yml
.github/workflows/pr-verify.yml
.github/workflows/release-publish-plugins.yml
.github/workflows/upstream-apply-changelog.yml
.github/workflows/work-resolve-issue.yml
.github/workflows/work-unblock.yml
plugins/review/skills/security-review/**
plugins/review/evals/security-review-*/**
# END hold-floor hold-paths
# The reviews role's key runs first; its subscription token only after the
# key hands off, and the central fallback token only after the
# subscription token hands off. Named secrets only, never `secrets: inherit`.
secrets:
CLAUDE_API_KEY: ${{ secrets.REVIEWS_CLAUDE_API_KEY }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.REVIEWS_CLAUDE_CODE_OAUTH_TOKEN }}
FALLBACK_CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.FALLBACK_CLAUDE_CODE_OAUTH_TOKEN }}