relacher 0.0.7-rc.4 → 0.0.7

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,5 +1,6 @@
1
1
  import type { ChangelogContext, PackageConfig } from '../types';
2
2
  import { type UpdateAction, type VersionFallback } from '../updater';
3
+ import type { BumpSize } from '../versioning/types';
3
4
  export interface PackageListError {
4
5
  name: string;
5
6
  message: string;
@@ -11,31 +12,34 @@ export declare class PackageList extends Array<PackageConfig> {
11
12
  onPackageBump(name: string, ...actions: UpdateAction[]): this;
12
13
  onAllPackages(...actions: UpdateAction[]): this;
13
14
  group(groupName: string, ...packageNames: string[]): this;
14
- /**
15
- * Couples two or more packages together so they share the exact same version bump.
16
- * Supports: `.couple('a', 'b')` or `.couple(['a', 'b'], ['c', 'd'])`.
17
- */
18
15
  couple(...args: [string, string] | [string[]]): this;
19
16
  /**
20
17
  * Automatically adds a CHANGELOG.md update action to each package's directory.
18
+ * Defaults to onlyOn: ['major', 'minor', 'patch'] so pre-releases are skipped.
21
19
  */
22
20
  withChangelogs(options?: {
23
21
  only?: string[];
24
22
  exclude?: string[];
23
+ onlyOn?: BumpSize[];
25
24
  template?: (ctx: ChangelogContext) => string;
26
25
  }): this;
27
26
  /**
28
27
  * Attaches a workspace-wide root CHANGELOG.md that captures all changes across packages.
28
+ * Defaults to onlyOn: ['major', 'minor', 'patch'] so pre-releases are skipped.
29
29
  */
30
30
  withRootChangelog(options?: {
31
31
  github?: string;
32
32
  path?: string;
33
+ onlyOn?: BumpSize[];
33
34
  template?: (ctx: ChangelogContext) => string;
34
35
  }): this;
35
36
  /**
36
37
  * Declaratively syncs a package version to any file on disk (e.g. `flake.nix`, `README.md`).
38
+ * Supports `options.onlyOn` to restrict updates to specific bump types.
37
39
  */
38
- syncVersion(packageName: string, filePath: string, pattern?: string | RegExp, replaceTemplate?: string): this;
40
+ syncVersion(packageName: string, filePath: string, pattern?: string | RegExp, replaceTemplate?: string, options?: {
41
+ onlyOn?: BumpSize[];
42
+ }): this;
39
43
  assertFound(...names: string[]): this;
40
44
  addDepsOn(pkgName: string, dependsOn: string | string[]): this;
41
45
  ignore(...names: string[]): this;
@@ -12,4 +12,4 @@ export declare const LOCKFILE_NAME = ".relacher.lock";
12
12
  export declare function readLockfile(cwd: string): LockfileData;
13
13
  export declare function writeLockfile(cwd: string, data: LockfileData): void;
14
14
  export declare function updateLockfile(cwd: string, reports: DependencyUpdateReport[]): void;
15
- export declare function findLastReleaseCommit(vcs: VcsProvider, packageName: string, currentVersion: string): Effect.Effect<string | null, VcsError>;
15
+ export declare function findLastReleaseCommit(vcs: VcsProvider, packageName: string, targetVersion: string): Effect.Effect<string | null, VcsError>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relacher",
3
- "version": "0.0.7-rc.4",
3
+ "version": "0.0.7",
4
4
  "description": "Scriptable release orchestration library for monorepos",
5
5
  "type": "module",
6
6
  "private": false,
@@ -1,7 +1,14 @@
1
1
  import path from 'node:path';
2
2
 
3
3
  import type { ChangelogContext, PackageConfig } from '../types';
4
- import { changelogUpdate, githubChangelogTemplate, regexUpdate, type UpdateAction, type VersionFallback } from '../updater';
4
+ import {
5
+ changelogUpdate,
6
+ githubChangelogTemplate,
7
+ regexUpdate,
8
+ type UpdateAction,
9
+ type VersionFallback,
10
+ } from '../updater';
11
+ import type { BumpSize } from '../versioning/types';
5
12
 
6
13
  export interface PackageListError {
7
14
  name: string;
@@ -70,10 +77,6 @@ export class PackageList extends Array<PackageConfig> {
70
77
  return this;
71
78
  }
72
79
 
73
- /**
74
- * Couples two or more packages together so they share the exact same version bump.
75
- * Supports: `.couple('a', 'b')` or `.couple(['a', 'b'], ['c', 'd'])`.
76
- */
77
80
  public couple(...args: [string, string] | [string[]]): this {
78
81
  if (Array.isArray(args[0])) {
79
82
  for (const groupMembers of args as string[][]) {
@@ -87,12 +90,16 @@ export class PackageList extends Array<PackageConfig> {
87
90
 
88
91
  /**
89
92
  * Automatically adds a CHANGELOG.md update action to each package's directory.
93
+ * Defaults to onlyOn: ['major', 'minor', 'patch'] so pre-releases are skipped.
90
94
  */
91
95
  public withChangelogs(options?: {
92
96
  only?: string[];
93
97
  exclude?: string[];
98
+ onlyOn?: BumpSize[];
94
99
  template?: (ctx: ChangelogContext) => string;
95
100
  }): this {
101
+ const onlyOn = options?.onlyOn ?? ['major', 'minor', 'patch'];
102
+
96
103
  for (const pkg of this) {
97
104
  if (options?.only && !options.only.includes(pkg.name)) continue;
98
105
  if (options?.exclude && options.exclude.includes(pkg.name)) continue;
@@ -103,6 +110,7 @@ export class PackageList extends Array<PackageConfig> {
103
110
  this.onPackageBump(
104
111
  pkg.name,
105
112
  changelogUpdate(changelogPath, {
113
+ onlyOn,
106
114
  template: options?.template,
107
115
  }),
108
116
  );
@@ -112,18 +120,22 @@ export class PackageList extends Array<PackageConfig> {
112
120
 
113
121
  /**
114
122
  * Attaches a workspace-wide root CHANGELOG.md that captures all changes across packages.
123
+ * Defaults to onlyOn: ['major', 'minor', 'patch'] so pre-releases are skipped.
115
124
  */
116
125
  public withRootChangelog(options?: {
117
126
  github?: string;
118
127
  path?: string;
128
+ onlyOn?: BumpSize[];
119
129
  template?: (ctx: ChangelogContext) => string;
120
130
  }): this {
121
131
  const changelogPath = options?.path || 'CHANGELOG.md';
122
132
  const template = options?.template ?? (options?.github ? githubChangelogTemplate(options.github) : undefined);
133
+ const onlyOn = options?.onlyOn ?? ['major', 'minor', 'patch'];
123
134
 
124
135
  this.onAllPackages(
125
136
  changelogUpdate(changelogPath, {
126
137
  global: true,
138
+ onlyOn,
127
139
  template,
128
140
  }),
129
141
  );
@@ -132,12 +144,14 @@ export class PackageList extends Array<PackageConfig> {
132
144
 
133
145
  /**
134
146
  * Declaratively syncs a package version to any file on disk (e.g. `flake.nix`, `README.md`).
147
+ * Supports `options.onlyOn` to restrict updates to specific bump types.
135
148
  */
136
149
  public syncVersion(
137
150
  packageName: string,
138
151
  filePath: string,
139
152
  pattern: string | RegExp = 'version = "[^"]+"',
140
153
  replaceTemplate = 'version = "{{version}}"',
154
+ options?: { onlyOn?: BumpSize[] },
141
155
  ): this {
142
156
  const search = typeof pattern === 'string' ? pattern : pattern.source;
143
157
  return this.onPackageBump(
@@ -145,6 +159,7 @@ export class PackageList extends Array<PackageConfig> {
145
159
  regexUpdate(filePath, {
146
160
  search,
147
161
  replace: replaceTemplate,
162
+ onlyOn: options?.onlyOn,
148
163
  }),
149
164
  );
150
165
  }
package/src/lockfile.ts CHANGED
@@ -5,6 +5,7 @@ import { Effect } from 'effect';
5
5
 
6
6
  import type { DependencyUpdateReport } from './types';
7
7
  import type { VcsError, VcsProvider } from './vcs';
8
+ import { inferLastStableVersion } from './versioning/semver';
8
9
 
9
10
  export interface LockfilePackageEntry {
10
11
  version: string;
@@ -44,7 +45,14 @@ export function updateLockfile(cwd: string, reports: DependencyUpdateReport[]):
44
45
  const isPrerelease = dep.newVersion.includes('-');
45
46
  const previousEntry = lockfile.packages[dep.name];
46
47
 
47
- let lastStable = previousEntry?.lastStableVersion || previousEntry?.version;
48
+ let lastStable =
49
+ previousEntry?.lastStableVersion ||
50
+ previousEntry?.version ||
51
+ dep.lastStableVersion ||
52
+ (dep.currentVersion && !dep.currentVersion.includes('-')
53
+ ? dep.currentVersion
54
+ : inferLastStableVersion(dep.currentVersion));
55
+
48
56
  if (!isPrerelease) {
49
57
  lastStable = dep.newVersion;
50
58
  }
@@ -61,7 +69,7 @@ export function updateLockfile(cwd: string, reports: DependencyUpdateReport[]):
61
69
  export function findLastReleaseCommit(
62
70
  vcs: VcsProvider,
63
71
  packageName: string,
64
- currentVersion: string,
72
+ targetVersion: string,
65
73
  ): Effect.Effect<string | null, VcsError> {
66
74
  return Effect.gen(function* () {
67
75
  if (!vcs.getFileHistoryCommits || !vcs.getFileAtCommit) {
@@ -71,8 +79,10 @@ export function findLastReleaseCommit(
71
79
  const hashes = yield* vcs.getFileHistoryCommits(LOCKFILE_NAME);
72
80
  if (hashes.length === 0) return null;
73
81
 
74
- let lastMatchingHash: string | null = hashes[0] || null;
82
+ let foundMatch = false;
83
+ let earliestMatchingHash: string | null = null;
75
84
 
85
+ // Hashes are ordered from newest to oldest
76
86
  for (const hash of hashes) {
77
87
  const content = yield* vcs.getFileAtCommit(LOCKFILE_NAME, hash);
78
88
  if (!content) continue;
@@ -81,16 +91,18 @@ export function findLastReleaseCommit(
81
91
  const data = JSON.parse(content) as LockfileData;
82
92
  const version = data.packages?.[packageName]?.version;
83
93
 
84
- if (version === currentVersion) {
85
- lastMatchingHash = hash;
86
- } else {
87
- return lastMatchingHash;
94
+ if (version === targetVersion) {
95
+ foundMatch = true;
96
+ earliestMatchingHash = hash;
97
+ } else if (foundMatch) {
98
+ // Reached an older version before targetVersion was introduced
99
+ return earliestMatchingHash;
88
100
  }
89
101
  } catch {
90
- return lastMatchingHash;
102
+ if (foundMatch) return earliestMatchingHash;
91
103
  }
92
104
  }
93
105
 
94
- return lastMatchingHash;
106
+ return earliestMatchingHash;
95
107
  }) as Effect.Effect<string | null, VcsError>;
96
108
  }
package/src/prepare.ts CHANGED
@@ -14,7 +14,7 @@ import type {
14
14
  PrepareOptions,
15
15
  } from './types';
16
16
  import type { UpdateAction, UpdateActionResolved } from './updater';
17
- import { VcsProviderService, type VcsProvider } from './vcs';
17
+ import { VcsProviderService } from './vcs';
18
18
  import { VersionManagerService, type VersionManager } from './versioning';
19
19
  import { isBumpEqual, isPreRelease } from './versioning/bump';
20
20
  import type { BumpSize } from './versioning/types';
@@ -116,6 +116,13 @@ export function initReportItems(
116
116
  }
117
117
 
118
118
  let selfBump = versionManager.evaluateCommitsBump(commitsSincePreRelease);
119
+
120
+ // If graduating an existing pre-release to stable (e.g. 1.1.0-rc.0 -> 1.1.0),
121
+ // ensure it is never skipped even if no new commits were added after the RC was cut.
122
+ if (!versionManager.isRCMode && actualVersion.includes('-') && selfBump === 'skip') {
123
+ selfBump = 'patch';
124
+ }
125
+
119
126
  selfBump = resolvePackageBump(dep.name, selfBump, options);
120
127
 
121
128
  let isErroneous = isMissingVersion;
@@ -177,32 +184,8 @@ export function prepare(
177
184
 
178
185
  const cwd = options.cwd || process.cwd();
179
186
 
180
- let trackedPackages: PackageConfig[] = packages;
181
- if (Option.isSome(vcsOption)) {
182
- const vcs: VcsProvider = vcsOption.value;
183
- if (typeof vcs.isTracked === 'function') {
184
- const filtered: PackageConfig[] = [];
185
- for (const pkg of packages) {
186
- // Use relative path directly rather than resolving to absolute
187
- const targetPath = pkg.manifestPath || pkg.watch?.[0] || '.';
188
-
189
- const isTracked = yield* vcs.isTracked(targetPath);
190
- if (isTracked) {
191
- filtered.push(pkg);
192
- }
193
- }
194
- trackedPackages = filtered;
195
-
196
- const activeNames = new Set(trackedPackages.map((p) => p.name));
197
- for (const pkg of trackedPackages) {
198
- if (pkg.depends) {
199
- pkg.depends = pkg.depends.filter((d) => activeNames.has(d));
200
- }
201
- }
202
- }
203
- }
204
-
205
- const items = yield* initReportItems(trackedPackages, cwd, options);
187
+ // Packages passed in were already validated and tracked by the builder.
188
+ const items = yield* initReportItems(packages, cwd, options);
206
189
  const sorted = topologicalSort(items);
207
190
  versionManager.propagateBumps(sorted);
208
191
  versionManager.propagateCoupledBumps(sorted);
@@ -104,10 +104,18 @@ export const makeVcsVersionManager = (
104
104
  return vcs.getCommits(dep.watch || [], null, excludePaths);
105
105
  }
106
106
 
107
+ // In stable release mode, if the package was in pre-release (e.g. 1.1.0-rc.0),
108
+ // we query commits since the last STABLE version (e.g. 1.0.0) so that all commits
109
+ // developed during the RC cycle are included in the final changelog and bump.
110
+ const targetVersion =
111
+ !isRCMode && lockVersion.includes('-') && entry?.lastStableVersion
112
+ ? entry.lastStableVersion
113
+ : lockVersion;
114
+
107
115
  const findCommitEffect =
108
116
  typeof vcs.findLastReleaseCommit === 'function'
109
- ? vcs.findLastReleaseCommit(dep.name, lockVersion)
110
- : findLastReleaseCommit(vcs, dep.name, lockVersion);
117
+ ? vcs.findLastReleaseCommit(dep.name, targetVersion)
118
+ : findLastReleaseCommit(vcs, dep.name, targetVersion);
111
119
 
112
120
  return findCommitEffect.pipe(
113
121
  Effect.flatMap((lastCommit) =>