Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@actions/core logging - extract AnnotationProperties from Error instances #1696

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
67 changes: 59 additions & 8 deletions packages/core/__tests__/core.test.ts
Expand Up @@ -49,6 +49,22 @@ const testEnvVars = {
const UUID = '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'
const DELIMITER = `ghadelimiter_${UUID}`

function extractErrorMetadata(error: Error): {
file: string | undefined
line: string | undefined
column: string | undefined
} {
const stackLines = error.stack?.split(os.EOL) || []
const firstTraceLine = stackLines[1]
const match = firstTraceLine.match(/at (?:.*) \((.*):(\d+):(\d+)\)/) || []
const [, file, line, column] = match
return {
file,
line,
column
}
}

describe('@actions/core', () => {
beforeAll(() => {
const filePath = path.join(__dirname, `test`)
Expand Down Expand Up @@ -379,9 +395,14 @@ describe('@actions/core', () => {

it('setFailed handles Error', () => {
const message = 'this is my error message'
core.setFailed(new Error(message))
const error = new Error(message)

core.setFailed(error)
expect(process.exitCode).toBe(core.ExitCode.Failure)
assertWriteCalls([`::error::Error: ${message}${os.EOL}`])
const {file, line, column} = extractErrorMetadata(error)
assertWriteCalls([
`::error title=Error,file=${file},line=${line},col=${column}::Error: ${message}${os.EOL}`
])
})

it('error sets the correct error message', () => {
Expand All @@ -396,11 +417,21 @@ describe('@actions/core', () => {

it('error handles an error object', () => {
const message = 'this is my error message'
core.error(new Error(message))
const error = new Error(message)
core.error(error)
const {file, line, column} = extractErrorMetadata(error)
assertWriteCalls([
`::error title=Error,file=${file},line=${line},col=${column}::Error: ${message}${os.EOL}`
])
})

it('error handles an error object and an empty properties', () => {
const message = 'this is my error message'
core.error(new Error(message), {})
assertWriteCalls([`::error::Error: ${message}${os.EOL}`])
})

it('error handles parameters correctly', () => {
it('error handles custom properties correctly', () => {
const message = 'this is my error message'
core.error(new Error(message), {
title: 'A title',
Expand All @@ -427,11 +458,21 @@ describe('@actions/core', () => {

it('warning handles an error object', () => {
const message = 'this is my error message'
core.warning(new Error(message))
const error = new Error(message)
core.warning(error)
const {file, line, column} = extractErrorMetadata(error)
assertWriteCalls([
`::warning title=Error,file=${file},line=${line},col=${column}::Error: ${message}${os.EOL}`
])
})

it('warning handles an error object and an empty properties', () => {
const message = 'this is my error message'
core.warning(new Error(message), {})
assertWriteCalls([`::warning::Error: ${message}${os.EOL}`])
})

it('warning handles parameters correctly', () => {
it('warning handles custom properties correctly', () => {
const message = 'this is my error message'
core.warning(new Error(message), {
title: 'A title',
Expand All @@ -458,11 +499,21 @@ describe('@actions/core', () => {

it('notice handles an error object', () => {
const message = 'this is my error message'
core.notice(new Error(message))
const error = new Error(message)
core.notice(error)
const {file, line, column} = extractErrorMetadata(error)
assertWriteCalls([
`::notice title=Error,file=${file},line=${line},col=${column}::Error: ${message}${os.EOL}`
])
})

it('notice handles an error object and an empty properties', () => {
const message = 'this is my error message'
core.notice(new Error(message), {})
assertWriteCalls([`::notice::Error: ${message}${os.EOL}`])
})

it('notice handles parameters correctly', () => {
it('notice handles custom properties correctly', () => {
const message = 'this is my error message'
core.notice(new Error(message), {
title: 'A title',
Expand Down
26 changes: 26 additions & 0 deletions packages/core/__tests__/utils.test.ts
@@ -0,0 +1,26 @@
import {toAnnotationProperties} from '../src/utils'

describe('@actions/core/src/utils', () => {
describe('.toAnnotationProperties', () => {
it('extracts title only from Error instance without a parseable stack', () => {
const error = new TypeError('Test error')
error.stack = ''
expect(toAnnotationProperties(error)).toEqual({
title: 'TypeError',
file: undefined,
startLine: undefined,
startColumn: undefined
})
})

it('extracts AnnotationProperties from Error instance', () => {
const error = new ReferenceError('Test error')
expect(toAnnotationProperties(error)).toEqual({
title: 'ReferenceError',
file: expect.stringMatching(/utils\.test\.ts$/),
startLine: expect.any(Number),
startColumn: expect.any(Number)
})
})
})
})
27 changes: 27 additions & 0 deletions packages/core/package-lock.json

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

3 changes: 2 additions & 1 deletion packages/core/package.json
Expand Up @@ -38,10 +38,11 @@
"dependencies": {
"@actions/exec": "^1.1.1",
"@actions/http-client": "^2.0.1",
"error-stack-parser": "^2.1.4",
"uuid": "^8.3.2"
},
"devDependencies": {
"@types/node": "^12.0.2",
"@types/uuid": "^8.3.4"
}
}
}
34 changes: 29 additions & 5 deletions packages/core/src/core.ts
@@ -1,7 +1,10 @@
import {issue, issueCommand} from './command'
import {issueFileCommand, prepareKeyValueMessage} from './file-command'
import {toCommandProperties, toCommandValue} from './utils'

import {
toAnnotationProperties,
toCommandProperties,
toCommandValue
} from './utils'
import * as os from 'os'
import * as path from 'path'

Expand Down Expand Up @@ -242,15 +245,32 @@ export function debug(message: string): void {
issueCommand('debug', {}, message)
}

function defaultAnnotationPropertes(
message: string | Error,
properties: AnnotationProperties | undefined = undefined
): AnnotationProperties {
// If no properties are provided, try to extract them from the Error instance
if (properties === undefined) {
if (message instanceof Error) {
properties = toAnnotationProperties(message)
} else {
properties = {}
}
}
return properties
}

/**
* Adds an error issue
* @param message error issue message. Errors will be converted to string via toString()
* @param properties optional properties to add to the annotation.
*/
export function error(
message: string | Error,
properties: AnnotationProperties = {}
properties: AnnotationProperties | undefined = undefined
): void {
properties = defaultAnnotationPropertes(message, properties)

issueCommand(
'error',
toCommandProperties(properties),
Expand All @@ -265,8 +285,10 @@ export function error(
*/
export function warning(
message: string | Error,
properties: AnnotationProperties = {}
properties: AnnotationProperties | undefined = undefined
): void {
properties = defaultAnnotationPropertes(message, properties)

issueCommand(
'warning',
toCommandProperties(properties),
Expand All @@ -281,8 +303,10 @@ export function warning(
*/
export function notice(
message: string | Error,
properties: AnnotationProperties = {}
properties: AnnotationProperties | undefined = undefined
): void {
properties = defaultAnnotationPropertes(message, properties)

issueCommand(
'notice',
toCommandProperties(properties),
Expand Down
19 changes: 19 additions & 0 deletions packages/core/src/utils.ts
Expand Up @@ -3,6 +3,7 @@

import {AnnotationProperties} from './core'
import {CommandProperties} from './command'
import ErrorStackParser from 'error-stack-parser'

/**
* Sanitizes an input into a string so it can be passed into issueCommand safely
Expand Down Expand Up @@ -39,3 +40,21 @@ export function toCommandProperties(
endColumn: annotationProperties.endColumn
}
}

export function toAnnotationProperties(error: Error): AnnotationProperties {
let firstFrame

try {
const stack = ErrorStackParser.parse(error)
firstFrame = stack?.[0]
} catch (parseError) {
// If we can't parse the stack, we'll just skip it
}

return {
title: error.name,
file: firstFrame?.fileName,
startLine: firstFrame?.lineNumber,
startColumn: firstFrame?.columnNumber
}
}