File size: 7,717 Bytes
b410f5c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
#!/usr/bin/env node
/**
 * Build n8n and runners Docker images locally
 *
 * This script simulates the CI build process for local testing.
 * Default output: 'n8nio/n8n:local' and 'n8nio/runners:local'
 * Override with IMAGE_BASE_NAME and IMAGE_TAG environment variables.
 */

import { $, echo, fs, chalk, os } from 'zx';
import { fileURLToPath } from 'url';
import path from 'path';

// Disable verbose mode for cleaner output
$.verbose = false;
process.env.FORCE_COLOR = '1';

// #region ===== Helper Functions =====

/**
 * Get Docker platform string based on host architecture
 * @returns {string} Platform string (e.g., 'linux/amd64')
 */
function getDockerPlatform() {
	const arch = os.arch();
	const dockerArch = {
		x64: 'amd64',
		arm64: 'arm64',
	}[arch];

	if (!dockerArch) {
		throw new Error(`Unsupported architecture: ${arch}. Only x64 and arm64 are supported.`);
	}

	return `linux/${dockerArch}`;
}

/**
 * Format duration in seconds
 * @param {number} ms - Duration in milliseconds
 * @returns {string} Formatted duration
 */
function formatDuration(ms) {
	return `${Math.floor(ms / 1000)}s`;
}

/**
 * Get Docker image size
 * @param {string} imageName - Full image name with tag
 * @returns {Promise<string>} Image size or 'Unknown'
 */
async function getImageSize(imageName) {
	try {
		const { stdout } = await $`docker images ${imageName} --format "{{.Size}}"`;
		return stdout.trim();
	} catch {
		return 'Unknown';
	}
}

/**
 * Check if a command exists
 * @param {string} command - Command to check
 * @returns {Promise<boolean>} True if command exists
 */
async function commandExists(command) {
	try {
		await $`command -v ${command}`;
		return true;
	} catch {
		return false;
	}
}

const SupportedContainerEngines = /** @type {const} */ (['docker', 'podman']);

/**
 * Detect if the local `docker` CLI is actually Podman via the docker shim.
 * @returns {Promise<boolean>}
 */
async function isDockerPodmanShim() {
	try {
		const { stdout } = await $`docker version`;
		return stdout.toLowerCase().includes('podman');
	} catch {
		return false;
	}
}
/**
 * @returns {Promise<(typeof SupportedContainerEngines[number])>}
 */
async function getContainerEngine() {
	// Allow explicit override via env var
	const override = process.env.CONTAINER_ENGINE?.toLowerCase();
	if (override && /** @type {readonly string[]} */ (SupportedContainerEngines).includes(override)) {
		return /** @type {typeof SupportedContainerEngines[number]} */ (override);
	}

	const hasDocker = await commandExists('docker');
	const hasPodman = await commandExists('podman');

	if (hasDocker) {
		// If docker is actually a Podman shim, use podman path to avoid unsupported flags like --load
		if (hasPodman && (await isDockerPodmanShim())) {
			return 'podman';
		}
		return 'docker';
	}

	if (hasPodman) return 'podman';

	throw new Error('No supported container engine found. Please install Docker or Podman.');
}

// #endregion ===== Helper Functions =====

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const isInScriptsDir = path.basename(__dirname) === 'scripts';
const rootDir = isInScriptsDir ? path.join(__dirname, '..') : __dirname;

const config = {
	n8n: {
		dockerfilePath: path.join(rootDir, 'docker/images/n8n/Dockerfile'),
		imageBaseName: process.env.IMAGE_BASE_NAME || 'n8nio/n8n',
		imageTag: process.env.IMAGE_TAG || 'local',
		get fullImageName() {
			return `${this.imageBaseName}:${this.imageTag}`;
		},
	},
	runners: {
		dockerfilePath: path.join(rootDir, 'docker/images/runners/Dockerfile'),
		imageBaseName: process.env.RUNNERS_IMAGE_BASE_NAME || 'n8nio/runners',
		get imageTag() {
			// Runners use the same tag as n8n for consistency
			return config.n8n.imageTag;
		},
		get fullImageName() {
			return `${this.imageBaseName}:${this.imageTag}`;
		},
	},
	buildContext: rootDir,
	compiledAppDir: path.join(rootDir, 'compiled'),
	compiledTaskRunnerDir: path.join(rootDir, 'dist', 'task-runner-javascript'),
};

