Skip to content

Dependency Policy

Ankit Upadhyay edited this page Aug 13, 2026 · 1 revision

Dependency policy

This page explains how dependencies enter, update, and are constrained in openrunic, and the reasoning behind each rule. It is for anyone reviewing a Dependabot pull request or adding a package.

Adding a dependency

Ask three questions before adding anything.

Does a platform primitive already do this? The API generates its OpenAPI document from Zod's native JSON Schema output rather than adding a wrapper library, on the grounds that OpenAPI 3.1's schema object already is JSON Schema 2020-12. The component library uses a native <select> and a native <dialog> rather than headless-component packages.

Is its licence on the allow list? .grant.yaml is deny-by-default. See Security and supply chain.

What is its transitive weight? The SBOM covers the whole tree, and grype scans all of it. A small package with a large dependency graph is not a small package.

The install-time guards

Two settings in pnpm-workspace.yaml apply to everything.

minimumReleaseAge: 4320

pnpm refuses to install any package version published less than three days ago. Most compromised-release incidents are caught and yanked within hours; a three-day quarantine turns the fastest-moving class of supply-chain attack into a non-event for this repository, at the cost of never being first to a new patch.

onlyBuiltDependencies:
  - '@prisma/client'
  - '@prisma/engines'
  - prisma
  - esbuild
  - sharp
  - unrs-resolver

pnpm 10 blocks dependency postinstall scripts by default. Only these six may run. A postinstall script executes arbitrary code on every developer machine and every CI runner, so the allow list is short and each entry is there because the package genuinely cannot work without it.

Dependabot

Configured in .github/dependabot.yml, weekly on Monday, targeting dev, with a limit of ten open pull requests per ecosystem. npm uses the chore(repo) prefix and GitHub Actions uses ci(repo), because the PR-title gate requires a scope from the allowed list and deps is not one.

Dependabot reads this file from the default branch (main), not from dev. target-branch only aims the pull requests. A change here sits inert until it is promoted, so keep the two copies byte-identical.

Grouping, and the trap in it

groups:
  typescript-eslint:
    patterns: ['typescript', '@types/*', 'eslint*', '@typescript-eslint/*']
    update-types: [minor, patch]
  testing:
    patterns: ['vitest*', '@vitest/*', '@testing-library/*']
    update-types: [minor, patch]
  build-tooling:
    patterns: ['turbo', 'prettier', 'husky', 'lint-staged']
    update-types: [minor, patch]
  minor-and-patch:
    patterns: ['*']
    update-types: [minor, patch]

Two rules are doing real work here, and both are the kind of thing you only learn by being bitten.

Every group declares update-types, including the named ones. A named group without it silently swallows majors too. That is how a breaking major bump ends up inside an unreviewable group pull request instead of the individual pull request a breaking major needs. Majors always get their own pull request and their own review.

The catch-all group must stay last. A dependency joins the first group it matches, so an earlier * pattern would swallow every named group above it.

The GitHub Actions ecosystem groups minor and patch only for the same reason: action majors are routinely breaking, so each lands alone.

Ignores

Every ignore entry needs a prose rationale and a revisit condition. There is exactly one today, on TypeScript majors: the compiler rewrite ships without a JavaScript compiler API that the current lint tooling peer-caps against, so majors are blocked while minors and patches still flow. The entry records the condition under which it is removed.

Overrides

pnpm-workspace.yaml carries exact-version overrides for transitive dependencies that cannot be fixed in range:

overrides:
  postcss: 8.5.26
  sharp: 0.35.1
  esbuild: 0.28.2

Two conventions here matter.

Exact versions, never ranges. A range key silently stops matching when the parent changes its own pin, and an override that stops applying leaves you believing a vulnerability is fixed when it is not.

Every override carries its reason and its re-check condition in a comment. The first two are advisory fixes where the parent framework pins a lower version exactly. The third is subtler and worth knowing about: the bundler ships a prebuilt binary, so its embedded standard library is what vulnerability scanners see, and the old line embeds a version with a critical finding. It is pulled in transitively by a build-tooling dependency, so the override is the only route to it.

Reviewing a Dependabot pull request

  1. Read the changelog. Green CI on a dependency bump means the suite still passes, not that behaviour is unchanged. A green tick is not a review.
  2. Check whether the group swallowed a major. If it did, the config is wrong; fix the config rather than merging the group.
  3. Check the lockfile diff, not only the manifest. A patch bump on a direct dependency can move a large transitive subtree.
  4. Confirm the supply-chain gate ran. Dependabot runs with its own secret store, so the Sonar stage is skipped for its pull requests, and grype's SARIF upload is skipped for Dependabot-actor runs. The job's own exit code still gates.
  5. Prefer a small revert to a fast fix. These are dependency changes, not feature work.

Version pinning across the toolchain

Thing Pinned where
Node .nvmrc and engines in package.json, with engine-strict=true
pnpm packageManager in package.json; CI reads it rather than declaring its own
GitHub Actions Full commit SHA in every uses:, version in a trailing comment
CLI security tools Version plus SHA-256 in the workflow, verified before extraction
PostgreSQL in CI postgres:17-alpine, with a note to pin it to production's major once one exists

Related pages

Clone this wiki locally