mirror of
https://github.com/n8n-io/n8n.git
synced 2026-08-30 18:01:23 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user