feat(cli): split commit and release into target-based workflows
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
import * as plugins from "./plugins.js";
|
||||
|
||||
export type TChangelogBucket =
|
||||
| "Breaking Changes"
|
||||
| "Features"
|
||||
| "Fixes"
|
||||
| "Documentation"
|
||||
| "Maintenance";
|
||||
|
||||
export interface IChangelogEntry {
|
||||
type: string;
|
||||
scope: string;
|
||||
message: string;
|
||||
details?: string[];
|
||||
}
|
||||
|
||||
export interface IPendingChangelog {
|
||||
block: string;
|
||||
isEmpty: boolean;
|
||||
}
|
||||
|
||||
const bucketForCommitType = (commitType: string): TChangelogBucket => {
|
||||
switch (commitType) {
|
||||
case "BREAKING CHANGE":
|
||||
return "Breaking Changes";
|
||||
case "feat":
|
||||
return "Features";
|
||||
case "fix":
|
||||
return "Fixes";
|
||||
case "docs":
|
||||
return "Documentation";
|
||||
default:
|
||||
return "Maintenance";
|
||||
}
|
||||
};
|
||||
|
||||
const readChangelog = async (filePath: string): Promise<string> => {
|
||||
if (!(await plugins.smartfs.file(filePath).exists())) {
|
||||
return "# Changelog\n\n";
|
||||
}
|
||||
return (await plugins.smartfs.file(filePath).encoding("utf8").read()) as string;
|
||||
};
|
||||
|
||||
const writeChangelog = async (filePath: string, content: string): Promise<void> => {
|
||||
await plugins.smartfs.file(filePath).encoding("utf8").write(content.endsWith("\n") ? content : `${content}\n`);
|
||||
};
|
||||
|
||||
const findPendingSection = (
|
||||
content: string,
|
||||
sectionName: string,
|
||||
): { start: number; bodyStart: number; end: number } | null => {
|
||||
const headingRegex = new RegExp(`^##\\s+${sectionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "m");
|
||||
const match = headingRegex.exec(content);
|
||||
if (!match || match.index === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bodyStart = match.index + match[0].length;
|
||||
const rest = content.slice(bodyStart);
|
||||
const nextHeadingMatch = /^##\s+/m.exec(rest);
|
||||
const end = nextHeadingMatch ? bodyStart + nextHeadingMatch.index : content.length;
|
||||
return { start: match.index, bodyStart, end };
|
||||
};
|
||||
|
||||
export const ensurePendingSection = async (
|
||||
filePath: string,
|
||||
sectionName = "Pending",
|
||||
): Promise<string> => {
|
||||
let content = await readChangelog(filePath);
|
||||
if (findPendingSection(content, sectionName)) {
|
||||
return content;
|
||||
}
|
||||
|
||||
const pendingSection = `## ${sectionName}\n\n`;
|
||||
const titleMatch = /^#\s+.+$/m.exec(content);
|
||||
if (titleMatch && titleMatch.index !== undefined) {
|
||||
const insertAt = titleMatch.index + titleMatch[0].length;
|
||||
content = `${content.slice(0, insertAt)}\n\n${pendingSection}${content.slice(insertAt).replace(/^\n+/, "")}`;
|
||||
} else {
|
||||
content = `# Changelog\n\n${pendingSection}${content}`;
|
||||
}
|
||||
|
||||
await writeChangelog(filePath, content);
|
||||
return content;
|
||||
};
|
||||
|
||||
export const appendPendingChangelogEntry = async (
|
||||
filePath: string,
|
||||
sectionName: string,
|
||||
entry: IChangelogEntry,
|
||||
): Promise<void> => {
|
||||
let content = await ensurePendingSection(filePath, sectionName);
|
||||
const pendingSection = findPendingSection(content, sectionName)!;
|
||||
let pendingBody = content.slice(pendingSection.bodyStart, pendingSection.end);
|
||||
const bucket = bucketForCommitType(entry.type);
|
||||
const bucketHeading = `### ${bucket}`;
|
||||
|
||||
const entryLines = [`- ${entry.message}${entry.scope ? ` (${entry.scope})` : ""}`];
|
||||
for (const detail of entry.details || []) {
|
||||
entryLines.push(` - ${detail}`);
|
||||
}
|
||||
const renderedEntry = entryLines.join("\n");
|
||||
|
||||
const bucketRegex = new RegExp(`^###\\s+${bucket.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "m");
|
||||
const bucketMatch = bucketRegex.exec(pendingBody);
|
||||
if (!bucketMatch || bucketMatch.index === undefined) {
|
||||
pendingBody = `${pendingBody.trimEnd()}\n\n${bucketHeading}\n\n${renderedEntry}\n`;
|
||||
} else {
|
||||
const bucketBodyStart = bucketMatch.index + bucketMatch[0].length;
|
||||
const afterBucket = pendingBody.slice(bucketBodyStart);
|
||||
const nextBucketMatch = /^###\s+/m.exec(afterBucket);
|
||||
const insertAt = nextBucketMatch ? bucketBodyStart + nextBucketMatch.index : pendingBody.length;
|
||||
const beforeInsert = pendingBody.slice(0, insertAt).trimEnd();
|
||||
const afterInsert = pendingBody.slice(insertAt).replace(/^\n+/, "");
|
||||
pendingBody = `${beforeInsert}\n${renderedEntry}\n\n${afterInsert}`;
|
||||
}
|
||||
|
||||
content = `${content.slice(0, pendingSection.bodyStart)}\n${pendingBody.trim()}\n\n${content.slice(pendingSection.end).replace(/^\n+/, "")}`;
|
||||
await writeChangelog(filePath, content);
|
||||
};
|
||||
|
||||
export const readPendingChangelog = async (
|
||||
filePath: string,
|
||||
sectionName = "Pending",
|
||||
): Promise<IPendingChangelog> => {
|
||||
const content = await ensurePendingSection(filePath, sectionName);
|
||||
const pendingSection = findPendingSection(content, sectionName)!;
|
||||
const block = content.slice(pendingSection.bodyStart, pendingSection.end).trim();
|
||||
return {
|
||||
block,
|
||||
isEmpty: block.length === 0,
|
||||
};
|
||||
};
|
||||
|
||||
export const inferVersionTypeFromPending = (pendingBlock: string): "patch" | "minor" | "major" => {
|
||||
if (/^###\s+Breaking Changes\s*$/m.test(pendingBlock)) {
|
||||
return "major";
|
||||
}
|
||||
if (/^###\s+Features\s*$/m.test(pendingBlock)) {
|
||||
return "minor";
|
||||
}
|
||||
return "patch";
|
||||
};
|
||||
|
||||
export const movePendingToVersion = async (
|
||||
filePath: string,
|
||||
sectionName: string,
|
||||
versionHeading: string,
|
||||
version: string,
|
||||
dateString: string,
|
||||
): Promise<void> => {
|
||||
let content = await ensurePendingSection(filePath, sectionName);
|
||||
const pendingSection = findPendingSection(content, sectionName)!;
|
||||
const pendingBlock = content.slice(pendingSection.bodyStart, pendingSection.end).trim();
|
||||
if (!pendingBlock) {
|
||||
throw new Error("No pending changelog entries. Nothing to release.");
|
||||
}
|
||||
|
||||
const renderedHeading = versionHeading
|
||||
.replaceAll("{{version}}", version)
|
||||
.replaceAll("{{date}}", dateString);
|
||||
const nextContent = content.slice(pendingSection.end).replace(/^\n+/, "");
|
||||
content = `${content.slice(0, pendingSection.bodyStart)}\n\n${renderedHeading}\n\n${pendingBlock}\n\n${nextContent}`;
|
||||
await writeChangelog(filePath, content);
|
||||
};
|
||||
Reference in New Issue
Block a user