This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022
|
||||
},
|
||||
env: {
|
||||
node: true,
|
||||
es2022: true
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const {execSync} = require('node:child_process');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '../..');
|
||||
const REPO_URL = 'https://github.com/TryGhost/Ghost';
|
||||
|
||||
// Emoji priority order (lowest index = lowest priority, sorted descending)
|
||||
const EMOJI_ORDER = ['💡', '🐛', '🎨', '💄', '✨', '🔒'];
|
||||
|
||||
// User-facing emojis — only these are included in release notes
|
||||
const USER_FACING_EMOJIS = new Set(EMOJI_ORDER);
|
||||
|
||||
function getCommitLog(fromTag, toTag) {
|
||||
const range = `${fromTag}..${toTag}`;
|
||||
const format = '* %s - %an';
|
||||
const cmd = `git log --no-merges --pretty=tformat:'${format}' ${range}`;
|
||||
|
||||
let log;
|
||||
try {
|
||||
log = execSync(cmd, {cwd: ROOT, encoding: 'utf8'}).trim();
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!log) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return log.split('\n').map(line => line.trim());
|
||||
}
|
||||
|
||||
function extractLeadingEmoji(line) {
|
||||
// Line format: * <message> - <author>
|
||||
const match = line.match(/^\* (.)/u);
|
||||
return match ? match[1] : '';
|
||||
}
|
||||
|
||||
function filterAndSortByEmoji(lines) {
|
||||
const emojiLines = lines.filter((line) => {
|
||||
const emoji = extractLeadingEmoji(line);
|
||||
return USER_FACING_EMOJIS.has(emoji);
|
||||
});
|
||||
|
||||
emojiLines.sort((a, b) => {
|
||||
const emojiA = extractLeadingEmoji(a);
|
||||
const emojiB = extractLeadingEmoji(b);
|
||||
const indexA = EMOJI_ORDER.indexOf(emojiA);
|
||||
const indexB = EMOJI_ORDER.indexOf(emojiB);
|
||||
return indexB - indexA;
|
||||
});
|
||||
|
||||
return emojiLines;
|
||||
}
|
||||
|
||||
function generateReleaseNotes(fromTag, toTag) {
|
||||
const lines = getCommitLog(fromTag, toTag);
|
||||
const filtered = filterAndSortByEmoji(lines);
|
||||
|
||||
let body;
|
||||
if (filtered.length === 0) {
|
||||
body = 'This release contains fixes for minor bugs and issues reported by Ghost users.';
|
||||
} else {
|
||||
// Deduplicate (preserving order)
|
||||
body = [...new Set(filtered)].join('\n');
|
||||
}
|
||||
|
||||
body += `\n\n---\n\nView the changelog for full details: ${REPO_URL}/compare/${fromTag}...${toTag}`;
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
// CLI: node release-notes.js <from-tag> <to-tag>
|
||||
if (require.main === module) {
|
||||
const [fromTag, toTag] = process.argv.slice(2);
|
||||
|
||||
if (!fromTag || !toTag) {
|
||||
console.error('Usage: node release-notes.js <from-tag> <to-tag>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.stdout.write(generateReleaseNotes(fromTag, toTag));
|
||||
}
|
||||
|
||||
module.exports = {generateReleaseNotes};
|
||||
@@ -0,0 +1,31 @@
|
||||
const semver = require('semver');
|
||||
const {execSync} = require('node:child_process');
|
||||
|
||||
/**
|
||||
* Resolve the base git tag for diff/log comparisons during release preparation.
|
||||
*
|
||||
* For stable versions (e.g. "6.18.0"), returns "v6.18.0" — the tag for that version.
|
||||
* For prerelease versions (e.g. "6.19.0-rc.0"), the tag "v6.19.0-rc.0" won't exist,
|
||||
* so we find the most recent stable version tag in HEAD's ancestry using git describe.
|
||||
*
|
||||
* @param {string} version - The current Ghost version from package.json
|
||||
* @param {string} repoDir - Path to the Ghost repo checkout
|
||||
* @returns {{tag: string, isPrerelease: boolean}}
|
||||
*/
|
||||
function resolveBaseTag(version, repoDir) {
|
||||
if (semver.prerelease(version)) {
|
||||
const tag = execSync(
|
||||
`git describe --tags --abbrev=0 --match 'v[0-9]*.[0-9]*.[0-9]*' --exclude 'v*-*' HEAD`,
|
||||
{cwd: repoDir, encoding: 'utf8'}
|
||||
).trim();
|
||||
|
||||
return {tag, isPrerelease: true};
|
||||
}
|
||||
|
||||
return {
|
||||
tag: `v${version}`,
|
||||
isPrerelease: false
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {resolveBaseTag};
|
||||
@@ -0,0 +1,326 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const path = require('node:path');
|
||||
const fs = require('node:fs');
|
||||
const {execSync} = require('node:child_process');
|
||||
const semver = require('semver');
|
||||
const {resolveBaseTag} = require('./lib/resolve-base-tag');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const GHOST_CORE_PKG = path.join(ROOT, 'ghost/core/package.json');
|
||||
const GHOST_ADMIN_PKG = path.join(ROOT, 'ghost/admin/package.json');
|
||||
const CASPER_DIR = path.join(ROOT, 'ghost/core/content/themes/casper');
|
||||
const SOURCE_DIR = path.join(ROOT, 'ghost/core/content/themes/source');
|
||||
|
||||
const MAX_WAIT_MS = 30 * 60 * 1000; // 30 minutes
|
||||
const POLL_INTERVAL_MS = 30 * 1000; // 30 seconds
|
||||
|
||||
// --- Argument parsing ---
|
||||
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
const opts = {
|
||||
bumpType: 'auto',
|
||||
branch: 'main',
|
||||
dryRun: false,
|
||||
skipChecks: false
|
||||
};
|
||||
|
||||
for (const arg of args) {
|
||||
if (arg.startsWith('--bump-type=')) {
|
||||
opts.bumpType = arg.split('=')[1];
|
||||
} else if (arg.startsWith('--branch=')) {
|
||||
opts.branch = arg.split('=')[1];
|
||||
} else if (arg === '--dry-run') {
|
||||
opts.dryRun = true;
|
||||
} else if (arg === '--skip-checks') {
|
||||
opts.skipChecks = true;
|
||||
} else {
|
||||
console.error(`Unknown argument: ${arg}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
function run(cmd, opts = {}) {
|
||||
const result = execSync(cmd, {cwd: ROOT, encoding: 'utf8', ...opts});
|
||||
return result.trim();
|
||||
}
|
||||
|
||||
function readPkgVersion(pkgPath) {
|
||||
return JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version;
|
||||
}
|
||||
|
||||
function writePkgVersion(pkgPath, version) {
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
||||
pkg.version = version;
|
||||
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
||||
}
|
||||
|
||||
function log(msg) {
|
||||
console.log(` ${msg}`);
|
||||
}
|
||||
|
||||
function logStep(msg) {
|
||||
console.log(`\n▸ ${msg}`);
|
||||
}
|
||||
|
||||
// --- Version detection ---
|
||||
|
||||
function detectBumpType(baseTag, bumpType) {
|
||||
// Check for new migration files
|
||||
const migrationsPath = 'ghost/core/core/server/data/migrations/versions/';
|
||||
try {
|
||||
const addedFiles = run(`git diff --diff-filter=A --name-only ${baseTag} HEAD -- ${migrationsPath}`);
|
||||
if (addedFiles?.includes('core/')) {
|
||||
log('New migrations detected');
|
||||
if (bumpType === 'auto') {
|
||||
log('Auto-detecting: bumping to minor');
|
||||
bumpType = 'minor';
|
||||
}
|
||||
} else {
|
||||
log('No new migrations detected');
|
||||
}
|
||||
} catch {
|
||||
log('Warning: could not diff migrations');
|
||||
}
|
||||
|
||||
// Check for feature commits (✨ or 🎉)
|
||||
try {
|
||||
const commits = run(`git log --oneline ${baseTag}..HEAD`);
|
||||
if (commits) {
|
||||
const featureCommits = commits.split('\n').filter(c => c.includes('✨') || c.includes('🎉') || c.includes(':sparkles:'));
|
||||
if (featureCommits.length) {
|
||||
log(`Feature commits detected (${featureCommits.length})`);
|
||||
if (bumpType === 'auto') {
|
||||
log('Auto-detecting: bumping to minor');
|
||||
bumpType = 'minor';
|
||||
}
|
||||
} else {
|
||||
log('No feature commits detected');
|
||||
}
|
||||
} else {
|
||||
log('No commits since base tag');
|
||||
}
|
||||
} catch {
|
||||
log('Warning: could not read commit log');
|
||||
}
|
||||
|
||||
if (bumpType === 'auto') {
|
||||
log('Defaulting to patch');
|
||||
bumpType = 'patch';
|
||||
}
|
||||
|
||||
return bumpType;
|
||||
}
|
||||
|
||||
// --- CI check polling ---
|
||||
|
||||
const REQUIRED_CHECK_NAME = 'All required tests passed or skipped';
|
||||
|
||||
async function waitForChecks(commit) {
|
||||
logStep(`Waiting for CI checks on ${commit.slice(0, 8)}...`);
|
||||
|
||||
const token = process.env.GITHUB_TOKEN || process.env.RELEASE_TOKEN;
|
||||
if (!token) {
|
||||
throw new Error('GITHUB_TOKEN or RELEASE_TOKEN required for check polling');
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const response = await fetch(`https://api.github.com/repos/TryGhost/Ghost/commits/${commit}/check-runs`, {
|
||||
headers: {
|
||||
Authorization: `token ${token}`,
|
||||
Accept: 'application/vnd.github+json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const {check_runs: checkRuns} = await response.json();
|
||||
|
||||
// Find the required check — this is the CI gate that aggregates all mandatory checks
|
||||
const requiredCheck = checkRuns.find(r => r.name === REQUIRED_CHECK_NAME);
|
||||
|
||||
if (requiredCheck) {
|
||||
if (requiredCheck.status === 'completed' && requiredCheck.conclusion === 'success') {
|
||||
log(`Required check "${REQUIRED_CHECK_NAME}" passed`);
|
||||
return;
|
||||
}
|
||||
if (requiredCheck.status === 'completed' && requiredCheck.conclusion !== 'success') {
|
||||
throw new Error(`Required check "${REQUIRED_CHECK_NAME}" failed (${requiredCheck.conclusion})`);
|
||||
}
|
||||
log(`Required check is ${requiredCheck.status}, waiting...`);
|
||||
} else {
|
||||
log('Required check not found yet, waiting...');
|
||||
}
|
||||
|
||||
const elapsedMs = Date.now() - startTime;
|
||||
const elapsed = Math.round(elapsedMs / 1000);
|
||||
|
||||
if (elapsedMs >= MAX_WAIT_MS) {
|
||||
throw new Error(`Timed out waiting for "${REQUIRED_CHECK_NAME}" after ${elapsed}s`);
|
||||
}
|
||||
|
||||
log(`(${elapsed}s elapsed), polling in 30s...`);
|
||||
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS));
|
||||
}
|
||||
}
|
||||
|
||||
// --- Theme submodule updates ---
|
||||
|
||||
function updateThemeSubmodule(themeDir, themeName) {
|
||||
if (!fs.existsSync(themeDir)) {
|
||||
log(`${themeName} not present, skipping`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentPkg = JSON.parse(fs.readFileSync(path.join(themeDir, 'package.json'), 'utf8'));
|
||||
const currentVersion = currentPkg.version;
|
||||
|
||||
// Checkout latest stable tag on main branch
|
||||
try {
|
||||
execSync(
|
||||
`git checkout $(git describe --abbrev=0 --tags $(git rev-list --tags --max-count=1 --branches=main))`,
|
||||
{cwd: themeDir, encoding: 'utf8', stdio: 'pipe'}
|
||||
);
|
||||
} catch (err) {
|
||||
log(`Warning: failed to update ${themeName}: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const updatedPkg = JSON.parse(fs.readFileSync(path.join(themeDir, 'package.json'), 'utf8'));
|
||||
const newVersion = updatedPkg.version;
|
||||
|
||||
if (semver.gt(newVersion, currentVersion)) {
|
||||
log(`${themeName} updated: v${currentVersion} → v${newVersion}`);
|
||||
run(`git add -f ${path.relative(ROOT, themeDir)}`);
|
||||
run(`git commit -m "🎨 Updated ${themeName} to v${newVersion}"`);
|
||||
return true;
|
||||
}
|
||||
|
||||
log(`${themeName} already at latest (v${currentVersion})`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Main ---
|
||||
|
||||
async function main() {
|
||||
const opts = parseArgs();
|
||||
|
||||
console.log('Ghost Release Script');
|
||||
console.log('====================');
|
||||
log(`Branch: ${opts.branch}`);
|
||||
log(`Bump type: ${opts.bumpType}`);
|
||||
log(`Dry run: ${opts.dryRun}`);
|
||||
|
||||
// 1. Read current version
|
||||
logStep('Reading current version');
|
||||
const currentVersion = readPkgVersion(GHOST_CORE_PKG);
|
||||
log(`Current version: ${currentVersion}`);
|
||||
|
||||
// 2. Resolve base tag
|
||||
logStep('Resolving base tag');
|
||||
const {tag: baseTag, isPrerelease} = resolveBaseTag(currentVersion, ROOT);
|
||||
if (isPrerelease) {
|
||||
log(`Prerelease detected (${currentVersion}), resolved base tag: ${baseTag}`);
|
||||
} else {
|
||||
log(`Base tag: ${baseTag}`);
|
||||
}
|
||||
|
||||
// 3. Detect bump type
|
||||
logStep('Detecting bump type');
|
||||
const resolvedBumpType = detectBumpType(baseTag, opts.bumpType);
|
||||
const newVersion = semver.inc(currentVersion, resolvedBumpType);
|
||||
if (!newVersion) {
|
||||
console.error(`Failed to calculate new version from ${currentVersion} with bump type ${resolvedBumpType}`);
|
||||
process.exit(1);
|
||||
}
|
||||
log(`Bump type: ${resolvedBumpType}`);
|
||||
log(`New version: ${newVersion}`);
|
||||
|
||||
// 4. Check tag doesn't exist
|
||||
logStep('Checking remote tags');
|
||||
try {
|
||||
const tagCheck = run(`git ls-remote --tags origin refs/tags/v${newVersion}`);
|
||||
if (tagCheck) {
|
||||
console.error(`Tag v${newVersion} already exists on remote. Cannot release this version.`);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch {
|
||||
// ls-remote returns non-zero if no match — that's what we want
|
||||
}
|
||||
log(`Tag v${newVersion} does not exist on remote`);
|
||||
|
||||
// 5. Wait for CI checks
|
||||
if (!opts.skipChecks) {
|
||||
const headSha = run('git rev-parse HEAD');
|
||||
await waitForChecks(headSha);
|
||||
} else {
|
||||
log('Skipping CI checks');
|
||||
}
|
||||
|
||||
// 6. Update theme submodules (main branch only)
|
||||
if (opts.branch === 'main') {
|
||||
logStep('Updating theme submodules');
|
||||
run('git submodule update --init');
|
||||
updateThemeSubmodule(CASPER_DIR, 'Casper');
|
||||
updateThemeSubmodule(SOURCE_DIR, 'Source');
|
||||
} else {
|
||||
logStep('Skipping theme updates (not main branch)');
|
||||
}
|
||||
|
||||
// 7. Bump versions
|
||||
logStep(`Bumping version to ${newVersion}`);
|
||||
writePkgVersion(GHOST_CORE_PKG, newVersion);
|
||||
writePkgVersion(GHOST_ADMIN_PKG, newVersion);
|
||||
|
||||
// 8. Commit and tag
|
||||
run(`git add ${path.relative(ROOT, GHOST_CORE_PKG)} ${path.relative(ROOT, GHOST_ADMIN_PKG)}`);
|
||||
run(`git commit -m "v${newVersion}"`);
|
||||
run(`git tag v${newVersion}`);
|
||||
log(`Created tag v${newVersion}`);
|
||||
|
||||
// 9. Push
|
||||
if (opts.dryRun) {
|
||||
logStep('DRY RUN — skipping push');
|
||||
log(`Would push branch ${opts.branch} and tag v${newVersion}`);
|
||||
} else {
|
||||
logStep('Pushing');
|
||||
run('git push origin HEAD');
|
||||
run(`git push origin v${newVersion}`);
|
||||
log('Pushed branch and tag');
|
||||
}
|
||||
|
||||
// 10. Advance to next RC
|
||||
logStep('Advancing to next RC');
|
||||
const nextMinor = semver.inc(newVersion, 'minor');
|
||||
const nextRc = `${nextMinor}-rc.0`;
|
||||
log(`Next RC: ${nextRc}`);
|
||||
writePkgVersion(GHOST_CORE_PKG, nextRc);
|
||||
writePkgVersion(GHOST_ADMIN_PKG, nextRc);
|
||||
run(`git add ${path.relative(ROOT, GHOST_CORE_PKG)} ${path.relative(ROOT, GHOST_ADMIN_PKG)}`);
|
||||
run(`git commit -m "Bumped version to ${nextRc}"`);
|
||||
|
||||
if (opts.dryRun) {
|
||||
log('DRY RUN — skipping RC push');
|
||||
} else {
|
||||
run('git push origin HEAD');
|
||||
log('Pushed RC version');
|
||||
}
|
||||
|
||||
console.log(`\n✓ Release ${newVersion} complete`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(`\n✗ Release failed: ${err.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,179 @@
|
||||
const {describe, it, before, after} = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const fs = require('node:fs');
|
||||
const {execSync} = require('node:child_process');
|
||||
const semver = require('semver');
|
||||
const {resolveBaseTag} = require('../lib/resolve-base-tag');
|
||||
|
||||
/**
|
||||
* Create a temporary git repo with semver tags for testing.
|
||||
* Returns the repo path. Caller is responsible for cleanup.
|
||||
*/
|
||||
function createTestRepo() {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ghost-release-test-'));
|
||||
|
||||
execSync([
|
||||
`cd ${tmpDir}`,
|
||||
'git init',
|
||||
'git config user.email "test@test.com"',
|
||||
'git config user.name "Test"',
|
||||
|
||||
// Initial commit + stable tag v6.17.0
|
||||
'echo "initial" > file.txt',
|
||||
'git add .',
|
||||
'git commit -m "initial"',
|
||||
'git tag v6.17.0',
|
||||
|
||||
// Second commit + stable tag v6.17.1
|
||||
'echo "patch" >> file.txt',
|
||||
'git add .',
|
||||
'git commit -m "patch release"',
|
||||
'git tag v6.17.1',
|
||||
|
||||
// Third commit (no tag — simulates post-release development)
|
||||
'echo "new work" >> file.txt',
|
||||
'git add .',
|
||||
'git commit -m "post-release commit"'
|
||||
].join(' && '));
|
||||
|
||||
return tmpDir;
|
||||
}
|
||||
|
||||
describe('resolveBaseTag', () => {
|
||||
let testRepo;
|
||||
|
||||
before(() => {
|
||||
testRepo = createTestRepo();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
fs.rmSync(testRepo, {recursive: true, force: true});
|
||||
});
|
||||
|
||||
describe('semver.prerelease detection', () => {
|
||||
it('returns null for stable versions', () => {
|
||||
assert.strictEqual(semver.prerelease('6.17.1'), null);
|
||||
assert.strictEqual(semver.prerelease('6.18.0'), null);
|
||||
assert.strictEqual(semver.prerelease('5.0.0'), null);
|
||||
});
|
||||
|
||||
it('returns prerelease components for rc versions', () => {
|
||||
const result = semver.prerelease('6.19.0-rc.0');
|
||||
assert.deepStrictEqual(result, ['rc', 0]);
|
||||
});
|
||||
|
||||
it('returns prerelease components for alpha versions', () => {
|
||||
const result = semver.prerelease('7.0.0-alpha.1');
|
||||
assert.deepStrictEqual(result, ['alpha', 1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stable version path', () => {
|
||||
it('constructs tag directly from version', () => {
|
||||
// Stable versions don't hit git — the tag is just v{version}
|
||||
const {tag, isPrerelease} = resolveBaseTag('6.17.1', '/nonexistent');
|
||||
assert.strictEqual(tag, 'v6.17.1');
|
||||
assert.strictEqual(isPrerelease, false);
|
||||
});
|
||||
|
||||
it('works for any stable version format', () => {
|
||||
const {tag} = resolveBaseTag('5.0.0', '/nonexistent');
|
||||
assert.strictEqual(tag, 'v5.0.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('prerelease version path', () => {
|
||||
it('resolves to the latest stable tag', () => {
|
||||
const {tag, isPrerelease} = resolveBaseTag('6.19.0-rc.0', testRepo);
|
||||
assert.strictEqual(isPrerelease, true);
|
||||
assert.strictEqual(tag, 'v6.17.1');
|
||||
});
|
||||
|
||||
it('resolved tag is a stable version (no prerelease suffix)', () => {
|
||||
const {tag} = resolveBaseTag('6.19.0-rc.0', testRepo);
|
||||
assert.match(tag, /^v\d+\.\d+\.\d+$/);
|
||||
assert.strictEqual(semver.prerelease(tag.slice(1)), null);
|
||||
});
|
||||
|
||||
it('resolved tag is a valid git ref', () => {
|
||||
const {tag} = resolveBaseTag('6.19.0-rc.0', testRepo);
|
||||
const sha = execSync(`cd ${testRepo} && git rev-parse ${tag}`, {encoding: 'utf8'});
|
||||
assert.ok(sha.trim().length > 0, 'Tag should resolve to a commit SHA');
|
||||
});
|
||||
|
||||
it('git diff succeeds with the resolved tag', () => {
|
||||
const {tag} = resolveBaseTag('6.19.0-rc.0', testRepo);
|
||||
const result = execSync(
|
||||
`cd ${testRepo} && git diff --diff-filter=A --name-only ${tag} HEAD`,
|
||||
{encoding: 'utf8'}
|
||||
);
|
||||
assert.strictEqual(typeof result, 'string');
|
||||
});
|
||||
|
||||
it('git log succeeds with the resolved tag', () => {
|
||||
const {tag} = resolveBaseTag('6.19.0-rc.0', testRepo);
|
||||
const result = execSync(
|
||||
`cd ${testRepo} && git log --oneline ${tag}..HEAD`,
|
||||
{encoding: 'utf8'}
|
||||
);
|
||||
assert.strictEqual(typeof result, 'string');
|
||||
// Should have at least 1 commit (the post-release commit)
|
||||
assert.ok(result.trim().length > 0, 'Should find commits after the tag');
|
||||
});
|
||||
});
|
||||
|
||||
describe('prerelease excludes prerelease tags', () => {
|
||||
let repoWithPrereleaseTag;
|
||||
|
||||
before(() => {
|
||||
// Create a repo that has both stable and prerelease tags
|
||||
repoWithPrereleaseTag = fs.mkdtempSync(path.join(os.tmpdir(), 'ghost-release-test-pre-'));
|
||||
execSync([
|
||||
`cd ${repoWithPrereleaseTag}`,
|
||||
'git init',
|
||||
'git config user.email "test@test.com"',
|
||||
'git config user.name "Test"',
|
||||
|
||||
'echo "initial" > file.txt',
|
||||
'git add .',
|
||||
'git commit -m "initial"',
|
||||
'git tag v6.17.0',
|
||||
|
||||
'echo "alpha" >> file.txt',
|
||||
'git add .',
|
||||
'git commit -m "alpha work"',
|
||||
'git tag v7.0.0-alpha.0',
|
||||
|
||||
'echo "more" >> file.txt',
|
||||
'git add .',
|
||||
'git commit -m "more work"'
|
||||
].join(' && '));
|
||||
});
|
||||
|
||||
after(() => {
|
||||
fs.rmSync(repoWithPrereleaseTag, {recursive: true, force: true});
|
||||
});
|
||||
|
||||
it('skips prerelease tags and finds the stable one', () => {
|
||||
const {tag} = resolveBaseTag('7.0.0-rc.0', repoWithPrereleaseTag);
|
||||
// Should find v6.17.0 (stable), NOT v7.0.0-alpha.0 (prerelease)
|
||||
assert.strictEqual(tag, 'v6.17.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('semver.inc compatibility', () => {
|
||||
it('patch of an rc produces the stable release', () => {
|
||||
assert.strictEqual(semver.inc('6.19.0-rc.0', 'patch'), '6.19.0');
|
||||
});
|
||||
|
||||
it('minor of an rc produces the stable release (not next minor)', () => {
|
||||
assert.strictEqual(semver.inc('6.19.0-rc.0', 'minor'), '6.19.0');
|
||||
});
|
||||
|
||||
it('prerelease rc of an rc increments the rc number', () => {
|
||||
assert.strictEqual(semver.inc('6.19.0-rc.0', 'prerelease', 'rc'), '6.19.0-rc.1');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user