Skip to content

Commit

Permalink
github: write build summary
Browse files Browse the repository at this point in the history
Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
  • Loading branch information
crazy-max committed May 7, 2024
1 parent 63d1c8c commit 3d602aa
Show file tree
Hide file tree
Showing 9 changed files with 301 additions and 32 deletions.
28 changes: 4 additions & 24 deletions __tests__/buildx/history.test.itg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,7 @@ maybe('export', () => {

expect(res?.dockerbuildFilename).toBeDefined();
expect(res?.dockerbuildSize).toBeDefined();
expect(res?.summaryFilename).toBeDefined();
expect(res?.summarySize).toBeDefined();

const summary = JSON.parse(fs.readFileSync(res?.summaryFilename, {encoding: 'utf-8'}));
expect(summary).toBeDefined();
console.log('summary', JSON.stringify(summary, null, 2));
expect(res?.summaries).toBeDefined();
});

it('exports build multi-platform', async () => {
Expand Down Expand Up @@ -116,12 +111,7 @@ maybe('export', () => {

expect(res?.dockerbuildFilename).toBeDefined();
expect(res?.dockerbuildSize).toBeDefined();
expect(res?.summaryFilename).toBeDefined();
expect(res?.summarySize).toBeDefined();

const summary = JSON.parse(fs.readFileSync(res?.summaryFilename, {encoding: 'utf-8'}));
expect(summary).toBeDefined();
console.log('summary', JSON.stringify(summary, null, 2));
expect(res?.summaries).toBeDefined();
});

it('exports bake build', async () => {
Expand Down Expand Up @@ -161,12 +151,7 @@ maybe('export', () => {

expect(res?.dockerbuildFilename).toBeDefined();
expect(res?.dockerbuildSize).toBeDefined();
expect(res?.summaryFilename).toBeDefined();
expect(res?.summarySize).toBeDefined();

const summary = JSON.parse(fs.readFileSync(res?.summaryFilename, {encoding: 'utf-8'}));
expect(summary).toBeDefined();
console.log('summary', JSON.stringify(summary, null, 2));
expect(res?.summaries).toBeDefined();
});

it('exports bake build group', async () => {
Expand Down Expand Up @@ -206,11 +191,6 @@ maybe('export', () => {

expect(res?.dockerbuildFilename).toBeDefined();
expect(res?.dockerbuildSize).toBeDefined();
expect(res?.summaryFilename).toBeDefined();
expect(res?.summarySize).toBeDefined();

const summary = JSON.parse(fs.readFileSync(res?.summaryFilename, {encoding: 'utf-8'}));
expect(summary).toBeDefined();
console.log('summary', JSON.stringify(summary, null, 2));
expect(res?.summaries).toBeDefined();
});
});
11 changes: 11 additions & 0 deletions __tests__/fixtures/hello-bake.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,14 @@ target "hello-bar" {
group "hello-all" {
targets = ["hello", "hello-bar"]
}

target "hello-matrix" {
name = "matrix-${name}"
matrix = {
name = ["bar", "baz", "boo", "far", "faz", "foo", "aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg", "hhh", "iii", "jjj"]
}
dockerfile = "hello.Dockerfile"
args = {
NAME = name
}
}
157 changes: 156 additions & 1 deletion __tests__/github.test.itg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,22 @@
* limitations under the License.
*/

import {beforeEach, describe, expect, it, jest} from '@jest/globals';
import {beforeEach, describe, expect, it, jest, test} from '@jest/globals';
import fs from 'fs';
import * as path from 'path';

import {Buildx} from '../src/buildx/buildx';
import {Bake} from '../src/buildx/bake';
import {Build} from '../src/buildx/build';
import {Exec} from '../src/exec';
import {GitHub} from '../src/github';
import {History} from '../src/buildx/history';

const fixturesDir = path.join(__dirname, 'fixtures');

// prettier-ignore
const tmpDir = path.join(process.env.TEMP || '/tmp', 'github-jest');

const maybe = !process.env.GITHUB_ACTIONS || (process.env.GITHUB_ACTIONS === 'true' && process.env.ImageOS && process.env.ImageOS.startsWith('ubuntu')) ? describe : describe.skip;

beforeEach(() => {
Expand All @@ -39,3 +48,149 @@ maybe('uploadArtifact', () => {
expect(res?.url).toBeDefined();
});
});

maybe('writeBuildSummary', () => {
// prettier-ignore
test.each([
[
"single",
[
'build',
'-f', path.join(fixturesDir, 'hello.Dockerfile'),
fixturesDir
],
],
[
"multiplatform",
[
'build',
'-f', path.join(fixturesDir, 'hello.Dockerfile'),
'--platform', 'linux/amd64,linux/arm64',
fixturesDir
],
]
])('write build summary %p', async (_, bargs) => {
const buildx = new Buildx();
const build = new Build({buildx: buildx});

fs.mkdirSync(tmpDir, {recursive: true});
await expect(
(async () => {
// prettier-ignore
const buildCmd = await buildx.getCommand([
'--builder', process.env.CTN_BUILDER_NAME ?? 'default',
...bargs,
'--metadata-file', build.getMetadataFilePath()
]);
await Exec.exec(buildCmd.command, buildCmd.args);
})()
).resolves.not.toThrow();

const metadata = build.resolveMetadata();
expect(metadata).toBeDefined();
const buildRef = build.resolveRef(metadata);
expect(buildRef).toBeDefined();

const history = new History({buildx: buildx});
const exportRes = await history.export({
refs: [buildRef ?? '']
});
expect(exportRes).toBeDefined();
expect(exportRes?.dockerbuildFilename).toBeDefined();
expect(exportRes?.dockerbuildSize).toBeDefined();
expect(exportRes?.summaries).toBeDefined();

const uploadRes = await GitHub.uploadArtifact({
filename: exportRes?.dockerbuildFilename,
mimeType: 'application/gzip',
retentionDays: 1
});
expect(uploadRes).toBeDefined();
expect(uploadRes?.url).toBeDefined();

await GitHub.writeBuildSummary({
exportRes: exportRes,
uploadRes: uploadRes,
inputs: {
context: fixturesDir,
file: path.join(fixturesDir, 'hello.Dockerfile')
}
});
});

// prettier-ignore
test.each([
[
'single',
[
'bake',
'-f', path.join(fixturesDir, 'hello-bake.hcl'),
'hello'
],
],
[
'group',
[
'bake',
'-f', path.join(fixturesDir, 'hello-bake.hcl'),
'hello-all'
],
],
[
'matrix',
[
'bake',
'-f', path.join(fixturesDir, 'hello-bake.hcl'),
'hello-matrix'
],
]
])('write bake summary %p', async (_, bargs) => {
const buildx = new Buildx();
const bake = new Bake({buildx: buildx});

fs.mkdirSync(tmpDir, {recursive: true});
await expect(
(async () => {
// prettier-ignore
const buildCmd = await buildx.getCommand([
'--builder', process.env.CTN_BUILDER_NAME ?? 'default',
...bargs,
'--metadata-file', bake.getMetadataFilePath()
]);
await Exec.exec(buildCmd.command, buildCmd.args, {
cwd: fixturesDir
});
})()
).resolves.not.toThrow();

const metadata = bake.resolveMetadata();
expect(metadata).toBeDefined();
const buildRefs = bake.resolveRefs(metadata);
expect(buildRefs).toBeDefined();

const history = new History({buildx: buildx});
const exportRes = await history.export({
refs: buildRefs ?? []
});
expect(exportRes).toBeDefined();
expect(exportRes?.dockerbuildFilename).toBeDefined();
expect(exportRes?.dockerbuildSize).toBeDefined();
expect(exportRes?.summaries).toBeDefined();

const uploadRes = await GitHub.uploadArtifact({
filename: exportRes?.dockerbuildFilename,
mimeType: 'application/gzip',
retentionDays: 1
});
expect(uploadRes).toBeDefined();
expect(uploadRes?.url).toBeDefined();

await GitHub.writeBuildSummary({
exportRes: exportRes,
uploadRes: uploadRes,
inputs: {
files: path.join(fixturesDir, 'hello-bake.hcl')
}
});
});
});
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,14 @@
"async-retry": "^1.3.3",
"csv-parse": "^5.5.5",
"handlebars": "^4.7.8",
"js-yaml": "^4.1.0",
"jwt-decode": "^4.0.0",
"semver": "^7.6.0",
"tmp": "^0.2.3"
},
"devDependencies": {
"@types/csv-parse": "^1.2.2",
"@types/js-yaml": "^4.0.9",
"@types/node": "^20.12.10",
"@types/semver": "^7.5.8",
"@types/tmp": "^0.2.6",
Expand Down
10 changes: 6 additions & 4 deletions src/buildx/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {Context} from '../context';
import {Exec} from '../exec';
import {GitHub} from '../github';

import {ExportRecordOpts, ExportRecordResponse} from '../types/history';
import {ExportRecordOpts, ExportRecordResponse, Summaries} from '../types/history';

export interface HistoryOpts {
buildx?: Buildx;
Expand Down Expand Up @@ -142,13 +142,15 @@ export class History {
const dockerbuildPath = path.join(outDir, `${dockerbuildFilename}.dockerbuild`);
fs.renameSync(tmpDockerbuildFilename, dockerbuildPath);
const dockerbuildStats = fs.statSync(dockerbuildPath);
const summaryStats = fs.statSync(summaryFilename);

core.info(`Parsing ${summaryFilename}`);
fs.statSync(summaryFilename);
const summaries = <Summaries>JSON.parse(fs.readFileSync(summaryFilename, {encoding: 'utf-8'}));

return {
dockerbuildFilename: dockerbuildPath,
dockerbuildSize: dockerbuildStats.size,
summaryFilename: summaryFilename,
summarySize: summaryStats.size,
summaries: summaries,
builderName: builderName,
nodeName: nodeName,
refs: refs
Expand Down
87 changes: 86 additions & 1 deletion src/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,26 @@

import crypto from 'crypto';
import fs from 'fs';
import jsyaml from 'js-yaml';
import os from 'os';
import path from 'path';
import {CreateArtifactRequest, FinalizeArtifactRequest, StringValue} from '@actions/artifact/lib/generated';
import {internalArtifactTwirpClient} from '@actions/artifact/lib/internal/shared/artifact-twirp-client';
import {getBackendIdsFromToken} from '@actions/artifact/lib/internal/shared/util';
import {getExpiration} from '@actions/artifact/lib/internal/upload/retention';
import {InvalidResponseError, NetworkError} from '@actions/artifact';
import * as core from '@actions/core';
import {SummaryTableCell} from '@actions/core/lib/summary';
import * as github from '@actions/github';
import {GitHub as Octokit} from '@actions/github/lib/utils';
import {Context} from '@actions/github/lib/context';
import {TransferProgressEvent} from '@azure/core-http';
import {BlobClient, BlobHTTPHeaders} from '@azure/storage-blob';
import {jwtDecode, JwtPayload} from 'jwt-decode';

import {GitHubActionsRuntimeToken, GitHubActionsRuntimeTokenAC, GitHubRepo, UploadArtifactOpts, UploadArtifactResponse} from './types/github';
import {Util} from './util';

import {BuildSummaryOpts, GitHubActionsRuntimeToken, GitHubActionsRuntimeTokenAC, GitHubRepo, UploadArtifactOpts, UploadArtifactResponse} from './types/github';

export interface GitHubOpts {
token?: string;
Expand Down Expand Up @@ -190,4 +195,84 @@ export class GitHub {
url: artifactURL
};
}

public static async writeBuildSummary(opts: BuildSummaryOpts): Promise<void> {
// can't use original core.summary.addLink due to the need to make
// EOL optional
const addLink = function (text: string, url: string, addEOL = false): string {
return `<a href="${url}">${text}</a>` + (addEOL ? os.EOL : '');
};

const refsSize = Object.keys(opts.exportRes.refs).length;

// prettier-ignore
const sum = core.summary
.addHeading('Docker Build summary', 1)
.addRaw(`<p>`)
.addRaw(`For a detailed look at the build, download the following <code>.dockerbuild</code> file and import it into Docker Desktop's Build views. `)
.addBreak()
.addRaw(`The build file provides insights of your build, such as timing, dependencies, results, logs, traces. `)
.addRaw(addLink('Learn more', 'https://docs.docker.com/desktop/use-desktop/builds/#import-build-records'))
.addRaw('</p>')
.addRaw(`<p>`)
.addRaw(`:arrow_down: ${addLink(`<strong>${opts.uploadRes.filename}</strong>`, opts.uploadRes.url)} (${Util.formatFileSize(opts.uploadRes.size)})`)
.addBreak()
.addRaw(`This file includes <strong>${refsSize} build record${refsSize > 1 ? 's' : ''}</strong>.`)
.addRaw(`</p>`)
.addRaw(`<p>`)
.addRaw(`Was this useful? `)
.addRaw(addLink('Let us know', 'https://docs.docker.com/feedback/desktop-build'))
.addRaw('</p>');

sum.addHeading('Preview', 2);

const summaryTableData: Array<Array<SummaryTableCell>> = [
[
{header: true, data: 'ID'},
{header: true, data: 'Name'},
{header: true, data: 'Status'},
{header: true, data: 'Cached'},
{header: true, data: 'Duration'}
]
];
let summaryError: string | undefined;
for (const ref in opts.exportRes.summaries) {
if (Object.prototype.hasOwnProperty.call(opts.exportRes.summaries, ref)) {
const summary = opts.exportRes.summaries[ref];
// prettier-ignore
summaryTableData.push([
{data: `<code>${ref.substring(0, 6).toUpperCase()}</code>`},
{data: `<strong>${summary.name}</strong>`},
{data: `${summary.status === 'completed' ? ':white_check_mark:' : summary.status === 'canceled' ? ':no_entry_sign:' : ':x:'} ${summary.status}`},
{data: `${summary.numCachedSteps > 0 ? Math.round((summary.numCachedSteps / summary.numTotalSteps) * 100) : 0}%`},
{data: summary.duration}
]);
if (summary.error) {
summaryError = summary.error;
}
}
}
sum.addTable([...summaryTableData]);
if (summaryError) {
sum.addHeading('Error', 4);
sum.addCodeBlock(summaryError, 'text');
}

if (opts.inputs) {
sum.addHeading('Build inputs', 2).addCodeBlock(
jsyaml.dump(opts.inputs, {
indent: 2,
lineWidth: -1
}),
'yaml'
);
}

if (opts.bakeDefinition) {
sum.addHeading('Bake definition', 2).addCodeBlock(JSON.stringify(opts.bakeDefinition, null, 2), 'json');
}

core.info(`Writing summary`);
await sum.addSeparator().write();
}
}

0 comments on commit 3d602aa

Please sign in to comment.