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.
- package/dist/branch-health.js +14 -9
- package/dist/cli/cli.js +4 -0
- package/dist/cli/generate-ai-content.js +2 -0
- package/dist/editor/Editor.js +2 -2
- package/dist/editor/components/StagedChangesDrawer.d.ts +37 -0
- package/dist/editor/components/StagedChangesDrawer.js +75 -0
- package/dist/editor/group-manager/GroupCard.d.ts +2 -0
- package/dist/editor/group-manager/GroupCard.js +2 -2
- package/dist/editor/group-manager/GroupForm.js +1 -1
- package/dist/editor/group-manager/InternalGroupsTab.d.ts +1 -0
- package/dist/editor/group-manager/InternalGroupsTab.js +2 -2
- package/dist/editor/group-manager/hooks/useGroupState.d.ts +2 -0
- package/dist/editor/group-manager/hooks/useGroupState.js +18 -1
- package/dist/editor/group-manager/index.js +9 -4
- package/dist/editor/group-manager/types.d.ts +3 -1
- package/dist/editor/hooks/usePermissionManager.js +4 -3
- package/dist/editor/permission-manager/index.js +9 -3
- package/dist/editor/permission-manager/types.d.ts +3 -1
- package/dist/git-manager.d.ts +10 -0
- package/dist/git-manager.js +6 -5
- package/dist/utils/error.js +6 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/cms-worker.d.ts +39 -124
- package/dist/worker/cms-worker.js +232 -259
- package/dist/worker/git-sync.d.ts +7 -5
- package/dist/worker/git-sync.js +32 -29
- package/dist/worker/github-auth.d.ts +7 -7
- package/dist/worker/github-auth.js +12 -14
- package/dist/worker/github-gateway.d.ts +144 -0
- package/dist/worker/github-gateway.js +370 -0
- package/dist/worker/github-mirror.d.ts +84 -10
- package/dist/worker/github-mirror.js +237 -21
- package/dist/worker/rebase.d.ts +1 -1
- package/dist/worker/rebase.js +1 -6
- package/dist/worker/shared-repo-git.d.ts +6 -0
- package/dist/worker/shared-repo-git.js +8 -0
- package/dist/worker/task-runner.d.ts +2 -2
- package/dist/worker/task-runner.js +77 -127
- package/dist/worker/worker-context.d.ts +12 -40
- 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 `
|
|
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' | '
|
|
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
|
|
38
|
-
*
|
|
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>,
|
|
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
|
package/dist/worker/git-sync.js
CHANGED
|
@@ -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
|
|
34
|
-
*
|
|
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,
|
|
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) => !
|
|
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
|
|
87
|
-
//
|
|
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
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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:
|
|
97
|
-
//
|
|
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
|
-
//
|
|
284
|
-
// it is a working set for pushSettingsBranches
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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
|
-
//
|
|
371
|
-
|
|
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
|
|
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
|
|
5
|
-
* credential in
|
|
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
|
|
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
|
-
*
|
|
72
|
-
*
|
|
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`
|
|
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
|
|
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
|
|
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
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
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
|
|
141
|
-
//
|
|
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
|
|
147
|
-
//
|
|
148
|
-
//
|
|
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
|
|
223
|
-
//
|
|
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 {};
|