diff --git a/README.md b/README.md index af7e9b6..2a8ac4c 100644 --- a/README.md +++ b/README.md @@ -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). - **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: diff --git a/__tests__/Action.test.ts b/__tests__/Action.test.ts index 8a98a9d..7ab56a4 100644 --- a/__tests__/Action.test.ts +++ b/__tests__/Action.test.ts @@ -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() @@ -48,6 +50,7 @@ describe("Action", () => { 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 () => { @@ -59,6 +62,7 @@ describe("Action", () => { 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 () => { @@ -75,6 +79,7 @@ describe("Action", () => { expect(createMock).toBeCalledWith(tag, createBody, commit, discussionCategory, draft, createName, prerelease) expect(uploadMock).toBeCalledWith(artifacts, releaseId, url) + assertOutputApplied() }) @@ -85,6 +90,7 @@ describe("Action", () => { expect(createMock).toBeCalledWith(tag, createBody, commit, discussionCategory, draft, createName, prerelease) expect(uploadMock).toBeCalledWith(artifacts, releaseId, url) + assertOutputApplied() }) it('throws error when create fails', async () => { @@ -192,7 +198,7 @@ describe("Action", () => { prerelease ) expect(uploadMock).toBeCalledWith(artifacts, releaseId, url) - + assertOutputApplied() }) it('updates release but does not upload if no artifact', async () => { @@ -211,7 +217,7 @@ describe("Action", () => { prerelease ) expect(uploadMock).not.toBeCalled() - + assertOutputApplied() }) it('updates release then uploads artifact', async () => { @@ -230,9 +236,13 @@ describe("Action", () => { 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) { @@ -294,6 +304,11 @@ describe("Action", () => { updatedReleaseName: updateName } }) + const MockOutputs = jest.fn(() => { + return { + applyReleaseData: applyReleaseDataMock + } + }) const MockUploader = jest.fn(() => { return { uploadArtifacts: uploadMock @@ -301,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) } }) diff --git a/__tests__/Integration.test.ts b/__tests__/Integration.test.ts index f326b1f..fd00962 100644 --- a/__tests__/Integration.test.ts +++ b/__tests__/Integration.test.ts @@ -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,13 +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, inputs.artifactErrorsFailBuild, ) - action = new Action(inputs, releases, uploader) + action = new Action(inputs, outputs, releases, uploader) }) it('Performs action', async () => { @@ -54,6 +56,17 @@ describe.skip('Integration Test', () => { return new MockInputs(); } + function getOutputs(): Outputs { + const MockOutputs = jest.fn(() => { + 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') diff --git a/__tests__/Outputs.test.ts b/__tests__/Outputs.test.ts new file mode 100644 index 0000000..56bacd6 --- /dev/null +++ b/__tests__/Outputs.test.ts @@ -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) + }) +}) diff --git a/action.yml b/action.yml index 40457f9..062ca8f 100644 --- a/action.yml +++ b/action.yml @@ -87,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' diff --git a/lib/Action.js b/lib/Action.js index 33c5d73..c72ab74 100644 --- a/lib/Action.js +++ b/lib/Action.js @@ -12,20 +12,23 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.Action = void 0; 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() { diff --git a/lib/Main.js b/lib/Main.js index 583468b..bdb8485 100644 --- a/lib/Main.js +++ b/lib/Main.js @@ -36,6 +36,7 @@ const Action_1 = require("./Action"); const ArtifactUploader_1 = require("./ArtifactUploader"); const ArtifactGlobber_1 = require("./ArtifactGlobber"); const GithubError_1 = require("./GithubError"); +const Outputs_1 = require("./Outputs"); function run() { return __awaiter(this, void 0, void 0, function* () { try { @@ -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, inputs.artifactErrorsFailBuild); - return new Action_1.Action(inputs, releases, uploader); + return new Action_1.Action(inputs, outputs, releases, uploader); } run(); diff --git a/lib/Outputs.js b/lib/Outputs.js new file mode 100644 index 0000000..af881aa --- /dev/null +++ b/lib/Outputs.js @@ -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; diff --git a/src/Action.ts b/src/Action.ts index bf8552b..2f89dcf 100644 --- a/src/Action.ts +++ b/src/Action.ts @@ -1,31 +1,43 @@ import {Inputs} from "./Inputs"; -import {CreateReleaseResponse, ReleaseByTagResponse, Releases, UpdateReleaseResponse} from "./Releases"; +import { + CreateOrUpdateReleaseResponse, + CreateReleaseResponse, + ReleaseByTagResponse, + Releases, + UpdateReleaseResponse +} from "./Releases"; import {ArtifactUploader} from "./ArtifactUploader"; 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 { + private async createOrUpdateRelease(): Promise { if (this.inputs.allowUpdates) { let getResponse: ReleaseByTagResponse try { @@ -39,8 +51,8 @@ export class Action { return await this.createRelease() } } - - private async checkForMissingReleaseError(error: Error) : Promise { + + private async checkForMissingReleaseError(error: Error): Promise { if (Action.noPublishedRelease(error)) { return await this.updateDraftOrCreateRelease() } else { diff --git a/src/Main.ts b/src/Main.ts index 899dc1d..d3f1df8 100644 --- a/src/Main.ts +++ b/src/Main.ts @@ -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 { GithubError } from './GithubError'; +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 githubError = new GithubError(error) - core.setFailed(githubError.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, inputs.artifactErrorsFailBuild) - 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(); diff --git a/src/Outputs.ts b/src/Outputs.ts new file mode 100644 index 0000000..3630e3e --- /dev/null +++ b/src/Outputs.ts @@ -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) + } +} \ No newline at end of file diff --git a/src/Releases.ts b/src/Releases.ts index 0bb71c8..42fb981 100644 --- a/src/Releases.ts +++ b/src/Releases.ts @@ -9,6 +9,13 @@ export type ListReleasesResponse = RestEndpointMethodTypes["repos"]["listRelease 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(