Fixes #33 Add artifactErrorsFailBuild flag

This commit is contained in:
Nick Cipollo
2021-03-21 19:05:15 -04:00
parent 5b1448e480
commit af980963d6
15 changed files with 149 additions and 49 deletions

View File

@@ -4,6 +4,7 @@ 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.

View File

@@ -17,6 +17,7 @@ const artifacts = [
new Artifact('b/art2')
]
const artifactErrorsFailBuild = false
const createBody = 'createBody'
const createName = 'createName'
const commit = 'commit'
@@ -239,6 +240,7 @@ describe("Action", () => {
const MockInputs = jest.fn<Inputs, any>(() => {
return {
allowUpdates: allowUpdates,
artifactErrorsFailBuild: true,
artifacts: inputArtifact,
createdReleaseBody: createBody,
createdReleaseName: createName,

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 {
@@ -194,14 +209,24 @@ describe('ArtifactUploader', () => {
}
function mockListWithoutAssets() {
listArtifactsMock.mockResolvedValue({ data: [] })
listArtifactsMock.mockResolvedValue({data: []})
}
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

@@ -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)
})
})
@@ -164,7 +189,7 @@ describe('Inputs', () => {
expect(inputs.draft).toBe(true)
})
})
describe('owner', () => {
it('returns owner from context', function () {
process.env.GITHUB_REPOSITORY = "owner/repo"
@@ -175,7 +200,7 @@ describe('Inputs', () => {
mockGetInput.mockReturnValue("owner")
expect(inputs.owner).toBe("owner")
});
})
})
describe('prerelase', () => {
it('returns false', () => {

View File

@@ -18,7 +18,11 @@ describe.skip('Integration Test', () => {
const inputs = getInputs()
const releases = new GithubReleases(inputs, git)
const uploader = new GithubArtifactUploader(releases, inputs.replacesArtifacts)
const uploader = new GithubArtifactUploader(
releases,
inputs.artifactErrorsFailBuild,
inputs.replacesArtifacts
)
action = new Action(inputs, releases, uploader)
})
@@ -30,6 +34,7 @@ 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",
@@ -47,12 +52,12 @@ describe.skip('Integration Test', () => {
})
return new MockInputs();
}
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 {

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)'

View File

@@ -80,8 +80,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.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,7 +58,12 @@ 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}.`);
}
}
}
});

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;

View File

@@ -78,7 +78,7 @@ 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,
@@ -86,7 +86,5 @@ export class Action {
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,7 +42,11 @@ 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}.`)
}
}
}
}

View File

@@ -6,6 +6,7 @@ import {Artifact} from './Artifact';
export interface Inputs {
readonly allowUpdates: boolean
readonly artifactErrorsFailBuild: boolean
readonly artifacts: Artifact[]
readonly commit: string
readonly createdReleaseBody?: string
@@ -46,11 +47,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