Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 45 additions & 13 deletions .github/workflows/version-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,35 @@ name: version-guard

# Fail any PR to main that changes SHIPPED BYTES without bumping package.json's
# version. Shipped bytes = the committed esbuild bundle under build/** (it IS
# what npm and marketplace users run), plus src/** (non-test) and
# requirements.txt as first-cause detectors with better error messages, plus
# the RUNTIME `dependencies` map in package.json. build/** was added
# 2026-07-20: it subsumes every cause that changes what users receive — direct
# deps, lockfile-only transitive bumps, and esbuild codegen changes — none of
# which the src/deps detectors could see (a lockfile-only runtime-dep bump
# merged un-bumped on 2026-07-15 and sat unshipped for two days).
# what npm and marketplace users run), plus the files package.json `files[]`
# ships verbatim (requirements.txt and the src/**/*.py sidecars), plus the
# RUNTIME `dependencies` map in package.json. build/** was added 2026-07-20:
# it subsumes every cause that changes what users receive — direct deps,
# lockfile-only transitive bumps, and esbuild codegen changes — none of which
# the src/deps detectors could see (a lockfile-only runtime-dep bump merged
# un-bumped on 2026-07-15 and sat unshipped for two days).
# Docs-only, test-only, and github-actions changes stay exempt; byte-neutral
# devDependency bumps stay exempt because they leave build/ untouched.
# Rationale: publish.yml only publishes when the version is absent from npm,
# so an un-bumped shipped-byte change silently never releases. This guard
# makes the bump mandatory; dependabot-rebuild.yml auto-bumps bot PRs whose
# rebuilt bundle changed, so Dependabot automation keeps flowing.
# The guard file itself lives in .github/ (doesn't ship), so it needs no bump.
#
# TypeScript under src/ is a FIRST-CAUSE DETECTOR, not a shipped file: it
# reaches users only after esbuild inlines it into build/index.js. So it only
# implies a bump when build/** changed too — a src/**/*.ts edit that leaves the
# committed bundle byte-identical (comments, formatting, types) ships nothing
# and owes no version. That is safe because ci.yml's "Verify committed build/
# matches source" step rebuilds and requires `git diff --quiet build/`, and it
# runs in the `test` job whose `test (22)`/`test (24)` contexts are REQUIRED by
# branch protection — so at merge time an unchanged build/ provably matches
# src/. Keep that step and its required contexts, or this exemption loses its
# proof (conformance-check.sh asserts both).
# Everything else under src/ (the .py sidecars, which package.json ships
# verbatim rather than bundling) stays an unconditional detector, and the rule
# is written fail-safe: only `.ts` is treated as bundle-only, so any new file
# type added under src/ defaults to requiring a bump.

on:
pull_request:
Expand Down Expand Up @@ -57,12 +72,29 @@ jobs:
changed="$(git diff --name-only "${BASE_SHA}...${HEAD_SHA}")"
echo "Changed files:"; printf '%s\n' "$changed" | sed 's/^/ /'

# Shipped bytes: the committed bundle itself (build/**), plus source
# and the python dependency manifest (minus tests/mocks) as
# first-cause detectors.
code="$(printf '%s\n' "$changed" \
| grep -E '^(src/.*|requirements\.txt|build/.*)$' \
# Files that ARE shipped bytes: the committed bundle itself, plus
# everything package.json ships verbatim (requirements.txt and the
# non-.ts sources — i.e. the python sidecars). Anything new under
# src/ that is not .ts lands here too, which is the fail-safe side.
ships="$(printf '%s\n' "$changed" \
| grep -E '^(build/.*|requirements\.txt|src/.*)$' \
| grep -vE '^src/.*\.ts$' \
| grep -vE '(/__tests__/|/__mocks__/)' || true)"

# TypeScript sources reach users only through the bundle, so they
# imply a bump only when the bundle actually moved. Listed alongside
# build/** purely for a better error message.
ts="$(printf '%s\n' "$changed" \
| grep -E '^src/.*\.ts$' \
| grep -vE '(\.test\.ts$|\.spec\.ts$|/__tests__/|/__mocks__/)' || true)"
bundle_changed="$(printf '%s\n' "$changed" | grep -E '^build/' || true)"

code="$ships"
if [ -n "$ts" ] && [ -n "$bundle_changed" ]; then
code="$(printf '%s\n%s' "$ships" "$ts" | grep -v '^$' || true)"
elif [ -n "$ts" ]; then
echo "::notice::src/ TypeScript changed but the committed bundle is byte-identical — nothing ships, so no version bump is required. (ci.yml's build-verify step proves build/ matches src/.)"
fi

# Runtime deps are shipped code too: they are inlined into the
# committed bundle. Compare only the `dependencies` map —
Expand Down Expand Up @@ -122,7 +154,7 @@ jobs:
if [ -n "$deps_changed" ]; then
echo "::error::Runtime dependencies changed (the shipped bundle changes) but package.json version is unchanged (${oldv}). Bump at least a patch + add a CHANGELOG entry so publish.yml actually ships the dependency update: pnpm version patch --no-git-tag-version"
else
echo "::error::Shipped bytes changed (src/, requirements.txt, or the committed build/ bundle) but package.json version is unchanged (${oldv}). Bump it at least a patch: pnpm version patch --no-git-tag-version (and add a CHANGELOG entry)."
echo "::error::Shipped bytes changed (the committed build/ bundle, requirements.txt, or a verbatim-shipped src/ file) but package.json version is unchanged (${oldv}). Bump it at least a patch: pnpm version patch --no-git-tag-version (and add a CHANGELOG entry)."
fi
exit 1
fi
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Fixed
- **`version-guard` no longer demands a version bump for byte-neutral `src/` changes.** The shipped-bytes detector treated every non-test file under `src/` as shipped, but TypeScript there reaches users only after esbuild inlines it into `build/index.js` — so a comment-, formatting- or type-only edit that leaves the committed bundle byte-identical was hard-blocked, leaving only two bad options: publish a release of literally nothing, or do not write the comment. `src/**/*.ts` is now a first-cause detector that implies a bump only when `build/**` changed too. The exemption is sound rather than merely convenient: ci.yml's "Verify committed build/ matches source" step rebuilds and requires `git diff --quiet build/`, and it runs in the `test` job whose `test (22)`/`test (24)` contexts are required by branch protection — so at merge time an unchanged `build/` provably matches `src/`. Everything else under `src/` (the verbatim-shipped `*_reader.py` sidecars), `requirements.txt` and `build/**` stay unconditional detectors, and the rule is written fail-safe: only `.ts` counts as bundle-only, so any new file type under `src/` still requires a bump.
- **Dependabot auto-bump silently stopped staging its own changes.** `dependabot-rebuild.yml`'s bump step writes the patch version, syncs the plugin manifests and prepends a CHANGELOG entry, then staged them with `git add package.json CHANGELOG.md build .claude-plugin .agents codex .hermes-plugin .antigravity-plugin`. Once `.hermes-plugin/` was removed that pathspec matched nothing, and `git add` is all-or-nothing — it exited 128 and staged **none** of the others, with `2>/dev/null || true` hiding the failure. The following step re-adds only `build/`, so a Dependabot PR would have committed a rebuilt bundle with no version bump and no changelog entry, failing `require-version-bump` and blocking the automation that is meant to run without a human. Dropped the stale path, and dropped the error suppression so a future missing path fails loudly instead of silently skipping the bump.

### Removed
Expand Down
Loading