Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Release PR actions #76

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
249 changes: 249 additions & 0 deletions .github/actions/release-pr/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
# Workflow to create a new release PR, with one of the following two scenarios:
#
# - Major release
# - Pushes a new `release/<tag-prefix>-v<version>` branch based on latest `major.minor` version, e.g. `release/aptos-v1.0`
# - Creates a new `release-pr` branch from the release, then bumps the version with the `version` input
# - Opens a release PR from `release-pr` to `release/<tag-prefix>-v<version>`
# - Minor release
# - Pushes a new `release/<tag-prefix>-v<version>` branch based on the latest compatible major release
# - Creates a new `release-pr` branch from the release, then bumps the version with the `version` input
# - Opens a release PR from `release-pr` to `release/<tag-prefix>-v<version>`
# - Patch release
# - Pushes a new `patch/<tag-prefix>-v<version>` branch based on `release/<tag-prefix>-v<version>`, then bumps the verision with the `version` input
# - Errors if the `release/<tag-prefix>-v<version>` branch doesn't exist
# - Opens a release PR from `patch/<tag-prefix>-v<version>` to `release/<tag-prefix>-v<version>`
#
# When the PR is merged, the caller can then trigger a release from `ci-workflows/actions/tag-release`
# The PR branch can then be safely deleted, while the release branch should have a branch protection rule for historical preservation
#
# Example caller workflow: TODO: Link to Sphinx, ZKLC
# TODO: Add unique ID to PR branch name, e.g. release-pr-<version>-<tag-prefix> in order to allow multiple release PRs at once
name: Create release PR

description: Bump crate versions and open release PR

inputs:
# Optional working directory relative to `${{ github.workspace }}`, e.g. "aptos/" in "zk-light-clients/"
path:
description: 'Relative path under the GitHub workspace'
required: false
tag-prefix:
description: 'Optional tag prefix before version number'
required: false
more-crates:
description: 'Additional non-workspace crate paths to release'
required: false
# Caller workflows should specify a `choice` input or otherwise ensure the only options are `major`, `minor`, `patch`
release-type:
description: 'Semver release type: `major`, `minor`, or `patch`'
required: true
default: 'major'
# The release branch will be persistent across patches, so any input `patch` version must correspond to an existing branch
version:
description: 'Semver version corresponding to `release-type`, e.g. `1.0.0` for `major`'
required: true
token:
description: 'Git token'
required: true
reviewers:
description: 'Requested PR reviewers'
required: false

runs:
using: "composite"
steps:
- uses: dtolnay/rust-toolchain@stable

- run: cargo install tq-rs
shell: bash

# Checks that the input version is valid SemVer, e.g. `1.0.0`, `2.1.1`, or `3.0.0-alpha.rc`
# Discards patch version for a major or minor release. Patches are reserved for the `patch` `release-type`,
# as they update the existing release branch rather than creating a new one
- name: Validate version
run: |
echo "Validating input version ${{ inputs.version }}..."

# Regex from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
D='0|[1-9][0-9]*'
PW='[0-9]*[a-zA-Z-][0-9a-zA-Z-]*'
MW='[0-9a-zA-Z-]+'
SEMVER_REGEX="^($D)\.($D)\.($D)(-(($D|$PW)(\.($D|$PW))*))?(\+($MW(\.$MW)*))?$"

if [[ "${{ inputs.version }}" =~ $SEMVER_REGEX ]]; then
# Extract major, minor, patch versions and anything after the patch (e.g., pre-release or build metadata)
MAJOR="${BASH_REMATCH[1]}"
MINOR="${BASH_REMATCH[2]}"
PATCH="${BASH_REMATCH[3]}"
EXTRA="${BASH_REMATCH[4]}" # Pre-release or build metadata (if present)

# Error if the patch version is greater than 0 for a release
if [[ "${{ inputs.release-type }}" != "patch" ]]; then
if [[ "$PATCH" -gt 0 ]]; then
echo "Error: Patch version must be 0 for a major/minor release, but found $PATCH."
exit 1
fi
else
if [[ ! "$PATCH" -gt 0 ]]; then
echo "Error: Patch version must be >0 for a patch, but found $PATCH."
exit 1
fi
fi

echo "Version ${{ inputs.version }} is valid."

# Construct BRANCH_VERSION as MAJOR.MINOR + EXTRA (retain pre-release and build metadata)
BRANCH_VERSION="${MAJOR}.${MINOR}${EXTRA}"
echo "Branch version: $BRANCH_VERSION"

if [[ "${{ inputs.release-type }}" == "major" ]]; then
echo "PRIOR_VERSION=$(( MAJOR - 1 ))" | tee -a $GITHUB_ENV
elif [[ "${{ inputs.release-type }}" == "minor" ]]; then
echo "PRIOR_VERSION=${MAJOR}" | tee -a $GITHUB_ENV
fi
else
echo "Version ${{ inputs.version }} is not valid SemVer. Aborting..."
exit 1
fi

echo "BRANCH_VERSION=$BRANCH_VERSION" | tee -a $GITHUB_ENV
echo "CRATE_VERSION=${{ inputs.version }}" | tee -a $GITHUB_ENV
shell: bash

