agentplane 0.3.16 → 0.3.17
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/.build-manifest.json +159 -54
- package/dist/backends/task-backend/redmine/backend-runtime.d.ts +4 -146
- package/dist/backends/task-backend/redmine/backend-runtime.d.ts.map +1 -1
- package/dist/backends/task-backend/redmine/backend-runtime.js +4 -258
- package/dist/backends/task-backend/redmine/mapping.js +1 -1
- package/dist/backends/task-backend/redmine/runtime-context.d.ts +98 -0
- package/dist/backends/task-backend/redmine/runtime-context.d.ts.map +1 -0
- package/dist/backends/task-backend/redmine/runtime-context.js +57 -0
- package/dist/backends/task-backend/redmine/runtime-methods.d.ts +33 -0
- package/dist/backends/task-backend/redmine/runtime-methods.d.ts.map +1 -0
- package/dist/backends/task-backend/redmine/runtime-methods.js +86 -0
- package/dist/backends/task-backend/redmine/runtime-operations.d.ts +19 -0
- package/dist/backends/task-backend/redmine/runtime-operations.d.ts.map +1 -0
- package/dist/backends/task-backend/redmine/runtime-operations.js +83 -0
- package/dist/backends/task-backend/redmine/runtime-state.d.ts +10 -0
- package/dist/backends/task-backend/redmine/runtime-state.d.ts.map +1 -0
- package/dist/backends/task-backend/redmine/runtime-state.js +45 -0
- package/dist/backends/task-backend/shared/constants.d.ts +1 -1
- package/dist/backends/task-backend/shared/constants.d.ts.map +1 -1
- package/dist/backends/task-backend/shared/constants.js +1 -1
- package/dist/backends/task-index.d.ts +0 -4
- package/dist/backends/task-index.d.ts.map +1 -1
- package/dist/backends/task-index.js +0 -33
- package/dist/cli/exit-codes.d.ts.map +1 -1
- package/dist/cli/exit-codes.js +1 -0
- package/dist/cli/http.d.ts.map +1 -1
- package/dist/cli/http.js +34 -15
- package/dist/cli/run-cli/command-loaders.d.ts +1 -5
- package/dist/cli/run-cli/command-loaders.d.ts.map +1 -1
- package/dist/cli/spec/errors.d.ts +5 -0
- package/dist/cli/spec/errors.d.ts.map +1 -1
- package/dist/cli/spec/errors.js +14 -1
- package/dist/cli/spec/parse.d.ts.map +1 -1
- package/dist/cli/spec/parse.js +8 -1
- package/dist/commands/branch/work-start.d.ts.map +1 -1
- package/dist/commands/branch/work-start.direct.d.ts +13 -0
- package/dist/commands/branch/work-start.direct.d.ts.map +1 -0
- package/dist/commands/branch/work-start.direct.js +75 -0
- package/dist/commands/branch/work-start.git.d.ts +3 -0
- package/dist/commands/branch/work-start.git.d.ts.map +1 -0
- package/dist/commands/branch/work-start.git.js +19 -0
- package/dist/commands/branch/work-start.hook-shim.d.ts +2 -0
- package/dist/commands/branch/work-start.hook-shim.d.ts.map +1 -0
- package/dist/commands/branch/work-start.hook-shim.js +38 -0
- package/dist/commands/branch/work-start.js +6 -235
- package/dist/commands/branch/work-start.materialize.d.ts +16 -0
- package/dist/commands/branch/work-start.materialize.d.ts.map +1 -0
- package/dist/commands/branch/work-start.materialize.js +110 -0
- package/dist/commands/doctor/fixes.d.ts +0 -5
- package/dist/commands/doctor/fixes.d.ts.map +1 -1
- package/dist/commands/doctor/fixes.js +0 -70
- package/dist/commands/doctor/workflow.d.ts.map +1 -1
- package/dist/commands/doctor/workflow.js +2 -23
- package/dist/commands/doctor.run.d.ts.map +1 -1
- package/dist/commands/doctor.run.js +1 -3
- package/dist/commands/hooks/index.d.ts +4 -20
- package/dist/commands/hooks/index.d.ts.map +1 -1
- package/dist/commands/hooks/index.js +4 -432
- package/dist/commands/hooks/install.d.ts +11 -0
- package/dist/commands/hooks/install.d.ts.map +1 -0
- package/dist/commands/hooks/install.js +136 -0
- package/dist/commands/hooks/run.commit-msg.d.ts +3 -0
- package/dist/commands/hooks/run.commit-msg.d.ts.map +1 -0
- package/dist/commands/hooks/run.commit-msg.js +67 -0
- package/dist/commands/hooks/run.d.ts +9 -0
- package/dist/commands/hooks/run.d.ts.map +1 -0
- package/dist/commands/hooks/run.js +45 -0
- package/dist/commands/hooks/run.post-merge.d.ts +3 -0
- package/dist/commands/hooks/run.post-merge.d.ts.map +1 -0
- package/dist/commands/hooks/run.post-merge.js +44 -0
- package/dist/commands/hooks/run.pre-commit.d.ts +3 -0
- package/dist/commands/hooks/run.pre-commit.d.ts.map +1 -0
- package/dist/commands/hooks/run.pre-commit.js +48 -0
- package/dist/commands/hooks/run.pre-push.d.ts +6 -0
- package/dist/commands/hooks/run.pre-push.d.ts.map +1 -0
- package/dist/commands/hooks/run.pre-push.js +88 -0
- package/dist/commands/hooks/shared.d.ts +7 -0
- package/dist/commands/hooks/shared.d.ts.map +1 -0
- package/dist/commands/hooks/shared.js +41 -0
- package/dist/commands/recipes/impl/index.d.ts.map +1 -1
- package/dist/commands/recipes/impl/index.js +13 -3
- package/dist/commands/task/hosted-close-pr.command.d.ts +2 -7
- package/dist/commands/task/hosted-close-pr.command.d.ts.map +1 -1
- package/dist/commands/task/hosted-close-pr.command.js +9 -373
- package/dist/commands/task/hosted-close-pr.execute.d.ts +3 -0
- package/dist/commands/task/hosted-close-pr.execute.d.ts.map +1 -0
- package/dist/commands/task/hosted-close-pr.execute.js +135 -0
- package/dist/commands/task/hosted-close-pr.postcheck.d.ts +3 -0
- package/dist/commands/task/hosted-close-pr.postcheck.d.ts.map +1 -0
- package/dist/commands/task/hosted-close-pr.postcheck.js +13 -0
- package/dist/commands/task/hosted-close-pr.precheck.d.ts +4 -0
- package/dist/commands/task/hosted-close-pr.precheck.d.ts.map +1 -0
- package/dist/commands/task/hosted-close-pr.precheck.js +288 -0
- package/dist/commands/task/hosted-close-pr.report.d.ts +4 -0
- package/dist/commands/task/hosted-close-pr.report.d.ts.map +1 -0
- package/dist/commands/task/hosted-close-pr.report.js +42 -0
- package/dist/commands/task/hosted-close-pr.types.d.ts +75 -0
- package/dist/commands/task/hosted-close-pr.types.d.ts.map +1 -0
- package/dist/commands/task/hosted-close-pr.types.js +1 -0
- package/dist/commands/upgrade/materialize.d.ts.map +1 -1
- package/dist/commands/upgrade/materialize.js +0 -7
- package/dist/commands/upgrade/source.d.ts +0 -1
- package/dist/commands/upgrade/source.d.ts.map +1 -1
- package/dist/commands/upgrade/source.js +1 -9
- package/dist/runner/context/base-prompts.d.ts.map +1 -1
- package/dist/runner/context/base-prompts.js +4 -0
- package/dist/runner/context/project-skill-prompt-blocks.d.ts +5 -0
- package/dist/runner/context/project-skill-prompt-blocks.d.ts.map +1 -0
- package/dist/runner/context/project-skill-prompt-blocks.js +57 -0
- package/dist/runner/process-supervision/run.d.ts.map +1 -1
- package/dist/runner/process-supervision/run.js +61 -59
- package/dist/shared/errors.d.ts +4 -1
- package/dist/shared/errors.d.ts.map +1 -1
- package/dist/shared/errors.js +6 -0
- package/dist/shared/workflow-artifacts.d.ts.map +1 -1
- package/dist/shared/workflow-artifacts.js +1 -8
- package/dist/workflow-runtime/file-ops.d.ts.map +1 -1
- package/dist/workflow-runtime/file-ops.js +1 -20
- package/dist/workflow-runtime/paths.d.ts.map +1 -1
- package/dist/workflow-runtime/paths.js +0 -1
- package/dist/workflow-runtime/types.d.ts +0 -1
- package/dist/workflow-runtime/types.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -1,19 +1,10 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
1
|
import { usageError } from "../../cli/spec/errors.js";
|
|
3
|
-
import { createCliEmitter } from "../../cli/output.js";
|
|
4
2
|
import { mapBackendError } from "../../cli/error-map.js";
|
|
5
|
-
import { exitCodeForError } from "../../cli/exit-codes.js";
|
|
6
|
-
import { fileExists } from "../../cli/fs-utils.js";
|
|
7
3
|
import { CliError } from "../../shared/errors.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { parsePrMeta } from "../shared/pr-meta.js";
|
|
13
|
-
import { resolveDefaultGithubRepo, runGhApiJson } from "../pr/internal/gh-api.js";
|
|
14
|
-
import { resolvePrPaths } from "../pr/internal/pr-paths.js";
|
|
15
|
-
import { resolveBaseBranch } from "@agentplaneorg/core";
|
|
16
|
-
import { taskCloseAlreadyRecordedOnBase } from "./close-tail-state.js";
|
|
4
|
+
import { executeHostedClosePrPlan } from "./hosted-close-pr.execute.js";
|
|
5
|
+
import { postcheckHostedClosePrResult } from "./hosted-close-pr.postcheck.js";
|
|
6
|
+
import { precheckHostedClosePr } from "./hosted-close-pr.precheck.js";
|
|
7
|
+
import { reportHostedClosePrExecutionResult, reportHostedClosePrOutcome, } from "./hosted-close-pr.report.js";
|
|
17
8
|
export const taskHostedClosePrSpec = {
|
|
18
9
|
id: ["task", "hosted-close-pr"],
|
|
19
10
|
group: "Task",
|
|
@@ -81,369 +72,14 @@ export const taskHostedClosePrSpec = {
|
|
|
81
72
|
repo: typeof raw.opts.repo === "string" ? raw.opts.repo : null,
|
|
82
73
|
}),
|
|
83
74
|
};
|
|
84
|
-
async function resolveGithubRepo(opts) {
|
|
85
|
-
const repo = opts.repoOverride?.trim() ?? "";
|
|
86
|
-
if (repo)
|
|
87
|
-
return repo;
|
|
88
|
-
return await resolveDefaultGithubRepo(opts.gitRoot);
|
|
89
|
-
}
|
|
90
|
-
function selectMergedPullRecord(opts) {
|
|
91
|
-
const merged = opts.pulls.filter((record) => {
|
|
92
|
-
if (typeof record.merged_at !== "string" || record.merged_at.trim().length === 0)
|
|
93
|
-
return false;
|
|
94
|
-
const headRef = record.head?.ref?.trim() ?? "";
|
|
95
|
-
if (opts.sourceBranch && headRef && headRef !== opts.sourceBranch)
|
|
96
|
-
return false;
|
|
97
|
-
const baseRef = record.base?.ref?.trim() ?? "";
|
|
98
|
-
if (opts.baseBranch && baseRef && baseRef !== opts.baseBranch)
|
|
99
|
-
return false;
|
|
100
|
-
return true;
|
|
101
|
-
});
|
|
102
|
-
if (merged.length === 0)
|
|
103
|
-
return null;
|
|
104
|
-
if (typeof opts.prNumber === "number" && opts.prNumber > 0) {
|
|
105
|
-
const exact = merged.find((record) => Number(record.number ?? 0) === opts.prNumber);
|
|
106
|
-
if (exact)
|
|
107
|
-
return exact;
|
|
108
|
-
}
|
|
109
|
-
return ([...merged].toSorted((left, right) => {
|
|
110
|
-
const leftAt = Date.parse(left.merged_at ?? "");
|
|
111
|
-
const rightAt = Date.parse(right.merged_at ?? "");
|
|
112
|
-
return Number.isNaN(rightAt) || Number.isNaN(leftAt) ? 0 : rightAt - leftAt;
|
|
113
|
-
})[0] ?? null);
|
|
114
|
-
}
|
|
115
|
-
async function resolveHostedCloseMergeRecord(opts) {
|
|
116
|
-
const owner = opts.repo.split("/")[0]?.trim() ?? "";
|
|
117
|
-
if (!owner)
|
|
118
|
-
return null;
|
|
119
|
-
const query = new URLSearchParams({
|
|
120
|
-
state: "closed",
|
|
121
|
-
head: `${owner}:${opts.sourceBranch}`,
|
|
122
|
-
});
|
|
123
|
-
if (opts.baseBranch)
|
|
124
|
-
query.set("base", opts.baseBranch);
|
|
125
|
-
const records = await runGhApiJson(opts.gitRoot, [
|
|
126
|
-
`repos/${opts.repo}/pulls?${query.toString()}`,
|
|
127
|
-
]);
|
|
128
|
-
return selectMergedPullRecord({
|
|
129
|
-
pulls: Array.isArray(records) ? records : [],
|
|
130
|
-
sourceBranch: opts.sourceBranch,
|
|
131
|
-
baseBranch: opts.baseBranch,
|
|
132
|
-
prNumber: opts.prNumber,
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
async function resolveHostedCloseMergeRecordByCommit(opts) {
|
|
136
|
-
const records = await runGhApiJson(opts.gitRoot, [
|
|
137
|
-
`repos/${opts.repo}/commits/${opts.mergeCommit}/pulls`,
|
|
138
|
-
]);
|
|
139
|
-
return selectMergedPullRecord({
|
|
140
|
-
pulls: Array.isArray(records) ? records : [],
|
|
141
|
-
sourceBranch: opts.sourceBranch,
|
|
142
|
-
baseBranch: opts.baseBranch,
|
|
143
|
-
prNumber: opts.prNumber,
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
async function readHostedCloseState(opts) {
|
|
147
|
-
const task = (await opts.ctx.taskBackend.getTask(opts.taskId)) ??
|
|
148
|
-
(await loadTaskFromContext({
|
|
149
|
-
ctx: opts.ctx,
|
|
150
|
-
taskId: opts.taskId,
|
|
151
|
-
preferBranchSnapshot: false,
|
|
152
|
-
}));
|
|
153
|
-
const taskBranch = await resolveTaskBranchFromContext({ ctx: opts.ctx, taskId: opts.taskId });
|
|
154
|
-
const { metaPath, config } = await resolvePrPaths({
|
|
155
|
-
ctx: opts.ctx,
|
|
156
|
-
cwd: opts.cwd,
|
|
157
|
-
rootOverride: opts.rootOverride,
|
|
158
|
-
taskId: opts.taskId,
|
|
159
|
-
});
|
|
160
|
-
if (config.workflow_mode !== "branch_pr") {
|
|
161
|
-
throw new CliError({
|
|
162
|
-
exitCode: exitCodeForError("E_USAGE"),
|
|
163
|
-
code: "E_USAGE",
|
|
164
|
-
message: `Invalid workflow_mode: ${config.workflow_mode} (expected branch_pr)`,
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
if (!(await fileExists(metaPath))) {
|
|
168
|
-
return { meta: null, task, taskBranch };
|
|
169
|
-
}
|
|
170
|
-
const meta = parsePrMeta(await readFile(metaPath, "utf8"), opts.taskId);
|
|
171
|
-
return { meta, task, taskBranch };
|
|
172
|
-
}
|
|
173
|
-
async function listRemoteTaskCloseBranches(opts) {
|
|
174
|
-
const { stdout } = await execFileAsync("git", ["ls-remote", "--heads", "origin", `task-close/${opts.taskId}/*`], {
|
|
175
|
-
cwd: opts.gitRoot,
|
|
176
|
-
env: gitEnv(),
|
|
177
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
178
|
-
});
|
|
179
|
-
return stdout
|
|
180
|
-
.split("\n")
|
|
181
|
-
.map((line) => line.trim())
|
|
182
|
-
.filter((line) => line.length > 0)
|
|
183
|
-
.map((line) => line.split(/\s+/, 2)[1] ?? "")
|
|
184
|
-
.map((ref) => ref.replace(/^refs\/heads\//, ""))
|
|
185
|
-
.filter((ref) => ref.length > 0);
|
|
186
|
-
}
|
|
187
|
-
function shortSha(value) {
|
|
188
|
-
return value.trim().slice(0, 12);
|
|
189
|
-
}
|
|
190
|
-
function stripBranchRef(branch) {
|
|
191
|
-
return branch.startsWith("refs/heads/") ? branch.slice("refs/heads/".length) : branch;
|
|
192
|
-
}
|
|
193
|
-
async function resolveHostedCloseBranch(opts) {
|
|
194
|
-
const remoteBranches = await listRemoteTaskCloseBranches({
|
|
195
|
-
gitRoot: opts.gitRoot,
|
|
196
|
-
taskId: opts.taskId,
|
|
197
|
-
});
|
|
198
|
-
const explicitBranch = stripBranchRef(opts.explicitBranch?.trim() ?? "");
|
|
199
|
-
if (explicitBranch) {
|
|
200
|
-
const parsedTaskId = parseTaskIdFromCloseBranch(explicitBranch);
|
|
201
|
-
if (parsedTaskId && parsedTaskId !== opts.taskId) {
|
|
202
|
-
throw new CliError({
|
|
203
|
-
exitCode: exitCodeForError("E_VALIDATION"),
|
|
204
|
-
code: "E_VALIDATION",
|
|
205
|
-
message: `Branch ${explicitBranch} does not belong to task ${opts.taskId}.`,
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
if (!remoteBranches.includes(explicitBranch)) {
|
|
209
|
-
throw new CliError({
|
|
210
|
-
exitCode: exitCodeForError("E_VALIDATION"),
|
|
211
|
-
code: "E_VALIDATION",
|
|
212
|
-
message: `Remote hosted closure branch not found: ${explicitBranch}.`,
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
return explicitBranch;
|
|
216
|
-
}
|
|
217
|
-
if (opts.mergeCommit) {
|
|
218
|
-
const expected = `task-close/${opts.taskId}/${shortSha(opts.mergeCommit)}`;
|
|
219
|
-
if (remoteBranches.includes(expected))
|
|
220
|
-
return expected;
|
|
221
|
-
if (remoteBranches.length === 1)
|
|
222
|
-
return remoteBranches[0] ?? expected;
|
|
223
|
-
if (remoteBranches.length > 1) {
|
|
224
|
-
throw new CliError({
|
|
225
|
-
exitCode: exitCodeForError("E_VALIDATION"),
|
|
226
|
-
code: "E_VALIDATION",
|
|
227
|
-
message: `Multiple remote hosted closure branches match ${opts.taskId}: ${remoteBranches.join(", ")} (use --branch).`,
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
throw new CliError({
|
|
231
|
-
exitCode: exitCodeForError("E_VALIDATION"),
|
|
232
|
-
code: "E_VALIDATION",
|
|
233
|
-
message: `Remote hosted closure branch not found for ${opts.taskId}: ${expected}.`,
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
if (remoteBranches.length === 1)
|
|
237
|
-
return remoteBranches[0] ?? "";
|
|
238
|
-
if (remoteBranches.length > 1) {
|
|
239
|
-
throw new CliError({
|
|
240
|
-
exitCode: exitCodeForError("E_VALIDATION"),
|
|
241
|
-
code: "E_VALIDATION",
|
|
242
|
-
message: `Multiple remote hosted closure branches match ${opts.taskId}: ${remoteBranches.join(", ")} (use --branch).`,
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
throw new CliError({
|
|
246
|
-
exitCode: exitCodeForError("E_VALIDATION"),
|
|
247
|
-
code: "E_VALIDATION",
|
|
248
|
-
message: `Could not resolve remote hosted closure branch for ${opts.taskId}.`,
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
function buildHostedClosePrTitle(taskId) {
|
|
252
|
-
return `📝 ${taskId} task: close after hosted merge`;
|
|
253
|
-
}
|
|
254
|
-
function buildHostedClosePrBody(opts) {
|
|
255
|
-
const prLine = typeof opts.prNumber === "number" && opts.prNumber > 0
|
|
256
|
-
? `Automated closure for merged task PR #${opts.prNumber}.`
|
|
257
|
-
: "Automated closure for merged task PR.";
|
|
258
|
-
return [
|
|
259
|
-
prLine,
|
|
260
|
-
"",
|
|
261
|
-
`- task_id: \`${opts.taskId}\``,
|
|
262
|
-
`- source_branch: \`${opts.sourceBranch}\``,
|
|
263
|
-
`- merge_sha: \`${opts.mergeSha}\``,
|
|
264
|
-
"",
|
|
265
|
-
"This PR contains only tracked task artifacts produced by the hosted branch_pr closure flow.",
|
|
266
|
-
].join("\n");
|
|
267
|
-
}
|
|
268
|
-
function normalizeGithubPrLink(prNumber, prUrl, verb) {
|
|
269
|
-
return prUrl?.trim()
|
|
270
|
-
? `${verb} GitHub PR #${prNumber}: ${prUrl.trim()}`
|
|
271
|
-
: `${verb} GitHub PR #${prNumber}`;
|
|
272
|
-
}
|
|
273
|
-
async function maybeCleanupMergedTaskBranch(opts) {
|
|
274
|
-
try {
|
|
275
|
-
const cleanup = await cleanupMergedLocalBranch({
|
|
276
|
-
gitRoot: opts.gitRoot,
|
|
277
|
-
branch: opts.branch,
|
|
278
|
-
});
|
|
279
|
-
if (!cleanup.removedBranch && !cleanup.removedWorktree) {
|
|
280
|
-
if (cleanup.skippedReason === "current_worktree") {
|
|
281
|
-
opts.output.info(`local merged branch cleanup skipped: ${opts.branch} is the current checkout`);
|
|
282
|
-
}
|
|
283
|
-
else if (cleanup.skippedReason === "outside_repo") {
|
|
284
|
-
opts.output.info(`local merged branch cleanup skipped: ${opts.branch} is attached to a worktree outside the current checkout root`);
|
|
285
|
-
}
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
const details = [];
|
|
289
|
-
if (cleanup.removedWorktree && cleanup.worktreePath) {
|
|
290
|
-
details.push(`removed worktree ${cleanup.worktreePath}`);
|
|
291
|
-
}
|
|
292
|
-
if (cleanup.removedBranch) {
|
|
293
|
-
details.push(`deleted branch ${opts.branch}`);
|
|
294
|
-
}
|
|
295
|
-
opts.output.info(`local merged branch cleanup: ${details.join("; ")}`);
|
|
296
|
-
}
|
|
297
|
-
catch (error) {
|
|
298
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
299
|
-
opts.output.warn(`local merged branch cleanup failed for ${opts.branch}: ${message}`);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
75
|
async function openHostedClosePr(opts) {
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
cwd: opts.cwd,
|
|
307
|
-
rootOverride: opts.rootOverride,
|
|
308
|
-
taskId: opts.taskId,
|
|
309
|
-
});
|
|
310
|
-
const gitRoot = opts.ctx.resolvedProject.gitRoot;
|
|
311
|
-
const repo = await resolveGithubRepo({ gitRoot, repoOverride: opts.repo ?? null });
|
|
312
|
-
const defaultBaseBranch = await resolveBaseBranch({
|
|
313
|
-
cwd: opts.cwd,
|
|
314
|
-
rootOverride: opts.rootOverride ?? null,
|
|
315
|
-
cliBaseOpt: null,
|
|
316
|
-
mode: opts.ctx.config.workflow_mode,
|
|
317
|
-
});
|
|
318
|
-
let sourceBranch = meta?.branch?.trim() ?? taskBranch?.trim() ?? "";
|
|
319
|
-
let baseBranch = meta?.base?.trim() ?? defaultBaseBranch?.trim() ?? "";
|
|
320
|
-
const localMergeCommit = meta?.merge_commit?.trim() ?? task.commit?.hash?.trim() ?? "";
|
|
321
|
-
const canonicalCloseAlreadyPresent = meta?.status === "MERGED" &&
|
|
322
|
-
sourceBranch.length > 0 &&
|
|
323
|
-
localMergeCommit.length > 0 &&
|
|
324
|
-
String(task.status || "TODO").toUpperCase() === "DONE" &&
|
|
325
|
-
(task.commit?.hash?.trim() ?? "") === localMergeCommit;
|
|
326
|
-
if (canonicalCloseAlreadyPresent) {
|
|
327
|
-
output.info(`hosted-close-pr skipped: ${opts.taskId} is already closed on ${baseBranch || "the base branch"} for merge ${shortSha(localMergeCommit)}`);
|
|
76
|
+
const precheck = await precheckHostedClosePr(opts);
|
|
77
|
+
if (precheck.kind === "skip") {
|
|
78
|
+
reportHostedClosePrOutcome(precheck.outcome);
|
|
328
79
|
return 0;
|
|
329
80
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
gitRoot,
|
|
333
|
-
repo,
|
|
334
|
-
sourceBranch,
|
|
335
|
-
baseBranch: baseBranch || null,
|
|
336
|
-
prNumber: typeof meta?.pr_number === "number" ? meta.pr_number : null,
|
|
337
|
-
})
|
|
338
|
-
: null;
|
|
339
|
-
if (!mergedRecord && localMergeCommit) {
|
|
340
|
-
mergedRecord = await resolveHostedCloseMergeRecordByCommit({
|
|
341
|
-
gitRoot,
|
|
342
|
-
repo,
|
|
343
|
-
mergeCommit: localMergeCommit,
|
|
344
|
-
sourceBranch: sourceBranch || null,
|
|
345
|
-
baseBranch: baseBranch || null,
|
|
346
|
-
prNumber: typeof meta?.pr_number === "number" ? meta.pr_number : null,
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
if (!sourceBranch) {
|
|
350
|
-
sourceBranch = mergedRecord?.head?.ref?.trim() ?? "";
|
|
351
|
-
}
|
|
352
|
-
if (!baseBranch) {
|
|
353
|
-
baseBranch = mergedRecord?.base?.ref?.trim() ?? defaultBaseBranch?.trim() ?? "";
|
|
354
|
-
}
|
|
355
|
-
const mergeCommit = meta?.merge_commit?.trim() ??
|
|
356
|
-
task.commit?.hash?.trim() ??
|
|
357
|
-
mergedRecord?.merge_commit_sha?.trim() ??
|
|
358
|
-
"";
|
|
359
|
-
if (!sourceBranch) {
|
|
360
|
-
throw new CliError({
|
|
361
|
-
exitCode: exitCodeForError("E_VALIDATION"),
|
|
362
|
-
code: "E_VALIDATION",
|
|
363
|
-
message: `Missing hosted close source branch for ${opts.taskId}.`,
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
if (!mergeCommit) {
|
|
367
|
-
throw new CliError({
|
|
368
|
-
exitCode: exitCodeForError("E_VALIDATION"),
|
|
369
|
-
code: "E_VALIDATION",
|
|
370
|
-
message: `Missing hosted close merge commit for ${opts.taskId}.`,
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
if (meta?.status !== "MERGED" && !mergedRecord?.merged_at) {
|
|
374
|
-
throw new CliError({
|
|
375
|
-
exitCode: exitCodeForError("E_USAGE"),
|
|
376
|
-
code: "E_USAGE",
|
|
377
|
-
message: `Task ${opts.taskId} is not in MERGED hosted-close state.`,
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
const branch = await resolveHostedCloseBranch({
|
|
381
|
-
gitRoot,
|
|
382
|
-
taskId: opts.taskId,
|
|
383
|
-
explicitBranch: opts.branch ?? null,
|
|
384
|
-
mergeCommit,
|
|
385
|
-
});
|
|
386
|
-
await maybeCleanupMergedTaskBranch({
|
|
387
|
-
output,
|
|
388
|
-
gitRoot,
|
|
389
|
-
branch: sourceBranch,
|
|
390
|
-
});
|
|
391
|
-
const alreadyClosedOnBase = await taskCloseAlreadyRecordedOnBase({
|
|
392
|
-
gitRoot,
|
|
393
|
-
workflowDir: opts.ctx.config.paths.workflow_dir,
|
|
394
|
-
taskId: opts.taskId,
|
|
395
|
-
baseBranch,
|
|
396
|
-
});
|
|
397
|
-
if (alreadyClosedOnBase) {
|
|
398
|
-
output.info(`hosted close already recorded on ${baseBranch}; no follow-up PR needed`);
|
|
399
|
-
output.success("task hosted-close-pr", opts.taskId, `hosted close already recorded on ${baseBranch}; skipped follow-up PR`);
|
|
400
|
-
return 0;
|
|
401
|
-
}
|
|
402
|
-
const owner = repo.split("/")[0]?.trim() ?? "";
|
|
403
|
-
const existingQuery = new URLSearchParams({
|
|
404
|
-
state: "open",
|
|
405
|
-
head: `${owner}:${branch}`,
|
|
406
|
-
});
|
|
407
|
-
const existing = await runGhApiJson(gitRoot, [
|
|
408
|
-
`repos/${repo}/pulls?${existingQuery.toString()}`,
|
|
409
|
-
]);
|
|
410
|
-
const existingPr = Array.isArray(existing) ? (existing[0] ?? null) : null;
|
|
411
|
-
const existingNumber = Number(existingPr?.number ?? 0);
|
|
412
|
-
if (Number.isInteger(existingNumber) && existingNumber > 0) {
|
|
413
|
-
output.success("task hosted-close-pr", opts.taskId, normalizeGithubPrLink(existingNumber, existingPr?.html_url ?? null, "linked to"));
|
|
414
|
-
return 0;
|
|
415
|
-
}
|
|
416
|
-
const created = await runGhApiJson(gitRoot, [
|
|
417
|
-
`repos/${repo}/pulls`,
|
|
418
|
-
"-X",
|
|
419
|
-
"POST",
|
|
420
|
-
"-f",
|
|
421
|
-
`title=${buildHostedClosePrTitle(opts.taskId)}`,
|
|
422
|
-
"-f",
|
|
423
|
-
`body=${buildHostedClosePrBody({
|
|
424
|
-
taskId: opts.taskId,
|
|
425
|
-
prNumber: typeof meta?.pr_number === "number"
|
|
426
|
-
? meta.pr_number
|
|
427
|
-
: typeof mergedRecord?.number === "number"
|
|
428
|
-
? mergedRecord.number
|
|
429
|
-
: null,
|
|
430
|
-
sourceBranch,
|
|
431
|
-
mergeSha: mergeCommit,
|
|
432
|
-
})}`,
|
|
433
|
-
"-f",
|
|
434
|
-
`head=${branch}`,
|
|
435
|
-
"-f",
|
|
436
|
-
`base=${baseBranch}`,
|
|
437
|
-
]);
|
|
438
|
-
const createdNumber = Number(created.number ?? 0);
|
|
439
|
-
if (!Number.isInteger(createdNumber) || createdNumber <= 0) {
|
|
440
|
-
throw new CliError({
|
|
441
|
-
exitCode: exitCodeForError("E_VALIDATION"),
|
|
442
|
-
code: "E_VALIDATION",
|
|
443
|
-
message: `GitHub did not return a valid PR number for hosted closure branch ${branch}.`,
|
|
444
|
-
});
|
|
445
|
-
}
|
|
446
|
-
output.success("task hosted-close-pr", opts.taskId, normalizeGithubPrLink(createdNumber, created.html_url ?? null, "created"));
|
|
81
|
+
const result = postcheckHostedClosePrResult(await executeHostedClosePrPlan(precheck.plan));
|
|
82
|
+
reportHostedClosePrExecutionResult(result);
|
|
447
83
|
return 0;
|
|
448
84
|
}
|
|
449
85
|
export function makeRunTaskHostedClosePrHandler(getCtx) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hosted-close-pr.execute.d.ts","sourceRoot":"","sources":["../../../src/commands/task/hosted-close-pr.execute.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAEV,4BAA4B,EAE5B,iBAAiB,EAClB,MAAM,4BAA4B,CAAC;AA0EpC,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,iBAAiB,GACtB,OAAO,CAAC,4BAA4B,CAAC,CAwEvC"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { runGhApiJson } from "../pr/internal/gh-api.js";
|
|
2
|
+
import { cleanupMergedLocalBranch } from "../shared/merged-branch-cleanup.js";
|
|
3
|
+
import { taskCloseAlreadyRecordedOnBase } from "./close-tail-state.js";
|
|
4
|
+
function buildHostedClosePrTitle(taskId) {
|
|
5
|
+
return `📝 ${taskId} task: close after hosted merge`;
|
|
6
|
+
}
|
|
7
|
+
function buildHostedClosePrBody(opts) {
|
|
8
|
+
const prLine = typeof opts.prNumber === "number" && opts.prNumber > 0
|
|
9
|
+
? `Automated closure for merged task PR #${opts.prNumber}.`
|
|
10
|
+
: "Automated closure for merged task PR.";
|
|
11
|
+
return [
|
|
12
|
+
prLine,
|
|
13
|
+
"",
|
|
14
|
+
`- task_id: \`${opts.taskId}\``,
|
|
15
|
+
`- source_branch: \`${opts.sourceBranch}\``,
|
|
16
|
+
`- merge_sha: \`${opts.mergeSha}\``,
|
|
17
|
+
"",
|
|
18
|
+
"This PR contains only tracked task artifacts produced by the hosted branch_pr closure flow.",
|
|
19
|
+
].join("\n");
|
|
20
|
+
}
|
|
21
|
+
async function maybeCleanupMergedTaskBranch(opts) {
|
|
22
|
+
try {
|
|
23
|
+
const cleanup = await cleanupMergedLocalBranch({
|
|
24
|
+
gitRoot: opts.gitRoot,
|
|
25
|
+
branch: opts.branch,
|
|
26
|
+
});
|
|
27
|
+
if (!cleanup.removedBranch && !cleanup.removedWorktree) {
|
|
28
|
+
if (cleanup.skippedReason === "current_worktree") {
|
|
29
|
+
return [
|
|
30
|
+
{
|
|
31
|
+
level: "info",
|
|
32
|
+
message: `local merged branch cleanup skipped: ${opts.branch} is the current checkout`,
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
if (cleanup.skippedReason === "outside_repo") {
|
|
37
|
+
return [
|
|
38
|
+
{
|
|
39
|
+
level: "info",
|
|
40
|
+
message: `local merged branch cleanup skipped: ${opts.branch} is attached to a worktree outside the current checkout root`,
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
const details = [];
|
|
47
|
+
if (cleanup.removedWorktree && cleanup.worktreePath) {
|
|
48
|
+
details.push(`removed worktree ${cleanup.worktreePath}`);
|
|
49
|
+
}
|
|
50
|
+
if (cleanup.removedBranch) {
|
|
51
|
+
details.push(`deleted branch ${opts.branch}`);
|
|
52
|
+
}
|
|
53
|
+
return [{ level: "info", message: `local merged branch cleanup: ${details.join("; ")}` }];
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
57
|
+
return [
|
|
58
|
+
{
|
|
59
|
+
level: "warn",
|
|
60
|
+
message: `local merged branch cleanup failed for ${opts.branch}: ${message}`,
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export async function executeHostedClosePrPlan(plan) {
|
|
66
|
+
const notices = await maybeCleanupMergedTaskBranch({
|
|
67
|
+
gitRoot: plan.gitRoot,
|
|
68
|
+
branch: plan.sourceBranch,
|
|
69
|
+
});
|
|
70
|
+
const alreadyClosedOnBase = await taskCloseAlreadyRecordedOnBase({
|
|
71
|
+
gitRoot: plan.gitRoot,
|
|
72
|
+
workflowDir: plan.workflowDir,
|
|
73
|
+
taskId: plan.taskId,
|
|
74
|
+
baseBranch: plan.baseBranch,
|
|
75
|
+
});
|
|
76
|
+
if (alreadyClosedOnBase) {
|
|
77
|
+
return {
|
|
78
|
+
notices,
|
|
79
|
+
outcome: {
|
|
80
|
+
kind: "base-already-recorded",
|
|
81
|
+
taskId: plan.taskId,
|
|
82
|
+
baseBranch: plan.baseBranch,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const owner = plan.repo.split("/")[0]?.trim() ?? "";
|
|
87
|
+
const existingQuery = new URLSearchParams({
|
|
88
|
+
state: "open",
|
|
89
|
+
head: `${owner}:${plan.closeBranch}`,
|
|
90
|
+
});
|
|
91
|
+
const existing = await runGhApiJson(plan.gitRoot, [
|
|
92
|
+
`repos/${plan.repo}/pulls?${existingQuery.toString()}`,
|
|
93
|
+
]);
|
|
94
|
+
const existingPr = Array.isArray(existing) ? (existing[0] ?? null) : null;
|
|
95
|
+
const existingNumber = Number(existingPr?.number ?? 0);
|
|
96
|
+
if (Number.isInteger(existingNumber) && existingNumber > 0) {
|
|
97
|
+
return {
|
|
98
|
+
notices,
|
|
99
|
+
outcome: {
|
|
100
|
+
kind: "existing-pr",
|
|
101
|
+
taskId: plan.taskId,
|
|
102
|
+
prNumber: existingNumber,
|
|
103
|
+
prUrl: existingPr?.html_url ?? null,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const created = await runGhApiJson(plan.gitRoot, [
|
|
108
|
+
`repos/${plan.repo}/pulls`,
|
|
109
|
+
"-X",
|
|
110
|
+
"POST",
|
|
111
|
+
"-f",
|
|
112
|
+
`title=${buildHostedClosePrTitle(plan.taskId)}`,
|
|
113
|
+
"-f",
|
|
114
|
+
`body=${buildHostedClosePrBody({
|
|
115
|
+
taskId: plan.taskId,
|
|
116
|
+
prNumber: plan.sourcePrNumber,
|
|
117
|
+
sourceBranch: plan.sourceBranch,
|
|
118
|
+
mergeSha: plan.mergeCommit,
|
|
119
|
+
})}`,
|
|
120
|
+
"-f",
|
|
121
|
+
`head=${plan.closeBranch}`,
|
|
122
|
+
"-f",
|
|
123
|
+
`base=${plan.baseBranch}`,
|
|
124
|
+
]);
|
|
125
|
+
return {
|
|
126
|
+
notices,
|
|
127
|
+
outcome: {
|
|
128
|
+
kind: "created-pr",
|
|
129
|
+
taskId: plan.taskId,
|
|
130
|
+
closeBranch: plan.closeBranch,
|
|
131
|
+
prNumber: Number(created.number ?? 0),
|
|
132
|
+
prUrl: created.html_url ?? null,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hosted-close-pr.postcheck.d.ts","sourceRoot":"","sources":["../../../src/commands/task/hosted-close-pr.postcheck.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,4BAA4B,CAAC;AAE/E,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,4BAA4B,GACnC,4BAA4B,CAY9B"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { exitCodeForError } from "../../cli/exit-codes.js";
|
|
2
|
+
import { CliError } from "../../shared/errors.js";
|
|
3
|
+
export function postcheckHostedClosePrResult(result) {
|
|
4
|
+
if (result.outcome.kind === "created-pr" &&
|
|
5
|
+
(!Number.isInteger(result.outcome.prNumber) || result.outcome.prNumber <= 0)) {
|
|
6
|
+
throw new CliError({
|
|
7
|
+
exitCode: exitCodeForError("E_VALIDATION"),
|
|
8
|
+
code: "E_VALIDATION",
|
|
9
|
+
message: `GitHub did not return a valid PR number for hosted closure branch ${result.outcome.closeBranch}.`,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { HostedClosePrPrecheckOptions, HostedClosePrPrecheckResult } from "./hosted-close-pr.types.js";
|
|
2
|
+
export declare function shortHostedCloseSha(value: string): string;
|
|
3
|
+
export declare function precheckHostedClosePr(opts: HostedClosePrPrecheckOptions): Promise<HostedClosePrPrecheckResult>;
|
|
4
|
+
//# sourceMappingURL=hosted-close-pr.precheck.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hosted-close-pr.precheck.d.ts","sourceRoot":"","sources":["../../../src/commands/task/hosted-close-pr.precheck.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAEV,4BAA4B,EAC5B,2BAA2B,EAC5B,MAAM,4BAA4B,CAAC;AAiJpC,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEzD;AAqED,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,4BAA4B,GACjC,OAAO,CAAC,2BAA2B,CAAC,CAoHtC"}
|