import assert from "node:assert/strict" import { execFileSync } from "node:child_process" import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs" import { tmpdir } from "node:os" import { basename, dirname, join, resolve } from "node:path" import { fileURLToPath } from "node:url" import test from "node:test" const source = fileURLToPath(new URL("./compute-release.mjs", import.meta.url)) const cases = [ { name: "исправление повышает patch", commits: [["fix(ui): исправить отображение"]], version: "1.47.7", bump: "patch" }, { name: "новая возможность повышает minor", commits: [["feat(ui): добавить раздел"]], version: "1.48.0", bump: "minor" }, { name: "breaking со scope повышает major", commits: [["chore(release)!: подготовить вторую основную версию"]], version: "2.0.0", bump: "major" }, { name: "breaking без scope повышает major", commits: [["feat!: заменить контракт"]], version: "2.0.0", bump: "major" }, { name: "footer со пробелом повышает major", commits: [["fix(api): заменить контракт", "BREAKING CHANGE: изменён контракт"]], version: "2.0.0", bump: "major" }, { name: "footer с дефисом повышает major", commits: [["fix(api): заменить контракт", "BREAKING-CHANGE: изменён контракт"]], version: "2.0.0", bump: "major" }, { name: "major имеет приоритет в смешанной истории", commits: [["chore(release)!: подготовить выпуск"], ["feat(ui): добавить раздел"], ["fix(ui): исправить отображение"]], version: "2.0.0", bump: "major" }, { name: "после второй версии сохраняется patch", base: "2.0.0", commits: [["fix(ui): исправить отображение"]], version: "2.0.1", bump: "patch" }, { name: "после второй версии сохраняется minor", base: "2.0.0", commits: [["feat(ui): добавить раздел"]], version: "2.1.0", bump: "minor" }, { name: "готовый тег не выпускается повторно", base: "2.0.0", commits: [], version: "2.0.0", bump: "none" }, ] for (const scenario of cases) test(scenario.name, () => { const parent = resolve(tmpdir()) const root = mkdtempSync(join(parent, "mm-release-")) const git = (...args) => execFileSync("git", args, { cwd: root, stdio: "pipe" }) try { mkdirSync(join(root, ".ci/scripts"), { recursive: true }) copyFileSync(source, join(root, ".ci/scripts/compute-release.mjs")) git("init", "--initial-branch=main") git("config", "user.name", "Release Test") git("config", "user.email", "release@example.invalid") git("config", "core.hooksPath", ".git/hooks") writeFileSync(join(root, "fixture"), "baseline") git("add", "fixture") git("commit", "-m", "chore(test): создать исходную базу") git("tag", `v${scenario.base ?? "1.47.6"}`) for (const [index, [subject, body]] of scenario.commits.entries()) { writeFileSync(join(root, "fixture"), `change ${index}`) git("add", "fixture") git("commit", "-m", subject, ...(body ? ["-m", body] : [])) } execFileSync(process.execPath, [join(root, ".ci/scripts/compute-release.mjs")], { cwd: root, stdio: "pipe", env: { ...process.env, BASE_VERSION: "1.0.0", RELEASE_OUTPUT_DIR: ".ci/release", GITHUB_OUTPUT: join(root, "outputs"), GITEA_REPOSITORY: "test/repo", GITEA_SERVER_URL: "https://example.invalid" }, }) const manifest = JSON.parse(readFileSync(join(root, ".ci/release/release-manifest.json"), "utf8")) const output = readFileSync(join(root, "outputs"), "utf8") assert.equal(manifest.version, scenario.version) assert.equal(manifest.tag, `v${scenario.version}`) assert.equal(manifest.releaseUrl, `https://example.invalid/test/repo/releases/tag/v${scenario.version}`) assert.match(output, new RegExp(`^bump=${scenario.bump}$`, "m")) assert.match(output, new RegExp(`^should_release=${scenario.bump !== "none"}$`, "m")) if (scenario.bump !== "none") assert.match(readFileSync(join(root, ".ci/release/release_notes.md"), "utf8"), new RegExp(`# v${scenario.version.replaceAll(".", "\\.")}`)) } finally { assert.equal(dirname(resolve(root)), parent) assert.ok(basename(root).startsWith("mm-release-")) rmSync(root, { recursive: true, force: true }) } })