fix: use correct platform when creating remote buildx builder

The remote builder was hardcoded to use --platform linux/amd64
regardless of user input or runner architecture. This caused
performance issues on ARM runners and cache inefficiencies.

Now properly uses the platforms input or detects host architecture
to avoid unnecessary QEMU emulation and improve build performance.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude 2025-06-11 12:59:55 -04:00
parent 6fe3b1c366
commit a7fa33c366
8 changed files with 594 additions and 488 deletions

984
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -40,7 +40,7 @@
},
"devDependencies": {
"@babel/core": "^7.26.10",
"@babel/preset-env": "^7.26.9",
"@babel/preset-env": "^7.27.2",
"@babel/preset-typescript": "^7.27.0",
"@types/jest": "^29.5.14",
"@types/node": "^20.12.12",

View File

@ -9,4 +9,4 @@ export class Metric {
type: Metric_MetricType = Metric_MetricType.UNSPECIFIED;
value: number = 0;
labels: Record<string, string> = {};
}
}

View File

@ -1,23 +1,23 @@
export class StickyDiskService {
constructor() {}
async commitStickyDisk() {
return {};
}
async getStickyDisk() {
return {};
}
async queueDockerJob() {
return {};
}
async reportMetric() {
return {};
}
async up() {
return {};
}
}
}

View File

@ -4,4 +4,4 @@ export const execa = jest.fn().mockImplementation(() => {
stderr: '',
exitCode: 0
};
});
});

View File

@ -0,0 +1,41 @@
import * as os from 'os';
import {getRemoteBuilderArgs, resolveRemoteBuilderPlatforms} from '../context';
jest.mock('@actions/core', () => ({
info: jest.fn(),
debug: jest.fn(),
warning: jest.fn(),
error: jest.fn()
}));
describe('Remote builder platform argument resolution', () => {
const builderName = 'test-builder';
const builderUrl = 'tcp://127.0.0.1:1234';
afterEach(() => {
jest.restoreAllMocks();
});
test('returns comma-separated list when platforms are supplied', async () => {
const platforms = ['linux/arm64', 'linux/amd64'];
const platformStr = resolveRemoteBuilderPlatforms(platforms);
expect(platformStr).toBe('linux/arm64,linux/amd64');
const args = await getRemoteBuilderArgs(builderName, builderUrl, platforms);
const idx = args.indexOf('--platform');
expect(idx).toBeGreaterThan(-1);
expect(args[idx + 1]).toBe('linux/arm64,linux/amd64');
});
test('falls back to host architecture when no platforms supplied', async () => {
jest.spyOn(os, 'arch').mockReturnValue('arm64' as any);
const platformStr = resolveRemoteBuilderPlatforms([]);
expect(platformStr).toBe('linux/arm64');
const args = await getRemoteBuilderArgs(builderName, builderUrl, []);
const idx = args.indexOf('--platform');
expect(idx).toBeGreaterThan(-1);
expect(args[idx + 1]).toBe('linux/arm64');
});
});

View File

@ -1,6 +1,6 @@
import * as core from '@actions/core';
import * as handlebars from 'handlebars';
import * as fs from 'fs';
import * as os from 'os';
import {Build} from '@docker/actions-toolkit/lib/buildx/build';
import {Context} from '@docker/actions-toolkit/lib/context';
@ -336,12 +336,39 @@ export const tlsClientKeyPath = '/tmp/blacksmith_client_key.pem';
export const tlsClientCaCertificatePath = '/tmp/blacksmith_client_ca_certificate.pem';
export const tlsRootCaCertificatePath = '/tmp/blacksmith_root_ca_certificate.pem';
export async function getRemoteBuilderArgs(name: string, builderUrl: string): Promise<Array<string>> {
/**
* Resolve the platform list that should be passed to `docker buildx create`.
*
* Priority:
* 1. Use the user-supplied platforms list (comma-joined) if provided.
* 2. Fallback to the architecture of the host runner.
*
* The function is exported to allow isolated unit testing.
*/
export function resolveRemoteBuilderPlatforms(platforms?: string[]): string {
// If user explicitly provided platforms, honour them verbatim.
if (platforms && platforms.length > 0) {
return platforms.join(',');
}
// Otherwise derive from host architecture.
const nodeArch = os.arch(); // e.g. 'x64', 'arm64', 'arm'
const archMap: {[key: string]: string} = {
x64: 'amd64',
arm64: 'arm64',
arm: 'arm'
};
const mappedArch = archMap[nodeArch] || nodeArch;
return `linux/${mappedArch}`;
}
export async function getRemoteBuilderArgs(name: string, builderUrl: string, platforms?: string[]): Promise<Array<string>> {
const args: Array<string> = ['create', '--name', name, '--driver', 'remote'];
// TODO(aayush): Instead of hardcoding the platform, we should fail the build if the platform is
// unsupported.
args.push('--platform', 'linux/amd64');
const platformFlag = resolveRemoteBuilderPlatforms(platforms);
core.info(`Determined remote builder platform(s): ${platformFlag}`);
args.push('--platform', platformFlag);
// Always use the remote builder, overriding whatever has been configured so far.
args.push('--use');
// Use the provided builder URL

View File

@ -202,7 +202,7 @@ actionsToolkit.run(
if (builderInfo.addr) {
await core.group(`Creating a builder instance`, async () => {
const name = `blacksmith-${Date.now().toString(36)}`;
const createCmd = await toolkit.buildx.getCommand(await context.getRemoteBuilderArgs(name, builderInfo.addr!));
const createCmd = await toolkit.buildx.getCommand(await context.getRemoteBuilderArgs(name, builderInfo.addr!, inputs.platforms));
core.info(`Creating builder with command: ${createCmd.command}`);
await Exec.getExecOutput(createCmd.command, createCmd.args, {
ignoreReturnCode: true