# Always checks out an existing release branch (if existent) as the base for the release PR. Maintainers are expected to
# cherry-pick the desired changes from the default branch and push to the temporary `release-pr` branch before merging the PR.
#
# The `release/<tag-prefix>-v<version>` branch always removes the patch number from `<version>` as we don't want to change
# the release branch name for patch versions.
# However, when we make a version bump, we include the full SemVer input in `Cargo.toml` and the release tag for clarity
- name: Create or checkout base branch
run: |
if [[ -n "${{ inputs.tag-prefix }}" ]]; then
TAG_PREFIX="${{ inputs.tag-prefix }}-"
fi

BASE_BRANCH="release/${TAG_PREFIX}v${{ env.BRANCH_VERSION }}"
echo $BASE_BRANCH

# If major release, get latest minor release from prior major version
# If minor release, get latest minor release from given major version
if [[ "${{ inputs.release-type }}" == "major" || "${{ inputs.release-type }}" == "minor" ]]; then
set +o pipefail
PRIOR_RELEASE=$(git branch -r | grep -E "origin/release/${TAG_PREFIX}v${{ env.PRIOR_VERSION }}\.[0-9]+" | sed 's/origin\///' | sort -V | tail -n 1)
set -o pipefail
echo "Prior release: $PRIOR_RELEASE"
# If a prior release is found, check it out as the source of the new release branch (which is the PR base)
# Otherwise, use the default branch (e.g. if this is the first release).
if [[ -n "$PRIOR_RELEASE" ]]; then
git checkout $PRIOR_RELEASE
fi
git checkout -b $BASE_BRANCH
git push origin $BASE_BRANCH
else
git checkout $BASE_BRANCH
fi

echo "BASE_BRANCH=$BASE_BRANCH" | tee -a $GITHUB_ENV
env:
GITHUB_TOKEN: ${{ inputs.token }}
shell: bash

# Include the full patch version in the PR branch name if a patch, otherwise call it `release-pr` for a major or minor release
- name: Create PR branch
run: |
if [[ "${{ inputs.release-type }}" == "patch" ]]; then
if [[ -n "${{ inputs.tag-prefix }}" ]]; then
PR_BRANCH="${{ inputs.release-type }}/${{ inputs.tag-prefix }}-v${{ env.CRATE_VERSION }}"
else
PR_BRANCH="${{ inputs.release-type }}/v${{ env.CRATE_VERSION }}"
fi
else
PR_BRANCH="release-pr"
fi

echo "PR_BRANCH=$PR_BRANCH" | tee -a $GITHUB_ENV
if [[ -n "${{ inputs.path }}" ]]; then
echo "PR_TITLE=chore(${{ inputs.path }}): Release ${{ env.CRATE_VERSION }}" | tee -a $GITHUB_ENV
else
echo "PR_TITLE=chore: Release ${{ env.CRATE_VERSION }}" | tee -a $GITHUB_ENV
fi
env:
GITHUB_TOKEN: ${{ inputs.token }}
shell: bash

- name: Update version in Cargo.toml
run: |
echo "Updating version in Cargo.toml..."

MEMBERS=$(tq workspace.members -f Cargo.toml)

if [[ -n "${{ inputs.more-crates }}" ]]; then
CRATES=$(echo ${{ inputs.more-crates }} | jq -Rc 'split(",")')
RELEASE_CRATES=$(echo "$MEMBERS" | jq --argjson more "$CRATES" -r '. += $more | .[]')
else
RELEASE_CRATES=$(echo "$MEMBERS" | jq -r '.[]')
fi

bump_version() {
cd "$1"
OLD_VERSION=$(grep -oP 'version = "\K[^"]+' Cargo.toml | head -n1)
if [[ "${{ env.CRATE_VERSION }}" > "$OLD_VERSION" ]]; then
sed -i "s/version = \"$OLD_VERSION\"/version = \"${{ env.CRATE_VERSION }}\"/" Cargo.toml
else
echo "New version is not greater than the current version for $1. Aborting..."
exit 1
fi
cd ${{ github.workspace }}/${{ inputs.path }}
}

