Skip to content

Publish from GitHub Releases, deriving the version from the tag - #180

Merged
lloydwatkin merged 1 commit into
mainfrom
janus-release-from-github-releases
Sep 4, 2026
Merged

lloydwatkin merged 1 commit into
mainfrom
janus-release-from-github-releases

Conversation

@lloydwatkin

Copy link
Copy Markdown
Member

Follow-up to #179, which fixed the mechanics of publishing but got the ownership backwards.

The problem

#179 treated lib/janus-ar/version.rb as the source of truth and made the tag prove it agreed:

##[error]Tag v8.1.0 does not match lib/janus-ar/version.rb (8.0.1).
           Use bin/release.sh to cut releases.

That only works if you bump the file by hand before tagging. Releases here are cut from the GitHub Releases UI, so the tag comes first and nothing bumps the file — meaning every release would fail that guard, exactly as v8.1.0 did. The guard was right; the model behind it was wrong.

Worth noting the old workflow would not have saved this either: version.rb and Gemfile.lock already agreed at 8.0.1, so guard_clean would have passed and rake release would have tried to push janus-ar 8.0.1 under a v8.1.0 tag, which RubyGems would have rejected as an existing version.

The fix — invert the ownership

The tag is now the source of truth. On release: published the workflow:

  1. Reads the version from github.event.release.tag_name (v8.1.0 → 8.1.0), rejecting anything that is not a valid gem version.
  2. Checks the tag is at the head of main — the bump has to land there.
  3. Writes version.rb, runs bundle install to move Gemfile.lock (it records the gem's own version in its PATH section), and asserts the generated file actually reports the expected version.
  4. Commits Release vX.Y.Z to main and moves the tag onto that commit.
  5. Publishes via rubygems/release-gem — the tree is clean by now, and rake release skips tag creation because the tag already exists.

Nothing is bumped by hand, ever. bin/release.sh is removed, since it cut releases the old way and would now fight the workflow.

Two calls worth reviewing

Moving the tag keeps it honest — checking out v8.1.0 gives you the code that was actually published, not the commit before the bump. It uses --force-with-lease pinned to the SHA validated in step 2, so it cannot clobber a tag that moved underneath the run. If you would rather the tag stayed put, deleting those three lines is the whole change; the cost is that tags point one commit behind the version they name, which is the drift that caused this mess in the first place.

Requiring the tag at main's head is what makes the commit-back safe. It rules out releasing from an older commit or a maintenance branch. Nothing in this repo does that today, and the error message says exactly what to do if you hit it.

Verification

I extracted each run: block straight from the YAML (so it is dedented exactly as bash will receive it) and executed them:

  • Version parsing — accepts v8.1.0, v8.1.0.rc1, v10.0.2, bare 8.1.0; rejects v8.1, v8.1.0-beta1 (RubyGems needs a dot, not a dash), vfoo.
  • Tag-at-head guard — run against this repo's real tags: passes for v8.1.0 (at main head) and captures TAG_OBJECT=84d08cc; correctly rejects v8.0.1 with the diagnostic naming both SHAs.
  • Version write — produces a version.rb byte-identical in shape to the current one, moves Gemfile.lock to janus-ar (8.1.0), and passes RuboCop. Pre-release path verified too: v8.2.0.rc1 → PRE = 'rc1', Janus::VERSION.to_s == "8.2.0.rc1".
  • rubocop --parallel clean (28 files); all three workflow/config YAMLs parse; gemspec still ships 19 files and no executables.

What I could not test locally is the end-to-end run — the commit-back, the tag move, and the RubyGems push only happen on a real release: published event. The first actual release is the proof.

Before the next release

The orphan v8.1.0 tag from the failed attempt is still on the remote, pointing at the #179 merge commit. It needs deleting before you re-release, otherwise the Releases UI will reuse the existing tag, which will no longer be at main's head once this merges, and the workflow will correctly refuse:

git push origin :refs/tags/v8.1.0

Then create the v8.1.0 release from the UI as normal.

🤖 Generated with Claude Code

The previous workflow had the ownership backwards. It treated
lib/janus-ar/version.rb as the source of truth and required the tag to
agree with it, which only works if the version is bumped by hand before
tagging. Releases here are cut from the GitHub Releases UI, so the tag
is created first and nothing bumps the file -- every release would fail
the version guard, as v8.1.0 did.

Invert it: the tag is now the source of truth.

On `release: published` the workflow reads the version off the tag,
writes it into version.rb, refreshes Gemfile.lock (which records the
gem's own version), commits that bump to main as "Release vX.Y.Z", moves
the tag onto that commit, and then publishes. Nothing needs bumping by
hand, and `rake release` gets the clean tree it insists on.

Moving the tag keeps it honest: a checkout of vX.Y.Z is the code that
was actually published, rather than the commit before the bump. It uses
--force-with-lease against the SHA validated earlier in the run, so it
cannot clobber a tag that moved underneath us.

Because the bump lands on main, the release has to be cut from its head;
the workflow checks this and says so plainly if not.

bin/release.sh is removed -- it cut releases the old way and would now
conflict with the workflow doing its own bumping.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lloydwatkin
lloydwatkin merged commit 06e4298 into main Sep 4, 2026
12 checks passed
@lloydwatkin
lloydwatkin deleted the janus-release-from-github-releases branch September 4, 2026 08:44
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