merge-steward 0.8.13 → 0.9.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.
package/dist/config.d.ts CHANGED
@@ -31,6 +31,10 @@ export declare const stewardConfigSchema: z.ZodObject<{
31
31
  admissionLabel: z.ZodDefault<z.ZodString>;
32
32
  mergeQueueCheckName: z.ZodDefault<z.ZodString>;
33
33
  excludeBranches: z.ZodDefault<z.ZodArray<z.ZodString>>;
34
+ autoResolvePatterns: z.ZodDefault<z.ZodArray<z.ZodObject<{
35
+ glob: z.ZodString;
36
+ command: z.ZodArray<z.ZodString>;
37
+ }, z.core.$strip>>>;
34
38
  webhookSecret: z.ZodOptional<z.ZodString>;
35
39
  }, z.core.$strip>;
36
40
  export type StewardConfig = z.infer<typeof stewardConfigSchema>;
package/dist/config.js CHANGED
@@ -33,6 +33,19 @@ export const stewardConfigSchema = z.object({
33
33
  mergeQueueCheckName: z.string().default(DEFAULT_MERGE_QUEUE_CHECK_NAME),
34
34
  /** Branch name patterns to exclude from admission (glob-style). */
35
35
  excludeBranches: z.array(z.string()).default(["release-please--*"]),
36
+ /**
37
+ * File patterns with regen commands for auto-resolving merge conflicts.
38
+ * When all conflicting files match a pattern, the command is run to regenerate them.
39
+ * Defaults to lockfile resolution for npm/pnpm/yarn.
40
+ */
41
+ autoResolvePatterns: z.array(z.object({
42
+ glob: z.string().min(1),
43
+ command: z.array(z.string()).min(1),
44
+ })).default([
45
+ { glob: "**/package-lock.json", command: ["npm", "install", "--package-lock-only"] },
46
+ { glob: "**/pnpm-lock.yaml", command: ["pnpm", "install", "--lockfile-only"] },
47
+ { glob: "**/yarn.lock", command: ["yarn", "install", "--mode", "update-lockfile"] },
48
+ ]),
36
49
  webhookSecret: z.string().optional(),
37
50
  });
38
51
  function readEnvFile(filePath) {
@@ -4,14 +4,20 @@ export interface BotIdentity {
4
4
  name: string;
5
5
  email: string;
6
6
  }
7
+ export interface AutoResolvePattern {
8
+ glob: string;
9
+ command: string[];
10
+ }
7
11
  export declare class ShellGitOperations implements GitOperations, SpeculativeBranchBuilder {
8
12
  private readonly clonePath;
9
13
  private readonly repoFullName;
10
14
  private readonly gitBin;
11
15
  private readonly worktreeBase;
12
16
  private botIdentity;
17
+ private autoResolvePatterns;
13
18
  constructor(clonePath: string, repoFullName: string, gitBin?: string);
14
19
  setBotIdentity(identity: BotIdentity): void;
20
+ setAutoResolvePatterns(patterns: AutoResolvePattern[]): void;
15
21
  private git;
16
22
  /** Run a git command in a specific directory (worktree). */
17
23
  private gitIn;
@@ -27,10 +33,9 @@ export declare class ShellGitOperations implements GitOperations, SpeculativeBra
27
33
  */
28
34
  buildSpeculative(prBranch: string, baseBranch: string, specName: string, mergeMessage?: string): Promise<MergeResult>;
29
35
  /**
30
- * During a merge conflict, check if the only unmerged files are lockfiles.
31
- * If so, resolve by regenerating from the merged manifest via the
32
- * appropriate package manager.
36
+ * During a merge conflict, check if all unmerged files match a configured
37
+ * auto-resolve pattern. If so, regenerate them via the pattern's command.
33
38
  */
34
- private tryResolveLockfileConflict;
39
+ private tryAutoResolveConflict;
35
40
  deleteSpeculative(specName: string): Promise<void>;
36
41
  }
@@ -12,16 +12,23 @@ function parseConflicts(stderr) {
12
12
  });
13
13
  return files.length > 0 ? files : undefined;
14
14
  }
