Skip to content

fix(release): stop shipping binaries that report the previous version - #50

Merged
martient merged 2 commits into
developfrom
fix/release-version-guard
Sep 27, 2026
Merged

martient merged 2 commits into
developfrom
fix/release-version-guard

Conversation

@martient

Copy link
Copy Markdown
Owner

Why

committy --update upgraded to v1.10.0 and committy --version still printed 1.9.1. The update worked; the release was mislabelled. The v1.10.0 tag points at a tree whose Cargo.toml still says 1.9.1, and since --version prints env!("CARGO_PKG_VERSION"), every published v1.10.0 asset honestly reports the old version — which also means self-update keeps offering v1.10.0 forever.

The root cause is that bumping version files was opt-in (--bump-files), so the tag was cut on a commit that predated any version change.

What this changes

A guard in the release workflow. A new verify-version job compares Cargo.toml with the tag name and both create-release and the build matrix wait on it, so a mismatched tag fails before a release is created or any asset is uploaded.

Version files are bumped by default. committy tag now updates and commits version files unless --no-bump-files is passed. --bump-files is still accepted for existing scripts; passing both flags is an error.

Two latent bugs surfaced once the bump became the default, and are fixed here:

  • A version file that already carried the new version was still reported as updated, and the resulting empty commit failed and aborted the whole tag operation.
  • Multi-package repositories staged the bumped files but never committed them, so their tags pointed at a commit still carrying the old versions — the same defect, in the other code path.

Tests

tests/tag_bump_files_tests.rs covers the default bump, --no-bump-files, the retained --bump-files, the conflicting-flag error, and that a multi-package tag's tree actually contains the bumped version. tests/release_workflow_tests.rs pins the workflow guard and its job wiring.

cargo test, cargo fmt --all -- --check and cargo clippy --all-targets --all-features are clean.

Note on the published v1.10.0

This prevents a repeat; it does not repair the release that is already out. Anyone on v1.10.0 has a binary that reports 1.9.1. Fixing that needs a re-tag of v1.10.0 or a jump to v1.10.1, which touches published artifacts and is a separate decision.

Behaviour change

committy tag now writes and commits to version files where it previously did nothing. That is the intent, but it is a behaviour change for anyone tagging in a repo with a Cargo.toml/package.json who relied on the old default; --no-bump-files restores it.

v1.10.0 was tagged on a tree whose Cargo.toml still said 1.9.1, so every
published binary reported the old version to --version and self-update kept
offering the same release forever.

A verify-version job now compares the Cargo.toml version with the tag name and
both the release creation and the build matrix wait on it, so a mismatched tag
fails before any release or asset is published.
Tagging without --bump-files left the version files stale, which is how a
release can be cut whose binaries report the previous version. Version files
are now bumped and committed unless --no-bump-files is passed; --bump-files is
still accepted so existing scripts keep working, and passing both is an error.

Two bugs surfaced by making this the default are fixed alongside it:

- version files that already carried the new version were reported as updated,
  and the empty commit that followed aborted the whole tag operation
- multi-package repositories staged the bumped files but never committed them,
  so their tags pointed at a commit with the old versions
@martient martient self-assigned this Sep 27, 2026
@martient martient added the bug Something isn't working label Sep 27, 2026
@martient
martient merged commit b9c9389 into develop Sep 27, 2026
1 check passed
@martient
martient deleted the fix/release-version-guard branch September 27, 2026 19:09
martient added a commit that referenced this pull request Sep 27, 2026
* fix: tag mapping

* fix: scope detection

* fix: tag behavior

* fix: prevent tag dry-run side effects

* Feat agent convention enforcement (#41)

* fix(core): preserve linked worktree discovery

* feat(core): stabilize agent json contract

* feat(core): add typed convention capabilities

* feat(core): add native convention hooks

* feat(agents): ship shared workflow plugin

* chore(agents): streamline convention guidance

* chore(core): satisfy strict clippy checks

* fix(docs): update starlight social config

* fix(core): preserve legacy branch defaults

* fix(hooks): support Windows release builds

* chore: bump version to 1.9.1

* docs(agents): add September 2026 agent compatibility audit

Audit the repository's agent-facing surfaces (AGENTS.md/CLAUDE.md, the
shared Codex/Claude plugin and skills, the schema capability contract,
generated hooks/CI, and the src/ai LLM integration) against the current
state of the agent-integration ecosystem.

