versionary 0.22.0 → 0.23.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/README.md CHANGED
@@ -157,6 +157,32 @@ For a quick trial, use:
157
157
  - per-package `package-name` can override release identity (labels + tag base)
158
158
  - per-package `changelog-file` writes package release notes to
159
159
  `<package-path>/<changelog-file>`
160
+ - per-package `follows` declares an asymmetric version link to one or more
161
+ source packages: when any source bumps, the follower releases too, with
162
+ bump = `max(own bump, max(source bumps))`. The follower's changelog gets a
163
+ `### Dependencies` section listing the followed sources. Use it when one
164
+ package bundles another's artifact (e.g. an editor extension that ships
165
+ the CLI binary). Cycles, self-references, unknown source paths, and
166
+ combining `follows` with `monorepo-mode: "fixed"` are config errors.
167
+ `follows` is non-transitive: A follows B does not imply A follows what B
168
+ follows.
169
+
170
+ ```jsonc
171
+ // Editor extension that bundles the root CLI artifact
172
+ {
173
+ "version": 1,
174
+ "release-type": "rust",
175
+ "monorepo-mode": "independent",
176
+ "packages": {
177
+ ".": { "exclude-paths": ["editors"] },
178
+ "editors/code": {
179
+ "release-type": "node",
180
+ "package-name": "panache-code",
181
+ "follows": ["."]
182
+ }
183
+ }
184
+ }
185
+ ```
160
186
 
161
187
  Rust strategy examples:
162
188
 
@@ -53,6 +53,7 @@ export declare const configSchema: z.ZodObject<{
53
53
  jsonpath: z.ZodOptional<z.ZodString>;
54
54
  pattern: z.ZodOptional<z.ZodString>;
55
55
  }, z.core.$strip>>>;
56
+ follows: z.ZodOptional<z.ZodArray<z.ZodString>>;
56
57
  }, z.core.$strict>>>;
57
58
  }, z.core.$strict>;
58
59
  export type ConfigSchema = z.infer<typeof configSchema>;
@@ -60,6 +60,7 @@ const packageSchema = zod_1.z
60
60
  "changelog-format": zod_1.z.enum(["markdown-changelog", "r-news"]).optional(),
61
61
  "exclude-paths": zod_1.z.array(zod_1.z.string()).optional(),
62
62
  "extra-files": zod_1.z.array(artifactRuleSchema).optional(),
63
+ follows: zod_1.z.array(zod_1.z.string().min(1)).optional(),
63
64
  })
64
65
  .strict();
65
66
  exports.configSchema = zod_1.z
@@ -85,4 +86,88 @@ exports.configSchema = zod_1.z
85
86
  "release-type": zod_1.z.string().optional(),
86
87
  packages: zod_1.z.record(zod_1.z.string().min(1), packageSchema).optional(),
87
88
  })
88
- .strict();
89
+ .strict()
90
+ .superRefine((value, ctx) => {
91
+ const packages = value.packages;
92
+ if (!packages) {
93
+ return;
94
+ }
95
+ const knownPaths = new Set(Object.keys(packages));
96
+ const followsByPath = new Map();
97
+ for (const [packagePath, packageConfig] of Object.entries(packages)) {
98
+ const follows = packageConfig.follows;
99
+ if (!follows || follows.length === 0) {
100
+ continue;
101
+ }
102
+ followsByPath.set(packagePath, follows);
103
+ for (const sourcePath of follows) {
104
+ if (sourcePath === packagePath) {
105
+ ctx.addIssue({
106
+ code: zod_1.z.ZodIssueCode.custom,
107
+ message: `Package "${packagePath}" cannot follow itself.`,
108
+ path: ["packages", packagePath, "follows"],
109
+ });
110
+ continue;
111
+ }
112
+ if (!knownPaths.has(sourcePath)) {
113
+ ctx.addIssue({
114
+ code: zod_1.z.ZodIssueCode.custom,
115
+ message: `Package "${packagePath}" follows unknown package "${sourcePath}".`,
116
+ path: ["packages", packagePath, "follows"],
117
+ });
118
+ }
119
+ }
120
+ }
121
+ if (followsByPath.size > 0 && value["monorepo-mode"] === "fixed") {
122
+ for (const followerPath of followsByPath.keys()) {
123
+ ctx.addIssue({
124
+ code: zod_1.z.ZodIssueCode.custom,
125
+ message: 'Package "follows" cannot be combined with monorepo-mode "fixed" (fixed mode already pins all package versions together).',
126
+ path: ["packages", followerPath, "follows"],
127
+ });
128
+ }
129
+ }
130
+ const reportedCycles = new Set();
131
+ const findCycleFrom = (start) => {
132
+ const trail = [];
133
+ const visit = (node) => {
134
+ const trailIndex = trail.indexOf(node);
135
+ if (trailIndex !== -1) {
136
+ return [...trail.slice(trailIndex), node];
137
+ }
138
+ const sources = followsByPath.get(node);
139
+ if (!sources || sources.length === 0) {
140
+ return null;
141
+ }
142
+ trail.push(node);
143
+ for (const source of sources) {
144
+ if (!knownPaths.has(source)) {
145
+ continue;
146
+ }
147
+ const cycle = visit(source);
148
+ if (cycle) {
149
+ return cycle;
150
+ }
151
+ }
152
+ trail.pop();
153
+ return null;
154
+ };
155
+ return visit(start);
156
+ };
157
+ for (const followerPath of followsByPath.keys()) {
158
+ const cycle = findCycleFrom(followerPath);
159
+ if (!cycle) {
160
+ continue;
161
+ }
162
+ const key = [...cycle].sort().join("->");
163
+ if (reportedCycles.has(key)) {
164
+ continue;
165
+ }
166
+ reportedCycles.add(key);
167
+ ctx.addIssue({
168
+ code: zod_1.z.ZodIssueCode.custom,
169
+ message: `Package "follows" cycle detected: ${cycle.join(" -> ")}.`,
170
+ path: ["packages", cycle[0] ?? followerPath, "follows"],
171
+ });
172
+ }
173
+ });
@@ -18,7 +18,7 @@ export interface ReleasePlan {
18
18
  releaseType: ReleaseType;
19
19
  currentVersion: string;
20
20
  nextVersion: string | null;
21
- bumpReason?: "direct" | "dependency-propagation";
21
+ bumpReason?: "direct" | "dependency-propagation" | "follows";
22
22
  dependencySourcePaths?: string[];
23
23
  commits: ParsedCommit[];
24
24
  }>;
@@ -98,6 +98,8 @@ function createReleasePlan(cwd = process.cwd()) {
98
98
  releaseType: null,
99
99
  currentVersion: explicitPackagePlans[0]?.currentVersion ?? "0.0.0",
100
100
  nextVersion: null,
101
+ bumpReason: undefined,
102
+ dependencySourcePaths: undefined,
101
103
  commits: [],
102
104
  parsedCommits: [],
103
105
  resolvedVersionFile: versionFile,
@@ -171,7 +173,7 @@ function createReleasePlan(cwd = process.cwd()) {
171
173
  }
172
174
  }
173
175
  }
