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

File tree

10 files changed

+574
-31
lines changed

10 files changed

+574
-31
lines changed

‎dist/index.js‎

Lines changed: 397 additions & 9 deletions
Large diffs are not rendered by default.

‎dist/index.js.map‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎dist/licenses.txt‎

Lines changed: 46 additions & 0 deletions
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‎

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 2 additions & 0 deletions
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‎

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

45
asyncfunctionenablePagesSite({ githubToken }){
56
constoctokit=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
throwerror
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=awaitenablePagesSite({ 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
throwerror
6162
}
6263

@@ -66,7 +67,7 @@ async function findOrCreatePagesSite({githubToken, enablement = true }){
6667
try{
6768
pageObject=awaitgetPagesSite({ 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
throwerror
7172
}
7273
}

‎src/api-client.test.js‎

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

45
constmockGetPages=jest.fn()
56
constmockCreatePagesSite=jest.fn()
67

8+
constgenerateRequestError=statusCode=>{
9+
constfakeRequest={headers: {},url: '/'}
10+
constfakeResponse={status: statusCode}
11+
letmessage='Oops'
12+
if(statusCode===404){
13+
message='Not Found'
14+
}
15+
if(statusCode===409){
16+
message='Too Busy'
17+
}
18+
consterror=newRequestError(message,statusCode,{request: fakeRequest,response: fakeResponse})
19+
returnerror
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
constresult=awaitapiClient.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
leterred=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
leterred=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
constPAGE_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
constresult=awaitapiClient.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
constPAGE_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
constresult=awaitapiClient.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
constPAGE_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
constresult=awaitapiClient.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
leterred=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
leterred=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
constPAGE_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
constresult=awaitapiClient.findOrCreatePagesSite({
192207
githubToken: GITHUB_TOKEN

‎src/error-utils.js‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
constErrorStackParser=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+
functionconvertErrorToAnnotationProperties(error,title=error.name){
6+
if(!(errorinstanceofError)){
7+
thrownewTypeError('error must be an instance of Error')
8+
}
9+
10+
conststack=ErrorStackParser.parse(error)
11+
constfirstFrame=stack&&stack.length>0 ? stack[0] : null
12+
if(!firstFrame){
13+
thrownewError('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‎

Lines changed: 38 additions & 0 deletions
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+
consterror=newError('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+
constresult=convertErrorToAnnotationProperties(newTypeError('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+
constresult=convertErrorToAnnotationProperties(newTypeError('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‎

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

56
constSUPPORTED_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)