Aventuras/scripts/version.test.js
Pento 6ca1673d2d
Validated release script, extracted version arithmetic, lint config fixes (#416)
## Release script

- Version arithmetic extracted into `scripts/version.js`, covered by
`scripts/version.test.js`
(17 cases). `vitest.config.ts` now includes `scripts/**/*.test.js` for
it.
- **Every precondition is checked before anything is written**: a clean
tree, a version that
moves forward, and a tag/branch that exists neither locally *nor on the
remote*. A failure
after that point deletes the branch and tag it created and returns to
the original branch.
- `--dry-run` runs the checks and stops. `--no-merge-back` skips the
fast-forward of the
  branch the script was run from.
- Unknown flags are rejected rather than silently ignored.
- The rollback's `reset --hard` is guarded behind a **successful
checkout**: it moves the
*current* branch, so a failed checkout dragged the release branch onto
the start commit.
- Everything after the push is explicitly best-effort and is **not**
rolled back — the tag is
out and the release is building, so a protected branch or a race there
is a reason to print
  two commands, not to unpick a published release.
- Only `X.Y.Z` and `X.Y.Z-pre.N` are accepted. Other pre-release
spellings are valid semver
but match neither workflow trigger, so they would tag and build nothing.
- Works on Windows again.

Note the `--` in `npm run release -- <bump>`: without it npm consumes
the flags before the
script sees them. Documented in the README.

## eslint

`eslint-plugin-boundaries` v7 renamed `rules` to `policies` and takes
the element type inside
the `disallow` target rather than in a second outer `to`. The config was
still written for the
v6 shape — it worked, but on the deprecated path.

Verified **warning-for-warning identical** across the repo: 189
warnings, same files, same
positions, before and after.

`mode: 'folder'` dropped where it is already the default.

## Android

`src-tauri/gen/android/app/tauri.build.gradle.kts` is autogenerated by
the Tauri CLI and was
being regenerated on every build, so it showed up as a dirty file
constantly. Untracked and
gitignored.

---

Base: `upstream/master`. `check`, `lint` and the full Vitest suite pass.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Improved release automation with validation, dry-run support, rollback
handling, and optional branch merge-back.
* Added support for stable and prerelease version validation,
comparison, and version bumping.

* **Bug Fixes**
* Added safeguards for repository state, remote references, network
availability, and Windows npm execution.

* **Documentation**
* Expanded release instructions with supported version formats, safety
checks, rollback behavior, and usage options.

* **Tests**
* Added comprehensive coverage for version handling and release-related
scenarios.
2026-08-08 19:55:23 +02:00

107 lines
4 KiB
JavaScript

import { describe, it, expect } from 'vitest'
import { bumpVersion, isValidVersion, parseVersion, compareVersions } from './version.js'
describe('bumpVersion', () => {
it('bumps a stable version', () => {
expect(bumpVersion('0.7.6', 'patch')).toBe('0.7.7')
expect(bumpVersion('0.7.6', 'minor')).toBe('0.8.0')
expect(bumpVersion('0.7.6', 'major')).toBe('1.0.0')
})
it('promotes a pre-release that already sits at the level being bumped', () => {
expect(bumpVersion('0.7.6-pre.1', 'patch')).toBe('0.7.6')
expect(bumpVersion('0.8.0-pre.1', 'minor')).toBe('0.8.0')
expect(bumpVersion('1.0.0-pre.1', 'major')).toBe('1.0.0')
})
it('never moves backwards from a pre-release', () => {
expect(bumpVersion('0.7.6-pre.1', 'minor')).toBe('0.8.0')
expect(bumpVersion('0.7.6-pre.1', 'major')).toBe('1.0.0')
})
it('moves the patch before starting a pre-release from a stable version', () => {
// The alternative sorts before the version already shipped: an offered downgrade.
expect(bumpVersion('0.7.6', 'prerelease')).toBe('0.7.7-pre.1')
expect(compareVersions(bumpVersion('0.7.6', 'prerelease'), '0.7.6')).toBeGreaterThan(0)
})
it('counts up within a pre-release series', () => {
expect(bumpVersion('0.7.7-pre.1', 'prerelease')).toBe('0.7.7-pre.2')
expect(bumpVersion('0.7.7-pre.9', 'prerelease')).toBe('0.7.7-pre.10')
})
it('every bump type produces a version strictly greater than its input', () => {
const inputs = ['0.0.0', '0.7.6', '1.0.0', '0.7.6-pre.1', '0.8.0-pre.2', '1.0.0-pre.1']
for (const from of inputs) {
for (const type of ['major', 'minor', 'patch', 'prerelease']) {
const to = bumpVersion(from, type)
expect(
compareVersions(to, from),
`${from} + ${type} produced ${to}, which does not follow it`,
).toBeGreaterThan(0)
}
}
})
it('passes a literal version straight through', () => {
expect(bumpVersion('0.7.6', '1.2.3')).toBe('1.2.3')
})
it('returns undefined when no type was given, which the caller reports as usage', () => {
expect(bumpVersion('0.7.6', undefined)).toBeUndefined()
})
})
describe('isValidVersion', () => {
it('accepts the two shapes the release workflows build', () => {
expect(isValidVersion('0.7.6')).toBe(true)
expect(isValidVersion('0.7.6-pre.1')).toBe(true)
expect(isValidVersion('10.20.30')).toBe(true)
})
it('rejects a pre-release shape no workflow would build', () => {
expect(isValidVersion('1.2.3-beta.1')).toBe(false)
expect(isValidVersion('1.2.3-pre')).toBe(false)
})
it('rejects anything that is not a version', () => {
expect(isValidVersion('banana')).toBe(false)
expect(isValidVersion('; rm -rf /')).toBe(false)
expect(isValidVersion('1.2')).toBe(false)
expect(isValidVersion('v1.2.3')).toBe(false)
expect(isValidVersion('')).toBe(false)
expect(isValidVersion(undefined)).toBe(false)
})
})
describe('compareVersions', () => {
it('orders a pre-release before the version it leads to', () => {
expect(compareVersions('0.7.6-pre.1', '0.7.6')).toBeLessThan(0)
expect(compareVersions('0.7.6', '0.7.6-pre.1')).toBeGreaterThan(0)
})
it('orders a pre-release series numerically, not as text', () => {
expect(compareVersions('0.7.6-pre.9', '0.7.6-pre.10')).toBeLessThan(0)
})
it('orders by major, then minor, then patch', () => {
expect(compareVersions('0.8.0', '0.7.9')).toBeGreaterThan(0)
expect(compareVersions('1.0.0', '0.99.99')).toBeGreaterThan(0)
expect(compareVersions('0.7.6', '0.7.6')).toBe(0)
})
it('refuses to compare something it cannot parse', () => {
expect(() => compareVersions('banana', '0.7.6')).toThrow()
})
})
describe('parseVersion', () => {
it('returns null rather than a half-parsed object', () => {
expect(parseVersion('banana')).toBeNull()
})
it('distinguishes no pre-release from pre-release zero', () => {
expect(parseVersion('0.7.6').pre).toBeNull()
expect(parseVersion('0.7.6-pre.0').pre).toBe(0)
})
})