mirror of
https://github.com/anthropics/claude-plugins-official.git
synced 2026-06-10 02:03:34 +00:00
Compare commits
1 Commits
zenexer/sc
...
feat/emit-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb7c2d3bd3 |
163
.github/workflows/scan-plugins.yml
vendored
163
.github/workflows/scan-plugins.yml
vendored
@@ -378,3 +378,166 @@ jobs:
|
||||
echo "::error::Scan step failed without a parseable policy verdict (likely an infra error)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# emit-verdict: post a sticky comment per entry to the bump PR with the
|
||||
# structured verdict, so downstream tooling (label automation, delist
|
||||
# authoring) can read verdicts directly instead of scraping job logs.
|
||||
# Sticky comment marker: `<!-- bump-pr-verdict:<name> -->`.
|
||||
#
|
||||
# Mirrors the schema_v1 contract from
|
||||
# anthropics/claude-plugins-community-internal#3908 so the triage scripts
|
||||
# in mcp-local-directory/scripts/triage/ work uniformly across both repos.
|
||||
# -official doesn't run per-entry static checks (zombie, schema, binaries,
|
||||
# etc.) so the `scan.*` axes are emitted as "skipped". The granular policy
|
||||
# booleans (`has_broad_scope_hooks`, `has_undisclosed_telemetry`,
|
||||
# `description_matches_behavior`) aren't surfaced by this workflow's
|
||||
# per-entry artifact yet, so they're emitted as null; the triage
|
||||
# `triage_bool_to_str` helper maps null → "?" so display is graceful.
|
||||
# Status describes the execution state, not the outcome — `ran` when the
|
||||
# scan action evaluated this SHA fresh, `cached` when a prior verdict was
|
||||
# reused (cf. run-verdicts.json's `source` field). Outcome lives in
|
||||
# `policy.passes`. policy-sweep.sh dispatches on this exact vocabulary.
|
||||
#
|
||||
# PR resolution: pull_request events carry the PR number directly. The
|
||||
# bump workflow creates bump PRs via GITHUB_TOKEN (which doesn't fire
|
||||
# pull_request triggers — recursion guard) and dispatches this scan via
|
||||
# workflow_dispatch on the bump branch. In that case we look up the
|
||||
# open PR by head ref. No PR (scan_all dispatch on main, etc.) → no-op.
|
||||
#
|
||||
# continue-on-error at the job level: emit failure must NOT block the
|
||||
# `scan` required check. Consumers fall back to log-scraping if the
|
||||
# comment is absent (gradual migration; no flag day).
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
emit-verdict:
|
||||
needs: [scan]
|
||||
if: always() && needs.scan.result != 'skipped' && needs.scan.result != 'cancelled'
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Download scan verdicts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: scan-verdicts
|
||||
path: /tmp/scan-verdicts
|
||||
continue-on-error: true
|
||||
|
||||
- name: Resolve PR number for this ref
|
||||
id: pr
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
PR_FROM_EVENT: ${{ github.event.pull_request.number }}
|
||||
REF: ${{ github.ref_name }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ "$EVENT_NAME" == "pull_request" && -n "$PR_FROM_EVENT" ]]; then
|
||||
echo "number=$PR_FROM_EVENT" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
# workflow_dispatch on the bump branch: find the open PR for it.
|
||||
# head filter takes the form owner:branch.
|
||||
owner="${REPO%%/*}"
|
||||
pr=$(gh api "/repos/${REPO}/pulls?state=open&head=${owner}:${REF}&per_page=1" \
|
||||
--jq '.[0].number // ""')
|
||||
if [[ -z "$pr" ]]; then
|
||||
echo "::notice::No open PR for ref ${REF} — sticky comments skipped (verdicts still in scan-verdicts artifact)"
|
||||
fi
|
||||
echo "number=$pr" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build and post sticky comments
|
||||
if: steps.pr.outputs.number != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
PR: ${{ steps.pr.outputs.number }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
verdicts_path=/tmp/scan-verdicts/run-verdicts.json
|
||||
# Missing/empty artifact: scan job ran but didn't produce verdicts
|
||||
# (e.g. the relevance gate said "no changes"). Nothing to comment;
|
||||
# exit clean.
|
||||
if [[ ! -s "$verdicts_path" ]]; then
|
||||
echo "::notice::No run-verdicts.json artifact — nothing to emit"
|
||||
exit 0
|
||||
fi
|
||||
count=$(jq 'length' "$verdicts_path")
|
||||
if [[ "$count" == "0" ]]; then
|
||||
echo "::notice::run-verdicts.json is empty — nothing to emit"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ran_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
|
||||
# scan.* axes: -official doesn't run per-entry static checks; emit
|
||||
# "skipped" for each so the schema is shape-compatible with -internal.
|
||||
scan_stub='{"clone":"skipped","subpath_missing":"skipped","schema":"skipped","zombie":"skipped","tool_allowlist":"skipped","binaries":"skipped","unique":"skipped","mcp":"skipped"}'
|
||||
|
||||
# Pre-fetch all PR comments once (paginated) for the marker lookup.
|
||||
gh api --paginate "/repos/$REPO/issues/$PR/comments" \
|
||||
--jq '.[] | {id, body}' > /tmp/comments.ndjson
|
||||
|
||||
jq -c '.[]' "$verdicts_path" | while read -r entry; do
|
||||
name=$(jq -r '.name' <<< "$entry")
|
||||
passes=$(jq -r '.passes' <<< "$entry")
|
||||
summary=$(jq -r '.summary // ""' <<< "$entry")
|
||||
violations=$(jq -r '.violations // ""' <<< "$entry")
|
||||
source=$(jq -r '.source // "scan"' <<< "$entry")
|
||||
|
||||
# status = execution state (cf. -internal#3908 vocabulary).
|
||||
# Outcome is in `passes`. Map source → status: scan-action-run
|
||||
# → "ran"; cache-served → "cached". Anything else falls through
|
||||
# as "ran" (only those two values appear in run-verdicts.json).
|
||||
case "$source" in
|
||||
cache) status="cached" ;;
|
||||
scan) status="ran" ;;
|
||||
*) status="ran" ;;
|
||||
esac
|
||||
|
||||
policy=$(jq -n \
|
||||
--argjson passes "$passes" \
|
||||
--arg summary "$summary" \
|
||||
--arg violations "$violations" \
|
||||
--arg source "$source" \
|
||||
--arg status "$status" \
|
||||
'{passes: $passes,
|
||||
has_broad_scope_hooks: null,
|
||||
has_undisclosed_telemetry: null,
|
||||
description_matches_behavior: null,
|
||||
summary: $summary,
|
||||
violations: $violations,
|
||||
source: $source,
|
||||
status: $status}')
|
||||
|
||||
verdict=$(jq -n \
|
||||
--argjson scan "$scan_stub" \
|
||||
--argjson policy "$policy" \
|
||||
--arg ran_at "$ran_at" \
|
||||
--arg run_id "$RUN_ID" \
|
||||
'{schema_version: 1, ran_at: $ran_at, run_id: $run_id, scan: $scan, policy: $policy}')
|
||||
|
||||
marker="<!-- bump-pr-verdict:$name -->"
|
||||
body=$(printf '%s\n```json\n%s\n```' "$marker" "$verdict")
|
||||
|
||||
# jq's first() short-circuits and avoids SIGPIPE under pipefail if
|
||||
# duplicate markers exist (shouldn't, but a prior buggy run could
|
||||
# double-post). -s slurps NDJSON; `// empty` yields no output when
|
||||
# no match.
|
||||
existing=$(jq -rs --arg m "$marker" \
|
||||
'first(.[] | select(.body | startswith($m)) | .id) // empty' \
|
||||
/tmp/comments.ndjson)
|
||||
|
||||
if [[ -n "$existing" ]]; then
|
||||
gh api -X PATCH "/repos/$REPO/issues/comments/$existing" -f body="$body" >/dev/null
|
||||
echo "Updated comment $existing for $name"
|
||||
else
|
||||
gh api -X POST "/repos/$REPO/issues/$PR/comments" -f body="$body" >/dev/null
|
||||
echo "Created comment for $name"
|
||||
fi
|
||||
done
|
||||
|
||||
Reference in New Issue
Block a user