patchrelay 0.26.0 → 0.29.1
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/README.md +83 -31
- package/dist/agent-session-plan.js +0 -7
- package/dist/build-info.json +3 -3
- package/dist/cli/args.js +22 -18
- package/dist/cli/commands/feed.js +1 -1
- package/dist/cli/commands/issues.js +44 -4
- package/dist/cli/commands/linear.js +67 -0
- package/dist/cli/commands/repo.js +213 -0
- package/dist/cli/commands/setup.js +140 -21
- package/dist/cli/connect-flow.js +5 -3
- package/dist/cli/formatters/text.js +1 -1
- package/dist/cli/help.js +134 -63
- package/dist/cli/index.js +166 -188
- package/dist/cli/interactive.js +25 -0
- package/dist/cli/operator-client.js +11 -0
- package/dist/cli/service-commands.js +11 -4
- package/dist/cli/watch/App.js +1 -1
- package/dist/cli/watch/FactoryStateGraph.js +31 -0
- package/dist/cli/watch/FeedView.js +3 -2
- package/dist/cli/watch/FreshnessBadge.js +13 -0
- package/dist/cli/watch/IssueDetailView.js +9 -2
- package/dist/cli/watch/IssueListView.js +2 -2
- package/dist/cli/watch/IssueRow.js +9 -11
- package/dist/cli/watch/QueueObservationView.js +15 -0
- package/dist/cli/watch/StateHistoryView.js +0 -1
- package/dist/cli/watch/StatusBar.js +5 -2
- package/dist/cli/watch/format-utils.js +7 -0
- package/dist/cli/watch/freshness.js +30 -0
- package/dist/cli/watch/state-visualization.js +147 -0
- package/dist/cli/watch/theme.js +6 -7
- package/dist/cli/watch/use-watch-stream.js +5 -2
- package/dist/cli/watch/watch-state.js +9 -5
- package/dist/config.js +129 -36
- package/dist/db/linear-installation-store.js +23 -0
- package/dist/db/migrations.js +42 -0
- package/dist/db/repository-link-store.js +103 -0
- package/dist/db.js +61 -11
- package/dist/factory-state.js +1 -5
- package/dist/github-webhook-handler.js +115 -46
- package/dist/github-webhooks.js +4 -0
- package/dist/http.js +162 -0
- package/dist/install.js +93 -13
- package/dist/issue-query-service.js +34 -1
- package/dist/linear-client.js +80 -25
- package/dist/merge-queue-incident.js +104 -0
- package/dist/merge-queue-protocol.js +54 -0
- package/dist/preflight.js +28 -1
- package/dist/repository-linking.js +42 -0
- package/dist/run-orchestrator.js +197 -21
- package/dist/runtime-paths.js +0 -8
- package/dist/service.js +94 -49
- package/package.json +8 -7
- package/dist/cli/commands/connect.js +0 -54
- package/dist/cli/commands/project.js +0 -146
- package/dist/merge-queue.js +0 -200
- package/infra/patchrelay-reload.service +0 -6
- package/infra/patchrelay.path +0 -13
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import { loadConfig } from "../../config.js";
|
|
2
|
-
import { installServiceUnits, upsertProjectInConfig } from "../../install.js";
|
|
3
|
-
import { hasHelpFlag, parseCsvFlag } from "../args.js";
|
|
4
|
-
import { runConnectFlow, parseTimeoutSeconds } from "../connect-flow.js";
|
|
5
|
-
import { CliUsageError } from "../errors.js";
|
|
6
|
-
import { formatJson } from "../formatters/json.js";
|
|
7
|
-
import { projectHelpText } from "../help.js";
|
|
8
|
-
import { writeOutput } from "../output.js";
|
|
9
|
-
import { installServiceCommands, tryManageService } from "../service-commands.js";
|
|
10
|
-
export async function handleProjectCommand(params) {
|
|
11
|
-
if (hasHelpFlag(params.parsed)) {
|
|
12
|
-
writeOutput(params.stdout, `${projectHelpText()}\n`);
|
|
13
|
-
return 0;
|
|
14
|
-
}
|
|
15
|
-
if (params.commandArgs.length === 0) {
|
|
16
|
-
throw new CliUsageError("patchrelay project requires a subcommand.", "project");
|
|
17
|
-
}
|
|
18
|
-
const subcommand = params.commandArgs[0];
|
|
19
|
-
if (subcommand !== "apply") {
|
|
20
|
-
throw new CliUsageError(`Unknown project command: ${subcommand}`, "project");
|
|
21
|
-
}
|
|
22
|
-
const projectId = params.commandArgs[1];
|
|
23
|
-
const repoPath = params.commandArgs[2];
|
|
24
|
-
if (!projectId || !repoPath) {
|
|
25
|
-
throw new CliUsageError("patchrelay project apply requires <id> and <repo-path>.", "project");
|
|
26
|
-
}
|
|
27
|
-
const result = await upsertProjectInConfig({
|
|
28
|
-
id: projectId,
|
|
29
|
-
repoPath,
|
|
30
|
-
issueKeyPrefixes: parseCsvFlag(params.parsed.flags.get("issue-prefix")),
|
|
31
|
-
linearTeamIds: parseCsvFlag(params.parsed.flags.get("team-id")),
|
|
32
|
-
});
|
|
33
|
-
const serviceUnits = await installServiceUnits();
|
|
34
|
-
const noConnect = params.parsed.flags.get("no-connect") === true;
|
|
35
|
-
const lines = [
|
|
36
|
-
`Config file: ${result.configPath}`,
|
|
37
|
-
`${result.status === "created" ? "Created" : result.status === "updated" ? "Updated" : "Verified"} project ${result.project.id} for ${result.project.repoPath}`,
|
|
38
|
-
result.project.issueKeyPrefixes.length > 0 ? `Issue key prefixes: ${result.project.issueKeyPrefixes.join(", ")}` : undefined,
|
|
39
|
-
result.project.linearTeamIds.length > 0 ? `Linear team ids: ${result.project.linearTeamIds.join(", ")}` : undefined,
|
|
40
|
-
`Service unit: ${serviceUnits.unitPath} (${serviceUnits.serviceStatus})`,
|
|
41
|
-
`Watcher unit: ${serviceUnits.pathUnitPath} (${serviceUnits.pathStatus})`,
|
|
42
|
-
].filter(Boolean);
|
|
43
|
-
let fullConfig;
|
|
44
|
-
try {
|
|
45
|
-
fullConfig = loadConfig(undefined, { profile: "doctor" });
|
|
46
|
-
}
|
|
47
|
-
catch (error) {
|
|
48
|
-
if (params.json) {
|
|
49
|
-
writeOutput(params.stdout, formatJson({
|
|
50
|
-
...result,
|
|
51
|
-
serviceUnits,
|
|
52
|
-
readiness: {
|
|
53
|
-
ok: false,
|
|
54
|
-
error: error instanceof Error ? error.message : String(error),
|
|
55
|
-
},
|
|
56
|
-
connect: {
|
|
57
|
-
attempted: false,
|
|
58
|
-
skipped: "missing_env",
|
|
59
|
-
},
|
|
60
|
-
}));
|
|
61
|
-
return 0;
|
|
62
|
-
}
|
|
63
|
-
lines.push(`Linear connect was skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
64
|
-
lines.push("Finish the required env vars and rerun `patchrelay project apply`.");
|
|
65
|
-
writeOutput(params.stdout, `${lines.join("\n")}\n`);
|
|
66
|
-
return 0;
|
|
67
|
-
}
|
|
68
|
-
const { runPreflight } = await import("../../preflight.js");
|
|
69
|
-
const report = await runPreflight(fullConfig, { skipServiceCheck: true });
|
|
70
|
-
const failedChecks = report.checks.filter((check) => check.status === "fail");
|
|
71
|
-
if (failedChecks.length > 0) {
|
|
72
|
-
if (params.json) {
|
|
73
|
-
writeOutput(params.stdout, formatJson({
|
|
74
|
-
...result,
|
|
75
|
-
serviceUnits,
|
|
76
|
-
readiness: report,
|
|
77
|
-
connect: {
|
|
78
|
-
attempted: false,
|
|
79
|
-
skipped: "preflight_failed",
|
|
80
|
-
},
|
|
81
|
-
}));
|
|
82
|
-
return 0;
|
|
83
|
-
}
|
|
84
|
-
lines.push("Linear connect was skipped because PatchRelay is not ready yet:");
|
|
85
|
-
lines.push(...failedChecks.map((check) => `- [${check.scope}] ${check.message}`));
|
|
86
|
-
lines.push("Fix the failures above and rerun `patchrelay project apply`.");
|
|
87
|
-
writeOutput(params.stdout, `${lines.join("\n")}\n`);
|
|
88
|
-
return 0;
|
|
89
|
-
}
|
|
90
|
-
const serviceState = await tryManageService(params.runInteractive, installServiceCommands());
|
|
91
|
-
if (!serviceState.ok) {
|
|
92
|
-
throw new Error(`Project was saved, but PatchRelay could not be reloaded: ${serviceState.error}`);
|
|
93
|
-
}
|
|
94
|
-
const cliData = params.options?.data ?? (await createCliOperatorDataAccess(fullConfig));
|
|
95
|
-
try {
|
|
96
|
-
if (params.json) {
|
|
97
|
-
const connectResult = noConnect ? undefined : await cliData.connect(projectId);
|
|
98
|
-
writeOutput(params.stdout, formatJson({
|
|
99
|
-
...result,
|
|
100
|
-
serviceUnits,
|
|
101
|
-
readiness: report,
|
|
102
|
-
serviceReloaded: true,
|
|
103
|
-
...(noConnect
|
|
104
|
-
? {
|
|
105
|
-
connect: {
|
|
106
|
-
attempted: false,
|
|
107
|
-
skipped: "no_connect",
|
|
108
|
-
},
|
|
109
|
-
}
|
|
110
|
-
: {
|
|
111
|
-
connect: {
|
|
112
|
-
attempted: true,
|
|
113
|
-
result: connectResult,
|
|
114
|
-
},
|
|
115
|
-
}),
|
|
116
|
-
}));
|
|
117
|
-
return 0;
|
|
118
|
-
}
|
|
119
|
-
if (noConnect) {
|
|
120
|
-
lines.push("Project saved and PatchRelay was reloaded.");
|
|
121
|
-
lines.push(`Next: patchrelay connect --project ${result.project.id}`);
|
|
122
|
-
writeOutput(params.stdout, `${lines.join("\n")}\n`);
|
|
123
|
-
return 0;
|
|
124
|
-
}
|
|
125
|
-
writeOutput(params.stdout, `${lines.join("\n")}\n`);
|
|
126
|
-
return await runConnectFlow({
|
|
127
|
-
config: fullConfig,
|
|
128
|
-
data: cliData,
|
|
129
|
-
stdout: params.stdout,
|
|
130
|
-
noOpen: params.parsed.flags.get("no-open") === true,
|
|
131
|
-
timeoutSeconds: parseTimeoutSeconds(params.parsed.flags.get("timeout"), "project apply"),
|
|
132
|
-
projectId,
|
|
133
|
-
...(params.options?.openExternal ? { openExternal: params.options.openExternal } : {}),
|
|
134
|
-
...(params.options?.connectPollIntervalMs !== undefined ? { connectPollIntervalMs: params.options.connectPollIntervalMs } : {}),
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
finally {
|
|
138
|
-
if (!params.options?.data) {
|
|
139
|
-
cliData.close();
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
async function createCliOperatorDataAccess(config) {
|
|
144
|
-
const { CliOperatorApiClient } = await import("../operator-client.js");
|
|
145
|
-
return new CliOperatorApiClient(config);
|
|
146
|
-
}
|
package/dist/merge-queue.js
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
import { buildMergePrepActivity, buildMergePrepEscalationActivity } from "./linear-session-reporting.js";
|
|
2
|
-
import { execCommand } from "./utils.js";
|
|
3
|
-
const DEFAULT_MERGE_PREP_BUDGET = 3;
|
|
4
|
-
/**
|
|
5
|
-
* Merge queue steward — keeps PatchRelay-managed PR branches up to date
|
|
6
|
-
* with the base branch via rebase and enables auto-merge so GitHub merges
|
|
7
|
-
* when CI passes. Uses rebase (not merge) to maintain linear history.
|
|
8
|
-
*
|
|
9
|
-
* Serialization: all calls are routed through the issue queue, and
|
|
10
|
-
* prepareForMerge checks front-of-queue before acting. The issue processor
|
|
11
|
-
* in service.ts checks pendingRunType before pendingMergePrep, so repair
|
|
12
|
-
* runs always take priority over merge prep.
|
|
13
|
-
*/
|
|
14
|
-
export class MergeQueue {
|
|
15
|
-
config;
|
|
16
|
-
db;
|
|
17
|
-
enqueueIssue;
|
|
18
|
-
logger;
|
|
19
|
-
feed;
|
|
20
|
-
onLinearActivity;
|
|
21
|
-
constructor(config, db, enqueueIssue, logger, feed, onLinearActivity) {
|
|
22
|
-
this.config = config;
|
|
23
|
-
this.db = db;
|
|
24
|
-
this.enqueueIssue = enqueueIssue;
|
|
25
|
-
this.logger = logger;
|
|
26
|
-
this.feed = feed;
|
|
27
|
-
this.onLinearActivity = onLinearActivity;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Prepare the front-of-queue issue for merge:
|
|
31
|
-
* 1. Enable auto-merge
|
|
32
|
-
* 2. Rebase the branch onto latest base
|
|
33
|
-
* 3. Force-push (triggers CI; auto-merge fires when CI passes)
|
|
34
|
-
*
|
|
35
|
-
* On conflict: abort rebase, transition to repairing_queue, enqueue queue_repair.
|
|
36
|
-
* On transient failure: leave pendingMergePrep set so the next event retries.
|
|
37
|
-
*/
|
|
38
|
-
async prepareForMerge(issue, project) {
|
|
39
|
-
// Only prepare the front-of-queue issue for this project
|
|
40
|
-
const queue = this.db.listIssuesByState(project.id, "awaiting_queue");
|
|
41
|
-
const front = queue.find((i) => i.activeRunId === undefined && i.pendingRunType === undefined);
|
|
42
|
-
if (!front || front.id !== issue.id) {
|
|
43
|
-
this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, pendingMergePrep: false });
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
if (!issue.worktreePath || !issue.prNumber) {
|
|
47
|
-
this.logger.warn({ issueKey: issue.issueKey }, "Merge prep skipped: missing worktree or PR number");
|
|
48
|
-
this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, pendingMergePrep: false });
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
// Retry budget — escalate after repeated infrastructure failures
|
|
52
|
-
const attempts = issue.mergePrepAttempts + 1;
|
|
53
|
-
this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, mergePrepAttempts: attempts });
|
|
54
|
-
if (attempts > DEFAULT_MERGE_PREP_BUDGET) {
|
|
55
|
-
this.logger.warn({ issueKey: issue.issueKey, attempts }, "Merge prep budget exhausted, escalating");
|
|
56
|
-
this.db.upsertIssue({
|
|
57
|
-
projectId: issue.projectId,
|
|
58
|
-
linearIssueId: issue.linearIssueId,
|
|
59
|
-
factoryState: "escalated",
|
|
60
|
-
pendingMergePrep: false,
|
|
61
|
-
});
|
|
62
|
-
this.feed?.publish({
|
|
63
|
-
level: "error",
|
|
64
|
-
kind: "workflow",
|
|
65
|
-
issueKey: issue.issueKey,
|
|
66
|
-
projectId: issue.projectId,
|
|
67
|
-
stage: "awaiting_queue",
|
|
68
|
-
status: "escalated",
|
|
69
|
-
summary: `Merge prep failed ${attempts - 1} times — escalating for human help`,
|
|
70
|
-
});
|
|
71
|
-
this.onLinearActivity?.(issue, buildMergePrepEscalationActivity(attempts - 1));
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
const repoFullName = project.github?.repoFullName;
|
|
75
|
-
const baseBranch = project.github?.baseBranch ?? "main";
|
|
76
|
-
const gitBin = this.config.runner.gitBin;
|
|
77
|
-
// Enable auto-merge (idempotent)
|
|
78
|
-
const autoMergeOk = repoFullName ? await this.enableAutoMerge(issue, repoFullName) : false;
|
|
79
|
-
if (autoMergeOk) {
|
|
80
|
-
this.onLinearActivity?.(issue, buildMergePrepActivity("auto_merge"), { ephemeral: true });
|
|
81
|
-
}
|
|
82
|
-
// Fetch latest base branch
|
|
83
|
-
const fetchResult = await execCommand(gitBin, ["-C", issue.worktreePath, "fetch", "origin", baseBranch], {
|
|
84
|
-
timeoutMs: 60_000,
|
|
85
|
-
});
|
|
86
|
-
if (fetchResult.exitCode !== 0) {
|
|
87
|
-
// Transient failure — leave pendingMergePrep set so the next event retries.
|
|
88
|
-
this.logger.warn({ issueKey: issue.issueKey, stderr: fetchResult.stderr?.slice(0, 300) }, "Merge prep: fetch failed, will retry on next event");
|
|
89
|
-
this.onLinearActivity?.(issue, buildMergePrepActivity("fetch_retry"), { ephemeral: true });
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
// Check if rebase is needed: is HEAD already on top of origin/baseBranch?
|
|
93
|
-
const mergeBaseResult = await execCommand(gitBin, ["-C", issue.worktreePath, "merge-base", "--is-ancestor", `origin/${baseBranch}`, "HEAD"], {
|
|
94
|
-
timeoutMs: 10_000,
|
|
95
|
-
});
|
|
96
|
-
if (mergeBaseResult.exitCode === 0) {
|
|
97
|
-
this.logger.debug({ issueKey: issue.issueKey }, "Merge prep: branch already up to date");
|
|
98
|
-
this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, pendingMergePrep: false, mergePrepAttempts: 0 });
|
|
99
|
-
if (!autoMergeOk) {
|
|
100
|
-
this.feed?.publish({
|
|
101
|
-
level: "warn",
|
|
102
|
-
kind: "workflow",
|
|
103
|
-
issueKey: issue.issueKey,
|
|
104
|
-
projectId: issue.projectId,
|
|
105
|
-
stage: "awaiting_queue",
|
|
106
|
-
status: "blocked",
|
|
107
|
-
summary: "Branch up to date but auto-merge not enabled — check gh auth and repo settings",
|
|
108
|
-
});
|
|
109
|
-
this.onLinearActivity?.(issue, buildMergePrepActivity("blocked"));
|
|
110
|
-
}
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
// Rebase onto latest base branch (clean linear history, no merge commits)
|
|
114
|
-
const rebaseResult = await execCommand(gitBin, ["-C", issue.worktreePath, "rebase", `origin/${baseBranch}`], {
|
|
115
|
-
timeoutMs: 120_000,
|
|
116
|
-
});
|
|
117
|
-
if (rebaseResult.exitCode !== 0) {
|
|
118
|
-
// Conflict — abort and trigger queue_repair
|
|
119
|
-
await execCommand(gitBin, ["-C", issue.worktreePath, "rebase", "--abort"], { timeoutMs: 10_000 });
|
|
120
|
-
this.logger.info({ issueKey: issue.issueKey }, "Merge prep: rebase conflict detected, triggering queue repair");
|
|
121
|
-
this.db.upsertIssue({
|
|
122
|
-
projectId: issue.projectId,
|
|
123
|
-
linearIssueId: issue.linearIssueId,
|
|
124
|
-
factoryState: "repairing_queue",
|
|
125
|
-
pendingRunType: "queue_repair",
|
|
126
|
-
pendingRunContextJson: JSON.stringify({ failureReason: "rebase_conflict" }),
|
|
127
|
-
pendingMergePrep: false,
|
|
128
|
-
});
|
|
129
|
-
this.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
130
|
-
this.feed?.publish({
|
|
131
|
-
level: "warn",
|
|
132
|
-
kind: "workflow",
|
|
133
|
-
issueKey: issue.issueKey,
|
|
134
|
-
projectId: issue.projectId,
|
|
135
|
-
stage: "repairing_queue",
|
|
136
|
-
status: "conflict",
|
|
137
|
-
summary: `Rebase conflict with ${baseBranch} — queue repair enqueued`,
|
|
138
|
-
});
|
|
139
|
-
this.onLinearActivity?.(issue, buildMergePrepActivity("conflict"));
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
// Push the rebased branch (force-with-lease to protect against concurrent changes)
|
|
143
|
-
const pushResult = await execCommand(gitBin, ["-C", issue.worktreePath, "push", "--force-with-lease"], {
|
|
144
|
-
timeoutMs: 60_000,
|
|
145
|
-
});
|
|
146
|
-
if (pushResult.exitCode !== 0) {
|
|
147
|
-
// Push failed — leave pendingMergePrep set so the next event retries.
|
|
148
|
-
this.logger.warn({ issueKey: issue.issueKey, stderr: pushResult.stderr?.slice(0, 300) }, "Merge prep: push failed, will retry on next event");
|
|
149
|
-
this.onLinearActivity?.(issue, buildMergePrepActivity("push_retry"), { ephemeral: true });
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
this.logger.info({ issueKey: issue.issueKey, baseBranch }, "Merge prep: rebased and pushed");
|
|
153
|
-
this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, pendingMergePrep: false, mergePrepAttempts: 0 });
|
|
154
|
-
this.feed?.publish({
|
|
155
|
-
level: "info",
|
|
156
|
-
kind: "workflow",
|
|
157
|
-
issueKey: issue.issueKey,
|
|
158
|
-
projectId: issue.projectId,
|
|
159
|
-
stage: "awaiting_queue",
|
|
160
|
-
status: "prepared",
|
|
161
|
-
summary: `Branch rebased onto latest ${baseBranch} — CI will run`,
|
|
162
|
-
});
|
|
163
|
-
this.onLinearActivity?.(issue, buildMergePrepActivity("branch_update", baseBranch), { ephemeral: true });
|
|
164
|
-
}
|
|
165
|
-
/**
|
|
166
|
-
* Seed the merge queue on startup: for each project, ensure the front-of-queue
|
|
167
|
-
* issue has pendingMergePrep set. Catches issues that entered awaiting_queue
|
|
168
|
-
* but whose merge prep was never triggered or was lost to a crash/restart.
|
|
169
|
-
*/
|
|
170
|
-
seedOnStartup() {
|
|
171
|
-
for (const project of this.config.projects) {
|
|
172
|
-
this.advanceQueue(project.id);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
/**
|
|
176
|
-
* Advance the queue: find the next awaiting_queue issue and prepare it.
|
|
177
|
-
* Called when a PR merges (pr_merged event) and on startup.
|
|
178
|
-
*/
|
|
179
|
-
advanceQueue(projectId) {
|
|
180
|
-
const queue = this.db.listIssuesByState(projectId, "awaiting_queue");
|
|
181
|
-
const next = queue.find((i) => i.activeRunId === undefined && i.pendingRunType === undefined && !i.pendingMergePrep);
|
|
182
|
-
if (!next)
|
|
183
|
-
return;
|
|
184
|
-
this.logger.info({ issueKey: next.issueKey, projectId }, "Advancing merge queue");
|
|
185
|
-
this.db.upsertIssue({ projectId: next.projectId, linearIssueId: next.linearIssueId, pendingMergePrep: true });
|
|
186
|
-
this.enqueueIssue(next.projectId, next.linearIssueId);
|
|
187
|
-
}
|
|
188
|
-
/** Returns true if auto-merge was successfully enabled (or already enabled). */
|
|
189
|
-
async enableAutoMerge(issue, repoFullName) {
|
|
190
|
-
// Uses the host's existing gh auth — same credentials Codex uses to create PRs.
|
|
191
|
-
const result = await execCommand("gh", ["pr", "merge", String(issue.prNumber), "--repo", repoFullName, "--auto", "--squash"], {
|
|
192
|
-
timeoutMs: 30_000,
|
|
193
|
-
});
|
|
194
|
-
if (result.exitCode !== 0) {
|
|
195
|
-
this.logger.warn({ issueKey: issue.issueKey, stderr: result.stderr?.slice(0, 200) }, "Merge prep: auto-merge enablement failed");
|
|
196
|
-
return false;
|
|
197
|
-
}
|
|
198
|
-
return true;
|
|
199
|
-
}
|
|
200
|
-
}
|
package/infra/patchrelay.path
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
[Unit]
|
|
2
|
-
Description=Watch PatchRelay config and env changes
|
|
3
|
-
|
|
4
|
-
[Path]
|
|
5
|
-
Unit=patchrelay-reload.service
|
|
6
|
-
PathChanged=/home/your-user/.config/patchrelay/runtime.env
|
|
7
|
-
PathChanged=/home/your-user/.config/patchrelay/service.env
|
|
8
|
-
PathChanged=/home/your-user/.config/patchrelay/patchrelay.json
|
|
9
|
-
TriggerLimitIntervalSec=5
|
|
10
|
-
TriggerLimitBurst=1
|
|
11
|
-
|
|
12
|
-
[Install]
|
|
13
|
-
WantedBy=multi-user.target
|