@jsenv/github-check-run 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -0
- package/package.json +40 -0
- package/src/internal/github_rest_api.js +117 -0
- package/src/main.js +2 -0
- package/src/read_github_workflow_env.js +28 -0
- package/src/start_github_check_run.js +201 -0
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# GitHub check run [](https://www.npmjs.com/package/@jsenv/github-check-run)
|
|
2
|
+
|
|
3
|
+
```js
|
|
4
|
+
import { startGithubCheckRun } from "@jsenv/github-check-run";
|
|
5
|
+
|
|
6
|
+
const values = [1_000, 2_000, 4_000];
|
|
7
|
+
|
|
8
|
+
const githubCheck = await startGithubCheckRun({
|
|
9
|
+
githubToken: "",
|
|
10
|
+
repositoryOwner: "dmail",
|
|
11
|
+
repositoryName: "repo-name",
|
|
12
|
+
commitSha: "",
|
|
13
|
+
checkName: "demo",
|
|
14
|
+
checkTitle: "Running demo",
|
|
15
|
+
checkSummary: `preparing execution of ${values.length} timeouts`,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
let index = 0;
|
|
20
|
+
for (const value of values) {
|
|
21
|
+
githubCheck.progress({
|
|
22
|
+
summary: `executing timeout ${index + 1}/${values.length}`,
|
|
23
|
+
});
|
|
24
|
+
await new Promise((resolve) => {
|
|
25
|
+
setTimeout(resolve, value);
|
|
26
|
+
});
|
|
27
|
+
index++;
|
|
28
|
+
}
|
|
29
|
+
await githubCheck.pass({
|
|
30
|
+
summary: `${values.length} timeout execution done`,
|
|
31
|
+
});
|
|
32
|
+
} catch (e) {
|
|
33
|
+
await githubCheck.fail({
|
|
34
|
+
summary: `error while executing timeout number ${index + 1}: ${e.stack}`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
If the code is executed inside a Github workflow, several parameters can be retrieved using `readGitHubWorkflowEnv`:
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
import {
|
|
43
|
+
startGithubCheckRun,
|
|
44
|
+
readGitHubWorkflowEnv,
|
|
45
|
+
} from "@jsenv/github-check-run";
|
|
46
|
+
|
|
47
|
+
const githubCheck = await startGithubCheckRun({
|
|
48
|
+
...readGitHubWorkflowEnv(),
|
|
49
|
+
checkName: "demo",
|
|
50
|
+
checkTitle: "Running demo",
|
|
51
|
+
checkSummary: `Executing ${values.length} setTimeouts`,
|
|
52
|
+
});
|
|
53
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jsenv/github-check-run",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Interact with GitHub check rest API",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "dmail",
|
|
8
|
+
"email": "dmaillard06@gmail.com",
|
|
9
|
+
"url": "https://twitter.com/damienmaillard"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/jsenv/workflow",
|
|
14
|
+
"directory": "packages/github-check-run"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18.1.0"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"type": "module",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"import": "./src/main.js"
|
|
26
|
+
},
|
|
27
|
+
"./*": "./*"
|
|
28
|
+
},
|
|
29
|
+
"main": "./src/main.js",
|
|
30
|
+
"files": [
|
|
31
|
+
"/src/"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test": "node ./scripts/test.mjs"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@jsenv/fetch": "1.1.40",
|
|
38
|
+
"@jsenv/log": "3.4.0"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { fetchUrl } from "@jsenv/fetch";
|
|
2
|
+
|
|
3
|
+
export const GET = ({ url, githubToken, headers }) => {
|
|
4
|
+
return sendHttpRequest({
|
|
5
|
+
url,
|
|
6
|
+
method: "GET",
|
|
7
|
+
headers: {
|
|
8
|
+
...tokenToHeaders(githubToken),
|
|
9
|
+
headers,
|
|
10
|
+
},
|
|
11
|
+
responseStatusHandlers: {
|
|
12
|
+
200: async (response) => {
|
|
13
|
+
const json = await response.json();
|
|
14
|
+
return json;
|
|
15
|
+
},
|
|
16
|
+
404: () => null,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const POST = ({ url, body, githubToken, headers }) => {
|
|
22
|
+
return sendHttpRequest({
|
|
23
|
+
url,
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: {
|
|
26
|
+
...tokenToHeaders(githubToken),
|
|
27
|
+
headers,
|
|
28
|
+
},
|
|
29
|
+
body: JSON.stringify(body),
|
|
30
|
+
responseStatusHandlers: {
|
|
31
|
+
201: async (response) => {
|
|
32
|
+
const json = await response.json();
|
|
33
|
+
return json;
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const PATCH = ({ signal, url, body, githubToken, headers }) => {
|
|
40
|
+
return sendHttpRequest({
|
|
41
|
+
signal,
|
|
42
|
+
url,
|
|
43
|
+
method: "PATCH",
|
|
44
|
+
headers: {
|
|
45
|
+
...tokenToHeaders(githubToken),
|
|
46
|
+
headers,
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify(body),
|
|
49
|
+
responseStatusHandlers: {
|
|
50
|
+
200: async (response) => {
|
|
51
|
+
const json = await response.json();
|
|
52
|
+
return json;
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const tokenToHeaders = (token) => {
|
|
59
|
+
if (!token) {
|
|
60
|
+
throw new Error(`missing token, request will not be authorized.`);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
authorization: `token ${token}`,
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const sendHttpRequest = async ({
|
|
68
|
+
signal,
|
|
69
|
+
url,
|
|
70
|
+
method,
|
|
71
|
+
headers,
|
|
72
|
+
body,
|
|
73
|
+
responseStatusHandlers = {},
|
|
74
|
+
}) => {
|
|
75
|
+
let response;
|
|
76
|
+
try {
|
|
77
|
+
response = await fetchUrl(url, {
|
|
78
|
+
signal,
|
|
79
|
+
method,
|
|
80
|
+
headers: {
|
|
81
|
+
...(typeof body === "undefined"
|
|
82
|
+
? {}
|
|
83
|
+
: { "content-length": Buffer.byteLength(body) }),
|
|
84
|
+
...headers,
|
|
85
|
+
},
|
|
86
|
+
body,
|
|
87
|
+
});
|
|
88
|
+
} catch (error) {
|
|
89
|
+
throw new Error(`network error during request.
|
|
90
|
+
--- request method ---
|
|
91
|
+
${method}
|
|
92
|
+
--- request url ---
|
|
93
|
+
${url}
|
|
94
|
+
--- error stack ---
|
|
95
|
+
${error.stack}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const { status } = response;
|
|
99
|
+
const responseStatusHandler = responseStatusHandlers[status];
|
|
100
|
+
if (responseStatusHandler) {
|
|
101
|
+
return responseStatusHandler(response);
|
|
102
|
+
}
|
|
103
|
+
const responseBodyAsJson = await response.json();
|
|
104
|
+
const error = new Error(`unexpected response status.
|
|
105
|
+
--- response status ---
|
|
106
|
+
${response.status}
|
|
107
|
+
--- expected response status ---
|
|
108
|
+
${Object.keys(responseStatusHandlers).join(", ")}
|
|
109
|
+
--- request method ---
|
|
110
|
+
${method}
|
|
111
|
+
--- request url ---
|
|
112
|
+
${url}
|
|
113
|
+
--- response json ---
|
|
114
|
+
${(JSON.stringify(responseBodyAsJson), null, " ")}`);
|
|
115
|
+
error.responseStatus = status;
|
|
116
|
+
throw error;
|
|
117
|
+
};
|
package/src/main.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables
|
|
2
|
+
export const readGitHubWorkflowEnv = () => {
|
|
3
|
+
const eventName = process.env.GITHUB_EVENT_NAME;
|
|
4
|
+
if (!eventName) {
|
|
5
|
+
throw new Error(
|
|
6
|
+
`missing process.env.GITHUB_EVENT_NAME, we are not in a github workflow`,
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
if (eventName !== "pull_request" && eventName !== "pull_request_target") {
|
|
10
|
+
throw new Error(`must be called only in a pull request`);
|
|
11
|
+
}
|
|
12
|
+
const githubToken = process.env.GITHUB_TOKEN;
|
|
13
|
+
if (!githubToken) {
|
|
14
|
+
throw new Error(`missing githubToken`);
|
|
15
|
+
}
|
|
16
|
+
const githubRepository = process.env.GITHUB_REPOSITORY;
|
|
17
|
+
if (!githubRepository) {
|
|
18
|
+
throw new Error(`missing process.env.GITHUB_REPOSITORY`);
|
|
19
|
+
}
|
|
20
|
+
const [repositoryOwner, repositoryName] = githubRepository.split("/");
|
|
21
|
+
return {
|
|
22
|
+
rootDirectoryUrl: process.env.GITHUB_WORKSPACE,
|
|
23
|
+
githubToken,
|
|
24
|
+
repositoryOwner,
|
|
25
|
+
repositoryName,
|
|
26
|
+
commitSha: process.env.GITHUB_SHA,
|
|
27
|
+
};
|
|
28
|
+
};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* https://github.com/IgnusG/jest-report-action/blob/c006b890ba3c3b650e6c55916a643ca82b64133b/tasks/github-api.js#L12
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createLogger } from "@jsenv/log";
|
|
7
|
+
|
|
8
|
+
import { POST, PATCH } from "./internal/github_rest_api.js";
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
annotation object contains the following:
|
|
12
|
+
{path: filePath,
|
|
13
|
+
title: `${ describe } > ${ test }`,
|
|
14
|
+
start_line: location.start.line,
|
|
15
|
+
end_line: location.end.line,
|
|
16
|
+
annotation_level: 'failure',
|
|
17
|
+
message: `${ title }\n${ expectations }\n\n${ stacktrace }`
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// https://docs.github.com/fr/rest/checks/runs?apiVersion=2022-11-28#create-a-check-run
|
|
21
|
+
export const startGithubCheckRun = async ({
|
|
22
|
+
logLevel,
|
|
23
|
+
githubToken,
|
|
24
|
+
repositoryOwner,
|
|
25
|
+
repositoryName,
|
|
26
|
+
commitSha,
|
|
27
|
+
checkStatus = "in_progress",
|
|
28
|
+
checkName,
|
|
29
|
+
checkTitle,
|
|
30
|
+
checkSummary,
|
|
31
|
+
}) => {
|
|
32
|
+
if (typeof githubToken !== "string") {
|
|
33
|
+
throw new TypeError(
|
|
34
|
+
`githubToken must be a string but received ${githubToken}`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
if (typeof repositoryOwner !== "string") {
|
|
38
|
+
throw new TypeError(
|
|
39
|
+
`repositoryOwner must be a string but received ${repositoryOwner}`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
if (typeof repositoryName !== "string") {
|
|
43
|
+
throw new TypeError(
|
|
44
|
+
`repositoryName must be a string but received ${repositoryName}`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
if (typeof commitSha !== "string") {
|
|
48
|
+
throw new TypeError(`commitSha must be a string but received ${commitSha}`);
|
|
49
|
+
}
|
|
50
|
+
if (typeof checkName !== "string") {
|
|
51
|
+
throw new TypeError(`checkName must be a string but received ${checkName}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const logger = createLogger({ logLevel });
|
|
55
|
+
logger.debug("create check on");
|
|
56
|
+
|
|
57
|
+
const check = await POST({
|
|
58
|
+
url: `/repos/${repositoryOwner}/${repositoryName}/check-runs`,
|
|
59
|
+
githubToken,
|
|
60
|
+
body: {
|
|
61
|
+
head_sha: commitSha,
|
|
62
|
+
status: checkStatus,
|
|
63
|
+
name: checkName,
|
|
64
|
+
output: {
|
|
65
|
+
title: checkTitle,
|
|
66
|
+
summary: checkSummary,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
let previousUpdatePromise;
|
|
72
|
+
const update = async ({
|
|
73
|
+
status,
|
|
74
|
+
conclusion,
|
|
75
|
+
title,
|
|
76
|
+
summary,
|
|
77
|
+
annotations = [],
|
|
78
|
+
}) => {
|
|
79
|
+
if (previousUpdatePromise) {
|
|
80
|
+
await previousUpdatePromise;
|
|
81
|
+
}
|
|
82
|
+
previousUpdatePromise = (async () => {
|
|
83
|
+
let annotationsSent = 0;
|
|
84
|
+
const annotationsBatch = annotations.slice(annotationsSent, 50);
|
|
85
|
+
await PATCH({
|
|
86
|
+
url: `/repos/${repositoryOwner}/${repositoryName}/check-runs`,
|
|
87
|
+
githubToken,
|
|
88
|
+
body: {
|
|
89
|
+
head_sha: commitSha,
|
|
90
|
+
check_run_id: check.id,
|
|
91
|
+
...(status ? { status } : {}),
|
|
92
|
+
...(conclusion ? { conclusion } : {}),
|
|
93
|
+
output: {
|
|
94
|
+
...(title ? { title } : {}),
|
|
95
|
+
...(summary ? { summary } : {}),
|
|
96
|
+
annotations: annotationsBatch,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
if (status) {
|
|
101
|
+
checkStatus = status;
|
|
102
|
+
}
|
|
103
|
+
if (title) {
|
|
104
|
+
checkTitle = title;
|
|
105
|
+
}
|
|
106
|
+
if (summary) {
|
|
107
|
+
checkSummary = summary;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
annotationsSent += annotationsBatch.length;
|
|
111
|
+
while (annotationsSent < annotations.length) {
|
|
112
|
+
const annotationsBatch = annotations.slice(annotationsSent, 50);
|
|
113
|
+
await PATCH({
|
|
114
|
+
url: `/repos/${repositoryOwner}/${repositoryName}/check-runs`,
|
|
115
|
+
githubToken,
|
|
116
|
+
body: {
|
|
117
|
+
head_sha: commitSha,
|
|
118
|
+
check_run_id: check.id,
|
|
119
|
+
output: {
|
|
120
|
+
annotations: annotationsBatch,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
annotationsSent += annotationsBatch.length;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
previousUpdatePromise = null;
|
|
128
|
+
})();
|
|
129
|
+
return previousUpdatePromise;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
let lastProgressCall;
|
|
133
|
+
let pendingAnnotations = [];
|
|
134
|
+
let pendingAbortController;
|
|
135
|
+
let msBetweenProgressCalls = 500;
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
progress: async ({ title, summary, annotations = [] }) => {
|
|
139
|
+
const nowMs = Date.now();
|
|
140
|
+
const isFirstCall = !lastProgressCall;
|
|
141
|
+
lastProgressCall = nowMs;
|
|
142
|
+
if (isFirstCall) {
|
|
143
|
+
await update({
|
|
144
|
+
title,
|
|
145
|
+
summary,
|
|
146
|
+
annotations,
|
|
147
|
+
});
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (pendingAbortController) {
|
|
151
|
+
pendingAbortController.abort();
|
|
152
|
+
}
|
|
153
|
+
const msEllapsedSinceLastProgressCall = nowMs - lastProgressCall;
|
|
154
|
+
const msEllapsedIsBigEnough =
|
|
155
|
+
msEllapsedSinceLastProgressCall > msBetweenProgressCalls;
|
|
156
|
+
if (msEllapsedIsBigEnough) {
|
|
157
|
+
annotations = [...pendingAnnotations, ...annotations];
|
|
158
|
+
pendingAnnotations.length = 0;
|
|
159
|
+
await update({
|
|
160
|
+
title,
|
|
161
|
+
summary,
|
|
162
|
+
annotations,
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
pendingAnnotations.push(...annotations);
|
|
167
|
+
pendingAbortController = new AbortController();
|
|
168
|
+
await new Promise((resolve) => {
|
|
169
|
+
pendingAbortController.signal.onabort = resolve;
|
|
170
|
+
setTimeout(resolve, msBetweenProgressCalls);
|
|
171
|
+
});
|
|
172
|
+
if (pendingAbortController.signal.aborted) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
pendingAbortController = null;
|
|
176
|
+
await update({
|
|
177
|
+
title,
|
|
178
|
+
summary,
|
|
179
|
+
annotations,
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
fail: ({ title, summary, annotations } = {}) => {
|
|
183
|
+
return update({
|
|
184
|
+
status: "completed",
|
|
185
|
+
conclusion: "failure",
|
|
186
|
+
title,
|
|
187
|
+
summary,
|
|
188
|
+
annotations,
|
|
189
|
+
});
|
|
190
|
+
},
|
|
191
|
+
pass: ({ title, summary, annotations } = {}) => {
|
|
192
|
+
return update({
|
|
193
|
+
status: "completed",
|
|
194
|
+
conclusion: "success",
|
|
195
|
+
title,
|
|
196
|
+
summary,
|
|
197
|
+
annotations,
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
};
|