Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/t2code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
Release requests are handled in the local Codex task on the maintainer's machine.
The task reviews and adapts the analytics overlay, opens the source update PR,
waits for CI, merges it, and dispatches `release.yml`. GitHub-hosted runners build
and publish the cross-platform packages. No scheduled sync or homelab runner is
required.
and publish Linux packages only, for x64 and ARM64. Keep this release policy
unless the maintainer explicitly changes it. Releases include AppImage and Debian
installers, CLI archives, and Linux updater feeds. No scheduled sync or homelab
runner is required.

## Prepare a source update

Expand Down
68 changes: 20 additions & 48 deletions .github/t2code/release.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@ import * as NodePath from "node:path";
const { resolve } = NodePath;
import { nightlyVersion, verifyNightly, forkRelease, forkRepository } from "./nightly.mjs";
import { assertSha } from "./overlay.mjs";
import {
mergeUpdateManifests,
parseUpdateManifest,
serializeUpdateManifest,
} from "../../scripts/lib/update-manifest.ts";
import { parseUpdateManifest } from "../../scripts/lib/update-manifest.ts";

const repo = forkRepository;

Expand Down Expand Up @@ -48,24 +44,6 @@ function sourcePin(sha, tag) {
return pin;
}

function mergeManifests(directory, first, second, output, platform) {
const primary = resolve(directory, first);
const secondary = resolve(directory, second);
if (!existsSync(primary) || !existsSync(secondary))
throw new Error(`Missing ${platform} update manifest.`);
const merged = mergeUpdateManifests(
parseUpdateManifest(readFileSync(primary, "utf8"), first, platform),
parseUpdateManifest(readFileSync(secondary, "utf8"), second, platform),
platform,
);
rmSync(primary);
rmSync(secondary);
writeFileSync(
resolve(directory, output),
serializeUpdateManifest(merged, { platformLabel: platform }),
);
}

function assemble() {
const directory = resolve(process.argv[3]);
const version = process.env.T2_RELEASE_VERSION;
Expand All @@ -75,21 +53,21 @@ function assemble() {
const sha = assertSha(process.env.T2_RELEASE_SHA);
const pin = sourcePin(sha, tag);
const upstream = pin.commit;
mergeManifests(directory, "nightly-mac.yml", "nightly-mac-x64.yml", "nightly-mac.yml", "macOS");
mergeManifests(
directory,
"nightly-win-x64.yml",
"nightly-win-arm64.yml",
"nightly.yml",
"Windows",
);
for (const arch of ["x64", "arm64"]) {
for (const ext of ["dmg", "AppImage", "exe"]) {
const artifactArch = ext === "AppImage" && arch === "x64" ? "x86_64" : arch;
if (!existsSync(resolve(directory, `T2-Code-${version}-${artifactArch}.${ext}`)))
throw new Error(`Missing ${arch} ${ext} installer.`);
}
const requiredAssets = [
`T2-Code-${version}-x86_64.AppImage`,
`T2-Code-${version}-arm64.AppImage`,
`T2-Code-${version}-amd64.deb`,
`T2-Code-${version}-arm64.deb`,
`t3-${version}-linux-x64.tar.gz`,
`t3-${version}-linux-arm64.tar.gz`,
"nightly-linux.yml",
"nightly-linux-arm64.yml",
];
for (const file of requiredAssets) {
if (!existsSync(resolve(directory, file)))
throw new Error(`Missing Linux release asset: ${file}`);
}
const allowedAssets = new Set([...requiredAssets, "provenance.json", "SHA256SUMS"]);
const provenance = {
repository: repo,
commit: sha,
Expand All @@ -102,22 +80,16 @@ function assemble() {
};
writeFileSync(resolve(directory, "provenance.json"), JSON.stringify(provenance, null, 2) + "\n");
for (const file of readdirSync(directory)) {
if (file === "builder-debug.yml" || file.endsWith("-win-arm64.yml"))
rmSync(resolve(directory, file));
if (file === "builder-debug.yml") rmSync(resolve(directory, file));
else if (/^latest.*\.yml$/.test(file))
throw new Error(`Stable updater feed in nightly release: ${file}`);
else if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(file))
throw new Error(`Unsafe release filename: ${file}`);
}
for (const name of ["nightly-linux.yml", "nightly-linux-arm64.yml"]) {
if (!existsSync(resolve(directory, name)))
throw new Error(`Missing Linux update manifest: ${name}`);
else if (!allowedAssets.has(file)) throw new Error(`Unexpected Linux release asset: ${file}`);
}
for (const file of readdirSync(directory).filter((name) => /^nightly.*\.yml$/.test(name))) {
for (const file of ["nightly-linux.yml", "nightly-linux-arm64.yml"]) {
const manifest = parseUpdateManifest(
readFileSync(resolve(directory, file), "utf8"),
file,
"desktop",
"Linux",
);
if (manifest.version !== version) throw new Error(`Wrong updater version in ${file}.`);
for (const asset of manifest.files) {
Expand All @@ -144,7 +116,7 @@ function assemble() {
writeFileSync(resolve(directory, "SHA256SUMS"), hashes + "\n");
writeFileSync(
"release-notes.md",
`T2 Code Nightly ${version}\n\nDownload the installer for your operating system and processor. These builds are unsigned; macOS and Windows may require an installation override.\n\nThis distribution runs locally without a product account. Analytics and external diagnostic exporters are disabled in code. Local diagnostics are retained. Provider harnesses use their own credentials. Upstream Git, update, and optional remote-connection features are retained.\n\nUpstream nightly: https://github.com/pingdotgg/t3code/releases/tag/${tag}\nSource: https://github.com/${repo}/commit/${sha}\n${upstream ? `Upstream: https://github.com/pingdotgg/t3code/commit/${upstream}\n` : ""}\nInstall this nightly separately if you use an older 0.1.x T2 release. Its stable update channel does not switch to nightly automatically.\n\nSHA256SUMS covers the attached artifacts. provenance.json records the build source.\n`,
`T2 Code Nightly ${version}\n\nDownload the AppImage or Debian package for your Linux processor, x64 or ARM64. Linux CLI archives are also attached.\n\nThis distribution runs locally without a product account. Analytics and external diagnostic exporters are disabled in code. Local diagnostics are retained. Provider harnesses use their own credentials. Upstream Git, update, and optional remote-connection features are retained.\n\nUpstream nightly: https://github.com/pingdotgg/t3code/releases/tag/${tag}\nSource: https://github.com/${repo}/commit/${sha}\n${upstream ? `Upstream: https://github.com/pingdotgg/t3code/commit/${upstream}\n` : ""}\nInstall this nightly separately if you use an older 0.1.x T2 release. Its stable update channel does not switch to nightly automatically.\n\nSHA256SUMS covers the attached artifacts. provenance.json records the build source.\n`,
);
}

Expand Down
85 changes: 65 additions & 20 deletions .github/t2code/release.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -61,26 +61,39 @@ function fixture() {
const bytes = Buffer.from("installer fixture");
const hash = NodeCrypto.createHash("sha512").update(bytes).digest("base64");
for (const [arch, ext] of [
["x64", "dmg"],
["arm64", "dmg"],
["x86_64", "AppImage"],
["arm64", "AppImage"],
["x64", "exe"],
["arm64", "exe"],
["amd64", "deb"],
["arm64", "deb"],
]) {
NodeFS.writeFileSync(NodePath.join(assets, `T2-Code-${version}-${arch}.${ext}`), bytes);
}
for (const [name, arch, ext] of [
["nightly-mac.yml", "arm64", "dmg"],
["nightly-mac-x64.yml", "x64", "dmg"],
["nightly-win-x64.yml", "x64", "exe"],
["nightly-win-arm64.yml", "arm64", "exe"],
["nightly-linux.yml", "x86_64", "AppImage"],
["nightly-linux-arm64.yml", "arm64", "AppImage"],
for (const arch of ["x64", "arm64"]) {
NodeFS.writeFileSync(NodePath.join(assets, `t3-${version}-linux-${arch}.tar.gz`), bytes);
}
for (const [name, appImageArch, debArch] of [
["nightly-linux.yml", "x86_64", "amd64"],
["nightly-linux-arm64.yml", "arm64", "arm64"],
]) {
NodeFS.writeFileSync(
NodePath.join(assets, name),
`version: ${version}\nfiles:\n - url: T2-Code-${version}-${arch}.${ext}\n sha512: ${hash}\n size: ${bytes.length}\n${ext === "AppImage" ? " blockMapSize: 4\n" : ""}releaseDate: '2026-09-16T00:00:00.000Z'\n`,
serializeUpdateManifest(
{
version,
releaseDate: "2026-09-16T00:00:00.000Z",
files: [
{
url: `T2-Code-${version}-${appImageArch}.AppImage`,
sha512: hash,
size: bytes.length,
blockMapSize: 4,
},
{ url: `T2-Code-${version}-${debArch}.deb`, sha512: hash, size: bytes.length },
],
extras: {},
},
{ platformLabel: "Linux" },
),
);
}
return {
Expand All @@ -106,7 +119,7 @@ function fixture() {
}

NodeTest.test(
"release assembly records the pinned upstream and hashes each published artifact",
"Linux-only release assembly records the pinned upstream and hashes each artifact",
() => {
const { assets, sha, upstream, assemble } = fixture();
const result = assemble();
Expand All @@ -132,11 +145,6 @@ NodeTest.test(
.digest("hex"),
);
}
NodeAssert.match(
NodeFS.readFileSync(NodePath.join(assets, "nightly.yml"), "utf8"),
/arm64\.exe/,
);
NodeAssert.match(NodeFS.readFileSync(NodePath.join(assets, "nightly.yml"), "utf8"), /x64\.exe/);
},
);

Expand Down Expand Up @@ -171,14 +179,14 @@ NodeTest.test("the actual AppImage manifest shape retains its embedded block map
NodeTest.test("release assembly rejects modified installer bytes and missing architectures", () => {
for (const remove of [false, true]) {
const { assets, assemble } = fixture();
const path = NodePath.join(assets, `T2-Code-${version}-arm64.exe`);
const path = NodePath.join(assets, `T2-Code-${version}-arm64.AppImage`);
if (remove) NodeFS.rmSync(path);
else NodeFS.appendFileSync(path, "modified");
const result = assemble();
NodeAssert.notEqual(result.status, 0);
NodeAssert.match(
result.stderr,
remove ? /Missing arm64 exe installer/ : /Updater checksum mismatch/,
remove ? /Missing Linux release asset: .*arm64.AppImage/ : /Updater checksum mismatch/,
);
}
});
Expand All @@ -200,3 +208,40 @@ NodeTest.test("release assembly rejects stable feeds and mismatched nightly iden
NodeAssert.notEqual(stable.status, 0);
NodeAssert.match(stable.stderr, /Stable updater feed/);
});

NodeTest.test(
"release assembly requires Debian packages, CLI archives and feeds for both Linux architectures",
() => {
for (const name of [
`T2-Code-${version}-amd64.deb`,
`T2-Code-${version}-arm64.deb`,
`t3-${version}-linux-x64.tar.gz`,
`t3-${version}-linux-arm64.tar.gz`,
"nightly-linux.yml",
"nightly-linux-arm64.yml",
]) {
const { assets, assemble } = fixture();
NodeFS.rmSync(NodePath.join(assets, name));
const result = assemble();
NodeAssert.notEqual(result.status, 0);
NodeAssert.ok(result.stderr.includes(`Missing Linux release asset: ${name}`), result.stderr);
}
},
);

NodeTest.test("release assembly rejects non-Linux installers, archives and updater feeds", () => {
for (const name of [
`T2-Code-${version}-x64.dmg`,
`T2-Code-${version}-arm64.exe`,
`t3-${version}-darwin-arm64.tar.gz`,
`t3-${version}-win32-x64.zip`,
"nightly-mac.yml",
"nightly.yml",
]) {
const { assets, assemble } = fixture();
NodeFS.writeFileSync(NodePath.join(assets, name), "non-Linux artifact");
const result = assemble();
NodeAssert.notEqual(result.status, 0);
NodeAssert.ok(result.stderr.includes(`Unexpected Linux release asset: ${name}`), result.stderr);
}
});
84 changes: 3 additions & 81 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,40 +104,6 @@ jobs:
if-no-files-found: error
retention-days: 3

desktop_mac_arm64:
name: macOS arm64
needs: [metadata, build_bundle]
uses: ./.github/workflows/release-desktop.yml
with:
label: macOS arm64
runner: macos-15
platform: mac
target: dmg
arch: arm64
rust_target: aarch64-apple-darwin
resource_key: darwin-arm64
cli_archive: true
version: ${{ needs.metadata.outputs.version }}
ref: ${{ needs.metadata.outputs.sha }}
release_channel: nightly

desktop_mac_x64:
name: macOS x64
needs: [metadata, build_bundle]
uses: ./.github/workflows/release-desktop.yml
with:
label: macOS x64
runner: macos-15-intel
platform: mac
target: dmg
arch: x64
rust_target: x86_64-apple-darwin
resource_key: darwin-x64
cli_archive: false
version: ${{ needs.metadata.outputs.version }}
ref: ${{ needs.metadata.outputs.sha }}
release_channel: nightly

desktop_linux_x64:
name: Linux x64
needs: [metadata, build_bundle]
Expand Down Expand Up @@ -172,52 +138,8 @@ jobs:
ref: ${{ needs.metadata.outputs.sha }}
release_channel: nightly

desktop_win_x64:
name: Windows x64
needs: [metadata, build_bundle, desktop_linux_x64]
uses: ./.github/workflows/release-desktop.yml
with:
label: Windows x64
runner: windows-2025
platform: win
target: nsis
arch: x64
rust_target: x86_64-pc-windows-msvc
resource_key: win32-x64
cli_archive: true
version: ${{ needs.metadata.outputs.version }}
ref: ${{ needs.metadata.outputs.sha }}
release_channel: nightly

desktop_win_arm64:
name: Windows arm64
needs: [metadata, build_bundle, desktop_linux_arm64]
uses: ./.github/workflows/release-desktop.yml
with:
label: Windows arm64
runner: windows-11-arm
platform: win
target: nsis
arch: arm64
rust_target: aarch64-pc-windows-msvc
resource_key: win32-arm64
cli_archive: true
version: ${{ needs.metadata.outputs.version }}
ref: ${{ needs.metadata.outputs.sha }}
release_channel: nightly

publish:
needs:
[
metadata,
quality,
desktop_mac_arm64,
desktop_mac_x64,
desktop_linux_x64,
desktop_linux_arm64,
desktop_win_x64,
desktop_win_arm64,
]
needs: [metadata, quality, desktop_linux_x64, desktop_linux_arm64]
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
Expand All @@ -240,12 +162,12 @@ jobs:
node-version: 24
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: desktop-*
pattern: desktop-linux-*
merge-multiple: true
path: release-assets
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: cli-*
pattern: cli-linux-*
merge-multiple: true
path: release-assets
- name: Assemble feeds, provenance and checksums
Expand Down
Loading