/** * Release-tag validation (scripts/releaseVersion.mjs). * * Covered by the suite because this function is the only thing between a mistyped git tag and an * unintended npm publish — and an npm version can never be reused once taken, not even after * `npm unpublish`. Every rejection below is a release that would otherwise have shipped, or a * legitimate release that would otherwise have been blocked. */ import { describe, expect, it } from 'vitest'; import { versionFromTag } from '../../../../../../scripts/releaseVersion.mjs'; describe('versionFromTag', () => { it('uses the tag as the published version', () => { // The whole point of the scheme: no timestamp is generated, so the tag on GitHub and the // version on npm cannot disagree. (Not quite VERBATIM any more — a padded day is normalised, // see below — but still one-to-one and readable in both directions.) expect(versionFromTag('2607.31.1')).toBe('2607.31.1'); expect(versionFromTag('2601.1.1')).toBe('2601.1.1'); expect(versionFromTag('2612.9.12')).toBe('2612.9.12'); }); it('tolerates a v or release- prefix rather than reading it as a different version', () => { // The repo's previous scheme tagged `release-*`. Stripping the prefix means an old-style tag // publishes the version it visibly names instead of failing obscurely. expect(versionFromTag('v2607.31.1')).toBe('2607.31.1'); expect(versionFromTag('release-2607.31.1')).toBe('2607.31.1'); }); /** * A PADDED DAY IS ACCEPTED, and publishes unpadded (Jazz, 2026-08-07). * * *"I wanted `2608.07.2` to be OK, YYMM.DD.V+"* — asked for after two release attempts were spent on it, * which is the measure of how natural `07` is to write for a date. * * It cannot be the published version, and that is not this repo's choice: semver forbids leading zeros in a * numeric identifier, so `2608.07.2` is not a version at all — `yarn version` refuses it and * `semver.valid()` returns null. So the tag may be padded and the version is normalised. */ it('accepts a zero-padded day and publishes it unpadded', () => { expect(versionFromTag('2608.07.2')).toBe('2608.7.2'); expect(versionFromTag('2607.01.1')).toBe('2607.1.1'); // The count too, for the same reason and by the same rule. expect(versionFromTag('2607.1.01')).toBe('2607.1.1'); expect(versionFromTag('2607.09.09')).toBe('2607.9.9'); // Two digits is the most a day can be. `007` is not a spelling of a date and the shape check refuses it, // which is where that belongs — normalising it would accept a tag nobody means to write. expect(() => versionFromTag('2607.007.1')).toThrow(/yymm\.dd\.count/); }); it('maps both spellings of a day to ONE version, so neither can publish twice', () => { // Tagging `2608.07.2` after `2608.7.2` must not produce a second, different release. It reaches npm with // a version that already exists and fails there, which is the behaviour that cannot be got wrong. expect(versionFromTag('2608.07.2')).toBe(versionFromTag('2608.7.2')); }); it('refuses a leading zero in the yymm segment, which cannot be normalised', () => { // `0801` is January 2008; `801` is nothing. Stripping it would change the month, so this one is refused // rather than mangled — unreachable until 2100, and the point of this script is never to emit a version // npm will reject. expect(() => versionFromTag('0801.1.1')).toThrow(/leading zero/); expect(() => versionFromTag('0801.1.1')).toThrow(/cannot be normalised/); }); it('refuses a shape that is not yymm.dd.count', () => { expect(() => versionFromTag('2607.31')).toThrow(/yymm\.dd\.count/); expect(() => versionFromTag('26.7.31.1')).toThrow(/yymm\.dd\.count/); expect(() => versionFromTag('0.0.20260731120000')).toThrow( /yymm\.dd\.count/, ); expect(() => versionFromTag('release-candidate')).toThrow( /yymm\.dd\.count/, ); expect(() => versionFromTag('2607.31.1-beta')).toThrow(/yymm\.dd\.count/); }); it('refuses an impossible date', () => { expect(() => versionFromTag('2607.32.1')).toThrow(/not a day of the month/); expect(() => versionFromTag('2607.0.1')).toThrow(/not a day of the month/); expect(() => versionFromTag('2613.1.1')).toThrow(/not a month/); expect(() => versionFromTag('2600.1.1')).toThrow(/not a month/); }); it('refuses a zeroth release of a day', () => { expect(() => versionFromTag('2607.31.0')).toThrow( /first release of a day is 1/, ); }); it('refuses an absent tag rather than publishing something', () => { expect(() => versionFromTag('')).toThrow(/no version to publish/); expect(() => versionFromTag(undefined)).toThrow(/no version to publish/); }); it('produces versions that sort in release order', () => { // The scheme has to be monotonic or npm will reject a later release as a downgrade — which // is exactly what bit the previous 0.0. scheme under Yarn Berry. const ordered = [ '2601.1.1', '2601.1.2', '2601.9.1', '2601.10.1', '2607.31.1', '2612.1.1', ]; const asKey = (v: string) => { const [yymm, d, c] = v.split('.').map(Number); return yymm * 1_000_000 + d * 1_000 + c; }; const keys = ordered.map(asKey); expect([...keys].sort((a, b) => a - b)).toEqual(keys); }); });