@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.
- package/.smartconfig.json +49 -0
- package/changelog.md +17 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/classes.certificateauthority.d.ts +22 -0
- package/dist_ts/classes.certificateauthority.js +93 -0
- package/dist_ts/classes.containerlifecycle.d.ts +88 -0
- package/dist_ts/classes.containerlifecycle.js +383 -0
- package/dist_ts/classes.giteafixture.d.ts +41 -0
- package/dist_ts/classes.giteafixture.js +182 -0
- package/dist_ts/classes.giteaseed.d.ts +13 -0
- package/dist_ts/classes.giteaseed.js +432 -0
- package/dist_ts/classes.gitlabfixture.d.ts +49 -0
- package/dist_ts/classes.gitlabfixture.js +237 -0
- package/dist_ts/classes.gitlabseed.d.ts +13 -0
- package/dist_ts/classes.gitlabseed.js +466 -0
- package/dist_ts/classes.httpclient.d.ts +53 -0
- package/dist_ts/classes.httpclient.js +116 -0
- package/dist_ts/classes.reaper.d.ts +25 -0
- package/dist_ts/classes.reaper.js +102 -0
- package/dist_ts/classes.tlsterminator.d.ts +21 -0
- package/dist_ts/classes.tlsterminator.js +131 -0
- package/dist_ts/constants.d.ts +22 -0
- package/dist_ts/constants.js +30 -0
- package/dist_ts/giteaseed.default.d.ts +10 -0
- package/dist_ts/giteaseed.default.js +87 -0
- package/dist_ts/gitlabseed.default.d.ts +12 -0
- package/dist_ts/gitlabseed.default.js +84 -0
- package/dist_ts/index.d.ts +17 -0
- package/dist_ts/index.js +17 -0
- package/dist_ts/interfaces.d.ts +92 -0
- package/dist_ts/interfaces.giteaseed.d.ts +182 -0
- package/dist_ts/interfaces.giteaseed.js +2 -0
- package/dist_ts/interfaces.gitlabseed.d.ts +183 -0
- package/dist_ts/interfaces.gitlabseed.js +2 -0
- package/dist_ts/interfaces.js +2 -0
- package/dist_ts/ownership.d.ts +29 -0
- package/dist_ts/ownership.js +140 -0
- package/dist_ts/plugins.d.ts +13 -0
- package/dist_ts/plugins.js +18 -0
- package/dist_ts/responses.d.ts +7 -0
- package/dist_ts/responses.js +30 -0
- package/license.md +21 -0
- package/package.json +65 -0
- package/readme.md +206 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/classes.certificateauthority.ts +117 -0
- package/ts/classes.containerlifecycle.ts +432 -0
- package/ts/classes.giteafixture.ts +205 -0
- package/ts/classes.giteaseed.ts +486 -0
- package/ts/classes.gitlabfixture.ts +258 -0
- package/ts/classes.gitlabseed.ts +502 -0
- package/ts/classes.httpclient.ts +156 -0
- package/ts/classes.reaper.ts +126 -0
- package/ts/classes.tlsterminator.ts +136 -0
- package/ts/constants.ts +35 -0
- package/ts/giteaseed.default.ts +88 -0
- package/ts/gitlabseed.default.ts +86 -0
- package/ts/index.ts +17 -0
- package/ts/interfaces.giteaseed.ts +130 -0
- package/ts/interfaces.gitlabseed.ts +135 -0
- package/ts/interfaces.ts +94 -0
- package/ts/ownership.ts +160 -0
- package/ts/plugins.ts +23 -0
- 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
|
+
}
|