From cd244ce56c9a7d0aadbeac18515b22dac3a6f5a6 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Wed, 28 Oct 2026 12:50:01 -0700 Subject: [PATCH 1/5] fix(release): enable desktop updates from fork releases --- .github/workflows/fork-release.yml | 81 +++++++++++++++++++++------ .github/workflows/release-desktop.yml | 12 +++- docs/operations/release.md | 21 +++++++ 3 files changed, 96 insertions(+), 18 deletions(-) diff --git a/.github/workflows/fork-release.yml b/.github/workflows/fork-release.yml index a9f8d00682609..addd27ba1bc41 100644 --- a/.github/workflows/fork-release.yml +++ b/.github/workflows/fork-release.yml @@ -4,20 +4,20 @@ name: Fork release # release.yml needs upstream's Blacksmith runners, signing identities, and # deploy secrets, so this reuses only its per-platform packaging # (release-desktop.yml) on GitHub-hosted runners. -# -# Builds are unsigned and carry a -preview.YYYYMMDD.N version, which ships no -# update feed (see isDesktopPreviewVersion in scripts/build-desktop-artifact.ts). -# An install never auto-updates, so it cannot jump to an upstream build; install -# a newer release to update. on: push: branches: [main] workflow_dispatch: + inputs: + publish: + description: Publish a signed nightly release (leave off to validate packaging only) + type: boolean + default: false concurrency: - group: fork-release - cancel-in-progress: true + group: fork-release-${{ github.ref }} + cancel-in-progress: false permissions: # release-desktop.yml's Windows job lists this run's artifacts to wait for the @@ -70,7 +70,7 @@ jobs: run: | set -euo pipefail base_version="$(node -p "require('./apps/desktop/package.json').version")" - version="${base_version}-preview.$(date -u +%Y%m%d).${GITHUB_RUN_NUMBER}" + version="${base_version}-nightly.$(date -u +%Y%m%d).${GITHUB_RUN_NUMBER}" node scripts/update-release-package-versions.ts "$version" echo "version=$version" >> "$GITHUB_OUTPUT" @@ -119,6 +119,13 @@ jobs: arch: arm64 rust_target: aarch64-apple-darwin resource_key: darwin-arm64 + - label: macOS x64 + runner: macos-26 + platform: mac + target: dmg + arch: x64 + rust_target: x86_64-apple-darwin + resource_key: darwin-x64 # The Windows job waits for this job's CLI archive by its label. - label: Linux x64 runner: ubuntu-24.04 @@ -141,11 +148,20 @@ jobs: 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 + rust_target: aarch64-pc-windows-msvc + resource_key: win32-arm64 uses: ./.github/workflows/release-desktop.yml + secrets: inherit with: version: ${{ needs.bundle.outputs.version }} ref: ${{ github.sha }} - release_channel: preview + release_channel: nightly + require_macos_signing: ${{ github.event_name == 'push' || inputs.publish }} relay_client_tracing: false clerk_publishable_key: ${{ needs.bundle.outputs.clerk_publishable_key }} clerk_jwt_template: ${{ needs.bundle.outputs.clerk_jwt_template }} @@ -158,16 +174,30 @@ jobs: arch: ${{ matrix.arch }} rust_target: ${{ matrix.rust_target }} resource_key: ${{ matrix.resource_key }} - cli_archive: true + cli_archive: ${{ matrix.platform != 'mac' || matrix.arch != 'x64' }} publish: - name: Publish GitHub Release + name: Prepare and publish GitHub Release needs: [bundle, desktop] runs-on: ubuntu-24.04 timeout-minutes: 15 permissions: contents: write steps: + - name: Checkout + shell: bash + run: | + set -euo pipefail + git init . + git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" + git fetch --no-tags --depth=1 origin "$GITHUB_SHA" + git sparse-checkout set --no-cone '/*' '!/.repos/' + git checkout --detach FETCH_HEAD + + - uses: ./.github/actions/setup-ci-deps + with: + install-args: --filter=@t3tools/scripts... + - name: Download desktop installers uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: @@ -182,7 +212,22 @@ jobs: path: assets merge-multiple: true + - name: Prepare updater metadata and checksums + shell: bash + run: | + set -euo pipefail + node scripts/merge-update-manifests.ts --platform mac assets/nightly-mac.yml assets/nightly-mac-x64.yml + node scripts/merge-update-manifests.ts --platform win assets/nightly-win-x64.yml assets/nightly-win-arm64.yml assets/nightly.yml + cd assets + rm nightly-mac-x64.yml nightly-win-x64.yml nightly-win-arm64.yml + for manifest in nightly.yml nightly-mac.yml nightly-linux.yml nightly-linux-arm64.yml; do + test -s "$manifest" + done + rm -f builder-debug.yml + sha256sum t3-*.tar.gz t3-*.zip > SHA256SUMS + - name: Publish release + if: github.event_name == 'push' || inputs.publish shell: bash env: GH_TOKEN: ${{ github.token }} @@ -193,16 +238,18 @@ jobs: cat > notes.md < "$dest/node.exe" @@ -528,6 +532,8 @@ jobs: APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }} MACOS_PROVISIONING_PROFILE: ${{ secrets.MACOS_PROVISIONING_PROFILE }} + REQUIRE_MACOS_SIGNING: ${{ inputs.require_macos_signing }} + T3CODE_DESKTOP_UPDATE_REPOSITORY: ${{ github.repository }} T3CODE_CLERK_PASSKEY_RP_DOMAINS: ${{ vars.CLERK_PASSKEY_RP_DOMAINS }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} @@ -578,6 +584,10 @@ jobs: echo "macOS signing enabled." args+=(--signed) else + if [[ "$REQUIRE_MACOS_SIGNING" == "true" ]]; then + echo "A macOS update release requires CSC_LINK, CSC_KEY_PASSWORD, APPLE_API_KEY, APPLE_API_KEY_ID, and APPLE_API_ISSUER." >&2 + exit 1 + fi echo "macOS signing disabled (missing one or more Apple signing secrets)." fi elif [[ "${{ inputs.platform }}" == "win" ]]; then diff --git a/docs/operations/release.md b/docs/operations/release.md index 39f4f63ddd91b..c9be3636f5b75 100644 --- a/docs/operations/release.md +++ b/docs/operations/release.md @@ -58,6 +58,27 @@ the version. - nightly releases are aliased to the `nightly` hosted app channel - Signing is optional and auto-detected per platform from secrets. +## Fork desktop releases + +`fork-release.yml` packages macOS, Windows, and Linux for x64 and arm64 on +GitHub-hosted runners. A push to `main` publishes a `v-nightly..` +prerelease in the fork. The bundled updater follows that repository's Nightly feed. +Manual dispatch defaults to packaging only; enable `publish` to publish the selected ref. +No npm, mobile, relay, or hosted web deployment runs in this workflow. + +Publishing requires the macOS signing secrets described below: `CSC_LINK`, +`CSC_KEY_PASSWORD`, `APPLE_API_KEY`, `APPLE_API_KEY_ID`, `APPLE_API_ISSUER`, and +`MACOS_PROVISIONING_PROFILE`, plus the `APPLE_TEAM_ID` repository variable. Use a +Developer ID Application certificate and a profile belonging to the same team. +The reusable packaging workflow receives repository secrets and refuses to publish +an unsigned macOS update. Windows uses Azure Trusted Signing when its existing +secrets are configured; otherwise its installer is unsigned. + +Install the first nightly release manually on machines running old preview or source +builds: those builds have no update feed. Subsequent releases use the app's Update +control with the Nightly channel. Switching channels stays within the fork; it does +not switch to upstream releases. + ## Pull request macOS previews Labeling a PR `preview:mac` publishes a signed, notarized Apple Silicon DMG with T3 Connect enabled From f02885e15318a5e35093bcac13f92ec22942d9ee Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Wed, 28 Oct 2026 12:58:19 -0700 Subject: [PATCH 2/5] fix(release): select cache paths by host and serialize nightly publishing Use the Windows tsdown cache on Windows hosts while preserving the Linux cache for Windows cross-builds. Keep checksum verification before extracting the target Node executable. Serialize fork releases across refs because every publishing run updates the same Nightly feed; retain non-cancelling behavior for an active release. Validated with the required changed-file gate, including formatting and 37 package typechecks, plus 58 packaging/manifest tests, actionlint on both workflows, and six executable cache-step cases. --- .github/workflows/fork-release.yml | 2 +- .github/workflows/release-desktop.yml | 6 +++++- docs/operations/release.md | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/fork-release.yml b/.github/workflows/fork-release.yml index addd27ba1bc41..c4899144bd81f 100644 --- a/.github/workflows/fork-release.yml +++ b/.github/workflows/fork-release.yml @@ -16,7 +16,7 @@ on: default: false concurrency: - group: fork-release-${{ github.ref }} + group: fork-release cancel-in-progress: false permissions: diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index 85f3f6b4e21ae..a2f4211cc0451 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -332,7 +332,11 @@ jobs: set -euo pipefail name="node-v$VERSION-win-${{ inputs.arch }}" dir="$RUNNER_TEMP/node-download" - dest="$(cygpath -u "$LOCALAPPDATA")/tsdown/Caches/node/v$VERSION/win-${{ inputs.arch }}" + if [[ "$RUNNER_OS" == "Windows" ]]; then + dest="$(cygpath -u "$LOCALAPPDATA")/tsdown/Caches/node/v$VERSION/win-${{ inputs.arch }}" + else + dest="${XDG_CACHE_HOME:-$HOME/.cache}/tsdown/node/v$VERSION/win-${{ inputs.arch }}" + fi mkdir -p "$dest" (cd "$dir" && grep " $name.zip\$" SHASUMS256.txt | sha256sum -c -) unzip -p "$dir/$name.zip" "$name/node.exe" > "$dest/node.exe" diff --git a/docs/operations/release.md b/docs/operations/release.md index c9be3636f5b75..29829ffafbe1c 100644 --- a/docs/operations/release.md +++ b/docs/operations/release.md @@ -64,6 +64,7 @@ the version. GitHub-hosted runners. A push to `main` publishes a `v-nightly..` prerelease in the fork. The bundled updater follows that repository's Nightly feed. Manual dispatch defaults to packaging only; enable `publish` to publish the selected ref. +Release runs share a queue across refs because every ref updates the same Nightly feed. No npm, mobile, relay, or hosted web deployment runs in this workflow. Publishing requires the macOS signing secrets described below: `CSC_LINK`, From 02abcb0a9b29a08609a912202c4ec50d7644c8b6 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Wed, 28 Oct 2026 13:09:56 -0700 Subject: [PATCH 3/5] fix(desktop): account for bundled codegraph in Windows payload checks --- scripts/build-desktop-artifact.test.ts | 6 +++++- scripts/build-desktop-artifact.ts | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/build-desktop-artifact.test.ts b/scripts/build-desktop-artifact.test.ts index bddc1428e41ef..bc94b7dd861b7 100644 --- a/scripts/build-desktop-artifact.test.ts +++ b/scripts/build-desktop-artifact.test.ts @@ -1355,7 +1355,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { ), ); - it.effect("allows the vendored pstack files within the Windows payload budget", () => + it.effect("allows bundled platform packages and pstack within the Windows payload budget", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -1364,6 +1364,10 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { path.join(import.meta.dirname, "../packages/pstack/upstream"), path.join(fixture.packagedAppDir, "resources/pstack/upstream"), ); + yield* stageSelfContainedPlatformPackages( + path.join(import.meta.dirname, "../apps/server/node_modules"), + path.join(fixture.packagedAppDir, "resources/node_modules"), + ); yield* validateWindowsPackagedPayload({ stageDistDir: fixture.stageDistDir, appExecutableName: fixture.appExecutableName, diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index b1544f9b95bd7..726b22d574807 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -1064,8 +1064,8 @@ export function resolveWindowsServerAsarIgnoreGlobs(arch: typeof BuildArch.Type) ]; } -// The external pstack pack contributes about 200 files in addition to the server's native sidecars. -export const WINDOWS_PACKAGED_PAYLOAD_FILE_LIMIT = 300; +// Codegraph requires its unpacked JS and nested dependencies, alongside pstack and native sidecars. +export const WINDOWS_PACKAGED_PAYLOAD_FILE_LIMIT = 1800; export const WINDOWS_SERVER_RESOURCE_SOURCE_DIR = "apps/desktop/prod-resources/windows-server"; export const WINDOWS_SERVER_EXTRA_RESOURCES = [ { From fa42c221422a00f2c9bc026a826a86d87a342a37 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Wed, 28 Oct 2026 13:43:43 -0700 Subject: [PATCH 4/5] fix(release): support fork-owned macOS signing identifiers --- .github/workflows/release-desktop.yml | 1 + docs/operations/release.md | 3 ++ scripts/build-desktop-artifact.test.ts | 70 +++++++++++++++++--------- scripts/build-desktop-artifact.ts | 8 ++- 4 files changed, 56 insertions(+), 26 deletions(-) diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index a2f4211cc0451..8bffaa39150bd 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -535,6 +535,7 @@ jobs: APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }} + T3CODE_MACOS_APP_ID: ${{ vars.MACOS_APP_ID }} MACOS_PROVISIONING_PROFILE: ${{ secrets.MACOS_PROVISIONING_PROFILE }} REQUIRE_MACOS_SIGNING: ${{ inputs.require_macos_signing }} T3CODE_DESKTOP_UPDATE_REPOSITORY: ${{ github.repository }} diff --git a/docs/operations/release.md b/docs/operations/release.md index 29829ffafbe1c..a9b0cd6eef4e4 100644 --- a/docs/operations/release.md +++ b/docs/operations/release.md @@ -71,6 +71,9 @@ Publishing requires the macOS signing secrets described below: `CSC_LINK`, `CSC_KEY_PASSWORD`, `APPLE_API_KEY`, `APPLE_API_KEY_ID`, `APPLE_API_ISSUER`, and `MACOS_PROVISIONING_PROFILE`, plus the `APPLE_TEAM_ID` repository variable. Use a Developer ID Application certificate and a profile belonging to the same team. +Register your own explicit app identifier with Associated Domains and set the +`MACOS_APP_ID` repository variable to match its provisioning profile; the upstream +identifier cannot be registered by another Apple team. The reusable packaging workflow receives repository secrets and refuses to publish an unsigned macOS update. Windows uses Azure Trusted Signing when its existing secrets are configured; otherwise its installer is unsigned. diff --git a/scripts/build-desktop-artifact.test.ts b/scripts/build-desktop-artifact.test.ts index bc94b7dd861b7..622133b252ac3 100644 --- a/scripts/build-desktop-artifact.test.ts +++ b/scripts/build-desktop-artifact.test.ts @@ -1491,31 +1491,53 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { }); }); - it("normalizes explicit macOS passkey RP domains and renders required entitlements", () => { - const configuration = resolveMacPasskeySigningConfiguration({ - T3CODE_APPLE_TEAM_ID: "ABC1234567", - T3CODE_MACOS_PROVISIONING_PROFILE: "/tmp/t3code.provisionprofile", - T3CODE_CLERK_PASSKEY_RP_DOMAINS: - " Clerk.Example.com,example.clerk.accounts.dev,clerk.example.com ", - }); - const entitlements = renderMacPasskeyEntitlements(configuration, { - touchIdKeychainAccessGroup: undefined, - browserPasskeys: false, - }); + it.effect("normalizes explicit macOS passkey RP domains and renders required entitlements", () => + Effect.gen(function* () { + const configuration = resolveMacPasskeySigningConfiguration({ + T3CODE_APPLE_TEAM_ID: "ABC1234567", + T3CODE_MACOS_PROVISIONING_PROFILE: "/tmp/t3code.provisionprofile", + T3CODE_MACOS_APP_ID: "com.scriptedalchemy.t3code", + T3CODE_CLERK_PASSKEY_RP_DOMAINS: + " Clerk.Example.com,example.clerk.accounts.dev,clerk.example.com ", + }); + const entitlements = renderMacPasskeyEntitlements(configuration, { + touchIdKeychainAccessGroup: undefined, + browserPasskeys: false, + }); - assert.deepStrictEqual(configuration.rpDomains, [ - "clerk.example.com", - "example.clerk.accounts.dev", - ]); - assert.include(entitlements, "ABC1234567.com.t3tools.t3code"); - assert.include(entitlements, "webcredentials:clerk.example.com"); - assert.include(entitlements, "webcredentials:example.clerk.accounts.dev"); - assert.include(entitlements, "com.apple.security.cs.allow-jit"); - assert.match(entitlements, /com\.apple\.security\.device\.camera<\/key>\s*/u); - assert.match(entitlements, /com\.apple\.security\.device\.audio-input<\/key>\s*/u); - assert.notInclude(entitlements, "keychain-access-groups"); - assert.notInclude(entitlements, "com.apple.developer.web-browser.public-key-credential"); - }); + assert.deepStrictEqual(configuration.rpDomains, [ + "clerk.example.com", + "example.clerk.accounts.dev", + ]); + assert.include(entitlements, "ABC1234567.com.scriptedalchemy.t3code"); + const build = yield* createBuildConfig( + "mac", + "dmg", + "1.2.3", + true, + false, + undefined, + undefined, + ); + assert.equal(build.appId, configuration.appId); + assert.include(entitlements, "webcredentials:clerk.example.com"); + assert.include(entitlements, "webcredentials:example.clerk.accounts.dev"); + assert.include(entitlements, "com.apple.security.cs.allow-jit"); + assert.match(entitlements, /com\.apple\.security\.device\.camera<\/key>\s*/u); + assert.match( + entitlements, + /com\.apple\.security\.device\.audio-input<\/key>\s*/u, + ); + assert.notInclude(entitlements, "keychain-access-groups"); + assert.notInclude(entitlements, "com.apple.developer.web-browser.public-key-credential"); + }).pipe( + Effect.provide( + ConfigProvider.layer( + ConfigProvider.fromEnv({ env: { T3CODE_MACOS_APP_ID: "com.scriptedalchemy.t3code" } }), + ), + ), + ), + ); it("grants in-app browser passkey entitlements only when the provisioning profile does", () => { const configuration = { appId: "com.t3tools.t3code", teamId: "ABC1234567" }; diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index 726b22d574807..4c2870e9fac80 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -1299,7 +1299,7 @@ export function resolveMacPasskeySigningConfiguration( } return { - appId: DESKTOP_APP_ID, + appId: env.T3CODE_MACOS_APP_ID?.trim() || DESKTOP_APP_ID, teamId, rpDomains: uniqueRpDomains, provisioningProfilePath, @@ -2798,8 +2798,12 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* ( wslRuntimeBundled = false, arch?: typeof BuildArch.Type, ) { + const macAppId = yield* Config.String("T3CODE_MACOS_APP_ID").pipe(Config.option); const buildConfig: Record = { - appId: DESKTOP_APP_ID, + appId: + platform === "mac" + ? Option.getOrUndefined(macAppId)?.trim() || DESKTOP_APP_ID + : DESKTOP_APP_ID, productName: resolveDesktopProductName(version), artifactName: "T3-Code-${version}-${arch}.${ext}", electronLanguages: [...DESKTOP_ELECTRON_LANGUAGES], From 755bd997494d61c7486048731d90ac073241a010 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Wed, 28 Oct 2026 14:54:29 -0700 Subject: [PATCH 5/5] fix(ci): repair cached Windows tools and fork release validation --- .github/actions/setup-ci-deps/action.yml | 9 +++++++-- .github/workflows/fork-release.yml | 2 +- apps/server/src/storageCleanup.ts | 7 ++++++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/actions/setup-ci-deps/action.yml b/.github/actions/setup-ci-deps/action.yml index e9b7ae70a36ad..330de88425734 100644 --- a/.github/actions/setup-ci-deps/action.yml +++ b/.github/actions/setup-ci-deps/action.yml @@ -44,8 +44,13 @@ runs: if: steps.vp_cache.outputs.cache-hit == 'true' shell: bash run: | - echo "$HOME/.vite-plus/bin" >> "$GITHUB_PATH" - "$HOME/.vite-plus/bin/vp" env use "${{ steps.vp.outputs.node }}" + vp_bin="$HOME/.vite-plus/bin" + if [[ "$RUNNER_OS" == "Windows" ]]; then + cygpath -w "$vp_bin" >> "$GITHUB_PATH" + else + echo "$vp_bin" >> "$GITHUB_PATH" + fi + "$vp_bin/vp" env use "${{ steps.vp.outputs.node }}" # vp gives each Node.js download attempt 10 minutes by default, so one # stalled stream holds the job that long. It retries 3 times and resumes diff --git a/.github/workflows/fork-release.yml b/.github/workflows/fork-release.yml index c4899144bd81f..c3f89e6afdffd 100644 --- a/.github/workflows/fork-release.yml +++ b/.github/workflows/fork-release.yml @@ -16,7 +16,7 @@ on: default: false concurrency: - group: fork-release + group: fork-nightly-release cancel-in-progress: false permissions: diff --git a/apps/server/src/storageCleanup.ts b/apps/server/src/storageCleanup.ts index 526aaffa2abb3..4f383eda8ae50 100644 --- a/apps/server/src/storageCleanup.ts +++ b/apps/server/src/storageCleanup.ts @@ -504,7 +504,12 @@ const make = Effect.gen(function* () { ); if (result === null) return null; if (result.exitCode === 0 || result.exitCode === allowedExitCode) return result; - if (result.stderr.toLowerCase().includes("not a git repository")) return null; + const stderr = result.stderr.toLowerCase(); + if ( + stderr.includes("not a git repository") || + stderr.includes("gitfile does not point to a valid repository") + ) + return null; return yield* failure( result.stderr.trim().split(/\r?\n/)[0]?.slice(0, 1000) || "command failed", );