@goodfellow-cms/github 0.3.0 → 0.5.0
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/dist/deploy-keys-DAj2oX-N.js +108 -0
- package/dist/index.d.ts +56 -15
- package/dist/index.js +522 -64
- package/dist/testing.d.ts +50 -2
- package/dist/testing.js +231 -11
- package/package.json +5 -3
package/dist/testing.d.ts
CHANGED
|
@@ -20,6 +20,10 @@ export interface FakeGitHubUser {
|
|
|
20
20
|
actions?: boolean;
|
|
21
21
|
/** Whether the token may run workflows, as owner tokens can. Defaults to the `administration` setting. */
|
|
22
22
|
actionsWrite?: boolean;
|
|
23
|
+
/** Whether the token may read and change Actions variables, as owner tokens can. Defaults to the `administration` setting. */
|
|
24
|
+
variables?: boolean;
|
|
25
|
+
/** Whether the token may set Actions secrets and add deploy keys. Defaults to true. */
|
|
26
|
+
secrets?: boolean;
|
|
23
27
|
}
|
|
24
28
|
/** A GitHub Actions workflow run, with its jobs and their steps. */
|
|
25
29
|
export interface FakeRun {
|
|
@@ -32,6 +36,8 @@ export interface FakeRun {
|
|
|
32
36
|
createdAt?: string;
|
|
33
37
|
status: "queued" | "in_progress" | "completed";
|
|
34
38
|
conclusion: "success" | "failure" | "startup_failure" | "cancelled" | null;
|
|
39
|
+
/** The workflow's file, `.github/workflows/deploy.yml` by default. */
|
|
40
|
+
path?: string;
|
|
35
41
|
jobs: Array<{
|
|
36
42
|
name: string;
|
|
37
43
|
conclusion: string | null;
|
|
@@ -70,6 +76,17 @@ export interface FakeGitHubOptions {
|
|
|
70
76
|
* reports success after being checked this many times. Off by default.
|
|
71
77
|
*/
|
|
72
78
|
deployAfterChecks?: number;
|
|
79
|
+
/**
|
|
80
|
+
* Simulates Cloudflare Pages: every commit to the default branch gets a
|
|
81
|
+
* build, which reports success after being checked this many times, as a
|
|
82
|
+
* check run of Cloudflare's GitHub App, or as a commit status. Off by default.
|
|
83
|
+
*/
|
|
84
|
+
cloudflare?: {
|
|
85
|
+
afterChecks: number;
|
|
86
|
+
report?: "check-runs" | "statuses";
|
|
87
|
+
};
|
|
88
|
+
/** Answers 403 to reading check runs, as GitHub does for fine-grained tokens, which can't have the permission. */
|
|
89
|
+
checkRunsForbidden?: boolean;
|
|
73
90
|
/** Organizations the users belong to, which new repositories can be created in. */
|
|
74
91
|
orgs?: string[];
|
|
75
92
|
/** The default branch of repositories created through the API, as an account's settings choose. Defaults to `main`. */
|
|
@@ -84,6 +101,8 @@ export interface FakeGitHubOptions {
|
|
|
84
101
|
accounts?: string[];
|
|
85
102
|
/** GitHub Apps, which the sign-in worker signs people in with. */
|
|
86
103
|
apps?: FakeGitHubApp[];
|
|
104
|
+
/** Whether the site's repository is private. Public by default. */
|
|
105
|
+
private?: boolean;
|
|
87
106
|
}
|
|
88
107
|
/** A GitHub App, as the sign-in worker uses one: its JWTs, installations, OAuth sign-in and tokens. */
|
|
89
108
|
export interface FakeGitHubApp {
|
|
@@ -125,8 +144,21 @@ export interface FakeCreatedRepo {
|
|
|
125
144
|
repo: FakeRepo;
|
|
126
145
|
pages?: {
|
|
127
146
|
build_type: string;
|
|
147
|
+
cname?: string;
|
|
128
148
|
};
|
|
129
149
|
rulesets: unknown[];
|
|
150
|
+
/** Deploy keys added to it, by their public key. */
|
|
151
|
+
deployKeys: Array<{
|
|
152
|
+
title: string;
|
|
153
|
+
key: string;
|
|
154
|
+
read_only: boolean;
|
|
155
|
+
}>;
|
|
156
|
+
/** Its Actions secrets, opened. */
|
|
157
|
+
secrets: Map<string, string>;
|
|
158
|
+
/** The key pair GitHub seals secrets with. */
|
|
159
|
+
secretKey?: CryptoKeyPair;
|
|
160
|
+
/** People's permission on it, by login, as `collaborators/{login}/permission` says; others have none. */
|
|
161
|
+
collaborators?: Map<string, "admin" | "write" | "read">;
|
|
130
162
|
}
|
|
131
163
|
interface Deployment {
|
|
132
164
|
id: number;
|
|
@@ -134,6 +166,11 @@ interface Deployment {
|
|
|
134
166
|
statuses: FakeDeploymentStatus[];
|
|
135
167
|
checks: number;
|
|
136
168
|
}
|
|
169
|
+
/** A build Cloudflare Pages reports on a commit, which tests can fail with `failCloudflare()`. */
|
|
170
|
+
export interface FakeCloudflareBuild {
|
|
171
|
+
checks: number;
|
|
172
|
+
failed?: boolean;
|
|
173
|
+
}
|
|
137
174
|
export declare function fakeGitHub(options: FakeGitHubOptions): {
|
|
138
175
|
repo: FakeRepo;
|
|
139
176
|
/** How many GraphQL requests read files, as `readFiles()` makes. */
|
|
@@ -150,8 +187,13 @@ export declare function fakeGitHub(options: FakeGitHubOptions): {
|
|
|
150
187
|
code: string;
|
|
151
188
|
installation: number;
|
|
152
189
|
};
|
|
153
|
-
/** Repositories created through the API, by `owner/name`. */
|
|
190
|
+
/** Repositories created through the API, or added with `addRepo()`, by `owner/name`. */
|
|
154
191
|
created: Map<string, FakeCreatedRepo>;
|
|
192
|
+
addRepo: (fullName: string, repoOptions?: {
|
|
193
|
+
files?: Record<string, string>;
|
|
194
|
+
private?: boolean;
|
|
195
|
+
collaborators?: Record<string, "admin" | "write" | "read">;
|
|
196
|
+
}) => FakeCreatedRepo;
|
|
155
197
|
/** The site's GitHub Pages settings, which tests can change, such as to issue the certificate. */
|
|
156
198
|
pages: {
|
|
157
199
|
current?: FakePages;
|
|
@@ -161,9 +203,15 @@ export declare function fakeGitHub(options: FakeGitHubOptions): {
|
|
|
161
203
|
invitations: FakeInvitation[];
|
|
162
204
|
accept: (login: string) => void;
|
|
163
205
|
deployments: Deployment[];
|
|
164
|
-
/**
|
|
206
|
+
/** Cloudflare Pages' builds, by commit, with `cloudflare`; `failCloudflare()` makes one fail. */
|
|
207
|
+
cloudflareBuilds: Map<string, FakeCloudflareBuild>;
|
|
208
|
+
failCloudflare: (sha: string) => void;
|
|
209
|
+
/** GitHub Actions workflow runs, which tests add with `failBuild()` and `cancelBuild()`. */
|
|
165
210
|
runs: FakeRun[];
|
|
211
|
+
/** The repository's Actions variables, such as `GOODFELLOW_NIGHTLY`. */
|
|
212
|
+
variables: Map<string, string>;
|
|
166
213
|
failBuild: (sha: string, step?: string, job?: string) => FakeRun;
|
|
214
|
+
cancelBuild: (sha: string) => FakeRun;
|
|
167
215
|
/** Every request received, for checking what the backend sent. */
|
|
168
216
|
requests: Request[];
|
|
169
217
|
/** Simulates someone else publishing. */
|
package/dist/testing.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { n as openSealedSecret } from "./deploy-keys-DAj2oX-N.js";
|
|
1
2
|
import { decodeBase64, encodeBase64Bytes } from "@goodfellow-cms/core";
|
|
2
3
|
import { FakeConflictError, FakeRepo, fakeFileBytes, fakeFileFromBytes } from "@goodfellow-cms/core/testing";
|
|
3
4
|
//#region src/testing.ts
|
|
@@ -52,7 +53,10 @@ function fakeGitHub(options) {
|
|
|
52
53
|
let nextToken = 1;
|
|
53
54
|
let nextRepoId = 100;
|
|
54
55
|
const deployments = [];
|
|
56
|
+
const cloudflareBuilds = /* @__PURE__ */ new Map();
|
|
55
57
|
const runs = [];
|
|
58
|
+
/** The repository's Actions variables, by name. */
|
|
59
|
+
const variables = /* @__PURE__ */ new Map();
|
|
56
60
|
const requests = [];
|
|
57
61
|
let nextDeployment = 1;
|
|
58
62
|
let graphqlReads = 0;
|
|
@@ -186,12 +190,44 @@ function fakeGitHub(options) {
|
|
|
186
190
|
runs.push(run);
|
|
187
191
|
return run;
|
|
188
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* Simulates the deploy workflow's run for a commit being cancelled, as a
|
|
195
|
+
* newer run in its concurrency group replaces one still waiting: it never
|
|
196
|
+
* deploys.
|
|
197
|
+
*/
|
|
198
|
+
function cancelBuild(sha) {
|
|
199
|
+
for (const [index, deployment] of [...deployments.entries()].reverse()) if (deployment.sha === sha) deployments.splice(index, 1);
|
|
200
|
+
const run = {
|
|
201
|
+
id: runs.length + 1,
|
|
202
|
+
sha,
|
|
203
|
+
status: "completed",
|
|
204
|
+
conclusion: "cancelled",
|
|
205
|
+
jobs: []
|
|
206
|
+
};
|
|
207
|
+
runs.push(run);
|
|
208
|
+
return run;
|
|
209
|
+
}
|
|
189
210
|
/** Simulates an invited person accepting the invitation. */
|
|
190
211
|
function accept(login) {
|
|
191
212
|
const index = invitations.findIndex((invitation) => invitation.login === login);
|
|
192
213
|
const [invitation] = index === -1 ? [] : invitations.splice(index, 1);
|
|
193
214
|
if (invitation) collaborators.set(login, invitation.permission === "admin" ? "admin" : "push");
|
|
194
215
|
}
|
|
216
|
+
/** Adds another repository, as if someone had made it, such as a site's code repository beside its content's. */
|
|
217
|
+
function addRepo(fullName, repoOptions = {}) {
|
|
218
|
+
const added = {
|
|
219
|
+
id: nextRepoId++,
|
|
220
|
+
fullName,
|
|
221
|
+
private: repoOptions.private === true,
|
|
222
|
+
repo: new FakeRepo(repoOptions.files ?? {}, "main"),
|
|
223
|
+
rulesets: [],
|
|
224
|
+
deployKeys: [],
|
|
225
|
+
secrets: /* @__PURE__ */ new Map(),
|
|
226
|
+
collaborators: new Map(Object.entries(repoOptions.collaborators ?? {}))
|
|
227
|
+
};
|
|
228
|
+
created.set(fullName, added);
|
|
229
|
+
return added;
|
|
230
|
+
}
|
|
195
231
|
/** Repository creation and the Git data API, for repositories created through the fake. */
|
|
196
232
|
async function handleCreated(request, path, principal) {
|
|
197
233
|
const login = principal.login;
|
|
@@ -215,7 +251,9 @@ function fakeGitHub(options) {
|
|
|
215
251
|
private: body.private === true,
|
|
216
252
|
description: body.description,
|
|
217
253
|
repo,
|
|
218
|
-
rulesets: []
|
|
254
|
+
rulesets: [],
|
|
255
|
+
deployKeys: [],
|
|
256
|
+
secrets: /* @__PURE__ */ new Map()
|
|
219
257
|
});
|
|
220
258
|
return json({
|
|
221
259
|
id,
|
|
@@ -234,12 +272,48 @@ function fakeGitHub(options) {
|
|
|
234
272
|
if (rest === "" && method === "GET") return json({
|
|
235
273
|
id: site.id,
|
|
236
274
|
full_name: site.fullName,
|
|
275
|
+
private: site.private,
|
|
237
276
|
default_branch: repo.defaultBranch,
|
|
238
277
|
permissions: {
|
|
239
278
|
push: true,
|
|
240
279
|
admin: true
|
|
241
280
|
}
|
|
242
281
|
});
|
|
282
|
+
const tree = rest.match(/^\/git\/trees\/([0-9a-f]{40})$/);
|
|
283
|
+
if (tree && method === "GET" && repo.commitAt(tree[1] ?? "")) {
|
|
284
|
+
const entries = [...repo.files(tree[1] ?? "")].map(([file, content]) => ({
|
|
285
|
+
path: file,
|
|
286
|
+
type: "blob",
|
|
287
|
+
sha: repo.blobSha(content),
|
|
288
|
+
size: fakeFileBytes(content).length
|
|
289
|
+
}));
|
|
290
|
+
return json({
|
|
291
|
+
sha: tree[1],
|
|
292
|
+
tree: entries,
|
|
293
|
+
truncated: false
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
const blob = rest.match(/^\/git\/blobs\/([0-9a-f]{40})$/);
|
|
297
|
+
if (blob && method === "GET") {
|
|
298
|
+
const content = repo.blobs.get(blob[1] ?? "");
|
|
299
|
+
if (content === void 0) return json({ message: "Not Found" }, 404);
|
|
300
|
+
return new Response(fakeFileBytes(content));
|
|
301
|
+
}
|
|
302
|
+
const contents = rest.match(/^\/contents\/(.+)$/);
|
|
303
|
+
if (contents && method === "GET") {
|
|
304
|
+
if (principal.app && !principal.permissions?.contents) return json({ message: "Resource not accessible" }, 403);
|
|
305
|
+
const file = repo.files().get(decodeURIComponent(contents[1] ?? ""));
|
|
306
|
+
if (file === void 0) return json({ message: "Not Found" }, 404);
|
|
307
|
+
return new Response(typeof file === "string" ? file : fakeFileBytes(file));
|
|
308
|
+
}
|
|
309
|
+
const permission = rest.match(/^\/collaborators\/([^/]+)\/permission$/);
|
|
310
|
+
if (permission && method === "GET") {
|
|
311
|
+
const login = decodeURIComponent(permission[1] ?? "");
|
|
312
|
+
return json({
|
|
313
|
+
permission: site.collaborators?.get(login) ?? "none",
|
|
314
|
+
user: { login }
|
|
315
|
+
});
|
|
316
|
+
}
|
|
243
317
|
if (rest === "" && method === "PATCH") {
|
|
244
318
|
const body = await request.json();
|
|
245
319
|
if (body.default_branch) {
|
|
@@ -279,6 +353,48 @@ function fakeGitHub(options) {
|
|
|
279
353
|
build_type: site.pages.build_type
|
|
280
354
|
}, 201);
|
|
281
355
|
}
|
|
356
|
+
if (rest === "/pages" && method === "PUT" && site.pages) {
|
|
357
|
+
const body = await request.json();
|
|
358
|
+
if (body.cname && options.takenDomains?.includes(body.cname)) return json({ message: `The CNAME \`${body.cname}\` is already taken.` }, 422);
|
|
359
|
+
if (body.cname) site.pages.cname = body.cname;
|
|
360
|
+
return new Response(null, { status: 204 });
|
|
361
|
+
}
|
|
362
|
+
const notAllowed = (permission) => principal.app ? principal.permissions?.[permission] !== "write" : principal.secrets === false;
|
|
363
|
+
if (rest === "/keys" && method === "POST") {
|
|
364
|
+
if (notAllowed("administration")) return json({ message: "Resource not accessible by integration" }, 403);
|
|
365
|
+
const body = await request.json();
|
|
366
|
+
if (!/^ssh-ed25519 [A-Za-z0-9+/=]+( |$)/.test(body.key)) return json({ message: "key is invalid" }, 422);
|
|
367
|
+
site.deployKeys.push({
|
|
368
|
+
title: body.title,
|
|
369
|
+
key: body.key,
|
|
370
|
+
read_only: body.read_only === true
|
|
371
|
+
});
|
|
372
|
+
return json({
|
|
373
|
+
id: site.deployKeys.length,
|
|
374
|
+
...body
|
|
375
|
+
}, 201);
|
|
376
|
+
}
|
|
377
|
+
if (rest === "/actions/secrets/public-key" && method === "GET") {
|
|
378
|
+
if (notAllowed("secrets")) return json({ message: "Resource not accessible by integration" }, 403);
|
|
379
|
+
site.secretKey ??= await crypto.subtle.generateKey({ name: "X25519" }, true, ["deriveBits"]);
|
|
380
|
+
const raw = new Uint8Array(await crypto.subtle.exportKey("raw", site.secretKey.publicKey));
|
|
381
|
+
return json({
|
|
382
|
+
key_id: `key-${site.id}`,
|
|
383
|
+
key: encodeBase64Bytes(raw)
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
const secret = rest.match(/^\/actions\/secrets\/([A-Z0-9_]+)$/);
|
|
387
|
+
if (secret && method === "PUT") {
|
|
388
|
+
if (notAllowed("secrets")) return json({ message: "Resource not accessible by integration" }, 403);
|
|
389
|
+
const body = await request.json();
|
|
390
|
+
if (!site.secretKey || body.key_id !== `key-${site.id}`) return json({ message: "Bad key id" }, 422);
|
|
391
|
+
try {
|
|
392
|
+
site.secrets.set(secret[1] ?? "", await openSealedSecret(decodeBase64(body.encrypted_value), site.secretKey));
|
|
393
|
+
} catch {
|
|
394
|
+
return json({ message: "Bad encrypted value" }, 422);
|
|
395
|
+
}
|
|
396
|
+
return new Response(null, { status: 201 });
|
|
397
|
+
}
|
|
282
398
|
if (rest === "/rulesets" && method === "POST") {
|
|
283
399
|
if (site.private && !options.paidPlan) return json({ message: "Upgrade to GitHub Pro" }, 403);
|
|
284
400
|
site.rulesets.push(await request.json());
|
|
@@ -357,6 +473,7 @@ function fakeGitHub(options) {
|
|
|
357
473
|
}
|
|
358
474
|
}
|
|
359
475
|
function deploy(sha) {
|
|
476
|
+
if (options.cloudflare) cloudflareBuilds.set(sha, { checks: 0 });
|
|
360
477
|
if (options.deployAfterChecks === void 0) return;
|
|
361
478
|
deployments.unshift({
|
|
362
479
|
id: nextDeployment++,
|
|
@@ -365,6 +482,20 @@ function fakeGitHub(options) {
|
|
|
365
482
|
checks: 0
|
|
366
483
|
});
|
|
367
484
|
}
|
|
485
|
+
/** Where a Cloudflare build is up to, counting the check. */
|
|
486
|
+
function cloudflareState(sha) {
|
|
487
|
+
const build = cloudflareBuilds.get(sha);
|
|
488
|
+
if (!build) return void 0;
|
|
489
|
+
build.checks += 1;
|
|
490
|
+
if (build.failed) return "failure";
|
|
491
|
+
return build.checks > (options.cloudflare?.afterChecks ?? 0) ? "success" : "building";
|
|
492
|
+
}
|
|
493
|
+
function failCloudflare(sha) {
|
|
494
|
+
cloudflareBuilds.set(sha, {
|
|
495
|
+
checks: 0,
|
|
496
|
+
failed: true
|
|
497
|
+
});
|
|
498
|
+
}
|
|
368
499
|
function commit(changes, message, expectedHead, author, branch = repo.defaultBranch) {
|
|
369
500
|
const sha = repo.commit(changes, message, {
|
|
370
501
|
expectedHead,
|
|
@@ -427,7 +558,8 @@ function fakeGitHub(options) {
|
|
|
427
558
|
pages: app.permissions.pages === "write",
|
|
428
559
|
workflow: app.permissions.workflows === "write",
|
|
429
560
|
actions: app.permissions.actions !== void 0,
|
|
430
|
-
actionsWrite: app.permissions.actions === "write"
|
|
561
|
+
actionsWrite: app.permissions.actions === "write",
|
|
562
|
+
variables: app.permissions.variables === "write"
|
|
431
563
|
});
|
|
432
564
|
refreshTokens.set(refresh, {
|
|
433
565
|
app,
|
|
@@ -535,7 +667,8 @@ function fakeGitHub(options) {
|
|
|
535
667
|
administration: permissions.administration === "write",
|
|
536
668
|
admin: permissions.administration === "write",
|
|
537
669
|
actions: permissions.actions !== void 0,
|
|
538
|
-
actionsWrite: permissions.actions === "write"
|
|
670
|
+
actionsWrite: permissions.actions === "write",
|
|
671
|
+
variables: permissions.variables === "write"
|
|
539
672
|
});
|
|
540
673
|
issued.push({
|
|
541
674
|
token,
|
|
@@ -699,6 +832,17 @@ function fakeGitHub(options) {
|
|
|
699
832
|
object: { sha: body.sha }
|
|
700
833
|
}, 201);
|
|
701
834
|
}
|
|
835
|
+
const matching = rest.match(/^\/git\/matching-refs\/heads\/(.*)$/);
|
|
836
|
+
if (matching) {
|
|
837
|
+
const prefix = decodeURIComponent(matching[1] ?? "");
|
|
838
|
+
return json([...repo.branches].filter(([branch]) => branch.startsWith(prefix)).map(([branch, sha]) => ({
|
|
839
|
+
ref: `refs/heads/${branch}`,
|
|
840
|
+
object: {
|
|
841
|
+
sha,
|
|
842
|
+
type: "commit"
|
|
843
|
+
}
|
|
844
|
+
})));
|
|
845
|
+
}
|
|
702
846
|
const branchRef = rest.match(/^\/git\/refs\/heads\/(.+)$/);
|
|
703
847
|
if (branchRef && request.method === "DELETE") {
|
|
704
848
|
if (user.push === false) return json({ message: "Resource not accessible by integration" }, 403);
|
|
@@ -756,6 +900,8 @@ function fakeGitHub(options) {
|
|
|
756
900
|
if (rest === "") return json({
|
|
757
901
|
id: 1,
|
|
758
902
|
full_name: options.repo,
|
|
903
|
+
private: options.private === true,
|
|
904
|
+
visibility: options.private === true ? "private" : "public",
|
|
759
905
|
default_branch: repo.defaultBranch,
|
|
760
906
|
permissions: {
|
|
761
907
|
push: user.push !== false,
|
|
@@ -814,7 +960,35 @@ function fakeGitHub(options) {
|
|
|
814
960
|
author: null
|
|
815
961
|
})));
|
|
816
962
|
}
|
|
817
|
-
|
|
963
|
+
const variable = rest.match(/^\/actions\/variables(?:\/([\w-]+))?$/);
|
|
964
|
+
if (variable) {
|
|
965
|
+
if (!(user.variables ?? user.administration)) return json({ message: "Resource not accessible by personal access token" }, 403);
|
|
966
|
+
const [, name] = variable;
|
|
967
|
+
if (!name && request.method === "POST") {
|
|
968
|
+
const body = await request.json();
|
|
969
|
+
if (variables.has(body.name)) return json({ message: "Already exists" }, 409);
|
|
970
|
+
variables.set(body.name, body.value);
|
|
971
|
+
return new Response(null, { status: 201 });
|
|
972
|
+
}
|
|
973
|
+
if (name && !variables.has(name)) return json({ message: "Not Found" }, 404);
|
|
974
|
+
if (name && request.method === "GET") return json({
|
|
975
|
+
name,
|
|
976
|
+
value: variables.get(name)
|
|
977
|
+
});
|
|
978
|
+
if (name && request.method === "PATCH") {
|
|
979
|
+
const body = await request.json();
|
|
980
|
+
variables.set(name, body.value);
|
|
981
|
+
return new Response(null, { status: 204 });
|
|
982
|
+
}
|
|
983
|
+
if (name && request.method === "DELETE") {
|
|
984
|
+
variables.delete(name);
|
|
985
|
+
return new Response(null, { status: 204 });
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
const workflowFile = rest.match(/^\/actions\/workflows\/([\w.-]+)\/(?:dispatches|runs)$/)?.[1];
|
|
989
|
+
const workflowPath = `.github/workflows/${workflowFile}`;
|
|
990
|
+
const ofWorkflow = (run) => (run.path ?? ".github/workflows/deploy.yml") === workflowPath;
|
|
991
|
+
if (workflowFile && rest.endsWith("/dispatches") && request.method === "POST") {
|
|
818
992
|
if (!(user.actionsWrite ?? user.administration)) return json({ message: "Resource not accessible by personal access token" }, 403);
|
|
819
993
|
const body = await request.json();
|
|
820
994
|
runs.push({
|
|
@@ -824,15 +998,17 @@ function fakeGitHub(options) {
|
|
|
824
998
|
inputs: body.inputs,
|
|
825
999
|
status: "queued",
|
|
826
1000
|
conclusion: null,
|
|
1001
|
+
path: workflowPath,
|
|
827
1002
|
jobs: []
|
|
828
1003
|
});
|
|
829
1004
|
return new Response(null, { status: 204 });
|
|
830
1005
|
}
|
|
831
|
-
if (
|
|
1006
|
+
if (workflowFile && rest.endsWith("/runs")) {
|
|
832
1007
|
if (user.actions === false) return json({ message: "Resource not accessible by personal access token" }, 403);
|
|
1008
|
+
const found = runs.filter(ofWorkflow);
|
|
833
1009
|
return json({
|
|
834
|
-
total_count:
|
|
835
|
-
workflow_runs: [...
|
|
1010
|
+
total_count: found.length,
|
|
1011
|
+
workflow_runs: [...found].reverse().map((run) => ({
|
|
836
1012
|
id: run.id,
|
|
837
1013
|
event: run.event ?? "push",
|
|
838
1014
|
head_sha: run.sha,
|
|
@@ -856,8 +1032,8 @@ function fakeGitHub(options) {
|
|
|
856
1032
|
head_sha: run.sha,
|
|
857
1033
|
status: run.status,
|
|
858
1034
|
conclusion: run.conclusion,
|
|
859
|
-
name: "Deploy to GitHub Pages",
|
|
860
|
-
path: ".github/workflows/deploy.yml",
|
|
1035
|
+
name: run.path ? "Rebuild on Vercel" : "Deploy to GitHub Pages",
|
|
1036
|
+
path: run.path ?? ".github/workflows/deploy.yml",
|
|
861
1037
|
html_url: htmlUrl(run)
|
|
862
1038
|
}))
|
|
863
1039
|
});
|
|
@@ -881,6 +1057,43 @@ function fakeGitHub(options) {
|
|
|
881
1057
|
files: repo.changedPaths(base, b).map((filename) => ({ filename }))
|
|
882
1058
|
});
|
|
883
1059
|
}
|
|
1060
|
+
const commitSha = rest.match(/^\/commits\/([0-9a-f]{40})$/)?.[1];
|
|
1061
|
+
if (commitSha) {
|
|
1062
|
+
const found = repo.commitAt(commitSha);
|
|
1063
|
+
if (!found) return json({ message: "No commit found for SHA" }, 422);
|
|
1064
|
+
return json({
|
|
1065
|
+
sha: commitSha,
|
|
1066
|
+
parents: found.parent ? [{ sha: found.parent }] : []
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
const checkRunsSha = rest.match(/^\/commits\/([0-9a-f]{40})\/check-runs$/)?.[1];
|
|
1070
|
+
if (checkRunsSha) {
|
|
1071
|
+
if (options.checkRunsForbidden) return json({ message: "Resource not accessible by personal access token" }, 403);
|
|
1072
|
+
const state = options.cloudflare?.report === "statuses" ? void 0 : cloudflareState(checkRunsSha);
|
|
1073
|
+
const runsFound = state ? [{
|
|
1074
|
+
id: 1,
|
|
1075
|
+
name: "Cloudflare Pages",
|
|
1076
|
+
app: { slug: "cloudflare-workers-and-pages" },
|
|
1077
|
+
status: state === "building" ? "in_progress" : "completed",
|
|
1078
|
+
conclusion: state === "building" ? null : state,
|
|
1079
|
+
details_url: `https://dash.cloudflare.com/builds/${checkRunsSha}`,
|
|
1080
|
+
output: { title: state === "failure" ? "Build failed" : "Deployed" }
|
|
1081
|
+
}] : [];
|
|
1082
|
+
return json({
|
|
1083
|
+
total_count: runsFound.length,
|
|
1084
|
+
check_runs: runsFound
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
const statusesSha = rest.match(/^\/commits\/([0-9a-f]{40})\/statuses$/)?.[1];
|
|
1088
|
+
if (statusesSha) {
|
|
1089
|
+
const state = options.cloudflare?.report === "statuses" ? cloudflareState(statusesSha) : void 0;
|
|
1090
|
+
return json(state ? [{
|
|
1091
|
+
context: "Cloudflare Pages",
|
|
1092
|
+
state: state === "building" ? "pending" : state,
|
|
1093
|
+
target_url: `https://dash.cloudflare.com/builds/${statusesSha}`,
|
|
1094
|
+
description: state === "failure" ? "Build failed" : "Deployed"
|
|
1095
|
+
}] : []);
|
|
1096
|
+
}
|
|
884
1097
|
if (rest === "/deployments") {
|
|
885
1098
|
const sha = url.searchParams.get("sha");
|
|
886
1099
|
return json(deployments.filter((deployment) => !sha || deployment.sha === sha).slice(0, Number(url.searchParams.get("per_page") ?? 30)).map(({ id, sha }) => ({
|
|
@@ -913,8 +1126,9 @@ function fakeGitHub(options) {
|
|
|
913
1126
|
commitAuthors,
|
|
914
1127
|
authorize,
|
|
915
1128
|
install,
|
|
916
|
-
/** Repositories created through the API, by `owner/name`. */
|
|
1129
|
+
/** Repositories created through the API, or added with `addRepo()`, by `owner/name`. */
|
|
917
1130
|
created,
|
|
1131
|
+
addRepo,
|
|
918
1132
|
/** The site's GitHub Pages settings, which tests can change, such as to issue the certificate. */
|
|
919
1133
|
pages,
|
|
920
1134
|
/** The repository's collaborators, by login, and invitations waiting to be accepted. */
|
|
@@ -922,9 +1136,15 @@ function fakeGitHub(options) {
|
|
|
922
1136
|
invitations,
|
|
923
1137
|
accept,
|
|
924
1138
|
deployments,
|
|
925
|
-
/**
|
|
1139
|
+
/** Cloudflare Pages' builds, by commit, with `cloudflare`; `failCloudflare()` makes one fail. */
|
|
1140
|
+
cloudflareBuilds,
|
|
1141
|
+
failCloudflare,
|
|
1142
|
+
/** GitHub Actions workflow runs, which tests add with `failBuild()` and `cancelBuild()`. */
|
|
926
1143
|
runs,
|
|
1144
|
+
/** The repository's Actions variables, such as `GOODFELLOW_NIGHTLY`. */
|
|
1145
|
+
variables,
|
|
927
1146
|
failBuild,
|
|
1147
|
+
cancelBuild,
|
|
928
1148
|
/** Every request received, for checking what the backend sent. */
|
|
929
1149
|
requests,
|
|
930
1150
|
/** Simulates someone else publishing. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodfellow-cms/github",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Edit a Goodfellow site stored on GitHub, straight from the browser.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"goodfellow",
|
|
@@ -35,13 +35,15 @@
|
|
|
35
35
|
"dist"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@goodfellow-cms/core": "0.
|
|
38
|
+
"@goodfellow-cms/core": "0.5.0",
|
|
39
|
+
"@noble/ciphers": "2.4.0",
|
|
40
|
+
"@noble/hashes": "2.4.0"
|
|
39
41
|
},
|
|
40
42
|
"devDependencies": {
|
|
41
43
|
"@puckeditor/core": "0.23.0"
|
|
42
44
|
},
|
|
43
45
|
"engines": {
|
|
44
|
-
"node": ">=
|
|
46
|
+
"node": "^22.23.4 || ^24.22.1 || >=26.12.1"
|
|
45
47
|
},
|
|
46
48
|
"publishConfig": {
|
|
47
49
|
"access": "public"
|