canopycms 0.7.1 → 0.8.1
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 +197 -47
- package/dist/api/admin-branch-health.js +47 -20
- package/dist/api/admin.d.ts +13 -1
- package/dist/api/admin.js +14 -24
- package/dist/api/branch-changes.d.ts +87 -0
- package/dist/api/branch-changes.js +269 -0
- package/dist/api/branch-entry-changes.d.ts +83 -0
- package/dist/api/branch-entry-changes.js +262 -0
- package/dist/api/branch-merge.js +2 -2
- package/dist/api/branch-review.js +35 -4
- package/dist/api/branch-status.js +14 -5
- package/dist/api/branch-withdraw.js +7 -0
- package/dist/api/branch.d.ts +13 -1
- package/dist/api/branch.js +171 -97
- package/dist/api/client.d.ts +15 -0
- package/dist/api/client.js +13 -1
- package/dist/api/content.d.ts +18 -18
- package/dist/api/content.js +1 -1
- package/dist/api/github-sync.js +7 -3
- package/dist/api/groups.d.ts +4 -4
- package/dist/api/pr-comment-sync.d.ts +9 -8
- package/dist/api/pr-comment-sync.js +23 -13
- package/dist/api/routes.js +4 -0
- package/dist/api/types.d.ts +3 -1
- package/dist/authorization/groups/schema.d.ts +4 -4
- package/dist/authorization/protected-branch.d.ts +3 -3
- package/dist/authorization/protected-branch.js +3 -3
- package/dist/branch-metadata-file.d.ts +84 -0
- package/dist/branch-metadata-file.js +9 -0
- package/dist/branch-metadata.d.ts +4 -4
- package/dist/branch-metadata.js +4 -5
- package/dist/branch-param.d.ts +24 -0
- package/dist/branch-param.js +87 -0
- package/dist/branch-provisioning.d.ts +6 -3
- package/dist/branch-provisioning.js +12 -10
- package/dist/branch-registry.d.ts +6 -0
- package/dist/branch-registry.js +12 -3
- package/dist/branch-schema-cache.d.ts +22 -2
- package/dist/branch-schema-cache.js +88 -12
- package/dist/branch-workspace.js +22 -15
- package/dist/cli/cli.js +1122 -181
- package/dist/cli/generate-ai-content.js +791 -109
- package/dist/cli/init.js +126 -0
- package/dist/comment-store.d.ts +3 -0
- package/dist/comment-store.js +4 -1
- package/dist/config/schemas/config.d.ts +3 -0
- package/dist/config/schemas/config.js +2 -0
- package/dist/config/types.d.ts +11 -0
- package/dist/content-id-index.d.ts +5 -0
- package/dist/content-id-index.js +1 -1
- package/dist/content-listing.d.ts +53 -0
- package/dist/content-listing.js +123 -27
- package/dist/content-reader.d.ts +24 -10
- package/dist/content-reader.js +66 -22
- package/dist/context.js +51 -48
- package/dist/editor/BranchManager.d.ts +7 -3
- package/dist/editor/BranchManager.js +51 -6
- package/dist/editor/Editor.js +166 -34
- package/dist/editor/EntryNavigator.d.ts +34 -10
- package/dist/editor/EntryNavigator.js +185 -29
- package/dist/editor/admin/SystemHealthPanel.js +36 -1
- package/dist/editor/admin/useSystemHealth.js +1 -0
- package/dist/editor/branch-names.d.ts +43 -0
- package/dist/editor/branch-names.js +75 -0
- package/dist/editor/components/BranchPill.d.ts +2 -2
- package/dist/editor/components/BranchPill.js +2 -2
- package/dist/editor/components/EditorHeader.d.ts +3 -1
- package/dist/editor/components/EditorHeader.js +2 -2
- package/dist/editor/components/EntryChangesDrawer.d.ts +51 -0
- package/dist/editor/components/EntryChangesDrawer.js +363 -0
- package/dist/editor/components/EntryChangesSkeleton.d.ts +9 -0
- package/dist/editor/components/EntryChangesSkeleton.js +7 -0
- package/dist/editor/components/EntryHeader.d.ts +9 -1
- package/dist/editor/components/EntryHeader.js +25 -3
- package/dist/editor/components/LazyEntryChangesDrawer.d.ts +16 -0
- package/dist/editor/components/LazyEntryChangesDrawer.js +45 -0
- package/dist/editor/components/ReviewDialogs.js +3 -3
- package/dist/editor/context/ApiClientContext.d.ts +9 -0
- package/dist/editor/context/ApiClientContext.js +26 -2
- package/dist/editor/context/index.d.ts +1 -1
- package/dist/editor/context/index.js +1 -1
- package/dist/editor/copy.d.ts +1 -0
- package/dist/editor/copy.js +1 -0
- package/dist/editor/editor-utils.d.ts +2 -0
- package/dist/editor/editor-utils.js +15 -0
- package/dist/editor/entry-diff.d.ts +143 -0
- package/dist/editor/entry-diff.js +593 -0
- package/dist/editor/hooks/create-branch-request.d.ts +11 -1
- package/dist/editor/hooks/create-branch-request.js +16 -6
- package/dist/editor/hooks/useBranchActions.d.ts +19 -2
- package/dist/editor/hooks/useBranchActions.js +29 -20
- package/dist/editor/hooks/useBranchChangesData.d.ts +57 -0
- package/dist/editor/hooks/useBranchChangesData.js +134 -0
- package/dist/editor/hooks/useBranchManager.d.ts +16 -0
- package/dist/editor/hooks/useBranchManager.js +142 -54
- package/dist/editor/hooks/useDraftManager.js +6 -2
- package/dist/editor/hooks/useEntryManager.d.ts +18 -2
- package/dist/editor/hooks/useEntryManager.js +36 -10
- package/dist/editor/utils/editor-errors.d.ts +5 -2
- package/dist/git-manager.d.ts +71 -0
- package/dist/git-manager.js +270 -1
- package/dist/http/handler.js +11 -9
- package/dist/operating-mode/branch-namespace-fixtures.d.ts +17 -0
- package/dist/operating-mode/branch-namespace-fixtures.js +65 -0
- package/dist/operating-mode/branch-namespace.d.ts +34 -0
- package/dist/operating-mode/branch-namespace.js +121 -0
- package/dist/operating-mode/client-unsafe-strategy.js +7 -0
- package/dist/operating-mode/types.d.ts +8 -0
- package/dist/paths/branch-name.d.ts +11 -19
- package/dist/paths/branch-name.js +17 -27
- package/dist/paths/index.d.ts +2 -2
- package/dist/paths/index.js +2 -2
- package/dist/paths/resolve.d.ts +0 -1
- package/dist/paths/resolve.js +0 -1
- package/dist/paths/validation.d.ts +40 -1
- package/dist/paths/validation.js +83 -4
- package/dist/pr-comments.d.ts +17 -9
- package/dist/pr-comments.js +53 -15
- package/dist/server.d.ts +6 -1
- package/dist/server.js +6 -1
- package/dist/services.js +2 -1
- package/dist/submission-attribution.d.ts +22 -0
- package/dist/submission-attribution.js +62 -6
- package/dist/task-queue/worker-status.d.ts +6 -1
- package/dist/task-queue/worker-status.js +23 -2
- package/dist/types.d.ts +59 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/branch-policy.d.ts +111 -16
- package/dist/worker/branch-policy.js +222 -17
- package/dist/worker/cms-worker.d.ts +24 -2
- package/dist/worker/cms-worker.js +35 -24
- package/dist/worker/git-sync.d.ts +3 -3
- package/dist/worker/git-sync.js +42 -1
- package/dist/worker/github-gateway-http.js +2 -2
- package/dist/worker/github-gateway-wire.d.ts +42 -27
- package/dist/worker/github-gateway-wire.js +14 -3
- package/dist/worker/github-gateway.d.ts +17 -6
- package/dist/worker/github-gateway.js +62 -6
- package/dist/worker/github-mirror.d.ts +7 -4
- package/dist/worker/github-mirror.js +27 -16
- package/dist/worker/history-rewrite.d.ts +23 -10
- package/dist/worker/history-rewrite.js +43 -20
- package/dist/worker/legacy-branch-fixtures.d.ts +21 -0
- package/dist/worker/legacy-branch-fixtures.js +71 -0
- package/dist/worker/rebase.d.ts +2 -1
- package/dist/worker/rebase.js +93 -36
- package/dist/worker/task-runner.d.ts +4 -1
- package/dist/worker/task-runner.js +77 -48
- package/dist/worker/unpublishable-branches.d.ts +34 -0
- package/dist/worker/unpublishable-branches.js +182 -0
- package/package.json +2 -1
|
@@ -131,34 +131,49 @@ export declare const answers: {
|
|
|
131
131
|
fatalError: z.ZodNullable<z.ZodString>;
|
|
132
132
|
preflight: z.ZodEnum<["not-an-app", "verified", "unverified", "pending"]>;
|
|
133
133
|
clerkConfigured: z.ZodBoolean;
|
|
134
|
-
|
|
134
|
+
branchPolicy: z.ZodObject<{
|
|
135
135
|
configured: z.ZodArray<z.ZodString, "many">;
|
|
136
136
|
githubDefault: z.ZodNullable<z.ZodString>;
|
|
137
|
+
namespace: z.ZodString;
|
|
138
|
+
legacy: z.ZodArray<z.ZodString, "many">;
|
|
139
|
+
settingsBranch: z.ZodNullable<z.ZodString>;
|
|
137
140
|
}, "strict", z.ZodTypeAny, {
|
|
141
|
+
settingsBranch: string | null;
|
|
142
|
+
namespace: string;
|
|
143
|
+
legacy: string[];
|
|
138
144
|
configured: string[];
|
|
139
145
|
githubDefault: string | null;
|
|
140
146
|
}, {
|
|
147
|
+
settingsBranch: string | null;
|
|
148
|
+
namespace: string;
|
|
149
|
+
legacy: string[];
|
|
141
150
|
configured: string[];
|
|
142
151
|
githubDefault: string | null;
|
|
143
152
|
}>;
|
|
144
153
|
}, "strict", z.ZodTypeAny, {
|
|
145
154
|
ready: boolean;
|
|
146
|
-
protectedBranches: {
|
|
147
|
-
configured: string[];
|
|
148
|
-
githubDefault: string | null;
|
|
149
|
-
};
|
|
150
155
|
preflight: "pending" | "not-an-app" | "verified" | "unverified";
|
|
151
156
|
fatalError: string | null;
|
|
152
157
|
clerkConfigured: boolean;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
158
|
+
branchPolicy: {
|
|
159
|
+
settingsBranch: string | null;
|
|
160
|
+
namespace: string;
|
|
161
|
+
legacy: string[];
|
|
156
162
|
configured: string[];
|
|
157
163
|
githubDefault: string | null;
|
|
158
164
|
};
|
|
165
|
+
}, {
|
|
166
|
+
ready: boolean;
|
|
159
167
|
preflight: "pending" | "not-an-app" | "verified" | "unverified";
|
|
160
168
|
fatalError: string | null;
|
|
161
169
|
clerkConfigured: boolean;
|
|
170
|
+
branchPolicy: {
|
|
171
|
+
settingsBranch: string | null;
|
|
172
|
+
namespace: string;
|
|
173
|
+
legacy: string[];
|
|
174
|
+
configured: string[];
|
|
175
|
+
githubDefault: string | null;
|
|
176
|
+
};
|
|
162
177
|
}>;
|
|
163
178
|
readonly defaultBranch: z.ZodObject<{
|
|
164
179
|
branch: z.ZodString;
|
|
@@ -227,13 +242,13 @@ export declare const answers: {
|
|
|
227
242
|
email: z.ZodString;
|
|
228
243
|
avatarUrl: z.ZodOptional<z.ZodString>;
|
|
229
244
|
}, "strict", z.ZodTypeAny, {
|
|
230
|
-
name: string;
|
|
231
245
|
id: string;
|
|
246
|
+
name: string;
|
|
232
247
|
email: string;
|
|
233
248
|
avatarUrl?: string | undefined;
|
|
234
249
|
}, {
|
|
235
|
-
name: string;
|
|
236
250
|
id: string;
|
|
251
|
+
name: string;
|
|
237
252
|
email: string;
|
|
238
253
|
avatarUrl?: string | undefined;
|
|
239
254
|
}>, "many">;
|
|
@@ -243,41 +258,41 @@ export declare const answers: {
|
|
|
243
258
|
description: z.ZodOptional<z.ZodString>;
|
|
244
259
|
memberCount: z.ZodOptional<z.ZodNumber>;
|
|
245
260
|
}, "strict", z.ZodTypeAny, {
|
|
246
|
-
name: string;
|
|
247
261
|
id: string;
|
|
262
|
+
name: string;
|
|
248
263
|
description?: string | undefined;
|
|
249
264
|
memberCount?: number | undefined;
|
|
250
265
|
}, {
|
|
251
|
-
name: string;
|
|
252
266
|
id: string;
|
|
267
|
+
name: string;
|
|
253
268
|
description?: string | undefined;
|
|
254
269
|
memberCount?: number | undefined;
|
|
255
270
|
}>, "many">;
|
|
256
271
|
memberships: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString, "many">>;
|
|
257
272
|
}, "strict", z.ZodTypeAny, {
|
|
258
273
|
groups: {
|
|
259
|
-
name: string;
|
|
260
274
|
id: string;
|
|
275
|
+
name: string;
|
|
261
276
|
description?: string | undefined;
|
|
262
277
|
memberCount?: number | undefined;
|
|
263
278
|
}[];
|
|
264
279
|
users: {
|
|
265
|
-
name: string;
|
|
266
280
|
id: string;
|
|
281
|
+
name: string;
|
|
267
282
|
email: string;
|
|
268
283
|
avatarUrl?: string | undefined;
|
|
269
284
|
}[];
|
|
270
285
|
memberships: Record<string, string[]>;
|
|
271
286
|
}, {
|
|
272
287
|
groups: {
|
|
273
|
-
name: string;
|
|
274
288
|
id: string;
|
|
289
|
+
name: string;
|
|
275
290
|
description?: string | undefined;
|
|
276
291
|
memberCount?: number | undefined;
|
|
277
292
|
}[];
|
|
278
293
|
users: {
|
|
279
|
-
name: string;
|
|
280
294
|
id: string;
|
|
295
|
+
name: string;
|
|
281
296
|
email: string;
|
|
282
297
|
avatarUrl?: string | undefined;
|
|
283
298
|
}[];
|
|
@@ -325,11 +340,11 @@ export declare const fetchPoll: z.ZodDiscriminatedUnion<"state", [z.ZodObject<{
|
|
|
325
340
|
state: z.ZodLiteral<"running">;
|
|
326
341
|
progress: z.ZodNumber;
|
|
327
342
|
}, "strict", z.ZodTypeAny, {
|
|
328
|
-
progress: number;
|
|
329
343
|
state: "running";
|
|
330
|
-
}, {
|
|
331
344
|
progress: number;
|
|
345
|
+
}, {
|
|
332
346
|
state: "running";
|
|
347
|
+
progress: number;
|
|
333
348
|
}>, z.ZodObject<{
|
|
334
349
|
state: z.ZodLiteral<"done">;
|
|
335
350
|
progress: z.ZodNumber;
|
|
@@ -337,13 +352,13 @@ export declare const fetchPoll: z.ZodDiscriminatedUnion<"state", [z.ZodObject<{
|
|
|
337
352
|
refs: z.ZodArray<z.ZodTuple<[z.ZodString, z.ZodString], null>, "many">;
|
|
338
353
|
bundleId: z.ZodNullable<z.ZodString>;
|
|
339
354
|
}, "strict", z.ZodTypeAny, {
|
|
340
|
-
progress: number;
|
|
341
355
|
state: "done";
|
|
356
|
+
progress: number;
|
|
342
357
|
refs: [string, string][];
|
|
343
358
|
bundleId: string | null;
|
|
344
359
|
}, {
|
|
345
|
-
progress: number;
|
|
346
360
|
state: "done";
|
|
361
|
+
progress: number;
|
|
347
362
|
refs: [string, string][];
|
|
348
363
|
bundleId: string | null;
|
|
349
364
|
}>]>;
|
|
@@ -401,22 +416,22 @@ declare const errorEnvelope: z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
|
|
|
401
416
|
push: z.ZodBoolean;
|
|
402
417
|
operation: z.ZodEnum<["push", "delete", "pull-request"]>;
|
|
403
418
|
branch: z.ZodString;
|
|
404
|
-
reason: z.ZodEnum<["invalid", "protected", "settings", "not-a-commit", "too-long"]>;
|
|
419
|
+
reason: z.ZodEnum<["invalid", "protected", "settings", "other-settings", "outside-namespace", "not-a-commit", "too-long"]>;
|
|
405
420
|
message: z.ZodString;
|
|
406
421
|
}, "strict", z.ZodTypeAny, {
|
|
422
|
+
branch: string;
|
|
407
423
|
push: boolean;
|
|
408
424
|
message: string;
|
|
409
|
-
|
|
410
|
-
reason: "settings" | "protected" | "invalid" | "not-a-commit" | "too-long";
|
|
425
|
+
reason: "settings" | "protected" | "invalid" | "other-settings" | "outside-namespace" | "not-a-commit" | "too-long";
|
|
411
426
|
kind: "refusal";
|
|
412
|
-
operation: "push" | "
|
|
427
|
+
operation: "push" | "delete" | "pull-request";
|
|
413
428
|
}, {
|
|
429
|
+
branch: string;
|
|
414
430
|
push: boolean;
|
|
415
431
|
message: string;
|
|
416
|
-
|
|
417
|
-
reason: "settings" | "protected" | "invalid" | "not-a-commit" | "too-long";
|
|
432
|
+
reason: "settings" | "protected" | "invalid" | "other-settings" | "outside-namespace" | "not-a-commit" | "too-long";
|
|
418
433
|
kind: "refusal";
|
|
419
|
-
operation: "push" | "
|
|
434
|
+
operation: "push" | "delete" | "pull-request";
|
|
420
435
|
}>, z.ZodObject<{
|
|
421
436
|
kind: z.ZodLiteral<"push">;
|
|
422
437
|
pushKind: z.ZodEnum<["rejected", "rejected-after-stale-lease"]>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { GatewayRefusalError, MAX_PROTECTED_BRANCHES } from './branch-policy.js';
|
|
2
|
+
import { GatewayRefusalError, MAX_LEGACY_BRANCHES, MAX_PROTECTED_BRANCHES } from './branch-policy.js';
|
|
3
3
|
import { GitHubPushError, MAX_FETCH_HAVE } from './github-gateway.js';
|
|
4
4
|
import { GitHubTransientError, RefusedPushError } from './github-mirror.js';
|
|
5
5
|
import { gatewayLogWarn, redactEgress } from './gateway-egress.js';
|
|
@@ -89,10 +89,13 @@ export const answers = {
|
|
|
89
89
|
fatalError: message.nullable(),
|
|
90
90
|
preflight: z.enum(['not-an-app', 'verified', 'unverified', 'pending']),
|
|
91
91
|
clerkConfigured: z.boolean(),
|
|
92
|
-
|
|
92
|
+
branchPolicy: z
|
|
93
93
|
.object({
|
|
94
94
|
configured: z.array(name).max(MAX_PROTECTED_BRANCHES + 1),
|
|
95
95
|
githubDefault: name.nullable(),
|
|
96
|
+
namespace: name,
|
|
97
|
+
legacy: z.array(name).max(MAX_LEGACY_BRANCHES),
|
|
98
|
+
settingsBranch: name.nullable(),
|
|
96
99
|
})
|
|
97
100
|
.strict(),
|
|
98
101
|
})
|
|
@@ -232,7 +235,15 @@ const errorEnvelope = z.discriminatedUnion('kind', [
|
|
|
232
235
|
push: z.boolean(),
|
|
233
236
|
operation: z.enum(['push', 'delete', 'pull-request']),
|
|
234
237
|
branch: name,
|
|
235
|
-
reason: z.enum([
|
|
238
|
+
reason: z.enum([
|
|
239
|
+
'invalid',
|
|
240
|
+
'protected',
|
|
241
|
+
'settings',
|
|
242
|
+
'other-settings',
|
|
243
|
+
'outside-namespace',
|
|
244
|
+
'not-a-commit',
|
|
245
|
+
'too-long',
|
|
246
|
+
]),
|
|
236
247
|
message,
|
|
237
248
|
})
|
|
238
249
|
.strict(),
|
|
@@ -4,6 +4,7 @@ import type { AuthCacheSnapshot } from '../auth/cache.js';
|
|
|
4
4
|
import { type CreateOrUpdatePullRequestParams } from '../github-service.js';
|
|
5
5
|
import { type ResolvedGitHubAuth } from './github-auth.js';
|
|
6
6
|
import { type GitHubRefUpdate } from './github-mirror.js';
|
|
7
|
+
import { type BranchPolicyDescription } from './branch-policy.js';
|
|
7
8
|
import { type GatewayInstanceOptions } from './gateway-instance.js';
|
|
8
9
|
/** What a {@link GitHubGateway.fetch} brought back. */
|
|
9
10
|
export interface GitHubFetchResult {
|
|
@@ -62,7 +63,14 @@ export declare function describeMove(update: Extract<GitHubRefUpdate, {
|
|
|
62
63
|
moved: true;
|
|
63
64
|
}>): string;
|
|
64
65
|
/** The fields of a pull request the worker reads. */
|
|
65
|
-
export type GitHubPullRequest = Pick<RestEndpointMethodTypes['pulls']['get']['response']['data'], 'merged' | 'merged_at' | 'state' | 'node_id'
|
|
66
|
+
export type GitHubPullRequest = Pick<RestEndpointMethodTypes['pulls']['get']['response']['data'], 'merged' | 'merged_at' | 'state' | 'node_id'> & {
|
|
67
|
+
/**
|
|
68
|
+
* The pull request's body, `''` when it has none. `null` when the gateway withholds it, as it
|
|
69
|
+
* does for every pull request it would not update (`LocalGitHubGateway.getPullRequest`). A pull
|
|
70
|
+
* request a person opened from a branch in the namespace has its body answered.
|
|
71
|
+
*/
|
|
72
|
+
body: string | null;
|
|
73
|
+
};
|
|
66
74
|
export type GitHubCreateOrUpdatePullRequest = Omit<CreateOrUpdatePullRequestParams, 'octokit' | 'owner' | 'repo' | 'signal'>;
|
|
67
75
|
/**
|
|
68
76
|
* Everything the worker does with GitHub, and the only place the credential and Octokit are used.
|
|
@@ -178,11 +186,8 @@ export interface GitHubGatewayHealth {
|
|
|
178
186
|
preflight: 'not-an-app' | 'verified' | 'unverified' | 'pending';
|
|
179
187
|
/** Whether the gateway holds a Clerk secret key. */
|
|
180
188
|
clerkConfigured: boolean;
|
|
181
|
-
/** The
|
|
182
|
-
|
|
183
|
-
configured: string[];
|
|
184
|
-
githubDefault: string | null;
|
|
185
|
-
};
|
|
189
|
+
/** The branch policy as it stands (`BranchPolicy.describe`). */
|
|
190
|
+
branchPolicy: BranchPolicyDescription;
|
|
186
191
|
}
|
|
187
192
|
/** `GatewayInstanceOptions` carries the termination watch and the lifecycle completion. */
|
|
188
193
|
export interface LocalGitHubGatewayOptions extends GatewayInstanceOptions {
|
|
@@ -211,6 +216,12 @@ export interface LocalGitHubGatewayOptions extends GatewayInstanceOptions {
|
|
|
211
216
|
baseBranch?: string;
|
|
212
217
|
/** Further branches it protects alike: names and `*` globs (worker/branch-policy.ts). */
|
|
213
218
|
protectedBranches?: readonly string[];
|
|
219
|
+
/** This deployment's settings branch, the only settings branch the gateway pushes. */
|
|
220
|
+
settingsBranch?: string;
|
|
221
|
+
/** The branch namespace it writes, resolved (operating-mode/branch-namespace.ts). */
|
|
222
|
+
branchNamespace?: string;
|
|
223
|
+
/** Branches outside the namespace it may still write: exact names (worker/branch-policy.ts). */
|
|
224
|
+
legacyBranches?: readonly string[];
|
|
214
225
|
/**
|
|
215
226
|
* Fetches the auth provider's snapshot with the provider's key; its presence is
|
|
216
227
|
* `health().clerkConfigured`. Without it, `authSnapshot()` throws.
|
|
@@ -5,7 +5,7 @@ import { getErrorMessage } from '../utils/error.js';
|
|
|
5
5
|
import { isStaleLeaseRejection, workflowPushRefusalFile } from '../utils/git.js';
|
|
6
6
|
import { isTransientAuthFailure } from './github-auth.js';
|
|
7
7
|
import { GitHubMirror, GitHubTransientError, RefusedPushError, assertPlainBranchName, isObjectId, } from './github-mirror.js';
|
|
8
|
-
import { BranchPolicy, GatewayRefusalError, assertPullRequestText } from './branch-policy.js';
|
|
8
|
+
import { BranchPolicy, GatewayRefusalError, assertPullRequestText, } from './branch-policy.js';
|
|
9
9
|
import { gatewayLog, gatewayLogError, gatewayLogWarn, redactEgress, redactError, redactValue, registerLiveSecret, } from './gateway-egress.js';
|
|
10
10
|
import { GatewayInstance, untilAborted } from './gateway-instance.js';
|
|
11
11
|
/** The most `have` IDs one fetch takes. */
|
|
@@ -78,6 +78,9 @@ class LocalGitHubGateway {
|
|
|
78
78
|
this.policy = new BranchPolicy({
|
|
79
79
|
baseBranch: options.baseBranch,
|
|
80
80
|
protectedBranches: options.protectedBranches,
|
|
81
|
+
settingsBranch: options.settingsBranch,
|
|
82
|
+
legacyBranches: options.legacyBranches,
|
|
83
|
+
namespace: options.branchNamespace,
|
|
81
84
|
});
|
|
82
85
|
}
|
|
83
86
|
get repo() {
|
|
@@ -341,7 +344,7 @@ class LocalGitHubGateway {
|
|
|
341
344
|
fatalError,
|
|
342
345
|
preflight: this.preflight,
|
|
343
346
|
clerkConfigured: this.options.authSnapshot !== undefined,
|
|
344
|
-
|
|
347
|
+
branchPolicy: this.policy.describe(),
|
|
345
348
|
};
|
|
346
349
|
}
|
|
347
350
|
/**
|
|
@@ -618,7 +621,9 @@ class LocalGitHubGateway {
|
|
|
618
621
|
}
|
|
619
622
|
updatePullRequest(number, update, signal) {
|
|
620
623
|
return this.credentialed(signal, async (reach) => {
|
|
621
|
-
|
|
624
|
+
const refusing = `Refusing to ${update.state === 'closed' ? 'close' : 'update'} pull request #${number}`;
|
|
625
|
+
assertPullRequestText(refusing, `#${number}`, update);
|
|
626
|
+
await this.assertOwnPullRequest(reach, number, refusing, signal);
|
|
622
627
|
await this.api(reach, () => this.octokit.pulls.update({
|
|
623
628
|
...this.repo,
|
|
624
629
|
pull_number: number,
|
|
@@ -627,11 +632,62 @@ class LocalGitHubGateway {
|
|
|
627
632
|
}));
|
|
628
633
|
});
|
|
629
634
|
}
|
|
635
|
+
/**
|
|
636
|
+
* Pull request `number`'s merge state, for any number, and its body only when `updatePullRequest`
|
|
637
|
+
* would take its head: a branch of this repository that `BranchPolicy.takesPullRequestFrom`
|
|
638
|
+
* admits (the namespace or a `legacyBranches` entry) and that is not protected, whoever opened
|
|
639
|
+
* the pull request. Withholding more would protect nothing a write cannot change. When GitHub's
|
|
640
|
+
* default branch cannot be read, the configured protected set and the default as last read
|
|
641
|
+
* decide. The number comes from the worker, which a compromised CMS Lambda can steer; the head is
|
|
642
|
+
* GitHub's record.
|
|
643
|
+
*/
|
|
630
644
|
getPullRequest(number, signal) {
|
|
631
|
-
return this.credentialed(signal, (reach) =>
|
|
645
|
+
return this.credentialed(signal, async (reach) => {
|
|
646
|
+
const { data } = await this.api(reach, () => this.octokit.pulls.get({ ...this.repo, pull_number: number, request: { signal } }));
|
|
647
|
+
const head = data.head.ref;
|
|
648
|
+
// By id, as `assertOwnPullRequest` compares; a deleted fork has no repo. A protected head is
|
|
649
|
+
// one the gateway refuses to update the pull request of, so its body is withheld too.
|
|
650
|
+
let ours = data.head.repo?.id === data.base.repo.id && this.policy.takesPullRequestFrom(head);
|
|
651
|
+
if (ours) {
|
|
652
|
+
try {
|
|
653
|
+
ours = !(await this.policy.isProtected(head, () => this.readDefaultBranch(reach, signal)));
|
|
654
|
+
}
|
|
655
|
+
catch (err) {
|
|
656
|
+
// `isProtected` reads GitHub only for a head the configured set (the base branch and
|
|
657
|
+
// each `protectedBranches` entry) does not cover. The merge poll needs the merge state,
|
|
658
|
+
// so an unread default branch leaves the body to that set and to the default as last
|
|
659
|
+
// read, however old: `updatePullRequest` judges the head again before writing it.
|
|
660
|
+
if (signal?.aborted)
|
|
661
|
+
throw err;
|
|
662
|
+
if (this.policy.matchesLastReadDefault(head))
|
|
663
|
+
ours = false;
|
|
664
|
+
gatewayLogWarn(`Could not read GitHub's default branch for pull request #${number} ` +
|
|
665
|
+
`(${redactEgress(getErrorMessage(err))}); judging its head by the configured protected set and the default as last read`);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return {
|
|
669
|
+
merged: data.merged,
|
|
670
|
+
merged_at: data.merged_at,
|
|
671
|
+
state: data.state,
|
|
672
|
+
node_id: data.node_id,
|
|
673
|
+
body: ours ? (data.body ?? '') : null,
|
|
674
|
+
};
|
|
675
|
+
});
|
|
632
676
|
}
|
|
633
|
-
|
|
677
|
+
/**
|
|
678
|
+
* Read pull request `number` and refuse it unless its head is a branch of this repository that
|
|
679
|
+
* a pull request may come from (`BranchPolicy.assertPullRequestHeadAllowed`). The number comes
|
|
680
|
+
* from the worker; the head is GitHub's record, which the worker cannot choose.
|
|
681
|
+
*/
|
|
682
|
+
async assertOwnPullRequest(reach, number, refusing, signal) {
|
|
634
683
|
const { data } = await this.api(reach, () => this.octokit.pulls.get({ ...this.repo, pull_number: number, request: { signal } }));
|
|
684
|
+
const head = data.head.ref;
|
|
685
|
+
// By id, not name: the configured name keeps working after a rename or transfer, and the base
|
|
686
|
+
// repository is the one asked. A deleted fork has no repo.
|
|
687
|
+
if (data.head.repo?.id !== data.base.repo.id) {
|
|
688
|
+
throw new GatewayRefusalError('pull-request', head, 'outside-namespace', `${refusing}: its head is not a branch of this repository`);
|
|
689
|
+
}
|
|
690
|
+
await this.policy.assertPullRequestHeadAllowed(`${refusing}, whose head is ${JSON.stringify(head)}`, head, () => this.readDefaultBranch(reach, signal));
|
|
635
691
|
return data;
|
|
636
692
|
}
|
|
637
693
|
createOrUpdatePullRequest(request, signal) {
|
|
@@ -649,7 +705,7 @@ class LocalGitHubGateway {
|
|
|
649
705
|
convertPullRequestToDraft(number, signal) {
|
|
650
706
|
return this.credentialed(signal, async (reach) => {
|
|
651
707
|
// The REST API cannot convert a PR to a draft; GraphQL can.
|
|
652
|
-
const pr = await this.
|
|
708
|
+
const pr = await this.assertOwnPullRequest(reach, number, `Refusing to convert pull request #${number} to a draft`, signal);
|
|
653
709
|
await this.api(reach, () => this.octokit.graphql(`mutation($id: ID!) { convertPullRequestToDraft(input: { pullRequestId: $id }) { pullRequest { isDraft } } }`, { id: pr.node_id, request: { signal } }));
|
|
654
710
|
});
|
|
655
711
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Readable } from 'node:stream';
|
|
2
2
|
import { type SimpleGit } from 'simple-git';
|
|
3
3
|
import { type BundleHeader } from './bundle-header.js';
|
|
4
|
-
import { BranchPolicy, GatewayRefusalError } from './branch-policy.js';
|
|
4
|
+
import { BranchPolicy, GatewayRefusalError, type PushPolicyRefusal } from './branch-policy.js';
|
|
5
5
|
/**
|
|
6
6
|
* The worker's private bare mirror of the GitHub repository, in a state directory the CMS Lambda
|
|
7
7
|
* cannot reach. It and the quarantines beside it are the only repositories in which git ever runs
|
|
@@ -222,11 +222,14 @@ export interface GitHubCredential {
|
|
|
222
222
|
*/
|
|
223
223
|
export declare function isOwnLockFailure(message: string): boolean;
|
|
224
224
|
/**
|
|
225
|
-
* A push the worker never makes: to a name that is not a plain branch name,
|
|
226
|
-
* (`BranchPolicy
|
|
225
|
+
* A push the worker never makes: to a name that is not a plain branch name, one the policy refuses
|
|
226
|
+
* (`BranchPolicy.assertPushAllowed`, which owns those messages), or of an object that is not a
|
|
227
|
+
* commit.
|
|
227
228
|
*/
|
|
228
229
|
export declare class RefusedPushError extends GatewayRefusalError {
|
|
229
|
-
constructor(branch: string, reason: 'invalid' |
|
|
230
|
+
constructor(branch: string, reason: 'invalid' | PushPolicyRefusal | 'not-a-commit', sha?: string,
|
|
231
|
+
/** The policy's own message, which names the namespace (`pushRefusalMessage`). */
|
|
232
|
+
message?: string);
|
|
230
233
|
}
|
|
231
234
|
/**
|
|
232
235
|
* Throw {@link RefusedPushError} (or, for another `operation`, {@link GatewayRefusalError}) unless
|
|
@@ -7,7 +7,7 @@ import { getErrorMessage } from '../utils/error.js';
|
|
|
7
7
|
import { BundleHeaderError, BundleHeaderReader, hasOnlyHead, } from './bundle-header.js';
|
|
8
8
|
import { runGit } from './git-process.js';
|
|
9
9
|
import { MaintenanceMutex, findCorruption, maintainMirror, objectStoreKiB, rebuildPaths, recoverRebuild, sweepLocksAndTemporaries, } from './github-mirror-upkeep.js';
|
|
10
|
-
import { GatewayRefusalError, MAX_BRANCH_NAME_LENGTH, branchNameProblem, } from './branch-policy.js';
|
|
10
|
+
import { GatewayRefusalError, MAX_BRANCH_NAME_LENGTH, branchNameProblem, namespaceBlockedMessage, branchInsideMessage, } from './branch-policy.js';
|
|
11
11
|
import { gatewayLog, gatewayLogError, gatewayLogWarn, redactEgress } from './gateway-egress.js';
|
|
12
12
|
import { mirrorGitOptions, mirrorPins, quarantinePins } from './shared-repo-git.js';
|
|
13
13
|
/**
|
|
@@ -553,23 +553,22 @@ async function assertOwnDirectory(dir) {
|
|
|
553
553
|
}
|
|
554
554
|
}
|
|
555
555
|
/**
|
|
556
|
-
* A push the worker never makes: to a name that is not a plain branch name,
|
|
557
|
-
* (`BranchPolicy
|
|
556
|
+
* A push the worker never makes: to a name that is not a plain branch name, one the policy refuses
|
|
557
|
+
* (`BranchPolicy.assertPushAllowed`, which owns those messages), or of an object that is not a
|
|
558
|
+
* commit.
|
|
558
559
|
*/
|
|
559
560
|
export class RefusedPushError extends GatewayRefusalError {
|
|
560
|
-
constructor(branch, reason, sha
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
? `Refusing to push "${branch}" to GitHub under a lease: a settings branch only ever ` +
|
|
569
|
-
`fast-forwards. Find out what set a history-rewrite marker on it.`
|
|
570
|
-
: `Refusing to push "${branch}" to GitHub: ${sha ?? 'the object asked for'} is not a ` +
|
|
561
|
+
constructor(branch, reason, sha,
|
|
562
|
+
/** The policy's own message, which names the namespace (`pushRefusalMessage`). */
|
|
563
|
+
message) {
|
|
564
|
+
super('push', branch, reason, message ??
|
|
565
|
+
(reason === 'invalid'
|
|
566
|
+
? `Refusing to push ${JSON.stringify(branch)} to GitHub: it is not a valid branch name.`
|
|
567
|
+
: reason === 'not-a-commit'
|
|
568
|
+
? `Refusing to push "${branch}" to GitHub: ${sha ?? 'the object asked for'} is not a ` +
|
|
571
569
|
`commit, and a branch can only point at one. Find out what put it at ` +
|
|
572
|
-
`refs/heads/${branch} in remote.git.`
|
|
570
|
+
`refs/heads/${branch} in remote.git.`
|
|
571
|
+
: `Refusing to push ${JSON.stringify(branch)} to GitHub (${reason}).`));
|
|
573
572
|
this.name = 'RefusedPushError';
|
|
574
573
|
}
|
|
575
574
|
}
|
|
@@ -890,7 +889,19 @@ export class MirrorSession {
|
|
|
890
889
|
() => this.githubDefaultBranch(credential, options.signal).catch((err) => {
|
|
891
890
|
throw new GitHubTransientError('default-branch-unread', `Could not read GitHub's default branch before pushing ${branch}: ` +
|
|
892
891
|
redactEgress(getErrorMessage(err)).trim());
|
|
893
|
-
}), (reason) => new RefusedPushError(branch, reason));
|
|
892
|
+
}), (reason, message) => new RefusedPushError(branch, reason, undefined, message));
|
|
893
|
+
// The mirror's heads are GitHub's as of this session's fetch.
|
|
894
|
+
if (branch.startsWith(options.policy.namespace)) {
|
|
895
|
+
const heads = await this.heads();
|
|
896
|
+
const blocker = options.policy.namespaceBlocker(heads);
|
|
897
|
+
if (blocker !== null) {
|
|
898
|
+
throw new RefusedPushError(branch, 'invalid', undefined, namespaceBlockedMessage(branch, blocker));
|
|
899
|
+
}
|
|
900
|
+
const inside = [...heads.keys()].find((name) => name.startsWith(`${branch}/`));
|
|
901
|
+
if (inside !== undefined) {
|
|
902
|
+
throw new RefusedPushError(branch, 'invalid', undefined, branchInsideMessage(branch, inside));
|
|
903
|
+
}
|
|
904
|
+
}
|
|
894
905
|
options.onPushing?.();
|
|
895
906
|
const status = await this.githubBound(credential, [
|
|
896
907
|
'push',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type BranchPolicyDescription } from './branch-policy.js';
|
|
1
2
|
import type { WorkerContext } from './worker-context.js';
|
|
2
3
|
/**
|
|
3
4
|
* [SYNC-H1] The history-rewrite kernel: what happens when the rebase loop
|
|
@@ -29,19 +30,25 @@ export type HistoryRewriteContext = Pick<WorkerContext, 'remoteGitPath' | 'conte
|
|
|
29
30
|
*/
|
|
30
31
|
export declare function readPublishedSha(ctx: Pick<HistoryRewriteContext, 'remoteGitPath'>, branchRef: string): Promise<string | null>;
|
|
31
32
|
/**
|
|
32
|
-
* Record that this worker rewrote `expectedSha` out of
|
|
33
|
-
* published history (see BranchMetadata.historyRewrittenFrom).
|
|
33
|
+
* Record that this worker rewrote `expectedSha` out of `branchRef`'s already
|
|
34
|
+
* published history (see BranchMetadata.historyRewrittenFrom), on a branch.json
|
|
35
|
+
* whose `name` is `branchRef` only: it never creates one, nor marks another
|
|
36
|
+
* branch's that shares the key.
|
|
34
37
|
*
|
|
35
38
|
* Set-once: an existing marker is LEFT ALONE. Across two rebases before any
|
|
36
39
|
* GitHub push lands, GitHub still holds the commit the FIRST rebase replaced,
|
|
37
40
|
* so advancing the marker would aim the lease at a commit GitHub never had and
|
|
38
41
|
* permanently wedge the branch.
|
|
39
42
|
*
|
|
40
|
-
*
|
|
41
|
-
* runs concurrently with syncGit() and may
|
|
42
|
-
* branch was rebasing.
|
|
43
|
+
* Decided against the metadata on disk inside the write's lock, not the
|
|
44
|
+
* caller's snapshot: the task loop runs concurrently with syncGit() and may
|
|
45
|
+
* have cleared the marker while this branch was rebasing.
|
|
46
|
+
*
|
|
47
|
+
* Resolves whether `branchRef`'s branch.json now holds a marker, set here or kept by set-once.
|
|
48
|
+
* When it does not, the caller publishes nothing: a rewrite pushed without its marker leaves
|
|
49
|
+
* remote.git rewritten and the queued GitHub push failing with a false divergence diagnosis.
|
|
43
50
|
*/
|
|
44
|
-
export declare function markHistoryRewritten(ctx: Pick<HistoryRewriteContext, 'contentBranchesPath'>, branchPath: string,
|
|
51
|
+
export declare function markHistoryRewritten(ctx: Pick<HistoryRewriteContext, 'contentBranchesPath'>, branchPath: string, branchRef: string, expectedSha: string): Promise<boolean>;
|
|
45
52
|
/**
|
|
46
53
|
* Clear the marker once GitHub is confirmed to hold the rewritten history.
|
|
47
54
|
*
|
|
@@ -56,7 +63,7 @@ export declare function markHistoryRewritten(ctx: Pick<HistoryRewriteContext, 'c
|
|
|
56
63
|
* Tracked, with the concurrent-clear race that can drop a marker mid-arming,
|
|
57
64
|
* in safeinsights/canopycms#902.
|
|
58
65
|
*/
|
|
59
|
-
export declare function clearHistoryRewrittenMarker(ctx: Pick<HistoryRewriteContext, 'contentBranchesPath'>, branchPath: string,
|
|
66
|
+
export declare function clearHistoryRewrittenMarker(ctx: Pick<HistoryRewriteContext, 'contentBranchesPath'>, branchPath: string, branchRef: string): Promise<void>;
|
|
60
67
|
/**
|
|
61
68
|
* Publish a branch clone's rebased history into `remote.git`, replacing
|
|
62
69
|
* EXACTLY `expectedSha` and nothing else.
|
|
@@ -79,10 +86,15 @@ export declare function forcePublishToLocalRemote(ctx: Pick<HistoryRewriteContex
|
|
|
79
86
|
*
|
|
80
87
|
* Deliberately NOT skipped when a marker is already set: inferring "a task must
|
|
81
88
|
* already be queued" from the marker starves this hop whenever a task was lost,
|
|
82
|
-
* failed permanently, or was never written.
|
|
83
|
-
*
|
|
89
|
+
* failed permanently, or was never written. Skipped instead when `gatewayPolicy`,
|
|
90
|
+
* the gateway's own description, says it would refuse the branch: the marker
|
|
91
|
+
* clears only on a push that lands, so such a task would fail permanently on
|
|
92
|
+
* every cycle, and System health's unpublishable alert already names the branch.
|
|
93
|
+
* Without a description every hop is queued. So while a marker stands, at most
|
|
94
|
+
* one push task is in flight and one is added per sync cycle, only for a failure
|
|
95
|
+
* the policy does not predict; the tasks are idempotent.
|
|
84
96
|
*/
|
|
85
|
-
export declare function enqueueGitHubPush(ctx: Pick<HistoryRewriteContext, 'taskDir'>, branchRef: string): Promise<void>;
|
|
97
|
+
export declare function enqueueGitHubPush(ctx: Pick<HistoryRewriteContext, 'taskDir'>, branchRef: string, gatewayPolicy: BranchPolicyDescription | undefined): Promise<void>;
|
|
86
98
|
/**
|
|
87
99
|
* Complete a rewrite this worker started but did not finish: get the rebased
|
|
88
100
|
* history into `remote.git` and queued for GitHub.
|
|
@@ -100,6 +112,7 @@ export declare function reconcilePendingRewrite(ctx: HistoryRewriteContext, opti
|
|
|
100
112
|
branchRef: string;
|
|
101
113
|
headSha: string;
|
|
102
114
|
marker: string;
|
|
115
|
+
gatewayPolicy: BranchPolicyDescription | undefined;
|
|
103
116
|
}): Promise<void>;
|
|
104
117
|
/**
|
|
105
118
|
* Whether this branch carries a pending history rewrite -- the rebase loop
|