canopycms 0.0.68-int.111 → 0.0.68-int.113

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 (41) hide show
  1. package/dist/branch-health.js +14 -9
  2. package/dist/cli/cli.js +4 -0
  3. package/dist/cli/generate-ai-content.js +2 -0
  4. package/dist/editor/Editor.js +2 -2
  5. package/dist/editor/components/StagedChangesDrawer.d.ts +37 -0
  6. package/dist/editor/components/StagedChangesDrawer.js +75 -0
  7. package/dist/editor/group-manager/GroupCard.d.ts +2 -0
  8. package/dist/editor/group-manager/GroupCard.js +2 -2
  9. package/dist/editor/group-manager/GroupForm.js +1 -1
  10. package/dist/editor/group-manager/InternalGroupsTab.d.ts +1 -0
  11. package/dist/editor/group-manager/InternalGroupsTab.js +2 -2
  12. package/dist/editor/group-manager/hooks/useGroupState.d.ts +2 -0
  13. package/dist/editor/group-manager/hooks/useGroupState.js +18 -1
  14. package/dist/editor/group-manager/index.js +9 -4
  15. package/dist/editor/group-manager/types.d.ts +3 -1
  16. package/dist/editor/hooks/usePermissionManager.js +4 -3
  17. package/dist/editor/permission-manager/index.js +9 -3
  18. package/dist/editor/permission-manager/types.d.ts +3 -1
  19. package/dist/git-manager.d.ts +10 -0
  20. package/dist/git-manager.js +6 -5
  21. package/dist/utils/error.js +6 -0
  22. package/dist/version.d.ts +1 -1
  23. package/dist/version.js +1 -1
  24. package/dist/worker/cms-worker.d.ts +39 -124
  25. package/dist/worker/cms-worker.js +232 -259
  26. package/dist/worker/git-sync.d.ts +7 -5
  27. package/dist/worker/git-sync.js +32 -29
  28. package/dist/worker/github-auth.d.ts +7 -7
  29. package/dist/worker/github-auth.js +12 -14
  30. package/dist/worker/github-gateway.d.ts +144 -0
  31. package/dist/worker/github-gateway.js +370 -0
  32. package/dist/worker/github-mirror.d.ts +84 -10
  33. package/dist/worker/github-mirror.js +237 -21
  34. package/dist/worker/rebase.d.ts +1 -1
  35. package/dist/worker/rebase.js +1 -6
  36. package/dist/worker/shared-repo-git.d.ts +6 -0
  37. package/dist/worker/shared-repo-git.js +8 -0
  38. package/dist/worker/task-runner.d.ts +2 -2
  39. package/dist/worker/task-runner.js +77 -127
  40. package/dist/worker/worker-context.d.ts +12 -40
  41. package/package.json +1 -1
@@ -24,18 +24,20 @@ import type { WorkerContext } from './worker-context.js';
24
24
  * `remote.git`'s `refs/heads/*` toward what the fetch above put in the tracking
25
25
  * namespace; reorder them and every branch rebases onto the PREVIOUS cycle's
26
26
  * base tip -- not corrupting, but silently a cycle behind.
