Skip to content

Commit 83c7a76

Browse files
authored
fix(release): embed version in binary asset names (#1603)
## Summary Homebrew's URL→version detection misparses the **x86_64** release assets: `mergify-x86_64-unknown-linux-gnu.tar.gz` is read as version `64-unknown-linux-gnu` (a fallback stem regex grabs the `_64` before the GitHub release-path parser runs). `aarch64` parses fine, so only amd64 breaks. This blocks the Homebrew tap's `brew audit --strict`. **Fix:** embed the release version *before* the Rust target triple — `mergify-<version>-<triple>.{tar.gz,zip}` — so Homebrew's dotted-version parser wins on every arch. The tap then auto-detects the version from the URL (no explicit `version` line, no misparse, no "redundant version" complaint). ## Changes - **`release.yml`** — repack emits `mergify-${tag}-${target}.{tar.gz,zip}`; `SHA256SUMS` + asset-presence check follow. - **`self_update.rs`** — builds `mergify-{latest}-{target}.{ext}` (already resolves the latest tag); tests updated. - **`install.sh`** — resolves the latest tag (GitHub API, or a `latest-release.json` stub in fixture mode) before constructing the per-version asset name. (The `releases/latest/download/<asset>` redirect can't be used once the version is in the filename.) - **`ci.yaml`** — install/self-update fixtures use the new names + serve `latest-release.json`. - **`README.md` / `RELEASING.md`** — documented asset names updated. Complementary to #1602 (which fixes the Linux binaries self-reporting `0.0.0`): #1602 fixes `mergify --version`; this fixes Homebrew's URL parsing.
1 parent c759e28 commit 83c7a76

6 files changed

Lines changed: 104 additions & 60 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,11 @@ jobs:
151151
target=$(rustc -vV | awk '/^host:/ {print $2}')
152152
echo "Building fixture for target: ${target}"
153153
mkdir -p fixture/release
154-
tar -C target/release -czf "fixture/release/mergify-${target}.tar.gz" mergify
155-
(cd fixture/release && shasum -a 256 "mergify-${target}.tar.gz" > SHA256SUMS)
154+
# install.sh resolves the version from latest-release.json (fixture
155+
# mode) before building the per-version asset name.
156+
echo '{"tag_name":"2099.1.1.1"}' > fixture/release/latest-release.json
157+
tar -C target/release -czf "fixture/release/mergify-2099.1.1.1-${target}.tar.gz" mergify
158+
(cd fixture/release && shasum -a 256 "mergify-2099.1.1.1-${target}.tar.gz" > SHA256SUMS)
156159
ls -la fixture/release
157160
158161
- name: Serve fixture
@@ -193,7 +196,7 @@ jobs:
193196
run: |
194197
set -euo pipefail
195198
target=$(rustc -vV | awk '/^host:/ {print $2}')
196-
asset="mergify-${target}.tar.gz"
199+
asset="mergify-2099.1.1.1-${target}.tar.gz"
197200
# Keep a clean copy so the malformed test below can write
198201
# its own bad version without losing the original.
199202
cp fixture/release/SHA256SUMS fixture/release/SHA256SUMS.orig
@@ -263,8 +266,8 @@ jobs:
263266
# Pretend a far-future release is available so the
264267
# `current == latest` short-circuit doesn't fire.
265268
echo '{"tag_name":"2099.1.1.1"}' > fixture/su/latest-release.json
266-
tar -C target/release -czf "fixture/su/mergify-${target}.tar.gz" mergify
267-
(cd fixture/su && shasum -a 256 "mergify-${target}.tar.gz" > SHA256SUMS)
269+
tar -C target/release -czf "fixture/su/mergify-2099.1.1.1-${target}.tar.gz" mergify
270+
(cd fixture/su && shasum -a 256 "mergify-2099.1.1.1-${target}.tar.gz" > SHA256SUMS)
268271
ls -la fixture/su
269272
270273
- name: Serve fixture
@@ -304,7 +307,7 @@ jobs:
304307
cp target/release/mergify /tmp/mergify-su-tamper
305308
before=$(shasum -a 256 /tmp/mergify-su-tamper | awk '{print $1}')
306309
# Replace SHA256SUMS with a wrong-but-well-formed hash.
307-
printf '%064d mergify-%s.tar.gz\n' 0 "${target}" > fixture/su/SHA256SUMS
310+
printf '%064d mergify-2099.1.1.1-%s.tar.gz\n' 0 "${target}" > fixture/su/SHA256SUMS
308311
! MERGIFY_BASE_URL=http://127.0.0.1:8766 /tmp/mergify-su-tamper self-update \
309312
> /tmp/su-tamper.log 2>&1
310313
grep -q 'checksum mismatch' /tmp/su-tamper.log

.github/workflows/release.yml

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ name: release
1313
# Maintainer opens Actions → "release" → "Run workflow" → enters
1414
# the tag (e.g. `2026.6.12.1`). The workflow builds the wheel
1515
# matrix, extracts the binary from each wheel, packages
16-
# `mergify-<target>.{tar.gz,zip}` + `SHA256SUMS`, and creates the
16+
# `mergify-<version>-<target>.{tar.gz,zip}` + `SHA256SUMS`, and creates the
1717
# GitHub Release as a *draft* with all assets attached and notes
1818
# auto-generated from the commit log via `--generate-notes`.
1919
#
@@ -167,13 +167,15 @@ jobs:
167167

168168
# Each `wheel-<target>` artifact is a maturin-built wheel with
169169
# the `mergify` binary at `<dist>.data/scripts/mergify[.exe]`.
170-
# Extract it, normalise into `mergify-<target>.{tar.gz,zip}`,
171-
# then hash the lot with sha256 so the installer can verify
172-
# what it pulled. Stable names (no embedded version) so the
173-
# `releases/latest/download/...` redirect keeps working for
174-
# the `install.sh` consumers — the version is still observable
175-
# via `mergify --version` after install.
170+
# Extract it, normalise into
171+
# `mergify-<version>-<target>.{tar.gz,zip}`, then hash the lot
172+
# with sha256 so the installer can verify what it pulled.
173+
# Embedding the version (before the Rust triple) lets
174+
# Homebrew's dotted-version parser win over the fallback stem
175+
# regex that would otherwise mangle `x86_64` assets.
176176
- name: Extract binaries and repack
177+
env:
178+
INPUT_TAG: ${{ needs.compute-tag.outputs.tag }}
177179
shell: bash
178180
run: |
179181
set -euo pipefail
@@ -199,10 +201,10 @@ jobs:
199201
# want `<bin>` extracted at the workdir root.
200202
unzip -joq "${whl}" "*/scripts/${bin}" -d "${workdir}"
201203
if [[ "${target}" == *windows* ]]; then
202-
(cd "${workdir}" && zip -q "${OLDPWD}/dist/mergify-${target}.zip" "${bin}")
204+
(cd "${workdir}" && zip -q "${OLDPWD}/dist/mergify-${INPUT_TAG}-${target}.zip" "${bin}")
203205
else
204206
chmod +x "${workdir}/${bin}"
205-
tar -C "${workdir}" -czf "dist/mergify-${target}.tar.gz" "${bin}"
207+
tar -C "${workdir}" -czf "dist/mergify-${INPUT_TAG}-${target}.tar.gz" "${bin}"
206208
fi
207209
rm -rf "${workdir}"
208210
done
@@ -270,11 +272,11 @@ jobs:
270272
printf ' %s\n' "${attached[@]}"
271273
272274
expected=(
273-
mergify-x86_64-unknown-linux-gnu.tar.gz
274-
mergify-aarch64-unknown-linux-gnu.tar.gz
275-
mergify-x86_64-apple-darwin.tar.gz
276-
mergify-aarch64-apple-darwin.tar.gz
277-
mergify-x86_64-pc-windows-msvc.zip
275+
"mergify-${tag}-x86_64-unknown-linux-gnu.tar.gz"
276+
"mergify-${tag}-aarch64-unknown-linux-gnu.tar.gz"
277+
"mergify-${tag}-x86_64-apple-darwin.tar.gz"
278+
"mergify-${tag}-aarch64-apple-darwin.tar.gz"
279+
"mergify-${tag}-x86_64-pc-windows-msvc.zip"
278280
SHA256SUMS
279281
)
280282
missing=()

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ or pin a version with `MERGIFY_VERSION=2026.4.23.1`. Once installed, upgrade wit
1818
For Windows, or to bypass the script, grab the matching archive from the
1919
[latest release](https://github.com/Mergifyio/mergify-cli/releases/latest):
2020

21-
- **Windows** — download `mergify-x86_64-pc-windows-msvc.zip`, extract it,
22-
and put `mergify.exe` anywhere on your `PATH`.
23-
- **Linux / macOS** — download `mergify-<target>.tar.gz` (e.g.
24-
`mergify-aarch64-apple-darwin.tar.gz`), extract with `tar -xzf`, and put
25-
the resulting `mergify` binary anywhere on your `PATH`.
21+
- **Windows** — download `mergify-<version>-x86_64-pc-windows-msvc.zip`,
22+
extract it, and put `mergify.exe` anywhere on your `PATH`.
23+
- **Linux / macOS** — download `mergify-<version>-<target>.tar.gz` (e.g.
24+
`mergify-2026.4.23.1-aarch64-apple-darwin.tar.gz`), extract with `tar -xzf`,
25+
and put the resulting `mergify` binary anywhere on your `PATH`.
2626

2727
Verify against `SHA256SUMS` from the same release if you care.
2828

RELEASING.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ minutes.
3030
1. Go to **Releases** → the new draft.
3131
2. Review the auto-generated notes; edit if needed (drafts are
3232
mutable).
33-
3. Confirm all six asset names are listed:
34-
- `mergify-x86_64-unknown-linux-gnu.tar.gz`
35-
- `mergify-aarch64-unknown-linux-gnu.tar.gz`
36-
- `mergify-x86_64-apple-darwin.tar.gz`
37-
- `mergify-aarch64-apple-darwin.tar.gz`
38-
- `mergify-x86_64-pc-windows-msvc.zip`
33+
3. Confirm all six asset names are listed (each prefixed with the release version):
34+
- `mergify-<version>-x86_64-unknown-linux-gnu.tar.gz`
35+
- `mergify-<version>-aarch64-unknown-linux-gnu.tar.gz`
36+
- `mergify-<version>-x86_64-apple-darwin.tar.gz`
37+
- `mergify-<version>-aarch64-apple-darwin.tar.gz`
38+
- `mergify-<version>-x86_64-pc-windows-msvc.zip`
3939
- `SHA256SUMS`
4040
4. Click **Publish release**.
4141

crates/mergify-cli/src/self_update.rs

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
//! tag.
1414
//! 2. If `tag == crate::VERSION` and `--force` wasn't passed,
1515
//! print "already up to date" and return.
16-
//! 3. Download `mergify-<target>.tar.gz` + `SHA256SUMS` from the
17-
//! matching release.
16+
//! 3. Download `mergify-<version>-<target>.{tar.gz,zip}` + `SHA256SUMS`
17+
//! from the matching release (`.zip` on Windows, `.tar.gz` elsewhere).
1818
//! 4. Verify the asset SHA256 against `SHA256SUMS`. Mirrors
1919
//! `install.sh`'s line-shape validation so a malformed
2020
//! `SHA256SUMS` can't slip past.
@@ -110,7 +110,7 @@ pub async fn run(opts: &Options) -> Result<(), CliError> {
110110
// extension is determined by `cfg!(windows)` here, not by
111111
// target string-sniffing.
112112
let ext = if cfg!(windows) { "zip" } else { "tar.gz" };
113-
let asset = format!("mergify-{target}.{ext}");
113+
let asset = format!("mergify-{latest}-{target}.{ext}");
114114
let ep = endpoints(&latest);
115115
let archive = download(&client, &format!("{}/{asset}", ep.asset_base)).await?;
116116
let sums = download_text(&client, &format!("{}/SHA256SUMS", ep.asset_base)).await?;
@@ -219,7 +219,7 @@ fn hex_encode(bytes: &[u8]) -> String {
219219
/// `asset_name` in `SHA256SUMS`. Mirrors `install.sh` exactly: the
220220
/// line must split as `<hash> <name>` on whitespace where the
221221
/// second field equals `asset_name` *literally* (not `ends_with`,
222-
/// so `mergify-fooX-target.tar.gz` can't accidentally pass the
222+
/// so `mergify-2099.1.1.1-fooX-target.tar.gz` can't accidentally pass the
223223
/// check for `target.tar.gz`), and the hash field must be 64 hex
224224
/// chars. Both layers fail closed.
225225
fn verify_checksum(archive: &[u8], asset_name: &str, sums: &str) -> Result<(), CliError> {
@@ -364,41 +364,54 @@ mod tests {
364364
let mut h = Sha256::new();
365365
h.update(&archive);
366366
let hash = hex_encode(&h.finalize());
367-
let sums = format!("{hash} mergify-x86_64-unknown-linux-gnu.tar.gz\n");
368-
verify_checksum(&archive, "mergify-x86_64-unknown-linux-gnu.tar.gz", &sums).unwrap();
367+
let sums = format!("{hash} mergify-2099.1.1.1-x86_64-unknown-linux-gnu.tar.gz\n");
368+
verify_checksum(
369+
&archive,
370+
"mergify-2099.1.1.1-x86_64-unknown-linux-gnu.tar.gz",
371+
&sums,
372+
)
373+
.unwrap();
369374
}
370375

371376
#[test]
372377
fn verify_checksum_rejects_mismatch() {
373378
let archive = fixture_archive();
374379
let wrong = "0".repeat(64);
375-
let sums = format!("{wrong} mergify-x86_64-unknown-linux-gnu.tar.gz\n");
376-
let err = verify_checksum(&archive, "mergify-x86_64-unknown-linux-gnu.tar.gz", &sums)
377-
.unwrap_err();
380+
let sums = format!("{wrong} mergify-2099.1.1.1-x86_64-unknown-linux-gnu.tar.gz\n");
381+
let err = verify_checksum(
382+
&archive,
383+
"mergify-2099.1.1.1-x86_64-unknown-linux-gnu.tar.gz",
384+
&sums,
385+
)
386+
.unwrap_err();
378387
assert!(err.to_string().contains("checksum mismatch"));
379388
}
380389

381390
#[test]
382391
fn verify_checksum_rejects_missing_entry() {
383-
let sums = "deadbeef mergify-aarch64-apple-darwin.tar.gz\n";
384-
let err =
385-
verify_checksum(&[], "mergify-x86_64-unknown-linux-gnu.tar.gz", sums).unwrap_err();
392+
let sums = "deadbeef mergify-2099.1.1.1-aarch64-apple-darwin.tar.gz\n";
393+
let err = verify_checksum(
394+
&[],
395+
"mergify-2099.1.1.1-x86_64-unknown-linux-gnu.tar.gz",
396+
sums,
397+
)
398+
.unwrap_err();
386399
assert!(err.to_string().contains("no checksum entry"));
387400
}
388401

389402
#[test]
390403
fn verify_checksum_does_not_accept_suffix_match() {
391404
// Regression for the pre-fix `ends_with` lookup: a sibling
392405
// asset whose name *ends in* `asset_name` (here
393-
// `mergify-x86_64-pc-windows-msvc.zip` ending in `.zip`)
394-
// could be matched and pass even when the requested asset
395-
// wasn't in the file. The literal second-field match must
396-
// reject this.
406+
// `mergify-2099.1.1.1-x86_64-pc-windows-msvc.zip` ending in
407+
// `.zip`) could be matched and pass even when the requested
408+
// asset wasn't in the file. The literal second-field match
409+
// must reject this.
397410
let archive = fixture_archive();
398411
let mut h = Sha256::new();
399412
h.update(&archive);
400413
let hash = hex_encode(&h.finalize());
401-
let sums = format!("{hash} mergify-x86_64-pc-windows-msvc.zip\n");
414+
let sums = format!("{hash} mergify-2099.1.1.1-x86_64-pc-windows-msvc.zip\n");
402415
let err = verify_checksum(&archive, "msvc.zip", &sums).unwrap_err();
403416
assert!(
404417
err.to_string().contains("no checksum entry"),
@@ -417,9 +430,13 @@ mod tests {
417430
let mut h = Sha256::new();
418431
h.update(&archive);
419432
let hash = hex_encode(&h.finalize());
420-
let sums = format!("{hash} mergify-x86_64-unknown-linux-gnu.tar.gz extra\n");
421-
let err = verify_checksum(&archive, "mergify-x86_64-unknown-linux-gnu.tar.gz", &sums)
422-
.unwrap_err();
433+
let sums = format!("{hash} mergify-2099.1.1.1-x86_64-unknown-linux-gnu.tar.gz extra\n");
434+
let err = verify_checksum(
435+
&archive,
436+
"mergify-2099.1.1.1-x86_64-unknown-linux-gnu.tar.gz",
437+
&sums,
438+
)
439+
.unwrap_err();
423440
assert!(
424441
err.to_string().contains("no checksum entry"),
425442
"expected 'no checksum entry', got: {err}",
@@ -431,9 +448,13 @@ mod tests {
431448
// Right asset name, wrong-shape hash. install.sh validates
432449
// the same way so a corrupted SHA256SUMS can't slip past
433450
// sha256sum's warn-but-pass behaviour; mirror it here.
434-
let sums = "bogus mergify-x86_64-unknown-linux-gnu.tar.gz\n";
435-
let err =
436-
verify_checksum(&[], "mergify-x86_64-unknown-linux-gnu.tar.gz", sums).unwrap_err();
451+
let sums = "bogus mergify-2099.1.1.1-x86_64-unknown-linux-gnu.tar.gz\n";
452+
let err = verify_checksum(
453+
&[],
454+
"mergify-2099.1.1.1-x86_64-unknown-linux-gnu.tar.gz",
455+
sums,
456+
)
457+
.unwrap_err();
437458
assert!(err.to_string().contains("malformed checksum entry"));
438459
}
439460

install.sh

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,6 @@ set -eu
2323
REPO="Mergifyio/mergify-cli"
2424
INSTALL_DIR="${MERGIFY_INSTALL_DIR:-${HOME}/.local/bin}"
2525
VERSION="${MERGIFY_VERSION:-latest}"
26-
if [ "${VERSION}" = "latest" ]; then
27-
BASE_URL="${MERGIFY_BASE_URL:-https://github.com/${REPO}/releases/latest/download}"
28-
else
29-
BASE_URL="${MERGIFY_BASE_URL:-https://github.com/${REPO}/releases/download/${VERSION}}"
30-
fi
3126

3227
die() {
3328
printf 'error: %s\n' "$1" >&2
@@ -72,8 +67,31 @@ main() {
7267
command -v curl > /dev/null 2>&1 || die "curl is required"
7368
command -v tar > /dev/null 2>&1 || die "tar is required"
7469

70+
# Resolve VERSION to the actual tag so we can embed it in the
71+
# asset filename. When MERGIFY_BASE_URL is set (fixture mode used
72+
# by the CI smoke test) the fixture already serves a
73+
# `latest-release.json` stub; otherwise call the GitHub API.
74+
if [ "${VERSION}" = "latest" ]; then
75+
if [ -n "${MERGIFY_BASE_URL:-}" ]; then
76+
# Fixture mode: the stub JSON lives next to the assets.
77+
VERSION=$(curl -fsSL "${MERGIFY_BASE_URL}/latest-release.json" \
78+
| grep -o '"tag_name":[[:space:]]*"[^"]*"' \
79+
| sed 's/.*"tag_name":[[:space:]]*"//; s/".*//')
80+
else
81+
VERSION=$(curl -fsSL \
82+
"https://api.github.com/repos/${REPO}/releases/latest" \
83+
| grep -o '"tag_name":[[:space:]]*"[^"]*"' \
84+
| sed 's/.*"tag_name":[[:space:]]*"//; s/".*//')
85+
fi
86+
[ -n "${VERSION}" ] || die "could not resolve latest release version"
87+
fi
88+
89+
# With the version known, build the per-version asset name and
90+
# the base URL for this release.
91+
BASE_URL="${MERGIFY_BASE_URL:-https://github.com/${REPO}/releases/download/${VERSION}}"
92+
7593
target=$(detect_target)
76-
asset="mergify-${target}.tar.gz"
94+
asset="mergify-${VERSION}-${target}.tar.gz"
7795
url="${BASE_URL}/${asset}"
7896
sums_url="${BASE_URL}/SHA256SUMS"
7997

0 commit comments

Comments
 (0)