release-suite 0.1.0 โ†’ 1.0.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.
@@ -1,132 +1,173 @@
1
- #!/usr/bin/env node
2
- import { execSync } from "node:child_process";
3
- import fs from "node:fs";
4
-
5
- function run(cmd) {
6
- return execSync(cmd, { encoding: "utf8" }).trim();
7
- }
8
-
9
- const isPreview = process.env.PREVIEW_MODE === "true";
10
-
11
- function getPackageVersion() {
12
- try {
13
- const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"));
14
- return pkg.version;
15
- } catch {
16
- return "0.0.0";
17
- }
18
- }
19
-
20
- function getLastTag() {
21
- try {
22
- const tag = run("git describe --tags --abbrev=0");
23
- return tag.replace(/^v/, "");
24
- } catch {
25
- const pkgVersion = getPackageVersion();
26
- if (isPreview) {
27
- console.log(
28
- `โš  No previous tag found. Using package.json version (${pkgVersion}) as base.`
29
- );
30
- }
31
- return pkgVersion;
32
- }
33
- }
34
-
35
- function getCommitsSince(version) {
36
- try {
37
- // If version came from package.json (no tag), include all commits
38
- const hasTag = (() => {
39
- try {
40
- run("git describe --tags --abbrev=0");
41
- return true;
42
- } catch {
43
- return false;
44
- }
45
- })();
46
-
47
- const range = hasTag ? `${version}..HEAD` : "HEAD";
48
-
49
- return run(
50
- `git log ${range} --pretty=format:%H%x1f%s%x1f%b`
51
- )
52
- .split("\n")
53
- .filter(Boolean);
54
- } catch {
55
- return [];
56
- }
57
- }
58
-
59
- function parseCommitLine(line) {
60
- const [hash, subject = "", body = ""] = line.split("\x1f");
61
- return { hash, subject, body };
62
- }
63
-
64
- function detectType(subject, body) {
65
- const re =
66
- /^(:\S+: )?(feat|fix|refactor|docs|chore|style|test|build|perf|ci|raw|cleanup|remove)(\(.+\))?(!)?:/i;
67
-
68
- const m = subject.match(re);
69
- const isBreaking =
70
- /BREAKING CHANGE/i.test(body) || (m && m[4] === "!");
71
-
72
- if (isBreaking) return "major";
73
- if (m) {
74
- const t = m[2].toLowerCase();
75
- if (t === "feat") return "minor";
76
- if (t === "fix") return "patch";
77
- }
78
- return "none";
79
- }
80
-
81
- function bumpVersion(type, version) {
82
- const [major, minor, patch] = version
83
- .split(".")
84
- .map(n => parseInt(n, 10) || 0);
85
-
86
- if (type === "major") return `${major + 1}.0.0`;
87
- if (type === "minor") return `${major}.${minor + 1}.0`;
88
- return `${major}.${minor}.${patch + 1}`;
89
- }
90
-
91
- function main() {
92
- const baseVersion = getLastTag();
93
- const commits = getCommitsSince(baseVersion).map(parseCommitLine);
94
-
95
- if (!commits.length) process.exit(0);
96
-
97
- let bump = null;
98
-
99
- for (const c of commits) {
100
- const t = detectType(c.subject, c.body);
101
- if (t === "major") {
102
- bump = "major";
103
- break;
104
- }
105
- if (t === "minor" && bump !== "major") bump = "minor";
106
- if (t === "patch" && !bump) bump = "patch";
107
- }
108
-
109
- if (!bump) {
110
- if (isPreview) {
111
- console.log("Mode: PREVIEW");
112
- console.log("Base version:", baseVersion);
113
- console.log("Commits analyzed:", commits.length);
114
- console.log("No version bump detected.");
115
- }
116
- process.exit(0);
117
- }
118
-
119
- const nextVersion = bumpVersion(bump, baseVersion);
120
-
121
- if (isPreview) {
122
- console.log("Mode: PREVIEW");
123
- console.log("Base version:", baseVersion);
124
- console.log("Commits analyzed:", commits.length);
125
- console.log("Highest bump detected:", bump);
126
- console.log("Next version:", nextVersion);
127
- } else {
128
- console.log(nextVersion);
129
- }
130
- }
131
-
132
- main();
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath } from "node:url";
3
+ import { getLastTag, getCommits, parseCommit } from "../lib/git.js";
4
+ import { readPackageVersion } from "../lib/utils.js";
5
+ import { bumpVersion, detectBumpType } from "../lib/versioning.js";
6
+
7
+ /* ===========================
8
+ * Core API (Programmatic)
9
+ * =========================== */
10
+
11
+ /**
12
+ * Analyze commits since the last tag (or since package.json version) and compute whether a release
13
+ * is needed and what the next semantic version should be.
14
+ *
15
+ * The function:
16
+ * - Determines a base version from the latest git tag (if any) or the package.json version.
17
+ * - Collects commits in the range (lastTag..HEAD or HEAD) and parses them.
18
+ * - Detects a bump type ('major' | 'minor' | 'patch') from the commits using conventional-commit style rules.
19
+ * - Returns either a release plan (nextVersion + bump) or a reason why no release is required.
20
+ *
21
+ * @param {Object} [options] - Options object.
22
+ * @param {string} [options.cwd=process.cwd()] - Working directory to run git/package lookups in.
23
+ *
24
+ * @returns {{
25
+ * hasRelease: boolean,
26
+ * // Present when hasRelease === false:
27
+ * reason?: 'no-commits' | 'no-bump-detected',
28
+ * // Always present:
29
+ * baseVersion: string,
30
+ * commitsAnalyzed: number,
31
+ * // Present when hasRelease === true:
32
+ * nextVersion?: string,
33
+ * bump?: 'major' | 'minor' | 'patch'
34
+ * }}
35
+ *
36
+ * Examples:
37
+ * // No commits since last tag
38
+ * // { hasRelease: false, reason: 'no-commits', baseVersion: '1.2.3', commitsAnalyzed: 0 }
39
+ *
40
+ * // Commits analyzed but none imply a version bump
41
+ * // { hasRelease: false, reason: 'no-bump-detected', baseVersion: '1.2.3', commitsAnalyzed: 5 }
42
+ *
43
+ * // Bump detected (e.g. minor)
44
+ * // { hasRelease: true, baseVersion: '1.2.3', nextVersion: '1.3.0', bump: 'minor', commitsAnalyzed: 4 }
45
+ *
46
+ * @throws {Error} If reading package version or git data fails (propagates errors from helper utilities).
47
+ */
48
+ export function computeVersion({ cwd = process.cwd() } = {}) {
49
+ const pkgVersion = readPackageVersion(cwd);
50
+ const lastTag = getLastTag(cwd);
51
+ const baseVersion = lastTag ?? pkgVersion;
52
+
53
+ const range = lastTag ? `${lastTag}..HEAD` : "HEAD";
54
+ const commits = getCommits(range, cwd).map(parseCommit);
55
+
56
+ if (commits.length === 0) {
57
+ return {
58
+ hasRelease: false,
59
+ reason: "no-commits",
60
+ baseVersion,
61
+ commitsAnalyzed: 0,
62
+ };
63
+ }
64
+
65
+ let bump = null;
66
+
67
+ for (const commit of commits) {
68
+ const type = detectBumpType(commit);
69
+
70
+ if (type === "major") {
71
+ bump = "major";
72
+ break;
73
+ }
74
+ if (type === "minor" && bump !== "major") bump = "minor";
75
+ if (type === "patch" && !bump) bump = "patch";
76
+ }
77
+
78
+ if (!bump) {
79
+ return {
80
+ hasRelease: false,
81
+ reason: "no-bump-detected",
82
+ baseVersion,
83
+ commitsAnalyzed: commits.length,
84
+ };
85
+ }
86
+
87
+ return {
88
+ hasRelease: true,
89
+ baseVersion,
90
+ nextVersion: bumpVersion(baseVersion, bump),
91
+ bump,
92
+ commitsAnalyzed: commits.length,
93
+ };
94
+ }
95
+
96
+ /* ===========================
97
+ * CLI
98
+ * =========================== */
99
+
100
+ /**
101
+ * Parses an array of command-line arguments and returns which known flags are present.
102
+ *
103
+ * Recognized flags:
104
+ * - "--ci"
105
+ * - "--json"
106
+ * - "--preview"
107
+ *
108
+ * @param {string[]} argv - Array of command-line arguments (e.g. process.argv.slice(2)).
109
+ * @returns {{ci: boolean, json: boolean, preview: boolean}} An object with boolean properties indicating presence of each flag.
110
+ */
111
+ function parseFlags(argv) {
112
+ return {
113
+ ci: argv.includes("--ci"),
114
+ json: argv.includes("--json"),
115
+ preview: argv.includes("--preview"),
116
+ };
117
+ }
118
+
119
+ /**
120
+ * Main CLI entrypoint that computes the next release version, prints the result,
121
+ * and exits the process according to a predetermined contract.
122
+ *
123
+ * Behavior:
124
+ * - Reads CLI flags via parseFlags(process.argv.slice(2)).
125
+ * - Calls computeVersion() to obtain an object describing the base version,
126
+ * whether a release should be generated, the next version, and a reason code.
127
+ * - If the parsed flags include `json`, writes the full computeVersion result as
128
+ * pretty-printed JSON to stdout.
129
+ * - Otherwise, if a release was generated (`result.hasRelease`), writes
130
+ * `result.nextVersion` to stdout.
131
+ * - If no release was generated, writes an explanatory error to stderr that
132
+ * includes `result.reason` and `result.baseVersion`.
133
+ *
134
+ * Exit codes (contract):
135
+ * 0 -> release generated
136
+ * 10 -> no bump detected
137
+ * 2 -> no commits
138
+ * 1 -> unexpected error
139
+ *
140
+ * Side effects:
141
+ * - Prints to stdout/stderr.
142
+ * - Terminates the Node.js process via process.exit(...) using the codes above.
143
+ *
144
+ * @function main
145
+ * @returns {void} This function does not return; it exits the process.
146
+ * @see parseFlags
147
+ * @see computeVersion
148
+ */
149
+ function main() {
150
+ const flags = parseFlags(process.argv.slice(2));
151
+ const result = computeVersion();
152
+
153
+ if (flags.json) {
154
+ console.log(JSON.stringify(result, null, 2));
155
+ } else if (result.hasRelease) {
156
+ console.log(result.nextVersion);
157
+ } else {
158
+ console.error(
159
+ `No release generated (${result.reason}). Base version: ${result.baseVersion}`
160
+ );
161
+ }
162
+
163
+ if (result.hasRelease) process.exit(0);
164
+ if (result.reason === "no-bump-detected") process.exit(10);
165
+ if (result.reason === "no-commits") process.exit(2);
166
+
167
+ process.exit(1);
168
+ }
169
+
170
+ const __filename = fileURLToPath(import.meta.url);
171
+ if (process.argv[1] === __filename) {
172
+ main();
173
+ }
package/bin/create-tag.js CHANGED
@@ -1,12 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import { execSync } from "node:child_process";
3
3
  import fs from "node:fs";
