#!/usr/bin/env node import { createHash } from "node:crypto"; import { spawnSync } from "node:child_process"; import { cp, lstat, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const scriptDir = dirname(fileURLToPath(import.meta.url)); const platformRoot = resolve(scriptDir, "../.."); const taskerRoot = resolve( process.env.NODEDC_TASKMANAGER_ROOT || resolve(platformRoot, "../../data/dc_taskmanager/NODEDC_TASKMANAGER"), ); const opsAgentsRoot = resolve( process.env.NODEDC_OPS_AGENTS_ROOT || resolve(platformRoot, "../../data/NODEDC_TASKMANAGER_CODEXAPI"), ); const artifactDir = resolve(process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(scriptDir, "../deploy-artifacts")); const [release = "20260806-001", ...extra] = process.argv.slice(2); if (extra.length || release !== "20260806-001") { throw new Error("usage: build-tasker-ops-ui-comment-edit-artifacts.mjs [20260806-001]"); } const descriptors = [ { artifactBasename: `nodedc-tasker-ops-ui-comment-edit-${release}.tgz`, component: "tasker", expectedCommit: "12b595b1ed12638fe2d4722d00dcd7b8e625a94c", files: [ "plane-src/apps/api/plane/authentication/views/nodedc_agent_adapter.py", "plane-src/apps/api/plane/urls.py", "plane-src/apps/web/core/components/dropdowns/member/member-options.tsx", "plane-src/apps/web/core/components/dropdowns/state/base.tsx", "plane-src/apps/web/styles/globals.css", "plane-src/packages/ui/src/dropdown/multi-select.tsx", "plane-src/packages/ui/src/dropdown/single-select.tsx", ], patchId: `tasker-ops-ui-comment-edit-${release}`, sourceRoot: taskerRoot, }, { artifactBasename: `nodedc-ops-agents-comment-edit-${release}.tgz`, component: "ops-agents", expectedCommit: "0f3bf9d8aacfb445d33ea02162e0707957646890", files: [ "src/mcp/tool-runtime.ts", "src/routes/tools.ts", "src/tasker/client.ts", ], patchId: `ops-agents-comment-edit-${release}`, sourceRoot: opsAgentsRoot, }, ]; await mkdir(artifactDir, { recursive: true }); const artifacts = []; for (const descriptor of descriptors) { const sourceCommit = gitOutput(descriptor.sourceRoot, ["rev-parse", "HEAD"]); if (sourceCommit !== descriptor.expectedCommit) { throw new Error(`source_commit_mismatch:${descriptor.component}:${sourceCommit}`); } const sourceStatus = gitOutput(descriptor.sourceRoot, ["status", "--porcelain"]); if (sourceStatus) throw new Error(`source_worktree_not_clean:${descriptor.component}`); const stage = await mkdtemp(join(tmpdir(), `nodedc-${descriptor.component}-ui-comment-edit-`)); const payload = join(stage, "payload"); const artifact = join(artifactDir, descriptor.artifactBasename); try { await assertFresh(artifact); await mkdir(payload, { recursive: true }); for (const relativePath of descriptor.files) { const source = resolve(descriptor.sourceRoot, relativePath); const sourceStat = await lstat(source); if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) { throw new Error(`source_file_rejected:${descriptor.component}:${relativePath}`); } const destination = join(payload, relativePath); await mkdir(dirname(destination), { recursive: true }); await cp(source, destination, { force: false, verbatimSymlinks: true }); } await writeFile( join(stage, "manifest.env"), `id=${descriptor.patchId}\ncomponent=${descriptor.component}\ntype=app-overlay\n`, "utf8", ); await writeFile(join(stage, "files.txt"), `${descriptor.files.join("\n")}\n`, "utf8"); const tar = spawnSync("python3", ["-c", canonicalTarScript(), artifact, stage], { encoding: "utf8", maxBuffer: 128 * 1024 * 1024, }); if (tar.status !== 0) throw new Error(`tar_failed:${descriptor.component}:${tar.stderr || tar.stdout}`); const sha256 = createHash("sha256").update(await readFile(artifact)).digest("hex"); artifacts.push({ artifact, component: descriptor.component, files: descriptor.files, patchId: descriptor.patchId, sha256, sourceCommit, }); } finally { await rm(stage, { recursive: true, force: true }); } } console.log(JSON.stringify({ ok: true, release, deployOrder: ["tasker", "ops-agents"], artifacts }, null, 2)); function gitOutput(cwd, args) { const result = spawnSync("git", args, { cwd, encoding: "utf8" }); if (result.status !== 0) throw new Error(`git_failed:${cwd}:${args.join("_")}:${result.stderr || result.stdout}`); return result.stdout.trim(); } async function assertFresh(path) { try { await lstat(path); } catch (error) { if (error?.code === "ENOENT") return; throw error; } throw new Error(`output_already_exists:${path}`); } function canonicalTarScript() { return [ "import gzip,io,pathlib,sys,tarfile", "root=pathlib.Path(sys.argv[2])", "with open(sys.argv[1],'wb') as out:", " with gzip.GzipFile(filename='',mode='wb',fileobj=out,compresslevel=9,mtime=0) as gz:", " with tarfile.open(fileobj=gz,mode='w',format=tarfile.PAX_FORMAT) as tar:", " for top in ('manifest.env','files.txt','payload'):", " p=root/top; paths=[p]+(sorted(p.rglob('*')) if p.is_dir() else [])", " for x in paths:", " info=tar.gettarinfo(str(x),arcname=x.relative_to(root).as_posix())", " info.uid=info.gid=0; info.uname=info.gname='root'; info.mtime=0; info.mode=0o755 if info.isdir() else 0o644", " with (open(x,'rb') if info.isfile() else io.BytesIO()) as src: tar.addfile(info,src if info.isfile() else None)", ].join("\n"); }