diff --git a/README.md b/README.md index 1e517e8..ef370cb 100644 --- a/README.md +++ b/README.md @@ -133,9 +133,16 @@ Same as primary action: `changelog`, `release-type`, `commit-count`, `relevant-c This action lives under `/last-tag`. Same step as the primary action uses. -Inputs: none +##Inputs -Outputs: `last-tag`, if resolved. +| Key | Required | Default | Description | +|----------|----------|---------|----------------------------------------------------------------------------------| +| `before` | :x: | | If specified, the last tag must come before this tag sorted by semantic version. | +| `after` | :x: | | If specified, the last tag must come after this tag sorted by semantic version. | + +## Outputs + +`last-tag`, if resolved. # Resolve the next tag to release diff --git a/src/lib/semver.mts b/src/lib/semver.mts index 66d0af9..ea0c121 100644 --- a/src/lib/semver.mts +++ b/src/lib/semver.mts @@ -18,14 +18,13 @@ class SemVer { this.#patch = fmtNum(patch); } - public static cmp(a: SemVer | undefined, b: SemVer | undefined): 1 | 0 | -1 { - if (a == null) { - return b == null ? 0 : -1; - } else if (b == null) { - return -1; - } - - return cmpNum(a.major, b.major) ?? cmpNum(a.#minor, b.#minor) ?? cmpNum(a.#patch, b.#patch) ?? 0; + /** Sorts in descending order, i.e. a higher version will come before a lower version */ + public static cmp(a: SemVer, b: SemVer): 1 | 0 | -1 { + return cmpNum(a.major, b.major) + ?? cmpNum(a.#minor, b.#minor) + ?? cmpNum(a.#patch, b.#patch) + ?? cmpPrefixed(a.prefixed, b.prefixed) + ?? 0; } public static parse(from: string, allowStrippingVPrefix = false): SemVer | undefined { @@ -38,23 +37,12 @@ class SemVer { } public static async resolveLastRelease(): Promise { - let raw: string; - try { - raw = await exec(`git tag --list`, `getting last tag`, false); - } catch { - return; // no tags created yet - } - if (!raw) { - return; // same - } + const [last] = await SemVer.resolveReleases(); - const semvers = raw.split(/\r?\n/g).map(v => SemVer.parse(v, true)); - semvers.sort(SemVer.cmp); - - if (semvers[0]) { - info(`Last tag resolved to ${semvers[0]}`); + if (last) { + info(`Last tag resolved to ${last}`); - return semvers[0]; + return last; } } @@ -70,6 +58,32 @@ class SemVer { return new SemVer(1, 0, 0); } + /** Newest at the start of the array */ + public static async resolveReleases(): Promise { + let raw: string; + try { + raw = await exec(`git tag --list`, `getting last tag`, false); + } catch { + return []; // no tags created yet + } + if (!raw) { + return []; // same + } + + const semvers = raw.split(/\r?\n/g) + .reduce((acc, v) => { + const parsed = SemVer.parse(v, true); + if (parsed) { + acc.push(parsed); + } + + return acc; + }, []); + semvers.sort(SemVer.cmp); + + return semvers; + } + public get minor(): number { return this.#minor ?? 0; } @@ -128,6 +142,18 @@ class SemVer { return this; } + public isEqual(other: SemVer): boolean { + return SemVer.cmp(this, other) === 0; + } + + public isGreaterThan(other: SemVer): boolean { + return SemVer.cmp(this, other) === -1; + } + + public isLowerThan(other: SemVer): boolean { + return SemVer.cmp(this, other) === 1; + } + /** Clone & ensure the prefix & all major/minor/patch versions are set */ public materialise(): SemVer { return new SemVer(this.major, this.minor, this.patch); @@ -160,6 +186,14 @@ function fmtNum(num: number | string | undefined): number | undefined { } } +function cmpPrefixed(a: boolean, b: boolean): 1 | -1 | undefined { + if (a && !b) { + return -1; + } else if (!a && b) { + return 1; + } +} + function cmpNum(a: number | undefined, b: number | undefined): 1 | -1 | undefined { if (a == null) { return b == null ? undefined : 1; diff --git a/src/modules/last-tag/action.yml b/src/modules/last-tag/action.yml index b83b46c..c2c2865 100644 --- a/src/modules/last-tag/action.yml +++ b/src/modules/last-tag/action.yml @@ -1,5 +1,10 @@ name: 'Semantic Release Lite: Last tag' description: Gets the last release's tag, if any +inputs: + after: + description: If specified, the last tag must come after this tag sorted by semantic version. + before: + description: If specified, the last tag must come before this tag sorted by semantic version. runs: using: node20 main: last-tag.js diff --git a/src/modules/last-tag/index.mts b/src/modules/last-tag/index.mts index d85c4a0..47a8f1e 100644 --- a/src/modules/last-tag/index.mts +++ b/src/modules/last-tag/index.mts @@ -1,11 +1,51 @@ -import {info, isDebug, setFailed} from '@actions/core'; +import {getInput, info, isDebug, setFailed} from '@actions/core'; +import InputMgr from '../../lib/input-mgr.mjs'; import OutputMgr from '../../lib/output-mgr.mjs'; import {SemVer} from '../../lib/semver.mjs'; import {ReleaseOutputName} from '../../output-mgr.mjs'; import Valueify = OutputMgr.Valueify; +interface Inputs { + after?: SemVer; + + before?: SemVer; +} + (async function getLastTag() { - const lastTag = await SemVer.resolveLastRelease(); + function loadInput(name: keyof Inputs): () => SemVer | undefined { + return function actualInputLoader() { + const str = getInput(name); + if (!str) { + return; + } + + const semVer = SemVer.parse(str); + if (semVer) { + return semVer; + } + + throw new Error('Invalid SemVer string'); + }; + } + + const inputs = new InputMgr({ + after: loadInput('after'), + before: loadInput('before'), + }); + inputs.load(); + + let tags = await SemVer.resolveReleases(); + if (inputs.after) { + if (inputs.before) { + tags = tags.filter(tag => tag.isGreaterThan(inputs.after!) && tag.isLowerThan(inputs.before!)); + } else { + tags = tags.filter(tag => tag.isGreaterThan(inputs.after!)); + } + } else if (inputs.before) { + tags = tags.filter(tag => tag.isLowerThan(inputs.before!)); + } + + const lastTag = tags[0]; if (lastTag) { new OutputMgr>() .set(ReleaseOutputName.LastTag, lastTag.toString())