jacobinwwey_obsidian-NotEMD/scripts/release/publish-github-release.js
2026-05-26 05:49:25 -05:00

173 lines
4.7 KiB
JavaScript

const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');
const OBSIDIAN_RELEASE_TAG_PATTERN = /^\d+\.\d+\.\d+$/;
const REQUIRED_RELEASE_ASSETS = ['main.js', 'manifest.json', 'styles.css', 'README.md'];
function ensureFileExists(filePath) {
if (!fs.existsSync(filePath)) {
throw new Error(`Missing required release file: ${filePath}`);
}
}
function validateReleaseTag(tag) {
if (!OBSIDIAN_RELEASE_TAG_PATTERN.test(tag)) {
throw new Error(
`Invalid release tag "${tag}". Obsidian releases must use numeric x.x.x tags without a v prefix.`
);
}
}
function resolveReleaseInputs(repoRoot, tag) {
if (!tag) {
throw new Error('Usage: node scripts/release/publish-github-release.js <tag> [--dry-run]');
}
validateReleaseTag(tag);
const assets = REQUIRED_RELEASE_ASSETS.map((assetName) => path.join(repoRoot, assetName));
assets.forEach(ensureFileExists);
const englishNotesFile = path.join(repoRoot, 'docs', 'releases', `${tag}.md`);
const chineseNotesFile = path.join(repoRoot, 'docs', 'releases', `${tag}.zh-CN.md`);
ensureFileExists(englishNotesFile);
ensureFileExists(chineseNotesFile);
return {
tag,
title: `Notemd ${tag}`,
englishNotesFile,
chineseNotesFile,
assets
};
}
function composeReleaseNotesFile({ tag, englishNotesFile, chineseNotesFile }) {
const englishNotes = fs.readFileSync(englishNotesFile, 'utf8').trim();
const chineseNotes = fs.readFileSync(chineseNotesFile, 'utf8').trim();
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `notemd-release-notes-${tag}-`));
const notesFile = path.join(tempDir, `${tag}.md`);
const combinedNotes = `${englishNotes}\n\n---\n\n${chineseNotes}\n`;
fs.writeFileSync(notesFile, combinedNotes, 'utf8');
return notesFile;
}
function cleanupReleaseNotesFile(notesFile) {
if (!notesFile) {
return;
}
fs.rmSync(path.dirname(notesFile), { recursive: true, force: true });
}
function buildGhReleaseCommands({ tag, title, notesFile, assets, releaseExists }) {
if (!notesFile) {
throw new Error('Missing composed release notes file for gh release publish/repair');
}
if (releaseExists) {
return [
['release', 'edit', tag, '--title', title, '--notes-file', notesFile],
['release', 'upload', tag, ...assets, '--clobber']
];
}
return [[
'release',
'create',
tag,
...assets,
'--title',
title,
'--notes-file',
notesFile,
'--verify-tag'
]];
}
function hasExistingRelease(repoRoot, tag) {
const result = spawnSync('gh', ['release', 'view', tag], {
cwd: repoRoot,
stdio: 'ignore'
});
if (result.error) {
throw result.error;
}
return result.status === 0;
}
function runGhCommand(repoRoot, commandArgs) {
const result = spawnSync('gh', commandArgs, {
cwd: repoRoot,
stdio: 'inherit'
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
throw new Error(`gh ${commandArgs.join(' ')} failed with exit code ${result.status}`);
}
}
function runGhCommands(repoRoot, commandArgsList) {
commandArgsList.forEach((commandArgs) => runGhCommand(repoRoot, commandArgs));
}
function main(argv = process.argv.slice(2)) {
const dryRun = argv.includes('--dry-run');
const tag = argv.find((arg) => !arg.startsWith('--'));
const repoRoot = path.join(__dirname, '..', '..');
const inputs = resolveReleaseInputs(repoRoot, tag);
let notesFile;
try {
const releaseExists = hasExistingRelease(repoRoot, tag);
notesFile = composeReleaseNotesFile(inputs);
const commandArgsList = buildGhReleaseCommands({
...inputs,
notesFile,
releaseExists
});
if (dryRun) {
console.log(commandArgsList.map((commandArgs) => `gh ${commandArgs.join(' ')}`).join('\n'));
return 0;
}
runGhCommands(repoRoot, commandArgsList);
return 0;
} finally {
cleanupReleaseNotesFile(notesFile);
}
}
if (require.main === module) {
try {
process.exitCode = main();
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
}
module.exports = {
OBSIDIAN_RELEASE_TAG_PATTERN,
REQUIRED_RELEASE_ASSETS,
buildGhReleaseCommand: buildGhReleaseCommands,
buildGhReleaseCommands,
cleanupReleaseNotesFile,
composeReleaseNotesFile,
hasExistingRelease,
main,
resolveReleaseInputs,
runGhCommands,
validateReleaseTag
};