merge-steward 0.8.12 → 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
- * (package-lock.json). If so, resolve by regenerating from the merged
32
- * package.json via `npm install --package-lock-only`.
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,12 +12,34 @@ function parseConflicts(stderr) {
12
12
  });
13
13
  return files.length > 0 ? files : undefined;
14
14
  }
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;
32
+ }
33
+ }
34
+ return undefined;
35
+ }
15
36
  export class ShellGitOperations {
16
37
  clonePath;
17
38
  repoFullName;
18
39
  gitBin;
19
40
  worktreeBase;
20
41
  botIdentity;
42
+ autoResolvePatterns = [];
21
43
  constructor(clonePath, repoFullName, gitBin = "git") {
22
44
  this.clonePath = clonePath;
23
45
  this.repoFullName = repoFullName;
@@ -27,6 +49,9 @@ export class ShellGitOperations {
27
49
  setBotIdentity(identity) {
28
50
  this.botIdentity = identity;
29
51
  }
52
+ setAutoResolvePatterns(patterns) {
53
+ this.autoResolvePatterns = patterns;
54
+ }
30
55
  git(args, opts) {
31
56
  return exec(this.gitBin, ["-C", this.clonePath, ...args], {
32
57
  timeoutMs: opts?.timeoutMs ?? 120_000,
@@ -98,7 +123,7 @@ export class ShellGitOperations {
98
123
  if (result.exitCode !== 0) {
99
124
  const conflictFiles = parseConflicts(result.stderr);
100
125
  // Lockfile-only conflicts can be auto-resolved by regenerating.
101
- if (await this.tryResolveLockfileConflict(wtPath)) {
126
+ if (await this.tryAutoResolveConflict(wtPath)) {
102
127
  const sha = (await this.gitIn(wtPath, ["rev-parse", "HEAD"])).stdout.trim();
103
128
  await this.git(["worktree", "remove", "--force", wtPath], { allowNonZero: true });
104
129
  return { success: true, sha };
@@ -114,25 +139,28 @@ export class ShellGitOperations {
114
139
  return { success: true, sha };
115
140
  }
116
141
  /**
117
- * During a merge conflict, check if the only unmerged files are lockfiles
118
- * (package-lock.json). If so, resolve by regenerating from the merged
119
- * package.json via `npm install --package-lock-only`.
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.
120
144
  */
121
- async tryResolveLockfileConflict(wtPath) {
145
+ async tryAutoResolveConflict(wtPath) {
146
+ if (this.autoResolvePatterns.length === 0)
147
+ return false;
122
148
  try {
123
149
  const unmerged = await this.gitIn(wtPath, ["diff", "--name-only", "--diff-filter=U"]);
124
150
  const files = unmerged.stdout.trim().split("\n").filter(Boolean);
125
- if (files.length === 0 || !files.every((f) => f.endsWith("package-lock.json"))) {
151
+ if (files.length === 0)
152
+ return false;
153
+ const regenerate = detectAutoResolveCommand(files, this.autoResolvePatterns);
154
+ if (!regenerate)
126
155
  return false;
127
- }
128
156
  for (const file of files) {
129
157
  await this.gitIn(wtPath, ["checkout", "--ours", "--", file]);
130
158
  }
131
- const npmResult = await exec("npm", ["install", "--package-lock-only"], {
159
+ const result = await exec(regenerate.bin, regenerate.args, {
132
160
  cwd: wtPath,
133
161
  timeoutMs: 60_000,
134
162
  });
135
- if (npmResult.exitCode !== 0)
163
+ if (result.exitCode !== 0)
136
164
  return false;
137
165
  for (const file of files) {
138
166
  await this.gitIn(wtPath, ["add", file]);
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.12",
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": {