#!/usr/bin/env node /** * Warns about commits since the last tag that semantic-release cannot parse. * Exit 0 always — semantic-release still decides release/no-op. */ import { execSync } from 'node:child_process'; import { CommitParser } from 'conventional-commits-parser'; const parser = new CommitParser(); const RELEASABLE = new Set(['feat', 'fix', 'perf', 'ci', 'refactor']); function lastTag() { try { return execSync('git describe --tags --abbrev=0', { encoding: 'utf8' }).trim(); } catch { return ''; } } function commitsSince(ref) { const range = ref ? `${ref}..HEAD` : 'HEAD'; const out = execSync(`git log ${range} --format=%H%x09%s`, { encoding: 'utf8' }).trim(); if (!out) return []; return out.split('\n').map((line) => { const [hash, subject] = line.split('\t'); return { hash: hash.trim(), subject: subject.trim() }; }); } const tag = lastTag(); const commits = commitsSince(tag); const unparseable = []; const releasable = []; for (const { hash, subject } of commits) { let parsed; try { parsed = parser.parse(subject); } catch { unparseable.push({ hash: hash.slice(0, 7), subject }); continue; } if (!parsed?.type) { unparseable.push({ hash: hash.slice(0, 7), subject }); continue; } // semantic-release expects lowercase types (feat|fix|…); "Refactor: …" is not releasable. if (parsed.type !== parsed.type.toLowerCase()) { unparseable.push({ hash: hash.slice(0, 7), subject }); continue; } if (RELEASABLE.has(parsed.type)) { releasable.push({ hash: hash.slice(0, 7), subject, type: parsed.type }); } } if (commits.length === 0) { console.log(`No new commits since ${tag || 'initial'}.`); process.exit(0); } if (unparseable.length > 0) { console.warn('::warning:: Commits not parseable by semantic-release (no version bump):'); for (const b of unparseable) { console.warn(` ${b.hash} ${b.subject}`); } console.warn('Fix: single scope without commas, e.g. refactor(web): summary'); } if (releasable.length > 0) { console.log(`Releasable since ${tag || 'initial'}: ${releasable.length} commit(s).`); for (const r of releasable) { console.log(` ${r.hash} ${r.type} ${r.subject}`); } } else { console.warn('::warning:: No releasable commits since last tag — release job will no-op.'); console.warn( 'Need at least one: feat|fix|perf|ci|refactor with Conventional Commits, e.g. feat(fleet): …', ); if (!tag) { console.warn( '::warning:: No git tags yet — first release (v1.0.0) also requires a releasable commit on HEAD history.', ); } } process.exit(0);