diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 02e5972..d2857f4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,19 +1,69 @@ 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 + queue: max 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 +77,15 @@ 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 view "$TAG" > /dev/null 2>&1 \ + || 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..c544fe5 --- /dev/null +++ b/scripts/bump-cli-version.sh @@ -0,0 +1,62 @@ +#!/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. Prints nothing when action.yml already pins +# that version or a newer one. +# 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; } +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 + +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..6e9b725 --- /dev/null +++ b/scripts/test-bump-cli-version.sh @@ -0,0 +1,92 @@ +#!/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 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 + exit 1 +fi +diff -u expected-action.yml action.yml + +echo "bump-cli-version tests passed"