Skip to content

ci(release): gate version bumps to 'chore: release' commits to prevent main breaking on optionalDependencies #196

Description

@momics

Summary

Version bumps in workspace manifests (Cargo.toml, package.json, npm/* platform manifests) currently land in feature commits whenever a breaking change is introduced. This breaks main: the bumped version's prebuilt platform binaries (@momics/iroh-http-node-darwin-arm64@X.Y.Z etc.) are only published from CI on tag, so for the entire window between "feature commit lands on main" and "next chore: release tag is cut", npm ci cannot resolve iroh-http-node's optionalDependencies and crashes on every platform.

The two known occurrences:

  • 195a99d (feat(api)!: rename node.connect→dial …) — bumped 0.3.4 → 0.4.0 inside a feature commit. Caught by the docs(adr-014,ffi): record D4 resolution CI run, fixed by 7f7c0c0 (revert to 0.3.4).
  • The same dep-range widening (@momics/iroh-http-shared ^0.3^0.4) was implicit in that bump and would have broken downstream consumers.

The intended workflow is: only scripts/release.sh bumps versions, and it does so immediately before tagging + publishing, so the bump and the binary upload land together. There is currently nothing enforcing this — only convention.

Evidence

  • scripts/version.sh is comprehensive (covers Cargo.toml, package.json, npm/*/package.json, deno.jsonc, deno.json, adapter.ts VERSION constant, dep ranges, both Cargo.locks, package-lock.json, deno.lock) but is only a tool — it doesn't restrict who or when calls it.
  • 7f7c0c0 (revert) commit message documents the failure mode in detail.
  • CI failure log on run 25207770562 shows the exact crash: npm error Missing: @momics/iroh-http-node-darwin-arm64@ from lock file (5 platforms) plus TypeError: Invalid Version: '' from semver when the lockfile has bare {"optional": true} stubs without a version field.

Impact

  • Main breaks silently for the duration of every cross-release breaking change. Two occurrences in two months.
  • Every contributor is one careless cargo set-version away from breaking CI for everyone else.
  • Release surprise: a bump mid-release-cycle widens dep ranges (^0.3^0.4) implicitly, which can re-resolve transitive deps in unexpected ways the next time anyone runs npm install.
  • Loss of trust in CI green — when CI breaks for "release plumbing" reasons unrelated to the change under review, contributors learn to ignore CI failures, which is the worst possible outcome.

Remediation

A small, mechanical CI gate plus a docs note. ~30 minutes of work.

1. Add a CI job version-bump-policy that fails when a non-release commit modifies any version-bearing field. Pseudocode:

# In .github/workflows/ci.yml, run on PR and on push to main.
git fetch origin main
CHANGED=$(git diff --name-only origin/main...HEAD)
VERSION_FILES=$(echo "$CHANGED" | grep -E '(Cargo\.toml|package\.json|deno\.json|deno\.jsonc|VERSION)$' || true)
if [ -n "$VERSION_FILES" ]; then
  # Verify every commit that touches a version file is "chore: release"
  for sha in $(git log origin/main..HEAD --format=%H -- $VERSION_FILES); do
    subject=$(git log -1 --format=%s "$sha")
    case "$subject" in
      "chore: release "*|"chore(release): "*) ;;
      *) echo "Commit $sha touches version file but is not 'chore: release': $subject"; exit 1 ;;
    esac
  done
fi

This catches the 195a99d failure mode at PR time, before merge.

2. Tighten scripts/version.sh with a single early check that fails if it is invoked from a dirty tree or a non-release working state:

# At the top of version.sh, after argument parsing:
if [ -n "$(git status --porcelain)" ] && [ -z "${ALLOW_DIRTY:-}" ]; then
  echo "Error: working tree is dirty. version.sh should only run from a clean tree as part of scripts/release.sh." >&2
  echo "Set ALLOW_DIRTY=1 to override (testing only)." >&2
  exit 1
fi

3. Document the policy in CONTRIBUTING.md (or wherever release process is described): "Do not edit version fields manually. Use scripts/release.sh."

Acceptance criteria

  1. CI workflow contains a version-bump-policy job (or step) that fails when a commit on the PR touches a version field with a non-chore: release subject.
  2. The job is required for merge.
  3. scripts/version.sh refuses to run on a dirty tree without ALLOW_DIRTY=1.
  4. CONTRIBUTING.md (or docs/build-and-test.md) explicitly documents that manual version edits are prohibited.
  5. A test PR that bumps Cargo.toml version outside a chore: release commit fails CI; a chore: release commit passes.

Out of scope

  • Changing the actual release process (still: scripts/release.sh then tag then CI publishes).
  • The optionalDependencies model itself (napi-rs prebuild pattern is fine).
  • Auto-generating release commits — humans still cut releases.

References

  • 7f7c0c0 revert(release): roll workspace back to 0.3.4 to unbreak CI
  • 195a99d feat(api)!: rename node.connect→dial — original offending bump
  • scripts/version.sh — current bump tool
  • scripts/release.sh — intended caller

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium prioritybugSomething isn't workingdxDeveloper experience

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions