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
package/dist/config.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { isIP } from "node:net";
|
|
3
|
+
import { homedir } from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { z } from "zod";
|
|
5
6
|
import { getDefaultConfigPath, getDefaultDatabasePath, getDefaultLogPath, getDefaultRuntimeEnvPath, getDefaultServiceEnvPath, getPatchRelayDataDir, } from "./runtime-paths.js";
|
|
@@ -40,6 +41,24 @@ const projectSchema = z.object({
|
|
|
40
41
|
base_branch: z.string().min(1).optional(),
|
|
41
42
|
}).optional(),
|
|
42
43
|
});
|
|
44
|
+
const repositorySchema = z.object({
|
|
45
|
+
github_repo: z.string().min(1),
|
|
46
|
+
local_path: z.string().min(1).optional(),
|
|
47
|
+
workspace: z.string().min(1).optional(),
|
|
48
|
+
linear_team_ids: z.array(z.string().min(1)).default([]),
|
|
49
|
+
linear_project_ids: z.array(z.string().min(1)).default([]),
|
|
50
|
+
issue_key_prefixes: z.array(z.string().min(1)).default([]),
|
|
51
|
+
review_checks: z.array(z.string().min(1)).default([]),
|
|
52
|
+
gate_checks: z.array(z.string().min(1)).default([]),
|
|
53
|
+
trigger_events: z.array(z.string().min(1)).min(1).optional(),
|
|
54
|
+
branch_prefix: z.string().min(1).optional(),
|
|
55
|
+
github: z.object({
|
|
56
|
+
webhook_secret: z.string().min(1).optional(),
|
|
57
|
+
base_branch: z.string().min(1).optional(),
|
|
58
|
+
merge_queue_label: z.string().min(1).optional(),
|
|
59
|
+
merge_queue_check_name: z.string().min(1).optional(),
|
|
60
|
+
}).optional(),
|
|
61
|
+
});
|
|
43
62
|
const configSchema = z.object({
|
|
44
63
|
server: z.object({
|
|
45
64
|
bind: z.string().default("127.0.0.1"),
|
|
@@ -102,7 +121,11 @@ const configSchema = z.object({
|
|
|
102
121
|
experimental_raw_events: z.boolean().default(true),
|
|
103
122
|
}),
|
|
104
123
|
}),
|
|
124
|
+
repos: z.object({
|
|
125
|
+
root: z.string().min(1).default(path.join(homedir(), "projects")),
|
|
126
|
+
}).default(() => ({ root: path.join(homedir(), "projects") })),
|
|
105
127
|
projects: z.array(projectSchema).default([]),
|
|
128
|
+
repositories: z.array(repositorySchema).default([]),
|
|
106
129
|
});
|
|
107
130
|
function defaultTriggerEvents(actor) {
|
|
108
131
|
if (actor === "app") {
|
|
@@ -141,7 +164,9 @@ function withSectionDefaults(input) {
|
|
|
141
164
|
logging: {},
|
|
142
165
|
database: {},
|
|
143
166
|
operator_api: {},
|
|
167
|
+
repos: {},
|
|
144
168
|
projects: [],
|
|
169
|
+
repositories: [],
|
|
145
170
|
...rest,
|
|
146
171
|
linear: {
|
|
147
172
|
...linear,
|
|
@@ -250,6 +275,13 @@ function readRepoSettings(repoPath, env) {
|
|
|
250
275
|
function defaultWorktreeRoot(projectId) {
|
|
251
276
|
return path.join(getPatchRelayDataDir(), "worktrees", projectId);
|
|
252
277
|
}
|
|
278
|
+
function defaultRepositoryLocalPath(reposRoot, githubRepo) {
|
|
279
|
+
const repoName = githubRepo.split("/").pop()?.trim();
|
|
280
|
+
if (!repoName) {
|
|
281
|
+
throw new Error(`Invalid github_repo: ${githubRepo}`);
|
|
282
|
+
}
|
|
283
|
+
return path.join(ensureAbsolutePath(reposRoot), repoName);
|
|
284
|
+
}
|
|
253
285
|
function defaultBranchPrefix(projectId) {
|
|
254
286
|
const sanitized = projectId
|
|
255
287
|
.trim()
|
|
@@ -317,6 +349,84 @@ export function loadConfig(configPath = process.env.PATCHRELAY_CONFIG ?? getDefa
|
|
|
317
349
|
}
|
|
318
350
|
const logFilePath = env.PATCHRELAY_LOG_FILE ?? parsed.logging.file_path;
|
|
319
351
|
const oauthRedirectUri = parsed.linear.oauth.redirect_uri ?? deriveLinearOAuthRedirectUri(parsed.server);
|
|
352
|
+
const reposRoot = ensureAbsolutePath(parsed.repos.root);
|
|
353
|
+
const repositories = parsed.repositories.map((repository) => {
|
|
354
|
+
const localPath = ensureAbsolutePath(repository.local_path ?? defaultRepositoryLocalPath(reposRoot, repository.github_repo));
|
|
355
|
+
return {
|
|
356
|
+
githubRepo: repository.github_repo,
|
|
357
|
+
localPath,
|
|
358
|
+
...(repository.workspace ? { workspace: repository.workspace } : {}),
|
|
359
|
+
linearTeamIds: repository.linear_team_ids,
|
|
360
|
+
linearProjectIds: repository.linear_project_ids,
|
|
361
|
+
issueKeyPrefixes: repository.issue_key_prefixes,
|
|
362
|
+
reviewChecks: repository.review_checks,
|
|
363
|
+
gateChecks: repository.gate_checks,
|
|
364
|
+
triggerEvents: repository.trigger_events,
|
|
365
|
+
branchPrefix: repository.branch_prefix,
|
|
366
|
+
github: repository.github,
|
|
367
|
+
};
|
|
368
|
+
});
|
|
369
|
+
const repositoryProjects = repositories.map((repository) => {
|
|
370
|
+
const repoSettings = readRepoSettings(repository.localPath, env);
|
|
371
|
+
return {
|
|
372
|
+
id: repository.githubRepo,
|
|
373
|
+
repoPath: repository.localPath,
|
|
374
|
+
worktreeRoot: ensureAbsolutePath(defaultWorktreeRoot(repository.githubRepo)),
|
|
375
|
+
issueKeyPrefixes: repository.issueKeyPrefixes,
|
|
376
|
+
linearTeamIds: repository.linearTeamIds,
|
|
377
|
+
allowLabels: [],
|
|
378
|
+
reviewChecks: repository.reviewChecks,
|
|
379
|
+
gateChecks: repository.gateChecks,
|
|
380
|
+
triggerEvents: normalizeTriggerEvents(parsed.linear.oauth.actor, repoSettings?.trigger_events ?? repository.triggerEvents),
|
|
381
|
+
branchPrefix: repoSettings?.branch_prefix ?? repository.branchPrefix ?? defaultBranchPrefix(repository.githubRepo),
|
|
382
|
+
...(repoSettings?.configPath ? { repoSettingsPath: repoSettings.configPath } : {}),
|
|
383
|
+
github: {
|
|
384
|
+
repoFullName: repository.githubRepo,
|
|
385
|
+
...(repository.github?.base_branch ? { baseBranch: repository.github.base_branch } : {}),
|
|
386
|
+
...(repository.github?.webhook_secret ? { webhookSecret: repository.github.webhook_secret } : {}),
|
|
387
|
+
...(repository.github?.merge_queue_label ? { mergeQueueLabel: repository.github.merge_queue_label } : {}),
|
|
388
|
+
...(repository.github?.merge_queue_check_name ? { mergeQueueCheckName: repository.github.merge_queue_check_name } : {}),
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
});
|
|
392
|
+
const legacyProjects = parsed.projects.map((project) => {
|
|
393
|
+
const repoPath = ensureAbsolutePath(project.repo_path);
|
|
394
|
+
const repoSettings = readRepoSettings(repoPath, env);
|
|
395
|
+
const trustedActors = project.trusted_actors;
|
|
396
|
+
return {
|
|
397
|
+
id: project.id,
|
|
398
|
+
repoPath,
|
|
399
|
+
worktreeRoot: ensureAbsolutePath(project.worktree_root ?? defaultWorktreeRoot(project.id)),
|
|
400
|
+
...(trustedActors
|
|
401
|
+
? {
|
|
402
|
+
trustedActors: {
|
|
403
|
+
ids: trustedActors.ids,
|
|
404
|
+
names: trustedActors.names,
|
|
405
|
+
emails: trustedActors.emails,
|
|
406
|
+
emailDomains: trustedActors.email_domains,
|
|
407
|
+
},
|
|
408
|
+
}
|
|
409
|
+
: {}),
|
|
410
|
+
issueKeyPrefixes: project.issue_key_prefixes,
|
|
411
|
+
linearTeamIds: project.linear_team_ids,
|
|
412
|
+
allowLabels: project.allow_labels,
|
|
413
|
+
reviewChecks: project.review_checks,
|
|
414
|
+
gateChecks: project.gate_checks,
|
|
415
|
+
triggerEvents: normalizeTriggerEvents(parsed.linear.oauth.actor, repoSettings?.trigger_events ??
|
|
416
|
+
project.trigger_events),
|
|
417
|
+
branchPrefix: repoSettings?.branch_prefix ?? project.branch_prefix ?? defaultBranchPrefix(project.id),
|
|
418
|
+
...(repoSettings?.configPath ? { repoSettingsPath: repoSettings.configPath } : {}),
|
|
419
|
+
...(project.github ? {
|
|
420
|
+
github: {
|
|
421
|
+
...(project.github.webhook_secret ? { webhookSecret: project.github.webhook_secret } : {}),
|
|
422
|
+
...(project.github.repo_full_name ? { repoFullName: project.github.repo_full_name } : {}),
|
|
423
|
+
...(project.github.base_branch ? { baseBranch: project.github.base_branch } : {}),
|
|
424
|
+
},
|
|
425
|
+
} : {}),
|
|
426
|
+
};
|
|
427
|
+
});
|
|
428
|
+
const repositoryPaths = new Set(repositoryProjects.map((project) => project.repoPath));
|
|
429
|
+
const derivedProjects = [...repositoryProjects, ...legacyProjects.filter((project) => !repositoryPaths.has(project.repoPath))];
|
|
320
430
|
const config = {
|
|
321
431
|
server: {
|
|
322
432
|
bind: parsed.server.bind,
|
|
@@ -377,42 +487,18 @@ export function loadConfig(configPath = process.env.PATCHRELAY_CONFIG ?? getDefa
|
|
|
377
487
|
experimentalRawEvents: parsed.runner.codex.experimental_raw_events,
|
|
378
488
|
},
|
|
379
489
|
},
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
names: trustedActors.names,
|
|
393
|
-
emails: trustedActors.emails,
|
|
394
|
-
emailDomains: trustedActors.email_domains,
|
|
395
|
-
},
|
|
396
|
-
}
|
|
397
|
-
: {}),
|
|
398
|
-
issueKeyPrefixes: project.issue_key_prefixes,
|
|
399
|
-
linearTeamIds: project.linear_team_ids,
|
|
400
|
-
allowLabels: project.allow_labels,
|
|
401
|
-
reviewChecks: project.review_checks,
|
|
402
|
-
gateChecks: project.gate_checks,
|
|
403
|
-
triggerEvents: normalizeTriggerEvents(parsed.linear.oauth.actor, repoSettings?.trigger_events ??
|
|
404
|
-
project.trigger_events),
|
|
405
|
-
branchPrefix: repoSettings?.branch_prefix ?? project.branch_prefix ?? defaultBranchPrefix(project.id),
|
|
406
|
-
...(repoSettings?.configPath ? { repoSettingsPath: repoSettings.configPath } : {}),
|
|
407
|
-
...(project.github ? {
|
|
408
|
-
github: {
|
|
409
|
-
...(project.github.webhook_secret ? { webhookSecret: project.github.webhook_secret } : {}),
|
|
410
|
-
...(project.github.repo_full_name ? { repoFullName: project.github.repo_full_name } : {}),
|
|
411
|
-
...(project.github.base_branch ? { baseBranch: project.github.base_branch } : {}),
|
|
412
|
-
},
|
|
413
|
-
} : {}),
|
|
414
|
-
};
|
|
415
|
-
}),
|
|
490
|
+
repos: {
|
|
491
|
+
root: reposRoot,
|
|
492
|
+
},
|
|
493
|
+
repositories: repositories.map((repository) => ({
|
|
494
|
+
githubRepo: repository.githubRepo,
|
|
495
|
+
localPath: repository.localPath,
|
|
496
|
+
...(repository.workspace ? { workspace: repository.workspace } : {}),
|
|
497
|
+
linearTeamIds: repository.linearTeamIds,
|
|
498
|
+
linearProjectIds: repository.linearProjectIds,
|
|
499
|
+
issueKeyPrefixes: repository.issueKeyPrefixes,
|
|
500
|
+
})),
|
|
501
|
+
projects: derivedProjects,
|
|
416
502
|
secretSources: {
|
|
417
503
|
"linear-webhook-secret": rWebhookSecret.source,
|
|
418
504
|
"token-encryption-key": rTokenEncryptionKey.source,
|
|
@@ -462,8 +548,15 @@ function validateConfigSemantics(config, options) {
|
|
|
462
548
|
throw new Error(`linear.oauth.redirect_uri must use the fixed "${LINEAR_OAUTH_CALLBACK_PATH}" path`);
|
|
463
549
|
}
|
|
464
550
|
const projectIds = new Set();
|
|
551
|
+
const githubRepos = new Set();
|
|
465
552
|
const issuePrefixes = new Map();
|
|
466
553
|
const linearTeamIds = new Map();
|
|
554
|
+
for (const repository of config.repositories) {
|
|
555
|
+
if (githubRepos.has(repository.githubRepo)) {
|
|
556
|
+
throw new Error(`Duplicate repository github_repo: ${repository.githubRepo}`);
|
|
557
|
+
}
|
|
558
|
+
githubRepos.add(repository.githubRepo);
|
|
559
|
+
}
|
|
467
560
|
for (const project of config.projects) {
|
|
468
561
|
if (projectIds.has(project.id)) {
|
|
469
562
|
throw new Error(`Duplicate project id: ${project.id}`);
|
|
@@ -79,6 +79,23 @@ export class LinearInstallationStore {
|
|
|
79
79
|
.all();
|
|
80
80
|
return rows.map((row) => mapLinearInstallation(row));
|
|
81
81
|
}
|
|
82
|
+
findLinearInstallationByWorkspace(query) {
|
|
83
|
+
const normalized = query.trim().toLowerCase();
|
|
84
|
+
if (!normalized)
|
|
85
|
+
return undefined;
|
|
86
|
+
const row = this.connection
|
|
87
|
+
.prepare(`
|
|
88
|
+
SELECT *
|
|
89
|
+
FROM linear_installations
|
|
90
|
+
WHERE LOWER(COALESCE(workspace_key, '')) = ?
|
|
91
|
+
OR LOWER(COALESCE(workspace_name, '')) = ?
|
|
92
|
+
OR LOWER(COALESCE(workspace_id, '')) = ?
|
|
93
|
+
ORDER BY updated_at DESC, id DESC
|
|
94
|
+
LIMIT 1
|
|
95
|
+
`)
|
|
96
|
+
.get(normalized, normalized, normalized);
|
|
97
|
+
return row ? mapLinearInstallation(row) : undefined;
|
|
98
|
+
}
|
|
82
99
|
linkProjectInstallation(projectId, installationId) {
|
|
83
100
|
const now = isoNow();
|
|
84
101
|
this.connection
|
|
@@ -108,6 +125,12 @@ export class LinearInstallationStore {
|
|
|
108
125
|
unlinkProjectInstallation(projectId) {
|
|
109
126
|
this.connection.prepare("DELETE FROM project_installations WHERE project_id = ?").run(projectId);
|
|
110
127
|
}
|
|
128
|
+
unlinkInstallationProjects(installationId) {
|
|
129
|
+
this.connection.prepare("DELETE FROM project_installations WHERE installation_id = ?").run(installationId);
|
|
130
|
+
}
|
|
131
|
+
deleteLinearInstallation(installationId) {
|
|
132
|
+
this.connection.prepare("DELETE FROM linear_installations WHERE id = ?").run(installationId);
|
|
133
|
+
}
|
|
111
134
|
getLinearInstallationForProject(projectId) {
|
|
112
135
|
const row = this.connection
|
|
113
136
|
.prepare(`
|
package/dist/db/migrations.js
CHANGED
|
@@ -87,6 +87,37 @@ CREATE TABLE IF NOT EXISTS project_installations (
|
|
|
87
87
|
linked_at TEXT NOT NULL
|
|
88
88
|
);
|
|
89
89
|
|
|
90
|
+
CREATE TABLE IF NOT EXISTS repository_links (
|
|
91
|
+
github_repo TEXT PRIMARY KEY,
|
|
92
|
+
local_path TEXT NOT NULL,
|
|
93
|
+
installation_id INTEGER NOT NULL,
|
|
94
|
+
linear_team_ids_json TEXT NOT NULL DEFAULT '[]',
|
|
95
|
+
linear_project_ids_json TEXT NOT NULL DEFAULT '[]',
|
|
96
|
+
issue_key_prefixes_json TEXT NOT NULL DEFAULT '[]',
|
|
97
|
+
linked_at TEXT NOT NULL,
|
|
98
|
+
updated_at TEXT NOT NULL
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
CREATE TABLE IF NOT EXISTS linear_catalog_teams (
|
|
102
|
+
installation_id INTEGER NOT NULL,
|
|
103
|
+
team_id TEXT NOT NULL,
|
|
104
|
+
team_key TEXT,
|
|
105
|
+
team_name TEXT,
|
|
106
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
107
|
+
updated_at TEXT NOT NULL,
|
|
108
|
+
PRIMARY KEY (installation_id, team_id)
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
CREATE TABLE IF NOT EXISTS linear_catalog_projects (
|
|
112
|
+
installation_id INTEGER NOT NULL,
|
|
113
|
+
project_id TEXT NOT NULL,
|
|
114
|
+
project_name TEXT,
|
|
115
|
+
team_ids_json TEXT NOT NULL DEFAULT '[]',
|
|
116
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
117
|
+
updated_at TEXT NOT NULL,
|
|
118
|
+
PRIMARY KEY (installation_id, project_id)
|
|
119
|
+
);
|
|
120
|
+
|
|
90
121
|
CREATE TABLE IF NOT EXISTS oauth_states (
|
|
91
122
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
92
123
|
provider TEXT NOT NULL,
|
|
@@ -124,6 +155,9 @@ CREATE INDEX IF NOT EXISTS idx_runs_thread ON runs(thread_id);
|
|
|
124
155
|
CREATE INDEX IF NOT EXISTS idx_run_thread_events_run ON run_thread_events(run_id, id);
|
|
125
156
|
CREATE INDEX IF NOT EXISTS idx_operator_feed_events_issue ON operator_feed_events(issue_key, id);
|
|
126
157
|
CREATE INDEX IF NOT EXISTS idx_operator_feed_events_project ON operator_feed_events(project_id, id);
|
|
158
|
+
CREATE INDEX IF NOT EXISTS idx_repository_links_installation ON repository_links(installation_id, github_repo);
|
|
159
|
+
CREATE INDEX IF NOT EXISTS idx_linear_catalog_teams_installation ON linear_catalog_teams(installation_id, team_key, team_name);
|
|
160
|
+
CREATE INDEX IF NOT EXISTS idx_linear_catalog_projects_installation ON linear_catalog_projects(installation_id, project_name);
|
|
127
161
|
`;
|
|
128
162
|
export function runPatchRelayMigrations(connection) {
|
|
129
163
|
connection.exec(schema);
|
|
@@ -144,6 +178,14 @@ export function runPatchRelayMigrations(connection) {
|
|
|
144
178
|
// Zombie/stale recovery backoff
|
|
145
179
|
addColumnIfMissing(connection, "issues", "zombie_recovery_attempts", "INTEGER NOT NULL DEFAULT 0");
|
|
146
180
|
addColumnIfMissing(connection, "issues", "last_zombie_recovery_at", "TEXT");
|
|
181
|
+
// Preserve GitHub failure provenance so reconciliation can distinguish
|
|
182
|
+
// branch CI failures from merge-queue evictions after webhook delivery.
|
|
183
|
+
addColumnIfMissing(connection, "issues", "last_github_failure_source", "TEXT");
|
|
184
|
+
addColumnIfMissing(connection, "issues", "last_github_failure_check_name", "TEXT");
|
|
185
|
+
addColumnIfMissing(connection, "issues", "last_github_failure_check_url", "TEXT");
|
|
186
|
+
addColumnIfMissing(connection, "issues", "last_github_failure_at", "TEXT");
|
|
187
|
+
addColumnIfMissing(connection, "issues", "last_queue_signal_at", "TEXT");
|
|
188
|
+
addColumnIfMissing(connection, "issues", "last_queue_incident_json", "TEXT");
|
|
147
189
|
}
|
|
148
190
|
function addColumnIfMissing(connection, table, column, definition) {
|
|
149
191
|
const cols = connection.prepare(`PRAGMA table_info(${table})`).all();
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { isoNow } from "./shared.js";
|
|
2
|
+
export class RepositoryLinkStore {
|
|
3
|
+
connection;
|
|
4
|
+
constructor(connection) {
|
|
5
|
+
this.connection = connection;
|
|
6
|
+
}
|
|
7
|
+
upsertRepositoryLink(params) {
|
|
8
|
+
const now = isoNow();
|
|
9
|
+
this.connection
|
|
10
|
+
.prepare(`
|
|
11
|
+
INSERT INTO repository_links (
|
|
12
|
+
github_repo, local_path, installation_id, linear_team_ids_json, linear_project_ids_json, issue_key_prefixes_json, linked_at, updated_at
|
|
13
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
14
|
+
ON CONFLICT(github_repo) DO UPDATE SET
|
|
15
|
+
local_path = excluded.local_path,
|
|
16
|
+
installation_id = excluded.installation_id,
|
|
17
|
+
linear_team_ids_json = excluded.linear_team_ids_json,
|
|
18
|
+
linear_project_ids_json = excluded.linear_project_ids_json,
|
|
19
|
+
issue_key_prefixes_json = excluded.issue_key_prefixes_json,
|
|
20
|
+
updated_at = excluded.updated_at
|
|
21
|
+
`)
|
|
22
|
+
.run(params.githubRepo, params.localPath, params.installationId, JSON.stringify(params.linearTeamIds), JSON.stringify(params.linearProjectIds ?? []), JSON.stringify(params.issueKeyPrefixes ?? []), now, now);
|
|
23
|
+
return this.getRepositoryLink(params.githubRepo);
|
|
24
|
+
}
|
|
25
|
+
getRepositoryLink(githubRepo) {
|
|
26
|
+
const row = this.connection
|
|
27
|
+
.prepare("SELECT * FROM repository_links WHERE github_repo = ?")
|
|
28
|
+
.get(githubRepo);
|
|
29
|
+
return row ? mapRepositoryLink(row) : undefined;
|
|
30
|
+
}
|
|
31
|
+
listRepositoryLinks() {
|
|
32
|
+
const rows = this.connection
|
|
33
|
+
.prepare("SELECT * FROM repository_links ORDER BY github_repo")
|
|
34
|
+
.all();
|
|
35
|
+
return rows.map(mapRepositoryLink);
|
|
36
|
+
}
|
|
37
|
+
deleteRepositoryLink(githubRepo) {
|
|
38
|
+
this.connection.prepare("DELETE FROM repository_links WHERE github_repo = ?").run(githubRepo);
|
|
39
|
+
}
|
|
40
|
+
replaceCatalog(params) {
|
|
41
|
+
const now = isoNow();
|
|
42
|
+
this.connection.prepare("DELETE FROM linear_catalog_teams WHERE installation_id = ?").run(params.installationId);
|
|
43
|
+
this.connection.prepare("DELETE FROM linear_catalog_projects WHERE installation_id = ?").run(params.installationId);
|
|
44
|
+
const teamStmt = this.connection.prepare(`
|
|
45
|
+
INSERT INTO linear_catalog_teams (installation_id, team_id, team_key, team_name, active, updated_at)
|
|
46
|
+
VALUES (?, ?, ?, ?, 1, ?)
|
|
47
|
+
`);
|
|
48
|
+
for (const team of params.teams) {
|
|
49
|
+
teamStmt.run(params.installationId, team.id, team.key ?? null, team.name ?? null, now);
|
|
50
|
+
}
|
|
51
|
+
const projectStmt = this.connection.prepare(`
|
|
52
|
+
INSERT INTO linear_catalog_projects (installation_id, project_id, project_name, team_ids_json, active, updated_at)
|
|
53
|
+
VALUES (?, ?, ?, ?, 1, ?)
|
|
54
|
+
`);
|
|
55
|
+
for (const project of params.projects) {
|
|
56
|
+
projectStmt.run(params.installationId, project.id, project.name ?? null, JSON.stringify(project.teamIds), now);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
listCatalogTeams(installationId) {
|
|
60
|
+
const rows = this.connection
|
|
61
|
+
.prepare("SELECT * FROM linear_catalog_teams WHERE installation_id = ? ORDER BY COALESCE(team_key, team_name, team_id)")
|
|
62
|
+
.all(installationId);
|
|
63
|
+
return rows.map(mapCatalogTeam);
|
|
64
|
+
}
|
|
65
|
+
listCatalogProjects(installationId) {
|
|
66
|
+
const rows = this.connection
|
|
67
|
+
.prepare("SELECT * FROM linear_catalog_projects WHERE installation_id = ? ORDER BY COALESCE(project_name, project_id)")
|
|
68
|
+
.all(installationId);
|
|
69
|
+
return rows.map(mapCatalogProject);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function mapRepositoryLink(row) {
|
|
73
|
+
return {
|
|
74
|
+
githubRepo: String(row.github_repo),
|
|
75
|
+
localPath: String(row.local_path),
|
|
76
|
+
installationId: Number(row.installation_id),
|
|
77
|
+
linearTeamIdsJson: String(row.linear_team_ids_json ?? "[]"),
|
|
78
|
+
linearProjectIdsJson: String(row.linear_project_ids_json ?? "[]"),
|
|
79
|
+
issueKeyPrefixesJson: String(row.issue_key_prefixes_json ?? "[]"),
|
|
80
|
+
linkedAt: String(row.linked_at),
|
|
81
|
+
updatedAt: String(row.updated_at),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function mapCatalogTeam(row) {
|
|
85
|
+
return {
|
|
86
|
+
installationId: Number(row.installation_id),
|
|
87
|
+
teamId: String(row.team_id),
|
|
88
|
+
...(row.team_key === null ? {} : { key: String(row.team_key) }),
|
|
89
|
+
...(row.team_name === null ? {} : { name: String(row.team_name) }),
|
|
90
|
+
active: Number(row.active ?? 0) !== 0,
|
|
91
|
+
updatedAt: String(row.updated_at),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function mapCatalogProject(row) {
|
|
95
|
+
return {
|
|
96
|
+
installationId: Number(row.installation_id),
|
|
97
|
+
projectId: String(row.project_id),
|
|
98
|
+
...(row.project_name === null ? {} : { name: String(row.project_name) }),
|
|
99
|
+
teamIdsJson: String(row.team_ids_json ?? "[]"),
|
|
100
|
+
active: Number(row.active ?? 0) !== 0,
|
|
101
|
+
updatedAt: String(row.updated_at),
|
|
102
|
+
};
|
|
103
|
+
}
|
package/dist/db.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { LinearInstallationStore } from "./db/linear-installation-store.js";
|
|
2
2
|
import { OperatorFeedStore } from "./db/operator-feed-store.js";
|
|
3
|
+
import { RepositoryLinkStore } from "./db/repository-link-store.js";
|
|
3
4
|
import { runPatchRelayMigrations } from "./db/migrations.js";
|
|
4
5
|
import { SqliteConnection, isoNow } from "./db/shared.js";
|
|
5
6
|
export class PatchRelayDatabase {
|
|
6
7
|
connection;
|
|
7
8
|
linearInstallations;
|
|
8
9
|
operatorFeed;
|
|
10
|
+
repositories;
|
|
9
11
|
constructor(databasePath, wal) {
|
|
10
12
|
this.connection = new SqliteConnection(databasePath);
|
|
11
13
|
this.connection.pragma("foreign_keys = ON");
|
|
@@ -14,6 +16,7 @@ export class PatchRelayDatabase {
|
|
|
14
16
|
}
|
|
15
17
|
this.linearInstallations = new LinearInstallationStore(this.connection);
|
|
16
18
|
this.operatorFeed = new OperatorFeedStore(this.connection);
|
|
19
|
+
this.repositories = new RepositoryLinkStore(this.connection);
|
|
17
20
|
}
|
|
18
21
|
runMigrations() {
|
|
19
22
|
runPatchRelayMigrations(this.connection);
|
|
@@ -153,6 +156,30 @@ export class PatchRelayDatabase {
|
|
|
153
156
|
sets.push("pr_check_status = @prCheckStatus");
|
|
154
157
|
values.prCheckStatus = params.prCheckStatus;
|
|
155
158
|
}
|
|
159
|
+
if (params.lastGitHubFailureSource !== undefined) {
|
|
160
|
+
sets.push("last_github_failure_source = @lastGitHubFailureSource");
|
|
161
|
+
values.lastGitHubFailureSource = params.lastGitHubFailureSource;
|
|
162
|
+
}
|
|
163
|
+
if (params.lastGitHubFailureCheckName !== undefined) {
|
|
164
|
+
sets.push("last_github_failure_check_name = @lastGitHubFailureCheckName");
|
|
165
|
+
values.lastGitHubFailureCheckName = params.lastGitHubFailureCheckName;
|
|
166
|
+
}
|
|
167
|
+
if (params.lastGitHubFailureCheckUrl !== undefined) {
|
|
168
|
+
sets.push("last_github_failure_check_url = @lastGitHubFailureCheckUrl");
|
|
169
|
+
values.lastGitHubFailureCheckUrl = params.lastGitHubFailureCheckUrl;
|
|
170
|
+
}
|
|
171
|
+
if (params.lastGitHubFailureAt !== undefined) {
|
|
172
|
+
sets.push("last_github_failure_at = @lastGitHubFailureAt");
|
|
173
|
+
values.lastGitHubFailureAt = params.lastGitHubFailureAt;
|
|
174
|
+
}
|
|
175
|
+
if (params.lastQueueSignalAt !== undefined) {
|
|
176
|
+
sets.push("last_queue_signal_at = @lastQueueSignalAt");
|
|
177
|
+
values.lastQueueSignalAt = params.lastQueueSignalAt;
|
|
178
|
+
}
|
|
179
|
+
if (params.lastQueueIncidentJson !== undefined) {
|
|
180
|
+
sets.push("last_queue_incident_json = @lastQueueIncidentJson");
|
|
181
|
+
values.lastQueueIncidentJson = params.lastQueueIncidentJson;
|
|
182
|
+
}
|
|
156
183
|
if (params.ciRepairAttempts !== undefined) {
|
|
157
184
|
sets.push("ci_repair_attempts = @ciRepairAttempts");
|
|
158
185
|
values.ciRepairAttempts = params.ciRepairAttempts;
|
|
@@ -165,14 +192,6 @@ export class PatchRelayDatabase {
|
|
|
165
192
|
sets.push("review_fix_attempts = @reviewFixAttempts");
|
|
166
193
|
values.reviewFixAttempts = params.reviewFixAttempts;
|
|
167
194
|
}
|
|
168
|
-
if (params.mergePrepAttempts !== undefined) {
|
|
169
|
-
sets.push("merge_prep_attempts = @mergePrepAttempts");
|
|
170
|
-
values.mergePrepAttempts = params.mergePrepAttempts;
|
|
171
|
-
}
|
|
172
|
-
if (params.pendingMergePrep !== undefined) {
|
|
173
|
-
sets.push("pending_merge_prep = @pendingMergePrep");
|
|
174
|
-
values.pendingMergePrep = params.pendingMergePrep ? 1 : 0;
|
|
175
|
-
}
|
|
176
195
|
if (params.zombieRecoveryAttempts !== undefined) {
|
|
177
196
|
sets.push("zombie_recovery_attempts = @zombieRecoveryAttempts");
|
|
178
197
|
values.zombieRecoveryAttempts = params.zombieRecoveryAttempts;
|
|
@@ -191,6 +210,8 @@ export class PatchRelayDatabase {
|
|
|
191
210
|
current_linear_state, factory_state, pending_run_type, pending_run_context_json,
|
|
192
211
|
branch_name, worktree_path, thread_id, active_run_id,
|
|
193
212
|
agent_session_id,
|
|
213
|
+
pr_number, pr_url, pr_state, pr_review_state, pr_check_status,
|
|
214
|
+
last_github_failure_source, last_github_failure_check_name, last_github_failure_check_url, last_github_failure_at, last_queue_signal_at, last_queue_incident_json,
|
|
194
215
|
updated_at
|
|
195
216
|
) VALUES (
|
|
196
217
|
@projectId, @linearIssueId, @issueKey, @title, @description, @url,
|
|
@@ -198,6 +219,8 @@ export class PatchRelayDatabase {
|
|
|
198
219
|
@currentLinearState, @factoryState, @pendingRunType, @pendingRunContextJson,
|
|
199
220
|
@branchName, @worktreePath, @threadId, @activeRunId,
|
|
200
221
|
@agentSessionId,
|
|
222
|
+
@prNumber, @prUrl, @prState, @prReviewState, @prCheckStatus,
|
|
223
|
+
@lastGitHubFailureSource, @lastGitHubFailureCheckName, @lastGitHubFailureCheckUrl, @lastGitHubFailureAt, @lastQueueSignalAt, @lastQueueIncidentJson,
|
|
201
224
|
@now
|
|
202
225
|
)
|
|
203
226
|
`).run({
|
|
@@ -218,6 +241,17 @@ export class PatchRelayDatabase {
|
|
|
218
241
|
threadId: params.threadId ?? null,
|
|
219
242
|
activeRunId: params.activeRunId ?? null,
|
|
220
243
|
agentSessionId: params.agentSessionId ?? null,
|
|
244
|
+
prNumber: params.prNumber ?? null,
|
|
245
|
+
prUrl: params.prUrl ?? null,
|
|
246
|
+
prState: params.prState ?? null,
|
|
247
|
+
prReviewState: params.prReviewState ?? null,
|
|
248
|
+
prCheckStatus: params.prCheckStatus ?? null,
|
|
249
|
+
lastGitHubFailureSource: params.lastGitHubFailureSource ?? null,
|
|
250
|
+
lastGitHubFailureCheckName: params.lastGitHubFailureCheckName ?? null,
|
|
251
|
+
lastGitHubFailureCheckUrl: params.lastGitHubFailureCheckUrl ?? null,
|
|
252
|
+
lastGitHubFailureAt: params.lastGitHubFailureAt ?? null,
|
|
253
|
+
lastQueueSignalAt: params.lastQueueSignalAt ?? null,
|
|
254
|
+
lastQueueIncidentJson: params.lastQueueIncidentJson ?? null,
|
|
221
255
|
now,
|
|
222
256
|
});
|
|
223
257
|
}
|
|
@@ -247,7 +281,7 @@ export class PatchRelayDatabase {
|
|
|
247
281
|
}
|
|
248
282
|
listIssuesReadyForExecution() {
|
|
249
283
|
const rows = this.connection
|
|
250
|
-
.prepare("SELECT project_id, linear_issue_id FROM issues WHERE
|
|
284
|
+
.prepare("SELECT project_id, linear_issue_id FROM issues WHERE pending_run_type IS NOT NULL AND active_run_id IS NULL")
|
|
251
285
|
.all();
|
|
252
286
|
return rows.map((row) => ({
|
|
253
287
|
projectId: String(row.project_id),
|
|
@@ -427,11 +461,27 @@ function mapIssueRow(row) {
|
|
|
427
461
|
...(row.pr_state !== null && row.pr_state !== undefined ? { prState: String(row.pr_state) } : {}),
|
|
428
462
|
...(row.pr_review_state !== null && row.pr_review_state !== undefined ? { prReviewState: String(row.pr_review_state) } : {}),
|
|
429
463
|
...(row.pr_check_status !== null && row.pr_check_status !== undefined ? { prCheckStatus: String(row.pr_check_status) } : {}),
|
|
464
|
+
...(row.last_github_failure_source !== null && row.last_github_failure_source !== undefined
|
|
465
|
+
? { lastGitHubFailureSource: String(row.last_github_failure_source) }
|
|
466
|
+
: {}),
|
|
467
|
+
...(row.last_github_failure_check_name !== null && row.last_github_failure_check_name !== undefined
|
|
468
|
+
? { lastGitHubFailureCheckName: String(row.last_github_failure_check_name) }
|
|
469
|
+
: {}),
|
|
470
|
+
...(row.last_github_failure_check_url !== null && row.last_github_failure_check_url !== undefined
|
|
471
|
+
? { lastGitHubFailureCheckUrl: String(row.last_github_failure_check_url) }
|
|
472
|
+
: {}),
|
|
473
|
+
...(row.last_github_failure_at !== null && row.last_github_failure_at !== undefined
|
|
474
|
+
? { lastGitHubFailureAt: String(row.last_github_failure_at) }
|
|
475
|
+
: {}),
|
|
476
|
+
...(row.last_queue_signal_at !== null && row.last_queue_signal_at !== undefined
|
|
477
|
+
? { lastQueueSignalAt: String(row.last_queue_signal_at) }
|
|
478
|
+
: {}),
|
|
479
|
+
...(row.last_queue_incident_json !== null && row.last_queue_incident_json !== undefined
|
|
480
|
+
? { lastQueueIncidentJson: String(row.last_queue_incident_json) }
|
|
481
|
+
: {}),
|
|
430
482
|
ciRepairAttempts: Number(row.ci_repair_attempts ?? 0),
|
|
431
483
|
queueRepairAttempts: Number(row.queue_repair_attempts ?? 0),
|
|
432
484
|
reviewFixAttempts: Number(row.review_fix_attempts ?? 0),
|
|
433
|
-
mergePrepAttempts: Number(row.merge_prep_attempts ?? 0),
|
|
434
|
-
pendingMergePrep: Boolean(row.pending_merge_prep),
|
|
435
485
|
zombieRecoveryAttempts: Number(row.zombie_recovery_attempts ?? 0),
|
|
436
486
|
...(row.last_zombie_recovery_at !== null && row.last_zombie_recovery_at !== undefined ? { lastZombieRecoveryAt: String(row.last_zombie_recovery_at) } : {}),
|
|
437
487
|
};
|
package/dist/factory-state.js
CHANGED
|
@@ -61,12 +61,8 @@ const TRANSITION_RULES = [
|
|
|
61
61
|
{ event: "check_failed",
|
|
62
62
|
guard: (s, ctx) => isOpen(s) && ctx.activeRunId === undefined,
|
|
63
63
|
to: "repairing_ci" },
|
|
64
|
-
// ── Merge queue events ─────────────────────────────────────────
|
|
65
|
-
{ event: "merge_group_failed",
|
|
66
|
-
guard: (s) => s === "awaiting_queue",
|
|
67
|
-
to: "repairing_queue" },
|
|
68
|
-
// merge_group_passed: no rule → no transition (merge event follows)
|
|
69
64
|
// pr_synchronize: no rule → no transition (resets counters only)
|
|
65
|
+
// merge_group events: not used — merge queue is handled by external steward
|
|
70
66
|
];
|
|
71
67
|
/**
|
|
72
68
|
* Resolve the next factory state from a GitHub webhook event.
|