27
- * (`pushSettingsBranches` consuming `trackedNames` is a DATA dependency its
27
+ * (`pushSettingsBranches` consuming `trackedTips` is a DATA dependency its
28
28
  * signature already enforces; `refreshBaseBranchWorkspace` and the two sweeps
29
29
  * are order-independent.)
30
30
  *
31
31
  * The whole cycle is wrapped so both outcomes record a worker-status.json
32
32
  * snapshot, since an operator's only view of this loop is the admin panel.
33
33
  */
34
- export type GitSyncContext = Pick<WorkerContext, 'baseBranch' | 'sanitizedBaseBranch' | 'contentBranchesPath' | 'remoteGitPath' | 'schemaHoldMaxMs' | 'taskDir' | 'taskTimeoutMs' | 'log' | 'buildGitHubUrl' | 'githubMirror' | 'ensureSettingsBranch' | 'ensureStatusReport' | 'isRunning' | 'isDraining' | 'shutdownSignal'> & RebaseContext;
34
+ export type GitSyncContext = Pick<WorkerContext, 'baseBranch' | 'sanitizedBaseBranch' | 'contentBranchesPath' | 'remoteGitPath' | 'schemaHoldMaxMs' | 'taskDir' | 'taskTimeoutMs' | 'log' | 'github' | 'ensureSettingsBranch' | 'ensureStatusReport' | 'isRunning' | 'isDraining' | 'shutdownSignal'> & RebaseContext;
35
35
  /**
36
36
  * Push THIS deployment's own settings branch (`ensureSettingsBranch()`) from
37
- * remote.git to GitHub. Non-fatal: a no-op push for an up-to-date branch just
38
- * succeeds quietly.
37
+ * remote.git to GitHub. Non-fatal. Skipped when `trackedTips`, GitHub's tips
38
+ * as fetched this cycle, already has the local head, so an unchanged branch
39
+ * costs no GitHub round trip; every push that does happen still passes the
40
+ * refusal checks in `MirrorSession.pushToGitHub`.
39
41
  *
40
42
  * Narrowed to that ONE branch, never every local `canopycms-settings-*`:
41
43
  * `reconcileTrackedBranches` creates local heads for branches that exist on
@@ -43,7 +45,7 @@ export type GitSyncContext = Pick<WorkerContext, 'baseBranch' | 'sanitizedBaseBr
43
45
  * can legitimately show up as a local head here. Pushing it would be this
44
46
  * deployment shipping settings state it does not own.
45
47
  */
46
- export declare function pushSettingsBranches(ctx: GitSyncContext, git: ReturnType<typeof simpleGit>, trackedNames: ReadonlySet<string>): Promise<void>;
48
+ export declare function pushSettingsBranches(ctx: GitSyncContext, git: ReturnType<typeof simpleGit>, trackedTips: ReadonlyMap<string, string>): Promise<void>;
47
49
  export declare function syncGit(ctx: GitSyncContext): Promise<void>;
48
50
  /**
49
51
  * Repair what killed provisioning and deletes left under the branches root: quarantine residue
@@ -14,6 +14,7 @@ import { CANOPY_META_DIR, isCanopyInternalPath, isNonFastForwardRejection } from
14
14
  import { MAX_REPORTED_PATHS, TRACKED_CANOPY_STATE_FIX, isUntracked, listTrackedCanopyState, restoreRetiredSchemaCache, splitByUpstreamTracking, untrackInIndex, } from './canopy-state.js';
15
15
  import { hasPendingHistoryRewrite } from './history-rewrite.js';
16
16
  import { runRebaseCycle } from './rebase.js';
17
+ import { describeMove } from './github-gateway.js';
17
18
  import { cleanupOldTasks } from '../task-queue/cms-task-queue.js';
18
19
  import { writeWorkerStatus } from '../task-queue/worker-status.js';
19
20
  import { workerLog, workerLogError, workerLogWarn } from './log.js';
@@ -30,8 +31,10 @@ import { reapplySparseCones } from './sparse-cone.js';
30
31
  const TRASH_RETENTION_MS = 30 * 24 * 60 * 60_000;
31
32
  /**
32
33
  * Push THIS deployment's own settings branch (`ensureSettingsBranch()`) from
33
- * remote.git to GitHub. Non-fatal: a no-op push for an up-to-date branch just
34
- * succeeds quietly.
34
+ * remote.git to GitHub. Non-fatal. Skipped when `trackedTips`, GitHub's tips
35
+ * as fetched this cycle, already has the local head, so an unchanged branch
36
+ * costs no GitHub round trip; every push that does happen still passes the
37
+ * refusal checks in `MirrorSession.pushToGitHub`.
35
38
  *
36
39
  * Narrowed to that ONE branch, never every local `canopycms-settings-*`:
37
40
  * `reconcileTrackedBranches` creates local heads for branches that exist on
@@ -39,7 +42,7 @@ const TRASH_RETENTION_MS = 30 * 24 * 60 * 60_000;
39
42
  * can legitimately show up as a local head here. Pushing it would be this
40
43
  * deployment shipping settings state it does not own.
41
44
  */
42
- export async function pushSettingsBranches(ctx, git, trackedNames) {
45
+ export async function pushSettingsBranches(ctx, git, trackedTips) {
43
46
  try {
44
47
  const settingsBranch = ctx.ensureSettingsBranch();
45
48
  const branches = await git.branch();
@@ -65,7 +68,7 @@ export async function pushSettingsBranches(ctx, git, trackedNames) {
65
68
  // With this deployment's own branch also missing, the API and this worker
66
69
  // have resolved different deploymentNames, and every settings change the
67
70
  // API commits is stranded in remote.git forever.
68
- const strandedLocal = foreign.filter((b) => !trackedNames.has(b));
71
+ const strandedLocal = foreign.filter((b) => !trackedTips.has(b));
69
72
  if (strandedLocal.length > 0) {
70
73
  workerLogWarn(ownBranchMissing
71
74
  ? `Settings branch mismatch: this worker owns "${settingsBranch}", which does not ` +
@@ -83,18 +86,24 @@ export async function pushSettingsBranches(ctx, git, trackedNames) {
83
86
  return;
84
87
  }
85
88
  try {
86
- // `git` is remote.git, read for the commit to send; the push itself runs in the
87
- // worker's private mirror, which alone ever sees the credential.
89
+ // `git` is remote.git, read for the commit to send; the push itself goes through the
90
+ // GitHub gateway, which alone ever sees the credential.
88
91
  const sha = (await git.revparse(['--verify', `refs/heads/${settingsBranch}`])).trim();
89
- const githubUrl = await ctx.buildGitHubUrl();
90
- await ctx
91
- .githubMirror()
92
- .exclusive((mirror) => mirror.pushToGitHub(githubUrl, settingsBranch, sha, { signal: ctx.shutdownSignal() }));
93
- workerLog(`Pushed settings branch ${settingsBranch} to GitHub`);
92
+ const outcome = trackedTips.get(settingsBranch) === sha
93
+ ? null
94
+ : await ctx
95
+ .github()
96
+ .push({ branch: settingsBranch, sha, protectedBranches: [ctx.baseBranch] }, ctx.shutdownSignal());
97
+ if (outcome?.moved) {
98
+ workerLog(`Pushed settings branch ${settingsBranch} to GitHub (${describeMove(outcome)})`);
99
+ }
100
+ else {
101
+ ctx.log.debug(`Settings branch ${settingsBranch} already up to date on GitHub`);
102
+ }
94
103
  }
95
104
  catch (err) {
96
- // Non-fatal: the branch may already be up to date, and this call site has
97
- // no task to throw a PermanentTaskError into (unlike pushBranchToGitHub).
105
+ // Non-fatal: this call site has no task to throw a PermanentTaskError
106
+ // into (unlike pushBranchToGitHub).
98
107
  // A non-fast-forward rejection here still gets its own wording: it means
99
108
  // another deployment's worker already pushed ITS OWN state to this
100
109
  // settings-branch name on GitHub -- an actual collision, not just the
@@ -280,15 +289,15 @@ async function reconcileTrackedBranches(ctx, git) {
280
289
  if (rewritten.length > 0) {
281
290
  workerLog(` Tracked-branch reconcile: ${rewritten.length} branch(es) rebased locally with the GitHub push still pending: ${rewritten.join(', ')}`);
282
291
  }
283
- // trackedNames is returned alongside the summary rather than folded into it:
284
- // it is a working set for pushSettingsBranches' stranded-branch check, and
292
+ // trackedTips is returned alongside the summary rather than folded into it:
293
+ // it is a working set for pushSettingsBranches, and
285
294
  // listing every branch on GitHub would bloat worker-status.json for no reader.
286
295
  // A base GitHub no longer has can be held for nothing.
287
296
  if (!tracked.has(ctx.baseBranch))
288
297
  baseHold = { hold: undefined };
289
298
  return {
290
299
  summary: { created, fastForwarded, ahead, diverged, rewritten },
291
- trackedNames: new Set(tracked.keys()),
300
+ trackedTips: tracked,
292
301
  baseHold,
293
302
  };
294
303
  }
@@ -339,7 +348,7 @@ export async function syncGit(ctx) {
339
348
  workerLogWarn(`remote.git maintenance failed: ${getErrorMessage(err)}`);
340
349
  }
341
350
  try {
342
- await ctx.githubMirror().maintain();
351
+ await ctx.github().maintain();
343
352
  }
344
353
  catch (err) {
345
354
  workerLogWarn(`GitHub mirror maintenance failed: ${getErrorMessage(err)}`);
@@ -350,13 +359,9 @@ export async function syncGit(ctx) {
350
359
  // refs/heads/* -- see that constant's doc comment for the destructive-fetch bug this avoids.
351
360
  // Both are killed at the drain deadline; a killed ref update leaves the ref as it was or
352
361
  // fully moved.
353
- const githubUrl = await ctx.buildGitHubUrl();
354
- await ctx.githubMirror().exclusive(async (mirror) => {
355
- await mirror.fetchFromGitHub(githubUrl, ctx.shutdownSignal());
356
- await mirror.publishTrackingRefs(ctx.shutdownSignal());
357
- });
362
+ await ctx.github().fetch({ have: [] }, ctx.shutdownSignal());
358
363
  workerLog('Fetched from GitHub');
359
- const { summary: trackedSummary, trackedNames, baseHold, } = await reconcileTrackedBranches(ctx, git);
364
+ const { summary: trackedSummary, trackedTips, baseHold, } = await reconcileTrackedBranches(ctx, git);
360
365
  // Recorded at once, so a step below that throws still persists the hold through the catch.
361
366
  if (baseHold) {
362
367
  const report = ctx.ensureStatusReport();
@@ -366,11 +371,9 @@ export async function syncGit(ctx) {
366
371
  delete report.baseHold;
367
372
  }
368
373
  // Push settings branches to GitHub (belt-and-suspenders for task queue).
369
- // Ensures settings reach GitHub even if a task queue entry is lost.
370
- // Ordering relative to the fetch/reconcile above is no longer a
371
- // correctness dependency now that the fetch can't clobber refs/heads/*
372
- // -- this could run before or after them just as safely.
373
- await pushSettingsBranches(ctx, git, trackedNames);
374
+ // Ensures settings reach GitHub even if a task queue entry is lost. After
375
+ // the fetch, whose tips (`trackedTips`) let it skip a branch GitHub holds.
376
+ await pushSettingsBranches(ctx, git, trackedTips);
374
377
  if (stoppedForDrain(ctx, 'the base-branch refresh'))
375
378
  return;
376
379
  const baseRefresh = await refreshBaseBranchWorkspace(ctx);
@@ -410,7 +413,7 @@ export async function syncGit(ctx) {
410
413
  const report = ctx.ensureStatusReport();
411
414
  // [REDACT] Persisted to worker-status.json and served to the browser
412
415
  // by the admin panel -- a fetch/push failure's message can embed the
413
- // bot token via buildGitHubUrl().
416
+ // bot token from the gateway's git URL.
414
417
  report.lastGitSyncError = {
415
418
  message: redactCredentials(getErrorMessage(err)),
416
419
  at: new Date().toISOString(),
@@ -1,8 +1,8 @@
1
1
  import type { CanopyOctokitAuthOptions, OctokitAuthStrategyOptions } from '../github-service.js';
2
2
  /**
3
3
  * How the worker authenticates to GitHub, for both halves of its access:
4
- * Octokit (the REST API) and git-over-HTTPS (push/fetch, which carries the
5
- * credential in the remote URL).
4
+ * Octokit (the REST API) and git-over-HTTPS (push/fetch, which the gateway
5
+ * hands the credential in a per-command config file).
6
6
  *
7
7
  * Two shapes are supported, and they are equals — nothing here deprecates,
8
8
  * warns on, or nudges away from the token.
@@ -18,7 +18,7 @@ import type { CanopyOctokitAuthOptions, OctokitAuthStrategyOptions } from '../gi
18
18
  * a year, acts as the person who created it, and dies when they leave the
19
19
  * organisation. The cost is that its installation tokens last about an
20
20
  * hour, so the credential must be minted on demand rather than read once at
21
- * boot — which is why `CmsWorker.buildGitHubUrl()` is async.
21
+ * boot — which is why the gateway resolves it per operation (github-gateway.ts).
22
22
  */
23
23
  export interface GitHubAuthConfig {
24
24
  /**
@@ -68,8 +68,8 @@ export interface GitHubAuthConfig {
68
68
  *
69
69
  * Core's own backstop, not a substitute for one the provider keeps: a
70
70
  * failing publish retries on a 5s/10s/20s backoff (task-queue.ts), and
71
- * `CmsWorker` calls `refreshCredential` after every failed task attempt AND
72
- * every failed git sync, so a burst of failures would otherwise reach an
71
+ * the gateway calls `refreshCredential` after every operation that failed
72
+ * reaching GitHub other than with a 422, so a burst of failures would otherwise reach an
73
73
  * adopter-supplied `refreshGitHubToken` every few seconds. The AWS provider
74
74
  * (`packages/canopycms-cdk/worker/credential-refresh.ts`) already enforces
75
75
  * its own five-minute floor; an adopter's own provider has none unless they
@@ -165,7 +165,7 @@ export interface ResolvedGitHubAuth {
165
165
  * `refreshGitHubTokenMinIntervalMs`, and swaps the result in.
166
166
  *
167
167
  * Nothing needs rebuilding afterwards, on either path. Both consumers read
168
- * the credential per use — `resolveGitToken` on every `buildGitHubUrl()`,
168
+ * the credential per use — `resolveGitToken` for every GitHub-bound git operation,
169
169
  * and Octokit through a strategy hook that reads it per request — so a
170
170
  * swapped token is live at the next use with no cache to invalidate.
171
171
  *
@@ -179,7 +179,7 @@ export interface ResolvedGitHubAuth {
179
179
  /**
180
180
  * Choose how this worker authenticates, ONCE, from its config.
181
181
  *
182
- * Called from `CmsWorker.ensureGitHubAuth()`, which start() invokes inside
182
+ * Called where `CmsWorker` creates its GitHub gateway, which start() does inside
183
183
  * its try — NOT from the constructor, so that a half-configured worker still
184
184
  * constructs and the throw lands where start()'s catch can record it in
185
185
  * worker-status.json. Resolved once, so both consumers (Octokit and the git
@@ -38,7 +38,7 @@ function dynamicTokenAuth(getToken) {
38
38
  /**
39
39
  * Choose how this worker authenticates, ONCE, from its config.
40
40
  *
41
- * Called from `CmsWorker.ensureGitHubAuth()`, which start() invokes inside
41
+ * Called where `CmsWorker` creates its GitHub gateway, which start() does inside
42
42
  * its try — NOT from the constructor, so that a half-configured worker still
43
43
  * constructs and the throw lands where start()'s catch can record it in
44
44
  * worker-status.json. Resolved once, so both consumers (Octokit and the git
@@ -125,10 +125,10 @@ export function resolveWorkerGitHubAuth(config) {
125
125
  // Stamped BEFORE the await, not after, for the same reason
126
126
  // credential-refresh.ts's does: stamping after lets two overlapping
127
127
  // calls each see an unstamped clock and both reach the provider, which
128
- // is the floor not holding. Overlap is real here -- the task loop and
129
- // the git-sync loop both call `refreshCredential`, on separate loops
130
- // `scheduleLoop` does not serialise against each other -- so this is
131
- // what collapses them into one provider call. A provider that THROWS
128
+ // is the floor not holding. Overlap is real here -- the gateway stops
129
+ // waiting for a re-read after its timeout without cancelling it, and the
130
+ // next failure arms another -- so this is what collapses them into one
131
+ // provider call. A provider that THROWS
132
132
  // still counts as reached: the stamp already landed by the time the
133
133
  // rejection surfaces, so a failing provider is bounded by the floor
134
134
  // too, and this call is not counted in `refreshesStarted` below unless
@@ -137,16 +137,15 @@ export function resolveWorkerGitHubAuth(config) {
137
137
  const refresh = ++refreshesStarted;
138
138
  const refreshed = await config.refreshGitHubToken();
139
139
  // Falsy covers both "nothing rotated" (`undefined`) and an empty secret:
140
- // an empty token would build `https://x-access-token:@github.com/…`,
141
- // which git sends anonymously, so keeping the known-bad-but-real token
140
+ // an empty token would send an empty basic credential, which GitHub
141
+ // treats as anonymous, so keeping the known-bad-but-real token
142
142
  // fails more legibly than replacing it with nothing.
143
143
  if (!refreshed)
144
144
  return;
145
145
  // A refresh that STARTED before one already applied read the store
146
- // earlier, so its value can only be older. Refreshes do overlap: the task
147
- // loop and the git-sync loop both call this, and
148
- // CmsWorker.refreshGitHubCredential stops waiting after `taskTimeoutMs`
149
- // without cancelling. Only a refresh that returned a value counts as
146
+ // earlier, so its value can only be older. Refreshes do overlap: the
147
+ // gateway stops waiting for one after `taskTimeoutMs` without cancelling
148
+ // it, and may start the next. Only a refresh that returned a value counts as
150
149
  // applied -- one that returned `undefined` (a provider's floor, say) says
151
150
  // nothing about how recent it is, so it must not block a slower real read.
152
151
  if (refresh < newestRefreshApplied)
@@ -219,9 +218,8 @@ async function mintInstallationToken(app, timeoutMs) {
219
218
  });
220
219
  const minted = await Promise.race([minting, timedOut]);
221
220
  if (!minted) {
222
- // An empty token would build `https://x-access-token:@github.com/…`, which
223
- // git sends as an anonymous request and GitHub answers with a 403 that
224
- // says nothing about the credential.
221
+ // An empty token would send an empty basic credential, which GitHub
222
+ // answers as anonymous, with a 403 that says nothing about the credential.
225
223
  throw new Error('GitHub App authentication returned an empty installation token');
226
224
  }
227
225
  return minted;
@@ -0,0 +1,144 @@
1
+ import type { Octokit, RestEndpointMethodTypes } from '@octokit/rest';
2
+ import type { BareRemoteRepackResult } from '../git-manager.js';
3
+ import { type CreateOrUpdatePullRequestParams } from '../github-service.js';
4
+ import { type ResolvedGitHubAuth } from './github-auth.js';
5
+ import { type GitHubRefUpdate } from './github-mirror.js';
6
+ /** What a {@link GitHubGateway.fetch} brought back. */
7
+ interface GitHubFetchResult {
8
+ /** The objects the caller lacks, as a bundle to download; always null in-process. */
9
+ bundleId: string | null;
10
+ }
11
+ declare const inMirrorSession: unique symbol;
12
+ /**
13
+ * Which GitHub branches reached each of a list of commits, at the last fetch, asked inside a
14
+ * mirror session that is already held. Branded so the gateway itself, whose own `onGitHub` takes
15
+ * that session, cannot be passed in its place: the session is not re-entrant.
16
+ */
17
+ export interface GitHubReachability {
18
+ readonly [inMirrorSession]: true;
19
+ onGitHub(ids: readonly string[]): Promise<ReadonlySet<string>>;
20
+ }
21
+ interface GitHubPushRequest {
22
+ branch: string;
23
+ /** The commit to put at `refs/heads/<branch>` on GitHub, already in remote.git. */
24
+ sha: string;
25
+ /** The commit GitHub must still be at for the push to replace it (`--force-with-lease`). */
26
+ lease?: string;
27
+ /** Branches no push may name; GitHub's default branch is always added. */
28
+ protectedBranches: readonly string[];
29
+ }
30
+ /**
31
+ * What a push did to GitHub's ref, and whether it got there by the plain retry after GitHub
32
+ * refused a stale lease, which proves GitHub was not at the lease's commit.
33
+ */
34
+ export type GitHubPushOutcome = GitHubRefUpdate & {
35
+ pastStaleLease: boolean;
36
+ };
37
+ /**
38
+ * A push to GitHub failed: `rejected` on the first attempt, `rejected-after-stale-lease` on the
39
+ * plain retry after GitHub refused a stale lease. The message is the failure's own (git's output,
40
+ * for a refusal); `cause` is the error itself. A {@link RefusedPushError} from the first attempt is
41
+ * thrown as itself.
42
+ */
43
+ export declare class GitHubPushError extends Error {
44
+ readonly kind: 'rejected' | 'rejected-after-stale-lease';
45
+ readonly cause: unknown;
46
+ constructor(kind: 'rejected' | 'rejected-after-stale-lease', cause: unknown);
47
+ }
48
+ /** A moved ref for a log line: `<old7>..<new7>`, or `new branch at <new7>`. */
49
+ export declare function describeMove(update: Extract<GitHubRefUpdate, {
50
+ moved: true;
51
+ }>): string;
52
+ /** The fields of a pull request the worker reads. */
53
+ type GitHubPullRequest = Pick<RestEndpointMethodTypes['pulls']['get']['response']['data'], 'merged' | 'merged_at' | 'state' | 'node_id'>;
54
+ type GitHubCreateOrUpdatePullRequest = Omit<CreateOrUpdatePullRequestParams, 'octokit' | 'owner' | 'repo'>;
55
+ /**
56
+ * Everything the worker does with GitHub, and the only place the credential and Octokit are used.
57
+ * Octokit errors propagate unchanged (status, headers, response data), so the task runner's
58
+ * classifiers read them as they would Octokit's own. The gateway re-reads its own credential after
59
+ * a failure (`LocalGitHubGateway.credentialed`); the worker never asks it to.
60
+ */
61
+ export interface GitHubGateway {
62
+ /**
63
+ * At startup, before anything uses the credential: prove a GitHub App credential works, refuse
64
+ * a state directory the Lambda could write, and create the mirror.
65
+ */
66
+ prepare(): Promise<void>;
67
+ /** GitHub's default branch. */
68
+ defaultBranch(signal?: AbortSignal): Promise<string>;
69
+ /**
70
+ * Fetch every GitHub branch, and copy them into remote.git's tracking namespace in the same
71
+ * mirror session, pruning the ones GitHub no longer has. `have` is ignored.
72
+ */
73
+ fetch(request: {
74
+ have: readonly string[];
75
+ }, signal?: AbortSignal): Promise<GitHubFetchResult>;
76
+ /** Of `ids`, the ones a GitHub branch contained at the last fetch. */
77
+ onGitHub(ids: readonly string[]): Promise<ReadonlySet<string>>;
78
+ /**
79
+ * Seed the bare repository at `gitDir` with every GitHub branch, in one mirror session: fetch
80
+ * GitHub, run `beforeSeed` (whose throw stops the seed), require `baseBranch` on GitHub, run
81
+ * `createRepository` to make the empty repository at `gitDir`, then push into it. `beforeSeed`
82
+ * runs inside that session, so it asks GitHub only through the object it is given, never
83
+ * through the gateway.
84
+ */
85
+ seedBareRepository(gitDir: string, options: {
86
+ baseBranch: string;
87
+ beforeSeed?: (github: GitHubReachability) => Promise<void>;
88
+ createRepository: () => Promise<void>;
89
+ }): Promise<void>;
90
+ /**
91
+ * Push `sha` to GitHub's `refs/heads/<branch>`, resolving with whether GitHub's ref moved. A
92
+ * refused lease is retried once without one, which succeeds only as a fast-forward. Throws
93
+ * {@link RefusedPushError} for a push the worker never makes, {@link GitHubPushError} when the
94
+ * push fails, and anything else as is.
95
+ */
96
+ push(request: GitHubPushRequest, signal?: AbortSignal): Promise<GitHubPushOutcome>;
97
+ createPullRequest(request: {
98
+ head: string;
99
+ base: string;
100
+ title: string;
101
+ body: string;
102
+ }, signal?: AbortSignal): Promise<{
103
+ number: number;
104
+ url: string;
105
+ }>;
106
+ updatePullRequest(number: number, update: {
107
+ title?: string;
108
+ body?: string;
109
+ state?: 'closed';
110
+ }, signal?: AbortSignal): Promise<void>;
111
+ getPullRequest(number: number, signal?: AbortSignal): Promise<GitHubPullRequest>;
112
+ createOrUpdatePullRequest(request: GitHubCreateOrUpdatePullRequest): Promise<{
113
+ number: number;
114
+ url: string;
115
+ created: boolean;
116
+ }>;
117
+ convertPullRequestToDraft(number: number, signal?: AbortSignal): Promise<void>;
118
+ deleteBranch(branch: string, signal?: AbortSignal): Promise<void>;
119
+ /** Mirror upkeep. */
120
+ maintain(): Promise<BareRemoteRepackResult>;
121
+ }
122
+ export interface LocalGitHubGatewayOptions {
123
+ githubOwner: string;
124
+ githubRepo: string;
125
+ auth: ResolvedGitHubAuth;
126
+ /** Whether `auth` is a GitHub App's, which `prepare()` proves before anything uses it. */
127
+ githubApp: boolean;
128
+ /** Holds the mirror; see `CmsWorkerConfig.stateDirectory`. */
129
+ stateDirectory: string;
130
+ workspacePath: string;
131
+ remoteGitPath: string;
132
+ /** simple-git's inactivity timeout, and the bound on a credential re-read. */
133
+ timeoutMs: number;
134
+ /**
135
+ * Test seam: the repository standing in for GitHub, resolved once per operation. Default:
136
+ * `https://github.com/<owner>/<repo>.git`, which never carries the token.
137
+ */
138
+ remoteUrl?: string | (() => string | Promise<string>);
139
+ /** Test seam: the Octokit client. Default: built from `auth`. */
140
+ octokit?: Octokit;
141
+ }
142
+ /** The in-process {@link GitHubGateway}. */
143
+ export declare function createLocalGitHubGateway(options: LocalGitHubGatewayOptions): GitHubGateway;
144
+ export {};