11 Commits

Author SHA1 Message Date
Nick Cipollo
95215a3cb6 Prepate 1.8.5 release
Some checks failed
Test / check_pr (push) Has been cancelled
2021-05-12 11:44:59 -04:00
Nick Cipollo
616708020f Fixes #50 Add plugin outputs which contain release information. 2021-05-12 11:36:59 -04:00
dependabot[bot]
085dc21232 Bump hosted-git-info from 2.8.8 to 2.8.9
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-10 19:11:16 -04:00
Nick Cipollo
06fefc702f Bump jest 2021-05-10 19:10:49 -04:00
dependabot[bot]
a84d04e910 Bump lodash from 4.17.20 to 4.17.21
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.20...4.17.21)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-10 19:05:50 -04:00
ph1ll
ec90733eaa Use pagination for listing release assets 2021-04-15 13:15:38 -04:00
Nick Cipollo
839c2ee3df Skip integration test 2021-04-07 17:38:35 -04:00
Nick Cipollo
a43fb1aa82 Fixes #37 Add discussion category action argument. 2021-04-07 17:36:43 -04:00
Nick Cipollo
8f0b206fd3 Cleanup error classes and CI 2021-03-23 17:43:38 -04:00
Nick Cipollo
9b14e2e2d3 Respect artifactErrorsFailBuild in production builds 2021-03-21 20:00:17 -04:00
Nick Cipollo
af980963d6 Fixes #33 Add artifactErrorsFailBuild flag 2021-03-21 19:47:27 -04:00
38 changed files with 875 additions and 530 deletions

28
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: "PR Checks"
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
check_pr:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: "yarn install"
run: yarn install
- name: "yarn build"
run: yarn build
- name: "check for uncommitted changes"
# Ensure no changes, but ignore node_modules dir since dev/fresh ci deps installed.
run: |
git diff --exit-code --stat -- . ':!node_modules' \
|| (echo "##[error] found changed files after build. please 'yarn build && npm run format'" \
"and check in all changes" \
&& exit 1)

View File

@@ -1,25 +0,0 @@
name: "PR Checks"
on: [pull_request, push]
jobs:
check_pr:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: "yarn install"
run: yarn install
- name: "yarn build"
run: yarn build
- name: "yarn test"
run: yarn test
- name: "check for uncommitted changes"
# Ensure no changes, but ignore node_modules dir since dev/fresh ci deps installed.
run: |
git diff --exit-code --stat -- . ':!node_modules' \
|| (echo "##[error] found changed files after build. please 'yarn build && npm run format'" \
"and check in all changes" \
&& exit 1)

14
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,14 @@
name: "Test"
on: [push, pull_request]
jobs:
check_pr:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: "yarn install"
run: yarn install
- name: "yarn test"
run: yarn test

2
.gitignore vendored
View File

