versionary 0.17.0 → 0.19.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.
@@ -36,6 +36,20 @@ function hasOriginRemote(cwd) {
36
36
  return false;
37
37
  }
38
38
  }
39
+ function getRemoteRefSha(cwd, ref) {
40
+ try {
41
+ const output = runGit(cwd, ["ls-remote", "origin", ref]);
42
+ const line = output.split("\n")[0]?.trim() ?? "";
43
+ if (!line) {
44
+ return null;
45
+ }
46
+ const sha = line.split(/\s+/u)[0]?.trim() ?? "";
47
+ return sha.length > 0 ? sha : null;
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
39
53
  function setOutput(name, value) {
40
54
  const outputPath = process.env.GITHUB_OUTPUT;
41
55
  if (!outputPath) {
@@ -77,6 +91,35 @@ function main() {
77
91
  `https://x-access-token:${encodedToken}@${base}/${repository}.git`,
78
92
  ]);
79
93
  }
94
+ const eventName = process.env.GITHUB_EVENT_NAME ?? "";
95
+ const ref = process.env.GITHUB_REF ?? "";
96
+ const sha = process.env.GITHUB_SHA?.trim() ?? "";
97
+ if (eventName === "push" &&
98
+ ref.startsWith("refs/heads/") &&
99
+ sha.length > 0 &&
100
+ hasOriginRemote(cwd)) {
101
+ const remoteSha = getRemoteRefSha(cwd, ref);
102
+ if (remoteSha && remoteSha !== sha) {
103
+ const staleMessage = `Skipping stale push run for ${sha.slice(0, 7)}; ` +
104
+ `${ref} now points to ${remoteSha.slice(0, 7)}.`;
105
+ const stalePayload = {
106
+ action: "stale-run-skipped",
107
+ message: staleMessage,
108
+ releaseCreated: false,
109
+ tagNames: [],
110
+ };
111
+ process.stdout.write(`${JSON.stringify(stalePayload)}\n`);
112
+ setOutput("action", stalePayload.action);
113
+ setOutput("message", stalePayload.message);
114
+ setOutput("release_created", "false");
115
+ setOutput("tag_name", "");
116
+ setOutput("tag_names", "[]");
117
+ setOutput("review_url", "");
118
+ setOutput("branch", "");
119
+ setOutput("title", "");
120
+ return;
121
+ }
122
+ }
80
123
  const raw = (0, node_child_process_1.execFileSync)("npx", ["--yes", `versionary@${versionaryVersion}`, "run", "--json"], {
81
124
  cwd,
82
125
  encoding: "utf8",
package/dist/cli/index.js CHANGED
@@ -99,6 +99,9 @@ async function main() {
99
99
  }
100
100
  const plan = (0, plan_js_1.createReleasePlan)();
101
101
  if (!plan.nextVersion) {
102
+ if (!flags["dry-run"]) {
103
+ await (0, pr_js_1.closeStaleReviewRequestIfExists)(process.cwd(), { logger });
104
+ }
102
105
  const message = "No releasable commits found. Nothing to do.";
103
106
  if (flags.json) {
104
107
  emitJson({
@@ -203,12 +206,17 @@ async function main() {
203
206
  return 0;
204
207
  }
205
208
  if (command === "pr") {
206
- if (flags["dry-run"]) {
207
- const plan = (0, plan_js_1.createReleasePlan)();
208
- if (!plan.nextVersion) {
209
- console.log("No releasable commits found. Nothing to do.");
210
- return 0;
209
+ const plan = (0, plan_js_1.createReleasePlan)();
210
+ if (!plan.nextVersion) {
211
+ if (!flags["dry-run"]) {
212
+ await (0, pr_js_1.closeStaleReviewRequestIfExists)(process.cwd(), {
213
+ logger: console,
214
+ });
211
215
  }
216
+ console.log("No releasable commits found. Nothing to do.");
217
+ return 0;
218
+ }
219
+ if (flags["dry-run"]) {
212
220
  console.log(`Dry run: would prepare release PR branch ${plan.releaseBranchPrefix} for ${plan.nextVersion}`);
213
221
  return 0;
214
222
  }
@@ -437,17 +437,26 @@ function applyRevertSuppression(commits) {
437
437
  }
438
438
  const presentShas = new Set(commits.map((commit) => commit.hash.toLowerCase()));
439
439
  const reverted = new Set();
440
+ const suppressedReverts = new Set();
440
441
  for (const commit of commits) {
441
442
  if (!commit.isRevert) {
442
443
  continue;
443
444
  }
445
+ const revertsOnlyInWindow = commit.revertedShas.length > 0 &&
446
+ commit.revertedShas.every((sha) => presentShas.has(sha));
447
+ if (revertsOnlyInWindow) {
448
+ suppressedReverts.add(commit.hash.toLowerCase());
449
+ }
444
450
  for (const sha of commit.revertedShas) {
445
451
  if (presentShas.has(sha)) {
446
452
  reverted.add(sha);
447
453
  }
448
454
  }
449
455
  }
450
- return commits.filter((commit) => !reverted.has(commit.hash.toLowerCase()));
456
+ return commits.filter((commit) => {
457
+ const hash = commit.hash.toLowerCase();
458
+ return !reverted.has(hash) && !suppressedReverts.has(hash);
459
+ });
451
460
  }
452
461
  function analyzeParsedCommits(commits) {
453
462
  let result = null;
@@ -77,6 +77,7 @@ function groupCommitLines(commits, repoUrl) {
77
77
  const features = [];
78
78
  const fixes = [];
79
79
  const reverts = [];
80
+ const effectiveCommits = (0, commits_js_1.applyRevertSuppression)(commits);
80
81
  const getRevertedSubject = (commit) => {
81
82
  const normalizedDescription = (commit.description ?? "")
82
83
  .trim()
@@ -99,7 +100,7 @@ function groupCommitLines(commits, repoUrl) {
99
100
  const revertedCommit = (0, commits_js_1.parseConventionalCommitMessage)(revertedSubject);
100
101
  return (0, commits_js_1.inferReleaseTypeFromParsedCommit)(revertedCommit) !== null;
101
102
  };
102
- for (const commit of commits) {
103
+ for (const commit of effectiveCommits) {
103
104
  const type = (0, commits_js_1.inferReleaseTypeFromParsedCommit)(commit);
104
105
  if (!type) {
105
106
  continue;
@@ -20,6 +20,13 @@ export declare function renderSimpleReviewRequestBody(version: string, previousV
20
20
  export declare function openOrUpdateReviewRequest(cwd: string, branch: string, title: string, version: string, previousVersion: string, commits: ParsedCommit[], plan?: SimplePlan | null, options?: {
21
21
  logger?: VersionaryPluginContext["logger"];
22
22
  }): Promise<string>;
23
+ export declare function closeStaleReviewRequestIfExists(cwd?: string, options?: {
24
+ logger?: VersionaryPluginContext["logger"];
25
+ }): Promise<{
26
+ closed: boolean;
27
+ url?: string;
28
+ number?: number;
29
+ }>;
23
30
  /** @deprecated Use prepareReleasePr. */
24
31
  export declare function prepareSimpleReleasePr(cwd?: string, options?: {
25
32
  logger?: VersionaryPluginContext["logger"];
@@ -7,6 +7,7 @@ exports.splitSafeDirtyFiles = splitSafeDirtyFiles;
7
7
  exports.prepareReleasePr = prepareReleasePr;
8
8
  exports.renderSimpleReviewRequestBody = renderSimpleReviewRequestBody;
9
9
  exports.openOrUpdateReviewRequest = openOrUpdateReviewRequest;
10
+ exports.closeStaleReviewRequestIfExists = closeStaleReviewRequestIfExists;
10
11
  exports.prepareSimpleReleasePr = prepareSimpleReleasePr;
11
12
  exports.openOrUpdateSimpleReviewRequest = openOrUpdateSimpleReviewRequest;
12
13
  exports.pushReleaseBranch = pushReleaseBranch;
@@ -408,6 +409,32 @@ async function openOrUpdateReviewRequest(cwd, branch, title, version, previousVe
408
409
  });
409
410
  return result.url;
410
411
  }
412
+ function hasScmRuntimeContext() {
413
+ const hasRepository = Boolean(process.env.GITHUB_REPOSITORY);
414
+ const hasToken = Boolean(process.env.VERSIONARY_PR_TOKEN ??
415
+ process.env.GH_TOKEN ??
416
+ process.env.GITHUB_TOKEN);
417
+ return hasRepository && hasToken;
418
+ }
419
+ async function closeStaleReviewRequestIfExists(cwd = process.cwd(), options = {}) {
420
+ if (!hasScmRuntimeContext()) {
421
+ return { closed: false };
422
+ }
423
+ const plan = (0, plan_js_1.createReleasePlan)(cwd);
424
+ const scmClient = (0, client_js_1.getScmClient)();
425
+ const result = await scmClient.closeReviewRequestIfExists({
426
+ baseBranch: process.env.VERSIONARY_BASE_BRANCH ?? "main",
427
+ headBranch: plan.releaseBranchPrefix,
428
+ reason: "Closing stale release PR because no releasable commits remain for the current baseline/tag state.",
429
+ }, {
430
+ cwd,
431
+ logger: options.logger,
432
+ });
433
+ if (result.closed && result.number) {
434
+ options.logger?.info(`Closed stale release review request #${result.number} for ${plan.releaseBranchPrefix}.`);
435
+ }
436
+ return result;
437
+ }
411
438
  /** @deprecated Use prepareReleasePr. */
412
439
  function prepareSimpleReleasePr(cwd = process.cwd(), options = {}) {
413
440
  return prepareReleasePr(cwd, options);
@@ -174,6 +174,68 @@ function createGitHubPlugin() {
174
174
  state: toReviewRequestState(created, `pull request #${created.number} in ${repoRef(repo)}`),
175
175
  };
176
176
  },
177
+ async closeReviewRequestIfExists(input, _context) {
178
+ const repo = getRepoFromEnv();
179
+ const octokit = new rest_1.Octokit({ auth: getGitHubToken() });
180
+ const listHead = resolveHeadForList(repo, input.headBranch);
181
+ let existing;
182
+ try {
183
+ const response = await octokit.pulls.list({
184
+ owner: repo.owner,
185
+ repo: repo.repo,
186
+ state: "open",
187
+ head: listHead,
188
+ base: input.baseBranch,
189
+ per_page: 100,
190
+ });
191
+ existing = response.data;
192
+ }
193
+ catch (error) {
194
+ const { message } = parseGitHubError(error);
195
+ throw new Error(`Failed listing open pull requests for branch "${input.headBranch}" into "${input.baseBranch}": [${repoRef(repo)}] ${message}`);
196
+ }
197
+ if (existing.length > 1) {
198
+ const matches = existing.map((item) => `#${item.number}`).join(", ");
199
+ throw new Error(`Ambiguous open pull request matches for "${input.headBranch}" into "${input.baseBranch}": [${repoRef(repo)}] ${matches}`);
200
+ }
201
+ if (existing.length === 0) {
202
+ return { closed: false };
203
+ }
204
+ const pr = existing[0];
205
+ let updated;
206
+ try {
207
+ const response = await octokit.pulls.update({
208
+ owner: repo.owner,
209
+ repo: repo.repo,
210
+ pull_number: pr.number,
211
+ state: "closed",
212
+ });
213
+ updated = response.data;
214
+ }
215
+ catch (error) {
216
+ const { message } = parseGitHubError(error);
217
+ throw new Error(`Failed closing pull request #${pr.number}: [${repoRef(repo)} base=${input.baseBranch} head=${input.headBranch}] ${message}`);
218
+ }
219
+ try {
220
+ await octokit.issues.createComment({
221
+ owner: repo.owner,
222
+ repo: repo.repo,
223
+ issue_number: pr.number,
224
+ body: input.reason,
225
+ });
226
+ }
227
+ catch (error) {
228
+ const { status, message } = parseGitHubError(error);
229
+ if (status !== 404 && status !== 410) {
230
+ throw new Error(`Failed commenting on closed pull request #${pr.number}: [${repoRef(repo)} base=${input.baseBranch} head=${input.headBranch}] ${message}`);
231
+ }
232
+ }
233
+ return {
234
+ closed: true,
235
+ number: updated.number,
236
+ url: updated.html_url,
237
+ };
238
+ },
177
239
  async createReleaseMetadata(input, _context) {
178
240
  const repo = getRepoFromEnv();
179
241
  const octokit = new rest_1.Octokit({ auth: getGitHubToken() });
@@ -20,6 +20,16 @@ export interface ScmReviewRequestResult {
20
20
  url: string;
21
21
  state: "open" | "closed" | "merged";
22
22
  }
23
+ export interface ScmCloseReviewRequestInput {
24
+ baseBranch: string;
25
+ headBranch: string;
26
+ reason: string;
27
+ }
28
+ export interface ScmCloseReviewRequestResult {
29
+ closed: boolean;
30
+ number?: number;
31
+ url?: string;
32
+ }
23
33
  export interface ScmReleaseMetadataInput {
24
34
  tag: string;
25
35
  version: string;
@@ -43,6 +53,7 @@ export interface ScmReleaseReferenceCommentsResult {
43
53
  export interface ScmClient {
44
54
  provider: ScmProvider;
45
55
  createOrUpdateReviewRequest: (input: ScmReviewRequestInput, context: ScmClientContext) => Promise<ScmReviewRequestResult>;
56
+ closeReviewRequestIfExists: (input: ScmCloseReviewRequestInput, context: ScmClientContext) => Promise<ScmCloseReviewRequestResult>;
46
57
  createReleaseMetadata: (input: ScmReleaseMetadataInput, context: ScmClientContext) => Promise<ScmReleaseMetadataResult>;
47
58
  createReleaseReferenceComments?: (input: ScmReleaseReferenceCommentsInput, context: ScmClientContext) => Promise<ScmReleaseReferenceCommentsResult>;
48
59
  }
@@ -8,6 +8,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
9
  const BUILD_LUA_VERSION_PATTERN = /^(\s*version\s*=\s*")([^"]+)(")/mu;
10
10
  const PROVIDES_PACKAGE_PATTERN = /\\ProvidesPackage\{([^}]+)\}\[\d{4}-\d{2}-\d{2} v([0-9]+\.[0-9]+\.[0-9]+) ([^\]]+)\]/gu;
11
+ const PROVIDES_EXPL_PACKAGE_PATTERN = /\\ProvidesExplPackage\{([^}]+)\}\{\d{4}-\d{2}-\d{2}\}\{[^}]+\}\{([^}]*)\}/gu;
11
12
  function normalizeRelative(base, target) {
12
13
  return node_path_1.default.relative(base, target).replaceAll("\\", "/");
13
14
  }
@@ -37,9 +38,16 @@ function replaceBuildLuaVersion(content, version, versionFile) {
37
38
  return content.replace(BUILD_LUA_VERSION_PATTERN, `$1${version}$3`);
38
39
  }
39
40
  function replaceProvidesPackageMetadata(content, version, releaseDate, relativePath) {
40
- const matches = [...content.matchAll(PROVIDES_PACKAGE_PATTERN)];
41
+ const packageMatches = [...content.matchAll(PROVIDES_PACKAGE_PATTERN)];
42
+ const explPackageMatches = [
43
+ ...content.matchAll(PROVIDES_EXPL_PACKAGE_PATTERN),
44
+ ];
45
+ const matches = [...packageMatches, ...explPackageMatches];
41
46
  if (matches.length !== 1) {
42
- throw new Error(`${relativePath} must contain exactly one \\ProvidesPackage metadata entry; matched ${matches.length}.`);
47
+ throw new Error(`${relativePath} must contain exactly one \\ProvidesPackage or \\ProvidesExplPackage metadata entry; matched ${matches.length}.`);
48
+ }
49
+ if (explPackageMatches.length === 1) {
50
+ return content.replace(PROVIDES_EXPL_PACKAGE_PATTERN, (_full, pkg, desc) => `\\ProvidesExplPackage{${pkg}}{${releaseDate}}{${version}}{${desc}}`);
43
51
  }
44
52
  return content.replace(PROVIDES_PACKAGE_PATTERN, (_full, pkg, _prevVersion, desc) => `\\ProvidesPackage{${pkg}}[${releaseDate} v${version} ${desc}]`);
45
53
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "versionary",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "Automatic release framework based on conventional commits and semantic versioning",
5
5
  "keywords": [
6
6
  "releasing",
@@ -34,9 +34,9 @@
34
34
  "zod": "^4.1.12"
35
35
  },
36
36
  "devDependencies": {
37
- "@types/node": "^24.7.2",
37
+ "@types/node": "^25.6.0",
38
38
  "tsx": "^4.20.6",
39
- "typescript": "^5.9.3",
39
+ "typescript": "^6.0.3",
40
40
  "vitest": "^4.1.4"
41
41
  },
42
42
  "scripts": {