@smwb/onreel-git-mcp 0.3.0 → 0.5.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/README.md +222 -1
- package/dist/{chunk-L7CPE2SO.js → chunk-AGGJK2XZ.js} +2 -1
- package/dist/{chunk-GPRMI4PG.js → chunk-ZTME4HFD.js} +2 -2
- package/dist/git/index.js +3 -3
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.js +3 -3
- package/dist/tracker/ci.lib.d.ts +36 -0
- package/dist/tracker/ci.schema.d.ts +162 -0
- package/dist/tracker/ci.types.d.ts +117 -0
- package/dist/tracker/github-ci.lib.d.ts +2 -0
- package/dist/tracker/github-pull.lib.d.ts +2 -0
- package/dist/tracker/gitlab-ci.lib.d.ts +2 -0
- package/dist/tracker/gitlab-pull.lib.d.ts +2 -0
- package/dist/tracker/http.lib.d.ts +9 -1
- package/dist/tracker/index.d.ts +12 -0
- package/dist/tracker/index.js +1986 -90
- package/dist/tracker/index.js.map +1 -1
- package/dist/tracker/mapping.const.d.ts +24 -0
- package/dist/tracker/pull.lib.d.ts +4 -0
- package/dist/tracker/pull.schema.d.ts +63 -0
- package/dist/tracker/pull.types.d.ts +74 -0
- package/dist/tracker/sourcecraft-ci.lib.d.ts +2 -0
- package/dist/tracker/sourcecraft-pull.lib.d.ts +2 -0
- package/dist/tracker/tracker.const.d.ts +12 -0
- package/dist/tracker/tracker.types.d.ts +37 -0
- package/dist/worktree/index.js +2 -2
- package/package.json +1 -1
- /package/dist/{chunk-L7CPE2SO.js.map → chunk-AGGJK2XZ.js.map} +0 -0
- /package/dist/{chunk-GPRMI4PG.js.map → chunk-ZTME4HFD.js.map} +0 -0
package/dist/tracker/index.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
+
REDACTED,
|
|
2
3
|
defaultSecretSink,
|
|
3
4
|
redactSecretText
|
|
4
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-AGGJK2XZ.js";
|
|
6
|
+
import {
|
|
7
|
+
issueLinkSchema,
|
|
8
|
+
timestampSchema
|
|
9
|
+
} from "../chunk-4ZGRXIVA.js";
|
|
5
10
|
|
|
6
11
|
// src/tracker/tracker.const.ts
|
|
7
12
|
var TRACKER_TIMEOUT_MS = 15e3;
|
|
@@ -16,6 +21,12 @@ var LEGACY_ISSUE_CAPABILITIES = {
|
|
|
16
21
|
list: { filters: ["status"], clientSide: ["status"], sorts: [] },
|
|
17
22
|
comments: { filters: [], clientSide: [], order: ["asc"] }
|
|
18
23
|
};
|
|
24
|
+
var CI_LOG_DEFAULT_BYTES = 16 * 1024;
|
|
25
|
+
var CI_LOG_MAX_BYTES = 1024 * 1024;
|
|
26
|
+
var CI_LOG_READ_MAX_BYTES = 64 * 1024 * 1024;
|
|
27
|
+
var CI_LOG_TIMEOUT_MS = 6e4;
|
|
28
|
+
var CI_LOG_PAGES_MAX = 50;
|
|
29
|
+
var CI_FINISHED_STATUSES = ["success", "failed", "canceled", "skipped"];
|
|
19
30
|
|
|
20
31
|
// src/tracker/tracker.lib.ts
|
|
21
32
|
var TrackerError = class extends Error {
|
|
@@ -211,6 +222,99 @@ var GITHUB_PRIORITY_LABELS = {
|
|
|
211
222
|
"priority: blocker": "blocker"
|
|
212
223
|
};
|
|
213
224
|
var ALL_STATUSES = ["open", "in_progress", "done", "declined"];
|
|
225
|
+
var GITLAB_MERGE_REQUEST_STATES = {
|
|
226
|
+
opened: "open",
|
|
227
|
+
locked: "open",
|
|
228
|
+
merged: "merged",
|
|
229
|
+
closed: "closed"
|
|
230
|
+
};
|
|
231
|
+
var GITLAB_DRAFT_PREFIX = "Draft: ";
|
|
232
|
+
var GITLAB_DRAFT_PATTERN = /^\s*(?:\[draft\]|\(draft\)|draft:)\s*/i;
|
|
233
|
+
var SOURCECRAFT_PULL_STATUSES = {
|
|
234
|
+
draft: "draft",
|
|
235
|
+
open: "open",
|
|
236
|
+
merging: "open",
|
|
237
|
+
merged: "merged",
|
|
238
|
+
discarded: "closed"
|
|
239
|
+
};
|
|
240
|
+
var GITHUB_RUN_STATUSES = {
|
|
241
|
+
queued: "queued",
|
|
242
|
+
requested: "queued",
|
|
243
|
+
pending: "queued",
|
|
244
|
+
waiting: "waiting",
|
|
245
|
+
in_progress: "running"
|
|
246
|
+
};
|
|
247
|
+
var GITHUB_RUN_CONCLUSIONS = {
|
|
248
|
+
success: "success",
|
|
249
|
+
neutral: "success",
|
|
250
|
+
failure: "failed",
|
|
251
|
+
timed_out: "failed",
|
|
252
|
+
startup_failure: "failed",
|
|
253
|
+
cancelled: "canceled",
|
|
254
|
+
stale: "canceled",
|
|
255
|
+
skipped: "skipped",
|
|
256
|
+
action_required: "waiting"
|
|
257
|
+
};
|
|
258
|
+
var GITHUB_RUN_EVENTS = {
|
|
259
|
+
push: "push",
|
|
260
|
+
pull_request: "pull_request",
|
|
261
|
+
pull_request_target: "pull_request",
|
|
262
|
+
merge_group: "pull_request",
|
|
263
|
+
workflow_dispatch: "manual",
|
|
264
|
+
schedule: "schedule",
|
|
265
|
+
repository_dispatch: "trigger",
|
|
266
|
+
workflow_run: "trigger",
|
|
267
|
+
workflow_call: "trigger"
|
|
268
|
+
};
|
|
269
|
+
var GITLAB_CI_STATUSES = {
|
|
270
|
+
created: "queued",
|
|
271
|
+
waiting_for_resource: "queued",
|
|
272
|
+
preparing: "queued",
|
|
273
|
+
pending: "queued",
|
|
274
|
+
running: "running",
|
|
275
|
+
canceling: "running",
|
|
276
|
+
waiting_for_callback: "waiting",
|
|
277
|
+
manual: "waiting",
|
|
278
|
+
scheduled: "waiting",
|
|
279
|
+
success: "success",
|
|
280
|
+
failed: "failed",
|
|
281
|
+
canceled: "canceled",
|
|
282
|
+
skipped: "skipped"
|
|
283
|
+
};
|
|
284
|
+
var GITLAB_PIPELINE_SOURCES = {
|
|
285
|
+
push: "push",
|
|
286
|
+
merge_request_event: "pull_request",
|
|
287
|
+
external_pull_request_event: "pull_request",
|
|
288
|
+
web: "manual",
|
|
289
|
+
api: "manual",
|
|
290
|
+
chat: "manual",
|
|
291
|
+
webide: "manual",
|
|
292
|
+
schedule: "schedule",
|
|
293
|
+
pipeline_execution_policy_schedule: "schedule",
|
|
294
|
+
trigger: "trigger",
|
|
295
|
+
pipeline: "trigger",
|
|
296
|
+
parent_pipeline: "trigger"
|
|
297
|
+
};
|
|
298
|
+
var SOURCECRAFT_CI_STATUSES = {
|
|
299
|
+
created: "queued",
|
|
300
|
+
prepared: "queued",
|
|
301
|
+
processing: "running",
|
|
302
|
+
awaiting_approval: "waiting",
|
|
303
|
+
success: "success",
|
|
304
|
+
failed: "failed",
|
|
305
|
+
timeout: "failed",
|
|
306
|
+
canceled: "canceled",
|
|
307
|
+
rejected: "canceled",
|
|
308
|
+
skipped: "skipped"
|
|
309
|
+
};
|
|
310
|
+
var SOURCECRAFT_RUN_EVENTS = {
|
|
311
|
+
push: "push",
|
|
312
|
+
pr_update: "pull_request",
|
|
313
|
+
manual: "manual",
|
|
314
|
+
restart: "retry",
|
|
315
|
+
schedule: "schedule",
|
|
316
|
+
repository_event: "trigger"
|
|
317
|
+
};
|
|
214
318
|
|
|
215
319
|
// src/tracker/http.lib.ts
|
|
216
320
|
async function requestJson(url, request) {
|
|
@@ -223,7 +327,7 @@ async function requestJson(url, request) {
|
|
|
223
327
|
signal: AbortSignal.timeout(TRACKER_TIMEOUT_MS)
|
|
224
328
|
});
|
|
225
329
|
} catch (error) {
|
|
226
|
-
throw
|
|
330
|
+
throw notAnswered(url, error);
|
|
227
331
|
}
|
|
228
332
|
const raw = await response.text();
|
|
229
333
|
if (!response.ok) throw failureOf(url, response, raw, request);
|
|
@@ -234,6 +338,131 @@ async function requestJson(url, request) {
|
|
|
234
338
|
throw new TrackerError("failed", `\u0422\u0440\u0435\u043A\u0435\u0440 ${hostOf(url)} \u0432\u0435\u0440\u043D\u0443\u043B \u043D\u0435 JSON.`);
|
|
235
339
|
}
|
|
236
340
|
}
|
|
341
|
+
var ERROR_BODY_READ_MAX = 16 * 1024;
|
|
342
|
+
async function requestText(url, request) {
|
|
343
|
+
const skip = request.skip ?? 0;
|
|
344
|
+
const { bytes, from } = request.keep;
|
|
345
|
+
const readMax = request.readMax ?? CI_LOG_READ_MAX_BYTES;
|
|
346
|
+
if (![skip, bytes, readMax].every((value) => Number.isSafeInteger(value) && value >= 0)) {
|
|
347
|
+
throw new TrackerError("invalid_request", "\u041E\u043A\u043D\u043E \u0447\u0442\u0435\u043D\u0438\u044F \u0442\u0435\u043A\u0441\u0442\u0430 \u2014 \u0446\u0435\u043B\u044B\u0435 \u0447\u0438\u0441\u043B\u0430 \u0431\u0430\u0439\u0442 \u043D\u0435 \u043C\u0435\u043D\u044C\u0448\u0435 0.");
|
|
348
|
+
}
|
|
349
|
+
let response;
|
|
350
|
+
try {
|
|
351
|
+
response = await request.fetch(url, {
|
|
352
|
+
method: "GET",
|
|
353
|
+
headers: { accept: request.accept ?? "text/plain", ...request.headers },
|
|
354
|
+
signal: AbortSignal.timeout(request.timeoutMs ?? CI_LOG_TIMEOUT_MS)
|
|
355
|
+
});
|
|
356
|
+
} catch (error) {
|
|
357
|
+
throw notAnswered(url, error);
|
|
358
|
+
}
|
|
359
|
+
if (!response.ok) {
|
|
360
|
+
const head = await readBody(url, response, { skip: 0, bytes: ERROR_BODY_READ_MAX, from: "head", readMax: ERROR_BODY_READ_MAX });
|
|
361
|
+
throw failureOf(url, response, decodeUtf8(head.window), request);
|
|
362
|
+
}
|
|
363
|
+
const body = await readBody(url, response, { skip, bytes, from, readMax });
|
|
364
|
+
let start = Math.min(body.start, body.end);
|
|
365
|
+
let end = body.end;
|
|
366
|
+
let window = body.window;
|
|
367
|
+
const lead = start > 0 ? leadingContinuation(window) : 0;
|
|
368
|
+
const tail = trailingPartial(window.subarray(lead));
|
|
369
|
+
if (lead > 0 || tail > 0) {
|
|
370
|
+
window = window.subarray(lead, window.length - tail);
|
|
371
|
+
start += lead;
|
|
372
|
+
end -= tail;
|
|
373
|
+
}
|
|
374
|
+
return { text: decodeUtf8(window), start, end, read: body.read, whole: body.whole, headers: response.headers };
|
|
375
|
+
}
|
|
376
|
+
async function readBody(url, response, window) {
|
|
377
|
+
const { skip, bytes, from, readMax } = window;
|
|
378
|
+
const reader = response.body?.getReader();
|
|
379
|
+
const kept = from === "head" ? headBuffer(bytes) : ringBuffer(bytes);
|
|
380
|
+
let read = 0;
|
|
381
|
+
let whole = true;
|
|
382
|
+
if (reader !== void 0) {
|
|
383
|
+
try {
|
|
384
|
+
for (; ; ) {
|
|
385
|
+
const { done, value } = await reader.read();
|
|
386
|
+
if (done) break;
|
|
387
|
+
const chunk = value.subarray(0, Math.max(0, readMax - read));
|
|
388
|
+
const cut = chunk.length < value.length;
|
|
389
|
+
const offset = Math.max(0, skip - read);
|
|
390
|
+
if (offset < chunk.length) kept.push(chunk.subarray(offset));
|
|
391
|
+
read += chunk.length;
|
|
392
|
+
if (cut || kept.full()) {
|
|
393
|
+
whole = false;
|
|
394
|
+
await reader.cancel().catch(() => void 0);
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
} catch (error) {
|
|
399
|
+
await reader.cancel().catch(() => void 0);
|
|
400
|
+
throw notAnswered(url, error);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const data = kept.bytes();
|
|
404
|
+
const start = from === "head" ? skip : Math.max(skip, read - data.length);
|
|
405
|
+
return { window: data, start, end: from === "head" ? skip + data.length : read, read, whole };
|
|
406
|
+
}
|
|
407
|
+
function headBuffer(capacity) {
|
|
408
|
+
const buffer = new Uint8Array(capacity);
|
|
409
|
+
let length = 0;
|
|
410
|
+
let overflow = false;
|
|
411
|
+
return {
|
|
412
|
+
push(chunk) {
|
|
413
|
+
const room = capacity - length;
|
|
414
|
+
buffer.set(chunk.subarray(0, room), length);
|
|
415
|
+
length += Math.min(room, chunk.length);
|
|
416
|
+
if (chunk.length > room) overflow = true;
|
|
417
|
+
},
|
|
418
|
+
full: () => overflow,
|
|
419
|
+
bytes: () => buffer.subarray(0, length)
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function ringBuffer(capacity) {
|
|
423
|
+
const buffer = new Uint8Array(capacity);
|
|
424
|
+
let next = 0;
|
|
425
|
+
let length = 0;
|
|
426
|
+
return {
|
|
427
|
+
push(chunk) {
|
|
428
|
+
if (capacity === 0) return;
|
|
429
|
+
const part = chunk.subarray(Math.max(0, chunk.length - capacity));
|
|
430
|
+
const first = Math.min(part.length, capacity - next);
|
|
431
|
+
buffer.set(part.subarray(0, first), next);
|
|
432
|
+
buffer.set(part.subarray(first), 0);
|
|
433
|
+
next = (next + part.length) % capacity;
|
|
434
|
+
length = Math.min(capacity, length + part.length);
|
|
435
|
+
},
|
|
436
|
+
full: () => false,
|
|
437
|
+
bytes() {
|
|
438
|
+
if (length < capacity) return buffer.subarray(0, length);
|
|
439
|
+
const ordered = new Uint8Array(capacity);
|
|
440
|
+
ordered.set(buffer.subarray(next), 0);
|
|
441
|
+
ordered.set(buffer.subarray(0, next), capacity - next);
|
|
442
|
+
return ordered;
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
function leadingContinuation(window) {
|
|
447
|
+
let count = 0;
|
|
448
|
+
while (count < 3 && count < window.length && ((window[count] ?? 0) & 192) === 128) count += 1;
|
|
449
|
+
return count;
|
|
450
|
+
}
|
|
451
|
+
function trailingPartial(window) {
|
|
452
|
+
for (let back = 1; back <= Math.min(4, window.length); back += 1) {
|
|
453
|
+
const byte = window[window.length - back] ?? 0;
|
|
454
|
+
if ((byte & 192) === 128) continue;
|
|
455
|
+
const length = byte >= 240 ? 4 : byte >= 224 ? 3 : byte >= 192 ? 2 : 1;
|
|
456
|
+
return length > back ? back : 0;
|
|
457
|
+
}
|
|
458
|
+
return 0;
|
|
459
|
+
}
|
|
460
|
+
function decodeUtf8(bytes) {
|
|
461
|
+
return new TextDecoder("utf-8").decode(bytes);
|
|
462
|
+
}
|
|
463
|
+
function notAnswered(url, error) {
|
|
464
|
+
return new TrackerError("failed", redactSecretText(`\u0422\u0440\u0435\u043A\u0435\u0440 \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B (${hostOf(url)}): ${error instanceof Error ? error.message : String(error)}`));
|
|
465
|
+
}
|
|
237
466
|
function failureOf(url, response, raw, request) {
|
|
238
467
|
const status = response.status;
|
|
239
468
|
if (status === 404 && request.missing !== void 0) return new TrackerError(request.missing.failure, request.missing.message);
|
|
@@ -263,48 +492,1262 @@ function retryAfterOf(value, now) {
|
|
|
263
492
|
const at = Date.parse(value);
|
|
264
493
|
return Number.isFinite(at) ? Math.max(0, Math.ceil((at - now) / 1e3)) : void 0;
|
|
265
494
|
}
|
|
266
|
-
function resetDelay(reset, now) {
|
|
267
|
-
return reset >= 1e9 ? Math.max(0, reset - Math.floor(now / 1e3)) : reset;
|
|
495
|
+
function resetDelay(reset, now) {
|
|
496
|
+
return reset >= 1e9 ? Math.max(0, reset - Math.floor(now / 1e3)) : reset;
|
|
497
|
+
}
|
|
498
|
+
function hostOf(url) {
|
|
499
|
+
try {
|
|
500
|
+
return new URL(url).host;
|
|
501
|
+
} catch {
|
|
502
|
+
return "\u0442\u0440\u0435\u043A\u0435\u0440";
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function baseUrl(value, fallback) {
|
|
506
|
+
const raw = (value ?? fallback).replace(/\/+$/, "");
|
|
507
|
+
let parsed;
|
|
508
|
+
try {
|
|
509
|
+
parsed = new URL(raw);
|
|
510
|
+
} catch {
|
|
511
|
+
throw new TrackerError("invalid_request", "\u0410\u0434\u0440\u0435\u0441 \u0442\u0440\u0435\u043A\u0435\u0440\u0430 \u2014 \u043D\u0435 URL.");
|
|
512
|
+
}
|
|
513
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new TrackerError("invalid_request", "\u0410\u0434\u0440\u0435\u0441 \u0442\u0440\u0435\u043A\u0435\u0440\u0430 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C http \u0438\u043B\u0438 https.");
|
|
514
|
+
return raw;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// src/tracker/ci.schema.ts
|
|
518
|
+
import { z } from "zod";
|
|
519
|
+
var ciStatusCategorySchema = z.enum(["queued", "running", "waiting", "success", "failed", "canceled", "skipped"]);
|
|
520
|
+
var ciEventCategorySchema = z.enum(["push", "pull_request", "manual", "schedule", "retry", "trigger", "other"]);
|
|
521
|
+
var ciRunFilterSchema = z.enum(["status", "ref", "sha", "event", "workflow", "actor", "pullRequest", "createdAfter", "createdBefore", "updatedAfter", "updatedBefore"]);
|
|
522
|
+
var ciRunSortFieldSchema = z.enum(["created", "updated"]);
|
|
523
|
+
var ciJobFilterSchema = z.enum(["status", "allAttempts"]);
|
|
524
|
+
var ciStatusSchema = z.object({ category: ciStatusCategorySchema, name: z.string().min(1).max(100) });
|
|
525
|
+
var ciWorkflowSchema = z.object({
|
|
526
|
+
/** Ключ workflow: GitHub — id строкой; подходит в trigger.workflows и фильтр workflow. */
|
|
527
|
+
key: z.string().min(1).max(200),
|
|
528
|
+
name: z.string().min(1).max(500),
|
|
529
|
+
path: z.string().min(1).max(1e3).optional(),
|
|
530
|
+
enabled: z.boolean()
|
|
531
|
+
});
|
|
532
|
+
var ciRunSchema = z.object({
|
|
533
|
+
/** Ключ прогона: GitHub run id, GitLab pipeline id, SourceCraft slug-счётчик; непрозрачная строка. */
|
|
534
|
+
key: z.string().min(1).max(200),
|
|
535
|
+
/** Номер для людей: GitHub run_number, GitLab iid, SourceCraft — счётчик. */
|
|
536
|
+
number: z.number().int().nonnegative().optional(),
|
|
537
|
+
/** Workflow прогона: GitHub — один, SourceCraft — все в прогоне, GitLab — имя пайплайна, если задано. */
|
|
538
|
+
workflows: z.array(z.string().min(1).max(500)).max(100),
|
|
539
|
+
status: ciStatusSchema,
|
|
540
|
+
event: z.object({ category: ciEventCategorySchema, name: z.string().min(1).max(100) }).optional(),
|
|
541
|
+
/** Ветка или тег прогона. */
|
|
542
|
+
ref: z.string().min(1).max(500).optional(),
|
|
543
|
+
sha: z.string().regex(/^[0-9a-f]{7,64}$/).optional(),
|
|
544
|
+
/** Кто запустил — логин или slug трекера. */
|
|
545
|
+
actor: z.string().min(1).max(200).optional(),
|
|
546
|
+
/** Ключ PR того же трекера, для которого прогон. */
|
|
547
|
+
pullRequest: z.string().min(1).max(200).optional(),
|
|
548
|
+
/** Попытка прогона (GitHub run_attempt). */
|
|
549
|
+
attempt: z.number().int().positive().optional(),
|
|
550
|
+
/** Ошибки прогона от трекера (SourceCraft error_messages). */
|
|
551
|
+
errors: z.array(z.string().min(1).max(4e3)).max(20).optional(),
|
|
552
|
+
links: z.array(issueLinkSchema).max(10),
|
|
553
|
+
createdAt: timestampSchema.optional(),
|
|
554
|
+
startedAt: timestampSchema.optional(),
|
|
555
|
+
finishedAt: timestampSchema.optional(),
|
|
556
|
+
updatedAt: timestampSchema.optional()
|
|
557
|
+
});
|
|
558
|
+
var ciJobSchema = z.object({
|
|
559
|
+
/** Ключ задания: GitHub и GitLab — job id, SourceCraft — `<run>/<workflow>/<task>/<cube>`, части через encodeURIComponent. */
|
|
560
|
+
key: z.string().min(1).max(1e3),
|
|
561
|
+
runKey: z.string().min(1).max(200),
|
|
562
|
+
name: z.string().min(1).max(500),
|
|
563
|
+
/** Группа: GitLab — stage, SourceCraft — `<workflow>/<task>`; у GitHub нет. */
|
|
564
|
+
stage: z.string().min(1).max(1e3).optional(),
|
|
565
|
+
status: ciStatusSchema,
|
|
566
|
+
/** Шаги GitHub: где упало; у остальных нет. */
|
|
567
|
+
steps: z.array(z.object({ number: z.number().int().nonnegative(), name: z.string().min(1).max(500), status: ciStatusSchema })).max(1e3).optional(),
|
|
568
|
+
attempt: z.number().int().positive().optional(),
|
|
569
|
+
links: z.array(issueLinkSchema).max(10),
|
|
570
|
+
createdAt: timestampSchema.optional(),
|
|
571
|
+
startedAt: timestampSchema.optional(),
|
|
572
|
+
finishedAt: timestampSchema.optional()
|
|
573
|
+
});
|
|
574
|
+
var ciLogSchema = z.object({
|
|
575
|
+
/** Окно лога: очищено от ANSI (если не raw), секреты замаскированы. */
|
|
576
|
+
text: z.string().max(CI_LOG_MAX_BYTES),
|
|
577
|
+
/** Окно не покрывает весь лог от курсора: срезано спереди (tail), сзади (head) или упёрлись в потолок чтения. */
|
|
578
|
+
truncated: z.boolean(),
|
|
579
|
+
/** Задание закончилось — лог больше не растёт. */
|
|
580
|
+
complete: z.boolean(),
|
|
581
|
+
/** Курсор дочитывания: передайте в log() — придёт текст после этого места. Нет — дальше ничего не будет. */
|
|
582
|
+
nextCursor: z.string().min(1).max(200).optional(),
|
|
583
|
+
/** Размер лога в байтах, если тело прочитано целиком. */
|
|
584
|
+
size: z.number().int().nonnegative().optional()
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// src/tracker/ci.lib.ts
|
|
588
|
+
function ciAdapterOf(definition, context) {
|
|
589
|
+
if (definition.ci === void 0) throw new TrackerError("unsupported", `\u0422\u0440\u0435\u043A\u0435\u0440 ${definition.descriptor.title} \u043D\u0435 \u0432\u0435\u0434\u0451\u0442 CI.`);
|
|
590
|
+
return definition.ci.create(context);
|
|
591
|
+
}
|
|
592
|
+
var LOG_CURSOR = /^c:(\d{1,15}):(\d{1,15})$/;
|
|
593
|
+
function logWindow(query) {
|
|
594
|
+
const bytes = query.maxBytes ?? CI_LOG_DEFAULT_BYTES;
|
|
595
|
+
if (!Number.isInteger(bytes) || bytes < 1 || bytes > CI_LOG_MAX_BYTES) {
|
|
596
|
+
throw new TrackerError("invalid_request", `\u041E\u043A\u043D\u043E \u043B\u043E\u0433\u0430 maxBytes \u2014 \u0446\u0435\u043B\u043E\u0435 \u043E\u0442 1 \u0434\u043E ${String(CI_LOG_MAX_BYTES)}.`);
|
|
597
|
+
}
|
|
598
|
+
const from = query.from ?? "tail";
|
|
599
|
+
if (from !== "head" && from !== "tail") throw new TrackerError("invalid_request", "\u041D\u0430\u0447\u0430\u043B\u043E \u043E\u043A\u043D\u0430 \u043B\u043E\u0433\u0430 from \u2014 head \u0438\u043B\u0438 tail.");
|
|
600
|
+
if (query.cursor === void 0) return { bytes, from };
|
|
601
|
+
const match = LOG_CURSOR.exec(query.cursor);
|
|
602
|
+
if (match === null) {
|
|
603
|
+
throw new TrackerError("invalid_request", "\u041A\u0443\u0440\u0441\u043E\u0440 \u043B\u043E\u0433\u0430 \u043D\u0435\u0432\u0435\u0440\u0435\u043D: \u043F\u0435\u0440\u0435\u0434\u0430\u0439\u0442\u0435 nextCursor \u043F\u0440\u043E\u0448\u043B\u043E\u0433\u043E \u043E\u0442\u0432\u0435\u0442\u0430 log() \u0438\u043B\u0438 \u043D\u0435 \u043F\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0439\u0442\u0435 cursor.");
|
|
604
|
+
}
|
|
605
|
+
return { bytes, from: "head", resume: { page: Number(match[1]), offset: Number(match[2]) } };
|
|
606
|
+
}
|
|
607
|
+
function logCursor(page, offset) {
|
|
608
|
+
return `c:${String(page)}:${String(offset)}`;
|
|
609
|
+
}
|
|
610
|
+
function logOf(input) {
|
|
611
|
+
const stalled = input.text === "" && input.more;
|
|
612
|
+
const follow = !input.complete || input.more && !stalled;
|
|
613
|
+
const window = lineWindow(input.text, { start: input.startCut ?? (input.truncated && !input.more), end: follow, endBytes: input.end });
|
|
614
|
+
const clean = maskKeyTail(input.raw ? window.text : cleanLogText(window.text));
|
|
615
|
+
const text2 = clipText(redactSecretText(clean), CI_LOG_MAX_BYTES);
|
|
616
|
+
return {
|
|
617
|
+
text: text2,
|
|
618
|
+
truncated: input.truncated,
|
|
619
|
+
complete: input.complete,
|
|
620
|
+
...follow ? { nextCursor: logCursor(input.page, input.end - window.held) } : {},
|
|
621
|
+
...input.size === void 0 ? {} : { size: input.size }
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
var KEY_BEGIN = /-----BEGIN [A-Z ]*PRIVATE KEY-----/g;
|
|
625
|
+
var KEY_END = /-----END [A-Z ]*PRIVATE KEY-----/;
|
|
626
|
+
var WHOLE_ANSI_SEQUENCE = /^(?:\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[ -/]*(?![[\]])[0-~])/;
|
|
627
|
+
var VALUE_KEY = /(?:[A-Za-z][\w.-]{0,63}["']?\s*[=:]|\b(?:Bearer|Basic|Token))\s*["']?$/i;
|
|
628
|
+
var VALUE_KEY_LOOKBACK = 80;
|
|
629
|
+
function lineWindow(text2, cut) {
|
|
630
|
+
let to = text2.length;
|
|
631
|
+
if (cut.end) {
|
|
632
|
+
const line = text2.lastIndexOf("\n");
|
|
633
|
+
to = keyStart(text2, line >= 0 ? line + 1 : wordCut(text2, byteCut(text2)));
|
|
634
|
+
}
|
|
635
|
+
let held = to === text2.length ? 0 : utf8Length(text2.slice(to));
|
|
636
|
+
if (held > cut.endBytes) {
|
|
637
|
+
to = suffixStart(text2, cut.endBytes);
|
|
638
|
+
held = cut.endBytes;
|
|
639
|
+
}
|
|
640
|
+
let from = 0;
|
|
641
|
+
if (cut.start) {
|
|
642
|
+
const line = text2.indexOf("\n");
|
|
643
|
+
from = Math.min(line >= 0 ? line + 1 : firstWordEnd(text2), to);
|
|
644
|
+
}
|
|
645
|
+
return { text: text2.slice(from, to), held };
|
|
646
|
+
}
|
|
647
|
+
function byteCut(text2) {
|
|
648
|
+
let to = text2.length;
|
|
649
|
+
const escape = text2.lastIndexOf("\x1B");
|
|
650
|
+
if (escape > 0 && !WHOLE_ANSI_SEQUENCE.test(text2.slice(escape))) to = escape;
|
|
651
|
+
if (to > 1 && text2[to - 1] === "\r") to -= 1;
|
|
652
|
+
return to;
|
|
653
|
+
}
|
|
654
|
+
function wordCut(text2, to) {
|
|
655
|
+
let space = to - 1;
|
|
656
|
+
while (space >= 0 && !/\s/.test(text2.charAt(space))) space -= 1;
|
|
657
|
+
if (space < 0) return to;
|
|
658
|
+
const word = space + 1;
|
|
659
|
+
const before = text2.slice(Math.max(0, word - VALUE_KEY_LOOKBACK), word);
|
|
660
|
+
const key = VALUE_KEY.exec(before);
|
|
661
|
+
const keyed = key === null ? word : word - before.length + key.index;
|
|
662
|
+
return keyed > 0 ? keyed : word;
|
|
663
|
+
}
|
|
664
|
+
function firstWordEnd(text2) {
|
|
665
|
+
const space = /\s+/.exec(text2);
|
|
666
|
+
return space === null ? 0 : space.index + space[0].length;
|
|
667
|
+
}
|
|
668
|
+
function keyStart(text2, to) {
|
|
669
|
+
const head = text2.slice(0, to);
|
|
670
|
+
const begin = [...head.matchAll(KEY_BEGIN)].at(-1);
|
|
671
|
+
if (begin === void 0 || KEY_END.test(head.slice(begin.index))) return to;
|
|
672
|
+
const line = head.lastIndexOf("\n", begin.index) + 1;
|
|
673
|
+
return line > 0 ? line : begin.index > 0 ? begin.index : to;
|
|
674
|
+
}
|
|
675
|
+
function maskKeyTail(text2) {
|
|
676
|
+
const end = KEY_END.exec(text2);
|
|
677
|
+
if (end !== null && [...text2.slice(0, end.index).matchAll(KEY_BEGIN)].length === 0) return REDACTED + text2.slice(end.index + end[0].length);
|
|
678
|
+
let at = 0;
|
|
679
|
+
for (; ; ) {
|
|
680
|
+
const next = text2.indexOf("\n", at);
|
|
681
|
+
if (!isKeyBodyLine(text2.slice(at, next < 0 ? text2.length : next))) break;
|
|
682
|
+
at = next < 0 ? text2.length : next + 1;
|
|
683
|
+
if (next < 0) break;
|
|
684
|
+
}
|
|
685
|
+
if (at === 0) return text2;
|
|
686
|
+
return REDACTED + (text2[at - 1] === "\n" ? text2.slice(at - 1) : "");
|
|
687
|
+
}
|
|
688
|
+
function isKeyBodyLine(line) {
|
|
689
|
+
const body = line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
690
|
+
return /^[A-Za-z0-9+/]{16,76}={0,2}$/.test(body) && /[A-Z]/.test(body) && /[a-z]/.test(body) && /\d/.test(body);
|
|
691
|
+
}
|
|
692
|
+
var utf8 = new TextEncoder();
|
|
693
|
+
function utf8Length(text2) {
|
|
694
|
+
return utf8.encode(text2).length;
|
|
695
|
+
}
|
|
696
|
+
function suffixStart(text2, bytes) {
|
|
697
|
+
let index = text2.length;
|
|
698
|
+
let count = 0;
|
|
699
|
+
while (index > 0 && count < bytes) {
|
|
700
|
+
const low = text2.charCodeAt(index - 1);
|
|
701
|
+
const pair = index > 1 && low >= 56320 && low <= 57343 && text2.charCodeAt(index - 2) >= 55296 && text2.charCodeAt(index - 2) <= 56319;
|
|
702
|
+
count += utf8Length(text2.slice(index - (pair ? 2 : 1), index));
|
|
703
|
+
index -= pair ? 2 : 1;
|
|
704
|
+
}
|
|
705
|
+
return index;
|
|
706
|
+
}
|
|
707
|
+
var ANSI_SEQUENCE = /\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\|$)|\x1b[ -/]*[0-~]/g;
|
|
708
|
+
var GITLAB_SECTION_MARKER = /section_(?:start|end):\d+:[A-Za-z0-9_.-]+(?:\[[^\]\r\n]*\])?\r?/g;
|
|
709
|
+
function cleanLogText(text2) {
|
|
710
|
+
const plain = text2.replace(ANSI_SEQUENCE, "").replace(GITLAB_SECTION_MARKER, "").replace(/\r+\n/g, "\n");
|
|
711
|
+
if (!plain.includes("\r")) return plain;
|
|
712
|
+
return plain.split("\n").map((line) => line.includes("\r") ? line.split("\r").findLast((part) => part !== "") ?? "" : line).join("\n");
|
|
713
|
+
}
|
|
714
|
+
function isFinished(category) {
|
|
715
|
+
return CI_FINISHED_STATUSES.includes(category);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// src/tracker/github-ci.lib.ts
|
|
719
|
+
var DEFAULT_API = "https://api.github.com";
|
|
720
|
+
var TARGET = /^[\w.-]+\/[\w.-]+$/;
|
|
721
|
+
var ID = /^[1-9]\d{0,19}$/;
|
|
722
|
+
var SHA = /^[0-9a-f]{7,64}$/;
|
|
723
|
+
var DISPATCH_INPUTS_MAX = 25;
|
|
724
|
+
var RUN_FILTERS = ciRunFilterSchema.options;
|
|
725
|
+
var JOB_FILTERS = ciJobFilterSchema.options;
|
|
726
|
+
var RUNS_SUBJECT = "\u041F\u0440\u043E\u0433\u043E\u043D\u044B CI GitHub";
|
|
727
|
+
var JOBS_SUBJECT = "\u0417\u0430\u0434\u0430\u043D\u0438\u044F CI GitHub";
|
|
728
|
+
var WORKFLOWS_SUBJECT = "\u0421\u043F\u0438\u0441\u043E\u043A workflow GitHub";
|
|
729
|
+
var TRIGGER_SUBJECT = "\u0417\u0430\u043F\u0443\u0441\u043A CI GitHub";
|
|
730
|
+
var CAPABILITIES = {
|
|
731
|
+
workflows: true,
|
|
732
|
+
runs: { filters: RUN_FILTERS, clientSide: ["status", "event", "updatedAfter", "updatedBefore"], sorts: [] },
|
|
733
|
+
jobs: { filters: JOB_FILTERS, clientSide: ["status"], sorts: [] },
|
|
734
|
+
log: { live: false, cursor: true },
|
|
735
|
+
trigger: { supported: true, workflows: "one", inputs: true, tags: true },
|
|
736
|
+
rerun: { all: true, failedOnly: true, job: true, debug: true },
|
|
737
|
+
cancel: { run: true, job: false }
|
|
738
|
+
};
|
|
739
|
+
function githubCi() {
|
|
740
|
+
return { capabilities: CAPABILITIES, create: (context) => createGithubCiAdapter(context) };
|
|
741
|
+
}
|
|
742
|
+
function createGithubCiAdapter({ connection, fetch }) {
|
|
743
|
+
const target = requireTarget(connection.target, "owner/repo \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F GitHub");
|
|
744
|
+
if (!TARGET.test(target)) throw new TrackerError("invalid_request", "target GitHub \u2014 owner/repo.");
|
|
745
|
+
const api = baseUrl(connection.url, DEFAULT_API);
|
|
746
|
+
const root = `${api}/repos/${target}`;
|
|
747
|
+
const actions = `${root}/actions`;
|
|
748
|
+
const headers = {
|
|
749
|
+
accept: "application/vnd.github+json",
|
|
750
|
+
"x-github-api-version": "2022-11-28",
|
|
751
|
+
...connection.token === void 0 || connection.token === "" ? {} : { authorization: `Bearer ${connection.token}` }
|
|
752
|
+
};
|
|
753
|
+
const runKey = (key) => {
|
|
754
|
+
if (!ID.test(key)) throw new TrackerError("not_found", `\u0412 GitHub \u043D\u0435\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 ${key}.`);
|
|
755
|
+
return key;
|
|
756
|
+
};
|
|
757
|
+
const jobKey = (key) => {
|
|
758
|
+
if (!ID.test(key)) throw new TrackerError("not_found", `\u0412 GitHub \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u043D\u0438\u044F ${key}.`);
|
|
759
|
+
return key;
|
|
760
|
+
};
|
|
761
|
+
const missingRun = (key) => ({ failure: "not_found", message: `\u0412 GitHub \u043D\u0435\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 ${key}.` });
|
|
762
|
+
const missingJob = (key) => ({ failure: "not_found", message: `\u0412 GitHub \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u043D\u0438\u044F ${key}.` });
|
|
763
|
+
const getRun = async (key) => runOf((await requestJson(`${actions}/runs/${runKey(key)}`, { fetch, headers, missing: missingRun(key) })).body);
|
|
764
|
+
const headShaOf = async (query) => {
|
|
765
|
+
if (query.pullRequest === void 0) return query.sha;
|
|
766
|
+
if (query.sha !== void 0) throw new TrackerError("invalid_request", `${RUNS_SUBJECT}: sha \u0438 pullRequest \u0437\u0430\u0434\u0430\u044E\u0442\u0441\u044F \u043F\u043E \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E\u0441\u0442\u0438 \u2014 \u043F\u0440\u043E\u0433\u043E\u043D\u044B PR \u0431\u0435\u0440\u0443\u0442\u0441\u044F \u043F\u043E \u0435\u0433\u043E \u0433\u043E\u043B\u043E\u0432\u0435.`);
|
|
767
|
+
const pull = query.pullRequest;
|
|
768
|
+
if (!ID.test(pull)) throw new TrackerError("not_found", `\u0412 GitHub \u043D\u0435\u0442 pull request ${pull}.`);
|
|
769
|
+
const { body } = await requestJson(`${root}/pulls/${pull}`, { fetch, headers, missing: { failure: "not_found", message: `\u0412 GitHub \u043D\u0435\u0442 pull request ${pull}.` } });
|
|
770
|
+
const head = isRecord(body) && isRecord(body.head) ? text(body.head.sha) : void 0;
|
|
771
|
+
if (head === void 0) throw new TrackerError("failed", "GitHub \u043D\u0435 \u0432\u0435\u0440\u043D\u0443\u043B \u0433\u043E\u043B\u043E\u0432\u0443 pull request.");
|
|
772
|
+
return head;
|
|
773
|
+
};
|
|
774
|
+
return {
|
|
775
|
+
listWorkflows: async (query) => {
|
|
776
|
+
assertListQuery(WORKFLOWS_SUBJECT, { filters: [], clientSide: [], sorts: [] }, query, []);
|
|
777
|
+
const page = pageOf(query.cursor);
|
|
778
|
+
const params = new URLSearchParams({ per_page: String(query.limit), page: String(page) });
|
|
779
|
+
const { body, headers: responseHeaders } = await requestJson(`${actions}/workflows?${params.toString()}`, { fetch, headers });
|
|
780
|
+
return { items: rowsOf(body, "workflows").map(workflowOf), ...nextCursorOf(page, responseHeaders.get("link") ?? "") };
|
|
781
|
+
},
|
|
782
|
+
listRuns: async (query) => {
|
|
783
|
+
assertListQuery(RUNS_SUBJECT, CAPABILITIES.runs, query, RUN_FILTERS);
|
|
784
|
+
const page = pageOf(query.cursor);
|
|
785
|
+
const sha = await headShaOf(query);
|
|
786
|
+
const params = new URLSearchParams({ per_page: String(query.limit), page: String(page) });
|
|
787
|
+
if (query.ref !== void 0) params.set("branch", query.ref);
|
|
788
|
+
if (sha !== void 0) params.set("head_sha", sha);
|
|
789
|
+
if (query.actor !== void 0) params.set("actor", query.actor);
|
|
790
|
+
const status = statusParam(query.status);
|
|
791
|
+
if (status !== void 0) params.set("status", status);
|
|
792
|
+
const event = eventParam(query.event);
|
|
793
|
+
if (event !== void 0) params.set("event", event);
|
|
794
|
+
const created = createdParam(query);
|
|
795
|
+
if (created !== void 0) params.set("created", created);
|
|
796
|
+
const path = query.workflow === void 0 ? `${actions}/runs` : `${actions}/workflows/${encodeURIComponent(query.workflow)}/runs`;
|
|
797
|
+
const { body, headers: responseHeaders } = await requestJson(`${path}?${params.toString()}`, { fetch, headers });
|
|
798
|
+
const items = rowsOf(body, "workflow_runs").map(runOf).filter(
|
|
799
|
+
(run) => (query.status === void 0 || query.status.includes(run.status.category)) && (query.event === void 0 || run.event !== void 0 && query.event.includes(run.event.category)) && inTimeRange(run.updatedAt, query.updatedAfter, query.updatedBefore)
|
|
800
|
+
);
|
|
801
|
+
return { items, ...nextCursorOf(page, responseHeaders.get("link") ?? "") };
|
|
802
|
+
},
|
|
803
|
+
getRun,
|
|
804
|
+
listJobs: async (key, query) => {
|
|
805
|
+
const run = runKey(key);
|
|
806
|
+
const { allAttempts, ...rest } = query;
|
|
807
|
+
assertListQuery(JOBS_SUBJECT, CAPABILITIES.jobs, rest, JOB_FILTERS);
|
|
808
|
+
const page = pageOf(query.cursor);
|
|
809
|
+
const params = new URLSearchParams({ per_page: String(query.limit), page: String(page), filter: allAttempts === true ? "all" : "latest" });
|
|
810
|
+
const { body, headers: responseHeaders } = await requestJson(`${actions}/runs/${run}/jobs?${params.toString()}`, { fetch, headers, missing: missingRun(run) });
|
|
811
|
+
const items = rowsOf(body, "jobs").map(jobOf).filter((job) => query.status === void 0 || query.status.includes(job.status.category));
|
|
812
|
+
return { items, ...nextCursorOf(page, responseHeaders.get("link") ?? "") };
|
|
813
|
+
},
|
|
814
|
+
getJob: async (key) => {
|
|
815
|
+
const id = jobKey(key);
|
|
816
|
+
return jobOf((await requestJson(`${actions}/jobs/${id}`, { fetch, headers, missing: missingJob(id) })).body);
|
|
817
|
+
},
|
|
818
|
+
log: async (key, query) => {
|
|
819
|
+
const id = jobKey(key);
|
|
820
|
+
const window = logWindow(query);
|
|
821
|
+
const job = (await requestJson(`${actions}/jobs/${id}`, { fetch, headers, missing: missingJob(id) })).body;
|
|
822
|
+
const complete = isFinished(statusOf(isRecord(job) ? job : {}).category);
|
|
823
|
+
if (!complete) return { text: "", truncated: false, complete: false, nextCursor: logCursor(0, 0) };
|
|
824
|
+
const response = await requestText(`${actions}/jobs/${id}/logs`, {
|
|
825
|
+
fetch,
|
|
826
|
+
headers,
|
|
827
|
+
skip: window.resume?.offset ?? 0,
|
|
828
|
+
keep: { bytes: window.bytes, from: window.from },
|
|
829
|
+
missing: missingJob(id)
|
|
830
|
+
});
|
|
831
|
+
return logOf({
|
|
832
|
+
text: response.text,
|
|
833
|
+
truncated: window.from === "tail" ? response.start > 0 : !response.whole,
|
|
834
|
+
complete,
|
|
835
|
+
page: 0,
|
|
836
|
+
end: response.end,
|
|
837
|
+
// Курсор дальше имеет смысл только когда окно head оборвалось раньше конца тела: tail уже читает конец.
|
|
838
|
+
more: window.from === "head" && !response.whole,
|
|
839
|
+
...response.whole ? { size: response.read } : {},
|
|
840
|
+
raw: query.raw ?? false
|
|
841
|
+
});
|
|
842
|
+
},
|
|
843
|
+
trigger: async (input) => {
|
|
844
|
+
const workflows = input.workflows ?? [];
|
|
845
|
+
const workflow = workflows[0];
|
|
846
|
+
if (workflows.length !== 1 || workflow === void 0) {
|
|
847
|
+
throw new TrackerError("invalid_request", `${TRIGGER_SUBJECT}: \u043D\u0443\u0436\u0435\u043D \u0440\u043E\u0432\u043D\u043E \u043E\u0434\u0438\u043D workflow \u2014 id \u0438\u043B\u0438 \u0438\u043C\u044F \u0444\u0430\u0439\u043B\u0430.`);
|
|
848
|
+
}
|
|
849
|
+
const inputs = input.inputs ?? {};
|
|
850
|
+
const count = Object.keys(inputs).length;
|
|
851
|
+
if (count > DISPATCH_INPUTS_MAX) throw new TrackerError("invalid_request", `${TRIGGER_SUBJECT}: workflow_dispatch \u043F\u0440\u0438\u043D\u0438\u043C\u0430\u0435\u0442 \u043D\u0435 \u0431\u043E\u043B\u044C\u0448\u0435 ${String(DISPATCH_INPUTS_MAX)} \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432.`);
|
|
852
|
+
const { body } = await requestJson(`${actions}/workflows/${encodeURIComponent(workflow)}/dispatches`, {
|
|
853
|
+
fetch,
|
|
854
|
+
headers,
|
|
855
|
+
method: "POST",
|
|
856
|
+
body: { ref: input.ref, ...count === 0 ? {} : { inputs }, return_run_details: true }
|
|
857
|
+
});
|
|
858
|
+
const run = runIdOf(isRecord(body) ? body.workflow_run_id : void 0);
|
|
859
|
+
return run === void 0 ? {} : { run: await getRun(run) };
|
|
860
|
+
},
|
|
861
|
+
rerun: async (key, input) => {
|
|
862
|
+
const run = runKey(key);
|
|
863
|
+
await requestJson(`${actions}/runs/${run}/${input.failedOnly === true ? "rerun-failed-jobs" : "rerun"}`, {
|
|
864
|
+
fetch,
|
|
865
|
+
headers,
|
|
866
|
+
method: "POST",
|
|
867
|
+
body: { enable_debug_logging: input.debug === true },
|
|
868
|
+
missing: missingRun(run)
|
|
869
|
+
});
|
|
870
|
+
return getRun(run);
|
|
871
|
+
},
|
|
872
|
+
rerunJob: async (key, input) => {
|
|
873
|
+
const id = jobKey(key);
|
|
874
|
+
const job = (await requestJson(`${actions}/jobs/${id}`, { fetch, headers, missing: missingJob(id) })).body;
|
|
875
|
+
const run = runIdOf(isRecord(job) ? job.run_id : void 0);
|
|
876
|
+
if (run === void 0) throw new TrackerError("failed", "GitHub \u043D\u0435 \u0432\u0435\u0440\u043D\u0443\u043B \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u0434\u0430\u043D\u0438\u044F.");
|
|
877
|
+
await requestJson(`${actions}/jobs/${id}/rerun`, { fetch, headers, method: "POST", body: { enable_debug_logging: input.debug === true }, missing: missingJob(id) });
|
|
878
|
+
return getRun(run);
|
|
879
|
+
},
|
|
880
|
+
cancel: async (key) => {
|
|
881
|
+
const run = runKey(key);
|
|
882
|
+
await requestJson(`${actions}/runs/${run}/cancel`, { fetch, headers, method: "POST", conflictOn: [409], missing: missingRun(run) });
|
|
883
|
+
return getRun(run);
|
|
884
|
+
},
|
|
885
|
+
cancelJob: async () => {
|
|
886
|
+
throw new TrackerError("unsupported", "\u041E\u0442\u043C\u0435\u043D\u0430 CI GitHub: \u0437\u0430\u0434\u0430\u043D\u0438\u044F \u043F\u043E \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E\u0441\u0442\u0438 \u043D\u0435 \u043E\u0442\u043C\u0435\u043D\u044F\u044E\u0442\u0441\u044F, \u043E\u0442\u043C\u0435\u043D\u044F\u0435\u0442\u0441\u044F \u043F\u0440\u043E\u0433\u043E\u043D.");
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
function pageOf(cursor) {
|
|
891
|
+
const page = cursor === void 0 ? 1 : Number(cursor);
|
|
892
|
+
return Number.isInteger(page) && page >= 1 ? page : 1;
|
|
893
|
+
}
|
|
894
|
+
function nextCursorOf(page, link) {
|
|
895
|
+
return /rel="next"/.test(link) ? { nextCursor: String(page + 1) } : {};
|
|
896
|
+
}
|
|
897
|
+
function rowsOf(body, field) {
|
|
898
|
+
const rows = isRecord(body) ? body[field] : void 0;
|
|
899
|
+
return Array.isArray(rows) ? rows : [];
|
|
900
|
+
}
|
|
901
|
+
function keyOf(value) {
|
|
902
|
+
if (typeof value === "number" && Number.isSafeInteger(value)) return String(value);
|
|
903
|
+
return typeof value === "string" && value !== "" ? value : "?";
|
|
904
|
+
}
|
|
905
|
+
function positiveInt(value) {
|
|
906
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
|
|
907
|
+
}
|
|
908
|
+
function runIdOf(value) {
|
|
909
|
+
const id = positiveInt(value);
|
|
910
|
+
return id === void 0 ? void 0 : String(id);
|
|
911
|
+
}
|
|
912
|
+
function statusOf(item) {
|
|
913
|
+
const raw = text(item.status) ?? "unknown";
|
|
914
|
+
if (raw !== "completed") return { category: lookup(GITHUB_RUN_STATUSES, raw, "running"), name: raw };
|
|
915
|
+
const conclusion = text(item.conclusion);
|
|
916
|
+
return { category: lookup(GITHUB_RUN_CONCLUSIONS, conclusion, "failed"), name: conclusion ?? raw };
|
|
917
|
+
}
|
|
918
|
+
function statusParam(status) {
|
|
919
|
+
if (status === void 0 || status.length === 0) return void 0;
|
|
920
|
+
if (status.every(isFinished)) return "completed";
|
|
921
|
+
return status.length === 1 && status[0] === "running" ? "in_progress" : void 0;
|
|
922
|
+
}
|
|
923
|
+
function eventParam(event) {
|
|
924
|
+
if (event === void 0 || event.length !== 1) return void 0;
|
|
925
|
+
const raws = keysFor(GITHUB_RUN_EVENTS, event);
|
|
926
|
+
return raws.length === 1 ? raws[0] : void 0;
|
|
927
|
+
}
|
|
928
|
+
function createdParam(query) {
|
|
929
|
+
const { createdAfter: after, createdBefore: before } = query;
|
|
930
|
+
if (after !== void 0 && before !== void 0) return `${isoTime(after)}..${isoTime(before)}`;
|
|
931
|
+
if (after !== void 0) return `>=${isoTime(after)}`;
|
|
932
|
+
return before !== void 0 ? `<=${isoTime(before)}` : void 0;
|
|
933
|
+
}
|
|
934
|
+
function workflowOf(raw) {
|
|
935
|
+
const item = isRecord(raw) ? raw : {};
|
|
936
|
+
const key = keyOf(item.id);
|
|
937
|
+
const path = text(item.path);
|
|
938
|
+
return { key, name: text(item.name) ?? key, ...path === void 0 ? {} : { path }, enabled: item.state === "active" };
|
|
939
|
+
}
|
|
940
|
+
function runOf(raw) {
|
|
941
|
+
const item = isRecord(raw) ? raw : {};
|
|
942
|
+
const key = keyOf(item.id);
|
|
943
|
+
const status = statusOf(item);
|
|
944
|
+
const name = text(item.name) ?? text(item.path);
|
|
945
|
+
const branch = text(item.head_branch);
|
|
946
|
+
const sha = text(item.head_sha);
|
|
947
|
+
const actor = isRecord(item.actor) ? text(item.actor.login) : void 0;
|
|
948
|
+
const pull = Array.isArray(item.pull_requests) ? item.pull_requests[0] : void 0;
|
|
949
|
+
const pullNumber = isRecord(pull) && typeof pull.number === "number" ? String(pull.number) : void 0;
|
|
950
|
+
const number = positiveInt(item.run_number);
|
|
951
|
+
const attempt = positiveInt(item.run_attempt);
|
|
952
|
+
const url = text(item.html_url);
|
|
953
|
+
const createdAt = toTimestamp(item.created_at);
|
|
954
|
+
const startedAt = toTimestamp(item.run_started_at);
|
|
955
|
+
const updatedAt = toTimestamp(item.updated_at);
|
|
956
|
+
const finishedAt = text(item.status) === "completed" ? updatedAt : void 0;
|
|
957
|
+
const event = text(item.event);
|
|
958
|
+
return {
|
|
959
|
+
key,
|
|
960
|
+
...number === void 0 ? {} : { number },
|
|
961
|
+
workflows: name === void 0 ? [] : [name],
|
|
962
|
+
status,
|
|
963
|
+
...event === void 0 ? {} : { event: { category: lookup(GITHUB_RUN_EVENTS, event, "other"), name: event } },
|
|
964
|
+
...branch === void 0 ? {} : { ref: branch },
|
|
965
|
+
...sha === void 0 || !SHA.test(sha) ? {} : { sha },
|
|
966
|
+
...actor === void 0 ? {} : { actor },
|
|
967
|
+
...pullNumber === void 0 ? {} : { pullRequest: pullNumber },
|
|
968
|
+
...attempt === void 0 ? {} : { attempt },
|
|
969
|
+
links: url === void 0 ? [] : [{ url, title: "GitHub Actions" }],
|
|
970
|
+
...createdAt === void 0 ? {} : { createdAt },
|
|
971
|
+
...startedAt === void 0 ? {} : { startedAt },
|
|
972
|
+
...finishedAt === void 0 ? {} : { finishedAt },
|
|
973
|
+
...updatedAt === void 0 ? {} : { updatedAt }
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
function jobOf(raw) {
|
|
977
|
+
const item = isRecord(raw) ? raw : {};
|
|
978
|
+
const key = keyOf(item.id);
|
|
979
|
+
const url = text(item.html_url);
|
|
980
|
+
const startedAt = toTimestamp(item.started_at);
|
|
981
|
+
const finishedAt = toTimestamp(item.completed_at);
|
|
982
|
+
const steps = Array.isArray(item.steps) ? item.steps.map(stepOf) : void 0;
|
|
983
|
+
return {
|
|
984
|
+
key,
|
|
985
|
+
runKey: runIdOf(item.run_id) ?? "?",
|
|
986
|
+
name: text(item.name) ?? key,
|
|
987
|
+
status: statusOf(item),
|
|
988
|
+
...steps === void 0 ? {} : { steps },
|
|
989
|
+
links: url === void 0 ? [] : [{ url, title: "GitHub Actions" }],
|
|
990
|
+
...startedAt === void 0 ? {} : { startedAt },
|
|
991
|
+
...finishedAt === void 0 ? {} : { finishedAt }
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
function stepOf(raw) {
|
|
995
|
+
const item = isRecord(raw) ? raw : {};
|
|
996
|
+
const number = positiveInt(item.number) ?? 0;
|
|
997
|
+
return { number, name: text(item.name) ?? String(number), status: statusOf(item) };
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// src/tracker/gitlab-ci.lib.ts
|
|
1001
|
+
var DEFAULT_URL = "https://gitlab.com";
|
|
1002
|
+
var TARGET2 = /^(\d{1,15}|[\w.-]+(\/[\w.-]+)+)$/;
|
|
1003
|
+
var ID2 = /^[1-9]\d{0,19}$/;
|
|
1004
|
+
var RUN_FILTERS2 = ciRunFilterSchema.options;
|
|
1005
|
+
var JOB_FILTERS2 = ciJobFilterSchema.options;
|
|
1006
|
+
var CAPABILITIES2 = {
|
|
1007
|
+
workflows: false,
|
|
1008
|
+
runs: { filters: RUN_FILTERS2, clientSide: ["status", "event"], sorts: ["created", "updated"] },
|
|
1009
|
+
jobs: { filters: JOB_FILTERS2, clientSide: [], sorts: [] },
|
|
1010
|
+
log: { live: true, cursor: true },
|
|
1011
|
+
trigger: { supported: true, workflows: "none", inputs: true, tags: true },
|
|
1012
|
+
rerun: { all: false, failedOnly: true, job: true, debug: false },
|
|
1013
|
+
cancel: { run: true, job: true }
|
|
1014
|
+
};
|
|
1015
|
+
function gitlabCi() {
|
|
1016
|
+
return { capabilities: CAPABILITIES2, create: (context) => createGitlabCiAdapter(context) };
|
|
1017
|
+
}
|
|
1018
|
+
function createGitlabCiAdapter({ connection, fetch }) {
|
|
1019
|
+
const target = requireTarget(connection.target, "\u043F\u0443\u0442\u044C \u043F\u0440\u043E\u0435\u043A\u0442\u0430 GitLab, group/project, \u0438\u043B\u0438 \u0435\u0433\u043E id");
|
|
1020
|
+
if (!TARGET2.test(target)) throw new TrackerError("invalid_request", "target GitLab \u2014 group/project \u0438\u043B\u0438 \u0447\u0438\u0441\u043B\u043E\u0432\u043E\u0439 id.");
|
|
1021
|
+
const api = `${baseUrl(connection.url, DEFAULT_URL)}/api/v4`;
|
|
1022
|
+
const project = `${api}/projects/${encodeURIComponent(target)}`;
|
|
1023
|
+
const headers = connection.token === void 0 || connection.token === "" ? {} : { "private-token": connection.token };
|
|
1024
|
+
const rawOf = (table, categories) => {
|
|
1025
|
+
if (categories === void 0 || categories.length !== 1) return void 0;
|
|
1026
|
+
const raws = keysFor(table, categories);
|
|
1027
|
+
return raws.length === 1 ? raws[0] : void 0;
|
|
1028
|
+
};
|
|
1029
|
+
const pipelineKey = (key) => {
|
|
1030
|
+
if (!ID2.test(key)) throw new TrackerError("not_found", `\u0412 GitLab \u043D\u0435\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 ${key}.`);
|
|
1031
|
+
return key;
|
|
1032
|
+
};
|
|
1033
|
+
const jobKey = (key) => {
|
|
1034
|
+
if (!ID2.test(key)) throw new TrackerError("not_found", `\u0412 GitLab \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u043D\u0438\u044F ${key}.`);
|
|
1035
|
+
return key;
|
|
1036
|
+
};
|
|
1037
|
+
const missingPipeline = (key) => ({ failure: "not_found", message: `\u0412 GitLab \u043D\u0435\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 ${key}.` });
|
|
1038
|
+
const missingJob = (key) => ({ failure: "not_found", message: `\u0412 GitLab \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u043D\u0438\u044F ${key}.` });
|
|
1039
|
+
const getPipeline = async (key) => runOf2((await requestJson(`${project}/pipelines/${pipelineKey(key)}`, { fetch, headers, missing: missingPipeline(key) })).body);
|
|
1040
|
+
return {
|
|
1041
|
+
listWorkflows: async () => {
|
|
1042
|
+
throw new TrackerError("unsupported", "\u041F\u0440\u043E\u0433\u043E\u043D\u044B CI GitLab: \u0441\u043F\u0438\u0441\u043A\u0430 workflow \u0443 GitLab \u043D\u0435\u0442.");
|
|
1043
|
+
},
|
|
1044
|
+
listRuns: async (query) => {
|
|
1045
|
+
assertListQuery("\u041F\u0440\u043E\u0433\u043E\u043D\u044B CI GitLab", CAPABILITIES2.runs, query, RUN_FILTERS2);
|
|
1046
|
+
const params = new URLSearchParams({ per_page: String(query.limit), page: String(pageOf2(query.cursor)) });
|
|
1047
|
+
let body;
|
|
1048
|
+
let responseHeaders;
|
|
1049
|
+
let keep;
|
|
1050
|
+
if (query.pullRequest !== void 0) {
|
|
1051
|
+
const mr = query.pullRequest;
|
|
1052
|
+
if (!ID2.test(mr)) throw new TrackerError("not_found", `\u0412 GitLab \u043D\u0435\u0442 merge request ${mr}.`);
|
|
1053
|
+
if (query.sort !== void 0) throw new TrackerError("unsupported", "\u041F\u0440\u043E\u0433\u043E\u043D\u044B CI GitLab \u0434\u043B\u044F merge request: \u0441\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u043A\u0430 \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F.");
|
|
1054
|
+
if (query.actor !== void 0) throw new TrackerError("unsupported", "\u041F\u0440\u043E\u0433\u043E\u043D\u044B CI GitLab \u0434\u043B\u044F merge request: \u0444\u0438\u043B\u044C\u0442\u0440 actor \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F.");
|
|
1055
|
+
if (query.workflow !== void 0) throw new TrackerError("unsupported", "\u041F\u0440\u043E\u0433\u043E\u043D\u044B CI GitLab \u0434\u043B\u044F merge request: \u0444\u0438\u043B\u044C\u0442\u0440 workflow \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F.");
|
|
1056
|
+
const result = await requestJson(`${project}/merge_requests/${mr}/pipelines?${params.toString()}`, { fetch, headers, missing: { failure: "not_found", message: `\u0412 GitLab \u043D\u0435\u0442 merge request ${mr}.` } });
|
|
1057
|
+
body = result.body;
|
|
1058
|
+
responseHeaders = result.headers;
|
|
1059
|
+
keep = (run) => (query.status === void 0 || query.status.includes(run.status.category)) && (query.event === void 0 || run.event !== void 0 && query.event.includes(run.event.category)) && (query.ref === void 0 || run.ref === query.ref) && (query.sha === void 0 || run.sha === query.sha) && inTimeRange(run.createdAt, query.createdAfter, query.createdBefore) && inTimeRange(run.updatedAt, query.updatedAfter, query.updatedBefore);
|
|
1060
|
+
} else {
|
|
1061
|
+
if (query.ref !== void 0) params.set("ref", query.ref);
|
|
1062
|
+
if (query.sha !== void 0) params.set("sha", query.sha);
|
|
1063
|
+
if (query.actor !== void 0) params.set("username", query.actor);
|
|
1064
|
+
if (query.workflow !== void 0) params.set("name", query.workflow);
|
|
1065
|
+
if (query.status !== void 0 && query.status.length > 0) {
|
|
1066
|
+
const status = rawOf(GITLAB_CI_STATUSES, query.status);
|
|
1067
|
+
if (status !== void 0) params.set("status", status);
|
|
1068
|
+
if (status === void 0 && query.status.every(isFinished)) params.set("scope", "finished");
|
|
1069
|
+
}
|
|
1070
|
+
const source = rawOf(GITLAB_PIPELINE_SOURCES, query.event);
|
|
1071
|
+
if (source !== void 0) params.set("source", source);
|
|
1072
|
+
if (query.createdAfter !== void 0) params.set("created_after", isoTime(query.createdAfter));
|
|
1073
|
+
if (query.createdBefore !== void 0) params.set("created_before", isoTime(query.createdBefore));
|
|
1074
|
+
if (query.updatedAfter !== void 0) params.set("updated_after", isoTime(query.updatedAfter));
|
|
1075
|
+
if (query.updatedBefore !== void 0) params.set("updated_before", isoTime(query.updatedBefore));
|
|
1076
|
+
params.set("order_by", query.sort?.field === "updated" ? "updated_at" : "id");
|
|
1077
|
+
params.set("sort", query.sort?.direction ?? "desc");
|
|
1078
|
+
const result = await requestJson(`${project}/pipelines?${params.toString()}`, { fetch, headers });
|
|
1079
|
+
body = result.body;
|
|
1080
|
+
responseHeaders = result.headers;
|
|
1081
|
+
keep = (run) => (query.status === void 0 || query.status.includes(run.status.category)) && (query.event === void 0 || run.event !== void 0 && query.event.includes(run.event.category));
|
|
1082
|
+
}
|
|
1083
|
+
const items = (Array.isArray(body) ? body : []).map(runOf2).filter(keep);
|
|
1084
|
+
const next = responseHeaders.get("x-next-page");
|
|
1085
|
+
return { items, ...next === null || next === "" ? {} : { nextCursor: next } };
|
|
1086
|
+
},
|
|
1087
|
+
getRun: getPipeline,
|
|
1088
|
+
listJobs: async (runKey, query) => {
|
|
1089
|
+
const { allAttempts, ...rest } = query;
|
|
1090
|
+
assertListQuery("\u0417\u0430\u0434\u0430\u043D\u0438\u044F CI GitLab", CAPABILITIES2.jobs, rest, JOB_FILTERS2);
|
|
1091
|
+
const params = new URLSearchParams({ per_page: String(query.limit), page: String(pageOf2(query.cursor)), include_retried: String(allAttempts ?? false) });
|
|
1092
|
+
for (const scope of keysFor(GITLAB_CI_STATUSES, query.status ?? [])) params.append("scope[]", scope);
|
|
1093
|
+
const { body, headers: responseHeaders } = await requestJson(`${project}/pipelines/${pipelineKey(runKey)}/jobs?${params.toString()}`, { fetch, headers, missing: missingPipeline(runKey) });
|
|
1094
|
+
const items = (Array.isArray(body) ? body : []).map(jobOf2);
|
|
1095
|
+
const next = responseHeaders.get("x-next-page");
|
|
1096
|
+
return { items, ...next === null || next === "" ? {} : { nextCursor: next } };
|
|
1097
|
+
},
|
|
1098
|
+
getJob: async (key) => jobOf2((await requestJson(`${project}/jobs/${jobKey(key)}`, { fetch, headers, missing: missingJob(key) })).body),
|
|
1099
|
+
log: async (key, query) => {
|
|
1100
|
+
const window = logWindow(query);
|
|
1101
|
+
const job = (await requestJson(`${project}/jobs/${jobKey(key)}`, { fetch, headers, missing: missingJob(key) })).body;
|
|
1102
|
+
const rawStatus = isRecord(job) && typeof job.status === "string" ? job.status : "unknown";
|
|
1103
|
+
const complete = isFinished(lookup(GITLAB_CI_STATUSES, rawStatus, "running"));
|
|
1104
|
+
const response = await requestText(`${project}/jobs/${jobKey(key)}/trace`, { fetch, headers, skip: window.resume?.offset ?? 0, keep: { bytes: window.bytes, from: window.from }, missing: missingJob(key) });
|
|
1105
|
+
return logOf({
|
|
1106
|
+
text: response.text,
|
|
1107
|
+
truncated: window.from === "tail" ? response.start > 0 : !response.whole,
|
|
1108
|
+
complete,
|
|
1109
|
+
page: 0,
|
|
1110
|
+
end: response.end,
|
|
1111
|
+
more: !response.whole,
|
|
1112
|
+
...response.whole ? { size: response.read } : {},
|
|
1113
|
+
// Кольцевой буфер отрезал начало хвоста и при потолке чтения (whole=false): там default
|
|
1114
|
+
// startCut (truncated && !more) молчит, а первая неполная строка в окне остаётся.
|
|
1115
|
+
startCut: window.from === "tail" && response.start > 0,
|
|
1116
|
+
raw: query.raw ?? false
|
|
1117
|
+
});
|
|
1118
|
+
},
|
|
1119
|
+
trigger: async (input) => {
|
|
1120
|
+
if (input.workflows !== void 0 && input.workflows.length > 0) {
|
|
1121
|
+
throw new TrackerError("unsupported", "\u0417\u0430\u043F\u0443\u0441\u043A CI GitLab: workflow \u043D\u0435 \u0437\u0430\u0434\u0430\u0451\u0442\u0441\u044F \u2014 \u0442\u043E\u043B\u044C\u043A\u043E ref \u0438 \u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u044B\u0435.");
|
|
1122
|
+
}
|
|
1123
|
+
const variables = Object.entries(input.inputs ?? {}).map(([key, value]) => ({ key, value, variable_type: "env_var" }));
|
|
1124
|
+
const { body } = await requestJson(`${project}/pipeline`, {
|
|
1125
|
+
fetch,
|
|
1126
|
+
headers,
|
|
1127
|
+
method: "POST",
|
|
1128
|
+
body: { ref: input.ref, ...variables.length === 0 ? {} : { variables } },
|
|
1129
|
+
missing: { failure: "not_found", message: `\u0412 GitLab \u043D\u0435\u0442 \u0432\u0435\u0442\u043A\u0438 \u0438\u043B\u0438 \u0442\u0435\u0433\u0430 ${input.ref}.` }
|
|
1130
|
+
});
|
|
1131
|
+
return { run: runOf2(body) };
|
|
1132
|
+
},
|
|
1133
|
+
rerun: async (runKey, input) => {
|
|
1134
|
+
if (input.failedOnly !== true) throw new TrackerError("unsupported", "\u041F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A CI GitLab \u0443\u043C\u0435\u0435\u0442 \u0442\u043E\u043B\u044C\u043A\u043E \u0443\u043F\u0430\u0432\u0448\u0438\u0435 \u0437\u0430\u0434\u0430\u043D\u0438\u044F: \u0437\u0430\u0434\u0430\u0439\u0442\u0435 failedOnly.");
|
|
1135
|
+
if (input.debug === true) throw new TrackerError("unsupported", "\u041F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A CI GitLab \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u043E\u0442\u043B\u0430\u0434\u043E\u0447\u043D\u043E\u0435 \u043B\u043E\u0433\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435.");
|
|
1136
|
+
return runOf2((await requestJson(`${project}/pipelines/${pipelineKey(runKey)}/retry`, { fetch, headers, method: "POST", missing: missingPipeline(runKey) })).body);
|
|
1137
|
+
},
|
|
1138
|
+
rerunJob: async (jobKeyValue, input) => {
|
|
1139
|
+
if (input.debug === true) throw new TrackerError("unsupported", "\u041F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A \u0437\u0430\u0434\u0430\u043D\u0438\u044F GitLab \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u043E\u0442\u043B\u0430\u0434\u043E\u0447\u043D\u043E\u0435 \u043B\u043E\u0433\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435.");
|
|
1140
|
+
const body = (await requestJson(`${project}/jobs/${jobKey(jobKeyValue)}/retry`, { fetch, headers, method: "POST", missing: missingJob(jobKeyValue) })).body;
|
|
1141
|
+
const pipeline = isRecord(body) && isRecord(body.pipeline) && typeof body.pipeline.id === "number" ? String(body.pipeline.id) : void 0;
|
|
1142
|
+
if (pipeline === void 0) throw new TrackerError("failed", "GitLab \u043D\u0435 \u0432\u0435\u0440\u043D\u0443\u043B \u043F\u0430\u0439\u043F\u043B\u0430\u0439\u043D \u043D\u043E\u0432\u043E\u0433\u043E \u0437\u0430\u0434\u0430\u043D\u0438\u044F.");
|
|
1143
|
+
return getPipeline(pipeline);
|
|
1144
|
+
},
|
|
1145
|
+
cancel: async (runKey) => runOf2((await requestJson(`${project}/pipelines/${pipelineKey(runKey)}/cancel`, { fetch, headers, method: "POST", missing: missingPipeline(runKey) })).body),
|
|
1146
|
+
cancelJob: async (jobKeyValue) => jobOf2((await requestJson(`${project}/jobs/${jobKey(jobKeyValue)}/cancel`, { fetch, headers, method: "POST", missing: missingJob(jobKeyValue) })).body)
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
function pageOf2(cursor) {
|
|
1150
|
+
const page = cursor === void 0 ? 1 : Number(cursor);
|
|
1151
|
+
return Number.isInteger(page) && page >= 1 ? page : 1;
|
|
1152
|
+
}
|
|
1153
|
+
function runOf2(raw) {
|
|
1154
|
+
const item = isRecord(raw) ? raw : {};
|
|
1155
|
+
const key = typeof item.id === "number" ? String(item.id) : "?";
|
|
1156
|
+
const rawStatus = typeof item.status === "string" ? item.status : "unknown";
|
|
1157
|
+
const rawSource = text(item.source);
|
|
1158
|
+
const mr = typeof item.ref === "string" ? /^refs\/merge-requests\/(\d+)\/head$/.exec(item.ref) : null;
|
|
1159
|
+
const user = isRecord(item.user) ? text(item.user.username) : void 0;
|
|
1160
|
+
const url = text(item.web_url);
|
|
1161
|
+
const name = text(item.name);
|
|
1162
|
+
const createdAt = toTimestamp(item.created_at);
|
|
1163
|
+
const startedAt = toTimestamp(item.started_at);
|
|
1164
|
+
const finishedAt = toTimestamp(item.finished_at);
|
|
1165
|
+
const updatedAt = toTimestamp(item.updated_at);
|
|
1166
|
+
return {
|
|
1167
|
+
key,
|
|
1168
|
+
...typeof item.iid === "number" ? { number: item.iid } : {},
|
|
1169
|
+
workflows: name === void 0 ? [] : [name],
|
|
1170
|
+
status: { category: lookup(GITLAB_CI_STATUSES, rawStatus, "running"), name: rawStatus },
|
|
1171
|
+
...rawSource === void 0 ? {} : { event: { category: lookup(GITLAB_PIPELINE_SOURCES, rawSource, "other"), name: rawSource } },
|
|
1172
|
+
...typeof item.ref === "string" && item.ref !== "" ? { ref: item.ref } : {},
|
|
1173
|
+
...typeof item.sha === "string" && /^[0-9a-f]{7,64}$/.test(item.sha) ? { sha: item.sha } : {},
|
|
1174
|
+
...user === void 0 ? {} : { actor: user },
|
|
1175
|
+
...mr === null ? {} : { pullRequest: mr[1] },
|
|
1176
|
+
links: url === void 0 ? [] : [{ url, title: "GitLab" }],
|
|
1177
|
+
...createdAt === void 0 ? {} : { createdAt },
|
|
1178
|
+
...startedAt === void 0 ? {} : { startedAt },
|
|
1179
|
+
...finishedAt === void 0 ? {} : { finishedAt },
|
|
1180
|
+
...updatedAt === void 0 ? {} : { updatedAt }
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
function jobOf2(raw) {
|
|
1184
|
+
const item = isRecord(raw) ? raw : {};
|
|
1185
|
+
const key = typeof item.id === "number" ? String(item.id) : "?";
|
|
1186
|
+
const rawStatus = typeof item.status === "string" ? item.status : "unknown";
|
|
1187
|
+
const pipeline = isRecord(item.pipeline) && typeof item.pipeline.id === "number" ? String(item.pipeline.id) : "?";
|
|
1188
|
+
const url = text(item.web_url);
|
|
1189
|
+
const stage = text(item.stage);
|
|
1190
|
+
const startedAt = toTimestamp(item.started_at);
|
|
1191
|
+
const finishedAt = toTimestamp(item.finished_at);
|
|
1192
|
+
return {
|
|
1193
|
+
key,
|
|
1194
|
+
runKey: pipeline,
|
|
1195
|
+
name: text(item.name) ?? key,
|
|
1196
|
+
...stage === void 0 ? {} : { stage },
|
|
1197
|
+
status: { category: lookup(GITLAB_CI_STATUSES, rawStatus, "running"), name: rawStatus },
|
|
1198
|
+
links: url === void 0 ? [] : [{ url, title: "GitLab" }],
|
|
1199
|
+
...startedAt === void 0 ? {} : { startedAt },
|
|
1200
|
+
...finishedAt === void 0 ? {} : { finishedAt }
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// src/tracker/sourcecraft-ci.lib.ts
|
|
1205
|
+
var DEFAULT_API2 = "https://api.sourcecraft.tech";
|
|
1206
|
+
var TARGET3 = /^[\w.-]+\/[\w.-]+$/;
|
|
1207
|
+
var RUN_SLUG = /^\d{1,20}$/;
|
|
1208
|
+
var PR_SLUG = /^[\w-]{1,100}$/;
|
|
1209
|
+
var JOB_KEY = /^([^/]+)\/([^/]+)\/([^/]+)\/([^/]+)$/;
|
|
1210
|
+
var EXTERNAL_USER = "<external-user>";
|
|
1211
|
+
var JOB_OFFSET_CURSOR = /^o:(\d{1,15})$/;
|
|
1212
|
+
var RUN_FILTER_NAMES = ciRunFilterSchema.options;
|
|
1213
|
+
var RUN_FILTERS3 = RUN_FILTER_NAMES.filter((name) => name !== "ref" && name !== "sha");
|
|
1214
|
+
var JOB_FILTER_NAMES = ciJobFilterSchema.options;
|
|
1215
|
+
var JOB_FILTERS3 = ["status"];
|
|
1216
|
+
var CAPABILITIES3 = {
|
|
1217
|
+
workflows: false,
|
|
1218
|
+
runs: { filters: RUN_FILTERS3, clientSide: RUN_FILTERS3, sorts: [] },
|
|
1219
|
+
jobs: { filters: JOB_FILTERS3, clientSide: JOB_FILTERS3, sorts: [] },
|
|
1220
|
+
log: { live: true, cursor: true },
|
|
1221
|
+
trigger: { supported: true, workflows: "many", inputs: true, tags: true },
|
|
1222
|
+
rerun: { all: false, failedOnly: false, job: false, debug: false },
|
|
1223
|
+
cancel: { run: false, job: false }
|
|
1224
|
+
};
|
|
1225
|
+
function sourcecraftCi() {
|
|
1226
|
+
return { capabilities: CAPABILITIES3, create: (context) => createSourceCraftCiAdapter(context) };
|
|
1227
|
+
}
|
|
1228
|
+
function createSourceCraftCiAdapter({ connection, fetch }) {
|
|
1229
|
+
const target = requireTarget(connection.target, "org/repo \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F SourceCraft");
|
|
1230
|
+
if (!TARGET3.test(target)) throw new TrackerError("invalid_request", "target SourceCraft \u2014 org/repo.");
|
|
1231
|
+
const repo = `${baseUrl(connection.url, DEFAULT_API2)}/repos/${target}`;
|
|
1232
|
+
const root = `${repo}/cicd`;
|
|
1233
|
+
const headers = { authorization: `Bearer ${connection.token ?? ""}` };
|
|
1234
|
+
const missingRun = (key) => ({ failure: "not_found", message: `\u0412 SourceCraft \u043D\u0435\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 ${key}.` });
|
|
1235
|
+
const missingJob = (key) => ({ failure: "not_found", message: `\u0412 SourceCraft \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u043D\u0438\u044F ${key}.` });
|
|
1236
|
+
const missingWorkflow = (run, workflow) => ({ failure: "not_found", message: `\u0412 \u043F\u0440\u043E\u0433\u043E\u043D\u0435 ${run} SourceCraft \u043D\u0435\u0442 workflow ${workflow}.` });
|
|
1237
|
+
const runKey = (key) => {
|
|
1238
|
+
if (!RUN_SLUG.test(key)) throw new TrackerError("not_found", `\u0412 SourceCraft \u043D\u0435\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 ${key}.`);
|
|
1239
|
+
return key;
|
|
1240
|
+
};
|
|
1241
|
+
const jobPath = (key) => {
|
|
1242
|
+
const match = JOB_KEY.exec(key);
|
|
1243
|
+
if (match === null) throw new TrackerError("not_found", `\u0412 SourceCraft \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u043D\u0438\u044F ${key}.`);
|
|
1244
|
+
const path = { run: partOf(match[1], key), workflow: partOf(match[2], key), task: partOf(match[3], key), cube: partOf(match[4], key) };
|
|
1245
|
+
runKey(path.run);
|
|
1246
|
+
return path;
|
|
1247
|
+
};
|
|
1248
|
+
const getRun = async (key) => runOf3((await requestJson(`${root}/runs/${runKey(key)}`, { fetch, headers, missing: missingRun(key) })).body);
|
|
1249
|
+
const getWorkflow = async (run, workflow) => {
|
|
1250
|
+
const { body } = await requestJson(`${root}/runs/${runKey(run)}/${encodeURIComponent(workflow)}`, { fetch, headers, missing: missingWorkflow(run, workflow) });
|
|
1251
|
+
return isRecord(body) ? body : {};
|
|
1252
|
+
};
|
|
1253
|
+
const pullIdOf = async (slug) => {
|
|
1254
|
+
if (!PR_SLUG.test(slug)) throw new TrackerError("invalid_request", `\u041A\u043B\u044E\u0447 pull request SourceCraft \u2014 slug \u0438\u0437 \u0431\u0443\u043A\u0432, \u0446\u0438\u0444\u0440, _ \u0438 - \u0434\u043B\u0438\u043D\u043E\u0439 \u0434\u043E 100.`);
|
|
1255
|
+
const { body } = await requestJson(`${repo}/pulls/${slug}`, { fetch, headers, missing: { failure: "not_found", message: `\u0412 SourceCraft \u043D\u0435\u0442 pull request ${slug}.` } });
|
|
1256
|
+
const id = isRecord(body) ? text(body.id) : void 0;
|
|
1257
|
+
if (id === void 0) throw new TrackerError("failed", `\u041E\u0442\u0432\u0435\u0442 SourceCraft \u043D\u0430 /pulls/${slug} \u043D\u0435 \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u0442 id.`);
|
|
1258
|
+
return id;
|
|
1259
|
+
};
|
|
1260
|
+
const flattenJobs = async (run) => {
|
|
1261
|
+
const jobs = [];
|
|
1262
|
+
for (const workflow of (await getRun(run)).workflows) {
|
|
1263
|
+
const tree = await getWorkflow(run, workflow);
|
|
1264
|
+
for (const task of recordList(tree.tasks)) {
|
|
1265
|
+
const taskSlug = text(task.slug);
|
|
1266
|
+
if (taskSlug === void 0) continue;
|
|
1267
|
+
for (const cube of recordList(task.cubes)) {
|
|
1268
|
+
const cubeSlug = text(cube.slug);
|
|
1269
|
+
if (cubeSlug === void 0) continue;
|
|
1270
|
+
jobs.push(jobOf3(run, workflow, taskSlug, cubeSlug, cube));
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
return jobs;
|
|
1275
|
+
};
|
|
1276
|
+
return {
|
|
1277
|
+
listWorkflows: async () => {
|
|
1278
|
+
throw new TrackerError("unsupported", "\u041F\u0440\u043E\u0433\u043E\u043D\u044B CI SourceCraft: \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E\u0433\u043E \u0441\u043F\u0438\u0441\u043A\u0430 workflow \u043D\u0435\u0442, workflow \u0435\u0441\u0442\u044C \u0443 \u043F\u0440\u043E\u0433\u043E\u043D\u0430.");
|
|
1279
|
+
},
|
|
1280
|
+
listRuns: async (query) => {
|
|
1281
|
+
assertListQuery("\u041F\u0440\u043E\u0433\u043E\u043D\u044B CI SourceCraft", CAPABILITIES3.runs, query, RUN_FILTER_NAMES);
|
|
1282
|
+
const pullId = query.pullRequest === void 0 ? void 0 : await pullIdOf(query.pullRequest);
|
|
1283
|
+
const params = new URLSearchParams({ page_size: String(query.limit) });
|
|
1284
|
+
if (query.cursor !== void 0) params.set("page_token", query.cursor);
|
|
1285
|
+
const { body } = await requestJson(`${root}/runs?${params.toString()}`, { fetch, headers });
|
|
1286
|
+
const record = isRecord(body) ? body : {};
|
|
1287
|
+
const items = recordList(record.runs).map((raw) => pullSlugOf(runOf3(raw), raw, pullId, query.pullRequest)).filter((run) => keepsRun(run, query));
|
|
1288
|
+
const token = text(record.next_page_token);
|
|
1289
|
+
return { items, ...token === void 0 ? {} : { nextCursor: token } };
|
|
1290
|
+
},
|
|
1291
|
+
getRun,
|
|
1292
|
+
listJobs: async (runKeyValue, query) => {
|
|
1293
|
+
assertListQuery("\u0417\u0430\u0434\u0430\u043D\u0438\u044F CI SourceCraft", CAPABILITIES3.jobs, query, JOB_FILTER_NAMES);
|
|
1294
|
+
const jobs = await flattenJobs(runKey(runKeyValue));
|
|
1295
|
+
const from = offsetOf(query.cursor);
|
|
1296
|
+
const next = from + query.limit;
|
|
1297
|
+
const page = jobs.slice(from, next);
|
|
1298
|
+
const items = query.status === void 0 ? page : page.filter((job) => query.status?.includes(job.status.category) === true);
|
|
1299
|
+
return { items, ...next < jobs.length ? { nextCursor: `o:${String(next)}` } : {} };
|
|
1300
|
+
},
|
|
1301
|
+
getJob: async (key) => {
|
|
1302
|
+
const path = jobPath(key);
|
|
1303
|
+
const tree = await getWorkflow(path.run, path.workflow);
|
|
1304
|
+
for (const task of recordList(tree.tasks)) {
|
|
1305
|
+
if (text(task.slug) !== path.task) continue;
|
|
1306
|
+
const cube = recordList(task.cubes).find((item) => text(item.slug) === path.cube);
|
|
1307
|
+
if (cube !== void 0) return jobOf3(path.run, path.workflow, path.task, path.cube, cube);
|
|
1308
|
+
}
|
|
1309
|
+
throw new TrackerError("not_found", `\u0412 SourceCraft \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u043D\u0438\u044F ${key}.`);
|
|
1310
|
+
},
|
|
1311
|
+
log: async (key, query) => {
|
|
1312
|
+
const parsed = jobPath(key);
|
|
1313
|
+
if (parsed.cube.startsWith("#")) {
|
|
1314
|
+
throw new TrackerError("unsupported", `\u041B\u043E\u0433 \u0437\u0430\u0434\u0430\u043D\u0438\u044F SourceCraft ${key}: \u0441\u043B\u0443\u0436\u0435\u0431\u043D\u044B\u0439 \u043A\u0443\u0431, \u043B\u043E\u0433 \u0447\u0435\u0440\u0435\u0437 API \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D.`);
|
|
1315
|
+
}
|
|
1316
|
+
const window = logWindow(query);
|
|
1317
|
+
const path = pathOf(parsed);
|
|
1318
|
+
const tail = window.resume === void 0 && window.from === "tail";
|
|
1319
|
+
const first = window.resume?.page ?? 1;
|
|
1320
|
+
const deadline = Date.now() + CI_LOG_TIMEOUT_MS;
|
|
1321
|
+
let page = first;
|
|
1322
|
+
let skip = window.resume?.offset ?? 0;
|
|
1323
|
+
let kept = new Uint8Array(0);
|
|
1324
|
+
let dropped = 0;
|
|
1325
|
+
let seen = 0;
|
|
1326
|
+
let lastPage = first;
|
|
1327
|
+
let lastEnd = 0;
|
|
1328
|
+
let done = false;
|
|
1329
|
+
let unread = false;
|
|
1330
|
+
const encoder = new TextEncoder();
|
|
1331
|
+
const decoder = new TextDecoder("utf-8");
|
|
1332
|
+
for (; ; ) {
|
|
1333
|
+
if (!tail && kept.length >= window.bytes) {
|
|
1334
|
+
unread = true;
|
|
1335
|
+
break;
|
|
1336
|
+
}
|
|
1337
|
+
const { body } = await requestJson(`${root}/logs/${path}?page=${String(page)}`, { fetch, headers, missing: missingJob(key) });
|
|
1338
|
+
const record = isRecord(body) ? body : {};
|
|
1339
|
+
const data = encoder.encode(text(record.logs) ?? "");
|
|
1340
|
+
const rest = skip >= data.length ? new Uint8Array(0) : data.subarray(skip);
|
|
1341
|
+
let slice = rest;
|
|
1342
|
+
if (!tail) {
|
|
1343
|
+
const room = rest.subarray(0, Math.min(window.bytes - kept.length, rest.length));
|
|
1344
|
+
slice = room.subarray(0, room.length - trailingByte(room));
|
|
1345
|
+
}
|
|
1346
|
+
const merged = concat(kept, slice);
|
|
1347
|
+
if (merged.length > window.bytes) {
|
|
1348
|
+
const fitted = merged.subarray(merged.length - window.bytes);
|
|
1349
|
+
dropped += merged.length - fitted.length;
|
|
1350
|
+
kept = fitted;
|
|
1351
|
+
} else {
|
|
1352
|
+
kept = merged;
|
|
1353
|
+
}
|
|
1354
|
+
seen += data.length;
|
|
1355
|
+
lastPage = page;
|
|
1356
|
+
lastEnd = tail ? data.length : skip + slice.length;
|
|
1357
|
+
skip = 0;
|
|
1358
|
+
done = record.done === true;
|
|
1359
|
+
if (!tail && rest.length > slice.length) {
|
|
1360
|
+
unread = true;
|
|
1361
|
+
break;
|
|
1362
|
+
}
|
|
1363
|
+
if (done || record.page_complete === false) break;
|
|
1364
|
+
if (page - first + 1 >= CI_LOG_PAGES_MAX || seen >= CI_LOG_READ_MAX_BYTES || Date.now() >= deadline) {
|
|
1365
|
+
unread = true;
|
|
1366
|
+
break;
|
|
1367
|
+
}
|
|
1368
|
+
page += 1;
|
|
1369
|
+
}
|
|
1370
|
+
return logOf({
|
|
1371
|
+
text: decoder.decode(kept.subarray(leadingByte(kept))),
|
|
1372
|
+
truncated: dropped > 0 || unread,
|
|
1373
|
+
complete: done,
|
|
1374
|
+
page: lastPage,
|
|
1375
|
+
end: lastEnd,
|
|
1376
|
+
more: unread,
|
|
1377
|
+
// Размер всего лога известен, только если с первой страницы прочитали их все.
|
|
1378
|
+
size: window.resume === void 0 && !unread ? seen : void 0,
|
|
1379
|
+
// Страниц было прочитано больше окна: начало хвоста отрезано кольцевым буфером, и первая неполная
|
|
1380
|
+
// строка уходит из окна даже когда за ним ещё есть страницы (default startCut там молчит).
|
|
1381
|
+
startCut: tail && dropped > 0,
|
|
1382
|
+
raw: query.raw ?? false
|
|
1383
|
+
});
|
|
1384
|
+
},
|
|
1385
|
+
trigger: async (input) => {
|
|
1386
|
+
const workflows = input.workflows ?? [];
|
|
1387
|
+
if (workflows.length === 0) throw new TrackerError("invalid_request", "\u0417\u0430\u043F\u0443\u0441\u043A CI SourceCraft: \u043D\u0443\u0436\u0435\u043D \u0445\u043E\u0442\u044F \u0431\u044B \u043E\u0434\u0438\u043D workflow.");
|
|
1388
|
+
const values = Object.entries(input.inputs ?? {}).map(([name, value]) => ({ name, value }));
|
|
1389
|
+
const head = input.refType === "tag" ? { tag: input.ref } : { branch: input.ref };
|
|
1390
|
+
const { body } = await requestJson(`${root}/runs`, { fetch, headers, method: "POST", body: { head, workflows: workflows.map((name) => ({ name, values })) } });
|
|
1391
|
+
return { run: runOf3(body) };
|
|
1392
|
+
},
|
|
1393
|
+
// Публичный API SourceCraft не умеет ни перезапускать, ни отменять (§1.3): отказ без запроса (§3.6).
|
|
1394
|
+
rerun: async () => {
|
|
1395
|
+
throw new TrackerError("unsupported", "\u041F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A \u043F\u0440\u043E\u0433\u043E\u043D\u0430 CI SourceCraft \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D: \u0443 \u043F\u043B\u043E\u0449\u0430\u0434\u043A\u0438 \u043D\u0435\u0442 \u0442\u0430\u043A\u043E\u0433\u043E \u0437\u0430\u043F\u0440\u043E\u0441\u0430 \u0432 API.");
|
|
1396
|
+
},
|
|
1397
|
+
rerunJob: async () => {
|
|
1398
|
+
throw new TrackerError("unsupported", "\u041F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A \u0437\u0430\u0434\u0430\u043D\u0438\u044F CI SourceCraft \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D: \u0443 \u043F\u043B\u043E\u0449\u0430\u0434\u043A\u0438 \u043D\u0435\u0442 \u0442\u0430\u043A\u043E\u0433\u043E \u0437\u0430\u043F\u0440\u043E\u0441\u0430 \u0432 API.");
|
|
1399
|
+
},
|
|
1400
|
+
cancel: async () => {
|
|
1401
|
+
throw new TrackerError("unsupported", "\u041E\u0442\u043C\u0435\u043D\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 CI SourceCraft \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430: \u0443 \u043F\u043B\u043E\u0449\u0430\u0434\u043A\u0438 \u043D\u0435\u0442 \u0442\u0430\u043A\u043E\u0433\u043E \u0437\u0430\u043F\u0440\u043E\u0441\u0430 \u0432 API.");
|
|
1402
|
+
},
|
|
1403
|
+
cancelJob: async () => {
|
|
1404
|
+
throw new TrackerError("unsupported", "\u041E\u0442\u043C\u0435\u043D\u0430 \u0437\u0430\u0434\u0430\u043D\u0438\u044F CI SourceCraft \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430: \u0443 \u043F\u043B\u043E\u0449\u0430\u0434\u043A\u0438 \u043D\u0435\u0442 \u0442\u0430\u043A\u043E\u0433\u043E \u0437\u0430\u043F\u0440\u043E\u0441\u0430 \u0432 API.");
|
|
1405
|
+
}
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
function partOf(value, key) {
|
|
1409
|
+
const refusal = new TrackerError("not_found", `\u0412 SourceCraft \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u043D\u0438\u044F ${key}.`);
|
|
1410
|
+
if (value === void 0) throw refusal;
|
|
1411
|
+
let decoded;
|
|
1412
|
+
try {
|
|
1413
|
+
decoded = decodeURIComponent(value);
|
|
1414
|
+
} catch {
|
|
1415
|
+
throw refusal;
|
|
1416
|
+
}
|
|
1417
|
+
if (decoded === "") throw refusal;
|
|
1418
|
+
return decoded;
|
|
1419
|
+
}
|
|
1420
|
+
function pathOf(path) {
|
|
1421
|
+
return [path.run, path.workflow, path.task, path.cube].map(encodeURIComponent).join("/");
|
|
1422
|
+
}
|
|
1423
|
+
function recordList(value) {
|
|
1424
|
+
return Array.isArray(value) ? value.filter(isRecord) : [];
|
|
1425
|
+
}
|
|
1426
|
+
function offsetOf(cursor) {
|
|
1427
|
+
if (cursor === void 0) return 0;
|
|
1428
|
+
const match = JOB_OFFSET_CURSOR.exec(cursor);
|
|
1429
|
+
if (match === null) return 0;
|
|
1430
|
+
const from = Number(match[1]);
|
|
1431
|
+
return Number.isSafeInteger(from) && from >= 0 ? from : 0;
|
|
1432
|
+
}
|
|
1433
|
+
function runOf3(raw) {
|
|
1434
|
+
const item = isRecord(raw) ? raw : {};
|
|
1435
|
+
const key = text(item.slug) ?? "?";
|
|
1436
|
+
const rawStatus = text(item.status) ?? "unknown";
|
|
1437
|
+
const rawEvent = text(item.event_type);
|
|
1438
|
+
const user = isRecord(item.user) ? text(item.user.slug) : void 0;
|
|
1439
|
+
const pull = isRecord(item.pull) ? text(item.pull.slug) : void 0;
|
|
1440
|
+
const dates = isRecord(item.dates) ? item.dates : {};
|
|
1441
|
+
const createdAt = toTimestamp(dates.created_at);
|
|
1442
|
+
const startedAt = toTimestamp(dates.started_at);
|
|
1443
|
+
const finishedAt = toTimestamp(dates.finished_at);
|
|
1444
|
+
const updatedAt = toTimestamp(dates.updated_at);
|
|
1445
|
+
const errors = stringList(item.error_messages);
|
|
1446
|
+
return {
|
|
1447
|
+
key,
|
|
1448
|
+
...RUN_SLUG.test(key) ? { number: Number(key) } : {},
|
|
1449
|
+
workflows: stringList(recordList(item.workflows).map((workflow) => workflow.slug)),
|
|
1450
|
+
status: { category: lookup(SOURCECRAFT_CI_STATUSES, rawStatus, "running"), name: rawStatus },
|
|
1451
|
+
...rawEvent === void 0 ? {} : { event: { category: lookup(SOURCECRAFT_RUN_EVENTS, rawEvent, "other"), name: rawEvent } },
|
|
1452
|
+
...user === void 0 || user === EXTERNAL_USER ? {} : { actor: user },
|
|
1453
|
+
...pull === void 0 ? {} : { pullRequest: pull },
|
|
1454
|
+
...errors.length === 0 ? {} : { errors },
|
|
1455
|
+
links: [],
|
|
1456
|
+
...createdAt === void 0 ? {} : { createdAt },
|
|
1457
|
+
...startedAt === void 0 ? {} : { startedAt },
|
|
1458
|
+
...finishedAt === void 0 ? {} : { finishedAt },
|
|
1459
|
+
...updatedAt === void 0 ? {} : { updatedAt }
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
function jobOf3(run, workflow, task, cubeSlug, cube) {
|
|
1463
|
+
const rawStatus = text(cube.status) ?? "unknown";
|
|
1464
|
+
const dates = isRecord(cube.dates) ? cube.dates : {};
|
|
1465
|
+
const createdAt = toTimestamp(dates.created_at);
|
|
1466
|
+
const startedAt = toTimestamp(dates.started_at);
|
|
1467
|
+
const finishedAt = toTimestamp(dates.finished_at);
|
|
1468
|
+
return {
|
|
1469
|
+
key: [run, workflow, task, cubeSlug].map(encodeURIComponent).join("/"),
|
|
1470
|
+
runKey: run,
|
|
1471
|
+
name: cubeSlug,
|
|
1472
|
+
stage: `${workflow}/${task}`,
|
|
1473
|
+
status: { category: lookup(SOURCECRAFT_CI_STATUSES, rawStatus, "running"), name: rawStatus },
|
|
1474
|
+
links: [],
|
|
1475
|
+
...createdAt === void 0 ? {} : { createdAt },
|
|
1476
|
+
...startedAt === void 0 ? {} : { startedAt },
|
|
1477
|
+
...finishedAt === void 0 ? {} : { finishedAt }
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
function keepsRun(run, query) {
|
|
1481
|
+
return (query.status === void 0 || query.status.includes(run.status.category)) && (query.event === void 0 || run.event !== void 0 && query.event.includes(run.event.category)) && (query.workflow === void 0 || run.workflows.includes(query.workflow)) && (query.actor === void 0 || run.actor === query.actor) && (query.pullRequest === void 0 || run.pullRequest === query.pullRequest) && inTimeRange(run.createdAt, query.createdAfter, query.createdBefore) && inTimeRange(run.updatedAt, query.updatedAfter, query.updatedBefore);
|
|
1482
|
+
}
|
|
1483
|
+
function pullSlugOf(run, raw, pullId, slug) {
|
|
1484
|
+
if (run.pullRequest !== void 0 || pullId === void 0 || slug === void 0) return run;
|
|
1485
|
+
const id = isRecord(raw.pull) ? text(raw.pull.id) : void 0;
|
|
1486
|
+
return id === pullId ? { ...run, pullRequest: slug } : run;
|
|
1487
|
+
}
|
|
1488
|
+
function leadingByte(window) {
|
|
1489
|
+
let count = 0;
|
|
1490
|
+
while (count < 3 && count < window.length && ((window[count] ?? 0) & 192) === 128) count += 1;
|
|
1491
|
+
return count;
|
|
1492
|
+
}
|
|
1493
|
+
function trailingByte(window) {
|
|
1494
|
+
for (let back = 1; back <= Math.min(4, window.length); back += 1) {
|
|
1495
|
+
const byte = window[window.length - back] ?? 0;
|
|
1496
|
+
if ((byte & 192) === 128) continue;
|
|
1497
|
+
const size = byte >= 240 ? 4 : byte >= 224 ? 3 : byte >= 192 ? 2 : 1;
|
|
1498
|
+
return size > back ? back : 0;
|
|
1499
|
+
}
|
|
1500
|
+
return 0;
|
|
1501
|
+
}
|
|
1502
|
+
function concat(head, tail) {
|
|
1503
|
+
if (head.length === 0) return tail;
|
|
1504
|
+
if (tail.length === 0) return head;
|
|
1505
|
+
const merged = new Uint8Array(head.length + tail.length);
|
|
1506
|
+
merged.set(head, 0);
|
|
1507
|
+
merged.set(tail, head.length);
|
|
1508
|
+
return merged;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// src/tracker/registry.lib.ts
|
|
1512
|
+
var definitions = /* @__PURE__ */ new Map();
|
|
1513
|
+
function registerTracker(definition) {
|
|
1514
|
+
const kind = definition.descriptor.tracker;
|
|
1515
|
+
if (definitions.has(kind)) throw new Error(`\u0422\u0440\u0435\u043A\u0435\u0440 ${kind} \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u043D.`);
|
|
1516
|
+
definitions.set(kind, definition);
|
|
1517
|
+
}
|
|
1518
|
+
function findTracker(kind) {
|
|
1519
|
+
return definitions.get(kind);
|
|
1520
|
+
}
|
|
1521
|
+
function listTrackers() {
|
|
1522
|
+
return [...definitions.values()];
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// src/tracker/pull.schema.ts
|
|
1526
|
+
import { z as z2 } from "zod";
|
|
1527
|
+
var pullRequestStatusCategorySchema = z2.enum(["draft", "open", "merged", "closed"]);
|
|
1528
|
+
var pullRequestFilterSchema = z2.enum(["status", "author", "source", "target", "labels", "search", "createdAfter", "createdBefore", "updatedAfter", "updatedBefore"]);
|
|
1529
|
+
var pullRequestSortFieldSchema = z2.enum(["created", "updated", "title"]);
|
|
1530
|
+
var mergeMethodSchema = z2.enum(["merge", "squash", "rebase"]);
|
|
1531
|
+
var pullRequestSchema = z2.object({
|
|
1532
|
+
/** Номер PR в своём трекере: GitHub number, GitLab iid, SourceCraft slug; для клиента непрозрачная строка. */
|
|
1533
|
+
key: z2.string().min(1).max(200),
|
|
1534
|
+
title: z2.string().min(1).max(1024),
|
|
1535
|
+
description: z2.string().max(1048576).optional(),
|
|
1536
|
+
status: z2.object({ category: pullRequestStatusCategorySchema, name: z2.string().min(1).max(200) }),
|
|
1537
|
+
/** Автор — логин или slug трекера. */
|
|
1538
|
+
author: z2.string().min(1).max(200).optional(),
|
|
1539
|
+
/** Ветка-источник и целевая ветка — имя без refs/heads/. */
|
|
1540
|
+
source: z2.string().min(1).max(500),
|
|
1541
|
+
target: z2.string().min(1).max(500),
|
|
1542
|
+
/** Коммит головы источника. */
|
|
1543
|
+
headSha: z2.string().regex(/^[0-9a-f]{7,64}$/).optional(),
|
|
1544
|
+
labels: z2.array(z2.string().min(1).max(200)).max(100),
|
|
1545
|
+
/** Запрошенные ревьюеры; нет — трекер их в PR не отдаёт. */
|
|
1546
|
+
reviewers: z2.array(z2.string().min(1).max(200)).max(100).optional(),
|
|
1547
|
+
links: z2.array(issueLinkSchema).max(100),
|
|
1548
|
+
createdAt: timestampSchema.optional(),
|
|
1549
|
+
updatedAt: timestampSchema.optional(),
|
|
1550
|
+
mergedAt: timestampSchema.optional(),
|
|
1551
|
+
closedAt: timestampSchema.optional()
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
// src/tracker/github-pull.lib.ts
|
|
1555
|
+
var DEFAULT_API3 = "https://api.github.com";
|
|
1556
|
+
var TARGET4 = /^[\w.-]+\/[\w.-]+$/;
|
|
1557
|
+
var NUMBER = /^[1-9]\d{0,19}$/;
|
|
1558
|
+
var SHA2 = /^[0-9a-f]{7,64}$/;
|
|
1559
|
+
var LIST_SUBJECT = "PR GitHub";
|
|
1560
|
+
var COMMENTS_SUBJECT = "\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 PR GitHub";
|
|
1561
|
+
var PULL_CAPABILITIES = {
|
|
1562
|
+
list: {
|
|
1563
|
+
filters: pullRequestFilterSchema.options,
|
|
1564
|
+
clientSide: ["status", "author", "labels", "search", "createdAfter", "createdBefore", "updatedAfter", "updatedBefore"],
|
|
1565
|
+
sorts: ["created", "updated"]
|
|
1566
|
+
},
|
|
1567
|
+
comments: { filters: COMMENT_FILTERS, clientSide: [], order: ["asc"] },
|
|
1568
|
+
create: true,
|
|
1569
|
+
createDraft: true,
|
|
1570
|
+
update: ["title", "description", "target"],
|
|
1571
|
+
statuses: ["open", "closed"],
|
|
1572
|
+
comment: true,
|
|
1573
|
+
merge: ["merge", "squash", "rebase"],
|
|
1574
|
+
deleteSourceOnMerge: false
|
|
1575
|
+
};
|
|
1576
|
+
function githubPulls() {
|
|
1577
|
+
return {
|
|
1578
|
+
capabilities: PULL_CAPABILITIES,
|
|
1579
|
+
create: (context) => createGithubPullAdapter(context)
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
function createGithubPullAdapter({ connection, fetch }) {
|
|
1583
|
+
const target = requireTarget(connection.target, "owner/repo \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F GitHub");
|
|
1584
|
+
if (!TARGET4.test(target)) throw new TrackerError("invalid_request", "target GitHub \u2014 owner/repo.");
|
|
1585
|
+
const api = baseUrl(connection.url, DEFAULT_API3);
|
|
1586
|
+
const root = `${api}/repos/${target}/pulls`;
|
|
1587
|
+
const issues = `${api}/repos/${target}/issues`;
|
|
1588
|
+
const owner = target.split("/")[0] ?? target;
|
|
1589
|
+
const headers = {
|
|
1590
|
+
accept: "application/vnd.github+json",
|
|
1591
|
+
"x-github-api-version": "2022-11-28",
|
|
1592
|
+
...connection.token === void 0 || connection.token === "" ? {} : { authorization: `Bearer ${connection.token}` }
|
|
1593
|
+
};
|
|
1594
|
+
const missing = (key) => ({ failure: "not_found", message: `\u0412 GitHub \u043D\u0435\u0442 PR ${key}.` });
|
|
1595
|
+
const number = (key) => {
|
|
1596
|
+
if (!NUMBER.test(key)) throw new TrackerError("not_found", `\u0412 GitHub \u043D\u0435\u0442 PR ${key}.`);
|
|
1597
|
+
return key;
|
|
1598
|
+
};
|
|
1599
|
+
const getPull = async (key) => pullOf((await requestJson(`${root}/${number(key)}`, { fetch, headers, missing: missing(key) })).body);
|
|
1600
|
+
return {
|
|
1601
|
+
list: async (query) => {
|
|
1602
|
+
assertListQuery(LIST_SUBJECT, PULL_CAPABILITIES.list, query, pullRequestFilterSchema.options);
|
|
1603
|
+
const status = usedStatuses(query.status);
|
|
1604
|
+
const page = pageOf3(query.cursor);
|
|
1605
|
+
const params = new URLSearchParams({ state: stateFilter(status), per_page: String(query.limit), page: String(page) });
|
|
1606
|
+
if (query.source !== void 0) params.set("head", `${owner}:${query.source}`);
|
|
1607
|
+
if (query.target !== void 0) params.set("base", query.target);
|
|
1608
|
+
if (query.sort !== void 0) {
|
|
1609
|
+
params.set("sort", query.sort.field);
|
|
1610
|
+
params.set("direction", query.sort.direction ?? "desc");
|
|
1611
|
+
}
|
|
1612
|
+
const { body, headers: responseHeaders } = await requestJson(`${root}?${params.toString()}`, { fetch, headers });
|
|
1613
|
+
const items = (Array.isArray(body) ? body : []).map(pullOf).filter((pull) => matchesPage(pull, query, status));
|
|
1614
|
+
return { items, ...nextOf(responseHeaders.get("link"), page) };
|
|
1615
|
+
},
|
|
1616
|
+
get: (key) => getPull(key),
|
|
1617
|
+
create: async (input) => pullOf((await requestJson(root, { fetch, headers, method: "POST", body: createBody(input) })).body),
|
|
1618
|
+
update: async (key, patch) => {
|
|
1619
|
+
if (patch.status === "draft") throw new TrackerError("unsupported", "PR GitHub: \u043F\u0435\u0440\u0435\u0432\u043E\u0434 \u0432 \u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u0447\u0435\u0440\u0435\u0437 REST.");
|
|
1620
|
+
const current = patch.status === "open" ? await getPull(key) : void 0;
|
|
1621
|
+
if (current?.status.category === "draft") throw new TrackerError("unsupported", "PR GitHub: \u0441\u043D\u044F\u0442\u0438\u0435 \u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A\u0430 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E \u0447\u0435\u0440\u0435\u0437 REST.");
|
|
1622
|
+
if (current !== void 0 && current.status.category === "open" && patch.title === void 0 && patch.description === void 0 && patch.target === void 0) return current;
|
|
1623
|
+
return pullOf((await requestJson(`${root}/${number(key)}`, { fetch, headers, method: "PATCH", body: patchBody(patch), missing: missing(key) })).body);
|
|
1624
|
+
},
|
|
1625
|
+
merge: async (key, input) => {
|
|
1626
|
+
if (input.deleteSource === true) throw new TrackerError("unsupported", "PR GitHub: \u0443\u0434\u0430\u043B\u0435\u043D\u0438\u0435 \u0432\u0435\u0442\u043A\u0438-\u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A\u0430 \u043F\u0440\u0438 \u0441\u043B\u0438\u044F\u043D\u0438\u0438 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E.");
|
|
1627
|
+
if (input.method !== void 0 && !PULL_CAPABILITIES.merge.includes(input.method)) {
|
|
1628
|
+
throw new TrackerError("unsupported", `PR GitHub: \u0441\u043B\u0438\u044F\u043D\u0438\u0435 \u0441\u043F\u043E\u0441\u043E\u0431\u043E\u043C ${input.method} \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F.`);
|
|
1629
|
+
}
|
|
1630
|
+
await requestJson(`${root}/${number(key)}/merge`, { fetch, headers, method: "PUT", body: mergeBody(input), conflictOn: [405, 409], missing: missing(key) });
|
|
1631
|
+
return getPull(key);
|
|
1632
|
+
},
|
|
1633
|
+
comments: async (key, query) => {
|
|
1634
|
+
assertCommentQuery(COMMENTS_SUBJECT, PULL_CAPABILITIES.comments, query);
|
|
1635
|
+
const page = pageOf3(query.cursor);
|
|
1636
|
+
const params = new URLSearchParams({ per_page: String(query.limit), page: String(page) });
|
|
1637
|
+
if (query.updatedAfter !== void 0) params.set("since", isoTime(query.updatedAfter));
|
|
1638
|
+
const { body, headers: responseHeaders } = await requestJson(`${issues}/${number(key)}/comments?${params.toString()}`, { fetch, headers, missing: missing(key) });
|
|
1639
|
+
return { items: (Array.isArray(body) ? body : []).map(commentOf), ...nextOf(responseHeaders.get("link"), page) };
|
|
1640
|
+
},
|
|
1641
|
+
comment: async (key, body) => commentOf((await requestJson(`${issues}/${number(key)}/comments`, { fetch, headers, method: "POST", body: { body }, missing: missing(key) })).body)
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
function pageOf3(cursor) {
|
|
1645
|
+
const page = cursor === void 0 ? 1 : Number(cursor);
|
|
1646
|
+
return Number.isInteger(page) && page >= 1 ? page : 1;
|
|
1647
|
+
}
|
|
1648
|
+
function nextOf(link, page) {
|
|
1649
|
+
return link !== null && /rel="next"/.test(link) ? { nextCursor: String(page + 1) } : {};
|
|
268
1650
|
}
|
|
269
|
-
function
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
1651
|
+
function stateFilter(status) {
|
|
1652
|
+
if (status === void 0) return "all";
|
|
1653
|
+
if (status.every((item) => item === "draft" || item === "open")) return "open";
|
|
1654
|
+
if (status.every((item) => item === "merged" || item === "closed")) return "closed";
|
|
1655
|
+
return "all";
|
|
275
1656
|
}
|
|
276
|
-
function
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
1657
|
+
function matchesPage(pull, query, status) {
|
|
1658
|
+
if (status !== void 0 && !status.includes(pull.status.category)) return false;
|
|
1659
|
+
if (query.author !== void 0 && pull.author !== query.author) return false;
|
|
1660
|
+
if (query.labels !== void 0 && !query.labels.every((label) => pull.labels.includes(label))) return false;
|
|
1661
|
+
if (query.search !== void 0 && !containsText([pull.title, pull.description], query.search)) return false;
|
|
1662
|
+
return inTimeRange(pull.createdAt, query.createdAfter, query.createdBefore) && inTimeRange(pull.updatedAt, query.updatedAfter, query.updatedBefore);
|
|
1663
|
+
}
|
|
1664
|
+
function usedStatuses(status) {
|
|
1665
|
+
if (status === void 0 || status.length === 0) return void 0;
|
|
1666
|
+
for (const item of status) {
|
|
1667
|
+
if (!pullRequestStatusCategorySchema.options.includes(item)) {
|
|
1668
|
+
throw new TrackerError("invalid_request", `PR GitHub: \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043A\u0430\u0442\u0435\u0433\u043E\u0440\u0438\u044F \u0441\u0442\u0430\u0442\u0443\u0441\u0430 ${String(item)}.`);
|
|
1669
|
+
}
|
|
283
1670
|
}
|
|
284
|
-
|
|
285
|
-
return raw;
|
|
1671
|
+
return status;
|
|
286
1672
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
1673
|
+
function createBody(input) {
|
|
1674
|
+
return {
|
|
1675
|
+
title: input.title,
|
|
1676
|
+
head: input.source,
|
|
1677
|
+
base: input.target,
|
|
1678
|
+
...input.description === void 0 ? {} : { body: input.description },
|
|
1679
|
+
...input.draft === true ? { draft: true } : {}
|
|
1680
|
+
};
|
|
294
1681
|
}
|
|
295
|
-
function
|
|
296
|
-
return
|
|
1682
|
+
function patchBody(patch) {
|
|
1683
|
+
return {
|
|
1684
|
+
...patch.title === void 0 ? {} : { title: patch.title },
|
|
1685
|
+
...patch.description === void 0 ? {} : { body: patch.description },
|
|
1686
|
+
...patch.target === void 0 ? {} : { base: patch.target },
|
|
1687
|
+
...patch.status === void 0 ? {} : { state: patch.status === "closed" ? "closed" : "open" }
|
|
1688
|
+
};
|
|
297
1689
|
}
|
|
298
|
-
function
|
|
299
|
-
|
|
1690
|
+
function mergeBody(input) {
|
|
1691
|
+
const lines = input.message?.split("\n") ?? [];
|
|
1692
|
+
return {
|
|
1693
|
+
...input.method === void 0 ? {} : { merge_method: input.method },
|
|
1694
|
+
...input.headSha === void 0 ? {} : { sha: input.headSha },
|
|
1695
|
+
...input.message === void 0 ? {} : { commit_title: lines[0] ?? "" },
|
|
1696
|
+
...lines.length > 1 ? { commit_message: lines.slice(1).join("\n") } : {}
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
function pullOf(raw) {
|
|
1700
|
+
const item = isRecord(raw) ? raw : {};
|
|
1701
|
+
const head = isRecord(item.head) ? item.head : {};
|
|
1702
|
+
const base = isRecord(item.base) ? item.base : {};
|
|
1703
|
+
const key = typeof item.number === "number" ? String(item.number) : "?";
|
|
1704
|
+
const description = text(item.body);
|
|
1705
|
+
const author = isRecord(item.user) ? text(item.user.login) : void 0;
|
|
1706
|
+
const sha = text(head.sha);
|
|
1707
|
+
const url = text(item.html_url);
|
|
1708
|
+
const createdAt = toTimestamp(item.created_at);
|
|
1709
|
+
const updatedAt = toTimestamp(item.updated_at);
|
|
1710
|
+
const mergedAt = toTimestamp(item.merged_at);
|
|
1711
|
+
const closedAt = toTimestamp(item.closed_at);
|
|
1712
|
+
const closed = item.state === "closed";
|
|
1713
|
+
const merged = item.merged_at !== void 0 && item.merged_at !== null;
|
|
1714
|
+
const category = closed ? merged ? "merged" : "closed" : item.draft === true ? "draft" : "open";
|
|
1715
|
+
return {
|
|
1716
|
+
key,
|
|
1717
|
+
title: text(item.title) ?? key,
|
|
1718
|
+
...description === void 0 ? {} : { description },
|
|
1719
|
+
status: { category, name: category },
|
|
1720
|
+
...author === void 0 ? {} : { author },
|
|
1721
|
+
source: text(head.ref) ?? "?",
|
|
1722
|
+
target: text(base.ref) ?? "?",
|
|
1723
|
+
...sha !== void 0 && SHA2.test(sha) ? { headSha: sha } : {},
|
|
1724
|
+
labels: Array.isArray(item.labels) ? stringList(item.labels.map((label) => isRecord(label) ? label.name : label)) : [],
|
|
1725
|
+
reviewers: logins(item.requested_reviewers),
|
|
1726
|
+
links: url === void 0 ? [] : [{ url, title: "GitHub" }],
|
|
1727
|
+
...createdAt === void 0 ? {} : { createdAt },
|
|
1728
|
+
...updatedAt === void 0 ? {} : { updatedAt },
|
|
1729
|
+
...mergedAt === void 0 ? {} : { mergedAt },
|
|
1730
|
+
...closedAt === void 0 ? {} : { closedAt }
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1733
|
+
function logins(value) {
|
|
1734
|
+
if (!Array.isArray(value)) return [];
|
|
1735
|
+
const names = value.map((item) => isRecord(item) ? text(item.login) : void 0);
|
|
1736
|
+
return [...new Set(names.filter((name) => name !== void 0))];
|
|
1737
|
+
}
|
|
1738
|
+
function commentOf(raw) {
|
|
1739
|
+
const item = isRecord(raw) ? raw : {};
|
|
1740
|
+
const author = isRecord(item.user) ? text(item.user.login) : void 0;
|
|
1741
|
+
const createdAt = toTimestamp(item.created_at);
|
|
1742
|
+
return { id: typeof item.id === "number" ? String(item.id) : "?", ...author === void 0 ? {} : { author }, body: typeof item.body === "string" ? item.body : "", ...createdAt === void 0 ? {} : { createdAt } };
|
|
300
1743
|
}
|
|
301
1744
|
|
|
302
1745
|
// src/tracker/github.lib.ts
|
|
303
|
-
var
|
|
304
|
-
var
|
|
305
|
-
var
|
|
306
|
-
var
|
|
307
|
-
var
|
|
1746
|
+
var DEFAULT_API4 = "https://api.github.com";
|
|
1747
|
+
var TARGET5 = /^[\w.-]+\/[\w.-]+$/;
|
|
1748
|
+
var NUMBER2 = /^[1-9]\d{0,14}$/;
|
|
1749
|
+
var LIST_SUBJECT2 = "\u0417\u0430\u0434\u0430\u0447\u0438 GitHub";
|
|
1750
|
+
var COMMENTS_SUBJECT2 = "\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 GitHub";
|
|
308
1751
|
function githubTracker() {
|
|
309
1752
|
const issues = {
|
|
310
1753
|
list: { filters: ISSUE_FILTERS, clientSide: ["status"], sorts: ["created", "updated", "comments"] },
|
|
@@ -322,13 +1765,15 @@ function githubTracker() {
|
|
|
322
1765
|
features: { create: true, update: true, comment: true, priority: true, labels: true, assignee: true, statuses: ["open", "in_progress", "done", "declined"] }
|
|
323
1766
|
},
|
|
324
1767
|
create: (context) => createGithubAdapter(context, issues),
|
|
325
|
-
issues
|
|
1768
|
+
issues,
|
|
1769
|
+
pulls: githubPulls(),
|
|
1770
|
+
ci: githubCi()
|
|
326
1771
|
};
|
|
327
1772
|
}
|
|
328
1773
|
function createGithubAdapter({ connection, fetch }, capabilities) {
|
|
329
1774
|
const target = requireTarget(connection.target, "owner/repo \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F GitHub");
|
|
330
|
-
if (!
|
|
331
|
-
const api = baseUrl(connection.url,
|
|
1775
|
+
if (!TARGET5.test(target)) throw new TrackerError("invalid_request", "target GitHub \u2014 owner/repo.");
|
|
1776
|
+
const api = baseUrl(connection.url, DEFAULT_API4);
|
|
332
1777
|
const root = `${api}/repos/${target}/issues`;
|
|
333
1778
|
const headers = {
|
|
334
1779
|
accept: "application/vnd.github+json",
|
|
@@ -336,7 +1781,7 @@ function createGithubAdapter({ connection, fetch }, capabilities) {
|
|
|
336
1781
|
...connection.token === void 0 || connection.token === "" ? {} : { authorization: `Bearer ${connection.token}` }
|
|
337
1782
|
};
|
|
338
1783
|
const number = (key) => {
|
|
339
|
-
if (!
|
|
1784
|
+
if (!NUMBER2.test(key)) throw new TrackerError("unknown_issue", `\u0412 GitHub \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 ${key}.`);
|
|
340
1785
|
return key;
|
|
341
1786
|
};
|
|
342
1787
|
const rawOf = async (key) => {
|
|
@@ -345,13 +1790,13 @@ function createGithubAdapter({ connection, fetch }, capabilities) {
|
|
|
345
1790
|
};
|
|
346
1791
|
return {
|
|
347
1792
|
list: async (query) => {
|
|
348
|
-
assertListQuery(
|
|
1793
|
+
assertListQuery(LIST_SUBJECT2, capabilities.list, query, ISSUE_FILTERS);
|
|
349
1794
|
assertCsvLabels(query.labels);
|
|
350
|
-
const page =
|
|
1795
|
+
const page = pageOf4(query.cursor);
|
|
351
1796
|
const search = needsSearch(query);
|
|
352
1797
|
const params = search ? searchParams(target, query, page) : issuesParams(query, page);
|
|
353
1798
|
const { body, headers: responseHeaders } = await requestJson(`${search ? `${api}/search/issues` : root}?${params.toString()}`, { fetch, headers });
|
|
354
|
-
return issuePageOf(
|
|
1799
|
+
return issuePageOf(rowsOf2(body, search), query, responseHeaders.get("link") ?? "", page);
|
|
355
1800
|
},
|
|
356
1801
|
get: async (key) => issueOf(await rawOf(key)),
|
|
357
1802
|
create: async (input) => {
|
|
@@ -366,25 +1811,25 @@ function createGithubAdapter({ connection, fetch }, capabilities) {
|
|
|
366
1811
|
},
|
|
367
1812
|
update: async (key, patch) => {
|
|
368
1813
|
const current = patch.labels === void 0 && patch.priority === void 0 && patch.status === void 0 ? void 0 : issueOf(await rawOf(key));
|
|
369
|
-
const response = await requestJson(`${root}/${number(key)}`, { fetch, headers, method: "PATCH", body:
|
|
1814
|
+
const response = await requestJson(`${root}/${number(key)}`, { fetch, headers, method: "PATCH", body: patchBody2(patch, current), missingIsIssue: true });
|
|
370
1815
|
return issueOf(response.body);
|
|
371
1816
|
},
|
|
372
1817
|
comments: async (key, query) => {
|
|
373
|
-
assertCommentQuery(
|
|
374
|
-
const page =
|
|
1818
|
+
assertCommentQuery(COMMENTS_SUBJECT2, capabilities.comments, query);
|
|
1819
|
+
const page = pageOf4(query.cursor);
|
|
375
1820
|
const params = new URLSearchParams({ per_page: String(query.limit), page: String(page) });
|
|
376
1821
|
if (query.updatedAfter !== void 0) params.set("since", isoTime(query.updatedAfter));
|
|
377
1822
|
const { body, headers: responseHeaders } = await requestJson(`${root}/${number(key)}/comments?${params.toString()}`, { fetch, headers, missingIsIssue: true });
|
|
378
|
-
return { items: (Array.isArray(body) ? body : []).map(
|
|
1823
|
+
return { items: (Array.isArray(body) ? body : []).map(commentOf2), .../rel="next"/.test(responseHeaders.get("link") ?? "") ? { nextCursor: String(page + 1) } : {} };
|
|
379
1824
|
},
|
|
380
|
-
comment: async (key, body) =>
|
|
1825
|
+
comment: async (key, body) => commentOf2((await requestJson(`${root}/${number(key)}/comments`, { fetch, headers, method: "POST", body: { body }, missingIsIssue: true })).body)
|
|
381
1826
|
};
|
|
382
1827
|
}
|
|
383
|
-
function
|
|
1828
|
+
function pageOf4(cursor) {
|
|
384
1829
|
const page = cursor === void 0 ? 1 : Number(cursor);
|
|
385
1830
|
return Number.isInteger(page) && page >= 1 ? page : 1;
|
|
386
1831
|
}
|
|
387
|
-
function
|
|
1832
|
+
function stateFilter2(status) {
|
|
388
1833
|
if (status === void 0) return "all";
|
|
389
1834
|
if (status.every((item) => item === "open" || item === "in_progress")) return "open";
|
|
390
1835
|
if (status.every((item) => item === "done" || item === "declined")) return "closed";
|
|
@@ -394,10 +1839,10 @@ function needsSearch(query) {
|
|
|
394
1839
|
return query.search !== void 0 || query.createdAfter !== void 0 || query.createdBefore !== void 0 || query.updatedBefore !== void 0;
|
|
395
1840
|
}
|
|
396
1841
|
function assertCsvLabels(labels) {
|
|
397
|
-
if (labels?.some((label) => label.includes(",")) === true) throw new TrackerError("invalid_request", `${
|
|
1842
|
+
if (labels?.some((label) => label.includes(",")) === true) throw new TrackerError("invalid_request", `${LIST_SUBJECT2}: \u043C\u0435\u0442\u043A\u0430 \u043D\u0435 \u0434\u043E\u043B\u0436\u043D\u0430 \u0441\u043E\u0434\u0435\u0440\u0436\u0430\u0442\u044C \u0437\u0430\u043F\u044F\u0442\u0443\u044E.`);
|
|
398
1843
|
}
|
|
399
1844
|
function issuesParams(query, page) {
|
|
400
|
-
const params = new URLSearchParams({ state:
|
|
1845
|
+
const params = new URLSearchParams({ state: stateFilter2(query.status), per_page: String(query.limit), page: String(page) });
|
|
401
1846
|
if (query.labels !== void 0 && query.labels.length > 0) params.set("labels", query.labels.join(","));
|
|
402
1847
|
if (query.assignee !== void 0) params.set("assignee", query.assignee ?? "none");
|
|
403
1848
|
if (query.author !== void 0) params.set("creator", query.author);
|
|
@@ -435,7 +1880,7 @@ function timeRangeQualifier(field, after, before) {
|
|
|
435
1880
|
if (before !== void 0) return `${field}:<=${isoTime(before)}`;
|
|
436
1881
|
return void 0;
|
|
437
1882
|
}
|
|
438
|
-
function
|
|
1883
|
+
function rowsOf2(body, search) {
|
|
439
1884
|
if (!search) return Array.isArray(body) ? body : [];
|
|
440
1885
|
return isRecord(body) && Array.isArray(body.items) ? body.items : [];
|
|
441
1886
|
}
|
|
@@ -456,7 +1901,7 @@ function composeLabels(plain, priority, status) {
|
|
|
456
1901
|
const priorityLabel = priority === void 0 ? void 0 : priorityName(GITHUB_PRIORITY_LABELS, priority);
|
|
457
1902
|
return [...plain, ...priorityLabel === void 0 ? [] : [priorityLabel], ...status === "in_progress" ? [GITHUB_IN_PROGRESS_LABEL] : []];
|
|
458
1903
|
}
|
|
459
|
-
function
|
|
1904
|
+
function patchBody2(patch, current) {
|
|
460
1905
|
const status = patch.status ?? current?.status.category;
|
|
461
1906
|
const touchesLabels = patch.labels !== void 0 || patch.priority !== void 0 || patch.status !== void 0;
|
|
462
1907
|
return {
|
|
@@ -492,22 +1937,216 @@ function issueOf(raw) {
|
|
|
492
1937
|
...updatedAt === void 0 ? {} : { updatedAt }
|
|
493
1938
|
};
|
|
494
1939
|
}
|
|
495
|
-
function
|
|
1940
|
+
function commentOf2(raw) {
|
|
496
1941
|
const item = isRecord(raw) ? raw : {};
|
|
497
1942
|
const author = isRecord(item.user) ? text(item.user.login) : void 0;
|
|
498
1943
|
const createdAt = toTimestamp(item.created_at);
|
|
499
1944
|
return { id: typeof item.id === "number" ? String(item.id) : "?", ...author === void 0 ? {} : { author }, body: typeof item.body === "string" ? item.body : "", ...createdAt === void 0 ? {} : { createdAt } };
|
|
500
1945
|
}
|
|
501
1946
|
|
|
1947
|
+
// src/tracker/gitlab-pull.lib.ts
|
|
1948
|
+
var DEFAULT_URL2 = "https://gitlab.com";
|
|
1949
|
+
var TARGET6 = /^(\d{1,15}|[\w.-]+(\/[\w.-]+)+)$/;
|
|
1950
|
+
var IID = /^[1-9]\d{0,19}$/;
|
|
1951
|
+
var SHA3 = /^[0-9a-f]{7,64}$/;
|
|
1952
|
+
var PULL_CAPABILITIES2 = {
|
|
1953
|
+
list: { filters: pullRequestFilterSchema.options, clientSide: ["status"], sorts: ["created", "updated", "title"] },
|
|
1954
|
+
comments: { filters: COMMENT_FILTERS, clientSide: COMMENT_FILTERS, order: ["asc", "desc"] },
|
|
1955
|
+
create: true,
|
|
1956
|
+
createDraft: true,
|
|
1957
|
+
update: ["title", "description", "target"],
|
|
1958
|
+
statuses: ["draft", "open", "closed"],
|
|
1959
|
+
comment: true,
|
|
1960
|
+
merge: ["merge", "squash"],
|
|
1961
|
+
deleteSourceOnMerge: true
|
|
1962
|
+
};
|
|
1963
|
+
var ORDER_BY = { created: "created_at", updated: "updated_at", title: "title" };
|
|
1964
|
+
function gitlabPulls() {
|
|
1965
|
+
return {
|
|
1966
|
+
capabilities: PULL_CAPABILITIES2,
|
|
1967
|
+
create: (context) => createGitlabPullAdapter(context)
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
function createGitlabPullAdapter({ connection, fetch }) {
|
|
1971
|
+
const target = requireTarget(connection.target, "\u043F\u0443\u0442\u044C \u043F\u0440\u043E\u0435\u043A\u0442\u0430 GitLab, group/project, \u0438\u043B\u0438 \u0435\u0433\u043E id");
|
|
1972
|
+
if (!TARGET6.test(target)) throw new TrackerError("invalid_request", "target GitLab \u2014 group/project \u0438\u043B\u0438 \u0447\u0438\u0441\u043B\u043E\u0432\u043E\u0439 id.");
|
|
1973
|
+
const root = `${baseUrl(connection.url, DEFAULT_URL2)}/api/v4/projects/${encodeURIComponent(target)}/merge_requests`;
|
|
1974
|
+
const headers = connection.token === void 0 || connection.token === "" ? {} : { "private-token": connection.token };
|
|
1975
|
+
const iid = (key) => {
|
|
1976
|
+
if (!IID.test(key)) throw new TrackerError("not_found", `\u0412 GitLab \u043D\u0435\u0442 MR ${key}.`);
|
|
1977
|
+
return key;
|
|
1978
|
+
};
|
|
1979
|
+
const currentOf = async (key) => pullOf2((await requestJson(`${root}/${iid(key)}`, { fetch, headers, missing: { failure: "not_found", message: `\u0412 GitLab \u043D\u0435\u0442 MR ${key}.` } })).body);
|
|
1980
|
+
return {
|
|
1981
|
+
list: async (query) => {
|
|
1982
|
+
assertListQuery("PR GitLab", PULL_CAPABILITIES2.list, query, pullRequestFilterSchema.options);
|
|
1983
|
+
const status = usedStatuses2(query.status);
|
|
1984
|
+
const page = pageOf5(query.cursor);
|
|
1985
|
+
const params = new URLSearchParams({
|
|
1986
|
+
state: stateFilter3(status),
|
|
1987
|
+
per_page: String(query.limit),
|
|
1988
|
+
page: String(page),
|
|
1989
|
+
order_by: query.sort === void 0 ? "created_at" : ORDER_BY[query.sort.field] ?? "created_at",
|
|
1990
|
+
sort: query.sort?.direction ?? "desc"
|
|
1991
|
+
});
|
|
1992
|
+
if (query.labels !== void 0 && query.labels.length > 0) {
|
|
1993
|
+
if (query.labels.some((label) => label.includes(","))) throw new TrackerError("invalid_request", "PR GitLab: \u043C\u0435\u0442\u043A\u0430 \u043D\u0435 \u0434\u043E\u043B\u0436\u043D\u0430 \u0441\u043E\u0434\u0435\u0440\u0436\u0430\u0442\u044C \u0437\u0430\u043F\u044F\u0442\u0443\u044E.");
|
|
1994
|
+
params.set("labels", query.labels.join(","));
|
|
1995
|
+
}
|
|
1996
|
+
if (query.author !== void 0) params.set("author_username", query.author);
|
|
1997
|
+
if (query.source !== void 0) params.set("source_branch", query.source);
|
|
1998
|
+
if (query.target !== void 0) params.set("target_branch", query.target);
|
|
1999
|
+
if (query.search !== void 0) params.set("search", query.search);
|
|
2000
|
+
if (query.createdAfter !== void 0) params.set("created_after", isoTime(query.createdAfter));
|
|
2001
|
+
if (query.createdBefore !== void 0) params.set("created_before", isoTime(query.createdBefore));
|
|
2002
|
+
if (query.updatedAfter !== void 0) params.set("updated_after", isoTime(query.updatedAfter));
|
|
2003
|
+
if (query.updatedBefore !== void 0) params.set("updated_before", isoTime(query.updatedBefore));
|
|
2004
|
+
const { body, headers: responseHeaders } = await requestJson(`${root}?${params.toString()}`, { fetch, headers });
|
|
2005
|
+
const next = responseHeaders.get("x-next-page");
|
|
2006
|
+
const items = (Array.isArray(body) ? body : []).map(pullOf2);
|
|
2007
|
+
return {
|
|
2008
|
+
items: status === void 0 ? items : items.filter((pr) => status.includes(pr.status.category) === true),
|
|
2009
|
+
...next === null || next === "" ? {} : { nextCursor: next }
|
|
2010
|
+
};
|
|
2011
|
+
},
|
|
2012
|
+
get: currentOf,
|
|
2013
|
+
create: async (input) => {
|
|
2014
|
+
const body = {
|
|
2015
|
+
source_branch: input.source,
|
|
2016
|
+
target_branch: input.target,
|
|
2017
|
+
title: (input.draft === true ? GITLAB_DRAFT_PREFIX : "") + input.title,
|
|
2018
|
+
...input.description === void 0 ? {} : { description: input.description }
|
|
2019
|
+
};
|
|
2020
|
+
return pullOf2((await requestJson(root, { fetch, headers, method: "POST", body, conflictOn: [409] })).body);
|
|
2021
|
+
},
|
|
2022
|
+
update: async (key, patch) => {
|
|
2023
|
+
const current = patch.status === void 0 ? void 0 : await currentOf(key);
|
|
2024
|
+
if (patch.status === "draft" && current?.status.category === "closed") {
|
|
2025
|
+
throw new TrackerError("unsupported", "PR GitLab: \u043F\u0435\u0440\u0435\u0432\u043E\u0434 \u0437\u0430\u043A\u0440\u044B\u0442\u043E\u0433\u043E MR \u0432 \u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D.");
|
|
2026
|
+
}
|
|
2027
|
+
const body = patchBody3(patch, current);
|
|
2028
|
+
if (Object.keys(body).length === 0) return current ?? pullOf2((await requestJson(`${root}/${iid(key)}`, { fetch, headers, missing: { failure: "not_found", message: `\u0412 GitLab \u043D\u0435\u0442 MR ${key}.` } })).body);
|
|
2029
|
+
return pullOf2((await requestJson(`${root}/${iid(key)}`, { fetch, headers, method: "PUT", body, missing: { failure: "not_found", message: `\u0412 GitLab \u043D\u0435\u0442 MR ${key}.` } })).body);
|
|
2030
|
+
},
|
|
2031
|
+
merge: async (key, input) => {
|
|
2032
|
+
if (input.method !== void 0 && !PULL_CAPABILITIES2.merge.includes(input.method)) {
|
|
2033
|
+
throw new TrackerError("unsupported", `PR GitLab: \u0441\u043B\u0438\u044F\u043D\u0438\u0435 \u0441\u043F\u043E\u0441\u043E\u0431\u043E\u043C ${input.method} \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F.`);
|
|
2034
|
+
}
|
|
2035
|
+
const body = {
|
|
2036
|
+
// squash: true ставит squash; false GitLab игнорирует — при method 'merge' действует настройка MR/проекта.
|
|
2037
|
+
...input.method === "squash" ? { squash: true } : {},
|
|
2038
|
+
...input.message === void 0 ? {} : input.method === "squash" ? { squash_commit_message: input.message } : { merge_commit_message: input.message },
|
|
2039
|
+
...input.deleteSource === void 0 ? {} : { should_remove_source_branch: input.deleteSource },
|
|
2040
|
+
...input.headSha === void 0 ? {} : { sha: input.headSha }
|
|
2041
|
+
};
|
|
2042
|
+
return pullOf2((await requestJson(`${root}/${iid(key)}/merge`, { fetch, headers, method: "PUT", body, conflictOn: [405, 409, 422], missing: { failure: "not_found", message: `\u0412 GitLab \u043D\u0435\u0442 MR ${key}.` } })).body);
|
|
2043
|
+
},
|
|
2044
|
+
comments: async (key, query) => {
|
|
2045
|
+
assertCommentQuery("\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 GitLab", PULL_CAPABILITIES2.comments, query);
|
|
2046
|
+
const page = pageOf5(query.cursor);
|
|
2047
|
+
const params = new URLSearchParams({ sort: query.order ?? "asc", order_by: "created_at", per_page: String(query.limit), page: String(page) });
|
|
2048
|
+
const { body, headers: responseHeaders } = await requestJson(`${root}/${iid(key)}/notes?${params.toString()}`, { fetch, headers, missing: { failure: "not_found", message: `\u0412 GitLab \u043D\u0435\u0442 MR ${key}.` } });
|
|
2049
|
+
const next = responseHeaders.get("x-next-page");
|
|
2050
|
+
const items = (Array.isArray(body) ? body : []).filter((note) => isRecord(note) && note.system !== true && inTimeRange(toTimestamp(note.updated_at), query.updatedAfter, void 0)).map(commentOf3);
|
|
2051
|
+
return { items, ...next === null || next === "" ? {} : { nextCursor: next } };
|
|
2052
|
+
},
|
|
2053
|
+
comment: async (key, body) => commentOf3((await requestJson(`${root}/${iid(key)}/notes`, { fetch, headers, method: "POST", body: { body }, missing: { failure: "not_found", message: `\u0412 GitLab \u043D\u0435\u0442 MR ${key}.` } })).body)
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
2056
|
+
function pageOf5(cursor) {
|
|
2057
|
+
const page = cursor === void 0 ? 1 : Number(cursor);
|
|
2058
|
+
return Number.isInteger(page) && page >= 1 ? page : 1;
|
|
2059
|
+
}
|
|
2060
|
+
function stateFilter3(status) {
|
|
2061
|
+
if (status === void 0) return "all";
|
|
2062
|
+
if (status.every((item) => item === "draft" || item === "open")) return "opened";
|
|
2063
|
+
if (status.every((item) => item === "merged")) return "merged";
|
|
2064
|
+
if (status.every((item) => item === "closed")) return "closed";
|
|
2065
|
+
return "all";
|
|
2066
|
+
}
|
|
2067
|
+
function patchBody3(patch, current) {
|
|
2068
|
+
const body = {};
|
|
2069
|
+
if (patch.description !== void 0) body.description = patch.description;
|
|
2070
|
+
if (patch.target !== void 0) body.target_branch = patch.target;
|
|
2071
|
+
if (patch.status === "closed" && current?.status.category !== "closed") body.state_event = "close";
|
|
2072
|
+
if (patch.status === "open" && current?.status.category === "closed") body.state_event = "reopen";
|
|
2073
|
+
if (patch.status === "draft" || patch.status === "open") {
|
|
2074
|
+
const title = patch.title ?? current?.title ?? "";
|
|
2075
|
+
const base = title.replace(GITLAB_DRAFT_PATTERN, "");
|
|
2076
|
+
const next = patch.status === "draft" ? GITLAB_DRAFT_PREFIX + base : current?.status.category === "draft" || title !== base ? base : patch.title;
|
|
2077
|
+
if (next !== void 0 && next !== current?.title) body.title = next;
|
|
2078
|
+
} else if (patch.title !== void 0 && patch.title !== current?.title) {
|
|
2079
|
+
body.title = patch.title;
|
|
2080
|
+
}
|
|
2081
|
+
return body;
|
|
2082
|
+
}
|
|
2083
|
+
function usedStatuses2(status) {
|
|
2084
|
+
if (status === void 0 || status.length === 0) return void 0;
|
|
2085
|
+
for (const item of status) {
|
|
2086
|
+
if (!pullRequestStatusCategorySchema.options.includes(item)) {
|
|
2087
|
+
throw new TrackerError("invalid_request", `PR GitLab: \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043A\u0430\u0442\u0435\u0433\u043E\u0440\u0438\u044F \u0441\u0442\u0430\u0442\u0443\u0441\u0430 ${String(item)}.`);
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
return status;
|
|
2091
|
+
}
|
|
2092
|
+
function labelNames(value) {
|
|
2093
|
+
if (!Array.isArray(value)) return [];
|
|
2094
|
+
const names = value.map((item) => typeof item === "string" ? item : isRecord(item) ? text(item.name) : void 0);
|
|
2095
|
+
return [...new Set(names.filter((name) => name !== void 0 && name !== ""))];
|
|
2096
|
+
}
|
|
2097
|
+
function usernames(value) {
|
|
2098
|
+
if (!Array.isArray(value)) return [];
|
|
2099
|
+
const names = value.map((item) => isRecord(item) ? text(item.username) : void 0);
|
|
2100
|
+
return [...new Set(names.filter((name) => name !== void 0 && name !== ""))];
|
|
2101
|
+
}
|
|
2102
|
+
function pullOf2(raw) {
|
|
2103
|
+
const item = isRecord(raw) ? raw : {};
|
|
2104
|
+
const state = text(item.state) ?? "opened";
|
|
2105
|
+
const draft = item.draft === true;
|
|
2106
|
+
const category = lookup(GITLAB_MERGE_REQUEST_STATES, state, "open");
|
|
2107
|
+
const key = typeof item.iid === "number" ? String(item.iid) : "?";
|
|
2108
|
+
const description = text(item.description);
|
|
2109
|
+
const author = isRecord(item.author) ? text(item.author.username) : void 0;
|
|
2110
|
+
const sha = text(item.sha);
|
|
2111
|
+
const url = text(item.web_url);
|
|
2112
|
+
const createdAt = toTimestamp(item.created_at);
|
|
2113
|
+
const updatedAt = toTimestamp(item.updated_at);
|
|
2114
|
+
const mergedAt = toTimestamp(item.merged_at);
|
|
2115
|
+
const closedAt = toTimestamp(item.closed_at);
|
|
2116
|
+
return {
|
|
2117
|
+
key,
|
|
2118
|
+
title: text(item.title) ?? key,
|
|
2119
|
+
...description === void 0 ? {} : { description },
|
|
2120
|
+
status: { category: draft && category === "open" ? "draft" : category, name: draft && category === "open" ? "draft" : state },
|
|
2121
|
+
...author === void 0 ? {} : { author },
|
|
2122
|
+
source: text(item.source_branch) ?? "?",
|
|
2123
|
+
target: text(item.target_branch) ?? "?",
|
|
2124
|
+
...sha !== void 0 && SHA3.test(sha) ? { headSha: sha } : {},
|
|
2125
|
+
labels: labelNames(item.labels),
|
|
2126
|
+
reviewers: usernames(item.reviewers),
|
|
2127
|
+
links: url === void 0 ? [] : [{ url, title: "GitLab" }],
|
|
2128
|
+
...createdAt === void 0 ? {} : { createdAt },
|
|
2129
|
+
...updatedAt === void 0 ? {} : { updatedAt },
|
|
2130
|
+
...mergedAt === void 0 ? {} : { mergedAt },
|
|
2131
|
+
...closedAt === void 0 ? {} : { closedAt }
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
function commentOf3(raw) {
|
|
2135
|
+
const item = isRecord(raw) ? raw : {};
|
|
2136
|
+
const author = isRecord(item.author) ? text(item.author.username) : void 0;
|
|
2137
|
+
const createdAt = toTimestamp(item.created_at);
|
|
2138
|
+
return { id: typeof item.id === "number" ? String(item.id) : "?", ...author === void 0 ? {} : { author }, body: typeof item.body === "string" ? item.body : "", ...createdAt === void 0 ? {} : { createdAt } };
|
|
2139
|
+
}
|
|
2140
|
+
|
|
502
2141
|
// src/tracker/gitlab.lib.ts
|
|
503
|
-
var
|
|
504
|
-
var
|
|
505
|
-
var
|
|
2142
|
+
var DEFAULT_URL3 = "https://gitlab.com";
|
|
2143
|
+
var TARGET7 = /^(\d{1,15}|[\w.-]+(\/[\w.-]+)+)$/;
|
|
2144
|
+
var IID2 = /^[1-9]\d{0,14}$/;
|
|
506
2145
|
var ISSUE_CAPABILITIES = {
|
|
507
2146
|
list: { filters: ISSUE_FILTERS, clientSide: ["status"], sorts: ["created", "updated", "priority", "title"] },
|
|
508
2147
|
comments: { filters: COMMENT_FILTERS, clientSide: COMMENT_FILTERS, order: ["asc", "desc"] }
|
|
509
2148
|
};
|
|
510
|
-
var
|
|
2149
|
+
var ORDER_BY2 = {
|
|
511
2150
|
created: "created_at",
|
|
512
2151
|
updated: "updated_at",
|
|
513
2152
|
priority: "priority",
|
|
@@ -526,28 +2165,30 @@ function gitlabTracker() {
|
|
|
526
2165
|
features: { create: true, update: true, comment: true, priority: true, labels: true, assignee: false, statuses: ["open", "in_progress", "done"] }
|
|
527
2166
|
},
|
|
528
2167
|
create: (context) => createGitlabAdapter(context),
|
|
529
|
-
issues: ISSUE_CAPABILITIES
|
|
2168
|
+
issues: ISSUE_CAPABILITIES,
|
|
2169
|
+
pulls: gitlabPulls(),
|
|
2170
|
+
ci: gitlabCi()
|
|
530
2171
|
};
|
|
531
2172
|
}
|
|
532
2173
|
function createGitlabAdapter({ connection, fetch }) {
|
|
533
2174
|
const target = requireTarget(connection.target, "\u043F\u0443\u0442\u044C \u043F\u0440\u043E\u0435\u043A\u0442\u0430 GitLab, group/project, \u0438\u043B\u0438 \u0435\u0433\u043E id");
|
|
534
|
-
if (!
|
|
535
|
-
const root = `${baseUrl(connection.url,
|
|
2175
|
+
if (!TARGET7.test(target)) throw new TrackerError("invalid_request", "target GitLab \u2014 group/project \u0438\u043B\u0438 \u0447\u0438\u0441\u043B\u043E\u0432\u043E\u0439 id.");
|
|
2176
|
+
const root = `${baseUrl(connection.url, DEFAULT_URL3)}/api/v4/projects/${encodeURIComponent(target)}/issues`;
|
|
536
2177
|
const headers = connection.token === void 0 || connection.token === "" ? {} : { "private-token": connection.token };
|
|
537
2178
|
const iid = (key) => {
|
|
538
|
-
if (!
|
|
2179
|
+
if (!IID2.test(key)) throw new TrackerError("unknown_issue", `\u0412 GitLab \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 ${key}.`);
|
|
539
2180
|
return key;
|
|
540
2181
|
};
|
|
541
2182
|
const currentOf = async (key) => issueOf2((await requestJson(`${root}/${iid(key)}`, { fetch, headers, missingIsIssue: true })).body);
|
|
542
2183
|
return {
|
|
543
2184
|
list: async (query) => {
|
|
544
2185
|
assertListQuery("\u0417\u0430\u0434\u0430\u0447\u0438 GitLab", ISSUE_CAPABILITIES.list, query, ISSUE_FILTERS);
|
|
545
|
-
const page =
|
|
2186
|
+
const page = pageOf6(query.cursor);
|
|
546
2187
|
const params = new URLSearchParams({
|
|
547
|
-
state:
|
|
2188
|
+
state: stateFilter4(query.status),
|
|
548
2189
|
per_page: String(query.limit),
|
|
549
2190
|
page: String(page),
|
|
550
|
-
order_by: query.sort === void 0 ? "created_at" :
|
|
2191
|
+
order_by: query.sort === void 0 ? "created_at" : ORDER_BY2[query.sort.field] ?? "created_at",
|
|
551
2192
|
sort: query.sort?.direction ?? "desc"
|
|
552
2193
|
});
|
|
553
2194
|
if (query.labels !== void 0 && query.labels.length > 0) {
|
|
@@ -577,25 +2218,25 @@ function createGitlabAdapter({ connection, fetch }) {
|
|
|
577
2218
|
},
|
|
578
2219
|
update: async (key, patch) => {
|
|
579
2220
|
const current = patch.labels === void 0 && patch.priority === void 0 && patch.status === void 0 ? void 0 : await currentOf(key);
|
|
580
|
-
return issueOf2((await requestJson(`${root}/${iid(key)}`, { fetch, headers, method: "PUT", body:
|
|
2221
|
+
return issueOf2((await requestJson(`${root}/${iid(key)}`, { fetch, headers, method: "PUT", body: patchBody4(patch, current), missingIsIssue: true })).body);
|
|
581
2222
|
},
|
|
582
2223
|
comments: async (key, query) => {
|
|
583
2224
|
assertCommentQuery("\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 GitLab", ISSUE_CAPABILITIES.comments, query);
|
|
584
|
-
const page =
|
|
2225
|
+
const page = pageOf6(query.cursor);
|
|
585
2226
|
const params = new URLSearchParams({ sort: query.order ?? "asc", order_by: "created_at", per_page: String(query.limit), page: String(page) });
|
|
586
2227
|
const { body, headers: responseHeaders } = await requestJson(`${root}/${iid(key)}/notes?${params.toString()}`, { fetch, headers, missingIsIssue: true });
|
|
587
2228
|
const next = responseHeaders.get("x-next-page");
|
|
588
|
-
const items = (Array.isArray(body) ? body : []).filter((note) => isRecord(note) && note.system !== true && inTimeRange(toTimestamp(note.updated_at), query.updatedAfter, void 0)).map(
|
|
2229
|
+
const items = (Array.isArray(body) ? body : []).filter((note) => isRecord(note) && note.system !== true && inTimeRange(toTimestamp(note.updated_at), query.updatedAfter, void 0)).map(commentOf4);
|
|
589
2230
|
return { items, ...next === null || next === "" ? {} : { nextCursor: next } };
|
|
590
2231
|
},
|
|
591
|
-
comment: async (key, body) =>
|
|
2232
|
+
comment: async (key, body) => commentOf4((await requestJson(`${root}/${iid(key)}/notes`, { fetch, headers, method: "POST", body: { body }, missingIsIssue: true })).body)
|
|
592
2233
|
};
|
|
593
2234
|
}
|
|
594
|
-
function
|
|
2235
|
+
function pageOf6(cursor) {
|
|
595
2236
|
const page = cursor === void 0 ? 1 : Number(cursor);
|
|
596
2237
|
return Number.isInteger(page) && page >= 1 ? page : 1;
|
|
597
2238
|
}
|
|
598
|
-
function
|
|
2239
|
+
function stateFilter4(status) {
|
|
599
2240
|
if (status === void 0) return "all";
|
|
600
2241
|
if (status.every((item) => item === "open" || item === "in_progress")) return "opened";
|
|
601
2242
|
if (status.every((item) => item === "done" || item === "declined")) return "closed";
|
|
@@ -605,7 +2246,7 @@ function composeLabels2(plain, priority, status) {
|
|
|
605
2246
|
const priorityLabel = priority === void 0 ? void 0 : priorityName(GITLAB_PRIORITY_LABELS, priority);
|
|
606
2247
|
return [...plain, ...priorityLabel === void 0 ? [] : [priorityLabel], ...status === "in_progress" ? [GITLAB_IN_PROGRESS_LABEL] : []];
|
|
607
2248
|
}
|
|
608
|
-
function
|
|
2249
|
+
function patchBody4(patch, current) {
|
|
609
2250
|
const status = patch.status ?? current?.status.category;
|
|
610
2251
|
const touchesLabels = patch.labels !== void 0 || patch.priority !== void 0 || patch.status !== void 0;
|
|
611
2252
|
if (patch.status === "declined") throw new TrackerError("invalid_request", "\u0412 GitLab \u043D\u0435\u0442 \u0441\u0442\u0430\u0442\u0443\u0441\u0430 \xAB\u043E\u0442\u043A\u043B\u043E\u043D\u0435\u043D\u0430\xBB.");
|
|
@@ -643,7 +2284,7 @@ function issueOf2(raw) {
|
|
|
643
2284
|
...updatedAt === void 0 ? {} : { updatedAt }
|
|
644
2285
|
};
|
|
645
2286
|
}
|
|
646
|
-
function
|
|
2287
|
+
function commentOf4(raw) {
|
|
647
2288
|
const item = isRecord(raw) ? raw : {};
|
|
648
2289
|
const author = isRecord(item.author) ? text(item.author.username) : void 0;
|
|
649
2290
|
const createdAt = toTimestamp(item.created_at);
|
|
@@ -748,9 +2389,9 @@ function createJiraAdapter({ connection, fetch }) {
|
|
|
748
2389
|
const items = raw.filter((item) => inTimeRange(toTimestamp(isRecord(item) ? item.updated : void 0), query.updatedAfter, void 0));
|
|
749
2390
|
const total = typeof record.total === "number" ? record.total : 0;
|
|
750
2391
|
const at = typeof record.startAt === "number" ? record.startAt : 0;
|
|
751
|
-
return { items: items.map(
|
|
2392
|
+
return { items: items.map(commentOf5), ...at + raw.length < total && raw.length > 0 ? { nextCursor: String(at + raw.length) } : {} };
|
|
752
2393
|
},
|
|
753
|
-
comment: async (value, body) =>
|
|
2394
|
+
comment: async (value, body) => commentOf5((await requestJson(`${api}/issue/${key(value)}/comment`, { fetch, headers, method: "POST", body: { body }, missingIsIssue: true })).body)
|
|
754
2395
|
};
|
|
755
2396
|
}
|
|
756
2397
|
function listJql(project, query) {
|
|
@@ -822,7 +2463,7 @@ function issueOf3(raw, site) {
|
|
|
822
2463
|
...updatedAt === void 0 ? {} : { updatedAt }
|
|
823
2464
|
};
|
|
824
2465
|
}
|
|
825
|
-
function
|
|
2466
|
+
function commentOf5(raw) {
|
|
826
2467
|
const item = isRecord(raw) ? raw : {};
|
|
827
2468
|
const author = isRecord(item.author) ? text(item.author.displayName) : void 0;
|
|
828
2469
|
const createdAt = toTimestamp(item.created);
|
|
@@ -909,11 +2550,212 @@ function toComment(row) {
|
|
|
909
2550
|
return { id: String(row.id), ...row.author === void 0 ? {} : { author: row.author }, body: row.body, createdAt: row.createdAt };
|
|
910
2551
|
}
|
|
911
2552
|
|
|
2553
|
+
// src/tracker/sourcecraft-pull.lib.ts
|
|
2554
|
+
var DEFAULT_API5 = "https://api.sourcecraft.tech";
|
|
2555
|
+
var TARGET8 = /^[\w.-]+\/[\w.-]+$/;
|
|
2556
|
+
var SLUG = /^[\w-]{1,100}$/;
|
|
2557
|
+
var SHA4 = /^[0-9a-f]{7,64}$/;
|
|
2558
|
+
var PULL_SORT_FIELDS_UPSTREAM = {
|
|
2559
|
+
created: "created_at",
|
|
2560
|
+
updated: "updated_at",
|
|
2561
|
+
title: "title"
|
|
2562
|
+
};
|
|
2563
|
+
var PULL_CAPABILITIES3 = {
|
|
2564
|
+
list: {
|
|
2565
|
+
filters: pullRequestFilterSchema.options,
|
|
2566
|
+
clientSide: ["labels", "search", "createdAfter", "createdBefore", "updatedAfter", "updatedBefore"],
|
|
2567
|
+
sorts: ["created", "updated", "title"]
|
|
2568
|
+
},
|
|
2569
|
+
comments: { filters: COMMENT_FILTERS, clientSide: COMMENT_FILTERS, order: ["asc", "desc"] },
|
|
2570
|
+
create: true,
|
|
2571
|
+
createDraft: true,
|
|
2572
|
+
update: ["title", "description", "target"],
|
|
2573
|
+
statuses: ["draft", "open", "closed"],
|
|
2574
|
+
comment: true,
|
|
2575
|
+
merge: ["merge", "squash", "rebase"],
|
|
2576
|
+
deleteSourceOnMerge: true
|
|
2577
|
+
};
|
|
2578
|
+
function sourcecraftPulls() {
|
|
2579
|
+
return { capabilities: PULL_CAPABILITIES3, create: (context) => createPullAdapter(context) };
|
|
2580
|
+
}
|
|
2581
|
+
function createPullAdapter({ connection, fetch }) {
|
|
2582
|
+
const target = requireTarget(connection.target, "org/repo \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F SourceCraft");
|
|
2583
|
+
if (!TARGET8.test(target)) throw new TrackerError("invalid_request", "target SourceCraft \u2014 org/repo.");
|
|
2584
|
+
const api = baseUrl(connection.url, DEFAULT_API5);
|
|
2585
|
+
const root = `${api}/repos/${target}/pulls`;
|
|
2586
|
+
const headers = { authorization: `Bearer ${connection.token ?? ""}` };
|
|
2587
|
+
const missing = (key) => ({ failure: "not_found", message: `\u0412 SourceCraft \u043D\u0435\u0442 PR ${key}.` });
|
|
2588
|
+
const slug = (key) => {
|
|
2589
|
+
if (!SLUG.test(key)) throw new TrackerError("not_found", `\u0412 SourceCraft \u043D\u0435\u0442 PR ${key}.`);
|
|
2590
|
+
return key;
|
|
2591
|
+
};
|
|
2592
|
+
const getPull = async (key) => pullOf3((await requestJson(`${root}/${slug(key)}`, { fetch, headers, missing: missing(key) })).body);
|
|
2593
|
+
return {
|
|
2594
|
+
list: async (query) => {
|
|
2595
|
+
assertListQuery("PR SourceCraft", PULL_CAPABILITIES3.list, query, pullRequestFilterSchema.options);
|
|
2596
|
+
const status = usedStatuses3(query.status);
|
|
2597
|
+
const params = new URLSearchParams({ page_size: String(query.limit) });
|
|
2598
|
+
if (query.cursor !== void 0) params.set("page_token", query.cursor);
|
|
2599
|
+
const filter = pullFilterOf(query, status);
|
|
2600
|
+
if (filter !== void 0) params.set("filter", filter);
|
|
2601
|
+
if (query.sort !== void 0) params.set("sort_by", `${query.sort.direction === "asc" ? "" : "-"}${PULL_SORT_FIELDS_UPSTREAM[query.sort.field]}`);
|
|
2602
|
+
const { body } = await requestJson(`${root}?${params.toString()}`, { fetch, headers });
|
|
2603
|
+
const record = isRecord(body) ? body : {};
|
|
2604
|
+
const pulls = (Array.isArray(record.pull_requests) ? record.pull_requests : []).map(pullOf3).filter((pull) => matchesPage2(pull, query));
|
|
2605
|
+
return { items: pulls, ...nextOf2(record.next_page_token) };
|
|
2606
|
+
},
|
|
2607
|
+
get: (key) => getPull(key),
|
|
2608
|
+
create: async (input) => pullOf3((await requestJson(root, { fetch, headers, method: "POST", body: createBody2(input) })).body),
|
|
2609
|
+
update: async (key, patch) => {
|
|
2610
|
+
const id = slug(key);
|
|
2611
|
+
let updated;
|
|
2612
|
+
if (patch.title !== void 0 || patch.description !== void 0 || patch.target !== void 0) {
|
|
2613
|
+
const body = {
|
|
2614
|
+
...patch.title === void 0 ? {} : { title: patch.title },
|
|
2615
|
+
...patch.description === void 0 ? {} : { description: patch.description },
|
|
2616
|
+
...patch.target === void 0 ? {} : { target_branch: patch.target }
|
|
2617
|
+
};
|
|
2618
|
+
updated = (await requestJson(`${root}/${id}`, { fetch, headers, method: "PATCH", body, missing: missing(key) })).body;
|
|
2619
|
+
}
|
|
2620
|
+
if (patch.status !== void 0) {
|
|
2621
|
+
const action = statusActionOf(patch.status, (await getPull(key)).status.category);
|
|
2622
|
+
if (action !== void 0) await requestJson(`${root}/${id}/${action}`, { fetch, headers, method: "POST", missing: missing(key) });
|
|
2623
|
+
return getPull(key);
|
|
2624
|
+
}
|
|
2625
|
+
return pullOf3(updated ?? (await requestJson(`${root}/${id}`, { fetch, headers, missing: missing(key) })).body);
|
|
2626
|
+
},
|
|
2627
|
+
merge: async (key, input) => {
|
|
2628
|
+
const id = slug(key);
|
|
2629
|
+
if (input.message !== void 0) throw new TrackerError("unsupported", "PR SourceCraft: \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u043A\u043E\u043C\u043C\u0438\u0442\u0430 \u0441\u043B\u0438\u044F\u043D\u0438\u044F \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F.");
|
|
2630
|
+
if (input.method !== void 0 && !PULL_CAPABILITIES3.merge.includes(input.method)) {
|
|
2631
|
+
throw new TrackerError("unsupported", `PR SourceCraft: \u0441\u043B\u0438\u044F\u043D\u0438\u0435 \u0441\u043F\u043E\u0441\u043E\u0431\u043E\u043C ${input.method} \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F.`);
|
|
2632
|
+
}
|
|
2633
|
+
if (input.headSha !== void 0) {
|
|
2634
|
+
const current = await getPull(key);
|
|
2635
|
+
if ((current.headSha ?? "").toLowerCase() !== input.headSha.toLowerCase()) {
|
|
2636
|
+
throw new TrackerError("conflict", `\u0413\u043E\u043B\u043E\u0432\u0430 PR ${key} \u0438\u0437\u043C\u0435\u043D\u0438\u043B\u0430\u0441\u044C: \u043E\u0436\u0438\u0434\u0430\u043B\u0441\u044F ${input.headSha}, \u0441\u0435\u0439\u0447\u0430\u0441 ${current.headSha ?? "\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043D"}.`);
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
const { body } = await requestJson(`${root}/${id}/merge`, {
|
|
2640
|
+
fetch,
|
|
2641
|
+
headers,
|
|
2642
|
+
method: "POST",
|
|
2643
|
+
// squash/rebase по умолчанию наследуются из настройки репозитория — без method не шлём явно false.
|
|
2644
|
+
body: {
|
|
2645
|
+
...input.method === void 0 ? {} : { squash: input.method === "squash", rebase: input.method === "rebase" },
|
|
2646
|
+
...input.deleteSource === void 0 ? {} : { delete_branch: input.deleteSource }
|
|
2647
|
+
},
|
|
2648
|
+
missing: missing(key)
|
|
2649
|
+
});
|
|
2650
|
+
const operation = isRecord(body) ? body : {};
|
|
2651
|
+
if (operation.status === "failed" || operation.status === "cancel") {
|
|
2652
|
+
const errorMessage = isRecord(operation.error) ? text(operation.error.message) : void 0;
|
|
2653
|
+
throw new TrackerError("conflict", `\u0421\u043B\u0438\u044F\u043D\u0438\u0435 PR ${key} \u043D\u0435 \u0437\u0430\u043F\u0443\u0449\u0435\u043D\u043E: ${redactSecretText(errorMessage ?? String(operation.status))}.`);
|
|
2654
|
+
}
|
|
2655
|
+
return getPull(key);
|
|
2656
|
+
},
|
|
2657
|
+
comments: async (key, query) => {
|
|
2658
|
+
const id = slug(key);
|
|
2659
|
+
assertCommentQuery("\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 PR SourceCraft", PULL_CAPABILITIES3.comments, query);
|
|
2660
|
+
const params = new URLSearchParams({ page_size: String(query.limit) });
|
|
2661
|
+
if (query.cursor !== void 0) params.set("page_token", query.cursor);
|
|
2662
|
+
if (query.order !== void 0) params.set("sort_by", query.order === "asc" ? "created_at" : "-created_at");
|
|
2663
|
+
const { body } = await requestJson(`${root}/${id}/comments?${params.toString()}`, { fetch, headers, missing: missing(key) });
|
|
2664
|
+
const record = isRecord(body) ? body : {};
|
|
2665
|
+
const rawComments = Array.isArray(record.pull_request_comments) ? record.pull_request_comments : [];
|
|
2666
|
+
const visible = rawComments.filter((raw) => !(isRecord(raw) && raw.is_deleted === true));
|
|
2667
|
+
const filtered = query.updatedAfter === void 0 ? visible : visible.filter((raw) => inTimeRange(toTimestamp(isRecord(raw) ? raw.updated_at : void 0), query.updatedAfter, void 0));
|
|
2668
|
+
return { items: filtered.map(commentOf6), ...nextOf2(record.next_page_token) };
|
|
2669
|
+
},
|
|
2670
|
+
comment: async (key, body) => commentOf6((await requestJson(`${root}/${slug(key)}/comments`, { fetch, headers, method: "POST", body: { body }, missing: missing(key) })).body)
|
|
2671
|
+
};
|
|
2672
|
+
}
|
|
2673
|
+
function createBody2(input) {
|
|
2674
|
+
return {
|
|
2675
|
+
title: input.title,
|
|
2676
|
+
source_branch: input.source,
|
|
2677
|
+
target_branch: input.target,
|
|
2678
|
+
...input.description === void 0 ? {} : { description: input.description },
|
|
2679
|
+
publish: input.draft !== true
|
|
2680
|
+
};
|
|
2681
|
+
}
|
|
2682
|
+
function statusActionOf(requested, current) {
|
|
2683
|
+
if (current === requested) return void 0;
|
|
2684
|
+
if (requested === "draft") return "draft";
|
|
2685
|
+
if (requested === "open") return current === "draft" ? "publish" : "reopen";
|
|
2686
|
+
return "discard";
|
|
2687
|
+
}
|
|
2688
|
+
function usedStatuses3(status) {
|
|
2689
|
+
if (status === void 0 || status.length === 0) return void 0;
|
|
2690
|
+
for (const item of status) {
|
|
2691
|
+
if (!pullRequestStatusCategorySchema.options.includes(item)) {
|
|
2692
|
+
throw new TrackerError("invalid_request", `PR SourceCraft: \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043A\u0430\u0442\u0435\u0433\u043E\u0440\u0438\u044F \u0441\u0442\u0430\u0442\u0443\u0441\u0430 ${String(item)}.`);
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
return status;
|
|
2696
|
+
}
|
|
2697
|
+
function matchesPage2(pull, query) {
|
|
2698
|
+
if (query.labels !== void 0 && !query.labels.every((label) => pull.labels.includes(label))) return false;
|
|
2699
|
+
if (query.search !== void 0 && !containsText([pull.title, pull.description], query.search)) return false;
|
|
2700
|
+
return inTimeRange(pull.createdAt, query.createdAfter, query.createdBefore) && inTimeRange(pull.updatedAt, query.updatedAfter, query.updatedBefore);
|
|
2701
|
+
}
|
|
2702
|
+
function pullFilterOf(query, status) {
|
|
2703
|
+
const clauses = [];
|
|
2704
|
+
if (status !== void 0) {
|
|
2705
|
+
const slugs = [...new Set(keysFor(SOURCECRAFT_PULL_STATUSES, status))];
|
|
2706
|
+
clauses.push(`status in (${slugs.join(", ")})`);
|
|
2707
|
+
}
|
|
2708
|
+
if (query.author !== void 0) clauses.push(`author_slug=${quoted(query.author)}`);
|
|
2709
|
+
if (query.source !== void 0) clauses.push(`source_branch=${quoted(query.source)}`);
|
|
2710
|
+
if (query.target !== void 0) clauses.push(`target_branch=${quoted(query.target)}`);
|
|
2711
|
+
return clauses.length === 0 ? void 0 : clauses.join(" and ");
|
|
2712
|
+
}
|
|
2713
|
+
function nextOf2(token) {
|
|
2714
|
+
const value = text(token);
|
|
2715
|
+
return value === void 0 ? {} : { nextCursor: value };
|
|
2716
|
+
}
|
|
2717
|
+
function pullOf3(raw) {
|
|
2718
|
+
const item = isRecord(raw) ? raw : {};
|
|
2719
|
+
const source = isRecord(item.source) ? item.source : {};
|
|
2720
|
+
const target = isRecord(item.target) ? item.target : {};
|
|
2721
|
+
const headSha = text(source.sha);
|
|
2722
|
+
const description = text(item.description);
|
|
2723
|
+
const author = isRecord(item.author) ? text(item.author.slug) : void 0;
|
|
2724
|
+
const createdAt = toTimestamp(item.created_at);
|
|
2725
|
+
const updatedAt = toTimestamp(item.updated_at);
|
|
2726
|
+
const mergedAt = toTimestamp(item.merged_at);
|
|
2727
|
+
const closedAt = toTimestamp(item.closed_at);
|
|
2728
|
+
const key = text(item.slug) ?? "?";
|
|
2729
|
+
const statusName = text(item.status) ?? "open";
|
|
2730
|
+
return {
|
|
2731
|
+
key,
|
|
2732
|
+
title: text(item.title) ?? key,
|
|
2733
|
+
...description === void 0 ? {} : { description },
|
|
2734
|
+
status: { category: lookup(SOURCECRAFT_PULL_STATUSES, statusName, "open"), name: statusName },
|
|
2735
|
+
...author === void 0 ? {} : { author },
|
|
2736
|
+
source: text(source.ref) ?? text(item.source_branch) ?? "?",
|
|
2737
|
+
target: text(target.ref) ?? text(item.target_branch) ?? "?",
|
|
2738
|
+
...headSha !== void 0 && SHA4.test(headSha) ? { headSha } : {},
|
|
2739
|
+
labels: Array.isArray(item.labels) ? stringList(item.labels.map((label) => isRecord(label) ? label.name : void 0)) : [],
|
|
2740
|
+
links: [],
|
|
2741
|
+
...createdAt === void 0 ? {} : { createdAt },
|
|
2742
|
+
...updatedAt === void 0 ? {} : { updatedAt },
|
|
2743
|
+
...mergedAt === void 0 ? {} : { mergedAt },
|
|
2744
|
+
...closedAt === void 0 ? {} : { closedAt }
|
|
2745
|
+
};
|
|
2746
|
+
}
|
|
2747
|
+
function commentOf6(raw) {
|
|
2748
|
+
const item = isRecord(raw) ? raw : {};
|
|
2749
|
+
const author = isRecord(item.author) ? text(item.author.slug) : void 0;
|
|
2750
|
+
const createdAt = toTimestamp(item.created_at);
|
|
2751
|
+
return { id: text(item.id) ?? "?", ...author === void 0 ? {} : { author }, body: typeof item.body === "string" ? item.body : "", ...createdAt === void 0 ? {} : { createdAt } };
|
|
2752
|
+
}
|
|
2753
|
+
|
|
912
2754
|
// src/tracker/sourcecraft.lib.ts
|
|
913
|
-
var
|
|
2755
|
+
var DEFAULT_API6 = "https://api.sourcecraft.tech";
|
|
914
2756
|
var DEFAULT_WEB = "https://sourcecraft.dev";
|
|
915
|
-
var
|
|
916
|
-
var
|
|
2757
|
+
var TARGET9 = /^[\w.-]+\/[\w.-]+$/;
|
|
2758
|
+
var SLUG2 = /^[\w-]{1,100}$/;
|
|
917
2759
|
var ISSUE_SORT_FIELDS_UPSTREAM = {
|
|
918
2760
|
created: "created_at",
|
|
919
2761
|
updated: "updated_at",
|
|
@@ -937,18 +2779,20 @@ function sourcecraftTracker() {
|
|
|
937
2779
|
features: { create: true, update: true, comment: true, priority: true, labels: false, assignee: false, statuses: [...ALL_STATUSES] }
|
|
938
2780
|
},
|
|
939
2781
|
issues: ISSUE_CAPABILITIES4,
|
|
940
|
-
create: (context) => createSourceCraftAdapter(context)
|
|
2782
|
+
create: (context) => createSourceCraftAdapter(context),
|
|
2783
|
+
pulls: sourcecraftPulls(),
|
|
2784
|
+
ci: sourcecraftCi()
|
|
941
2785
|
};
|
|
942
2786
|
}
|
|
943
2787
|
function createSourceCraftAdapter({ connection, fetch }) {
|
|
944
2788
|
const target = requireTarget(connection.target, "org/repo \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F SourceCraft");
|
|
945
|
-
if (!
|
|
946
|
-
const api = baseUrl(connection.url,
|
|
2789
|
+
if (!TARGET9.test(target)) throw new TrackerError("invalid_request", "target SourceCraft \u2014 org/repo.");
|
|
2790
|
+
const api = baseUrl(connection.url, DEFAULT_API6);
|
|
947
2791
|
const root = `${api}/repos/${target}/issues`;
|
|
948
2792
|
const headers = { authorization: `Bearer ${connection.token ?? ""}` };
|
|
949
2793
|
const web = connection.url === void 0 ? `${DEFAULT_WEB}/${target}/issues` : void 0;
|
|
950
2794
|
const slug = (key) => {
|
|
951
|
-
if (!
|
|
2795
|
+
if (!SLUG2.test(key)) throw new TrackerError("unknown_issue", `\u0412 SourceCraft \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 ${key}.`);
|
|
952
2796
|
return key;
|
|
953
2797
|
};
|
|
954
2798
|
const toIssue2 = (raw) => issueOf4(raw, web);
|
|
@@ -965,14 +2809,14 @@ function createSourceCraftAdapter({ connection, fetch }) {
|
|
|
965
2809
|
let issues = (Array.isArray(record.issues) ? record.issues : []).map(toIssue2);
|
|
966
2810
|
if (query.status !== void 0) issues = issues.filter((issue) => query.status?.includes(issue.status.category) === true);
|
|
967
2811
|
if (query.assignee === null) issues = issues.filter((issue) => issue.assignee === void 0);
|
|
968
|
-
return { items: issues, ...
|
|
2812
|
+
return { items: issues, ...nextOf3(record.next_page_token) };
|
|
969
2813
|
},
|
|
970
2814
|
get: async (key) => toIssue2((await requestJson(`${root}/${slug(key)}`, { fetch, headers, missingIsIssue: true })).body),
|
|
971
2815
|
create: async (input) => {
|
|
972
2816
|
const body = { title: input.title, ...input.description === void 0 ? {} : { description: input.description }, ...input.priority === void 0 ? {} : { priority: input.priority } };
|
|
973
2817
|
return toIssue2((await requestJson(root, { fetch, headers, method: "POST", body })).body);
|
|
974
2818
|
},
|
|
975
|
-
update: async (key, patch) => toIssue2((await requestJson(`${root}/${slug(key)}`, { fetch, headers, method: "PATCH", body:
|
|
2819
|
+
update: async (key, patch) => toIssue2((await requestJson(`${root}/${slug(key)}`, { fetch, headers, method: "PATCH", body: patchBody5(patch), missingIsIssue: true })).body),
|
|
976
2820
|
comments: async (key, query) => {
|
|
977
2821
|
assertCommentQuery("\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 SourceCraft", ISSUE_CAPABILITIES4.comments, query);
|
|
978
2822
|
const params = new URLSearchParams({ page_size: String(query.limit) });
|
|
@@ -982,12 +2826,12 @@ function createSourceCraftAdapter({ connection, fetch }) {
|
|
|
982
2826
|
const record = isRecord(body) ? body : {};
|
|
983
2827
|
const rawComments = Array.isArray(record.issue_comments) ? record.issue_comments : [];
|
|
984
2828
|
const filtered = query.updatedAfter === void 0 ? rawComments : rawComments.filter((raw) => inTimeRange(toTimestamp(isRecord(raw) ? raw.updated_at : void 0), query.updatedAfter, void 0));
|
|
985
|
-
return { items: filtered.map(
|
|
2829
|
+
return { items: filtered.map(commentOf7), ...nextOf3(record.next_page_token) };
|
|
986
2830
|
},
|
|
987
|
-
comment: async (key, body) =>
|
|
2831
|
+
comment: async (key, body) => commentOf7((await requestJson(`${root}/${slug(key)}/comments`, { fetch, headers, method: "POST", body: { body }, missingIsIssue: true })).body)
|
|
988
2832
|
};
|
|
989
2833
|
}
|
|
990
|
-
function
|
|
2834
|
+
function patchBody5(patch) {
|
|
991
2835
|
return {
|
|
992
2836
|
...patch.title === void 0 ? {} : { title: patch.title },
|
|
993
2837
|
...patch.description === void 0 ? {} : { description: patch.description },
|
|
@@ -995,7 +2839,7 @@ function patchBody3(patch) {
|
|
|
995
2839
|
...patch.priority === void 0 ? {} : { priority: patch.priority }
|
|
996
2840
|
};
|
|
997
2841
|
}
|
|
998
|
-
function
|
|
2842
|
+
function nextOf3(token) {
|
|
999
2843
|
const value = text(token);
|
|
1000
2844
|
return value === void 0 ? {} : { nextCursor: value };
|
|
1001
2845
|
}
|
|
@@ -1044,7 +2888,7 @@ function issueOf4(raw, web) {
|
|
|
1044
2888
|
...updatedAt === void 0 ? {} : { updatedAt }
|
|
1045
2889
|
};
|
|
1046
2890
|
}
|
|
1047
|
-
function
|
|
2891
|
+
function commentOf7(raw) {
|
|
1048
2892
|
const item = isRecord(raw) ? raw : {};
|
|
1049
2893
|
const author = isRecord(item.author) ? text(item.author.slug) : void 0;
|
|
1050
2894
|
const createdAt = toTimestamp(item.created_at);
|
|
@@ -1107,13 +2951,33 @@ function writeTrackerToken(dataDir, projectId, token, sink = defaultSecretSink)
|
|
|
1107
2951
|
function deleteTrackerToken(dataDir, projectId, sink = defaultSecretSink) {
|
|
1108
2952
|
return writeTrackerToken(dataDir, projectId, "", sink);
|
|
1109
2953
|
}
|
|
2954
|
+
|
|
2955
|
+
// src/tracker/pull.lib.ts
|
|
2956
|
+
function pullRequestAdapterOf(definition, context) {
|
|
2957
|
+
if (definition.pulls === void 0) throw new TrackerError("unsupported", `\u0422\u0440\u0435\u043A\u0435\u0440 ${definition.descriptor.title} \u043D\u0435 \u0432\u0435\u0434\u0451\u0442 pull request-\u044B.`);
|
|
2958
|
+
return definition.pulls.create(context);
|
|
2959
|
+
}
|
|
1110
2960
|
export {
|
|
1111
2961
|
ALL_STATUSES,
|
|
2962
|
+
CI_FINISHED_STATUSES,
|
|
2963
|
+
CI_LOG_DEFAULT_BYTES,
|
|
2964
|
+
CI_LOG_MAX_BYTES,
|
|
2965
|
+
CI_LOG_PAGES_MAX,
|
|
2966
|
+
CI_LOG_READ_MAX_BYTES,
|
|
2967
|
+
CI_LOG_TIMEOUT_MS,
|
|
1112
2968
|
COMMENT_FILTERS,
|
|
1113
2969
|
GITHUB_CLOSE_REASONS,
|
|
1114
2970
|
GITHUB_IN_PROGRESS_LABEL,
|
|
1115
2971
|
GITHUB_PRIORITY_LABELS,
|
|
2972
|
+
GITHUB_RUN_CONCLUSIONS,
|
|
2973
|
+
GITHUB_RUN_EVENTS,
|
|
2974
|
+
GITHUB_RUN_STATUSES,
|
|
2975
|
+
GITLAB_CI_STATUSES,
|
|
2976
|
+
GITLAB_DRAFT_PATTERN,
|
|
2977
|
+
GITLAB_DRAFT_PREFIX,
|
|
1116
2978
|
GITLAB_IN_PROGRESS_LABEL,
|
|
2979
|
+
GITLAB_MERGE_REQUEST_STATES,
|
|
2980
|
+
GITLAB_PIPELINE_SOURCES,
|
|
1117
2981
|
GITLAB_PRIORITY_LABELS,
|
|
1118
2982
|
ISSUE_FILTERS,
|
|
1119
2983
|
ISSUE_SORT_FIELDS,
|
|
@@ -1122,7 +2986,10 @@ export {
|
|
|
1122
2986
|
JIRA_PRIORITY_NAMES,
|
|
1123
2987
|
JIRA_STATUS_CATEGORIES,
|
|
1124
2988
|
LEGACY_ISSUE_CAPABILITIES,
|
|
2989
|
+
SOURCECRAFT_CI_STATUSES,
|
|
1125
2990
|
SOURCECRAFT_PRIORITIES,
|
|
2991
|
+
SOURCECRAFT_PULL_STATUSES,
|
|
2992
|
+
SOURCECRAFT_RUN_EVENTS,
|
|
1126
2993
|
SOURCECRAFT_STATUSES,
|
|
1127
2994
|
SOURCECRAFT_STATUS_FILTER,
|
|
1128
2995
|
SOURCECRAFT_STATUS_SLUGS,
|
|
@@ -1135,14 +3002,31 @@ export {
|
|
|
1135
3002
|
assertCommentQuery,
|
|
1136
3003
|
assertListQuery,
|
|
1137
3004
|
baseUrl,
|
|
3005
|
+
ciAdapterOf,
|
|
3006
|
+
ciEventCategorySchema,
|
|
3007
|
+
ciJobFilterSchema,
|
|
3008
|
+
ciJobSchema,
|
|
3009
|
+
ciLogSchema,
|
|
3010
|
+
ciRunFilterSchema,
|
|
3011
|
+
ciRunSchema,
|
|
3012
|
+
ciRunSortFieldSchema,
|
|
3013
|
+
ciStatusCategorySchema,
|
|
3014
|
+
ciStatusSchema,
|
|
3015
|
+
ciWorkflowSchema,
|
|
3016
|
+
cleanLogText,
|
|
1138
3017
|
clipText,
|
|
1139
3018
|
containsText,
|
|
1140
3019
|
deleteTrackerToken,
|
|
1141
3020
|
findTracker,
|
|
3021
|
+
githubCi,
|
|
3022
|
+
githubPulls,
|
|
1142
3023
|
githubTracker,
|
|
3024
|
+
gitlabCi,
|
|
3025
|
+
gitlabPulls,
|
|
1143
3026
|
gitlabTracker,
|
|
1144
3027
|
hasTrackerToken,
|
|
1145
3028
|
inTimeRange,
|
|
3029
|
+
isFinished,
|
|
1146
3030
|
isRecord,
|
|
1147
3031
|
isoTime,
|
|
1148
3032
|
issueCapabilitiesOf,
|
|
@@ -1150,15 +3034,27 @@ export {
|
|
|
1150
3034
|
keysFor,
|
|
1151
3035
|
listTrackers,
|
|
1152
3036
|
localTracker,
|
|
3037
|
+
logCursor,
|
|
3038
|
+
logOf,
|
|
3039
|
+
logWindow,
|
|
1153
3040
|
lookup,
|
|
3041
|
+
mergeMethodSchema,
|
|
1154
3042
|
nameOf,
|
|
1155
3043
|
priorityName,
|
|
3044
|
+
pullRequestAdapterOf,
|
|
3045
|
+
pullRequestFilterSchema,
|
|
3046
|
+
pullRequestSchema,
|
|
3047
|
+
pullRequestSortFieldSchema,
|
|
3048
|
+
pullRequestStatusCategorySchema,
|
|
1156
3049
|
quoted,
|
|
1157
3050
|
readTrackerToken,
|
|
1158
3051
|
registerBuiltinTrackers,
|
|
1159
3052
|
registerTracker,
|
|
1160
3053
|
requestJson,
|
|
3054
|
+
requestText,
|
|
1161
3055
|
requireTarget,
|
|
3056
|
+
sourcecraftCi,
|
|
3057
|
+
sourcecraftPulls,
|
|
1162
3058
|
sourcecraftTracker,
|
|
1163
3059
|
stringList,
|
|
1164
3060
|
text,
|