canopycms 0.0.67 → 0.0.68-int.94
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/README.md +3 -2
- package/dist/ai/handler.js +8 -0
- package/dist/api/admin-branch-health.js +12 -7
- package/dist/api/admin.d.ts +21 -9
- package/dist/api/admin.js +18 -2
- package/dist/api/branch-status.js +19 -4
- package/dist/api/branch.js +11 -11
- package/dist/api/client.d.ts +12 -0
- package/dist/api/client.js +34 -11
- package/dist/api/content.d.ts +6 -5
- package/dist/api/content.js +44 -41
- package/dist/api/entries.js +16 -15
- package/dist/api/github-sync.d.ts +10 -1
- package/dist/api/github-sync.js +20 -10
- package/dist/api/reference-options.js +1 -1
- package/dist/api/resolve-references.js +14 -40
- package/dist/api/settings-helpers.d.ts +1 -1
- package/dist/api/settings-helpers.js +1 -4
- package/dist/api/user.d.ts +3 -0
- package/dist/api/user.js +3 -0
- package/dist/assets/factory.d.ts +5 -0
- package/dist/assets/factory.js +5 -0
- package/dist/authorization/content.d.ts +5 -4
- package/dist/authorization/content.js +5 -5
- package/dist/authorization/path.d.ts +11 -6
- package/dist/authorization/path.js +11 -6
- package/dist/authorization/types.d.ts +2 -2
- package/dist/branch-health.js +3 -3
- package/dist/branch-schema-cache.d.ts +5 -3
- package/dist/branch-schema-cache.js +29 -11
- package/dist/branch-workspace.js +8 -3
- package/dist/build-identity.d.ts +3 -0
- package/dist/build-identity.js +13 -0
- package/dist/cli/cli.js +1070 -388
- package/dist/cli/generate-ai-content.js +537 -150
- package/dist/cli/github-app-manifest.d.ts +4 -3
- package/dist/cli/github-app-manifest.js +4 -3
- package/dist/cli/init.js +21 -11
- package/dist/cli/migrate.js +12 -8
- package/dist/cli/sync.js +83 -37
- package/dist/cli/template-files/Dockerfile.cms.template +8 -0
- package/dist/cli/template-files/cdk-app.ts.template +4 -0
- package/dist/cli/template-files/cms-stack.ts.template +6 -0
- package/dist/cli/template-files/deploy-cms.yml.template +2 -0
- package/dist/config/schemas/config.d.ts +11 -3
- package/dist/config/schemas/config.js +5 -1
- package/dist/config/schemas/field.js +1 -0
- package/dist/config/schemas/url.d.ts +2 -0
- package/dist/config/schemas/url.js +7 -0
- package/dist/config/types.d.ts +28 -4
- package/dist/config/validation.d.ts +6 -0
- package/dist/config/validation.js +37 -0
- package/dist/content-listing.d.ts +12 -12
- package/dist/content-listing.js +10 -6
- package/dist/content-reader.d.ts +11 -2
- package/dist/content-reader.js +22 -14
- package/dist/content-store.d.ts +37 -9
- package/dist/content-store.js +70 -43
- package/dist/content-tree.d.ts +1 -1
- package/dist/content-tree.js +2 -2
- package/dist/context.d.ts +27 -9
- package/dist/context.js +154 -70
- package/dist/editor/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +1 -1
- package/dist/editor/Editor.d.ts +2 -0
- package/dist/editor/Editor.js +6 -5
- package/dist/editor/FormRenderer.js +20 -5
- package/dist/editor/admin/SystemHealthPanel.js +45 -7
- package/dist/editor/components/NoEditPermissionNotice.d.ts +10 -0
- package/dist/editor/components/NoEditPermissionNotice.js +17 -0
- package/dist/editor/editor-utils.d.ts +17 -15
- package/dist/editor/editor-utils.js +43 -36
- package/dist/editor/hooks/useBranchesData.js +3 -1
- package/dist/editor/hooks/useCommentSystem.js +5 -1
- package/dist/editor/hooks/useDraftManager.d.ts +1 -1
- package/dist/editor/hooks/useDraftManager.js +2 -1
- package/dist/editor/hooks/useEntriesData.js +2 -2
- package/dist/editor/hooks/useEntryManager.js +17 -11
- package/dist/editor/preview-bridge.js +4 -1
- package/dist/editor/preview-path.d.ts +10 -0
- package/dist/editor/preview-path.js +20 -0
- package/dist/entry-schema-registry.d.ts +4 -4
- package/dist/entry-schema-registry.js +10 -6
- package/dist/entry-schema.d.ts +27 -4
- package/dist/entry-schema.js +20 -3
- package/dist/git-manager.d.ts +79 -5
- package/dist/git-manager.js +308 -18
- package/dist/github-service.d.ts +8 -0
- package/dist/github-service.js +5 -1
- package/dist/http/handler.js +35 -13
- package/dist/http/index.d.ts +2 -0
- package/dist/http/index.js +2 -0
- package/dist/http/router.d.ts +2 -0
- package/dist/http/router.js +2 -1
- package/dist/http/worker-not-ready.d.ts +9 -0
- package/dist/http/worker-not-ready.js +17 -0
- package/dist/operating-mode/client-unsafe-strategy.js +0 -6
- package/dist/operating-mode/types.d.ts +0 -3
- package/dist/paths/branch-name.d.ts +5 -0
- package/dist/paths/branch-name.js +9 -0
- package/dist/paths/branch.d.ts +6 -1
- package/dist/paths/branch.js +15 -2
- package/dist/paths/index.d.ts +3 -3
- package/dist/paths/index.js +3 -3
- package/dist/paths/normalize.d.ts +7 -0
- package/dist/paths/normalize.js +9 -0
- package/dist/reference-resolver.d.ts +5 -21
- package/dist/reference-resolver.js +9 -44
- package/dist/resolve-canopy-user.js +2 -1
- package/dist/schema/schema-store.d.ts +48 -22
- package/dist/schema/schema-store.js +66 -22
- package/dist/services.d.ts +18 -6
- package/dist/services.js +85 -70
- package/dist/settings-workspace.js +42 -4
- package/dist/static/seo.d.ts +2 -14
- package/dist/static/seo.js +2 -25
- package/dist/submission-attribution.d.ts +73 -0
- package/dist/submission-attribution.js +221 -0
- package/dist/sync-core.d.ts +12 -1
- package/dist/sync-core.js +31 -13
- package/dist/task-queue/worker-status.d.ts +8 -0
- package/dist/task-queue/worker-status.js +17 -0
- package/dist/types.d.ts +31 -2
- package/dist/utils/content-write-lock.d.ts +18 -7
- package/dist/utils/content-write-lock.js +18 -9
- package/dist/utils/debug.d.ts +8 -0
- package/dist/utils/debug.js +10 -2
- package/dist/utils/git.d.ts +28 -0
- package/dist/utils/git.js +38 -0
- package/dist/utils/provisioning-lock.d.ts +15 -5
- package/dist/utils/provisioning-lock.js +31 -11
- package/dist/utils/request-timing.d.ts +25 -0
- package/dist/utils/request-timing.js +101 -0
- package/dist/utils/url-prefix.d.ts +30 -0
- package/dist/utils/url-prefix.js +61 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/worker/canopy-state.d.ts +45 -0
- package/dist/worker/canopy-state.js +75 -0
- package/dist/worker/cms-worker.js +23 -2
- package/dist/worker/git-sync.d.ts +8 -2
- package/dist/worker/git-sync.js +138 -24
- package/dist/worker/provisioned-workspace.d.ts +35 -0
- package/dist/worker/provisioned-workspace.js +50 -0
- package/dist/worker/rebase.d.ts +1 -1
- package/dist/worker/rebase.js +83 -12
- package/dist/worker/task-runner.js +39 -4
- package/package.json +1 -1
package/dist/worker/rebase.js
CHANGED
|
@@ -4,11 +4,14 @@ import { simpleGit } from 'simple-git';
|
|
|
4
4
|
import { BranchMetadataFileManager, buildMergedBranchUpdate, getBranchMetadataFileManager, } from '../branch-metadata.js';
|
|
5
5
|
import { extractIdFromFilename } from '../content-id-index.js';
|
|
6
6
|
import { invalidateBranchContentCaches } from '../content-index-generation.js';
|
|
7
|
+
import { isSettingsBranchName } from '../paths/branch-name.js';
|
|
7
8
|
import { normalizeFilesystemPath } from '../paths/normalize.js';
|
|
8
9
|
import { ROOT_COLLECTION_ID } from '../paths/types.js';
|
|
9
10
|
import { tryAcquireContentWriteLock } from '../utils/content-write-lock.js';
|
|
11
|
+
import { holdProvisionedWorkspace, releaseProvisionedWorkspace, } from './provisioned-workspace.js';
|
|
10
12
|
import { getErrorMessage, isNodeError, redactCredentials } from '../utils/error.js';
|
|
11
|
-
import { isRebaseInProgress } from '../utils/git.js';
|
|
13
|
+
import { isCanopyInternalPath, isRebaseInProgress } from '../utils/git.js';
|
|
14
|
+
import { TRACKED_CANOPY_STATE_FIX, restoreRetiredSchemaCache, splitByUpstreamTracking, trackedCanopyStateChanges, } from './canopy-state.js';
|
|
12
15
|
import { enqueueGitHubPush, forcePublishToLocalRemote, markHistoryRewritten, readPublishedSha, reconcilePendingRewrite, } from './history-rewrite.js';
|
|
13
16
|
import { workerLog, workerLogWarn } from './log.js';
|
|
14
17
|
/**
|
|
@@ -123,7 +126,7 @@ const MAX_REBASE_ROUNDS = 50;
|
|
|
123
126
|
* - the **unexpected-error** exit aborts HERE, before breaking, so the caller's
|
|
124
127
|
* `!completed` abort is a caught no-op for that path;
|
|
125
128
|
* - the **lock-compromised** exit leaves the rebase in progress and is aborted
|
|
126
|
-
* by `rebaseOneBranch`'s own `
|
|
129
|
+
* by `rebaseOneBranch`'s own `lockCompromised() && !completed` branch,
|
|
127
130
|
* which returns `skippedLocked` before the `!completed` block below it is
|
|
128
131
|
* reached -- a distinct site that cannot be folded into that one;
|
|
129
132
|
* - the **conflict-resolution-failure** and **MAX_REBASE_ROUNDS** exits are the
|
|
@@ -406,7 +409,14 @@ export async function runRebaseCycle(ctx) {
|
|
|
406
409
|
workerLog(` Skipping ${branchDir}: base branch (refreshed separately)`);
|
|
407
410
|
continue;
|
|
408
411
|
}
|
|
409
|
-
|
|
412
|
+
// A settings branch is an orphan: rebasing it onto the base would give it content
|
|
413
|
+
// history. The API refuses to provision one, so a directory under that name is a
|
|
414
|
+
// leftover and is left alone.
|
|
415
|
+
if (isSettingsBranchName(branchDir, ctx.ensureSettingsBranch())) {
|
|
416
|
+
workerLogWarn(` Skipping ${branchDir}: settings branch, never a content workspace`);
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const outcome = await holdAndRebaseOneBranch(ctx, branchDir, branchPath);
|
|
410
420
|
if (outcome.kind === 'rebased')
|
|
411
421
|
rebased.push(branchDir);
|
|
412
422
|
else if (outcome.kind === 'skippedDirty')
|
|
@@ -423,6 +433,39 @@ export async function runRebaseCycle(ctx) {
|
|
|
423
433
|
}
|
|
424
434
|
return { rebased, skippedDirty, skippedLocked, failed };
|
|
425
435
|
}
|
|
436
|
+
/**
|
|
437
|
+
* {@link rebaseOneBranch} under the branch's provisioning lock, so no git step races a clone of
|
|
438
|
+
* this directory. A branch whose lock is held elsewhere is `skippedLocked` and retried next cycle;
|
|
439
|
+
* one with no `branch.json` yet is skipped as not provisioned. Lock order: provisioning, then the
|
|
440
|
+
* content-write lock inside it; see `provisioned-workspace.ts` for why that order cannot deadlock.
|
|
441
|
+
* Never throws, like rebaseOneBranch.
|
|
442
|
+
*/
|
|
443
|
+
async function holdAndRebaseOneBranch(ctx, branchDir, branchPath) {
|
|
444
|
+
let hold;
|
|
445
|
+
try {
|
|
446
|
+
hold = await holdProvisionedWorkspace(ctx.contentBranchesPath, branchDir);
|
|
447
|
+
}
|
|
448
|
+
catch (err) {
|
|
449
|
+
// [REDACT] failed[] is served to the browser via worker-status.json.
|
|
450
|
+
const error = redactCredentials(`could not take the provisioning lock: ${getErrorMessage(err)}`);
|
|
451
|
+
workerLogWarn(` Skipping ${branchDir}: ${error}`);
|
|
452
|
+
return { kind: 'failed', error };
|
|
453
|
+
}
|
|
454
|
+
if (hold.kind === 'locked') {
|
|
455
|
+
workerLog(` Skipping ${branchDir}: provisioning lock held elsewhere (retrying next cycle)`);
|
|
456
|
+
return { kind: 'skippedLocked' };
|
|
457
|
+
}
|
|
458
|
+
if (hold.kind === 'not-provisioned') {
|
|
459
|
+
workerLog(` Skipping ${branchDir}: not yet provisioned (no branch.json)`);
|
|
460
|
+
return { kind: 'none' };
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
463
|
+
return await rebaseOneBranch(ctx, branchDir, branchPath, hold.isCompromised);
|
|
464
|
+
}
|
|
465
|
+
finally {
|
|
466
|
+
await releaseProvisionedWorkspace(hold.release, branchDir);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
426
469
|
/**
|
|
427
470
|
* Rebase ONE branch workspace onto the base branch, keeping the branch's
|
|
428
471
|
* version of every conflicting file.
|
|
@@ -435,7 +478,7 @@ export async function runRebaseCycle(ctx) {
|
|
|
435
478
|
* returned as `{ kind: 'failed' }`, so one unreadable branch cannot halt the
|
|
436
479
|
* rest of the cycle ([SYNC-M2]).
|
|
437
480
|
*/
|
|
438
|
-
async function rebaseOneBranch(ctx, branchDir, branchPath) {
|
|
481
|
+
async function rebaseOneBranch(ctx, branchDir, branchPath, isProvisioningLockCompromised) {
|
|
439
482
|
// Read only by the catch below. See the `rebased` rider on
|
|
440
483
|
// BranchRebaseOutcome.
|
|
441
484
|
let didRebase = false;
|
|
@@ -489,6 +532,9 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
|
|
|
489
532
|
// record it and bail before the next one rather than replaying over
|
|
490
533
|
// an editor's concurrent save.
|
|
491
534
|
let contentLockCompromised = false;
|
|
535
|
+
// Either lock lost means another writer may be live against this tree;
|
|
536
|
+
// both get the same bail-before-the-next-destructive-step treatment.
|
|
537
|
+
const lockCompromised = () => contentLockCompromised || isProvisioningLockCompromised();
|
|
492
538
|
try {
|
|
493
539
|
releaseContentLock = await tryAcquireContentWriteLock(branchPath, (lockErr) => {
|
|
494
540
|
contentLockCompromised = true;
|
|
@@ -574,12 +620,37 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
|
|
|
574
620
|
}
|
|
575
621
|
// Skip dirty branches — the editor has changes that cannot be rebased.
|
|
576
622
|
// Inside the lock, so no write can land between this check and the rebase
|
|
577
|
-
// below.
|
|
578
|
-
|
|
579
|
-
if (
|
|
623
|
+
// below. canopycms's own untracked state is not dirt.
|
|
624
|
+
let dirtyCheck = await branchGit.status();
|
|
625
|
+
if (await restoreRetiredSchemaCache(branchGit, dirtyCheck)) {
|
|
626
|
+
workerLog(` ${branchDir}: restored the retired in-tree schema cache`);
|
|
627
|
+
dirtyCheck = await branchGit.status();
|
|
628
|
+
}
|
|
629
|
+
const editorDirt = dirtyCheck.files.filter((f) => !isCanopyInternalPath(f.path));
|
|
630
|
+
if (editorDirt.length > 0) {
|
|
580
631
|
workerLog(` Skipping ${branchDir}: has uncommitted changes`);
|
|
581
632
|
return { kind: 'skippedDirty' };
|
|
582
633
|
}
|
|
634
|
+
// `git rebase` refuses to start over ANY modified tracked file, so
|
|
635
|
+
// tracked canopycms state blocks. The worker never untracks it here:
|
|
636
|
+
// replaying a branch commit that touched it writes historical bytes over
|
|
637
|
+
// the live file, and the abort that follows deletes it. So the index and
|
|
638
|
+
// the bytes stay untouched, and the wedge is recorded for an operator.
|
|
639
|
+
const trackedState = trackedCanopyStateChanges(dirtyCheck);
|
|
640
|
+
if (trackedState.length > 0) {
|
|
641
|
+
await branchGit.fetch('origin', ctx.baseBranch);
|
|
642
|
+
const baseTip = (await branchGit.revparse(['FETCH_HEAD'])).trim();
|
|
643
|
+
const { stillTracked } = await splitByUpstreamTracking(branchGit, trackedState, baseTip);
|
|
644
|
+
const reason = stillTracked.length > 0
|
|
645
|
+
? `git cannot rebase over modified canopycms state the repo tracks ` +
|
|
646
|
+
`(${trackedState.join(', ')}). To fix, ${TRACKED_CANOPY_STATE_FIX}.`
|
|
647
|
+
: `git cannot rebase over modified canopycms state this clone still tracks ` +
|
|
648
|
+
`(${trackedState.join(', ')}), though the base branch no longer does; ` +
|
|
649
|
+
`the clone needs manual repair`;
|
|
650
|
+
workerLogWarn(` Skipping ${branchDir}: ${reason}`);
|
|
651
|
+
await recordRebaseFailure(ctx, branchPath, branchDir, reason);
|
|
652
|
+
return { kind: 'failed', error: reason };
|
|
653
|
+
}
|
|
583
654
|
// The clone's own ref name: branchDir is the sanitized DIRECTORY name and
|
|
584
655
|
// need not match it. A literal 'HEAD' means a detached clone (a crashed
|
|
585
656
|
// rebase can leave one), where nothing below can safely name a ref, so
|
|
@@ -651,7 +722,7 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
|
|
|
651
722
|
// the rebase makes unreadable.
|
|
652
723
|
const preRebaseHead = (await branchGit.revparse(['HEAD'])).trim();
|
|
653
724
|
const publishedSha = canPublish ? await readPublishedSha(ctx, branchRef) : null;
|
|
654
|
-
const { completed, conflictedFiles, failureReason } = await runRebaseRounds(ctx, branchGit, branchDir, fetchedBaseTip,
|
|
725
|
+
const { completed, conflictedFiles, failureReason } = await runRebaseRounds(ctx, branchGit, branchDir, fetchedBaseTip, lockCompromised);
|
|
655
726
|
// Outside the round loop's try/catch, so a throwing test hook can
|
|
656
727
|
// never be misread as a rebase error.
|
|
657
728
|
if (completed)
|
|
@@ -676,14 +747,14 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
|
|
|
676
747
|
// history while a concurrent save is uncommitted working-tree state, and
|
|
677
748
|
// that writer is already told to retry via ContentWriteLockBusyError. So
|
|
678
749
|
// when the rebase completed, log the lost exclusivity loudly and finish.
|
|
679
|
-
if (
|
|
680
|
-
workerLogWarn(` Skipping ${branchDir}:
|
|
750
|
+
if (lockCompromised() && !completed) {
|
|
751
|
+
workerLogWarn(` Skipping ${branchDir}: a lock it held was compromised mid-rebase (retrying next cycle)`);
|
|
681
752
|
// No-op when no rebase is in progress; failure is expected there.
|
|
682
753
|
await branchGit.rebase(['--abort']).catch(() => { });
|
|
683
754
|
return { kind: 'skippedLocked' };
|
|
684
755
|
}
|
|
685
|
-
if (
|
|
686
|
-
workerLogWarn(`
|
|
756
|
+
if (lockCompromised()) {
|
|
757
|
+
workerLogWarn(` A lock held for ${branchDir} was compromised, but its rebase had already completed -- finishing the sync (history is rewritten; skipping now would strand the history-rewrite marker and wedge the branch)`);
|
|
687
758
|
}
|
|
688
759
|
if (!completed) {
|
|
689
760
|
// failureReason is only set on the "unexpected error" break above;
|
|
@@ -3,10 +3,10 @@ import { simpleGit } from 'simple-git';
|
|
|
3
3
|
import { completeTask, dequeueTask, failTask, recoverOrphanedTasks, retryTask, } from '../task-queue/cms-task-queue.js';
|
|
4
4
|
import { createOrUpdatePullRequest } from '../github-service.js';
|
|
5
5
|
import { BranchMetadataFileManager, getBranchMetadataFileManager } from '../branch-metadata.js';
|
|
6
|
-
import { sanitizeBranchName } from '../paths/branch-name.js';
|
|
6
|
+
import { sanitizeBranchName, RESERVED_SETTINGS_BRANCH_PREFIX } from '../paths/branch-name.js';
|
|
7
7
|
import { gitNetworkChildEnv } from '../git-manager.js';
|
|
8
8
|
import { getErrorMessage, redactCredentials } from '../utils/error.js';
|
|
9
|
-
import { isNonFastForwardRejection, isStaleLeaseRejection } from '../utils/git.js';
|
|
9
|
+
import { isNonFastForwardRejection, isStaleLeaseRejection, workflowPushRefusalFile, } from '../utils/git.js';
|
|
10
10
|
import { clearHistoryRewrittenMarker, readPublishedSha } from './history-rewrite.js';
|
|
11
11
|
import { writeWorkerStatus } from '../task-queue/worker-status.js';
|
|
12
12
|
import { workerLog, workerLogError } from './log.js';
|
|
@@ -90,6 +90,15 @@ function isRateLimitSignal403(err) {
|
|
|
90
90
|
return false;
|
|
91
91
|
}
|
|
92
92
|
// Payload validation helpers — fail fast with clear errors instead of silent `as` casts
|
|
93
|
+
/**
|
|
94
|
+
* Whether `branch` carries the reserved settings-branch prefix. Matching on the prefix
|
|
95
|
+
* alone, never on a configured name, is what keeps a content branch out: branch creation
|
|
96
|
+
* rejects the prefix, while a configured name the worker and API disagree on could be a
|
|
97
|
+
* content branch's.
|
|
98
|
+
*/
|
|
99
|
+
function isSettingsBranch(branch) {
|
|
100
|
+
return branch.startsWith(RESERVED_SETTINGS_BRANCH_PREFIX);
|
|
101
|
+
}
|
|
93
102
|
function requireString(payload, key) {
|
|
94
103
|
const val = payload[key];
|
|
95
104
|
if (typeof val !== 'string')
|
|
@@ -282,6 +291,13 @@ export async function executeTask(ctx, task, signal) {
|
|
|
282
291
|
if (sanitizeBranchName(branch) === ctx.sanitizedBaseBranch) {
|
|
283
292
|
throw new PermanentTaskError(`Refusing to push-and-create-or-update-pr for "${branch}": it is the base branch -- submitting the base branch is never valid`);
|
|
284
293
|
}
|
|
294
|
+
// The settings branch is an orphan with no history in common with the
|
|
295
|
+
// base, so GitHub 422s a PR for it: push it and stop.
|
|
296
|
+
if (isSettingsBranch(branch)) {
|
|
297
|
+
await ctx.pushBranchToGitHub(branch);
|
|
298
|
+
workerLog(`Pushed settings branch ${branch}; settings branches never get a PR`);
|
|
299
|
+
return { pushed: true };
|
|
300
|
+
}
|
|
285
301
|
await ctx.pushBranchToGitHub(branch);
|
|
286
302
|
const result = await createOrUpdatePullRequest({
|
|
287
303
|
octokit: ctx.octokit(),
|
|
@@ -291,9 +307,9 @@ export async function executeTask(ctx, task, signal) {
|
|
|
291
307
|
base,
|
|
292
308
|
title: optionalString(payload, 'title', `Submit ${branch}`),
|
|
293
309
|
body: optionalString(payload, 'body', ''),
|
|
294
|
-
// Content submits (api/github-sync.ts) set
|
|
295
|
-
// syncs (services.ts) deliberately don't.
|
|
310
|
+
// Content submits (api/github-sync.ts) set both.
|
|
296
311
|
markReadyIfDraft: payload.markReadyIfDraft === true,
|
|
312
|
+
mergeSectionIntoBody: payload.mergeSectionIntoBody === true,
|
|
297
313
|
signal,
|
|
298
314
|
});
|
|
299
315
|
workerLog(result.created
|
|
@@ -415,6 +431,23 @@ async function updateBranchMetadataOnFailure(ctx, task, error) {
|
|
|
415
431
|
workerLogError(`Failed to update failure metadata for ${branch}:`, err instanceof Error ? err.message : err);
|
|
416
432
|
}
|
|
417
433
|
}
|
|
434
|
+
/**
|
|
435
|
+
* Fail fast when GitHub refused the push for adding workflow content it does not already hold: the
|
|
436
|
+
* worker's credential deliberately lacks the workflows permission, so the identical push can never
|
|
437
|
+
* succeed. Rebasing onto a base that changed a workflow does not trigger it; a workflow edit made
|
|
438
|
+
* outside the editor, auto-merged by the rebase with a base change to the same file, does.
|
|
439
|
+
*/
|
|
440
|
+
function throwIfWorkflowRefusal(branch, message) {
|
|
441
|
+
const file = workflowPushRefusalFile(message);
|
|
442
|
+
if (file === null)
|
|
443
|
+
return;
|
|
444
|
+
throw new PermanentTaskError(`Push refused for branch "${branch}": it would put a version of ${file} on GitHub that ` +
|
|
445
|
+
`GitHub does not already have, and this deployment's GitHub credential is deliberately not ` +
|
|
446
|
+
`allowed to change workflow files. Such a change usually comes from outside the editor, ` +
|
|
447
|
+
`such as a direct push to this branch. Nothing was pushed, and ` +
|
|
448
|
+
`retrying will not help until a developer with permission to change workflow files ` +
|
|
449
|
+
`resolves it on GitHub.`);
|
|
450
|
+
}
|
|
418
451
|
export async function pushBranchToGitHub(ctx, branch) {
|
|
419
452
|
const git = simpleGit({
|
|
420
453
|
baseDir: ctx.remoteGitPath,
|
|
@@ -473,6 +506,7 @@ export async function pushBranchToGitHub(ctx, branch) {
|
|
|
473
506
|
}
|
|
474
507
|
catch (err) {
|
|
475
508
|
const message = getErrorMessage(err);
|
|
509
|
+
throwIfWorkflowRefusal(branch, message);
|
|
476
510
|
// A refused lease means GitHub is not at the commit we rewrote, so the
|
|
477
511
|
// marker is stale -- routine, not exceptional: tasks are re-run after a
|
|
478
512
|
// crash (recoverOrphanedTasks) and the marker survives any failure to
|
|
@@ -494,6 +528,7 @@ export async function pushBranchToGitHub(ctx, branch) {
|
|
|
494
528
|
}
|
|
495
529
|
catch (retryErr) {
|
|
496
530
|
const retryMessage = getErrorMessage(retryErr);
|
|
531
|
+
throwIfWorkflowRefusal(branch, retryMessage);
|
|
497
532
|
if (isNonFastForwardRejection(retryMessage)) {
|
|
498
533
|
throw new PermanentTaskError(`Push rejected for branch "${branch}": GitHub's tip is neither the commit this ` +
|
|
499
534
|
`deployment last published nor an ancestor of what it is pushing, so the branch ` +
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//": "@codemirror/language, @lezer/highlight: workaround — @mdxeditor/editor uses cm6-theme-basic-light which peer-requires these but mdxeditor doesn't declare them as dependencies",
|
|
3
3
|
"name": "canopycms",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.68-int.94",
|
|
5
5
|
"description": "CanopyCMS core package: schema-driven content, branch-aware editing, and editor UI for Next.js.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|