15
- const LOCKFILE_REGEN = [
16
- { suffix: "package-lock.json", command: { bin: "npm", args: ["install", "--package-lock-only"] } },
17
- { suffix: "pnpm-lock.yaml", command: { bin: "pnpm", args: ["install", "--lockfile-only"] } },
18
- { suffix: "yarn.lock", command: { bin: "yarn", args: ["install", "--mode", "update-lockfile"] } },
19
- ];
20
- /** If all conflicting files are lockfiles of the same type, return the regen command. */
21
- function detectLockfileRegenCommand(conflictFiles) {
22
- for (const entry of LOCKFILE_REGEN) {
23
- if (conflictFiles.every((f) => f.endsWith(entry.suffix))) {
24
- return entry.command;
15
+ /** Check if a file path matches a glob pattern (simple suffix/wildcard match). */
16
+ function matchesGlob(filePath, pattern) {
17
+ if (pattern.startsWith("**/")) {
18
+ return filePath.endsWith(pattern.slice(2));
19
+ }
20
+ if (pattern.startsWith("*/")) {
21
+ const name = filePath.split("/").pop() ?? filePath;
22
+ return name === pattern.slice(2);
23
+ }
24
+ return filePath === pattern || filePath.endsWith(`/${pattern}`);
25
+ }
26
+ /** If all conflicting files match a single auto-resolve pattern, return its command. */
27
+ function detectAutoResolveCommand(conflictFiles, patterns) {
28
+ for (const pattern of patterns) {
29
+ if (conflictFiles.every((f) => matchesGlob(f, pattern.glob))) {
30
+ const [bin, ...args] = pattern.command;
31
+ return bin ? { bin, args } : undefined;
25
32
  }
26
33
  }
27
34
  return undefined;
@@ -32,6 +39,7 @@ export class ShellGitOperations {
32
39
  gitBin;
33
40
  worktreeBase;
34
41
  botIdentity;
42
+ autoResolvePatterns = [];
35
43
  constructor(clonePath, repoFullName, gitBin = "git") {
36
44
  this.clonePath = clonePath;
37
45
  this.repoFullName = repoFullName;
@@ -41,6 +49,9 @@ export class ShellGitOperations {
41
49
  setBotIdentity(identity) {
42
50
  this.botIdentity = identity;
43
51
  }
52
+ setAutoResolvePatterns(patterns) {
53
+ this.autoResolvePatterns = patterns;
54
+ }
44
55
  git(args, opts) {
45
56
  return exec(this.gitBin, ["-C", this.clonePath, ...args], {
46
57
  timeoutMs: opts?.timeoutMs ?? 120_000,
@@ -112,7 +123,7 @@ export class ShellGitOperations {
112
123
  if (result.exitCode !== 0) {
113
124
  const conflictFiles = parseConflicts(result.stderr);
114
125
  // Lockfile-only conflicts can be auto-resolved by regenerating.
115
- if (await this.tryResolveLockfileConflict(wtPath)) {
126
+ if (await this.tryAutoResolveConflict(wtPath)) {
116
127
  const sha = (await this.gitIn(wtPath, ["rev-parse", "HEAD"])).stdout.trim();
117
128
  await this.git(["worktree", "remove", "--force", wtPath], { allowNonZero: true });
118
129
  return { success: true, sha };
@@ -128,17 +139,18 @@ export class ShellGitOperations {
128
139
  return { success: true, sha };
129
140
  }
130
141
  /**
131
- * During a merge conflict, check if the only unmerged files are lockfiles.
132
- * If so, resolve by regenerating from the merged manifest via the
133
- * appropriate package manager.
142
+ * During a merge conflict, check if all unmerged files match a configured
143
+ * auto-resolve pattern. If so, regenerate them via the pattern's command.
134
144
  */
135
- async tryResolveLockfileConflict(wtPath) {
145
+ async tryAutoResolveConflict(wtPath) {
146
+ if (this.autoResolvePatterns.length === 0)
147
+ return false;
136
148
  try {
137
149
  const unmerged = await this.gitIn(wtPath, ["diff", "--name-only", "--diff-filter=U"]);
138
150
  const files = unmerged.stdout.trim().split("\n").filter(Boolean);
139
151
  if (files.length === 0)
140
152
  return false;
141
- const regenerate = detectLockfileRegenCommand(files);
153
+ const regenerate = detectAutoResolveCommand(files, this.autoResolvePatterns);
142
154
  if (!regenerate)
143
155
  return false;
144
156
  for (const file of files) {
package/dist/server.js CHANGED
@@ -24,6 +24,8 @@ async function createRepoInstance(config, logger, botIdentity) {
24
24
  const git = new ShellGitOperations(clone.path, config.repoFullName, config.gitBin);
25
25
  if (botIdentity)
26
26
  git.setBotIdentity(botIdentity);
27
+ if (config.autoResolvePatterns.length > 0)
28
+ git.setAutoResolvePatterns(config.autoResolvePatterns);
27
29
  const ci = new GitHubActionsRunner(config.repoFullName, config.requiredChecks);
28
30
  const github = new GitHubPRClient(config.repoFullName);
29
31
  const eviction = new GitHubCheckRunReporter(config.repoFullName, config.server.bind, config.server.port, config.server.publicBaseUrl, config.admissionLabel, config.mergeQueueCheckName);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "merge-steward",
3
- "version": "0.8.13",
3
+ "version": "0.9.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": {