diff --git a/.github/workflows/smoke-screen-tests.yml b/.github/workflows/smoke-screen-tests.yml new file mode 100644 index 0000000..26769e3 --- /dev/null +++ b/.github/workflows/smoke-screen-tests.yml @@ -0,0 +1,14 @@ +name: "smoke-screen-tests" +on: + push: + branches-ignore: + - master + +jobs: + sst: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: | + npm install + npm test \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 58b69a9..26b7270 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,10 +10,34 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Run the action + - name: Test min args uses: ./ with: authToken: ${{secrets.GITHUB_TOKEN}} - context: 'Test run' - description: 'Passed' - state: 'success' \ No newline at end of file + state: 'success' + - name: Test with all args except owner/repo (success) + uses: ./ + with: + authToken: ${{secrets.GITHUB_TOKEN}} + context: "Test run" + description: "Test with all args" + target_url: "https://github.com/Sibz/github-status-action" + sha: ${{github.event.pull_request.head.sha || github.sha}} + state: 'success' + - name: Test failing action + uses: ./ + with: + authToken: ${{secrets.GITHUB_TOKEN}} + context: "Test run failed" + description: "Failed test" + sha: ${{github.event.pull_request.head.sha || github.sha}} + state: 'failure' + - name: Test failing action now succeeded + uses: ./ + with: + authToken: ${{secrets.GITHUB_TOKEN}} + context: "Test run failed" + description: "Failed test now succeeded" + sha: ${{github.event.pull_request.head.sha || github.sha}} + state: 'success' + \ No newline at end of file diff --git a/README.md b/README.md index fd6ea92..8eff13c 100644 --- a/README.md +++ b/README.md @@ -4,36 +4,32 @@ github-status-action status

