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 14, 2024
1 parent 624e16f commit 450a6e0
Show file tree
Hide file tree
Showing 8 changed files with 289 additions and 3 deletions.
2 changes: 2 additions & 0 deletions __tests__/buildx/history.test.itg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ maybe('exportBuild', () => {
expect(exportRes?.dockerbuildFilename).toBeDefined();
expect(exportRes?.dockerbuildSize).toBeDefined();
expect(fs.existsSync(exportRes?.dockerbuildFilename)).toBe(true);
expect(exportRes?.summaries).toBeDefined();
});

// prettier-ignore
Expand Down Expand Up @@ -147,5 +148,6 @@ maybe('exportBuild', () => {
expect(exportRes?.dockerbuildFilename).toBeDefined();
expect(exportRes?.dockerbuildSize).toBeDefined();
expect(fs.existsSync(exportRes?.dockerbuildFilename)).toBe(true);
expect(exportRes?.summaries).toBeDefined();
});
});
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.6",
"handlebars": "^4.7.8",
"js-yaml": "^4.1.0",
"jwt-decode": "^4.0.0",
"semver": "^7.6.2",
"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
8 changes: 7 additions & 1 deletion src/buildx/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {Docker} from '../docker/docker';
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 @@ -95,6 +95,7 @@ export class History {
buildxDialStdioProc.stdout.pipe(fs.createWriteStream(buildxOutFifoPath));

const tmpDockerbuildFilename = path.join(outDir, 'rec.dockerbuild');
const summaryFilename = path.join(outDir, 'summary.json');

await new Promise<void>((resolve, reject) => {
const ebargs: Array<string> = ['--ref-state-dir=/buildx-refs', `--node=${builderName}/${nodeName}`];
Expand Down Expand Up @@ -145,9 +146,14 @@ export class History {
fs.renameSync(tmpDockerbuildFilename, dockerbuildPath);
const dockerbuildStats = fs.statSync(dockerbuildPath);

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

return {
dockerbuildFilename: dockerbuildPath,
dockerbuildSize: dockerbuildStats.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 Builds view. `)
.addBreak()
.addRaw(`The build file provides insights into your build, such as timing, dependencies, results, logs, traces, and more. `)
.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();
}
}
11 changes: 11 additions & 0 deletions src/types/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
import {components as OctoOpenApiTypes} from '@octokit/openapi-types';
import {JwtPayload} from 'jwt-decode';

import {BakeDefinition} from './bake';
import {ExportRecordResponse} from './history';

export interface GitHubRelease {
id: number;
tag_name: string;
Expand Down Expand Up @@ -47,3 +50,11 @@ export interface UploadArtifactResponse {
size: number;
url: string;
}

export interface BuildSummaryOpts {
exportRes: ExportRecordResponse;
uploadRes: UploadArtifactResponse;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
inputs?: any;
bakeDefinition?: BakeDefinition;
}
16 changes: 16 additions & 0 deletions src/types/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,23 @@ export interface ExportRecordOpts {
export interface ExportRecordResponse {
dockerbuildFilename: string;
dockerbuildSize: number;
summaries: Summaries;
builderName: string;
nodeName: string;
refs: Array<string>;
}

export interface Summaries {
[ref: string]: RecordSummary;
}

export interface RecordSummary {
name: string;
status: string;
duration: string;
numCachedSteps: number;
numTotalSteps: number;
numCompletedSteps: number;
frontendAttrs: Record<string, string>;
error?: string;
}
Loading

0 comments on commit 450a6e0

Please sign in to comment.