Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 4 additions & 85 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand All @@ -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]"
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down
106 changes: 25 additions & 81 deletions devops/ga-release-plan.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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=<main|develop> SOURCE_BRANCH=<branch> RELEASE_VERSION=X.Y.Z MAIN_BASE_VERSION=X.Y.Z DEVELOP_BASE_VERSION=X.Y.Z $0" >&2
echo "usage: TARGET_BRANCH=<main|develop> 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}"
Loading