@remogram/provider-gitlab-api 0.1.0-beta.3 → 0.1.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +546 -27
  2. package/package.json +2 -2
package/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  fetchJson,
3
+ fetchJsonWithMeta,
3
4
  sanitizeField,
4
5
  sanitizeUrl,
5
6
  assertGitRef,
@@ -9,19 +10,47 @@ import {
9
10
  gitAheadBehind,
10
11
  refsInventory,
11
12
  crInventory,
12
- mergeBlockersFromFacts,
13
+ buildMergePlanBodyFromFacts,
13
14
  ERROR_CODES,
14
15
  forgeError,
15
16
  forgeIngestCapabilityFacts,
16
17
  checkPaginationCapabilityFacts,
18
+ openPullListCapabilityFacts,
17
19
  DEFAULT_CHECK_STATUS_PAGE_SIZE,
18
20
  MAX_CHECK_STATUS_PAGES,
21
+ DEFAULT_OPEN_PULL_LIST_PAGE_SIZE,
19
22
  paginateCheckStatusPages,
20
23
  paginateOffsetListPages,
21
24
  fetchWithIngestPageBackoff,
22
25
  fetchPageWithIngestBackoff,
23
26
  withPerPageParam,
24
27
  apiProviderCommands,
28
+ normalizeCrInventorySort,
29
+ DEFAULT_CR_INVENTORY_SLICE_SORT,
30
+ parseTotalCountHeader,
31
+ isCrInventoryFastPathEligible,
32
+ validateFastPathPageLength,
33
+ isNumberSortFastPathEligible,
34
+ isNumberSortFullCollectRequired,
35
+ resolveListTruncatedWithTrustedTotal,
36
+ orderOpenPullNumbers,
37
+ buildOpenPullListMeta,
38
+ gitlabOpenPullSortQuery,
39
+ appendSortQuery,
40
+ buildProviderIdentityFromGitLabUser,
41
+ buildBranchProtectionFromGitLabProtection,
42
+ buildCrFilesFromGitLabChanges,
43
+ buildCrCommentsBody,
44
+ buildCrCommentsFromGitLabDiscussions,
45
+ parseSinceObservedAt,
46
+ buildForgeChangesFromGiteaPulls,
47
+ buildChecksConclusionObservedEvent,
48
+ appendForgeChangeEvents,
49
+ parseStatusSetArgs,
50
+ buildCommitStatusSetBody,
51
+ normalizeStatusSetState,
52
+ MAX_OPEN_PULL_IDEMPOTENCY_PAGES,
53
+ statusSetIdempotencyScanCapabilityFacts,
25
54
  } from '@remogram/core';
26
55
 
27
56
  const PUBLIC_GITLAB_HOST = 'gitlab.com';
@@ -35,8 +64,16 @@ const AUTH_CAPABILITIES = [
35
64
  'pr_checks',
36
65
  'merge_plan',
37
66
  'sync_plan',
67
+ 'status_set',
68
+ 'whoami',
38
69
  ];
39
- const STRUCTURED_COMMANDS = apiProviderCommands();
70
+ const STRUCTURED_COMMANDS = apiProviderCommands({
71
+ branchProtectionImplemented: true,
72
+ crFilesImplemented: true,
73
+ crCommentsImplemented: true,
74
+ forgeChangesImplemented: true,
75
+ statusSetImplemented: true,
76
+ });
40
77
 
41
78
  export function gitlabToken() {
42
79
  return process.env.GITLAB_TOKEN || null;
@@ -129,6 +166,16 @@ export async function gitlabFetch(config, parsed, path, options = {}) {
129
166
  });
130
167
  }
131
168
 
169
+ export async function gitlabFetchWithMeta(config, parsed, path, options = {}) {
170
+ const base = apiBase(config, parsed);
171
+ const token = requireToken();
172
+ const url = `${base}${path}`;
173
+ return fetchJsonWithMeta(url, {
174
+ ...options,
175
+ headers: { ...authHeaders(token), ...(options.headers || {}) },
176
+ });
177
+ }
178
+
132
179
  const MAX_CHECK_PAGES = MAX_CHECK_STATUS_PAGES;
133
180
  const GITLAB_PAGE_SIZE = 100;
134
181
 
