canopycms 0.12.1 → 0.13.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/CHANGELOG.md +211 -49
- package/dist/api/branch-review.d.ts +20 -2
- package/dist/api/branch-review.js +112 -23
- package/dist/api/branch-status.d.ts +11 -2
- package/dist/api/branch-status.js +3 -2
- package/dist/api/branch-update-from-base.js +57 -6
- package/dist/api/branch.d.ts +5 -0
- package/dist/api/branch.js +27 -2
- package/dist/api/client.d.ts +2 -2
- package/dist/api/client.js +2 -2
- package/dist/api/content.js +1 -1
- package/dist/api/merge-conflicts.d.ts +10 -1
- package/dist/api/merge-conflicts.js +42 -7
- package/dist/api/types.d.ts +3 -2
- package/dist/base-update.d.ts +6 -0
- package/dist/branch-metadata-file.d.ts +15 -0
- package/dist/branch-metadata-file.js +5 -1
- package/dist/cli/cli.js +238 -108
- package/dist/cli/generate-ai-content.js +20 -6
- package/dist/cli/github-app-manifest.d.ts +4 -1
- package/dist/cli/github-app-manifest.js +5 -1
- package/dist/cli/init.js +7 -1
- package/dist/editor/Editor.js +39 -16
- package/dist/editor/admin/ConflictsStatusAlert.d.ts +8 -0
- package/dist/editor/admin/ConflictsStatusAlert.js +22 -0
- package/dist/editor/admin/SystemHealthPanel.js +4 -3
- package/dist/editor/base-update-wait.d.ts +4 -0
- package/dist/editor/base-update-wait.js +7 -0
- package/dist/editor/components/ConflictsGroup.d.ts +13 -1
- package/dist/editor/components/ConflictsGroup.js +52 -12
- package/dist/editor/components/EditorHeader.d.ts +3 -1
- package/dist/editor/components/EditorHeader.js +11 -7
- package/dist/editor/components/EntryChangesDrawer.d.ts +1 -1
- package/dist/editor/components/EntryChangesDrawer.js +58 -6
- package/dist/editor/conflict-cards.d.ts +4 -1
- package/dist/editor/conflict-cards.js +14 -9
- package/dist/editor/hooks/useBaseUpdate.d.ts +2 -0
- package/dist/editor/hooks/useBaseUpdate.js +12 -2
- package/dist/editor/hooks/useBranchChangesData.d.ts +7 -1
- package/dist/editor/hooks/useBranchChangesData.js +13 -0
- package/dist/editor/hooks/useBranchManager.d.ts +2 -0
- package/dist/editor/hooks/useBranchManager.js +80 -6
- package/dist/editor/hooks/useDraftManager.d.ts +8 -1
- package/dist/editor/hooks/useDraftManager.js +16 -1
- package/dist/editor/hooks/useEntryManager.d.ts +8 -3
- package/dist/editor/hooks/useEntryManager.js +6 -5
- package/dist/git-manager.d.ts +1 -0
- package/dist/git-manager.js +8 -3
- package/dist/merge/entry-merge.js +4 -3
- package/dist/merge/list-alignment.d.ts +9 -6
- package/dist/merge/list-alignment.js +65 -16
- package/dist/merge/rekey.d.ts +11 -0
- package/dist/merge/rekey.js +75 -0
- package/dist/operating-mode/client-safe-strategy.js +1 -1
- package/dist/operating-mode/client-unsafe-strategy.js +6 -0
- package/dist/operating-mode/types.d.ts +10 -2
- package/dist/services.d.ts +12 -4
- package/dist/services.js +56 -0
- package/dist/types.d.ts +35 -3
- package/dist/utils/git.d.ts +7 -0
- package/dist/utils/git.js +35 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/branch-merge.d.ts +27 -3
- package/dist/worker/branch-merge.js +106 -5
- package/dist/worker/branch-policy.d.ts +18 -2
- package/dist/worker/branch-policy.js +36 -0
- package/dist/worker/cms-worker.d.ts +21 -0
- package/dist/worker/cms-worker.js +29 -4
- package/dist/worker/conflicts-status.d.ts +20 -0
- package/dist/worker/conflicts-status.js +204 -0
- package/dist/worker/field-merge.d.ts +17 -2
- package/dist/worker/field-merge.js +95 -2
- package/dist/worker/git-sync.d.ts +3 -2
- package/dist/worker/git-sync.js +9 -0
- package/dist/worker/github-commits.d.ts +3 -3
- package/dist/worker/github-commits.js +192 -40
- package/dist/worker/github-errors.d.ts +15 -0
- package/dist/worker/github-errors.js +47 -0
- package/dist/worker/github-gateway-http.js +7 -0
- package/dist/worker/github-gateway-wire.d.ts +30 -10
- package/dist/worker/github-gateway-wire.js +14 -2
- package/dist/worker/github-gateway.d.ts +17 -0
- package/dist/worker/github-gateway.js +37 -1
- package/dist/worker/history-rewrite.d.ts +40 -8
- package/dist/worker/history-rewrite.js +51 -23
- package/dist/worker/publish-decision.d.ts +37 -0
- package/dist/worker/publish-decision.js +16 -0
- package/dist/worker/rebase.d.ts +25 -5
- package/dist/worker/rebase.js +225 -81
- package/dist/worker/task-runner.js +16 -39
- package/dist/worker/worker-context.d.ts +6 -0
- package/package.json +1 -1
|
@@ -9,11 +9,12 @@ import { getErrorMessage } from '../utils/error.js';
|
|
|
9
9
|
import { escapeForLog } from '../utils/escape-text.js';
|
|
10
10
|
import { isCanopyInternalPath } from '../utils/git.js';
|
|
11
11
|
import { publishesBranch } from './branch-policy.js';
|
|
12
|
-
import { commitMerge, commitsApart, isCleanMergeOf, planMerge, publishPlainToLocalRemote, } from './branch-merge.js';
|
|
12
|
+
import { buildRekey, commitMerge, commitsApart, isCleanMergeOf, planMerge, publishPlainToLocalRemote, } from './branch-merge.js';
|
|
13
13
|
import { MAX_REPORTED_PATHS, RETIRED_SCHEMA_CACHE_PATH, TRACKED_CANOPY_STATE_FIX, } from './canopy-state.js';
|
|
14
14
|
import { enqueueGitHubPush, readPublishedSha, recordSeenOnGitHub } from './history-rewrite.js';
|
|
15
15
|
import { workerLog, workerLogWarn } from './log.js';
|
|
16
|
-
import {
|
|
16
|
+
import { decidePublish } from './publish-decision.js';
|
|
17
|
+
import { SHARED_REPO_STATUS_ARGS, assertNoIncomingSubmodules, fetchFromRemoteGit, } from './shared-repo-git.js';
|
|
17
18
|
export const COMMIT_ID = /^[0-9a-f]{40}([0-9a-f]{24})?$/;
|
|
18
19
|
const MAX_LISTED_PATH_LENGTH = 200;
|
|
19
20
|
/** Why a push was refused while a rewrite stands (task-runner.ts); editors read it as a failed sync. */
|
|
@@ -63,7 +64,10 @@ export async function catchUpWithPublished(ctx, args) {
|
|
|
63
64
|
const parents = (await git.raw(['rev-list', '--parents', '-n', '1', '--end-of-options', published]))
|
|
64
65
|
.trim()
|
|
65
66
|
.split(' ');
|
|
66
|
-
|
|
67
|
+
// A merge built on the clone's head, or a re-key built on GitHub's commits past it.
|
|
68
|
+
const builtOnHead = parents.length === 3 && parents[1] === head;
|
|
69
|
+
const rekeyPastHead = parents.length === 2 && (await contains(git, published, head));
|
|
70
|
+
if (!builtOnHead && !rekeyPastHead)
|
|
67
71
|
return null;
|
|
68
72
|
return fastForward(ctx, args, head, published);
|
|
69
73
|
}
|
|
@@ -124,6 +128,12 @@ export async function bringInGitHubCommits(ctx, args) {
|
|
|
124
128
|
// below builds on remote.git's head whenever it can.
|
|
125
129
|
if (published !== null && published !== head) {
|
|
126
130
|
if ((await commitsApart(git, head, published)).onlyA === 0) {
|
|
131
|
+
// GitHub's commits, which reconcile moved `remote.git` to, come in re-keyed where they need it.
|
|
132
|
+
if (gitHubTip === published) {
|
|
133
|
+
const rekeyed = await rekeyTakenIn(ctx, args, head, published);
|
|
134
|
+
if (rekeyed)
|
|
135
|
+
return rekeyed;
|
|
136
|
+
}
|
|
127
137
|
const stopped = await fastForward(ctx, args, head, published);
|
|
128
138
|
if (stopped)
|
|
129
139
|
return stopped;
|
|
@@ -316,7 +326,7 @@ async function fastForward(ctx, args, from, to) {
|
|
|
316
326
|
await moveClone(git, branchPath, to);
|
|
317
327
|
}
|
|
318
328
|
catch (err) {
|
|
319
|
-
if (
|
|
329
|
+
if (!overwritesSaves(err))
|
|
320
330
|
throw err;
|
|
321
331
|
workerLog(` ${branchDir}: local changes overlap commits from GitHub, which wait for a submit`);
|
|
322
332
|
return { kind: 'wait' };
|
|
@@ -329,29 +339,139 @@ async function fastForward(ctx, args, from, to) {
|
|
|
329
339
|
}
|
|
330
340
|
return null;
|
|
331
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* GitHub's commits brought in under the item keys a field merge gives, where a merge among them
|
|
344
|
+
* left an item under a key that names another item (branch-merge.ts `buildRekey`): one commit on
|
|
345
|
+
* `tip`, recorded, published and moved to in {@link mergeIntoBranch}'s order, its remaps moving the
|
|
346
|
+
* branch's comment threads once it lands. Null, for the caller to fast-forward to `tip` as git
|
|
347
|
+
* keyed it, where no item needs a new key, where the commit could not be published (gateway policy,
|
|
348
|
+
* {@link decidePublish}, a `remote.git` head it doesn't contain), or where the clone's saves would
|
|
349
|
+
* keep the clone from moving to it ({@link savesMeet}).
|
|
350
|
+
*/
|
|
351
|
+
async function rekeyTakenIn(ctx, args, head, tip) {
|
|
352
|
+
const { git, branchPath, branchDir, branchRef, gatewayPolicy, lockLost } = args;
|
|
353
|
+
if (gatewayPolicy && !publishesBranch(gatewayPolicy, branchRef))
|
|
354
|
+
return null;
|
|
355
|
+
const published = await readPublishedSha(ctx, branchRef);
|
|
356
|
+
if ((await decidePublish(git, { head, published })).kind !== 'publish')
|
|
357
|
+
return null;
|
|
358
|
+
const built = await rekeyOf(ctx, args, head, tip);
|
|
359
|
+
if (built === null)
|
|
360
|
+
return null;
|
|
361
|
+
const { sha } = built;
|
|
362
|
+
// A re-key the clone can't move to stays unpublished; the commits come in once the saves do.
|
|
363
|
+
if (args.dirty && (await savesMeet(git, head, sha)))
|
|
364
|
+
return null;
|
|
365
|
+
if (published !== null && !(await contains(git, sha, published)))
|
|
366
|
+
return null;
|
|
367
|
+
const refused = await refuseIncoming(args, head, sha);
|
|
368
|
+
if (refused)
|
|
369
|
+
return refused;
|
|
370
|
+
if (lockLost())
|
|
371
|
+
return { kind: 'wait' };
|
|
372
|
+
// Judged on content: the commit changes item keys only, which `isCleanMergeOf` sets aside.
|
|
373
|
+
if (await changesWhatWasReviewed(ctx, args, head, sha))
|
|
374
|
+
await returnToReview(ctx, args);
|
|
375
|
+
await recordMerge(ctx, args, built, { head, also: tip, fromGitHub: true });
|
|
376
|
+
if (lockLost())
|
|
377
|
+
return { kind: 'wait' };
|
|
378
|
+
await recordSeenOnGitHub(ctx, branchRef, tip);
|
|
379
|
+
if (!(await publishPlainToLocalRemote(ctx, branchPath, branchRef, sha)))
|
|
380
|
+
return { kind: 'wait' };
|
|
381
|
+
await enqueueGitHubPush(ctx, branchRef, gatewayPolicy);
|
|
382
|
+
if (lockLost())
|
|
383
|
+
return { kind: 'wait' };
|
|
384
|
+
try {
|
|
385
|
+
await moveClone(git, branchPath, sha);
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
if (!overwritesSaves(err))
|
|
389
|
+
throw err;
|
|
390
|
+
// Unreached: `savesMeet` looked under the content-write lock this cycle still holds.
|
|
391
|
+
workerLog(` ${branchDir}: local changes overlap commits from GitHub, which wait for a submit`);
|
|
392
|
+
return { kind: 'wait' };
|
|
393
|
+
}
|
|
394
|
+
await landed(ctx, args, sha).catch((err) => workerLogWarn(` ${branchDir}: what this re-key moves is noted on the next cycle: ${getErrorMessage(err)}`));
|
|
395
|
+
await recordGitHubTip(ctx, args, tip);
|
|
396
|
+
workerLog(` ${branchDir}: brought in commits from GitHub, with the items its merges brought in keyed anew (now at ${sha.slice(0, 7)})`);
|
|
397
|
+
return { kind: 'merged', keptEntries: 0 };
|
|
398
|
+
}
|
|
399
|
+
/** {@link buildRekey} on GitHub's commits from `head` to `tip`, or null when none needs a new key. */
|
|
400
|
+
function rekeyOf(ctx, { git, branchPath, branchRef }, head, tip, unchangedIn) {
|
|
401
|
+
return buildRekey(git, branchPath, {
|
|
402
|
+
from: head,
|
|
403
|
+
tip,
|
|
404
|
+
contentRoot: ctx.contentRoot,
|
|
405
|
+
timeoutMs: ctx.taskTimeoutMs,
|
|
406
|
+
message: `Key the items that merges on GitHub brought into ${branchRef}`,
|
|
407
|
+
...(unchangedIn && { unchangedIn }),
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Whether the clone's saves (changes, deletions, new files) touch a path `from..to` changes: the
|
|
412
|
+
* clone can't move to `to` until they are committed, and that commit merges against `from`'s keys.
|
|
413
|
+
*/
|
|
414
|
+
async function savesMeet(git, from, to) {
|
|
415
|
+
const saved = new Set((await git.status([...SHARED_REPO_STATUS_ARGS])).files.flatMap((f) => f.from ? [f.path, f.from] : [f.path]));
|
|
416
|
+
const changed = (await git.raw(['diff-tree', '-r', '-z', '--name-only', '--end-of-options', from, to]))
|
|
417
|
+
.split('\0')
|
|
418
|
+
.filter(Boolean);
|
|
419
|
+
return changed.some((file) => saved.has(file));
|
|
420
|
+
}
|
|
421
|
+
/** A move of the clone refused for saves it would overwrite, as git refuses a changed file. */
|
|
422
|
+
class SavesInTheWay extends Error {
|
|
423
|
+
}
|
|
424
|
+
/** Whether `err` is a move refused for the clone's saves: git's own refusal, or {@link moveClone}'s. */
|
|
425
|
+
const overwritesSaves = (err) => err instanceof SavesInTheWay || /would be overwritten/.test(getErrorMessage(err));
|
|
332
426
|
/**
|
|
333
427
|
* Fast-forward the clone to `to`. The caches' markers move after it, as for any bulk mutation, and
|
|
334
428
|
* before it too: nothing repairs a marker lost after the move, as the next cycle sees a clone in
|
|
335
|
-
* step, and the earlier bump leaves only a read racing the move able to stamp the old tree.
|
|
429
|
+
* step, and the earlier bump leaves only a read racing the move able to stamp the old tree. Git
|
|
430
|
+
* refuses to move over a saved change to a file the commits change, but writes a file deleted in
|
|
431
|
+
* the working tree back: an entry the editor deleted, or a renamed entry's old path. So a move over
|
|
432
|
+
* such a deletion is refused here alike ({@link SavesInTheWay}), until the deletion is submitted.
|
|
336
433
|
*/
|
|
337
434
|
async function moveClone(git, branchPath, to) {
|
|
435
|
+
const deleted = (await git.status([...SHARED_REPO_STATUS_ARGS])).files.flatMap((f) => [
|
|
436
|
+
...(f.working_dir === 'D' || f.index === 'D' ? [f.path] : []),
|
|
437
|
+
...(f.from ? [f.from] : []),
|
|
438
|
+
]);
|
|
439
|
+
if (deleted.some((file) => !isCanopyInternalPath(file))) {
|
|
440
|
+
const changed = new Set((await git.raw(['diff-tree', '-r', '-z', '--name-only', '--end-of-options', 'HEAD', to]))
|
|
441
|
+
.split('\0')
|
|
442
|
+
.filter(Boolean));
|
|
443
|
+
const lost = deleted.filter((file) => changed.has(file) && !isCanopyInternalPath(file));
|
|
444
|
+
if (lost.length > 0) {
|
|
445
|
+
throw new SavesInTheWay(`saved deletions would be brought back: ${listPaths(lost)}`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
338
448
|
await invalidateBranchContentCaches(branchPath);
|
|
339
449
|
await git.raw(['merge', '--ff-only', '--end-of-options', to]);
|
|
340
450
|
await invalidateBranchContentCaches(branchPath);
|
|
341
451
|
}
|
|
342
452
|
/**
|
|
343
|
-
* `other` merged into the branch: built, recorded, published to `remote.git
|
|
344
|
-
* and the clone moved to it, in that order,
|
|
345
|
-
* only the last touches the working tree. `fromGitHub` says `other` is GitHub's tip rather than the
|
|
453
|
+
* `other` merged into the branch: built, recorded, published to `remote.git` and queued for GitHub
|
|
454
|
+
* unless it holds unsubmitted work (publish-decision.ts), and the clone moved to it, in that order,
|
|
455
|
+
* so each step leaves what the next cycle picks up and only the last touches the working tree. `fromGitHub` says `other` is GitHub's tip rather than the
|
|
346
456
|
* base. `inReview` takes only a merge with nothing for a person to choose; any other leaves the
|
|
347
457
|
* branch as it is and records nothing, and GitHub's own conflict notice stands on the pull request. A lock lost before the clone moves leaves it where it was: `wait`.
|
|
348
458
|
*/
|
|
349
459
|
export async function mergeIntoBranch(ctx, args, merge) {
|
|
350
460
|
const { git, branchPath, branchDir, branchRef, gatewayPolicy, lockLost } = args;
|
|
351
461
|
const { head, other, message, fromGitHub, inReview } = merge;
|
|
462
|
+
// GitHub's merges come in under the field merge's keys, as a fast-forward takes them in, for the
|
|
463
|
+
// entries the merge takes from GitHub whole: one the branch changed too merges by git's keys.
|
|
464
|
+
const bases = fromGitHub
|
|
465
|
+
? (await git.raw(['merge-base', '--all', '--end-of-options', head, other]))
|
|
466
|
+
.split('\n')
|
|
467
|
+
.filter((line) => COMMIT_ID.test(line))
|
|
468
|
+
: [];
|
|
469
|
+
// With one merge base, as the field merge needs; none or several leave git's merge as it is.
|
|
470
|
+
const rekey = bases.length === 1 ? await rekeyOf(ctx, args, head, other, { head, since: bases[0] }) : null;
|
|
471
|
+
const into = rekey?.sha ?? other;
|
|
352
472
|
const plan = await planMerge(git, branchPath, {
|
|
353
473
|
head,
|
|
354
|
-
other,
|
|
474
|
+
other: into,
|
|
355
475
|
contentRoot: ctx.contentRoot,
|
|
356
476
|
timeoutMs: ctx.taskTimeoutMs,
|
|
357
477
|
});
|
|
@@ -371,7 +491,9 @@ export async function mergeIntoBranch(ctx, args, merge) {
|
|
|
371
491
|
`To continue, a developer merges them by hand.`,
|
|
372
492
|
};
|
|
373
493
|
}
|
|
374
|
-
const
|
|
494
|
+
const merged = await commitMerge(git, plan, { head, other: into, message });
|
|
495
|
+
// The re-key's remaps cover entries the branch left alone, the merge's those both changed.
|
|
496
|
+
const built = rekey ? { ...merged, keyRemaps: [...rekey.keyRemaps, ...merged.keyRemaps] } : merged;
|
|
375
497
|
const { sha, conflicts } = built;
|
|
376
498
|
const refused = await refuseIncoming(args, head, sha);
|
|
377
499
|
if (refused)
|
|
@@ -392,24 +514,38 @@ export async function mergeIntoBranch(ctx, args, merge) {
|
|
|
392
514
|
// Recorded before the merge is visible anywhere: the next cycle only fast-forwards to it, and
|
|
393
515
|
// the conflicts cannot be read back from the merge. A merge that never lands leaves records the
|
|
394
516
|
// next cycle's reconcile turns back.
|
|
395
|
-
await recordMerge(ctx, args, built, { head, fromGitHub });
|
|
517
|
+
await recordMerge(ctx, args, built, { head, ...(fromGitHub && { also: other }), fromGitHub });
|
|
396
518
|
if (lockLost())
|
|
397
519
|
return { kind: 'wait' };
|
|
398
520
|
// Before `remote.git` holds it, as reconcile does: a crash past this point leaves GitHub's commit
|
|
399
521
|
// in `remote.git`, and a removal then has to read as one (`rewrittenFrom`).
|
|
400
522
|
if (fromGitHub)
|
|
401
523
|
await recordSeenOnGitHub(ctx, branchRef, other);
|
|
402
|
-
|
|
524
|
+
const publish = await decidePublish(git, { head, published });
|
|
525
|
+
if (publish.kind === 'publish') {
|
|
403
526
|
if (!(await publishPlainToLocalRemote(ctx, branchPath, branchRef, sha))) {
|
|
404
527
|
return { kind: 'blocked', reason: unpublishable };
|
|
405
528
|
}
|
|
406
529
|
await enqueueGitHubPush(ctx, branchRef, gatewayPolicy);
|
|
407
530
|
}
|
|
531
|
+
else if (publish.kind === 'hold') {
|
|
532
|
+
workerLog(` ${branchDir}: holds work not yet submitted, so the merge stays unpublished`);
|
|
533
|
+
}
|
|
408
534
|
// Published and not yet in the clone: the next cycle brings it in, by `bringInGitHubCommits` or
|
|
409
535
|
// `catchUpWithPublished`.
|
|
410
536
|
if (lockLost())
|
|
411
537
|
return { kind: 'wait' };
|
|
412
|
-
|
|
538
|
+
try {
|
|
539
|
+
await moveClone(git, branchPath, sha);
|
|
540
|
+
}
|
|
541
|
+
catch (err) {
|
|
542
|
+
if (!overwritesSaves(err))
|
|
543
|
+
throw err;
|
|
544
|
+
// Callers move a clean tree. Published, the next cycle catches up with it once the saves are
|
|
545
|
+
// committed; held, reconcile turns it back.
|
|
546
|
+
workerLog(` ${branchDir}: local changes overlap the merge, which waits for a submit`);
|
|
547
|
+
return { kind: 'wait' };
|
|
548
|
+
}
|
|
413
549
|
// Landed: a failure here leaves the remaps and the stamp for the next cycle's reconcile.
|
|
414
550
|
await landed(ctx, args, sha).catch((err) => workerLogWarn(` ${branchDir}: what this merge kept is noted on the next cycle: ${getErrorMessage(err)}`));
|
|
415
551
|
if (fromGitHub)
|
|
@@ -450,19 +586,25 @@ export async function contains(git, commit, ancestor) {
|
|
|
450
586
|
return (await commitsApart(git, ancestor, commit)).onlyA === 0;
|
|
451
587
|
}
|
|
452
588
|
/**
|
|
453
|
-
* Whether moving an approved branch
|
|
454
|
-
*
|
|
455
|
-
* it was reviewed.
|
|
456
|
-
*
|
|
589
|
+
* Whether moving an approved branch to `to` changes what was approved: yes unless `to` is exactly
|
|
590
|
+
* the approved commit with the base up to where `to` has it, which leaves the pull request's diff
|
|
591
|
+
* as it was reviewed. The approved commit is the one the approval recorded, so clean merges since
|
|
592
|
+
* it are judged together; `from`, the head before this move, stands in for an approval that
|
|
593
|
+
* recorded none. A recorded commit this clone doesn't hold counts as a change, as does a
|
|
594
|
+
* hand-made change inside a merge commit. Read fresh, so an approval given since the cycle's
|
|
595
|
+
* start counts too.
|
|
457
596
|
*/
|
|
458
597
|
async function changesWhatWasReviewed(ctx, { git, branchPath }, from, to) {
|
|
459
598
|
const fresh = (await BranchMetadataFileManager.loadOnly(branchPath))?.branch;
|
|
460
599
|
if (fresh?.status !== 'approved')
|
|
461
600
|
return false;
|
|
601
|
+
const approved = fresh.review?.commit ?? from;
|
|
602
|
+
if (!(await holdsCommit(git, approved)))
|
|
603
|
+
return true;
|
|
462
604
|
await fetchFromRemoteGit(git, ctx.remoteGitPath, fresh.baseBranch ?? ctx.baseBranch);
|
|
463
605
|
const base = (await git.revparse(['FETCH_HEAD'])).trim();
|
|
464
606
|
const forkPoint = (await git.raw(['merge-base', '--end-of-options', to, base])).trim();
|
|
465
|
-
return forkPoint === '' || !(await isCleanMergeOf(git, to,
|
|
607
|
+
return forkPoint === '' || !(await isCleanMergeOf(git, to, approved, forkPoint, ctx.contentRoot));
|
|
466
608
|
}
|
|
467
609
|
/**
|
|
468
610
|
* Send an approved branch back to review and clear the verdict from the PR body. Called before the
|
|
@@ -482,12 +624,18 @@ export async function returnToReview(ctx, { branchPath, branchDir, branchRef, me
|
|
|
482
624
|
* IDs it names in `branch.json` `conflictFiles`. The record is first settled against `head`, which
|
|
483
625
|
* the merge builds on, so a record of a merge that never landed can't carry into this one. Before
|
|
484
626
|
* the merge is visible: one that never lands leaves records the next
|
|
485
|
-
* {@link reconcileMergeConflicts} turns back.
|
|
627
|
+
* {@link reconcileMergeConflicts} turns back. A merge `also` holds, GitHub's tip the merge takes in,
|
|
628
|
+
* is landing rather than gone. An earlier landing either commit holds is published, and this one
|
|
629
|
+
* replaces it: what it kept over GitHub's commits is noted for the pull request body now.
|
|
486
630
|
*/
|
|
487
631
|
async function recordMerge(ctx, args, built, merge) {
|
|
488
632
|
const { git, branchPath, branchRef } = args;
|
|
489
633
|
const { sha, conflicts, keptPaths, keyRemaps, paths } = built;
|
|
490
|
-
const inHead = await settleRecord(git, branchPath, merge.head);
|
|
634
|
+
const inHead = await settleRecord(git, branchPath, merge.head, merge.also);
|
|
635
|
+
const earlier = (await new MergeConflictStore(branchPath).read())?.landing;
|
|
636
|
+
if (earlier && earlier.mergeSha !== sha && inHead(earlier.mergeSha)) {
|
|
637
|
+
await noteKeptOverGitHub(ctx, args, earlier.keptOverGitHub ?? []);
|
|
638
|
+
}
|
|
491
639
|
const state = await new MergeConflictStore(branchPath).update((current) => ({
|
|
492
640
|
entries: withFoundConflicts(withRemappedItemKeys(current ? withoutMergesGone(current.entries, inHead) : {}, keyRemaps, sha), conflicts, sha, paths),
|
|
493
641
|
remaps: [
|
|
@@ -519,17 +667,7 @@ async function landed(ctx, { git, branchPath, branchRef, branchDir, metaFile, },
|
|
|
519
667
|
const landing = current.landing;
|
|
520
668
|
if (landing?.mergeSha !== sha)
|
|
521
669
|
return;
|
|
522
|
-
|
|
523
|
-
if (kept.length > 0) {
|
|
524
|
-
await getBranchMetadataFileManager(branchPath, ctx.contentBranchesPath).update((existing) => existing?.branch.name === branchRef
|
|
525
|
-
? {
|
|
526
|
-
branch: {
|
|
527
|
-
keptOverGitHub: [...new Set([...(existing.branch.keptOverGitHub ?? []), ...kept])],
|
|
528
|
-
},
|
|
529
|
-
}
|
|
530
|
-
: null);
|
|
531
|
-
await enqueueReviewBlockSync(ctx, branchRef, metaFile);
|
|
532
|
-
}
|
|
670
|
+
await noteKeptOverGitHub(ctx, { branchPath, branchRef, metaFile }, landing.keptOverGitHub ?? []);
|
|
533
671
|
await store.update((now) => {
|
|
534
672
|
if (now?.landing?.mergeSha !== sha)
|
|
535
673
|
return null;
|
|
@@ -537,6 +675,19 @@ async function landed(ctx, { git, branchPath, branchRef, branchDir, metaFile, },
|
|
|
537
675
|
return rest;
|
|
538
676
|
});
|
|
539
677
|
}
|
|
678
|
+
/** `kept`, paths a merge kept over GitHub's commits, in `keptOverGitHub` for the PR body. */
|
|
679
|
+
async function noteKeptOverGitHub(ctx, { branchPath, branchRef, metaFile, }, kept) {
|
|
680
|
+
if (kept.length === 0)
|
|
681
|
+
return;
|
|
682
|
+
await getBranchMetadataFileManager(branchPath, ctx.contentBranchesPath).update((existing) => existing?.branch.name === branchRef
|
|
683
|
+
? {
|
|
684
|
+
branch: {
|
|
685
|
+
keptOverGitHub: [...new Set([...(existing.branch.keptOverGitHub ?? []), ...kept])],
|
|
686
|
+
},
|
|
687
|
+
}
|
|
688
|
+
: null);
|
|
689
|
+
await enqueueReviewBlockSync(ctx, branchRef, metaFile);
|
|
690
|
+
}
|
|
540
691
|
/** Every merge a record names, its earlier versions' included. */
|
|
541
692
|
function mergesNamedBy(entry) {
|
|
542
693
|
return [
|
|
@@ -562,25 +713,26 @@ async function mergesHeld(git, state, tips) {
|
|
|
562
713
|
return held;
|
|
563
714
|
}
|
|
564
715
|
/**
|
|
565
|
-
* The record as `head` has it: comment threads take the item keys of merges
|
|
566
|
-
*
|
|
567
|
-
* Resolves whether
|
|
568
|
-
* held, as nothing settled it.
|
|
716
|
+
* The record as `head` has it, or `also` with it: comment threads take the item keys of merges
|
|
717
|
+
* `head` holds, the remaps of merges neither holds go, and records changed by merges neither holds
|
|
718
|
+
* give back the ones they replaced. Resolves whether either holds a merge the record named then; a
|
|
719
|
+
* merge recorded since counts as held, as nothing settled it.
|
|
569
720
|
*/
|
|
570
|
-
async function settleRecord(git, branchPath, head) {
|
|
721
|
+
async function settleRecord(git, branchPath, head, also) {
|
|
571
722
|
const store = new MergeConflictStore(branchPath);
|
|
572
723
|
const current = await store.read();
|
|
573
724
|
if (current === null)
|
|
574
725
|
return () => true;
|
|
575
|
-
const [held] = await mergesHeld(git, current, [head]);
|
|
726
|
+
const [held, alsoHeld] = await mergesHeld(git, current, also ? [head, also] : [head]);
|
|
576
727
|
const known = new Set([
|
|
577
728
|
...Object.values(current.entries).flatMap(mergesNamedBy),
|
|
578
729
|
...(current.remaps ?? []).map(({ mergeSha }) => mergeSha),
|
|
579
730
|
]);
|
|
580
731
|
const inHead = (sha) => held.has(sha) || !known.has(sha);
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
732
|
+
const kept = (sha) => inHead(sha) || (alsoHeld?.has(sha) ?? false);
|
|
733
|
+
// Threads move only to merges the clone holds; the rest wait, unless neither commit holds them.
|
|
734
|
+
await applyRemaps(branchPath, inHead, (sha) => !kept(sha)).catch((err) => workerLogWarn(` comment threads keep their item keys for now: ${getErrorMessage(err)}`));
|
|
735
|
+
return kept;
|
|
584
736
|
}
|
|
585
737
|
/**
|
|
586
738
|
* Point the branch's comment threads at the item keys the merges in `landed` gave their items, in
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a GitHub API failure as the gateway passes it on: an Octokit error in-process, or the
|
|
3
|
+
* wire's `GitHubRequestError`, both carrying `status` and the response headers that classify it.
|
|
4
|
+
*/
|
|
5
|
+
/** Extract an HTTP status from an error, if present (Octokit RequestError shape). */
|
|
6
|
+
export declare function getHttpStatus(err: unknown): number | null;
|
|
7
|
+
/**
|
|
8
|
+
* Whether a 403 is a GitHub rate-limit response rather than a plain permission
|
|
9
|
+
* denial. GitHub signals rate limiting on 403s three ways: the primary limit
|
|
10
|
+
* zeroes `x-ratelimit-remaining`, secondary limits often carry `retry-after`,
|
|
11
|
+
* and both produce a message containing "rate limit".
|
|
12
|
+
*/
|
|
13
|
+
export declare function isRateLimitSignal403(err: unknown): boolean;
|
|
14
|
+
/** A 403 that is GitHub denying the credential the operation, not a rate limit. */
|
|
15
|
+
export declare function isPermissionDenial(err: unknown): boolean;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a GitHub API failure as the gateway passes it on: an Octokit error in-process, or the
|
|
3
|
+
* wire's `GitHubRequestError`, both carrying `status` and the response headers that classify it.
|
|
4
|
+
*/
|
|
5
|
+
/** Extract an HTTP status from an error, if present (Octokit RequestError shape). */
|
|
6
|
+
export function getHttpStatus(err) {
|
|
7
|
+
if (err instanceof Error && 'status' in err) {
|
|
8
|
+
const status = err.status;
|
|
9
|
+
if (typeof status === 'number')
|
|
10
|
+
return status;
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
/** Narrow an unknown value to a response-headers record, if present (Octokit lowercases header names). */
|
|
15
|
+
function getResponseHeaders(err) {
|
|
16
|
+
if (typeof err !== 'object' || err === null || !('response' in err))
|
|
17
|
+
return null;
|
|
18
|
+
const response = err.response;
|
|
19
|
+
if (typeof response !== 'object' || response === null || !('headers' in response))
|
|
20
|
+
return null;
|
|
21
|
+
const headers = response.headers;
|
|
22
|
+
if (typeof headers !== 'object' || headers === null)
|
|
23
|
+
return null;
|
|
24
|
+
return headers;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Whether a 403 is a GitHub rate-limit response rather than a plain permission
|
|
28
|
+
* denial. GitHub signals rate limiting on 403s three ways: the primary limit
|
|
29
|
+
* zeroes `x-ratelimit-remaining`, secondary limits often carry `retry-after`,
|
|
30
|
+
* and both produce a message containing "rate limit".
|
|
31
|
+
*/
|
|
32
|
+
export function isRateLimitSignal403(err) {
|
|
33
|
+
const headers = getResponseHeaders(err);
|
|
34
|
+
if (headers) {
|
|
35
|
+
if (headers['x-ratelimit-remaining'] === '0')
|
|
36
|
+
return true;
|
|
37
|
+
if (typeof headers['retry-after'] === 'string' && headers['retry-after'].length > 0)
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
if (err instanceof Error && /rate limit/i.test(err.message))
|
|
41
|
+
return true;
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
/** A 403 that is GitHub denying the credential the operation, not a rate limit. */
|
|
45
|
+
export function isPermissionDenial(err) {
|
|
46
|
+
return getHttpStatus(err) === 403 && !isRateLimitSignal403(err);
|
|
47
|
+
}
|
|
@@ -231,6 +231,10 @@ class GatewayRequestHandler {
|
|
|
231
231
|
await gateway.convertPullRequestToDraft(number, signal);
|
|
232
232
|
return {};
|
|
233
233
|
},
|
|
234
|
+
setConflictsStatus: async (status, signal) => {
|
|
235
|
+
await gateway.setConflictsStatus(status, signal);
|
|
236
|
+
return {};
|
|
237
|
+
},
|
|
234
238
|
deleteBranch: async ({ branch }, signal) => {
|
|
235
239
|
await gateway.deleteBranch(branch, signal);
|
|
236
240
|
return {};
|
|
@@ -824,6 +828,9 @@ class GitHubGatewayClient {
|
|
|
824
828
|
async convertPullRequestToDraft(number, signal) {
|
|
825
829
|
await this.call('convertPullRequestToDraft', { number }, signal);
|
|
826
830
|
}
|
|
831
|
+
async setConflictsStatus(status, signal) {
|
|
832
|
+
await this.call('setConflictsStatus', status, signal);
|
|
833
|
+
}
|
|
827
834
|
async deleteBranch(branch, signal) {
|
|
828
835
|
await this.call('deleteBranch', { branch }, signal);
|
|
829
836
|
}
|
|
@@ -119,6 +119,25 @@ export declare const requests: {
|
|
|
119
119
|
}, {
|
|
120
120
|
number: number;
|
|
121
121
|
}>;
|
|
122
|
+
readonly setConflictsStatus: z.ZodObject<{
|
|
123
|
+
pullRequest: z.ZodNumber;
|
|
124
|
+
sha: z.ZodString;
|
|
125
|
+
state: z.ZodEnum<["success", "failure"]>;
|
|
126
|
+
description: z.ZodString;
|
|
127
|
+
targetUrl: z.ZodOptional<z.ZodString>;
|
|
128
|
+
}, "strict", z.ZodTypeAny, {
|
|
129
|
+
description: string;
|
|
130
|
+
state: "success" | "failure";
|
|
131
|
+
sha: string;
|
|
132
|
+
pullRequest: number;
|
|
133
|
+
targetUrl?: string | undefined;
|
|
134
|
+
}, {
|
|
135
|
+
description: string;
|
|
136
|
+
state: "success" | "failure";
|
|
137
|
+
sha: string;
|
|
138
|
+
pullRequest: number;
|
|
139
|
+
targetUrl?: string | undefined;
|
|
140
|
+
}>;
|
|
122
141
|
readonly deleteBranch: z.ZodObject<{
|
|
123
142
|
branch: z.ZodString;
|
|
124
143
|
}, "strict", z.ZodTypeAny, {
|
|
@@ -146,15 +165,15 @@ export declare const answers: {
|
|
|
146
165
|
}, "strict", z.ZodTypeAny, {
|
|
147
166
|
settingsBranch: string | null;
|
|
148
167
|
namespace: string;
|
|
149
|
-
legacy: string[];
|
|
150
168
|
configured: string[];
|
|
151
169
|
githubDefault: string | null;
|
|
170
|
+
legacy: string[];
|
|
152
171
|
}, {
|
|
153
172
|
settingsBranch: string | null;
|
|
154
173
|
namespace: string;
|
|
155
|
-
legacy: string[];
|
|
156
174
|
configured: string[];
|
|
157
175
|
githubDefault: string | null;
|
|
176
|
+
legacy: string[];
|
|
158
177
|
}>;
|
|
159
178
|
}, "strict", z.ZodTypeAny, {
|
|
160
179
|
ready: boolean;
|
|
@@ -164,9 +183,9 @@ export declare const answers: {
|
|
|
164
183
|
branchPolicy: {
|
|
165
184
|
settingsBranch: string | null;
|
|
166
185
|
namespace: string;
|
|
167
|
-
legacy: string[];
|
|
168
186
|
configured: string[];
|
|
169
187
|
githubDefault: string | null;
|
|
188
|
+
legacy: string[];
|
|
170
189
|
};
|
|
171
190
|
}, {
|
|
172
191
|
ready: boolean;
|
|
@@ -176,9 +195,9 @@ export declare const answers: {
|
|
|
176
195
|
branchPolicy: {
|
|
177
196
|
settingsBranch: string | null;
|
|
178
197
|
namespace: string;
|
|
179
|
-
legacy: string[];
|
|
180
198
|
configured: string[];
|
|
181
199
|
githubDefault: string | null;
|
|
200
|
+
legacy: string[];
|
|
182
201
|
};
|
|
183
202
|
}>;
|
|
184
203
|
readonly defaultBranch: z.ZodObject<{
|
|
@@ -249,6 +268,7 @@ export declare const answers: {
|
|
|
249
268
|
url: string;
|
|
250
269
|
}>;
|
|
251
270
|
readonly convertPullRequestToDraft: z.ZodObject<{}, "strict", z.ZodTypeAny, {}, {}>;
|
|
271
|
+
readonly setConflictsStatus: z.ZodObject<{}, "strict", z.ZodTypeAny, {}, {}>;
|
|
252
272
|
readonly deleteBranch: z.ZodObject<{}, "strict", z.ZodTypeAny, {}, {}>;
|
|
253
273
|
readonly authSnapshot: z.ZodObject<{
|
|
254
274
|
users: z.ZodArray<z.ZodObject<{
|
|
@@ -432,24 +452,24 @@ export declare function readAnswerBody(json: unknown): {
|
|
|
432
452
|
declare const errorEnvelope: z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
|
|
433
453
|
kind: z.ZodLiteral<"refusal">;
|
|
434
454
|
push: z.ZodBoolean;
|
|
435
|
-
operation: z.ZodEnum<["push", "delete", "pull-request"]>;
|
|
455
|
+
operation: z.ZodEnum<["push", "delete", "pull-request", "commit-status"]>;
|
|
436
456
|
branch: z.ZodString;
|
|
437
|
-
reason: z.ZodEnum<["invalid", "protected", "settings", "other-settings", "outside-namespace", "not-a-commit", "too-long"]>;
|
|
457
|
+
reason: z.ZodEnum<["invalid", "protected", "settings", "other-settings", "outside-namespace", "not-a-commit", "too-long", "not-head"]>;
|
|
438
458
|
message: z.ZodString;
|
|
439
459
|
}, "strict", z.ZodTypeAny, {
|
|
440
460
|
branch: string;
|
|
441
461
|
push: boolean;
|
|
442
462
|
message: string;
|
|
443
|
-
reason: "settings" | "protected" | "invalid" | "other-settings" | "outside-namespace" | "not-a-commit" | "too-long";
|
|
463
|
+
reason: "settings" | "protected" | "invalid" | "other-settings" | "outside-namespace" | "not-a-commit" | "too-long" | "not-head";
|
|
444
464
|
kind: "refusal";
|
|
445
|
-
operation: "push" | "delete" | "pull-request";
|
|
465
|
+
operation: "push" | "delete" | "pull-request" | "commit-status";
|
|
446
466
|
}, {
|
|
447
467
|
branch: string;
|
|
448
468
|
push: boolean;
|
|
449
469
|
message: string;
|
|
450
|
-
reason: "settings" | "protected" | "invalid" | "other-settings" | "outside-namespace" | "not-a-commit" | "too-long";
|
|
470
|
+
reason: "settings" | "protected" | "invalid" | "other-settings" | "outside-namespace" | "not-a-commit" | "too-long" | "not-head";
|
|
451
471
|
kind: "refusal";
|
|
452
|
-
operation: "push" | "delete" | "pull-request";
|
|
472
|
+
operation: "push" | "delete" | "pull-request" | "commit-status";
|
|
453
473
|
}>, z.ZodObject<{
|
|
454
474
|
kind: z.ZodLiteral<"push">;
|
|
455
475
|
pushKind: z.ZodEnum<["rejected", "rejected-after-stale-lease", "seen-commit-gone"]>;
|
|
@@ -77,6 +77,15 @@ export const requests = {
|
|
|
77
77
|
})
|
|
78
78
|
.strict(),
|
|
79
79
|
convertPullRequestToDraft: z.object({ number: prNumber }).strict(),
|
|
80
|
+
setConflictsStatus: z
|
|
81
|
+
.object({
|
|
82
|
+
pullRequest: prNumber,
|
|
83
|
+
sha: objectId,
|
|
84
|
+
state: z.enum(['success', 'failure']),
|
|
85
|
+
description: z.string().max(4096),
|
|
86
|
+
targetUrl: z.string().max(4096).optional(),
|
|
87
|
+
})
|
|
88
|
+
.strict(),
|
|
80
89
|
deleteBranch: z.object({ branch: name }).strict(),
|
|
81
90
|
authSnapshot: z.object({}).strict(),
|
|
82
91
|
termination: z.object({}).strict(),
|
|
@@ -121,6 +130,7 @@ export const answers = {
|
|
|
121
130
|
.strict(),
|
|
122
131
|
createOrUpdatePullRequest: pullRequestRef.extend({ created: z.boolean() }).strict(),
|
|
123
132
|
convertPullRequestToDraft: empty,
|
|
133
|
+
setConflictsStatus: empty,
|
|
124
134
|
deleteBranch: empty,
|
|
125
135
|
authSnapshot: z
|
|
126
136
|
.object({
|
|
@@ -168,6 +178,7 @@ export const JSON_ROUTES = {
|
|
|
168
178
|
getPullRequest: `${WIRE_PREFIX}/pull-request/get`,
|
|
169
179
|
createOrUpdatePullRequest: `${WIRE_PREFIX}/pull-request/create-or-update`,
|
|
170
180
|
convertPullRequestToDraft: `${WIRE_PREFIX}/pull-request/convert-to-draft`,
|
|
181
|
+
setConflictsStatus: `${WIRE_PREFIX}/pull-request/conflicts-status`,
|
|
171
182
|
deleteBranch: `${WIRE_PREFIX}/branch/delete`,
|
|
172
183
|
authSnapshot: `${WIRE_PREFIX}/auth-snapshot`,
|
|
173
184
|
termination: `${WIRE_PREFIX}/termination`,
|
|
@@ -240,7 +251,7 @@ const errorEnvelope = z.discriminatedUnion('kind', [
|
|
|
240
251
|
.object({
|
|
241
252
|
kind: z.literal('refusal'),
|
|
242
253
|
push: z.boolean(),
|
|
243
|
-
operation: z.enum(['push', 'delete', 'pull-request']),
|
|
254
|
+
operation: z.enum(['push', 'delete', 'pull-request', 'commit-status']),
|
|
244
255
|
branch: name,
|
|
245
256
|
reason: z.enum([
|
|
246
257
|
'invalid',
|
|
@@ -250,6 +261,7 @@ const errorEnvelope = z.discriminatedUnion('kind', [
|
|
|
250
261
|
'outside-namespace',
|
|
251
262
|
'not-a-commit',
|
|
252
263
|
'too-long',
|
|
264
|
+
'not-head',
|
|
253
265
|
]),
|
|
254
266
|
message,
|
|
255
267
|
})
|
|
@@ -461,7 +473,7 @@ export function decodeError(value) {
|
|
|
461
473
|
const { push, operation, branch, reason } = envelope;
|
|
462
474
|
if (!push)
|
|
463
475
|
return new GatewayRefusalError(operation, branch, reason, envelope.message);
|
|
464
|
-
if (operation !== 'push' || reason === 'too-long') {
|
|
476
|
+
if (operation !== 'push' || reason === 'too-long' || reason === 'not-head') {
|
|
465
477
|
return new GitHubGatewayProtocolError(`The GitHub gateway answered with a push refusal for ${operation}, reason ${reason}`);
|
|
466
478
|
}
|
|
467
479
|
const err = new RefusedPushError(branch, reason);
|
|
@@ -89,6 +89,16 @@ export type GitHubPullRequest = Pick<RestEndpointMethodTypes['pulls']['get']['re
|
|
|
89
89
|
/** The pull request's head commit, read with `mergeable_state`. */
|
|
90
90
|
headSha?: string;
|
|
91
91
|
};
|
|
92
|
+
/** A "CanopyCMS conflicts" status for the head of pull request `pullRequest`. */
|
|
93
|
+
export interface GitHubConflictsStatus {
|
|
94
|
+
pullRequest: number;
|
|
95
|
+
/** The pull request's head commit; the gateway refuses any other. */
|
|
96
|
+
sha: string;
|
|
97
|
+
state: 'success' | 'failure';
|
|
98
|
+
description: string;
|
|
99
|
+
/** The editor, opened on the branch. */
|
|
100
|
+
targetUrl?: string;
|
|
101
|
+
}
|
|
92
102
|
export type GitHubCreateOrUpdatePullRequest = Omit<CreateOrUpdatePullRequestParams, 'octokit' | 'owner' | 'repo' | 'signal'>;
|
|
93
103
|
/**
|
|
94
104
|
* Everything the worker does with GitHub, and the only place the credential and Octokit are used.
|
|
@@ -166,6 +176,13 @@ export interface GitHubGateway {
|
|
|
166
176
|
created: boolean;
|
|
167
177
|
}>;
|
|
168
178
|
convertPullRequestToDraft(number: number, signal?: AbortSignal): Promise<void>;
|
|
179
|
+
/**
|
|
180
|
+
* Set the {@link CONFLICTS_STATUS_CONTEXT} status on a pull request's head. Refused
|
|
181
|
+
* (`GatewayRefusalError`) with reason `not-head` unless `sha` is the head GitHub holds now, as
|
|
182
|
+
* `updatePullRequest` refuses a pull request it would not update, and for a description or link
|
|
183
|
+
* `assertCommitStatusText` refuses (worker/branch-policy.ts).
|
|
184
|
+
*/
|
|
185
|
+
setConflictsStatus(status: GitHubConflictsStatus, signal?: AbortSignal): Promise<void>;
|
|
169
186
|
deleteBranch(branch: string, signal?: AbortSignal): Promise<void>;
|
|
170
187
|
/**
|
|
171
188
|
* The auth provider's users, groups and memberships, which the worker writes to the auth cache:
|