🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

versionary

Package Overview
Dependencies
Maintainers
1
Versions
43
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

versionary - npm Package Compare versions

Comparing version
0.28.2
to
0.29.0
+7
-6
dist/cli/index.js

@@ -6,3 +6,3 @@ #!/usr/bin/env node

import { createReleasePlan } from "../release/plan.js";
import { closeStaleReviewRequestIfExists, consumeNextReleaseFile, isReleaseCommitMessage, openOrUpdateReviewRequest, prepareReleasePr, pushReleaseBranch, readNextReleaseHighlights, } from "../release/pr.js";
import { closeStaleReviewRequestIfExists, consumeNextReleaseFile, isReleaseCommitMessage, openOrUpdateReviewRequest, prepareReleasePr, pushReleaseBranch, resolveReleaseHighlights, } from "../release/pr.js";
import { runRelease, runReleaseDetailed } from "../release/release.js";

@@ -200,5 +200,6 @@ import { verifyProject } from "../release/verify-project.js";

const loaded = loadConfig();
const highlightsRead = readNextReleaseHighlights(process.cwd(), loaded.config);
const highlights = highlightsRead?.content ?? "";
const section = renderReleasePlanChangelog(plan, { highlights });
const highlightsResult = resolveReleaseHighlights(process.cwd(), loaded.config, plan.changelogFile, plan.changelogFormat, logger);
const section = renderReleasePlanChangelog(plan, {
highlights: highlightsResult.highlights,
});
if (!write) {

@@ -209,4 +210,4 @@ console.log(section);

prependChangelog(process.cwd(), plan.changelogFile, section, plan.changelogFormat);
if (highlightsRead) {
consumeNextReleaseFile(process.cwd(), highlightsRead.filePath);
if (highlightsResult.source === "file" && highlightsResult.filePath) {
consumeNextReleaseFile(process.cwd(), highlightsResult.filePath);
}

@@ -213,0 +214,0 @@ console.log(`Updated ${plan.changelogFile}`);

@@ -28,2 +28,22 @@ import { type ParsedCommit } from "../git/commits.js";

export declare function renderSimpleChangelog(plan: SimplePlan): string;
/**
* Locate a manual-notes block at the top of an existing changelog and split it
* out. The notes block is the first release-level heading whose text is not a
* version (`##` for markdown, `#` for r-news), captured up to — but excluding —
* the first subsequent version heading at the same level.
*
* Returns the captured prose as `highlights` (the heading line itself dropped,
* trimmed) and the changelog with that block removed as `body`. When the first
* release-level heading is already a version, or there is none, nothing is
* stripped and `highlights` is empty.
*
* Only the topmost heading is ever eligible, and capture stops at the first
* version heading, so prior releases are never swallowed. For r-news this also
* subsumes the conventional `# pkg (development version)` header, whose body
* (if any) becomes the release notes.
*/
export declare function extractUnreleasedNotes(existing: string, format?: VersionaryChangelogFormat): {
highlights: string;
body: string;
};
export declare function prependChangelog(cwd: string, changelogFile: string, section: string, format?: VersionaryChangelogFormat): void;

@@ -30,0 +50,0 @@ export declare function renderRNewsReleaseNotes(input: {

@@ -6,2 +6,3 @@ import fs from "node:fs";

import { resolvePackageDependencies, } from "./plan.js";
import { isVersionHeading } from "./semver.js";
const REVIEW_REQUEST_FOOTER = "---\n\nThis PR was generated by [Versionary](https://github.com/jolars/versionary).";

@@ -205,11 +206,59 @@ function formatDate() {

}
/**
* Locate a manual-notes block at the top of an existing changelog and split it
* out. The notes block is the first release-level heading whose text is not a
* version (`##` for markdown, `#` for r-news), captured up to — but excluding —
* the first subsequent version heading at the same level.
*
* Returns the captured prose as `highlights` (the heading line itself dropped,
* trimmed) and the changelog with that block removed as `body`. When the first
* release-level heading is already a version, or there is none, nothing is
* stripped and `highlights` is empty.
*
* Only the topmost heading is ever eligible, and capture stops at the first
* version heading, so prior releases are never swallowed. For r-news this also
* subsumes the conventional `# pkg (development version)` header, whose body
* (if any) becomes the release notes.
*/
export function extractUnreleasedNotes(existing, format = "markdown-changelog") {
const headingPattern = format === "r-news" ? /^#(?!#)\s+/u : /^##(?!#)\s+/u;
const lines = existing.split("\n");
let firstHeadingIdx = -1;
for (let index = 0; index < lines.length; index += 1) {
if (headingPattern.test(lines[index])) {
firstHeadingIdx = index;
break;
}
}
if (firstHeadingIdx === -1 || isVersionHeading(lines[firstHeadingIdx])) {
return { highlights: "", body: existing };
}
let nextVersionIdx = lines.length;
for (let index = firstHeadingIdx + 1; index < lines.length; index += 1) {
if (headingPattern.test(lines[index]) && isVersionHeading(lines[index])) {
nextVersionIdx = index;
break;
}
}
const highlights = lines
.slice(firstHeadingIdx + 1, nextVersionIdx)
.join("\n")
.trim();
const body = [
...lines.slice(0, firstHeadingIdx),
...lines.slice(nextVersionIdx),
].join("\n");
return { highlights, body };
}
export function prependChangelog(cwd, changelogFile, section, format = "markdown-changelog") {
const changelogPath = path.join(cwd, changelogFile);
const existing = fs.existsSync(changelogPath)
const existingRaw = fs.existsSync(changelogPath)
? fs.readFileSync(changelogPath, "utf8")
: "";
// Strip any manual-notes block; its prose is folded into `section` upstream,
// so leaving it here would duplicate it.
const { body: existing } = extractUnreleasedNotes(existingRaw, format);
if (format === "r-news") {
const bodyWithoutDevHeader = existing.replace(/^#\s+.+\s+\(development version\)\s*(?:\r?\n)*/u, "");
const separator = bodyWithoutDevHeader.length > 0 ? "\n\n" : "";
const next = `${`${section}${separator}${bodyWithoutDevHeader}`.trimEnd()}\n`;
const separator = existing.length > 0 ? "\n\n" : "";
const next = `${`${section}${separator}${existing}`.trimEnd()}\n`;
fs.writeFileSync(changelogPath, next, "utf8");

@@ -216,0 +265,0 @@ return;

import type { ParsedCommit } from "../git/commits.js";
import type { VersionaryConfig } from "../types/config.js";
import type { VersionaryChangelogFormat, VersionaryConfig } from "../types/config.js";
import type { VersionaryPluginContext } from "../types/plugins.js";

@@ -13,2 +13,18 @@ import { type ReleasePlan, type SimplePlan } from "./plan.js";

};
/**
* Read the manual-notes ("Unreleased") prose from the top of a changelog file.
* Returns an empty string when the file is absent or has no notes block.
*/
export declare function readChangelogHighlights(cwd: string, changelogFile: string, format: VersionaryChangelogFormat): string;
export interface ResolvedReleaseHighlights {
highlights: string;
source: "changelog" | "file" | "none";
filePath?: string;
}
/**
* Resolve release highlights, preferring an editable "Unreleased" section at the
* top of the changelog. Falls back to the deprecated `NEXT_RELEASE.md` file
* (emitting a warning) so existing setups keep working.
*/
export declare function resolveReleaseHighlights(cwd: string, config: VersionaryConfig, changelogFile: string, format: VersionaryChangelogFormat, logger?: VersionaryPluginContext["logger"]): ResolvedReleaseHighlights;
export declare function splitSafeDirtyFiles(files: string[]): {

@@ -15,0 +31,0 @@ ignored: string[];

@@ -9,3 +9,3 @@ import { execFileSync } from "node:child_process";

import { applyConfiguredArtifactRules } from "./artifact-rules.js";
import { prependChangelog, renderReleaseNotesSection, renderReleasePlanChangelog, renderReviewRequestFooter, } from "./changelog.js";
import { extractUnreleasedNotes, prependChangelog, renderReleaseNotesSection, renderReleasePlanChangelog, renderReviewRequestFooter, } from "./changelog.js";
import { createReleasePlan, getChangelogDefaults, resolvePackageDependencies, } from "./plan.js";

@@ -53,2 +53,35 @@ import { getBaselineStatePath, writeBaselineSha, } from "./state.js";

}
/**
* Read the manual-notes ("Unreleased") prose from the top of a changelog file.
* Returns an empty string when the file is absent or has no notes block.
*/
export function readChangelogHighlights(cwd, changelogFile, format) {
const changelogPath = path.join(cwd, changelogFile);
if (!fs.existsSync(changelogPath)) {
return "";
}
const existing = fs.readFileSync(changelogPath, "utf8");
return extractUnreleasedNotes(existing, format).highlights;
}
/**
* Resolve release highlights, preferring an editable "Unreleased" section at the
* top of the changelog. Falls back to the deprecated `NEXT_RELEASE.md` file
* (emitting a warning) so existing setups keep working.
*/
export function resolveReleaseHighlights(cwd, config, changelogFile, format, logger) {
const fromChangelog = readChangelogHighlights(cwd, changelogFile, format);
if (fromChangelog.length > 0) {
return { highlights: fromChangelog, source: "changelog" };
}
const fromFile = readNextReleaseHighlights(cwd, config);
if (fromFile) {
logger?.warn(`${fromFile.filePath} is deprecated; add release notes under an "Unreleased" heading at the top of ${changelogFile} instead.`);
return {
highlights: fromFile.content,
source: "file",
filePath: fromFile.filePath,
};
}
return { highlights: "", source: "none" };
}
function listTrackedDirtyFiles(cwd) {

@@ -269,4 +302,4 @@ const status = execFileSync("git", ["status", "--porcelain", "--untracked-files=no"], {

const packageReleaseMetadata = buildPackageReleaseMetadata(cwd, plan, loaded.config);
const nextReleaseRead = readNextReleaseHighlights(cwd, loaded.config);
const highlights = nextReleaseRead?.content ?? "";
const highlightsResult = resolveReleaseHighlights(cwd, loaded.config, plan.changelogFile, plan.changelogFormat, options.logger);
const highlights = highlightsResult.highlights;
const section = renderReleasePlanChangelog(plan, { highlights, cwd });

@@ -276,6 +309,8 @@ prependChangelog(cwd, plan.changelogFile, section, plan.changelogFormat);

let consumedHighlightsPath = null;
if (nextReleaseRead) {
const { tracked } = consumeNextReleaseFile(cwd, nextReleaseRead.filePath);
// The changelog "Unreleased" block is stripped in-place by prependChangelog;
// only the legacy side file needs explicit consumption and staging.
if (highlightsResult.source === "file" && highlightsResult.filePath) {
const { tracked } = consumeNextReleaseFile(cwd, highlightsResult.filePath);
if (tracked) {
consumedHighlightsPath = nextReleaseRead.filePath;
consumedHighlightsPath = highlightsResult.filePath;
}

@@ -299,2 +334,4 @@ }

}
const packageChangelogPath = path.posix.join(packagePlan.path, packageChangelogFile);
const packageHighlights = readChangelogHighlights(cwd, packageChangelogPath, "markdown-changelog");
const packageSection = renderReleaseNotesSection({

@@ -307,5 +344,6 @@ currentVersion: packagePlan.currentVersion,

dependencies: resolvePackageDependencies(plan, packagePlan.path),
highlights: packageHighlights,
});
prependChangelog(cwd, path.posix.join(packagePlan.path, packageChangelogFile), packageSection, "markdown-changelog");
updatedChangelogFiles.push(path.posix.join(packagePlan.path, packageChangelogFile));
prependChangelog(cwd, packageChangelogPath, packageSection, "markdown-changelog");
updatedChangelogFiles.push(packageChangelogPath);
}

@@ -312,0 +350,0 @@ const releaseTargets = buildReleaseTargets(cwd, plan, loaded.config);

@@ -15,3 +15,15 @@ export type ReleaseType = "major" | "minor" | "patch" | null;

export declare function isValidVersion(version: string): boolean;
/**
* Decide whether a changelog heading denotes a released version (e.g.
* `## [0.28.2](url) (2026-06-02)`, `## 1.2.3`, `## v1.2.3`, `## 0.28.2.9000`)
* rather than a manual-notes heading (e.g. `## Unreleased`, `## Upcoming`).
*
* Intentionally liberal: a heading counts as a version if it contains any
* version-like token that {@link isValidVersion} accepts. This errs toward
* "it's a version", so a real release heading is never mistaken for a notes
* block (and therefore never stripped). Dates such as `2026-06-02` use dashes,
* not dots, so they never match the three-component token.
*/
export declare function isVersionHeading(heading: string): boolean;
export declare function compareVersions(leftRaw: string, rightRaw: string): number;
export declare function bumpVersion(current: string, releaseType: Exclude<ReleaseType, null>, options?: BumpVersionOptions): string;

@@ -41,2 +41,21 @@ export function maxReleaseType(types) {

}
const VERSION_TOKEN_PATTERN = /v?(\d+\.\d+\.\d+(?:\.\d+)?)/u;
/**
* Decide whether a changelog heading denotes a released version (e.g.
* `## [0.28.2](url) (2026-06-02)`, `## 1.2.3`, `## v1.2.3`, `## 0.28.2.9000`)
* rather than a manual-notes heading (e.g. `## Unreleased`, `## Upcoming`).
*
* Intentionally liberal: a heading counts as a version if it contains any
* version-like token that {@link isValidVersion} accepts. This errs toward
* "it's a version", so a real release heading is never mistaken for a notes
* block (and therefore never stripped). Dates such as `2026-06-02` use dashes,
* not dots, so they never match the three-component token.
*/
export function isVersionHeading(heading) {
const match = heading.match(VERSION_TOKEN_PATTERN);
if (!match?.[1]) {
return false;
}
return isValidVersion(match[1]);
}
function isNumericIdentifier(identifier) {

@@ -43,0 +62,0 @@ return /^(0|[1-9]\d*)$/u.test(identifier);

{
"name": "versionary",
"version": "0.28.2",
"version": "0.29.0",
"description": "Automatic release framework based on conventional commits and semantic versioning",

@@ -5,0 +5,0 @@ "keywords": [