Skip to content

Commit a3c0968

Browse files
committed
Add image digests to output
1 parent 26831aa commit a3c0968

File tree

3 files changed

+117
-9
lines changed

3 files changed

+117
-9
lines changed

action.yml

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,19 @@ inputs:
1212
required: false
1313
description: One or more comma-separated image tags (defaults to latest)
1414
platform:
15-
require: false
15+
required: false
1616
description: Platforms for which the image should be built. If omitted, defaults to the platform of the GitHub Actions Runner. Multiple platforms should be comma separated.
1717
runCmd:
1818
required: false
1919
description: Specify the command to run after building the dev container image. Can be omitted to skip starting the container.
2020
subFolder:
2121
required: false
2222
description: Specify a child folder (containing a .devcontainer) instead of using the repository root
23-
default:
23+
default: ""
2424
configFile:
2525
required: false
2626
description: Specify the path to a devcontainer.json file instead of using `./.devcontainer/devcontainer.json` or `./.devcontainer.json`
27-
default:
27+
default: ""
2828
checkoutPath:
2929
required: false
3030
description: Specify path to checked out folder if not using default (or for testing with nektos/act)
@@ -35,7 +35,7 @@ inputs:
3535
description: Control when images are pushed. Options are never, filter, always. For filter (default), images are pushed if the refFilterForPush and eventFilterForPush conditions are met
3636
refFilterForPush:
3737
required: false
38-
default:
38+
default: ""
3939
description: Set the source branches (e.g. refs/heads/main) that are allowed to trigger a push of the dev container image. Leave empty to allow all.
4040
eventFilterForPush:
4141
required: false
@@ -46,11 +46,11 @@ inputs:
4646
description: Specify environment variables to pass to the docker run command
4747
inheritEnv:
4848
required: false
49-
default: false
49+
default: "false"
5050
description: Inherit all environment variables of the runner CI machine.
5151
skipContainerUserIdUpdate:
5252
required: false
53-
default: false
53+
default: "false"
5454
description: For non-root Dev Containers (i.e. where `remoteUser` is specified), the action attempts to make the container user UID and GID match those of the host user. Set this to true to skip this step (defaults to false)
5555
userDataFolder:
5656
required: false
@@ -59,16 +59,17 @@ inputs:
5959
required: false
6060
description: Specify additional images to use for build caching
6161
noCache:
62-
type: boolean
6362
required: false
64-
default: false
63+
default: "false"
6564
description: Builds the image with `--no-cache` (takes precedence over `cacheFrom`)
6665
cacheTo:
6766
required: false
6867
description: Specify the image to cache the built image to
6968
outputs:
7069
runCmdOutput:
7170
description: The output of the command specified in the runCmd input
71+
imageDigests:
72+
description: The SHA256 digests of the built images. JSON object with platform keys and digest values.
7273
runs:
7374
using: 'node20'
7475
main: 'github-action/run-main.js'

common/src/dev-container-cli.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,9 @@ export interface DevContainerCliSuccessResult {
151151
}
152152

