refactor: remove legacy dev cd scripts

This commit is contained in:
Codex
2026-05-26 05:31:57 +08:00
parent f3191736fa
commit e6ac2898fe
32 changed files with 56 additions and 11242 deletions
-9
View File
@@ -1,9 +0,0 @@
import { runCicdWorker } from "./cicd-jobs.mjs";
const jobId = process.argv[2];
if (!jobId) {
process.stderr.write(`${JSON.stringify({ ok: false, code: "missing-job-id", message: "usage: node tools/hwlab-cli/lib/cicd-job-worker.mjs <jobId>" }, null, 2)}\n`);
process.exitCode = 1;
} else {
process.exitCode = await runCicdWorker({ repoRoot: process.cwd(), jobId });
}
-398
View File
@@ -1,398 +0,0 @@
import { spawn } from "node:child_process";
import { randomBytes } from "node:crypto";
import { createWriteStream } from "node:fs";
import { mkdir, open, readdir, readFile, stat, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
const stateRoot = ".state/hwlab-cicd/jobs";
const knownKinds = new Set(["ci-publish", "dev-cd-apply"]);
function isoNow(now) {
return (now ? now() : new Date()).toISOString();
}
function compactTimestamp(value) {
return value.replace(/[-:.]/gu, "").slice(0, 15);
}
function makeJobId(kind, now) {
return `${kind}-${compactTimestamp(isoNow(now))}-${randomBytes(3).toString("hex")}`;
}
function jobDir(repoRoot, jobId) {
return path.join(repoRoot, stateRoot, jobId);
}
function jobPaths(repoRoot, jobId) {
const dir = jobDir(repoRoot, jobId);
return {
dir,
specPath: path.join(dir, "spec.json"),
statePath: path.join(dir, "state.json"),
stdoutPath: path.join(dir, "stdout.log"),
stderrPath: path.join(dir, "stderr.log"),
reportPath: path.join(dir, "report.json")
};
}
async function readJson(filePath) {
return JSON.parse(await readFile(filePath, "utf8"));
}
async function writeJsonFile(filePath, value) {
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}
function result(exitCode, payload, stream = "stdout") {
return { exitCode, payload, stream };
}
function error(code, message, details = {}) {
return { ok: false, code, message, ...details };
}
function followup(jobId) {
return {
status: `hwlab-cli cicd status ${jobId}`,
logs: `hwlab-cli cicd logs ${jobId}`,
report: `hwlab-cli cicd report ${jobId}`
};
}
function optionValue(options, name, fallback = null) {
return options.get(name) ?? fallback;
}
function positiveInteger(value, fallback) {
if (value == null) return fallback;
const parsed = Number.parseInt(value, 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}
function buildJobSpec({ kind, flags, options, repoRoot, env, now }) {
if (!knownKinds.has(kind)) {
return { error: error("unsupported-cicd-kind", "Use --kind ci-publish or --kind dev-cd-apply.", { kind }) };
}
const jobId = makeJobId(kind, now);
const paths = jobPaths(repoRoot, jobId);
const envPatch = {};
const args = [];
if (kind === "ci-publish") {
args.push("scripts/dev-artifact-publish.mjs", "--publish", "--report", paths.reportPath, "--quiet-build");
const services = optionValue(options, "--services");
if (services) args.push("--services", services);
const concurrency = positiveInteger(optionValue(options, "--concurrency"), 4);
args.push("--concurrency", String(concurrency));
envPatch.HWLAB_CI_ARTIFACT_RUN_ID = optionValue(options, "--owner-task-id", jobId);
envPatch.HWLAB_CI_ARTIFACT_RUN_OWNER = optionValue(options, "--owner", "hwlab-cli");
} else {
if (!flags.has("--confirm-dev") || !flags.has("--confirmed-non-production")) {
return {
error: error("confirmation-required", "DEV CD apply requires --confirm-dev --confirmed-non-production.", {
requiredFlags: ["--confirm-dev", "--confirmed-non-production"]
})
};
}
args.push(
"scripts/dev-cd-apply.mjs",
"--apply",
"--confirm-dev",
"--confirmed-non-production",
"--owner-task-id",
optionValue(options, "--owner-task-id", jobId),
"--report",
paths.reportPath
);
if (flags.has("--break-stale-lock")) args.push("--break-stale-lock");
const concurrency = optionValue(options, "--concurrency");
if (concurrency) envPatch.HWLAB_DEV_CD_CONCURRENCY = String(positiveInteger(concurrency, 4));
const registryManifestBaseUrl = optionValue(options, "--registry-manifest-base-url");
if (registryManifestBaseUrl) args.push("--registry-manifest-base-url", registryManifestBaseUrl);
}
if (optionValue(options, "--target-ref")) args.push("--target-ref", optionValue(options, "--target-ref"));
if (optionValue(options, "--kubeconfig")) args.push("--kubeconfig", optionValue(options, "--kubeconfig"));
return {
spec: {
jobId,
kind,
cwd: repoRoot,
node: process.execPath,
command: process.execPath,
args,
envPatch,
createdAt: isoNow(now),
host: os.hostname(),
paths
},
paths,
env
};
}
async function defaultSpawnDetached({ repoRoot, jobId, env }) {
const workerPath = path.join(repoRoot, "tools/hwlab-cli/lib/cicd-job-worker.mjs");
const child = spawn(process.execPath, [workerPath, jobId], {
cwd: repoRoot,
env,
detached: true,
stdio: "ignore"
});
child.unref();
return { pid: child.pid };
}
async function submitJob(ctx) {
const kind = optionValue(ctx.options, "--kind");
const built = buildJobSpec({ ...ctx, kind });
if (built.error) return result(1, built.error, "stderr");
await mkdir(built.paths.dir, { recursive: true });
await writeJsonFile(built.paths.specPath, built.spec);
const initialState = {
ok: true,
jobId: built.spec.jobId,
kind: built.spec.kind,
status: "submitted",
nonBlocking: true,
createdAt: built.spec.createdAt,
updatedAt: built.spec.createdAt,
cwd: built.spec.cwd,
command: [path.basename(built.spec.command), ...built.spec.args].join(" "),
statePath: built.paths.statePath,
stdoutPath: built.paths.stdoutPath,
stderrPath: built.paths.stderrPath,
reportPath: built.paths.reportPath
};
await writeJsonFile(built.paths.statePath, initialState);
const launcher = ctx.spawnDetached ?? defaultSpawnDetached;
const launched = await launcher({ repoRoot: ctx.repoRoot, jobId: built.spec.jobId, env: ctx.env, spec: built.spec });
const submitted = {
...initialState,
pid: launched?.pid ?? null,
followup: followup(built.spec.jobId)
};
await writeJsonFile(built.paths.statePath, submitted);
return result(0, submitted);
}
async function appendLog(filePath, line) {
await writeFile(filePath, `${line}\n`, { flag: "a" });
}
function reportSummary(report) {
if (report?.artifactPublish) {
return {
kind: "ci-publish",
status: report.artifactPublish.status ?? report.status ?? null,
sourceCommitId: report.sourceCommitId ?? report.commitId ?? null,
serviceCount: report.artifactPublish.serviceCount ?? null,
requiredServiceCount: report.artifactPublish.requiredServiceCount ?? null,
publishedCount: report.artifactPublish.publishedCount ?? null,
timings: report.artifactPublish.timings ?? null,
blockers: report.blockers ?? []
};
}
if (report?.devCdApply) {
return {
kind: "dev-cd-apply",
status: report.status ?? null,
commitId: report.commitId ?? null,
target: report.target ?? null,
transaction: report.transaction
? {
transactionId: report.transaction.transactionId ?? null,
ownerTaskId: report.transaction.ownerTaskId ?? null,
phases: report.transaction.phases ?? [],
release: report.transaction.release ?? null
}
: null,
steps: report.devCdApply.steps?.map((step) => ({ id: step.id, status: step.status, reportPath: step.reportPath })) ?? [],
blockers: report.blockers ?? []
};
}
return {
kind: "generic",
status: report?.status ?? report?.ok ?? null,
keys: report && typeof report === "object" ? Object.keys(report).slice(0, 20) : []
};
}
async function runJob(ctx) {
const jobId = ctx.rest[0];
if (!jobId) return result(1, error("missing-job-id", "usage: hwlab-cli cicd run-job <jobId>"), "stderr");
const paths = jobPaths(ctx.repoRoot, jobId);
const spec = await readJson(paths.specPath);
const startedAt = isoNow(ctx.now);
await writeJsonFile(paths.statePath, {
ok: true,
jobId,
kind: spec.kind,
status: "running",
nonBlocking: true,
createdAt: spec.createdAt,
startedAt,
updatedAt: startedAt,
cwd: spec.cwd,
pid: process.pid,
command: [path.basename(spec.command), ...spec.args].join(" "),
statePath: paths.statePath,
stdoutPath: paths.stdoutPath,
stderrPath: paths.stderrPath,
reportPath: paths.reportPath
});
await appendLog(paths.stdoutPath, `[hwlab-cli] job ${jobId} started at ${startedAt}`);
const child = spawn(spec.command, spec.args, {
cwd: spec.cwd,
env: { ...process.env, ...spec.envPatch },
stdio: ["ignore", "pipe", "pipe"]
});
const stdoutStream = createWriteStream(paths.stdoutPath, { flags: "a" });
const stderrStream = createWriteStream(paths.stderrPath, { flags: "a" });
child.stdout.pipe(stdoutStream);
child.stderr.pipe(stderrStream);
const exitCode = await new Promise((resolve) => {
child.on("close", resolve);
});
stdoutStream.end();
stderrStream.end();
const finishedAt = isoNow(ctx.now);
const state = await readJson(paths.statePath);
let summary = null;
try {
summary = reportSummary(await readJson(paths.reportPath));
} catch {
summary = null;
}
const finalState = {
...state,
status: exitCode === 0 ? "succeeded" : "failed",
ok: exitCode === 0,
exitCode,
updatedAt: finishedAt,
finishedAt,
durationMs: Date.parse(finishedAt) - Date.parse(startedAt),
reportSummary: summary,
followup: followup(jobId)
};
await writeJsonFile(paths.statePath, finalState);
return result(exitCode === 0 ? 0 : 2, finalState, exitCode === 0 ? "stdout" : "stderr");
}
async function readState(repoRoot, jobId) {
const paths = jobPaths(repoRoot, jobId);
const state = await readJson(paths.statePath);
return { paths, state };
}
async function statusJob(ctx) {
const jobId = ctx.rest[0];
if (!jobId) return result(1, error("missing-job-id", "usage: hwlab-cli cicd status <jobId>"), "stderr");
try {
const { state } = await readState(ctx.repoRoot, jobId);
return result(0, { ...state, followup: followup(jobId) });
} catch (err) {
return result(2, error("job-not-found", `No CI/CD job state found for ${jobId}.`, { detail: err.message }), "stderr");
}
}
async function tailFile(filePath, bytes) {
try {
const info = await stat(filePath);
const length = Math.min(info.size, bytes);
const handle = await open(filePath, "r");
try {
const buffer = Buffer.alloc(length);
await handle.read(buffer, 0, length, info.size - length);
return { path: filePath, totalBytes: info.size, returnedBytes: length, truncated: info.size > length, text: buffer.toString("utf8") };
} finally {
await handle.close();
}
} catch {
return { path: filePath, totalBytes: 0, returnedBytes: 0, truncated: false, text: "" };
}
}
async function logsJob(ctx) {
const jobId = ctx.rest[0];
if (!jobId) return result(1, error("missing-job-id", "usage: hwlab-cli cicd logs <jobId>"), "stderr");
const bytes = Math.min(positiveInteger(optionValue(ctx.options, "--tail-bytes"), 8000), 20000);
try {
const { paths, state } = await readState(ctx.repoRoot, jobId);
return result(0, {
ok: true,
jobId,
status: state.status,
tailBytes: bytes,
stdout: await tailFile(paths.stdoutPath, bytes),
stderr: await tailFile(paths.stderrPath, bytes),
followup: followup(jobId)
});
} catch (err) {
return result(2, error("job-not-found", `No CI/CD job state found for ${jobId}.`, { detail: err.message }), "stderr");
}
}
async function reportJob(ctx) {
const jobId = ctx.rest[0];
if (!jobId) return result(1, error("missing-job-id", "usage: hwlab-cli cicd report <jobId>"), "stderr");
try {
const { paths, state } = await readState(ctx.repoRoot, jobId);
const report = await readJson(paths.reportPath);
return result(0, {
ok: true,
jobId,
status: state.status,
reportPath: paths.reportPath,
summary: reportSummary(report),
report: ctx.flags.has("--full") ? report : undefined,
followup: followup(jobId)
});
} catch (err) {
return result(2, error("report-not-ready", `No report is available for ${jobId}.`, { detail: err.message }), "stderr");
}
}
async function listJobs(ctx) {
const root = path.join(ctx.repoRoot, stateRoot);
const limit = Math.min(positiveInteger(optionValue(ctx.options, "--limit"), 20), 100);
try {
const names = (await readdir(root)).sort().reverse().slice(0, limit);
const jobs = [];
for (const name of names) {
try {
jobs.push((await readState(ctx.repoRoot, name)).state);
} catch {
// Ignore incomplete job directories; logs/report still remain on disk.
}
}
return result(0, { ok: true, command: "hwlab-cli cicd jobs", count: jobs.length, jobs });
} catch {
return result(0, { ok: true, command: "hwlab-cli cicd jobs", count: 0, jobs: [] });
}
}
export async function runCicdCommand(ctx) {
if (ctx.subcommand === "submit") return submitJob(ctx);
if (ctx.subcommand === "status") return statusJob(ctx);
if (ctx.subcommand === "logs") return logsJob(ctx);
if (ctx.subcommand === "report") return reportJob(ctx);
if (ctx.subcommand === "jobs") return listJobs(ctx);
return result(1, error("usage", "usage: hwlab-cli cicd submit --kind <ci-publish|dev-cd-apply> | status|logs|report <jobId> | jobs"), "stderr");
}
export async function runCicdWorker({ repoRoot = process.cwd(), jobId, now }) {
const outcome = await runJob({
repoRoot,
rest: [jobId],
now
});
return outcome.exitCode;
}
+10 -19
View File
@@ -1,6 +1,5 @@
import { DEV_ENDPOINT, loadMvpGateSummary } from "../../../internal/mvp-gate/summary.mjs";
import { runM3IoSkillCommand } from "../../../skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs";
import { runCicdCommand } from "./cicd-jobs.mjs";
function formatJson(value) {
return JSON.stringify(value, null, 2);
@@ -174,12 +173,7 @@ function writeHelp(stdout) {
"m3 status --api-base-url URL",
"m3 io --action do.write --value true --approved --api-base-url URL",
"m3 io --action di.read --api-base-url URL",
"cicd submit --kind ci-publish --concurrency 4",
"cicd submit --kind dev-cd-apply --confirm-dev --confirmed-non-production --concurrency 4",
"cicd status JOB_ID",
"cicd logs JOB_ID --tail-bytes 8000",
"cicd report JOB_ID",
"cicd jobs --limit 20",
"G14 CI/CD is managed by native Tekton + GitOps; hwlab-cli cicd is removed",
"test e2e --env dev --mvp",
"test e2e --env dev --mvp --dry-run",
"test e2e --env dev --mvp --live --confirm-dev --confirmed-non-production"
@@ -202,18 +196,15 @@ export async function runCli(argv, io) {
const { flags, options } = parseArgs(rest);
if (command === "cicd") {
const outcome = await runCicdCommand({
subcommand,
rest,
flags,
options,
repoRoot,
env,
now: io.now,
spawnDetached: io.spawnDetached
});
writeJson(outcome.stream === "stderr" ? stderr : stdout, outcome.payload);
return outcome.exitCode;
writeJson(stderr, makeError("legacy-cicd-removed", "hwlab-cli cicd was removed from the active G14 release path. Use G14 k3s Tekton/GitOps via tran G14:k3s and scripts/g14-gitops-render.mjs.", {
subcommand: subcommand ?? null,
replacement: {
observePipelineRuns: "tran G14:k3s kubectl get pipelineruns -n hwlab-ci",
observeArgo: "tran G14:k3s kubectl -n argocd get application hwlab-g14-dev hwlab-g14-prod",
renderSource: "node scripts/g14-gitops-render.mjs --check"
}
}));
return 2;
}
if (command === "m3" && ["io", "status"].includes(subcommand)) {
+8 -77
View File
@@ -1,7 +1,4 @@
import assert from "node:assert/strict";
import { mkdtemp, readFile, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import {
@@ -34,80 +31,14 @@ async function captureCli(args, options = {}) {
return { exitCode, stdout, stderr };
}
test("repo hwlab-cli cicd submit is non-blocking and records follow-up commands", async () => {
const cwd = await mkdtemp(path.join(os.tmpdir(), "hwlab-cli-cicd-"));
const launched = [];
const result = await captureCli(
["cicd", "submit", "--kind", "ci-publish", "--services", "hwlab-cloud-web", "--concurrency", "2"],
{
cwd,
spawnDetached: async (job) => {
launched.push(job);
return { pid: 123456 };
}
}
);
assert.equal(result.exitCode, 0, result.stderr);
const body = JSON.parse(result.stdout);
assert.equal(body.ok, true);
assert.equal(body.nonBlocking, true);
assert.equal(body.kind, "ci-publish");
assert.match(body.jobId, /^ci-publish-/u);
assert.equal(body.pid, 123456);
assert.equal(body.followup.status, `hwlab-cli cicd status ${body.jobId}`);
assert.equal(launched.length, 1);
const spec = JSON.parse(await readFile(path.join(cwd, ".state", "hwlab-cicd", "jobs", body.jobId, "spec.json"), "utf8"));
assert.deepEqual(spec.args.slice(0, 4), ["scripts/dev-artifact-publish.mjs", "--publish", "--report", body.reportPath]);
assert.ok(spec.args.includes("--quiet-build"));
assert.equal(spec.envPatch.HWLAB_CI_ARTIFACT_RUN_ID, body.jobId);
});
test("repo hwlab-cli cicd status/logs/report disclose bounded job state", async () => {
const cwd = await mkdtemp(path.join(os.tmpdir(), "hwlab-cli-cicd-"));
const submit = await captureCli(["cicd", "submit", "--kind", "ci-publish"], {
cwd,
spawnDetached: async () => ({ pid: 123457 })
});
const submitted = JSON.parse(submit.stdout);
const jobDir = path.join(cwd, ".state", "hwlab-cicd", "jobs", submitted.jobId);
await writeFile(path.join(jobDir, "stdout.log"), "build line 1\nbuild line 2\n", "utf8");
await writeFile(path.join(jobDir, "stderr.log"), "warning line\n", "utf8");
await writeFile(
path.join(jobDir, "report.json"),
`${JSON.stringify({
sourceCommitId: "abc123",
artifactPublish: {
status: "published",
serviceCount: 1,
requiredServiceCount: 1,
publishedCount: 1,
timings: { durationMs: 42 }
},
blockers: []
})}\n`,
"utf8"
);
const status = await captureCli(["cicd", "status", submitted.jobId], { cwd });
assert.equal(status.exitCode, 0, status.stderr);
assert.equal(JSON.parse(status.stdout).status, "submitted");
const logs = await captureCli(["cicd", "logs", submitted.jobId, "--tail-bytes", "12"], { cwd });
assert.equal(logs.exitCode, 0, logs.stderr);
const logBody = JSON.parse(logs.stdout);
assert.equal(logBody.stdout.returnedBytes, 12);
assert.equal(logBody.stdout.truncated, true);
assert.ok(logBody.stdout.text.includes("line 2"));
const report = await captureCli(["cicd", "report", submitted.jobId], { cwd });
assert.equal(report.exitCode, 0, report.stderr);
const reportBody = JSON.parse(report.stdout);
assert.equal(reportBody.summary.kind, "ci-publish");
assert.equal(reportBody.summary.status, "published");
assert.equal(reportBody.summary.publishedCount, 1);
assert.equal(Object.hasOwn(reportBody, "report"), false);
test("repo hwlab-cli cicd is removed from the active G14 release path", async () => {
const result = await captureCli(["cicd", "submit", "--kind", "ci-publish"]);
assert.equal(result.exitCode, 2);
const body = JSON.parse(result.stderr);
assert.equal(body.ok, false);
assert.equal(body.code, "legacy-cicd-removed");
assert.match(body.message, /G14 release path/u);
assert.match(body.replacement.observePipelineRuns, /tran G14:k3s/u);
});
test("repo hwlab-cli m3 status uses Skill CLI and calls only HWLAB API /v1/m3/status", async () => {
+1 -1
View File
@@ -7,7 +7,7 @@
"hwlab-cli": "./bin/hwlab-cli.mjs"
},
"scripts": {
"check": "node --check bin/hwlab-cli.mjs && node --check lib/cli.mjs && node --check lib/cicd-jobs.mjs && node --check lib/cicd-job-worker.mjs && node --test lib/cli.test.mjs && node ../../scripts/l6-cli-web-smoke.mjs",
"check": "node --check bin/hwlab-cli.mjs && node --check lib/cli.mjs && node --test lib/cli.test.mjs && node ../../scripts/l6-cli-web-smoke.mjs",
"health": "node bin/hwlab-cli.mjs health"
}
}