patchrelay 0.74.0 → 0.74.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/dist/build-info.json +3 -3
- package/dist/cli/data.js +3 -0
- package/dist/db/schema-guard.js +17 -0
- package/dist/db.js +7 -0
- package/dist/github-cli-auth.js +43 -0
- package/dist/preflight.js +2 -0
- package/dist/run-notification-handler.js +125 -6
- package/dist/run-orchestrator.js +1 -1
- package/dist/service-runtime.js +1 -1
- package/dist/service.js +11 -1
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
package/dist/cli/data.js
CHANGED
|
@@ -98,6 +98,9 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
98
98
|
super(config);
|
|
99
99
|
this.config = config;
|
|
100
100
|
this.db = options?.db ?? new PatchRelayDatabase(config.database.path, config.database.wal);
|
|
101
|
+
if (!options?.db) {
|
|
102
|
+
this.db.assertSchemaReady();
|
|
103
|
+
}
|
|
101
104
|
this.codex = options?.codex;
|
|
102
105
|
}
|
|
103
106
|
close() {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const REQUIRED_PATCHRELAY_TABLES = [
|
|
2
|
+
"issues",
|
|
3
|
+
"runs",
|
|
4
|
+
"issue_sessions",
|
|
5
|
+
"issue_session_events",
|
|
6
|
+
];
|
|
7
|
+
export function assertPatchRelaySchemaReady(connection, databasePath) {
|
|
8
|
+
const rows = connection
|
|
9
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table'")
|
|
10
|
+
.all();
|
|
11
|
+
const tables = new Set(rows.map((row) => String(row.name)));
|
|
12
|
+
const missing = REQUIRED_PATCHRELAY_TABLES.filter((table) => !tables.has(table));
|
|
13
|
+
if (missing.length === 0) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
throw new Error(`PatchRelay database is uninitialized or points at the wrong path: ${databasePath}. Missing required table(s): ${missing.join(", ")}`);
|
|
17
|
+
}
|
package/dist/db.js
CHANGED
|
@@ -7,6 +7,7 @@ import { RepositoryLinkStore } from "./db/repository-link-store.js";
|
|
|
7
7
|
import { RunStore } from "./db/run-store.js";
|
|
8
8
|
import { WebhookEventStore } from "./db/webhook-event-store.js";
|
|
9
9
|
import { runPatchRelayMigrations } from "./db/migrations.js";
|
|
10
|
+
import { assertPatchRelaySchemaReady } from "./db/schema-guard.js";
|
|
10
11
|
import { SqliteConnection } from "./db/shared.js";
|
|
11
12
|
import { syncIssueSessionFromIssue } from "./issue-session-projector.js";
|
|
12
13
|
import { TrackedIssueQuery } from "./tracked-issue-query.js";
|
|
@@ -23,6 +24,7 @@ export class PatchRelayDatabase {
|
|
|
23
24
|
runs;
|
|
24
25
|
trackedIssues;
|
|
25
26
|
constructor(databasePath, wal) {
|
|
27
|
+
this.databasePath = databasePath;
|
|
26
28
|
this.connection = new SqliteConnection(databasePath);
|
|
27
29
|
this.connection.pragma("foreign_keys = ON");
|
|
28
30
|
if (wal) {
|
|
@@ -45,8 +47,13 @@ export class PatchRelayDatabase {
|
|
|
45
47
|
this.workflowWakes = new WorkflowWakeResolver(this.issues, this.issueSessions);
|
|
46
48
|
this.trackedIssues = new TrackedIssueQuery(this.issues, this.issueSessions, this.workflowWakes, this.runs);
|
|
47
49
|
}
|
|
50
|
+
databasePath;
|
|
48
51
|
runMigrations() {
|
|
49
52
|
runPatchRelayMigrations(this.connection);
|
|
53
|
+
this.assertSchemaReady();
|
|
54
|
+
}
|
|
55
|
+
assertSchemaReady() {
|
|
56
|
+
assertPatchRelaySchemaReady(this.connection, this.databasePath);
|
|
50
57
|
}
|
|
51
58
|
transaction(fn) {
|
|
52
59
|
return this.connection.transaction(fn)();
|
package/dist/github-cli-auth.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
/**
|
|
5
6
|
* Unified GitHub App credential delivery for `git` and the `gh` CLI.
|
|
@@ -88,3 +89,45 @@ export function buildAgentChildEnv(parentEnv = process.env) {
|
|
|
88
89
|
delete env.GITHUB_TOKEN;
|
|
89
90
|
return env;
|
|
90
91
|
}
|
|
92
|
+
export async function verifyGitHubCliAuthEnv(env = process.env, options = {}) {
|
|
93
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
94
|
+
await new Promise((resolve, reject) => {
|
|
95
|
+
const child = spawn("git", ["credential", "fill"], {
|
|
96
|
+
env,
|
|
97
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
98
|
+
});
|
|
99
|
+
let stderr = "";
|
|
100
|
+
let settled = false;
|
|
101
|
+
const timer = setTimeout(() => {
|
|
102
|
+
if (settled)
|
|
103
|
+
return;
|
|
104
|
+
settled = true;
|
|
105
|
+
child.kill("SIGTERM");
|
|
106
|
+
reject(new Error(`GitHub git credential check timed out after ${timeoutMs}ms`));
|
|
107
|
+
}, timeoutMs);
|
|
108
|
+
timer.unref?.();
|
|
109
|
+
child.stderr.on("data", (chunk) => {
|
|
110
|
+
stderr += String(chunk);
|
|
111
|
+
});
|
|
112
|
+
child.on("error", (error) => {
|
|
113
|
+
if (settled)
|
|
114
|
+
return;
|
|
115
|
+
settled = true;
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
reject(error);
|
|
118
|
+
});
|
|
119
|
+
child.on("close", (code) => {
|
|
120
|
+
if (settled)
|
|
121
|
+
return;
|
|
122
|
+
settled = true;
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
if (code === 0) {
|
|
125
|
+
resolve();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const detail = stderr.trim();
|
|
129
|
+
reject(new Error(`GitHub git credential check failed${detail ? `: ${detail}` : ""}`));
|
|
130
|
+
});
|
|
131
|
+
child.stdin.end(`protocol=https\nhost=${GITHUB_HOST}\n\n`);
|
|
132
|
+
});
|
|
133
|
+
}
|
package/dist/preflight.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { accessSync, constants, existsSync, mkdirSync, statSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { runPatchRelayMigrations } from "./db/migrations.js";
|
|
4
|
+
import { assertPatchRelaySchemaReady } from "./db/schema-guard.js";
|
|
4
5
|
import { resolveMergeQueueProtocol } from "./merge-queue-protocol.js";
|
|
5
6
|
import { SqliteConnection } from "./db/shared.js";
|
|
6
7
|
import { execCommand } from "./utils.js";
|
|
@@ -132,6 +133,7 @@ function checkDatabaseHealth(config) {
|
|
|
132
133
|
connection.pragma("journal_mode = WAL");
|
|
133
134
|
}
|
|
134
135
|
runPatchRelayMigrations(connection);
|
|
136
|
+
assertPatchRelaySchemaReady(connection, config.database.path);
|
|
135
137
|
const quickCheck = connection.prepare("PRAGMA quick_check").get();
|
|
136
138
|
const quickCheckResult = quickCheck ? Object.values(quickCheck)[0] : undefined;
|
|
137
139
|
if (quickCheckResult !== "ok") {
|
|
@@ -4,6 +4,7 @@ import { resolveRecoverablePostRunState } from "./interrupted-run-recovery.js";
|
|
|
4
4
|
function isRequestedChangesRunType(runType) {
|
|
5
5
|
return runType === "review_fix" || runType === "branch_upkeep";
|
|
6
6
|
}
|
|
7
|
+
const DEFAULT_PUBLISH_COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
|
|
7
8
|
export class RunNotificationHandler {
|
|
8
9
|
config;
|
|
9
10
|
db;
|
|
@@ -15,8 +16,10 @@ export class RunNotificationHandler {
|
|
|
15
16
|
heartbeatIssueSessionLease;
|
|
16
17
|
releaseIssueSessionLease;
|
|
17
18
|
feed;
|
|
19
|
+
options;
|
|
18
20
|
activeThreadId;
|
|
19
|
-
|
|
21
|
+
publishCommandWatchdogs = new Map();
|
|
22
|
+
constructor(config, db, logger, linearSync, runFinalizer, readThreadWithRetry, withHeldIssueSessionLease, heartbeatIssueSessionLease, releaseIssueSessionLease, feed, options = {}) {
|
|
20
23
|
this.config = config;
|
|
21
24
|
this.db = db;
|
|
22
25
|
this.logger = logger;
|
|
@@ -27,6 +30,7 @@ export class RunNotificationHandler {
|
|
|
27
30
|
this.heartbeatIssueSessionLease = heartbeatIssueSessionLease;
|
|
28
31
|
this.releaseIssueSessionLease = releaseIssueSessionLease;
|
|
29
32
|
this.feed = feed;
|
|
33
|
+
this.options = options;
|
|
30
34
|
}
|
|
31
35
|
async handle(notification) {
|
|
32
36
|
let threadId = typeof notification.params.threadId === "string" ? notification.params.threadId : undefined;
|
|
@@ -50,6 +54,7 @@ export class RunNotificationHandler {
|
|
|
50
54
|
return;
|
|
51
55
|
}
|
|
52
56
|
const turnId = typeof notification.params.turnId === "string" ? notification.params.turnId : undefined;
|
|
57
|
+
this.observePublishCommand(notification, run, threadId, turnId ?? run.turnId);
|
|
53
58
|
if (this.config.runner.codex.persistExtendedHistory) {
|
|
54
59
|
this.db.runs.saveThreadEvent({
|
|
55
60
|
runId: run.id,
|
|
@@ -59,15 +64,13 @@ export class RunNotificationHandler {
|
|
|
59
64
|
eventJson: JSON.stringify(notification.params),
|
|
60
65
|
});
|
|
61
66
|
}
|
|
62
|
-
this.
|
|
67
|
+
this.maybeEmitProgress(notification, run);
|
|
63
68
|
if (notification.method === "turn/plan/updated") {
|
|
64
|
-
|
|
65
|
-
if (issue) {
|
|
66
|
-
void this.linearSync.syncCodexPlan(issue, notification.params);
|
|
67
|
-
}
|
|
69
|
+
this.syncCodexPlan(notification, run);
|
|
68
70
|
}
|
|
69
71
|
if (notification.method !== "turn/completed")
|
|
70
72
|
return;
|
|
73
|
+
this.clearPublishWatchdogsForThread(threadId);
|
|
71
74
|
const thread = await this.readThreadWithRetry(threadId);
|
|
72
75
|
const issue = this.db.issues.getIssue(run.projectId, run.linearIssueId);
|
|
73
76
|
if (!issue)
|
|
@@ -137,4 +140,120 @@ export class RunNotificationHandler {
|
|
|
137
140
|
});
|
|
138
141
|
this.activeThreadId = undefined;
|
|
139
142
|
}
|
|
143
|
+
observePublishCommand(notification, run, threadId, turnId) {
|
|
144
|
+
const item = notification.params.item;
|
|
145
|
+
if (!item || typeof item !== "object") {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const itemRecord = item;
|
|
149
|
+
const itemId = typeof itemRecord.id === "string" ? itemRecord.id : undefined;
|
|
150
|
+
if (!itemId) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (notification.method === "item/completed" || isTerminalItemUpdate(notification.method, itemRecord)) {
|
|
154
|
+
this.clearPublishWatchdog(threadId, itemId);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (notification.method !== "item/started" || itemRecord.type !== "commandExecution" || !turnId || !this.options.interruptTurn) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const command = extractCommandText(itemRecord.command);
|
|
161
|
+
if (!command || !isGitPushCommand(command)) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const key = publishWatchdogKey(threadId, itemId);
|
|
165
|
+
if (this.publishCommandWatchdogs.has(key)) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const timeoutMs = this.options.publishCommandTimeoutMs ?? DEFAULT_PUBLISH_COMMAND_TIMEOUT_MS;
|
|
169
|
+
const timer = setTimeout(() => {
|
|
170
|
+
this.publishCommandWatchdogs.delete(key);
|
|
171
|
+
this.logger.warn({ runId: run.id, projectId: run.projectId, issueId: run.linearIssueId, threadId, turnId, timeoutMs }, "Interrupting stuck git push command");
|
|
172
|
+
this.feed?.publish({
|
|
173
|
+
level: "error",
|
|
174
|
+
kind: "turn",
|
|
175
|
+
projectId: run.projectId,
|
|
176
|
+
stage: run.runType,
|
|
177
|
+
status: "interrupted",
|
|
178
|
+
summary: `Interrupted stuck publish command after ${Math.round(timeoutMs / 1000)}s`,
|
|
179
|
+
});
|
|
180
|
+
void this.options.interruptTurn?.({ threadId, turnId }).catch((error) => {
|
|
181
|
+
this.logger.warn({ runId: run.id, projectId: run.projectId, issueId: run.linearIssueId, threadId, turnId, error: formatError(error) }, "Failed to interrupt stuck git push command");
|
|
182
|
+
});
|
|
183
|
+
}, timeoutMs);
|
|
184
|
+
timer.unref?.();
|
|
185
|
+
this.publishCommandWatchdogs.set(key, timer);
|
|
186
|
+
}
|
|
187
|
+
clearPublishWatchdog(threadId, itemId) {
|
|
188
|
+
const key = publishWatchdogKey(threadId, itemId);
|
|
189
|
+
const timer = this.publishCommandWatchdogs.get(key);
|
|
190
|
+
if (!timer) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
clearTimeout(timer);
|
|
194
|
+
this.publishCommandWatchdogs.delete(key);
|
|
195
|
+
}
|
|
196
|
+
clearPublishWatchdogsForThread(threadId) {
|
|
197
|
+
for (const [key, timer] of this.publishCommandWatchdogs) {
|
|
198
|
+
if (!key.startsWith(`${threadId}:`)) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
clearTimeout(timer);
|
|
202
|
+
this.publishCommandWatchdogs.delete(key);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
maybeEmitProgress(notification, run) {
|
|
206
|
+
try {
|
|
207
|
+
this.linearSync.maybeEmitProgress(notification, run);
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
this.logger.warn({ runId: run.id, projectId: run.projectId, issueId: run.linearIssueId, method: notification.method, error: formatError(error) }, "Linear progress reporting failed");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
syncCodexPlan(notification, run) {
|
|
214
|
+
let issue;
|
|
215
|
+
try {
|
|
216
|
+
issue = this.db.issues.getIssue(run.projectId, run.linearIssueId);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
this.logger.warn({ runId: run.id, projectId: run.projectId, issueId: run.linearIssueId, method: notification.method, error: formatError(error) }, "Linear plan sync lookup failed");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (!issue)
|
|
223
|
+
return;
|
|
224
|
+
try {
|
|
225
|
+
void this.linearSync.syncCodexPlan(issue, notification.params).catch((error) => {
|
|
226
|
+
this.logger.warn({ runId: run.id, issueKey: issue.issueKey, projectId: run.projectId, issueId: run.linearIssueId, method: notification.method, error: formatError(error) }, "Linear plan sync failed");
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
this.logger.warn({ runId: run.id, issueKey: issue.issueKey, projectId: run.projectId, issueId: run.linearIssueId, method: notification.method, error: formatError(error) }, "Linear plan sync failed");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function formatError(error) {
|
|
235
|
+
return error instanceof Error ? error.message : String(error);
|
|
236
|
+
}
|
|
237
|
+
function publishWatchdogKey(threadId, itemId) {
|
|
238
|
+
return `${threadId}:${itemId}`;
|
|
239
|
+
}
|
|
240
|
+
function extractCommandText(command) {
|
|
241
|
+
if (typeof command === "string") {
|
|
242
|
+
return command;
|
|
243
|
+
}
|
|
244
|
+
if (Array.isArray(command)) {
|
|
245
|
+
return command.map((entry) => String(entry)).join(" ");
|
|
246
|
+
}
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
function isGitPushCommand(command) {
|
|
250
|
+
const normalized = command.replace(/\s+/g, " ").trim();
|
|
251
|
+
return /(?:^|[;&|({\s])git(?:\s+-C\s+\S+)?\s+push(?:\s|$)/.test(normalized);
|
|
252
|
+
}
|
|
253
|
+
function isTerminalItemUpdate(method, item) {
|
|
254
|
+
if (method !== "item/updated") {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
const status = typeof item.status === "string" ? item.status.toLowerCase() : "";
|
|
258
|
+
return status === "completed" || status === "failed" || status === "cancelled" || status === "canceled";
|
|
140
259
|
}
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -129,7 +129,7 @@ export class RunOrchestrator {
|
|
|
129
129
|
this.issueTriage = new IssueTriageService(codex, logger);
|
|
130
130
|
this.runFinalizer = new RunFinalizer(db, logger, this.linearSync, this.wakeDispatcher, this.leasePorts.withHeldLease, this.leasePorts.releaseLease, (lease, issue, runType, context, dedupeScope) => this.appendWakeEventWithLease(lease, issue, runType, context, dedupeScope), this.recoveryPorts.failRunAndClear, this.runCompletionPolicy, this.completionCheck, this.publicationRecap, feed);
|
|
131
131
|
this.runLauncher = new RunLauncher(config, db, codex, logger, this.worktreeManager);
|
|
132
|
-
this.runNotificationHandler = new RunNotificationHandler(config, db, logger, this.linearSync, this.runFinalizer, this.threadPorts.readThreadWithRetry, this.leasePorts.withHeldLease, this.leasePorts.heartbeatLease, this.leasePorts.releaseLease, feed);
|
|
132
|
+
this.runNotificationHandler = new RunNotificationHandler(config, db, logger, this.linearSync, this.runFinalizer, this.threadPorts.readThreadWithRetry, this.leasePorts.withHeldLease, this.leasePorts.heartbeatLease, this.leasePorts.releaseLease, feed, { interruptTurn: (options) => codex.interruptTurn(options) });
|
|
133
133
|
this.runRecovery = new RunRecoveryService(db, logger, this.linearSync, this.leasePorts.withHeldLease, this.leasePorts.getHeldLease, (lease, issue, runType, context, dedupeScope) => this.appendWakeEventWithLease(lease, issue, runType, context, dedupeScope), this.leasePorts.releaseLease, (projectId, issueId) => this.enqueueIssue(projectId, issueId), feed);
|
|
134
134
|
this.interruptedRunRecovery = new InterruptedRunRecovery(db, logger, this.linearSync, this.leasePorts.withHeldLease, this.leasePorts.releaseLease, this.recoveryPorts.failRunAndClear, this.recoveryPorts.restoreIdleWorktree, this.runCompletionPolicy, (projectId, issueId) => this.enqueueIssue(projectId, issueId), feed);
|
|
135
135
|
this.runReconciler = new RunReconciler(db, logger, linearProvider, this.linearSync, this.interruptedRunRecovery, this.runFinalizer, this.leasePorts.withHeldLease, this.leasePorts.releaseLease, this.threadPorts.readThreadWithRetry, this.recoveryPorts.recoverOrEscalate, (projectId) => this.config.projects.find((project) => project.id === projectId)?.github?.repoFullName, feed);
|
package/dist/service-runtime.js
CHANGED
|
@@ -65,7 +65,7 @@ export class ServiceRuntime {
|
|
|
65
65
|
}
|
|
66
66
|
getReadiness() {
|
|
67
67
|
return {
|
|
68
|
-
ready: this.ready && this.codex.isStarted() && this.linearConnected,
|
|
68
|
+
ready: this.ready && this.codex.isStarted() && this.linearConnected && this.githubAppAuthHealthy,
|
|
69
69
|
codexStarted: this.codex.isStarted(),
|
|
70
70
|
linearConnected: this.linearConnected,
|
|
71
71
|
githubAppAuthHealthy: this.githubAppAuthHealthy,
|
package/dist/service.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveGitHubAppCredentials, createGitHubAppTokenManager, getGitHubAppPaths, } from "./github-app-token.js";
|
|
2
|
-
import { applyGitHubCliAuthEnv, resolveGhBin } from "./github-cli-auth.js";
|
|
2
|
+
import { applyGitHubCliAuthEnv, resolveGhBin, verifyGitHubCliAuthEnv } from "./github-cli-auth.js";
|
|
3
3
|
import { remediateLeakedBotAuth } from "./github-auth-remediation.js";
|
|
4
4
|
import { GitHubWebhookHandler } from "./github-webhook-handler.js";
|
|
5
5
|
import { IssueQueryService } from "./issue-query-service.js";
|
|
@@ -149,8 +149,18 @@ export class PatchRelayService {
|
|
|
149
149
|
this.runtime.setGithubAppAuthHealthy(ghAuthStatus.healthy, ghAuthStatus.lastRefreshError ?? undefined);
|
|
150
150
|
if (!ghAuthStatus.healthy) {
|
|
151
151
|
this.logger.error({ ghAuthStatus }, "GitHub App auth is NOT healthy at startup — git/gh operations will fail until a token is minted");
|
|
152
|
+
throw new Error(`GitHub App auth is not healthy at startup: ${ghAuthStatus.lastRefreshError ?? "no fresh installation token"}`);
|
|
152
153
|
}
|
|
153
154
|
else {
|
|
155
|
+
try {
|
|
156
|
+
await verifyGitHubCliAuthEnv(process.env);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
160
|
+
this.runtime.setGithubAppAuthHealthy(false, msg);
|
|
161
|
+
this.logger.error({ error: msg, ghConfigDir }, "GitHub App auth smoke test failed — service will not accept work");
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
154
164
|
this.logger.info({ installationId: ghAuthStatus.installationId, expiresAt: ghAuthStatus.expiresAt }, "GitHub App auth ready — gh + git authenticate as the bot");
|
|
155
165
|
}
|
|
156
166
|
// Clean up credentials older versions persisted into managed repo configs.
|