@goodfellow-cms/gitlab 0.2.0 → 0.4.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 +37 -2
- package/dist/index.js +307 -9
- package/dist/testing.d.ts +22 -1
- package/dist/testing.js +137 -19
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConnectDomainOptions, DeployStatus, DnsRecord, FileChange, FileVersion, GitBackend, GitHost, GitUser, HistoryOptions, PagesDomains, SetupHost, SiteEditors, SiteUpdates, StorageLike, WriteOptions } from "@goodfellow-cms/core";
|
|
1
|
+
import { ConnectDomainOptions, DeployStatus, DnsRecord, FileChange, FileVersion, GitBackend, GitHost, GitUser, HistoryOptions, PagesDomains, SetupHost, SiteDrafts, SiteEditors, SiteUpdates, StorageLike, WriteOptions } from "@goodfellow-cms/core";
|
|
2
2
|
//#region src/api.d.ts
|
|
3
3
|
interface ApiOptions {
|
|
4
4
|
/** The API root, such as `https://gitlab.com/api/v4`. */
|
|
@@ -19,6 +19,20 @@ interface GitLabBackendOptions {
|
|
|
19
19
|
branch: string;
|
|
20
20
|
user: GitUser;
|
|
21
21
|
onSignOut: () => void;
|
|
22
|
+
/** Who commits name as their author, where that isn't the token's owner, as on a full-stack site's server. */
|
|
23
|
+
author?: {
|
|
24
|
+
name: string;
|
|
25
|
+
email: string;
|
|
26
|
+
};
|
|
27
|
+
/** The commit reads see until `revision()` is next called, as a full-stack site's server reads what a browser loaded. */
|
|
28
|
+
revision?: string;
|
|
29
|
+
/** The project holds only the site's content, with its code in a project of its own (`GitBackend.contentOnly`). */
|
|
30
|
+
contentOnly?: boolean;
|
|
31
|
+
/** The code project, for a project that holds only content, where the site's updates run. */
|
|
32
|
+
code?: {
|
|
33
|
+
project: string;
|
|
34
|
+
webUrl: string;
|
|
35
|
+
};
|
|
22
36
|
}
|
|
23
37
|
/**
|
|
24
38
|
* The site's files on GitLab. Reads come from the commit `revision()` last
|
|
@@ -29,7 +43,11 @@ export declare class GitLabBackend implements GitBackend {
|
|
|
29
43
|
readonly user: GitUser;
|
|
30
44
|
readonly pages: PagesDomains;
|
|
31
45
|
readonly editors: SiteEditors;
|
|
32
|
-
|
|
46
|
+
/** Absent where the project holds only content: the update job runs where the code is. */
|
|
47
|
+
readonly updates?: SiteUpdates;
|
|
48
|
+
readonly drafts: SiteDrafts;
|
|
49
|
+
readonly contentOnly: boolean;
|
|
50
|
+
private readonly options;
|
|
33
51
|
private readonly api;
|
|
34
52
|
private readonly project;
|
|
35
53
|
private readonly branch;
|
|
@@ -38,6 +56,8 @@ export declare class GitLabBackend implements GitBackend {
|
|
|
38
56
|
private readonly trees;
|
|
39
57
|
private readonly blobs;
|
|
40
58
|
constructor(options: GitLabBackendOptions);
|
|
59
|
+
/** Drafts on branches of the project, with merge requests for review. */
|
|
60
|
+
private draftHost;
|
|
41
61
|
private repo;
|
|
42
62
|
revision(): Promise<string>;
|
|
43
63
|
/** Every file in one folder at a commit, page by page. */
|
|
@@ -46,8 +66,12 @@ export declare class GitLabBackend implements GitBackend {
|
|
|
46
66
|
private tree;
|
|
47
67
|
private currentTree;
|
|
48
68
|
read(path: string): Promise<string | undefined>;
|
|
69
|
+
/** Reads many text files in a few GraphQL requests rather than one request each. */
|
|
70
|
+
readFiles(paths: string[]): Promise<Map<string, string>>;
|
|
49
71
|
readBytes(path: string): Promise<Uint8Array | undefined>;
|
|
50
72
|
list(dir: string): Promise<string[]>;
|
|
73
|
+
/** GitLab's tree doesn't say how large files are, so they're asked for with GraphQL, in batches. */
|
|
74
|
+
fileSizes(dir: string): Promise<Map<string, number>>;
|
|
51
75
|
private lastCommitFor;
|
|
52
76
|
write(changes: FileChange[], { message, expectedRevision }: WriteOptions): Promise<{
|
|
53
77
|
revision: string;
|
|
@@ -122,6 +146,17 @@ export interface GitLabOptions {
|
|
|
122
146
|
* one-click "Sign in with GitLab". Without it, people sign in with a token.
|
|
123
147
|
*/
|
|
124
148
|
clientId?: string;
|
|
149
|
+
/**
|
|
150
|
+
* The project holds only the site's content (`content/` and
|
|
151
|
+
* `public/media/`), and its code is in a project of its own. The admin
|
|
152
|
+
* panel then leaves adding blocks, which changes code, to a developer.
|
|
153
|
+
*/
|
|
154
|
+
contentOnly?: boolean;
|
|
155
|
+
/**
|
|
156
|
+
* With `contentOnly`, the project the site's code is kept in, where its
|
|
157
|
+
* updates run: the Updates screen follows them, for people who can see it.
|
|
158
|
+
*/
|
|
159
|
+
code?: string;
|
|
125
160
|
/** For tests. */
|
|
126
161
|
fetch?: typeof fetch;
|
|
127
162
|
/** For tests. */
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConflictError, DomainError, EDITABLE_FOLDERS, EditorsError, GitApiError, SetupError, SignInError, UpdatesError, buildStepFor, credentialStorage, encodeBase64Bytes, findBuildCause, findUpdateResult } from "@goodfellow-cms/core";
|
|
1
|
+
import { ConflictError, DomainError, DraftError, EDITABLE_FOLDERS, EditorsError, GitApiError, REBUILD_JOB, SetupError, SignInError, UpdatesError, assertContentChanges, branchDrafts, buildStepFor, credentialStorage, encodeBase64Bytes, findBuildCause, findUpdateResult } from "@goodfellow-cms/core";
|
|
2
2
|
//#region src/api.ts
|
|
3
3
|
/** Calls the GitLab API. A 401 refreshes the token once; after that it means the sign-in has expired. */
|
|
4
4
|
async function gitlabRequest(api, path, init = {}) {
|
|
@@ -343,7 +343,7 @@ function notAllowed(error) {
|
|
|
343
343
|
* It publishes with the job's own token, which GitLab only allows once the
|
|
344
344
|
* project lets job tokens push.
|
|
345
345
|
*/
|
|
346
|
-
function gitlabUpdates(api, project, branch, readAt) {
|
|
346
|
+
function gitlabUpdates(api, project, branch, readAt, jobName = "pages") {
|
|
347
347
|
const base = `/projects/${encodeURIComponent(project)}`;
|
|
348
348
|
async function schedules() {
|
|
349
349
|
return gitlabJson(api, `${base}/pipeline_schedules`);
|
|
@@ -397,7 +397,7 @@ function gitlabUpdates(api, project, branch, readAt) {
|
|
|
397
397
|
state: "running",
|
|
398
398
|
...run
|
|
399
399
|
};
|
|
400
|
-
const job = (await gitlabJson(api, `${base}/pipelines/${pipeline.id}/jobs?per_page=100`)).find((candidate) => candidate.name ===
|
|
400
|
+
const job = (await gitlabJson(api, `${base}/pipelines/${pipeline.id}/jobs?per_page=100`)).find((candidate) => candidate.name === jobName);
|
|
401
401
|
if (!job) continue;
|
|
402
402
|
const response = await gitlabRequest(api, `${base}/jobs/${job.id}/trace`, { allow: [403, 404] });
|
|
403
403
|
const result = response.ok ? findUpdateResult(await response.text()) : void 0;
|
|
@@ -424,9 +424,69 @@ function gitlabUpdates(api, project, branch, readAt) {
|
|
|
424
424
|
}
|
|
425
425
|
};
|
|
426
426
|
}
|
|
427
|
+
function noAccess(error) {
|
|
428
|
+
return error instanceof GitApiError && (error.status === 403 || error.status === 404);
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* The nightly update of a site whose content is kept in a project of its own,
|
|
432
|
+
* which runs in the code project's `update` job. The sign-in needs to see the
|
|
433
|
+
* code project too; where it can't, `setup()` says `no-access`.
|
|
434
|
+
*/
|
|
435
|
+
function gitlabCodeUpdates(options) {
|
|
436
|
+
const { api, project } = options;
|
|
437
|
+
let branch;
|
|
438
|
+
const liveBranch = () => {
|
|
439
|
+
branch ??= gitlabJson(api, `/projects/${encodeURIComponent(project)}`).then((found) => found.default_branch);
|
|
440
|
+
branch.catch(() => {
|
|
441
|
+
branch = void 0;
|
|
442
|
+
});
|
|
443
|
+
return branch;
|
|
444
|
+
};
|
|
445
|
+
/** The code project at its newest commit, read again each time, since updates change it. */
|
|
446
|
+
const files = async () => options.open(await liveBranch());
|
|
447
|
+
/** A file as it is now: its setup files are outside the folders backends list, so they're read at the newest commit. */
|
|
448
|
+
const readNow = async (path) => {
|
|
449
|
+
const backend = await files();
|
|
450
|
+
return backend.readAt(path, await backend.revision());
|
|
451
|
+
};
|
|
452
|
+
const updates = async () => gitlabUpdates(api, project, await liveBranch(), readNow, "update");
|
|
453
|
+
return {
|
|
454
|
+
code: {
|
|
455
|
+
repo: project,
|
|
456
|
+
webUrl: `${options.webUrl}/${project}`,
|
|
457
|
+
read: readNow,
|
|
458
|
+
history: async (path, history) => (await files()).history(path, history),
|
|
459
|
+
readAt: async (path, revision) => (await files()).readAt(path, revision)
|
|
460
|
+
},
|
|
461
|
+
async setup() {
|
|
462
|
+
try {
|
|
463
|
+
return await (await updates()).setup();
|
|
464
|
+
} catch (error) {
|
|
465
|
+
if (noAccess(error)) return "no-access";
|
|
466
|
+
throw error;
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
async enable() {
|
|
470
|
+
await (await updates()).enable?.();
|
|
471
|
+
},
|
|
472
|
+
async lastRun() {
|
|
473
|
+
try {
|
|
474
|
+
return await (await updates()).lastRun();
|
|
475
|
+
} catch (error) {
|
|
476
|
+
if (noAccess(error)) return void 0;
|
|
477
|
+
throw error;
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
async start(target) {
|
|
481
|
+
await (await updates()).start(target);
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
}
|
|
427
485
|
//#endregion
|
|
428
486
|
//#region src/backend.ts
|
|
429
487
|
const PAGE_SIZE = 100;
|
|
488
|
+
/** How many files one GraphQL request reads. */
|
|
489
|
+
const GRAPHQL_BATCH = 50;
|
|
430
490
|
/** The color and section codes in GitLab's job logs. */
|
|
431
491
|
const ANSI_CODES = new RegExp(`${String.fromCharCode(27)}\\[[\\d;]*[A-Za-z]`, "g");
|
|
432
492
|
/**
|
|
@@ -434,11 +494,15 @@ const ANSI_CODES = new RegExp(`${String.fromCharCode(27)}\\[[\\d;]*[A-Za-z]`, "g
|
|
|
434
494
|
* returned. Saves are single commits; each changed file carries the commit it
|
|
435
495
|
* was last changed in, so GitLab refuses to overwrite anyone else's edits.
|
|
436
496
|
*/
|
|
437
|
-
var GitLabBackend = class {
|
|
497
|
+
var GitLabBackend = class GitLabBackend {
|
|
438
498
|
user;
|
|
439
499
|
pages;
|
|
440
500
|
editors;
|
|
501
|
+
/** Absent where the project holds only content: the update job runs where the code is. */
|
|
441
502
|
updates;
|
|
503
|
+
drafts;
|
|
504
|
+
contentOnly;
|
|
505
|
+
options;
|
|
442
506
|
api;
|
|
443
507
|
project;
|
|
444
508
|
branch;
|
|
@@ -447,14 +511,93 @@ var GitLabBackend = class {
|
|
|
447
511
|
trees = /* @__PURE__ */ new Map();
|
|
448
512
|
blobs = /* @__PURE__ */ new Map();
|
|
449
513
|
constructor(options) {
|
|
514
|
+
this.options = options;
|
|
450
515
|
this.api = options.api;
|
|
451
516
|
this.project = encodeURIComponent(options.project);
|
|
452
517
|
this.branch = options.branch;
|
|
453
518
|
this.user = options.user;
|
|
454
519
|
this.onSignOut = options.onSignOut;
|
|
520
|
+
this.pinned = options.revision;
|
|
455
521
|
this.pages = gitlabPages(this.api, options.project);
|
|
456
522
|
this.editors = gitlabEditors(this.api, options.project, this.branch, this.user.login);
|
|
457
|
-
this.
|
|
523
|
+
this.contentOnly = options.contentOnly === true;
|
|
524
|
+
if (!this.contentOnly) this.updates = gitlabUpdates(this.api, options.project, this.branch, async (path) => this.readAt(path, this.pinned ?? await this.revision()));
|
|
525
|
+
else if (options.code) {
|
|
526
|
+
const code = options.code;
|
|
527
|
+
this.updates = gitlabCodeUpdates({
|
|
528
|
+
api: this.api,
|
|
529
|
+
project: code.project,
|
|
530
|
+
webUrl: code.webUrl,
|
|
531
|
+
open: (branch) => new GitLabBackend({
|
|
532
|
+
api: this.api,
|
|
533
|
+
project: code.project,
|
|
534
|
+
branch,
|
|
535
|
+
user: this.user,
|
|
536
|
+
onSignOut: () => {}
|
|
537
|
+
})
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
this.drafts = branchDrafts(this, this.draftHost());
|
|
541
|
+
}
|
|
542
|
+
/** Drafts on branches of the project, with merge requests for review. */
|
|
543
|
+
draftHost() {
|
|
544
|
+
const project = `/projects/${this.project}`;
|
|
545
|
+
return {
|
|
546
|
+
liveBranch: this.branch,
|
|
547
|
+
branchStore: (branch) => new GitLabBackend({
|
|
548
|
+
...this.options,
|
|
549
|
+
branch
|
|
550
|
+
}),
|
|
551
|
+
createBranch: async (branch, revision) => {
|
|
552
|
+
if ((await gitlabRequest(this.api, this.repo(`/branches?${new URLSearchParams({
|
|
553
|
+
branch,
|
|
554
|
+
ref: revision
|
|
555
|
+
})}`), {
|
|
556
|
+
method: "POST",
|
|
557
|
+
allow: [400]
|
|
558
|
+
})).status === 400) throw new DraftError("exists", `There's already a draft called ${branch}.`);
|
|
559
|
+
},
|
|
560
|
+
deleteBranch: async (branch) => {
|
|
561
|
+
await gitlabRequest(this.api, this.repo(`/branches/${encodeURIComponent(branch)}`), {
|
|
562
|
+
method: "DELETE",
|
|
563
|
+
allow: [404]
|
|
564
|
+
});
|
|
565
|
+
},
|
|
566
|
+
mergeBase: async (branch) => {
|
|
567
|
+
const query = new URLSearchParams();
|
|
568
|
+
query.append("refs[]", this.branch);
|
|
569
|
+
query.append("refs[]", branch);
|
|
570
|
+
const response = await gitlabRequest(this.api, this.repo(`/merge_base?${query}`), { allow: [400, 404] });
|
|
571
|
+
if (!response.ok) throw new DraftError("not-found", `There's no draft ${branch}.`);
|
|
572
|
+
return (await response.json()).id;
|
|
573
|
+
},
|
|
574
|
+
openReview: async ({ branch, title, description }) => {
|
|
575
|
+
const response = await gitlabRequest(this.api, `${project}/merge_requests`, {
|
|
576
|
+
method: "POST",
|
|
577
|
+
body: {
|
|
578
|
+
source_branch: branch,
|
|
579
|
+
target_branch: this.branch,
|
|
580
|
+
title,
|
|
581
|
+
description,
|
|
582
|
+
remove_source_branch: true
|
|
583
|
+
},
|
|
584
|
+
allow: [
|
|
585
|
+
403,
|
|
586
|
+
404,
|
|
587
|
+
409
|
|
588
|
+
]
|
|
589
|
+
});
|
|
590
|
+
if (response.ok) return { url: (await response.json()).web_url };
|
|
591
|
+
if (response.status !== 409) throw new DraftError("no-reviews", "This sign-in can't open merge requests on the site's project.");
|
|
592
|
+
const query = new URLSearchParams({
|
|
593
|
+
source_branch: branch,
|
|
594
|
+
state: "opened"
|
|
595
|
+
});
|
|
596
|
+
const [open] = await gitlabJson(this.api, `${project}/merge_requests?${query}`);
|
|
597
|
+
if (!open) throw new GitApiError(409, "GitLab couldn't open the merge request.");
|
|
598
|
+
return { url: open.web_url };
|
|
599
|
+
}
|
|
600
|
+
};
|
|
458
601
|
}
|
|
459
602
|
repo(path) {
|
|
460
603
|
return `/projects/${this.project}/repository${path}`;
|
|
@@ -506,6 +649,39 @@ var GitLabBackend = class {
|
|
|
506
649
|
}
|
|
507
650
|
return blob;
|
|
508
651
|
}
|
|
652
|
+
/** Reads many text files in a few GraphQL requests rather than one request each. */
|
|
653
|
+
async readFiles(paths) {
|
|
654
|
+
const tree = await this.currentTree();
|
|
655
|
+
const wanted = paths.filter((path) => tree.has(path));
|
|
656
|
+
const fetch = wanted.filter((path) => !this.blobs.has(tree.get(path) ?? ""));
|
|
657
|
+
const ref = this.pinned ?? await this.revision();
|
|
658
|
+
for (let start = 0; start < fetch.length; start += GRAPHQL_BATCH) {
|
|
659
|
+
const batch = fetch.slice(start, start + GRAPHQL_BATCH);
|
|
660
|
+
const response = await gitlabJson(this.api, "/../graphql", {
|
|
661
|
+
method: "POST",
|
|
662
|
+
body: {
|
|
663
|
+
query: `query ($project: ID!, $ref: String!, $paths: [String!]!) {
|
|
664
|
+
project(fullPath: $project) { repository { blobs(ref: $ref, paths: $paths) { nodes { path rawTextBlob } } } }
|
|
665
|
+
}`,
|
|
666
|
+
variables: {
|
|
667
|
+
project: decodeURIComponent(this.project),
|
|
668
|
+
ref,
|
|
669
|
+
paths: batch
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
for (const node of response.data?.project?.repository?.blobs?.nodes ?? []) {
|
|
674
|
+
const sha = tree.get(node.path);
|
|
675
|
+
if (sha && typeof node.rawTextBlob === "string") this.blobs.set(sha, Promise.resolve(node.rawTextBlob));
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
const result = /* @__PURE__ */ new Map();
|
|
679
|
+
for (const path of wanted) {
|
|
680
|
+
const text = await this.read(path);
|
|
681
|
+
if (text !== void 0) result.set(path, text);
|
|
682
|
+
}
|
|
683
|
+
return result;
|
|
684
|
+
}
|
|
509
685
|
async readBytes(path) {
|
|
510
686
|
const sha = (await this.currentTree()).get(path);
|
|
511
687
|
if (!sha) return void 0;
|
|
@@ -516,10 +692,37 @@ var GitLabBackend = class {
|
|
|
516
692
|
const prefix = `${dir}/`;
|
|
517
693
|
return [...(await this.currentTree()).keys()].filter((path) => path.startsWith(prefix)).sort();
|
|
518
694
|
}
|
|
695
|
+
/** GitLab's tree doesn't say how large files are, so they're asked for with GraphQL, in batches. */
|
|
696
|
+
async fileSizes(dir) {
|
|
697
|
+
const paths = await this.list(dir);
|
|
698
|
+
const ref = this.pinned ?? await this.revision();
|
|
699
|
+
const sizes = /* @__PURE__ */ new Map();
|
|
700
|
+
for (let start = 0; start < paths.length; start += GRAPHQL_BATCH) {
|
|
701
|
+
const response = await gitlabJson(this.api, "/../graphql", {
|
|
702
|
+
method: "POST",
|
|
703
|
+
body: {
|
|
704
|
+
query: `query ($project: ID!, $ref: String!, $paths: [String!]!) {
|
|
705
|
+
project(fullPath: $project) { repository { blobs(ref: $ref, paths: $paths) { nodes { path size } } } }
|
|
706
|
+
}`,
|
|
707
|
+
variables: {
|
|
708
|
+
project: decodeURIComponent(this.project),
|
|
709
|
+
ref,
|
|
710
|
+
paths: paths.slice(start, start + GRAPHQL_BATCH)
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
for (const node of response.data?.project?.repository?.blobs?.nodes ?? []) {
|
|
715
|
+
const size = Number(node.size);
|
|
716
|
+
if (Number.isFinite(size)) sizes.set(node.path, size);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return sizes;
|
|
720
|
+
}
|
|
519
721
|
async lastCommitFor(path, ref) {
|
|
520
722
|
return (await gitlabJson(this.api, this.repo(`/files/${encodeURIComponent(path)}?ref=${ref}`))).last_commit_id;
|
|
521
723
|
}
|
|
522
724
|
async write(changes, { message, expectedRevision }) {
|
|
725
|
+
if (this.contentOnly) assertContentChanges(changes);
|
|
523
726
|
if (await this.revision() !== expectedRevision) throw new ConflictError();
|
|
524
727
|
const existing = await this.tree(expectedRevision);
|
|
525
728
|
const toApply = (await Promise.all(changes.map(async (change) => {
|
|
@@ -551,7 +754,11 @@ var GitLabBackend = class {
|
|
|
551
754
|
body: {
|
|
552
755
|
branch: this.branch,
|
|
553
756
|
commit_message: message,
|
|
554
|
-
actions: toApply
|
|
757
|
+
actions: toApply,
|
|
758
|
+
...this.options.author && {
|
|
759
|
+
author_name: this.options.author.name,
|
|
760
|
+
author_email: this.options.author.email
|
|
761
|
+
}
|
|
555
762
|
}
|
|
556
763
|
});
|
|
557
764
|
this.pinned = commit.id;
|
|
@@ -595,7 +802,9 @@ var GitLabBackend = class {
|
|
|
595
802
|
*/
|
|
596
803
|
async deployStatus(revision) {
|
|
597
804
|
try {
|
|
598
|
-
const
|
|
805
|
+
const all = await gitlabJson(this.api, this.repo(`/commits/${revision}/statuses`));
|
|
806
|
+
const statuses = all.filter((status) => status.name !== REBUILD_JOB);
|
|
807
|
+
if (all.length > 0 && statuses.length === 0) return { state: "unknown" };
|
|
599
808
|
if (statuses.length === 0) return { state: (await gitlabJson(this.api, `/projects/${this.project}/pipelines?per_page=1`)).length > 0 ? "building" : "unknown" };
|
|
600
809
|
const failed = statuses.find((status) => status.status === "failed" || status.status === "canceled");
|
|
601
810
|
if (failed) {
|
|
@@ -738,6 +947,46 @@ function gitlabSetup(options = {}) {
|
|
|
738
947
|
async function isAvailable(owner, name) {
|
|
739
948
|
return (await gitlabRequest(api, `/projects/${encodeURIComponent(`${owner.path}/${name}`)}`, { allow: [404] })).status === 404;
|
|
740
949
|
}
|
|
950
|
+
/**
|
|
951
|
+
* Lets each project's pipelines reach the other, as their setup files say:
|
|
952
|
+
* a job token is refused by a project whose job token allowlist doesn't
|
|
953
|
+
* name its project, even a public one. Also lets the code project's job
|
|
954
|
+
* token publish updates, and runs pipelines each night where they need it.
|
|
955
|
+
* Returns whether it worked.
|
|
956
|
+
*/
|
|
957
|
+
async function connectProjects(link) {
|
|
958
|
+
const project = (path) => `/projects/${encodeURIComponent(path)}`;
|
|
959
|
+
try {
|
|
960
|
+
const ids = /* @__PURE__ */ new Map();
|
|
961
|
+
for (const which of ["code", "content"]) ids.set(which, (await gitlabJson(api, project(link[which]))).id);
|
|
962
|
+
for (const { reader, read } of link.links.reads) await gitlabRequest(api, `${project(link[read])}/job_token_scope/allowlist`, {
|
|
963
|
+
method: "POST",
|
|
964
|
+
body: { target_project_id: ids.get(reader) }
|
|
965
|
+
});
|
|
966
|
+
if (link.links.updates) await gitlabRequest(api, project(link[link.links.updates]), {
|
|
967
|
+
method: "PUT",
|
|
968
|
+
body: { ci_push_repository_for_job_token_allowed: true }
|
|
969
|
+
});
|
|
970
|
+
for (const which of link.links.nightly) await gitlabRequest(api, `${project(link[which])}/pipeline_schedules`, {
|
|
971
|
+
method: "POST",
|
|
972
|
+
body: {
|
|
973
|
+
description: which === link.links.updates ? "Rebuild the site and install Goodfellow's fixes each night" : "Rebuild the site each night",
|
|
974
|
+
ref: MAIN,
|
|
975
|
+
cron: NIGHTLY,
|
|
976
|
+
cron_timezone: "UTC",
|
|
977
|
+
active: true
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
return true;
|
|
981
|
+
} catch (error) {
|
|
982
|
+
if (error instanceof GitApiError && [
|
|
983
|
+
400,
|
|
984
|
+
403,
|
|
985
|
+
404
|
|
986
|
+
].includes(error.status)) return false;
|
|
987
|
+
throw error;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
741
990
|
return {
|
|
742
991
|
user: {
|
|
743
992
|
login: user.username,
|
|
@@ -775,6 +1024,11 @@ function gitlabSetup(options = {}) {
|
|
|
775
1024
|
if (error instanceof GitApiError && (error.status === 403 || error.status === 404)) throw new SetupError("not-allowed", `This sign-in can't create projects in ${site.owner.path}.`);
|
|
776
1025
|
throw error;
|
|
777
1026
|
}
|
|
1027
|
+
const warnings = [];
|
|
1028
|
+
if (site.link) {
|
|
1029
|
+
onStep?.("connect");
|
|
1030
|
+
if (!await connectProjects(site.link)) warnings.push("connect-unavailable");
|
|
1031
|
+
}
|
|
778
1032
|
onStep?.("files");
|
|
779
1033
|
await gitlabRequest(api, `/projects/${project.id}/repository/commits`, {
|
|
780
1034
|
method: "POST",
|
|
@@ -798,7 +1052,7 @@ function gitlabSetup(options = {}) {
|
|
|
798
1052
|
webUrl: project.web_url,
|
|
799
1053
|
buildsUrl: `${project.web_url}/-/pipelines`,
|
|
800
1054
|
pagesUrl: site.pages ? `${project.web_url}/pages` : void 0,
|
|
801
|
-
warnings
|
|
1055
|
+
warnings
|
|
802
1056
|
};
|
|
803
1057
|
}
|
|
804
1058
|
};
|
|
@@ -931,6 +1185,11 @@ function gitlab(options) {
|
|
|
931
1185
|
api,
|
|
932
1186
|
project: options.project,
|
|
933
1187
|
branch: options.branch ?? project.default_branch,
|
|
1188
|
+
...options.contentOnly && { contentOnly: true },
|
|
1189
|
+
...options.contentOnly && options.code && { code: {
|
|
1190
|
+
project: options.code,
|
|
1191
|
+
webUrl
|
|
1192
|
+
} },
|
|
934
1193
|
user: {
|
|
935
1194
|
login: user.username,
|
|
936
1195
|
name: user.name,
|
|
@@ -967,9 +1226,47 @@ function gitlab(options) {
|
|
|
967
1226
|
code_verifier: redirect.verifier
|
|
968
1227
|
}), redirect.verifier), redirect.remember);
|
|
969
1228
|
}
|
|
1229
|
+
/** The project through a token of the site's own, such as a project access token, on a full-stack site's server. */
|
|
1230
|
+
function server({ token }) {
|
|
1231
|
+
const api = {
|
|
1232
|
+
apiUrl,
|
|
1233
|
+
token: async () => typeof token === "string" ? token : token(),
|
|
1234
|
+
canRefresh: false,
|
|
1235
|
+
fetch: fetcher
|
|
1236
|
+
};
|
|
1237
|
+
let branch;
|
|
1238
|
+
const liveBranch = () => {
|
|
1239
|
+
branch ??= options.branch ? Promise.resolve(options.branch) : gitlabJson(api, `/projects/${encodeURIComponent(options.project)}`).then((project) => project.default_branch);
|
|
1240
|
+
branch.catch(() => {
|
|
1241
|
+
branch = void 0;
|
|
1242
|
+
});
|
|
1243
|
+
return branch;
|
|
1244
|
+
};
|
|
1245
|
+
const backend = async (revision, author) => new GitLabBackend({
|
|
1246
|
+
api,
|
|
1247
|
+
project: options.project,
|
|
1248
|
+
branch: await liveBranch(),
|
|
1249
|
+
...options.contentOnly && { contentOnly: true },
|
|
1250
|
+
user: {
|
|
1251
|
+
login: author?.name ?? options.project,
|
|
1252
|
+
...author && { name: author.name }
|
|
1253
|
+
},
|
|
1254
|
+
onSignOut: () => {},
|
|
1255
|
+
...revision && { revision },
|
|
1256
|
+
...author && { author }
|
|
1257
|
+
});
|
|
1258
|
+
return {
|
|
1259
|
+
...options.contentOnly && { contentOnly: true },
|
|
1260
|
+
async revision() {
|
|
1261
|
+
return (await backend()).revision();
|
|
1262
|
+
},
|
|
1263
|
+
at: backend
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
970
1266
|
return {
|
|
971
1267
|
name: "GitLab",
|
|
972
1268
|
canRedirect: Boolean(options.clientId),
|
|
1269
|
+
redirectAddressesListed: Boolean(options.clientId),
|
|
973
1270
|
tokenLinks: [{
|
|
974
1271
|
url: `${webUrl}/-/user_settings/personal_access_tokens?${new URLSearchParams({
|
|
975
1272
|
name: "Goodfellow admin",
|
|
@@ -1019,7 +1316,8 @@ function gitlab(options) {
|
|
|
1019
1316
|
code_challenge_method: "S256"
|
|
1020
1317
|
});
|
|
1021
1318
|
location().assign(`${webUrl}/oauth/authorize?${query}`);
|
|
1022
|
-
}
|
|
1319
|
+
},
|
|
1320
|
+
server
|
|
1023
1321
|
};
|
|
1024
1322
|
}
|
|
1025
1323
|
//#endregion
|
package/dist/testing.d.ts
CHANGED
|
@@ -21,6 +21,10 @@ export interface FakeGitLabOptions {
|
|
|
21
21
|
};
|
|
22
22
|
/** Simulates a GitLab Pages pipeline that succeeds after being checked this many times. Off by default. */
|
|
23
23
|
deployAfterChecks?: number;
|
|
24
|
+
/** The job each published commit's pipeline runs: `pages` by default. */
|
|
25
|
+
deployJob?: string;
|
|
26
|
+
/** The job the update runs in, which `addUpdateRun()` logs: `pages` by default, as in the starters, or a code project's `update`. */
|
|
27
|
+
updateJob?: string;
|
|
24
28
|
/** Groups the users belong to, which new projects can be created in. */
|
|
25
29
|
groups?: string[];
|
|
26
30
|
/** Publishes the site with GitLab Pages. Off by default. */
|
|
@@ -63,8 +67,20 @@ export interface FakeCreatedProject {
|
|
|
63
67
|
description?: string;
|
|
64
68
|
pagesAccessLevel?: string;
|
|
65
69
|
repo: FakeRepo;
|
|
70
|
+
/** The projects whose job tokens may reach this one (its job token allowlist), by id. */
|
|
71
|
+
jobTokenAllowlist: number[];
|
|
72
|
+
/** Whether its pipelines' job tokens may push to it. */
|
|
73
|
+
jobTokenPush: boolean;
|
|
74
|
+
schedules: Array<{
|
|
75
|
+
description: string;
|
|
76
|
+
ref: string;
|
|
77
|
+
cron: string;
|
|
78
|
+
active: boolean;
|
|
79
|
+
}>;
|
|
66
80
|
}
|
|
67
81
|
export declare function fakeGitLab(options: FakeGitLabOptions): {
|
|
82
|
+
/** How many GraphQL requests read files, as `readFiles()` makes. */
|
|
83
|
+
readonly graphqlReads: number;
|
|
68
84
|
/** The project's members by user id, invitations by email, and protected branches, which tests can change. */
|
|
69
85
|
members: Map<number, FakeMember>;
|
|
70
86
|
invitations: Map<string, number>;
|
|
@@ -121,8 +137,13 @@ export declare function fakeGitLab(options: FakeGitLabOptions): {
|
|
|
121
137
|
created: Map<string, FakeCreatedProject>;
|
|
122
138
|
/** The site's GitLab Pages, if it has them, which tests can change. */
|
|
123
139
|
pages: FakeGitLabPages | undefined;
|
|
140
|
+
/** Who commits that named their author did name, by commit. */
|
|
141
|
+
commitAuthors: Map<string, {
|
|
142
|
+
name: string;
|
|
143
|
+
email: string;
|
|
144
|
+
}>;
|
|
124
145
|
requests: Request[];
|
|
125
|
-
commit: (changes: FileChange[], message: string, author?: string) => string;
|
|
146
|
+
commit: (changes: FileChange[], message: string, author?: string, branch?: string) => string;
|
|
126
147
|
authorize: (authorizeUrl: string) => string;
|
|
127
148
|
handle: (request: Request) => Promise<Response>;
|
|
128
149
|
fetch: typeof fetch;
|
package/dist/testing.js
CHANGED
|
@@ -20,6 +20,8 @@ async function sha256Base64Url(text) {
|
|
|
20
20
|
function fakeGitLab(options) {
|
|
21
21
|
const webUrl = options.url ?? "https://gitlab.com";
|
|
22
22
|
const repo = new FakeRepo(options.files);
|
|
23
|
+
/** Who each commit that named its author did name, by commit. */
|
|
24
|
+
const commitAuthors = /* @__PURE__ */ new Map();
|
|
23
25
|
const tokens = new Map(Object.entries(options.tokens ?? { "test-token": {
|
|
24
26
|
username: "editor",
|
|
25
27
|
name: "Test Editor"
|
|
@@ -187,6 +189,7 @@ function fakeGitLab(options) {
|
|
|
187
189
|
const pipelineId = (sha) => [...pipelines.keys()].indexOf(sha) + 1;
|
|
188
190
|
const requests = [];
|
|
189
191
|
let counter = 0;
|
|
192
|
+
let graphqlReads = 0;
|
|
190
193
|
const created = /* @__PURE__ */ new Map();
|
|
191
194
|
const pages = options.pages ? {
|
|
192
195
|
uniqueDomain: options.pages.uniqueDomain ?? true,
|
|
@@ -296,7 +299,10 @@ function fakeGitLab(options) {
|
|
|
296
299
|
visibility: body.visibility,
|
|
297
300
|
description: body.description,
|
|
298
301
|
pagesAccessLevel: body.pages_access_level,
|
|
299
|
-
repo: new FakeRepo({})
|
|
302
|
+
repo: new FakeRepo({}),
|
|
303
|
+
jobTokenAllowlist: [],
|
|
304
|
+
jobTokenPush: false,
|
|
305
|
+
schedules: []
|
|
300
306
|
};
|
|
301
307
|
created.set(fullPath, project);
|
|
302
308
|
return json({
|
|
@@ -310,11 +316,40 @@ function fakeGitLab(options) {
|
|
|
310
316
|
const project = created.get(key) ?? [...created.values()].find((candidate) => String(candidate.id) === key);
|
|
311
317
|
if (!match || !project) return void 0;
|
|
312
318
|
const rest = match[2] ?? "";
|
|
313
|
-
if (rest === "") return json({
|
|
319
|
+
if (rest === "" && request.method === "GET") return json({
|
|
314
320
|
id: project.id,
|
|
315
321
|
path_with_namespace: project.path,
|
|
316
|
-
default_branch: "main"
|
|
322
|
+
default_branch: "main",
|
|
323
|
+
ci_push_repository_for_job_token_allowed: project.jobTokenPush
|
|
317
324
|
});
|
|
325
|
+
if (rest === "" && request.method === "PUT") {
|
|
326
|
+
const body = await request.json();
|
|
327
|
+
if (body.ci_push_repository_for_job_token_allowed !== void 0) project.jobTokenPush = body.ci_push_repository_for_job_token_allowed;
|
|
328
|
+
return json({
|
|
329
|
+
id: project.id,
|
|
330
|
+
ci_push_repository_for_job_token_allowed: project.jobTokenPush
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
if (rest === "/job_token_scope/allowlist" && request.method === "POST") {
|
|
334
|
+
const body = await request.json();
|
|
335
|
+
if (![...created.values()].some((candidate) => candidate.id === body.target_project_id)) return json({ message: "404 Project Not Found" }, 404);
|
|
336
|
+
if (!project.jobTokenAllowlist.includes(body.target_project_id)) project.jobTokenAllowlist.push(body.target_project_id);
|
|
337
|
+
return json({
|
|
338
|
+
source_project_id: project.id,
|
|
339
|
+
target_project_id: body.target_project_id
|
|
340
|
+
}, 201);
|
|
341
|
+
}
|
|
342
|
+
if (rest === "/pipeline_schedules" && request.method === "POST") {
|
|
343
|
+
const body = await request.json();
|
|
344
|
+
project.schedules.push({
|
|
345
|
+
...body,
|
|
346
|
+
active: body.active !== false
|
|
347
|
+
});
|
|
348
|
+
return json({
|
|
349
|
+
id: project.schedules.length,
|
|
350
|
+
...body
|
|
351
|
+
}, 201);
|
|
352
|
+
}
|
|
318
353
|
if (rest === "/repository/commits" && request.method === "POST") {
|
|
319
354
|
const body = await request.json();
|
|
320
355
|
return json({
|
|
@@ -330,9 +365,12 @@ function fakeGitLab(options) {
|
|
|
330
365
|
}
|
|
331
366
|
return json({ message: "404 Not Found" }, 404);
|
|
332
367
|
}
|
|
333
|
-
function commit(changes, message, author) {
|
|
334
|
-
const sha = repo.commit(changes, message, {
|
|
335
|
-
|
|
368
|
+
function commit(changes, message, author, branch = repo.defaultBranch) {
|
|
369
|
+
const sha = repo.commit(changes, message, {
|
|
370
|
+
author,
|
|
371
|
+
branch
|
|
372
|
+
});
|
|
373
|
+
if (options.deployAfterChecks !== void 0 && branch === repo.defaultBranch) pipelines.set(sha, { checks: 0 });
|
|
336
374
|
return sha;
|
|
337
375
|
}
|
|
338
376
|
function issueTokens(user) {
|
|
@@ -390,6 +428,22 @@ function fakeGitLab(options) {
|
|
|
390
428
|
}
|
|
391
429
|
return json({ error: "unsupported_grant_type" }, 400);
|
|
392
430
|
}
|
|
431
|
+
if (url.pathname === "/api/graphql" && request.method === "POST") {
|
|
432
|
+
if (!tokens.has(request.headers.get("authorization")?.replace(/^Bearer /, "") ?? "")) return json({ message: "401 Unauthorized" }, 401);
|
|
433
|
+
const { variables } = await request.json();
|
|
434
|
+
if (variables.project !== options.project) return json({ data: { project: null } });
|
|
435
|
+
graphqlReads += 1;
|
|
436
|
+
const files = repo.files(variables.ref);
|
|
437
|
+
return json({ data: { project: { repository: { blobs: { nodes: variables.paths.flatMap((path) => {
|
|
438
|
+
const content = files.get(path);
|
|
439
|
+
if (content === void 0) return [];
|
|
440
|
+
return [{
|
|
441
|
+
path,
|
|
442
|
+
rawTextBlob: typeof content === "string" ? content : null,
|
|
443
|
+
size: String(fakeFileBytes(content).length)
|
|
444
|
+
}];
|
|
445
|
+
}) } } } } });
|
|
446
|
+
}
|
|
393
447
|
if (!url.pathname.startsWith("/api/v4/")) return json({ message: "404 Not Found" }, 404);
|
|
394
448
|
const user = tokens.get(request.headers.get("authorization")?.replace(/^Bearer /, "") ?? "");
|
|
395
449
|
if (!user) return json({ message: "401 Unauthorized" }, 401);
|
|
@@ -401,6 +455,14 @@ function fakeGitLab(options) {
|
|
|
401
455
|
});
|
|
402
456
|
const setup = await handleCreated(request, path, user);
|
|
403
457
|
if (setup) return setup;
|
|
458
|
+
if (path === "/projects" && request.method === "GET") {
|
|
459
|
+
const level = members.get(accounts.get(user.username) ?? 0)?.accessLevel ?? 0;
|
|
460
|
+
const min = Number(url.searchParams.get("min_access_level") ?? "0");
|
|
461
|
+
return json(level > 0 && level >= min ? [{
|
|
462
|
+
path_with_namespace: options.project,
|
|
463
|
+
default_branch: repo.defaultBranch
|
|
464
|
+
}] : []);
|
|
465
|
+
}
|
|
404
466
|
if (path === "/users") {
|
|
405
467
|
const name = url.searchParams.get("username") ?? "";
|
|
406
468
|
const id = [...accounts].find(([username]) => username.toLowerCase() === name.toLowerCase())?.[1];
|
|
@@ -425,11 +487,55 @@ function fakeGitLab(options) {
|
|
|
425
487
|
},
|
|
426
488
|
ci_push_repository_for_job_token_allowed: updateSettings.jobTokenPush
|
|
427
489
|
});
|
|
490
|
+
if (rest === "/repository/branches" && request.method === "POST") {
|
|
491
|
+
if (accessLevel < 30) return json({ message: "403 Forbidden" }, 403);
|
|
492
|
+
const name = url.searchParams.get("branch") ?? "";
|
|
493
|
+
const ref = url.searchParams.get("ref") ?? "";
|
|
494
|
+
if (repo.branches.has(name)) return json({ message: "Branch already exists" }, 400);
|
|
495
|
+
const sha = repo.branches.get(ref) ?? (repo.commitAt(ref) ? ref : void 0);
|
|
496
|
+
if (!sha) return json({ message: "Invalid reference name" }, 400);
|
|
497
|
+
repo.branches.set(name, sha);
|
|
498
|
+
return json({
|
|
499
|
+
name,
|
|
500
|
+
commit: { id: sha }
|
|
501
|
+
}, 201);
|
|
502
|
+
}
|
|
428
503
|
const branch = rest.match(/^\/repository\/branches\/(.+)$/);
|
|
504
|
+
if (branch && request.method === "DELETE") {
|
|
505
|
+
if (accessLevel < 30) return json({ message: "403 Forbidden" }, 403);
|
|
506
|
+
const name = decodeURIComponent(branch[1] ?? "");
|
|
507
|
+
if (!repo.branches.has(name)) return json({ message: "404 Branch Not Found" }, 404);
|
|
508
|
+
repo.branches.delete(name);
|
|
509
|
+
return new Response(null, { status: 204 });
|
|
510
|
+
}
|
|
429
511
|
if (branch) {
|
|
430
512
|
const sha = repo.branches.get(decodeURIComponent(branch[1] ?? ""));
|
|
431
513
|
return sha ? json({ commit: { id: sha } }) : json({ message: "404 Branch Not Found" }, 404);
|
|
432
514
|
}
|
|
515
|
+
if (rest === "/repository/merge_base") {
|
|
516
|
+
const refs = url.searchParams.getAll("refs[]").map((ref) => repo.branches.get(ref) ?? ref);
|
|
517
|
+
if (refs.length !== 2 || refs.some((ref) => !repo.commitAt(ref))) return json({ message: "Could not find ref" }, 400);
|
|
518
|
+
const base = repo.mergeBase(refs[0] ?? "", refs[1] ?? "");
|
|
519
|
+
return base ? json({ id: base }) : json({ message: "404 Not Found" }, 404);
|
|
520
|
+
}
|
|
521
|
+
if (rest === "/merge_requests" && request.method === "POST") {
|
|
522
|
+
if (accessLevel < 30) return json({ message: "403 Forbidden" }, 403);
|
|
523
|
+
const body = await request.json();
|
|
524
|
+
if (!repo.branches.has(body.source_branch) || !repo.branches.has(body.target_branch)) return json({ message: ["Source branch does not exist"] }, 422);
|
|
525
|
+
if (repo.reviews.some((review) => review.branch === body.source_branch && review.state === "open")) return json({ message: ["Another open merge request already exists for this source branch"] }, 409);
|
|
526
|
+
const review = repo.openReview(body.source_branch, body.title, body.description ?? "", body.target_branch);
|
|
527
|
+
return json({
|
|
528
|
+
iid: review.number,
|
|
529
|
+
web_url: `${webUrl}/${options.project}/-/merge_requests/${review.number}`
|
|
530
|
+
}, 201);
|
|
531
|
+
}
|
|
532
|
+
if (rest === "/merge_requests" && request.method === "GET") {
|
|
533
|
+
const source = url.searchParams.get("source_branch");
|
|
534
|
+
return json(repo.reviews.filter((review) => review.state === "open" && (!source || review.branch === source)).map((review) => ({
|
|
535
|
+
iid: review.number,
|
|
536
|
+
web_url: `${webUrl}/${options.project}/-/merge_requests/${review.number}`
|
|
537
|
+
})));
|
|
538
|
+
}
|
|
433
539
|
if (rest === "/repository/tree") {
|
|
434
540
|
const ref = url.searchParams.get("ref") ?? repo.defaultBranch;
|
|
435
541
|
const dir = url.searchParams.get("path") ?? "";
|
|
@@ -477,17 +583,22 @@ function fakeGitLab(options) {
|
|
|
477
583
|
if ((action.action === "update" || action.action === "delete") && !files.has(action.file_path)) return json({ message: "A file with this name doesn't exist" }, 400);
|
|
478
584
|
if (action.last_commit_id && repo.lastCommitFor(action.file_path, body.branch) !== action.last_commit_id) return json({ message: "You are attempting to update a file that has changed since you started editing it." }, 400);
|
|
479
585
|
}
|
|
586
|
+
const sha = commit(body.actions.map((action) => action.action === "delete" ? {
|
|
587
|
+
path: action.file_path,
|
|
588
|
+
delete: true
|
|
589
|
+
} : action.encoding === "base64" ? {
|
|
590
|
+
path: action.file_path,
|
|
591
|
+
bytes: decodeBase64(action.content ?? "")
|
|
592
|
+
} : {
|
|
593
|
+
path: action.file_path,
|
|
594
|
+
content: action.content ?? ""
|
|
595
|
+
}), body.commit_message, body.author_name ?? user.name ?? user.username, body.branch);
|
|
596
|
+
if (body.author_name && body.author_email) commitAuthors.set(sha, {
|
|
597
|
+
name: body.author_name,
|
|
598
|
+
email: body.author_email
|
|
599
|
+
});
|
|
480
600
|
return json({
|
|
481
|
-
id:
|
|
482
|
-
path: action.file_path,
|
|
483
|
-
delete: true
|
|
484
|
-
} : action.encoding === "base64" ? {
|
|
485
|
-
path: action.file_path,
|
|
486
|
-
bytes: decodeBase64(action.content ?? "")
|
|
487
|
-
} : {
|
|
488
|
-
path: action.file_path,
|
|
489
|
-
content: action.content ?? ""
|
|
490
|
-
}), body.commit_message, user.name ?? user.username),
|
|
601
|
+
id: sha,
|
|
491
602
|
message: body.commit_message
|
|
492
603
|
}, 201);
|
|
493
604
|
}
|
|
@@ -524,9 +635,10 @@ function fakeGitLab(options) {
|
|
|
524
635
|
}]);
|
|
525
636
|
}
|
|
526
637
|
pipeline.checks += 1;
|
|
638
|
+
const done = pipeline.checks > (options.deployAfterChecks ?? 0);
|
|
527
639
|
return json([{
|
|
528
|
-
name: "pages",
|
|
529
|
-
status:
|
|
640
|
+
name: options.deployJob ?? "pages",
|
|
641
|
+
status: done ? "success" : "running",
|
|
530
642
|
target_url: `${webUrl}/${options.project}/-/jobs/1`
|
|
531
643
|
}]);
|
|
532
644
|
}
|
|
@@ -575,7 +687,7 @@ function fakeGitLab(options) {
|
|
|
575
687
|
const run = updateRuns.find((candidate) => candidate.id === Number(updateJobs[1]));
|
|
576
688
|
return run ? json([{
|
|
577
689
|
id: run.id,
|
|
578
|
-
name: "pages",
|
|
690
|
+
name: options.updateJob ?? "pages",
|
|
579
691
|
status: run.status
|
|
580
692
|
}]) : json({ message: "404 Not Found" }, 404);
|
|
581
693
|
}
|
|
@@ -617,6 +729,10 @@ function fakeGitLab(options) {
|
|
|
617
729
|
return json({ message: "404 Not Found" }, 404);
|
|
618
730
|
}
|
|
619
731
|
return {
|
|
732
|
+
/** How many GraphQL requests read files, as `readFiles()` makes. */
|
|
733
|
+
get graphqlReads() {
|
|
734
|
+
return graphqlReads;
|
|
735
|
+
},
|
|
620
736
|
/** The project's members by user id, invitations by email, and protected branches, which tests can change. */
|
|
621
737
|
members,
|
|
622
738
|
invitations,
|
|
@@ -631,6 +747,8 @@ function fakeGitLab(options) {
|
|
|
631
747
|
created,
|
|
632
748
|
/** The site's GitLab Pages, if it has them, which tests can change. */
|
|
633
749
|
pages,
|
|
750
|
+
/** Who commits that named their author did name, by commit. */
|
|
751
|
+
commitAuthors,
|
|
634
752
|
requests,
|
|
635
753
|
commit,
|
|
636
754
|
authorize,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodfellow-cms/gitlab",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Edit a Goodfellow site stored on GitLab, 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.4.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@puckeditor/core": "0.23.0"
|