This is an automated email from the ASF dual-hosted git repository. potiuk pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/airflow-checks-action.git
commit d3d705842bf1b5a37ab2362b9cedf5959cdd5db9 Author: Louis Brunner <[email protected]> AuthorDate: Sat Feb 29 21:09:12 2020 +0000 First draft --- .eslintrc.json | 1 + .github/workflows/{test.yml => build.yml} | 12 +- .github/workflows/examples.yml | 252 ++++++++++++++++++++++++++++++ .prettierrc.json | 4 +- __tests__/main.test.ts | 16 +- action.yml | 23 +++ dist/index.js | 2 +- jest.config.js | 2 +- package.json | 4 +- src/checks.ts | 69 ++++++++ src/inputs.ts | 51 ++++++ src/main.ts | 60 +++---- src/namespaces/Inputs.ts | 38 +++++ 13 files changed, 479 insertions(+), 55 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index ef9fcf4..528d8da 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -14,6 +14,7 @@ "project": "./tsconfig.eslint.json" }, "rules": { + "@typescript-eslint/camelcase": ["off"] }, "env": { "node": true, diff --git a/.github/workflows/test.yml b/.github/workflows/build.yml similarity index 53% rename from .github/workflows/test.yml rename to .github/workflows/build.yml index c058dd1..ce4ac4d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/build.yml @@ -4,6 +4,7 @@ on: # rebuild any PRs and main branch changes push: branches: - master + - 'feature/*' - 'releases/*' jobs: @@ -15,14 +16,3 @@ jobs: - run: | npm install npm run all - - # make sure the action works on a clean machines without building - test1: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - uses: ./ - with: - token: ${{ secrets.GITHUB_TOKEN }} - - # TODO: add more (race conditions, failures, etc) diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml new file mode 100644 index 0000000..1f82161 --- /dev/null +++ b/.github/workflows/examples.yml @@ -0,0 +1,252 @@ +name: "examples" +on: [push] + +jobs: + # make sure the action works on a clean machines without building + + ## Basic + test_basic_success: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: ./ + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: Test Basic Success + conclusion: success + + test_basic_success_with_output: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: ./ + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: Test Basic Success (Implicit) + conclusion: success + output: + summary: Test was a success + text_description: | + This is a text description of the annotations and images + With more stuff + And more + + test_basic_failure: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: ./ + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: Test Basic Failure + conclusion: failure + + # Other codes + test_basic_neutral: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: ./ + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: Test 1 = Neutral + conclusion: neutral + + test_basic_cancelled: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: ./ + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: Test Basic Cancelled + conclusion: cancelled + + test_basic_timed_out: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: ./ + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: Test Basic Timed Out + conclusion: timed_out + + test_basic_action_required: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: ./ + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: Test Basic Action Required + conclusion: action_required + + ## Based on command + test_with_annotation: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: ./ + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: Test Advanced With Output + conclusion: success + + ## With annotations + test_with_annotations: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: ./ + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: Test With Annotations + conclusion: success + output: # output.summary is required with annotations! + summary: Some warnings in README.md + annotations: + - path: README.md + annotation_level: warning + title: Spell Checker + message: Check your spelling for 'banaas'. + raw_details: Do you mean 'bananas' or 'banana'? + start_line: 1 + end_line: 2 + + test_with_annotations_from_run: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - id: annotations + run: | + echo ::set-output name=value::{"path":"README.md","start_line":1,"end_line":2,"message":"Check your spelling for 'banaas'.","annotation_level":"warning"} + - uses: ./ + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: Test With Annotations From Run + conclusion: success + output: # output.summary is required with annotations! + summary: Some warnings in README.md + annotations: ${{ steps.annotations.outputs.value }} + + ## With images + test_with_images: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: ./ + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: Test With Images + conclusion: success + output: # output.summary is required with images! + summary: Some cool pics + images: + - alt: Cool pic + image_url: https://via.placeholder.com/150 + caption: Cool description + + test_with_images_from_run: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - id: images + run: | + echo ::set-output name=value::{"alt":"Cool pic","image_url":"https://via.placeholder.com/150","caption":"Cool description"} + - uses: ./ + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: Test With Images From Run + conclusion: success + output: # output.summary is required with images! + summary: Some warnings in README.md + images: ${{ steps.images.outputs.value }} + + ## With actions + # TODO: HOW DO WE SET THE WEBHOOK! + test_with_actions: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: ./ + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: Test With Actions + conclusion: success + actions: + - label: Click Me + description: Click me to get free RAM + identifier: sent_to_webhook + + test_with_actions_from_run: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - id: actions + run: | + echo ::set-output name=value::{"label":"Click Me","description":"Click me to get free RAM","identifier":"sent_to_webhook"} + - uses: ./ + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: Test With Actions From Run + conclusion: success + # output.summary is required with actions! + output: + summary: Some warnings in README.md + actions: ${{ steps.actions.outputs.value }} + + ## With init + test_with_init: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: ./ + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: Test With Init + status: in_progress + - run: sleep 30 + - uses: ./ + with: + token: ${{ secrets.GITHUB_TOKEN }} + status: completed + conclusion: failure + + test_with_init_implicit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: ./ + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: Test With Init (Implicit) + status: in_progress + - run: sleep 30 + - uses: ./ + with: + token: ${{ secrets.GITHUB_TOKEN }} + conclusion: failure + + ## Based on job + test_based_job_success: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: ./ + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: Test Based On Job (Success) + conclusion: ${{ job }} + + test_based_job_failure: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - run: false + - uses: ./ + if: always() + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: Test Based On Job (Failure) + conclusion: ${{ job }} diff --git a/.prettierrc.json b/.prettierrc.json index 9d64c21..bf0887a 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,8 +1,8 @@ { - "printWidth": 80, + "printWidth": 120, "tabWidth": 2, "useTabs": false, - "semi": false, + "semi": true, "singleQuote": true, "trailingComma": "all", "bracketSpacing": false, diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index 32cf034..bced80d 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -1,15 +1,15 @@ -import * as process from 'process' -import * as cp from 'child_process' -import * as path from 'path' +import * as process from 'process'; +import * as cp from 'child_process'; +import * as path from 'path'; // shows how the runner will run a javascript action with env / stdout protocol test('test runs', () => { - process.env['TOKEN'] = 'ABC' - const ip = path.join(__dirname, '..', 'lib', 'main.js') + process.env['TOKEN'] = 'ABC'; + const ip = path.join(__dirname, '..', 'lib', 'main.js'); const options: cp.ExecSyncOptions = { env: process.env, - } - console.log(cp.execSync(`node ${ip}`, options).toString()) -}) + }; + console.log(cp.execSync(`node ${ip}`, options).toString()); +}); // TODO: add more diff --git a/action.yml b/action.yml index e7a778d..125221d 100644 --- a/action.yml +++ b/action.yml @@ -4,6 +4,29 @@ author: 'Louis Brunner' inputs: token: description: 'your GITHUB_TOKEN' + required: true + name: + description: 'the name of your check' + required: true + conclusion: + description: 'the conclusion of your check' + required: true + status: + description: 'the status of your check' + required: false + default: completed + output: + description: 'the output of your check' + required: false + annotations: + description: 'the annotations of your check' + required: false + images: + description: 'the images of your check' + required: false + actions: + description: 'the actions of your check' + required: false runs: using: 'node12' main: 'dist/index.js' diff --git a/dist/index.js b/dist/index.js index 24aa44e..37237e3 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1 +1 @@ -module.exports=function(e,t){"use strict";var r={};function __webpack_require__(t){if(r[t]){return r[t].exports}var n=r[t]={i:t,l:false,exports:{}};e[t].call(n.exports,n,n.exports,__webpack_require__);n.l=true;return n.exports}__webpack_require__.ab=__dirname+"/";function startup(){return __webpack_require__(429)}return startup()}({87:function(e){e.exports=require("os")},429:function(e,t,r){e.exports=function(e,t){"use strict";var r={};function __webpack_require__(t){if(r[t]){return r[t] [...] \ No newline at end of file +module.exports=function(e,t){"use strict";var r={};function __webpack_require__(t){if(r[t]){return r[t].exports}var n=r[t]={i:t,l:false,exports:{}};e[t].call(n.exports,n,n.exports,__webpack_require__);n.l=true;return n.exports}__webpack_require__.ab=__dirname+"/";function startup(){return __webpack_require__(429)}return startup()}({87:function(e){e.exports=require("os")},429:function(e,t,r){e.exports=function(e,t){"use strict";var r={};function __webpack_require__(t){if(r[t]){return r[t] [...] \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index 9bac3cc..f3a27fa 100644 --- a/jest.config.js +++ b/jest.config.js @@ -8,4 +8,4 @@ module.exports = { '^.+\\.ts$': 'ts-jest', }, verbose: true, -} +}; diff --git a/package.json b/package.json index dfd516b..381c53c 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,10 @@ "main": "dist/index.js", "scripts": { "build": "tsc", - "lint": "eslint *.js **/*.ts", + "lint": "eslint '**/*.js' '**/*.ts'", "pack": "ncc build -m", "test": "jest", - "format": "prettier --write *.js **/*.ts", + "format": "prettier --write '**/*.js' '**/*.ts'", "all": "npm run build && npm run lint && npm run pack && npm test" }, "repository": { diff --git a/src/checks.ts b/src/checks.ts new file mode 100644 index 0000000..8271217 --- /dev/null +++ b/src/checks.ts @@ -0,0 +1,69 @@ +import {GitHub} from '@actions/github'; +import * as Inputs from './namespaces/Inputs'; + +type Ownership = { + owner: string; + repo: string; +}; + +type CreateOptions = { + completed: boolean; +}; + +const unpackInputs = (inputs: Inputs.Args): object => { + let output; + if (inputs.output) { + output = { + summary: inputs.output.summary, + text: inputs.output.text_description, + actions: inputs.actions, + images: inputs.images, + }; + } + return { + status: inputs.status.toString(), + conclusion: inputs.conclusion.toString(), + output, + actions: inputs.actions, + }; +}; + +const formatDate = (): string => { + return new Date().toISOString(); +}; + +export const createRun = async ( + octokit: GitHub, + sha: string, + ownership: Ownership, + inputs: Inputs.Args, + options?: CreateOptions, +): Promise<number> => { + const dates: {completed_at?: string} = {}; + if (!options || options.completed) { + dates.completed_at = formatDate(); + } + const {data} = await octokit.checks.create({ + ...ownership, + head_sha: sha, + name: inputs.name, + started_at: formatDate(), + ...dates, + ...unpackInputs(inputs), + }); + return data.id; +}; + +export const updateRun = async ( + octokit: GitHub, + id: number, + ownership: Ownership, + inputs: Inputs.Args, +): Promise<void> => { + await octokit.checks.update({ + ...ownership, + check_run_id: id, + completed_at: formatDate(), + ...unpackInputs(inputs), + }); +}; diff --git a/src/inputs.ts b/src/inputs.ts new file mode 100644 index 0000000..6b5ed0d --- /dev/null +++ b/src/inputs.ts @@ -0,0 +1,51 @@ +import {InputOptions} from '@actions/core'; +import * as Inputs from './namespaces/Inputs'; + +type GetInput = (name: string, options?: InputOptions | undefined) => string; + +const parseJSON = <T>(getInput: GetInput, property: string): T | undefined => { + const value = getInput(property); + if (!value) { + return; + } + try { + const obj = JSON.parse(value); + return obj as T; + } catch (e) { + throw new Error(`invalid format for '${property}: ${e.toString()}`); + } +}; + +export const parseInputs = (getInput: GetInput): Inputs.Args => { + const token = getInput('token', {required: true}); + const name = getInput('name', {required: true}); + const statusStr = getInput('status', {required: true}); + const conclusionStr = getInput('conclusion', {required: true}); + + if (!(statusStr in Inputs.Status)) { + throw new Error(`invalid value for 'status': '${statusStr}'`); + } + const status = statusStr as Inputs.Status; + + if (!(conclusionStr in Inputs.Conclusion)) { + throw new Error(`invalid value for 'conclusion': '${conclusionStr}'`); + } + const conclusion = conclusionStr as Inputs.Conclusion; + + const output = parseJSON<Inputs.Output>(getInput, 'output'); + const annotations = parseJSON<Inputs.Annotations>(getInput, 'annotations'); + const images = parseJSON<Inputs.Images>(getInput, 'images'); + const actions = parseJSON<Inputs.Actions>(getInput, 'actions'); + + return { + name, + token, + status, + conclusion, + + output, + annotations, + images, + actions, + }; +}; diff --git a/src/main.ts b/src/main.ts index 0208cac..2a5ff19 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,44 +1,44 @@ -import * as core from '@actions/core' -import * as github from '@actions/github' +import * as core from '@actions/core'; +import * as github from '@actions/github'; +import {parseInputs} from './inputs'; +import {createRun, updateRun} from './checks'; + +const stateID = 'checkID'; -// eslint-disable-next-line @typescript-eslint/require-await async function run(): Promise<void> { try { - const token: string = core.getInput('token') + core.debug(`Parsing inputs`); + const inputs = parseInputs(core.getInput); - core.debug(`Setting up OctoKit`) - const octokit = new github.GitHub(token) + core.debug(`Setting up OctoKit`); + const octokit = new github.GitHub(inputs.token); const ownership = { owner: github.context.repo.owner, repo: github.context.repo.repo, - } - - const info = { // TODO: from argument - } - - const { data } = await octokit.checks.listForRef({ - ...ownership, - ref: github.context.sha, - }) + }; + const sha = github.context.sha; - if (data.check_runs.length > 0) { - octokit.checks.update({ - ...ownership, - check_run_id: data.check_runs[0].id, - ...info, - }) - } else { - octokit.checks.create({ - ...ownership, - head_sha: github.context.sha, - name: 'Check Run Test', // TODO: from argument - ...info, - }) + switch (inputs.status) { + case 'in_progress': + case 'queued': { + const id = await createRun(octokit, sha, ownership, inputs, {completed: false}); + core.saveState(stateID, id.toString()); + break; + } + case 'completed': { + const id = core.getState(stateID); + if (id) { + updateRun(octokit, parseInt(id), ownership, inputs); + } else { + createRun(octokit, sha, ownership, inputs); + } + break; + } } } catch (error) { - core.setFailed(error.message) + core.setFailed(error.message); } } -run() +run(); diff --git a/src/namespaces/Inputs.ts b/src/namespaces/Inputs.ts new file mode 100644 index 0000000..9bfcba3 --- /dev/null +++ b/src/namespaces/Inputs.ts @@ -0,0 +1,38 @@ +import {Octokit} from '@octokit/rest'; + +export type Args = { + name: string; + token: string; + conclusion: Conclusion; + status: Status; + output?: Output; + annotations?: Annotations; + images?: Images; + actions?: Actions; +}; + +export type Annotations = Octokit.ChecksCreateParamsOutputAnnotations[]; + +export type Images = Octokit.ChecksCreateParamsOutputImages[]; + +export type Actions = Octokit.ChecksCreateParamsActions[]; + +export type Output = { + summary: string; + text_description?: string; +}; + +export enum Conclusion { + Success = 'success', + Failure = 'failure', + Neutral = 'neutral', + Cancelled = 'cancelled', + TimedOut = 'timed_out', + ActionRequired = 'action_required', +} + +export enum Status { + Queued = 'queued', + InProgress = 'in_progress', + Completed = 'completed', +}
