-
Notifications
You must be signed in to change notification settings - Fork 1.7k
ci: gate external plugin entries on community scan merge #569
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
tobinsouth
wants to merge
1
commit into
staging
Choose a base branch
from
ci/verify-community-merged
base: staging
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+160
−0
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,160 @@ | ||
| name: Verify community scan merged | ||
|
|
||
| # Enforces the invariant: any external plugin entry added to this repo's | ||
| # marketplace.json must already exist (same name, same SHA) on | ||
| # claude-plugins-community main. | ||
| # | ||
| # claude-plugins-community is the security scan gate. This repo has no | ||
| # scan — the merge click here is a mirror, not an approval. If an entry | ||
| # isn't on community main, either the scan hasn't run, hasn't passed, | ||
| # or someone is trying to bypass the gate. | ||
| # | ||
| # Vendored entries (source: "./path") are skipped — they're authored | ||
| # in-repo and reviewed here directly. | ||
|
|
||
| on: | ||
| pull_request: | ||
| paths: | ||
| - '.claude-plugin/marketplace.json' | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| verify: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout PR head | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| # Need base ref too, to diff and find what's new | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Find added external entries | ||
| id: diff | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| base="${{ github.event.pull_request.base.sha }}" | ||
| head="${{ github.event.pull_request.head.sha }}" | ||
|
|
||
| # Pull both versions of marketplace.json | ||
| git show "$base:.claude-plugin/marketplace.json" > /tmp/base.json | ||
| git show "$head:.claude-plugin/marketplace.json" > /tmp/head.json | ||
|
|
||
| # An "external" entry is one whose .source is an object (url-kind | ||
| # or git-subdir). Vendored entries have .source as a string path. | ||
| # Key each by name+sha — that pair is what the community scan | ||
| # pinned its result to. | ||
| jq -c '.plugins[] | ||
| | select(.source | type == "object") | ||
| | {name, sha: .source.sha}' /tmp/base.json | sort > /tmp/base-ext.jsonl | ||
| jq -c '.plugins[] | ||
| | select(.source | type == "object") | ||
| | {name, sha: .source.sha}' /tmp/head.json | sort > /tmp/head-ext.jsonl | ||
|
|
||
| # Added = in head but not in base. This catches: | ||
| # - brand new entries | ||
| # - SHA bumps on existing entries (new sha = new scan needed) | ||
| # - name changes (new name = new identity) | ||
| # It deliberately does NOT catch: | ||
| # - removals (no scan needed to delete) | ||
| # - description/category/homepage edits (cosmetic, scan irrelevant) | ||
| comm -13 /tmp/base-ext.jsonl /tmp/head-ext.jsonl > /tmp/added.jsonl | ||
|
|
||
| count=$(wc -l < /tmp/added.jsonl) | ||
| echo "Found $count added/changed external entries:" | ||
| cat /tmp/added.jsonl | ||
| echo "count=$count" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Fetch community main marketplace | ||
| if: steps.diff.outputs.count != '0' | ||
| shell: bash | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| set -euo pipefail | ||
| # gh api uses the workflow's GITHUB_TOKEN — works whether | ||
| # the community repo is public or private (as long as this | ||
| # repo's Actions have read access, which same-org repos do | ||
| # by default). More reliable than raw.githubusercontent.com | ||
| # which occasionally flakes with curl exit 56. | ||
| gh api \ | ||
| -H "Accept: application/vnd.github.raw" \ | ||
| "repos/anthropics/claude-plugins-community/contents/.claude-plugin/marketplace.json?ref=main" \ | ||
| > /tmp/community.json | ||
| echo "Community main has $(jq '.plugins | length' /tmp/community.json) entries" | ||
|
|
||
| - name: Check each added entry exists in community main | ||
| if: steps.diff.outputs.count != '0' | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| # Build the same name+sha keyset for community | ||
| jq -c '.plugins[] | ||
| | select(.source | type == "object") | ||
| | {name, sha: .source.sha}' /tmp/community.json | sort > /tmp/community-ext.jsonl | ||
|
|
||
| fail=0 | ||
| while IFS= read -r entry; do | ||
| name=$(jq -r .name <<< "$entry") | ||
| sha=$(jq -r '.sha // "∅"' <<< "$entry") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replace ∅ sentinel with ASCII equivalent |
||
| short=${sha:0:8} | ||
|
|
||
| # Reject new entries without a SHA pin outright. The scan | ||
| # result is meaningless if it isn't anchored to a commit. | ||
| # (Old pre-invariant entries won't hit this — they're in | ||
| # base too, so they don't show up in the added diff.) | ||
| if [[ "$sha" == "∅" || "$sha" == "null" ]]; then | ||
| echo "::error title=Community::'$name' has no source.sha. External entries must be SHA-pinned so the scan result is anchored to a commit." | ||
| fail=1 | ||
| continue | ||
| fi | ||
|
|
||
| if grep -qxF "$entry" /tmp/community-ext.jsonl; then | ||
| echo "::notice title=Community::✓ '$name' @ $short found in community main" | ||
| else | ||
| # Give a precise diagnosis: is the name there with a | ||
| # different SHA (scan ran on a different commit), or | ||
| # is it entirely absent (scan never ran / PR not merged)? | ||
| alt_sha=$(jq -r --arg n "$name" \ | ||
| '.plugins[] | select(.name == $n and (.source | type == "object")) | .source.sha // "∅"' \ | ||
| /tmp/community.json) | ||
| if [[ -n "$alt_sha" && "$alt_sha" != "∅" ]]; then | ||
| echo "::error title=Community::'$name' exists in community main at SHA ${alt_sha:0:8}, not $short. The scan ran on a different commit — re-pin this entry to match, or open a new community PR with the new SHA." | ||
| else | ||
| echo "::error title=Community::'$name' @ $short not found in community main. Merge the community PR first, then re-run this check." | ||
| fi | ||
| fail=1 | ||
| fi | ||
| done < /tmp/added.jsonl | ||
|
|
||
| if [[ $fail -eq 1 ]]; then | ||
| { | ||
| echo "### ❌ Community scan gate not satisfied" | ||
| echo "" | ||
| echo "One or more external plugin entries in this PR are not present" | ||
| echo "on [\`claude-plugins-community\` main](https://github.com/anthropics/claude-plugins-community/blob/main/.claude-plugin/marketplace.json)." | ||
| echo "" | ||
| echo "This repo does not run a security scan. The scan runs in" | ||
| echo "\`claude-plugins-community\` — entries must land there first." | ||
| echo "" | ||
| echo "**To fix:** merge the corresponding community PR, then re-run" | ||
| echo "this workflow." | ||
| } >> "$GITHUB_STEP_SUMMARY" | ||
| exit 1 | ||
| fi | ||
|
|
||
| { | ||
| echo "### ✓ Community scan gate satisfied" | ||
| echo "" | ||
| echo "All added external entries found in \`claude-plugins-community\` main." | ||
| } >> "$GITHUB_STEP_SUMMARY" | ||
|
|
||
| - name: No external entries changed | ||
| if: steps.diff.outputs.count == '0' | ||
| run: | | ||
| echo "::notice::No external plugin entries added or changed — nothing to verify." | ||
| echo "### ✓ No external entries to verify" >> "$GITHUB_STEP_SUMMARY" | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reduce fetch-depth from 0 to something minimal