From f13baefcdec458be3eb0fbc2353711493d506a25 Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Wed, 12 Aug 2026 17:50:55 -0400 Subject: [PATCH 1/2] fix: write .version directly instead of hardcoding the version in urls ci/bump_homebrew_formula.rb now writes Formula/linear-cli/.version to the release tag directly (the formula's own single source of truth, per rubyists/homebrew-tap's matching change) instead of rewriting a literal version number inside each url - the url lines are now a constant `v#{version}/` in the formula's own source, so only the sha256 that follows each one actually needs updating per release. Verified against a real copy of the formula: correct .version/sha256 output, url lines left untouched, idempotent on a repeated run with the same inputs, and the resulting formula evaluates to the correct version/url/sha256 via a small Homebrew-Formula-DSL stub. Also bumps the vendor/rubyists-homebrew-tap submodule pointer to the merged fix (rubyists/homebrew-tap#6). --- ci/bump_homebrew_formula.rb | 32 +++++++++++++++++++------------- vendor/rubyists-homebrew-tap | 2 +- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/ci/bump_homebrew_formula.rb b/ci/bump_homebrew_formula.rb index 0113f94..fc201a5 100755 --- a/ci/bump_homebrew_formula.rb +++ b/ci/bump_homebrew_formula.rb @@ -1,13 +1,16 @@ #!/usr/bin/env ruby -# Bumps a Homebrew formula's per-platform `url`/`sha256` pairs to a new -# rubyists/linear-cli release - the `.github/workflows/main.yaml` -# `homebrew-tap-bump` job's only real logic, kept out of the workflow YAML -# so it's runnable/testable locally without a real CI run. +# Bumps a Homebrew formula to a new rubyists/linear-cli release: writes +# `.version` (the formula's own single source of truth - it interpolates +# `#{version}` into each `url` itself, see the formula file, so the url +# lines never need touching here) and updates each platform's `sha256`. +# The `.github/workflows/main.yaml` `homebrew-tap-bump` job's only real +# logic, kept out of the workflow YAML so it's runnable/testable locally +# without a real CI run. # # Usage: bump_homebrew_formula.rb FORMULA_PATH TAG SHA256SUMS_PATH # # FORMULA_PATH path to the formula file (e.g. Formula/linear-cli/linear-cli.rb) -# TAG the release tag, e.g. "v1.3.0" +# TAG the release tag, e.g. "v1.4.0" # SHA256SUMS_PATH that release's own SHA256SUMS asset, as downloaded formula_path, tag, sha256sums_path = ARGV @@ -22,27 +25,30 @@ acc[name.strip] = sha if name end +version = tag.delete_prefix("v") +version_file = File.join(File.dirname(formula_path), ".version") +File.write(version_file, "#{version}\n") + content = File.read(formula_path) %w[macos_aarch64 linux_x86_64].each do |target| asset = "lc_#{target}.tar.gz" sha = sha256sums.fetch(asset) { abort "No checksum found for #{asset} in #{sha256sums_path}" } + # The url line is a constant - it always reads v#{version}/ + # verbatim in the formula's own source, never a literal version - only + # the sha256 that follows it actually changes per release. pattern = / - url\ "https:\/\/github\.com\/rubyists\/linear-cli\/releases\/download\/v[\d.]+\/#{Regexp.escape(asset)}" - \n(\s*) - sha256\ "[a-f0-9]+" + (url\ "https:\/\/github\.com\/rubyists\/linear-cli\/releases\/download\/v\#\{version\}\/#{Regexp.escape(asset)}" + \n\s*sha256\ ")[a-f0-9]+(") /x unless content.match?(pattern) abort "Could not find a url/sha256 pair for #{asset} in #{formula_path}" end - content = content.sub(pattern) do - indent = ::Regexp.last_match(1) - %(url "https://github.com/rubyists/linear-cli/releases/download/#{tag}/#{asset}"\n#{indent}sha256 "#{sha}") - end + content = content.sub(pattern, "\\1#{sha}\\2") end File.write(formula_path, content) -puts "Bumped #{formula_path} to #{tag}" +puts "Wrote #{version_file} (#{version}) and updated #{formula_path}'s sha256 pairs to match" diff --git a/vendor/rubyists-homebrew-tap b/vendor/rubyists-homebrew-tap index 23d1237..ab0aae0 160000 --- a/vendor/rubyists-homebrew-tap +++ b/vendor/rubyists-homebrew-tap @@ -1 +1 @@ -Subproject commit 23d1237a1489a93c2671bd68ca7f6b9ec085ff32 +Subproject commit ab0aae00839b2abe2156a8e6ab5852eaed749bf5 From beed61a6e268b90cd0f02911f40285c0093ed3ec Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Wed, 12 Aug 2026 18:10:25 -0400 Subject: [PATCH 2/2] fix: address PR review comments on the homebrew bump script Copilot's review on #98 flagged three real issues in ci/bump_homebrew_formula.rb, all verified against the actual behavior before fixing: - TAG's "v" prefix was assumed, never validated - a bare "1.5.0" would silently write a .version that doesn't match what the formula's hardcoded v#{version} url actually requests. Now aborts loudly if TAG doesn't start with "v". - .version was written before the validation loop that can abort on a missing checksum or pattern mismatch, so a failure mid-run could leave .version bumped while the formula's actual sha256/url stayed on the old release. Both writes now happen only after every asset's checked and every pattern's matched. - The sha256 match pattern accepted any-length hex ([a-f0-9]+) instead of exactly 64 chars, risking a match against malformed content. Tightened to [a-fA-F0-9]{64}. --- ci/bump_homebrew_formula.rb | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/ci/bump_homebrew_formula.rb b/ci/bump_homebrew_formula.rb index fc201a5..a6d9e8d 100755 --- a/ci/bump_homebrew_formula.rb +++ b/ci/bump_homebrew_formula.rb @@ -19,6 +19,14 @@ abort "Usage: #{$PROGRAM_NAME} FORMULA_PATH TAG SHA256SUMS_PATH" end +# The formula's url lines hardcode a literal "v" before the interpolated +# version (v#{version}/) - a TAG without that same prefix would +# silently write a .version that doesn't match what the url actually +# requests. Fail loudly instead. +unless tag.start_with?("v") + abort "TAG must start with 'v' (e.g. v1.4.0), got: #{tag}" +end + sha256sums = File.readlines(sha256sums_path).each_with_object({}) do |line, acc| sha, name = line.split(/\s+/, 2) @@ -27,8 +35,6 @@ version = tag.delete_prefix("v") version_file = File.join(File.dirname(formula_path), ".version") -File.write(version_file, "#{version}\n") - content = File.read(formula_path) %w[macos_aarch64 linux_x86_64].each do |target| @@ -37,10 +43,13 @@ # The url line is a constant - it always reads v#{version}/ # verbatim in the formula's own source, never a literal version - only - # the sha256 that follows it actually changes per release. + # the sha256 that follows it actually changes per release. sha256 is + # always exactly 64 hex chars - matching that exactly (rather than + # [a-f0-9]+, any length) rejects a malformed/truncated checksum instead + # of silently writing one. pattern = / (url\ "https:\/\/github\.com\/rubyists\/linear-cli\/releases\/download\/v\#\{version\}\/#{Regexp.escape(asset)}" - \n\s*sha256\ ")[a-f0-9]+(") + \n\s*sha256\ ")[a-fA-F0-9]{64}(") /x unless content.match?(pattern) @@ -50,5 +59,10 @@ content = content.sub(pattern, "\\1#{sha}\\2") end +# Both writes deferred until every asset's checked and every pattern +# matched - an abort above now never leaves .version bumped while the +# formula's actual sha256/url pairs are still on the old release (or +# vice versa). +File.write(version_file, "#{version}\n") File.write(formula_path, content) puts "Wrote #{version_file} (#{version}) and updated #{formula_path}'s sha256 pairs to match"