Skip to content
Permalink
Browse files
Rework create/update workflow, unify Checks API arguments
  • Loading branch information
LouisBrunner committed Sep 8, 2020
1 parent e2a613e commit 88ce8c595fcf9e7e3829dca9b6b59a972a98133e
Showing 9 changed files with 129 additions and 78 deletions.
@@ -207,6 +207,7 @@ jobs:
steps:
- uses: actions/checkout@v1
- uses: ./
id: init
with:
token: ${{ secrets.GITHUB_TOKEN }}
name: Test With Init
@@ -215,15 +216,18 @@ jobs:
- uses: ./
with:
token: ${{ secrets.GITHUB_TOKEN }}
name: Will not be used
check_id: ${{ steps.init.outputs.check_id }}
status: completed
output: |
{"summary":"Some warnings in README.md"}
conclusion: failure

test_with_init_implicit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: ./
id: init
with:
token: ${{ secrets.GITHUB_TOKEN }}
name: Test With Init (Implicit)
@@ -232,8 +236,8 @@ jobs:
- uses: ./
with:
token: ${{ secrets.GITHUB_TOKEN }}
name: Will not be used
conclusion: failure
check_id: ${{ steps.init.outputs.check_id }}
conclusion: success

## Based on job
test_based_job_success:
@@ -22,10 +22,9 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}
name: Test XYZ
conclusion: ${{ job }}
output:
summary: ${{ steps.test.outputs.summary }}
text_description: ${{ steps.test.outputs.description }}
conclusion: ${{ job.status }}
output: |
{"summary":${{ steps.test.outputs.summary }}}
```

See the [examples workflow](.github/workflows/examples.yml) for more details and examples (and see the [associated runs](https://github.com/LouisBrunner/checks-action/actions?query=workflow%3Aexamples) to see how it will look like).
@@ -38,7 +37,11 @@ See the [examples workflow](.github/workflows/examples.yml) for more details and

### `name`

**Required** The name of your check
**Required** for creation, the name of the check to create (mutually exclusive with `check_id`)

### `check_id`

**Required** for update, ID of the check to update (mutually exclusive with `name`)

### `conclusion`

@@ -93,9 +96,14 @@ Supports the same properties with the same types and names as the [Check Runs AP

Note that this will override `details_url` as it relies on `action_url` (the two inputs set the same check attribute, `details_url`)

## Outputs

### `check_id`

The ID of the created check, useful to update it in another action (e.g. non-`completed` `status`)

## Issues

- Action Required conclusion: button doesn't work
- Action elements: button doesn't work
- Action Required conclusion: button doesn't work?
- Action elements: button doesn't work?
- Non-completed status: too many arguments required
- Name is required when completing a non-`completed` `status` check even though we don't use it (see examples `test_with_init*`)
@@ -1,25 +1,48 @@
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['GITHUB_REPOSITORY'] = 'LB/ABC';
process.env['INPUT_TOKEN'] = 'ABC';
process.env['INPUT_NAME'] = 'ABC';
process.env['INPUT_STATUS'] = 'completed';
process.env['INPUT_CONCLUSION'] = 'success';
const ip = path.join(__dirname, '..', 'lib', 'main.js');
test('test runs (creation)', () => {
const entry = path.join(__dirname, '..', 'lib', 'main.js');
const options: cp.ExecSyncOptions = {
env: process.env,
env: {
GITHUB_REPOSITORY: 'LB/ABC',
INPUT_TOKEN: 'ABC',
INPUT_NAME: 'ABC',
INPUT_STATUS: 'completed',
INPUT_CONCLUSION: 'success',
},
};
try {
console.log(cp.execSync(`node ${ip}`, options).toString());
console.log(cp.execSync(`node ${entry}`, options).toString());
} catch (e) {
const error = e as Error & {stdout: Buffer};
const output = error.stdout.toString();
console.log(output);
expect(output).toMatch(/::debug::Error: HttpError: Bad credentials/);
expect(output).toMatch(/::debug::Creating a new Run/);
expect(output).toMatch(/::debug::HttpError: Bad credentials/);
}
});

test('test runs (update)', () => {
const entry = path.join(__dirname, '..', 'lib', 'main.js');
const options: cp.ExecSyncOptions = {
env: {
GITHUB_REPOSITORY: 'LB/ABC',
INPUT_TOKEN: 'ABC',
INPUT_CHECK_ID: '123',
INPUT_STATUS: 'completed',
INPUT_CONCLUSION: 'success',
},
};
try {
console.log(cp.execSync(`node ${entry}`, options).toString());
} catch (e) {
const error = e as Error & {stdout: Buffer};
const output = error.stdout.toString();
console.log(output);
expect(output).toMatch(/::debug::Updating a Run/);
expect(output).toMatch(/::debug::HttpError: Bad credentials/);
}
});

@@ -9,8 +9,11 @@ inputs:
description: 'your GITHUB_TOKEN'
required: true
name:
description: 'the name of your check'
required: true
description: 'the name of the check to create (incompatible with `check_id`)'
required: false
check_id:
description: 'ID of the check to update (incompatible with `name`)'
required: false
conclusion:
description: 'the conclusion of your check'
required: false
@@ -36,6 +39,9 @@ inputs:
actions:
description: 'the actions of your check'
required: false
outputs:
check_id:
description: 'the ID of the created check, useful to update it in another action'
runs:
using: 'node12'
main: 'dist/index.js'

Large diffs are not rendered by default.

@@ -7,25 +7,20 @@ type Ownership = {
repo: string;
};

type CreateOptions = {
completed: boolean;
};

const unpackInputs = (inputs: Inputs.Args, options: {update: boolean} = {update: false}): Record<string, unknown> => {
const unpackInputs = (title: string, inputs: Inputs.Args): Record<string, unknown> => {
let output;
if (inputs.output) {
output = {
title: options.update ? undefined : inputs.name,
title,
summary: inputs.output.summary,
text: inputs.output.text_description,
actions: inputs.actions,
images: inputs.images,
};
}
const more: {
details_url?: string;
conclusion?: string;
} = {};

let details_url;

if (inputs.conclusion === Inputs.Conclusion.ActionRequired || inputs.actions) {
if (inputs.detailsURL) {
const reasonList = [];
@@ -36,22 +31,22 @@ const unpackInputs = (inputs: Inputs.Args, options: {update: boolean} = {update:
reasonList.push(`'actions' was provided`);
}
const reasons = reasonList.join(' and ');
core.warning(
core.info(
`'details_url' was ignored in favor of 'action_url' because ${reasons} (see documentation for details)`,
);
}
more.details_url = inputs.actionURL;
details_url = inputs.actionURL;
} else if (inputs.detailsURL) {
more.details_url = inputs.detailsURL;
}
if (inputs.conclusion) {
more.conclusion = inputs.conclusion.toString();
details_url = inputs.detailsURL;
}

return {
status: inputs.status.toString(),
output,
actions: inputs.actions,
...more,
conclusion: inputs.conclusion ? inputs.conclusion.toString() : undefined,
completed_at: inputs.status === Inputs.Status.Completed ? formatDate() : undefined,
details_url,
};
};

@@ -61,22 +56,17 @@ const formatDate = (): string => {

export const createRun = async (
octokit: InstanceType<typeof GitHub>,
name: string,
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,
name: name,
started_at: formatDate(),
...dates,
...unpackInputs(inputs),
...unpackInputs(name, inputs),
});
return data.id;
};
@@ -87,10 +77,13 @@ export const updateRun = async (
ownership: Ownership,
inputs: Inputs.Args,
): Promise<void> => {
const previous = await octokit.checks.get({
...ownership,
check_run_id: id,
});
await octokit.checks.update({
...ownership,
check_run_id: id,
completed_at: formatDate(),
...unpackInputs(inputs, {update: true}),
...unpackInputs(previous.data.name, inputs),
});
};
@@ -18,12 +18,26 @@ const parseJSON = <T>(getInput: GetInput, property: string): T | undefined => {

export const parseInputs = (getInput: GetInput): Inputs.Args => {
const token = getInput('token', {required: true});
const name = getInput('name', {required: true});

const name = getInput('name');
const checkIDStr = getInput('check_id');

const status = getInput('status', {required: true}) as Inputs.Status;
let conclusion = getInput('conclusion') as Inputs.Conclusion;

const actionURL = getInput('action_url');
const detailsURL = getInput('details_url');

if (name && checkIDStr) {
throw new Error(`can only provide 'name' or 'check_id'`);
}

if (!name && !checkIDStr) {
throw new Error(`must provide 'name' or 'check_id'`);
}

const checkID = checkIDStr ? parseInt(checkIDStr) : undefined;

if (!Object.values(Inputs.Status).includes(status)) {
throw new Error(`invalid value for 'status': '${status}'`);
}
@@ -64,6 +78,8 @@ export const parseInputs = (getInput: GetInput): Inputs.Args => {
status,
conclusion,

checkID,

actionURL,
detailsURL,

@@ -1,9 +1,12 @@
import * as core from '@actions/core';
import * as github from '@actions/github';
import * as Inputs from './namespaces/Inputs';
import {parseInputs} from './inputs';
import {createRun, updateRun} from './checks';

const stateID = 'checkID';
const isCreation = (inputs: Inputs.Args): inputs is Inputs.ArgsCreate => {
return !!(inputs as Inputs.ArgsCreate).name;
};

async function run(): Promise<void> {
try {
@@ -19,30 +22,19 @@ async function run(): Promise<void> {
};
const sha = github.context.sha;

switch (inputs.status) {
case 'in_progress':
case 'queued': {
core.debug(`Creating a new Run`);
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) {
core.debug(`Updating a Run (${id})`);
await updateRun(octokit, parseInt(id), ownership, inputs);
} else {
core.debug(`Creating a new Run`);
await createRun(octokit, sha, ownership, inputs);
}
break;
}
if (isCreation(inputs)) {
core.debug(`Creating a new Run`);
const id = await createRun(octokit, inputs.name, sha, ownership, inputs);
core.setOutput('check_id', id);
} else {
const id = inputs.checkID;
core.debug(`Updating a Run (${id})`);
await updateRun(octokit, id, ownership, inputs);
}
core.debug(`Done`);
} catch (e) {
const error = e as Error;
core.debug(`Error: ${error.toString()}`);
core.debug(error.toString());
core.setFailed(error.message);
}
}
@@ -1,19 +1,28 @@
import {RestEndpointMethodTypes} from '@octokit/rest';

export type Args = {
name: string;
interface ArgsBase {
token: string;
conclusion?: Conclusion;
status: Status;

actionURL: string;
actionURL?: string;
detailsURL?: string;

output?: Output;
annotations?: Annotations;
images?: Images;
actions?: Actions;
};
}

export interface ArgsCreate extends ArgsBase {
name: string;
}

export interface ArgsUpdate extends ArgsBase {
checkID: number;
}

export type Args = ArgsCreate | ArgsUpdate;

// ChecksCreateParamsOutputAnnotations[]
export type Annotations = NonNullable<

0 comments on commit 88ce8c5

Please sign in to comment.