canopycms 0.12.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +211 -49
- package/dist/api/branch-review.d.ts +20 -2
- package/dist/api/branch-review.js +112 -23
- package/dist/api/branch-status.d.ts +11 -2
- package/dist/api/branch-status.js +3 -2
- package/dist/api/branch-update-from-base.js +57 -6
- package/dist/api/branch.d.ts +5 -0
- package/dist/api/branch.js +27 -2
- package/dist/api/client.d.ts +2 -2
- package/dist/api/client.js +2 -2
- package/dist/api/content.js +1 -1
- package/dist/api/merge-conflicts.d.ts +10 -1
- package/dist/api/merge-conflicts.js +42 -7
- package/dist/api/types.d.ts +3 -2
- package/dist/base-update.d.ts +6 -0
- package/dist/branch-metadata-file.d.ts +15 -0
- package/dist/branch-metadata-file.js +5 -1
- package/dist/cli/cli.js +238 -108
- package/dist/cli/generate-ai-content.js +20 -6
- package/dist/cli/github-app-manifest.d.ts +4 -1
- package/dist/cli/github-app-manifest.js +5 -1
- package/dist/cli/init.js +7 -1
- package/dist/editor/Editor.js +39 -16
- package/dist/editor/admin/ConflictsStatusAlert.d.ts +8 -0
- package/dist/editor/admin/ConflictsStatusAlert.js +22 -0
- package/dist/editor/admin/SystemHealthPanel.js +4 -3
- package/dist/editor/base-update-wait.d.ts +4 -0
- package/dist/editor/base-update-wait.js +7 -0
- package/dist/editor/components/ConflictsGroup.d.ts +13 -1
- package/dist/editor/components/ConflictsGroup.js +52 -12
- package/dist/editor/components/EditorHeader.d.ts +3 -1
- package/dist/editor/components/EditorHeader.js +11 -7
- package/dist/editor/components/EntryChangesDrawer.d.ts +1 -1
- package/dist/editor/components/EntryChangesDrawer.js +58 -6
- package/dist/editor/conflict-cards.d.ts +4 -1
- package/dist/editor/conflict-cards.js +14 -9
- package/dist/editor/hooks/useBaseUpdate.d.ts +2 -0
- package/dist/editor/hooks/useBaseUpdate.js +12 -2
- package/dist/editor/hooks/useBranchChangesData.d.ts +7 -1
- package/dist/editor/hooks/useBranchChangesData.js +13 -0
- package/dist/editor/hooks/useBranchManager.d.ts +2 -0
- package/dist/editor/hooks/useBranchManager.js +80 -6
- package/dist/editor/hooks/useDraftManager.d.ts +8 -1
- package/dist/editor/hooks/useDraftManager.js +16 -1
- package/dist/editor/hooks/useEntryManager.d.ts +8 -3
- package/dist/editor/hooks/useEntryManager.js +6 -5
- package/dist/git-manager.d.ts +1 -0
- package/dist/git-manager.js +8 -3
- package/dist/merge/entry-merge.js +4 -3
- package/dist/merge/list-alignment.d.ts +9 -6
- package/dist/merge/list-alignment.js +65 -16
- package/dist/merge/rekey.d.ts +11 -0
- package/dist/merge/rekey.js +75 -0
- package/dist/operating-mode/client-safe-strategy.js +1 -1
- package/dist/operating-mode/client-unsafe-strategy.js +6 -0
- package/dist/operating-mode/types.d.ts +10 -2
- package/dist/services.d.ts +12 -4
- package/dist/services.js +56 -0
- package/dist/types.d.ts +35 -3
- package/dist/utils/git.d.ts +7 -0
- package/dist/utils/git.js +35 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/branch-merge.d.ts +27 -3
- package/dist/worker/branch-merge.js +106 -5
- package/dist/worker/branch-policy.d.ts +18 -2
- package/dist/worker/branch-policy.js +36 -0
- package/dist/worker/cms-worker.d.ts +21 -0
- package/dist/worker/cms-worker.js +29 -4
- package/dist/worker/conflicts-status.d.ts +20 -0
- package/dist/worker/conflicts-status.js +204 -0
- package/dist/worker/field-merge.d.ts +17 -2
- package/dist/worker/field-merge.js +95 -2
- package/dist/worker/git-sync.d.ts +3 -2
- package/dist/worker/git-sync.js +9 -0
- package/dist/worker/github-commits.d.ts +3 -3
- package/dist/worker/github-commits.js +192 -40
- package/dist/worker/github-errors.d.ts +15 -0
- package/dist/worker/github-errors.js +47 -0
- package/dist/worker/github-gateway-http.js +7 -0
- package/dist/worker/github-gateway-wire.d.ts +30 -10
- package/dist/worker/github-gateway-wire.js +14 -2
- package/dist/worker/github-gateway.d.ts +17 -0
- package/dist/worker/github-gateway.js +37 -1
- package/dist/worker/history-rewrite.d.ts +40 -8
- package/dist/worker/history-rewrite.js +51 -23
- package/dist/worker/publish-decision.d.ts +37 -0
- package/dist/worker/publish-decision.js +16 -0
- package/dist/worker/rebase.d.ts +25 -5
- package/dist/worker/rebase.js +225 -81
- package/dist/worker/task-runner.js +16 -39
- package/dist/worker/worker-context.d.ts +6 -0
- package/package.json +1 -1
package/dist/services.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { GitManager } from './git-manager.js';
|
|
|
7
7
|
import { BranchRegistry } from './branch-registry.js';
|
|
8
8
|
import { type GitHubService } from './github-service.js';
|
|
9
9
|
import { BranchSchemaCache } from './branch-schema-cache.js';
|
|
10
|
+
import type { BaseUpdateRun } from './base-update.js';
|
|
10
11
|
import { type EditorLookup, type SubmissionEditor } from './submission-attribution.js';
|
|
11
12
|
import { type StampedEntry } from './stamp-on-publish.js';
|
|
12
13
|
/**
|
|
@@ -80,10 +81,9 @@ export interface CanopyServices {
|
|
|
80
81
|
message: string;
|
|
81
82
|
}) => Promise<void>;
|
|
82
83
|
/**
|
|
83
|
-
* Commit the branch's saved but uncommitted work in its clone, pushing nothing
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* commit.
|
|
84
|
+
* Commit the branch's saved but uncommitted work in its clone, pushing nothing: only a later
|
|
85
|
+
* submit publishes it, whatever the worker merges on top (worker/publish-decision.ts). Its
|
|
86
|
+
* trailers name the editors who saved since the last commit.
|
|
87
87
|
* Holds the branch's content-write lock; rejects with `ContentWriteLockBusyError` when busy.
|
|
88
88
|
* Resolves whether there was anything to commit.
|
|
89
89
|
*/
|
|
@@ -110,6 +110,14 @@ export interface CanopyServices {
|
|
|
110
110
|
lookupEditor?: EditorLookup;
|
|
111
111
|
message?: string;
|
|
112
112
|
}) => Promise<SubmitBranchResult>;
|
|
113
|
+
/**
|
|
114
|
+
* Bring the base into the branch now and answer its request for the latest changes, where the
|
|
115
|
+
* mode's strategy runs that in the request (operating-mode `baseUpdateRunsInRequest`); rejects
|
|
116
|
+
* in any other mode, where only the worker may. Resolves `busy` when a lock held the merge back,
|
|
117
|
+
* the request then answered `failed`, and `other-remote`, running nothing, for a clone of another
|
|
118
|
+
* remote than the workspace's `remote.git`.
|
|
119
|
+
*/
|
|
120
|
+
runBaseUpdateNow: (context: BranchContext) => Promise<BaseUpdateRun>;
|
|
113
121
|
/** Commit to the settings branch (for permissions/groups) and push it; never opens a PR */
|
|
114
122
|
commitToSettingsBranch: (options: {
|
|
115
123
|
branchRoot: string;
|
package/dist/services.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import fs from 'node:fs/promises';
|
|
19
19
|
import path from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
20
21
|
import { getConfigDefaults } from './config/index.js';
|
|
21
22
|
import { createCheckBranchAccess, createCheckContentAccess, createContentAccessChecker, loadPathPermissions, } from './authorization/index.js';
|
|
22
23
|
import { GitManager, GitRemoteRefMissingError } from './git-manager.js';
|
|
@@ -139,6 +140,33 @@ export const createTestCanopyServices = async (config, options = {}) => {
|
|
|
139
140
|
};
|
|
140
141
|
return _createCanopyServicesInternal(testConfig, options);
|
|
141
142
|
};
|
|
143
|
+
/**
|
|
144
|
+
* A remote URL's local path as git reads it: a `file://` URL, a drive path, or a path relative to
|
|
145
|
+
* `cwd` with its links resolved; undefined for a scheme or git's scp form (a colon before any
|
|
146
|
+
* slash), which name another host.
|
|
147
|
+
*/
|
|
148
|
+
const localPathOf = async (url, cwd) => {
|
|
149
|
+
if (url.startsWith('file://')) {
|
|
150
|
+
try {
|
|
151
|
+
return fileURLToPath(url);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (process.platform === 'win32' && /^[A-Za-z]:[\\/]/.test(url))
|
|
158
|
+
return path.resolve(url);
|
|
159
|
+
const colon = url.indexOf(':');
|
|
160
|
+
const slash = url.search(/[\\/]/);
|
|
161
|
+
if (colon >= 0 && (slash < 0 || colon < slash))
|
|
162
|
+
return undefined;
|
|
163
|
+
return path.resolve(await fs.realpath(cwd).catch(() => cwd), url);
|
|
164
|
+
};
|
|
165
|
+
/** Whether two paths name one directory, symlinks resolved where they exist. */
|
|
166
|
+
const samePath = async (a, b) => {
|
|
167
|
+
const real = (p) => fs.realpath(p).catch(() => path.resolve(p));
|
|
168
|
+
return (await real(a)) === (await real(b));
|
|
169
|
+
};
|
|
142
170
|
/** Shared implementation; call createCanopyServices or createTestCanopyServices. */
|
|
143
171
|
async function _createCanopyServicesInternal(config, options) {
|
|
144
172
|
// Mode-specific requirements, e.g. prod's git bot credentials for GitHub.
|
|
@@ -269,6 +297,10 @@ async function _createCanopyServicesInternal(config, options) {
|
|
|
269
297
|
// stopped on a conflict is discarded by its abort.
|
|
270
298
|
const committed = await withContentWriteLock(options.context.branchRoot, async () => {
|
|
271
299
|
await refuseDuringRebase(options.context.branchRoot);
|
|
300
|
+
// A submit since the caller's read: a branch in review takes no commit until it is withdrawn.
|
|
301
|
+
const branchStatus = (await readBranchMetadataFile(options.context.branchRoot))?.branch.status;
|
|
302
|
+
if (branchStatus !== undefined && branchStatus !== 'editing')
|
|
303
|
+
return false;
|
|
272
304
|
await git.checkoutBranch(options.context.branch.name);
|
|
273
305
|
const status = await git.status();
|
|
274
306
|
const uncommitted = (await readRecordedEditors(options.context))?.uncommittedEditors ?? [];
|
|
@@ -509,6 +541,29 @@ async function _createCanopyServicesInternal(config, options) {
|
|
|
509
541
|
};
|
|
510
542
|
}
|
|
511
543
|
};
|
|
544
|
+
const runBaseUpdateNow = async (context) => {
|
|
545
|
+
const mode = services.config.mode;
|
|
546
|
+
const strategy = operatingStrategy(mode);
|
|
547
|
+
if (!strategy.baseUpdateRunsInRequest()) {
|
|
548
|
+
throw new Error(`CanopyCMS: in ${mode} mode the worker answers requests for the latest changes, never a request.`);
|
|
549
|
+
}
|
|
550
|
+
// baseRoot is the workspace's content-branches directory, where the worker looks.
|
|
551
|
+
const workspacePath = path.dirname(context.baseRoot);
|
|
552
|
+
const origin = await createGitManagerFor(context.branchRoot).getRemoteUrl();
|
|
553
|
+
const originPath = origin && (await localPathOf(origin, context.branchRoot));
|
|
554
|
+
if (!originPath || !(await samePath(originPath, path.join(workspacePath, 'remote.git')))) {
|
|
555
|
+
return 'other-remote';
|
|
556
|
+
}
|
|
557
|
+
// Loaded only when a run is asked for: a server that never runs one never loads the worker.
|
|
558
|
+
const { createLocalWorker } = await import('./worker/cms-worker.js');
|
|
559
|
+
const worker = createLocalWorker({
|
|
560
|
+
workspacePath,
|
|
561
|
+
baseBranch: context.branch.baseBranch ?? baseBranchOf(services.config),
|
|
562
|
+
contentRoot: services.config.contentRoot || 'content',
|
|
563
|
+
settingsBranch: strategy.getSettingsBranchName(services.config),
|
|
564
|
+
});
|
|
565
|
+
return worker.syncBranchNow(context.branch.name);
|
|
566
|
+
};
|
|
512
567
|
const operatingMode = config.mode;
|
|
513
568
|
const modeStrategy = operatingStrategy(operatingMode);
|
|
514
569
|
const registry = modeStrategy.supportsBranching()
|
|
@@ -528,6 +583,7 @@ async function _createCanopyServicesInternal(config, options) {
|
|
|
528
583
|
commitFiles,
|
|
529
584
|
commitSavedWork,
|
|
530
585
|
submitBranch,
|
|
586
|
+
runBaseUpdateNow,
|
|
531
587
|
commitToSettingsBranch,
|
|
532
588
|
getSettingsBranchRoot,
|
|
533
589
|
recordBranchEditor,
|
package/dist/types.d.ts
CHANGED
|
@@ -49,6 +49,8 @@ export interface BranchReview {
|
|
|
49
49
|
};
|
|
50
50
|
/** ISO timestamp of the verdict. */
|
|
51
51
|
at: string;
|
|
52
|
+
/** The head commit the reviewer's page showed, which the API checked against the clone's. */
|
|
53
|
+
commit?: string;
|
|
52
54
|
}
|
|
53
55
|
/** What keeps a pull request behind its base. `base` is the branch GitHub says it targets now. */
|
|
54
56
|
export type PullRequestHold = {
|
|
@@ -108,9 +110,10 @@ export interface BranchMetadata {
|
|
|
108
110
|
*/
|
|
109
111
|
pushedToGitHubAt?: string;
|
|
110
112
|
/**
|
|
111
|
-
* The
|
|
112
|
-
*
|
|
113
|
-
* comment summary with config.editorUrl or this, so a commenter never supplies the
|
|
113
|
+
* The editor URL the latest submit resolved (editor-url.ts): config.editorUrl, else one taken
|
|
114
|
+
* from its request; a submit that resolved none leaves it as it was. A comment change re-renders
|
|
115
|
+
* the PR's comment summary with config.editorUrl or this, so a commenter never supplies the
|
|
116
|
+
* links. The worker links the conflicts status with it (worker/conflicts-status.ts).
|
|
114
117
|
*/
|
|
115
118
|
editorUrl?: string;
|
|
116
119
|
/** Sync status for async GitHub operations (used when Lambda has no internet) */
|
|
@@ -165,6 +168,12 @@ export interface BranchMetadata {
|
|
|
165
168
|
* rebase loop's self-heal pass.
|
|
166
169
|
*/
|
|
167
170
|
historyRewrittenFrom?: string;
|
|
171
|
+
/**
|
|
172
|
+
* The rebased tip of the latest rewrite while `historyRewrittenFrom` is set: the one commit a
|
|
173
|
+
* rewrite's publish moves `remote.git` to, since the clone can hold a commit made after it that
|
|
174
|
+
* no one has submitted. Advanced by each rewrite, cleared with the marker.
|
|
175
|
+
*/
|
|
176
|
+
historyRewrittenTo?: string;
|
|
168
177
|
/**
|
|
169
178
|
* The newest commit this deployment has seen on GitHub's branch, or knows it will hold once a
|
|
170
179
|
* queued push of a rewrite lands: the last one it pushed, took in or published, and any
|
|
@@ -228,6 +237,12 @@ export interface BranchMetadata {
|
|
|
228
237
|
baseUpdateRequestedAt?: string;
|
|
229
238
|
/** The worker's answer to the latest request for the latest changes (worker/rebase.ts). */
|
|
230
239
|
lastBaseUpdate?: LastBaseUpdate;
|
|
240
|
+
/**
|
|
241
|
+
* The first base commit the worker's automatic sync left out because the branch holds saved but
|
|
242
|
+
* uncommitted work, which it never commits (worker/rebase.ts). The editor shows that the latest
|
|
243
|
+
* changes are waiting; cleared once the sync finds the branch holding the base, or in review.
|
|
244
|
+
*/
|
|
245
|
+
latestChangesWaiting?: string;
|
|
231
246
|
}
|
|
232
247
|
/** How a requested update from the base went. */
|
|
233
248
|
export type BaseUpdateOutcome = 'merged' | 'up-to-date' | 'conflicts' | 'failed';
|
|
@@ -334,6 +349,21 @@ export interface UnpublishableBranchesReport {
|
|
|
334
349
|
count: number;
|
|
335
350
|
at: string;
|
|
336
351
|
}
|
|
352
|
+
/**
|
|
353
|
+
* The last git sync's failures to set the "CanopyCMS conflicts" status on pull request heads
|
|
354
|
+
* (worker/conflicts-status.ts): `permission` when GitHub refused the credential (it lacks the Commit
|
|
355
|
+
* statuses permission), `unreachable` when GitHub or the gateway could not be reached or GitHub was
|
|
356
|
+
* busy, `other` for anything else, a gateway refusal or an unreadable conflict record included.
|
|
357
|
+
* `branches` lists at most 50 names, `count` all of them.
|
|
358
|
+
*/
|
|
359
|
+
export interface ConflictsStatusFailure {
|
|
360
|
+
reason: 'permission' | 'unreachable' | 'other';
|
|
361
|
+
/** [REDACT] Served to the admin panel, so sanitized. */
|
|
362
|
+
message: string;
|
|
363
|
+
branches: string[];
|
|
364
|
+
count: number;
|
|
365
|
+
at: string;
|
|
366
|
+
}
|
|
337
367
|
/**
|
|
338
368
|
* Wire shape of the worker's self-reported status file (worker-status.json, under
|
|
339
369
|
* the task queue dir), written by the CmsWorker daemon. Read-only here: GET
|
|
@@ -432,6 +462,8 @@ export interface WorkerStatusReport {
|
|
|
432
462
|
* into a restarted worker's first snapshot, so the bound survives restarts.
|
|
433
463
|
*/
|
|
434
464
|
baseHold?: BaseSchemaHold;
|
|
465
|
+
/** Present while the last git sync failed to set a conflicts status; not carried over a restart. */
|
|
466
|
+
conflictsStatusFailure?: ConflictsStatusFailure;
|
|
435
467
|
/**
|
|
436
468
|
* How the last worker that ran stopped, carried into each new worker's snapshot
|
|
437
469
|
* (task-queue/worker-status.ts's `readCarriedOverStatus`). A worker that fails at startup
|
package/dist/utils/git.d.ts
CHANGED
|
@@ -99,6 +99,13 @@ export declare function stageAllExceptCanopyState(git: SimpleGit): Promise<void>
|
|
|
99
99
|
* for).
|
|
100
100
|
*/
|
|
101
101
|
export declare function isRebaseInProgress(repoPath: string): Promise<boolean>;
|
|
102
|
+
/**
|
|
103
|
+
* The commit HEAD names, read from the files: a detached HEAD's sha, or the branch's loose ref,
|
|
104
|
+
* else its line in `packed-refs`. Null for any other layout, a reftable store among them, which
|
|
105
|
+
* the caller reads with git instead. Files only, as {@link resolveGitDir} is, for the branch
|
|
106
|
+
* listing that reads every clone in review (api/branch.ts).
|
|
107
|
+
*/
|
|
108
|
+
export declare function readHeadCommitFromFiles(repoPath: string): Promise<string | null>;
|
|
102
109
|
/**
|
|
103
110
|
* The repository's current HEAD branch name, or `fallback` when detection fails or HEAD is
|
|
104
111
|
* detached.
|
package/dist/utils/git.js
CHANGED
|
@@ -223,6 +223,41 @@ export async function isRebaseInProgress(repoPath) {
|
|
|
223
223
|
.catch(() => false)));
|
|
224
224
|
return results.some(Boolean);
|
|
225
225
|
}
|
|
226
|
+
const FULL_SHA = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
227
|
+
/**
|
|
228
|
+
* The commit HEAD names, read from the files: a detached HEAD's sha, or the branch's loose ref,
|
|
229
|
+
* else its line in `packed-refs`. Null for any other layout, a reftable store among them, which
|
|
230
|
+
* the caller reads with git instead. Files only, as {@link resolveGitDir} is, for the branch
|
|
231
|
+
* listing that reads every clone in review (api/branch.ts).
|
|
232
|
+
*/
|
|
233
|
+
export async function readHeadCommitFromFiles(repoPath) {
|
|
234
|
+
const gitDir = await resolveGitDir(repoPath);
|
|
235
|
+
if (!gitDir)
|
|
236
|
+
return null;
|
|
237
|
+
try {
|
|
238
|
+
const head = (await fs.readFile(path.join(gitDir, 'HEAD'), 'utf-8')).trim();
|
|
239
|
+
if (FULL_SHA.test(head))
|
|
240
|
+
return head;
|
|
241
|
+
const ref = /^ref: (refs\/heads\/\S+)$/.exec(head)?.[1];
|
|
242
|
+
if (!ref || ref.split('/').includes('..'))
|
|
243
|
+
return null;
|
|
244
|
+
const commonFile = await fs.readFile(path.join(gitDir, 'commondir'), 'utf-8').catch(() => null);
|
|
245
|
+
const refsDir = commonFile ? path.resolve(gitDir, commonFile.trim()) : gitDir;
|
|
246
|
+
const loose = await fs.readFile(path.join(refsDir, ref), 'utf-8').catch(() => null);
|
|
247
|
+
if (loose !== null)
|
|
248
|
+
return FULL_SHA.test(loose.trim()) ? loose.trim() : null;
|
|
249
|
+
const packed = await fs.readFile(path.join(refsDir, 'packed-refs'), 'utf-8').catch(() => '');
|
|
250
|
+
for (const line of packed.split('\n')) {
|
|
251
|
+
const [sha, name] = line.split(' ');
|
|
252
|
+
if (name === ref && FULL_SHA.test(sha))
|
|
253
|
+
return sha;
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
226
261
|
/**
|
|
227
262
|
* The repository's current HEAD branch name, or `fallback` when detection fails or HEAD is
|
|
228
263
|
* detached.
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CANOPYCMS_VERSION = "0.
|
|
1
|
+
export declare const CANOPYCMS_VERSION = "0.13.0";
|
package/dist/version.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Generated by scripts/bump-version.mjs, its only writer. The package builds with plain
|
|
2
2
|
// tsc (rootDir: src), so src/ cannot import ../package.json; version.test.ts keeps the two equal.
|
|
3
|
-
export const CANOPYCMS_VERSION = '0.
|
|
3
|
+
export const CANOPYCMS_VERSION = '0.13.0';
|
|
@@ -67,10 +67,34 @@ export declare function commitMerge(git: SimpleGit, plan: Extract<MergePlan, {
|
|
|
67
67
|
/** Whether `plan` is git's own merge with nothing for a person. */
|
|
68
68
|
export declare const mergesAsGitDoes: (plan: MergePlan) => boolean;
|
|
69
69
|
/**
|
|
70
|
-
* Whether `merged` holds exactly what `from` and `other` merge to cleanly, and so nothing else
|
|
71
|
-
*
|
|
70
|
+
* Whether `merged` holds exactly what `from` and `other` merge to cleanly, and so nothing else, the
|
|
71
|
+
* item keys of entries under `contentRoot` aside: a re-key ({@link buildRekey}) changes no content.
|
|
72
|
+
* A conflict counts as no: whoever settled it chose what `merged` holds.
|
|
72
73
|
*/
|
|
73
|
-
export declare function isCleanMergeOf(git: SimpleGit, merged: string, from: string, other: string): Promise<boolean>;
|
|
74
|
+
export declare function isCleanMergeOf(git: SimpleGit, merged: string, from: string, other: string, contentRoot: string): Promise<boolean>;
|
|
75
|
+
/**
|
|
76
|
+
* A commit on `tip` that gives the items of every entry a merge on the branch's own line in
|
|
77
|
+
* `from..tip` (first parents: never the base's history) brought in the keys the field merge of
|
|
78
|
+
* that merge's two parents gives them, where it holds what git merged
|
|
79
|
+
* (field-merge.ts `rekeyOnly`) and `tip` still holds the merge's version. Git keeps each item under
|
|
80
|
+
* the key its side's copy held, which can name another item once the other side changed it; the
|
|
81
|
+
* field merge's are the keys a later merge reads correctly. Null when no item needs a new key.
|
|
82
|
+
*/
|
|
83
|
+
export declare function buildRekey(git: SimpleGit, clonePath: string, args: {
|
|
84
|
+
from: string;
|
|
85
|
+
tip: string;
|
|
86
|
+
contentRoot: string;
|
|
87
|
+
timeoutMs: number;
|
|
88
|
+
message: string;
|
|
89
|
+
/**
|
|
90
|
+
* Only entries `head` holds as `since` held them: a merge of `head` with the re-key reads an
|
|
91
|
+
* entry `head` changed against `since`'s keys, which git's merge keeps and a re-key would not.
|
|
92
|
+
*/
|
|
93
|
+
unchangedIn?: {
|
|
94
|
+
head: string;
|
|
95
|
+
since: string;
|
|
96
|
+
};
|
|
97
|
+
}): Promise<BuiltMerge | null>;
|
|
74
98
|
/**
|
|
75
99
|
* Move `refs/heads/<branchRef>` in remote.git to `sha` as a plain push, so it can only ever
|
|
76
100
|
* fast-forward. Resolves false when it would not: under the caller's locks, the only writers left
|
|
@@ -7,7 +7,8 @@ import { normalizeFilesystemPath } from '../paths/normalize.js';
|
|
|
7
7
|
import { ROOT_COLLECTION_ID } from '../paths/types.js';
|
|
8
8
|
import { isNonFastForwardRejection } from '../utils/git.js';
|
|
9
9
|
import { getErrorMessage, redactCredentials } from '../utils/error.js';
|
|
10
|
-
import {
|
|
10
|
+
import { parseTypedFilename } from '../utils/typed-filename.js';
|
|
11
|
+
import { changedFiles, resolveFieldMerge, differsInItemKeysOnly, } from './field-merge.js';
|
|
11
12
|
import { runGit } from './git-process.js';
|
|
12
13
|
import { pinnedReceivePack, sharedRepoGit, sharedRepoProcessOptions } from './shared-repo-git.js';
|
|
13
14
|
/**
|
|
@@ -190,10 +191,11 @@ export async function commitMerge(git, plan, { head, other, message }) {
|
|
|
190
191
|
/** Whether `plan` is git's own merge with nothing for a person. */
|
|
191
192
|
export const mergesAsGitDoes = (plan) => plan.kind === 'planned' && plan.asGitDoes;
|
|
192
193
|
/**
|
|
193
|
-
* Whether `merged` holds exactly what `from` and `other` merge to cleanly, and so nothing else
|
|
194
|
-
*
|
|
194
|
+
* Whether `merged` holds exactly what `from` and `other` merge to cleanly, and so nothing else, the
|
|
195
|
+
* item keys of entries under `contentRoot` aside: a re-key ({@link buildRekey}) changes no content.
|
|
196
|
+
* A conflict counts as no: whoever settled it chose what `merged` holds.
|
|
195
197
|
*/
|
|
196
|
-
export async function isCleanMergeOf(git, merged, from, other) {
|
|
198
|
+
export async function isCleanMergeOf(git, merged, from, other, contentRoot) {
|
|
197
199
|
const output = await git.raw([
|
|
198
200
|
'merge-tree',
|
|
199
201
|
'--write-tree',
|
|
@@ -205,7 +207,106 @@ export async function isCleanMergeOf(git, merged, from, other) {
|
|
|
205
207
|
const { tree, stages } = parseMergeTree(output, from, other);
|
|
206
208
|
if (stages.length > 0)
|
|
207
209
|
return false;
|
|
208
|
-
|
|
210
|
+
const mergedTree = (await git.revparse(['--verify', '--end-of-options', `${merged}^{tree}`])).trim();
|
|
211
|
+
if (mergedTree === tree)
|
|
212
|
+
return true;
|
|
213
|
+
for (const { before, after } of await changedFiles(git, tree, mergedTree)) {
|
|
214
|
+
if (!before || !after || !(await differsInItemKeysOnly(git, before, after, contentRoot))) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* A commit on `tip` that gives the items of every entry a merge on the branch's own line in
|
|
222
|
+
* `from..tip` (first parents: never the base's history) brought in the keys the field merge of
|
|
223
|
+
* that merge's two parents gives them, where it holds what git merged
|
|
224
|
+
* (field-merge.ts `rekeyOnly`) and `tip` still holds the merge's version. Git keeps each item under
|
|
225
|
+
* the key its side's copy held, which can name another item once the other side changed it; the
|
|
226
|
+
* field merge's are the keys a later merge reads correctly. Null when no item needs a new key.
|
|
227
|
+
*/
|
|
228
|
+
export async function buildRekey(git, clonePath, args) {
|
|
229
|
+
const { from, tip, contentRoot, timeoutMs, message, unchangedIn } = args;
|
|
230
|
+
const merges = (await git.raw([
|
|
231
|
+
'rev-list',
|
|
232
|
+
'--first-parent',
|
|
233
|
+
'--merges',
|
|
234
|
+
'--reverse',
|
|
235
|
+
'--end-of-options',
|
|
236
|
+
`${from}..${tip}`,
|
|
237
|
+
]))
|
|
238
|
+
.split('\n')
|
|
239
|
+
.filter((line) => OBJECT_ID.test(line));
|
|
240
|
+
if (merges.length === 0)
|
|
241
|
+
return null;
|
|
242
|
+
const flatSchema = await readSchemaSnapshot(clonePath);
|
|
243
|
+
const updates = new Map();
|
|
244
|
+
const keyRemaps = [];
|
|
245
|
+
for (const merge of merges) {
|
|
246
|
+
const parents = (await git.raw(['rev-list', '--parents', '-n', '1', '--end-of-options', merge]))
|
|
247
|
+
.trim()
|
|
248
|
+
.split(' ')
|
|
249
|
+
.slice(1);
|
|
250
|
+
if (parents.length !== 2)
|
|
251
|
+
continue;
|
|
252
|
+
const bases = (await git.raw(['merge-base', '--all', '--end-of-options', ...parents]))
|
|
253
|
+
.split('\n')
|
|
254
|
+
.filter((line) => OBJECT_ID.test(line));
|
|
255
|
+
if (bases.length !== 1)
|
|
256
|
+
continue;
|
|
257
|
+
const field = await resolveFieldMerge({
|
|
258
|
+
git,
|
|
259
|
+
clonePath,
|
|
260
|
+
ancestor: bases[0],
|
|
261
|
+
head: parents[0],
|
|
262
|
+
other: parents[1],
|
|
263
|
+
mergedTree: (await git.revparse(['--verify', '--end-of-options', `${merge}^{tree}`])).trim(),
|
|
264
|
+
conflicted: new Set(),
|
|
265
|
+
contentRoot,
|
|
266
|
+
flatSchema,
|
|
267
|
+
collectionIdOf: (file) => conflictFilesToContentIds([file], contentRoot)[0],
|
|
268
|
+
timeoutMs,
|
|
269
|
+
rekeyOnly: true,
|
|
270
|
+
});
|
|
271
|
+
const applied = new Set();
|
|
272
|
+
for (const [file, blob] of field.updates) {
|
|
273
|
+
// A later commit that changed the entry again decided its keys itself.
|
|
274
|
+
if (!blob || (await blobAt(git, merge, file)) !== (await blobAt(git, tip, file)))
|
|
275
|
+
continue;
|
|
276
|
+
if (unchangedIn &&
|
|
277
|
+
(await blobAt(git, unchangedIn.head, file)) !== (await blobAt(git, unchangedIn.since, file))) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
updates.set(file, blob);
|
|
281
|
+
const id = parseTypedFilename(path.posix.basename(file))?.id;
|
|
282
|
+
if (id !== undefined)
|
|
283
|
+
applied.add(id);
|
|
284
|
+
}
|
|
285
|
+
keyRemaps.push(...field.keyRemaps.filter((r) => r.contentId !== undefined && applied.has(r.contentId)));
|
|
286
|
+
}
|
|
287
|
+
if (updates.size === 0)
|
|
288
|
+
return null;
|
|
289
|
+
const tipTree = (await git.revparse(['--verify', '--end-of-options', `${tip}^{tree}`])).trim();
|
|
290
|
+
const tree = await withUpdates(clonePath, { gitTree: tipTree, updates, timeoutMs });
|
|
291
|
+
const sha = (await git.raw(['commit-tree', '-p', tip, '-m', message, '--end-of-options', tree])).trim();
|
|
292
|
+
return {
|
|
293
|
+
kind: 'built',
|
|
294
|
+
sha,
|
|
295
|
+
gitConflicted: [],
|
|
296
|
+
conflicts: [],
|
|
297
|
+
keptPaths: [],
|
|
298
|
+
asGitDoes: false,
|
|
299
|
+
keyRemaps,
|
|
300
|
+
paths: new Map(),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
/** The blob `commit` holds at `file`, or null for none. */
|
|
304
|
+
async function blobAt(git, commit, file) {
|
|
305
|
+
const out = await git
|
|
306
|
+
.raw(['rev-parse', '--verify', '--quiet', '--end-of-options', `${commit}:${file}`])
|
|
307
|
+
.catch(() => '');
|
|
308
|
+
const oid = out.trim();
|
|
309
|
+
return OBJECT_ID.test(oid) ? oid : null;
|
|
209
310
|
}
|
|
210
311
|
/** `gitTree` with each of `updates` in place, through a throwaway index. */
|
|
211
312
|
async function withUpdates(clonePath, args) {
|
|
@@ -15,8 +15,8 @@ export declare const MAX_PULL_REQUEST_TITLE_LENGTH = 1024;
|
|
|
15
15
|
* @internal Exported for tests.
|
|
16
16
|
*/
|
|
17
17
|
export declare const MAX_PULL_REQUEST_BODY_LENGTH = 65536;
|
|
18
|
-
export type GatewayOperation = 'push' | 'delete' | 'pull-request';
|
|
19
|
-
export type RefusalReason = 'invalid' | 'protected' | 'settings' | 'other-settings' | 'outside-namespace' | 'not-a-commit' | 'too-long';
|
|
18
|
+
export type GatewayOperation = 'push' | 'delete' | 'pull-request' | 'commit-status';
|
|
19
|
+
export type RefusalReason = 'invalid' | 'protected' | 'settings' | 'other-settings' | 'outside-namespace' | 'not-a-commit' | 'too-long' | 'not-head';
|
|
20
20
|
/** The refusals {@link BranchPolicy.assertPushAllowed} makes. */
|
|
21
21
|
export type PushPolicyRefusal = 'protected' | 'settings' | 'other-settings' | 'outside-namespace';
|
|
22
22
|
/**
|
|
@@ -217,3 +217,19 @@ export declare function assertPullRequestText(refusing: string, branch: string,
|
|
|
217
217
|
title?: string;
|
|
218
218
|
body?: string;
|
|
219
219
|
}): void;
|
|
220
|
+
/**
|
|
221
|
+
* The longest commit status description the gateway sends, the length GitHub documents for a
|
|
222
|
+
* deployment status's; its own descriptions are far shorter.
|
|
223
|
+
* @internal Exported for tests.
|
|
224
|
+
*/
|
|
225
|
+
export declare const MAX_COMMIT_STATUS_DESCRIPTION_LENGTH = 140;
|
|
226
|
+
/** The longest commit status target URL the gateway sends, as the editor URL is capped. */
|
|
227
|
+
export declare const MAX_COMMIT_STATUS_URL_LENGTH = 2048;
|
|
228
|
+
/**
|
|
229
|
+
* Refuse a commit status description over the cap or holding a control character, and a target
|
|
230
|
+
* URL that is not plain http(s) or carries credentials. `branch` names the pull request's head.
|
|
231
|
+
*/
|
|
232
|
+
export declare function assertCommitStatusText(refusing: string, branch: string, text: {
|
|
233
|
+
description: string;
|
|
234
|
+
targetUrl?: string;
|
|
235
|
+
}): void;
|
|
@@ -442,6 +442,42 @@ export function assertPullRequestText(refusing, branch, text) {
|
|
|
442
442
|
throw new GatewayRefusalError('pull-request', branch, 'too-long', `${refusing}: ${over}`);
|
|
443
443
|
}
|
|
444
444
|
}
|
|
445
|
+
/**
|
|
446
|
+
* The longest commit status description the gateway sends, the length GitHub documents for a
|
|
447
|
+
* deployment status's; its own descriptions are far shorter.
|
|
448
|
+
* @internal Exported for tests.
|
|
449
|
+
*/
|
|
450
|
+
export const MAX_COMMIT_STATUS_DESCRIPTION_LENGTH = 140;
|
|
451
|
+
/** The longest commit status target URL the gateway sends, as the editor URL is capped. */
|
|
452
|
+
export const MAX_COMMIT_STATUS_URL_LENGTH = 2048;
|
|
453
|
+
/**
|
|
454
|
+
* Refuse a commit status description over the cap or holding a control character, and a target
|
|
455
|
+
* URL that is not plain http(s) or carries credentials. `branch` names the pull request's head.
|
|
456
|
+
*/
|
|
457
|
+
export function assertCommitStatusText(refusing, branch, text) {
|
|
458
|
+
if (text.description.length > MAX_COMMIT_STATUS_DESCRIPTION_LENGTH) {
|
|
459
|
+
throw new GatewayRefusalError('commit-status', branch, 'too-long', `${refusing}: its description is longer than ${MAX_COMMIT_STATUS_DESCRIPTION_LENGTH} characters`);
|
|
460
|
+
}
|
|
461
|
+
// eslint-disable-next-line no-control-regex -- the control characters are what it refuses
|
|
462
|
+
if (/[\u0000-\u001f\u007f]/.test(text.description)) {
|
|
463
|
+
throw new GatewayRefusalError('commit-status', branch, 'invalid', `${refusing}: its description holds a control character`);
|
|
464
|
+
}
|
|
465
|
+
if (text.targetUrl !== undefined && !isPlainWebUrl(text.targetUrl)) {
|
|
466
|
+
throw new GatewayRefusalError('commit-status', branch, 'invalid', `${refusing}: its target URL is not a plain http(s) URL of at most ${MAX_COMMIT_STATUS_URL_LENGTH} characters`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function isPlainWebUrl(raw) {
|
|
470
|
+
if (raw.length > MAX_COMMIT_STATUS_URL_LENGTH)
|
|
471
|
+
return false;
|
|
472
|
+
let url;
|
|
473
|
+
try {
|
|
474
|
+
url = new URL(raw);
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
return (url.protocol === 'https:' || url.protocol === 'http:') && !url.username && !url.password;
|
|
480
|
+
}
|
|
445
481
|
/** Equal, or naming the same workspace directory. */
|
|
446
482
|
function sameBranch(a, b) {
|
|
447
483
|
return a === b || sanitizeBranchName(a) === sanitizeBranchName(b);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type GitHubAuthConfig } from './github-auth.js';
|
|
2
2
|
import { type GitHubGateway } from './github-gateway.js';
|
|
3
3
|
import type { WorkerStatusReport } from '../types.js';
|
|
4
|
+
import { type RebaseSummary } from './rebase.js';
|
|
4
5
|
export { PermanentTaskError, isPermanentTaskFailure } from './task-runner.js';
|
|
5
6
|
export { workerLog, workerLogWarn, workerLogError, installWorkerLogger } from './log.js';
|
|
6
7
|
export { resolveWorkerGitHubAuth, normalizeGitHubAppPrivateKey, DEFAULT_GIT_TOKEN_MINT_TIMEOUT_MS, DEFAULT_GITHUB_TOKEN_REFRESH_MIN_INTERVAL_MS, type GitHubAppAuth, type GitHubAuthConfig, } from './github-auth.js';
|
|
@@ -205,6 +206,7 @@ export declare class CmsWorker {
|
|
|
205
206
|
private log;
|
|
206
207
|
private statusReport?;
|
|
207
208
|
private warnedDivergences;
|
|
209
|
+
private conflictsStatusesSet;
|
|
208
210
|
private branchSyncRequests;
|
|
209
211
|
private gitSyncRequested;
|
|
210
212
|
private wakeGitSync;
|
|
@@ -510,6 +512,13 @@ export declare class CmsWorker {
|
|
|
510
512
|
*/
|
|
511
513
|
protected afterRebaseCompletedForTesting(): Promise<void>;
|
|
512
514
|
private rebaseActiveBranches;
|
|
515
|
+
/**
|
|
516
|
+
* Bring the base into one branch now and answer its request for the latest changes
|
|
517
|
+
* (rebase.ts `syncBranchNow`).
|
|
518
|
+
*/
|
|
519
|
+
syncBranchNow(branchName: string): Promise<'answered' | 'busy'>;
|
|
520
|
+
/** Bring the base into every branch now, answering their requests from this run's start. */
|
|
521
|
+
syncBranchesNow(): Promise<RebaseSummary>;
|
|
513
522
|
syncGit(): Promise<void>;
|
|
514
523
|
private pushSettingsBranches;
|
|
515
524
|
private refreshBaseBranchWorkspace;
|
|
@@ -517,3 +526,15 @@ export declare class CmsWorker {
|
|
|
517
526
|
private pollMergeState;
|
|
518
527
|
refreshAuthCache(): Promise<void>;
|
|
519
528
|
}
|
|
529
|
+
/**
|
|
530
|
+
* A worker over a workspace with no GitHub, for git steps run where no worker loop runs: dev's
|
|
531
|
+
* "Get latest changes" (services.ts `runBaseUpdateNow`) and the test app's e2e sync route. It is
|
|
532
|
+
* never started, so it takes no worker lock. Its GitHub settings are placeholders: a sync asks
|
|
533
|
+
* GitHub only about a branch's pull request, which dev never opens.
|
|
534
|
+
*/
|
|
535
|
+
export declare function createLocalWorker(options: {
|
|
536
|
+
workspacePath: string;
|
|
537
|
+
baseBranch: string;
|
|
538
|
+
contentRoot: string;
|
|
539
|
+
settingsBranch?: string;
|
|
540
|
+
}): CmsWorker;
|
|
@@ -20,7 +20,7 @@ import { DEFAULT_SCHEMA_HOLD_MAX_MS, readCarriedBaseHold } from './schema-gate.j
|
|
|
20
20
|
import { readCarriedGatewayPolicy, readCarriedUnpublishableBranches, } from './unpublishable-branches.js';
|
|
21
21
|
import { SharedRepoRefusalError, UntrustedRepoConfigError, assertSharedRepoConfig, sharedRepoGit, } from './shared-repo-git.js';
|
|
22
22
|
import { executeTask, orphanRecoveryMaxAgeMs, processTaskQueue, pushBranchToGitHub, updateBranchMetadata, } from './task-runner.js';
|
|
23
|
-
import { pollMergeState, runRebaseCycle } from './rebase.js';
|
|
23
|
+
import { pollMergeState, runRebaseCycle, syncBranchNow } from './rebase.js';
|
|
24
24
|
import { cleanupTrashedBranchDirs, pushSettingsBranches, refreshBaseBranchWorkspace, syncGit, } from './git-sync.js';
|
|
25
25
|
// Re-exported because this module is the package's advertised worker
|
|
26
26
|
// entrypoint (`canopycms/worker/cms-worker`).
|
|
@@ -246,6 +246,7 @@ export class CmsWorker {
|
|
|
246
246
|
this.releaseLockFn = null;
|
|
247
247
|
this.log = cmsTaskQueueLogger;
|
|
248
248
|
this.warnedDivergences = new Map();
|
|
249
|
+
this.conflictsStatusesSet = new Map();
|
|
249
250
|
// Branches asked for since the git sync last took them (requestBranchSync), by directory name.
|
|
250
251
|
this.branchSyncRequests = new Set();
|
|
251
252
|
// Set by any request; cleared by the next cycle, which takes it.
|
|
@@ -365,6 +366,7 @@ export class CmsWorker {
|
|
|
365
366
|
ensureStatusReport: () => this.ensureStatusReport(),
|
|
366
367
|
ensureSettingsBranch: () => this.ensureSettingsBranch(),
|
|
367
368
|
warnedDivergences: () => this.warnedDivergences,
|
|
369
|
+
conflictsStatusesSet: () => this.conflictsStatusesSet,
|
|
368
370
|
requestBranchSync: (branchRef) => this.requestBranchSync(branchRef),
|
|
369
371
|
takeBranchSyncRequests: () => this.takeBranchSyncRequests(),
|
|
370
372
|
afterConflictDetectedForTesting: () => this.afterConflictDetectedForTesting(),
|
|
@@ -1305,12 +1307,21 @@ export class CmsWorker {
|
|
|
1305
1307
|
// directly and that calls `pollMergeState` directly. Neither is on
|
|
1306
1308
|
// WorkerContext, so a stub installed on either is a no-op for production.
|
|
1307
1309
|
//
|
|
1308
|
-
// `rebaseActiveBranches` also has a consumer outside the test files:
|
|
1309
|
-
// apps/test-app/app/api/e2e-test/rebase/route.ts drives it from an e2e
|
|
1310
|
-
// fixture route. Do not delete it as unused.
|
|
1311
1310
|
async rebaseActiveBranches(gatewayPolicy, gitHub, requested) {
|
|
1312
1311
|
return runRebaseCycle(this.ctx(), gatewayPolicy, gitHub, requested);
|
|
1313
1312
|
}
|
|
1313
|
+
// --- Runs with no loop ({@link createLocalWorker}) -----------------------
|
|
1314
|
+
/**
|
|
1315
|
+
* Bring the base into one branch now and answer its request for the latest changes
|
|
1316
|
+
* (rebase.ts `syncBranchNow`).
|
|
1317
|
+
*/
|
|
1318
|
+
async syncBranchNow(branchName) {
|
|
1319
|
+
return syncBranchNow(this.ctx(), branchName);
|
|
1320
|
+
}
|
|
1321
|
+
/** Bring the base into every branch now, answering their requests from this run's start. */
|
|
1322
|
+
async syncBranchesNow() {
|
|
1323
|
+
return runRebaseCycle(this.ctx(), undefined, undefined, new Set(), { fetchedAt: Date.now() }, true);
|
|
1324
|
+
}
|
|
1314
1325
|
// --- Git-sync cluster (worker/git-sync.ts) -----------------------------
|
|
1315
1326
|
//
|
|
1316
1327
|
// `syncGit` is the public loop entry `scheduleLoop` drives. The three private
|
|
@@ -1349,3 +1360,17 @@ export class CmsWorker {
|
|
|
1349
1360
|
}
|
|
1350
1361
|
}
|
|
1351
1362
|
}
|
|
1363
|
+
/**
|
|
1364
|
+
* A worker over a workspace with no GitHub, for git steps run where no worker loop runs: dev's
|
|
1365
|
+
* "Get latest changes" (services.ts `runBaseUpdateNow`) and the test app's e2e sync route. It is
|
|
1366
|
+
* never started, so it takes no worker lock. Its GitHub settings are placeholders: a sync asks
|
|
1367
|
+
* GitHub only about a branch's pull request, which dev never opens.
|
|
1368
|
+
*/
|
|
1369
|
+
export function createLocalWorker(options) {
|
|
1370
|
+
return new CmsWorker({
|
|
1371
|
+
...options,
|
|
1372
|
+
githubOwner: 'local',
|
|
1373
|
+
githubRepo: 'local',
|
|
1374
|
+
githubToken: 'local',
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ConflictsStatusFailure } from '../types.js';
|
|
2
|
+
import type { GitHubConflictsStatus } from './github-gateway.js';
|
|
3
|
+
import type { WorkerContext } from './worker-context.js';
|
|
4
|
+
export type ConflictsStatusContext = Pick<WorkerContext, 'contentBranchesPath' | 'sanitizedBaseBranch' | 'github' | 'conflictsStatusesSet' | 'shutdownSignal' | 'taskTimeoutMs' | 'requestBranchSync'>;
|
|
5
|
+
/** The state and description a branch's conflict record calls for. */
|
|
6
|
+
type ConflictsStatusText = Pick<GitHubConflictsStatus, 'state' | 'description'>;
|
|
7
|
+
/**
|
|
8
|
+
* What `branchRoot`'s conflict record calls for: failure with the number of choices open, or while
|
|
9
|
+
* the record is held after a damaged one was set aside, or does not parse; success otherwise.
|
|
10
|
+
* @internal Exported for tests.
|
|
11
|
+
*/
|
|
12
|
+
export declare function conflictsStatusOf(branchRoot: string): Promise<ConflictsStatusText>;
|
|
13
|
+
/**
|
|
14
|
+
* Set the conflicts status on every pull request whose head, as `githubTips` (this cycle's GitHub
|
|
15
|
+
* fetch) holds it, lacks the one its branch calls for. Returns the failure to report, undefined
|
|
16
|
+
* when every status was set, or null when the stage could not say: the branches could not be
|
|
17
|
+
* listed, or a drain cut it off. Never throws.
|
|
18
|
+
*/
|
|
19
|
+
export declare function setConflictsStatuses(ctx: ConflictsStatusContext, githubTips: ReadonlyMap<string, string>): Promise<ConflictsStatusFailure | undefined | null>;
|
|
20
|
+
export {};
|