Skip to content
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
8 changes: 8 additions & 0 deletions .github/scripts/bump-major-version.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail

VERSION="${1:?Usage: bump-major-version.sh <version> [version_file]}"
VERSION_FILE="${2:-.github/workflows/version.txt}"
MAJOR="${VERSION%%.*}"

echo "$MAJOR" > "$VERSION_FILE"
43 changes: 43 additions & 0 deletions .github/scripts/bump-major-version.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BUMP_MAJOR_SH="$SCRIPT_DIR/bump-major-version.sh"
FAILURES=0

assert_eq() {
local description="$1" expected="$2" actual="$3"
if [ "$expected" != "$actual" ]; then
echo "FAIL: $description (expected '$expected', got '$actual')" >&2
FAILURES=$((FAILURES + 1))
else
echo "PASS: $description"
fi
}

# Test: writes the major version number to the given file
tmp_file=$(mktemp)
bash "$BUMP_MAJOR_SH" "4.0.0" "$tmp_file"
assert_eq "writes major version" "4" "$(cat "$tmp_file")"
rm -f "$tmp_file"

# Test: multi-digit major version
tmp_file=$(mktemp)
bash "$BUMP_MAJOR_SH" "12.3.4" "$tmp_file"
assert_eq "writes multi-digit major version" "12" "$(cat "$tmp_file")"
rm -f "$tmp_file"

# Test: missing version argument fails
if bash "$BUMP_MAJOR_SH" >/dev/null 2>&1; then
echo "FAIL: expected failure with no version argument" >&2
FAILURES=$((FAILURES + 1))
else
echo "PASS: fails with no version argument"
fi

if [ "$FAILURES" -gt 0 ]; then
echo "$FAILURES test(s) failed" >&2
exit 1
fi

echo "All bump-major-version.sh tests passed"
66 changes: 66 additions & 0 deletions .github/scripts/next-version.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
set -euo pipefail

BUMP="${1:?Usage: next-version.sh <patch|minor|major>}"

case "$BUMP" in
patch|minor|major) ;;
*)
echo "Unknown bump type: $BUMP" >&2
exit 1
;;
esac

LATEST_SEMVER=$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
| sed 's/^v//' \
| sort -t. -k1,1n -k2,2n -k3,3n \
| tail -1 || true)

if [ -n "$LATEST_SEMVER" ]; then
MAJOR=$(echo "$LATEST_SEMVER" | cut -d. -f1)
MINOR=$(echo "$LATEST_SEMVER" | cut -d. -f2)
PATCH=$(echo "$LATEST_SEMVER" | cut -d. -f3)

case "$BUMP" in
major)
MAJOR=$((MAJOR + 1))
MINOR=0
PATCH=0
;;
minor)
MINOR=$((MINOR + 1))
PATCH=0
;;
patch)
PATCH=$((PATCH + 1))
;;
esac
else
LATEST_FLOATING=$(git tag -l 'v[0-9]*' \
| grep -E '^v[0-9]+$' \
| sed 's/^v//' \
| sort -n \
| tail -1 || true)

if [ -z "$LATEST_FLOATING" ]; then
echo "No version tags found (neither vX.Y.Z nor vN)." >&2
exit 1
fi

echo "No vX.Y.Z tag found; bootstrapping from floating tag v${LATEST_FLOATING}. Ignoring requested bump '${BUMP}'." >&2
MAJOR="$LATEST_FLOATING"
MINOR=0
PATCH=0
fi

NEXT_VERSION="${MAJOR}.${MINOR}.${PATCH}"

if [ "$MAJOR" = "3" ]; then
IS_V3=true
else
IS_V3=false
fi

echo "NEXT_VERSION=${NEXT_VERSION}"
echo "IS_V3=${IS_V3}"
98 changes: 98 additions & 0 deletions .github/scripts/next-version.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
NEXT_VERSION_SH="$SCRIPT_DIR/next-version.sh"
FAILURES=0

assert_eq() {
local description="$1" expected="$2" actual="$3"
if [ "$expected" != "$actual" ]; then
echo "FAIL: $description (expected '$expected', got '$actual')" >&2
FAILURES=$((FAILURES + 1))
else
echo "PASS: $description"
fi
}

new_temp_repo() {
local tmp
tmp=$(mktemp -d)
git init -q "$tmp"
git -C "$tmp" config user.email "test@example.com"
git -C "$tmp" config user.name "Test"
git -C "$tmp" commit -q --allow-empty -m "init"
echo "$tmp"
}

# Test: no tags at all -> non-zero exit
repo=$(new_temp_repo)
if (cd "$repo" && bash "$NEXT_VERSION_SH" patch) >/dev/null 2>&1; then
echo "FAIL: expected failure with no tags present" >&2
FAILURES=$((FAILURES + 1))
else
echo "PASS: fails with no tags present"
fi
rm -rf "$repo"

# Test: bootstrap from floating v3 tag, ignoring the requested bump
repo=$(new_temp_repo)
git -C "$repo" tag v3
OUTPUT=$(cd "$repo" && bash "$NEXT_VERSION_SH" patch)
assert_eq "bootstrap ignores bump, uses floating major" "$(printf 'NEXT_VERSION=3.0.0\nIS_V3=true')" "$OUTPUT"
rm -rf "$repo"

# Test: patch bump from full semver tag
repo=$(new_temp_repo)
git -C "$repo" tag v3.2.1
OUTPUT=$(cd "$repo" && bash "$NEXT_VERSION_SH" patch)
assert_eq "patch bump" "$(printf 'NEXT_VERSION=3.2.2\nIS_V3=true')" "$OUTPUT"
rm -rf "$repo"

# Test: minor bump resets patch
repo=$(new_temp_repo)
git -C "$repo" tag v3.2.1
OUTPUT=$(cd "$repo" && bash "$NEXT_VERSION_SH" minor)
assert_eq "minor bump resets patch" "$(printf 'NEXT_VERSION=3.3.0\nIS_V3=true')" "$OUTPUT"
rm -rf "$repo"

# Test: major bump resets minor/patch and flips IS_V3 to false
repo=$(new_temp_repo)
git -C "$repo" tag v3.2.1
OUTPUT=$(cd "$repo" && bash "$NEXT_VERSION_SH" major)
assert_eq "major bump resets minor/patch" "$(printf 'NEXT_VERSION=4.0.0\nIS_V3=false')" "$OUTPUT"
rm -rf "$repo"

# Test: a full semver tag takes priority over an existing floating tag
repo=$(new_temp_repo)
git -C "$repo" tag v3
git -C "$repo" tag v3.2.1
OUTPUT=$(cd "$repo" && bash "$NEXT_VERSION_SH" patch)
assert_eq "full semver tag wins over floating tag" "$(printf 'NEXT_VERSION=3.2.2\nIS_V3=true')" "$OUTPUT"
rm -rf "$repo"

# Test: numeric sort, not lexicographic (v3.10.0 > v3.2.1)
repo=$(new_temp_repo)
git -C "$repo" tag v3.2.1
git -C "$repo" tag v3.10.0
OUTPUT=$(cd "$repo" && bash "$NEXT_VERSION_SH" patch)
assert_eq "numeric sort picks v3.10.0 over v3.2.1" "$(printf 'NEXT_VERSION=3.10.1\nIS_V3=true')" "$OUTPUT"
rm -rf "$repo"

# Test: invalid bump type is rejected
repo=$(new_temp_repo)
git -C "$repo" tag v3.2.1
if (cd "$repo" && bash "$NEXT_VERSION_SH" bogus) >/dev/null 2>&1; then
echo "FAIL: expected failure for invalid bump type" >&2
FAILURES=$((FAILURES + 1))
else
echo "PASS: rejects invalid bump type"
fi
rm -rf "$repo"

if [ "$FAILURES" -gt 0 ]; then
echo "$FAILURES test(s) failed" >&2
exit 1
fi

echo "All next-version.sh tests passed"
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,22 @@ jobs:
working-directory: ${{ matrix.working-directory }}
run: npm run ci-test

test-release-scripts:
name: Release Scripts Tests
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Run next-version.sh tests
run: bash .github/scripts/next-version.test.sh

- name: Run bump-major-version.sh tests
run: bash .github/scripts/bump-major-version.test.sh

test-python:
name: Python Tests
runs-on: ubuntu-latest
Expand Down
125 changes: 125 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
name: Release

on:
workflow_dispatch:
inputs:
bump:
description: Version part to bump
required: true
type: choice
options:
- patch
- minor
- major
dry_run:
description: Whether this is a dry run
required: true
type: boolean
default: true

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

jobs:
release:
name: Release
runs-on: ubuntu-latest
environment: release
if: github.ref == 'refs/heads/main'
permissions:
contents: write
id-token: write

steps:
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
id: app-token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
permission-contents: write

- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
token: ${{ steps.app-token.outputs.token }}
fetch-depth: 0
# Needed to push the tag and version bump commit in later steps.
persist-credentials: true

- name: Setup
uses: $/setup
with:
aws_role_arn: ${{ secrets.AWS_ROLE_ARN }}
aws_region_name: ${{ vars.AWS_REGION_NAME }}
aws_secret_id: ${{ secrets.AWS_SECRET_ID }}

- name: Determine next version
shell: bash
run: |
set +e
bash .github/scripts/next-version.sh "${{ inputs.bump }}" > /tmp/next-version.out 2> /tmp/next-version.err
STATUS=$?
set -e
if [ -s /tmp/next-version.err ]; then
cat /tmp/next-version.err
cat /tmp/next-version.err >> "$GITHUB_STEP_SUMMARY"
fi
if [ "$STATUS" -ne 0 ]; then
exit "$STATUS"
fi
cat /tmp/next-version.out >> "$GITHUB_ENV"

- name: Bump the major version file
if: inputs.bump == 'major'
uses: $/bump-version
with:
version: ${{ env.NEXT_VERSION }}
version_bump_script: "bash .github/scripts/bump-major-version.sh"
push_commit: ${{ inputs.dry_run == false }}

- name: Tag the version
uses: $/tag-version
with:
version: ${{ env.NEXT_VERSION }}
tag_template: "v${VERSION}"
push_tag: ${{ inputs.dry_run == false }}

- name: Report floating v3 tag update (dry run)
if: env.IS_V3 == 'true' && inputs.dry_run == true
shell: bash
run: |
echo "[dry-run] Would move floating tag v3 to $(git rev-parse HEAD)" >> "$GITHUB_STEP_SUMMARY"

- name: Create a new signed v3 tag
if: env.IS_V3 == 'true' && inputs.dry_run == false
uses: $/git-sign
with:
command: git tag -a "v3" -f -m "Update tag" -s --local-user=${{ env.GPG_KEY_ID }}

- name: Push the v3 tag
if: env.IS_V3 == 'true' && inputs.dry_run == false
shell: bash -eux {0}
run: |
git push --force origin refs/tags/v3:refs/tags/v3

- name: Warn about floating tag retirement
if: env.IS_V3 != 'true'
shell: bash
run: |
MESSAGE="Release v${NEXT_VERSION} does not use floating tags. Remove the floating-tag-update steps (Create/Push v3 tag, and this warning) from .github/workflows/release.yml."
echo "::warning::$MESSAGE"
echo "$MESSAGE" >> "$GITHUB_STEP_SUMMARY"

- name: Create GitHub Release
if: inputs.dry_run == false
shell: bash
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
gh release create "v${NEXT_VERSION}" --title "v${NEXT_VERSION}" --generate-notes
echo "Created release v${NEXT_VERSION}" >> "$GITHUB_STEP_SUMMARY"

- name: Report release (dry run)
if: inputs.dry_run == true
shell: bash
run: |
echo "[dry-run] Would create tag v${NEXT_VERSION} and GitHub Release v${NEXT_VERSION}" >> "$GITHUB_STEP_SUMMARY"
Loading
Loading