Skip to content

Commit 7781abd

Browse files
authored
Merge pull request #138 from actions/error-utils
Convert errors into Actions-compatible logging with annotations
2 parents b85f2a6 + fc47e3c commit 7781abd

10 files changed

+574
-31
lines changed

dist/index.js

+397-9
Large diffs are not rendered by default.

dist/index.js.map

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/licenses.txt

+46
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,29 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
513513
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
514514

515515

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

768791

792+
stackframe
793+
MIT
794+
Copyright (c) 2017 Eric Wendelin and other contributors
795+
796+
Permission is hereby granted, free of charge, to any person obtaining a copy of
797+
this software and associated documentation files (the "Software"), to deal in
798+
the Software without restriction, including without limitation the rights to
799+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
800+
of the Software, and to permit persons to whom the Software is furnished to do
801+
so, subject to the following conditions:
802+
803+
The above copyright notice and this permission notice shall be included in all
804+
copies or substantial portions of the Software.
805+
806+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
807+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
808+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
809+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
810+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
811+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
812+
SOFTWARE.
813+
814+
769815
tunnel
770816
MIT
771817
The MIT License (MIT)

package-lock.json

+28
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@
2626
"dependencies": {
2727
"@actions/core": "^1.10.1",
2828
"@actions/github": "^6.0.0",
29+
"error-stack-parser": "^2.1.4",
2930
"espree": "^9.6.1"
3031
},
3132
"devDependencies": {
33+
"@octokit/request-error": "^5.0.1",
3234
"@vercel/ncc": "^0.38.1",
3335
"eslint": "^8.57.0",
3436
"eslint-config-prettier": "^8.8.0",

src/api-client.js

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const core = require('@actions/core')
22
const github = require('@actions/github')
3+
const { convertErrorToAnnotationProperties } = require('./error-utils')
34

45
async function enablePagesSite({ githubToken }) {
56
const octokit = github.getOctokit(githubToken)
@@ -43,20 +44,20 @@ async function findOrCreatePagesSite({ githubToken, enablement = true }) {
4344
} catch (error) {
4445
if (!enablement) {
4546
core.error(
46-
'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.',
47-
error
47+
`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}`,
48+
convertErrorToAnnotationProperties(error)
4849
)
4950
throw error
5051
}
51-
core.warning('Get Pages site failed', error)
52+
core.warning(`Get Pages site failed. Error: ${error.message}`, convertErrorToAnnotationProperties(error))
5253
}
5354

5455
if (!pageObject && enablement) {
5556
// Create a new Pages site if one doesn't exist
5657
try {
5758
pageObject = await enablePagesSite({ githubToken })
5859
} catch (error) {
59-
core.error('Create Pages site failed', error)
60+
core.error(`Create Pages site failed. Error: ${error.message}`, convertErrorToAnnotationProperties(error))
6061
throw error
6162
}
6263

@@ -66,7 +67,7 @@ async function findOrCreatePagesSite({ githubToken, enablement = true }) {
6667
try {
6768
pageObject = await getPagesSite({ githubToken })
6869
} catch (error) {
69-
core.error('Get Pages site still failed', error)
70+
core.error(`Get Pages site still failed. Error: ${error.message}`, convertErrorToAnnotationProperties(error))
7071
throw error
7172
}
7273
}

src/api-client.test.js

+27-12
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
11
const core = require('@actions/core')
22
const apiClient = require('./api-client')
3+
const { RequestError } = require('@octokit/request-error')
34

45
const mockGetPages = jest.fn()
56
const mockCreatePagesSite = jest.fn()
67

8+
const generateRequestError = statusCode => {
9+
const fakeRequest = { headers: {}, url: '/' }
10+
const fakeResponse = { status: statusCode }
11+
let message = 'Oops'
12+
if (statusCode === 404) {
13+
message = 'Not Found'
14+
}
15+
if (statusCode === 409) {
16+
message = 'Too Busy'
17+
}
18+
const error = new RequestError(message, statusCode, { request: fakeRequest, response: fakeResponse })
19+
return error
20+
}
21+
722
jest.mock('@actions/github', () => ({
823
context: {
924
repo: {
@@ -48,7 +63,7 @@ describe('apiClient', () => {
4863
})
4964

5065
it('handles a 409 response when the page already exists', async () => {
51-
mockCreatePagesSite.mockImplementationOnce(() => Promise.reject({ response: { status: 409 } }))
66+
mockCreatePagesSite.mockImplementationOnce(() => Promise.reject(generateRequestError(409)))
5267

5368
// Simply assert that no error is raised
5469
const result = await apiClient.enablePagesSite({
@@ -59,7 +74,7 @@ describe('apiClient', () => {
5974
})
6075

6176
it('re-raises errors on failure status codes', async () => {
62-
mockCreatePagesSite.mockImplementationOnce(() => Promise.reject({ response: { status: 404 } }))
77+
mockCreatePagesSite.mockImplementationOnce(() => Promise.reject(generateRequestError(404)))
6378

6479
let erred = false
6580
try {
@@ -86,7 +101,7 @@ describe('apiClient', () => {
86101
})
87102

88103
it('re-raises errors on failure status codes', async () => {
89-
mockGetPages.mockImplementationOnce(() => Promise.reject({ response: { status: 404 } }))
104+
mockGetPages.mockImplementationOnce(() => Promise.reject(generateRequestError(404)))
90105

91106
let erred = false
92107
try {
@@ -105,7 +120,7 @@ describe('apiClient', () => {
105120
it('does not make a request to create a page if it already exists', async () => {
106121
const PAGE_OBJECT = { html_url: 'https://actions.github.io/is-awesome/' }
107122
mockGetPages.mockImplementationOnce(() => Promise.resolve({ status: 200, data: PAGE_OBJECT }))
108-
mockCreatePagesSite.mockImplementationOnce(() => Promise.reject({ response: { status: 404 } }))
123+
mockCreatePagesSite.mockImplementationOnce(() => Promise.reject(generateRequestError(404)))
109124

110125
const result = await apiClient.findOrCreatePagesSite({
111126
githubToken: GITHUB_TOKEN
@@ -117,7 +132,7 @@ describe('apiClient', () => {
117132

118133
it('makes request to create a page by default if it does not exist', async () => {
119134
const PAGE_OBJECT = { html_url: 'https://actions.github.io/is-awesome/' }
120-
mockGetPages.mockImplementationOnce(() => Promise.reject({ response: { status: 404 } }))
135+
mockGetPages.mockImplementationOnce(() => Promise.reject(generateRequestError(404)))
121136
mockCreatePagesSite.mockImplementationOnce(() => Promise.resolve({ status: 201, data: PAGE_OBJECT }))
122137

123138
const result = await apiClient.findOrCreatePagesSite({
@@ -130,7 +145,7 @@ describe('apiClient', () => {
130145

131146
it('makes a request to create a page when explicitly enabled if it does not exist', async () => {
132147
const PAGE_OBJECT = { html_url: 'https://actions.github.io/is-awesome/' }
133-
mockGetPages.mockImplementationOnce(() => Promise.reject({ response: { status: 404 } }))
148+
mockGetPages.mockImplementationOnce(() => Promise.reject(generateRequestError(404)))
134149
mockCreatePagesSite.mockImplementationOnce(() => Promise.resolve({ status: 201, data: PAGE_OBJECT }))
135150

136151
const result = await apiClient.findOrCreatePagesSite({
@@ -143,8 +158,8 @@ describe('apiClient', () => {
143158
})
144159

145160
it('does not make a request to create a page when explicitly disabled even if it does not exist', async () => {
146-
mockGetPages.mockImplementationOnce(() => Promise.reject({ response: { status: 404 } }))
147-
mockCreatePagesSite.mockImplementationOnce(() => Promise.reject({ response: { status: 500 } })) // just so they both aren't 404
161+
mockGetPages.mockImplementationOnce(() => Promise.reject(generateRequestError(404)))
162+
mockCreatePagesSite.mockImplementationOnce(() => Promise.reject(generateRequestError(500))) // just so they both aren't 404
148163

149164
let erred = false
150165
try {
@@ -163,8 +178,8 @@ describe('apiClient', () => {
163178
})
164179

165180
it('does not make a second request to get page if create fails for reason other than existence', async () => {
166-
mockGetPages.mockImplementationOnce(() => Promise.reject({ response: { status: 404 } }))
167-
mockCreatePagesSite.mockImplementationOnce(() => Promise.reject({ response: { status: 500 } })) // just so they both aren't 404
181+
mockGetPages.mockImplementationOnce(() => Promise.reject(generateRequestError(404)))
182+
mockCreatePagesSite.mockImplementationOnce(() => Promise.reject(generateRequestError(500))) // just so they both aren't 404
168183

169184
let erred = false
170185
try {
@@ -184,9 +199,9 @@ describe('apiClient', () => {
184199
it('makes second request to get page if create fails because of existence', async () => {
185200
const PAGE_OBJECT = { html_url: 'https://actions.github.io/is-awesome/' }
186201
mockGetPages
187-
.mockImplementationOnce(() => Promise.reject({ response: { status: 404 } }))
202+
.mockImplementationOnce(() => Promise.reject(generateRequestError(404)))
188203
.mockImplementationOnce(() => Promise.resolve({ status: 200, data: PAGE_OBJECT }))
189-
mockCreatePagesSite.mockImplementationOnce(() => Promise.reject({ response: { status: 409 } }))
204+
mockCreatePagesSite.mockImplementationOnce(() => Promise.reject(generateRequestError(409)))
190205

191206
const result = await apiClient.findOrCreatePagesSite({
192207
githubToken: GITHUB_TOKEN

src/error-utils.js

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const ErrorStackParser = require('error-stack-parser')
2+
3+
// Convert an Error's stack into `@actions/core` toolkit AnnotationProperties:
4+
// https://github.com/actions/toolkit/blob/ef77c9d60bdb03700d7758b0d04b88446e72a896/packages/core/src/core.ts#L36-L71
5+
function convertErrorToAnnotationProperties(error, title = error.name) {
6+
if (!(error instanceof Error)) {
7+
throw new TypeError('error must be an instance of Error')
8+
}
9+
10+
const stack = ErrorStackParser.parse(error)
11+
const firstFrame = stack && stack.length > 0 ? stack[0] : null
12+
if (!firstFrame) {
13+
throw new Error('Error stack is empty or unparseable')
14+
}
15+
16+
return {
17+
title,
18+
file: firstFrame.fileName,
19+
startLine: firstFrame.lineNumber,
20+
startColumn: firstFrame.columnNumber
21+
}
22+
}
23+
24+
module.exports = { convertErrorToAnnotationProperties }

src/error-utils.test.js

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
const { convertErrorToAnnotationProperties } = require('./error-utils')
2+
3+
describe('error-utils', () => {
4+
describe('convertErrorToAnnotationProperties', () => {
5+
it('throws a TypeError if the first argument is not an Error instance', () => {
6+
expect(() => convertErrorToAnnotationProperties('not an Error')).toThrow(
7+
TypeError,
8+
'error must be an instance of Error'
9+
)
10+
})
11+
12+
it('throws an Error if the first argument is an Error instance without a parseable stack', () => {
13+
const error = new Error('Test error')
14+
error.stack = ''
15+
expect(() => convertErrorToAnnotationProperties(error)).toThrow(Error, 'Error stack is empty or unparseable')
16+
})
17+
18+
it('returns an AnnotationProperties-compatible object', () => {
19+
const result = convertErrorToAnnotationProperties(new TypeError('Test error'))
20+
expect(result).toEqual({
21+
title: 'TypeError',
22+
file: __filename,
23+
startLine: expect.any(Number),
24+
startColumn: expect.any(Number)
25+
})
26+
})
27+
28+
it('returns an AnnotationProperties-compatible object with a custom title', () => {
29+
const result = convertErrorToAnnotationProperties(new TypeError('Test error'), 'custom title')
30+
expect(result).toEqual({
31+
title: 'custom title',
32+
file: __filename,
33+
startLine: expect.any(Number),
34+
startColumn: expect.any(Number)
35+
})
36+
})
37+
})
38+
})

src/set-pages-config.js

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const core = require('@actions/core')
22
const { ConfigParser } = require('./config-parser')
33
const removeTrailingSlash = require('./remove-trailing-slash')
4+
const { convertErrorToAnnotationProperties } = require('./error-utils')
45

56
const SUPPORTED_FILE_EXTENSIONS = ['.js', '.cjs', '.mjs']
67

@@ -88,13 +89,13 @@ function setPagesConfig({ staticSiteGenerator, generatorConfigFile, siteUrl }) {
8889
core.warning(
8990
`Unsupported configuration file extension. Currently supported extensions: ${SUPPORTED_FILE_EXTENSIONS.map(
9091
ext => JSON.stringify(ext)
91-
).join(', ')}`,
92-
error
92+
).join(', ')}. Error: ${error.message}`,
93+
convertErrorToAnnotationProperties(error)
9394
)
9495
} else {
9596
core.warning(
96-
`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.`,
97-
error
97+
`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}`,
98+
convertErrorToAnnotationProperties(error)
9899
)
99100
}
100101
}

0 commit comments

Comments
 (0)