@@ -155,13 +202,19 @@ export function providerCapabilities() {
155
202
  mergeability_confidence: 'derived',
156
203
  host_binding: 'verified_remote_host',
157
204
  pagination: 'supported',
158
- write_support: false,
205
+ write_support: true,
206
+ write_commands: ['status_set'],
159
207
  ...forgeIngestCapabilityFacts(),
208
+ ...statusSetIdempotencyScanCapabilityFacts(),
160
209
  ...checkPaginationCapabilityFacts({
161
210
  strategy: 'offset_limit',
162
211
  pageSizeParam: 'per_page',
163
212
  sourceCount: check_sources.length,
164
213
  }),
214
+ ...openPullListCapabilityFacts({
215
+ totalCountSource: 'response_header',
216
+ totalCountHeader: 'X-Total',
217
+ }),
165
218
  };
166
219
  }
167
220
 
@@ -314,45 +367,321 @@ export async function prChecks(ctx, opts) {
314
367
  export async function mergePlan(ctx, opts) {
315
368
  const view = await prView(ctx, opts);
316
369
  const checks = await prChecks(ctx, { number: view.pr_number });
317
- const blockers = mergeBlockersFromFacts(view, checks);
370
+ let mergeOpts = opts;
371
+ if (opts.allowed_paths) {
372
+ try {
373
+ const crFilesBody = await crFiles(ctx, { number: view.pr_number });
374
+ mergeOpts = { ...opts, changed_paths: crFilesBody.changed_paths };
375
+ } catch {
376
+ // Fall back to local git diff in resolveMergePlanPathScope.
377
+ }
378
+ }
379
+ return buildMergePlanBodyFromFacts(ctx, view, checks, mergeOpts);
380
+ }
381
+
382
+ export async function crFiles(ctx, { number }) {
383
+ requireToken();
384
+ if (number == null) {
385
+ throw Object.assign(new Error('--number required'), {
386
+ forgeError: forgeError(ERROR_CODES.INVALID_ARGS, 'Provide --number for MR changed paths'),
387
+ });
388
+ }
389
+ const data = await gitlabFetch(
390
+ ctx.config,
391
+ ctx.parsed,
392
+ projectApiPath(ctx.config, 'merge_requests', number, 'changes'),
393
+ );
394
+ return buildCrFilesFromGitLabChanges(number, data?.changes);
395
+ }
396
+
397
+ export async function crComments(ctx, { number }) {
398
+ requireToken();
399
+ if (number == null) {
400
+ throw Object.assign(new Error('--number required'), {
401
+ forgeError: forgeError(ERROR_CODES.INVALID_ARGS, 'Provide --number for MR review comments'),
402
+ });
403
+ }
404
+ const path = projectApiPath(ctx.config, 'merge_requests', number, 'discussions');
405
+ const pageSep = path.includes('?') ? '&' : '?';
406
+ let activeLimit = DEFAULT_CHECK_STATUS_PAGE_SIZE;
407
+ const allDiscussions = [];
408
+ let listTruncated = false;
409
+ let entryCount = 0;
410
+
411
+ for (let page = 1; page <= MAX_CHECK_STATUS_PAGES; page += 1) {
412
+ const { items, usedLimit } = await fetchPageWithIngestBackoff(
413
+ async ({ page: pageNum, limit }) => {
414
+ const body = await gitlabFetch(
415
+ ctx.config,
416
+ ctx.parsed,
417
+ `${path}${pageSep}per_page=${limit}&page=${pageNum}`,
418
+ );
419
+ if (!Array.isArray(body)) {
420
+ throw Object.assign(new Error('Provider returned non-array MR discussions list'), {
421
+ forgeError: forgeError(
422
+ ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT,
423
+ 'Provider returned non-array MR discussions list',
424
+ ),
425
+ });
426
+ }
427
+ return body;
428
+ },
429
+ page,
430
+ activeLimit,
431
+ );
432
+ activeLimit = usedLimit;
433
+ for (const discussion of items) {
434
+ const notes = Array.isArray(discussion?.notes) ? discussion.notes : [];
435
+ for (const note of notes) {
436
+ if (note?.system === true) continue;
437
+ entryCount += 1;
438
+ }
439
+ }
440
+ allDiscussions.push(...items);
441
+ if (items.length < usedLimit) break;
442
+ if (page === MAX_CHECK_STATUS_PAGES) {
443
+ listTruncated = true;
444
+ }
445
+ }
446
+
447
+ const body = buildCrCommentsFromGitLabDiscussions(number, allDiscussions);
448
+ if (listTruncated) {
449
+ return buildCrCommentsBody({
450
+ pr_number: body.pr_number,
451
+ comments: body.comments,
452
+ comments_truncated: true,
453
+ comment_count: entryCount,
454
+ });
455
+ }
456
+ return body;
457
+ }
458
+
459
+ function gitlabMergeRequestAsPull(mr) {
460
+ if (mr == null || mr.iid == null) return null;
461
+ let state = 'unknown';
462
+ if (mr.state === 'opened') state = 'open';
463
+ else if (mr.state === 'merged' || mr.state === 'closed') state = 'closed';
318
464
  return {
319
- pr_number: view.pr_number,
320
- mergeability: view.mergeability,
321
- checks_conclusion: checks.check_conclusion,
322
- blockers,
465
+ number: mr.iid,
466
+ title: mr.title,
467
+ html_url: mr.web_url,
468
+ state,
469
+ created_at: mr.created_at,
470
+ updated_at: mr.updated_at,
471
+ closed_at: mr.closed_at,
472
+ merged_at: mr.merged_at,
473
+ head: { sha: mr.sha ?? mr.diff_refs?.head_sha ?? null },
323
474
  };
324
475
  }
325
476
 
326
- export async function listOpenPullsWithMeta(ctx, opts = {}) {
327
- apiBase(ctx.config, ctx.parsed);
477
+ export async function forgeChanges(ctx, { since }) {
328
478
  requireToken();
479
+ const sinceIso = parseSinceObservedAt(since);
480
+ const path = `${projectApiPath(ctx.config, 'merge_requests')}?state=all&order_by=updated_at&sort=desc`;
481
+ const pageSep = '&';
482
+ let activeLimit = DEFAULT_CHECK_STATUS_PAGE_SIZE;
483
+ const allMrs = [];
484
+ let listTruncated = false;
485
+
486
+ for (let page = 1; page <= MAX_CHECK_STATUS_PAGES; page += 1) {
487
+ const { items, usedLimit } = await fetchPageWithIngestBackoff(
488
+ async ({ page: pageNum, limit }) => {
489
+ const body = await gitlabFetch(
490
+ ctx.config,
491
+ ctx.parsed,
492
+ `${path}${pageSep}per_page=${limit}&page=${pageNum}`,
493
+ );
494
+ if (!Array.isArray(body)) {
495
+ throw Object.assign(new Error('Provider returned non-array merge request list'), {
496
+ forgeError: forgeError(
497
+ ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT,
498
+ 'Provider returned non-array merge request list',
499
+ ),
500
+ });
501
+ }
502
+ return body;
503
+ },
504
+ page,
505
+ activeLimit,
506
+ );
507
+ activeLimit = usedLimit;
508
+ allMrs.push(...items);
509
+ if (items.length < usedLimit) break;
510
+ if (page === MAX_CHECK_STATUS_PAGES) {
511
+ listTruncated = true;
512
+ }
513
+ }
514
+
515
+ const pulls = allMrs.map(gitlabMergeRequestAsPull).filter(Boolean);
516
+ let body = buildForgeChangesFromGiteaPulls(sinceIso, pulls, { listTruncated });
517
+ const checkNumbers = new Set();
518
+ for (const event of body.events) {
519
+ if (event.kind === 'pr_opened' || event.kind === 'head_sha_moved') {
520
+ checkNumbers.add(event.pr_number);
521
+ }
522
+ }
523
+
524
+ const checkEvents = [];
525
+ for (const number of checkNumbers) {
526
+ const checks = await prChecks(ctx, { number });
527
+ checkEvents.push(buildChecksConclusionObservedEvent(number, checks));
528
+ }
529
+
530
+ if (checkEvents.length > 0) {
531
+ body = appendForgeChangeEvents(body, checkEvents, { listTruncated });
532
+ }
533
+
534
+ return body;
535
+ }
536
+
537
+ const GITLAB_OPEN_PULL_COMPLIANT_MAX =
538
+ DEFAULT_OPEN_PULL_LIST_PAGE_SIZE * MAX_CHECK_STATUS_PAGES;
539
+
540
+ async function probeGitlabOpenPullPageOne(ctx, retainMax, sliceSort) {
541
+ const maxTrusted = GITLAB_OPEN_PULL_COMPLIANT_MAX * 2;
542
+ let path = `${projectApiPath(ctx.config, 'merge_requests')}?state=opened`;
543
+ path = appendSortQuery(path, gitlabOpenPullSortQuery(sliceSort));
544
+ const separator = path.includes('?') ? '&' : '?';
545
+ const requestLimit = Math.min(retainMax, GITLAB_PAGE_SIZE);
546
+ try {
547
+ const { body, headers } = await gitlabFetchWithMeta(
548
+ ctx.config,
549
+ ctx.parsed,
550
+ `${path}${separator}per_page=${requestLimit}&page=1`,
551
+ );
552
+ if (!Array.isArray(body)) return null;
553
+ const totalCount = parseTotalCountHeader(headers, 'X-Total', { maxTrusted });
554
+ if (totalCount == null) return null;
555
+ const listTruncated = totalCount > GITLAB_OPEN_PULL_COMPLIANT_MAX;
556
+ return { body, totalCount, listTruncated, requestLimit };
557
+ } catch {
558
+ return null;
559
+ }
560
+ }
561
+
562
+ function buildGitlabOpenPullMetaFromPage(body, retainMax, sliceSort, totalCount, listTruncated) {
563
+ let numbers = orderOpenPullNumbers(body, (mr) => mr?.iid, sliceSort);
564
+ if (numbers.length > retainMax) numbers = numbers.slice(0, retainMax);
565
+ return buildOpenPullListMeta({
566
+ totalCount,
567
+ numbers,
568
+ listTruncated,
569
+ sliceSort,
570
+ });
571
+ }
572
+
573
+ function gitlabProbePaginationOpts(probe, extra = {}) {
574
+ const { body, totalCount, requestLimit } = probe;
575
+ return {
576
+ trustedTotalCount: totalCount,
577
+ seededFirstPage: { items: body, usedLimit: requestLimit },
578
+ ...extra,
579
+ };
580
+ }
581
+
582
+ async function paginateGitlabOpenPullList(ctx, opts, sliceSort, paginationOpts = {}) {
583
+ const {
584
+ trustedTotalCount = null,
585
+ numberSortFullCollect = false,
586
+ seededFirstPage = null,
587
+ startPage = 1,
588
+ maxPages = MAX_CHECK_STATUS_PAGES,
589
+ suppressFinalPageProbe = false,
590
+ } = paginationOpts;
329
591
  const listLimit =
330
592
  opts.limit != null && Number.isInteger(Number(opts.limit)) && Number(opts.limit) > 0
331
593
  ? Number(opts.limit)
332
594
  : null;
333
- const path = `${projectApiPath(ctx.config, 'merge_requests')}?state=opened`;
595
+ const retainMax =
596
+ listLimit == null &&
597
+ opts.retain_max != null &&
598
+ Number.isInteger(Number(opts.retain_max)) &&
599
+ Number(opts.retain_max) > 0
600
+ ? Number(opts.retain_max)
601
+ : null;
602
+ let path = `${projectApiPath(ctx.config, 'merge_requests')}?state=opened`;
603
+ path = appendSortQuery(path, gitlabOpenPullSortQuery(sliceSort));
334
604
  const separator = path.includes('?') ? '&' : '?';
335
- const { items: all, list_truncated: listTruncated } = await paginateOffsetListPages({
605
+ const effectiveRetainMax = numberSortFullCollect ? null : retainMax;
606
+ const {
607
+ items: all,
608
+ list_truncated: listTruncated,
609
+ entry_count: entryCount,
610
+ walked_count: walkedCount,
611
+ } = await paginateOffsetListPages({
336
612
  pageSize: GITLAB_PAGE_SIZE,
337
613
  listLimit,
614
+ retainMax: effectiveRetainMax,
615
+ trustedEntryCount: trustedTotalCount,
616
+ seededFirstPage,
617
+ startPage,
618
+ maxPages,
619
+ suppressFinalPageProbe,
338
620
  ...(listLimit != null ? { maxPagesTruncatesWithLimit: true } : {}),
339
621
  fetchPage: async ({ page, limit }) => {
340
- const body = await gitlabFetch(
341
- ctx.config,
342
- ctx.parsed,
343
- `${path}${separator}per_page=${limit}&page=${page}`,
344
- );
345
- return Array.isArray(body) ? body : [];
346
- },
347
- });
348
- let numbers = all
349
- .map((mr) => mr.iid)
350
- .filter((number) => Number.isInteger(number))
351
- .sort((a, b) => a - b);
352
- if (listLimit != null && numbers.length > listLimit) {
353
- numbers = numbers.slice(0, listLimit);
622
+ const body = await gitlabFetch(
623
+ ctx.config,
624
+ ctx.parsed,
625
+ `${path}${separator}per_page=${limit}&page=${page}`,
626
+ );
627
+ return Array.isArray(body) ? body : [];
628
+ },
629
+ });
630
+ let numbers = orderOpenPullNumbers(all, (mr) => mr?.iid, sliceSort);
631
+ const outputCap = listLimit ?? retainMax;
632
+ if (outputCap != null && numbers.length > outputCap) {
633
+ numbers = numbers.slice(0, outputCap);
634
+ }
635
+ return {
636
+ numbers,
637
+ list_truncated: resolveListTruncatedWithTrustedTotal({
638
+ listTruncated,
639
+ trustedTotalCount,
640
+ walkedCount,
641
+ fullCollect: numberSortFullCollect,
642
+ }),
643
+ ...(entryCount != null ? { entry_count: entryCount } : {}),
644
+ slice_sort: sliceSort,
645
+ };
646
+ }
647
+
648
+ export async function listOpenPullsWithMeta(ctx, opts = {}) {
649
+ apiBase(ctx.config, ctx.parsed);
650
+ requireToken();
651
+ const sliceSort = normalizeCrInventorySort(opts.sort ?? DEFAULT_CR_INVENTORY_SLICE_SORT);
652
+ if (!isCrInventoryFastPathEligible(opts)) {
653
+ return paginateGitlabOpenPullList(ctx, opts, sliceSort);
654
+ }
655
+
656
+ const retainMax = Number(opts.retain_max);
657
+ const probe = await probeGitlabOpenPullPageOne(ctx, retainMax, sliceSort);
658
+ if (!probe) {
659
+ return paginateGitlabOpenPullList(ctx, opts, sliceSort);
660
+ }
661
+
662
+ const { body, totalCount, listTruncated, requestLimit } = probe;
663
+
664
+ if (listTruncated) {
665
+ if (body.length === 0) {
666
+ return paginateGitlabOpenPullList(ctx, opts, sliceSort, gitlabProbePaginationOpts(probe));
667
+ }
668
+ return buildGitlabOpenPullMetaFromPage(body, retainMax, sliceSort, totalCount, true);
354
669
  }
355
- return { numbers, list_truncated: listTruncated };
670
+
671
+ if (
672
+ isNumberSortFastPathEligible(totalCount, retainMax, sliceSort) &&
673
+ validateFastPathPageLength(totalCount, requestLimit, body.length)
674
+ ) {
675
+ return buildGitlabOpenPullMetaFromPage(body, retainMax, sliceSort, totalCount, false);
676
+ }
677
+
678
+ const numberSortFullCollect = isNumberSortFullCollectRequired(totalCount, retainMax, sliceSort);
679
+ return paginateGitlabOpenPullList(
680
+ ctx,
681
+ opts,
682
+ sliceSort,
683
+ gitlabProbePaginationOpts(probe, { numberSortFullCollect }),
684
+ );
356
685
  }
357
686
 
358
687
  export async function listOpenPulls(ctx, opts = {}) {
@@ -392,6 +721,190 @@ export async function syncPlan(ctx, remoteName = 'origin') {
392
721
  };
393
722
  }
394
723
 
724
+ async function fetchGitLabPatSelf(ctx) {
725
+ try {
726
+ return await gitlabFetch(ctx.config, ctx.parsed, '/personal_access_tokens/self');
727
+ } catch {
728
+ return null;
729
+ }
730
+ }
731
+
732
+ export async function whoami(ctx) {
733
+ requireToken();
734
+ const user = await gitlabFetch(ctx.config, ctx.parsed, '/user');
735
+ const patSelf = await fetchGitLabPatSelf(ctx);
736
+ return buildProviderIdentityFromGitLabUser(user, patSelf);
737
+ }
738
+
739
+ function approvalRulesForBranch(rules, branchRef) {
740
+ if (!Array.isArray(rules)) return [];
741
+ return rules.filter((rule) => {
742
+ const branches = rule?.protected_branches;
743
+ if (!Array.isArray(branches)) return false;
744
+ return branches.some((branch) => branch?.name === branchRef);
745
+ });
746
+ }
747
+
748
+ export async function branchProtection(ctx, { branchRef }) {
749
+ assertGitRef(branchRef, '--branch-ref');
750
+ requireToken();
751
+ let protectedBranch = null;
752
+ try {
753
+ protectedBranch = await gitlabFetch(
754
+ ctx.config,
755
+ ctx.parsed,
756
+ projectApiPath(ctx.config, 'protected_branches', branchRef),
757
+ );
758
+ } catch (err) {
759
+ if (err?.status !== 404) throw err;
760
+ }
761
+ let approvalRules = [];
762
+ if (protectedBranch != null) {
763
+ try {
764
+ const allRules = await gitlabFetch(
765
+ ctx.config,
766
+ ctx.parsed,
767
+ projectApiPath(ctx.config, 'approval_rules'),
768
+ );
769
+ approvalRules = approvalRulesForBranch(allRules, branchRef);
770
+ } catch {
771
+ approvalRules = [];
772
+ }
773
+ }
774
+ return buildBranchProtectionFromGitLabProtection(branchRef, { protectedBranch, approvalRules });
775
+ }
776
+
777
+ function gitlabStatusRecordOrder(a, b) {
778
+ const aUpdated = Date.parse(a.updated_at ?? a.created_at ?? '') || 0;
779
+ const bUpdated = Date.parse(b.updated_at ?? b.created_at ?? '') || 0;
780
+ if (aUpdated !== bUpdated) return aUpdated - bUpdated;
781
+ const aId = Number(a.id) || 0;
782
+ const bId = Number(b.id) || 0;
783
+ return aId - bId;
784
+ }
785
+
786
+ function gitlabStatusAsRemogramState(status) {
787
+ const normalized = String(status ?? '').toLowerCase();
788
+ if (normalized === 'failed' || normalized === 'canceled') return 'failure';
789
+ if (
790
+ normalized === 'running'
791
+ || normalized === 'created'
792
+ || normalized === 'waiting_for_resource'
793
+ || normalized === 'preparing'
794
+ ) {
795
+ return 'pending';
796
+ }
797
+ return normalizeStatusSetState(normalized);
798
+ }
799
+
800
+ function remogramStateToGitlabPostState(state) {
801
+ if (state === 'failure' || state === 'error') return 'failed';
802
+ return state;
803
+ }
804
+
805
+ function statusSetIdempotencyScanIncompleteError(pagesScanned, pageSizeUsed) {
806
+ return forgeError(
807
+ ERROR_CODES.IDEMPOTENCY_SCAN_INCOMPLETE,
808
+ 'Cannot prove no commit status exists for sha+context within scan limit; retry or set manually',
809
+ null,
810
+ {
811
+ idempotency_scan: {
812
+ pages: pagesScanned,
813
+ max_pages: MAX_OPEN_PULL_IDEMPOTENCY_PAGES,
814
+ page_size: pageSizeUsed,
815
+ },
816
+ },
817
+ );
818
+ }
819
+
820
+ /** Paginated commit-status scan for idempotent status set; fail-closed when scan cap prevents proof of absence. */
821
+ export async function findCommitStatusByContext(ctx, sha, context) {
822
+ requireToken();
823
+ const path = projectApiPath(ctx.config, 'repository', 'commits', sha, 'statuses');
824
+ const pageSep = path.includes('?') ? '&' : '?';
825
+ let activeLimit = DEFAULT_CHECK_STATUS_PAGE_SIZE;
826
+ let bestMatch = null;
827
+
828
+ for (let page = 1; page <= MAX_OPEN_PULL_IDEMPOTENCY_PAGES; page += 1) {
829
+ const { items, usedLimit } = await fetchPageWithIngestBackoff(
830
+ async ({ page: pageNum, limit }) => {
831
+ const body = await gitlabFetch(
832
+ ctx.config,
833
+ ctx.parsed,
834
+ `${path}${pageSep}per_page=${limit}&page=${pageNum}`,
835
+ );
836
+ if (!Array.isArray(body)) {
837
+ throw Object.assign(new Error('Provider returned non-array commit status list'), {
838
+ forgeError: forgeError(
839
+ ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT,
840
+ 'Provider returned non-array commit status list',
841
+ ),
842
+ });
843
+ }
844
+ return body;
845
+ },
846
+ page,
847
+ activeLimit,
848
+ );
849
+ activeLimit = usedLimit;
850
+
851
+ for (const record of items) {
852
+ if (record?.name !== context) continue;
853
+ if (!bestMatch || gitlabStatusRecordOrder(record, bestMatch) > 0) {
854
+ bestMatch = record;
855
+ }
856
+ }
857
+
858
+ if (items.length < usedLimit) return bestMatch;
859
+ if (page === MAX_OPEN_PULL_IDEMPOTENCY_PAGES) {
860
+ throw Object.assign(new Error('Commit status idempotency scan incomplete'), {
861
+ forgeError: statusSetIdempotencyScanIncompleteError(page, usedLimit),
862
+ });
863
+ }
864
+ }
865
+ return bestMatch;
866
+ }
867
+
868
+ export async function statusSet(ctx, args) {
869
+ const parsed = parseStatusSetArgs(args);
870
+ const existing = await findCommitStatusByContext(ctx, parsed.sha, parsed.context);
871
+ if (existing) {
872
+ const requestedGitlabState = remogramStateToGitlabPostState(parsed.state);
873
+ const existingGitlabState = String(existing.status ?? existing.state ?? '').toLowerCase();
874
+ if (existingGitlabState === requestedGitlabState) {
875
+ const remogramState = gitlabStatusAsRemogramState(existing.status ?? existing.state);
876
+ return buildCommitStatusSetBody(
877
+ { ...existing, status: remogramState },
878
+ parsed,
879
+ { reusedExisting: true },
880
+ );
881
+ }
882
+ }
883
+ const payload = {
884
+ state: remogramStateToGitlabPostState(parsed.state),
885
+ name: parsed.context,
886
+ };
887
+ if (parsed.description != null) payload.description = parsed.description;
888
+ if (parsed.target_url != null) payload.target_url = parsed.target_url;
889
+ const response = await gitlabFetch(
890
+ ctx.config,
891
+ ctx.parsed,
892
+ projectApiPath(ctx.config, 'statuses', parsed.sha),
893
+ {
894
+ method: 'POST',
895
+ headers: { 'Content-Type': 'application/json' },
896
+ body: JSON.stringify(payload),
897
+ },
898
+ );
899
+ return buildCommitStatusSetBody(
900
+ {
901
+ ...response,
902
+ status: gitlabStatusAsRemogramState(response?.status ?? response?.state ?? parsed.state),
903
+ },
904
+ parsed,
905
+ );
906
+ }
907
+
395
908
  export const provider = {
396
909
  id: 'gitlab-api',
397
910
  providerCapabilities,
@@ -404,4 +917,10 @@ export const provider = {
404
917
  prChecks,
405
918
  mergePlan,
406
919
  syncPlan,
920
+ whoami,
921
+ branchProtection,
922
+ crFiles,
923
+ crComments,
924
+ forgeChanges,
925
+ statusSet,
407
926
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remogram/provider-gitlab-api",
3
- "version": "0.1.0-beta.3",
3
+ "version": "0.1.0-beta.5",
4
4
  "description": "GitLab API provider for remogram",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -22,6 +22,6 @@
22
22
  "node": ">=20"
23
23
  },
24
24
  "dependencies": {
25
- "@remogram/core": "0.1.0-beta.3"
25
+ "@remogram/core": "0.1.0-beta.5"
26
26
  }
27
27
  }