canopycms 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +326 -48
- package/dist/api/admin.d.ts +4 -4
- package/dist/api/branch-review.d.ts +20 -2
- package/dist/api/branch-review.js +153 -32
- package/dist/api/branch-status.d.ts +23 -2
- package/dist/api/branch-status.js +16 -2
- package/dist/api/branch-update-from-base.d.ts +23 -0
- package/dist/api/branch-update-from-base.js +227 -0
- package/dist/api/branch.d.ts +10 -0
- package/dist/api/branch.js +28 -2
- package/dist/api/client.d.ts +15 -2
- package/dist/api/client.js +24 -2
- package/dist/api/comments.d.ts +2 -2
- package/dist/api/content.d.ts +38 -1
- package/dist/api/content.js +41 -5
- package/dist/api/merge-conflicts.d.ts +235 -0
- package/dist/api/merge-conflicts.js +709 -0
- package/dist/api/routes.js +2 -0
- package/dist/api/types.d.ts +8 -3
- package/dist/base-update.d.ts +32 -0
- package/dist/base-update.js +31 -0
- package/dist/branch-metadata-file.d.ts +66 -0
- package/dist/branch-metadata-file.js +16 -1
- package/dist/branch-schema-cache.d.ts +12 -0
- package/dist/branch-schema-cache.js +27 -2
- package/dist/cli/backfill-item-keys.d.ts +77 -0
- package/dist/cli/backfill-item-keys.js +320 -0
- package/dist/cli/cli.d.ts +1 -1
- package/dist/cli/cli.js +9798 -1509
- package/dist/cli/generate-ai-content.js +373 -249
- package/dist/cli/github-app-manifest.d.ts +4 -1
- package/dist/cli/github-app-manifest.js +5 -1
- package/dist/cli/init.js +12 -0
- package/dist/cli/site-config.d.ts +14 -2
- package/dist/cli/site-config.js +20 -4
- package/dist/comment-store.d.ts +8 -0
- package/dist/comment-store.js +28 -0
- package/dist/content-index-generation.d.ts +1 -4
- package/dist/content-index-generation.js +1 -4
- package/dist/content-store.d.ts +39 -3
- package/dist/content-store.js +48 -15
- package/dist/editor/BranchManager.d.ts +4 -1
- package/dist/editor/BranchManager.js +9 -5
- package/dist/editor/Editor.js +462 -62
- package/dist/editor/EntryNavigator.d.ts +10 -2
- package/dist/editor/EntryNavigator.js +12 -4
- package/dist/editor/FormRenderer.d.ts +8 -2
- package/dist/editor/FormRenderer.js +5 -6
- package/dist/editor/admin/ConflictsStatusAlert.d.ts +8 -0
- package/dist/editor/admin/ConflictsStatusAlert.js +22 -0
- package/dist/editor/admin/SystemHealthPanel.js +4 -3
- package/dist/editor/base-update-wait.d.ts +78 -0
- package/dist/editor/base-update-wait.js +98 -0
- package/dist/editor/branch-status.js +1 -1
- package/dist/editor/components/BaseUpdateNotice.d.ts +12 -0
- package/dist/editor/components/BaseUpdateNotice.js +24 -0
- package/dist/editor/components/BranchPill.d.ts +4 -1
- package/dist/editor/components/BranchPill.js +5 -2
- package/dist/editor/components/ConflictsGroup.d.ts +112 -0
- package/dist/editor/components/ConflictsGroup.js +404 -0
- package/dist/editor/components/EditorHeader.d.ts +18 -1
- package/dist/editor/components/EditorHeader.js +99 -33
- package/dist/editor/components/EntryChangesDrawer.d.ts +16 -2
- package/dist/editor/components/EntryChangesDrawer.js +85 -167
- package/dist/editor/components/EntryHeader.d.ts +3 -1
- package/dist/editor/components/EntryHeader.js +2 -2
- package/dist/editor/components/GetLatestChangesButton.d.ts +15 -0
- package/dist/editor/components/GetLatestChangesButton.js +29 -0
- package/dist/editor/components/ReleaseHoldButton.d.ts +20 -0
- package/dist/editor/components/ReleaseHoldButton.js +36 -0
- package/dist/editor/components/RetryToastMessage.js +1 -1
- package/dist/editor/components/change-parts.d.ts +51 -0
- package/dist/editor/components/change-parts.js +171 -0
- package/dist/editor/conflict-cards.d.ts +93 -0
- package/dist/editor/conflict-cards.js +284 -0
- package/dist/editor/conflict-choices.d.ts +60 -0
- package/dist/editor/conflict-choices.js +108 -0
- package/dist/editor/conflict-notice.d.ts +25 -0
- package/dist/editor/conflict-notice.js +39 -0
- package/dist/editor/copy.d.ts +12 -0
- package/dist/editor/copy.js +12 -0
- package/dist/editor/entry-diff.d.ts +2 -1
- package/dist/editor/entry-diff.js +5 -21
- package/dist/editor/hooks/useBaseUpdate.d.ts +38 -0
- package/dist/editor/hooks/useBaseUpdate.js +279 -0
- package/dist/editor/hooks/useBranchChangesData.d.ts +7 -1
- package/dist/editor/hooks/useBranchChangesData.js +13 -0
- package/dist/editor/hooks/useBranchManager.d.ts +8 -1
- package/dist/editor/hooks/useBranchManager.js +108 -13
- package/dist/editor/hooks/useBranchesData.js +3 -0
- package/dist/editor/hooks/useConflictChoices.d.ts +24 -0
- package/dist/editor/hooks/useConflictChoices.js +75 -0
- package/dist/editor/hooks/useConflictsData.d.ts +26 -0
- package/dist/editor/hooks/useConflictsData.js +89 -0
- package/dist/editor/hooks/useDraftManager.d.ts +13 -1
- package/dist/editor/hooks/useDraftManager.js +40 -9
- package/dist/editor/hooks/useEntryManager.d.ts +17 -3
- package/dist/editor/hooks/useEntryManager.js +11 -5
- package/dist/editor/release-hold.d.ts +12 -0
- package/dist/editor/release-hold.js +39 -0
- package/dist/editor/server-clock.d.ts +4 -0
- package/dist/editor/server-clock.js +15 -0
- package/dist/editor/undo-change.d.ts +7 -0
- package/dist/editor/undo-change.js +19 -4
- package/dist/git-manager.d.ts +6 -0
- package/dist/git-manager.js +38 -3
- package/dist/item-keys.d.ts +13 -0
- package/dist/item-keys.js +36 -14
- package/dist/merge/conflict-path.d.ts +40 -0
- package/dist/merge/conflict-path.js +128 -0
- package/dist/merge/diff3.d.ts +27 -0
- package/dist/merge/diff3.js +113 -0
- package/dist/merge/entry-merge.d.ts +94 -0
- package/dist/merge/entry-merge.js +876 -0
- package/dist/merge/list-alignment.d.ts +55 -0
- package/dist/merge/list-alignment.js +490 -0
- package/dist/merge/rekey.d.ts +11 -0
- package/dist/merge/rekey.js +75 -0
- package/dist/merge/text-merge.d.ts +29 -0
- package/dist/merge/text-merge.js +59 -0
- package/dist/merge/value-equality.d.ts +18 -0
- package/dist/merge/value-equality.js +50 -0
- package/dist/merge-conflicts.d.ts +232 -0
- package/dist/merge-conflicts.js +451 -0
- package/dist/operating-mode/client-safe-strategy.d.ts +2 -0
- package/dist/operating-mode/client-safe-strategy.js +6 -0
- package/dist/operating-mode/client-unsafe-strategy.js +6 -0
- package/dist/operating-mode/types.d.ts +13 -0
- package/dist/services.d.ts +24 -1
- package/dist/services.js +138 -22
- package/dist/task-queue/cms-task-queue.d.ts +3 -1
- package/dist/types.d.ts +63 -5
- package/dist/utils/content-serialize.d.ts +18 -1
- package/dist/utils/content-serialize.js +12 -0
- package/dist/utils/content-write-lock.d.ts +3 -3
- package/dist/utils/content-write-lock.js +3 -3
- package/dist/utils/git.d.ts +7 -0
- package/dist/utils/git.js +35 -0
- package/dist/utils/markdown-body-splice.d.ts +1 -1
- package/dist/utils/markdown-body-splice.js +1 -19
- package/dist/utils/markdown-parse.d.ts +9 -0
- package/dist/utils/markdown-parse.js +27 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/branch-merge.d.ts +80 -40
- package/dist/worker/branch-merge.js +244 -58
- package/dist/worker/branch-policy.d.ts +18 -2
- package/dist/worker/branch-policy.js +36 -0
- package/dist/worker/cms-worker.d.ts +37 -3
- package/dist/worker/cms-worker.js +88 -16
- package/dist/worker/conflicts-status.d.ts +20 -0
- package/dist/worker/conflicts-status.js +204 -0
- package/dist/worker/field-merge.d.ts +80 -0
- package/dist/worker/field-merge.js +729 -0
- package/dist/worker/git-sync.d.ts +3 -2
- package/dist/worker/git-sync.js +16 -6
- package/dist/worker/github-commits.d.ts +42 -10
- package/dist/worker/github-commits.js +444 -52
- package/dist/worker/github-errors.d.ts +15 -0
- package/dist/worker/github-errors.js +47 -0
- package/dist/worker/github-gateway-http.js +24 -9
- package/dist/worker/github-gateway-wire.d.ts +49 -20
- package/dist/worker/github-gateway-wire.js +19 -3
- package/dist/worker/github-gateway.d.ts +27 -1
- package/dist/worker/github-gateway.js +55 -7
- package/dist/worker/github-transfer.d.ts +5 -3
- package/dist/worker/github-transfer.js +8 -5
- package/dist/worker/history-rewrite.d.ts +40 -8
- package/dist/worker/history-rewrite.js +51 -23
- package/dist/worker/publish-decision.d.ts +37 -0
- package/dist/worker/publish-decision.js +16 -0
- package/dist/worker/rebase.d.ts +29 -8
- package/dist/worker/rebase.js +492 -211
- package/dist/worker/task-runner.d.ts +1 -1
- package/dist/worker/task-runner.js +30 -41
- package/dist/worker/worker-context.d.ts +24 -2
- package/package.json +1 -1
|
@@ -28,16 +28,19 @@ const MAX_PUSH_RESTARTS = 3;
|
|
|
28
28
|
/**
|
|
29
29
|
* Fetch every GitHub branch into `remote.git`'s tracking namespace (`GITHUB_TRACKING_REF_PREFIX`),
|
|
30
30
|
* which then holds exactly GitHub's branches. `have` is every tip `remote.git` holds, so the
|
|
31
|
-
* bundle carries only what it lacks.
|
|
32
|
-
*
|
|
31
|
+
* bundle carries only what it lacks. `fresh` takes no GitHub fetch the gateway began before this
|
|
32
|
+
* call. Returns GitHub's branches, when the GitHub fetch they come from began, and what an operator
|
|
33
|
+
* must fix in remote.git for the namespace to keep up, if anything (see {@link moveTrackingRefs}).
|
|
33
34
|
*/
|
|
34
|
-
export async function fetchGitHubIntoRemoteGit(ctx, signal) {
|
|
35
|
+
export async function fetchGitHubIntoRemoteGit(ctx, signal, fresh = false) {
|
|
35
36
|
const gitDir = ctx.remoteGitPath;
|
|
36
|
-
const { refs, bundleId } = await ctx
|
|
37
|
+
const { refs, bundleId, startedAt } = await ctx
|
|
38
|
+
.github()
|
|
39
|
+
.fetch({ have: await haveOf(gitDir), fresh }, signal);
|
|
37
40
|
if (bundleId !== null)
|
|
38
41
|
await unbundle(ctx, gitDir, bundleId, signal);
|
|
39
42
|
const needsOperator = await moveTrackingRefs(ctx, gitDir, refs, signal);
|
|
40
|
-
return { refs, needsOperator };
|
|
43
|
+
return { refs, fetchedAt: startedAt, needsOperator };
|
|
41
44
|
}
|
|
42
45
|
/**
|
|
43
46
|
* Seed the new bare repository at `gitDir`, which must not exist yet, with every GitHub branch as
|
|
@@ -46,7 +46,8 @@ export declare function readPublishedSha(ctx: Pick<HistoryRewriteContext, 'remot
|
|
|
46
46
|
*
|
|
47
47
|
* `rewrittenTip` goes in the same write as `lastGitHubTip`, so a push that lands
|
|
48
48
|
* and dies before its stamp is never read as someone rewriting the branch on
|
|
49
|
-
* GitHub (github-commits.ts)
|
|
49
|
+
* GitHub (github-commits.ts), and as `historyRewrittenTo`, which unlike the
|
|
50
|
+
* marker each rewrite advances: it is what the publish sends.
|
|
50
51
|
*
|
|
51
52
|
* Resolves whether `branchRef`'s branch.json now holds a marker, set here or kept by set-once.
|
|
52
53
|
* When it does not, the caller publishes nothing: a rewrite pushed without its marker leaves
|
|
@@ -68,9 +69,20 @@ export declare function markHistoryRewritten(ctx: Pick<HistoryRewriteContext, 'c
|
|
|
68
69
|
* in safeinsights/canopycms#902.
|
|
69
70
|
*/
|
|
70
71
|
export declare function clearHistoryRewrittenMarker(ctx: Pick<HistoryRewriteContext, 'contentBranchesPath'>, branchPath: string, branchRef: string): Promise<void>;
|
|
72
|
+
/** How {@link forcePublishToLocalRemote} ended. */
|
|
73
|
+
export type RewritePublish = {
|
|
74
|
+
kind: 'published';
|
|
75
|
+
} | {
|
|
76
|
+
kind: 'moved';
|
|
77
|
+
} | {
|
|
78
|
+
kind: 'failed';
|
|
79
|
+
message: string;
|
|
80
|
+
};
|
|
71
81
|
/**
|
|
72
|
-
* Publish a branch clone's rebased history into `remote.git`,
|
|
73
|
-
* EXACTLY `expectedSha` and nothing else.
|
|
82
|
+
* Publish `rewrittenTip`, a branch clone's rebased history, into `remote.git`,
|
|
83
|
+
* replacing EXACTLY `expectedSha` and nothing else. The tip, never the clone's
|
|
84
|
+
* branch ref, which can hold a commit made since that no one has submitted
|
|
85
|
+
* (publish-decision.ts).
|
|
74
86
|
*
|
|
75
87
|
* The lease is the entire safety argument: `--force-with-lease=<ref>:<sha>`
|
|
76
88
|
* refuses unless `remote.git` still stands at `<sha>`, so this can only ever
|
|
@@ -79,10 +91,12 @@ export declare function clearHistoryRewrittenMarker(ctx: Pick<HistoryRewriteCont
|
|
|
79
91
|
* carryForwardRewrittenHistory for the interleaving where that would silently
|
|
80
92
|
* delete someone's direct push.
|
|
81
93
|
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
94
|
+
* Resolves `published`, `moved` for a refused lease (a concurrent Lambda push
|
|
95
|
+
* moved the ref; the next cycle judges the branch again), or `failed` with
|
|
96
|
+
* git's message.
|
|
97
|
+
* Never throws.
|
|
84
98
|
*/
|
|
85
|
-
export declare function forcePublishToLocalRemote(ctx: Pick<HistoryRewriteContext, 'remoteGitPath' | 'taskTimeoutMs'>, branchPath: string, branchRef: string, expectedSha: string): Promise<
|
|
99
|
+
export declare function forcePublishToLocalRemote(ctx: Pick<HistoryRewriteContext, 'remoteGitPath' | 'taskTimeoutMs'>, branchPath: string, branchRef: string, expectedSha: string, rewrittenTip: string): Promise<RewritePublish>;
|
|
86
100
|
/**
|
|
87
101
|
* Queue the GitHub hop for a branch whose rewritten history now sits in
|
|
88
102
|
* `remote.git`, so an open PR's head follows the rebase within a cycle instead
|
|
@@ -99,6 +113,18 @@ export declare function forcePublishToLocalRemote(ctx: Pick<HistoryRewriteContex
|
|
|
99
113
|
* the policy does not predict; the tasks are idempotent.
|
|
100
114
|
*/
|
|
101
115
|
export declare function enqueueGitHubPush(ctx: Pick<HistoryRewriteContext, 'taskDir'>, branchRef: string, gatewayPolicy: BranchPolicyDescription | undefined): Promise<void>;
|
|
116
|
+
/**
|
|
117
|
+
* `settled`: `remote.git` no longer stands at the marker, or never will by this pass. `waiting`: a
|
|
118
|
+
* concurrent push moved it this cycle. `failed`: the publish cannot land; `reason` says why.
|
|
119
|
+
*/
|
|
120
|
+
export type PendingRewrite = {
|
|
121
|
+
kind: 'settled';
|
|
122
|
+
} | {
|
|
123
|
+
kind: 'waiting';
|
|
124
|
+
} | {
|
|
125
|
+
kind: 'failed';
|
|
126
|
+
reason: string;
|
|
127
|
+
};
|
|
102
128
|
/**
|
|
103
129
|
* Complete a rewrite this worker started but did not finish: get the rebased
|
|
104
130
|
* history into `remote.git` and queued for GitHub.
|
|
@@ -108,7 +134,11 @@ export declare function enqueueGitHubPush(ctx: Pick<HistoryRewriteContext, 'task
|
|
|
108
134
|
* waiting for the next base-branch advance.
|
|
109
135
|
*
|
|
110
136
|
* Always leases on the MARKER, never on remote.git's current tip -- the marker
|
|
111
|
-
* is the one commit we know our own rebase replaced
|
|
137
|
+
* is the one commit we know our own rebase replaced -- and sends the recorded
|
|
138
|
+
* rebased tip, `target`, never `headSha`: a commit made in the clone since the
|
|
139
|
+
* rewrite is no one's submitted work. Resolves {@link PendingRewrite}: while
|
|
140
|
+
* `remote.git` still stands at the marker the branch takes nothing in, since a
|
|
141
|
+
* rebase would replace the rewrite along with any commit made on top of it.
|
|
112
142
|
*/
|
|
113
143
|
export declare function reconcilePendingRewrite(ctx: HistoryRewriteContext, options: {
|
|
114
144
|
branchPath: string;
|
|
@@ -116,8 +146,10 @@ export declare function reconcilePendingRewrite(ctx: HistoryRewriteContext, opti
|
|
|
116
146
|
branchRef: string;
|
|
117
147
|
headSha: string;
|
|
118
148
|
marker: string;
|
|
149
|
+
/** `historyRewrittenTo`, the rebased tip the rewrite recorded. */
|
|
150
|
+
target: string | undefined;
|
|
119
151
|
gatewayPolicy: BranchPolicyDescription | undefined;
|
|
120
|
-
}): Promise<
|
|
152
|
+
}): Promise<PendingRewrite>;
|
|
121
153
|
/**
|
|
122
154
|
* Whether this branch carries a pending history rewrite -- the rebase loop
|
|
123
155
|
* rewrote already-published history and the GitHub push has not landed yet
|
|
@@ -49,7 +49,8 @@ export async function readPublishedSha(ctx, branchRef) {
|
|
|
49
49
|
*
|
|
50
50
|
* `rewrittenTip` goes in the same write as `lastGitHubTip`, so a push that lands
|
|
51
51
|
* and dies before its stamp is never read as someone rewriting the branch on
|
|
52
|
-
* GitHub (github-commits.ts)
|
|
52
|
+
* GitHub (github-commits.ts), and as `historyRewrittenTo`, which unlike the
|
|
53
|
+
* marker each rewrite advances: it is what the publish sends.
|
|
53
54
|
*
|
|
54
55
|
* Resolves whether `branchRef`'s branch.json now holds a marker, set here or kept by set-once.
|
|
55
56
|
* When it does not, the caller publishes nothing: a rewrite pushed without its marker leaves
|
|
@@ -66,6 +67,7 @@ export async function markHistoryRewritten(ctx, branchPath, branchRef, expectedS
|
|
|
66
67
|
return {
|
|
67
68
|
branch: {
|
|
68
69
|
historyRewrittenFrom: typeof marker === 'string' ? marker : expectedSha,
|
|
70
|
+
historyRewrittenTo: rewrittenTip,
|
|
69
71
|
lastGitHubTip: rewrittenTip,
|
|
70
72
|
},
|
|
71
73
|
};
|
|
@@ -92,7 +94,7 @@ export async function clearHistoryRewrittenMarker(ctx, branchPath, branchRef) {
|
|
|
92
94
|
// Explicit undefined clears the key -- save()'s merge only overwrites
|
|
93
95
|
// keys present in the update (same pattern as rebaseFailure). Only a
|
|
94
96
|
// marker on `branchRef`'s own branch.json is cleared, so none is ever created here.
|
|
95
|
-
await meta.saveIf({ branch: { historyRewrittenFrom: undefined } }, (current) => current?.branch.name === branchRef &&
|
|
97
|
+
await meta.saveIf({ branch: { historyRewrittenFrom: undefined, historyRewrittenTo: undefined } }, (current) => current?.branch.name === branchRef &&
|
|
96
98
|
typeof current.branch.historyRewrittenFrom === 'string');
|
|
97
99
|
}
|
|
98
100
|
catch (err) {
|
|
@@ -100,8 +102,10 @@ export async function clearHistoryRewrittenMarker(ctx, branchPath, branchRef) {
|
|
|
100
102
|
}
|
|
101
103
|
}
|
|
102
104
|
/**
|
|
103
|
-
* Publish a branch clone's rebased history into `remote.git`,
|
|
104
|
-
* EXACTLY `expectedSha` and nothing else.
|
|
105
|
+
* Publish `rewrittenTip`, a branch clone's rebased history, into `remote.git`,
|
|
106
|
+
* replacing EXACTLY `expectedSha` and nothing else. The tip, never the clone's
|
|
107
|
+
* branch ref, which can hold a commit made since that no one has submitted
|
|
108
|
+
* (publish-decision.ts).
|
|
105
109
|
*
|
|
106
110
|
* The lease is the entire safety argument: `--force-with-lease=<ref>:<sha>`
|
|
107
111
|
* refuses unless `remote.git` still stands at `<sha>`, so this can only ever
|
|
@@ -110,10 +114,12 @@ export async function clearHistoryRewrittenMarker(ctx, branchPath, branchRef) {
|
|
|
110
114
|
* carryForwardRewrittenHistory for the interleaving where that would silently
|
|
111
115
|
* delete someone's direct push.
|
|
112
116
|
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
117
|
+
* Resolves `published`, `moved` for a refused lease (a concurrent Lambda push
|
|
118
|
+
* moved the ref; the next cycle judges the branch again), or `failed` with
|
|
119
|
+
* git's message.
|
|
120
|
+
* Never throws.
|
|
115
121
|
*/
|
|
116
|
-
export async function forcePublishToLocalRemote(ctx, branchPath, branchRef, expectedSha) {
|
|
122
|
+
export async function forcePublishToLocalRemote(ctx, branchPath, branchRef, expectedSha, rewrittenTip) {
|
|
117
123
|
// sharedRepoGit's env is gitChildEnv's, whose C-locale pin is what keeps
|
|
118
124
|
// isStaleLeaseRejection below from silently becoming a no-op on a
|
|
119
125
|
// non-English host.
|
|
@@ -121,24 +127,26 @@ export async function forcePublishToLocalRemote(ctx, branchPath, branchRef, expe
|
|
|
121
127
|
try {
|
|
122
128
|
await pushGit.raw([
|
|
123
129
|
'push',
|
|
124
|
-
`--force-with-lease
|
|
130
|
+
`--force-with-lease=refs/heads/${branchRef}:${expectedSha}`,
|
|
125
131
|
// Otherwise remote.git's own hooks run, as the worker.
|
|
126
132
|
`--receive-pack=${pinnedReceivePack()}`,
|
|
127
133
|
// Real flags must precede --end-of-options; everything after it is
|
|
128
134
|
// positional (see GitManager.push() for the same guard).
|
|
129
135
|
'--end-of-options',
|
|
130
136
|
ctx.remoteGitPath,
|
|
131
|
-
`${
|
|
137
|
+
`${rewrittenTip}:refs/heads/${branchRef}`,
|
|
132
138
|
]);
|
|
133
139
|
workerLog(` Published rebased ${branchRef} into remote.git`);
|
|
134
|
-
return
|
|
140
|
+
return { kind: 'published' };
|
|
135
141
|
}
|
|
136
142
|
catch (err) {
|
|
137
143
|
const message = redactCredentials(getErrorMessage(err));
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
144
|
+
if (isStaleLeaseRejection(message)) {
|
|
145
|
+
workerLogWarn(` Did not publish rebased ${branchRef} into remote.git: it moved since this cycle read it (concurrent submit?) -- retrying next cycle`);
|
|
146
|
+
return { kind: 'moved' };
|
|
147
|
+
}
|
|
148
|
+
workerLogWarn(` Failed to publish rebased ${branchRef} into remote.git: ${message}`);
|
|
149
|
+
return { kind: 'failed', message };
|
|
142
150
|
}
|
|
143
151
|
}
|
|
144
152
|
/**
|
|
@@ -178,6 +186,10 @@ export async function enqueueGitHubPush(ctx, branchRef, gatewayPolicy) {
|
|
|
178
186
|
workerLogWarn(` Failed to queue GitHub push for ${branchRef}: ${getErrorMessage(err)}`);
|
|
179
187
|
}
|
|
180
188
|
}
|
|
189
|
+
/** Why a branch whose rewrite was recorded without its rebased tip takes nothing in. */
|
|
190
|
+
const REWRITE_TIP_UNKNOWN = `this branch's rebased history was recorded without the commit to publish, so neither it nor the ` +
|
|
191
|
+
`base is taken in. A developer publishes the rewrite by hand before the branch is submitted again.`;
|
|
192
|
+
const COMMIT_ID = /^[0-9a-f]{40}([0-9a-f]{24})?$/;
|
|
181
193
|
/**
|
|
182
194
|
* Complete a rewrite this worker started but did not finish: get the rebased
|
|
183
195
|
* history into `remote.git` and queued for GitHub.
|
|
@@ -187,33 +199,49 @@ export async function enqueueGitHubPush(ctx, branchRef, gatewayPolicy) {
|
|
|
187
199
|
* waiting for the next base-branch advance.
|
|
188
200
|
*
|
|
189
201
|
* Always leases on the MARKER, never on remote.git's current tip -- the marker
|
|
190
|
-
* is the one commit we know our own rebase replaced
|
|
202
|
+
* is the one commit we know our own rebase replaced -- and sends the recorded
|
|
203
|
+
* rebased tip, `target`, never `headSha`: a commit made in the clone since the
|
|
204
|
+
* rewrite is no one's submitted work. Resolves {@link PendingRewrite}: while
|
|
205
|
+
* `remote.git` still stands at the marker the branch takes nothing in, since a
|
|
206
|
+
* rebase would replace the rewrite along with any commit made on top of it.
|
|
191
207
|
*/
|
|
192
208
|
export async function reconcilePendingRewrite(ctx, options) {
|
|
193
209
|
const { branchPath, branchDir, branchRef, headSha, marker, gatewayPolicy } = options;
|
|
210
|
+
const target = options.target !== undefined && COMMIT_ID.test(options.target) ? options.target : undefined;
|
|
194
211
|
const publishedSha = await readPublishedSha(ctx, branchRef);
|
|
195
212
|
if (publishedSha === null) {
|
|
196
213
|
workerLogWarn(` ${branchDir}: a rewritten history is recorded but ${branchRef} is gone from remote.git -- nothing to publish`);
|
|
197
|
-
return;
|
|
214
|
+
return { kind: 'settled' };
|
|
198
215
|
}
|
|
199
|
-
if (publishedSha === headSha) {
|
|
200
|
-
// remote.git already carries the rewrite
|
|
201
|
-
// (a crash between the push and the queue, or a task
|
|
216
|
+
if (publishedSha === target || publishedSha === headSha) {
|
|
217
|
+
// remote.git already carries the rewrite, or a submit since; only the
|
|
218
|
+
// GitHub hop is left (a crash between the push and the queue, or a task
|
|
219
|
+
// that was lost).
|
|
202
220
|
await enqueueGitHubPush(ctx, branchRef, gatewayPolicy);
|
|
203
|
-
return;
|
|
221
|
+
return { kind: 'settled' };
|
|
204
222
|
}
|
|
205
223
|
if (publishedSha === marker) {
|
|
206
224
|
// The publish into remote.git never landed -- a crash right after the
|
|
207
225
|
// marker was written, or a lease refused by a concurrent submit.
|
|
208
|
-
if (
|
|
209
|
-
|
|
226
|
+
if (target === undefined)
|
|
227
|
+
return { kind: 'failed', reason: REWRITE_TIP_UNKNOWN };
|
|
228
|
+
const publish = await forcePublishToLocalRemote(ctx, branchPath, branchRef, marker, target);
|
|
229
|
+
if (publish.kind === 'moved')
|
|
230
|
+
return { kind: 'waiting' };
|
|
231
|
+
if (publish.kind === 'failed') {
|
|
232
|
+
return {
|
|
233
|
+
kind: 'failed',
|
|
234
|
+
reason: `this branch's rebased history could not be published into the deployment's repository: ${publish.message}`,
|
|
235
|
+
};
|
|
210
236
|
}
|
|
211
|
-
|
|
237
|
+
await enqueueGitHubPush(ctx, branchRef, gatewayPolicy);
|
|
238
|
+
return { kind: 'settled' };
|
|
212
239
|
}
|
|
213
240
|
// Neither this branch's rebased tip nor the commit its rewrite replaced:
|
|
214
241
|
// something else moved remote.git. Never force over that.
|
|
215
242
|
workerLogWarn(` ${branchDir}: remote.git is at ${publishedSha} for ${branchRef}, which is neither the ` +
|
|
216
243
|
`rebased tip nor the commit the rewrite replaced -- left untouched`);
|
|
244
|
+
return { kind: 'settled' };
|
|
217
245
|
}
|
|
218
246
|
/**
|
|
219
247
|
* Whether this branch carries a pending history rewrite -- the rebase loop
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { SimpleGit } from 'simple-git';
|
|
2
|
+
/**
|
|
3
|
+
* What the worker may publish for a branch: move its head in `remote.git`, which is all a GitHub
|
|
4
|
+
* push ever sends (task-runner.ts `pushBranchToGitHub`). Work the author has saved but not
|
|
5
|
+
* submitted reaches neither until their submit pushes it, so a commit the worker builds on top of
|
|
6
|
+
* such work stays in the clone. The worker publishes in two places, and each asks here or follows
|
|
7
|
+
* the rule stated here:
|
|
8
|
+
*
|
|
9
|
+
* - a merge it built (github-commits.ts `mergeIntoBranch`): {@link decidePublish};
|
|
10
|
+
* - a rewrite of history already published (history-rewrite.ts): only the rebased tip recorded with
|
|
11
|
+
* the rewrite, never the clone's current head, which can carry a commit made since. While that
|
|
12
|
+
* tip is unpublished the branch takes nothing in (rebase.ts), so nothing is built on it.
|
|
13
|
+
*/
|
|
14
|
+
export type PublishDecision =
|
|
15
|
+
/** `remote.git` has no head for the branch: nothing of it was ever published. */
|
|
16
|
+
{
|
|
17
|
+
kind: 'never-published';
|
|
18
|
+
}
|
|
19
|
+
/** Every commit on the branch's side is already in `remote.git`'s head. */
|
|
20
|
+
| {
|
|
21
|
+
kind: 'publish';
|
|
22
|
+
}
|
|
23
|
+
/** The branch's side holds commits the author has not submitted: the merge stays in the clone. */
|
|
24
|
+
| {
|
|
25
|
+
kind: 'hold';
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Whether a merge built on `head` may be published over `published`, `remote.git`'s head: only
|
|
29
|
+
* when `remote.git` already holds all of `head`, whatever the branch's status, since a save can be
|
|
30
|
+
* committed in the moment between a submit's push and its status write. Judged from the commits
|
|
31
|
+
* every time, so nothing records a hold: the next submit pushes the clone, held merges included,
|
|
32
|
+
* as a fast-forward. The caller has checked that the clone holds `published`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function decidePublish(git: SimpleGit, { head, published }: {
|
|
35
|
+
head: string;
|
|
36
|
+
published: string | null;
|
|
37
|
+
}): Promise<PublishDecision>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { commitsApart } from './branch-merge.js';
|
|
2
|
+
/**
|
|
3
|
+
* Whether a merge built on `head` may be published over `published`, `remote.git`'s head: only
|
|
4
|
+
* when `remote.git` already holds all of `head`, whatever the branch's status, since a save can be
|
|
5
|
+
* committed in the moment between a submit's push and its status write. Judged from the commits
|
|
6
|
+
* every time, so nothing records a hold: the next submit pushes the clone, held merges included,
|
|
7
|
+
* as a fast-forward. The caller has checked that the clone holds `published`.
|
|
8
|
+
*/
|
|
9
|
+
export async function decidePublish(git, { head, published }) {
|
|
10
|
+
if (published === null)
|
|
11
|
+
return { kind: 'never-published' };
|
|
12
|
+
if (published === head)
|
|
13
|
+
return { kind: 'publish' };
|
|
14
|
+
const { onlyA } = await commitsApart(git, head, published);
|
|
15
|
+
return onlyA === 0 ? { kind: 'publish' } : { kind: 'hold' };
|
|
16
|
+
}
|
package/dist/worker/rebase.d.ts
CHANGED
|
@@ -7,8 +7,9 @@ import type { WorkerContext } from './worker-context.js';
|
|
|
7
7
|
* The rebase loop: the deepest leaf of the git-sync cluster, and the most
|
|
8
8
|
* safety-critical part of the worker. `syncGit` (git-sync.ts) calls
|
|
9
9
|
* `runRebaseCycle` once per cycle; every branch workspace behind the base
|
|
10
|
-
* branch
|
|
11
|
-
*
|
|
10
|
+
* branch takes it in: by a rebase where git and the field merge agree there is
|
|
11
|
+
* nothing to decide, and by a merge (github-commits.ts' `mergeIntoBranch`)
|
|
12
|
+
* wherever there is. Three things make it the part most likely to misbehave under real EFS
|
|
12
13
|
* and spot conditions, all documented at the point they happen:
|
|
13
14
|
*
|
|
14
15
|
* - it holds the [SYNC-C1] cross-host content-write lock for the whole rebase,
|
|
@@ -27,7 +28,7 @@ import type { WorkerContext } from './worker-context.js';
|
|
|
27
28
|
* but still need their PR's resolution polled, and this loop is the only thing
|
|
28
29
|
* that walks every branch workspace.
|
|
29
30
|
*/
|
|
30
|
-
export type RebaseContext = Pick<WorkerContext, 'baseBranch' | 'sanitizedBaseBranch' | 'contentBranchesPath' | 'contentRoot' | 'remoteGitPath' | 'taskDir' | 'taskTimeoutMs' | 'github' | 'ensureStatusReport' | 'afterConflictDetectedForTesting' | 'afterRebaseCompletedForTesting' | 'ensureSettingsBranch' | 'isDraining'>;
|
|
31
|
+
export type RebaseContext = Pick<WorkerContext, 'baseBranch' | 'sanitizedBaseBranch' | 'contentBranchesPath' | 'contentRoot' | 'remoteGitPath' | 'taskDir' | 'taskTimeoutMs' | 'github' | 'ensureStatusReport' | 'afterConflictDetectedForTesting' | 'afterRebaseCompletedForTesting' | 'ensureSettingsBranch' | 'isDraining' | 'requestBranchSync'>;
|
|
31
32
|
/**
|
|
32
33
|
* Per-cycle outcome of `rebaseActiveBranches()`, folded by `syncGit()` into the
|
|
33
34
|
* worker's self-reported status (`WorkerStatusReport.lastGitSync`, see
|
|
@@ -73,16 +74,36 @@ export interface RebaseSummary {
|
|
|
73
74
|
* next sync cycle. Resolves GitHub's answer for a pull request not merged.
|
|
74
75
|
*/
|
|
75
76
|
export declare function pollMergeState(ctx: RebaseContext, branchDir: string, branchPath: string, metaFile: BranchMetadataFile | null): Promise<GitHubPullRequest | null>;
|
|
77
|
+
/**
|
|
78
|
+
* What a request for the latest changes is answered from: the fetch of the base that the branch
|
|
79
|
+
* was brought up to. The worker's cycle takes it from its GitHub fetch; a run in the request
|
|
80
|
+
* ({@link syncBranchNow}) from its own start, its base being `remote.git`'s.
|
|
81
|
+
*/
|
|
82
|
+
interface BaseUpdateSource {
|
|
83
|
+
/** When that fetch began. */
|
|
84
|
+
fetchedAt: number;
|
|
85
|
+
/** The base commit it found; undefined when it reported none. */
|
|
86
|
+
baseTip?: string;
|
|
87
|
+
}
|
|
76
88
|
/**
|
|
77
89
|
* Rebase every branch workspace that is behind the base branch, one cycle.
|
|
78
90
|
*
|
|
79
|
-
* The per-branch work is {@link
|
|
80
|
-
* over `content-branches
|
|
81
|
-
*
|
|
82
|
-
*
|
|
91
|
+
* The per-branch work is {@link syncBranchDir}; what stays here is the walk
|
|
92
|
+
* over `content-branches/` and folding each branch's outcome into the summary.
|
|
93
|
+
* `source` answers requests for the latest changes; the GitHub fetch's by default. `local` is a
|
|
94
|
+
* run with no GitHub (see {@link syncBranchDir}).
|
|
83
95
|
*
|
|
84
96
|
* Deliberately sequential rather than concurrent: each branch's rebase holds
|
|
85
97
|
* that branch's cross-host content-write lock and runs a series of git
|
|
86
98
|
* subprocesses against a shared filesystem.
|
|
87
99
|
*/
|
|
88
|
-
export declare function runRebaseCycle(ctx: RebaseContext, gatewayPolicy?: BranchPolicyDescription, gitHub?: GitHubSnapshot): Promise<RebaseSummary>;
|
|
100
|
+
export declare function runRebaseCycle(ctx: RebaseContext, gatewayPolicy?: BranchPolicyDescription, gitHub?: GitHubSnapshot, requested?: ReadonlySet<string>, source?: BaseUpdateSource | undefined, local?: boolean): Promise<RebaseSummary>;
|
|
101
|
+
/**
|
|
102
|
+
* Bring the base into one branch now and answer its request for the latest changes, for a mode that
|
|
103
|
+
* runs no worker loop (operating-mode `baseUpdateRunsInRequest`). With no GitHub, the base is
|
|
104
|
+
* `remote.git`'s, so the answer is from this run's start. A run that answers nothing writes
|
|
105
|
+
* `failed`, since nothing else ever will: `busy` says a lock held it back, which a retry in a moment
|
|
106
|
+
* gets past. Never throws but for the metadata write of that fallback.
|
|
107
|
+
*/
|
|
108
|
+
export declare function syncBranchNow(ctx: RebaseContext, branchName: string): Promise<'answered' | 'busy'>;
|
|
109
|
+
export {};
|