diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e16ab61 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,85 @@ +# A full semantic-version tag is version-specific release history. After the complete action test +# matrix passes, publish that release and move only its matching major tag so `@v1` users receive +# the new compatible version. The tagged commit must already be part of main. + +name: release + +on: + push: + tags: ["v*.*.*"] + +permissions: + contents: read + +concurrency: + group: release-${{ github.ref }} + +jobs: + validate: + runs-on: ubuntu-latest + outputs: + major: ${{ steps.version.outputs.major }} + release-commit: ${{ steps.version.outputs.release-commit }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Validate release tag + id: version + shell: bash + run: | + set -euo pipefail + tag="$GITHUB_REF_NAME" + if [[ ! "$tag" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then + echo "release tag must be an exact version such as v1.1.0" >&2 + exit 1 + fi + + major="v${BASH_REMATCH[1]}" + release_commit="$(git rev-parse "$tag^{}")" + git fetch origin main --no-tags + if ! git merge-base --is-ancestor "$release_commit" origin/main; then + echo "$tag does not point to a commit on main" >&2 + exit 1 + fi + + echo "major=$major" >>"$GITHUB_OUTPUT" + echo "release-commit=$release_commit" >>"$GITHUB_OUTPUT" + + test: + needs: validate + uses: ./.github/workflows/test.yml + + release: + needs: [validate, test] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Publish versioned release + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + if ! gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then + gh release create "$GITHUB_REF_NAME" \ + --verify-tag \ + --title "$GITHUB_REF_NAME" \ + --generate-notes + fi + + - name: Move major tag + env: + MAJOR_TAG: ${{ needs.validate.outputs.major }} + RELEASE_COMMIT: ${{ needs.validate.outputs.release-commit }} + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git tag -fa "$MAJOR_TAG" "$RELEASE_COMMIT" -m "Update $MAJOR_TAG to $GITHUB_REF_NAME" + git push origin "refs/tags/$MAJOR_TAG" --force diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 164ea24..c770cea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,25 +14,57 @@ on: push: branches: [main] pull_request: + workflow_call: env: SOTTO_TEST_VERSION: v0.4.0 jobs: reject-invalid-version: + name: reject-invalid-version (${{ matrix.name }}) + strategy: + fail-fast: false + matrix: + include: + - name: empty + version: "" + - name: floating + version: latest + - name: partial + version: v0.4 + - name: prerelease + version: v0.4.0-rc.1 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Reject an unpinned version + - name: Reject invalid version id: invalid continue-on-error: true uses: ./ with: - sotto-version: latest + sotto-version: ${{ matrix.version }} - name: Check rejection if: steps.invalid.outcome != 'failure' + env: + REJECTED_VERSION: ${{ matrix.version }} + run: | + echo "expected the action to reject '$REJECTED_VERSION'" >&2 + exit 1 + + reject-missing-release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Reject a version with no release + id: missing + continue-on-error: true + uses: ./ + with: + sotto-version: v999.999.999 + - name: Check rejection + if: steps.missing.outcome != 'failure' run: | - echo "expected the action to reject an unpinned version" >&2 + echo "expected the action to reject a version with no release" >&2 exit 1 install-and-verify: diff --git a/README.md b/README.md index 05b60a5..b0a8f0e 100644 --- a/README.md +++ b/README.md @@ -55,3 +55,19 @@ Give the step an `id` to use the resolved installation details in later steps: This action is tagged independently of the `sotto` CLI's own version (`v0.1.0`, `v0.2.0`, ...) - `sotto-action@v1` and `sotto-version: v0.4.0` are two unrelated version numbers. See [getsotto/sotto#67](https://github.com/getsotto/sotto/issues/67) for why. + +Use `@v1` to receive backwards-compatible action updates, a full tag such as `@v1.1.0` to pin a +version-specific action release, or a full commit SHA for the strongest workflow pinning. + +## Releasing + +Maintainers release the action by pushing an exact semantic-version tag from `main`: + +```sh +git tag -a v1.1.0 -m "v1.1.0" +git push origin v1.1.0 +``` + +The release workflow validates the tag, runs the complete target matrix, creates the versioned +`v1.1.0` GitHub release, then moves the compatible major tag (`v1`) to the same commit. Invalid tags +and tags whose commit is not on `main` fail without publishing or moving the major tag.