canopycms 0.8.1 → 0.8.2
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 +102 -47
- package/dist/api/admin-branch-health.d.ts +11 -0
- package/dist/api/admin-branch-health.js +36 -0
- package/dist/api/admin.d.ts +8 -1
- package/dist/api/branch-merge.js +1 -1
- package/dist/api/branch-review.js +20 -3
- package/dist/api/branch-status.js +30 -19
- package/dist/api/branch.d.ts +8 -3
- package/dist/api/branch.js +32 -3
- package/dist/api/client.d.ts +3 -1
- package/dist/api/client.js +4 -0
- package/dist/api/github-sync.d.ts +2 -0
- package/dist/api/github-sync.js +6 -1
- package/dist/api/pr-comment-sync.js +3 -1
- package/dist/branch-metadata-file.d.ts +66 -0
- package/dist/branch-metadata-file.js +14 -0
- package/dist/branch-metadata.d.ts +4 -2
- package/dist/branch-metadata.js +10 -2
- package/dist/cli/cli.js +28 -8
- package/dist/cli/generate-ai-content.js +13 -3
- package/dist/cli/init.js +2 -2
- package/dist/editor/BranchManager.d.ts +8 -4
- package/dist/editor/BranchManager.js +43 -18
- package/dist/editor/Editor.js +3 -2
- package/dist/editor/admin/SystemHealthPanel.js +65 -31
- package/dist/editor/admin/UnpublishedAtMergeAlert.d.ts +16 -0
- package/dist/editor/admin/UnpublishedAtMergeAlert.js +50 -0
- package/dist/editor/admin/useSystemHealth.d.ts +2 -0
- package/dist/editor/admin/useSystemHealth.js +21 -0
- package/dist/editor/branch-lock.d.ts +2 -0
- package/dist/editor/branch-lock.js +5 -3
- package/dist/editor/branch-status.d.ts +25 -1
- package/dist/editor/branch-status.js +75 -0
- package/dist/editor/components/EditorHeader.d.ts +6 -2
- package/dist/editor/components/EditorHeader.js +51 -30
- package/dist/editor/components/EntryHeader.d.ts +3 -1
- package/dist/editor/components/EntryHeader.js +2 -1
- package/dist/editor/components/TooltipDetail.d.ts +14 -0
- package/dist/editor/components/TooltipDetail.js +18 -0
- package/dist/editor/copy.d.ts +7 -0
- package/dist/editor/copy.js +9 -0
- package/dist/editor/hooks/useBranchManager.d.ts +8 -3
- package/dist/editor/hooks/useBranchManager.js +48 -5
- package/dist/pr-comments.d.ts +2 -0
- package/dist/pr-comments.js +20 -14
- package/dist/resource-generation.js +2 -3
- package/dist/submission-attribution.d.ts +1 -1
- package/dist/submission-attribution.js +20 -2
- package/dist/task-queue/task-queue.d.ts +5 -1
- package/dist/task-queue/task-queue.js +4 -2
- package/dist/types.d.ts +61 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/branch-merge.d.ts +68 -0
- package/dist/worker/branch-merge.js +228 -0
- package/dist/worker/canopy-state.d.ts +2 -0
- package/dist/worker/canopy-state.js +1 -1
- package/dist/worker/cms-worker.d.ts +1 -0
- package/dist/worker/cms-worker.js +5 -3
- package/dist/worker/git-sync.js +15 -8
- package/dist/worker/github-commits.d.ts +106 -0
- package/dist/worker/github-commits.js +485 -0
- package/dist/worker/github-gateway-http.js +3 -2
- package/dist/worker/github-gateway-wire.d.ts +22 -13
- package/dist/worker/github-gateway-wire.js +4 -1
- package/dist/worker/github-gateway.d.ts +16 -5
- package/dist/worker/github-gateway.js +22 -5
- package/dist/worker/github-mirror.d.ts +8 -0
- package/dist/worker/github-mirror.js +12 -1
- package/dist/worker/github-transfer.d.ts +3 -1
- package/dist/worker/github-transfer.js +3 -2
- package/dist/worker/history-rewrite.d.ts +13 -1
- package/dist/worker/history-rewrite.js +27 -3
- package/dist/worker/rebase.d.ts +13 -7
- package/dist/worker/rebase.js +351 -95
- package/dist/worker/task-runner.d.ts +1 -1
- package/dist/worker/task-runner.js +172 -34
- package/dist/worker/worker-context.d.ts +2 -0
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import { withdrawBranch } from './branch-withdraw.js';
|
|
|
6
6
|
import { requestChanges, approveBranch } from './branch-review.js';
|
|
7
7
|
import { markAsMerged } from './branch-merge.js';
|
|
8
8
|
import { defineEndpoint } from './route-builder.js';
|
|
9
|
-
import { canPerformWorkflowAction, getBranchProtection } from '../authorization/index.js';
|
|
9
|
+
import { canPerformWorkflowAction, getBranchProtection, isAdmin } from '../authorization/index.js';
|
|
10
10
|
import { syncSubmitPr } from './github-sync.js';
|
|
11
11
|
import { getErrorMessage, redactCredentials, sanitizeErrorMessage } from '../utils/error.js';
|
|
12
12
|
import { isNonFastForwardRejection } from '../utils/git.js';
|
|
@@ -85,6 +85,19 @@ const submitBranchForMergeHandler = async (gc, ctx, req, _params, body) => {
|
|
|
85
85
|
: `Cannot submit branch with status '${status}'. Only 'editing' branches can be submitted.`,
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
|
+
// The worker pushes nothing for such a branch, so this would commit and report a failure later.
|
|
89
|
+
if (branchContext.branch.rewrittenOnGitHub !== undefined) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
status: 409,
|
|
93
|
+
error: `"${shortNameOf(ctx.services.config, branchContext.branch.name)}" was changed on GitHub in ` +
|
|
94
|
+
"a way CanopyCMS can't combine with the work here, so it can't be submitted. Nothing is " +
|
|
95
|
+
'lost. ' +
|
|
96
|
+
(isAdmin(req.user.groups)
|
|
97
|
+
? 'See the "Branches" tab in "System health".'
|
|
98
|
+
: 'Ask an admin for help.'),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
88
101
|
// Commit and push changes
|
|
89
102
|
const submitter = submissionEditorFromUser(req.user);
|
|
90
103
|
const { authPlugin } = ctx;
|
|
@@ -122,28 +135,21 @@ const submitBranchForMergeHandler = async (gc, ctx, req, _params, body) => {
|
|
|
122
135
|
// (e.g. x-access-token:TOKEN@github.com in git errors) are redacted even
|
|
123
136
|
// from server logs — a live installation token must never persist in logs.
|
|
124
137
|
console.error(`CanopyCMS: Failed to push branch changes (${branchContext.branchRoot}):`, redactCredentials(message));
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
// names no cause: a foreign deployment cannot reach this repo, and the
|
|
133
|
-
// realistic explanations are all internal (the worker's rebase loop
|
|
134
|
-
// reconciling the branch, or a commit reaching remote.git that this
|
|
135
|
-
// workspace never had). It also never advises renaming the branch --
|
|
136
|
-
// a branch that reaches this point has usually been submitted before, so
|
|
137
|
-
// it may well have an open PR that a rename would orphan.
|
|
138
|
+
// This push targets the deployment's OWN remote.git, so a non-fast-forward
|
|
139
|
+
// rejection means it holds a commit this clone lacks, which reached it from
|
|
140
|
+
// GitHub (worker/github-commits.ts). The commit made above stays: the tree is
|
|
141
|
+
// then clean, which is what the worker's merge needs, and the next submit
|
|
142
|
+
// pushes the result. A branch that reaches this point has usually been
|
|
143
|
+
// submitted before, so the message never advises renaming it: that would
|
|
144
|
+
// orphan its open PR.
|
|
138
145
|
if (isNonFastForwardRejection(message)) {
|
|
139
146
|
return {
|
|
140
147
|
ok: false,
|
|
141
148
|
status: 409,
|
|
142
|
-
error: `Could not submit "${shortName}": it has
|
|
143
|
-
`
|
|
144
|
-
`
|
|
145
|
-
|
|
146
|
-
`to check the worker if it persists.`,
|
|
149
|
+
error: `Could not submit "${shortName}": it has commits from GitHub that aren't in the editor ` +
|
|
150
|
+
`yet. Your saved changes are safe. CanopyCMS brings those commits in at its next sync, ` +
|
|
151
|
+
`usually within a few minutes, so submit again then. If this keeps happening, ` +
|
|
152
|
+
`${isAdmin(req.user.groups) ? 'check' : 'ask an admin to check'} "System health".`,
|
|
147
153
|
};
|
|
148
154
|
}
|
|
149
155
|
return {
|
|
@@ -200,6 +206,11 @@ const submitBranchForMergeHandler = async (gc, ctx, req, _params, body) => {
|
|
|
200
206
|
// warning through review and archive.
|
|
201
207
|
rebaseFailure: undefined,
|
|
202
208
|
review: undefined,
|
|
209
|
+
// Read by the card as soon as the status says review, and the worker's next read is minutes
|
|
210
|
+
// away, so what held the last submit must not show as holding this one.
|
|
211
|
+
pullRequestHold: undefined,
|
|
212
|
+
submitUnfinished: prResult.submitNotQueued,
|
|
213
|
+
submitNotQueued: prResult.submitNotQueued,
|
|
203
214
|
},
|
|
204
215
|
});
|
|
205
216
|
return {
|
package/dist/api/branch.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import type { BranchContext, BranchMetadata } from '../types.js';
|
|
2
|
+
import type { BranchContext, BranchHold, BranchMetadata } from '../types.js';
|
|
3
3
|
import type { ApiContext, ApiRequest, ApiResponse } from './types.js';
|
|
4
4
|
import { branchParamSchema } from './validators.js';
|
|
5
5
|
import type { UserProfile } from '../auth/types.js';
|
|
@@ -15,7 +15,11 @@ import type { UserProfile } from '../auth/types.js';
|
|
|
15
15
|
* rather than silently behaving as if nothing changed. That is by design:
|
|
16
16
|
* wire compatibility here means "doesn't break", not "behaves the same".
|
|
17
17
|
*/
|
|
18
|
-
export interface BranchListItem extends Omit<BranchMetadata, 'review'> {
|
|
18
|
+
export interface BranchListItem extends Omit<BranchMetadata, 'review' | 'keptOverGitHub' | 'lastGitHubTip' | 'rewrittenOnGitHub' | 'submitUnfinished' | 'unpublishedAtMerge' | 'submitNotQueued' | 'rebaseFailure' | 'pullRequestHold'> {
|
|
19
|
+
/** What is stuck and needs a person, for the branch card. Names no file. */
|
|
20
|
+
hold?: BranchHold;
|
|
21
|
+
/** The last submit didn't finish, which the server refuses Approve for. */
|
|
22
|
+
approveBlocked?: boolean;
|
|
19
23
|
/**
|
|
20
24
|
* What pull request links carry for this branch, which `?branch=` and the `/:branch` segment
|
|
21
25
|
* accept beside `name` (branch-param.ts's `shortNameOf`): `name` less the namespace for a branch
|
|
@@ -32,7 +36,8 @@ export interface BranchListItem extends Omit<BranchMetadata, 'review'> {
|
|
|
32
36
|
*/
|
|
33
37
|
writeBlocked?: boolean;
|
|
34
38
|
/**
|
|
35
|
-
* Populated from {@link BranchWriteProtection.submitBlockedIncludingStatus}
|
|
39
|
+
* Populated from {@link BranchWriteProtection.submitBlockedIncludingStatus},
|
|
40
|
+
* or true for a branch rewritten on GitHub
|
|
36
41
|
* -- READ THAT DOC COMMENT before touching this field. On the wire this
|
|
37
42
|
* name means the COMPOUND answer (base-branch OR non-'editing' status),
|
|
38
43
|
* mirroring how `writeBlocked` above is the compound of `readOnly` + status
|
package/dist/api/branch.js
CHANGED
|
@@ -126,16 +126,36 @@ const accessForViewer = (access, viewer) => {
|
|
|
126
126
|
*/
|
|
127
127
|
export const toBranchListItem = (config, branch, viewer) => {
|
|
128
128
|
const protection = getBranchWriteProtection(config, branch.name, branch.baseBranch, branch.status);
|
|
129
|
-
// `review` names the reviewer, whom the editor has no use for.
|
|
130
|
-
|
|
129
|
+
// `review` names the reviewer, whom the editor has no use for. `keptOverGitHub` and
|
|
130
|
+
// `rebaseFailure` can name files, which a viewer's path rules can hide; System Health reads
|
|
131
|
+
// failures from the admin endpoint. The others are the worker's.
|
|
132
|
+
const { review: _review, keptOverGitHub: _kept, lastGitHubTip: _tip, rewrittenOnGitHub: _rewritten, submitUnfinished: _unfinished, submitNotQueued, unpublishedAtMerge: _unpublished, rebaseFailure: _failure, pullRequestHold, ...listed } = branch;
|
|
133
|
+
const inReview = branch.status === 'submitted' || branch.status === 'approved';
|
|
134
|
+
const rewritten = branch.rewrittenOnGitHub !== undefined && branch.status !== 'archived';
|
|
135
|
+
const hold = rewritten
|
|
136
|
+
? { kind: 'rewritten' }
|
|
137
|
+
: !inReview
|
|
138
|
+
? branch.unpublishedAtMerge
|
|
139
|
+
? { kind: 'unpublished-at-merge' }
|
|
140
|
+
: undefined
|
|
141
|
+
: branch.submitUnfinished
|
|
142
|
+
? {
|
|
143
|
+
kind: 'submit-unfinished',
|
|
144
|
+
opened: branch.pullRequestNumber !== undefined,
|
|
145
|
+
notQueued: submitNotQueued,
|
|
146
|
+
}
|
|
147
|
+
: pullRequestHold;
|
|
131
148
|
return {
|
|
132
149
|
...listed,
|
|
150
|
+
hold,
|
|
151
|
+
approveBlocked: (inReview && branch.submitUnfinished) || undefined,
|
|
133
152
|
shortName: shortNameOf(config, branch.name),
|
|
134
153
|
...accessForViewer(branch.access ?? {}, viewer),
|
|
135
154
|
isProtected: protection.isProtected,
|
|
136
155
|
readOnly: protection.readOnly,
|
|
137
156
|
writeBlocked: protection.writeBlocked,
|
|
138
|
-
|
|
157
|
+
// The worker refuses to push such a branch, so a submit could only fail later.
|
|
158
|
+
submitBlocked: protection.submitBlockedIncludingStatus || rewritten,
|
|
139
159
|
};
|
|
140
160
|
};
|
|
141
161
|
/**
|
|
@@ -572,6 +592,15 @@ export const deleteBranchHandler = async (ctx, req, params) => {
|
|
|
572
592
|
error: 'Cannot delete branch with open pull request',
|
|
573
593
|
};
|
|
574
594
|
}
|
|
595
|
+
// It may hold the only copy of commits the merge left out, and deleting it would drop the alert
|
|
596
|
+
// that says so.
|
|
597
|
+
if (branchContext.branch.unpublishedAtMerge && !isAdmin(req.user.groups)) {
|
|
598
|
+
return {
|
|
599
|
+
ok: false,
|
|
600
|
+
status: 409,
|
|
601
|
+
error: 'This branch may hold changes that never reached GitHub. Ask an admin to check it before deleting it.',
|
|
602
|
+
};
|
|
603
|
+
}
|
|
575
604
|
// The branch directory leaves its name by one rename, under the same
|
|
576
605
|
// server-enforced lockfile branch-metadata saves hold (see
|
|
577
606
|
// utils/occ-json-write.ts), so a save() that reaches that lock after the
|
package/dist/api/client.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ import type { ExternalGroupsResponse, InternalGroupsResponse, UpdateInternalGrou
|
|
|
23
23
|
import type { UserInfoResponse } from './user.js';
|
|
24
24
|
import type { AddEntryTypeApiResponse, CreateCollectionApiResponse, DeleteCollectionApiResponse, GetCollectionApiResponse, GetSchemaApiResponse, InvalidateSchemaCacheApiResponse, RemoveEntryTypeApiResponse, UpdateCollectionApiResponse, UpdateEntryTypeApiResponse, UpdateOrderApiResponse, UpdateOrderBody } from './schema.js';
|
|
25
25
|
import type { CreateCollectionInput, CreateEntryTypeInput, UpdateCollectionInput, UpdateEntryTypeInput } from './../schema/schema-store-types.js';
|
|
26
|
-
import type { AdminDeleteTaskResponse, AdminRetryTaskResponse, AdminStatusResponse, AdminTasksResponse, BranchHealthResponse, PurgeBranchDirResponse, RepairBranchDirResponse, RepairContentDuplicatesResponse } from './admin.js';
|
|
26
|
+
import type { AdminDeleteTaskResponse, AdminRetryTaskResponse, AdminStatusResponse, AdminTasksResponse, BranchHealthResponse, DismissUnpublishedResponse, PurgeBranchDirResponse, RepairBranchDirResponse, RepairContentDuplicatesResponse } from './admin.js';
|
|
27
27
|
/** Options for {@link createApiClient}. */
|
|
28
28
|
export interface ApiClientOptions {
|
|
29
29
|
/** Base URL for API requests. Defaults to '/api/canopycms'. */
|
|
@@ -206,6 +206,8 @@ export declare class CanopyApiClient {
|
|
|
206
206
|
purgeBranchDir: (params: Record<string, string>, options?: RequestOptions) => Promise<PurgeBranchDirResponse>;
|
|
207
207
|
/** POST /admin/branch-dirs/:dirName/repair-metadata */
|
|
208
208
|
repairBranchDir: (params: Record<string, string>, options?: RequestOptions) => Promise<RepairBranchDirResponse>;
|
|
209
|
+
/** POST /admin/branch-dirs/:dirName/dismiss-unpublished */
|
|
210
|
+
dismissUnpublished: (params: Record<string, string>, options?: RequestOptions) => Promise<DismissUnpublishedResponse>;
|
|
209
211
|
/** POST /admin/branch-dirs/:dirName/repair-content-duplicates */
|
|
210
212
|
repairContentDuplicates: (params: Record<string, string>, options?: RequestOptions) => Promise<RepairContentDuplicatesResponse>;
|
|
211
213
|
/** GET /admin/status */
|
package/dist/api/client.js
CHANGED
|
@@ -259,6 +259,10 @@ export class CanopyApiClient {
|
|
|
259
259
|
repairBranchDir: (params, options) => {
|
|
260
260
|
return this.request('POST', this.buildPath('/admin/branch-dirs/:dirName/repair-metadata', params), undefined, 'long', options);
|
|
261
261
|
},
|
|
262
|
+
/** POST /admin/branch-dirs/:dirName/dismiss-unpublished */
|
|
263
|
+
dismissUnpublished: (params, options) => {
|
|
264
|
+
return this.request('POST', this.buildPath('/admin/branch-dirs/:dirName/dismiss-unpublished', params), undefined, 'standard', options);
|
|
265
|
+
},
|
|
262
266
|
/** POST /admin/branch-dirs/:dirName/repair-content-duplicates */
|
|
263
267
|
repairContentDuplicates: (params, options) => {
|
|
264
268
|
return this.request('POST', this.buildPath('/admin/branch-dirs/:dirName/repair-content-duplicates', params), undefined, 'long', options);
|
|
@@ -14,6 +14,8 @@ export interface GitHubSyncResult {
|
|
|
14
14
|
nothingToSubmit?: boolean;
|
|
15
15
|
/** The direct path, whose submit pushes to GitHub itself: the branch is there whatever the PR call did. */
|
|
16
16
|
pushedToGitHub?: boolean;
|
|
17
|
+
/** No task runs this submit, so only submitting again finishes it. */
|
|
18
|
+
submitNotQueued?: boolean;
|
|
17
19
|
}
|
|
18
20
|
/** What the PR body records about a submit. */
|
|
19
21
|
export interface SubmissionRecord {
|
package/dist/api/github-sync.js
CHANGED
|
@@ -10,6 +10,8 @@ import { shortNameOf } from '../branch-param.js';
|
|
|
10
10
|
import { CommentStore } from '../comment-store.js';
|
|
11
11
|
import { prBodyLockKey, refreshCommentsBlock } from '../pr-comments.js';
|
|
12
12
|
import { withLock } from '../utils/async-mutex.js';
|
|
13
|
+
/** A submit's commits reached this deployment's repository, and nothing will take them further. */
|
|
14
|
+
const SUBMIT_NOT_QUEUED_REASON = "This submit couldn't be queued, so nothing reached GitHub. Nothing is lost.";
|
|
13
15
|
/** The branch's comment threads for the PR body, or null (said so in the body) if unreadable. */
|
|
14
16
|
async function readThreadsOrWarn(context) {
|
|
15
17
|
try {
|
|
@@ -153,7 +155,7 @@ export async function syncSubmitPr(ctx, context, submission) {
|
|
|
153
155
|
// recorded the number, the next submit would otherwise re-enqueue
|
|
154
156
|
// 'push-and-create-pr' and 422 on GitHub's duplicate-PR check, wedging the
|
|
155
157
|
// branch in 'sync-failed' with no way to recover.
|
|
156
|
-
|
|
158
|
+
const queued = await enqueueGitHubTask(ctx, context, {
|
|
157
159
|
action: 'push-and-create-or-update-pr',
|
|
158
160
|
payload: {
|
|
159
161
|
branch: context.branch.name,
|
|
@@ -171,6 +173,9 @@ export async function syncSubmitPr(ctx, context, submission) {
|
|
|
171
173
|
submittedAt: submission.submittedAt,
|
|
172
174
|
},
|
|
173
175
|
});
|
|
176
|
+
return queued.syncStatus === 'sync-failed'
|
|
177
|
+
? { ...queued, syncFailureReason: SUBMIT_NOT_QUEUED_REASON, submitNotQueued: true }
|
|
178
|
+
: queued;
|
|
174
179
|
}
|
|
175
180
|
/**
|
|
176
181
|
* Used by withdraw and request-changes. Uses githubService directly if
|
|
@@ -66,7 +66,9 @@ export async function syncPrComments(ctx, context) {
|
|
|
66
66
|
const pending = await listTasks(taskDir, 'pending', Number.MAX_SAFE_INTEGER);
|
|
67
67
|
const queued = pending.some((t) => t.action === SYNC_PR_COMMENTS_ACTION &&
|
|
68
68
|
t.payload.branch === branch &&
|
|
69
|
-
t.payload.pullRequestNumber === prNumber
|
|
69
|
+
t.payload.pullRequestNumber === prNumber &&
|
|
70
|
+
// The worker's own task leaves the comment summary as it is, so it carries no comment.
|
|
71
|
+
t.payload.reviewBlockOnly !== true);
|
|
70
72
|
if (queued)
|
|
71
73
|
return;
|
|
72
74
|
await enqueueTask(taskDir, {
|
|
@@ -82,9 +82,31 @@ export declare const branchMetadataSchema: z.ZodObject<{
|
|
|
82
82
|
lastAt: string;
|
|
83
83
|
}>>;
|
|
84
84
|
historyRewrittenFrom: z.ZodOptional<z.ZodString>;
|
|
85
|
+
lastGitHubTip: z.ZodOptional<z.ZodString>;
|
|
86
|
+
keptOverGitHub: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
87
|
+
rewrittenOnGitHub: z.ZodOptional<z.ZodString>;
|
|
88
|
+
submitUnfinished: z.ZodOptional<z.ZodBoolean>;
|
|
89
|
+
submitNotQueued: z.ZodOptional<z.ZodBoolean>;
|
|
90
|
+
unpublishedAtMerge: z.ZodOptional<z.ZodBoolean>;
|
|
85
91
|
syncFailureReason: z.ZodOptional<z.ZodString>;
|
|
86
92
|
editors: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
87
93
|
uncommittedEditors: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
94
|
+
pullRequestHold: z.ZodCatch<z.ZodOptional<z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
|
|
95
|
+
kind: z.ZodLiteral<"conflicts">;
|
|
96
|
+
}, "strip", z.ZodTypeAny, {
|
|
97
|
+
kind: "conflicts";
|
|
98
|
+
}, {
|
|
99
|
+
kind: "conflicts";
|
|
100
|
+
}>, z.ZodObject<{
|
|
101
|
+
kind: z.ZodLiteral<"retargeted">;
|
|
102
|
+
base: z.ZodString;
|
|
103
|
+
}, "strip", z.ZodTypeAny, {
|
|
104
|
+
kind: "retargeted";
|
|
105
|
+
base: string;
|
|
106
|
+
}, {
|
|
107
|
+
kind: "retargeted";
|
|
108
|
+
base: string;
|
|
109
|
+
}>]>>>;
|
|
88
110
|
review: z.ZodCatch<z.ZodOptional<z.ZodObject<{
|
|
89
111
|
state: z.ZodEnum<["approved", "changes-requested"]>;
|
|
90
112
|
by: z.ZodObject<{
|
|
@@ -175,9 +197,31 @@ export declare const branchMetadataSchema: z.ZodObject<{
|
|
|
175
197
|
lastAt: string;
|
|
176
198
|
}>>;
|
|
177
199
|
historyRewrittenFrom: z.ZodOptional<z.ZodString>;
|
|
200
|
+
lastGitHubTip: z.ZodOptional<z.ZodString>;
|
|
201
|
+
keptOverGitHub: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
202
|
+
rewrittenOnGitHub: z.ZodOptional<z.ZodString>;
|
|
203
|
+
submitUnfinished: z.ZodOptional<z.ZodBoolean>;
|
|
204
|
+
submitNotQueued: z.ZodOptional<z.ZodBoolean>;
|
|
205
|
+
unpublishedAtMerge: z.ZodOptional<z.ZodBoolean>;
|
|
178
206
|
syncFailureReason: z.ZodOptional<z.ZodString>;
|
|
179
207
|
editors: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
180
208
|
uncommittedEditors: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
209
|
+
pullRequestHold: z.ZodCatch<z.ZodOptional<z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
|
|
210
|
+
kind: z.ZodLiteral<"conflicts">;
|
|
211
|
+
}, "strip", z.ZodTypeAny, {
|
|
212
|
+
kind: "conflicts";
|
|
213
|
+
}, {
|
|
214
|
+
kind: "conflicts";
|
|
215
|
+
}>, z.ZodObject<{
|
|
216
|
+
kind: z.ZodLiteral<"retargeted">;
|
|
217
|
+
base: z.ZodString;
|
|
218
|
+
}, "strip", z.ZodTypeAny, {
|
|
219
|
+
kind: "retargeted";
|
|
220
|
+
base: string;
|
|
221
|
+
}, {
|
|
222
|
+
kind: "retargeted";
|
|
223
|
+
base: string;
|
|
224
|
+
}>]>>>;
|
|
181
225
|
review: z.ZodCatch<z.ZodOptional<z.ZodObject<{
|
|
182
226
|
state: z.ZodEnum<["approved", "changes-requested"]>;
|
|
183
227
|
by: z.ZodObject<{
|
|
@@ -268,9 +312,31 @@ export declare const branchMetadataSchema: z.ZodObject<{
|
|
|
268
312
|
lastAt: string;
|
|
269
313
|
}>>;
|
|
270
314
|
historyRewrittenFrom: z.ZodOptional<z.ZodString>;
|
|
315
|
+
lastGitHubTip: z.ZodOptional<z.ZodString>;
|
|
316
|
+
keptOverGitHub: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
317
|
+
rewrittenOnGitHub: z.ZodOptional<z.ZodString>;
|
|
318
|
+
submitUnfinished: z.ZodOptional<z.ZodBoolean>;
|
|
319
|
+
submitNotQueued: z.ZodOptional<z.ZodBoolean>;
|
|
320
|
+
unpublishedAtMerge: z.ZodOptional<z.ZodBoolean>;
|
|
271
321
|
syncFailureReason: z.ZodOptional<z.ZodString>;
|
|
272
322
|
editors: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
273
323
|
uncommittedEditors: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
324
|
+
pullRequestHold: z.ZodCatch<z.ZodOptional<z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
|
|
325
|
+
kind: z.ZodLiteral<"conflicts">;
|
|
326
|
+
}, "strip", z.ZodTypeAny, {
|
|
327
|
+
kind: "conflicts";
|
|
328
|
+
}, {
|
|
329
|
+
kind: "conflicts";
|
|
330
|
+
}>, z.ZodObject<{
|
|
331
|
+
kind: z.ZodLiteral<"retargeted">;
|
|
332
|
+
base: z.ZodString;
|
|
333
|
+
}, "strip", z.ZodTypeAny, {
|
|
334
|
+
kind: "retargeted";
|
|
335
|
+
base: string;
|
|
336
|
+
}, {
|
|
337
|
+
kind: "retargeted";
|
|
338
|
+
base: string;
|
|
339
|
+
}>]>>>;
|
|
274
340
|
review: z.ZodCatch<z.ZodOptional<z.ZodObject<{
|
|
275
341
|
state: z.ZodEnum<["approved", "changes-requested"]>;
|
|
276
342
|
by: z.ZodObject<{
|
|
@@ -81,9 +81,23 @@ const branchMetadataShape = {
|
|
|
81
81
|
.object({ message: z.string(), firstAt: z.string(), lastAt: z.string() })
|
|
82
82
|
.optional(),
|
|
83
83
|
historyRewrittenFrom: z.string().optional(),
|
|
84
|
+
lastGitHubTip: z.string().optional(),
|
|
85
|
+
keptOverGitHub: z.array(z.string()).optional(),
|
|
86
|
+
rewrittenOnGitHub: z.string().optional(),
|
|
87
|
+
submitUnfinished: z.boolean().optional(),
|
|
88
|
+
submitNotQueued: z.boolean().optional(),
|
|
89
|
+
unpublishedAtMerge: z.boolean().optional(),
|
|
84
90
|
syncFailureReason: z.string().optional(),
|
|
85
91
|
editors: z.array(z.string()).optional(),
|
|
86
92
|
uncommittedEditors: z.array(z.string()).optional(),
|
|
93
|
+
// Dropped when malformed: it only decorates the branch card, so it never fails a read.
|
|
94
|
+
pullRequestHold: z
|
|
95
|
+
.discriminatedUnion('kind', [
|
|
96
|
+
z.object({ kind: z.literal('conflicts') }),
|
|
97
|
+
z.object({ kind: z.literal('retargeted'), base: z.string() }),
|
|
98
|
+
])
|
|
99
|
+
.optional()
|
|
100
|
+
.catch(undefined),
|
|
87
101
|
// Dropped when malformed: it only decorates the PR body, so it never fails a read.
|
|
88
102
|
review: z
|
|
89
103
|
.object({
|
|
@@ -119,8 +119,10 @@ export declare function buildInitialBranchMetadata(incoming: BranchMetadataUpdat
|
|
|
119
119
|
export interface BranchMetadataUpdate {
|
|
120
120
|
branch?: Partial<Omit<BranchMetadata, 'createdAt' | 'updatedAt'>>;
|
|
121
121
|
}
|
|
122
|
+
/** A failed submit whose pull request step hasn't run; no other task's success clears its failure. */
|
|
123
|
+
export declare const awaitsSubmitRetry: ({ submitUnfinished, status }: BranchMetadata) => boolean;
|
|
122
124
|
/**
|
|
123
|
-
* The metadata update for archiving
|
|
125
|
+
* The metadata update for archiving `branch`, whose PR merged. Shared by the
|
|
124
126
|
* worker's merge-poll (CmsWorker.pollMergeState) and the manual markAsMerged
|
|
125
127
|
* API (api/branch-merge.ts), so both produce identical metadata.
|
|
126
128
|
*
|
|
@@ -128,7 +130,7 @@ export interface BranchMetadataUpdate {
|
|
|
128
130
|
* existing values for fields the incoming update omits, so the branch's ref and
|
|
129
131
|
* its recorded PR number and URL survive untouched.
|
|
130
132
|
*/
|
|
131
|
-
export declare function buildMergedBranchUpdate(now?: Date): NonNullable<BranchMetadataUpdate['branch']>;
|
|
133
|
+
export declare function buildMergedBranchUpdate(branch: BranchMetadata, now?: Date): NonNullable<BranchMetadataUpdate['branch']>;
|
|
132
134
|
/** {@link BranchMetadataFileManager.get} as a function. */
|
|
133
135
|
export declare const getBranchMetadataFileManager: (branchRoot: string, baseRoot: string, options?: {
|
|
134
136
|
settleMs?: number;
|
package/dist/branch-metadata.js
CHANGED
|
@@ -267,8 +267,14 @@ function mergeBranchMetadata(existing, version, incoming, now = new Date().toISO
|
|
|
267
267
|
export function buildInitialBranchMetadata(incoming, now) {
|
|
268
268
|
return mergeBranchMetadata(null, null, incoming, now);
|
|
269
269
|
}
|
|
270
|
+
/** A failed submit whose pull request step hasn't run; no other task's success clears its failure. */
|
|
271
|
+
export const awaitsSubmitRetry = ({ submitUnfinished, status }) => submitUnfinished === true && (status === 'submitted' || status === 'approved');
|
|
272
|
+
/** Only a submit writes `pending-sync` to branch.json, so that too is a submit not yet finished. */
|
|
273
|
+
const owesSubmit = (branch) => awaitsSubmitRetry(branch) ||
|
|
274
|
+
(branch.syncStatus === 'pending-sync' &&
|
|
275
|
+
(branch.status === 'submitted' || branch.status === 'approved'));
|
|
270
276
|
/**
|
|
271
|
-
* The metadata update for archiving
|
|
277
|
+
* The metadata update for archiving `branch`, whose PR merged. Shared by the
|
|
272
278
|
* worker's merge-poll (CmsWorker.pollMergeState) and the manual markAsMerged
|
|
273
279
|
* API (api/branch-merge.ts), so both produce identical metadata.
|
|
274
280
|
*
|
|
@@ -276,7 +282,7 @@ export function buildInitialBranchMetadata(incoming, now) {
|
|
|
276
282
|
* existing values for fields the incoming update omits, so the branch's ref and
|
|
277
283
|
* its recorded PR number and URL survive untouched.
|
|
278
284
|
*/
|
|
279
|
-
export function buildMergedBranchUpdate(now = new Date()) {
|
|
285
|
+
export function buildMergedBranchUpdate(branch, now = new Date()) {
|
|
280
286
|
return {
|
|
281
287
|
status: 'archived',
|
|
282
288
|
pullRequestState: 'merged',
|
|
@@ -284,6 +290,8 @@ export function buildMergedBranchUpdate(now = new Date()) {
|
|
|
284
290
|
// The merge settled every conflict the record named.
|
|
285
291
|
conflictStatus: 'clean',
|
|
286
292
|
conflictFiles: [],
|
|
293
|
+
submitUnfinished: undefined,
|
|
294
|
+
...(owesSubmit(branch) ? { unpublishedAtMerge: true } : {}),
|
|
287
295
|
};
|
|
288
296
|
}
|
|
289
297
|
/** {@link BranchMetadataFileManager.get} as a function. */
|
package/dist/cli/cli.js
CHANGED
|
@@ -562,7 +562,7 @@ async function failTask(taskDir, taskId, error, logger = nullLogger) {
|
|
|
562
562
|
});
|
|
563
563
|
logger.debug("Failed task", { id: taskId, error });
|
|
564
564
|
}
|
|
565
|
-
async function retryTask(taskDir, taskId, error, logger = nullLogger) {
|
|
565
|
+
async function retryTask(taskDir, taskId, error, logger = nullLogger, options = {}) {
|
|
566
566
|
const processingPath = path6.join(taskDir, "processing", `${taskId}.json`);
|
|
567
567
|
const pendingDir = path6.join(taskDir, "pending");
|
|
568
568
|
const pendingPath = path6.join(pendingDir, `${taskId}.json`);
|
|
@@ -583,7 +583,7 @@ async function retryTask(taskDir, taskId, error, logger = nullLogger) {
|
|
|
583
583
|
throw err2;
|
|
584
584
|
}
|
|
585
585
|
const retryCount = (task.retryCount ?? 0) + 1;
|
|
586
|
-
const backoffMs = Math.min(5e3 * Math.pow(2, retryCount - 1), 6e4);
|
|
586
|
+
const backoffMs = options.delayMs ?? Math.min(5e3 * Math.pow(2, retryCount - 1), 6e4);
|
|
587
587
|
task.status = "pending";
|
|
588
588
|
task.retryCount = retryCount;
|
|
589
589
|
task.retryAfter = new Date(Date.now() + backoffMs).toISOString();
|
|
@@ -5357,9 +5357,7 @@ async function bumpResourceGeneration(root, resource, options) {
|
|
|
5357
5357
|
await atomicWriteFile2(resourceGenerationPath(root, resource), token);
|
|
5358
5358
|
return token;
|
|
5359
5359
|
} catch (err2) {
|
|
5360
|
-
|
|
5361
|
-
error: getErrorMessage3(err2)
|
|
5362
|
-
});
|
|
5360
|
+
canopyLogWarn2(`CanopyCMS: failed to bump ${resource} generation for ${root}:`, getErrorMessage3(err2));
|
|
5363
5361
|
if (options?.mustSucceed)
|
|
5364
5362
|
throw err2;
|
|
5365
5363
|
return null;
|
|
@@ -10746,6 +10744,7 @@ var init_generate_ai_content = __esm({
|
|
|
10746
10744
|
init_field_traversal();
|
|
10747
10745
|
init_atomic_write2();
|
|
10748
10746
|
init_error3();
|
|
10747
|
+
init_logger2();
|
|
10749
10748
|
log4 = createDebugLogger2({ prefix: "ResourceGeneration" });
|
|
10750
10749
|
META_DIR2 = ".canopy-meta";
|
|
10751
10750
|
init_logger2();
|
|
@@ -12394,9 +12393,20 @@ var init_generate_ai_content = __esm({
|
|
|
12394
12393
|
mergedAt: z72.string().optional(),
|
|
12395
12394
|
rebaseFailure: z72.object({ message: z72.string(), firstAt: z72.string(), lastAt: z72.string() }).optional(),
|
|
12396
12395
|
historyRewrittenFrom: z72.string().optional(),
|
|
12396
|
+
lastGitHubTip: z72.string().optional(),
|
|
12397
|
+
keptOverGitHub: z72.array(z72.string()).optional(),
|
|
12398
|
+
rewrittenOnGitHub: z72.string().optional(),
|
|
12399
|
+
submitUnfinished: z72.boolean().optional(),
|
|
12400
|
+
submitNotQueued: z72.boolean().optional(),
|
|
12401
|
+
unpublishedAtMerge: z72.boolean().optional(),
|
|
12397
12402
|
syncFailureReason: z72.string().optional(),
|
|
12398
12403
|
editors: z72.array(z72.string()).optional(),
|
|
12399
12404
|
uncommittedEditors: z72.array(z72.string()).optional(),
|
|
12405
|
+
// Dropped when malformed: it only decorates the branch card, so it never fails a read.
|
|
12406
|
+
pullRequestHold: z72.discriminatedUnion("kind", [
|
|
12407
|
+
z72.object({ kind: z72.literal("conflicts") }),
|
|
12408
|
+
z72.object({ kind: z72.literal("retargeted"), base: z72.string() })
|
|
12409
|
+
]).optional().catch(void 0),
|
|
12400
12410
|
// Dropped when malformed: it only decorates the PR body, so it never fails a read.
|
|
12401
12411
|
review: z72.object({
|
|
12402
12412
|
state: z72.enum(["approved", "changes-requested"]),
|
|
@@ -17848,9 +17858,7 @@ async function bumpResourceGeneration2(root, resource, options) {
|
|
|
17848
17858
|
await atomicWriteFile3(resourceGenerationPath2(root, resource), token);
|
|
17849
17859
|
return token;
|
|
17850
17860
|
} catch (err2) {
|
|
17851
|
-
|
|
17852
|
-
error: getErrorMessage(err2)
|
|
17853
|
-
});
|
|
17861
|
+
canopyLogWarn3(`CanopyCMS: failed to bump ${resource} generation for ${root}:`, getErrorMessage(err2));
|
|
17854
17862
|
if (options?.mustSucceed)
|
|
17855
17863
|
throw err2;
|
|
17856
17864
|
return null;
|
|
@@ -17879,6 +17887,7 @@ var init_resource_generation = __esm({
|
|
|
17879
17887
|
init_atomic_write3();
|
|
17880
17888
|
init_error();
|
|
17881
17889
|
init_debug2();
|
|
17890
|
+
init_logger3();
|
|
17882
17891
|
log11 = createDebugLogger3({ prefix: "ResourceGeneration" });
|
|
17883
17892
|
META_DIR3 = ".canopy-meta";
|
|
17884
17893
|
}
|
|
@@ -19206,9 +19215,20 @@ var init_branch_metadata_file = __esm({
|
|
|
19206
19215
|
mergedAt: z18.string().optional(),
|
|
19207
19216
|
rebaseFailure: z18.object({ message: z18.string(), firstAt: z18.string(), lastAt: z18.string() }).optional(),
|
|
19208
19217
|
historyRewrittenFrom: z18.string().optional(),
|
|
19218
|
+
lastGitHubTip: z18.string().optional(),
|
|
19219
|
+
keptOverGitHub: z18.array(z18.string()).optional(),
|
|
19220
|
+
rewrittenOnGitHub: z18.string().optional(),
|
|
19221
|
+
submitUnfinished: z18.boolean().optional(),
|
|
19222
|
+
submitNotQueued: z18.boolean().optional(),
|
|
19223
|
+
unpublishedAtMerge: z18.boolean().optional(),
|
|
19209
19224
|
syncFailureReason: z18.string().optional(),
|
|
19210
19225
|
editors: z18.array(z18.string()).optional(),
|
|
19211
19226
|
uncommittedEditors: z18.array(z18.string()).optional(),
|
|
19227
|
+
// Dropped when malformed: it only decorates the branch card, so it never fails a read.
|
|
19228
|
+
pullRequestHold: z18.discriminatedUnion("kind", [
|
|
19229
|
+
z18.object({ kind: z18.literal("conflicts") }),
|
|
19230
|
+
z18.object({ kind: z18.literal("retargeted"), base: z18.string() })
|
|
19231
|
+
]).optional().catch(void 0),
|
|
19212
19232
|
// Dropped when malformed: it only decorates the PR body, so it never fails a read.
|
|
19213
19233
|
review: z18.object({
|
|
19214
19234
|
state: z18.enum(["approved", "changes-requested"]),
|
|
@@ -4404,6 +4404,7 @@ init_error();
|
|
|
4404
4404
|
import fs5 from "node:fs/promises";
|
|
4405
4405
|
import path7 from "node:path";
|
|
4406
4406
|
import { randomUUID } from "node:crypto";
|
|
4407
|
+
init_logger();
|
|
4407
4408
|
var log4 = createDebugLogger({ prefix: "ResourceGeneration" });
|
|
4408
4409
|
var META_DIR2 = ".canopy-meta";
|
|
4409
4410
|
function resourceGenerationPath(root, resource) {
|
|
@@ -4415,9 +4416,7 @@ async function bumpResourceGeneration(root, resource, options) {
|
|
|
4415
4416
|
await atomicWriteFile(resourceGenerationPath(root, resource), token);
|
|
4416
4417
|
return token;
|
|
4417
4418
|
} catch (err) {
|
|
4418
|
-
|
|
4419
|
-
error: getErrorMessage(err)
|
|
4420
|
-
});
|
|
4419
|
+
canopyLogWarn(`CanopyCMS: failed to bump ${resource} generation for ${root}:`, getErrorMessage(err));
|
|
4421
4420
|
if (options?.mustSucceed)
|
|
4422
4421
|
throw err;
|
|
4423
4422
|
return null;
|
|
@@ -7848,9 +7847,20 @@ var branchMetadataShape = {
|
|
|
7848
7847
|
mergedAt: z7.string().optional(),
|
|
7849
7848
|
rebaseFailure: z7.object({ message: z7.string(), firstAt: z7.string(), lastAt: z7.string() }).optional(),
|
|
7850
7849
|
historyRewrittenFrom: z7.string().optional(),
|
|
7850
|
+
lastGitHubTip: z7.string().optional(),
|
|
7851
|
+
keptOverGitHub: z7.array(z7.string()).optional(),
|
|
7852
|
+
rewrittenOnGitHub: z7.string().optional(),
|
|
7853
|
+
submitUnfinished: z7.boolean().optional(),
|
|
7854
|
+
submitNotQueued: z7.boolean().optional(),
|
|
7855
|
+
unpublishedAtMerge: z7.boolean().optional(),
|
|
7851
7856
|
syncFailureReason: z7.string().optional(),
|
|
7852
7857
|
editors: z7.array(z7.string()).optional(),
|
|
7853
7858
|
uncommittedEditors: z7.array(z7.string()).optional(),
|
|
7859
|
+
// Dropped when malformed: it only decorates the branch card, so it never fails a read.
|
|
7860
|
+
pullRequestHold: z7.discriminatedUnion("kind", [
|
|
7861
|
+
z7.object({ kind: z7.literal("conflicts") }),
|
|
7862
|
+
z7.object({ kind: z7.literal("retargeted"), base: z7.string() })
|
|
7863
|
+
]).optional().catch(void 0),
|
|
7854
7864
|
// Dropped when malformed: it only decorates the PR body, so it never fails a read.
|
|
7855
7865
|
review: z7.object({
|
|
7856
7866
|
state: z7.enum(["approved", "changes-requested"]),
|
package/dist/cli/init.js
CHANGED
|
@@ -1207,7 +1207,7 @@ async function failTask(taskDir, taskId, error, logger = nullLogger) {
|
|
|
1207
1207
|
});
|
|
1208
1208
|
logger.debug("Failed task", { id: taskId, error });
|
|
1209
1209
|
}
|
|
1210
|
-
async function retryTask(taskDir, taskId, error, logger = nullLogger) {
|
|
1210
|
+
async function retryTask(taskDir, taskId, error, logger = nullLogger, options = {}) {
|
|
1211
1211
|
const processingPath = path6.join(taskDir, "processing", `${taskId}.json`);
|
|
1212
1212
|
const pendingDir = path6.join(taskDir, "pending");
|
|
1213
1213
|
const pendingPath = path6.join(pendingDir, `${taskId}.json`);
|
|
@@ -1228,7 +1228,7 @@ async function retryTask(taskDir, taskId, error, logger = nullLogger) {
|
|
|
1228
1228
|
throw err;
|
|
1229
1229
|
}
|
|
1230
1230
|
const retryCount = (task.retryCount ?? 0) + 1;
|
|
1231
|
-
const backoffMs = Math.min(5e3 * Math.pow(2, retryCount - 1), 6e4);
|
|
1231
|
+
const backoffMs = options.delayMs ?? Math.min(5e3 * Math.pow(2, retryCount - 1), 6e4);
|
|
1232
1232
|
task.status = "pending";
|
|
1233
1233
|
task.retryCount = retryCount;
|
|
1234
1234
|
task.retryAfter = new Date(Date.now() + backoffMs).toISOString();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { OperatingMode } from '../operating-mode/index.js';
|
|
3
|
-
import type { ConflictStatus, PullRequestState, SyncStatus } from '../types.js';
|
|
3
|
+
import type { BranchHold, ConflictStatus, PullRequestState, SyncStatus } from '../types.js';
|
|
4
4
|
import type { CommentThread } from '../comment-store.js';
|
|
5
5
|
import type { UserSearchResult } from '../auth/types.js';
|
|
6
6
|
import type { CreateRefusal } from './hooks/create-branch-request.js';
|
|
@@ -32,17 +32,21 @@ export interface BranchSummary {
|
|
|
32
32
|
conflictStatus?: ConflictStatus;
|
|
33
33
|
/** ContentIds of entries where --theirs was applied during rebase; cleared on clean rebase */
|
|
34
34
|
conflictFiles?: string[];
|
|
35
|
+
/** What is stuck and needs a person (api/branch.ts). */
|
|
36
|
+
hold?: BranchHold;
|
|
35
37
|
commentCount?: number;
|
|
36
38
|
isProtected?: boolean;
|
|
37
39
|
readOnly?: boolean;
|
|
38
40
|
/**
|
|
39
|
-
* Server-computed compound submit rule (base-branch
|
|
40
|
-
* status) -- see `useBranchManager.tsx`'s
|
|
41
|
-
* `BranchWriteProtection.submitBlockedIncludingStatus` for the full
|
|
41
|
+
* Server-computed compound submit rule (base-branch, non-'editing'
|
|
42
|
+
* status, or rewritten on GitHub) -- see `useBranchManager.tsx`'s
|
|
43
|
+
* `BranchSummary.submitBlocked` and `BranchWriteProtection.submitBlockedIncludingStatus` for the full
|
|
42
44
|
* rationale. `getBranchPermissions`'s `canSubmit` consumes this directly
|
|
43
45
|
* instead of re-deriving `status === 'editing' && !isProtected`.
|
|
44
46
|
*/
|
|
45
47
|
submitBlocked?: boolean;
|
|
48
|
+
/** Server-computed: the API refuses Approve, as the last submit didn't finish. */
|
|
49
|
+
approveBlocked?: boolean;
|
|
46
50
|
}
|
|
47
51
|
export interface UserContext {
|
|
48
52
|
userId: string;
|