Skip to content

Commit 56db8cf

Browse files
committed
add release automation
1 parent c2166b4 commit 56db8cf

File tree

8 files changed

+413
-1
lines changed

8 files changed

+413
-1
lines changed

automation/config.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"devBranch": "master",
3+
"releaseBranch": "release",
4+
"github": "https://github.com/mProjectsCode/obsidian-meta-bind-plugin"
5+
}

automation/release.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { $seq, Verboseness, $input, $choise, $confirm, CMD_FMT } from './shellUtils';
2+
import config from './config.json';
3+
import { Version, getIncrementOptions, parseVersion, stringifyVersion } from 'versionUtils';
4+
import { UserError } from 'utils';
5+
6+
async function runPreconditions(): Promise<void> {
7+
// run preconditions
8+
await $seq(
9+
[`bun run format`, `bun run lint:fix`, `bun run test`],
10+
(cmd: string) => {
11+
throw new UserError(`precondition "${cmd}" failed`);
12+
},
13+
() => {},
14+
Verboseness.VERBOSE,
15+
);
16+
17+
// add changed files
18+
await $seq(
19+
[`git add .`],
20+
() => {
21+
throw new UserError('failed to add preconditions changes to git');
22+
},
23+
() => {},
24+
Verboseness.NORMAL,
25+
);
26+
27+
// check if there were any changes
28+
let changesToCommit = false;
29+
await $seq(
30+
[`git diff --quiet`, `git diff --cached --quiet`],
31+
() => {
32+
changesToCommit = true;
33+
},
34+
() => {},
35+
Verboseness.QUITET,
36+
);
37+
38+
// if there were any changes, commit them
39+
if (changesToCommit) {
40+
await $seq(
41+
[`git commit -m "[auto] run release preconditions"`],
42+
() => {
43+
throw new UserError('failed to add preconditions changes to git');
44+
},
45+
() => {},
46+
Verboseness.NORMAL,
47+
);
48+
}
49+
}
50+
51+
async function run() {
52+
console.log('looking for untracked changes ...');
53+
54+
// check for any uncommited files and exit if there are any
55+
await $seq(
56+
[`git add .`, `git diff --quiet`, `git diff --cached --quiet`, `git checkout ${config.devBranch}`],
57+
() => {
58+
throw new UserError('there are still untracked changes');
59+
},
60+
() => {},
61+
Verboseness.QUITET,
62+
);
63+
64+
console.log('\nrunning preconditions ...\n');
65+
66+
await runPreconditions();
67+
68+
console.log('\nbumping versions ...\n');
69+
70+
const manifestFile = Bun.file('./manifest.json');
71+
const manifest = await manifestFile.json();
72+
73+
const versionString: string = manifest.version;
74+
const currentVersion: Version = parseVersion(versionString);
75+
const currentVersionString = stringifyVersion(currentVersion);
76+
77+
const versionIncrementOptions = getIncrementOptions(currentVersion);
78+
79+
const selctedIndex = await $choise(
80+
`Current version "${currentVersionString}". Select new version`,
81+
versionIncrementOptions.map(x => stringifyVersion(x)),
82+
);
83+
const newVersion = versionIncrementOptions[selctedIndex];
84+
const newVersionString = stringifyVersion(newVersion);
85+
86+
console.log('');
87+
88+
await $confirm(`Version will be updated "${currentVersionString}" -> "${newVersionString}". Are you sure`, () => {
89+
throw new UserError('user canceled script');
90+
});
91+
92+
manifest.version = newVersionString;
93+
94+
await Bun.write(manifestFile, JSON.stringify(manifest, null, '\t'));
95+
96+
const versionsFile = Bun.file('./versions.json');
97+
const versionsJson = await versionsFile.json();
98+
99+
versionsJson[newVersionString] = manifest.minAppVersion;
100+
101+
await Bun.write(versionsFile, JSON.stringify(versionsJson, null, '\t'));
102+
103+
const packageFile = Bun.file('./package.json');
104+
const packageJson = await packageFile.json();
105+
106+
packageJson.version = newVersionString;
107+
108+
await Bun.write(packageFile, JSON.stringify(packageJson, null, '\t'));
109+
110+
await $seq(
111+
[`bun run format`, `git add .`, `git commit -m "[auto] bump version to \`${newVersionString}\`"`],
112+
() => {
113+
throw new UserError('failed to add preconditions changes to git');
114+
},
115+
() => {},
116+
Verboseness.NORMAL,
117+
);
118+
119+
console.log('\ncreating release tag ...\n');
120+
121+
await $seq(
122+
[
123+
`git checkout ${config.releaseBranch}`,
124+
`git merge ${config.devBranch} --commit -m "[auto] merge \`${newVersionString}\` release commit"`,
125+
`git push origin ${config.releaseBranch}`,
126+
`git tag -a ${newVersionString} -m "release version ${newVersionString}"`,
127+
`git push origin ${newVersionString}`,
128+
`git checkout ${config.devBranch}`,
129+
`git merge ${config.releaseBranch}`,
130+
`git push origin ${config.devBranch}`,
131+
],
132+
() => {
133+
throw new UserError('failed to merge or create tag');
134+
},
135+
() => {},
136+
Verboseness.NORMAL,
137+
);
138+
139+
console.log('');
140+
141+
console.log(`${CMD_FMT.BgGreen}done${CMD_FMT.Reset}`);
142+
console.log(`${config.github}`);
143+
console.log(`${config.github}/releases/tag/${newVersionString}`);
144+
}
145+
146+
try {
147+
await run();
148+
} catch (e) {
149+
if (e instanceof UserError) {
150+
console.error(e.message);
151+
} else {
152+
console.error(e);
153+
}
154+
}

automation/shellUtils.ts

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { Subprocess } from 'bun';
2+
import stringArgv from 'string-argv';
3+
4+
export enum Verboseness {
5+
QUITET,
6+
NORMAL,
7+
VERBOSE,
8+
}
9+
10+
function exec(c: string): Subprocess<'ignore', 'pipe', 'inherit'> {
11+
return Bun.spawn(stringArgv(c));
12+
}
13+
14+
export async function $(cmd: string, verboseness: Verboseness = Verboseness.NORMAL): Promise<{ stdout: string; stderr: string; exit: number }> {
15+
if (verboseness === Verboseness.NORMAL || verboseness === Verboseness.VERBOSE) {
16+
console.log(`\n${CMD_FMT.Bright}running${CMD_FMT.Reset} - ${cmd}\n`);
17+
}
18+
19+
const proc = exec(cmd);
20+
const stdout = await new Response(proc.stdout).text();
21+
const stderr = await new Response(proc.stdout).text();
22+
23+
if (verboseness === Verboseness.VERBOSE) {
24+
if (stdout !== '') {
25+
console.log(
26+
stdout
27+
.split('\n')
28+
.map(x => `${CMD_FMT.FgGray}>${CMD_FMT.Reset} ${x}\n`)
29+
.join(''),
30+
);
31+
}
32+
33+
if (stderr !== '') {
34+
console.log(
35+
stderr
36+
.split('\n')
37+
.map(x => `${CMD_FMT.FgRed}>${CMD_FMT.Reset} ${x}\n`)
38+
.join(''),
39+
);
40+
}
41+
}
42+
43+
const exit = await proc.exited;
44+
45+
if (verboseness === Verboseness.NORMAL || verboseness === Verboseness.VERBOSE) {
46+
if (exit === 0) {
47+
console.log(`${CMD_FMT.FgGreen}success${CMD_FMT.Reset} - ${cmd}\n`);
48+
} else {
49+
console.log(`${CMD_FMT.FgRed}fail${CMD_FMT.Reset} - ${cmd} - code ${exit}\n`);
50+
}
51+
}
52+
53+
return {
54+
stdout,
55+
stderr,
56+
exit,
57+
};
58+
}
59+
60+
export async function $seq(
61+
cmds: string[],
62+
onError: (cmd: string, index: number) => void,
63+
onSuccess: () => void,
64+
verboseness: Verboseness = Verboseness.NORMAL,
65+
): Promise<void> {
66+
const results = [];
67+
for (let i = 0; i < cmds.length; i += 1) {
68+
const cmd = cmds[i];
69+
const result = await $(cmd, verboseness);
70+
71+
if (result.exit !== 0) {
72+
onError(cmd, i);
73+
return;
74+
}
75+
76+
results.push(result);
77+
}
78+
onSuccess();
79+
}
80+
81+
export async function $input(message: string): Promise<string> {
82+
console.write(`${message} `);
83+
const stdin = Bun.stdin.stream();
84+
const reader = stdin.getReader();
85+
const chunk = await reader.read();
86+
const text = Buffer.from(chunk.value ?? '').toString();
87+
return text.trim();
88+
}
89+
90+
export async function $choise(message: string, options: string[]): Promise<number> {
91+
console.log(`${message} `);
92+
93+
let optionNumbers = new Map<string, number>();
94+
95+
for (let i = 0; i < options.length; i++) {
96+
const option = options[i];
97+
console.log(`[${i}] ${option}`);
98+
99+
optionNumbers.set(i.toString(), i);
100+
}
101+
102+
let ret: undefined | number = undefined;
103+
104+
while (ret === undefined) {
105+
const selectedStr = await $input(`Select [${[...optionNumbers.keys()].join('/')}]:`);
106+
107+
ret = optionNumbers.get(selectedStr);
108+
109+
if (ret === undefined) {
110+
console.log(`${CMD_FMT.FgRed}invalid selection, please select a valid option${CMD_FMT.Reset}`);
111+
}
112+
}
113+
114+
return ret;
115+
}
116+
117+
export async function $confirm(message: string, onReject: () => void): Promise<void> {
118+
while (true) {
119+
const answer = await $input(`${message} [Y/N]?`);
120+
121+
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
122+
return;
123+
}
124+
125+
if (answer.toLowerCase() === 'n' || answer.toLowerCase() === 'no') {
126+
onReject();
127+
return;
128+
}
129+
130+
console.log(`${CMD_FMT.FgRed}invalid selection, please select a valid option${CMD_FMT.Reset}`);
131+
}
132+
}
133+
134+
export const CMD_FMT = {
135+
Reset: '\x1b[0m',
136+
Bright: '\x1b[1m',
137+
Dim: '\x1b[2m',
138+
Underscore: '\x1b[4m',
139+
Blink: '\x1b[5m',
140+
Reverse: '\x1b[7m',
141+
Hidden: '\x1b[8m',
142+
143+
FgBlack: '\x1b[30m',
144+
FgRed: '\x1b[31m',
145+
FgGreen: '\x1b[32m',
146+
FgYellow: '\x1b[33m',
147+
FgBlue: '\x1b[34m',
148+
FgMagenta: '\x1b[35m',
149+
FgCyan: '\x1b[36m',
150+
FgWhite: '\x1b[37m',
151+
FgGray: '\x1b[90m',
152+
153+
BgBlack: '\x1b[40m',
154+
BgRed: '\x1b[41m',
155+
BgGreen: '\x1b[42m',
156+
BgYellow: '\x1b[43m',
157+
BgBlue: '\x1b[44m',
158+
BgMagenta: '\x1b[45m',
159+
BgCyan: '\x1b[46m',
160+
BgWhite: '\x1b[47m',
161+
BgGray: '\x1b[100m',
162+
};

automation/tsconfig.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"compilerOptions": {
3+
"baseUrl": ".",
4+
"module": "ESNext",
5+
"target": "ESNext",
6+
"allowJs": true,
7+
"noImplicitAny": true,
8+
"strict": true,
9+
"strictNullChecks": true,
10+
"noImplicitReturns": true,
11+
"moduleResolution": "node",
12+
"importHelpers": true,
13+
"isolatedModules": true,
14+
"lib": ["DOM", "ES5", "ES6", "ES7", "Es2021"],
15+
"types": ["bun-types"],
16+
"allowSyntheticDefaultImports": true,
17+
"resolveJsonModule": true
18+
},
19+
"include": ["**/*.ts"]
20+
}

automation/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export class UserError extends Error {}

0 commit comments

Comments
 (0)