@@ -1,4 +1,4 @@
#node_modules/
# node_modules/
__tests__/runner/*
# Created by https://www.gitignore.io/api/webstorm

View File

@@ -4,12 +4,14 @@ This action will create a github release and optionally upload an artifact to it
## Action Inputs
- **allowUpdates**: An optional flag which indicates if we should update a release if it already exists. Defaults to false.
- **artifactErrorsFailBuild**: An optional flag which indicates if artifact read or upload errors should fail the build.
- **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.
- **commit**: An optional commit reference. This will be used to create the tag if it does not exist.
- **discussionCategory**: When provided this will generate a discussion of the specified category. The category must exist otherwise this will cause the action to fail. This isn't used with draft releases.
- **draft**: Optionally marks this release as a draft release. Set to `true` to enable.
- **name**: An optional name for the release. If this is omitted the tag will be used.
- **omitBody**: Indicates if the release body should be omitted.
@@ -23,6 +25,11 @@ This action will create a github release and optionally upload an artifact to it
- **tag**: An optional tag for the release. If this is omitted the git ref will be used (if it is a tag).
- **token**: (**Required**) The Github token. Typically this will be `${{ secrets.GITHUB_TOKEN }}`.
## Action Outputs
- **id**: The identifier of the created release.
- **html_url**: The HTML URL of the release.
- **upload_url**: The URL for uploading assets to the release.
## Example
This example will create a release when tag is pushed:

View File

@@ -3,7 +3,9 @@ import {Artifact} from "../src/Artifact";
import {Inputs} from "../src/Inputs";
import {Releases} from "../src/Releases";
import {ArtifactUploader} from "../src/ArtifactUploader";
import {Outputs} from "../src/Outputs";
const applyReleaseDataMock = jest.fn()
const createMock = jest.fn()
const deleteMock = jest.fn()
const getMock = jest.fn()
@@ -20,6 +22,7 @@ const artifacts = [
const createBody = 'createBody'
const createName = 'createName'
const commit = 'commit'
const discussionCategory = 'discussionCategory'
const draft = true
const id = 100
const prerelease = true
@@ -45,8 +48,9 @@ describe("Action", () => {
await action.perform()
expect(createMock).toBeCalledWith(tag, createBody, commit, draft, createName, prerelease)
expect(createMock).toBeCalledWith(tag, createBody, commit, discussionCategory, draft, createName, prerelease)
expect(uploadMock).not.toBeCalled()
assertOutputApplied()
})
it('creates release if no release exists to update', async () => {
@@ -56,8 +60,9 @@ describe("Action", () => {
await action.perform()
expect(createMock).toBeCalledWith(tag, createBody, commit, draft, createName, prerelease)
expect(createMock).toBeCalledWith(tag, createBody, commit, discussionCategory, draft, createName, prerelease)
expect(uploadMock).toBeCalledWith(artifacts, releaseId, url)
assertOutputApplied()
})
it('creates release if no draft releases', async () => {
@@ -72,8 +77,9 @@ describe("Action", () => {
await action.perform()
expect(createMock).toBeCalledWith(tag, createBody, commit, draft, createName, prerelease)
expect(createMock).toBeCalledWith(tag, createBody, commit, discussionCategory, draft, createName, prerelease)
expect(uploadMock).toBeCalledWith(artifacts, releaseId, url)
assertOutputApplied()
})
@@ -82,8 +88,9 @@ describe("Action", () => {
await action.perform()
expect(createMock).toBeCalledWith(tag, createBody, commit, draft, createName, prerelease)
expect(createMock).toBeCalledWith(tag, createBody, commit, discussionCategory, draft, createName, prerelease)
expect(uploadMock).toBeCalledWith(artifacts, releaseId, url)
assertOutputApplied()
})
it('throws error when create fails', async () => {
@@ -97,7 +104,7 @@ describe("Action", () => {
expect(error).toEqual("error")
}
expect(createMock).toBeCalledWith(tag, createBody, commit, draft, createName, prerelease)
expect(createMock).toBeCalledWith(tag, createBody, commit, discussionCategory, draft, createName, prerelease)
expect(uploadMock).not.toBeCalled()
})
@@ -138,22 +145,32 @@ describe("Action", () => {
expect(error).toEqual("error")
}
expect(updateMock).toBeCalledWith(id, tag, updateBody, commit, draft, updateName, prerelease)
expect(updateMock).toBeCalledWith(
id,
tag,
updateBody,
commit,
discussionCategory,
draft,
updateName,
prerelease
)
expect(uploadMock).not.toBeCalled()
})
it('throws error when upload fails', async () => {
const action = createAction(false, true)
uploadMock.mockRejectedValue("error")
const expectedError = {status: 404}
uploadMock.mockRejectedValue(expectedError)
expect.hasAssertions()
try {
await action.perform()
} catch (error) {
expect(error).toEqual("error")
expect(error).toEqual(expectedError)
}
expect(createMock).toBeCalledWith(tag, createBody, commit, draft, createName, prerelease)
expect(createMock).toBeCalledWith(tag, createBody, commit, discussionCategory, draft, createName, prerelease)
expect(uploadMock).toBeCalledWith(artifacts, releaseId, url)
})
@@ -170,9 +187,18 @@ describe("Action", () => {
await action.perform()
expect(updateMock).toBeCalledWith(id, tag, updateBody, commit, draft, updateName, prerelease)
expect(updateMock).toBeCalledWith(
id,
tag,
updateBody,
commit,
discussionCategory,
draft,
updateName,
prerelease
)
expect(uploadMock).toBeCalledWith(artifacts, releaseId, url)
assertOutputApplied()
})
it('updates release but does not upload if no artifact', async () => {
@@ -180,9 +206,18 @@ describe("Action", () => {
await action.perform()
expect(updateMock).toBeCalledWith(id, tag, updateBody, commit, draft, updateName, prerelease)
expect(updateMock).toBeCalledWith(
id,
tag,
updateBody,
commit,
discussionCategory,
draft,
updateName,
prerelease
)
expect(uploadMock).not.toBeCalled()
assertOutputApplied()
})
it('updates release then uploads artifact', async () => {
@@ -190,11 +225,24 @@ describe("Action", () => {
await action.perform()
expect(updateMock).toBeCalledWith(id, tag, updateBody, commit, draft, updateName, prerelease)
expect(updateMock).toBeCalledWith(
id,
tag,
updateBody,
commit,
discussionCategory,
draft,
updateName,
prerelease
)
expect(uploadMock).toBeCalledWith(artifacts, releaseId, url)
assertOutputApplied()
})
function assertOutputApplied() {
expect(applyReleaseDataMock).toBeCalledWith({id: releaseId, upload_url: url})
}
function createAction(allowUpdates: boolean, hasArtifact: boolean): Action {
let inputArtifact: Artifact[]
if (hasArtifact) {
@@ -239,10 +287,12 @@ describe("Action", () => {
const MockInputs = jest.fn<Inputs, any>(() => {
return {
allowUpdates: allowUpdates,
artifactErrorsFailBuild: true,
artifacts: inputArtifact,
createdReleaseBody: createBody,
createdReleaseName: createName,
commit: commit,
discussionCategory: discussionCategory,
draft: draft,
owner: "owner",
prerelease: prerelease,
@@ -254,6 +304,11 @@ describe("Action", () => {
updatedReleaseName: updateName
}
})
const MockOutputs = jest.fn<Outputs, any>(() => {
return {
applyReleaseData: applyReleaseDataMock
}
})
const MockUploader = jest.fn<ArtifactUploader, any>(() => {
return {
uploadArtifacts: uploadMock
@@ -261,9 +316,10 @@ describe("Action", () => {
})
const inputs = new MockInputs()
const outputs = new MockOutputs()
const releases = new MockReleases()
const uploader = new MockUploader()
return new Action(inputs, releases, uploader)
return new Action(inputs, outputs, releases, uploader)
}
})

View File

@@ -1,8 +1,8 @@
const warnMock = jest.fn()
import { FileArtifactGlobber } from "../src/ArtifactGlobber"
import { Globber } from "../src/Globber";
import { Artifact } from "../src/Artifact";
import {FileArtifactGlobber} from "../src/ArtifactGlobber"
import {Globber} from "../src/Globber";
import {Artifact} from "../src/Artifact";
import untildify = require("untildify");
const contentType = "raw"
@@ -24,19 +24,19 @@ describe("ArtifactGlobber", () => {
const expectedArtifacts =
globResults.map((path) => new Artifact(path, contentType))
expect(globber.globArtifactString('~/path', 'raw'))
expect(globber.globArtifactString('~/path', 'raw', false))
.toEqual(expectedArtifacts)
expect(globMock).toBeCalledWith(untildify('~/path'))
expect(warnMock).not.toBeCalled()
})
it("globs simple path", () => {
const globber = createArtifactGlobber()
const expectedArtifacts =
globResults.map((path) => new Artifact(path, contentType))
expect(globber.globArtifactString('path', 'raw'))
expect(globber.globArtifactString('path', 'raw', false))
.toEqual(expectedArtifacts)
expect(globMock).toBeCalledWith('path')
expect(warnMock).not.toBeCalled()
@@ -50,24 +50,28 @@ describe("ArtifactGlobber", () => {
.concat(globResults)
.map((path) => new Artifact(path, contentType))
expect(globber.globArtifactString('path1,path2', 'raw'))
expect(globber.globArtifactString('path1,path2', 'raw', false))
.toEqual(expectedArtifacts)
expect(globMock).toBeCalledWith('path1')
expect(globMock).toBeCalledWith('path2')
expect(warnMock).not.toBeCalled()
})
it("warns when no glob results are produced", () => {
it("warns when no glob results are produced and empty results shouldn't throw", () => {
const globber = createArtifactGlobber([])
const expectedArtifacts =
globResults.map((path) => new Artifact(path, contentType))
expect(globber.globArtifactString('path', 'raw'))
expect(globber.globArtifactString('path', 'raw', false))
.toEqual([])
expect(warnMock).toBeCalled()
})
it("throws when no glob results are produced and empty results shouild throw", () => {
const globber = createArtifactGlobber([])
expect(() => {
globber.globArtifactString('path', 'raw', true)
}).toThrow()
})
function createArtifactGlobber(results: string[] = globResults): FileArtifactGlobber {
const MockGlobber = jest.fn<Globber, any>(() => {
return {

View File

@@ -1,7 +1,7 @@
import { Artifact } from "../src/Artifact"
import { GithubArtifactUploader } from "../src/ArtifactUploader"
import { Releases } from "../src/Releases";
import { RequestError } from '@octokit/request-error'
import {Artifact} from "../src/Artifact"
import {GithubArtifactUploader} from "../src/ArtifactUploader"
import {Releases} from "../src/Releases";
import {RequestError} from '@octokit/request-error'
const artifacts = [
new Artifact('a/art1'),
@@ -19,7 +19,9 @@ const uploadMock = jest.fn()
jest.mock('fs', () => {
return {
readFileSync: () => fileContents,
statSync: () => { return { size: contentLength } }
statSync: () => {
return {size: contentLength}
}
};
})
@@ -124,6 +126,19 @@ describe('ArtifactUploader', () => {
expect(deleteMock).toBeCalledTimes(0)
})
it('throws upload error when replacesExistingArtifacts is true', async () => {
mockListWithoutAssets()
mockUploadError()
const uploader = createUploader(true, true)
expect.hasAssertions()
try {
await uploader.uploadArtifacts(artifacts, releaseId, url)
} catch (error) {
expect(error).toEqual(Error("Failed to upload artifact art1. error."))
}
})
it('throws error from replace', async () => {
mockDeleteError()
mockListWithAssets()
@@ -155,7 +170,7 @@ describe('ArtifactUploader', () => {
expect(deleteMock).toBeCalledTimes(0)
})
function createUploader(replaces: boolean): GithubArtifactUploader {
function createUploader(replaces: boolean, throws: boolean = false): GithubArtifactUploader {
const MockReleases = jest.fn<Releases, any>(() => {
return {
create: jest.fn(),
@@ -167,7 +182,7 @@ describe('ArtifactUploader', () => {
uploadArtifact: uploadMock
}
})
return new GithubArtifactUploader(new MockReleases(), replaces)
return new GithubArtifactUploader(new MockReleases(), replaces, throws)
}
function mockDeleteError(): any {
@@ -179,29 +194,37 @@ describe('ArtifactUploader', () => {
}
function mockListWithAssets() {
listArtifactsMock.mockResolvedValue({
data: [
{
name: "art1",
id: 1
},
{
name: "art2",
id: 2
}
]
})
listArtifactsMock.mockResolvedValue([
{
name: "art1",
id: 1
},
{
name: "art2",
id: 2
}
])
}
function mockListWithoutAssets() {
listArtifactsMock.mockResolvedValue({ data: [] })
listArtifactsMock.mockResolvedValue([])
}
function mockUploadArtifact(status: number = 200, failures: number = 0) {
const error = new RequestError(`HTTP ${status}`, status, { headers: {}, request: { method: 'GET', url: '', headers: {} } })
const error = new RequestError(`HTTP ${status}`, status, {
headers: {},
request: {method: 'GET', url: '', headers: {}}
})
for (let index = 0; index < failures; index++) {
uploadMock.mockRejectedValueOnce(error)
}
uploadMock.mockResolvedValue({})
}
function mockUploadError() {
uploadMock.mockRejectedValue({
message: "error",
status: 502
})
}
});

View File

@@ -1,70 +0,0 @@
import { ErrorMessage } from "../src/ErrorMessage"
describe('ErrorMessage', () => {
describe('has error with code', () => {
const error = {
message: 'something bad happened',
errors: [
{
code: 'missing',
resource: 'release'
},
{
code: 'already_exists',
resource: 'release'
}
],
status: 422
}
it('does not have error', () => {
const errorMessage = new ErrorMessage(error)
expect(errorMessage.hasErrorWithCode('missing_field')).toBeFalsy()
})
it('has error', () => {
const errorMessage = new ErrorMessage(error)
expect(errorMessage.hasErrorWithCode('missing')).toBeTruthy()
})
})
it('generates message with errors', () => {
const error = {
message: 'something bad happened',
errors: [
{
code: 'missing',
resource: 'release'
},
{
code: 'already_exists',
resource: 'release'
}
],
status: 422
}
const errorMessage = new ErrorMessage(error)
const expectedString = "Error 422: something bad happened\nErrors:\n- release does not exist.\n- release already exists."
expect(errorMessage.toString()).toBe(expectedString)
})
it('generates message without errors', () => {
const error = {
message: 'something bad happened',
status: 422
}
const errorMessage = new ErrorMessage(error)
expect(errorMessage.toString()).toBe('Error 422: something bad happened')
})
it('provides error status', () => {
const error = { status: 404 }
const errorMessage = new ErrorMessage(error)
expect(errorMessage.status).toBe(404)
})
})

View File

@@ -1,99 +1,70 @@
import { GithubError } from "../src/GithubError"
describe('GithubError', () => {
describe('ErrorMessage', () => {
it('provides error code', () => {
describe('has error with code', () => {
const error = {
code: "missing"
message: 'something bad happened',
errors: [
{
code: 'missing',
resource: 'release'
},
{
code: 'already_exists',
resource: 'release'
}
],
status: 422
}
const githubError = new GithubError(error)
expect(githubError.code).toBe('missing')
})
it('generates missing resource error message', () => {
const resource = "release"
const error = {
code: "missing",
resource: resource
}
const githubError = new GithubError(error)
const message = githubError.toString()
expect(message).toBe(`${resource} does not exist.`)
})
it('generates missing field error message', () => {
const resource = "release"
const field = "body"
const error = {
code: "missing_field",
field: field,
resource: resource
}
const githubError = new GithubError(error)
const message = githubError.toString()
expect(message).toBe(`The ${field} field on ${resource} is missing.`)
})
it('generates invalid field error message', () => {
const resource = "release"
const field = "body"
const error = {
code: "invalid",
field: field,
resource: resource
}
const githubError = new GithubError(error)
const message = githubError.toString()
expect(message).toBe(`The ${field} field on ${resource} is an invalid format.`)
})
it('generates resource already exists error message', () => {
const resource = "release"
const field = "body"
const error = {
code: "already_exists",
resource: resource
}
const githubError = new GithubError(error)
const message = githubError.toString()
expect(message).toBe(`${resource} already exists.`)
})
describe('generates custom error message', () => {
it('with documentation url', () => {
const url = "https://api.example.com"
const error = {
code: "custom",
message: "foo",
documentation_url: url
}
it('does not have error', () => {
const githubError = new GithubError(error)
const message = githubError.toString()
expect(message).toBe(`foo\nPlease see ${url}.`)
expect(githubError.hasErrorWithCode('missing_field')).toBeFalsy()
})
it('without documentation url', () => {
const error = {
code: "custom",
message: "foo"
}
it('has error', () => {
const githubError = new GithubError(error)
const message = githubError.toString()
expect(message).toBe('foo')
expect(githubError.hasErrorWithCode('missing')).toBeTruthy()
})
})
})
it('generates message with errors', () => {
const error = {
message: 'something bad happened',
errors: [
{
code: 'missing',
resource: 'release'
},
{
code: 'already_exists',
resource: 'release'
}
],
status: 422
}
const githubError = new GithubError(error)
const expectedString = "Error 422: something bad happened\nErrors:\n- release does not exist.\n- release already exists."
expect(githubError.toString()).toBe(expectedString)
})
it('generates message without errors', () => {
const error = {
message: 'something bad happened',
status: 422
}
const githubError = new GithubError(error)
expect(githubError.toString()).toBe('Error 422: something bad happened')
})
it('provides error status', () => {
const error = { status: 404 }
const githubError = new GithubError(error)
expect(githubError.status).toBe(404)
})
})

View File

@@ -0,0 +1,98 @@
import { GithubErrorDetail } from "../src/GithubErrorDetail"
describe('GithubErrorDetail', () => {
it('provides error code', () => {
const error = {
code: "missing"
}
const detail = new GithubErrorDetail(error)
expect(detail.code).toBe('missing')
})
it('generates missing resource error message', () => {
const resource = "release"
const error = {
code: "missing",
resource: resource
}
const detail = new GithubErrorDetail(error)
const message = detail.toString()
expect(message).toBe(`${resource} does not exist.`)
})
it('generates missing field error message', () => {
const resource = "release"
const field = "body"
const error = {
code: "missing_field",
field: field,
resource: resource
}
const detail = new GithubErrorDetail(error)
const message = detail.toString()
expect(message).toBe(`The ${field} field on ${resource} is missing.`)
})
it('generates invalid field error message', () => {
const resource = "release"
const field = "body"
const error = {
code: "invalid",
field: field,
resource: resource
}
const detail = new GithubErrorDetail(error)
const message = detail.toString()
expect(message).toBe(`The ${field} field on ${resource} is an invalid format.`)
})
it('generates resource already exists error message', () => {
const resource = "release"
const error = {
code: "already_exists",
resource: resource
}
const detail = new GithubErrorDetail(error)
const message = detail.toString()
expect(message).toBe(`${resource} already exists.`)
})
describe('generates custom error message', () => {
it('with documentation url', () => {
const url = "https://api.example.com"
const error = {
code: "custom",
message: "foo",
documentation_url: url
}
const detail = new GithubErrorDetail(error)
const message = detail.toString()
expect(message).toBe(`foo\nPlease see ${url}.`)
})
it('without documentation url', () => {
const error = {
code: "custom",
message: "foo"
}
const detail = new GithubErrorDetail(error)
const message = detail.toString()
expect(message).toBe('foo')
})
})
})

View File

@@ -59,7 +59,28 @@ describe('Inputs', () => {
})
})
describe('artifactErrorsFailBuild', () => {
it('returns false', () => {
expect(inputs.artifactErrorsFailBuild).toBe(false)
})
it('returns true', () => {
mockGetInput.mockReturnValue('true')
expect(inputs.artifactErrorsFailBuild).toBe(true)
})
})
describe('artifacts', () => {
it('globber told to throw errors', () => {
mockGetInput.mockReturnValueOnce('art1')
.mockReturnValueOnce('contentType')
.mockReturnValueOnce('true')
expect(inputs.artifacts).toEqual(artifacts)
expect(mockGlob).toBeCalledTimes(1)
expect(mockGlob).toBeCalledWith('art1', 'contentType', true)
})
it('returns empty artifacts', () => {
mockGetInput.mockReturnValueOnce('')
.mockReturnValueOnce('')
@@ -71,28 +92,32 @@ describe('Inputs', () => {
it('returns input.artifacts', () => {
mockGetInput.mockReturnValueOnce('art1')
.mockReturnValueOnce('contentType')
.mockReturnValueOnce('false')
expect(inputs.artifacts).toEqual(artifacts)
expect(mockGlob).toBeCalledTimes(1)
expect(mockGlob).toBeCalledWith('art1', 'contentType')
expect(mockGlob).toBeCalledWith('art1', 'contentType', false)
})
it('returns input.artifacts with default contentType', () => {
mockGetInput.mockReturnValueOnce('art1')
.mockReturnValueOnce('raw')
.mockReturnValueOnce('false')
expect(inputs.artifacts).toEqual(artifacts)
expect(mockGlob).toBeCalledTimes(1)
expect(mockGlob).toBeCalledWith('art1', 'raw')
expect(mockGlob).toBeCalledWith('art1', 'raw', false)
})
it('returns input.artifact', () => {
mockGetInput.mockReturnValueOnce('')
.mockReturnValueOnce('art2')
.mockReturnValueOnce('contentType')
.mockReturnValueOnce('false')
expect(inputs.artifacts).toEqual(artifacts)
expect(mockGlob).toBeCalledTimes(1)
expect(mockGlob).toBeCalledWith('art2', 'contentType')
expect(mockGlob).toBeCalledWith('art2', 'contentType', false)
})
})
@@ -154,6 +179,18 @@ describe('Inputs', () => {
})
})
describe('discussionCategory', () => {
it('returns category', () => {
mockGetInput.mockReturnValue('Release')
expect(inputs.discussionCategory).toBe('Release')
})
it('returns undefined', () => {
mockGetInput.mockReturnValue('')
expect(inputs.discussionCategory).toBe(undefined)
})
})
describe('draft', () => {
it('returns false', () => {
expect(inputs.draft).toBe(false)
@@ -164,7 +201,7 @@ describe('Inputs', () => {
expect(inputs.draft).toBe(true)
})
})
describe('owner', () => {
it('returns owner from context', function () {
process.env.GITHUB_REPOSITORY = "owner/repo"
@@ -175,7 +212,7 @@ describe('Inputs', () => {
mockGetInput.mockReturnValue("owner")
expect(inputs.owner).toBe("owner")
});
})
})
describe('prerelase', () => {
it('returns false', () => {

View File

@@ -1,10 +1,11 @@
import {Action} from "../src/Action";
import * as github from "@actions/github";
import {Inputs} from "../src/Inputs";
import {GithubReleases} from "../src/Releases";
import {GithubReleases, ReleaseData} from "../src/Releases";
import {GithubArtifactUploader} from "../src/ArtifactUploader";
import * as path from "path";
import {FileArtifactGlobber} from "../src/ArtifactGlobber";
import {Outputs} from "../src/Outputs";
// This test is currently intended to be manually run during development. To run:
// - Make sure you have an environment variable named GITHUB_TOKEN assigned to your token
@@ -17,9 +18,14 @@ describe.skip('Integration Test', () => {
const git = github.getOctokit(token)
const inputs = getInputs()
const outputs = getOutputs()
const releases = new GithubReleases(inputs, git)
const uploader = new GithubArtifactUploader(releases, inputs.replacesArtifacts)
action = new Action(inputs, releases, uploader)
const uploader = new GithubArtifactUploader(
releases,
inputs.replacesArtifacts,
inputs.artifactErrorsFailBuild,
)
action = new Action(inputs, outputs, releases, uploader)
})
it('Performs action', async () => {
@@ -30,10 +36,12 @@ describe.skip('Integration Test', () => {
const MockInputs = jest.fn<Inputs, any>(() => {
return {
allowUpdates: true,
artifactErrorsFailBuild: false,
artifacts: artifacts(),
createdReleaseBody: "This release was generated by release-action's integration test",
createdReleaseName: "Releases Action Integration Test",
createdReleaseName: "Releases Action Integration Test 2",
commit: "",
discussionCategory: 'Release',
draft: false,
owner: "ncipollo",
prerelease: false,
@@ -42,17 +50,28 @@ describe.skip('Integration Test', () => {
tag: "release-action-test",
token: getToken(),
updatedReleaseBody: "This release was generated by release-action's integration test",
updatedReleaseName: "Releases Action Integration Test"
updatedReleaseName: "Releases Action Integration Test 2"
}
})
return new MockInputs();
}
function getOutputs(): Outputs {
const MockOutputs = jest.fn<Outputs, any>(() => {
return {
applyReleaseData(releaseData: ReleaseData) {
console.log(`Release Data: ${releaseData}`)
}
}
})
return new MockOutputs()
}
function artifacts() {
const globber = new FileArtifactGlobber()
const artifactPath = path.join(__dirname, 'Integration.test.ts')
const artifactString = `~/Desktop/test.txt,blarg.tx, ${artifactPath}`
return globber.globArtifactString(artifactString, "raw")
return globber.globArtifactString(artifactString, "raw", false)
}
function getToken(): string {

29
__tests__/Outputs.test.ts Normal file
View File

@@ -0,0 +1,29 @@
const mockSetOutput = jest.fn();
import {CoreOutputs, Outputs} from "../src/Outputs";
import {ReleaseData} from "../src/Releases";
jest.mock('@actions/core', () => {
return {setOutput: mockSetOutput};
})
describe('Outputs', () => {
let outputs: Outputs;
let releaseData: ReleaseData
beforeEach(() => {
outputs = new CoreOutputs()
releaseData = {
id: 1,
html_url: 'https://api.example.com/assets',
upload_url: 'https://api.example.com'
}
})
it('Applies the release data to the action output', () => {
outputs.applyReleaseData(releaseData)
expect(mockSetOutput).toBeCalledWith('id', releaseData.id)
expect(mockSetOutput).toBeCalledWith('html_url', releaseData.html_url)
expect(mockSetOutput).toBeCalledWith('upload_url', releaseData.upload_url)
})
})

View File

@@ -6,6 +6,10 @@ inputs:
description: 'An optional flag which indicates if we should update a release if it already exists. Defaults to false.'
required: false
default: ''
artifactErrorsFailBuild:
description: 'An optional flag which indicates if artifact read or upload errors should fail the build.'
required: false
default: ''
artifact:
deprecationMessage: Use 'artifacts' instead.
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)'
@@ -30,6 +34,10 @@ inputs:
commit:
description: "An optional commit reference. This will be used to create the tag if it does not exist."
required: false
default: ''
discussionCategory:
description: "When provided this will generate a discussion of the specified category. The category must exist otherwise this will cause the action to fail. This isn't used with draft releases"
required: false
default: ''
draft:
description: "Optionally marks this release as a draft release. Set to true to enable."
@@ -79,6 +87,13 @@ inputs:
description: 'The Github token.'
required: true
default: ''
outputs:
id:
description: 'The identifier of the created release.'
html_url:
description: 'The HTML URL of the release.'
upload_url:
description: 'The URL for uploading assets to the release.'
runs:
using: 'node12'
main: 'lib/Main.js'

View File

@@ -10,53 +10,62 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Action = void 0;
const ErrorMessage_1 = require("./ErrorMessage");
const GithubError_1 = require("./GithubError");
class Action {
constructor(inputs, releases, uploader) {
constructor(inputs, outputs, releases, uploader) {
this.inputs = inputs;
this.outputs = outputs;
this.releases = releases;
this.uploader = uploader;
}
perform() {
return __awaiter(this, void 0, void 0, function* () {
const releaseResponse = yield this.createOrUpdateRelease();
const releaseId = releaseResponse.data.id;
const uploadUrl = releaseResponse.data.upload_url;
const releaseData = releaseResponse.data;
const releaseId = releaseData.id;
const uploadUrl = releaseData.upload_url;
const artifacts = this.inputs.artifacts;
if (artifacts.length > 0) {
yield this.uploader.uploadArtifacts(artifacts, releaseId, uploadUrl);
}
this.outputs.applyReleaseData(releaseData);
});
}
createOrUpdateRelease() {
return __awaiter(this, void 0, void 0, function* () {
if (this.inputs.allowUpdates) {
let getResponse;
try {
const getResponse = yield this.releases.getByTag(this.inputs.tag);
return yield this.updateRelease(getResponse.data.id);
getResponse = yield this.releases.getByTag(this.inputs.tag);
}
catch (error) {
if (Action.noPublishedRelease(error)) {
return yield this.updateDraftOrCreateRelease();
}
else {
throw error;
}
return yield this.checkForMissingReleaseError(error);
}
return yield this.updateRelease(getResponse.data.id);
}
else {
return yield this.createRelease();
}
});
}
checkForMissingReleaseError(error) {
return __awaiter(this, void 0, void 0, function* () {
if (Action.noPublishedRelease(error)) {
return yield this.updateDraftOrCreateRelease();
}
else {
throw error;
}
});
}
updateRelease(id) {
return __awaiter(this, void 0, void 0, function* () {
return yield this.releases.update(id, this.inputs.tag, this.inputs.updatedReleaseBody, this.inputs.commit, this.inputs.draft, this.inputs.updatedReleaseName, this.inputs.prerelease);
return yield this.releases.update(id, this.inputs.tag, this.inputs.updatedReleaseBody, this.inputs.commit, this.inputs.discussionCategory, this.inputs.draft, this.inputs.updatedReleaseName, this.inputs.prerelease);
});
}
static noPublishedRelease(error) {
const errorMessage = new ErrorMessage_1.ErrorMessage(error);
return errorMessage.status == 404;
const githubError = new GithubError_1.GithubError(error);
return githubError.status == 404;
}
updateDraftOrCreateRelease() {
return __awaiter(this, void 0, void 0, function* () {
@@ -80,8 +89,7 @@ class Action {
}
createRelease() {
return __awaiter(this, void 0, void 0, function* () {
const response = yield this.releases.create(this.inputs.tag, this.inputs.createdReleaseBody, this.inputs.commit, this.inputs.draft, this.inputs.createdReleaseName, this.inputs.prerelease);
return response;
return yield this.releases.create(this.inputs.tag, this.inputs.createdReleaseBody, this.inputs.commit, this.inputs.discussionCategory, this.inputs.draft, this.inputs.createdReleaseName, this.inputs.prerelease);
});
}
}

View File

@@ -31,23 +31,31 @@ class FileArtifactGlobber {
constructor(globber = new Globber_1.FileGlobber()) {
this.globber = globber;
}
globArtifactString(artifact, contentType) {
globArtifactString(artifact, contentType, throwsWhenNoFiles) {
return artifact.split(',')
.map(path => FileArtifactGlobber.expandPath(path))
.map(pattern => this.globPattern(pattern))
.map(pattern => this.globPattern(pattern, throwsWhenNoFiles))
.reduce((accumulated, current) => accumulated.concat(current))
.map(path => new Artifact_1.Artifact(path, contentType));
}
globPattern(pattern) {
globPattern(pattern, throwsWhenNoFiles) {
const paths = this.globber.glob(pattern);
if (paths.length == 0) {
FileArtifactGlobber.reportGlobWarning(pattern);
if (throwsWhenNoFiles) {
FileArtifactGlobber.throwGlobError(pattern);
}
else {
FileArtifactGlobber.reportGlobWarning(pattern);
}
}
return paths;
}
static reportGlobWarning(pattern) {
core.warning(`Artifact pattern :${pattern} did not match any files`);
}
static throwGlobError(pattern) {
throw Error(`Artifact pattern :${pattern} did not match any files`);
}
static expandPath(path) {
return untildify_1.default(path);
}

View File

@@ -31,9 +31,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.GithubArtifactUploader = void 0;
const core = __importStar(require("@actions/core"));
class GithubArtifactUploader {
constructor(releases, replacesExistingArtifacts = true) {
constructor(releases, replacesExistingArtifacts = true, throwsUploadErrors = false) {
this.releases = releases;
this.replacesExistingArtifacts = replacesExistingArtifacts;
this.throwsUploadErrors = throwsUploadErrors;
}
uploadArtifacts(artifacts, releaseId, uploadUrl) {
return __awaiter(this, void 0, void 0, function* () {
@@ -57,15 +58,19 @@ class GithubArtifactUploader {
yield this.uploadArtifact(artifact, releaseId, uploadUrl, retry - 1);
}
else {
core.warning(`Failed to upload artifact ${artifact.name}. ${error.message}.`);
if (this.throwsUploadErrors) {
throw Error(`Failed to upload artifact ${artifact.name}. ${error.message}.`);
}
else {
core.warning(`Failed to upload artifact ${artifact.name}. ${error.message}.`);
}
}
}
});
}
deleteUpdatedArtifacts(artifacts, releaseId) {
return __awaiter(this, void 0, void 0, function* () {
const response = yield this.releases.listArtifactsForRelease(releaseId);
const releaseAssets = response.data;
const releaseAssets = yield this.releases.listArtifactsForRelease(releaseId);
const assetByName = {};
releaseAssets.forEach(asset => {
assetByName[asset.name] = asset;

View File

@@ -1,40 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ErrorMessage = void 0;
const GithubError_1 = require("./GithubError");
class ErrorMessage {
constructor(error) {
this.error = error;
this.githubErrors = this.generateGithubErrors();
}
generateGithubErrors() {
const errors = this.error.errors;
if (errors instanceof Array) {
return errors.map((err) => new GithubError_1.GithubError(err));
}
else {
return [];
}
}
get status() {
return this.error.status;
}
hasErrorWithCode(code) {
return this.githubErrors.some((err) => err.code == code);
}
toString() {
const message = this.error.message;
const errors = this.githubErrors;
const status = this.status;
if (errors.length > 0) {
return `Error ${status}: ${message}\nErrors:\n${this.errorBulletedList(errors)}`;
}
else {
return `Error ${status}: ${message}`;
}
}
errorBulletedList(errors) {
return errors.map((err) => `- ${err}`).join("\n");
}
}
exports.ErrorMessage = ErrorMessage;

View File

@@ -1,57 +1,40 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GithubError = void 0;
const GithubErrorDetail_1 = require("./GithubErrorDetail");
class GithubError {
constructor(error) {
this.error = error;
this.githubErrors = this.generateGithubErrors();
}
get code() {
return this.error.code;
}
toString() {
const code = this.error.code;
switch (code) {
case 'missing':
return this.missingResourceMessage();
case 'missing_field':
return this.missingFieldMessage();
case 'invalid':
return this.invalidFieldMessage();
case 'already_exists':
return this.resourceAlreadyExists();
default:
return this.customErrorMessage();
}
}
customErrorMessage() {
const message = this.error.message;
const documentation = this.error.documentation_url;
let documentationMessage;
if (documentation) {
documentationMessage = `\nPlease see ${documentation}.`;
generateGithubErrors() {
const errors = this.error.errors;
if (errors instanceof Array) {
return errors.map((err) => new GithubErrorDetail_1.GithubErrorDetail(err));
}
else {
documentationMessage = "";
return [];
}
return `${message}${documentationMessage}`;
}
invalidFieldMessage() {
const resource = this.error.resource;
const field = this.error.field;
return `The ${field} field on ${resource} is an invalid format.`;
get status() {
return this.error.status;
}
missingResourceMessage() {
const resource = this.error.resource;
return `${resource} does not exist.`;
hasErrorWithCode(code) {
return this.githubErrors.some((err) => err.code == code);
}
missingFieldMessage() {
const resource = this.error.resource;
const field = this.error.field;
return `The ${field} field on ${resource} is missing.`;
toString() {
const message = this.error.message;
const errors = this.githubErrors;
const status = this.status;
if (errors.length > 0) {
return `Error ${status}: ${message}\nErrors:\n${this.errorBulletedList(errors)}`;
}
else {
return `Error ${status}: ${message}`;
}
}
resourceAlreadyExists() {
const resource = this.error.resource;
return `${resource} already exists.`;
errorBulletedList(errors) {
return errors.map((err) => `- ${err}`).join("\n");
}
}
exports.GithubError = GithubError;

57
lib/GithubErrorDetail.js Normal file
View File

@@ -0,0 +1,57 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GithubErrorDetail = void 0;
class GithubErrorDetail {
constructor(error) {
this.error = error;
}
get code() {
return this.error.code;
}
toString() {
const code = this.error.code;
switch (code) {
case 'missing':
return this.missingResourceMessage();
case 'missing_field':
return this.missingFieldMessage();
case 'invalid':
return this.invalidFieldMessage();
case 'already_exists':
return this.resourceAlreadyExists();
default:
return this.customErrorMessage();
}
}
customErrorMessage() {
const message = this.error.message;
const documentation = this.error.documentation_url;
let documentationMessage;
if (documentation) {
documentationMessage = `\nPlease see ${documentation}.`;
}
else {
documentationMessage = "";
}
return `${message}${documentationMessage}`;
}
invalidFieldMessage() {
const resource = this.error.resource;
const field = this.error.field;
return `The ${field} field on ${resource} is an invalid format.`;
}
missingResourceMessage() {
const resource = this.error.resource;
return `${resource} does not exist.`;
}
missingFieldMessage() {
const resource = this.error.resource;
const field = this.error.field;
return `The ${field} field on ${resource} is missing.`;
}
resourceAlreadyExists() {
const resource = this.error.resource;
return `${resource} already exists.`;
}
}
exports.GithubErrorDetail = GithubErrorDetail;

View File

@@ -42,10 +42,14 @@ class CoreInputs {
contentType = 'raw';
}
return this.artifactGlobber
.globArtifactString(artifacts, contentType);
.globArtifactString(artifacts, contentType, this.artifactErrorsFailBuild);
}
return [];
}
get artifactErrorsFailBuild() {
const allow = core.getInput('artifactErrorsFailBuild');
return allow == 'true';
}
get createdReleaseBody() {
if (CoreInputs.omitBody)
return undefined;
@@ -73,6 +77,13 @@ class CoreInputs {
return undefined;
return this.name;
}
get discussionCategory() {
const category = core.getInput('discussionCategory');
if (category) {
return category;
}
return undefined;
}
static get omitName() {
return core.getInput('omitName') == 'true';
}

View File

@@ -35,7 +35,8 @@ const Releases_1 = require("./Releases");
const Action_1 = require("./Action");
const ArtifactUploader_1 = require("./ArtifactUploader");
const ArtifactGlobber_1 = require("./ArtifactGlobber");
const ErrorMessage_1 = require("./ErrorMessage");
const GithubError_1 = require("./GithubError");
const Outputs_1 = require("./Outputs");
function run() {
return __awaiter(this, void 0, void 0, function* () {
try {
@@ -43,8 +44,8 @@ function run() {
yield action.perform();
}
catch (error) {
const errorMessage = new ErrorMessage_1.ErrorMessage(error);
core.setFailed(errorMessage.toString());
const githubError = new GithubError_1.GithubError(error);
core.setFailed(githubError.toString());
}
});
}
@@ -54,8 +55,9 @@ function createAction() {
const git = github.getOctokit(token);
const globber = new ArtifactGlobber_1.FileArtifactGlobber();
const inputs = new Inputs_1.CoreInputs(globber, context);
const outputs = new Outputs_1.CoreOutputs();
const releases = new Releases_1.GithubReleases(inputs, git);
const uploader = new ArtifactUploader_1.GithubArtifactUploader(releases, inputs.replacesArtifacts);
return new Action_1.Action(inputs, releases, uploader);
const uploader = new ArtifactUploader_1.GithubArtifactUploader(releases, inputs.replacesArtifacts, inputs.artifactErrorsFailBuild);
return new Action_1.Action(inputs, outputs, releases, uploader);
}
run();

31
lib/Outputs.js Normal file
View File

@@ -0,0 +1,31 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CoreOutputs = void 0;
const core = __importStar(require("@actions/core"));
class CoreOutputs {
applyReleaseData(releaseData) {
core.setOutput('id', releaseData.id);
core.setOutput('html_url', releaseData.html_url);
core.setOutput('upload_url', releaseData.upload_url);
}
}
exports.CoreOutputs = CoreOutputs;

View File

@@ -15,12 +15,13 @@ class GithubReleases {
this.inputs = inputs;
this.git = git;
}
create(tag, body, commitHash, draft, name, prerelease) {
create(tag, body, commitHash, discussionCategory, draft, name, prerelease) {
return __awaiter(this, void 0, void 0, function* () {
// noinspection TypeScriptValidateJSTypes
return this.git.repos.createRelease({
body: body,
name: name,
discussion_category_name: discussionCategory,
draft: draft,
owner: this.inputs.owner,
prerelease: prerelease,
@@ -50,7 +51,7 @@ class GithubReleases {
}
listArtifactsForRelease(releaseId) {
return __awaiter(this, void 0, void 0, function* () {
return this.git.repos.listReleaseAssets({
return this.git.paginate(this.git.repos.listReleaseAssets, {
owner: this.inputs.owner,
release_id: releaseId,
repo: this.inputs.repo
@@ -65,13 +66,14 @@ class GithubReleases {
});
});
}
update(id, tag, body, commitHash, draft, name, prerelease) {
update(id, tag, body, commitHash, discussionCategory, draft, name, prerelease) {
return __awaiter(this, void 0, void 0, function* () {
// noinspection TypeScriptValidateJSTypes
return this.git.repos.updateRelease({
release_id: id,
body: body,
name: name,
discussion_category_name: discussionCategory,
draft: draft,
owner: this.inputs.owner,
prerelease: prerelease,

15
node_modules/.yarn-integrity generated vendored
View File

@@ -15,8 +15,8 @@
"@types/node@^14.14.25",
"glob@^7.1.4",
"jest-circus@^26.6.3",
"jest@^26.1.0",
"ts-jest@^26.5.1",
"jest@^26.6.3",
"ts-jest@^26.5.6",
"typescript@^4.1.4",
"untildify@^4.0.0"
],
@@ -112,7 +112,6 @@
"@types/istanbul-lib-coverage@^2.0.1": "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762",
"@types/istanbul-lib-report@*": "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686",
"@types/istanbul-reports@^3.0.0": "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz#508b13aa344fa4976234e75dddcc34925737d821",
"@types/jest@26.x": "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.20.tgz#cd2f2702ecf69e86b586e1f5223a60e454056307",
"@types/jest@^26.0.20": "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.20.tgz#cd2f2702ecf69e86b586e1f5223a60e454056307",
"@types/minimatch@*": "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d",
"@types/node@*": "https://registry.yarnpkg.com/@types/node/-/node-14.14.25.tgz#15967a7b577ff81383f9b888aa6705d43fbbae93",
@@ -286,7 +285,7 @@
"has-values@^0.1.4": "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771",
"has-values@^1.0.0": "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f",
"has@^1.0.3": "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796",
"hosted-git-info@^2.1.4": "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488",
"hosted-git-info@^2.1.4": "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9",
"html-encoding-sniffer@^2.0.1": "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3",
"html-escaper@^2.0.0": "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453",
"http-signature@~1.2.0": "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1",
@@ -368,7 +367,7 @@
"jest-validate@^26.6.2": "https://registry.yarnpkg.com/jest-validate/-/jest-validate-26.6.2.tgz#23d380971587150467342911c3d7b4ac57ab20ec",
"jest-watcher@^26.6.2": "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-26.6.2.tgz#a5b683b8f9d68dbcb1d7dae32172d2cca0592975",
"jest-worker@^26.6.2": "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed",
"jest@^26.1.0": "https://registry.yarnpkg.com/jest/-/jest-26.6.3.tgz#40e8fdbe48f00dfa1f0ce8121ca74b88ac9148ef",
"jest@^26.6.3": "https://registry.yarnpkg.com/jest/-/jest-26.6.3.tgz#40e8fdbe48f00dfa1f0ce8121ca74b88ac9148ef",
"js-tokens@^4.0.0": "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499",
"js-yaml@^3.13.1": "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537",
"jsbn@~0.1.0": "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513",
@@ -394,8 +393,8 @@
"lines-and-columns@^1.1.6": "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00",
"locate-path@^5.0.0": "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0",
"lodash.sortby@^4.7.0": "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438",
"lodash@4.x": "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52",
"lodash@^4.17.19": "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52",
"lodash@4.x": "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c",
"lodash@^4.17.19": "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c",
"lru-cache@^6.0.0": "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94",
"make-dir@^3.0.0": "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f",
"make-error@1.x": "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2",
@@ -568,7 +567,7 @@
"tough-cookie@^3.0.1": "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2",
"tough-cookie@~2.5.0": "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2",
"tr46@^2.0.2": "https://registry.yarnpkg.com/tr46/-/tr46-2.0.2.tgz#03273586def1595ae08fedb38d7733cee91d2479",
"ts-jest@^26.5.1": "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.5.1.tgz#4d53ee4481552f57c1624f0bd3425c8b17996150",
"ts-jest@^26.5.6": "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.5.6.tgz#c32e0746425274e1dfe333f43cd3c800e014ec35",
"tunnel-agent@^0.6.0": "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd",
"tunnel@0.0.6": "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c",
"tweetnacl@^0.14.3": "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64",

View File

@@ -32,9 +32,9 @@
"devDependencies": {
"@types/jest": "^26.0.20",
"@types/node": "^14.14.25",
"jest": "^26.1.0",
"jest": "^26.6.3",
"jest-circus": "^26.6.3",
"ts-jest": "^26.5.1",
"ts-jest": "^26.5.6",
"typescript": "^4.1.4"
}
}

View File

@@ -1,53 +1,72 @@
import {Inputs} from "./Inputs";
import {CreateReleaseResponse, Releases, UpdateReleaseResponse} from "./Releases";
import {
CreateOrUpdateReleaseResponse,
CreateReleaseResponse,
ReleaseByTagResponse,
Releases,
UpdateReleaseResponse
} from "./Releases";
import {ArtifactUploader} from "./ArtifactUploader";
import {ErrorMessage} from "./ErrorMessage";
import {GithubError} from "./GithubError";
import {Outputs} from "./Outputs";
export class Action {
private inputs: Inputs
private outputs: Outputs
private releases: Releases
private uploader: ArtifactUploader
constructor(inputs: Inputs, releases: Releases, uploader: ArtifactUploader) {
constructor(inputs: Inputs, outputs: Outputs, releases: Releases, uploader: ArtifactUploader) {
this.inputs = inputs
this.outputs = outputs
this.releases = releases
this.uploader = uploader
}
async perform() {
const releaseResponse = await this.createOrUpdateRelease();
const releaseId = releaseResponse.data.id
const uploadUrl = releaseResponse.data.upload_url
const releaseData = releaseResponse.data
const releaseId = releaseData.id
const uploadUrl = releaseData.upload_url
const artifacts = this.inputs.artifacts
if (artifacts.length > 0) {
await this.uploader.uploadArtifacts(artifacts, releaseId, uploadUrl)
}
this.outputs.applyReleaseData(releaseData)
}
private async createOrUpdateRelease(): Promise<CreateReleaseResponse | UpdateReleaseResponse> {
private async createOrUpdateRelease(): Promise<CreateOrUpdateReleaseResponse> {
if (this.inputs.allowUpdates) {
let getResponse: ReleaseByTagResponse
try {
const getResponse = await this.releases.getByTag(this.inputs.tag)
return await this.updateRelease(getResponse.data.id)
getResponse = await this.releases.getByTag(this.inputs.tag)
} catch (error) {
if (Action.noPublishedRelease(error)) {
return await this.updateDraftOrCreateRelease()
} else {
throw error
}
return await this.checkForMissingReleaseError(error)
}
return await this.updateRelease(getResponse.data.id)
} else {
return await this.createRelease()
}
}
private async checkForMissingReleaseError(error: Error): Promise<CreateOrUpdateReleaseResponse> {
if (Action.noPublishedRelease(error)) {
return await this.updateDraftOrCreateRelease()
} else {
throw error
}
}
private async updateRelease(id: number): Promise<UpdateReleaseResponse> {
return await this.releases.update(
id,
this.inputs.tag,
this.inputs.updatedReleaseBody,
this.inputs.commit,
this.inputs.discussionCategory,
this.inputs.draft,
this.inputs.updatedReleaseName,
this.inputs.prerelease
@@ -55,8 +74,8 @@ export class Action {
}
private static noPublishedRelease(error: any): boolean {
const errorMessage = new ErrorMessage(error)
return errorMessage.status == 404
const githubError = new GithubError(error)
return githubError.status == 404
}
private async updateDraftOrCreateRelease(): Promise<CreateReleaseResponse | UpdateReleaseResponse> {
@@ -78,15 +97,14 @@ export class Action {
}
private async createRelease(): Promise<CreateReleaseResponse> {
const response = await this.releases.create(
return await this.releases.create(
this.inputs.tag,
this.inputs.createdReleaseBody,
this.inputs.commit,
this.inputs.discussionCategory,
this.inputs.draft,
this.inputs.createdReleaseName,
this.inputs.prerelease
)
return response
}
}

View File

@@ -4,7 +4,7 @@ import {Artifact} from "./Artifact";
import untildify from "untildify";
export interface ArtifactGlobber {
globArtifactString(artifact: string, contentType: string): Artifact[]
globArtifactString(artifact: string, contentType: string, throwsWhenNoFiles: boolean): Artifact[]
}
export class FileArtifactGlobber implements ArtifactGlobber {
@@ -14,18 +14,22 @@ export class FileArtifactGlobber implements ArtifactGlobber {
this.globber = globber
}
globArtifactString(artifact: string, contentType: string): Artifact[] {
globArtifactString(artifact: string, contentType: string, throwsWhenNoFiles: boolean): Artifact[] {
return artifact.split(',')
.map(path => FileArtifactGlobber.expandPath(path))
.map(pattern => this.globPattern(pattern))
.map(pattern => this.globPattern(pattern, throwsWhenNoFiles))
.reduce((accumulated, current) => accumulated.concat(current))
.map(path => new Artifact(path, contentType))
}
private globPattern(pattern: string): string[] {
private globPattern(pattern: string, throwsWhenNoFiles: boolean): string[] {
const paths = this.globber.glob(pattern)
if (paths.length == 0) {
FileArtifactGlobber.reportGlobWarning(pattern)
if (throwsWhenNoFiles) {
FileArtifactGlobber.throwGlobError(pattern)
} else {
FileArtifactGlobber.reportGlobWarning(pattern)
}
}
return paths
}
@@ -34,6 +38,10 @@ export class FileArtifactGlobber implements ArtifactGlobber {
core.warning(`Artifact pattern :${pattern} did not match any files`)
}
private static throwGlobError(pattern: string) {
throw Error(`Artifact pattern :${pattern} did not match any files`)
}
private static expandPath(path: string): string {
return untildify(path)
}

View File

@@ -10,6 +10,7 @@ export class GithubArtifactUploader implements ArtifactUploader {
constructor(
private releases: Releases,
private replacesExistingArtifacts: boolean = true,
private throwsUploadErrors: boolean = false,
) {
}
@@ -41,14 +42,17 @@ export class GithubArtifactUploader implements ArtifactUploader {
core.warning(`Failed to upload artifact ${artifact.name}. ${error.message}. Retrying...`)
await this.uploadArtifact(artifact, releaseId, uploadUrl, retry - 1)
} else {
core.warning(`Failed to upload artifact ${artifact.name}. ${error.message}.`)
if (this.throwsUploadErrors) {
throw Error(`Failed to upload artifact ${artifact.name}. ${error.message}.`)
} else {
core.warning(`Failed to upload artifact ${artifact.name}. ${error.message}.`)
}
}
}
}
private async deleteUpdatedArtifacts(artifacts: Artifact[], releaseId: number): Promise<void> {
const response = await this.releases.listArtifactsForRelease(releaseId)
const releaseAssets = response.data
const releaseAssets = await this.releases.listArtifactsForRelease(releaseId)
const assetByName: Record<string, { id: number; name: string }> = {}
releaseAssets.forEach(asset => {
assetByName[asset.name] = asset

View File

@@ -1,44 +0,0 @@
import {GithubError} from "./GithubError"
export class ErrorMessage {
private error: any
private githubErrors: GithubError[]
constructor(error: any) {
this.error = error
this.githubErrors = this.generateGithubErrors()
}
private generateGithubErrors(): GithubError[] {
const errors = this.error.errors
if (errors instanceof Array) {
return errors.map((err) => new GithubError(err))
} else {
return []
}
}
get status(): number {
return this.error.status
}
hasErrorWithCode(code: String): boolean {
return this.githubErrors.some((err) => err.code == code)
}
toString(): string {
const message = this.error.message
const errors = this.githubErrors
const status = this.status
if (errors.length > 0) {
return `Error ${status}: ${message}\nErrors:\n${this.errorBulletedList(errors)}`
} else {
return `Error ${status}: ${message}`
}
}
private errorBulletedList(errors: GithubError[]): string {
return errors.map((err) => `- ${err}`).join("\n")
}
}

View File

@@ -1,65 +1,44 @@
import {GithubErrorDetail} from "./GithubErrorDetail"
export class GithubError {
private error: any;
private error: any
private readonly githubErrors: GithubErrorDetail[]
constructor(error: any) {
this.error = error
this.githubErrors = this.generateGithubErrors()
}
get code(): string {
return this.error.code
private generateGithubErrors(): GithubErrorDetail[] {
const errors = this.error.errors
if (errors instanceof Array) {
return errors.map((err) => new GithubErrorDetail(err))
} else {
return []
}
}
get status(): number {
return this.error.status
}
hasErrorWithCode(code: String): boolean {
return this.githubErrors.some((err) => err.code == code)
}
toString(): string {
const code = this.error.code
switch (code) {
case 'missing':
return this.missingResourceMessage()
case 'missing_field':
return this.missingFieldMessage()
case 'invalid':
return this.invalidFieldMessage()
case 'already_exists':
return this.resourceAlreadyExists()
default:
return this.customErrorMessage()
}
}
private customErrorMessage(): string {
const message = this.error.message;
const documentation = this.error.documentation_url
let documentationMessage: string
if (documentation) {
documentationMessage = `\nPlease see ${documentation}.`
const message = this.error.message
const errors = this.githubErrors
const status = this.status
if (errors.length > 0) {
return `Error ${status}: ${message}\nErrors:\n${this.errorBulletedList(errors)}`
} else {
documentationMessage = ""
return `Error ${status}: ${message}`
}
return `${message}${documentationMessage}`
}
private invalidFieldMessage(): string {
const resource = this.error.resource
const field = this.error.field
return `The ${field} field on ${resource} is an invalid format.`
}
private missingResourceMessage(): string {
const resource = this.error.resource
return `${resource} does not exist.`
}
private missingFieldMessage(): string {
const resource = this.error.resource
const field = this.error.field
return `The ${field} field on ${resource} is missing.`
}
private resourceAlreadyExists(): string {
const resource = this.error.resource
return `${resource} already exists.`
private errorBulletedList(errors: GithubErrorDetail[]): string {
return errors.map((err) => `- ${err}`).join("\n")
}
}

65
src/GithubErrorDetail.ts Normal file
View File

@@ -0,0 +1,65 @@
export class GithubErrorDetail {
private error: any;
constructor(error: any) {
this.error = error
}
get code(): string {
return this.error.code
}
toString(): string {
const code = this.error.code
switch (code) {
case 'missing':
return this.missingResourceMessage()
case 'missing_field':
return this.missingFieldMessage()
case 'invalid':
return this.invalidFieldMessage()
case 'already_exists':
return this.resourceAlreadyExists()
default:
return this.customErrorMessage()
}
}
private customErrorMessage(): string {
const message = this.error.message;
const documentation = this.error.documentation_url
let documentationMessage: string
if (documentation) {
documentationMessage = `\nPlease see ${documentation}.`
} else {
documentationMessage = ""
}
return `${message}${documentationMessage}`
}
private invalidFieldMessage(): string {
const resource = this.error.resource
const field = this.error.field
return `The ${field} field on ${resource} is an invalid format.`
}
private missingResourceMessage(): string {
const resource = this.error.resource
return `${resource} does not exist.`
}
private missingFieldMessage(): string {
const resource = this.error.resource
const field = this.error.field
return `The ${field} field on ${resource} is missing.`
}
private resourceAlreadyExists(): string {
const resource = this.error.resource
return `${resource} already exists.`
}
}

View File

@@ -6,10 +6,12 @@ import {Artifact} from './Artifact';
export interface Inputs {
readonly allowUpdates: boolean
readonly artifactErrorsFailBuild: boolean
readonly artifacts: Artifact[]
readonly commit: string
readonly createdReleaseBody?: string
readonly createdReleaseName?: string
readonly discussionCategory?: string
readonly draft: boolean
readonly owner: string
readonly prerelease: boolean
@@ -46,11 +48,16 @@ export class CoreInputs implements Inputs {
contentType = 'raw'
}
return this.artifactGlobber
.globArtifactString(artifacts, contentType)
.globArtifactString(artifacts, contentType, this.artifactErrorsFailBuild)
}
return []
}
get artifactErrorsFailBuild(): boolean {
const allow = core.getInput('artifactErrorsFailBuild')
return allow == 'true'
}
get createdReleaseBody(): string | undefined {
if (CoreInputs.omitBody) return undefined
return this.body
@@ -60,7 +67,7 @@ export class CoreInputs implements Inputs {
return core.getInput('omitBody') == 'true'
}
private get body() : string | undefined {
private get body(): string | undefined {
const body = core.getInput('body')
if (body) {
return body
@@ -83,6 +90,14 @@ export class CoreInputs implements Inputs {
return this.name
}
get discussionCategory(): string | undefined {
const category = core.getInput('discussionCategory')
if (category) {
return category
}
return undefined
}
private static get omitName(): boolean {
return core.getInput('omitName') == 'true'
}
@@ -156,7 +171,7 @@ export class CoreInputs implements Inputs {
}
get updatedReleaseName(): string | undefined {
if (CoreInputs.omitName || CoreInputs.omitNameDuringUpdate) return undefined
if (CoreInputs.omitName || CoreInputs.omitNameDuringUpdate) return undefined
return this.name
}

View File

@@ -1,32 +1,34 @@
import * as github from '@actions/github';
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';
import { ErrorMessage } from './ErrorMessage';
import {CoreInputs} from './Inputs';
import {GithubReleases} from './Releases';
import {Action} from './Action';
import {GithubArtifactUploader} from './ArtifactUploader';
import {FileArtifactGlobber} from './ArtifactGlobber';
import {GithubError} from './GithubError';
import {CoreOutputs} from "./Outputs";
async function run() {
try {
const action = createAction()
await action.perform()
} catch (error) {
const errorMessage = new ErrorMessage(error)
core.setFailed(errorMessage.toString());
}
try {
const action = createAction()
await action.perform()
} catch (error) {
const githubError = new GithubError(error)
core.setFailed(githubError.toString());
}
}
function createAction(): Action {
const token = core.getInput('token')
const context = github.context
const git = github.getOctokit(token)
const globber = new FileArtifactGlobber()
const token = core.getInput('token')
const context = github.context
const git = github.getOctokit(token)
const globber = new FileArtifactGlobber()
const inputs = new CoreInputs(globber, context)
const releases = new GithubReleases(inputs, git)
const uploader = new GithubArtifactUploader(releases, inputs.replacesArtifacts)
return new Action(inputs, releases, uploader)
const inputs = new CoreInputs(globber, context)
const outputs = new CoreOutputs()
const releases = new GithubReleases(inputs, git)
const uploader = new GithubArtifactUploader(releases, inputs.replacesArtifacts, inputs.artifactErrorsFailBuild)
return new Action(inputs, outputs, releases, uploader)
}
run();

14
src/Outputs.ts Normal file
View File

@@ -0,0 +1,14 @@
import * as core from '@actions/core';
import {ReleaseData} from "./Releases";
export interface Outputs {
applyReleaseData(releaseData: ReleaseData): void
}
export class CoreOutputs implements Outputs {
applyReleaseData(releaseData: ReleaseData) {
core.setOutput('id', releaseData.id)
core.setOutput('html_url', releaseData.html_url)
core.setOutput('upload_url', releaseData.upload_url)
}
}

View File

@@ -6,15 +6,23 @@ import {Inputs} from "./Inputs";
export type CreateReleaseResponse = RestEndpointMethodTypes["repos"]["createRelease"]["response"]
export type ReleaseByTagResponse = RestEndpointMethodTypes["repos"]["getReleaseByTag"]["response"]
export type ListReleasesResponse = RestEndpointMethodTypes["repos"]["listReleases"]["response"]
export type ListReleaseAssetsResponse = RestEndpointMethodTypes["repos"]["listReleaseAssets"]["response"]
export type ListReleaseAssetsResponseData = RestEndpointMethodTypes["repos"]["listReleaseAssets"]["response"]["data"]
export type UpdateReleaseResponse = RestEndpointMethodTypes["repos"]["updateRelease"]["response"]
export type UploadArtifactResponse = RestEndpointMethodTypes["repos"]["uploadReleaseAsset"]["response"]
export type CreateOrUpdateReleaseResponse = CreateReleaseResponse | UpdateReleaseResponse
export type ReleaseData = {
id: number
html_url: string
upload_url: string
}
export interface Releases {
create(
tag: string,
body?: string,
commitHash?: string,
discussionCategory?: string,
draft?: boolean,
name?: string,
prerelease?: boolean
@@ -24,7 +32,7 @@ export interface Releases {
getByTag(tag: string): Promise<ReleaseByTagResponse>
listArtifactsForRelease(releaseId: number): Promise<ListReleaseAssetsResponse>
listArtifactsForRelease(releaseId: number): Promise<ListReleaseAssetsResponseData>
listReleases(): Promise<ListReleasesResponse>
@@ -33,6 +41,7 @@ export interface Releases {
tag: string,
body?: string,
commitHash?: string,
discussionCategory?: string,
draft?: boolean,
name?: string,
prerelease?: boolean
@@ -61,6 +70,7 @@ export class GithubReleases implements Releases {
tag: string,
body?: string,
commitHash?: string,
discussionCategory?: string,
draft?: boolean,
name?: string,
prerelease?: boolean
@@ -69,6 +79,7 @@ export class GithubReleases implements Releases {
return this.git.repos.createRelease({
body: body,
name: name,
discussion_category_name: discussionCategory,
draft: draft,
owner: this.inputs.owner,
prerelease: prerelease,
@@ -98,8 +109,8 @@ export class GithubReleases implements Releases {
async listArtifactsForRelease(
releaseId: number
): Promise<ListReleaseAssetsResponse> {
return this.git.repos.listReleaseAssets({
): Promise<ListReleaseAssetsResponseData> {
return this.git.paginate(this.git.repos.listReleaseAssets, {
owner: this.inputs.owner,
release_id: releaseId,
repo: this.inputs.repo
@@ -118,6 +129,7 @@ export class GithubReleases implements Releases {
tag: string,
body?: string,
commitHash?: string,
discussionCategory?: string,
draft?: boolean,
name?: string,
prerelease?: boolean
@@ -127,6 +139,7 @@ export class GithubReleases implements Releases {
release_id: id,
body: body,
name: name,
discussion_category_name: discussionCategory,
draft: draft,
owner: this.inputs.owner,
prerelease: prerelease,

View File

@@ -656,7 +656,7 @@
dependencies:
"@types/istanbul-lib-report" "*"
"@types/jest@26.x", "@types/jest@^26.0.20":
"@types/jest@^26.0.20":
version "26.0.20"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.20.tgz#cd2f2702ecf69e86b586e1f5223a60e454056307"
integrity sha512-9zi2Y+5USJRxd0FsahERhBwlcvFh6D2GLQnY2FH2BzK8J9s9omvNHIbvABwIluXa0fD8XVKMLTO0aOEuUfACAA==
@@ -1695,9 +1695,9 @@ has@^1.0.3:
function-bind "^1.1.1"
hosted-git-info@^2.1.4:
version "2.8.8"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
version "2.8.9"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
html-encoding-sniffer@^2.0.1:
version "2.0.1"
@@ -2375,7 +2375,7 @@ jest-worker@^26.6.2:
merge-stream "^2.0.0"
supports-color "^7.0.0"
jest@^26.1.0:
jest@^26.6.3:
version "26.6.3"
resolved "https://registry.yarnpkg.com/jest/-/jest-26.6.3.tgz#40e8fdbe48f00dfa1f0ce8121ca74b88ac9148ef"
integrity sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q==
@@ -2536,9 +2536,9 @@ lodash.sortby@^4.7.0:
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
lodash@4.x, lodash@^4.17.19:
version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
lru-cache@^6.0.0:
version "6.0.0"
@@ -3517,12 +3517,11 @@ tr46@^2.0.2:
dependencies:
punycode "^2.1.1"
ts-jest@^26.5.1:
version "26.5.1"
resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.5.1.tgz#4d53ee4481552f57c1624f0bd3425c8b17996150"
integrity sha512-G7Rmo3OJMvlqE79amJX8VJKDiRcd7/r61wh9fnvvG8cAjhA9edklGw/dCxRSQmfZ/z8NDums5srSVgwZos1qfg==
ts-jest@^26.5.6:
version "26.5.6"
resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.5.6.tgz#c32e0746425274e1dfe333f43cd3c800e014ec35"
integrity sha512-rua+rCP8DxpA8b4DQD/6X2HQS8Zy/xzViVYfEs2OQu68tkCuKLV0Md8pmX55+W24uRIyAsf/BajRfxOs+R2MKA==
dependencies:
"@types/jest" "26.x"
bs-logger "0.x"
buffer-from "1.x"
fast-json-stable-stringify "2.x"