Skip to content

Commit 7c876ba

Browse files
author
Illia Obukhau
authored
[WC-1418] Add changelog helper script (#47)
2 parents 679c023 + 408004a commit 7c876ba

File tree

10 files changed

+356
-92
lines changed

10 files changed

+356
-92
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
"release": "turbo run release",
2020
"create-gh-release": "turbo run create-gh-release",
2121
"publish-marketplace": "turbo run publish-marketplace",
22-
"version": "ts-node --project ./scripts/tsconfig.json ./scripts/release/BumpVersion.ts",
22+
"version": "pnpm --filter release-utils-internal run version",
23+
"changelog": "pnpm --filter release-utils-internal run changelog",
2324
"validate-staged-widget-versions": "node scripts/validation/validate-versions-staged-files.js"
2425
},
2526
"devDependencies": {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/usr/bin/env ts-node-script
2+
3+
import { prompt } from "enquirer";
4+
import { selectPackage } from "../src";
5+
import { getNextVersion, writeVersion } from "../src/bump-version";
6+
7+
import { oraPromise } from "../src/cli-utils";
8+
9+
async function main(): Promise<void> {
10+
const pkg = await selectPackage();
11+
const nextVersion = await getNextVersion(pkg.version);
12+
13+
const { save } = await prompt<{ save: boolean }>({
14+
type: "confirm",
15+
name: "save",
16+
message: "Save changes?"
17+
});
18+
19+
if (save) {
20+
await oraPromise(writeVersion(pkg, nextVersion), "Writing changes...");
21+
console.log("Done.");
22+
} else {
23+
console.log("Exit without changes.");
24+
}
25+
}
26+
27+
main().catch(e => {
28+
console.error(e);
29+
process.exit(1);
30+
});
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#!/usr/bin/env ts-node-script
2+
3+
import { prompt } from "enquirer";
4+
import { getModuleInfo, PackageListing, selectPackage } from "../src";
5+
import { getNextVersion, writeVersion } from "../src/bump-version";
6+
import {
7+
getModuleChangelog,
8+
getWidgetChangelog,
9+
ModuleChangelogFileWrapper,
10+
WidgetChangelogFileWrapper
11+
} from "../src/changelog-parser";
12+
import { LogSection } from "../src/changelog-parser/types";
13+
import { oraPromise } from "../src/cli-utils";
14+
15+
async function getChangelogSections(): Promise<LogSection[]> {
16+
const sections: LogSection[] = [];
17+
let adding = true;
18+
19+
while (adding) {
20+
const { sectionType } = await prompt<{ sectionType: string }>({
21+
type: "autocomplete",
22+
name: "sectionType",
23+
message: "Please select section type",
24+
choices: [
25+
"Added",
26+
"Changed",
27+
"Deprecated",
28+
"Removed",
29+
"Fixed",
30+
"Security",
31+
"Breaking changes",
32+
"Documentation"
33+
]
34+
});
35+
36+
const { message } = await prompt<{ message: string }>({
37+
type: "input",
38+
name: "message",
39+
message: "Message"
40+
});
41+
42+
sections.push({
43+
type: sectionType as LogSection["type"],
44+
logs: [message]
45+
});
46+
47+
const { addMore } = await prompt<{ addMore: boolean }>({
48+
type: "confirm",
49+
name: "addMore",
50+
message: "Add one more record?"
51+
});
52+
53+
adding = addMore;
54+
}
55+
56+
return sections;
57+
}
58+
59+
async function selectNextVersion(currentVersion: string): Promise<string | undefined> {
60+
const { bump } = await prompt<{ bump: boolean }>({
61+
type: "confirm",
62+
name: "bump",
63+
message: "Would you like to bump the package version?"
64+
});
65+
66+
if (bump) {
67+
return getNextVersion(currentVersion);
68+
}
69+
70+
return undefined;
71+
}
72+
73+
async function writeChanges(pkg: PackageListing, sections: LogSection[], nextVersion?: string): Promise<void> {
74+
let changelog: WidgetChangelogFileWrapper | ModuleChangelogFileWrapper;
75+
try {
76+
changelog = await getWidgetChangelog(pkg.path);
77+
} catch {
78+
const module = await getModuleInfo(pkg.path);
79+
changelog = await getModuleChangelog(pkg.path, module.mxpackage.name);
80+
}
81+
82+
changelog.addUnreleasedSections(sections).save();
83+
84+
if (nextVersion) {
85+
await writeVersion(pkg, nextVersion);
86+
}
87+
}
88+
89+
async function main(): Promise<void> {
90+
const pkg = await selectPackage();
91+
const sections = await getChangelogSections();
92+
const nextVersion = await selectNextVersion(pkg.version);
93+
94+
const { save } = await prompt<{ save: boolean }>({
95+
type: "confirm",
96+
name: "save",
97+
message: "Save changes?"
98+
});
99+
100+
if (save) {
101+
await oraPromise(writeChanges(pkg, sections, nextVersion), "Writing changes...");
102+
console.log("Done.");
103+
} else {
104+
console.log("Exit without changes.");
105+
}
106+
}
107+
108+
main().catch(e => {
109+
console.error(e);
110+
process.exit(1);
111+
});

packages/tools/release-utils-internal/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,21 @@
1717
"lint": "eslint --config ../../../.eslintrc.js --ext .jsx,.js,.ts,.tsx src/",
1818
"compile:parser:widget": "peggy -o ./src/changelog-parser/parser/module/module.js ./src/changelog-parser/parser/module/module.pegjs",
1919
"compile:parser:module": "peggy -o ./src/changelog-parser/parser/widget/widget.js ./src/changelog-parser/parser/widget/widget.pegjs",
20-
"prepare": "pnpm run compile:parser:widget && pnpm run compile:parser:module && tsc"
20+
"prepare": "pnpm run compile:parser:widget && pnpm run compile:parser:module && tsc",
21+
"changelog": "ts-node bin/rui-changelog-helper.ts",
22+
"version": "ts-node bin/rui-bump-version.ts"
2123
},
2224
"devDependencies": {
2325
"@types/cross-zip": "^4.0.0",
2426
"@types/node-fetch": "2.6.1",
27+
"chalk": "^4.1.2",
2528
"cross-zip": "^4.0.0",
29+
"enquirer": "^2.3.6",
2630
"eslint": "^7.20.0",
2731
"execa": "^5.1.1",
2832
"fast-xml-parser": "^4.0.1",
2933
"node-fetch": "^2.6.1",
34+
"ora": "^5.4.1",
3035
"peggy": "^1.2.0",
3136
"shelljs": "^0.8.4",
3237
"ts-node": "^9.0.0",
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import chalk from "chalk";
2+
import { spawnSync } from "child_process";
3+
import { prompt } from "enquirer";
4+
import { promises as fs } from "fs";
5+
import { join } from "path";
6+
import { nextTick } from "process";
7+
import { PackageListing } from "./monorepo";
8+
9+
export type BumpVersionType = "patch" | "minor" | "major" | string;
10+
11+
export function getNewVersion(bumpVersionType: BumpVersionType, currentVersion: string): string {
12+
const [major, minor, patch] = currentVersion.split(".");
13+
switch (bumpVersionType) {
14+
case "patch":
15+
return [major, minor, Number(patch) + 1].join(".");
16+
case "minor":
17+
return [major, Number(minor) + 1, 0].join(".");
18+
case "major":
19+
return [Number(major) + 1, 0, 0].join(".");
20+
default:
21+
return bumpVersionType;
22+
}
23+
}
24+
25+
export function bumpPackageJson(path: string, version: string): void {
26+
spawnSync("npm", ["version", version], { cwd: path });
27+
}
28+
29+
export async function bumpXml(path: string, version: string): Promise<boolean> {
30+
const packageXmlFile = join(path, "src", "package.xml");
31+
try {
32+
const content = await fs.readFile(packageXmlFile);
33+
if (content) {
34+
const newContent = content.toString().replace(/version=.+xmlns/, `version="${version}" xmlns`);
35+
await fs.writeFile(packageXmlFile, newContent);
36+
return true;
37+
}
38+
return false;
39+
} catch (e) {
40+
throw new Error("package.xml not found");
41+
}
42+
}
43+
44+
export async function writeVersion(pkg: PackageListing, version: string): Promise<void> {
45+
bumpPackageJson(pkg.path, version);
46+
try {
47+
await bumpXml(pkg.path, version);
48+
} catch {
49+
nextTick(() => {
50+
const msg = `[WARN] Update version: package ${pkg.name} is missing package.xml, skip`;
51+
console.warn(chalk.yellow(msg));
52+
});
53+
}
54+
}
55+
56+
export async function selectBumpVersionType(): Promise<BumpVersionType> {
57+
const { bumpType } = await prompt<{ bumpType: string }>({
58+
type: "autocomplete",
59+
name: "bumpType",
60+
message: "Want to bump?",
61+
choices: ["major", "minor", "patch", "set manually"]
62+
});
63+
64+
if (bumpType === "set manually") {
65+
const { nextVersion } = await prompt<{ nextVersion: string }>({
66+
type: "input",
67+
name: "nextVersion",
68+
message: "Set package version to"
69+
});
70+
71+
return nextVersion;
72+
} else {
73+
return bumpType;
74+
}
75+
}
76+
77+
export async function getNextVersion(currentVersion: string): Promise<string> {
78+
const bumpVersionType = await selectBumpVersionType();
79+
const nextVersion = getNewVersion(bumpVersionType, currentVersion);
80+
console.log(chalk.green(`Version change: ${currentVersion} => ${nextVersion}`));
81+
return nextVersion;
82+
}

packages/tools/release-utils-internal/src/changelog-parser/index.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,28 @@ function formatDate(date: Date): string {
8080
.padStart(2, "0")}`;
8181
}
8282

83+
function mergeUnreleased<T extends UnreleasedVersionEntry>(unreleased: T, sections: LogSection[]): T {
84+
const currentTypes = unreleased.sections.map(s => s.type);
85+
const incomingTypes = sections.map(s => s.type);
86+
const uniqueTypes = new Set([...currentTypes, ...incomingTypes]);
87+
88+
const nextSections = Array.from(uniqueTypes).map(type => {
89+
const section = unreleased.sections.find(s => s.type === type) ?? {
90+
type,
91+
logs: []
92+
};
93+
94+
const incomingLogs = sections.flatMap(s => (s.type === type ? s.logs : []));
95+
96+
return { type: section.type, logs: [...section.logs, ...incomingLogs] };
97+
});
98+
99+
return {
100+
...unreleased,
101+
sections: nextSections
102+
};
103+
}
104+
83105
export class WidgetChangelogFileWrapper {
84106
changelog: WidgetChangelogFile;
85107

@@ -139,6 +161,18 @@ export class WidgetChangelogFileWrapper {
139161
);
140162
}
141163

164+
addUnreleasedSections(sections: LogSection[]): WidgetChangelogFileWrapper {
165+
const [unreleased, ...rest] = this.changelog.content;
166+
167+
return new WidgetChangelogFileWrapper(
168+
{
169+
header: this.changelog.header,
170+
content: [mergeUnreleased(unreleased, sections), ...rest]
171+
},
172+
this.changelogPath
173+
);
174+
}
175+
142176
static fromFile(filePath: string): WidgetChangelogFileWrapper {
143177
return new WidgetChangelogFileWrapper(
144178
parseWidgetChangelogFile(readFileSync(filePath).toString(), { Version }),
@@ -215,6 +249,19 @@ export class ModuleChangelogFileWrapper {
215249
);
216250
}
217251

252+
addUnreleasedSections(sections: LogSection[]): ModuleChangelogFileWrapper {
253+
const [unreleased, ...rest] = this.changelog.content;
254+
255+
return new ModuleChangelogFileWrapper(
256+
{
257+
header: this.changelog.header,
258+
content: [mergeUnreleased(unreleased, sections), ...rest],
259+
moduleName: this.moduleName
260+
},
261+
this.changelogPath
262+
);
263+
}
264+
218265
addUnreleasedSubcomponents(subcomponents: SubComponentEntry[]): ModuleChangelogFileWrapper {
219266
const [unreleased, ...releasedContent] = this.changelog.content;
220267

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import ora from "ora";
2+
3+
export async function oraPromise<T>(task: Promise<T>, msg: string): Promise<T> {
4+
const spinner = ora(msg);
5+
spinner.start();
6+
const r = await task;
7+
spinner.stop();
8+
return r;
9+
}

packages/tools/release-utils-internal/src/monorepo.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { prompt } from "enquirer";
2+
import { oraPromise } from "./cli-utils";
13
import { exec, find, mkdir, cp } from "./shell";
24

35
type DependencyName = string;
@@ -37,3 +39,22 @@ export async function copyMpkFiles(packageNames: string[], dest: string): Promis
3739
mkdir("-p", dest);
3840
cp(paths, dest);
3941
}
42+
43+
export async function selectPackage(): Promise<PackageListing> {
44+
const pkgs = await oraPromise(listPackages(["'*'", "!web-widgets"]), "Loading packages...");
45+
46+
const { packageName } = await prompt<{ packageName: string }>({
47+
type: "autocomplete",
48+
name: "packageName",
49+
message: "Please select package",
50+
choices: pkgs.map(pkg => pkg.name)
51+
});
52+
53+
const pkg = pkgs.find(p => p.name === packageName);
54+
55+
if (!pkg) {
56+
throw new Error(`Unable to find package meta for ${packageName}`);
57+
}
58+
59+
return pkg;
60+
}

0 commit comments

Comments
 (0)