Skip to content

feat(next-version): support monorepo VERSION paths via --version-path + .gstack/version-path#1627

Open
cfeddersen wants to merge 1 commit into
garrytan:mainfrom
cfeddersen:fix/monorepo-version-path
Open

feat(next-version): support monorepo VERSION paths via --version-path + .gstack/version-path#1627
cfeddersen wants to merge 1 commit into
garrytan:mainfrom
cfeddersen:fix/monorepo-version-path

Conversation

@cfeddersen
Copy link
Copy Markdown

@cfeddersen cfeddersen commented May 20, 2026

Summary

bin/gstack-next-version hardcodes the VERSION file at <repo-root>/VERSION. In monorepos where versioning is subproject-scoped — one shippable app inside a larger repo — every PR's VERSION lookup 404s, the workspace-aware ship queue silently empties, and parallel /ship sessions all bump from "current main + 1". The result is a cascade of slot collisions.

The fix adds a --version-path CLI flag and a repo-local .gstack/version-path config file. Resolution priority: CLI flag > config file > "VERSION" (existing default, backward-compatible). The resolved path threads through every call site that touches the file. The previous misleading warning "could not fetch VERSION (fork or private)" becomes honest and actionable.

Repro

A private monorepo I work in. Root VERSION is absent; the real VERSION lives at Apps With Spaces/web-app/VERSION (path intentionally has a space to exercise URL-encoding). In one day, four sequential collisions:

0.4.0.1 → 0.5.0.0 → 0.5.0.1 → 0.5.0.2 → 0.5.0.3

Every parallel /ship saw an empty claimed array because gh api repos/{owner}/{repo}/contents/VERSION?ref=<head> returned 404 for every open PR. The util emitted "PR #X: could not fetch VERSION (fork or private)" for each — a misleading warning, since the real cause was wrong path.

Confirmed against an open PR of that repo:

$ gh api 'repos/{owner}/{repo}/contents/VERSION?ref=...' → 404
$ gh api 'repos/{owner}/{repo}/contents/Apps%20With%20Spaces/web-app/VERSION?ref=...' → 0.5.1.0

The file is fully accessible — the util just queries the wrong path.

Root cause

Three call sites all hardcode "VERSION":

  1. readBaseVersiongit show origin/${base}:VERSION
  2. fetchGithubClaimedgh api repos/{owner}/{repo}/contents/VERSION?ref=...
  3. fetchGitlabClaimedglab api projects/:id/repository/files/VERSION?ref=...
  4. scanSiblingsjoin(p, "VERSION") for local sibling-worktree scans

No flag, env, or config knob exists to override.

Fix

New helper resolveVersionPath(override, repoRoot) with priority:

  1. --version-path <path> CLI flag (highest)
  2. .gstack/version-path file at repo root — a single line, the relative path from repo root. Committed to the repo so all collaborators benefit.
  3. "VERSION" default (current behavior — fully backward-compatible)

Path threads through all four call sites. GitHub's Contents API gets the path via encodeURI so spaces in subproject names (e.g. Apps With Spaces/...) become %20 while / segments stay intact. GitLab's files API gets encodeURIComponent (the full path becomes a single URL-encoded segment with %2F separators), which matches the existing pattern for ref=.

The resolved path surfaces in the JSON output as version_path so /ship and operators can see what got picked. The previous warning is replaced with:

PR #X: could not fetch <path> (fork, private, or wrong path — try --version-path or .gstack/version-path)

End-to-end verification

Patched binary, run against the monorepo with .gstack/version-path containing Apps With Spaces/web-app/VERSION:

{
  "version": "0.6.0.1",
  "version_path": "Apps With Spaces/web-app/VERSION",
  "claimed": [
    { "pr": 274, "version": "0.6.0.0", ... },
    { "pr": 272, "version": "0.5.1.1", ... }
  ],
  "reason": "bumped past claimed 0.6.0.0",
  ...
}

Before the fix: "claimed": [] and 3 misleading "could not fetch VERSION (fork or private)" warnings. After: queue is fully visible, slot allocation is correct.

Tests added

test/gstack-next-version.test.ts gains 7 new tests:

6 unit tests for resolveVersionPath (new pure-function export):

  • CLI flag wins over .gstack/version-path
  • .gstack/version-path config is picked up when no flag
  • Trims whitespace and ignores lines after the first
  • Empty config file falls back to default "VERSION"
  • Missing config file falls back to default "VERSION"
  • Empty override string ("") falls back to config/default (defensive)

1 integration smoke that drives --version-path end-to-end and asserts it surfaces in the JSON output as version_path.

The existing integration test gains an assertion that version_path defaults to "VERSION" when no flag/config present.

Test plan

  • bun test test/gstack-next-version.test.ts — 28/28 pass (was 21 pre-change)
  • Bun bundles cleanly (no TS errors): bun --bun build bin/gstack-next-version
  • End-to-end against real monorepo: queue correctly picks up subproject VERSION at all 4 call sites
  • Backward-compatible: no flag, no config, no change in behavior

Out of scope (surfaced while investigating)

  • CHANGELOG-path is the same problem class. This binary doesn't touch CHANGELOG (only /ship does), so it's a separate fix in a separate file. Happy to follow up if useful.
  • gstack-pr-title-rewrite.sh also references VERSION but takes the version string as an argument — not a file-path bug. No fix needed.

🤖 Generated with Claude Code

… + .gstack/version-path

The workspace-aware ship queue hardcoded the VERSION file at the repo root.
In monorepos where versioning is subproject-scoped (one app inside a larger
repo), every PR's VERSION lookup 404s, the queue silently empties, and
parallel /ship sessions all bump from "current main + 1" — producing a
cascade of slot collisions.

Repro: tinas-second-brain repo. Root VERSION is absent; the real VERSION
lives at "Tinas Second Brain/health-tracker/VERSION". In one day, four
sequential collisions: 0.4.0.1 -> 0.5.0.0 -> 0.5.0.1 -> 0.5.0.2 -> 0.5.0.3.

Fix: add a --version-path flag and a repo-local .gstack/version-path
config file. Resolution priority: CLI flag > .gstack/version-path > "VERSION".
The resolved path threads through all four call sites — git show
origin/<base>:<path>, the GitHub Contents API, the GitLab files API, and
the local sibling-worktree scan — and shows up in the JSON output as
version_path so /ship and operators can see what got picked.

The previous warning "could not fetch VERSION (fork or private)" was
misleading whenever the real cause was wrong path. The new wording names
the path that 404'd and hints at the two knobs.

Backward-compatible: no flag, no config, no change in behavior.

Tests: 6 unit tests for resolveVersionPath (priority, parsing, blank /
missing / empty edge cases) + a second integration smoke that drives
--version-path end-to-end and asserts it surfaces in JSON output.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant