@goodfellow-cms/github 0.5.0 → 0.6.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/README.md +1 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +68 -1
- package/dist/testing.d.ts +20 -0
- package/dist/testing.js +18 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ import { github } from "@goodfellow-cms/github";
|
|
|
8
8
|
export default defineConfig({ blocks, backend: github({ repo: "your-name/your-site" }) });
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
[Goodfellow](https://github.com/goodfellow-cms/goodfellow-cms) is a website builder for people who aren't developers, with no server or database: pages are files in your own GitHub or
|
|
11
|
+
[Goodfellow](https://github.com/goodfellow-cms/goodfellow-cms) is a website builder for people who aren't developers, with no server or database: pages are files in your own GitHub, GitLab or Bitbucket repository, edited in a visual editor built on [Puck](https://puckeditor.com). To start a new site, run `npm create goodfellow@latest my-site`. See the [documentation](https://goodfellow-cms.github.io/goodfellow-cms/) for everything else.
|
|
12
12
|
|
|
13
13
|
More in the docs: [Hosts and business sites](https://goodfellow-cms.github.io/goodfellow-cms/docs/hosts).
|
|
14
14
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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";
|
|
1
|
+
import { ConnectDomainOptions, DeployStatus, DnsRecord, FileChange, FileVersion, GitBackend, GitHost, GitUser, HistoryOptions, OwnerAccess, PagesDomains, PluginTasks, 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 {
|
|
@@ -20,6 +20,15 @@ interface ApiOptions {
|
|
|
20
20
|
fetch: typeof fetch;
|
|
21
21
|
}
|
|
22
22
|
//#endregion
|
|
23
|
+
//#region src/tasks.d.ts
|
|
24
|
+
interface GitHubTasksOptions {
|
|
25
|
+
/** How often to look for the answer, and how long to wait for it. */
|
|
26
|
+
pollMs?: number;
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
/** Waits between looks; tests pass one that doesn't. */
|
|
29
|
+
wait?: (ms: number) => Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
23
32
|
//#region src/backend.d.ts
|
|
24
33
|
interface GitHubBackendOptions {
|
|
25
34
|
api: ApiOptions;
|
|
@@ -42,6 +51,8 @@ interface GitHubBackendOptions {
|
|
|
42
51
|
};
|
|
43
52
|
/** The commit reads see until `revision()` is next called, as a full-stack site's server reads what a browser loaded. */
|
|
44
53
|
revision?: string;
|
|
54
|
+
/** How plugins' tasks wait for their answers; tests pass short waits. */
|
|
55
|
+
tasks?: GitHubTasksOptions;
|
|
45
56
|
/** The repository holds only the site's content, with its code in a repository of its own (`GitBackend.contentOnly`). */
|
|
46
57
|
contentOnly?: boolean;
|
|
47
58
|
/**
|
|
@@ -68,6 +79,7 @@ export declare class GitHubBackend implements GitBackend {
|
|
|
68
79
|
readonly updates?: SiteUpdates;
|
|
69
80
|
readonly drafts: SiteDrafts;
|
|
70
81
|
readonly scheduledBuilds: ScheduledBuilds;
|
|
82
|
+
readonly tasks: PluginTasks;
|
|
71
83
|
readonly contentOnly: boolean;
|
|
72
84
|
private readonly options;
|
|
73
85
|
private readonly api;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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";
|
|
1
|
+
import { ConflictError, DomainError, DraftError, EditorsError, GitApiError, NIGHTLY_VARIABLE, PluginTaskError, REBUILD_JOB, SetupError, SignInError, TASK_BRANCH_PREFIX, TASK_EVENT, TASK_POLL_MS, TASK_RESULT_FILE, TASK_TIMEOUT_MS, TASK_WORKFLOW, UpdatesError, assertContentChanges, branchDrafts, buildStepFor, compareReleases, credentialStorage, decodeBase64, encodeBase64, encodeBase64Bytes, hostSource, nightlyMode, siteVersion, taskRequestProblem } 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 = {}) {
|
|
@@ -349,6 +349,71 @@ function githubScheduledBuilds(read) {
|
|
|
349
349
|
} };
|
|
350
350
|
}
|
|
351
351
|
//#endregion
|
|
352
|
+
//#region src/tasks.ts
|
|
353
|
+
/** GitHub's own pages for an API address: `github.com` for its API, the host itself for GitHub Enterprise's `/api/v3`. */
|
|
354
|
+
function webBase(apiUrl) {
|
|
355
|
+
const url = new URL(apiUrl);
|
|
356
|
+
if (url.hostname === "api.github.com") return "https://github.com";
|
|
357
|
+
return `${url.origin}${url.pathname.replace(/\/api\/v3\/?$/, "")}`.replace(/\/+$/, "");
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Plugins' tasks on GitHub, through the starters' `plugin-task.yml`: a
|
|
361
|
+
* `repository_dispatch` event starts it, which editors' tokens can send with
|
|
362
|
+
* the Contents permission they publish with, and its `answer` job leaves the
|
|
363
|
+
* sealed answer on a branch of its own, `goodfellow-task-<request>`, which is
|
|
364
|
+
* read here and deleted. No Actions permission is needed.
|
|
365
|
+
*/
|
|
366
|
+
function githubTasks(api, repo, branch, options = {}) {
|
|
367
|
+
const wait = options.wait ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
|
|
368
|
+
const contents = (path, ref) => githubRequest(api, `/repos/${repo}/contents/${path}?ref=${encodeURIComponent(ref)}`, { allow: [404] });
|
|
369
|
+
const supported = async () => (await contents(`.github/workflows/${TASK_WORKFLOW}`, branch)).ok;
|
|
370
|
+
return {
|
|
371
|
+
supported,
|
|
372
|
+
secretsPlace: async () => ({
|
|
373
|
+
kind: "github",
|
|
374
|
+
url: `${webBase(api.apiUrl)}/${repo}/settings/secrets/actions/new`
|
|
375
|
+
}),
|
|
376
|
+
async run(request) {
|
|
377
|
+
const problem = taskRequestProblem(request);
|
|
378
|
+
if (problem) throw new PluginTaskError("failed", problem);
|
|
379
|
+
if (!await supported()) throw new PluginTaskError("missing", "The site's setup files don't run plugins' tasks: it needs plugin-task.yml.");
|
|
380
|
+
try {
|
|
381
|
+
await githubRequest(api, `/repos/${repo}/dispatches`, {
|
|
382
|
+
method: "POST",
|
|
383
|
+
body: {
|
|
384
|
+
event_type: TASK_EVENT,
|
|
385
|
+
client_payload: {
|
|
386
|
+
request: request.id,
|
|
387
|
+
plugin: request.plugin,
|
|
388
|
+
task: request.task,
|
|
389
|
+
input: request.input,
|
|
390
|
+
key: request.key
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
} catch (error) {
|
|
395
|
+
if (error instanceof GitApiError && (error.status === 403 || error.status === 404)) throw new PluginTaskError("not-allowed", "Running plugins' tasks needs a sign-in that can publish to the site.");
|
|
396
|
+
throw error;
|
|
397
|
+
}
|
|
398
|
+
const answerBranch = `${TASK_BRANCH_PREFIX}${request.id}`;
|
|
399
|
+
const deadline = Date.now() + (options.timeoutMs ?? TASK_TIMEOUT_MS);
|
|
400
|
+
for (;;) {
|
|
401
|
+
const found = await contents(TASK_RESULT_FILE, answerBranch);
|
|
402
|
+
if (found.ok) {
|
|
403
|
+
const { content = "" } = await found.json();
|
|
404
|
+
await githubRequest(api, `/repos/${repo}/git/refs/heads/${encodeURIComponent(answerBranch)}`, {
|
|
405
|
+
method: "DELETE",
|
|
406
|
+
allow: [404, 422]
|
|
407
|
+
}).catch(() => {});
|
|
408
|
+
return new TextDecoder().decode(decodeBase64(content.replace(/\s/g, "")));
|
|
409
|
+
}
|
|
410
|
+
if (Date.now() >= deadline) throw new PluginTaskError("timeout", "The site's build service didn't answer in time.");
|
|
411
|
+
await wait(options.pollMs ?? TASK_POLL_MS);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
//#endregion
|
|
352
417
|
//#region src/updates.ts
|
|
353
418
|
/** The starters' GitHub Pages workflow, which runs `goodfellow update` each night and when it's dispatched. */
|
|
354
419
|
const WORKFLOW = "deploy.yml";
|
|
@@ -581,6 +646,7 @@ var GitHubBackend = class GitHubBackend {
|
|
|
581
646
|
updates;
|
|
582
647
|
drafts;
|
|
583
648
|
scheduledBuilds;
|
|
649
|
+
tasks;
|
|
584
650
|
contentOnly;
|
|
585
651
|
options;
|
|
586
652
|
api;
|
|
@@ -626,6 +692,7 @@ var GitHubBackend = class GitHubBackend {
|
|
|
626
692
|
});
|
|
627
693
|
this.drafts = branchDrafts(this, this.draftHost());
|
|
628
694
|
this.scheduledBuilds = githubScheduledBuilds((path) => this.read(path));
|
|
695
|
+
this.tasks = githubTasks(this.api, this.repo, this.branch, options.tasks);
|
|
629
696
|
}
|
|
630
697
|
async repositoryVisibility() {
|
|
631
698
|
const found = await githubJson(this.api, `/repos/${this.repo}`);
|
package/dist/testing.d.ts
CHANGED
|
@@ -103,6 +103,21 @@ export interface FakeGitHubOptions {
|
|
|
103
103
|
apps?: FakeGitHubApp[];
|
|
104
104
|
/** Whether the site's repository is private. Public by default. */
|
|
105
105
|
private?: boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Answers plugins' tasks, as the site's `plugin-task.yml` would: given each
|
|
108
|
+
* `repository_dispatch` event's payload, it gives the text the workflow's
|
|
109
|
+
* `answer` job leaves on the request's branch, or nothing for a workflow
|
|
110
|
+
* that never answers. Without it, events are only recorded.
|
|
111
|
+
*/
|
|
112
|
+
tasks?: (payload: FakeTaskPayload) => string | undefined | Promise<string | undefined>;
|
|
113
|
+
}
|
|
114
|
+
/** A plugin task's `repository_dispatch` payload, as the admin panel sends it. */
|
|
115
|
+
export interface FakeTaskPayload {
|
|
116
|
+
request: string;
|
|
117
|
+
plugin: string;
|
|
118
|
+
task: string;
|
|
119
|
+
input: string;
|
|
120
|
+
key: string;
|
|
106
121
|
}
|
|
107
122
|
/** A GitHub App, as the sign-in worker uses one: its JWTs, installations, OAuth sign-in and tokens. */
|
|
108
123
|
export interface FakeGitHubApp {
|
|
@@ -208,6 +223,11 @@ export declare function fakeGitHub(options: FakeGitHubOptions): {
|
|
|
208
223
|
failCloudflare: (sha: string) => void;
|
|
209
224
|
/** GitHub Actions workflow runs, which tests add with `failBuild()` and `cancelBuild()`. */
|
|
210
225
|
runs: FakeRun[];
|
|
226
|
+
/** `repository_dispatch` events sent, such as plugins' tasks. */
|
|
227
|
+
dispatches: {
|
|
228
|
+
event_type: string;
|
|
229
|
+
client_payload: FakeTaskPayload;
|
|
230
|
+
}[];
|
|
211
231
|
/** The repository's Actions variables, such as `GOODFELLOW_NIGHTLY`. */
|
|
212
232
|
variables: Map<string, string>;
|
|
213
233
|
failBuild: (sha: string, step?: string, job?: string) => FakeRun;
|
package/dist/testing.js
CHANGED
|
@@ -55,6 +55,8 @@ function fakeGitHub(options) {
|
|
|
55
55
|
const deployments = [];
|
|
56
56
|
const cloudflareBuilds = /* @__PURE__ */ new Map();
|
|
57
57
|
const runs = [];
|
|
58
|
+
/** `repository_dispatch` events, in the order they were sent. */
|
|
59
|
+
const dispatches = [];
|
|
58
60
|
/** The repository's Actions variables, by name. */
|
|
59
61
|
const variables = /* @__PURE__ */ new Map();
|
|
60
62
|
const requests = [];
|
|
@@ -811,7 +813,9 @@ function fakeGitHub(options) {
|
|
|
811
813
|
const contents = rest.match(/^\/contents\/(.+)$/);
|
|
812
814
|
if (contents && request.method === "GET") {
|
|
813
815
|
if (user.bot && !user.permissions?.contents) return json({ message: "Resource not accessible by integration" }, 403);
|
|
814
|
-
const
|
|
816
|
+
const ref = url.searchParams.get("ref") ?? repo.defaultBranch;
|
|
817
|
+
if (!repo.branches.has(ref) && !repo.commitAt(ref)) return json({ message: `No commit found for the ref ${ref}` }, 404);
|
|
818
|
+
const file = repo.files(ref).get(decodeURIComponent(contents[1] ?? ""));
|
|
815
819
|
if (file === void 0) return json({ message: "Not Found" }, 404);
|
|
816
820
|
return request.headers.get("accept")?.includes("raw") ? new Response(fakeFileBytes(file)) : json({
|
|
817
821
|
content: encodeBase64Bytes(fakeFileBytes(file)),
|
|
@@ -985,6 +989,17 @@ function fakeGitHub(options) {
|
|
|
985
989
|
return new Response(null, { status: 204 });
|
|
986
990
|
}
|
|
987
991
|
}
|
|
992
|
+
if (rest === "/dispatches" && request.method === "POST") {
|
|
993
|
+
if (user.push === false || user.bot && user.permissions?.contents !== "write") return json({ message: "Resource not accessible by integration" }, 403);
|
|
994
|
+
const body = await request.json();
|
|
995
|
+
dispatches.push(body);
|
|
996
|
+
const answer = await options.tasks?.(body.client_payload);
|
|
997
|
+
if (answer !== void 0 && body.event_type === "goodfellow-plugin-task") {
|
|
998
|
+
const sha = repo.addCommit(void 0, `The answer to plugin task ${body.client_payload.request}`, /* @__PURE__ */ new Map([["goodfellow-task-result.json", `${answer}\n`]]));
|
|
999
|
+
repo.branches.set(`goodfellow-task-${body.client_payload.request}`, sha);
|
|
1000
|
+
}
|
|
1001
|
+
return new Response(null, { status: 204 });
|
|
1002
|
+
}
|
|
988
1003
|
const workflowFile = rest.match(/^\/actions\/workflows\/([\w.-]+)\/(?:dispatches|runs)$/)?.[1];
|
|
989
1004
|
const workflowPath = `.github/workflows/${workflowFile}`;
|
|
990
1005
|
const ofWorkflow = (run) => (run.path ?? ".github/workflows/deploy.yml") === workflowPath;
|
|
@@ -1141,6 +1156,8 @@ function fakeGitHub(options) {
|
|
|
1141
1156
|
failCloudflare,
|
|
1142
1157
|
/** GitHub Actions workflow runs, which tests add with `failBuild()` and `cancelBuild()`. */
|
|
1143
1158
|
runs,
|
|
1159
|
+
/** `repository_dispatch` events sent, such as plugins' tasks. */
|
|
1160
|
+
dispatches,
|
|
1144
1161
|
/** The repository's Actions variables, such as `GOODFELLOW_NIGHTLY`. */
|
|
1145
1162
|
variables,
|
|
1146
1163
|
failBuild,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodfellow-cms/github",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Edit a Goodfellow site stored on GitHub, straight from the browser.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"goodfellow",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"dist"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@goodfellow-cms/core": "0.
|
|
38
|
+
"@goodfellow-cms/core": "0.6.0",
|
|
39
39
|
"@noble/ciphers": "2.4.0",
|
|
40
40
|
"@noble/hashes": "2.4.0"
|
|
41
41
|
},
|