Part VII · Reach and shipDistribution artifacts
Chapter 37source checked

Distribution: artifacts, channels, and what actually ships

T3 Code distributes several deliberately different products: an npm CLI that contains a bundled web client, platform-specific Electron installers built from a staged closure, a release-controlled hosted web channel, store binaries plus fingerprint-gated mobile OTAs, and checksum-pinned AUR packages derived from the published Linux AppImage. The release matrix—not every target the builder knows—defines what is actually shipped.

What this chapter resolves
  • Identify the concrete artifact and publication boundary for CLI, desktop, hosted web, mobile, and AUR delivery.
  • Trace how the npm CLI embeds the web renderer and why its exact version publishes before the GitHub Release.
  • Separate the desktop builder's supported platform targets from the four artifacts release CI currently produces.
  • Explain release-controlled hosted channels and fingerprint-gated mobile updates without calling either an arbitrary-commit deployment.
  • Follow a marketing download link and an AUR package back to a named GitHub Release asset.

“T3 Code release” does not name one file. It is a coordinated set of artifacts with different consumers, packaging closures, update mechanisms, and trust boundaries: an npm executable, desktop installers, a hosted web deployment, native mobile builds and JavaScript updates, and an Arch package that repackages a release asset. The important question is therefore not “what command builds T3 Code?” but which artifact is this consumer actually receiving, and what gate certified it?

1. One source revision fans out into several delivery contracts

The monorepo can produce a server/CLI bundle, a web build, desktop process output, and mobile app output, but their distribution boundaries diverge quickly. The CLI is an npm package named t3; desktop is an Electron-builder product with installer and updater side files; the hosted web app is a channel alias; mobile has native-store binaries and compatible OTA bundles; the AUR packages consume a published AppImage rather than independently publishing a desktop build.

The release workflow first resolves stable versus nightly metadata, pins all later jobs to its chosen ref, and gives the channel a version, tag, npm dist-tag, and “latest” policy. Quality, relay configuration, desktop builds, and resource-monitor artifacts feed into CLI publication. Only a successful npm publication unlocks the GitHub Release. A successful GitHub Release then unlocks the AUR handoff and hosted web deploy. This is dependency ordering, not an assertion that all downstream consumer updates happen simultaneously.

Figure 37.1 · One revision, several artifact contractssolid arrows describe published or deployed products; dashed arrows are dependency gates
Distribution artifacts and gatesDiagram loading

Zoom with the controls, +/, or Ctrl/ + trackpad scroll. Enable Pan to drag, use two-finger scrolling, or use the arrow keys. 0 fits the diagram; Esc leaves Pan or expanded view.

Distribution artifacts and gates
Text equivalent

A release starts from a pinned source revision. Web output is copied into the CLI package, which publishes to npm. Desktop builds create macOS arm64 and x64 DMGs, a Linux x64 AppImage, and a Windows x64 NSIS installer, then those files go to GitHub Releases. AUR reads the Linux AppImage and digest from that release. Hosted web deploys one release-controlled latest or nightly channel. Mobile store binaries and OTA updates take their own EAS path; OTA requires a matching native fingerprint.

Figure 37.1. A pinned release ref builds the web and server output. The CLI package embeds the web output and publishes to npm before GitHub Release publication. Desktop jobs stage desktop, server, web, dependencies, native resources, and platform packaging into four currently active installer lanes; their files are attached to GitHub Releases. The same release gates an AUR workflow that checksum-pins the released Linux AppImage, while hosted web deploys to the chosen latest or nightly channel. Mobile CI separately reconciles store builds and publishes an OTA only when a matching native fingerprint exists.

2. The npm artifact is a server executable plus an embedded renderer

apps/server/package.json declares a public executable: t3 resolves to ./dist/bin.mjs, and npm publishes only dist. The CLI build first runs the server bundle, then looks for apps/web/dist. When that web output exists it is copied into apps/server/dist/client; development icon replacements are applied there as a post-copy product adjustment. That means the npm artifact can carry both the server entrypoints and a renderer payload—rather than downloading the web product at install time.

