From 19badbffc434572c78fdeec0045454b33e7a89df Mon Sep 17 00:00:00 2001 From: Ian Duffy Date: Fri, 31 Jul 2026 14:51:47 +0100 Subject: [PATCH 1/6] fix(ENG-13692): stop Homebrew rewriting bundled dylib IDs Since 1.20.1 the formula ships a PyInstaller onedir bundle instead of a pure Python zipapp, so the keg now contains prebuilt Mach-O files. Homebrew rewrites the dylib ID of every one of them to an absolute Cellar path, and that name does not fit in the header padding of pydantic_core's prebuilt wheel: Failed changing dylib ID of .../_pydantic_core.cpython-312-darwin.so from @rpath/pydantic_core._pydantic_core.cpython-312-darwin.so to /opt/homebrew/opt/cloudsmith-cli/libexec/_internal/... Error: Failed to fix install linkage The install still links and the CLI runs, but brew exits non-zero, which breaks `brew upgrade` and any automation around it. Both macOS architectures are affected; the Intel bundle has even less padding to spare. Rewriting these IDs has no value: every non-system linkage in the bundle is @rpath-relative and private to it, and nothing outside links against them. preserve_rpath tells Homebrew to leave @rpath dylib IDs alone, which covers all six dylibs in the bundle. Co-Authored-By: Claude Opus 5 (1M context) --- Formula/cloudsmith-cli.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Formula/cloudsmith-cli.rb b/Formula/cloudsmith-cli.rb index 5fe0b73..d2ded63 100644 --- a/Formula/cloudsmith-cli.rb +++ b/Formula/cloudsmith-cli.rb @@ -30,6 +30,12 @@ class CloudsmithCli < Formula regex(/^version=(\d+(?:\.\d+)+)$/i) end + # The bundled libraries are private to the PyInstaller bundle and are resolved + # via @rpath, so Homebrew must not rewrite their dylib IDs: the absolute Cellar + # path does not fit in the Mach-O header padding of prebuilt wheels such as + # pydantic_core, which fails the install. + preserve_rpath + def install # PyInstaller onedir bundle: the executable must stay next to _internal/. libexec.install Dir["*"] From e5745399e9b6730e3f6c0e59cf8974d563b467da Mon Sep 17 00:00:00 2001 From: Ian Duffy Date: Fri, 31 Jul 2026 14:51:56 +0100 Subject: [PATCH 2/6] test(ENG-13692): exercise the formula lifecycle on macOS runners The previous release broke installs on macOS and nothing in CI noticed: the existing checks only parse the formula and grep for required fields, while the failure came from Homebrew's install-time relocation of a prebuilt bundle. Run the real lifecycle on both macOS architectures: install, uninstall, install a baseline version, upgrade, downgrade, uninstall. Downgrade is covered because Homebrew has no downgrade command, and a plain `brew install` of an older formula is a silent no-op that leaves the newer version linked. The tap is assembled from the checkout under its real name, because Homebrew resolves preserve_rpath through Formula[name]; under any other name the flag would not apply and the regression guard would go untested. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/formula-test.yml | 120 +++++++++++++++++++++++++++++ scripts/assert-cli-version.sh | 27 +++++++ scripts/render-formula-version.py | 62 +++++++++++++++ 3 files changed, 209 insertions(+) create mode 100644 .github/workflows/formula-test.yml create mode 100755 scripts/assert-cli-version.sh create mode 100755 scripts/render-formula-version.py diff --git a/.github/workflows/formula-test.yml b/.github/workflows/formula-test.yml new file mode 100644 index 0000000..0c0cd09 --- /dev/null +++ b/.github/workflows/formula-test.yml @@ -0,0 +1,120 @@ +# Copyright 2026 Cloudsmith Ltd +name: Test formula on macOS + +# The formula ships a prebuilt PyInstaller bundle, so the things that break it +# are Homebrew's own install-time behaviour (relocation, linking) rather than +# anything a syntax check can see. Exercise the real lifecycle on real runners. +on: + pull_request: + push: + branches: + - main + # Homebrew itself changes underneath us; a scheduled run catches a formula + # that stops installing without this repository changing at all. + schedule: + - cron: "17 6 * * 1" + workflow_dispatch: + +permissions: + contents: read + +env: + # A fixed known-good release, used purely as the "other end" of the upgrade + # and downgrade transitions. This is a deliberate baseline, not "the previous + # release": it does not need bumping when a new version ships. + BASELINE_VERSION: "1.20.1" + BASELINE_SHA256_ARM64: "2c2580eb8725467877f2a296d675fd681abe01050099a6405d5b4c37c6f3b901" + BASELINE_SHA256_X86_64: "a8e959909caab7d8d6fac390d530cd2a4bff3e70c97e12e06e2587a5b7ddfc85" + HOMEBREW_NO_ANALYTICS: 1 + HOMEBREW_NO_AUTO_UPDATE: 1 + HOMEBREW_NO_ENV_HINTS: 1 + HOMEBREW_NO_INSTALL_CLEANUP: 1 + # The tap is assembled from this checkout rather than cloned, so Homebrew has + # no upstream commit to verify it against. The formula under test is this + # repository's own code. + HOMEBREW_NO_REQUIRE_TAP_TRUST: 1 + +jobs: + lifecycle: + name: ${{ matrix.arch }} + runs-on: ${{ matrix.runner }} + strategy: + # Both architectures are affected by relocation failures, and the Intel + # bundles have less Mach-O header padding to spare, so never let one + # architecture's result stand in for the other. + fail-fast: false + matrix: + include: + - runner: macos-latest + arch: arm64 + - runner: macos-15-intel + arch: x86_64 + + steps: + - uses: actions/checkout@v4 + + - name: Install this checkout as the tap + run: | + # The tap has to carry its real name. Homebrew looks preserve_rpath up + # through Formula[name], so under any other tap name the flag silently + # would not apply and the relocation guard would go untested. + tap_dir="$(brew --repository)/Library/Taps/cloudsmith-io/homebrew-cloudsmith-cli" + mkdir -p "$tap_dir" + cp -R Formula Aliases "$tap_dir/" + echo "TAP_DIR=$tap_dir" >> "$GITHUB_ENV" + + - name: Lint formula + run: | + brew style "$TAP_DIR/Formula/cloudsmith-cli.rb" + brew audit --formula cloudsmith-io/cloudsmith-cli/cloudsmith-cli + + - name: Install + run: brew install cloudsmith-io/cloudsmith-cli/cloudsmith-cli + + - name: Verify install + run: | + # The formula's own test block covers the binary inside the keg; this + # additionally proves the version on PATH, i.e. that linking worked. + brew test cloudsmith-io/cloudsmith-cli/cloudsmith-cli + scripts/assert-cli-version.sh "$(sed -n 's/^ version "\(.*\)"$/\1/p' Formula/cloudsmith-cli.rb)" + + - name: Uninstall + run: | + brew uninstall cloudsmith-cli + if command -v cloudsmith; then + echo "cloudsmith still on PATH after uninstall" >&2 + exit 1 + fi + if brew list --versions cloudsmith-cli; then + echo "keg still present after uninstall" >&2 + exit 1 + fi + + - name: Install baseline version + run: | + python3 scripts/render-formula-version.py \ + "$TAP_DIR/Formula/cloudsmith-cli.rb" \ + "$BASELINE_VERSION" "$BASELINE_SHA256_ARM64" "$BASELINE_SHA256_X86_64" + brew install cloudsmith-io/cloudsmith-cli/cloudsmith-cli + scripts/assert-cli-version.sh "$BASELINE_VERSION" + + - name: Upgrade to this formula + run: | + cp Formula/cloudsmith-cli.rb "$TAP_DIR/Formula/cloudsmith-cli.rb" + brew upgrade cloudsmith-io/cloudsmith-cli/cloudsmith-cli + scripts/assert-cli-version.sh "$(sed -n 's/^ version "\(.*\)"$/\1/p' Formula/cloudsmith-cli.rb)" + + - name: Downgrade to baseline version + run: | + python3 scripts/render-formula-version.py \ + "$TAP_DIR/Formula/cloudsmith-cli.rb" \ + "$BASELINE_VERSION" "$BASELINE_SHA256_ARM64" "$BASELINE_SHA256_X86_64" + # Homebrew has no downgrade command, and plain `brew install` of an + # older formula is a silent no-op that leaves the newer version + # linked, so a downgrade has to uninstall first. + brew uninstall cloudsmith-cli + brew install cloudsmith-io/cloudsmith-cli/cloudsmith-cli + scripts/assert-cli-version.sh "$BASELINE_VERSION" + + - name: Uninstall baseline version + run: brew uninstall cloudsmith-cli diff --git a/scripts/assert-cli-version.sh b/scripts/assert-cli-version.sh new file mode 100755 index 0000000..ee7feca --- /dev/null +++ b/scripts/assert-cli-version.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Copyright 2026 Cloudsmith Ltd +# +# Assert that the cloudsmith CLI on PATH reports an expected version. +# +# Deliberately resolves the binary through PATH rather than through the keg, so +# that a formula which installs correctly but links the wrong version, or fails +# to link at all, is still caught. +set -euo pipefail + +expected="${1:?usage: assert-cli-version.sh EXPECTED_VERSION}" + +if ! command -v cloudsmith >/dev/null; then + printf 'cloudsmith is not on PATH\n' >&2 + exit 1 +fi + +output="$(cloudsmith --version &2 + exit 1 +fi + +printf 'cloudsmith on PATH reports %s\n' "$actual" diff --git a/scripts/render-formula-version.py b/scripts/render-formula-version.py new file mode 100755 index 0000000..69afbfc --- /dev/null +++ b/scripts/render-formula-version.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# Copyright 2026 Cloudsmith Ltd +# +# Repoint a rendered formula at a different Cloudsmith CLI version. +# +# Used by the macOS formula workflow to produce the second version needed to +# exercise upgrade and downgrade transitions. Homebrew cannot downgrade a +# formula on its own: a downgrade is an uninstall followed by installing an +# older formula, so CI has to be able to materialise that older formula. +# +# Only the macOS sha256 values are rewritten, because the transitions run on +# macOS runners. The Linux sha256 values are deliberately left untouched and +# must not be relied on in the rendered output. +import re +import sys + +SHA256_LINE = re.compile(r'^(\s*sha256 ")[0-9a-f]{64}(")$') +VERSION_LINE = re.compile(r'^ version "(.+)"$', re.MULTILINE) +MACOS_SHA_KEYS = ("macos-arm64", "macos-x86_64") + + +def rewrite(formula, version, shas): + """Return formula repointed at version, with macOS sha256 values replaced. + + The version appears in the version stanza and in every url, so it is + replaced as a plain string. Each sha256 is matched to a platform by the url + line that precedes it, which is how the formula pairs them. + """ + current_version = VERSION_LINE.search(formula) + if not current_version: + raise SystemExit("no version stanza found in formula") + formula = formula.replace(current_version.group(1), version) + + rendered = [] + platform = None + for line in formula.split("\n"): + if ' url "' in line: + platform = next((key for key in shas if key in line), None) + sha256 = SHA256_LINE.match(line) + if sha256 and platform: + line = f"{sha256.group(1)}{shas[platform]}{sha256.group(2)}" + platform = None + rendered.append(line) + return "\n".join(rendered) + + +def main(): + if len(sys.argv) != 5: + raise SystemExit( + f"usage: {sys.argv[0]} FORMULA VERSION ARM64_SHA256 X86_64_SHA256" + ) + path, version, arm64_sha256, x86_64_sha256 = sys.argv[1:5] + shas = dict(zip(MACOS_SHA_KEYS, (arm64_sha256, x86_64_sha256))) + + with open(path, encoding="utf-8") as formula: + rendered = rewrite(formula.read(), version, shas) + with open(path, "w", encoding="utf-8") as formula: + formula.write(rendered) + + +if __name__ == "__main__": + main() From 526058d38e3bb5346a2e5cb10035754bad22f097 Mon Sep 17 00:00:00 2001 From: Ian Duffy Date: Fri, 31 Jul 2026 14:57:58 +0100 Subject: [PATCH 3/6] feat(ENG-13693): provide a pinnable rollback target A bad release currently leaves users with no supported way back. Homebrew has no downgrade command, `brew install` of an older formula is a silent no-op that leaves the newer version linked, `brew link` only ever links the newest keg, and `brew switch` no longer exists. The only routes are uninstall-then-install or reconstructing an old formula out of this tap's git history. Keep 1.19.0 as a versioned formula so the last release before the standalone binary is directly installable and pinnable, and document both it and the `brew extract` route for arbitrary versions. Versioned formulae are keg-only, but Homebrew auto-links one installed on request when no other version is present, so the rollback lands on PATH without extra steps. The documented `brew extract` route carries a caveat: formulae extracted for 1.20.1 or 1.20.2 predate the relocation fix and need preserve_rpath added. This is the immediate pin target only. Emitting a versioned formula for every release from the bump script and release workflow remains open on ENG-13693. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/formula-test.yml | 45 ++++++++++++++++++++++++++++++ Formula/cloudsmith-cli@1.19.0.rb | 30 ++++++++++++++++++++ README.md | 37 ++++++++++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 Formula/cloudsmith-cli@1.19.0.rb diff --git a/.github/workflows/formula-test.yml b/.github/workflows/formula-test.yml index 0c0cd09..49d5cac 100644 --- a/.github/workflows/formula-test.yml +++ b/.github/workflows/formula-test.yml @@ -118,3 +118,48 @@ jobs: - name: Uninstall baseline version run: brew uninstall cloudsmith-cli + + pin-target: + name: pin target ${{ matrix.arch }} + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - runner: macos-latest + arch: arm64 + - runner: macos-15-intel + arch: x86_64 + + steps: + - uses: actions/checkout@v4 + + - name: Install this checkout as the tap + run: | + tap_dir="$(brew --repository)/Library/Taps/cloudsmith-io/homebrew-cloudsmith-cli" + mkdir -p "$tap_dir" + cp -R Formula Aliases "$tap_dir/" + echo "TAP_DIR=$tap_dir" >> "$GITHUB_ENV" + + - name: Lint pin target + run: | + brew style "$TAP_DIR/Formula/cloudsmith-cli@1.19.0.rb" + brew audit --formula cloudsmith-io/cloudsmith-cli/cloudsmith-cli@1.19.0 + + - name: Install pin target + run: brew install cloudsmith-io/cloudsmith-cli/cloudsmith-cli@1.19.0 + + - name: Verify pin target is usable and holdable + run: | + # The formula is keg_only :versioned_formula, but Homebrew auto-links a + # versioned keg-only formula installed on request when no other version + # is present, so the rollback lands on PATH without extra steps. + # Matched loosely: this release predates the current --version format. + cloudsmith --version | tee /tmp/pin-version + grep -qF "1.19.0" /tmp/pin-version + brew pin cloudsmith-cli@1.19.0 + brew list --pinned | grep -qF "cloudsmith-cli@1.19.0" + brew unpin cloudsmith-cli@1.19.0 + + - name: Uninstall pin target + run: brew uninstall cloudsmith-cli@1.19.0 diff --git a/Formula/cloudsmith-cli@1.19.0.rb b/Formula/cloudsmith-cli@1.19.0.rb new file mode 100644 index 0000000..d789108 --- /dev/null +++ b/Formula/cloudsmith-cli@1.19.0.rb @@ -0,0 +1,30 @@ +# Copyright 2026 Cloudsmith Ltd +# +# Pinnable rollback target for the last release before the CLI switched to a +# PyInstaller bundle. Kept so that anyone broken by a newer release can return +# to a known-good version with `brew install cloudsmith-cli@1.19.0`, rather than +# reconstructing an old formula out of this tap's git history. +# +# Intentionally frozen: this file describes 1.19.0 and should not be bumped. +class CloudsmithCliAT1190 < Formula + desc "Official Cloudsmith Command-Line Interface (pinned 1.19.0)" + homepage "https://docs.cloudsmith.com/developer-tools/cli" + url "https://github.com/cloudsmith-io/cloudsmith-cli/releases/download/v1.19.0/cloudsmith.pyz" + sha256 "c076e4b002ee07f26774c0f8a9134f52a73b16a3fb10adb31891475485e28038" + license "Apache-2.0" + + keg_only :versioned_formula + + # The PEX/zipapp bundles all Python dependencies, so we only need Python 3.10. + depends_on "python@3.10" + + def install + libexec.install "cloudsmith.pyz" + chmod 0755, libexec/"cloudsmith.pyz" + (bin/"cloudsmith").write_env_script libexec/"cloudsmith.pyz", {} + end + + test do + assert_match version.to_s, shell_output("#{bin}/cloudsmith --version") + end +end diff --git a/README.md b/README.md index e12c259..28d50e1 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,43 @@ To verify the installed CLI: cloudsmith --version ``` +## Holding or Rolling Back a Version + +To stay on the version you already have and stop `brew upgrade` moving it: + +```bash +brew pin cloudsmith-cli +``` + +Use `brew unpin cloudsmith-cli` to release it again. + +Homebrew has no downgrade command, and installing an older formula on top of a +newer one does nothing: it reports that the version is already installed and +leaves the newer one linked. A rollback therefore has to uninstall first. + +To roll back to 1.19.0, the last release before the standalone binary, install +the pinned formula kept in this tap for that purpose: + +```bash +brew uninstall cloudsmith-cli +brew install cloudsmith-io/cloudsmith-cli/cloudsmith-cli@1.19.0 +brew pin cloudsmith-cli@1.19.0 +``` + +To roll back to any other version, extract that version's formula from this +tap's history into a tap of your own: + +```bash +brew tap-new /cloudsmith-cli-versions +brew extract --version=1.20.1 cloudsmith-io/cloudsmith-cli/cloudsmith-cli /cloudsmith-cli-versions +brew uninstall cloudsmith-cli +brew install /cloudsmith-cli-versions/cloudsmith-cli@1.20.1 +``` + +Note that formulae extracted for 1.20.1 or 1.20.2 predate a fix for a macOS +install failure and will fail with `Error: Failed to fix install linkage`. Add +`preserve_rpath` to the extracted formula to install those versions on macOS. + ## Supported Platforms | Platform | Architecture | From 9ad842b508777bea90a3af647e40d8bb4c37b00c Mon Sep 17 00:00:00 2001 From: Ian Duffy Date: Fri, 31 Jul 2026 15:09:20 +0100 Subject: [PATCH 4/6] fix(ENG-13692): make the downgrade and pin target work on both runners The first CI run found two real problems. The downgrade step left nothing on PATH. HOMEBREW_NO_INSTALL_CLEANUP keeps the superseded keg after an upgrade, so a plain `brew uninstall` removed only the linked version and left the older one installed but unlinked; the following `brew install` then reported it was "already installed, it's just not linked" and did nothing. Uninstall all versions instead. The 1.19.0 pin target failed on Intel. The zipapp's `#!/usr/bin/env python3` shebang resolved to Xcode's Python 3.9 while the zipapp requires 3.10, so the formula depended on python@3.10 without ever using it. Invoke the interpreter it depends on explicitly. This was latent in the original 1.19.0 formula and would have hit anyone whose default python3 predated 3.10. Also drops explanatory comments from the workflow and helpers. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/formula-test.yml | 19 +------------------ Formula/cloudsmith-cli@1.19.0.rb | 13 +++++++++++-- README.md | 8 -------- scripts/render-formula-version.py | 4 +--- 4 files changed, 13 insertions(+), 31 deletions(-) diff --git a/.github/workflows/formula-test.yml b/.github/workflows/formula-test.yml index 49d5cac..5bcc662 100644 --- a/.github/workflows/formula-test.yml +++ b/.github/workflows/formula-test.yml @@ -1,9 +1,6 @@ # Copyright 2026 Cloudsmith Ltd name: Test formula on macOS -# The formula ships a prebuilt PyInstaller bundle, so the things that break it -# are Homebrew's own install-time behaviour (relocation, linking) rather than -# anything a syntax check can see. Exercise the real lifecycle on real runners. on: pull_request: push: @@ -29,9 +26,6 @@ env: HOMEBREW_NO_AUTO_UPDATE: 1 HOMEBREW_NO_ENV_HINTS: 1 HOMEBREW_NO_INSTALL_CLEANUP: 1 - # The tap is assembled from this checkout rather than cloned, so Homebrew has - # no upstream commit to verify it against. The formula under test is this - # repository's own code. HOMEBREW_NO_REQUIRE_TAP_TRUST: 1 jobs: @@ -39,9 +33,6 @@ jobs: name: ${{ matrix.arch }} runs-on: ${{ matrix.runner }} strategy: - # Both architectures are affected by relocation failures, and the Intel - # bundles have less Mach-O header padding to spare, so never let one - # architecture's result stand in for the other. fail-fast: false matrix: include: @@ -55,9 +46,6 @@ jobs: - name: Install this checkout as the tap run: | - # The tap has to carry its real name. Homebrew looks preserve_rpath up - # through Formula[name], so under any other tap name the flag silently - # would not apply and the relocation guard would go untested. tap_dir="$(brew --repository)/Library/Taps/cloudsmith-io/homebrew-cloudsmith-cli" mkdir -p "$tap_dir" cp -R Formula Aliases "$tap_dir/" @@ -73,8 +61,6 @@ jobs: - name: Verify install run: | - # The formula's own test block covers the binary inside the keg; this - # additionally proves the version on PATH, i.e. that linking worked. brew test cloudsmith-io/cloudsmith-cli/cloudsmith-cli scripts/assert-cli-version.sh "$(sed -n 's/^ version "\(.*\)"$/\1/p' Formula/cloudsmith-cli.rb)" @@ -109,10 +95,7 @@ jobs: python3 scripts/render-formula-version.py \ "$TAP_DIR/Formula/cloudsmith-cli.rb" \ "$BASELINE_VERSION" "$BASELINE_SHA256_ARM64" "$BASELINE_SHA256_X86_64" - # Homebrew has no downgrade command, and plain `brew install` of an - # older formula is a silent no-op that leaves the newer version - # linked, so a downgrade has to uninstall first. - brew uninstall cloudsmith-cli + brew uninstall --force cloudsmith-cli brew install cloudsmith-io/cloudsmith-cli/cloudsmith-cli scripts/assert-cli-version.sh "$BASELINE_VERSION" diff --git a/Formula/cloudsmith-cli@1.19.0.rb b/Formula/cloudsmith-cli@1.19.0.rb index d789108..b73c06f 100644 --- a/Formula/cloudsmith-cli@1.19.0.rb +++ b/Formula/cloudsmith-cli@1.19.0.rb @@ -20,8 +20,17 @@ class CloudsmithCliAT1190 < Formula def install libexec.install "cloudsmith.pyz" - chmod 0755, libexec/"cloudsmith.pyz" - (bin/"cloudsmith").write_env_script libexec/"cloudsmith.pyz", {} + + # Run the zipapp under the interpreter this formula depends on. Its + # `#!/usr/bin/env python3` shebang would otherwise pick up whatever python3 + # comes first on PATH, which on some machines is older than the 3.10 the + # zipapp requires. + python = formula_opt_bin("python@3.10")/"python3.10" + (bin/"cloudsmith").write <<~BASH + #!/bin/bash + exec "#{python}" "#{libexec}/cloudsmith.pyz" "$@" + BASH + chmod 0755, bin/"cloudsmith" end test do diff --git a/README.md b/README.md index 28d50e1..5f51a87 100644 --- a/README.md +++ b/README.md @@ -46,10 +46,6 @@ brew pin cloudsmith-cli Use `brew unpin cloudsmith-cli` to release it again. -Homebrew has no downgrade command, and installing an older formula on top of a -newer one does nothing: it reports that the version is already installed and -leaves the newer one linked. A rollback therefore has to uninstall first. - To roll back to 1.19.0, the last release before the standalone binary, install the pinned formula kept in this tap for that purpose: @@ -69,10 +65,6 @@ brew uninstall cloudsmith-cli brew install /cloudsmith-cli-versions/cloudsmith-cli@1.20.1 ``` -Note that formulae extracted for 1.20.1 or 1.20.2 predate a fix for a macOS -install failure and will fail with `Error: Failed to fix install linkage`. Add -`preserve_rpath` to the extracted formula to install those versions on macOS. - ## Supported Platforms | Platform | Architecture | diff --git a/scripts/render-formula-version.py b/scripts/render-formula-version.py index 69afbfc..9fe705a 100755 --- a/scripts/render-formula-version.py +++ b/scripts/render-formula-version.py @@ -4,9 +4,7 @@ # Repoint a rendered formula at a different Cloudsmith CLI version. # # Used by the macOS formula workflow to produce the second version needed to -# exercise upgrade and downgrade transitions. Homebrew cannot downgrade a -# formula on its own: a downgrade is an uninstall followed by installing an -# older formula, so CI has to be able to materialise that older formula. +# exercise upgrade and downgrade transitions. # # Only the macOS sha256 values are rewritten, because the transitions run on # macOS runners. The Linux sha256 values are deliberately left untouched and From 1e54536322b3d257b0bced3762568dcd16d9d1dd Mon Sep 17 00:00:00 2001 From: Ian Duffy Date: Fri, 31 Jul 2026 15:16:01 +0100 Subject: [PATCH 5/6] fix(ENG-13693): require arm64 for the 1.19.0 pin target on macOS CI showed the pin target failing on Intel with a PEX resolution dump. The 1.19.0 zipapp bundles native wheels for macOS arm64 only: every rpds-py, pydantic-core and cffi wheel in it is macosx_11_0_arm64, with no macosx x86_64 build for any CPython version, so no interpreter on an Intel Mac can satisfy it. Linux carries both architectures and is unaffected. That is a property of the published artifact rather than of the formula, so declare the requirement and fail immediately with a clear message. Restrict the pin target job to arm64 and note the limitation in the README. Intel Macs are consequently left without a first-class rollback target. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/formula-test.yml | 14 ++++---------- Formula/cloudsmith-cli@1.19.0.rb | 7 +++++++ README.md | 4 +++- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/.github/workflows/formula-test.yml b/.github/workflows/formula-test.yml index 5bcc662..3e7c7ab 100644 --- a/.github/workflows/formula-test.yml +++ b/.github/workflows/formula-test.yml @@ -102,17 +102,11 @@ jobs: - name: Uninstall baseline version run: brew uninstall cloudsmith-cli + # arm64 only: the 1.19.0 zipapp carries no macOS x86_64 native wheels, so the + # formula requires arm64 on macOS and there is nothing to test on Intel. pin-target: - name: pin target ${{ matrix.arch }} - runs-on: ${{ matrix.runner }} - strategy: - fail-fast: false - matrix: - include: - - runner: macos-latest - arch: arm64 - - runner: macos-15-intel - arch: x86_64 + name: pin target arm64 + runs-on: macos-latest steps: - uses: actions/checkout@v4 diff --git a/Formula/cloudsmith-cli@1.19.0.rb b/Formula/cloudsmith-cli@1.19.0.rb index b73c06f..7d882bc 100644 --- a/Formula/cloudsmith-cli@1.19.0.rb +++ b/Formula/cloudsmith-cli@1.19.0.rb @@ -18,6 +18,13 @@ class CloudsmithCliAT1190 < Formula # The PEX/zipapp bundles all Python dependencies, so we only need Python 3.10. depends_on "python@3.10" + # The 1.19.0 zipapp bundles native wheels for macOS arm64 only: it carries no + # macosx x86_64 build of rpds-py, pydantic-core or cffi, so it cannot run on an + # Intel Mac. Fail with that up front rather than a PEX resolution dump. + on_macos do + depends_on arch: :arm64 + end + def install libexec.install "cloudsmith.pyz" diff --git a/README.md b/README.md index 5f51a87..b116c01 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,9 @@ brew pin cloudsmith-cli Use `brew unpin cloudsmith-cli` to release it again. To roll back to 1.19.0, the last release before the standalone binary, install -the pinned formula kept in this tap for that purpose: +the pinned formula kept in this tap for that purpose. On macOS this needs Apple +Silicon, because the 1.19.0 zipapp shipped no Intel macOS builds of its native +dependencies: ```bash brew uninstall cloudsmith-cli From 4cc947e8d5b511187924f1191f39b856c817bac9 Mon Sep 17 00:00:00 2001 From: Ian Duffy Date: Fri, 31 Jul 2026 15:20:12 +0100 Subject: [PATCH 6/6] feat(ENG-13693): add a 1.20.1 pin target for all platforms cloudsmith-cli@1.19.0 covers rolling back past the PyInstaller packaging change, but its artifact is macOS arm64 only, which left Intel Macs without a rollback target. Keep 1.20.1 as well: it is the standalone binary, so it ships every supported platform, and it carries preserve_rpath so it installs cleanly. Verified locally on arm64: installs, auto-links onto PATH, reports 1.20.1, keeps its @rpath dylib IDs, passes brew test, and pins. The pin target job now covers both formulae, with 1.19.0 on arm64 only. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/formula-test.yml | 43 +++++++++++++++++++----------- Formula/cloudsmith-cli@1.20.1.rb | 40 +++++++++++++++++++++++++++ README.md | 19 ++++++++----- 3 files changed, 80 insertions(+), 22 deletions(-) create mode 100644 Formula/cloudsmith-cli@1.20.1.rb diff --git a/.github/workflows/formula-test.yml b/.github/workflows/formula-test.yml index 3e7c7ab..9ff4f51 100644 --- a/.github/workflows/formula-test.yml +++ b/.github/workflows/formula-test.yml @@ -102,11 +102,23 @@ jobs: - name: Uninstall baseline version run: brew uninstall cloudsmith-cli - # arm64 only: the 1.19.0 zipapp carries no macOS x86_64 native wheels, so the - # formula requires arm64 on macOS and there is nothing to test on Intel. pin-target: - name: pin target arm64 - runs-on: macos-latest + name: pin ${{ matrix.version }} ${{ matrix.arch }} + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + # 1.19.0 ships macOS arm64 builds only, so it has no Intel entry. + - runner: macos-latest + arch: arm64 + version: "1.19.0" + - runner: macos-latest + arch: arm64 + version: "1.20.1" + - runner: macos-15-intel + arch: x86_64 + version: "1.20.1" steps: - uses: actions/checkout@v4 @@ -117,26 +129,25 @@ jobs: mkdir -p "$tap_dir" cp -R Formula Aliases "$tap_dir/" echo "TAP_DIR=$tap_dir" >> "$GITHUB_ENV" + echo "PIN=cloudsmith-cli@${{ matrix.version }}" >> "$GITHUB_ENV" - name: Lint pin target run: | - brew style "$TAP_DIR/Formula/cloudsmith-cli@1.19.0.rb" - brew audit --formula cloudsmith-io/cloudsmith-cli/cloudsmith-cli@1.19.0 + brew style "$TAP_DIR/Formula/$PIN.rb" + brew audit --formula "cloudsmith-io/cloudsmith-cli/$PIN" - name: Install pin target - run: brew install cloudsmith-io/cloudsmith-cli/cloudsmith-cli@1.19.0 + run: brew install "cloudsmith-io/cloudsmith-cli/$PIN" - name: Verify pin target is usable and holdable run: | - # The formula is keg_only :versioned_formula, but Homebrew auto-links a - # versioned keg-only formula installed on request when no other version - # is present, so the rollback lands on PATH without extra steps. - # Matched loosely: this release predates the current --version format. + # Matched loosely so that both the current --version format and the + # older one 1.19.0 predates are accepted. cloudsmith --version | tee /tmp/pin-version - grep -qF "1.19.0" /tmp/pin-version - brew pin cloudsmith-cli@1.19.0 - brew list --pinned | grep -qF "cloudsmith-cli@1.19.0" - brew unpin cloudsmith-cli@1.19.0 + grep -qF "${{ matrix.version }}" /tmp/pin-version + brew pin "$PIN" + brew list --pinned | grep -qF "$PIN" + brew unpin "$PIN" - name: Uninstall pin target - run: brew uninstall cloudsmith-cli@1.19.0 + run: brew uninstall "$PIN" diff --git a/Formula/cloudsmith-cli@1.20.1.rb b/Formula/cloudsmith-cli@1.20.1.rb new file mode 100644 index 0000000..f913d5a --- /dev/null +++ b/Formula/cloudsmith-cli@1.20.1.rb @@ -0,0 +1,40 @@ +# Copyright 2026 Cloudsmith Ltd +# +# Pinnable rollback target covering every supported platform. Kept alongside +# cloudsmith-cli@1.19.0, which is the escape hatch from the PyInstaller +# packaging but ships macOS arm64 builds only. +# +# Intentionally frozen: this file describes 1.20.1 and should not be bumped. +class CloudsmithCliAT1201 < Formula + desc "Official Cloudsmith Command-Line Interface (pinned 1.20.1)" + homepage "https://docs.cloudsmith.com/developer-tools/cli" + version "1.20.1" + license "Apache-2.0" + + if OS.mac? && Hardware::CPU.arm? + url "https://dl.cloudsmith.io/public/cloudsmith/cli/raw/names/cloudsmith-cli-macos-arm64/versions/1.20.1/cloudsmith-1.20.1-macos-arm64.tar.gz" + sha256 "2c2580eb8725467877f2a296d675fd681abe01050099a6405d5b4c37c6f3b901" + elsif OS.mac? && Hardware::CPU.intel? + url "https://dl.cloudsmith.io/public/cloudsmith/cli/raw/names/cloudsmith-cli-macos-x86_64/versions/1.20.1/cloudsmith-1.20.1-macos-x86_64.tar.gz" + sha256 "a8e959909caab7d8d6fac390d530cd2a4bff3e70c97e12e06e2587a5b7ddfc85" + elsif OS.linux? && Hardware::CPU.arm? && Hardware::CPU.is_64_bit? + url "https://dl.cloudsmith.io/public/cloudsmith/cli/raw/names/cloudsmith-cli-linux-aarch64-gnu/versions/1.20.1/cloudsmith-1.20.1-linux-aarch64-gnu.tar.gz" + sha256 "7ff869d1d059759a938d97bdc5173d7f481782dfa7677870797b8643bd09c95c" + elsif OS.linux? && Hardware::CPU.intel? && Hardware::CPU.is_64_bit? + url "https://dl.cloudsmith.io/public/cloudsmith/cli/raw/names/cloudsmith-cli-linux-x86_64-gnu/versions/1.20.1/cloudsmith-1.20.1-linux-x86_64-gnu.tar.gz" + sha256 "1738b6057cac7fb60dd9a6bd72fe335560ef51d93f79b052be2df379fb2fb385" + end + + keg_only :versioned_formula + + preserve_rpath + + def install + libexec.install Dir["*"] + bin.write_exec_script libexec/"cloudsmith" + end + + test do + assert_match "CLI Package Version: #{version}", shell_output("#{bin}/cloudsmith --version") + end +end diff --git a/README.md b/README.md index b116c01..5c98a3d 100644 --- a/README.md +++ b/README.md @@ -46,15 +46,22 @@ brew pin cloudsmith-cli Use `brew unpin cloudsmith-cli` to release it again. -To roll back to 1.19.0, the last release before the standalone binary, install -the pinned formula kept in this tap for that purpose. On macOS this needs Apple -Silicon, because the 1.19.0 zipapp shipped no Intel macOS builds of its native -dependencies: +This tap keeps two older versions as pinnable rollback targets: + +| Formula | Version | Platforms | +| --- | --- | --- | +| `cloudsmith-cli@1.20.1` | 1.20.1, standalone binary | all supported platforms | +| `cloudsmith-cli@1.19.0` | 1.19.0, last Python zipapp release | macOS arm64 and Linux | + +`cloudsmith-cli@1.19.0` is unavailable on Intel macOS because that release +shipped no Intel macOS builds of its native dependencies. + +To roll back, uninstall the current version and install the target: ```bash brew uninstall cloudsmith-cli -brew install cloudsmith-io/cloudsmith-cli/cloudsmith-cli@1.19.0 -brew pin cloudsmith-cli@1.19.0 +brew install cloudsmith-io/cloudsmith-cli/cloudsmith-cli@1.20.1 +brew pin cloudsmith-cli@1.20.1 ``` To roll back to any other version, extract that version's formula from this