merge-steward 0.8.8 → 0.8.10

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.
@@ -1,11 +1,17 @@
1
1
  import type { GitOperations, SpeculativeBranchBuilder } from "../interfaces.ts";
2
2
  import type { MergeResult } from "../types.ts";
3
+ export interface BotIdentity {
4
+ name: string;
5
+ email: string;
6
+ }
3
7
  export declare class ShellGitOperations implements GitOperations, SpeculativeBranchBuilder {
4
8
  private readonly clonePath;
5
9
  private readonly repoFullName;
6
10
  private readonly gitBin;
7
11
  private readonly worktreeBase;
12
+ private botIdentity;
8
13
  constructor(clonePath: string, repoFullName: string, gitBin?: string);
14
+ setBotIdentity(identity: BotIdentity): void;
9
15
  private git;
10
16
  /** Run a git command in a specific directory (worktree). */
11
17
  private gitIn;
@@ -17,12 +17,16 @@ export class ShellGitOperations {
17
17
  repoFullName;
18
18
  gitBin;
19
19
  worktreeBase;
20
+ botIdentity;
20
21
  constructor(clonePath, repoFullName, gitBin = "git") {
21
22
  this.clonePath = clonePath;
22
23
  this.repoFullName = repoFullName;
23
24
  this.gitBin = gitBin;
24
25
  this.worktreeBase = `${clonePath}-worktrees`;
25
26
  }
27
+ setBotIdentity(identity) {
28
+ this.botIdentity = identity;
29
+ }
26
30
  git(args, opts) {
27
31
  return exec(this.gitBin, ["-C", this.clonePath, ...args], {
28
32
  timeoutMs: opts?.timeoutMs ?? 120_000,
@@ -77,6 +81,11 @@ export class ShellGitOperations {
77
81
  // Create isolated worktree with spec branch starting at baseBranch.
78
82
  mkdirSync(this.worktreeBase, { recursive: true });
79
83
  await this.git(["worktree", "add", "-B", specName, wtPath, baseBranch]);
84
+ // Override git identity so merge commits are attributed to the steward, not the clone owner.
85
+ if (this.botIdentity) {
86
+ await this.gitIn(wtPath, ["config", "user.name", this.botIdentity.name]);
87
+ await this.gitIn(wtPath, ["config", "user.email", this.botIdentity.email]);
88
+ }
80
89
  // Merge PR into spec branch inside the worktree.
81
90
  // PR branches are always remote refs — use explicit origin/ prefix
82
91
  // because git DWIM doesn't reliably resolve bare names in worktrees.
@@ -21,6 +21,7 @@ export interface GitHubAppTokenManager extends RuntimeGitHubAuthProvider {
21
21
  start(): Promise<void>;
22
22
  stop(): void;
23
23
  }
24
+ export declare function resolveAppSlug(credentials: GitHubAppCredentials): Promise<string>;
24
25
  export declare function issueGitHubAppToken(credentials: GitHubAppCredentials, options?: {
25
26
  repoFullName?: string;
26
27
  }): Promise<{
@@ -30,6 +30,11 @@ function githubHeaders(token) {
30
30
  "X-GitHub-Api-Version": "2022-11-28",
31
31
  };
32
32
  }
33
+ export async function resolveAppSlug(credentials) {
34
+ const jwt = generateJwt(credentials.appId, credentials.privateKey);
35
+ const data = await fetchJson("https://api.github.com/app", { headers: githubHeaders(jwt) });
36
+ return data.slug;
37
+ }
33
38
  async function resolveInstallationIdForRepo(jwt, repoFullName) {
34
39
  const encodedRepo = repoFullName
35
40
  .split("/")
@@ -222,6 +222,8 @@ async function prepareEntry(ctx, entry, isHead, prevEntry) {
222
222
  }
223
223
  const specSha = result.sha ?? entry.headSha;
224
224
  emit(ctx, entry, "spec_build_succeeded", { specBranch: specName, ...(prevEntry ? { dependsOn: prevEntry.id } : {}) });
225
+ // Push spec to remote so CI can access it.
226
+ await ctx.git.push(specName, true);
225
227
  // Trigger CI on the spec branch.
226
228
  const runId = await ctx.ci.triggerRun(specName, specSha);
227
229
  emit(ctx, entry, "ci_triggered", { ciRunId: runId, specBranch: specName });
package/dist/server.js CHANGED
@@ -10,18 +10,20 @@ import { buildMultiRepoHttpServer } from "./http-multi.js";
10
10
  import { loadAllRepoConfigs } from "./install.js";
11
11
  import { parseHomeConfigObject } from "./steward-home.js";
12
12
  import { getMergeStewardPathLayout } from "./runtime-paths.js";
13
- import { createGitHubAppTokenManager, resolveGitHubAuthConfig } from "./github-auth.js";
13
+ import { createGitHubAppTokenManager, resolveGitHubAuthConfig, resolveAppSlug } from "./github-auth.js";
14
14
  import { discoverRepoSettings } from "./github-repo-discovery.js";
15
15
  import { resolveSecret } from "./resolve-secret.js";
16
16
  import { setRuntimeGitHubAuthProvider } from "./exec.js";
17
17
  import { readFileSync, existsSync } from "node:fs";
18
- async function createRepoInstance(config, logger) {
18
+ async function createRepoInstance(config, logger, botIdentity) {
19
19
  const repoUrl = `https://github.com/${config.repoFullName}.git`;
20
20
  const clone = new CloneManager(config.clonePath, repoUrl, config.repoFullName, config.gitBin, logger);
21
21
  await clone.ensureClone();
22
22
  await clone.fetch();
23
23
  const store = new SqliteStore(config.database.path);
24
24
  const git = new ShellGitOperations(clone.path, config.repoFullName, config.gitBin);
25
+ if (botIdentity)
26
+ git.setBotIdentity(botIdentity);
25
27
  const ci = new GitHubActionsRunner(config.repoFullName, config.requiredChecks);
26
28
  const github = new GitHubPRClient(config.repoFullName);
27
29
  const eviction = new GitHubCheckRunReporter(config.repoFullName, config.server.bind, config.server.port, config.server.publicBaseUrl, config.admissionLabel, config.mergeQueueCheckName);
@@ -41,6 +43,7 @@ export async function startMultiServer() {
41
43
  const configs = await loadAllRepoConfigs();
42
44
  const githubAuth = resolveGitHubAuthConfig();
43
45
  let githubAppTokenManager;
46
+ let botIdentity;
44
47
  let githubRuntimeStatus = {
45
48
  mode: "none",
46
49
  configured: false,
@@ -64,6 +67,17 @@ export async function startMultiServer() {
64
67
  setRuntimeGitHubAuthProvider(githubAppTokenManager);
65
68
  try {
66
69
  await githubAppTokenManager.start();
70
+ try {
71
+ const slug = await resolveAppSlug(githubAuth.credentials);
72
+ botIdentity = {
73
+ name: `${slug}[bot]`,
74
+ email: `${githubAuth.credentials.appId}+${slug}[bot]@users.noreply.github.com`,
75
+ };
76
+ logger.info({ botName: botIdentity.name }, "Resolved GitHub App bot identity");
77
+ }
78
+ catch {
79
+ logger.warn("Could not resolve GitHub App slug, merge commits will use clone owner identity");
80
+ }
67
81
  githubRuntimeStatus = {
68
82
  ...githubRuntimeStatus,
69
83
  ready: true,
@@ -97,7 +111,7 @@ export async function startMultiServer() {
97
111
  const instances = new Map();
98
112
  for (const config of configs) {
99
113
  logger.info({ repoId: config.repoId, repoFullName: config.repoFullName }, "Initializing repo");
100
- const instance = await createRepoInstance(config, logger.child({ repoId: config.repoId }));
114
+ const instance = await createRepoInstance(config, logger.child({ repoId: config.repoId }), botIdentity);
101
115
  instances.set(config.repoFullName, instance);
102
116
  }
103
117
  const app = await buildMultiRepoHttpServer({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "merge-steward",
3
- "version": "0.8.8",
3
+ "version": "0.8.10",
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": {