From 2c22e8aeec828d5b42441b0b72ea81203db0904f Mon Sep 17 00:00:00 2001 From: Daulet Zhanguzin Date: Tue, 10 Mar 2026 11:36:48 -0700 Subject: [PATCH] feat: offload release to GHA --- .github/workflows/release-cut.yml | 120 ++++++++++++++++++++++ scripts/release_bump.sh | 159 ++++++++++++++++++++++++++++++ 2 files changed, 279 insertions(+) create mode 100644 .github/workflows/release-cut.yml create mode 100755 scripts/release_bump.sh diff --git a/.github/workflows/release-cut.yml b/.github/workflows/release-cut.yml new file mode 100644 index 00000000..f7b58d85 --- /dev/null +++ b/.github/workflows/release-cut.yml @@ -0,0 +1,120 @@ +name: Release + +on: + workflow_dispatch: + inputs: + bump: + description: Semver bump to apply when version is not provided. + required: true + type: choice + default: patch + options: + - patch + - minor + - major + version: + description: Optional explicit version (x.y.z). If set, bump is ignored. + required: false + type: string + +permissions: + contents: write + +concurrency: + group: release-cut-${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: false + +jobs: + prepare: + name: Bump, Commit, and Tag + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.version.outputs.tag }} + version: ${{ steps.version.outputs.version }} + release_commit: ${{ steps.commit.outputs.release_commit }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Validate release branch + shell: bash + run: | + set -euo pipefail + if [[ "${GITHUB_REF_TYPE}" != "branch" ]]; then + echo "workflow_dispatch release must run from a branch." >&2 + exit 1 + fi + if [[ "${GITHUB_REF_NAME}" != "main" ]]; then + echo "Releases must run from main (current: ${GITHUB_REF_NAME})." >&2 + exit 1 + fi + + - name: Compute and apply release version + id: version + shell: bash + run: | + set -euo pipefail + + if [[ -n "${{ inputs.version }}" ]]; then + VERSION="$(./scripts/release_bump.sh --version "${{ inputs.version }}")" + else + VERSION="$(./scripts/release_bump.sh --bump "${{ inputs.bump }}")" + fi + + TAG="v${VERSION}" + if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then + echo "Tag ${TAG} already exists locally." >&2 + exit 1 + fi + if git ls-remote --tags origin "refs/tags/${TAG}" | grep -q "${TAG}$"; then + echo "Tag ${TAG} already exists on origin." >&2 + exit 1 + fi + + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + + - name: Commit, tag, and push + id: commit + shell: bash + run: | + set -euo pipefail + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + git add Cargo.lock \ + MODULE.bazel \ + crates/tokenizers/BUILD.bazel \ + crates/tokenizers/Cargo.toml \ + crates/tokenizers/src/lib.rs \ + tokenizer.go + if git diff --cached --quiet; then + echo "No release version changes detected." >&2 + exit 1 + fi + + git commit -m "chore: release ${{ steps.version.outputs.tag }}" + git tag -a "${{ steps.version.outputs.tag }}" -m "Release ${{ steps.version.outputs.tag }}" + + git push origin "HEAD:${GITHUB_REF_NAME}" + git push origin "${{ steps.version.outputs.tag }}" + + echo "release_commit=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + + publish: + name: Create GitHub Release + needs: prepare + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Create or update GitHub release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.prepare.outputs.tag }} + name: ${{ needs.prepare.outputs.tag }} + target_commitish: ${{ needs.prepare.outputs.release_commit }} + generate_release_notes: true diff --git a/scripts/release_bump.sh b/scripts/release_bump.sh new file mode 100755 index 00000000..cc9c53e4 --- /dev/null +++ b/scripts/release_bump.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: scripts/release_bump.sh [--bump patch|minor|major] [--version x.y.z] + +Examples: + scripts/release_bump.sh --bump patch + scripts/release_bump.sh --version 1.25.0 +EOF +} + +validate_semver() { + local version="$1" + [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] +} + +semver_gt() { + local left="$1" + local right="$2" + + local l_major l_minor l_patch r_major r_minor r_patch + IFS='.' read -r l_major l_minor l_patch <<< "${left}" + IFS='.' read -r r_major r_minor r_patch <<< "${right}" + + if (( l_major != r_major )); then + (( l_major > r_major )) + return + fi + if (( l_minor != r_minor )); then + (( l_minor > r_minor )) + return + fi + (( l_patch > r_patch )) +} + +bump_level="patch" +explicit_version="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --bump) + if [[ $# -lt 2 ]]; then + echo "Missing value for --bump" >&2 + usage + exit 1 + fi + bump_level="$2" + shift 2 + ;; + --version) + if [[ $# -lt 2 ]]; then + echo "Missing value for --version" >&2 + usage + exit 1 + fi + explicit_version="$2" + shift 2 + ;; + --help|-h) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + usage + exit 1 + ;; + esac +done + +current_version="$(perl -0ne 'if (/\[package\]\nname = "tokenizers-ffi"\nversion = "([^"]+)"/s) { print $1; exit }' crates/tokenizers/Cargo.toml)" +if [[ -z "${current_version}" ]]; then + echo "Failed to read current version from crates/tokenizers/Cargo.toml" >&2 + exit 1 +fi +if ! validate_semver "${current_version}"; then + echo "Current version is not semver: ${current_version}" >&2 + exit 1 +fi + +latest_tag="$(git tag -l 'v*' --sort=-v:refname | head -n1 || true)" +latest_tag_version="" +if [[ -n "${latest_tag}" ]]; then + latest_tag_version="${latest_tag#v}" + if ! validate_semver "${latest_tag_version}"; then + latest_tag_version="" + fi +fi + +base_version="${current_version}" +if [[ -n "${latest_tag_version}" ]] && semver_gt "${latest_tag_version}" "${base_version}"; then + base_version="${latest_tag_version}" +fi + +if [[ -n "${explicit_version}" ]]; then + if ! validate_semver "${explicit_version}"; then + echo "Invalid semver value for --version: ${explicit_version}" >&2 + exit 1 + fi + if ! semver_gt "${explicit_version}" "${base_version}"; then + echo "Explicit version must be greater than ${base_version}: ${explicit_version}" >&2 + exit 1 + fi + next_version="${explicit_version}" +else + IFS='.' read -r major minor patch <<< "${base_version}" + case "${bump_level}" in + patch) + patch=$((patch + 1)) + ;; + minor) + minor=$((minor + 1)) + patch=0 + ;; + major) + major=$((major + 1)) + minor=0 + patch=0 + ;; + *) + echo "Invalid --bump value: ${bump_level} (expected patch|minor|major)" >&2 + exit 1 + ;; + esac + next_version="${major}.${minor}.${patch}" +fi + +next_symbol="${next_version//./_}" + +perl -0pi -e "s/(\\[package\\]\\nname = \"tokenizers-ffi\"\\nversion = \")[^\"]+(\"\\n)/\$1${next_version}\$2/s" crates/tokenizers/Cargo.toml +perl -0pi -e "s/(\\[\\[package\\]\\]\\nname = \"tokenizers-ffi\"\\nversion = \")[^\"]+(\"\\n)/\$1${next_version}\$2/s" Cargo.lock +perl -pi -e "s/tokenizers_version_\\d+_\\d+_\\d+/tokenizers_version_${next_symbol}/g" tokenizer.go crates/tokenizers/src/lib.rs +perl -pi -e 'if (/name = "tokenizers_rs"/) { $in = 1 } if ($in && /version = "/) { s/version = "[^"]+"/version = "'"${next_version}"'"/; $in = 0 }' crates/tokenizers/BUILD.bazel +perl -0pi -e "s/(module\\(\\n\\s*name = \"com_github_daulet_tokenizers\",\\n\\s*version = \")[^\"]+(\",\\n\\))/\$1${next_version}\$2/s" MODULE.bazel + +if ! grep -Eq "^version = \"${next_version}\"$" crates/tokenizers/Cargo.toml; then + echo "Failed to write crates/tokenizers/Cargo.toml version ${next_version}" >&2 + exit 1 +fi +if ! grep -Eq "tokenizers_version_${next_symbol}" tokenizer.go; then + echo "Failed to write tokenizer.go symbol tokenizers_version_${next_symbol}" >&2 + exit 1 +fi +if ! grep -Eq "tokenizers_version_${next_symbol}" crates/tokenizers/src/lib.rs; then + echo "Failed to write crates/tokenizers/src/lib.rs symbol tokenizers_version_${next_symbol}" >&2 + exit 1 +fi +if ! grep -Eq "^ version = \"${next_version}\",$" MODULE.bazel; then + echo "Failed to write MODULE.bazel module version ${next_version}" >&2 + exit 1 +fi +if ! grep -Eq "^ version = \"${next_version}\",$" crates/tokenizers/BUILD.bazel; then + echo "Failed to write crates/tokenizers/BUILD.bazel version ${next_version}" >&2 + exit 1 +fi + +echo "${next_version}"