canopycms 0.0.61 → 0.0.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/json-to-markdown.js +1 -2
- package/dist/ai/json-to-markdown.js.map +1 -1
- package/dist/api/__test__/mock-client.d.ts +5 -1
- package/dist/api/__test__/mock-client.d.ts.map +1 -1
- package/dist/api/__test__/mock-client.js +9 -2
- package/dist/api/__test__/mock-client.js.map +1 -1
- package/dist/api/admin-branch-health.d.ts +36 -1
- package/dist/api/admin-branch-health.d.ts.map +1 -1
- package/dist/api/admin-branch-health.js +176 -2
- package/dist/api/admin-branch-health.js.map +1 -1
- package/dist/api/admin.d.ts +8 -1
- package/dist/api/admin.d.ts.map +1 -1
- package/dist/api/admin.js.map +1 -1
- package/dist/api/branch.d.ts.map +1 -1
- package/dist/api/branch.js +16 -2
- package/dist/api/branch.js.map +1 -1
- package/dist/api/client.d.ts +5 -1
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +6 -0
- package/dist/api/client.js.map +1 -1
- package/dist/api/comments.d.ts +2 -2
- package/dist/api/content.d.ts +11 -5
- package/dist/api/content.d.ts.map +1 -1
- package/dist/api/content.js +79 -6
- package/dist/api/content.js.map +1 -1
- package/dist/api/entries.d.ts +10 -0
- package/dist/api/entries.d.ts.map +1 -1
- package/dist/api/entries.js +40 -15
- package/dist/api/entries.js.map +1 -1
- package/dist/api/schema.d.ts +4 -4
- package/dist/api/schema.d.ts.map +1 -1
- package/dist/api/schema.js +13 -13
- package/dist/api/schema.js.map +1 -1
- package/dist/assets/transform.d.ts.map +1 -1
- package/dist/assets/transform.js +19 -6
- package/dist/assets/transform.js.map +1 -1
- package/dist/branch-health.d.ts +13 -0
- package/dist/branch-health.d.ts.map +1 -1
- package/dist/branch-health.js +23 -0
- package/dist/branch-health.js.map +1 -1
- package/dist/cli/cli.js +858 -537
- package/dist/cli/generate-ai-content.js +760 -455
- package/dist/cli/init.js +25 -7
- package/dist/cli/template-files/Dockerfile.cms.template +19 -1
- package/dist/cli/template-files/canopycms.config.ts.template +7 -0
- package/dist/cli/template-files/cms-stack.ts.template +7 -1
- package/dist/client.d.ts +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js.map +1 -1
- package/dist/config/schemas/field.d.ts +15 -15
- package/dist/config/types.d.ts +2 -2
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +0 -1
- package/dist/config/types.js.map +1 -1
- package/dist/config/validation.d.ts +9 -0
- package/dist/config/validation.d.ts.map +1 -1
- package/dist/config/validation.js +14 -0
- package/dist/config/validation.js.map +1 -1
- package/dist/content-id-index.d.ts +97 -5
- package/dist/content-id-index.d.ts.map +1 -1
- package/dist/content-id-index.js +170 -18
- package/dist/content-id-index.js.map +1 -1
- package/dist/content-store.d.ts +81 -7
- package/dist/content-store.d.ts.map +1 -1
- package/dist/content-store.js +443 -258
- package/dist/content-store.js.map +1 -1
- package/dist/editor/BranchManager.d.ts.map +1 -1
- package/dist/editor/BranchManager.js +18 -6
- package/dist/editor/BranchManager.js.map +1 -1
- package/dist/editor/CanopyEditor.d.ts.map +1 -1
- package/dist/editor/CanopyEditor.js +2 -2
- package/dist/editor/CanopyEditor.js.map +1 -1
- package/dist/editor/CanopyEditorPage.d.ts +9 -1
- package/dist/editor/CanopyEditorPage.d.ts.map +1 -1
- package/dist/editor/CanopyEditorPage.js +9 -2
- package/dist/editor/CanopyEditorPage.js.map +1 -1
- package/dist/editor/Editor.d.ts +9 -1
- package/dist/editor/Editor.d.ts.map +1 -1
- package/dist/editor/Editor.js +58 -24
- package/dist/editor/Editor.js.map +1 -1
- package/dist/editor/FormRenderer.d.ts.map +1 -1
- package/dist/editor/FormRenderer.js +12 -0
- package/dist/editor/FormRenderer.js.map +1 -1
- package/dist/editor/admin/SystemHealthPanel.js +6 -2
- package/dist/editor/admin/SystemHealthPanel.js.map +1 -1
- package/dist/editor/components/EntryCreateModal.d.ts +9 -1
- package/dist/editor/components/EntryCreateModal.d.ts.map +1 -1
- package/dist/editor/components/EntryCreateModal.js +4 -1
- package/dist/editor/components/EntryCreateModal.js.map +1 -1
- package/dist/editor/fields/DateTimeField.d.ts +46 -0
- package/dist/editor/fields/DateTimeField.d.ts.map +1 -0
- package/dist/editor/fields/DateTimeField.js +66 -0
- package/dist/editor/fields/DateTimeField.js.map +1 -0
- package/dist/editor/fields/NumberField.d.ts +34 -0
- package/dist/editor/fields/NumberField.d.ts.map +1 -0
- package/dist/editor/fields/NumberField.js +53 -0
- package/dist/editor/fields/NumberField.js.map +1 -0
- package/dist/editor/fields/NumberListField.d.ts +25 -0
- package/dist/editor/fields/NumberListField.d.ts.map +1 -0
- package/dist/editor/fields/NumberListField.js +53 -0
- package/dist/editor/fields/NumberListField.js.map +1 -0
- package/dist/editor/hooks/useBranchManager.d.ts.map +1 -1
- package/dist/editor/hooks/useBranchManager.js +115 -26
- package/dist/editor/hooks/useBranchManager.js.map +1 -1
- package/dist/editor/hooks/useDraftManager.d.ts +8 -0
- package/dist/editor/hooks/useDraftManager.d.ts.map +1 -1
- package/dist/editor/hooks/useDraftManager.js +261 -21
- package/dist/editor/hooks/useDraftManager.js.map +1 -1
- package/dist/editor/hooks/useEntryManager.d.ts +21 -0
- package/dist/editor/hooks/useEntryManager.d.ts.map +1 -1
- package/dist/editor/hooks/useEntryManager.js +25 -2
- package/dist/editor/hooks/useEntryManager.js.map +1 -1
- package/dist/editor/theme.d.ts +8 -8
- package/dist/git-manager.d.ts +51 -7
- package/dist/git-manager.d.ts.map +1 -1
- package/dist/git-manager.js +105 -14
- package/dist/git-manager.js.map +1 -1
- package/dist/http/handler.d.ts.map +1 -1
- package/dist/http/handler.js +70 -27
- package/dist/http/handler.js.map +1 -1
- package/dist/http/router.d.ts.map +1 -1
- package/dist/http/router.js +40 -3
- package/dist/http/router.js.map +1 -1
- package/dist/operating-mode/deployment-name-fixtures.d.ts +29 -0
- package/dist/operating-mode/deployment-name-fixtures.d.ts.map +1 -0
- package/dist/operating-mode/deployment-name-fixtures.js +55 -0
- package/dist/operating-mode/deployment-name-fixtures.js.map +1 -0
- package/dist/operating-mode/deployment-name.d.ts +9 -0
- package/dist/operating-mode/deployment-name.d.ts.map +1 -1
- package/dist/operating-mode/deployment-name.js +9 -1
- package/dist/operating-mode/deployment-name.js.map +1 -1
- package/dist/operating-mode/index.d.ts +2 -1
- package/dist/operating-mode/index.d.ts.map +1 -1
- package/dist/operating-mode/index.js +4 -1
- package/dist/operating-mode/index.js.map +1 -1
- package/dist/operating-mode/mode-env.d.ts +58 -0
- package/dist/operating-mode/mode-env.d.ts.map +1 -0
- package/dist/operating-mode/mode-env.js +97 -0
- package/dist/operating-mode/mode-env.js.map +1 -0
- package/dist/resolve-canopy-user.d.ts +34 -0
- package/dist/resolve-canopy-user.d.ts.map +1 -0
- package/dist/resolve-canopy-user.js +55 -0
- package/dist/resolve-canopy-user.js.map +1 -0
- package/dist/schema/schema-store.d.ts +53 -2
- package/dist/schema/schema-store.d.ts.map +1 -1
- package/dist/schema/schema-store.js +92 -21
- package/dist/schema/schema-store.js.map +1 -1
- package/dist/server.d.ts +9 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +9 -0
- package/dist/server.js.map +1 -1
- package/dist/services.d.ts.map +1 -1
- package/dist/services.js +40 -6
- package/dist/services.js.map +1 -1
- package/dist/settings-workspace.d.ts +40 -0
- package/dist/settings-workspace.d.ts.map +1 -1
- package/dist/settings-workspace.js +139 -151
- package/dist/settings-workspace.js.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/content-write-lock.d.ts +127 -0
- package/dist/utils/content-write-lock.d.ts.map +1 -0
- package/dist/utils/content-write-lock.js +170 -0
- package/dist/utils/content-write-lock.js.map +1 -0
- package/dist/utils/error.d.ts +1 -1
- package/dist/utils/error.js +1 -1
- package/dist/utils/git.d.ts +16 -0
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +22 -0
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/sanitize-href.d.ts +28 -3
- package/dist/utils/sanitize-href.d.ts.map +1 -1
- package/dist/utils/sanitize-href.js +64 -6
- package/dist/utils/sanitize-href.js.map +1 -1
- package/dist/validation/entry-link-validator.d.ts +1 -1
- package/dist/validation/entry-link-validator.js +3 -3
- package/dist/validation/entry-link-validator.js.map +1 -1
- package/dist/validation/entry-validator.js +1 -1
- package/dist/validation/entry-validator.js.map +1 -1
- package/dist/worker/cms-worker.d.ts +13 -0
- package/dist/worker/cms-worker.d.ts.map +1 -1
- package/dist/worker/cms-worker.js +326 -264
- package/dist/worker/cms-worker.js.map +1 -1
- package/package.json +2 -2
|
@@ -12,6 +12,7 @@ import { sanitizeBranchName, RESERVED_SETTINGS_BRANCH_PREFIX } from '../paths/br
|
|
|
12
12
|
import { normalizeFilesystemPath } from '../paths/normalize.js';
|
|
13
13
|
import { GITHUB_TRACKING_REF_PREFIX, gitChildEnv, gitNetworkChildEnv } from '../git-manager.js';
|
|
14
14
|
import { resolveDeploymentName } from '../operating-mode/deployment-name.js';
|
|
15
|
+
import { tryAcquireContentWriteLock } from '../utils/content-write-lock.js';
|
|
15
16
|
import { getErrorMessage, isNodeError, redactCredentials } from '../utils/error.js';
|
|
16
17
|
import { isNonFastForwardRejection, isStaleLeaseRejection } from '../utils/git.js';
|
|
17
18
|
import { writeWorkerStatus } from './worker-status.js';
|
|
@@ -1465,6 +1466,7 @@ export class CmsWorker {
|
|
|
1465
1466
|
durationMs: Date.now() - cycleStartedAt,
|
|
1466
1467
|
rebased: rebaseSummary.rebased,
|
|
1467
1468
|
skippedDirty: rebaseSummary.skippedDirty,
|
|
1469
|
+
skippedLocked: rebaseSummary.skippedLocked,
|
|
1468
1470
|
failed: rebaseSummary.failed,
|
|
1469
1471
|
tracked: trackedSummary,
|
|
1470
1472
|
};
|
|
@@ -1765,19 +1767,33 @@ export class CmsWorker {
|
|
|
1765
1767
|
workerLogWarn(` Failed to record rebase failure for ${branchDir}: ${getErrorMessage(err)}`);
|
|
1766
1768
|
}
|
|
1767
1769
|
}
|
|
1770
|
+
/**
|
|
1771
|
+
* Test-only seam: awaited inside `rebaseActiveBranches()`'s conflict round,
|
|
1772
|
+
* after `git rebase` reported conflicted files and BEFORE the
|
|
1773
|
+
* `checkout --theirs` resolution loop overwrites them. No-op in production.
|
|
1774
|
+
*
|
|
1775
|
+
* A test subclass overrides this to land a real `ContentStore` write at
|
|
1776
|
+
* exactly the instant the rebase is mid-flight -- the window the old TOCTOU
|
|
1777
|
+
* comment above the dirty check wrongly called safe -- without any sleeps or
|
|
1778
|
+
* shell rendezvous. See the "Deterministic interleavings" testing pattern in
|
|
1779
|
+
* docs/concurrency.md, and `ContentStore.afterPrePassForTesting()` for the
|
|
1780
|
+
* same idiom on the write side.
|
|
1781
|
+
*/
|
|
1782
|
+
async afterConflictDetectedForTesting() { }
|
|
1768
1783
|
async rebaseActiveBranches() {
|
|
1769
1784
|
// PR-W1: collected across the loop below and returned as a summary
|
|
1770
1785
|
// (folded into worker-status.json by syncGit()). Purely additive
|
|
1771
1786
|
// bookkeeping -- doesn't change any control flow or existing logging.
|
|
1772
1787
|
const rebased = [];
|
|
1773
1788
|
const skippedDirty = [];
|
|
1789
|
+
const skippedLocked = [];
|
|
1774
1790
|
const failed = [];
|
|
1775
1791
|
let branchDirs;
|
|
1776
1792
|
try {
|
|
1777
1793
|
branchDirs = await fs.readdir(this.contentBranchesPath);
|
|
1778
1794
|
}
|
|
1779
1795
|
catch {
|
|
1780
|
-
return { rebased, skippedDirty, failed };
|
|
1796
|
+
return { rebased, skippedDirty, skippedLocked, failed };
|
|
1781
1797
|
}
|
|
1782
1798
|
for (const branchDir of branchDirs) {
|
|
1783
1799
|
// Known structural entries under content-branches/, not branch
|
|
@@ -1839,285 +1855,331 @@ export class CmsWorker {
|
|
|
1839
1855
|
config: ['core.editor=true'],
|
|
1840
1856
|
unsafe: { allowUnsafeEditor: true },
|
|
1841
1857
|
});
|
|
1842
|
-
//
|
|
1843
|
-
//
|
|
1844
|
-
//
|
|
1845
|
-
//
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1858
|
+
// [SYNC-C1] Take the branch's cross-host content-write lock BEFORE the
|
|
1859
|
+
// dirty check, and hold it for the whole rebase.
|
|
1860
|
+
//
|
|
1861
|
+
// The dirty check alone is check-then-act. The old comment here claimed
|
|
1862
|
+
// the residual window was safe ("the rebase will fail and the catch
|
|
1863
|
+
// block will abort safely"), which only holds for a save landing before
|
|
1864
|
+
// `git rebase` STARTS. After that -- a window spanning fetch, replay and
|
|
1865
|
+
// N conflict rounds of awaited git subprocesses on EFS -- a save is
|
|
1866
|
+
// destroyed two ways: `checkout --theirs` below overwrites the
|
|
1867
|
+
// just-saved file with the branch's committed version and the rebase
|
|
1868
|
+
// then SUCCEEDS (nothing logs a failure at all), and `rebase --abort`
|
|
1869
|
+
// hard-resets the tree. The editor already got its 200 either way.
|
|
1870
|
+
//
|
|
1871
|
+
// Zero-retry acquisition, deliberately: on contention this branch is
|
|
1872
|
+
// skipped and retried on the next sync cycle (~5 min), which is the
|
|
1873
|
+
// same principle as the skip-dirty-branches behavior below. Writers get
|
|
1874
|
+
// the patient side of the asymmetry -- see utils/content-write-lock.ts.
|
|
1875
|
+
//
|
|
1876
|
+
// The heartbeat that keeps this lock fresh (proper-lockfile refreshes
|
|
1877
|
+
// every `stale`/2 = 15s) is a timer on this event loop; every git step
|
|
1878
|
+
// below is an awaited subprocess, never a synchronous block, so the
|
|
1879
|
+
// refresh keeps firing for the whole hold.
|
|
1880
|
+
let releaseContentLock;
|
|
1881
|
+
try {
|
|
1882
|
+
releaseContentLock = await tryAcquireContentWriteLock(branchPath);
|
|
1851
1883
|
}
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
// sequence below leaves the marker set with the work unfinished, and
|
|
1862
|
-
// this is what completes it -- without it, one lost lease race would
|
|
1863
|
-
// strand the branch until the base branch happened to advance again.
|
|
1864
|
-
// Gated on the loop-top snapshot, so unmarked branches (nearly all
|
|
1865
|
-
// of them, every cycle) cost nothing extra.
|
|
1866
|
-
if (canPublish && metaFile?.branch.historyRewrittenFrom) {
|
|
1867
|
-
await this.reconcilePendingRewrite({
|
|
1868
|
-
branchPath,
|
|
1869
|
-
branchDir,
|
|
1870
|
-
branchRef,
|
|
1871
|
-
headSha: (await branchGit.revparse(['HEAD'])).trim(),
|
|
1872
|
-
marker: metaFile.branch.historyRewrittenFrom,
|
|
1873
|
-
});
|
|
1884
|
+
catch (lockErr) {
|
|
1885
|
+
if (isNodeError(lockErr) && lockErr.code === 'ELOCKED') {
|
|
1886
|
+
workerLog(` Skipping ${branchDir}: content write in progress (retrying next cycle)`);
|
|
1887
|
+
skippedLocked.push(branchDir);
|
|
1888
|
+
continue;
|
|
1889
|
+
}
|
|
1890
|
+
// Anything else (ENOENT on a branch dir deleted mid-cycle, EACCES,
|
|
1891
|
+
// ...) is a real failure: let the outer catch record it.
|
|
1892
|
+
throw lockErr;
|
|
1874
1893
|
}
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
//
|
|
1889
|
-
//
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
//
|
|
1893
|
-
//
|
|
1894
|
-
//
|
|
1895
|
-
//
|
|
1896
|
-
//
|
|
1897
|
-
//
|
|
1898
|
-
//
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1894
|
+
try {
|
|
1895
|
+
// Skip dirty branches — editor has unsaved changes that can't be rebased.
|
|
1896
|
+
// Now inside the lock, so no write can land between this check and the
|
|
1897
|
+
// rebase below.
|
|
1898
|
+
const dirtyCheck = await branchGit.status();
|
|
1899
|
+
if (dirtyCheck.files.length > 0) {
|
|
1900
|
+
workerLog(` Skipping ${branchDir}: has uncommitted changes`);
|
|
1901
|
+
skippedDirty.push(branchDir);
|
|
1902
|
+
continue;
|
|
1903
|
+
}
|
|
1904
|
+
// The clone's own ref name: branchDir is the sanitized DIRECTORY
|
|
1905
|
+
// name and need not match it. A literal 'HEAD' means a detached
|
|
1906
|
+
// clone (e.g. a crashed rebase left one behind) -- nothing below can
|
|
1907
|
+
// safely name a ref then, so every publish path stays disarmed,
|
|
1908
|
+
// which is the safe direction.
|
|
1909
|
+
const branchRef = (await branchGit.revparse(['--abbrev-ref', 'HEAD'])).trim();
|
|
1910
|
+
const canPublish = branchRef.length > 0 && branchRef !== 'HEAD';
|
|
1911
|
+
// [SYNC-H1] Self-heal: finish an interrupted publish even when this
|
|
1912
|
+
// branch is not behind base. Every crash window in the arming
|
|
1913
|
+
// sequence below leaves the marker set with the work unfinished, and
|
|
1914
|
+
// this is what completes it -- without it, one lost lease race would
|
|
1915
|
+
// strand the branch until the base branch happened to advance again.
|
|
1916
|
+
// Gated on the loop-top snapshot, so unmarked branches (nearly all
|
|
1917
|
+
// of them, every cycle) cost nothing extra.
|
|
1918
|
+
if (canPublish && metaFile?.branch.historyRewrittenFrom) {
|
|
1919
|
+
await this.reconcilePendingRewrite({
|
|
1920
|
+
branchPath,
|
|
1921
|
+
branchDir,
|
|
1922
|
+
branchRef,
|
|
1923
|
+
headSha: (await branchGit.revparse(['HEAD'])).trim(),
|
|
1924
|
+
marker: metaFile.branch.historyRewrittenFrom,
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
await branchGit.fetch('origin', this.baseBranch);
|
|
1928
|
+
// Use rev-list instead of status.behind — status.behind only works when the
|
|
1929
|
+
// branch has an upstream tracking branch configured, which isn't guaranteed
|
|
1930
|
+
// (checkoutBranch fallback paths create branches without --track).
|
|
1931
|
+
// The just-fetched tip, not origin/<base>: branch clones are
|
|
1932
|
+
// --single-branch, so no remote-tracking ref exists for a base branch
|
|
1933
|
+
// other than the one they were cloned from (see the base-refresh
|
|
1934
|
+
// comment above). Pinned to a SHA immediately — FETCH_HEAD is one
|
|
1935
|
+
// shared mutable file per repo, repointed by any concurrent fetch.
|
|
1936
|
+
const fetchedBaseTip = (await branchGit.revparse(['FETCH_HEAD'])).trim();
|
|
1937
|
+
const behindCount = parseInt((await branchGit.raw(['rev-list', '--count', `HEAD..${fetchedBaseTip}`])).trim(), 10);
|
|
1938
|
+
const meta = getBranchMetadataFileManager(branchPath, this.contentBranchesPath);
|
|
1939
|
+
if (behindCount === 0) {
|
|
1940
|
+
// Already in sync. This is the overwhelmingly common outcome per
|
|
1941
|
+
// branch per cycle (most branches are caught up most of the
|
|
1942
|
+
// time), so skip the save entirely when metadata already reflects
|
|
1943
|
+
// a clean state -- every save() now eager-regenerates the branch
|
|
1944
|
+
// registry (branch-metadata.ts's invalidateRegistry(), O(branch
|
|
1945
|
+
// count) fs reads on EFS), so an unconditional save here turns
|
|
1946
|
+
// every rebase cycle into O(N^2) registry work across N branches
|
|
1947
|
+
// for what is otherwise a true no-op. Re-load fresh (not the
|
|
1948
|
+
// `metaFile` snapshot from before the fetch/rev-list above) so a
|
|
1949
|
+
// concurrent editor-driven metadata change during that window
|
|
1950
|
+
// isn't clobbered by a stale skip decision.
|
|
1951
|
+
const currentMeta = await BranchMetadataFileManager.loadOnly(branchPath);
|
|
1952
|
+
const conflictStatus = currentMeta?.branch.conflictStatus;
|
|
1953
|
+
const conflictFiles = currentMeta?.branch.conflictFiles;
|
|
1954
|
+
const conflictAlreadyClean = (conflictStatus === undefined || conflictStatus === 'clean') &&
|
|
1955
|
+
(conflictFiles === undefined || conflictFiles.length === 0);
|
|
1956
|
+
// PR-W2: a lingering rebaseFailure must also be cleared once the
|
|
1957
|
+
// branch catches up clean -- otherwise it sticks as a stale
|
|
1958
|
+
// warning forever (nothing else touches this branch once it's
|
|
1959
|
+
// caught up, so no other save site would ever clear it).
|
|
1960
|
+
const alreadyClean = conflictAlreadyClean && currentMeta?.branch.rebaseFailure === undefined;
|
|
1961
|
+
if (alreadyClean) {
|
|
1962
|
+
continue;
|
|
1963
|
+
}
|
|
1964
|
+
await meta.save({
|
|
1965
|
+
branch: {
|
|
1966
|
+
name: branchDir,
|
|
1967
|
+
conflictStatus: 'clean',
|
|
1968
|
+
conflictFiles: [],
|
|
1969
|
+
rebaseFailure: undefined,
|
|
1970
|
+
},
|
|
1971
|
+
});
|
|
1972
|
+
continue;
|
|
1973
|
+
}
|
|
1974
|
+
workerLog(`Rebasing ${branchDir} (${behindCount} commits behind)...`);
|
|
1975
|
+
// Read BOTH sides before rewriting anything: the arming guard after
|
|
1976
|
+
// the rebase compares what remote.git published against what this
|
|
1977
|
+
// clone is about to rebase away. Reading them afterwards would be
|
|
1978
|
+
// useless -- the clone's pre-rebase tip is exactly what disappears.
|
|
1979
|
+
const preRebaseHead = (await branchGit.revparse(['HEAD'])).trim();
|
|
1980
|
+
const publishedSha = canPublish ? await this.readPublishedSha(branchRef) : null;
|
|
1981
|
+
// Resolve-and-continue loop: keep branch version for conflicting files, then continue
|
|
1982
|
+
// Non-conflicting files get main's changes; conflicting files keep branch version.
|
|
1983
|
+
const conflictedFiles = [];
|
|
1984
|
+
let nextAction = 'start';
|
|
1985
|
+
let completed = false;
|
|
1986
|
+
// PR-W1: captured only on the "unexpected error" exit below, for the
|
|
1987
|
+
// failed-summary entry pushed at the `if (!completed)` check.
|
|
1988
|
+
let failureReason;
|
|
1989
|
+
const MAX_ROUNDS = 50; // safety limit against infinite loops
|
|
1990
|
+
for (let round = 0; round < MAX_ROUNDS && !completed; round++) {
|
|
1991
|
+
try {
|
|
1992
|
+
if (nextAction === 'start') {
|
|
1993
|
+
// The pinned base tip fetched above (single-branch clones have
|
|
1994
|
+
// no origin/<base> remote-tracking ref for other branches).
|
|
1995
|
+
await branchGit.rebase([fetchedBaseTip]);
|
|
1996
|
+
}
|
|
1997
|
+
else if (nextAction === 'continue') {
|
|
1998
|
+
await branchGit.rebase(['--continue']);
|
|
1999
|
+
}
|
|
2000
|
+
else {
|
|
2001
|
+
await branchGit.rebase(['--skip']);
|
|
2002
|
+
}
|
|
2003
|
+
completed = true;
|
|
2004
|
+
}
|
|
2005
|
+
catch (rebaseErr) {
|
|
2006
|
+
nextAction = 'continue';
|
|
2007
|
+
const st = await branchGit.status();
|
|
2008
|
+
if (st.conflicted.length > 0) {
|
|
2009
|
+
await this.afterConflictDetectedForTesting();
|
|
2010
|
+
// During rebase, --theirs = the branch being replayed (editor's work).
|
|
2011
|
+
// (git rebase reverses ours/theirs: "ours" is the rebase target, "theirs" is the branch.)
|
|
2012
|
+
for (const file of st.conflicted) {
|
|
2013
|
+
await branchGit.raw(['checkout', '--theirs', file]);
|
|
2014
|
+
await branchGit.add(file);
|
|
2015
|
+
conflictedFiles.push(file);
|
|
2016
|
+
}
|
|
2017
|
+
// nextAction stays 'continue'
|
|
2018
|
+
}
|
|
2019
|
+
else {
|
|
2020
|
+
const msg = rebaseErr instanceof Error ? rebaseErr.message : '';
|
|
2021
|
+
if (msg.toLowerCase().includes('nothing to commit') ||
|
|
2022
|
+
msg.toLowerCase().includes('apply --skip')) {
|
|
2023
|
+
// Empty commit after --theirs resolution — skip it
|
|
2024
|
+
nextAction = 'skip';
|
|
2025
|
+
}
|
|
2026
|
+
else {
|
|
2027
|
+
// Unexpected error — abort and leave branch behind.
|
|
2028
|
+
// We intentionally don't update conflictStatus/conflictFiles here:
|
|
2029
|
+
// the rebase didn't complete so we can't determine the true conflict
|
|
2030
|
+
// state. Previous metadata (possibly stale) is preserved until the
|
|
2031
|
+
// next successful rebase cycle corrects it.
|
|
2032
|
+
workerLogWarn(` Unexpected rebase error in ${branchDir}: ${msg || 'Unknown error'}`);
|
|
2033
|
+
failureReason = msg || 'Unknown error';
|
|
2034
|
+
await branchGit.rebase(['--abort']).catch(() => { });
|
|
2035
|
+
break;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
if (!completed) {
|
|
2041
|
+
// PR-W2 (M1 rider): failureReason is only set on the "unexpected
|
|
2042
|
+
// error" break above -- MAX_ROUNDS exhaustion is a distinct exit
|
|
2043
|
+
// path with no error message of its own, so the warn text must
|
|
2044
|
+
// not conflate the two.
|
|
2045
|
+
workerLogWarn(failureReason !== undefined
|
|
2046
|
+
? ` Rebase of ${branchDir} aborted due to unexpected error: ${failureReason}`
|
|
2047
|
+
: ` Rebase of ${branchDir} did not complete within ${MAX_ROUNDS} rounds, aborting`);
|
|
2048
|
+
await branchGit.rebase(['--abort']).catch(() => { });
|
|
2049
|
+
const rebaseFailureMessage = failureReason ?? `did not complete within ${MAX_ROUNDS} rounds`;
|
|
2050
|
+
// [HIGH-1] failed[] folds into worker-status.json's
|
|
2051
|
+
// lastGitSync.failed, served to the browser -- failureReason can
|
|
2052
|
+
// be an arbitrary git error message that embeds the bot token.
|
|
2053
|
+
const redactedRebaseFailureMessage = redactCredentials(rebaseFailureMessage);
|
|
2054
|
+
failed.push({
|
|
2055
|
+
branch: branchDir,
|
|
2056
|
+
error: redactedRebaseFailureMessage,
|
|
2057
|
+
});
|
|
2058
|
+
// PR-W2: record once here for the "!completed" exit -- the
|
|
2059
|
+
// unexpected-error break above is NOT disjoint from this block (it
|
|
2060
|
+
// always falls through here), so recording at the break itself
|
|
2061
|
+
// would double-record. The outer catch below is the only other
|
|
2062
|
+
// record site (a distinct, non-overlapping failure class: errors
|
|
2063
|
+
// outside this round loop, e.g. fetch/rev-list failures).
|
|
2064
|
+
await this.recordRebaseFailure(branchPath, branchDir, redactedRebaseFailureMessage);
|
|
1910
2065
|
continue;
|
|
1911
2066
|
}
|
|
2067
|
+
// The rebase rewrote the branch clone's working tree — mark ContentStore
|
|
2068
|
+
// ID indexes rooted here stale so lookups rebuild from disk, in this
|
|
2069
|
+
// process and (via the on-disk generation marker) in the Lambda
|
|
2070
|
+
// containers sharing this filesystem.
|
|
2071
|
+
await invalidateBranchContentCaches(branchPath);
|
|
2072
|
+
// Convert file paths to ContentIds — immutable, survives slug renames.
|
|
2073
|
+
// Entry files have IDs in their filename (e.g., "post.slug.a1b2c3d4e5f6.mdx").
|
|
2074
|
+
// .collection.json files have no ID themselves (extractIdFromFilename returns null
|
|
2075
|
+
// for dot-prefixed files), so we extract the ID from the parent directory instead.
|
|
2076
|
+
// The root content directory (e.g., "content/", or "cms/content/" for a
|
|
2077
|
+
// multi-segment contentRoot) has no embedded ID, so we use ROOT_COLLECTION_ID
|
|
2078
|
+
// as a sentinel — but only for the configured contentRoot.
|
|
2079
|
+
//
|
|
2080
|
+
// Two different notions of "parent" are needed below, and conflating them
|
|
2081
|
+
// reproduces the exact bug this comparison guards against (see
|
|
2082
|
+
// schema-store.ts's contentRootName doc comment for the same shape elsewhere):
|
|
2083
|
+
// - `parentDir` (a basename) recovers a SUB-collection's own embedded ID
|
|
2084
|
+
// (e.g. "posts.cNbR5xFm2Kpd" -> "cNbR5xFm2Kpd") — correct as a basename,
|
|
2085
|
+
// since a collection directory carries its ID in its own name, one path
|
|
2086
|
+
// segment.
|
|
2087
|
+
// - `parentPath` (the full relative parent path, normalized) is what must be
|
|
2088
|
+
// compared against `this.contentRoot`, because `contentRoot` is documented
|
|
2089
|
+
// (config/helpers.ts) as allowed to span multiple segments (e.g.
|
|
2090
|
+
// "cms/content"). Comparing a basename ("content") against that full value
|
|
2091
|
+
// is always false, which silently drops the root collection's conflict.
|
|
2092
|
+
// Git reports POSIX-style paths and the configured value may be authored
|
|
2093
|
+
// with either separator, so both sides go through normalizeFilesystemPath
|
|
2094
|
+
// before comparing.
|
|
2095
|
+
const normalizedContentRoot = normalizeFilesystemPath(this.contentRoot);
|
|
2096
|
+
const conflictIds = [...new Set(conflictedFiles)]
|
|
2097
|
+
.map((f) => {
|
|
2098
|
+
const fileId = extractIdFromFilename(path.basename(f));
|
|
2099
|
+
if (fileId)
|
|
2100
|
+
return fileId;
|
|
2101
|
+
const parentDir = path.basename(path.dirname(f));
|
|
2102
|
+
const dirId = extractIdFromFilename(parentDir);
|
|
2103
|
+
if (dirId)
|
|
2104
|
+
return dirId;
|
|
2105
|
+
// Only assign ROOT_COLLECTION_ID when the file's parent directory IS the
|
|
2106
|
+
// configured content root. Other unrecognized paths are filtered out.
|
|
2107
|
+
const parentPath = normalizeFilesystemPath(path.dirname(f));
|
|
2108
|
+
if (path.basename(f) === '.collection.json' && parentPath === normalizedContentRoot) {
|
|
2109
|
+
return ROOT_COLLECTION_ID;
|
|
2110
|
+
}
|
|
2111
|
+
return null;
|
|
2112
|
+
})
|
|
2113
|
+
.filter((id) => id !== null);
|
|
2114
|
+
const conflictIdsDeduped = [...new Set(conflictIds)];
|
|
2115
|
+
const hadConflicts = conflictIdsDeduped.length > 0;
|
|
2116
|
+
workerLog(hadConflicts
|
|
2117
|
+
? ` Rebased ${branchDir} (kept branch version for ${conflictIdsDeduped.length} conflicting file(s))`
|
|
2118
|
+
: ` Rebased ${branchDir} successfully`);
|
|
1912
2119
|
await meta.save({
|
|
1913
2120
|
branch: {
|
|
1914
2121
|
name: branchDir,
|
|
1915
|
-
conflictStatus: 'clean',
|
|
1916
|
-
conflictFiles:
|
|
2122
|
+
conflictStatus: hadConflicts ? 'conflicts-detected' : 'clean',
|
|
2123
|
+
conflictFiles: conflictIdsDeduped,
|
|
2124
|
+
// PR-W2: the cycle completed successfully -- clear any prior
|
|
2125
|
+
// failure record regardless of conflict outcome.
|
|
1917
2126
|
rebaseFailure: undefined,
|
|
1918
2127
|
},
|
|
1919
2128
|
});
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
//
|
|
1942
|
-
//
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
if (st.conflicted.length > 0) {
|
|
1957
|
-
// During rebase, --theirs = the branch being replayed (editor's work).
|
|
1958
|
-
// (git rebase reverses ours/theirs: "ours" is the rebase target, "theirs" is the branch.)
|
|
1959
|
-
for (const file of st.conflicted) {
|
|
1960
|
-
await branchGit.raw(['checkout', '--theirs', file]);
|
|
1961
|
-
await branchGit.add(file);
|
|
1962
|
-
conflictedFiles.push(file);
|
|
2129
|
+
// PR-W1: the branch was behind and the rebase completed (with or
|
|
2130
|
+
// without --theirs conflict resolution) -- it moved, so it belongs
|
|
2131
|
+
// in the summary. Branches already up to date `continue`d above and
|
|
2132
|
+
// are deliberately not listed here.
|
|
2133
|
+
rebased.push(branchDir);
|
|
2134
|
+
// [SYNC-H1] The rebase just rewrote this clone's history. If that
|
|
2135
|
+
// history was already published, nothing else will ever reconcile
|
|
2136
|
+
// remote.git (and GitHub) with it -- the editor's next submit would
|
|
2137
|
+
// simply be rejected non-fast-forward. Carry the rewrite forward.
|
|
2138
|
+
if (publishedSha !== null) {
|
|
2139
|
+
if (publishedSha === preRebaseHead) {
|
|
2140
|
+
// ARMING GUARD. remote.git holds EXACTLY what this clone just
|
|
2141
|
+
// rebased away and nothing more, so a lease keyed to it can only
|
|
2142
|
+
// undo our own rewrite.
|
|
2143
|
+
//
|
|
2144
|
+
// The inequality case below is not defensive padding: branch
|
|
2145
|
+
// clones never fetch their own branch (GitManager's clone is
|
|
2146
|
+
// --single-branch and checkoutBranch only checks out an existing
|
|
2147
|
+
// local branch), while reconcileTrackedBranches fast-forwards
|
|
2148
|
+
// remote.git to GitHub's tip. So after a reviewer pushes a fixup
|
|
2149
|
+
// straight to the PR branch, remote.git legitimately holds a
|
|
2150
|
+
// commit this clone has never seen. Leasing on "whatever
|
|
2151
|
+
// remote.git currently holds" would be SATISFIED there and would
|
|
2152
|
+
// delete that fixup from remote.git and then from GitHub,
|
|
2153
|
+
// silently. Keying the lease to the pre-rebase tip turns that
|
|
2154
|
+
// case into the visible divergence it should be.
|
|
2155
|
+
//
|
|
2156
|
+
// Order matters: mark, then push, then queue. A crash after any
|
|
2157
|
+
// step leaves the marker set with the work unfinished, which
|
|
2158
|
+
// reconcilePendingRewrite() completes on a later cycle. Pushing
|
|
2159
|
+
// first would leave remote.git rewritten, GitHub stale and
|
|
2160
|
+
// nothing recorded -- unrecoverable, and landing on exactly the
|
|
2161
|
+
// false "another deployment" diagnosis this change removes.
|
|
2162
|
+
await this.markHistoryRewritten(branchPath, branchDir, publishedSha);
|
|
2163
|
+
if (await this.forcePublishToLocalRemote(branchPath, branchRef, publishedSha)) {
|
|
2164
|
+
await this.enqueueGitHubPush(branchRef);
|
|
1963
2165
|
}
|
|
1964
|
-
// nextAction stays 'continue'
|
|
1965
2166
|
}
|
|
1966
2167
|
else {
|
|
1967
|
-
const
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
}
|
|
1973
|
-
else {
|
|
1974
|
-
// Unexpected error — abort and leave branch behind.
|
|
1975
|
-
// We intentionally don't update conflictStatus/conflictFiles here:
|
|
1976
|
-
// the rebase didn't complete so we can't determine the true conflict
|
|
1977
|
-
// state. Previous metadata (possibly stale) is preserved until the
|
|
1978
|
-
// next successful rebase cycle corrects it.
|
|
1979
|
-
workerLogWarn(` Unexpected rebase error in ${branchDir}: ${msg || 'Unknown error'}`);
|
|
1980
|
-
failureReason = msg || 'Unknown error';
|
|
1981
|
-
await branchGit.rebase(['--abort']).catch(() => { });
|
|
1982
|
-
break;
|
|
1983
|
-
}
|
|
2168
|
+
const divergence = `rebased locally, but remote.git holds ${publishedSha} for ${branchRef}, which this ` +
|
|
2169
|
+
`clone never had (a direct push to the branch?). Left untouched -- reconcile it ` +
|
|
2170
|
+
`before submitting again.`;
|
|
2171
|
+
workerLogWarn(` ${branchDir}: ${divergence}`);
|
|
2172
|
+
await this.recordRebaseFailure(branchPath, branchDir, divergence);
|
|
1984
2173
|
}
|
|
1985
2174
|
}
|
|
1986
2175
|
}
|
|
1987
|
-
|
|
1988
|
-
//
|
|
1989
|
-
//
|
|
1990
|
-
//
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
? ` Rebase of ${branchDir} aborted due to unexpected error: ${failureReason}`
|
|
1994
|
-
: ` Rebase of ${branchDir} did not complete within ${MAX_ROUNDS} rounds, aborting`);
|
|
1995
|
-
await branchGit.rebase(['--abort']).catch(() => { });
|
|
1996
|
-
const rebaseFailureMessage = failureReason ?? `did not complete within ${MAX_ROUNDS} rounds`;
|
|
1997
|
-
// [HIGH-1] failed[] folds into worker-status.json's
|
|
1998
|
-
// lastGitSync.failed, served to the browser -- failureReason can
|
|
1999
|
-
// be an arbitrary git error message that embeds the bot token.
|
|
2000
|
-
const redactedRebaseFailureMessage = redactCredentials(rebaseFailureMessage);
|
|
2001
|
-
failed.push({
|
|
2002
|
-
branch: branchDir,
|
|
2003
|
-
error: redactedRebaseFailureMessage,
|
|
2176
|
+
finally {
|
|
2177
|
+
// [SYNC-C1] Released on EVERY exit -- the `continue`s above, a throw
|
|
2178
|
+
// into the outer catch, and the happy path alike. A stranded lock
|
|
2179
|
+
// would wedge every write to this branch until it went stale.
|
|
2180
|
+
await releaseContentLock?.().catch((releaseErr) => {
|
|
2181
|
+
workerLogWarn(` Failed to release content-write lock for ${branchDir}: ${getErrorMessage(releaseErr)}`);
|
|
2004
2182
|
});
|
|
2005
|
-
// PR-W2: record once here for the "!completed" exit -- the
|
|
2006
|
-
// unexpected-error break above is NOT disjoint from this block (it
|
|
2007
|
-
// always falls through here), so recording at the break itself
|
|
2008
|
-
// would double-record. The outer catch below is the only other
|
|
2009
|
-
// record site (a distinct, non-overlapping failure class: errors
|
|
2010
|
-
// outside this round loop, e.g. fetch/rev-list failures).
|
|
2011
|
-
await this.recordRebaseFailure(branchPath, branchDir, redactedRebaseFailureMessage);
|
|
2012
|
-
continue;
|
|
2013
|
-
}
|
|
2014
|
-
// The rebase rewrote the branch clone's working tree — mark ContentStore
|
|
2015
|
-
// ID indexes rooted here stale so lookups rebuild from disk, in this
|
|
2016
|
-
// process and (via the on-disk generation marker) in the Lambda
|
|
2017
|
-
// containers sharing this filesystem.
|
|
2018
|
-
await invalidateBranchContentCaches(branchPath);
|
|
2019
|
-
// Convert file paths to ContentIds — immutable, survives slug renames.
|
|
2020
|
-
// Entry files have IDs in their filename (e.g., "post.slug.a1b2c3d4e5f6.mdx").
|
|
2021
|
-
// .collection.json files have no ID themselves (extractIdFromFilename returns null
|
|
2022
|
-
// for dot-prefixed files), so we extract the ID from the parent directory instead.
|
|
2023
|
-
// The root content directory (e.g., "content/", or "cms/content/" for a
|
|
2024
|
-
// multi-segment contentRoot) has no embedded ID, so we use ROOT_COLLECTION_ID
|
|
2025
|
-
// as a sentinel — but only for the configured contentRoot.
|
|
2026
|
-
//
|
|
2027
|
-
// Two different notions of "parent" are needed below, and conflating them
|
|
2028
|
-
// reproduces the exact bug this comparison guards against (see
|
|
2029
|
-
// schema-store.ts's contentRootName doc comment for the same shape elsewhere):
|
|
2030
|
-
// - `parentDir` (a basename) recovers a SUB-collection's own embedded ID
|
|
2031
|
-
// (e.g. "posts.cNbR5xFm2Kpd" -> "cNbR5xFm2Kpd") — correct as a basename,
|
|
2032
|
-
// since a collection directory carries its ID in its own name, one path
|
|
2033
|
-
// segment.
|
|
2034
|
-
// - `parentPath` (the full relative parent path, normalized) is what must be
|
|
2035
|
-
// compared against `this.contentRoot`, because `contentRoot` is documented
|
|
2036
|
-
// (config/helpers.ts) as allowed to span multiple segments (e.g.
|
|
2037
|
-
// "cms/content"). Comparing a basename ("content") against that full value
|
|
2038
|
-
// is always false, which silently drops the root collection's conflict.
|
|
2039
|
-
// Git reports POSIX-style paths and the configured value may be authored
|
|
2040
|
-
// with either separator, so both sides go through normalizeFilesystemPath
|
|
2041
|
-
// before comparing.
|
|
2042
|
-
const normalizedContentRoot = normalizeFilesystemPath(this.contentRoot);
|
|
2043
|
-
const conflictIds = [...new Set(conflictedFiles)]
|
|
2044
|
-
.map((f) => {
|
|
2045
|
-
const fileId = extractIdFromFilename(path.basename(f));
|
|
2046
|
-
if (fileId)
|
|
2047
|
-
return fileId;
|
|
2048
|
-
const parentDir = path.basename(path.dirname(f));
|
|
2049
|
-
const dirId = extractIdFromFilename(parentDir);
|
|
2050
|
-
if (dirId)
|
|
2051
|
-
return dirId;
|
|
2052
|
-
// Only assign ROOT_COLLECTION_ID when the file's parent directory IS the
|
|
2053
|
-
// configured content root. Other unrecognized paths are filtered out.
|
|
2054
|
-
const parentPath = normalizeFilesystemPath(path.dirname(f));
|
|
2055
|
-
if (path.basename(f) === '.collection.json' && parentPath === normalizedContentRoot) {
|
|
2056
|
-
return ROOT_COLLECTION_ID;
|
|
2057
|
-
}
|
|
2058
|
-
return null;
|
|
2059
|
-
})
|
|
2060
|
-
.filter((id) => id !== null);
|
|
2061
|
-
const conflictIdsDeduped = [...new Set(conflictIds)];
|
|
2062
|
-
const hadConflicts = conflictIdsDeduped.length > 0;
|
|
2063
|
-
workerLog(hadConflicts
|
|
2064
|
-
? ` Rebased ${branchDir} (kept branch version for ${conflictIdsDeduped.length} conflicting file(s))`
|
|
2065
|
-
: ` Rebased ${branchDir} successfully`);
|
|
2066
|
-
await meta.save({
|
|
2067
|
-
branch: {
|
|
2068
|
-
name: branchDir,
|
|
2069
|
-
conflictStatus: hadConflicts ? 'conflicts-detected' : 'clean',
|
|
2070
|
-
conflictFiles: conflictIdsDeduped,
|
|
2071
|
-
// PR-W2: the cycle completed successfully -- clear any prior
|
|
2072
|
-
// failure record regardless of conflict outcome.
|
|
2073
|
-
rebaseFailure: undefined,
|
|
2074
|
-
},
|
|
2075
|
-
});
|
|
2076
|
-
// PR-W1: the branch was behind and the rebase completed (with or
|
|
2077
|
-
// without --theirs conflict resolution) -- it moved, so it belongs
|
|
2078
|
-
// in the summary. Branches already up to date `continue`d above and
|
|
2079
|
-
// are deliberately not listed here.
|
|
2080
|
-
rebased.push(branchDir);
|
|
2081
|
-
// [SYNC-H1] The rebase just rewrote this clone's history. If that
|
|
2082
|
-
// history was already published, nothing else will ever reconcile
|
|
2083
|
-
// remote.git (and GitHub) with it -- the editor's next submit would
|
|
2084
|
-
// simply be rejected non-fast-forward. Carry the rewrite forward.
|
|
2085
|
-
if (publishedSha !== null) {
|
|
2086
|
-
if (publishedSha === preRebaseHead) {
|
|
2087
|
-
// ARMING GUARD. remote.git holds EXACTLY what this clone just
|
|
2088
|
-
// rebased away and nothing more, so a lease keyed to it can only
|
|
2089
|
-
// undo our own rewrite.
|
|
2090
|
-
//
|
|
2091
|
-
// The inequality case below is not defensive padding: branch
|
|
2092
|
-
// clones never fetch their own branch (GitManager's clone is
|
|
2093
|
-
// --single-branch and checkoutBranch only checks out an existing
|
|
2094
|
-
// local branch), while reconcileTrackedBranches fast-forwards
|
|
2095
|
-
// remote.git to GitHub's tip. So after a reviewer pushes a fixup
|
|
2096
|
-
// straight to the PR branch, remote.git legitimately holds a
|
|
2097
|
-
// commit this clone has never seen. Leasing on "whatever
|
|
2098
|
-
// remote.git currently holds" would be SATISFIED there and would
|
|
2099
|
-
// delete that fixup from remote.git and then from GitHub,
|
|
2100
|
-
// silently. Keying the lease to the pre-rebase tip turns that
|
|
2101
|
-
// case into the visible divergence it should be.
|
|
2102
|
-
//
|
|
2103
|
-
// Order matters: mark, then push, then queue. A crash after any
|
|
2104
|
-
// step leaves the marker set with the work unfinished, which
|
|
2105
|
-
// reconcilePendingRewrite() completes on a later cycle. Pushing
|
|
2106
|
-
// first would leave remote.git rewritten, GitHub stale and
|
|
2107
|
-
// nothing recorded -- unrecoverable, and landing on exactly the
|
|
2108
|
-
// false "another deployment" diagnosis this change removes.
|
|
2109
|
-
await this.markHistoryRewritten(branchPath, branchDir, publishedSha);
|
|
2110
|
-
if (await this.forcePublishToLocalRemote(branchPath, branchRef, publishedSha)) {
|
|
2111
|
-
await this.enqueueGitHubPush(branchRef);
|
|
2112
|
-
}
|
|
2113
|
-
}
|
|
2114
|
-
else {
|
|
2115
|
-
const divergence = `rebased locally, but remote.git holds ${publishedSha} for ${branchRef}, which this ` +
|
|
2116
|
-
`clone never had (a direct push to the branch?). Left untouched -- reconcile it ` +
|
|
2117
|
-
`before submitting again.`;
|
|
2118
|
-
workerLogWarn(` ${branchDir}: ${divergence}`);
|
|
2119
|
-
await this.recordRebaseFailure(branchPath, branchDir, divergence);
|
|
2120
|
-
}
|
|
2121
2183
|
}
|
|
2122
2184
|
}
|
|
2123
2185
|
catch (err) {
|
|
@@ -2134,7 +2196,7 @@ export class CmsWorker {
|
|
|
2134
2196
|
await this.recordRebaseFailure(branchPath, branchDir, redactedMessage);
|
|
2135
2197
|
}
|
|
2136
2198
|
}
|
|
2137
|
-
return { rebased, skippedDirty, failed };
|
|
2199
|
+
return { rebased, skippedDirty, skippedLocked, failed };
|
|
2138
2200
|
}
|
|
2139
2201
|
async refreshAuthCache() {
|
|
2140
2202
|
if (!this.running || !this.config.refreshAuthCache)
|