diff --git a/scripts/artifact-registry-local-provider-contract-test.ts b/scripts/artifact-registry-local-provider-contract-test.ts new file mode 100644 index 00000000..b5581e75 --- /dev/null +++ b/scripts/artifact-registry-local-provider-contract-test.ts @@ -0,0 +1,46 @@ +import { readFileSync } from "node:fs"; + +function assertCondition(condition: unknown, message: string, detail: unknown = {}): void { + if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`); +} + +const artifactRegistrySource = readFileSync("scripts/src/artifact-registry.ts", "utf8"); +const deploySource = readFileSync("scripts/src/deploy.ts", "utf8"); + +assertCondition( + artifactRegistrySource.includes('function isLocalProvider(providerId: string): boolean'), + "artifact-registry must define an explicit local provider predicate", +); +assertCondition( + artifactRegistrySource.includes('providerId === "local"') && artifactRegistrySource.includes('providerId === "D601-local"'), + "local provider predicate must accept only explicit local aliases", +); +assertCondition( + artifactRegistrySource.includes('runCommand(["bash", "-lc", script], repoRoot, { timeoutMs })'), + "local provider must execute the generated artifact script directly with bash -lc", +); +assertCondition( + artifactRegistrySource.includes('local bash -lc '), + "readonly command shape must disclose local execution instead of host.ssh", +); +assertCondition( + deploySource.includes('providerId: string;'), + "deploy options must carry providerId for artifact consumers", +); +assertCondition( + deploySource.includes('providerId: optionValue(args, ["--provider-id", "--provider"]) ?? "D601"'), + "deploy apply must parse provider-id with D601 as the default", +); +assertCondition( + deploySource.includes('"--provider-id", options.providerId'), + "deploy apply must forward provider-id to artifact-registry deploy-service", +); + +process.stdout.write(`${JSON.stringify({ + ok: true, + checks: [ + "artifact-registry supports an explicit local/D601-local provider for D601 host CLI execution", + "local provider runs the same generated scripts through bash -lc without provider SSH self-dispatch", + "deploy apply forwards --provider-id to artifact-registry consumers while defaulting to D601", + ], +}, null, 2)}\n`); diff --git a/scripts/src/artifact-registry.ts b/scripts/src/artifact-registry.ts index 74ff0d09..683ccbfe 100644 --- a/scripts/src/artifact-registry.ts +++ b/scripts/src/artifact-registry.ts @@ -1245,9 +1245,14 @@ function registryRecommendedAction(classification: ArtifactRegistryFailureClassi } function readonlyRemoteCommandShape(action: "status" | "health", options: ArtifactRegistryOptions): string { + if (isLocalProvider(options.providerId)) return `local bash -lc timeoutMs=${options.timeoutMs}`; return `host.ssh provider=${options.providerId} mode=exec argv=bash -lc timeoutMs=${options.timeoutMs}`; } +function isLocalProvider(providerId: string): boolean { + return providerId === "local" || providerId === "D601-local"; +} + function classifyProviderSshCommandFailure(command: CommandResult): ArtifactRegistryFailureClassification { const output = `${command.stderr}\n${command.stdout}`.toLowerCase(); if (command.timedOut || output.includes("timed out") || output.includes("timeout")) return "remote-command-timeout"; @@ -1652,8 +1657,11 @@ function artifactRegistryDeployJsonMirrors( } function runRemoteScript(options: ArtifactRegistryOptions, script: string, timeoutMs = options.timeoutMs, runtime: ArtifactRegistryCommandRuntime = {}): CommandResult { - const command = [process.execPath, "scripts/cli.ts", "ssh", options.providerId, "argv", "bash", "-lc", script]; if (runtime.runRemoteScriptForTest !== undefined) return runtime.runRemoteScriptForTest(options, script, timeoutMs); + if (isLocalProvider(options.providerId)) { + return runCommand(["bash", "-lc", script], repoRoot, { timeoutMs }); + } + const command = [process.execPath, "scripts/cli.ts", "ssh", options.providerId, "argv", "bash", "-lc", script]; return runCommand(command, repoRoot, { timeoutMs }); } diff --git a/scripts/src/check.ts b/scripts/src/check.ts index 4bfbd9d9..b85e53ce 100644 --- a/scripts/src/check.ts +++ b/scripts/src/check.ts @@ -461,6 +461,7 @@ export async function runChecks(config: UniDeskConfig, options: CheckOptions = d fileItem("scripts/src/ci.ts"), fileItem("scripts/src/e2e.ts"), fileItem("scripts/deploy-artifact-matrix-contract-test.ts"), + fileItem("scripts/artifact-registry-local-provider-contract-test.ts"), fileItem("scripts/decision-center-diary-summary-contract-test.ts"), fileItem("scripts/decision-center-desired-state-contract-test.ts"), fileItem("scripts/code-queue-prompt-observation-test.ts"), @@ -524,6 +525,7 @@ export async function runChecks(config: UniDeskConfig, options: CheckOptions = d items.push(await commandItem("provider:runner-triage-contract", ["bun", "scripts/provider-runner-triage-contract-test.ts"], 30_000, process.env, options.checkHeartbeatMs)); items.push(await commandItem("ssh:argv-guidance-contract", ["bun", "scripts/ssh-argv-guidance-contract-test.ts"], 30_000, process.env, options.checkHeartbeatMs)); items.push(await commandItem("deploy:artifact-matrix-contract", ["bun", "scripts/deploy-artifact-matrix-contract-test.ts"], 90_000, process.env, options.checkHeartbeatMs)); + items.push(await commandItem("artifact-registry:local-provider-contract", ["bun", "scripts/artifact-registry-local-provider-contract-test.ts"], 30_000, process.env, options.checkHeartbeatMs)); items.push(await commandItem("decision-center:diary-summary-contract", ["bun", "scripts/decision-center-diary-summary-contract-test.ts"], 30_000, process.env, options.checkHeartbeatMs)); items.push(await commandItem("decision-center:desired-state-contract", ["bun", "scripts/decision-center-desired-state-contract-test.ts"], 30_000, process.env, options.checkHeartbeatMs)); items.push(await commandItem("code-queue:active-run-heartbeat-visible", ["bun", "scripts/code-queue-liveness-diagnostics-test.ts", "--only", "code-queue:active-run-heartbeat-visible"], 30_000, process.env, options.checkHeartbeatMs)); @@ -574,6 +576,7 @@ export async function runChecks(config: UniDeskConfig, options: CheckOptions = d items.push(skippedItem("provider:runner-triage-contract", "Provider runner triage contract is opt-in with script checks", "--scripts-typecheck or --full")); items.push(skippedItem("ssh:argv-guidance-contract", "SSH argv guidance and failure hint contract is opt-in with script checks", "--scripts-typecheck or --full")); items.push(skippedItem("deploy:artifact-matrix-contract", "deploy artifact matrix contract is opt-in with script checks", "--scripts-typecheck or --full")); + items.push(skippedItem("artifact-registry:local-provider-contract", "artifact registry local provider contract is opt-in with script checks", "--scripts-typecheck or --full")); items.push(skippedItem("decision-center:diary-summary-contract", "Decision Center diary summary contract is opt-in with script checks", "--scripts-typecheck or --full")); items.push(skippedItem("decision-center:desired-state-contract", "Decision Center desired-state drift contract is opt-in with script checks", "--scripts-typecheck or --full")); items.push(skippedItem("code-queue:liveness-diagnostics-fixtures", "Code Queue liveness diagnostics fixtures are opt-in with script checks", "--scripts-typecheck or --full")); diff --git a/scripts/src/deploy.ts b/scripts/src/deploy.ts index 7c7345db..17d92dac 100644 --- a/scripts/src/deploy.ts +++ b/scripts/src/deploy.ts @@ -40,6 +40,7 @@ interface DeployOptions { environment: DeployEnvironment | null; serviceId: string | null; commitOverride: string | null; + providerId: string; runNow: boolean; dryRun: boolean; force: boolean; @@ -221,7 +222,7 @@ export function deployHelp(action: string | undefined = undefined): Record", default: defaultTimeoutMs, description: "Per-step timeout budget where supported." }, + { name: "--provider-id ", default: "D601", description: "Provider used by artifact-registry consumers; use local only from the D601 host CLI." }, { name: "--run-now", description: "Run apply in the foreground worker process; omit it for fire-and-forget async job mode." }, { name: "guard code-queue-source --root ", description: "Validate Code Queue hostPath source relative imports before any scheduler rollout; failures report degradedReason and missing import targets." }, ], @@ -523,6 +525,7 @@ function parseOptions(args: string[]): DeployOptions { environment, serviceId, commitOverride: commitOverride?.toLowerCase() ?? null, + providerId: optionValue(args, ["--provider-id", "--provider"]) ?? "D601", runNow: args.includes("--run-now"), dryRun: args.includes("--dry-run"), force: args.includes("--force"), @@ -2882,6 +2885,7 @@ async function runDevArtifactConsumerService( "--commit", commit, "--source-repo", desired.repo, "--timeout-ms", String(options.timeoutMs), + "--provider-id", options.providerId, "--run-now", ...(options.dryRun ? ["--dry-run"] : []), ]; @@ -3404,6 +3408,7 @@ async function runArtifactConsumerApplyNow( ...(options.dryRun && hasDeployJsonExecutorContract(service) ? ["--deploy-json-service", encodeDeployJsonServiceContract(service)] : []), "--env", environment, "--timeout-ms", String(options.timeoutMs), + "--provider-id", options.providerId, "--run-now", ...(options.dryRun ? ["--dry-run"] : []), ];