ci: Exercise the runners images in the Docker smoke test (no-changelog) (#37118)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mike Repeć
2026-08-27 08:31:06 +00:00
committed by GitHub
parent ad24a481c2
commit 35969d93c5
3 changed files with 76 additions and 3 deletions
+19 -2
View File
@@ -13,6 +13,11 @@ env:
# Node. dockerize-n8n.mjs reads this env for the image builds. Keep in sync with
# NODE_VERSION in docker-build-push.yml, which is what the published images use.
NODE_VERSION: '26.7.0'
# Also build the distroless runners image and exec-check
# both runners images' interpreters, so a broken runtime assembly (e.g. a
# missing shared library after a Node bump) fails at PR time. The smoke
# script derives the images to check from this same flag.
DOCKER_BUILD_DISTROLESS: 'true'
on:
schedule:
@@ -33,8 +38,20 @@ concurrency:
jobs:
docker-smoke-test:
name: 'Docker Build (no cache)'
runs-on: blacksmith-4vcpu-ubuntu-2204
name: 'Docker Build (no cache, ${{ matrix.platform }})'
# Both architectures build natively: the runtime images assemble binaries
# across images with arch-specific paths (e.g. the ELF interpreter lives at
# /lib/ld-linux-aarch64.so.1 only on arm64), so amd64 alone can miss
# arm64-only runtime breaks.
strategy:
fail-fast: false
matrix:
include:
- platform: amd64
runner: blacksmith-4vcpu-ubuntu-2204
- platform: arm64
runner: blacksmith-8vcpu-ubuntu-2204-arm
runs-on: ${{ matrix.runner }}
if: ${{ !github.event.pull_request.head.repo.fork }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+23
View File
@@ -142,6 +142,8 @@ const rootDir = isInScriptsDir ? path.join(__dirname, '..') : __dirname;
const noCache = process.env.DOCKER_BUILD_NO_CACHE === 'true';
const withBaseImage = process.env.DOCKER_BUILD_BASE_IMAGE === 'true';
// Opt-in: only cloud deploys the distroless runners image, so local builds skip it.
const withDistroless = process.env.DOCKER_BUILD_DISTROLESS === 'true';
// Keep in sync with NODE_VERSION in .github/workflows/docker-build-push.yml,
// which is what the published images are actually built with.
const nodeVersion = process.env.NODE_VERSION || '26.7.0';
@@ -172,6 +174,12 @@ const config = {
return `${this.imageBaseName}:${this.imageTag}`;
},
},
runnersDistroless: {
dockerfilePath: path.join(rootDir, 'docker/images/runners/Dockerfile.distroless'),
get fullImageName() {
return `${config.runners.fullImageName}-distroless`;
},
},
buildContext: rootDir,
compiledAppDir: path.join(rootDir, 'compiled'),
compiledTaskRunnerDir: path.join(rootDir, 'dist', 'task-runner-javascript'),
@@ -236,6 +244,21 @@ async function main() {
},
];
if (withDistroless) {
const buildTime = await buildDockerImage({
name: 'runners-distroless',
dockerfilePath: config.runnersDistroless.dockerfilePath,
fullImageName: config.runnersDistroless.fullImageName,
buildArgs: nodeVersionArgs,
});
imageStats.push({
imageName: config.runnersDistroless.fullImageName,
platform,
size: await getImageSize(config.runnersDistroless.fullImageName),
buildTime,
});
}
// Write docker build manifest for telemetry collection
const dockerManifest = {
buildTime: new Date().toISOString(),
+34 -1
View File
@@ -11,6 +11,12 @@ $.verbose = false;
process.env.FORCE_COLOR = '1';
const IMAGE = process.env.SMOKE_IMAGE || 'n8nio/n8n:local';
// Runners images to exec-check. Tracks DOCKER_BUILD_DISTROLESS so the same
// flag drives both build and check.
const RUNNERS_IMAGES = [
'n8nio/runners:local',
...(process.env.DOCKER_BUILD_DISTROLESS === 'true' ? ['n8nio/runners:local-distroless'] : []),
];
const TIMEOUT = '45s';
// Matches an n8n runtime image ref (e.g. `n8nio/n8n:2.4.4`, `ghcr.io/n8n-io/n8n@sha256:…`)
// but not sidecars like `n8nio/runners:…` or controller images that happen to contain "n8n".
@@ -120,6 +126,27 @@ async function runWorkspaceDedupCheck() {
}
}
// Interpreter paths as launched by docker/images/runners/n8n-task-runners.json.
// The runners images assemble node/python by copying binaries across images, so a
// missing shared library only surfaces at exec time.
const RUNNER_INTERPRETERS = [
['/usr/local/bin/node', '--version'],
['/opt/runners/task-runner-python/.venv/bin/python', '--version'],
];
async function runRunnersInterpreterCheck(image) {
const name = `runner interpreters exec in ${image}`;
try {
for (const [entrypoint, arg] of RUNNER_INTERPRETERS) {
await $({ timeout: TIMEOUT })`docker run --rm --entrypoint ${entrypoint} ${image} ${arg}`;
}
echo(chalk.green(`${name}`));
return true;
} catch (err) {
return reportFailure(name, err);
}
}
async function run({ name, user, entrypoint, args }) {
const dockerArgs = ['run', '--rm', '--user', `${user}:${user}`];
if (entrypoint) dockerArgs.push('--entrypoint', entrypoint);
@@ -152,5 +179,11 @@ const invocations = [
];
echo(chalk.bold(`Verifying ${IMAGE} against ${invocations.length} deployment pattern(s)`));
const ok = (await Promise.all([...invocations.map(run), runWorkspaceDedupCheck()])).every(Boolean);
const ok = (
await Promise.all([
...invocations.map(run),
runWorkspaceDedupCheck(),
...RUNNERS_IMAGES.map(runRunnersInterpreterCheck),
])
).every(Boolean);
if (!ok) process.exit(1);