Merge pull request #138 from actions/error-utils

Convert errors into Actions-compatible logging with annotations
This commit is contained in:
James M. Greene
2024-03-28 15:01:39 -05:00
committed by GitHub
10 changed files with 574 additions and 31 deletions

406
dist/index.js vendored
View File

@@ -12749,6 +12749,213 @@ class Deprecation extends Error {
exports.Deprecation = Deprecation;
/***/ }),
/***/ 9176:
/***/ (function(module, __unused_webpack_exports, __nccwpck_require__) {
(function(root, factory) {
'use strict';
// Universal Module Definition (UMD) to support AMD, CommonJS/Node.js, Rhino, and browsers.
/* istanbul ignore next */
if (typeof define === 'function' && define.amd) {
define('error-stack-parser', ['stackframe'], factory);
} else if (true) {
module.exports = factory(__nccwpck_require__(5046));
} else {}
}(this, function ErrorStackParser(StackFrame) {
'use strict';
var FIREFOX_SAFARI_STACK_REGEXP = /(^|@)\S+:\d+/;
var CHROME_IE_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m;
var SAFARI_NATIVE_CODE_REGEXP = /^(eval@)?(\[native code])?$/;
return {
/**
* Given an Error object, extract the most information from it.
*
* @param {Error} error object
* @return {Array} of StackFrames
*/
parse: function ErrorStackParser$$parse(error) {
if (typeof error.stacktrace !== 'undefined' || typeof error['opera#sourceloc'] !== 'undefined') {
return this.parseOpera(error);
} else if (error.stack && error.stack.match(CHROME_IE_STACK_REGEXP)) {
return this.parseV8OrIE(error);
} else if (error.stack) {
return this.parseFFOrSafari(error);
} else {
throw new Error('Cannot parse given Error object');
}
},
// Separate line and column numbers from a string of the form: (URI:Line:Column)
extractLocation: function ErrorStackParser$$extractLocation(urlLike) {
// Fail-fast but return locations like "(native)"
if (urlLike.indexOf(':') === -1) {
return [urlLike];
}
var regExp = /(.+?)(?::(\d+))?(?::(\d+))?$/;
var parts = regExp.exec(urlLike.replace(/[()]/g, ''));
return [parts[1], parts[2] || undefined, parts[3] || undefined];
},
parseV8OrIE: function ErrorStackParser$$parseV8OrIE(error) {
var filtered = error.stack.split('\n').filter(function(line) {
return !!line.match(CHROME_IE_STACK_REGEXP);
}, this);
return filtered.map(function(line) {
if (line.indexOf('(eval ') > -1) {
// Throw away eval information until we implement stacktrace.js/stackframe#8
line = line.replace(/eval code/g, 'eval').replace(/(\(eval at [^()]*)|(,.*$)/g, '');
}
var sanitizedLine = line.replace(/^\s+/, '').replace(/\(eval code/g, '(').replace(/^.*?\s+/, '');
// capture and preseve the parenthesized location "(/foo/my bar.js:12:87)" in
// case it has spaces in it, as the string is split on \s+ later on
var location = sanitizedLine.match(/ (\(.+\)$)/);
// remove the parenthesized location from the line, if it was matched
sanitizedLine = location ? sanitizedLine.replace(location[0], '') : sanitizedLine;
// if a location was matched, pass it to extractLocation() otherwise pass all sanitizedLine
// because this line doesn't have function name
var locationParts = this.extractLocation(location ? location[1] : sanitizedLine);
var functionName = location && sanitizedLine || undefined;
var fileName = ['eval', '<anonymous>'].indexOf(locationParts[0]) > -1 ? undefined : locationParts[0];
return new StackFrame({
functionName: functionName,
fileName: fileName,
lineNumber: locationParts[1],
columnNumber: locationParts[2],
source: line
});
}, this);
},
parseFFOrSafari: function ErrorStackParser$$parseFFOrSafari(error) {
var filtered = error.stack.split('\n').filter(function(line) {
return !line.match(SAFARI_NATIVE_CODE_REGEXP);
}, this);
return filtered.map(function(line) {
// Throw away eval information until we implement stacktrace.js/stackframe#8
if (line.indexOf(' > eval') > -1) {
line = line.replace(/ line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g, ':$1');
}
if (line.indexOf('@') === -1 && line.indexOf(':') === -1) {
// Safari eval frames only have function names and nothing else
return new StackFrame({
functionName: line
});
} else {
var functionNameRegex = /((.*".+"[^@]*)?[^@]*)(?:@)/;
var matches = line.match(functionNameRegex);
var functionName = matches && matches[1] ? matches[1] : undefined;
var locationParts = this.extractLocation(line.replace(functionNameRegex, ''));
return new StackFrame({
functionName: functionName,
fileName: locationParts[0],
lineNumber: locationParts[1],
columnNumber: locationParts[2],
source: line
});
}
}, this);
},
parseOpera: function ErrorStackParser$$parseOpera(e) {
if (!e.stacktrace || (e.message.indexOf('\n') > -1 &&
e.message.split('\n').length > e.stacktrace.split('\n').length)) {
return this.parseOpera9(e);
} else if (!e.stack) {
return this.parseOpera10(e);
} else {
return this.parseOpera11(e);
}
},
parseOpera9: function ErrorStackParser$$parseOpera9(e) {
var lineRE = /Line (\d+).*script (?:in )?(\S+)/i;
var lines = e.message.split('\n');
var result = [];
for (var i = 2, len = lines.length; i < len; i += 2) {
var match = lineRE.exec(lines[i]);
if (match) {
result.push(new StackFrame({
fileName: match[2],
lineNumber: match[1],
source: lines[i]
}));
}
}
return result;
},
parseOpera10: function ErrorStackParser$$parseOpera10(e) {
var lineRE = /Line (\d+).*script (?:in )?(\S+)(?:: In function (\S+))?$/i;
var lines = e.stacktrace.split('\n');
var result = [];
for (var i = 0, len = lines.length; i < len; i += 2) {
var match = lineRE.exec(lines[i]);
if (match) {
result.push(
new StackFrame({
functionName: match[3] || undefined,
fileName: match[2],
lineNumber: match[1],
source: lines[i]
})
);
}
}
return result;
},
// Opera 10.65+ Error.stack very similar to FF/Safari
parseOpera11: function ErrorStackParser$$parseOpera11(error) {
var filtered = error.stack.split('\n').filter(function(line) {
return !!line.match(FIREFOX_SAFARI_STACK_REGEXP) && !line.match(/^Error created at/);
}, this);
return filtered.map(function(line) {
var tokens = line.split('@');
var locationParts = this.extractLocation(tokens.pop());
var functionCall = (tokens.shift() || '');
var functionName = functionCall
.replace(/<anonymous function(: (\w+))?>/, '$2')
.replace(/\([^)]*\)/g, '') || undefined;
var argsRaw;
if (functionCall.match(/\(([^)]*)\)/)) {
argsRaw = functionCall.replace(/^[^(]+\(([^)]*)\)$/, '$1');
}
var args = (argsRaw === undefined || argsRaw === '[arguments not available]') ?
undefined : argsRaw.split(',');
return new StackFrame({
functionName: functionName,
args: args,
fileName: locationParts[0],
lineNumber: locationParts[1],
columnNumber: locationParts[2],
source: line
});
}, this);
}
};
}));
/***/ }),
/***/ 1223:
@@ -12798,6 +13005,154 @@ function onceStrict (fn) {
}
/***/ }),
/***/ 5046:
/***/ (function(module) {
(function(root, factory) {
'use strict';
// Universal Module Definition (UMD) to support AMD, CommonJS/Node.js, Rhino, and browsers.
/* istanbul ignore next */
if (typeof define === 'function' && define.amd) {
define('stackframe', [], factory);
} else if (true) {
module.exports = factory();
} else {}
}(this, function() {
'use strict';
function _isNumber(n) {
return !isNaN(parseFloat(n)) && isFinite(n);
}
function _capitalize(str) {
return str.charAt(0).toUpperCase() + str.substring(1);
}
function _getter(p) {
return function() {
return this[p];
};
}
var booleanProps = ['isConstructor', 'isEval', 'isNative', 'isToplevel'];
var numericProps = ['columnNumber', 'lineNumber'];
var stringProps = ['fileName', 'functionName', 'source'];
var arrayProps = ['args'];
var objectProps = ['evalOrigin'];
var props = booleanProps.concat(numericProps, stringProps, arrayProps, objectProps);
function StackFrame(obj) {
if (!obj) return;
for (var i = 0; i < props.length; i++) {
if (obj[props[i]] !== undefined) {
this['set' + _capitalize(props[i])](obj[props[i]]);
}
}
}
StackFrame.prototype = {
getArgs: function() {
return this.args;
},
setArgs: function(v) {
if (Object.prototype.toString.call(v) !== '[object Array]') {
throw new TypeError('Args must be an Array');
}
this.args = v;
},
getEvalOrigin: function() {
return this.evalOrigin;
},
setEvalOrigin: function(v) {
if (v instanceof StackFrame) {
this.evalOrigin = v;
} else if (v instanceof Object) {
this.evalOrigin = new StackFrame(v);
} else {
throw new TypeError('Eval Origin must be an Object or StackFrame');
}
},
toString: function() {
var fileName = this.getFileName() || '';
var lineNumber = this.getLineNumber() || '';
var columnNumber = this.getColumnNumber() || '';
var functionName = this.getFunctionName() || '';
if (this.getIsEval()) {
if (fileName) {
return '[eval] (' + fileName + ':' + lineNumber + ':' + columnNumber + ')';
}
return '[eval]:' + lineNumber + ':' + columnNumber;
}
if (functionName) {
return functionName + ' (' + fileName + ':' + lineNumber + ':' + columnNumber + ')';
}
return fileName + ':' + lineNumber + ':' + columnNumber;
}
};
StackFrame.fromString = function StackFrame$$fromString(str) {
var argsStartIndex = str.indexOf('(');
var argsEndIndex = str.lastIndexOf(')');
var functionName = str.substring(0, argsStartIndex);
var args = str.substring(argsStartIndex + 1, argsEndIndex).split(',');
var locationString = str.substring(argsEndIndex + 1);
if (locationString.indexOf('@') === 0) {
var parts = /@(.+?)(?::(\d+))?(?::(\d+))?$/.exec(locationString, '');
var fileName = parts[1];
var lineNumber = parts[2];
var columnNumber = parts[3];
}
return new StackFrame({
functionName: functionName,
args: args || undefined,
fileName: fileName,
lineNumber: lineNumber || undefined,
columnNumber: columnNumber || undefined
});
};
for (var i = 0; i < booleanProps.length; i++) {
StackFrame.prototype['get' + _capitalize(booleanProps[i])] = _getter(booleanProps[i]);
StackFrame.prototype['set' + _capitalize(booleanProps[i])] = (function(p) {
return function(v) {
this[p] = Boolean(v);
};
})(booleanProps[i]);
}
for (var j = 0; j < numericProps.length; j++) {
StackFrame.prototype['get' + _capitalize(numericProps[j])] = _getter(numericProps[j]);
StackFrame.prototype['set' + _capitalize(numericProps[j])] = (function(p) {
return function(v) {
if (!_isNumber(v)) {
throw new TypeError(p + ' must be a Number');
}
this[p] = Number(v);
};
})(numericProps[j]);
}
for (var k = 0; k < stringProps.length; k++) {
StackFrame.prototype['get' + _capitalize(stringProps[k])] = _getter(stringProps[k]);
StackFrame.prototype['set' + _capitalize(stringProps[k])] = (function(p) {
return function(v) {
this[p] = String(v);
};
})(stringProps[k]);
}
return StackFrame;
}));
/***/ }),
/***/ 4294:
@@ -35742,6 +36097,7 @@ function wrappy (fn, cb) {
const core = __nccwpck_require__(2186)
const github = __nccwpck_require__(5438)
const { convertErrorToAnnotationProperties } = __nccwpck_require__(1507)
async function enablePagesSite({ githubToken }) {
const octokit = github.getOctokit(githubToken)
@@ -35785,12 +36141,12 @@ async function findOrCreatePagesSite({ githubToken, enablement = true }) {
} catch (error) {
if (!enablement) {
core.error(
'Get Pages site failed. Please verify that the repository has Pages enabled and configured to build using GitHub Actions, or consider exploring the `enablement` parameter for this action.',
error
`Get Pages site failed. Please verify that the repository has Pages enabled and configured to build using GitHub Actions, or consider exploring the \`enablement\` parameter for this action. Error: ${error.message}`,
convertErrorToAnnotationProperties(error)
)
throw error
}
core.warning('Get Pages site failed', error)
core.warning(`Get Pages site failed. Error: ${error.message}`, convertErrorToAnnotationProperties(error))
}
if (!pageObject && enablement) {
@@ -35798,7 +36154,7 @@ async function findOrCreatePagesSite({ githubToken, enablement = true }) {
try {
pageObject = await enablePagesSite({ githubToken })
} catch (error) {
core.error('Create Pages site failed', error)
core.error(`Create Pages site failed. Error: ${error.message}`, convertErrorToAnnotationProperties(error))
throw error
}
@@ -35808,7 +36164,7 @@ async function findOrCreatePagesSite({ githubToken, enablement = true }) {
try {
pageObject = await getPagesSite({ githubToken })
} catch (error) {
core.error('Get Pages site still failed', error)
core.error(`Get Pages site still failed. Error: ${error.message}`, convertErrorToAnnotationProperties(error))
throw error
}
}
@@ -36295,6 +36651,37 @@ function getContext() {
module.exports = { getContext }
/***/ }),
/***/ 1507:
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
const ErrorStackParser = __nccwpck_require__(9176)
// Convert an Error's stack into `@actions/core` toolkit AnnotationProperties:
// https://github.com/actions/toolkit/blob/ef77c9d60bdb03700d7758b0d04b88446e72a896/packages/core/src/core.ts#L36-L71
function convertErrorToAnnotationProperties(error, title = error.name) {
if (!(error instanceof Error)) {
throw new TypeError('error must be an instance of Error')
}
const stack = ErrorStackParser.parse(error)
const firstFrame = stack && stack.length > 0 ? stack[0] : null
if (!firstFrame) {
throw new Error('Error stack is empty or unparseable')
}
return {
title,
file: firstFrame.fileName,
startLine: firstFrame.lineNumber,
startColumn: firstFrame.columnNumber
}
}
module.exports = { convertErrorToAnnotationProperties }
/***/ }),
/***/ 7527:
@@ -36335,6 +36722,7 @@ module.exports = function removeTrailingSlash(str) {
const core = __nccwpck_require__(2186)
const { ConfigParser } = __nccwpck_require__(8395)
const removeTrailingSlash = __nccwpck_require__(9255)
const { convertErrorToAnnotationProperties } = __nccwpck_require__(1507)
const SUPPORTED_FILE_EXTENSIONS = ['.js', '.cjs', '.mjs']
@@ -36422,13 +36810,13 @@ function setPagesConfig({ staticSiteGenerator, generatorConfigFile, siteUrl }) {
core.warning(
`Unsupported configuration file extension. Currently supported extensions: ${SUPPORTED_FILE_EXTENSIONS.map(
ext => JSON.stringify(ext)
).join(', ')}`,
error
).join(', ')}. Error: ${error.message}`,
convertErrorToAnnotationProperties(error)
)
} else {
core.warning(
`We were unable to determine how to inject the site metadata into your config. Generated URLs may be incorrect. The base URL for this site should be ${siteUrl}. Please ensure your framework is configured to generate relative links appropriately.`,
error
`We were unable to determine how to inject the site metadata into your config. Generated URLs may be incorrect. The base URL for this site should be ${siteUrl}. Please ensure your framework is configured to generate relative links appropriately. Error: ${error.message}`,
convertErrorToAnnotationProperties(error)
)
}
}

2
dist/index.js.map vendored

File diff suppressed because one or more lines are too long

46
dist/licenses.txt vendored
View File

@@ -513,6 +513,29 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
error-stack-parser
MIT
Copyright (c) 2017 Eric Wendelin and other contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
eslint-visitor-keys
Apache-2.0
Apache License
@@ -766,6 +789,29 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
stackframe
MIT
Copyright (c) 2017 Eric Wendelin and other contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
tunnel
MIT
The MIT License (MIT)

28
package-lock.json generated
View File

@@ -11,9 +11,11 @@
"dependencies": {
"@actions/core": "^1.10.1",
"@actions/github": "^6.0.0",
"error-stack-parser": "^2.1.4",
"espree": "^9.6.1"
},
"devDependencies": {
"@octokit/request-error": "^5.0.1",
"@vercel/ncc": "^0.38.1",
"eslint": "^8.57.0",
"eslint-config-prettier": "^8.8.0",
@@ -2793,6 +2795,14 @@
"is-arrayish": "^0.2.1"
}
},
"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/es-abstract": {
"version": "1.21.1",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.1.tgz",
@@ -6145,6 +6155,11 @@
"node": ">=10"
}
},
"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/stop-iteration-iterator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz",
@@ -8805,6 +8820,14 @@
"is-arrayish": "^0.2.1"
}
},
"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"
}
},
"es-abstract": {
"version": "1.21.1",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.1.tgz",
@@ -11242,6 +11265,11 @@
"escape-string-regexp": "^2.0.0"
}
},
"stackframe": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
"integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="
},
"stop-iteration-iterator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz",

View File

@@ -26,9 +26,11 @@
"dependencies": {
"@actions/core": "^1.10.1",
"@actions/github": "^6.0.0",
"error-stack-parser": "^2.1.4",
"espree": "^9.6.1"
},
"devDependencies": {
"@octokit/request-error": "^5.0.1",
"@vercel/ncc": "^0.38.1",
"eslint": "^8.57.0",
"eslint-config-prettier": "^8.8.0",

View File

@@ -1,5 +1,6 @@
const core = require('@actions/core')
const github = require('@actions/github')
const { convertErrorToAnnotationProperties } = require('./error-utils')
async function enablePagesSite({ githubToken }) {
const octokit = github.getOctokit(githubToken)
@@ -43,12 +44,12 @@ async function findOrCreatePagesSite({ githubToken, enablement = true }) {
} catch (error) {
if (!enablement) {
core.error(
'Get Pages site failed. Please verify that the repository has Pages enabled and configured to build using GitHub Actions, or consider exploring the `enablement` parameter for this action.',
error
`Get Pages site failed. Please verify that the repository has Pages enabled and configured to build using GitHub Actions, or consider exploring the \`enablement\` parameter for this action. Error: ${error.message}`,
convertErrorToAnnotationProperties(error)
)
throw error
}
core.warning('Get Pages site failed', error)
core.warning(`Get Pages site failed. Error: ${error.message}`, convertErrorToAnnotationProperties(error))
}
if (!pageObject && enablement) {
@@ -56,7 +57,7 @@ async function findOrCreatePagesSite({ githubToken, enablement = true }) {
try {
pageObject = await enablePagesSite({ githubToken })
} catch (error) {
core.error('Create Pages site failed', error)
core.error(`Create Pages site failed. Error: ${error.message}`, convertErrorToAnnotationProperties(error))
throw error
}
@@ -66,7 +67,7 @@ async function findOrCreatePagesSite({ githubToken, enablement = true }) {
try {
pageObject = await getPagesSite({ githubToken })
} catch (error) {
core.error('Get Pages site still failed', error)
core.error(`Get Pages site still failed. Error: ${error.message}`, convertErrorToAnnotationProperties(error))
throw error
}
}

View File

@@ -1,9 +1,24 @@
const core = require('@actions/core')
const apiClient = require('./api-client')
const { RequestError } = require('@octokit/request-error')
const mockGetPages = jest.fn()
const mockCreatePagesSite = jest.fn()
const generateRequestError = statusCode => {
const fakeRequest = { headers: {}, url: '/' }
const fakeResponse = { status: statusCode }
let message = 'Oops'
if (statusCode === 404) {
message = 'Not Found'
}
if (statusCode === 409) {
message = 'Too Busy'
}
const error = new RequestError(message, statusCode, { request: fakeRequest, response: fakeResponse })
return error
}
jest.mock('@actions/github', () => ({
context: {
repo: {
@@ -48,7 +63,7 @@ describe('apiClient', () => {
})
it('handles a 409 response when the page already exists', async () => {
mockCreatePagesSite.mockImplementationOnce(() => Promise.reject({ response: { status: 409 } }))
mockCreatePagesSite.mockImplementationOnce(() => Promise.reject(generateRequestError(409)))
// Simply assert that no error is raised
const result = await apiClient.enablePagesSite({
@@ -59,7 +74,7 @@ describe('apiClient', () => {
})
it('re-raises errors on failure status codes', async () => {
mockCreatePagesSite.mockImplementationOnce(() => Promise.reject({ response: { status: 404 } }))
mockCreatePagesSite.mockImplementationOnce(() => Promise.reject(generateRequestError(404)))
let erred = false
try {
@@ -86,7 +101,7 @@ describe('apiClient', () => {
})
it('re-raises errors on failure status codes', async () => {
mockGetPages.mockImplementationOnce(() => Promise.reject({ response: { status: 404 } }))
mockGetPages.mockImplementationOnce(() => Promise.reject(generateRequestError(404)))
let erred = false
try {
@@ -105,7 +120,7 @@ describe('apiClient', () => {
it('does not make a request to create a page if it already exists', async () => {
const PAGE_OBJECT = { html_url: 'https://actions.github.io/is-awesome/' }
mockGetPages.mockImplementationOnce(() => Promise.resolve({ status: 200, data: PAGE_OBJECT }))
mockCreatePagesSite.mockImplementationOnce(() => Promise.reject({ response: { status: 404 } }))
mockCreatePagesSite.mockImplementationOnce(() => Promise.reject(generateRequestError(404)))
const result = await apiClient.findOrCreatePagesSite({
githubToken: GITHUB_TOKEN
@@ -117,7 +132,7 @@ describe('apiClient', () => {
it('makes request to create a page by default if it does not exist', async () => {
const PAGE_OBJECT = { html_url: 'https://actions.github.io/is-awesome/' }
mockGetPages.mockImplementationOnce(() => Promise.reject({ response: { status: 404 } }))
mockGetPages.mockImplementationOnce(() => Promise.reject(generateRequestError(404)))
mockCreatePagesSite.mockImplementationOnce(() => Promise.resolve({ status: 201, data: PAGE_OBJECT }))
const result = await apiClient.findOrCreatePagesSite({
@@ -130,7 +145,7 @@ describe('apiClient', () => {
it('makes a request to create a page when explicitly enabled if it does not exist', async () => {
const PAGE_OBJECT = { html_url: 'https://actions.github.io/is-awesome/' }
mockGetPages.mockImplementationOnce(() => Promise.reject({ response: { status: 404 } }))
mockGetPages.mockImplementationOnce(() => Promise.reject(generateRequestError(404)))
mockCreatePagesSite.mockImplementationOnce(() => Promise.resolve({ status: 201, data: PAGE_OBJECT }))
const result = await apiClient.findOrCreatePagesSite({
@@ -143,8 +158,8 @@ describe('apiClient', () => {
})
it('does not make a request to create a page when explicitly disabled even if it does not exist', async () => {
mockGetPages.mockImplementationOnce(() => Promise.reject({ response: { status: 404 } }))
mockCreatePagesSite.mockImplementationOnce(() => Promise.reject({ response: { status: 500 } })) // just so they both aren't 404
mockGetPages.mockImplementationOnce(() => Promise.reject(generateRequestError(404)))
mockCreatePagesSite.mockImplementationOnce(() => Promise.reject(generateRequestError(500))) // just so they both aren't 404
let erred = false
try {
@@ -163,8 +178,8 @@ describe('apiClient', () => {
})
it('does not make a second request to get page if create fails for reason other than existence', async () => {
mockGetPages.mockImplementationOnce(() => Promise.reject({ response: { status: 404 } }))
mockCreatePagesSite.mockImplementationOnce(() => Promise.reject({ response: { status: 500 } })) // just so they both aren't 404
mockGetPages.mockImplementationOnce(() => Promise.reject(generateRequestError(404)))
mockCreatePagesSite.mockImplementationOnce(() => Promise.reject(generateRequestError(500))) // just so they both aren't 404
let erred = false
try {
@@ -184,9 +199,9 @@ describe('apiClient', () => {
it('makes second request to get page if create fails because of existence', async () => {
const PAGE_OBJECT = { html_url: 'https://actions.github.io/is-awesome/' }
mockGetPages
.mockImplementationOnce(() => Promise.reject({ response: { status: 404 } }))
.mockImplementationOnce(() => Promise.reject(generateRequestError(404)))
.mockImplementationOnce(() => Promise.resolve({ status: 200, data: PAGE_OBJECT }))
mockCreatePagesSite.mockImplementationOnce(() => Promise.reject({ response: { status: 409 } }))
mockCreatePagesSite.mockImplementationOnce(() => Promise.reject(generateRequestError(409)))
const result = await apiClient.findOrCreatePagesSite({
githubToken: GITHUB_TOKEN

24
src/error-utils.js Normal file
View File

@@ -0,0 +1,24 @@
const ErrorStackParser = require('error-stack-parser')
// Convert an Error's stack into `@actions/core` toolkit AnnotationProperties:
// https://github.com/actions/toolkit/blob/ef77c9d60bdb03700d7758b0d04b88446e72a896/packages/core/src/core.ts#L36-L71
function convertErrorToAnnotationProperties(error, title = error.name) {
if (!(error instanceof Error)) {
throw new TypeError('error must be an instance of Error')
}
const stack = ErrorStackParser.parse(error)
const firstFrame = stack && stack.length > 0 ? stack[0] : null
if (!firstFrame) {
throw new Error('Error stack is empty or unparseable')
}
return {
title,
file: firstFrame.fileName,
startLine: firstFrame.lineNumber,
startColumn: firstFrame.columnNumber
}
}
module.exports = { convertErrorToAnnotationProperties }

38
src/error-utils.test.js Normal file
View File

@@ -0,0 +1,38 @@
const { convertErrorToAnnotationProperties } = require('./error-utils')
describe('error-utils', () => {
describe('convertErrorToAnnotationProperties', () => {
it('throws a TypeError if the first argument is not an Error instance', () => {
expect(() => convertErrorToAnnotationProperties('not an Error')).toThrow(
TypeError,
'error must be an instance of Error'
)
})
it('throws an Error if the first argument is an Error instance without a parseable stack', () => {
const error = new Error('Test error')
error.stack = ''
expect(() => convertErrorToAnnotationProperties(error)).toThrow(Error, 'Error stack is empty or unparseable')
})
it('returns an AnnotationProperties-compatible object', () => {
const result = convertErrorToAnnotationProperties(new TypeError('Test error'))
expect(result).toEqual({
title: 'TypeError',
file: __filename,
startLine: expect.any(Number),
startColumn: expect.any(Number)
})
})
it('returns an AnnotationProperties-compatible object with a custom title', () => {
const result = convertErrorToAnnotationProperties(new TypeError('Test error'), 'custom title')
expect(result).toEqual({
title: 'custom title',
file: __filename,
startLine: expect.any(Number),
startColumn: expect.any(Number)
})
})
})
})

View File

@@ -1,6 +1,7 @@
const core = require('@actions/core')
const { ConfigParser } = require('./config-parser')
const removeTrailingSlash = require('./remove-trailing-slash')
const { convertErrorToAnnotationProperties } = require('./error-utils')
const SUPPORTED_FILE_EXTENSIONS = ['.js', '.cjs', '.mjs']
@@ -88,13 +89,13 @@ function setPagesConfig({ staticSiteGenerator, generatorConfigFile, siteUrl }) {
core.warning(
`Unsupported configuration file extension. Currently supported extensions: ${SUPPORTED_FILE_EXTENSIONS.map(
ext => JSON.stringify(ext)
).join(', ')}`,
error
).join(', ')}. Error: ${error.message}`,
convertErrorToAnnotationProperties(error)
)
} else {
core.warning(
`We were unable to determine how to inject the site metadata into your config. Generated URLs may be incorrect. The base URL for this site should be ${siteUrl}. Please ensure your framework is configured to generate relative links appropriately.`,
error
`We were unable to determine how to inject the site metadata into your config. Generated URLs may be incorrect. The base URL for this site should be ${siteUrl}. Please ensure your framework is configured to generate relative links appropriately. Error: ${error.message}`,
convertErrorToAnnotationProperties(error)
)
}
}