From 82a0b8366c27f079b74a3ba667cd84cda389dfc3 Mon Sep 17 00:00:00 2001 From: Ian Duffy Date: Thu, 1 Oct 2026 22:49:02 +0100 Subject: [PATCH 1/7] feat(ENG-14534): open a cloudsmith-cli-action bump PR on each release Add a publish-action release job. It clones cloudsmith-cli-action as cloudsmith-bot and runs the action's scripts/bump-cli-version.sh. Then it makes a signed commit and opens a pull request, as publish-homebrew does for the tap. Without the ACTION_PUSH_TOKEN secret, the job skips. Move the Cloudsmith Bot commit-signing setup into the setup-cloudsmith-bot-git composite action, which both jobs use. Co-Authored-By: Claude Opus 5.5 --- .../setup-cloudsmith-bot-git/action.yml | 27 +++++++ .github/workflows/release.yml | 73 ++++++++++++++++--- 2 files changed, 90 insertions(+), 10 deletions(-) create mode 100644 .github/actions/setup-cloudsmith-bot-git/action.yml diff --git a/.github/actions/setup-cloudsmith-bot-git/action.yml b/.github/actions/setup-cloudsmith-bot-git/action.yml new file mode 100644 index 00000000..4ee926b0 --- /dev/null +++ b/.github/actions/setup-cloudsmith-bot-git/action.yml @@ -0,0 +1,27 @@ +name: Set up Cloudsmith Bot git signing +description: >- + Configure git to commit as Cloudsmith Bot and to sign each commit with the + SSH key of the bot. + +inputs: + ssh-signing-key: + description: Private SSH key that signs the commits of Cloudsmith Bot. + required: true + +runs: + using: composite + steps: + - shell: bash + env: + CLOUDSMITH_BOT_SSH_KEY: ${{ inputs.ssh-signing-key }} + run: | + set -euo pipefail + SIGNING_KEY="${RUNNER_TEMP}/cloudsmith-bot-signing-key" + printf '%s\n' "${CLOUDSMITH_BOT_SSH_KEY}" > "${SIGNING_KEY}" + chmod 600 "${SIGNING_KEY}" + ssh-keygen -y -f "${SIGNING_KEY}" > "${SIGNING_KEY}.pub" + git config --global user.name "Cloudsmith Bot" + git config --global user.email "vftbot@cloudsmith.io" + git config --global gpg.format ssh + git config --global user.signingkey "${SIGNING_KEY}.pub" + git config --global commit.gpgsign true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7e1d459e..181d822f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -619,11 +619,16 @@ jobs: echo "verified ${URL}" done + - name: Set up commit signing as Cloudsmith Bot + if: env.HAS_TAP_TOKEN == 'true' + uses: ./.github/actions/setup-cloudsmith-bot-git + with: + ssh-signing-key: ${{ secrets.CLOUDSMITH_BOT_SSH_KEY }} + - name: Open a bump pull request against the tap repository if: env.HAS_TAP_TOKEN == 'true' env: GH_TOKEN: ${{ secrets.TAP_PUSH_TOKEN }} - CLOUDSMITH_BOT_SSH_KEY: ${{ secrets.CLOUDSMITH_BOT_SSH_KEY }} TAP_REPO: ${{ github.repository_owner }}/homebrew-cloudsmith-cli run: | set -euo pipefail @@ -637,15 +642,6 @@ jobs: echo "tap already up to date; skipping" exit 0 fi - SIGNING_KEY="${RUNNER_TEMP}/cloudsmith-bot-signing-key" - printf '%s\n' "${CLOUDSMITH_BOT_SSH_KEY}" > "${SIGNING_KEY}" - chmod 600 "${SIGNING_KEY}" - ssh-keygen -y -f "${SIGNING_KEY}" > "${SIGNING_KEY}.pub" - git -C tap config user.name "Cloudsmith Bot" - git -C tap config user.email "vftbot@cloudsmith.io" - git -C tap config gpg.format ssh - git -C tap config user.signingkey "${SIGNING_KEY}.pub" - git -C tap config commit.gpgsign true git -C tap checkout -b "${BRANCH}" git -C tap add Formula/cloudsmith-cli.rb Aliases/cloudsmith git -C tap commit -m "Bump cloudsmith-cli to v${VERSION}" @@ -663,6 +659,63 @@ jobs: if: env.HAS_TAP_TOKEN != 'true' run: echo "TAP_PUSH_TOKEN not configured; formula staged as a workflow artifact only." + publish-action: + # Opens a pull request against /cloudsmith-cli-action that pins the + # default cli-version of the action to this release. The bump script in + # the action repository also adds a release to its CHANGELOG.md. + # The action repository requires reviewed pull requests. When the pull + # request merges, the action repository tests, tags and releases it. + needs: [validate, publish-github] + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + env: + HAS_ACTION_TOKEN: ${{ secrets.ACTION_PUSH_TOKEN != '' }} + VERSION: ${{ needs.validate.outputs.version }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up commit signing as Cloudsmith Bot + if: env.HAS_ACTION_TOKEN == 'true' + uses: ./.github/actions/setup-cloudsmith-bot-git + with: + ssh-signing-key: ${{ secrets.CLOUDSMITH_BOT_SSH_KEY }} + + - name: Open a CLI bump pull request against the action repository + if: env.HAS_ACTION_TOKEN == 'true' + env: + GH_TOKEN: ${{ secrets.ACTION_PUSH_TOKEN }} + ACTION_REPO: ${{ github.repository_owner }}/cloudsmith-cli-action + run: | + set -euo pipefail + BRANCH="release/cloudsmith-cli-v${VERSION}" + gh auth setup-git + git clone --depth 1 "https://github.com/${ACTION_REPO}.git" action + ACTION_VERSION="$(cd action && bash scripts/bump-cli-version.sh "${VERSION}")" + if [ -z "${ACTION_VERSION}" ]; then + echo "action already pins cloudsmith-cli v${VERSION}; skipping" + exit 0 + fi + git -C action checkout -b "${BRANCH}" + git -C action add action.yml CHANGELOG.md + git -C action commit -m "Bump cloudsmith-cli to v${VERSION}" + git -C action push --force origin "${BRANCH}" + if [ -z "$(gh pr list --repo "${ACTION_REPO}" --head "${BRANCH}" --state open --json number --jq '.[].number')" ]; then + gh pr create --repo "${ACTION_REPO}" \ + --head "${BRANCH}" --base master \ + --title "Bump cloudsmith-cli to v${VERSION}" \ + --body "Automated update that pins the default cli-version to cloudsmith-cli v${VERSION}. When this pull request merges and the tests pass, the action releases v${ACTION_VERSION} and moves its major tag. Merge with a squash merge." + else + echo "pull request for ${BRANCH} already open" + fi + + - name: Report a skipped action update + if: env.HAS_ACTION_TOKEN != 'true' + run: echo "ACTION_PUSH_TOKEN not configured; action update skipped." + publish-github: needs: - validate From 4575b2973802dd1a28c51233c4e62e905a270680 Mon Sep 17 00:00:00 2001 From: Ian Duffy Date: Thu, 1 Oct 2026 22:55:52 +0100 Subject: [PATCH 2/7] fix(ENG-14534): replace an unmerged action bump with the newest release Rebuild one release/cloudsmith-cli branch from the action master on each CLI release, and update the open pull request. Two CLI releases before a merge then cannot produce two pull requests with the same action version. Run the job in one concurrency group. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 181d822f..250a4191 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -665,9 +665,15 @@ jobs: # the action repository also adds a release to its CHANGELOG.md. # The action repository requires reviewed pull requests. When the pull # request merges, the action repository tests, tags and releases it. + # Each run rebuilds one branch from master, so a newer CLI release + # replaces a bump that is not merged yet. The bump never repeats an + # action version and never pins an older CLI. needs: [validate, publish-github] runs-on: ubuntu-24.04 timeout-minutes: 10 + concurrency: + group: publish-action + cancel-in-progress: false permissions: contents: read env: @@ -691,7 +697,8 @@ jobs: ACTION_REPO: ${{ github.repository_owner }}/cloudsmith-cli-action run: | set -euo pipefail - BRANCH="release/cloudsmith-cli-v${VERSION}" + BRANCH="release/cloudsmith-cli" + TITLE="Bump cloudsmith-cli to v${VERSION}" gh auth setup-git git clone --depth 1 "https://github.com/${ACTION_REPO}.git" action ACTION_VERSION="$(cd action && bash scripts/bump-cli-version.sh "${VERSION}")" @@ -701,15 +708,17 @@ jobs: fi git -C action checkout -b "${BRANCH}" git -C action add action.yml CHANGELOG.md - git -C action commit -m "Bump cloudsmith-cli to v${VERSION}" + git -C action commit -m "${TITLE}" git -C action push --force origin "${BRANCH}" - if [ -z "$(gh pr list --repo "${ACTION_REPO}" --head "${BRANCH}" --state open --json number --jq '.[].number')" ]; then + BODY="Automated update that pins the default cli-version to cloudsmith-cli v${VERSION}. When this pull request merges and the tests pass, the action releases v${ACTION_VERSION} and moves its major tag. Merge with a squash merge." + PR_NUMBER="$(gh pr list --repo "${ACTION_REPO}" --head "${BRANCH}" --state open --json number --jq '.[].number')" + if [ -z "${PR_NUMBER}" ]; then gh pr create --repo "${ACTION_REPO}" \ --head "${BRANCH}" --base master \ - --title "Bump cloudsmith-cli to v${VERSION}" \ - --body "Automated update that pins the default cli-version to cloudsmith-cli v${VERSION}. When this pull request merges and the tests pass, the action releases v${ACTION_VERSION} and moves its major tag. Merge with a squash merge." + --title "${TITLE}" --body "${BODY}" else - echo "pull request for ${BRANCH} already open" + gh pr edit "${PR_NUMBER}" --repo "${ACTION_REPO}" \ + --title "${TITLE}" --body "${BODY}" fi - name: Report a skipped action update From a9358fcabcd489d7e9721ed7d074a1ca10cdcdf5 Mon Sep 17 00:00:00 2001 From: Ian Duffy Date: Thu, 1 Oct 2026 23:23:54 +0100 Subject: [PATCH 3/7] fix(ENG-14534): keep a newer pending action bump on an older re-run Read the pin on the open release/cloudsmith-cli pull request before the bump. When that pin is newer than this release, skip the job, so that a re-run of an older CLI release cannot replace a newer pending bump. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 250a4191..0011b03b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -667,7 +667,8 @@ jobs: # request merges, the action repository tests, tags and releases it. # Each run rebuilds one branch from master, so a newer CLI release # replaces a bump that is not merged yet. The bump never repeats an - # action version and never pins an older CLI. + # action version. It never pins a CLI older than master or the open pull + # request already pins. needs: [validate, publish-github] runs-on: ubuntu-24.04 timeout-minutes: 10 @@ -701,6 +702,16 @@ jobs: TITLE="Bump cloudsmith-cli to v${VERSION}" gh auth setup-git git clone --depth 1 "https://github.com/${ACTION_REPO}.git" action + PR_NUMBER="$(gh pr list --repo "${ACTION_REPO}" --head "${BRANCH}" --state open --json number --jq '.[].number')" + if [ -n "${PR_NUMBER}" ]; then + git -C action fetch --quiet --depth 1 origin "${BRANCH}" + PENDING_VERSION="$(git -C action show FETCH_HEAD:action.yml | yq '.inputs["cli-version"].default')" + NEWEST_VERSION="$(printf '%s\n' "${PENDING_VERSION}" "${VERSION}" | sort -V | tail -n 1)" + if [ "${PENDING_VERSION}" != "${VERSION}" ] && [ "${NEWEST_VERSION}" = "${PENDING_VERSION}" ]; then + echo "pull request #${PR_NUMBER} already pins newer cloudsmith-cli v${PENDING_VERSION}; skipping" + exit 0 + fi + fi ACTION_VERSION="$(cd action && bash scripts/bump-cli-version.sh "${VERSION}")" if [ -z "${ACTION_VERSION}" ]; then echo "action already pins cloudsmith-cli v${VERSION}; skipping" @@ -711,7 +722,6 @@ jobs: git -C action commit -m "${TITLE}" git -C action push --force origin "${BRANCH}" BODY="Automated update that pins the default cli-version to cloudsmith-cli v${VERSION}. When this pull request merges and the tests pass, the action releases v${ACTION_VERSION} and moves its major tag. Merge with a squash merge." - PR_NUMBER="$(gh pr list --repo "${ACTION_REPO}" --head "${BRANCH}" --state open --json number --jq '.[].number')" if [ -z "${PR_NUMBER}" ]; then gh pr create --repo "${ACTION_REPO}" \ --head "${BRANCH}" --base master \ From c4ce6162682114e165a4e12d79b8884c73953c76 Mon Sep 17 00:00:00 2001 From: Ian Duffy Date: Mon, 5 Oct 2026 13:09:58 +0100 Subject: [PATCH 4/7] fix(ENG-14534): queue every action bump and retry when the PR closes Keep all pending publish-action jobs in the concurrency queue. A re-run of an older tag no longer cancels a newer pending release. Check the bump pull request after the push. When it closed during the update, start the update again from the latest master. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 67 ++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 28 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0011b03b..c4158527 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -668,13 +668,15 @@ jobs: # Each run rebuilds one branch from master, so a newer CLI release # replaces a bump that is not merged yet. The bump never repeats an # action version. It never pins a CLI older than master or the open pull - # request already pins. + # request already pins. When the pull request closes during the update, + # the update starts again from master. needs: [validate, publish-github] runs-on: ubuntu-24.04 timeout-minutes: 10 concurrency: group: publish-action cancel-in-progress: false + queue: max permissions: contents: read env: @@ -701,35 +703,44 @@ jobs: BRANCH="release/cloudsmith-cli" TITLE="Bump cloudsmith-cli to v${VERSION}" gh auth setup-git - git clone --depth 1 "https://github.com/${ACTION_REPO}.git" action - PR_NUMBER="$(gh pr list --repo "${ACTION_REPO}" --head "${BRANCH}" --state open --json number --jq '.[].number')" - if [ -n "${PR_NUMBER}" ]; then - git -C action fetch --quiet --depth 1 origin "${BRANCH}" - PENDING_VERSION="$(git -C action show FETCH_HEAD:action.yml | yq '.inputs["cli-version"].default')" - NEWEST_VERSION="$(printf '%s\n' "${PENDING_VERSION}" "${VERSION}" | sort -V | tail -n 1)" - if [ "${PENDING_VERSION}" != "${VERSION}" ] && [ "${NEWEST_VERSION}" = "${PENDING_VERSION}" ]; then - echo "pull request #${PR_NUMBER} already pins newer cloudsmith-cli v${PENDING_VERSION}; skipping" + for attempt in 1 2 3; do + rm -rf action + git clone --depth 1 "https://github.com/${ACTION_REPO}.git" action + PR_NUMBER="$(gh pr list --repo "${ACTION_REPO}" --head "${BRANCH}" --state open --json number --jq '.[].number')" + if [ -n "${PR_NUMBER}" ]; then + git -C action fetch --quiet --depth 1 origin "${BRANCH}" + PENDING_VERSION="$(git -C action show FETCH_HEAD:action.yml | yq '.inputs["cli-version"].default')" + NEWEST_VERSION="$(printf '%s\n' "${PENDING_VERSION}" "${VERSION}" | sort -V | tail -n 1)" + if [ "${PENDING_VERSION}" != "${VERSION}" ] && [ "${NEWEST_VERSION}" = "${PENDING_VERSION}" ]; then + echo "pull request #${PR_NUMBER} already pins newer cloudsmith-cli v${PENDING_VERSION}; skipping" + exit 0 + fi + fi + ACTION_VERSION="$(cd action && bash scripts/bump-cli-version.sh "${VERSION}")" + if [ -z "${ACTION_VERSION}" ]; then + echo "action already pins cloudsmith-cli v${VERSION}; skipping" exit 0 fi - fi - ACTION_VERSION="$(cd action && bash scripts/bump-cli-version.sh "${VERSION}")" - if [ -z "${ACTION_VERSION}" ]; then - echo "action already pins cloudsmith-cli v${VERSION}; skipping" - exit 0 - fi - git -C action checkout -b "${BRANCH}" - git -C action add action.yml CHANGELOG.md - git -C action commit -m "${TITLE}" - git -C action push --force origin "${BRANCH}" - BODY="Automated update that pins the default cli-version to cloudsmith-cli v${VERSION}. When this pull request merges and the tests pass, the action releases v${ACTION_VERSION} and moves its major tag. Merge with a squash merge." - if [ -z "${PR_NUMBER}" ]; then - gh pr create --repo "${ACTION_REPO}" \ - --head "${BRANCH}" --base master \ - --title "${TITLE}" --body "${BODY}" - else - gh pr edit "${PR_NUMBER}" --repo "${ACTION_REPO}" \ - --title "${TITLE}" --body "${BODY}" - fi + git -C action checkout -b "${BRANCH}" + git -C action add action.yml CHANGELOG.md + git -C action commit -m "${TITLE}" + git -C action push --force origin "${BRANCH}" + BODY="Automated update that pins the default cli-version to cloudsmith-cli v${VERSION}. When this pull request merges and the tests pass, the action releases v${ACTION_VERSION} and moves its major tag. Merge with a squash merge." + if [ -z "${PR_NUMBER}" ]; then + gh pr create --repo "${ACTION_REPO}" \ + --head "${BRANCH}" --base master \ + --title "${TITLE}" --body "${BODY}" + exit 0 + fi + if [ "$(gh pr view "${PR_NUMBER}" --repo "${ACTION_REPO}" --json state --jq .state)" = OPEN ]; then + gh pr edit "${PR_NUMBER}" --repo "${ACTION_REPO}" \ + --title "${TITLE}" --body "${BODY}" + exit 0 + fi + echo "pull request #${PR_NUMBER} closed during attempt ${attempt}; starting again from master" + done + echo "pull request for ${BRANCH} kept closing during the update" >&2 + exit 1 - name: Report a skipped action update if: env.HAS_ACTION_TOKEN != 'true' From bfb14575afc628cdac27a1d86386859d2df5ab52 Mon Sep 17 00:00:00 2001 From: Bartosz Blizniak Date: Mon, 19 Oct 2026 13:05:13 +0100 Subject: [PATCH 5/7] ci: share downstream release token across action and Homebrew updates --- .github/workflows/release.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 91e6f96a..2f4d006a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -537,7 +537,7 @@ jobs: # that publish-cloudsmith just made live and opens a bump pull request # against the Homebrew tap (/homebrew-cloudsmith-cli). When the tap # checks pass, the job squash-merges the pull request with --admin. - # Without TAP_PUSH_TOKEN the rendered formula is only staged as a + # Without DOWNSTREAM_RELEASE_PUSH_TOKEN the rendered formula is only staged as a # workflow artifact. needs: [validate, publish-cloudsmith] runs-on: ubuntu-24.04 @@ -547,7 +547,7 @@ jobs: env: CLOUDSMITH_NAMESPACE: ${{ vars.CLOUDSMITH_NAMESPACE }} CLOUDSMITH_REPO: ${{ vars.CLOUDSMITH_REPO }} - HAS_TAP_TOKEN: ${{ secrets.TAP_PUSH_TOKEN != '' }} + HAS_DOWNSTREAM_TOKEN: ${{ secrets.DOWNSTREAM_RELEASE_PUSH_TOKEN != '' }} VERSION: ${{ needs.validate.outputs.version }} # Targets shipped via Homebrew (subset of BINARY_TARGETS; Homebrew on # Linux is glibc-based, so the gnu builds). @@ -620,15 +620,15 @@ jobs: done - name: Set up commit signing as Cloudsmith Bot - if: env.HAS_TAP_TOKEN == 'true' + if: env.HAS_DOWNSTREAM_TOKEN == 'true' uses: ./.github/actions/setup-cloudsmith-bot-git with: ssh-signing-key: ${{ secrets.CLOUDSMITH_BOT_SSH_KEY }} - name: Open and merge a bump pull request against the tap repository - if: env.HAS_TAP_TOKEN == 'true' + if: env.HAS_DOWNSTREAM_TOKEN == 'true' env: - GH_TOKEN: ${{ secrets.TAP_PUSH_TOKEN }} + GH_TOKEN: ${{ secrets.DOWNSTREAM_RELEASE_PUSH_TOKEN }} TAP_REPO: ${{ github.repository_owner }}/homebrew-cloudsmith-cli run: | set -euo pipefail @@ -662,8 +662,8 @@ jobs: --match-head-commit "$(git -C tap rev-parse HEAD)" - name: Report a skipped tap update - if: env.HAS_TAP_TOKEN != 'true' - run: echo "TAP_PUSH_TOKEN not configured; formula staged as a workflow artifact only." + if: env.HAS_DOWNSTREAM_TOKEN != 'true' + run: echo "DOWNSTREAM_RELEASE_PUSH_TOKEN not configured; formula staged as a workflow artifact only." publish-action: # Opens a pull request against /cloudsmith-cli-action that pins the @@ -686,7 +686,7 @@ jobs: permissions: contents: read env: - HAS_ACTION_TOKEN: ${{ secrets.ACTION_PUSH_TOKEN != '' }} + HAS_DOWNSTREAM_TOKEN: ${{ secrets.DOWNSTREAM_RELEASE_PUSH_TOKEN != '' }} VERSION: ${{ needs.validate.outputs.version }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -694,15 +694,15 @@ jobs: persist-credentials: false - name: Set up commit signing as Cloudsmith Bot - if: env.HAS_ACTION_TOKEN == 'true' + if: env.HAS_DOWNSTREAM_TOKEN == 'true' uses: ./.github/actions/setup-cloudsmith-bot-git with: ssh-signing-key: ${{ secrets.CLOUDSMITH_BOT_SSH_KEY }} - name: Open a CLI bump pull request against the action repository - if: env.HAS_ACTION_TOKEN == 'true' + if: env.HAS_DOWNSTREAM_TOKEN == 'true' env: - GH_TOKEN: ${{ secrets.ACTION_PUSH_TOKEN }} + GH_TOKEN: ${{ secrets.DOWNSTREAM_RELEASE_PUSH_TOKEN }} ACTION_REPO: ${{ github.repository_owner }}/cloudsmith-cli-action run: | set -euo pipefail @@ -749,8 +749,8 @@ jobs: exit 1 - name: Report a skipped action update - if: env.HAS_ACTION_TOKEN != 'true' - run: echo "ACTION_PUSH_TOKEN not configured; action update skipped." + if: env.HAS_DOWNSTREAM_TOKEN != 'true' + run: echo "DOWNSTREAM_RELEASE_PUSH_TOKEN not configured; action update skipped." publish-github: needs: From 6c3c170d7a9967a8bac47e3dcc6e652592c4dda9 Mon Sep 17 00:00:00 2001 From: Ian Duffy Date: Mon, 19 Oct 2026 14:36:59 +0100 Subject: [PATCH 6/7] feat(ENG-14534): send a release event to downstream repositories Replace the publish-homebrew and publish-action jobs with one notify-downstream-repos job. The job sends a cloudsmith-cli-release event with the version to each repository in its matrix. Each repository owns the change to its files and runs its own workflow. Add the open-cli-bump-pr composite action for those workflows. It runs the bump command of the repository in a clone and opens or updates a pull request from Cloudsmith Bot. A newer release replaces a bump that is not merged yet, and an older release never replaces a newer one. The script also runs locally, with --dry-run to show the commit only. Move the Homebrew formula template to the tap repository. Co-Authored-By: Claude Opus 5.5 --- .github/actions/open-cli-bump-pr/action.yml | 60 +++++ .../open-cli-bump-pr/open-cli-bump-pr.sh | 99 ++++++++ .../open-cli-bump-pr/test-open-cli-bump-pr.sh | 114 +++++++++ .../setup-cloudsmith-bot-git/action.yml | 27 --- .github/workflows/lint.yml | 2 + .github/workflows/release.yml | 224 ++---------------- packaging/homebrew/cloudsmith-cli.rb.tmpl | 48 ---- 7 files changed, 293 insertions(+), 281 deletions(-) create mode 100644 .github/actions/open-cli-bump-pr/action.yml create mode 100755 .github/actions/open-cli-bump-pr/open-cli-bump-pr.sh create mode 100755 .github/actions/open-cli-bump-pr/test-open-cli-bump-pr.sh delete mode 100644 .github/actions/setup-cloudsmith-bot-git/action.yml delete mode 100644 packaging/homebrew/cloudsmith-cli.rb.tmpl diff --git a/.github/actions/open-cli-bump-pr/action.yml b/.github/actions/open-cli-bump-pr/action.yml new file mode 100644 index 00000000..335f4aed --- /dev/null +++ b/.github/actions/open-cli-bump-pr/action.yml @@ -0,0 +1,60 @@ +name: Open a Cloudsmith CLI bump pull request +description: >- + Run a bump command in a clone of the calling repository and open or update a + pull request from Cloudsmith Bot that moves it to a Cloudsmith CLI release. + The command runs in the root of the clone, with VERSION in the environment. + +inputs: + version: + description: The Cloudsmith CLI version to move to, for example 1.29.1. + required: true + bump-command: + description: The shell command that changes the files of the repository. + required: true + merge: + description: Set to true to squash-merge the pull request when its checks pass. + required: false + default: "false" + token: + description: >- + A Cloudsmith Bot token that can push branches and open pull requests. A + pull request from GITHUB_TOKEN does not start the checks of the repository. + required: true + ssh-signing-key: + description: The private SSH key that signs the commits of Cloudsmith Bot. + required: true + +runs: + using: composite + steps: + - name: Set up commit signing as Cloudsmith Bot + shell: bash + env: + CLOUDSMITH_BOT_SSH_KEY: ${{ inputs.ssh-signing-key }} + run: | + set -euo pipefail + SIGNING_KEY="${RUNNER_TEMP}/cloudsmith-bot-signing-key" + printf '%s\n' "${CLOUDSMITH_BOT_SSH_KEY}" > "${SIGNING_KEY}" + chmod 600 "${SIGNING_KEY}" + ssh-keygen -y -f "${SIGNING_KEY}" > "${SIGNING_KEY}.pub" + git config --global user.name "Cloudsmith Bot" + git config --global user.email "vftbot@cloudsmith.io" + git config --global gpg.format ssh + git config --global user.signingkey "${SIGNING_KEY}.pub" + git config --global commit.gpgsign true + + - name: Open the bump pull request + shell: bash + env: + GH_TOKEN: ${{ inputs.token }} + VERSION: ${{ inputs.version }} + BUMP_COMMAND: ${{ inputs.bump-command }} + MERGE: ${{ inputs.merge }} + run: | + set -euo pipefail + gh auth setup-git + OPTIONS=() + if [ "${MERGE}" = "true" ]; then + OPTIONS+=(--merge) + fi + "${GITHUB_ACTION_PATH}/open-cli-bump-pr.sh" "${OPTIONS[@]}" "${GITHUB_REPOSITORY}" "${VERSION}" "${BUMP_COMMAND}" diff --git a/.github/actions/open-cli-bump-pr/open-cli-bump-pr.sh b/.github/actions/open-cli-bump-pr/open-cli-bump-pr.sh new file mode 100755 index 00000000..46a2e6ce --- /dev/null +++ b/.github/actions/open-cli-bump-pr/open-cli-bump-pr.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# Opens or updates a pull request that moves a downstream repository to a +# Cloudsmith CLI release. +# The script clones REPOSITORY and runs COMMAND in the clone, with VERSION in +# the environment. It commits the changes to the release/cloudsmith-cli branch +# and opens a pull request against the default branch. A newer release +# replaces a bump that is not merged yet, but an older release never replaces +# a newer one. +# Usage: open-cli-bump-pr.sh [--merge] [--dry-run] REPOSITORY VERSION COMMAND +# --merge Wait for the checks to pass, then squash-merge as an admin. +# --dry-run Show the commit, but do not push it. +set -euo pipefail + +usage() { + echo "usage: $0 [--merge] [--dry-run] OWNER/NAME MAJOR.MINOR.PATCH COMMAND" >&2 + exit 1 +} + +merge=false +dry_run=false +while [[ "${1:-}" == --* ]]; do + case "$1" in + --merge) merge=true ;; + --dry-run) dry_run=true ;; + *) usage ;; + esac + shift +done +[[ $# -eq 3 ]] || usage +repository="$1" +version="$2" +update_command="$3" +[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || usage + +branch="release/cloudsmith-cli" +title="Bump cloudsmith-cli to v$version" +body="Automated update to Cloudsmith CLI v$version." +if [[ "$merge" == true ]]; then + body="$body The workflow merges it when the checks pass." +fi +clone="$(mktemp -d)" +trap 'rm -rf "$clone"' EXIT + +pending_bump_is_newer() { + local pending_version newest_version + git -C "$clone" fetch --quiet --depth 1 origin "$branch" + pending_version="$(git -C "$clone" log -1 --format=%s FETCH_HEAD | sed -n 's/^Bump cloudsmith-cli to v//p')" + newest_version="$(printf '%s\n' "$pending_version" "$version" | sort -V | tail -n 1)" + [[ "$pending_version" != "$version" && "$pending_version" == "$newest_version" ]] +} + +commit_update() { + git -C "$clone" checkout --quiet -b "$branch" + git -C "$clone" add --all + git -C "$clone" commit --quiet -m "$title" +} + +merge_when_checks_pass() { + # gh pr checks fails when no checks are registered yet. + sleep 60 + gh pr checks "$branch" --repo "$repository" --watch --fail-fast + gh pr merge "$branch" --repo "$repository" --admin --squash \ + --match-head-commit "$(git -C "$clone" rev-parse HEAD)" +} + +for attempt in 1 2 3; do + find "$clone" -mindepth 1 -delete + git clone --quiet --depth 1 "https://github.com/$repository.git" "$clone" + pr_number="$(gh pr list --repo "$repository" --head "$branch" --state open --json number --jq '.[].number')" + if [[ -n "$pr_number" ]] && pending_bump_is_newer; then + echo "pull request #$pr_number already moves $repository to a newer release; skipping" + exit 0 + fi + (cd "$clone" && VERSION="$version" bash -c "$update_command") + if [[ -z "$(git -C "$clone" status --porcelain)" ]]; then + echo "$repository already uses cloudsmith-cli v$version; skipping" + exit 0 + fi + commit_update + if [[ "$dry_run" == true ]]; then + git -C "$clone" show + exit 0 + fi + git -C "$clone" push --quiet --force origin "$branch" + if [[ -z "$pr_number" ]]; then + gh pr create --repo "$repository" --head "$branch" --title "$title" --body "$body" + elif [[ "$(gh pr view "$pr_number" --repo "$repository" --json state --jq .state)" == OPEN ]]; then + gh pr edit "$pr_number" --repo "$repository" --title "$title" --body "$body" + else + echo "pull request #$pr_number closed during attempt $attempt; starting again from the default branch" + continue + fi + if [[ "$merge" == true ]]; then + merge_when_checks_pass + fi + exit 0 +done +echo "pull request for $branch kept closing during the update" >&2 +exit 1 diff --git a/.github/actions/open-cli-bump-pr/test-open-cli-bump-pr.sh b/.github/actions/open-cli-bump-pr/test-open-cli-bump-pr.sh new file mode 100755 index 00000000..23622c85 --- /dev/null +++ b/.github/actions/open-cli-bump-pr/test-open-cli-bump-pr.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +# Tests open-cli-bump-pr.sh against a local bare repository and a stub gh. +set -euo pipefail + +script="$(cd "$(dirname "$0")" && pwd)/open-cli-bump-pr.sh" +test_root="$(mktemp -d)" +trap 'rm -rf "$test_root"' EXIT +remote="$test_root/remote/acme/tool.git" +branch="release/cloudsmith-cli" +bump="printf '%s\\n' \"\$VERSION\" > version.txt" + +mkdir -p "$test_root/bin" +cat > "$test_root/bin/gh" <<'EOF' +#!/usr/bin/env bash +echo "gh $*" >> "$STUB_DIR/gh.log" +case "$1 $2" in + "pr list") cat "$STUB_DIR/pr-number" 2>/dev/null || true ;; + "pr view") + if [[ -f "$STUB_DIR/pr-closed" ]]; then + echo CLOSED + rm "$STUB_DIR/pr-number" "$STUB_DIR/pr-closed" + else + echo OPEN + fi ;; +esac +EOF +printf '#!/bin/sh\n' > "$test_root/bin/sleep" +chmod +x "$test_root/bin/gh" "$test_root/bin/sleep" +printf '[user]\n\tname = Test\n\temail = test@example.com\n' > "$test_root/gitconfig" +export PATH="$test_root/bin:$PATH" STUB_DIR="$test_root/stub" +export GIT_CONFIG_GLOBAL="$test_root/gitconfig" GIT_CONFIG_NOSYSTEM=1 +export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0="url.file://$test_root/remote/.insteadOf" GIT_CONFIG_VALUE_0="https://github.com/" + +reset_remote() { + rm -rf "$test_root/remote" "$test_root/seed" "$STUB_DIR" + mkdir -p "$STUB_DIR" + git init --quiet --bare --initial-branch main "$remote" + git init --quiet --initial-branch main "$test_root/seed" + echo 1.0.0 > "$test_root/seed/version.txt" + git -C "$test_root/seed" add version.txt + git -C "$test_root/seed" commit --quiet -m init + git -C "$test_root/seed" push --quiet "$remote" main +} + +open_pending_pr() { + git -C "$test_root/seed" checkout --quiet -b "$branch" + echo "$1" > "$test_root/seed/version.txt" + git -C "$test_root/seed" commit --quiet -am "Bump cloudsmith-cli to v$1" + git -C "$test_root/seed" push --quiet "$remote" "$branch" + echo 7 > "$STUB_DIR/pr-number" +} + +pushed_version() { + git -C "$remote" show "$branch:version.txt" +} + +assert_nothing_pushed() { + if git -C "$remote" rev-parse --quiet --verify "$branch" > /dev/null; then exit 1; fi +} + +# A new release opens a pull request with one bump commit. +reset_remote +"$script" acme/tool 1.1.0 "$bump" +test "$(pushed_version)" = 1.1.0 +test "$(git -C "$remote" log -1 --format=%s "$branch")" = "Bump cloudsmith-cli to v1.1.0" +grep -q "^gh pr create --repo acme/tool --head $branch" "$STUB_DIR/gh.log" + +# A repository that already uses the release changes nothing. +reset_remote +"$script" acme/tool 1.0.0 "$bump" +assert_nothing_pushed + +# An older release keeps a newer pending bump. +reset_remote +open_pending_pr 1.2.0 +"$script" acme/tool 1.1.0 "$bump" +test "$(pushed_version)" = 1.2.0 +if grep -q "^gh pr edit" "$STUB_DIR/gh.log"; then exit 1; fi + +# A newer release replaces an older pending bump. +reset_remote +open_pending_pr 1.2.0 +"$script" acme/tool 1.3.0 "$bump" +test "$(pushed_version)" = 1.3.0 +grep -q "^gh pr edit 7" "$STUB_DIR/gh.log" + +# A pull request that closes during the update makes the update start again. +reset_remote +open_pending_pr 1.2.0 +touch "$STUB_DIR/pr-closed" +"$script" acme/tool 1.3.0 "$bump" +test "$(pushed_version)" = 1.3.0 +grep -q "^gh pr create" "$STUB_DIR/gh.log" + +# --merge merges the pushed commit when the checks pass. +reset_remote +"$script" --merge acme/tool 1.1.0 "$bump" +grep -q "^gh pr checks $branch --repo acme/tool --watch --fail-fast" "$STUB_DIR/gh.log" +grep -q "^gh pr merge $branch --repo acme/tool --admin --squash --match-head-commit $(git -C "$remote" rev-parse "$branch")" "$STUB_DIR/gh.log" + +# --dry-run shows the commit and pushes nothing. +reset_remote +"$script" --dry-run acme/tool 1.1.0 "$bump" | grep -q '^+1.1.0' +assert_nothing_pushed + +# A failed bump command fails the script and pushes nothing. +reset_remote +if "$script" acme/tool 1.1.0 'exit 3'; then exit 1; fi +assert_nothing_pushed + +# A version that is not MAJOR.MINOR.PATCH is rejected. +if "$script" acme/tool 1.1 "$bump" 2> /dev/null; then exit 1; fi + +echo "all open-cli-bump-pr.sh tests passed" diff --git a/.github/actions/setup-cloudsmith-bot-git/action.yml b/.github/actions/setup-cloudsmith-bot-git/action.yml deleted file mode 100644 index 4ee926b0..00000000 --- a/.github/actions/setup-cloudsmith-bot-git/action.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Set up Cloudsmith Bot git signing -description: >- - Configure git to commit as Cloudsmith Bot and to sign each commit with the - SSH key of the bot. - -inputs: - ssh-signing-key: - description: Private SSH key that signs the commits of Cloudsmith Bot. - required: true - -runs: - using: composite - steps: - - shell: bash - env: - CLOUDSMITH_BOT_SSH_KEY: ${{ inputs.ssh-signing-key }} - run: | - set -euo pipefail - SIGNING_KEY="${RUNNER_TEMP}/cloudsmith-bot-signing-key" - printf '%s\n' "${CLOUDSMITH_BOT_SSH_KEY}" > "${SIGNING_KEY}" - chmod 600 "${SIGNING_KEY}" - ssh-keygen -y -f "${SIGNING_KEY}" > "${SIGNING_KEY}.pub" - git config --global user.name "Cloudsmith Bot" - git config --global user.email "vftbot@cloudsmith.io" - git config --global gpg.format ssh - git config --global user.signingkey "${SIGNING_KEY}.pub" - git config --global commit.gpgsign true diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 5e918db0..cf2de90f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -35,3 +35,5 @@ jobs: - run: uv sync --locked --group dev - run: uv run pre-commit run --all-files + + - run: .github/actions/open-cli-bump-pr/test-open-cli-bump-pr.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2f4d006a..7169fdda 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -532,225 +532,37 @@ jobs: "${TAGS[@]}" \ --push . - publish-homebrew: - # Renders packaging/homebrew/cloudsmith-cli.rb.tmpl against the archives - # that publish-cloudsmith just made live and opens a bump pull request - # against the Homebrew tap (/homebrew-cloudsmith-cli). When the tap - # checks pass, the job squash-merges the pull request with --admin. - # Without DOWNSTREAM_RELEASE_PUSH_TOKEN the rendered formula is only staged as a - # workflow artifact. - needs: [validate, publish-cloudsmith] - runs-on: ubuntu-24.04 - timeout-minutes: 30 - permissions: - contents: read - env: - CLOUDSMITH_NAMESPACE: ${{ vars.CLOUDSMITH_NAMESPACE }} - CLOUDSMITH_REPO: ${{ vars.CLOUDSMITH_REPO }} - HAS_DOWNSTREAM_TOKEN: ${{ secrets.DOWNSTREAM_RELEASE_PUSH_TOKEN != '' }} - VERSION: ${{ needs.validate.outputs.version }} - # Targets shipped via Homebrew (subset of BINARY_TARGETS; Homebrew on - # Linux is glibc-based, so the gnu builds). - HOMEBREW_TARGETS: "macos-arm64 macos-x86_64 linux-x86_64-gnu linux-aarch64-gnu" - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 - with: - pattern: cloudsmith-macos-* - merge-multiple: true - path: binaries - - - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 - with: - pattern: cloudsmith-linux-*-gnu - merge-multiple: true - path: binaries - - - name: Render the formula from the template - run: | - set -euo pipefail - FORMULA=rendered/cloudsmith-cli.rb - mkdir -p rendered - cp packaging/homebrew/cloudsmith-cli.rb.tmpl "${FORMULA}" - sed -i \ - -e "s/__VERSION__/${VERSION}/g" \ - -e "s/__NAMESPACE__/${CLOUDSMITH_NAMESPACE}/g" \ - -e "s/__REPO__/${CLOUDSMITH_REPO}/g" \ - "${FORMULA}" - for TARGET in ${HOMEBREW_TARGETS}; do - SHA_FILE="binaries/cloudsmith-${VERSION}-${TARGET}.tar.gz.sha256" - test -f "${SHA_FILE}" - SHA256="$(awk '{print $1}' "${SHA_FILE}")" - KEY="SHA256_$(printf '%s' "${TARGET}" | tr 'a-z-' 'A-Z_')" - sed -i "s/__${KEY}__/${SHA256}/g" "${FORMULA}" - done - if grep -n '__' "${FORMULA}"; then - echo "FAIL: unrendered placeholders remain" - exit 1 - fi - ruby -c "${FORMULA}" - - - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: homebrew-formula - path: rendered/cloudsmith-cli.rb - retention-days: 14 - if-no-files-found: error - - - name: Verify the published downloads match the formula checksums - run: | - set -euo pipefail - for TARGET in ${HOMEBREW_TARGETS}; do - URL="https://dl.cloudsmith.io/public/${CLOUDSMITH_NAMESPACE}/${CLOUDSMITH_REPO}/raw/names/cloudsmith-cli-${TARGET}/versions/${VERSION}/cloudsmith-${VERSION}-${TARGET}.tar.gz" - EXPECTED="$(awk '{print $1}' "binaries/cloudsmith-${VERSION}-${TARGET}.tar.gz.sha256")" - ACTUAL="" - # Retry to absorb post-publish sync delay. - for ATTEMPT in 1 2 3 4 5 6 7 8 9 10; do - if ACTUAL="$(curl -fsSL "${URL}" | sha256sum | awk '{print $1}')"; then - break - fi - echo "attempt ${ATTEMPT}: ${URL} not ready; retrying" - sleep 30 - done - test "${ACTUAL}" = "${EXPECTED}" - echo "verified ${URL}" - done - - - name: Set up commit signing as Cloudsmith Bot - if: env.HAS_DOWNSTREAM_TOKEN == 'true' - uses: ./.github/actions/setup-cloudsmith-bot-git - with: - ssh-signing-key: ${{ secrets.CLOUDSMITH_BOT_SSH_KEY }} - - - name: Open and merge a bump pull request against the tap repository - if: env.HAS_DOWNSTREAM_TOKEN == 'true' - env: - GH_TOKEN: ${{ secrets.DOWNSTREAM_RELEASE_PUSH_TOKEN }} - TAP_REPO: ${{ github.repository_owner }}/homebrew-cloudsmith-cli - run: | - set -euo pipefail - BRANCH="release/cloudsmith-cli-v${VERSION}" - gh auth setup-git - git clone --depth 1 "https://github.com/${TAP_REPO}.git" tap - mkdir -p tap/Formula tap/Aliases - cp rendered/cloudsmith-cli.rb tap/Formula/cloudsmith-cli.rb - ln -sfn ../Formula/cloudsmith-cli.rb tap/Aliases/cloudsmith - if [ -z "$(git -C tap status --porcelain)" ]; then - echo "tap already up to date; skipping" - exit 0 - fi - git -C tap checkout -b "${BRANCH}" - git -C tap add Formula/cloudsmith-cli.rb Aliases/cloudsmith - git -C tap commit -m "Bump cloudsmith-cli to v${VERSION}" - git -C tap push --force origin "${BRANCH}" - if [ -z "$(gh pr list --repo "${TAP_REPO}" --head "${BRANCH}" --state open --json number --jq '.[].number')" ]; then - gh pr create --repo "${TAP_REPO}" \ - --head "${BRANCH}" --base main \ - --title "Bump cloudsmith-cli to v${VERSION}" \ - --body "Automated formula update for cloudsmith-cli v${VERSION}. The release workflow verified the formula checksums against the published downloads. The workflow merges it when the checks pass." - else - echo "pull request for ${BRANCH} already open" - fi - # The tap checks start when the pull request opens. Give them time to - # register, because gh pr checks fails when it finds no checks. - sleep 60 - gh pr checks "${BRANCH}" --repo "${TAP_REPO}" --watch --fail-fast - gh pr merge "${BRANCH}" --repo "${TAP_REPO}" --admin --squash \ - --match-head-commit "$(git -C tap rev-parse HEAD)" - - - name: Report a skipped tap update - if: env.HAS_DOWNSTREAM_TOKEN != 'true' - run: echo "DOWNSTREAM_RELEASE_PUSH_TOKEN not configured; formula staged as a workflow artifact only." - - publish-action: - # Opens a pull request against /cloudsmith-cli-action that pins the - # default cli-version of the action to this release. The bump script in - # the action repository also adds a release to its CHANGELOG.md. - # The action repository requires reviewed pull requests. When the pull - # request merges, the action repository tests, tags and releases it. - # Each run rebuilds one branch from master, so a newer CLI release - # replaces a bump that is not merged yet. The bump never repeats an - # action version. It never pins a CLI older than master or the open pull - # request already pins. When the pull request closes during the update, - # the update starts again from master. + notify-downstream-repos: + # Sends a cloudsmith-cli-release event with the version to each repository + # in the matrix. The workflow in each repository opens a bump pull request. needs: [validate, publish-github] runs-on: ubuntu-24.04 - timeout-minutes: 10 - concurrency: - group: publish-action - cancel-in-progress: false - queue: max + timeout-minutes: 5 permissions: contents: read + strategy: + fail-fast: false + matrix: + repository: + - homebrew-cloudsmith-cli + - cloudsmith-cli-action env: HAS_DOWNSTREAM_TOKEN: ${{ secrets.DOWNSTREAM_RELEASE_PUSH_TOKEN != '' }} - VERSION: ${{ needs.validate.outputs.version }} steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Set up commit signing as Cloudsmith Bot - if: env.HAS_DOWNSTREAM_TOKEN == 'true' - uses: ./.github/actions/setup-cloudsmith-bot-git - with: - ssh-signing-key: ${{ secrets.CLOUDSMITH_BOT_SSH_KEY }} - - - name: Open a CLI bump pull request against the action repository + - name: Send the release event if: env.HAS_DOWNSTREAM_TOKEN == 'true' env: GH_TOKEN: ${{ secrets.DOWNSTREAM_RELEASE_PUSH_TOKEN }} - ACTION_REPO: ${{ github.repository_owner }}/cloudsmith-cli-action + REPOSITORY: ${{ github.repository_owner }}/${{ matrix.repository }} + VERSION: ${{ needs.validate.outputs.version }} run: | - set -euo pipefail - BRANCH="release/cloudsmith-cli" - TITLE="Bump cloudsmith-cli to v${VERSION}" - gh auth setup-git - for attempt in 1 2 3; do - rm -rf action - git clone --depth 1 "https://github.com/${ACTION_REPO}.git" action - PR_NUMBER="$(gh pr list --repo "${ACTION_REPO}" --head "${BRANCH}" --state open --json number --jq '.[].number')" - if [ -n "${PR_NUMBER}" ]; then - git -C action fetch --quiet --depth 1 origin "${BRANCH}" - PENDING_VERSION="$(git -C action show FETCH_HEAD:action.yml | yq '.inputs["cli-version"].default')" - NEWEST_VERSION="$(printf '%s\n' "${PENDING_VERSION}" "${VERSION}" | sort -V | tail -n 1)" - if [ "${PENDING_VERSION}" != "${VERSION}" ] && [ "${NEWEST_VERSION}" = "${PENDING_VERSION}" ]; then - echo "pull request #${PR_NUMBER} already pins newer cloudsmith-cli v${PENDING_VERSION}; skipping" - exit 0 - fi - fi - ACTION_VERSION="$(cd action && bash scripts/bump-cli-version.sh "${VERSION}")" - if [ -z "${ACTION_VERSION}" ]; then - echo "action already pins cloudsmith-cli v${VERSION}; skipping" - exit 0 - fi - git -C action checkout -b "${BRANCH}" - git -C action add action.yml CHANGELOG.md - git -C action commit -m "${TITLE}" - git -C action push --force origin "${BRANCH}" - BODY="Automated update that pins the default cli-version to cloudsmith-cli v${VERSION}. When this pull request merges and the tests pass, the action releases v${ACTION_VERSION} and moves its major tag. Merge with a squash merge." - if [ -z "${PR_NUMBER}" ]; then - gh pr create --repo "${ACTION_REPO}" \ - --head "${BRANCH}" --base master \ - --title "${TITLE}" --body "${BODY}" - exit 0 - fi - if [ "$(gh pr view "${PR_NUMBER}" --repo "${ACTION_REPO}" --json state --jq .state)" = OPEN ]; then - gh pr edit "${PR_NUMBER}" --repo "${ACTION_REPO}" \ - --title "${TITLE}" --body "${BODY}" - exit 0 - fi - echo "pull request #${PR_NUMBER} closed during attempt ${attempt}; starting again from master" - done - echo "pull request for ${BRANCH} kept closing during the update" >&2 - exit 1 + gh api "repos/${REPOSITORY}/dispatches" \ + -f event_type=cloudsmith-cli-release \ + -f "client_payload[version]=${VERSION}" - - name: Report a skipped action update + - name: Report a skipped event if: env.HAS_DOWNSTREAM_TOKEN != 'true' - run: echo "DOWNSTREAM_RELEASE_PUSH_TOKEN not configured; action update skipped." + run: echo "DOWNSTREAM_RELEASE_PUSH_TOKEN not configured; release event not sent." publish-github: needs: diff --git a/packaging/homebrew/cloudsmith-cli.rb.tmpl b/packaging/homebrew/cloudsmith-cli.rb.tmpl deleted file mode 100644 index 66435157..00000000 --- a/packaging/homebrew/cloudsmith-cli.rb.tmpl +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2026 Cloudsmith Ltd -# -# Template for the Homebrew formula published to the Homebrew tap -# repository. Rendered by the release workflow (publish-homebrew job); -# do not edit the rendered copy in the tap by hand. -class CloudsmithCli < Formula - desc "Official Cloudsmith Command-Line Interface" - homepage "https://docs.cloudsmith.com/developer-tools/cli" - version "__VERSION__" - license "Apache-2.0" - - if OS.mac? && Hardware::CPU.arm? - url "https://dl.cloudsmith.io/public/__NAMESPACE__/__REPO__/raw/names/cloudsmith-cli-macos-arm64/versions/__VERSION__/cloudsmith-__VERSION__-macos-arm64.tar.gz" - sha256 "__SHA256_MACOS_ARM64__" - elsif OS.mac? && Hardware::CPU.intel? - url "https://dl.cloudsmith.io/public/__NAMESPACE__/__REPO__/raw/names/cloudsmith-cli-macos-x86_64/versions/__VERSION__/cloudsmith-__VERSION__-macos-x86_64.tar.gz" - sha256 "__SHA256_MACOS_X86_64__" - elsif OS.linux? && Hardware::CPU.arm? && Hardware::CPU.is_64_bit? - url "https://dl.cloudsmith.io/public/__NAMESPACE__/__REPO__/raw/names/cloudsmith-cli-linux-aarch64-gnu/versions/__VERSION__/cloudsmith-__VERSION__-linux-aarch64-gnu.tar.gz" - sha256 "__SHA256_LINUX_AARCH64_GNU__" - elsif OS.linux? && Hardware::CPU.intel? && Hardware::CPU.is_64_bit? - url "https://dl.cloudsmith.io/public/__NAMESPACE__/__REPO__/raw/names/cloudsmith-cli-linux-x86_64-gnu/versions/__VERSION__/cloudsmith-__VERSION__-linux-x86_64-gnu.tar.gz" - sha256 "__SHA256_LINUX_X86_64_GNU__" - end - - # Placed after the url stanzas: a leading livecheck url is misdetected as the - # stable url by audit when every real url is inside a conditional. - livecheck do - url "https://dl.cloudsmith.io/public/__NAMESPACE__/__REPO__/raw/names/cloudsmith-cli-manifest-macos-arm64/versions/latest/manifest.txt" - 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["*"] - bin.write_exec_script libexec/"cloudsmith" - end - - test do - assert_match "CLI Package Version: #{version}", shell_output("#{bin}/cloudsmith --version") - end -end From 8f9506d4a2895abb971efd387ae77771cec41705 Mon Sep 17 00:00:00 2001 From: Ian Duffy Date: Mon, 19 Oct 2026 15:51:55 +0100 Subject: [PATCH 7/7] refactor(ENG-14534): remove the extras from the downstream release event Remove the --dry-run option and the retry when the bump pull request closes during the update. Remove the test script of open-cli-bump-pr. Remove the check for DOWNSTREAM_RELEASE_PUSH_TOKEN, so that a release without the token fails instead of skipping the event. Co-Authored-By: Claude Opus 5.5 --- .../open-cli-bump-pr/open-cli-bump-pr.sh | 60 ++++----- .../open-cli-bump-pr/test-open-cli-bump-pr.sh | 114 ------------------ .github/workflows/lint.yml | 2 - .github/workflows/release.yml | 7 -- 4 files changed, 22 insertions(+), 161 deletions(-) delete mode 100755 .github/actions/open-cli-bump-pr/test-open-cli-bump-pr.sh diff --git a/.github/actions/open-cli-bump-pr/open-cli-bump-pr.sh b/.github/actions/open-cli-bump-pr/open-cli-bump-pr.sh index 46a2e6ce..844383dd 100755 --- a/.github/actions/open-cli-bump-pr/open-cli-bump-pr.sh +++ b/.github/actions/open-cli-bump-pr/open-cli-bump-pr.sh @@ -6,22 +6,19 @@ # and opens a pull request against the default branch. A newer release # replaces a bump that is not merged yet, but an older release never replaces # a newer one. -# Usage: open-cli-bump-pr.sh [--merge] [--dry-run] REPOSITORY VERSION COMMAND +# Usage: open-cli-bump-pr.sh [--merge] REPOSITORY VERSION COMMAND # --merge Wait for the checks to pass, then squash-merge as an admin. -# --dry-run Show the commit, but do not push it. set -euo pipefail usage() { - echo "usage: $0 [--merge] [--dry-run] OWNER/NAME MAJOR.MINOR.PATCH COMMAND" >&2 + echo "usage: $0 [--merge] OWNER/NAME MAJOR.MINOR.PATCH COMMAND" >&2 exit 1 } merge=false -dry_run=false while [[ "${1:-}" == --* ]]; do case "$1" in --merge) merge=true ;; - --dry-run) dry_run=true ;; *) usage ;; esac shift @@ -63,37 +60,24 @@ merge_when_checks_pass() { --match-head-commit "$(git -C "$clone" rev-parse HEAD)" } -for attempt in 1 2 3; do - find "$clone" -mindepth 1 -delete - git clone --quiet --depth 1 "https://github.com/$repository.git" "$clone" - pr_number="$(gh pr list --repo "$repository" --head "$branch" --state open --json number --jq '.[].number')" - if [[ -n "$pr_number" ]] && pending_bump_is_newer; then - echo "pull request #$pr_number already moves $repository to a newer release; skipping" - exit 0 - fi - (cd "$clone" && VERSION="$version" bash -c "$update_command") - if [[ -z "$(git -C "$clone" status --porcelain)" ]]; then - echo "$repository already uses cloudsmith-cli v$version; skipping" - exit 0 - fi - commit_update - if [[ "$dry_run" == true ]]; then - git -C "$clone" show - exit 0 - fi - git -C "$clone" push --quiet --force origin "$branch" - if [[ -z "$pr_number" ]]; then - gh pr create --repo "$repository" --head "$branch" --title "$title" --body "$body" - elif [[ "$(gh pr view "$pr_number" --repo "$repository" --json state --jq .state)" == OPEN ]]; then - gh pr edit "$pr_number" --repo "$repository" --title "$title" --body "$body" - else - echo "pull request #$pr_number closed during attempt $attempt; starting again from the default branch" - continue - fi - if [[ "$merge" == true ]]; then - merge_when_checks_pass - fi +git clone --quiet --depth 1 "https://github.com/$repository.git" "$clone" +pr_number="$(gh pr list --repo "$repository" --head "$branch" --state open --json number --jq '.[].number')" +if [[ -n "$pr_number" ]] && pending_bump_is_newer; then + echo "pull request #$pr_number already moves $repository to a newer release; skipping" exit 0 -done -echo "pull request for $branch kept closing during the update" >&2 -exit 1 +fi +(cd "$clone" && VERSION="$version" bash -c "$update_command") +if [[ -z "$(git -C "$clone" status --porcelain)" ]]; then + echo "$repository already uses cloudsmith-cli v$version; skipping" + exit 0 +fi +commit_update +git -C "$clone" push --quiet --force origin "$branch" +if [[ -z "$pr_number" ]]; then + gh pr create --repo "$repository" --head "$branch" --title "$title" --body "$body" +else + gh pr edit "$pr_number" --repo "$repository" --title "$title" --body "$body" +fi +if [[ "$merge" == true ]]; then + merge_when_checks_pass +fi diff --git a/.github/actions/open-cli-bump-pr/test-open-cli-bump-pr.sh b/.github/actions/open-cli-bump-pr/test-open-cli-bump-pr.sh deleted file mode 100755 index 23622c85..00000000 --- a/.github/actions/open-cli-bump-pr/test-open-cli-bump-pr.sh +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env bash -# Tests open-cli-bump-pr.sh against a local bare repository and a stub gh. -set -euo pipefail - -script="$(cd "$(dirname "$0")" && pwd)/open-cli-bump-pr.sh" -test_root="$(mktemp -d)" -trap 'rm -rf "$test_root"' EXIT -remote="$test_root/remote/acme/tool.git" -branch="release/cloudsmith-cli" -bump="printf '%s\\n' \"\$VERSION\" > version.txt" - -mkdir -p "$test_root/bin" -cat > "$test_root/bin/gh" <<'EOF' -#!/usr/bin/env bash -echo "gh $*" >> "$STUB_DIR/gh.log" -case "$1 $2" in - "pr list") cat "$STUB_DIR/pr-number" 2>/dev/null || true ;; - "pr view") - if [[ -f "$STUB_DIR/pr-closed" ]]; then - echo CLOSED - rm "$STUB_DIR/pr-number" "$STUB_DIR/pr-closed" - else - echo OPEN - fi ;; -esac -EOF -printf '#!/bin/sh\n' > "$test_root/bin/sleep" -chmod +x "$test_root/bin/gh" "$test_root/bin/sleep" -printf '[user]\n\tname = Test\n\temail = test@example.com\n' > "$test_root/gitconfig" -export PATH="$test_root/bin:$PATH" STUB_DIR="$test_root/stub" -export GIT_CONFIG_GLOBAL="$test_root/gitconfig" GIT_CONFIG_NOSYSTEM=1 -export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0="url.file://$test_root/remote/.insteadOf" GIT_CONFIG_VALUE_0="https://github.com/" - -reset_remote() { - rm -rf "$test_root/remote" "$test_root/seed" "$STUB_DIR" - mkdir -p "$STUB_DIR" - git init --quiet --bare --initial-branch main "$remote" - git init --quiet --initial-branch main "$test_root/seed" - echo 1.0.0 > "$test_root/seed/version.txt" - git -C "$test_root/seed" add version.txt - git -C "$test_root/seed" commit --quiet -m init - git -C "$test_root/seed" push --quiet "$remote" main -} - -open_pending_pr() { - git -C "$test_root/seed" checkout --quiet -b "$branch" - echo "$1" > "$test_root/seed/version.txt" - git -C "$test_root/seed" commit --quiet -am "Bump cloudsmith-cli to v$1" - git -C "$test_root/seed" push --quiet "$remote" "$branch" - echo 7 > "$STUB_DIR/pr-number" -} - -pushed_version() { - git -C "$remote" show "$branch:version.txt" -} - -assert_nothing_pushed() { - if git -C "$remote" rev-parse --quiet --verify "$branch" > /dev/null; then exit 1; fi -} - -# A new release opens a pull request with one bump commit. -reset_remote -"$script" acme/tool 1.1.0 "$bump" -test "$(pushed_version)" = 1.1.0 -test "$(git -C "$remote" log -1 --format=%s "$branch")" = "Bump cloudsmith-cli to v1.1.0" -grep -q "^gh pr create --repo acme/tool --head $branch" "$STUB_DIR/gh.log" - -# A repository that already uses the release changes nothing. -reset_remote -"$script" acme/tool 1.0.0 "$bump" -assert_nothing_pushed - -# An older release keeps a newer pending bump. -reset_remote -open_pending_pr 1.2.0 -"$script" acme/tool 1.1.0 "$bump" -test "$(pushed_version)" = 1.2.0 -if grep -q "^gh pr edit" "$STUB_DIR/gh.log"; then exit 1; fi - -# A newer release replaces an older pending bump. -reset_remote -open_pending_pr 1.2.0 -"$script" acme/tool 1.3.0 "$bump" -test "$(pushed_version)" = 1.3.0 -grep -q "^gh pr edit 7" "$STUB_DIR/gh.log" - -# A pull request that closes during the update makes the update start again. -reset_remote -open_pending_pr 1.2.0 -touch "$STUB_DIR/pr-closed" -"$script" acme/tool 1.3.0 "$bump" -test "$(pushed_version)" = 1.3.0 -grep -q "^gh pr create" "$STUB_DIR/gh.log" - -# --merge merges the pushed commit when the checks pass. -reset_remote -"$script" --merge acme/tool 1.1.0 "$bump" -grep -q "^gh pr checks $branch --repo acme/tool --watch --fail-fast" "$STUB_DIR/gh.log" -grep -q "^gh pr merge $branch --repo acme/tool --admin --squash --match-head-commit $(git -C "$remote" rev-parse "$branch")" "$STUB_DIR/gh.log" - -# --dry-run shows the commit and pushes nothing. -reset_remote -"$script" --dry-run acme/tool 1.1.0 "$bump" | grep -q '^+1.1.0' -assert_nothing_pushed - -# A failed bump command fails the script and pushes nothing. -reset_remote -if "$script" acme/tool 1.1.0 'exit 3'; then exit 1; fi -assert_nothing_pushed - -# A version that is not MAJOR.MINOR.PATCH is rejected. -if "$script" acme/tool 1.1 "$bump" 2> /dev/null; then exit 1; fi - -echo "all open-cli-bump-pr.sh tests passed" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index cf2de90f..5e918db0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -35,5 +35,3 @@ jobs: - run: uv sync --locked --group dev - run: uv run pre-commit run --all-files - - - run: .github/actions/open-cli-bump-pr/test-open-cli-bump-pr.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7169fdda..6f757529 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -546,11 +546,8 @@ jobs: repository: - homebrew-cloudsmith-cli - cloudsmith-cli-action - env: - HAS_DOWNSTREAM_TOKEN: ${{ secrets.DOWNSTREAM_RELEASE_PUSH_TOKEN != '' }} steps: - name: Send the release event - if: env.HAS_DOWNSTREAM_TOKEN == 'true' env: GH_TOKEN: ${{ secrets.DOWNSTREAM_RELEASE_PUSH_TOKEN }} REPOSITORY: ${{ github.repository_owner }}/${{ matrix.repository }} @@ -560,10 +557,6 @@ jobs: -f event_type=cloudsmith-cli-release \ -f "client_payload[version]=${VERSION}" - - name: Report a skipped event - if: env.HAS_DOWNSTREAM_TOKEN != 'true' - run: echo "DOWNSTREAM_RELEASE_PUSH_TOKEN not configured; release event not sent." - publish-github: needs: - validate