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.
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.
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.
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.
{
"name": "t3",
"version": "0.0.40",
"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": "tsc --noEmit",
"test": "vp test run"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)."));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.
}
});
yield* restoreRelativeSymlinks(source, destination);
},
);
const verifyPackagedBundleIsSelfContained = Effect.fn("verifyPackagedBundleIsSelfContained")(
function* (input: { readonly asarPath: string; readonly verbose: boolean }) {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const probeRoot = yield* fs.makeTempDirectoryScoped({
prefix: "t3code-bundle-selfcheck-",
});
const extractedApp = path.join(probeRoot, "extracted");
const probeApp = path.join(probeRoot, "app");
yield* Effect.try({
try: () => extractAll(input.asarPath, extractedApp),
catch: (cause) =>
new BundleNotSelfContainedError({
exitCode: -1,
output: `Could not extract ${input.asarPath} for the bundle self-containment check: ${String(cause)}`,
}),
});
// Keep the existing symlink isolation guard even though the sidecar stage
// is hoisted and should be physical. A future package-manager layout change
// must not let the probe resolve through the build tree.
yield* copyDirectoryPreservingSymlinks(extractedApp, probeApp);
// Guard the guard: if anything above the probe provides a node_modules, a
// missing dependency would resolve there and the check would pass while the
// packaged tree is broken.
for (const candidate of ancestorNodeModulesPaths(probeApp, path.sep)) {
if (yield* fs.exists(candidate).pipe(Effect.orElseSucceed(() => false))) {
return yield* new BundleNotSelfContainedError({
exitCode: -1,
output: `Refusing to report success: ${candidate} is visible from the probe directory, so bare imports could resolve outside the packaged tree. Remove or rename it, or point TMPDIR somewhere without one.`,
});
}
}
const entryPoint = path.join(probeApp, "apps/server/dist/bin.mjs");
if (!(yield* fs.exists(entryPoint).pipe(Effect.orElseSucceed(() => false)))) {
return yield* new BundleNotSelfContainedError({
exitCode: -1,
output: `Expected the server entry at ${entryPoint}.`,
});
} {
label: "server sidecar self-containment check (node bin.mjs --version)",
verbose: input.verbose,
},
).pipe(
// Printing a version should be immediate. A regression that blocks (on
// stdin, a port, a lock) would otherwise hang release CI until the job
// times out with nothing useful in the log.
Effect.timeout(BUNDLE_SELF_CHECK_TIMEOUT),
Effect.catchTags({
TimeoutError: () =>
Effect.fail(
new BundleNotSelfContainedError({
exitCode: -1,
output: `The packaged bundle did not print its version within ${Duration.toSeconds(BUNDLE_SELF_CHECK_TIMEOUT)}s; it is hanging rather than failing to resolve.`,
}),
),
BuildCommandFailedError: (error) =>
Effect.fail(
new BundleNotSelfContainedError({
exitCode: error.exitCode,
output: `${error.stderrTail ?? ""}${error.stdoutTail ?? ""}`.trim(),
}),
),
}),
);
},
);
export const stageLinuxCaptureHelper = Effect.fn("stageLinuxCaptureHelper")(function* (input: {
readonly backend: "kde" | "hyprland";
readonly repoRoot: string;
readonly stageResourcesDir: string;
readonly arch: typeof BuildArch.Type;
readonly verbose: boolean;
}) {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const [rustTarget] = resolveResourceMonitorRustTargets("linux", input.arch);
// Release CI restores these binaries from a cache keyed on the crate sources and
// skips the Rust toolchain on a hit, so the build must be skippable too.
const reuseHelpers = yield* Config.boolean("T3CODE_DESKTOP_REUSE_LINUX_CAPTURE_HELPERS").pipe(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 |
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: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.
- 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
done5. 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.
<div class="platform-header">
<svg class="platform-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M12.504 0c-.155 0-.315.008-.48.021-4.226.333-3.105 4.807-3.17 6.298-.076 1.092-.3 1.953-1.05 3.02-.885 1.051-2.127 2.75-2.716 4.521-.278.832-.41 1.684-.287 2.489a.424.424 0 00-.11.135c-.26.268-.45.6-.663.839-.199.199-.485.267-.797.4-.313.136-.658.269-.864.68-.09.189-.136.394-.132.602 0 .199.027.4.055.536.058.399.116.728.04.97-.249.68-.28 1.145-.106 1.484.174.334.535.47.94.601.81.2 1.91.135 2.774.6.926.466 1.866.67 2.616.47.526-.116.97-.464 1.208-.946.587-.003 1.23-.269 2.26-.334.699-.058 1.574.267 2.577.2.025.134.063.198.114.333l.003.003c.391.778 1.113 1.132 1.884 1.071.771-.06 1.592-.536 2.257-1.306.631-.765 1.683-1.084 2.378-1.503.348-.199.629-.469.649-.853.023-.4-.2-.811-.714-1.376v-.097l-.003-.003c-.17-.2-.25-.535-.338-.926-.085-.401-.182-.786-.492-1.046h-.003c-.059-.054-.123-.067-.188-.135a.357.357 0 00-.19-.064c.431-1.278.264-2.55-.173-3.694-.533-1.41-1.465-2.638-2.175-3.483-.796-1.005-1.576-1.957-1.56-3.368.026-2.152.236-6.133-3.544-6.139zm.529 3.405h.013c.213 0 .396.062.584.198.19.135.33.332.438.533.105.259.158.459.166.724 0-.02.006-.04.006-.06v.105a.086.086 0 01-.004-.021l-.004-.024a1.807 1.807 0 01-.15.706.953.953 0 01-.213.335.71.71 0 00-.088-.042c-.104-.045-.198-.064-.284-.133a1.312 1.312 0 00-.22-.066c.05-.06.146-.133.183-.198.053-.128.082-.264.088-.402v-.02a1.21 1.21 0 00-.061-.4c-.045-.134-.101-.2-.183-.333-.084-.066-.167-.132-.267-.132h-.016c-.093 0-.176.03-.262.132a.8.8 0 00-.205.334 1.18 1.18 0 00-.09.4v.019c.002.089.008.179.02.267-.193-.067-.438-.135-.607-.202a1.635 1.635 0 01-.018-.2v-.02a1.772 1.772 0 01.15-.768c.082-.22.232-.406.43-.533a.985.985 0 01.594-.2zm-2.962.059h.036c.142 0 .27.048.399.135.146.129.264.288.344.465.09.199.14.4.153.667v.004c.007.134.006.2-.002.266v.08c-.03.007-.056.018-.083.024-.152.055-.274.135-.393.2.012-.09.013-.18.003-.267v-.015c-.012-.133-.04-.2-.082-.333a.613.613 0 00-.166-.267.248.248 0 00-.183-.064h-.021c-.071.006-.13.04-.186.132a.552.552 0 00-.12.27.944.944 0 00-.023.33v.015c.012.135.037.2.08.334.046.134.098.2.166.268.01.009.02.018.034.024-.07.057-.117.07-.176.136a.304.304 0 01-.131.068 2.62 2.62 0 01-.275-.402 1.772 1.772 0 01-.155-.667 1.759 1.759 0 01.08-.668 1.43 1.43 0 01.283-.535c.128-.133.26-.2.418-.2zm1.37 1.706c.332 0 .733.065 1.216.399.293.2.523.269 1.052.468h.003c.255.136.405.266.478.399v-.131a.571.571 0 01.016.47c-.123.31-.516.643-1.063.842v.002c-.268.135-.501.333-.775.465-.276.135-.588.292-1.012.267a1.139 1.139 0 01-.448-.067 3.566 3.566 0 01-.322-.198c-.195-.135-.363-.332-.612-.465v-.005h-.005c-.4-.246-.616-.512-.686-.71-.07-.268-.005-.47.193-.6.224-.135.38-.271.483-.336.104-.074.143-.102.176-.131h.002v-.003c.169-.202.436-.47.839-.601.139-.036.294-.065.466-.065zm2.8 2.142c.358 1.417 1.196 3.475 1.735 4.473.286.534.855 1.659 1.102 3.024.156-.005.33.018.513.064.646-1.671-.546-3.467-1.089-3.966-.22-.2-.232-.335-.123-.335.59.534 1.365 1.572 1.646 2.757.13.535.16 1.104.021 1.67.067.028.135.06.205.067 1.032.534 1.413.938 1.23 1.537v-.043c-.06-.003-.12 0-.18 0h-.016c.151-.467-.182-.825-1.065-1.224-.915-.4-1.646-.336-1.77.465-.008.043-.013.066-.018.135-.068.023-.139.053-.209.064-.43.268-.662.669-.793 1.187-.13.533-.17 1.156-.205 1.869v.003c-.02.334-.17.838-.319 1.35-1.5 1.072-3.58 1.538-5.348.334a2.645 2.645 0 00-.402-.533 1.45 1.45 0 00-.275-.333c.182 0 .338-.03.465-.067a.615.615 0 00.314-.334c.108-.267 0-.697-.345-1.163-.345-.467-.931-.995-1.788-1.521-.63-.4-.986-.87-1.15-1.396-.165-.534-.143-1.085-.015-1.645.245-1.07.873-2.11 1.274-2.763.107-.065.037.135-.408.974-.396.751-1.14 2.497-.122 3.854a8.123 8.123 0 01.647-2.876c.564-1.278 1.743-3.504 1.836-5.268.048.036.217.135.289.202.218.133.38.333.59.465.21.201.477.335.876.335.039.003.075.006.11.006.412 0 .73-.134.997-.268.29-.134.52-.334.74-.4h.005c.467-.135.835-.402 1.044-.7zm2.185 8.958c.037.6.343 1.245.882 1.377.588.134 1.434-.333 1.791-.765l.211-.01c.315-.007.577.01.847.268l.003.003c.208.199.305.53.391.876.085.4.154.78.409 1.066.486.527.645.906.636 1.14l.003-.007v.018l-.003-.012c-.015.262-.185.396-.498.595-.63.401-1.746.712-2.457 1.57-.618.737-1.37 1.14-2.036 1.191-.664.053-1.237-.2-1.574-.898l-.005-.003c-.21-.4-.12-1.025.056-1.69.176-.668.428-1.344.463-1.897.037-.714.076-1.335.195-1.814.12-.465.308-.797.641-.984l.045-.022zm-10.814.049h.01c.053 0 .105.005.157.014.376.055.706.333 1.023.752l.91 1.664.003.003c.243.533.754 1.064 1.189 1.637.434.598.77 1.131.729 1.57v.006c-.057.744-.48 1.148-1.125 1.294-.645.135-1.52.002-2.395-.464-.968-.536-2.118-.469-2.857-.602-.369-.066-.61-.2-.723-.4-.11-.2-.113-.602.123-1.23v-.004l.002-.003c.117-.334.03-.752-.027-1.118-.055-.401-.083-.71.043-.94.16-.334.396-.4.69-.533.294-.135.64-.202.915-.47h.002v-.002c.256-.268.445-.601.668-.838.19-.201.38-.336.663-.336zm7.159-9.074c-.435.201-.945.535-1.488.535-.542 0-.97-.267-1.28-.466-.154-.134-.28-.268-.373-.335-.164-.134-.144-.333-.074-.333.109.016.129.134.199.2.096.066.215.2.36.333.292.2.68.467 1.167.467.485 0 1.053-.267 1.398-.466.195-.135.445-.334.648-.467.156-.136.149-.267.279-.267.128.016.034.134-.147.332a8.097 8.097 0 01-.69.468zm-1.082-1.583V5.64c-.006-.02.013-.042.029-.05.074-.043.18-.027.26.004.063 0 .16.067.15.135-.006.049-.085.066-.135.066-.055 0-.092-.043-.141-.068-.052-.018-.146-.008-.163-.065zm-.551 0c-.02.058-.113.049-.166.066-.047.025-.086.068-.14.068-.05 0-.13-.02-.136-.068-.01-.066.088-.133.15-.133.08-.031.184-.047.259-.005.019.009.036.03.03.05v.02h.003z"/></svg>
<h2 class="platform-name">Linux</h2>
</div>
<div class="download-cards">
<a class="download-card" data-asset="x86_64.AppImage" href={RELEASES_URL}>
<span class="card-icon">
<Image class="card-icon--stable" src={iconStable} width={28} height={28} loading="lazy" {...imageProps} />
<Image class="card-icon--nightly" src={iconNightly} width={28} height={28} loading="lazy" {...imageProps} />
</span>
<span class="card-text">
<span class="card-arch">x86_64</span>
<span class="card-format">AppImage</span>
</span>
</a>
</div>
</section>
<!-- Mobile. Stable only, there is no nightly mobile build. -->
<section class="platform-section platform-section--mobile" data-rise style="--d:420ms">
<div class="platform-header">
<svg class="platform-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="7" y="2" width="10" height="20" rx="2.5"/><path d="M11 18.5h2"/></svg>
<h2 class="platform-name">Mobile</h2>
</div>
<div class="download-cards">
<a class="download-card" href={IOS_APP_STORE_URL} target="_blank" rel="noopener noreferrer">
<span class="card-text">
<span class="card-arch">iPhone & iPad</span>
<span class="card-format">App Store</span>
</span>
</a>
<a class="download-card" href={ANDROID_PLAY_STORE_URL} target="_blank" rel="noopener noreferrer">
<span class="card-text">
<span class="card-arch">Android</span>
<span class="card-format">Google Play</span>
</span>
</a>
</div>
</section>
<section class="platform-section platform-section--cli" data-rise style="--d:420ms">
<div class="platform-header">
<svg class="platform-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 7l5 5-5 5M12 17h7"/></svg>
<h2 class="platform-name">Terminal</h2>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.
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'/" \
PKGBUILDWork 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.
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.
npm CLI · Gate 1 of 4
- 1Build server
- 2Embed renderer
- 3Assert publish inputs
- 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
| Lane | Output | Last gate |
|---|---|---|
| npm CLI | t3 package | Publish exact version — The release is held if npm publication fails. |
| Desktop release | DMG · AppImage · NSIS | Release four lanes — Windows arm64 is commented out in the release matrix: capability is not a published artifact. |
| Hosted + mobile | Channel deploy · store build · OTA | Gate OTA — Native drift skips OTA instead of shipping JavaScript into an incompatible binary. |
| AUR bridge | Arch package metadata | Validate 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.