@wardby/cli 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +8 -1
- package/README.md +3 -1
- package/deploy/local/docker-compose.quickstart-coding.yml +27 -0
- package/dist/claude-coding-worker/driver.d.ts +2 -0
- package/dist/claude-coding-worker/driver.js +19 -3
- package/dist/claude-coding-worker/sdk.js +1 -1
- package/dist/cli.js +41 -22
- package/dist/coding/continuation-wording.d.ts +10 -0
- package/dist/coding/continuation-wording.js +13 -0
- package/dist/coding/docker-preflight.d.ts +12 -0
- package/dist/coding/docker-preflight.js +40 -0
- package/dist/coding/local-git.d.ts +22 -0
- package/dist/coding/local-git.js +102 -0
- package/dist/coding/local-repo-status.d.ts +7 -0
- package/dist/coding/local-repo-status.js +56 -0
- package/dist/coding/local-repo.d.ts +17 -0
- package/dist/coding/local-repo.js +65 -0
- package/dist/coding/observability.d.ts +1 -1
- package/dist/coding/observability.js +1 -0
- package/dist/coding/profile.d.ts +6 -0
- package/dist/coding/profile.js +13 -4
- package/dist/coding/protocol.d.ts +32 -7
- package/dist/coding/protocol.js +66 -4
- package/dist/coding-worker/artifact.d.ts +1 -0
- package/dist/coding-worker/errors.js +2 -0
- package/dist/config/providers.d.ts +13 -0
- package/dist/config/providers.js +21 -9
- package/dist/core/attribution.d.ts +13 -0
- package/dist/core/attribution.js +30 -0
- package/dist/core/budget-groups.d.ts +3 -1
- package/dist/core/budget-groups.js +53 -5
- package/dist/core/ci-context.d.ts +8 -0
- package/dist/core/ci-context.js +59 -0
- package/dist/core/delegation-siblings.d.ts +22 -0
- package/dist/core/delegation-siblings.js +40 -0
- package/dist/core/dispatch.d.ts +28 -4
- package/dist/core/dispatch.js +57 -15
- package/dist/core/engine-native.d.ts +16 -0
- package/dist/core/engine-native.js +49 -2
- package/dist/core/host-events.d.ts +25 -2
- package/dist/core/host-events.js +361 -19
- package/dist/core/host-status.d.ts +8 -3
- package/dist/core/host-status.js +38 -7
- package/dist/core/issue-events.d.ts +2 -6
- package/dist/core/issue-events.js +13 -19
- package/dist/core/pull-request-state-sync.d.ts +17 -0
- package/dist/core/pull-request-state-sync.js +53 -0
- package/dist/core/reconciler.d.ts +10 -1
- package/dist/core/reconciler.js +15 -2
- package/dist/core/related-pull-requests.d.ts +95 -0
- package/dist/core/related-pull-requests.js +338 -0
- package/dist/core/repo-access.d.ts +5 -1
- package/dist/core/repo-access.js +17 -1
- package/dist/core/review-fix-ledger.d.ts +20 -0
- package/dist/core/review-fix-ledger.js +41 -0
- package/dist/core/review-fix.d.ts +60 -0
- package/dist/core/review-fix.js +155 -0
- package/dist/core/review-host-tools.d.ts +13 -2
- package/dist/core/review-host-tools.js +72 -11
- package/dist/core/runner.d.ts +2 -2
- package/dist/core/runner.js +379 -184
- package/dist/core/serial-gate.d.ts +11 -0
- package/dist/core/serial-gate.js +19 -0
- package/dist/core/timing.d.ts +7 -0
- package/dist/core/timing.js +7 -0
- package/dist/generated/prisma/browser.d.ts +23 -0
- package/dist/generated/prisma/client.d.ts +23 -0
- package/dist/generated/prisma/commonInputTypes.d.ts +22 -0
- package/dist/generated/prisma/internal/class.d.ts +33 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +274 -1
- package/dist/generated/prisma/internal/prismaNamespace.js +47 -2
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +48 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +47 -2
- package/dist/generated/prisma/models/Agent.d.ts +422 -1
- package/dist/generated/prisma/models/AgentRepository.d.ts +120 -2
- package/dist/generated/prisma/models/CodingAgentProfile.d.ts +42 -1
- package/dist/generated/prisma/models/CodingRun.d.ts +205 -1
- package/dist/generated/prisma/models/DeferredReview.d.ts +1336 -0
- package/dist/generated/prisma/models/DeferredReview.js +1 -0
- package/dist/generated/prisma/models/LocalPullRequest.d.ts +1384 -0
- package/dist/generated/prisma/models/LocalPullRequest.js +1 -0
- package/dist/generated/prisma/models/LocalReview.d.ts +1315 -0
- package/dist/generated/prisma/models/LocalReview.js +1 -0
- package/dist/generated/prisma/models/Run.d.ts +223 -0
- package/dist/generated/prisma/models/RunHostCheck.d.ts +148 -1
- package/dist/generated/prisma/models.d.ts +3 -0
- package/dist/help-index.json +476 -37
- package/dist/import/neutral-schema.d.ts +2 -2
- package/dist/mcp/auth/access.d.ts +3 -1
- package/dist/mcp/auth/host-account-cli.js +2 -2
- package/dist/mcp/auth/repo-authorization.d.ts +6 -7
- package/dist/mcp/auth/repo-authorization.js +21 -0
- package/dist/mcp/index.js +4 -1
- package/dist/mcp/tools/agents.js +46 -4
- package/dist/mcp/tools/host-accounts.js +2 -2
- package/dist/mcp/tools/repositories.js +71 -12
- package/dist/mcp/tools/runs.js +23 -0
- package/dist/mcp/tools/trigger.js +153 -24
- package/dist/providers/engine/types.d.ts +11 -0
- package/dist/providers/executor/composition.js +12 -7
- package/dist/providers/executor/container.d.ts +40 -3
- package/dist/providers/executor/container.js +140 -9
- package/dist/providers/executor/types.d.ts +1 -1
- package/dist/providers/jobs/fake-kubernetes-api.d.ts +3 -2
- package/dist/providers/jobs/fake-kubernetes-api.js +3 -0
- package/dist/providers/jobs/kubernetes-api.d.ts +3 -1
- package/dist/providers/jobs/kubernetes-client.d.ts +2 -1
- package/dist/providers/jobs/kubernetes-client.js +3 -0
- package/dist/providers/jobs/kubernetes-isolation.d.ts +7 -0
- package/dist/providers/jobs/kubernetes-isolation.js +8 -3
- package/dist/providers/jobs/kubernetes-preflight.d.ts +8 -0
- package/dist/providers/jobs/kubernetes-preflight.js +7 -0
- package/dist/providers/jobs/kubernetes-quota.d.ts +31 -0
- package/dist/providers/jobs/kubernetes-quota.js +110 -0
- package/dist/providers/jobs/kubernetes.d.ts +9 -0
- package/dist/providers/jobs/kubernetes.js +50 -0
- package/dist/providers/jobs/types.d.ts +7 -0
- package/dist/providers/review-host/ci.d.ts +7 -0
- package/dist/providers/review-host/ci.js +20 -0
- package/dist/providers/review-host/github-events.d.ts +7 -0
- package/dist/providers/review-host/github-events.js +31 -7
- package/dist/providers/review-host/github.d.ts +19 -2
- package/dist/providers/review-host/github.js +145 -2
- package/dist/providers/review-host/index.d.ts +8 -2
- package/dist/providers/review-host/index.js +14 -3
- package/dist/providers/review-host/local.d.ts +72 -0
- package/dist/providers/review-host/local.js +434 -0
- package/dist/providers/review-host/types.d.ts +65 -3
- package/dist/providers/review-host/types.js +5 -3
- package/dist/providers/vcs/git.d.ts +10 -20
- package/dist/providers/vcs/git.js +71 -127
- package/dist/providers/vcs/github-remote.d.ts +67 -0
- package/dist/providers/vcs/github-remote.js +201 -0
- package/dist/providers/vcs/github.d.ts +81 -0
- package/dist/providers/vcs/github.js +203 -6
- package/dist/providers/vcs/index.d.ts +16 -1
- package/dist/providers/vcs/index.js +43 -15
- package/dist/providers/vcs/local-remote.d.ts +44 -0
- package/dist/providers/vcs/local-remote.js +108 -0
- package/dist/providers/vcs/remote.d.ts +58 -0
- package/dist/providers/vcs/remote.js +1 -0
- package/dist/providers/vcs/routing.d.ts +33 -0
- package/dist/providers/vcs/routing.js +52 -0
- package/dist/providers/vcs/types.d.ts +12 -1
- package/dist/quickstart/coding-db.d.ts +11 -0
- package/dist/quickstart/coding-db.js +75 -0
- package/dist/quickstart/coding-doctor.d.ts +13 -0
- package/dist/quickstart/coding-doctor.js +88 -0
- package/dist/quickstart/coding-images.d.ts +15 -0
- package/dist/quickstart/coding-images.js +79 -0
- package/dist/quickstart/coding-seed.d.ts +63 -0
- package/dist/quickstart/coding-seed.js +85 -0
- package/dist/quickstart/coding.d.ts +66 -0
- package/dist/quickstart/coding.js +430 -0
- package/dist/quickstart/images.d.ts +19 -0
- package/dist/quickstart/images.js +63 -0
- package/dist/quickstart/index.js +61 -12
- package/dist/quickstart/starter-services.d.ts +45 -0
- package/dist/quickstart/starter-services.js +186 -0
- package/dist/quickstart-images.json +1 -0
- package/dist/serve.js +14 -1
- package/dist/viewer/api-schema.d.ts +176 -0
- package/dist/viewer/api-schema.js +27 -2
- package/dist/viewer/graph.d.ts +7 -2
- package/dist/viewer/graph.js +25 -11
- package/dist/viewer/http.d.ts +7 -1
- package/dist/viewer/http.js +8 -3
- package/dist/viewer/infra.d.ts +2 -0
- package/dist/viewer/infra.js +23 -0
- package/dist/viewer/run-detail.d.ts +2 -1
- package/dist/viewer/run-detail.js +2 -2
- package/docs/agent-recipes.md +123 -2
- package/docs/code-review-agents.md +293 -38
- package/docs/coding-agent-setup.md +141 -2
- package/docs/coding-packages.md +21 -0
- package/docs/coding-services.md +3 -0
- package/docs/coding-worker-isolation.md +56 -20
- package/docs/getting-started-gke.md +24 -0
- package/docs/getting-started.md +82 -8
- package/docs/jira-agents.md +26 -5
- package/docs/security-deployment.md +9 -0
- package/docs/viewer-api.md +21 -2
- package/help/admin-viewer.md +11 -1
- package/help/agent-recipes.md +25 -1
- package/help/builder-agent.md +4 -0
- package/help/code-review-agents.md +82 -2
- package/help/coding-packages.md +12 -1
- package/help/coding-services.md +4 -0
- package/help/cost-attribution.md +3 -1
- package/help/creating-agents.md +3 -0
- package/help/deploy-gke.md +2 -1
- package/help/errors/coding-provider-not-configured.md +60 -0
- package/help/errors/coding-turn-limit.md +25 -0
- package/help/errors/continuation-closed.md +75 -0
- package/help/errors/local-branch-conflict.md +37 -0
- package/help/errors/local-path-invalid.md +30 -0
- package/help/errors/local-ref-invalid.md +36 -0
- package/help/errors/local-ref-not-found.md +41 -0
- package/help/errors/local-repo-not-allowed.md +46 -0
- package/help/errors/local-repo-not-found.md +39 -0
- package/help/errors/model-unavailable.md +7 -6
- package/help/errors/vcs-github-not-configured.md +34 -0
- package/help/getting-started.md +4 -0
- package/help/github.md +22 -5
- package/help/jira.md +9 -2
- package/help/local-repositories.md +182 -0
- package/help/related-pull-requests.md +89 -0
- package/help/review-fix-rounds.md +69 -0
- package/help/troubleshooting/budgets.md +11 -0
- package/help/troubleshooting/coding-workers.md +22 -0
- package/help/troubleshooting/repository-access.md +5 -0
- package/package.json +3 -2
- package/prisma/migrations/20261004100000_delegations_and_coding_turns/migration.sql +12 -0
- package/prisma/migrations/20261005000000_review_fix_rounds/migration.sql +5 -0
- package/prisma/migrations/20261006000000_parallel_delegations/migration.sql +6 -0
- package/prisma/migrations/20261007000000_ci_rereview/migration.sql +4 -0
- package/prisma/migrations/20261008000000_review_after_ci/migration.sql +33 -0
- package/prisma/migrations/20261009000001_coding_run_local_branch/migration.sql +7 -0
- package/prisma/migrations/20261009000002_local_review/migration.sql +50 -0
- package/prisma/schema.prisma +98 -1
|
@@ -32,6 +32,8 @@ export interface PullRequestInput {
|
|
|
32
32
|
/** Deduplicated by the caller isn't required — pullRequestBody dedupes itself (ecosystem+name+version / +reason). */
|
|
33
33
|
packages?: readonly PackageReport[];
|
|
34
34
|
packageRefusals?: readonly PackageRefusal[];
|
|
35
|
+
/** Lock files the run changed (repository paths); with refusals, the PR body warns they may be incomplete. */
|
|
36
|
+
changedLockfiles?: readonly string[];
|
|
35
37
|
/**
|
|
36
38
|
* A continuation re-finds the PR its root run opened, and a person may
|
|
37
39
|
* have marked that PR ready for review since. The push has already updated
|
|
@@ -53,6 +55,11 @@ export interface PullRequestInput {
|
|
|
53
55
|
url?: string;
|
|
54
56
|
trackerName?: string;
|
|
55
57
|
};
|
|
58
|
+
/**
|
|
59
|
+
* The request's other pull requests (control-plane rows only), rendered as
|
|
60
|
+
* the marked "Related pull requests" section. Rendering failures omit it.
|
|
61
|
+
*/
|
|
62
|
+
related?: RelatedPullRequestsInput;
|
|
56
63
|
}
|
|
57
64
|
export interface PullRequestResult {
|
|
58
65
|
number: number;
|
|
@@ -90,6 +97,17 @@ export interface RepositoryFileInput {
|
|
|
90
97
|
export interface GitHubRepositoryAccess {
|
|
91
98
|
withRepositoryToken<T>(repository: string, action: (token: string) => Promise<T>): Promise<T>;
|
|
92
99
|
createOrFindDraftPullRequest(input: PullRequestInput): Promise<PullRequestResult>;
|
|
100
|
+
/**
|
|
101
|
+
* The root run's marked pull request if it is still open (draft or ready),
|
|
102
|
+
* else null (merged, closed, or gone). Optional: without it, continuations
|
|
103
|
+
* are not checked.
|
|
104
|
+
*/
|
|
105
|
+
findOpenPullRequest?(input: {
|
|
106
|
+
runId: string;
|
|
107
|
+
repository: string;
|
|
108
|
+
baseRef: string;
|
|
109
|
+
headRef: string;
|
|
110
|
+
}): Promise<PullRequestResult | null>;
|
|
93
111
|
/** Creates the status comment if none exists yet for this run, else updates it. */
|
|
94
112
|
upsertContinuationStatusComment(input: ContinuationStatusCommentInput): Promise<void>;
|
|
95
113
|
/** Updates the status comment if one already exists for this run; a no-op otherwise (never creates). */
|
|
@@ -115,10 +133,61 @@ export declare function isSafeGitHubInstallationToken(value: unknown): value is
|
|
|
115
133
|
*/
|
|
116
134
|
export declare const PR_ISSUE_KEY: RegExp;
|
|
117
135
|
export declare function pullRequestTitle(input: PullRequestInput): string;
|
|
136
|
+
export declare const RELATED_SECTION_START = "<!-- wardby-related:start -->";
|
|
137
|
+
export declare const RELATED_SECTION_END = "<!-- wardby-related:end -->";
|
|
138
|
+
/** At most this many pull requests are listed; the rest are counted. */
|
|
139
|
+
export declare const MAX_RELATED_PULL_REQUESTS = 20;
|
|
140
|
+
/** GitHub rejects a pull request body longer than this. */
|
|
141
|
+
export declare const MAX_PULL_REQUEST_BODY_CHARS = 65536;
|
|
142
|
+
export type RelatedPullRequestState = "open" | "draft" | "merged" | "closed";
|
|
143
|
+
/** One pull request of the request's set, from stored control-plane rows only. */
|
|
144
|
+
export interface RelatedPullRequestEntry {
|
|
145
|
+
repository: string;
|
|
146
|
+
/** Absent only for `self` while the pull request is being created. */
|
|
147
|
+
number?: number;
|
|
148
|
+
/** Live state; absent when unknown (at creation). */
|
|
149
|
+
state?: RelatedPullRequestState;
|
|
150
|
+
/** The pull request whose description this section is in. */
|
|
151
|
+
self?: boolean;
|
|
152
|
+
}
|
|
153
|
+
export interface RelatedPullRequestsInput {
|
|
154
|
+
/** In suggested merge order (the order the request's coding runs were dispatched). */
|
|
155
|
+
entries: readonly RelatedPullRequestEntry[];
|
|
156
|
+
issue?: {
|
|
157
|
+
key: string;
|
|
158
|
+
url?: string;
|
|
159
|
+
trackerName?: string;
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The "Related pull requests" block, markers included, built only from
|
|
164
|
+
* stored rows (never agent text). Open (and draft, and not-yet-known) pull
|
|
165
|
+
* requests form the numbered suggested merge order; merged and closed ones
|
|
166
|
+
* follow as context. Undefined when no other valid pull request is left after
|
|
167
|
+
* validation: a set of one is not a set.
|
|
168
|
+
*/
|
|
169
|
+
export declare function renderRelatedSection(input: RelatedPullRequestsInput): string | undefined;
|
|
170
|
+
/**
|
|
171
|
+
* The body with `block` in place of the existing marked section, or inserted
|
|
172
|
+
* after the run marker (and the issue line) when there is none. Null — do not
|
|
173
|
+
* write — when the body has no leading run marker, its section markers are
|
|
174
|
+
* malformed (one missing, reversed, or repeated), or `block` isn't a marked block.
|
|
175
|
+
*/
|
|
176
|
+
export declare function upsertRelatedSection(body: string, block: string): string | null;
|
|
118
177
|
/** At most this many packages, and separately this many refusals, are listed
|
|
119
178
|
* in a PR body; the rest are counted in a "…and N more" line pointing at
|
|
120
179
|
* get_run, which returns the full list. */
|
|
121
180
|
export declare const MAX_PR_PACKAGE_LINES = 100;
|
|
181
|
+
export declare function isLockfilePath(path: string): boolean;
|
|
182
|
+
/**
|
|
183
|
+
* A visible warning when the registry refused packages: the run's own
|
|
184
|
+
* installs were then incomplete, so failed checks in Tests may be the
|
|
185
|
+
* sandbox, not the change, and a changed lock file may not install in CI.
|
|
186
|
+
* Built only from registry records and the run's own diff, never from the
|
|
187
|
+
* agent's summary, so it cannot be softened or left out by the model.
|
|
188
|
+
*/
|
|
189
|
+
/** Rendered verbatim in refusalWarning's first line, and used to locate it in tests. */
|
|
190
|
+
export declare const DEPENDENCY_INSTALL_INCOMPLETE = "Dependency install incomplete.";
|
|
122
191
|
/** The hidden run marker stays first and unconditional: createOrFindDraftPullRequest's idempotent lookup depends on it. */
|
|
123
192
|
export declare function pullRequestBody(input: PullRequestInput): string;
|
|
124
193
|
export declare class GitHubAppClient implements GitHubRepositoryAccess {
|
|
@@ -169,6 +238,18 @@ export declare class GitHubAppClient implements GitHubRepositoryAccess {
|
|
|
169
238
|
createContinuationCheckRun(input: ContinuationCheckRunInput): Promise<void>;
|
|
170
239
|
completeContinuationCheckRun(input: ContinuationCheckRunCompleteInput): Promise<void>;
|
|
171
240
|
createOrFindDraftPullRequest(input: PullRequestInput): Promise<PullRequestResult>;
|
|
241
|
+
/**
|
|
242
|
+
* The root run's marked pull request if GitHub's `state=open` list still
|
|
243
|
+
* contains it (draft or ready for review), else null -- merged, closed, or
|
|
244
|
+
* gone. Used by git.ts's revision-in-place continuation to refuse a push to
|
|
245
|
+
* a pull request that is no longer open.
|
|
246
|
+
*/
|
|
247
|
+
findOpenPullRequest(input: {
|
|
248
|
+
runId: string;
|
|
249
|
+
repository: string;
|
|
250
|
+
baseRef: string;
|
|
251
|
+
headRef: string;
|
|
252
|
+
}): Promise<PullRequestResult | null>;
|
|
172
253
|
private appJwt;
|
|
173
254
|
private mintRepositoryToken;
|
|
174
255
|
/**
|
|
@@ -49,14 +49,22 @@ export function pullRequestTitle(input) {
|
|
|
49
49
|
const key = validIssueKey(input);
|
|
50
50
|
return key ? `[${key}] ${base}` : base;
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
if (
|
|
52
|
+
/** Validated issue parts; the tracker prefix is "" or "<Name> ". */
|
|
53
|
+
function issueParts(issue) {
|
|
54
|
+
const key = issue?.key;
|
|
55
|
+
if (typeof key !== "string" || !PR_ISSUE_KEY.test(key))
|
|
56
56
|
return undefined;
|
|
57
|
-
const
|
|
57
|
+
const url = issue?.url;
|
|
58
|
+
const safeUrl = typeof url === "string" && url.startsWith("https://") && !/[\s<>()]/.test(url) ? url : undefined;
|
|
59
|
+
const name = issue?.trackerName;
|
|
58
60
|
const tracker = typeof name === "string" && /^[A-Za-z][A-Za-z0-9 .-]{0,39}$/.test(name) ? `${name.trim()} ` : "";
|
|
59
|
-
return
|
|
61
|
+
return { key, ...(safeUrl ? { url: safeUrl } : {}), tracker };
|
|
62
|
+
}
|
|
63
|
+
function issueLine(input) {
|
|
64
|
+
const parts = issueParts(input.issue);
|
|
65
|
+
if (!parts?.url)
|
|
66
|
+
return undefined;
|
|
67
|
+
return `Resolves ${parts.tracker}issue [${parts.key}](${parts.url})`;
|
|
60
68
|
}
|
|
61
69
|
/**
|
|
62
70
|
* Package names/versions/reasons come from registry metadata, not from
|
|
@@ -69,6 +77,105 @@ function issueLine(input) {
|
|
|
69
77
|
function isSafeMarkdownFragment(value) {
|
|
70
78
|
return !value.includes("`") && !value.includes("\n") && !value.includes("\r");
|
|
71
79
|
}
|
|
80
|
+
export const RELATED_SECTION_START = "<!-- wardby-related:start -->";
|
|
81
|
+
export const RELATED_SECTION_END = "<!-- wardby-related:end -->";
|
|
82
|
+
/** At most this many pull requests are listed; the rest are counted. */
|
|
83
|
+
export const MAX_RELATED_PULL_REQUESTS = 20;
|
|
84
|
+
/** GitHub rejects a pull request body longer than this. */
|
|
85
|
+
export const MAX_PULL_REQUEST_BODY_CHARS = 65_536;
|
|
86
|
+
const RELATED_STATES = new Set(["open", "draft", "merged", "closed"]);
|
|
87
|
+
/** The repository normalized, or undefined when it isn't a plain owner/name safe inside a link. */
|
|
88
|
+
function safeRepository(value) {
|
|
89
|
+
try {
|
|
90
|
+
const repository = normalizeGitHubRepository(value);
|
|
91
|
+
return isSafeMarkdownFragment(repository) && !/[[\]()<>\s]/.test(repository) ? repository : undefined;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** One list line; `prefix` is "1." … for the merge order, "-" for the context list. */
|
|
98
|
+
function relatedLine(entry, prefix) {
|
|
99
|
+
const repository = safeRepository(entry.repository);
|
|
100
|
+
if (!repository)
|
|
101
|
+
return undefined;
|
|
102
|
+
const state = entry.state && RELATED_STATES.has(entry.state) ? ` — ${entry.state}` : "";
|
|
103
|
+
if (entry.self)
|
|
104
|
+
return `${prefix} **This pull request**${state}`;
|
|
105
|
+
if (!Number.isSafeInteger(entry.number) || (entry.number ?? 0) <= 0)
|
|
106
|
+
return undefined;
|
|
107
|
+
return `${prefix} [${repository}#${entry.number}](https://github.com/${repository}/pull/${entry.number})${state}`;
|
|
108
|
+
}
|
|
109
|
+
const isDone = (entry) => entry.state === "merged" || entry.state === "closed";
|
|
110
|
+
/**
|
|
111
|
+
* The "Related pull requests" block, markers included, built only from
|
|
112
|
+
* stored rows (never agent text). Open (and draft, and not-yet-known) pull
|
|
113
|
+
* requests form the numbered suggested merge order; merged and closed ones
|
|
114
|
+
* follow as context. Undefined when no other valid pull request is left after
|
|
115
|
+
* validation: a set of one is not a set.
|
|
116
|
+
*/
|
|
117
|
+
export function renderRelatedSection(input) {
|
|
118
|
+
const valid = input.entries.filter((entry) => relatedLine(entry, "-") !== undefined);
|
|
119
|
+
if (!valid.some((entry) => !entry.self))
|
|
120
|
+
return undefined;
|
|
121
|
+
const issue = issueParts(input.issue);
|
|
122
|
+
const issueText = issue ? ` for ${issue.tracker}issue ${issue.url ? `[${issue.key}](${issue.url})` : issue.key}` : "";
|
|
123
|
+
const open = valid.filter((entry) => !isDone(entry)).slice(0, MAX_RELATED_PULL_REQUESTS);
|
|
124
|
+
const done = valid.filter(isDone).slice(0, MAX_RELATED_PULL_REQUESTS - open.length);
|
|
125
|
+
const more = valid.length - open.length - done.length;
|
|
126
|
+
return [
|
|
127
|
+
RELATED_SECTION_START,
|
|
128
|
+
"**Related pull requests**",
|
|
129
|
+
"",
|
|
130
|
+
`Wardby opened these pull requests for the same request${issueText}.`,
|
|
131
|
+
...(open.length > 0
|
|
132
|
+
? [
|
|
133
|
+
"",
|
|
134
|
+
"Suggested merge order (the order Wardby's agent opened them in; not a guarantee, so check dependencies before merging):",
|
|
135
|
+
"",
|
|
136
|
+
...open.map((entry, index) => relatedLine(entry, `${index + 1}.`)),
|
|
137
|
+
]
|
|
138
|
+
: []),
|
|
139
|
+
...(done.length > 0
|
|
140
|
+
? ["", "Already merged or closed (context only):", "", ...done.map((entry) => relatedLine(entry, "-"))]
|
|
141
|
+
: []),
|
|
142
|
+
...(more > 0 ? ["", `…and ${more} more`] : []),
|
|
143
|
+
"",
|
|
144
|
+
"<sub>Written by Wardby from its run records; this section is replaced when the request's runs finish.</sub>",
|
|
145
|
+
RELATED_SECTION_END,
|
|
146
|
+
].join("\n");
|
|
147
|
+
}
|
|
148
|
+
/** The run marker, then optionally blank lines and the "Resolves …" line: the section goes right after. */
|
|
149
|
+
const BODY_HEAD = /^[^\S\n]*<!-- (?:wardby|reevo-run):\S+ -->[^\S\n]*(?:\n|$)(?:[^\S\n]*\n)*(?:Resolves [^\n]*(?:\n|$))?/;
|
|
150
|
+
/**
|
|
151
|
+
* The body with `block` in place of the existing marked section, or inserted
|
|
152
|
+
* after the run marker (and the issue line) when there is none. Null — do not
|
|
153
|
+
* write — when the body has no leading run marker, its section markers are
|
|
154
|
+
* malformed (one missing, reversed, or repeated), or `block` isn't a marked block.
|
|
155
|
+
*/
|
|
156
|
+
export function upsertRelatedSection(body, block) {
|
|
157
|
+
if (!block.startsWith(RELATED_SECTION_START) || !block.endsWith(RELATED_SECTION_END))
|
|
158
|
+
return null;
|
|
159
|
+
const text = body.replace(/\r\n/g, "\n");
|
|
160
|
+
const start = text.indexOf(RELATED_SECTION_START);
|
|
161
|
+
const end = text.indexOf(RELATED_SECTION_END);
|
|
162
|
+
if (start !== -1 || end !== -1) {
|
|
163
|
+
if (start === -1 ||
|
|
164
|
+
end === -1 ||
|
|
165
|
+
end < start ||
|
|
166
|
+
text.indexOf(RELATED_SECTION_START, start + 1) !== -1 ||
|
|
167
|
+
text.indexOf(RELATED_SECTION_END, end + 1) !== -1) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
return text.slice(0, start) + block + text.slice(end + RELATED_SECTION_END.length);
|
|
171
|
+
}
|
|
172
|
+
const head = BODY_HEAD.exec(text);
|
|
173
|
+
if (!head)
|
|
174
|
+
return null;
|
|
175
|
+
const before = text.slice(0, head[0].length).trimEnd();
|
|
176
|
+
const rest = text.slice(head[0].length).trimStart();
|
|
177
|
+
return rest ? `${before}\n\n${block}\n\n${rest}` : `${before}\n\n${block}`;
|
|
178
|
+
}
|
|
72
179
|
/** At most this many packages, and separately this many refusals, are listed
|
|
73
180
|
* in a PR body; the rest are counted in a "…and N more" line pointing at
|
|
74
181
|
* get_run, which returns the full list. */
|
|
@@ -122,6 +229,66 @@ function packagesSection(input) {
|
|
|
122
229
|
"</details>",
|
|
123
230
|
].join("\n");
|
|
124
231
|
}
|
|
232
|
+
/** Lock file names whose change, alongside a refusal, means CI's clean install may fail. */
|
|
233
|
+
const LOCKFILE_NAMES = new Set([
|
|
234
|
+
"package-lock.json",
|
|
235
|
+
"npm-shrinkwrap.json",
|
|
236
|
+
"yarn.lock",
|
|
237
|
+
"pnpm-lock.yaml",
|
|
238
|
+
"poetry.lock",
|
|
239
|
+
"uv.lock",
|
|
240
|
+
"Pipfile.lock",
|
|
241
|
+
]);
|
|
242
|
+
export function isLockfilePath(path) {
|
|
243
|
+
return LOCKFILE_NAMES.has(path.slice(path.lastIndexOf("/") + 1));
|
|
244
|
+
}
|
|
245
|
+
/** What each refusal code means to someone reading the PR, without the registry's internals. */
|
|
246
|
+
const REFUSAL_MEANING = {
|
|
247
|
+
wardby_package_not_allowed: "not on the allowlist, or only reachable through a refused package",
|
|
248
|
+
wardby_version_filtered: "withheld: newer than the release-age limit, or a high-severity advisory",
|
|
249
|
+
wardby_graph_incomplete: "its dependencies could not be confirmed (usually transient)",
|
|
250
|
+
wardby_lockfile_integrity_mismatch: "the lock file's integrity does not match the registry's",
|
|
251
|
+
wardby_upstream_error: "the upstream registry failed (transient)",
|
|
252
|
+
wardby_audit_unavailable: "the advisory check was unavailable (transient)",
|
|
253
|
+
};
|
|
254
|
+
/** At most this many refused packages are named in the warning; the full list stays in the details. */
|
|
255
|
+
const MAX_WARNING_NAMES = 8;
|
|
256
|
+
/**
|
|
257
|
+
* A visible warning when the registry refused packages: the run's own
|
|
258
|
+
* installs were then incomplete, so failed checks in Tests may be the
|
|
259
|
+
* sandbox, not the change, and a changed lock file may not install in CI.
|
|
260
|
+
* Built only from registry records and the run's own diff, never from the
|
|
261
|
+
* agent's summary, so it cannot be softened or left out by the model.
|
|
262
|
+
*/
|
|
263
|
+
/** Rendered verbatim in refusalWarning's first line, and used to locate it in tests. */
|
|
264
|
+
export const DEPENDENCY_INSTALL_INCOMPLETE = "Dependency install incomplete.";
|
|
265
|
+
function refusalWarning(input) {
|
|
266
|
+
const refusals = (input.packageRefusals ?? []).filter((r) => isSafeMarkdownFragment(r.ecosystem) && isSafeMarkdownFragment(r.name) && isSafeMarkdownFragment(r.reason));
|
|
267
|
+
if (refusals.length === 0)
|
|
268
|
+
return undefined;
|
|
269
|
+
const byReason = new Map();
|
|
270
|
+
for (const r of refusals) {
|
|
271
|
+
const names = byReason.get(r.reason) ?? new Set();
|
|
272
|
+
names.add(`${r.ecosystem} \`${r.name}\``);
|
|
273
|
+
byReason.set(r.reason, names);
|
|
274
|
+
}
|
|
275
|
+
const lines = [
|
|
276
|
+
"> [!WARNING]",
|
|
277
|
+
`> **${DEPENDENCY_INSTALL_INCOMPLETE}** Wardby's package registry refused packages during this run, so its installs did not finish and failed checks under **Tests** may come from the sandbox rather than this change.`,
|
|
278
|
+
];
|
|
279
|
+
for (const [reason, names] of byReason) {
|
|
280
|
+
const listed = [...names];
|
|
281
|
+
const shown = listed.slice(0, MAX_WARNING_NAMES).join(", ");
|
|
282
|
+
const more = listed.length > MAX_WARNING_NAMES ? ` and ${listed.length - MAX_WARNING_NAMES} more` : "";
|
|
283
|
+
const meaning = REFUSAL_MEANING[reason];
|
|
284
|
+
lines.push(`> - ${shown}${more}: \`${reason}\`${meaning ? ` (${meaning})` : ""}`);
|
|
285
|
+
}
|
|
286
|
+
const lockfiles = (input.changedLockfiles ?? []).filter(isSafeMarkdownFragment);
|
|
287
|
+
if (lockfiles.length > 0) {
|
|
288
|
+
lines.push(`> ${lockfiles.map((f) => `\`${f}\``).join(", ")} changed in this run and may be incomplete: a clean install (\`npm ci\` or similar) in CI may fail until it is regenerated.`);
|
|
289
|
+
}
|
|
290
|
+
return lines.join("\n");
|
|
291
|
+
}
|
|
125
292
|
/** The hidden run marker stays first and unconditional: createOrFindDraftPullRequest's idempotent lookup depends on it. */
|
|
126
293
|
export function pullRequestBody(input) {
|
|
127
294
|
const sections = [`${RUN_MARKER_PREFIX}${input.runId} -->`];
|
|
@@ -129,6 +296,22 @@ export function pullRequestBody(input) {
|
|
|
129
296
|
const issue = issueLine(input);
|
|
130
297
|
if (issue)
|
|
131
298
|
sections.push(issue);
|
|
299
|
+
try {
|
|
300
|
+
const related = input.related ? renderRelatedSection(input.related) : undefined;
|
|
301
|
+
if (related)
|
|
302
|
+
sections.push(related);
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
// omitted, like the packages section
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
const warning = refusalWarning(input);
|
|
309
|
+
if (warning)
|
|
310
|
+
sections.push(warning);
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
// omitted, like the packages section
|
|
314
|
+
}
|
|
132
315
|
if (input.summary)
|
|
133
316
|
sections.push(input.summary);
|
|
134
317
|
if (input.tests?.length) {
|
|
@@ -420,6 +603,20 @@ export class GitHubAppClient {
|
|
|
420
603
|
throw safeApiError(response);
|
|
421
604
|
});
|
|
422
605
|
}
|
|
606
|
+
/**
|
|
607
|
+
* The root run's marked pull request if GitHub's `state=open` list still
|
|
608
|
+
* contains it (draft or ready for review), else null -- merged, closed, or
|
|
609
|
+
* gone. Used by git.ts's revision-in-place continuation to refuse a push to
|
|
610
|
+
* a pull request that is no longer open.
|
|
611
|
+
*/
|
|
612
|
+
async findOpenPullRequest(input) {
|
|
613
|
+
const repository = normalizeGitHubRepository(input.repository);
|
|
614
|
+
const baseRef = normalizeGitRef(input.baseRef);
|
|
615
|
+
const headRef = normalizeGitRef(input.headRef);
|
|
616
|
+
if (!SAFE_RUN_ID.test(input.runId))
|
|
617
|
+
throw new Error("github_pull_request_input_invalid");
|
|
618
|
+
return this.withRepositoryToken(repository, (token) => this.findPullRequest(token, { runId: input.runId, repository, baseRef, headRef, acceptReadyForReview: true }));
|
|
619
|
+
}
|
|
423
620
|
async appJwt() {
|
|
424
621
|
let keyObject;
|
|
425
622
|
try {
|
|
@@ -3,4 +3,19 @@ import type { VcsProvider } from "./types.js";
|
|
|
3
3
|
export * from "./types.js";
|
|
4
4
|
export { GitHubAppClient } from "./github.js";
|
|
5
5
|
export { GitVcsProvider, NodeGitCommandRunner } from "./git.js";
|
|
6
|
-
export
|
|
6
|
+
export { GitHubRemote } from "./github-remote.js";
|
|
7
|
+
export { LocalRemote } from "./local-remote.js";
|
|
8
|
+
export { RoutingVcsProvider, VCS_GITHUB_NOT_CONFIGURED_ERROR } from "./routing.js";
|
|
9
|
+
export type { GitRemote, PublishInput, PublishResult } from "./remote.js";
|
|
10
|
+
export interface BuildVcsProviderOptions {
|
|
11
|
+
/** Startup warnings; the module logger by default. */
|
|
12
|
+
warn?: (message: string) => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* GitHub is built only with App credentials, local repositories only with
|
|
16
|
+
* LOCAL_REPO_ROOTS; neither being set is allowed (the server still starts) and
|
|
17
|
+
* surfaces when a repository of the missing kind is used (see
|
|
18
|
+
* RoutingVcsProvider). With neither configured no coding run can succeed, so a
|
|
19
|
+
* startup warning names both options. Only container launchers build this.
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildVcsProvider(providerConfig: Pick<ProviderConfig, "vcs">, config: GitHubVcsConfig, env?: NodeJS.ProcessEnv, options?: BuildVcsProviderOptions): VcsProvider;
|
|
@@ -2,25 +2,53 @@ import { tmpdir } from "node:os";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { GitHubAppClient } from "./github.js";
|
|
4
4
|
import { GitVcsProvider } from "./git.js";
|
|
5
|
+
import { loadLocalRepoRoots } from "../../coding/local-repo.js";
|
|
6
|
+
import { GitHubRemote } from "./github-remote.js";
|
|
7
|
+
import { LocalRemote } from "./local-remote.js";
|
|
8
|
+
import { RoutingVcsProvider } from "./routing.js";
|
|
9
|
+
import { logger } from "../../core/logger.js";
|
|
10
|
+
const vcsLog = logger.child({ module: "vcs" });
|
|
5
11
|
export * from "./types.js";
|
|
6
12
|
export { GitHubAppClient } from "./github.js";
|
|
7
13
|
export { GitVcsProvider, NodeGitCommandRunner } from "./git.js";
|
|
8
|
-
export
|
|
14
|
+
export { GitHubRemote } from "./github-remote.js";
|
|
15
|
+
export { LocalRemote } from "./local-remote.js";
|
|
16
|
+
export { RoutingVcsProvider, VCS_GITHUB_NOT_CONFIGURED_ERROR } from "./routing.js";
|
|
17
|
+
/**
|
|
18
|
+
* GitHub is built only with App credentials, local repositories only with
|
|
19
|
+
* LOCAL_REPO_ROOTS; neither being set is allowed (the server still starts) and
|
|
20
|
+
* surfaces when a repository of the missing kind is used (see
|
|
21
|
+
* RoutingVcsProvider). With neither configured no coding run can succeed, so a
|
|
22
|
+
* startup warning names both options. Only container launchers build this.
|
|
23
|
+
*/
|
|
24
|
+
export function buildVcsProvider(providerConfig, config, env = process.env, options = {}) {
|
|
9
25
|
if (providerConfig.vcs !== "github")
|
|
10
26
|
throw new Error(`VCS_PROVIDER=${String(providerConfig.vcs)} is not supported.`);
|
|
11
|
-
if (!config.appId || !config.privateKey) {
|
|
12
|
-
throw new Error("GITHUB_APP_ID and GITHUB_APP_PRIVATE_KEY are required by the GitHub VCS adapter.");
|
|
13
|
-
}
|
|
14
27
|
const rootDir = resolve(config.workRoot ?? resolve(tmpdir(), "wardby-vcs"));
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
const limits = { maxChangedFiles: config.maxChangedFiles, maxDiffBytes: config.maxDiffBytes };
|
|
29
|
+
let github = null;
|
|
30
|
+
if (config.appId && config.privateKey) {
|
|
31
|
+
const client = new GitHubAppClient({
|
|
32
|
+
appId: config.appId,
|
|
33
|
+
privateKey: config.privateKey,
|
|
34
|
+
apiVersion: config.apiVersion,
|
|
35
|
+
});
|
|
36
|
+
github = new GitVcsProvider({ rootDir, remote: new GitHubRemote({ github: client }), ...limits });
|
|
37
|
+
}
|
|
38
|
+
let local = null;
|
|
39
|
+
if (loadLocalRepoRoots(env).roots.length > 0) {
|
|
40
|
+
// Re-read per call so a root that disappears (or appears) takes effect without a restart.
|
|
41
|
+
local = new GitVcsProvider({
|
|
42
|
+
rootDir,
|
|
43
|
+
remote: new LocalRemote({ roots: () => loadLocalRepoRoots(env).roots }),
|
|
44
|
+
...limits,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (!github && !local) {
|
|
48
|
+
const warn = options.warn ?? ((message) => vcsLog.warn(message));
|
|
49
|
+
warn("No repositories are configured for coding agents: set GITHUB_APP_ID and GITHUB_APP_PRIVATE_KEY " +
|
|
50
|
+
"for GitHub repositories, or LOCAL_REPO_ROOTS for local git repositories on this server. " +
|
|
51
|
+
"Coding runs fail until one of them is set.");
|
|
52
|
+
}
|
|
53
|
+
return new RoutingVcsProvider({ github, local });
|
|
26
54
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { RepositoryFileInput } from "./github.js";
|
|
2
|
+
import type { GitRemote, PublishInput, PublishResult } from "./remote.js";
|
|
3
|
+
export interface LocalRemoteOptions {
|
|
4
|
+
/** Trusted roots (realpaths), re-read on every call so a change takes effect without a restart. */
|
|
5
|
+
roots: () => readonly string[];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* A git repository on the control plane's own disk (`local:/abs/path`).
|
|
9
|
+
*
|
|
10
|
+
* The source repository is touched only by `ls-remote`, the clone's
|
|
11
|
+
* upload-pack, and a push to `refs/heads/wardby/run-*`: the clone goes through
|
|
12
|
+
* a `file://` URL (so `--depth 1` is honored and nothing is hard-linked), never
|
|
13
|
+
* reads the source's working tree, and every hardening check in GitVcsProvider
|
|
14
|
+
* still applies. `protocol.file.allow=always` is set only on this remote's
|
|
15
|
+
* clone/ls-remote/push invocations; HARDENED_GIT_CONFIG's `protocol.allow=never`
|
|
16
|
+
* keeps every other transport off.
|
|
17
|
+
*/
|
|
18
|
+
export declare class LocalRemote implements GitRemote {
|
|
19
|
+
private readonly options;
|
|
20
|
+
readonly gitConfig: readonly string[];
|
|
21
|
+
constructor(options: LocalRemoteOptions);
|
|
22
|
+
normalizeRepository(repository: string): string;
|
|
23
|
+
/** The repository must already be named by its realpath (withAccess verifies it against the roots). */
|
|
24
|
+
cloneUrl(repository: string): string;
|
|
25
|
+
withAccess<T>(repository: string, fn: (token: string | undefined) => Promise<T>): Promise<T>;
|
|
26
|
+
/** No pull request to check: a continuation's branch must exist, which prepare's ls-remote pre-check enforces. */
|
|
27
|
+
assertContinuationOpen(): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* One committed file at a ref, read from git objects (never the working tree).
|
|
30
|
+
* null = the file does not exist at that ref; a missing ref is
|
|
31
|
+
* local_ref_not_found; a repository outside the roots is refused.
|
|
32
|
+
*/
|
|
33
|
+
readRepositoryFile(input: RepositoryFileInput): Promise<string | null>;
|
|
34
|
+
publish({ workspace }: PublishInput): Promise<PublishResult>;
|
|
35
|
+
conflictError(): Error;
|
|
36
|
+
refNotFoundError(ref: string): Error;
|
|
37
|
+
/**
|
|
38
|
+
* receive.denyCurrentBranch=updateInstead (a user setting) would rewrite the
|
|
39
|
+
* working tree of whichever worktree has the pushed branch checked out --
|
|
40
|
+
* the main checkout or any linked worktree -- so refuse before pushing.
|
|
41
|
+
*/
|
|
42
|
+
beforePush(repository: string, headRef: string): Promise<void>;
|
|
43
|
+
assertPushRef(headRef: string): void;
|
|
44
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
import { LOCAL_REPO_PREFIX, LocalRepoError, normalizeLocalRepository, resolveLocalRepository, } from "../../coding/local-repo.js";
|
|
3
|
+
import { SHA, isSafeRefName, isSafeRepoPath, localGit, localGitBytes } from "../../coding/local-git.js";
|
|
4
|
+
/** The only refs a local remote ever writes into the user's repository. */
|
|
5
|
+
const LOCAL_PUSH_REF = /^wardby\/run-[A-Za-z0-9_-]+$/;
|
|
6
|
+
/**
|
|
7
|
+
* A git repository on the control plane's own disk (`local:/abs/path`).
|
|
8
|
+
*
|
|
9
|
+
* The source repository is touched only by `ls-remote`, the clone's
|
|
10
|
+
* upload-pack, and a push to `refs/heads/wardby/run-*`: the clone goes through
|
|
11
|
+
* a `file://` URL (so `--depth 1` is honored and nothing is hard-linked), never
|
|
12
|
+
* reads the source's working tree, and every hardening check in GitVcsProvider
|
|
13
|
+
* still applies. `protocol.file.allow=always` is set only on this remote's
|
|
14
|
+
* clone/ls-remote/push invocations; HARDENED_GIT_CONFIG's `protocol.allow=never`
|
|
15
|
+
* keeps every other transport off.
|
|
16
|
+
*/
|
|
17
|
+
export class LocalRemote {
|
|
18
|
+
options;
|
|
19
|
+
gitConfig = ["-c", "protocol.file.allow=always"];
|
|
20
|
+
constructor(options) {
|
|
21
|
+
this.options = options;
|
|
22
|
+
}
|
|
23
|
+
normalizeRepository(repository) {
|
|
24
|
+
return normalizeLocalRepository(repository);
|
|
25
|
+
}
|
|
26
|
+
/** The repository must already be named by its realpath (withAccess verifies it against the roots). */
|
|
27
|
+
cloneUrl(repository) {
|
|
28
|
+
return pathToFileURL(normalizeLocalRepository(repository).slice(LOCAL_REPO_PREFIX.length)).href;
|
|
29
|
+
}
|
|
30
|
+
async withAccess(repository, fn) {
|
|
31
|
+
const requested = normalizeLocalRepository(repository);
|
|
32
|
+
const resolved = await resolveLocalRepository(requested, this.options.roots());
|
|
33
|
+
// cloneUrl is built from the requested path, so it must be the path that
|
|
34
|
+
// was just checked: a symlinked or otherwise non-canonical name is refused.
|
|
35
|
+
if (resolved.repository !== requested) {
|
|
36
|
+
throw new LocalRepoError("local_repo_not_allowed", "repository must be named by its real path");
|
|
37
|
+
}
|
|
38
|
+
return fn(undefined);
|
|
39
|
+
}
|
|
40
|
+
/** No pull request to check: a continuation's branch must exist, which prepare's ls-remote pre-check enforces. */
|
|
41
|
+
async assertContinuationOpen() { }
|
|
42
|
+
/**
|
|
43
|
+
* One committed file at a ref, read from git objects (never the working tree).
|
|
44
|
+
* null = the file does not exist at that ref; a missing ref is
|
|
45
|
+
* local_ref_not_found; a repository outside the roots is refused.
|
|
46
|
+
*/
|
|
47
|
+
async readRepositoryFile(input) {
|
|
48
|
+
const { path: dir } = await resolveLocalRepository(input.repository, this.options.roots());
|
|
49
|
+
if (!isSafeRepoPath(input.path))
|
|
50
|
+
throw new LocalRepoError("local_path_invalid", "invalid repository file path");
|
|
51
|
+
if (!SHA.test(input.ref) && !(await isSafeRefName(dir, input.ref))) {
|
|
52
|
+
throw new LocalRepoError("local_ref_invalid", "invalid ref");
|
|
53
|
+
}
|
|
54
|
+
let sha;
|
|
55
|
+
try {
|
|
56
|
+
sha = (await localGit(dir, ["rev-parse", "--verify", "--quiet", "--end-of-options", `${input.ref}^{commit}`], 64 * 1024)).trim();
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
throw new LocalRepoError("local_ref_not_found", `ref ${input.ref} does not exist in the local repository`);
|
|
60
|
+
}
|
|
61
|
+
if (!SHA.test(sha))
|
|
62
|
+
throw new LocalRepoError("local_ref_not_found", `ref ${input.ref} does not exist`);
|
|
63
|
+
const listing = await localGit(dir, ["ls-tree", "-z", "--end-of-options", sha, "--", input.path], 64 * 1024);
|
|
64
|
+
if (!listing)
|
|
65
|
+
return null;
|
|
66
|
+
if (!/^100(?:644|755) blob /.test(listing))
|
|
67
|
+
throw new Error("local_file_not_a_file");
|
|
68
|
+
const object = `${sha}:${input.path}`;
|
|
69
|
+
const size = Number((await localGit(dir, ["cat-file", "-s", "--end-of-options", object], 64 * 1024)).trim());
|
|
70
|
+
if (!Number.isSafeInteger(size) || size > input.maxBytes)
|
|
71
|
+
throw new Error("local_file_too_large");
|
|
72
|
+
const bytes = await localGitBytes(dir, ["cat-file", "blob", "--end-of-options", object], input.maxBytes + 1024);
|
|
73
|
+
if (bytes.byteLength > input.maxBytes)
|
|
74
|
+
throw new Error("local_file_too_large");
|
|
75
|
+
try {
|
|
76
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
throw new Error("local_file_not_utf8");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async publish({ workspace }) {
|
|
83
|
+
this.assertPushRef(workspace.headRef);
|
|
84
|
+
return { kind: "branch", branch: workspace.headRef };
|
|
85
|
+
}
|
|
86
|
+
conflictError() {
|
|
87
|
+
return new LocalRepoError("local_branch_conflict", "the branch moved in the local repository since the run started");
|
|
88
|
+
}
|
|
89
|
+
refNotFoundError(ref) {
|
|
90
|
+
return new LocalRepoError("local_ref_not_found", `branch ${ref} does not exist in the local repository`);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* receive.denyCurrentBranch=updateInstead (a user setting) would rewrite the
|
|
94
|
+
* working tree of whichever worktree has the pushed branch checked out --
|
|
95
|
+
* the main checkout or any linked worktree -- so refuse before pushing.
|
|
96
|
+
*/
|
|
97
|
+
async beforePush(repository, headRef) {
|
|
98
|
+
const path = normalizeLocalRepository(repository).slice(LOCAL_REPO_PREFIX.length);
|
|
99
|
+
const listing = await localGit(path, ["worktree", "list", "--porcelain"], 1024 * 1024);
|
|
100
|
+
if (listing.split("\n").some((line) => line.trim() === `branch refs/heads/${headRef}`)) {
|
|
101
|
+
throw new LocalRepoError("local_branch_conflict", `branch ${headRef} is checked out in the local repository`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
assertPushRef(headRef) {
|
|
105
|
+
if (!LOCAL_PUSH_REF.test(headRef))
|
|
106
|
+
throw new Error("vcs_head_ref_invalid");
|
|
107
|
+
}
|
|
108
|
+
}
|