diff --git a/__tests__/buildx/buildx.test.itg.ts b/__tests__/buildx/buildx.test.itg.ts new file mode 100644 index 00000000..bec2623e --- /dev/null +++ b/__tests__/buildx/buildx.test.itg.ts @@ -0,0 +1,41 @@ +/** + * Copyright 2024 actions-toolkit authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {beforeEach, describe, expect, it, jest} from '@jest/globals'; +import * as fs from 'fs'; +import * as path from 'path'; + +import {Buildx} from '../../src/buildx/buildx'; +import {LintResults} from '../../src/types/buildkit'; + +const fixturesDir = path.join(__dirname, '..', 'fixtures'); + +const maybe = !process.env.GITHUB_ACTIONS || (process.env.GITHUB_ACTIONS === 'true' && process.env.ImageOS && process.env.ImageOS.startsWith('ubuntu')) ? describe : describe.skip; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +maybe('lint', () => { + it('runs lint', async () => { + const buildx = new Buildx(); + const buildCmd = await buildx.getCommand(['build', '-f', path.join(fixturesDir, 'lint.Dockerfile'), fixturesDir]); + const res = await buildx.lint(buildCmd.command, buildCmd.args); + expect(res).toBeDefined(); + const expected = JSON.parse(fs.readFileSync(path.join(fixturesDir, 'buildkit-lint-results.json'), {encoding: 'utf-8'}).trim()); + expect(res?.warnings).toEqual(expected.warnings); + }); +}); diff --git a/__tests__/fixtures/buildkit-lint-results.json b/__tests__/fixtures/buildkit-lint-results.json new file mode 100644 index 00000000..3e166d13 --- /dev/null +++ b/__tests__/fixtures/buildkit-lint-results.json @@ -0,0 +1,109 @@ +{ + "warnings": [ + { + "ruleName": "SelfConsistentCommandCasing", + "description": "Commands should be in consistent casing (all lower or all upper)", + "detail": "Command 'frOM' should be consistently cased", + "location": { + "ranges": [ + { + "start": { + "line": 17 + }, + "end": { + "line": 17 + } + } + ] + } + }, + { + "ruleName": "SelfConsistentCommandCasing", + "description": "Commands should be in consistent casing (all lower or all upper)", + "detail": "Command 'cOpy' should be consistently cased", + "location": { + "ranges": [ + { + "start": { + "line": 18 + }, + "end": { + "line": 18 + } + } + ] + } + }, + { + "ruleName": "SelfConsistentCommandCasing", + "description": "Commands should be in consistent casing (all lower or all upper)", + "detail": "Command 'COPy' should be consistently cased", + "location": { + "ranges": [ + { + "start": { + "line": 21 + }, + "end": { + "line": 21 + } + }, + { + "start": { + "line": 22 + }, + "end": { + "line": 22 + } + }, + { + "start": { + "line": 23 + }, + "end": { + "line": 23 + } + } + ] + } + } + ], + "sources": [ + { + "fileName": "lint.Dockerfile", + "language": "Dockerfile", + "definition": { + "def": [ + "GsIBChJsb2NhbDovL2RvY2tlcmZpbGUSFAoMbG9jYWwuZGlmZmVyEgRub25lEkcKEWxvY2FsLmZvbGxvd3BhdGhzEjJbImxpbnQuRG9ja2VyZmlsZSIsImxpbnQuRG9ja2VyZmlsZS5kb2NrZXJpZ25vcmUiXRIqCg1sb2NhbC5zZXNzaW9uEhlrMnNldGgzbmkzZm4zaDdnMzRhaDVyeGRjEiEKE2xvY2FsLnNoYXJlZGtleWhpbnQSCmRvY2tlcmZpbGVaAA==", + "CkkKR3NoYTI1NjplY2E2OTY3MjczZjQ0MmMyNzBkNjljOWRlZGE0MDgyMzg1MjcyNGFiNWFhYmJkODcxYTM0NzFjZThiMzQ0ZWQx" + ], + "metadata": { + "sha256:7717cebab4f00fa481c309aeeec4b8e0c6b4703ad50927cb3392a8d15cf2e86b": { + "caps": { + "constraints": true, + "meta.description": true, + "platform": true + } + }, + "sha256:eca6967273f442c270d69c9deda40823852724ab5aabbd871a3471ce8b344ed1": { + "description": { + "llb.customname": "[internal] load build definition from lint.Dockerfile" + }, + "caps": { + "source.local": true, + "source.local.followpaths": true, + "source.local.sessionid": true, + "source.local.sharedkeyhint": true + } + } + }, + "Source": { + "locations": { + "sha256:eca6967273f442c270d69c9deda40823852724ab5aabbd871a3471ce8b344ed1": {} + } + } + }, + "data": "IyBzeW50YXg9ZG9ja2VyL2RvY2tlcmZpbGUtdXBzdHJlYW06bWFzdGVyCgojIENvcHlyaWdodCAyMDI0IGFjdGlvbnMtdG9vbGtpdCBhdXRob3JzCiMKIyBMaWNlbnNlZCB1bmRlciB0aGUgQXBhY2hlIExpY2Vuc2UsIFZlcnNpb24gMi4wICh0aGUgIkxpY2Vuc2UiKTsKIyB5b3UgbWF5IG5vdCB1c2UgdGhpcyBmaWxlIGV4Y2VwdCBpbiBjb21wbGlhbmNlIHdpdGggdGhlIExpY2Vuc2UuCiMgWW91IG1heSBvYnRhaW4gYSBjb3B5IG9mIHRoZSBMaWNlbnNlIGF0CiMKIyAgICAgIGh0dHA6Ly93d3cuYXBhY2hlLm9yZy9saWNlbnNlcy9MSUNFTlNFLTIuMAojCiMgVW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzb2Z0d2FyZQojIGRpc3RyaWJ1dGVkIHVuZGVyIHRoZSBMaWNlbnNlIGlzIGRpc3RyaWJ1dGVkIG9uIGFuICJBUyBJUyIgQkFTSVMsCiMgV0lUSE9VVCBXQVJSQU5USUVTIE9SIENPTkRJVElPTlMgT0YgQU5ZIEtJTkQsIGVpdGhlciBleHByZXNzIG9yIGltcGxpZWQuCiMgU2VlIHRoZSBMaWNlbnNlIGZvciB0aGUgc3BlY2lmaWMgbGFuZ3VhZ2UgZ292ZXJuaW5nIHBlcm1pc3Npb25zIGFuZAojIGxpbWl0YXRpb25zIHVuZGVyIHRoZSBMaWNlbnNlLgoKZnJPTSBidXN5Ym94IGFzIGJhc2UKY09weSBsaW50LkRvY2tlcmZpbGUgLgoKZnJvbSBzY3JhdGNoCkNPUHkgLS1mcm9tPWJhc2UgXAogIC9saW50LkRvY2tlcmZpbGUgXAogIC8K" + } + ] +} diff --git a/__tests__/fixtures/lint.Dockerfile b/__tests__/fixtures/lint.Dockerfile new file mode 100644 index 00000000..40271b0f --- /dev/null +++ b/__tests__/fixtures/lint.Dockerfile @@ -0,0 +1,23 @@ +# syntax=docker/dockerfile-upstream:master + +# Copyright 2024 actions-toolkit authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +frOM busybox as base +cOpy lint.Dockerfile . + +from scratch +COPy --from=base \ + /lint.Dockerfile \ + / diff --git a/src/buildx/buildx.ts b/src/buildx/buildx.ts index 085b1b73..c0afbaca 100644 --- a/src/buildx/buildx.ts +++ b/src/buildx/buildx.ts @@ -22,6 +22,8 @@ import * as semver from 'semver'; import {Docker} from '../docker/docker'; import {Exec} from '../exec'; +import {ExecOptions} from '@actions/exec'; +import {LintResults} from '../types/buildkit'; import {Cert} from '../types/buildx'; export interface BuildxOpts { @@ -168,4 +170,26 @@ export class Buildx { } return driverOpts; } + + public async lint(cmd: string, args: Array, execOptions?: ExecOptions): Promise { + if (!(await this.versionSatisfies('>=0.14.0'))) { + core.debug(`Buildx version does not support lint (>=0.14.0)`); + return undefined; + } + + execOptions = execOptions || {ignoreReturnCode: true}; + execOptions.ignoreReturnCode = true; + execOptions.env = Object.assign({}, execOptions.env || process.env, { + BUILDX_EXPERIMENTAL: '1' + }) as { + [key: string]: string; + }; + + return await Exec.getExecOutput(cmd, [...args, '--print=lint,format=json,ignorestatus=true'], execOptions).then(execOutput => { + if (execOutput.stderr.length > 0 && execOutput.exitCode != 0) { + throw new Error(`lint failed with: ${execOutput.stderr.match(/(.*)\s*$/)?.[0]?.trim() ?? 'unknown error'}`); + } + return JSON.parse(execOutput.stdout); + }); + } } diff --git a/src/types/buildkit.ts b/src/types/buildkit.ts new file mode 100644 index 00000000..627a199d --- /dev/null +++ b/src/types/buildkit.ts @@ -0,0 +1,85 @@ +/** + * Copyright 2024 actions-toolkit authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface LintResults { + warnings: Array; + sources: Array; +} + +export interface Warning { + ruleName: string; + description?: string; + url?: string; + detail?: string; + location?: Location; +} + +export interface Definition { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + def?: Array; + metadata?: Record; + Source?: Source; +} + +export interface Metadata { + ignore_cache?: boolean; + description?: Record; + export_cache?: ExportCache; + caps?: Record; + progress_group?: ProgressGroup; +} + +export interface Source { + locations?: Record; + infos?: Array; +} + +export interface Locations { + locations?: Array; +} + +export interface Location { + sourceIndex: number; + ranges: Array; +} + +export interface Range { + start: Position; + end: Position; +} + +export interface Position { + line: number; + character: number; +} + +export interface SourceInfo { + filename?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data?: any; + definition?: Definition; + language?: string; +} + +export interface ExportCache { + Value?: boolean; +} + +export interface ProgressGroup { + id?: string; + name?: string; + weak?: boolean; +}