153153
export interface DevContainerCliBuildResult
154-
extends DevContainerCliSuccessResult {}
154+
extends DevContainerCliSuccessResult {
155+
imageDigests?: Record<string, string>;
156+
}
155157
export interface DevContainerCliBuildArgs {
156158
workspaceFolder: string;
157159
configFile: string | undefined;

github-action/src/main.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,54 @@ export async function runMain(): Promise<void> {
138138
return;
139139
}
140140

141+
// If we have a platform specified and the image was built, get the image digest
142+
if (buildResult.outcome === 'success') {
143+
// Create a digests object to track digests for each platform
144+
const digestsObj: Record<string, string> = {};
145+
146+
if (platform) {
147+
// Extract the image digest from the build output
148+
if (buildResult.imageDigest) {
149+
core.info(`Image digest for ${platform}: ${buildResult.imageDigest}`);
150+
digestsObj[platform] = buildResult.imageDigest;
151+
} else {
152+
// If buildResult doesn't have imageDigest, try to get it from the built image
153+
if (imageName) {
154+
const inspectCmd = await exec('docker', ['buildx', 'imagetools', 'inspect', `${imageName}:${imageTagArray[0]}`, '--format', '{{json .}}'], { silent: true });
155+
if (inspectCmd.exitCode === 0) {
156+
try {
157+
const imageInfo = JSON.parse(inspectCmd.stdout);
158+
if (imageInfo.manifest && imageInfo.manifest.digest) {
159+
const digest = imageInfo.manifest.digest;
160+
core.info(`Image digest for ${platform}: ${digest}`);
161+
digestsObj[platform] = digest;
162+
}
163+
} catch (error) {
164+
core.warning(`Failed to parse image digest: ${error.message}`);
165+
}
166+
} else {
167+
core.warning(`Failed to inspect image: ${inspectCmd.stderr}`);
168+
}
169+
}
170+
}
171+
} else if (imageName) {
172+
// For non-platform specific builds, still try to get the digest
173+
const inspectCmd = await exec('docker', ['inspect', `${imageName}:${imageTagArray[0]}`, '--format', '{{.Id}}'], { silent: true });
174+
if (inspectCmd.exitCode === 0) {
175+
const digest = inspectCmd.stdout.trim();
176+
core.info(`Image digest: ${digest}`);
177+
digestsObj['default'] = digest;
178+
}
179+
}
180+
181+
// Output the digests as a JSON string
182+
if (Object.keys(digestsObj).length > 0) {
183+
const digestsJson = JSON.stringify(digestsObj);
184+
core.info(`Image digests: ${digestsJson}`);
185+
core.setOutput('imageDigests', digestsJson);
186+
}
187+
}
188+
141189
for (const [key, value] of Object.entries(githubEnvs)) {
142190
if (process.env[key]) {
143191
// Add additional bind mount
@@ -264,17 +312,74 @@ export async function runPost(): Promise<void> {
264312

265313
const platform = emptyStringAsUndefined(core.getInput('platform'));
266314
if (platform) {
315+
// Create a digests object to track digests for each platform
316+
const digestsObj: Record<string, string> = {};
317+
const platforms = platform.split(/\s*,\s*/);
318+
267319
for (const tag of imageTagArray) {
268320
core.info(`Copying multiplatform image '${imageName}:${tag}'...`);
269321
const imageSource = `oci-archive:/tmp/output.tar:${tag}`;
270322
const imageDest = `docker://${imageName}:${tag}`;
271323

272324
await copyImage(true, imageSource, imageDest);
325+
326+
// After pushing, get and set digest
327+
const inspectCmd = await exec('docker', ['buildx', 'imagetools', 'inspect', `${imageName}:${tag}`, '--format', '{{json .}}'], { silent: true });
328+
if (inspectCmd.exitCode === 0) {
329+
try {
330+
const imageInfo = JSON.parse(inspectCmd.stdout);
331+
332+
// If it's a manifest list, extract digests for each platform
333+
if (imageInfo.manifests) {
334+
for (const manifest of imageInfo.manifests) {
335+
if (manifest.platform && manifest.digest) {
336+
const platformStr = `${manifest.platform.os}/${manifest.platform.architecture}${manifest.platform.variant ? '/' + manifest.platform.variant : ''}`;
337+
core.info(`Image digest for ${imageName}:${tag} (${platformStr}): ${manifest.digest}`);
338+
digestsObj[platformStr] = manifest.digest;
339+
}
340+
}
341+
} else if (imageInfo.manifest && imageInfo.manifest.digest) {
342+
// Single platform image
343+
const digest = imageInfo.manifest.digest;
344+
core.info(`Image digest for ${imageName}:${tag}: ${digest}`);
345+
digestsObj[platforms[0] || 'default'] = digest;
346+
}
347+
} catch (error) {
348+
core.warning(`Failed to parse image digest: ${error.message}`);
349+
}
350+
}
351+
}
352+
353+
// Output the digests as a JSON string
354+
if (Object.keys(digestsObj).length > 0) {
355+
const digestsJson = JSON.stringify(digestsObj);
356+
core.info(`Image digests: ${digestsJson}`);
357+
core.setOutput('imageDigests', digestsJson);
273358
}
274359
} else {
360+
// Create a digests object for non-platform specific builds
361+
const digestsObj: Record<string, string> = {};
362+
275363
for (const tag of imageTagArray) {
276364
core.info(`Pushing image '${imageName}:${tag}'...`);
277365
await pushImage(imageName, tag);
366+
367+
// After pushing, get and set digest
368+
const inspectCmd = await exec('docker', ['inspect', `${imageName}:${tag}`, '--format', '{{.Id}}'], { silent: true });
369+
if (inspectCmd.exitCode === 0) {
370+
const digest = inspectCmd.stdout.trim();
371+
core.info(`Image digest for ${imageName}:${tag}: ${digest}`);
372+
digestsObj[tag] = digest;
373+
} else {
374+
core.warning(`Failed to get image digest: ${inspectCmd.stderr}`);
375+
}
376+
}
377+
378+
// Output the digests as a JSON string
379+
if (Object.keys(digestsObj).length > 0) {
380+
const digestsJson = JSON.stringify(digestsObj);
381+
core.info(`Image digests: ${digestsJson}`);
382+
core.setOutput('imageDigests', digestsJson);
278383
}
279384
}
280385
}

0 commit comments

Comments
 (0)