Skip to content

Commit

Permalink
feat: add output to step summary (#21)
Browse files Browse the repository at this point in the history
* feat: add output to step summary
  • Loading branch information
jcdickinson authored Nov 8, 2022
1 parent 1e9b578 commit c1b717a
Show file tree
Hide file tree
Showing 10 changed files with 2,073 additions and 119 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,11 @@ jobs:
# none
list-tests: 'all'
# The location to write the report to. Supported options:
# checks
# step-summary
output-to: 'checks'
# Limits number of created annotations with error message and stack trace captured during test execution.
# Must be less or equal to 50.
max-annotations: '10'
Expand Down
1 change: 1 addition & 0 deletions __tests__/dotnet-trx.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ describe('dotnet-trx tests', () => {
listSuites: 'all',
listTests: 'failed',
onlySummary: false,
slugPrefix: '',
baseUrl: ''
}
const report = getReport([result], reportOptions)
Expand Down
7 changes: 7 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,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: 'checks'
required: false
token:
description: GitHub Access Token
required: false
Expand Down
1,960 changes: 1,888 additions & 72 deletions dist/index.js

Large diffs are not rendered by default.

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

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions dist/licenses.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 46 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"author": "Michal Dorner <dorner.michal@gmail.com>",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.2.6",
"@actions/core": "^1.9.1",
"@actions/exec": "^1.0.4",
"@actions/github": "^4.0.0",
"adm-zip": "^0.5.3",
Expand Down
131 changes: 101 additions & 30 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -30,6 +31,16 @@ async function main(): Promise<void> {
}
}

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})
Expand All @@ -42,7 +53,9 @@ class TestReporter {
readonly failOnError = core.getInput('fail-on-error', {required: true}) === 'true'
readonly workDirInput = core.getInput('working-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<typeof GitHub>
readonly context = getCheckRunContext()

Expand All @@ -63,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<void> {
Expand Down Expand Up @@ -154,22 +176,37 @@ class TestReporter {
results.push(tr)
}

core.info(`Creating check run ${name}`)
const createResp = await this.octokit.checks.create({
head_sha: this.context.sha,
name,
status: 'in_progress',
output: {
title: name,
summary: ''
},
...github.context.repo
})
let createResp = null,
baseUrl = '',
check_run_id = 0

switch (this.outputTo) {
case 'checks': {
core.info(`Creating check run ${name}`)
createResp = await this.octokit.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
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)
Expand All @@ -183,22 +220,56 @@ class TestReporter {
const shortSummary = `${passed} passed, ${failed} failed and ${skipped} skipped `

core.info(`Updating check run conclusion (${conclusion}) and output`)
const resp = await this.octokit.checks.update({
check_run_id: createResp.data.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}`)

core.setOutput(Outputs.runHtmlUrl, `${resp.data.html_url}`)
switch (this.outputTo) {
case 'checks': {
const resp = await this.octokit.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
}
Expand Down
18 changes: 10 additions & 8 deletions src/report/get-report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ const MAX_REPORT_LENGTH = 65535
export interface ReportOptions {
listSuites: 'all' | 'failed'
listTests: 'all' | 'failed' | 'none'
slugPrefix: string
baseUrl: string
onlySummary: boolean
}

const defaultOptions: ReportOptions = {
listSuites: 'all',
listTests: 'all',
slugPrefix: '',
baseUrl: '',
onlySummary: false
}
Expand Down Expand Up @@ -138,7 +140,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 passed = tr.passed > 0 ? `${tr.passed}${Icon.success}` : ''
const failed = tr.failed > 0 ? `${tr.failed}${Icon.fail}` : ''
Expand All @@ -164,7 +166,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 = `<a id="${trSlug.id}" href="${options.baseUrl + trSlug.link}">${tr.path}</a>`
const icon = getResultIcon(tr.result)
sections.push(`## ${icon}\xa0${nameLink}`)
Expand All @@ -185,7 +187,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 passed = s.passed > 0 ? `${s.passed}${Icon.success}` : ''
const failed = s.failed > 0 ? `${s.failed}${Icon.fail}` : ''
Expand Down Expand Up @@ -219,7 +221,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 = `<a id="${tsSlug.id}" href="${options.baseUrl + tsSlug.link}">${tsName}</a>`
const icon = getResultIcon(ts.result)
sections.push(`### ${icon}\xa0${tsNameLink}`)
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit c1b717a

Please sign in to comment.