From 5f11ec389a4578055e76f1f378db5bf2a8fdd8e6 Mon Sep 17 00:00:00 2001 From: Code Queue Review Date: Sat, 23 May 2026 09:13:18 +0000 Subject: [PATCH] fix: format dev cd lease timestamps --- scripts/src/dev-cd-apply.mjs | 18 ++++++++++++++---- scripts/src/dev-cd-apply.test.mjs | 7 +++++++ scripts/validate-dev-gate-report.mjs | 13 +++++++++---- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/scripts/src/dev-cd-apply.mjs b/scripts/src/dev-cd-apply.mjs index aee2e340..04ce2590 100644 --- a/scripts/src/dev-cd-apply.mjs +++ b/scripts/src/dev-cd-apply.mjs @@ -154,6 +154,16 @@ function nowIso() { return new Date().toISOString(); } +function kubernetesMicroTime(value) { + const date = value instanceof Date ? value : new Date(value); + if (!Number.isFinite(date.getTime())) { + throw new DevCdApplyError("invalid Kubernetes Lease timestamp", { + code: "invalid-lease-timestamp" + }); + } + return date.toISOString().replace(/\.(\d{3})Z$/u, ".$1000Z"); +} + function parseJsonMaybe(value) { try { return JSON.parse(value); @@ -580,7 +590,7 @@ function mergeLease(lease, patch, { resetLockAnnotations = false } = {}) { } async function acquireDeployLock({ ctx, args, kubectlContext, target, deployBefore, transactionId, ownerTaskId, liveBefore, now }) { - const startedAt = now.toISOString(); + const startedAt = kubernetesMicroTime(now); const get = await kubectl(ctx, kubectlContext, ["-n", args.targetNamespace, "get", "lease", args.lockName, "-o", "json"]); if (get.code === 0) { const lease = JSON.parse(get.stdout); @@ -698,7 +708,7 @@ async function patchOwnedLease({ ctx, args, kubectlContext, lockState, transacti } async function updateDeployLockPhase({ ctx, args, kubectlContext, transactionId, lockState, phase, status = "running" }) { - const updatedAt = ctx.now().toISOString(); + const updatedAt = kubernetesMicroTime(ctx.now()); const patch = { metadata: { annotations: { @@ -724,7 +734,7 @@ async function updateDeployLockPhase({ ctx, args, kubectlContext, transactionId, } async function updateDeployLockLiveBefore({ ctx, args, kubectlContext, transactionId, lockState, liveBefore }) { - const updatedAt = ctx.now().toISOString(); + const updatedAt = kubernetesMicroTime(ctx.now()); const patch = { metadata: { annotations: { @@ -767,7 +777,7 @@ async function releaseDeployLock({ ctx, args, kubectlContext, transactionId, sta phase: current.phase }; } - const releasedAt = ctx.now().toISOString(); + const releasedAt = kubernetesMicroTime(ctx.now()); const patch = { metadata: { annotations: { diff --git a/scripts/src/dev-cd-apply.test.mjs b/scripts/src/dev-cd-apply.test.mjs index 51ed4513..427adf9f 100644 --- a/scripts/src/dev-cd-apply.test.mjs +++ b/scripts/src/dev-cd-apply.test.mjs @@ -100,6 +100,9 @@ function makeHttpGetJson() { function makeRunCommand({ heldLock = null, commandLog = [] } = {}) { let lease = heldLock ? leaseFromLock(heldLock) : null; + const assertLeaseMicroTime = (value) => { + assert.match(value, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}Z$/u); + }; return async (command, args, options = {}) => { commandLog.push({ command, args, env: options.env ?? {}, input: options.input ?? "" }); if (command === "git" && args[0] === "rev-parse" && args.includes("origin/main^{commit}")) { @@ -117,12 +120,16 @@ function makeRunCommand({ heldLock = null, commandLog = [] } = {}) { } if (command.includes("kubectl") && args.includes("create")) { lease = JSON.parse(options.input); + assertLeaseMicroTime(lease.spec.acquireTime); + assertLeaseMicroTime(lease.spec.renewTime); lease.metadata.resourceVersion = "2"; return { code: 0, stdout: `${JSON.stringify(lease)}\n`, stderr: "" }; } if (command.includes("kubectl") && args.includes("replace")) { const nextLease = JSON.parse(options.input); assert.equal(nextLease.metadata?.resourceVersion, lease?.metadata?.resourceVersion); + if (nextLease.spec?.acquireTime) assertLeaseMicroTime(nextLease.spec.acquireTime); + if (nextLease.spec?.renewTime) assertLeaseMicroTime(nextLease.spec.renewTime); lease = nextLease; lease.metadata.resourceVersion = String(Number(lease.metadata.resourceVersion ?? "2") + 1); return { code: 0, stdout: `${JSON.stringify(lease)}\n`, stderr: "" }; diff --git a/scripts/validate-dev-gate-report.mjs b/scripts/validate-dev-gate-report.mjs index 825c8b98..42ea299c 100644 --- a/scripts/validate-dev-gate-report.mjs +++ b/scripts/validate-dev-gate-report.mjs @@ -1015,10 +1015,6 @@ function assertDevCdApplyReport(report, label) { assertString(report.transaction.ownerTaskId, `${label}.transaction.ownerTaskId`); assert.ok(Number.isInteger(report.transaction.ttlSeconds), `${label}.transaction.ttlSeconds`); assertArray(report.transaction.phases, `${label}.transaction.phases`); - assert.ok( - report.transaction.phases.some((phase) => phase.phase === "publishing"), - `${label}.transaction.phases must include publishing` - ); assertObject(report.devCdApply, `${label}.devCdApply`); for (const field of ["mode", "target", "deployJson", "lock", "steps", "liveBefore", "liveVerify", "reportPaths", "safety"]) { @@ -1042,6 +1038,15 @@ function assertDevCdApplyReport(report, label) { assertObject(report.devCdApply.lock, `${label}.devCdApply.lock`); assert.equal(typeof report.devCdApply.lock.acquired, "boolean", `${label}.devCdApply.lock.acquired`); + if (report.devCdApply.lock.acquired) { + assert.ok( + report.transaction.phases.some((phase) => phase.phase === "publishing"), + `${label}.transaction.phases must include publishing after lock acquisition` + ); + } else { + assert.equal(report.status, "blocked", `${label}.status must be blocked when lock was not acquired`); + assert.equal(report.devCdApply.safety.mutationAttempted, false, `${label}.devCdApply.safety.mutationAttempted`); + } assertArray(report.devCdApply.steps, `${label}.devCdApply.steps`); for (const [index, step] of report.devCdApply.steps.entries()) {