src/shared-code.ts
342 lines and 40 functions in DrDustinEdwards/capsid, at commit daa875b on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| jsonList | 60 to 67 | 4 | 71% | OkCRAP 4.4 | No data: mutation testing has not run on this repo yet |
| <anonymous:63> | 63 to 63 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| readSharedConfig | 69 to 72 | 2 | 100% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:71> | 71 to 71 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| gitSpec | 77 to 87 | 9 | 100% | WatchCRAP 9 | No data: mutation testing has not run on this repo yet |
| presetSpec | 90 to 93 | 3 | 100% | OkCRAP 3 | No data: mutation testing has not run on this repo yet |
| semver | 97 to 100 | 2 | 100% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| cmp | 102 to 104 | 3 | 100% | OkCRAP 3 | No data: mutation testing has not run on this repo yet |
| latestTag | 107 to 114 | 6 | 100% | WatchCRAP 6 | No data: mutation testing has not run on this repo yet |
| behindBy | 117 to 124 | 3 | 100% | OkCRAP 3 | No data: mutation testing has not run on this repo yet |
| <anonymous:120> | 120 to 123 | 2 | 100% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| rootManifest | 135 to 140 | 6 | 100% | WatchCRAP 6 | No data: mutation testing has not run on this repo yet |
| releaseDecision | 156 to 168 | 7 | 100% | WatchCRAP 7 | No data: mutation testing has not run on this repo yet |
| <anonymous:159> | 159 to 162 | 2 | 100% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| sharedCodeView | 187 to 246 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| <anonymous:194> | 194 to 237 | 14 | 100% | WatchCRAP 14 | No data: mutation testing has not run on this repo yet |
| <anonymous:195> | 195 to 195 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| behind | 200 to 200 | 2 | 100% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:217> | 217 to 224 | 5 | 100% | OkCRAP 5 | No data: mutation testing has not run on this repo yet |
| <anonymous:221> | 221 to 221 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| <anonymous:222> | 222 to 222 | 2 | 100% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:233> | 233 to 233 | 2 | 100% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:235> | 235 to 235 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| <anonymous:242> | 242 to 242 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| <anonymous:243> | 243 to 243 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| <anonymous:243> | 243 to 243 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| releaseFields | 252 to 269 | 10 | 100% | WatchCRAP 10 | No data: mutation testing has not run on this repo yet |
| readRootManifest | 275 to 286 | 6 | 75% | WatchCRAP 6.6 | No data: mutation testing has not run on this repo yet |
| readRelease | 288 to 297 | 5 | 0% | WatchCRAP 30 | No data: mutation testing has not run on this repo yet |
| depsOf | 299 to 307 | 6 | 0% | CriticalCRAP 42 | No data: mutation testing has not run on this repo yet |
| readApp | 309 to 339 | 11 | 0% | CriticalCRAP 132 | No data: mutation testing has not run on this repo yet |
| <anonymous:314> | 314 to 314 | 3 | 0% | WatchCRAP 12 | No data: mutation testing has not run on this repo yet |
| <anonymous:314> | 314 to 314 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| <anonymous:318> | 318 to 318 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:333> | 333 to 333 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| readTags | 341 to 346 | 2 | 0% | WatchCRAP 6 | No data: mutation testing has not run on this repo yet |
| <anonymous:345> | 345 to 345 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| readSharedCode | 349 to 372 | 10 | 0% | CriticalCRAP 110 | No data: mutation testing has not run on this repo yet |
| <anonymous:349> | 349 to 349 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| <anonymous:366> | 366 to 366 | 2 | 0% | WatchCRAP 6 | No data: mutation testing has not run on this repo yet |
Imports
It imports src/encoding.ts, src/env.ts, src/github/client.ts, src/github/contents.ts, src/ops-packages.ts, src/ops-sites.ts, src/ops-types/feed.ts, src/ops-types/sites.ts. Imported by src/maintenance-releases.ts, src/portal-packages.ts.
Source
import type { Env } from "./env";
import { base64Decode } from "./encoding";
import { ghFetch } from "./github/client";
import { readRepoFile, repoBlobPaths } from "./github/contents";
import { readNpm, type FetchLike } from "./ops-packages";
import { readSiteConfig } from "./ops-sites";
import type { PackageSnapshot } from "./ops-types/sites";
import type { SharedCodeApp, SharedCodePackage, SharedCodeView } from "./ops-types/feed";
// SHARED CODE: how far the centralization has got (job_584b4e7f2824; ruling
// capsid/rulings/shared-homes-2026-10-06.md). For each shared package in configuration
// (shared_packages, migrations/0036): its newest tag, the tag each app pins, how many
// releases behind each app is, and how many known local copies are still in the apps.
//
// The apps are the configured sites' namespaces (ops_sites, the same list the inbox and
// the watcher use), each read on its default branch through the GitHub App the watcher
// already uses: one recursive tree, then every package.json outside node_modules (up to
// MANIFESTS_PER_APP) and the Renovate config. Every shared package is a git dependency
// ("github:DrDustinEdwards/<repo>#v1.2.3"), so the manifest names the tag itself; the
// lockfile is read only for a spec with no tag, for the version it resolved. devkit is
// a Renovate preset ("github>DrDustinEdwards/devkit"), so it is found in the Renovate
// config, not in a manifest.
//
// Release tracking (Capsid improvement list, item 13): each package's own repo is read
// too, for the version in its root package.json on the default branch, whether that
// version has a release tag yet (untagged), and, when the package.json is not private,
// npm's latest dist-tag from the registry's abbreviated document (src/ops-packages.ts,
// readNpm). So each panel says tagged, on npm, and which apps are behind. The daily
// maintenance pass tags an untagged version (src/maintenance-releases.ts), using
// releaseDecision below, so the view and the pass agree on what "untagged" means.
//
// Read on demand by the Portal (GET /portal/api/shared-code) and kept for an hour in KV:
// a few dozen GitHub reads per refresh, not per watcher pass. Read-only: it writes
// nothing but its own cache. The key's version moves when the view's shape does, so a
// cached answer without the newer fields is never served.
export const SHARED_CODE_CACHE_KEY = "portal:shared-code:v2";
const CACHE_TTL_SECONDS = 3600;
const MANIFESTS_PER_APP = 8;
const TAGS_PER_PAGE = 100;
const RENOVATE_FILES = ["renovate.json", ".github/renovate.json", "renovate.json5", ".github/renovate.json5", ".renovaterc", ".renovaterc.json"];
interface SharedRow {
name: string;
repo: string;
formerly: string;
local_paths: string;
}
export interface SharedConfig {
name: string;
repo: string;
// Earlier names of the repo (security-headers for site-runtime): an app still pinned
// through GitHub's redirect counts as a consumer.
formerly: string[];
// Known local copies the package replaces, "namespace:path".
local_paths: string[];
}
function jsonList(raw: string | null | undefined): string[] {
try {
const v: unknown = JSON.parse(raw ?? "[]");
return Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : [];
} catch {
return [];
}
}
export async function readSharedConfig(db: D1Database): Promise<SharedConfig[]> {
const { results } = await db.prepare("SELECT name, repo, formerly, local_paths FROM shared_packages ORDER BY name LIMIT 50").all<SharedRow>();
return (results ?? []).map((r) => ({ name: r.name, repo: r.repo, formerly: jsonList(r.formerly), local_paths: jsonList(r.local_paths) }));
}
// ---- pure parts ----------------------------------------------------------------------
/** "owner/repo" lower-cased, from any GitHub spelling npm accepts, with its ref; or null. */
export function gitSpec(spec: string): { repo: string; ref: string | null } | null {
const s = spec.trim();
const m =
/^(?:github:)?([A-Za-z0-9-]+\/[A-Za-z0-9._-]+?)(?:\.git)?(?:#(.+))?$/.exec(s) ??
/^git\+(?:https|ssh):\/\/(?:git@)?github\.com[/:]([A-Za-z0-9-]+\/[A-Za-z0-9._-]+?)(?:\.git)?(?:#(.+))?$/.exec(s) ??
/^https:\/\/github\.com\/([A-Za-z0-9-]+\/[A-Za-z0-9._-]+?)(?:\.git)?(?:#(.+))?$/.exec(s);
if (!m) return null;
// A bare "a/b" without github: and without a ref is a path or a scope, not a repo.
if (!s.startsWith("github:") && !s.startsWith("git+") && !s.startsWith("https://") && m[2] === undefined) return null;
return { repo: m[1].toLowerCase(), ref: m[2] ?? null };
}
/** A Renovate extends entry naming a GitHub preset repo, with its ref. */
export function presetSpec(entry: string): { repo: string; ref: string | null } | null {
const m = /^github>([A-Za-z0-9-]+\/[A-Za-z0-9._-]+?)(?::[^#]+)?(?:#(.+))?$/.exec(entry.trim());
return m ? { repo: m[1].toLowerCase(), ref: m[2] ?? null } : null;
}
const SEMVER = /^v?(\d+)\.(\d+)\.(\d+)$/;
function semver(tag: string): [number, number, number] | null {
const m = SEMVER.exec(tag);
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
}
function cmp(a: [number, number, number], b: [number, number, number]): number {
return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
}
/** The newest release tag, by version, ignoring tags that are not x.y.z. */
export function latestTag(tags: readonly string[]): string | null {
let best: { tag: string; v: [number, number, number] } | null = null;
for (const tag of tags) {
const v = semver(tag);
if (v && (!best || cmp(v, best.v) > 0)) best = { tag, v };
}
return best?.tag ?? null;
}
/** How many release tags are newer than `pinned`, or null when the pin is not a release. */
export function behindBy(pinned: string | null, tags: readonly string[]): number | null {
const p = pinned ? semver(pinned) : null;
if (!p) return null;
return tags.filter((t) => {
const v = semver(t);
return v !== null && cmp(v, p) > 0;
}).length;
}
/** What a repo's root package.json says about a release: the name npm knows it by, its
* version, and whether it is private (never published). */
export interface RootManifest {
name: string | null;
version: string | null;
private: boolean;
}
/** The release fields of a package.json's text. Throws when it is not a JSON object. */
export function rootManifest(raw: string): RootManifest {
const j: unknown = JSON.parse(raw);
if (!j || typeof j !== "object" || Array.isArray(j)) throw new Error("package.json is not a JSON object");
const o = j as Record<string, unknown>;
return { name: typeof o.name === "string" ? o.name : null, version: typeof o.version === "string" ? o.version : null, private: o.private === true };
}
/**
* What a package's version asks for, given the repo's tags:
* - tagged: a tag names this version already (v1.2.3 or 1.2.3);
* - tag: no tag names it and it is newer than every release tag, so it is the next release;
* - backwards: no tag names it but a newer release tag exists. Never tagged: the tag would
* start bump-consumers on an older version and move the apps backwards. Listed instead;
* - not-a-release: no version, or not a plain x.y.z (a pre-release is tagged by hand).
*/
export type ReleaseDecision =
| { kind: "tagged"; tag: string }
| { kind: "tag"; tag: string; newest: string | null }
| { kind: "backwards"; tag: string; newest: string }
| { kind: "not-a-release"; version: string | null };
export function releaseDecision(version: string | null, tags: readonly string[]): ReleaseDecision {
const v = version !== null && /^\d+\.\d+\.\d+$/.test(version) ? semver(version) : null;
if (!v) return { kind: "not-a-release", version };
const existing = tags.find((t) => {
const s = semver(t);
return s !== null && cmp(s, v) === 0;
});
if (existing) return { kind: "tagged", tag: existing };
const tag = `v${version}`;
const newest = latestTag(tags);
if (newest && cmp(v, semver(newest)!) < 0) return { kind: "backwards", tag, newest };
return { kind: "tag", tag, newest };
}
/** What was read of a package's own repo and of npm, or why the repo was not read. */
export interface ReleaseRead {
manifest: RootManifest | string;
npm: PackageSnapshot["npm"] | null;
}
interface AppRead {
namespace: string;
name: string;
blobs: Set<string> | null;
// Each manifest's dependencies of every kind, by path.
manifests: Array<{ path: string; deps: Record<string, string>; lockVersions: Record<string, string> }>;
presets: string[];
error: string | null;
}
/** The view, from what was read. Pure, so every rule is tested without GitHub. */
export function sharedCodeView(
config: readonly SharedConfig[],
tags: ReadonlyMap<string, string[] | string>,
apps: readonly AppRead[],
now: Date,
releases: ReadonlyMap<string, ReleaseRead> = new Map()
): SharedCodeView {
const packages: SharedCodePackage[] = config.map((pkg) => {
const names = new Set([pkg.repo, ...pkg.formerly].map((r) => r.toLowerCase()));
const t = tags.get(pkg.name);
const list = Array.isArray(t) ? t : [];
const latest = latestTag(list);
// Without the tags, how far behind is unknown, never zero.
const behind = (pinned: string | null) => (Array.isArray(t) ? behindBy(pinned, list) : null);
const users: SharedCodeApp[] = [];
for (const app of apps) {
for (const m of app.manifests) {
for (const [dep, spec] of Object.entries(m.deps)) {
const g = gitSpec(spec);
if (!g || !names.has(g.repo)) continue;
const pinned = g.ref ?? m.lockVersions[dep] ?? null;
users.push({ namespace: app.namespace, name: app.name, where: m.path, pinned, behind: behind(pinned), via_old_name: g.repo !== pkg.repo.toLowerCase() });
}
}
for (const entry of app.presets) {
const p = presetSpec(entry);
if (!p || !names.has(p.repo)) continue;
users.push({ namespace: app.namespace, name: app.name, where: "renovate", pinned: p.ref, behind: behind(p.ref), via_old_name: p.repo !== pkg.repo.toLowerCase() });
}
}
const local = pkg.local_paths.map((entry) => {
const at = entry.indexOf(":");
const namespace = at > 0 ? entry.slice(0, at) : "";
const path = (at > 0 ? entry.slice(at + 1) : entry).replace(/\/+$/, "");
const blobs = apps.find((a) => a.namespace === namespace)?.blobs ?? null;
const present = blobs ? [...blobs].some((b) => b === path || b.startsWith(`${path}/`)) : null;
return { namespace, path, present };
});
const release = releaseFields(releases.get(pkg.name), Array.isArray(t) ? list : null);
return {
name: pkg.name,
repo: pkg.repo,
latest,
tags_error: typeof t === "string" ? t : null,
...release,
users,
behind: users.filter((u) => (u.behind ?? 0) > 0).length,
local_copies: local,
local_left: local.filter((l) => l.present === true).length,
};
});
return {
generated: now.toISOString(),
configured: true,
error: null,
apps_read: apps.filter((a) => a.error === null).length,
apps_failed: apps.filter((a) => a.error !== null).map((a) => ({ namespace: a.namespace, error: a.error! })),
packages,
};
}
type ReleaseFields = Pick<SharedCodePackage, "version" | "untagged" | "private" | "npm_latest" | "npm_note" | "manifest_error">;
/** A package's release fields. Untagged only when the tags were read and none names the
* version: without the tags it is unknown, and unknown is not untagged. */
function releaseFields(read: ReleaseRead | undefined, tags: readonly string[] | null): ReleaseFields {
const unread = { version: null, untagged: false, private: false, npm_latest: null, npm_note: null };
if (!read) return { ...unread, manifest_error: null };
if (typeof read.manifest === "string") return { ...unread, manifest_error: read.manifest };
const m = read.manifest;
const decision = tags ? releaseDecision(m.version, tags) : null;
const untagged = decision?.kind === "tag" || decision?.kind === "backwards";
let npm_latest: string | null = null;
let npm_note: string | null = null;
if (m.private) npm_note = null;
else if (!m.name) npm_note = "package.json has no name";
else if (!read.npm) npm_note = "npm was not read";
else if (read.npm.state === "ok") {
npm_latest = read.npm.latest;
if (!npm_latest) npm_note = "npm has no latest dist-tag";
} else npm_note = read.npm.reason;
return { version: m.version, untagged, private: m.private, npm_latest, npm_note, manifest_error: null };
}
// ---- reading -------------------------------------------------------------------------
/** A repo's root package.json, on its default branch or at `ref`, through the App.
* Uncached: the release-tag rule reads it to decide a write. */
export async function readRootManifest(env: Env, full: string, ref?: string): Promise<RootManifest> {
const [owner, repo] = full.split("/");
const query = ref ? `?ref=${encodeURIComponent(ref)}` : "";
const resp = await ghFetch(env, owner, repo, `/repos/${owner}/${repo}/contents/package.json${query}`);
if (!resp.ok) {
await resp.body?.cancel();
throw new Error(`GitHub answered ${resp.status} for ${full}'s package.json${ref ? ` at ${ref.slice(0, 12)}` : ""}`);
}
const data = (await resp.json()) as { content?: unknown; encoding?: unknown };
if (data.encoding !== "base64" || typeof data.content !== "string") throw new Error(`${full}'s package.json came back without its content`);
return rootManifest(base64Decode(data.content));
}
async function readRelease(env: Env, full: string, fetchImpl: FetchLike): Promise<ReleaseRead> {
let manifest: RootManifest;
try {
manifest = await readRootManifest(env, full);
} catch (err) {
return { manifest: err instanceof Error ? err.message : String(err), npm: null };
}
const npm = manifest.private || !manifest.name ? null : await readNpm(fetchImpl, manifest.name);
return { manifest, npm };
}
function depsOf(raw: string): Record<string, string> {
const j = JSON.parse(raw) as Record<string, unknown>;
const out: Record<string, string> = {};
for (const key of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) {
const block = j[key];
if (block && typeof block === "object") for (const [k, v] of Object.entries(block)) if (typeof v === "string") out[k] = v;
}
return out;
}
async function readApp(env: Env, namespace: string, name: string): Promise<AppRead> {
const app: AppRead = { namespace, name, blobs: null, manifests: [], presets: [], error: null };
try {
app.blobs = await repoBlobPaths(env, namespace);
if (!app.blobs) throw new Error("its file tree could not be read (missing, or too large)");
const manifests = [...app.blobs].filter((p) => (p === "package.json" || p.endsWith("/package.json")) && !p.includes("node_modules/")).sort((a, b) => a.split("/").length - b.split("/").length).slice(0, MANIFESTS_PER_APP);
for (const path of manifests) {
const deps = depsOf((await readRepoFile(env, namespace, path)).content);
const lockVersions: Record<string, string> = {};
const untagged = Object.entries(deps).filter(([, spec]) => gitSpec(spec)?.ref === null);
const lockPath = path.replace(/package\.json$/, "package-lock.json");
if (untagged.length && app.blobs.has(lockPath)) {
const lock = JSON.parse((await readRepoFile(env, namespace, lockPath)).content) as { packages?: Record<string, { version?: string }> };
for (const [dep] of untagged) {
const v = lock.packages?.[`node_modules/${dep}`]?.version;
if (v) lockVersions[dep] = `v${v}`;
}
}
app.manifests.push({ path, deps, lockVersions });
}
for (const path of RENOVATE_FILES) {
if (!app.blobs.has(path)) continue;
// renovate.json5 allows comments; the extends line is read either way.
const text = (await readRepoFile(env, namespace, path)).content;
app.presets.push(...[...text.matchAll(/"(github>[^"]+)"/g)].map((m) => m[1]));
}
} catch (err) {
app.error = err instanceof Error ? err.message : String(err);
}
return app;
}
async function readTags(env: Env, full: string): Promise<string[] | string> {
const [owner, repo] = full.split("/");
const resp = await ghFetch(env, owner, repo, `/repos/${owner}/${repo}/tags?per_page=${TAGS_PER_PAGE}`);
if (!resp.ok) return `GitHub answered ${resp.status} for ${full}'s tags`;
return ((await resp.json()) as Array<{ name: string }>).map((t) => t.name);
}
/** The view, from the hour's cache unless `fresh`. A missing table says so. */
export async function readSharedCode(env: Env, now: Date, fresh = false, fetchImpl: FetchLike = (url, init) => fetch(url, init)): Promise<SharedCodeView> {
if (!fresh) {
const cached = await env.APP_KV.get(SHARED_CODE_CACHE_KEY);
if (cached) return JSON.parse(cached) as SharedCodeView;
}
let config: SharedConfig[];
try {
config = await readSharedConfig(env.DB);
} catch (err) {
const why = err instanceof Error ? err.message : String(err);
return { generated: now.toISOString(), configured: false, error: /no such table/i.test(why) ? "shared_packages does not exist yet: migrations/0036_shared_packages.sql is the seat's to apply" : why, apps_read: 0, apps_failed: [], packages: [] };
}
if (config.length === 0) return { generated: now.toISOString(), configured: false, error: null, apps_read: 0, apps_failed: [], packages: [] };
const sites = await readSiteConfig(env.DB);
const apps: AppRead[] = [];
for (const s of sites) apps.push(await readApp(env, s.namespace, s.name));
const tags = new Map<string, string[] | string>();
for (const pkg of config) tags.set(pkg.name, await readTags(env, pkg.repo).catch((err: unknown) => (err instanceof Error ? err.message : String(err))));
const releases = new Map<string, ReleaseRead>();
for (const pkg of config) releases.set(pkg.name, await readRelease(env, pkg.repo, fetchImpl));
const view = sharedCodeView(config, tags, apps, now, releases);
await env.APP_KV.put(SHARED_CODE_CACHE_KEY, JSON.stringify(view), { expirationTtl: CACHE_TTL_SECONDS });
return view;
}