diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ff8d75a..e76d072 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,113 +14,44 @@ on: description: Release version (X.Y.Z) required: true type: string - pull_request: - types: [closed] - branches: - - develop - - main permissions: actions: write contents: write - pull-requests: read concurrency: - group: release-${{ github.event_name == 'workflow_dispatch' && inputs.target_branch || github.event.pull_request.base.ref }} + group: release-${{ inputs.target_branch }} cancel-in-progress: false env: CARGO_TERM_COLOR: always - MAIN_BASE_VERSION: ${{ vars.MAIN_BASE_VERSION }} - DEVELOP_BASE_VERSION: ${{ vars.DEVELOP_BASE_VERSION }} jobs: plan: - if: github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true runs-on: ubuntu-latest outputs: - should_release: ${{ steps.plan.outputs.should_release }} - reason: ${{ steps.plan.outputs.reason }} - bump: ${{ steps.plan.outputs.bump }} - base_version: ${{ steps.plan.outputs.base_version }} previous_version: ${{ steps.plan.outputs.previous_version }} version: ${{ steps.plan.outputs.version }} target_branch: ${{ steps.plan.outputs.target_branch }} - source_branch: ${{ steps.plan.outputs.source_branch }} - merge_sha: ${{ steps.context.outputs.merge_sha }} steps: - name: Checkout selected branch - if: github.event_name == 'workflow_dispatch' uses: actions/checkout@v4 with: ref: ${{ inputs.target_branch }} fetch-depth: 0 - - name: Checkout merge commit - if: github.event_name == 'pull_request' - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.merge_commit_sha }} - fetch-depth: 0 - - name: Fetch tags run: git fetch --force --tags - - name: Resolve base versions - id: bases - shell: bash - run: | - set -euo pipefail - - current_version="$( - sed -nE 's/^version = "([0-9]+\.[0-9]+\.[0-9]+)"/\1/p' Cargo.toml | head -n1 - )" - - if [[ -z "${current_version}" ]]; then - echo "Could not determine the current Cargo version." >&2 - exit 1 - fi - - echo "main_base_version=${MAIN_BASE_VERSION:-$current_version}" >> "$GITHUB_OUTPUT" - echo "develop_base_version=${DEVELOP_BASE_VERSION:-$current_version}" >> "$GITHUB_OUTPUT" - - - name: Resolve release context - id: context - shell: bash - run: | - set -euo pipefail - - if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then - echo "target_branch=${{ inputs.target_branch }}" >> "$GITHUB_OUTPUT" - echo "source_branch=workflow_dispatch" >> "$GITHUB_OUTPUT" - echo "release_version=${{ inputs.version }}" >> "$GITHUB_OUTPUT" - echo "merge_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - else - echo "target_branch=${{ github.event.pull_request.base.ref }}" >> "$GITHUB_OUTPUT" - echo "source_branch=${{ github.event.pull_request.head.ref }}" >> "$GITHUB_OUTPUT" - echo "release_version=" >> "$GITHUB_OUTPUT" - echo "merge_sha=${{ github.event.pull_request.merge_commit_sha }}" >> "$GITHUB_OUTPUT" - fi - - name: Plan release id: plan env: - TARGET_BRANCH: ${{ steps.context.outputs.target_branch }} - SOURCE_BRANCH: ${{ steps.context.outputs.source_branch }} - RELEASE_VERSION: ${{ steps.context.outputs.release_version }} - MAIN_BASE_VERSION: ${{ steps.bases.outputs.main_base_version }} - DEVELOP_BASE_VERSION: ${{ steps.bases.outputs.develop_base_version }} + TARGET_BRANCH: ${{ inputs.target_branch }} + RELEASE_VERSION: ${{ inputs.version }} run: devops/ga-release-plan.sh - - name: Stop on non-release branches - if: steps.plan.outputs.should_release != 'true' - shell: bash - run: | - echo "Skipping release: ${{ steps.plan.outputs.reason }}" - prepare_release: needs: plan - if: needs.plan.outputs.should_release == 'true' runs-on: ubuntu-latest outputs: release_sha: ${{ steps.release_commit.outputs.release_sha }} @@ -142,17 +73,6 @@ jobs: exit 1 fi - - name: Ensure release commit includes merged change - if: github.event_name == 'pull_request' - shell: bash - run: | - set -euo pipefail - - if ! git merge-base --is-ancestor "${{ needs.plan.outputs.merge_sha }}" HEAD; then - echo "The target branch no longer contains merge commit ${{ needs.plan.outputs.merge_sha }}." >&2 - exit 1 - fi - - name: Configure git author run: | git config user.name "github-actions[bot]" @@ -189,7 +109,6 @@ jobs: create_release: needs: [plan, prepare_release] - if: needs.plan.outputs.should_release == 'true' runs-on: ubuntu-latest steps: - name: Check out release commit @@ -205,7 +124,7 @@ jobs: set -euo pipefail NOTES_ARGS=() - if [[ "${{ needs.plan.outputs.previous_version }}" != "${{ needs.plan.outputs.base_version }}" ]]; then + if [[ -n "${{ needs.plan.outputs.previous_version }}" ]]; then NOTES_ARGS+=(-f previous_tag_name="${{ needs.plan.outputs.previous_version }}") fi diff --git a/devops/ga-release-plan.sh b/devops/ga-release-plan.sh index 41e95c0..f849235 100755 --- a/devops/ga-release-plan.sh +++ b/devops/ga-release-plan.sh @@ -3,122 +3,66 @@ set -euo pipefail TARGET_BRANCH="${TARGET_BRANCH:-${1:-}}" -SOURCE_BRANCH="${SOURCE_BRANCH:-${2:-}}" -MAIN_BASE_VERSION="${MAIN_BASE_VERSION:-}" -DEVELOP_BASE_VERSION="${DEVELOP_BASE_VERSION:-}" -RELEASE_VERSION="${RELEASE_VERSION:-}" +RELEASE_VERSION="${RELEASE_VERSION:-${2:-}}" if [[ -z "${TARGET_BRANCH}" ]]; then - echo "usage: TARGET_BRANCH= SOURCE_BRANCH= RELEASE_VERSION=X.Y.Z MAIN_BASE_VERSION=X.Y.Z DEVELOP_BASE_VERSION=X.Y.Z $0" >&2 + echo "usage: TARGET_BRANCH= RELEASE_VERSION=X.Y.Z $0" >&2 exit 1 fi -semver_pattern='^[0-9]+\.[0-9]+\.[0-9]+$' - -require_semver() { - local value="$1" - local label="$2" - - if [[ ! "${value}" =~ ${semver_pattern} ]]; then - echo "${label} must be a semantic version like 1.0.0" >&2 - exit 1 - fi -} - -require_semver "${MAIN_BASE_VERSION}" "MAIN_BASE_VERSION" -require_semver "${DEVELOP_BASE_VERSION}" "DEVELOP_BASE_VERSION" - -if [[ -n "${RELEASE_VERSION}" ]]; then - require_semver "${RELEASE_VERSION}" "RELEASE_VERSION" -fi - case "${TARGET_BRANCH}" in - main) - BASE_VERSION="${MAIN_BASE_VERSION}" - ;; - develop) - BASE_VERSION="${DEVELOP_BASE_VERSION}" - ;; + main|develop) ;; *) echo "Unsupported target branch: ${TARGET_BRANCH}" >&2 exit 1 ;; esac -BASE_MAJOR="${BASE_VERSION%%.*}" -TAG_PATTERN="^${BASE_MAJOR}\\.[0-9]+\\.[0-9]+$" +semver_pattern='^[0-9]+\.[0-9]+\.[0-9]+$' + +if [[ ! "${RELEASE_VERSION}" =~ ${semver_pattern} ]]; then + echo "RELEASE_VERSION must be a semantic version like 1.0.0" >&2 + exit 1 +fi + +# Newest released version (empty when no semver tags exist yet). PREVIOUS_VERSION="$( git tag --list \ - | grep -E "${TAG_PATTERN}" || true + | grep -E "${semver_pattern}" || true )" PREVIOUS_VERSION="$( printf '%s\n' "${PREVIOUS_VERSION}" \ | sort -V \ | tail -n 1 )" -PREVIOUS_VERSION="${PREVIOUS_VERSION:-${BASE_VERSION}}" + +# Baseline for the next version: the newest of the latest tag and the version +# currently in Cargo.toml, so the requested version can never move backwards. +CARGO_VERSION="$( + sed -nE 's/^version = "([0-9]+\.[0-9]+\.[0-9]+)"/\1/p' Cargo.toml | head -n1 +)" CURRENT_VERSION="$( - printf '%s\n%s\n' "${BASE_VERSION}" "${PREVIOUS_VERSION}" \ + printf '%s\n%s\n' "${PREVIOUS_VERSION}" "${CARGO_VERSION}" \ | sort -V \ | tail -n 1 )" -if [[ -n "${RELEASE_VERSION}" ]]; then - SHOULD_RELEASE="true" - REASON="" - BUMP="manual" - - RELEASE_MAJOR="${RELEASE_VERSION%%.*}" - if [[ "${RELEASE_MAJOR}" != "${BASE_MAJOR}" ]]; then - echo "RELEASE_VERSION ${RELEASE_VERSION} does not match the ${TARGET_BRANCH} release line ${BASE_MAJOR}.x.x" >&2 - exit 1 - fi - - if [[ "$(printf '%s\n%s\n' "${CURRENT_VERSION}" "${RELEASE_VERSION}" | sort -V | tail -n 1)" != "${RELEASE_VERSION}" ]] || [[ "${RELEASE_VERSION}" == "${CURRENT_VERSION}" ]]; then - echo "RELEASE_VERSION ${RELEASE_VERSION} must be newer than ${CURRENT_VERSION}" >&2 - exit 1 - fi - - VERSION="${RELEASE_VERSION}" -elif [[ "${SOURCE_BRANCH}" == feat/* ]]; then - SHOULD_RELEASE="true" - REASON="" - BUMP="minor" - IFS='.' read -r MAJOR MINOR PATCH <<< "${CURRENT_VERSION}" - MINOR=$((MINOR + 1)) - PATCH=0 - VERSION="${MAJOR}.${MINOR}.${PATCH}" -elif [[ "${SOURCE_BRANCH}" == bugfix/* ]]; then - SHOULD_RELEASE="true" - REASON="" - BUMP="patch" - IFS='.' read -r MAJOR MINOR PATCH <<< "${CURRENT_VERSION}" - PATCH=$((PATCH + 1)) - VERSION="${MAJOR}.${MINOR}.${PATCH}" -else - SHOULD_RELEASE="false" - REASON="Source branch must start with feat/ or bugfix/." - BUMP="none" - VERSION="" +newest="$(printf '%s\n%s\n' "${CURRENT_VERSION}" "${RELEASE_VERSION}" | sort -V | tail -n 1)" +if [[ "${newest}" != "${RELEASE_VERSION}" ]] || [[ "${RELEASE_VERSION}" == "${CURRENT_VERSION}" ]]; then + echo "RELEASE_VERSION ${RELEASE_VERSION} must be newer than ${CURRENT_VERSION}" >&2 + exit 1 fi +VERSION="${RELEASE_VERSION}" + if [[ -n "${GITHUB_OUTPUT:-}" ]]; then { - echo "should_release=${SHOULD_RELEASE}" - echo "reason=${REASON}" - echo "bump=${BUMP}" - echo "base_version=${BASE_VERSION}" echo "previous_version=${PREVIOUS_VERSION}" echo "version=${VERSION}" echo "target_branch=${TARGET_BRANCH}" - echo "source_branch=${SOURCE_BRANCH}" } >> "${GITHUB_OUTPUT}" fi -echo "should_release=${SHOULD_RELEASE}" -echo "reason=${REASON}" -echo "bump=${BUMP}" -echo "base_version=${BASE_VERSION}" echo "previous_version=${PREVIOUS_VERSION}" echo "version=${VERSION}"