@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/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConflictError, DomainError, DraftError, EditorsError, GitApiError, SetupError, SignInError, UpdatesError, branchDrafts, buildStepFor, compareReleases, credentialStorage, 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 = {}) {
|
|
@@ -148,6 +148,8 @@ function githubEditors(read, manage, repo, login) {
|
|
|
148
148
|
*/
|
|
149
149
|
function githubOwnerAccess(base, repo, tokenLink, redirect) {
|
|
150
150
|
let owner;
|
|
151
|
+
/** The owner's access to the site's code repository, where its content is kept in a repository of its own. */
|
|
152
|
+
let ownerCode;
|
|
151
153
|
let endSignIn;
|
|
152
154
|
let problem;
|
|
153
155
|
return {
|
|
@@ -177,21 +179,28 @@ function githubOwnerAccess(base, repo, tokenLink, redirect) {
|
|
|
177
179
|
throw error;
|
|
178
180
|
}
|
|
179
181
|
owner = api;
|
|
182
|
+
ownerCode = api;
|
|
180
183
|
problem = void 0;
|
|
181
184
|
},
|
|
182
185
|
forget() {
|
|
183
186
|
owner = void 0;
|
|
187
|
+
ownerCode = void 0;
|
|
184
188
|
endSignIn?.();
|
|
185
189
|
endSignIn = void 0;
|
|
186
190
|
}
|
|
187
191
|
},
|
|
188
192
|
api: () => owner,
|
|
189
|
-
|
|
190
|
-
|
|
193
|
+
codeApi: () => ownerCode,
|
|
194
|
+
/** Uses an owner's sign-in through the worker, with its token for the code repository if it gave one. `end` runs when it's forgotten. */
|
|
195
|
+
useSignIn(token, end, codeToken) {
|
|
191
196
|
owner = {
|
|
192
197
|
...base,
|
|
193
198
|
token
|
|
194
199
|
};
|
|
200
|
+
ownerCode = codeToken ? {
|
|
201
|
+
...base,
|
|
202
|
+
token: codeToken
|
|
203
|
+
} : void 0;
|
|
195
204
|
endSignIn = end;
|
|
196
205
|
problem = void 0;
|
|
197
206
|
},
|
|
@@ -286,6 +295,7 @@ function githubPages(api, repo) {
|
|
|
286
295
|
}
|
|
287
296
|
}
|
|
288
297
|
return {
|
|
298
|
+
severalDomains: false,
|
|
289
299
|
async site() {
|
|
290
300
|
const found = await pages();
|
|
291
301
|
return found && {
|
|
@@ -319,20 +329,45 @@ function githubPages(api, repo) {
|
|
|
319
329
|
};
|
|
320
330
|
}
|
|
321
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
|
|
322
352
|
//#region src/updates.ts
|
|
323
353
|
/** The starters' GitHub Pages workflow, which runs `goodfellow update` each night and when it's dispatched. */
|
|
324
354
|
const WORKFLOW = "deploy.yml";
|
|
325
|
-
|
|
355
|
+
/** The code repository's workflow, where a site keeps its content in a repository of its own. */
|
|
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";
|
|
326
359
|
/**
|
|
327
360
|
* The site's nightly update, which runs in its GitHub Pages workflow. Running
|
|
328
361
|
* it now dispatches the workflow, which needs GitHub's Actions permission
|
|
329
362
|
* with write access: `manage()` gives the owner token when one is in use.
|
|
330
363
|
*/
|
|
331
|
-
function githubUpdates(read, manage, repo, branch, files) {
|
|
332
|
-
const path = `/repos/${repo}/actions/workflows/${
|
|
364
|
+
function githubUpdates(read, manage, repo, branch, files, workflow = WORKFLOW) {
|
|
365
|
+
const path = `/repos/${repo}/actions/workflows/${workflow}`;
|
|
366
|
+
const workflowFile = `.github/workflows/${workflow}`;
|
|
333
367
|
return {
|
|
334
368
|
async setup() {
|
|
335
|
-
|
|
369
|
+
const text = await files.read(workflowFile);
|
|
370
|
+
return text && /update --(publish|commit)\b/.test(text) ? "ready" : "missing";
|
|
336
371
|
},
|
|
337
372
|
async lastRun() {
|
|
338
373
|
const response = await githubRequest(read, `${path}/runs?branch=${encodeURIComponent(branch)}&per_page=20`, { allow: [403, 404] });
|
|
@@ -354,6 +389,11 @@ function githubUpdates(read, manage, repo, branch, files) {
|
|
|
354
389
|
state: "failed",
|
|
355
390
|
...base
|
|
356
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
|
+
};
|
|
357
397
|
const before = siteVersion(await files.readAt("package.json", run.head_sha) ?? "");
|
|
358
398
|
const now = siteVersion(await files.read("package.json") ?? "");
|
|
359
399
|
if (!before || !now || before === now) return {
|
|
@@ -373,30 +413,160 @@ function githubUpdates(read, manage, repo, branch, files) {
|
|
|
373
413
|
};
|
|
374
414
|
}
|
|
375
415
|
},
|
|
416
|
+
nightly: githubNightly(read, manage, repo, () => files.read(workflowFile)),
|
|
417
|
+
async rebuild() {
|
|
418
|
+
await dispatch({});
|
|
419
|
+
},
|
|
376
420
|
async start(target) {
|
|
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;
|
|
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);
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
/** Whether an error means the sign-in can't see the code repository, or has no token for it. */
|
|
487
|
+
function noAccess(error) {
|
|
488
|
+
return error instanceof SignInError && error.problem === "no-access" || error instanceof GitApiError && (error.status === 403 || error.status === 404);
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* The nightly update of a site whose content is kept in a repository of its
|
|
492
|
+
* own, which runs in the code repository's `update.yml`. The sign-in needs to
|
|
493
|
+
* see the code repository too; where it can't, `setup()` says `no-access`.
|
|
494
|
+
*/
|
|
495
|
+
function githubCodeUpdates(options) {
|
|
496
|
+
const { read } = options;
|
|
497
|
+
const repo = repoPath(options.repo);
|
|
498
|
+
let branch;
|
|
499
|
+
const liveBranch = (api) => {
|
|
500
|
+
branch ??= githubJson(api, `/repos/${repo}`).then((found) => found.default_branch);
|
|
501
|
+
branch.catch(() => {
|
|
502
|
+
branch = void 0;
|
|
503
|
+
});
|
|
504
|
+
return branch;
|
|
505
|
+
};
|
|
506
|
+
/** The code repository at its newest commit, read again each time, since updates change it. */
|
|
507
|
+
async function files() {
|
|
508
|
+
if (!read) throw new SignInError("no-access", `This sign-in has no access to ${options.repo}.`);
|
|
509
|
+
return options.open(read, await liveBranch(read));
|
|
510
|
+
}
|
|
511
|
+
/** A file as it is now: setup files are outside the folders backends list, so they're read at the newest commit. */
|
|
512
|
+
async function readNow(path) {
|
|
513
|
+
const backend = await files();
|
|
514
|
+
return backend.readAt(path, await backend.revision());
|
|
515
|
+
}
|
|
516
|
+
async function updates() {
|
|
517
|
+
if (!read) throw new SignInError("no-access", `This sign-in has no access to ${options.repo}.`);
|
|
518
|
+
const target = await liveBranch(read);
|
|
519
|
+
return githubUpdates(read, options.manage, repo, target, {
|
|
520
|
+
read: readNow,
|
|
521
|
+
readAt: async (path, revision) => (await files()).readAt(path, revision)
|
|
522
|
+
}, CODE_WORKFLOW);
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
code: {
|
|
526
|
+
repo: options.repo,
|
|
527
|
+
webUrl: `${options.webUrl}/${options.repo}`,
|
|
528
|
+
read: readNow,
|
|
529
|
+
history: async (path, history) => (await files()).history(path, history),
|
|
530
|
+
readAt: async (path, revision) => (await files()).readAt(path, revision)
|
|
531
|
+
},
|
|
532
|
+
async setup() {
|
|
377
533
|
try {
|
|
378
|
-
await
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
534
|
+
return await (await updates()).setup();
|
|
535
|
+
} catch (error) {
|
|
536
|
+
if (noAccess(error)) return "no-access";
|
|
537
|
+
throw error;
|
|
538
|
+
}
|
|
539
|
+
},
|
|
540
|
+
async lastRun() {
|
|
541
|
+
try {
|
|
542
|
+
return await (await updates()).lastRun();
|
|
385
543
|
} catch (error) {
|
|
386
|
-
if (
|
|
387
|
-
if (error instanceof GitApiError && (error.status === 404 || error.status === 422)) throw new UpdatesError("missing", "The site's GitHub Pages workflow doesn't update Goodfellow.");
|
|
544
|
+
if (noAccess(error)) return void 0;
|
|
388
545
|
throw error;
|
|
389
546
|
}
|
|
547
|
+
},
|
|
548
|
+
async start(target) {
|
|
549
|
+
await (await updates()).start(target);
|
|
550
|
+
},
|
|
551
|
+
async rebuild() {
|
|
552
|
+
await (await updates()).rebuild?.();
|
|
390
553
|
}
|
|
391
554
|
};
|
|
392
555
|
}
|
|
393
556
|
//#endregion
|
|
394
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;
|
|
395
560
|
/** How many files one GraphQL request reads. */
|
|
396
561
|
const GRAPHQL_BATCH = 100;
|
|
397
562
|
const COMMIT_MUTATION = `mutation ($input: CreateCommitOnBranchInput!) {
|
|
398
563
|
createCommitOnBranch(input: $input) { commit { oid } }
|
|
399
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
|
+
}
|
|
400
570
|
/**
|
|
401
571
|
* The site's files on GitHub. Reads come from the commit `revision()` last
|
|
402
572
|
* returned; saves are single commits made with GraphQL's `createCommitOnBranch`,
|
|
@@ -407,8 +577,11 @@ var GitHubBackend = class GitHubBackend {
|
|
|
407
577
|
pages;
|
|
408
578
|
editors;
|
|
409
579
|
ownerAccess;
|
|
580
|
+
/** Absent where the repository holds only content: the update job runs where the code is. */
|
|
410
581
|
updates;
|
|
411
582
|
drafts;
|
|
583
|
+
scheduledBuilds;
|
|
584
|
+
contentOnly;
|
|
412
585
|
options;
|
|
413
586
|
api;
|
|
414
587
|
repo;
|
|
@@ -429,13 +602,35 @@ var GitHubBackend = class GitHubBackend {
|
|
|
429
602
|
this.user = options.user;
|
|
430
603
|
this.onSignOut = options.onSignOut;
|
|
431
604
|
this.author = options.author;
|
|
605
|
+
this.pinned = options.revision;
|
|
432
606
|
const owner = githubOwnerAccess(this.api, this.repo, options.ownerTokenLink, options.ownerRedirect);
|
|
433
607
|
this.owner = owner;
|
|
434
608
|
this.ownerAccess = owner.access;
|
|
435
609
|
this.pages = githubPages(() => owner.api() ?? this.api, this.repo);
|
|
436
610
|
this.editors = githubEditors(this.api, owner.api, this.repo, this.user.login);
|
|
437
|
-
this.
|
|
611
|
+
this.contentOnly = options.contentOnly === true;
|
|
612
|
+
if (!this.contentOnly) this.updates = githubUpdates(this.api, owner.api, this.repo, this.branch, this);
|
|
613
|
+
else if (options.code) this.updates = githubCodeUpdates({
|
|
614
|
+
repo: options.code.repo,
|
|
615
|
+
webUrl: options.code.webUrl,
|
|
616
|
+
read: options.code.api,
|
|
617
|
+
manage: owner.codeApi,
|
|
618
|
+
open: (api, branch) => new GitHubBackend({
|
|
619
|
+
api,
|
|
620
|
+
repo: options.code?.repo ?? "",
|
|
621
|
+
branch,
|
|
622
|
+
user: this.user,
|
|
623
|
+
onSignOut: () => {},
|
|
624
|
+
ownerTokenLink: options.ownerTokenLink
|
|
625
|
+
})
|
|
626
|
+
});
|
|
438
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";
|
|
439
634
|
}
|
|
440
635
|
/** Drafts on branches of the repository, with pull requests for review. */
|
|
441
636
|
draftHost() {
|
|
@@ -468,6 +663,9 @@ var GitHubBackend = class GitHubBackend {
|
|
|
468
663
|
if (response.status === 404) throw new DraftError("not-found", `There's no draft ${branch}.`);
|
|
469
664
|
return (await response.json()).merge_base_commit.sha;
|
|
470
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
|
+
},
|
|
471
669
|
openReview: async ({ branch, title, description }) => {
|
|
472
670
|
const response = await githubRequest(this.api, `${base}/pulls`, {
|
|
473
671
|
method: "POST",
|
|
@@ -497,10 +695,13 @@ var GitHubBackend = class GitHubBackend {
|
|
|
497
695
|
};
|
|
498
696
|
}
|
|
499
697
|
async revision() {
|
|
500
|
-
|
|
501
|
-
this.pinned = ref.object.sha;
|
|
698
|
+
this.pinned = await this.head();
|
|
502
699
|
return this.pinned;
|
|
503
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
|
+
}
|
|
504
705
|
/** Path → blob id for every file at a commit. Commits never change, so this is cached. */
|
|
505
706
|
tree(sha) {
|
|
506
707
|
let tree = this.trees.get(sha);
|
|
@@ -535,7 +736,9 @@ var GitHubBackend = class GitHubBackend {
|
|
|
535
736
|
}
|
|
536
737
|
/** Reads many text files in a few GraphQL requests rather than one request each. */
|
|
537
738
|
async readFiles(paths) {
|
|
538
|
-
|
|
739
|
+
return this.readFilesFrom(await this.currentTree(), paths);
|
|
740
|
+
}
|
|
741
|
+
async readFilesFrom(tree, paths) {
|
|
539
742
|
const wanted = paths.flatMap((path) => {
|
|
540
743
|
const sha = tree.get(path);
|
|
541
744
|
return sha ? [[path, sha]] : [];
|
|
@@ -568,14 +771,24 @@ var GitHubBackend = class GitHubBackend {
|
|
|
568
771
|
return result;
|
|
569
772
|
}
|
|
570
773
|
async readBytes(path) {
|
|
571
|
-
|
|
774
|
+
return this.bytesFrom(await this.currentTree(), path);
|
|
775
|
+
}
|
|
776
|
+
async bytesFrom(tree, path) {
|
|
777
|
+
const sha = tree.get(path);
|
|
572
778
|
if (!sha) return void 0;
|
|
573
779
|
const response = await githubRequest(this.api, `/repos/${this.repo}/git/blobs/${sha}`, { accept: "application/vnd.github.raw+json" });
|
|
574
780
|
return new Uint8Array(await response.arrayBuffer());
|
|
575
781
|
}
|
|
576
782
|
async list(dir) {
|
|
577
|
-
|
|
578
|
-
|
|
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
|
+
};
|
|
579
792
|
}
|
|
580
793
|
async fileSizes(dir) {
|
|
581
794
|
const prefix = `${dir}/`;
|
|
@@ -584,14 +797,15 @@ var GitHubBackend = class GitHubBackend {
|
|
|
584
797
|
return new Map([...sizes].filter(([path]) => path.startsWith(prefix)));
|
|
585
798
|
}
|
|
586
799
|
/** Uses an owner's sign-in through the worker for editors and GitHub Pages settings, until it's forgotten. */
|
|
587
|
-
useOwnerSignIn(token, end) {
|
|
588
|
-
this.owner.useSignIn(token, end);
|
|
800
|
+
useOwnerSignIn(token, end, codeToken) {
|
|
801
|
+
this.owner.useSignIn(token, end, codeToken);
|
|
589
802
|
}
|
|
590
803
|
/** Remembers why an owner's sign-in through the worker didn't work, which the Editors and Domain screens show. */
|
|
591
804
|
ownerSignInFailed(error) {
|
|
592
805
|
this.owner.failed(error);
|
|
593
806
|
}
|
|
594
807
|
async write(changes, options) {
|
|
808
|
+
if (this.contentOnly) assertContentChanges(changes);
|
|
595
809
|
return this.author ? this.writeCommit(changes, options, this.author) : this.writeGraphQL(changes, options);
|
|
596
810
|
}
|
|
597
811
|
/**
|
|
@@ -728,10 +942,11 @@ var GitHubBackend = class GitHubBackend {
|
|
|
728
942
|
return [...new Set((comparison.files ?? []).flatMap((file) => [file.filename, file.previous_filename ?? []].flat()))];
|
|
729
943
|
}
|
|
730
944
|
/**
|
|
731
|
-
* Reads the deployment GitHub Pages or Vercel created for a commit,
|
|
732
|
-
*
|
|
733
|
-
*
|
|
734
|
-
* 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.
|
|
735
950
|
*/
|
|
736
951
|
async deployStatus(revision) {
|
|
737
952
|
try {
|
|
@@ -760,7 +975,13 @@ var GitHubBackend = class GitHubBackend {
|
|
|
760
975
|
};
|
|
761
976
|
}
|
|
762
977
|
}
|
|
978
|
+
const cloudflare = await this.cloudflareBuild(revision);
|
|
979
|
+
if (cloudflare) return cloudflare;
|
|
763
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
|
+
}
|
|
764
985
|
if (run?.status === "completed" && run.conclusion !== "success" && run.conclusion !== "skipped") return {
|
|
765
986
|
state: "failed",
|
|
766
987
|
detailsUrl: run.html_url,
|
|
@@ -770,18 +991,83 @@ var GitHubBackend = class GitHubBackend {
|
|
|
770
991
|
state: "building",
|
|
771
992
|
detailsUrl: run.html_url
|
|
772
993
|
};
|
|
773
|
-
|
|
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" };
|
|
774
997
|
} catch (error) {
|
|
775
998
|
if (error instanceof GitApiError) return { state: "unknown" };
|
|
776
999
|
throw error;
|
|
777
1000
|
}
|
|
778
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
|
+
}
|
|
779
1064
|
/** The site's deploy workflow run for a commit, or `undefined` if there's none or the token can't see Actions. */
|
|
780
1065
|
async workflowRun(revision) {
|
|
781
1066
|
const response = await githubRequest(this.api, `/repos/${this.repo}/actions/runs?head_sha=${revision}&per_page=20`, { allow: [403, 404] });
|
|
782
1067
|
if (!response.ok) return void 0;
|
|
783
1068
|
const { workflow_runs: runs = [] } = await response.json();
|
|
784
|
-
|
|
1069
|
+
const builds = runs.filter((run) => !run.path?.endsWith(`/${REBUILD_JOB}.yml`));
|
|
1070
|
+
return builds.find((run) => /deploy|pages/i.test(`${run.path ?? ""} ${run.name ?? ""}`)) ?? builds[0];
|
|
785
1071
|
}
|
|
786
1072
|
/** Which step of a failed run failed. */
|
|
787
1073
|
async runProblem(run) {
|
|
@@ -814,8 +1100,6 @@ var GitHubBackend = class GitHubBackend {
|
|
|
814
1100
|
* repository, lasting an hour. Requests are sent as plain text, so browsers
|
|
815
1101
|
* send them straight away without asking the worker first.
|
|
816
1102
|
*/
|
|
817
|
-
/** The sign-in worker Goodfellow runs for every site. Sites can run their own and name it in `github({ signIn })`. */
|
|
818
|
-
const GOODFELLOW_SIGN_IN = "https://sign-in.goodfellow-cms.workers.dev";
|
|
819
1103
|
/** The worker's problems, as the admin panel's sign-in problems. */
|
|
820
1104
|
const PROBLEMS = {
|
|
821
1105
|
invalid: "invalid",
|
|
@@ -935,32 +1219,46 @@ var SignInService = class {
|
|
|
935
1219
|
});
|
|
936
1220
|
return `${this.url}/github/start?${query}`;
|
|
937
1221
|
}
|
|
938
|
-
/**
|
|
939
|
-
|
|
1222
|
+
/**
|
|
1223
|
+
* Exchanges the grant the worker came back with, and this tab's verifier,
|
|
1224
|
+
* for a session (and for a site, its first token, and with `code`, one for
|
|
1225
|
+
* its code repository).
|
|
1226
|
+
*/
|
|
1227
|
+
session(grant, verifier, code) {
|
|
940
1228
|
return this.post("/github/session", {
|
|
941
1229
|
grant,
|
|
942
|
-
verifier
|
|
1230
|
+
verifier,
|
|
1231
|
+
...code && { code }
|
|
943
1232
|
});
|
|
944
1233
|
}
|
|
945
|
-
token(session) {
|
|
946
|
-
return this.post("/github/token", {
|
|
1234
|
+
token(session, code) {
|
|
1235
|
+
return this.post("/github/token", {
|
|
1236
|
+
session,
|
|
1237
|
+
...code && { code }
|
|
1238
|
+
});
|
|
947
1239
|
}
|
|
948
1240
|
};
|
|
949
1241
|
/** A token that renews itself through the worker before it expires, or when GitHub stops accepting it. */
|
|
950
|
-
function renewingToken(service, first, onRenew) {
|
|
1242
|
+
function renewingToken(service, first, onRenew, code) {
|
|
951
1243
|
let current = first;
|
|
952
1244
|
let renewing;
|
|
1245
|
+
async function token({ refresh = false } = {}) {
|
|
1246
|
+
if (refresh || Date.parse(current.expiresAt) - 3e5 < Date.now()) {
|
|
1247
|
+
renewing ??= service.token(current.session, code).finally(() => {
|
|
1248
|
+
renewing = void 0;
|
|
1249
|
+
});
|
|
1250
|
+
current = await renewing;
|
|
1251
|
+
onRenew(current);
|
|
1252
|
+
}
|
|
1253
|
+
return current.token;
|
|
1254
|
+
}
|
|
953
1255
|
return {
|
|
954
1256
|
current: () => current,
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
current = await renewing;
|
|
961
|
-
onRenew(current);
|
|
962
|
-
}
|
|
963
|
-
return current.token;
|
|
1257
|
+
token,
|
|
1258
|
+
async codeToken(options) {
|
|
1259
|
+
await token(options);
|
|
1260
|
+
if (!current.code) throw new SignInError("no-access", "The sign-in service gave no token for the code repository.");
|
|
1261
|
+
return current.code.token;
|
|
964
1262
|
}
|
|
965
1263
|
};
|
|
966
1264
|
}
|
|
@@ -970,8 +1268,9 @@ function renewingToken(service, first, onRenew) {
|
|
|
970
1268
|
const MAIN = "main";
|
|
971
1269
|
/**
|
|
972
1270
|
* Links to GitHub's token pages with what creating a site needs: creating the
|
|
973
|
-
* repository (administration), writing its files and
|
|
974
|
-
* (contents and workflows),
|
|
1271
|
+
* repository and adding deploy keys (administration), writing its files and
|
|
1272
|
+
* its build setup (contents and workflows), turning on GitHub Pages (pages),
|
|
1273
|
+
* and giving a workflow the key to another of the site's repositories (secrets).
|
|
975
1274
|
*/
|
|
976
1275
|
function githubSetupTokenLinks(webUrl = "https://github.com") {
|
|
977
1276
|
const fineGrained = new URLSearchParams({
|
|
@@ -981,7 +1280,8 @@ function githubSetupTokenLinks(webUrl = "https://github.com") {
|
|
|
981
1280
|
administration: "write",
|
|
982
1281
|
contents: "write",
|
|
983
1282
|
workflows: "write",
|
|
984
|
-
pages: "write"
|
|
1283
|
+
pages: "write",
|
|
1284
|
+
secrets: "write"
|
|
985
1285
|
});
|
|
986
1286
|
const classic = new URLSearchParams({
|
|
987
1287
|
scopes: "repo,workflow",
|
|
@@ -994,6 +1294,8 @@ function githubSetupTokenLinks(webUrl = "https://github.com") {
|
|
|
994
1294
|
}
|
|
995
1295
|
/** The steps after the repository exists, which are the same however it was created. */
|
|
996
1296
|
function setupAccount(creator) {
|
|
1297
|
+
/** Each repository created, and the token for setting it up, for connecting it to another one later. */
|
|
1298
|
+
const createdApis = /* @__PURE__ */ new Map();
|
|
997
1299
|
/** Makes `main` the default branch, which the deploy setups build from. */
|
|
998
1300
|
async function useMainBranch(api, repo) {
|
|
999
1301
|
if (repo.default_branch === MAIN) return;
|
|
@@ -1029,6 +1331,25 @@ function setupAccount(creator) {
|
|
|
1029
1331
|
throw error;
|
|
1030
1332
|
}
|
|
1031
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
|
+
}
|
|
1032
1353
|
/** Writes every file in one commit, which replaces the starting README rather than following it. */
|
|
1033
1354
|
async function writeFiles(api, repo, site) {
|
|
1034
1355
|
const base = `/repos/${repoPath(repo.full_name)}`;
|
|
@@ -1108,6 +1429,53 @@ function setupAccount(creator) {
|
|
|
1108
1429
|
]
|
|
1109
1430
|
})).ok;
|
|
1110
1431
|
}
|
|
1432
|
+
/**
|
|
1433
|
+
* Lets each private repository's workflows read the other, as their setup
|
|
1434
|
+
* files say: a read-only deploy key on the one read, with its private half
|
|
1435
|
+
* a secret of the one reading. Public repositories need nothing. Returns
|
|
1436
|
+
* whether it worked: an older sign-in may not be allowed to set secrets.
|
|
1437
|
+
*/
|
|
1438
|
+
async function connectRepositories(link) {
|
|
1439
|
+
if (!link.private) return true;
|
|
1440
|
+
const { newDeployKey, sealSecret } = await import("./deploy-keys-DAj2oX-N.js").then((n) => n.t);
|
|
1441
|
+
try {
|
|
1442
|
+
for (const { reader, read, secret } of link.links.reads) {
|
|
1443
|
+
if (!secret) continue;
|
|
1444
|
+
const readerRepo = link[reader];
|
|
1445
|
+
const readRepo = link[read];
|
|
1446
|
+
const readerApi = createdApis.get(readerRepo.toLowerCase());
|
|
1447
|
+
const readApi = createdApis.get(readRepo.toLowerCase());
|
|
1448
|
+
if (!readerApi || !readApi) throw new Error(`${readerRepo} and ${readRepo} have to be created first.`);
|
|
1449
|
+
const key = await newDeployKey(`goodfellow-${secret.toLowerCase()}`);
|
|
1450
|
+
await githubRequest(readApi, `/repos/${repoPath(readRepo)}/keys`, {
|
|
1451
|
+
method: "POST",
|
|
1452
|
+
body: {
|
|
1453
|
+
title: `${readerRepo}'s builds`,
|
|
1454
|
+
key: key.publicKey,
|
|
1455
|
+
read_only: true
|
|
1456
|
+
}
|
|
1457
|
+
});
|
|
1458
|
+
const base = `/repos/${repoPath(readerRepo)}/actions/secrets`;
|
|
1459
|
+
const publicKey = await githubJson(readerApi, `${base}/public-key`);
|
|
1460
|
+
const sealed = await sealSecret(key.privateKey, decodeBase64(publicKey.key));
|
|
1461
|
+
await githubRequest(readerApi, `${base}/${secret}`, {
|
|
1462
|
+
method: "PUT",
|
|
1463
|
+
body: {
|
|
1464
|
+
encrypted_value: encodeBase64Bytes(sealed),
|
|
1465
|
+
key_id: publicKey.key_id
|
|
1466
|
+
}
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
return true;
|
|
1470
|
+
} catch (error) {
|
|
1471
|
+
if (error instanceof GitApiError && [
|
|
1472
|
+
403,
|
|
1473
|
+
404,
|
|
1474
|
+
422
|
|
1475
|
+
].includes(error.status)) return false;
|
|
1476
|
+
throw error;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1111
1479
|
return {
|
|
1112
1480
|
user: creator.user,
|
|
1113
1481
|
owners: () => creator.owners(),
|
|
@@ -1116,13 +1484,23 @@ function setupAccount(creator) {
|
|
|
1116
1484
|
const warnings = [];
|
|
1117
1485
|
onStep?.("repository");
|
|
1118
1486
|
const { repo, api, signInInstallUrl } = await creator.create(site);
|
|
1487
|
+
const connect = async (link) => {
|
|
1488
|
+
onStep?.("connect");
|
|
1489
|
+
return connectRepositories(link);
|
|
1490
|
+
};
|
|
1119
1491
|
await useMainBranch(api, repo);
|
|
1120
1492
|
let siteUrl;
|
|
1121
1493
|
if (site.pages) {
|
|
1122
1494
|
onStep?.("pages");
|
|
1123
1495
|
siteUrl = await turnOnPages(api, repo);
|
|
1124
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
|
+
}
|
|
1125
1501
|
}
|
|
1502
|
+
createdApis.set(repo.full_name.toLowerCase(), api);
|
|
1503
|
+
if (site.link && !await connect(site.link)) warnings.push("connect-unavailable");
|
|
1126
1504
|
onStep?.("files");
|
|
1127
1505
|
await writeFiles(api, repo, site);
|
|
1128
1506
|
onStep?.("protection");
|
|
@@ -1233,7 +1611,8 @@ function workerCreator(service, apiUrl, fetcher, signedIn) {
|
|
|
1233
1611
|
owner: site.owner.path,
|
|
1234
1612
|
name: site.name,
|
|
1235
1613
|
private: site.private,
|
|
1236
|
-
description: site.description
|
|
1614
|
+
description: site.description,
|
|
1615
|
+
...site.more && { more: true }
|
|
1237
1616
|
});
|
|
1238
1617
|
if (!created.repo || !created.token) throw new SignInError("unavailable", "The sign-in service didn't answer.");
|
|
1239
1618
|
return {
|
|
@@ -1264,7 +1643,7 @@ function githubSetup(options = {}) {
|
|
|
1264
1643
|
const links = githubSetupTokenLinks(options.webUrl);
|
|
1265
1644
|
const fetcher = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
1266
1645
|
const location = options.location ?? browserLocation();
|
|
1267
|
-
const signInUrl = options.signIn
|
|
1646
|
+
const signInUrl = options.signIn;
|
|
1268
1647
|
const service = signInUrl && location && !isLocalAddress(location.href()) ? new SignInService(signInUrl, fetcher) : void 0;
|
|
1269
1648
|
const pending = credentialStorage("goodfellow:github:setup:pending", options.storage);
|
|
1270
1649
|
return {
|
|
@@ -1336,7 +1715,8 @@ function githubTokenLinks(repo, webUrl = "https://github.com") {
|
|
|
1336
1715
|
expires_in: "90",
|
|
1337
1716
|
contents: "write",
|
|
1338
1717
|
deployments: "read",
|
|
1339
|
-
actions: "read"
|
|
1718
|
+
actions: "read",
|
|
1719
|
+
statuses: "read"
|
|
1340
1720
|
});
|
|
1341
1721
|
const classic = new URLSearchParams({
|
|
1342
1722
|
scopes: "repo",
|
|
@@ -1349,7 +1729,8 @@ function githubTokenLinks(repo, webUrl = "https://github.com") {
|
|
|
1349
1729
|
expires_in: "1",
|
|
1350
1730
|
administration: "write",
|
|
1351
1731
|
pages: "write",
|
|
1352
|
-
actions: "write"
|
|
1732
|
+
actions: "write",
|
|
1733
|
+
variables: "write"
|
|
1353
1734
|
});
|
|
1354
1735
|
return {
|
|
1355
1736
|
fineGrained: `${webUrl}/settings/personal-access-tokens/new?${fineGrained}`,
|
|
@@ -1360,14 +1741,24 @@ function githubTokenLinks(repo, webUrl = "https://github.com") {
|
|
|
1360
1741
|
/** Sign-in and storage for a site whose repository is on GitHub. */
|
|
1361
1742
|
function github(options) {
|
|
1362
1743
|
if (!/^[\w.-]+\/[\w.-]+$/.test(options.repo)) throw new Error(`github({ repo }) must look like "owner/name" (got "${options.repo}").`);
|
|
1744
|
+
if (options.code !== void 0 && !/^[\w.-]+\/[\w.-]+$/.test(options.code)) throw new Error(`github({ code }) must look like "owner/name" (got "${options.code}").`);
|
|
1363
1745
|
const apiUrl = (options.apiUrl ?? "https://api.github.com").replace(/\/$/, "");
|
|
1746
|
+
const webUrl = (options.webUrl ?? "https://github.com").replace(/\/$/, "");
|
|
1747
|
+
/** The code repository, where the content is in a repository of its own, read with `api`. */
|
|
1748
|
+
const codeRepository = (api) => options.contentOnly && options.code ? { code: {
|
|
1749
|
+
repo: options.code,
|
|
1750
|
+
webUrl,
|
|
1751
|
+
...api && { api }
|
|
1752
|
+
} } : {};
|
|
1364
1753
|
const links = githubTokenLinks(options.repo, options.webUrl);
|
|
1365
1754
|
const storage = credentialStorage(`goodfellow:github:${options.repo}`, options.storage);
|
|
1366
1755
|
const pending = credentialStorage(`goodfellow:github:pending:${options.repo}`, options.storage);
|
|
1367
1756
|
const fetcher = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
1368
1757
|
const location = options.location ?? browserLocation();
|
|
1369
|
-
const signInUrl = options.signIn
|
|
1758
|
+
const signInUrl = options.signIn;
|
|
1370
1759
|
const service = signInUrl && location && !isLocalAddress(location.href()) ? new SignInService(signInUrl, fetcher) : void 0;
|
|
1760
|
+
/** The code repository to ask the worker for a token for, where the Updates screen follows it. */
|
|
1761
|
+
const codeName = () => options.contentOnly ? options.code : void 0;
|
|
1371
1762
|
function save(value, remember) {
|
|
1372
1763
|
storage.save(value.type === "token" ? value.token : JSON.stringify(value), remember);
|
|
1373
1764
|
}
|
|
@@ -1410,7 +1801,7 @@ function github(options) {
|
|
|
1410
1801
|
const source = renewingToken(service, issued, (renewed) => save({
|
|
1411
1802
|
type: "worker",
|
|
1412
1803
|
session: renewed.session
|
|
1413
|
-
}, remember));
|
|
1804
|
+
}, remember), options.code);
|
|
1414
1805
|
save({
|
|
1415
1806
|
type: "worker",
|
|
1416
1807
|
session: issued.session
|
|
@@ -1426,6 +1817,12 @@ function github(options) {
|
|
|
1426
1817
|
api,
|
|
1427
1818
|
repo: options.repo,
|
|
1428
1819
|
branch: options.branch ?? repo.default_branch,
|
|
1820
|
+
...options.contentOnly && { contentOnly: true },
|
|
1821
|
+
...codeRepository({
|
|
1822
|
+
apiUrl,
|
|
1823
|
+
token: source.codeToken,
|
|
1824
|
+
fetch: fetcher
|
|
1825
|
+
}),
|
|
1429
1826
|
user: {
|
|
1430
1827
|
login: user.login,
|
|
1431
1828
|
name: user.name,
|
|
@@ -1437,7 +1834,7 @@ function github(options) {
|
|
|
1437
1834
|
},
|
|
1438
1835
|
onSignOut: () => {
|
|
1439
1836
|
storage.clear();
|
|
1440
|
-
|
|
1837
|
+
revokeIssued(source.current());
|
|
1441
1838
|
},
|
|
1442
1839
|
ownerTokenLink: {
|
|
1443
1840
|
url: links.owner,
|
|
@@ -1454,12 +1851,17 @@ function github(options) {
|
|
|
1454
1851
|
fetch: fetcher
|
|
1455
1852
|
}, "/installation/token", { method: "DELETE" }).catch(() => {});
|
|
1456
1853
|
}
|
|
1854
|
+
/** Revokes the tokens the worker gave: the site's, and the code repository's. */
|
|
1855
|
+
function revokeIssued(issued) {
|
|
1856
|
+
revokeToken(issued.token);
|
|
1857
|
+
if (issued.code) revokeToken(issued.code.token);
|
|
1858
|
+
}
|
|
1457
1859
|
/** Gives the backend an owner's sign-in that has just come back from GitHub. It's kept in memory only. */
|
|
1458
1860
|
async function useOwner(backend, grant, verifier) {
|
|
1459
1861
|
const worker = service;
|
|
1460
1862
|
try {
|
|
1461
|
-
const source = renewingToken(worker, await worker.session(grant, verifier), () => {});
|
|
1462
|
-
backend.useOwnerSignIn(source.token, () =>
|
|
1863
|
+
const source = renewingToken(worker, await worker.session(grant, verifier, codeName()), () => {}, codeName());
|
|
1864
|
+
backend.useOwnerSignIn(source.token, () => revokeIssued(source.current()), codeName() ? source.codeToken : void 0);
|
|
1463
1865
|
} catch (error) {
|
|
1464
1866
|
backend.ownerSignInFailed(error);
|
|
1465
1867
|
}
|
|
@@ -1486,7 +1888,7 @@ function github(options) {
|
|
|
1486
1888
|
return backend;
|
|
1487
1889
|
}
|
|
1488
1890
|
if (failed || !grant) throw signInProblem({ problem: failed });
|
|
1489
|
-
return connectWorker(await service.session(grant, redirect.verifier), redirect.remember);
|
|
1891
|
+
return connectWorker(await service.session(grant, redirect.verifier, codeName()), redirect.remember);
|
|
1490
1892
|
}
|
|
1491
1893
|
async function restoreSaved() {
|
|
1492
1894
|
const saved = loadSaved();
|
|
@@ -1497,7 +1899,7 @@ function github(options) {
|
|
|
1497
1899
|
storage.clear();
|
|
1498
1900
|
return null;
|
|
1499
1901
|
}
|
|
1500
|
-
return await connectWorker(await service.token(saved.session), storage.remembered());
|
|
1902
|
+
return await connectWorker(await service.token(saved.session, codeName()), storage.remembered());
|
|
1501
1903
|
} catch (error) {
|
|
1502
1904
|
if (forgets(error)) storage.clear();
|
|
1503
1905
|
throw error;
|
|
@@ -1522,6 +1924,8 @@ function github(options) {
|
|
|
1522
1924
|
api,
|
|
1523
1925
|
repo: options.repo,
|
|
1524
1926
|
branch: options.branch ?? repo.default_branch,
|
|
1927
|
+
...options.contentOnly && { contentOnly: true },
|
|
1928
|
+
...codeRepository(api),
|
|
1525
1929
|
user: {
|
|
1526
1930
|
login: user.login,
|
|
1527
1931
|
name: user.name ?? void 0,
|
|
@@ -1535,9 +1939,62 @@ function github(options) {
|
|
|
1535
1939
|
...service && { ownerRedirect: (redirectOptions) => redirect("owner", false, redirectOptions) }
|
|
1536
1940
|
});
|
|
1537
1941
|
}
|
|
1942
|
+
/** The repository through a token of the site's own, on a full-stack site's server. */
|
|
1943
|
+
function server({ token }) {
|
|
1944
|
+
const api = {
|
|
1945
|
+
apiUrl,
|
|
1946
|
+
token: typeof token === "string" ? token : () => token(),
|
|
1947
|
+
fetch: fetcher
|
|
1948
|
+
};
|
|
1949
|
+
let branch;
|
|
1950
|
+
const liveBranch = () => {
|
|
1951
|
+
branch ??= options.branch ? Promise.resolve(options.branch) : githubJson(api, `/repos/${repoPath(options.repo)}`).then((repo) => repo.default_branch);
|
|
1952
|
+
branch.catch(() => {
|
|
1953
|
+
branch = void 0;
|
|
1954
|
+
});
|
|
1955
|
+
return branch;
|
|
1956
|
+
};
|
|
1957
|
+
const backend = async (revision, author) => new GitHubBackend({
|
|
1958
|
+
api,
|
|
1959
|
+
repo: options.repo,
|
|
1960
|
+
branch: await liveBranch(),
|
|
1961
|
+
...options.contentOnly && { contentOnly: true },
|
|
1962
|
+
user: {
|
|
1963
|
+
login: author?.name ?? options.repo,
|
|
1964
|
+
...author && { name: author.name }
|
|
1965
|
+
},
|
|
1966
|
+
onSignOut: () => {},
|
|
1967
|
+
ownerTokenLink: {
|
|
1968
|
+
url: links.owner,
|
|
1969
|
+
label: "owner.token.create"
|
|
1970
|
+
},
|
|
1971
|
+
...revision && { revision },
|
|
1972
|
+
...author && { author }
|
|
1973
|
+
});
|
|
1974
|
+
return {
|
|
1975
|
+
...options.contentOnly && { contentOnly: true },
|
|
1976
|
+
async revision() {
|
|
1977
|
+
return (await backend()).revision();
|
|
1978
|
+
},
|
|
1979
|
+
at: backend
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1538
1982
|
return {
|
|
1539
1983
|
name: "GitHub",
|
|
1984
|
+
source: hostSource("@goodfellow-cms/github", "github", options, [
|
|
1985
|
+
"fetch",
|
|
1986
|
+
"storage",
|
|
1987
|
+
"location"
|
|
1988
|
+
]),
|
|
1540
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
|
+
}),
|
|
1541
1998
|
tokenLinks: [{
|
|
1542
1999
|
url: links.fineGrained,
|
|
1543
2000
|
label: "signIn.token.create"
|
|
@@ -1563,8 +2020,9 @@ function github(options) {
|
|
|
1563
2020
|
},
|
|
1564
2021
|
startRedirect(remember, redirectOptions) {
|
|
1565
2022
|
return redirect("edit", remember, redirectOptions);
|
|
1566
|
-
}
|
|
2023
|
+
},
|
|
2024
|
+
server
|
|
1567
2025
|
};
|
|
1568
2026
|
}
|
|
1569
2027
|
//#endregion
|
|
1570
|
-
export {
|
|
2028
|
+
export { GitHubBackend, github, githubPagesRecords, githubSetup, githubSetupTokenLinks, githubTokenLinks };
|