Skip to content

Commit cf2c4de

Browse files
authored
Merge pull request #6 from Chimp-Stack/release-chimp/feat/ci-flag-support
chore(release-chimp): add CI mode flag for skipping changelog, git, and package.json
2 parents 45262d3 + 25be234 commit cf2c4de

10 files changed

Lines changed: 171 additions & 47 deletions

File tree

.chimprc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"releaseChimp": {
3+
"bumpType": "patch",
4+
"dryRun": false,
5+
"noPackageJson": false,
6+
"noChangelog": false,
7+
"noGit": false,
8+
"changelog": {
9+
"path": "CHANGELOG.md",
10+
"useAI": false
11+
}
12+
},
13+
"tagFormat": "${name}@${version}"
14+
}

packages/chimp-core/src/cli/changelog.ts

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { simpleGit } from 'simple-git';
2+
import fs from 'node:fs';
23
import { Command } from 'commander';
34
import {
5+
extractTagPrefixFromFormat,
46
generateSemanticChangelog,
57
logError,
68
logSuccess,
79
writeChangelogToFile,
810
} from '../utils';
11+
import { loadChimpConfig } from 'src/config';
912

1013
export function addChangelogCommand(
1114
program: Command,
@@ -18,23 +21,50 @@ export function addChangelogCommand(
1821
.option('--to <tag>', 'End tag or commit (default: HEAD)')
1922
.option('--output <file>', 'Output file to append changelog')
2023
.option('--ai', 'Use AI to generate a summary section')
24+
.option(
25+
'--latest',
26+
'Generate changelog between the last two tags'
27+
)
2128
.action(
2229
async (options: {
2330
from?: string;
2431
to?: string;
2532
output?: string;
2633
ai?: boolean;
34+
latest?: boolean;
2735
}) => {
28-
const { from, to, output, ai } = options;
36+
const config = loadChimpConfig(toolName);
37+
const tagFormat = config.tagFormat || '';
38+
const { from, to, output, ai, latest } = options;
2939

30-
const start = from ?? (await getLatestTag()) ?? '0.0.0';
31-
const end = to ?? 'HEAD';
40+
let start = from;
41+
let end = to ?? 'HEAD';
3242

33-
if (!start) {
34-
logError(
35-
'❌ No tags found to use as a starting point. Use --from manually.'
43+
if (latest) {
44+
const pkgJson = JSON.parse(
45+
fs.readFileSync('package.json', 'utf8')
3646
);
37-
process.exit(1);
47+
const name = pkgJson.name;
48+
const tags = await getSortedTags(tagFormat, name);
49+
if (tags.length === 0) {
50+
logError('❌ No tags found in this repository.');
51+
process.exit(1);
52+
}
53+
54+
end = tags[0];
55+
start = tags[1] ?? '0.0.0';
56+
57+
console.log(
58+
`🪵 Generating changelog from ${start}${end}`
59+
);
60+
}
61+
62+
if (!start) {
63+
start = (await getLatestTag()) ?? '0.0.0';
64+
if (!start) {
65+
logError('❌ No starting tag or commit specified.');
66+
process.exit(1);
67+
}
3868
}
3969

4070
const changelog = await generateSemanticChangelog({
@@ -64,3 +94,28 @@ async function getLatestTag(): Promise<string | null> {
6494
const tags = await git.tags();
6595
return tags.latest ?? null;
6696
}
97+
98+
async function getSortedTags(
99+
tagPrefix?: string,
100+
name?: string
101+
): Promise<string[]> {
102+
const git = simpleGit();
103+
const tags = await git.tags();
104+
105+
let prefix = '';
106+
if (tagPrefix && name) {
107+
prefix = extractTagPrefixFromFormat(tagPrefix, name);
108+
}
109+
110+
const filtered = tagPrefix
111+
? tags.all.filter((tag) => tag.startsWith(prefix))
112+
: tags.all;
113+
114+
return filtered.sort((a, b) => {
115+
// fallback to semantic sort
116+
return b.localeCompare(a, undefined, {
117+
numeric: true,
118+
sensitivity: 'base',
119+
});
120+
});
121+
}

packages/chimp-core/src/config.ts

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,47 @@ import { ChimpConfig } from './types/config';
66
const CONFIG_FILENAME = '.chimprc';
77

88
export function loadChimpConfig(scope?: string): ChimpConfig {
9-
const locations = [
10-
path.join(process.cwd(), CONFIG_FILENAME),
11-
path.join(os.homedir(), CONFIG_FILENAME),
12-
];
13-
14-
for (const loc of locations) {
15-
if (fs.existsSync(loc)) {
16-
const data = JSON.parse(fs.readFileSync(loc, 'utf-8'));
17-
18-
if (scope) {
19-
// Merge top-level keys into the scoped config
20-
const { [scope]: scoped = {}, ...topLevel } = data;
21-
const globalKeys = Object.fromEntries(
22-
Object.entries(topLevel).filter(([k]) =>
23-
['openaiApiKey', 'githubToken'].includes(k)
24-
)
25-
);
26-
return { ...globalKeys, ...scoped };
27-
}
28-
29-
return data;
30-
}
9+
const globalPath = path.join(os.homedir(), CONFIG_FILENAME);
10+
const localPath = path.join(process.cwd(), CONFIG_FILENAME);
11+
12+
const globalConfig = fs.existsSync(globalPath)
13+
? JSON.parse(fs.readFileSync(globalPath, 'utf-8'))
14+
: {};
15+
16+
const localConfig = fs.existsSync(localPath)
17+
? JSON.parse(fs.readFileSync(localPath, 'utf-8'))
18+
: {};
19+
20+
if (scope) {
21+
const { [scope]: globalScoped = {}, ...globalTopLevel } =
22+
globalConfig;
23+
24+
const { [scope]: localScoped = {}, ...localTopLevel } =
25+
localConfig;
26+
27+
const globalGlobals = extractGlobals(globalTopLevel);
28+
const localGlobals = extractGlobals(localTopLevel);
29+
30+
return {
31+
...globalGlobals,
32+
...localGlobals,
33+
...globalScoped,
34+
...localScoped,
35+
};
3136
}
3237

33-
return {};
38+
// Unscoped: merge all
39+
return {
40+
...globalConfig,
41+
...localConfig,
42+
};
43+
}
44+
45+
function extractGlobals(obj: Record<string, any>) {
46+
const allowed = ['openaiApiKey', 'githubToken', 'tagFormat'];
47+
return Object.fromEntries(
48+
Object.entries(obj).filter(([k]) => allowed.includes(k))
49+
);
3450
}
3551

3652
export function writeChimpConfig(

packages/chimp-core/src/prompts/releaseChimp.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,6 @@ export async function askReleaseChimpQuestions(): Promise<ReleaseChimpConfig> {
2222
default: false,
2323
});
2424

25-
const tagFormat = await input({
26-
message:
27-
'Custom git tag format (e.g., v{version}, @scope/pkg@{version})',
28-
default: 'v{version}',
29-
});
30-
3125
const changelogPath = await input({
3226
message: 'Path to changelog file (e.g., CHANGELOG.md)',
3327
default: 'CHANGELOG.md',
@@ -61,7 +55,6 @@ export async function askReleaseChimpQuestions(): Promise<ReleaseChimpConfig> {
6155
return {
6256
bumpType,
6357
dryRun,
64-
tagFormat,
6558
noPackageJson,
6659
noChangelog,
6760
noGit,

packages/chimp-core/src/prompts/shared.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,11 @@ export async function askSharedQuestions(
1515
message: 'GitHub Token (optional)',
1616
});
1717

18+
answers.tagFormat = await input({
19+
message:
20+
'Custom git tag format (e.g., v{version}, @scope/pkg@{version})',
21+
default: '${name}@${version}',
22+
});
23+
1824
return answers;
1925
}

packages/chimp-core/src/types/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export type ChimpConfig = {
22
openaiApiKey?: string;
33
githubToken?: string;
4+
tagFormat?: string;
45
[key: string]: any;
56
};
67

@@ -33,7 +34,6 @@ export type DocChimpConfig = ChimpConfig & {
3334

3435
export type ReleaseChimpConfig = ChimpConfig & {
3536
bumpType?: 'major' | 'minor' | 'patch';
36-
tagFormat?: string;
3737
changelog?: {
3838
path?: string; // Default: 'CHANGELOG.md'
3939
useAI?: boolean;

packages/chimp-core/src/utils/changelog/changelog.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ export async function generateSemanticChangelog({
7979
'chore',
8080
];
8181

82+
console.log({ commits, groupOrder });
83+
8284
for (const type of groupOrder) {
8385
if (semanticGroups[type]?.length) {
8486
output += `### ${getSectionTitle(type)}\n`;

packages/chimp-core/src/utils/git/tagFormat.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,12 @@ export function applyTagFormat(
77
.replace(/\$\{name\}/g, name)
88
.replace(/\$\{version\}/g, version);
99
}
10+
11+
export function extractTagPrefixFromFormat(
12+
format: string,
13+
name: string
14+
): string {
15+
return format
16+
.replace(/\$\{name\}/g, name)
17+
.replace(/\$\{version\}/g, '');
18+
}

packages/release-chimp/src/commands/bump.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,28 +20,39 @@ export async function handleBump(
2020
cliPart: string,
2121
cliOptions: Command & {
2222
ai?: boolean;
23+
ci?: boolean;
2324
dryRun?: boolean;
2425
noPackageJson?: boolean;
2526
noChangelog?: boolean;
2627
noGit?: boolean;
28+
output?: string;
2729
}
2830
) {
2931
const config = loadChimpConfig(
3032
'releaseChimp'
3133
) as ReleaseChimpConfig;
3234

33-
const part = cliPart || config.bumpType || 'patch';
3435
const dryRun = cliOptions.dryRun ?? config.dryRun ?? false;
36+
const part = cliPart || config.bumpType || 'patch';
37+
const inferVersionOnly = cliPart === undefined && !dryRun;
38+
const isCI = cliOptions.ci ?? false;
39+
40+
if (isCI) {
41+
console.log(
42+
'🤖 CI mode enabled: Skipping package.json, changelog, and git.'
43+
);
44+
}
45+
3546
const noPackageJson =
3647
cliOptions.noPackageJson ?? config.noPackageJson ?? false;
3748
const noChangelog =
38-
cliOptions.noChangelog ?? config.noChangelog ?? false;
39-
const noGit = cliOptions.noGit ?? config.noGit ?? false;
49+
isCI || (cliOptions.noChangelog ?? config.noChangelog ?? false);
50+
const noGit = isCI || (cliOptions.noGit ?? config.noGit ?? false);
51+
const useAI = cliOptions.ai ?? config.changelog?.useAI ?? false;
52+
const outputFormat = cliOptions.output ?? 'text';
4053

4154
const validParts = ['major', 'minor', 'patch'] as const;
4255

43-
const useAI = cliOptions.ai ?? config.changelog?.useAI ?? false;
44-
4556
if (!validParts.includes(part as any)) {
4657
console.error(
4758
`❌ Invalid bump type: '${part}'. Must be one of: ${validParts.join(', ')}`
@@ -54,11 +65,17 @@ export async function handleBump(
5465
});
5566

5667
const rawVersion = extractVersionFromTag(current);
57-
const next = bumpVersion(rawVersion, part as any);
68+
const next = inferVersionOnly
69+
? rawVersion
70+
: bumpVersion(rawVersion, part as any);
5871

5972
console.log(`🐵 Current version: ${current}`);
6073
console.log(`🍌 Next version: ${next}`);
6174

75+
if (inferVersionOnly) {
76+
console.log(`🔄 Inferring version from latest tag`);
77+
}
78+
6279
if (dryRun) {
6380
const changelog = noChangelog
6481
? '_Changelog generation skipped (dry run)._'
@@ -87,7 +104,7 @@ export async function handleBump(
87104
return;
88105
}
89106

90-
// Write package.json version bump unless opted out
107+
// Update package.json version
91108
if (!noPackageJson) {
92109
const packageJsonPath = path.resolve(
93110
process.cwd(),
@@ -113,7 +130,7 @@ export async function handleBump(
113130
console.log('📦 Skipping package.json update');
114131
}
115132

116-
// Generate and write changelog unless opted out
133+
// Generate
117134
if (!noChangelog) {
118135
const changelog = await generateSemanticChangelog({
119136
from: isGitRef ? current : undefined,
@@ -140,4 +157,10 @@ export async function handleBump(
140157
} else {
141158
console.log('🚀 Skipping git commit, tag, and push');
142159
}
160+
161+
if (outputFormat === 'json') {
162+
console.log(JSON.stringify({ next }, null, 2));
163+
} else {
164+
console.log(next);
165+
}
143166
}

packages/release-chimp/src/commands/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ export function runCLI() {
2323
addChangelogCommand(program, 'releaseChimp');
2424

2525
program
26-
.command('bump <part>')
26+
.command('bump [part]')
2727
.description('Bump version: patch, minor, or major')
28+
.option('--ci', 'CI mode: skips changelog, git, and package.json')
2829
.option(
2930
'--dry-run',
3031
'Preview changes without writing files or committing'
@@ -33,6 +34,11 @@ export function runCLI() {
3334
.option('--no-package-json', 'Skip updating package.json version')
3435
.option('--no-changelog', 'Skip generating and writing changelog')
3536
.option('--no-git', 'Skip git commit, tag, and push')
37+
.option(
38+
'--output <format>',
39+
'Output format: json or text',
40+
'text'
41+
)
3642
.action(handleBump);
3743

3844
program.parse(process.argv);

0 commit comments

Comments
 (0)