Records 15 prioritised findings covering defects, compatibility gaps and
removals, plus two explicit non-recommendations.

* fix(cli): make agent invocations parse and fail machine-readably

Agents consistently failed against the CLI for three mechanical reasons,
each now reproduced and covered by tests/agent_ergonomics_tests.rs.

- --non-interactive and -q/--quiet are global, so they parse on either
  side of the subcommand. --verbose stays root-only: -v belongs to
  `branch --validate` and --verbose to `config validate|show`, and
  promoting it would steal both shorts. A test pins that trade-off.
- Argument-parse rejections now emit the machine envelope on stdout with
  code "invalid_usage" when --output json was requested, instead of an
  empty stdout and prose on stderr. Text mode is unchanged, and --help
  and --version keep their plain-text form and exit 0.
- schema capabilities cover the whole agent command surface (tag, bump,
  changelog, group-commit, config, packages, schema) and each entry now
  declares `mutating` and `requires_confirmation`, so an agent can reason
  about consent without pattern-matching command names.
- COMMITTY_NONINTERACTIVE is documented in AGENTS.md, CLAUDE.md and the
  agent-workflows reference; it makes flag placement irrelevant.

Also withdraws the audit's blanket "do not build an MCP server": that was
argued against eager-loading MCP, which the 2026-07-28 stateless spec has
moved off.

* fix(cli): global flag placement, usage envelope, capability surface

Second half of the agent-ergonomics change: main.rs flag globals and the
clap rejection handler, the expanded schema capability list, the
ergonomics test suite, and the docs describing flag placement.

* test(agents): cover agent CLI ergonomics and update the audit

Adds tests/agent_ergonomics_tests.rs (12 tests) covering flag placement
on both sides of the subcommand, the invalid_usage envelope on argv
rejection, the expanded capability surface with consent metadata, and
the presence of COMMITTY_NONINTERACTIVE in agent-facing docs.

Documents flag placement in the agent-workflows reference and records
the diagnosis in the audit, withdrawing its blanket recommendation
against an MCP server.

* docs(agents): document flag placement and the invalid_usage envelope

* docs(audit): record the CLI ergonomics diagnosis and withdraw the MCP verdict

* docs(agents): ban AI attribution trailers in commits and PRs

Tooling re-adds Co-Authored-By and Claude-Session by default, so the
prohibition has to live where agents read it rather than being restated
each session.

* fix(ai): make the Ollama provider work and stop sending an empty prompt

Four real defects from the agent audit, plus one withdrawn finding.

- Ollama requests now set `stream: false`. The endpoint defaults to
  streaming, so every previous call received newline-delimited events and
  died in LlmError::Parse before silently falling back to the default
  message. The provider has never worked.
- `format` moves from `options` to the top level, where Ollama reads it,
  and now carries a JSON Schema for AiCommitSuggestion rather than the
  string "json", so decoding is constrained instead of merely requested.
- The default (non-sensitive) AI path sent only the group name and an
  instruction not to reveal anything, leaving the model nothing to work
  from. It now sends a redacted shape summary: file count, extension
  histogram and top-level areas. No filename, no path below the first
  segment, and no file content is sent in either mode.
- `--ai-diff-lines-per-file` was declared, documented and accepted but
  never read. Removed, along with the dead LlmError::_Timeout variant and
  the commented-out LlmProvider enum.

The audit's P0-4 ("--ai silently ignored in --mode apply") was wrong and
is withdrawn: the apply arm has always had its own AI block. The real
problem there was duplication -- plan and apply carried identical copies
of group building and the AI block -- which is what made the misreading
possible. Both are extracted into build_groups() and ai_user_prompt().

Request bodies are now built by pure functions with unit tests, so the
wire format is verifiable without a network call.

* fix(ci): run checks on pull requests targeting nested branch names

The pull_request trigger filtered on branches: ["*"], and in Actions
branch filters `*` matches everything except `/`. Any pull request whose
base branch contains a slash therefore matched nothing and ran no checks
at all -- silently, since a workflow that does not trigger looks the same
as one with nothing to report.

That covers every stacked pull request in this series (all but the first
target a claude/... base and have zero check runs), and would equally
cover anything based on a feat/ or fix/ prefix.

`**` matches across slashes, which is what the filter meant.

* docs(agents): make AGENTS.md canonical and reach every runtime

AGENTS.md and CLAUDE.md were near-identical files kept in sync by hand;
the diff was two lines and had already started drifting (the trailer ban
landed in one of them only). AGENTS.md is now the single source and
CLAUDE.md is an @AGENTS.md import plus the one genuinely Claude-specific
line, which is the pattern the ecosystem settled on.

Two runtimes read neither file, so they now get thin pointers:
.github/copilot-instructions.md and GEMINI.md.

Skills are exposed at .agents/skills, the cross-tool discovery path that
Codex, Claude Code and Copilot CLI all resolve, via a symlink to the
canonical copy under plugins/committy/skills -- no duplication. A test
guards the symlink, because a checkout that materialises it as a plain
file would break discovery silently.

Skill frontmatter gains the spec's optional fields: license,
allowed-tools (so security-conscious runtimes can pre-authorise exactly
what each skill needs) and metadata carrying a skill version independent
of the CLI version.

committy-release referenced siblings with Codex's `$skill` syntax inside
a body shared by both runtimes; they are now named plainly and each
runtime resolves them its own way.

The COMMITTY_NONINTERACTIVE docs test now accepts a file that defers to
AGENTS.md rather than restating it, which is what the split requires.

* test(agents): cover the plugin manifests and skill frontmatter

AGENTS.md asks for public-seam tests over "machine output, exit codes,
Git behavior, hook protocol, and plugin manifests". The first four were
covered; nothing compiled or executed the manifests, so a typo in any of
the six JSON and Markdown files shipped silently.

Nine tests: both marketplace manifests parse and their declared sources
resolve to real directories carrying the expected plugin manifest; the
Claude and Codex plugin manifests agree on name, version and license;
the Codex skills path resolves; skill frontmatter uses only the keys the
Agent Skills spec allows and carries the two required ones; a skill's
frontmatter name matches its directory; descriptions carry a trigger
rather than just a capability; no skill body uses runtime-specific
invocation syntax, since that body is shared by Codex and Claude Code;
and every skill is reachable from the cross-tool .agents/skills path.

Each assertion was mutation-checked -- a bogus frontmatter key and a
marketplace source pointing at a missing directory both fail with the
message that names the problem.

* chore(ci): modernise the generated workflow and tighten the agent guardrail

The workflow `hooks install --with-ci` writes for users pinned
actions/checkout@v4 and ran `cargo install committy --locked` on every
pull request -- a multi-minute Rust build per run, uncached, with no
toolchain pin. It now pins checkout@v5, pins the toolchain, caches the
build, and takes non-interactive mode from COMMITTY_NONINTERACTIVE so the
command cannot be broken by flag placement. The repository's own three
workflows carried the same stale checkout pin and are bumped too.

The local PreToolUse guardrail grepped the raw tool input for nine
substrings, which made it both over- and under-inclusive. Over: it
matched the *text* of a command rather than an invocation, so writing
documentation that merely named a push was refused -- including,
circularly, the heredoc that would have replaced this file -- and it
refused the push-with-upstream form the repository's own agent
instructions require. Under: two spaces between git and the subcommand,
an `env X=1` prefix, or `git -C .` all slipped through, so it never was
a control. It now resolves the command field (with a jq-free fallback),
discards heredoc bodies, returns early unless git is invoked, and refuses
only what destroys history or uncommitted work: unguarded force-push
(--force-with-lease allowed), hard reset, forced clean, forced branch
delete, and bulk working-tree discards. Plain push is left to the
pre-push hook, which is policy-engine backed.

PARTIALLY VALIDATED: the full behaviour matrix could not be run from the
session that wrote the hook, since executing one's own permission hook is
refused as self-modification. Observed in normal use: ordinary commands
and a plain push pass, and `git branch -D` was correctly refused. The
remaining deny cases are unverified and a reviewer should confirm them.

Also removes the roadmap page, which carried frontmatter and no body and
published as an empty page.

