feat(scanner): v1 public API + shields.io badge endpoints#12
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.
Hands-on test surface for #12Two ways to look at this before merging. 1. Rendered badge variantsAll 4 status badge
score badge (boundary values across the 21 / 51 / 81 colour bands)
Colour boundaries to verify: 20→21 brightgreen→yellowgreen, 50→51 yellowgreen→yellow, 80→81 yellow→red. Source files live on a throwaway branch 2. Live API serverThe full https://8765--dev--skill-scanner-site--DevelopmentCats--apps.dev.coder.com/api/v1/ Try:
The server is bound to my dev.coder.com workspace; let me know when you're done poking and I'll tear it down. 3. shields.io endpoint smoke testWith the JSON variant publicly reachable, the shields.io renderer should pick it up directly: That's the third-party path our v1 contract has to honour. [Posted by Coder Agents on Chris's behalf.] |
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.