diff --git a/README.md b/README.md index 218fefaf..a3591edd 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ This action will create a github release and optionally upload an artifact to it. ## Action Inputs -- **artifact**: A path to an optional artifact to upload to the release. +- **artifact**: 'An optional set of paths representing artifacts to upload to the release. This may be a single path or a comma delimited list of paths (or globs). +- **artifacts**: 'An optional set of paths representing artifacts to upload to the release. This may be a single path or a comma delimited list of paths (or globs). - **artifactContentType**: The content type of the artifact. Defaults to raw. - **body**: An optional body for the release. - **bodyFile**: An optional body file for the release. This should be the path to the file. @@ -32,7 +33,7 @@ jobs: - uses: actions/checkout@v1 - uses: ncipollo/release-action@v1 with: - artifact: "release.tar.gz" + artifacts: "release.tar.gz,foo/*.txt" bodyFile: "body.md" token: ${{ secrets.GITHUB_TOKEN }} diff --git a/__tests__/Action.test.ts b/__tests__/Action.test.ts index fe24cd99..3c3ea6cd 100644 --- a/__tests__/Action.test.ts +++ b/__tests__/Action.test.ts @@ -1,13 +1,17 @@ import { Action } from "../src/Action"; +import { Artifact } from "../src/Artifact"; import { Inputs } from "../src/Inputs"; import { Releases } from "../src/Releases"; +import { ArtifactUploader } from "../src/ArtifactUploader"; const createMock = jest.fn() const uploadMock = jest.fn() -const artifactPath = 'a/path' -const artifactName = 'path' -const artifactData = Buffer.from('blob','utf-8') +const artifacts = [ + new Artifact('a/art1'), + new Artifact('b/art2') +] +const artifactData = Buffer.from('blob', 'utf-8') const body = 'body' const commit = 'commit' const contentType = "raw" @@ -44,7 +48,7 @@ describe("Action", () => { await action.perform() expect(createMock).toBeCalledWith(tag, body, commit, draft, name) - expect(uploadMock).toBeCalledWith(url, contentLength, contentType, artifactData, 'path') + expect(uploadMock).toBeCalledWith(artifacts, url) }) it('throws error when create fails', async () => { @@ -79,28 +83,25 @@ describe("Action", () => { } expect(createMock).toBeCalledWith(tag, body, commit, draft, name) - expect(uploadMock).toBeCalledWith(url, contentLength, contentType, artifactData, 'path') + expect(uploadMock).toBeCalledWith(artifacts, url) }) function createAction(hasArtifact: boolean): Action { - let artifact: string + let inputArtifact: Artifact[] if (hasArtifact) { - artifact = artifactPath + inputArtifact = artifacts } else { - artifact = '' + inputArtifact = [] } const MockReleases = jest.fn(() => { return { create: createMock, - uploadArtifact: uploadMock + uploadArtifact: jest.fn() } }) const MockInputs = jest.fn(() => { return { - artifact: artifact, - artifactName: artifactName, - artifactContentType: contentType, - artifactContentLength: contentLength, + artifacts: inputArtifact, body: body, commit: commit, draft: draft, @@ -110,9 +111,16 @@ describe("Action", () => { readArtifact: () => artifactData } }) + const MockUploader = jest.fn(() => { + return { + uploadArtifacts: uploadMock + } + }) + const inputs = new MockInputs() const releases = new MockReleases() + const uploader = new MockUploader() - return new Action(inputs, releases) + return new Action(inputs, releases, uploader) } }) \ No newline at end of file diff --git a/__tests__/Artifact.test.ts b/__tests__/Artifact.test.ts new file mode 100644 index 00000000..23cf3f51 --- /dev/null +++ b/__tests__/Artifact.test.ts @@ -0,0 +1,38 @@ +import { Artifact } from "../src/Artifact"; + +const fileContents = Buffer.from('artful facts', 'utf-8') +const contentLength = 42 + +jest.mock('fs', () => { + return { + readFileSync: () => fileContents, + statSync: () => { return { size: contentLength } } + }; +}) + +describe("Artifact", () => { + it('defaults contentType to raw', () => { + const artifact = new Artifact('') + expect(artifact.contentType).toBe('raw') + }) + + it('generates name from path', () => { + const artifact = new Artifact('some/artifact') + expect(artifact.name).toBe('artifact') + }) + + it('provides contentLength', () => { + const artifact = new Artifact('some/artifact') + expect(artifact.contentLength).toBe(contentLength) + }) + + it('provides path', () => { + const artifact = new Artifact('some/artifact') + expect(artifact.path).toBe('some/artifact') + }) + + it('reads artifact', () => { + const artifact = new Artifact('some/artifact') + expect(artifact.readFile()).toBe(fileContents) + }) +}) \ No newline at end of file diff --git a/__tests__/ArtifactGlobber.test.ts b/__tests__/ArtifactGlobber.test.ts new file mode 100644 index 00000000..b9b72145 --- /dev/null +++ b/__tests__/ArtifactGlobber.test.ts @@ -0,0 +1,39 @@ +import { FileArtifactGlobber } from "../src/ArtifactGlobber" +import { Globber } from "../src/Globber"; +import { Artifact } from "../src/Artifact"; + +const contentType = "raw" +const globResults = ["file1", "file2"] + +describe("ArtifactGlobber", () => { + it("globs simple path", () => { + const globber = createArtifactGlobber() + + const expectedArtifacts = + globResults.map((path) => new Artifact(path, contentType)) + + expect(globber.globArtifactString('path', 'raw')) + .toEqual(expectedArtifacts) + }) + + it("splits multiple paths", () => { + const globber = createArtifactGlobber() + + const expectedArtifacts = + globResults + .concat(globResults) + .map((path) => new Artifact(path, contentType)) + + expect(globber.globArtifactString('path1,path2', 'raw')) + .toEqual(expectedArtifacts) + }) + + function createArtifactGlobber(): FileArtifactGlobber { + const MockGlobber = jest.fn(() => { + return { + glob: () => globResults + } + }) + return new FileArtifactGlobber(new MockGlobber()) + } +}) \ No newline at end of file diff --git a/__tests__/ArtifactUploader.test.ts b/__tests__/ArtifactUploader.test.ts new file mode 100644 index 00000000..12704f93 --- /dev/null +++ b/__tests__/ArtifactUploader.test.ts @@ -0,0 +1,43 @@ +import { Artifact } from "../src/Artifact" +import { GithubArtifactUploader } from "../src/ArtifactUploader" +import { Releases } from "../src/Releases"; + +const artifacts = [ + new Artifact('a/art1'), + new Artifact('b/art2') +] +const fileContents = Buffer.from('artful facts', 'utf-8') +const contentLength = 42 +const uploadMock = jest.fn() +const url = 'http://api.example.com' + +jest.mock('fs', () => { + return { + readFileSync: () => fileContents, + statSync: () => { return { size: contentLength } } + }; +}) + +describe('ArtifactUploader', () => { + it('uploads artifacts', () => { + const uploader = createUploader() + + uploader.uploadArtifacts(artifacts, url) + + expect(uploadMock).toBeCalledTimes(2) + expect(uploadMock) + .toBeCalledWith(url, contentLength, 'raw', fileContents, 'art1') + expect(uploadMock) + .toBeCalledWith(url, contentLength, 'raw', fileContents, 'art2') + }) + + function createUploader(): GithubArtifactUploader { + const MockReleases = jest.fn(() => { + return { + create: jest.fn(), + uploadArtifact: uploadMock + } + }) + return new GithubArtifactUploader(new MockReleases()) + } +}); \ No newline at end of file diff --git a/__tests__/Inputs.test.ts b/__tests__/Inputs.test.ts index 194b1ced..79819dad 100644 --- a/__tests__/Inputs.test.ts +++ b/__tests__/Inputs.test.ts @@ -1,89 +1,100 @@ const mockGetInput = jest.fn(); +const mockGlob = jest.fn() const mockReadFileSync = jest.fn(); const mockStatSync = jest.fn(); -import { Inputs, CoreInputs } from "../src/Inputs"; +import { Artifact } from "../src/Artifact"; +import { ArtifactGlobber } from "../src/ArtifactGlobber"; import { Context } from "@actions/github/lib/context"; +import { Inputs, CoreInputs } from "../src/Inputs"; +const artifacts = [ + new Artifact('a/art1'), + new Artifact('b/art2') +] jest.mock('@actions/core', () => { return { getInput: mockGetInput }; }) jest.mock('fs', () => { - return { + return { readFileSync: mockReadFileSync, statSync: mockStatSync - }; + }; }) describe('Inputs', () => { let context: Context; let inputs: Inputs; beforeEach(() => { - mockGetInput.mockReturnValue(null) + mockGetInput.mockReset() context = new Context() - inputs = new CoreInputs(context) - }) - - it('reads artifact', () => { - mockGetInput.mockReturnValue("a/path") - mockReadFileSync.mockReturnValue('file') - expect(inputs.artifact).toBe("a/path") - }) - - it('returns artifact', () => { - mockGetInput.mockReturnValue("a/path") - expect(inputs.artifact).toBe("a/path") - }) - - it('returns artifactContentLength', () => { - mockGetInput.mockReturnValue("a/path") - mockStatSync.mockReturnValue({size: 100}) - expect(inputs.artifactContentLength).toBe(100) - }) - - it('returns artifactName', () => { - mockGetInput.mockReturnValue("a/path") - expect(inputs.artifactName).toBe("path") + inputs = new CoreInputs(createGlobber(), context) }) it('returns targetCommit', () => { - mockGetInput.mockReturnValue("42") - expect(inputs.commit).toBe("42") + mockGetInput.mockReturnValue('42') + expect(inputs.commit).toBe('42') }) it('returns token', () => { - mockGetInput.mockReturnValue("42") - expect(inputs.token).toBe("42") + mockGetInput.mockReturnValue('42') + expect(inputs.token).toBe('42') }) - describe('artifactContentType', () => { - it('returns input content-type', () => { - mockGetInput.mockReturnValue("type") - expect(inputs.artifactContentType).toBe("type") + describe('artifacts', () => { + it('returns empty artifacts', () => { + mockGetInput.mockReturnValueOnce('') + .mockReturnValueOnce('') + + expect(inputs.artifacts).toEqual([]) + expect(mockGlob).toBeCalledTimes(0) + }) + + it('returns input.artifacts', () => { + mockGetInput.mockReturnValueOnce('art1') + .mockReturnValueOnce('contentType') + + expect(inputs.artifacts).toEqual(artifacts) + expect(mockGlob).toBeCalledTimes(1) + expect(mockGlob).toBeCalledWith('art1', 'contentType') }) - it('returns raw as default', () => { - expect(inputs.artifactContentType).toBe("raw") + it('returns input.artifacts with default contentType', () => { + mockGetInput.mockReturnValueOnce('art1') + + expect(inputs.artifacts).toEqual(artifacts) + expect(mockGlob).toBeCalledTimes(1) + expect(mockGlob).toBeCalledWith('art1', 'raw') + }) + + it('returns input.artifact', () => { + mockGetInput.mockReturnValueOnce('') + .mockReturnValueOnce('art2') + .mockReturnValueOnce('contentType') + + expect(inputs.artifacts).toEqual(artifacts) + expect(mockGlob).toBeCalledTimes(1) + expect(mockGlob).toBeCalledWith('art2', 'contentType') }) }) describe('body', () => { it('returns input body', () => { - mockGetInput.mockReturnValue("body") - expect(inputs.body).toBe("body") + mockGetInput.mockReturnValue('body') + expect(inputs.body).toBe('body') }) it('returns body file contents', () => { - mockGetInput.mockReturnValueOnce("").mockReturnValueOnce("a/path") - mockReadFileSync.mockReturnValue("file") + mockGetInput.mockReturnValueOnce('').mockReturnValueOnce('a/path') + mockReadFileSync.mockReturnValue('file') - expect(inputs.body).toBe("file") + expect(inputs.body).toBe('file') }) it('returns empty', () => { - expect(inputs.body).toBe("") + expect(inputs.body).toBe('') }) }) @@ -91,43 +102,53 @@ describe('Inputs', () => { it('returns false', () => { expect(inputs.draft).toBe(false) }) - + it('returns true', () => { - mockGetInput.mockReturnValue("true") + mockGetInput.mockReturnValue('true') expect(inputs.draft).toBe(true) }) }) describe('name', () => { it('returns input name', () => { - mockGetInput.mockReturnValue("name") - expect(inputs.name).toBe("name") + mockGetInput.mockReturnValue('name') + expect(inputs.name).toBe('name') }) it('returns tag', () => { - mockGetInput.mockReturnValue("") - context.ref = "refs/tags/sha-tag" - expect(inputs.name).toBe("sha-tag") + mockGetInput.mockReturnValue('') + context.ref = 'refs/tags/sha-tag' + expect(inputs.name).toBe('sha-tag') }) }) describe('tag', () => { it('returns input tag', () => { - mockGetInput.mockReturnValue("tag") - expect(inputs.tag).toBe("tag") + mockGetInput.mockReturnValue('tag') + expect(inputs.tag).toBe('tag') }) it('returns context sha when input is empty', () => { - mockGetInput.mockReturnValue("") - context.ref = "refs/tags/sha-tag" - expect(inputs.tag).toBe("sha-tag") + mockGetInput.mockReturnValue('') + context.ref = 'refs/tags/sha-tag' + expect(inputs.tag).toBe('sha-tag') }) it('returns context sha when input is null', () => { mockGetInput.mockReturnValue(null) - context.ref = "refs/tags/sha-tag" - expect(inputs.tag).toBe("sha-tag") + context.ref = 'refs/tags/sha-tag' + expect(inputs.tag).toBe('sha-tag') }) it('throws if no tag', () => { expect(() => inputs.tag).toThrow() }) }) + + function createGlobber(): ArtifactGlobber { + const MockGlobber = jest.fn(() => { + return { + globArtifactString: mockGlob + } + }) + mockGlob.mockImplementation(() => artifacts) + return new MockGlobber() + } }) \ No newline at end of file diff --git a/action.yml b/action.yml index 82c673ca..c7ea2660 100644 --- a/action.yml +++ b/action.yml @@ -3,8 +3,11 @@ description: 'Creates github releases' author: 'Nick Cipollo' inputs: artifact: - description: 'A path to an optional artifact to upload to the release.' + description: 'An optional set of paths representing artifacts to upload to the release. This may be a single path or a comma delimited list of paths (or globs)' default: '' + artifacts: + description: 'An optional set of paths representing artifacts to upload to the release. This may be a single path or a comma delimited list of paths (or globs)' + default: '' artifactContentType: description: 'The content type of the artifact. Defaults to raw' default: '' diff --git a/docs/contributors.md b/docs/contributors.md deleted file mode 100644 index fece2ea2..00000000 --- a/docs/contributors.md +++ /dev/null @@ -1,22 +0,0 @@ -# Contributors - -### Checkin - -- Do checkin source (src) -- Do checkin build output (lib) -- Do checkin runtime node_modules -- Do not checkin devDependency node_modules (husky can help see below) - -### devDependencies - -In order to handle correctly checking in node_modules without devDependencies, we run [Husky](https://github.com/typicode/husky) before each commit. -This step ensures that formatting and checkin rules are followed and that devDependencies are excluded. To make sure Husky runs correctly, please use the following workflow: - -``` -npm install # installs all devDependencies including Husky -git add abc.ext # Add the files you've changed. This should include files in src, lib, and node_modules (see above) -git commit -m "Informative commit message" # Commit. This will run Husky -``` - -During the commit step, Husky will take care of formatting all files with [Prettier](https://github.com/prettier/prettier) as well as pruning out devDependencies using `npm prune --production`. -It will also make sure these changes are appropriately included in your commit (no further work is needed) \ No newline at end of file diff --git a/package.json b/package.json index 087591fb..5ef8c13d 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,11 @@ "license": "MIT", "dependencies": { "@actions/core": "^1.0.0", - "@actions/github": "^1.0.0" + "@actions/github": "^1.0.0", + "glob": "^7.1.4" }, "devDependencies": { + "@types/glob": "^7.1.1", "@types/jest": "^24.0.13", "@types/node": "^12.0.4", "jest": "^24.8.0", diff --git a/src/Action.ts b/src/Action.ts index 9881e216..7c42da3d 100644 --- a/src/Action.ts +++ b/src/Action.ts @@ -1,15 +1,16 @@ import { Inputs } from "./Inputs"; import { Releases } from "./Releases"; -import { basename } from "path"; -import { readFileSync } from "fs"; +import { ArtifactUploader } from "./ArtifactUploader"; export class Action { private inputs: Inputs private releases: Releases + private uploader: ArtifactUploader - constructor(inputs: Inputs, releases: Releases) { + constructor(inputs: Inputs, releases: Releases, uploader: ArtifactUploader) { this.inputs = inputs this.releases = releases + this.uploader = uploader } async perform() { @@ -21,14 +22,11 @@ export class Action { this.inputs.name ) - if (this.inputs.artifact) { - const artifactData = this.inputs.readArtifact() - await this.releases.uploadArtifact( - createResult.data.upload_url, - this.inputs.artifactContentLength, - this.inputs.artifactContentType, - artifactData, - this.inputs.artifactName + const artifacts = this.inputs.artifacts + if (artifacts.length > 0) { + await this.uploader.uploadArtifacts( + artifacts, + createResult.data.upload_url ) } } diff --git a/src/Artifact.ts b/src/Artifact.ts new file mode 100644 index 00000000..00e7d511 --- /dev/null +++ b/src/Artifact.ts @@ -0,0 +1,22 @@ +import { basename } from "path"; +import { readFileSync, statSync } from "fs"; + +export class Artifact { + readonly contentType: string + readonly name: string + readonly path: string + + constructor(path: string, contentType: string = "raw") { + this.path = path + this.name = basename(path) + this.contentType = contentType; + } + + get contentLength(): number { + return statSync(this.path).size + } + + readFile(): Buffer { + return readFileSync(this.path) + } +} \ No newline at end of file diff --git a/src/ArtifactGlobber.ts b/src/ArtifactGlobber.ts new file mode 100644 index 00000000..bc8c6ffa --- /dev/null +++ b/src/ArtifactGlobber.ts @@ -0,0 +1,22 @@ +import { Globber, FileGlobber } from "./Globber"; +import { Artifact } from "./Artifact"; + +export interface ArtifactGlobber { + globArtifactString(artifact: string, contentType: string): Artifact[] +} + +export class FileArtifactGlobber implements ArtifactGlobber { + private globber: Globber + + constructor(globber: Globber = new FileGlobber()) { + this.globber = globber + } + + globArtifactString(artifact: string, contentType: string): Artifact[] { + return artifact.split(',') + .map((path) => this.globber.glob(path)) + .reduce((accumulated, current) => accumulated.concat(current)) + .map((path) => new Artifact(path, contentType)) + } +} + diff --git a/src/ArtifactUploader.ts b/src/ArtifactUploader.ts new file mode 100644 index 00000000..a6f7f7e7 --- /dev/null +++ b/src/ArtifactUploader.ts @@ -0,0 +1,24 @@ +import { Artifact } from "./Artifact"; +import { Releases } from "./Releases"; + +export interface ArtifactUploader { + uploadArtifacts(artifacts: Artifact[], uploadUrl: string): void +} + +export class GithubArtifactUploader implements ArtifactUploader { + private releases: Releases + + constructor(releases: Releases) { + this.releases = releases + } + + async uploadArtifacts(artifacts: Artifact[], uploadUrl: string) { + artifacts.forEach(async artifact => { + await this.releases.uploadArtifact(uploadUrl, + artifact.contentLength, + artifact.contentType, + artifact.readFile(), + artifact.name) + }); + } +} \ No newline at end of file diff --git a/src/Globber.ts b/src/Globber.ts new file mode 100644 index 00000000..05ebf28b --- /dev/null +++ b/src/Globber.ts @@ -0,0 +1,11 @@ +import { GlobSync } from "glob"; + +export interface Globber { + glob(pattern: string): string[] +} + +export class FileGlobber implements Globber { + glob(pattern: string): string[] { + return new GlobSync(pattern, { mark: true }).found + } +} \ No newline at end of file diff --git a/src/Inputs.ts b/src/Inputs.ts index d3300e7f..a2d5d78d 100644 --- a/src/Inputs.ts +++ b/src/Inputs.ts @@ -1,49 +1,42 @@ import * as core from '@actions/core'; import { Context } from "@actions/github/lib/context"; -import { readFileSync, statSync } from 'fs'; -import { basename } from 'path'; +import { readFileSync } from 'fs'; +import { ArtifactGlobber } from './ArtifactGlobber'; +import { Artifact } from './Artifact'; export interface Inputs { - readonly artifact: string - readonly artifactName: string - readonly artifactContentType: string - readonly artifactContentLength: number + readonly artifacts: Artifact[] readonly body: string readonly commit: string readonly draft: boolean readonly name: string readonly tag: string readonly token: string - - readArtifact(): Buffer } export class CoreInputs implements Inputs { + private artifactGlobber: ArtifactGlobber private context: Context - constructor(context: Context) { + constructor(artifactGlobber: ArtifactGlobber, context: Context) { + this.artifactGlobber = artifactGlobber this.context = context } - get artifact(): string { - return core.getInput('artifact') - } - - get artifactName(): string { - return basename(this.artifact) - } - - get artifactContentType(): string { - const type = core.getInput('artifactContentType') - if(type) { - return type; + get artifacts(): Artifact[] { + let artifacts = core.getInput('artifacts') + if (!artifacts) { + artifacts = core.getInput('artifacts') } - - return 'raw' - } - - get artifactContentLength(): number { - return statSync(this.artifact).size + if (artifacts) { + let contentType = core.getInput('artifactContentType') + if (!contentType) { + contentType = 'raw' + } + return this.artifactGlobber + .globArtifactString(artifacts, contentType) + } + return [] } get body(): string { @@ -97,10 +90,6 @@ export class CoreInputs implements Inputs { return core.getInput('token', { required: true }) } - readArtifact(): Buffer { - return readFileSync(this.artifact) - } - stringFromFile(path: string): string { return readFileSync(path, 'utf-8') } diff --git a/src/Main.ts b/src/Main.ts index 857e881c..70947fa9 100644 --- a/src/Main.ts +++ b/src/Main.ts @@ -3,6 +3,8 @@ import * as core from '@actions/core'; import { CoreInputs } from './Inputs'; import { GithubReleases } from './Releases'; import { Action } from './Action'; +import { GithubArtifactUploader } from './ArtifactUploader'; +import { FileArtifactGlobber } from './ArtifactGlobber'; async function run() { try { @@ -17,9 +19,12 @@ function createAction(): Action { const token = core.getInput('token') const context = github.context const git = new github.GitHub(token) + const globber = new FileArtifactGlobber() + + const inputs = new CoreInputs(globber, context) const releases = new GithubReleases(context, git) - const inputs = new CoreInputs(context) - return new Action(inputs, releases) + const uploader = new GithubArtifactUploader(releases) + return new Action(inputs, releases, uploader) } run(); diff --git a/yarn.lock b/yarn.lock index 650ace0e..d4fda09c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -391,6 +391,20 @@ dependencies: "@babel/types" "^7.3.0" +"@types/events@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" + integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== + +"@types/glob@^7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" + integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w== + dependencies: + "@types/events" "*" + "@types/minimatch" "*" + "@types/node" "*" + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" @@ -423,6 +437,16 @@ dependencies: "@types/jest-diff" "*" +"@types/minimatch@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" + integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== + +"@types/node@*": + version "12.7.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.3.tgz#27b3f40addaf2f580459fdb405222685542f907a" + integrity sha512-3SiLAIBkDWDg6vFo0+5YJyHPWU9uwu40Qe+v+0MH8wRKYBimHvvAOyk3EzMrD/TrIlLYfXrqDqrg913PynrMJQ== + "@types/node@^12.0.4": version "12.7.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.2.tgz#c4e63af5e8823ce9cc3f0b34f7b998c2171f0c44" @@ -1346,7 +1370,7 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" -glob@^7.1.1, glob@^7.1.2, glob@^7.1.3: +glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: version "7.1.4" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==