@goodfellow-cms/github 0.1.0 → 0.2.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/testing.d.ts CHANGED
@@ -6,6 +6,52 @@ export interface FakeGitHubUser {
6
6
  name?: string;
7
7
  /** Whether the user may publish. Defaults to true. */
8
8
  push?: boolean;
9
+ /** Whether the token may write `.github/workflows/`. Defaults to true. */
10
+ workflow?: boolean;
11
+ /** Whether the token may change GitHub Pages settings. Defaults to true. */
12
+ pages?: boolean;
13
+ /** Whether the user is an owner (admin) of the site's repository. Defaults to false. */
14
+ admin?: boolean;
15
+ /** Whether the token has GitHub's Administration permission, which changing collaborators needs. Defaults to false. */
16
+ administration?: boolean;
17
+ /** Whether the token may read GitHub Actions. Defaults to true. */
18
+ actions?: boolean;
19
+ /** Whether the token may run workflows, as owner tokens can. Defaults to the `administration` setting. */
20
+ actionsWrite?: boolean;
21
+ }
22
+ /** A GitHub Actions workflow run, with its jobs and their steps. */
23
+ export interface FakeRun {
24
+ id: number;
25
+ sha: string;
26
+ /** What started it: `push` (the default), `schedule` or `workflow_dispatch`. */
27
+ event?: string;
28
+ /** For `workflow_dispatch`, the inputs it was given. */
29
+ inputs?: Record<string, string>;
30
+ createdAt?: string;
31
+ status: "queued" | "in_progress" | "completed";
32
+ conclusion: "success" | "failure" | "startup_failure" | "cancelled" | null;
33
+ jobs: Array<{
34
+ name: string;
35
+ conclusion: string | null;
36
+ steps: Array<{
37
+ name: string;
38
+ conclusion: string | null;
39
+ }>;
40
+ }>;
41
+ }
42
+ /** A repository invitation that hasn't been accepted yet. */
43
+ export interface FakeInvitation {
44
+ id: number;
45
+ login: string;
46
+ permission: "admin" | "write";
47
+ }
48
+ /** The site's GitHub Pages settings, as `GET /repos/{repo}/pages` reports them. */
49
+ export interface FakePages {
50
+ html_url: string;
51
+ cname: string | null;
52
+ https_enforced: boolean;
53
+ /** The certificate's state, such as `new` while it's being issued and `approved` once it's ready. */
54
+ certificate?: string;
9
55
  }
10
56
  export interface FakeDeploymentStatus {
11
57
  state: "queued" | "in_progress" | "success" | "failure" | "error" | "inactive";
@@ -22,6 +68,29 @@ export interface FakeGitHubOptions {
22
68
  * reports success after being checked this many times. Off by default.
23
69
  */
24
70
  deployAfterChecks?: number;
71
+ /** Organizations the users belong to, which new repositories can be created in. */
72
+ orgs?: string[];
73
+ /** The default branch of repositories created through the API, as an account's settings choose. Defaults to `main`. */
74
+ newRepoBranch?: string;
75
+ /** Whether private repositories can have GitHub Pages and rules, as on paid plans. Off by default, as on the free plan. */
76
+ paidPlan?: boolean;
77
+ /** Publishes the site with GitHub Pages, at `https://<owner>.github.io/<name>/`. Off by default. */
78
+ pages?: boolean;
79
+ /** Domains another GitHub Pages site already uses. */
80
+ takenDomains?: string[];
81
+ /** Other GitHub accounts that exist, which can be invited. Every token's user exists too. */
82
+ accounts?: string[];
83
+ }
84
+ /** A repository created through the fake's API, as the setup page creates them. */
85
+ export interface FakeCreatedRepo {
86
+ fullName: string;
87
+ private: boolean;
88
+ description?: string;
89
+ repo: FakeRepo;
90
+ pages?: {
91
+ build_type: string;
92
+ };
93
+ rulesets: unknown[];
25
94
  }
26
95
  interface Deployment {
27
96
  id: number;
@@ -31,11 +100,24 @@ interface Deployment {
31
100
  }
32
101
  export declare function fakeGitHub(options: FakeGitHubOptions): {
33
102
  repo: FakeRepo;
103
+ /** Repositories created through the API, by `owner/name`. */
104
+ created: Map<string, FakeCreatedRepo>;
105
+ /** The site's GitHub Pages settings, which tests can change, such as to issue the certificate. */
106
+ pages: {
107
+ current?: FakePages;
108
+ };
109
+ /** The repository's collaborators, by login, and invitations waiting to be accepted. */
110
+ collaborators: Map<string, "admin" | "push" | "read">;
111
+ invitations: FakeInvitation[];
112
+ accept: (login: string) => void;
34
113
  deployments: Deployment[];
114
+ /** GitHub Actions workflow runs, which tests add with `failBuild()`. */
115
+ runs: FakeRun[];
116
+ failBuild: (sha: string, step?: string, job?: string) => FakeRun;
35
117
  /** Every request received, for checking what the backend sent. */
36
118
  requests: Request[];
37
119
  /** Simulates someone else publishing. */
38
- commit: (changes: FileChange[], message: string, expectedHead?: string) => string;
120
+ commit: (changes: FileChange[], message: string, expectedHead?: string, author?: string) => string;
39
121
  handle: (request: Request) => Promise<Response>;
40
122
  /** A `fetch` that answers from the fake instead of the network. */
41
123
  fetch: typeof fetch;
package/dist/testing.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { decodeBase64, encodeBase64Bytes } from "@goodfellow-cms/core";
2
- import { FakeConflictError, FakeRepo, fakeFileBytes } from "@goodfellow-cms/core/testing";
2
+ import { FakeConflictError, FakeRepo, fakeFileBytes, fakeFileFromBytes } from "@goodfellow-cms/core/testing";
3
3
  //#region src/testing.ts
4
4
  /**
5
5
  * A fake of the parts of GitHub's API that `@goodfellow-cms/github` uses, backed by
@@ -19,8 +19,246 @@ function fakeGitHub(options) {
19
19
  name: "Test Editor"
20
20
  } };
21
21
  const deployments = [];
22
+ const runs = [];
22
23
  const requests = [];
23
24
  let nextDeployment = 1;
25
+ const created = /* @__PURE__ */ new Map();
26
+ const [owner = "", name = ""] = options.repo.split("/");
27
+ const pages = { current: options.pages ? {
28
+ html_url: `https://${owner.toLowerCase()}.github.io/${name}/`,
29
+ cname: null,
30
+ https_enforced: false
31
+ } : void 0 };
32
+ const trees = /* @__PURE__ */ new Map();
33
+ /** The repository's collaborators and their permission. Every token's user is one. */
34
+ const collaborators = new Map(Object.values(tokens).map((user) => [user.login, user.admin ? "admin" : user.push === false ? "read" : "push"]));
35
+ const invitations = [];
36
+ const accounts = /* @__PURE__ */ new Set([...Object.values(tokens).map((user) => user.login), ...options.accounts ?? []]);
37
+ let nextInvitation = 1;
38
+ /** Collaborators and invitations, which only owners' tokens with the Administration permission can change. */
39
+ async function handleCollaborators(request, rest, user) {
40
+ const forbidden = () => json({ message: "Resource not accessible by personal access token" }, 403);
41
+ const canManage = user.admin === true && user.administration === true;
42
+ if (rest === "/collaborators" && request.method === "GET") return json([...collaborators].map(([login, permission]) => ({
43
+ login,
44
+ avatar_url: `https://avatars.example/${login}`,
45
+ permissions: {
46
+ admin: permission === "admin",
47
+ maintain: false,
48
+ push: permission !== "read",
49
+ pull: true
50
+ }
51
+ })));
52
+ if (rest === "/invitations" && request.method === "GET") {
53
+ if (!canManage) return forbidden();
54
+ return json(invitations.map((invitation) => ({
55
+ id: invitation.id,
56
+ invitee: {
57
+ login: invitation.login,
58
+ avatar_url: `https://avatars.example/${invitation.login}`
59
+ },
60
+ permissions: invitation.permission
61
+ })));
62
+ }
63
+ const collaborator = rest.match(/^\/collaborators\/([^/]+)$/);
64
+ if (collaborator) {
65
+ if (!canManage) return forbidden();
66
+ const login = decodeURIComponent(collaborator[1] ?? "");
67
+ if (request.method === "DELETE") {
68
+ collaborators.delete(login);
69
+ return new Response(null, { status: 204 });
70
+ }
71
+ if (request.method === "PUT") {
72
+ if (!accounts.has(login)) return json({ message: "Not Found" }, 404);
73
+ const { permission = "push" } = await request.json();
74
+ if (collaborators.has(login)) {
75
+ collaborators.set(login, permission);
76
+ return new Response(null, { status: 204 });
77
+ }
78
+ const existing = invitations.find((invitation) => invitation.login === login);
79
+ const invitation = existing ?? {
80
+ id: nextInvitation++,
81
+ login,
82
+ permission: "write"
83
+ };
84
+ invitation.permission = permission === "admin" ? "admin" : "write";
85
+ if (!existing) invitations.push(invitation);
86
+ return json({
87
+ id: invitation.id,
88
+ invitee: { login },
89
+ permissions: invitation.permission
90
+ }, 201);
91
+ }
92
+ }
93
+ const invitation = rest.match(/^\/invitations\/(\d+)$/);
94
+ if (invitation) {
95
+ if (!canManage) return forbidden();
96
+ const index = invitations.findIndex((candidate) => candidate.id === Number(invitation[1]));
97
+ if (index === -1) return json({ message: "Not Found" }, 404);
98
+ if (request.method === "DELETE") {
99
+ invitations.splice(index, 1);
100
+ return new Response(null, { status: 204 });
101
+ }
102
+ if (request.method === "PATCH") {
103
+ const { permissions } = await request.json();
104
+ invitations[index].permission = permissions;
105
+ return json({
106
+ id: invitations[index]?.id,
107
+ permissions
108
+ });
109
+ }
110
+ }
111
+ }
112
+ /**
113
+ * Simulates the deploy workflow failing for a commit at a step, such as
114
+ * `"Run npm ci"`, in the `build` job or another, or before any step ran.
115
+ */
116
+ function failBuild(sha, step, job = "build") {
117
+ const run = {
118
+ id: runs.length + 1,
119
+ sha,
120
+ status: "completed",
121
+ conclusion: "failure",
122
+ jobs: [{
123
+ name: job,
124
+ conclusion: "failure",
125
+ steps: step ? [{
126
+ name: "Set up job",
127
+ conclusion: "success"
128
+ }, {
129
+ name: step,
130
+ conclusion: "failure"
131
+ }] : []
132
+ }]
133
+ };
134
+ runs.push(run);
135
+ return run;
136
+ }
137
+ /** Simulates an invited person accepting the invitation. */
138
+ function accept(login) {
139
+ const index = invitations.findIndex((invitation) => invitation.login === login);
140
+ const [invitation] = index === -1 ? [] : invitations.splice(index, 1);
141
+ if (invitation) collaborators.set(login, invitation.permission === "admin" ? "admin" : "push");
142
+ }
143
+ /** Repository creation and the Git data API, for repositories created through the fake. */
144
+ async function handleCreated(request, path, login) {
145
+ if (path === "/user/orgs") return json((options.orgs ?? []).map((org) => ({ login: org })));
146
+ const create = path === "/user/repos" ? login : path.match(/^\/orgs\/([^/]+)\/repos$/)?.[1];
147
+ if (create !== void 0 && request.method === "POST") {
148
+ if (path !== "/user/repos" && !options.orgs?.includes(create)) return json({ message: "Not Found" }, 404);
149
+ const body = await request.json();
150
+ if (!/^[\w.-]+$/.test(body.name)) return json({ message: "Repository creation failed." }, 422);
151
+ const fullName = `${create}/${body.name}`;
152
+ if (created.has(fullName) || fullName === options.repo) return json({
153
+ message: "Repository creation failed.",
154
+ errors: [{ message: "name already exists on this account" }]
155
+ }, 422);
156
+ const repo = new FakeRepo({ "README.md": `# ${body.name}\n` }, options.newRepoBranch ?? "main");
157
+ created.set(fullName, {
158
+ fullName,
159
+ private: body.private === true,
160
+ description: body.description,
161
+ repo,
162
+ rulesets: []
163
+ });
164
+ return json({
165
+ full_name: fullName,
166
+ html_url: `https://github.com/${fullName}`,
167
+ default_branch: repo.defaultBranch
168
+ }, 201);
169
+ }
170
+ const match = path.match(/^\/repos\/([^/]+\/[^/]+)(\/.*)?$/);
171
+ const site = created.get(decodeURIComponent(match?.[1] ?? ""));
172
+ if (!match || !site) return void 0;
173
+ const { repo } = site;
174
+ const rest = match[2] ?? "";
175
+ const method = request.method;
176
+ if (rest === "" && method === "GET") return json({
177
+ full_name: site.fullName,
178
+ default_branch: repo.defaultBranch,
179
+ permissions: { push: true }
180
+ });
181
+ if (rest === "" && method === "PATCH") {
182
+ const body = await request.json();
183
+ if (body.default_branch) {
184
+ if (!repo.branches.has(body.default_branch)) return json({ message: "Not Found" }, 422);
185
+ repo.defaultBranch = body.default_branch;
186
+ }
187
+ return json({
188
+ full_name: site.fullName,
189
+ default_branch: repo.defaultBranch
190
+ });
191
+ }
192
+ const ref = rest.match(/^\/git\/refs?\/heads\/(.+)$/);
193
+ if (ref && method === "GET") {
194
+ const sha = repo.branches.get(decodeURIComponent(ref[1] ?? ""));
195
+ return sha ? json({ object: { sha } }) : json({ message: "Not Found" }, 404);
196
+ }
197
+ if (ref && method === "DELETE") {
198
+ repo.branches.delete(decodeURIComponent(ref[1] ?? ""));
199
+ return new Response(null, { status: 204 });
200
+ }
201
+ if (ref && method === "PATCH") {
202
+ const body = await request.json();
203
+ const branch = decodeURIComponent(ref[1] ?? "");
204
+ const commit = repo.commitAt(body.sha);
205
+ if (!commit || !repo.branches.has(branch)) return json({ message: "Reference does not exist" }, 422);
206
+ if (!body.force && commit.parent !== repo.branches.get(branch)) return json({ message: "Update is not a fast forward" }, 422);
207
+ if (site.rulesets.length > 0 && commit.parent !== repo.branches.get(branch)) return json({ message: "Cannot force-push to this branch" }, 422);
208
+ repo.branches.set(branch, body.sha);
209
+ deploy(body.sha);
210
+ return json({ object: { sha: body.sha } });
211
+ }
212
+ if (rest === "/git/refs" && method === "POST") {
213
+ const body = await request.json();
214
+ repo.branches.set(body.ref.replace(/^refs\/heads\//, ""), body.sha);
215
+ return json({
216
+ ref: body.ref,
217
+ object: { sha: body.sha }
218
+ }, 201);
219
+ }
220
+ if (rest === "/git/blobs" && method === "POST") {
221
+ const body = await request.json();
222
+ return json({ sha: repo.blobSha(body.encoding === "base64" ? fakeFileFromBytes(decodeBase64(body.content)) : body.content) }, 201);
223
+ }
224
+ if (rest === "/git/trees" && method === "POST") {
225
+ const body = await request.json();
226
+ const files = /* @__PURE__ */ new Map();
227
+ for (const entry of body.tree) {
228
+ if (entry.path.startsWith(".github/workflows/") && user(request)?.workflow === false) return json({ message: "Resource not accessible by personal access token" }, 404);
229
+ const content = entry.content ?? repo.blobs.get(entry.sha ?? "");
230
+ if (content === void 0) return json({ message: `No blob ${entry.sha}` }, 422);
231
+ files.set(entry.path, content);
232
+ }
233
+ const sha = repo.blobSha(`tree:${[...files.keys()].join(",")}:${trees.size}`);
234
+ trees.set(sha, files);
235
+ return json({ sha }, 201);
236
+ }
237
+ if (rest === "/git/commits" && method === "POST") {
238
+ const body = await request.json();
239
+ const files = trees.get(body.tree);
240
+ if (!files) return json({ message: "Tree not found" }, 422);
241
+ return json({ sha: repo.addCommit(body.parents[0], body.message, new Map(files)) }, 201);
242
+ }
243
+ if (rest === "/pages" && method === "POST") {
244
+ if (site.private && !options.paidPlan) return json({ message: "Your current plan does not support GitHub Pages for this repository." }, 422);
245
+ site.pages = await request.json();
246
+ const [owner, name] = site.fullName.split("/");
247
+ return json({
248
+ html_url: `https://${owner}.github.io/${name}/`,
249
+ build_type: site.pages.build_type
250
+ }, 201);
251
+ }
252
+ if (rest === "/rulesets" && method === "POST") {
253
+ if (site.private && !options.paidPlan) return json({ message: "Upgrade to GitHub Pro" }, 403);
254
+ site.rulesets.push(await request.json());
255
+ return json({ id: site.rulesets.length }, 201);
256
+ }
257
+ return json({ message: "Not Found" }, 404);
258
+ }
259
+ function user(request) {
260
+ return tokens[request.headers.get("authorization")?.replace(/^Bearer /, "") ?? ""];
261
+ }
24
262
  function deploy(sha) {
25
263
  if (options.deployAfterChecks === void 0) return;
26
264
  deployments.unshift({
@@ -30,8 +268,11 @@ function fakeGitHub(options) {
30
268
  checks: 0
31
269
  });
32
270
  }
33
- function commit(changes, message, expectedHead) {
34
- const sha = repo.commit(changes, message, { expectedHead });
271
+ function commit(changes, message, expectedHead, author) {
272
+ const sha = repo.commit(changes, message, {
273
+ expectedHead,
274
+ author
275
+ });
35
276
  deploy(sha);
36
277
  return sha;
37
278
  }
@@ -46,6 +287,13 @@ function fakeGitHub(options) {
46
287
  name: user.name ?? null,
47
288
  avatar_url: `https://avatars.example/${user.login}`
48
289
  });
290
+ const setup = await handleCreated(request, path, user.login);
291
+ if (setup) return setup;
292
+ const account = path.match(/^\/users\/([^/]+)$/);
293
+ if (account) {
294
+ const login = decodeURIComponent(account[1] ?? "");
295
+ return accounts.has(login) ? json({ login }) : json({ message: "Not Found" }, 404);
296
+ }
49
297
  if (path === "/graphql" && request.method === "POST") {
50
298
  const { variables } = await request.json();
51
299
  const { input } = variables;
@@ -66,7 +314,7 @@ function fakeGitHub(options) {
66
314
  })), ...(input.fileChanges.deletions ?? []).map((deletion) => ({
67
315
  path: deletion.path,
68
316
  delete: true
69
- }))], input.message.headline, input.expectedHeadOid) } } } });
317
+ }))], input.message.headline, input.expectedHeadOid, user.name ?? user.login) } } } });
70
318
  } catch (error) {
71
319
  if (error instanceof FakeConflictError) return json({
72
320
  data: { createCommitOnBranch: null },
@@ -81,11 +329,44 @@ function fakeGitHub(options) {
81
329
  const match = path.match(/^\/repos\/([^/]+\/[^/]+)(\/.*)?$/);
82
330
  if (!match || decodeURIComponent(match[1] ?? "") !== options.repo) return json({ message: "Not Found" }, 404);
83
331
  const rest = match[2] ?? "";
332
+ if (rest === "/pages" && request.method === "GET") {
333
+ const site = pages.current;
334
+ if (!site) return json({ message: "Not Found" }, 404);
335
+ return json({
336
+ html_url: site.cname ? `https://${site.cname}/` : site.html_url,
337
+ cname: site.cname,
338
+ https_enforced: site.https_enforced,
339
+ https_certificate: site.cname ? {
340
+ state: site.certificate ?? "new",
341
+ domains: [site.cname]
342
+ } : null
343
+ });
344
+ }
345
+ if (rest === "/pages" && request.method === "PUT") {
346
+ const site = pages.current;
347
+ if (!site) return json({ message: "Not Found" }, 404);
348
+ if (user.pages === false) return json({ message: "Resource not accessible by personal access token" }, 403);
349
+ const body = await request.json();
350
+ if (body.cname && options.takenDomains?.includes(body.cname)) return json({ message: `The CNAME \`${body.cname}\` is already taken.` }, 422);
351
+ if (body.https_enforced && !["approved", "issued"].includes(site.certificate ?? "")) return json({ message: "The certificate does not exist yet" }, 422);
352
+ if (body.cname !== void 0) {
353
+ site.cname = body.cname;
354
+ site.certificate = body.cname ? "new" : void 0;
355
+ if (!body.cname) site.https_enforced = false;
356
+ }
357
+ if (body.https_enforced !== void 0) site.https_enforced = body.https_enforced;
358
+ return new Response(null, { status: 204 });
359
+ }
84
360
  if (rest === "") return json({
85
361
  full_name: options.repo,
86
362
  default_branch: repo.defaultBranch,
87
- permissions: { push: user.push !== false }
363
+ permissions: {
364
+ push: user.push !== false,
365
+ admin: user.admin === true
366
+ }
88
367
  });
368
+ const people = await handleCollaborators(request, rest, user);
369
+ if (people) return people;
89
370
  const ref = rest.match(/^\/git\/ref\/heads\/(.+)$/);
90
371
  if (ref) {
91
372
  const sha = repo.branches.get(decodeURIComponent(ref[1] ?? ""));
@@ -117,6 +398,79 @@ function fakeGitHub(options) {
117
398
  encoding: "base64"
118
399
  });
119
400
  }
401
+ if (rest === "/commits") {
402
+ const ref = url.searchParams.get("sha") ?? repo.defaultBranch;
403
+ const file = url.searchParams.get("path");
404
+ if (!repo.commitAt(repo.branches.get(ref) ?? ref)) return json({ message: "No commit found for SHA" }, 422);
405
+ const perPage = Number(url.searchParams.get("per_page") ?? 30);
406
+ const page = Number(url.searchParams.get("page") ?? 1);
407
+ return json((file ? repo.history(file, ref) : []).slice((page - 1) * perPage, page * perPage).map((commit) => ({
408
+ sha: commit.sha,
409
+ commit: {
410
+ message: commit.message,
411
+ author: {
412
+ name: commit.author ?? "GitHub",
413
+ date: commit.date
414
+ }
415
+ },
416
+ author: null
417
+ })));
418
+ }
419
+ if (rest === "/actions/workflows/deploy.yml/dispatches" && request.method === "POST") {
420
+ if (!(user.actionsWrite ?? user.administration)) return json({ message: "Resource not accessible by personal access token" }, 403);
421
+ const body = await request.json();
422
+ runs.push({
423
+ id: runs.length + 1,
424
+ sha: repo.head(body.ref),
425
+ event: "workflow_dispatch",
426
+ inputs: body.inputs,
427
+ status: "queued",
428
+ conclusion: null,
429
+ jobs: []
430
+ });
431
+ return new Response(null, { status: 204 });
432
+ }
433
+ if (rest === "/actions/workflows/deploy.yml/runs") {
434
+ if (user.actions === false) return json({ message: "Resource not accessible by personal access token" }, 403);
435
+ return json({
436
+ total_count: runs.length,
437
+ workflow_runs: [...runs].reverse().map((run) => ({
438
+ id: run.id,
439
+ event: run.event ?? "push",
440
+ head_sha: run.sha,
441
+ status: run.status,
442
+ conclusion: run.conclusion,
443
+ created_at: run.createdAt ?? "2026-01-05T04:17:00Z",
444
+ html_url: `https://github.com/${options.repo}/actions/runs/${run.id}`
445
+ }))
446
+ });
447
+ }
448
+ if (rest === "/actions/runs" || rest.startsWith("/actions/runs/")) {
449
+ if (user.actions === false) return json({ message: "Resource not accessible by personal access token" }, 403);
450
+ const htmlUrl = (run) => `https://github.com/${options.repo}/actions/runs/${run.id}`;
451
+ if (rest === "/actions/runs") {
452
+ const sha = url.searchParams.get("head_sha");
453
+ const found = runs.filter((run) => !sha || run.sha === sha);
454
+ return json({
455
+ total_count: found.length,
456
+ workflow_runs: found.map((run) => ({
457
+ id: run.id,
458
+ head_sha: run.sha,
459
+ status: run.status,
460
+ conclusion: run.conclusion,
461
+ name: "Deploy to GitHub Pages",
462
+ path: ".github/workflows/deploy.yml",
463
+ html_url: htmlUrl(run)
464
+ }))
465
+ });
466
+ }
467
+ const jobs = rest.match(/^\/actions\/runs\/(\d+)\/jobs$/);
468
+ const run = runs.find((candidate) => candidate.id === Number(jobs?.[1]));
469
+ return run ? json({
470
+ total_count: run.jobs.length,
471
+ jobs: run.jobs
472
+ }) : json({ message: "Not Found" }, 404);
473
+ }
120
474
  const compare = rest.match(/^\/compare\/([0-9a-f]{40})\.\.\.([0-9a-f]{40})$/);
121
475
  if (compare) return json({ files: repo.changedPaths(compare[1] ?? "", compare[2] ?? "").map((filename) => ({ filename })) });
122
476
  if (rest === "/deployments") {
@@ -141,7 +495,18 @@ function fakeGitHub(options) {
141
495
  }
142
496
  return {
143
497
  repo,
498
+ /** Repositories created through the API, by `owner/name`. */
499
+ created,
500
+ /** The site's GitHub Pages settings, which tests can change, such as to issue the certificate. */
501
+ pages,
502
+ /** The repository's collaborators, by login, and invitations waiting to be accepted. */
503
+ collaborators,
504
+ invitations,
505
+ accept,
144
506
  deployments,
507
+ /** GitHub Actions workflow runs, which tests add with `failBuild()`. */
508
+ runs,
509
+ failBuild,
145
510
  /** Every request received, for checking what the backend sent. */
146
511
  requests,
147
512
  /** Simulates someone else publishing. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodfellow-cms/github",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Edit a Goodfellow site stored on GitHub, straight from the browser.",
5
5
  "keywords": [
6
6
  "goodfellow",
@@ -10,12 +10,12 @@
10
10
  "github",
11
11
  "git-based-cms"
12
12
  ],
13
- "homepage": "https://chartung17.github.io/goodfellow-cms/docs/hosts",
14
- "bugs": "https://github.com/chartung17/goodfellow-cms/issues",
13
+ "homepage": "https://goodfellow-cms.github.io/goodfellow-cms/docs/hosts",
14
+ "bugs": "https://github.com/goodfellow-cms/goodfellow-cms/issues",
15
15
  "license": "MIT",
16
16
  "repository": {
17
17
  "type": "git",
18
- "url": "git+https://github.com/chartung17/goodfellow-cms.git",
18
+ "url": "git+https://github.com/goodfellow-cms/goodfellow-cms.git",
19
19
  "directory": "packages/github"
20
20
  },
21
21
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  "dist"
36
36
  ],
37
37
  "dependencies": {
38
- "@goodfellow-cms/core": "0.1.0"
38
+ "@goodfellow-cms/core": "0.2.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@puckeditor/core": "0.23.0"