@juancr11/sibu 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/admin/changelog.js +155 -0
- package/bin/admin/release.js +217 -0
- package/bin/entrypoints/cli/create-program.js +1 -1
- package/bin/entrypoints/cli/execute-command.js +6 -6
- package/bin/modules/interactive-guidance/index.js +1 -0
- package/bin/{shared → modules/interactive-guidance}/prompts.js +17 -1
- package/bin/modules/maintainer-release-support/generate-changelog/changelog-format.js +213 -0
- package/bin/modules/maintainer-release-support/generate-changelog/changelog-writer.js +91 -0
- package/bin/modules/maintainer-release-support/generate-changelog/git-history.js +93 -0
- package/bin/modules/maintainer-release-support/generate-changelog/handler.js +167 -0
- package/bin/modules/maintainer-release-support/generate-changelog/semver.js +40 -0
- package/bin/modules/maintainer-release-support/index.js +2 -0
- package/bin/modules/maintainer-release-support/release-workflow/git-release.js +114 -0
- package/bin/modules/maintainer-release-support/release-workflow/handler.js +369 -0
- package/bin/modules/maintainer-release-support/release-workflow/package-json.js +181 -0
- package/bin/modules/maintainer-release-support/release-workflow/release-plan.js +87 -0
- package/bin/{features/init-project → modules/project-adoption}/handler.js +7 -5
- package/bin/modules/project-adoption/index.js +1 -0
- package/bin/modules/skill-selection-management/index.js +3 -0
- package/bin/{features → modules/skill-selection-management}/list-skills/handler.js +14 -4
- package/bin/{features → modules/skill-selection-management}/stop-managing-file/handler.js +15 -8
- package/bin/modules/skill-selection-management/use-skill/command.js +1 -0
- package/bin/{features → modules/skill-selection-management}/use-skill/handler.js +42 -8
- package/bin/{features/sync-project → modules/sync-review}/apply-action.js +5 -3
- package/bin/modules/sync-review/command.js +1 -0
- package/bin/{features/sync-project → modules/sync-review}/handler.js +4 -4
- package/bin/modules/sync-review/index.js +5 -0
- package/bin/{shared → modules/sync-review}/sync-preview.js +11 -7
- package/bin/modules/template-catalog-rendering/index.js +1 -0
- package/bin/modules/template-catalog-rendering/templates.js +60 -0
- package/bin/modules/version-advisory/index.js +1 -0
- package/bin/{shared → modules/version-advisory}/npm-version.js +29 -2
- package/bin/modules/workflow-health-diagnosis/command.js +1 -0
- package/bin/{features/doctor-project → modules/workflow-health-diagnosis}/handler.js +13 -7
- package/bin/modules/workflow-health-diagnosis/index.js +1 -0
- package/bin/modules/workflow-mutation-readiness/index.js +1 -0
- package/bin/{shared → modules/workflow-mutation-readiness}/workflow-mutation-readiness.js +3 -3
- package/bin/modules/workflow-state-registry/index.js +1 -0
- package/bin/{shared → modules/workflow-state-registry}/state.js +3 -1
- package/bin/modules/workflow-target-planning/catalog.js +252 -0
- package/bin/modules/workflow-target-planning/index.js +2 -0
- package/bin/modules/workflow-target-planning/workflow-targets.js +130 -0
- package/bin/shared/catalog.js +0 -253
- package/bin/shared/paths.js +1 -12
- package/bin/shared/workflow-targets.js +3 -2
- package/package.json +4 -2
- package/templates/AGENTS.md +3 -3
- package/templates/manifest.json +38 -27
- package/templates/skills/ai-implementation-plan-executor/SKILL.md +74 -70
- package/templates/skills/ai-implementation-planner/SKILL.md +24 -17
- package/templates/skills/architecture/command-pattern/SKILL.md +32 -20
- package/templates/skills/architecture/ddd-hexagonal/SKILL.md +14 -9
- package/templates/skills/deep-module-map-writer/SKILL.md +241 -0
- package/templates/skills/feature-brief-writer/SKILL.md +24 -24
- package/templates/skills/postgresql-expert/SKILL.md +72 -0
- package/templates/skills/product-vision-writer/SKILL.md +2 -2
- package/templates/skills/scrum-master-planner/SKILL.md +2 -2
- package/templates/skills/technical-design-writer/SKILL.md +15 -15
- package/templates/skills/ux-expert/SKILL.md +1 -1
- package/bin/features/sync-project/preview.js +0 -1
- package/templates/skills/product-context-map-writer/SKILL.md +0 -211
- /package/bin/{features/doctor-project/command.js → modules/cli-command-surface/index.js} +0 -0
- /package/bin/{features/init-project → modules/maintainer-release-support/generate-changelog}/command.js +0 -0
- /package/bin/{features/list-skills → modules/maintainer-release-support/release-workflow}/command.js +0 -0
- /package/bin/{features/stop-managing-file → modules/project-adoption}/command.js +0 -0
- /package/bin/{features/sync-project → modules/skill-selection-management/list-skills}/command.js +0 -0
- /package/bin/{features/use-skill → modules/skill-selection-management/stop-managing-file}/command.js +0 -0
- /package/bin/{features/sync-project → modules/sync-review}/action-prompt.js +0 -0
- /package/bin/{features/sync-project → modules/sync-review}/log-preview.js +0 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { renderChangelogSection } from './changelog-format.js';
|
|
2
|
+
export const STANDARD_CHANGELOG_HEADER = `# Changelog
|
|
3
|
+
|
|
4
|
+
All notable changes to this project will be documented in this file.
|
|
5
|
+
|
|
6
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
7
|
+
`;
|
|
8
|
+
export function planChangelogUpdate(existingContent, proposal) {
|
|
9
|
+
const nextSection = renderChangelogSection(proposal).trimEnd();
|
|
10
|
+
if (!existingContent || existingContent.trim().length === 0) {
|
|
11
|
+
return {
|
|
12
|
+
status: 'ok',
|
|
13
|
+
content: `${STANDARD_CHANGELOG_HEADER}\n${nextSection}\n`,
|
|
14
|
+
replacingExistingSection: false,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const parsedChangelog = parseChangelogSections(existingContent);
|
|
18
|
+
if (parsedChangelog.status === 'blocked') {
|
|
19
|
+
return parsedChangelog;
|
|
20
|
+
}
|
|
21
|
+
const targetSectionIndex = findTargetSectionIndex(parsedChangelog.sections, proposal);
|
|
22
|
+
if (targetSectionIndex >= 0) {
|
|
23
|
+
const sections = [...parsedChangelog.sections];
|
|
24
|
+
sections[targetSectionIndex] = nextSection;
|
|
25
|
+
return {
|
|
26
|
+
status: 'ok',
|
|
27
|
+
content: joinChangelog(parsedChangelog.header, sections),
|
|
28
|
+
replacingExistingSection: true,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
status: 'ok',
|
|
33
|
+
content: joinChangelog(parsedChangelog.header, [nextSection, ...parsedChangelog.sections]),
|
|
34
|
+
replacingExistingSection: false,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function parseChangelogSections(content) {
|
|
38
|
+
if (!content.trimStart().startsWith('# Changelog')) {
|
|
39
|
+
return unsafeParsedChangelog('Existing CHANGELOG.md does not start with a # Changelog heading.');
|
|
40
|
+
}
|
|
41
|
+
const sectionMatches = [...content.matchAll(/^## .+$/gm)];
|
|
42
|
+
if (sectionMatches.length === 0) {
|
|
43
|
+
return unsafeParsedChangelog('Existing CHANGELOG.md has no parseable release sections.');
|
|
44
|
+
}
|
|
45
|
+
const firstSectionIndex = sectionMatches[0]?.index;
|
|
46
|
+
if (firstSectionIndex === undefined) {
|
|
47
|
+
return unsafeParsedChangelog('Existing CHANGELOG.md has no parseable release sections.');
|
|
48
|
+
}
|
|
49
|
+
const header = content.slice(0, firstSectionIndex).trimEnd();
|
|
50
|
+
const sections = sectionMatches.map((match, index) => {
|
|
51
|
+
const start = match.index ?? 0;
|
|
52
|
+
const end = sectionMatches[index + 1]?.index ?? content.length;
|
|
53
|
+
return content.slice(start, end).trim();
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
status: 'ok',
|
|
57
|
+
header,
|
|
58
|
+
sections,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function findTargetSectionIndex(sections, proposal) {
|
|
62
|
+
const expectedHeading = proposal.targetSection.type === 'unreleased' ? '## Unreleased' : `## ${proposal.targetSection.version} - ${proposal.targetSection.date}`;
|
|
63
|
+
return sections.findIndex((section) => section.split('\n')[0] === expectedHeading);
|
|
64
|
+
}
|
|
65
|
+
function joinChangelog(header, sections) {
|
|
66
|
+
return `${header.trimEnd()}\n\n${sections.map((section) => section.trim()).join('\n\n')}\n`;
|
|
67
|
+
}
|
|
68
|
+
function unsafeChangelog(message) {
|
|
69
|
+
return {
|
|
70
|
+
status: 'blocked',
|
|
71
|
+
message,
|
|
72
|
+
warnings: [
|
|
73
|
+
{
|
|
74
|
+
code: 'unsafe-changelog',
|
|
75
|
+
message,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function unsafeParsedChangelog(message) {
|
|
81
|
+
return {
|
|
82
|
+
status: 'blocked',
|
|
83
|
+
message,
|
|
84
|
+
warnings: [
|
|
85
|
+
{
|
|
86
|
+
code: 'unsafe-changelog',
|
|
87
|
+
message,
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
export function readGitHistory(command, cwd = process.cwd()) {
|
|
3
|
+
if (!isInsideGitRepository(cwd)) {
|
|
4
|
+
return blocked('not-git-repository', 'Run the changelog workflow inside a git repository.');
|
|
5
|
+
}
|
|
6
|
+
const toRef = command.toRef ?? 'HEAD';
|
|
7
|
+
if (!isValidCommitRef(toRef, cwd)) {
|
|
8
|
+
return blocked('invalid-git-ref', `Could not resolve git ref \`${toRef}\`.`);
|
|
9
|
+
}
|
|
10
|
+
if (command.fromRef && !isValidCommitRef(command.fromRef, cwd)) {
|
|
11
|
+
return blocked('invalid-git-ref', `Could not resolve git ref \`${command.fromRef}\`.`);
|
|
12
|
+
}
|
|
13
|
+
const latestTag = command.fromRef ? undefined : getLatestReachableTag(toRef, cwd);
|
|
14
|
+
const fromRef = command.fromRef ?? latestTag;
|
|
15
|
+
const missingTag = !command.fromRef && !latestTag;
|
|
16
|
+
const range = fromRef ? `${fromRef}..${toRef}` : toRef;
|
|
17
|
+
const commits = readCommits(range, cwd);
|
|
18
|
+
const warnings = missingTag
|
|
19
|
+
? [
|
|
20
|
+
{
|
|
21
|
+
code: 'missing-tag',
|
|
22
|
+
message: 'No previous tag found. Using all reachable commits.',
|
|
23
|
+
},
|
|
24
|
+
]
|
|
25
|
+
: [];
|
|
26
|
+
return {
|
|
27
|
+
status: 'ok',
|
|
28
|
+
commits,
|
|
29
|
+
sourceRange: {
|
|
30
|
+
fromRef,
|
|
31
|
+
toRef,
|
|
32
|
+
usedLatestTag: !command.fromRef && Boolean(latestTag),
|
|
33
|
+
missingTag,
|
|
34
|
+
},
|
|
35
|
+
warnings,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function isInsideGitRepository(cwd) {
|
|
39
|
+
try {
|
|
40
|
+
return runGit(['rev-parse', '--is-inside-work-tree'], cwd).trim() === 'true';
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function isValidCommitRef(ref, cwd) {
|
|
47
|
+
try {
|
|
48
|
+
runGit(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], cwd);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function getLatestReachableTag(toRef, cwd) {
|
|
56
|
+
try {
|
|
57
|
+
const tag = runGit(['describe', '--tags', '--abbrev=0', toRef], cwd).trim();
|
|
58
|
+
return tag || undefined;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function readCommits(range, cwd) {
|
|
65
|
+
const output = runGit(['log', '--format=%H%x1f%s%x1f%b%x1e', range], cwd);
|
|
66
|
+
return output
|
|
67
|
+
.split('\x1e')
|
|
68
|
+
.map((record) => record.trim())
|
|
69
|
+
.filter((record) => record.length > 0)
|
|
70
|
+
.map(parseCommitRecord);
|
|
71
|
+
}
|
|
72
|
+
function parseCommitRecord(record) {
|
|
73
|
+
const [hash = '', subject = '', body = ''] = record.split('\x1f');
|
|
74
|
+
return {
|
|
75
|
+
hash,
|
|
76
|
+
subject,
|
|
77
|
+
body,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function blocked(code, message) {
|
|
81
|
+
return {
|
|
82
|
+
status: 'blocked',
|
|
83
|
+
message,
|
|
84
|
+
warnings: [{ code, message }],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function runGit(args, cwd) {
|
|
88
|
+
return execFileSync('git', args, {
|
|
89
|
+
cwd,
|
|
90
|
+
encoding: 'utf8',
|
|
91
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { buildChangelogProposal, renderChangelogPreview } from './changelog-format.js';
|
|
4
|
+
import { planChangelogUpdate } from './changelog-writer.js';
|
|
5
|
+
import { readGitHistory } from './git-history.js';
|
|
6
|
+
import { determineSemverBump, parseSemverVersion } from './semver.js';
|
|
7
|
+
export function handleGenerateChangelogProposal(command, cwd = process.cwd()) {
|
|
8
|
+
const targetSection = getTargetSection(command);
|
|
9
|
+
if (targetSection.status === 'blocked') {
|
|
10
|
+
return targetSection.result;
|
|
11
|
+
}
|
|
12
|
+
const gitHistory = readGitHistory(command, cwd);
|
|
13
|
+
if (gitHistory.status === 'blocked') {
|
|
14
|
+
return {
|
|
15
|
+
status: 'blocked',
|
|
16
|
+
message: gitHistory.message,
|
|
17
|
+
warnings: gitHistory.warnings,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const proposal = buildChangelogProposal({
|
|
21
|
+
commits: gitHistory.commits,
|
|
22
|
+
sourceRange: gitHistory.sourceRange,
|
|
23
|
+
targetSection: targetSection.value,
|
|
24
|
+
warnings: gitHistory.warnings,
|
|
25
|
+
});
|
|
26
|
+
return {
|
|
27
|
+
status: 'proposed',
|
|
28
|
+
proposal: addSemverMismatchWarning(proposal),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export async function handleGenerateChangelogWrite(command, ports = createNodeChangelogWritePorts(), cwd = process.cwd()) {
|
|
32
|
+
const proposalResult = handleGenerateChangelogProposal(command, cwd);
|
|
33
|
+
if (proposalResult.status === 'blocked') {
|
|
34
|
+
return proposalResult;
|
|
35
|
+
}
|
|
36
|
+
const changelogPath = path.join(cwd, 'CHANGELOG.md');
|
|
37
|
+
const preview = renderChangelogPreview(proposalResult.proposal, 'CHANGELOG.md');
|
|
38
|
+
ports.showPreview(preview);
|
|
39
|
+
const updatePlan = planChangelogUpdate(ports.readFile(changelogPath), proposalResult.proposal);
|
|
40
|
+
if (updatePlan.status === 'blocked') {
|
|
41
|
+
return {
|
|
42
|
+
status: 'blocked',
|
|
43
|
+
message: updatePlan.message,
|
|
44
|
+
warnings: updatePlan.warnings,
|
|
45
|
+
preview,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (!command.assumeYes) {
|
|
49
|
+
const confirmed = await ports.confirmWrite();
|
|
50
|
+
if (!confirmed) {
|
|
51
|
+
return {
|
|
52
|
+
status: 'declined',
|
|
53
|
+
changelogPath,
|
|
54
|
+
preview,
|
|
55
|
+
proposal: proposalResult.proposal,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
ports.writeFile(changelogPath, updatePlan.content);
|
|
60
|
+
return {
|
|
61
|
+
status: 'written',
|
|
62
|
+
changelogPath,
|
|
63
|
+
preview,
|
|
64
|
+
content: updatePlan.content,
|
|
65
|
+
proposal: proposalResult.proposal,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function getTargetSection(command) {
|
|
69
|
+
if (!command.version) {
|
|
70
|
+
return {
|
|
71
|
+
status: 'ok',
|
|
72
|
+
value: { type: 'unreleased' },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const version = parseSemverVersion(command.version);
|
|
76
|
+
if (version.status === 'invalid') {
|
|
77
|
+
return {
|
|
78
|
+
status: 'blocked',
|
|
79
|
+
result: {
|
|
80
|
+
status: 'blocked',
|
|
81
|
+
message: version.message,
|
|
82
|
+
warnings: [
|
|
83
|
+
{
|
|
84
|
+
code: 'invalid-version',
|
|
85
|
+
message: version.message,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const date = command.date ?? new Date().toISOString().slice(0, 10);
|
|
92
|
+
if (!isIsoDate(date)) {
|
|
93
|
+
const message = `Release date \`${date}\` must use ISO format YYYY-MM-DD.`;
|
|
94
|
+
return {
|
|
95
|
+
status: 'blocked',
|
|
96
|
+
result: {
|
|
97
|
+
status: 'blocked',
|
|
98
|
+
message,
|
|
99
|
+
warnings: [
|
|
100
|
+
{
|
|
101
|
+
code: 'invalid-date',
|
|
102
|
+
message,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
status: 'ok',
|
|
110
|
+
value: {
|
|
111
|
+
type: 'version',
|
|
112
|
+
version: version.version.version,
|
|
113
|
+
date,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function addSemverMismatchWarning(proposal) {
|
|
118
|
+
if (proposal.targetSection.type !== 'version' || !proposal.sourceRange.fromRef) {
|
|
119
|
+
return proposal;
|
|
120
|
+
}
|
|
121
|
+
const previousVersion = parseSemverVersion(proposal.sourceRange.fromRef);
|
|
122
|
+
if (previousVersion.status !== 'ok') {
|
|
123
|
+
return proposal;
|
|
124
|
+
}
|
|
125
|
+
const targetVersion = parseSemverVersion(proposal.targetSection.version);
|
|
126
|
+
if (targetVersion.status !== 'ok') {
|
|
127
|
+
return proposal;
|
|
128
|
+
}
|
|
129
|
+
const actualBump = determineSemverBump(previousVersion.version, targetVersion.version);
|
|
130
|
+
if (actualBump.status !== 'ok' || actualBump.bump === proposal.semverGuidance.suggestedBump) {
|
|
131
|
+
return proposal;
|
|
132
|
+
}
|
|
133
|
+
const warning = {
|
|
134
|
+
code: 'semver-bump-mismatch',
|
|
135
|
+
message: `Provided version ${proposal.targetSection.version} is a ${actualBump.bump} bump from ${previousVersion.version.version}, but commit history suggests a ${proposal.semverGuidance.suggestedBump} bump.`,
|
|
136
|
+
};
|
|
137
|
+
return {
|
|
138
|
+
...proposal,
|
|
139
|
+
warnings: [...proposal.warnings, warning],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function isIsoDate(value) {
|
|
143
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
const date = new Date(`${value}T00:00:00.000Z`);
|
|
147
|
+
return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === value;
|
|
148
|
+
}
|
|
149
|
+
function createNodeChangelogWritePorts() {
|
|
150
|
+
return {
|
|
151
|
+
readFile(filePath) {
|
|
152
|
+
if (!fs.existsSync(filePath)) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
156
|
+
},
|
|
157
|
+
writeFile(filePath, content) {
|
|
158
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
159
|
+
},
|
|
160
|
+
confirmWrite() {
|
|
161
|
+
return false;
|
|
162
|
+
},
|
|
163
|
+
showPreview(preview) {
|
|
164
|
+
process.stdout.write(preview);
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function parseSemverVersion(input) {
|
|
2
|
+
const trimmedInput = input.trim();
|
|
3
|
+
const match = /^v?(?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)\.(?<patch>0|[1-9]\d*)$/.exec(trimmedInput);
|
|
4
|
+
if (!match?.groups) {
|
|
5
|
+
return {
|
|
6
|
+
status: 'invalid',
|
|
7
|
+
message: `Release version \`${input}\` must use SemVer format MAJOR.MINOR.PATCH, with an optional leading v.`,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
const major = Number(match.groups.major);
|
|
11
|
+
const minor = Number(match.groups.minor);
|
|
12
|
+
const patch = Number(match.groups.patch);
|
|
13
|
+
return {
|
|
14
|
+
status: 'ok',
|
|
15
|
+
version: {
|
|
16
|
+
version: `${major}.${minor}.${patch}`,
|
|
17
|
+
major,
|
|
18
|
+
minor,
|
|
19
|
+
patch,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function determineSemverBump(previousVersion, nextVersion) {
|
|
24
|
+
if (nextVersion.major > previousVersion.major) {
|
|
25
|
+
return { status: 'ok', bump: 'major' };
|
|
26
|
+
}
|
|
27
|
+
if (nextVersion.major !== previousVersion.major) {
|
|
28
|
+
return { status: 'not-comparable' };
|
|
29
|
+
}
|
|
30
|
+
if (nextVersion.minor > previousVersion.minor) {
|
|
31
|
+
return { status: 'ok', bump: 'minor' };
|
|
32
|
+
}
|
|
33
|
+
if (nextVersion.minor !== previousVersion.minor) {
|
|
34
|
+
return { status: 'not-comparable' };
|
|
35
|
+
}
|
|
36
|
+
if (nextVersion.patch > previousVersion.patch) {
|
|
37
|
+
return { status: 'ok', bump: 'patch' };
|
|
38
|
+
}
|
|
39
|
+
return { status: 'not-comparable' };
|
|
40
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { parseSemverVersion } from '../generate-changelog/semver.js';
|
|
3
|
+
export function resolveReleaseRange(command, cwd = process.cwd()) {
|
|
4
|
+
if (!isInsideGitRepository(cwd)) {
|
|
5
|
+
return blocked('not-git-repository', 'Run the release workflow inside a git repository.');
|
|
6
|
+
}
|
|
7
|
+
if (!isWorkingTreeClean(cwd)) {
|
|
8
|
+
return blocked('dirty-working-tree', 'Working tree must be clean before planning a release.');
|
|
9
|
+
}
|
|
10
|
+
const toRef = command.toRef ?? 'HEAD';
|
|
11
|
+
if (!isValidCommitRef(toRef, cwd)) {
|
|
12
|
+
return blocked('invalid-git-ref', `Could not resolve git ref \`${toRef}\`.`);
|
|
13
|
+
}
|
|
14
|
+
if (command.fromRef) {
|
|
15
|
+
if (!isValidCommitRef(command.fromRef, cwd)) {
|
|
16
|
+
return blocked('invalid-git-ref', `Could not resolve git ref \`${command.fromRef}\`.`);
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
status: 'ok',
|
|
20
|
+
range: {
|
|
21
|
+
fromRef: command.fromRef,
|
|
22
|
+
toRef,
|
|
23
|
+
},
|
|
24
|
+
usedLatestSemverTag: false,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const latestSemverTag = getLatestReachableSemverTag(toRef, cwd);
|
|
28
|
+
if (!latestSemverTag) {
|
|
29
|
+
return blocked('missing-semver-tag', 'No previous SemVer-like release tag found. Provide --from and --version to release from an explicit baseline.');
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
status: 'ok',
|
|
33
|
+
range: {
|
|
34
|
+
fromRef: latestSemverTag,
|
|
35
|
+
toRef,
|
|
36
|
+
},
|
|
37
|
+
usedLatestSemverTag: true,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function checkReleaseTagAvailable(tagName, cwd = process.cwd()) {
|
|
41
|
+
const tagCommit = resolveRef(`refs/tags/${tagName}`, cwd);
|
|
42
|
+
if (!tagCommit) {
|
|
43
|
+
return { status: 'ok', existingTagAtHead: false };
|
|
44
|
+
}
|
|
45
|
+
const headCommit = resolveRef('HEAD', cwd);
|
|
46
|
+
if (tagCommit === headCommit) {
|
|
47
|
+
return { status: 'ok', existingTagAtHead: true };
|
|
48
|
+
}
|
|
49
|
+
return blockedTag('existing-target-tag', `Release tag \`${tagName}\` already exists and does not point at HEAD.`);
|
|
50
|
+
}
|
|
51
|
+
function isInsideGitRepository(cwd) {
|
|
52
|
+
try {
|
|
53
|
+
return runGit(['rev-parse', '--is-inside-work-tree'], cwd).trim() === 'true';
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function isWorkingTreeClean(cwd) {
|
|
60
|
+
return runGit(['status', '--porcelain'], cwd).trim().length === 0;
|
|
61
|
+
}
|
|
62
|
+
function isValidCommitRef(ref, cwd) {
|
|
63
|
+
try {
|
|
64
|
+
runGit(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], cwd);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function getLatestReachableSemverTag(toRef, cwd) {
|
|
72
|
+
const tags = readReachableTags(toRef, cwd);
|
|
73
|
+
return tags.find((tag) => parseSemverVersion(tag).status === 'ok');
|
|
74
|
+
}
|
|
75
|
+
function readReachableTags(toRef, cwd) {
|
|
76
|
+
try {
|
|
77
|
+
return runGit(['tag', '--merged', toRef, '--sort=-version:refname'], cwd)
|
|
78
|
+
.split('\n')
|
|
79
|
+
.map((tag) => tag.trim())
|
|
80
|
+
.filter((tag) => tag.length > 0);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function resolveRef(ref, cwd) {
|
|
87
|
+
try {
|
|
88
|
+
return runGit(['rev-parse', '--verify', '--quiet', ref], cwd).trim();
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function blocked(code, message) {
|
|
95
|
+
return {
|
|
96
|
+
status: 'blocked',
|
|
97
|
+
message,
|
|
98
|
+
warnings: [{ code, message }],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function blockedTag(code, message) {
|
|
102
|
+
return {
|
|
103
|
+
status: 'blocked',
|
|
104
|
+
message,
|
|
105
|
+
warnings: [{ code, message }],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function runGit(args, cwd) {
|
|
109
|
+
return execFileSync('git', args, {
|
|
110
|
+
cwd,
|
|
111
|
+
encoding: 'utf8',
|
|
112
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
113
|
+
});
|
|
114
|
+
}
|