* test: stop the tag signing test depending on ambient git config

test_tag_respects_repo_git_overrides_and_cli_can_override_them asserts
that a repo-configured gpg.program is consulted, by pointing it at a
script that always fails and expecting the tag creation to fail with it.

That only holds when gpg.format is openpgp. On a machine whose global
config sets gpg.format=ssh, git routes signing to gpg.ssh.program, the
stub never runs, the tag is signed successfully by the ssh signer, and
the assertion evaporates -- the test fails claiming Committy ignored the
override when Committy had passed it through correctly. Verified: the tag
git produced in that environment carried a valid SSH signature.

The test now pins gpg.format=openpgp alongside the other overrides, so it
exercises the path it means to exercise regardless of the host.

Separately, the three config::hierarchy tests call MergedConfig::load,
which reads the user config from $HOME. The tests in config.rs mutate
HOME and are marked #[serial]; these were not, so they could read a
half-written or relocated user config while one of those held it, and
MergedConfig::load(...).unwrap() would panic. They are now serial too.

Caveat on that second change: it closes a real race but is unproven. The
failure was observed once, and did not recur in thirteen subsequent runs
(eight lib-only, five full) even with the fix reverted, so causality is
reasoned rather than demonstrated. It is cheap and correct regardless --
a test reading global state that other tests mutate belongs in the same
serial group.

* test(hooks): add a behaviour matrix for the guardrail, and fix what it caught

The rewritten guardrail shipped unvalidated: nothing executed it, so its
deny rules were assumed rather than checked. That matters more than for
ordinary code, because a guardrail that silently stops refusing looks
exactly like one that works.

Twelve tests feed the hook the same JSON the harness does and assert the
exit code. Writing them found three defects in the hook, all now fixed:

- `git push -f origin main` was not refused. The force check was one
  ordered regex whose `-f` alternative required a preceding space, which
  the `push ` match had already consumed. Force detection is now two
  independent conditions -- a push, and a force flag anywhere -- so flag
  order and adjacency stop mattering.
- A heredoc feeding an interpreter was dropped along with every other
  heredoc body, so `bash <<EOF` / `git reset --hard` / `EOF` sailed
  through. Heredoc bodies are still discarded when the opener writes to a
  file, but scanned when it names a shell or interpreter.
- Quoted prose naming a dangerous command is still refused. This one is
  deliberate and now recorded as a decision: stripping quoted segments
  would fix it and would also let `bash -c "git reset --hard"` through,
  and for a guardrail a false refusal costs far less than a false pass.

Coverage includes the variants the previous substring hook missed
entirely -- doubled spaces, a leading env assignment, `git -C .`, and a
git call after `&&`.

* fix(release): stop shipping binaries that report the previous version (#50)

* ci(release): fail the release when Cargo.toml disagrees with the tag

v1.10.0 was tagged on a tree whose Cargo.toml still said 1.9.1, so every
published binary reported the old version to --version and self-update kept
offering the same release forever.

A verify-version job now compares the Cargo.toml version with the tag name and
both the release creation and the build matrix wait on it, so a mismatched tag
fails before any release or asset is published.

* feat(tag)!: bump version files by default when tagging

Tagging without --bump-files left the version files stale, which is how a
release can be cut whose binaries report the previous version. Version files
are now bumped and committed unless --no-bump-files is passed; --bump-files is
still accepted so existing scripts keep working, and passing both is an error.

Two bugs surfaced by making this the default are fixed alongside it:

- version files that already carried the new version were reported as updated,
  and the empty commit that followed aborted the whole tag operation
- multi-package repositories staged the bumped files but never committed them,
  so their tags pointed at a commit with the old versions

* chore(release): align version files with published v1.10.0

* fix(docs): update vulnerable astro, starlight, and sharp dependencies (#51)

Resolves 89 Dependabot alerts, all transitive deps of the pinned
Astro 5 / Starlight 0.35 / sharp 0.34 versions in the docs site
(vite, js-yaml, postcss, nanoid, h3, picomatch, devalue, and others).
Starlight 0.39 dropped the label+autogenerate sidebar shorthand, so
astro.config.mjs now wraps autogenerate entries in items arrays.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant