2020-09-02 10:07:11 +02:00
import * as fs from 'fs' ;
2023-09-08 15:28:08 +02:00
import * as path from 'path' ;
2020-09-02 10:07:11 +02:00
import * as stateHelper from './state-helper' ;
2020-08-16 00:36:41 +02:00
import * as core from '@actions/core' ;
2023-02-20 11:11:15 +01:00
import * as actionsToolkit from '@docker/actions-toolkit' ;
2024-04-26 11:20:49 +02:00
2024-11-18 10:59:50 -05:00
import { Buildx } from '@docker/actions-toolkit/lib/buildx/buildx' ;
import { History as BuildxHistory } from '@docker/actions-toolkit/lib/buildx/history' ;
import { Context } from '@docker/actions-toolkit/lib/context' ;
import { Docker } from '@docker/actions-toolkit/lib/docker/docker' ;
import { Exec } from '@docker/actions-toolkit/lib/exec' ;
import { GitHub } from '@docker/actions-toolkit/lib/github' ;
import { Toolkit } from '@docker/actions-toolkit/lib/toolkit' ;
import { Util } from '@docker/actions-toolkit/lib/util' ;
import { BuilderInfo } from '@docker/actions-toolkit/lib/types/buildx/builder' ;
import { ConfigFile } from '@docker/actions-toolkit/lib/types/docker/docker' ;
2020-08-16 00:36:41 +02:00
2023-02-20 11:11:15 +01:00
import * as context from './context' ;
2024-11-18 10:59:50 -05:00
import { promisify } from 'util' ;
import { exec } from 'child_process' ;
2024-12-08 14:19:31 -05:00
import * as reporter from './reporter' ;
2025-04-22 16:21:08 -04:00
import { setupStickyDisk , startAndConfigureBuildkitd , getNumCPUs , leaveTailnet , pruneBuildkitCache } from './setup_builder' ;
2025-01-01 02:05:30 -05:00
import { Metric_MetricType } from '@buf/blacksmith_vm-agent.bufbuild_es/stickydisk/v1/stickydisk_pb' ;
2022-04-28 09:31:47 +02:00
2025-05-30 12:31:51 -04:00
const DEFAULT_BUILDX_VERSION = 'v0.23.0' ;
2024-10-31 15:48:44 +05:30
const mountPoint = '/var/lib/buildkit' ;
const execAsync = promisify ( exec ) ;
2024-09-11 20:08:08 -04:00
2025-05-18 16:22:27 -04:00
async function retryWithBackoff < T > ( operation : ( ) = > Promise < T > , maxRetries : number = 5 , initialBackoffMs : number = 200 ) : Promise < T > {
let lastError : Error = new Error ( 'No error occurred' ) ;
for ( let attempt = 0 ; attempt < maxRetries ; attempt ++ ) {
try {
return await operation ( ) ;
} catch ( error ) {
lastError = error ;
if ( error . message ? . includes ( '429' ) || error . status === 429 ) {
if ( attempt < maxRetries - 1 ) {
const backoffMs = initialBackoffMs * Math . pow ( 2 , attempt ) ;
core . info ( ` Rate limited (429). Retrying in ${ backoffMs } ms... ` ) ;
await new Promise ( resolve = > setTimeout ( resolve , backoffMs ) ) ;
continue ;
}
}
throw error ;
}
}
throw lastError ;
}
2024-09-11 20:08:08 -04:00
async function setupBuildx ( version : string , toolkit : Toolkit ) : Promise < void > {
let toolPath ;
const standalone = await toolkit . buildx . isStandalone ( ) ;
if ( ! ( await toolkit . buildx . isAvailable ( ) ) || version ) {
await core . group ( ` Download buildx from GitHub Releases ` , async ( ) = > {
2025-05-18 16:22:27 -04:00
toolPath = await retryWithBackoff ( ( ) = > toolkit . buildxInstall . download ( version || 'latest' , true ) ) ;
2024-09-11 20:08:08 -04:00
} ) ;
}
if ( toolPath ) {
await core . group ( ` Install buildx ` , async ( ) = > {
if ( standalone ) {
await toolkit . buildxInstall . installStandalone ( toolPath ) ;
} else {
await toolkit . buildxInstall . installPlugin ( toolPath ) ;
}
} ) ;
}
await core . group ( ` Buildx version ` , async ( ) = > {
await toolkit . buildx . printVersion ( ) ;
} ) ;
}
2025-05-30 12:31:51 -04:00
// Validates the version string to ensure it matches a basic expected pattern.
// Accepts versions of the form `v<MAJOR>.<MINOR>.<PATCH>` (e.g., v0.20.0) or the literal string `latest`.
function isValidBuildxVersion ( version : string ) : boolean {
return version === 'latest' || /^v\d+\.\d+\.\d+$/ . test ( version ) ;
}
2024-12-08 16:36:08 -05:00
/ * *
* Attempts to set up a Blacksmith builder for Docker builds .
*
* @param inputs - Configuration inputs including the nofallback flag
* @returns { Object } Builder configuration
* @returns { string | null } addr - The buildkit socket address if setup succeeded , null if using local build
* @returns { string | null } buildId - ID used to track build progress and report metrics
* @returns { string } exposeId - ID used to track and cleanup sticky disk resources
*
* The addr is used to configure the Docker buildx builder instance .
* The buildId is used for build progress tracking and metrics reporting .
* The exposeId is used during cleanup to ensure proper resource cleanup of sticky disks .
*
* Throws an error if setup fails and nofallback is false .
* Returns null values if setup fails and nofallback is true .
* /
2024-12-08 14:19:31 -05:00
export async function startBlacksmithBuilder ( inputs : context.Inputs ) : Promise < { addr : string | null ; buildId : string | null ; exposeId : string } > {
try {
2025-04-09 17:30:07 -04:00
// We only use the dockerfile path to report the build to our control plane.
// If setup-only is true, we don't want to report the build to our control plane
// since we are only setting up the builder and therefore cannot expose any analytics
// about the build.
2025-04-22 16:21:08 -04:00
const dockerfilePath = inputs . setupOnly ? '' : context . getDockerfilePath ( inputs ) ;
2025-04-09 17:30:07 -04:00
if ( ! inputs . setupOnly && ! dockerfilePath ) {
2025-04-22 16:21:08 -04:00
throw new Error ( 'Failed to resolve dockerfile path' ) ;
2024-12-08 14:19:31 -05:00
}
2024-12-20 17:25:34 -05:00
const stickyDiskStartTime = Date . now ( ) ;
2025-04-09 17:30:07 -04:00
const stickyDiskSetup = await setupStickyDisk ( dockerfilePath || '' , inputs . setupOnly ) ;
2024-12-20 17:25:34 -05:00
const stickyDiskDurationMs = Date . now ( ) - stickyDiskStartTime ;
await reporter . reportMetric ( Metric_MetricType . BPA_HOTLOAD_DURATION_MS , stickyDiskDurationMs ) ;
2024-12-08 16:36:08 -05:00
const parallelism = await getNumCPUs ( ) ;
2024-12-20 17:25:34 -05:00
const buildkitdStartTime = Date . now ( ) ;
2025-04-09 17:30:07 -04:00
const buildkitdAddr = await startAndConfigureBuildkitd ( parallelism , inputs . setupOnly , inputs . platforms ) ;
2024-12-20 17:25:34 -05:00
const buildkitdDurationMs = Date . now ( ) - buildkitdStartTime ;
await reporter . reportMetric ( Metric_MetricType . BPA_BUILDKITD_READY_DURATION_MS , buildkitdDurationMs ) ;
2025-04-09 17:30:07 -04:00
stateHelper . setExposeId ( stickyDiskSetup . exposeId ) ;
2024-12-08 16:36:08 -05:00
return { addr : buildkitdAddr , buildId : stickyDiskSetup.buildId || null , exposeId : stickyDiskSetup.exposeId } ;
2024-12-08 14:19:31 -05:00
} catch ( error ) {
2024-12-08 16:36:08 -05:00
// 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.
2025-01-01 02:05:30 -05:00
await reporter . reportBuildPushActionFailure ( error , 'starting blacksmith builder' ) ;
2024-12-08 16:36:08 -05:00
let errorMessage = ` Error during Blacksmith builder setup: ${ error . message } ` ;
if ( error . message . includes ( 'buildkitd' ) ) {
errorMessage = ` Error during buildkitd setup: ${ error . message } ` ;
}
2024-12-08 14:19:31 -05:00
if ( inputs . nofallback ) {
2024-12-08 16:36:08 -05:00
core . warning ( ` ${ errorMessage } . Failing the build because nofallback is set. ` ) ;
2024-12-08 14:19:31 -05:00
throw error ;
}
2024-12-08 16:36:08 -05:00
core . warning ( ` ${ errorMessage } . Falling back to a local build. ` ) ;
return { addr : null , buildId : null , exposeId : '' } ;
2025-02-16 23:58:32 +05:30
} finally {
await leaveTailnet ( ) ;
2024-12-08 14:19:31 -05:00
}
}
2023-02-20 11:11:15 +01:00
actionsToolkit . run (
// main
async ( ) = > {
2024-12-20 17:25:34 -05:00
await reporter . reportMetric ( Metric_MetricType . BPA_FEATURE_USAGE , 1 ) ;
2024-05-02 13:49:01 +02:00
const startedTime = new Date ( ) ;
2023-04-17 01:32:21 +02:00
const inputs : context.Inputs = await context . getInputs ( ) ;
2024-05-02 13:49:01 +02:00
stateHelper . setInputs ( inputs ) ;
2024-03-06 14:20:33 +01:00
2023-02-20 11:11:15 +01:00
const toolkit = new Toolkit ( ) ;
2022-04-28 09:31:47 +02:00
2023-02-20 11:11:15 +01:00
await core . group ( ` GitHub Actions runtime token ACs ` , async ( ) = > {
try {
await GitHub . printActionsRuntimeTokenACs ( ) ;
} catch ( e ) {
core . warning ( e . message ) ;
2023-01-12 19:44:15 +01:00
}
} ) ;
2023-02-20 11:11:15 +01:00
await core . group ( ` Docker info ` , async ( ) = > {
try {
await Docker . printVersion ( ) ;
await Docker . printInfo ( ) ;
} catch ( e ) {
core . info ( e . message ) ;
}
} ) ;
2020-08-16 00:36:41 +02:00
2025-05-30 12:31:51 -04:00
// Determine which Buildx version to install. If the user provided an input, validate it;
// otherwise, fall back to the default.
let buildxVersion = DEFAULT_BUILDX_VERSION ;
if ( inputs [ 'buildx-version' ] && inputs [ 'buildx-version' ] . trim ( ) !== '' ) {
if ( isValidBuildxVersion ( inputs [ 'buildx-version' ] ) ) {
buildxVersion = inputs [ 'buildx-version' ] ;
} else {
core . warning ( ` Invalid buildx-version ' ${ inputs [ 'buildx-version' ] } '. ` + ` Expected 'latest' or a version in the form v<MAJOR>.<MINOR>.<PATCH>. ` + ` Falling back to default ${ DEFAULT_BUILDX_VERSION } . ` ) ;
}
}
2024-09-11 20:08:08 -04:00
await core . group ( ` Setup buildx ` , async ( ) = > {
await setupBuildx ( buildxVersion , toolkit ) ;
if ( ! ( await toolkit . buildx . isAvailable ( ) ) ) {
core . setFailed ( ` Docker buildx is required. See https://github.com/docker/setup-buildx-action to set up buildx. ` ) ;
return ;
}
} ) ;
2024-11-26 22:30:36 -05:00
let builderInfo = {
addr : null as string | null ,
2024-12-02 17:33:33 -05:00
buildId : null as string | null ,
exposeId : '' as string
2024-11-26 22:30:36 -05:00
} ;
2024-11-30 09:25:09 -05:00
let buildError : Error | undefined ;
let buildDurationSeconds : string | undefined ;
let ref : string | undefined ;
try {
2024-12-16 22:24:30 -05:00
await core . group ( ` Starting Blacksmith builder ` , async ( ) = > {
builderInfo = await startBlacksmithBuilder ( inputs ) ;
} ) ;
2025-04-09 17:30:07 -04:00
2024-11-30 09:25:09 -05:00
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 ! ) ) ;
core . info ( ` Creating builder with command: ${ createCmd . command } ` ) ;
await Exec . getExecOutput ( createCmd . command , createCmd . args , {
ignoreReturnCode : true
} ) . then ( res = > {
if ( res . stderr . length > 0 && res . exitCode != 0 ) {
throw new Error ( res . stderr . match ( /(.*)\s*$/ ) ? . [ 0 ] ? . trim ( ) ? ? 'unknown error' ) ;
}
} ) ;
2025-04-09 17:30:07 -04:00
// 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' ) ;
}
} ) ;
}
2024-11-30 09:25:09 -05:00
} ) ;
} else {
2025-04-09 17:30:07 -04:00
core . warning ( 'Failed to setup Blacksmith builder, falling back to default builder' ) ;
2024-11-30 09:25:09 -05:00
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 } ` ) ;
2024-09-11 20:08:08 -04:00
}
} ) ;
2024-11-30 09:25:09 -05:00
}
2025-04-09 17:30:07 -04:00
2025-01-10 15:52:55 -05:00
// Write a sentinel file to indicate builder setup is complete.
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 } ` ) ;
}
2024-11-30 09:25:09 -05:00
2025-04-09 17:30:07 -04:00
let builder : BuilderInfo ;
await core . group ( ` Builder info ` , async ( ) = > {
builder = await toolkit . builder . inspect ( ) ;
core . info ( JSON . stringify ( builder , null , 2 ) ) ;
} ) ;
// If setup-only is true, we don't want to continue configuring and running the build.
if ( inputs . setupOnly ) {
core . info ( 'setup-only mode enabled, builder is ready for use by Docker' ) ;
2025-04-29 17:01:42 -04:00
stateHelper . setSetupOnly ( true ) ;
2025-04-22 16:21:08 -04:00
// Let's remove the default
2025-04-09 17:30:07 -04:00
process . exit ( 0 ) ;
}
2024-11-30 09:25:09 -05:00
await core . group ( ` Proxy configuration ` , async ( ) = > {
let dockerConfig : ConfigFile | undefined ;
let dockerConfigMalformed = false ;
2024-09-11 20:08:08 -04:00
try {
2024-11-30 09:25:09 -05:00
dockerConfig = await Docker . configFile ( ) ;
} catch ( e ) {
dockerConfigMalformed = true ;
core . warning ( ` Unable to parse config file ${ path . join ( Docker . configDir , 'config.json' ) } : ${ e } ` ) ;
}
if ( dockerConfig && dockerConfig . proxies ) {
for ( const host in dockerConfig . proxies ) {
let prefix = '' ;
if ( Object . keys ( dockerConfig . proxies ) . length > 1 ) {
prefix = ' ' ;
core . info ( host ) ;
}
for ( const key in dockerConfig . proxies [ host ] ) {
core . info ( ` ${ prefix } ${ key } : ${ dockerConfig . proxies [ host ] [ key ] } ` ) ;
2024-09-13 12:26:02 -07:00
}
2024-09-11 20:08:08 -04:00
}
2024-11-30 09:25:09 -05:00
} else if ( ! dockerConfigMalformed ) {
core . info ( 'No proxy configuration found' ) ;
2024-09-11 20:08:08 -04:00
}
} ) ;
2024-11-30 09:25:09 -05:00
stateHelper . setTmpDir ( Context . tmpDir ( ) ) ;
2023-06-13 11:46:07 +02:00
2024-11-30 09:25:09 -05:00
const args : string [ ] = await context . getArgs ( inputs , toolkit ) ;
args . push ( '--debug' ) ;
core . debug ( ` context.getArgs: ${ JSON . stringify ( args ) } ` ) ;
2024-05-29 12:52:50 +02:00
2024-11-30 09:25:09 -05:00
const buildCmd = await toolkit . buildx . getCommand ( args ) ;
2024-03-06 14:20:33 +01:00
2024-11-30 09:25:09 -05:00
core . debug ( ` buildCmd.command: ${ buildCmd . command } ` ) ;
core . debug ( ` buildCmd.args: ${ JSON . stringify ( buildCmd . args ) } ` ) ;
2024-10-02 16:57:18 -04:00
2024-11-30 09:25:09 -05:00
const buildStartTime = Date . now ( ) ;
await Exec . getExecOutput ( buildCmd . command , buildCmd . args , {
ignoreReturnCode : true ,
env : Object.assign ( { } , process . env , {
BUILDX_METADATA_WARNINGS : 'true'
} ) as {
[ key : string ] : string ;
}
} ) . then ( res = > {
buildDurationSeconds = Math . round ( ( Date . now ( ) - buildStartTime ) / 1000 ) . toString ( ) ;
stateHelper . setDockerBuildDurationSeconds ( buildDurationSeconds ) ;
if ( res . stderr . length > 0 && res . exitCode != 0 ) {
throw Error ( ` buildx failed with: ${ res . stderr . match ( /(.*)\s*$/ ) ? . [ 0 ] ? . trim ( ) ? ? 'unknown error' } ` ) ;
}
} ) ;
2024-03-06 14:20:33 +01:00
2024-11-30 09:25:09 -05:00
const imageID = toolkit . buildxBuild . resolveImageID ( ) ;
const metadata = toolkit . buildxBuild . resolveMetadata ( ) ;
const digest = toolkit . buildxBuild . resolveDigest ( metadata ) ;
if ( imageID ) {
await core . group ( ` ImageID ` , async ( ) = > {
core . info ( imageID ) ;
core . setOutput ( 'imageid' , imageID ) ;
} ) ;
2024-07-30 17:48:59 +02:00
}
2024-11-30 09:25:09 -05:00
if ( digest ) {
await core . group ( ` Digest ` , async ( ) = > {
core . info ( digest ) ;
core . setOutput ( 'digest' , digest ) ;
} ) ;
}
if ( metadata ) {
await core . group ( ` Metadata ` , async ( ) = > {
const metadatadt = JSON . stringify ( metadata , null , 2 ) ;
core . info ( metadatadt ) ;
core . setOutput ( 'metadata' , metadatadt ) ;
} ) ;
2023-02-20 11:11:15 +01:00
}
2020-08-17 22:18:15 +02:00
2024-11-30 09:25:09 -05:00
await core . group ( ` Reference ` , async ( ) = > {
ref = await buildRef ( toolkit , startedTime , builder . name ) ;
if ( ref ) {
core . info ( ref ) ;
stateHelper . setBuildRef ( ref ) ;
} else {
core . info ( 'No build reference found' ) ;
}
2022-02-09 11:32:35 +01:00
} ) ;
2024-06-27 11:26:35 +02:00
2024-11-30 09:25:09 -05:00
if ( buildChecksAnnotationsEnabled ( ) ) {
const warnings = toolkit . buildxBuild . resolveWarnings ( metadata ) ;
if ( ref && warnings && warnings . length > 0 ) {
const annotations = await Buildx . convertWarningsToGitHubAnnotations ( warnings , [ ref ] ) ;
core . debug ( ` annotations: ${ JSON . stringify ( annotations , null , 2 ) } ` ) ;
if ( annotations && annotations . length > 0 ) {
await core . group ( ` Generating GitHub annotations ( ${ annotations . length } build checks found) ` , async ( ) = > {
for ( const annotation of annotations ) {
core . warning ( annotation . message , annotation ) ;
}
} ) ;
}
}
2024-05-02 13:49:01 +02:00
}
2024-06-27 11:26:35 +02:00
2024-11-30 09:25:09 -05:00
await core . group ( ` Check build summary support ` , async ( ) = > {
if ( ! buildSummaryEnabled ( ) ) {
core . info ( 'Build summary disabled' ) ;
} else if ( GitHub . isGHES ) {
core . info ( 'Build summary is not yet supported on GHES' ) ;
} else if ( ! ( await toolkit . buildx . versionSatisfies ( '>=0.13.0' ) ) ) {
core . info ( 'Build summary requires Buildx >= 0.13.0' ) ;
} else if ( builder && builder . driver === 'cloud' ) {
core . info ( 'Build summary is not yet supported with Docker Build Cloud' ) ;
} else if ( ! ref ) {
core . info ( 'Build summary requires a build reference' ) ;
} else {
core . info ( 'Build summary supported!' ) ;
stateHelper . setSummarySupported ( ) ;
2024-07-31 12:39:27 +02:00
}
2024-11-30 09:25:09 -05:00
} ) ;
} catch ( error ) {
buildError = error as Error ;
2024-07-30 17:48:59 +02:00
}
2024-11-30 09:25:09 -05:00
await core . group ( 'Cleaning up Blacksmith builder' , async ( ) = > {
2024-12-16 19:21:13 -05:00
try {
let exportRes ;
if ( ! buildError ) {
const buildxHistory = new BuildxHistory ( ) ;
exportRes = await buildxHistory . export ( {
refs : ref ? [ ref ] : [ ]
} ) ;
}
2024-12-31 15:45:11 +04:00
2025-04-22 16:21:08 -04:00
// Prune buildkit cache to clean up unused layers before shutting down buildkitd.
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 } ` ) ;
}
2024-12-31 15:45:11 +04:00
await leaveTailnet ( ) ;
2024-12-16 22:24:30 -05:00
try {
2024-12-19 13:32:54 -05:00
const { stdout } = await execAsync ( 'pgrep buildkitd' ) ;
2024-12-16 22:24:30 -05:00
if ( stdout . trim ( ) ) {
2024-12-20 17:25:34 -05:00
const buildkitdShutdownStartTime = Date . now ( ) ;
2024-12-16 22:24:30 -05:00
await shutdownBuildkitd ( ) ;
2024-12-20 17:25:34 -05:00
const buildkitdShutdownDurationMs = Date . now ( ) - buildkitdShutdownStartTime ;
await reporter . reportMetric ( Metric_MetricType . BPA_BUILDKITD_SHUTDOWN_DURATION_MS , buildkitdShutdownDurationMs ) ;
2024-12-16 22:24:30 -05:00
core . info ( 'Shutdown buildkitd' ) ;
}
} catch ( error ) {
2024-12-19 12:35:41 -05:00
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 } ` ) ;
}
2024-12-16 22:24:30 -05:00
}
try {
2025-01-21 19:34:23 -05:00
// Run sync to flush any pending writes before unmounting.
await execAsync ( 'sync' ) ;
2024-12-16 22:24:30 -05:00
const { stdout : mountOutput } = await execAsync ( ` mount | grep ${ mountPoint } ` ) ;
if ( mountOutput ) {
for ( let attempt = 1 ; attempt <= 3 ; attempt ++ ) {
try {
await execAsync ( ` sudo umount ${ mountPoint } ` ) ;
core . debug ( ` ${ mountPoint } has been unmounted ` ) ;
break ;
} catch ( error ) {
if ( attempt === 3 ) {
throw error ;
}
core . warning ( ` Unmount failed, retrying ( ${ attempt } /3)... ` ) ;
await new Promise ( resolve = > setTimeout ( resolve , 100 ) ) ;
}
2024-11-26 22:09:05 -05:00
}
2024-12-16 22:24:30 -05:00
core . info ( 'Unmounted device' ) ;
}
} catch ( error ) {
// grep returns exit code 1 when no matches are found.
if ( error . code === 1 ) {
core . debug ( 'No dangling mounts found to clean up' ) ;
} else {
// Only warn for actual errors, not for the expected case where grep finds nothing.
core . warning ( ` Error during cleanup: ${ error . message } ` ) ;
2024-11-26 22:09:05 -05:00
}
2024-12-16 19:21:13 -05:00
}
if ( builderInfo . addr ) {
2024-11-30 09:25:09 -05:00
if ( ! buildError ) {
2024-12-08 14:19:31 -05:00
await reporter . reportBuildCompleted ( exportRes , builderInfo . buildId , ref , buildDurationSeconds , builderInfo . exposeId ) ;
2024-11-30 09:25:09 -05:00
} else {
2024-12-08 14:19:31 -05:00
await reporter . reportBuildFailed ( builderInfo . buildId , buildDurationSeconds , builderInfo . exposeId ) ;
2024-11-26 22:09:05 -05:00
}
2024-12-16 19:21:13 -05:00
}
} catch ( error ) {
core . warning ( ` Error during Blacksmith builder shutdown: ${ error . message } ` ) ;
2025-01-01 02:05:30 -05:00
await reporter . reportBuildPushActionFailure ( error , 'shutting down blacksmith builder' ) ;
2024-12-16 19:21:13 -05:00
} finally {
if ( buildError ) {
try {
2025-04-09 17:30:07 -04:00
const buildkitdLog = fs . readFileSync ( '/tmp/buildkitd.log' , 'utf8' ) ;
2024-12-16 19:21:13 -05:00
core . info ( 'buildkitd.log contents:' ) ;
core . info ( buildkitdLog ) ;
} catch ( error ) {
core . warning ( ` Failed to read buildkitd.log: ${ error . message } ` ) ;
}
2024-11-26 22:09:05 -05:00
}
2024-09-11 20:54:27 -04:00
}
2024-11-30 09:25:09 -05:00
} ) ;
2024-11-26 22:09:05 -05:00
2024-11-30 09:25:09 -05:00
// Re-throw the error after cleanup
if ( buildError ) {
throw buildError ;
2024-09-11 20:54:27 -04:00
}
2023-02-20 11:11:15 +01:00
} ,
// post
async ( ) = > {
2025-04-09 17:30:07 -04:00
await core . group ( 'Final cleanup' , async ( ) = > {
2024-12-10 21:26:18 -05:00
try {
2025-04-09 17:30:07 -04:00
await leaveTailnet ( ) ;
try {
const { stdout } = await execAsync ( 'pgrep buildkitd' ) ;
if ( stdout . trim ( ) ) {
2025-04-22 16:21:08 -04:00
// Prune buildkit cache to clean up unused layers before shutting down buildkitd.
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 } ` ) ;
}
2025-04-09 17:30:07 -04:00
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 } ` ) ;
}
2024-12-19 12:35:41 -05:00
}
2024-11-30 11:13:02 -05:00
2025-04-09 17:30:07 -04:00
try {
// Run sync to flush any pending writes before unmounting.
await execAsync ( 'sync' ) ;
const { stdout : mountOutput } = await execAsync ( ` mount | grep ${ mountPoint } ` ) ;
if ( mountOutput ) {
for ( let attempt = 1 ; attempt <= 3 ; attempt ++ ) {
try {
await execAsync ( ` sudo umount ${ mountPoint } ` ) ;
core . debug ( ` ${ mountPoint } has been unmounted ` ) ;
break ;
} catch ( error ) {
if ( attempt === 3 ) {
throw error ;
}
core . warning ( ` Unmount failed, retrying ( ${ attempt } /3)... ` ) ;
await new Promise ( resolve = > setTimeout ( resolve , 100 ) ) ;
2024-11-30 11:13:02 -05:00
}
}
2025-04-09 17:30:07 -04:00
core . info ( 'Unmounted device' ) ;
}
} catch ( error ) {
if ( error . code === 1 ) {
core . debug ( 'No dangling mounts found to clean up' ) ;
} else {
core . warning ( ` Error during cleanup: ${ error . message } ` ) ;
2024-11-30 11:13:02 -05:00
}
}
2025-04-09 17:30:07 -04:00
// 4. Clean up temp directory if it exists.
if ( stateHelper . tmpDir . length > 0 ) {
fs . rmSync ( stateHelper . tmpDir , { recursive : true } ) ;
core . debug ( ` Removed temp folder ${ stateHelper . tmpDir } ` ) ;
2024-12-13 12:32:05 -05:00
}
2025-04-09 17:30:07 -04:00
2025-04-29 17:01:42 -04:00
// 5. Commit sticky disk if the builder was booted in setup-only mode.
// If the builder was not booted in setup-only mode, the sticky disk was committed as part
// of the main routine.
if ( stateHelper . getSetupOnly ( ) ) {
core . info ( 'Committing sticky disk in post cleanup as setup-only mode was enabled' ) ;
if ( stateHelper . getExposeId ( ) !== '' ) {
await reporter . commitStickyDisk ( stateHelper . getExposeId ( ) ) ;
} else {
core . warning ( 'Expose ID not found in state, skipping sticky disk commit' ) ;
}
}
2025-04-09 17:30:07 -04:00
} catch ( error ) {
core . warning ( ` Error during final cleanup: ${ error . message } ` ) ;
await reporter . reportBuildPushActionFailure ( error , 'final cleanup' ) ;
2024-11-30 11:13:02 -05:00
}
2025-04-09 17:30:07 -04:00
} ) ;
2020-09-02 10:07:11 +02:00
}
2023-02-20 11:11:15 +01:00
) ;
2024-05-02 13:49:01 +02:00
async function buildRef ( toolkit : Toolkit , since : Date , builder? : string ) : Promise < string > {
// get ref from metadata file
const ref = toolkit . buildxBuild . resolveRef ( ) ;
if ( ref ) {
return ref ;
}
// otherwise, look for the very first build ref since the build has started
if ( ! builder ) {
const currentBuilder = await toolkit . builder . inspect ( ) ;
builder = currentBuilder . name ;
}
const refs = Buildx . refs ( {
dir : Buildx.refsDir ,
builderName : builder ,
since : since
} ) ;
return Object . keys ( refs ) . length > 0 ? Object . keys ( refs ) [ 0 ] : '' ;
}
2024-06-24 10:14:14 +02:00
2024-07-31 12:39:27 +02:00
function buildChecksAnnotationsEnabled ( ) : boolean {
if ( process . env . DOCKER_BUILD_CHECKS_ANNOTATIONS ) {
return Util . parseBool ( process . env . DOCKER_BUILD_CHECKS_ANNOTATIONS ) ;
}
return true ;
}
2024-07-02 17:38:24 +02:00
function buildSummaryEnabled ( ) : boolean {
2024-07-02 14:49:31 +02:00
if ( process . env . DOCKER_BUILD_NO_SUMMARY ) {
2024-07-02 17:38:24 +02:00
core . warning ( 'DOCKER_BUILD_NO_SUMMARY is deprecated. Set DOCKER_BUILD_SUMMARY to false instead.' ) ;
return ! Util . parseBool ( process . env . DOCKER_BUILD_NO_SUMMARY ) ;
} else if ( process . env . DOCKER_BUILD_SUMMARY ) {
return Util . parseBool ( process . env . DOCKER_BUILD_SUMMARY ) ;
2024-07-02 14:49:31 +02:00
}
2024-07-02 17:38:24 +02:00
return true ;
2024-12-01 12:51:40 -05:00
}
2024-12-08 14:19:31 -05:00
export async function shutdownBuildkitd ( ) : Promise < void > {
2024-12-19 12:35:41 -05:00
const startTime = Date . now ( ) ;
const timeout = 10000 ; // 10 seconds
const backoff = 300 ; // 300ms
2024-12-08 14:19:31 -05:00
try {
await execAsync ( ` sudo pkill -TERM buildkitd ` ) ;
2024-12-19 12:35:41 -05:00
// Wait for buildkitd to shutdown with backoff retry
while ( Date . now ( ) - startTime < timeout ) {
try {
2024-12-19 13:32:54 -05:00
const { stdout } = await execAsync ( 'pgrep buildkitd' ) ;
core . debug ( ` buildkitd process still running with PID: ${ stdout . trim ( ) } ` ) ;
2024-12-19 13:04:09 -05:00
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' ) ;
2024-12-19 12:35:41 -05:00
return ;
}
2024-12-19 13:04:09 -05:00
// Some other error occurred
throw error ;
2024-12-19 12:35:41 -05:00
}
}
throw new Error ( 'Timed out waiting for buildkitd to shutdown after 10 seconds' ) ;
2024-12-08 14:19:31 -05:00
} catch ( error ) {
core . error ( 'error shutting down buildkitd process:' , error ) ;
throw error ;
}
}