Route Codex main updates to fork flows
All checks were successful
check / check (push) Successful in 36s
All checks were successful
check / check (push) Successful in 36s
This commit is contained in:
parent
927effa853
commit
c14470a8f4
22 changed files with 951 additions and 291 deletions
|
|
@ -1,8 +1,12 @@
|
|||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
type FlowContext = {
|
||||
flow: {
|
||||
name?: string;
|
||||
root?: string;
|
||||
step?: string;
|
||||
config?: Record<string, unknown>;
|
||||
event: {
|
||||
id: string;
|
||||
|
|
@ -10,6 +14,9 @@ type FlowContext = {
|
|||
payload: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
runtime?: {
|
||||
workspaceBackendUrl?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type CommandResult = {
|
||||
|
|
@ -23,7 +30,7 @@ type CommandResult = {
|
|||
|
||||
const context = JSON.parse(await Bun.stdin.text()) as FlowContext;
|
||||
const config = context.flow.config ?? {};
|
||||
const repoRoot = process.cwd();
|
||||
const repoRoot = path.resolve(envConfig(stringConfig("codex_flows_repo_env", "")) || stringConfig("codex_flows_repo", process.cwd()));
|
||||
const commands: CommandResult[] = [];
|
||||
|
||||
try {
|
||||
|
|
@ -52,15 +59,20 @@ try {
|
|||
|
||||
await updatePackageVersion(packageJsonPath, version);
|
||||
await run("refresh Bun lockfile", ["bun", "install"]);
|
||||
|
||||
const changedStatus = await run("changed file status", ["git", "status", "--short"]);
|
||||
if (!changedStatus.stdout.trim()) {
|
||||
finish("skipped", `No generated binding changes for ${tag}.`, { version, tag });
|
||||
}
|
||||
|
||||
const followupTurn = await maybeRunFollowupTurn({ tag, version, changedStatus: changedStatus.stdout });
|
||||
|
||||
await run("codex-flows package release check", ["bun", "run", "--filter", packageName, "release:check"]);
|
||||
await run("workspace typecheck", ["bun", "run", "check:types"]);
|
||||
await run("workspace tests", ["bun", "run", "test"]);
|
||||
await run("git diff check", ["git", "diff", "--check"]);
|
||||
|
||||
const status = await run("final git status", ["git", "status", "--short"]);
|
||||
if (!status.stdout.trim()) {
|
||||
finish("skipped", `No generated binding changes for ${tag}.`, { version, tag });
|
||||
}
|
||||
|
||||
if (enabled("commit", true)) {
|
||||
await run("stage binding update", ["git", "add", "--", generatedDir, packageJsonPath, path.join(repoRoot, "bun.lock")]);
|
||||
|
|
@ -96,6 +108,7 @@ try {
|
|||
committed: enabled("commit", true),
|
||||
pushed: enabled("push", false),
|
||||
published: enabled("publish", false),
|
||||
followupTurn,
|
||||
});
|
||||
} catch (error) {
|
||||
finish("failed", error instanceof Error ? error.message : String(error));
|
||||
|
|
@ -127,6 +140,89 @@ async function updatePackageVersion(packageJsonPath: string, version: string): P
|
|||
await writeFile(packageJsonPath, `${JSON.stringify(parsed, null, "\t")}\n`);
|
||||
}
|
||||
|
||||
async function maybeRunFollowupTurn(input: {
|
||||
tag: string;
|
||||
version: string;
|
||||
changedStatus: string;
|
||||
}): Promise<Record<string, unknown> | undefined> {
|
||||
if (!enabled("followup_turn", true)) {
|
||||
return undefined;
|
||||
}
|
||||
const runCodexAgentTurnFromFlow = await loadRunCodexAgentTurnFromFlow();
|
||||
const threadJson = stringConfig("followup_thread_json", ".codex/flow-artifacts/openai-codex-bindings-thread.json");
|
||||
const prompt = [
|
||||
`OpenAI Codex ${input.tag} regenerated app-server TypeScript bindings for @peezy.tech/codex-flows ${input.version}.`,
|
||||
"",
|
||||
"Review the changed files and make any source, test, or export updates needed so the package still builds against the regenerated bindings.",
|
||||
"Keep changes focused on codex-flows compatibility with the regenerated app-server surface.",
|
||||
"Do not push, publish, or edit release metadata beyond what this flow already changed.",
|
||||
"",
|
||||
"Current changed files:",
|
||||
input.changedStatus.trim(),
|
||||
].join("\n");
|
||||
const turn = await runCodexAgentTurnFromFlow(
|
||||
{
|
||||
flow: {
|
||||
name: context.flow.name ?? "openai-codex-bindings",
|
||||
root: repoRoot,
|
||||
step: context.flow.step ?? "regenerate-bindings",
|
||||
event: context.flow.event,
|
||||
},
|
||||
runtime: context.runtime,
|
||||
},
|
||||
{
|
||||
prompt,
|
||||
cwd: repoRoot,
|
||||
approvalPolicy: "never",
|
||||
sandbox: "danger-full-access",
|
||||
appServerUrl: process.env.CODEX_APP_SERVER_URL,
|
||||
requestTimeoutMs: numberConfig("followup_request_timeout_ms", 900000),
|
||||
wait: {
|
||||
timeoutMs: numberConfig("followup_wait_timeout_ms", 900000),
|
||||
throwOnFailure: true,
|
||||
},
|
||||
exportThreadJson: threadJson || false,
|
||||
},
|
||||
) as {
|
||||
artifacts?: Record<string, unknown>;
|
||||
threadId?: string;
|
||||
turnId?: string;
|
||||
threadJsonPath?: string;
|
||||
};
|
||||
return {
|
||||
threadId: turn.threadId,
|
||||
turnId: turn.turnId,
|
||||
threadJsonPath: turn.threadJsonPath,
|
||||
...(turn.artifacts ?? {}),
|
||||
};
|
||||
}
|
||||
|
||||
async function loadRunCodexAgentTurnFromFlow(): Promise<(
|
||||
context: unknown,
|
||||
options: Record<string, unknown>,
|
||||
) => Promise<unknown>> {
|
||||
const candidates = [
|
||||
"@peezy.tech/codex-flows/flows",
|
||||
pathToFileURL(path.join(repoRoot, "packages/codex-client/src/app-server/flows.ts")).href,
|
||||
];
|
||||
const errors: string[] = [];
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
const module = await import(candidate) as { runCodexAgentTurnFromFlow?: unknown };
|
||||
if (typeof module.runCodexAgentTurnFromFlow === "function") {
|
||||
return module.runCodexAgentTurnFromFlow as (
|
||||
context: unknown,
|
||||
options: Record<string, unknown>,
|
||||
) => Promise<unknown>;
|
||||
}
|
||||
errors.push(`${candidate}: runCodexAgentTurnFromFlow is not exported`);
|
||||
} catch (error) {
|
||||
errors.push(`${candidate}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
throw new Error(`Could not load codex-flows follow-up turn helper:\n${errors.join("\n")}`);
|
||||
}
|
||||
|
||||
async function run(
|
||||
label: string,
|
||||
cmd: string[],
|
||||
|
|
@ -171,11 +267,23 @@ function enabled(name: string, fallback: boolean): boolean {
|
|||
return typeof value === "boolean" ? value : fallback;
|
||||
}
|
||||
|
||||
function envConfig(name: string): string | undefined {
|
||||
return name.trim() ? process.env[name]?.trim() || undefined : undefined;
|
||||
}
|
||||
|
||||
function stringConfig(name: string, fallback: string): string {
|
||||
const value = config[name];
|
||||
return typeof value === "string" && value.trim() ? value : fallback;
|
||||
}
|
||||
|
||||
function numberConfig(name: string, fallback: number): number {
|
||||
const envName = `CODEX_FLOW_${name.toUpperCase()}`;
|
||||
const envValue = process.env[envName];
|
||||
const raw = envValue !== undefined ? envValue : config[name];
|
||||
const value = typeof raw === "number" ? raw : typeof raw === "string" ? Number(raw) : fallback;
|
||||
return Number.isFinite(value) ? value : fallback;
|
||||
}
|
||||
|
||||
function stringValue(value: unknown, name: string): string {
|
||||
if (typeof value !== "string" || !value.trim()) {
|
||||
throw new Error(`${name} must be a non-empty string`);
|
||||
|
|
|
|||
|
|
@ -4,9 +4,13 @@ description = "Regenerate @peezy.tech/codex-flows bindings from a canonical open
|
|||
|
||||
[config]
|
||||
package_name = "@peezy.tech/codex-flows"
|
||||
codex_flows_repo_env = "PEEZY_CODEX_FLOWS_REPO"
|
||||
codex_flows_repo = "/home/peezy/meta-workspace/codex-flows"
|
||||
generated_dir = "packages/codex-client/src/app-server/generated"
|
||||
package_json = "packages/codex-client/package.json"
|
||||
commit = true
|
||||
followup_turn = true
|
||||
followup_thread_json = ".codex/flow-artifacts/openai-codex-bindings-thread.json"
|
||||
push = false
|
||||
publish = false
|
||||
github_repo = "peezy-tech/codex-flows"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
const config = flow.config || {};
|
||||
const payload = flow.event.payload || {};
|
||||
const eventType = String(flow.event.type || "");
|
||||
const commands = [];
|
||||
|
||||
function q(value) {
|
||||
|
|
@ -88,16 +89,16 @@ async function run(label, cmd, options = {}) {
|
|||
yield_time_ms: options.yield_time_ms || 1000,
|
||||
max_output_tokens: options.max_output_tokens || 12000
|
||||
});
|
||||
const result = {
|
||||
const command = {
|
||||
label,
|
||||
cmd,
|
||||
workdir,
|
||||
exit_code: exitCodeOf(raw),
|
||||
output: outputOf(raw)
|
||||
};
|
||||
commands.push({ ...result, output: truncate(result.output, 4000) });
|
||||
text("exit_code=" + String(result.exit_code) + "\n" + truncate(result.output, options.textLimit || 12000) + "\n");
|
||||
return result;
|
||||
commands.push({ ...command, output: truncate(command.output, 4000) });
|
||||
text("exit_code=" + String(command.exit_code) + "\n" + truncate(command.output, options.textLimit || 12000) + "\n");
|
||||
return command;
|
||||
}
|
||||
|
||||
function finish(status, message, artifacts = {}) {
|
||||
|
|
@ -105,56 +106,541 @@ function finish(status, message, artifacts = {}) {
|
|||
status,
|
||||
message,
|
||||
artifacts: {
|
||||
eventType,
|
||||
operation,
|
||||
releaseTag,
|
||||
version,
|
||||
codexRepo,
|
||||
targetBranch,
|
||||
upstreamBranch,
|
||||
patchPrefix,
|
||||
commands,
|
||||
...artifacts
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function collectRebaseContext(rebaseOutput, beforeSha) {
|
||||
const status = await run("rebase conflict status", "git status --short --branch", { max_output_tokens: 12000 });
|
||||
async function collectConflictContext(params) {
|
||||
const status = await run("patch rebuild conflict status", "git status --short --branch", { max_output_tokens: 12000 });
|
||||
const unmerged = await run("unmerged files", "git diff --name-only --diff-filter=U", { max_output_tokens: 12000 });
|
||||
const diffStat = await run("conflict diff stat", "git diff --cc --stat", { max_output_tokens: 12000 });
|
||||
const conflictDiff = await run("conflict diff", "git diff --cc", { max_output_tokens: 30000, textLimit: 20000 });
|
||||
const currentPatch = await run("current rebase patch", "git rebase --show-current-patch", { max_output_tokens: 20000, textLimit: 12000 });
|
||||
const patchDetails = params.failedPatch
|
||||
? await run(
|
||||
"failed patch details",
|
||||
"git show --stat --oneline --decorate --no-renames " + q(params.failedPatch.sha),
|
||||
{ max_output_tokens: 16000, textLimit: 12000 }
|
||||
)
|
||||
: undefined;
|
||||
return {
|
||||
beforeSha,
|
||||
rebaseOutput,
|
||||
beforeSha: params.beforeSha,
|
||||
baseRef: params.baseRef,
|
||||
baseSha: params.baseSha,
|
||||
rebuildOutput: params.rebuildOutput,
|
||||
statusOutput: status.output,
|
||||
unmergedFiles: unmerged.output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean),
|
||||
diffStat: diffStat.output,
|
||||
conflictDiff: truncate(conflictDiff.output, 20000),
|
||||
currentPatch: truncate(currentPatch.output, 12000),
|
||||
interventionPrompt: "Continue this same Code Mode thread to resolve the paused rebase. Preserve the fork patch stack, do not abort or reset unless explicitly instructed, then run the configured verification commands."
|
||||
failedPatch: params.failedPatch,
|
||||
patchDetails: patchDetails ? truncate(patchDetails.output, 12000) : undefined,
|
||||
applied: params.applied,
|
||||
interventionPrompt: [
|
||||
"Continue this same Code Mode thread to resolve the paused cherry-pick.",
|
||||
"Preserve the fork patch branch semantics: each patch/* branch remains one logical patch commit, main is rebuilt output, and upstream follows upstream/main.",
|
||||
"After resolving conflicts, continue the cherry-pick, update main to the rebuilt HEAD, switch back to main, and run the configured verification commands."
|
||||
].join(" ")
|
||||
};
|
||||
}
|
||||
|
||||
async function requireCleanWorktree() {
|
||||
const status = await run("dirty check", "git status --porcelain=v1", { max_output_tokens: 12000 });
|
||||
if (trim(status.output)) {
|
||||
finish("blocked", "Codex checkout has local changes before fork maintenance.", {
|
||||
dirtyStatus: status.output
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function requireNoPausedGitOperation() {
|
||||
const cherryPick = await run(
|
||||
"check existing cherry-pick state",
|
||||
"test -f \"$(git rev-parse --git-path CHERRY_PICK_HEAD)\"",
|
||||
{ max_output_tokens: 4000 }
|
||||
);
|
||||
if (cherryPick.exit_code === 0) {
|
||||
const context = await collectConflictContext({
|
||||
beforeSha: undefined,
|
||||
baseRef: undefined,
|
||||
baseSha: undefined,
|
||||
rebuildOutput: "A cherry-pick was already in progress before this flow started.",
|
||||
applied: []
|
||||
});
|
||||
finish("blocked", "A cherry-pick is already in progress in the Codex checkout.", context);
|
||||
}
|
||||
|
||||
const rebase = await run(
|
||||
"check existing rebase state",
|
||||
"test -d \"$(git rev-parse --git-path rebase-merge)\" -o -d \"$(git rev-parse --git-path rebase-apply)\"",
|
||||
{ max_output_tokens: 4000 }
|
||||
);
|
||||
if (rebase.exit_code === 0) {
|
||||
finish("blocked", "A rebase is already in progress in the Codex checkout.", {
|
||||
statusOutput: (await run("rebase status", "git status --short --branch", { max_output_tokens: 12000 })).output
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureUpstreamRemote() {
|
||||
const remote = await run(
|
||||
"ensure upstream openai/codex remote",
|
||||
"git remote get-url " + q(upstreamRemote) + " >/dev/null 2>&1 && git remote set-url " + q(upstreamRemote) + " " + q(upstreamRepoUrl) + " || git remote add " + q(upstreamRemote) + " " + q(upstreamRepoUrl),
|
||||
{ max_output_tokens: 12000 }
|
||||
);
|
||||
if (!ok(remote)) {
|
||||
finish("failed", "Could not configure upstream remote.", { remoteOutput: remote.output });
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchUpstreamMainAndTags() {
|
||||
const refspec = "+refs/heads/" + upstreamMainRef + ":refs/remotes/" + upstreamRemote + "/" + upstreamMainRef;
|
||||
const fetch = await run(
|
||||
"fetch upstream main and tags",
|
||||
"git fetch " + q(upstreamRemote) + " --tags --prune " + q(refspec),
|
||||
{ max_output_tokens: 20000, textLimit: 16000 }
|
||||
);
|
||||
if (!ok(fetch)) {
|
||||
finish("failed", "Could not fetch upstream main and release tags.", { fetchOutput: fetch.output });
|
||||
}
|
||||
const remoteMain = await run(
|
||||
"resolve upstream main",
|
||||
"git rev-parse --verify " + q("refs/remotes/" + upstreamRemote + "/" + upstreamMainRef + "^{commit}"),
|
||||
{ max_output_tokens: 4000 }
|
||||
);
|
||||
if (!ok(remoteMain)) {
|
||||
finish("failed", "Could not resolve fetched upstream main.", { upstreamMainOutput: remoteMain.output });
|
||||
}
|
||||
|
||||
const current = await currentBranch();
|
||||
if (current === upstreamBranch) {
|
||||
const switched = await run("switch away from upstream branch", "git switch " + q(targetBranch), {
|
||||
max_output_tokens: 12000
|
||||
});
|
||||
if (!ok(switched)) {
|
||||
const detached = await run("detach from upstream branch", "git switch --detach", { max_output_tokens: 12000 });
|
||||
if (!ok(detached)) {
|
||||
finish("failed", "Could not leave the upstream branch before updating it.", {
|
||||
switchOutput: switched.output,
|
||||
detachOutput: detached.output
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const update = await run(
|
||||
"update local upstream branch",
|
||||
"git update-ref " + q("refs/heads/" + upstreamBranch) + " " + q(trim(remoteMain.output)),
|
||||
{ max_output_tokens: 12000 }
|
||||
);
|
||||
if (!ok(update)) {
|
||||
finish("failed", "Could not update local upstream branch.", { updateOutput: update.output });
|
||||
}
|
||||
return trim(remoteMain.output);
|
||||
}
|
||||
|
||||
async function resolveReleaseBase() {
|
||||
const release = await run(
|
||||
"resolve release tag",
|
||||
"git rev-parse --verify " + q("refs/tags/" + releaseTag + "^{commit}"),
|
||||
{ max_output_tokens: 4000 }
|
||||
);
|
||||
if (!ok(release)) {
|
||||
finish("failed", "Could not resolve upstream release tag after fetch.", {
|
||||
releaseTag,
|
||||
resolveOutput: release.output
|
||||
});
|
||||
}
|
||||
return trim(release.output);
|
||||
}
|
||||
|
||||
async function currentBranch() {
|
||||
const branch = await run("current branch", "git rev-parse --abbrev-ref HEAD", { max_output_tokens: 4000 });
|
||||
return ok(branch) ? trim(branch.output) : "";
|
||||
}
|
||||
|
||||
async function resolveCommit(ref) {
|
||||
const result = await run(
|
||||
"resolve " + ref,
|
||||
"git rev-parse --verify " + q(ref + "^{commit}"),
|
||||
{ max_output_tokens: 4000 }
|
||||
);
|
||||
return ok(result) ? trim(result.output) : "";
|
||||
}
|
||||
|
||||
async function resolveTree(ref) {
|
||||
const result = await run(
|
||||
"resolve tree " + ref,
|
||||
"git rev-parse --verify " + q(ref + "^{tree}"),
|
||||
{ max_output_tokens: 4000 }
|
||||
);
|
||||
return ok(result) ? trim(result.output) : "";
|
||||
}
|
||||
|
||||
async function listPatchBranches() {
|
||||
const refRoot = "refs/heads/" + patchPrefix.replace(/\/+$/, "");
|
||||
const result = await run(
|
||||
"list patch branches",
|
||||
"git for-each-ref --format='%(refname:short)%09%(objectname)%09%(contents:subject)' " + q(refRoot),
|
||||
{ max_output_tokens: 20000, textLimit: 16000 }
|
||||
);
|
||||
if (!ok(result)) {
|
||||
finish("failed", "Could not list patch branches.", { patchListOutput: result.output });
|
||||
}
|
||||
const patches = trim(result.output)
|
||||
.split(/\r?\n/)
|
||||
.map((line) => {
|
||||
const parts = line.split("\t");
|
||||
return {
|
||||
name: parts[0] || "",
|
||||
sha: parts[1] || "",
|
||||
subject: parts[2] || ""
|
||||
};
|
||||
})
|
||||
.filter((patch) => patch.name.startsWith(patchPrefix) && patch.sha)
|
||||
.sort((left, right) => left.name.localeCompare(right.name));
|
||||
if (patches.length === 0) {
|
||||
finish("blocked", "No patch branches were found for prefix " + patchPrefix + ".");
|
||||
}
|
||||
return patches;
|
||||
}
|
||||
|
||||
async function rebuildMainFromBase(baseRef, baseSha, beforeSha) {
|
||||
const beforeTree = beforeSha ? await resolveTree(targetBranch) : "";
|
||||
const patches = await listPatchBranches();
|
||||
const detach = await run("checkout rebuild base", "git switch --detach " + q(baseSha), {
|
||||
max_output_tokens: 12000
|
||||
});
|
||||
if (!ok(detach)) {
|
||||
finish("failed", "Could not switch to the rebuild base.", { checkoutOutput: detach.output });
|
||||
}
|
||||
|
||||
const applied = [];
|
||||
for (const patch of patches) {
|
||||
const pick = await run("apply " + patch.name, "git cherry-pick " + q(patch.sha), {
|
||||
max_output_tokens: 30000,
|
||||
textLimit: 20000
|
||||
});
|
||||
if (!ok(pick)) {
|
||||
const context = await collectConflictContext({
|
||||
beforeSha,
|
||||
baseRef,
|
||||
baseSha,
|
||||
rebuildOutput: pick.output,
|
||||
failedPatch: patch,
|
||||
applied
|
||||
});
|
||||
finish("needs_intervention", "Patch workspace rebuild paused on " + patch.name + ".", context);
|
||||
}
|
||||
applied.push(patch);
|
||||
}
|
||||
|
||||
const afterHead = await run("rebuilt head", "git rev-parse HEAD", { max_output_tokens: 4000 });
|
||||
const afterSha = trim(afterHead.output);
|
||||
const afterTree = await resolveTree("HEAD");
|
||||
|
||||
if (beforeTree && afterTree && beforeTree === afterTree) {
|
||||
const restore = await run("restore target branch", "git switch " + q(targetBranch), { max_output_tokens: 12000 });
|
||||
if (!ok(restore)) {
|
||||
finish("failed", "Rebuilt tree matched target, but switching back failed.", { restoreOutput: restore.output });
|
||||
}
|
||||
return {
|
||||
changed: false,
|
||||
beforeSha,
|
||||
afterSha: beforeSha,
|
||||
rebuiltSha: afterSha,
|
||||
applied
|
||||
};
|
||||
}
|
||||
|
||||
const update = await run("update target branch", "git branch -f " + q(targetBranch) + " " + q(afterSha), {
|
||||
max_output_tokens: 12000
|
||||
});
|
||||
if (!ok(update)) {
|
||||
finish("failed", "Could not update target branch after rebuild.", { updateOutput: update.output });
|
||||
}
|
||||
const target = await run("switch target branch", "git switch " + q(targetBranch), { max_output_tokens: 12000 });
|
||||
if (!ok(target)) {
|
||||
finish("failed", "Could not switch to target branch after rebuild.", { switchOutput: target.output });
|
||||
}
|
||||
|
||||
return {
|
||||
changed: beforeSha !== afterSha,
|
||||
beforeSha,
|
||||
afterSha,
|
||||
rebuiltSha: afterSha,
|
||||
applied
|
||||
};
|
||||
}
|
||||
|
||||
async function verifyBranchUpdateCandidate(rebuild) {
|
||||
const diffCheck = await run("codex diff whitespace check", "git diff --check", { max_output_tokens: 12000 });
|
||||
if (!ok(diffCheck)) {
|
||||
finish("failed", "Codex git diff --check failed after branch update.", {
|
||||
beforeSha: rebuild.beforeSha,
|
||||
afterSha: rebuild.afterSha,
|
||||
diffCheckOutput: diffCheck.output
|
||||
});
|
||||
}
|
||||
|
||||
const cargoCheck = await run(
|
||||
"cargo check code mode packages",
|
||||
"CARGO_TARGET_DIR=" + q(cargoTargetDir) + " cargo check -p codex-app-server -p codex-core -p codex-app-server-protocol",
|
||||
{ workdir: codexRustDir, max_output_tokens: 30000, textLimit: 20000 }
|
||||
);
|
||||
if (!ok(cargoCheck)) {
|
||||
finish("failed", "Cargo check failed after branch update.", {
|
||||
beforeSha: rebuild.beforeSha,
|
||||
afterSha: rebuild.afterSha,
|
||||
cargoCheckOutput: cargoCheck.output
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyReleaseCandidate(rebuild) {
|
||||
const cargoVersion = await run(
|
||||
"validate Cargo.toml version",
|
||||
"grep -m1 '^version' " + q(codexRustDir + "/Cargo.toml") + " | sed -E 's/version *= *\"([^\"]+)\".*/\\1/'",
|
||||
{ max_output_tokens: 4000 }
|
||||
);
|
||||
if (!ok(cargoVersion) || trim(cargoVersion.output) !== version) {
|
||||
finish("failed", "Release tag does not match codex-rs/Cargo.toml version.", {
|
||||
releaseTag,
|
||||
version,
|
||||
cargoVersion: trim(cargoVersion.output)
|
||||
});
|
||||
}
|
||||
|
||||
const build = await run(
|
||||
"build fork binary",
|
||||
"CARGO_TARGET_DIR=" + q(cargoTargetDir) + " cargo build -p codex-cli --bin codex",
|
||||
{ workdir: codexRustDir, max_output_tokens: 30000, textLimit: 20000 }
|
||||
);
|
||||
if (!ok(build)) {
|
||||
finish("failed", "Fork binary build failed.", {
|
||||
beforeSha: rebuild.beforeSha,
|
||||
afterSha: rebuild.afterSha,
|
||||
buildOutput: build.output
|
||||
});
|
||||
}
|
||||
|
||||
const versionCheck = await run("verify fork binary", q(codexBinary) + " --version", { max_output_tokens: 4000 });
|
||||
if (!ok(versionCheck)) {
|
||||
finish("failed", "Built fork binary did not run.", {
|
||||
beforeSha: rebuild.beforeSha,
|
||||
afterSha: rebuild.afterSha,
|
||||
versionOutput: versionCheck.output
|
||||
});
|
||||
}
|
||||
|
||||
const cargoCheck = await run(
|
||||
"cargo check code mode packages",
|
||||
"CARGO_TARGET_DIR=" + q(cargoTargetDir) + " cargo check -p codex-app-server -p codex-core -p codex-app-server-protocol",
|
||||
{ workdir: codexRustDir, max_output_tokens: 30000, textLimit: 20000 }
|
||||
);
|
||||
if (!ok(cargoCheck)) {
|
||||
finish("failed", "Cargo check failed after release rebuild.", {
|
||||
beforeSha: rebuild.beforeSha,
|
||||
afterSha: rebuild.afterSha,
|
||||
cargoCheckOutput: cargoCheck.output
|
||||
});
|
||||
}
|
||||
|
||||
const protocolTest = await run(
|
||||
"protocol code mode execute test",
|
||||
"CARGO_TARGET_DIR=" + q(cargoTargetDir) + " cargo test -p codex-app-server-protocol thread_code_mode_execute -- --nocapture",
|
||||
{ workdir: codexRustDir, max_output_tokens: 30000, textLimit: 20000 }
|
||||
);
|
||||
if (!ok(protocolTest)) {
|
||||
finish("failed", "Protocol Code Mode API test failed after release rebuild.", {
|
||||
beforeSha: rebuild.beforeSha,
|
||||
afterSha: rebuild.afterSha,
|
||||
protocolTestOutput: protocolTest.output
|
||||
});
|
||||
}
|
||||
|
||||
const fmt = await run("cargo fmt check", "cargo fmt --check", {
|
||||
workdir: codexRustDir,
|
||||
max_output_tokens: 20000
|
||||
});
|
||||
if (!ok(fmt)) {
|
||||
finish("failed", "Cargo fmt --check failed after release rebuild.", {
|
||||
beforeSha: rebuild.beforeSha,
|
||||
afterSha: rebuild.afterSha,
|
||||
fmtOutput: fmt.output
|
||||
});
|
||||
}
|
||||
|
||||
const diffCheck = await run("codex diff whitespace check", "git diff --check", { max_output_tokens: 12000 });
|
||||
if (!ok(diffCheck)) {
|
||||
finish("failed", "Codex git diff --check failed after release rebuild.", {
|
||||
beforeSha: rebuild.beforeSha,
|
||||
afterSha: rebuild.afterSha,
|
||||
diffCheckOutput: diffCheck.output
|
||||
});
|
||||
}
|
||||
|
||||
const artifacts = {
|
||||
codexBinary,
|
||||
codexVersion: trim(versionCheck.output)
|
||||
};
|
||||
if (enabled("stage_npm_wrapper", true)) {
|
||||
Object.assign(artifacts, await stageLocalNpmWrapper());
|
||||
}
|
||||
return artifacts;
|
||||
}
|
||||
|
||||
async function detectLocalTargetTriple() {
|
||||
const target = await run(
|
||||
"detect local npm target triple",
|
||||
[
|
||||
"case \"$(uname -s):$(uname -m)\" in",
|
||||
" Linux:x86_64) printf x86_64-unknown-linux-musl ;;",
|
||||
" Linux:aarch64|Linux:arm64) printf aarch64-unknown-linux-musl ;;",
|
||||
" Darwin:x86_64) printf x86_64-apple-darwin ;;",
|
||||
" Darwin:arm64) printf aarch64-apple-darwin ;;",
|
||||
" *) exit 1 ;;",
|
||||
"esac"
|
||||
].join("\n"),
|
||||
{ max_output_tokens: 4000 }
|
||||
);
|
||||
if (!ok(target) || !trim(target.output)) {
|
||||
finish("failed", "Could not infer local target triple for npm wrapper staging.", {
|
||||
targetOutput: target.output
|
||||
});
|
||||
}
|
||||
return trim(target.output);
|
||||
}
|
||||
|
||||
async function stageLocalNpmWrapper() {
|
||||
const targetTriple = await detectLocalTargetTriple();
|
||||
const binaryName = targetTriple.includes("windows") ? "codex.exe" : "codex";
|
||||
const stage = await run(
|
||||
"stage local npm wrapper",
|
||||
[
|
||||
"set -euo pipefail",
|
||||
"stage_root=$(mktemp -d /tmp/peezy-codex-npm-stage.XXXXXX)",
|
||||
"package_dir=\"$stage_root/package\"",
|
||||
"python3 " + q(codexRepo + "/codex-cli/scripts/build_npm_package.py") + " --package codex --version " + q(version) + " --staging-dir \"$package_dir\"",
|
||||
"mkdir -p \"$package_dir/vendor/" + targetTriple + "/codex\"",
|
||||
"cp " + q(codexBinary) + " \"$package_dir/vendor/" + targetTriple + "/codex/" + binaryName + "\"",
|
||||
"chmod 0755 \"$package_dir/vendor/" + targetTriple + "/codex/" + binaryName + "\"",
|
||||
"node \"$package_dir/bin/codex.js\" --version",
|
||||
"printf '\\nSTAGED_PACKAGE=%s\\n' \"$package_dir\""
|
||||
].join("\n"),
|
||||
{ max_output_tokens: 20000, textLimit: 16000 }
|
||||
);
|
||||
if (!ok(stage)) {
|
||||
finish("failed", "Local npm wrapper staging failed.", { stageOutput: stage.output });
|
||||
}
|
||||
const match = stage.output.match(/STAGED_PACKAGE=(.+)/);
|
||||
const stagedPackage = match ? trim(match[1]) : "";
|
||||
if (enabled("link_local_package", false) && stagedPackage) {
|
||||
const link = await run("link local npm wrapper with Bun", "bun pm link", {
|
||||
workdir: stagedPackage,
|
||||
max_output_tokens: 12000
|
||||
});
|
||||
if (!ok(link)) {
|
||||
finish("failed", "Bun link of local Codex package failed.", { linkOutput: link.output, stagedPackage });
|
||||
}
|
||||
}
|
||||
return {
|
||||
targetTriple,
|
||||
stagedPackage,
|
||||
linked: enabled("link_local_package", false)
|
||||
};
|
||||
}
|
||||
|
||||
async function maybePushTargetBranch(afterSha) {
|
||||
if (!enabled("push", false)) {
|
||||
return false;
|
||||
}
|
||||
const push = await run("push fork branch", "git push origin HEAD:" + q(targetBranch) + " --force-with-lease", {
|
||||
max_output_tokens: 20000
|
||||
});
|
||||
if (!ok(push)) {
|
||||
finish("failed", "Could not push rebuilt fork branch.", { pushOutput: push.output });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function maybePublishReleaseTag() {
|
||||
if (!enabled("publish", false)) {
|
||||
return false;
|
||||
}
|
||||
const tagName = "rust-v" + version;
|
||||
const existing = await run("check release tag", "git rev-parse --verify " + q("refs/tags/" + tagName), {
|
||||
max_output_tokens: 4000
|
||||
});
|
||||
if (existing.exit_code !== 0) {
|
||||
const tag = await run("create release tag", "git tag -a " + q(tagName) + " -m " + q("Release " + version), {
|
||||
max_output_tokens: 12000
|
||||
});
|
||||
if (!ok(tag)) {
|
||||
finish("failed", "Could not create release tag.", { tagOutput: tag.output });
|
||||
}
|
||||
}
|
||||
const pushTag = await run("push release tag", "git push origin " + q(tagName), {
|
||||
max_output_tokens: 20000
|
||||
});
|
||||
if (!ok(pushTag)) {
|
||||
finish("failed", "Could not push release tag.", { pushTagOutput: pushTag.output });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function candidateRef(afterSha, pushed) {
|
||||
return {
|
||||
kind: "branch",
|
||||
repo: "peezy-tech/codex",
|
||||
remote: pushed ? "origin" : "local",
|
||||
ref: "refs/heads/" + targetBranch,
|
||||
sha: afterSha,
|
||||
pushed
|
||||
};
|
||||
}
|
||||
|
||||
const releaseTag = String(payload.tag || "");
|
||||
const version = versionFromTag(releaseTag);
|
||||
const flowFlagOverrides = {
|
||||
force: await envFlag("CODEX_FLOW_FORCE"),
|
||||
push: await envFlag("CODEX_FLOW_PUSH"),
|
||||
publish: await envFlag("CODEX_FLOW_PUBLISH"),
|
||||
squash_patch_stack: await envFlag("CODEX_FLOW_SQUASH_PATCH_STACK")
|
||||
stage_npm_wrapper: await envFlag("CODEX_FLOW_STAGE_NPM_WRAPPER"),
|
||||
link_local_package: await envFlag("CODEX_FLOW_LINK_LOCAL_PACKAGE")
|
||||
};
|
||||
const releaseTag = eventType === "upstream.release" ? String(payload.tag || "") : "";
|
||||
const version = releaseTag ? versionFromTag(releaseTag) : "";
|
||||
const operation = eventType === "upstream.release" ? "release-cycle" : "main-branch-update";
|
||||
const packageName = cfg("package_name", "@peezy.tech/codex");
|
||||
const targetBranch = (await env(cfg("target_branch_env", ""))) || cfg("target_branch", "main");
|
||||
const upstreamBranch = cfg("upstream_branch", "upstream");
|
||||
const patchPrefix = cfg("patch_prefix", "patch/");
|
||||
const upstreamRemote = cfg("upstream_remote", "upstream");
|
||||
const upstreamMainRef = cfg("upstream_main_ref", "main");
|
||||
const upstreamRepoUrl = cfg("upstream_repo_url", "https://github.com/openai/codex.git");
|
||||
const cargoTargetDir = (await env(cfg("cargo_target_dir_env", ""))) || cfg("cargo_target_dir", "/tmp/peezy-codex-flow-target");
|
||||
const codexRepo = (await env(cfg("codex_repo_env", ""))) || cfg("codex_repo", "");
|
||||
const codexRustDir = codexRepo + "/codex-rs";
|
||||
const codexBinary = cargoTargetDir + "/debug/codex";
|
||||
|
||||
if (!releaseTag) {
|
||||
if (eventType !== "upstream.release" && eventType !== "upstream.branch_update") {
|
||||
finish("failed", "Unsupported event type " + eventType + ".");
|
||||
}
|
||||
if (String(payload.repo || "") !== "openai/codex") {
|
||||
finish("skipped", "Ignoring upstream event for " + String(payload.repo || "unknown") + ".");
|
||||
}
|
||||
if (eventType === "upstream.release" && !releaseTag) {
|
||||
finish("failed", "Release payload is missing tag.");
|
||||
}
|
||||
if (!version) {
|
||||
finish("failed", "Could not infer semantic version from release tag " + releaseTag);
|
||||
if (eventType === "upstream.release" && !version) {
|
||||
finish("failed", "Could not infer semantic version from release tag " + releaseTag + ".");
|
||||
}
|
||||
if (!codexRepo) {
|
||||
finish("blocked", "No Codex fork checkout configured. Set codex_repo or codex_repo_env in flow.toml.");
|
||||
|
|
@ -163,230 +649,76 @@ if (!codexRepo) {
|
|||
text([
|
||||
"Peezy Codex fork update flow",
|
||||
"",
|
||||
"Release: " + releaseTag,
|
||||
"Version: " + version,
|
||||
"Event type: " + eventType,
|
||||
"Operation: " + operation,
|
||||
releaseTag ? "Release: " + releaseTag : "Upstream ref: " + String(payload.ref || "refs/heads/" + upstreamMainRef),
|
||||
version ? "Version: " + version : undefined,
|
||||
"Target branch: " + targetBranch,
|
||||
"Upstream branch: " + upstreamBranch,
|
||||
"Patch prefix: " + patchPrefix,
|
||||
"Codex repo: " + codexRepo,
|
||||
"Upstream remote: " + upstreamRemote + " -> " + upstreamRepoUrl,
|
||||
"Cargo target dir: " + cargoTargetDir
|
||||
].join("\n") + "\n");
|
||||
].filter(Boolean).join("\n") + "\n");
|
||||
|
||||
const published = await run("published fork package check", "npm view " + q(packageName + "@" + version) + " version --json", {
|
||||
max_output_tokens: 4000
|
||||
});
|
||||
if (ok(published) && !enabled("force", false)) {
|
||||
finish("skipped", packageName + "@" + version + " is already published.");
|
||||
if (eventType === "upstream.release") {
|
||||
const published = await run("published fork package check", "npm view " + q(packageName + "@" + version) + " version --json", {
|
||||
max_output_tokens: 4000
|
||||
});
|
||||
if (ok(published) && !enabled("force", false)) {
|
||||
finish("skipped", packageName + "@" + version + " is already published.");
|
||||
}
|
||||
}
|
||||
|
||||
const repoCheck = await run("verify codex repo", "git rev-parse --show-toplevel");
|
||||
if (!ok(repoCheck)) {
|
||||
finish("failed", "codex repo is not a git checkout", { repoCheck: repoCheck.output });
|
||||
finish("failed", "Codex repo is not a git checkout.", { repoCheck: repoCheck.output });
|
||||
}
|
||||
|
||||
const rustWorkspaceCheck = await run("verify codex Rust workspace", "test -f " + q(codexRustDir + "/Cargo.toml"), {
|
||||
max_output_tokens: 4000
|
||||
});
|
||||
if (!ok(rustWorkspaceCheck)) {
|
||||
finish("failed", "codex Rust workspace was not found at the expected codex-rs path.", {
|
||||
finish("failed", "Codex Rust workspace was not found at the expected codex-rs path.", {
|
||||
codexRustDir,
|
||||
rustWorkspaceCheck: rustWorkspaceCheck.output
|
||||
});
|
||||
}
|
||||
|
||||
const existingRebase = await run(
|
||||
"check existing rebase state",
|
||||
"test -d \"$(git rev-parse --git-path rebase-merge)\" -o -d \"$(git rev-parse --git-path rebase-apply)\"",
|
||||
{ max_output_tokens: 4000 }
|
||||
);
|
||||
if (existingRebase.exit_code === 0) {
|
||||
const context = await collectRebaseContext("A rebase was already in progress before this flow started.", undefined);
|
||||
finish("blocked", "A rebase is already in progress in the Codex checkout.", context);
|
||||
}
|
||||
|
||||
await run("codex status before update", "git status --short --branch", { max_output_tokens: 12000 });
|
||||
const branch = await run("current branch", "git rev-parse --abbrev-ref HEAD", { max_output_tokens: 4000 });
|
||||
if (!ok(branch)) {
|
||||
finish("failed", "could not read current branch", { branchOutput: branch.output });
|
||||
}
|
||||
|
||||
if (trim(branch.output) !== targetBranch) {
|
||||
const dirtyBeforeSwitch = await run("dirty check before branch switch", "git status --porcelain=v1", { max_output_tokens: 12000 });
|
||||
if (trim(dirtyBeforeSwitch.output)) {
|
||||
finish("blocked", "codex checkout has local changes before switching branches.", {
|
||||
dirtyStatus: dirtyBeforeSwitch.output
|
||||
});
|
||||
}
|
||||
const switched = await run("switch target branch", "git switch " + q(targetBranch), { max_output_tokens: 12000 });
|
||||
if (!ok(switched)) {
|
||||
finish("failed", "could not switch to target branch", { switchOutput: switched.output });
|
||||
}
|
||||
}
|
||||
|
||||
const dirty = await run("dirty check on target branch", "git status --porcelain=v1", { max_output_tokens: 12000 });
|
||||
if (trim(dirty.output)) {
|
||||
finish("blocked", "codex target branch has local changes. Resolve or stash them before updating.", {
|
||||
dirtyStatus: dirty.output
|
||||
});
|
||||
}
|
||||
|
||||
const remote = await run(
|
||||
"ensure upstream openai/codex remote",
|
||||
"git remote get-url " + q(upstreamRemote) + " >/dev/null 2>&1 && git remote set-url " + q(upstreamRemote) + " " + q(upstreamRepoUrl) + " || git remote add " + q(upstreamRemote) + " " + q(upstreamRepoUrl),
|
||||
{ max_output_tokens: 12000 }
|
||||
);
|
||||
if (!ok(remote)) {
|
||||
finish("failed", "could not configure upstream remote", { remoteOutput: remote.output });
|
||||
}
|
||||
|
||||
const fetch = await run("fetch upstream tags", "git fetch " + q(upstreamRemote) + " --tags --prune", {
|
||||
max_output_tokens: 20000
|
||||
});
|
||||
if (!ok(fetch)) {
|
||||
finish("failed", "could not fetch upstream release tags", { fetchOutput: fetch.output });
|
||||
}
|
||||
|
||||
const releaseCommit = await run("resolve release tag", "git rev-parse --verify " + q("refs/tags/" + releaseTag + "^{commit}"), {
|
||||
max_output_tokens: 4000
|
||||
});
|
||||
if (!ok(releaseCommit)) {
|
||||
finish("failed", "could not resolve upstream release tag after fetch", {
|
||||
releaseTag,
|
||||
resolveOutput: releaseCommit.output
|
||||
});
|
||||
}
|
||||
|
||||
const beforeHead = await run("codex head before rebase", "git rev-parse HEAD", { max_output_tokens: 4000 });
|
||||
const rebase = await run("rebase target branch onto upstream release", "git rebase " + q(releaseTag), {
|
||||
max_output_tokens: 30000,
|
||||
textLimit: 20000
|
||||
});
|
||||
if (!ok(rebase)) {
|
||||
const context = await collectRebaseContext(rebase.output, trim(beforeHead.output));
|
||||
finish("needs_intervention", "Rebase paused with conflicts.", context);
|
||||
}
|
||||
|
||||
if (enabled("squash_patch_stack", true)) {
|
||||
const count = await run("count fork patch commits", "git rev-list --count " + q(releaseTag) + "..HEAD", {
|
||||
max_output_tokens: 4000
|
||||
});
|
||||
const commitCount = Number(trim(count.output));
|
||||
if (Number.isFinite(commitCount) && commitCount > 1) {
|
||||
const reset = await run("squash patch stack reset", "git reset --soft " + q(releaseTag), { max_output_tokens: 12000 });
|
||||
if (!ok(reset)) {
|
||||
finish("failed", "could not soft reset patch stack for squashing", { resetOutput: reset.output });
|
||||
}
|
||||
const commit = await run("squash patch stack commit", "git commit -m " + q("peezy: codex fork patches for " + releaseTag), {
|
||||
max_output_tokens: 20000
|
||||
});
|
||||
if (!ok(commit)) {
|
||||
finish("failed", "could not commit squashed patch stack", { commitOutput: commit.output });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const afterHead = await run("codex head after rebase", "git rev-parse HEAD", { max_output_tokens: 4000 });
|
||||
await run("codex status after rebase", "git status --short --branch", { max_output_tokens: 12000 });
|
||||
|
||||
const build = await run(
|
||||
"build fork binary",
|
||||
"CARGO_TARGET_DIR=" + q(cargoTargetDir) + " cargo build -p codex-cli --bin codex",
|
||||
{ workdir: codexRustDir, max_output_tokens: 30000, textLimit: 20000 }
|
||||
);
|
||||
if (!ok(build)) {
|
||||
finish("failed", "fork binary build failed", {
|
||||
beforeSha: trim(beforeHead.output),
|
||||
afterSha: trim(afterHead.output),
|
||||
buildOutput: build.output
|
||||
});
|
||||
}
|
||||
|
||||
const versionCheck = await run("verify fork binary", q(codexBinary) + " --version", { max_output_tokens: 4000 });
|
||||
if (!ok(versionCheck)) {
|
||||
finish("failed", "built fork binary did not run", {
|
||||
beforeSha: trim(beforeHead.output),
|
||||
afterSha: trim(afterHead.output),
|
||||
versionOutput: versionCheck.output
|
||||
});
|
||||
}
|
||||
|
||||
const cargoCheck = await run(
|
||||
"cargo check code mode packages",
|
||||
"CARGO_TARGET_DIR=" + q(cargoTargetDir) + " cargo check -p codex-app-server -p codex-core -p codex-app-server-protocol",
|
||||
{ workdir: codexRustDir, max_output_tokens: 30000, textLimit: 20000 }
|
||||
);
|
||||
if (!ok(cargoCheck)) {
|
||||
finish("failed", "cargo check failed after rebase", {
|
||||
beforeSha: trim(beforeHead.output),
|
||||
afterSha: trim(afterHead.output),
|
||||
cargoCheckOutput: cargoCheck.output
|
||||
});
|
||||
}
|
||||
|
||||
const protocolTest = await run(
|
||||
"protocol code mode execute test",
|
||||
"CARGO_TARGET_DIR=" + q(cargoTargetDir) + " cargo test -p codex-app-server-protocol thread_code_mode_execute -- --nocapture",
|
||||
{ workdir: codexRustDir, max_output_tokens: 30000, textLimit: 20000 }
|
||||
);
|
||||
if (!ok(protocolTest)) {
|
||||
finish("failed", "protocol Code Mode API test failed after rebase", {
|
||||
beforeSha: trim(beforeHead.output),
|
||||
afterSha: trim(afterHead.output),
|
||||
protocolTestOutput: protocolTest.output
|
||||
});
|
||||
}
|
||||
|
||||
const fmt = await run("cargo fmt check", "cargo fmt --check", {
|
||||
workdir: codexRustDir,
|
||||
max_output_tokens: 20000
|
||||
});
|
||||
if (!ok(fmt)) {
|
||||
finish("failed", "cargo fmt --check failed after rebase", {
|
||||
beforeSha: trim(beforeHead.output),
|
||||
afterSha: trim(afterHead.output),
|
||||
fmtOutput: fmt.output
|
||||
});
|
||||
}
|
||||
|
||||
const diffCheck = await run("codex diff whitespace check", "git diff --check", { max_output_tokens: 12000 });
|
||||
if (!ok(diffCheck)) {
|
||||
finish("failed", "codex git diff --check failed after rebase", {
|
||||
beforeSha: trim(beforeHead.output),
|
||||
afterSha: trim(afterHead.output),
|
||||
diffCheckOutput: diffCheck.output
|
||||
});
|
||||
}
|
||||
|
||||
if (enabled("push", false)) {
|
||||
const push = await run("push fork branch", "git push origin HEAD:" + q(targetBranch) + " --force-with-lease", {
|
||||
max_output_tokens: 20000
|
||||
});
|
||||
if (!ok(push)) {
|
||||
finish("failed", "could not push rebased fork branch", { pushOutput: push.output });
|
||||
}
|
||||
}
|
||||
|
||||
if (enabled("publish", false)) {
|
||||
const tagCommand = "git tag -a " + q("rust-v" + version) + " -m " + q("Release " + version);
|
||||
const tag = await run("create release tag", tagCommand, { max_output_tokens: 12000 });
|
||||
if (!ok(tag)) {
|
||||
finish("failed", "could not create release tag", { tagOutput: tag.output });
|
||||
}
|
||||
const pushTag = await run("push release tag", "git push origin " + q("rust-v" + version), {
|
||||
max_output_tokens: 20000
|
||||
});
|
||||
if (!ok(pushTag)) {
|
||||
finish("failed", "could not push release tag", { pushTagOutput: pushTag.output });
|
||||
}
|
||||
}
|
||||
await requireCleanWorktree();
|
||||
await requireNoPausedGitOperation();
|
||||
await ensureUpstreamRemote();
|
||||
const upstreamMainSha = await fetchUpstreamMainAndTags();
|
||||
const baseSha = eventType === "upstream.release" ? await resolveReleaseBase() : upstreamMainSha;
|
||||
const baseRef = eventType === "upstream.release" ? releaseTag : upstreamBranch;
|
||||
const beforeSha = await resolveCommit(targetBranch);
|
||||
const rebuild = await rebuildMainFromBase(baseRef, baseSha, beforeSha);
|
||||
|
||||
const verificationArtifacts = eventType === "upstream.release"
|
||||
? await verifyReleaseCandidate(rebuild)
|
||||
: await verifyBranchUpdateCandidate(rebuild).then(() => ({}));
|
||||
const pushed = await maybePushTargetBranch(rebuild.afterSha);
|
||||
const published = eventType === "upstream.release" ? await maybePublishReleaseTag() : false;
|
||||
const finalStatus = await run("final codex status", "git status --short --branch", { max_output_tokens: 12000 });
|
||||
finish("changed", "Peezy Codex fork rebased onto upstream release and verified.", {
|
||||
beforeSha: trim(beforeHead.output),
|
||||
afterSha: trim(afterHead.output),
|
||||
codexHead: trim(afterHead.output),
|
||||
codexBinary,
|
||||
codexVersion: trim(versionCheck.output),
|
||||
finalStatus: finalStatus.output,
|
||||
pushed: enabled("push", false),
|
||||
published: enabled("publish", false)
|
||||
});
|
||||
const status = rebuild.changed ? "changed" : "completed";
|
||||
finish(
|
||||
status,
|
||||
eventType === "upstream.release"
|
||||
? "Peezy Codex fork rebuilt from upstream release and verified."
|
||||
: "Peezy Codex fork main rebuilt from upstream/main and verified.",
|
||||
{
|
||||
beforeSha: rebuild.beforeSha,
|
||||
afterSha: rebuild.afterSha,
|
||||
rebuiltSha: rebuild.rebuiltSha,
|
||||
baseRef,
|
||||
baseSha,
|
||||
upstreamMainSha,
|
||||
applied: rebuild.applied,
|
||||
finalStatus: finalStatus.output,
|
||||
pushed,
|
||||
published,
|
||||
candidateRefs: [candidateRef(rebuild.afterSha, pushed)],
|
||||
...verificationArtifacts
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,26 +1,30 @@
|
|||
name = "peezy-codex-fork"
|
||||
version = 1
|
||||
description = "Rebase the Peezy Codex fork patch stack onto a canonical openai/codex release."
|
||||
description = "Maintain the Peezy Codex fork patch workspace from openai/codex releases and main updates."
|
||||
|
||||
[config]
|
||||
package_name = "@peezy.tech/codex"
|
||||
codex_repo_env = "PEEZY_CODEX_REPO"
|
||||
codex_repo = "/home/peezy/meta-workspace/codex"
|
||||
target_branch_env = "PEEZY_CODEX_TARGET_BRANCH"
|
||||
target_branch = "code-mode-exec-hooks"
|
||||
target_branch = "main"
|
||||
upstream_branch = "upstream"
|
||||
patch_prefix = "patch/"
|
||||
upstream_remote = "upstream"
|
||||
upstream_main_ref = "main"
|
||||
upstream_repo_url = "https://github.com/openai/codex.git"
|
||||
cargo_target_dir_env = "PEEZY_CODEX_CARGO_TARGET_DIR"
|
||||
cargo_target_dir = "/tmp/peezy-codex-flow-target"
|
||||
squash_patch_stack = true
|
||||
stage_npm_wrapper = true
|
||||
link_local_package = false
|
||||
push = false
|
||||
publish = false
|
||||
|
||||
[guidance]
|
||||
skills = ["jojo-development-flow"]
|
||||
skills = ["jojo-development-flow", "code-mode-flow-author"]
|
||||
|
||||
[[steps]]
|
||||
name = "rebase-patch-stack"
|
||||
name = "release-cycle"
|
||||
runner = "code-mode"
|
||||
script = "exec/update-fork.code-mode.js"
|
||||
timeout_ms = 3600000
|
||||
|
|
@ -28,3 +32,13 @@ timeout_ms = 3600000
|
|||
[steps.trigger]
|
||||
type = "upstream.release"
|
||||
schema = "schemas/upstream-release.schema.json"
|
||||
|
||||
[[steps]]
|
||||
name = "main-branch-update"
|
||||
runner = "code-mode"
|
||||
script = "exec/update-fork.code-mode.js"
|
||||
timeout_ms = 3600000
|
||||
|
||||
[steps.trigger]
|
||||
type = "upstream.branch_update"
|
||||
schema = "schemas/upstream-branch-update.schema.json"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"type": "object",
|
||||
"required": ["repo", "ref"],
|
||||
"properties": {
|
||||
"provider": { "type": "string" },
|
||||
"repo": { "type": "string", "enum": ["openai/codex"] },
|
||||
"repoOwner": { "type": "string" },
|
||||
"repoName": { "type": "string" },
|
||||
"ref": { "type": "string" },
|
||||
"sha": { "type": "string" },
|
||||
"url": { "type": "string" },
|
||||
"publishedAt": { "type": "string" },
|
||||
"sourceId": { "type": "string" },
|
||||
"entryId": { "type": "string" }
|
||||
}
|
||||
}
|
||||
|
|
@ -7,12 +7,12 @@
|
|||
"source": {
|
||||
"input": "../codex-flows",
|
||||
"type": "local",
|
||||
"commit": "7083213cd6e8a3a3a175ca63529134f7807e2afe"
|
||||
"commit": "c3905c73feff72add4f88ba8ec5c11cb1921c386"
|
||||
},
|
||||
"sourcePath": "flows/openai-codex-bindings",
|
||||
"destinationPath": ".codex/flows/openai-codex-bindings",
|
||||
"contentHash": "sha256:5fcb04e9525e94c24ca319fcefc99fd05ed973e9c640a5e27629e263f13ca968",
|
||||
"installedAt": "2026-05-18T18:57:28.444Z"
|
||||
"contentHash": "sha256:6bfd8118be031c145813dcacbf47f6939b2460aeeb0d8959075e6018297d0403",
|
||||
"installedAt": "2026-05-18T19:14:31.421Z"
|
||||
},
|
||||
{
|
||||
"name": "peezy-codex-fork",
|
||||
|
|
@ -20,12 +20,12 @@
|
|||
"source": {
|
||||
"input": "../codex-flows",
|
||||
"type": "local",
|
||||
"commit": "7083213cd6e8a3a3a175ca63529134f7807e2afe"
|
||||
"commit": "c3905c73feff72add4f88ba8ec5c11cb1921c386"
|
||||
},
|
||||
"sourcePath": "flows/peezy-codex-fork",
|
||||
"destinationPath": ".codex/flows/peezy-codex-fork",
|
||||
"contentHash": "sha256:521e7766cd1888fd54fc0c10a6d2a3a7f235d9a7c8aa2577b4f4681e3a8fdabe",
|
||||
"installedAt": "2026-05-18T18:57:28.444Z"
|
||||
"contentHash": "sha256:613f0733ebea22b191ed6e706ad09e05a2dc97aad16cdbe14db01065a1b06582",
|
||||
"installedAt": "2026-05-18T19:14:31.421Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,10 +53,15 @@
|
|||
"defaultBranch": "main"
|
||||
},
|
||||
"target": {
|
||||
"provider": "github",
|
||||
"repoFullName": "peezy-tech/codex",
|
||||
"branch": "main",
|
||||
"mode": "notify_only"
|
||||
"mode": "workspace_flow",
|
||||
"eventType": "upstream.branch_update",
|
||||
"workspaceUrlEnv": "PATCH_WORKSPACE_BACKEND_URL",
|
||||
"workspaceSecretEnv": "PATCH_WORKSPACE_BACKEND_SECRET",
|
||||
"payload": {
|
||||
"provider": "github",
|
||||
"repo": "openai/codex",
|
||||
"ref": "refs/heads/main"
|
||||
}
|
||||
},
|
||||
"pollIntervalSeconds": 300
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { discoverFlows, matchingSteps, type FlowEvent as RuntimeFlowEvent } from
|
|||
import {
|
||||
dispatchWorkspaceEventDetailed,
|
||||
maintenanceAttemptForWorkspaceDispatch,
|
||||
patchUpstreamBranchUpdateEvent,
|
||||
patchUpstreamReleaseEvent,
|
||||
replayWorkspaceEventDetailed,
|
||||
type WorkspaceDispatchConfig,
|
||||
|
|
@ -53,6 +54,7 @@ Usage:
|
|||
patch.moi attempts [--event-id ID] [--status STATUS] [--limit N] [--data-dir DIR] [--json]
|
||||
patch.moi run harness [--event FILE] [--workspace-root DIR] [--data-dir DIR] [--dry-run] [--json]
|
||||
patch.moi run codex-release --tag TAG [--repo openai/codex] [--workspace-root DIR] [--data-dir DIR] [--dry-run] [--record-only] [--allow-local] [--json]
|
||||
patch.moi run codex-main [--sha SHA] [--repo openai/codex] [--ref refs/heads/main] [--workspace-root DIR] [--data-dir DIR] [--dry-run] [--record-only] [--allow-local] [--json]
|
||||
patch.moi run event --file FILE [--workspace-root DIR] [--data-dir DIR] [--dry-run] [--record-only] [--json]
|
||||
patch.moi patch doctor [--repo DIR] [--main BRANCH] [--upstream BRANCH] [--json]
|
||||
patch.moi patch list [--repo DIR] [--prefix patch/] [--json]
|
||||
|
|
@ -252,6 +254,19 @@ async function handleRun(positionals: string[], context: CliContext): Promise<nu
|
|||
}
|
||||
return await runEvent(event, context);
|
||||
}
|
||||
if (target === "codex-main") {
|
||||
const repo = flagValue(context.parsed, "repo") ?? "openai/codex";
|
||||
const ref = flagValue(context.parsed, "ref") ?? "refs/heads/main";
|
||||
const event = patchUpstreamBranchUpdateEvent({
|
||||
repo,
|
||||
ref,
|
||||
sha: flagValue(context.parsed, "sha"),
|
||||
});
|
||||
if (!flagBool(context.parsed, "dry-run") && !flagBool(context.parsed, "record-only")) {
|
||||
assertCodexDispatchAllowed(context, "codex-main");
|
||||
}
|
||||
return await runEvent(event, context);
|
||||
}
|
||||
if (target === "event") {
|
||||
const eventFile = flagValue(context.parsed, "file");
|
||||
if (!eventFile) {
|
||||
|
|
@ -431,7 +446,7 @@ async function handleSetup(positionals: string[], context: CliContext): Promise<
|
|||
const repoPath = resolvePath(context.workspaceRoot, flagValue(context.parsed, "repo") ?? context.env.PEEZY_CODEX_REPO ?? "../codex");
|
||||
const upstreamRemote = flagValue(context.parsed, "upstream-remote") ?? "upstream";
|
||||
const upstreamUrl = flagValue(context.parsed, "upstream-url") ?? "https://github.com/openai/codex.git";
|
||||
const targetBranch = flagValue(context.parsed, "target-branch") ?? "code-mode-exec-hooks";
|
||||
const targetBranch = flagValue(context.parsed, "target-branch") ?? "main";
|
||||
const apply = flagBool(context.parsed, "apply");
|
||||
const repo = await inspectGitRepo(repoPath, {
|
||||
upstreamRemote,
|
||||
|
|
@ -560,7 +575,7 @@ async function appendFlowEventIfMissing(store: EventStore, event: FlowEvent): Pr
|
|||
return true;
|
||||
}
|
||||
|
||||
function assertCodexDispatchAllowed(context: CliContext): void {
|
||||
function assertCodexDispatchAllowed(context: CliContext, command = "codex-release"): void {
|
||||
if (
|
||||
flagBool(context.parsed, "allow-local") ||
|
||||
workspaceBackendConfigured(context.env) ||
|
||||
|
|
@ -569,7 +584,7 @@ function assertCodexDispatchAllowed(context: CliContext): void {
|
|||
return;
|
||||
}
|
||||
throw new UsageError(
|
||||
"codex-release dispatch requires PATCH_WORKSPACE_BACKEND_URL, CODEX_WORKSPACE_MODE=actions, or --allow-local; use --dry-run to verify matching without executing release work",
|
||||
`${command} dispatch requires PATCH_WORKSPACE_BACKEND_URL, CODEX_WORKSPACE_MODE=actions, or --allow-local; use --dry-run to verify matching without executing maintenance work`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -111,6 +111,25 @@ export function patchUpstreamReleaseEvent(input: {
|
|||
};
|
||||
}
|
||||
|
||||
export function patchUpstreamBranchUpdateEvent(input: {
|
||||
repo: string;
|
||||
ref: string;
|
||||
sha?: string;
|
||||
receivedAt?: string;
|
||||
}): FlowEvent<Record<string, unknown>> {
|
||||
return {
|
||||
id: `${serviceSource}:upstream.branch_update:${input.repo}:${input.ref}${input.sha ? `:${input.sha}` : ""}`,
|
||||
type: "upstream.branch_update",
|
||||
source: serviceSource,
|
||||
receivedAt: input.receivedAt ?? new Date().toISOString(),
|
||||
payload: {
|
||||
repo: input.repo,
|
||||
ref: input.ref,
|
||||
...(input.sha ? { sha: input.sha } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function dispatchFlowEvent(
|
||||
event: FlowEvent,
|
||||
target: Partial<FeedWorkspaceFlowTarget> = {},
|
||||
|
|
|
|||
|
|
@ -84,10 +84,38 @@ describe("patch.moi CLI", () => {
|
|||
expect(dryRun.code).toBe(0);
|
||||
expect(JSON.parse(dryRun.stdout).matches).toEqual([
|
||||
{ flow: "openai-codex-bindings", step: "regenerate-bindings", runner: "bun" },
|
||||
{ flow: "peezy-codex-fork", step: "rebase-patch-stack", runner: "code-mode" },
|
||||
{ flow: "peezy-codex-fork", step: "release-cycle", runner: "code-mode" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("dry-runs Codex main branch update matching", async () => {
|
||||
const dryRun = await invoke([
|
||||
"run",
|
||||
"codex-main",
|
||||
"--sha",
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"--workspace-root",
|
||||
workspaceRoot,
|
||||
"--dry-run",
|
||||
"--json",
|
||||
]);
|
||||
|
||||
expect(dryRun.code).toBe(0);
|
||||
expect(JSON.parse(dryRun.stdout)).toMatchObject({
|
||||
event: {
|
||||
type: "upstream.branch_update",
|
||||
payload: {
|
||||
repo: "openai/codex",
|
||||
ref: "refs/heads/main",
|
||||
sha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
},
|
||||
},
|
||||
matches: [
|
||||
{ flow: "peezy-codex-fork", step: "main-branch-update", runner: "code-mode" },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("syncs a maintenance attempt from workspace run state", async () => {
|
||||
const dataDir = await mkdtemp(join(tmpdir(), "patch-cli-"));
|
||||
const store = new EventStore(dataDir);
|
||||
|
|
@ -155,7 +183,7 @@ describe("patch.moi CLI", () => {
|
|||
test("sets up the Codex upstream remote when explicitly applied", async () => {
|
||||
const repo = await mkdtemp(join(tmpdir(), "patch-cli-codex-"));
|
||||
await mkdir(repo, { recursive: true });
|
||||
await git(repo, ["init", "-b", "code-mode-exec-hooks"]);
|
||||
await git(repo, ["init", "-b", "main"]);
|
||||
await git(repo, ["remote", "add", "origin", "https://github.com/peezy-tech/codex"]);
|
||||
|
||||
const result = await invoke([
|
||||
|
|
@ -170,7 +198,7 @@ describe("patch.moi CLI", () => {
|
|||
expect(result.code).toBe(0);
|
||||
expect(JSON.parse(result.stdout)).toMatchObject({
|
||||
path: repo,
|
||||
branch: "code-mode-exec-hooks",
|
||||
branch: "main",
|
||||
upstream: "https://github.com/openai/codex.git",
|
||||
addedUpstream: true,
|
||||
clean: true,
|
||||
|
|
|
|||
|
|
@ -89,6 +89,10 @@ describe("feed watcher", () => {
|
|||
"github-openai-codex-main",
|
||||
"github-openai-codex-releases",
|
||||
]);
|
||||
expect(sources.find((item) => item.id === "github-openai-codex-main")?.target).toMatchObject({
|
||||
mode: "workspace_flow",
|
||||
eventType: "upstream.branch_update",
|
||||
});
|
||||
});
|
||||
|
||||
test("first poll primes state without emitting old entries", async () => {
|
||||
|
|
@ -210,6 +214,66 @@ describe("feed watcher", () => {
|
|||
});
|
||||
});
|
||||
|
||||
test("later main commit polls dispatch upstream branch update events", async () => {
|
||||
const dataDir = await mkdtemp(join(tmpdir(), "patch-feed-"));
|
||||
const sourcesPath = join(dataDir, "sources.json");
|
||||
const branchSource: FeedSourceConfig = {
|
||||
...source,
|
||||
target: {
|
||||
mode: "workspace_flow",
|
||||
eventType: "upstream.branch_update",
|
||||
workspaceUrlEnv: "WORKSPACE_URL",
|
||||
payload: {
|
||||
repo: "openai/codex",
|
||||
provider: "github",
|
||||
ref: "refs/heads/main",
|
||||
},
|
||||
},
|
||||
};
|
||||
await writeFile(sourcesPath, JSON.stringify({ sources: [branchSource] }), "utf8");
|
||||
await writeFile(join(dataDir, "feed-state.json"), JSON.stringify({
|
||||
"github-openai-codex-main": {
|
||||
lastSeenId: "tag:github.com,2008:Grit::Commit/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
lastCheckedAt: "2026-05-12T09:00:00.000Z",
|
||||
},
|
||||
}), "utf8");
|
||||
|
||||
let dispatchedBody = "";
|
||||
await pollFeedsOnce({
|
||||
dataDir,
|
||||
sourcesPath,
|
||||
discord: { enabled: false, notifyEvents: new Set(["push"]) },
|
||||
flowDispatch: {
|
||||
env: {
|
||||
WORKSPACE_URL: "https://workspace.example/events",
|
||||
},
|
||||
fetchImpl: async (_url, init) => {
|
||||
dispatchedBody = String(init.body);
|
||||
const eventId = JSON.parse(String(init.body ?? "{}")).id;
|
||||
return Response.json({ status: "accepted", eventId, runIds: [], matched: 1 }, { status: 202 });
|
||||
},
|
||||
},
|
||||
}, async () => {
|
||||
return new Response(atom, { status: 200 });
|
||||
});
|
||||
|
||||
const flowEventText = await readFile(join(dataDir, "flow-events.jsonl"), "utf8");
|
||||
const flowEvent = JSON.parse(flowEventText.trim()) as Record<string, any>;
|
||||
expect(flowEvent.type).toBe("upstream.branch_update");
|
||||
expect(flowEvent.payload.repo).toBe("openai/codex");
|
||||
expect(flowEvent.payload.ref).toBe("refs/heads/main");
|
||||
expect(flowEvent.payload.sha).toBe("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||
expect(JSON.parse(dispatchedBody).id).toBe(flowEvent.id);
|
||||
const attempt = JSON.parse((await readFile(join(dataDir, "maintenance-attempts.jsonl"), "utf8")).trim());
|
||||
expect(attempt).toMatchObject({
|
||||
eventType: "upstream.branch_update",
|
||||
status: "started",
|
||||
upstreamRepo: "openai/codex",
|
||||
upstreamRef: "refs/heads/main",
|
||||
upstreamSha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
});
|
||||
});
|
||||
|
||||
test("workspace dispatch uses default workspace backend env names", async () => {
|
||||
let dispatchedUrl = "";
|
||||
let dispatchedSignature = "";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { describe, expect, test } from "bun:test";
|
||||
import type { FlowRunView } from "@peezy.tech/codex-flows/flow-runtime/client";
|
||||
import {
|
||||
patchUpstreamBranchUpdateEvent,
|
||||
maintenanceAttemptForWorkspaceDispatch,
|
||||
maintenanceAttemptWithWorkspaceRuns,
|
||||
patchUpstreamReleaseEvent,
|
||||
|
|
@ -88,6 +89,25 @@ describe("maintenance attempt sync", () => {
|
|||
{ ref: "refs/heads/codex-candidate", sha: "def" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("Patch upstream branch helper creates deterministic product events", () => {
|
||||
expect(patchUpstreamBranchUpdateEvent({
|
||||
repo: "openai/codex",
|
||||
ref: "refs/heads/main",
|
||||
sha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
receivedAt: "2026-05-16T00:00:00.000Z",
|
||||
})).toEqual({
|
||||
id: "patch:upstream.branch_update:openai/codex:refs/heads/main:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
type: "upstream.branch_update",
|
||||
source: "patch",
|
||||
receivedAt: "2026-05-16T00:00:00.000Z",
|
||||
payload: {
|
||||
repo: "openai/codex",
|
||||
ref: "refs/heads/main",
|
||||
sha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function statusFor(statuses: string[]): string {
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ describe("patch.moi harness flow", () => {
|
|||
|
||||
expect(matches.map(({ flow, step }) => `${flow.manifest.name}/${step.name}`)).toEqual([
|
||||
"openai-codex-bindings/regenerate-bindings",
|
||||
"peezy-codex-fork/rebase-patch-stack",
|
||||
"peezy-codex-fork/release-cycle",
|
||||
]);
|
||||
|
||||
const codeModeMatch = matches.find((entry) => entry.flow.manifest.name === "peezy-codex-fork");
|
||||
|
|
|
|||
|
|
@ -68,8 +68,8 @@ The Codex fork makes the desired model concrete:
|
|||
|
||||
- `origin/main` is a useful local comparison baseline, but a real upstream
|
||||
remote is still needed for canonical release tags.
|
||||
- `code-mode-exec-hooks` is the maintained patch branch, but internal use is
|
||||
not simply "run from this branch."
|
||||
- `main` is the maintained output branch; `code-mode-exec-hooks` remains only a
|
||||
legacy reference for the initial conversion.
|
||||
- `rust-v0.130.0` at the branch head is a downstream candidate or release tag.
|
||||
- the npm package rename to `@peezy.tech/*` is part of the patch stack, not a
|
||||
Patch service setting.
|
||||
|
|
@ -91,12 +91,16 @@ git remote get-url origin
|
|||
git remote get-url upstream || git remote add upstream https://github.com/openai/codex.git
|
||||
git fetch upstream --tags --prune
|
||||
git fetch origin --prune
|
||||
git rev-list --oneline origin/main..code-mode-exec-hooks
|
||||
git branch --list 'patch/*'
|
||||
git status --short --branch
|
||||
```
|
||||
|
||||
For an upstream release event, the workspace can resolve the upstream tag,
|
||||
rebase `code-mode-exec-hooks`, run Codex-specific checks, and push a candidate
|
||||
branch or tag when policy allows.
|
||||
rebuild `main` from that tag plus ordered `patch/*` branches, run Codex-specific
|
||||
checks, and push a candidate branch or tag when policy allows. For an upstream
|
||||
main commit event, the workspace updates the local `upstream` branch from
|
||||
`upstream/main` and rebuilds `main` from that branch plus the same patch
|
||||
inventory.
|
||||
|
||||
In service mode, that work should be triggered through the remote fork host. For
|
||||
example, patch.moi can create or update a maintenance branch on GitHub, trigger
|
||||
|
|
@ -109,7 +113,7 @@ service's durable execution surface.
|
|||
Feature development should happen in a separate workspace or branch. A new
|
||||
custom feature starts from the current maintained branch, produces commits, and
|
||||
only becomes part of the maintained patch stack after it is intentionally merged
|
||||
or rebased into `code-mode-exec-hooks`.
|
||||
or captured into a single logical `patch/*` branch.
|
||||
|
||||
## Channel Split
|
||||
|
||||
|
|
|
|||
|
|
@ -5,26 +5,27 @@ description: How patch.moi applies to Codex fork maintenance.
|
|||
|
||||
# Codex Use Case
|
||||
|
||||
patch.moi watches OpenAI Codex branch and release feeds. Branch activity can
|
||||
notify operators. Release activity can emit a deterministic `upstream.release`
|
||||
flow event that starts Codex fork maintenance.
|
||||
patch.moi watches OpenAI Codex branch and release feeds. Branch activity emits
|
||||
deterministic `upstream.branch_update` events for main-branch maintenance.
|
||||
Release activity emits deterministic `upstream.release` events for release-cycle
|
||||
maintenance.
|
||||
|
||||
The concrete local model is the neighboring `../codex` checkout:
|
||||
|
||||
- `origin` points at `https://github.com/peezy-tech/codex`.
|
||||
- `code-mode-exec-hooks` is the maintained patch branch.
|
||||
- `origin/main` is the current comparison branch.
|
||||
- `rust-v0.130.0` is a downstream tag on the maintained branch head.
|
||||
- the patch stack contains Code Mode exec/replay work and Peezy npm release
|
||||
changes.
|
||||
- `main` is the rebuildable maintained fork output.
|
||||
- `upstream` mirrors `upstream/main`.
|
||||
- ordered `patch/*` branches hold the logical patch commits.
|
||||
- `rust-v0.130.0` is a downstream tag on the legacy maintained branch head.
|
||||
- the current patch inventory contains Code Mode exec/replay work and Peezy npm
|
||||
release changes.
|
||||
|
||||
That checkout currently lacks an `upstream` remote, so a patch.moi setup flow
|
||||
should add or confirm `https://github.com/openai/codex.git` before a release
|
||||
rebase.
|
||||
That checkout has the canonical `upstream` remote, so maintenance can fetch
|
||||
OpenAI Codex main and release tags before rebuilding the fork output branch.
|
||||
|
||||
The Codex maintenance flow rebases the Peezy Codex fork patch stack onto a
|
||||
canonical upstream release tag. That is a patch application workspace job, not
|
||||
a public release by itself.
|
||||
The Codex maintenance flow rebuilds the Peezy Codex fork from a canonical
|
||||
upstream base plus ordered patch branches. That is a patch application workspace
|
||||
job, not a public release by itself.
|
||||
|
||||
Internal Codex use can track a fast-moving branch for local work. Public npm
|
||||
release can follow upstream release tags and trusted publishing. Those channels
|
||||
|
|
|
|||
|
|
@ -38,9 +38,9 @@ The workspace may be local or runner-managed. The patch stack itself still lives
|
|||
in Git. Repo-native `codex-flows workspace` tasks are operator automation for
|
||||
running known commands; they are not a separate patch-stack database.
|
||||
|
||||
In the current Codex fork, this means carrying the commits on
|
||||
`code-mode-exec-hooks` ahead of `origin/main` onto a canonical upstream release
|
||||
tag from `openai/codex`.
|
||||
In the current Codex fork, this means rebuilding `main` from a canonical
|
||||
OpenAI Codex release tag or `upstream/main` plus the ordered local `patch/*`
|
||||
branches.
|
||||
|
||||
## Feature Development Workspace
|
||||
|
||||
|
|
|
|||
|
|
@ -46,8 +46,9 @@ backend adapter:
|
|||
```
|
||||
|
||||
For patch-stack maintenance, prefer `workspace_flow` to create an
|
||||
`upstream.release` or `upstream.update` trigger. Let the receiving workspace read
|
||||
Git to discover the maintained patch branch and candidate refs.
|
||||
`upstream.release` or `upstream.branch_update` trigger. Let the receiving
|
||||
workspace read Git to discover the maintained branch, patch inventory, and
|
||||
candidate refs.
|
||||
|
||||
Use explicit payload fields for patch.moi-dispatched events. Do not rely on
|
||||
implicit workspace flow defaults for maintenance events, because patch.moi uses
|
||||
|
|
|
|||
|
|
@ -98,7 +98,18 @@ bun run patch.moi -- run codex-release \
|
|||
|
||||
The output should show the flow steps that will receive the event. For the
|
||||
current Codex release package, the expected fanout is the bindings update flow
|
||||
and the fork rebase flow.
|
||||
and the Codex fork release-cycle flow.
|
||||
|
||||
Dry-run the upstream main branch update path separately:
|
||||
|
||||
```bash
|
||||
bun run patch.moi -- run codex-main \
|
||||
--sha '<upstream-main-sha>' \
|
||||
--dry-run \
|
||||
--json
|
||||
```
|
||||
|
||||
That event should match the Codex fork `main-branch-update` Code Mode step.
|
||||
|
||||
## 5. Dispatch the maintenance attempt
|
||||
|
||||
|
|
@ -195,7 +206,8 @@ bun run patch.moi -- replay '<event-id>' --json
|
|||
|
||||
Flow packages decide exactly what `CODEX_FLOW_PUSH=1` means. The harness flow
|
||||
pushes configured branch refs with `--force-with-lease`. The Codex fork flow
|
||||
pushes the maintained branch and, when configured to publish, release tags.
|
||||
pushes the maintained `main` branch and, when configured to publish, release
|
||||
tags.
|
||||
|
||||
Keep `CODEX_FLOW_PUBLISH=0` unless you are intentionally entering the public
|
||||
release channel.
|
||||
|
|
|
|||
|
|
@ -90,6 +90,14 @@ Verify Codex release flow matching without executing release work:
|
|||
bun run patch.moi -- run codex-release --tag rust-v0.130.0 --dry-run
|
||||
```
|
||||
|
||||
Verify Codex upstream main update matching without executing branch maintenance:
|
||||
|
||||
```bash
|
||||
bun run patch.moi -- run codex-main \
|
||||
--sha '<upstream-main-sha>' \
|
||||
--dry-run
|
||||
```
|
||||
|
||||
Dispatching the Codex release task requires an explicit execution surface. Use
|
||||
Actions/local mode when no workspace backend is running:
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ adapter. The flow payload includes provider, event, source id, entry id, title,
|
|||
URL, author, published time, repository fields, ref, SHA, tag, and raw feed
|
||||
metadata. Values from `target.payload` are merged last.
|
||||
|
||||
For release maintenance, use a stable event type such as `upstream.release` and
|
||||
include only routing hints in `payload`. Avoid copying branch topology into the
|
||||
feed source when it can be read from the repository.
|
||||
For release maintenance, use a stable event type such as `upstream.release`.
|
||||
For upstream main movement, use `upstream.branch_update`. Include only routing
|
||||
hints in `payload`; avoid copying branch topology into the feed source when it
|
||||
can be read from the repository.
|
||||
|
|
|
|||
|
|
@ -60,10 +60,11 @@ maintenance flows from the sibling `../codex-flows` repository:
|
|||
codex-flows pack doctor --json
|
||||
```
|
||||
|
||||
`openai-codex-bindings` and `peezy-codex-fork` both match
|
||||
`upstream.release` events for `openai/codex`. They are installed capabilities,
|
||||
not patch.moi product state. patch.moi still records feed-owned flow events,
|
||||
workspace dispatches, and maintenance attempts under `DATA_DIR`.
|
||||
`openai-codex-bindings` matches `upstream.release` events for `openai/codex`.
|
||||
`peezy-codex-fork` matches both `upstream.release` and
|
||||
`upstream.branch_update` events. They are installed capabilities, not patch.moi
|
||||
product state. patch.moi still records feed-owned flow events, workspace
|
||||
dispatches, and maintenance attempts under `DATA_DIR`.
|
||||
|
||||
## Related Runtime Package
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ description: Connect the OpenAI Codex release feed to a Codex patch-stack mainte
|
|||
|
||||
This tutorial connects upstream OpenAI Codex releases to Codex fork
|
||||
maintenance. Patch records the upstream release and dispatches a deterministic
|
||||
event. The receiving workspace or runner rebases the maintained patch stack
|
||||
onto the upstream release tag and verifies the candidate.
|
||||
event. The receiving workspace or runner rebuilds the maintained `main` branch
|
||||
from the upstream release tag plus the ordered `patch/*` branches, then
|
||||
verifies the candidate.
|
||||
|
||||
## 1. Use the release source
|
||||
|
||||
|
|
@ -18,7 +19,13 @@ the upstream repository and release tag in the payload.
|
|||
|
||||
The maintained Codex fork should still be modeled in Git. In the neighboring
|
||||
`../codex` checkout, `origin` is `https://github.com/peezy-tech/codex` and
|
||||
`code-mode-exec-hooks` is the maintained patch branch.
|
||||
the local branch shape is:
|
||||
|
||||
```text
|
||||
main rebuildable maintained fork output
|
||||
upstream local mirror of upstream/main
|
||||
patch/* ordered one-commit logical patches
|
||||
```
|
||||
|
||||
Before running release maintenance, make sure the checkout has a canonical
|
||||
upstream remote:
|
||||
|
|
@ -26,13 +33,13 @@ upstream remote:
|
|||
```bash
|
||||
cd ../codex
|
||||
git remote get-url upstream || git remote add upstream https://github.com/openai/codex.git
|
||||
git fetch upstream --tags --prune
|
||||
git fetch upstream --tags --prune main
|
||||
git fetch origin --prune
|
||||
git status --short --branch
|
||||
```
|
||||
|
||||
If `git status` shows local changes or untracked files, resolve them before an
|
||||
automated rebase.
|
||||
automated rebuild.
|
||||
|
||||
## 2. Install the Codex release capabilities
|
||||
|
||||
|
|
@ -55,8 +62,8 @@ repo flow.
|
|||
|
||||
Safe local verification stops at event matching and runner gating. The test
|
||||
suite confirms that a stored `upstream.release` event for `openai/codex`
|
||||
selects both installed Codex release steps, and that the Code Mode step still
|
||||
requires `CODEX_FLOWS_MODE=code-mode`. Do not fabricate a full
|
||||
selects both installed release steps, and that the Code Mode step still requires
|
||||
`CODEX_FLOWS_MODE=code-mode`. Do not fabricate a full
|
||||
`openai/codex` release lifecycle just to exercise the flow.
|
||||
|
||||
You can run the same safe match check through the CLI:
|
||||
|
|
@ -113,10 +120,10 @@ or `X-Patch-Admin-Token: <token>`.
|
|||
Patch dispatches the generic event. The installed Codex release flow or
|
||||
workspace owns the work that happens next:
|
||||
|
||||
- fetch upstream tags
|
||||
- fetch upstream main and tags
|
||||
- resolve the release tag
|
||||
- rebase the maintained patch branch
|
||||
- collect conflict context when the rebase stops
|
||||
- rebuild `main` from the release tag plus ordered `patch/*` branches
|
||||
- collect conflict context when a cherry-pick stops
|
||||
- run the configured checks
|
||||
- optionally push a candidate ref
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue