@foss.global/forgefixtures 0.2.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.
Files changed (65) hide show
  1. package/.smartconfig.json +49 -0
  2. package/changelog.md +17 -0
  3. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  4. package/dist_ts/00_commitinfo_data.js +9 -0
  5. package/dist_ts/classes.certificateauthority.d.ts +22 -0
  6. package/dist_ts/classes.certificateauthority.js +93 -0
  7. package/dist_ts/classes.containerlifecycle.d.ts +88 -0
  8. package/dist_ts/classes.containerlifecycle.js +383 -0
  9. package/dist_ts/classes.giteafixture.d.ts +41 -0
  10. package/dist_ts/classes.giteafixture.js +182 -0
  11. package/dist_ts/classes.giteaseed.d.ts +13 -0
  12. package/dist_ts/classes.giteaseed.js +432 -0
  13. package/dist_ts/classes.gitlabfixture.d.ts +49 -0
  14. package/dist_ts/classes.gitlabfixture.js +237 -0
  15. package/dist_ts/classes.gitlabseed.d.ts +13 -0
  16. package/dist_ts/classes.gitlabseed.js +466 -0
  17. package/dist_ts/classes.httpclient.d.ts +53 -0
  18. package/dist_ts/classes.httpclient.js +116 -0
  19. package/dist_ts/classes.reaper.d.ts +25 -0
  20. package/dist_ts/classes.reaper.js +102 -0
  21. package/dist_ts/classes.tlsterminator.d.ts +21 -0
  22. package/dist_ts/classes.tlsterminator.js +131 -0
  23. package/dist_ts/constants.d.ts +22 -0
  24. package/dist_ts/constants.js +30 -0
  25. package/dist_ts/giteaseed.default.d.ts +10 -0
  26. package/dist_ts/giteaseed.default.js +87 -0
  27. package/dist_ts/gitlabseed.default.d.ts +12 -0
  28. package/dist_ts/gitlabseed.default.js +84 -0
  29. package/dist_ts/index.d.ts +17 -0
  30. package/dist_ts/index.js +17 -0
  31. package/dist_ts/interfaces.d.ts +92 -0
  32. package/dist_ts/interfaces.giteaseed.d.ts +182 -0
  33. package/dist_ts/interfaces.giteaseed.js +2 -0
  34. package/dist_ts/interfaces.gitlabseed.d.ts +183 -0
  35. package/dist_ts/interfaces.gitlabseed.js +2 -0
  36. package/dist_ts/interfaces.js +2 -0
  37. package/dist_ts/ownership.d.ts +29 -0
  38. package/dist_ts/ownership.js +140 -0
  39. package/dist_ts/plugins.d.ts +13 -0
  40. package/dist_ts/plugins.js +18 -0
  41. package/dist_ts/responses.d.ts +7 -0
  42. package/dist_ts/responses.js +30 -0
  43. package/license.md +21 -0
  44. package/package.json +65 -0
  45. package/readme.md +206 -0
  46. package/ts/00_commitinfo_data.ts +8 -0
  47. package/ts/classes.certificateauthority.ts +117 -0
  48. package/ts/classes.containerlifecycle.ts +432 -0
  49. package/ts/classes.giteafixture.ts +205 -0
  50. package/ts/classes.giteaseed.ts +486 -0
  51. package/ts/classes.gitlabfixture.ts +258 -0
  52. package/ts/classes.gitlabseed.ts +502 -0
  53. package/ts/classes.httpclient.ts +156 -0
  54. package/ts/classes.reaper.ts +126 -0
  55. package/ts/classes.tlsterminator.ts +136 -0
  56. package/ts/constants.ts +35 -0
  57. package/ts/giteaseed.default.ts +88 -0
  58. package/ts/gitlabseed.default.ts +86 -0
  59. package/ts/index.ts +17 -0
  60. package/ts/interfaces.giteaseed.ts +130 -0
  61. package/ts/interfaces.gitlabseed.ts +135 -0
  62. package/ts/interfaces.ts +94 -0
  63. package/ts/ownership.ts +160 -0
  64. package/ts/plugins.ts +23 -0
  65. package/ts/responses.ts +33 -0