-# Github Status Action +# GitHub Status Action Adds a status update to a commit. GitHub will always show the latest state of a context. ## Usage ### Inputs -```yml - authToken: - description: 'Use secrets.GITHUB_TOKEN or your own token if you need to trigger other workflows the use "on: status"' - required: true - context: - description: 'The context, this is displayed as the name of the check' - required: true - description: - description: 'Short text explaining the status of the check' - required: true - state: - description: 'The status of the check: success, error, failure or pending' - required: true - owner: - description: 'Repostory onwer, defaults to context github.repository_owner if ommited' - default: ${{ github.repository_owner }} - repository: - description: 'Repository, default to context github.repository if ommited' - default: ${{ github.repository }} - sha: - description: 'SHA of commit to update status on, defaults to context github.sha' - default: ${{ github.sha }} - ``` + + * `authToken` (required) + Use secrets.GITHUB_TOKEN or your own token if you need to trigger other workflows that use "on: status"' + * `state` (required) + The status of the check should only be `success`, `error`, `failure` or `pending` + * `context` + The context, this is displayed as the name of the check + * `description` + Short text explaining the status of the check + * `owner` + Repostory onwer, defaults to context github.repository_owner if omited + * `repository` + Repository, default to context github.repository if omited + * `sha` + SHA of commit to update status on, defaults to context github.sha + *If using `on: pull_request` use `github.event.pull_request.head.sha`* + * `target_url` + Url to use for the details link. If omited no link is shown. + ### Outputs None. @@ -58,4 +54,5 @@ on: # run on any PRs and main branch changes context: 'Test run' description: 'Passed' state: 'success' + sha: ${{github.event.pull_request.head.sha || github.sha}} ``` diff --git a/__tests__/makeStatusRequestTests.ts b/__tests__/makeStatusRequestTests.ts new file mode 100644 index 0000000..9c02489 --- /dev/null +++ b/__tests__/makeStatusRequestTests.ts @@ -0,0 +1,106 @@ +import test from 'ava'; +import makeStatusRequest, { CoreActionsForTesting, CommitState, ERR_INVALID_OWNER, ERR_INVALID_STATE} from '../src/makeStatusRequest' +import inputNames from '../src/inputNames' + +const INPUT_CONTEXT = "test context"; +const INPUT_OWNER = "TestOwner"; +const INPUT_OWNER_INVALID = "-TestOwner"; +const INPUT_REPOSITORY = "Test.Repository-1"; +const INPUT_REPOSITORY_WITHOWNER = INPUT_OWNER + "/Test.Repository-1"; +const INPUT_STATE: CommitState = "success"; +const INPUT_STATE_INVALID: CommitState = "failed" as CommitState; +const INPUT_DESC = "Test Description"; +const INPUT_SHA = "TestSHA"; +const INPUT_TARGETURL = "test/uri"; + +const actionsCore: CoreActionsForTesting = { + getInput: (arg:string) => { + switch(arg) { + case inputNames.context: + return INPUT_CONTEXT; + case inputNames.owner: + return INPUT_OWNER; + case inputNames.repo: + return INPUT_REPOSITORY; + case inputNames.state: + return INPUT_STATE; + case inputNames.desc: + return INPUT_DESC; + case inputNames.sha: + return INPUT_SHA; + case inputNames.target_url: + return INPUT_TARGETURL; + default: + return "input not in test mock"; + } + } +} +const actionsCoreAlt1: CoreActionsForTesting = { + getInput: (arg:string) => { + switch(arg) { + case inputNames.repo: + return INPUT_REPOSITORY_WITHOWNER; + default: + return actionsCore.getInput(arg); + } + } +} + +const actionsCoreAlt2: CoreActionsForTesting = { + getInput: (arg:string) => { + switch(arg) { + case inputNames.owner: + return INPUT_OWNER_INVALID; + default: + return actionsCore.getInput(arg); + } + } +} + +const actionsCoreAlt3: CoreActionsForTesting = { + getInput: (arg:string) => { + switch(arg) { + case inputNames.state: + return INPUT_STATE_INVALID; + default: + return actionsCore.getInput(arg); + } + } +} + + +test("should getInput context", t=> { + t.is(makeStatusRequest(actionsCore).context, INPUT_CONTEXT); +}); +test("should getInput owner", t=> { + t.is(makeStatusRequest(actionsCore).owner, INPUT_OWNER); +}); +test("should getInput repo", t=> { + t.is(makeStatusRequest(actionsCore).repo, INPUT_REPOSITORY); +}); +test("should getInput state", t=> { + t.is(makeStatusRequest(actionsCore).state, INPUT_STATE); +}); +test("should getInput description", t=> { + t.is(makeStatusRequest(actionsCore).description, INPUT_DESC); +}); +test("should getInput sha", t=> { + t.is(makeStatusRequest(actionsCore).sha, INPUT_SHA); +}); +test("should getInput target_url", t=> { + t.is(makeStatusRequest(actionsCore).target_url, INPUT_TARGETURL); +}); + +test("should getInput repo and remove leading owner name", t=> { + t.is(makeStatusRequest(actionsCoreAlt1).repo, INPUT_REPOSITORY); +}); + +test("when owner is not a valid GitHub username, should throw", t=> { + let err = t.throws(()=> makeStatusRequest(actionsCoreAlt2)); + t.is(err.message, ERR_INVALID_OWNER) +}); + +test("should validate state", t=> { + let err = t.throws(()=> makeStatusRequest(actionsCoreAlt3)); + t.is(err.message, ERR_INVALID_STATE) +}); \ No newline at end of file diff --git a/action.yml b/action.yml index e470755..c5654fd 100644 --- a/action.yml +++ b/action.yml @@ -1,5 +1,5 @@ name: 'github-status-action' -description: 'Adds a check status on a commit, github reports the last status added for a particular context.' +description: 'Adds a check status on a commit, GitHub reports the last status added for a particular context.' author: 'Sibz@EntityZero' branding: icon: 'check' @@ -7,25 +7,33 @@ branding: inputs: authToken: description: 'Use secrets.GITHUB_TOKEN or your own token if you need to trigger other workflows the use "on: status"' + required: true + state: + description: 'The status of the check: success, error, failure or pending' required: true context: description: 'The context, this is displayed as the name of the check' - required: true + default: 'default' + required: false description: description: 'Short text explaining the status of the check' - required: true - state: - description: 'The status of the check: success, error, failure or pending' - required: true + default: '' + required: false owner: - description: 'Repostory onwer, defaults to context github.repository_owner if ommited' + description: 'Repostory onwer, defaults to context github.repository_owner if omited' default: ${{ github.repository_owner }} + required: false repository: - description: 'Repository, default to context github.repository if ommited' + description: 'Repository, default to context github.repository if omited' default: ${{ github.repository }} + required: false sha: description: 'SHA of commit to update status on, defaults to context github.sha' default: ${{ github.sha }} + required: false + target_url: + description: 'URL/URI to use for further details.' + required: false runs: using: 'node12' main: 'dist/index.js' diff --git a/dist/index.js b/dist/index.js index 1d56932..a01f55c 100644 --- a/dist/index.js +++ b/dist/index.js @@ -604,35 +604,18 @@ var __importStar = (this && this.__importStar) || function (mod) { result["default"] = mod; return result; }; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); const core = __importStar(__webpack_require__(470)); const rest_1 = __webpack_require__(889); +const makeStatusRequest_1 = __importDefault(__webpack_require__(677)); function run() { return __awaiter(this, void 0, void 0, function* () { - let authToken = ''; - let context = ''; - let description = ''; - let state = ''; - let owner = ''; - let repository = ''; - let sha = ''; + const authToken = core.getInput('authToken'); let octokit = null; try { - authToken = core.getInput('authToken'); - context = core.getInput('context'); - description = core.getInput('description'); - state = core.getInput('state'); - owner = core.getInput('owner'); - repository = core.getInput('repository'); - sha = core.getInput('sha'); - } - catch (error) { - core.setFailed("Error getting inputs:\n" + error.message); - } - try { - if (repository.startsWith(`${owner}/`)) { - repository = repository.replace(`${owner}/`, ''); - } octokit = new rest_1.Octokit({ auth: authToken, userAgent: "github-status-action", @@ -651,24 +634,26 @@ function run() { }); } catch (error) { - core.setFailed("Error creating ovtokit:\n" + error.message); + core.setFailed("Error creating octokit:\n" + error.message); + return; } if (octokit == null) { + core.setFailed("Error creating octokit:\noctokit was null"); return; } + let statusRequest; try { - console.log(repository); - yield octokit.repos.createStatus({ - owner: owner, - repo: repository, - context: context, - sha: sha, - state: state, - description: description - }); + statusRequest = makeStatusRequest_1.default(); } catch (error) { - core.setFailed(`Error setting status:\n${error.message}\nDetails:\nowner:${owner}\nrepo:${repository}\nsha:${sha}`); + core.setFailed(`Error creating status request object: ${error.message}`); + return; + } + try { + yield octokit.repos.createStatus(statusRequest); + } + catch (error) { + core.setFailed(`Error setting status:\n${error.message}\nRequest object:\n${JSON.stringify(statusRequest, null, 2)}`); } }); } @@ -5734,6 +5719,81 @@ if (process.platform === 'linux') { module.exports = require("util"); +/***/ }), + +/***/ 677: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const actionsCore = __importStar(__webpack_require__(470)); +const inputNames_1 = __importDefault(__webpack_require__(686)); +exports.ERR_INVALID_OWNER = "Input 'owner' must be a valid GitHub username"; +exports.ERR_INVALID_STATE = "Input 'state' must be one of success | error | failure | pending"; +const regExUsername = /^\w+-?\w+(?!-)$/; +function makeStatusRequest(testCore = null) { + var _a; + let core = (_a = testCore, (_a !== null && _a !== void 0 ? _a : actionsCore)); + let request = {}; + request.context = core.getInput(inputNames_1.default.context); + request.description = core.getInput(inputNames_1.default.desc); + request.state = core.getInput(inputNames_1.default.state); + request.owner = core.getInput(inputNames_1.default.owner); + request.repo = core.getInput(inputNames_1.default.repo); + request.sha = core.getInput(inputNames_1.default.sha); + request.target_url = core.getInput(inputNames_1.default.target_url); + if (!regExUsername.test(request.owner)) { + throw new Error(exports.ERR_INVALID_OWNER); + } + if (!validateState(request.state)) { + throw new Error(exports.ERR_INVALID_STATE); + } + if (request.repo.startsWith(`${request.owner}/`)) { + request.repo = request.repo.replace(`${request.owner}/`, ''); + } + return request; +} +exports.default = makeStatusRequest; +function validateState(state) { + return (state == "success" + || state == "error" + || state == "failure" + || state == "pending"); +} + + +/***/ }), + +/***/ 686: +/***/ (function(__unusedmodule, exports) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.INPUT_NAMES = { + authToken: "authToken", + owner: "owner", + repo: "repository", + state: "state", + context: "context", + sha: "sha", + desc: "description", + target_url: "target_url" +}; +exports.default = exports.INPUT_NAMES; + + /***/ }), /***/ 692: diff --git a/package-lock.json b/package-lock.json index 606628a..d6c87e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { - "name": "typescript-action", - "version": "0.0.0", + "name": "github-status-action", + "version": "1.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index b2b2347..d088eb5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "github-status-action", - "version": "1.0.0-alpha.1", + "version": "1.1.0", "private": true, "description": "Github status action", "main": "lib/main.js", @@ -9,7 +9,7 @@ "test": "ava", "pack": "ncc build", "all": "npm run build && npm test", - "push": "npm run build && npm run pack && git add * && git commit -m \"Updated built file\" && git push" + "push": "npm run build && npm run pack && git add * && git commit -m \"ci: update built file\" && git push" }, "repository": { "type": "git", diff --git a/src/inputNames.ts b/src/inputNames.ts new file mode 100644 index 0000000..88f57f0 --- /dev/null +++ b/src/inputNames.ts @@ -0,0 +1,13 @@ + +export const INPUT_NAMES = { + authToken: "authToken", + owner: "owner", + repo: "repository", + state: "state", + context: "context", + sha: "sha", + desc: "description", + target_url: "target_url" +} + +export default INPUT_NAMES; \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 3883dbd..1f4a381 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,37 +1,14 @@ import * as core from '@actions/core' import { Octokit } from '@octokit/rest' +import makeStatus, { StatusRequest } from './makeStatusRequest' +import makeStatusRequest from './makeStatusRequest'; +import { RequestParameters } from '@octokit/types'; async function run(): Promise { - let authToken: string = ''; - let context: string = ''; - let description: string = ''; - let state: any = ''; - let owner: string = ''; - let repository: string = ''; - let sha: string = ''; - + const authToken: string = core.getInput('authToken'); let octokit: Octokit | null = null; try { - authToken = core.getInput('authToken'); - context = core.getInput('context'); - description = core.getInput('description'); - state = core.getInput('state'); - owner = core.getInput('owner'); - repository = core.getInput('repository'); - sha = core.getInput('sha'); - - } catch (error) { - core.setFailed("Error getting inputs:\n" + error.message); - } - - try { - - - if (repository.startsWith(`${owner}/`)) { - repository = repository.replace(`${owner}/`, ''); - } - octokit = new Octokit({ auth: authToken, userAgent: "github-status-action", @@ -49,23 +26,28 @@ async function run(): Promise { } }); } catch (error) { - core.setFailed("Error creating ovtokit:\n" + error.message); + core.setFailed("Error creating octokit:\n" + error.message); + return; } + if (octokit == null) { + core.setFailed("Error creating octokit:\noctokit was null"); return; } + + let statusRequest: StatusRequest try { - console.log(repository); - await octokit.repos.createStatus({ - owner: owner, - repo: repository, - context: context, - sha: sha, - state: state, - description: description - }); + statusRequest = makeStatusRequest(); + } + catch (error) { + core.setFailed(`Error creating status request object: ${error.message}`); + return; + } + + try { + await octokit.repos.createStatus(statusRequest); } catch (error) { - core.setFailed(`Error setting status:\n${error.message}\nDetails:\nowner:${owner}\nrepo:${repository}\nsha:${sha}`); + core.setFailed(`Error setting status:\n${error.message}\nRequest object:\n${JSON.stringify(statusRequest, null, 2)}`); } } diff --git a/src/makeStatusRequest.ts b/src/makeStatusRequest.ts new file mode 100644 index 0000000..e032034 --- /dev/null +++ b/src/makeStatusRequest.ts @@ -0,0 +1,49 @@ +import * as actionsCore from '@actions/core' +import inputNames from './inputNames'; +import { RequestParameters } from '@octokit/types'; + +export type CommitState = "success" | "error" | "failure" | "pending"; +export type StatusRequest = RequestParameters & Pick; +export const ERR_INVALID_OWNER = "Input 'owner' must be a valid GitHub username"; +export const ERR_INVALID_STATE = "Input 'state' must be one of success | error | failure | pending"; + +const regExUsername = /^\w+-?\w+(?!-)$/; + +export default function makeStatusRequest(testCore: any | null = null): StatusRequest { + let core: CoreActionsForTesting = + testCore as CoreActionsForTesting ?? actionsCore as CoreActionsForTesting; + + let request: StatusRequest = {} as StatusRequest; + + request.context = core.getInput(inputNames.context); + request.description = core.getInput(inputNames.desc); + request.state = core.getInput(inputNames.state) as CommitState; + request.owner = core.getInput(inputNames.owner); + request.repo = core.getInput(inputNames.repo); + request.sha = core.getInput(inputNames.sha); + request.target_url = core.getInput(inputNames.target_url); + + if (!regExUsername.test(request.owner)) { + throw new Error(ERR_INVALID_OWNER); + } + + if (!validateState(request.state)) { + throw new Error(ERR_INVALID_STATE); + } + + if (request.repo.startsWith(`${request.owner}/`)) { + request.repo = request.repo.replace(`${request.owner}/`, ''); + } + + return request; +} + +function validateState(state: any): boolean { + return (state == "success" + || state == "error" + || state == "failure" + || state == "pending"); +} +export interface CoreActionsForTesting { + getInput: (arg: string) => string +} \ No newline at end of file