@wardby/cli 0.4.0 → 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/env.js +6 -1
- 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.d.ts +6 -0
- package/dist/quickstart/index.js +72 -40
- 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 +87 -24
- 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 +5 -3
- 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
|
@@ -6,7 +6,7 @@ import { gitExcludePathspecs, normalizeCollectExclusions } from "../../coding/co
|
|
|
6
6
|
import { WARDBY_PROTECTED_PATHS, isProtectedPathException, isWellFormedProtectedPath, protectedPathBody, protectsSomePath, } from "../../coding/protected-paths.js";
|
|
7
7
|
import { ensurePrivateDirectory } from "../../core/private-directory.js";
|
|
8
8
|
import { globRegex } from "../../core/glob.js";
|
|
9
|
-
import { MAX_REDACTED_SPAN, redactTokenShapedValues,
|
|
9
|
+
import { MAX_REDACTED_SPAN, redactTokenShapedValues, normalizeGitRef } from "../../coding/protocol.js";
|
|
10
10
|
import { isSafeGitHubInstallationToken } from "./github.js";
|
|
11
11
|
export const DEFAULT_MAX_CHANGED_FILES = 1_000;
|
|
12
12
|
export const DEFAULT_MAX_DIFF_BYTES = 1024 * 1024;
|
|
@@ -177,7 +177,7 @@ export function protectedPathMatcher(patterns) {
|
|
|
177
177
|
return protectedPatterns.some((matcher) => matcher.test(path)) && !exceptions.has(path);
|
|
178
178
|
};
|
|
179
179
|
}
|
|
180
|
-
function validateChangedPath(path) {
|
|
180
|
+
export function validateChangedPath(path) {
|
|
181
181
|
if (!path ||
|
|
182
182
|
isAbsolute(path) ||
|
|
183
183
|
path.includes("\\") ||
|
|
@@ -195,19 +195,13 @@ function nullSeparated(value) {
|
|
|
195
195
|
function directChild(root, child) {
|
|
196
196
|
return child.startsWith(`${root}${sep}`) && !child.slice(root.length + 1).includes(sep);
|
|
197
197
|
}
|
|
198
|
-
/** Human-readable label for continuation status notifications -- falls back to the opaque run id alone when no agent name is known. */
|
|
199
|
-
function runLabel(agentName, runId) {
|
|
200
|
-
return agentName ? `${agentName} (wardby run ${runId})` : `wardby run ${runId}`;
|
|
201
|
-
}
|
|
202
198
|
export class GitVcsProvider {
|
|
203
|
-
options;
|
|
204
199
|
rootDir;
|
|
205
200
|
git;
|
|
206
201
|
maxChangedFiles;
|
|
207
202
|
maxDiffBytes;
|
|
208
|
-
|
|
203
|
+
remote;
|
|
209
204
|
constructor(options) {
|
|
210
|
-
this.options = options;
|
|
211
205
|
this.rootDir = resolve(options.rootDir);
|
|
212
206
|
if (this.rootDir === resolve("/"))
|
|
213
207
|
throw new Error("vcs_root_invalid");
|
|
@@ -220,11 +214,11 @@ export class GitVcsProvider {
|
|
|
220
214
|
this.maxDiffBytes <= 0) {
|
|
221
215
|
throw new Error("vcs_limits_invalid");
|
|
222
216
|
}
|
|
223
|
-
this.
|
|
224
|
-
options.cloneUrlForRepository ?? ((repository) => `https://github.com/${repository}.git`);
|
|
217
|
+
this.remote = options.remote;
|
|
225
218
|
}
|
|
226
219
|
async prepareWorkspace(input) {
|
|
227
220
|
const normalized = this.validateInput(input);
|
|
221
|
+
await this.remote.assertContinuationOpen(normalized);
|
|
228
222
|
await ensurePrivateDirectory(this.rootDir);
|
|
229
223
|
await mkdir(resolve(this.rootDir, ".home"), { recursive: true, mode: 0o700 });
|
|
230
224
|
const runRoot = resolve(this.rootDir, normalized.runId);
|
|
@@ -240,10 +234,9 @@ export class GitVcsProvider {
|
|
|
240
234
|
}
|
|
241
235
|
const workspacePath = resolve(runRoot, "workspace");
|
|
242
236
|
const gitMetadataPath = resolve(runRoot, "git");
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
237
|
+
// The remote validates its own URL (GitHubRemote: vcs_remote_invalid
|
|
238
|
+
// unless it is exactly github.com's, outside the test-only override).
|
|
239
|
+
const cloneUrl = this.remote.cloneUrl(normalized.repository);
|
|
247
240
|
// Revision-in-place: a continuation clones the EXISTING headRef branch
|
|
248
241
|
// directly (its current tip becomes baseCommit below) instead of
|
|
249
242
|
// baseRef -- everything after this line (branch/checkout/reset, and
|
|
@@ -252,9 +245,24 @@ export class GitVcsProvider {
|
|
|
252
245
|
// round started."
|
|
253
246
|
const cloneBranch = normalized.continuation ? normalized.headRef : normalized.baseRef;
|
|
254
247
|
try {
|
|
255
|
-
await this.
|
|
248
|
+
await this.remote.withAccess(normalized.repository, async (token) => {
|
|
249
|
+
const auth = token !== undefined ? { authToken: token } : {};
|
|
250
|
+
if (this.remote.refNotFoundError) {
|
|
251
|
+
const listed = await this.git.run([
|
|
252
|
+
...HARDENED_GIT_CONFIG,
|
|
253
|
+
...this.remote.gitConfig,
|
|
254
|
+
"ls-remote",
|
|
255
|
+
"--heads",
|
|
256
|
+
cloneUrl,
|
|
257
|
+
`refs/heads/${cloneBranch}`,
|
|
258
|
+
], { cwd: runRoot, ...auth });
|
|
259
|
+
const found = listed.stdout.split("\n").some((line) => line.split("\t")[1] === `refs/heads/${cloneBranch}`);
|
|
260
|
+
if (!found)
|
|
261
|
+
throw this.remote.refNotFoundError(cloneBranch);
|
|
262
|
+
}
|
|
256
263
|
await this.git.run([
|
|
257
264
|
...HARDENED_GIT_CONFIG,
|
|
265
|
+
...this.remote.gitConfig,
|
|
258
266
|
"clone",
|
|
259
267
|
"--no-checkout",
|
|
260
268
|
"--single-branch",
|
|
@@ -271,7 +279,7 @@ export class GitVcsProvider {
|
|
|
271
279
|
gitMetadataPath,
|
|
272
280
|
cloneUrl,
|
|
273
281
|
workspacePath,
|
|
274
|
-
], { cwd: runRoot,
|
|
282
|
+
], { cwd: runRoot, ...auth });
|
|
275
283
|
});
|
|
276
284
|
await rm(resolve(workspacePath, ".git"), { force: true });
|
|
277
285
|
const baseCommit = await this.revParseRaw(gitMetadataPath, workspacePath, `refs/remotes/origin/${cloneBranch}^{commit}`);
|
|
@@ -417,28 +425,21 @@ export class GitVcsProvider {
|
|
|
417
425
|
throw new Error("vcs_finalized_workspace_changed");
|
|
418
426
|
commitSha = currentHead;
|
|
419
427
|
}
|
|
428
|
+
await this.remote.assertContinuationOpen(prepared);
|
|
420
429
|
await this.pushOnce(prepared, commitSha);
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
headRef: prepared.headRef,
|
|
435
|
-
summary: details?.summary,
|
|
436
|
-
tests: details?.tests,
|
|
437
|
-
tag: details?.tag,
|
|
438
|
-
packages: details?.packages,
|
|
439
|
-
packageRefusals: details?.packageRefusals,
|
|
440
|
-
...(details?.issue ? { issue: details.issue } : {}),
|
|
441
|
-
});
|
|
430
|
+
const published = await this.remote.publish({ workspace: prepared, details, changedPaths: changed });
|
|
431
|
+
if (published.kind === "branch") {
|
|
432
|
+
if (published.branch !== prepared.headRef)
|
|
433
|
+
throw new Error("vcs_head_ref_mismatch");
|
|
434
|
+
return {
|
|
435
|
+
outcome: "branch_pushed",
|
|
436
|
+
repository: prepared.repository,
|
|
437
|
+
baseRef: prepared.baseRef,
|
|
438
|
+
baseCommit: prepared.baseCommit,
|
|
439
|
+
headRef: prepared.headRef,
|
|
440
|
+
commitSha,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
442
443
|
return {
|
|
443
444
|
outcome: prepared.continuation ? "pull_request_updated" : "pull_request_opened",
|
|
444
445
|
repository: prepared.repository,
|
|
@@ -446,8 +447,8 @@ export class GitVcsProvider {
|
|
|
446
447
|
baseCommit: prepared.baseCommit,
|
|
447
448
|
headRef: prepared.headRef,
|
|
448
449
|
commitSha,
|
|
449
|
-
pullRequestNumber:
|
|
450
|
-
pullRequestUrl:
|
|
450
|
+
pullRequestNumber: published.number,
|
|
451
|
+
pullRequestUrl: published.url,
|
|
451
452
|
};
|
|
452
453
|
}
|
|
453
454
|
async cleanup(workspace) {
|
|
@@ -460,102 +461,36 @@ export class GitVcsProvider {
|
|
|
460
461
|
await rm(expected.runRoot, { recursive: true, force: true });
|
|
461
462
|
}
|
|
462
463
|
/**
|
|
463
|
-
* Best-effort "wardby is working on this PR" signal (see VcsProvider)
|
|
464
|
-
*
|
|
465
|
-
* real coding run
|
|
466
|
-
* ("diagnostics are optional and must never affect terminal cleanup").
|
|
467
|
-
* No-op for a fresh (non-continuation) workspace: there's no PR to
|
|
468
|
-
* attach anything to until its one commit lands.
|
|
464
|
+
* Best-effort "wardby is working on this PR" signal (see VcsProvider),
|
|
465
|
+
* delegated to the remote; a remote without one is a no-op. Wrapped so
|
|
466
|
+
* this can NEVER throw or otherwise affect the real coding run.
|
|
469
467
|
*/
|
|
470
468
|
async notifyContinuationStarted(workspace, details) {
|
|
471
|
-
if (!workspace.continuation)
|
|
472
|
-
return;
|
|
473
469
|
try {
|
|
474
|
-
|
|
475
|
-
runId: workspace.runId,
|
|
476
|
-
rootRunId: workspace.continuation.rootRunId,
|
|
477
|
-
repository: workspace.repository,
|
|
478
|
-
baseRef: workspace.baseRef,
|
|
479
|
-
headRef: workspace.headRef,
|
|
480
|
-
};
|
|
481
|
-
await Promise.allSettled([
|
|
482
|
-
this.options.github.upsertContinuationStatusComment({
|
|
483
|
-
...identity,
|
|
484
|
-
body: `🔄 ${runLabel(details?.agentName, workspace.runId)} is working on this PR...`,
|
|
485
|
-
}),
|
|
486
|
-
this.options.github.createContinuationCheckRun({
|
|
487
|
-
repository: workspace.repository,
|
|
488
|
-
headSha: workspace.baseCommit,
|
|
489
|
-
runId: workspace.runId,
|
|
490
|
-
}),
|
|
491
|
-
]);
|
|
470
|
+
await this.remote.notifyContinuationStarted?.(workspace, details);
|
|
492
471
|
}
|
|
493
472
|
catch {
|
|
494
473
|
// Best-effort observability only -- must never affect the real run.
|
|
495
474
|
}
|
|
496
475
|
}
|
|
497
|
-
/**
|
|
498
|
-
* Companion to notifyContinuationStarted -- finds and updates whatever
|
|
499
|
-
* that call created, never creates fresh state itself (see
|
|
500
|
-
* upsertContinuationStatusComment vs updateContinuationStatusComment,
|
|
501
|
-
* and createContinuationCheckRun vs completeContinuationCheckRun in
|
|
502
|
-
* github.ts). Safe to call more than once for the same run. Same
|
|
503
|
-
* never-throw contract as notifyContinuationStarted.
|
|
504
|
-
*/
|
|
476
|
+
/** Companion to notifyContinuationStarted, delegated to the remote under the same never-throw contract. */
|
|
505
477
|
async notifyContinuationFinished(workspace, outcome, details) {
|
|
506
|
-
if (!workspace.continuation)
|
|
507
|
-
return;
|
|
508
478
|
try {
|
|
509
|
-
|
|
510
|
-
runId: workspace.runId,
|
|
511
|
-
rootRunId: workspace.continuation.rootRunId,
|
|
512
|
-
repository: workspace.repository,
|
|
513
|
-
baseRef: workspace.baseRef,
|
|
514
|
-
headRef: workspace.headRef,
|
|
515
|
-
};
|
|
516
|
-
const label = runLabel(details?.agentName, workspace.runId);
|
|
517
|
-
const summarySuffix = details?.summary ? `\n\n${details.summary}` : "";
|
|
518
|
-
const budgetSuffix = details?.budgetSentence ? ` ${details.budgetSentence}` : "";
|
|
519
|
-
const body = outcome === "succeeded"
|
|
520
|
-
? `✅ ${label} finished.${summarySuffix}`
|
|
521
|
-
: outcome === "budget_exhausted"
|
|
522
|
-
? `❌ ${label} ran out of budget.${budgetSuffix}${summarySuffix}`
|
|
523
|
-
: details?.serviceSentence
|
|
524
|
-
? `❌ ${label} could not start: ${details.serviceSentence}${summarySuffix}`
|
|
525
|
-
: details?.providerSentence
|
|
526
|
-
? `❌ ${label} could not run: ${details.providerSentence}${summarySuffix}`
|
|
527
|
-
: details?.protectedPathSentence
|
|
528
|
-
? `❌ ${label} could not open its changes: ${details.protectedPathSentence}${summarySuffix}`
|
|
529
|
-
: `❌ ${label} failed.${summarySuffix}`;
|
|
530
|
-
await Promise.allSettled([
|
|
531
|
-
this.options.github.updateContinuationStatusComment({ ...identity, body }),
|
|
532
|
-
this.options.github.completeContinuationCheckRun({
|
|
533
|
-
repository: workspace.repository,
|
|
534
|
-
headSha: workspace.baseCommit,
|
|
535
|
-
runId: workspace.runId,
|
|
536
|
-
outcome: outcome === "succeeded" ? "succeeded" : "failed",
|
|
537
|
-
}),
|
|
538
|
-
]);
|
|
479
|
+
await this.remote.notifyContinuationFinished?.(workspace, outcome, details);
|
|
539
480
|
}
|
|
540
481
|
catch {
|
|
541
482
|
// Best-effort observability only -- must never affect the real run.
|
|
542
483
|
}
|
|
543
484
|
}
|
|
544
485
|
async readRepositoryFile(input) {
|
|
545
|
-
|
|
546
|
-
if (!github.readFileAtRef)
|
|
486
|
+
if (!this.remote.readRepositoryFile)
|
|
547
487
|
throw new Error("vcs_read_file_unsupported");
|
|
548
|
-
return
|
|
549
|
-
repository: normalizeGitHubRepository(input.repository),
|
|
550
|
-
ref: normalizeGitRef(input.ref),
|
|
551
|
-
path: validateChangedPath(input.path),
|
|
552
|
-
maxBytes: input.maxBytes,
|
|
553
|
-
});
|
|
488
|
+
return this.remote.readRepositoryFile(input);
|
|
554
489
|
}
|
|
555
490
|
validateInput(input) {
|
|
556
491
|
if (!SAFE_RUN_ID.test(input.runId))
|
|
557
492
|
throw new Error("vcs_run_id_invalid");
|
|
558
|
-
const repository =
|
|
493
|
+
const repository = this.remote.normalizeRepository(input.repository);
|
|
559
494
|
const baseRef = normalizeGitRef(input.baseRef);
|
|
560
495
|
const headRef = normalizeGitRef(input.headRef);
|
|
561
496
|
// Revision-in-place: a continuation's headRef legitimately belongs to a
|
|
@@ -660,7 +595,7 @@ export class GitVcsProvider {
|
|
|
660
595
|
await visit(workspacePath);
|
|
661
596
|
}
|
|
662
597
|
async assertRemote(workspace) {
|
|
663
|
-
const expectedUrl = this.
|
|
598
|
+
const expectedUrl = this.remote.cloneUrl(workspace.repository);
|
|
664
599
|
const remotes = (await this.gitFor(workspace, ["remote"])).stdout.trim().split("\n").filter(Boolean);
|
|
665
600
|
const fetchUrls = (await this.gitFor(workspace, ["remote", "get-url", "--all", "origin"])).stdout
|
|
666
601
|
.trim()
|
|
@@ -701,33 +636,42 @@ export class GitVcsProvider {
|
|
|
701
636
|
}
|
|
702
637
|
}
|
|
703
638
|
async pushOnce(workspace, commitSha) {
|
|
704
|
-
|
|
705
|
-
|
|
639
|
+
this.remote.assertPushRef?.(workspace.headRef);
|
|
640
|
+
await this.remote.withAccess(workspace.repository, async (token) => {
|
|
641
|
+
const auth = token !== undefined ? { authToken: token } : {};
|
|
642
|
+
const remote = await this.remoteHead(workspace, auth);
|
|
706
643
|
if (remote === commitSha)
|
|
707
644
|
return;
|
|
645
|
+
await this.remote.beforePush?.(workspace.repository, workspace.headRef);
|
|
708
646
|
// A continuation's remote branch legitimately already sits at
|
|
709
647
|
// baseCommit (that's the tip we cloned and committed on top of) --
|
|
710
648
|
// that's the expected fast-forward pre-push state, not a conflict.
|
|
711
649
|
// Any OTHER non-null value means something else moved the branch
|
|
712
650
|
// since we cloned (a human push, a race), which is a real conflict.
|
|
713
651
|
if (remote && remote !== workspace.baseCommit)
|
|
714
|
-
throw
|
|
652
|
+
throw this.remote.conflictError();
|
|
715
653
|
try {
|
|
716
|
-
await this.gitFor(workspace, ["push", "origin", `${commitSha}:refs/heads/${workspace.headRef}`],
|
|
717
|
-
authToken: token,
|
|
718
|
-
});
|
|
654
|
+
await this.gitFor(workspace, [...this.remote.gitConfig, "push", "origin", `${commitSha}:refs/heads/${workspace.headRef}`], auth);
|
|
719
655
|
}
|
|
720
656
|
catch (error) {
|
|
721
|
-
|
|
657
|
+
const after = await this.remoteHead(workspace, auth);
|
|
658
|
+
if (after === commitSha)
|
|
722
659
|
return;
|
|
660
|
+
// The branch moved between the check above and the push (to something
|
|
661
|
+
// other than where we started or where we were going): report it as the
|
|
662
|
+
// remote's conflict rather than the push's own (non-fast-forward)
|
|
663
|
+
// failure. A remote still at baseCommit (a continuation) or still absent
|
|
664
|
+
// means the push itself failed -- auth, a protected branch, a network
|
|
665
|
+
// error, a rejecting hook -- so that error is the one to surface.
|
|
666
|
+
if (after !== null && after !== workspace.baseCommit && after !== commitSha) {
|
|
667
|
+
throw this.remote.conflictError();
|
|
668
|
+
}
|
|
723
669
|
throw error;
|
|
724
670
|
}
|
|
725
671
|
});
|
|
726
672
|
}
|
|
727
|
-
async remoteHead(workspace,
|
|
728
|
-
const result = await this.gitFor(workspace, ["ls-remote", "--heads", "origin", `refs/heads/${workspace.headRef}`],
|
|
729
|
-
authToken: token,
|
|
730
|
-
});
|
|
673
|
+
async remoteHead(workspace, auth) {
|
|
674
|
+
const result = await this.gitFor(workspace, [...this.remote.gitConfig, "ls-remote", "--heads", "origin", `refs/heads/${workspace.headRef}`], auth);
|
|
731
675
|
const line = result.stdout.trim();
|
|
732
676
|
if (!line)
|
|
733
677
|
return null;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type GitHubRepositoryAccess, type RepositoryFileInput } from "./github.js";
|
|
2
|
+
import type { GitRemote, PublishInput, PublishResult } from "./remote.js";
|
|
3
|
+
import type { ContinuationFinishedDetails, ContinuationOutcome, PreparedWorkspace } from "./types.js";
|
|
4
|
+
export interface GitHubRemoteOptions {
|
|
5
|
+
github: GitHubRepositoryAccess;
|
|
6
|
+
/** Test-only transport override; production composition always uses github.com. */
|
|
7
|
+
cloneUrlForRepository?: (repository: string) => string;
|
|
8
|
+
/** Test-only delay override for assertContinuationOpen's single retry wait. */
|
|
9
|
+
sleep?: (milliseconds: number) => Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
/** github.com through a GitHub App: installation tokens for git, draft pull requests after a push. */
|
|
12
|
+
export declare class GitHubRemote implements GitRemote {
|
|
13
|
+
private readonly options;
|
|
14
|
+
readonly gitConfig: readonly string[];
|
|
15
|
+
private readonly cloneUrlForRepository;
|
|
16
|
+
private readonly sleep;
|
|
17
|
+
constructor(options: GitHubRemoteOptions);
|
|
18
|
+
normalizeRepository(repository: string): string;
|
|
19
|
+
cloneUrl(repository: string): string;
|
|
20
|
+
withAccess<T>(repository: string, fn: (token: string | undefined) => Promise<T>): Promise<T>;
|
|
21
|
+
conflictError(): Error;
|
|
22
|
+
/**
|
|
23
|
+
* A continuation never pushes to a pull request that is no longer open.
|
|
24
|
+
* Fresh runs (no `continuation`) skip this, and so does a GitHub client
|
|
25
|
+
* that exposes no `findOpenPullRequest` (optional on GitHubRepositoryAccess).
|
|
26
|
+
*
|
|
27
|
+
* GitHub's answer is trusted only when it is definite: a found open PR
|
|
28
|
+
* proceeds normally, and a confirmed "no open PR" (a successful call that
|
|
29
|
+
* returns null) refuses outright with CONTINUATION_CLOSED_ERROR. A
|
|
30
|
+
* transient failure -- network error, 5xx, timeout, rate limit, anything
|
|
31
|
+
* that makes the call itself throw rather than answer -- is retried once;
|
|
32
|
+
* if it throws again, this proceeds as if the pull request were still
|
|
33
|
+
* open and logs a warning, rather than failing the run on an unconfirmed
|
|
34
|
+
* answer (especially not here, right before the push, after all the
|
|
35
|
+
* work is already done).
|
|
36
|
+
*/
|
|
37
|
+
assertContinuationOpen(input: {
|
|
38
|
+
repository: string;
|
|
39
|
+
baseRef: string;
|
|
40
|
+
headRef: string;
|
|
41
|
+
continuation?: {
|
|
42
|
+
rootRunId: string;
|
|
43
|
+
};
|
|
44
|
+
}): Promise<void>;
|
|
45
|
+
publish({ workspace: prepared, details, changedPaths }: PublishInput): Promise<PublishResult>;
|
|
46
|
+
/**
|
|
47
|
+
* Best-effort "wardby is working on this PR" signal (see VcsProvider) --
|
|
48
|
+
* whole body wrapped so this can NEVER throw or otherwise affect the
|
|
49
|
+
* real coding run, mirroring docker.ts's readWorkerFailureDiagnostic
|
|
50
|
+
* ("diagnostics are optional and must never affect terminal cleanup").
|
|
51
|
+
* No-op for a fresh (non-continuation) workspace: there's no PR to
|
|
52
|
+
* attach anything to until its one commit lands.
|
|
53
|
+
*/
|
|
54
|
+
notifyContinuationStarted(workspace: PreparedWorkspace, details?: {
|
|
55
|
+
agentName?: string;
|
|
56
|
+
}): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Companion to notifyContinuationStarted -- finds and updates whatever
|
|
59
|
+
* that call created, never creates fresh state itself (see
|
|
60
|
+
* upsertContinuationStatusComment vs updateContinuationStatusComment,
|
|
61
|
+
* and createContinuationCheckRun vs completeContinuationCheckRun in
|
|
62
|
+
* github.ts). Safe to call more than once for the same run. Same
|
|
63
|
+
* never-throw contract as notifyContinuationStarted.
|
|
64
|
+
*/
|
|
65
|
+
notifyContinuationFinished(workspace: PreparedWorkspace, outcome: ContinuationOutcome, details?: ContinuationFinishedDetails): Promise<void>;
|
|
66
|
+
readRepositoryFile(input: RepositoryFileInput): Promise<string | null>;
|
|
67
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { CONTINUATION_CLOSED_ERROR } from "../../coding/continuation-wording.js";
|
|
2
|
+
import { normalizeGitHubRepository, normalizeGitRef } from "../../coding/protocol.js";
|
|
3
|
+
import { logger } from "../../core/logger.js";
|
|
4
|
+
import { validateChangedPath } from "./git.js";
|
|
5
|
+
import { isLockfilePath, } from "./github.js";
|
|
6
|
+
/** How long assertContinuationOpen waits before its single retry on a transient open-PR check failure. */
|
|
7
|
+
const CONTINUATION_OPEN_RETRY_DELAY_MS = 1_000;
|
|
8
|
+
/** Human-readable label for continuation status notifications -- falls back to the opaque run id alone when no agent name is known. */
|
|
9
|
+
function runLabel(agentName, runId) {
|
|
10
|
+
return agentName ? `${agentName} (wardby run ${runId})` : `wardby run ${runId}`;
|
|
11
|
+
}
|
|
12
|
+
/** github.com through a GitHub App: installation tokens for git, draft pull requests after a push. */
|
|
13
|
+
export class GitHubRemote {
|
|
14
|
+
options;
|
|
15
|
+
gitConfig = [];
|
|
16
|
+
cloneUrlForRepository;
|
|
17
|
+
sleep;
|
|
18
|
+
constructor(options) {
|
|
19
|
+
this.options = options;
|
|
20
|
+
this.cloneUrlForRepository =
|
|
21
|
+
options.cloneUrlForRepository ?? ((repository) => `https://github.com/${repository}.git`);
|
|
22
|
+
this.sleep =
|
|
23
|
+
options.sleep ?? ((milliseconds) => new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds)));
|
|
24
|
+
}
|
|
25
|
+
normalizeRepository(repository) {
|
|
26
|
+
return normalizeGitHubRepository(repository);
|
|
27
|
+
}
|
|
28
|
+
cloneUrl(repository) {
|
|
29
|
+
const cloneUrl = this.cloneUrlForRepository(repository);
|
|
30
|
+
if (!this.options.cloneUrlForRepository && cloneUrl !== `https://github.com/${repository}.git`) {
|
|
31
|
+
throw new Error("vcs_remote_invalid");
|
|
32
|
+
}
|
|
33
|
+
return cloneUrl;
|
|
34
|
+
}
|
|
35
|
+
withAccess(repository, fn) {
|
|
36
|
+
return this.options.github.withRepositoryToken(repository, fn);
|
|
37
|
+
}
|
|
38
|
+
conflictError() {
|
|
39
|
+
return new Error("vcs_head_ref_conflict");
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A continuation never pushes to a pull request that is no longer open.
|
|
43
|
+
* Fresh runs (no `continuation`) skip this, and so does a GitHub client
|
|
44
|
+
* that exposes no `findOpenPullRequest` (optional on GitHubRepositoryAccess).
|
|
45
|
+
*
|
|
46
|
+
* GitHub's answer is trusted only when it is definite: a found open PR
|
|
47
|
+
* proceeds normally, and a confirmed "no open PR" (a successful call that
|
|
48
|
+
* returns null) refuses outright with CONTINUATION_CLOSED_ERROR. A
|
|
49
|
+
* transient failure -- network error, 5xx, timeout, rate limit, anything
|
|
50
|
+
* that makes the call itself throw rather than answer -- is retried once;
|
|
51
|
+
* if it throws again, this proceeds as if the pull request were still
|
|
52
|
+
* open and logs a warning, rather than failing the run on an unconfirmed
|
|
53
|
+
* answer (especially not here, right before the push, after all the
|
|
54
|
+
* work is already done).
|
|
55
|
+
*/
|
|
56
|
+
async assertContinuationOpen(input) {
|
|
57
|
+
if (!input.continuation || !this.options.github.findOpenPullRequest)
|
|
58
|
+
return;
|
|
59
|
+
const request = {
|
|
60
|
+
runId: input.continuation.rootRunId,
|
|
61
|
+
repository: input.repository,
|
|
62
|
+
baseRef: input.baseRef,
|
|
63
|
+
headRef: input.headRef,
|
|
64
|
+
};
|
|
65
|
+
let open;
|
|
66
|
+
try {
|
|
67
|
+
open = await this.options.github.findOpenPullRequest(request);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
await this.sleep(CONTINUATION_OPEN_RETRY_DELAY_MS);
|
|
71
|
+
try {
|
|
72
|
+
open = await this.options.github.findOpenPullRequest(request);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
logger.warn({ err: error, runId: request.runId, repository: request.repository }, "continuation open-PR check failed twice in a row; proceeding as if the pull request is still open");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!open)
|
|
80
|
+
throw new Error(CONTINUATION_CLOSED_ERROR);
|
|
81
|
+
}
|
|
82
|
+
async publish({ workspace: prepared, details, changedPaths }) {
|
|
83
|
+
// Revision-in-place: identify the PR by the run that originally opened
|
|
84
|
+
// it (createOrFindDraftPullRequest's marker-based lookup keys on that
|
|
85
|
+
// run's id), not this run's own -- this finds the existing open PR and
|
|
86
|
+
// returns it rather than creating a new one, since headRef/baseRef
|
|
87
|
+
// already match it exactly. No other change needed here: pushing a new
|
|
88
|
+
// commit onto that branch already updates the PR natively -- which is
|
|
89
|
+
// also why a PR a person has since marked ready for review is accepted
|
|
90
|
+
// (acceptReadyForReview) rather than failing an already-pushed run.
|
|
91
|
+
const pullRequest = await this.options.github.createOrFindDraftPullRequest({
|
|
92
|
+
runId: prepared.continuation?.rootRunId ?? prepared.runId,
|
|
93
|
+
...(prepared.continuation ? { acceptReadyForReview: true } : {}),
|
|
94
|
+
repository: prepared.repository,
|
|
95
|
+
baseRef: prepared.baseRef,
|
|
96
|
+
headRef: prepared.headRef,
|
|
97
|
+
summary: details?.summary,
|
|
98
|
+
tests: details?.tests,
|
|
99
|
+
tag: details?.tag,
|
|
100
|
+
packages: details?.packages,
|
|
101
|
+
packageRefusals: details?.packageRefusals,
|
|
102
|
+
changedLockfiles: changedPaths.filter(isLockfilePath),
|
|
103
|
+
...(details?.issue ? { issue: details.issue } : {}),
|
|
104
|
+
...(details?.related ? { related: details.related } : {}),
|
|
105
|
+
});
|
|
106
|
+
return { kind: "pull_request", number: pullRequest.number, url: pullRequest.url };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Best-effort "wardby is working on this PR" signal (see VcsProvider) --
|
|
110
|
+
* whole body wrapped so this can NEVER throw or otherwise affect the
|
|
111
|
+
* real coding run, mirroring docker.ts's readWorkerFailureDiagnostic
|
|
112
|
+
* ("diagnostics are optional and must never affect terminal cleanup").
|
|
113
|
+
* No-op for a fresh (non-continuation) workspace: there's no PR to
|
|
114
|
+
* attach anything to until its one commit lands.
|
|
115
|
+
*/
|
|
116
|
+
async notifyContinuationStarted(workspace, details) {
|
|
117
|
+
if (!workspace.continuation)
|
|
118
|
+
return;
|
|
119
|
+
try {
|
|
120
|
+
const identity = {
|
|
121
|
+
runId: workspace.runId,
|
|
122
|
+
rootRunId: workspace.continuation.rootRunId,
|
|
123
|
+
repository: workspace.repository,
|
|
124
|
+
baseRef: workspace.baseRef,
|
|
125
|
+
headRef: workspace.headRef,
|
|
126
|
+
};
|
|
127
|
+
await Promise.allSettled([
|
|
128
|
+
this.options.github.upsertContinuationStatusComment({
|
|
129
|
+
...identity,
|
|
130
|
+
body: `🔄 ${runLabel(details?.agentName, workspace.runId)} is working on this PR...`,
|
|
131
|
+
}),
|
|
132
|
+
this.options.github.createContinuationCheckRun({
|
|
133
|
+
repository: workspace.repository,
|
|
134
|
+
headSha: workspace.baseCommit,
|
|
135
|
+
runId: workspace.runId,
|
|
136
|
+
}),
|
|
137
|
+
]);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Best-effort observability only -- must never affect the real run.
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Companion to notifyContinuationStarted -- finds and updates whatever
|
|
145
|
+
* that call created, never creates fresh state itself (see
|
|
146
|
+
* upsertContinuationStatusComment vs updateContinuationStatusComment,
|
|
147
|
+
* and createContinuationCheckRun vs completeContinuationCheckRun in
|
|
148
|
+
* github.ts). Safe to call more than once for the same run. Same
|
|
149
|
+
* never-throw contract as notifyContinuationStarted.
|
|
150
|
+
*/
|
|
151
|
+
async notifyContinuationFinished(workspace, outcome, details) {
|
|
152
|
+
if (!workspace.continuation)
|
|
153
|
+
return;
|
|
154
|
+
try {
|
|
155
|
+
const identity = {
|
|
156
|
+
runId: workspace.runId,
|
|
157
|
+
rootRunId: workspace.continuation.rootRunId,
|
|
158
|
+
repository: workspace.repository,
|
|
159
|
+
baseRef: workspace.baseRef,
|
|
160
|
+
headRef: workspace.headRef,
|
|
161
|
+
};
|
|
162
|
+
const label = runLabel(details?.agentName, workspace.runId);
|
|
163
|
+
const summarySuffix = details?.summary ? `\n\n${details.summary}` : "";
|
|
164
|
+
const budgetSuffix = details?.budgetSentence ? ` ${details.budgetSentence}` : "";
|
|
165
|
+
const body = outcome === "succeeded"
|
|
166
|
+
? `✅ ${label} finished.${summarySuffix}`
|
|
167
|
+
: outcome === "budget_exhausted"
|
|
168
|
+
? `❌ ${label} ran out of budget.${budgetSuffix}${summarySuffix}`
|
|
169
|
+
: details?.serviceSentence
|
|
170
|
+
? `❌ ${label} could not start: ${details.serviceSentence}${summarySuffix}`
|
|
171
|
+
: details?.providerSentence
|
|
172
|
+
? `❌ ${label} could not run: ${details.providerSentence}${summarySuffix}`
|
|
173
|
+
: details?.protectedPathSentence
|
|
174
|
+
? `❌ ${label} could not open its changes: ${details.protectedPathSentence}${summarySuffix}`
|
|
175
|
+
: `❌ ${label} failed.${summarySuffix}`;
|
|
176
|
+
await Promise.allSettled([
|
|
177
|
+
this.options.github.updateContinuationStatusComment({ ...identity, body }),
|
|
178
|
+
this.options.github.completeContinuationCheckRun({
|
|
179
|
+
repository: workspace.repository,
|
|
180
|
+
headSha: workspace.baseCommit,
|
|
181
|
+
runId: workspace.runId,
|
|
182
|
+
outcome: outcome === "succeeded" ? "succeeded" : "failed",
|
|
183
|
+
}),
|
|
184
|
+
]);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// Best-effort observability only -- must never affect the real run.
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async readRepositoryFile(input) {
|
|
191
|
+
const github = this.options.github;
|
|
192
|
+
if (!github.readFileAtRef)
|
|
193
|
+
throw new Error("vcs_read_file_unsupported");
|
|
194
|
+
return github.readFileAtRef({
|
|
195
|
+
repository: normalizeGitHubRepository(input.repository),
|
|
196
|
+
ref: normalizeGitRef(input.ref),
|
|
197
|
+
path: validateChangedPath(input.path),
|
|
198
|
+
maxBytes: input.maxBytes,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|