@rosthq/cli 0.7.45 → 0.7.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/forge-workspace.d.ts +15 -0
- package/dist/forge-workspace.d.ts.map +1 -1
- package/dist/index.js +490 -13
- package/dist/index.js.map +3 -3
- package/dist/runner-sandbox.d.ts +1 -0
- package/dist/runner-sandbox.d.ts.map +1 -1
- package/dist/runner-serve.d.ts +53 -0
- package/dist/runner-serve.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -35,6 +35,21 @@ export type PrepareForgeWorkspaceOptions = {
|
|
|
35
35
|
ledger?: StepLedger;
|
|
36
36
|
};
|
|
37
37
|
export declare function prepareForgeWorkspace(options: PrepareForgeWorkspaceOptions): Promise<ForgeWorkspace>;
|
|
38
|
+
export type PrepareMergeWorkspaceOptions = {
|
|
39
|
+
baseDir: string;
|
|
40
|
+
tenantId: string;
|
|
41
|
+
buildRequestId: string;
|
|
42
|
+
repo: {
|
|
43
|
+
owner: string;
|
|
44
|
+
name: string;
|
|
45
|
+
default_branch: string;
|
|
46
|
+
};
|
|
47
|
+
credential: ForgeCredential;
|
|
48
|
+
headBranch: string;
|
|
49
|
+
remoteUrl?: string;
|
|
50
|
+
ledger?: StepLedger;
|
|
51
|
+
};
|
|
52
|
+
export declare function prepareMergeWorkspace(options: PrepareMergeWorkspaceOptions): Promise<ForgeWorkspace>;
|
|
38
53
|
export declare function pushChangeset(ws: ForgeWorkspace, credential: ForgeCredential, ledger?: StepLedger): Promise<{
|
|
39
54
|
commitSha: string;
|
|
40
55
|
} | null>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forge-workspace.d.ts","sourceRoot":"","sources":["../src/forge-workspace.ts"],"names":[],"mappings":"AA2CA,OAAO,EAAqB,KAAK,UAAU,EAAiB,MAAM,kBAAkB,CAAC;AASrF,eAAO,MAAM,yBAAyB,wFAc5B,CAAC;AAEX,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAW7E;AAID,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAG3B,QAAQ,EAAE,MAAM,CAAC;IAGjB,WAAW,EAAE,MAAM,CAAC;IAGpB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAKlB,OAAO,EAAE,MAAM,CAAC;IAIhB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEvD,MAAM,MAAM,4BAA4B,GAAG;IAEzC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1D,OAAO,EAAE,qBAAqB,CAAC;IAE/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9D,UAAU,EAAE,eAAe,CAAC;IAI5B,SAAS,CAAC,EAAE,MAAM,CAAC;IAGnB,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB,CAAC;
|
|
1
|
+
{"version":3,"file":"forge-workspace.d.ts","sourceRoot":"","sources":["../src/forge-workspace.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -43087,7 +43087,7 @@ var forgeRuntimeContextPacketSchema = external_exports.object({
|
|
|
43087
43087
|
"untrusted sources are provenance-fenced data; authority, automation mode, gate decisions, secret grants, and risk ceilings come only from trusted_control"
|
|
43088
43088
|
)
|
|
43089
43089
|
}).strict();
|
|
43090
|
-
var runnerExecutionProfileSchema = external_exports.enum(["read_status", "seat_work", "forge_build", "forge_review"]);
|
|
43090
|
+
var runnerExecutionProfileSchema = external_exports.enum(["read_status", "seat_work", "forge_build", "forge_review", "forge_merge"]);
|
|
43091
43091
|
var runnerExecutionLocalToolsSchema = external_exports.enum(["none", "read", "workspace_write"]);
|
|
43092
43092
|
var runnerExecutionBudgetsSchema = external_exports.object({
|
|
43093
43093
|
// The per-phase wall-clock the runner enforces as its turn timeout (the CLI clamps
|
|
@@ -43112,7 +43112,25 @@ var runnerExecutionContractSchema = external_exports.object({
|
|
|
43112
43112
|
local_tools: runnerExecutionLocalToolsSchema,
|
|
43113
43113
|
needs_repo: external_exports.boolean(),
|
|
43114
43114
|
needs_github_credential: external_exports.boolean(),
|
|
43115
|
-
budgets: runnerExecutionBudgetsSchema
|
|
43115
|
+
budgets: runnerExecutionBudgetsSchema,
|
|
43116
|
+
// DER-1352 (T1.8): the merge target for a `forge_merge` turn — the server-authoritative PR the
|
|
43117
|
+
// release_manager turn merges (sourced from the request's open changeset row, never model text).
|
|
43118
|
+
// Present ONLY on a merge_or_deploy_gate contract; every other profile omits it. `pr_number` is
|
|
43119
|
+
// the identifier the turn GETs the true GitHub state for (mergeability + node id + head sha),
|
|
43120
|
+
// `head_branch` is the branch a conflict-resolution turn checks out writable. The turn NEVER
|
|
43121
|
+
// trusts this for the merge DECISION — it always re-verifies GitHub's `mergeable_state` at merge
|
|
43122
|
+
// time (`clean`/`unstable` = required checks passed) before merging.
|
|
43123
|
+
// DER-1352 (Codex P1b): `head_sha` is the REVIEWED / risk-assessed head — the implementation
|
|
43124
|
+
// commit the review + risk phases ran against (server-sourced, never model text). The merge turn
|
|
43125
|
+
// merges ONLY if GitHub's CURRENT head still equals it; if the PR head advanced since review, the
|
|
43126
|
+
// new commits are unreviewed + un-risk-assessed, so it parks for re-review instead of merging.
|
|
43127
|
+
// Omitted only for a transitional changeset with no recorded implementation head, in which case
|
|
43128
|
+
// the merge turn fails CLOSED (parks — it cannot prove the head was risk-assessed).
|
|
43129
|
+
merge: external_exports.object({
|
|
43130
|
+
pr_number: external_exports.number().int().positive(),
|
|
43131
|
+
head_branch: external_exports.string().trim().min(1).max(255),
|
|
43132
|
+
head_sha: external_exports.string().trim().regex(/^[0-9a-f]{7,40}$/i, "head_sha must be a git SHA (7\u201340 hex chars)").optional()
|
|
43133
|
+
}).strict().optional()
|
|
43116
43134
|
}).strict();
|
|
43117
43135
|
var turnReportText = external_exports.string().trim().min(1).max(2e3);
|
|
43118
43136
|
var runnerTurnReportIssueSchema = external_exports.object({
|
|
@@ -52127,22 +52145,18 @@ async function configureForgeIdentity(baseRepo) {
|
|
|
52127
52145
|
await runGit(["config", "user.name", FORGE_GIT_AUTHOR_NAME], baseRepo);
|
|
52128
52146
|
await runGit(["config", "user.email", FORGE_GIT_AUTHOR_EMAIL], baseRepo);
|
|
52129
52147
|
}
|
|
52130
|
-
async function
|
|
52131
|
-
|
|
52132
|
-
throw new Error("prepareForgeWorkspace: review profile requires headSha");
|
|
52133
|
-
}
|
|
52134
|
-
const baseRepo = baseRepoDir(options.baseDir, options.buildRequestId);
|
|
52135
|
-
const url2 = remoteUrlFor(options);
|
|
52148
|
+
async function ensureBaseClone(input) {
|
|
52149
|
+
const baseRepo = baseRepoDir(input.baseDir, input.buildRequestId);
|
|
52136
52150
|
await mkdir2(path3.dirname(baseRepo), { recursive: true, mode: 448 });
|
|
52137
|
-
await withLedgerStep(
|
|
52151
|
+
await withLedgerStep(input.ledger, "clone", (retryCounter) => withAskpass(input.credential, async (netEnv) => {
|
|
52138
52152
|
if (await isGitRepo(baseRepo)) {
|
|
52139
|
-
await runGitNetwork("git fetch", ["fetch", "--prune", "--quiet",
|
|
52153
|
+
await runGitNetwork("git fetch", ["fetch", "--prune", "--quiet", input.url, "+refs/heads/*:refs/remotes/origin/*"], netEnv, baseRepo, FETCH_TIMEOUT_MS, retryCounter);
|
|
52140
52154
|
} else {
|
|
52141
52155
|
await runGitNetwork(
|
|
52142
52156
|
"git clone",
|
|
52143
52157
|
// Non-shallow (no --depth) so merge-base/ancestry survive for later phases;
|
|
52144
52158
|
// --no-checkout keeps the base a bare-ish object store the worktrees hang off.
|
|
52145
|
-
["clone", "--no-checkout", "--quiet",
|
|
52159
|
+
["clone", "--no-checkout", "--quiet", input.url, baseRepo],
|
|
52146
52160
|
netEnv,
|
|
52147
52161
|
void 0,
|
|
52148
52162
|
CLONE_TIMEOUT_MS,
|
|
@@ -52151,11 +52165,52 @@ async function prepareForgeWorkspace(options) {
|
|
|
52151
52165
|
}
|
|
52152
52166
|
}));
|
|
52153
52167
|
await configureForgeIdentity(baseRepo);
|
|
52168
|
+
return baseRepo;
|
|
52169
|
+
}
|
|
52170
|
+
async function prepareForgeWorkspace(options) {
|
|
52171
|
+
if (options.profile === "review" && !options.headSha) {
|
|
52172
|
+
throw new Error("prepareForgeWorkspace: review profile requires headSha");
|
|
52173
|
+
}
|
|
52174
|
+
const baseRepo = await ensureBaseClone({
|
|
52175
|
+
baseDir: options.baseDir,
|
|
52176
|
+
buildRequestId: options.buildRequestId,
|
|
52177
|
+
url: remoteUrlFor(options),
|
|
52178
|
+
credential: options.credential,
|
|
52179
|
+
ledger: options.ledger
|
|
52180
|
+
});
|
|
52154
52181
|
if (options.profile === "review") {
|
|
52155
52182
|
return await prepareReviewWorktree(options, baseRepo);
|
|
52156
52183
|
}
|
|
52157
52184
|
return await prepareBuildWorktree(options, baseRepo);
|
|
52158
52185
|
}
|
|
52186
|
+
async function prepareMergeWorkspace(options) {
|
|
52187
|
+
if (!options.headBranch || options.headBranch.trim().length === 0) {
|
|
52188
|
+
throw new Error("prepareMergeWorkspace: headBranch is required");
|
|
52189
|
+
}
|
|
52190
|
+
const url2 = options.remoteUrl ?? `https://github.com/${options.repo.owner}/${options.repo.name}.git`;
|
|
52191
|
+
const baseRepo = await ensureBaseClone({
|
|
52192
|
+
baseDir: options.baseDir,
|
|
52193
|
+
buildRequestId: options.buildRequestId,
|
|
52194
|
+
url: url2,
|
|
52195
|
+
credential: options.credential,
|
|
52196
|
+
ledger: options.ledger
|
|
52197
|
+
});
|
|
52198
|
+
const branch = options.headBranch;
|
|
52199
|
+
const worktreeDir = path3.join(requestRoot(options.baseDir, options.buildRequestId), "merge");
|
|
52200
|
+
const originDefault = `origin/${options.repo.default_branch}`;
|
|
52201
|
+
const originHead = `origin/${branch}`;
|
|
52202
|
+
await withLedgerStep(options.ledger, "branch", async () => {
|
|
52203
|
+
if (await worktreeRegistered(baseRepo, worktreeDir)) {
|
|
52204
|
+
await removeWorktree(baseRepo, worktreeDir);
|
|
52205
|
+
}
|
|
52206
|
+
if (await localBranchExists(baseRepo, branch)) {
|
|
52207
|
+
await runGit(["branch", "-D", branch], baseRepo);
|
|
52208
|
+
}
|
|
52209
|
+
await runGit(["worktree", "add", "-b", branch, worktreeDir, originHead], baseRepo);
|
|
52210
|
+
});
|
|
52211
|
+
const baseSha = (await runGit(["merge-base", "HEAD", originDefault], worktreeDir)).trim();
|
|
52212
|
+
return { baseRepo, worktreeDir, branch, readOnly: false, baseSha, remoteUrl: url2 };
|
|
52213
|
+
}
|
|
52159
52214
|
async function prepareBuildWorktree(options, baseRepo) {
|
|
52160
52215
|
const branch = branchName(options.buildRequestId, options.changeset);
|
|
52161
52216
|
const worktreeDir = buildWorktreeDir(options.baseDir, options.buildRequestId, options.changeset.ordinal);
|
|
@@ -52366,6 +52421,147 @@ async function openDraftPullRequestInner(options, retryCounter) {
|
|
|
52366
52421
|
}
|
|
52367
52422
|
throw new Error(`github pr create failed ${created.status}: ${redactSecrets(await readErrorText(created)).slice(0, 500)}`);
|
|
52368
52423
|
}
|
|
52424
|
+
function classifyMergeReadiness(state) {
|
|
52425
|
+
if (state.merged) {
|
|
52426
|
+
return "already_merged";
|
|
52427
|
+
}
|
|
52428
|
+
if (state.draft) {
|
|
52429
|
+
return "draft";
|
|
52430
|
+
}
|
|
52431
|
+
if (state.mergeable === false || state.mergeableState === "dirty") {
|
|
52432
|
+
return "conflicted";
|
|
52433
|
+
}
|
|
52434
|
+
switch (state.mergeableState) {
|
|
52435
|
+
// clean = all required checks passed, no conflicts. unstable = only NON-required checks are
|
|
52436
|
+
// failing/pending (GitHub still permits the merge) — treat as mergeable.
|
|
52437
|
+
case "clean":
|
|
52438
|
+
case "unstable":
|
|
52439
|
+
return "mergeable";
|
|
52440
|
+
case "blocked":
|
|
52441
|
+
return "blocked";
|
|
52442
|
+
case "behind":
|
|
52443
|
+
return "behind";
|
|
52444
|
+
case "unknown":
|
|
52445
|
+
case "":
|
|
52446
|
+
return "unknown";
|
|
52447
|
+
default:
|
|
52448
|
+
return "blocked";
|
|
52449
|
+
}
|
|
52450
|
+
}
|
|
52451
|
+
function toPullRequestState(json2) {
|
|
52452
|
+
const record2 = json2 && typeof json2 === "object" && !Array.isArray(json2) ? json2 : {};
|
|
52453
|
+
const head2 = record2.head && typeof record2.head === "object" ? record2.head : {};
|
|
52454
|
+
const number4 = typeof record2.number === "number" ? record2.number : Number.NaN;
|
|
52455
|
+
const nodeId = typeof record2.node_id === "string" ? record2.node_id : "";
|
|
52456
|
+
if (!Number.isFinite(number4) || !nodeId) {
|
|
52457
|
+
throw new Error("github pr response missing number or node_id");
|
|
52458
|
+
}
|
|
52459
|
+
return {
|
|
52460
|
+
number: number4,
|
|
52461
|
+
nodeId,
|
|
52462
|
+
merged: record2.merged === true,
|
|
52463
|
+
draft: record2.draft === true,
|
|
52464
|
+
mergeable: typeof record2.mergeable === "boolean" ? record2.mergeable : null,
|
|
52465
|
+
mergeableState: typeof record2.mergeable_state === "string" ? record2.mergeable_state : "unknown",
|
|
52466
|
+
headSha: typeof head2.sha === "string" ? head2.sha : "",
|
|
52467
|
+
mergedSha: typeof record2.merge_commit_sha === "string" && record2.merged === true ? record2.merge_commit_sha : null
|
|
52468
|
+
};
|
|
52469
|
+
}
|
|
52470
|
+
async function fetchPullRequestState(options) {
|
|
52471
|
+
const baseUrl = (options.apiBaseUrl ?? GITHUB_API_BASE_URL).replace(/\/+$/u, "");
|
|
52472
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
52473
|
+
const response = await withGithubRetries(
|
|
52474
|
+
"github pr get",
|
|
52475
|
+
async () => fetchImpl(`${baseUrl}/repos/${options.repo.owner}/${options.repo.name}/pulls/${options.prNumber}`, {
|
|
52476
|
+
method: "GET",
|
|
52477
|
+
headers: githubHeaders(options.credential)
|
|
52478
|
+
})
|
|
52479
|
+
);
|
|
52480
|
+
if (response.status !== 200) {
|
|
52481
|
+
throw new Error(`github pr get failed ${response.status}: ${redactSecrets(await readErrorText(response)).slice(0, 500)}`);
|
|
52482
|
+
}
|
|
52483
|
+
return toPullRequestState(await response.json());
|
|
52484
|
+
}
|
|
52485
|
+
async function mergePullRequest(options) {
|
|
52486
|
+
const baseUrl = (options.apiBaseUrl ?? GITHUB_API_BASE_URL).replace(/\/+$/u, "");
|
|
52487
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
52488
|
+
const { owner, name } = options.repo;
|
|
52489
|
+
if (options.state.merged) {
|
|
52490
|
+
return { status: "merged", mergedSha: options.state.mergedSha };
|
|
52491
|
+
}
|
|
52492
|
+
const merge2 = await withGithubRetries(
|
|
52493
|
+
"github pr merge",
|
|
52494
|
+
async () => fetchImpl(`${baseUrl}/repos/${owner}/${name}/pulls/${options.state.number}/merge`, {
|
|
52495
|
+
method: "PUT",
|
|
52496
|
+
headers: githubHeaders(options.credential),
|
|
52497
|
+
// SHA guard: merge ONLY the head we verified — never pass a merge_method (the repo default
|
|
52498
|
+
// wins; a queue owns the strategy). A stale sha → 409, re-checked by the caller.
|
|
52499
|
+
body: JSON.stringify({ sha: options.state.headSha })
|
|
52500
|
+
})
|
|
52501
|
+
);
|
|
52502
|
+
if (merge2.status === 405) {
|
|
52503
|
+
const detail = await readErrorText(merge2);
|
|
52504
|
+
if (/merge queue/i.test(detail)) {
|
|
52505
|
+
await enablePullRequestAutoMerge({ repo: options.repo, nodeId: options.state.nodeId, credential: options.credential, fetchImpl, apiBaseUrl: baseUrl });
|
|
52506
|
+
return { status: "queued" };
|
|
52507
|
+
}
|
|
52508
|
+
throw new Error(`github pr merge failed 405: ${redactSecrets(detail).slice(0, 500)}`);
|
|
52509
|
+
}
|
|
52510
|
+
if (merge2.status !== 200) {
|
|
52511
|
+
throw new Error(`github pr merge failed ${merge2.status}: ${redactSecrets(await readErrorText(merge2)).slice(0, 500)}`);
|
|
52512
|
+
}
|
|
52513
|
+
const confirmed = await fetchPullRequestState({ repo: options.repo, prNumber: options.state.number, credential: options.credential, fetchImpl, apiBaseUrl: baseUrl });
|
|
52514
|
+
if (!confirmed.merged) {
|
|
52515
|
+
throw new Error("github pr merge postcondition failed: PR is not merged after the merge call");
|
|
52516
|
+
}
|
|
52517
|
+
return { status: "merged", mergedSha: confirmed.mergedSha };
|
|
52518
|
+
}
|
|
52519
|
+
async function enablePullRequestAutoMerge(options) {
|
|
52520
|
+
const baseUrl = (options.apiBaseUrl ?? GITHUB_API_BASE_URL).replace(/\/+$/u, "");
|
|
52521
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
52522
|
+
const response = await withGithubRetries(
|
|
52523
|
+
"github enable auto-merge",
|
|
52524
|
+
async () => fetchImpl(`${baseUrl}/graphql`, {
|
|
52525
|
+
method: "POST",
|
|
52526
|
+
headers: githubHeaders(options.credential),
|
|
52527
|
+
body: JSON.stringify({
|
|
52528
|
+
query: "mutation($id:ID!){enablePullRequestAutoMerge(input:{pullRequestId:$id}){clientMutationId}}",
|
|
52529
|
+
variables: { id: options.nodeId }
|
|
52530
|
+
})
|
|
52531
|
+
})
|
|
52532
|
+
);
|
|
52533
|
+
if (response.status !== 200) {
|
|
52534
|
+
throw new Error(`github enable auto-merge failed ${response.status}: ${redactSecrets(await readErrorText(response)).slice(0, 500)}`);
|
|
52535
|
+
}
|
|
52536
|
+
const body = await response.json();
|
|
52537
|
+
if (Array.isArray(body.errors) && body.errors.length > 0) {
|
|
52538
|
+
throw new Error("github enable auto-merge returned errors");
|
|
52539
|
+
}
|
|
52540
|
+
}
|
|
52541
|
+
async function forcePushWithLease(ws, credential) {
|
|
52542
|
+
if (ws.readOnly || ws.branch.length === 0) {
|
|
52543
|
+
throw new Error("force-push requires a writable build worktree on a branch");
|
|
52544
|
+
}
|
|
52545
|
+
const head2 = (await runGit(["rev-parse", "HEAD"], ws.worktreeDir)).trim();
|
|
52546
|
+
await withAskpass(credential, async (netEnv) => {
|
|
52547
|
+
const before = await runGitNetwork("git ls-remote", ["ls-remote", ws.remoteUrl, `refs/heads/${ws.branch}`], netEnv, ws.worktreeDir, FETCH_TIMEOUT_MS);
|
|
52548
|
+
const beforeSha = before.split(/\s+/)[0]?.trim() ?? "";
|
|
52549
|
+
const forceArg = beforeSha ? `--force-with-lease=refs/heads/${ws.branch}:${beforeSha}` : null;
|
|
52550
|
+
await runGitNetwork(
|
|
52551
|
+
"git push --force-with-lease",
|
|
52552
|
+
["push", ...forceArg ? [forceArg] : [], "--quiet", ws.remoteUrl, `HEAD:refs/heads/${ws.branch}`],
|
|
52553
|
+
netEnv,
|
|
52554
|
+
ws.worktreeDir,
|
|
52555
|
+
PUSH_TIMEOUT_MS
|
|
52556
|
+
);
|
|
52557
|
+
const remote = await runGitNetwork("git ls-remote", ["ls-remote", ws.remoteUrl, `refs/heads/${ws.branch}`], netEnv, ws.worktreeDir, FETCH_TIMEOUT_MS);
|
|
52558
|
+
const remoteSha = remote.split(/\s+/)[0]?.trim() ?? "";
|
|
52559
|
+
if (remoteSha !== head2) {
|
|
52560
|
+
throw new Error(`git force-push postcondition failed: remote ${ws.branch} is ${remoteSha || "absent"}, expected ${head2}`);
|
|
52561
|
+
}
|
|
52562
|
+
});
|
|
52563
|
+
return { commitSha: head2 };
|
|
52564
|
+
}
|
|
52369
52565
|
async function removeWorktree(baseRepo, worktreeDir) {
|
|
52370
52566
|
try {
|
|
52371
52567
|
await runGit(["worktree", "remove", "--force", worktreeDir], baseRepo);
|
|
@@ -52548,6 +52744,9 @@ function isSandboxExecAvailable() {
|
|
|
52548
52744
|
return false;
|
|
52549
52745
|
}
|
|
52550
52746
|
}
|
|
52747
|
+
function claudeBashTempRoot(uid) {
|
|
52748
|
+
return `/private/tmp/claude-${uid}`;
|
|
52749
|
+
}
|
|
52551
52750
|
function resolveSandboxSpec(options) {
|
|
52552
52751
|
if (options.mode === "off") {
|
|
52553
52752
|
return { kind: "none", reason: "RUNNER_SANDBOX=off (loosened fallback - OS confinement disabled)" };
|
|
@@ -52570,12 +52769,17 @@ function resolveSandboxSpec(options) {
|
|
|
52570
52769
|
const denyWritePaths = (options.denyWritePaths ?? []).map(
|
|
52571
52770
|
(p) => p.endsWith("/") ? `${canonical(p.replace(/\/+$/, ""))}/` : canonicalFile(p)
|
|
52572
52771
|
);
|
|
52772
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
52773
|
+
const allowWritePaths = [
|
|
52774
|
+
...(options.allowWritePaths ?? []).map(canonical),
|
|
52775
|
+
...uid !== null ? [canonicalFile(claudeBashTempRoot(uid))] : []
|
|
52776
|
+
];
|
|
52573
52777
|
const profile = buildSeatbeltProfile({
|
|
52574
52778
|
workspaceDir,
|
|
52575
52779
|
tmpDir,
|
|
52576
52780
|
homeDir,
|
|
52577
52781
|
denyReadPaths,
|
|
52578
|
-
allowWritePaths
|
|
52782
|
+
allowWritePaths,
|
|
52579
52783
|
denyWritePaths,
|
|
52580
52784
|
allowNetwork,
|
|
52581
52785
|
strict: options.mode === "strict"
|
|
@@ -52993,7 +53197,7 @@ async function detectCapabilities(cliVersion, env, homeDir) {
|
|
|
52993
53197
|
claude: { ...claude, account_alias: accountAlias, rate_limited_until: null },
|
|
52994
53198
|
codex: { ...codex, account_alias: accountAlias, rate_limited_until: null },
|
|
52995
53199
|
turn_executions: { report_supported: true },
|
|
52996
|
-
...cliVersion ? { forge_build: cliVersion, seat_work: cliVersion } : {},
|
|
53200
|
+
...cliVersion ? { forge_build: cliVersion, seat_work: cliVersion, forge_merge: cliVersion } : {},
|
|
52997
53201
|
git,
|
|
52998
53202
|
gh,
|
|
52999
53203
|
scheduler: { max_parallel_sessions_estimate: null },
|
|
@@ -53149,6 +53353,11 @@ async function executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtim
|
|
|
53149
53353
|
...result.commitSha ? { commit_sha: result.commitSha } : {},
|
|
53150
53354
|
...result.evidence && result.evidence.length > 0 ? { evidence: result.evidence } : {},
|
|
53151
53355
|
...result.changedPaths && result.changedPaths.length > 0 ? { changed_paths: result.changedPaths } : {},
|
|
53356
|
+
// DER-1352 (T1.8): the merge-turn postcondition. Only sent when the harness GET-verified the PR
|
|
53357
|
+
// merged on GitHub — the driver's completion writer marks the changeset merged + writes the
|
|
53358
|
+
// decisions row ONLY on this. Gated on result.ok so a failed turn can never carry it.
|
|
53359
|
+
...result.ok && result.merged === true ? { merged: true } : {},
|
|
53360
|
+
...result.ok && result.merged === true && result.mergedCommitSha ? { merged_commit_sha: result.mergedCommitSha } : {},
|
|
53152
53361
|
// DER-1331 (T1.9): the step ledger + the model's (already redacted, bounded) transcript.
|
|
53153
53362
|
// Only the work-orders result route accepts these today (turn-executions is a separate,
|
|
53154
53363
|
// narrower endpoint — T1.9 scope); its Zod schema strips unrecognized keys on a non-strict
|
|
@@ -53492,6 +53701,9 @@ function buildTurnPrompt(input) {
|
|
|
53492
53701
|
}
|
|
53493
53702
|
async function spawnRunnerTurn(ctx, workOrder, runtime, kind) {
|
|
53494
53703
|
const execution = parseRunnerExecutionContract(workOrder);
|
|
53704
|
+
if (execution?.profile === "forge_merge") {
|
|
53705
|
+
return runForgeMergeTurn(ctx, workOrder, execution, runtime);
|
|
53706
|
+
}
|
|
53495
53707
|
let forgePrep = null;
|
|
53496
53708
|
if (execution?.needs_repo) {
|
|
53497
53709
|
const prepared = await prepareForgeTurn(ctx, workOrder, execution);
|
|
@@ -53574,6 +53786,271 @@ async function spawnRunnerTurn(ctx, workOrder, runtime, kind) {
|
|
|
53574
53786
|
}
|
|
53575
53787
|
return result;
|
|
53576
53788
|
}
|
|
53789
|
+
var MERGE_MERGEABILITY_POLLS = 5;
|
|
53790
|
+
var MERGE_MERGEABILITY_POLL_MS = 2e3;
|
|
53791
|
+
var MERGE_QUEUE_POLLS = 10;
|
|
53792
|
+
var MERGE_QUEUE_POLL_MS = 3e3;
|
|
53793
|
+
var MERGE_CONFLICT_SUMMARY_MAX = 12e3;
|
|
53794
|
+
function mergeParkResult(note, extra) {
|
|
53795
|
+
return { ok: true, summary: `[forge-merge] ${note}`.slice(0, MERGE_CONFLICT_SUMMARY_MAX), ...extra };
|
|
53796
|
+
}
|
|
53797
|
+
function mergeMergedResult(mergedSha, note) {
|
|
53798
|
+
return {
|
|
53799
|
+
ok: true,
|
|
53800
|
+
merged: true,
|
|
53801
|
+
...mergedSha ? { mergedCommitSha: mergedSha } : {},
|
|
53802
|
+
summary: `[forge-merge] ${note}${mergedSha ? ` (${mergedSha})` : ""}`.slice(0, MERGE_CONFLICT_SUMMARY_MAX)
|
|
53803
|
+
};
|
|
53804
|
+
}
|
|
53805
|
+
async function brokerForgeMergeCredential(ctx, workOrder) {
|
|
53806
|
+
const workOrderId = typeof workOrder.id === "string" ? workOrder.id : "";
|
|
53807
|
+
const forge = typeof workOrder.forge === "object" && workOrder.forge !== null ? workOrder.forge : null;
|
|
53808
|
+
const buildRequestId = typeof forge?.build_request_id === "string" ? forge.build_request_id : "";
|
|
53809
|
+
if (!workOrderId || !buildRequestId) {
|
|
53810
|
+
return { ok: false, summary: "[forge-merge] work order missing id or build request id" };
|
|
53811
|
+
}
|
|
53812
|
+
const credentialMintStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
53813
|
+
const brokered = await post2(
|
|
53814
|
+
ctx.fetchImpl,
|
|
53815
|
+
ctx.appUrl,
|
|
53816
|
+
`/api/runner/work-orders/${encodeURIComponent(workOrderId)}/github-credential`,
|
|
53817
|
+
{},
|
|
53818
|
+
ctx.state.runner_secret
|
|
53819
|
+
);
|
|
53820
|
+
ctx.ledger.record({
|
|
53821
|
+
step: "credential_mint",
|
|
53822
|
+
started_at: credentialMintStartedAt,
|
|
53823
|
+
ended_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
53824
|
+
ok: brokered.status === 200,
|
|
53825
|
+
retry_count: 0,
|
|
53826
|
+
...brokered.status === 200 ? {} : { error_class: classifyHttpStatus(brokered.status) }
|
|
53827
|
+
});
|
|
53828
|
+
if (brokered.status !== 200) {
|
|
53829
|
+
return { ok: false, summary: `[forge-merge] credential broker failed ${brokered.status}: ${redactForLog(JSON.stringify(brokered.json))}` };
|
|
53830
|
+
}
|
|
53831
|
+
const repoRaw = typeof brokered.json.repo === "object" && brokered.json.repo !== null ? brokered.json.repo : null;
|
|
53832
|
+
const token = typeof brokered.json.token === "string" ? brokered.json.token : "";
|
|
53833
|
+
const expiresAt = typeof brokered.json.expires_at === "string" ? brokered.json.expires_at : "";
|
|
53834
|
+
const owner = typeof repoRaw?.owner === "string" ? repoRaw.owner : "";
|
|
53835
|
+
const name = typeof repoRaw?.name === "string" ? repoRaw.name : "";
|
|
53836
|
+
const defaultBranch = typeof repoRaw?.default_branch === "string" ? repoRaw.default_branch : "";
|
|
53837
|
+
if (!token || !expiresAt || !owner || !name || !defaultBranch) {
|
|
53838
|
+
return { ok: false, summary: "[forge-merge] credential broker returned an incomplete credential" };
|
|
53839
|
+
}
|
|
53840
|
+
return { ok: true, credential: { token, expiresAt }, repo: { owner, name, default_branch: defaultBranch }, buildRequestId };
|
|
53841
|
+
}
|
|
53842
|
+
async function fetchSettledPullRequestState(ctx, input) {
|
|
53843
|
+
let state = await fetchPullRequestState({ repo: input.repo, prNumber: input.prNumber, credential: input.credential, fetchImpl: ctx.fetchImpl });
|
|
53844
|
+
for (let attempt = 0; attempt < MERGE_MERGEABILITY_POLLS && state.mergeable === null && !state.merged; attempt += 1) {
|
|
53845
|
+
await sleep3(MERGE_MERGEABILITY_POLL_MS);
|
|
53846
|
+
state = await fetchPullRequestState({ repo: input.repo, prNumber: input.prNumber, credential: input.credential, fetchImpl: ctx.fetchImpl });
|
|
53847
|
+
}
|
|
53848
|
+
return state;
|
|
53849
|
+
}
|
|
53850
|
+
async function pollForQueueMerge(ctx, input) {
|
|
53851
|
+
for (let attempt = 0; attempt < MERGE_QUEUE_POLLS; attempt += 1) {
|
|
53852
|
+
await sleep3(MERGE_QUEUE_POLL_MS);
|
|
53853
|
+
const state = await fetchPullRequestState({ repo: input.repo, prNumber: input.prNumber, credential: input.credential, fetchImpl: ctx.fetchImpl });
|
|
53854
|
+
if (state.merged) {
|
|
53855
|
+
return { mergedSha: state.mergedSha };
|
|
53856
|
+
}
|
|
53857
|
+
}
|
|
53858
|
+
return null;
|
|
53859
|
+
}
|
|
53860
|
+
async function resolveMergeConflictTurn(ctx, input) {
|
|
53861
|
+
const tenantId = typeof input.workOrder.tenant_id === "string" ? input.workOrder.tenant_id : ctx.state.tenant_id ?? "runner";
|
|
53862
|
+
let workspace;
|
|
53863
|
+
try {
|
|
53864
|
+
workspace = await prepareMergeWorkspace({
|
|
53865
|
+
baseDir: path5.join(ctx.config.homeDir, ".rost", "forge", ctx.state.runner_id),
|
|
53866
|
+
tenantId,
|
|
53867
|
+
buildRequestId: input.buildRequestId,
|
|
53868
|
+
repo: input.repo,
|
|
53869
|
+
credential: input.credential,
|
|
53870
|
+
headBranch: input.headBranch,
|
|
53871
|
+
ledger: ctx.ledger
|
|
53872
|
+
});
|
|
53873
|
+
} catch (error51) {
|
|
53874
|
+
return { pushed: false, result: { ok: false, summary: `[forge-merge] conflict workspace preparation failed: ${redactForLog(error51 instanceof Error ? error51.message : String(error51))}`.slice(0, MERGE_CONFLICT_SUMMARY_MAX) } };
|
|
53875
|
+
}
|
|
53876
|
+
const sandbox = resolveSandboxSpec({
|
|
53877
|
+
mode: ctx.config.sandboxMode,
|
|
53878
|
+
platform: process.platform,
|
|
53879
|
+
workspaceDir: workspace.worktreeDir,
|
|
53880
|
+
tmpDir: tmpdir2(),
|
|
53881
|
+
homeDir: ctx.config.homeDir,
|
|
53882
|
+
stateFile: ctx.config.stateFile,
|
|
53883
|
+
runnerBaseDir: path5.join(ctx.config.homeDir, ".rost"),
|
|
53884
|
+
allowWritePaths: [workspace.baseRepo],
|
|
53885
|
+
denyWritePaths: [
|
|
53886
|
+
`${path5.join(workspace.baseRepo, ".git", "hooks")}/`,
|
|
53887
|
+
path5.join(workspace.baseRepo, ".git", "config"),
|
|
53888
|
+
path5.join(workspace.worktreeDir, ".git")
|
|
53889
|
+
],
|
|
53890
|
+
allowNetwork: ctx.config.sandboxAllowNetwork
|
|
53891
|
+
});
|
|
53892
|
+
const modelResult = await spawnMergeConflictModel(ctx, {
|
|
53893
|
+
workOrder: input.workOrder,
|
|
53894
|
+
execution: input.execution,
|
|
53895
|
+
runtime: input.runtime,
|
|
53896
|
+
workspace,
|
|
53897
|
+
sandbox,
|
|
53898
|
+
defaultBranch: input.repo.default_branch
|
|
53899
|
+
});
|
|
53900
|
+
if (!modelResult.ok) {
|
|
53901
|
+
await removeWorkspace(workspace).catch(() => void 0);
|
|
53902
|
+
return { pushed: false, result: mergeParkResult(`could not resolve the merge conflict (${modelResult.summary.slice(0, 400)}); parking for a human.`) };
|
|
53903
|
+
}
|
|
53904
|
+
try {
|
|
53905
|
+
const pushed = await forcePushWithLease(workspace, input.credential);
|
|
53906
|
+
await removeWorkspace(workspace).catch(() => void 0);
|
|
53907
|
+
return { pushed: true, result: mergeParkResult(`resolved the conflict and force-pushed ${pushed.commitSha}`, { commitSha: pushed.commitSha }) };
|
|
53908
|
+
} catch (error51) {
|
|
53909
|
+
await removeWorkspace(workspace).catch(() => void 0);
|
|
53910
|
+
return { pushed: false, result: { ok: false, summary: `[forge-merge] conflict resolution force-push failed: ${redactForLog(error51 instanceof Error ? error51.message : String(error51))}`.slice(0, MERGE_CONFLICT_SUMMARY_MAX) } };
|
|
53911
|
+
}
|
|
53912
|
+
}
|
|
53913
|
+
async function spawnMergeConflictModel(ctx, input) {
|
|
53914
|
+
const mcp = typeof input.workOrder.mcp === "object" && input.workOrder.mcp !== null ? input.workOrder.mcp : {};
|
|
53915
|
+
const token = typeof mcp.token === "string" ? mcp.token : "";
|
|
53916
|
+
const url2 = typeof mcp.url === "string" ? `${ctx.appUrl.replace(/\/+$/, "")}${mcp.url}` : `${ctx.appUrl.replace(/\/+$/, "")}/mcp`;
|
|
53917
|
+
const mcpConfig = JSON.stringify({ mcpServers: { rost: { type: "http", url: url2, headers: { Authorization: `Bearer ${token}` } } } });
|
|
53918
|
+
const configPath = path5.join(tmpdir2(), `rost-runner-mcp-${createHash2("sha256").update(`${url2}:${Date.now()}:${Math.random()}`).digest("hex").slice(0, 16)}.json`);
|
|
53919
|
+
await writeFile3(configPath, `${mcpConfig}
|
|
53920
|
+
`, { mode: 384 });
|
|
53921
|
+
await chmod2(configPath, 384);
|
|
53922
|
+
const plan = resolveTurnPlan({ kind: "work_order", execution: input.execution, repoDir: input.workspace.worktreeDir });
|
|
53923
|
+
const prompt = [
|
|
53924
|
+
"You are resolving a merge conflict on a pull request branch so it can merge into the base branch.",
|
|
53925
|
+
`A WRITABLE checkout of the PR's head branch is at your current working directory. Run \`git merge origin/${input.defaultBranch}\` to merge the base branch in, resolve EVERY conflict so the result is correct and builds, then \`git add\` the resolved files and \`git commit\` the merge. If the project has a fast build or test, run it to confirm the resolution.`,
|
|
53926
|
+
"Do NOT run git push, git remote, or any network git command, and never request or handle GitHub tokens \u2014 the harness pushes and merges after you finish. If the conflict cannot be resolved safely, do NOT force a bad merge: leave the branch uncommitted and stop with a short explanation.",
|
|
53927
|
+
"End with a single concise final message summarizing exactly what you resolved."
|
|
53928
|
+
].join("\n");
|
|
53929
|
+
let sandboxProfilePath = null;
|
|
53930
|
+
if (input.sandbox.kind === "seatbelt") {
|
|
53931
|
+
sandboxProfilePath = path5.join(tmpdir2(), `rost-runner-sandbox-${createHash2("sha256").update(`${configPath}:${Math.random()}`).digest("hex").slice(0, 16)}.sb`);
|
|
53932
|
+
await writeFile3(sandboxProfilePath, input.sandbox.profile, { mode: 384 });
|
|
53933
|
+
await chmod2(sandboxProfilePath, 384);
|
|
53934
|
+
}
|
|
53935
|
+
const built = buildTurnCommand({ runtime: input.runtime, prompt, configPath, plan });
|
|
53936
|
+
const { command, args } = wrapCommandWithSandbox(input.sandbox, built.command, built.args, sandboxProfilePath);
|
|
53937
|
+
const spawnStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
53938
|
+
const result = await runModelProcess({
|
|
53939
|
+
command,
|
|
53940
|
+
args,
|
|
53941
|
+
cwd: input.workspace.worktreeDir,
|
|
53942
|
+
timeoutMs: plan.timeoutMs,
|
|
53943
|
+
// The model gets the EXPLICIT allowlist env — never the merge token, GIT_ASKPASS, or the bearer.
|
|
53944
|
+
env: buildModelSpawnEnv(process.env),
|
|
53945
|
+
expectSessionEnvelope: false
|
|
53946
|
+
});
|
|
53947
|
+
const exitClass = classifyModelExit(result);
|
|
53948
|
+
ctx.ledger.record({
|
|
53949
|
+
step: "model_exit",
|
|
53950
|
+
started_at: spawnStartedAt,
|
|
53951
|
+
ended_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
53952
|
+
ok: result.ok,
|
|
53953
|
+
retry_count: 0,
|
|
53954
|
+
...exitClass.errorClass ? { error_class: exitClass.errorClass } : {}
|
|
53955
|
+
});
|
|
53956
|
+
void rm4(configPath, { force: true });
|
|
53957
|
+
if (sandboxProfilePath) {
|
|
53958
|
+
void rm4(sandboxProfilePath, { force: true });
|
|
53959
|
+
}
|
|
53960
|
+
return result;
|
|
53961
|
+
}
|
|
53962
|
+
async function runForgeMergeTurn(ctx, workOrder, execution, runtime) {
|
|
53963
|
+
const merge2 = execution.merge;
|
|
53964
|
+
if (!merge2) {
|
|
53965
|
+
return mergeParkResult("no merge target bound on the execution contract; parking for a human.");
|
|
53966
|
+
}
|
|
53967
|
+
const prepared = await brokerForgeMergeCredential(ctx, workOrder);
|
|
53968
|
+
if (!prepared.ok) {
|
|
53969
|
+
return { ok: false, summary: prepared.summary };
|
|
53970
|
+
}
|
|
53971
|
+
const { credential, repo, buildRequestId } = prepared;
|
|
53972
|
+
const mergeStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
53973
|
+
try {
|
|
53974
|
+
const outcome = await executeForgeMerge(ctx, {
|
|
53975
|
+
workOrder,
|
|
53976
|
+
execution,
|
|
53977
|
+
runtime,
|
|
53978
|
+
repo,
|
|
53979
|
+
credential,
|
|
53980
|
+
buildRequestId,
|
|
53981
|
+
prNumber: merge2.pr_number,
|
|
53982
|
+
headBranch: merge2.head_branch
|
|
53983
|
+
});
|
|
53984
|
+
ctx.ledger.record({
|
|
53985
|
+
step: "merge",
|
|
53986
|
+
started_at: mergeStartedAt,
|
|
53987
|
+
ended_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
53988
|
+
ok: outcome.ok,
|
|
53989
|
+
retry_count: 0,
|
|
53990
|
+
...outcome.ok ? {} : { error_class: "conflict" }
|
|
53991
|
+
});
|
|
53992
|
+
return outcome;
|
|
53993
|
+
} catch (error51) {
|
|
53994
|
+
ctx.ledger.record({
|
|
53995
|
+
step: "merge",
|
|
53996
|
+
started_at: mergeStartedAt,
|
|
53997
|
+
ended_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
53998
|
+
ok: false,
|
|
53999
|
+
retry_count: 0,
|
|
54000
|
+
error_class: "server_error"
|
|
54001
|
+
});
|
|
54002
|
+
return { ok: false, summary: `[forge-merge] merge turn failed: ${redactForLog(error51 instanceof Error ? error51.message : String(error51))}`.slice(0, MERGE_CONFLICT_SUMMARY_MAX) };
|
|
54003
|
+
}
|
|
54004
|
+
}
|
|
54005
|
+
async function executeForgeMerge(ctx, input) {
|
|
54006
|
+
const { repo, credential, prNumber } = input;
|
|
54007
|
+
let state = await fetchSettledPullRequestState(ctx, { repo, prNumber, credential });
|
|
54008
|
+
let readiness = classifyMergeReadiness(state);
|
|
54009
|
+
if (readiness === "already_merged") {
|
|
54010
|
+
return mergeMergedResult(state.mergedSha, "PR already merged on GitHub");
|
|
54011
|
+
}
|
|
54012
|
+
const reviewedHeadSha = input.execution.merge?.head_sha;
|
|
54013
|
+
if (!reviewedHeadSha) {
|
|
54014
|
+
return mergeParkResult("no reviewed head SHA bound on the merge contract; parking for a human (cannot verify the PR head was risk-assessed).");
|
|
54015
|
+
}
|
|
54016
|
+
if (state.headSha !== reviewedHeadSha) {
|
|
54017
|
+
return mergeParkResult(`PR head advanced since review (reviewed ${reviewedHeadSha.slice(0, 12)}, GitHub now ${state.headSha.slice(0, 12) || "unknown"}); the new commits need re-review/re-risk \u2014 parking.`);
|
|
54018
|
+
}
|
|
54019
|
+
if (readiness === "blocked" || readiness === "draft" || readiness === "unknown") {
|
|
54020
|
+
return mergeParkResult(`PR is not ready to merge (state=${state.mergeableState}); parking for a human.`);
|
|
54021
|
+
}
|
|
54022
|
+
if (readiness === "conflicted" || readiness === "behind") {
|
|
54023
|
+
const resolved = await resolveMergeConflictTurn(ctx, {
|
|
54024
|
+
workOrder: input.workOrder,
|
|
54025
|
+
execution: input.execution,
|
|
54026
|
+
runtime: input.runtime,
|
|
54027
|
+
repo: input.repo,
|
|
54028
|
+
credential: input.credential,
|
|
54029
|
+
buildRequestId: input.buildRequestId,
|
|
54030
|
+
headBranch: input.headBranch
|
|
54031
|
+
});
|
|
54032
|
+
if (!resolved.pushed) {
|
|
54033
|
+
return resolved.result ?? mergeParkResult("could not resolve the merge conflict; parking for a human.");
|
|
54034
|
+
}
|
|
54035
|
+
state = await fetchSettledPullRequestState(ctx, { repo, prNumber, credential });
|
|
54036
|
+
readiness = classifyMergeReadiness(state);
|
|
54037
|
+
if (readiness === "already_merged") {
|
|
54038
|
+
return mergeMergedResult(state.mergedSha, "PR merged during conflict resolution");
|
|
54039
|
+
}
|
|
54040
|
+
if (readiness !== "mergeable") {
|
|
54041
|
+
return mergeParkResult(`conflict resolved and pushed, but the PR is not yet mergeable (state=${state.mergeableState}); parking for a re-check.`, resolved.result?.commitSha ? { commitSha: resolved.result.commitSha } : void 0);
|
|
54042
|
+
}
|
|
54043
|
+
}
|
|
54044
|
+
const outcome = await mergePullRequest({ repo, state, credential, fetchImpl: ctx.fetchImpl });
|
|
54045
|
+
if (outcome.status === "merged") {
|
|
54046
|
+
return mergeMergedResult(outcome.mergedSha, "PR merged");
|
|
54047
|
+
}
|
|
54048
|
+
const merged = await pollForQueueMerge(ctx, { repo, prNumber, credential });
|
|
54049
|
+
if (merged) {
|
|
54050
|
+
return mergeMergedResult(merged.mergedSha, "PR merged via the merge queue");
|
|
54051
|
+
}
|
|
54052
|
+
return mergeParkResult("auto-merge enabled (merge queue); the merge was not confirmed within the turn \u2014 parking for a re-check.");
|
|
54053
|
+
}
|
|
53577
54054
|
function scrubTranscriptSessionIds(value) {
|
|
53578
54055
|
return value.replace(/("?(?:session_id|sessionId)"?\s*[:=]\s*"?)[A-Za-z0-9._-]+("?)/gi, "$1[redacted]$2");
|
|
53579
54056
|
}
|