apps/server/package.json:1–22 ↗verbatim · json · d17e2567
{
  "name": "t3",
  "version": "0.0.33",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/pingdotgg/t3code",
    "directory": "apps/server"
  },
  "bin": {
    "t3": "./dist/bin.mjs"
  },
  "files": [
    "dist"
  ],
  "type": "module",
  "scripts": {
    "dev": "node --watch src/bin.ts",
    "build:bundle": "vp pack && vp pack src/service-launcher.ts --out-dir dist --no-clean",
    "start": "node dist/bin.mjs",
    "typecheck": "tsgo --noEmit",
    "test": "vp test run"
Read this as: The server package names the `t3` executable and limits npm's published payload to `dist`.
apps/server/scripts/cli.ts:143–176 ↗verbatim · typescript · 4ad46f56
const buildCmd = Command.make(
  "build",
  {
    verbose: Flag.boolean("verbose").pipe(Flag.withDefault(false)),
  },
  (config) =>
    Effect.gen(function* () {
      const path = yield* Path.Path;
      const fs = yield* FileSystem.FileSystem;
      const repoRoot = yield* RepoRoot;
      const serverDir = path.join(repoRoot, "apps/server");
 
      yield* Effect.log("[cli] Running tsdown...");
      yield* runCommand(
        ChildProcess.make(process.execPath, ["--run", "build:bundle"], {
          cwd: serverDir,
          stdout: config.verbose ? "inherit" : "ignore",
          stderr: "inherit",
          shell: false,
        }),
      );
 
      const webDist = path.join(repoRoot, "apps/web/dist");
      const clientTarget = path.join(serverDir, "dist/client");
 
      if (yield* fs.exists(webDist)) {
        yield* fs.copy(webDist, clientTarget);
        yield* applyDevelopmentIconOverrides(repoRoot, serverDir);
        yield* Effect.log("[cli] Bundled web app into dist/client");
      } else {
        yield* Effect.logWarning("[cli] Web dist not found — skipping client bundle.");
      }
    }),
).pipe(Command.withDescription("Build the server package (tsdown + bundle web client)."));
Read this as: The CLI build creates the server bundle, copies the web distribution into `dist/client` when present, and logs the resulting composition.

The build step can warn and continue if apps/web/dist is missing. Publication is stricter: before it rewrites temporary publish metadata it verifies dist/bin.mjs, dist/service-launcher.mjs, and dist/client/index.html. The publish path then resolves workspace catalog/override dependencies into an npm-ready manifest, applies publish icon overrides, invokes package publication with the selected dist-tag/version, and restores the original local metadata and icons in its release finalizer. That restore is a workspace-cleanliness guarantee; it does not undo an already completed npm publication.

3. Desktop packaging constructs a closure, then CI selects four lanes

The desktop artifact builder does more than wrap apps/desktop. It requires emitted desktop, resource, and server directories; verifies that the server bundle is self-contained enough for its runtime needs; requires the embedded server/dist/client/index.html; applies channel-appropriate web branding; and stages Electron output, resources, server output, production dependencies, patches, and platform-specific native materials into a temporary application tree.

The platform split matters. macOS and Linux package a merged application tree. The Windows packaging path deliberately keeps desktop main-process dependencies in app.asar and builds a separate server asar sidecar; it also carries the Windows-to-WSL server support needed by that product. This is an implementation choice about runtime resolution and payload shape, not a claim that every desktop consumer launches a local WSL server.

scripts/build-desktop-artifact.ts:2059–2107 ↗verbatim · typescript · d8686865
  const buildConfig: Record<string, unknown> = {
    appId: DESKTOP_APP_ID,
    productName: resolveDesktopProductName(version),
    artifactName: "T3-Code-${version}-${arch}.${ext}",
    electronLanguages: [...DESKTOP_ELECTRON_LANGUAGES],
    files: [...DESKTOP_FILE_EXCLUSIONS],
    directories: {
      buildResources: "apps/desktop/resources",
    },
    // All platforms keep app.asar fully packed; electron-builder's default
    // smart unpack extracts native libraries, which loaders find in
    // app.asar.unpacked. Windows additionally ships the server tree as the
    // hand-packed server.asar sidecar (see WINDOWS_SERVER_ASAR_RESOURCE).
    extraResources: [
      ...DESKTOP_EXTRA_RESOURCES,
      ...(platform === "win" ? WINDOWS_SERVER_EXTRA_RESOURCES : []),
    ],
  };
  const updateChannel = resolveDesktopUpdateChannel(version);
  const publishConfig = yield* resolveGitHubPublishConfig(updateChannel);
  if (publishConfig) {
    buildConfig.publish = [publishConfig];
  } else if (mockUpdates) {
    buildConfig.publish = [
      {
        provider: "generic",
        url: resolveMockUpdateServerUrl(mockUpdateServerPort),
      },
    ];
  }
 
  if (platform === "mac") {
    buildConfig.mac = {
      target: target === "dmg" ? [target, "zip"] : [target],
      icon: "icon.icns",
      category: "public.app-category.developer-tools",
      protocols: [
        {
          name: "T3 Code",
          schemes: ["t3code", "t3code-dev"],
        },
      ],
      ...(macPasskeySigning
        ? {
            entitlements: macPasskeySigning.entitlementsPath,
            provisioningProfile: macPasskeySigning.provisioningProfilePath,
          }
        : {}),
    };
Read this as: The builder fixes product/artifact naming, packed resources, update publication configuration, and macOS packaging/protocol settings.
scripts/build-desktop-artifact.ts:2133–2174 ↗verbatim · typescript · 86c6ea4e
  if (platform === "linux") {
    buildConfig.linux = {
      target: [target],
      executableName: "t3code",
      icon: "icons",
      category: "Development",
      // electron-builder turns these into MimeType=x-scheme-handler/<scheme>;
      // in the .desktop entry (Exec already gets %U), so browsers can hand
      // t3code:// OAuth callbacks to the app.
      protocols: [
        {
          name: "T3 Code",
          schemes: ["t3code", "t3code-dev"],
        },
      ],
      desktop: {
        entry: {
          StartupWMClass: "t3code",
        },
      },
    };
  }
 
  if (platform === "win") {
    buildConfig.npmRebuild = false;
    // Keep blockmap-based differential downloads enabled while changing the
    // installed file topology. The optimization is in the payload shape, not
    // in trading update bandwidth for install speed.
    buildConfig.nsis = { differentialPackage: true };
    const winConfig: Record<string, unknown> = {
      target: [target],
      icon: "icon.ico",
      // Resource editing applies the product metadata and icon independently
      // of code signing. Disabling it for local unsigned builds leaves the
      // packaged executable with Electron's stock icon.
      signAndEditExecutable: true,
    };
    if (signed) {
      winConfig.azureSignOptions = yield* AzureTrustedSigningOptionsConfig;
    }
    buildConfig.win = winConfig;
  }
Read this as: Linux receives its executable/protocol/desktop configuration; Windows receives NSIS/differential-package and executable/signing configuration.

electron-builder receives an explicit platform flag, architecture, and --publish never; packaging jobs upload their artifacts, then the release job merges the relevant updater manifests and attaches DMGs, ZIPs, AppImages, EXEs, blockmaps, and YAML manifests to GitHub Releases. The builder’s macOS dmg target includes a ZIP updater artifact, which explains why a GitHub Release can have more files than the user-facing installer matrix names.

Shipped matrix vs. possible matrix

Release CI entry Product installer target Status in inspected workflow
macOS arm64 DMG (plus updater ZIP) active
macOS x64 DMG (plus updater ZIP) active
Linux x64 AppImage active
Windows x64 NSIS installer active
Windows arm64 NSIS configuration is structurally possible commented out; not shipped
.github/workflows/release.yml:342–397 ↗verbatim · yaml · e7a92ff3
  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:
Read this as: Release CI declares four active desktop builds and leaves the Windows arm64 entry commented out.

4. Hosted web and mobile use channels, but their compatibility gates differ

The hosted web app deliberately disables Vercel’s normal Git deployment trigger. Release CI performs the deployment, chooses latest for stable or nightly for a nightly release, and aliases the deployment to that channel domain. The public router uses a long-lived secure cookie to route app.t3.codes requests to the selected channel origin. Thus “latest” is a release result, not necessarily the newest source commit in the repository.

Mobile has a separate production workflow. It runs from Linux because its Expo fingerprint needs to match the build environment, reconciles store builds when the declared mobile version needs one, and publishes an OTA only for each platform with at least one completed production build matching the current native fingerprint. The mobile config explicitly uses the fingerprint runtime-version policy: native dependencies, config plugins, and patches participate in the compatibility identity.

.github/workflows/mobile-eas-production.yml:263–286 ↗verbatim · yaml · fe28375e
      - 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
Read this as: The production OTA path checks for a finished build whose fingerprint matches before it publishes a platform update.

5. Download resolution and AUR both trust named release assets

The marketing download page fetches the latest GitHub Release and maps cards by asset suffix: arm64/x64 DMGs, an x64 Windows .exe, and an x86_64 AppImage. If the asset lookup or fetch fails, cards fall back to the releases page rather than inventing a direct URL. Mobile cards are intentionally different: they point to the App Store and Google Play, not a GitHub asset.

apps/marketing/src/pages/download.astro:89–132 ↗verbatim · typescript · 2797f244
<script>
  import { fetchLatestRelease, RELEASES_URL } from "../lib/releases";
 
  async function init() {
    const versionLabel = document.getElementById("version-label");
    // Only release-asset links; mobile store cards have no data-asset and keep their href.
    const cards = document.querySelectorAll<HTMLAnchorElement>("a[data-asset]");
 
    try {
      const release = await fetchLatestRelease();
 
      if (versionLabel && release.tag_name) {
        versionLabel.textContent = `Latest (${release.tag_name})`;
      }
 
      const changelogLink = document.getElementById("changelog-link") as HTMLAnchorElement | null;
      if (changelogLink && release.html_url) {
        changelogLink.href = release.html_url;
        changelogLink.style.display = "";
      }
 
      cards.forEach((card) => {
        const suffix = card.dataset.asset;
        if (!suffix) return;
 
        const match = (release.assets ?? []).find((a) => a.name.endsWith(`-${suffix}`));
        if (match) {
          card.href = match.browser_download_url;
        } else {
          card.href = RELEASES_URL;
        }
      });
    } catch {
      if (versionLabel) {
        versionLabel.textContent = "Could not load release info.";
      }
      cards.forEach((card) => {
        card.href = RELEASES_URL;
      });
    }
  }
 
  init();
</script>
Read this as: The marketing page resolves expected desktop suffixes against the latest GitHub Release and falls back to the release index on errors.

The AUR workflow begins only after GitHub Release publication. Its release script accepts either a stable semver tag or the prescribed nightly tag pattern; selects t3code-bin or t3code-nightly-bin accordingly; reads the exact AppImage digest from that GitHub Release; reads the LICENSE at the same tag; edits the matching PKGBUILD; validates it with namcap and makepkg; then pushes only if its AUR SSH credential exists. A missing key is intentionally a “validated, not published” end state.

packaging/aur/scripts/release.sh:9–44 ↗verbatim · bash · fc2a09bb
if [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
  pkgname='t3code-bin'
elif [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-nightly\.[0-9]{8}\.[0-9]+$ ]]; then
  pkgname='t3code-nightly-bin'
else
  echo "Release $tag does not publish an AUR package."
  exit 0
fi
 
version="${tag#v}"
pkgver="${version//-/_}"
asset_name="T3-Code-${version}-x86_64.AppImage"
release_json="$(gh api "repos/$repo/releases/tags/$tag")"
asset_digest="$(jq -r --arg name "$asset_name" \
  '.assets[] | select(.name == $name) | .digest' <<<"$release_json")"
appimage_sha256="${asset_digest#sha256:}"
 
if [[ ! "$appimage_sha256" =~ ^[0-9a-f]{64}$ ]]; then
  echo "Release $tag is missing $asset_name or its SHA-256 digest." >&2
  exit 1
fi
 
work_dir="$(mktemp -d)"
trap 'rm -rf -- "$work_dir"' EXIT
gh api -H 'Accept: application/vnd.github.raw' \
  "repos/$repo/contents/LICENSE?ref=$tag" > "$work_dir/LICENSE"
license_sha256="$(sha256sum "$work_dir/LICENSE" | awk '{print $1}')"
 
package_dir="$repo_root/packaging/aur/$pkgname"
cd "$package_dir"
sed -Ei \
  -e "s/^pkgver=.*/pkgver=$pkgver/" \
  -e "s/^pkgrel=.*/pkgrel=$pkgrel/" \
  -e "/# AppImage$/s/'[0-9a-f]{64}'/'$appimage_sha256'/" \
  -e "/# upstream license$/s/'[0-9a-f]{64}'/'$license_sha256'/" \
  PKGBUILD
Read this as: The AUR release script chooses a stable or nightly package from the tag and obtains the exact AppImage digest from that release before changing package metadata.

Work the artifact factory

Choose a delivery lane below and advance it one gate at a time. The desktop lane is useful for testing the crucial distinction: the builder can describe Windows arm64, but the final gate reports that the inspected release matrix does not activate it.

Interactive artifact factory · Chapter 37

Choose a delivery lane, then inspect its gates

Each lane has a different output and failure boundary. Advance manually; no state progresses on its own.

Delivery lane

npm CLI · Gate 1 of 4

  1. 1Build server
  2. 2Embed renderer
  3. 3Assert publish inputs
  4. 4Publish exact version

npm CLI

Build server

The server bundle emits dist/bin.mjs and the service launcher.

Output
t3 package
Failure boundary
Bundled code is not yet a complete product surface.
Lane rule
The published package contains dist only; publishing first proves the exact server version clients will invoke.
Static artifact map
LaneOutputLast gate
npm CLIt3 packagePublish exact version — The release is held if npm publication fails.
Desktop releaseDMG · AppImage · NSISRelease four lanes — Windows arm64 is commented out in the release matrix: capability is not a published artifact.
Hosted + mobileChannel deploy · store build · OTAGate OTA — Native drift skips OTA instead of shipping JavaScript into an incompatible binary.
AUR bridgeArch package metadataValidate then publish — Without the AUR SSH key, validation completes but publication is skipped.

The durable reading rule is: source builds components; packaging assembles a consumer-specific closure; release policy selects and publishes a subset; channel and platform compatibility gates decide what a particular client may receive. Treating those as one step hides the real failure boundaries.

T3
Source-locked editionRead against fa219001d · 23 Aug 2026
Book search

Find a concept, module, or source path

Type two or more characters.