buildx: convert vertex warnings to github annotations based on localstate
Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
This commit is contained in:
@@ -19,10 +19,16 @@ import path from 'path';
|
||||
import * as core from '@actions/core';
|
||||
import * as semver from 'semver';
|
||||
|
||||
import {Git} from '../buildkit/git';
|
||||
import {Docker} from '../docker/docker';
|
||||
import {GitHub} from '../github';
|
||||
import {Exec} from '../exec';
|
||||
import {Util} from '../util';
|
||||
|
||||
import {VertexWarning} from '../types/buildkit/client';
|
||||
import {GitURL} from '../types/buildkit/git';
|
||||
import {Cert, LocalRefsOpts, LocalRefsResponse, LocalState} from '../types/buildx/buildx';
|
||||
import {GitHubAnnotation} from '../types/github';
|
||||
|
||||
export interface BuildxOpts {
|
||||
standalone?: boolean;
|
||||
@@ -266,4 +272,151 @@ export class Buildx {
|
||||
|
||||
return refs;
|
||||
}
|
||||
|
||||
public static async convertWarningsToGitHubAnnotations(warnings: Array<VertexWarning>, buildRefs: Array<string>, refsDir?: string): Promise<Array<GitHubAnnotation> | undefined> {
|
||||
if (warnings.length === 0) {
|
||||
return;
|
||||
}
|
||||
const fnGitURL = function (inp: string): GitURL | undefined {
|
||||
try {
|
||||
return Git.parseURL(inp);
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
};
|
||||
const fnLocalState = function (ref: string): LocalState | undefined {
|
||||
try {
|
||||
return Buildx.localState(ref, refsDir);
|
||||
} catch (e) {
|
||||
core.debug(`Buildx.convertWarningsToGitHubAnnotations(${ref}): local state not found: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
interface Dockerfile {
|
||||
path: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
content?: any;
|
||||
remote?: boolean;
|
||||
}
|
||||
|
||||
const dockerfiles: Array<Dockerfile> = [];
|
||||
for (const ref of buildRefs) {
|
||||
const ls = fnLocalState(ref);
|
||||
if (!ls) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ls.DockerfilePath == '-') {
|
||||
// exclude dockerfile from stdin
|
||||
core.debug(`Buildx.convertWarningsToGitHubAnnotations(${ref}): skipping stdin Dockerfile`);
|
||||
continue;
|
||||
} else if (ls.DockerfilePath == '') {
|
||||
ls.DockerfilePath = 'Dockerfile';
|
||||
}
|
||||
|
||||
const gitURL = fnGitURL(ls.LocalPath);
|
||||
if (gitURL) {
|
||||
core.debug(`Buildx.convertWarningsToGitHubAnnotations(${ref}): git context detected: ${ls.LocalPath}`);
|
||||
const remoteHost = gitURL.host.replace(/:.*/, '');
|
||||
if (remoteHost !== 'github.com' && !remoteHost.endsWith('.ghe.com')) {
|
||||
// we only support running actions on GitHub for now
|
||||
// we might add support for GitLab in the future
|
||||
core.debug(`Buildx.convertWarningsToGitHubAnnotations(${ref}): not a GitHub repo: ${remoteHost}`);
|
||||
continue;
|
||||
}
|
||||
// if repository matches then we can link to the Dockerfile
|
||||
const remoteRepo = gitURL.path.replace(/^\//, '').replace(/\.git$/, '');
|
||||
if (remoteRepo !== GitHub.repository) {
|
||||
core.debug(`Buildx.convertWarningsToGitHubAnnotations(${ref}): not same GitHub repo: ${remoteRepo} != ${GitHub.repository}`);
|
||||
continue;
|
||||
}
|
||||
dockerfiles.push({
|
||||
path: ls.DockerfilePath, // dockerfile path is always relative for a git context
|
||||
remote: true
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(ls.DockerfilePath)) {
|
||||
core.debug(`Buildx.convertWarningsToGitHubAnnotations: Dockerfile not found from localstate ref ${ref}: ${ls.DockerfilePath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const workspaceDir = GitHub.workspace;
|
||||
// only treat dockerfile path relative to GitHub actions workspace dir
|
||||
if (Util.isPathRelativeTo(workspaceDir, ls.DockerfilePath)) {
|
||||
dockerfiles.push({
|
||||
path: path.relative(workspaceDir, ls.DockerfilePath),
|
||||
content: btoa(fs.readFileSync(ls.DockerfilePath, {encoding: 'utf-8'}))
|
||||
});
|
||||
} else {
|
||||
core.debug(`Buildx.convertWarningsToGitHubAnnotations: skipping Dockerfile outside of workspace: ${ls.DockerfilePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (dockerfiles.length === 0) {
|
||||
core.debug(`Buildx.convertWarningsToGitHubAnnotations: no Dockerfiles found`);
|
||||
return;
|
||||
}
|
||||
core.debug(`Buildx.convertWarningsToGitHubAnnotations: found ${dockerfiles.length} Dockerfiles: ${JSON.stringify(dockerfiles, null, 2)}`);
|
||||
|
||||
const annotations: Array<GitHubAnnotation> = [];
|
||||
for (const warning of warnings) {
|
||||
if (!warning.detail || !warning.short) {
|
||||
core.debug(`Buildx.convertWarningsToGitHubAnnotations: skipping warning without detail or short`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const warningSourceFilename = warning.sourceInfo?.filename;
|
||||
const warningSourceData = warning.sourceInfo?.data;
|
||||
if (!warningSourceFilename || !warningSourceData) {
|
||||
core.debug(`Buildx.convertWarningsToGitHubAnnotations: skipping warning without source info filename or data`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const title = warning.detail.map(encoded => atob(encoded)).join(' ');
|
||||
let message = atob(warning.short).replace(/\s\(line \d+\)$/, '');
|
||||
if (warning.url) {
|
||||
// https://github.com/docker/buildx/blob/d8c9ebde1fdcf659f1fa3efa6ccc27a28b0f1564/commands/build.go#L854
|
||||
message += `\nMore info: ${warning.url}`;
|
||||
}
|
||||
|
||||
// GitHub's annotations don't clearly show ranges of lines, so we'll just
|
||||
// show the first line: https://github.com/orgs/community/discussions/129899
|
||||
const startLine = warning.range && warning.range.length > 0 ? warning.range[0]?.start.line : undefined;
|
||||
|
||||
// TODO: When GitHub's annotations support showing ranges properly, we can use this code
|
||||
// let startLine: number | undefined, endLine: number | undefined;
|
||||
// for (const range of warning.range ?? []) {
|
||||
// if (range.start.line && (!startLine || range.start.line < startLine)) {
|
||||
// startLine = range.start.line;
|
||||
// }
|
||||
// if (range.end.line && (!endLine || range.end.line > endLine)) {
|
||||
// endLine = range.end.line;
|
||||
// }
|
||||
// }
|
||||
|
||||
let annotated = false;
|
||||
for (const dockerfile of dockerfiles) {
|
||||
// a valid dockerfile path and content is required to match the warning
|
||||
// source info or always assume it's valid if this is a remote git
|
||||
// context as we can't read the content of the Dockerfile in this case.
|
||||
if (dockerfile.remote || (dockerfile.path.endsWith(warningSourceFilename) && dockerfile.content === warningSourceData)) {
|
||||
annotations.push({
|
||||
title: title,
|
||||
message: message,
|
||||
file: dockerfile.path,
|
||||
startLine: startLine
|
||||
});
|
||||
annotated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!annotated) {
|
||||
core.debug(`Buildx.convertWarningsToGitHubAnnotations: skipping warning without matching Dockerfile ${warningSourceFilename}: ${title}`);
|
||||
}
|
||||
}
|
||||
|
||||
return annotations;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ import {jwtDecode, JwtPayload} from 'jwt-decode';
|
||||
|
||||
import {Util} from './util';
|
||||
|
||||
import {VertexWarning} from './types/buildkit/client';
|
||||
import {BuildSummaryOpts, GitHubActionsRuntimeToken, GitHubActionsRuntimeTokenAC, GitHubRepo, UploadArtifactOpts, UploadArtifactResponse} from './types/github';
|
||||
|
||||
export interface GitHubOpts {
|
||||
@@ -77,6 +76,10 @@ export class GitHub {
|
||||
return `${github.context.repo.owner}/${github.context.repo.repo}`;
|
||||
}
|
||||
|
||||
static get workspace(): string {
|
||||
return process.env.GITHUB_WORKSPACE || process.cwd();
|
||||
}
|
||||
|
||||
static get runId(): number {
|
||||
return process.env.GITHUB_RUN_ID ? +process.env.GITHUB_RUN_ID : github.context.runId;
|
||||
}
|
||||
@@ -342,39 +345,4 @@ export class GitHub {
|
||||
core.info(`Writing summary`);
|
||||
await sum.addSeparator().write();
|
||||
}
|
||||
|
||||
public static async annotateBuildWarnings(source: string, warnings?: Array<VertexWarning>): Promise<void> {
|
||||
(warnings ?? []).forEach(warning => {
|
||||
if (!warning.detail || !warning.short) {
|
||||
return;
|
||||
}
|
||||
const title = warning.detail.map(encoded => atob(encoded)).join(' ');
|
||||
let message = atob(warning.short).replace(/\s\(line \d+\)$/, '');
|
||||
if (warning.url) {
|
||||
// https://github.com/docker/buildx/blob/d8c9ebde1fdcf659f1fa3efa6ccc27a28b0f1564/commands/build.go#L854
|
||||
message += `\nMore info: ${warning.url}`;
|
||||
}
|
||||
|
||||
// GitHub annotations don't clearly show ranges of lines, so we'll just
|
||||
// show the first line
|
||||
const startLine = warning.range && warning.range.length > 0 ? warning.range[0]?.start.line : undefined;
|
||||
|
||||
// TODO: When GitHub annotations support showing ranges properly, we can use this code
|
||||
// let startLine: number | undefined, endLine: number | undefined;
|
||||
// for (const range of warning.range ?? []) {
|
||||
// if (range.start.line && (!startLine || range.start.line < startLine)) {
|
||||
// startLine = range.start.line;
|
||||
// }
|
||||
// if (range.end.line && (!endLine || range.end.line > endLine)) {
|
||||
// endLine = range.end.line;
|
||||
// }
|
||||
// }
|
||||
|
||||
core.warning(message, {
|
||||
title: title,
|
||||
file: source,
|
||||
startLine: startLine
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {AnnotationProperties} from '@actions/core';
|
||||
import {components as OctoOpenApiTypes} from '@octokit/openapi-types';
|
||||
import {JwtPayload} from 'jwt-decode';
|
||||
|
||||
@@ -38,6 +39,10 @@ export interface GitHubActionsRuntimeTokenAC {
|
||||
Permission: number;
|
||||
}
|
||||
|
||||
export interface GitHubAnnotation extends AnnotationProperties {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface UploadArtifactOpts {
|
||||
filename: string;
|
||||
mimeType?: string;
|
||||
|
||||
Reference in New Issue
Block a user