From af2bc20e225f9e8c09532450ba35dab8ad067824 Mon Sep 17 00:00:00 2001 From: DevCats Date: Thu, 25 Jun 2026 12:07:29 -0500 Subject: [PATCH 1/4] feat(site): add CNAME for scanner.registry.coder.com With GitHub Pages deployed via actions/deploy-pages, the custom-domain setting is restored from the CNAME file at the root of the uploaded artifact on every deploy. Without it, each scheduled scan silently wiped the Pages custom-domain binding. Vite copies the entire site/public/ tree verbatim into site/dist/, so putting CNAME here lands it at the root of the Pages artifact built by the scan workflow. --- site/public/CNAME | 1 + 1 file changed, 1 insertion(+) create mode 100644 site/public/CNAME diff --git a/site/public/CNAME b/site/public/CNAME new file mode 100644 index 0000000..2af9d2e --- /dev/null +++ b/site/public/CNAME @@ -0,0 +1 @@ +scanner.registry.coder.com From e9b9c2e81a98fbd4ec255bd8132013670f9e36c2 Mon Sep 17 00:00:00 2001 From: DevCats Date: Thu, 25 Jun 2026 12:08:21 -0500 Subject: [PATCH 2/4] fix(site): resolve Vite base to `/` when CNAME exists The production build hardcoded `/coder-skill-scanner/` as the asset base prefix (derived from $GITHUB_REPOSITORY). On a custom domain the site is served at the apex, so every asset URL 404s. resolveProductionBase() now checks public/CNAME and returns `/` when present. Forks without a CNAME keep the project-page behaviour. The change flows through automatically to: + asset URLs emitted by Vite + index.html %BASE_URL% substitutions (favicon, noscript link) + the rewrite-public-base-url plugin's 404.html SPA-fallback target + React Router's `basename`, which already uses import.meta.env.BASE_URL --- site/vite.config.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/site/vite.config.ts b/site/vite.config.ts index 6b39ff2..1e29f4a 100644 --- a/site/vite.config.ts +++ b/site/vite.config.ts @@ -20,10 +20,13 @@ const REPORT_REGEX = /^\/(latest\.json|schema\.json|history\/.+\.json)$/; * Resolve the production `base` path. Lookup priority: * 1. `VITE_BASE_PATH` env (explicit override; useful for local prod * builds when you want to mimic a specific Pages deployment). - * 2. The repo name parsed from `GITHUB_REPOSITORY` (CI default; a fork - * named `/` gets `//` automatically with zero - * config). - * 3. `/` (apex / local-build fallback). + * 2. A `public/CNAME` file. When the site is published to a custom + * Pages domain, GitHub serves it at the apex of that domain so + * assets must resolve at `/`, not under `//`. + * 3. The repo name parsed from `GITHUB_REPOSITORY` (CI default; a fork + * named `/` without a custom domain gets `//` + * automatically with zero config). + * 4. `/` (apex / local-build fallback). * * Always returns a value with leading and trailing slashes so Vite's * own asset URL logic does not have to special-case the input. @@ -34,6 +37,9 @@ function resolveProductionBase(): string { const prefixed = explicit.startsWith("/") ? explicit : `/${explicit}`; return prefixed.endsWith("/") ? prefixed : `${prefixed}/`; } + if (fs.existsSync(path.resolve("public", "CNAME"))) { + return "/"; + } const repo = process.env.GITHUB_REPOSITORY?.split("/")[1]?.trim(); if (repo) return `/${repo}/`; return "/"; From fff82b5ae68c1129db91cf3d0f560fa9b64642ca Mon Sep 17 00:00:00 2001 From: DevCats Date: Thu, 25 Jun 2026 12:08:45 -0500 Subject: [PATCH 3/4] fix(scanner): resolve carry_history default base from CNAME Mirror the new vite.config.ts behaviour: when site/public/CNAME exists, the canonical Pages URL is https://, not the github.io project page. Carrying history from the canonical origin avoids a redirect hop and ensures the manifest URLs we mirror match the host the SPA fetches them from at runtime. PAGES_URL repo var override still takes precedence; only the fallback is newly CNAME-aware. --- scanner/_carry_history.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/scanner/_carry_history.py b/scanner/_carry_history.py index f152338..41f92fb 100644 --- a/scanner/_carry_history.py +++ b/scanner/_carry_history.py @@ -18,17 +18,33 @@ import urllib.request from pathlib import Path -DEFAULT_BASE = "https://coder.github.io/coder-skill-scanner" +DEFAULT_BASE_FALLBACK = "https://coder.github.io/coder-skill-scanner" MAX_SNAPSHOTS = 120 # roughly 30 days at a 6h cadence. +def _default_base() -> str: + """Return the canonical Pages URL for this repo. + + Prefer ``site/public/CNAME`` when present so a custom-domain deploy + fetches prior history from the same origin it will publish to. Fall + back to the github.io project-page URL otherwise. + """ + cname = Path("site/public/CNAME") + if cname.is_file(): + for line in cname.read_text(encoding="utf-8").splitlines(): + host = line.strip() + if host: + return f"https://{host}" + return DEFAULT_BASE_FALLBACK + + def _fetch(url: str, dest: Path) -> None: dest.parent.mkdir(parents=True, exist_ok=True) urllib.request.urlretrieve(url, dest) def main(out_dir: str = "prior-history") -> int: - base = os.environ.get("PAGES_URL") or DEFAULT_BASE + base = os.environ.get("PAGES_URL") or _default_base() base = base.rstrip("/") out = Path(out_dir) out.mkdir(parents=True, exist_ok=True) From 6cee1c24f8f68e1061f8aa69400b195ef15970b9 Mon Sep 17 00:00:00 2001 From: DevCats Date: Thu, 25 Jun 2026 12:11:18 -0500 Subject: [PATCH 4/4] docs: point reader URLs at scanner.registry.coder.com Also document the CNAME-as-source-of-truth convention so a fork only needs to edit (or delete) site/public/CNAME to control where its Pages site is served, with no workflow edits required. --- README.md | 43 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 629cde0..a8cbd92 100644 --- a/README.md +++ b/README.md @@ -31,13 +31,17 @@ The registry's deploys are not gated on the scan result. Stable URLs, no auth required: -+ Public site: `https://coder.github.io/coder-skill-scanner/` -+ Per-skill detail: `https://coder.github.io/coder-skill-scanner/skills//` -+ Run history: `https://coder.github.io/coder-skill-scanner/history` -+ CDN-cached JSON: `https://coder.github.io/coder-skill-scanner/latest.json` ++ Public site: `https://scanner.registry.coder.com/` ++ Per-skill detail: `https://scanner.registry.coder.com/skills//` ++ Run history: `https://scanner.registry.coder.com/history` ++ CDN-cached JSON: `https://scanner.registry.coder.com/latest.json` + Tagged release: `https://github.com/coder/coder-skill-scanner/releases/latest/download/latest.json` -+ Schema: `https://coder.github.io/coder-skill-scanner/schema.json` (v1) -+ Per-scan history (JSON): `https://coder.github.io/coder-skill-scanner/history/index.json` ++ Schema: `https://scanner.registry.coder.com/schema.json` (v1) ++ Per-scan history (JSON): `https://scanner.registry.coder.com/history/index.json` + +The custom domain is configured via `site/public/CNAME`; the legacy +project-page URL (`https://coder.github.io/coder-skill-scanner/`) is +still redirected by GitHub Pages but should not be used in new code. ## Public API (v1) @@ -68,19 +72,23 @@ Two badges per skill: Embed a status badge in a README: ```markdown -![skill scan](https://coder.github.io/coder-skill-scanner/api/v1/skills/coder/setup/badge/status.svg) +![skill scan](https://scanner.registry.coder.com/api/v1/skills/coder/setup/badge/status.svg) ``` Or via shields.io if you want their renderer: ```markdown -![skill scan](https://img.shields.io/endpoint?url=https://coder.github.io/coder-skill-scanner/api/v1/skills/coder/setup/badge/status.json) +![skill scan](https://img.shields.io/endpoint?url=https://scanner.registry.coder.com/api/v1/skills/coder/setup/badge/status.json) ``` -For a fork, swap the host: `https://.github.io//api/v1/...`. -The scanner derives the public base URL from `$GITHUB_REPOSITORY` at -publish time, so the same URL pattern is correct for any fork without -config changes. +For a fork, swap the host: `https:///api/v1/...`. The scanner +picks the public base URL at publish time in this order: + +1. `site/public/CNAME` (the custom Pages domain, if set), +2. otherwise `$GITHUB_REPOSITORY` -> `https://.github.io/`. + +So a fork that just sets a CNAME gets the right URLs everywhere without +touching workflow code. ## Running locally @@ -114,6 +122,7 @@ as `/skills/coder/setup` stay client-side. |-- scanner/ # Python module (CLI + enumerate + combine + aggregate + history) |-- tests/ # pytest, no on-disk fixtures |-- site/ # React SPA (Vite + Tailwind + Radix + react-router-dom) +| `-- public/CNAME # custom Pages domain (drop or change for a fork) |-- pyproject.toml |-- Makefile |-- mise.toml # pinned Python + Node versions @@ -140,15 +149,19 @@ This scanner is data-driven. To run it against a different registry: 2. Edit `config.yaml`'s `catalogue.registry_repo` block. 3. Configure GitHub Pages on your fork (Settings, Pages, source: "GitHub Actions"). -4. Set Actions workflow permissions to "Read and write" so the +4. Optional: set a custom domain by editing `site/public/CNAME` (one + line, the bare host). Delete the file to publish at the github.io + project-page URL instead. Whichever you choose, DNS for the host + needs to point at `.github.io` separately. +5. Set Actions workflow permissions to "Read and write" so the publish-release job can create releases. -5. To enable the LLM semantic pass, set the credential secret matching +6. To enable the LLM semantic pass, set the credential secret matching `config.yaml`'s `scanners.skillspector.llm.provider` on your fork (for the default `anthropic` provider, `ANTHROPIC_API_KEY`), AND confirm `.github/workflows/scan.yaml` exports that secret into the SkillSpector step. Static-only mode (without the secret) is the default and works out of the box. -6. Enable Actions. +7. Enable Actions. No source changes required for catalogue changes.