From aae4a2d7bc9be8929f6541035497dc16cc3fc039 Mon Sep 17 00:00:00 2001 From: CrazyMax Date: Mon, 23 Jan 2023 10:07:14 +0100 Subject: [PATCH] Some improvements - Use classes - Split buildx/builder modules - Additional tests Signed-off-by: CrazyMax --- .github/workflows/publish.yml | 18 ++ __tests__/builder.test.ts | 158 ++++++++++ __tests__/buildkit.test.ts | 68 ++-- __tests__/buildx.test.ts | 256 ++++----------- __tests__/docker.test.ts | 5 +- __tests__/fixtures/github-payload.json | 195 ++++++++++++ __tests__/github.test.ts | 37 ++- __tests__/util.test.ts | 51 ++- jest.config.ts | 1 + package.json | 1 + src/builder.ts | 124 ++++++++ src/buildkit.ts | 193 +++++++----- src/buildx.ts | 419 ++++++++++--------------- src/docker.ts | 32 +- src/github.ts | 79 +++-- src/util.ts | 115 +++---- yarn.lock | 5 + 17 files changed, 1045 insertions(+), 712 deletions(-) create mode 100644 .github/workflows/publish.yml create mode 100644 __tests__/builder.test.ts create mode 100644 __tests__/fixtures/github-payload.json create mode 100644 src/builder.ts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..7edb019 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,18 @@ +name: publish + +on: + push: + tags: + - 'v*' + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v3 + - + name: Publish + run: | + PUBLISH_VERSION=${} make publish diff --git a/__tests__/builder.test.ts b/__tests__/builder.test.ts new file mode 100644 index 0000000..5c2f59a --- /dev/null +++ b/__tests__/builder.test.ts @@ -0,0 +1,158 @@ +import {describe, expect, it, test} from '@jest/globals'; +import * as fs from 'fs'; +import * as path from 'path'; +import {Builder} from '../src/builder'; + +describe('inspect', () => { + it('valid', async () => { + const builder = new Builder(); + const builderInfo = await builder.inspect(''); + expect(builderInfo).not.toBeUndefined(); + expect(builderInfo.name).not.toEqual(''); + expect(builderInfo.driver).not.toEqual(''); + expect(builderInfo.nodes).not.toEqual({}); + }, 100000); +}); + +describe('parseInspect', () => { + // prettier-ignore + test.each([ + [ + 'inspect1.txt', + { + "name": "builder-5cb467f7-0940-47e1-b94b-d51f54054d62", + "driver": "docker-container", + "nodes": [ + { + "name": "builder-5cb467f7-0940-47e1-b94b-d51f54054d620", + "endpoint": "unix:///var/run/docker.sock", + "status": "running", + "buildkitdFlags": "--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host", + "buildkitVersion": "v0.10.4", + "platforms": "linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/amd64/v4,linux/arm64,linux/riscv64,linux/386,linux/arm/v7,linux/arm/v6" + } + ] + } + ], + [ + 'inspect2.txt', + { + "name": "builder-5f449644-ff29-48af-8344-abb0292d0673", + "driver": "docker-container", + "nodes": [ + { + "name": "builder-5f449644-ff29-48af-8344-abb0292d06730", + "endpoint": "unix:///var/run/docker.sock", + "driverOpts": [ + "image=moby/buildkit:latest" + ], + "status": "running", + "buildkitdFlags": "--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host", + "buildkitVersion": "v0.10.4", + "platforms": "linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/amd64/v4,linux/386" + } + ] + } + ], + [ + 'inspect3.txt', + { + "name": "builder-9929e463-7954-4dc3-89cd-514cca29ff80", + "driver": "docker-container", + "nodes": [ + { + "name": "builder-9929e463-7954-4dc3-89cd-514cca29ff800", + "endpoint": "unix:///var/run/docker.sock", + "driverOpts": [ + "image=moby/buildkit:master", + "network=host" + ], + "status": "running", + "buildkitdFlags": "--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host", + "buildkitVersion": "3fab389", + "platforms": "linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/amd64/v4,linux/386" + } + ] + } + ], + [ + 'inspect4.txt', + { + "name": "default", + "driver": "docker", + "nodes": [ + { + "name": "default", + "endpoint": "default", + "status": "running", + "buildkitVersion": "20.10.17", + "platforms": "linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/arm/v7,linux/arm/v6" + } + ] + } + ], + [ + 'inspect5.txt', + { + "name": "remote-builder", + "driver": "remote", + "nodes": [ + { + "name": "aws_graviton2", + "endpoint": "tcp://1.23.45.67:1234", + "driverOpts": [ + "cert=/home/user/.certs/aws_graviton2/cert.pem", + "key=/home/user/.certs/aws_graviton2/key.pem", + "cacert=/home/user/.certs/aws_graviton2/ca.pem" + ], + "status": "running", + "platforms": "darwin/arm64,linux/arm64,linux/arm/v5,linux/arm/v6,linux/arm/v7,windows/arm64" + } + ] + } + ], + [ + 'inspect6.txt', + { + "nodes": [ + { + "name": "builder-17cfff01-48d9-4c3d-9332-9992e308a5100", + "endpoint": "unix:///var/run/docker.sock", + "status": "running", + "buildkitdFlags": "--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host", + "platforms": "linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/386" + } + ], + "name": "builder-17cfff01-48d9-4c3d-9332-9992e308a510", + "driver": "docker-container" + } + ], + [ + 'inspect7.txt', + { + "name": "builder2", + "driver": "docker-container", + "lastActivity": new Date("2023-01-16T09:45:23.000Z"), + "nodes": [ + { + "buildkitVersion": "v0.11.0", + "buildkitdFlags": "--debug --allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host", + "driverOpts": [ + "BUILDKIT_STEP_LOG_MAX_SIZE=10485760", + "BUILDKIT_STEP_LOG_MAX_SPEED=10485760", + "JAEGER_TRACE=localhost:6831", + "image=moby/buildkit:latest", + "network=host" + ], + "endpoint": "unix:///var/run/docker.sock", + "name": "builder20", + "platforms": "linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6", + "status": "running" + } + ] + } + ] + ])('given %p', async (inspectFile, expected) => { + expect(await Builder.parseInspect(fs.readFileSync(path.join(__dirname, 'fixtures', inspectFile)).toString())).toEqual(expected); + }); +}); diff --git a/__tests__/buildkit.test.ts b/__tests__/buildkit.test.ts index f3a1a60..c058bff 100644 --- a/__tests__/buildkit.test.ts +++ b/__tests__/buildkit.test.ts @@ -1,32 +1,40 @@ -import {describe, expect, it, jest, test} from '@jest/globals'; +import {afterEach, describe, expect, it, jest, test} from '@jest/globals'; import * as fs from 'fs'; -import * as os from 'os'; import * as path from 'path'; import * as semver from 'semver'; -import * as buildkit from '../src/buildkit'; -import * as buildx from '../src/buildx'; -import * as util from '../src/util'; +import rimraf from 'rimraf'; -const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), 'docker-actions-toolkit-')).split(path.sep).join(path.posix.sep); -jest.spyOn(util, 'tmpDir').mockImplementation((): string => { - return tmpdir; +import {BuildKit} from '../src/buildkit'; +import {Builder, BuilderInfo} from '../src/builder'; + +const tmpDir = path.join('/tmp/.docker-actions-toolkit-jest').split(path.sep).join(path.posix.sep); +const tmpName = path.join(tmpDir, '.tmpname-jest').split(path.sep).join(path.posix.sep); + +jest.spyOn(BuildKit.prototype as any, 'tmpDir').mockImplementation((): string => { + if (!fs.existsSync(tmpDir)) { + fs.mkdirSync(tmpDir, {recursive: true}); + } + return tmpDir; }); -const tmpname = path.join(tmpdir, '.tmpname').split(path.sep).join(path.posix.sep); -jest.spyOn(util, 'tmpNameSync').mockImplementation((): string => { - return tmpname; +jest.spyOn(BuildKit.prototype as any, 'tmpName').mockImplementation((): string => { + return tmpName; }); -jest.spyOn(buildx, 'inspect').mockImplementation(async (): Promise => { +afterEach(() => { + rimraf.sync(tmpDir); +}); + +jest.spyOn(Builder.prototype, 'inspect').mockImplementation(async (): Promise => { return { name: 'builder2', driver: 'docker-container', - 'last-activity': new Date('2023-01-16 09:45:23 +0000 UTC'), + lastActivity: new Date('2023-01-16 09:45:23 +0000 UTC'), nodes: [ { - buildkit: 'v0.11.0', - 'buildkitd-flags': '--debug --allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host', - 'driver-opts': ['BUILDKIT_STEP_LOG_MAX_SIZE=10485760', 'BUILDKIT_STEP_LOG_MAX_SPEED=10485760', 'JAEGER_TRACE=localhost:6831', 'image=moby/buildkit:latest', 'network=host'], + buildkitVersion: 'v0.11.0', + buildkitdFlags: '--debug --allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host', + driverOpts: ['BUILDKIT_STEP_LOG_MAX_SIZE=10485760', 'BUILDKIT_STEP_LOG_MAX_SPEED=10485760', 'JAEGER_TRACE=localhost:6831', 'image=moby/buildkit:latest', 'network=host'], endpoint: 'unix:///var/run/docker.sock', name: 'builder20', platforms: 'linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6', @@ -38,6 +46,7 @@ jest.spyOn(buildx, 'inspect').mockImplementation(async (): Promise { it('valid', async () => { + const buildkit = new BuildKit(); const version = await buildkit.getVersion('builder2'); expect(semver.valid(version)).not.toBeNull(); }); @@ -48,14 +57,15 @@ describe('satisfies', () => { ['builder2', '>=0.10.0', true], ['builder2', '>0.11.0', false] ])('given %p', async (builderName, range, expected) => { - expect(await buildkit.satisfies(builderName, range)).toBe(expected); + const buildkit = new BuildKit(); + expect(await buildkit.versionSatisfies(builderName, range)).toBe(expected); }); }); -describe('getConfig', () => { +describe('generateConfig', () => { test.each([ - ['debug = true', false, 'debug = true', false], - [`notfound.toml`, true, '', true], + ['debug = true', false, 'debug = true', null], + [`notfound.toml`, true, '', new Error('config file notfound.toml not found')], [ `${path.join(__dirname, 'fixtures', 'buildkitd.toml').split(path.sep).join(path.posix.sep)}`, true, @@ -63,23 +73,23 @@ describe('getConfig', () => { [registry."docker.io"] mirrors = ["mirror.gcr.io"] `, - false + null ] - ])('given %p config', async (val, file, exValue, invalid) => { + ])('given %p config', async (val, file, exValue, error: Error) => { try { + const buildkit = new BuildKit(); let config: string; if (file) { - config = await buildkit.getConfigFile(val); + config = buildkit.generateConfigFile(val); } else { - config = await buildkit.getConfigInline(val); + config = buildkit.generateConfigInline(val); } - expect(true).toBe(!invalid); - expect(config).toEqual(tmpname); - const configValue = fs.readFileSync(tmpname, 'utf-8'); + expect(config).toEqual(tmpName); + const configValue = fs.readFileSync(tmpName, 'utf-8'); expect(configValue).toEqual(exValue); - } catch (err) { + } catch (e) { // eslint-disable-next-line jest/no-conditional-expect - expect(true).toBe(invalid); + expect(e.message).toEqual(error?.message); } }); }); diff --git a/__tests__/buildx.test.ts b/__tests__/buildx.test.ts index f55b874..70922f7 100644 --- a/__tests__/buildx.test.ts +++ b/__tests__/buildx.test.ts @@ -1,53 +1,61 @@ -import {describe, expect, it, jest, test} from '@jest/globals'; +import {afterEach, describe, expect, it, jest, test} from '@jest/globals'; import * as fs from 'fs'; import * as path from 'path'; import * as semver from 'semver'; import * as exec from '@actions/exec'; -import * as buildx from '../src/buildx'; -import * as util from '../src/util'; +import rimraf from 'rimraf'; -const tmpNameSync = path.join('/tmp/.docker-actions-toolkit-jest', '.tmpname-jest').split(path.sep).join(path.posix.sep); -const imageID = 'sha256:bfb45ab72e46908183546477a08f8867fc40cebadd00af54b071b097aed127a9'; +import {Buildx} from '../src/buildx'; + +const tmpDir = path.join('/tmp/.docker-actions-toolkit-jest').split(path.sep).join(path.posix.sep); +const tmpName = path.join(tmpDir, '.tmpname-jest').split(path.sep).join(path.posix.sep); const metadata = `{ "containerimage.config.digest": "sha256:059b68a595b22564a1cbc167af369349fdc2ecc1f7bc092c2235cbf601a795fd", "containerimage.digest": "sha256:b09b9482c72371486bb2c1d2c2a2633ed1d0b8389e12c8d52b9e052725c0c83c" }`; -jest.spyOn(util, 'tmpDir').mockImplementation((): string => { - const tmpDir = path.join('/tmp/.docker-actions-toolkit-jest').split(path.sep).join(path.posix.sep); +jest.spyOn(Buildx.prototype as any, 'tmpDir').mockImplementation((): string => { if (!fs.existsSync(tmpDir)) { fs.mkdirSync(tmpDir, {recursive: true}); } return tmpDir; }); -jest.spyOn(util, 'tmpNameSync').mockImplementation((): string => { - return tmpNameSync; +afterEach(() => { + rimraf.sync(tmpDir); }); -describe('getImageID', () => { +jest.spyOn(Buildx.prototype as any, 'tmpName').mockImplementation((): string => { + return tmpName; +}); + +describe('getBuildImageID', () => { it('matches', async () => { - const imageIDFile = await buildx.getImageIDFile(); + const buildx = new Buildx(); + const imageID = 'sha256:bfb45ab72e46908183546477a08f8867fc40cebadd00af54b071b097aed127a9'; + const imageIDFile = buildx.getBuildImageIDFilePath(); await fs.writeFileSync(imageIDFile, imageID); - const expected = await buildx.getImageID(); + const expected = buildx.getBuildImageID(); expect(expected).toEqual(imageID); }); }); -describe('getMetadata', () => { +describe('getBuildMetadata', () => { it('matches', async () => { - const metadataFile = await buildx.getMetadataFile(); + const buildx = new Buildx(); + const metadataFile = buildx.getBuildMetadataFilePath(); await fs.writeFileSync(metadataFile, metadata); - const expected = await buildx.getMetadata(); + const expected = buildx.getBuildMetadata(); expect(expected).toEqual(metadata); }); }); describe('getDigest', () => { it('matches', async () => { - const metadataFile = await buildx.getMetadataFile(); + const buildx = new Buildx(); + const metadataFile = buildx.getBuildMetadataFilePath(); await fs.writeFileSync(metadataFile, metadata); - const expected = await buildx.getDigest(metadata); + const expected = buildx.getDigest(); expect(expected).toEqual('sha256:b09b9482c72371486bb2c1d2c2a2633ed1d0b8389e12c8d52b9e052725c0c83c'); }); }); @@ -62,14 +70,15 @@ describe('hasLocalOrTarExporter', () => { [['"type=tar","dest=/tmp/image.tar"'], true], [['" type= local" , dest=./release-out'], true], [['.'], true] - ])('given %p returns %p', async (outputs: Array, expected: boolean) => { - expect(buildx.hasLocalOrTarExporter(outputs)).toEqual(expected); + ])('given %p returns %p', async (exporters: Array, expected: boolean) => { + expect(Buildx.hasLocalExporter(exporters) || Buildx.hasTarExporter(exporters)).toEqual(expected); }); }); describe('isAvailable', () => { it('docker cli', async () => { const execSpy = jest.spyOn(exec, 'getExecOutput'); + const buildx = new Buildx(); await buildx.isAvailable(); // eslint-disable-next-line jest/no-standalone-expect expect(execSpy).toHaveBeenCalledWith(`docker`, ['buildx'], { @@ -79,7 +88,10 @@ describe('isAvailable', () => { }); it('standalone', async () => { const execSpy = jest.spyOn(exec, 'getExecOutput'); - await buildx.isAvailable(true); + const buildx = new Buildx({ + standalone: true + }); + await buildx.isAvailable(); // eslint-disable-next-line jest/no-standalone-expect expect(execSpy).toHaveBeenCalledWith(`buildx`, [], { silent: true, @@ -88,161 +100,9 @@ describe('isAvailable', () => { }); }); -describe('inspect', () => { - it('valid', async () => { - const builder = await buildx.inspect(''); - expect(builder).not.toBeUndefined(); - expect(builder.name).not.toEqual(''); - expect(builder.driver).not.toEqual(''); - expect(builder.nodes).not.toEqual({}); - }, 100000); -}); - -describe('parseInspect', () => { - // prettier-ignore - test.each([ - [ - 'inspect1.txt', - { - "name": "builder-5cb467f7-0940-47e1-b94b-d51f54054d62", - "driver": "docker-container", - "nodes": [ - { - "name": "builder-5cb467f7-0940-47e1-b94b-d51f54054d620", - "endpoint": "unix:///var/run/docker.sock", - "status": "running", - "buildkitd-flags": "--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host", - "buildkit": "v0.10.4", - "platforms": "linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/amd64/v4,linux/arm64,linux/riscv64,linux/386,linux/arm/v7,linux/arm/v6" - } - ] - } - ], - [ - 'inspect2.txt', - { - "name": "builder-5f449644-ff29-48af-8344-abb0292d0673", - "driver": "docker-container", - "nodes": [ - { - "name": "builder-5f449644-ff29-48af-8344-abb0292d06730", - "endpoint": "unix:///var/run/docker.sock", - "driver-opts": [ - "image=moby/buildkit:latest" - ], - "status": "running", - "buildkitd-flags": "--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host", - "buildkit": "v0.10.4", - "platforms": "linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/amd64/v4,linux/386" - } - ] - } - ], - [ - 'inspect3.txt', - { - "name": "builder-9929e463-7954-4dc3-89cd-514cca29ff80", - "driver": "docker-container", - "nodes": [ - { - "name": "builder-9929e463-7954-4dc3-89cd-514cca29ff800", - "endpoint": "unix:///var/run/docker.sock", - "driver-opts": [ - "image=moby/buildkit:master", - "network=host" - ], - "status": "running", - "buildkitd-flags": "--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host", - "buildkit": "3fab389", - "platforms": "linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/amd64/v4,linux/386" - } - ] - } - ], - [ - 'inspect4.txt', - { - "name": "default", - "driver": "docker", - "nodes": [ - { - "name": "default", - "endpoint": "default", - "status": "running", - "buildkit": "20.10.17", - "platforms": "linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/arm/v7,linux/arm/v6" - } - ] - } - ], - [ - 'inspect5.txt', - { - "name": "remote-builder", - "driver": "remote", - "nodes": [ - { - "name": "aws_graviton2", - "endpoint": "tcp://1.23.45.67:1234", - "driver-opts": [ - "cert=/home/user/.certs/aws_graviton2/cert.pem", - "key=/home/user/.certs/aws_graviton2/key.pem", - "cacert=/home/user/.certs/aws_graviton2/ca.pem" - ], - "status": "running", - "platforms": "darwin/arm64,linux/arm64,linux/arm/v5,linux/arm/v6,linux/arm/v7,windows/arm64" - } - ] - } - ], - [ - 'inspect6.txt', - { - "nodes": [ - { - "name": "builder-17cfff01-48d9-4c3d-9332-9992e308a5100", - "endpoint": "unix:///var/run/docker.sock", - "status": "running", - "buildkitd-flags": "--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host", - "platforms": "linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/386" - } - ], - "name": "builder-17cfff01-48d9-4c3d-9332-9992e308a510", - "driver": "docker-container" - } - ], - [ - 'inspect7.txt', - { - "name": "builder2", - "driver": "docker-container", - "last-activity": new Date("2023-01-16T09:45:23.000Z"), - "nodes": [ - { - "buildkit": "v0.11.0", - "buildkitd-flags": "--debug --allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host", - "driver-opts": [ - "BUILDKIT_STEP_LOG_MAX_SIZE=10485760", - "BUILDKIT_STEP_LOG_MAX_SPEED=10485760", - "JAEGER_TRACE=localhost:6831", - "image=moby/buildkit:latest", - "network=host" - ], - "endpoint": "unix:///var/run/docker.sock", - "name": "builder20", - "platforms": "linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6", - "status": "running" - } - ] - } - ] - ])('given %p', async (inspectFile, expected) => { - expect(await buildx.parseInspect(fs.readFileSync(path.join(__dirname, 'fixtures', inspectFile)).toString())).toEqual(expected); - }); -}); - describe('getVersion', () => { it('valid', async () => { + const buildx = new Buildx(); const version = await buildx.getVersion(); expect(semver.valid(version)).not.toBeNull(); }); @@ -255,44 +115,54 @@ describe('parseVersion', () => { ['github.com/docker/buildx v0.4.2 fb7b670b764764dc4716df3eba07ffdae4cc47b2', '0.4.2'], ['github.com/docker/buildx f117971 f11797113e5a9b86bd976329c5dbb8a8bfdfadfa', 'f117971'] ])('given %p', async (stdout, expected) => { - expect(buildx.parseVersion(stdout)).toEqual(expected); + expect(Buildx.parseVersion(stdout)).toEqual(expected); }); }); -describe('satisfies', () => { +describe('versionSatisfies', () => { test.each([ ['0.4.1', '>=0.3.2', true], ['bda4882a65349ca359216b135896bddc1d92461c', '>0.1.0', false], ['f117971', '>0.6.0', true] ])('given %p', async (version, range, expected) => { - expect(buildx.satisfies(version, range)).toBe(expected); + expect(Buildx.versionSatisfies(version, range)).toBe(expected); }); }); -describe('getSecret', () => { +describe('generateBuildSecret', () => { test.each([ - ['A_SECRET=abcdef0123456789', false, 'A_SECRET', 'abcdef0123456789', false], - ['GIT_AUTH_TOKEN=abcdefghijklmno=0123456789', false, 'GIT_AUTH_TOKEN', 'abcdefghijklmno=0123456789', false], - ['MY_KEY=c3RyaW5nLXdpdGgtZXF1YWxzCg==', false, 'MY_KEY', 'c3RyaW5nLXdpdGgtZXF1YWxzCg==', false], - ['aaaaaaaa', false, '', '', true], - ['aaaaaaaa=', false, '', '', true], - ['=bbbbbbb', false, '', '', true], - [`foo=${path.join(__dirname, 'fixtures', 'secret.txt').split(path.sep).join(path.posix.sep)}`, true, 'foo', 'bar', false], - [`notfound=secret`, true, '', '', true] - ])('given %p key and %p secret', async (kvp, file, exKey, exValue, invalid) => { + ['A_SECRET=abcdef0123456789', false, 'A_SECRET', 'abcdef0123456789', null], + ['GIT_AUTH_TOKEN=abcdefghijklmno=0123456789', false, 'GIT_AUTH_TOKEN', 'abcdefghijklmno=0123456789', null], + ['MY_KEY=c3RyaW5nLXdpdGgtZXF1YWxzCg==', false, 'MY_KEY', 'c3RyaW5nLXdpdGgtZXF1YWxzCg==', null], + ['aaaaaaaa', false, '', '', new Error('aaaaaaaa is not a valid secret')], + ['aaaaaaaa=', false, '', '', new Error('aaaaaaaa= is not a valid secret')], + ['=bbbbbbb', false, '', '', new Error('=bbbbbbb is not a valid secret')], + [`foo=${path.join(__dirname, 'fixtures', 'secret.txt').split(path.sep).join(path.posix.sep)}`, true, 'foo', 'bar', null], + [`notfound=secret`, true, '', '', new Error('secret file secret not found')] + ])('given %p key and %p secret', async (kvp: string, file: boolean, exKey: string, exValue: string, error: Error) => { try { + const buildx = new Buildx(); let secret: string; if (file) { - secret = await buildx.getSecretFile(kvp); + secret = buildx.generateBuildSecretFile(kvp); } else { - secret = await buildx.getSecretString(kvp); + secret = buildx.generateBuildSecretString(kvp); } - expect(true).toBe(!invalid); - expect(secret).toEqual(`id=${exKey},src=${tmpNameSync}`); - expect(fs.readFileSync(tmpNameSync, 'utf-8')).toEqual(exValue); - } catch (err) { + expect(secret).toEqual(`id=${exKey},src=${tmpName}`); + expect(fs.readFileSync(tmpName, 'utf-8')).toEqual(exValue); + } catch (e) { // eslint-disable-next-line jest/no-conditional-expect - expect(true).toBe(invalid); + expect(e.message).toEqual(error?.message); } }); }); + +describe('hasGitAuthTokenSecret', () => { + // prettier-ignore + test.each([ + [['A_SECRET=abcdef0123456789'], false], + [['GIT_AUTH_TOKEN=abcdefghijklmno=0123456789'], true], + ])('given %p secret', async (kvp: Array, expected: boolean) => { + expect(Buildx.hasGitAuthTokenSecret(kvp)).toBe(expected); + }); +}); diff --git a/__tests__/docker.test.ts b/__tests__/docker.test.ts index d1b7075..d99c27d 100644 --- a/__tests__/docker.test.ts +++ b/__tests__/docker.test.ts @@ -1,11 +1,12 @@ import {describe, expect, it, jest} from '@jest/globals'; -import * as docker from '../src/docker'; import * as exec from '@actions/exec'; +import {Docker} from '../src/docker'; + describe('isAvailable', () => { it('cli', () => { const execSpy = jest.spyOn(exec, 'getExecOutput'); - docker.isAvailable(); + Docker.isAvailable(); // eslint-disable-next-line jest/no-standalone-expect expect(execSpy).toHaveBeenCalledWith(`docker`, undefined, { diff --git a/__tests__/fixtures/github-payload.json b/__tests__/fixtures/github-payload.json new file mode 100644 index 0000000..3a5abdb --- /dev/null +++ b/__tests__/fixtures/github-payload.json @@ -0,0 +1,195 @@ +{ + "after": "860c1904a1ce19322e91ac35af1ab07466440c37", + "base_ref": null, + "before": "5f3331d7f7044c18ca9f12c77d961c4d7cf3276a", + "commits": [ + { + "author": { + "email": "crazy-max@users.noreply.github.com", + "name": "CrazyMax", + "username": "crazy-max" + }, + "committer": { + "email": "crazy-max@users.noreply.github.com", + "name": "CrazyMax", + "username": "crazy-max" + }, + "distinct": true, + "id": "860c1904a1ce19322e91ac35af1ab07466440c37", + "message": "hello dev", + "timestamp": "2022-04-19T11:27:24+02:00", + "tree_id": "d2c60af597e863787d2d27f569e30495b0b92820", + "url": "https://github.com/docker/test-docker-action/commit/860c1904a1ce19322e91ac35af1ab07466440c37" + } + ], + "compare": "https://github.com/docker/test-docker-action/compare/5f3331d7f704...860c1904a1ce", + "created": false, + "deleted": false, + "forced": false, + "head_commit": { + "author": { + "email": "crazy-max@users.noreply.github.com", + "name": "CrazyMax", + "username": "crazy-max" + }, + "committer": { + "email": "crazy-max@users.noreply.github.com", + "name": "CrazyMax", + "username": "crazy-max" + }, + "distinct": true, + "id": "860c1904a1ce19322e91ac35af1ab07466440c37", + "message": "hello dev", + "timestamp": "2022-04-19T11:27:24+02:00", + "tree_id": "d2c60af597e863787d2d27f569e30495b0b92820", + "url": "https://github.com/docker/test-docker-action/commit/860c1904a1ce19322e91ac35af1ab07466440c37" + }, + "organization": { + "avatar_url": "https://avatars.githubusercontent.com/u/5429470?v=4", + "description": "Docker helps developers bring their ideas to life by conquering the complexity of app development.", + "events_url": "https://api.github.com/orgs/docker/events", + "hooks_url": "https://api.github.com/orgs/docker/hooks", + "id": 5429470, + "issues_url": "https://api.github.com/orgs/docker/issues", + "login": "docker", + "members_url": "https://api.github.com/orgs/docker/members{/member}", + "node_id": "MDEyOk9yZ2FuaXphdGlvbjU0Mjk0NzA=", + "public_members_url": "https://api.github.com/orgs/docker/public_members{/member}", + "repos_url": "https://api.github.com/orgs/docker/repos", + "url": "https://api.github.com/orgs/docker" + }, + "pusher": { + "email": "github@crazymax.dev", + "name": "crazy-max" + }, + "ref": "refs/heads/dev", + "repository": { + "allow_forking": true, + "archive_url": "https://api.github.com/repos/docker/test-docker-action/{archive_format}{/ref}", + "archived": false, + "assignees_url": "https://api.github.com/repos/docker/test-docker-action/assignees{/user}", + "blobs_url": "https://api.github.com/repos/docker/test-docker-action/git/blobs{/sha}", + "branches_url": "https://api.github.com/repos/docker/test-docker-action/branches{/branch}", + "clone_url": "https://github.com/docker/test-docker-action.git", + "collaborators_url": "https://api.github.com/repos/docker/test-docker-action/collaborators{/collaborator}", + "comments_url": "https://api.github.com/repos/docker/test-docker-action/comments{/number}", + "commits_url": "https://api.github.com/repos/docker/test-docker-action/commits{/sha}", + "compare_url": "https://api.github.com/repos/docker/test-docker-action/compare/{base}...{head}", + "contents_url": "https://api.github.com/repos/docker/test-docker-action/contents/{+path}", + "contributors_url": "https://api.github.com/repos/docker/test-docker-action/contributors", + "created_at": 1596792180, + "default_branch": "master", + "deployments_url": "https://api.github.com/repos/docker/test-docker-action/deployments", + "description": "Test \"Docker\" Actions", + "disabled": false, + "downloads_url": "https://api.github.com/repos/docker/test-docker-action/downloads", + "events_url": "https://api.github.com/repos/docker/test-docker-action/events", + "fork": false, + "forks": 1, + "forks_count": 1, + "forks_url": "https://api.github.com/repos/docker/test-docker-action/forks", + "full_name": "docker/test-docker-action", + "git_commits_url": "https://api.github.com/repos/docker/test-docker-action/git/commits{/sha}", + "git_refs_url": "https://api.github.com/repos/docker/test-docker-action/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/docker/test-docker-action/git/tags{/sha}", + "git_url": "git://github.com/docker/test-docker-action.git", + "has_downloads": true, + "has_issues": true, + "has_pages": false, + "has_projects": true, + "has_wiki": true, + "homepage": "", + "hooks_url": "https://api.github.com/repos/docker/test-docker-action/hooks", + "html_url": "https://github.com/docker/test-docker-action", + "id": 285789493, + "is_template": false, + "issue_comment_url": "https://api.github.com/repos/docker/test-docker-action/issues/comments{/number}", + "issue_events_url": "https://api.github.com/repos/docker/test-docker-action/issues/events{/number}", + "issues_url": "https://api.github.com/repos/docker/test-docker-action/issues{/number}", + "keys_url": "https://api.github.com/repos/docker/test-docker-action/keys{/key_id}", + "labels_url": "https://api.github.com/repos/docker/test-docker-action/labels{/name}", + "language": "JavaScript", + "languages_url": "https://api.github.com/repos/docker/test-docker-action/languages", + "license": { + "key": "mit", + "name": "MIT License", + "node_id": "MDc6TGljZW5zZTEz", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit" + }, + "master_branch": "master", + "merges_url": "https://api.github.com/repos/docker/test-docker-action/merges", + "milestones_url": "https://api.github.com/repos/docker/test-docker-action/milestones{/number}", + "mirror_url": null, + "name": "test-docker-action", + "node_id": "MDEwOlJlcG9zaXRvcnkyODU3ODk0OTM=", + "notifications_url": "https://api.github.com/repos/docker/test-docker-action/notifications{?since,all,participating}", + "open_issues": 6, + "open_issues_count": 6, + "organization": "docker", + "owner": { + "avatar_url": "https://avatars.githubusercontent.com/u/5429470?v=4", + "email": "info@docker.com", + "events_url": "https://api.github.com/users/docker/events{/privacy}", + "followers_url": "https://api.github.com/users/docker/followers", + "following_url": "https://api.github.com/users/docker/following{/other_user}", + "gists_url": "https://api.github.com/users/docker/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/docker", + "id": 5429470, + "login": "docker", + "name": "docker", + "node_id": "MDEyOk9yZ2FuaXphdGlvbjU0Mjk0NzA=", + "organizations_url": "https://api.github.com/users/docker/orgs", + "received_events_url": "https://api.github.com/users/docker/received_events", + "repos_url": "https://api.github.com/users/docker/repos", + "site_admin": false, + "starred_url": "https://api.github.com/users/docker/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/docker/subscriptions", + "type": "Organization", + "url": "https://api.github.com/users/docker" + }, + "private": true, + "pulls_url": "https://api.github.com/repos/docker/test-docker-action/pulls{/number}", + "pushed_at": 1650360446, + "releases_url": "https://api.github.com/repos/docker/test-docker-action/releases{/id}", + "size": 796, + "ssh_url": "git@github.com:docker/test-docker-action.git", + "stargazers": 0, + "stargazers_count": 0, + "stargazers_url": "https://api.github.com/repos/docker/test-docker-action/stargazers", + "statuses_url": "https://api.github.com/repos/docker/test-docker-action/statuses/{sha}", + "subscribers_url": "https://api.github.com/repos/docker/test-docker-action/subscribers", + "subscription_url": "https://api.github.com/repos/docker/test-docker-action/subscription", + "svn_url": "https://github.com/docker/test-docker-action", + "tags_url": "https://api.github.com/repos/docker/test-docker-action/tags", + "teams_url": "https://api.github.com/repos/docker/test-docker-action/teams", + "topics": [], + "trees_url": "https://api.github.com/repos/docker/test-docker-action/git/trees{/sha}", + "updated_at": "2022-04-19T09:05:09Z", + "url": "https://github.com/docker/test-docker-action", + "visibility": "private", + "watchers": 0, + "watchers_count": 0 + }, + "sender": { + "avatar_url": "https://avatars.githubusercontent.com/u/1951866?v=4", + "events_url": "https://api.github.com/users/crazy-max/events{/privacy}", + "followers_url": "https://api.github.com/users/crazy-max/followers", + "following_url": "https://api.github.com/users/crazy-max/following{/other_user}", + "gists_url": "https://api.github.com/users/crazy-max/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/crazy-max", + "id": 1951866, + "login": "crazy-max", + "node_id": "MDQ6VXNlcjE5NTE4NjY=", + "organizations_url": "https://api.github.com/users/crazy-max/orgs", + "received_events_url": "https://api.github.com/users/crazy-max/received_events", + "repos_url": "https://api.github.com/users/crazy-max/repos", + "site_admin": false, + "starred_url": "https://api.github.com/users/crazy-max/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/crazy-max/subscriptions", + "type": "User", + "url": "https://api.github.com/users/crazy-max" + } +} diff --git a/__tests__/github.test.ts b/__tests__/github.test.ts index 591a867..2478b50 100644 --- a/__tests__/github.test.ts +++ b/__tests__/github.test.ts @@ -1,14 +1,41 @@ import {describe, expect, jest, it} from '@jest/globals'; -import * as github from '../src/github'; +import {Context} from '@actions/github/lib/context'; + +import {GitHub, Payload, ReposGetResponseData} from '../src/github'; + +jest.spyOn(GitHub.prototype, 'context').mockImplementation((): Context => { + return new Context(); +}); import * as repoFixture from './fixtures/repo.json'; -jest.spyOn(github, 'repo').mockImplementation((): Promise => { - return >(repoFixture as unknown); +jest.spyOn(GitHub.prototype, 'repo').mockImplementation((): Promise => { + return >(repoFixture as unknown); +}); + +import * as payloadFixture from './fixtures/github-payload.json'; +jest.spyOn(GitHub.prototype as any, 'payload').mockImplementation((): Promise => { + return >(payloadFixture as unknown); +}); +jest.spyOn(GitHub.prototype as any, 'ref').mockImplementation((): string => { + return 'refs/heads/master'; +}); + +describe('gitContext', () => { + it('returns refs/heads/master', async () => { + expect(GitHub.getInstance().gitContext()).toEqual('https://github.com/docker/test-docker-action.git#refs/heads/master'); + }); }); describe('repo', () => { it('returns GitHub repository', async () => { - const repo = await github.repo(process.env.GITHUB_TOKEN || ''); - expect(repo.name).not.toBeNull(); + const repo = await GitHub.getInstance().repo(process.env.GITHUB_TOKEN || ''); + expect(repo.name).toEqual('Hello-World'); + }); +}); + +describe('fromPayload', () => { + it('returns repository name from payload', async () => { + const repoName = await GitHub.getInstance().fromPayload('repository.name'); + expect(repoName).toEqual('test-docker-action'); }); }); diff --git a/__tests__/util.test.ts b/__tests__/util.test.ts index 7a38f28..6035cee 100644 --- a/__tests__/util.test.ts +++ b/__tests__/util.test.ts @@ -1,76 +1,61 @@ -import {describe, expect, it, jest, test} from '@jest/globals'; +import {describe, expect, it, test} from '@jest/globals'; import * as fs from 'fs'; import * as path from 'path'; -import * as util from '../src/util'; -jest.spyOn(util, 'defaultContext').mockImplementation((): string => { - return 'https://github.com/docker/actions-toolkit.git#refs/heads/test-jest'; -}); - -jest.spyOn(util, 'tmpDir').mockImplementation((): string => { - const tmpDir = path.join('/tmp/.docker-build-push-jest').split(path.sep).join(path.posix.sep); - if (!fs.existsSync(tmpDir)) { - fs.mkdirSync(tmpDir, {recursive: true}); - } - return tmpDir; -}); - -jest.spyOn(util, 'tmpNameSync').mockImplementation((): string => { - return path.join('/tmp/.docker-build-push-jest', '.tmpname-jest').split(path.sep).join(path.posix.sep); -}); +import {Util} from '../src/util'; describe('getInputList', () => { it('single line correctly', async () => { await setInput('foo', 'bar'); - const res = util.getInputList('foo'); + const res = Util.getInputList('foo'); expect(res).toEqual(['bar']); }); it('multiline correctly', async () => { setInput('foo', 'bar\nbaz'); - const res = util.getInputList('foo'); + const res = Util.getInputList('foo'); expect(res).toEqual(['bar', 'baz']); }); it('empty lines correctly', async () => { setInput('foo', 'bar\n\nbaz'); - const res = util.getInputList('foo'); + const res = Util.getInputList('foo'); expect(res).toEqual(['bar', 'baz']); }); it('comma correctly', async () => { setInput('foo', 'bar,baz'); - const res = util.getInputList('foo'); + const res = Util.getInputList('foo'); expect(res).toEqual(['bar', 'baz']); }); it('empty result correctly', async () => { setInput('foo', 'bar,baz,'); - const res = util.getInputList('foo'); + const res = Util.getInputList('foo'); expect(res).toEqual(['bar', 'baz']); }); it('different new lines correctly', async () => { setInput('foo', 'bar\r\nbaz'); - const res = util.getInputList('foo'); + const res = Util.getInputList('foo'); expect(res).toEqual(['bar', 'baz']); }); it('different new lines and comma correctly', async () => { setInput('foo', 'bar\r\nbaz,bat'); - const res = util.getInputList('foo'); + const res = Util.getInputList('foo'); expect(res).toEqual(['bar', 'baz', 'bat']); }); it('multiline and ignoring comma correctly', async () => { setInput('cache-from', 'user/app:cache\ntype=local,src=path/to/dir'); - const res = util.getInputList('cache-from', true); + const res = Util.getInputList('cache-from', true); expect(res).toEqual(['user/app:cache', 'type=local,src=path/to/dir']); }); it('different new lines and ignoring comma correctly', async () => { setInput('cache-from', 'user/app:cache\r\ntype=local,src=path/to/dir'); - const res = util.getInputList('cache-from', true); + const res = Util.getInputList('cache-from', true); expect(res).toEqual(['user/app:cache', 'type=local,src=path/to/dir']); }); @@ -83,7 +68,7 @@ bbbbbbb ccccccccc" FOO=bar` ); - const res = util.getInputList('secrets', true); + const res = Util.getInputList('secrets', true); expect(res).toEqual([ 'GIT_AUTH_TOKEN=abcdefgh,ijklmno=0123456789', `MYSECRET=aaaaaaaa @@ -106,7 +91,7 @@ FOO=bar bbbb ccc"` ); - const res = util.getInputList('secrets', true); + const res = Util.getInputList('secrets', true); expect(res).toEqual([ 'GIT_AUTH_TOKEN=abcdefgh,ijklmno=0123456789', `MYSECRET=aaaaaaaa @@ -129,7 +114,7 @@ bbbbbbb ccccccccc FOO=bar` ); - const res = util.getInputList('secrets', true); + const res = Util.getInputList('secrets', true); expect(res).toEqual(['GIT_AUTH_TOKEN=abcdefgh,ijklmno=0123456789', 'MYSECRET=aaaaaaaa', 'bbbbbbb', 'ccccccccc', 'FOO=bar']); }); @@ -140,7 +125,7 @@ FOO=bar` `"GPG_KEY=${pgp}" FOO=bar` ); - const res = util.getInputList('secrets', true); + const res = Util.getInputList('secrets', true); expect(res).toEqual([`GPG_KEY=${pgp}`, 'FOO=bar']); }); @@ -153,7 +138,7 @@ bbbb""bbb ccccccccc" FOO=bar` ); - const res = util.getInputList('secrets', true); + const res = Util.getInputList('secrets', true); expect(res).toEqual([ 'GIT_AUTH_TOKEN=abcdefgh,ijklmno=0123456789', `MYSECRET=aaaaaaaa @@ -169,7 +154,7 @@ describe('asyncForEach', () => { const testValues = [1, 2, 3, 4, 5]; const results: number[] = []; - await util.asyncForEach(testValues, async value => { + await Util.asyncForEach(testValues, async value => { results.push(value); }); @@ -183,7 +168,7 @@ describe('isValidUrl', () => { ['https://github.com/docker/buildx.git#refs/pull/648/head', true], ['v0.4.1', false] ])('given %p', async (url, expected) => { - expect(util.isValidUrl(url)).toEqual(expected); + expect(Util.isValidUrl(url)).toEqual(expected); }); }); diff --git a/jest.config.ts b/jest.config.ts index 1168b48..e351829 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,4 +1,5 @@ process.env = Object.assign({}, process.env, { + GITHUB_REPOSITORY: 'docker/test-docker-action', RUNNER_TEMP: '/tmp/github_runner', RUNNER_TOOL_CACHE: '/tmp/github_tool_cache' }) as { diff --git a/package.json b/package.json index 44cafab..5711272 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "eslint-plugin-prettier": "^4.0.0", "jest": "^27.2.5", "prettier": "^2.3.1", + "rimraf": "^4.1.1", "ts-jest": "^27.1.2", "ts-node": "^10.7.0", "typescript": "^4.4.4" diff --git a/src/builder.ts b/src/builder.ts new file mode 100644 index 0000000..a75e95b --- /dev/null +++ b/src/builder.ts @@ -0,0 +1,124 @@ +import * as exec from '@actions/exec'; + +import {Buildx} from './buildx'; + +export interface BuilderInfo { + name?: string; + driver?: string; + lastActivity?: Date; + nodes: NodeInfo[]; +} + +export interface NodeInfo { + name?: string; + endpoint?: string; + driverOpts?: Array; + status?: string; + buildkitdFlags?: string; + buildkitVersion?: string; + platforms?: string; +} + +export interface BuilderOpts { + buildx?: Buildx; +} + +export class Builder { + private buildx: Buildx; + + constructor(opts?: BuilderOpts) { + this.buildx = opts?.buildx || new Buildx(); + } + + public async inspect(name: string): Promise { + const cmd = this.buildx.getCommand(['inspect', name]); + return await exec + .getExecOutput(cmd.command, cmd.args, { + ignoreReturnCode: true, + silent: true + }) + .then(res => { + if (res.stderr.length > 0 && res.exitCode != 0) { + throw new Error(res.stderr.trim()); + } + return Builder.parseInspect(res.stdout); + }); + } + + public static parseInspect(data: string): BuilderInfo { + const builder: BuilderInfo = { + nodes: [] + }; + let node: NodeInfo = {}; + for (const line of data.trim().split(`\n`)) { + const [key, ...rest] = line.split(':'); + const value = rest.map(v => v.trim()).join(':'); + if (key.length == 0 || value.length == 0) { + continue; + } + switch (key.toLowerCase()) { + case 'name': { + if (builder.name == undefined) { + builder.name = value; + } else { + if (Object.keys(node).length > 0) { + builder.nodes.push(node); + node = {}; + } + node.name = value; + } + break; + } + case 'driver': { + builder.driver = value; + break; + } + case 'last activity': { + builder.lastActivity = new Date(value); + break; + } + case 'endpoint': { + node.endpoint = value; + break; + } + case 'driver options': { + node.driverOpts = (value.match(/(\w+)="([^"]*)"/g) || []).map(v => v.replace(/^(.*)="(.*)"$/g, '$1=$2')); + break; + } + case 'status': { + node.status = value; + break; + } + case 'flags': { + node.buildkitdFlags = value; + break; + } + case 'buildkit': { + node.buildkitVersion = value; + break; + } + case 'platforms': { + let platforms: Array = []; + // if a preferred platform is being set then use only these + // https://docs.docker.com/engine/reference/commandline/buildx_inspect/#get-information-about-a-builder-instance + if (value.includes('*')) { + for (const platform of value.split(', ')) { + if (platform.includes('*')) { + platforms.push(platform.replace('*', '')); + } + } + } else { + // otherwise set all platforms available + platforms = value.split(', '); + } + node.platforms = platforms.join(','); + break; + } + } + } + if (Object.keys(node).length > 0) { + builder.nodes.push(node); + } + return builder; + } +} diff --git a/src/buildkit.ts b/src/buildkit.ts index fd697b5..7d8469b 100644 --- a/src/buildkit.ts +++ b/src/buildkit.ts @@ -1,96 +1,125 @@ -import * as fs from 'fs'; -import * as semver from 'semver'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; import * as core from '@actions/core'; import * as exec from '@actions/exec'; -import * as buildx from './buildx'; -import * as util from './util'; +import * as semver from 'semver'; +import * as tmp from 'tmp'; -export async function getConfigInline(s: string): Promise { - return getConfig(s, false); +import {Buildx} from './buildx'; +import {Builder, BuilderInfo} from './builder'; + +export interface BuildKitOpts { + buildx?: Buildx; } -export async function getConfigFile(s: string): Promise { - return getConfig(s, true); -} +export class BuildKit { + private buildx: Buildx; + private containerNamePrefix = 'buildx_buildkit_'; + private tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), 'docker-actions-toolkit-')).split(path.sep).join(path.posix.sep); -export async function getConfig(s: string, file: boolean): Promise { - if (file) { - if (!fs.existsSync(s)) { - throw new Error(`config file ${s} not found`); + constructor(opts?: BuildKitOpts) { + this.buildx = opts?.buildx || new Buildx(); + } + + private async getBuilderInfo(name: string): Promise { + const builder = new Builder({buildx: this.buildx}); + return builder.inspect(name); + } + + public async getVersion(builderName: string): Promise { + const builderInfo = await this.getBuilderInfo(builderName); + if (builderInfo.nodes.length == 0) { + // a builder always have on node, should not happen. + return undefined; } - s = fs.readFileSync(s, {encoding: 'utf-8'}); - } - const configFile = util.tmpNameSync({ - tmpdir: util.tmpDir() - }); - fs.writeFileSync(configFile, s); - return configFile; -} - -export async function getVersion(builderName: string, standalone?: boolean): Promise { - const builder = await buildx.inspect(builderName, standalone); - if (builder.nodes.length == 0) { - // a builder always have on node, should not happen. - return undefined; - } - // TODO: get version for all nodes - const node = builder.nodes[0]; - if (!node.buildkit && node.name) { - try { - return await getVersionWithinImage(node.name); - } catch (e) { - core.warning(e); - } - } - return node.buildkit; -} - -async function getVersionWithinImage(nodeName: string): Promise { - return exec - .getExecOutput(`docker`, ['inspect', '--format', '{{.Config.Image}}', `buildx_buildkit_${nodeName}`], { - ignoreReturnCode: true, - silent: true - }) - .then(bkitimage => { - if (bkitimage.exitCode == 0 && bkitimage.stdout.length > 0) { - return exec - .getExecOutput(`docker`, ['run', '--rm', bkitimage.stdout.trim(), '--version'], { - ignoreReturnCode: true, - silent: true - }) - .then(bkitversion => { - if (bkitversion.exitCode == 0 && bkitversion.stdout.length > 0) { - return `${bkitimage.stdout.trim()} => ${bkitversion.stdout.trim()}`; - } else if (bkitversion.stderr.length > 0) { - throw new Error(bkitimage.stderr.trim()); - } - return bkitversion.stdout.trim(); - }); - } else if (bkitimage.stderr.length > 0) { - throw new Error(bkitimage.stderr.trim()); - } - return bkitimage.stdout.trim(); - }); -} - -export async function satisfies(builderName: string, range: string, standalone?: boolean): Promise { - const builder = await buildx.inspect(builderName, standalone); - for (const node of builder.nodes) { - let bkversion = node.buildkit; - if (!bkversion) { + // TODO: get version for all nodes + const node = builderInfo.nodes[0]; + if (!node.buildkitVersion && node.name) { try { - bkversion = await getVersionWithinImage(node.name || ''); + return await this.getVersionWithinImage(node.name); } catch (e) { + core.warning(e); + } + } + return node.buildkitVersion; + } + + private async getVersionWithinImage(nodeName: string): Promise { + return exec + .getExecOutput(`docker`, ['inspect', '--format', '{{.Config.Image}}', `${this.containerNamePrefix}${nodeName}`], { + ignoreReturnCode: true, + silent: true + }) + .then(bkitimage => { + if (bkitimage.exitCode == 0 && bkitimage.stdout.length > 0) { + return exec + .getExecOutput(`docker`, ['run', '--rm', bkitimage.stdout.trim(), '--version'], { + ignoreReturnCode: true, + silent: true + }) + .then(bkitversion => { + if (bkitversion.exitCode == 0 && bkitversion.stdout.length > 0) { + return `${bkitimage.stdout.trim()} => ${bkitversion.stdout.trim()}`; + } else if (bkitversion.stderr.length > 0) { + throw new Error(bkitimage.stderr.trim()); + } + return bkitversion.stdout.trim(); + }); + } else if (bkitimage.stderr.length > 0) { + throw new Error(bkitimage.stderr.trim()); + } + return bkitimage.stdout.trim(); + }); + } + + public async versionSatisfies(builderName: string, range: string): Promise { + const builderInfo = await this.getBuilderInfo(builderName); + for (const node of builderInfo.nodes) { + let bkversion = node.buildkitVersion; + if (!bkversion) { + try { + bkversion = await this.getVersionWithinImage(node.name || ''); + } catch (e) { + return false; + } + } + // BuildKit version reported by moby is in the format of `v0.11.0-moby` + if (builderInfo.driver == 'docker' && !bkversion.endsWith('-moby')) { + return false; + } + if (!semver.satisfies(bkversion.replace(/-moby$/, ''), range)) { return false; } } - // BuildKit version reported by moby is in the format of `v0.11.0-moby` - if (builder.driver == 'docker' && !bkversion.endsWith('-moby')) { - return false; - } - if (!semver.satisfies(bkversion.replace(/-moby$/, ''), range)) { - return false; - } + return true; + } + + public generateConfigInline(s: string): string { + return this.generateConfig(s, false); + } + + public generateConfigFile(s: string): string { + return this.generateConfig(s, true); + } + + private generateConfig(s: string, file: boolean): string { + if (file) { + if (!fs.existsSync(s)) { + throw new Error(`config file ${s} not found`); + } + s = fs.readFileSync(s, {encoding: 'utf-8'}); + } + const configFile = this.tmpName({tmpdir: this.tmpDir()}); + fs.writeFileSync(configFile, s); + return configFile; + } + + private tmpDir() { + return this.tmpdir; + } + + private tmpName(options?: tmp.TmpNameOptions): string { + return tmp.tmpNameSync(options); } - return true; } diff --git a/src/buildx.ts b/src/buildx.ts index ed27246..282f275 100644 --- a/src/buildx.ts +++ b/src/buildx.ts @@ -1,281 +1,188 @@ -import {parse} from 'csv-parse/sync'; import fs from 'fs'; +import os from 'os'; import path from 'path'; -import * as semver from 'semver'; import * as exec from '@actions/exec'; -import * as util from './util'; +import {parse} from 'csv-parse/sync'; +import * as semver from 'semver'; +import * as tmp from 'tmp'; -export type Builder = { - name?: string; - driver?: string; - 'last-activity'?: Date; - nodes: Node[]; -}; - -export type Node = { - name?: string; - endpoint?: string; - 'driver-opts'?: Array; - status?: string; - 'buildkitd-flags'?: string; - buildkit?: string; - platforms?: string; -}; - -export async function getImageIDFile(): Promise { - return path.join(util.tmpDir(), 'iidfile').split(path.sep).join(path.posix.sep); +export interface BuildxOpts { + standalone?: boolean; } -export async function getImageID(): Promise { - const iidFile = await getImageIDFile(); - if (!fs.existsSync(iidFile)) { - return undefined; - } - return fs.readFileSync(iidFile, {encoding: 'utf-8'}).trim(); -} +export class Buildx { + private standalone: boolean; + private version: Promise; + private tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), 'docker-actions-toolkit-')).split(path.sep).join(path.posix.sep); -export async function getMetadataFile(): Promise { - return path.join(util.tmpDir(), 'metadata-file').split(path.sep).join(path.posix.sep); -} - -export async function getMetadata(): Promise { - const metadataFile = await getMetadataFile(); - if (!fs.existsSync(metadataFile)) { - return undefined; - } - const content = fs.readFileSync(metadataFile, {encoding: 'utf-8'}).trim(); - if (content === 'null') { - return undefined; - } - return content; -} - -export async function getDigest(metadata: string | undefined): Promise { - if (metadata === undefined) { - return undefined; - } - const metadataJSON = JSON.parse(metadata); - if (metadataJSON['containerimage.digest']) { - return metadataJSON['containerimage.digest']; - } - return undefined; -} - -export async function getSecretString(kvp: string): Promise { - return getSecret(kvp, false); -} - -export async function getSecretFile(kvp: string): Promise { - return getSecret(kvp, true); -} - -export async function getSecret(kvp: string, file: boolean): Promise { - const delimiterIndex = kvp.indexOf('='); - const key = kvp.substring(0, delimiterIndex); - let value = kvp.substring(delimiterIndex + 1); - if (key.length == 0 || value.length == 0) { - throw new Error(`${kvp} is not a valid secret`); + constructor(opts?: BuildxOpts) { + this.standalone = opts?.standalone ?? false; + this.version = this.getVersion(); } - if (file) { - if (!fs.existsSync(value)) { - throw new Error(`secret file ${value} not found`); + public getCommand(args: Array) { + return { + command: this.standalone ? 'buildx' : 'docker', + args: this.standalone ? args : ['buildx', ...args] + }; + } + + public async isAvailable(): Promise { + const cmd = this.getCommand([]); + return await exec + .getExecOutput(cmd.command, cmd.args, { + ignoreReturnCode: true, + silent: true + }) + .then(res => { + if (res.stderr.length > 0 && res.exitCode != 0) { + return false; + } + return res.exitCode == 0; + }) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .catch(error => { + return false; + }); + } + + public async getVersion(): Promise { + const cmd = this.getCommand(['version']); + return await exec + .getExecOutput(cmd.command, cmd.args, { + ignoreReturnCode: true, + silent: true + }) + .then(res => { + if (res.stderr.length > 0 && res.exitCode != 0) { + throw new Error(res.stderr.trim()); + } + return Buildx.parseVersion(res.stdout.trim()); + }); + } + + public static parseVersion(stdout: string): string { + const matches = /\sv?([0-9a-f]{7}|[0-9.]+)/.exec(stdout); + if (!matches) { + throw new Error(`Cannot parse buildx version`); } - value = fs.readFileSync(value, {encoding: 'utf-8'}); + return matches[1]; } - const secretFile = util.tmpNameSync({ - tmpdir: util.tmpDir() - }); - fs.writeFileSync(secretFile, value); + public static versionSatisfies(version: string, range: string): boolean { + return semver.satisfies(version, range) || /^[0-9a-f]{7}$/.exec(version) !== null; + } - return `id=${key},src=${secretFile}`; -} + public getBuildImageIDFilePath(): string { + return path.join(this.tmpDir(), 'iidfile').split(path.sep).join(path.posix.sep); + } -export function hasLocalExporter(outputs: string[]): boolean { - return hasExporterType('local', outputs); -} + public getBuildMetadataFilePath(): string { + return path.join(this.tmpDir(), 'metadata-file').split(path.sep).join(path.posix.sep); + } -export function hasTarExporter(outputs: string[]): boolean { - return hasExporterType('tar', outputs); -} - -export function hasLocalOrTarExporter(outputs: string[]): boolean { - return hasLocalExporter(outputs) || hasTarExporter(outputs); -} - -function hasExporterType(name: string, outputs: string[]): boolean { - const records = parse(outputs.join(`\n`), { - delimiter: ',', - trim: true, - columns: false, - relaxColumnCount: true - }); - for (const record of records) { - if (record.length == 1 && !record[0].startsWith('type=')) { - // Local if no type is defined - // https://github.com/docker/buildx/blob/d2bf42f8b4784d83fde17acb3ed84703ddc2156b/build/output.go#L29-L43 - return name == 'local'; + public getBuildImageID(): string | undefined { + const iidFile = this.getBuildImageIDFilePath(); + if (!fs.existsSync(iidFile)) { + return undefined; } - for (const [key, value] of record.map(chunk => chunk.split('=').map(item => item.trim()))) { - if (key == 'type' && value == name) { + return fs.readFileSync(iidFile, {encoding: 'utf-8'}).trim(); + } + + public getBuildMetadata(): string | undefined { + const metadataFile = this.getBuildMetadataFilePath(); + if (!fs.existsSync(metadataFile)) { + return undefined; + } + const content = fs.readFileSync(metadataFile, {encoding: 'utf-8'}).trim(); + if (content === 'null') { + return undefined; + } + return content; + } + + public getDigest(): string | undefined { + const metadata = this.getBuildMetadata(); + if (metadata === undefined) { + return undefined; + } + const metadataJSON = JSON.parse(metadata); + if (metadataJSON['containerimage.digest']) { + return metadataJSON['containerimage.digest']; + } + return undefined; + } + + public generateBuildSecretString(kvp: string): string { + return this.generateBuildSecret(kvp, false); + } + + public generateBuildSecretFile(kvp: string): string { + return this.generateBuildSecret(kvp, true); + } + + private generateBuildSecret(kvp: string, file: boolean): string { + const delimiterIndex = kvp.indexOf('='); + const key = kvp.substring(0, delimiterIndex); + let value = kvp.substring(delimiterIndex + 1); + if (key.length == 0 || value.length == 0) { + throw new Error(`${kvp} is not a valid secret`); + } + if (file) { + if (!fs.existsSync(value)) { + throw new Error(`secret file ${value} not found`); + } + value = fs.readFileSync(value, {encoding: 'utf-8'}); + } + const secretFile = this.tmpName({tmpdir: this.tmpDir()}); + fs.writeFileSync(secretFile, value); + return `id=${key},src=${secretFile}`; + } + + public static hasLocalExporter(exporters: string[]): boolean { + return Buildx.hasExporterType('local', exporters); + } + + public static hasTarExporter(exporters: string[]): boolean { + return Buildx.hasExporterType('tar', exporters); + } + + public static hasExporterType(name: string, exporters: string[]): boolean { + const records = parse(exporters.join(`\n`), { + delimiter: ',', + trim: true, + columns: false, + relaxColumnCount: true + }); + for (const record of records) { + if (record.length == 1 && !record[0].startsWith('type=')) { + // Local if no type is defined + // https://github.com/docker/buildx/blob/d2bf42f8b4784d83fde17acb3ed84703ddc2156b/build/output.go#L29-L43 + return name == 'local'; + } + for (const [key, value] of record.map(chunk => chunk.split('=').map(item => item.trim()))) { + if (key == 'type' && value == name) { + return true; + } + } + } + return false; + } + + public static hasGitAuthTokenSecret(secrets: string[]): boolean { + for (const secret of secrets) { + if (secret.startsWith('GIT_AUTH_TOKEN=')) { return true; } } + return false; } - return false; -} -export function hasGitAuthToken(secrets: string[]): boolean { - for (const secret of secrets) { - if (secret.startsWith('GIT_AUTH_TOKEN=')) { - return true; - } + private tmpDir() { + return this.tmpdir; } - return false; -} -export async function isAvailable(standalone?: boolean): Promise { - const cmd = getCommand([], standalone); - return await exec - .getExecOutput(cmd.command, cmd.args, { - ignoreReturnCode: true, - silent: true - }) - .then(res => { - if (res.stderr.length > 0 && res.exitCode != 0) { - return false; - } - return res.exitCode == 0; - }) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - .catch(error => { - return false; - }); -} - -export async function inspect(name: string, standalone?: boolean): Promise { - const cmd = getCommand(['inspect', name], standalone); - return await exec - .getExecOutput(cmd.command, cmd.args, { - ignoreReturnCode: true, - silent: true - }) - .then(res => { - if (res.stderr.length > 0 && res.exitCode != 0) { - throw new Error(res.stderr.trim()); - } - return parseInspect(res.stdout); - }); -} - -export async function parseInspect(data: string): Promise { - const builder: Builder = { - nodes: [] - }; - let node: Node = {}; - for (const line of data.trim().split(`\n`)) { - const [key, ...rest] = line.split(':'); - const value = rest.map(v => v.trim()).join(':'); - if (key.length == 0 || value.length == 0) { - continue; - } - switch (key.toLowerCase()) { - case 'name': { - if (builder.name == undefined) { - builder.name = value; - } else { - if (Object.keys(node).length > 0) { - builder.nodes.push(node); - node = {}; - } - node.name = value; - } - break; - } - case 'driver': { - builder.driver = value; - break; - } - case 'last activity': { - builder['last-activity'] = new Date(value); - break; - } - case 'endpoint': { - node.endpoint = value; - break; - } - case 'driver options': { - node['driver-opts'] = (value.match(/(\w+)="([^"]*)"/g) || []).map(v => v.replace(/^(.*)="(.*)"$/g, '$1=$2')); - break; - } - case 'status': { - node.status = value; - break; - } - case 'flags': { - node['buildkitd-flags'] = value; - break; - } - case 'buildkit': { - node.buildkit = value; - break; - } - case 'platforms': { - let platforms: Array = []; - // if a preferred platform is being set then use only these - // https://docs.docker.com/engine/reference/commandline/buildx_inspect/#get-information-about-a-builder-instance - if (value.includes('*')) { - for (const platform of value.split(', ')) { - if (platform.includes('*')) { - platforms.push(platform.replace('*', '')); - } - } - } else { - // otherwise set all platforms available - platforms = value.split(', '); - } - node.platforms = platforms.join(','); - break; - } - } + private tmpName(options?: tmp.TmpNameOptions): string { + return tmp.tmpNameSync(options); } - if (Object.keys(node).length > 0) { - builder.nodes.push(node); - } - return builder; -} - -export async function getVersion(standalone?: boolean): Promise { - const cmd = getCommand(['version'], standalone); - return await exec - .getExecOutput(cmd.command, cmd.args, { - ignoreReturnCode: true, - silent: true - }) - .then(res => { - if (res.stderr.length > 0 && res.exitCode != 0) { - throw new Error(res.stderr.trim()); - } - return parseVersion(res.stdout.trim()); - }); -} - -export function parseVersion(stdout: string): string { - const matches = /\sv?([0-9a-f]{7}|[0-9.]+)/.exec(stdout); - if (!matches) { - throw new Error(`Cannot parse buildx version`); - } - return matches[1]; -} - -export function satisfies(version: string, range: string): boolean { - return semver.satisfies(version, range) || /^[0-9a-f]{7}$/.exec(version) !== null; -} - -export function getCommand(args: Array, standalone?: boolean) { - return { - command: standalone ? 'buildx' : 'docker', - args: standalone ? args : ['buildx', ...args] - }; } diff --git a/src/docker.ts b/src/docker.ts index 46497b2..05dcbec 100644 --- a/src/docker.ts +++ b/src/docker.ts @@ -1,19 +1,21 @@ import * as exec from '@actions/exec'; -export async function isAvailable(): Promise { - return await exec - .getExecOutput('docker', undefined, { - ignoreReturnCode: true, - silent: true - }) - .then(res => { - if (res.stderr.length > 0 && res.exitCode != 0) { +export class Docker { + public static async isAvailable(): Promise { + return await exec + .getExecOutput('docker', undefined, { + ignoreReturnCode: true, + silent: true + }) + .then(res => { + if (res.stderr.length > 0 && res.exitCode != 0) { + return false; + } + return res.exitCode == 0; + }) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .catch(error => { return false; - } - return res.exitCode == 0; - }) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - .catch(error => { - return false; - }); + }); + } } diff --git a/src/github.ts b/src/github.ts index 8db4af3..efbe808 100644 --- a/src/github.ts +++ b/src/github.ts @@ -2,30 +2,73 @@ import jwt_decode, {JwtPayload} from 'jwt-decode'; import * as github from '@actions/github'; import {Context} from '@actions/github/lib/context'; import {components as OctoOpenApiTypes} from '@octokit/openapi-types'; -import * as util from './util'; +import {WebhookPayload} from '@actions/github/lib/interfaces'; +export type Payload = WebhookPayload; export type ReposGetResponseData = OctoOpenApiTypes['schemas']['repository']; -export function context(): Context { - return github.context; -} - -export async function repo(token: string): Promise { - return github - .getOctokit(token) - .rest.repos.get({...github.context.repo}) - .then(response => response.data as ReposGetResponseData); -} - interface Jwt extends JwtPayload { ac?: string; } -export const parseRuntimeToken = (token: string): Jwt => { - return jwt_decode(token); -}; +export class GitHub { + private static instance?: GitHub; + static getInstance = (): GitHub => (GitHub.instance = GitHub.instance ?? new GitHub()); + private static _gitContext: string; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function fromPayload(path: string): any { - return util.select(github.context.payload, path); + private constructor() { + let ref = this.ref(); + if (github.context.sha && ref && !ref.startsWith('refs/')) { + ref = `refs/heads/${this.ref()}`; + } + if (github.context.sha && !ref.startsWith(`refs/pull/`)) { + ref = github.context.sha; + } + GitHub._gitContext = `${process.env.GITHUB_SERVER_URL || 'https://github.com'}/${github.context.repo.owner}/${github.context.repo.repo}.git#${ref}`; + } + + public context(): Context { + return github.context; + } + + private ref(): string { + return github.context.ref; + } + + public gitContext() { + return GitHub._gitContext; + } + + private payload(): Payload { + return github.context.payload; + } + + public repo(token: string): Promise { + return github + .getOctokit(token) + .rest.repos.get({...github.context.repo}) + .then(response => response.data as ReposGetResponseData); + } + + public parseRuntimeToken(): Jwt { + return jwt_decode(process.env['ACTIONS_RUNTIME_TOKEN'] || ''); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public fromPayload(path: string): any { + return this.select(this.payload(), path); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private select(obj: any, path: string): any { + if (!obj) { + return undefined; + } + const i = path.indexOf('.'); + if (i < 0) { + return obj[path]; + } + const key = path.slice(0, i); + return this.select(obj[key], path.slice(i + 1)); + } } diff --git a/src/util.ts b/src/util.ts index d9d4864..4a2a496 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,92 +1,49 @@ -import * as fs from 'fs'; -import * as os from 'os'; -import * as path from 'path'; -import * as tmp from 'tmp'; import * as core from '@actions/core'; -import * as github from '@actions/github'; import {parse} from 'csv-parse/sync'; -let _defaultContext, _tmpDir: string; +export class Util { + public static getInputList(name: string, ignoreComma?: boolean): string[] { + const res: Array = []; -export function defaultContext(): string { - if (!_defaultContext) { - let ref = github.context.ref; - if (github.context.sha && ref && !ref.startsWith('refs/')) { - ref = `refs/heads/${github.context.ref}`; + const items = core.getInput(name); + if (items == '') { + return res; } - if (github.context.sha && !ref.startsWith(`refs/pull/`)) { - ref = github.context.sha; + + const records = parse(items, { + columns: false, + relaxQuotes: true, + comment: '#', + relaxColumnCount: true, + skipEmptyLines: true + }); + + for (const record of records as Array) { + if (record.length == 1) { + res.push(record[0]); + continue; + } else if (!ignoreComma) { + res.push(...record); + continue; + } + res.push(record.join(',')); } - _defaultContext = `${process.env.GITHUB_SERVER_URL || 'https://github.com'}/${github.context.repo.owner}/${github.context.repo.repo}.git#${ref}`; - } - return _defaultContext; -} -export function tmpDir(): string { - if (!_tmpDir) { - _tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'docker-actions-toolkit-')).split(path.sep).join(path.posix.sep); - } - return _tmpDir; -} - -export function tmpNameSync(options?: tmp.TmpNameOptions): string { - return tmp.tmpNameSync(options); -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function select(obj: any, path: string): any { - if (!obj) { - return undefined; - } - const i = path.indexOf('.'); - if (i < 0) { - return obj[path]; - } - const key = path.slice(0, i); - return select(obj[key], path.slice(i + 1)); -} - -export function getInputList(name: string, ignoreComma?: boolean): string[] { - const res: Array = []; - - const items = core.getInput(name); - if (items == '') { - return res; + return res.filter(item => item).map(pat => pat.trim()); } - const records = parse(items, { - columns: false, - relaxQuotes: true, - comment: '#', - relaxColumnCount: true, - skipEmptyLines: true - }); - - for (const record of records as Array) { - if (record.length == 1) { - res.push(record[0]); - continue; - } else if (!ignoreComma) { - res.push(...record); - continue; + public static async asyncForEach(array, callback) { + for (let index = 0; index < array.length; index++) { + await callback(array[index], index, array); } - res.push(record.join(',')); } - return res.filter(item => item).map(pat => pat.trim()); -} - -export const asyncForEach = async (array, callback) => { - for (let index = 0; index < array.length; index++) { - await callback(array[index], index, array); - } -}; - -export function isValidUrl(url: string): boolean { - try { - new URL(url); - } catch (e) { - return false; - } - return true; + public static isValidUrl(url: string): boolean { + try { + new URL(url); + } catch (e) { + return false; + } + return true; + } } diff --git a/yarn.lock b/yarn.lock index 9163781..4209353 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3541,6 +3541,11 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" +rimraf@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-4.1.1.tgz#ec29817863e5d82d22bca82f9dc4325be2f1e72b" + integrity sha512-Z4Y81w8atcvaJuJuBB88VpADRH66okZAuEm+Jtaufa+s7rZmIz+Hik2G53kGaNytE7lsfXyWktTmfVz0H9xuDg== + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"