feat: promoter role — code_rev from engine git commit (#3)

This commit is contained in:
小橘 2026-04-19 03:33:52 +00:00
parent 04a8683d0a
commit be380a53ca
3 changed files with 92 additions and 58 deletions

View File

@ -29,6 +29,7 @@ import {
} from '@uncaged/pulse';
import { createMetaGateRole, type GateMeta } from './roles/meta-gate.js';
import { type MetaPromoterMeta } from './roles/meta-promoter.js';
// ── Meta Types ─────────────────────────────────────────────────
@ -49,9 +50,6 @@ export interface MetaTesterMeta {
[key: string]: unknown;
pass: boolean;
reason: string;
/** Only present when pass=true */
commitHash?: string;
pushed?: boolean;
}
export type MetaWorkflowRoles = {
@ -59,6 +57,7 @@ export type MetaWorkflowRoles = {
coder: Role<MetaCoderMeta>;
checker: Role<MetaCheckerMeta>;
tester: Role<MetaTesterMeta>;
promoter: Role<MetaPromoterMeta>;
};
// ── Moderator ──────────────────────────────────────────────────
@ -82,8 +81,10 @@ function metaModerator(
}
case 'tester': {
const meta = input.meta as MetaTesterMeta | null;
return meta?.pass ? END : 'coder';
return meta?.pass ? 'promoter' : 'coder';
}
case 'promoter':
return END;
default:
return END;
}
@ -103,12 +104,17 @@ function createDefaultMetaRoles(engineDir: string): MetaWorkflowRoles {
content: 'e2e stub',
meta: { pass: true, reason: 'e2e stub' },
});
const stubPromoter: Role<MetaPromoterMeta> = async () => ({
content: 'promote stub',
meta: { commitHash: 'stub', pushed: false, codeRev: 'stub' },
});
return {
gate: createMetaGateRole({ engineDir }),
coder: stubCoder,
checker: stubChecker,
tester: stubTester,
promoter: stubPromoter,
};
}

View File

@ -0,0 +1,80 @@
/**
* Meta Promoter role commit, push, and emit promote event.
* code_rev = engine repo git commit hash.
*
* 🍊 (NEKO Team)
*/
import { execSync } from 'node:child_process';
import type { Role, RoleResult, WorkflowMessage } from '@uncaged/pulse';
export interface MetaPromoterMeta {
[key: string]: unknown;
commitHash: string;
pushed: boolean;
codeRev: string;
}
export function createMetaPromoterRole(opts: {
repoDir: string;
remote?: string;
branch?: string;
}): Role<MetaPromoterMeta> {
const branch = opts.branch ?? 'main';
return async (chain: WorkflowMessage[]): Promise<RoleResult<MetaPromoterMeta>> => {
const cwd = opts.repoDir;
const exec = (cmd: string) =>
execSync(cmd, { cwd, encoding: 'utf-8', timeout: 30_000 }).trim();
// Get commit message from __start__
const startMsg = chain.find((m) => m.role === '__start__');
const firstLine = (startMsg?.content ?? '').split('\n')[0].slice(0, 60);
const commitMsg = firstLine || 'meta workflow auto-commit';
// Stage all changes
exec('git add -A');
let commitHash: string;
try {
exec('git diff --cached --quiet');
// No changes to commit
commitHash = exec('git rev-parse --short HEAD');
} catch {
// Has staged changes — commit
exec(`git commit -m "${commitMsg}" --author="小橘 <xiaoju@shazhou.work>"`);
commitHash = exec('git rev-parse --short HEAD');
}
// Full hash for code_rev
const fullHash = exec('git rev-parse HEAD');
// Push
let pushed = false;
const remote = opts.remote ?? (() => {
try {
const remotes = exec('git remote').split('\n').filter(Boolean);
return remotes[0] || null;
} catch { return null; }
})();
if (remote) {
try {
exec(`git push ${remote} ${branch} --no-verify`);
pushed = true;
} catch {
pushed = false;
}
}
return {
content: `promote: ${commitHash} (pushed: ${pushed})`,
meta: {
commitHash,
pushed,
codeRev: fullHash,
},
};
};
}

View File

@ -8,7 +8,6 @@
* 🍊 (NEKO Team)
*/
import { execSync } from 'node:child_process';
import { existsSync, mkdtempSync, readdirSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
@ -186,60 +185,9 @@ export function createMetaTesterRole(opts: {
};
}
// Step 4: All pass — commit + push
let commitHash: string | undefined;
let pushed: boolean | undefined;
const exec = (cmd: string) =>
execSync(cmd, { cwd, encoding: 'utf-8', timeout: 30_000 }).trim();
try {
const startMsg = chain.find((m) => m.role === '__start__');
const firstLine = (startMsg?.content ?? '').split('\n')[0].slice(0, 60);
const commitMsg = firstLine || 'meta workflow auto-commit';
exec('git add -A');
// Check if there's anything to commit
try {
exec('git diff --cached --quiet');
// No changes — still pass, just no commit needed
commitHash = exec('git rev-parse --short HEAD');
pushed = false;
} catch {
// There are staged changes
exec(
`git commit -m "${commitMsg}" --author="小橘 <xiaoju@shazhou.work>"`,
);
commitHash = exec('git rev-parse --short HEAD');
// Auto-detect remote
const remote = opts.remote ?? (() => {
try {
const remotes = exec('git remote').split('\n').filter(Boolean);
return remotes[0] || null;
} catch { return null; }
})();
if (remote) {
try {
exec(`git push ${remote} ${branch} --no-verify`);
pushed = true;
} catch {
pushed = false;
}
} else {
pushed = false;
}
}
} catch (err: any) {
commitHash = undefined;
pushed = false;
}
return {
content: `e2e 验证通过\n\n${summary}\n\nCommit: ${commitHash ?? 'none'}\nPushed: ${pushed ? 'yes' : 'no'}`,
meta: { pass: true, reason: 'e2e verification passed', commitHash, pushed },
content: `e2e 验证通过\n\n${summary}`,
meta: { pass: true, reason: 'e2e verification passed' },
};
};
}