versionary 0.13.0 → 0.14.1

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
@@ -144,6 +144,10 @@ For a quick trial, use:
144
144
  request)
145
145
  - `release-draft` (default `false`) publishes GitHub releases as drafts when
146
146
  enabled
147
+ - `release-reference-comments` controls release comments on linked issues/PRs:
148
+ - `off` (default): do not post comments
149
+ - `best-effort`: post comments and continue on API/permission failures
150
+ - `strict`: fail release if comment posting fails
147
151
  - optional monorepo planning with `monorepo-mode` and `packages`:
148
152
  - `independent` computes package bumps per path
149
153
  - `fixed` computes one shared bump across configured package paths
@@ -14,6 +14,11 @@ export declare const configSchema: z.ZodObject<{
14
14
  "r-news": "r-news";
15
15
  }>>;
16
16
  "release-draft": z.ZodOptional<z.ZodBoolean>;
17
+ "release-reference-comments": z.ZodOptional<z.ZodEnum<{
18
+ off: "off";
19
+ "best-effort": "best-effort";
20
+ strict: "strict";
21
+ }>>;
17
22
  "release-branch": z.ZodOptional<z.ZodString>;
18
23
  "baseline-file": z.ZodOptional<z.ZodString>;
19
24
  "bootstrap-sha": z.ZodOptional<z.ZodString>;
@@ -68,6 +68,9 @@ exports.configSchema = zod_1.z
68
68
  "changelog-file": zod_1.z.string().optional(),
69
69
  "changelog-format": zod_1.z.enum(["markdown-changelog", "r-news"]).optional(),
70
70
  "release-draft": zod_1.z.boolean().optional(),
71
+ "release-reference-comments": zod_1.z
72
+ .enum(["off", "best-effort", "strict"])
73
+ .optional(),
71
74
  "release-branch": zod_1.z.string().optional(),
72
75
  "baseline-file": zod_1.z.string().optional(),
73
76
  "bootstrap-sha": zod_1.z.string().optional(),
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export { verifyProject } from "./release/verify-project.js";
3
3
  export { findPluginsByCapability, pluginHasCapability, } from "./scm/capabilities.js";
4
4
  export { getScmClient } from "./scm/client.js";
5
5
  export { createGitHubPlugin } from "./scm/github-plugin.js";
6
- export type { ScmClient, ScmClientContext, ScmProvider, ScmReleaseMetadataInput, ScmReleaseMetadataResult, ScmReviewRequestInput, ScmReviewRequestResult, } from "./scm/types.js";
6
+ export type { ScmClient, ScmClientContext, ScmProvider, ScmReleaseMetadataInput, ScmReleaseMetadataResult, ScmReleaseReferenceCommentsInput, ScmReleaseReferenceCommentsResult, ScmReviewRequestInput, ScmReviewRequestResult, } from "./scm/types.js";
7
7
  export { resolveVersionStrategy } from "./strategy/resolve.js";
8
8
  export type { VersionaryArtifactRule, VersionaryConfig, VersionaryPackage, } from "./types/config.js";
9
- export type { VersionaryPluginCapability, VersionaryPluginContext, VersionaryPluginRuntime, VersionaryScmReleaseMetadataInput, VersionaryScmReleaseMetadataResult, VersionaryScmReviewRequestInput, VersionaryScmReviewRequestResult, } from "./types/plugins.js";
9
+ export type { VersionaryPluginCapability, VersionaryPluginContext, VersionaryPluginRuntime, VersionaryScmReleaseMetadataInput, VersionaryScmReleaseMetadataResult, VersionaryScmReleaseReferenceCommentsInput, VersionaryScmReleaseReferenceCommentsResult, VersionaryScmReviewRequestInput, VersionaryScmReviewRequestResult, } from "./types/plugins.js";
@@ -51,6 +51,28 @@ function groupCommitLines(commits, repoUrl) {
51
51
  const features = [];
52
52
  const fixes = [];
53
53
  const reverts = [];
54
+ const getRevertedSubject = (commit) => {
55
+ const normalizedDescription = (commit.description ?? "")
56
+ .trim()
57
+ .replace(/^"(.*)"$/u, "$1");
58
+ if (normalizedDescription.length > 0) {
59
+ return normalizedDescription;
60
+ }
61
+ const quoted = commit.subject.match(/^revert:\s+"(.+)"$/iu);
62
+ if (quoted) {
63
+ return quoted[1].trim();
64
+ }
65
+ const plain = commit.subject.match(/^revert:\s+(.+)$/iu);
66
+ return plain ? plain[1].trim() : "";
67
+ };
68
+ const shouldIncludeRevert = (commit) => {
69
+ const revertedSubject = getRevertedSubject(commit);
70
+ if (revertedSubject.length === 0) {
71
+ return true;
72
+ }
73
+ const revertedCommit = (0, commits_js_1.parseConventionalCommitMessage)(revertedSubject);
74
+ return (0, commits_js_1.inferReleaseTypeFromParsedCommit)(revertedCommit) !== null;
75
+ };
54
76
  for (const commit of commits) {
55
77
  const type = (0, commits_js_1.inferReleaseTypeFromParsedCommit)(commit);
56
78
  if (!type) {
@@ -67,6 +89,9 @@ function groupCommitLines(commits, repoUrl) {
67
89
  const commitType = (commit.type ?? "").toLowerCase();
68
90
  const isBreaking = type === "major";
69
91
  if (commit.isRevert) {
92
+ if (!shouldIncludeRevert(commit)) {
93
+ continue;
94
+ }
70
95
  reverts.push(line);
71
96
  continue;
72
97
  }
@@ -130,18 +155,21 @@ function renderReleasePlanChangelog(plan) {
130
155
  version: pkg.nextVersion,
131
156
  }))
132
157
  : [];
158
+ const dedupedCommits = [
159
+ ...new Map(plan.commits.map((commit) => [commit.hash, commit])).values(),
160
+ ];
133
161
  if (plan.changelogFormat === "r-news") {
134
162
  return renderRNewsReleaseNotes({
135
163
  packageName: plan.packageName,
136
164
  nextVersion: plan.nextVersion,
137
- commits: plan.commits,
165
+ commits: dedupedCommits,
138
166
  cwd: process.cwd(),
139
167
  });
140
168
  }
141
169
  return renderSimpleReleaseNotes({
142
170
  currentVersion: plan.currentVersion,
143
171
  nextVersion: plan.nextVersion,
144
- commits: plan.commits,
172
+ commits: dedupedCommits,
145
173
  cwd: process.cwd(),
146
174
  dependencies,
147
175
  });
@@ -1,6 +1,7 @@
1
1
  import type { VersionaryConfig } from "../types/config.js";
2
2
  import type { VersionaryPluginContext } from "../types/plugins.js";
3
3
  export declare function extractReleaseNotes(content: string, version: string, changelogFormat: "markdown-changelog" | "r-news"): string;
4
+ export declare function extractClosingReferencesFromNotes(notes: string): number[];
4
5
  export declare function resolveTargetChangelogFile(config: VersionaryConfig, rootChangelogFile: string, targetPath: string): string;
5
6
  export declare function runRelease(cwd?: string): Promise<string>;
6
7
  export type RunReleaseResult = {
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.extractReleaseNotes = extractReleaseNotes;
7
+ exports.extractClosingReferencesFromNotes = extractClosingReferencesFromNotes;
7
8
  exports.resolveTargetChangelogFile = resolveTargetChangelogFile;
8
9
  exports.runRelease = runRelease;
9
10
  exports.runReleaseDetailed = runReleaseDetailed;
@@ -75,6 +76,24 @@ function readReleaseNotes(cwd, version, changelogFile, changelogFormat) {
75
76
  const notes = extractReleaseNotes(content, version, changelogFormat);
76
77
  return notes.length > 0 ? notes : `Automated release for v${version}`;
77
78
  }
79
+ function extractClosingReferencesFromNotes(notes) {
80
+ const refs = new Set();
81
+ const linkedPattern = /\b(close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\b\s+\[#(\d+)\]\([^)]+\)/giu;
82
+ for (const match of notes.matchAll(linkedPattern)) {
83
+ const issue = Number(match[2] ?? "");
84
+ if (Number.isInteger(issue) && issue > 0) {
85
+ refs.add(issue);
86
+ }
87
+ }
88
+ const plainPattern = /\b(close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\b\s+#(\d+)/giu;
89
+ for (const match of notes.matchAll(plainPattern)) {
90
+ const issue = Number(match[2] ?? "");
91
+ if (Number.isInteger(issue) && issue > 0) {
92
+ refs.add(issue);
93
+ }
94
+ }
95
+ return [...refs].sort((a, b) => a - b);
96
+ }
78
97
  function resolveTargetChangelogFile(config, rootChangelogFile, targetPath) {
79
98
  if (targetPath === ".") {
80
99
  return rootChangelogFile;
@@ -114,6 +133,7 @@ async function runReleaseDetailed(cwd = process.cwd(), options = {}) {
114
133
  const loaded = (0, load_config_js_1.loadConfig)(cwd);
115
134
  const strategy = (0, resolve_js_1.resolveVersionStrategy)(loaded.config);
116
135
  const { changelogFile } = (0, plan_js_1.getChangelogDefaults)(loaded.config);
136
+ const referenceCommentMode = loaded.config["release-reference-comments"] ?? "off";
117
137
  const version = strategy.readVersion(cwd, loaded.config);
118
138
  const defaultTag = `v${version}`;
119
139
  const releaseTargets = (0, state_js_1.readReleaseTargets)(cwd);
@@ -139,13 +159,16 @@ async function runReleaseDetailed(cwd = process.cwd(), options = {}) {
139
159
  }
140
160
  const scmClient = (0, client_js_1.getScmClient)();
141
161
  const releases = [];
162
+ const referencesByTag = new Map();
142
163
  for (const target of targets) {
143
164
  const targetChangelogFile = resolveTargetChangelogFile(loaded.config, changelogFile, target.path);
144
165
  const targetChangelogFormat = resolveTargetChangelogFormat(loaded.config, target.path);
166
+ const releaseNotes = readReleaseNotes(cwd, target.version, targetChangelogFile, targetChangelogFormat);
167
+ referencesByTag.set(target.tag, extractClosingReferencesFromNotes(releaseNotes));
145
168
  const outcome = await (0, recovery_js_1.executeIdempotentReleaseTarget)(cwd, {
146
169
  tag: target.tag,
147
170
  version: target.version,
148
- notes: readReleaseNotes(cwd, target.version, targetChangelogFile, targetChangelogFormat),
171
+ notes: releaseNotes,
149
172
  draft: loaded.config["release-draft"] ?? false,
150
173
  }, {
151
174
  createReleaseMetadata: (input) => scmClient.createReleaseMetadata(input, {
@@ -160,6 +183,20 @@ async function runReleaseDetailed(cwd = process.cwd(), options = {}) {
160
183
  tagStatus: outcome.tagStatus,
161
184
  metadataStatus: outcome.metadataStatus,
162
185
  });
186
+ const references = referencesByTag.get(outcome.tag) ?? [];
187
+ if (references.length > 0 &&
188
+ referenceCommentMode !== "off" &&
189
+ scmClient.createReleaseReferenceComments) {
190
+ await scmClient.createReleaseReferenceComments({
191
+ version: target.version,
192
+ releaseUrl: outcome.url,
193
+ references,
194
+ mode: referenceCommentMode,
195
+ }, {
196
+ cwd,
197
+ logger: options.logger,
198
+ });
199
+ }
163
200
  }
164
201
  const published = releases.map((outcome) => `${outcome.tag}: ${outcome.url} (tag=${outcome.tagStatus}, metadata=${outcome.metadataStatus})`);
165
202
  return {
@@ -89,7 +89,11 @@ async function ensureLabels(octokit, repo, pullNumber, labels, branchContext) {
89
89
  function createGitHubPlugin() {
90
90
  return {
91
91
  name: "github",
92
- capabilities: ["scm.reviewRequest", "scm.releaseMetadata"],
92
+ capabilities: [
93
+ "scm.reviewRequest",
94
+ "scm.releaseMetadata",
95
+ "scm.releaseReferenceComments",
96
+ ],
93
97
  provider: "github",
94
98
  async createOrUpdateReviewRequest(input, _context) {
95
99
  const repo = getRepoFromEnv();
@@ -205,5 +209,38 @@ function createGitHubPlugin() {
205
209
  }
206
210
  return { url: data.html_url, status: "created" };
207
211
  },
212
+ async createReleaseReferenceComments(input, context) {
213
+ const repo = getRepoFromEnv();
214
+ const octokit = new rest_1.Octokit({ auth: getGitHubToken() });
215
+ const uniqueReferences = [...new Set(input.references)].sort((a, b) => a - b);
216
+ const commented = [];
217
+ const mode = input.mode ?? "best-effort";
218
+ for (const reference of uniqueReferences) {
219
+ const body = reference >= 1_000_000_000
220
+ ? `This pull request is included in [version ${input.version}](${input.releaseUrl}).`
221
+ : `This issue has been resolved in [version ${input.version}](${input.releaseUrl}).`;
222
+ try {
223
+ await octokit.issues.createComment({
224
+ owner: repo.owner,
225
+ repo: repo.repo,
226
+ issue_number: reference,
227
+ body,
228
+ });
229
+ commented.push(reference);
230
+ }
231
+ catch (error) {
232
+ const { status, message } = parseGitHubError(error);
233
+ if (mode === "strict") {
234
+ throw new Error(`Failed creating release reference comment on #${reference}: [${repoRef(repo)}] ${message}`);
235
+ }
236
+ if (status === 404 || status === 410 || status === 403) {
237
+ context.logger?.warn(`Skipping release reference comment on #${reference}: [${repoRef(repo)}] ${message}`);
238
+ continue;
239
+ }
240
+ context.logger?.warn(`Skipping release reference comment on #${reference}: [${repoRef(repo)}] ${message}`);
241
+ }
242
+ }
243
+ return { commented };
244
+ },
208
245
  };
209
246
  }
@@ -30,8 +30,18 @@ export interface ScmReleaseMetadataResult {
30
30
  url: string;
31
31
  status?: "created" | "exists";
32
32
  }
33
+ export interface ScmReleaseReferenceCommentsInput {
34
+ version: string;
35
+ releaseUrl: string;
36
+ references: number[];
37
+ mode?: "best-effort" | "strict";
38
+ }
39
+ export interface ScmReleaseReferenceCommentsResult {
40
+ commented: number[];
41
+ }
33
42
  export interface ScmClient {
34
43
  provider: ScmProvider;
35
44
  createOrUpdateReviewRequest: (input: ScmReviewRequestInput, context: ScmClientContext) => Promise<ScmReviewRequestResult>;
36
45
  createReleaseMetadata: (input: ScmReleaseMetadataInput, context: ScmClientContext) => Promise<ScmReleaseMetadataResult>;
46
+ createReleaseReferenceComments?: (input: ScmReleaseReferenceCommentsInput, context: ScmClientContext) => Promise<ScmReleaseReferenceCommentsResult>;
37
47
  }
@@ -1,5 +1,6 @@
1
1
  export type ConfigFileFormat = "jsonc" | "json" | "toml" | "js";
2
2
  export type VersionaryChangelogFormat = "markdown-changelog" | "r-news";
3
+ export type ReleaseReferenceCommentsMode = "off" | "best-effort" | "strict";
3
4
  export interface VersionaryArtifactRule {
4
5
  type: "json" | "toml" | "yaml" | "regex";
5
6
  path: string;
@@ -22,6 +23,7 @@ export interface VersionaryConfig {
22
23
  "changelog-file"?: string;
23
24
  "changelog-format"?: VersionaryChangelogFormat;
24
25
  "release-draft"?: boolean;
26
+ "release-reference-comments"?: ReleaseReferenceCommentsMode;
25
27
  "release-branch"?: string;
26
28
  "baseline-file"?: string;
27
29
  "bootstrap-sha"?: string;
@@ -1,9 +1,11 @@
1
- import type { ScmClientContext, ScmProvider, ScmReleaseMetadataInput, ScmReleaseMetadataResult, ScmReviewRequestInput, ScmReviewRequestResult } from "../scm/types.js";
2
- export type VersionaryPluginCapability = "scm.reviewRequest" | "scm.releaseMetadata";
1
+ import type { ScmClientContext, ScmProvider, ScmReleaseMetadataInput, ScmReleaseMetadataResult, ScmReleaseReferenceCommentsInput, ScmReleaseReferenceCommentsResult, ScmReviewRequestInput, ScmReviewRequestResult } from "../scm/types.js";
2
+ export type VersionaryPluginCapability = "scm.reviewRequest" | "scm.releaseMetadata" | "scm.releaseReferenceComments";
3
3
  export type VersionaryScmReviewRequestInput = ScmReviewRequestInput;
4
4
  export type VersionaryScmReviewRequestResult = ScmReviewRequestResult;
5
5
  export type VersionaryScmReleaseMetadataInput = ScmReleaseMetadataInput;
6
6
  export type VersionaryScmReleaseMetadataResult = ScmReleaseMetadataResult;
7
+ export type VersionaryScmReleaseReferenceCommentsInput = ScmReleaseReferenceCommentsInput;
8
+ export type VersionaryScmReleaseReferenceCommentsResult = ScmReleaseReferenceCommentsResult;
7
9
  export type VersionaryPluginContext = ScmClientContext;
8
10
  export interface VersionaryPluginRuntime {
9
11
  name: string;
@@ -11,4 +13,5 @@ export interface VersionaryPluginRuntime {
11
13
  provider?: ScmProvider;
12
14
  createOrUpdateReviewRequest?: (input: VersionaryScmReviewRequestInput, context: VersionaryPluginContext) => Promise<VersionaryScmReviewRequestResult>;
13
15
  createReleaseMetadata?: (input: VersionaryScmReleaseMetadataInput, context: VersionaryPluginContext) => Promise<VersionaryScmReleaseMetadataResult>;
16
+ createReleaseReferenceComments?: (input: VersionaryScmReleaseReferenceCommentsInput, context: VersionaryPluginContext) => Promise<VersionaryScmReleaseReferenceCommentsResult>;
14
17
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "versionary",
3
- "version": "0.13.0",
3
+ "version": "0.14.1",
4
4
  "description": "Automatic release framework based on conventional commits and semantic versioning",
5
5
  "keywords": [
6
6
  "releasing",