From 73d50df75f9b60389eb4c4ef6d6de82dcae9452d Mon Sep 17 00:00:00 2001 From: Jacob Hands Date: Wed, 21 Oct 2026 05:18:29 -0400 Subject: [PATCH 1/2] ci(release): jobs after publish download prebuilt tools instead of installing the workspace --- .github/workflows/publish-aur.yml | 2 + .github/workflows/release.yml | 151 ++++++++++++++++-------------- apps/server/scripts/cli.ts | 48 +--------- apps/server/scripts/cliErrors.ts | 11 --- docs/operations/release.md | 6 +- 5 files changed, 88 insertions(+), 130 deletions(-) diff --git a/.github/workflows/publish-aur.yml b/.github/workflows/publish-aur.yml index 4ab6db89ab703..a679ee9463453 100644 --- a/.github/workflows/publish-aur.yml +++ b/.github/workflows/publish-aur.yml @@ -49,6 +49,8 @@ jobs: - name: Checkout packaging sources uses: actions/checkout@v6 + with: + sparse-checkout: packaging - name: Create unprivileged build user run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 65ea056e85a19..079381742ccac 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -731,6 +731,58 @@ jobs: resource_key: win32-arm64 cli_archive: true + # Everything after the desktop builds runs one job after another, so their + # Node inputs are built here, alongside those builds, and those jobs skip + # checkout and install. + prepare_publish: + name: Prepare publish tools + needs: [preflight] + if: ${{ !cancelled() && needs.preflight.result == 'success' }} + runs-on: blacksmith-2vcpu-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: Build the npm launcher + run: node scripts/build-npm-launcher.ts --version "${{ needs.preflight.outputs.version }}" --output-dir "$RUNNER_TEMP/npm-packages" + + # import.meta.main is defined because the runner's own Node may predate it. + - name: Bundle publish scripts + run: | + set -euo pipefail + mkdir -p publish-tools + cp "$RUNNER_TEMP/npm-packages/t3.tgz" .github/scripts/mirror-play-apk.cjs publish-tools/ + banner="import{createRequire as __cr}from'node:module';const require=__cr(import.meta.url);" + for script in merge-update-manifests notify-discord-release; do + scripts/node_modules/.bin/esbuild "scripts/$script.ts" --bundle --platform=node --format=esm \ + --target=node20 --define:import.meta.main=true --banner:js="$banner" \ + --outfile="publish-tools/$script.mjs" + done + + - name: Upload publish tools + uses: actions/upload-artifact@v7 + with: + name: publish-tools + path: publish-tools/* + if-no-files-found: error + retention-days: 1 + release: name: Publish GitHub Release needs: @@ -747,30 +799,19 @@ jobs: desktop_linux_arm64, desktop_win_x64, desktop_win_arm64, + prepare_publish, ] - if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.quality.result == 'success' && needs.test.result == 'success' && needs.test_web.result == 'success' && needs.test_server.result == 'success' && needs.desktop_mac_arm64.result == 'success' && needs.desktop_mac_x64.result == 'success' && needs.desktop_linux_x64.result == 'success' && needs.desktop_linux_arm64.result == 'success' && needs.desktop_win_x64.result == 'success' && needs.desktop_win_arm64.result == 'success' }} + if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.quality.result == 'success' && needs.test.result == 'success' && needs.test_web.result == 'success' && needs.test_server.result == 'success' && needs.desktop_mac_arm64.result == 'success' && needs.desktop_mac_x64.result == 'success' && needs.desktop_linux_x64.result == 'success' && needs.desktop_linux_arm64.result == 'success' && needs.desktop_win_x64.result == 'success' && needs.desktop_win_arm64.result == 'success' && needs.prepare_publish.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 + - name: Download publish tools + uses: actions/download-artifact@v8 with: - node-version-file: package.json - cache: true - run-install: | - args: - - --filter=@t3tools/scripts... + name: publish-tools + path: ${{ runner.temp }}/publish-tools - name: Download all desktop artifacts uses: actions/download-artifact@v8 @@ -795,7 +836,7 @@ jobs: APK_OUTPUT_DIR: release-assets run: | if [ -n "$GOOGLE_PLAY_SERVICE_ACCOUNT_JSON" ]; then - node .github/scripts/mirror-play-apk.cjs + node "$RUNNER_TEMP/publish-tools/mirror-play-apk.cjs" fi # Installers verify archives against this file, so it is written from the @@ -841,7 +882,7 @@ jobs: 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" + node "$RUNNER_TEMP/publish-tools/merge-update-manifests.mjs" --platform mac "$arm64_manifest" "$x64_manifest" rm -f "$x64_manifest" fi done @@ -854,7 +895,7 @@ jobs: arm64_manifest="${x64_manifest%-x64.yml}-arm64.yml" merged_manifest="${x64_manifest%-win-x64.yml}.yml" if [[ -f "$arm64_manifest" ]]; then - node scripts/merge-update-manifests.ts --platform win "$x64_manifest" "$arm64_manifest" "$merged_manifest" + node "$RUNNER_TEMP/publish-tools/merge-update-manifests.mjs" --platform win "$x64_manifest" "$arm64_manifest" "$merged_manifest" rm -f "$x64_manifest" "$arm64_manifest" else mv "$x64_manifest" "$merged_manifest" @@ -950,31 +991,29 @@ jobs: contents: read id-token: write steps: - - name: Checkout + # Only for the Node version, whose npm must support trusted publishing. + - name: Checkout package.json uses: actions/checkout@v6 with: ref: ${{ needs.preflight.outputs.ref }} - sparse-checkout: | - /* - !/.repos/ + sparse-checkout: package.json sparse-checkout-cone-mode: false - - name: Setup Vite+ - uses: voidzero-dev/setup-vp@v1 + - name: Setup Node + uses: actions/setup-node@v7 with: node-version-file: package.json - cache: true - run-install: | - args: - - --filter=t3... - - --filter=@t3tools/scripts... - - name: Build the npm launcher - run: node scripts/build-npm-launcher.ts --version "${{ needs.preflight.outputs.version }}" --output-dir npm-packages + - name: Download publish tools + uses: actions/download-artifact@v8 + with: + name: publish-tools + path: ${{ runner.temp }}/publish-tools - name: Publish the npm launcher + working-directory: ${{ runner.temp }}/publish-tools run: | - if ! node apps/server/scripts/cli.ts publish --packages-dir npm-packages --tag "${{ needs.preflight.outputs.cli_dist_tag }}" --provenance --verbose; then + if ! npm publish --access public --tag "${{ needs.preflight.outputs.cli_dist_tag }}" --provenance ./t3.tgz; then echo "::error::npm publish failed. Make sure package t3 has a trusted publisher registered for .github/workflows/release.yml (see docs/operations/release.md)." >&2 exit 1 fi @@ -1110,22 +1149,6 @@ jobs: 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: false - run-install: false - - name: Alias channel shell: bash run: | @@ -1154,13 +1177,13 @@ jobs: vercel_scope_args=(--scope "${VERCEL_TEAM_SLUG:-$VERCEL_ORG_ID}") echo "Aliasing $DEPLOYMENT_URL to $channel_domain." - vp dlx vercel@53.1.1 alias set "$DEPLOYMENT_URL" "$channel_domain" \ + npx --yes 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" \ + npx --yes vercel@53.1.1 alias set "$DEPLOYMENT_URL" "$router_domain" \ --token "$VERCEL_TOKEN" \ "${vercel_scope_args[@]}" fi @@ -1370,23 +1393,11 @@ jobs: 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 + - name: Download publish tools + uses: actions/download-artifact@v8 with: - node-version-file: package.json - cache: true - run-install: | - args: - - --filter=@t3tools/scripts... + name: publish-tools + path: ${{ runner.temp }}/publish-tools - name: Read published release notes if: needs.preflight.outputs.is_prerelease == 'true' @@ -1413,7 +1424,7 @@ jobs: 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 \ + node "$RUNNER_TEMP/publish-tools/notify-discord-release.mjs" prerelease \ --role-id "$DISCORD_MENTION_ROLE_ID" \ --release-name "${{ needs.preflight.outputs.release_name }}" \ --release-version "${{ needs.preflight.outputs.version }}" \ @@ -1428,7 +1439,7 @@ jobs: 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 \ + node "$RUNNER_TEMP/publish-tools/notify-discord-release.mjs" latest \ --role-id "$DISCORD_MENTION_ROLE_ID" \ --release-name "${{ needs.preflight.outputs.release_name }}" \ --release-version "${{ needs.preflight.outputs.version }}" \ diff --git a/apps/server/scripts/cli.ts b/apps/server/scripts/cli.ts index fca0adeb690cd..ea1f59a02f485 100644 --- a/apps/server/scripts/cli.ts +++ b/apps/server/scripts/cli.ts @@ -13,7 +13,6 @@ import { DEVELOPMENT_ICON_OVERRIDES } from "../../../scripts/lib/brand-assets.ts import { findEsmImportsOfExternalPackages } from "../../../scripts/lib/cli-executable-imports.ts"; import { resolveSpawnCommand } from "@t3tools/shared/shell"; import { - ServerCliBuildAssetMissingError, ServerCliCommandExitError, ServerCliDevelopmentIconSourceMissingError, ServerCliDevelopmentIconTargetMissingError, @@ -153,52 +152,9 @@ const buildExeCmd = Command.make( ), ); -/** - * Publishes the `t3.tgz` launcher scripts/build-npm-launcher.ts produced. The - * launcher downloads its executable from the GitHub Release, so the release - * workflow runs this only after that release is published. - */ -const publishCmd = Command.make( - "publish", - { - packagesDir: Flag.String("packages-dir").pipe( - Flag.withDescription("Output dir of scripts/build-npm-launcher.ts."), - ), - tag: Flag.String("tag").pipe(Flag.withDefault("latest")), - access: Flag.String("access").pipe(Flag.withDefault("public")), - provenance: Flag.Boolean("provenance").pipe(Flag.withDefault(false)), - dryRun: Flag.Boolean("dry-run").pipe(Flag.withDefault(false)), - verbose: Flag.Boolean("verbose").pipe(Flag.withDefault(false)), - }, - (config) => - Effect.gen(function* () { - const path = yield* Path.Path; - const fs = yield* FileSystem.FileSystem; - const packagesDir = path.resolve(config.packagesDir); - const launcherTarball = path.join(packagesDir, "t3.tgz"); - if (!(yield* fs.exists(launcherTarball))) { - return yield* new ServerCliBuildAssetMissingError({ assetPath: launcherTarball }); - } - - const args = ["publish", "--access", config.access, "--tag", config.tag]; - if (config.provenance) args.push("--provenance"); - if (config.dryRun) args.push("--dry-run"); - const spawnCommand = yield* resolveSpawnCommand("npm", [...args, launcherTarball]); - yield* Effect.log(`[cli] npm ${args.join(" ")} t3.tgz`); - yield* runCommand( - ChildProcess.make(spawnCommand.command, spawnCommand.args, { - cwd: packagesDir, - stdout: config.verbose ? "inherit" : "ignore", - stderr: "inherit", - shell: spawnCommand.shell, - }), - ); - }), -).pipe(Command.withDescription("Publish the t3 launcher to npm.")); - const cli = Command.make("cli").pipe( - Command.withDescription("T3 server build & publish CLI."), - Command.withSubcommands([buildCmd, buildExeCmd, publishCmd]), + Command.withDescription("T3 server build CLI."), + Command.withSubcommands([buildCmd, buildExeCmd]), ); Command.run(cli, { version: "0.0.0" }).pipe( diff --git a/apps/server/scripts/cliErrors.ts b/apps/server/scripts/cliErrors.ts index 5c02281aabb1b..8ee9184629db8 100644 --- a/apps/server/scripts/cliErrors.ts +++ b/apps/server/scripts/cliErrors.ts @@ -36,17 +36,6 @@ export class ServerCliDevelopmentIconTargetMissingError extends Schema.TaggedErr } } -export class ServerCliBuildAssetMissingError extends Schema.TaggedError()( - "ServerCliBuildAssetMissingError", - { - assetPath: Schema.String, - }, -) { - override get message(): string { - return `Missing build asset: ${this.assetPath}. Run the build subcommand first.`; - } -} - export class ServerCliExecutableImportError extends Schema.TaggedError()( "ServerCliExecutableImportError", { diff --git a/docs/operations/release.md b/docs/operations/release.md index c6725efdcdad4..ed326bde86da9 100644 --- a/docs/operations/release.md +++ b/docs/operations/release.md @@ -514,9 +514,9 @@ blockmaps, with a 60 MB maximum for a representative sidecar-to-sidecar update. ## 0) npm OIDC trusted publishing setup (CLI) -After the GitHub Release is published, the workflow runs `node scripts/build-npm-launcher.ts`, then -`node apps/server/scripts/cli.ts publish --packages-dir npm-packages`, which runs `npm publish` on -`t3.tgz`. That is the only package published per release. +`prepare_publish` builds `t3.tgz` with `node scripts/build-npm-launcher.ts` alongside the desktop +builds. After the GitHub Release is published, `publish_cli` runs `npm publish` on it. That is the +only package published per release. Checklist: From 0d892b5fca239f1a2cd55a7fd36670bd5ff4ea35 Mon Sep 17 00:00:00 2001 From: Jacob Hands Date: Wed, 21 Oct 2026 05:22:02 -0400 Subject: [PATCH 2/2] ci(release): web deploy keeps vp dlx and checks out only package.json --- .github/workflows/release.yml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 079381742ccac..64266224ef7a8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1149,6 +1149,20 @@ jobs: T3CODE_WEB_NIGHTLY_DOMAIN: ${{ vars.T3CODE_WEB_NIGHTLY_DOMAIN }} VERCEL_TEAM_SLUG: ${{ vars.VERCEL_TEAM_SLUG }} steps: + - name: Checkout package.json + uses: actions/checkout@v6 + with: + ref: ${{ needs.preflight.outputs.ref }} + sparse-checkout: package.json + sparse-checkout-cone-mode: false + + - name: Setup Vite+ + uses: voidzero-dev/setup-vp@v1 + with: + node-version-file: package.json + cache: false + run-install: false + - name: Alias channel shell: bash run: | @@ -1177,13 +1191,13 @@ jobs: vercel_scope_args=(--scope "${VERCEL_TEAM_SLUG:-$VERCEL_ORG_ID}") echo "Aliasing $DEPLOYMENT_URL to $channel_domain." - npx --yes vercel@53.1.1 alias set "$DEPLOYMENT_URL" "$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." - npx --yes vercel@53.1.1 alias set "$DEPLOYMENT_URL" "$router_domain" \ + vp dlx vercel@53.1.1 alias set "$DEPLOYMENT_URL" "$router_domain" \ --token "$VERCEL_TOKEN" \ "${vercel_scope_args[@]}" fi