mirror of
https://github.com/larksuite/cli.git
synced 2026-08-03 08:32:46 +08:00
612 lines
21 KiB
JavaScript
612 lines
21 KiB
JavaScript
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
const assert = require("node:assert/strict");
|
|
const { spawnSync } = require("node:child_process");
|
|
const fs = require("node:fs");
|
|
const os = require("node:os");
|
|
const path = require("node:path");
|
|
const { describe, it } = require("node:test");
|
|
|
|
const {
|
|
validateReleasePreflight,
|
|
} = require("./release-preflight");
|
|
|
|
const repoRoot = path.resolve(__dirname, "..");
|
|
|
|
function createReleaseFixture(t, env = {}) {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "tag-release-test-"));
|
|
const scriptsDir = path.join(root, "scripts");
|
|
const binDir = path.join(root, "bin");
|
|
const stateDir = path.join(root, "state");
|
|
const logPath = path.join(root, "git-calls.jsonl");
|
|
const npmLogPath = path.join(root, "npm-calls.jsonl");
|
|
fs.mkdirSync(scriptsDir);
|
|
fs.mkdirSync(binDir);
|
|
fs.mkdirSync(stateDir);
|
|
fs.copyFileSync(
|
|
path.join(repoRoot, "scripts/release-preflight.js"),
|
|
path.join(scriptsDir, "release-preflight.js"),
|
|
);
|
|
fs.copyFileSync(
|
|
path.join(repoRoot, "scripts/tag-release.sh"),
|
|
path.join(scriptsDir, "tag-release.sh"),
|
|
);
|
|
fs.writeFileSync(path.join(root, "package.json"), '{"version":"1.2.3-beta.0"}\n');
|
|
fs.writeFileSync(
|
|
path.join(root, "package-lock.json"),
|
|
'{"version":"1.2.3-beta.0","packages":{"":{"version":"1.2.3-beta.0"}}}\n',
|
|
);
|
|
|
|
const fakeGitPath = path.join(binDir, "git");
|
|
fs.writeFileSync(fakeGitPath, String.raw`#!/usr/bin/env node
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
const args = process.argv.slice(2);
|
|
const stateDir = process.env.FAKE_GIT_STATE_DIR;
|
|
const localTagPath = path.join(stateDir, "local-tag");
|
|
if (process.cwd() !== process.env.FAKE_EXPECTED_GIT_CWD) {
|
|
process.stderr.write("git invoked outside repository root: " + process.cwd() + "\n");
|
|
process.exit(96);
|
|
}
|
|
fs.appendFileSync(process.env.FAKE_GIT_LOG, JSON.stringify(args) + "\n");
|
|
|
|
function print(value) {
|
|
process.stdout.write(value + "\n");
|
|
}
|
|
|
|
switch (args[0]) {
|
|
case "branch":
|
|
print(process.env.FAKE_BRANCH || "test/npm-staged-publish-rehearsal");
|
|
break;
|
|
case "status":
|
|
if (process.env.FAKE_STATUS_OUTPUT) print(process.env.FAKE_STATUS_OUTPUT);
|
|
break;
|
|
case "fetch":
|
|
break;
|
|
case "rev-parse": {
|
|
const ref = args[args.length - 1];
|
|
if (ref === "HEAD") {
|
|
print(process.env.FAKE_HEAD_SHA);
|
|
break;
|
|
}
|
|
if (ref === "FETCH_HEAD^{commit}") {
|
|
print(process.env.FAKE_REHEARSAL_SHA);
|
|
break;
|
|
}
|
|
if (ref.startsWith("refs/tags/")) {
|
|
if (fs.existsSync(localTagPath)) {
|
|
print(fs.readFileSync(localTagPath, "utf8").trim());
|
|
break;
|
|
}
|
|
process.exit(1);
|
|
}
|
|
process.stderr.write("unexpected rev-parse ref: " + ref + "\n");
|
|
process.exit(97);
|
|
break;
|
|
}
|
|
case "ls-remote": {
|
|
const tagRef = args.find((arg) => arg.startsWith("refs/tags/") && !arg.endsWith("^{}"));
|
|
const kind = process.env.FAKE_REMOTE_TAG_KIND || "absent";
|
|
if (kind === "lightweight" || kind === "annotated") {
|
|
print(process.env.FAKE_REMOTE_TAG_SHA + "\t" + tagRef);
|
|
}
|
|
break;
|
|
}
|
|
case "show":
|
|
print(process.env.FAKE_WORKFLOW);
|
|
break;
|
|
case "tag":
|
|
fs.writeFileSync(localTagPath, args[2] || process.env.FAKE_HEAD_SHA);
|
|
break;
|
|
case "push": {
|
|
const failedMarker = path.join(stateDir, "push-failed");
|
|
if (process.env.FAKE_PUSH_FAIL_ONCE && !fs.existsSync(failedMarker)) {
|
|
fs.writeFileSync(failedMarker, "1");
|
|
process.exit(Number(process.env.FAKE_PUSH_FAIL_ONCE));
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
process.stderr.write("unexpected git command: " + args.join(" ") + "\n");
|
|
process.exit(97);
|
|
}
|
|
`);
|
|
fs.chmodSync(fakeGitPath, 0o755);
|
|
|
|
const fakeNpmPath = path.join(binDir, "npm");
|
|
fs.writeFileSync(fakeNpmPath, String.raw`#!/usr/bin/env node
|
|
const fs = require("node:fs");
|
|
const args = process.argv.slice(2);
|
|
fs.appendFileSync(process.env.FAKE_NPM_LOG, JSON.stringify(args) + "\n");
|
|
if (args[0] !== "view") {
|
|
process.stderr.write("unexpected npm command: " + args.join(" ") + "\n");
|
|
process.exit(97);
|
|
}
|
|
const output = process.env.FAKE_NPM_VIEW_OUTPUT || "npm error code E404\nnpm error 404 Not Found";
|
|
(Number(process.env.FAKE_NPM_VIEW_STATUS || "1") === 0 ? process.stdout : process.stderr).write(output + "\n");
|
|
process.exit(Number(process.env.FAKE_NPM_VIEW_STATUS || "1"));
|
|
`);
|
|
fs.chmodSync(fakeNpmPath, 0o755);
|
|
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
|
|
return {
|
|
root,
|
|
stateDir,
|
|
logPath,
|
|
env: {
|
|
...process.env,
|
|
PATH: `${binDir}${path.delimiter}${process.env.PATH}`,
|
|
LANG: "C",
|
|
LC_ALL: "C",
|
|
FAKE_GIT_LOG: logPath,
|
|
FAKE_NPM_LOG: npmLogPath,
|
|
FAKE_GIT_STATE_DIR: stateDir,
|
|
FAKE_EXPECTED_GIT_CWD: fs.realpathSync(root),
|
|
FAKE_HEAD_SHA: "aaaaaaaa",
|
|
FAKE_REHEARSAL_SHA: "aaaaaaaa",
|
|
FAKE_WORKFLOW: "args: release --clean --skip=publish\nrun: npm stage publish package.tgz --access public --tag beta",
|
|
...env,
|
|
},
|
|
};
|
|
}
|
|
|
|
function runTagRelease(fixture, options = {}) {
|
|
const { cwd = fixture.root, args = [], input = "" } = options;
|
|
return spawnSync("bash", [path.join(fixture.root, "scripts/tag-release.sh"), ...args], {
|
|
cwd,
|
|
env: fixture.env,
|
|
encoding: "utf8",
|
|
input,
|
|
});
|
|
}
|
|
|
|
function readGitCalls(fixture) {
|
|
if (!fs.existsSync(fixture.logPath)) {
|
|
return [];
|
|
}
|
|
return fs.readFileSync(fixture.logPath, "utf8")
|
|
.trim()
|
|
.split("\n")
|
|
.filter(Boolean)
|
|
.map((line) => JSON.parse(line));
|
|
}
|
|
|
|
function assertNoTagOperations(calls) {
|
|
const tagOperations = calls.filter((args) =>
|
|
args[0] === "ls-remote" ||
|
|
args[0] === "tag" ||
|
|
args[0] === "push" ||
|
|
(args[0] === "rev-parse" && args.some((arg) => arg.startsWith("refs/tags/"))),
|
|
);
|
|
assert.deepEqual(tagOperations, []);
|
|
}
|
|
|
|
function assertNoTagWrites(calls) {
|
|
assert.equal(calls.some((args) => args[0] === "tag" || args[0] === "push"), false);
|
|
}
|
|
|
|
function validInputs(version = "1.2.3") {
|
|
return {
|
|
packageJson: { version },
|
|
packageLockJson: {
|
|
version,
|
|
packages: { "": { version } },
|
|
},
|
|
};
|
|
}
|
|
|
|
function assertStructuredError(result) {
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.error.type, "release_preflight");
|
|
assert.equal(typeof result.error.message, "string");
|
|
assert.ok(result.error.message.length > 0);
|
|
assert.equal(typeof result.error.observed, "object");
|
|
assert.equal(typeof result.error.hint, "string");
|
|
assert.ok(result.error.hint.length > 0);
|
|
}
|
|
|
|
function assertInOrder(source, snippets) {
|
|
let previous = -1;
|
|
for (const snippet of snippets) {
|
|
const index = source.indexOf(snippet);
|
|
assert.ok(index >= 0, `missing fragment: ${snippet}`);
|
|
assert.ok(index > previous, `fragment is out of order: ${snippet}`);
|
|
previous = index;
|
|
}
|
|
}
|
|
|
|
describe("validateReleasePreflight", () => {
|
|
it("accepts matching stable and beta rehearsal versions", () => {
|
|
for (const version of ["1.2.3", "1.2.3-beta.0"]) {
|
|
const { packageJson, packageLockJson } = validInputs(version);
|
|
|
|
assert.deepEqual(validateReleasePreflight(packageJson, packageLockJson), {
|
|
ok: true,
|
|
data: {
|
|
packageVersion: version,
|
|
lockVersion: version,
|
|
lockRootVersion: version,
|
|
tagVersion: null,
|
|
},
|
|
});
|
|
assert.deepEqual(
|
|
validateReleasePreflight(packageJson, packageLockJson, `v${version}`),
|
|
{
|
|
ok: true,
|
|
data: {
|
|
packageVersion: version,
|
|
lockVersion: version,
|
|
lockRootVersion: version,
|
|
tagVersion: version,
|
|
},
|
|
},
|
|
);
|
|
}
|
|
});
|
|
|
|
it("rejects prerelease forms other than beta rehearsal versions", () => {
|
|
const { packageJson, packageLockJson } = validInputs("1.2.3-rc.1");
|
|
|
|
const result = validateReleasePreflight(packageJson, packageLockJson);
|
|
|
|
assertStructuredError(result);
|
|
assert.equal(
|
|
result.error.message,
|
|
"package.json.version must use X.Y.Z or the rehearsal form X.Y.Z-beta.N",
|
|
);
|
|
assert.equal(
|
|
result.error.hint,
|
|
"Use the same version in all package fields; only stable releases and the temporary beta rehearsal form are allowed.",
|
|
);
|
|
});
|
|
|
|
it("rejects build metadata package versions with the stable release contract", () => {
|
|
const { packageJson, packageLockJson } = validInputs("1.2.3+build.7");
|
|
|
|
const result = validateReleasePreflight(packageJson, packageLockJson);
|
|
|
|
assertStructuredError(result);
|
|
assert.equal(
|
|
result.error.message,
|
|
"package.json.version must use X.Y.Z or the rehearsal form X.Y.Z-beta.N",
|
|
);
|
|
assert.equal(
|
|
result.error.hint,
|
|
"Use the same version in all package fields; only stable releases and the temporary beta rehearsal form are allowed.",
|
|
);
|
|
});
|
|
|
|
it("rejects invalid and missing package or lock SemVer values", () => {
|
|
const invalid = validInputs();
|
|
invalid.packageJson.version = "01.2.3";
|
|
const missing = validInputs();
|
|
delete missing.packageLockJson.packages[""].version;
|
|
|
|
for (const result of [
|
|
validateReleasePreflight(invalid.packageJson, invalid.packageLockJson),
|
|
validateReleasePreflight(missing.packageJson, missing.packageLockJson),
|
|
]) {
|
|
assertStructuredError(result);
|
|
}
|
|
});
|
|
|
|
it("rejects a top-level package-lock version mismatch", () => {
|
|
const { packageJson, packageLockJson } = validInputs();
|
|
packageLockJson.version = "1.2.4";
|
|
|
|
const result = validateReleasePreflight(packageJson, packageLockJson);
|
|
|
|
assertStructuredError(result);
|
|
assert.deepEqual(result.error.observed, {
|
|
packageVersion: "1.2.3",
|
|
lockVersion: "1.2.4",
|
|
lockRootVersion: "1.2.3",
|
|
tagVersion: null,
|
|
});
|
|
});
|
|
|
|
it("rejects a package-lock root package version mismatch", () => {
|
|
const { packageJson, packageLockJson } = validInputs();
|
|
packageLockJson.packages[""].version = "1.2.4";
|
|
|
|
const result = validateReleasePreflight(packageJson, packageLockJson);
|
|
|
|
assertStructuredError(result);
|
|
assert.deepEqual(result.error.observed, {
|
|
packageVersion: "1.2.3",
|
|
lockVersion: "1.2.3",
|
|
lockRootVersion: "1.2.4",
|
|
tagVersion: null,
|
|
});
|
|
});
|
|
|
|
it("rejects invalid and mismatched tags", () => {
|
|
const { packageJson, packageLockJson } = validInputs();
|
|
|
|
for (const tag of ["1.2.3", "v01.2.3", "v1.2.4"]) {
|
|
const result = validateReleasePreflight(packageJson, packageLockJson, tag);
|
|
assertStructuredError(result);
|
|
assert.equal(result.error.observed.tag, tag);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("release configuration", () => {
|
|
it("writes success to stdout and structured failures to stderr", () => {
|
|
const scriptPath = path.join(repoRoot, "scripts/release-preflight.js");
|
|
const packageVersion = require(path.join(repoRoot, "package.json")).version;
|
|
const success = spawnSync(process.execPath, [scriptPath, "--tag", `v${packageVersion}`], {
|
|
cwd: repoRoot,
|
|
encoding: "utf8",
|
|
});
|
|
const failure = spawnSync(process.execPath, [scriptPath, "--tag", "invalid"], {
|
|
cwd: repoRoot,
|
|
encoding: "utf8",
|
|
});
|
|
|
|
assert.equal(success.status, 0);
|
|
assert.equal(success.stderr, "");
|
|
assert.deepEqual(JSON.parse(success.stdout), {
|
|
ok: true,
|
|
data: {
|
|
packageVersion,
|
|
lockVersion: packageVersion,
|
|
lockRootVersion: packageVersion,
|
|
tagVersion: packageVersion,
|
|
},
|
|
});
|
|
assert.equal(failure.status, 1);
|
|
assert.equal(failure.stdout, "");
|
|
assertStructuredError(JSON.parse(failure.stderr));
|
|
});
|
|
|
|
it("keeps package metadata synchronized without changing the Node engine", () => {
|
|
const packageJson = require(path.join(repoRoot, "package.json"));
|
|
const packageLockJson = require(path.join(repoRoot, "package-lock.json"));
|
|
|
|
assert.equal(packageJson.scripts["release:check"], "node scripts/release-preflight.js");
|
|
assert.equal(packageJson.engines.node, ">=16");
|
|
assert.equal(packageLockJson.version, packageJson.version);
|
|
assert.equal(packageLockJson.packages[""].version, packageJson.version);
|
|
});
|
|
|
|
it("runs every release gate before any tag query, creation, or push", () => {
|
|
const script = fs.readFileSync(path.join(repoRoot, "scripts/tag-release.sh"), "utf8");
|
|
const preflight = script.indexOf('node "${SCRIPT_DIR}/release-preflight.js" --tag "${TAG}"');
|
|
const requiredGates = [
|
|
'CURRENT_BRANCH=$(git branch --show-current)',
|
|
'git status --porcelain',
|
|
'git fetch origin "${REHEARSAL_BRANCH}"',
|
|
'git rev-parse "FETCH_HEAD^{commit}"',
|
|
'git show "${HEAD_SHA}:.github/workflows/release.yml"',
|
|
'npm view "@larksuite/cli@${VERSION}" version',
|
|
];
|
|
const tagOperations = [
|
|
'git rev-parse -q --verify "refs/tags/${TAG}"',
|
|
'git ls-remote --tags origin "refs/tags/${TAG}"',
|
|
'git tag "${TAG}" "${HEAD_SHA}"',
|
|
'git push origin "refs/tags/${TAG}:refs/tags/${TAG}"',
|
|
];
|
|
|
|
assert.ok(preflight >= 0, "release preflight invocation is missing");
|
|
assertInOrder(script, [
|
|
'REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"',
|
|
'cd "${REPO_ROOT}"',
|
|
'node "${SCRIPT_DIR}/release-preflight.js"',
|
|
]);
|
|
assert.equal(script.includes("require('${REPO_ROOT}/package.json')"), false);
|
|
for (const gate of requiredGates) {
|
|
const index = script.indexOf(gate);
|
|
assert.ok(index >= 0, `required release gate is missing: ${gate}`);
|
|
assert.ok(index < script.indexOf(tagOperations[0]), `${gate} must run before tag queries`);
|
|
}
|
|
for (const operation of tagOperations) {
|
|
const index = script.indexOf(operation);
|
|
assert.ok(index >= 0, `tag operation is missing: ${operation}`);
|
|
assert.ok(preflight < index, `preflight must run before: ${operation}`);
|
|
}
|
|
assertInOrder(script, [
|
|
'if [ "${PUSH_TAG}" != true ]',
|
|
'read -r CONFIRM_TAG',
|
|
'git tag "${TAG}" "${HEAD_SHA}"',
|
|
'git push origin "refs/tags/${TAG}:refs/tags/${TAG}"',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("tag-release.sh behavior", () => {
|
|
it("runs repository checks from the script repository when invoked elsewhere", (t) => {
|
|
const fixture = createReleaseFixture(t);
|
|
const outside = fs.mkdtempSync(path.join(os.tmpdir(), "tag-release-cwd-"));
|
|
t.after(() => fs.rmSync(outside, { recursive: true, force: true }));
|
|
|
|
const result = runTagRelease(fixture, { cwd: outside });
|
|
|
|
assert.equal(result.status, 0, result.stderr);
|
|
});
|
|
|
|
it("rejects a non-rehearsal branch before querying or modifying tags", (t) => {
|
|
const fixture = createReleaseFixture(t, { FAKE_BRANCH: "feature/release" });
|
|
|
|
const result = runTagRelease(fixture);
|
|
const calls = readGitCalls(fixture);
|
|
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /must be created from test\/npm-staged-publish-rehearsal/i);
|
|
assertNoTagOperations(calls);
|
|
});
|
|
|
|
it("rejects a dirty working tree before tag operations", (t) => {
|
|
const fixture = createReleaseFixture(t, { FAKE_STATUS_OUTPUT: " M README.md" });
|
|
const result = runTagRelease(fixture);
|
|
const calls = readGitCalls(fixture);
|
|
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /working tree must be clean/i);
|
|
assertNoTagOperations(calls);
|
|
});
|
|
|
|
it("rejects HEAD that differs from the fetched rehearsal branch", (t) => {
|
|
const fixture = createReleaseFixture(t, { FAKE_REHEARSAL_SHA: "bbbbbbbb" });
|
|
|
|
const result = runTagRelease(fixture);
|
|
const calls = readGitCalls(fixture);
|
|
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /HEAD must exactly match origin\/test\/npm-staged-publish-rehearsal/i);
|
|
assertNoTagOperations(calls);
|
|
});
|
|
|
|
it("compares HEAD with the exact fetched rehearsal commit", (t) => {
|
|
const fixture = createReleaseFixture(t);
|
|
|
|
const result = runTagRelease(fixture);
|
|
const calls = readGitCalls(fixture);
|
|
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.ok(calls.some((args) => args.join(" ") === "fetch origin test/npm-staged-publish-rehearsal"));
|
|
assert.ok(calls.some((args) => args.join(" ") === "rev-parse FETCH_HEAD^{commit}"));
|
|
assert.equal(calls.some((args) => args.includes("origin/test/npm-staged-publish-rehearsal")), false);
|
|
});
|
|
|
|
it("fails when the local tag already exists", (t) => {
|
|
const fixture = createReleaseFixture(t);
|
|
fs.writeFileSync(path.join(fixture.stateDir, "local-tag"), "bbbbbbbb");
|
|
|
|
const result = runTagRelease(fixture);
|
|
const calls = readGitCalls(fixture);
|
|
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /local tag .* already exists/i);
|
|
assert.equal(calls.some((args) => args[0] === "ls-remote"), false);
|
|
assert.equal(calls.some((args) => args[0] === "push"), false);
|
|
});
|
|
|
|
it("fails when a lightweight or annotated remote tag already exists", (t) => {
|
|
for (const kind of ["lightweight", "annotated"]) {
|
|
const fixture = createReleaseFixture(t, {
|
|
FAKE_REMOTE_TAG_KIND: kind,
|
|
FAKE_REMOTE_TAG_SHA: "aaaaaaaa",
|
|
});
|
|
|
|
const result = runTagRelease(fixture);
|
|
const calls = readGitCalls(fixture);
|
|
|
|
assert.equal(result.status, 1, `${kind}: ${result.stderr}`);
|
|
assert.match(result.stderr, /remote tag .* already exists/i);
|
|
assert.equal(calls.some((args) => args[0] === "tag"), false);
|
|
assert.equal(calls.some((args) => args[0] === "push"), false);
|
|
}
|
|
});
|
|
|
|
it("check mode completes without creating or pushing a tag", (t) => {
|
|
const fixture = createReleaseFixture(t);
|
|
|
|
const result = runTagRelease(fixture);
|
|
const calls = readGitCalls(fixture);
|
|
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.match(result.stdout, /No tag was created or pushed/);
|
|
assertNoTagWrites(calls);
|
|
});
|
|
|
|
it("rejects a production version before invoking git", (t) => {
|
|
const fixture = createReleaseFixture(t);
|
|
fs.writeFileSync(path.join(fixture.root, "package.json"), '{"version":"1.2.3"}\n');
|
|
fs.writeFileSync(
|
|
path.join(fixture.root, "package-lock.json"),
|
|
'{"version":"1.2.3","packages":{"":{"version":"1.2.3"}}}\n',
|
|
);
|
|
|
|
const result = runTagRelease(fixture);
|
|
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /require an X\.Y\.Z-beta\.N version/);
|
|
assert.deepEqual(readGitCalls(fixture), []);
|
|
});
|
|
|
|
it("rejects a workflow that can publish live", (t) => {
|
|
for (const workflow of [
|
|
"args: release --clean --skip=publish\nrun: npm publish --access public",
|
|
"args: release --clean --skip=publish\nrun: npm stage publish package.tgz --access public --tag beta\nrun: gh release create v1.2.3-beta.0",
|
|
"args: release --clean --skip=publish\npermissions:\n contents: write\nrun: npm stage publish package.tgz --access public --tag beta",
|
|
"args: release --clean --skip=publish\nenv:\n GITHUB_TOKEN: ${{ github.token }}\nrun: npm stage publish package.tgz --access public --tag beta",
|
|
]) {
|
|
const fixture = createReleaseFixture(t, { FAKE_WORKFLOW: workflow });
|
|
const result = runTagRelease(fixture);
|
|
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /must be stage-only/i);
|
|
assertNoTagWrites(readGitCalls(fixture));
|
|
}
|
|
});
|
|
|
|
it("fails closed when npm cannot prove that the version is unused", (t) => {
|
|
const fixture = createReleaseFixture(t, {
|
|
FAKE_NPM_VIEW_OUTPUT: "npm error code ETIMEDOUT",
|
|
});
|
|
|
|
const result = runTagRelease(fixture);
|
|
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /npm version lookup failed/i);
|
|
assertNoTagWrites(readGitCalls(fixture));
|
|
});
|
|
|
|
it("rejects an existing npm version", (t) => {
|
|
const fixture = createReleaseFixture(t, {
|
|
FAKE_NPM_VIEW_STATUS: "0",
|
|
FAKE_NPM_VIEW_OUTPUT: "1.2.3-beta.0",
|
|
});
|
|
|
|
const result = runTagRelease(fixture);
|
|
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /already exists on npm/i);
|
|
assertNoTagWrites(readGitCalls(fixture));
|
|
});
|
|
|
|
it("requires the full tag confirmation in push mode", (t) => {
|
|
const fixture = createReleaseFixture(t);
|
|
|
|
const result = runTagRelease(fixture, { args: ["--push"], input: "no\n" });
|
|
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /confirmation did not exactly match/i);
|
|
assertNoTagWrites(readGitCalls(fixture));
|
|
});
|
|
|
|
it("pushes only the exact confirmed tag ref", (t) => {
|
|
const fixture = createReleaseFixture(t);
|
|
|
|
const result = runTagRelease(fixture, {
|
|
args: ["--push"],
|
|
input: "v1.2.3-beta.0\n",
|
|
});
|
|
const calls = readGitCalls(fixture);
|
|
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.ok(calls.some((args) => args.join(" ") === "tag v1.2.3-beta.0 aaaaaaaa"));
|
|
assert.ok(calls.some((args) =>
|
|
args.join(" ") === "push origin refs/tags/v1.2.3-beta.0:refs/tags/v1.2.3-beta.0"));
|
|
assert.equal(
|
|
calls.some((args) => args[0] === "push" && args.includes("--tags")),
|
|
false,
|
|
);
|
|
});
|
|
|
|
it("reports an invalid package version before invoking git", (t) => {
|
|
const fixture = createReleaseFixture(t);
|
|
fs.writeFileSync(path.join(fixture.root, "package.json"), '{"version":"01.2.3"}\n');
|
|
|
|
const result = runTagRelease(fixture);
|
|
|
|
assert.equal(result.status, 1);
|
|
assert.equal(result.stdout, "");
|
|
assertStructuredError(JSON.parse(result.stderr));
|
|
assert.deepEqual(readGitCalls(fixture), []);
|
|
});
|
|
});
|