mirror of
https://github.com/docker/build-push-action.git
synced 2025-08-10 02:22:11 +00:00
refactor: Remove buildkit management from build-push-action
- Remove buildkitd startup and configuration logic - Remove buildkitd shutdown and cleanup from both main and post actions - Remove buildkitd-related imports and helper functions - Update startBlacksmithBuilder to check for existing builder from setup-docker-builder - Keep sticky disk setup and build reporting functionality intact BREAKING CHANGE: This action now requires setup-docker-builder to be run first to manage the Docker builder lifecycle
This commit is contained in:
parent
ac765fe619
commit
7894682343
30
dist/index.js
generated
vendored
30
dist/index.js
generated
vendored
File diff suppressed because one or more lines are too long
2
dist/index.js.map
generated
vendored
2
dist/index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
78
plan.md
Normal file
78
plan.md
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
# Build-Push-Action Refactoring Plan
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Split the current `useblacksmith/build-push-action` into two separate actions:
|
||||||
|
1. `useblacksmith/setup-docker-builder` - Manages buildkitd lifecycle and stickydisk
|
||||||
|
2. `useblacksmith/build-push-action` - Focuses on Docker builds and metrics reporting
|
||||||
|
|
||||||
|
## Current Problems
|
||||||
|
- The existing action supports two modes: "setup-only" and normal mode
|
||||||
|
- Complex logic to manage buildkitd lifecycle across multiple invocations
|
||||||
|
- Buildkitd must be shut down after each build to support multiple builds in one job
|
||||||
|
- Post-action steps run in reverse order, complicating cleanup
|
||||||
|
|
||||||
|
## Proposed Architecture
|
||||||
|
|
||||||
|
### useblacksmith/setup-docker-builder
|
||||||
|
**Responsibilities:**
|
||||||
|
- Start buildkitd once per workflow job
|
||||||
|
- Mount stickydisk at `/var/lib/buildkit` for shared Docker layer cache
|
||||||
|
- Handle all cleanup, shutdown, and commit logic in post-action
|
||||||
|
- Manage the entire buildkitd lifecycle
|
||||||
|
|
||||||
|
**Key Features:**
|
||||||
|
- Single buildkitd instance for entire job
|
||||||
|
- All stickydisk logic centralized here
|
||||||
|
- Post-action handles:
|
||||||
|
- Buildkitd shutdown
|
||||||
|
- Stickydisk commit (conditional based on build success)
|
||||||
|
- Cleanup
|
||||||
|
|
||||||
|
### useblacksmith/build-push-action
|
||||||
|
**Responsibilities:**
|
||||||
|
- Execute Docker builds against running buildkitd
|
||||||
|
- Report build metrics to control plane
|
||||||
|
- No buildkitd lifecycle management
|
||||||
|
|
||||||
|
**Key Features:**
|
||||||
|
- Simplified logic - just build and push
|
||||||
|
- Can be invoked multiple times in same job
|
||||||
|
- Focuses on Docker operations and telemetry
|
||||||
|
|
||||||
|
## Usage Patterns
|
||||||
|
|
||||||
|
### Multiple Dockerfiles
|
||||||
|
```yaml
|
||||||
|
- uses: useblacksmith/setup-docker-builder
|
||||||
|
- uses: useblacksmith/build-push-action # dockerfile 1
|
||||||
|
- uses: useblacksmith/build-push-action # dockerfile 2
|
||||||
|
- uses: useblacksmith/build-push-action # dockerfile 3
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Docker Commands
|
||||||
|
```yaml
|
||||||
|
- uses: useblacksmith/setup-docker-builder
|
||||||
|
- run: docker bake
|
||||||
|
- run: # other custom docker commands
|
||||||
|
```
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
1. How can the post-action of `setup-docker-builder` access build results from `build-push-action` invocations?
|
||||||
|
- Need to determine if stickydisk should be committed based on build success
|
||||||
|
- Possible solutions:
|
||||||
|
- Environment variables
|
||||||
|
- File-based communication
|
||||||
|
- GitHub Actions outputs/state
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
1. **Cleaner separation of concerns** - Setup vs build logic separated
|
||||||
|
2. **Simpler maintenance** - Each action has focused responsibility
|
||||||
|
3. **Better user experience** - One buildkitd instance regardless of build count
|
||||||
|
4. **More flexible** - Users can mix our build action with custom Docker commands
|
||||||
|
|
||||||
|
## Migration Path
|
||||||
|
1. Create new `setup-docker-builder` repository/action
|
||||||
|
2. Move buildkitd setup and stickydisk logic from current action
|
||||||
|
3. Refactor `build-push-action` to remove setup logic
|
||||||
|
4. Update documentation and examples
|
||||||
|
5. Provide migration guide for existing users
|
192
src/main.ts
192
src/main.ts
@ -20,7 +20,7 @@ import * as context from './context';
|
|||||||
import {promisify} from 'util';
|
import {promisify} from 'util';
|
||||||
import {exec} from 'child_process';
|
import {exec} from 'child_process';
|
||||||
import * as reporter from './reporter';
|
import * as reporter from './reporter';
|
||||||
import {setupStickyDisk, startAndConfigureBuildkitd, getNumCPUs, leaveTailnet, pruneBuildkitCache} from './setup_builder';
|
import {setupStickyDisk, leaveTailnet} from './setup_builder';
|
||||||
import {Metric_MetricType} from '@buf/blacksmith_vm-agent.bufbuild_es/stickydisk/v1/stickydisk_pb';
|
import {Metric_MetricType} from '@buf/blacksmith_vm-agent.bufbuild_es/stickydisk/v1/stickydisk_pb';
|
||||||
|
|
||||||
const DEFAULT_BUILDX_VERSION = 'v0.23.0';
|
const DEFAULT_BUILDX_VERSION = 'v0.23.0';
|
||||||
@ -110,23 +110,25 @@ export async function startBlacksmithBuilder(inputs: context.Inputs): Promise<{a
|
|||||||
const stickyDiskSetup = await setupStickyDisk(dockerfilePath || '', inputs.setupOnly);
|
const stickyDiskSetup = await setupStickyDisk(dockerfilePath || '', inputs.setupOnly);
|
||||||
const stickyDiskDurationMs = Date.now() - stickyDiskStartTime;
|
const stickyDiskDurationMs = Date.now() - stickyDiskStartTime;
|
||||||
await reporter.reportMetric(Metric_MetricType.BPA_HOTLOAD_DURATION_MS, stickyDiskDurationMs);
|
await reporter.reportMetric(Metric_MetricType.BPA_HOTLOAD_DURATION_MS, stickyDiskDurationMs);
|
||||||
const parallelism = await getNumCPUs();
|
|
||||||
|
// For now, we'll check if a builder is already available from setup-docker-builder
|
||||||
const buildkitdStartTime = Date.now();
|
// by looking for the sentinel file
|
||||||
const buildkitdAddr = await startAndConfigureBuildkitd(parallelism, inputs.setupOnly, inputs.platforms);
|
const sentinelPath = path.join('/tmp', 'builder-setup-complete');
|
||||||
const buildkitdDurationMs = Date.now() - buildkitdStartTime;
|
const builderAvailable = fs.existsSync(sentinelPath);
|
||||||
await reporter.reportMetric(Metric_MetricType.BPA_BUILDKITD_READY_DURATION_MS, buildkitdDurationMs);
|
|
||||||
|
if (!builderAvailable) {
|
||||||
|
throw new Error('Docker builder not available. Please use setup-docker-builder action first.');
|
||||||
|
}
|
||||||
|
|
||||||
stateHelper.setExposeId(stickyDiskSetup.exposeId);
|
stateHelper.setExposeId(stickyDiskSetup.exposeId);
|
||||||
return {addr: buildkitdAddr, buildId: stickyDiskSetup.buildId || null, exposeId: stickyDiskSetup.exposeId};
|
// Return null for addr since we're not managing the builder anymore
|
||||||
|
return {addr: null, buildId: stickyDiskSetup.buildId || null, exposeId: stickyDiskSetup.exposeId};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If the builder setup fails for any reason, we check if we should fallback to a local build.
|
// If the builder setup fails for any reason, we check if we should fallback to a local build.
|
||||||
// If we should not fallback, we rethrow the error and fail the build.
|
// If we should not fallback, we rethrow the error and fail the build.
|
||||||
await reporter.reportBuildPushActionFailure(error, 'starting blacksmith builder');
|
await reporter.reportBuildPushActionFailure(error, 'starting blacksmith builder');
|
||||||
|
|
||||||
let errorMessage = `Error during Blacksmith builder setup: ${error.message}`;
|
let errorMessage = `Error during Blacksmith builder setup: ${error.message}`;
|
||||||
if (error.message.includes('buildkitd')) {
|
|
||||||
errorMessage = `Error during buildkitd setup: ${error.message}`;
|
|
||||||
}
|
|
||||||
if (inputs.nofallback) {
|
if (inputs.nofallback) {
|
||||||
core.warning(`${errorMessage}. Failing the build because nofallback is set.`);
|
core.warning(`${errorMessage}. Failing the build because nofallback is set.`);
|
||||||
throw error;
|
throw error;
|
||||||
@ -195,66 +197,25 @@ actionsToolkit.run(
|
|||||||
let buildDurationSeconds: string | undefined;
|
let buildDurationSeconds: string | undefined;
|
||||||
let ref: string | undefined;
|
let ref: string | undefined;
|
||||||
try {
|
try {
|
||||||
await core.group(`Starting Blacksmith builder`, async () => {
|
await core.group(`Setting up Blacksmith build tracking`, async () => {
|
||||||
builderInfo = await startBlacksmithBuilder(inputs);
|
builderInfo = await startBlacksmithBuilder(inputs);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (builderInfo.addr) {
|
// Check that a builder is available (either from setup-docker-builder or existing)
|
||||||
await core.group(`Creating a builder instance`, async () => {
|
await core.group(`Checking for configured builder`, async () => {
|
||||||
const name = `blacksmith-${Date.now().toString(36)}`;
|
try {
|
||||||
const createCmd = await toolkit.buildx.getCommand(await context.getRemoteBuilderArgs(name, builderInfo.addr!, inputs.platforms));
|
const builder = await toolkit.builder.inspect();
|
||||||
core.info(`Creating builder with command: ${createCmd.command}`);
|
if (builder) {
|
||||||
await Exec.getExecOutput(createCmd.command, createCmd.args, {
|
core.info(`Found configured builder: ${builder.name}`);
|
||||||
ignoreReturnCode: true
|
} else {
|
||||||
}).then(res => {
|
core.setFailed(`No Docker builder found. Please use setup-docker-builder action or configure a builder before using build-push-action.`);
|
||||||
if (res.stderr.length > 0 && res.exitCode != 0) {
|
|
||||||
throw new Error(res.stderr.match(/(.*)\s*$/)?.[0]?.trim() ?? 'unknown error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Set this builder as the global default since future docker commands will use this builder.
|
|
||||||
if (inputs.setupOnly) {
|
|
||||||
const setDefaultCmd = await toolkit.buildx.getCommand(await context.getUseBuilderArgs(name));
|
|
||||||
core.info('Setting builder as global default');
|
|
||||||
await Exec.getExecOutput(setDefaultCmd.command, setDefaultCmd.args, {
|
|
||||||
ignoreReturnCode: true
|
|
||||||
}).then(res => {
|
|
||||||
if (res.stderr.length > 0 && res.exitCode != 0) {
|
|
||||||
throw new Error(res.stderr.match(/(.*)\s*$/)?.[0]?.trim() ?? 'unknown error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
} catch (error) {
|
||||||
} else {
|
core.setFailed(`Error checking for builder: ${error.message}`);
|
||||||
core.warning('Failed to setup Blacksmith builder, falling back to default builder');
|
}
|
||||||
await core.group(`Checking for configured builder`, async () => {
|
});
|
||||||
try {
|
|
||||||
const builder = await toolkit.builder.inspect();
|
|
||||||
if (builder) {
|
|
||||||
core.info(`Found configured builder: ${builder.name}`);
|
|
||||||
} else {
|
|
||||||
// Create a local builder using the docker-container driver (which is the default driver in setup-buildx)
|
|
||||||
const createLocalBuilderCmd = 'docker buildx create --name local --driver docker-container --use';
|
|
||||||
try {
|
|
||||||
await Exec.exec(createLocalBuilderCmd);
|
|
||||||
core.info('Created and set a local builder for use');
|
|
||||||
} catch (error) {
|
|
||||||
core.setFailed(`Failed to create local builder: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
core.setFailed(`Error configuring builder: ${error.message}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write a sentinel file to indicate builder setup is complete.
|
// The sentinel file should already exist from setup-docker-builder
|
||||||
const sentinelPath = path.join('/tmp', 'builder-setup-complete');
|
|
||||||
try {
|
|
||||||
fs.writeFileSync(sentinelPath, 'Builder setup completed successfully.');
|
|
||||||
core.debug(`Created builder setup sentinel file at ${sentinelPath}`);
|
|
||||||
} catch (error) {
|
|
||||||
core.warning(`Failed to create builder setup sentinel file: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let builder: BuilderInfo;
|
let builder: BuilderInfo;
|
||||||
await core.group(`Builder info`, async () => {
|
await core.group(`Builder info`, async () => {
|
||||||
@ -400,34 +361,7 @@ actionsToolkit.run(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Buildkitd is now managed by setup-docker-builder, not here
|
||||||
const {stdout} = await execAsync('pgrep buildkitd');
|
|
||||||
if (stdout.trim()) {
|
|
||||||
try {
|
|
||||||
core.info('Pruning BuildKit cache');
|
|
||||||
await pruneBuildkitCache();
|
|
||||||
core.info('BuildKit cache pruned');
|
|
||||||
} catch (error) {
|
|
||||||
// Log warning but don't fail the cleanup
|
|
||||||
core.warning(`Error pruning BuildKit cache: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildkitdShutdownStartTime = Date.now();
|
|
||||||
await shutdownBuildkitd();
|
|
||||||
const buildkitdShutdownDurationMs = Date.now() - buildkitdShutdownStartTime;
|
|
||||||
await reporter.reportMetric(Metric_MetricType.BPA_BUILDKITD_SHUTDOWN_DURATION_MS, buildkitdShutdownDurationMs);
|
|
||||||
core.info('Shutdown buildkitd');
|
|
||||||
} else {
|
|
||||||
core.debug('No buildkitd process found running');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code === 1) {
|
|
||||||
// pgrep returns non-zero if no processes found, which is fine
|
|
||||||
core.debug('No buildkitd process found running');
|
|
||||||
} else {
|
|
||||||
core.warning(`Error checking for buildkitd processes: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await leaveTailnet();
|
await leaveTailnet();
|
||||||
try {
|
try {
|
||||||
@ -471,21 +405,7 @@ actionsToolkit.run(
|
|||||||
core.warning(`Error during Blacksmith builder shutdown: ${error.message}`);
|
core.warning(`Error during Blacksmith builder shutdown: ${error.message}`);
|
||||||
await reporter.reportBuildPushActionFailure(error, 'shutting down blacksmith builder');
|
await reporter.reportBuildPushActionFailure(error, 'shutting down blacksmith builder');
|
||||||
} finally {
|
} finally {
|
||||||
if (buildError) {
|
// Buildkitd logs are managed by setup-docker-builder
|
||||||
try {
|
|
||||||
const buildkitdLog = fs.readFileSync('/tmp/buildkitd.log', 'utf8');
|
|
||||||
core.info('buildkitd.log contents:');
|
|
||||||
core.info(buildkitdLog);
|
|
||||||
} catch (error) {
|
|
||||||
// Only log warning if the file was expected to exist (builder setup completed)
|
|
||||||
const sentinelPath = path.join('/tmp', 'builder-setup-complete');
|
|
||||||
if (fs.existsSync(sentinelPath)) {
|
|
||||||
core.warning(`Failed to read buildkitd.log: ${error.message}`);
|
|
||||||
} else {
|
|
||||||
core.debug(`buildkitd.log not found (builder setup incomplete): ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -500,28 +420,7 @@ actionsToolkit.run(
|
|||||||
try {
|
try {
|
||||||
await leaveTailnet();
|
await leaveTailnet();
|
||||||
|
|
||||||
try {
|
// Buildkitd is now managed by setup-docker-builder, not here
|
||||||
const {stdout} = await execAsync('pgrep buildkitd');
|
|
||||||
if (stdout.trim()) {
|
|
||||||
try {
|
|
||||||
core.info('Pruning BuildKit cache');
|
|
||||||
await pruneBuildkitCache();
|
|
||||||
core.info('BuildKit cache pruned');
|
|
||||||
} catch (error) {
|
|
||||||
// Log warning but don't fail the cleanup
|
|
||||||
core.warning(`Error pruning BuildKit cache: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await shutdownBuildkitd();
|
|
||||||
core.info('Shutdown buildkitd');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code === 1) {
|
|
||||||
core.debug('No buildkitd process found running');
|
|
||||||
} else {
|
|
||||||
core.warning(`Error checking for buildkitd processes: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Run sync to flush any pending writes before unmounting.
|
// Run sync to flush any pending writes before unmounting.
|
||||||
@ -612,34 +511,3 @@ function buildSummaryEnabled(): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function shutdownBuildkitd(): Promise<void> {
|
|
||||||
const startTime = Date.now();
|
|
||||||
const timeout = 10000; // 10 seconds
|
|
||||||
const backoff = 300; // 300ms
|
|
||||||
|
|
||||||
try {
|
|
||||||
await execAsync(`sudo pkill -TERM buildkitd`);
|
|
||||||
|
|
||||||
// Wait for buildkitd to shutdown with backoff retry
|
|
||||||
while (Date.now() - startTime < timeout) {
|
|
||||||
try {
|
|
||||||
const {stdout} = await execAsync('pgrep buildkitd');
|
|
||||||
core.debug(`buildkitd process still running with PID: ${stdout.trim()}`);
|
|
||||||
await new Promise(resolve => setTimeout(resolve, backoff));
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code === 1) {
|
|
||||||
// pgrep returns exit code 1 when no process is found, which means shutdown successful
|
|
||||||
core.debug('buildkitd successfully shutdown');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Some other error occurred
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Timed out waiting for buildkitd to shutdown after 10 seconds');
|
|
||||||
} catch (error) {
|
|
||||||
core.error('error shutting down buildkitd process:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user