// #region ===== Main Build Process =====

const platform = getDockerPlatform();

async function main() {
	echo(chalk.blue.bold('===== Docker Build for n8n & Runners ====='));
	echo(`INFO: n8n Image: ${config.n8n.fullImageName}`);
	echo(`INFO: Runners Image: ${config.runners.fullImageName}`);
	echo(`INFO: Platform: ${platform}`);
	echo(chalk.gray('-'.repeat(47)));

	await checkPrerequisites();

	// Build n8n Docker image
	const n8nBuildTime = await buildDockerImage({
		name: 'n8n',
		dockerfilePath: config.n8n.dockerfilePath,
		fullImageName: config.n8n.fullImageName,
	});

	// Build runners Docker image
	const runnersBuildTime = await buildDockerImage({
		name: 'runners',
		dockerfilePath: config.runners.dockerfilePath,
		fullImageName: config.runners.fullImageName,
	});

	// Get image details
	const n8nImageSize = await getImageSize(config.n8n.fullImageName);
	const runnersImageSize = await getImageSize(config.runners.fullImageName);

	// Display summary
	displaySummary([
		{
			imageName: config.n8n.fullImageName,
			platform,
			size: n8nImageSize,
			buildTime: n8nBuildTime,
		},
		{
			imageName: config.runners.fullImageName,
			platform,
			size: runnersImageSize,
			buildTime: runnersBuildTime,
		},
	]);
}

async function checkPrerequisites() {
	if (!(await fs.pathExists(config.compiledAppDir))) {
		echo(chalk.red(`Error: Compiled app directory not found at ${config.compiledAppDir}`));
		echo(chalk.yellow('Please run build-n8n.mjs first!'));
		process.exit(1);
	}

	if (!(await fs.pathExists(config.compiledTaskRunnerDir))) {
		echo(chalk.red(`Error: Task runner directory not found at ${config.compiledTaskRunnerDir}`));
		echo(chalk.yellow('Please run build-n8n.mjs first!'));
		process.exit(1);
	}

	// Ensure at least one supported container engine is available
	if (!(await commandExists('docker')) && !(await commandExists('podman'))) {
		echo(chalk.red('Error: Neither Docker nor Podman is installed or in PATH'));
		process.exit(1);
	}
}

async function buildDockerImage({ name, dockerfilePath, fullImageName }) {
	const startTime = Date.now();
	const containerEngine = await getContainerEngine();
	echo(chalk.yellow(`INFO: Building ${name} Docker image using ${containerEngine}...`));

	try {
		if (containerEngine === 'podman') {
			const { stdout } = await $`podman build \
				--platform ${platform} \
				--build-arg TARGETPLATFORM=${platform} \
				-t ${fullImageName} \
				-f ${dockerfilePath} \
				${config.buildContext}`;
			echo(stdout);
		} else {
			// Use docker buildx build to leverage Blacksmith's layer caching when running in CI.
			// The setup-docker-builder action creates a buildx builder with sticky disk cache.
			const { stdout } = await $`docker buildx build \
				--platform ${platform} \
				--build-arg TARGETPLATFORM=${platform} \
				-t ${fullImageName} \
				-f ${dockerfilePath} \
				--load \
				${config.buildContext}`;
			echo(stdout);
		}

		return formatDuration(Date.now() - startTime);
	} catch (error) {
		echo(chalk.red(`ERROR: ${name} Docker build failed: ${error.stderr || error.message}`));
		process.exit(1);
	}
}

function displaySummary(images) {
	echo('');
	echo(chalk.green.bold('═'.repeat(54)));
	echo(chalk.green.bold('           DOCKER BUILD COMPLETE'));
	echo(chalk.green.bold('═'.repeat(54)));
	for (const { imageName, platform, size, buildTime } of images) {
		echo(chalk.green(`✅ Image built: ${imageName}`));
		echo(`   Platform: ${platform}`);
		echo(`   Size: ${size}`);
		echo(`   Build time: ${buildTime}`);
		echo('');
	}
	echo(chalk.green.bold('═'.repeat(54)));
}

// #endregion ===== Main Build Process =====

main().catch((error) => {
	echo(chalk.red(`Unexpected error: ${error.message}`));
	process.exit(1);
});