relacher 0.0.4-rc.6 → 0.0.4

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.
@@ -10,7 +10,7 @@ export declare function evaluateCommitsBump(commits: Commit[], sizes: SizePatter
10
10
  export declare function topologicalSort(items: IntermediateReport[]): IntermediateReport[];
11
11
  export declare function propagateBumps(sorted: IntermediateReport[], rules?: CascadeRules): void;
12
12
  export declare function propagateCoupledBumps(sorted: IntermediateReport[]): void;
13
- export declare function initReportItems(cargoDeps: DependencyConfig[], vcs: VcsProvider, sizes: SizePatterns, cwd: string): Promise<IntermediateReport[]>;
13
+ export declare function initReportItems(cargoDeps: DependencyConfig[], vcs: VcsProvider, sizes: SizePatterns, cwd: string, excludeNestedWatches?: boolean): Promise<IntermediateReport[]>;
14
14
  export declare function prepare(cargoDeps: DependencyConfig[], vcs: VcsProvider, options?: PrepareOptions): Promise<DependencyUpdateReport[]>;
15
15
  export declare function finalizeReports(sorted: IntermediateReport[]): DependencyUpdateReport[];
16
16
  export declare function mapResolvedUpdates(updates: UpdateAction[], newVersion: string, crateCommits: Commit[], globalCommits: Commit[]): UpdateActionResolved[];
@@ -39,10 +39,71 @@ export interface CascadeRules {
39
39
  minor?: Record<BumpSize, BumpSize>;
40
40
  major?: Record<BumpSize, BumpSize>;
41
41
  }
42
+ /**
43
+ * Configuration options for the `prepare` process.
44
+ */
42
45
  export interface PrepareOptions {
46
+ /**
47
+ * The directory from which to run VCS commands and locate repository files.
48
+ * Defaults to `process.cwd()`.
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * const options = { cwd: "/path/to/my-workspace" };
53
+ * ```
54
+ */
55
+ cwd?: string;
56
+ /**
57
+ * Custom regular expression patterns to match commit messages and categorize
58
+ * them into Semantic Versioning bump sizes (major, minor, patch, or skip).
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * const options = {
63
+ * sizes: {
64
+ * major: { pattern: "^BREAKING CHANGE" },
65
+ * minor: { pattern: "^feat|^revert" },
66
+ * patch: { pattern: "^fix|^refactor" },
67
+ * skip: { pattern: "^chore|^docs" }
68
+ * }
69
+ * };
70
+ * ```
71
+ */
43
72
  sizes?: SizePatterns;
73
+ /**
74
+ * Custom cascade rules defining how semver bumps propagate upwards from
75
+ * dependencies to parent packages.
76
+ *
77
+ * For example, this decides if a `minor` bump in a sub-crate should trigger a
78
+ * `patch` bump in the workspace root package.
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * const options = {
83
+ * cascade: {
84
+ * skip: { major: "patch", minor: "patch", patch: "patch" },
85
+ * patch: { major: "patch", minor: "patch", patch: "patch" },
86
+ * minor: { major: "minor", minor: "minor", patch: "minor" },
87
+ * major: { major: "major", minor: "major", patch: "major" }
88
+ * }
89
+ * };
90
+ * ```
91
+ */
44
92
  cascade?: CascadeRules;
45
- cwd?: string;
93
+ /**
94
+ * Automatically subtract watch paths of sub-packages when they are physically nested
95
+ * inside a parent package's watch path (unless they are explicitly coupled).
96
+ *
97
+ * This prevents commits made strictly inside a sub-package (such as a procedural macro
98
+ * crate inside its parent crate folder) from incorrectly triggering a commit-based bump
99
+ * on the parent package itself.
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * const options = { excludeNestedWatches: true };
104
+ * ```
105
+ */
106
+ excludeNestedWatches?: boolean;
46
107
  }
47
108
  export interface DependencyUpdateReport {
48
109
  name: string;
@@ -2,13 +2,13 @@ import type { Commit, VcsProvider } from '../types';
2
2
  export declare function runGit(cmd: string, cwd: string): string;
3
3
  export declare function findLatestTag(tags: string[], prefix: string): string | null;
4
4
  export declare function resolveGitTag(name: string, allTags: string[]): string | null;
5
- export declare function getGitCommits(name: string, watch: string[], allTags: string[], cwd: string): Commit[];
5
+ export declare function getGitCommits(name: string, watch: string[], allTags: string[], cwd: string, exclude?: string[]): Commit[];
6
6
  export declare class GitVcsProvider implements VcsProvider {
7
7
  private cwd;
8
8
  private allTags;
9
9
  constructor(cwd: string);
10
10
  private getAllTags;
11
- getCommits(name: string, watch: string[]): Promise<Commit[]>;
11
+ getCommits(name: string, watch: string[], exclude?: string[]): Promise<Commit[]>;
12
12
  getLatestTag(name?: string): Promise<string | null>;
13
13
  commit(message: string): Promise<void>;
14
14
  tag(tagName: string): Promise<void>;
@@ -1,6 +1,6 @@
1
1
  import type { Commit } from '../types';
2
2
  export interface VcsProvider {
3
- getCommits(name: string, watch: string[]): Promise<Commit[]>;
3
+ getCommits(name: string, watch: string[], exclude?: string[]): Promise<Commit[]>;
4
4
  getLatestTag(name?: string): Promise<string | null>;
5
5
  commit(message: string): Promise<void>;
6
6
  tag(tagName: string): Promise<void>;
@@ -2,13 +2,13 @@ import type { Commit, VcsProvider } from '../types';
2
2
  export declare function runJj(cmd: string, cwd: string): string;
3
3
  export declare function findLatestJjTag(tags: string[], prefix: string): string | null;
4
4
  export declare function resolveJjTag(name: string, allTags: string[]): string | null;
5
- export declare function getJjCommits(name: string, watch: string[], allTags: string[], cwd: string): Commit[];
5
+ export declare function getJjCommits(name: string, watch: string[], allTags: string[], cwd: string, exclude?: string[]): Commit[];
6
6
  export declare class JjVcsProvider implements VcsProvider {
7
7
  private cwd;
8
8
  private allTags;
9
9
  constructor(cwd: string);
10
10
  private getAllTags;
11
- getCommits(name: string, watch: string[]): Promise<Commit[]>;
11
+ getCommits(name: string, watch: string[], exclude?: string[]): Promise<Commit[]>;
12
12
  getLatestTag(name?: string): Promise<string | null>;
13
13
  commit(message: string): Promise<void>;
14
14
  tag(tagName: string): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relacher",
3
- "version": "0.0.4-rc.6",
3
+ "version": "0.0.4",
4
4
  "description": "Scriptable release orchestration library for monorepos",
5
5
  "type": "module",
6
6
  "private": false,
@@ -202,8 +202,8 @@ function createLockUpdate(selfName: string, hasLock: boolean) {
202
202
  }).skipIf(() => !hasLock);
203
203
  }
204
204
 
205
- function createWorkspaceDepUpdate(selfName: string) {
206
- return tomlUpdate('Cargo.toml', (doc, report) => {
205
+ function createWorkspaceDepUpdate(rootCargoPath: string, selfName: string) {
206
+ return tomlUpdate(rootCargoPath, (doc, report) => {
207
207
  const dep = doc.workspace?.dependencies?.[selfName];
208
208
  if (typeof dep === 'object' && dep.version) {
209
209
  dep.version = report.newVersion;
@@ -211,9 +211,9 @@ function createWorkspaceDepUpdate(selfName: string) {
211
211
  doc.workspace.dependencies[selfName] = report.newVersion;
212
212
  }
213
213
  }).skipIf((cwd) => {
214
- // Only run this update action if the package is declared in the root workspace dependencies list
214
+ // Only apply if the workspace dependencies definition contains the dependency
215
215
  try {
216
- const cargoPath = path.join(cwd, 'Cargo.toml');
216
+ const cargoPath = path.resolve(cwd, rootCargoPath);
217
217
  if (fs.existsSync(cargoPath)) {
218
218
  const doc = parse(fs.readFileSync(cargoPath, 'utf8')) as any;
219
219
  return !doc.workspace?.dependencies?.[selfName];
@@ -226,10 +226,10 @@ function createWorkspaceDepUpdate(selfName: string) {
226
226
  function buildCrateConfig(
227
227
  info: any,
228
228
  workspaceRoot: string,
229
+ rootCargoPath: string,
229
230
  localNames: Set<string>,
230
231
  hasLock: boolean,
231
232
  wsDeps: Record<string, any>,
232
- _options?: CargoBuilderOptions,
233
233
  ): DependencyConfig {
234
234
  const relPath = path.relative(workspaceRoot, info.memberPath);
235
235
  const posixPath = relPath.split(path.sep).join(path.posix.sep);
@@ -246,7 +246,7 @@ function buildCrateConfig(
246
246
  updates: [
247
247
  createSelfUpdate(relCargo, info.name),
248
248
  createLockUpdate(info.name, hasLock),
249
- createWorkspaceDepUpdate(info.name),
249
+ createWorkspaceDepUpdate(rootCargoPath, info.name),
250
250
  ],
251
251
  };
252
252
  }
@@ -280,13 +280,28 @@ export function cargoDeps(cwd: string, options?: CargoBuilderOptions): Dependenc
280
280
  const scanner = scanWorkspace(workspaceRoot, rootDoc);
281
281
  const hasLock = fs.existsSync(path.join(workspaceRoot, 'Cargo.lock'));
282
282
  const localNames = new Set(scanner.crateInfos.map((c) => c.name));
283
+
284
+ // Calculate relative path from executing cwd to the workspace root Cargo.toml
285
+ const relWorkspaceRoot = path.relative(cwd, workspaceRoot);
286
+ const rootCargoPath = relWorkspaceRoot
287
+ ? path.join(relWorkspaceRoot, 'Cargo.toml')
288
+ : 'Cargo.toml';
289
+
283
290
  const configs = scanner.crateInfos.map((info) =>
284
- buildCrateConfig(info, workspaceRoot, localNames, hasLock, scanner.workspaceDeps, options),
291
+ buildCrateConfig(
292
+ info,
293
+ workspaceRoot,
294
+ rootCargoPath,
295
+ localNames,
296
+ hasLock,
297
+ scanner.workspaceDeps,
298
+ ),
285
299
  );
286
300
  const list = decorateList(configs) as any;
287
301
  list.couple = (a: string, b: string) => (coupleConfigs(configs, a, b) ? list : list);
288
302
  return list;
289
- } catch {
303
+ } catch (e) {
304
+ console.warn(e);
290
305
  return decorateList([]);
291
306
  }
292
307
  }
package/src/prepare.ts CHANGED
@@ -163,6 +163,7 @@ export async function initReportItems(
163
163
  vcs: VcsProvider,
164
164
  sizes: SizePatterns,
165
165
  cwd: string,
166
+ excludeNestedWatches = false,
166
167
  ): Promise<IntermediateReport[]> {
167
168
  const reports: IntermediateReport[] = [];
168
169
  for (const dep of cargoDeps) {
@@ -170,7 +171,38 @@ export async function initReportItems(
170
171
  const isMissingVersion = currentVersion === null;
171
172
  const actualVersion = currentVersion || '0.0.0';
172
173
 
173
- const commits = await vcs.getCommits(dep.name, dep.watch || []);
174
+ const exclude: string[] = [];
175
+ const watchPaths = dep.watch || [];
176
+
177
+ // Only compute exclusions if the option is explicitly enabled
178
+ if (excludeNestedWatches) {
179
+ for (const other of cargoDeps) {
180
+ if (other.name === dep.name) continue;
181
+
182
+ // Check if coupled
183
+ const isCoupled =
184
+ ((dep as any).coupled || []).includes(other.name) ||
185
+ ((other as any).coupled || []).includes(dep.name);
186
+ if (isCoupled) continue;
187
+
188
+ const otherWatchPaths = other.watch || [];
189
+ for (const wp of watchPaths) {
190
+ const wpNorm = path.normalize(wp).replace(/\\/g, '/');
191
+ for (const owp of otherWatchPaths) {
192
+ const owpNorm = path.normalize(owp).replace(/\\/g, '/');
193
+ const relative = path.relative(wpNorm, owpNorm);
194
+
195
+ const isNested =
196
+ relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative);
197
+ if (isNested) {
198
+ exclude.push(owp);
199
+ }
200
+ }
201
+ }
202
+ }
203
+ }
204
+
205
+ const commits = await vcs.getCommits(dep.name, watchPaths, exclude);
174
206
  const selfBump = evaluateCommitsBump(commits, sizes);
175
207
 
176
208
  let isErroneous = isMissingVersion;
@@ -211,7 +243,7 @@ export async function prepare(
211
243
  ): Promise<DependencyUpdateReport[]> {
212
244
  const cwd = options.cwd || process.cwd();
213
245
  const sizes = options.sizes || defaultSizes;
214
- const items = await initReportItems(cargoDeps, vcs, sizes, cwd);
246
+ const items = await initReportItems(cargoDeps, vcs, sizes, cwd, options.excludeNestedWatches);
215
247
  const sorted = topologicalSort(items);
216
248
  propagateBumps(sorted, options.cascade);
217
249
  propagateCoupledBumps(sorted);
package/src/types.ts CHANGED
@@ -47,10 +47,74 @@ export interface CascadeRules {
47
47
  major?: Record<BumpSize, BumpSize>;
48
48
  }
49
49
 
50
+ /**
51
+ * Configuration options for the `prepare` process.
52
+ */
50
53
  export interface PrepareOptions {
54
+ /**
55
+ * The directory from which to run VCS commands and locate repository files.
56
+ * Defaults to `process.cwd()`.
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * const options = { cwd: "/path/to/my-workspace" };
61
+ * ```
62
+ */
63
+ cwd?: string;
64
+
65
+ /**
66
+ * Custom regular expression patterns to match commit messages and categorize
67
+ * them into Semantic Versioning bump sizes (major, minor, patch, or skip).
68
+ *
69
+ * @example
70
+ * ```typescript
71
+ * const options = {
72
+ * sizes: {
73
+ * major: { pattern: "^BREAKING CHANGE" },
74
+ * minor: { pattern: "^feat|^revert" },
75
+ * patch: { pattern: "^fix|^refactor" },
76
+ * skip: { pattern: "^chore|^docs" }
77
+ * }
78
+ * };
79
+ * ```
80
+ */
51
81
  sizes?: SizePatterns;
82
+
83
+ /**
84
+ * Custom cascade rules defining how semver bumps propagate upwards from
85
+ * dependencies to parent packages.
86
+ *
87
+ * For example, this decides if a `minor` bump in a sub-crate should trigger a
88
+ * `patch` bump in the workspace root package.
89
+ *
90
+ * @example
91
+ * ```typescript
92
+ * const options = {
93
+ * cascade: {
94
+ * skip: { major: "patch", minor: "patch", patch: "patch" },
95
+ * patch: { major: "patch", minor: "patch", patch: "patch" },
96
+ * minor: { major: "minor", minor: "minor", patch: "minor" },
97
+ * major: { major: "major", minor: "major", patch: "major" }
98
+ * }
99
+ * };
100
+ * ```
101
+ */
52
102
  cascade?: CascadeRules;
53
- cwd?: string;
103
+
104
+ /**
105
+ * Automatically subtract watch paths of sub-packages when they are physically nested
106
+ * inside a parent package's watch path (unless they are explicitly coupled).
107
+ *
108
+ * This prevents commits made strictly inside a sub-package (such as a procedural macro
109
+ * crate inside its parent crate folder) from incorrectly triggering a commit-based bump
110
+ * on the parent package itself.
111
+ *
112
+ * @example
113
+ * ```typescript
114
+ * const options = { excludeNestedWatches: true };
115
+ * ```
116
+ */
117
+ excludeNestedWatches?: boolean;
54
118
  }
55
119
 
56
120
  export interface DependencyUpdateReport {
@@ -66,9 +66,9 @@ export function defaultChangelogTemplate({ version, date, commits }: ChangelogCo
66
66
 
67
67
  if (groups.breaking.length)
68
68
  block += `### ⚠️ BREAKING CHANGES\n${formatGroup(groups.breaking)}\n\n`;
69
- if (groups.feat.length) block += `### ✨ Features\n${formatGroup(groups.feat)}\n\n`;
70
- if (groups.fix.length) block += `### 🐛 Bug Fixes\n${formatGroup(groups.fix)}\n\n`;
71
- if (groups.other.length) block += `### 🛠 Other Changes\n${formatGroup(groups.other)}\n\n`;
69
+ if (groups.feat.length) block += `### Features\n${formatGroup(groups.feat)}\n\n`;
70
+ if (groups.fix.length) block += `### Bug Fixes\n${formatGroup(groups.fix)}\n\n`;
71
+ if (groups.other.length) block += `### Other Changes\n${formatGroup(groups.other)}\n\n`;
72
72
 
73
73
  return block.trim() + '\n';
74
74
  }
package/src/vcs/git.ts CHANGED
@@ -43,11 +43,17 @@ export function getGitCommits(
43
43
  watch: string[],
44
44
  allTags: string[],
45
45
  cwd: string,
46
+ exclude: string[] = [],
46
47
  ): Commit[] {
47
48
  const lastTag = resolveGitTag(name, allTags);
48
- const watchPaths = watch.join(' ');
49
49
  const range = lastTag ? `${lastTag}..HEAD` : 'HEAD';
50
50
 
51
+ const paths = [...watch];
52
+ if (exclude && exclude.length > 0) {
53
+ paths.push(...exclude.map((e) => `:(exclude)${e}`));
54
+ }
55
+
56
+ const watchPaths = paths.join(' ');
51
57
  const gitCmd = watchPaths
52
58
  ? `git log ${range} --no-show-signature --format='%H|%an|%ad|%s' --date=short -- ${watchPaths}`
53
59
  : `git log ${range} --no-show-signature --format='%H|%an|%ad|%s' --date=short`;
@@ -99,9 +105,9 @@ export class GitVcsProvider implements VcsProvider {
99
105
  return this.allTags;
100
106
  }
101
107
 
102
- async getCommits(name: string, watch: string[]): Promise<Commit[]> {
108
+ async getCommits(name: string, watch: string[], exclude?: string[]): Promise<Commit[]> {
103
109
  const tags = this.getAllTags();
104
- return getGitCommits(name, watch, tags, this.cwd);
110
+ return getGitCommits(name, watch, tags, this.cwd, exclude);
105
111
  }
106
112
 
107
113
  async getLatestTag(name?: string): Promise<string | null> {
package/src/vcs/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { Commit } from '../types';
2
2
 
3
3
  export interface VcsProvider {
4
- getCommits(name: string, watch: string[]): Promise<Commit[]>;
4
+ getCommits(name: string, watch: string[], exclude?: string[]): Promise<Commit[]>;
5
5
  getLatestTag(name?: string): Promise<string | null>;
6
6
  commit(message: string): Promise<void>;
7
7
  tag(tagName: string): Promise<void>;
package/src/vcs/jj.ts CHANGED
@@ -47,10 +47,18 @@ export function getJjCommits(
47
47
  watch: string[],
48
48
  allTags: string[],
49
49
  cwd: string,
50
+ exclude: string[] = [],
50
51
  ): Commit[] {
51
52
  const lastTag = resolveJjTag(name, allTags);
52
53
 
53
- const range = lastTag ? `"${lastTag}"..@ & ~root()` : `::@ & ~root()`;
54
+ let range = lastTag ? `"${lastTag}"..@ & ~root()` : `::@ & ~root()`;
55
+
56
+ // Append negative path constraints to the revset range
57
+ if (exclude && exclude.length > 0) {
58
+ for (const ext of exclude) {
59
+ range += ` & ~file("${ext}")`;
60
+ }
61
+ }
54
62
 
55
63
  const template =
56
64
  'commit_id.short(40) ++ "|" ++ author.name() ++ "|" ++ author.timestamp().format("%Y-%m-%d") ++ "|" ++ description.first_line() ++ "\\n"';
@@ -111,9 +119,9 @@ export class JjVcsProvider implements VcsProvider {
111
119
  return this.allTags;
112
120
  }
113
121
 
114
- async getCommits(name: string, watch: string[]): Promise<Commit[]> {
122
+ async getCommits(name: string, watch: string[], exclude?: string[]): Promise<Commit[]> {
115
123
  const tags = this.getAllTags();
116
- return getJjCommits(name, watch, tags, this.cwd);
124
+ return getJjCommits(name, watch, tags, this.cwd, exclude);
117
125
  }
118
126
 
119
127
  async getLatestTag(name?: string): Promise<string | null> {