Skip to content

Commit

Permalink
Fixes #267 Add ActionSkipper
Browse files Browse the repository at this point in the history
  • Loading branch information
ncipollo committed Nov 11, 2022
1 parent 3ac4132 commit 3bacd9e
Show file tree
Hide file tree
Showing 16 changed files with 297 additions and 17 deletions.
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ This action will create a GitHub release and optionally upload an artifact to it
| removeArtifacts | Indicates if existing release artifacts should be removed. | false | false |
| replacesArtifacts | Indicates if existing release artifacts should be replaced. | false | true |
| repo | Optionally specify the repo where the release should be generated. | false | current repo |
| skipIfReleaseExists | When skipIfReleaseExists is enabled the action will be skipped if a non-draft release already exists for the provided tag. | false | current repo |

This comment has been minimized.

Copy link
@phdru

phdru Nov 25, 2022

skipIfReleaseExists must be documented as boolean; possible values true/false, not "current repo".

| tag | An optional tag for the release. If this is omitted the git ref will be used (if it is a tag). | false | "" |
| token | The GitHub token. This will default to the GitHub app token. This is primarily useful if you want to use your personal token (for targeting other repos, etc). If you are using a personal access token it should have access to the `repo` scope. | false | github.token |
| updateOnlyUnreleased | When allowUpdates is enabled, this will fail the action if the release it is updating is not a draft or a prerelease. | false | false |
Expand Down Expand Up @@ -71,8 +72,6 @@ jobs:
with:
artifacts: "release.tar.gz,foo/*.txt"
bodyFile: "body.md"
token: ${{ secrets.YOUR_GITHUB_TOKEN }}

