Fixes #50 Add plugin outputs which contain release information.

This commit is contained in:
Nick Cipollo
2021-05-12 10:45:15 -04:00
parent 085dc21232
commit 616708020f
12 changed files with 179 additions and 38 deletions

View File

@@ -25,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). - **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 }}`. - **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 ## Example
This example will create a release when tag is pushed: 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 {Inputs} from "../src/Inputs";
import {Releases} from "../src/Releases"; import {Releases} from "../src/Releases";
import {ArtifactUploader} from "../src/ArtifactUploader"; import {ArtifactUploader} from "../src/ArtifactUploader";
import {Outputs} from "../src/Outputs";
const applyReleaseDataMock = jest.fn()
const createMock = jest.fn() const createMock = jest.fn()
const deleteMock = jest.fn() const deleteMock = jest.fn()
const getMock = jest.fn() const getMock = jest.fn()
@@ -48,6 +50,7 @@ describe("Action", () => {
expect(createMock).toBeCalledWith(tag, createBody, commit, discussionCategory, draft, createName, prerelease) expect(createMock).toBeCalledWith(tag, createBody, commit, discussionCategory, draft, createName, prerelease)
expect(uploadMock).not.toBeCalled() expect(uploadMock).not.toBeCalled()
assertOutputApplied()
}) })
it('creates release if no release exists to update', async () => { it('creates release if no release exists to update', async () => {
@@ -59,6 +62,7 @@ describe("Action", () => {
expect(createMock).toBeCalledWith(tag, createBody, commit, discussionCategory, draft, createName, prerelease) expect(createMock).toBeCalledWith(tag, createBody, commit, discussionCategory, draft, createName, prerelease)
expect(uploadMock).toBeCalledWith(artifacts, releaseId, url) expect(uploadMock).toBeCalledWith(artifacts, releaseId, url)
assertOutputApplied()
}) })
it('creates release if no draft releases', async () => { it('creates release if no draft releases', async () => {
@@ -75,6 +79,7 @@ describe("Action", () => {
expect(createMock).toBeCalledWith(tag, createBody, commit, discussionCategory, draft, createName, prerelease) expect(createMock).toBeCalledWith(tag, createBody, commit, discussionCategory, draft, createName, prerelease)
expect(uploadMock).toBeCalledWith(artifacts, releaseId, url) expect(uploadMock).toBeCalledWith(artifacts, releaseId, url)
assertOutputApplied()
}) })
@@ -85,6 +90,7 @@ describe("Action", () => {
expect(createMock).toBeCalledWith(tag, createBody, commit, discussionCategory, draft, createName, prerelease) expect(createMock).toBeCalledWith(tag, createBody, commit, discussionCategory, draft, createName, prerelease)
expect(uploadMock).toBeCalledWith(artifacts, releaseId, url) expect(uploadMock).toBeCalledWith(artifacts, releaseId, url)
assertOutputApplied()
}) })
it('throws error when create fails', async () => { it('throws error when create fails', async () => {
@@ -192,7 +198,7 @@ describe("Action", () => {
prerelease prerelease
) )
expect(uploadMock).toBeCalledWith(artifacts, releaseId, url) expect(uploadMock).toBeCalledWith(artifacts, releaseId, url)
assertOutputApplied()
}) })
it('updates release but does not upload if no artifact', async () => { it('updates release but does not upload if no artifact', async () => {
@@ -211,7 +217,7 @@ describe("Action", () => {
prerelease prerelease
) )
expect(uploadMock).not.toBeCalled() expect(uploadMock).not.toBeCalled()
assertOutputApplied()
}) })
it('updates release then uploads artifact', async () => { it('updates release then uploads artifact', async () => {
@@ -230,9 +236,13 @@ describe("Action", () => {
prerelease prerelease
) )
expect(uploadMock).toBeCalledWith(artifacts, releaseId, url) expect(uploadMock).toBeCalledWith(artifacts, releaseId, url)
assertOutputApplied()
}) })
function assertOutputApplied() {
expect(applyReleaseDataMock).toBeCalledWith({id: releaseId, upload_url: url})
}
function createAction(allowUpdates: boolean, hasArtifact: boolean): Action { function createAction(allowUpdates: boolean, hasArtifact: boolean): Action {
let inputArtifact: Artifact[] let inputArtifact: Artifact[]
if (hasArtifact) { if (hasArtifact) {
@@ -294,6 +304,11 @@ describe("Action", () => {
updatedReleaseName: updateName updatedReleaseName: updateName
} }
}) })
const MockOutputs = jest.fn<Outputs, any>(() => {
return {
applyReleaseData: applyReleaseDataMock
}
})
const MockUploader = jest.fn<ArtifactUploader, any>(() => { const MockUploader = jest.fn<ArtifactUploader, any>(() => {
return { return {
uploadArtifacts: uploadMock uploadArtifacts: uploadMock
@@ -301,9 +316,10 @@ describe("Action", () => {
}) })
const inputs = new MockInputs() const inputs = new MockInputs()
const outputs = new MockOutputs()
const releases = new MockReleases() const releases = new MockReleases()
const uploader = new MockUploader() const uploader = new MockUploader()
return new Action(inputs, releases, uploader) return new Action(inputs, outputs, releases, uploader)
} }
}) })

View File

@@ -1,10 +1,11 @@
import {Action} from "../src/Action"; import {Action} from "../src/Action";
import * as github from "@actions/github"; import * as github from "@actions/github";
import {Inputs} from "../src/Inputs"; import {Inputs} from "../src/Inputs";
import {GithubReleases} from "../src/Releases"; import {GithubReleases, ReleaseData} from "../src/Releases";
import {GithubArtifactUploader} from "../src/ArtifactUploader"; import {GithubArtifactUploader} from "../src/ArtifactUploader";
import * as path from "path"; import * as path from "path";
import {FileArtifactGlobber} from "../src/ArtifactGlobber"; import {FileArtifactGlobber} from "../src/ArtifactGlobber";
import {Outputs} from "../src/Outputs";
// This test is currently intended to be manually run during development. To run: // 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 // - Make sure you have an environment variable named GITHUB_TOKEN assigned to your token
@@ -17,13 +18,14 @@ describe.skip('Integration Test', () => {
const git = github.getOctokit(token) const git = github.getOctokit(token)
const inputs = getInputs() const inputs = getInputs()
const outputs = getOutputs()
const releases = new GithubReleases(inputs, git) const releases = new GithubReleases(inputs, git)
const uploader = new GithubArtifactUploader( const uploader = new GithubArtifactUploader(
releases, releases,
inputs.replacesArtifacts, inputs.replacesArtifacts,
inputs.artifactErrorsFailBuild, inputs.artifactErrorsFailBuild,
) )
action = new Action(inputs, releases, uploader) action = new Action(inputs, outputs, releases, uploader)
}) })
it('Performs action', async () => { it('Performs action', async () => {
@@ -54,6 +56,17 @@ describe.skip('Integration Test', () => {
return new MockInputs(); 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() { function artifacts() {
const globber = new FileArtifactGlobber() const globber = new FileArtifactGlobber()
const artifactPath = path.join(__dirname, 'Integration.test.ts') const artifactPath = path.join(__dirname, 'Integration.test.ts')

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

@@ -87,6 +87,13 @@ inputs:
description: 'The Github token.' description: 'The Github token.'
required: true required: true
default: '' 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: runs:
using: 'node12' using: 'node12'
main: 'lib/Main.js' main: 'lib/Main.js'

View File

@@ -12,20 +12,23 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.Action = void 0; exports.Action = void 0;
const GithubError_1 = require("./GithubError"); const GithubError_1 = require("./GithubError");
class Action { class Action {
constructor(inputs, releases, uploader) { constructor(inputs, outputs, releases, uploader) {
this.inputs = inputs; this.inputs = inputs;
this.outputs = outputs;
this.releases = releases; this.releases = releases;
this.uploader = uploader; this.uploader = uploader;
} }
perform() { perform() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const releaseResponse = yield this.createOrUpdateRelease(); const releaseResponse = yield this.createOrUpdateRelease();
const releaseId = releaseResponse.data.id; const releaseData = releaseResponse.data;
const uploadUrl = releaseResponse.data.upload_url; const releaseId = releaseData.id;
const uploadUrl = releaseData.upload_url;
const artifacts = this.inputs.artifacts; const artifacts = this.inputs.artifacts;
if (artifacts.length > 0) { if (artifacts.length > 0) {
yield this.uploader.uploadArtifacts(artifacts, releaseId, uploadUrl); yield this.uploader.uploadArtifacts(artifacts, releaseId, uploadUrl);
} }
this.outputs.applyReleaseData(releaseData);
}); });
} }
createOrUpdateRelease() { createOrUpdateRelease() {

View File

@@ -36,6 +36,7 @@ const Action_1 = require("./Action");
const ArtifactUploader_1 = require("./ArtifactUploader"); const ArtifactUploader_1 = require("./ArtifactUploader");
const ArtifactGlobber_1 = require("./ArtifactGlobber"); const ArtifactGlobber_1 = require("./ArtifactGlobber");
const GithubError_1 = require("./GithubError"); const GithubError_1 = require("./GithubError");
const Outputs_1 = require("./Outputs");
function run() { function run() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
try { try {
@@ -54,8 +55,9 @@ function createAction() {
const git = github.getOctokit(token); const git = github.getOctokit(token);
const globber = new ArtifactGlobber_1.FileArtifactGlobber(); const globber = new ArtifactGlobber_1.FileArtifactGlobber();
const inputs = new Inputs_1.CoreInputs(globber, context); const inputs = new Inputs_1.CoreInputs(globber, context);
const outputs = new Outputs_1.CoreOutputs();
const releases = new Releases_1.GithubReleases(inputs, git); const releases = new Releases_1.GithubReleases(inputs, git);
const uploader = new ArtifactUploader_1.GithubArtifactUploader(releases, inputs.replacesArtifacts, inputs.artifactErrorsFailBuild); const uploader = new ArtifactUploader_1.GithubArtifactUploader(releases, inputs.replacesArtifacts, inputs.artifactErrorsFailBuild);
return new Action_1.Action(inputs, releases, uploader); return new Action_1.Action(inputs, outputs, releases, uploader);
} }
run(); 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

@@ -1,31 +1,43 @@
import {Inputs} from "./Inputs"; import {Inputs} from "./Inputs";
import {CreateReleaseResponse, ReleaseByTagResponse, Releases, UpdateReleaseResponse} from "./Releases"; import {
CreateOrUpdateReleaseResponse,
CreateReleaseResponse,
ReleaseByTagResponse,
Releases,
UpdateReleaseResponse
} from "./Releases";
import {ArtifactUploader} from "./ArtifactUploader"; import {ArtifactUploader} from "./ArtifactUploader";
import {GithubError} from "./GithubError"; import {GithubError} from "./GithubError";
import {Outputs} from "./Outputs";
export class Action { export class Action {
private inputs: Inputs private inputs: Inputs
private outputs: Outputs
private releases: Releases private releases: Releases
private uploader: ArtifactUploader private uploader: ArtifactUploader
constructor(inputs: Inputs, releases: Releases, uploader: ArtifactUploader) { constructor(inputs: Inputs, outputs: Outputs, releases: Releases, uploader: ArtifactUploader) {
this.inputs = inputs this.inputs = inputs
this.outputs = outputs
this.releases = releases this.releases = releases
this.uploader = uploader this.uploader = uploader
} }
async perform() { async perform() {
const releaseResponse = await this.createOrUpdateRelease(); const releaseResponse = await this.createOrUpdateRelease();
const releaseId = releaseResponse.data.id const releaseData = releaseResponse.data
const uploadUrl = releaseResponse.data.upload_url const releaseId = releaseData.id
const uploadUrl = releaseData.upload_url
const artifacts = this.inputs.artifacts const artifacts = this.inputs.artifacts
if (artifacts.length > 0) { if (artifacts.length > 0) {
await this.uploader.uploadArtifacts(artifacts, releaseId, uploadUrl) 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) { if (this.inputs.allowUpdates) {
let getResponse: ReleaseByTagResponse let getResponse: ReleaseByTagResponse
try { try {
@@ -39,8 +51,8 @@ export class Action {
return await this.createRelease() return await this.createRelease()
} }
} }
private async checkForMissingReleaseError(error: Error) : Promise<CreateReleaseResponse | UpdateReleaseResponse> { private async checkForMissingReleaseError(error: Error): Promise<CreateOrUpdateReleaseResponse> {
if (Action.noPublishedRelease(error)) { if (Action.noPublishedRelease(error)) {
return await this.updateDraftOrCreateRelease() return await this.updateDraftOrCreateRelease()
} else { } else {

View File

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

@@ -9,6 +9,13 @@ export type ListReleasesResponse = RestEndpointMethodTypes["repos"]["listRelease
export type ListReleaseAssetsResponseData = RestEndpointMethodTypes["repos"]["listReleaseAssets"]["response"]["data"] export type ListReleaseAssetsResponseData = RestEndpointMethodTypes["repos"]["listReleaseAssets"]["response"]["data"]
export type UpdateReleaseResponse = RestEndpointMethodTypes["repos"]["updateRelease"]["response"] export type UpdateReleaseResponse = RestEndpointMethodTypes["repos"]["updateRelease"]["response"]
export type UploadArtifactResponse = RestEndpointMethodTypes["repos"]["uploadReleaseAsset"]["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 { export interface Releases {
create( create(