rman 1.0.8 → 1.0.9
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 +12 -3
- package/commands/changelog.command.js +16 -6
- package/commands/publish.command.js +68 -9
- package/commands/version.command.js +4 -2
- package/constants.js +1 -1
- package/core/config.d.ts +1 -99
- package/core/package.d.ts +1 -1
- package/index.d.ts +1 -1
- package/index.js +11 -0
- package/interfaces/rman-config.interface.d.ts +130 -0
- package/interfaces/rman-config.interface.js +1 -0
- package/package.json +1 -1
- package/rmanrc.schema.json +125 -21
- package/services/changelog.service.d.ts +21 -12
- package/services/changelog.service.js +17 -16
- package/services/github-release.service.d.ts +68 -0
- package/services/github-release.service.js +265 -0
- package/services/list.service.d.ts +1 -1
- package/services/version.service.d.ts +15 -4
- package/services/version.service.js +70 -32
- package/services.d.ts +1 -0
- package/services.js +1 -0
- package/utils/change-hash.d.ts +21 -12
- package/utils/change-hash.js +38 -26
- package/utils/conventional-commits.d.ts +13 -0
- package/utils/conventional-commits.js +47 -0
- package/utils/git.d.ts +8 -1
- package/utils/git.js +26 -2
- package/utils/release-version.d.ts +50 -0
- package/utils/release-version.js +66 -0
package/utils/change-hash.js
CHANGED
|
@@ -20,6 +20,22 @@ export async function findLatestTag(git, pkg) {
|
|
|
20
20
|
const expanded = pattern.replace('{name}', pkg.name);
|
|
21
21
|
return pattern.includes('{name}') ? (await git.listTags(expanded))[0] : await git.describeTag(expanded);
|
|
22
22
|
}
|
|
23
|
+
/** The forward direction of `findLatestTag`: expands `pkg`'s (cascaded) `.rmanrc
|
|
24
|
+
* changelog.tagPattern` into the concrete tag name `version` belongs under - `{name}` becomes the
|
|
25
|
+
* package's own name, `*` becomes `version`. Shared by `version` (creating the tag), `publish
|
|
26
|
+
* --target github` (finding the release that tag belongs to), and `detectChangeHash`'s own npm
|
|
27
|
+
* fallback (mapping a published version back onto a tag), so all three name tags identically. */
|
|
28
|
+
export function expandTag(pkg, version) {
|
|
29
|
+
return applyTagPattern(tagPattern(pkg), pkg.name, version);
|
|
30
|
+
}
|
|
31
|
+
/** The pattern expansion `expandTag` performs, on any pattern - `{name}` becomes `name`, `*` becomes
|
|
32
|
+
* `version`. Shared with the repository's own release tag, which uses a different pattern (see
|
|
33
|
+
* `releaseTagPattern`) but names tags the same way. */
|
|
34
|
+
export function applyTagPattern(pattern, name, version) {
|
|
35
|
+
const expanded = pattern.replace('{name}', name);
|
|
36
|
+
const starIdx = expanded.indexOf('*');
|
|
37
|
+
return starIdx === -1 ? expanded : expanded.slice(0, starIdx) + version + expanded.slice(starIdx + 1);
|
|
38
|
+
}
|
|
23
39
|
/** Strips the pattern's literal prefix (everything before its first `*`) from `tag` to get just
|
|
24
40
|
* the version part - e.g. tag `@sqb/builder@1.2.3` against pattern `@sqb/builder@*` -> `1.2.3`.
|
|
25
41
|
* A pattern with no `*` is returned as its own "version" verbatim (an exact tag, nothing to strip). */
|
|
@@ -47,40 +63,36 @@ export async function defaultNpmViewVersion(name, cwd) {
|
|
|
47
63
|
* Resolves the commit/hash a package's changes should be measured "since" - the boundary
|
|
48
64
|
* `changelog --from` uses, but reusable anywhere a command wants to answer "what changed for this
|
|
49
65
|
* package". An explicit `options.from` (anything but `"npm"`) is returned as-is, applying the same
|
|
50
|
-
* way to every package. Otherwise, it's auto-detected in order: (1)
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* no hash is given).
|
|
66
|
+
* way to every package. Otherwise, it's auto-detected in order: (1) this package's own most recent
|
|
67
|
+
* release tag - the same network-free `findLatestTag` lookup `version`/`changed` themselves use,
|
|
68
|
+
* so all three commands agree on "since when" for any repo whose tags are the ones `rman version`
|
|
69
|
+
* actually created; (2) failing that (no tag at all yet - e.g. onboarding `rman` onto a repo with
|
|
70
|
+
* real npm history but no `rman`-created tags), the package's currently-published npm version -
|
|
71
|
+
* looked up via `npmViewVersion`, then mapped to a git tag using `.rmanrc changelog.tagPattern`
|
|
72
|
+
* (see `tagPattern`). Either way, if `catchUpFile` is given and exists, the result is widened to
|
|
73
|
+
* also cover anything that file hasn't caught up on yet (see its doc comment). Returns `undefined`
|
|
74
|
+
* when nothing can be resolved at all (never tagged *and* never published, no catch-up file - a
|
|
75
|
+
* genuinely first-ever release) - callers should fall back to their own default in that case (e.g.
|
|
76
|
+
* the whole history, since nothing has ever been released).
|
|
62
77
|
*/
|
|
63
78
|
export async function detectChangeHash(git, pkg, options = {}) {
|
|
64
79
|
if (options.from && options.from !== 'npm')
|
|
65
80
|
return options.from;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
npmHash = (await git.tagExists(tag)) ? tag : undefined;
|
|
81
|
+
let tagHash = await findLatestTag(git, pkg);
|
|
82
|
+
if (!tagHash) {
|
|
83
|
+
const npmViewVersion = options.npmViewVersion ?? defaultNpmViewVersion;
|
|
84
|
+
const publishedVersion = await npmViewVersion(pkg.name, git.cwd);
|
|
85
|
+
if (publishedVersion) {
|
|
86
|
+
const tag = expandTag(pkg, publishedVersion);
|
|
87
|
+
tagHash = (await git.tagExists(tag)) ? tag : undefined;
|
|
88
|
+
}
|
|
75
89
|
}
|
|
76
|
-
if (!npmHash)
|
|
77
|
-
npmHash = await findLatestTag(git, pkg);
|
|
78
90
|
const fileHash = options.catchUpFile ? await git.lastCommitTouching(options.catchUpFile) : undefined;
|
|
79
91
|
if (!fileHash)
|
|
80
|
-
return
|
|
81
|
-
if (!
|
|
92
|
+
return tagHash;
|
|
93
|
+
if (!tagHash)
|
|
82
94
|
return fileHash;
|
|
83
|
-
return (await git.mergeBase(
|
|
95
|
+
return (await git.mergeBase(tagHash, fileHash)) ?? tagHash;
|
|
84
96
|
}
|
|
85
97
|
const execFileAsync = promisify(execFile);
|
|
86
98
|
const DEFAULT_TAG_PATTERN = 'v*';
|
|
@@ -10,6 +10,19 @@ export declare const CONVENTIONAL_PATTERN: RegExp;
|
|
|
10
10
|
* not a real change worth describing (or worth bumping a version over on its own), so it's
|
|
11
11
|
* dropped everywhere a real change is being looked for. */
|
|
12
12
|
export declare const VERSION_BUMP_PATTERN: RegExp;
|
|
13
|
+
/**
|
|
14
|
+
* Whether `subject` is a release marker rather than a real change - dropped everywhere real
|
|
15
|
+
* changes are looked for (changelog entries, and what counts as "changed" for a version bump).
|
|
16
|
+
*
|
|
17
|
+
* Covers the bare-version form other tools use (`VERSION_BUMP_PATTERN`) plus every message shape
|
|
18
|
+
* `version` itself writes: its commit message (`commitMessageTemplate`, or the built-in
|
|
19
|
+
* `"chore(release): v{version}"` when a repo doesn't override it), the multi-version form that
|
|
20
|
+
* template falls back to when one commit spans several versions (`chore(release): a@1.2.0,
|
|
21
|
+
* b@1.3.0`), and the monorepo root's own version-sync commit. Without this, rman's own release
|
|
22
|
+
* commits show up in the changelogs it generates - visible whenever the boundary reaches back past
|
|
23
|
+
* a previous release (see `detectChangeHash`'s `catchUpFile`).
|
|
24
|
+
*/
|
|
25
|
+
export declare function isReleaseCommit(subject: string, commitMessageTemplate?: string): boolean;
|
|
13
26
|
export interface ParsedCommitSubject {
|
|
14
27
|
type: string;
|
|
15
28
|
scope?: string;
|
|
@@ -10,6 +10,32 @@ export const CONVENTIONAL_PATTERN = /^(\w+)(\(([^)]+)\))?(!)?:\s*(.+)$/;
|
|
|
10
10
|
* not a real change worth describing (or worth bumping a version over on its own), so it's
|
|
11
11
|
* dropped everywhere a real change is being looked for. */
|
|
12
12
|
export const VERSION_BUMP_PATTERN = /^v?\d+\.\d+\.\d+(?:[-+][\w.]+)?$/;
|
|
13
|
+
/**
|
|
14
|
+
* Whether `subject` is a release marker rather than a real change - dropped everywhere real
|
|
15
|
+
* changes are looked for (changelog entries, and what counts as "changed" for a version bump).
|
|
16
|
+
*
|
|
17
|
+
* Covers the bare-version form other tools use (`VERSION_BUMP_PATTERN`) plus every message shape
|
|
18
|
+
* `version` itself writes: its commit message (`commitMessageTemplate`, or the built-in
|
|
19
|
+
* `"chore(release): v{version}"` when a repo doesn't override it), the multi-version form that
|
|
20
|
+
* template falls back to when one commit spans several versions (`chore(release): a@1.2.0,
|
|
21
|
+
* b@1.3.0`), and the monorepo root's own version-sync commit. Without this, rman's own release
|
|
22
|
+
* commits show up in the changelogs it generates - visible whenever the boundary reaches back past
|
|
23
|
+
* a previous release (see `detectChangeHash`'s `catchUpFile`).
|
|
24
|
+
*/
|
|
25
|
+
export function isReleaseCommit(subject, commitMessageTemplate) {
|
|
26
|
+
if (VERSION_BUMP_PATTERN.test(subject))
|
|
27
|
+
return true;
|
|
28
|
+
if (ROOT_SYNC_PATTERN.test(subject))
|
|
29
|
+
return true;
|
|
30
|
+
if (MULTI_VERSION_RELEASE_PATTERN.test(subject))
|
|
31
|
+
return true;
|
|
32
|
+
// The built-in message is checked even when a repo overrides it: the override only applies to
|
|
33
|
+
// commits spanning a single version (see `buildCommitMessage`), and a repo that adopted one later
|
|
34
|
+
// still has older releases committed under the default.
|
|
35
|
+
if (templatePattern(DEFAULT_COMMIT_MESSAGE).test(subject))
|
|
36
|
+
return true;
|
|
37
|
+
return !!commitMessageTemplate && templatePattern(commitMessageTemplate).test(subject);
|
|
38
|
+
}
|
|
13
39
|
/** Parses a commit subject as Conventional Commits, or `undefined` if it doesn't match at all
|
|
14
40
|
* (a non-conventional message - still a real change, just with no `type` to key off of). */
|
|
15
41
|
export function parseConventionalCommit(subject) {
|
|
@@ -26,6 +52,27 @@ export function parseConventionalCommit(subject) {
|
|
|
26
52
|
export function hasBreakingChangeFooter(body) {
|
|
27
53
|
return /^BREAKING[ -]CHANGE:/im.test(body);
|
|
28
54
|
}
|
|
55
|
+
/** A semver version, as it appears inside a commit subject - the `\d+\.\d+\.\d+` core of
|
|
56
|
+
* `VERSION_BUMP_PATTERN`, reusable inside the larger patterns below. */
|
|
57
|
+
const SEMVER_SOURCE = String.raw `\d+\.\d+\.\d+(?:[-+][\w.]+)?`;
|
|
58
|
+
/** Mirrors `VersionService`'s own default `version.commitMessage` - kept in sync by
|
|
59
|
+
* `version.command.ts`'s documented default, not imported, to keep this module dependency-free. */
|
|
60
|
+
const DEFAULT_COMMIT_MESSAGE = 'chore(release): v{version}';
|
|
61
|
+
/** `VersionService.applyPlan`'s trailing commit for a monorepo root's informational version. */
|
|
62
|
+
const ROOT_SYNC_PATTERN = new RegExp(String.raw `^chore: sync root version to ${SEMVER_SOURCE}$`);
|
|
63
|
+
/** What the commit-message template falls back to when one commit covers several versions at once
|
|
64
|
+
* (a cross-group ripple) - `{version}` has nothing single to substitute, so each bumped package is
|
|
65
|
+
* listed by name instead. */
|
|
66
|
+
const MULTI_VERSION_RELEASE_PATTERN = new RegExp(String.raw `^chore\(release\): \S+@${SEMVER_SOURCE}(?:, \S+@${SEMVER_SOURCE})*$`);
|
|
67
|
+
function escapeRegExp(value) {
|
|
68
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
69
|
+
}
|
|
70
|
+
/** Turns a `version.commitMessage` template into a matcher for the commits it produces: every
|
|
71
|
+
* literal part escaped, each `{version}` placeholder standing in for any semver. */
|
|
72
|
+
function templatePattern(template) {
|
|
73
|
+
const source = template.split('{version}').map(escapeRegExp).join(SEMVER_SOURCE);
|
|
74
|
+
return new RegExp(`^${source}$`);
|
|
75
|
+
}
|
|
29
76
|
/**
|
|
30
77
|
* A `Release-As: patch|minor|major` footer in a commit `body` - lets that one commit's own
|
|
31
78
|
* contribution to a detected bump severity be overridden by hand, regardless of what its subject
|
package/utils/git.d.ts
CHANGED
|
@@ -57,7 +57,11 @@ export declare class GitHelper {
|
|
|
57
57
|
/** The most recent tag matching the glob `pattern` that HEAD actually descends from (unlike
|
|
58
58
|
* `listTags`, this follows commit ancestry rather than just sorting tag names - the right
|
|
59
59
|
* choice for a single repo-wide tag scheme, where a package has no tag of its own). */
|
|
60
|
-
describeTag(pattern: string): Promise<string | undefined>;
|
|
60
|
+
describeTag(pattern: string, ref?: string): Promise<string | undefined>;
|
|
61
|
+
/** The repository's very first commit (the oldest root commit, for a history with several) -
|
|
62
|
+
* `undefined` for a repository with no commits at all. The "since the beginning" boundary for
|
|
63
|
+
* a package being released for the first time, with no earlier tag to measure from. */
|
|
64
|
+
rootCommit(): Promise<string | undefined>;
|
|
61
65
|
/** Stages and commits exactly `files` (relative to `cwd`, or absolute) with `message` - never a
|
|
62
66
|
* blanket `git add -A`, so the commit only ever contains what the caller explicitly asked for. */
|
|
63
67
|
commit(files: string[], message: string): Promise<void>;
|
|
@@ -71,6 +75,9 @@ export declare class GitHelper {
|
|
|
71
75
|
remote?: string;
|
|
72
76
|
tags?: boolean;
|
|
73
77
|
}): Promise<void>;
|
|
78
|
+
/** `git remote get-url <remote>` (default `"origin"`) - `undefined` when that remote isn't
|
|
79
|
+
* configured at all, which is a perfectly normal state for a local-only repository. */
|
|
80
|
+
remoteUrl(remote?: string): Promise<string | undefined>;
|
|
74
81
|
/** Raw `git diff <hash>..HEAD` text (committed changes plus any uncommitted local ones, same as
|
|
75
82
|
* plain `git diff <hash>`) - unlike `listChangedSince`, the actual patch content, not just
|
|
76
83
|
* which files changed. `pathspec`, if given, narrows it to just that file/directory. */
|
package/utils/git.js
CHANGED
|
@@ -183,9 +183,9 @@ export class GitHelper {
|
|
|
183
183
|
/** The most recent tag matching the glob `pattern` that HEAD actually descends from (unlike
|
|
184
184
|
* `listTags`, this follows commit ancestry rather than just sorting tag names - the right
|
|
185
185
|
* choice for a single repo-wide tag scheme, where a package has no tag of its own). */
|
|
186
|
-
async describeTag(pattern) {
|
|
186
|
+
async describeTag(pattern, ref = 'HEAD') {
|
|
187
187
|
try {
|
|
188
|
-
const { stdout } = await execFileAsync('git', ['describe', '--tags', '--abbrev=0', '--match', pattern], {
|
|
188
|
+
const { stdout } = await execFileAsync('git', ['describe', '--tags', '--abbrev=0', '--match', pattern, ref], {
|
|
189
189
|
cwd: this.cwd,
|
|
190
190
|
});
|
|
191
191
|
return stdout.trim() || undefined;
|
|
@@ -194,6 +194,19 @@ export class GitHelper {
|
|
|
194
194
|
return undefined;
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
|
+
/** The repository's very first commit (the oldest root commit, for a history with several) -
|
|
198
|
+
* `undefined` for a repository with no commits at all. The "since the beginning" boundary for
|
|
199
|
+
* a package being released for the first time, with no earlier tag to measure from. */
|
|
200
|
+
async rootCommit() {
|
|
201
|
+
try {
|
|
202
|
+
const { stdout } = await execFileAsync('git', ['rev-list', '--max-parents=0', 'HEAD'], { cwd: this.cwd });
|
|
203
|
+
const shas = stdout.trim().split(/\r?\n/).filter(Boolean);
|
|
204
|
+
return shas[shas.length - 1] || undefined;
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
197
210
|
/** Stages and commits exactly `files` (relative to `cwd`, or absolute) with `message` - never a
|
|
198
211
|
* blanket `git add -A`, so the commit only ever contains what the caller explicitly asked for. */
|
|
199
212
|
async commit(files, message) {
|
|
@@ -229,6 +242,17 @@ export class GitHelper {
|
|
|
229
242
|
throw new Error(`Unable to push to "${remote}": ${e.message}`, { cause: e });
|
|
230
243
|
}
|
|
231
244
|
}
|
|
245
|
+
/** `git remote get-url <remote>` (default `"origin"`) - `undefined` when that remote isn't
|
|
246
|
+
* configured at all, which is a perfectly normal state for a local-only repository. */
|
|
247
|
+
async remoteUrl(remote = 'origin') {
|
|
248
|
+
try {
|
|
249
|
+
const { stdout } = await execFileAsync('git', ['remote', 'get-url', remote], { cwd: this.cwd });
|
|
250
|
+
return stdout.trim() || undefined;
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
232
256
|
/** Raw `git diff <hash>..HEAD` text (committed changes plus any uncommitted local ones, same as
|
|
233
257
|
* plain `git diff <hash>`) - unlike `listChangedSince`, the actual patch content, not just
|
|
234
258
|
* which files changed. `pathspec`, if given, narrows it to just that file/directory. */
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Package } from '../core/package.js';
|
|
2
|
+
import type { GitHelper } from './git.js';
|
|
3
|
+
/**
|
|
4
|
+
* A calendar release version: `YYYY.M.D-HHmm`, every part **unpadded** (`2026.9.5-930`, not
|
|
5
|
+
* `2026.09.05-0930`). The padding isn't a style choice - semver forbids leading zeroes in numeric
|
|
6
|
+
* identifiers, so a padded month or time makes the version invalid, and a monorepo root's
|
|
7
|
+
* `package.json` has to hold a valid one. Unpadded still orders correctly: the date parts compare
|
|
8
|
+
* numerically as major/minor/patch, and the time compares numerically as a prerelease identifier
|
|
9
|
+
* (`930` < `1430`).
|
|
10
|
+
*/
|
|
11
|
+
export declare const CALENDAR_VERSION_PATTERN: RegExp;
|
|
12
|
+
/** `.rmanrc version.releaseTagPattern` (root-level) - the tag naming the repository's own release,
|
|
13
|
+
* as opposed to the per-package/group tags `changelog.tagPattern` names. Deliberately a **separate**
|
|
14
|
+
* pattern with a non-`v` default: `findLatestTag` resolves a repo-wide package pattern with `git
|
|
15
|
+
* describe --match`, so a release tag that also matched `v*` would be picked up as some package's
|
|
16
|
+
* own last release - corrupting both its changelog boundary and the version its entry is headed
|
|
17
|
+
* with. */
|
|
18
|
+
export declare function releaseTagPattern(root: Package): string;
|
|
19
|
+
export declare function isCalendarVersion(version: string): boolean;
|
|
20
|
+
/** `date` as a calendar release version - see `CALENDAR_VERSION_PATTERN` for why nothing is padded.
|
|
21
|
+
* The time becomes a single number (`14:30` -> `1430`, `09:30` -> `930`), which is both unpadded
|
|
22
|
+
* by construction and ordered the way the clock is. */
|
|
23
|
+
export declare function formatCalendarVersion(date: Date): string;
|
|
24
|
+
/**
|
|
25
|
+
* Whether this repository's releases are identified by a calendar version rather than a shared
|
|
26
|
+
* semver one. Derived, never configured - a repo that picked "highest package version" would only
|
|
27
|
+
* be picking a bug: with two version lines the highest can stay put while a lower one releases,
|
|
28
|
+
* leaving the release with no identity of its own.
|
|
29
|
+
*
|
|
30
|
+
* `groupCount > 1` makes the first call: with several version lines there is no meaningful shared
|
|
31
|
+
* number, so any semver-looking identity would claim something untrue. The other two make it
|
|
32
|
+
* **sticky** - going back would *lower* the root version (`2026.9.15-1430` -> `1.4.0` compares as a
|
|
33
|
+
* decrease), so once a calendar release exists the repo stays on calendar even if its groups later
|
|
34
|
+
* collapse back to one. The last-release-tag check is the authoritative one (tags record what was
|
|
35
|
+
* actually released); the root's own current version covers the case where tags aren't available
|
|
36
|
+
* at all, e.g. a shallow clone.
|
|
37
|
+
*/
|
|
38
|
+
export declare function usesCalendarVersion(options: {
|
|
39
|
+
groupCount: number;
|
|
40
|
+
rootVersion: string;
|
|
41
|
+
lastReleaseVersion?: string;
|
|
42
|
+
}): boolean;
|
|
43
|
+
/** The tag naming a given repository release - `releaseTagPattern` run forward, the way
|
|
44
|
+
* `expandTag` runs `changelog.tagPattern` forward for a package. */
|
|
45
|
+
export declare function expandReleaseTag(root: Package, version: string): string;
|
|
46
|
+
/** The version of the repository's most recent release, from its release tags (highest by version
|
|
47
|
+
* sort) - `undefined` for a repo that has never cut one, or whose tags aren't available (a shallow
|
|
48
|
+
* clone). The authoritative half of `usesCalendarVersion`'s stickiness check: tags record what was
|
|
49
|
+
* actually released, unlike a `package.json` that can drift. */
|
|
50
|
+
export declare function findLastReleaseVersion(git: GitHelper, root: Package): Promise<string | undefined>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { applyTagPattern, extractVersion } from './change-hash.js';
|
|
2
|
+
/**
|
|
3
|
+
* A calendar release version: `YYYY.M.D-HHmm`, every part **unpadded** (`2026.9.5-930`, not
|
|
4
|
+
* `2026.09.05-0930`). The padding isn't a style choice - semver forbids leading zeroes in numeric
|
|
5
|
+
* identifiers, so a padded month or time makes the version invalid, and a monorepo root's
|
|
6
|
+
* `package.json` has to hold a valid one. Unpadded still orders correctly: the date parts compare
|
|
7
|
+
* numerically as major/minor/patch, and the time compares numerically as a prerelease identifier
|
|
8
|
+
* (`930` < `1430`).
|
|
9
|
+
*/
|
|
10
|
+
export const CALENDAR_VERSION_PATTERN = /^\d{4}\.\d{1,2}\.\d{1,2}-\d+$/;
|
|
11
|
+
/** `.rmanrc version.releaseTagPattern` (root-level) - the tag naming the repository's own release,
|
|
12
|
+
* as opposed to the per-package/group tags `changelog.tagPattern` names. Deliberately a **separate**
|
|
13
|
+
* pattern with a non-`v` default: `findLatestTag` resolves a repo-wide package pattern with `git
|
|
14
|
+
* describe --match`, so a release tag that also matched `v*` would be picked up as some package's
|
|
15
|
+
* own last release - corrupting both its changelog boundary and the version its entry is headed
|
|
16
|
+
* with. */
|
|
17
|
+
export function releaseTagPattern(root) {
|
|
18
|
+
const cfg = root.config?.version?.releaseTagPattern;
|
|
19
|
+
return typeof cfg === 'string' && cfg ? cfg : DEFAULT_RELEASE_TAG_PATTERN;
|
|
20
|
+
}
|
|
21
|
+
export function isCalendarVersion(version) {
|
|
22
|
+
return CALENDAR_VERSION_PATTERN.test(version);
|
|
23
|
+
}
|
|
24
|
+
/** `date` as a calendar release version - see `CALENDAR_VERSION_PATTERN` for why nothing is padded.
|
|
25
|
+
* The time becomes a single number (`14:30` -> `1430`, `09:30` -> `930`), which is both unpadded
|
|
26
|
+
* by construction and ordered the way the clock is. */
|
|
27
|
+
export function formatCalendarVersion(date) {
|
|
28
|
+
const time = date.getHours() * 100 + date.getMinutes();
|
|
29
|
+
return `${date.getFullYear()}.${date.getMonth() + 1}.${date.getDate()}-${time}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Whether this repository's releases are identified by a calendar version rather than a shared
|
|
33
|
+
* semver one. Derived, never configured - a repo that picked "highest package version" would only
|
|
34
|
+
* be picking a bug: with two version lines the highest can stay put while a lower one releases,
|
|
35
|
+
* leaving the release with no identity of its own.
|
|
36
|
+
*
|
|
37
|
+
* `groupCount > 1` makes the first call: with several version lines there is no meaningful shared
|
|
38
|
+
* number, so any semver-looking identity would claim something untrue. The other two make it
|
|
39
|
+
* **sticky** - going back would *lower* the root version (`2026.9.15-1430` -> `1.4.0` compares as a
|
|
40
|
+
* decrease), so once a calendar release exists the repo stays on calendar even if its groups later
|
|
41
|
+
* collapse back to one. The last-release-tag check is the authoritative one (tags record what was
|
|
42
|
+
* actually released); the root's own current version covers the case where tags aren't available
|
|
43
|
+
* at all, e.g. a shallow clone.
|
|
44
|
+
*/
|
|
45
|
+
export function usesCalendarVersion(options) {
|
|
46
|
+
if (options.lastReleaseVersion && isCalendarVersion(options.lastReleaseVersion))
|
|
47
|
+
return true;
|
|
48
|
+
if (isCalendarVersion(options.rootVersion))
|
|
49
|
+
return true;
|
|
50
|
+
return options.groupCount > 1;
|
|
51
|
+
}
|
|
52
|
+
/** The tag naming a given repository release - `releaseTagPattern` run forward, the way
|
|
53
|
+
* `expandTag` runs `changelog.tagPattern` forward for a package. */
|
|
54
|
+
export function expandReleaseTag(root, version) {
|
|
55
|
+
return applyTagPattern(releaseTagPattern(root), root.name, version);
|
|
56
|
+
}
|
|
57
|
+
/** The version of the repository's most recent release, from its release tags (highest by version
|
|
58
|
+
* sort) - `undefined` for a repo that has never cut one, or whose tags aren't available (a shallow
|
|
59
|
+
* clone). The authoritative half of `usesCalendarVersion`'s stickiness check: tags record what was
|
|
60
|
+
* actually released, unlike a `package.json` that can drift. */
|
|
61
|
+
export async function findLastReleaseVersion(git, root) {
|
|
62
|
+
const glob = releaseTagPattern(root).replace('{name}', root.name);
|
|
63
|
+
const tag = (await git.listTags(glob))[0];
|
|
64
|
+
return tag ? extractVersion(tag, glob) : undefined;
|
|
65
|
+
}
|
|
66
|
+
const DEFAULT_RELEASE_TAG_PATTERN = 'release-*';
|