diff --git a/config/agentrun.yaml b/config/agentrun.yaml index 7efbe8fd..f8a5417a 100644 --- a/config/agentrun.yaml +++ b/config/agentrun.yaml @@ -95,6 +95,8 @@ controlPlane: containerfile: deploy/container/Containerfile repository: agentrun-mgr-env network: host + buildArgs: + BUN_IMAGE: oven/bun:1.2.15-alpine httpProxy: http://127.0.0.1:10808 httpsProxy: http://127.0.0.1:10808 noProxy: @@ -225,6 +227,8 @@ controlPlane: containerfile: deploy/container/Containerfile repository: agentrun-mgr-env network: host + buildArgs: + BUN_IMAGE: oven/bun:1-alpine httpProxy: http://127.0.0.1:18789 httpsProxy: http://127.0.0.1:18789 noProxy: diff --git a/scripts/src/agentrun-lanes.ts b/scripts/src/agentrun-lanes.ts index f4014c74..b97fa38c 100644 --- a/scripts/src/agentrun-lanes.ts +++ b/scripts/src/agentrun-lanes.ts @@ -140,6 +140,7 @@ export interface AgentRunImageBuildSpec { readonly containerfile: string; readonly repository: string; readonly network: string; + readonly buildArgs: Readonly>; readonly httpProxy: string | null; readonly httpsProxy: string | null; readonly noProxy: readonly string[]; @@ -247,6 +248,7 @@ export function agentRunLaneSummary(spec: AgentRunLaneSpec): Record, path: string): AgentRun containerfile: relativePathField(input, "containerfile", path), repository: stringField(input, "repository", path), network: stringField(input, "network", path), + buildArgs: stringRecordField(recordField(input, "buildArgs", path), `${path}.buildArgs`), httpProxy: optionalStringField(input, "httpProxy", path) ?? null, httpsProxy: optionalStringField(input, "httpsProxy", path) ?? null, noProxy: stringArrayField(input, "noProxy", path), @@ -631,6 +634,16 @@ function stringArrayField(obj: Record, key: string, path: strin }); } +function stringRecordField(obj: Record, path: string): Readonly> { + const result: Record = {}; + for (const [key, value] of Object.entries(obj)) { + if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) throw new Error(`${path}.${key} must be a valid build arg name`); + if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${path}.${key} must be a non-empty string`); + result[key] = value.trim(); + } + return result; +} + function enumField(obj: Record, key: string, path: string, values: readonly T[]): T { const value = stringField(obj, key, path); if (!values.includes(value as T)) throw new Error(`${path}.${key} must be one of ${values.join(", ")}`); diff --git a/scripts/src/agentrun.ts b/scripts/src/agentrun.ts index 7ccfdae2..20ff17df 100644 --- a/scripts/src/agentrun.ts +++ b/scripts/src/agentrun.ts @@ -2674,6 +2674,7 @@ async function triggerCurrentYamlLane(config: UniDeskConfig, options: TriggerOpt imageBuild: { repository: `${spec.ci.registryPrefix}/${spec.deployment.manager.imageBuild.repository}`, containerfile: spec.deployment.manager.imageBuild.containerfile, + buildArgNames: Object.keys(spec.deployment.manager.imageBuild.buildArgs).sort(), timeoutSeconds: spec.deployment.manager.imageBuild.timeoutSeconds, pollSeconds: spec.deployment.manager.imageBuild.pollSeconds, proxyConfigured: spec.deployment.manager.imageBuild.httpProxy !== null || spec.deployment.manager.imageBuild.httpsProxy !== null, @@ -3296,6 +3297,9 @@ function yamlLaneBuildImageSubmitScript(spec: AgentRunLaneSpec, sourceCommit: st const noProxy = build.noProxy.join(","); const imageRepository = `${spec.ci.registryPrefix}/${build.repository}`; const stateDir = `/tmp/unidesk-agentrun-build-${spec.nodeId}-${spec.lane}`; + const buildArgs = Object.entries(build.buildArgs) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, value]) => `${key}=${value}`); const script = [ "set -eu", `workspace=${shQuote(spec.source.workspace)}`, @@ -3309,14 +3313,17 @@ function yamlLaneBuildImageSubmitScript(spec: AgentRunLaneSpec, sourceCommit: st `https_proxy_value=${build.httpsProxy === null ? "''" : shQuote(build.httpsProxy)}`, `no_proxy_value=${shQuote(noProxy)}`, `env_identity_files=${shQuote(JSON.stringify(build.envIdentityFiles))}`, + `build_args_json=${shQuote(JSON.stringify(buildArgs))}`, "mkdir -p \"$state_dir\"", "cd \"$workspace\"", "git checkout \"$source_commit\"", - "env_identity=$(ENV_IDENTITY_FILES=\"$env_identity_files\" node <<'NODE'", + "env_identity=$(ENV_IDENTITY_FILES=\"$env_identity_files\" BUILD_ARGS_JSON=\"$build_args_json\" node <<'NODE'", "const { createHash } = require('node:crypto');", "const { readFileSync, existsSync } = require('node:fs');", "const files = JSON.parse(process.env.ENV_IDENTITY_FILES || '[]');", + "const buildArgs = JSON.parse(process.env.BUILD_ARGS_JSON || '[]');", "const hash = createHash('sha256');", + "for (const item of buildArgs) { hash.update('build-arg'); hash.update('\\0'); hash.update(item); hash.update('\\0'); }", "for (const file of files) { hash.update(file); hash.update('\\0'); if (existsSync(file)) hash.update(readFileSync(file)); hash.update('\\0'); }", "process.stdout.write(hash.digest('hex').slice(0, 24));", "NODE", @@ -3343,6 +3350,14 @@ function yamlLaneBuildImageSubmitScript(spec: AgentRunLaneSpec, sourceCommit: st " if [ -n \"$http_proxy_value\" ]; then args=\"$args --build-arg HTTP_PROXY=$http_proxy_value --build-arg http_proxy=$http_proxy_value\"; fi", " if [ -n \"$https_proxy_value\" ]; then args=\"$args --build-arg HTTPS_PROXY=$https_proxy_value --build-arg https_proxy=$https_proxy_value\"; fi", " if [ -n \"$no_proxy_value\" ]; then args=\"$args --build-arg NO_PROXY=$no_proxy_value --build-arg no_proxy=$no_proxy_value\"; fi", + " build_arg_values=$(BUILD_ARGS_JSON=\"$build_args_json\" node <<'NODE'", + "const values = JSON.parse(process.env.BUILD_ARGS_JSON || '[]');", + "for (const value of values) console.log(value);", + "NODE", + " )", + " while IFS= read -r build_arg_value; do [ -n \"$build_arg_value\" ] && args=\"$args --build-arg $build_arg_value\"; done </dev/null 2>&1; then build_status=reused; else docker build $args -f \"$containerfile\" -t \"$image\" \"$context_dir\"; build_status=built; fi", " docker push \"$image\"", " digest=$(docker inspect --format='{{index .RepoDigests 0}}' \"$image\" 2>/dev/null | sed 's/^.*@//' || true)",