Reshape harness flows like fork maintenance
Some checks failed
check / check (push) Failing after 26s
Some checks failed
check / check (push) Failing after 26s
This commit is contained in:
parent
08404f1f0b
commit
8b1e31df1a
26 changed files with 1089 additions and 469 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -6,6 +6,7 @@ coverage
|
|||
/.codex/workspace/local/
|
||||
/.codex/workspace/actions/
|
||||
/.codex/pack-backups/
|
||||
/.codex/flow-artifacts/
|
||||
data/
|
||||
apps/*/data/
|
||||
docs/out/
|
||||
|
|
|
|||
|
|
@ -240,7 +240,7 @@ async function handleRun(positionals: string[], context: CliContext): Promise<nu
|
|||
}
|
||||
if (target === "harness") {
|
||||
const eventFile = flagValue(context.parsed, "event") ??
|
||||
path.join(context.workspaceRoot, "flows/patch-moi-harness/fixtures/upstream-release-v0.1.3.json");
|
||||
path.join(context.workspaceRoot, "flows/patch-moi-harness-fork/fixtures/upstream-release-v0.1.3.json");
|
||||
const event = await readFlowEvent(eventFile, context.workspaceRoot);
|
||||
return await runEvent(event, context);
|
||||
}
|
||||
|
|
@ -732,7 +732,7 @@ function findWorkspaceRoot(cwd: string): string {
|
|||
while (true) {
|
||||
if (
|
||||
existsSync(path.join(current, ".codex/workspace.toml")) ||
|
||||
existsSync(path.join(current, "flows/patch-moi-harness/flow.toml"))
|
||||
existsSync(path.join(current, "flows/patch-moi-harness-fork/flow.toml"))
|
||||
) {
|
||||
return current;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,25 +10,43 @@ import {
|
|||
const workspaceRoot = path.resolve(import.meta.dir, "../../..");
|
||||
const fixturePath = path.resolve(
|
||||
workspaceRoot,
|
||||
process.argv[2] ?? "flows/patch-moi-harness/fixtures/upstream-release-v0.1.3.json",
|
||||
process.argv[2] ?? "flows/patch-moi-harness-fork/fixtures/upstream-release-v0.1.3.json",
|
||||
);
|
||||
const event = JSON.parse(await readFile(fixturePath, "utf8")) as FlowEvent<Record<string, unknown>>;
|
||||
const flows = await discoverFlows({ cwd: workspaceRoot });
|
||||
const matches = await matchingSteps(flows, event);
|
||||
const match = matches.find((entry) => entry.flow.manifest.name === "patch-moi-harness");
|
||||
const harnessMatches = matches.filter((entry) => entry.flow.manifest.name.startsWith("patch-moi-harness-"));
|
||||
|
||||
if (!match) {
|
||||
throw new Error(`No patch-moi-harness flow step matched ${event.type} from ${fixturePath}`);
|
||||
if (harnessMatches.length === 0) {
|
||||
throw new Error(`No patch-moi-harness-* flow steps matched ${event.type} from ${fixturePath}`);
|
||||
}
|
||||
|
||||
const result = await runFlowStep({
|
||||
flow: match.flow,
|
||||
step: match.step,
|
||||
const results = [];
|
||||
for (const match of harnessMatches) {
|
||||
results.push({
|
||||
flow: match.flow.manifest.name,
|
||||
step: match.step.name,
|
||||
result: await runFlowStep({
|
||||
flow: match.flow,
|
||||
step: match.step,
|
||||
event,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({
|
||||
status: aggregateStatus(results.map((entry) => entry.result.status)),
|
||||
event,
|
||||
});
|
||||
results,
|
||||
}, null, 2));
|
||||
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
|
||||
if (["blocked", "failed", "needs_intervention"].includes(result.status)) {
|
||||
if (results.some((entry) => ["blocked", "failed", "needs_intervention"].includes(entry.result.status))) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function aggregateStatus(statuses: string[]): string {
|
||||
for (const status of ["failed", "blocked", "needs_intervention", "changed"]) {
|
||||
if (statuses.includes(status)) return status;
|
||||
}
|
||||
return statuses.includes("completed") ? "completed" : "skipped";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { readFile } from "node:fs/promises";
|
||||
import { access, readdir, readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
|
|
@ -12,24 +12,50 @@ const workspaceRoot = path.resolve(import.meta.dir, "../../..");
|
|||
const harnessFork = path.join(workspaceRoot, "harness/fork");
|
||||
|
||||
describe("patch.moi harness flow", () => {
|
||||
test("matches the release fixture and verifies the current fork", async () => {
|
||||
test("matches the release fixture like the Codex fork release fanout", async () => {
|
||||
const event = JSON.parse(await readFile(
|
||||
path.join(workspaceRoot, "flows/patch-moi-harness/fixtures/upstream-release-v0.1.3.json"),
|
||||
path.join(workspaceRoot, "flows/patch-moi-harness-fork/fixtures/upstream-release-v0.1.3.json"),
|
||||
"utf8",
|
||||
)) as FlowEvent<Record<string, unknown>>;
|
||||
const flows = await discoverFlows({ cwd: workspaceRoot });
|
||||
const matches = await matchingSteps(flows, event);
|
||||
const match = matches.find((entry) => entry.flow.manifest.name === "patch-moi-harness");
|
||||
const harnessMatches = matches
|
||||
.filter((entry) => entry.flow.manifest.name.startsWith("patch-moi-harness-"))
|
||||
.map(({ flow, step }) => `${flow.manifest.name}/${step.name}`);
|
||||
|
||||
expect(match).toBeDefined();
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
expect(harnessMatches).toEqual([
|
||||
"patch-moi-harness-bindings/generate-bindings",
|
||||
"patch-moi-harness-fork/release-cycle",
|
||||
]);
|
||||
});
|
||||
|
||||
test("release fixture regenerates bindings and rebuilds the patch-branch fork", async () => {
|
||||
const event = JSON.parse(await readFile(
|
||||
path.join(workspaceRoot, "flows/patch-moi-harness-fork/fixtures/upstream-release-v0.1.3.json"),
|
||||
"utf8",
|
||||
)) as FlowEvent<Record<string, unknown>>;
|
||||
const flows = await discoverFlows({ cwd: workspaceRoot });
|
||||
const matches = await matchingSteps(flows, event);
|
||||
const bindingMatch = matches.find((entry) => entry.flow.manifest.name === "patch-moi-harness-bindings");
|
||||
const forkMatch = matches.find((entry) => entry.flow.manifest.name === "patch-moi-harness-fork");
|
||||
|
||||
expect(bindingMatch).toBeDefined();
|
||||
expect(forkMatch).toBeDefined();
|
||||
if (!bindingMatch || !forkMatch) return;
|
||||
|
||||
const beforeHead = await git(["rev-parse", "HEAD"]);
|
||||
const result = await runFlowStep({
|
||||
flow: match.flow,
|
||||
step: match.step,
|
||||
const bindings = await runFlowStep({
|
||||
flow: bindingMatch.flow,
|
||||
step: bindingMatch.step,
|
||||
event,
|
||||
env: {
|
||||
CODEX_FLOW_FETCH: "0",
|
||||
CODEX_FLOW_PUSH: "0",
|
||||
},
|
||||
});
|
||||
const fork = await runFlowStep({
|
||||
flow: forkMatch.flow,
|
||||
step: forkMatch.step,
|
||||
event,
|
||||
env: {
|
||||
CODEX_FLOW_FETCH: "0",
|
||||
|
|
@ -38,9 +64,18 @@ describe("patch.moi harness flow", () => {
|
|||
});
|
||||
const afterHead = await git(["rev-parse", "HEAD"]);
|
||||
|
||||
expect(result.status).toBe("completed");
|
||||
expect(result.message).toContain("package checks passed");
|
||||
expect(result.artifacts?.candidateRefs).toMatchObject([
|
||||
expect(["changed", "completed"]).toContain(bindings.status);
|
||||
expect(typeof bindings.artifacts?.artifactPath).toBe("string");
|
||||
await access(String(bindings.artifacts?.artifactPath));
|
||||
|
||||
expect(fork.status).toBe("completed");
|
||||
expect(fork.message).toContain("Harness fork already matches");
|
||||
expect(fork.artifacts?.applied).toMatchObject([
|
||||
{ name: "patch/010-maintained-greeting" },
|
||||
{ name: "patch/020-shout-mode" },
|
||||
{ name: "patch/030-package-identity" },
|
||||
]);
|
||||
expect(fork.artifacts?.candidateRefs).toMatchObject([
|
||||
{
|
||||
kind: "branch",
|
||||
repo: "matamune-peezy/patch-moi-harness",
|
||||
|
|
@ -54,6 +89,72 @@ describe("patch.moi harness flow", () => {
|
|||
expect(await git(["status", "--porcelain=v1"])).toBe("");
|
||||
});
|
||||
|
||||
test("matches and runs the harness main branch update path", async () => {
|
||||
const event = JSON.parse(await readFile(
|
||||
path.join(workspaceRoot, "flows/patch-moi-harness-fork/fixtures/upstream-main-v0.1.3.json"),
|
||||
"utf8",
|
||||
)) as FlowEvent<Record<string, unknown>>;
|
||||
const flows = await discoverFlows({ cwd: workspaceRoot });
|
||||
const matches = await matchingSteps(flows, event);
|
||||
expect(matches.map(({ flow, step }) => `${flow.manifest.name}/${step.name}`)).toContain(
|
||||
"patch-moi-harness-fork/main-branch-update",
|
||||
);
|
||||
const match = matches.find((entry) => entry.flow.manifest.name === "patch-moi-harness-fork");
|
||||
if (!match) return;
|
||||
|
||||
const beforeHead = await git(["rev-parse", "HEAD"]);
|
||||
const result = await runFlowStep({
|
||||
flow: match.flow,
|
||||
step: match.step,
|
||||
event,
|
||||
env: {
|
||||
CODEX_FLOW_FETCH: "0",
|
||||
CODEX_FLOW_PUSH: "0",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.status).toBe("completed");
|
||||
expect(result.artifacts?.upstreamBranch).toBe("upstream");
|
||||
expect(await git(["rev-parse", "HEAD"])).toBe(beforeHead);
|
||||
expect(await git(["status", "--porcelain=v1"])).toBe("");
|
||||
});
|
||||
|
||||
test("matches and runs the downstream harness fork package artifact path", async () => {
|
||||
const event = JSON.parse(await readFile(
|
||||
path.join(workspaceRoot, "flows/patch-moi-harness-flows-fork/fixtures/downstream-fork-release-v0.1.3-fork.0.json"),
|
||||
"utf8",
|
||||
)) as FlowEvent<Record<string, unknown>>;
|
||||
const flows = await discoverFlows({ cwd: workspaceRoot });
|
||||
const matches = await matchingSteps(flows, event);
|
||||
expect(matches.map(({ flow, step }) => `${flow.manifest.name}/${step.name}`)).toContain(
|
||||
"patch-moi-harness-flows-fork/release-fork",
|
||||
);
|
||||
const match = matches.find((entry) => entry.flow.manifest.name === "patch-moi-harness-flows-fork");
|
||||
if (!match) return;
|
||||
|
||||
const result = await runFlowStep({
|
||||
flow: match.flow,
|
||||
step: match.step,
|
||||
event,
|
||||
env: {
|
||||
CODEX_FLOW_FETCH: "0",
|
||||
CODEX_FLOW_PUSH: "0",
|
||||
CODEX_FLOW_PUBLISH: "0",
|
||||
},
|
||||
});
|
||||
const tarballs = await readdir(path.join(workspaceRoot, ".codex/flow-artifacts/patch-moi-harness-flows-fork-release"));
|
||||
|
||||
expect(result.status).toBe("changed");
|
||||
expect(result.artifacts?.candidateRefs).toMatchObject([
|
||||
{
|
||||
kind: "artifact",
|
||||
repo: "matamune-peezy/patch-moi-harness",
|
||||
pushed: false,
|
||||
},
|
||||
]);
|
||||
expect(tarballs.some((entry) => entry.endsWith(".tgz"))).toBe(true);
|
||||
});
|
||||
|
||||
test("matches installed Codex release flows without executing release work", async () => {
|
||||
const flows = await discoverFlows({ cwd: workspaceRoot });
|
||||
const matches = await matchingSteps(flows, {
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ The current service has these pieces:
|
|||
| feed poller | reads configured upstream feeds and emits normalized signals |
|
||||
| JSONL store | writes feed events, flow events, workspace dispatches, and maintenance attempts under `DATA_DIR` |
|
||||
| workspace backend adapter | dispatches locally when no backend URL is set, or calls a configured Codex workspace backend |
|
||||
| harness flow | exercises real fork maintenance through `flows/patch-moi-harness` |
|
||||
| harness flows | exercise Codex-shaped fork maintenance through `flows/patch-moi-harness-*` |
|
||||
| repo workspace config | exposes manual operator tasks through `codex-flows workspace doctor|tick|run` |
|
||||
|
||||
Those pieces are intentionally narrow. The service coordinates and records; the
|
||||
|
|
|
|||
|
|
@ -112,7 +112,8 @@ DATA_DIR=./data FEED_SOURCES_PATH=./feed-sources.json bun run --filter @peezy.te
|
|||
|
||||
- `apps/patch`: Patch service, feed poller, JSONL store, admin API, Discord
|
||||
output, and workspace backend adapter.
|
||||
- `flows/patch-moi-harness`: executable maintenance flow for the harness repos.
|
||||
- `flows/patch-moi-harness-*`: source harness flows that mirror the Codex
|
||||
fork release, main-update, and downstream-release surfaces.
|
||||
- `.codex/flows`: installed external flow capabilities, currently the Codex
|
||||
release maintenance flows from the neighboring `../codex-flows` pack.
|
||||
- `harness`: upstream and maintained fork repositories used for rehearsal.
|
||||
|
|
|
|||
|
|
@ -6,8 +6,14 @@ description: Use the patch.moi harness repos to rehearse an upstream release and
|
|||
# Run the harness maintenance flow
|
||||
|
||||
This tutorial runs the smallest real patch.moi maintenance loop. The upstream
|
||||
repo is `harness/upstream`. The maintained fork is `harness/fork`. The flow
|
||||
package is `flows/patch-moi-harness`.
|
||||
repo is `harness/upstream`. The maintained fork is `harness/fork`. The source
|
||||
flow packages mirror the Codex fork structure:
|
||||
|
||||
- `flows/patch-moi-harness-bindings` handles upstream release metadata.
|
||||
- `flows/patch-moi-harness-fork` rebuilds the maintained fork from
|
||||
`upstream` plus ordered `patch/*` branches.
|
||||
- `flows/patch-moi-harness-flows-fork` prepares a downstream fork package
|
||||
artifact.
|
||||
|
||||
There are two local operator paths:
|
||||
|
||||
|
|
@ -39,9 +45,11 @@ bun run harness:flow
|
|||
```
|
||||
|
||||
The fixture event is `v0.1.3`, which the current fork already contains. The
|
||||
flow should skip rebase work, run `npm test` and `npm run pack:dry-run` in the
|
||||
fork, report `candidateRefs` for the maintained fork branch, and leave the fork
|
||||
checkout unchanged.
|
||||
release event fans out to the bindings flow and the fork flow. The fork flow
|
||||
seeds the local `upstream` and `patch/*` branches when needed, verifies that
|
||||
`main` already equals `upstream + patches`, runs `npm test` and
|
||||
`npm run pack:dry-run`, reports `candidateRefs` for the maintained fork branch,
|
||||
and leaves the fork checkout unchanged.
|
||||
|
||||
## 3. Run the fixture through workspace autonomy
|
||||
|
||||
|
|
@ -107,9 +115,10 @@ bun run harness:flow <event-file>
|
|||
```
|
||||
|
||||
Use an event file whose `id`, `occurredAt`, `receivedAt`, and `payload.tag`
|
||||
identify the new upstream tag. The flow rebases `harness/fork` onto that tag,
|
||||
verifies the fork package, reports the local candidate branch, and keeps pushes
|
||||
off.
|
||||
identify the new upstream tag. The fork flow updates the local `upstream`
|
||||
branch to that tag, rebuilds `main` by cherry-picking ordered `patch/*` branch
|
||||
tips, verifies the fork package, reports the local candidate branch, and keeps
|
||||
pushes off.
|
||||
|
||||
## 6. Push only after review
|
||||
|
||||
|
|
@ -119,7 +128,7 @@ When the local result is the maintained fork state you want:
|
|||
CODEX_FLOW_PUSH=1 bun run harness:flow <event-file>
|
||||
```
|
||||
|
||||
That pushes the rebased fork branch to the configured `origin` and `jojo`
|
||||
That pushes the rebuilt fork branch to the configured `origin` and `jojo`
|
||||
remotes with `--force-with-lease` and reports those pushed branch refs as
|
||||
candidate refs. Public npm publishing remains a separate trusted-publishing
|
||||
release path.
|
||||
|
|
|
|||
124
flows/patch-moi-harness-bindings/exec/generate-bindings.ts
Normal file
124
flows/patch-moi-harness-bindings/exec/generate-bindings.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
type FlowContext = {
|
||||
flow: {
|
||||
config?: Record<string, unknown>;
|
||||
event: {
|
||||
id: string;
|
||||
type: string;
|
||||
payload?: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type CommandResult = {
|
||||
code: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
};
|
||||
|
||||
const context = JSON.parse(await Bun.stdin.text()) as FlowContext;
|
||||
const config = context.flow.config ?? {};
|
||||
const payload = context.flow.event.payload ?? {};
|
||||
|
||||
function finish(value: Record<string, unknown>): never {
|
||||
process.stdout.write(`FLOW_RESULT ${JSON.stringify(value)}\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
const expectedRepo = stringConfig("expected_repo", "peezy-tech/patch-moi-harness");
|
||||
const repo = stringValue(payload.repo);
|
||||
const tag = stringValue(payload.tag) || shortTag(stringValue(payload.ref) ?? "");
|
||||
if (repo !== expectedRepo) {
|
||||
finish({ status: "skipped", message: `Harness bindings ignore ${repo}.` });
|
||||
}
|
||||
if (!tag) {
|
||||
finish({ status: "failed", message: "upstream.release requires payload.tag." });
|
||||
}
|
||||
|
||||
const workspaceRoot = process.cwd();
|
||||
const upstreamRepo = path.resolve(workspaceRoot, stringConfig("upstream_repo", "harness/upstream"));
|
||||
const artifactDir = path.resolve(workspaceRoot, stringConfig("artifact_dir", ".codex/flow-artifacts/patch-moi-harness-bindings"));
|
||||
const packageJsonPath = path.join(upstreamRepo, "package.json");
|
||||
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8")) as {
|
||||
name?: string;
|
||||
version?: string;
|
||||
exports?: unknown;
|
||||
bin?: unknown;
|
||||
};
|
||||
const releaseSha = (await runChecked(["git", "rev-parse", "--verify", `${tag}^{commit}`], upstreamRepo)).stdout.trim();
|
||||
const bindings = {
|
||||
generatedBy: "patch-moi-harness-bindings",
|
||||
eventId: context.flow.event.id,
|
||||
repo,
|
||||
tag,
|
||||
releaseSha,
|
||||
packageName: packageJson.name,
|
||||
version: packageJson.version,
|
||||
exports: packageJson.exports ?? null,
|
||||
bin: packageJson.bin ?? null,
|
||||
};
|
||||
const artifactJson = `${JSON.stringify(bindings, null, 2)}\n`;
|
||||
const artifactPath = path.join(artifactDir, "bindings.json");
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
const previous = await readFile(artifactPath, "utf8").catch(() => "");
|
||||
await writeFile(artifactPath, artifactJson, "utf8");
|
||||
const changed = previous !== artifactJson;
|
||||
|
||||
finish({
|
||||
status: changed ? "changed" : "completed",
|
||||
message: changed
|
||||
? `Regenerated harness bindings for ${repo}@${tag}.`
|
||||
: `Harness bindings already current for ${repo}@${tag}.`,
|
||||
artifacts: {
|
||||
repo,
|
||||
tag,
|
||||
releaseSha,
|
||||
artifactPath,
|
||||
candidateRefs: [{
|
||||
kind: "artifact",
|
||||
repo: expectedRepo,
|
||||
ref: artifactPath,
|
||||
sha: releaseSha,
|
||||
pushed: false,
|
||||
}],
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
finish({
|
||||
status: "failed",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
|
||||
async function runChecked(command: string[], cwd: string): Promise<CommandResult> {
|
||||
const proc = Bun.spawn(command, {
|
||||
cwd,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout, stderr, code] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
]);
|
||||
if (code !== 0) {
|
||||
throw new Error(`${command.join(" ")} failed with exit ${code}:\n${stderr || stdout}`);
|
||||
}
|
||||
return { code, stdout, stderr };
|
||||
}
|
||||
|
||||
function stringConfig(name: string, fallback: string): string {
|
||||
const value = config[name];
|
||||
return typeof value === "string" && value.trim() ? value : fallback;
|
||||
}
|
||||
|
||||
function stringValue(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function shortTag(value: string): string {
|
||||
return value.replace(/^refs\/tags\//, "");
|
||||
}
|
||||
|
|
@ -4,10 +4,8 @@
|
|||
"source": "patch",
|
||||
"receivedAt": "2026-05-16T00:00:00.000Z",
|
||||
"payload": {
|
||||
"provider": "github",
|
||||
"repo": "peezy-tech/patch-moi-harness",
|
||||
"tag": "v0.1.3",
|
||||
"url": "https://github.com/peezy-tech/patch-moi-harness/releases/tag/v0.1.3",
|
||||
"publishedAt": "2026-05-16T00:00:00.000Z"
|
||||
"url": "https://github.com/peezy-tech/patch-moi-harness/releases/tag/v0.1.3"
|
||||
}
|
||||
}
|
||||
19
flows/patch-moi-harness-bindings/flow.toml
Normal file
19
flows/patch-moi-harness-bindings/flow.toml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
name = "patch-moi-harness-bindings"
|
||||
version = 1
|
||||
description = "Regenerate a local harness bindings artifact from upstream harness releases."
|
||||
|
||||
[config]
|
||||
expected_repo = "peezy-tech/patch-moi-harness"
|
||||
upstream_repo = "harness/upstream"
|
||||
artifact_dir = ".codex/flow-artifacts/patch-moi-harness-bindings"
|
||||
|
||||
[[steps]]
|
||||
name = "generate-bindings"
|
||||
runner = "bun"
|
||||
script = "exec/generate-bindings.ts"
|
||||
cwd = "../.."
|
||||
timeout_ms = 300000
|
||||
|
||||
[steps.trigger]
|
||||
type = "upstream.release"
|
||||
schema = "schemas/upstream-release.schema.json"
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"required": ["repo", "tag"],
|
||||
"properties": {
|
||||
"repo": {
|
||||
"type": "string",
|
||||
"enum": ["peezy-tech/patch-moi-harness"]
|
||||
},
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
178
flows/patch-moi-harness-flows-fork/exec/release-fork.ts
Normal file
178
flows/patch-moi-harness-flows-fork/exec/release-fork.ts
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
import { existsSync } from "node:fs";
|
||||
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
type FlowContext = {
|
||||
flow: {
|
||||
config?: Record<string, unknown>;
|
||||
event: {
|
||||
id: string;
|
||||
payload?: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type CommandResult = {
|
||||
code: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
};
|
||||
|
||||
const context = JSON.parse(await Bun.stdin.text()) as FlowContext;
|
||||
const config = context.flow.config ?? {};
|
||||
const payload = context.flow.event.payload ?? {};
|
||||
|
||||
function finish(value: Record<string, unknown>): never {
|
||||
process.stdout.write(`FLOW_RESULT ${JSON.stringify(value)}\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
const packageName = stringValue(payload.packageName);
|
||||
const version = stringValue(payload.version);
|
||||
if (!packageName || !version) {
|
||||
finish({ status: "failed", message: "downstream.release requires packageName and version." });
|
||||
}
|
||||
const acceptedPackages = stringArrayConfig("accepted_packages", [
|
||||
"@peezy.tech/patch-moi-harness",
|
||||
"@peezy.tech/patch-moi-harness-fork",
|
||||
]);
|
||||
if (!acceptedPackages.includes(packageName)) {
|
||||
finish({ status: "skipped", message: `Harness fork release ignores ${packageName}.` });
|
||||
}
|
||||
|
||||
const workspaceRoot = process.cwd();
|
||||
const forkRepo = path.resolve(workspaceRoot, stringConfig("fork_repo", "harness/fork"));
|
||||
const forkRepoFullName = stringConfig("fork_repo_full_name", "matamune-peezy/patch-moi-harness");
|
||||
const sourceBranch = stringConfig("source_branch", "main");
|
||||
const worktreeDir = path.resolve(workspaceRoot, stringConfig("worktree_dir", ".codex/flow-artifacts/patch-moi-harness-flows-fork-worktree"));
|
||||
const artifactDir = path.resolve(workspaceRoot, stringConfig("artifact_dir", ".codex/flow-artifacts/patch-moi-harness-flows-fork-release"));
|
||||
|
||||
await requireCleanRepo(forkRepo);
|
||||
const sourceSha = (await runChecked("resolve harness fork branch", ["git", "rev-parse", "--verify", `${sourceBranch}^{commit}`], forkRepo)).stdout.trim();
|
||||
await prepareWorktree(forkRepo, worktreeDir, sourceBranch);
|
||||
|
||||
const packageJsonPath = path.join(worktreeDir, "package.json");
|
||||
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8")) as Record<string, unknown>;
|
||||
const baseVersion = typeof packageJson.version === "string" ? packageJson.version : "0.0.0";
|
||||
const forkVersion = forkPackageVersion(baseVersion, version);
|
||||
packageJson.version = forkVersion;
|
||||
packageJson.description = `Fork release artifact prepared from ${packageName}@${version}.`;
|
||||
await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf8");
|
||||
|
||||
await runChecked("install harness fork package", ["npm", "install", "--package-lock-only"], worktreeDir);
|
||||
await runChecked("test harness fork package", ["npm", "test"], worktreeDir);
|
||||
|
||||
await rm(artifactDir, { recursive: true, force: true });
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
const pack = await runChecked("pack harness fork package", ["npm", "pack", "--pack-destination", artifactDir], worktreeDir);
|
||||
const tarball = pack.stdout.trim().split(/\r?\n/).filter(Boolean).at(-1);
|
||||
const tarballPath = tarball ? path.join(artifactDir, tarball) : undefined;
|
||||
|
||||
const candidateSha = (await runChecked("read harness fork release candidate", ["git", "rev-parse", "HEAD"], worktreeDir)).stdout.trim();
|
||||
finish({
|
||||
status: "changed",
|
||||
message: `Prepared harness fork package ${forkVersion} from ${packageName}@${version}.`,
|
||||
artifacts: {
|
||||
eventId: context.flow.event.id,
|
||||
sourcePackage: packageName,
|
||||
sourceVersion: version,
|
||||
forkRepo,
|
||||
forkRepoFullName,
|
||||
sourceBranch,
|
||||
sourceSha,
|
||||
baseVersion,
|
||||
forkVersion,
|
||||
worktreeDir,
|
||||
tarballPath,
|
||||
pushed: false,
|
||||
published: false,
|
||||
candidateRefs: [{
|
||||
kind: "artifact",
|
||||
repo: forkRepoFullName,
|
||||
ref: tarballPath ?? artifactDir,
|
||||
sha: candidateSha,
|
||||
pushed: false,
|
||||
}],
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
finish({
|
||||
status: "failed",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
|
||||
async function requireCleanRepo(repoRoot: string): Promise<void> {
|
||||
const status = await runChecked("read harness fork status", ["git", "status", "--porcelain=v1"], repoRoot);
|
||||
if (status.stdout.trim()) {
|
||||
finish({
|
||||
status: "blocked",
|
||||
message: "Harness fork checkout has local changes before release artifact preparation.",
|
||||
artifacts: { status: status.stdout },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareWorktree(repoRoot: string, worktreeDir: string, sourceBranch: string): Promise<void> {
|
||||
if (existsSync(worktreeDir)) {
|
||||
await run("remove old harness fork release worktree", ["git", "worktree", "remove", "--force", worktreeDir], repoRoot);
|
||||
await rm(worktreeDir, { recursive: true, force: true });
|
||||
}
|
||||
await run("prune harness fork worktrees", ["git", "worktree", "prune"], repoRoot);
|
||||
await runChecked("create harness fork release worktree", ["git", "worktree", "add", "--detach", worktreeDir, sourceBranch], repoRoot);
|
||||
}
|
||||
|
||||
async function runChecked(label: string, command: string[], cwd: string): Promise<CommandResult> {
|
||||
const result = await run(label, command, cwd);
|
||||
if (result.code !== 0) {
|
||||
throw new Error(`${label} failed with exit ${result.code}:\n${result.stderr || result.stdout}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function run(label: string, command: string[], cwd: string): Promise<CommandResult> {
|
||||
process.stderr.write(`+ ${label}: ${command.join(" ")}\n`);
|
||||
const proc = Bun.spawn(command, {
|
||||
cwd,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout, stderr, code] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
]);
|
||||
if (stdout) process.stderr.write(stdout);
|
||||
if (stderr) process.stderr.write(stderr);
|
||||
return { code, stdout, stderr };
|
||||
}
|
||||
|
||||
function forkPackageVersion(baseVersion: string, sourceVersion: string): string {
|
||||
return `${baseVersion}-harness.${sanitizePrerelease(sourceVersion)}`;
|
||||
}
|
||||
|
||||
function sanitizePrerelease(value: string): string {
|
||||
return value
|
||||
.replace(/^v/, "")
|
||||
.replace(/[^0-9A-Za-z]+/g, ".")
|
||||
.split(".")
|
||||
.filter(Boolean)
|
||||
.join(".") || "0";
|
||||
}
|
||||
|
||||
function stringConfig(name: string, fallback: string): string {
|
||||
const value = config[name];
|
||||
return typeof value === "string" && value.trim() ? value : fallback;
|
||||
}
|
||||
|
||||
function stringArrayConfig(name: string, fallback: string[]): string[] {
|
||||
const value = config[name];
|
||||
if (!Array.isArray(value)) return fallback;
|
||||
const entries = value.filter((entry): entry is string => typeof entry === "string" && entry.trim());
|
||||
return entries.length > 0 ? entries : fallback;
|
||||
}
|
||||
|
||||
function stringValue(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"id": "patch:downstream.release:@peezy.tech/patch-moi-harness-fork:0.1.3-fork.0",
|
||||
"type": "downstream.release",
|
||||
"source": "patch",
|
||||
"receivedAt": "2026-05-16T00:00:00.000Z",
|
||||
"payload": {
|
||||
"packageName": "@peezy.tech/patch-moi-harness-fork",
|
||||
"version": "0.1.3-fork.0",
|
||||
"repo": "matamune-peezy/patch-moi-harness"
|
||||
}
|
||||
}
|
||||
28
flows/patch-moi-harness-flows-fork/flow.toml
Normal file
28
flows/patch-moi-harness-flows-fork/flow.toml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
name = "patch-moi-harness-flows-fork"
|
||||
version = 1
|
||||
description = "Prepare a local harness fork package artifact from downstream harness package releases."
|
||||
|
||||
[config]
|
||||
fork_repo = "harness/fork"
|
||||
fork_repo_full_name = "matamune-peezy/patch-moi-harness"
|
||||
source_branch = "main"
|
||||
worktree_dir = ".codex/flow-artifacts/patch-moi-harness-flows-fork-worktree"
|
||||
artifact_dir = ".codex/flow-artifacts/patch-moi-harness-flows-fork-release"
|
||||
fetch = false
|
||||
push = false
|
||||
publish = false
|
||||
accepted_packages = [
|
||||
"@peezy.tech/patch-moi-harness",
|
||||
"@peezy.tech/patch-moi-harness-fork",
|
||||
]
|
||||
|
||||
[[steps]]
|
||||
name = "release-fork"
|
||||
runner = "bun"
|
||||
script = "exec/release-fork.ts"
|
||||
cwd = "../.."
|
||||
timeout_ms = 600000
|
||||
|
||||
[steps.trigger]
|
||||
type = "downstream.release"
|
||||
schema = "schemas/downstream-release.schema.json"
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"required": ["packageName", "version"],
|
||||
"properties": {
|
||||
"packageName": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"@peezy.tech/patch-moi-harness",
|
||||
"@peezy.tech/patch-moi-harness-fork"
|
||||
]
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"repo": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
375
flows/patch-moi-harness-fork/exec/update-fork.ts
Normal file
375
flows/patch-moi-harness-fork/exec/update-fork.ts
Normal file
|
|
@ -0,0 +1,375 @@
|
|||
import path from "node:path";
|
||||
|
||||
type FlowContext = {
|
||||
flow: {
|
||||
config?: Record<string, unknown>;
|
||||
event: {
|
||||
id: string;
|
||||
type: string;
|
||||
payload?: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type CommandResult = {
|
||||
label: string;
|
||||
cmd: string[];
|
||||
cwd: string;
|
||||
code: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
};
|
||||
|
||||
type PatchBranch = {
|
||||
name: string;
|
||||
sha: string;
|
||||
subject: string;
|
||||
};
|
||||
|
||||
const context = JSON.parse(await Bun.stdin.text()) as FlowContext;
|
||||
const config = context.flow.config ?? {};
|
||||
const payload = context.flow.event.payload ?? {};
|
||||
const commands: CommandResult[] = [];
|
||||
|
||||
const workspaceRoot = process.cwd();
|
||||
const forkRepo = path.resolve(workspaceRoot, stringConfig("fork_repo", "harness/fork"));
|
||||
const forkRepoFullName = stringConfig("fork_repo_full_name", "matamune-peezy/patch-moi-harness");
|
||||
const targetBranch = stringConfig("target_branch", "main");
|
||||
const upstreamBranch = stringConfig("upstream_branch", "upstream");
|
||||
const patchPrefix = stringConfig("patch_prefix", "patch/");
|
||||
const upstreamRemote = stringConfig("upstream_remote", "upstream");
|
||||
const upstreamRepoUrl = stringConfig("upstream_repo_url", "https://github.com/peezy-tech/patch-moi-harness.git");
|
||||
const verifyCommands = stringArrayConfig("verify_commands", ["npm test", "npm run pack:dry-run"]);
|
||||
const pushRemotes = stringArrayConfig("push_remotes", ["origin", "jojo"]);
|
||||
|
||||
function finish(value: Record<string, unknown>): never {
|
||||
process.stdout.write(`FLOW_RESULT ${JSON.stringify(value)}\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
const expectedRepo = stringConfig("expected_repo", "peezy-tech/patch-moi-harness");
|
||||
const repo = stringValue(payload.repo);
|
||||
if (repo !== expectedRepo) {
|
||||
finish({ status: "skipped", message: `Harness fork flow ignores ${repo}.` });
|
||||
}
|
||||
|
||||
await requireGitRepo();
|
||||
await ensureUpstreamRemote();
|
||||
if (enabled("fetch", true)) {
|
||||
await run("fetch upstream refs", ["git", "fetch", upstreamRemote, "--tags", "--prune"]);
|
||||
}
|
||||
await requireNoRebaseOrCherryPickInProgress();
|
||||
await requireCleanWorktree("before harness patch rebuild");
|
||||
|
||||
const base = await resolveBase();
|
||||
await run("update local upstream branch", ["git", "branch", "-f", upstreamBranch, base.sha]);
|
||||
await ensureSeedPatchBranches();
|
||||
|
||||
const currentSha = (await runChecked("read harness checkout head", ["git", "rev-parse", "HEAD"])).stdout.trim();
|
||||
const targetExists = await branchExists(targetBranch);
|
||||
const beforeSha = targetExists ? await resolveCommit(targetBranch) : currentSha;
|
||||
const beforeTree = beforeSha ? await resolveTree(beforeSha) : "";
|
||||
const patchBranches = await listPatchBranches();
|
||||
if (patchBranches.length === 0) {
|
||||
finish({
|
||||
status: "blocked",
|
||||
message: `Harness fork has no ${patchPrefix} branches.`,
|
||||
artifacts: baseArtifacts(base),
|
||||
});
|
||||
}
|
||||
|
||||
await run("switch to upstream rebuild base", ["git", "switch", "--detach", base.sha]);
|
||||
const applied: PatchBranch[] = [];
|
||||
for (const patchBranch of patchBranches) {
|
||||
const pick = await run(`apply ${patchBranch.name}`, ["git", "cherry-pick", patchBranch.sha], { allowFailure: true });
|
||||
if (pick.code !== 0) {
|
||||
const status = await run("patch rebuild conflict status", ["git", "status", "--short", "--branch"], { allowFailure: true });
|
||||
const unmerged = await run("unmerged files", ["git", "diff", "--name-only", "--diff-filter=U"], { allowFailure: true });
|
||||
finish({
|
||||
status: "needs_intervention",
|
||||
message: `Harness patch rebuild stopped while applying ${patchBranch.name}.`,
|
||||
artifacts: {
|
||||
...baseArtifacts(base),
|
||||
beforeSha,
|
||||
applied,
|
||||
failedPatch: patchBranch,
|
||||
statusOutput: status.stdout,
|
||||
unmergedFiles: lines(unmerged.stdout),
|
||||
commands: commandArtifacts(),
|
||||
},
|
||||
});
|
||||
}
|
||||
applied.push(patchBranch);
|
||||
}
|
||||
|
||||
const candidateSha = (await runChecked("read rebuilt harness head", ["git", "rev-parse", "HEAD"])).stdout.trim();
|
||||
const candidateTree = await resolveTree(candidateSha);
|
||||
const changed = !beforeSha || candidateTree !== beforeTree;
|
||||
if (changed) {
|
||||
await run("update maintained harness branch", ["git", "branch", "-f", targetBranch, candidateSha]);
|
||||
} else if (!targetExists) {
|
||||
await run("seed maintained harness branch", ["git", "branch", "-f", targetBranch, beforeSha]);
|
||||
}
|
||||
await run("switch maintained harness branch", ["git", "switch", targetBranch]);
|
||||
|
||||
for (const command of verifyCommands) {
|
||||
const result = await run(`verify: ${command}`, ["bash", "-lc", command], { allowFailure: true });
|
||||
if (result.code !== 0) {
|
||||
finish({
|
||||
status: "needs_intervention",
|
||||
message: `Harness verification failed: ${command}.`,
|
||||
artifacts: {
|
||||
...baseArtifacts(base),
|
||||
beforeSha,
|
||||
candidateSha,
|
||||
applied,
|
||||
failedCommand: command,
|
||||
commands: commandArtifacts(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
await requireCleanWorktree("after harness verification");
|
||||
|
||||
const afterSha = (await runChecked("read maintained harness head", ["git", "rev-parse", "HEAD"])).stdout.trim();
|
||||
if (enabled("push", false)) {
|
||||
for (const remote of pushRemotes) {
|
||||
await run(`push ${remote}/${targetBranch}`, [
|
||||
"git",
|
||||
"push",
|
||||
"--force-with-lease",
|
||||
remote,
|
||||
`HEAD:${targetBranch}`,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
finish({
|
||||
status: changed ? "changed" : "completed",
|
||||
message: changed
|
||||
? `Harness fork rebuilt ${targetBranch} from ${base.label} plus ${patchBranches.length} patches.`
|
||||
: `Harness fork already matches ${base.label} plus ${patchBranches.length} patches.`,
|
||||
artifacts: {
|
||||
...baseArtifacts(base),
|
||||
eventId: context.flow.event.id,
|
||||
forkRepo,
|
||||
forkRepoFullName,
|
||||
targetBranch,
|
||||
upstreamBranch,
|
||||
patchPrefix,
|
||||
beforeSha,
|
||||
afterSha,
|
||||
applied,
|
||||
checks: verifyCommands.map((name) => ({ name, status: "passed" })),
|
||||
candidateRefs: candidateRefsFor(afterSha),
|
||||
commands: commandArtifacts(),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
finish({
|
||||
status: "failed",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
artifacts: { commands: commandArtifacts() },
|
||||
});
|
||||
}
|
||||
|
||||
async function requireGitRepo(): Promise<void> {
|
||||
await runChecked("verify harness fork checkout", ["git", "rev-parse", "--show-toplevel"]);
|
||||
}
|
||||
|
||||
async function ensureUpstreamRemote(): Promise<void> {
|
||||
const current = await run("read upstream remote", ["git", "remote", "get-url", upstreamRemote], { allowFailure: true });
|
||||
if (current.code === 0) return;
|
||||
if (!upstreamRepoUrl) {
|
||||
finish({ status: "blocked", message: `Missing ${upstreamRemote} remote and no upstream_repo_url is configured.` });
|
||||
}
|
||||
await run("add upstream remote", ["git", "remote", "add", upstreamRemote, upstreamRepoUrl]);
|
||||
}
|
||||
|
||||
async function requireNoRebaseOrCherryPickInProgress(): Promise<void> {
|
||||
const state = await run(
|
||||
"check existing replay state",
|
||||
["bash", "-lc", 'test -d "$(git rev-parse --git-path rebase-merge)" -o -d "$(git rev-parse --git-path rebase-apply)" -o -f "$(git rev-parse --git-path CHERRY_PICK_HEAD)"'],
|
||||
{ allowFailure: true },
|
||||
);
|
||||
if (state.code === 0) {
|
||||
finish({
|
||||
status: "blocked",
|
||||
message: "A rebase or cherry-pick is already in progress in the harness fork checkout.",
|
||||
artifacts: { forkRepo, commands: commandArtifacts() },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function requireCleanWorktree(stage: string): Promise<void> {
|
||||
const status = await run(`dirty worktree check ${stage}`, ["git", "status", "--porcelain=v1"]);
|
||||
if (status.stdout.trim()) {
|
||||
finish({
|
||||
status: "blocked",
|
||||
message: `Harness fork checkout has local changes ${stage}.`,
|
||||
artifacts: {
|
||||
dirtyStatus: status.stdout,
|
||||
forkRepo,
|
||||
commands: commandArtifacts(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveBase(): Promise<{ kind: "release" | "branch"; label: string; sha: string }> {
|
||||
if (context.flow.event.type === "upstream.release") {
|
||||
const tag = stringValue(payload.tag) || shortTag(stringValue(payload.ref) ?? "");
|
||||
if (!tag) {
|
||||
finish({ status: "failed", message: "upstream.release requires payload.tag." });
|
||||
}
|
||||
return { kind: "release", label: tag, sha: await resolveCommit(`refs/tags/${tag}`).catch(() => resolveCommit(tag)) };
|
||||
}
|
||||
if (context.flow.event.type === "upstream.branch_update") {
|
||||
const ref = stringValue(payload.ref) ?? "refs/heads/main";
|
||||
const sha = stringValue(payload.sha);
|
||||
if (!sha) {
|
||||
finish({ status: "failed", message: "upstream.branch_update requires payload.sha." });
|
||||
}
|
||||
return { kind: "branch", label: `${ref}@${sha}`, sha: await resolveCommit(sha) };
|
||||
}
|
||||
finish({ status: "skipped", message: `Unsupported harness fork event ${context.flow.event.type}.` });
|
||||
}
|
||||
|
||||
async function ensureSeedPatchBranches(): Promise<void> {
|
||||
const seeds = stringArrayConfig("seed_patch_refs", []);
|
||||
for (const seed of seeds) {
|
||||
const [name, ref] = seed.split("=");
|
||||
if (!name?.startsWith(patchPrefix) || !ref) {
|
||||
throw new Error(`Invalid seed_patch_refs entry: ${seed}`);
|
||||
}
|
||||
const exists = await branchExists(name);
|
||||
if (exists) continue;
|
||||
await resolveCommit(ref);
|
||||
await run(`seed ${name}`, ["git", "branch", "-f", name, ref]);
|
||||
}
|
||||
}
|
||||
|
||||
async function listPatchBranches(): Promise<PatchBranch[]> {
|
||||
const refsPath = `refs/heads/${patchPrefix.replace(/\/+$/, "")}`;
|
||||
const result = await run("list harness patch branches", [
|
||||
"git",
|
||||
"for-each-ref",
|
||||
"--format=%(refname:short)%09%(objectname)%09%(contents:subject)",
|
||||
refsPath,
|
||||
], { allowFailure: true });
|
||||
if (result.code !== 0 || !result.stdout.trim()) return [];
|
||||
return result.stdout.trim().split(/\r?\n/).map((line) => {
|
||||
const [name = "", sha = "", subject = ""] = line.split("\t");
|
||||
return { name, sha, subject };
|
||||
}).filter((branch) => branch.name.startsWith(patchPrefix)).sort((left, right) => left.name.localeCompare(right.name));
|
||||
}
|
||||
|
||||
async function branchExists(branch: string): Promise<boolean> {
|
||||
return (await run(`check branch ${branch}`, ["git", "show-ref", "--verify", "--quiet", `refs/heads/${branch}`], { allowFailure: true })).code === 0;
|
||||
}
|
||||
|
||||
async function resolveCommit(ref: string): Promise<string> {
|
||||
return (await runChecked(`resolve ${ref}`, ["git", "rev-parse", "--verify", `${ref}^{commit}`])).stdout.trim();
|
||||
}
|
||||
|
||||
async function resolveTree(ref: string): Promise<string> {
|
||||
return (await runChecked(`resolve tree ${ref}`, ["git", "rev-parse", "--verify", `${ref}^{tree}`])).stdout.trim();
|
||||
}
|
||||
|
||||
async function runChecked(label: string, cmd: string[]): Promise<CommandResult> {
|
||||
const result = await run(label, cmd);
|
||||
if (result.code !== 0) {
|
||||
throw new Error(`${label} failed with exit ${result.code}:\n${result.stderr || result.stdout}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function run(label: string, cmd: string[], options: { allowFailure?: boolean } = {}): Promise<CommandResult> {
|
||||
process.stderr.write(`+ ${label}: ${cmd.join(" ")}\n`);
|
||||
const child = Bun.spawn(cmd, {
|
||||
cwd: forkRepo,
|
||||
env: process.env,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout, stderr, code] = await Promise.all([
|
||||
new Response(child.stdout).text(),
|
||||
new Response(child.stderr).text(),
|
||||
child.exited,
|
||||
]);
|
||||
if (stdout) process.stderr.write(stdout);
|
||||
if (stderr) process.stderr.write(stderr);
|
||||
const result = { label, cmd, cwd: forkRepo, code, stdout, stderr };
|
||||
commands.push(result);
|
||||
if (code !== 0 && !options.allowFailure) {
|
||||
throw new Error(`${label} failed with exit ${code}:\n${stderr || stdout}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function baseArtifacts(base: { kind: string; label: string; sha: string }): Record<string, unknown> {
|
||||
return {
|
||||
upstreamKind: base.kind,
|
||||
upstreamLabel: base.label,
|
||||
upstreamSha: base.sha,
|
||||
};
|
||||
}
|
||||
|
||||
function candidateRefsFor(sha: string): Array<Record<string, unknown>> {
|
||||
const pushed = enabled("push", false);
|
||||
const remotes = pushed ? pushRemotes : ["local"];
|
||||
return remotes.map((remote) => ({
|
||||
kind: "branch",
|
||||
repo: forkRepoFullName,
|
||||
remote,
|
||||
ref: `refs/heads/${targetBranch}`,
|
||||
sha,
|
||||
pushed,
|
||||
}));
|
||||
}
|
||||
|
||||
function commandArtifacts(): Array<Record<string, unknown>> {
|
||||
return commands.map((command) => ({
|
||||
...command,
|
||||
stdout: truncate(command.stdout),
|
||||
stderr: truncate(command.stderr),
|
||||
}));
|
||||
}
|
||||
|
||||
function enabled(name: string, fallback: boolean): boolean {
|
||||
const envValue = process.env[`CODEX_FLOW_${name.toUpperCase()}`];
|
||||
if (envValue !== undefined) {
|
||||
return ["1", "true", "yes", "on"].includes(envValue.trim().toLowerCase());
|
||||
}
|
||||
const value = config[name];
|
||||
return typeof value === "boolean" ? value : fallback;
|
||||
}
|
||||
|
||||
function stringConfig(name: string, fallback: string): string {
|
||||
const value = config[name];
|
||||
return typeof value === "string" && value.trim() ? value : fallback;
|
||||
}
|
||||
|
||||
function stringArrayConfig(name: string, fallback: string[]): string[] {
|
||||
const value = config[name];
|
||||
if (!Array.isArray(value)) return fallback;
|
||||
const entries = value.filter((entry): entry is string => typeof entry === "string" && entry.trim());
|
||||
return entries.length > 0 ? entries : fallback;
|
||||
}
|
||||
|
||||
function stringValue(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function lines(value: string): string[] {
|
||||
return value.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
function shortTag(value: string): string {
|
||||
return value.replace(/^refs\/tags\//, "");
|
||||
}
|
||||
|
||||
function truncate(value: string, max = 4000): string {
|
||||
return value.length <= max ? value : `${value.slice(0, max)}\n...[truncated ${value.length - max} chars]`;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"id": "patch:harness-main:772bdd62c6031302b57b175e428110fa51671861:upstream.branch_update",
|
||||
"type": "upstream.branch_update",
|
||||
"source": "patch",
|
||||
"receivedAt": "2026-05-16T00:00:00.000Z",
|
||||
"payload": {
|
||||
"repo": "peezy-tech/patch-moi-harness",
|
||||
"ref": "refs/heads/main",
|
||||
"sha": "772bdd62c6031302b57b175e428110fa51671861"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"id": "patch:harness:v0.1.3:upstream.release",
|
||||
"type": "upstream.release",
|
||||
"source": "patch",
|
||||
"receivedAt": "2026-05-16T00:00:00.000Z",
|
||||
"payload": {
|
||||
"repo": "peezy-tech/patch-moi-harness",
|
||||
"tag": "v0.1.3",
|
||||
"url": "https://github.com/peezy-tech/patch-moi-harness/releases/tag/v0.1.3"
|
||||
}
|
||||
}
|
||||
44
flows/patch-moi-harness-fork/flow.toml
Normal file
44
flows/patch-moi-harness-fork/flow.toml
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
name = "patch-moi-harness-fork"
|
||||
version = 1
|
||||
description = "Maintain the harness fork patch workspace from upstream releases and main updates."
|
||||
|
||||
[config]
|
||||
expected_repo = "peezy-tech/patch-moi-harness"
|
||||
fork_repo = "harness/fork"
|
||||
fork_repo_full_name = "matamune-peezy/patch-moi-harness"
|
||||
target_branch = "main"
|
||||
upstream_branch = "upstream"
|
||||
patch_prefix = "patch/"
|
||||
upstream_remote = "upstream"
|
||||
upstream_repo_url = "https://github.com/peezy-tech/patch-moi-harness.git"
|
||||
fetch = true
|
||||
push = false
|
||||
push_remotes = ["origin", "jojo"]
|
||||
verify_commands = ["npm test", "npm run pack:dry-run"]
|
||||
seed_patch_refs = [
|
||||
"patch/010-maintained-greeting=e8e54070d4465b509b07d9d718ed693a75b6f870",
|
||||
"patch/020-shout-mode=5a06a998390b0b835adcad134f8055a2c2eb5b02",
|
||||
"patch/030-package-identity=9fd9d8c31a004059e16f769453d75afba29959be",
|
||||
]
|
||||
|
||||
[[steps]]
|
||||
name = "release-cycle"
|
||||
runner = "bun"
|
||||
script = "exec/update-fork.ts"
|
||||
cwd = "../.."
|
||||
timeout_ms = 600000
|
||||
|
||||
[steps.trigger]
|
||||
type = "upstream.release"
|
||||
schema = "schemas/upstream-release.schema.json"
|
||||
|
||||
[[steps]]
|
||||
name = "main-branch-update"
|
||||
runner = "bun"
|
||||
script = "exec/update-fork.ts"
|
||||
cwd = "../.."
|
||||
timeout_ms = 600000
|
||||
|
||||
[steps.trigger]
|
||||
type = "upstream.branch_update"
|
||||
schema = "schemas/upstream-branch-update.schema.json"
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"required": ["repo", "ref", "sha"],
|
||||
"properties": {
|
||||
"repo": {
|
||||
"type": "string",
|
||||
"enum": ["peezy-tech/patch-moi-harness"]
|
||||
},
|
||||
"ref": {
|
||||
"type": "string",
|
||||
"enum": ["refs/heads/main"]
|
||||
},
|
||||
"sha": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-fA-F]{7,40}$"
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"required": ["repo", "tag"],
|
||||
"properties": {
|
||||
"repo": {
|
||||
"type": "string",
|
||||
"enum": ["peezy-tech/patch-moi-harness"]
|
||||
},
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
# patch-moi-harness Flow
|
||||
|
||||
This flow is the first executable patch.moi maintenance harness. It consumes a
|
||||
generic `upstream.release` event for `peezy-tech/patch-moi-harness`, then uses
|
||||
Git state in `harness/fork` to keep the maintained fork on top of the upstream
|
||||
release tag.
|
||||
|
||||
The default behavior is local and reviewable:
|
||||
|
||||
- fetch the configured upstream remote
|
||||
- switch to the maintained fork branch
|
||||
- rebase onto the release tag when the tag is not already an ancestor
|
||||
- run the configured package checks
|
||||
- emit candidate branch refs in the `FLOW_RESULT` artifacts
|
||||
- leave pushes disabled unless `CODEX_FLOW_PUSH=1` is set
|
||||
|
||||
Useful overrides:
|
||||
|
||||
```bash
|
||||
CODEX_FLOW_FETCH=0 bun run harness:flow
|
||||
CODEX_FLOW_PUSH=1 bun run harness:flow
|
||||
```
|
||||
|
||||
The fixture event is `fixtures/upstream-release-v0.1.3.json`. It should be a
|
||||
no-op rebase against the current harness fork while still verifying the package
|
||||
surface and reporting the local maintained branch as the candidate ref.
|
||||
|
||||
The repo also exposes an experimental workspace-owned flow smoke task:
|
||||
|
||||
```bash
|
||||
cd ../codex-flows
|
||||
bun run workspace:backend --cwd /home/peezy/meta-workspace/patch.moi
|
||||
```
|
||||
|
||||
```bash
|
||||
CODEX_WORKSPACE_BACKEND_WS_URL=ws://127.0.0.1:3586 \
|
||||
CODEX_FLOW_FETCH=0 CODEX_FLOW_PUSH=0 \
|
||||
bun run workspace:run:harness-flow
|
||||
```
|
||||
|
||||
That task requires a running Codex workspace backend and writes backend
|
||||
event/run state, not patch.moi `DATA_DIR` maintenance-attempt state.
|
||||
|
|
@ -1,336 +0,0 @@
|
|||
import path from "node:path";
|
||||
import { defineBunFlow } from "@peezy.tech/codex-flows/flow-runtime/bun";
|
||||
import type { FlowResult, FlowResultStatus, FlowRunContext } from "@peezy.tech/codex-flows/flow-runtime";
|
||||
|
||||
type HarnessFlowContext = FlowRunContext & {
|
||||
flow: FlowRunContext["flow"] & {
|
||||
event: FlowRunContext["flow"]["event"] & {
|
||||
payload: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type CommandResult = {
|
||||
label: string;
|
||||
cmd: string[];
|
||||
cwd: string;
|
||||
exitCode: number | null;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
};
|
||||
|
||||
const expectedRepo = "peezy-tech/patch-moi-harness";
|
||||
let context: HarnessFlowContext;
|
||||
let config: Record<string, unknown>;
|
||||
let commands: CommandResult[];
|
||||
let releaseTag: string;
|
||||
let repo: string;
|
||||
let forkRepo: string;
|
||||
let forkRepoFullName: string;
|
||||
let targetBranch: string;
|
||||
let upstreamRemote: string;
|
||||
let upstreamRepoUrl: string;
|
||||
let verifyCommands: string[];
|
||||
let pushRemotes: string[];
|
||||
|
||||
class FlowFinished extends Error {
|
||||
constructor(readonly result: FlowResult) {
|
||||
super(result.message ?? result.status);
|
||||
}
|
||||
}
|
||||
|
||||
export default defineBunFlow(async (flowContext: FlowRunContext): Promise<FlowResult> => {
|
||||
context = flowContext as HarnessFlowContext;
|
||||
config = context.flow.config ?? {};
|
||||
commands = [];
|
||||
|
||||
const workspaceRoot = process.cwd();
|
||||
const payload = context.flow.event.payload ?? {};
|
||||
releaseTag = tagFromPayload(payload);
|
||||
repo = stringValue(payload.repo, "payload.repo");
|
||||
forkRepo = path.resolve(workspaceRoot, stringConfig("fork_repo", "harness/fork"));
|
||||
forkRepoFullName = stringConfig("fork_repo_full_name", "matamune-peezy/patch-moi-harness");
|
||||
targetBranch = stringConfig("target_branch", "main");
|
||||
upstreamRemote = stringConfig("upstream_remote", "upstream");
|
||||
upstreamRepoUrl = stringConfig("upstream_repo_url", "https://github.com/peezy-tech/patch-moi-harness.git");
|
||||
verifyCommands = stringArrayConfig("verify_commands", ["npm test", "npm run pack:dry-run"]);
|
||||
pushRemotes = stringArrayConfig("push_remotes", ["origin", "jojo"]);
|
||||
|
||||
try {
|
||||
if (repo !== expectedRepo) {
|
||||
finish("skipped", `Harness flow ignores ${repo}.`, { repo, expectedRepo });
|
||||
}
|
||||
if (!releaseTag) {
|
||||
finish("failed", "Release payload is missing tag or refs/tags ref.");
|
||||
}
|
||||
|
||||
const repoCheck = await run("verify harness fork checkout", ["git", "rev-parse", "--show-toplevel"]);
|
||||
if (repoCheck.exitCode !== 0) {
|
||||
finish("blocked", `Harness fork checkout is not available at ${forkRepo}.`, { forkRepo });
|
||||
}
|
||||
|
||||
await ensureUpstreamRemote();
|
||||
|
||||
if (enabled("fetch", true)) {
|
||||
await run("fetch upstream release refs", ["git", "fetch", upstreamRemote, "--tags", "--prune"]);
|
||||
}
|
||||
|
||||
await requireNoRebaseInProgress();
|
||||
await requireCleanWorktree("before harness fork rebase");
|
||||
|
||||
const branch = (await run("read current branch", ["git", "branch", "--show-current"])).stdout.trim();
|
||||
if (branch !== targetBranch) {
|
||||
await run("switch maintained fork branch", ["git", "switch", targetBranch]);
|
||||
}
|
||||
|
||||
const beforeSha = (await run("read fork head before rebase", ["git", "rev-parse", "HEAD"])).stdout.trim();
|
||||
const releaseSha = await resolveReleaseCommit(releaseTag);
|
||||
const alreadyContainsRelease = (await run(
|
||||
"check release ancestor",
|
||||
["git", "merge-base", "--is-ancestor", releaseSha, "HEAD"],
|
||||
{ allowFailure: true },
|
||||
)).exitCode === 0;
|
||||
|
||||
if (!alreadyContainsRelease) {
|
||||
const rebase = await run(
|
||||
"rebase harness fork onto upstream release",
|
||||
["git", "rebase", releaseSha],
|
||||
{ allowFailure: true },
|
||||
);
|
||||
if (rebase.exitCode !== 0) {
|
||||
const context = await collectRebaseContext(rebase, beforeSha);
|
||||
finish("needs_intervention", `Harness fork rebase stopped on ${releaseTag}.`, context);
|
||||
}
|
||||
}
|
||||
|
||||
for (const command of verifyCommands) {
|
||||
const result = await run(`verify: ${command}`, ["bash", "-lc", command], { allowFailure: true });
|
||||
if (result.exitCode !== 0) {
|
||||
finish("needs_intervention", `Harness verification failed: ${command}.`, {
|
||||
failedCommand: command,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await requireCleanWorktree("after harness fork verification");
|
||||
|
||||
const afterSha = (await run("read fork head after rebase", ["git", "rev-parse", "HEAD"])).stdout.trim();
|
||||
|
||||
if (enabled("push", false)) {
|
||||
for (const remote of pushRemotes) {
|
||||
await run(`push ${remote}/${targetBranch}`, [
|
||||
"git",
|
||||
"push",
|
||||
"--force-with-lease",
|
||||
remote,
|
||||
`HEAD:${targetBranch}`,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
finish(beforeSha === afterSha ? "completed" : "changed", harnessMessage(beforeSha, afterSha), {
|
||||
eventId: context.flow.event.id,
|
||||
repo,
|
||||
forkRepo,
|
||||
forkRepoFullName,
|
||||
targetBranch,
|
||||
releaseTag,
|
||||
releaseSha,
|
||||
beforeSha,
|
||||
afterSha,
|
||||
pushed: enabled("push", false),
|
||||
checks: verifyCommands.map((command) => ({ name: command, status: "passed" })),
|
||||
candidateRefs: candidateRefsFor(afterSha),
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof FlowFinished) {
|
||||
return error.result;
|
||||
}
|
||||
return buildResult("failed", error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
});
|
||||
|
||||
async function ensureUpstreamRemote(): Promise<void> {
|
||||
const current = await run("read upstream remote", ["git", "remote", "get-url", upstreamRemote], {
|
||||
allowFailure: true,
|
||||
});
|
||||
if (current.exitCode === 0) {
|
||||
return;
|
||||
}
|
||||
if (!upstreamRepoUrl) {
|
||||
finish("blocked", `Missing ${upstreamRemote} remote and no upstream_repo_url is configured.`);
|
||||
}
|
||||
await run("add upstream remote", ["git", "remote", "add", upstreamRemote, upstreamRepoUrl]);
|
||||
}
|
||||
|
||||
async function requireNoRebaseInProgress(): Promise<void> {
|
||||
const state = await run(
|
||||
"check existing rebase state",
|
||||
["bash", "-lc", 'test -d "$(git rev-parse --git-path rebase-merge)" -o -d "$(git rev-parse --git-path rebase-apply)"'],
|
||||
{ allowFailure: true },
|
||||
);
|
||||
if (state.exitCode === 0) {
|
||||
finish("blocked", "A rebase is already in progress in the harness fork checkout.", {
|
||||
forkRepo,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function requireCleanWorktree(stage: string): Promise<void> {
|
||||
const status = await run(`dirty worktree check ${stage}`, ["git", "status", "--porcelain=v1"]);
|
||||
if (status.stdout.trim()) {
|
||||
finish("blocked", `Harness fork checkout has local changes ${stage}.`, {
|
||||
dirtyStatus: status.stdout,
|
||||
forkRepo,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveReleaseCommit(tag: string): Promise<string> {
|
||||
const candidates = tag.startsWith("refs/")
|
||||
? [`${tag}^{commit}`, `${shortTag(tag)}^{commit}`]
|
||||
: [`refs/tags/${tag}^{commit}`, `${tag}^{commit}`];
|
||||
for (const candidate of candidates) {
|
||||
const result = await run(`resolve release ref ${candidate}`, ["git", "rev-parse", "--verify", candidate], {
|
||||
allowFailure: true,
|
||||
});
|
||||
if (result.exitCode === 0 && result.stdout.trim()) {
|
||||
return result.stdout.trim();
|
||||
}
|
||||
}
|
||||
finish("blocked", `Could not resolve upstream release tag ${tag}.`, {
|
||||
tag,
|
||||
forkRepo,
|
||||
});
|
||||
}
|
||||
|
||||
async function collectRebaseContext(rebase: CommandResult, beforeSha: string): Promise<Record<string, unknown>> {
|
||||
const status = await run("rebase conflict status", ["git", "status", "--short", "--branch"], {
|
||||
allowFailure: true,
|
||||
});
|
||||
const unmerged = await run("unmerged files", ["git", "diff", "--name-only", "--diff-filter=U"], {
|
||||
allowFailure: true,
|
||||
});
|
||||
const currentPatch = await run("current rebase patch", ["git", "rebase", "--show-current-patch"], {
|
||||
allowFailure: true,
|
||||
});
|
||||
return {
|
||||
beforeSha,
|
||||
rebaseOutput: truncate(rebase.stderr || rebase.stdout, 8000),
|
||||
statusOutput: status.stdout,
|
||||
unmergedFiles: unmerged.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean),
|
||||
currentPatch: truncate(currentPatch.stdout || currentPatch.stderr, 12000),
|
||||
};
|
||||
}
|
||||
|
||||
async function run(
|
||||
label: string,
|
||||
cmd: string[],
|
||||
options: { allowFailure?: boolean } = {},
|
||||
): Promise<CommandResult> {
|
||||
const child = Bun.spawn(cmd, {
|
||||
cwd: forkRepo,
|
||||
env: process.env,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
new Response(child.stdout).text(),
|
||||
new Response(child.stderr).text(),
|
||||
child.exited,
|
||||
]);
|
||||
const result = { label, cmd, cwd: forkRepo, exitCode, stdout, stderr };
|
||||
commands.push(result);
|
||||
if (exitCode !== 0 && !options.allowFailure) {
|
||||
throw new Error(`${label} failed with exit ${exitCode}:\n${stderr || stdout}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function finish(status: FlowResultStatus, message: string, artifacts: Record<string, unknown> = {}): never {
|
||||
throw new FlowFinished(buildResult(status, message, artifacts));
|
||||
}
|
||||
|
||||
function buildResult(status: FlowResultStatus, message: string, artifacts: Record<string, unknown> = {}): FlowResult {
|
||||
const commandArtifacts = commands.map((command) => ({
|
||||
...command,
|
||||
stdout: truncate(command.stdout),
|
||||
stderr: truncate(command.stderr),
|
||||
}));
|
||||
return {
|
||||
status,
|
||||
message,
|
||||
artifacts: {
|
||||
...artifacts,
|
||||
commands: commandArtifacts,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function harnessMessage(beforeSha: string, afterSha: string): string {
|
||||
if (beforeSha === afterSha) {
|
||||
return `Harness fork already contains ${releaseTag}; package checks passed.`;
|
||||
}
|
||||
return `Harness fork rebased onto ${releaseTag}; package checks passed.`;
|
||||
}
|
||||
|
||||
function candidateRefsFor(sha: string): Array<Record<string, unknown>> {
|
||||
const pushed = enabled("push", false);
|
||||
const remotes = pushed ? pushRemotes : ["local"];
|
||||
return remotes.map((remote) => ({
|
||||
kind: "branch",
|
||||
repo: forkRepoFullName,
|
||||
remote,
|
||||
ref: `refs/heads/${targetBranch}`,
|
||||
sha,
|
||||
pushed,
|
||||
}));
|
||||
}
|
||||
|
||||
function enabled(name: string, fallback: boolean): boolean {
|
||||
const envValue = process.env[`CODEX_FLOW_${name.toUpperCase()}`];
|
||||
if (envValue !== undefined) {
|
||||
return ["1", "true", "yes", "on"].includes(envValue.trim().toLowerCase());
|
||||
}
|
||||
const value = config[name];
|
||||
return typeof value === "boolean" ? value : fallback;
|
||||
}
|
||||
|
||||
function stringConfig(name: string, fallback: string): string {
|
||||
const value = config[name];
|
||||
return typeof value === "string" && value.trim() ? value : fallback;
|
||||
}
|
||||
|
||||
function stringArrayConfig(name: string, fallback: string[]): string[] {
|
||||
const value = config[name];
|
||||
if (!Array.isArray(value)) {
|
||||
return fallback;
|
||||
}
|
||||
const entries = value.filter((entry): entry is string => typeof entry === "string" && entry.trim());
|
||||
return entries.length > 0 ? entries : fallback;
|
||||
}
|
||||
|
||||
function stringValue(value: unknown, name: string): string {
|
||||
if (typeof value !== "string" || !value.trim()) {
|
||||
throw new Error(`${name} must be a non-empty string`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function tagFromPayload(value: Record<string, unknown>): string {
|
||||
if (typeof value.tag === "string" && value.tag.trim()) {
|
||||
return value.tag.trim();
|
||||
}
|
||||
if (typeof value.ref === "string" && value.ref.startsWith("refs/tags/")) {
|
||||
return value.ref.trim();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function shortTag(value: string): string {
|
||||
return value.replace(/^refs\/tags\//, "");
|
||||
}
|
||||
|
||||
function truncate(value: string, max = 4000): string {
|
||||
return value.length <= max ? value : `${value.slice(0, max)}\n...[truncated ${value.length - max} chars]`;
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
name = "patch-moi-harness"
|
||||
version = 1
|
||||
description = "Rebase the maintained patch.moi harness fork onto an upstream harness release."
|
||||
|
||||
[config]
|
||||
fork_repo = "harness/fork"
|
||||
fork_repo_full_name = "matamune-peezy/patch-moi-harness"
|
||||
target_branch = "main"
|
||||
upstream_remote = "upstream"
|
||||
upstream_repo_url = "https://github.com/peezy-tech/patch-moi-harness.git"
|
||||
fetch = true
|
||||
push = false
|
||||
push_remotes = ["origin", "jojo"]
|
||||
verify_commands = ["npm test", "npm run pack:dry-run"]
|
||||
|
||||
[[steps]]
|
||||
name = "rebase-fork"
|
||||
runner = "bun"
|
||||
script = "exec/rebase-fork.ts"
|
||||
cwd = "../.."
|
||||
timeout_ms = 600000
|
||||
|
||||
[steps.trigger]
|
||||
type = "upstream.release"
|
||||
schema = "schemas/upstream-release.schema.json"
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"type": "object",
|
||||
"required": ["repo", "tag"],
|
||||
"properties": {
|
||||
"provider": { "type": "string" },
|
||||
"repo": {
|
||||
"type": "string",
|
||||
"enum": ["peezy-tech/patch-moi-harness"]
|
||||
},
|
||||
"tag": { "type": "string", "minLength": 1 },
|
||||
"ref": { "type": "string" },
|
||||
"url": { "type": "string" },
|
||||
"publishedAt": { "type": "string" }
|
||||
}
|
||||
}
|
||||
|
|
@ -34,8 +34,13 @@ git -C harness/fork fetch jojo
|
|||
## Branch Model
|
||||
|
||||
- `harness/upstream` `main`: upstream package history and upstream releases.
|
||||
- `harness/fork` `upstream/main`: fetched copy of upstream `main`.
|
||||
- `harness/fork` `main`: maintained fork with patch commits applied.
|
||||
- `harness/fork` `upstream`: local branch that follows the selected upstream
|
||||
release tag or main commit for a maintenance run.
|
||||
- `harness/fork` `main`: maintained fork rebuilt from `upstream` plus ordered
|
||||
`patch/*` branches.
|
||||
- `harness/fork` `patch/*`: one logical fork patch per branch tip. The current
|
||||
seeds are `patch/010-maintained-greeting`, `patch/020-shout-mode`, and
|
||||
`patch/030-package-identity`.
|
||||
- `harness/fork` `jojo/main`: service-style maintained fork remote.
|
||||
|
||||
The upstream npm package is `@peezy.tech/patch-moi-harness`. It publishes from
|
||||
|
|
@ -84,7 +89,7 @@ git push jojo main
|
|||
Expected result: the GitHub fork and jojo maintained branch both move ahead of
|
||||
the last upstream commit.
|
||||
|
||||
## Scenario: Rebase Fork Onto Upstream
|
||||
## Scenario: Rebuild Fork Onto Upstream
|
||||
|
||||
Use this when upstream has released and the maintained fork needs to carry its
|
||||
patches forward.
|
||||
|
|
@ -92,25 +97,30 @@ patches forward.
|
|||
```bash
|
||||
cd harness/fork
|
||||
git fetch upstream
|
||||
git fetch origin
|
||||
git branch -f upstream upstream/main
|
||||
git checkout --detach upstream
|
||||
for patch in $(git for-each-ref --format='%(refname:short)' refs/heads/patch | sort); do
|
||||
git cherry-pick "$patch"
|
||||
done
|
||||
git branch -f main HEAD
|
||||
git checkout main
|
||||
git rebase upstream/main
|
||||
npm test
|
||||
git push --force-with-lease origin main
|
||||
git push --force-with-lease jojo main
|
||||
```
|
||||
|
||||
Expected result: fork `main` is still patched, but its base is the latest
|
||||
upstream release.
|
||||
upstream release or main commit.
|
||||
|
||||
The same maintenance path is executable through the first patch.moi harness
|
||||
flow:
|
||||
The same maintenance path is executable through the patch.moi harness flows:
|
||||
|
||||
```bash
|
||||
CODEX_FLOW_FETCH=0 CODEX_FLOW_PUSH=0 bun run harness:flow
|
||||
```
|
||||
|
||||
That direct command is local-mode execution. The repo-native workspace autonomy
|
||||
That direct command is local-mode execution. The default upstream release
|
||||
fixture fans out to `patch-moi-harness-bindings/generate-bindings` and
|
||||
`patch-moi-harness-fork/release-cycle`. The repo-native workspace autonomy
|
||||
surface runs the same harness through a manual command task:
|
||||
|
||||
```bash
|
||||
|
|
@ -125,7 +135,26 @@ flow event to the configured workspace backend.
|
|||
The default fixture targets `v0.1.3`, which should verify the current fork
|
||||
without changing it and report `candidateRefs` for the maintained fork branch.
|
||||
For a new upstream tag, run the same command with an event file whose
|
||||
`payload.tag` names that tag.
|
||||
`payload.tag` names that tag. For upstream main movement, use the
|
||||
`flows/patch-moi-harness-fork/fixtures/upstream-main-v0.1.3.json` event shape
|
||||
with the new main SHA.
|
||||
|
||||
## Scenario: Downstream Release Artifact
|
||||
|
||||
Use this when a downstream package release should prepare a local fork artifact
|
||||
without pushing or publishing:
|
||||
|
||||
```bash
|
||||
bun run patch.moi -- run event \
|
||||
--file flows/patch-moi-harness-flows-fork/fixtures/downstream-fork-release-v0.1.3-fork.0.json \
|
||||
--allow-local \
|
||||
--json
|
||||
```
|
||||
|
||||
Expected result: `patch-moi-harness-flows-fork/release-fork` creates a
|
||||
flow-owned worktree under `.codex/flow-artifacts`, runs the fork package tests,
|
||||
and writes an npm tarball under
|
||||
`.codex/flow-artifacts/patch-moi-harness-flows-fork-release`.
|
||||
|
||||
## Scenario: Fork Release
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue