From 8e82b9c312d2e41d9091573dc2a0c3fc1510149b Mon Sep 17 00:00:00 2001 From: Ian Duffy Date: Thu, 1 Oct 2026 22:48:26 +0100 Subject: [PATCH 1/2] feat(ENG-14534): pin the CLI version and release on each CLI bump Pin the default cli-version to Cloudsmith CLI 1.27.0, so that a workflow that pins the action to a SHA always installs the same CLI. An explicit cli-version, including latest, still overrides the pin. Add scripts/bump-cli-version.sh. The cloudsmith-cli release workflow runs it as cloudsmith-bot to pin each new CLI version and add a CHANGELOG release section. The Release workflow now runs when CHANGELOG.md changes on master. If the newest CHANGELOG version has no tag, it runs the test matrix, then creates the tag and release and moves the major tag. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 61 +++++++++++++++++++--- .github/workflows/test.yml | 51 +++++++++++++++++++ CHANGELOG.md | 7 +++ CONTRIBUTION.md | 18 +++++++ README.md | 4 +- action.yml | 4 +- scripts/bump-cli-version.sh | 60 ++++++++++++++++++++++ scripts/test-bump-cli-version.sh | 87 ++++++++++++++++++++++++++++++++ 8 files changed, 282 insertions(+), 10 deletions(-) create mode 100644 scripts/bump-cli-version.sh create mode 100644 scripts/test-bump-cli-version.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 02e5972..a95bbe9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,19 +1,68 @@ name: Release +# Releases the newest version in CHANGELOG.md when no tag exists for it yet. +# The test matrix must pass before the tag and the GitHub release are created. on: push: - tags: - - "v*.*.*" + branches: + - master + paths: + - CHANGELOG.md + workflow_dispatch: {} permissions: {} concurrency: - group: release-${{ github.workflow }}-${{ github.ref }} + group: release-${{ github.workflow }} cancel-in-progress: false jobs: + version: + name: Find the version to release + if: github.ref == 'refs/heads/master' + permissions: + contents: read + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + tag: ${{ steps.version.outputs.tag }} + released: ${{ steps.version.outputs.released }} + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Read the newest CHANGELOG version + id: version + run: | + set -euo pipefail + version="$(sed -n 's/^## \[\([0-9]*\.[0-9]*\.[0-9]*\)\].*/\1/p' CHANGELOG.md | head -n 1)" + test -n "$version" + tag="v$version" + echo "tag=$tag" >> "$GITHUB_OUTPUT" + status=0 + git ls-remote --exit-code --tags origin "refs/tags/$tag" > /dev/null || status=$? + case "$status" in + 0) echo "$tag is already released"; echo "released=true" >> "$GITHUB_OUTPUT" ;; + 2) echo "$tag is not released yet"; echo "released=false" >> "$GITHUB_OUTPUT" ;; + *) exit "$status" ;; + esac + + test: + name: Test + needs: version + if: needs.version.outputs.released == 'false' + permissions: + contents: read + id-token: write # required whenever OIDC inputs are set + uses: $/.github/workflows/test.yml + secrets: + CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }} + release: name: Create release and move major tag + needs: [version, test] permissions: contents: write # create the release and move the major tag runs-on: ubuntu-latest @@ -27,13 +76,13 @@ jobs: - name: Create GitHub release env: GH_TOKEN: ${{ github.token }} - TAG: ${{ github.ref_name }} - run: gh release create "$TAG" --title "Release $TAG" --verify-tag + TAG: ${{ needs.version.outputs.tag }} + run: gh release create "$TAG" --title "Release $TAG" --target "$GITHUB_SHA" - name: Update floating major tag env: GH_TOKEN: ${{ github.token }} - TAG: ${{ github.ref_name }} + TAG: ${{ needs.version.outputs.tag }} run: | major="${TAG%%.*}" echo "Pointing $major at $TAG ($GITHUB_SHA)" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cb9067e..e5c019b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,6 +3,10 @@ name: Test on: push: workflow_dispatch: {} + workflow_call: + secrets: + CLOUDSMITH_API_KEY: + required: false permissions: contents: read @@ -26,6 +30,8 @@ jobs: run: | bash -n scripts/setup.sh bash -n scripts/test-setup.sh + bash -n scripts/bump-cli-version.sh + bash -n scripts/test-bump-cli-version.sh sh -n installer/install.sh - name: Install ShellCheck @@ -35,6 +41,8 @@ jobs: run: | shellcheck --severity=style scripts/setup.sh shellcheck --severity=style scripts/test-setup.sh + shellcheck --severity=style scripts/bump-cli-version.sh + shellcheck --severity=style scripts/test-bump-cli-version.sh shellcheck --shell=sh --severity=style installer/install.sh lint-powershell: @@ -94,6 +102,11 @@ jobs: shell: bash run: bash scripts/test-setup.sh + - name: Test CLI version bump + if: runner.os != 'Windows' + shell: bash + run: bash scripts/test-bump-cli-version.sh + - name: Test PowerShell setup if: runner.os == 'Windows' shell: pwsh @@ -163,6 +176,18 @@ jobs: test -n "$OUTPUT_BIN_DIRECTORY" cloudsmith --version | tr -d '\r' | grep -Fx "CLI Package Version: $OUTPUT_CLI_VERSION" + - name: Check the pinned CLI version + shell: bash + env: + OUTPUT_CLI_VERSION: ${{ steps.setup.outputs.cli-version }} + run: | + set -euo pipefail + pinned_version="$(tr -d '\r' < action.yml | awk ' + /^ cli-version:/ { in_cli_version = 1 } + in_cli_version && /^ default:/ { gsub(/"/, "", $2); print $2; exit } + ')" + test "$OUTPUT_CLI_VERSION" = "$pinned_version" + - name: Check exported environment shell: bash run: | @@ -212,6 +237,32 @@ jobs: test "$CLOUDSMITH_OIDC_AUDIENCE" = "test-audience" cloudsmith --version | tr -d '\r' | grep -Fx "CLI Package Version: 1.20.1" + test-latest-version: + name: Install latest version + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Install Cloudsmith CLI + id: setup + uses: $/ + with: + cli-version: latest + api-key: placeholder-key + + - name: Check installation + shell: bash + env: + OUTPUT_CLI_VERSION: ${{ steps.setup.outputs.cli-version }} + run: | + set -euo pipefail + test -n "$OUTPUT_CLI_VERSION" + cloudsmith --version | tr -d '\r' | grep -Fx "CLI Package Version: $OUTPUT_CLI_VERSION" + test-input-validation: name: Input validation runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index e6e08bf..0c6a462 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] --- +## [3.2.0] - 2026-10-01 +--- +### Changed + +- `cli-version` now defaults to a pinned Cloudsmith CLI version (1.27.0) instead of `latest`. A workflow that pins this action to a commit SHA now installs the same CLI version on every run. Set `cli-version: latest` to keep the previous floating behavior. +- Each Cloudsmith CLI release now opens a pull request that pins the new CLI version. When the pull request merges, the action publishes a patch release, or a minor release if unreleased changes are pending, and moves the `v3` tag. + ## [3.1.1] - 2026-09-10 --- ### Fixed diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md index 2aa13b7..597c13b 100644 --- a/CONTRIBUTION.md +++ b/CONTRIBUTION.md @@ -22,11 +22,29 @@ Thank you for considering contributing to the Cloudsmith CLI Setup action! shellcheck --severity=style scripts/setup.sh pwsh -Command "Invoke-ScriptAnalyzer -Path scripts/setup.ps1 -Settings PSGallery -EnableExit" bash scripts/test-setup.sh + bash scripts/test-bump-cli-version.sh pwsh -NoProfile -File scripts/test-setup.ps1 ``` 4. Commit, push to your fork, and open a pull request. +## Releasing + +The `Release` workflow runs when `CHANGELOG.md` changes on `master`. If the +newest version in `CHANGELOG.md` has no tag, the workflow runs the test +matrix. When the tests pass, it creates the tag and the GitHub release. It +also moves the floating major tag, for example `v3`, to the new release. + +To release action changes, add a version section to `CHANGELOG.md` in a pull +request. Use a minor or major version as Semantic Versioning requires. + +Each Cloudsmith CLI release opens a pull request from `cloudsmith-bot`. The +release workflow of the CLI runs `scripts/bump-cli-version.sh` to make the +change. The script pins the default `cli-version` in `action.yml` to the new +CLI version. It also adds a patch version section to `CHANGELOG.md`. If +`[Unreleased]` has entries, the script adds a minor version section that +includes them. Merge the pull request to release it. + ## Variables and Secrets for GitHub Actions The install and validation test jobs run without any configuration. To run the diff --git a/README.md b/README.md index b1109f1..a6ab415 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Install the standalone [Cloudsmith CLI](https://github.com/cloudsmith-io/cloudsm | Runners | Linux, macOS, and Windows | | Architectures | x86-64, plus Linux and macOS ARM64 | | Runtime dependencies | No Python or Node.js; `export-auth-token` additionally uses `jq` on Linux and macOS | -| Version selection | Latest release or a specific CLI version | +| Version selection | A CLI version pinned by each action release, a specific CLI version, or the latest release | ## Quick start @@ -96,7 +96,7 @@ An authentication method is required: provide `api-key`, or provide both `oidc-n | Input | Description | Required | Default | | --- | --- | --- | --- | -| `cli-version` | CLI version to install, such as `1.20.0` | No | `latest` | +| `cli-version` | CLI version to install, such as `1.20.0`, or `latest` | No | The CLI version that the action release pins | | `install-directory` | Root directory for versioned CLI installations | No | `RUNNER_TEMP/cloudsmith-cli` | | `verify-auth` | Run `cloudsmith whoami` after setup | No | `false` | diff --git a/action.yml b/action.yml index 9979eb4..5c26a30 100644 --- a/action.yml +++ b/action.yml @@ -8,9 +8,9 @@ branding: inputs: cli-version: - description: "Cloudsmith CLI version to install, for example '1.20.0', or 'latest'." + description: "Cloudsmith CLI version to install, for example '1.20.0', or 'latest'. Defaults to the CLI version that this action release pins." required: false - default: "latest" + default: "1.27.0" install-directory: description: "Root directory for versioned CLI installations. Defaults to RUNNER_TEMP/cloudsmith-cli." required: false diff --git a/scripts/bump-cli-version.sh b/scripts/bump-cli-version.sh new file mode 100644 index 0000000..89184be --- /dev/null +++ b/scripts/bump-cli-version.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Pins the default cli-version in action.yml to a new Cloudsmith CLI release +# and records the change as a release of the action in CHANGELOG.md. The +# release is a patch release, or a minor release that also includes the +# entries under [Unreleased] when there are any. +# Prints the new action version, or nothing when the version is already pinned. +# Usage: scripts/bump-cli-version.sh CLI_VERSION +set -euo pipefail + +cli_version="${1:-}" +[[ "$cli_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \ + || { echo "usage: $0 MAJOR.MINOR.PATCH" >&2; exit 1; } + +pinned_version="$(awk ' + /^ cli-version:/ { in_cli_version = 1 } + in_cli_version && /^ default:/ { gsub(/"/, "", $2); print $2; exit } +' action.yml)" +[[ -n "$pinned_version" ]] || { echo "no cli-version default in action.yml" >&2; exit 1; } +if [[ "$pinned_version" == "$cli_version" ]]; then + echo "action.yml already pins Cloudsmith CLI $cli_version" >&2 + exit 0 +fi + +released_version="$(sed -n 's/^## \[\([0-9]*\.[0-9]*\.[0-9]*\)\].*/\1/p' CHANGELOG.md | head -n 1)" +[[ -n "$released_version" ]] || { echo "no released version in CHANGELOG.md" >&2; exit 1; } +unreleased_changes="$(awk ' + /^## \[/ { in_unreleased = ($0 == "## [Unreleased]"); next } + in_unreleased && $0 != "" && $0 != "---" +' CHANGELOG.md)" +IFS=. read -r major minor patch <<< "$released_version" +if [[ -n "$unreleased_changes" ]]; then + action_version="$major.$(( minor + 1 )).0" +else + action_version="$major.$minor.$(( patch + 1 ))" +fi + +awk -v cli_version="$cli_version" ' + /^ cli-version:/ { in_cli_version = 1 } + in_cli_version && /^ default:/ { sub(/".*"/, "\"" cli_version "\""); in_cli_version = 0 } + { print } +' action.yml > action.yml.new +mv action.yml.new action.yml + +awk -v action_version="$action_version" -v cli_version="$cli_version" -v today="$(date -u +%Y-%m-%d)" ' + inserted && $0 != "" { print "" } + { inserted = 0; print } + previous == "## [Unreleased]" && $0 == "---" { + inserted = 1 + print "" + print "## [" action_version "] - " today + print "---" + print "### Changed" + print "" + print "- Pin the default `cli-version` to Cloudsmith CLI " cli_version "." + } + { previous = $0 } +' CHANGELOG.md > CHANGELOG.md.new +mv CHANGELOG.md.new CHANGELOG.md + +echo "$action_version" diff --git a/scripts/test-bump-cli-version.sh b/scripts/test-bump-cli-version.sh new file mode 100644 index 0000000..d097386 --- /dev/null +++ b/scripts/test-bump-cli-version.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +set -euo pipefail + +bump_script="$(cd "$(dirname "$0")" && pwd)/bump-cli-version.sh" +test_root="$(mktemp -d)" +trap 'rm -rf "$test_root"' EXIT +today="$(date -u +%Y-%m-%d)" + +cat > "$test_root/action.yml" <<'EOF' +inputs: + cli-version: + description: "Cloudsmith CLI version to install." + required: false + default: "1.27.0" + install-directory: + default: "" +EOF + +cat > "$test_root/CHANGELOG.md" <<'EOF' +# Changelog + +## [Unreleased] +--- +### Fixed + +- An unreleased fix. + +## [3.2.0] - 2026-10-01 +--- +### Changed + +- An earlier change. +EOF + +cat > "$test_root/expected-CHANGELOG.md" < "$test_root/expected-action.yml" + +cd "$test_root" + +# Unreleased changes make the CLI bump a minor release that includes them. +test "$(bash "$bump_script" 1.28.0)" = "3.3.0" +diff -u expected-action.yml action.yml +diff -u expected-CHANGELOG.md CHANGELOG.md + +# With no unreleased changes, the CLI bump is a patch release. +test "$(bash "$bump_script" 1.29.0)" = "3.3.1" +sed -i.bak 's/default: "1.28.0"/default: "1.29.0"/' expected-action.yml +diff -u expected-action.yml action.yml +grep -Fx "## [3.3.1] - $today" CHANGELOG.md +grep -Fx -- "- Pin the default \`cli-version\` to Cloudsmith CLI 1.29.0." CHANGELOG.md + +# The version that is already pinned changes nothing. +cp CHANGELOG.md expected-CHANGELOG.md +test -z "$(bash "$bump_script" 1.29.0)" +diff -u expected-action.yml action.yml +diff -u expected-CHANGELOG.md CHANGELOG.md + +# A version that is not MAJOR.MINOR.PATCH is rejected. +if bash "$bump_script" latest 2> /dev/null; then + echo "expected 'latest' to be rejected" >&2 + exit 1 +fi +diff -u expected-action.yml action.yml + +echo "bump-cli-version tests passed" From 18ccc8647a7cc48c2558cd3182faf1b2c840c5f7 Mon Sep 17 00:00:00 2001 From: Ian Duffy Date: Thu, 1 Oct 2026 22:55:33 +0100 Subject: [PATCH 2/2] fix(ENG-14534): keep queued releases and never pin an older CLI Keep every queued Release run with queue: max, so that each CHANGELOG version gets its tag. Skip the release creation when the release exists, so that a re-run of a failed job can still move the major tag. Make bump-cli-version.sh skip a CLI version older than the pinned version. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 5 ++++- scripts/bump-cli-version.sh | 8 +++++--- scripts/test-bump-cli-version.sh | 5 +++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a95bbe9..d2857f4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,6 +15,7 @@ permissions: {} concurrency: group: release-${{ github.workflow }} cancel-in-progress: false + queue: max jobs: version: @@ -77,7 +78,9 @@ jobs: env: GH_TOKEN: ${{ github.token }} TAG: ${{ needs.version.outputs.tag }} - run: gh release create "$TAG" --title "Release $TAG" --target "$GITHUB_SHA" + run: | + gh release view "$TAG" > /dev/null 2>&1 \ + || gh release create "$TAG" --title "Release $TAG" --target "$GITHUB_SHA" - name: Update floating major tag env: diff --git a/scripts/bump-cli-version.sh b/scripts/bump-cli-version.sh index 89184be..c544fe5 100644 --- a/scripts/bump-cli-version.sh +++ b/scripts/bump-cli-version.sh @@ -3,7 +3,8 @@ # and records the change as a release of the action in CHANGELOG.md. The # release is a patch release, or a minor release that also includes the # entries under [Unreleased] when there are any. -# Prints the new action version, or nothing when the version is already pinned. +# Prints the new action version. Prints nothing when action.yml already pins +# that version or a newer one. # Usage: scripts/bump-cli-version.sh CLI_VERSION set -euo pipefail @@ -16,8 +17,9 @@ pinned_version="$(awk ' in_cli_version && /^ default:/ { gsub(/"/, "", $2); print $2; exit } ' action.yml)" [[ -n "$pinned_version" ]] || { echo "no cli-version default in action.yml" >&2; exit 1; } -if [[ "$pinned_version" == "$cli_version" ]]; then - echo "action.yml already pins Cloudsmith CLI $cli_version" >&2 +newest_version="$(printf '%s\n' "$pinned_version" "$cli_version" | sort -V | tail -n 1)" +if [[ "$pinned_version" == "$newest_version" ]]; then + echo "action.yml already pins Cloudsmith CLI $pinned_version" >&2 exit 0 fi diff --git a/scripts/test-bump-cli-version.sh b/scripts/test-bump-cli-version.sh index d097386..6e9b725 100644 --- a/scripts/test-bump-cli-version.sh +++ b/scripts/test-bump-cli-version.sh @@ -77,6 +77,11 @@ test -z "$(bash "$bump_script" 1.29.0)" diff -u expected-action.yml action.yml diff -u expected-CHANGELOG.md CHANGELOG.md +# A version older than the pinned version changes nothing. +test -z "$(bash "$bump_script" 1.28.5)" +diff -u expected-action.yml action.yml +diff -u expected-CHANGELOG.md CHANGELOG.md + # A version that is not MAJOR.MINOR.PATCH is rejected. if bash "$bump_script" latest 2> /dev/null; then echo "expected 'latest' to be rejected" >&2