merge-steward 0.27.0 → 0.28.0

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.
@@ -52,13 +52,12 @@ export async function handleAttach(parsed, stdout, runCommand) {
52
52
  const warnings = [];
53
53
  if (needsDiscovery) {
54
54
  try {
55
- const response = await fetchServiceRepoDiscovery(repoFullName, {
56
- ...(explicitBaseBranch
57
- ? { baseBranch: explicitBaseBranch }
58
- : existing?.baseBranch && !shouldDiscoverBaseBranch
59
- ? { baseBranch: existing.baseBranch }
60
- : {}),
61
- });
55
+ const discoveryOptions = explicitBaseBranch
56
+ ? { baseBranch: explicitBaseBranch }
57
+ : existing?.baseBranch && !shouldDiscoverBaseBranch
58
+ ? { baseBranch: existing.baseBranch }
59
+ : {};
60
+ const response = await fetchServiceRepoDiscovery(repoFullName, discoveryOptions);
62
61
  discovered = response.discovery;
63
62
  }
64
63
  catch (error) {
package/dist/exec.js CHANGED
@@ -58,6 +58,25 @@ function shouldRetryWithoutRuntimeGitHubAuth(command, githubEnv, failure) {
58
58
  }
59
59
  return (failure.stderr ?? "").includes(WORKFLOW_PERMISSION_REJECTION);
60
60
  }
61
+ // Markers that indicate GitHub rejected our credentials (stale/expired installation token).
62
+ const AUTH_FAILURE_MARKERS = [
63
+ "Bad credentials",
64
+ "Invalid username or token",
65
+ "Authentication failed",
66
+ "could not read Username",
67
+ "401 Unauthorized",
68
+ "HTTP 401",
69
+ ];
70
+ function isGitHubAuthFailure(command, githubEnv, failure) {
71
+ if (command !== "git" && command !== "gh")
72
+ return false;
73
+ if (Object.keys(githubEnv).length === 0)
74
+ return false;
75
+ if (failure.timedOut)
76
+ return false;
77
+ const text = `${failure.stderr ?? ""}\n${failure.stdout ?? ""}\n${failure.message ?? ""}`;
78
+ return AUTH_FAILURE_MARKERS.some((marker) => text.includes(marker));
79
+ }
61
80
  async function runExecFile(command, args, env, options) {
62
81
  return await new Promise((resolve, reject) => {
63
82
  execFile(command, args, {
@@ -103,7 +122,7 @@ async function runExecFile(command, args, env, options) {
103
122
  export async function exec(command, args, options) {
104
123
  const baseEnv = {
105
124
  ...process.env,
106
- ...(options?.env ?? {}),
125
+ ...options?.env,
107
126
  };
108
127
  const githubEnv = resolveGitHubCommandEnv(command, baseEnv, {
109
128
  ...(options?.githubRepoFullName ? { githubRepoFullName: options.githubRepoFullName } : {}),
@@ -118,6 +137,25 @@ export async function exec(command, args, options) {
118
137
  }
119
138
  catch (error) {
120
139
  const failure = error;
140
+ // Self-heal on broken auth: record the failure, proactively re-mint the installation
141
+ // token, and retry once with a fresh credential before giving up.
142
+ const repoFullName = options?.githubRepoFullName;
143
+ if (repoFullName
144
+ && isGitHubAuthFailure(command, githubEnv, failure)
145
+ && runtimeGitHubAuthProvider?.refreshTokenForRepo) {
146
+ runtimeGitHubAuthProvider.recordAuthFailure?.(repoFullName, (failure.stderr ?? failure.message ?? "").slice(0, 200));
147
+ try {
148
+ await runtimeGitHubAuthProvider.refreshTokenForRepo(repoFullName, "github_401");
149
+ const refreshedGithubEnv = resolveGitHubCommandEnv(command, baseEnv, {
150
+ githubRepoFullName: repoFullName,
151
+ runtimeAuthProvider: runtimeGitHubAuthProvider,
152
+ });
153
+ return await runExecFile(command, args, { ...baseEnv, ...refreshedGithubEnv }, options);
154
+ }
155
+ catch {
156
+ // Fall through to the original error / workflow-permission fallback below.
157
+ }
158
+ }
121
159
  if (!shouldRetryWithoutRuntimeGitHubAuth(command, githubEnv, failure)) {
122
160
  throw error;
123
161
  }
@@ -9,8 +9,7 @@ import type { CIStatus } from "../types.ts";
9
9
  export declare class GitHubActionsRunner implements CIRunner {
10
10
  private readonly repoFullName;
11
11
  private readonly getRequiredChecks;
12
- private readonly shouldRequireAllChecksOnEmptyRequiredSet;
13
- constructor(repoFullName: string, getRequiredChecks?: () => string[], shouldRequireAllChecksOnEmptyRequiredSet?: () => boolean);
12
+ constructor(repoFullName: string, getRequiredChecks?: () => string[], _shouldRequireAllChecksOnEmptyRequiredSet?: () => boolean);
14
13
  triggerRun(_branch: string, sha: string): Promise<string>;
15
14
  getStatus(runId: string): Promise<CIStatus>;
16
15
  cancelRun(_runId: string): Promise<void>;
@@ -11,11 +11,9 @@ function normalizeCheckName(name) {
11
11
  export class GitHubActionsRunner {
12
12
  repoFullName;
13
13
  getRequiredChecks;
14
- shouldRequireAllChecksOnEmptyRequiredSet;
15
- constructor(repoFullName, getRequiredChecks = () => [], shouldRequireAllChecksOnEmptyRequiredSet = () => false) {
14
+ constructor(repoFullName, getRequiredChecks = () => [], _shouldRequireAllChecksOnEmptyRequiredSet = () => false) {
16
15
  this.repoFullName = repoFullName;
17
16
  this.getRequiredChecks = getRequiredChecks;
18
- this.shouldRequireAllChecksOnEmptyRequiredSet = shouldRequireAllChecksOnEmptyRequiredSet;
19
17
  }
20
18
  async triggerRun(_branch, sha) {
21
19
  // CI is triggered by the push. Return the SHA as the poll key.
@@ -37,7 +35,6 @@ export class GitHubActionsRunner {
37
35
  const requiredChecks = this.getRequiredChecks();
38
36
  const normalizedRequired = requiredChecks.map(normalizeCheckName);
39
37
  const hasRequired = requiredChecks.length > 0;
40
- const requireAllChecks = !hasRequired && this.shouldRequireAllChecksOnEmptyRequiredSet();
41
38
  const relevant = hasRequired
42
39
  ? checkRuns.filter((c) => normalizedRequired.includes(normalizeCheckName(c.name)))
43
40
  : checkRuns;
@@ -14,12 +14,35 @@ export type GitHubAuthConfig = {
14
14
  } | {
15
15
  mode: "none";
16
16
  };
17
+ export type GitHubAuthRefreshReason = "startup" | "scheduled" | "before_use" | "github_401" | "manual";
18
+ export interface GitHubInstallationAuthStatus {
19
+ installationId: string;
20
+ repoFullNames: string[];
21
+ hasToken: boolean;
22
+ expiresAt: string | null;
23
+ lastRefreshAt: string | null;
24
+ lastRefreshError: string | null;
25
+ fresh: boolean;
26
+ }
27
+ export interface GitHubAuthRuntimeStatus {
28
+ ready: boolean;
29
+ lastRefreshAt: string | null;
30
+ lastRefreshError: string | null;
31
+ recentAuthFailureCount: number;
32
+ lastAuthFailureAt: string | null;
33
+ installations: GitHubInstallationAuthStatus[];
34
+ }
17
35
  export interface RuntimeGitHubAuthProvider {
18
36
  currentTokenForRepo(repoFullName?: string): string | undefined;
37
+ refreshTokenForRepo?(repoFullName: string, reason: GitHubAuthRefreshReason): Promise<void>;
38
+ recordAuthFailure?(repoFullName: string, message: string): void;
19
39
  }
20
40
  export interface GitHubAppTokenManager extends RuntimeGitHubAuthProvider {
21
41
  start(): Promise<void>;
22
42
  stop(): void;
43
+ refreshTokenForRepo(repoFullName: string, reason: GitHubAuthRefreshReason): Promise<void>;
44
+ recordAuthFailure(repoFullName: string, message: string): void;
45
+ authStatus(): GitHubAuthRuntimeStatus;
23
46
  }
24
47
  export declare function generateJwt(appId: string, privateKey: string): string;
25
48
  export declare function resolveAppSlug(credentials: GitHubAppCredentials): Promise<string>;
@@ -2,6 +2,9 @@ import { Buffer } from "node:buffer";
2
2
  import { createSign } from "node:crypto";
3
3
  import { resolveSecretWithSource } from "./resolve-secret.js";
4
4
  const TOKEN_REFRESH_MS = 30 * 60_000;
5
+ const TOKEN_EXPIRY_MARGIN_MS = 5 * 60_000;
6
+ const RECENT_AUTH_FAILURE_WINDOW_MS = 15 * 60_000;
7
+ const MAX_AUTH_FAILURES_TO_KEEP = 50;
5
8
  export function generateJwt(appId, privateKey) {
6
9
  const now = Math.floor(Date.now() / 1000);
7
10
  const header = Buffer.from(JSON.stringify({ alg: "RS256", typ: "JWT" })).toString("base64url");
@@ -36,10 +39,7 @@ export async function resolveAppSlug(credentials) {
36
39
  return data.slug;
37
40
  }
38
41
  async function resolveInstallationIdForRepo(jwt, repoFullName) {
39
- const encodedRepo = repoFullName
40
- .split("/")
41
- .map((segment) => encodeURIComponent(segment))
42
- .join("/");
42
+ const encodedRepo = repoFullName.split("/").map((segment) => encodeURIComponent(segment)).join("/");
43
43
  const data = await fetchJson(`https://api.github.com/repos/${encodedRepo}/installation`, { headers: githubHeaders(jwt) });
44
44
  return String(data.id);
45
45
  }
@@ -52,11 +52,11 @@ async function resolveFirstInstallationId(jwt) {
52
52
  return String(first.id);
53
53
  }
54
54
  async function fetchInstallationToken(jwt, installationId) {
55
- const data = await fetchJson(`https://api.github.com/app/installations/${installationId}/access_tokens`, {
56
- method: "POST",
57
- headers: githubHeaders(jwt),
58
- });
59
- return data.token;
55
+ const data = await fetchJson(`https://api.github.com/app/installations/${installationId}/access_tokens`, { method: "POST", headers: githubHeaders(jwt) });
56
+ return {
57
+ token: data.token,
58
+ ...(typeof data.expires_at === "string" ? { expiresAt: data.expires_at } : {}),
59
+ };
60
60
  }
61
61
  export async function issueGitHubAppToken(credentials, options) {
62
62
  const jwt = generateJwt(credentials.appId, credentials.privateKey);
@@ -64,7 +64,7 @@ export async function issueGitHubAppToken(credentials, options) {
64
64
  ?? (options?.repoFullName
65
65
  ? await resolveInstallationIdForRepo(jwt, options.repoFullName)
66
66
  : await resolveFirstInstallationId(jwt));
67
- const token = await fetchInstallationToken(jwt, installationId);
67
+ const { token } = await fetchInstallationToken(jwt, installationId);
68
68
  return { installationId, token };
69
69
  }
70
70
  export function resolveGitHubAppCredentials(env = process.env) {
@@ -85,10 +85,7 @@ export function resolveGitHubAppCredentials(env = process.env) {
85
85
  }
86
86
  export function resolveGitHubAuthConfig(env = process.env) {
87
87
  const app = resolveGitHubAppCredentials(env);
88
- if (app) {
89
- return { mode: "app", credentials: app };
90
- }
91
- return { mode: "none" };
88
+ return app ? { mode: "app", credentials: app } : { mode: "none" };
92
89
  }
93
90
  function unique(values) {
94
91
  return [...new Set(values)];
@@ -97,38 +94,120 @@ export function createGitHubAppTokenManager(credentials, repoFullNames, logger)
97
94
  let timer;
98
95
  const repoInstallationIds = new Map();
99
96
  const installationTokens = new Map();
97
+ const installationRefreshErrors = new Map();
98
+ const authFailures = [];
99
+ let lastRefreshAt = null;
100
+ let lastRefreshError = null;
101
+ function rememberInstallationRepo(installationId, repoFullName) {
102
+ repoInstallationIds.set(repoFullName, installationId);
103
+ }
104
+ function reposForInstallation(installationId) {
105
+ return repoFullNames.filter((repoFullName) => (repoInstallationIds.get(repoFullName) ?? credentials.installationId) === installationId);
106
+ }
107
+ function isTokenFresh(token) {
108
+ if (!token)
109
+ return false;
110
+ if (!token.expiresAt)
111
+ return true;
112
+ const expiresAtMs = Date.parse(token.expiresAt);
113
+ if (!Number.isFinite(expiresAtMs))
114
+ return false;
115
+ return expiresAtMs - Date.now() > TOKEN_EXPIRY_MARGIN_MS;
116
+ }
117
+ function recordRefreshSuccess(installationId, token) {
118
+ const refreshedAt = new Date().toISOString();
119
+ installationTokens.set(installationId, {
120
+ token: token.token,
121
+ ...(token.expiresAt ? { expiresAt: token.expiresAt } : {}),
122
+ lastRefreshAt: refreshedAt,
123
+ lastRefreshError: null,
124
+ });
125
+ installationRefreshErrors.delete(installationId);
126
+ lastRefreshAt = refreshedAt;
127
+ lastRefreshError = null;
128
+ }
129
+ function recordRefreshFailure(installationId, message) {
130
+ lastRefreshError = message;
131
+ if (!installationId)
132
+ return;
133
+ installationRefreshErrors.set(installationId, message);
134
+ const existing = installationTokens.get(installationId);
135
+ if (existing) {
136
+ installationTokens.set(installationId, { ...existing, lastRefreshError: message });
137
+ }
138
+ }
139
+ async function resolveInstallationForRepo(jwt, repoFullName) {
140
+ const existing = repoInstallationIds.get(repoFullName) ?? credentials.installationId;
141
+ if (existing) {
142
+ rememberInstallationRepo(existing, repoFullName);
143
+ return existing;
144
+ }
145
+ const installationId = await resolveInstallationIdForRepo(jwt, repoFullName);
146
+ rememberInstallationRepo(installationId, repoFullName);
147
+ logger.info({ repoFullName, installationId }, "Resolved GitHub App installation for repo");
148
+ return installationId;
149
+ }
150
+ async function refreshInstallation(installationId, jwt, reason) {
151
+ const token = await fetchInstallationToken(jwt, installationId);
152
+ recordRefreshSuccess(installationId, token);
153
+ // Transparency: every rotation is logged so operators can see gh/git auth staying fresh.
154
+ logger.info({ installationId, expiresAt: token.expiresAt ?? null, reason }, "Rotated GitHub App installation token (gh + git authenticate as the bot with a fresh token)");
155
+ }
156
+ function escalateIfDegraded() {
157
+ const status = authStatus();
158
+ if (!status.ready) {
159
+ logger.error({ lastRefreshError: status.lastRefreshError, installations: status.installations }, "GitHub App auth is degraded — gh/git operations will fail until a fresh token is minted");
160
+ }
161
+ }
100
162
  async function refresh(options) {
101
163
  const throwOnError = options?.throwOnError ?? false;
164
+ const reason = options?.reason ?? "scheduled";
102
165
  try {
103
166
  const jwt = generateJwt(credentials.appId, credentials.privateKey);
104
167
  const installationIds = new Set();
105
- if (credentials.installationId) {
168
+ if (credentials.installationId)
106
169
  installationIds.add(credentials.installationId);
107
- }
108
170
  for (const repoFullName of repoFullNames) {
109
- let installationId = repoInstallationIds.get(repoFullName) ?? credentials.installationId;
110
- if (!installationId) {
111
- installationId = await resolveInstallationIdForRepo(jwt, repoFullName);
112
- repoInstallationIds.set(repoFullName, installationId);
113
- logger.info({ repoFullName, installationId }, "Resolved GitHub App installation for repo");
114
- }
171
+ const installationId = await resolveInstallationForRepo(jwt, repoFullName);
115
172
  installationIds.add(installationId);
116
173
  }
117
174
  for (const installationId of unique(installationIds)) {
118
- const token = await fetchInstallationToken(jwt, installationId);
119
- installationTokens.set(installationId, token);
120
- }
121
- if (installationTokens.size === 0 && !credentials.installationId && repoFullNames.length === 0) {
122
- logger.warn("GitHub App auth configured, but no repositories are attached yet");
175
+ try {
176
+ await refreshInstallation(installationId, jwt, reason);
177
+ }
178
+ catch (error) {
179
+ const message = error instanceof Error ? error.message : String(error);
180
+ recordRefreshFailure(installationId, message);
181
+ if (throwOnError)
182
+ throw error;
183
+ logger.warn({ installationId, error: message }, "Failed to refresh GitHub App installation token");
184
+ }
123
185
  }
124
186
  }
125
187
  catch (error) {
126
188
  const message = error instanceof Error ? error.message : String(error);
127
- if (throwOnError) {
189
+ recordRefreshFailure(undefined, message);
190
+ if (throwOnError)
128
191
  throw new Error(`Failed to initialize GitHub App auth: ${message}`);
129
- }
130
192
  logger.warn({ error: message }, "Failed to refresh GitHub App installation token");
131
193
  }
194
+ finally {
195
+ escalateIfDegraded();
196
+ }
197
+ }
198
+ async function refreshTokenForRepo(repoFullName, reason) {
199
+ const jwt = generateJwt(credentials.appId, credentials.privateKey);
200
+ let installationId;
201
+ try {
202
+ installationId = await resolveInstallationForRepo(jwt, repoFullName);
203
+ await refreshInstallation(installationId, jwt, reason);
204
+ }
205
+ catch (error) {
206
+ const message = error instanceof Error ? error.message : String(error);
207
+ recordRefreshFailure(installationId, message);
208
+ logger.warn({ repoFullName, installationId, reason, error: message }, "Failed to refresh GitHub App installation token");
209
+ throw error;
210
+ }
132
211
  }
133
212
  function schedule() {
134
213
  timer = setTimeout(() => {
@@ -136,29 +215,83 @@ export function createGitHubAppTokenManager(credentials, repoFullNames, logger)
136
215
  }, TOKEN_REFRESH_MS);
137
216
  timer.unref?.();
138
217
  }
218
+ function recentAuthFailures() {
219
+ const cutoff = Date.now() - RECENT_AUTH_FAILURE_WINDOW_MS;
220
+ return authFailures.filter((entry) => Date.parse(entry.at) >= cutoff);
221
+ }
222
+ function currentTokenForRepo(repoFullName) {
223
+ if (repoFullName) {
224
+ const installationId = repoInstallationIds.get(repoFullName) ?? credentials.installationId;
225
+ if (installationId) {
226
+ const token = installationTokens.get(installationId);
227
+ return token && isTokenFresh(token) ? token.token : undefined;
228
+ }
229
+ }
230
+ if (credentials.installationId) {
231
+ const token = installationTokens.get(credentials.installationId);
232
+ return token && isTokenFresh(token) ? token.token : undefined;
233
+ }
234
+ const first = installationTokens.values().next();
235
+ if (first.done)
236
+ return undefined;
237
+ const token = first.value;
238
+ return isTokenFresh(token) ? token.token : undefined;
239
+ }
240
+ function authStatus() {
241
+ const installationIds = unique([
242
+ ...installationTokens.keys(),
243
+ ...installationRefreshErrors.keys(),
244
+ ...(credentials.installationId ? [credentials.installationId] : []),
245
+ ...repoFullNames.flatMap((repoFullName) => {
246
+ const installationId = repoInstallationIds.get(repoFullName);
247
+ return installationId ? [installationId] : [];
248
+ }),
249
+ ]);
250
+ const installations = installationIds.map((installationId) => {
251
+ const token = installationTokens.get(installationId);
252
+ const refreshError = token?.lastRefreshError ?? installationRefreshErrors.get(installationId) ?? null;
253
+ return {
254
+ installationId,
255
+ repoFullNames: reposForInstallation(installationId),
256
+ hasToken: Boolean(token),
257
+ expiresAt: token?.expiresAt ?? null,
258
+ lastRefreshAt: token?.lastRefreshAt ?? null,
259
+ lastRefreshError: refreshError,
260
+ fresh: isTokenFresh(token) && !refreshError,
261
+ };
262
+ });
263
+ const failures = recentAuthFailures();
264
+ const missingRepoToken = repoFullNames.some((repoFullName) => !currentTokenForRepo(repoFullName));
265
+ const tokenRefreshFailing = installations.some((installation) => installation.lastRefreshError !== null && !installation.fresh);
266
+ return {
267
+ ready: !missingRepoToken && !tokenRefreshFailing,
268
+ lastRefreshAt,
269
+ lastRefreshError,
270
+ recentAuthFailureCount: failures.length,
271
+ lastAuthFailureAt: failures.at(-1)?.at ?? null,
272
+ installations,
273
+ };
274
+ }
139
275
  return {
140
276
  async start() {
141
- await refresh({ throwOnError: true });
277
+ await refresh({ throwOnError: true, reason: "startup" });
142
278
  schedule();
143
279
  },
144
280
  stop() {
145
- if (timer) {
281
+ if (timer)
146
282
  clearTimeout(timer);
147
- timer = undefined;
148
- }
283
+ timer = undefined;
149
284
  },
150
- currentTokenForRepo(repoFullName) {
151
- if (repoFullName) {
152
- const installationId = repoInstallationIds.get(repoFullName) ?? credentials.installationId;
153
- if (installationId) {
154
- return installationTokens.get(installationId);
155
- }
156
- }
157
- if (credentials.installationId) {
158
- return installationTokens.get(credentials.installationId);
285
+ currentTokenForRepo,
286
+ async refreshTokenForRepo(repoFullName, reason) {
287
+ await refreshTokenForRepo(repoFullName, reason);
288
+ },
289
+ recordAuthFailure(repoFullName, message) {
290
+ authFailures.push({ repoFullName, message, at: new Date().toISOString() });
291
+ if (authFailures.length > MAX_AUTH_FAILURES_TO_KEEP) {
292
+ authFailures.splice(0, authFailures.length - MAX_AUTH_FAILURES_TO_KEEP);
159
293
  }
160
- const first = installationTokens.values().next();
161
- return first.done ? undefined : first.value;
162
294
  },
295
+ authStatus,
163
296
  };
164
297
  }
package/dist/install.js CHANGED
@@ -3,7 +3,7 @@ import { existsSync } from "node:fs";
3
3
  import { mkdir, readFile, writeFile, readdir } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
5
  import { DEFAULT_MERGE_QUEUE_CHECK_NAME, parseConfig } from "./config.js";
6
- import { getBuiltCliEntryPath, getDefaultConfigPath, getDefaultRepoConfigDir, getDefaultRuntimeEnvPath, getDefaultServiceEnvPath, getDefaultStateDir, getMergeStewardPathLayout, getRepoConfigPath, getSystemdUnitPath, readBundledAsset } from "./runtime-paths.js";
6
+ import { getBuiltCliEntryPath, getDefaultConfigPath, getDefaultRuntimeEnvPath, getDefaultServiceEnvPath, getDefaultStateDir, getMergeStewardPathLayout, getRepoConfigPath, getSystemdUnitPath, readBundledAsset } from "./runtime-paths.js";
7
7
  import { parseHomeConfigObject, stringifyJson } from "./steward-home.js";
8
8
  function renderTemplate(template, replacements) {
9
9
  const home = homedir();
@@ -53,7 +53,7 @@ async function applyPublicBaseUrlToConfig(configPath, publicBaseUrl) {
53
53
  const next = {
54
54
  ...document,
55
55
  server: {
56
- ...(document.server ?? {}),
56
+ ...document.server,
57
57
  public_base_url: publicBaseUrl,
58
58
  },
59
59
  };
@@ -1,5 +1,5 @@
1
1
  import { TERMINAL_STATUSES } from "./types.js";
2
- import { CLEAN_CI, CLEAN_SPEC, emit } from "./reconciler-core.js";
2
+ import { emit } from "./reconciler-core.js";
3
3
  import { sanitizeEntry } from "./reconciler-sanitize.js";
4
4
  import { prepareEntry } from "./reconciler-prepare.js";
5
5
  import { checkValidation } from "./reconciler-validate.js";
package/dist/server.js CHANGED
@@ -13,7 +13,7 @@ import { parseHomeConfigObject } from "./steward-home.js";
13
13
  import { getMergeStewardPathLayout } from "./runtime-paths.js";
14
14
  import { createGitHubAppTokenManager, generateJwt, resolveGitHubAuthConfig, resolveAppSlug } from "./github-auth.js";
15
15
  import { discoverRepoSettings } from "./github-repo-discovery.js";
16
- import { resolveSecret, resolveSecretWithSource } from "./resolve-secret.js";
16
+ import { resolveSecretWithSource } from "./resolve-secret.js";
17
17
  import { setRuntimeGitHubAuthProvider } from "./exec.js";
18
18
  import { readFileSync, existsSync } from "node:fs";
19
19
  async function createRepoInstance(config, policy, logger, botIdentity) {
@@ -158,9 +158,8 @@ export async function startMultiServer() {
158
158
  if (githubAuth.mode !== "app") {
159
159
  throw new Error("GitHub App auth is not configured in the merge-steward service.");
160
160
  }
161
- return await discoverRepoSettings(githubAuth.credentials, params.repoFullName, {
162
- ...(params.baseBranch ? { baseBranch: params.baseBranch } : {}),
163
- });
161
+ const discoveryOptions = params.baseBranch ? { baseBranch: params.baseBranch } : {};
162
+ return await discoverRepoSettings(githubAuth.credentials, params.repoFullName, discoveryOptions);
164
163
  },
165
164
  async checkRepoAccess(params) {
166
165
  if (!githubAppTokenManager) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "merge-steward",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "Serial merge queue for GitHub — rebase, CI-gate, and merge PRs one at a time",
5
5
  "type": "module",
6
6
  "repository": {