@@ -0,0 +1,140 @@
1
+ import * as plugins from './plugins.js';
2
+ import { forgeFixtureLabels } from './constants.js';
3
+ const machineIdPattern = /^[0-9a-f]{32}$/;
4
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
5
+ const decimalPattern = /^[0-9]{1,20}$/;
6
+ const pidNamespacePattern = /^pid:\[[0-9]{1,20}\]$/;
7
+ const isErrnoException = (errorArg) => errorArg instanceof Error && 'code' in errorArg;
8
+ /**
9
+ * Reads field 22 (`starttime`) of `/proc/<pid>/stat`. Returns `undefined` when
10
+ * the process does not exist. The command name may contain spaces and
11
+ * parentheses, so fields are counted after its closing parenthesis.
12
+ */
13
+ export const readProcessStartTicks = async (pidArg) => {
14
+ let stat;
15
+ try {
16
+ stat = await plugins.fs.promises.readFile(`/proc/${pidArg}/stat`, 'utf8');
17
+ }
18
+ catch (error) {
19
+ if (isErrnoException(error) && error.code === 'ENOENT')
20
+ return undefined;
21
+ throw error;
22
+ }
23
+ const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
24
+ // fields[0] is field 3 (state), so field 22 is fields[19].
25
+ const startTicks = fields[19];
26
+ if (startTicks === undefined || !decimalPattern.test(startTicks)) {
27
+ throw new Error(`Process ${pidArg} has a malformed /proc stat record.`);
28
+ }
29
+ return startTicks;
30
+ };
31
+ /**
32
+ * Stands in for the machine ID of a process whose system has no valid
33
+ * `/etc/machine-id`, as in many container images. It is fixed for the life of
34
+ * the process and never matches another process, so every other owner is
35
+ * foreign to this one, and this owner is foreign to every other process: both
36
+ * sides judge each other by expiry only.
37
+ */
38
+ const processLocalMachineId = plugins.crypto.randomBytes(16).toString('hex');
39
+ const readMachineId = async () => {
40
+ let machineId;
41
+ try {
42
+ machineId = (await plugins.fs.promises.readFile('/etc/machine-id', 'utf8')).trim();
43
+ }
44
+ catch (error) {
45
+ if (isErrnoException(error) && error.code === 'ENOENT')
46
+ return processLocalMachineId;
47
+ throw error;
48
+ }
49
+ return machineIdPattern.test(machineId) ? machineId : processLocalMachineId;
50
+ };
51
+ export const readCurrentProcessIdentity = async () => {
52
+ const machineId = await readMachineId();
53
+ const bootId = (await plugins.fs.promises.readFile('/proc/sys/kernel/random/boot_id', 'utf8')).trim();
54
+ if (!uuidPattern.test(bootId))
55
+ throw new Error('The kernel boot id is malformed.');
56
+ const processStartTicks = await readProcessStartTicks(process.pid);
57
+ if (processStartTicks === undefined)
58
+ throw new Error('The current process has no /proc stat record.');
59
+ const pidNamespace = await plugins.fs.promises.readlink('/proc/self/ns/pid');
60
+ if (!pidNamespacePattern.test(pidNamespace))
61
+ throw new Error('The current PID namespace identity is malformed.');
62
+ return { machineId, hostname: plugins.os.hostname(), bootId, pidNamespace, pid: process.pid, processStartTicks };
63
+ };
64
+ export const createForgeFixtureOwner = async (kindArg, maxLifetimeMsArg) => {
65
+ if (!Number.isSafeInteger(maxLifetimeMsArg) || maxLifetimeMsArg < 60_000) {
66
+ throw new TypeError('maxLifetimeMs must be an integer of at least 60000.');
67
+ }
68
+ const identity = await readCurrentProcessIdentity();
69
+ const createdAt = new Date();
70
+ return {
71
+ lifecycleId: plugins.crypto.randomUUID(),
72
+ kind: kindArg,
73
+ ...identity,
74
+ createdAt: createdAt.toISOString(),
75
+ expiresAt: new Date(createdAt.getTime() + maxLifetimeMsArg).toISOString(),
76
+ };
77
+ };
78
+ /** Labels carried by every Docker resource of one lifecycle. */
79
+ export const forgeFixtureOwnerLabels = (ownerArg) => ({
80
+ [forgeFixtureLabels.managed]: 'true',
81
+ [forgeFixtureLabels.lifecycleId]: ownerArg.lifecycleId,
82
+ [forgeFixtureLabels.kind]: ownerArg.kind,
83
+ [forgeFixtureLabels.machineId]: ownerArg.machineId,
84
+ [forgeFixtureLabels.hostname]: ownerArg.hostname,
85
+ [forgeFixtureLabels.bootId]: ownerArg.bootId,
86
+ [forgeFixtureLabels.pidNamespace]: ownerArg.pidNamespace,
87
+ [forgeFixtureLabels.pid]: String(ownerArg.pid),
88
+ [forgeFixtureLabels.processStartTicks]: ownerArg.processStartTicks,
89
+ [forgeFixtureLabels.createdAt]: ownerArg.createdAt,
90
+ [forgeFixtureLabels.expiresAt]: ownerArg.expiresAt,
91
+ });
92
+ const isIsoTimestamp = (valueArg) => valueArg !== undefined && !Number.isNaN(Date.parse(valueArg)) && new Date(valueArg).toISOString() === valueArg;
93
+ /** Parses ownership labels; returns `undefined` for anything this package did not write. */
94
+ export const parseForgeFixtureOwnerLabels = (labelsArg) => {
95
+ if (!labelsArg || labelsArg[forgeFixtureLabels.managed] !== 'true')
96
+ return undefined;
97
+ const lifecycleId = labelsArg[forgeFixtureLabels.lifecycleId];
98
+ const kind = labelsArg[forgeFixtureLabels.kind];
99
+ const machineId = labelsArg[forgeFixtureLabels.machineId];
100
+ const hostname = labelsArg[forgeFixtureLabels.hostname];
101
+ const bootId = labelsArg[forgeFixtureLabels.bootId];
102
+ const pidNamespace = labelsArg[forgeFixtureLabels.pidNamespace];
103
+ const pid = labelsArg[forgeFixtureLabels.pid];
104
+ const processStartTicks = labelsArg[forgeFixtureLabels.processStartTicks];
105
+ const createdAt = labelsArg[forgeFixtureLabels.createdAt];
106
+ const expiresAt = labelsArg[forgeFixtureLabels.expiresAt];
107
+ if (lifecycleId === undefined || !uuidPattern.test(lifecycleId)
108
+ || (kind !== 'gitea' && kind !== 'gitlab')
109
+ || machineId === undefined || !machineIdPattern.test(machineId)
110
+ || !hostname
111
+ || bootId === undefined || !uuidPattern.test(bootId)
112
+ || pidNamespace === undefined || !pidNamespacePattern.test(pidNamespace)
113
+ || pid === undefined || !decimalPattern.test(pid) || !Number.isSafeInteger(Number(pid))
114
+ || processStartTicks === undefined || !decimalPattern.test(processStartTicks)
115
+ || !isIsoTimestamp(createdAt) || !isIsoTimestamp(expiresAt))
116
+ return undefined;
117
+ return {
118
+ lifecycleId, kind, machineId, hostname, bootId, pidNamespace, pid: Number(pid), processStartTicks, createdAt, expiresAt,
119
+ };
120
+ };
121
+ /**
122
+ * Decides whether a lifecycle is stale. A PID identifies a process only inside
123
+ * its PID namespace: host-network containers sharing the Docker socket share
124
+ * hostname and boot id but not PIDs, and machines sharing a remote daemon can
125
+ * share a host name. Owners on another machine (machine id or host name) or in
126
+ * another PID namespace are therefore judged by expiry only.
127
+ */
128
+ export const classifyForgeFixtureOwner = async (ownerArg, currentArg, nowArg) => {
129
+ if (Date.parse(ownerArg.expiresAt) <= nowArg.getTime())
130
+ return 'expired';
131
+ if (ownerArg.machineId !== currentArg.machineId || ownerArg.hostname !== currentArg.hostname)
132
+ return 'alive';
133
+ if (ownerArg.bootId !== currentArg.bootId)
134
+ return 'owner-rebooted';
135
+ if (ownerArg.pidNamespace !== currentArg.pidNamespace)
136
+ return 'alive';
137
+ const startTicks = await readProcessStartTicks(ownerArg.pid);
138
+ return startTicks === ownerArg.processStartTicks ? 'alive' : 'owner-exited';
139
+ };
140
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3duZXJzaGlwLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvb3duZXJzaGlwLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sY0FBYyxDQUFDO0FBQ3hDLE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBWXBELE1BQU0sZ0JBQWdCLEdBQUcsZ0JBQWdCLENBQUM7QUFDMUMsTUFBTSxXQUFXLEdBQUcsZ0VBQWdFLENBQUM7QUFDckYsTUFBTSxjQUFjLEdBQUcsZUFBZSxDQUFDO0FBQ3ZDLE1BQU0sbUJBQW1CLEdBQUcsdUJBQXVCLENBQUM7QUFFcEQsTUFBTSxnQkFBZ0IsR0FBRyxDQUFDLFFBQWlCLEVBQXFDLEVBQUUsQ0FDaEYsUUFBUSxZQUFZLEtBQUssSUFBSSxNQUFNLElBQUksUUFBUSxDQUFDO0FBRWxEOzs7O0dBSUc7QUFDSCxNQUFNLENBQUMsTUFBTSxxQkFBcUIsR0FBRyxLQUFLLEVBQUUsTUFBYyxFQUErQixFQUFFO0lBQ3pGLElBQUksSUFBWSxDQUFDO0lBQ2pCLElBQUksQ0FBQztRQUNILElBQUksR0FBRyxNQUFNLE9BQU8sQ0FBQyxFQUFFLENBQUMsUUFBUSxDQUFDLFFBQVEsQ0FBQyxTQUFTLE1BQU0sT0FBTyxFQUFFLE1BQU0sQ0FBQyxDQUFDO0lBQzVFLENBQUM7SUFBQyxPQUFPLEtBQUssRUFBRSxDQUFDO1FBQ2YsSUFBSSxnQkFBZ0IsQ0FBQyxLQUFLLENBQUMsSUFBSSxLQUFLLENBQUMsSUFBSSxLQUFLLFFBQVE7WUFBRSxPQUFPLFNBQVMsQ0FBQztRQUN6RSxNQUFNLEtBQUssQ0FBQztJQUNkLENBQUM7SUFDRCxNQUFNLE1BQU0sR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxXQUFXLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDO0lBQ2hFLDJEQUEyRDtJQUMzRCxN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
@@ -0,0 +1,13 @@
1
+ import 'reflect-metadata';
2
+ import * as crypto from 'node:crypto';
3
+ import * as fs from 'node:fs';
4
+ import * as http from 'node:http';
5
+ import * as https from 'node:https';
6
+ import * as net from 'node:net';
7
+ import * as os from 'node:os';
8
+ import * as tls from 'node:tls';
9
+ export { crypto, fs, http, https, net, os, tls };
10
+ import * as docker from '@apiclient.xyz/docker';
11
+ export { docker };
12
+ import * as x509 from '@peculiar/x509';
13
+ export { x509 };
@@ -0,0 +1,18 @@
1
+ // Polyfill required by @peculiar/x509 (through tsyringe). This import must precede its evaluation.
2
+ import 'reflect-metadata';
3
+ // Node native scope
4
+ import * as crypto from 'node:crypto';
5
+ import * as fs from 'node:fs';
6
+ import * as http from 'node:http';
7
+ import * as https from 'node:https';
8
+ import * as net from 'node:net';
9
+ import * as os from 'node:os';
10
+ import * as tls from 'node:tls';
11
+ export { crypto, fs, http, https, net, os, tls };
12
+ // @apiclient.xyz scope
13
+ import * as docker from '@apiclient.xyz/docker';
14
+ export { docker };
15
+ // third-party scope
16
+ import * as x509 from '@peculiar/x509';
17
+ export { x509 };
18
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3BsdWdpbnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsbUdBQW1HO0FBQ25HLE9BQU8sa0JBQWtCLENBQUM7QUFFMUIsb0JBQW9CO0FBQ3BCLE9BQU8sS0FBSyxNQUFNLE1BQU0sYUFBYSxDQUFDO0FBQ3RDLE9BQU8sS0FBSyxFQUFFLE1BQU0sU0FBUyxDQUFDO0FBQzlCLE9BQU8sS0FBSyxJQUFJLE1BQU0sV0FBVyxDQUFDO0FBQ2xDLE9BQU8sS0FBSyxLQUFLLE1BQU0sWUFBWSxDQUFDO0FBQ3BDLE9BQU8sS0FBSyxHQUFHLE1BQU0sVUFBVSxDQUFDO0FBQ2hDLE9BQU8sS0FBSyxFQUFFLE1BQU0sU0FBUyxDQUFDO0FBQzlCLE9BQU8sS0FBSyxHQUFHLE1BQU0sVUFBVSxDQUFDO0FBRWhDLE9BQU8sRUFBRSxNQUFNLEVBQUUsRUFBRSxFQUFFLElBQUksRUFBRSxLQUFLLEVBQUUsR0FBRyxFQUFFLEVBQUUsRUFBRSxHQUFHLEVBQUUsQ0FBQztBQUVqRCx1QkFBdUI7QUFDdkIsT0FBTyxLQUFLLE1BQU0sTUFBTSx1QkFBdUIsQ0FBQztBQUVoRCxPQUFPLEVBQUUsTUFBTSxFQUFFLENBQUM7QUFFbEIsb0JBQW9CO0FBQ3BCLE9BQU8sS0FBSyxJQUFJLE1BQU0sZ0JBQWdCLENBQUM7QUFFdkMsT0FBTyxFQUFFLElBQUksRUFBRSxDQUFDIn0=
@@ -0,0 +1,7 @@
1
+ /** Narrow readers for forge JSON responses. Each throws with the field it expected. */
2
+ export declare const record: (valueArg: unknown, whatArg: string) => Record<string, unknown>;
3
+ export declare const array: (valueArg: unknown, whatArg: string) => unknown[];
4
+ export declare const integer: (valueArg: unknown, whatArg: string) => number;
5
+ export declare const text: (valueArg: unknown, whatArg: string) => string;
6
+ export declare const boolean: (valueArg: unknown, whatArg: string) => boolean;
7
+ export declare const field: (valueArg: unknown, keyArg: string, whatArg: string) => unknown;
@@ -0,0 +1,30 @@
1
+ /** Narrow readers for forge JSON responses. Each throws with the field it expected. */
2
+ export const record = (valueArg, whatArg) => {
3
+ if (typeof valueArg !== 'object' || valueArg === null || Array.isArray(valueArg)) {
4
+ throw new Error(`Expected ${whatArg} to be an object.`);
5
+ }
6
+ return valueArg;
7
+ };
8
+ export const array = (valueArg, whatArg) => {
9
+ if (!Array.isArray(valueArg))
10
+ throw new Error(`Expected ${whatArg} to be an array.`);
11
+ return valueArg;
12
+ };
13
+ export const integer = (valueArg, whatArg) => {
14
+ if (typeof valueArg !== 'number' || !Number.isSafeInteger(valueArg)) {
15
+ throw new Error(`Expected ${whatArg} to be a safe integer.`);
16
+ }
17
+ return valueArg;
18
+ };
19
+ export const text = (valueArg, whatArg) => {
20
+ if (typeof valueArg !== 'string')
21
+ throw new Error(`Expected ${whatArg} to be a string.`);
22
+ return valueArg;
23
+ };
24
+ export const boolean = (valueArg, whatArg) => {
25
+ if (typeof valueArg !== 'boolean')
26
+ throw new Error(`Expected ${whatArg} to be a boolean.`);
27
+ return valueArg;
28
+ };
29
+ export const field = (valueArg, keyArg, whatArg) => record(valueArg, whatArg)[keyArg];
30
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVzcG9uc2VzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvcmVzcG9uc2VzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLHVGQUF1RjtBQUV2RixNQUFNLENBQUMsTUFBTSxNQUFNLEdBQUcsQ0FBQyxRQUFpQixFQUFFLE9BQWUsRUFBMkIsRUFBRTtJQUNwRixJQUFJLE9BQU8sUUFBUSxLQUFLLFFBQVEsSUFBSSxRQUFRLEtBQUssSUFBSSxJQUFJLEtBQUssQ0FBQyxPQUFPLENBQUMsUUFBUSxDQUFDLEVBQUUsQ0FBQztRQUNqRixNQUFNLElBQUksS0FBSyxDQUFDLFlBQVksT0FBTyxtQkFBbUIsQ0FBQyxDQUFDO0lBQzFELENBQUM7SUFDRCxPQUFPLFFBQW1DLENBQUM7QUFDN0MsQ0FBQyxDQUFDO0FBRUYsTUFBTSxDQUFDLE1BQU0sS0FBSyxHQUFHLENBQUMsUUFBaUIsRUFBRSxPQUFlLEVBQWEsRUFBRTtJQUNyRSxJQUFJLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQUM7UUFBRSxNQUFNLElBQUksS0FBSyxDQUFDLFlBQVksT0FBTyxrQkFBa0IsQ0FBQyxDQUFDO0lBQ3JGLE9BQU8sUUFBUSxDQUFDO0FBQ2xCLENBQUMsQ0FBQztBQUVGLE1BQU0sQ0FBQyxNQUFNLE9BQU8sR0FBRyxDQUFDLFFBQWlCLEVBQUUsT0FBZSxFQUFVLEVBQUU7SUFDcEUsSUFBSSxPQUFPLFFBQVEsS0FBSyxRQUFRLElBQUksQ0FBQyxNQUFNLENBQUMsYUFBYSxDQUFDLFFBQVEsQ0FBQyxFQUFFLENBQUM7UUFDcEUsTUFBTSxJQUFJLEtBQUssQ0FBQyxZQUFZLE9BQU8sd0JBQXdCLENBQUMsQ0FBQztJQUMvRCxDQUFDO0lBQ0QsT0FBTyxRQUFRLENBQUM7QUFDbEIsQ0FBQyxDQUFDO0FBRUYsTUFBTSxDQUFDLE1BQU0sSUFBSSxHQUFHLENBQUMsUUFBaUIsRUFBRSxPQUFlLEVBQVUsRUFBRTtJQUNqRSxJQUFJLE9BQU8sUUFBUSxLQUFLLFFBQVE7UUFBRSxNQUFNLElBQUksS0FBSyxDQUFDLFlBQVksT0FBTyxrQkFBa0IsQ0FBQyxDQUFDO0lBQ3pGLE9BQU8sUUFBUSxDQUFDO0FBQ2xCLENBQUMsQ0FBQztBQUVGLE1BQU0sQ0FBQyxNQUFNLE9BQU8sR0FBRyxDQUFDLFFBQWlCLEVBQUUsT0FBZSxFQUFXLEVBQUU7SUFDckUsSUFBSSxPQUFPLFFBQVEsS0FBSyxTQUFTO1FBQUUsTUFBTSxJQUFJLEtBQUssQ0FBQyxZQUFZLE9BQU8sbUJBQW1CLENBQUMsQ0FBQztJQUMzRixPQUFPLFFBQVEsQ0FBQztBQUNsQixDQUFDLENBQUM7QUFFRixNQUFNLENBQUMsTUFBTSxLQUFLLEdBQUcsQ0FBQyxRQUFpQixFQUFFLE1BQWMsRUFBRSxPQUFlLEVBQVcsRUFBRSxDQUNuRixNQUFNLENBQUMsUUFBUSxFQUFFLE9BQU8sQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDIn0=
package/license.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Task Venture Capital GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@foss.global/forgefixtures",
3
+ "version": "0.2.0",
4
+ "private": false,
5
+ "description": "Disposable, digest-pinned Gitea and GitLab CE fixtures with owned cleanup, local TLS and ground-truth seed data for forge integration tests.",
6
+ "main": "dist_ts/index.js",
7
+ "typings": "dist_ts/index.d.ts",
8
+ "exports": {
9
+ ".": "./dist_ts/index.js"
10
+ },
11
+ "type": "module",
12
+ "author": "Task Venture Capital GmbH",
13
+ "license": "MIT",
14
+ "dependencies": {
15
+ "@apiclient.xyz/docker": "^9.2.0",
16
+ "@peculiar/x509": "^2.1.0",
17
+ "reflect-metadata": "^0.2.2"
18
+ },
19
+ "files": [
20
+ "ts/**/*",
21
+ "dist/**/*",
22
+ "dist_*/**/*",
23
+ "dist_ts/**/*",
24
+ ".smartconfig.json",
25
+ "readme.md",
26
+ "changelog.md",
27
+ "license.md"
28
+ ],
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://code.foss.global/foss.global/forgefixtures.git"
32
+ },
33
+ "bugs": {
34
+ "url": "https://code.foss.global/foss.global/forgefixtures/issues"
35
+ },
36
+ "homepage": "https://code.foss.global/foss.global/forgefixtures#readme",
37
+ "keywords": [
38
+ "foss.global",
39
+ "gitea",
40
+ "gitlab",
41
+ "forge",
42
+ "fixtures",
43
+ "docker",
44
+ "integration testing",
45
+ "typescript"
46
+ ],
47
+ "engines": {
48
+ "node": ">=24.18.0"
49
+ },
50
+ "os": [
51
+ "linux"
52
+ ],
53
+ "devDependencies": {
54
+ "@git.zone/tsbuild": "^5.0.0",
55
+ "@git.zone/tsrun": "^3.0.0",
56
+ "@git.zone/tstest": "^6.2.0",
57
+ "@types/node": "^26.6.1"
58
+ },
59
+ "scripts": {
60
+ "test": "(tstest test/ --verbose)",
61
+ "test:typecheck": "(tsbuild check 'test/**/*')",
62
+ "build": "(tsbuild tsfolders)",
63
+ "buildDocs": "(tsdoc)"
64
+ }
65
+ }
package/readme.md ADDED
@@ -0,0 +1,206 @@
1
+ # @foss.global/forgefixtures
2
+
3
+ Disposable, digest-pinned forge instances for integration tests. `@foss.global/forgefixtures` starts a real Gitea or GitLab Community Edition from its pinned upstream image behind a verified loopback TLS endpoint, bootstraps an administrator token through the forge's supported tooling, seeds realistic data through the documented REST APIs, and returns a ground-truth manifest read back from the forge itself. Every Docker resource carries ownership labels, so resources left behind by a crashed test process are removed by the next run.
4
+
5
+ ## Issue Reporting and Security
6
+
7
+ For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
8
+
9
+ ## Runtime Requirements
10
+
11
+ - Linux (ownership checks read `/proc`)
12
+ - Node.js 24.18 or newer
13
+ - a reachable Docker Engine; for Gitea the socket is taken from the `dockerSocketPath` option, otherwise `DOCKER_HOST`, otherwise `/var/run/docker.sock`
14
+ - for GitLab, a **rootless** Docker Engine: the omnibus image runs as root, and `@apiclient.xyz/docker` permits a root container user only after the daemon proves rootless mode. `GitlabFixture` defaults to the current user's rootless socket (`$XDG_RUNTIME_DIR/docker.sock`, otherwise `/run/user/<uid>/docker.sock`); a rootful daemon is refused
15
+ - network access to Docker Hub for the first pull of a pinned image
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ pnpm add --save-dev @foss.global/forgefixtures
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```typescript
26
+ import { GiteaFixture, GiteaSeedBuilder, createDefaultGiteaSeedSpec } from '@foss.global/forgefixtures';
27
+
28
+ const fixture = new GiteaFixture();
29
+ try {
30
+ const runtime = await fixture.start();
31
+ // runtime.baseUrl https://127.0.0.1:<port>
32
+ // runtime.caCertificatePem the only CA that issued the endpoint certificate
33
+ // runtime.admin.token administrator token with scope `all`
34
+ // runtime.version '1.27.3', verified against the image pin
35
+
36
+ const manifest = await new GiteaSeedBuilder(fixture).apply(createDefaultGiteaSeedSpec());
37
+ const repository = manifest.repositories.find((repositoryArg) => repositoryArg.fullName === 'team-with-hyphens/public-repo');
38
+ console.log(repository?.issues.map((issueArg) => issueArg.number)); // [1, 3]; #2 is a pull request
39
+
40
+ const aliceToken = await fixture.createAccessToken({
41
+ username: 'alice',
42
+ tokenName: 'inventory',
43
+ scopes: ['read:repository', 'read:user'],
44
+ });
45
+ } finally {
46
+ await fixture.stop();
47
+ }
48
+ ```
49
+
50
+ `start()` returns only after Gitea answers `/api/healthz`, reports the pinned version through `/api/v1/version`, and the administrator and its token exist. A fixture is single-use: after `stop()` create a new instance. `stop()` may be called while `start()` is still running: the start is cancelled at its next checkpoint (an image pull is aborted), `start()` rejects with `ForgeFixtureStoppedError`, and `stop()` returns only after every resource that start created is removed. `stop()` is idempotent; if a removal fails, the remaining resources stay recorded, the failures are thrown together, and a later `stop()` retries them.
51
+
52
+ ### GitLab
53
+
54
+ ```typescript
55
+ import { GitlabFixture, GitlabSeedBuilder, createDefaultGitlabSeedSpec } from '@foss.global/forgefixtures';
56
+
57
+ const fixture = new GitlabFixture(); // rootless daemon of the current user
58
+ try {
59
+ const runtime = await fixture.start(); // several minutes: omnibus runs gitlab-ctl reconfigure on first boot
60
+ // runtime.admin.username 'root'
61
+ // runtime.admin.token personal access token with scopes `api` and `sudo`
62
+ // runtime.version '19.4.1', verified against the image pin
63
+
64
+ const manifest = await new GitlabSeedBuilder(fixture).apply(createDefaultGitlabSeedSpec());
65
+ const app = manifest.projects.find((projectArg) => projectArg.pathWithNamespace === 'parent-group/sub-group/app');
66
+ console.log(app?.issues.map((issueArg) => issueArg.iid)); // [1, 2, 3, 4]; merge requests count separately
67
+
68
+ const token = await fixture.createAccessToken({ username: 'carol', tokenName: 'inventory', scopes: ['read_api'] });
69
+ } finally {
70
+ await fixture.stop();
71
+ }
72
+ ```
73
+
74
+ GitLab is configured through `GITLAB_OMNIBUS_CONFIG` with GitLab's memory-constrained settings (Puma in single mode, Sidekiq concurrency 10, Prometheus monitoring off) and `external_url` set to the fixture's HTTPS origin, with nginx listening on plain HTTP behind the TLS terminator and Let's Encrypt off. `start()` returns once all of these hold:
75
+
76
+ - the image's post-reconfigure hook (`GITLAB_POST_RECONFIGURE_SCRIPT`, run by the image's `/assets/init-container`) has run, so `gitlab-ctl reconfigure` finished
77
+ - Docker reports the container healthy through the image's own `gitlab-healthcheck`, gated on that hook
78
+ - an unauthenticated `/api/v4/version` answers 401 through the proxy, so nginx, Workhorse and Puma serve the API
79
+ - an administrator token exists and `/api/v4/version` reports the pinned version
80
+
81
+ Tokens are created with GitLab's documented programmatic method, `gitlab-rails runner`; the script is passed on stdin (`runner -`), so it never appears in process arguments. The default startup deadline is 12 minutes after the container starts; the image pull has its own deadline. Stop-during-start behaves exactly as for Gitea.
82
+
83
+ ### Talking to the fixture
84
+
85
+ The endpoint certificate is issued by a certificate authority that exists only in the memory of the lifecycle. Nothing changes process-global TLS state, so a client must trust `runtime.caCertificatePem` explicitly. `fixture.http` is such a scoped client: it trusts only that CA, refuses URLs outside the fixture origin before sending credentials, never follows redirects, and bounds time and response size.
86
+
87
+ ```typescript
88
+ const user = await fixture.http.requestJson({
89
+ method: 'GET',
90
+ url: '/api/v1/user',
91
+ headers: { authorization: `token ${runtime.admin.token}` },
92
+ expectedStatus: [200],
93
+ });
94
+ ```
95
+
96
+ The endpoint is served by an in-process TLS-terminating reverse proxy that listens before the container starts. Each forge is therefore configured with its exact external origin (Gitea `ROOT_URL`, GitLab `external_url`), and the proxy forwards requests with the client's `Host` header and `X-Forwarded-Proto: https`, as a correctly configured production proxy does. Links the forge generates, including pagination `Link` headers (GitLab keyset pagination included), Git LFS action URLs and GitLab `web_url` values, use the fixture origin.
97
+
98
+ ### Seed specs and manifests
99
+
100
+ `GiteaSeedBuilder.apply(spec)` validates the whole spec before the first request, then creates:
101
+
102
+ - users (with random, discarded passwords) and organizations with `public`, `limited` or `private` visibility
103
+ - repositories owned by users or organizations, auto-initialised on `main`, plus collaborators
104
+ - files on `main`, branches created by one commit each, labels, milestones and tags
105
+ - issues and pull requests in the given order, authored through the administrator's `Sudo` header; labels, milestones, assignees and closing are applied by the administrator so they are not silently dropped for authors without write access
106
+ - releases, and Git LFS objects uploaded through the batch API with their pointers and `.gitattributes` committed
107
+ - deletion of the users listed in `deleteUsers`, last
108
+
109
+ The returned `IGiteaSeedManifest` is read back from Gitea after all mutations: stable IDs, numbers, states, authors (after deletion), label and milestone associations, branch and tag commits, pull request head commits, release IDs and LFS object IDs.
110
+
111
+ `createDefaultGiteaSeedSpec()` covers behaviours an inventory reader must handle. These are observed facts of Gitea 1.27.3, asserted by the opt-in test:
112
+
113
+ - issues and pull requests share one number sequence per repository
114
+ - a deleted author's issues and comments are reattributed to the ghost user with ID `-1`
115
+ - `internal: true` marks a public repository of a **private** owner; a public repository of a `limited` organization reports `internal: false`
116
+ - private repositories are invisible (404) to anonymous callers, and a non-administrator token is refused on admin endpoints (403)
117
+
118
+ ### GitLab seed specs and manifests
119
+
120
+ `GitlabSeedBuilder.apply(spec)` validates the whole spec before the first request, then creates, as the `root` administrator unless noted:
121
+
122
+ - users (with random, discarded passwords), groups and nested subgroups with `public`, `internal` or `private` visibility, and group members
123
+ - projects in groups or personal namespaces, initialised with a README on `main`, plus project members
124
+ - files on `main`, branches created by one commit each, labels, milestones and tags
125
+ - issues (including confidential issues and incidents) and merge requests, authored through the administrator's `Sudo` header; labels, milestones, assignees and closing are applied by the administrator, and the seed fails if GitLab does not apply them
126
+ - releases, and closing of milestones marked closed
127
+ - deletion of the users listed in `deleteUsers`, last, waiting until GitLab finished moving their contributions to its ghost user
128
+
129
+ The returned `IGitlabSeedManifest` is read back after all mutations: group and project IDs, namespaces, visibility, `web_url` and clone URLs, branch and tag commits, labels, milestones, issue and merge request IDs and IIDs, states, types, confidentiality, authors (after deletion), assignees, notes and releases.
130
+
131
+ `createDefaultGitlabSeedSpec()` creates 25 projects, more than one default page of 20. These are observed facts of GitLab CE 19.4.1, asserted by the opt-in test or handled by the seed:
132
+
133
+ - issues and merge requests have separate IID sequences per project
134
+ - a deleted user's issues and notes are reattributed to the `ghost` user; the deletion runs in the background and took about 50 s
135
+ - a closed milestone assigned through the API is silently ignored (the milestone finder defaults to active milestones), so the seed closes milestones after assigning them
136
+ - group members get access to a project created in the group from a background job, so a member can see 404 for a few moments; the seed waits for access before acting as that member
137
+ - keyset pagination `Link` headers carry the fixture origin
138
+ - internal projects are 404 to anonymous callers and visible to any signed-in user; private projects and confidential issues are 404 to non-members; a non-administrator token is refused on admin endpoints (403)
139
+
140
+ ## Ownership and Cleanup
141
+
142
+ Every container, network and volume of a lifecycle carries labels under `global.foss.forgefixtures.*`: the lifecycle ID, fixture kind, machine ID (`/etc/machine-id`; a process whose system has none, as in many container images, uses a random ID fixed for its lifetime), host name, kernel boot ID, owner PID namespace (`pid:[<inode>]`), owner PID, owner process start time, creation time and expiry. Before starting, each lifecycle runs the reaper, which removes a lifecycle's resources when:
143
+
144
+ - its `maxLifetimeMs` (default two hours) has passed (`expired`)
145
+ - its owner ran on this machine, and the machine rebooted since (`owner-rebooted`)
146
+ - its owner ran on this machine in the reaper's own PID namespace, and that PID no longer exists or belongs to a process with a different start time (`owner-exited`)
147
+
148
+ A PID identifies a process only inside its PID namespace. Processes that share the Docker socket can share host name and boot ID without sharing PIDs, for example a runner in a host-network container, and machines sharing a remote daemon can share a host name. Owners with another machine ID or host name, or in another PID namespace, are therefore judged by expiry only. A process without a machine ID therefore judges every other owner by expiry only, and every other process judges its owners the same way. Resources whose labels cannot be interpreted, including labels without a machine ID or PID namespace, are reported and never removed. The reaper can also be run on its own:
149
+
150
+ ```typescript
151
+ import { ForgeFixtureReaper } from '@foss.global/forgefixtures';
152
+
153
+ const report = await new ForgeFixtureReaper().reap();
154
+ console.log(report.reapedLifecycles, report.unrecognizedResources);
155
+ ```
156
+
157
+ ## Pinned Images and Resources
158
+
159
+ | Fixture | Image | Limits |
160
+ | --- | --- | --- |
161
+ | Gitea | `gitea/gitea:1.27.3-rootless@sha256:1c17ecaead42eb3b5391553d8708103a4beb0e86edf5b9ebc1eb269c318845f2` (multi-architecture index) | 512 MiB memory without swap, 2 CPUs, 512 PIDs |
162
+ | GitLab CE | `gitlab/gitlab-ce:19.4.1-ce.0@sha256:9b33b45b9f42d176bada85ee5ecb81ddab7e506c435f44cd582206e284b2809c` (multi-architecture index) | 5 GiB memory without swap, 4 CPUs, 4096 PIDs, 256 MiB `/dev/shm` |
163
+
164
+ Images are pulled by digest and the pulled repository digest is verified; a moved tag cannot change what runs. Gitea runs as uid 1000 with SQLite, and its data directory, configuration and Git hooks live in one owned named volume that is removed with the lifecycle; hooks must be executable, which Docker's `noexec` tmpfs mounts would prevent. Only the plain-HTTP port is published, on `127.0.0.1`, on a port the lifecycle picks from the kernel's free loopback ports (a rootless daemon allocates ephemeral ports blind to host listeners); SSH is not published.
165
+
166
+ GitLab runs as root inside the container, which a rootless daemon maps to the invoking user. Its configuration, data and logs live in three owned named volumes that are removed with the lifecycle. Run one GitLab fixture at a time and check the host's available memory first: the opt-in test refuses to boot GitLab with less than 8 GiB available.
167
+
168
+ Measured on a 32-CPU host with the image already present:
169
+
170
+ - Gitea: start to ready in 7–8 s, the default seed in about 7 s, about 170 MiB of memory while idle after seeding, and a 3.6 MiB data volume.
171
+ - GitLab CE (four runs): start to ready in 253–269 s, the default seed in 63–138 s (about 50 s of it waiting for the user deletion), a token created through `gitlab-rails runner` in up to a minute. Anonymous memory is about 2.8 GiB while idle after seeding and peaked at 3.6 GiB while a token runner shared the container with Puma; a 4 GiB limit was reached through page cache (reclaimed by the kernel, no OOM kills), so the default limit is 5 GiB to leave headroom for anonymous memory peaks. After seeding the data volume holds 486 MB, the log volume 11 MB and the configuration volume 0.2 MB. The image is 3.8 GB.
172
+
173
+ ## Tests
174
+
175
+ `pnpm test` runs the Docker-free tests only. The Docker tests are opt-in:
176
+
177
+ ```bash
178
+ FORGEFIXTURES=gitea pnpm exec tstest test/test.gitea.node.ts --verbose --logfile
179
+ FORGEFIXTURES=gitea pnpm exec tstest test/test.gitea.crash.node.ts --verbose --logfile
180
+ FORGEFIXTURES=gitlab pnpm exec tstest test/test.gitlab.node.ts --verbose --logfile
181
+ ```
182
+
183
+ `FORGEFIXTURES` takes a comma list (`gitea,gitlab`) or `all`. All GitLab assertions live in one file, so GitLab boots fully once per run; that file also stops three GitLab starts at different phases, one at a time.
184
+
185
+ `test.gitea.crash.node.ts` starts a fixture in a child process that kills itself with `SIGKILL`, then proves the reaper removes the container, network and volume that outlived it.
186
+
187
+ ## License and Legal Information
188
+
189
+ This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license.md](./license.md) file.
190
+
191
+ **Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
192
+
193
+ ### Trademarks
194
+
195
+ This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
196
+
197
+ Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
198
+
199
+ ### Company Information
200
+
201
+ Task Venture Capital GmbH<br>
202
+ Registered at District Court Bremen HRB 35230 HB, Germany
203
+
204
+ For any legal inquiries or further information, please contact us via email at hello@task.vc.
205
+
206
+ By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
@@ -0,0 +1,8 @@
1
+ /**
2
+ * autocreated commitinfo by @push.rocks/commitinfo
3
+ */
4
+ export const commitinfo = {
5
+ name: '@foss.global/forgefixtures',
6
+ version: '0.2.0',
7
+ description: 'Disposable, digest-pinned Gitea and GitLab CE fixtures with owned cleanup, local TLS and ground-truth seed data for forge integration tests.'
8
+ }