Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | import chalk from 'chalk';
import { enforceBranchGuardrail } from '../lib/branch.js';
import { revParse, getLastTagForPackage } from '../lib/git.js';
import { createGithubRelease } from '../lib/github.js';
import { generateChangelogNotes } from '../lib/changelog.js';
import { resolvePackage, discoverPackages } from '../lib/workspace.js';
import { getConfig } from '../lib/config.js';
import { heading, info, success, error, setDryRun } from '../lib/log.js';
export function registerGithubCommand(program) {
program
.command('github')
.description('Create or update GitHub releases for package versions')
.requiredOption('--packages <list>', 'Comma-separated list of name@version entries')
.option('--sha <ref>', 'Target SHA (default: HEAD)', 'HEAD')
.option('--from <ref>', 'Commit range start for release notes (default: previous tag)')
.option('--force', 'Update existing release if present')
.option('--skip-existing', 'Silently skip releases that already exist (idempotent for CI re-runs)')
.option('--draft', 'Create release as draft')
.option('--prerelease', 'Mark the release as a prerelease (default: auto-detected from version)')
.option('--no-prerelease', 'Force prerelease=false even if the version looks like a prerelease')
.option('--allow-branch <pattern>', 'Additive branch pattern override')
.option('--no-branch-check', 'Bypass branch guardrail entirely')
.option('--dry-run', 'Print planned actions without executing')
.action(async (opts) => {
if (opts.dryRun) setDryRun(true);
try {
await runGithub(opts);
} catch (e) {
error(e.message);
process.exit(1);
}
});
}
function parsePackageVersionEntries(packagesArg) {
return packagesArg
.split(',')
.map((s) => s.trim())
.filter(Boolean)
.map((entry) => {
const atIdx = entry.lastIndexOf('@');
if (atIdx <= 0) {
error(`Invalid --packages entry "${entry}". Expected format: @scope/name@version`);
process.exit(1);
}
return {
name: entry.substring(0, atIdx),
version: entry.substring(atIdx + 1),
};
});
}
async function runGithub(opts) {
if (opts.branchCheck !== false) {
enforceBranchGuardrail('github', {
noBranchCheck: false,
allowBranch: opts.allowBranch,
});
}
if (opts.force && opts.skipExisting) {
error('--force and --skip-existing are mutually exclusive.');
process.exit(1);
}
const config = getConfig();
const entries = parsePackageVersionEntries(opts.packages);
heading('GitHub Release Plan');
for (const entry of entries) {
const tagName = config.tagFormat.replace('{name}', entry.name).replace('{version}', entry.version);
const pkg = resolvePackage(entry.name);
const pkgPaths = pkg ? [pkg.relativePath] : [];
const fromRef = opts.from || findPreviousTag(entry.name, tagName);
const notes = generateChangelogNotes(entry.name, entry.version, fromRef, pkgPaths);
// Prerelease: if explicitly set (--prerelease or --no-prerelease), honor it.
// Otherwise auto-detect from the version string (any SemVer prerelease
// identifier like -alpha.N / -beta.N / -rc.N / -next / -canary / -dev).
let prerelease;
if (opts.prerelease === true) prerelease = true;
else if (opts.prerelease === false) prerelease = false;
else prerelease = isPrereleaseVersion(entry.version);
info(` ${chalk.bold(tagName)}`);
info(` Notes from: ${chalk.dim(fromRef || 'beginning')} → ${chalk.dim(opts.sha)}`);
if (opts.draft) info(` ${chalk.yellow('(draft)')}`);
if (prerelease) info(` ${chalk.yellow('(prerelease)')}`);
info('');
const result = createGithubRelease(tagName, {
body: notes,
sha: opts.sha === 'HEAD' ? undefined : opts.sha,
draft: opts.draft || false,
prerelease,
force: opts.force || false,
skipExisting: opts.skipExisting || false,
});
if (result.skipped) continue;
success(`GitHub release: ${tagName}${result.updated ? ' (updated)' : ''}`);
}
}
/**
* Detect SemVer prerelease identifiers in a version string. Returns true for
* `*-alpha.N`, `*-beta.N`, `*-rc.N`, `*-next`, `*-canary`, `*-dev`, `*-pre`,
* and their plain (non-numbered) variants. Returns false for stable
* versions like `1.2.3` or `2.30.7`.
*
* Exported for test coverage.
*/
export function isPrereleaseVersion(version) {
return /-(alpha|beta|rc|next|canary|dev|pre)(\.\w+)?(\+|$)/i.test(version);
}
function findPreviousTag(packageName, currentTag) {
const lastTag = getLastTagForPackage(packageName);
if (lastTag && lastTag !== currentTag) {
return lastTag;
}
return null;
}
|