4
+ import { computeVersion } from "./compute-version.js";
4
5
 
5
- function run(cmd, silent = false) {
6
- return execSync(cmd, {
7
- stdio: silent ? "pipe" : "inherit",
8
- encoding: "utf8",
9
- }).trim();
6
+ function run(cmd, captureOutput = true) {
7
+ if (captureOutput) {
8
+ const output = execSync(cmd, { encoding: "utf8", stdio: "pipe" });
9
+ return output ? output.trim() : "";
10
+ } else {
11
+ execSync(cmd, { stdio: "inherit" });
12
+ }
13
+ return "";
10
14
  }
11
15
 
12
16
  const args = process.argv.slice(2);
@@ -18,7 +22,8 @@ let version;
18
22
  if (USE_COMPUTED) {
19
23
  console.log("๐Ÿ”ข Computing version dynamically...");
20
24
  try {
21
- version = run("node bin/compute-version.js", true);
25
+ const obj = computeVersion({ cwd: process.cwd() });
26
+ version = obj.nextVersion;
22
27
  } catch {
23
28
  console.error("โŒ Failed to compute version.");
24
29
  process.exit(1);
@@ -26,7 +31,7 @@ if (USE_COMPUTED) {
26
31
 
27
32
  if (!version) {
28
33
  console.log("โ„น No version bump detected. Skipping tag creation.");
29
- process.exit(0);
34
+ process.exit(10);
30
35
  }
31
36
  } else {
32
37
  console.log("๐Ÿ“ฆ Using version from package.json...");
@@ -40,7 +45,6 @@ if (USE_COMPUTED) {
40
45
  }
41
46
 
42
47
  const tag = version;
43
-
44
48
  console.log(`๐Ÿ”– Release version: ${tag}`);
45
49
 
46
50
  // check if tag exists
@@ -52,14 +56,22 @@ try {
52
56
  // OK
53
57
  }
54
58
 
59
+ const tagMessage = `Release ${tag}`;
60
+
55
61
  if (DRY_RUN) {
56
62
  console.log("๐Ÿงช Dry-run mode enabled.");
57
- console.log(`Would create and push tag: ${tag}`);
63
+ console.log(`Would create annotated tag: ${tag}`);
64
+ console.log(`Message: "${tagMessage}"`);
58
65
  console.log(`VERSION=${tag}`);
59
- process.exit(0);
66
+ process.exit(5);
60
67
  }
61
68
 
62
- run(`git tag ${tag}`);
63
- run(`git push origin ${tag}`);
64
-
65
- console.log(`โœ” Tag ${tag} created and pushed`);
69
+ try {
70
+ run(`git tag -a ${tag} -m "${tagMessage}"`, false);
71
+ run(`git push origin ${tag}`, false);
72
+ console.log(JSON.stringify({ tag, tagMessage }, null, 2));
73
+ process.exit(0);
74
+ } catch (err) {
75
+ console.error("โŒ Failed to create or push tag.", err);
76
+ process.exit(1);
77
+ }