174
- const adjustedPackages = packagePlans.map((pkgPlan) => {
176
+ const propagatedPackages = packagePlans.map((pkgPlan) => {
175
177
  const dependencySourcePaths = [
176
178
  ...(dependencySourcePathsByPackage.get(pkgPlan.path) ??
177
179
  new Set()),
@@ -196,6 +198,47 @@ function createReleasePlan(cwd = process.cwd()) {
196
198
  dependencySourcePaths,
197
199
  };
198
200
  });
201
+ const followsByPath = new Map();
202
+ for (const [packagePath, packageConfig] of Object.entries(loaded.config.packages ?? {})) {
203
+ const follows = packageConfig.follows ?? [];
204
+ if (follows.length > 0) {
205
+ followsByPath.set(packagePath, follows);
206
+ }
207
+ }
208
+ const adjustedPackages = propagatedPackages.map((pkgPlan) => {
209
+ const followsSources = followsByPath.get(pkgPlan.path) ?? [];
210
+ const bumpingSources = followsSources
211
+ .map((sourcePath) => propagatedPackages.find((pkg) => pkg.path === sourcePath))
212
+ .filter((sourcePlan) => Boolean(sourcePlan?.nextVersion));
213
+ if (bumpingSources.length === 0) {
214
+ return pkgPlan;
215
+ }
216
+ const ownReleaseType = pkgPlan.releaseType;
217
+ const combinedReleaseType = (0, semver_js_1.maxReleaseType)([
218
+ ownReleaseType,
219
+ ...bumpingSources.map((sourcePlan) => sourcePlan.releaseType),
220
+ ]);
221
+ const mergedDependencySourcePaths = [
222
+ ...new Set([
223
+ ...(pkgPlan.dependencySourcePaths ?? []),
224
+ ...bumpingSources.map((sourcePlan) => sourcePlan.path),
225
+ ]),
226
+ ].sort((a, b) => a.localeCompare(b));
227
+ const sourceDrove = combinedReleaseType !== ownReleaseType ||
228
+ pkgPlan.bumpReason === "dependency-propagation" ||
229
+ pkgPlan.bumpReason === undefined;
230
+ const baseVersion = packageCurrentVersionByPath[pkgPlan.path] ?? pkgPlan.currentVersion;
231
+ const nextVersion = combinedReleaseType
232
+ ? (0, semver_js_1.bumpVersion)(baseVersion, combinedReleaseType, { allowStableMajor })
233
+ : null;
234
+ return {
235
+ ...pkgPlan,
236
+ releaseType: combinedReleaseType,
237
+ nextVersion,
238
+ bumpReason: sourceDrove ? "follows" : pkgPlan.bumpReason,
239
+ dependencySourcePaths: mergedDependencySourcePaths,
240
+ };
241
+ });
199
242
  const visiblePackages = adjustedPackages.filter((pkgPlan) => !pkgPlan.implicitRoot || hasExplicitRootPackage);
200
243
  const rootPackagePlan = adjustedPackages.find((pkgPlan) => pkgPlan.path === ".");
201
244
  if (!rootPackagePlan) {
@@ -1,4 +1,5 @@
1
1
  export type ReleaseType = "major" | "minor" | "patch" | null;
2
+ export declare function maxReleaseType(types: ReleaseType[]): ReleaseType;
2
3
  export interface ParsedVersion {
3
4
  major: number;
4
5
  minor: number;
@@ -1,9 +1,22 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.maxReleaseType = maxReleaseType;
3
4
  exports.parseVersion = parseVersion;
4
5
  exports.isValidVersion = isValidVersion;
5
6
  exports.compareVersions = compareVersions;
6
7
  exports.bumpVersion = bumpVersion;
8
+ function maxReleaseType(types) {
9
+ if (types.includes("major")) {
10
+ return "major";
11
+ }
12
+ if (types.includes("minor")) {
13
+ return "minor";
14
+ }
15
+ if (types.includes("patch")) {
16
+ return "patch";
17
+ }
18
+ return null;
19
+ }
7
20
  const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/u;
8
21
  const COMPAT_DOTTED_PRERELEASE_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/u;
9
22
  function normalizeVersionInput(version) {
@@ -15,6 +15,7 @@ export interface VersionaryPackage {
15
15
  "changelog-format"?: VersionaryChangelogFormat;
16
16
  "exclude-paths"?: string[];
17
17
  "extra-files"?: VersionaryArtifactRule[];
18
+ follows?: string[];
18
19
  }
19
20
  export interface VersionaryConfig {
20
21
  version: 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "versionary",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Automatic release framework based on conventional commits and semantic versioning",
5
5
  "keywords": [
6
6
  "releasing",