promigence 0.1.0-beta.12
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/NOTICE +14 -0
- package/README.md +88 -0
- package/dist/account.js +850 -0
- package/dist/agent.js +374 -0
- package/dist/argv.js +159 -0
- package/dist/bin.js +332 -0
- package/dist/bundle.js +133 -0
- package/dist/capacity.js +69 -0
- package/dist/commands.d.ts +110 -0
- package/dist/commands.js +2675 -0
- package/dist/connectreq.js +125 -0
- package/dist/credfile.js +188 -0
- package/dist/credits.js +197 -0
- package/dist/devcontainer.d.ts +99 -0
- package/dist/devcontainer.js +744 -0
- package/dist/device.js +129 -0
- package/dist/dirsnap.js +378 -0
- package/dist/errfix.js +262 -0
- package/dist/generated.d.ts +35 -0
- package/dist/generated.js +307 -0
- package/dist/github.js +307 -0
- package/dist/gitlab.js +210 -0
- package/dist/guide.d.ts +19 -0
- package/dist/guide.js +366 -0
- package/dist/handlers.js +511 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +7 -0
- package/dist/init.d.ts +40 -0
- package/dist/init.js +352 -0
- package/dist/install.d.ts +23 -0
- package/dist/install.js +127 -0
- package/dist/io.d.ts +37 -0
- package/dist/io.js +318 -0
- package/dist/live.js +469 -0
- package/dist/login.js +1063 -0
- package/dist/mcp.js +889 -0
- package/dist/money.js +5 -0
- package/dist/orgs.js +475 -0
- package/dist/pricing.d.ts +52 -0
- package/dist/pricing.js +181 -0
- package/dist/report.js +132 -0
- package/dist/routes.js +135 -0
- package/dist/runners-github.js +257 -0
- package/dist/runners-gitlab.js +125 -0
- package/dist/runners-spec.js +137 -0
- package/dist/runners.js +1458 -0
- package/dist/sdk/blake3.d.ts +10 -0
- package/dist/sdk/blake3.js +111 -0
- package/dist/sdk/bundle.d.ts +96 -0
- package/dist/sdk/bundle.js +227 -0
- package/dist/sdk/ca-guard.js +166 -0
- package/dist/sdk/caller.d.ts +73 -0
- package/dist/sdk/caller.js +47 -0
- package/dist/sdk/client.d.ts +3015 -0
- package/dist/sdk/client.js +2583 -0
- package/dist/sdk/command-errors.d.ts +41 -0
- package/dist/sdk/command-errors.js +36 -0
- package/dist/sdk/context.d.ts +119 -0
- package/dist/sdk/context.js +588 -0
- package/dist/sdk/core.d.ts +285 -0
- package/dist/sdk/core.js +266 -0
- package/dist/sdk/credstore.d.ts +90 -0
- package/dist/sdk/credstore.js +206 -0
- package/dist/sdk/dispatcher.js +86 -0
- package/dist/sdk/github.d.ts +158 -0
- package/dist/sdk/github.js +78 -0
- package/dist/sdk/gitlab.d.ts +104 -0
- package/dist/sdk/gitlab.js +20 -0
- package/dist/sdk/handles.d.ts +82 -0
- package/dist/sdk/handles.js +135 -0
- package/dist/sdk/hint.js +7 -0
- package/dist/sdk/http2.js +1921 -0
- package/dist/sdk/index.d.ts +34 -0
- package/dist/sdk/index.js +15 -0
- package/dist/sdk/keyed.gen.js +7 -0
- package/dist/sdk/models.gen.d.ts +6162 -0
- package/dist/sdk/models.gen.js +1 -0
- package/dist/sdk/proxy.js +249 -0
- package/dist/sdk/pty.d.ts +171 -0
- package/dist/sdk/pty.js +496 -0
- package/dist/sdk/sandbox.d.ts +537 -0
- package/dist/sdk/sandbox.js +725 -0
- package/dist/sdk/ssh.d.ts +25 -0
- package/dist/sdk/ssh.js +114 -0
- package/dist/sdk/trace.js +80 -0
- package/dist/sdk/transport-worker.js +466 -0
- package/dist/sdk/warm.js +40 -0
- package/dist/ssh.js +267 -0
- package/dist/surface.d.ts +95 -0
- package/dist/surface.js +1452 -0
- package/dist/templates.js +53 -0
- package/dist/term.js +50 -0
- package/dist/timing.js +71 -0
- package/dist/untar.js +324 -0
- package/dist/uploadguard.js +75 -0
- package/llms.txt +209 -0
- package/package.json +52 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { RUNNER_TIERS, isRunnerTier } from "./runners-spec.js";
|
|
2
|
+
export const GITHUB_API_DEFAULT = "https://api.github.com";
|
|
3
|
+
export const GITHUB_USER_AGENT = "ci-runner-pool/1";
|
|
4
|
+
export const DEFAULT_RUNNER_LABELS = ["self-hosted", "linux", "x64"];
|
|
5
|
+
export const DEFAULT_POOL_LABEL = "promigence";
|
|
6
|
+
export const tierLabel = (t) => `${DEFAULT_POOL_LABEL}-${t}`;
|
|
7
|
+
const TIER_BY_LABEL = new Map(RUNNER_TIERS.map((t) => [tierLabel(t), t]));
|
|
8
|
+
export function redact(text, secrets) {
|
|
9
|
+
let out = text;
|
|
10
|
+
for (const s of secrets) {
|
|
11
|
+
if (!s || s.length < 6)
|
|
12
|
+
continue;
|
|
13
|
+
out = out.split(s).join("[redacted]");
|
|
14
|
+
const head = s.slice(0, 12);
|
|
15
|
+
if (head.length >= 8)
|
|
16
|
+
out = out.split(head).join("[redacted]");
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
export class GitHubError extends Error {
|
|
21
|
+
status;
|
|
22
|
+
retryAfterMs;
|
|
23
|
+
constructor(status, message, retryAfterMs) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.status = status;
|
|
26
|
+
this.retryAfterMs = retryAfterMs;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export class GitHubApi {
|
|
30
|
+
apiUrl;
|
|
31
|
+
#token;
|
|
32
|
+
#fetch;
|
|
33
|
+
#now;
|
|
34
|
+
#etags = new Map();
|
|
35
|
+
stats = { requests: 0, notModified: 0, rateLimited: 0 };
|
|
36
|
+
backoffUntil = 0;
|
|
37
|
+
remaining;
|
|
38
|
+
#secondaryBackoffMs = 0;
|
|
39
|
+
constructor(opts) {
|
|
40
|
+
this.apiUrl = (opts.apiUrl ?? GITHUB_API_DEFAULT).replace(/\/+$/, "");
|
|
41
|
+
this.#token = opts.token;
|
|
42
|
+
this.#fetch = opts.fetch ?? fetch;
|
|
43
|
+
this.#now = opts.now ?? Date.now;
|
|
44
|
+
}
|
|
45
|
+
get secret() {
|
|
46
|
+
return this.#token;
|
|
47
|
+
}
|
|
48
|
+
#headers(extra = {}) {
|
|
49
|
+
return {
|
|
50
|
+
accept: "application/vnd.github+json",
|
|
51
|
+
"x-github-api-version": "2022-11-28",
|
|
52
|
+
"user-agent": GITHUB_USER_AGENT,
|
|
53
|
+
authorization: `Bearer ${this.#token}`,
|
|
54
|
+
...extra,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
async #send(method, path, body) {
|
|
58
|
+
const now = this.#now();
|
|
59
|
+
if (now < this.backoffUntil) {
|
|
60
|
+
throw new GitHubError(429, `GitHub rate limit: waiting ${Math.ceil((this.backoffUntil - now) / 1000)} s`, this.backoffUntil - now);
|
|
61
|
+
}
|
|
62
|
+
const url = this.apiUrl + path;
|
|
63
|
+
const cached = method === "GET" ? this.#etags.get(url) : undefined;
|
|
64
|
+
this.stats.requests += 1;
|
|
65
|
+
let res;
|
|
66
|
+
try {
|
|
67
|
+
res = await this.#fetch(url, {
|
|
68
|
+
method,
|
|
69
|
+
headers: this.#headers({
|
|
70
|
+
...(cached ? { "if-none-match": cached.etag } : {}),
|
|
71
|
+
...(body === undefined ? {} : { "content-type": "application/json" }),
|
|
72
|
+
}),
|
|
73
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
throw new GitHubError(0, `GitHub API unreachable (${e.message})`);
|
|
78
|
+
}
|
|
79
|
+
const rem = res.headers.get("x-ratelimit-remaining");
|
|
80
|
+
if (rem !== null && rem !== "")
|
|
81
|
+
this.remaining = Number(rem);
|
|
82
|
+
if (res.status === 304 && cached) {
|
|
83
|
+
this.stats.notModified += 1;
|
|
84
|
+
this.#secondaryBackoffMs = 0;
|
|
85
|
+
return { status: 200, json: cached.body };
|
|
86
|
+
}
|
|
87
|
+
const text = await res.text();
|
|
88
|
+
let json = undefined;
|
|
89
|
+
try {
|
|
90
|
+
json = text ? JSON.parse(text) : undefined;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
json = undefined;
|
|
94
|
+
}
|
|
95
|
+
if (res.status === 403 || res.status === 429) {
|
|
96
|
+
const msg = String(json?.message ?? "");
|
|
97
|
+
const retryAfter = res.headers.get("retry-after");
|
|
98
|
+
const reset = res.headers.get("x-ratelimit-reset");
|
|
99
|
+
let waitMs;
|
|
100
|
+
if (retryAfter !== null && retryAfter !== "" && Number.isFinite(Number(retryAfter)))
|
|
101
|
+
waitMs = Number(retryAfter) * 1000;
|
|
102
|
+
else if (rem === "0" && reset !== null && Number.isFinite(Number(reset)))
|
|
103
|
+
waitMs = Math.max(1000, Number(reset) * 1000 - this.#now() + 1000);
|
|
104
|
+
else if (/secondary rate limit|abuse/i.test(msg)) {
|
|
105
|
+
this.#secondaryBackoffMs = Math.min(300_000, this.#secondaryBackoffMs ? this.#secondaryBackoffMs * 2 : 60_000);
|
|
106
|
+
waitMs = this.#secondaryBackoffMs;
|
|
107
|
+
}
|
|
108
|
+
if (waitMs !== undefined || res.status === 429) {
|
|
109
|
+
const wait = waitMs ?? 60_000;
|
|
110
|
+
this.stats.rateLimited += 1;
|
|
111
|
+
this.backoffUntil = this.#now() + wait;
|
|
112
|
+
throw new GitHubError(429, `GitHub rate limit: waiting ${Math.ceil(wait / 1000)} s`, wait);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (res.status >= 400) {
|
|
116
|
+
const msg = redact(String(json?.message ?? res.statusText ?? ""), [this.#token]);
|
|
117
|
+
throw new GitHubError(res.status, msg);
|
|
118
|
+
}
|
|
119
|
+
this.#secondaryBackoffMs = 0;
|
|
120
|
+
if (method === "GET") {
|
|
121
|
+
const etag = res.headers.get("etag");
|
|
122
|
+
if (etag)
|
|
123
|
+
this.#etags.set(url, { etag, body: json });
|
|
124
|
+
}
|
|
125
|
+
return { status: res.status, json };
|
|
126
|
+
}
|
|
127
|
+
get(path) {
|
|
128
|
+
return this.#send("GET", path);
|
|
129
|
+
}
|
|
130
|
+
post(path, body) {
|
|
131
|
+
return this.#send("POST", path, body);
|
|
132
|
+
}
|
|
133
|
+
delete(path) {
|
|
134
|
+
return this.#send("DELETE", path);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const enc = encodeURIComponent;
|
|
138
|
+
const repoPath = (repo) => {
|
|
139
|
+
const [o, r] = repo.split("/");
|
|
140
|
+
return `/repos/${enc(o)}/${enc(r)}`;
|
|
141
|
+
};
|
|
142
|
+
const scopePath = (s) => (s.kind === "repo" ? repoPath(s.name) : `/orgs/${enc(s.name)}`);
|
|
143
|
+
export async function checkAccess(api, scope) {
|
|
144
|
+
await api.get(`${scopePath(scope)}/actions/runners?per_page=1`);
|
|
145
|
+
await api.post(`${scopePath(scope)}/actions/runners/registration-token`, {});
|
|
146
|
+
if (scope.kind === "repo")
|
|
147
|
+
await api.get(`${scopePath(scope)}/actions/runs?per_page=1`);
|
|
148
|
+
}
|
|
149
|
+
export async function runnerGroupId(api, org, name) {
|
|
150
|
+
if (name === undefined || name.toLowerCase() === "default")
|
|
151
|
+
return 1;
|
|
152
|
+
const r = await api.get(`/orgs/${enc(org)}/actions/runner-groups?per_page=100`);
|
|
153
|
+
const groups = (r.json?.runner_groups ?? []);
|
|
154
|
+
const g = groups.find((x) => x.name.toLowerCase() === name.toLowerCase());
|
|
155
|
+
if (!g)
|
|
156
|
+
throw new GitHubError(404, `no runner group named '${name}' in ${org} (it has: ${groups.map((x) => x.name).join(", ") || "none"})`);
|
|
157
|
+
return g.id;
|
|
158
|
+
}
|
|
159
|
+
export async function orgRepos(api, org, limit = 100) {
|
|
160
|
+
const r = await api.get(`/orgs/${enc(org)}/repos?sort=pushed&direction=desc&per_page=${Math.min(100, limit)}`);
|
|
161
|
+
const repos = r.json ?? [];
|
|
162
|
+
return { repos: repos.filter((x) => !x.archived && !x.disabled).map((x) => x.full_name).slice(0, limit), more: repos.length >= Math.min(100, limit) };
|
|
163
|
+
}
|
|
164
|
+
export async function queuedJobs(api, repos, onMissing) {
|
|
165
|
+
const out = [];
|
|
166
|
+
for (const repo of repos) {
|
|
167
|
+
const runIds = new Set();
|
|
168
|
+
try {
|
|
169
|
+
for (const status of ["queued", "in_progress"]) {
|
|
170
|
+
const r = await api.get(`${repoPath(repo)}/actions/runs?status=${status}&per_page=100`);
|
|
171
|
+
for (const run of (r.json?.workflow_runs ?? []))
|
|
172
|
+
runIds.add(run.id);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
if (onMissing && e instanceof GitHubError && e.status === 404) {
|
|
177
|
+
onMissing(repo);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
throw e;
|
|
181
|
+
}
|
|
182
|
+
for (const id of runIds) {
|
|
183
|
+
const r = await api.get(`${repoPath(repo)}/actions/runs/${id}/jobs?filter=latest&per_page=100`);
|
|
184
|
+
for (const j of (r.json?.jobs ?? [])) {
|
|
185
|
+
if (j.status === "queued")
|
|
186
|
+
out.push({ ...j, repo });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
export async function mintJitConfig(api, scope, opts) {
|
|
193
|
+
const r = await api.post(`${scopePath(scope)}/actions/runners/generate-jitconfig`, {
|
|
194
|
+
name: opts.name, runner_group_id: opts.groupId, labels: opts.labels, work_folder: "_work",
|
|
195
|
+
});
|
|
196
|
+
const j = r.json;
|
|
197
|
+
if (!j?.encoded_jit_config || typeof j.runner?.id !== "number") {
|
|
198
|
+
throw new GitHubError(r.status, "GitHub answered generate-jitconfig without a runner configuration");
|
|
199
|
+
}
|
|
200
|
+
return { runnerId: j.runner.id, name: j.runner.name ?? opts.name, encoded: j.encoded_jit_config };
|
|
201
|
+
}
|
|
202
|
+
export async function getRunner(api, scope, id) {
|
|
203
|
+
try {
|
|
204
|
+
return (await api.get(`${scopePath(scope)}/actions/runners/${id}`)).json;
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
if (e instanceof GitHubError && e.status === 404)
|
|
208
|
+
return undefined;
|
|
209
|
+
throw e;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
export async function findRunnerByName(api, scope, name) {
|
|
213
|
+
const r = await api.get(`${scopePath(scope)}/actions/runners?name=${encodeURIComponent(name)}&per_page=100`);
|
|
214
|
+
return (r.json?.runners ?? []).find((x) => x.name === name);
|
|
215
|
+
}
|
|
216
|
+
export async function deleteRunner(api, scope, id) {
|
|
217
|
+
try {
|
|
218
|
+
await api.delete(`${scopePath(scope)}/actions/runners/${id}`);
|
|
219
|
+
return "deleted";
|
|
220
|
+
}
|
|
221
|
+
catch (e) {
|
|
222
|
+
if (e instanceof GitHubError && e.status === 404)
|
|
223
|
+
return "gone";
|
|
224
|
+
throw e;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const norm = (l) => l.trim().toLowerCase();
|
|
228
|
+
export function routeJob(labels, policy) {
|
|
229
|
+
const custom = [...new Set(labels.map(norm))].filter((l) => !DEFAULT_RUNNER_LABELS.includes(l));
|
|
230
|
+
if (custom.length === 0)
|
|
231
|
+
return undefined;
|
|
232
|
+
const tiers = custom.filter((l) => TIER_BY_LABEL.has(l));
|
|
233
|
+
if (tiers.length > 1)
|
|
234
|
+
return undefined;
|
|
235
|
+
if (!custom.every((l) => policy.pool.includes(l) || TIER_BY_LABEL.has(l)))
|
|
236
|
+
return undefined;
|
|
237
|
+
const tier = tiers.length ? TIER_BY_LABEL.get(tiers[0]) : policy.defaultTier;
|
|
238
|
+
const runnerLabels = [...new Set([...custom, tierLabel(tier)])].sort();
|
|
239
|
+
return { tier, labels: runnerLabels, key: runnerLabels.join(",") };
|
|
240
|
+
}
|
|
241
|
+
export function warmLabels(policy) {
|
|
242
|
+
return [...new Set([...policy.pool, tierLabel(policy.defaultTier)])].sort();
|
|
243
|
+
}
|
|
244
|
+
export function covers(runnerLabels, jobLabels) {
|
|
245
|
+
const have = new Set([...runnerLabels.map(norm), ...DEFAULT_RUNNER_LABELS]);
|
|
246
|
+
return jobLabels.map(norm).every((l) => have.has(l));
|
|
247
|
+
}
|
|
248
|
+
export function parsePoolLabels(spec) {
|
|
249
|
+
if (spec === undefined)
|
|
250
|
+
return [DEFAULT_POOL_LABEL];
|
|
251
|
+
const labels = spec.split(",").map(norm).filter(Boolean);
|
|
252
|
+
return [...new Set(labels)];
|
|
253
|
+
}
|
|
254
|
+
export function validLabel(l) {
|
|
255
|
+
return /^[a-z0-9][a-z0-9._-]{0,99}$/.test(l);
|
|
256
|
+
}
|
|
257
|
+
export { isRunnerTier };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { GitHubError as HttpError, redact } from "./runners-github.js";
|
|
2
|
+
export const GITLAB_URL_DEFAULT = "https://gitlab.com";
|
|
3
|
+
export const GITLAB_EXECUTORS = ["docker", "shell"];
|
|
4
|
+
export { GITLAB_DEFAULT_JOB_IMAGE as GITLAB_DEFAULT_IMAGE } from "./runners-spec.js";
|
|
5
|
+
const tomlString = (s) => JSON.stringify(s);
|
|
6
|
+
export function gitlabConfigToml(cfg) {
|
|
7
|
+
const lines = [
|
|
8
|
+
"concurrent = 1",
|
|
9
|
+
"check_interval = 3",
|
|
10
|
+
"shutdown_timeout = 30",
|
|
11
|
+
'log_format = "text"',
|
|
12
|
+
"",
|
|
13
|
+
"[[runners]]",
|
|
14
|
+
' name = "ci"',
|
|
15
|
+
` url = ${tomlString(cfg.url)}`,
|
|
16
|
+
` token = ${tomlString(cfg.token)}`,
|
|
17
|
+
` executor = ${tomlString(cfg.executor)}`,
|
|
18
|
+
' builds_dir = "/builds"',
|
|
19
|
+
' cache_dir = "/cache"',
|
|
20
|
+
];
|
|
21
|
+
if (cfg.executor === "docker") {
|
|
22
|
+
lines.push(" [runners.docker]", ` image = ${tomlString(cfg.defaultImage)}`, " privileged = true", ' pull_policy = ["if-not-present"]', ' volumes = ["/cache"]');
|
|
23
|
+
}
|
|
24
|
+
return lines.join("\n") + "\n";
|
|
25
|
+
}
|
|
26
|
+
export function systemId(seed, position) {
|
|
27
|
+
let h = 2166136261 >>> 0;
|
|
28
|
+
for (const ch of `${seed}:${position}`)
|
|
29
|
+
h = Math.imul(h ^ ch.charCodeAt(0), 16777619) >>> 0;
|
|
30
|
+
let out = "";
|
|
31
|
+
let x = h;
|
|
32
|
+
const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
33
|
+
for (let i = 0; i < 12; i++) {
|
|
34
|
+
out += alphabet[x % alphabet.length];
|
|
35
|
+
x = Math.imul(x ^ (x >>> 13), 2654435761) >>> 0;
|
|
36
|
+
}
|
|
37
|
+
return `s_${out}`;
|
|
38
|
+
}
|
|
39
|
+
export class GitlabApi {
|
|
40
|
+
url;
|
|
41
|
+
#token;
|
|
42
|
+
#fetch;
|
|
43
|
+
stats = { requests: 0 };
|
|
44
|
+
backoffUntil = 0;
|
|
45
|
+
constructor(url, token, f = fetch) {
|
|
46
|
+
this.url = url;
|
|
47
|
+
this.#token = token;
|
|
48
|
+
this.#fetch = f;
|
|
49
|
+
}
|
|
50
|
+
get secret() {
|
|
51
|
+
return this.#token;
|
|
52
|
+
}
|
|
53
|
+
async #req(method, path, body) {
|
|
54
|
+
if (Date.now() < this.backoffUntil) {
|
|
55
|
+
throw new HttpError(429, `GitLab rate limit: waiting ${Math.ceil((this.backoffUntil - Date.now()) / 1000)} s`, this.backoffUntil - Date.now());
|
|
56
|
+
}
|
|
57
|
+
this.stats.requests += 1;
|
|
58
|
+
let res;
|
|
59
|
+
try {
|
|
60
|
+
res = await this.#fetch(`${this.url.replace(/\/+$/, "")}/api/v4${path}`, {
|
|
61
|
+
method,
|
|
62
|
+
headers: {
|
|
63
|
+
"private-token": this.#token,
|
|
64
|
+
"user-agent": "ci-runner-pool/1",
|
|
65
|
+
...(body === undefined ? {} : { "content-type": "application/json" }),
|
|
66
|
+
},
|
|
67
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
throw new HttpError(0, `GitLab API unreachable (${e.message})`);
|
|
72
|
+
}
|
|
73
|
+
const text = await res.text();
|
|
74
|
+
let json;
|
|
75
|
+
try {
|
|
76
|
+
json = text ? JSON.parse(text) : undefined;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
json = undefined;
|
|
80
|
+
}
|
|
81
|
+
if (res.status === 429) {
|
|
82
|
+
const ra = Number(res.headers.get("retry-after") ?? "60");
|
|
83
|
+
const wait = (Number.isFinite(ra) && ra > 0 ? ra : 60) * 1000;
|
|
84
|
+
this.backoffUntil = Date.now() + wait;
|
|
85
|
+
throw new HttpError(429, `GitLab rate limit: waiting ${Math.ceil(wait / 1000)} s`, wait);
|
|
86
|
+
}
|
|
87
|
+
if (res.status >= 400) {
|
|
88
|
+
const m = json;
|
|
89
|
+
const msg = typeof m?.message === "string" ? m.message : typeof m?.error === "string" ? m.error : JSON.stringify(m?.message ?? res.statusText);
|
|
90
|
+
throw new HttpError(res.status, redact(String(msg), [this.#token]));
|
|
91
|
+
}
|
|
92
|
+
return json;
|
|
93
|
+
}
|
|
94
|
+
async runner(runnerToken) {
|
|
95
|
+
const v = await this.#req("POST", "/runners/verify", { token: runnerToken });
|
|
96
|
+
if (typeof v?.id !== "number")
|
|
97
|
+
throw new HttpError(404, "GitLab did not recognise the runner authentication token");
|
|
98
|
+
const r = await this.#req("GET", `/runners/${v.id}`);
|
|
99
|
+
return {
|
|
100
|
+
id: r.id,
|
|
101
|
+
tags: (r.tag_list ?? []).map((t) => t.toLowerCase()),
|
|
102
|
+
runUntagged: r.run_untagged !== false,
|
|
103
|
+
projects: (r.projects ?? []).map((p) => p.id),
|
|
104
|
+
groups: (r.groups ?? []).map((g) => g.id),
|
|
105
|
+
type: r.runner_type ?? "unknown",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
async groupProjects(group, limit = 100) {
|
|
109
|
+
const r = await this.#req("GET", `/groups/${group}/projects?include_subgroups=true&archived=false&per_page=${Math.min(100, limit)}&order_by=last_activity_at`);
|
|
110
|
+
return (r ?? []).map((p) => p.id).slice(0, limit);
|
|
111
|
+
}
|
|
112
|
+
async pendingJobs(project) {
|
|
113
|
+
const r = await this.#req("GET", `/projects/${project}/jobs?scope[]=pending&per_page=100`);
|
|
114
|
+
return (r ?? []).map((j) => ({ id: j.id, name: j.name, tags: (j.tag_list ?? []).map((t) => t.toLowerCase()), project, ...(j.web_url ? { web_url: j.web_url } : {}) }));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export function runnerTakes(runner, job) {
|
|
118
|
+
if (job.tags.length === 0)
|
|
119
|
+
return runner.runUntagged;
|
|
120
|
+
return job.tags.every((t) => runner.tags.includes(t));
|
|
121
|
+
}
|
|
122
|
+
export function receivedJobId(line) {
|
|
123
|
+
const m = /received\b.*\bjob=(\d+)/.exec(line) ?? /Checking for jobs\.\.\. received.*job=(\d+)/.exec(line);
|
|
124
|
+
return m ? Number(m[1]) : undefined;
|
|
125
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
export const RUNNER_BASE_IMAGE = "docker.io/library/ubuntu@sha256:a853f94d226358a79c740cfc7bce0c289748f3fe3488d921d038ccd752c61b60";
|
|
2
|
+
export const ACTIONS_RUNNER_VERSION = "2.337.0";
|
|
3
|
+
export const ACTIONS_RUNNER_SHA256 = "70920811a4f8ad4328818682bca5c6469c1c942fab52448868071d0063816613";
|
|
4
|
+
export const GITLAB_RUNNER_VERSION = "19.4.1";
|
|
5
|
+
export const GITLAB_RUNNER_SHA256 = "5a159b36c5ff54fda9248e00bb0cdfb62c1112ab3741f5282c04a41e94da1306";
|
|
6
|
+
export const GITLAB_RUNNER_DEB_SHA256 = "47d2fd991e0b17af6f85af8fe3cc0cc429aae782d4f9982724c1f11a4de4a47c";
|
|
7
|
+
export const RUNNER_NODE_VERSION = "v24.21.0";
|
|
8
|
+
export const RUNNER_NODE_SHA256 = "fd8e59d5a511510f6a298afb548f18c7d2b1be404d8b4a27d94fbe49f56cb2d6";
|
|
9
|
+
export const GITLAB_HELPER_IMAGE = `registry.gitlab.com/gitlab-org/gitlab-runner/gitlab-runner-helper:x86_64-v${GITLAB_RUNNER_VERSION}`;
|
|
10
|
+
export const GITLAB_DEFAULT_JOB_IMAGE = "ubuntu:24.04";
|
|
11
|
+
export const RUNNER_SPEC_REV = { github: 5, gitlab: 4 };
|
|
12
|
+
export const RUNNER_TIERS = ["small", "medium", "large"];
|
|
13
|
+
export const GITHUB_CONFIG_PATH = "/root/.ci-runner-config";
|
|
14
|
+
export const GITLAB_CONFIG_PATH = "/etc/gitlab-runner/config.toml";
|
|
15
|
+
export const GITHUB_LAUNCHER = "/usr/local/bin/ci-runner-github";
|
|
16
|
+
export const GITLAB_LAUNCHER = "/usr/local/bin/ci-runner-gitlab";
|
|
17
|
+
export const GITHUB_RUNNER_ROOT = "/home/runner/actions-runner";
|
|
18
|
+
export const USER_DOCKER_CLI = "/usr/local/libexec/ci-docker/docker";
|
|
19
|
+
export const DOCKER_STARTING = "/run/ci-docker-starting";
|
|
20
|
+
export function runnerAlias(kind, tier) {
|
|
21
|
+
const version = kind === "github" ? ACTIONS_RUNNER_VERSION : GITLAB_RUNNER_VERSION;
|
|
22
|
+
const rev = RUNNER_SPEC_REV[kind];
|
|
23
|
+
return `ci-${kind}-runner-${tier}-${version}${rev > 1 ? `-r${rev}` : ""}`;
|
|
24
|
+
}
|
|
25
|
+
const PACKAGES = [
|
|
26
|
+
"ca-certificates", "curl", "wget", "git", "git-lfs", "jq", "unzip", "zip", "xz-utils", "zstd", "bzip2", "tar", "gzip",
|
|
27
|
+
"build-essential", "pkg-config", "python3", "python3-pip", "python3-venv", "sudo", "lsb-release",
|
|
28
|
+
"gnupg", "openssh-client", "rsync", "tzdata", "locales",
|
|
29
|
+
];
|
|
30
|
+
const APT_STEP = "set -eu; export DEBIAN_FRONTEND=noninteractive; apt-get update -qq; " +
|
|
31
|
+
`apt-get install -y -qq --no-install-recommends ${PACKAGES.join(" ")}; ` +
|
|
32
|
+
"rm -rf /var/lib/apt/lists/*";
|
|
33
|
+
export const GITHUB_LAUNCHER_SCRIPT = `#!/bin/sh
|
|
34
|
+
set -eu
|
|
35
|
+
CFG=${GITHUB_CONFIG_PATH}
|
|
36
|
+
[ -s "$CFG" ] || { echo "ci: no runner configuration at $CFG" >&2; exit 64; }
|
|
37
|
+
ACTIONS_RUNNER_INPUT_JITCONFIG=$(cat "$CFG")
|
|
38
|
+
rm -f "$CFG"
|
|
39
|
+
export ACTIONS_RUNNER_INPUT_JITCONFIG
|
|
40
|
+
if [ -x ${USER_DOCKER_CLI} ]; then
|
|
41
|
+
: > ${DOCKER_STARTING}
|
|
42
|
+
( docker info >/dev/null 2>&1 || echo "ci: the Docker daemon did not start; jobs that use containers or services will fail" >&2
|
|
43
|
+
rm -f ${DOCKER_STARTING} ) &
|
|
44
|
+
fi
|
|
45
|
+
cd ${GITHUB_RUNNER_ROOT}
|
|
46
|
+
export HOME=/home/runner USER=runner LOGNAME=runner RUNNER_MANUALLY_TRAP_SIG=1 DOTNET_CLI_TELEMETRY_OPTOUT=1 DOTNET_NOLOGO=1
|
|
47
|
+
echo "ci: runner starting"
|
|
48
|
+
exec setpriv --reuid=runner --regid=runner --init-groups ./run.sh
|
|
49
|
+
`;
|
|
50
|
+
export const GITLAB_LAUNCHER_SCRIPT = `#!/bin/sh
|
|
51
|
+
set -eu
|
|
52
|
+
CFG=${GITLAB_CONFIG_PATH}
|
|
53
|
+
[ -s "$CFG" ] || { echo "ci: no runner configuration at $CFG" >&2; exit 64; }
|
|
54
|
+
chmod 0600 "$CFG"
|
|
55
|
+
if grep -q '^[[:space:]]*executor[[:space:]]*=[[:space:]]*"docker"' "$CFG"; then
|
|
56
|
+
docker info >/dev/null 2>&1 || { echo "ci: the Docker daemon did not start; the docker executor cannot run jobs" >&2; exit 65; }
|
|
57
|
+
fi
|
|
58
|
+
mkdir -p /builds /cache
|
|
59
|
+
if [ -n "\${CI_SYSTEM_ID:-}" ]; then printf '%s\\n' "$CI_SYSTEM_ID" > /etc/gitlab-runner/.runner_system_id; fi
|
|
60
|
+
set -- run-single --config "$CFG" --runner ci --max-builds 1
|
|
61
|
+
if [ -n "\${CI_WAIT_TIMEOUT:-}" ]; then set -- "$@" --wait-timeout "$CI_WAIT_TIMEOUT"; fi
|
|
62
|
+
echo "ci: runner starting"
|
|
63
|
+
exec gitlab-runner --log-format text "$@"
|
|
64
|
+
`;
|
|
65
|
+
export const DOCKER_WRAPPER_SCRIPT = `#!/bin/sh
|
|
66
|
+
if [ "$(id -u)" = 0 ] && [ -x /.promigence/docker/docker ]; then exec /.promigence/docker/docker "$@"; fi
|
|
67
|
+
if [ ! -S /var/run/docker.sock ] && [ -e ${DOCKER_STARTING} ]; then
|
|
68
|
+
i=0
|
|
69
|
+
while [ ! -S /var/run/docker.sock ] && [ -e ${DOCKER_STARTING} ] && [ "$i" -lt 600 ]; do sleep 0.1; i=$((i + 1)); done
|
|
70
|
+
fi
|
|
71
|
+
exec ${USER_DOCKER_CLI} "$@"
|
|
72
|
+
`;
|
|
73
|
+
export const GITHUB_RUNNER_ENV = "LANG=C.UTF-8\nImageOS=ubuntu24\n";
|
|
74
|
+
const b64 = (s) => Buffer.from(s, "utf8").toString("base64");
|
|
75
|
+
function writeStep(path, content, mode) {
|
|
76
|
+
return `printf '%s' '${b64(content)}' | base64 -d > ${path}; chmod ${mode} ${path}`;
|
|
77
|
+
}
|
|
78
|
+
export function runnerBuildSteps(s) {
|
|
79
|
+
return [...s.setup, ...s.platform_setup];
|
|
80
|
+
}
|
|
81
|
+
export function runnerImageSpec(kind, tier) {
|
|
82
|
+
if (kind === "github") {
|
|
83
|
+
const url = `https://github.com/actions/runner/releases/download/v${ACTIONS_RUNNER_VERSION}/actions-runner-linux-x64-${ACTIONS_RUNNER_VERSION}.tar.gz`;
|
|
84
|
+
const nodeUrl = `https://nodejs.org/dist/${RUNNER_NODE_VERSION}/node-${RUNNER_NODE_VERSION}-linux-x64.tar.xz`;
|
|
85
|
+
const setup = [
|
|
86
|
+
APT_STEP,
|
|
87
|
+
`set -eu; curl -fsSL --retry 3 -o /tmp/node.tar.xz ${nodeUrl}; sha256sum /tmp/node.tar.xz; ` +
|
|
88
|
+
`echo '${RUNNER_NODE_SHA256} /tmp/node.tar.xz' | sha256sum -c -; ` +
|
|
89
|
+
"tar -xJf /tmp/node.tar.xz -C /usr/local --strip-components=1 --no-same-owner; rm -f /tmp/node.tar.xz",
|
|
90
|
+
"set -eu; id -u runner >/dev/null 2>&1 || useradd --create-home --shell /bin/bash --uid 1001 runner; " +
|
|
91
|
+
"groupadd -f docker; usermod -aG docker runner; " +
|
|
92
|
+
"printf 'runner ALL=(ALL) NOPASSWD:ALL\\nDefaults:runner !requiretty\\n' > /etc/sudoers.d/runner; " +
|
|
93
|
+
"chmod 0440 /etc/sudoers.d/runner; visudo -c -f /etc/sudoers.d/runner",
|
|
94
|
+
`set -eu; mkdir -p ${GITHUB_RUNNER_ROOT}; curl -fsSL --retry 3 -o /tmp/actions-runner.tar.gz ${url}; ` +
|
|
95
|
+
"sha256sum /tmp/actions-runner.tar.gz; " +
|
|
96
|
+
`echo '${ACTIONS_RUNNER_SHA256} /tmp/actions-runner.tar.gz' | sha256sum -c -; ` +
|
|
97
|
+
`tar -xzf /tmp/actions-runner.tar.gz -C ${GITHUB_RUNNER_ROOT}; rm -f /tmp/actions-runner.tar.gz`,
|
|
98
|
+
`set -eu; export DEBIAN_FRONTEND=noninteractive; ${GITHUB_RUNNER_ROOT}/bin/installdependencies.sh; rm -rf /var/lib/apt/lists/*`,
|
|
99
|
+
`set -eu; ${writeStep(GITHUB_LAUNCHER, GITHUB_LAUNCHER_SCRIPT, "0755")}; ` +
|
|
100
|
+
`${writeStep(`${GITHUB_RUNNER_ROOT}/.env`, GITHUB_RUNNER_ENV, "0644")}; chown -R runner:runner /home/runner`,
|
|
101
|
+
];
|
|
102
|
+
const verify = `set -eu; cd ${GITHUB_RUNNER_ROOT}; setpriv --reuid=runner --regid=runner --init-groups env HOME=/home/runner ./config.sh --version; ` +
|
|
103
|
+
"git --version; jq --version; python3 --version; lsb_release -rs; " +
|
|
104
|
+
`test "$(node --version)" = ${RUNNER_NODE_VERSION}; ` +
|
|
105
|
+
"setpriv --reuid=runner --regid=runner --init-groups env HOME=/home/runner sh -c 'node --version && npm --version && npx --version'; " +
|
|
106
|
+
"setpriv --reuid=runner --regid=runner --init-groups env HOME=/home/runner sh -c 'docker --version && docker compose version && docker buildx version'; " +
|
|
107
|
+
`sudo -n -u runner true; id runner; test -x ${GITHUB_LAUNCHER}`;
|
|
108
|
+
const platform_setup = [
|
|
109
|
+
"set -eu; D=/.promigence/docker; test -x \"$D/bin/docker\"; mkdir -p /usr/local/libexec/ci-docker /usr/local/lib/docker/cli-plugins; " +
|
|
110
|
+
`cp "$D/bin/docker" ${USER_DOCKER_CLI}; ` +
|
|
111
|
+
"for p in docker-buildx docker-compose; do rm -f /usr/local/lib/docker/cli-plugins/$p; cp \"$D/cli-plugins/$p\" /usr/local/lib/docker/cli-plugins/$p; done; " +
|
|
112
|
+
`chmod 0755 ${USER_DOCKER_CLI} /usr/local/lib/docker/cli-plugins/docker-buildx /usr/local/lib/docker/cli-plugins/docker-compose; ` +
|
|
113
|
+
`rm -f /usr/local/bin/docker; ${writeStep("/usr/local/bin/docker", DOCKER_WRAPPER_SCRIPT, "0755")}`,
|
|
114
|
+
];
|
|
115
|
+
return { image: RUNNER_BASE_IMAGE, setup, platform_setup, verify, tier, alias: runnerAlias(kind, tier) };
|
|
116
|
+
}
|
|
117
|
+
const deb = `https://packages.gitlab.com/runner/gitlab-runner/ubuntu/pool/main/g/gitlab-runner/gitlab-runner_${GITLAB_RUNNER_VERSION}-1_amd64.deb`;
|
|
118
|
+
const setup = [
|
|
119
|
+
APT_STEP,
|
|
120
|
+
"set -eu; curl -fsSL --retry 3 -o /tmp/gitlab-runner.deb " + deb + "; " +
|
|
121
|
+
"sha256sum /tmp/gitlab-runner.deb; " +
|
|
122
|
+
`echo '${GITLAB_RUNNER_DEB_SHA256} /tmp/gitlab-runner.deb' | sha256sum -c -; ` +
|
|
123
|
+
"dpkg-deb -x /tmp/gitlab-runner.deb /tmp/gitlab-runner-deb; " +
|
|
124
|
+
"mv /tmp/gitlab-runner-deb/usr/bin/gitlab-runner /usr/local/bin/gitlab-runner; rm -rf /tmp/gitlab-runner.deb /tmp/gitlab-runner-deb; " +
|
|
125
|
+
"sha256sum /usr/local/bin/gitlab-runner; " +
|
|
126
|
+
`echo '${GITLAB_RUNNER_SHA256} /usr/local/bin/gitlab-runner' | sha256sum -c -; ` +
|
|
127
|
+
"chmod 0755 /usr/local/bin/gitlab-runner; mkdir -p /etc/gitlab-runner; chmod 0700 /etc/gitlab-runner",
|
|
128
|
+
`set -eu; ${writeStep(GITLAB_LAUNCHER, GITLAB_LAUNCHER_SCRIPT, "0755")}`,
|
|
129
|
+
`set -eu; docker pull -q ${GITLAB_HELPER_IMAGE}; docker pull -q ${GITLAB_DEFAULT_JOB_IMAGE}`,
|
|
130
|
+
];
|
|
131
|
+
const platform_setup = ["set -eu; promigence-dockerd stop"];
|
|
132
|
+
const verify = `set -eu; gitlab-runner --version; git --version; test -x ${GITLAB_LAUNCHER}`;
|
|
133
|
+
return { image: RUNNER_BASE_IMAGE, setup, platform_setup, verify, tier, alias: runnerAlias(kind, tier) };
|
|
134
|
+
}
|
|
135
|
+
export function isRunnerTier(t) {
|
|
136
|
+
return RUNNER_TIERS.includes(t);
|
|
137
|
+
}
|