@simplysf/simply-gitlab-core 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.
@@ -0,0 +1,342 @@
1
+ /*
2
+ * Copyright (c) 2026, SimplySF.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import { ConfigError } from './errors.js';
17
+ import { readTextFile } from './json-input.js';
18
+ /** The list sections, in the order `apply` reconciles them. */
19
+ export const LIST_SECTIONS = ['protectedBranches', 'protectedTags', 'approvalRules', 'variables'];
20
+ /** The object sections, which merge key by key rather than by entry. */
21
+ export const OBJECT_SECTIONS = ['settings', 'approvals', 'pushRules'];
22
+ /**
23
+ * What identifies an entry within its section, for merging an override onto a baseline and for
24
+ * matching a declared entry against one that already exists on the instance.
25
+ *
26
+ * Every section has a natural key, which is what makes a two-line override possible: restating a
27
+ * whole array to change one number is how these files rot.
28
+ */
29
+ const SECTION_KEYS = {
30
+ protectedBranches: ['name'],
31
+ protectedTags: ['name'],
32
+ approvalRules: ['name'],
33
+ variables: ['key', 'environment_scope'],
34
+ };
35
+ /** GitLab treats an unset environment scope as `*`, so the merge key has to agree with it. */
36
+ const SCOPE_DEFAULTS = { environment_scope: '*' };
37
+ /**
38
+ * Renders a key field as text.
39
+ *
40
+ * Key fields come out of parsed JSON, so they are `unknown` until proven otherwise. Coercing an
41
+ * object with `String()` would key every such entry as `[object Object]` and silently collide
42
+ * entries that are not the same thing at all.
43
+ */
44
+ function keyText(value) {
45
+ if (value === undefined || value === null)
46
+ return '';
47
+ if (typeof value === 'string')
48
+ return value;
49
+ if (typeof value === 'number' || typeof value === 'boolean')
50
+ return String(value);
51
+ return JSON.stringify(value) ?? '';
52
+ }
53
+ /** The identity of one entry, as a string, for keying a map. */
54
+ export function entryKey(section, entry) {
55
+ return SECTION_KEYS[section].map((field) => keyText(entry[field] ?? SCOPE_DEFAULTS[field])).join(' ');
56
+ }
57
+ /** The human-readable identity of one entry, for plan output. */
58
+ export function entryLabel(section, entry) {
59
+ return SECTION_KEYS[section]
60
+ .map((field) => keyText(entry[field] ?? SCOPE_DEFAULTS[field]))
61
+ .filter((part) => part !== '')
62
+ .join(' @ ');
63
+ }
64
+ function isPlainObject(value) {
65
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
66
+ }
67
+ export function normalizeSection(section) {
68
+ if (section === undefined)
69
+ return undefined;
70
+ if (Array.isArray(section))
71
+ return { prune: false, replace: false, entries: section };
72
+ const object = section;
73
+ return { prune: object.prune === true, replace: object.$replace === true, entries: object.entries };
74
+ }
75
+ // --- Validation ---------------------------------------------------------------------------------
76
+ /**
77
+ * Validates the parsed file, reporting the JSON path of whatever is wrong.
78
+ *
79
+ * Hand-written rather than schema-driven because the error is the product: a caller who wrote
80
+ * `"targets": {"group": "x"}` needs to be told that the key is `groups` and that it takes an array,
81
+ * at that path, not that the document failed to match a schema. The published JSON Schema gives an
82
+ * editor completion and hover text; this is what runs.
83
+ */
84
+ export function validateBaselineConfig(value, label = 'config') {
85
+ const problems = [];
86
+ const at = (path, message) => {
87
+ problems.push(`${path}: ${message}`);
88
+ };
89
+ if (!isPlainObject(value)) {
90
+ throw new ConfigError(`${label} must be a JSON object.`);
91
+ }
92
+ if (value.version !== 1) {
93
+ at('version', `must be 1, got ${JSON.stringify(value.version)}`);
94
+ }
95
+ const known = new Set(['$schema', 'version', 'targets', 'group', 'project', 'overrides']);
96
+ for (const key of Object.keys(value)) {
97
+ if (!known.has(key))
98
+ at(key, `is not a known top-level key (expected one of ${[...known].join(', ')})`);
99
+ }
100
+ if (value.targets !== undefined)
101
+ validateTargets(value.targets, 'targets', at);
102
+ if (value.group !== undefined)
103
+ validateGroupDesired(value.group, 'group', at);
104
+ if (value.project !== undefined)
105
+ validateProjectDesired(value.project, 'project', at);
106
+ if (value.overrides !== undefined) {
107
+ if (!isPlainObject(value.overrides)) {
108
+ at('overrides', 'must be an object keyed by project or group path');
109
+ }
110
+ else {
111
+ for (const [path, override] of Object.entries(value.overrides)) {
112
+ const where = `overrides["${path}"]`;
113
+ if (!isPlainObject(override)) {
114
+ at(where, 'must be an object');
115
+ continue;
116
+ }
117
+ for (const key of Object.keys(override)) {
118
+ if (key !== 'project' && key !== 'group')
119
+ at(`${where}.${key}`, 'must be "project" or "group"');
120
+ }
121
+ if (override.project !== undefined)
122
+ validateProjectDesired(override.project, `${where}.project`, at);
123
+ if (override.group !== undefined)
124
+ validateGroupDesired(override.group, `${where}.group`, at);
125
+ }
126
+ }
127
+ }
128
+ if (problems.length > 0) {
129
+ // Every problem at once. Fixing a config one error per run, fifty projects into a change, is
130
+ // the kind of loop that makes people stop using the tool.
131
+ throw new ConfigError(`${label} is not valid:\n ${problems.join('\n ')}`);
132
+ }
133
+ return value;
134
+ }
135
+ function validateStringArray(value, path, at) {
136
+ if (!Array.isArray(value)) {
137
+ at(path, 'must be an array of strings');
138
+ return;
139
+ }
140
+ for (const [index, item] of value.entries()) {
141
+ if (typeof item !== 'string' || item.trim() === '') {
142
+ at(`${path}[${index}]`, `must be a non-empty string, got ${JSON.stringify(item)}`);
143
+ }
144
+ }
145
+ }
146
+ function validateTargets(value, path, at) {
147
+ if (!isPlainObject(value)) {
148
+ at(path, 'must be an object');
149
+ return;
150
+ }
151
+ const known = new Set(['groups', 'projects', 'exclude']);
152
+ for (const key of Object.keys(value)) {
153
+ if (!known.has(key))
154
+ at(`${path}.${key}`, `is not a known key (expected ${[...known].join(', ')})`);
155
+ }
156
+ for (const key of known) {
157
+ if (value[key] !== undefined)
158
+ validateStringArray(value[key], `${path}.${key}`, at);
159
+ }
160
+ if (value.groups === undefined && value.projects === undefined) {
161
+ at(path, 'must name at least one of "groups" or "projects"');
162
+ }
163
+ }
164
+ function validateGroupDesired(value, path, at) {
165
+ if (!isPlainObject(value)) {
166
+ at(path, 'must be an object');
167
+ return;
168
+ }
169
+ for (const key of Object.keys(value)) {
170
+ if (key !== 'settings')
171
+ at(`${path}.${key}`, 'is not a known key (expected "settings")');
172
+ }
173
+ if (value.settings !== undefined && !isPlainObject(value.settings)) {
174
+ at(`${path}.settings`, 'must be an object of GitLab group attributes');
175
+ }
176
+ }
177
+ function validateProjectDesired(value, path, at) {
178
+ if (!isPlainObject(value)) {
179
+ at(path, 'must be an object');
180
+ return;
181
+ }
182
+ const known = new Set([...OBJECT_SECTIONS, ...LIST_SECTIONS]);
183
+ for (const key of Object.keys(value)) {
184
+ if (!known.has(key))
185
+ at(`${path}.${key}`, `is not a known section (expected ${[...known].join(', ')})`);
186
+ }
187
+ for (const section of OBJECT_SECTIONS) {
188
+ const entry = value[section];
189
+ if (entry !== undefined && !isPlainObject(entry)) {
190
+ at(`${path}.${section}`, 'must be an object of GitLab attributes');
191
+ }
192
+ }
193
+ for (const section of LIST_SECTIONS) {
194
+ if (value[section] !== undefined)
195
+ validateListSection(value[section], `${path}.${section}`, section, at);
196
+ }
197
+ }
198
+ function validateListSection(value, path, section, at) {
199
+ let entries;
200
+ if (Array.isArray(value)) {
201
+ entries = value;
202
+ }
203
+ else if (isPlainObject(value)) {
204
+ const known = new Set(['prune', '$replace', 'entries']);
205
+ for (const key of Object.keys(value)) {
206
+ if (!known.has(key))
207
+ at(`${path}.${key}`, `is not a known key (expected ${[...known].join(', ')})`);
208
+ }
209
+ for (const flag of ['prune', '$replace']) {
210
+ if (value[flag] !== undefined && typeof value[flag] !== 'boolean')
211
+ at(`${path}.${flag}`, 'must be a boolean');
212
+ }
213
+ if (value.entries === undefined) {
214
+ at(`${path}.entries`, 'is required when the section is written as an object');
215
+ return;
216
+ }
217
+ entries = value.entries;
218
+ }
219
+ else {
220
+ at(path, 'must be an array of entries, or an object with an "entries" array');
221
+ return;
222
+ }
223
+ if (!Array.isArray(entries)) {
224
+ at(Array.isArray(value) ? path : `${path}.entries`, 'must be an array');
225
+ return;
226
+ }
227
+ const base = Array.isArray(value) ? path : `${path}.entries`;
228
+ const seen = new Map();
229
+ for (const [index, entry] of entries.entries()) {
230
+ const where = `${base}[${index}]`;
231
+ if (!isPlainObject(entry)) {
232
+ at(where, 'must be an object');
233
+ continue;
234
+ }
235
+ for (const field of SECTION_KEYS[section]) {
236
+ // Only the first key field is required; `environment_scope` defaults to `*`, as GitLab does.
237
+ if (field !== SECTION_KEYS[section][0])
238
+ continue;
239
+ if (typeof entry[field] !== 'string' || entry[field].trim() === '') {
240
+ at(`${where}.${field}`, `is required and must be a non-empty string`);
241
+ }
242
+ }
243
+ const key = entryKey(section, entry);
244
+ const first = seen.get(key);
245
+ if (first !== undefined) {
246
+ // Two entries with the same identity would silently reconcile against each other.
247
+ at(where, `duplicates ${base}[${first}] (same ${SECTION_KEYS[section].join(' + ')})`);
248
+ }
249
+ else {
250
+ seen.set(key, index);
251
+ }
252
+ }
253
+ }
254
+ // --- Merging ------------------------------------------------------------------------------------
255
+ /**
256
+ * The effective configuration for one project: the baseline with its override merged over it.
257
+ *
258
+ * Objects merge key by key. Lists merge by the section's natural key, so an override adjusts one
259
+ * entry rather than restating the array — unless it sets `$replace`, which swaps the list wholesale
260
+ * for the project that genuinely needs different rules rather than adjusted ones.
261
+ */
262
+ export function mergeProjectDesired(base = {}, override = {}) {
263
+ const merged = {};
264
+ for (const section of OBJECT_SECTIONS) {
265
+ const value = mergeObjects(base[section], override[section]);
266
+ if (value !== undefined)
267
+ merged[section] = value;
268
+ }
269
+ for (const section of LIST_SECTIONS) {
270
+ const value = mergeSections(section, base[section], override[section]);
271
+ if (value !== undefined)
272
+ merged[section] = value;
273
+ }
274
+ return merged;
275
+ }
276
+ export function mergeGroupDesired(base = {}, override = {}) {
277
+ const settings = mergeObjects(base.settings, override.settings);
278
+ return settings === undefined ? {} : { settings };
279
+ }
280
+ function mergeObjects(base, override) {
281
+ if (base === undefined && override === undefined)
282
+ return undefined;
283
+ return { ...(base ?? {}), ...(override ?? {}) };
284
+ }
285
+ function mergeSections(section, base, override) {
286
+ const left = normalizeSection(base);
287
+ const right = normalizeSection(override);
288
+ if (left === undefined && right === undefined)
289
+ return undefined;
290
+ if (left === undefined)
291
+ return { prune: right.prune, entries: right.entries };
292
+ if (right === undefined)
293
+ return { prune: left.prune, entries: left.entries };
294
+ // Pruning is a property of the section as configured, so an override can turn it on for one
295
+ // project without the baseline turning it on for all fifty.
296
+ if (right.replace)
297
+ return { prune: right.prune, entries: right.entries };
298
+ const byKey = new Map();
299
+ const order = [];
300
+ for (const entry of left.entries) {
301
+ const key = entryKey(section, entry);
302
+ if (!byKey.has(key))
303
+ order.push(key);
304
+ byKey.set(key, entry);
305
+ }
306
+ for (const entry of right.entries) {
307
+ const key = entryKey(section, entry);
308
+ const existing = byKey.get(key);
309
+ if (existing === undefined)
310
+ order.push(key);
311
+ byKey.set(key, existing === undefined ? entry : { ...existing, ...entry });
312
+ }
313
+ return { prune: right.prune || left.prune, entries: order.map((key) => byKey.get(key)) };
314
+ }
315
+ /** The effective project configuration for one path, baseline merged with any override for it. */
316
+ export function effectiveProject(config, path) {
317
+ return mergeProjectDesired(config.project, config.overrides?.[path]?.project);
318
+ }
319
+ /** The effective group configuration for one path. */
320
+ export function effectiveGroup(config, path) {
321
+ return mergeGroupDesired(config.group, config.overrides?.[path]?.group);
322
+ }
323
+ /**
324
+ * Reads and validates a baseline config file.
325
+ *
326
+ * A parse failure reports only the position, never the parser's snippet of the content — the same
327
+ * rule the rest of this package follows, because a caller can name any path and the snippet would
328
+ * echo the first bytes of whatever file that was.
329
+ */
330
+ export function loadBaselineConfig(path) {
331
+ const source = readTextFile(path, 'Config file');
332
+ let parsed;
333
+ try {
334
+ parsed = JSON.parse(source);
335
+ }
336
+ catch (error) {
337
+ const position = error instanceof Error ? /position \d+(?::? line \d+ column \d+)?/.exec(error.message)?.[0] : undefined;
338
+ throw new ConfigError(`${path} is not valid JSON${position === undefined ? '' : ` at ${position}`}.`);
339
+ }
340
+ return validateBaselineConfig(parsed, path);
341
+ }
342
+ //# sourceMappingURL=baseline-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline-config.js","sourceRoot":"","sources":["../src/baseline-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAuE/C,+DAA+D;AAC/D,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,mBAAmB,EAAE,eAAe,EAAE,eAAe,EAAE,WAAW,CAAU,CAAC;AAG3G,wEAAwE;AACxE,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,UAAU,EAAE,WAAW,EAAE,WAAW,CAAU,CAAC;AAE/E;;;;;;GAMG;AACH,MAAM,YAAY,GAAqD;IACrE,iBAAiB,EAAE,CAAC,MAAM,CAAC;IAC3B,aAAa,EAAE,CAAC,MAAM,CAAC;IACvB,aAAa,EAAE,CAAC,MAAM,CAAC;IACvB,SAAS,EAAE,CAAC,KAAK,EAAE,mBAAmB,CAAC;CACxC,CAAC;AAEF,8FAA8F;AAC9F,MAAM,cAAc,GAAqC,EAAE,iBAAiB,EAAE,GAAG,EAAE,CAAC;AAEpF;;;;;;GAMG;AACH,SAAS,OAAO,CAAC,KAAc;IAC7B,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,EAAE,CAAC;IACrD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IAClF,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;AACrC,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,QAAQ,CAAC,OAAoB,EAAE,KAAY;IACzD,OAAO,YAAY,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACxG,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,UAAU,CAAC,OAAoB,EAAE,KAAY;IAC3D,OAAO,YAAY,CAAC,OAAO,CAAC;SACzB,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;SAC9D,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC;SAC7B,IAAI,CAAC,KAAK,CAAC,CAAC;AACjB,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,OAA4B;IAC3D,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC5C,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;IACtF,MAAM,MAAM,GAAG,OAAwB,CAAC;IACxC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,KAAK,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,QAAQ,KAAK,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC;AACtG,CAAC;AAED,mGAAmG;AAEnG;;;;;;;GAOG;AACH,MAAM,UAAU,sBAAsB,CAAC,KAAc,EAAE,KAAK,GAAG,QAAQ;IACrE,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,EAAE,GAAG,CAAC,IAAY,EAAE,OAAe,EAAQ,EAAE;QACjD,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,KAAK,OAAO,EAAE,CAAC,CAAC;IACvC,CAAC,CAAC;IAEF,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,WAAW,CAAC,GAAG,KAAK,yBAAyB,CAAC,CAAC;IAC3D,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,KAAK,CAAC,EAAE,CAAC;QACxB,EAAE,CAAC,SAAS,EAAE,kBAAkB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACnE,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC,CAAC;IAC1F,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACrC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,EAAE,CAAC,GAAG,EAAE,iDAAiD,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC1G,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS;QAAE,eAAe,CAAC,KAAK,CAAC,OAAO,EAAE,SAAS,EAAE,EAAE,CAAC,CAAC;IAC/E,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;QAAE,oBAAoB,CAAC,KAAK,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC;IAC9E,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS;QAAE,sBAAsB,CAAC,KAAK,CAAC,OAAO,EAAE,SAAS,EAAE,EAAE,CAAC,CAAC;IAEtF,IAAI,KAAK,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;QAClC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC;YACpC,EAAE,CAAC,WAAW,EAAE,kDAAkD,CAAC,CAAC;QACtE,CAAC;aAAM,CAAC;YACN,KAAK,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC;gBAC/D,MAAM,KAAK,GAAG,cAAc,IAAI,IAAI,CAAC;gBACrC,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,EAAE,CAAC;oBAC7B,EAAE,CAAC,KAAK,EAAE,mBAAmB,CAAC,CAAC;oBAC/B,SAAS;gBACX,CAAC;gBACD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;oBACxC,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,OAAO;wBAAE,EAAE,CAAC,GAAG,KAAK,IAAI,GAAG,EAAE,EAAE,8BAA8B,CAAC,CAAC;gBAClG,CAAC;gBACD,IAAI,QAAQ,CAAC,OAAO,KAAK,SAAS;oBAAE,sBAAsB,CAAC,QAAQ,CAAC,OAAO,EAAE,GAAG,KAAK,UAAU,EAAE,EAAE,CAAC,CAAC;gBACrG,IAAI,QAAQ,CAAC,KAAK,KAAK,SAAS;oBAAE,oBAAoB,CAAC,QAAQ,CAAC,KAAK,EAAE,GAAG,KAAK,QAAQ,EAAE,EAAE,CAAC,CAAC;YAC/F,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,6FAA6F;QAC7F,0DAA0D;QAC1D,MAAM,IAAI,WAAW,CAAC,GAAG,KAAK,qBAAqB,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC9E,CAAC;IAED,OAAO,KAAkC,CAAC;AAC5C,CAAC;AAID,SAAS,mBAAmB,CAAC,KAAc,EAAE,IAAY,EAAE,EAAU;IACnE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,EAAE,CAAC,IAAI,EAAE,6BAA6B,CAAC,CAAC;QACxC,OAAO;IACT,CAAC;IACD,KAAK,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;QAC5C,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YACnD,EAAE,CAAC,GAAG,IAAI,IAAI,KAAK,GAAG,EAAE,mCAAmC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACrF,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,eAAe,CAAC,KAAc,EAAE,IAAY,EAAE,EAAU;IAC/D,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,EAAE,CAAC,IAAI,EAAE,mBAAmB,CAAC,CAAC;QAC9B,OAAO;IACT,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC,CAAC;IACzD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACrC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,EAAE,CAAC,GAAG,IAAI,IAAI,GAAG,EAAE,EAAE,gCAAgC,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACtG,CAAC;IACD,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;QACxB,IAAI,KAAK,CAAC,GAAG,CAAC,KAAK,SAAS;YAAE,mBAAmB,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,GAAG,IAAI,IAAI,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC;IACtF,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC/D,EAAE,CAAC,IAAI,EAAE,kDAAkD,CAAC,CAAC;IAC/D,CAAC;AACH,CAAC;AAED,SAAS,oBAAoB,CAAC,KAAc,EAAE,IAAY,EAAE,EAAU;IACpE,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,EAAE,CAAC,IAAI,EAAE,mBAAmB,CAAC,CAAC;QAC9B,OAAO;IACT,CAAC;IACD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACrC,IAAI,GAAG,KAAK,UAAU;YAAE,EAAE,CAAC,GAAG,IAAI,IAAI,GAAG,EAAE,EAAE,0CAA0C,CAAC,CAAC;IAC3F,CAAC;IACD,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;QACnE,EAAE,CAAC,GAAG,IAAI,WAAW,EAAE,8CAA8C,CAAC,CAAC;IACzE,CAAC;AACH,CAAC;AAED,SAAS,sBAAsB,CAAC,KAAc,EAAE,IAAY,EAAE,EAAU;IACtE,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,EAAE,CAAC,IAAI,EAAE,mBAAmB,CAAC,CAAC;QAC9B,OAAO;IACT,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,GAAG,CAAS,CAAC,GAAG,eAAe,EAAE,GAAG,aAAa,CAAC,CAAC,CAAC;IACtE,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACrC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,EAAE,CAAC,GAAG,IAAI,IAAI,GAAG,EAAE,EAAE,oCAAoC,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC1G,CAAC;IAED,KAAK,MAAM,OAAO,IAAI,eAAe,EAAE,CAAC;QACtC,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;QAC7B,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;YACjD,EAAE,CAAC,GAAG,IAAI,IAAI,OAAO,EAAE,EAAE,wCAAwC,CAAC,CAAC;QACrE,CAAC;IACH,CAAC;IAED,KAAK,MAAM,OAAO,IAAI,aAAa,EAAE,CAAC;QACpC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,SAAS;YAAE,mBAAmB,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,GAAG,IAAI,IAAI,OAAO,EAAE,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC;IAC3G,CAAC;AACH,CAAC;AAED,SAAS,mBAAmB,CAAC,KAAc,EAAE,IAAY,EAAE,OAAoB,EAAE,EAAU;IACzF,IAAI,OAAgB,CAAC;IAErB,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,GAAG,KAAK,CAAC;IAClB,CAAC;SAAM,IAAI,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;QAChC,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC,CAAC;QACxD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YACrC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,EAAE,CAAC,GAAG,IAAI,IAAI,GAAG,EAAE,EAAE,gCAAgC,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACtG,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,UAAU,CAAU,EAAE,CAAC;YAClD,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,SAAS,IAAI,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,SAAS;gBAAE,EAAE,CAAC,GAAG,IAAI,IAAI,IAAI,EAAE,EAAE,mBAAmB,CAAC,CAAC;QAChH,CAAC;QACD,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;YAChC,EAAE,CAAC,GAAG,IAAI,UAAU,EAAE,sDAAsD,CAAC,CAAC;YAC9E,OAAO;QACT,CAAC;QACD,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;IAC1B,CAAC;SAAM,CAAC;QACN,EAAE,CAAC,IAAI,EAAE,mEAAmE,CAAC,CAAC;QAC9E,OAAO;IACT,CAAC;IAED,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5B,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,UAAU,EAAE,kBAAkB,CAAC,CAAC;QACxE,OAAO;IACT,CAAC;IAED,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,UAAU,CAAC;IAC7D,MAAM,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAC;IACvC,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;QAC/C,MAAM,KAAK,GAAG,GAAG,IAAI,IAAI,KAAK,GAAG,CAAC;QAClC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1B,EAAE,CAAC,KAAK,EAAE,mBAAmB,CAAC,CAAC;YAC/B,SAAS;QACX,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,YAAY,CAAC,OAAO,CAAC,EAAE,CAAC;YAC1C,6FAA6F;YAC7F,IAAI,KAAK,KAAK,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;gBAAE,SAAS;YACjD,IAAI,OAAO,KAAK,CAAC,KAAK,CAAC,KAAK,QAAQ,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;gBACnE,EAAE,CAAC,GAAG,KAAK,IAAI,KAAK,EAAE,EAAE,4CAA4C,CAAC,CAAC;YACxE,CAAC;QACH,CAAC;QACD,MAAM,GAAG,GAAG,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACrC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC5B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,kFAAkF;YAClF,EAAE,CAAC,KAAK,EAAE,cAAc,IAAI,IAAI,KAAK,WAAW,YAAY,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACxF,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACvB,CAAC;IACH,CAAC;AACH,CAAC;AAED,mGAAmG;AAEnG;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAuB,EAAE,EAAE,WAA2B,EAAE;IAC1F,MAAM,MAAM,GAA4B,EAAE,CAAC;IAE3C,KAAK,MAAM,OAAO,IAAI,eAAe,EAAE,CAAC;QACtC,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;QAC7D,IAAI,KAAK,KAAK,SAAS;YAAE,MAAM,CAAC,OAAO,CAAC,GAAG,KAAK,CAAC;IACnD,CAAC;IAED,KAAK,MAAM,OAAO,IAAI,aAAa,EAAE,CAAC;QACpC,MAAM,KAAK,GAAG,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;QACvE,IAAI,KAAK,KAAK,SAAS;YAAE,MAAM,CAAC,OAAO,CAAC,GAAG,KAAK,CAAC;IACnD,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,OAAqB,EAAE,EAAE,WAAyB,EAAE;IACpF,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAChE,OAAO,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC;AACpD,CAAC;AAED,SAAS,YAAY,CACnB,IAAmD,EACnD,QAAuD;IAEvD,IAAI,IAAI,KAAK,SAAS,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACnE,OAAO,EAAE,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC,EAAE,CAAC;AAClD,CAAC;AAED,SAAS,aAAa,CAAC,OAAoB,EAAE,IAAc,EAAE,QAAkB;IAC7E,MAAM,IAAI,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IACpC,MAAM,KAAK,GAAG,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IACzC,IAAI,IAAI,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAChE,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,EAAE,KAAK,EAAE,KAAM,CAAC,KAAK,EAAE,OAAO,EAAE,KAAM,CAAC,OAAO,EAAE,CAAC;IAChF,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;IAE7E,4FAA4F;IAC5F,4DAA4D;IAC5D,IAAI,KAAK,CAAC,OAAO;QAAE,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;IAEzE,MAAM,KAAK,GAAG,IAAI,GAAG,EAAiB,CAAC;IACvC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QACjC,MAAM,GAAG,GAAG,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACrC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACrC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACxB,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;QAClC,MAAM,GAAG,GAAG,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACrC,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAChC,IAAI,QAAQ,KAAK,SAAS;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC5C,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,QAAQ,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC;IAC7E,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAE,CAAC,EAAE,CAAC;AAC5F,CAAC;AAED,kGAAkG;AAClG,MAAM,UAAU,gBAAgB,CAAC,MAAsB,EAAE,IAAY;IACnE,OAAO,mBAAmB,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC;AAChF,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,cAAc,CAAC,MAAsB,EAAE,IAAY;IACjE,OAAO,iBAAiB,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,CAAC;AAC1E,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;IACjD,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC9B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,QAAQ,GACZ,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,yCAAyC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAC1G,MAAM,IAAI,WAAW,CAAC,GAAG,IAAI,qBAAqB,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,QAAQ,EAAE,GAAG,CAAC,CAAC;IACxG,CAAC;IACD,OAAO,sBAAsB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AAC9C,CAAC"}
@@ -0,0 +1,21 @@
1
+ import type { BaselineConfig, ProjectDesired } from './baseline-config.js';
2
+ import type { GitLabClient } from './gitlab-client.js';
3
+ /**
4
+ * Reads a live project and writes down what it looks like, as a config someone can edit.
5
+ *
6
+ * Writing a baseline by hand against forty attributes is miserable and the first draft would be
7
+ * wrong. Exporting the project that already looks right and deleting what you do not care about is
8
+ * how anyone actually starts, so this errs towards saying too much: deleting a line is easier than
9
+ * discovering that a key exists.
10
+ */
11
+ /** The sections `export` can emit, in the order they appear in the file. */
12
+ export declare const EXPORTABLE_SECTIONS: readonly ["settings", "protectedBranches", "protectedTags", "approvals", "approvalRules", "pushRules", "variables"];
13
+ export type ExportableSection = (typeof EXPORTABLE_SECTIONS)[number];
14
+ export interface ExportOptions {
15
+ /** Which sections to emit. Defaults to all of them. */
16
+ readonly sections?: readonly ExportableSection[];
17
+ }
18
+ /** Reads one project and returns the `project` block of a config describing it. */
19
+ export declare function exportProject(client: GitLabClient, project: string, options?: ExportOptions): Promise<ProjectDesired>;
20
+ /** A whole config file describing one project, ready to be widened to a group of them. */
21
+ export declare function exportConfig(client: GitLabClient, project: string, options?: ExportOptions): Promise<BaselineConfig>;
@@ -0,0 +1,245 @@
1
+ /*
2
+ * Copyright (c) 2026, SimplySF.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ /**
17
+ * Reads a live project and writes down what it looks like, as a config someone can edit.
18
+ *
19
+ * Writing a baseline by hand against forty attributes is miserable and the first draft would be
20
+ * wrong. Exporting the project that already looks right and deleting what you do not care about is
21
+ * how anyone actually starts, so this errs towards saying too much: deleting a line is easier than
22
+ * discovering that a key exists.
23
+ */
24
+ /** The sections `export` can emit, in the order they appear in the file. */
25
+ export const EXPORTABLE_SECTIONS = [
26
+ 'settings',
27
+ 'protectedBranches',
28
+ 'protectedTags',
29
+ 'approvals',
30
+ 'approvalRules',
31
+ 'pushRules',
32
+ 'variables',
33
+ ];
34
+ /**
35
+ * The project attributes worth putting in a baseline.
36
+ *
37
+ * A deliberate list rather than everything `GET /projects/:id` returns: that payload carries ids,
38
+ * timestamps, counts, and URLs, none of which can be set and all of which would look like intent
39
+ * once they were sitting in a config file.
40
+ */
41
+ const SETTING_KEYS = [
42
+ // Merge request behaviour
43
+ 'merge_method',
44
+ 'squash_option',
45
+ 'only_allow_merge_if_pipeline_succeeds',
46
+ 'only_allow_merge_if_all_discussions_are_resolved',
47
+ 'allow_merge_on_skipped_pipeline',
48
+ 'remove_source_branch_after_merge',
49
+ 'merge_trains_enabled',
50
+ 'printing_merge_request_link_enabled',
51
+ 'resolve_outdated_diff_discussions',
52
+ 'merge_requests_template',
53
+ // CI/CD
54
+ 'ci_config_path',
55
+ 'build_timeout',
56
+ 'auto_cancel_pending_pipelines',
57
+ 'ci_default_git_depth',
58
+ 'ci_forward_deployment_enabled',
59
+ 'ci_separated_caches',
60
+ 'ci_allow_fork_pipelines_to_run_in_parent_project',
61
+ 'keep_latest_artifact',
62
+ 'auto_devops_enabled',
63
+ 'shared_runners_enabled',
64
+ 'group_runners_enabled',
65
+ // Repository
66
+ 'default_branch',
67
+ 'issues_enabled',
68
+ 'wiki_enabled',
69
+ 'packages_enabled',
70
+ ];
71
+ /** Approval settings that can be set back, as opposed to reported. */
72
+ const APPROVAL_KEYS = [
73
+ 'approvals_before_merge',
74
+ 'reset_approvals_on_push',
75
+ 'disable_overriding_approvers_per_merge_request',
76
+ 'merge_requests_author_approval',
77
+ 'merge_requests_disable_committers_approval',
78
+ 'require_password_to_approve',
79
+ ];
80
+ /** Push-rule attributes, minus the id and the project it belongs to. */
81
+ const PUSH_RULE_KEYS = [
82
+ 'commit_message_regex',
83
+ 'commit_message_negative_regex',
84
+ 'branch_name_regex',
85
+ 'author_email_regex',
86
+ 'file_name_regex',
87
+ 'deny_delete_tag',
88
+ 'member_check',
89
+ 'prevent_secrets',
90
+ 'max_file_size',
91
+ 'commit_committer_check',
92
+ 'commit_committer_name_check',
93
+ 'reject_unsigned_commits',
94
+ 'reject_non_dco_commits',
95
+ ];
96
+ /** Protected-branch attributes, keeping the access arrays but dropping their server-side ids. */
97
+ const BRANCH_KEYS = ['name', 'allow_force_push', 'code_owner_approval_required'];
98
+ const ACCESS_ARRAYS = ['push_access_levels', 'merge_access_levels', 'unprotect_access_levels'];
99
+ const ACCESS_TARGET = {
100
+ push_access_levels: 'allowed_to_push',
101
+ merge_access_levels: 'allowed_to_merge',
102
+ unprotect_access_levels: 'allowed_to_unprotect',
103
+ };
104
+ function asRecord(value) {
105
+ return (typeof value === 'object' && value !== null ? value : {});
106
+ }
107
+ /** Copies the named keys that are actually present, so an absent attribute stays absent. */
108
+ function pick(source, keys) {
109
+ const picked = {};
110
+ for (const key of keys) {
111
+ if (source[key] !== undefined && source[key] !== null)
112
+ picked[key] = source[key];
113
+ }
114
+ return picked;
115
+ }
116
+ /**
117
+ * Turns an access-level array as GitLab reports it into the shape it accepts back.
118
+ *
119
+ * The read shape carries `id` and `access_level_description`, neither of which is writable, and the
120
+ * write shape is a different key. Exporting the raw read would produce a config that fails on its
121
+ * first apply.
122
+ */
123
+ function accessEntries(levels) {
124
+ if (!Array.isArray(levels))
125
+ return undefined;
126
+ const mapped = levels.map((raw) => {
127
+ const level = asRecord(raw);
128
+ const entry = {};
129
+ if (typeof level.user_id === 'number')
130
+ entry.user_id = level.user_id;
131
+ else if (typeof level.group_id === 'number')
132
+ entry.group_id = level.group_id;
133
+ else if (typeof level.access_level === 'number')
134
+ entry.access_level = level.access_level;
135
+ return entry;
136
+ });
137
+ return mapped.length === 0 ? undefined : mapped;
138
+ }
139
+ function exportBranch(raw) {
140
+ const branch = asRecord(raw);
141
+ const entry = pick(branch, BRANCH_KEYS);
142
+ for (const key of ACCESS_ARRAYS) {
143
+ const mapped = accessEntries(branch[key]);
144
+ if (mapped !== undefined)
145
+ entry[ACCESS_TARGET[key]] = mapped;
146
+ }
147
+ return entry;
148
+ }
149
+ function exportTag(raw) {
150
+ const tag = asRecord(raw);
151
+ const entry = { name: tag.name };
152
+ const levels = Array.isArray(tag.create_access_levels) ? tag.create_access_levels.map(asRecord) : [];
153
+ const first = levels[0];
154
+ if (first !== undefined && typeof first.access_level === 'number')
155
+ entry.create_access_level = first.access_level;
156
+ return entry;
157
+ }
158
+ function exportRule(raw) {
159
+ const rule = asRecord(raw);
160
+ // The implicit fallback rule is not something a config declares; exporting it would invite
161
+ // someone to prune it and lose the project's default approval behaviour.
162
+ if (rule.rule_type === 'any_approver')
163
+ return undefined;
164
+ const entry = { name: rule.name, approvals_required: rule.approvals_required };
165
+ const usernames = (Array.isArray(rule.eligible_approvers) ? rule.eligible_approvers : [])
166
+ .map((user) => asRecord(user).username)
167
+ .filter((username) => typeof username === 'string');
168
+ if (usernames.length > 0)
169
+ entry.usernames = usernames.sort();
170
+ const groups = (Array.isArray(rule.groups) ? rule.groups : [])
171
+ .map((group) => asRecord(group).full_path)
172
+ .filter((path) => typeof path === 'string');
173
+ if (groups.length > 0)
174
+ entry.groups = groups.sort();
175
+ const branches = (Array.isArray(rule.protected_branches) ? rule.protected_branches : [])
176
+ .map((branch) => asRecord(branch).name)
177
+ .filter((name) => typeof name === 'string');
178
+ if (branches.length > 0)
179
+ entry.protectedBranches = branches.sort();
180
+ else if (rule.applies_to_all_protected_branches === true)
181
+ entry.applies_to_all_protected_branches = true;
182
+ return entry;
183
+ }
184
+ /** Variables are exported as metadata. The `value` GitLab returns is never copied into the file. */
185
+ function exportVariable(raw) {
186
+ const variable = asRecord(raw);
187
+ return pick(variable, ['key', 'environment_scope', 'variable_type', 'protected', 'masked', 'raw']);
188
+ }
189
+ /** Reads a section, treating a Premium-only endpoint that refuses as simply absent. */
190
+ async function optional(read) {
191
+ try {
192
+ return await read();
193
+ }
194
+ catch {
195
+ return undefined;
196
+ }
197
+ }
198
+ /** Reads one project and returns the `project` block of a config describing it. */
199
+ export async function exportProject(client, project, options = {}) {
200
+ const wanted = new Set(options.sections ?? EXPORTABLE_SECTIONS);
201
+ const desired = {};
202
+ if (wanted.has('settings')) {
203
+ desired.settings = pick(asRecord(await client.getProject(project)), SETTING_KEYS);
204
+ }
205
+ if (wanted.has('protectedBranches')) {
206
+ desired.protectedBranches = (await client.listProtectedBranches(project)).items.map(exportBranch);
207
+ }
208
+ if (wanted.has('protectedTags')) {
209
+ desired.protectedTags = (await client.listProtectedTags(project)).items.map(exportTag);
210
+ }
211
+ if (wanted.has('approvals')) {
212
+ const approvals = await optional(() => client.getApprovalSettings(project));
213
+ if (approvals !== undefined)
214
+ desired.approvals = pick(asRecord(approvals), APPROVAL_KEYS);
215
+ }
216
+ if (wanted.has('approvalRules')) {
217
+ const rules = await optional(() => client.listApprovalRules(project));
218
+ if (rules !== undefined) {
219
+ desired.approvalRules = rules.items.map(exportRule).filter((rule) => rule !== undefined);
220
+ }
221
+ }
222
+ if (wanted.has('pushRules')) {
223
+ const pushRules = await optional(() => client.getPushRules(project));
224
+ if (pushRules !== undefined && pushRules !== null)
225
+ desired.pushRules = pick(asRecord(pushRules), PUSH_RULE_KEYS);
226
+ }
227
+ if (wanted.has('variables')) {
228
+ const variables = await optional(() => client.listVariables(project, 200));
229
+ if (variables !== undefined)
230
+ desired.variables = variables.items.map(exportVariable);
231
+ }
232
+ return desired;
233
+ }
234
+ /** A whole config file describing one project, ready to be widened to a group of them. */
235
+ export async function exportConfig(client, project, options = {}) {
236
+ const desired = await exportProject(client, project, options);
237
+ return {
238
+ version: 1,
239
+ // Deliberately the project it came from, not a group: whoever runs this next should choose the
240
+ // blast radius on purpose rather than inherit one from an export.
241
+ targets: { projects: [project] },
242
+ project: desired,
243
+ };
244
+ }
245
+ //# sourceMappingURL=baseline-export.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline-export.js","sourceRoot":"","sources":["../src/baseline-export.ts"],"names":[],"mappings":"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"}