feat(scanner): v1 public API + shields.io badge endpoints#12
Open
DevelopmentCats wants to merge 7 commits into
Open
feat(scanner): v1 public API + shields.io badge endpoints#12DevelopmentCats wants to merge 7 commits into
DevelopmentCats wants to merge 7 commits into
Conversation
Introduces a stable, versioned API surface that the registry-server
proxy (and any third-party badge embed) can depend on. Shipped as a
new 'scanner build-api-v1' subcommand that takes a generated
latest.json and writes the full v1 tree under an output directory:
api/v1/skills.json Compact index (verdict +
risk_score + source_sha
per skill).
api/v1/skills/<ns>/<slug>.json Per-skill detail with
reasons, findings by
severity/rule, and a
'links' block pointing
at the badge endpoints
and the immutable
source-tree URL.
api/v1/skills/<ns>/<slug>/badge/
verdict.json Shields.io endpoint
verdict.svg Inline two-rect SVG
risk.json Shields.io endpoint
risk.svg Inline two-rect SVG
api/v1/history.json Reshape of
history/index.json with
absolute report URLs.
The source-tree URL deliberately pins to source_sha (not source_ref)
so links into upstream skills survive branch movement. The badge
JSON shape is shields.io's documented endpoint contract so README
embeds can use
https://img.shields.io/endpoint?url=<our-json-url>
directly.
12 new pytest cases cover the index/detail/history shapes, the
source_sha pinning, badge colour bands, and SVG well-formedness.
Runs after index-history so the v1 history.json can mirror the same manifest. Derives the public base URL from $GITHUB_REPOSITORY_OWNER and the repo short name so forks get the right prefix automatically (mirrors the Vite build's GITHUB_REPOSITORY-derived base path from PR #11).
SkillTable and SkillDetailPage built the 'open this skill at the scan revision' link with source_ref (e.g. 'main'), which is a moving target -- clicking the link a week after the scan can land on a different tree. Use source_sha, the immutable commit the scan was actually run against.
scanner/api.py write_text calls and tests/test_api.py findings_by_rule literal were >100 cols; reformatted with no behaviour change. All 49 tests still pass.
There was a problem hiding this comment.
Pull request overview
Adds a stable, versioned api/v1 public surface (JSON + badge endpoints) generated from latest.json during the Pages publish workflow, and updates the SPA to link to immutable source SHAs.
Changes:
- Introduces
scanner/api.pyto generateapi/v1payloads (skills index, per-skill detail, history reshape) and write the fullapi/v1file tree. - Introduces
scanner/badges.pyto generate shields.io endpoint JSON and inline SVG badges for verdict and risk. - Wires a new
scanner build-api-v1CLI command into the Pages publish workflow, and updates SPA source links to usesource_sha.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
tests/test_badges.py |
Adds pytest coverage for badge JSON contract, color bands, and SVG well-formedness/width behavior. |
tests/test_api.py |
Adds pytest coverage for v1 API shapes, SHA-pinned source_tree links, history reshape, and file tree outputs. |
site/src/pages/SkillDetailPage.tsx |
Switches source tree links from source_ref to immutable source_sha. |
site/src/components/SkillTable/SkillTable.tsx |
Switches table source links from source_ref to immutable source_sha. |
scanner/cli.py |
Adds build-api-v1 subcommand to generate the v1 API tree from latest.json. |
scanner/badges.py |
Implements badge generators (shields.io endpoint JSON + inline SVG). |
scanner/api.py |
Implements v1 API builders and filesystem writer for the Pages-served API tree. |
.github/workflows/scan.yaml |
Adds a publish-pages step to generate and include pages/api/v1 in the deployed artifact. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Two defence-in-depth fixes for Copilot review feedback on PR #12. 1. scanner/badges.py: _flat_badge_svg now XML-escapes label and message before interpolating into SVG attribute and text contexts. Verdict and risk inputs never contain markup characters today, but escaping removes any SVG-injection or malformed-output path if the input shape ever drifts. 2. scanner/api.py: write_api_v1 now validates skill namespace and slug against ^[A-Za-z0-9][A-Za-z0-9._-]*$ before using them as filesystem path components. Rejects path traversal (../) and absolute paths in a malformed latest.json. Plus two regression-guard tests each. 52/52 pytest, ruff clean. Real-payload smoke run produces byte-identical output (no real input has markup characters).
Per review feedback: the two badge endpoints are now named for what they show, not the columns they come from.
- status badge: categorical scan outcome (clean/suspicious/malicious/unknown)
- score badge: numeric SkillSpector risk score (0-100, banded color)
URL paths: /api/v1/skills/<ns>/<slug>/badge/{status,score}.{json,svg}
Detail-JSON links keys: status_badge_{json,svg}, score_badge_{json,svg}
Badge function names follow the same convention.
The badge text labels ('skill scan', 'risk score') and the verdict/risk_score input parameter names are unchanged; those describe the data flowing in, not the URL surface flowing out. 52/52 pytest, ruff clean, real-payload smoke run produces the renamed file tree.
Two changes that make the v1 surface usable without first parsing skills.json:
1. README 'Public API (v1)' section lists every endpoint, the URL pattern (with {namespace}/{slug} placeholders), the stability contract, and copy-pasteable embed examples for both inline SVG and shields.io endpoint mode. Forks get the URL pattern unchanged: only the host swaps.
2. /api/v1/index.json is now generated alongside the rest of the tree by build-api-v1 (and write_api_v1). It carries the schema version, URL templates, and the current (namespace, slug) list - a single fetch a third-party consumer can use to learn the entire API surface and iterate over current skills without grabbing the heavier index.
Tests: 54 pytest (added two for build_v1_index covering history-on / history-off variants and URL-template shape), ruff clean, markdownlint clean. Real-payload smoke run now writes 18 files (was 17) under api/v1.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
What this does
Ships the stable, versioned public API surface the registry-server proxy and third-party README badges will consume. Everything is generated from
latest.jsonduring the existingpublish-pagesjob, served from the same Pages origin, and committed to av1stability contract.Endpoints
All relative to
https://coder.github.io/coder-skill-scanner/(or whatever Pages prefix a fork publishes under — the workflow derives the base URL from$GITHUB_REPOSITORYso this Just Works on forks)./api/v1/index.json(ns, slug)pairs/api/v1/skills.jsonnamespace,slug,verdict,risk_score,source_repo,source_sha,scanned_at/api/v1/skills/<ns>/<slug>.jsonreasons,findings_by_severity,findings_by_rule, plus alinksblock/api/v1/skills/<ns>/<slug>/badge/status.jsonhttps://img.shields.io/endpoint?url=.../api/v1/skills/<ns>/<slug>/badge/status.svg/api/v1/skills/<ns>/<slug>/badge/score.json/api/v1/skills/<ns>/<slug>/badge/score.svg/api/v1/history.jsonhistory/index.jsonwith absolutereport_urlper entryTwo badges per skill, named for what they show (not the columns they come from):
status— categorical scan outcome (clean/suspicious/malicious/unknown). Colour follows the verdict 1:1.score— numeric SkillSpector risk score (0/100…100/100). Colour is banded at the 21 / 51 / 81 cutoffs aligned to the verdict policy.Directly addressable, no API lookup required
The whole URL pattern is constructible from
(namespace, slug)alone. A consumer can hardcode:in any README and never touch
skills.jsonor the detail JSON. Thelinksblock in the detail JSON is a convenience for runtime discovery, not a requirement. The README's newPublic API (v1)section documents the pattern and the shields.io endpoint variant.A third-party consumer that wants to bootstrap programmatically fetches
/api/v1/index.json:{ "schema_version": 1, "urls": { "skill_detail": "https://.../api/v1/skills/{namespace}/{slug}.json", "status_badge_svg": "https://.../api/v1/skills/{namespace}/{slug}/badge/status.svg", "score_badge_svg": "https://.../api/v1/skills/{namespace}/{slug}/badge/score.svg", ... }, "skills": [{"namespace": "coder", "slug": "modules"}, ...] }v1 stability commitment
Field names and URL shapes inside the
v1prefix do not change. New optional fields are allowed. Removed/renamed fields force av2prefix with a deprecation window onv1. Documented inline inscanner/api.pyandscanner/badges.py.Source-tree URL: source_sha not source_ref
Both the API's
links.source_treeand the SPA's "open in upstream" links (SkillTable,SkillDetailPage) now buildgithub.com/<repo>/tree/<sha>/<path>fromsource_sha, the immutable commit the scan ran against. Previously these usedsource_ref(e.g.main), so a link a week after the scan could land on a different tree.Security review feedback (Copilot)
Two defence-in-depth fixes landed during review:
scanner/badges.py—_flat_badge_svgnow XML-escapeslabelandmessagebefore interpolating into SVG attribute and text contexts. The current inputs (verdict,<n>/100) never contain markup characters, but escaping keeps the renderer well-formed and injection-free if the input shape ever drifts.scanner/api.py—write_api_v1validates skillnamespaceandslugagainst^[A-Za-z0-9][A-Za-z0-9._-]*$before joining them into a filesystem path; a malformedlatest.jsoncannot write outsideoutput_dir.Tests
14 new pytest cases in
tests/test_api.pyandtests/test_badges.pycovering:source_refdeliberately not leaked into the compact indexsource_treepinning to SHA (regression guard for the SPA bug above)build_v1_index) with and without history, URL-template shape, sorted skills listwrite_api_v1(18 files for 3 skills + history)statusandscoreconfig.yaml's verdict thresholds)label/messagewrite_api_v1rejects path-traversal innamespaceandslug54/54 total tests green locally. Site lint-types/lint/vitest/build green. Ruff clean. Markdownlint clean.
Workflow change
One new step in
publish-pages, right afterindex-history:Same fork-friendly base-URL derivation pattern as the Vite build in PR #11.
After merge
Dispatch the workflow once and the v1 API is live at
https://coder.github.io/coder-skill-scanner/api/v1/.... The follow-upcoder/registry-serverPR (Step 3 of the v3 plan) will consumeindex.json+ the per-skill detail or badge endpoints to render the skill cards.This PR was prepared with help from Coder Agents.