while IFS= read -r path; do
if [[ "$path" == *"/*" ]]; then
for dir in "${path%/*}"/*; do
if [ -d "$dir" ] && [ -f "$dir/Cargo.toml" ]; then
bump_version "$dir"
fi
done
else
bump_version "$path"
fi
done <<< "$RELEASE_CRATES"

# Log Cargo.toml changes
git diff
working-directory: ${{ github.workspace }}/${{ inputs.path }}
shell: bash

- name: Create pull request description
run: |
REPO=$( echo "${{ github.repository }}" | awk -F'/' '{ print $2 }')
if [[ -n "${{ inputs.tag-prefix }}" ]]; then
TAG="${{ inputs.tag-prefix }}-v${{ env.CRATE_VERSION }}"
else
TAG="v${{ env.CRATE_VERSION }}"
fi
if [[ -n "${{ inputs.path }}" ]]; then
NAME="$REPO/${{ inputs.path }}"
else
NAME="$REPO"
fi

printf '%s\n' "This is an automated release PR for \`$NAME\` version \`${{ env.CRATE_VERSION }}\`.

On merge, this will trigger the [release publish workflow](${{ github.server_url }}/${{ github.repository }}/actions/workflows/tag-release.yml), which will upload a new GitHub release with tag \`$TAG\`.

[Workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" > body.md
shell: bash

# TODO: Also open PR to `dev` to bump version if this is the latest release
- name: Create release PR
uses: peter-evans/create-pull-request@v7
with:
token: ${{ inputs.token }}
add-paths: |
**/Cargo.toml
commit-message: ${{ env.PR_TITLE }}
title: ${{ env.PR_TITLE }}
body-path: ./body.md
branch: ${{ env.PR_BRANCH }}
labels: automated-issue
reviewers: ${{ inputs.reviewers }}
104 changes: 104 additions & 0 deletions .github/actions/tag-release/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Workflow to create a new tag release when a release branch is merged
#
# Only supports running via the `pull_request` trigger in the caller workflow
name: Tag release

description: Bump crate versions and open release PR

inputs:
tag-prefix:
description: 'Tag prefix used to get most recent release'
required: false
default: ""
changelog-path:
description: 'Relative path to look for changelog'
required: false
default: "."
changelog-config-file:
description: 'Relative path to the config file for `mikepenz/release-changelog-builder-action`'
required: false
default: ""

runs:
using: "composite"
steps:
- name: Get version
id: get-version
run: |
RELEASE_BRANCH=${{ github.event.pull_request.base.ref }}

if [[ -n "${{ inputs.tag-prefix }}" ]]; then
TAG_PREFIX="${{ inputs.tag-prefix }}-"
fi

# On a major/minor release, insert a `.0` for the patch version)
if [[ "${{ github.event.pull_request.head.ref }}" == "release-pr" ]]; then
# Extract the version from the branch
VERSION=$(echo "$RELEASE_BRANCH" | awk -F'-' '{print $(NF)}')

D='0|[1-9][0-9]*'
PW='[0-9]*[a-zA-Z-][0-9a-zA-Z-]*'
MW='[0-9a-zA-Z-]+'
SEMVER_REGEX="^v?($D)\.($D)(-(($D|$PW)(\.($D|$PW))*))?(\+($MW(\.$MW)*))?$"

# Insert the `.0` patch version
if [[ "$VERSION" =~ $SEMVER_REGEX ]]; then
# Extract major, minor versions and anything after the patch (e.g., pre-release or build metadata)
MAJOR="${BASH_REMATCH[1]}"
MINOR="${BASH_REMATCH[2]}"
EXTRA="${BASH_REMATCH[3]}" # Pre-release or build metadata (if present)
TAG_VERSION="${TAG_PREFIX}v$MAJOR.$MINOR.0$EXTRA"

echo "Tag version: $TAG_VERSION"
else
echo "Failed to parse tag version from release branch name"
exit 1
fi
else
TAG_VERSION=$(echo "${{ github.event.pull_request.head.ref }}" | cut -d'/' -f 2)
fi

git tag -a $TAG_VERSION -m "$TAG_VERSION" origin/$RELEASE_BRANCH
git push origin $TAG_VERSION --follow-tags
echo "tag-version=$TAG_VERSION" | tee -a "$GITHUB_OUTPUT"
echo "RELEASE_BRANCH=$RELEASE_BRANCH" | tee -a "$GITHUB_ENV"
shell: bash

- name: Get latest release reference
id: get-latest-release
run: |
set +o pipefail
LATEST_RELEASE=$(gh release list --repo ${{ github.repository }} --limit 100 | grep -Ei "${{ inputs.tag-prefix }}" | head -n 1 | awk '{ print $1 }')
set -o pipefail

if [ -z "$LATEST_RELEASE" ]; then
LATEST_RELEASE=$(git rev-list --max-parents=0 HEAD)
echo "The first commit on branch ${{ env.RELEASE_BRANCH }} is $LATEST_RELEASE"
else
echo "Found release: $LATEST_RELEASE"
fi

echo "latest_release=$LATEST_RELEASE" | tee -a "$GITHUB_OUTPUT"
env:
GH_TOKEN: ${{ github.token }}
shell: bash

# TODO: Add an automatic labeler for PRs based on title/commit prefix
- name: Build Changelog
id: github_release
uses: mikepenz/release-changelog-builder-action@v5
with:
configuration: ${{ inputs.changelog-config-file }}
path: "./${{ inputs.changelog-path }}"
fromTag: ${{ steps.get-latest-release.outputs.latest_release }}
toTag: ${{ steps.get-version.outputs.tag-version }}
env:
GITHUB_TOKEN: ${{ github.token }}

- name: Create Release
uses: ncipollo/release-action@v1
with:
body: ${{ steps.github_release.outputs.changelog }}
tag: ${{ steps.get-version.outputs.tag-version }}
commit: ${{ env.RELEASE_BRANCH }}
allowUpdates: true