Release #503
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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=T3 Code 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-12vcpu-macos-26 | |
| platform: mac | |
| target: dmg | |
| arch: arm64 | |
| - label: macOS x64 | |
| runner: blacksmith-12vcpu-macos-26 | |
| platform: mac | |
| target: dmg | |
| arch: x64 | |
| - label: Linux x64 | |
| runner: blacksmith-32vcpu-ubuntu-2404 | |
| platform: linux | |
| target: AppImage | |
| arch: x64 | |
| - label: Windows x64 | |
| runner: blacksmith-32vcpu-windows-2025 | |
| platform: win | |
| target: nsis | |
| arch: x64 | |
| # - label: Windows arm64 | |
| # runner: windows-11-arm | |
| # platform: win | |
| # target: nsis | |
| # arch: arm64 | |
| 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 Spectre-mitigated MSVC libs | |
| if: matrix.platform == 'win' | |
| shell: pwsh | |
| run: | | |
| $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" | |
| $installPath = & $vswhere -products * -latest -property installationPath | |
| $setupExe = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\setup.exe" | |
| $proc = Start-Process -FilePath $setupExe ` | |
| -ArgumentList "modify", "--installPath", "`"$installPath`"", "--add", ` | |
| "Microsoft.VisualStudio.Component.VC.Tools.x86.x64.Spectre", "--quiet", "--norestart" ` | |
| -Wait -PassThru -NoNewWindow | |
| if ($null -eq $proc -or $proc.ExitCode -ne 0) { | |
| $code = if ($null -ne $proc) { $proc.ExitCode } else { 1 } | |
| Write-Error "Visual Studio Installer failed with exit code $code" | |
| exit $code | |
| } | |
| - 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: Prepare Azure Trusted Signing | |
| if: matrix.platform == 'win' | |
| shell: pwsh | |
| env: | |
| AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} | |
| AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} | |
| AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} | |
| AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} | |
| AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} | |
| AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} | |
| AZURE_TRUSTED_SIGNING_PUBLISHER_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_PUBLISHER_NAME }} | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| $requiredSecrets = @( | |
| $env:AZURE_TENANT_ID, | |
| $env:AZURE_CLIENT_ID, | |
| $env:AZURE_CLIENT_SECRET, | |
| $env:AZURE_TRUSTED_SIGNING_ENDPOINT, | |
| $env:AZURE_TRUSTED_SIGNING_ACCOUNT_NAME, | |
| $env:AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME, | |
| $env:AZURE_TRUSTED_SIGNING_PUBLISHER_NAME | |
| ) | |
| if ($requiredSecrets | Where-Object { [string]::IsNullOrWhiteSpace($_) }) { | |
| Write-Host "Azure Trusted Signing disabled; skipping TrustedSigning module preparation." | |
| exit 0 | |
| } | |
| try { | |
| Install-PackageProvider ` | |
| -Name NuGet ` | |
| -MinimumVersion 2.8.5.201 ` | |
| -Force ` | |
| -Scope CurrentUser ` | |
| -ErrorAction Stop | |
| } catch { | |
| Write-Warning "Could not bootstrap NuGet package provider. Continuing because the runner may already have a usable provider. $($_.Exception.Message)" | |
| } | |
| Install-Module ` | |
| -Name TrustedSigning ` | |
| -MinimumVersion 0.5.0 ` | |
| -Force ` | |
| -AllowClobber ` | |
| -Repository PSGallery ` | |
| -Scope CurrentUser ` | |
| -ErrorAction Stop | |
| Import-Module TrustedSigning -MinimumVersion 0.5.0 -Force | |
| Get-Command Invoke-TrustedSigning -ErrorAction Stop | |
| $moduleRoots = @( | |
| [System.IO.Path]::Combine([Environment]::GetFolderPath("MyDocuments"), "PowerShell", "Modules"), | |
| [System.IO.Path]::Combine([Environment]::GetFolderPath("MyDocuments"), "WindowsPowerShell", "Modules"), | |
| [System.IO.Path]::Combine($env:ProgramFiles, "PowerShell", "Modules"), | |
| [System.IO.Path]::Combine($env:ProgramFiles, "WindowsPowerShell", "Modules") | |
| ) | |
| $modulePathEntries = @($moduleRoots + ($env:PSModulePath -split ";")) | | |
| Where-Object { $_ -and (Test-Path $_) } | | |
| Select-Object -Unique | |
| "PSModulePath=$($modulePathEntries -join ';')" >> $env:GITHUB_ENV | |
| - 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 }} | |
| AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} | |
| AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} | |
| AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} | |
| AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} | |
| AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} | |
| AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} | |
| AZURE_TRUSTED_SIGNING_PUBLISHER_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_PUBLISHER_NAME }} | |
| 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 | |
| elif [[ "${{ matrix.platform }}" == "win" ]]; then | |
| if has_all \ | |
| "$AZURE_TENANT_ID" \ | |
| "$AZURE_CLIENT_ID" \ | |
| "$AZURE_CLIENT_SECRET" \ | |
| "$AZURE_TRUSTED_SIGNING_ENDPOINT" \ | |
| "$AZURE_TRUSTED_SIGNING_ACCOUNT_NAME" \ | |
| "$AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME" \ | |
| "$AZURE_TRUSTED_SIGNING_PUBLISHER_NAME"; then | |
| echo "Windows signing enabled (Azure Trusted Signing)." | |
| args+=(--signed) | |
| else | |
| echo "Windows signing disabled (missing one or more Azure Trusted 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/*.exe" \ | |
| "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 | |
| # Enable if Windows arm64 builds are enabled. | |
| # Windows updater metadata is channel-specific (for example | |
| # "latest.yml" or "nightly.yml"). Suffix each per-arch copy so the | |
| # release job can merge matching arm64/x64 manifests back into one | |
| # canonical manifest per channel. | |
| # if [[ "${{ matrix.platform }}" == "win" ]]; then | |
| # shopt -s nullglob | |
| # for manifest in release-publish/*.yml; do | |
| # mv "$manifest" "${manifest%.yml}-win-${{ 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' }} | |
| runs-on: ubuntu-24.04 # blacksmith-8vcpu-ubuntu-2404 | |
| 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 }}" --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 | |
| steps: | |
| - id: app_token | |
| name: Mint release app token | |
| uses: actions/create-github-app-token@v2 | |
| with: | |
| app-id: ${{ secrets.RELEASE_APP_ID }} | |
| private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} | |
| owner: ${{ github.repository_owner }} | |
| - 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: Merge Windows updater manifests | |
| # run: | | |
| # shopt -s nullglob | |
| # found_windows_manifest=false | |
| # for x64_manifest in release-assets/*-win-x64.yml; do | |
| # if [[ "$(basename "$x64_manifest")" == builder-debug-* ]]; then | |
| # continue | |
| # fi | |
| # arm64_manifest="${x64_manifest/-x64.yml/-arm64.yml}" | |
| # output_manifest="${x64_manifest/-win-x64.yml/.yml}" | |
| # if [[ ! -f "$arm64_manifest" ]]; then | |
| # echo "Missing matching arm64 Windows manifest for $x64_manifest" >&2 | |
| # exit 1 | |
| # fi | |
| # found_windows_manifest=true | |
| # node scripts/merge-update-manifests.ts --platform win \ | |
| # "$arm64_manifest" \ | |
| # "$x64_manifest" \ | |
| # "$output_manifest" | |
| # rm -f "$arm64_manifest" "$x64_manifest" | |
| # done | |
| # if [[ "$found_windows_manifest" != true ]]; then | |
| # echo "No Windows updater manifests found to merge." >&2 | |
| # exit 1 | |
| # fi | |
| - 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/*.exe | |
| release-assets/*.blockmap | |
| release-assets/*.yml | |
| fail_on_unmatched_files: true | |
| token: ${{ steps.app_token.outputs.token }} | |
| - 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/*.exe | |
| release-assets/*.blockmap | |
| release-assets/*.yml | |
| fail_on_unmatched_files: true | |
| token: ${{ steps.app_token.outputs.token }} | |
| deploy_web: | |
| name: Deploy hosted web app | |
| needs: [preflight, relay_public_config, release] | |
| if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.release.result == 'success' }} | |
| runs-on: blacksmith-8vcpu-ubuntu-2404 | |
| timeout-minutes: 10 | |
| 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_RELAY_URL: ${{ needs.relay_public_config.outputs.relay_url }} | |
| VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} | |
| VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} | |
| VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} | |
| T3CODE_WEB_ROUTER_URL: ${{ vars.T3CODE_WEB_ROUTER_URL }} | |
| T3CODE_WEB_LATEST_DOMAIN: ${{ vars.T3CODE_WEB_LATEST_DOMAIN }} | |
| T3CODE_WEB_NIGHTLY_DOMAIN: ${{ vars.T3CODE_WEB_NIGHTLY_DOMAIN }} | |
| VERCEL_TEAM_SLUG: ${{ vars.VERCEL_TEAM_SLUG }} | |
| 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... | |
| - --filter=@t3tools/web... | |
| - name: Align package versions to release version | |
| run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}" | |
| - name: Refresh release lockfile | |
| run: vp install --lockfile-only --ignore-scripts | |
| - name: Deploy and alias channel | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [[ -z "${VERCEL_TOKEN:-}" || -z "${VERCEL_ORG_ID:-}" || -z "${VERCEL_PROJECT_ID:-}" ]]; then | |
| echo "Missing one or more required Vercel secrets: VERCEL_TOKEN, VERCEL_ORG_ID, VERCEL_PROJECT_ID." >&2 | |
| exit 1 | |
| fi | |
| router_url="${T3CODE_WEB_ROUTER_URL:-https://app.t3.codes}" | |
| latest_domain="${T3CODE_WEB_LATEST_DOMAIN:-latest.app.t3.codes}" | |
| nightly_domain="${T3CODE_WEB_NIGHTLY_DOMAIN:-nightly.app.t3.codes}" | |
| router_domain="${router_url#http://}" | |
| router_domain="${router_domain#https://}" | |
| router_domain="${router_domain%%/*}" | |
| if [[ "${{ needs.preflight.outputs.release_channel }}" == "stable" ]]; then | |
| channel_domain="$latest_domain" | |
| channel_name="latest" | |
| else | |
| channel_domain="$nightly_domain" | |
| channel_name="nightly" | |
| fi | |
| vercel_scope="${VERCEL_TEAM_SLUG:-$VERCEL_ORG_ID}" | |
| vercel_scope_args=(--scope "$vercel_scope") | |
| echo "Deploying hosted web app for $channel_name channel." | |
| deployment_url="$( | |
| vp dlx vercel@53.1.1 deploy \ | |
| --prod \ | |
| --skip-domain \ | |
| --yes \ | |
| --token "$VERCEL_TOKEN" \ | |
| "${vercel_scope_args[@]}" \ | |
| --build-env "APP_VERSION=${{ needs.preflight.outputs.version }}" \ | |
| --build-env "T3CODE_CLERK_PUBLISHABLE_KEY=${T3CODE_CLERK_PUBLISHABLE_KEY:-}" \ | |
| --build-env "T3CODE_CLERK_JWT_TEMPLATE=${T3CODE_CLERK_JWT_TEMPLATE:-}" \ | |
| --build-env "T3CODE_RELAY_URL=${T3CODE_RELAY_URL:-}" \ | |
| --build-env "VITE_HOSTED_APP_URL=$router_url" \ | |
| --build-env "VITE_HOSTED_APP_CHANNEL=$channel_name" | |
| )" | |
| echo "Aliasing $deployment_url to $channel_domain." | |
| vp dlx vercel@53.1.1 alias set "$deployment_url" "$channel_domain" \ | |
| --token "$VERCEL_TOKEN" \ | |
| "${vercel_scope_args[@]}" | |
| if [[ "$channel_name" == "latest" && -n "$router_domain" && "$router_domain" != "$channel_domain" ]]; then | |
| echo "Aliasing $deployment_url to router domain $router_domain." | |
| vp dlx vercel@53.1.1 alias set "$deployment_url" "$router_domain" \ | |
| --token "$VERCEL_TOKEN" \ | |
| "${vercel_scope_args[@]}" | |
| fi | |
| 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 | |
| steps: | |
| - id: app_token | |
| name: Mint release app token | |
| uses: actions/create-github-app-token@v2 | |
| with: | |
| app-id: ${{ secrets.RELEASE_APP_ID }} | |
| private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} | |
| owner: ${{ github.repository_owner }} | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: main | |
| fetch-depth: 0 | |
| token: ${{ steps.app_token.outputs.token }} | |
| persist-credentials: true | |
| - id: app_bot | |
| name: Resolve GitHub App bot identity | |
| env: | |
| GH_TOKEN: ${{ steps.app_token.outputs.token }} | |
| APP_SLUG: ${{ steps.app_token.outputs.app-slug }} | |
| run: | | |
| user_id="$(gh api "/users/${APP_SLUG}[bot]" --jq .id)" | |
| echo "name=${APP_SLUG}[bot]" >> "$GITHUB_OUTPUT" | |
| echo "email=${user_id}+${APP_SLUG}[bot]@users.noreply.github.com" >> "$GITHUB_OUTPUT" | |
| - 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 "${{ steps.app_bot.outputs.name }}" | |
| git config user.email "${{ steps.app_bot.outputs.email }}" | |
| 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 | |
| announce_discord: | |
| name: Announce release on Discord | |
| if: | | |
| always() && !cancelled() && | |
| needs.preflight.result == 'success' && | |
| needs.relay_public_config.result == 'success' && | |
| needs.release.result == 'success' && | |
| needs.deploy_web.result == 'success' && | |
| (needs.finalize.result == 'success' || needs.finalize.result == 'skipped') | |
| needs: [preflight, relay_public_config, release, deploy_web, finalize] | |
| runs-on: blacksmith-8vcpu-ubuntu-2404 | |
| timeout-minutes: 10 | |
| 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: Announce prerelease on Discord | |
| if: needs.preflight.outputs.is_prerelease == 'true' | |
| continue-on-error: true | |
| env: | |
| DISCORD_MENTION_ROLE_ID: ${{ secrets.DISCORD_RELEASE_NIGHTLY_ROLE_ID }} | |
| DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_RELEASE_WEBHOOK_URL }} | |
| run: | | |
| node scripts/notify-discord-release.ts prerelease \ | |
| --role-id "$DISCORD_MENTION_ROLE_ID" \ | |
| --release-name "${{ needs.preflight.outputs.release_name }}" \ | |
| --release-version "${{ needs.preflight.outputs.version }}" \ | |
| --tag "${{ needs.preflight.outputs.tag }}" \ | |
| --release-url "https://github.com/${{ github.repository }}/releases/tag/${{ needs.preflight.outputs.tag }}" | |
| - name: Announce latest release on Discord | |
| if: needs.preflight.outputs.make_latest == 'true' | |
| continue-on-error: true | |
| env: | |
| DISCORD_MENTION_ROLE_ID: ${{ secrets.DISCORD_RELEASE_LATEST_ROLE_ID }} | |
| DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_RELEASE_WEBHOOK_URL }} | |
| run: | | |
| node scripts/notify-discord-release.ts latest \ | |
| --role-id "$DISCORD_MENTION_ROLE_ID" \ | |
| --release-name "${{ needs.preflight.outputs.release_name }}" \ | |
| --release-version "${{ needs.preflight.outputs.version }}" \ | |
| --tag "${{ needs.preflight.outputs.tag }}" \ | |
| --release-url "https://github.com/${{ github.repository }}/releases/tag/${{ needs.preflight.outputs.tag }}" |