@skuba-lib/api 0.0.0 → 0.1.0-skuba-lib-api-20250925050503
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/LICENSE +21 -0
- package/README.md +8 -0
- package/buildkite/package.json +5 -0
- package/git/package.json +5 -0
- package/github/package.json +5 -0
- package/lib/buildkite/index.d.mts +2 -0
- package/lib/buildkite/index.d.ts +2 -0
- package/lib/buildkite/index.js +6 -0
- package/lib/buildkite/index.mjs +5 -0
- package/lib/buildkite-BXHjSY0I.js +67 -0
- package/lib/buildkite-DSHpKA4z.mjs +50 -0
- package/lib/chunk-CZCtuq8p.mjs +37 -0
- package/lib/chunk-odGzh3W-.js +58 -0
- package/lib/exec-B2akdw0z.js +17597 -0
- package/lib/exec-p0gJ30UO.mjs +17580 -0
- package/lib/git/index.d.mts +2 -0
- package/lib/git/index.d.ts +2 -0
- package/lib/git/index.js +14 -0
- package/lib/git/index.mjs +3 -0
- package/lib/git-DDfcZdbm.mjs +379 -0
- package/lib/git-DVG_dHiI.js +474 -0
- package/lib/github/index.d.mts +3 -0
- package/lib/github/index.d.ts +3 -0
- package/lib/github/index.js +12 -0
- package/lib/github/index.mjs +5 -0
- package/lib/github-B4auwKt7.js +310 -0
- package/lib/github-vycKoVtK.mjs +269 -0
- package/lib/index-1eEu26nG.d.ts +255 -0
- package/lib/index-9i8gHTGu.d.mts +38 -0
- package/lib/index-B91wXiwr.d.ts +278 -0
- package/lib/index-CfnlMf_4.d.ts +39 -0
- package/lib/index-Cl--YMPq.d.mts +255 -0
- package/lib/index-DNXQKEza.d.mts +278 -0
- package/lib/index-DiqTDK1U.d.mts +39 -0
- package/lib/index-MmnRaakr.d.ts +38 -0
- package/lib/index.d.mts +5 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +31 -0
- package/lib/index.mjs +8 -0
- package/lib/logging-6TTEX4U4.js +2217 -0
- package/lib/logging-Dr8kuQ_p.mjs +2194 -0
- package/lib/net/index.d.mts +2 -0
- package/lib/net/index.d.ts +2 -0
- package/lib/net/index.js +5 -0
- package/lib/net/index.mjs +5 -0
- package/lib/net-BteSPzmU.js +86 -0
- package/lib/net-ByUFe0qv.mjs +75 -0
- package/net/package.json +5 -0
- package/package.json +58 -8
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
const require_chunk = require('./chunk-odGzh3W-.js');
|
|
2
|
+
const require_logging = require('./logging-6TTEX4U4.js');
|
|
3
|
+
const require_git = require('./git-DVG_dHiI.js');
|
|
4
|
+
const path = require_chunk.__toESM(require("path"));
|
|
5
|
+
const fs_extra = require_chunk.__toESM(require("fs-extra"));
|
|
6
|
+
|
|
7
|
+
//#region src/github/octokit.ts
|
|
8
|
+
const createRestClient = async (options) => new (await (import("@octokit/rest"))).Octokit(options);
|
|
9
|
+
const graphql = async (query, parameters) => (await import("@octokit/graphql")).graphql(query, parameters);
|
|
10
|
+
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/github/checkRun.ts
|
|
13
|
+
const GITHUB_MAX_ANNOTATIONS = 50;
|
|
14
|
+
/**
|
|
15
|
+
* Suffixes the title with the number of annotations added, e.g.
|
|
16
|
+
*
|
|
17
|
+
* ```text
|
|
18
|
+
* Build #12 failed (24 annotations added)
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
const suffixTitle = (title, inputAnnotations) => {
|
|
22
|
+
const addedAnnotations = inputAnnotations > GITHUB_MAX_ANNOTATIONS ? GITHUB_MAX_ANNOTATIONS : inputAnnotations;
|
|
23
|
+
return `${title} (${require_logging.pluralise(addedAnnotations, "annotation")} added)`;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Enriches the summary with more context about the check run.
|
|
27
|
+
*/
|
|
28
|
+
const createEnrichedSummary = (summary, inputAnnotations) => [summary, ...inputAnnotations > GITHUB_MAX_ANNOTATIONS ? [`${inputAnnotations} annotations were provided, but only the first ${GITHUB_MAX_ANNOTATIONS} are visible in GitHub.`] : []].join("\n\n");
|
|
29
|
+
/**
|
|
30
|
+
* Asynchronously creates a GitHub check run with annotations.
|
|
31
|
+
*
|
|
32
|
+
* The first 50 `annotations` are written in full to GitHub.
|
|
33
|
+
*
|
|
34
|
+
* A `GITHUB_API_TOKEN` or `GITHUB_TOKEN` with the `checks:write` permission
|
|
35
|
+
* must be present on the environment.
|
|
36
|
+
*/
|
|
37
|
+
const createCheckRun = async ({ annotations, conclusion, name, summary, text, title }) => {
|
|
38
|
+
const dir = process.cwd();
|
|
39
|
+
const [commitId, { owner, repo }] = await Promise.all([require_git.getHeadCommitId({ dir }), require_git.getOwnerAndRepo({ dir })]);
|
|
40
|
+
const client = await createRestClient({ auth: require_git.apiTokenFromEnvironment() });
|
|
41
|
+
await client.checks.create({
|
|
42
|
+
conclusion,
|
|
43
|
+
head_sha: commitId,
|
|
44
|
+
name,
|
|
45
|
+
output: {
|
|
46
|
+
annotations: annotations.slice(0, GITHUB_MAX_ANNOTATIONS),
|
|
47
|
+
summary: createEnrichedSummary(summary, annotations.length),
|
|
48
|
+
text,
|
|
49
|
+
title: suffixTitle(title, annotations.length)
|
|
50
|
+
},
|
|
51
|
+
owner,
|
|
52
|
+
repo
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/github/pullRequest.ts
|
|
58
|
+
/**
|
|
59
|
+
* Gets the number of the current pull request.
|
|
60
|
+
*
|
|
61
|
+
* This tries to extract the pull request from common CI environment variables,
|
|
62
|
+
* and falls back to querying the GitHub Repos API for the latest pull request
|
|
63
|
+
* associated with the head commit. An error is thrown if there are no
|
|
64
|
+
* associated pull requests, or if they are all closed or locked.
|
|
65
|
+
*/
|
|
66
|
+
const getPullRequestNumber = async (params = {}) => {
|
|
67
|
+
const env = params.env ?? process.env;
|
|
68
|
+
const dir = process.cwd();
|
|
69
|
+
const number = Number(env.BUILDKITE_PULL_REQUEST ?? env.GITHUB_REF?.replace(/^refs\/pull\/(\d+).*$/, "$1"));
|
|
70
|
+
if (Number.isSafeInteger(number)) return number;
|
|
71
|
+
const client = params.client ?? await createRestClient({ auth: require_git.apiTokenFromEnvironment() });
|
|
72
|
+
const [commitId, { owner, repo }] = await Promise.all([require_git.getHeadCommitId({
|
|
73
|
+
dir,
|
|
74
|
+
env
|
|
75
|
+
}), require_git.getOwnerAndRepo({ dir })]);
|
|
76
|
+
const response = await client.repos.listPullRequestsAssociatedWithCommit({
|
|
77
|
+
commit_sha: commitId,
|
|
78
|
+
owner,
|
|
79
|
+
repo
|
|
80
|
+
});
|
|
81
|
+
const data = response.data.filter((pr) => pr.state === "open" && !pr.locked).sort((a, b) => b.updated_at.localeCompare(a.updated_at));
|
|
82
|
+
const pullRequestData = data[0];
|
|
83
|
+
if (!pullRequestData) throw new Error(`Commit ${commitId} is not associated with an open GitHub pull request`);
|
|
84
|
+
return pullRequestData.number;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/github/issueComment.ts
|
|
89
|
+
const getUserId = async (client) => {
|
|
90
|
+
const { data } = await client.users.getAuthenticated();
|
|
91
|
+
return data.id;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Asynchronously creates or updates a GitHub issue comment.
|
|
95
|
+
*
|
|
96
|
+
* This emulates `put` behaviour by overwriting the first existing comment by
|
|
97
|
+
* the same author on the issue, enabling use cases like a persistent bot
|
|
98
|
+
* comment at the top of the pull request that reflects the current status of a
|
|
99
|
+
* CI check.
|
|
100
|
+
*
|
|
101
|
+
* A `GITHUB_API_TOKEN` or `GITHUB_TOKEN` with write permissions must be present
|
|
102
|
+
* on the environment.
|
|
103
|
+
*/
|
|
104
|
+
const putIssueComment = async (params) => {
|
|
105
|
+
if (params.body === null && !params.internalId) throw new Error("Cannot remove comment without an internalId");
|
|
106
|
+
const env = params.env ?? process.env;
|
|
107
|
+
const dir = process.cwd();
|
|
108
|
+
const { owner, repo } = await require_git.getOwnerAndRepo({ dir });
|
|
109
|
+
const client = await createRestClient({ auth: require_git.apiTokenFromEnvironment() });
|
|
110
|
+
const issueNumber = params.issueNumber ?? await getPullRequestNumber({
|
|
111
|
+
client,
|
|
112
|
+
env
|
|
113
|
+
});
|
|
114
|
+
if (!issueNumber) throw new Error("Failed to infer an issue number");
|
|
115
|
+
const comments = await client.issues.listComments({
|
|
116
|
+
issue_number: issueNumber,
|
|
117
|
+
owner,
|
|
118
|
+
repo
|
|
119
|
+
});
|
|
120
|
+
const userId = params.userId === "seek-build-agency" ? 87109344 : params.userId ?? await getUserId(client);
|
|
121
|
+
const commentId = comments.data.find((comment) => comment.user?.id === userId && (params.internalId ? comment.body?.endsWith(`\n\n<!-- ${params.internalId} -->`) : true))?.id;
|
|
122
|
+
if (params.body === null) {
|
|
123
|
+
if (commentId) await client.issues.deleteComment({
|
|
124
|
+
comment_id: commentId,
|
|
125
|
+
owner,
|
|
126
|
+
repo
|
|
127
|
+
});
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const body = params.internalId ? [params.body.trim(), `<!-- ${params.internalId} -->`].join("\n\n") : params.body.trim();
|
|
131
|
+
const response = await (commentId ? client.issues.updateComment({
|
|
132
|
+
body,
|
|
133
|
+
comment_id: commentId,
|
|
134
|
+
issue_number: issueNumber,
|
|
135
|
+
owner,
|
|
136
|
+
repo
|
|
137
|
+
}) : client.issues.createComment({
|
|
138
|
+
body,
|
|
139
|
+
issue_number: issueNumber,
|
|
140
|
+
owner,
|
|
141
|
+
repo
|
|
142
|
+
}));
|
|
143
|
+
return { id: response.data.id };
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/github/push.ts
|
|
148
|
+
/**
|
|
149
|
+
* Retrieves all file changes from the local Git repository using
|
|
150
|
+
* `getChangedFiles`, then uploads the changes to a specified GitHub branch
|
|
151
|
+
* using `uploadFileChanges`.
|
|
152
|
+
*
|
|
153
|
+
* Returns the commit ID, or `undefined` if there are no changes to commit.
|
|
154
|
+
*
|
|
155
|
+
* The file changes will appear as verified commits on GitHub.
|
|
156
|
+
*
|
|
157
|
+
* This will not update the local Git repository unless `updateLocal` is
|
|
158
|
+
* specified.
|
|
159
|
+
*/
|
|
160
|
+
const uploadAllFileChanges = async ({ branch, dir, messageHeadline, ignore, messageBody, updateLocal = false }) => {
|
|
161
|
+
const changedFiles = await require_git.getChangedFiles({
|
|
162
|
+
dir,
|
|
163
|
+
ignore
|
|
164
|
+
});
|
|
165
|
+
if (!changedFiles.length) return;
|
|
166
|
+
const fileChanges = await readFileChanges(dir, changedFiles);
|
|
167
|
+
const commitId = await uploadFileChanges({
|
|
168
|
+
dir,
|
|
169
|
+
branch,
|
|
170
|
+
messageHeadline,
|
|
171
|
+
messageBody,
|
|
172
|
+
fileChanges
|
|
173
|
+
});
|
|
174
|
+
if (updateLocal) {
|
|
175
|
+
await Promise.all([...fileChanges.additions, ...fileChanges.deletions].map((file) => fs_extra.default.rm(file.path)));
|
|
176
|
+
await require_git.fastForwardBranch({
|
|
177
|
+
ref: branch,
|
|
178
|
+
auth: { type: "gitHubApp" },
|
|
179
|
+
dir
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return commitId;
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* Takes a list of `ChangedFiles`, reads them from the file system, and maps
|
|
186
|
+
* them to GitHub GraphQL `FileChanges`.
|
|
187
|
+
*
|
|
188
|
+
* https://docs.github.com/en/graphql/reference/input-objects#filechanges
|
|
189
|
+
*/
|
|
190
|
+
const readFileChanges = async (dir, changedFiles) => {
|
|
191
|
+
const { added, deleted } = changedFiles.reduce((files, changedFile) => {
|
|
192
|
+
const filePath = changedFile.path;
|
|
193
|
+
if (changedFile.state === "deleted") files.deleted.push(filePath);
|
|
194
|
+
else files.added.push(filePath);
|
|
195
|
+
return files;
|
|
196
|
+
}, {
|
|
197
|
+
added: [],
|
|
198
|
+
deleted: []
|
|
199
|
+
});
|
|
200
|
+
const gitRoot = await require_git.findRoot({ dir });
|
|
201
|
+
const toRootPath = (filePath) => {
|
|
202
|
+
if (!gitRoot) return filePath;
|
|
203
|
+
return path.default.resolve(gitRoot, filePath);
|
|
204
|
+
};
|
|
205
|
+
const additions = await Promise.all(added.map(async (filePath) => ({
|
|
206
|
+
path: filePath,
|
|
207
|
+
contents: await fs_extra.default.promises.readFile(toRootPath(filePath), { encoding: "base64" })
|
|
208
|
+
})));
|
|
209
|
+
const deletions = deleted.map((filePath) => ({ path: filePath }));
|
|
210
|
+
return {
|
|
211
|
+
additions,
|
|
212
|
+
deletions
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Uploads file changes from the local workspace to a specified GitHub branch.
|
|
217
|
+
*
|
|
218
|
+
* The file changes will appear as verified commits on GitHub.
|
|
219
|
+
*
|
|
220
|
+
* This will not update the local Git repository.
|
|
221
|
+
*/
|
|
222
|
+
const uploadFileChanges = async ({ dir, branch, messageHeadline, messageBody, fileChanges }) => {
|
|
223
|
+
const authToken = require_git.apiTokenFromEnvironment();
|
|
224
|
+
if (!authToken) throw new Error("Could not read a GitHub API token from the environment. Please set GITHUB_API_TOKEN or GITHUB_TOKEN.");
|
|
225
|
+
const [{ owner, repo }, headCommitId] = await Promise.all([require_git.getOwnerAndRepo({ dir }), require_git.getHeadCommitId({ dir })]);
|
|
226
|
+
const input = {
|
|
227
|
+
branch: {
|
|
228
|
+
repositoryNameWithOwner: `${owner}/${repo}`,
|
|
229
|
+
branchName: branch
|
|
230
|
+
},
|
|
231
|
+
message: {
|
|
232
|
+
headline: messageHeadline,
|
|
233
|
+
body: messageBody
|
|
234
|
+
},
|
|
235
|
+
expectedHeadOid: headCommitId,
|
|
236
|
+
clientMutationId: "skuba",
|
|
237
|
+
fileChanges
|
|
238
|
+
};
|
|
239
|
+
const result = await graphql(`
|
|
240
|
+
mutation Mutation($input: CreateCommitOnBranchInput!) {
|
|
241
|
+
createCommitOnBranch(input: $input) {
|
|
242
|
+
commit {
|
|
243
|
+
oid
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
`, {
|
|
248
|
+
input,
|
|
249
|
+
headers: { authorization: `Bearer ${authToken}` }
|
|
250
|
+
});
|
|
251
|
+
return result.createCommitOnBranch.commit.oid;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
//#endregion
|
|
255
|
+
//#region src/github/index.ts
|
|
256
|
+
var github_exports = {};
|
|
257
|
+
require_chunk.__export(github_exports, {
|
|
258
|
+
buildNameFromEnvironment: () => require_git.buildNameFromEnvironment,
|
|
259
|
+
createCheckRun: () => createCheckRun,
|
|
260
|
+
enabledFromEnvironment: () => require_git.enabledFromEnvironment,
|
|
261
|
+
getPullRequestNumber: () => getPullRequestNumber,
|
|
262
|
+
putIssueComment: () => putIssueComment,
|
|
263
|
+
readFileChanges: () => readFileChanges,
|
|
264
|
+
uploadAllFileChanges: () => uploadAllFileChanges,
|
|
265
|
+
uploadFileChanges: () => uploadFileChanges
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
//#endregion
|
|
269
|
+
Object.defineProperty(exports, 'createCheckRun', {
|
|
270
|
+
enumerable: true,
|
|
271
|
+
get: function () {
|
|
272
|
+
return createCheckRun;
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
Object.defineProperty(exports, 'getPullRequestNumber', {
|
|
276
|
+
enumerable: true,
|
|
277
|
+
get: function () {
|
|
278
|
+
return getPullRequestNumber;
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
Object.defineProperty(exports, 'github_exports', {
|
|
282
|
+
enumerable: true,
|
|
283
|
+
get: function () {
|
|
284
|
+
return github_exports;
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
Object.defineProperty(exports, 'putIssueComment', {
|
|
288
|
+
enumerable: true,
|
|
289
|
+
get: function () {
|
|
290
|
+
return putIssueComment;
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
Object.defineProperty(exports, 'readFileChanges', {
|
|
294
|
+
enumerable: true,
|
|
295
|
+
get: function () {
|
|
296
|
+
return readFileChanges;
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
Object.defineProperty(exports, 'uploadAllFileChanges', {
|
|
300
|
+
enumerable: true,
|
|
301
|
+
get: function () {
|
|
302
|
+
return uploadAllFileChanges;
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
Object.defineProperty(exports, 'uploadFileChanges', {
|
|
306
|
+
enumerable: true,
|
|
307
|
+
get: function () {
|
|
308
|
+
return uploadFileChanges;
|
|
309
|
+
}
|
|
310
|
+
});
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { __export } from "./chunk-CZCtuq8p.mjs";
|
|
2
|
+
import { pluralise } from "./logging-Dr8kuQ_p.mjs";
|
|
3
|
+
import { apiTokenFromEnvironment, buildNameFromEnvironment, enabledFromEnvironment, fastForwardBranch, findRoot, getChangedFiles, getHeadCommitId, getOwnerAndRepo } from "./git-DDfcZdbm.mjs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import fs from "fs-extra";
|
|
6
|
+
|
|
7
|
+
//#region src/github/octokit.ts
|
|
8
|
+
const createRestClient = async (options) => new (await (import("@octokit/rest"))).Octokit(options);
|
|
9
|
+
const graphql = async (query, parameters) => (await import("@octokit/graphql")).graphql(query, parameters);
|
|
10
|
+
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/github/checkRun.ts
|
|
13
|
+
const GITHUB_MAX_ANNOTATIONS = 50;
|
|
14
|
+
/**
|
|
15
|
+
* Suffixes the title with the number of annotations added, e.g.
|
|
16
|
+
*
|
|
17
|
+
* ```text
|
|
18
|
+
* Build #12 failed (24 annotations added)
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
const suffixTitle = (title, inputAnnotations) => {
|
|
22
|
+
const addedAnnotations = inputAnnotations > GITHUB_MAX_ANNOTATIONS ? GITHUB_MAX_ANNOTATIONS : inputAnnotations;
|
|
23
|
+
return `${title} (${pluralise(addedAnnotations, "annotation")} added)`;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Enriches the summary with more context about the check run.
|
|
27
|
+
*/
|
|
28
|
+
const createEnrichedSummary = (summary, inputAnnotations) => [summary, ...inputAnnotations > GITHUB_MAX_ANNOTATIONS ? [`${inputAnnotations} annotations were provided, but only the first ${GITHUB_MAX_ANNOTATIONS} are visible in GitHub.`] : []].join("\n\n");
|
|
29
|
+
/**
|
|
30
|
+
* Asynchronously creates a GitHub check run with annotations.
|
|
31
|
+
*
|
|
32
|
+
* The first 50 `annotations` are written in full to GitHub.
|
|
33
|
+
*
|
|
34
|
+
* A `GITHUB_API_TOKEN` or `GITHUB_TOKEN` with the `checks:write` permission
|
|
35
|
+
* must be present on the environment.
|
|
36
|
+
*/
|
|
37
|
+
const createCheckRun = async ({ annotations, conclusion, name, summary, text, title }) => {
|
|
38
|
+
const dir = process.cwd();
|
|
39
|
+
const [commitId, { owner, repo }] = await Promise.all([getHeadCommitId({ dir }), getOwnerAndRepo({ dir })]);
|
|
40
|
+
const client = await createRestClient({ auth: apiTokenFromEnvironment() });
|
|
41
|
+
await client.checks.create({
|
|
42
|
+
conclusion,
|
|
43
|
+
head_sha: commitId,
|
|
44
|
+
name,
|
|
45
|
+
output: {
|
|
46
|
+
annotations: annotations.slice(0, GITHUB_MAX_ANNOTATIONS),
|
|
47
|
+
summary: createEnrichedSummary(summary, annotations.length),
|
|
48
|
+
text,
|
|
49
|
+
title: suffixTitle(title, annotations.length)
|
|
50
|
+
},
|
|
51
|
+
owner,
|
|
52
|
+
repo
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/github/pullRequest.ts
|
|
58
|
+
/**
|
|
59
|
+
* Gets the number of the current pull request.
|
|
60
|
+
*
|
|
61
|
+
* This tries to extract the pull request from common CI environment variables,
|
|
62
|
+
* and falls back to querying the GitHub Repos API for the latest pull request
|
|
63
|
+
* associated with the head commit. An error is thrown if there are no
|
|
64
|
+
* associated pull requests, or if they are all closed or locked.
|
|
65
|
+
*/
|
|
66
|
+
const getPullRequestNumber = async (params = {}) => {
|
|
67
|
+
const env = params.env ?? process.env;
|
|
68
|
+
const dir = process.cwd();
|
|
69
|
+
const number = Number(env.BUILDKITE_PULL_REQUEST ?? env.GITHUB_REF?.replace(/^refs\/pull\/(\d+).*$/, "$1"));
|
|
70
|
+
if (Number.isSafeInteger(number)) return number;
|
|
71
|
+
const client = params.client ?? await createRestClient({ auth: apiTokenFromEnvironment() });
|
|
72
|
+
const [commitId, { owner, repo }] = await Promise.all([getHeadCommitId({
|
|
73
|
+
dir,
|
|
74
|
+
env
|
|
75
|
+
}), getOwnerAndRepo({ dir })]);
|
|
76
|
+
const response = await client.repos.listPullRequestsAssociatedWithCommit({
|
|
77
|
+
commit_sha: commitId,
|
|
78
|
+
owner,
|
|
79
|
+
repo
|
|
80
|
+
});
|
|
81
|
+
const data = response.data.filter((pr) => pr.state === "open" && !pr.locked).sort((a, b) => b.updated_at.localeCompare(a.updated_at));
|
|
82
|
+
const pullRequestData = data[0];
|
|
83
|
+
if (!pullRequestData) throw new Error(`Commit ${commitId} is not associated with an open GitHub pull request`);
|
|
84
|
+
return pullRequestData.number;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/github/issueComment.ts
|
|
89
|
+
const getUserId = async (client) => {
|
|
90
|
+
const { data } = await client.users.getAuthenticated();
|
|
91
|
+
return data.id;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Asynchronously creates or updates a GitHub issue comment.
|
|
95
|
+
*
|
|
96
|
+
* This emulates `put` behaviour by overwriting the first existing comment by
|
|
97
|
+
* the same author on the issue, enabling use cases like a persistent bot
|
|
98
|
+
* comment at the top of the pull request that reflects the current status of a
|
|
99
|
+
* CI check.
|
|
100
|
+
*
|
|
101
|
+
* A `GITHUB_API_TOKEN` or `GITHUB_TOKEN` with write permissions must be present
|
|
102
|
+
* on the environment.
|
|
103
|
+
*/
|
|
104
|
+
const putIssueComment = async (params) => {
|
|
105
|
+
if (params.body === null && !params.internalId) throw new Error("Cannot remove comment without an internalId");
|
|
106
|
+
const env = params.env ?? process.env;
|
|
107
|
+
const dir = process.cwd();
|
|
108
|
+
const { owner, repo } = await getOwnerAndRepo({ dir });
|
|
109
|
+
const client = await createRestClient({ auth: apiTokenFromEnvironment() });
|
|
110
|
+
const issueNumber = params.issueNumber ?? await getPullRequestNumber({
|
|
111
|
+
client,
|
|
112
|
+
env
|
|
113
|
+
});
|
|
114
|
+
if (!issueNumber) throw new Error("Failed to infer an issue number");
|
|
115
|
+
const comments = await client.issues.listComments({
|
|
116
|
+
issue_number: issueNumber,
|
|
117
|
+
owner,
|
|
118
|
+
repo
|
|
119
|
+
});
|
|
120
|
+
const userId = params.userId === "seek-build-agency" ? 87109344 : params.userId ?? await getUserId(client);
|
|
121
|
+
const commentId = comments.data.find((comment) => comment.user?.id === userId && (params.internalId ? comment.body?.endsWith(`\n\n<!-- ${params.internalId} -->`) : true))?.id;
|
|
122
|
+
if (params.body === null) {
|
|
123
|
+
if (commentId) await client.issues.deleteComment({
|
|
124
|
+
comment_id: commentId,
|
|
125
|
+
owner,
|
|
126
|
+
repo
|
|
127
|
+
});
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const body = params.internalId ? [params.body.trim(), `<!-- ${params.internalId} -->`].join("\n\n") : params.body.trim();
|
|
131
|
+
const response = await (commentId ? client.issues.updateComment({
|
|
132
|
+
body,
|
|
133
|
+
comment_id: commentId,
|
|
134
|
+
issue_number: issueNumber,
|
|
135
|
+
owner,
|
|
136
|
+
repo
|
|
137
|
+
}) : client.issues.createComment({
|
|
138
|
+
body,
|
|
139
|
+
issue_number: issueNumber,
|
|
140
|
+
owner,
|
|
141
|
+
repo
|
|
142
|
+
}));
|
|
143
|
+
return { id: response.data.id };
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/github/push.ts
|
|
148
|
+
/**
|
|
149
|
+
* Retrieves all file changes from the local Git repository using
|
|
150
|
+
* `getChangedFiles`, then uploads the changes to a specified GitHub branch
|
|
151
|
+
* using `uploadFileChanges`.
|
|
152
|
+
*
|
|
153
|
+
* Returns the commit ID, or `undefined` if there are no changes to commit.
|
|
154
|
+
*
|
|
155
|
+
* The file changes will appear as verified commits on GitHub.
|
|
156
|
+
*
|
|
157
|
+
* This will not update the local Git repository unless `updateLocal` is
|
|
158
|
+
* specified.
|
|
159
|
+
*/
|
|
160
|
+
const uploadAllFileChanges = async ({ branch, dir, messageHeadline, ignore, messageBody, updateLocal = false }) => {
|
|
161
|
+
const changedFiles = await getChangedFiles({
|
|
162
|
+
dir,
|
|
163
|
+
ignore
|
|
164
|
+
});
|
|
165
|
+
if (!changedFiles.length) return;
|
|
166
|
+
const fileChanges = await readFileChanges(dir, changedFiles);
|
|
167
|
+
const commitId = await uploadFileChanges({
|
|
168
|
+
dir,
|
|
169
|
+
branch,
|
|
170
|
+
messageHeadline,
|
|
171
|
+
messageBody,
|
|
172
|
+
fileChanges
|
|
173
|
+
});
|
|
174
|
+
if (updateLocal) {
|
|
175
|
+
await Promise.all([...fileChanges.additions, ...fileChanges.deletions].map((file) => fs.rm(file.path)));
|
|
176
|
+
await fastForwardBranch({
|
|
177
|
+
ref: branch,
|
|
178
|
+
auth: { type: "gitHubApp" },
|
|
179
|
+
dir
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return commitId;
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* Takes a list of `ChangedFiles`, reads them from the file system, and maps
|
|
186
|
+
* them to GitHub GraphQL `FileChanges`.
|
|
187
|
+
*
|
|
188
|
+
* https://docs.github.com/en/graphql/reference/input-objects#filechanges
|
|
189
|
+
*/
|
|
190
|
+
const readFileChanges = async (dir, changedFiles) => {
|
|
191
|
+
const { added, deleted } = changedFiles.reduce((files, changedFile) => {
|
|
192
|
+
const filePath = changedFile.path;
|
|
193
|
+
if (changedFile.state === "deleted") files.deleted.push(filePath);
|
|
194
|
+
else files.added.push(filePath);
|
|
195
|
+
return files;
|
|
196
|
+
}, {
|
|
197
|
+
added: [],
|
|
198
|
+
deleted: []
|
|
199
|
+
});
|
|
200
|
+
const gitRoot = await findRoot({ dir });
|
|
201
|
+
const toRootPath = (filePath) => {
|
|
202
|
+
if (!gitRoot) return filePath;
|
|
203
|
+
return path.resolve(gitRoot, filePath);
|
|
204
|
+
};
|
|
205
|
+
const additions = await Promise.all(added.map(async (filePath) => ({
|
|
206
|
+
path: filePath,
|
|
207
|
+
contents: await fs.promises.readFile(toRootPath(filePath), { encoding: "base64" })
|
|
208
|
+
})));
|
|
209
|
+
const deletions = deleted.map((filePath) => ({ path: filePath }));
|
|
210
|
+
return {
|
|
211
|
+
additions,
|
|
212
|
+
deletions
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Uploads file changes from the local workspace to a specified GitHub branch.
|
|
217
|
+
*
|
|
218
|
+
* The file changes will appear as verified commits on GitHub.
|
|
219
|
+
*
|
|
220
|
+
* This will not update the local Git repository.
|
|
221
|
+
*/
|
|
222
|
+
const uploadFileChanges = async ({ dir, branch, messageHeadline, messageBody, fileChanges }) => {
|
|
223
|
+
const authToken = apiTokenFromEnvironment();
|
|
224
|
+
if (!authToken) throw new Error("Could not read a GitHub API token from the environment. Please set GITHUB_API_TOKEN or GITHUB_TOKEN.");
|
|
225
|
+
const [{ owner, repo }, headCommitId] = await Promise.all([getOwnerAndRepo({ dir }), getHeadCommitId({ dir })]);
|
|
226
|
+
const input = {
|
|
227
|
+
branch: {
|
|
228
|
+
repositoryNameWithOwner: `${owner}/${repo}`,
|
|
229
|
+
branchName: branch
|
|
230
|
+
},
|
|
231
|
+
message: {
|
|
232
|
+
headline: messageHeadline,
|
|
233
|
+
body: messageBody
|
|
234
|
+
},
|
|
235
|
+
expectedHeadOid: headCommitId,
|
|
236
|
+
clientMutationId: "skuba",
|
|
237
|
+
fileChanges
|
|
238
|
+
};
|
|
239
|
+
const result = await graphql(`
|
|
240
|
+
mutation Mutation($input: CreateCommitOnBranchInput!) {
|
|
241
|
+
createCommitOnBranch(input: $input) {
|
|
242
|
+
commit {
|
|
243
|
+
oid
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
`, {
|
|
248
|
+
input,
|
|
249
|
+
headers: { authorization: `Bearer ${authToken}` }
|
|
250
|
+
});
|
|
251
|
+
return result.createCommitOnBranch.commit.oid;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
//#endregion
|
|
255
|
+
//#region src/github/index.ts
|
|
256
|
+
var github_exports = {};
|
|
257
|
+
__export(github_exports, {
|
|
258
|
+
buildNameFromEnvironment: () => buildNameFromEnvironment,
|
|
259
|
+
createCheckRun: () => createCheckRun,
|
|
260
|
+
enabledFromEnvironment: () => enabledFromEnvironment,
|
|
261
|
+
getPullRequestNumber: () => getPullRequestNumber,
|
|
262
|
+
putIssueComment: () => putIssueComment,
|
|
263
|
+
readFileChanges: () => readFileChanges,
|
|
264
|
+
uploadAllFileChanges: () => uploadAllFileChanges,
|
|
265
|
+
uploadFileChanges: () => uploadFileChanges
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
//#endregion
|
|
269
|
+
export { createCheckRun, getPullRequestNumber, github_exports, putIssueComment, readFileChanges, uploadAllFileChanges, uploadFileChanges };
|