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
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ For an already-started LastCode checkpoint service run, use the resumable
the action handoff and result rules in `docs/lastcode/release.md`; this action
does not start or repair the service.

For an authorized local Apple Silicon package build, select the exact installable
tag and use the resumable **Build Local Package** action (`lc-build-local-package`)
as described in `docs/lastcode/release.md`. End the turn after launch; do not run
the build helper directly or poll build logs. The action builds only; installation
and restart remain separate decisions. If another Action continuation is pending,
end the turn so its result can arrive, then list Actions again. An automated
message alone does not make Actions unavailable; use the actual disabled reason.

## Pull requests

- Never make a PR unless the developer explicitly asks you to do so.
Expand Down
35 changes: 35 additions & 0 deletions docs/lastcode/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,41 @@ checkpoint stamp cannot authorize a PR merge.

## Apple Silicon DMG builder

### Resumable local builds

For an authorized local Apple Silicon build, first select one exact installable
checkpoint or revision in the requesting thread's worktree:

```bash
mise exec node@24.13.1 -- node "$T3CODE_PROJECT_ROOT/scripts/lastcode-build-local-package.ts" select \
--tag lastcode/revision/v0.0.34-nightly.20260825.1185.3
```

`T3CODE_PROJECT_ROOT` must identify the primary repository checkout; when invoking
this outside a Project Action, substitute that checkout's absolute path if the
variable is unset. Selection records the tag and full commit for this worktree.
Then list Project Actions, launch the eligible **Build Local Package** action
(`lc-build-local-package`) with `run_project_action_and_resume`, and end the turn
immediately. Do not run the helper directly, tail logs, or poll the action.

The action runs the existing local-update build helper, including its build lock,
checkpoint validation, full CI, and packaging. It returns the selected tag and
commit plus the resulting artifact location, or a concrete failure requiring
attention. It does not install or restart the application. It starts its own
build operation and cannot attach to a build previously launched in a terminal.
Each selection permits one attempt. Inspect a failed or interrupted result before
selecting the tag again for a deliberate retry.

The `t3.json` declaration loads from the primary checkout so maintenance threads
with older worktrees can use it. Import the action and grant resume permission in
the owning environment; managed installations use the `lc-build-local-package`
trust entry. If a previous Action is running or awaiting continuation delivery,
preserve the next step and end the turn so its result can arrive. After delivery,
list Actions again. For other disabled reasons, report the specific blocker;
do not substitute a synchronous build or a polling loop.

### Direct builder

An Apple Silicon DMG builder builds the selected checkpoint or revision:

```bash
Expand Down
268 changes: 268 additions & 0 deletions scripts/lastcode-build-local-package.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
// @effect-diagnostics nodeBuiltinImport:off -- Host-side disposable fixtures.
import { describe, expect, it, onTestFinished } from "vite-plus/test";
import * as NodeChildProcess from "node:child_process";
import * as NodeFS from "node:fs";
import * as NodeOS from "node:os";
import * as NodePath from "node:path";
import * as NodeNet from "node:net";
import {
parseBuildResult,
consumeSelection,
executeBuild,
requestPath,
runLocalBuild,
selectLocalBuild,
verifyLocalBuild,
writeSelection,
type LocalBuildRequest,
type LocalBuildResult,
type LocalBuildDeps,
} from "./lastcode-build-local-package.ts";

const tag = "lastcode/checkpoint/v1.2.3-nightly.20260904.7";
const commit = "a".repeat(40);
const request = (overrides: Partial<LocalBuildRequest> = {}): LocalBuildRequest => ({
schemaVersion: 1,
tag,
commit,
requestToken: "local-12345678-1234-1234-1234-123456789abc",
...overrides,
});
const buildPayload = (overrides: Partial<LocalBuildResult> = {}): LocalBuildResult => ({
schemaVersion: 1,
status: "built",
checkpointTag: tag,
outputDir: "/builds/x",
manifestPath: "/builds/x/build-manifest.json",
dmgPath: "/builds/x/app.dmg",
dmgSha256: "b".repeat(64),
...overrides,
});

describe("local build package action", () => {
it("selects an exact tag and records its full commit", () => {
let saved: LocalBuildRequest | undefined;
expect(
selectLocalBuild(
"/repo",
tag,
{
git: () => commit,
writeRequest: (value) => {
saved = value;
},
},
request().requestToken,
),
).toEqual(request());
expect(saved).toEqual(request());
});
it("rejects moving or invalid selectors and parses the helper result", () => {
expect(() =>
selectLocalBuild("/repo", "latest", { git: () => commit, writeRequest: () => {} }),
).toThrow("exact");
expect(() => parseBuildResult("noise\n")).toThrow("did not return");
expect(() =>
parseBuildResult(
`LASTCODE_LOCAL_UPDATE_RESULT=${JSON.stringify(buildPayload())}\nLASTCODE_LOCAL_UPDATE_RESULT=${JSON.stringify(buildPayload())}`,
),
).toThrow("exactly one");
expect(
parseBuildResult(`LASTCODE_LOCAL_UPDATE_RESULT=${JSON.stringify(buildPayload())}`),
).toMatchObject({ checkpointTag: tag });
});
it("writes and consumes one selection, requiring reselection", () => {
const root = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "local-build-selection-"));
onTestFinished(() => NodeFS.rmSync(root, { recursive: true, force: true }));
const path = NodePath.join(root, "git", "lastcode-actions", "build.json");
writeSelection(path, request());
expect(() =>
writeSelection(path, request({ tag: "lastcode/revision/v1.2.3-nightly.20260904.7.2" })),
).toThrow("already pending");
expect(consumeSelection(path)).toEqual(request());
expect(() => consumeSelection(path)).toThrow("each selection permits one attempt");
writeSelection(path, request({ requestToken: "local-aaaaaaaa-1234-1234-1234-123456789abc" }));
expect(consumeSelection(path).requestToken).toContain("local-aaaaaaaa");
NodeFS.writeFileSync(path, JSON.stringify({ ...request(), commit: "not-a-commit" }));
expect(() => consumeSelection(path)).toThrow("invalid");
expect(requestPath(root, () => "git/lastcode-actions/build.json")).toBe(
NodePath.join(root, "git/lastcode-actions/build.json"),
);
});
it("verifies a complete helper artifact and rejects missing DMGs or wrong commits", () => {
const root = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "local-build-artifact-"));
onTestFinished(() => NodeFS.rmSync(root, { recursive: true, force: true }));
const git = (args: string[]) =>
NodeChildProcess.execFileSync(
"git",
[
"-c",
"user.name=Test",
"-c",
"user.email=test@example.invalid",
"-c",
"commit.gpgsign=false",
"-c",
"tag.gpgsign=false",
...args,
],
{ cwd: root, encoding: "utf8" },
).trim();
git(["init", "-q"]);
NodeFS.writeFileSync(NodePath.join(root, "source"), "source\n");
git(["add", "."]);
git(["commit", "-qm", "source"]);
const commit = git(["rev-parse", "HEAD"]);
const tagName = "lastcode/build/v1.2.3-nightly.20260904.7.1";
git(["tag", "-a", tagName, "-m", "build"]);
const output = NodePath.join(
root,
".lastcode",
"local-updates",
"artifacts",
"v1.2.3-nightly.20260904.7",
commit.slice(0, 10),
);
NodeFS.mkdirSync(output, { recursive: true });
NodeFS.writeFileSync(NodePath.join(output, "nightly-mac.yml"), "fixture\n");
NodeFS.writeFileSync(NodePath.join(output, "SHA256SUMS"), "fixture\n");
NodeFS.writeFileSync(NodePath.join(output, "LastCode.dmg"), "dmg\n");
NodeFS.writeFileSync(NodePath.join(output, "LastCode.zip"), "zip\n");
const sha = "b".repeat(64);
NodeFS.writeFileSync(
NodePath.join(output, "build-manifest.json"),
JSON.stringify({
schemaVersion: 1,
checkpointTag: tag,
lastCodeCommit: commit,
buildTag: tagName,
artifacts: [{ path: "LastCode.dmg", sha256: sha }],
}),
);
const req = request({ tag, commit });
const result = buildPayload({
checkpointTag: tag,
outputDir: output,
manifestPath: NodePath.join(output, "build-manifest.json"),
dmgPath: NodePath.join(output, "LastCode.dmg"),
dmgSha256: sha,
});
expect(() => verifyLocalBuild(result, req, root, root)).not.toThrow();
expect(() => verifyLocalBuild(result, request({ commit: "c".repeat(40) }), root, root)).toThrow(
"complete verified",
);
NodeFS.rmSync(NodePath.join(output, "LastCode.dmg"));
expect(() => verifyLocalBuild(result, req, root, root)).toThrow("complete");
});
it("runs the canonical helper and verifies the selected target before and after", async () => {
const calls: string[][] = [];
const build = await runLocalBuild("/repo", {
git: () => commit,
readRequest: () => request(),
writeRequest: () => {},
execute: (_command, args) => {
calls.push([...args]);
return {
code: 0,
stdout: `LASTCODE_LOCAL_UPDATE_RESULT=${JSON.stringify(buildPayload())}`,
stderr: "",
};
},
verifyResult: () => {},
});
expect(build.outputDir).toBe("/builds/x");
expect(calls[0]?.some((value) => value.endsWith("/scripts/lastcode-local-update.mjs"))).toBe(
true,
);
expect(calls[0]).toContain("--checkpoint");
});
it("turns child failures and mismatched results into actionable errors", async () => {
await expect(
runLocalBuild("/repo", {
git: () => commit,
readRequest: () => request(),
writeRequest: () => {},
execute: () => ({ code: 1, stdout: "", stderr: "compiler failed" }),
verifyResult: () => {},
}),
).rejects.toThrow("compiler failed");
await expect(
runLocalBuild("/repo", {
git: () => commit,
readRequest: () => request(),
writeRequest: () => {},
execute: () => ({
code: 0,
stdout: `LASTCODE_LOCAL_UPDATE_RESULT=${JSON.stringify(buildPayload({ checkpointTag: "other" }))}`,
stderr: "",
}),
verifyResult: () => {},
}),
).rejects.toThrow("expected");
});
it("bounds captured stdout and stderr from the owned child", async () => {
const result = await executeBuild(
process.execPath,
["-e", "process.stdout.write('x'.repeat(100000)); process.stderr.write('y'.repeat(100000));"],
process.cwd(),
);
expect(result.code).toBe(0);
expect(result.stdout.length).toBeLessThanOrEqual(64 * 1024);
expect(result.stderr.length).toBeLessThanOrEqual(64 * 1024);
});
it("rejects a moved target before execution and after successful helper completion", async () => {
let executions = 0;
const deps: LocalBuildDeps = {
git: () => "b".repeat(40),
readRequest: () => request(),
writeRequest: () => {},
execute: () => {
executions++;
return {
code: 0,
stdout: `LASTCODE_LOCAL_UPDATE_RESULT=${JSON.stringify(buildPayload())}`,
stderr: "",
};
},
verifyResult: () => {},
};
await expect(runLocalBuild("/repo", deps)).rejects.toThrow("moved");
expect(executions).toBe(0);
let resolutions = 0;
await expect(
runLocalBuild("/repo", {
...deps,
git: () => (resolutions++ === 0 ? commit : "b".repeat(40)),
}),
).rejects.toThrow("changed during");
expect(executions).toBe(1);
});

it("cancels its owned child when aborted after a readiness receipt", async () => {
const server = NodeNet.createServer();
server.listen(0, "127.0.0.1");
await new Promise<void>((resolve) => server.once("listening", resolve));
const address = server.address();
if (!address || typeof address === "string") throw new Error("Missing fixture port");
const controller = new AbortController();
const connected = new Promise<NodeNet.Socket>((resolve) => server.once("connection", resolve));
const running = executeBuild(
process.execPath,
["-e", `require('node:net').connect(${address.port}, '127.0.0.1');`],
process.cwd(),
controller.signal,
);
const rejection = expect(running).rejects.toThrow("fixture cancelled");
try {
const socket = await connected;
const closed = new Promise<void>((resolve) => socket.once("close", () => resolve()));
controller.abort(new Error("fixture cancelled"));
await rejection;
await closed;
} finally {
controller.abort(new Error("fixture cancelled"));
server.close();
await new Promise<void>((resolve) => server.once("close", resolve));
}
});
});
Loading