From 6ec397549898d22bc9a25137fdaec7ab7bf361b9 Mon Sep 17 00:00:00 2001 From: Sythos Date: Sat, 22 Aug 2026 18:45:42 +0200 Subject: [PATCH 1/6] fix: correct Ubuntu image checksum verification --- Dockerfile | 4 +++- scripts/m0-smoke.ps1 | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8c84167..cf46baf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,7 +43,9 @@ RUN set -eux; \ curl --fail --silent --show-error --location \ "https://nodejs.org/dist/v${NODE_VERSION}/SHASUMS256.txt" \ --output /tmp/SHASUMS256.txt; \ - grep -F " node-v${NODE_VERSION}-linux-${node_arch}.tar.xz" /tmp/SHASUMS256.txt | sha256sum --check --status -; \ + grep -F " node-v${NODE_VERSION}-linux-${node_arch}.tar.xz" /tmp/SHASUMS256.txt | \ + sed "s# node-v${NODE_VERSION}-linux-${node_arch}.tar.xz# /tmp/node.tar.xz#" | \ + sha256sum --check --status -; \ tar --extract --file /tmp/node.tar.xz --directory /usr/local --strip-components=1; \ rm -f /tmp/node.tar.xz /tmp/SHASUMS256.txt; \ apt-get purge -y --auto-remove curl xz-utils; \ diff --git a/scripts/m0-smoke.ps1 b/scripts/m0-smoke.ps1 index 5f0cb36..c8be4de 100644 --- a/scripts/m0-smoke.ps1 +++ b/scripts/m0-smoke.ps1 @@ -43,7 +43,7 @@ function Invoke-Compose { $exitCode = $LASTEXITCODE if ($exitCode -ne 0) { $renderedArguments = $commandArguments -join ' ' - throw "Docker Compose command failed with exit code \${exitCode}: docker $renderedArguments" + throw "Docker Compose command failed with exit code ${exitCode}: docker $renderedArguments" } } @@ -63,7 +63,7 @@ function Get-ComposeOutput { $exitCode = $LASTEXITCODE if ($exitCode -ne 0) { $renderedArguments = $commandArguments -join ' ' - throw "Docker Compose command failed with exit code \${exitCode}: docker $renderedArguments" + throw "Docker Compose command failed with exit code ${exitCode}: docker $renderedArguments" } return @($output) } From 78204355149cb093994ce8ce9c409ef4a49572f7 Mon Sep 17 00:00:00 2001 From: Sythos Date: Sat, 22 Aug 2026 18:47:40 +0200 Subject: [PATCH 2/6] fix: add Node runtime atomic library --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index cf46baf..0dfc5fc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,7 +36,7 @@ RUN set -eux; \ export DEBIAN_FRONTEND=noninteractive; \ apt-get update; \ apt-get upgrade -y; \ - apt-get install -y --no-install-recommends ca-certificates curl xz-utils; \ + apt-get install -y --no-install-recommends ca-certificates curl libatomic1 xz-utils; \ curl --fail --silent --show-error --location \ "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${node_arch}.tar.xz" \ --output /tmp/node.tar.xz; \ From 4471375f569ce2b2dfb9b95a57526b738e769654 Mon Sep 17 00:00:00 2001 From: Sythos Date: Sat, 22 Aug 2026 18:53:03 +0200 Subject: [PATCH 3/6] test: print Compose logs on smoke failure --- scripts/m0-smoke.ps1 | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts/m0-smoke.ps1 b/scripts/m0-smoke.ps1 index c8be4de..64699cf 100644 --- a/scripts/m0-smoke.ps1 +++ b/scripts/m0-smoke.ps1 @@ -104,6 +104,13 @@ function Write-ComposeFailureContext { } $statusArguments += @('ps', '-a') & docker @statusArguments + Write-Host 'Docker Compose logs:' + $logArguments = @('compose', '--project-name', $ProjectName, '--file', $resolvedComposeFile) + if (-not [string]::IsNullOrWhiteSpace($resolvedEnvFile)) { + $logArguments += @('--env-file', $resolvedEnvFile) + } + $logArguments += @('logs', '--no-color', '--tail', '100', 'gulogulo') + & docker @logArguments } catch { Write-Warning "Unable to collect Docker Compose status: $($_.Exception.Message)" From 601fad2b42d416de0e143154f63e9b928c01b610 Mon Sep 17 00:00:00 2001 From: Sythos Date: Sat, 22 Aug 2026 18:57:26 +0200 Subject: [PATCH 4/6] fix: accept the Node process environment in runtime config --- src/foundation/config.test.mjs | 7 +++++++ src/runtime/config.mjs | 14 +++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/foundation/config.test.mjs b/src/foundation/config.test.mjs index 8071166..66ee758 100644 --- a/src/foundation/config.test.mjs +++ b/src/foundation/config.test.mjs @@ -53,6 +53,13 @@ test('configuration defaults are versioned, deterministic, and secret-free', () assert.equal(JSON.stringify(first).includes('secret'), false); }); +test('the production process environment is accepted by the loader', () => { + const configuration = loadConfiguration(process.env, { configFilePath: null }); + + assert.equal(configuration.schemaVersion, CONFIG_SCHEMA_VERSION); + assert.equal(configuration.project.machineName, 'gulogulo'); +}); + test('environment variables override mounted file values with canonical names first', () => { withConfigFile( { diff --git a/src/runtime/config.mjs b/src/runtime/config.mjs index cbe1671..dedd9f1 100644 --- a/src/runtime/config.mjs +++ b/src/runtime/config.mjs @@ -629,15 +629,23 @@ function attachContractMetadata(legacyConfig, fullConfig) { * is mandatory and fails closed when it cannot be read or validated. */ export function loadConfiguration(environment = process.env, options = {}) { - assertObject(environment, 'environment'); + // Node exposes process.env as an environment-backed object rather than a + // normal plain object. Copy the default process environment before schema + // validation so the production entry point and direct test callers use the + // same contract without weakening the configuration-file checks. + const effectiveEnvironment = environment === process.env + ? Object.fromEntries(Object.entries(environment)) + : environment; + + assertObject(effectiveEnvironment, 'environment'); assertObject(options, 'options'); - const resolvedFile = resolveConfigurationFile(environment, options); + const resolvedFile = resolveConfigurationFile(effectiveEnvironment, options); const fileConfiguration = readConfigurationFile(resolvedFile.path, { optional: resolvedFile.optional, readFile: options.readFile, }); - const configuration = buildConfiguration(fileConfiguration, environment); + const configuration = buildConfiguration(fileConfiguration, effectiveEnvironment); return deepFreeze(configuration); } From ee5d643df9d163c785683e206f26231eb670c82c Mon Sep 17 00:00:00 2001 From: Sythos Date: Sat, 22 Aug 2026 19:01:14 +0200 Subject: [PATCH 5/6] fix: allow empty Compose secret references --- src/foundation/config.test.mjs | 13 +++++++++++++ src/runtime/config.mjs | 12 ++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/foundation/config.test.mjs b/src/foundation/config.test.mjs index 66ee758..464bf92 100644 --- a/src/foundation/config.test.mjs +++ b/src/foundation/config.test.mjs @@ -60,6 +60,19 @@ test('the production process environment is accepted by the loader', () => { assert.equal(configuration.project.machineName, 'gulogulo'); }); +test('empty Compose secret-reference placeholders remain unset', () => { + const configuration = loadConfiguration( + { + LDAP_BIND_SECRET_REF: '', + POSTGRES_DSN_SECRET_REF: '', + }, + { configFilePath: null }, + ); + + assert.equal(configuration.ldap.bindSecretRef, null); + assert.equal(configuration.postgres.dsnSecretRef, null); +}); + test('environment variables override mounted file values with canonical names first', () => { withConfigFile( { diff --git a/src/runtime/config.mjs b/src/runtime/config.mjs index dedd9f1..51a6609 100644 --- a/src/runtime/config.mjs +++ b/src/runtime/config.mjs @@ -243,6 +243,14 @@ function readSecretReference(value, name) { return readString(value, name, SECRET_REFERENCE_PATTERN); } +function readEnvironmentOptionalSecretReference(value, name) { + if (value === '') { + return null; + } + + return readSecretReference(value, name); +} + function readDistinguishedName(value, name) { return readString(value, name, DISTINGUISHED_NAME_PATTERN); } @@ -478,7 +486,7 @@ function buildConfiguration(fileConfiguration, environment) { applyEnvironmentValue(target, 'enabled', environment, variables.enabled, readEnvironmentBoolean); applyEnvironmentValue(target, 'url', environment, variables.url, readUrl); applyEnvironmentValue(target, 'startTls', environment, variables.startTls, readEnvironmentBoolean); - applyEnvironmentValue(target, 'bindSecretRef', environment, variables.bindSecretRef, readSecretReference); + applyEnvironmentValue(target, 'bindSecretRef', environment, variables.bindSecretRef, readEnvironmentOptionalSecretReference); applyEnvironmentValue(target, 'userBaseDn', environment, variables.userBaseDn, readDistinguishedName); applyEnvironmentValue(target, 'connectTimeoutMs', environment, variables.connectTimeoutMs, (value, name) => readEnvironmentInteger(value, name, 100, 120_000)); continue; @@ -491,7 +499,7 @@ function buildConfiguration(fileConfiguration, environment) { applyEnvironmentValue(target, 'database', environment, variables.database, (value, name) => readEnvironmentString(value, name, SAFE_NAME_PATTERN)); applyEnvironmentValue(target, 'user', environment, variables.user, (value, name) => readEnvironmentString(value, name, SAFE_NAME_PATTERN)); applyEnvironmentValue(target, 'sslMode', environment, variables.sslMode, (value, name) => readEnvironmentEnum(value, name, ['disable', 'allow', 'prefer', 'require', 'verify-ca', 'verify-full'])); - applyEnvironmentValue(target, 'dsnSecretRef', environment, variables.dsnSecretRef, readSecretReference); + applyEnvironmentValue(target, 'dsnSecretRef', environment, variables.dsnSecretRef, readEnvironmentOptionalSecretReference); applyEnvironmentValue(target, 'connectTimeoutMs', environment, variables.connectTimeoutMs, (value, name) => readEnvironmentInteger(value, name, 100, 120_000)); continue; } From 7e78dd3aa605c60beadd8db1a9a7901742df331b Mon Sep 17 00:00:00 2001 From: Sythos Date: Sat, 22 Aug 2026 19:06:12 +0200 Subject: [PATCH 6/6] fix: derive multiarch Node binaries from target architecture --- Dockerfile | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0dfc5fc..4645113 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ FROM ubuntu:26.04 ARG NODE_VERSION=26.7.0 -ARG TARGETARCH=amd64 +ARG TARGETARCH ENV NODE_ENV=production \ HOST=0.0.0.0 \ @@ -25,13 +25,19 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"] # Ubuntu package manager. TARGETARCH keeps the image usable on the OCI # architectures supported by the Node.js release. RUN set -eux; \ - case "${TARGETARCH}" in \ + debian_arch="$(dpkg --print-architecture)"; \ + target_arch="${TARGETARCH:-${debian_arch}}"; \ + if [ "${target_arch}" != "${debian_arch}" ]; then \ + echo "TARGETARCH ${target_arch} does not match the Ubuntu target architecture ${debian_arch}" >&2; \ + exit 1; \ + fi; \ + case "${target_arch}" in \ amd64) node_arch='x64' ;; \ arm64) node_arch='arm64' ;; \ arm) node_arch='armv7l' ;; \ ppc64le) node_arch='ppc64le' ;; \ s390x) node_arch='s390x' ;; \ - *) echo "Unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + *) echo "Unsupported TARGETARCH: ${target_arch}" >&2; exit 1 ;; \ esac; \ export DEBIAN_FRONTEND=noninteractive; \ apt-get update; \