From 0d1a4859a372ed0225cd1a9af819f741a8fcc9d4 Mon Sep 17 00:00:00 2001 From: Luiz Ferraz Date: Mon, 14 Sep 2026 21:04:36 +0000 Subject: [PATCH 1/2] chore(ci): trim workflows to run on a GitHub-hosted fork - Delete org-only workflows (relay deploy, EAS, release, previews, webhooks) - Switch CI runners from Blacksmith to standard GitHub runners - Limit CI triggers to the main branch --- .github/actions/setup-apt-mirrors/action.yml | 22 - .github/workflows/ci.yml | 103 +- .github/workflows/cursor-hygiene-webhook.yml | 36 - .github/workflows/deploy-relay.yml | 88 -- .github/workflows/desktop-macos-preview.yml | 361 ----- .github/workflows/issue-labels.yml | 75 - .github/workflows/mobile-eas-preview.yml | 94 -- .github/workflows/mobile-eas-production.yml | 290 ---- .../workflows/mobile-fingerprint-check.yml | 205 --- .../workflows/mobile-showcase-screenshots.yml | 169 --- .github/workflows/pr-size.yml | 295 ---- .github/workflows/pr-vouch.yml | 199 --- .github/workflows/publish-aur.yml | 65 - .github/workflows/release.yml | 1261 ----------------- .github/workflows/thread-transfer-report.yml | 75 - .github/workflows/web-preview.yml | 132 -- .github/workflows/windows-tests.yml | 81 -- infra/relay/scripts/deploy.test.ts | 23 - 18 files changed, 19 insertions(+), 3555 deletions(-) delete mode 100644 .github/actions/setup-apt-mirrors/action.yml delete mode 100644 .github/workflows/cursor-hygiene-webhook.yml delete mode 100644 .github/workflows/deploy-relay.yml delete mode 100644 .github/workflows/desktop-macos-preview.yml delete mode 100644 .github/workflows/issue-labels.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-fingerprint-check.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/publish-aur.yml delete mode 100644 .github/workflows/release.yml delete mode 100644 .github/workflows/thread-transfer-report.yml delete mode 100644 .github/workflows/web-preview.yml delete mode 100644 .github/workflows/windows-tests.yml diff --git a/.github/actions/setup-apt-mirrors/action.yml b/.github/actions/setup-apt-mirrors/action.yml deleted file mode 100644 index 5beff201d188..000000000000 --- a/.github/actions/setup-apt-mirrors/action.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Setup APT mirrors -description: Configure Ubuntu package downloads with automatic mirror failover. -runs: - using: composite - steps: - - shell: bash - run: | - # Replace the existing Blacksmith mirror list as well as direct sources. - printf '%s\tpriority:%s\n' \ - https://archive.ubuntu.com/ubuntu 1 \ - https://mirrors.edge.kernel.org/ubuntu 2 \ - https://mirror.math.princeton.edu/pub/ubuntu 3 \ - | sudo tee /etc/apt/blacksmith-ubuntu-mirrors.txt > /dev/null - - # APT's mirror transport retries each file against the next server. - sudo find /etc/apt -maxdepth 2 -type f \( -name '*.list' -o -name '*.sources' \) \ - -exec sed -i -E \ - 's#https?://(([^/]+\.)?archive|security)\.ubuntu\.com/ubuntu/?#mirror+file:/etc/apt/blacksmith-ubuntu-mirrors.txt#g' {} + - - # Move on to a fallback before an unreachable server exhausts the job. - printf '%s\n' 'Acquire::http::Timeout "15";' 'Acquire::https::Timeout "15";' \ - | sudo tee /etc/apt/apt.conf.d/80-mirror-timeouts > /dev/null diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 19a28f205d86..9007822ebe39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,8 @@ name: CI on: pull_request: + branches: + - main push: branches: - main @@ -16,8 +18,8 @@ concurrency: jobs: check: name: Check - runs-on: blacksmith-8vcpu-ubuntu-2404 - timeout-minutes: 10 + runs-on: ubuntu-24.04 + timeout-minutes: 30 steps: - name: Checkout uses: actions/checkout@v6 @@ -55,19 +57,6 @@ jobs: - name: Typecheck run: vpr typecheck - - uses: ./.github/actions/setup-apt-mirrors - - - name: Install browser secret helper build libraries - run: | - sudo sed -i 's|http://|https://|g' /etc/apt/blacksmith-ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources - sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config - - - name: Build desktop pipeline - run: vp run build:desktop - - - name: Verify preload bundle output - run: node apps/desktop/scripts/verify-preload-bundle.mjs - # Everything except `t3` (apps/server). `--parallel` drops the package # dependency ordering that `vp run` applies by default: these `test` tasks # declare no `dependsOn` and resolve workspace deps from source, so ordering @@ -75,8 +64,8 @@ jobs: # limit stays at the default 4 so peak load per runner is unchanged. test: name: Test - runs-on: blacksmith-8vcpu-ubuntu-2404 - timeout-minutes: 10 + runs-on: ubuntu-24.04 + timeout-minutes: 30 steps: - name: Checkout uses: actions/checkout@v6 @@ -96,15 +85,12 @@ jobs: - name: Ensure Electron runtime is installed run: vp run --filter @t3tools/desktop ensure:electron - - uses: ./.github/actions/setup-apt-mirrors - - name: Install browser secret helper build libraries run: | - sudo sed -i 's|http://|https://|g' /etc/apt/blacksmith-ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config - - name: Test nightly release checks - run: node --test .github/scripts/check-nightly-release.test.cjs + - name: Test workflow scripts + run: node --test .github/scripts/*.test.cjs - name: Test run: vp run --parallel --concurrency-limit 4 --filter '!t3' --filter '!@t3tools/monorepo' test @@ -115,8 +101,8 @@ jobs: # isolation that flag buys is preserved exactly. test_server: name: Test Server ${{ matrix.shard }} - runs-on: blacksmith-8vcpu-ubuntu-2404 - timeout-minutes: 10 + runs-on: ubuntu-24.04 + timeout-minutes: 30 strategy: fail-fast: false matrix: @@ -143,46 +129,21 @@ jobs: - name: Test env: T3CODE_TRANSFER_BUDGET_REPORT_PATH: ${{ runner.temp }}/t3code-transfer-budget.md - T3CODE_TRANSFER_BUDGET_RESULT_PATH: ${{ runner.temp }}/thread-transfer-result.json run: vp run --filter t3 test --shard ${{ matrix.shard }}/${{ strategy.job-total }} - # src/server.test.ts writes the budget report, so exactly one shard - # produces these files. Gating the upload on their presence keeps a - # single `thread-transfer-results` artifact per run, which is the name - # thread-transfer-report.yml resolves. - - name: Detect transfer budget report - id: transfer_budget - if: always() - run: | - if test -f "${{ runner.temp }}/thread-transfer-result.json"; then - echo "present=true" >> "$GITHUB_OUTPUT" - else - echo "present=false" >> "$GITHUB_OUTPUT" - fi - + # src/server.test.ts writes the budget report in exactly one shard. - name: Publish transfer budget report - if: always() && steps.transfer_budget.outputs.present == 'true' + if: always() run: | if test -f "${{ runner.temp }}/t3code-transfer-budget.md"; then tee -a "$GITHUB_STEP_SUMMARY" < "${{ runner.temp }}/t3code-transfer-budget.md" - else - echo "Transfer budget report was not produced." >> "$GITHUB_STEP_SUMMARY" fi - - name: Upload thread transfer result - if: always() && steps.transfer_budget.outputs.present == 'true' - uses: actions/upload-artifact@v7 - with: - name: thread-transfer-results - path: ${{ runner.temp }}/thread-transfer-result.json - if-no-files-found: ignore - retention-days: 30 - # Split out of Check and Test: both paid ~7-9s to install a Rust toolchain # for checks that take under 3s, on the critical path of every PR. rust: name: Rust - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-24.04 timeout-minutes: 10 steps: - name: Checkout @@ -210,13 +171,12 @@ jobs: cargo test --locked --manifest-path "native/$crate/Cargo.toml" done - # The static analysis below needs a macOS runner, which bills ~6.7x a Linux - # minute, so gate it on the native sources it actually lints instead of paying - # for it on every push. Detection is API-only (no checkout) and fails open: if - # the diff cannot be resolved, the lint runs. + # Run macOS static analysis only when its native sources or configuration + # change. Detection is API-only (no checkout) and fails open: if the diff + # cannot be resolved, the lint runs. mobile_native_changes: name: Mobile Native Changes - runs-on: blacksmith-2vcpu-ubuntu-2404 + runs-on: ubuntu-24.04 timeout-minutes: 5 permissions: contents: read @@ -294,8 +254,8 @@ jobs: # Skip only on an explicit "no": a gate job that failed or errored leaves the # output empty, and that must run the lint rather than silently skip it. if: ${{ !cancelled() && needs.mobile_native_changes.outputs.changed != 'false' }} - runs-on: blacksmith-6vcpu-macos-26 - timeout-minutes: 10 + runs-on: macos-26 + timeout-minutes: 30 steps: - name: Checkout uses: actions/checkout@v6 @@ -319,28 +279,3 @@ jobs: - name: Lint mobile native sources run: vp run lint:mobile - - release_smoke: - name: Release Smoke - runs-on: blacksmith-8vcpu-ubuntu-2404 - timeout-minutes: 10 - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - sparse-checkout: | - /* - !/.repos/ - sparse-checkout-cone-mode: false - - - name: Setup Vite+ - uses: voidzero-dev/setup-vp@v1 - with: - node-version-file: package.json - cache: true - run-install: | - args: - - --filter=@t3tools/scripts... - - - name: Exercise release-only workflow steps - run: node scripts/release-smoke.ts diff --git a/.github/workflows/cursor-hygiene-webhook.yml b/.github/workflows/cursor-hygiene-webhook.yml deleted file mode 100644 index ea0f579b4ac6..000000000000 --- a/.github/workflows/cursor-hygiene-webhook.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Forward to Cursor hygiene - -on: - push: - branches: [main] - pull_request: - types: [opened, reopened, ready_for_review] - issues: - types: [opened, closed, reopened] - discussion: - types: [created, closed, reopened] - -permissions: - contents: read - -jobs: - forward: - name: POST to Cursor - runs-on: ubuntu-24.04 - steps: - - name: POST to Cursor - env: - URL: ${{ secrets.CURSOR_T3CODE_WEBHOOK_URL }} - AUTH: ${{ secrets.CURSOR_T3CODE_WEBHOOK_AUTH }} - run: | - set -euo pipefail - if [ -z "${URL:-}" ] || [ -z "${AUTH:-}" ]; then - echo "Missing CURSOR_T3CODE_WEBHOOK_URL or CURSOR_T3CODE_WEBHOOK_AUTH — skipping." - exit 0 - fi - curl -fsS --max-time 60 -X POST "$URL" \ - -H "Authorization: $AUTH" \ - -H "Content-Type: application/json" \ - -H "X-GitHub-Event: ${{ github.event_name }}" \ - -H "X-GitHub-Delivery: ${{ github.run_id }}-${{ github.run_attempt }}" \ - --data-binary @"${{ github.event_path }}" diff --git a/.github/workflows/deploy-relay.yml b/.github/workflows/deploy-relay.yml deleted file mode 100644 index 25e744a42968..000000000000 --- a/.github/workflows/deploy-relay.yml +++ /dev/null @@ -1,88 +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 - with: - sparse-checkout: | - /* - !/.repos/ - sparse-checkout-cone-mode: false - - - name: Setup Vite+ - uses: voidzero-dev/setup-vp@v1 - with: - node-version-file: package.json - cache: true - run-install: | - args: - - --filter=t3code-relay... - - - 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 }} - FCM_SERVICE_ACCOUNT: ${{ secrets.FCM_SERVICE_ACCOUNT }} - - - 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-macos-preview.yml b/.github/workflows/desktop-macos-preview.yml deleted file mode 100644 index 7875aec6f36b..000000000000 --- a/.github/workflows/desktop-macos-preview.yml +++ /dev/null @@ -1,361 +0,0 @@ -name: Desktop macOS Preview - -on: - pull_request: - types: [labeled, unlabeled, synchronize, reopened, closed] - -permissions: - contents: read - -# Build events and cleanup events use separate groups: a push must cancel a -# stale in-flight build, but must never cancel a cleanup run mid-delete. The -# publish job re-checks PR state before uploading to cover the reverse race. -concurrency: - group: desktop-macos-preview-${{ github.event.pull_request.number }}-${{ contains(fromJSON('["closed", "unlabeled"]'), github.event.action) && 'cleanup' || 'build' }} - # Cleanup runs must complete (a close event right after an unlabel queues - # behind the running cleanup instead of canceling it mid-delete), and events - # that skip the build job, such as adding an unrelated label, must not - # cancel an in-flight build either. - cancel-in-progress: ${{ !contains(fromJSON('["closed", "unlabeled"]'), github.event.action) && (github.event.action != 'labeled' || github.event.label.name == 'preview:mac') }} - -jobs: - # Builds run PR code, so this job keeps a read-only token. Publishing to the - # release happens in the publish job below, which never checks out PR code. - build: - name: Build macOS Apple Silicon preview - if: >- - github.event.action != 'closed' && - github.event.action != 'unlabeled' && - github.event.pull_request.head.repo.full_name == github.repository && - contains(github.event.pull_request.labels.*.name, 'preview:mac') && - (github.event.action != 'labeled' || github.event.label.name == 'preview:mac') - runs-on: blacksmith-12vcpu-macos-26 - timeout-minutes: 30 - outputs: - dmg_name: ${{ steps.build.outputs.dmg_name }} - version: ${{ steps.version.outputs.version }} - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - ref: ${{ github.event.pull_request.head.sha }} - sparse-checkout: | - /* - !/.repos/ - sparse-checkout-cone-mode: false - - - name: Setup Vite+ - uses: voidzero-dev/setup-vp@v1 - with: - node-version-file: package.json - cache: true - run-install: false - - - name: Install desktop dependencies - run: vp install --filter=@t3tools/desktop... --filter=t3... --filter=@t3tools/scripts... - - - name: Cache resource monitor - id: resource_monitor_cache - uses: actions/cache@v6 - with: - path: native/resource-monitor/target/aarch64-apple-darwin/release/t3-resource-monitor - key: resource-monitor-aarch64-apple-darwin-${{ hashFiles('native/resource-monitor/Cargo.lock', 'native/resource-monitor/Cargo.toml', 'native/resource-monitor/src/**') }} - - - name: Setup Rust - if: steps.resource_monitor_cache.outputs.cache-hit != 'true' - uses: dtolnay/rust-toolchain@stable - with: - targets: aarch64-apple-darwin - - - id: version - name: Set preview version and public configuration - shell: bash - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - set -euo pipefail - - base_version="$(node -p "require('./apps/desktop/package.json').version")" - preview_version="${base_version}-pr.${PR_NUMBER}.${GITHUB_RUN_NUMBER}" - node scripts/update-release-package-versions.ts "$preview_version" - cp .env.example .env - - echo "version=$preview_version" >> "$GITHUB_OUTPUT" - - - id: build - name: Build unsigned macOS DMG - shell: bash - env: - T3CODE_DESKTOP_REUSE_RESOURCE_MONITOR: ${{ steps.resource_monitor_cache.outputs.cache-hit == 'true' }} - PREVIEW_VERSION: ${{ steps.version.outputs.version }} - run: | - set -euo pipefail - - vp run dist:desktop:artifact \ - --platform mac \ - --target dmg \ - --arch arm64 \ - --build-version "$PREVIEW_VERSION" \ - --verbose - - shopt -s nullglob - dmg_files=(release/*.dmg) - if (( ${#dmg_files[@]} != 1 )); then - printf 'Expected one DMG, found %s.\n' "${#dmg_files[@]}" >&2 - exit 1 - fi - printf 'dmg_name=%s\n' "$(basename "${dmg_files[0]}")" >> "$GITHUB_OUTPUT" - - # archive: false uploads the file as its own artifact named after the - # file, so the publish job downloads by *.dmg pattern, not by name. - - name: Upload macOS DMG - uses: actions/upload-artifact@v7 - with: - path: release/*.dmg - if-no-files-found: error - archive: false - overwrite: true - retention-days: 7 - - # Release assets download without a GitHub account, unlike workflow - # artifacts. All preview DMGs live on one rolling prerelease tagged - # "desktop-preview" (release.yml only matches v*.*.* tags), so publishing a - # build never notifies release watchers. This job holds the write token and - # only handles the artifact the build job produced; it never runs PR code. - publish: - name: Publish anonymous download - needs: build - runs-on: blacksmith-8vcpu-ubuntu-2404 - timeout-minutes: 10 - permissions: - contents: write - pull-requests: write - steps: - - name: Download macOS DMG - uses: actions/download-artifact@v8 - with: - pattern: "*.dmg" - merge-multiple: true - path: release - - - id: upload - name: Upload DMG to the rolling preview release - shell: bash - env: - GH_TOKEN: ${{ github.token }} - PR_NUMBER: ${{ github.event.pull_request.number }} - DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} - run: | - set -euo pipefail - - tag="desktop-preview" - - # True while the PR is open and still carries the preview label. - preview_eligible() { - [[ "$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" \ - --json state,labels \ - --jq '.state + " " + (.labels | map(.name) | contains(["preview:mac"]) | tostring)')" == "OPEN true" ]] - } - - # The build ran for many minutes. If the PR closed or lost the label - # meanwhile, cleanup already ran in its own concurrency group, so - # publishing now would resurrect a deleted download. - if ! preview_eligible; then - echo "PR closed or preview label removed while building. Skipping publish." - exit 0 - fi - - dmg_path="$(find release -type f -name '*.dmg' -print -quit)" - if [[ -z "$dmg_path" ]]; then - echo "No DMG found in the downloaded artifact." >&2 - exit 1 - fi - - # The filename comes out of the build, which runs PR code. Requiring - # this PR's marker keeps a build from clobbering or deleting another - # PR's asset, since those names carry a different -pr.N. marker. - if [[ "$(basename "$dmg_path")" != *"-pr.${PR_NUMBER}."* ]]; then - echo "DMG name '$(basename "$dmg_path")' does not carry this PR's -pr.${PR_NUMBER}. marker. Refusing to publish." >&2 - exit 1 - fi - - if ! gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then - # "|| true" tolerates a concurrent publish job creating the - # release between the check and the create. - gh release create "$tag" \ - --repo "$GITHUB_REPOSITORY" \ - --target "$DEFAULT_BRANCH" \ - --prerelease \ - --title "Desktop preview builds" \ - --notes "Rolling unsigned desktop builds from pull requests with a preview label. Each download is removed when its pull request closes or loses the label. Install stable builds from the latest release instead." \ - || true - fi - - # Keep one DMG per PR: drop this PR's older builds first. The - # trailing dot keeps -pr.12. from matching -pr.123. builds. - gh release view "$tag" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' \ - | { grep -F -- "-pr.${PR_NUMBER}." || true; } \ - | while read -r asset; do - gh release delete-asset "$tag" "$asset" --repo "$GITHUB_REPOSITORY" --yes \ - || echo "Asset $asset was already removed by a concurrent run." - done - - gh release upload "$tag" "$dmg_path" --repo "$GITHUB_REPOSITORY" --clobber - - # Re-check after uploading. A cleanup run that started during the - # upload listed assets before ours existed, so it cannot delete it. - # Whichever writer acts last sees the final PR state; if the preview - # became ineligible, delete what we just uploaded. - if ! preview_eligible; then - gh release delete-asset "$tag" "$(basename "$dmg_path")" --repo "$GITHUB_REPOSITORY" --yes \ - || echo "Asset was already removed by a concurrent run." - echo "PR closed or preview label removed during upload. Removed the download." - exit 0 - fi - - echo "download_url=https://github.com/${GITHUB_REPOSITORY}/releases/download/${tag}/$(basename "$dmg_path")" >> "$GITHUB_OUTPUT" - - - name: Comment download link - if: steps.upload.outputs.download_url != '' - uses: actions/github-script@v8 - env: - DOWNLOAD_URL: ${{ steps.upload.outputs.download_url }} - DMG_NAME: ${{ needs.build.outputs.dmg_name }} - HEAD_SHA: ${{ github.event.pull_request.head.sha }} - PREVIEW_VERSION: ${{ needs.build.outputs.version }} - with: - script: | - const { data: pullRequest } = await github.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: context.payload.pull_request.number, - }); - if ( - pullRequest.head.sha !== process.env.HEAD_SHA || - pullRequest.state !== "open" || - !pullRequest.labels.some((label) => label.name === "preview:mac") - ) { - core.info("Skipping the outdated macOS preview comment."); - return; - } - - const marker = ""; - const body = [ - marker, - "### macOS preview", - "", - `[Download Apple Silicon DMG](${process.env.DOWNLOAD_URL})`, - "", - `Version: ${process.env.PREVIEW_VERSION}`, - `Commit: ${process.env.HEAD_SHA.slice(0, 7)}`, - "", - "Unsigned build. Clear quarantine before opening:", - "```sh", - `xattr -d com.apple.quarantine ~/Downloads/${process.env.DMG_NAME}`, - "```", - "", - "No GitHub sign-in is needed. The download stays available until this PR closes or the preview label is removed.", - ].join("\n"); - - const comments = await github.paginate(github.rest.issues.listComments, { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.pull_request.number, - per_page: 100, - }); - const existing = comments.find((comment) => comment.body?.includes(marker)); - - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.pull_request.number, - body, - }); - } - - # The way out: closing the PR or removing the label deletes its DMG from the - # rolling release and updates the PR comment to say so. - cleanup: - name: Remove preview download - if: >- - github.event.pull_request.head.repo.full_name == github.repository && - ((github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'preview:mac')) || - (github.event.action == 'unlabeled' && github.event.label.name == 'preview:mac')) - runs-on: blacksmith-8vcpu-ubuntu-2404 - timeout-minutes: 10 - permissions: - contents: write - pull-requests: write - steps: - - id: delete - name: Delete this PR's preview assets - shell: bash - env: - GH_TOKEN: ${{ github.token }} - PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - set -euo pipefail - - tag="desktop-preview" - - # A stale cleanup must not delete a download that became valid - # again. If the PR is open and labeled once more, the next publish - # owns this PR's assets and replaces them itself. - if [[ "$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" \ - --json state,labels \ - --jq '.state + " " + (.labels | map(.name) | contains(["preview:mac"]) | tostring)')" == "OPEN true" ]]; then - echo "PR is open and labeled again. Skipping cleanup." - echo "removed=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - - echo "removed=true" >> "$GITHUB_OUTPUT" - - if ! gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then - echo "No preview release exists. Nothing to clean up." - exit 0 - fi - - gh release view "$tag" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' \ - | { grep -F -- "-pr.${PR_NUMBER}." || true; } \ - | while read -r asset; do - gh release delete-asset "$tag" "$asset" --repo "$GITHUB_REPOSITORY" --yes \ - || echo "Asset $asset was already removed by a concurrent run." - done - - - name: Mark the preview comment as removed - if: steps.delete.outputs.removed == 'true' - uses: actions/github-script@v8 - with: - script: | - const marker = ""; - const comments = await github.paginate(github.rest.issues.listComments, { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.pull_request.number, - per_page: 100, - }); - const existing = comments.find((comment) => comment.body?.includes(marker)); - if (!existing) { - return; - } - - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body: [ - marker, - "### macOS preview", - "", - "The preview download was removed because this PR closed or the preview label was removed.", - ].join("\n"), - }); diff --git a/.github/workflows/issue-labels.yml b/.github/workflows/issue-labels.yml deleted file mode 100644 index d6571d65d453..000000000000 --- a/.github/workflows/issue-labels.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: Issue Labels - -on: - push: - branches: - - main - paths: - - .github/ISSUE_TEMPLATE/** - - .github/workflows/issue-labels.yml - workflow_dispatch: - -permissions: - issues: write - -jobs: - sync: - name: Sync issue labels - runs-on: ubuntu-24.04 - steps: - - name: Ensure managed issue labels exist - uses: actions/github-script@v7 - with: - script: | - const managedLabels = [ - { - name: "bug", - color: "d73a4a", - description: "Something is broken or behaving incorrectly.", - }, - { - name: "enhancement", - color: "a2eeef", - description: "Requested improvement or new capability.", - }, - { - name: "needs-triage", - color: "fbca04", - description: "Issue needs maintainer review and initial categorization.", - }, - ]; - - 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; - } - - await github.rest.issues.createLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: label.name, - color: label.color, - description: label.description, - }); - } - } diff --git a/.github/workflows/mobile-eas-preview.yml b/.github/workflows/mobile-eas-preview.yml deleted file mode 100644 index d53602f8f5e8..000000000000 --- a/.github/workflows/mobile-eas-preview.yml +++ /dev/null @@ -1,94 +0,0 @@ -name: Mobile EAS Preview - -on: - pull_request: - types: [opened, reopened, synchronize, labeled] - -jobs: - preview: - name: EAS Preview - if: | - contains(github.event.pull_request.labels.*.name, '🚀 Mobile Continuous Deployment') && - (github.event.action != 'labeled' || github.event.label.name == '🚀 Mobile Continuous Deployment') - runs-on: blacksmith-8vcpu-ubuntu-2404 - concurrency: - group: mobile-eas-preview-${{ github.event.pull_request.number }} - cancel-in-progress: true - 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 - # No sparse-checkout here: it makes actions/checkout fetch with - # --filter=blob:none, and eas-cli archives the project via - # `git clone --depth 1 file://`, which fails (exit 128) - # when the partial clone can't serve the unfetched blobs. - - - 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: | - args: - - --filter=@t3tools/mobile... - - - 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 4ad9f4f7672b..000000000000 --- a/.github/workflows/mobile-eas-production.yml +++ /dev/null @@ -1,290 +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. -# -# Every merge to main that touches the mobile app reconciles, per platform: -# 1. Store builds: if the latest production build's version differs from -# app.config.ts, cut a new build and submit it (TestFlight + Play internal -# track). Bumping `version` is therefore all it takes to -# start the next release train — the first build of a version enters -# external-TestFlight beta review immediately, and later builds of the -# same version auto-approve until that version is released. After App -# Store approval, Apple closes the release train and `version` must be -# bumped before another iOS build can be submitted. Releasing to the App -# Store stays a manual App Store Connect step. -# 2. OTA: publish a production-channel update for each platform where at -# least one finished production build matches the current native -# fingerprint. Old-version binaries with a matching fingerprint receive -# it too. When native drift means no binary could install the update, -# it is skipped and flagged in the job summary instead of published -# into the void. -# workflow_dispatch remains as a manual override for both modes (e.g. to -# retry an errored build or force an OTA). -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 - version: - description: "Optional build version override (blank uses app.config.ts; an override is committed before building)" - required: false - type: string - message: - description: "OTA update message (mode=update only)" - required: false - type: string - push: - branches: [main] - paths: - - apps/mobile/** - - packages/client-runtime/** - - packages/contracts/** - - packages/shared/** - - assets/** - - scripts/** - - patches/** - - pnpm-lock.yaml - - pnpm-workspace.yaml - - .github/workflows/mobile-eas-production.yml - -# Serialize runs so OTAs publish in merge order. GitHub keeps at most one -# queued run per group, so a burst of merges collapses into one run of the -# newest commit — intermediate commits don't need their own OTA. -concurrency: - group: mobile-eas-production - cancel-in-progress: false - -jobs: - production: - name: EAS Production ${{ github.event_name == 'push' && 'auto' || 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 - - - id: version_app_token - name: Mint release app token for version override - if: steps.expo-token.outputs.present == 'true' && github.event_name == 'workflow_dispatch' && inputs.mode == 'build' && inputs.version != '' - 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 - if: steps.expo-token.outputs.present == 'true' - uses: actions/checkout@v6 - with: - fetch-depth: 0 - token: ${{ steps.version_app_token.outputs.token || github.token }} - # No sparse-checkout here: it makes actions/checkout fetch with - # --filter=blob:none, and eas-cli archives the project via - # `git clone --depth 1 file://`, which fails (exit 128) - # when the partial clone can't serve the unfetched blobs. - - - 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: | - args: - - --filter=@t3tools/mobile... - - - 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: Apply manual version override - if: steps.version_app_token.outcome == 'success' - env: - GH_TOKEN: ${{ steps.version_app_token.outputs.token }} - APP_SLUG: ${{ steps.version_app_token.outputs.app-slug }} - RELEASE_VERSION: ${{ inputs.version }} - run: | - if [ "$GITHUB_REF_TYPE" != "branch" ]; then - echo "Version overrides require dispatching this workflow from a branch; received $GITHUB_REF_TYPE '$GITHUB_REF_NAME'." >&2 - exit 1 - fi - if ! [[ "$RELEASE_VERSION" =~ ^[0-9]+(\.[0-9]+){1,2}$ ]]; then - echo "Version override must contain two or three dot-separated integers; received '$RELEASE_VERSION'." >&2 - exit 1 - fi - - node --input-type=module -e ' - import fs from "node:fs"; - const path = "apps/mobile/app.config.ts"; - const source = fs.readFileSync(path, "utf8"); - const next = source.replace( - /^( version: ")[^"]+(".*)$/m, - `$1${process.env.RELEASE_VERSION}$2`, - ); - if (next === source && !source.includes(` version: "${process.env.RELEASE_VERSION}"`)) { - throw new Error("Could not update app version"); - } - fs.writeFileSync(path, next); - ' - vp fmt apps/mobile/app.config.ts - - if git diff --quiet -- apps/mobile/app.config.ts; then - echo "app.config.ts is already at $RELEASE_VERSION; no version commit needed." - exit 0 - fi - - user_id="$(gh api "/users/${APP_SLUG}[bot]" --jq .id)" - git config user.name "${APP_SLUG}[bot]" - git config user.email "${user_id}+${APP_SLUG}[bot]@users.noreply.github.com" - git add apps/mobile/app.config.ts - git commit \ - -m "chore(mobile): bump app version to $RELEASE_VERSION" \ - -m "Co-authored-by: codex " - git push origin "HEAD:refs/heads/${GITHUB_REF_NAME}" - - - name: Summarize manual build version - if: steps.expo-token.outputs.present == 'true' && github.event_name == 'workflow_dispatch' && inputs.mode == 'build' - working-directory: apps/mobile - run: | - version="$(npx expo config --json --type public | jq -r '.version')" - { - echo "## Manual production build" - echo - echo "- App version: \`$version\`" - echo "- Platform: \`${{ inputs.platform }}\`" - echo - echo "> Apple closes an iOS release train after App Store approval. Before building iOS, confirm \`$version\` is newer than the approved App Store version." - } >> "$GITHUB_STEP_SUMMARY" - - - name: Build and submit (manual) - if: steps.expo-token.outputs.present == 'true' && github.event_name == 'workflow_dispatch' && 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 (manual) - if: steps.expo-token.outputs.present == 'true' && github.event_name == 'workflow_dispatch' && 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 - - # No --status filter on build:list: an in-queue/in-progress build must - # count as existing, or every merge during the build window would cut a - # duplicate. After an errored build, retry via workflow_dispatch - # mode=build — pushes won't re-trigger it until the app version changes. - - id: store_builds - name: Ensure store builds exist for the current app version - if: steps.expo-token.outputs.present == 'true' && github.event_name == 'push' - continue-on-error: true - working-directory: apps/mobile - env: - EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} - run: | - failed=0 - version="$(npx expo config --json --type public | jq -r '.version')" - for platform in ios android; do - latest="$(eas build:list --platform "$platform" --build-profile production --limit 1 --json --non-interactive | jq -r '.[0].appVersion // "none"')" - if [ "$latest" = "$version" ]; then - echo "$platform: production build for $version already exists (or is in progress)" - continue - fi - echo "$platform: latest production build is $latest, app.config.ts says $version — building" - if eas build --platform "$platform" --profile production --auto-submit --non-interactive --no-wait; then - echo ":building_construction: $platform: scheduled production build and submission for $version" >> "$GITHUB_STEP_SUMMARY" - else - failed=1 - echo ":x: $platform: production build or submission failed for $version" >> "$GITHUB_STEP_SUMMARY" - fi - done - exit "$failed" - - - name: Publish fingerprint-gated OTA - if: steps.expo-token.outputs.present == 'true' && github.event_name == 'push' - working-directory: apps/mobile - env: - EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} - run: | - message="$(git log -1 --pretty=%s | head -c 120) ($(git rev-parse --short=9 HEAD))" - for platform in ios android; do - # eas-cli prints an environment-loaded notice to stdout before the - # JSON even with --json, so discard everything before the document. - hash="$(eas fingerprint:generate --platform "$platform" --environment production --json --non-interactive | sed -n '/^{/,$p' | jq -er '.hash | select(type == "string" and length > 0)')" - matching="$(eas build:list --platform "$platform" --build-profile production --status finished --fingerprint-hash "$hash" --limit 1 --json --non-interactive | jq 'length')" - if [ "$matching" -gt 0 ]; then - eas update \ - --channel production \ - --environment production \ - --platform "$platform" \ - --message "$message" \ - --non-interactive - echo ":white_check_mark: $platform: OTA published to production (fingerprint \`$hash\`)" >> "$GITHUB_STEP_SUMMARY" - else - echo ":warning: $platform: no finished production build matches fingerprint \`$hash\` — OTA skipped; JS changes reach $platform only once a matching build ships" >> "$GITHUB_STEP_SUMMARY" - fi - done - - - name: Propagate store build failure - if: steps.store_builds.outcome == 'failure' - run: exit 1 diff --git a/.github/workflows/mobile-fingerprint-check.yml b/.github/workflows/mobile-fingerprint-check.yml deleted file mode 100644 index fd98817cd105..000000000000 --- a/.github/workflows/mobile-fingerprint-check.yml +++ /dev/null @@ -1,205 +0,0 @@ -name: Mobile Fingerprint Check - -# Detects whether a PR changes the native fingerprint — i.e. whether merging -# it would leave main un-OTA-able until a new store build ships. Native-change -# PRs get the "📱 Native Change" label so they can be held and merged as a -# batch right before the next store submission, keeping main OTA-able for -# everything else in between. (Once one native PR merges, every later merge -# inherits the drifted fingerprint and loses OTA reach too — that is why the -# signal has to fire before merge, not after.) -# -# The check is advisory: it always passes, the label is the signal. Both -# fingerprints are computed in this one job (same OS, same corepack-pinned -# pnpm), so the comparison is self-consistent; no EXPO_TOKEN needed. -on: - pull_request: - paths: - - apps/mobile/** - - packages/client-runtime/** - - packages/contracts/** - - packages/shared/** - - assets/** - - scripts/** - - patches/** - - pnpm-lock.yaml - - pnpm-workspace.yaml - - .github/workflows/mobile-fingerprint-check.yml - -concurrency: - group: mobile-fingerprint-check-${{ github.event.pull_request.number }} - cancel-in-progress: true - -jobs: - fingerprint: - name: Native fingerprint diff - runs-on: blacksmith-8vcpu-ubuntu-2404 - permissions: - contents: read - issues: write - pull-requests: write - env: - APP_VARIANT: production - NODE_OPTIONS: --max-old-space-size=8192 - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - # Default pull_request checkout is the merge commit (PR applied on - # top of base), so the "head" fingerprint is the state main would - # actually be in after merging — stale branches compare cleanly. - fetch-depth: 0 - - - name: Setup Vite+ - uses: voidzero-dev/setup-vp@v1 - with: - node-version-file: package.json - cache: true - run-install: | - args: - - --filter=@t3tools/mobile... - - - 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: Fingerprint merge result - working-directory: apps/mobile - run: | - mkdir -p "$RUNNER_TEMP/fp/head" "$RUNNER_TEMP/fp/base" - for platform in ios android; do - npx expo-updates fingerprint:generate --platform "$platform" > "$RUNNER_TEMP/fp/head/$platform.json" - done - - - name: Fingerprint base - run: | - git checkout --quiet "${{ github.event.pull_request.base.sha }}" - # Re-sync node_modules to the base commit's lockfile before - # fingerprinting — a dep-changing PR must not fingerprint the base - # against head's installed packages. - pnpm install --filter=@t3tools/mobile... - cd apps/mobile - for platform in ios android; do - npx expo-updates fingerprint:generate --platform "$platform" > "$RUNNER_TEMP/fp/base/$platform.json" - done - - - id: compare - name: Compare fingerprints - run: | - changed="" - { - echo "## Native fingerprint diff" - echo - for platform in ios android; do - head_hash="$(jq -r .hash "$RUNNER_TEMP/fp/head/$platform.json")" - base_hash="$(jq -r .hash "$RUNNER_TEMP/fp/base/$platform.json")" - if [ "$head_hash" = "$base_hash" ]; then - echo "- ✅ **$platform**: unchanged (\`$head_hash\`) — OTA-compatible" - continue - fi - changed="$changed $platform" - echo "- 📱 **$platform**: \`$base_hash\` → \`$head_hash\` — merging requires a new native build before OTAs work again" - jq -r -n \ - --slurpfile h "$RUNNER_TEMP/fp/head/$platform.json" \ - --slurpfile b "$RUNNER_TEMP/fp/base/$platform.json" ' - ($b[0].sources | map({ (.filePath // .id): .hash }) | add // {}) as $bm - | $h[0].sources[] - | select($bm[(.filePath // .id)] != .hash) - | " - \(.type): `\(.filePath // .id)`"' - done - } >> "$GITHUB_STEP_SUMMARY" - echo "changed_platforms=${changed# }" >> "$GITHUB_OUTPUT" - - - name: Sync native change label - # Fork PRs get a read-only token under pull_request; the check stays - # advisory there (summary only). This workflow must not move to - # pull_request_target — it installs and runs PR code. - if: github.event.pull_request.head.repo.full_name == github.repository - uses: actions/github-script@v8 - env: - CHANGED_PLATFORMS: ${{ steps.compare.outputs.changed_platforms }} - with: - script: | - const managedLabel = { - name: "📱 Native Change", - color: "d93f0b", - description: - "Changes the native fingerprint; merging blocks production OTAs until a new store build ships.", - }; - const nativeChanged = (process.env.CHANGED_PLATFORMS ?? "").trim() !== ""; - const issueNumber = context.payload.pull_request.number; - - try { - const { data: existing } = await github.rest.issues.getLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: managedLabel.name, - }); - - if ( - existing.color !== managedLabel.color || - (existing.description ?? "") !== managedLabel.description - ) { - await github.rest.issues.updateLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: managedLabel.name, - color: managedLabel.color, - description: managedLabel.description, - }); - } - } catch (error) { - if (error.status !== 404) { - throw error; - } - - try { - await github.rest.issues.createLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: managedLabel.name, - color: managedLabel.color, - description: managedLabel.description, - }); - } catch (createError) { - if (createError.status !== 422) { - throw createError; - } - } - } - - const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - per_page: 100, - }); - const hasLabel = currentLabels.some((label) => label.name === managedLabel.name); - - if (nativeChanged && !hasLabel) { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - labels: [managedLabel.name], - }); - } else if (!nativeChanged && hasLabel) { - try { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - name: managedLabel.name, - }); - } catch (removeError) { - if (removeError.status !== 404) { - throw removeError; - } - } - } - - core.info( - `PR #${issueNumber}: native fingerprint ${nativeChanged ? `changed (${process.env.CHANGED_PLATFORMS})` : "unchanged"}`, - ); diff --git a/.github/workflows/mobile-showcase-screenshots.yml b/.github/workflows/mobile-showcase-screenshots.yml deleted file mode 100644 index c64bccacdca8..000000000000 --- a/.github/workflows/mobile-showcase-screenshots.yml +++ /dev/null @@ -1,169 +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 - theme: - description: Palette to capture (all multiplies the run by six) - required: true - default: t3-code - type: choice - options: - - t3-code - - t3-chat - - grove - - ocean - - ember - - iris - - all - -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 - # Capturing every palette multiplies the device matrix by six, and only the - # one native build is shared between them. - timeout-minutes: ${{ inputs.theme == 'all' && 300 || 60 }} - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - sparse-checkout: | - /* - !/.repos/ - sparse-checkout-cone-mode: false - - - name: Setup Vite+ - uses: voidzero-dev/setup-vp@v1 - with: - node-version-file: package.json - cache: true - run-install: | - args: - - --filter=@t3tools/mobile... - - --filter=@t3tools/scripts... - - --filter=t3... - - - 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 }}" --theme "${{ inputs.theme }}" - - - name: Validate App Store Connect assets - run: pnpm screenshots:mobile --platform ios --appearance "${{ inputs.appearance }}" --theme "${{ inputs.theme }}" --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 - # Capturing every palette multiplies the device matrix by six, and only the - # one native build is shared between them. - timeout-minutes: ${{ inputs.theme == 'all' && 300 || 60 }} - env: - T3_SHOWCASE_ANDROID_ABI: x86_64 - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - sparse-checkout: | - /* - !/.repos/ - sparse-checkout-cone-mode: false - - - name: Setup Vite+ - uses: voidzero-dev/setup-vp@v1 - with: - node-version-file: package.json - cache: true - run-install: | - args: - - --filter=@t3tools/mobile... - - --filter=@t3tools/scripts... - - --filter=t3... - - - 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 }}" --theme "${{ inputs.theme }}" - - - name: Validate Google Play assets - run: pnpm screenshots:mobile --platform android --appearance "${{ inputs.appearance }}" --theme "${{ inputs.theme }}" --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/publish-aur.yml b/.github/workflows/publish-aur.yml deleted file mode 100644 index 62f8fd1f5470..000000000000 --- a/.github/workflows/publish-aur.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: Publish AUR package - -# See packaging/aur/README.md. - -on: - workflow_call: - inputs: - release_tag: - required: true - type: string - pkgrel: - required: false - default: "1" - type: string - secrets: - AUR_SSH_PRIVATE_KEY: - required: true - workflow_dispatch: - inputs: - release_tag: - description: "Release tag to publish" - required: true - type: string - pkgrel: - description: "Arch package release override" - required: false - default: "1" - type: string - -permissions: - contents: read - -concurrency: - group: publish-aur - cancel-in-progress: false - -jobs: - publish: - name: Validate and publish - runs-on: blacksmith-8vcpu-ubuntu-2404 - timeout-minutes: 30 - container: - image: archlinux:base-devel - - steps: - - name: Install Arch packaging tools - run: pacman -Syu --noconfirm --needed git github-cli jq namcap openssh sudo - - - name: Checkout packaging sources - uses: actions/checkout@v6 - - - name: Create unprivileged build user - run: | - useradd --create-home builder - install -Dm0440 /dev/stdin /etc/sudoers.d/builder <<'EOF' - builder ALL=(root) NOPASSWD: /usr/bin/pacman - EOF - - - name: Validate and publish package sources - env: - GH_TOKEN: ${{ github.token }} - RELEASE_TAG: ${{ inputs.release_tag }} - PKGREL: ${{ inputs.pkgrel || '1' }} - AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} - run: packaging/aur/scripts/release.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 39e485d42a98..000000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,1261 +0,0 @@ -name: Release - -on: - push: - tags: - - "v*.*.*" - - "!v*-nightly.*" - schedule: - # Avoid minute zero, when GitHub scheduled jobs are busiest. - - cron: "8,38 * * * *" - workflow_dispatch: - inputs: - channel: - description: "Release channel" - required: false - default: stable - type: choice - options: - - stable - - nightly - version: - description: "Stable version override (for example 1.2.3). Defaults to the version the latest nightly previewed." - required: false - type: string - -# Serialize nightlies (scheduled and manual) so overlapping runs cannot build -# the same commit twice or publish out of order. Stable tag releases get their -# own group so a nightly never blocks them. Running publishers are never -# canceled, and queue: max keeps every pending run instead of the default -# newest-wins single slot, so a queued stable tag can never be silently -# dropped. Automatic nightlies recheck the release gap after leaving the queue. -concurrency: - group: release-${{ (github.event_name == 'schedule' || inputs.channel == 'nightly') && 'nightly' || 'stable' }} - cancel-in-progress: false - queue: max - -permissions: - contents: read - id-token: none - -jobs: - # Picks the commit every later job builds. Nightlies and tag pushes build the - # triggering commit. Manual stable releases build the commit of the latest - # published nightly, so stable only ever ships a build that nightly users - # have already run. Scheduled runs also decide here whether a nightly is due. - resolve_commit: - name: Resolve release commit - runs-on: blacksmith-8vcpu-ubuntu-2404 - timeout-minutes: 5 - outputs: - ref: ${{ steps.resolve.outputs.ref }} - nightly_version: ${{ steps.resolve.outputs.nightly_version }} - has_changes: ${{ steps.resolve.outputs.has_changes }} - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - sparse-checkout: .github/scripts - - - id: resolve - name: Resolve release commit - uses: actions/github-script@v8 - env: - DISPATCH_CHANNEL: ${{ inputs.channel }} - with: - script: | - const { - shouldReleaseNightly, - resolveLatestNightlyCommit, - } = require('./.github/scripts/check-nightly-release.cjs'); - - if (context.eventName === 'schedule') { - core.setOutput('has_changes', await shouldReleaseNightly({ github, context, core })); - core.setOutput('ref', context.sha); - } else if (context.eventName === 'workflow_dispatch' && process.env.DISPATCH_CHANNEL !== 'nightly') { - const { tag, sha, version } = await resolveLatestNightlyCommit({ github, context, core }); - core.notice(`Stable release builds ${sha}, the commit shipped by ${tag}.`); - core.setOutput('ref', sha); - core.setOutput('nightly_version', version); - } else { - core.setOutput('ref', context.sha); - } - - preflight: - name: Preflight - needs: [resolve_commit] - if: | - needs.resolve_commit.result == 'success' && - (github.event_name != 'schedule' || needs.resolve_commit.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: ${{ needs.resolve_commit.outputs.ref }} - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - ref: ${{ needs.resolve_commit.outputs.ref }} - fetch-depth: 0 - sparse-checkout: | - /* - !/.repos/ - sparse-checkout-cone-mode: false - - - name: Setup Vite+ - uses: voidzero-dev/setup-vp@v1 - with: - node-version-file: package.json - cache: true - run-install: true - env: - pnpm_config_cache_dir: ${{ runner.temp }}/pnpm-metadata - - - id: release_meta - name: Resolve release version - shell: bash - env: - DISPATCH_CHANNEL: ${{ github.event.inputs.channel }} - DISPATCH_VERSION: ${{ github.event.inputs.version }} - NIGHTLY_VERSION: ${{ needs.resolve_commit.outputs.nightly_version }} - NIGHTLY_DATE: ${{ github.run_started_at }} - NIGHTLY_SHA: ${{ needs.resolve_commit.outputs.ref }} - 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:-$NIGHTLY_VERSION}" - if [[ -z "$raw" ]]; then - echo "workflow_dispatch stable releases need a version input or a published nightly." >&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 - - - 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 - - # Share only the verification results, not the large registry metadata cache. - - name: Upload dependency verification - continue-on-error: true - uses: actions/upload-artifact@v7 - with: - name: release-dependency-verification - path: ${{ runner.temp }}/pnpm-metadata/lockfile-verified.jsonl - - quality: - name: Release quality checks - needs: [preflight] - if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' }} - runs-on: blacksmith-8vcpu-ubuntu-2404 - timeout-minutes: 10 - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - ref: ${{ needs.preflight.outputs.ref }} - sparse-checkout: | - /* - !/.repos/ - sparse-checkout-cone-mode: false - - - 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 - - - name: Check - run: vp check - - - name: Typecheck - run: vp run typecheck - - - uses: ./.github/actions/setup-apt-mirrors - - - name: Install browser secret helper build libraries - run: sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config - - - name: Test - run: vp run test - - relay_public_config: - name: Resolve T3 Connect public config - # Consumes only the release commit, not preflight's resolved version, so it - # runs alongside preflight instead of after it. The condition mirrors preflight's. - needs: [resolve_commit] - if: | - needs.resolve_commit.result == 'success' && - (github.event_name != 'schedule' || needs.resolve_commit.outputs.has_changes == 'true') - 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.resolve_commit.outputs.ref }} - sparse-checkout: | - /* - !/.repos/ - sparse-checkout-cone-mode: false - - - 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) - # Same gating as relay_public_config: only the release commit is needed, so - # this runs alongside preflight. See the condition comment there. - needs: [resolve_commit] - if: | - needs.resolve_commit.result == 'success' && - (github.event_name != 'schedule' || needs.resolve_commit.outputs.has_changes == 'true') - runs-on: blacksmith-8vcpu-ubuntu-2404 - timeout-minutes: 15 - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - ref: ${{ needs.resolve_commit.outputs.ref }} - sparse-checkout: | - /* - !/.repos/ - sparse-checkout-cone-mode: false - - - name: Setup Vite+ - uses: voidzero-dev/setup-vp@v1 - with: - node-version-file: package.json - cache: true - run-install: | - args: - - --filter=t3... - - - 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 - rust_target: aarch64-apple-darwin - resource_key: darwin-arm64 - - label: macOS x64 - runner: blacksmith-12vcpu-macos-26 - platform: mac - target: dmg - arch: x64 - rust_target: x86_64-apple-darwin - resource_key: darwin-x64 - - label: Linux x64 - runner: blacksmith-32vcpu-ubuntu-2404 - platform: linux - target: AppImage - arch: x64 - rust_target: x86_64-unknown-linux-gnu - resource_key: linux-x64 - - label: Windows x64 - runner: blacksmith-32vcpu-windows-2025 - platform: win - target: nsis - arch: x64 - rust_target: x86_64-pc-windows-msvc - resource_key: win32-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 }} - sparse-checkout: | - /* - !/.repos/ - sparse-checkout-cone-mode: false - - - name: Setup Vite+ - uses: voidzero-dev/setup-vp@v1 - with: - node-version-file: package.json - cache: ${{ matrix.platform != 'win' }} - run-install: false - - - name: Resolve Windows package cache path - if: matrix.platform == 'win' - id: package_cache_path - shell: pwsh - run: '"path=$(vp pm cache dir)" >> $env:GITHUB_OUTPUT' - - - name: Cache Windows packages - if: matrix.platform == 'win' - uses: actions/cache@v6 - with: - path: ${{ steps.package_cache_path.outputs.path }} - key: windows-release-packages-v1-${{ matrix.arch }}-${{ hashFiles('pnpm-lock.yaml') }} - - # pnpm checks the lockfile and policy before reusing this result. A missing - # artifact leaves the cache empty, so installation runs the checks again. - - name: Download dependency verification - continue-on-error: true - uses: actions/download-artifact@v8 - with: - name: release-dependency-verification - path: ${{ runner.temp }}/pnpm-metadata - - - name: Install desktop dependencies - env: - pnpm_config_cache_dir: ${{ runner.temp }}/pnpm-metadata - run: vp install --filter=@t3tools/desktop... --filter=t3... --filter=@t3tools/scripts... - - - name: Cache resource monitor - id: resource_monitor_cache - uses: actions/cache@v6 - with: - path: native/resource-monitor/target/${{ matrix.rust_target }}/release/t3-resource-monitor${{ matrix.platform == 'win' && '.exe' || '' }} - key: resource-monitor-${{ matrix.rust_target }}-${{ hashFiles('native/resource-monitor/Cargo.lock', 'native/resource-monitor/Cargo.toml', 'native/resource-monitor/src/**') }} - - - name: Cache Linux capture helpers - if: matrix.platform == 'linux' - id: capture_helper_cache - uses: actions/cache@v6 - with: - path: | - native/kde-snap-shot/target/${{ matrix.rust_target }}/release/t3-kde-snap-shot - native/hyprland-snap-shot/target/${{ matrix.rust_target }}/release/t3-hyprland-snap-shot - key: linux-capture-helpers-${{ matrix.rust_target }}-${{ hashFiles('native/kde-snap-shot/Cargo.lock', 'native/kde-snap-shot/Cargo.toml', 'native/kde-snap-shot/src/**', 'native/hyprland-snap-shot/Cargo.lock', 'native/hyprland-snap-shot/Cargo.toml', 'native/hyprland-snap-shot/src/**', 'native/hyprland-snap-shot/protocols/**') }} - - - name: Setup Rust - if: steps.resource_monitor_cache.outputs.cache-hit != 'true' || (matrix.platform == 'linux' && steps.capture_helper_cache.outputs.cache-hit != 'true') - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.rust_target }} - - - 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.Runtimes.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 - } - - - uses: ./.github/actions/setup-apt-mirrors - if: matrix.platform == 'linux' - - - name: Install Linux desktop build libraries - if: matrix.platform == 'linux' - shell: bash - run: | - sudo apt-get update - sudo apt-get install -y libsecret-1-dev pkg-config - if ! command -v magick >/dev/null 2>&1 && ! command -v convert >/dev/null 2>&1; then - 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: - pnpm_config_cache_dir: ${{ runner.temp }}/pnpm-metadata - T3CODE_DESKTOP_REUSE_RESOURCE_MONITOR: ${{ steps.resource_monitor_cache.outputs.cache-hit == 'true' }} - T3CODE_DESKTOP_REUSE_LINUX_CAPTURE_HELPERS: ${{ steps.capture_helper_cache.outputs.cache-hit == 'true' }} - 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: Collect resource monitor - shell: bash - run: | - set -euo pipefail - binary_name="t3-resource-monitor" - if [[ "${{ matrix.platform }}" == "win" ]]; then - binary_name="${binary_name}.exe" - fi - source_path="native/resource-monitor/target/${{ matrix.rust_target }}/release/${binary_name}" - target_dir="resource-monitor-publish/${{ matrix.resource_key }}" - mkdir -p "$target_dir" - cp "$source_path" "$target_dir/$binary_name" - - - name: Upload build artifacts - uses: actions/upload-artifact@v7 - with: - name: desktop-${{ matrix.platform }}-${{ matrix.arch }} - path: release-publish/* - if-no-files-found: error - - - name: Upload resource monitor - uses: actions/upload-artifact@v7 - with: - name: resource-monitor-${{ matrix.resource_key }} - path: resource-monitor-publish/${{ matrix.resource_key }}/* - if-no-files-found: error - - publish_cli: - name: Publish CLI to npm - needs: [preflight, relay_public_config, quality, build] - if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.quality.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 }} - sparse-checkout: | - /* - !/.repos/ - sparse-checkout-cone-mode: false - - - 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 }}" - - # The t3 build task depends on @t3tools/web#build, so the web client is - # built (once) as part of this step. - - name: Build CLI package - run: vp run --filter t3 build - - - name: Download resource monitors - uses: actions/download-artifact@v8 - with: - pattern: resource-monitor-* - path: ${{ runner.temp }}/resource-monitors - - - name: Bundle resource monitors into CLI package - shell: bash - run: | - set -euo pipefail - for artifact_dir in "$RUNNER_TEMP"/resource-monitors/resource-monitor-*; do - resource_key="${artifact_dir##*/resource-monitor-}" - target_dir="apps/server/dist/resource-monitor/${resource_key}" - mkdir -p "$target_dir" - cp "$artifact_dir"/t3-resource-monitor* "$target_dir/" - chmod +x "$target_dir"/t3-resource-monitor 2>/dev/null || true - done - - - 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: 30 - permissions: - contents: write - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - ref: ${{ needs.preflight.outputs.ref }} - sparse-checkout: | - /* - !/.repos/ - sparse-checkout-cone-mode: false - - - name: Setup Vite+ - uses: voidzero-dev/setup-vp@v1 - with: - node-version-file: package.json - cache: true - run-install: | - args: - - --filter=@t3tools/scripts... - - - name: Download all desktop artifacts - uses: actions/download-artifact@v8 - with: - pattern: desktop-* - merge-multiple: true - path: release-assets - - - name: Merge macOS updater manifests - run: | - shopt -s nullglob - for x64_manifest in release-assets/*-mac-x64.yml; do - arm64_manifest="${x64_manifest%-x64.yml}.yml" - if [[ -f "$arm64_manifest" ]]; then - node scripts/merge-update-manifests.ts --platform mac "$arm64_manifest" "$x64_manifest" - rm -f "$x64_manifest" - fi - done - - - name: Publish release - if: needs.preflight.outputs.previous_tag != '' - uses: softprops/action-gh-release@v3 - 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: ${{ github.token }} - - - name: Publish first release - if: needs.preflight.outputs.previous_tag == '' - uses: softprops/action-gh-release@v3 - 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: ${{ github.token }} - - publish_aur: - name: Publish AUR package - needs: [preflight, release] - if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.release.result == 'success' }} - uses: ./.github/workflows/publish-aur.yml - with: - release_tag: ${{ needs.preflight.outputs.tag }} - secrets: - AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} - - 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 }} - sparse-checkout: | - /* - !/.repos/ - sparse-checkout-cone-mode: false - - - 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 \ - --archive=tgz \ - --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 - - deploy_marketing: - name: Deploy marketing site - needs: [preflight, release] - if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.release.result == 'success' && needs.preflight.outputs.release_channel == 'nightly' }} - runs-on: blacksmith-8vcpu-ubuntu-2404 - timeout-minutes: 10 - env: - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} - VERCEL_TEAM_SLUG: ${{ vars.VERCEL_TEAM_SLUG }} - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - ref: ${{ needs.preflight.outputs.ref }} - sparse-checkout: | - /* - !/.repos/ - sparse-checkout-cone-mode: false - - - name: Setup Vite+ - uses: voidzero-dev/setup-vp@v1 - with: - node-version-file: package.json - cache: true - run-install: | - args: - - --filter=@t3tools/marketing... - - - name: Deploy marketing site to Vercel - shell: bash - run: | - set -euo pipefail - - if [[ -z "${VERCEL_TOKEN:-}" || -z "${VERCEL_ORG_ID:-}" ]]; then - echo "Missing one or more required Vercel secrets: VERCEL_TOKEN, VERCEL_ORG_ID." >&2 - exit 1 - fi - - VERCEL_PROJECT_ID="$( - curl --fail --silent --show-error \ - --header "Authorization: Bearer $VERCEL_TOKEN" \ - "https://api.vercel.com/v9/projects/t3code-marketing?teamId=$VERCEL_ORG_ID" \ - | jq --exit-status --raw-output '.id' - )" - export VERCEL_PROJECT_ID - - vp dlx vercel@53.1.1 deploy \ - --archive=tgz \ - --prod \ - --yes \ - --token "$VERCEL_TOKEN" \ - --scope "${VERCEL_TEAM_SLUG:-$VERCEL_ORG_ID}" - - 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 - sparse-checkout: | - /* - !/.repos/ - sparse-checkout-cone-mode: false - - - 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 }} - sparse-checkout: | - /* - !/.repos/ - sparse-checkout-cone-mode: false - - - 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/.github/workflows/thread-transfer-report.yml b/.github/workflows/thread-transfer-report.yml deleted file mode 100644 index 23eec72923bd..000000000000 --- a/.github/workflows/thread-transfer-report.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: Thread Transfer Report - -on: - workflow_run: - workflows: [CI] - types: [completed] - -permissions: - actions: read - contents: read - pull-requests: write - -jobs: - publish: - name: Publish PR comment - if: github.event.workflow_run.event == 'pull_request' - runs-on: ubuntu-24.04 - concurrency: - group: thread-transfer-report-${{ github.event.workflow_run.pull_requests[0].number || github.event.workflow_run.id }} - cancel-in-progress: true - steps: - # workflow_run has a write-capable token even for fork PRs. Only load the - # publisher from the trusted default branch and never execute PR code. - - name: Checkout trusted publisher - uses: actions/checkout@v6 - with: - ref: ${{ github.event.repository.default_branch }} - sparse-checkout: .github/scripts - - - name: Test trusted publisher - run: node --test .github/scripts/thread-transfer-report.test.cjs - - - id: resolve - name: Resolve PR and baseline artifacts - uses: actions/github-script@v8 - with: - script: | - const reporter = require("./.github/scripts/thread-transfer-report.cjs"); - await reporter.resolve({ github, context, core }); - - - name: Download PR result - if: steps.resolve.outputs.publish == 'true' && steps.resolve.outputs.pr_artifact == 'true' - uses: actions/download-artifact@v8 - with: - name: thread-transfer-results - path: ${{ runner.temp }}/thread-transfer/pr - github-token: ${{ secrets.GITHUB_TOKEN }} - run-id: ${{ steps.resolve.outputs.pr_run_id }} - - - name: Download main baseline - if: steps.resolve.outputs.publish == 'true' && steps.resolve.outputs.baseline_artifact == 'true' - uses: actions/download-artifact@v8 - with: - name: thread-transfer-results - path: ${{ runner.temp }}/thread-transfer/main - github-token: ${{ secrets.GITHUB_TOKEN }} - run-id: ${{ steps.resolve.outputs.baseline_run_id }} - - - name: Update thread transfer comment - if: steps.resolve.outputs.publish == 'true' - uses: actions/github-script@v8 - env: - PR_NUMBER: ${{ steps.resolve.outputs.pull_number }} - PR_SHA: ${{ steps.resolve.outputs.pr_sha }} - PR_CONCLUSION: ${{ steps.resolve.outputs.pr_conclusion }} - PR_RUN_ID: ${{ steps.resolve.outputs.pr_run_id }} - PR_RESULT_DIR: ${{ runner.temp }}/thread-transfer/pr - BASELINE_SHA: ${{ steps.resolve.outputs.baseline_sha }} - BASELINE_MATCHES_BASE: ${{ steps.resolve.outputs.baseline_matches_base }} - BASELINE_RUN_ID: ${{ steps.resolve.outputs.baseline_run_id }} - BASELINE_RESULT_DIR: ${{ runner.temp }}/thread-transfer/main - with: - script: | - const reporter = require("./.github/scripts/thread-transfer-report.cjs"); - await reporter.publish({ github, context, core }); diff --git a/.github/workflows/web-preview.yml b/.github/workflows/web-preview.yml deleted file mode 100644 index f9cc3b063fcd..000000000000 --- a/.github/workflows/web-preview.yml +++ /dev/null @@ -1,132 +0,0 @@ -name: Web Preview - -# Label a PR `preview:web` to get a hosted-web preview deployment on Vercel for -# that push and every subsequent push. The deployment is a plain (non-prod, -# non-aliased) deploy into the existing hosted-web Vercel project, so the -# latest/nightly channel aliases are never touched. -# -# The build intentionally omits the T3 Connect cloud config (Clerk keys, relay -# URL): previews boot as the hosted-static app with manual pairing only. Pair a -# server into a preview with `t3 pair --tailscale` (or any reachable HTTPS -# backend) and open the pairing URL against the preview origin. -# -# The preview must be opened at the exact deployment URL from the PR comment. -# Vite bakes that URL in as the hosted origin (via VERCEL_URL), and -# `isHostedStaticApp` matches on origin, so branch-alias URLs will not -# self-identify as the hosted app. - -on: - pull_request: - types: [labeled, synchronize, reopened] - -permissions: - contents: read - pull-requests: write - -concurrency: - group: web-preview-${{ github.event.pull_request.number }} - cancel-in-progress: true - -jobs: - deploy: - name: Deploy web preview - # Same-repo PRs only: fork PRs do not receive the Vercel secrets, and this - # workflow should skip rather than fail for them. On `labeled` events, only - # the preview label itself triggers a deploy. - if: >- - github.event.pull_request.head.repo.full_name == github.repository && - contains(github.event.pull_request.labels.*.name, 'preview:web') && - (github.event.action != 'labeled' || github.event.label.name == 'preview:web') - runs-on: blacksmith-8vcpu-ubuntu-2404 - timeout-minutes: 10 - env: - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} - VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} - VERCEL_TEAM_SLUG: ${{ vars.VERCEL_TEAM_SLUG }} - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - ref: ${{ github.event.pull_request.head.sha }} - sparse-checkout: | - /* - !/.repos/ - sparse-checkout-cone-mode: false - - - 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... - - - id: deploy - name: Deploy preview - 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 - - vercel_scope="${VERCEL_TEAM_SLUG:-$VERCEL_ORG_ID}" - - deployment_url="$( - vp dlx vercel@53.1.1 deploy \ - --archive=tgz \ - --yes \ - --token "$VERCEL_TOKEN" \ - --scope "$vercel_scope" - )" - - echo "Deployed $deployment_url" - echo "deployment_url=$deployment_url" >> "$GITHUB_OUTPUT" - - - name: Comment deployment URL - uses: actions/github-script@v8 - env: - DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment_url }} - HEAD_SHA: ${{ github.event.pull_request.head.sha }} - with: - script: | - const marker = ""; - const body = [ - marker, - "### Web preview", - "", - `${process.env.DEPLOYMENT_URL} (for ${process.env.HEAD_SHA.slice(0, 7)})`, - "", - "Open this exact URL — the hosted-app origin is baked in at build time.", - "Pair a server into it with `t3 pair --tailscale`, or paste a host + pairing", - "code under Settings → Connections.", - ].join("\n"); - - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.pull_request.number, - per_page: 100, - }); - const existing = comments.find((comment) => comment.body?.includes(marker)); - - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.pull_request.number, - body, - }); - } diff --git a/.github/workflows/windows-tests.yml b/.github/workflows/windows-tests.yml deleted file mode 100644 index 3a70ad5a26a0..000000000000 --- a/.github/workflows/windows-tests.yml +++ /dev/null @@ -1,81 +0,0 @@ -# On-demand Windows test lane. Manual only: nothing in the suite passes on -# Windows yet, so this exists to give contributors (and agents) a cloud Windows -# box to iterate against. Once the suite is green here, fold it into ci.yml. -# -# gh workflow run windows-tests.yml --ref -f package=packages/shared -# gh workflow run windows-tests.yml --ref -f package=apps/server \ -# -f files="src/process/externalLauncher.test.ts src/cli/theme.test.ts" -# gh run watch && gh run view --log-failed -name: Windows Tests - -on: - workflow_dispatch: - inputs: - package: - description: "Workspace directory to test, e.g. apps/server or packages/shared. Empty runs every package except apps/server." - type: string - default: "" - files: - description: "Space-separated test files relative to the package directory. Empty runs the package's whole suite. Requires package." - type: string - default: "" - -permissions: - contents: read - -jobs: - test: - name: Test (${{ inputs.package || 'all non-server' }}) - runs-on: blacksmith-8vcpu-windows-2025 - timeout-minutes: 45 - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - sparse-checkout: | - /* - !/.repos/ - sparse-checkout-cone-mode: false - - # setup-vp's own cache restores a Linux-shaped store on Windows, which is - # slower than no cache (see #7975). Cache pnpm's Windows store directly. - - name: Setup Vite+ - uses: voidzero-dev/setup-vp@v1 - with: - node-version-file: package.json - cache: false - run-install: false - - - name: Resolve package cache path - id: package_cache_path - shell: pwsh - run: '"path=$(vp pm cache dir)" >> $env:GITHUB_OUTPUT' - - - name: Cache packages - uses: actions/cache@v6 - with: - path: ${{ steps.package_cache_path.outputs.path }} - key: windows-tests-packages-v1-${{ hashFiles('pnpm-lock.yaml') }} - - - name: Install - run: vp install - - - name: Ensure Electron runtime is installed - if: inputs.package == '' || inputs.package == 'apps/desktop' - run: vp run --filter "@t3tools/desktop" ensure:electron - - # `vp run ... test -- ` does not forward positional args to vitest, - # so file-scoped runs call `vp test run` inside the package instead. - - name: Test - shell: pwsh - run: | - $package = '${{ inputs.package }}' - $files = '${{ inputs.files }}' - if ($package -eq '') { - vp run --parallel --concurrency-limit 4 --filter '!t3' --filter '!@t3tools/monorepo' test - } elseif ($files -eq '') { - vp run --filter "./$package" test - } else { - Set-Location $package - vp test run $files.Split(' ') - } diff --git a/infra/relay/scripts/deploy.test.ts b/infra/relay/scripts/deploy.test.ts index 4447c3493fb5..4cbc5cd61311 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, @@ -179,25 +175,6 @@ 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)), - ); -}); - describe("publicConfigFromOutput", () => { it("reads the complete public tracing config from persisted Alchemy output", () => { expect( From e7aa777f323012d864558290a71d2d0d0eb46f87 Mon Sep 17 00:00:00 2001 From: Luiz Ferraz Date: Mon, 14 Sep 2026 21:24:51 +0000 Subject: [PATCH 2/2] chore(ci): limit pull_request trigger to opened and synchronize - update tests for new provider list, Forgejo host parsing, and environment routing --- .github/workflows/ci.yml | 3 +-- apps/server/src/provider/Layers/ProviderRegistry.test.ts | 1 + .../pullRequest/LinkPullRequestDialog.logic.test.ts | 3 ++- .../settings/ProviderSettingsPanel.environment.test.tsx | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9007822ebe39..2a48528dbd53 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,8 +2,7 @@ name: CI on: pull_request: - branches: - - main + types: [opened, synchronize] push: branches: - main diff --git a/apps/server/src/provider/Layers/ProviderRegistry.test.ts b/apps/server/src/provider/Layers/ProviderRegistry.test.ts index 988c89e1e679..c39b7648796a 100644 --- a/apps/server/src/provider/Layers/ProviderRegistry.test.ts +++ b/apps/server/src/provider/Layers/ProviderRegistry.test.ts @@ -2616,6 +2616,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te "codex", "cursor", "grok", + "ohMyPi", "opencode", ]); assert.strictEqual(cursorProvider?.enabled, false); diff --git a/apps/web/src/components/pullRequest/LinkPullRequestDialog.logic.test.ts b/apps/web/src/components/pullRequest/LinkPullRequestDialog.logic.test.ts index 630df4de60c7..420f46e851c1 100644 --- a/apps/web/src/components/pullRequest/LinkPullRequestDialog.logic.test.ts +++ b/apps/web/src/components/pullRequest/LinkPullRequestDialog.logic.test.ts @@ -90,7 +90,8 @@ describe("resolveLinkPullRequestInput", () => { }), ).toEqual({ link: { - host: "forge.example.test:3000", + host: "forge.example.test", + authority: "forge.example.test:3000", repository: "owner/repo", number: 42, url: "http://forge.example.test:3000/owner/repo/pulls/42", diff --git a/apps/web/src/components/settings/ProviderSettingsPanel.environment.test.tsx b/apps/web/src/components/settings/ProviderSettingsPanel.environment.test.tsx index 90d84d1c1efa..6709ab69cdfe 100644 --- a/apps/web/src/components/settings/ProviderSettingsPanel.environment.test.tsx +++ b/apps/web/src/components/settings/ProviderSettingsPanel.environment.test.tsx @@ -190,7 +190,7 @@ describe("EnvironmentProviderSettings routing", () => { it("routes refresh and provider update commands to the selected environment", async () => { atoms.providers = [provider()]; - const panel = renderPanel(); + const panel = renderPanel({ targetInstanceId: codexId }); const refreshButton = visitElements(panel, isRefreshButton); expect(refreshButton).not.toBeNull(); (refreshButton?.props.onClick as (() => void) | undefined)?.(); @@ -241,7 +241,7 @@ describe("EnvironmentProviderSettings routing", () => { ], ])("saves %s on this device without changing the selected server", (action, expected) => { atoms.providers = [provider()]; - const panel = renderPanel(); + const panel = renderPanel({ targetInstanceId: codexId }); const editor = visitElements( panel, (element) => element.props.instanceId === codexId && element.props.mode === "editor",