feat(cli): split commit and release into target-based workflows
This commit is contained in:
+249
-451
@@ -3,15 +3,11 @@
|
||||
import * as plugins from "./mod.plugins.js";
|
||||
import * as paths from "../paths.js";
|
||||
import { logger } from "../gitzone.logging.js";
|
||||
import * as helpers from "./mod.helpers.js";
|
||||
import * as ui from "./mod.ui.js";
|
||||
import { ReleaseConfig } from "../mod_config/classes.releaseconfig.js";
|
||||
import type { ICliMode } from "../helpers.climode.js";
|
||||
import {
|
||||
getCliMode,
|
||||
printJson,
|
||||
runWithSuppressedOutput,
|
||||
} from "../helpers.climode.js";
|
||||
import { getCliMode, printJson, runWithSuppressedOutput } from "../helpers.climode.js";
|
||||
import { appendPendingChangelogEntry } from "../helpers.changelog.js";
|
||||
import { resolveCommitWorkflow, type IResolvedCommitWorkflow } from "../helpers.workflow.js";
|
||||
|
||||
export const run = async (argvArg: any) => {
|
||||
const mode = await getCliMode(argvArg);
|
||||
@@ -36,431 +32,247 @@ export const run = async (argvArg: any) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Read commit config from .smartconfig.json
|
||||
const smartconfigInstance = new plugins.smartconfig.Smartconfig();
|
||||
const gitzoneConfig = smartconfigInstance.dataFor<{
|
||||
commit?: {
|
||||
alwaysTest?: boolean;
|
||||
alwaysBuild?: boolean;
|
||||
};
|
||||
}>("@git.zone/cli", {});
|
||||
const commitConfig = gitzoneConfig.commit || {};
|
||||
|
||||
// Check flags and merge with config options
|
||||
const wantsRelease = !!(argvArg.r || argvArg.release);
|
||||
const wantsTest = !!(argvArg.t || argvArg.test || commitConfig.alwaysTest);
|
||||
const wantsBuild = !!(argvArg.b || argvArg.build || commitConfig.alwaysBuild);
|
||||
let releaseConfig: ReleaseConfig | null = null;
|
||||
|
||||
if (wantsRelease) {
|
||||
releaseConfig = await ReleaseConfig.fromCwd();
|
||||
if (!releaseConfig.hasRegistries()) {
|
||||
logger.log("error", "No release registries configured.");
|
||||
console.log("");
|
||||
console.log(
|
||||
" Run `gitzone config add <registry-url>` to add registries.",
|
||||
);
|
||||
console.log("");
|
||||
process.exit(1);
|
||||
}
|
||||
const workflow = await resolveCommitWorkflow(argvArg);
|
||||
if (workflow.releaseFlagRequested) {
|
||||
logger.log(
|
||||
"warn",
|
||||
"`gitzone commit -r` is deprecated and no longer releases. Use `gitzone release` after committing.",
|
||||
);
|
||||
}
|
||||
|
||||
// Print execution plan at the start
|
||||
ui.printExecutionPlan({
|
||||
autoAccept: !!(argvArg.y || argvArg.yes),
|
||||
push: !!(argvArg.p || argvArg.push),
|
||||
test: wantsTest,
|
||||
build: wantsBuild,
|
||||
release: wantsRelease,
|
||||
format: !!argvArg.format,
|
||||
registries: releaseConfig?.getRegistries(),
|
||||
});
|
||||
|
||||
if (argvArg.format) {
|
||||
const formatMod = await import("../mod_format/index.js");
|
||||
await formatMod.run();
|
||||
printCommitExecutionPlan(workflow);
|
||||
if (workflow.confirmation === "plan") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Run tests early to fail fast before analysis
|
||||
if (wantsTest) {
|
||||
ui.printHeader("🧪 Running tests...");
|
||||
const smartshellForTest = new plugins.smartshell.Smartshell({
|
||||
executor: "bash",
|
||||
sourceFilePaths: [],
|
||||
});
|
||||
const testResult = await smartshellForTest.exec("pnpm test");
|
||||
if (testResult.exitCode !== 0) {
|
||||
logger.log("error", "Tests failed. Aborting commit.");
|
||||
process.exit(1);
|
||||
}
|
||||
logger.log("success", "All tests passed.");
|
||||
}
|
||||
|
||||
ui.printHeader("🔍 Analyzing repository changes...");
|
||||
|
||||
const aidoc = new plugins.tsdoc.AiDoc();
|
||||
await aidoc.start();
|
||||
|
||||
const nextCommitObject = await aidoc.buildNextCommitObject(paths.cwd);
|
||||
|
||||
await aidoc.stop();
|
||||
|
||||
ui.printRecommendation({
|
||||
recommendedNextVersion: nextCommitObject.recommendedNextVersion,
|
||||
recommendedNextVersionLevel: nextCommitObject.recommendedNextVersionLevel,
|
||||
recommendedNextVersionScope: nextCommitObject.recommendedNextVersionScope,
|
||||
recommendedNextVersionMessage:
|
||||
nextCommitObject.recommendedNextVersionMessage,
|
||||
});
|
||||
|
||||
let answerBucket: plugins.smartinteract.AnswerBucket;
|
||||
|
||||
// Check if -y/--yes flag is set AND version is not a breaking change
|
||||
// Breaking changes (major version bumps) always require manual confirmation
|
||||
const isBreakingChange =
|
||||
nextCommitObject.recommendedNextVersionLevel === "BREAKING CHANGE";
|
||||
const canAutoAccept = (argvArg.y || argvArg.yes) && !isBreakingChange;
|
||||
|
||||
if (canAutoAccept) {
|
||||
// Auto-mode: create AnswerBucket programmatically
|
||||
logger.log("info", "✓ Auto-accepting AI recommendations (--yes flag)");
|
||||
|
||||
answerBucket = new plugins.smartinteract.AnswerBucket();
|
||||
answerBucket.addAnswer({
|
||||
name: "commitType",
|
||||
value: nextCommitObject.recommendedNextVersionLevel,
|
||||
});
|
||||
answerBucket.addAnswer({
|
||||
name: "commitScope",
|
||||
value: nextCommitObject.recommendedNextVersionScope,
|
||||
});
|
||||
answerBucket.addAnswer({
|
||||
name: "commitDescription",
|
||||
value: nextCommitObject.recommendedNextVersionMessage,
|
||||
});
|
||||
answerBucket.addAnswer({
|
||||
name: "pushToOrigin",
|
||||
value: !!(argvArg.p || argvArg.push), // Only push if -p flag also provided
|
||||
});
|
||||
answerBucket.addAnswer({
|
||||
name: "createRelease",
|
||||
value: wantsRelease,
|
||||
});
|
||||
} else {
|
||||
// Warn if --yes was provided but we're requiring confirmation due to breaking change
|
||||
if (isBreakingChange && (argvArg.y || argvArg.yes)) {
|
||||
logger.log(
|
||||
"warn",
|
||||
"⚠️ BREAKING CHANGE detected - manual confirmation required",
|
||||
);
|
||||
}
|
||||
// Interactive mode: prompt user for input
|
||||
const commitInteract = new plugins.smartinteract.SmartInteract();
|
||||
commitInteract.addQuestions([
|
||||
{
|
||||
type: "list",
|
||||
name: `commitType`,
|
||||
message: `Choose TYPE of the commit:`,
|
||||
choices: [`fix`, `feat`, `BREAKING CHANGE`],
|
||||
default: nextCommitObject.recommendedNextVersionLevel,
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: `commitScope`,
|
||||
message: `What is the SCOPE of the commit:`,
|
||||
default: nextCommitObject.recommendedNextVersionScope,
|
||||
},
|
||||
{
|
||||
type: `input`,
|
||||
name: `commitDescription`,
|
||||
message: `What is the DESCRIPTION of the commit?`,
|
||||
default: nextCommitObject.recommendedNextVersionMessage,
|
||||
},
|
||||
{
|
||||
type: "confirm",
|
||||
name: `pushToOrigin`,
|
||||
message: `Do you want to push this version now?`,
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
type: "confirm",
|
||||
name: `createRelease`,
|
||||
message: `Do you want to publish to npm registries?`,
|
||||
default: wantsRelease,
|
||||
},
|
||||
]);
|
||||
answerBucket = await commitInteract.runQueue();
|
||||
}
|
||||
const commitString = createCommitStringFromAnswerBucket(answerBucket);
|
||||
const commitType = answerBucket.getAnswerFor("commitType");
|
||||
let commitVersionType: helpers.VersionType;
|
||||
switch (commitType) {
|
||||
case "fix":
|
||||
commitVersionType = "patch";
|
||||
break;
|
||||
case "feat":
|
||||
commitVersionType = "minor";
|
||||
break;
|
||||
case "BREAKING CHANGE":
|
||||
commitVersionType = "major";
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported commit type: ${commitType}`);
|
||||
}
|
||||
|
||||
ui.printHeader("✨ Creating Semantic Commit");
|
||||
ui.printCommitMessage(commitString);
|
||||
const smartshellInstance = new plugins.smartshell.Smartshell({
|
||||
executor: "bash",
|
||||
sourceFilePaths: [],
|
||||
});
|
||||
|
||||
// Load release config if user wants to release (interactively selected)
|
||||
if (answerBucket.getAnswerFor("createRelease") && !releaseConfig) {
|
||||
releaseConfig = await ReleaseConfig.fromCwd();
|
||||
if (!releaseConfig.hasRegistries()) {
|
||||
logger.log("error", "No release registries configured.");
|
||||
console.log("");
|
||||
console.log(
|
||||
" Run `gitzone config add <registry-url>` to add registries.",
|
||||
);
|
||||
console.log("");
|
||||
process.exit(1);
|
||||
let nextCommitObject: any;
|
||||
let answerBucket: plugins.smartinteract.AnswerBucket | undefined;
|
||||
|
||||
for (const step of workflow.steps) {
|
||||
switch (step) {
|
||||
case "format":
|
||||
await runFormatStep();
|
||||
break;
|
||||
case "test":
|
||||
await runCommandStep(smartshellInstance, "Running tests", workflow.testCommand);
|
||||
break;
|
||||
case "build":
|
||||
await runCommandStep(smartshellInstance, "Running build", workflow.buildCommand);
|
||||
break;
|
||||
case "analyze":
|
||||
nextCommitObject = await runAnalyzeStep();
|
||||
answerBucket = await buildAnswerBucket(nextCommitObject, workflow, mode, argvArg);
|
||||
break;
|
||||
case "changelog":
|
||||
assertAnalysisComplete(answerBucket, nextCommitObject);
|
||||
await runChangelogStep(workflow, answerBucket!, nextCommitObject);
|
||||
break;
|
||||
case "commit":
|
||||
assertAnalysisComplete(answerBucket, nextCommitObject);
|
||||
await runCommitStep(smartshellInstance, answerBucket!);
|
||||
break;
|
||||
case "push":
|
||||
await runPushStep(smartshellInstance, workflow);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine total steps based on options
|
||||
// Note: test runs early (like format) so not counted in numbered steps
|
||||
const willPush =
|
||||
answerBucket.getAnswerFor("pushToOrigin") && !(process.env.CI === "true");
|
||||
const willRelease =
|
||||
answerBucket.getAnswerFor("createRelease") &&
|
||||
releaseConfig?.hasRegistries();
|
||||
let totalSteps = 5; // Base steps: commitinfo, changelog, staging, commit, version
|
||||
if (wantsBuild) totalSteps += 2; // build step + verification step
|
||||
if (willPush) totalSteps++;
|
||||
if (willRelease) totalSteps++;
|
||||
let currentStep = 0;
|
||||
|
||||
// Step 1: Baking commitinfo
|
||||
currentStep++;
|
||||
ui.printStep(
|
||||
currentStep,
|
||||
totalSteps,
|
||||
"🔧 Baking commit info into code",
|
||||
"in-progress",
|
||||
);
|
||||
const commitInfo = new plugins.commitinfo.CommitInfo(
|
||||
paths.cwd,
|
||||
commitVersionType,
|
||||
);
|
||||
await commitInfo.writeIntoPotentialDirs();
|
||||
ui.printStep(
|
||||
currentStep,
|
||||
totalSteps,
|
||||
"🔧 Baking commit info into code",
|
||||
"done",
|
||||
);
|
||||
|
||||
// Step 2: Writing changelog
|
||||
currentStep++;
|
||||
ui.printStep(
|
||||
currentStep,
|
||||
totalSteps,
|
||||
"📄 Generating changelog.md",
|
||||
"in-progress",
|
||||
);
|
||||
let changelog = nextCommitObject.changelog || "# Changelog\n";
|
||||
changelog = changelog.replaceAll(
|
||||
"{{nextVersion}}",
|
||||
(await commitInfo.getNextPlannedVersion()).versionString,
|
||||
);
|
||||
changelog = changelog.replaceAll(
|
||||
"{{nextVersionScope}}",
|
||||
`${await answerBucket.getAnswerFor("commitType")}(${await answerBucket.getAnswerFor("commitScope")})`,
|
||||
);
|
||||
changelog = changelog.replaceAll(
|
||||
"{{nextVersionMessage}}",
|
||||
nextCommitObject.recommendedNextVersionMessage,
|
||||
);
|
||||
if (nextCommitObject.recommendedNextVersionDetails?.length > 0) {
|
||||
changelog = changelog.replaceAll(
|
||||
"{{nextVersionDetails}}",
|
||||
"- " + nextCommitObject.recommendedNextVersionDetails.join("\n- "),
|
||||
);
|
||||
} else {
|
||||
changelog = changelog.replaceAll("\n{{nextVersionDetails}}", "");
|
||||
}
|
||||
|
||||
await plugins.smartfs
|
||||
.file(plugins.path.join(paths.cwd, `changelog.md`))
|
||||
.encoding("utf8")
|
||||
.write(changelog);
|
||||
ui.printStep(currentStep, totalSteps, "📄 Generating changelog.md", "done");
|
||||
|
||||
// Step 3: Staging files
|
||||
currentStep++;
|
||||
ui.printStep(currentStep, totalSteps, "📦 Staging files", "in-progress");
|
||||
await smartshellInstance.exec(`git add -A`);
|
||||
ui.printStep(currentStep, totalSteps, "📦 Staging files", "done");
|
||||
|
||||
// Step 4: Creating commit
|
||||
currentStep++;
|
||||
ui.printStep(
|
||||
currentStep,
|
||||
totalSteps,
|
||||
"💾 Creating git commit",
|
||||
"in-progress",
|
||||
);
|
||||
await smartshellInstance.exec(`git commit -m "${commitString}"`);
|
||||
ui.printStep(currentStep, totalSteps, "💾 Creating git commit", "done");
|
||||
|
||||
// Step 5: Bumping version
|
||||
currentStep++;
|
||||
const projectType = await helpers.detectProjectType();
|
||||
const newVersion = await helpers.bumpProjectVersion(
|
||||
projectType,
|
||||
commitVersionType,
|
||||
currentStep,
|
||||
totalSteps,
|
||||
);
|
||||
|
||||
// Step 6: Run build (optional)
|
||||
if (wantsBuild) {
|
||||
currentStep++;
|
||||
ui.printStep(currentStep, totalSteps, "🔨 Running build", "in-progress");
|
||||
const buildResult = await smartshellInstance.exec("pnpm build");
|
||||
if (buildResult.exitCode !== 0) {
|
||||
ui.printStep(currentStep, totalSteps, "🔨 Running build", "error");
|
||||
logger.log("error", "Build failed. Aborting release.");
|
||||
process.exit(1);
|
||||
}
|
||||
ui.printStep(currentStep, totalSteps, "🔨 Running build", "done");
|
||||
|
||||
// Step 7: Verify no uncommitted changes
|
||||
currentStep++;
|
||||
ui.printStep(
|
||||
currentStep,
|
||||
totalSteps,
|
||||
"🔍 Verifying clean working tree",
|
||||
"in-progress",
|
||||
);
|
||||
const statusResult = await smartshellInstance.exec(
|
||||
"git status --porcelain",
|
||||
);
|
||||
if (statusResult.stdout.trim() !== "") {
|
||||
ui.printStep(
|
||||
currentStep,
|
||||
totalSteps,
|
||||
"🔍 Verifying clean working tree",
|
||||
"error",
|
||||
);
|
||||
logger.log(
|
||||
"error",
|
||||
"Build produced uncommitted changes. This usually means build output is not gitignored.",
|
||||
);
|
||||
logger.log("error", "Uncommitted files:");
|
||||
console.log(statusResult.stdout);
|
||||
logger.log(
|
||||
"error",
|
||||
"Aborting release. Please ensure build artifacts are in .gitignore",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
ui.printStep(
|
||||
currentStep,
|
||||
totalSteps,
|
||||
"🔍 Verifying clean working tree",
|
||||
"done",
|
||||
);
|
||||
}
|
||||
|
||||
// Step: Push to remote (optional)
|
||||
const currentBranch = await helpers.detectCurrentBranch();
|
||||
if (willPush) {
|
||||
currentStep++;
|
||||
ui.printStep(
|
||||
currentStep,
|
||||
totalSteps,
|
||||
`🚀 Pushing to origin/${currentBranch}`,
|
||||
"in-progress",
|
||||
);
|
||||
await smartshellInstance.exec(
|
||||
`git push origin ${currentBranch} --follow-tags`,
|
||||
);
|
||||
ui.printStep(
|
||||
currentStep,
|
||||
totalSteps,
|
||||
`🚀 Pushing to origin/${currentBranch}`,
|
||||
"done",
|
||||
);
|
||||
}
|
||||
|
||||
// Step 7: Publish to npm registries (optional)
|
||||
let releasedRegistries: string[] = [];
|
||||
if (willRelease && releaseConfig) {
|
||||
currentStep++;
|
||||
const registries = releaseConfig.getRegistries();
|
||||
ui.printStep(
|
||||
currentStep,
|
||||
totalSteps,
|
||||
`📦 Publishing to ${registries.length} registr${registries.length === 1 ? "y" : "ies"}`,
|
||||
"in-progress",
|
||||
);
|
||||
|
||||
const accessLevel = releaseConfig.getAccessLevel();
|
||||
for (const registry of registries) {
|
||||
try {
|
||||
await smartshellInstance.exec(
|
||||
`npm publish --registry=${registry} --access=${accessLevel}`,
|
||||
);
|
||||
releasedRegistries.push(registry);
|
||||
} catch (error) {
|
||||
logger.log("error", `Failed to publish to ${registry}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (releasedRegistries.length === registries.length) {
|
||||
ui.printStep(
|
||||
currentStep,
|
||||
totalSteps,
|
||||
`📦 Publishing to ${registries.length} registr${registries.length === 1 ? "y" : "ies"}`,
|
||||
"done",
|
||||
);
|
||||
} else {
|
||||
ui.printStep(
|
||||
currentStep,
|
||||
totalSteps,
|
||||
`📦 Publishing to ${registries.length} registr${registries.length === 1 ? "y" : "ies"}`,
|
||||
"error",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(""); // Add spacing before summary
|
||||
|
||||
// Get commit SHA for summary
|
||||
const commitShaResult = await smartshellInstance.exec(
|
||||
"git rev-parse --short HEAD",
|
||||
);
|
||||
const commitSha = commitShaResult.stdout.trim();
|
||||
|
||||
// Print final summary
|
||||
const commitShaResult = await smartshellInstance.exec("git rev-parse --short HEAD");
|
||||
const currentBranch = await detectCurrentBranch(smartshellInstance);
|
||||
ui.printSummary({
|
||||
projectType,
|
||||
projectType: "source",
|
||||
branch: currentBranch,
|
||||
commitType: answerBucket.getAnswerFor("commitType"),
|
||||
commitScope: answerBucket.getAnswerFor("commitScope"),
|
||||
commitMessage: answerBucket.getAnswerFor("commitDescription"),
|
||||
newVersion: newVersion,
|
||||
commitSha: commitSha,
|
||||
pushed: willPush,
|
||||
released: releasedRegistries.length > 0,
|
||||
releasedRegistries:
|
||||
releasedRegistries.length > 0 ? releasedRegistries : undefined,
|
||||
commitType: answerBucket!.getAnswerFor("commitType"),
|
||||
commitScope: answerBucket!.getAnswerFor("commitScope"),
|
||||
commitMessage: answerBucket!.getAnswerFor("commitDescription"),
|
||||
commitSha: commitShaResult.stdout.trim(),
|
||||
pushed: workflow.steps.includes("push"),
|
||||
});
|
||||
};
|
||||
|
||||
async function runFormatStep(): Promise<void> {
|
||||
ui.printHeader("Formatting project files");
|
||||
const formatMod = await import("../mod_format/index.js");
|
||||
await formatMod.run({ write: true, yes: true, interactive: false });
|
||||
}
|
||||
|
||||
async function runCommandStep(
|
||||
smartshellInstance: plugins.smartshell.Smartshell,
|
||||
label: string,
|
||||
command: string,
|
||||
): Promise<void> {
|
||||
ui.printHeader(label);
|
||||
const result = await smartshellInstance.exec(command);
|
||||
if (result.exitCode !== 0) {
|
||||
logger.log("error", `${label} failed. Aborting commit.`);
|
||||
process.exit(1);
|
||||
}
|
||||
logger.log("success", `${label} passed.`);
|
||||
}
|
||||
|
||||
async function runAnalyzeStep(): Promise<any> {
|
||||
ui.printHeader("Analyzing repository changes");
|
||||
const aidoc = new plugins.tsdoc.AiDoc();
|
||||
await aidoc.start();
|
||||
try {
|
||||
const nextCommitObject = await aidoc.buildNextCommitObject(paths.cwd);
|
||||
ui.printRecommendation({
|
||||
recommendedNextVersion: nextCommitObject.recommendedNextVersion,
|
||||
recommendedNextVersionLevel: nextCommitObject.recommendedNextVersionLevel,
|
||||
recommendedNextVersionScope: nextCommitObject.recommendedNextVersionScope,
|
||||
recommendedNextVersionMessage: nextCommitObject.recommendedNextVersionMessage,
|
||||
});
|
||||
return nextCommitObject;
|
||||
} finally {
|
||||
await aidoc.stop();
|
||||
}
|
||||
}
|
||||
|
||||
async function buildAnswerBucket(
|
||||
nextCommitObject: any,
|
||||
workflow: IResolvedCommitWorkflow,
|
||||
mode: ICliMode,
|
||||
argvArg: any,
|
||||
): Promise<plugins.smartinteract.AnswerBucket> {
|
||||
const isBreakingChange = nextCommitObject.recommendedNextVersionLevel === "BREAKING CHANGE";
|
||||
const canAutoAccept = workflow.confirmation === "auto" && !isBreakingChange;
|
||||
|
||||
if (canAutoAccept) {
|
||||
logger.log("info", "Auto-accepting AI recommendations");
|
||||
return createAnswerBucket({
|
||||
commitType: nextCommitObject.recommendedNextVersionLevel,
|
||||
commitScope: nextCommitObject.recommendedNextVersionScope,
|
||||
commitDescription: nextCommitObject.recommendedNextVersionMessage,
|
||||
});
|
||||
}
|
||||
|
||||
if (isBreakingChange && (workflow.confirmation === "auto" || argvArg.y || argvArg.yes)) {
|
||||
logger.log("warn", "BREAKING CHANGE detected - manual confirmation required");
|
||||
}
|
||||
|
||||
if (!mode.interactive) {
|
||||
throw new Error("Commit confirmation requires an interactive terminal. Use `-y` or set commit.confirmation to `auto`.");
|
||||
}
|
||||
|
||||
const commitInteract = new plugins.smartinteract.SmartInteract();
|
||||
commitInteract.addQuestions([
|
||||
{
|
||||
type: "list",
|
||||
name: "commitType",
|
||||
message: "Choose TYPE of the commit:",
|
||||
choices: ["fix", "feat", "BREAKING CHANGE"],
|
||||
default: nextCommitObject.recommendedNextVersionLevel,
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: "commitScope",
|
||||
message: "What is the SCOPE of the commit:",
|
||||
default: nextCommitObject.recommendedNextVersionScope,
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: "commitDescription",
|
||||
message: "What is the DESCRIPTION of the commit?",
|
||||
default: nextCommitObject.recommendedNextVersionMessage,
|
||||
},
|
||||
]);
|
||||
return await commitInteract.runQueue();
|
||||
}
|
||||
|
||||
function createAnswerBucket(answers: {
|
||||
commitType: string;
|
||||
commitScope: string;
|
||||
commitDescription: string;
|
||||
}): plugins.smartinteract.AnswerBucket {
|
||||
const answerBucket = new plugins.smartinteract.AnswerBucket();
|
||||
for (const [name, value] of Object.entries(answers)) {
|
||||
answerBucket.addAnswer({ name, value });
|
||||
}
|
||||
return answerBucket;
|
||||
}
|
||||
|
||||
async function runChangelogStep(
|
||||
workflow: IResolvedCommitWorkflow,
|
||||
answerBucket: plugins.smartinteract.AnswerBucket,
|
||||
nextCommitObject: any,
|
||||
): Promise<void> {
|
||||
await appendPendingChangelogEntry(
|
||||
plugins.path.join(paths.cwd, workflow.changelogFile),
|
||||
workflow.changelogSection,
|
||||
{
|
||||
type: answerBucket.getAnswerFor("commitType"),
|
||||
scope: answerBucket.getAnswerFor("commitScope"),
|
||||
message: answerBucket.getAnswerFor("commitDescription"),
|
||||
details: nextCommitObject.recommendedNextVersionDetails || [],
|
||||
},
|
||||
);
|
||||
logger.log("success", `Updated ${workflow.changelogFile} pending section.`);
|
||||
}
|
||||
|
||||
async function runCommitStep(
|
||||
smartshellInstance: plugins.smartshell.Smartshell,
|
||||
answerBucket: plugins.smartinteract.AnswerBucket,
|
||||
): Promise<void> {
|
||||
ui.printHeader("Creating Semantic Commit");
|
||||
const commitString = createCommitStringFromAnswerBucket(answerBucket);
|
||||
ui.printCommitMessage(commitString);
|
||||
await smartshellInstance.exec("git add -A");
|
||||
const result = await smartshellInstance.exec(`git commit -m ${shellQuote(commitString)}`);
|
||||
if (result.exitCode !== 0) {
|
||||
logger.log("error", "git commit failed.");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function runPushStep(
|
||||
smartshellInstance: plugins.smartshell.Smartshell,
|
||||
workflow: IResolvedCommitWorkflow,
|
||||
): Promise<void> {
|
||||
const currentBranch = await detectCurrentBranch(smartshellInstance);
|
||||
const followTags = workflow.pushFollowTags ? " --follow-tags" : "";
|
||||
const result = await smartshellInstance.exec(
|
||||
`git push ${workflow.pushRemote} ${currentBranch}${followTags}`,
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
logger.log("error", "git push failed.");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function detectCurrentBranch(
|
||||
smartshellInstance: plugins.smartshell.Smartshell,
|
||||
): Promise<string> {
|
||||
const branchResult = await smartshellInstance.exec("git branch --show-current");
|
||||
return branchResult.stdout.trim() || "master";
|
||||
}
|
||||
|
||||
function assertAnalysisComplete(
|
||||
answerBucket: plugins.smartinteract.AnswerBucket | undefined,
|
||||
nextCommitObject: any,
|
||||
): void {
|
||||
if (!answerBucket || !nextCommitObject) {
|
||||
throw new Error("Commit workflow requires analyze before changelog and commit steps.");
|
||||
}
|
||||
}
|
||||
|
||||
function shellQuote(value: string): string {
|
||||
return `'${value.replaceAll("'", "'\\''")}'`;
|
||||
}
|
||||
|
||||
function printCommitExecutionPlan(workflow: IResolvedCommitWorkflow): void {
|
||||
console.log("");
|
||||
console.log("gitzone commit - resolved workflow");
|
||||
console.log(`confirmation: ${workflow.confirmation}`);
|
||||
console.log(`steps: ${workflow.steps.join(" -> ")}`);
|
||||
console.log(`changelog: ${workflow.changelogFile}#${workflow.changelogSection}`);
|
||||
console.log("");
|
||||
}
|
||||
|
||||
async function handleRecommend(mode: ICliMode): Promise<void> {
|
||||
const recommendationBuilder = async () => {
|
||||
const aidoc = new plugins.tsdoc.AiDoc();
|
||||
@@ -507,40 +319,27 @@ export function showHelp(mode?: ICliMode): void {
|
||||
printJson({
|
||||
command: "commit",
|
||||
usage: "gitzone commit [recommend] [options]",
|
||||
description:
|
||||
"Creates semantic commits or emits a read-only recommendation.",
|
||||
description: "Analyzes changes and creates one semantic source commit.",
|
||||
commands: [
|
||||
{
|
||||
name: "recommend",
|
||||
description:
|
||||
"Generate a commit recommendation without mutating the repository",
|
||||
description: "Generate a commit recommendation without mutating the repository",
|
||||
},
|
||||
],
|
||||
flags: [
|
||||
{ flag: "-y, --yes", description: "Auto-accept AI recommendations" },
|
||||
{ flag: "-p, --push", description: "Push to origin after commit" },
|
||||
{ flag: "-t, --test", description: "Run tests before the commit flow" },
|
||||
{
|
||||
flag: "-b, --build",
|
||||
description: "Run the build after the commit flow",
|
||||
},
|
||||
{
|
||||
flag: "-r, --release",
|
||||
description: "Publish to configured registries after push",
|
||||
},
|
||||
{
|
||||
flag: "--format",
|
||||
description: "Run gitzone format before committing",
|
||||
},
|
||||
{
|
||||
flag: "--json",
|
||||
description: "Emit JSON for `commit recommend` only",
|
||||
},
|
||||
{ flag: "-y, --yes", description: "Auto-accept safe AI recommendations" },
|
||||
{ flag: "-p, --push", description: "Push to origin after committing" },
|
||||
{ flag: "-t, --test", description: "Run tests as part of the commit workflow" },
|
||||
{ flag: "-b, --build", description: "Run build as part of the commit workflow" },
|
||||
{ flag: "-f, --format", description: "Run gitzone format before committing" },
|
||||
{ flag: "--plan", description: "Show resolved workflow without mutating files" },
|
||||
{ flag: "--json", description: "Emit JSON for `commit recommend` only" },
|
||||
],
|
||||
examples: [
|
||||
"gitzone commit recommend --json",
|
||||
"gitzone commit -y",
|
||||
"gitzone commit -ypbr",
|
||||
"gitzone commit -ytbp",
|
||||
"gitzone release",
|
||||
],
|
||||
});
|
||||
return;
|
||||
@@ -549,25 +348,24 @@ export function showHelp(mode?: ICliMode): void {
|
||||
console.log("");
|
||||
console.log("Usage: gitzone commit [recommend] [options]");
|
||||
console.log("");
|
||||
console.log("Creates one semantic source commit. It does not version, tag, or publish.");
|
||||
console.log("");
|
||||
console.log("Commands:");
|
||||
console.log(
|
||||
" recommend Generate a commit recommendation without mutating the repository",
|
||||
);
|
||||
console.log(" recommend Generate a commit recommendation without mutating the repository");
|
||||
console.log("");
|
||||
console.log("Flags:");
|
||||
console.log(" -y, --yes Auto-accept AI recommendations");
|
||||
console.log(" -p, --push Push to origin after commit");
|
||||
console.log(" -t, --test Run tests before the commit flow");
|
||||
console.log(" -b, --build Run the build after the commit flow");
|
||||
console.log(
|
||||
" -r, --release Publish to configured registries after push",
|
||||
);
|
||||
console.log(" --format Run gitzone format before committing");
|
||||
console.log(" -y, --yes Auto-accept safe AI recommendations");
|
||||
console.log(" -p, --push Push after commit");
|
||||
console.log(" -t, --test Run tests in the configured order");
|
||||
console.log(" -b, --build Run build in the configured order");
|
||||
console.log(" -f, --format Run gitzone format before committing");
|
||||
console.log(" --plan Show resolved workflow without mutating files");
|
||||
console.log(" --json Emit JSON for `commit recommend` only");
|
||||
console.log("");
|
||||
console.log("Examples:");
|
||||
console.log(" gitzone commit recommend --json");
|
||||
console.log(" gitzone commit -y");
|
||||
console.log(" gitzone commit -ypbr");
|
||||
console.log(" gitzone commit -ytbp");
|
||||
console.log(" gitzone release");
|
||||
console.log("");
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ export async function detectProjectType(): Promise<ProjectType> {
|
||||
* @param versionType Type of version bump
|
||||
* @returns New version string
|
||||
*/
|
||||
function calculateNewVersion(currentVersion: string, versionType: VersionType): string {
|
||||
export function calculateNewVersion(currentVersion: string, versionType: VersionType): string {
|
||||
const versionMatch = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)/);
|
||||
|
||||
if (!versionMatch) {
|
||||
@@ -95,7 +95,7 @@ function calculateNewVersion(currentVersion: string, versionType: VersionType):
|
||||
* @param projectType The project type to determine which file to read
|
||||
* @returns The current version string
|
||||
*/
|
||||
async function readCurrentVersion(projectType: ProjectType): Promise<string> {
|
||||
export async function readCurrentVersion(projectType: ProjectType): Promise<string> {
|
||||
if (projectType === 'npm' || projectType === 'both') {
|
||||
const packageJsonPath = plugins.path.join(paths.cwd, 'package.json');
|
||||
const content = (await plugins.smartfs
|
||||
@@ -128,7 +128,7 @@ async function readCurrentVersion(projectType: ProjectType): Promise<string> {
|
||||
* @param filePath Path to the JSON file
|
||||
* @param newVersion The new version to write
|
||||
*/
|
||||
async function updateVersionFile(filePath: string, newVersion: string): Promise<void> {
|
||||
export async function updateVersionFile(filePath: string, newVersion: string): Promise<void> {
|
||||
const content = (await plugins.smartfs
|
||||
.file(filePath)
|
||||
.encoding('utf8')
|
||||
@@ -141,6 +141,30 @@ async function updateVersionFile(filePath: string, newVersion: string): Promise<
|
||||
.write(JSON.stringify(config, null, 2) + '\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates project version files without creating commits or tags.
|
||||
*/
|
||||
export async function updateProjectVersionFiles(
|
||||
projectType: ProjectType,
|
||||
newVersion: string,
|
||||
): Promise<string[]> {
|
||||
const filesToUpdate: string[] = [];
|
||||
const packageJsonPath = plugins.path.join(paths.cwd, 'package.json');
|
||||
const denoJsonPath = plugins.path.join(paths.cwd, 'deno.json');
|
||||
|
||||
if (projectType === 'npm' || projectType === 'both') {
|
||||
await updateVersionFile(packageJsonPath, newVersion);
|
||||
filesToUpdate.push('package.json');
|
||||
}
|
||||
|
||||
if (projectType === 'deno' || projectType === 'both') {
|
||||
await updateVersionFile(denoJsonPath, newVersion);
|
||||
filesToUpdate.push('deno.json');
|
||||
}
|
||||
|
||||
return filesToUpdate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bumps the project version based on project type
|
||||
* Handles npm-only, deno-only, and dual projects with unified logic
|
||||
@@ -182,19 +206,7 @@ export async function bumpProjectVersion(
|
||||
logger.log('info', `Bumping version: ${currentVersion} → ${newVersion}`);
|
||||
|
||||
// 3. Determine which files to update
|
||||
const filesToUpdate: string[] = [];
|
||||
const packageJsonPath = plugins.path.join(paths.cwd, 'package.json');
|
||||
const denoJsonPath = plugins.path.join(paths.cwd, 'deno.json');
|
||||
|
||||
if (projectType === 'npm' || projectType === 'both') {
|
||||
await updateVersionFile(packageJsonPath, newVersion);
|
||||
filesToUpdate.push('package.json');
|
||||
}
|
||||
|
||||
if (projectType === 'deno' || projectType === 'both') {
|
||||
await updateVersionFile(denoJsonPath, newVersion);
|
||||
filesToUpdate.push('deno.json');
|
||||
}
|
||||
const filesToUpdate = await updateProjectVersionFiles(projectType, newVersion);
|
||||
|
||||
// 4. Stage all updated files
|
||||
await smartshellInstance.exec(`git add ${filesToUpdate.join(' ')}`);
|
||||
|
||||
@@ -10,7 +10,7 @@ interface ICommitSummary {
|
||||
commitType: string;
|
||||
commitScope: string;
|
||||
commitMessage: string;
|
||||
newVersion: string;
|
||||
newVersion?: string;
|
||||
commitSha?: string;
|
||||
pushed: boolean;
|
||||
repoUrl?: string;
|
||||
@@ -197,9 +197,14 @@ export function printSummary(summary: ICommitSummary): void {
|
||||
`Branch: 🌿 ${summary.branch}`,
|
||||
`Commit Type: ${getCommitTypeEmoji(summary.commitType)}`,
|
||||
`Scope: 📍 ${summary.commitScope}`,
|
||||
`New Version: 🏷️ v${summary.newVersion}`,
|
||||
];
|
||||
|
||||
if (summary.newVersion) {
|
||||
lines.push(`New Version: 🏷️ v${summary.newVersion}`);
|
||||
} else {
|
||||
lines.push(`Version: ⊘ Not bumped`);
|
||||
}
|
||||
|
||||
if (summary.commitSha) {
|
||||
lines.push(`Commit SHA: 📌 ${summary.commitSha}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user