@goodfellow-cms/github 0.4.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/index.d.ts +21 -5
- package/dist/index.js +253 -27
- package/dist/testing.d.ts +28 -1
- package/dist/testing.js +127 -3
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConnectDomainOptions, DeployStatus, DnsRecord, FileChange, FileVersion, GitBackend, GitHost, GitUser, HistoryOptions, OwnerAccess, PagesDomains, RedirectOptions, SetupHost, SignInError, SiteDrafts, SiteEditors, SiteUpdates, StorageLike, TokenLink, WriteOptions } from "@goodfellow-cms/core";
|
|
1
|
+
import { ConnectDomainOptions, DeployStatus, DnsRecord, FileChange, FileVersion, GitBackend, GitHost, GitUser, HistoryOptions, OwnerAccess, PagesDomains, RedirectOptions, RepositoryVisibility, RevisionFiles, ScheduledBuilds, SetupHost, SignInError, SiteDrafts, SiteEditors, SiteUpdates, StorageLike, TokenLink, WriteOptions } from "@goodfellow-cms/core";
|
|
2
2
|
//#region src/sign-in.d.ts
|
|
3
3
|
/** The page's address and how to change it. Replaceable in tests. */
|
|
4
4
|
interface BrowserLocation {
|
|
@@ -67,6 +67,7 @@ export declare class GitHubBackend implements GitBackend {
|
|
|
67
67
|
/** Absent where the repository holds only content: the update job runs where the code is. */
|
|
68
68
|
readonly updates?: SiteUpdates;
|
|
69
69
|
readonly drafts: SiteDrafts;
|
|
70
|
+
readonly scheduledBuilds: ScheduledBuilds;
|
|
70
71
|
readonly contentOnly: boolean;
|
|
71
72
|
private readonly options;
|
|
72
73
|
private readonly api;
|
|
@@ -81,9 +82,12 @@ export declare class GitHubBackend implements GitBackend {
|
|
|
81
82
|
private readonly treeSizes;
|
|
82
83
|
private readonly blobs;
|
|
83
84
|
constructor(options: GitHubBackendOptions);
|
|
85
|
+
repositoryVisibility(): Promise<RepositoryVisibility>;
|
|
84
86
|
/** Drafts on branches of the repository, with pull requests for review. */
|
|
85
87
|
private draftHost;
|
|
86
88
|
revision(): Promise<string>;
|
|
89
|
+
/** The branch's latest commit, without moving the revision reads are pinned to. */
|
|
90
|
+
private head;
|
|
87
91
|
/** Path → blob id for every file at a commit. Commits never change, so this is cached. */
|
|
88
92
|
private tree;
|
|
89
93
|
private currentTree;
|
|
@@ -91,8 +95,11 @@ export declare class GitHubBackend implements GitBackend {
|
|
|
91
95
|
private readFrom;
|
|
92
96
|
/** Reads many text files in a few GraphQL requests rather than one request each. */
|
|
93
97
|
readFiles(paths: string[]): Promise<Map<string, string>>;
|
|
98
|
+
private readFilesFrom;
|
|
94
99
|
readBytes(path: string): Promise<Uint8Array | undefined>;
|
|
100
|
+
private bytesFrom;
|
|
95
101
|
list(dir: string): Promise<string[]>;
|
|
102
|
+
filesAt(revision: string): RevisionFiles;
|
|
96
103
|
fileSizes(dir: string): Promise<Map<string, number>>;
|
|
97
104
|
/** Uses an owner's sign-in through the worker for editors and GitHub Pages settings, until it's forgotten. */
|
|
98
105
|
useOwnerSignIn(token: TokenSource, end: () => void, codeToken?: TokenSource): void;
|
|
@@ -112,12 +119,21 @@ export declare class GitHubBackend implements GitBackend {
|
|
|
112
119
|
readAt(path: string, revision: string): Promise<string | undefined>;
|
|
113
120
|
changedPaths(from: string, to: string): Promise<string[]>;
|
|
114
121
|
/**
|
|
115
|
-
* Reads the deployment GitHub Pages or Vercel created for a commit,
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* if it has ever been deployed
|
|
122
|
+
* Reads the deployment GitHub Pages or Vercel created for a commit, or what
|
|
123
|
+
* Cloudflare Pages reports on it, and for GitHub Actions, the workflow run
|
|
124
|
+
* that builds it: a build that fails never creates a deployment. Before any
|
|
125
|
+
* exists, the site counts as building if it has ever been deployed, or if
|
|
126
|
+
* Cloudflare reported on the commit before.
|
|
119
127
|
*/
|
|
120
128
|
deployStatus(revision: string): Promise<DeployStatus>;
|
|
129
|
+
/**
|
|
130
|
+
* What Cloudflare Pages reports on a commit: a check run of its GitHub App,
|
|
131
|
+
* or a commit status, which fine-grained tokens can read where check runs
|
|
132
|
+
* need a GitHub App's permission. `undefined` if it reported nothing.
|
|
133
|
+
*/
|
|
134
|
+
private cloudflareBuild;
|
|
135
|
+
/** The commit a commit was made on top of, if the token can see it. */
|
|
136
|
+
private parentOf;
|
|
121
137
|
/** The site's deploy workflow run for a commit, or `undefined` if there's none or the token can't see Actions. */
|
|
122
138
|
private workflowRun;
|
|
123
139
|
/** Which step of a failed run failed. */
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConflictError, DomainError, DraftError, EditorsError, GitApiError, REBUILD_JOB, SetupError, SignInError, UpdatesError, assertContentChanges, branchDrafts, buildStepFor, compareReleases, credentialStorage, decodeBase64, encodeBase64, encodeBase64Bytes, siteVersion } from "@goodfellow-cms/core";
|
|
1
|
+
import { ConflictError, DomainError, DraftError, EditorsError, GitApiError, NIGHTLY_VARIABLE, REBUILD_JOB, SetupError, SignInError, UpdatesError, assertContentChanges, branchDrafts, buildStepFor, compareReleases, credentialStorage, decodeBase64, encodeBase64, encodeBase64Bytes, hostSource, nightlyMode, siteVersion } from "@goodfellow-cms/core";
|
|
2
2
|
//#region src/api.ts
|
|
3
3
|
/** Calls the GitHub REST or GraphQL API. Treats 401 as an expired sign-in; other errors become `GitApiError`. */
|
|
4
4
|
async function githubRequest({ apiUrl, token, fetch }, path, init = {}) {
|
|
@@ -295,6 +295,7 @@ function githubPages(api, repo) {
|
|
|
295
295
|
}
|
|
296
296
|
}
|
|
297
297
|
return {
|
|
298
|
+
severalDomains: false,
|
|
298
299
|
async site() {
|
|
299
300
|
const found = await pages();
|
|
300
301
|
return found && {
|
|
@@ -328,11 +329,33 @@ function githubPages(api, repo) {
|
|
|
328
329
|
};
|
|
329
330
|
}
|
|
330
331
|
//#endregion
|
|
332
|
+
//#region src/scheduled-builds.ts
|
|
333
|
+
/** The starters' hourly check, which builds the site once a scheduled change has come due. */
|
|
334
|
+
const SCHEDULED_WORKFLOW = ".github/workflows/scheduled.yml";
|
|
335
|
+
/** Workflows that build the site, or ask another host to, each night. */
|
|
336
|
+
const NIGHTLY_WORKFLOWS = [".github/workflows/deploy.yml", ".github/workflows/rebuild.yml"];
|
|
337
|
+
/**
|
|
338
|
+
* Scheduled changes on GitHub go live within the hour after their time, with
|
|
339
|
+
* the starters' hourly check (`scheduled.yml`), which builds the site only
|
|
340
|
+
* once one has come due. Sites set up without it get them with the nightly
|
|
341
|
+
* build.
|
|
342
|
+
*/
|
|
343
|
+
function githubScheduledBuilds(read) {
|
|
344
|
+
return { async timing() {
|
|
345
|
+
const [scheduled, ...nightly] = await Promise.all([SCHEDULED_WORKFLOW, ...NIGHTLY_WORKFLOWS].map(read));
|
|
346
|
+
if (scheduled && /\bdue\b/.test(scheduled) && /\bschedule:/.test(scheduled)) return "hourly";
|
|
347
|
+
if (nightly.some((workflow) => workflow && /\bschedule:/.test(workflow))) return "nightly";
|
|
348
|
+
return "none";
|
|
349
|
+
} };
|
|
350
|
+
}
|
|
351
|
+
//#endregion
|
|
331
352
|
//#region src/updates.ts
|
|
332
353
|
/** The starters' GitHub Pages workflow, which runs `goodfellow update` each night and when it's dispatched. */
|
|
333
354
|
const WORKFLOW = "deploy.yml";
|
|
334
355
|
/** The code repository's workflow, where a site keeps its content in a repository of its own. */
|
|
335
356
|
const CODE_WORKFLOW = "update.yml";
|
|
357
|
+
/** The job that publishes the update, which the job that builds the site with it can't. */
|
|
358
|
+
const PUBLISH_JOB = "publish-update";
|
|
336
359
|
/**
|
|
337
360
|
* The site's nightly update, which runs in its GitHub Pages workflow. Running
|
|
338
361
|
* it now dispatches the workflow, which needs GitHub's Actions permission
|
|
@@ -343,7 +366,8 @@ function githubUpdates(read, manage, repo, branch, files, workflow = WORKFLOW) {
|
|
|
343
366
|
const workflowFile = `.github/workflows/${workflow}`;
|
|
344
367
|
return {
|
|
345
368
|
async setup() {
|
|
346
|
-
|
|
369
|
+
const text = await files.read(workflowFile);
|
|
370
|
+
return text && /update --(publish|commit)\b/.test(text) ? "ready" : "missing";
|
|
347
371
|
},
|
|
348
372
|
async lastRun() {
|
|
349
373
|
const response = await githubRequest(read, `${path}/runs?branch=${encodeURIComponent(branch)}&per_page=20`, { allow: [403, 404] });
|
|
@@ -365,6 +389,11 @@ function githubUpdates(read, manage, repo, branch, files, workflow = WORKFLOW) {
|
|
|
365
389
|
state: "failed",
|
|
366
390
|
...base
|
|
367
391
|
};
|
|
392
|
+
const publish = jobs.find((candidate) => candidate.name === PUBLISH_JOB);
|
|
393
|
+
if (publish && publish.conclusion !== "success" && publish.conclusion !== "skipped") return {
|
|
394
|
+
state: "failed",
|
|
395
|
+
...base
|
|
396
|
+
};
|
|
368
397
|
const before = siteVersion(await files.readAt("package.json", run.head_sha) ?? "");
|
|
369
398
|
const now = siteVersion(await files.read("package.json") ?? "");
|
|
370
399
|
if (!before || !now || before === now) return {
|
|
@@ -384,20 +413,73 @@ function githubUpdates(read, manage, repo, branch, files, workflow = WORKFLOW) {
|
|
|
384
413
|
};
|
|
385
414
|
}
|
|
386
415
|
},
|
|
416
|
+
nightly: githubNightly(read, manage, repo, () => files.read(workflowFile)),
|
|
417
|
+
async rebuild() {
|
|
418
|
+
await dispatch({});
|
|
419
|
+
},
|
|
387
420
|
async start(target) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
421
|
+
await dispatch({ update: target });
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
/** Runs the workflow, with an update to install, or none to only build the site. */
|
|
425
|
+
async function dispatch(inputs) {
|
|
426
|
+
try {
|
|
427
|
+
await githubRequest(manage() ?? read, `${path}/dispatches`, {
|
|
428
|
+
method: "POST",
|
|
429
|
+
body: {
|
|
430
|
+
ref: branch,
|
|
431
|
+
inputs
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
} catch (error) {
|
|
435
|
+
if (error instanceof GitApiError && (error.status === 403 || error.status === 401)) throw new UpdatesError("not-allowed", "Running the site's build needs a token that can run its workflows.");
|
|
436
|
+
if (error instanceof GitApiError && (error.status === 404 || error.status === 422)) throw new UpdatesError("missing", "The site's GitHub Pages workflow doesn't update Goodfellow.");
|
|
437
|
+
throw error;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* The nightly build setting, kept in the repository variable the workflow's
|
|
443
|
+
* jobs read in their conditions (`GOODFELLOW_NIGHTLY`), which needs GitHub's
|
|
444
|
+
* Variables permission: the owner's sign-in, or a token that has it.
|
|
445
|
+
*/
|
|
446
|
+
function githubNightly(read, manage, repo, workflow) {
|
|
447
|
+
const path = `/repos/${repo}/actions/variables`;
|
|
448
|
+
const notAllowed = (error) => {
|
|
449
|
+
if (error instanceof GitApiError && (error.status === 401 || error.status === 403)) throw new UpdatesError("not-allowed", "Changing when the site rebuilds needs GitHub's Variables permission.");
|
|
450
|
+
throw error;
|
|
451
|
+
};
|
|
452
|
+
return {
|
|
453
|
+
async supported() {
|
|
454
|
+
return (await workflow())?.includes(NIGHTLY_VARIABLE) ?? false;
|
|
455
|
+
},
|
|
456
|
+
async get() {
|
|
457
|
+
const response = await githubRequest(manage() ?? read, `${path}/${NIGHTLY_VARIABLE}`, { allow: [403, 404] });
|
|
458
|
+
if (response.status === 404) return "changed";
|
|
459
|
+
if (!response.ok) return void 0;
|
|
460
|
+
return nightlyMode((await response.json()).value);
|
|
461
|
+
},
|
|
462
|
+
async set(mode) {
|
|
463
|
+
const api = manage() ?? read;
|
|
464
|
+
if (mode === "changed") {
|
|
465
|
+
await githubRequest(api, `${path}/${NIGHTLY_VARIABLE}`, {
|
|
466
|
+
method: "DELETE",
|
|
467
|
+
allow: [404]
|
|
468
|
+
}).catch(notAllowed);
|
|
469
|
+
return;
|
|
400
470
|
}
|
|
471
|
+
const body = {
|
|
472
|
+
name: NIGHTLY_VARIABLE,
|
|
473
|
+
value: mode
|
|
474
|
+
};
|
|
475
|
+
if ((await githubRequest(api, `${path}/${NIGHTLY_VARIABLE}`, {
|
|
476
|
+
method: "PATCH",
|
|
477
|
+
body,
|
|
478
|
+
allow: [404]
|
|
479
|
+
}).catch(notAllowed)).status === 404) await githubRequest(api, path, {
|
|
480
|
+
method: "POST",
|
|
481
|
+
body
|
|
482
|
+
}).catch(notAllowed);
|
|
401
483
|
}
|
|
402
484
|
};
|
|
403
485
|
}
|
|
@@ -465,16 +547,26 @@ function githubCodeUpdates(options) {
|
|
|
465
547
|
},
|
|
466
548
|
async start(target) {
|
|
467
549
|
await (await updates()).start(target);
|
|
550
|
+
},
|
|
551
|
+
async rebuild() {
|
|
552
|
+
await (await updates()).rebuild?.();
|
|
468
553
|
}
|
|
469
554
|
};
|
|
470
555
|
}
|
|
471
556
|
//#endregion
|
|
472
557
|
//#region src/backend.ts
|
|
558
|
+
/** Cloudflare Pages' check runs (its GitHub App is "Cloudflare Workers and Pages") and commit statuses. */
|
|
559
|
+
const CLOUDFLARE = /cloudflare/i;
|
|
473
560
|
/** How many files one GraphQL request reads. */
|
|
474
561
|
const GRAPHQL_BATCH = 100;
|
|
475
562
|
const COMMIT_MUTATION = `mutation ($input: CreateCommitOnBranchInput!) {
|
|
476
563
|
createCommitOnBranch(input: $input) { commit { oid } }
|
|
477
564
|
}`;
|
|
565
|
+
/** The files under a folder of a tree, in order. */
|
|
566
|
+
function listFrom(tree, dir) {
|
|
567
|
+
const prefix = `${dir}/`;
|
|
568
|
+
return [...tree.keys()].filter((path) => path.startsWith(prefix)).sort();
|
|
569
|
+
}
|
|
478
570
|
/**
|
|
479
571
|
* The site's files on GitHub. Reads come from the commit `revision()` last
|
|
480
572
|
* returned; saves are single commits made with GraphQL's `createCommitOnBranch`,
|
|
@@ -488,6 +580,7 @@ var GitHubBackend = class GitHubBackend {
|
|
|
488
580
|
/** Absent where the repository holds only content: the update job runs where the code is. */
|
|
489
581
|
updates;
|
|
490
582
|
drafts;
|
|
583
|
+
scheduledBuilds;
|
|
491
584
|
contentOnly;
|
|
492
585
|
options;
|
|
493
586
|
api;
|
|
@@ -532,6 +625,12 @@ var GitHubBackend = class GitHubBackend {
|
|
|
532
625
|
})
|
|
533
626
|
});
|
|
534
627
|
this.drafts = branchDrafts(this, this.draftHost());
|
|
628
|
+
this.scheduledBuilds = githubScheduledBuilds((path) => this.read(path));
|
|
629
|
+
}
|
|
630
|
+
async repositoryVisibility() {
|
|
631
|
+
const found = await githubJson(this.api, `/repos/${this.repo}`);
|
|
632
|
+
if (found.visibility === "internal") return "internal";
|
|
633
|
+
return found.private === false || found.visibility === "public" ? "public" : "private";
|
|
535
634
|
}
|
|
536
635
|
/** Drafts on branches of the repository, with pull requests for review. */
|
|
537
636
|
draftHost() {
|
|
@@ -564,6 +663,9 @@ var GitHubBackend = class GitHubBackend {
|
|
|
564
663
|
if (response.status === 404) throw new DraftError("not-found", `There's no draft ${branch}.`);
|
|
565
664
|
return (await response.json()).merge_base_commit.sha;
|
|
566
665
|
},
|
|
666
|
+
listBranches: async (prefix) => {
|
|
667
|
+
return (await githubJson(this.api, `${base}/git/matching-refs/heads/${encodeURIComponent(prefix)}`)).map((found) => found.ref.replace(/^refs\/heads\//, ""));
|
|
668
|
+
},
|
|
567
669
|
openReview: async ({ branch, title, description }) => {
|
|
568
670
|
const response = await githubRequest(this.api, `${base}/pulls`, {
|
|
569
671
|
method: "POST",
|
|
@@ -593,10 +695,13 @@ var GitHubBackend = class GitHubBackend {
|
|
|
593
695
|
};
|
|
594
696
|
}
|
|
595
697
|
async revision() {
|
|
596
|
-
|
|
597
|
-
this.pinned = ref.object.sha;
|
|
698
|
+
this.pinned = await this.head();
|
|
598
699
|
return this.pinned;
|
|
599
700
|
}
|
|
701
|
+
/** The branch's latest commit, without moving the revision reads are pinned to. */
|
|
702
|
+
async head() {
|
|
703
|
+
return (await githubJson(this.api, `/repos/${this.repo}/git/ref/heads/${encodeURIComponent(this.branch)}`)).object.sha;
|
|
704
|
+
}
|
|
600
705
|
/** Path → blob id for every file at a commit. Commits never change, so this is cached. */
|
|
601
706
|
tree(sha) {
|
|
602
707
|
let tree = this.trees.get(sha);
|
|
@@ -631,7 +736,9 @@ var GitHubBackend = class GitHubBackend {
|
|
|
631
736
|
}
|
|
632
737
|
/** Reads many text files in a few GraphQL requests rather than one request each. */
|
|
633
738
|
async readFiles(paths) {
|
|
634
|
-
|
|
739
|
+
return this.readFilesFrom(await this.currentTree(), paths);
|
|
740
|
+
}
|
|
741
|
+
async readFilesFrom(tree, paths) {
|
|
635
742
|
const wanted = paths.flatMap((path) => {
|
|
636
743
|
const sha = tree.get(path);
|
|
637
744
|
return sha ? [[path, sha]] : [];
|
|
@@ -664,14 +771,24 @@ var GitHubBackend = class GitHubBackend {
|
|
|
664
771
|
return result;
|
|
665
772
|
}
|
|
666
773
|
async readBytes(path) {
|
|
667
|
-
|
|
774
|
+
return this.bytesFrom(await this.currentTree(), path);
|
|
775
|
+
}
|
|
776
|
+
async bytesFrom(tree, path) {
|
|
777
|
+
const sha = tree.get(path);
|
|
668
778
|
if (!sha) return void 0;
|
|
669
779
|
const response = await githubRequest(this.api, `/repos/${this.repo}/git/blobs/${sha}`, { accept: "application/vnd.github.raw+json" });
|
|
670
780
|
return new Uint8Array(await response.arrayBuffer());
|
|
671
781
|
}
|
|
672
782
|
async list(dir) {
|
|
673
|
-
|
|
674
|
-
|
|
783
|
+
return listFrom(await this.currentTree(), dir);
|
|
784
|
+
}
|
|
785
|
+
filesAt(revision) {
|
|
786
|
+
return {
|
|
787
|
+
read: async (path) => this.readFrom(await this.tree(revision), path),
|
|
788
|
+
readFiles: async (paths) => this.readFilesFrom(await this.tree(revision), paths),
|
|
789
|
+
readBytes: async (path) => this.bytesFrom(await this.tree(revision), path),
|
|
790
|
+
list: async (dir) => listFrom(await this.tree(revision), dir)
|
|
791
|
+
};
|
|
675
792
|
}
|
|
676
793
|
async fileSizes(dir) {
|
|
677
794
|
const prefix = `${dir}/`;
|
|
@@ -825,10 +942,11 @@ var GitHubBackend = class GitHubBackend {
|
|
|
825
942
|
return [...new Set((comparison.files ?? []).flatMap((file) => [file.filename, file.previous_filename ?? []].flat()))];
|
|
826
943
|
}
|
|
827
944
|
/**
|
|
828
|
-
* Reads the deployment GitHub Pages or Vercel created for a commit,
|
|
829
|
-
*
|
|
830
|
-
*
|
|
831
|
-
* if it has ever been deployed
|
|
945
|
+
* Reads the deployment GitHub Pages or Vercel created for a commit, or what
|
|
946
|
+
* Cloudflare Pages reports on it, and for GitHub Actions, the workflow run
|
|
947
|
+
* that builds it: a build that fails never creates a deployment. Before any
|
|
948
|
+
* exists, the site counts as building if it has ever been deployed, or if
|
|
949
|
+
* Cloudflare reported on the commit before.
|
|
832
950
|
*/
|
|
833
951
|
async deployStatus(revision) {
|
|
834
952
|
try {
|
|
@@ -857,7 +975,13 @@ var GitHubBackend = class GitHubBackend {
|
|
|
857
975
|
};
|
|
858
976
|
}
|
|
859
977
|
}
|
|
978
|
+
const cloudflare = await this.cloudflareBuild(revision);
|
|
979
|
+
if (cloudflare) return cloudflare;
|
|
860
980
|
const run = await this.workflowRun(revision);
|
|
981
|
+
if (run?.status === "completed" && run.conclusion === "cancelled") {
|
|
982
|
+
const head = await this.head();
|
|
983
|
+
if (head !== revision) return this.deployStatus(head);
|
|
984
|
+
}
|
|
861
985
|
if (run?.status === "completed" && run.conclusion !== "success" && run.conclusion !== "skipped") return {
|
|
862
986
|
state: "failed",
|
|
863
987
|
detailsUrl: run.html_url,
|
|
@@ -867,12 +991,76 @@ var GitHubBackend = class GitHubBackend {
|
|
|
867
991
|
state: "building",
|
|
868
992
|
detailsUrl: run.html_url
|
|
869
993
|
};
|
|
870
|
-
|
|
994
|
+
if ((await githubJson(this.api, `/repos/${this.repo}/deployments?per_page=1`)).length > 0) return { state: "building" };
|
|
995
|
+
const parent = await this.parentOf(revision);
|
|
996
|
+
return { state: parent && await this.cloudflareBuild(parent) ? "building" : "unknown" };
|
|
871
997
|
} catch (error) {
|
|
872
998
|
if (error instanceof GitApiError) return { state: "unknown" };
|
|
873
999
|
throw error;
|
|
874
1000
|
}
|
|
875
1001
|
}
|
|
1002
|
+
/**
|
|
1003
|
+
* What Cloudflare Pages reports on a commit: a check run of its GitHub App,
|
|
1004
|
+
* or a commit status, which fine-grained tokens can read where check runs
|
|
1005
|
+
* need a GitHub App's permission. `undefined` if it reported nothing.
|
|
1006
|
+
*/
|
|
1007
|
+
async cloudflareBuild(revision) {
|
|
1008
|
+
const checks = await githubRequest(this.api, `/repos/${this.repo}/commits/${revision}/check-runs?per_page=50`, { allow: [403, 404] });
|
|
1009
|
+
if (checks.ok) {
|
|
1010
|
+
const { check_runs: found = [] } = await checks.json();
|
|
1011
|
+
const run = found.find((candidate) => CLOUDFLARE.test(`${candidate.app?.slug ?? ""} ${candidate.name}`));
|
|
1012
|
+
if (run) {
|
|
1013
|
+
const detailsUrl = run.details_url ?? run.html_url ?? void 0;
|
|
1014
|
+
if (run.status !== "completed") return {
|
|
1015
|
+
state: "building",
|
|
1016
|
+
detailsUrl
|
|
1017
|
+
};
|
|
1018
|
+
if (run.conclusion === "success" || run.conclusion === "neutral" || run.conclusion === "skipped") return {
|
|
1019
|
+
state: "live",
|
|
1020
|
+
detailsUrl
|
|
1021
|
+
};
|
|
1022
|
+
return {
|
|
1023
|
+
state: "failed",
|
|
1024
|
+
detailsUrl,
|
|
1025
|
+
problem: {
|
|
1026
|
+
step: "build",
|
|
1027
|
+
detail: run.output?.title ?? void 0
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
const statuses = await githubRequest(this.api, `/repos/${this.repo}/commits/${revision}/statuses?per_page=50`, { allow: [403, 404] });
|
|
1033
|
+
if (!statuses.ok) return void 0;
|
|
1034
|
+
const status = (await statuses.json()).find((candidate) => CLOUDFLARE.test(candidate.context));
|
|
1035
|
+
if (!status) return void 0;
|
|
1036
|
+
const detailsUrl = status.target_url ?? void 0;
|
|
1037
|
+
if (status.state === "success") return {
|
|
1038
|
+
state: "live",
|
|
1039
|
+
detailsUrl
|
|
1040
|
+
};
|
|
1041
|
+
if (status.state === "pending") return {
|
|
1042
|
+
state: "building",
|
|
1043
|
+
detailsUrl
|
|
1044
|
+
};
|
|
1045
|
+
return {
|
|
1046
|
+
state: "failed",
|
|
1047
|
+
detailsUrl,
|
|
1048
|
+
problem: {
|
|
1049
|
+
step: "build",
|
|
1050
|
+
detail: status.description ?? void 0
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
/** The commit a commit was made on top of, if the token can see it. */
|
|
1055
|
+
async parentOf(revision) {
|
|
1056
|
+
const response = await githubRequest(this.api, `/repos/${this.repo}/commits/${revision}`, { allow: [
|
|
1057
|
+
403,
|
|
1058
|
+
404,
|
|
1059
|
+
422
|
|
1060
|
+
] });
|
|
1061
|
+
if (!response.ok) return void 0;
|
|
1062
|
+
return (await response.json()).parents?.[0]?.sha;
|
|
1063
|
+
}
|
|
876
1064
|
/** The site's deploy workflow run for a commit, or `undefined` if there's none or the token can't see Actions. */
|
|
877
1065
|
async workflowRun(revision) {
|
|
878
1066
|
const response = await githubRequest(this.api, `/repos/${this.repo}/actions/runs?head_sha=${revision}&per_page=20`, { allow: [403, 404] });
|
|
@@ -1143,6 +1331,25 @@ function setupAccount(creator) {
|
|
|
1143
1331
|
throw error;
|
|
1144
1332
|
}
|
|
1145
1333
|
}
|
|
1334
|
+
/** Gives the repository's Pages a domain of its own, saying whether GitHub took it. */
|
|
1335
|
+
async function setDomain(api, repo, domain) {
|
|
1336
|
+
try {
|
|
1337
|
+
await githubRequest(api, `/repos/${repoPath(repo.full_name)}/pages`, {
|
|
1338
|
+
method: "PUT",
|
|
1339
|
+
body: { cname: domain }
|
|
1340
|
+
});
|
|
1341
|
+
return true;
|
|
1342
|
+
} catch (error) {
|
|
1343
|
+
if (error instanceof GitApiError && [
|
|
1344
|
+
400,
|
|
1345
|
+
403,
|
|
1346
|
+
404,
|
|
1347
|
+
409,
|
|
1348
|
+
422
|
|
1349
|
+
].includes(error.status)) return false;
|
|
1350
|
+
throw error;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1146
1353
|
/** Writes every file in one commit, which replaces the starting README rather than following it. */
|
|
1147
1354
|
async function writeFiles(api, repo, site) {
|
|
1148
1355
|
const base = `/repos/${repoPath(repo.full_name)}`;
|
|
@@ -1287,6 +1494,10 @@ function setupAccount(creator) {
|
|
|
1287
1494
|
onStep?.("pages");
|
|
1288
1495
|
siteUrl = await turnOnPages(api, repo);
|
|
1289
1496
|
if (!siteUrl) warnings.push("pages-unavailable");
|
|
1497
|
+
else if (site.domain) {
|
|
1498
|
+
if (await setDomain(api, repo, site.domain)) siteUrl = `https://${site.domain}/`;
|
|
1499
|
+
else warnings.push("domain-unavailable");
|
|
1500
|
+
}
|
|
1290
1501
|
}
|
|
1291
1502
|
createdApis.set(repo.full_name.toLowerCase(), api);
|
|
1292
1503
|
if (site.link && !await connect(site.link)) warnings.push("connect-unavailable");
|
|
@@ -1504,7 +1715,8 @@ function githubTokenLinks(repo, webUrl = "https://github.com") {
|
|
|
1504
1715
|
expires_in: "90",
|
|
1505
1716
|
contents: "write",
|
|
1506
1717
|
deployments: "read",
|
|
1507
|
-
actions: "read"
|
|
1718
|
+
actions: "read",
|
|
1719
|
+
statuses: "read"
|
|
1508
1720
|
});
|
|
1509
1721
|
const classic = new URLSearchParams({
|
|
1510
1722
|
scopes: "repo",
|
|
@@ -1517,7 +1729,8 @@ function githubTokenLinks(repo, webUrl = "https://github.com") {
|
|
|
1517
1729
|
expires_in: "1",
|
|
1518
1730
|
administration: "write",
|
|
1519
1731
|
pages: "write",
|
|
1520
|
-
actions: "write"
|
|
1732
|
+
actions: "write",
|
|
1733
|
+
variables: "write"
|
|
1521
1734
|
});
|
|
1522
1735
|
return {
|
|
1523
1736
|
fineGrained: `${webUrl}/settings/personal-access-tokens/new?${fineGrained}`,
|
|
@@ -1768,7 +1981,20 @@ function github(options) {
|
|
|
1768
1981
|
}
|
|
1769
1982
|
return {
|
|
1770
1983
|
name: "GitHub",
|
|
1984
|
+
source: hostSource("@goodfellow-cms/github", "github", options, [
|
|
1985
|
+
"fetch",
|
|
1986
|
+
"storage",
|
|
1987
|
+
"location"
|
|
1988
|
+
]),
|
|
1771
1989
|
canRedirect: service !== void 0,
|
|
1990
|
+
setup: () => githubSetup({
|
|
1991
|
+
...options.apiUrl && { apiUrl: options.apiUrl },
|
|
1992
|
+
...options.webUrl && { webUrl: options.webUrl },
|
|
1993
|
+
...options.fetch && { fetch: options.fetch },
|
|
1994
|
+
...options.storage && { storage: options.storage },
|
|
1995
|
+
...options.location && { location: options.location },
|
|
1996
|
+
signIn: false
|
|
1997
|
+
}),
|
|
1772
1998
|
tokenLinks: [{
|
|
1773
1999
|
url: links.fineGrained,
|
|
1774
2000
|
label: "signIn.token.create"
|
package/dist/testing.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ 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;
|
|
23
25
|
/** Whether the token may set Actions secrets and add deploy keys. Defaults to true. */
|
|
24
26
|
secrets?: boolean;
|
|
25
27
|
}
|
|
@@ -74,6 +76,17 @@ export interface FakeGitHubOptions {
|
|
|
74
76
|
* reports success after being checked this many times. Off by default.
|
|
75
77
|
*/
|
|
76
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;
|
|
77
90
|
/** Organizations the users belong to, which new repositories can be created in. */
|
|
78
91
|
orgs?: string[];
|
|
79
92
|
/** The default branch of repositories created through the API, as an account's settings choose. Defaults to `main`. */
|
|
@@ -88,6 +101,8 @@ export interface FakeGitHubOptions {
|
|
|
88
101
|
accounts?: string[];
|
|
89
102
|
/** GitHub Apps, which the sign-in worker signs people in with. */
|
|
90
103
|
apps?: FakeGitHubApp[];
|
|
104
|
+
/** Whether the site's repository is private. Public by default. */
|
|
105
|
+
private?: boolean;
|
|
91
106
|
}
|
|
92
107
|
/** A GitHub App, as the sign-in worker uses one: its JWTs, installations, OAuth sign-in and tokens. */
|
|
93
108
|
export interface FakeGitHubApp {
|
|
@@ -129,6 +144,7 @@ export interface FakeCreatedRepo {
|
|
|
129
144
|
repo: FakeRepo;
|
|
130
145
|
pages?: {
|
|
131
146
|
build_type: string;
|
|
147
|
+
cname?: string;
|
|
132
148
|
};
|
|
133
149
|
rulesets: unknown[];
|
|
134
150
|
/** Deploy keys added to it, by their public key. */
|
|
@@ -150,6 +166,11 @@ interface Deployment {
|
|
|
150
166
|
statuses: FakeDeploymentStatus[];
|
|
151
167
|
checks: number;
|
|
152
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
|
+
}
|
|
153
174
|
export declare function fakeGitHub(options: FakeGitHubOptions): {
|
|
154
175
|
repo: FakeRepo;
|
|
155
176
|
/** How many GraphQL requests read files, as `readFiles()` makes. */
|
|
@@ -182,9 +203,15 @@ export declare function fakeGitHub(options: FakeGitHubOptions): {
|
|
|
182
203
|
invitations: FakeInvitation[];
|
|
183
204
|
accept: (login: string) => void;
|
|
184
205
|
deployments: Deployment[];
|
|
185
|
-
/**
|
|
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()`. */
|
|
186
210
|
runs: FakeRun[];
|
|
211
|
+
/** The repository's Actions variables, such as `GOODFELLOW_NIGHTLY`. */
|
|
212
|
+
variables: Map<string, string>;
|
|
187
213
|
failBuild: (sha: string, step?: string, job?: string) => FakeRun;
|
|
214
|
+
cancelBuild: (sha: string) => FakeRun;
|
|
188
215
|
/** Every request received, for checking what the backend sent. */
|
|
189
216
|
requests: Request[];
|
|
190
217
|
/** Simulates someone else publishing. */
|
package/dist/testing.js
CHANGED
|
@@ -53,7 +53,10 @@ function fakeGitHub(options) {
|
|
|
53
53
|
let nextToken = 1;
|
|
54
54
|
let nextRepoId = 100;
|
|
55
55
|
const deployments = [];
|
|
56
|
+
const cloudflareBuilds = /* @__PURE__ */ new Map();
|
|
56
57
|
const runs = [];
|
|
58
|
+
/** The repository's Actions variables, by name. */
|
|
59
|
+
const variables = /* @__PURE__ */ new Map();
|
|
57
60
|
const requests = [];
|
|
58
61
|
let nextDeployment = 1;
|
|
59
62
|
let graphqlReads = 0;
|
|
@@ -187,6 +190,23 @@ function fakeGitHub(options) {
|
|
|
187
190
|
runs.push(run);
|
|
188
191
|
return run;
|
|
189
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
|
+
}
|
|
190
210
|
/** Simulates an invited person accepting the invitation. */
|
|
191
211
|
function accept(login) {
|
|
192
212
|
const index = invitations.findIndex((invitation) => invitation.login === login);
|
|
@@ -333,6 +353,12 @@ function fakeGitHub(options) {
|
|
|
333
353
|
build_type: site.pages.build_type
|
|
334
354
|
}, 201);
|
|
335
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
|
+
}
|
|
336
362
|
const notAllowed = (permission) => principal.app ? principal.permissions?.[permission] !== "write" : principal.secrets === false;
|
|
337
363
|
if (rest === "/keys" && method === "POST") {
|
|
338
364
|
if (notAllowed("administration")) return json({ message: "Resource not accessible by integration" }, 403);
|
|
@@ -447,6 +473,7 @@ function fakeGitHub(options) {
|
|
|
447
473
|
}
|
|
448
474
|
}
|
|
449
475
|
function deploy(sha) {
|
|
476
|
+
if (options.cloudflare) cloudflareBuilds.set(sha, { checks: 0 });
|
|
450
477
|
if (options.deployAfterChecks === void 0) return;
|
|
451
478
|
deployments.unshift({
|
|
452
479
|
id: nextDeployment++,
|
|
@@ -455,6 +482,20 @@ function fakeGitHub(options) {
|
|
|
455
482
|
checks: 0
|
|
456
483
|
});
|
|
457
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
|
+
}
|
|
458
499
|
function commit(changes, message, expectedHead, author, branch = repo.defaultBranch) {
|
|
459
500
|
const sha = repo.commit(changes, message, {
|
|
460
501
|
expectedHead,
|
|
@@ -517,7 +558,8 @@ function fakeGitHub(options) {
|
|
|
517
558
|
pages: app.permissions.pages === "write",
|
|
518
559
|
workflow: app.permissions.workflows === "write",
|
|
519
560
|
actions: app.permissions.actions !== void 0,
|
|
520
|
-
actionsWrite: app.permissions.actions === "write"
|
|
561
|
+
actionsWrite: app.permissions.actions === "write",
|
|
562
|
+
variables: app.permissions.variables === "write"
|
|
521
563
|
});
|
|
522
564
|
refreshTokens.set(refresh, {
|
|
523
565
|
app,
|
|
@@ -625,7 +667,8 @@ function fakeGitHub(options) {
|
|
|
625
667
|
administration: permissions.administration === "write",
|
|
626
668
|
admin: permissions.administration === "write",
|
|
627
669
|
actions: permissions.actions !== void 0,
|
|
628
|
-
actionsWrite: permissions.actions === "write"
|
|
670
|
+
actionsWrite: permissions.actions === "write",
|
|
671
|
+
variables: permissions.variables === "write"
|
|
629
672
|
});
|
|
630
673
|
issued.push({
|
|
631
674
|
token,
|
|
@@ -789,6 +832,17 @@ function fakeGitHub(options) {
|
|
|
789
832
|
object: { sha: body.sha }
|
|
790
833
|
}, 201);
|
|
791
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
|
+
}
|
|
792
846
|
const branchRef = rest.match(/^\/git\/refs\/heads\/(.+)$/);
|
|
793
847
|
if (branchRef && request.method === "DELETE") {
|
|
794
848
|
if (user.push === false) return json({ message: "Resource not accessible by integration" }, 403);
|
|
@@ -846,6 +900,8 @@ function fakeGitHub(options) {
|
|
|
846
900
|
if (rest === "") return json({
|
|
847
901
|
id: 1,
|
|
848
902
|
full_name: options.repo,
|
|
903
|
+
private: options.private === true,
|
|
904
|
+
visibility: options.private === true ? "private" : "public",
|
|
849
905
|
default_branch: repo.defaultBranch,
|
|
850
906
|
permissions: {
|
|
851
907
|
push: user.push !== false,
|
|
@@ -904,6 +960,31 @@ function fakeGitHub(options) {
|
|
|
904
960
|
author: null
|
|
905
961
|
})));
|
|
906
962
|
}
|
|
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
|
+
}
|
|
907
988
|
const workflowFile = rest.match(/^\/actions\/workflows\/([\w.-]+)\/(?:dispatches|runs)$/)?.[1];
|
|
908
989
|
const workflowPath = `.github/workflows/${workflowFile}`;
|
|
909
990
|
const ofWorkflow = (run) => (run.path ?? ".github/workflows/deploy.yml") === workflowPath;
|
|
@@ -976,6 +1057,43 @@ function fakeGitHub(options) {
|
|
|
976
1057
|
files: repo.changedPaths(base, b).map((filename) => ({ filename }))
|
|
977
1058
|
});
|
|
978
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
|
+
}
|
|
979
1097
|
if (rest === "/deployments") {
|
|
980
1098
|
const sha = url.searchParams.get("sha");
|
|
981
1099
|
return json(deployments.filter((deployment) => !sha || deployment.sha === sha).slice(0, Number(url.searchParams.get("per_page") ?? 30)).map(({ id, sha }) => ({
|
|
@@ -1018,9 +1136,15 @@ function fakeGitHub(options) {
|
|
|
1018
1136
|
invitations,
|
|
1019
1137
|
accept,
|
|
1020
1138
|
deployments,
|
|
1021
|
-
/**
|
|
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()`. */
|
|
1022
1143
|
runs,
|
|
1144
|
+
/** The repository's Actions variables, such as `GOODFELLOW_NIGHTLY`. */
|
|
1145
|
+
variables,
|
|
1023
1146
|
failBuild,
|
|
1147
|
+
cancelBuild,
|
|
1024
1148
|
/** Every request received, for checking what the backend sent. */
|
|
1025
1149
|
requests,
|
|
1026
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,15 +35,15 @@
|
|
|
35
35
|
"dist"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
+
"@goodfellow-cms/core": "0.5.0",
|
|
38
39
|
"@noble/ciphers": "2.4.0",
|
|
39
|
-
"@noble/hashes": "2.4.0"
|
|
40
|
-
"@goodfellow-cms/core": "0.4.0"
|
|
40
|
+
"@noble/hashes": "2.4.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@puckeditor/core": "0.23.0"
|
|
44
44
|
},
|
|
45
45
|
"engines": {
|
|
46
|
-
"node": ">=
|
|
46
|
+
"node": "^22.23.4 || ^24.22.1 || >=26.12.1"
|
|
47
47
|
},
|
|
48
48
|
"publishConfig": {
|
|
49
49
|
"access": "public"
|