From d574eaad0a295bf0983f58cec298143dc26fd004 Mon Sep 17 00:00:00 2001 From: John McLaren Date: Mon, 27 Jul 2026 16:58:20 +0000 Subject: [PATCH 1/2] Purge fork-unrunnable workflows, add desktop artifact builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every workflow in this fork now has to clear two bars: it runs on standard GitHub-hosted runners, and it needs no credentials beyond the automatic GITHUB_TOKEN. The fork's run history shows what the old set actually did here — 30+ consecutive scheduled Release runs cancelled after the 24h queue timeout because `blacksmith-*` labels never resolve, and the one push-triggered CI run on record went the same way. Deleted (needs credentials and/or Blacksmith runners): - release.yml (Cloudflare + Clerk + Apple + Azure + npm OIDC + release GitHub App; its 3-hourly nightly cron was pure noise here) - deploy-relay.yml (Cloudflare, PlanetScale, Axiom, Clerk, APNs; fired on every push to main) - mobile-eas-preview.yml, mobile-eas-production.yml (EXPO_TOKEN) - mobile-showcase-screenshots.yml (Blacksmith macOS/Linux runners) Deleted (upstream community governance, no value in a fork whose PRs are all authored by the owner and its agents), both on pull_request_target: - pr-vouch.yml + .github/VOUCHED.td (trust-gates external contributors against upstream's contributor list) - pr-size.yml (its sync-label-definitions job was dead code besides: `if: github.event_name != 'pull_request_target'` on a workflow whose only trigger is pull_request_target) Kept: ci.yml (already fork-adapted) and issue-labels.yml (GITHUB_TOKEN only, and it bootstraps the labels the issue forms apply — verified the labels API answers for this repo). Added desktop-artifacts.yml: builds the same four platforms upstream's release matrix covers — macOS arm64/x64 DMG, Linux x64 AppImage, Windows x64 NSIS — on every push to main and on dispatch, unsigned, uploaded as workflow artifacts with 14-day retention. It reuses upstream's build job minus everything credentialed, keeping the two secret-free pieces that matter: the Linux node-pty prebuild bundled into the Windows artifact (non-fatal when missing) and the Spectre-mitigated MSVC libs install. `--signed` is never passed, which is what keeps build-desktop-artifact.ts off the signing paths. Dropping release.yml also strands the guard in infra/relay/scripts/deploy.test.ts that read that file off disk — it would have failed `vp run test` — so it is removed with a note on restoring it. Docs that describe the deleted workflows (ci.md, release.md, mobile-app-store-screenshots.md, infra/relay/README.md) get fork notes rather than deletion, since they stay useful as upstream reference on rebases, and FORK.md section F records the standing rule so a rebase does not silently reintroduce a workflow that cannot run. All three remaining workflows pass actionlint with shellcheck enabled. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Uc2sbze61m6FQuyfkZBS9Y --- .github/VOUCHED.td | 35 - .github/workflows/deploy-relay.yml | 80 -- .github/workflows/desktop-artifacts.yml | 207 ++++ .github/workflows/mobile-eas-preview.yml | 83 -- .github/workflows/mobile-eas-production.yml | 113 -- .../workflows/mobile-showcase-screenshots.yml | 134 --- .github/workflows/pr-size.yml | 295 ----- .github/workflows/pr-vouch.yml | 199 ---- .github/workflows/release.yml | 1024 ----------------- CONTRIBUTING.md | 4 - FORK.md | 62 +- README.md | 6 + docs/operations/ci.md | 12 +- .../mobile-app-store-screenshots.md | 4 + docs/operations/release.md | 7 + infra/relay/README.md | 5 + infra/relay/scripts/deploy.test.ts | 27 +- 17 files changed, 293 insertions(+), 2004 deletions(-) delete mode 100644 .github/VOUCHED.td delete mode 100644 .github/workflows/deploy-relay.yml create mode 100644 .github/workflows/desktop-artifacts.yml delete mode 100644 .github/workflows/mobile-eas-preview.yml delete mode 100644 .github/workflows/mobile-eas-production.yml delete mode 100644 .github/workflows/mobile-showcase-screenshots.yml delete mode 100644 .github/workflows/pr-size.yml delete mode 100644 .github/workflows/pr-vouch.yml delete mode 100644 .github/workflows/release.yml diff --git a/.github/VOUCHED.td b/.github/VOUCHED.td deleted file mode 100644 index 73376110d9a1..000000000000 --- a/.github/VOUCHED.td +++ /dev/null @@ -1,35 +0,0 @@ -# Trust list for this repository. -# -# External contributors listed here are treated as trusted by the vouch -# workflow. Collaborators with write access are automatically trusted and -# do not need to be duplicated in this file. -# -# Syntax: -# github:username -# -github:username reason for denouncement -# -# Keep entries sorted alphabetically. -github:adityavardhansharma -github:binbandit -github:chuks-qua -github:cursoragent -github:gbarros-dev -github:github-actions[bot] -github:hwanseoc -github:jamesx0416 -github:jasonLaster -github:JoeEverest -github:maria-rcks -github:nmggithub -github:Noojuno -github:notkainoa -github:PatrickBauer -github:realAhmedRoach -github:shiroyasha9 -github:Yash-Singh1 -github:eggfriedrice24 -github:Ymit24 -github:shivamhwp -github:jappyjan -github:justsomelegs -github:UtkarshUsername diff --git a/.github/workflows/deploy-relay.yml b/.github/workflows/deploy-relay.yml deleted file mode 100644 index 94d4af17e41a..000000000000 --- a/.github/workflows/deploy-relay.yml +++ /dev/null @@ -1,80 +0,0 @@ -name: Deploy T3 Connect relay - -on: - push: - branches: - - main - -permissions: - contents: read - id-token: none - statuses: write - -concurrency: - group: relay-production - cancel-in-progress: false - -jobs: - deploy_relay: - name: Deploy production relay - runs-on: blacksmith-8vcpu-ubuntu-2404 - timeout-minutes: 15 - environment: - name: production - env: - CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }} - PLANETSCALE_ORGANIZATION: ${{ vars.PLANETSCALE_ORGANIZATION }} - AXIOM_ORG_ID: ${{ vars.AXIOM_ORG_ID }} - RELAY_DOMAIN: ${{ vars.RELAY_DOMAIN }} - RELAY_API_ZONE_NAME: ${{ vars.RELAY_API_ZONE_NAME }} - RELAY_TUNNEL_ZONE_NAME: ${{ vars.RELAY_TUNNEL_ZONE_NAME }} - CLERK_PUBLISHABLE_KEY: ${{ vars.CLERK_PUBLISHABLE_KEY }} - CLERK_JWT_AUDIENCE: ${{ vars.CLERK_JWT_AUDIENCE }} - APNS_ENVIRONMENT: ${{ vars.APNS_ENVIRONMENT }} - APNS_TEAM_ID: ${{ vars.APNS_TEAM_ID }} - APNS_KEY_ID: ${{ vars.APNS_KEY_ID }} - APNS_BUNDLE_ID: ${{ vars.APNS_BUNDLE_ID }} - ALCHEMY_TELEMETRY_DISABLED: "1" - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Setup Vite+ - uses: voidzero-dev/setup-vp@v1 - with: - node-version-file: package.json - cache: true - run-install: true - - - name: Deploy production relay stage - id: deploy - run: vp run --filter t3code-relay deploy --stage prod --yes --github-output - env: - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - PLANETSCALE_API_TOKEN_ID: ${{ secrets.PLANETSCALE_API_TOKEN_ID }} - PLANETSCALE_API_TOKEN: ${{ secrets.PLANETSCALE_API_TOKEN }} - AXIOM_TOKEN: ${{ secrets.AXIOM_TOKEN }} - CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }} - APNS_PRIVATE_KEY: ${{ secrets.APNS_PRIVATE_KEY }} - - - name: Publish relay deploy commit status - uses: actions/github-script@v8 - with: - script: | - const result = "${{ steps.deploy.outputs.result }}"; - const changed = "${{ steps.deploy.outputs.changed }}" === "true"; - const description = changed - ? "Relay production deploy applied infrastructure changes." - : result === "noop" - ? "Relay production deploy was a no-op." - : `Relay production deploy completed with result: ${result}.`; - - await github.rest.repos.createCommitStatus({ - owner: context.repo.owner, - repo: context.repo.repo, - sha: context.sha, - state: "success", - context: "Relay deploy / production", - description, - target_url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, - }); diff --git a/.github/workflows/desktop-artifacts.yml b/.github/workflows/desktop-artifacts.yml new file mode 100644 index 000000000000..1bcf9c87fa0f --- /dev/null +++ b/.github/workflows/desktop-artifacts.yml @@ -0,0 +1,207 @@ +# Fork workflow: build the desktop binaries for every platform this repo already +# supports, on every push to `main`. +# +# Upstream produces these from `release.yml`, which also reads the production +# T3 Connect relay config, signs, publishes to npm, and cuts a GitHub Release — +# all of which need Cloudflare / Apple / Azure / npm / GitHub App credentials +# this fork does not have. That workflow was removed; this one keeps the part a +# fork can actually run. +# +# Constraints this file deliberately respects: +# - no secrets beyond the automatic GITHUB_TOKEN (which it never uses) +# - standard GitHub-hosted runners only (upstream's `blacksmith-*` labels do +# not resolve here, so those jobs sat queued for 24h and were cancelled) +# - builds are unsigned: passing `--signed` is what pulls in signing +# credentials, so it is never passed. `build-desktop-artifact.ts` then sets +# CSC_IDENTITY_AUTO_DISCOVERY=false and skips notarization. +name: Desktop Artifacts + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: desktop-artifacts-${{ github.ref }} + cancel-in-progress: true + +jobs: + wsl_node_pty: + name: Build WSL node-pty (linux-x64) + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Vite+ + uses: voidzero-dev/setup-vp@v1 + with: + node-version-file: package.json + cache: true + run-install: true + + - name: Build node-pty linux-x64 prebuild + shell: bash + run: | + set -euo pipefail + # Resolve node-pty from apps/server (where it's a dependency) and build + # its native binary from source for Linux. node-addon-api resolves from + # node-pty's own dependency tree, so node-gyp has everything it needs. + pty_pkg="$(node -e "console.log(require.resolve('node-pty/package.json', { paths: ['$GITHUB_WORKSPACE/apps/server'] }))")" + pty_dir="$(dirname "$pty_pkg")" + ( cd "$pty_dir" && npx --yes node-gyp rebuild ) + mkdir -p wsl-prebuild + cp "$pty_dir/build/Release/pty.node" wsl-prebuild/pty.node + file wsl-prebuild/pty.node + + - name: Upload node-pty linux-x64 prebuild + uses: actions/upload-artifact@v7 + with: + name: wsl-node-pty-x64 + path: wsl-prebuild/pty.node + if-no-files-found: error + retention-days: 7 + + build: + name: ${{ matrix.label }} + # Only the Windows entry consumes the prebuild, and it degrades to a warning + # when the file is absent, so `!cancelled()` (not `!failure()`) keeps every + # platform building even when the prebuild job fails. + needs: [wsl_node_pty] + if: ${{ !cancelled() }} + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + # macOS x64 cross-builds from the arm64 runner the same way upstream + # does: the staged workspace pins pnpm `supportedArchitectures`, so the + # native deps are fetched for the target arch, not the host's. + - label: macOS arm64 + runner: macos-latest + platform: mac + target: dmg + arch: arm64 + - label: macOS x64 + runner: macos-latest + platform: mac + target: dmg + arch: x64 + - label: Linux x64 + runner: ubuntu-24.04 + platform: linux + target: AppImage + arch: x64 + - label: Windows x64 + runner: windows-2025 + platform: win + target: nsis + arch: x64 + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Vite+ + uses: voidzero-dev/setup-vp@v1 + with: + node-version-file: package.json + cache: true + run-install: true + + - 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: 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 + } + + # Bundles the Linux node-pty binary so the packaged WSL backend ships a + # ready binary. Non-fatal: without it the Windows artifact still builds, + # and its WSL backend compiles pty.node on first launch instead. + - name: Download WSL node-pty prebuild + id: wsl_prebuild + if: matrix.platform == 'win' + continue-on-error: true + uses: actions/download-artifact@v8 + with: + name: wsl-node-pty-x64 + path: wsl-prebuild + + - name: Build desktop artifact + shell: bash + run: | + set -euo pipefail + + args=( + --platform "${{ matrix.platform }}" + --target "${{ matrix.target }}" + --arch "${{ matrix.arch }}" + --verbose + ) + + if [[ "${{ matrix.platform }}" == "win" ]]; then + if [[ -f "$GITHUB_WORKSPACE/wsl-prebuild/pty.node" ]]; then + args+=(--wsl-prebuild "$GITHUB_WORKSPACE/wsl-prebuild/pty.node") + else + echo "::warning::WSL node-pty prebuild unavailable; the packaged WSL backend will compile pty.node on first launch." + fi + fi + + vp run dist:desktop:artifact "${args[@]}" + + # build-desktop-artifact.ts copies only files (never the unpacked app + # directories) into release/, so the whole directory is safe to upload. + - name: Summarize artifacts + if: always() + shell: bash + run: | + { + echo "### ${{ matrix.label }} (unsigned)" + echo + if [[ -d release ]]; then + echo '```' + ls -lh release + echo '```' + else + echo "No artifacts were produced." + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload desktop artifact + uses: actions/upload-artifact@v7 + with: + name: desktop-${{ matrix.platform }}-${{ matrix.arch }} + path: release/* + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/mobile-eas-preview.yml b/.github/workflows/mobile-eas-preview.yml deleted file mode 100644 index 32e45fef54e5..000000000000 --- a/.github/workflows/mobile-eas-preview.yml +++ /dev/null @@ -1,83 +0,0 @@ -name: Mobile EAS Preview - -on: - pull_request: - types: [opened, reopened, synchronize, labeled, unlabeled] - -jobs: - preview: - name: EAS Preview - if: contains(github.event.pull_request.labels.*.name, '🚀 Mobile Continuous Deployment') - runs-on: blacksmith-8vcpu-ubuntu-2404 - permissions: - contents: read - pull-requests: write - env: - APP_VARIANT: preview - NODE_OPTIONS: --max-old-space-size=8192 - MOBILE_VERSION_POLICY: fingerprint - steps: - - id: expo-token - name: Check for EXPO_TOKEN - env: - EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} - run: | - if [ -n "$EXPO_TOKEN" ]; then - echo "present=true" >> "$GITHUB_OUTPUT" - else - echo "present=false" >> "$GITHUB_OUTPUT" - echo "EXPO_TOKEN is not available; skipping EAS preview." - fi - - - name: Checkout - if: steps.expo-token.outputs.present == 'true' - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Setup Vite+ - if: steps.expo-token.outputs.present == 'true' - uses: voidzero-dev/setup-vp@v1 - with: - node-version-file: package.json - cache: true - run-install: true - - - name: Expose pnpm - if: steps.expo-token.outputs.present == 'true' - run: | - pnpm_version="$(node --print "require('./package.json').packageManager.split('@').pop()")" - vp_pnpm_bin="$HOME/.vite-plus/package_manager/pnpm/$pnpm_version/pnpm/bin" - echo "$vp_pnpm_bin" >> "$GITHUB_PATH" - "$vp_pnpm_bin/pnpm" --version - - - name: Setup EAS - if: steps.expo-token.outputs.present == 'true' - uses: expo/expo-github-action@v8 - with: - eas-version: latest - token: ${{ secrets.EXPO_TOKEN }} - # npm, not pnpm: this only installs eas-cli into the action's own - # tool dir, and pnpm 11 hard-fails that install on dtrace-provider's - # ignored build script (no allowBuilds config outside the repo). - packager: npm - - - name: Pull preview environment variables - if: steps.expo-token.outputs.present == 'true' - working-directory: apps/mobile - env: - EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} - run: eas env:pull preview --non-interactive - - - name: Deploy with fingerprint check - if: steps.expo-token.outputs.present == 'true' - uses: expo/expo-github-action/continuous-deploy-fingerprint@main - env: - EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} - with: - profile: preview:dev - branch: pr-${{ github.event.pull_request.number }} - platform: all - environment: preview - working-directory: apps/mobile - github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/mobile-eas-production.yml b/.github/workflows/mobile-eas-production.yml deleted file mode 100644 index 685df85e57ce..000000000000 --- a/.github/workflows/mobile-eas-production.yml +++ /dev/null @@ -1,113 +0,0 @@ -name: Mobile EAS Production - -# Production builds and OTA updates run from CI (Linux) — never from a laptop. -# Under the fingerprint runtime-version policy the fingerprint must be computed -# in the same OS/pnpm as the EAS build; a macOS `eas build` computes a different -# fingerprint (platform-specific deps + pnpm version) and errors. On this Linux -# runner, with corepack pinning pnpm 10.24 in eas.json, local == build. -on: - workflow_dispatch: - inputs: - mode: - description: "build (+ auto-submit to TestFlight) or update (OTA)" - required: true - type: choice - default: build - options: - - build - - update - platform: - description: "Target platform" - required: true - type: choice - default: ios - options: - - ios - - android - - all - message: - description: "OTA update message (mode=update only)" - required: false - type: string - -jobs: - production: - name: EAS Production ${{ inputs.mode }} - runs-on: blacksmith-8vcpu-ubuntu-2404 - permissions: - contents: read - env: - APP_VARIANT: production - NODE_OPTIONS: --max-old-space-size=8192 - steps: - - id: expo-token - name: Check for EXPO_TOKEN - env: - EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} - run: | - if [ -n "$EXPO_TOKEN" ]; then - echo "present=true" >> "$GITHUB_OUTPUT" - else - echo "present=false" >> "$GITHUB_OUTPUT" - echo "EXPO_TOKEN is not available; skipping EAS production job." - fi - - - name: Checkout - if: steps.expo-token.outputs.present == 'true' - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Setup Vite+ - if: steps.expo-token.outputs.present == 'true' - uses: voidzero-dev/setup-vp@v1 - with: - node-version-file: package.json - cache: true - run-install: true - - - name: Expose pnpm - if: steps.expo-token.outputs.present == 'true' - run: | - pnpm_version="$(node --print "require('./package.json').packageManager.split('@').pop()")" - vp_pnpm_bin="$HOME/.vite-plus/package_manager/pnpm/$pnpm_version/pnpm/bin" - echo "$vp_pnpm_bin" >> "$GITHUB_PATH" - "$vp_pnpm_bin/pnpm" --version - - - name: Setup EAS - if: steps.expo-token.outputs.present == 'true' - uses: expo/expo-github-action@v8 - with: - eas-version: latest - token: ${{ secrets.EXPO_TOKEN }} - # npm, not pnpm: this only installs eas-cli into the action's own - # tool dir, and pnpm 11 hard-fails that install on dtrace-provider's - # ignored build script (no allowBuilds config outside the repo). - packager: npm - - - name: Pull production environment variables - if: steps.expo-token.outputs.present == 'true' - working-directory: apps/mobile - env: - EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} - run: eas env:pull production --non-interactive - - - name: Build and submit - if: steps.expo-token.outputs.present == 'true' && inputs.mode == 'build' - working-directory: apps/mobile - env: - EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} - run: eas build --platform ${{ inputs.platform }} --profile production --auto-submit --non-interactive --no-wait - - - name: Publish OTA update - if: steps.expo-token.outputs.present == 'true' && inputs.mode == 'update' - working-directory: apps/mobile - env: - EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} - run: | - eas update \ - --channel production \ - --environment production \ - --platform ${{ inputs.platform }} \ - --message "${{ inputs.message || format('Production OTA ({0})', github.sha) }}" \ - --non-interactive diff --git a/.github/workflows/mobile-showcase-screenshots.yml b/.github/workflows/mobile-showcase-screenshots.yml deleted file mode 100644 index 36dfb61f73f5..000000000000 --- a/.github/workflows/mobile-showcase-screenshots.yml +++ /dev/null @@ -1,134 +0,0 @@ -name: Mobile Showcase Screenshots - -on: - workflow_dispatch: - inputs: - platform: - description: Device platforms to capture - required: true - default: all - type: choice - options: - - all - - ios - - android - appearance: - description: System appearances to capture - required: true - default: both - type: choice - options: - - both - - dark - - light - -permissions: - contents: read - -env: - NODE_OPTIONS: --max-old-space-size=8192 - -jobs: - ios: - name: iPhone 6.9, iPhone 6.5, and iPad 13 - if: inputs.platform == 'all' || inputs.platform == 'ios' - runs-on: blacksmith-12vcpu-macos-26 - timeout-minutes: 60 - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Setup Vite+ - uses: voidzero-dev/setup-vp@v1 - with: - node-version-file: package.json - cache: true - run-install: true - - - name: Expose pnpm - run: | - pnpm_version="$(node --print "require('./package.json').packageManager.split('@').pop()")" - vp_pnpm_bin="$HOME/.vite-plus/package_manager/pnpm/$pnpm_version/pnpm/bin" - echo "$vp_pnpm_bin" >> "$GITHUB_PATH" - "$vp_pnpm_bin/pnpm" --version - - - name: Capture iOS showcase - run: pnpm screenshots:mobile --platform ios --appearance "${{ inputs.appearance }}" - - - name: Validate App Store Connect assets - run: pnpm screenshots:mobile --platform ios --appearance "${{ inputs.appearance }}" --validate-only - - - name: Upload iOS screenshots - if: always() - uses: actions/upload-artifact@v7 - with: - name: app-store-connect-screenshots - path: artifacts/app-store/screenshots/apple/ - if-no-files-found: warn - retention-days: 14 - - android: - name: Android phone, 7-inch tablet, and 10-inch tablet - if: inputs.platform == 'all' || inputs.platform == 'android' - runs-on: blacksmith-16vcpu-ubuntu-2404 - timeout-minutes: 60 - env: - T3_SHOWCASE_ANDROID_ABI: x86_64 - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Setup Vite+ - uses: voidzero-dev/setup-vp@v1 - with: - node-version-file: package.json - cache: true - run-install: true - - - name: Expose pnpm - run: | - pnpm_version="$(node --print "require('./package.json').packageManager.split('@').pop()")" - vp_pnpm_bin="$HOME/.vite-plus/package_manager/pnpm/$pnpm_version/pnpm/bin" - echo "$vp_pnpm_bin" >> "$GITHUB_PATH" - "$vp_pnpm_bin/pnpm" --version - - - name: Setup Java - uses: actions/setup-java@v5 - with: - distribution: temurin - java-version: 17 - - - name: Setup Gradle cache - uses: gradle/actions/setup-gradle@v5 - - - name: Enable KVM - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS="static_node=kvm"' \ - | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - - name: Capture Android showcase - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: 36 - target: google_apis - arch: x86_64 - profile: pixel_7_pro - avd-name: Pixel_10_Pro - cores: 8 - ram-size: 4096M - disable-animations: false - script: pnpm screenshots:mobile --platform android --appearance "${{ inputs.appearance }}" - - - name: Validate Google Play assets - run: pnpm screenshots:mobile --platform android --appearance "${{ inputs.appearance }}" --validate-only - - - name: Upload Android screenshots - if: always() - uses: actions/upload-artifact@v7 - with: - name: google-play-screenshots - path: artifacts/app-store/screenshots/google-play/ - if-no-files-found: warn - retention-days: 14 diff --git a/.github/workflows/pr-size.yml b/.github/workflows/pr-size.yml deleted file mode 100644 index af557dff62df..000000000000 --- a/.github/workflows/pr-size.yml +++ /dev/null @@ -1,295 +0,0 @@ -name: PR Size - -on: - pull_request_target: - types: [opened, reopened, synchronize, ready_for_review, converted_to_draft] - -permissions: - contents: read - -jobs: - prepare-config: - name: Prepare PR size config - runs-on: ubuntu-24.04 - outputs: - labels_json: ${{ steps.config.outputs.labels_json }} - steps: - - id: config - name: Build PR size label config - uses: actions/github-script@v8 - with: - result-encoding: string - script: | - const managedLabels = [ - { - name: "size:XS", - color: "0e8a16", - description: "0-9 effective changed lines (test files excluded in mixed PRs).", - }, - { - name: "size:S", - color: "5ebd3e", - description: "10-29 effective changed lines (test files excluded in mixed PRs).", - }, - { - name: "size:M", - color: "fbca04", - description: "30-99 effective changed lines (test files excluded in mixed PRs).", - }, - { - name: "size:L", - color: "fe7d37", - description: "100-499 effective changed lines (test files excluded in mixed PRs).", - }, - { - name: "size:XL", - color: "d93f0b", - description: "500-999 effective changed lines (test files excluded in mixed PRs).", - }, - { - name: "size:XXL", - color: "b60205", - description: "1,000+ effective changed lines (test files excluded in mixed PRs).", - }, - ]; - - core.setOutput("labels_json", JSON.stringify(managedLabels)); - sync-label-definitions: - name: Sync PR size label definitions - needs: prepare-config - if: github.event_name != 'pull_request_target' - runs-on: ubuntu-24.04 - permissions: - contents: read - issues: write - steps: - - name: Ensure PR size labels exist - uses: actions/github-script@v8 - env: - PR_SIZE_LABELS_JSON: ${{ needs.prepare-config.outputs.labels_json }} - with: - script: | - const managedLabels = JSON.parse(process.env.PR_SIZE_LABELS_JSON ?? "[]"); - - for (const label of managedLabels) { - try { - const { data: existing } = await github.rest.issues.getLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: label.name, - }); - - if ( - existing.color !== label.color || - (existing.description ?? "") !== label.description - ) { - await github.rest.issues.updateLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: label.name, - color: label.color, - description: label.description, - }); - } - } catch (error) { - if (error.status !== 404) { - throw error; - } - - try { - await github.rest.issues.createLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: label.name, - color: label.color, - description: label.description, - }); - } catch (createError) { - if (createError.status !== 422) { - throw createError; - } - } - } - } - label: - name: Label PR size - needs: prepare-config - if: github.event_name == 'pull_request_target' - runs-on: ubuntu-24.04 - permissions: - contents: read - issues: read - pull-requests: write - concurrency: - group: pr-size-${{ github.event.pull_request.number }} - cancel-in-progress: true - steps: - # This pull_request_target job may fetch untrusted PR commits only as passive - # git data. Do not add dependency installs, build/test scripts, or cache - # actions here; use pull_request plus workflow_run for that pattern instead. - - name: Checkout base repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Sync PR size label - uses: actions/github-script@v8 - env: - PR_SIZE_LABELS_JSON: ${{ needs.prepare-config.outputs.labels_json }} - with: - script: | - const { execFileSync } = require("node:child_process"); - - const issueNumber = context.payload.pull_request.number; - const baseSha = context.payload.pull_request.base.sha; - const headSha = context.payload.pull_request.head.sha; - const headTrackingRef = `refs/remotes/pr-size/${issueNumber}`; - const managedLabels = JSON.parse(process.env.PR_SIZE_LABELS_JSON ?? "[]"); - const managedLabelNames = new Set(managedLabels.map((label) => label.name)); - // Keep this aligned with the repo's test entrypoints and test-only support files. - const testExcludePathspecs = [ - ":(glob,exclude)**/__tests__/**", - ":(glob,exclude)**/test/**", - ":(glob,exclude)**/tests/**", - ":(glob,exclude)apps/server/integration/**", - ":(glob,exclude)**/*.test.*", - ":(glob,exclude)**/*.spec.*", - ":(glob,exclude)**/*.browser.*", - ":(glob,exclude)**/*.integration.*", - ]; - - const sumNumstat = (text) => - text - .split("\n") - .filter(Boolean) - .reduce((total, line) => { - const [insertionsRaw = "0", deletionsRaw = "0"] = line.split("\t"); - const additions = - insertionsRaw === "-" ? 0 : Number.parseInt(insertionsRaw, 10) || 0; - const deletions = - deletionsRaw === "-" ? 0 : Number.parseInt(deletionsRaw, 10) || 0; - - return total + additions + deletions; - }, 0); - - const resolveSizeLabel = (totalChangedLines) => { - if (totalChangedLines < 10) { - return "size:XS"; - } - - if (totalChangedLines < 30) { - return "size:S"; - } - - if (totalChangedLines < 100) { - return "size:M"; - } - - if (totalChangedLines < 500) { - return "size:L"; - } - - if (totalChangedLines < 1000) { - return "size:XL"; - } - - return "size:XXL"; - }; - - execFileSync("git", ["fetch", "--no-tags", "origin", baseSha], { - stdio: "inherit", - }); - - execFileSync( - "git", - ["fetch", "--no-tags", "origin", `+refs/pull/${issueNumber}/head:${headTrackingRef}`], - { - stdio: "inherit", - }, - ); - - const resolvedHeadSha = execFileSync("git", ["rev-parse", headTrackingRef], { - encoding: "utf8", - }).trim(); - - if (resolvedHeadSha !== headSha) { - core.warning( - `Fetched head SHA ${resolvedHeadSha} does not match pull request head SHA ${headSha}; using fetched ref for sizing.`, - ); - } - - execFileSync("git", ["cat-file", "-e", `${baseSha}^{commit}`], { - stdio: "inherit", - }); - - const diffArgs = [ - "diff", - "--numstat", - "--ignore-all-space", - "--ignore-blank-lines", - `${baseSha}...${resolvedHeadSha}`, - ]; - - const totalChangedLines = sumNumstat( - execFileSync( - "git", - diffArgs, - { encoding: "utf8" }, - ), - ); - const nonTestChangedLines = sumNumstat( - execFileSync("git", [...diffArgs, "--", ".", ...testExcludePathspecs], { - encoding: "utf8", - }), - ); - const testChangedLines = Math.max(0, totalChangedLines - nonTestChangedLines); - - const changedLines = nonTestChangedLines === 0 ? testChangedLines : nonTestChangedLines; - const nextLabelName = resolveSizeLabel(changedLines); - - const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - per_page: 100, - }); - - for (const label of currentLabels) { - if (!managedLabelNames.has(label.name) || label.name === nextLabelName) { - continue; - } - - try { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - name: label.name, - }); - } catch (removeError) { - if (removeError.status !== 404) { - throw removeError; - } - } - } - - if (!currentLabels.some((label) => label.name === nextLabelName)) { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - labels: [nextLabelName], - }); - } - - const classification = - nonTestChangedLines === 0 - ? testChangedLines > 0 - ? "test-only PR" - : "no line changes" - : testChangedLines > 0 - ? "test lines excluded" - : "all non-test changes"; - - core.info( - `PR #${issueNumber}: ${nonTestChangedLines} non-test lines, ${testChangedLines} test lines, ${changedLines} effective lines -> ${nextLabelName} (${classification})`, - ); diff --git a/.github/workflows/pr-vouch.yml b/.github/workflows/pr-vouch.yml deleted file mode 100644 index c4abb08b727b..000000000000 --- a/.github/workflows/pr-vouch.yml +++ /dev/null @@ -1,199 +0,0 @@ -name: PR Vouch - -on: - pull_request_target: - types: [opened, reopened, synchronize, ready_for_review, converted_to_draft] - issue_comment: - types: [created] - push: - branches: - - main - paths: - - .github/VOUCHED.td - - .github/workflows/pr-vouch.yml - -permissions: - contents: read - issues: write - pull-requests: write - -jobs: - collect-targets: - name: Collect PR targets - runs-on: ubuntu-24.04 - outputs: - targets: ${{ steps.collect.outputs.targets }} - steps: - - id: collect - uses: actions/github-script@v8 - with: - script: | - if (context.eventName === "pull_request_target") { - const pr = context.payload.pull_request; - core.setOutput("targets", JSON.stringify([{ number: pr.number, user: pr.user.login }])); - return; - } - - if (context.eventName === "issue_comment") { - const issue = context.payload.issue; - const body = context.payload.comment?.body ?? ""; - if (!issue?.pull_request || !body.includes("/recheck-vouch")) { - core.setOutput("targets", "[]"); - return; - } - - core.setOutput( - "targets", - JSON.stringify([{ number: issue.number, user: issue.user.login }]), - ); - return; - } - - const pulls = await github.paginate(github.rest.pulls.list, { - owner: context.repo.owner, - repo: context.repo.repo, - state: "open", - per_page: 100, - }); - - const targets = pulls.map((pull) => ({ - number: pull.number, - user: pull.user.login, - })); - core.setOutput("targets", JSON.stringify(targets)); - - label: - name: Label PR ${{ matrix.target.number }} - needs: collect-targets - if: ${{ needs.collect-targets.outputs.targets != '[]' }} - runs-on: ubuntu-24.04 - concurrency: - group: pr-vouch-${{ matrix.target.number }} - cancel-in-progress: true - strategy: - fail-fast: false - matrix: - target: ${{ fromJson(needs.collect-targets.outputs.targets) }} - steps: - - id: vouch - name: Check PR author trust - uses: mitchellh/vouch/action/check-user@v1 - with: - user: ${{ matrix.target.user }} - allow-fail: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Sync PR labels - uses: actions/github-script@v8 - env: - PR_NUMBER: ${{ matrix.target.number }} - VOUCH_STATUS: ${{ steps.vouch.outputs.status }} - with: - script: | - const issueNumber = Number(process.env.PR_NUMBER); - const status = process.env.VOUCH_STATUS; - const managedLabels = [ - { - name: "vouch:trusted", - color: "1f883d", - description: "PR author is trusted by repo permissions or the VOUCHED list.", - }, - { - name: "vouch:unvouched", - color: "fbca04", - description: "PR author is not yet trusted in the VOUCHED list.", - }, - { - name: "vouch:denounced", - color: "d1242f", - description: "PR author is explicitly blocked by the VOUCHED list.", - }, - ]; - - const managedLabelNames = new Set(managedLabels.map((label) => label.name)); - - for (const label of managedLabels) { - try { - const { data: existing } = await github.rest.issues.getLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: label.name, - }); - - if ( - existing.color !== label.color || - (existing.description ?? "") !== label.description - ) { - await github.rest.issues.updateLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: label.name, - color: label.color, - description: label.description, - }); - } - } catch (error) { - if (error.status !== 404) { - throw error; - } - - try { - await github.rest.issues.createLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: label.name, - color: label.color, - description: label.description, - }); - } catch (createError) { - if (createError.status !== 422) { - throw createError; - } - } - } - } - - const nextLabelName = - status === "denounced" - ? "vouch:denounced" - : ["bot", "collaborator", "vouched"].includes(status) - ? "vouch:trusted" - : "vouch:unvouched"; - - const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - per_page: 100, - }); - - for (const label of currentLabels) { - if (!managedLabelNames.has(label.name) || label.name === nextLabelName) { - continue; - } - - try { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - name: label.name, - }); - } catch (removeError) { - if (removeError.status !== 404) { - throw removeError; - } - } - } - - if (!currentLabels.some((label) => label.name === nextLabelName)) { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - labels: [nextLabelName], - }); - } - - core.info(`PR #${issueNumber}: ${status} -> ${nextLabelName}`); diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 668d1fcb59d7..000000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,1024 +0,0 @@ -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 Connect 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: - CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }} - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - 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: - - 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=t3code-relay... - - - id: relay_state - name: Read production relay tracing config - shell: bash - run: | - vp run --filter t3code-relay deploy \ - --stage prod \ - --read-state \ - --github-output \ - --github-env-file "$RUNNER_TEMP/relay-client-tracing.env" - - - name: Upload relay client tracing config - uses: actions/upload-artifact@v7 - with: - name: relay-client-tracing-config - path: ${{ runner.temp }}/relay-client-tracing.env - if-no-files-found: error - retention-days: 1 - - - 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" - - # node-pty publishes no Linux prebuilt and the WSL backend runs under the - # distro's own (Linux) Node, which can't load the Windows/Electron binary. We - # build the Linux pty.node here, on Linux, and hand it to the Windows packaging - # job — the Windows artifact then ships a ready WSL backend binary with no - # cross-compiling and no first-launch compiler/node-gyp/network on the user's - # machine. node-pty is N-API, so one binary works across all WSL Node versions. - build_wsl_node_pty: - name: Build WSL node-pty (linux-x64) - needs: [preflight] - if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' }} - runs-on: blacksmith-8vcpu-ubuntu-2404 - timeout-minutes: 15 - 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: Build node-pty linux-x64 prebuild - shell: bash - run: | - set -euo pipefail - # Resolve node-pty from apps/server (where it's a dependency) and build - # its native binary from source for Linux. node-addon-api resolves from - # node-pty's own dependency tree, so node-gyp has everything it needs. - pty_pkg="$(node -e "console.log(require.resolve('node-pty/package.json', { paths: ['$GITHUB_WORKSPACE/apps/server'] }))")" - pty_dir="$(dirname "$pty_pkg")" - ( cd "$pty_dir" && npx --yes node-gyp rebuild ) - mkdir -p wsl-prebuild - cp "$pty_dir/build/Release/pty.node" wsl-prebuild/pty.node - file wsl-prebuild/pty.node - - - name: Upload node-pty linux-x64 prebuild - uses: actions/upload-artifact@v7 - with: - name: wsl-node-pty-x64 - path: wsl-prebuild/pty.node - if-no-files-found: error - - build: - name: Build ${{ matrix.label }} - # build_wsl_node_pty stays in `needs` so it runs first and its artifact is - # available to download, but only the Windows matrix entry consumes it. We - # therefore gate the job on preflight + relay (must succeed) WITHOUT requiring - # build_wsl_node_pty, so a failed Linux prebuild doesn't skip the macOS/Linux - # builds. `!cancelled()` (not `!failure()`) lets the job run even when - # build_wsl_node_pty failed; the Windows-only download step below then fails - # that single platform if the prebuild is missing. - needs: [preflight, relay_public_config, build_wsl_node_pty] - if: ${{ !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: Download relay client tracing config - uses: actions/download-artifact@v8 - with: - name: relay-client-tracing-config - path: ${{ runner.temp }}/relay-client-tracing - - - name: Load relay client tracing config - shell: bash - run: | - config_path="$RUNNER_TEMP/relay-client-tracing/relay-client-tracing.env" - tracing_token="$(sed -n 's/^T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN=//p' "$config_path")" - echo "::add-mask::$tracing_token" - cat "$config_path" >> "$GITHUB_ENV" - - - name: Align package versions to release version - run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}" - - - name: Download WSL node-pty prebuild - if: matrix.platform == 'win' - uses: actions/download-artifact@v7 - with: - name: wsl-node-pty-x64 - path: wsl-prebuild - - - 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 }} - APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }} - MACOS_PROVISIONING_PROFILE: ${{ secrets.MACOS_PROVISIONING_PROFILE }} - T3CODE_CLERK_PASSKEY_RP_DOMAINS: ${{ vars.CLERK_PASSKEY_RP_DOMAINS }} - 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 - if ! has_all "$APPLE_TEAM_ID" "$MACOS_PROVISIONING_PROFILE"; then - echo "macOS signing is configured, but APPLE_TEAM_ID or MACOS_PROVISIONING_PROFILE is missing." >&2 - exit 1 - fi - - key_path="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY_ID}.p8" - printf '%s' "$APPLE_API_KEY" > "$key_path" - export APPLE_API_KEY="$key_path" - - profile_path="$RUNNER_TEMP/t3code.provisionprofile" - printf '%s' "$MACOS_PROVISIONING_PROFILE" | base64 -D > "$profile_path" - security cms -D -i "$profile_path" >/dev/null - export T3CODE_APPLE_TEAM_ID="$APPLE_TEAM_ID" - export T3CODE_MACOS_PROVISIONING_PROFILE="$profile_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 - # Bundle the Linux node-pty binary built by the build_wsl_node_pty job - # so the packaged WSL backend ships a ready binary (no first-launch - # compile). Required for a working WSL backend on Windows. - args+=(--wsl-prebuild "$GITHUB_WORKSPACE/wsl-prebuild/pty.node") - 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: Download relay client tracing config - uses: actions/download-artifact@v8 - with: - name: relay-client-tracing-config - path: ${{ runner.temp }}/relay-client-tracing - - - name: Load relay client tracing config - shell: bash - run: | - config_path="$RUNNER_TEMP/relay-client-tracing/relay-client-tracing.env" - tracing_token="$(sed -n 's/^T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN=//p' "$config_path")" - echo "::add-mask::$tracing_token" - cat "$config_path" >> "$GITHUB_ENV" - - - 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_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 }} - 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: Download relay client tracing config - uses: actions/download-artifact@v8 - with: - name: relay-client-tracing-config - path: ${{ runner.temp }}/relay-client-tracing - - - name: Load relay client tracing config - shell: bash - run: | - config_path="$RUNNER_TEMP/relay-client-tracing/relay-client-tracing.env" - tracing_token="$(sed -n 's/^T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN=//p' "$config_path")" - echo "::add-mask::$tracing_token" - cat "$config_path" >> "$GITHUB_ENV" - - - 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_CLERK_CLI_OAUTH_CLIENT_ID=${T3CODE_CLERK_CLI_OAUTH_CLIENT_ID:-}" \ - --build-env "T3CODE_RELAY_URL=${T3CODE_RELAY_URL:-}" \ - --build-env "T3CODE_RELAY_CLIENT_OTLP_TRACES_URL=${T3CODE_RELAY_CLIENT_OTLP_TRACES_URL:-}" \ - --build-env "T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET=${T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET:-}" \ - --build-env "T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN=${T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN:-}" \ - --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 }}" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8b734a99bbb0..fb1992fe7390 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,10 +8,6 @@ You can still open an issue or PR, but please do so knowing there is a high chan If that sounds annoying, that is because it is. This project is still early and we are trying to keep scope, quality, and direction under control. -PRs are automatically labeled with a `vouch:*` trust status and a `size:*` diff size based on changed lines. - -If you are an external contributor, expect `vouch:unvouched` until we explicitly add you to [.github/VOUCHED.td](.github/VOUCHED.td). - ## What We Are Most Likely To Accept Small, focused bug fixes. diff --git a/FORK.md b/FORK.md index 49d19bdff5f1..8069cf7c87d1 100644 --- a/FORK.md +++ b/FORK.md @@ -126,6 +126,8 @@ old pingdotgg + aadit merges) and the fork's changes are being re-applied as **d (`scripts/install.sh`) is hardcoded to aadit's repo. Nothing additive for a dev fork. 7. **CI fork-runnable** (PR #8) — `ci.yml` Blacksmith runners → `ubuntu-24.04`, mobile job dropped. `release.yml` left as-is (release-only; needs secrets a fork lacks). + **Superseded 2026-07-27:** every credentialed / Blacksmith-only workflow (including + `release.yml`) was deleted and `desktop-artifacts.yml` added — see section F. > **Verification note:** several classes of test failure are **environmental, not > regressions** — always diff against clean upstream before chasing one. @@ -505,18 +507,56 @@ rebasing directly onto `pingdotgg/main`. ### F. CI / workflows adapted for the fork -- **Files:** `.github/workflows/ci.yml`, `.github/workflows/pr-size.yml`, - `.github/workflows/release.yml` -- **What:** `ci.yml`: upstream's Blacksmith runners (`blacksmith-8vcpu-ubuntu-2404` - on `check` / `test` / `release_smoke`) → `ubuntu-24.04`, and the macOS-only - `mobile_native_static_analysis` job dropped (this fork does not target mobile, - and `blacksmith-12vcpu-macos-26` is unavailable to it). `pr-size.yml` and - `release.yml` were part of the pre-reset fork; as of the 2026-07-23 reset only - `ci.yml` is modified — `release.yml` is left as upstream ships it, since it is - release-only and needs secrets a fork lacks. +- **Files:** `.github/workflows/*`, `.github/VOUCHED.td`, `docs/operations/ci.md`, + `docs/operations/release.md`, `infra/relay/README.md`, + `infra/relay/scripts/deploy.test.ts`, `CONTRIBUTING.md` +- **Standing rule:** a workflow stays in this fork only if it can actually run + here — **standard GitHub-hosted runners** (upstream's `blacksmith-*` labels do + not resolve; jobs sit queued for 24h and are auto-cancelled) and **no + credentials beyond the automatic `GITHUB_TOKEN`**. Everything else is deleted, + not disabled. Verified against the fork's run history: 30+ consecutive + scheduled `Release` runs were cancelled after the 24h queue timeout, and the + only push-triggered `CI` run ever recorded went the same way. +- **Kept (2 workflows):** + - `ci.yml` — upstream's Blacksmith runners (`blacksmith-8vcpu-ubuntu-2404` on + `check` / `test` / `release_smoke`) → `ubuntu-24.04`, and the macOS-only + `mobile_native_static_analysis` job dropped (this fork does not target + mobile, and `blacksmith-12vcpu-macos-26` is unavailable to it). + - `issue-labels.yml` — unmodified; `GITHUB_TOKEN` only, and it bootstraps the + labels `.github/ISSUE_TEMPLATE/*.yml` apply. +- **Added:** `desktop-artifacts.yml` — builds the four platforms upstream's + `release.yml` matrix covers (macOS `arm64`/`x64` DMG, Linux `x64` AppImage, + Windows `x64` NSIS) on every push to `main` and on dispatch, **unsigned**, and + uploads them as workflow artifacts. Carries over the two secret-free steps that + matter from upstream's build job: the Linux `node-pty` prebuild bundled into + the Windows artifact (non-fatal when missing) and the Spectre-mitigated MSVC + libs install. It never passes `--signed`, which is what pulls signing + credentials into `scripts/build-desktop-artifact.ts`. +- **Deleted (needs credentials and/or Blacksmith runners):** `release.yml` + (Cloudflare + Clerk + Apple + Azure + npm OIDC + release GitHub App; its + 3-hourly nightly cron was pure noise here), `deploy-relay.yml` (Cloudflare, + PlanetScale, Axiom, Clerk, APNs; ran on every push to `main`), + `mobile-eas-preview.yml` / `mobile-eas-production.yml` (`EXPO_TOKEN`), + `mobile-showcase-screenshots.yml` (`blacksmith-12vcpu-macos-26` / + `blacksmith-16vcpu-ubuntu-2404`). +- **Deleted (upstream community governance, no value in this fork):** + `pr-vouch.yml` + `.github/VOUCHED.td` (trust-gates external contributors + against upstream's contributor list) and `pr-size.yml` (size labels on PRs that + are all authored by the fork owner and its agents; its `sync-label-definitions` + job was also dead code — `if: github.event_name != 'pull_request_target'` on a + workflow whose only trigger is `pull_request_target`). Both used + `pull_request_target`. +- **Fallout fixed with the deletions:** the `release workflow tracing config + propagation` guard in `infra/relay/scripts/deploy.test.ts` read `release.yml` + off disk and would fail `vp run test` once it was gone — dropped, with a + restore note in place. `CONTRIBUTING.md` lost its `vouch:*` / `size:*` + paragraph; `docs/operations/ci.md`, `docs/operations/release.md`, and + `infra/relay/README.md` gained fork notes. - **Re-apply notes:** Highest-churn area. Re-derive from upstream's **new** - workflow files and re-apply these transformations (runner swap, drop - fork-unavailable jobs/secrets), rather than force-keeping stale fork copies. + workflow files and re-apply the standing rule above (runner swap, drop + credentialed/unavailable jobs) rather than force-keeping stale fork copies. A + new upstream workflow arriving in a rebase is opt-**in**: it ships only if it + passes the standing rule. - **Redundancy check (as of `89c5a19`): keep.** Upstream has not touched `.github/workflows/ci.yml` since `b41e89e`, so the runner swap replayed cleanly. Re-check on every rebase — a new upstream job would silently be diff --git a/README.md b/README.md index b8227f3bf819..0d5b0854cbdb 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,12 @@ > they need (Gemini CLI `.cmd` shim resolution, Copilot detection), terminal Ctrl-key > forwarding, and CI adjusted to run on standard GitHub-hosted runners. > +> **Fork builds:** every push to `main` here runs the +> [Desktop Artifacts](https://github.com/mclaren-data-systems/t3code/actions/workflows/desktop-artifacts.yml) +> workflow, which builds **unsigned** macOS (arm64/x64), Linux (x64), and Windows (x64) desktop +> binaries and attaches them to the run (14-day retention). This fork publishes no GitHub Releases +> and no npm package — the install instructions below point at upstream. +> > A few further UX refinements — thread-scoped commit selection, a completed dot that persists > until the thread is read, per-thread composer message history, and an always-visible > new-thread button — are maintained on separate branches and not yet on `main`. diff --git a/docs/operations/ci.md b/docs/operations/ci.md index 7a0447ec0700..ee17e3625c40 100644 --- a/docs/operations/ci.md +++ b/docs/operations/ci.md @@ -1,6 +1,10 @@ # CI quality gates -- `.github/workflows/ci.yml` runs `vp check` (lint + typecheck), `vpr typecheck`, and `vp run test` on pull requests and pushes to `main`. -- `.github/workflows/release.yml` builds macOS (`arm64` and `x64`), Linux (`x64`), and Windows (`x64`) desktop artifacts from a single `v*.*.*` tag and publishes one GitHub release. -- The release workflow auto-enables signing only when platform credentials are present. macOS passkey builds additionally require `APPLE_TEAM_ID` and the `MACOS_PROVISIONING_PROFILE` secret; Windows uses Azure Trusted Signing. Without the core signing credentials, it still releases unsigned artifacts. -- See [Release Checklist](./release.md) for the full release/signing setup checklist. +This fork runs a reduced workflow set: everything here uses standard GitHub-hosted +runners and no credentials beyond the automatic `GITHUB_TOKEN`. See +[FORK.md](../../FORK.md) section F for what was removed from upstream and why. + +- `.github/workflows/ci.yml` runs `vp check` (lint + typecheck), `vpr typecheck`, the desktop build pipeline, and `vp run test` on pull requests and pushes to `main`. +- `.github/workflows/desktop-artifacts.yml` builds macOS (`arm64` and `x64`), Linux (`x64`), and Windows (`x64`) desktop artifacts on every push to `main`, plus on manual dispatch. Artifacts are **unsigned** and uploaded as workflow artifacts (14-day retention); nothing is published to a GitHub Release. +- `.github/workflows/issue-labels.yml` keeps the labels the issue forms apply (`bug`, `enhancement`, `needs-triage`) in sync. +- Upstream's `release.yml` (tag-driven signed release + npm publish) and `deploy-relay.yml` are not present in this fork — see [Release Checklist](./release.md) for what they do upstream. diff --git a/docs/operations/mobile-app-store-screenshots.md b/docs/operations/mobile-app-store-screenshots.md index 2c36ab9d0086..05fabd3cd376 100644 --- a/docs/operations/mobile-app-store-screenshots.md +++ b/docs/operations/mobile-app-store-screenshots.md @@ -79,6 +79,10 @@ names, light/dark appearance, scenes, output directory, capture delay, Android A ## Capture in GitHub Actions +> **Fork note:** the `Mobile Showcase Screenshots` workflow is not present in this fork — it runs +> only on Blacksmith macOS/Linux runners this fork cannot schedule. Capture locally with +> `pnpm screenshots:mobile` (above); the rest of this section is upstream reference. + Run the `Mobile Showcase Screenshots` workflow from GitHub's Actions tab, choose `all`, `ios`, or `android`, and select `light`, `dark`, or `both`. The default dispatch captures both appearances and runs iOS and Android concurrently: iPhone and iPad capture on a diff --git a/docs/operations/release.md b/docs/operations/release.md index 9e9085500548..8cc61b560be7 100644 --- a/docs/operations/release.md +++ b/docs/operations/release.md @@ -1,5 +1,12 @@ # Release Checklist +> **Fork note:** the workflows this document describes (`.github/workflows/release.yml`, +> `.github/workflows/deploy-relay.yml`) are **not present in this fork** — they require +> Cloudflare, PlanetScale, Axiom, Clerk, Apple, Azure, npm, and GitHub App credentials a +> fork does not have. Kept as upstream reference for rebases. This fork builds unsigned +> desktop binaries from `.github/workflows/desktop-artifacts.yml` on every push to `main` +> and does not cut releases; see [CI quality gates](./ci.md). + This document covers the unified release workflow for stable and nightly desktop releases. ## What the workflow does diff --git a/infra/relay/README.md b/infra/relay/README.md index 114d5e9b07f8..40f5266ec692 100644 --- a/infra/relay/README.md +++ b/infra/relay/README.md @@ -114,6 +114,11 @@ the URL manually. ### Deployment CI +> **Fork note:** `.github/workflows/deploy-relay.yml` is not present in this fork — it needs +> Cloudflare, PlanetScale, Axiom, Clerk, and APNs credentials a fork does not have, so it +> failed on every push to `main`. The rest of this section is upstream reference; local +> `deploy` runs against a personal stage still work with your own credentials. + The relay is versioned separately from client releases. `.github/workflows/deploy-relay.yml` deploys the shared Alchemy `prod` stage on every push to `main`. Stable and nightly release builds both resolve their static public config from the same diff --git a/infra/relay/scripts/deploy.test.ts b/infra/relay/scripts/deploy.test.ts index 87098b25daf1..1e19673f2a3f 100644 --- a/infra/relay/scripts/deploy.test.ts +++ b/infra/relay/scripts/deploy.test.ts @@ -1,8 +1,4 @@ -import * as NodeServices from "@effect/platform-node/NodeServices"; import { describe, expect, it } from "@effect/vitest"; -import * as Effect from "effect/Effect"; -import * as FileSystem from "effect/FileSystem"; -import * as Path from "effect/Path"; import { hasDeployChanges, @@ -199,24 +195,11 @@ describe("serializeRelayClientTracingEnvironment", () => { }); }); -describe("release workflow tracing config propagation", () => { - it.effect("uses an artifact instead of a masked cross-job token output", () => - Effect.gen(function* () { - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const workflowPath = yield* path.fromFileUrl( - new URL("../../../.github/workflows/release.yml", import.meta.url), - ); - const workflow = yield* fileSystem.readFileString(workflowPath); - - expect(workflow).not.toContain("client_tracing_token:"); - expect(workflow).not.toContain("needs.relay_public_config.outputs.client_tracing_token"); - expect(workflow).toContain('--github-env-file "$RUNNER_TEMP/relay-client-tracing.env"'); - expect(workflow).toContain("name: relay-client-tracing-config"); - expect(workflow).toContain('cat "$config_path" >> "$GITHUB_ENV"'); - }).pipe(Effect.provide(NodeServices.layer)), - ); -}); +// Fork note: upstream also asserts here that `.github/workflows/release.yml` +// propagates the relay tracing config through an artifact rather than a masked +// cross-job output. This fork has no release/relay-deploy workflow (both need +// credentials a fork lacks), so the guard has nothing to read and was dropped. +// Restore it together with those workflows on any rebase that brings them back. describe("publicConfigFromOutput", () => { it("reads the complete public tracing config from persisted Alchemy output", () => { From c515c7e481935d316c130cc42dc148be186d206f Mon Sep 17 00:00:00 2001 From: John McLaren Date: Mon, 27 Jul 2026 17:01:52 +0000 Subject: [PATCH 2/2] Keep FORK.md formatter-clean MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `vp check` runs the formatter, and oxfmt rejected the new section F: an inline code span wrapped across a line boundary, so the continuation line got dedented to column 0. Reworded so the span stays on one line. Verified with oxfmt 0.57.0 (the version vp 0.2.2 ships) over every file this branch touches — all stable. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Uc2sbze61m6FQuyfkZBS9Y --- FORK.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/FORK.md b/FORK.md index 8069cf7c87d1..15a9cee9a3f7 100644 --- a/FORK.md +++ b/FORK.md @@ -546,11 +546,11 @@ rebasing directly onto `pingdotgg/main`. job was also dead code — `if: github.event_name != 'pull_request_target'` on a workflow whose only trigger is `pull_request_target`). Both used `pull_request_target`. -- **Fallout fixed with the deletions:** the `release workflow tracing config - propagation` guard in `infra/relay/scripts/deploy.test.ts` read `release.yml` - off disk and would fail `vp run test` once it was gone — dropped, with a - restore note in place. `CONTRIBUTING.md` lost its `vouch:*` / `size:*` - paragraph; `docs/operations/ci.md`, `docs/operations/release.md`, and +- **Fallout fixed with the deletions:** the release-workflow tracing-config guard + in `infra/relay/scripts/deploy.test.ts` read `release.yml` off disk and would + fail `vp run test` once it was gone — dropped, with a restore note in place. + `CONTRIBUTING.md` lost its `vouch:*` / `size:*` paragraph; + `docs/operations/ci.md`, `docs/operations/release.md`, and `infra/relay/README.md` gained fork notes. - **Re-apply notes:** Highest-churn area. Re-derive from upstream's **new** workflow files and re-apply the standing rule above (runner swap, drop