Skip to content

feat(scanner): v1 public API + shields.io badge endpoints#12

Open
DevelopmentCats wants to merge 7 commits into
mainfrom
cat/api-v1-and-badges
Open

feat(scanner): v1 public API + shields.io badge endpoints#12
DevelopmentCats wants to merge 7 commits into
mainfrom
cat/api-v1-and-badges

Conversation

@DevelopmentCats

@DevelopmentCats DevelopmentCats commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

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.json during the existing publish-pages job, served from the same Pages origin, and committed to a v1 stability 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_REPOSITORY so this Just Works on forks).

URL Shape Use
/api/v1/index.json discovery manifest: URL templates + current (ns, slug) pairs bootstrap a third-party consumer
/api/v1/skills.json compact entries with namespace, slug, verdict, risk_score, source_repo, source_sha, scanned_at listing / cache warmer
/api/v1/skills/<ns>/<slug>.json detail with reasons, findings_by_severity, findings_by_rule, plus a links block per-skill API consumer
/api/v1/skills/<ns>/<slug>/badge/status.json shields.io endpoint payload https://img.shields.io/endpoint?url=...
/api/v1/skills/<ns>/<slug>/badge/status.svg inline two-rect flat-style SVG direct embed, no shields.io hop
/api/v1/skills/<ns>/<slug>/badge/score.json shields.io endpoint payload (banded colour) same
/api/v1/skills/<ns>/<slug>/badge/score.svg inline SVG direct embed
/api/v1/history.json reshape of history/index.json with absolute report_url per entry history consumer

Two 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/100100/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:

![skill scan](https://coder.github.io/coder-skill-scanner/api/v1/skills/coder/setup/badge/status.svg)

in any README and never touch skills.json or the detail JSON. The links block in the detail JSON is a convenience for runtime discovery, not a requirement. The README's new Public 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 v1 prefix do not change. New optional fields are allowed. Removed/renamed fields force a v2 prefix with a deprecation window on v1. Documented inline in scanner/api.py and scanner/badges.py.

Source-tree URL: source_sha not source_ref

Both the API's links.source_tree and the SPA's "open in upstream" links (SkillTable, SkillDetailPage) now build github.com/<repo>/tree/<sha>/<path> from source_sha, the immutable commit the scan ran against. Previously these used source_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_svg now XML-escapes label and message before 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.pywrite_api_v1 validates skill namespace and slug against ^[A-Za-z0-9][A-Za-z0-9._-]*$ before joining them into a filesystem path; a malformed latest.json cannot write outside output_dir.

Tests

14 new pytest cases in tests/test_api.py and tests/test_badges.py covering:

  • Index payload shape and source_ref deliberately not leaked into the compact index
  • Per-skill detail source_tree pinning to SHA (regression guard for the SPA bug above)
  • History reshape attaching absolute report URLs
  • Discovery manifest (build_v1_index) with and without history, URL-template shape, sorted skills list
  • File-tree expectations of write_api_v1 (18 files for 3 skills + history)
  • Shields.io payload contract for both status and score
  • Score colour bands at 21 / 51 / 81 cutoffs (aligned to config.yaml's verdict thresholds)
  • SVG well-formedness, hex-colour bleed, width growth with message length
  • SVG XML-escapes markup in label / message
  • write_api_v1 rejects path-traversal in namespace and slug

54/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 after index-history:

repo_short="${GITHUB_REPOSITORY#*/}"
public_base_url="https://${GITHUB_REPOSITORY_OWNER}.github.io/${repo_short}"
scanner build-api-v1 latest.json \
  --output pages/api/v1 \
  --public-base-url "${public_base_url}" \
  --history-index pages/history/index.json

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-up coder/registry-server PR (Step 3 of the v3 plan) will consume index.json + the per-skill detail or badge endpoints to render the skill cards.

This PR was prepared with help from Coder Agents.

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.
Copilot AI review requested due to automatic review settings June 24, 2026 20:28
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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.py to generate api/v1 payloads (skills index, per-skill detail, history reshape) and write the full api/v1 file tree.
  • Introduces scanner/badges.py to generate shields.io endpoint JSON and inline SVG badges for verdict and risk.
  • Wires a new scanner build-api-v1 CLI command into the Pages publish workflow, and updates SPA source links to use source_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.

Comment thread scanner/badges.py Outdated
Comment thread scanner/api.py

@bpmct bpmct left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants