@goodfellow-cms/gitlab 0.8.0 → 0.10.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.js +105 -25
- package/dist/testing.d.ts +5 -0
- package/dist/testing.js +37 -2
- package/package.json +7 -3
package/dist/index.js
CHANGED
|
@@ -143,6 +143,11 @@ function gitlabEditors(api, project, branch, username) {
|
|
|
143
143
|
await gitlabRequest(api, editor.id.startsWith(INVITATION) ? `${base}/invitations/${encodeURIComponent(editor.id.slice(11))}` : `${base}/members/${editor.id.slice(7)}`, { method: "DELETE" }).catch(notAllowed$2);
|
|
144
144
|
},
|
|
145
145
|
async letEditorsPublish() {
|
|
146
|
+
const read = async () => {
|
|
147
|
+
const answer = await gitlabRequest(api, protectedPath, { allow: [404] }).catch(notAllowed$2);
|
|
148
|
+
return answer.ok ? await answer.json() : void 0;
|
|
149
|
+
};
|
|
150
|
+
const developersMay = (protection) => !protection || ["push_access_levels", "merge_access_levels"].every((kind) => (protection[kind] ?? []).some((level) => level.access_level !== null && level.access_level <= DEVELOPER));
|
|
146
151
|
const protect = (level) => gitlabRequest(api, `${base}/protected_branches`, {
|
|
147
152
|
method: "POST",
|
|
148
153
|
body: {
|
|
@@ -152,6 +157,30 @@ function gitlabEditors(api, project, branch, username) {
|
|
|
152
157
|
allow_force_push: false
|
|
153
158
|
}
|
|
154
159
|
});
|
|
160
|
+
const current = await read();
|
|
161
|
+
if (!current) {
|
|
162
|
+
await protect(DEVELOPER).catch(notAllowed$2);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (developersMay(current)) return;
|
|
166
|
+
const swap = (levels = []) => [{ access_level: DEVELOPER }, ...levels.flatMap((level) => level.id === void 0 ? [] : [{
|
|
167
|
+
id: level.id,
|
|
168
|
+
_destroy: true
|
|
169
|
+
}])];
|
|
170
|
+
if ((await gitlabRequest(api, protectedPath, {
|
|
171
|
+
method: "PATCH",
|
|
172
|
+
body: {
|
|
173
|
+
allow_force_push: false,
|
|
174
|
+
allowed_to_push: swap(current.push_access_levels),
|
|
175
|
+
allowed_to_merge: swap(current.merge_access_levels)
|
|
176
|
+
},
|
|
177
|
+
allow: [
|
|
178
|
+
400,
|
|
179
|
+
404,
|
|
180
|
+
405,
|
|
181
|
+
422
|
|
182
|
+
]
|
|
183
|
+
}).catch(notAllowed$2)).ok && developersMay(await read())) return;
|
|
155
184
|
await gitlabRequest(api, protectedPath, {
|
|
156
185
|
method: "DELETE",
|
|
157
186
|
allow: [404]
|
|
@@ -162,6 +191,11 @@ function gitlabEditors(api, project, branch, username) {
|
|
|
162
191
|
await protect(MAINTAINER).catch(() => void 0);
|
|
163
192
|
notAllowed$2(error);
|
|
164
193
|
}
|
|
194
|
+
const after = await read();
|
|
195
|
+
if (!after || !developersMay(after)) {
|
|
196
|
+
if (!after) await protect(MAINTAINER).catch(() => void 0);
|
|
197
|
+
throw new Error("GitLab didn't keep the main branch's new protection. Try again.");
|
|
198
|
+
}
|
|
165
199
|
}
|
|
166
200
|
};
|
|
167
201
|
}
|
|
@@ -1334,27 +1368,77 @@ function gitlabSetup(options = {}) {
|
|
|
1334
1368
|
async function isAvailable(owner, name) {
|
|
1335
1369
|
return (await gitlabRequest(api, `/projects/${encodeURIComponent(`${owner.path}/${name}`)}`, { allow: [404] })).status === 404;
|
|
1336
1370
|
}
|
|
1371
|
+
const projectPath = (path) => `/projects/${encodeURIComponent(path)}`;
|
|
1372
|
+
/**
|
|
1373
|
+
* Makes one connection between a site's projects, giving what's left to do
|
|
1374
|
+
* by hand where GitLab refuses it, with what it answered.
|
|
1375
|
+
*/
|
|
1376
|
+
async function attempt(unmade, request, left) {
|
|
1377
|
+
try {
|
|
1378
|
+
await request();
|
|
1379
|
+
} catch (error) {
|
|
1380
|
+
if (!(error instanceof GitApiError)) throw error;
|
|
1381
|
+
left.push({
|
|
1382
|
+
...unmade,
|
|
1383
|
+
reason: error.message
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1337
1387
|
/**
|
|
1338
1388
|
* Lets each project's pipelines reach the other, as their setup files say:
|
|
1339
1389
|
* a job token is refused by a project whose job token allowlist doesn't
|
|
1340
1390
|
* name its project, even a public one. Also lets the code project's job
|
|
1341
|
-
* token publish updates
|
|
1342
|
-
*
|
|
1391
|
+
* token publish updates. Runs before the project's files are written,
|
|
1392
|
+
* since their first pipeline reads the other project. Each is tried on its
|
|
1393
|
+
* own, so one GitLab refuses doesn't keep the others from being made.
|
|
1343
1394
|
*/
|
|
1344
|
-
async function connectProjects(link) {
|
|
1345
|
-
const
|
|
1346
|
-
try {
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1395
|
+
async function connectProjects(link, left) {
|
|
1396
|
+
const ids = /* @__PURE__ */ new Map();
|
|
1397
|
+
for (const which of ["code", "content"]) try {
|
|
1398
|
+
ids.set(which, (await gitlabJson(api, projectPath(link[which]))).id);
|
|
1399
|
+
} catch (error) {
|
|
1400
|
+
if (!(error instanceof GitApiError)) throw error;
|
|
1401
|
+
}
|
|
1402
|
+
for (const { reader, read } of link.links.reads) {
|
|
1403
|
+
const target = ids.get(reader);
|
|
1404
|
+
await attempt({
|
|
1405
|
+
kind: "read",
|
|
1406
|
+
reader: link[reader],
|
|
1407
|
+
read: link[read],
|
|
1408
|
+
reason: ""
|
|
1409
|
+
}, async () => {
|
|
1410
|
+
if (target === void 0) throw new GitApiError(404, `GitLab couldn't find ${link[reader]}.`);
|
|
1411
|
+
await gitlabRequest(api, `${projectPath(link[read])}/job_token_scope/allowlist`, {
|
|
1412
|
+
method: "POST",
|
|
1413
|
+
body: { target_project_id: target }
|
|
1414
|
+
});
|
|
1415
|
+
}, left);
|
|
1416
|
+
}
|
|
1417
|
+
if (link.links.updates) {
|
|
1418
|
+
const repo = link[link.links.updates];
|
|
1419
|
+
await attempt({
|
|
1420
|
+
kind: "push",
|
|
1421
|
+
repo,
|
|
1422
|
+
reason: ""
|
|
1423
|
+
}, () => gitlabRequest(api, projectPath(repo), {
|
|
1354
1424
|
method: "PUT",
|
|
1355
1425
|
body: { ci_push_repository_for_job_token_allowed: true }
|
|
1356
|
-
});
|
|
1357
|
-
|
|
1426
|
+
}), left);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* Runs the pipelines that need it each night. After the project's files
|
|
1431
|
+
* are written, since a schedule names the main branch, which an empty
|
|
1432
|
+
* project doesn't have yet.
|
|
1433
|
+
*/
|
|
1434
|
+
async function scheduleNightly(link, left) {
|
|
1435
|
+
for (const which of link.links.nightly) {
|
|
1436
|
+
const repo = link[which];
|
|
1437
|
+
await attempt({
|
|
1438
|
+
kind: "nightly",
|
|
1439
|
+
repo,
|
|
1440
|
+
reason: ""
|
|
1441
|
+
}, () => gitlabRequest(api, `${projectPath(repo)}/pipeline_schedules`, {
|
|
1358
1442
|
method: "POST",
|
|
1359
1443
|
body: {
|
|
1360
1444
|
description: which === link.links.updates ? "Rebuild the site and install Goodfellow's fixes each night" : "Rebuild the site each night",
|
|
@@ -1363,15 +1447,7 @@ function gitlabSetup(options = {}) {
|
|
|
1363
1447
|
cron_timezone: "UTC",
|
|
1364
1448
|
active: true
|
|
1365
1449
|
}
|
|
1366
|
-
});
|
|
1367
|
-
return true;
|
|
1368
|
-
} catch (error) {
|
|
1369
|
-
if (error instanceof GitApiError && [
|
|
1370
|
-
400,
|
|
1371
|
-
403,
|
|
1372
|
-
404
|
|
1373
|
-
].includes(error.status)) return false;
|
|
1374
|
-
throw error;
|
|
1450
|
+
}), left);
|
|
1375
1451
|
}
|
|
1376
1452
|
}
|
|
1377
1453
|
return {
|
|
@@ -1412,9 +1488,10 @@ function gitlabSetup(options = {}) {
|
|
|
1412
1488
|
throw error;
|
|
1413
1489
|
}
|
|
1414
1490
|
const warnings = [];
|
|
1491
|
+
const unconnected = [];
|
|
1415
1492
|
if (site.link) {
|
|
1416
1493
|
onStep?.("connect");
|
|
1417
|
-
|
|
1494
|
+
await connectProjects(site.link, unconnected);
|
|
1418
1495
|
}
|
|
1419
1496
|
let siteUrl;
|
|
1420
1497
|
if (site.pages && site.domain) {
|
|
@@ -1457,13 +1534,16 @@ function gitlabSetup(options = {}) {
|
|
|
1457
1534
|
}])
|
|
1458
1535
|
}
|
|
1459
1536
|
});
|
|
1537
|
+
if (site.link) await scheduleNightly(site.link, unconnected);
|
|
1538
|
+
if (unconnected.length) warnings.push("connect-unavailable");
|
|
1460
1539
|
return {
|
|
1461
1540
|
repo: project.path_with_namespace,
|
|
1462
1541
|
webUrl: project.web_url,
|
|
1463
1542
|
...siteUrl && { siteUrl },
|
|
1464
1543
|
buildsUrl: `${project.web_url}/-/pipelines`,
|
|
1465
1544
|
pagesUrl: site.pages ? `${project.web_url}/pages` : void 0,
|
|
1466
|
-
warnings
|
|
1545
|
+
warnings,
|
|
1546
|
+
...unconnected.length && { unconnected }
|
|
1467
1547
|
};
|
|
1468
1548
|
}
|
|
1469
1549
|
};
|
package/dist/testing.d.ts
CHANGED
|
@@ -40,6 +40,8 @@ export interface FakeGitLabOptions {
|
|
|
40
40
|
};
|
|
41
41
|
/** Domains another GitLab Pages site already uses. */
|
|
42
42
|
takenDomains?: string[];
|
|
43
|
+
/** What connecting created projects GitLab refuses, as a plan or a sign-in might: job token allowlists, job token pushes or pipeline schedules. */
|
|
44
|
+
refuseConnections?: Array<"allowlist" | "push" | "schedule">;
|
|
43
45
|
/** Other GitLab accounts that exist, which can be added to the project. Every token's user exists and is a member. */
|
|
44
46
|
accounts?: string[];
|
|
45
47
|
/** Who can see the project. Private by default. */
|
|
@@ -50,6 +52,8 @@ export interface FakeGitLabOptions {
|
|
|
50
52
|
* artifact, or nothing for a job that fails. Without it, task pipelines fail.
|
|
51
53
|
*/
|
|
52
54
|
tasks?: (variables: Record<string, string>) => string | undefined | Promise<string | undefined>;
|
|
55
|
+
/** GitLab before 15.6, which can't change a protected branch's access levels, only remove and add it. */
|
|
56
|
+
noProtectionChanges?: boolean;
|
|
53
57
|
}
|
|
54
58
|
/** A pipeline schedule, such as the nightly one or one the admin panel adds for a scheduled change. */
|
|
55
59
|
export interface FakeSchedule {
|
|
@@ -120,6 +124,7 @@ export declare function fakeGitLab(options: FakeGitLabOptions): {
|
|
|
120
124
|
merge: number;
|
|
121
125
|
forcePush: boolean;
|
|
122
126
|
}>;
|
|
127
|
+
protectionChanges: string[];
|
|
123
128
|
failBuild: (sha: string, failure?: {
|
|
124
129
|
job?: string;
|
|
125
130
|
failureReason?: string;
|
package/dist/testing.js
CHANGED
|
@@ -53,6 +53,8 @@ function fakeGitLab(options) {
|
|
|
53
53
|
merge: 40,
|
|
54
54
|
forcePush: false
|
|
55
55
|
}]]);
|
|
56
|
+
/** Each change to a protected branch, in order: `DELETE main`, `POST main`, `PATCH main`. */
|
|
57
|
+
const protectionChanges = [];
|
|
56
58
|
/** The project's members and invitations, and its protected branches. */
|
|
57
59
|
async function handleMembers(request, url, rest, accessLevel) {
|
|
58
60
|
const forbidden = () => json({ message: "403 Forbidden" }, 403);
|
|
@@ -134,20 +136,48 @@ function fakeGitLab(options) {
|
|
|
134
136
|
if (!found) return json({ message: "404 Not found" }, 404);
|
|
135
137
|
if (request.method === "GET") return json({
|
|
136
138
|
name,
|
|
137
|
-
push_access_levels: [{
|
|
138
|
-
|
|
139
|
+
push_access_levels: [{
|
|
140
|
+
id: 1,
|
|
141
|
+
access_level: found.push
|
|
142
|
+
}],
|
|
143
|
+
merge_access_levels: [{
|
|
144
|
+
id: 2,
|
|
145
|
+
access_level: found.merge
|
|
146
|
+
}],
|
|
139
147
|
allow_force_push: found.forcePush
|
|
140
148
|
});
|
|
141
149
|
if (request.method === "DELETE") {
|
|
142
150
|
if (accessLevel < 40) return forbidden();
|
|
143
151
|
protectedBranches.delete(name);
|
|
152
|
+
protectionChanges.push(`DELETE ${name}`);
|
|
144
153
|
return new Response(null, { status: 204 });
|
|
145
154
|
}
|
|
155
|
+
if (request.method === "PATCH" && !options.noProtectionChanges) {
|
|
156
|
+
if (accessLevel < 40) return forbidden();
|
|
157
|
+
const body = await request.json();
|
|
158
|
+
const levelFrom = (levels, id, now) => {
|
|
159
|
+
if (!levels) return now;
|
|
160
|
+
const kept = levels.some((level) => level.id === id && level._destroy) ? [] : [now];
|
|
161
|
+
const added = levels.flatMap((level) => level.access_level === void 0 ? [] : [level.access_level]);
|
|
162
|
+
return kept.length + added.length === 1 ? kept[0] ?? added[0] ?? now : void 0;
|
|
163
|
+
};
|
|
164
|
+
const push = levelFrom(body.allowed_to_push, 1, found.push);
|
|
165
|
+
const merge = levelFrom(body.allowed_to_merge, 2, found.merge);
|
|
166
|
+
if (push === void 0 || merge === void 0) return json({ message: "Only one access level allowed" }, 422);
|
|
167
|
+
protectedBranches.set(name, {
|
|
168
|
+
push,
|
|
169
|
+
merge,
|
|
170
|
+
forcePush: body.allow_force_push ?? found.forcePush
|
|
171
|
+
});
|
|
172
|
+
protectionChanges.push(`PATCH ${name}`);
|
|
173
|
+
return json({ name });
|
|
174
|
+
}
|
|
146
175
|
}
|
|
147
176
|
if (rest === "/protected_branches" && request.method === "POST") {
|
|
148
177
|
if (accessLevel < 40) return forbidden();
|
|
149
178
|
const body = await request.json();
|
|
150
179
|
if (protectedBranches.has(body.name)) return json({ message: "Protected branch already exists" }, 409);
|
|
180
|
+
protectionChanges.push(`POST ${body.name}`);
|
|
151
181
|
protectedBranches.set(body.name, {
|
|
152
182
|
push: body.push_access_level ?? 40,
|
|
153
183
|
merge: body.merge_access_level ?? 40,
|
|
@@ -345,6 +375,7 @@ function fakeGitLab(options) {
|
|
|
345
375
|
});
|
|
346
376
|
if (rest === "" && request.method === "PUT") {
|
|
347
377
|
const body = await request.json();
|
|
378
|
+
if (options.refuseConnections?.includes("push")) return json({ message: "403 Forbidden" }, 403);
|
|
348
379
|
if (body.ci_push_repository_for_job_token_allowed !== void 0) project.jobTokenPush = body.ci_push_repository_for_job_token_allowed;
|
|
349
380
|
return json({
|
|
350
381
|
id: project.id,
|
|
@@ -353,6 +384,7 @@ function fakeGitLab(options) {
|
|
|
353
384
|
}
|
|
354
385
|
if (rest === "/job_token_scope/allowlist" && request.method === "POST") {
|
|
355
386
|
const body = await request.json();
|
|
387
|
+
if (options.refuseConnections?.includes("allowlist")) return json({ message: "403 Forbidden" }, 403);
|
|
356
388
|
if (![...created.values()].some((candidate) => candidate.id === body.target_project_id)) return json({ message: "404 Project Not Found" }, 404);
|
|
357
389
|
if (!project.jobTokenAllowlist.includes(body.target_project_id)) project.jobTokenAllowlist.push(body.target_project_id);
|
|
358
390
|
return json({
|
|
@@ -371,6 +403,8 @@ function fakeGitLab(options) {
|
|
|
371
403
|
}
|
|
372
404
|
if (rest === "/pipeline_schedules" && request.method === "POST") {
|
|
373
405
|
const body = await request.json();
|
|
406
|
+
if (options.refuseConnections?.includes("schedule")) return json({ message: { ref: ["is ambiguous"] } }, 400);
|
|
407
|
+
if (project.repo.files().size === 0) return json({ message: { ref: ["is ambiguous"] } }, 400);
|
|
374
408
|
project.schedules.push({
|
|
375
409
|
...body,
|
|
376
410
|
active: body.active !== false
|
|
@@ -898,6 +932,7 @@ function fakeGitLab(options) {
|
|
|
898
932
|
members,
|
|
899
933
|
invitations,
|
|
900
934
|
protectedBranches,
|
|
935
|
+
protectionChanges,
|
|
901
936
|
failBuild,
|
|
902
937
|
cancelBuild,
|
|
903
938
|
/** The project's update settings, and the pipelines the update ran in, which tests add with `addUpdateRun()`. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodfellow-cms/gitlab",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Edit a Goodfellow site stored on GitLab, straight from the browser.",
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "Edit a Goodfellow CMS site stored on GitLab, straight from the browser.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"goodfellow",
|
|
7
7
|
"cms",
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
],
|
|
13
13
|
"homepage": "https://goodfellow-cms.github.io/goodfellow-cms/docs/hosts",
|
|
14
14
|
"bugs": "https://github.com/goodfellow-cms/goodfellow-cms/issues",
|
|
15
|
+
"funding": {
|
|
16
|
+
"type": "github",
|
|
17
|
+
"url": "https://github.com/sponsors/goodfellow-cms"
|
|
18
|
+
},
|
|
15
19
|
"license": "MIT",
|
|
16
20
|
"repository": {
|
|
17
21
|
"type": "git",
|
|
@@ -35,7 +39,7 @@
|
|
|
35
39
|
"dist"
|
|
36
40
|
],
|
|
37
41
|
"dependencies": {
|
|
38
|
-
"@goodfellow-cms/core": "0.
|
|
42
|
+
"@goodfellow-cms/core": "0.10.0"
|
|
39
43
|
},
|
|
40
44
|
"devDependencies": {
|
|
41
45
|
"@puckeditor/core": "0.23.0"
|