```

## Notes
Expand Down
26 changes: 24 additions & 2 deletions __tests__/Action.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@ import {Releases} from "../src/Releases";
import {ArtifactUploader} from "../src/ArtifactUploader";
import {Outputs} from "../src/Outputs";
import {ArtifactDestroyer} from "../src/ArtifactDestroyer";
import {ActionSkipper} from "../src/ActionSkipper";

const applyReleaseDataMock = jest.fn()
const artifactDestroyMock = jest.fn()
const createMock = jest.fn()
const deleteMock = jest.fn()
const getMock = jest.fn()
const listArtifactsMock = jest.fn()
const listMock = jest.fn()
const shouldSkipMock = jest.fn()
const updateMock = jest.fn()
const uploadMock = jest.fn()
const artifactDestroyMock = jest.fn()

const artifacts = [
new Artifact('a/art1'),
Expand Down Expand Up @@ -45,6 +47,7 @@ describe("Action", () => {
createMock.mockClear()
getMock.mockClear()
listMock.mockClear()
shouldSkipMock.mockClear()
updateMock.mockClear()
uploadMock.mockClear()
})
Expand Down Expand Up @@ -150,6 +153,16 @@ describe("Action", () => {
assertOutputApplied()
})

it('skips action', async () => {
const action = createAction(false, false, false)
shouldSkipMock.mockResolvedValue(true)

await action.perform()

expect(createMock).not.toBeCalled()
expect(updateMock).not.toBeCalled()
})

it('throws error when create fails', async () => {
const action = createAction(false, true)
createMock.mockRejectedValue("error")
Expand Down Expand Up @@ -353,6 +366,7 @@ describe("Action", () => {
listMock.mockResolvedValue({
data: []
})
shouldSkipMock.mockResolvedValue(false)
updateMock.mockResolvedValue({
data: {
id: releaseId,
Expand All @@ -377,6 +391,7 @@ describe("Action", () => {
replacesArtifacts: replacesArtifacts,
removeArtifacts: removeArtifacts,
repo: "repo",
skipIfReleaseExists: false,
tag: tag,
token: token,
updatedDraft: updateDraft,
Expand All @@ -401,13 +416,20 @@ describe("Action", () => {
destroyArtifacts: artifactDestroyMock
}
})

const MockActionSkipper = jest.fn<ActionSkipper, any>(() => {
return {
shouldSkip: shouldSkipMock
}
})

const inputs = new MockInputs()
const outputs = new MockOutputs()
const releases = new MockReleases()
const uploader = new MockUploader()
const artifactDestroyer = new MockArtifactDestroyer()
const actionSkipper = new MockActionSkipper()

return new Action(inputs, outputs, releases, uploader, artifactDestroyer)
return new Action(inputs, outputs, releases, uploader, artifactDestroyer, actionSkipper)
}
})
44 changes: 44 additions & 0 deletions __tests__/ActionSkipper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {ActionSkipper, ReleaseActionSkipper} from "../src/ActionSkipper";
import {Releases} from "../src/Releases";

describe("shouldSkip", () => {
const getMock = jest.fn()
const tag = "tag"
const MockReleases = jest.fn<Releases, any>(() => {
return {
create: jest.fn(),
deleteArtifact: jest.fn(),
getByTag: getMock,
listArtifactsForRelease: jest.fn(),
listReleases: jest.fn(),
update: jest.fn(),
uploadArtifact: jest.fn()
}
})

it('should return false when skipIfReleaseExists is false', async () => {
const actionSkipper = new ReleaseActionSkipper(false, MockReleases(), tag)
expect(await actionSkipper.shouldSkip()).toBe(false)
})

it('should return false when error occurs', async () => {
getMock.mockRejectedValue(new Error())

const actionSkipper = new ReleaseActionSkipper(true, MockReleases(), tag)
expect(await actionSkipper.shouldSkip()).toBe(false)
})

it('should return false when release does not exist', async () => {
getMock.mockResolvedValue({})

const actionSkipper = new ReleaseActionSkipper(true, MockReleases(), tag)
expect(await actionSkipper.shouldSkip()).toBe(false)
})

it('should return true when release does exist', async () => {
getMock.mockResolvedValue({data: {}})

const actionSkipper = new ReleaseActionSkipper(true, MockReleases(), tag)
expect(await actionSkipper.shouldSkip()).toBe(true)
})
})
24 changes: 20 additions & 4 deletions __tests__/Inputs.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const mockGetInput = jest.fn();
const mockGetBooleanInput = jest.fn();
const mockGlob = jest.fn()
const mockReadFileSync = jest.fn();
const mockStatSync = jest.fn();
Expand All @@ -14,7 +15,10 @@ const artifacts = [
]

jest.mock('@actions/core', () => {
return {getInput: mockGetInput};
return {
getInput: mockGetInput,
getBooleanInput: mockGetBooleanInput
};
})

jest.mock('fs', () => {
Expand Down Expand Up @@ -87,7 +91,7 @@ describe('Inputs', () => {
expect(mockGlob).toBeCalledTimes(1)
expect(mockGlob).toBeCalledWith('art1', 'contentType', true)
})

it('returns empty artifacts', () => {
mockGetInput.mockReturnValueOnce('')
.mockReturnValueOnce('')
Expand Down Expand Up @@ -202,7 +206,7 @@ describe('Inputs', () => {
mockGetInput.mockReturnValue('Release')
expect(inputs.discussionCategory).toBe('Release')
})

it('returns undefined', () => {
mockGetInput.mockReturnValue('')
expect(inputs.discussionCategory).toBe(undefined)
Expand All @@ -220,7 +224,7 @@ describe('Inputs', () => {
expect(inputs.generateReleaseNotes).toBe(false)
});
})

describe('owner', () => {
it('returns owner from context', function () {
process.env.GITHUB_REPOSITORY = "owner/repo"
Expand Down Expand Up @@ -278,6 +282,18 @@ describe('Inputs', () => {
});
})

describe('skipIfReleaseExists', () => {
it('returns false', () => {
mockGetBooleanInput.mockReturnValue(false)
expect(inputs.skipIfReleaseExists).toBe(false)
})

it('returns true', () => {
mockGetBooleanInput.mockReturnValue(true)
expect(inputs.skipIfReleaseExists).toBe(true)
})
})

describe('tag', () => {
it('returns input tag', () => {
mockGetInput.mockReturnValue('tag')
Expand Down
7 changes: 5 additions & 2 deletions __tests__/Integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as path from "path";
import {FileArtifactGlobber} from "../src/ArtifactGlobber";
import {Outputs} from "../src/Outputs";
import {GithubArtifactDestroyer} from "../src/ArtifactDestroyer";
import {ReleaseActionSkipper} from "../src/ActionSkipper";

// 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
Expand All @@ -27,8 +28,9 @@ describe.skip('Integration Test', () => {
inputs.artifactErrorsFailBuild,
)
const artifactDestroyer = new GithubArtifactDestroyer(releases)
const actionSkipper = new ReleaseActionSkipper(inputs.skipIfReleaseExists, releases, inputs.tag)

action = new Action(inputs, outputs, releases, uploader, artifactDestroyer)
action = new Action(inputs, outputs, releases, uploader, artifactDestroyer, actionSkipper)
})

it('Performs action', async () => {
Expand All @@ -52,10 +54,11 @@ describe.skip('Integration Test', () => {
replacesArtifacts: true,
removeArtifacts: false,
repo: "actions-playground",
skipIfReleaseExists: false,
tag: "release-action-test",
token: getToken(),
updatedDraft: false,
updatedReleaseBody: "This release was generated by release-action's integration test",
updatedReleaseBody: "This release was updated by release-action's integration test",
updatedReleaseName: "Releases Action Integration Test",
updatedPrerelease: false,
updateOnlyUnreleased: false
Expand Down
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ inputs:
description: "Optionally specify the repo where the release should be generated. Defaults to current repo"
required: false
default: ''
skipIfReleaseExists:
description: "When skipIfReleaseExists is enabled the action will be skipped if a non-draft release already exists for the provided tag."
required: false
default: 'false'
tag:
description: 'An optional tag for the release. If this is omitted the git ref will be used (if it is a tag).'
required: false
Expand Down
82 changes: 80 additions & 2 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,29 @@ require('./sourcemap-register.js');/******/ (() => { // webpackBootstrap

"use strict";

var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (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;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
Expand All @@ -17,19 +40,25 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.Action = void 0;
const core = __importStar(__nccwpck_require__(2559));
const GithubError_1 = __nccwpck_require__(7433);
const ReleaseValidator_1 = __nccwpck_require__(7579);
class Action {
constructor(inputs, outputs, releases, uploader, artifactDestroyer) {
constructor(inputs, outputs, releases, uploader, artifactDestroyer, skipper) {
this.inputs = inputs;
this.outputs = outputs;
this.releases = releases;
this.uploader = uploader;
this.artifactDestroyer = artifactDestroyer;
this.skipper = skipper;
this.releaseValidator = new ReleaseValidator_1.ReleaseValidator(inputs.updateOnlyUnreleased);
}
perform() {
return __awaiter(this, void 0, void 0, function* () {
if (yield this.skipper.shouldSkip()) {
core.notice("Skipping action, release already exists and skipIfReleaseExists is enabled.");
return;
}
const releaseResponse = yield this.createOrUpdateRelease();
const releaseData = releaseResponse.data;
const releaseId = releaseData.id;
Expand Down Expand Up @@ -111,6 +140,50 @@ class Action {
exports.Action = Action;


/***/ }),

/***/ 2746:
/***/ (function(__unused_webpack_module, exports) {

"use strict";

var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.ReleaseActionSkipper = void 0;
class ReleaseActionSkipper {
constructor(skipIfReleaseExists, releases, tag) {
this.skipIfReleaseExists = skipIfReleaseExists;
this.releases = releases;
this.tag = tag;
}
shouldSkip() {
return __awaiter(this, void 0, void 0, function* () {
if (!this.skipIfReleaseExists) {
// Bail if skip flag isn't set.
return false;
}
try {
const getResponse = yield this.releases.getByTag(this.tag);
return getResponse.data != null;
}
catch (error) {
// There is either no release or something else went wrong. Either way, run the action.
return false;
}
});
}
}
exports.ReleaseActionSkipper = ReleaseActionSkipper;


/***/ }),

/***/ 8568:
Expand Down Expand Up @@ -728,6 +801,9 @@ class CoreInputs {
}
return this.context.repo.repo;
}
get skipIfReleaseExists() {
return core.getBooleanInput("skipIfReleaseExists");
}
get tag() {
const tag = core.getInput('tag');
if (tag) {
Expand Down Expand Up @@ -1037,6 +1113,7 @@ const ArtifactGlobber_1 = __nccwpck_require__(8924);
const GithubError_1 = __nccwpck_require__(7433);
const Outputs_1 = __nccwpck_require__(6650);
const ArtifactDestroyer_1 = __nccwpck_require__(1770);
const ActionSkipper_1 = __nccwpck_require__(2746);
function run() {
return __awaiter(this, void 0, void 0, function* () {
try {
Expand All @@ -1057,9 +1134,10 @@ function createAction() {
const inputs = new Inputs_1.CoreInputs(globber, context);
const outputs = new Outputs_1.CoreOutputs();
const releases = new Releases_1.GithubReleases(inputs, git);
const skipper = new ActionSkipper_1.ReleaseActionSkipper(inputs.skipIfReleaseExists, releases, inputs.tag);
const uploader = new ArtifactUploader_1.GithubArtifactUploader(releases, inputs.replacesArtifacts, inputs.artifactErrorsFailBuild);
const artifactDestroyer = new ArtifactDestroyer_1.GithubArtifactDestroyer(releases);
return new Action_1.Action(inputs, outputs, releases, uploader, artifactDestroyer);
return new Action_1.Action(inputs, outputs, releases, uploader, artifactDestroyer, skipper);
}
run();

Expand Down
2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

0 comments on commit 3bacd9e

Please sign in to comment.