Compare commits

...

4 Commits

6 changed files with 162 additions and 14 deletions
+59 -8
View File
@@ -49,6 +49,22 @@ const testEnvVars = {
const UUID = '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d' const UUID = '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'
const DELIMITER = `ghadelimiter_${UUID}` 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', () => { describe('@actions/core', () => {
beforeAll(() => { beforeAll(() => {
const filePath = path.join(__dirname, `test`) const filePath = path.join(__dirname, `test`)
@@ -379,9 +395,14 @@ describe('@actions/core', () => {
it('setFailed handles Error', () => { it('setFailed handles Error', () => {
const message = 'this is my error message' 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) 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', () => { it('error sets the correct error message', () => {
@@ -396,11 +417,21 @@ describe('@actions/core', () => {
it('error handles an error object', () => { it('error handles an error object', () => {
const message = 'this is my error message' 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}`]) assertWriteCalls([`::error::Error: ${message}${os.EOL}`])
}) })
it('error handles parameters correctly', () => { it('error handles custom properties correctly', () => {
const message = 'this is my error message' const message = 'this is my error message'
core.error(new Error(message), { core.error(new Error(message), {
title: 'A title', title: 'A title',
@@ -427,11 +458,21 @@ describe('@actions/core', () => {
it('warning handles an error object', () => { it('warning handles an error object', () => {
const message = 'this is my error message' 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}`]) assertWriteCalls([`::warning::Error: ${message}${os.EOL}`])
}) })
it('warning handles parameters correctly', () => { it('warning handles custom properties correctly', () => {
const message = 'this is my error message' const message = 'this is my error message'
core.warning(new Error(message), { core.warning(new Error(message), {
title: 'A title', title: 'A title',
@@ -458,11 +499,21 @@ describe('@actions/core', () => {
it('notice handles an error object', () => { it('notice handles an error object', () => {
const message = 'this is my error message' 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}`]) assertWriteCalls([`::notice::Error: ${message}${os.EOL}`])
}) })
it('notice handles parameters correctly', () => { it('notice handles custom properties correctly', () => {
const message = 'this is my error message' const message = 'this is my error message'
core.notice(new Error(message), { core.notice(new Error(message), {
title: 'A title', title: 'A title',
+26
View File
@@ -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
View File
@@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@actions/exec": "^1.1.1", "@actions/exec": "^1.1.1",
"@actions/http-client": "^2.0.1", "@actions/http-client": "^2.0.1",
"error-stack-parser": "^2.1.4",
"uuid": "^8.3.2" "uuid": "^8.3.2"
}, },
"devDependencies": { "devDependencies": {
@@ -51,6 +52,19 @@
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
"dev": true "dev": true
}, },
"node_modules/error-stack-parser": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
"integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
"dependencies": {
"stackframe": "^1.3.4"
}
},
"node_modules/stackframe": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
"integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="
},
"node_modules/tunnel": { "node_modules/tunnel": {
"version": "0.0.6", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
@@ -102,6 +116,19 @@
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
"dev": true "dev": true
}, },
"error-stack-parser": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
"integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
"requires": {
"stackframe": "^1.3.4"
}
},
"stackframe": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
"integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="
},
"tunnel": { "tunnel": {
"version": "0.0.6", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
+1
View File
@@ -38,6 +38,7 @@
"dependencies": { "dependencies": {
"@actions/exec": "^1.1.1", "@actions/exec": "^1.1.1",
"@actions/http-client": "^2.0.1", "@actions/http-client": "^2.0.1",
"error-stack-parser": "^2.1.4",
"uuid": "^8.3.2" "uuid": "^8.3.2"
}, },
"devDependencies": { "devDependencies": {
+29 -5
View File
@@ -1,7 +1,10 @@
import {issue, issueCommand} from './command' import {issue, issueCommand} from './command'
import {issueFileCommand, prepareKeyValueMessage} from './file-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 os from 'os'
import * as path from 'path' import * as path from 'path'
@@ -242,6 +245,21 @@ export function debug(message: string): void {
issueCommand('debug', {}, message) 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 * Adds an error issue
* @param message error issue message. Errors will be converted to string via toString() * @param message error issue message. Errors will be converted to string via toString()
@@ -249,8 +267,10 @@ export function debug(message: string): void {
*/ */
export function error( export function error(
message: string | Error, message: string | Error,
properties: AnnotationProperties = {} properties: AnnotationProperties | undefined = undefined
): void { ): void {
properties = defaultAnnotationPropertes(message, properties)
issueCommand( issueCommand(
'error', 'error',
toCommandProperties(properties), toCommandProperties(properties),
@@ -265,8 +285,10 @@ export function error(
*/ */
export function warning( export function warning(
message: string | Error, message: string | Error,
properties: AnnotationProperties = {} properties: AnnotationProperties | undefined = undefined
): void { ): void {
properties = defaultAnnotationPropertes(message, properties)
issueCommand( issueCommand(
'warning', 'warning',
toCommandProperties(properties), toCommandProperties(properties),
@@ -281,8 +303,10 @@ export function warning(
*/ */
export function notice( export function notice(
message: string | Error, message: string | Error,
properties: AnnotationProperties = {} properties: AnnotationProperties | undefined = undefined
): void { ): void {
properties = defaultAnnotationPropertes(message, properties)
issueCommand( issueCommand(
'notice', 'notice',
toCommandProperties(properties), toCommandProperties(properties),
+19
View File
@@ -3,6 +3,7 @@
import {AnnotationProperties} from './core' import {AnnotationProperties} from './core'
import {CommandProperties} from './command' import {CommandProperties} from './command'
import ErrorStackParser from 'error-stack-parser'
/** /**
* Sanitizes an input into a string so it can be passed into issueCommand safely * Sanitizes an input into a string so it can be passed into issueCommand safely
@@ -39,3 +40,21 @@ export function toCommandProperties(
endColumn: annotationProperties.endColumn 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
}
}