Skip to content

Release

Release #55

Workflow file for this run

name: "Release"
permissions:
contents: write
actions: write
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
create_release:
description: "Bump version, create tag, and upload built assets"
required: false
default: false
type: boolean
version:
description: "Version to release when enabled (example: 0.6.0 or v0.6.0)"
required: false
type: string
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
resolve-tag:
name: Resolve release tag
runs-on: ubuntu-latest
outputs:
release_tag: ${{ steps.resolve.outputs.release_tag }}
checkout_ref: ${{ steps.resolve.outputs.checkout_ref }}
publish_release: ${{ steps.resolve.outputs.publish_release }}
steps:
- name: Determine release mode
id: mode
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
if [ "${{ github.event.inputs.create_release }}" = "true" ]; then
RAW_INPUT="${{ github.event.inputs.version }}"
if [ -z "$RAW_INPUT" ]; then
echo "version is required when create_release is enabled."
exit 1
fi
NORMALIZED_VERSION="${RAW_INPUT#v}"
if [[ ! "$NORMALIZED_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Invalid version: '$RAW_INPUT'. Use SemVer like 7.3.0 or v7.3.0."
exit 1
fi
RELEASE_TAG="v${NORMALIZED_VERSION}"
MODE="manual_release"
else
RELEASE_TAG=""
MODE="build_only"
fi
else
RELEASE_TAG="${{ github.ref_name }}"
MODE="tag_push"
fi
echo "release_tag=${RELEASE_TAG}" >> "$GITHUB_OUTPUT"
echo "mode=${MODE}" >> "$GITHUB_OUTPUT"
- name: Checkout repository
if: ${{ steps.mode.outputs.mode == 'manual_release' }}
uses: actions/checkout@v5
with:
fetch-depth: 0
fetch-tags: true
submodules: true
- name: Detect existing tag and release
id: existing
if: ${{ steps.mode.outputs.mode == 'manual_release' }}
env:
RELEASE_TAG: ${{ steps.mode.outputs.release_tag }}
GITHUB_TOKEN: ${{ github.token }}
run: |
TAG_EXISTS=false
if git ls-remote --exit-code --tags origin "refs/tags/${RELEASE_TAG}" >/dev/null 2>&1; then
TAG_EXISTS=true
fi
echo "tag_exists=${TAG_EXISTS}" >> "$GITHUB_OUTPUT"
if [ "${TAG_EXISTS}" = "true" ]; then
echo "Tag '${RELEASE_TAG}' already exists on origin; refreshing the release from the existing tag."
else
if gh release view "${RELEASE_TAG}" --repo "${{ github.repository }}" >/dev/null 2>&1; then
echo "Release '${RELEASE_TAG}' exists without a tag, which is unexpected." >&2
exit 1
fi
fi
- name: Configure git identity
if: ${{ steps.mode.outputs.mode == 'manual_release' && steps.existing.outputs.tag_exists != 'true' }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Update codebase version tags
if: ${{ steps.mode.outputs.mode == 'manual_release' && steps.existing.outputs.tag_exists != 'true' }}
env:
VERSION: ${{ steps.mode.outputs.release_tag }}
run: |
python3 - <<'PY'
import json
import os
import pathlib
import re
repo_root = pathlib.Path(".")
version = os.environ["VERSION"].removeprefix("v")
flake_path = repo_root / "flake.nix"
flake_content = flake_path.read_text(encoding="utf-8")
updated_flake_content, replacements = re.subn(
r'packageVersion = "[0-9]+\.[0-9]+\.[0-9]+";',
f'packageVersion = "{version}";',
flake_content,
count=1,
)
if replacements != 1:
raise SystemExit("Failed to update packageVersion in flake.nix")
flake_path.write_text(updated_flake_content, encoding="utf-8")
vcpkg_path = repo_root / "vcpkg.json"
vcpkg_data = json.loads(vcpkg_path.read_text(encoding="utf-8"))
vcpkg_data["version"] = version
vcpkg_path.write_text(
json.dumps(vcpkg_data, indent=2) + "\n",
encoding="utf-8",
)
PY
- name: Commit version updates
if: ${{ steps.mode.outputs.mode == 'manual_release' && steps.existing.outputs.tag_exists != 'true' }}
env:
RELEASE_TAG: ${{ steps.mode.outputs.release_tag }}
run: |
git add flake.nix vcpkg.json
if git diff --cached --quiet; then
echo "Version files were already up to date."
else
git commit -m "chore(release): prepare ${RELEASE_TAG}"
git push origin "HEAD:${{ github.ref_name }}"
fi
- name: Create and push release tag
if: ${{ steps.mode.outputs.mode == 'manual_release' && steps.existing.outputs.tag_exists != 'true' }}
env:
RELEASE_TAG: ${{ steps.mode.outputs.release_tag }}
run: |
git tag -a "${RELEASE_TAG}" -m "Release ${RELEASE_TAG}"
git push origin "${RELEASE_TAG}"
- name: Resolve and validate release tag
id: resolve
env:
MODE: ${{ steps.mode.outputs.mode }}
MODE_RELEASE_TAG: ${{ steps.mode.outputs.release_tag }}
run: |
PUBLISH_RELEASE="true"
if [ "$MODE" = "build_only" ]; then
RELEASE_TAG=""
CHECKOUT_REF="${{ github.ref }}"
PUBLISH_RELEASE="false"
else
RELEASE_TAG="${MODE_RELEASE_TAG#refs/tags/}"
if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Invalid tag '$RELEASE_TAG'. Expected vMAJOR.MINOR.PATCH"
exit 1
fi
TAG_READY="false"
for attempt in $(seq 1 12); do
if git ls-remote --exit-code --tags "https://github.com/${{ github.repository }}.git" "refs/tags/${RELEASE_TAG}" >/dev/null 2>&1; then
TAG_READY="true"
break
fi
echo "Tag refs/tags/${RELEASE_TAG} not visible on origin yet (attempt ${attempt}/12); waiting 5s..."
sleep 5
done
if [ "$TAG_READY" != "true" ]; then
echo "Tag refs/tags/${RELEASE_TAG} does not exist on origin."
exit 1
fi
CHECKOUT_REF="refs/tags/${RELEASE_TAG}"
fi
echo "release_tag=${RELEASE_TAG}" >> "$GITHUB_OUTPUT"
echo "checkout_ref=${CHECKOUT_REF}" >> "$GITHUB_OUTPUT"
echo "publish_release=${PUBLISH_RELEASE}" >> "$GITHUB_OUTPUT"
build-linux:
name: Build Linux binaries
uses: ./.github/workflows/build_linux_tools.yml
needs: resolve-tag
with:
checkout_ref: ${{ needs.resolve-tag.outputs.checkout_ref }}
secrets: inherit
build-windows:
name: Build Windows binaries
uses: ./.github/workflows/build_windows_tools.yml
needs: resolve-tag
with:
checkout_ref: ${{ needs.resolve-tag.outputs.checkout_ref }}
secrets: inherit
build-macos:
name: Build macOS binaries
uses: ./.github/workflows/build_macos_tools.yml
needs: resolve-tag
with:
checkout_ref: ${{ needs.resolve-tag.outputs.checkout_ref }}
release_tag: ${{ needs.resolve-tag.outputs.release_tag }}
secrets: inherit
upload-release-assets:
name: Upload release assets
needs: [resolve-tag, build-linux, build-windows, build-macos]
if: ${{ needs.resolve-tag.outputs.publish_release == 'true' }}
runs-on: ubuntu-latest
env:
RELEASE_TAG: ${{ needs.resolve-tag.outputs.release_tag }}
GITHUB_TOKEN: ${{ github.token }}
steps:
- name: Checkout release tag history
uses: actions/checkout@v5
with:
ref: ${{ needs.resolve-tag.outputs.checkout_ref }}
fetch-depth: 0
fetch-tags: true
- name: Download Linux artifacts
uses: actions/download-artifact@v8
with:
name: tbc-tools_linux_x86_64
path: ./artifacts/
- name: Download Linux ARM64 artifacts
uses: actions/download-artifact@v8
with:
name: tbc-tools_linux_arm64
path: ./artifacts/
- name: Download Windows artifacts
uses: actions/download-artifact@v8
with:
name: tbc-tools_windows_x86_64
path: ./artifacts/windows/
- name: Download Windows ARM64 artifacts
uses: actions/download-artifact@v8
with:
name: tbc-tools_windows_arm64
path: ./artifacts/windows-arm/
- name: Download macOS x86_64 artifacts
uses: actions/download-artifact@v8
with:
name: tbc-tools_macos_x86_64
path: ./artifacts/
- name: Download macOS arm64 artifacts
uses: actions/download-artifact@v8
with:
name: tbc-tools_macos_arm64
path: ./artifacts/
- name: Download macOS universal artifact
uses: actions/download-artifact@v8
with:
name: tbc-tools_macos_universal
path: ./artifacts/
- name: Build versioned release assets
run: |
mkdir -p artifacts/release
mv ./artifacts/tbc-tools_linux_x86_64.tar.xz "./artifacts/release/tbc-tools_${RELEASE_TAG}_linux_x86_64.tar.xz"
mv ./artifacts/tbc-tools_linux_arm64.tar.xz "./artifacts/release/tbc-tools_${RELEASE_TAG}_linux_arm64.tar.xz"
cd artifacts/windows
zip -r "../release/tbc-tools_${RELEASE_TAG}_windows_x86_64.zip" .
cd ../windows-arm
zip -r "../release/tbc-tools_${RELEASE_TAG}_windows_arm64.zip" .
cd ../..
mv ./artifacts/tbc-tools_x86_64.dmg "./artifacts/release/tbc-tools_${RELEASE_TAG}_macos_x86_64.dmg"
mv ./artifacts/tbc-tools_arm64.dmg "./artifacts/release/tbc-tools_${RELEASE_TAG}_macos_arm64.dmg"
mv ./artifacts/tbc-tools_universal.dmg "./artifacts/release/tbc-tools_${RELEASE_TAG}_macos_universal.dmg"
- name: Determine previous release tag
id: previous-tag
run: |
PREVIOUS_TAG="$(git describe --tags --abbrev=0 --match "v*" "${RELEASE_TAG}^" 2>/dev/null || true)"
if [ -n "$PREVIOUS_TAG" ]; then
echo "Using release notes range: ${PREVIOUS_TAG}..${RELEASE_TAG}"
else
echo "No previous release tag found before ${RELEASE_TAG}; generating notes from full history."
fi
echo "previous_tag=${PREVIOUS_TAG}" >> "$GITHUB_OUTPUT"
- name: Generate release notes
id: generate-notes
env:
PREVIOUS_TAG: ${{ steps.previous-tag.outputs.previous_tag }}
run: |
API_NOTES_FILE="$(mktemp)"
if [ -n "$PREVIOUS_TAG" ]; then
gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
"/repos/${{ github.repository }}/releases/generate-notes" \
-f tag_name="$RELEASE_TAG" \
-f previous_tag_name="$PREVIOUS_TAG" \
--jq '.body' > "$API_NOTES_FILE"
else
gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
"/repos/${{ github.repository }}/releases/generate-notes" \
-f tag_name="$RELEASE_TAG" \
--jq '.body' > "$API_NOTES_FILE"
fi
# GitHub's generate-notes API only lists merged pull requests and
# "New Contributors", which omits direct commits pushed to the
# default branch. Build a granular, commit-based changelog from git
# history (grouped by component, each change citing its hash) and
# preserve only the "New Contributors" section from the API output.
NOTES_FILE="$(mktemp)"
python3 scripts/release/generate_release_notes.py \
--repo "${{ github.repository }}" \
--tag "$RELEASE_TAG" \
--previous-tag "$PREVIOUS_TAG" \
--api-notes-file "$API_NOTES_FILE" \
--output "$NOTES_FILE"
echo "Final release notes:"
cat "$NOTES_FILE"
echo "notes_file=$NOTES_FILE" >> "$GITHUB_OUTPUT"
- name: Ensure published GitHub release exists
env:
NOTES_FILE: ${{ steps.generate-notes.outputs.notes_file }}
run: |
if gh release view "$RELEASE_TAG" --repo "${{ github.repository }}" >/dev/null 2>&1; then
IS_DRAFT="$(gh release view "$RELEASE_TAG" --repo "${{ github.repository }}" --json isDraft --jq '.isDraft')"
if [ "$IS_DRAFT" = "true" ]; then
echo "Release $RELEASE_TAG exists as draft; publishing it."
gh release edit "$RELEASE_TAG" --repo "${{ github.repository }}" --draft=false --notes-file "$NOTES_FILE"
else
echo "Release $RELEASE_TAG already exists; refreshing release notes."
gh release edit "$RELEASE_TAG" --repo "${{ github.repository }}" --notes-file "$NOTES_FILE"
fi
else
gh release create "$RELEASE_TAG" \
--repo "${{ github.repository }}" \
--title "$RELEASE_TAG" \
--notes-file "$NOTES_FILE"
fi
- name: Upload release assets
run: |
gh release upload "$RELEASE_TAG" ./artifacts/release/* --repo "${{ github.repository }}" --clobber