Skip to content

Release

Release #172

Workflow file for this run

name: Release
on:
push:
tags:
- "v*.*.*"
- "!v*-nightly.*"
schedule:
- cron: "0 */3 * * *"
workflow_dispatch:
inputs:
channel:
description: "Release channel"
required: false
default: stable
type: choice
options:
- stable
- nightly
version:
description: "Release version (for example 1.2.3 or v1.2.3)"
required: false
type: string
permissions:
contents: read
id-token: none
jobs:
check_changes:
name: Check for changes since last nightly
if: github.event_name == 'schedule'
runs-on: blacksmith-8vcpu-ubuntu-2404
outputs:
has_changes: ${{ steps.check.outputs.has_changes }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- id: check
name: Compare HEAD to last nightly tag
run: |
last_nightly_tag=$(git tag --list 'v*-nightly.*' 'nightly-v*' --sort=-creatordate | head -n 1)
if [[ -z "$last_nightly_tag" ]]; then
echo "No previous nightly tag found. Proceeding with release."
echo "has_changes=true" >> "$GITHUB_OUTPUT"
exit 0
fi
last_nightly_sha=$(git rev-parse "$last_nightly_tag^{commit}")
head_sha=$(git rev-parse HEAD)
if [[ "$last_nightly_sha" == "$head_sha" ]]; then
echo "No changes on main since last nightly release ($last_nightly_tag). Skipping."
echo "has_changes=false" >> "$GITHUB_OUTPUT"
else
echo "Changes detected on main since $last_nightly_tag ($last_nightly_sha → $head_sha). Proceeding."
echo "has_changes=true" >> "$GITHUB_OUTPUT"
fi
preflight:
name: Preflight
needs: [check_changes]
if: |
!failure() && !cancelled() &&
(github.event_name != 'schedule' || needs.check_changes.outputs.has_changes == 'true')
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 10
outputs:
release_channel: ${{ steps.release_meta.outputs.release_channel }}
version: ${{ steps.release_meta.outputs.version }}
tag: ${{ steps.release_meta.outputs.tag }}
release_name: ${{ steps.release_meta.outputs.name }}
short_sha: ${{ steps.release_meta.outputs.short_sha }}
previous_tag: ${{ steps.previous_tag.outputs.previous_tag }}
cli_dist_tag: ${{ steps.release_meta.outputs.cli_dist_tag }}
is_prerelease: ${{ steps.release_meta.outputs.is_prerelease }}
make_latest: ${{ steps.release_meta.outputs.make_latest }}
ref: ${{ github.sha }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Vite+
uses: voidzero-dev/setup-vp@v1
with:
node-version-file: package.json
cache: true
run-install: true
- name: Ensure Electron runtime is installed
run: vp run --filter @t3tools/desktop ensure:electron
- id: release_meta
name: Resolve release version
shell: bash
env:
DISPATCH_CHANNEL: ${{ github.event.inputs.channel }}
DISPATCH_VERSION: ${{ github.event.inputs.version }}
NIGHTLY_DATE: ${{ github.run_started_at }}
NIGHTLY_SHA: ${{ github.sha }}
NIGHTLY_RUN_NUMBER: ${{ github.run_number }}
run: |
if [[ "${GITHUB_EVENT_NAME}" == "schedule" || ( "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && "${DISPATCH_CHANNEL:-stable}" == "nightly" ) ]]; then
nightly_date="$(date -u -d "$NIGHTLY_DATE" +%Y%m%d)"
node scripts/resolve-nightly-release.ts \
--date "$nightly_date" \
--run-number "$NIGHTLY_RUN_NUMBER" \
--sha "$NIGHTLY_SHA" \
--github-output
echo "release_channel=nightly" >> "$GITHUB_OUTPUT"
echo "cli_dist_tag=nightly" >> "$GITHUB_OUTPUT"
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
echo "make_latest=false" >> "$GITHUB_OUTPUT"
else
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
raw="${DISPATCH_VERSION}"
if [[ -z "$raw" ]]; then
echo "workflow_dispatch stable releases require the version input." >&2
exit 1
fi
else
raw="${GITHUB_REF_NAME}"
fi
version="${raw#v}"
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then
echo "Invalid release version: $raw" >&2
exit 1
fi
echo "release_channel=stable" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "tag=v$version" >> "$GITHUB_OUTPUT"
echo "name=Pearce Codes v$version" >> "$GITHUB_OUTPUT"
echo "cli_dist_tag=latest" >> "$GITHUB_OUTPUT"
if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
echo "make_latest=true" >> "$GITHUB_OUTPUT"
else
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
echo "make_latest=false" >> "$GITHUB_OUTPUT"
fi
fi
- name: Check
run: vp check
- name: Typecheck
run: vp run typecheck
- name: Test
run: vp run test
- id: previous_tag
name: Resolve previous release tag
run: |
node scripts/resolve-previous-release-tag.ts \
--channel "${{ steps.release_meta.outputs.release_channel }}" \
--current-tag "${{ steps.release_meta.outputs.tag }}" \
--github-output
relay_public_config:
name: Resolve T3 Cloud public config
needs: preflight
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' }}
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 5
environment:
name: production
outputs:
clerk_publishable_key: ${{ steps.public_config.outputs.clerk_publishable_key }}
clerk_jwt_template: ${{ steps.public_config.outputs.clerk_jwt_template }}
clerk_cli_oauth_client_id: ${{ steps.public_config.outputs.clerk_cli_oauth_client_id }}
relay_url: ${{ steps.public_config.outputs.relay_url }}
env:
RELAY_DOMAIN: ${{ vars.RELAY_DOMAIN }}
RELAY_API_ZONE_NAME: ${{ vars.RELAY_API_ZONE_NAME }}
CLERK_PUBLISHABLE_KEY: ${{ vars.CLERK_PUBLISHABLE_KEY }}
CLERK_JWT_TEMPLATE: ${{ vars.CLERK_JWT_TEMPLATE }}
CLERK_CLI_OAUTH_CLIENT_ID: ${{ vars.CLERK_CLI_OAUTH_CLIENT_ID }}
steps:
- id: public_config
name: Resolve production relay public config
shell: bash
run: |
set -euo pipefail
relay_domain="${RELAY_DOMAIN:-}"
if [[ -z "$relay_domain" && -n "${RELAY_API_ZONE_NAME:-}" ]]; then
relay_domain="relay.$RELAY_API_ZONE_NAME"
fi
required=(
relay_domain
CLERK_PUBLISHABLE_KEY
CLERK_JWT_TEMPLATE
CLERK_CLI_OAUTH_CLIENT_ID
)
missing=()
for name in "${required[@]}"; do
if [[ -z "${!name:-}" ]]; then
missing+=("$name")
fi
done
if (( ${#missing[@]} > 0 )); then
printf 'Missing required relay deployment configuration: %s\n' "${missing[*]}" >&2
exit 1
fi
echo "clerk_publishable_key=$CLERK_PUBLISHABLE_KEY" >> "$GITHUB_OUTPUT"
echo "clerk_jwt_template=$CLERK_JWT_TEMPLATE" >> "$GITHUB_OUTPUT"
echo "clerk_cli_oauth_client_id=$CLERK_CLI_OAUTH_CLIENT_ID" >> "$GITHUB_OUTPUT"
echo "relay_url=https://$relay_domain" >> "$GITHUB_OUTPUT"
build:
name: Build ${{ matrix.label }}
needs: [preflight, relay_public_config]
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
env:
T3CODE_CLERK_PUBLISHABLE_KEY: ${{ needs.relay_public_config.outputs.clerk_publishable_key }}
T3CODE_CLERK_JWT_TEMPLATE: ${{ needs.relay_public_config.outputs.clerk_jwt_template }}
T3CODE_CLERK_CLI_OAUTH_CLIENT_ID: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }}
T3CODE_RELAY_URL: ${{ needs.relay_public_config.outputs.relay_url }}
strategy:
fail-fast: false
matrix:
include:
- label: macOS arm64
runner: blacksmith-6vcpu-macos-15
platform: mac
target: dmg
arch: arm64
- label: macOS x64
runner: macos-15-intel
platform: mac
target: dmg
arch: x64
- label: Linux x64
runner: blacksmith-8vcpu-ubuntu-2404
platform: linux
target: AppImage
arch: x64
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.ref }}
fetch-depth: 0
- name: Setup Vite+
uses: voidzero-dev/setup-vp@v1
with:
node-version-file: package.json
cache: true
run-install: true
- name: Align package versions to release version
run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}"
- name: Install ImageMagick
if: matrix.platform == 'linux'
shell: bash
run: |
if ! command -v magick >/dev/null 2>&1 && ! command -v convert >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y imagemagick
fi
if command -v magick >/dev/null 2>&1; then
magick -version
else
convert -version
fi
- name: Build desktop artifact
shell: bash
env:
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
run: |
args=(
--platform "${{ matrix.platform }}"
--target "${{ matrix.target }}"
--arch "${{ matrix.arch }}"
--build-version "${{ needs.preflight.outputs.version }}"
--verbose
)
has_all() {
for value in "$@"; do
if [[ -z "$value" ]]; then
return 1
fi
done
return 0
}
if [[ "${{ matrix.platform }}" == "mac" ]]; then
if has_all "$CSC_LINK" "$CSC_KEY_PASSWORD" "$APPLE_API_KEY" "$APPLE_API_KEY_ID" "$APPLE_API_ISSUER"; then
key_path="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY_ID}.p8"
printf '%s' "$APPLE_API_KEY" > "$key_path"
export APPLE_API_KEY="$key_path"
echo "macOS signing enabled."
args+=(--signed)
else
echo "macOS signing disabled (missing one or more Apple signing secrets)."
fi
else
echo "Signing disabled for ${{ matrix.platform }}."
fi
vp run dist:desktop:artifact "${args[@]}"
- name: Collect release assets
shell: bash
run: |
set -euo pipefail
mkdir -p release-publish
shopt -s nullglob
for pattern in \
"release/*.dmg" \
"release/*.zip" \
"release/*.AppImage" \
"release/*.blockmap" \
"release/*.yml"; do
for file in $pattern; do
cp "$file" release-publish/
done
done
if [[ "${{ matrix.platform }}" == "mac" && "${{ matrix.arch }}" != "arm64" ]]; then
shopt -s nullglob
for manifest in release-publish/*-mac.yml; do
mv "$manifest" "${manifest%.yml}-${{ matrix.arch }}.yml"
done
fi
- name: Upload build artifacts
uses: actions/upload-artifact@v7
with:
name: desktop-${{ matrix.platform }}-${{ matrix.arch }}
path: release-publish/*
if-no-files-found: error
publish_cli:
name: Publish CLI to npm
needs: [preflight, relay_public_config, build]
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.build.result == 'success' }}
# npm Trusted Publishing currently supports GitHub-hosted runners only.
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
id-token: write
env:
T3CODE_CLERK_PUBLISHABLE_KEY: ${{ needs.relay_public_config.outputs.clerk_publishable_key }}
T3CODE_CLERK_JWT_TEMPLATE: ${{ needs.relay_public_config.outputs.clerk_jwt_template }}
T3CODE_CLERK_CLI_OAUTH_CLIENT_ID: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }}
T3CODE_RELAY_URL: ${{ needs.relay_public_config.outputs.relay_url }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.ref }}
- name: Setup Vite+
uses: voidzero-dev/setup-vp@v1
with:
node-version-file: package.json
cache: true
run-install: |
args:
- --filter=t3...
- --filter=@t3tools/web...
- --filter=@t3tools/scripts...
- name: Align package versions to release version
run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}"
- name: Build web package
run: vp run --filter @t3tools/web build
- name: Build CLI package
run: vp run --filter t3 build
- name: Publish CLI package
run: node apps/server/scripts/cli.ts publish --tag "${{ needs.preflight.outputs.cli_dist_tag }}" --app-version "${{ needs.preflight.outputs.version }}" --provenance --verbose
release:
name: Publish GitHub Release
needs: [preflight, build, publish_cli]
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.build.result == 'success' && needs.publish_cli.result == 'success' }}
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 10
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.ref }}
- name: Setup Vite+
uses: voidzero-dev/setup-vp@v1
with:
node-version-file: package.json
cache: true
run-install: |
args:
- --filter=@t3tools/scripts...
- name: Download all desktop artifacts
uses: actions/download-artifact@v8
with:
pattern: desktop-*
merge-multiple: true
path: release-assets
- name: Merge macOS updater manifests
run: |
shopt -s nullglob
for x64_manifest in release-assets/*-mac-x64.yml; do
arm64_manifest="${x64_manifest%-x64.yml}.yml"
if [[ -f "$arm64_manifest" ]]; then
node scripts/merge-update-manifests.ts --platform mac "$arm64_manifest" "$x64_manifest"
rm -f "$x64_manifest"
fi
done
- name: Publish release
if: needs.preflight.outputs.previous_tag != ''
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.preflight.outputs.tag }}
target_commitish: ${{ needs.preflight.outputs.ref }}
name: ${{ needs.preflight.outputs.release_name }}
generate_release_notes: true
previous_tag: ${{ needs.preflight.outputs.previous_tag }}
prerelease: ${{ needs.preflight.outputs.is_prerelease }}
make_latest: ${{ needs.preflight.outputs.make_latest }}
files: |
release-assets/*.dmg
release-assets/*.zip
release-assets/*.AppImage
release-assets/*.blockmap
release-assets/*.yml
fail_on_unmatched_files: true
- name: Publish first release
if: needs.preflight.outputs.previous_tag == ''
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.preflight.outputs.tag }}
target_commitish: ${{ needs.preflight.outputs.ref }}
name: ${{ needs.preflight.outputs.release_name }}
generate_release_notes: true
prerelease: ${{ needs.preflight.outputs.is_prerelease }}
make_latest: ${{ needs.preflight.outputs.make_latest }}
files: |
release-assets/*.dmg
release-assets/*.zip
release-assets/*.AppImage
release-assets/*.blockmap
release-assets/*.yml
fail_on_unmatched_files: true
finalize:
name: Finalize release
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.release.result == 'success' && needs.preflight.outputs.release_channel == 'stable' }}
needs: [preflight, release]
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 10
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: main
fetch-depth: 0
persist-credentials: true
- name: Setup Vite+
uses: voidzero-dev/setup-vp@v1
with:
node-version-file: package.json
cache: true
run-install: |
args:
- --filter=@t3tools/scripts...
- --filter=@t3tools/oxlint-plugin-t3code...
- id: update_versions
name: Update version strings
env:
RELEASE_VERSION: ${{ needs.preflight.outputs.version }}
run: node scripts/update-release-package-versions.ts "$RELEASE_VERSION" --github-output
- name: Format package.json files
if: steps.update_versions.outputs.changed == 'true'
run: vp fmt apps/server/package.json apps/desktop/package.json apps/web/package.json packages/contracts/package.json
- name: Refresh lockfile
if: steps.update_versions.outputs.changed == 'true'
run: vp install --lockfile-only --ignore-scripts
- name: Commit and push version bump
if: steps.update_versions.outputs.changed == 'true'
shell: bash
env:
RELEASE_TAG: ${{ needs.preflight.outputs.tag }}
run: |
if git diff --quiet -- apps/server/package.json apps/desktop/package.json apps/web/package.json packages/contracts/package.json pnpm-lock.yaml; then
echo "No version changes to commit."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add apps/server/package.json apps/desktop/package.json apps/web/package.json packages/contracts/package.json pnpm-lock.yaml
git commit -m "chore(release): prepare $RELEASE_TAG"
git push origin HEAD:main