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 a9f8d00682609..c3f89e6afdffd 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-nightly-release + 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" @@ -527,7 +535,10 @@ 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 }} 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 +589,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..a9b0cd6eef4e4 100644 --- a/docs/operations/release.md +++ b/docs/operations/release.md @@ -58,6 +58,31 @@ 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. +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`, +`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. + +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 diff --git a/scripts/build-desktop-artifact.test.ts b/scripts/build-desktop-artifact.test.ts index bddc1428e41ef..622133b252ac3 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, @@ -1487,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 b1544f9b95bd7..4c2870e9fac80 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 = [ { @@ -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],