Skip to content

Refuse to bump the tap when the version string disagrees with the tag - #7

Closed
k3KAW8Pnf7mkmdSMPHz27 wants to merge 1 commit into
mainfrom
bump-tap-version-guard
Closed

k3KAW8Pnf7mkmdSMPHz27 wants to merge 1 commit into
mainfrom
bump-tap-version-guard

Conversation

@k3KAW8Pnf7mkmdSMPHz27

Copy link
Copy Markdown
Owner

Why

Sources/EventKitService/EventKitService.swift hardcodes public let eventKitServiceVersion = "1.0.0". The binary reports that string from --version (the ArgumentParser CommandConfiguration) and in MCP serverInfo (EventKitMCPServer.swift). Homebrew takes its version from the git tag. So if v1.1.0 is tagged without bumping the string, brew list says 1.1.0 while the server tells Claude Desktop it is 1.0.0. No test pins the string.

Every release passes through bump-tap.yml, so the check goes there.

What

A new step, Check the version string matches the tag, runs right after "Resolve tag and commit" and before "Mint a scoped token for the tap". It:

  1. Reads EventKitService.swift at the resolved commit SHA (steps.release.outputs.sha), not at the run's own ref. Under workflow_dispatch the run's ref is a branch, not the tag.

  2. Fails closed. The step errors, naming the file, when:

    • the fetch fails (missing file, bad SHA, API error), or
    • the public let eventKitServiceVersion = "..." declaration matches zero times or more than once.
  3. Compares the string to ${TAG#v}. On a mismatch it prints both values and what to do:

    ::error::Sources/EventKitService/EventKitService.swift at v9.9.9 declares eventKitServiceVersion = "1.0.0", but the tag says 9.9.9. Bump the string to "9.9.9" on main, then re-tag v9.9.9 on that commit.
    

Because it runs before the token step, a bad tag fails before any credential is minted. The tag, SHA and repo reach the script through env:, the same pattern as the existing steps. The file's contents never touch a ${{ }} expression.

How the file is fetched: gh api, not actions/checkout

gh api -H 'Accept: application/vnd.github.raw+json' \
  "repos/${SOURCE_REPO}/contents/${VERSION_FILE}?ref=${SHA}"

This uses the default GITHUB_TOKEN under the existing contents: read, so permissions are unchanged. I chose it over a sparse checkout because:

  • It is one request for one file. A checkout at a SHA still fetches the commit's tree and sets up a git repo on the runner.
  • It adds no new action use and no extra with: settings (ref, sparse-checkout, sparse-checkout-cone-mode, persist-credentials, path).
  • It leaves no token behind. actions/checkout writes the token into .git/config unless persist-credentials: false is set, and this job later checks out the tap with a different token.
  • A missing file comes back as an HTTP 404, and the step turns that into an ::error::.

The response goes to $RUNNER_TEMP, and grep/cut read it from there. cut -d '"' -f 2 takes the value between the first pair of quotes on the one matching line.

Testing

Real run. gh workflow run bump-tap.yml --ref bump-tap-version-guard -f tag=v1.0.0 ran run 35894192997, which succeeded:

Step Result
Resolve tag and commit success (Pinning v1.0.0 -> dc1316c…)
Check the version string matches the tag success (eventKitServiceVersion at v1.0.0 is "1.0.0"; matches the tag.)
Mint a scoped token for the tap success
Check out the tap success
Rewrite the formula pin success (Formula already pinned to v1.0.0; nothing to do.)
Write the PR body / Open or update the bump PR skipped (no change)
Revoke the token success

No PR was opened on the tap.

Local runs. I ran the step's parse and compare lines verbatim against the real file and against edited copies in /tmp. The fetch line ran separately with its values filled in.

Case Result
v1.0.0 vs the real file at dc1316c passes: eventKitServiceVersion at v1.0.0 is "1.0.0"; matches the tag.
v9.9.9 vs the real file exit 1, mismatch error as shown above
declaration changed to public var exit 1: Expected exactly one 'public let eventKitServiceVersion = "..."' in Sources/EventKitService/EventKitService.swift at v1.0.0, found 0
declaration duplicated exit 1: same error, found 2
empty file exit 1: same error, found 0
stale commented-out declaration above the real one passes (the pattern is anchored, so comments don't match)
missing file (Sources/Nope.swift) gh: Not Found (HTTP 404), then exit 1: Could not read Sources/Nope.swift at v1.0.0 (dc1316c…)

ruby -ryaml parses the workflow, and the new step is at index 1, ahead of the token step. actionlint and shellcheck were not run because they are not installed.

Sources/EventKitService/EventKitService.swift hardcodes
eventKitServiceVersion. The binary reports that string from --version
and in its MCP serverInfo, while Homebrew takes the version from the git
tag. If a release is tagged without bumping the string, `brew list` and
the server disagree about which version is installed, and nothing else
catches it: no test pins the string.

Every release passes through bump-tap.yml, so check there. The new step
reads the file at the commit the tag resolves to, not at the run's own
ref, which under workflow_dispatch is a branch. It fails unless the file
declares the string exactly once and that string equals the tag without
its leading "v". It runs before the tap token is minted, so a bad tag
stops before any credential exists.

The file is fetched with `gh api .../contents/<path>?ref=<sha>` using
the default GITHUB_TOKEN and the existing contents: read permission.
That is one request for one file. It adds no new action, fetches no
tree, and leaves no token behind in a .git/config, which a checkout
would do unless told otherwise.
@k3KAW8Pnf7mkmdSMPHz27

Copy link
Copy Markdown
Owner Author

Closing without merging. Superseded by #8.

This guard checks that the tagged commit's eventKitServiceVersion equals the tag, and it works as written. The dispatch run against v1.0.0 passed, and the negative cases failed closed. #8 changes where the version lives, though. The source now keeps a 0.0.0-dev placeholder, and the Homebrew formula writes the tag's version in at build time (k3KAW8Pnf7mkmdSMPHz27/homebrew-mcp#1). Under that design, every tagged commit holds the placeholder, so this guard would block every release.

The mismatch it was meant to catch is now caught in two other places:

  • The formula's inreplace fails the build if the declaration can't be found.
  • The formula's test do asserts that --version equals the formula's version.

@k3KAW8Pnf7mkmdSMPHz27
k3KAW8Pnf7mkmdSMPHz27 deleted the bump-tap-version-guard branch September 23, 2026 18:59
k3KAW8Pnf7mkmdSMPHz27 added a commit that referenced this pull request Sep 23, 2026
Every releasable merge to `main` is released automatically by
[semantic-release](https://semantic-release.org). It runs as a `release`
job in `ci.yml` after `build-and-test` passes on the pushed commit. It
tags the version, publishes a GitHub Release with generated notes, and
the tag triggers `bump-tap.yml`.

## Pieces

- **Titles.** `pr-title.yml` (check `conventional-title`) requires the
PR title to be a conventional commit. Squash-only merging makes the PR
title the commit subject on `main`.
- **Versioning.** semantic-release reads the commit subjects since the
last tag:
  - `feat` releases a minor version.
  - `fix`, `perf` and `revert` release a patch.
  - A `!` after the type, or a breaking-change footer, releases a major.
  - Any other type releases nothing.
- **`.releaserc.yml`**
- Uses the `conventionalcommits` preset. With the default `angular`
preset, `feat!:` releases nothing: I dry-ran it and got "no new version"
instead of 2.0.0.
  - Lists the plugins explicitly and leaves out `@semantic-release/npm`.
- Turns off the failure issue and the `released` label, because the
token has no issues permission.
- **Supply chain.**
- `package.json` and `package-lock.json` pin the release tooling, 445
packages. CI installs them with `--ignore-scripts`.
- `npm audit signatures` must pass before anything runs with write
access.
  - `node_modules/` is ignored.
- **Auth.** The job mints a token from the existing GitHub App, narrowed
to this repo with `contents` and `pull-requests` write. A tag pushed
with `GITHUB_TOKEN` can't trigger `bump-tap.yml`; the App's tag can.
- **Dependabot.** Swift package bumps are titled `fix(deps)` and release
a patch. Actions bumps are `ci(deps)`, and so are npm bumps (weekly,
grouped); neither releases anything. Major updates of
`conventional-changelog-conventionalcommits` are ignored. Preset 10
needs `conventional-changelog-writer` 9, but semantic-release's notes
generator still ships writer 8, so release notes crash and nothing gets
tagged.
- **Version string.** `eventKitServiceVersion` becomes the placeholder
`0.0.0-dev`. The formula writes the tag's version in before building
(k3KAW8Pnf7mkmdSMPHz27/homebrew-mcp#1), so a release never commits to
`main`.

## Verification

- I ran `semantic-release --dry-run` in a throwaway clone against the
real remote. All 10 cases completed with exit 0 and no errors, and every
releasable case rendered release notes:
  - This branch as it stands releases nothing.
  - `fix(deps)`, `revert` and `perf` each give 1.0.1.
- `feat` plus `fix` gives 1.1.0. The notes have Features and Bug Fixes
sections, and the `docs:` commit is hidden.
  - `feat!`, `feat(api)!` and a breaking-change footer each give 2.0.0.
  - `ci(deps)`, and `docs` plus `refactor`, release nothing.
- The first attempt pinned preset 10.4.0. The version calculation was
correct, but notes generation crashed. That's why the preset is pinned
to 9.3.1 and Dependabot ignores its majors.
- `npm audit signatures` shows 445 registry signatures verified.

## After merging

This merge releases nothing, because the commits since `v1.0.0` predate
the convention. `v1.1.0`, the modernization, needs one tag and GitHub
Release made by hand. semantic-release counts from that tag onward.
Merge homebrew-mcp#1 first, so that `v1.1.0` reports its real version.

Supersedes #7.
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