Skip to content

[Feature]: Pin docker images and npm dependencies to a minor version, with CI enforcement #106

@CaseyHoover

Description

@CaseyHoover

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-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:

  • Docker images pinned to major only (susceptible to the same class of breakage as PR fix(devcontainer): pin node base image to 26.0-slim #105):
    • docker-compose.yamlpostgres:17-alpine
    • .devcontainer/docker-compose.yamlpostgres:17-alpine
  • 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.

Affected area

tooling / CI

Additional context

  • Precedent: PR fix(devcontainer): pin node base image to 26.0-slim #105fix(devcontainer): pin node base image to 26.0-slim
  • 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions