ci: replace Cursor GitHub automation with Claude Code

Swap issue triage, implement, follow-up, and Codex-fix jobs from Cursor
CLI to Claude Code against Ollama Cloud (glm-5.3-flash:cloud). Isolated
research now uses Brave Search. Automation files and repo variables use
an ai- prefix; existing cursor-* HTML markers and cursor/issue-*
branches still parse so in-flight work is not reprocessed.
This commit is contained in:
bincxz
2026-08-27 20:55:21 +08:00
parent 180470a03b
commit f2dfd770ca
17 changed files with 1993 additions and 1536 deletions
+19 -19
View File
@@ -8,7 +8,7 @@ const readWorkflow = (name) => fs.readFileSync(path.join(workflowsDir, name), "u
const testWorkflow = readWorkflow("test.yml");
const buildWorkflow = readWorkflow("build.yml");
const cursorWorkflow = readWorkflow("cursor-automation.yml");
const aiWorkflow = readWorkflow("ai-automation.yml");
const etWorkflow = readWorkflow("build-et-binaries.yml");
const appBuilderPatch = fs.readFileSync(
path.join(__dirname, "..", "patches", "app-builder-lib+26.15.2.patch"),
@@ -278,7 +278,7 @@ test("Homebrew tap updates retry push races without downgrading newer releases",
});
test("Codex fix publishing treats a moved PR head as a stale result", () => {
const publishJob = cursorWorkflow.match(/\n publish_codex_fix:\n[\s\S]*?(?=\n own_rerequest_codex:)/);
const publishJob = aiWorkflow.match(/\n publish_codex_fix:\n[\s\S]*?(?=\n own_rerequest_codex:)/);
assert.ok(publishJob, "publish_codex_fix job must exist before own_rerequest_codex");
assert.match(publishJob[0], /--force-with-lease/);
assert.match(publishJob[0], /published=false/);
@@ -288,7 +288,7 @@ test("Codex fix publishing treats a moved PR head as a stale result", () => {
});
test("issue implementation publishing tolerates competing automation runs", () => {
const publishJob = cursorWorkflow.match(/\n publish_implement:\n[\s\S]*?(?=\n codex_loop:)/);
const publishJob = aiWorkflow.match(/\n publish_implement:\n[\s\S]*?(?=\n codex_loop:)/);
assert.ok(publishJob, "publish_implement job must exist before codex_loop");
assert.match(publishJob[0], /--force-with-lease/);
assert.match(publishJob[0], /candidate_tree/);
@@ -311,10 +311,10 @@ test("issue implementation publishing tolerates competing automation runs", () =
assert.doesNotMatch(publishJob[0], /steps\.publish\.outcome == 'failure'/);
assert.match(publishJob[0], /labels: \['ready-for-human'\]/);
assert.match(publishJob[0], /could not safely publish the implementation branch/);
assert.match(publishJob[0], /cursor-publish-handoff:/);
assert.match(publishJob[0], /ai-publish-handoff:/);
assert.match(publishJob[0], /github\.paginate\(github\.rest\.issues\.listComments/);
assert.match(publishJob[0], /steps\.publish\.outputs\.published == 'true'/);
assert.match(publishJob[0], /group: cursor-codex-head-/);
assert.match(publishJob[0], /group: ai-codex-head-/);
assert.match(publishJob[0], /status === 403 && createPermissionDenied/);
assert.match(publishJob[0], /resource not accessible by integration/);
assert.match(publishJob[0], /resource not accessible by personal access token/);
@@ -322,7 +322,7 @@ test("issue implementation publishing tolerates competing automation runs", () =
});
test("reused automation PRs still receive labels and one source-issue backlink", () => {
const openPr = cursorWorkflow.match(
const openPr = aiWorkflow.match(
/\n - name: Open draft PR[\s\S]*?(?=\n - name: Request Codex review on implement PR)/,
);
assert.ok(openPr, "open-PR step must exist before Codex review request");
@@ -333,7 +333,7 @@ test("reused automation PRs still receive labels and one source-issue backlink",
});
test("reused implementation PRs do not duplicate Codex requests for the same head", () => {
const requestCodex = cursorWorkflow.match(
const requestCodex = aiWorkflow.match(
/\n - name: Request Codex review on implement PR[\s\S]*?(?=\n codex_loop:)/,
);
assert.ok(requestCodex, "implement Codex request step must exist before codex_loop");
@@ -346,31 +346,31 @@ test("reused implementation PRs do not duplicate Codex requests for the same hea
});
test("all own-PR Codex request paths serialize on the head branch", () => {
const codexLoop = cursorWorkflow.match(/\n codex_loop:\n[\s\S]*?(?=\n publish_codex_fix:)/);
const ownRerequest = cursorWorkflow.match(/\n own_rerequest_codex:\n[\s\S]*?(?=\n external_rerequest_codex:)/);
const codexLoop = aiWorkflow.match(/\n codex_loop:\n[\s\S]*?(?=\n publish_codex_fix:)/);
const ownRerequest = aiWorkflow.match(/\n own_rerequest_codex:\n[\s\S]*?(?=\n external_rerequest_codex:)/);
assert.ok(codexLoop, "codex_loop job must exist before publish_codex_fix");
assert.ok(ownRerequest, "own_rerequest_codex must exist before external_rerequest_codex");
assert.match(cursorWorkflow, /head_ref: \$\{\{ steps\.decide\.outputs\.head_ref \}\}/);
assert.match(codexLoop[0], /group: cursor-codex-head-/);
assert.match(ownRerequest[0], /group: cursor-codex-head-/);
assert.match(aiWorkflow, /head_ref: \$\{\{ steps\.decide\.outputs\.head_ref \}\}/);
assert.match(codexLoop[0], /group: ai-codex-head-/);
assert.match(ownRerequest[0], /group: ai-codex-head-/);
assert.match(codexLoop[0], /needs\.route\.outputs\.head_ref/);
assert.match(ownRerequest[0], /needs\.route\.outputs\.head_ref/);
assert.match(cursorWorkflow, /head_ref: pr\.head\?\.ref \|\| ''/);
assert.match(aiWorkflow, /head_ref: pr\.head\?\.ref \|\| ''/);
assert.match(
cursorWorkflow,
aiWorkflow,
/issue_comment[\s\S]*?github\.rest\.pulls\.get[\s\S]*?head_ref: pr\.head\?\.ref \|\| ''/,
);
});
test("scheduled Codex polling ignores review comments remapped from old heads", () => {
const pollJob = cursorWorkflow.match(/\n codex_poll:\n[\s\S]*$/);
const pollJob = aiWorkflow.match(/\n codex_poll:\n[\s\S]*$/);
assert.ok(pollJob, "codex_poll job must exist");
assert.match(pollJob[0], /auto\.filterCodexReviewCommentsForHead\(\s*reviewComments,\s*pr\.head\.sha/);
assert.doesNotMatch(pollJob[0], /c\.commit_id \|\| c\.original_commit_id/);
});
test("clean Codex handoff updates labels without GraphQL-only organization scopes", () => {
const markReady = cursorWorkflow.match(/\n - name: Mark PR ready after clean Codex[\s\S]*?(?=\n - name: Give up after max rounds)/);
const markReady = aiWorkflow.match(/\n - name: Mark PR ready after clean Codex[\s\S]*?(?=\n - name: Give up after max rounds)/);
assert.ok(markReady, "mark-ready step must exist before give-up step");
assert.match(markReady[0], /gh api/);
assert.match(markReady[0], /issues\/\$\{PULL_NUMBER\}\/labels/);
@@ -384,9 +384,9 @@ test("clean Codex handoff updates labels without GraphQL-only organization scope
});
test("permission handoffs use the established ready-for-human label", () => {
assert.doesNotMatch(cursorWorkflow, /automation:needs-human/);
assert.match(cursorWorkflow, /labels: \['ready-for-human'\]/);
assert.match(cursorWorkflow, /-f 'labels\[\]=ready-for-human'/);
assert.doesNotMatch(aiWorkflow, /automation:needs-human/);
assert.match(aiWorkflow, /labels: \['ready-for-human'\]/);
assert.match(aiWorkflow, /-f 'labels\[\]=ready-for-human'/);
});
test("ET binary validation runs once and retries transient container pulls", () => {