You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PR #105 (fix(devcontainer): pin node base image to 26.0-slim) was the second time in recent memory that an upstream minor-version rebuild silently broke CI on this repo. The node:26-slim tag was rebuilt to ship Node 26.1.0, which hung inside devcontainers/ci across PRs #99, #100, and #102. Diagnosis took hours because nothing in our tree had changed — the floating tag had moved underneath us.
Today we have inconsistent pinning rigor across the project:
npm dependencies use ^ (caret) ranges in all 8 package.json files — root, apps/web, apps/api, and every packages/*. A caret range admits any compatible minor or patch release at install time, so a fresh pnpm install on a different machine (or after a lockfile-affecting change) can pull in unreviewed minor bumps.
Already good (no change needed): GitHub Actions are SHA-pinned and enforced via the repo setting; the devcontainer Dockerfile is now pinned to node:26.0-slim; pnpm itself is pinned via packageManager.
Affected: every contributor and every CI run. The risk is silent: things "just stop working" with no traceable diff.
Proposed solution
Pin all Docker image references to major.minor (e.g. postgres:17.6-alpine, matching the style we now use for node:26.0-slim). This caps drift at patch-level rebuilds, which is what PR fix(devcontainer): pin node base image to 26.0-slim #105 established as the project standard.
Convert all npm dependency ranges from ^ to ~ across every package.json. Tilde admits patch-only updates within the locked minor, which mirrors the Docker policy. Lockfile-managed installs already pin exact versions; this hardens the range so a future lockfile regeneration can't silently jump a minor.
Add a CI job that fails on under-pinned references, and document the policy in AGENTS.md / CLAUDE.md. Suggested checks:
Grep all Dockerfile, docker-compose*.yaml, and .devcontainer/** for image: / FROM lines and reject any tag that is not major.minor[.patch] or a SHA digest. Allow -alpine / -slim etc. as suffixes.
Walk all workspace package.json files (dependencies, devDependencies, peerDependencies, optionalDependencies) and reject ^, *, latest, x ranges, and unbounded ranges. Allow ~major.minor.patch, exact pins, workspace:*, link:, file:, npm: aliases, and git URLs.
Run on PR + main. Output should name the offending file and line so failures are self-explanatory.
Cross-check Dependabot config (.github/dependabot.yml) still produces useful PRs after the policy change. The current update-types: [\"minor\", \"patch\"] group will still surface minor bumps as PRs, which is what we want — they just become reviewed events instead of silent drift.
Alternatives considered
Fully exact-pin every npm range (no ~): stricter, but creates much higher Dependabot churn (every patch becomes a manifest PR, not just a lockfile PR) and offers little extra safety since the lockfile already pins exact versions for installs. Tilde is the right tradeoff.
Rely on the lockfile alone: doesn't help when the lockfile is regenerated (e.g. after a conflict resolution or pnpm install on a fresh checkout with a stale store). The range itself needs to be tight.
Renovate instead of Dependabot for finer-grained policy: out of scope; we already use Dependabot and the proposed checks are policy-orthogonal.
Digest-pin Docker images (postgres@sha256:…): maximum safety but very high maintenance cost for a project this size and breaks human-readable diffs. major.minor matches the PR fix(devcontainer): pin node base image to 26.0-slim #105 precedent.
Related precedent in this repo: GitHub Actions are already SHA-pinned and enforced at the repo-settings level ("Require actions pinned to SHA"), so the npm + Docker policy here would round out a consistent "no floating refs" stance across all three ecosystems we depend on.
The CI check is small enough to live as a single shell/Node script invoked from .github/workflows/ci.yaml; no new tooling dependency is required.
Problem / motivation
PR #105 (fix(devcontainer): pin node base image to 26.0-slim) was the second time in recent memory that an upstream minor-version rebuild silently broke CI on this repo. The
node:26-slimtag was rebuilt to ship Node 26.1.0, which hung insidedevcontainers/ciacross PRs #99, #100, and #102. Diagnosis took hours because nothing in our tree had changed — the floating tag had moved underneath us.Today we have inconsistent pinning rigor across the project:
docker-compose.yaml→postgres:17-alpine.devcontainer/docker-compose.yaml→postgres:17-alpine^(caret) ranges in all 8package.jsonfiles — root,apps/web,apps/api, and everypackages/*. A caret range admits any compatible minor or patch release at install time, so a freshpnpm installon a different machine (or after a lockfile-affecting change) can pull in unreviewed minor bumps.Dockerfileis now pinned tonode:26.0-slim;pnpmitself is pinned viapackageManager.Affected: every contributor and every CI run. The risk is silent: things "just stop working" with no traceable diff.
Proposed solution
major.minor(e.g.postgres:17.6-alpine, matching the style we now use fornode:26.0-slim). This caps drift at patch-level rebuilds, which is what PR fix(devcontainer): pin node base image to 26.0-slim #105 established as the project standard.^to~across everypackage.json. Tilde admits patch-only updates within the locked minor, which mirrors the Docker policy. Lockfile-managed installs already pin exact versions; this hardens the range so a future lockfile regeneration can't silently jump a minor.AGENTS.md/CLAUDE.md. Suggested checks:Dockerfile,docker-compose*.yaml, and.devcontainer/**forimage:/FROMlines and reject any tag that is notmajor.minor[.patch]or a SHA digest. Allow-alpine/-slimetc. as suffixes.package.jsonfiles (dependencies,devDependencies,peerDependencies,optionalDependencies) and reject^,*,latest,xranges, and unbounded ranges. Allow~major.minor.patch, exact pins,workspace:*,link:,file:,npm:aliases, and git URLs.main. Output should name the offending file and line so failures are self-explanatory..github/dependabot.yml) still produces useful PRs after the policy change. The currentupdate-types: [\"minor\", \"patch\"]group will still surface minor bumps as PRs, which is what we want — they just become reviewed events instead of silent drift.Alternatives considered
~): stricter, but creates much higher Dependabot churn (every patch becomes a manifest PR, not just a lockfile PR) and offers little extra safety since the lockfile already pins exact versions for installs. Tilde is the right tradeoff.pnpm installon a fresh checkout with a stale store). The range itself needs to be tight.postgres@sha256:…): maximum safety but very high maintenance cost for a project this size and breaks human-readable diffs.major.minormatches the PR fix(devcontainer): pin node base image to 26.0-slim #105 precedent.Affected area
tooling / CI
Additional context
fix(devcontainer): pin node base image to 26.0-slim.github/workflows/ci.yaml; no new tooling dependency is required.