Skip to the content

Capsid menu

src/shared-code.ts

342 lines and 40 functions in DrDustinEdwards/capsid, at commit daa875b on 2026-10-24. This file on GitHub.

Functions

The functions of shared-code.ts
FunctionLinesComplexityCoveredCRAPMutation
jsonList60 to 67471%OkCRAP 4.4No data: mutation testing has not run on this repo yet
<anonymous:63>63 to 631100%OkCRAP 1No data: mutation testing has not run on this repo yet
readSharedConfig69 to 722100%OkCRAP 2No data: mutation testing has not run on this repo yet
<anonymous:71>71 to 711100%OkCRAP 1No data: mutation testing has not run on this repo yet
gitSpec77 to 879100%WatchCRAP 9No data: mutation testing has not run on this repo yet
presetSpec90 to 933100%OkCRAP 3No data: mutation testing has not run on this repo yet
semver97 to 1002100%OkCRAP 2No data: mutation testing has not run on this repo yet
cmp102 to 1043100%OkCRAP 3No data: mutation testing has not run on this repo yet
latestTag107 to 1146100%WatchCRAP 6No data: mutation testing has not run on this repo yet
behindBy117 to 1243100%OkCRAP 3No data: mutation testing has not run on this repo yet
<anonymous:120>120 to 1232100%OkCRAP 2No data: mutation testing has not run on this repo yet
rootManifest135 to 1406100%WatchCRAP 6No data: mutation testing has not run on this repo yet
releaseDecision156 to 1687100%WatchCRAP 7No data: mutation testing has not run on this repo yet
<anonymous:159>159 to 1622100%OkCRAP 2No data: mutation testing has not run on this repo yet
sharedCodeView187 to 2461100%OkCRAP 1No data: mutation testing has not run on this repo yet
<anonymous:194>194 to 23714100%WatchCRAP 14No data: mutation testing has not run on this repo yet
<anonymous:195>195 to 1951100%OkCRAP 1No data: mutation testing has not run on this repo yet
behind200 to 2002100%OkCRAP 2No data: mutation testing has not run on this repo yet
<anonymous:217>217 to 2245100%OkCRAP 5No data: mutation testing has not run on this repo yet
<anonymous:221>221 to 2211100%OkCRAP 1No data: mutation testing has not run on this repo yet
<anonymous:222>222 to 2222100%OkCRAP 2No data: mutation testing has not run on this repo yet
<anonymous:233>233 to 2332100%OkCRAP 2No data: mutation testing has not run on this repo yet
<anonymous:235>235 to 2351100%OkCRAP 1No data: mutation testing has not run on this repo yet
<anonymous:242>242 to 2421100%OkCRAP 1No data: mutation testing has not run on this repo yet
<anonymous:243>243 to 2431100%OkCRAP 1No data: mutation testing has not run on this repo yet
<anonymous:243>243 to 2431100%OkCRAP 1No data: mutation testing has not run on this repo yet
releaseFields252 to 26910100%WatchCRAP 10No data: mutation testing has not run on this repo yet
readRootManifest275 to 286675%WatchCRAP 6.6No data: mutation testing has not run on this repo yet
readRelease288 to 29750%WatchCRAP 30No data: mutation testing has not run on this repo yet
depsOf299 to 30760%CriticalCRAP 42No data: mutation testing has not run on this repo yet
readApp309 to 339110%CriticalCRAP 132No data: mutation testing has not run on this repo yet
<anonymous:314>314 to 31430%WatchCRAP 12No data: mutation testing has not run on this repo yet
<anonymous:314>314 to 3141100%OkCRAP 1No data: mutation testing has not run on this repo yet
<anonymous:318>318 to 31810%OkCRAP 2No data: mutation testing has not run on this repo yet
<anonymous:333>333 to 33310%OkCRAP 2No data: mutation testing has not run on this repo yet
readTags341 to 34620%WatchCRAP 6No data: mutation testing has not run on this repo yet
<anonymous:345>345 to 34510%OkCRAP 2No data: mutation testing has not run on this repo yet
readSharedCode349 to 372100%CriticalCRAP 110No data: mutation testing has not run on this repo yet
<anonymous:349>349 to 3491100%OkCRAP 1No data: mutation testing has not run on this repo yet
<anonymous:366>366 to 36620%WatchCRAP 6No 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;
}