diff --git a/action.yml b/action.yml
index bcaad45..7a5dc45 100644
--- a/action.yml
+++ b/action.yml
@@ -70,6 +70,13 @@ inputs:
Detailed listing of test suites and test cases will be skipped.
default: 'false'
required: false
+ output-to:
+ description: |
+ The location to write the report to. Supported options:
+ - checks
+ - step-summary
+ default: 'step-summary'
+ required: false
token:
description: GitHub Access Token
required: false
diff --git a/dist/index.js b/dist/index.js
index 0536655..c03f637 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -256,6 +256,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
Object.defineProperty(exports, "__esModule", ({ value: true }));
const core = __importStar(__nccwpck_require__(2186));
const github = __importStar(__nccwpck_require__(5438));
+const crypto_1 = __nccwpck_require__(6113);
const artifact_provider_1 = __nccwpck_require__(7171);
const local_file_provider_1 = __nccwpck_require__(9399);
const get_annotations_1 = __nccwpck_require__(5867);
@@ -267,7 +268,6 @@ const jest_junit_parser_1 = __nccwpck_require__(1113);
const mocha_json_parser_1 = __nccwpck_require__(6043);
const path_utils_1 = __nccwpck_require__(4070);
const github_utils_1 = __nccwpck_require__(3522);
-const markdown_utils_1 = __nccwpck_require__(6482);
function main() {
return __awaiter(this, void 0, void 0, function* () {
try {
@@ -282,6 +282,15 @@ function main() {
}
});
}
+function createSlugPrefix() {
+ const step_summary = process.env['GITHUB_STEP_SUMMARY'];
+ if (!step_summary || step_summary === '') {
+ return '';
+ }
+ const hash = (0, crypto_1.createHash)('sha1');
+ hash.update(step_summary);
+ return hash.digest('hex').substring(0, 8);
+}
class TestReporter {
constructor() {
this.artifact = core.getInput('artifact', { required: false });
@@ -296,7 +305,9 @@ class TestReporter {
this.workDirInput = core.getInput('working-directory', { required: false });
this.buildDirInput = core.getInput('build-directory', { required: false });
this.onlySummary = core.getInput('only-summary', { required: false }) === 'true';
+ this.outputTo = core.getInput('output-to', { required: false });
this.token = core.getInput('token', { required: true });
+ this.slugPrefix = '';
this.context = (0, github_utils_1.getCheckRunContext)();
this.octokit = github.getOctokit(this.token);
if (this.listSuites !== 'all' && this.listSuites !== 'failed') {
@@ -311,6 +322,13 @@ class TestReporter {
core.setFailed(`Input parameter 'max-annotations' has invalid value`);
return;
}
+ if (this.outputTo !== 'checks' && this.outputTo !== 'step-summary') {
+ core.setFailed(`Input parameter 'output-to' has invalid value`);
+ return;
+ }
+ if (this.outputTo === 'step-summary') {
+ this.slugPrefix = createSlugPrefix();
+ }
}
run() {
return __awaiter(this, void 0, void 0, function* () {
@@ -376,6 +394,7 @@ class TestReporter {
});
}
createReport(parser, name, files) {
+ var _a, _b;
return __awaiter(this, void 0, void 0, function* () {
if (files.length === 0) {
core.warning(`No file matches path ${this.path}`);
@@ -393,29 +412,82 @@ class TestReporter {
throw error;
}
}
- core.info(`Creating check run ${name}`);
- const createResp = yield this.octokit.rest.checks.create(Object.assign({ head_sha: this.context.sha, name, status: 'in_progress', output: {
- title: name,
- summary: ''
- } }, github.context.repo));
+ let createResp = null;
+ let baseUrl = '';
+ let check_run_id = 0;
+ switch (this.outputTo) {
+ case 'checks': {
+ core.info(`Creating check run ${name}`);
+ createResp = yield this.octokit.rest.checks.create(Object.assign({ head_sha: this.context.sha, name, status: 'in_progress', output: {
+ title: name,
+ summary: ''
+ } }, github.context.repo));
+ baseUrl = (_a = createResp.data.html_url) !== null && _a !== void 0 ? _a : '';
+ check_run_id = createResp.data.id;
+ break;
+ }
+ case 'step-summary': {
+ const run_attempt = (_b = process.env['GITHUB_RUN_ATTEMPT']) !== null && _b !== void 0 ? _b : 1;
+ baseUrl = `https://github.com/${github.context.repo.owner}/${github.context.repo.repo}/actions/runs/${github.context.runId}/attempts/${run_attempt}`;
+ break;
+ }
+ }
core.info('Creating report summary');
- const { listSuites, listTests, onlySummary } = this;
- const baseUrl = createResp.data.html_url;
- const summary = (0, get_report_1.getReport)(results, { listSuites, listTests, baseUrl, onlySummary });
+ const { listSuites, listTests, onlySummary, slugPrefix } = this;
+ const summary = (0, get_report_1.getReport)(results, { listSuites, listTests, baseUrl, slugPrefix, onlySummary });
core.info('Creating annotations');
const annotations = (0, get_annotations_1.getAnnotations)(results, this.maxAnnotations);
const isFailed = this.failOnError && results.some(tr => tr.result === 'failed');
const conclusion = isFailed ? 'failure' : 'success';
- const icon = isFailed ? markdown_utils_1.Icon.fail : markdown_utils_1.Icon.success;
+ const passed = results.reduce((sum, tr) => sum + tr.passed, 0);
+ const failed = results.reduce((sum, tr) => sum + tr.failed, 0);
+ const skipped = results.reduce((sum, tr) => sum + tr.skipped, 0);
+ const shortSummary = `${passed} passed, ${failed} failed and ${skipped} skipped `;
core.info(`Updating check run conclusion (${conclusion}) and output`);
- const resp = yield this.octokit.rest.checks.update(Object.assign({ check_run_id: createResp.data.id, conclusion, status: 'completed', output: {
- title: `${name} ${icon}`,
- summary,
- annotations
- } }, github.context.repo));
- core.info(`Check run create response: ${resp.status}`);
- core.info(`Check run URL: ${resp.data.url}`);
- core.info(`Check run HTML: ${resp.data.html_url}`);
+ switch (this.outputTo) {
+ case 'checks': {
+ const resp = yield this.octokit.rest.checks.update(Object.assign({ check_run_id,
+ conclusion, status: 'completed', output: {
+ title: shortSummary,
+ summary,
+ annotations
+ } }, github.context.repo));
+ core.info(`Check run create response: ${resp.status}`);
+ core.info(`Check run URL: ${resp.data.url}`);
+ core.info(`Check run HTML: ${resp.data.html_url}`);
+ break;
+ }
+ case 'step-summary': {
+ core.summary.addRaw(`# ${shortSummary}`);
+ core.summary.addRaw(summary);
+ yield core.summary.write();
+ for (const annotation of annotations) {
+ let fn;
+ switch (annotation.annotation_level) {
+ case 'failure':
+ fn = core.error;
+ break;
+ case 'warning':
+ fn = core.warning;
+ break;
+ case 'notice':
+ fn = core.notice;
+ break;
+ default:
+ continue;
+ }
+ fn(annotation.message, {
+ title: annotation.title,
+ file: annotation.path,
+ startLine: annotation.start_line,
+ endLine: annotation.end_line,
+ startColumn: annotation.start_column,
+ endColumn: annotation.end_column
+ });
+ }
+ break;
+ }
+ }
return results;
});
}
@@ -1526,6 +1598,7 @@ const MAX_REPORT_LENGTH = 65535;
const defaultOptions = {
listSuites: 'all',
listTests: 'all',
+ slugPrefix: '',
baseUrl: '',
onlySummary: false
};
@@ -1629,7 +1702,7 @@ function getTestRunsReport(testRuns, options) {
const tableData = testRuns.map((tr, runIndex) => {
const time = (0, markdown_utils_1.formatTime)(tr.time);
const name = tr.path;
- const addr = options.baseUrl + makeRunSlug(runIndex).link;
+ const addr = options.baseUrl + makeRunSlug(runIndex, options.slugPrefix).link;
const nameLink = (0, markdown_utils_1.link)(name, addr);
const statusIcon = tr.failed > 0 ? markdown_utils_1.Icon.fail : tr.passed > 0 ? markdown_utils_1.Icon.success : markdown_utils_1.Icon.skip;
const passed = tr.passed === 0 ? '' : tr.passed;
@@ -1648,7 +1721,7 @@ function getTestRunsReport(testRuns, options) {
}
function getSuitesReport(tr, runIndex, options) {
const sections = [];
- const trSlug = makeRunSlug(runIndex);
+ const trSlug = makeRunSlug(runIndex, options.slugPrefix);
const nameLink = `${tr.path}`;
const icon = getResultIcon(tr.result);
sections.push(`## ${icon}\xa0${nameLink}`);
@@ -1663,7 +1736,7 @@ function getSuitesReport(tr, runIndex, options) {
const tsTime = (0, markdown_utils_1.formatTime)(s.time);
const tsName = s.name;
const skipLink = options.listTests === 'none' || (options.listTests === 'failed' && s.result !== 'failed');
- const tsAddr = options.baseUrl + makeSuiteSlug(runIndex, suiteIndex).link;
+ const tsAddr = options.baseUrl + makeSuiteSlug(runIndex, suiteIndex, options.slugPrefix).link;
const tsNameLink = skipLink ? tsName : (0, markdown_utils_1.link)(tsName, tsAddr);
const statusIcon = s.failed > 0 ? markdown_utils_1.Icon.fail : s.passed > 0 ? markdown_utils_1.Icon.success : markdown_utils_1.Icon.skip;
const passed = s.passed === 0 ? '' : s.passed;
@@ -1692,7 +1765,7 @@ function getTestsReport(ts, runIndex, suiteIndex, options) {
}
const sections = [];
const tsName = ts.name;
- const tsSlug = makeSuiteSlug(runIndex, suiteIndex);
+ const tsSlug = makeSuiteSlug(runIndex, suiteIndex, options.slugPrefix);
const tsNameLink = `${tsName}`;
const icon = getResultIcon(ts.result);
sections.push(`### ${icon}\xa0${tsNameLink}`);
@@ -1716,13 +1789,13 @@ function getTestsReport(ts, runIndex, suiteIndex, options) {
sections.push('```');
return sections;
}
-function makeRunSlug(runIndex) {
+function makeRunSlug(runIndex, slugPrefix) {
// use prefix to avoid slug conflicts after escaping the paths
- return (0, slugger_1.slug)(`r${runIndex}`);
+ return (0, slugger_1.slug)(`r${slugPrefix}${runIndex}`);
}
-function makeSuiteSlug(runIndex, suiteIndex) {
+function makeSuiteSlug(runIndex, suiteIndex, slugPrefix) {
// use prefix to avoid slug conflicts after escaping the paths
- return (0, slugger_1.slug)(`r${runIndex}s${suiteIndex}`);
+ return (0, slugger_1.slug)(`r${slugPrefix}${runIndex}s${suiteIndex}`);
}
function getResultIcon(result) {
switch (result) {
diff --git a/src/main.ts b/src/main.ts
index 3c4bb68..3a8c488 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,5 +1,6 @@
import * as core from '@actions/core'
import * as github from '@actions/github'
+import {createHash} from 'crypto'
import {GitHub} from '@actions/github/lib/utils'
import {ArtifactProvider} from './input-providers/artifact-provider'
@@ -18,7 +19,6 @@ import {MochaJsonParser} from './parsers/mocha-json/mocha-json-parser'
import {normalizeDirPath, normalizeFilePath} from './utils/path-utils'
import {getCheckRunContext} from './utils/github-utils'
-import {Icon} from './utils/markdown-utils'
async function main(): Promise {
try {
@@ -30,6 +30,16 @@ async function main(): Promise {
}
}
+function createSlugPrefix(): string {
+ const step_summary = process.env['GITHUB_STEP_SUMMARY']
+ if (!step_summary || step_summary === '') {
+ return ''
+ }
+ const hash = createHash('sha1')
+ hash.update(step_summary)
+ return hash.digest('hex').substring(0, 8)
+}
+
class TestReporter {
readonly artifact = core.getInput('artifact', {required: false})
readonly name = core.getInput('name', {required: true})
@@ -43,7 +53,9 @@ class TestReporter {
readonly workDirInput = core.getInput('working-directory', {required: false})
readonly buildDirInput = core.getInput('build-directory', {required: false})
readonly onlySummary = core.getInput('only-summary', {required: false}) === 'true'
+ readonly outputTo = core.getInput('output-to', {required: false})
readonly token = core.getInput('token', {required: true})
+ readonly slugPrefix: string = ''
readonly octokit: InstanceType
readonly context = getCheckRunContext()
@@ -64,6 +76,15 @@ class TestReporter {
core.setFailed(`Input parameter 'max-annotations' has invalid value`)
return
}
+
+ if (this.outputTo !== 'checks' && this.outputTo !== 'step-summary') {
+ core.setFailed(`Input parameter 'output-to' has invalid value`)
+ return
+ }
+
+ if (this.outputTo === 'step-summary') {
+ this.slugPrefix = createSlugPrefix()
+ }
}
async run(): Promise {
@@ -164,45 +185,100 @@ class TestReporter {
}
}
- core.info(`Creating check run ${name}`)
- const createResp = await this.octokit.rest.checks.create({
- head_sha: this.context.sha,
- name,
- status: 'in_progress',
- output: {
- title: name,
- summary: ''
- },
- ...github.context.repo
- })
+ let createResp = null
+ let baseUrl = ''
+ let check_run_id = 0
+
+ switch (this.outputTo) {
+ case 'checks': {
+ core.info(`Creating check run ${name}`)
+ createResp = await this.octokit.rest.checks.create({
+ head_sha: this.context.sha,
+ name,
+ status: 'in_progress',
+ output: {
+ title: name,
+ summary: ''
+ },
+ ...github.context.repo
+ })
+ baseUrl = createResp.data.html_url ?? ''
+ check_run_id = createResp.data.id
+ break
+ }
+ case 'step-summary': {
+ const run_attempt = process.env['GITHUB_RUN_ATTEMPT'] ?? 1
+ baseUrl = `https://github.com/${github.context.repo.owner}/${github.context.repo.repo}/actions/runs/${github.context.runId}/attempts/${run_attempt}`
+ break
+ }
+ }
core.info('Creating report summary')
- const {listSuites, listTests, onlySummary} = this
- const baseUrl = createResp.data.html_url as string
- const summary = getReport(results, {listSuites, listTests, baseUrl, onlySummary})
+ const {listSuites, listTests, onlySummary, slugPrefix} = this
+ const summary = getReport(results, {listSuites, listTests, baseUrl, slugPrefix, onlySummary})
core.info('Creating annotations')
const annotations = getAnnotations(results, this.maxAnnotations)
const isFailed = this.failOnError && results.some(tr => tr.result === 'failed')
const conclusion = isFailed ? 'failure' : 'success'
- const icon = isFailed ? Icon.fail : Icon.success
+
+ const passed = results.reduce((sum, tr) => sum + tr.passed, 0)
+ const failed = results.reduce((sum, tr) => sum + tr.failed, 0)
+ const skipped = results.reduce((sum, tr) => sum + tr.skipped, 0)
+ const shortSummary = `${passed} passed, ${failed} failed and ${skipped} skipped `
core.info(`Updating check run conclusion (${conclusion}) and output`)
- const resp = await this.octokit.rest.checks.update({
- check_run_id: createResp.data.id,
- conclusion,
- status: 'completed',
- output: {
- title: `${name} ${icon}`,
- summary,
- annotations
- },
- ...github.context.repo
- })
- core.info(`Check run create response: ${resp.status}`)
- core.info(`Check run URL: ${resp.data.url}`)
- core.info(`Check run HTML: ${resp.data.html_url}`)
+ switch (this.outputTo) {
+ case 'checks': {
+ const resp = await this.octokit.rest.checks.update({
+ check_run_id,
+ conclusion,
+ status: 'completed',
+ output: {
+ title: shortSummary,
+ summary,
+ annotations
+ },
+ ...github.context.repo
+ })
+ core.info(`Check run create response: ${resp.status}`)
+ core.info(`Check run URL: ${resp.data.url}`)
+ core.info(`Check run HTML: ${resp.data.html_url}`)
+ break
+ }
+ case 'step-summary': {
+ core.summary.addRaw(`# ${shortSummary}`)
+ core.summary.addRaw(summary)
+ await core.summary.write()
+ for (const annotation of annotations) {
+ let fn
+ switch (annotation.annotation_level) {
+ case 'failure':
+ fn = core.error
+ break
+ case 'warning':
+ fn = core.warning
+ break
+ case 'notice':
+ fn = core.notice
+ break
+ default:
+ continue
+ }
+
+ fn(annotation.message, {
+ title: annotation.title,
+ file: annotation.path,
+ startLine: annotation.start_line,
+ endLine: annotation.end_line,
+ startColumn: annotation.start_column,
+ endColumn: annotation.end_column
+ })
+ }
+ break
+ }
+ }
return results
}
diff --git a/src/report/get-report.ts b/src/report/get-report.ts
index 88bb32e..2918da1 100644
--- a/src/report/get-report.ts
+++ b/src/report/get-report.ts
@@ -10,6 +10,7 @@ const MAX_REPORT_LENGTH = 65535
export interface ReportOptions {
listSuites: 'all' | 'failed'
listTests: 'all' | 'failed' | 'none'
+ slugPrefix: string
baseUrl: string
onlySummary: boolean
}
@@ -17,6 +18,7 @@ export interface ReportOptions {
const defaultOptions: ReportOptions = {
listSuites: 'all',
listTests: 'all',
+ slugPrefix: '',
baseUrl: '',
onlySummary: false
}
@@ -139,7 +141,7 @@ function getTestRunsReport(testRuns: TestRunResult[], options: ReportOptions): s
const tableData = testRuns.map((tr, runIndex) => {
const time = formatTime(tr.time)
const name = tr.path
- const addr = options.baseUrl + makeRunSlug(runIndex).link
+ const addr = options.baseUrl + makeRunSlug(runIndex, options.slugPrefix).link
const nameLink = link(name, addr)
const statusIcon = tr.failed > 0 ? Icon.fail : tr.passed > 0 ? Icon.success : Icon.skip
const passed = tr.passed === 0 ? '' : tr.passed
@@ -166,7 +168,7 @@ function getTestRunsReport(testRuns: TestRunResult[], options: ReportOptions): s
function getSuitesReport(tr: TestRunResult, runIndex: number, options: ReportOptions): string[] {
const sections: string[] = []
- const trSlug = makeRunSlug(runIndex)
+ const trSlug = makeRunSlug(runIndex, options.slugPrefix)
const nameLink = `${tr.path}`
const icon = getResultIcon(tr.result)
sections.push(`## ${icon}\xa0${nameLink}`)
@@ -187,7 +189,7 @@ function getSuitesReport(tr: TestRunResult, runIndex: number, options: ReportOpt
const tsTime = formatTime(s.time)
const tsName = s.name
const skipLink = options.listTests === 'none' || (options.listTests === 'failed' && s.result !== 'failed')
- const tsAddr = options.baseUrl + makeSuiteSlug(runIndex, suiteIndex).link
+ const tsAddr = options.baseUrl + makeSuiteSlug(runIndex, suiteIndex, options.slugPrefix).link
const tsNameLink = skipLink ? tsName : link(tsName, tsAddr)
const statusIcon = s.failed > 0 ? Icon.fail : s.passed > 0 ? Icon.success : Icon.skip
const passed = s.passed === 0 ? '' : s.passed
@@ -222,7 +224,7 @@ function getTestsReport(ts: TestSuiteResult, runIndex: number, suiteIndex: numbe
const sections: string[] = []
const tsName = ts.name
- const tsSlug = makeSuiteSlug(runIndex, suiteIndex)
+ const tsSlug = makeSuiteSlug(runIndex, suiteIndex, options.slugPrefix)
const tsNameLink = `${tsName}`
const icon = getResultIcon(ts.result)
sections.push(`### ${icon}\xa0${tsNameLink}`)
@@ -251,14 +253,14 @@ function getTestsReport(ts: TestSuiteResult, runIndex: number, suiteIndex: numbe
return sections
}
-function makeRunSlug(runIndex: number): {id: string; link: string} {
+function makeRunSlug(runIndex: number, slugPrefix: string): {id: string; link: string} {
// use prefix to avoid slug conflicts after escaping the paths
- return slug(`r${runIndex}`)
+ return slug(`r${slugPrefix}${runIndex}`)
}
-function makeSuiteSlug(runIndex: number, suiteIndex: number): {id: string; link: string} {
+function makeSuiteSlug(runIndex: number, suiteIndex: number, slugPrefix: string): {id: string; link: string} {
// use prefix to avoid slug conflicts after escaping the paths
- return slug(`r${runIndex}s${suiteIndex}`)
+ return slug(`r${slugPrefix}${runIndex}s${suiteIndex}`)
}
function getResultIcon(result: TestExecutionResult): string {