release-suite 1.0.0 → 1.0.1

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.
@@ -82,6 +82,10 @@ jobs:
82
82
  echo "exists=false" >> "$GITHUB_OUTPUT"
83
83
  fi
84
84
 
85
+ - name: Stage dist if exists
86
+ if: steps.dist.outputs.exists == 'true'
87
+ run: git add dist
88
+
85
89
  # Automatically create branches, commits, and PRs with peter-evans
86
90
  - name: Create Release PR
87
91
  if: steps.compute.outputs.status == '0' && steps.compute.outputs.version != ''
@@ -104,4 +108,3 @@ jobs:
104
108
  add-paths: |
105
109
  package.json
106
110
  CHANGELOG.md
107
- ${{ steps.dist.outputs.exists == 'true' && 'dist/**' || '' }}
@@ -1,8 +1,7 @@
1
1
  name: Publish Release
2
2
 
3
3
  on:
4
- pull_request:
5
- types: [closed]
4
+ push:
6
5
  branches:
7
6
  - main
8
7
 
@@ -19,15 +18,9 @@ concurrency:
19
18
  jobs:
20
19
  publish:
21
20
  # Only runs when:
22
- # - The PR has been merged
23
- # - The PR has the "release" label
24
- # - The PR title starts with ":bricks: chore(release):"
25
- # - The PR was created by a bot
21
+ # - The commit message starts with ":bricks: chore(release):"
26
22
  if: >
27
- github.event.pull_request.merged == true &&
28
- startsWith(github.event.pull_request.title, ':bricks: chore(release):') &&
29
- contains(join(github.event.pull_request.labels.*.name, ','), 'release') &&
30
- github.event.pull_request.user.type == 'Bot'
23
+ startsWith(github.event.head_commit.message, ':bricks: chore(release):')
31
24
  runs-on: ubuntu-latest
32
25
  steps:
33
26
  - name: Checkout repository
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## 1.0.1
2
+
3
+ ### 🐛 Fixes
4
+
5
+ - Harden changelog generation for squash commits (#13)
6
+
7
+ ### 📚 Docs
8
+
9
+ - Update ci/cd examples with trigger adjustment
10
+
11
+ ### 🔁 CI
12
+
13
+ - Adjust trigger in workflow
14
+
1
15
  ## 1.0.0
2
16
 
3
17
  ### 💥 Breaking Changes
@@ -3,6 +3,9 @@ import { execSync } from "node:child_process";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { COMMIT_RE, COMMIT_EMOJI_RE } from "../lib/constants.js";
7
+ import { normalizeCommits, parseCommit } from "../lib/git.js";
8
+ import { normalizeSubject } from "../lib/versioning.js";
6
9
  import { computeVersion } from "./compute-version.js";
7
10
 
8
11
  function run(cmd, cwd = process.cwd()) {
@@ -37,26 +40,22 @@ function getAllTags(cwd = process.cwd()) {
37
40
 
38
41
  function getCommitsBetween(from, to, cwd = process.cwd()) {
39
42
  const range = from ? `${from}..${to}` : to;
43
+
40
44
  try {
41
- return run(`git log ${range} --pretty=format:%H%x1f%s%x1f%b`, cwd)
42
- .split("\n")
45
+ return run(
46
+ `git log ${range} --pretty=format:%H%x1f%s%x1f%B%x1e`,
47
+ cwd
48
+ )
49
+ .split("\x1e")
50
+ .map(c => c.trim())
43
51
  .filter(Boolean);
44
52
  } catch {
45
53
  return [];
46
54
  }
47
55
  }
48
56
 
49
- function parseCommit(line) {
50
- const [hash, subject = "", body = ""] = line.split("\x1f");
51
- return { hash, subject: subject.trim(), body: body.trim() };
52
- }
53
-
54
57
  function cleanSubject(subject) {
55
- let s = subject.replace(/^(:\S+: )?/, "");
56
- s = s.replace(
57
- /^(feat|fix|refactor|docs|chore|style|test|build|perf|ci|raw|cleanup|remove)(\(.+\))?(!)?:\s*/i,
58
- ""
59
- );
58
+ const s = normalizeSubject(subject).replace(COMMIT_RE, "");
60
59
  return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
61
60
  }
62
61
 
@@ -78,9 +77,6 @@ function categorize(commits) {
78
77
  remove: [],
79
78
  };
80
79
 
81
- const reType =
82
- /^(:\S+: )?(feat|fix|refactor|docs|chore|style|test|build|perf|ci|raw|cleanup|remove)(\(.+\))?(!)?:/i;
83
-
84
80
  for (const c of commits) {
85
81
  const { subject, body } = c;
86
82
 
@@ -89,7 +85,7 @@ function categorize(commits) {
89
85
  continue;
90
86
  }
91
87
 
92
- const match = subject.match(reType);
88
+ const match = subject.match(COMMIT_EMOJI_RE);
93
89
  const desc = cleanSubject(subject);
94
90
 
95
91
  if (!match) {
@@ -98,7 +94,14 @@ function categorize(commits) {
98
94
  }
99
95
 
100
96
  const type = match[2].toLowerCase();
101
- (buckets[type] || buckets.chore).push({ desc, hash: c.hash });
97
+ const finalDesc = desc || cleanSubject(subject) || subject;
98
+
99
+ if (finalDesc && finalDesc.trim()) {
100
+ (buckets[type] || buckets.chore).push({
101
+ desc: finalDesc.trim(),
102
+ hash: c.hash
103
+ });
104
+ }
102
105
  }
103
106
 
104
107
  return buckets;
@@ -128,12 +131,18 @@ function buildSection(version, buckets) {
128
131
  let hasContent = false;
129
132
 
130
133
  for (const [key, title] of sections) {
131
- if (buckets[key].length) {
132
- hasContent = true;
133
- out.push(`${title}\n`);
134
- for (const c of buckets[key]) out.push(`- ${c.desc}`);
135
- out.push("");
134
+ const items = buckets[key]
135
+ .map(c => c.desc && c.desc.trim())
136
+ .filter(Boolean);
137
+
138
+ if (!items.length) continue;
139
+
140
+ hasContent = true;
141
+ out.push(`${title}\n`);
142
+ for (const desc of items) {
143
+ out.push(`- ${desc}`);
136
144
  }
145
+ out.push("");
137
146
  }
138
147
 
139
148
  if (!hasContent) out.push("_No changes._\n");
@@ -165,7 +174,10 @@ export function generateChangelog({ isPreview = process.env.PREVIEW_MODE === "tr
165
174
 
166
175
  // Always generate the upcoming version (preview & release)
167
176
  if (!changelogHasVersion(CHANGELOG_FILE, nextVersion, cwd)) {
168
- const commits = getCommitsBetween(lastTag, "HEAD", cwd).map(parseCommit);
177
+ const commits = normalizeCommits(
178
+ getCommitsBetween(lastTag, "HEAD", cwd).map(parseCommit)
179
+ );
180
+
169
181
 
170
182
  if (commits.length) {
171
183
  const buckets = categorize(commits);
@@ -180,7 +192,11 @@ export function generateChangelog({ isPreview = process.env.PREVIEW_MODE === "tr
180
192
 
181
193
  if (changelogHasVersion(CHANGELOG_FILE, tag, cwd)) continue;
182
194
 
183
- const commits = getCommitsBetween(previous, tag, cwd).map(parseCommit);
195
+ const commits = normalizeCommits(
196
+ getCommitsBetween(previous, tag, cwd)
197
+ .map(parseCommit)
198
+ );
199
+
184
200
  if (!commits.length) continue;
185
201
 
186
202
  const buckets = categorize(commits);
package/docs/ci.md CHANGED
@@ -87,6 +87,10 @@ jobs:
87
87
  echo "exists=false" >> "$GITHUB_OUTPUT"
88
88
  fi
89
89
 
90
+ - name: Stage dist if exists
91
+ if: steps.dist.outputs.exists == 'true'
92
+ run: git add dist
93
+
90
94
  # Automatically create branches, commits, and PRs with peter-evans
91
95
  - name: Create Release PR
92
96
  if: steps.compute.outputs.status == '0' && steps.compute.outputs.version != ''
@@ -109,7 +113,6 @@ jobs:
109
113
  add-paths: |
110
114
  package.json
111
115
  CHANGELOG.md
112
- ${{ steps.dist.outputs.exists == 'true' && 'dist/**' || '' }}
113
116
 
114
117
  ```
115
118
 
@@ -119,8 +122,7 @@ jobs:
119
122
  name: Publish Release
120
123
 
121
124
  on:
122
- pull_request:
123
- types: [closed]
125
+ push:
124
126
  branches:
125
127
  - main
126
128
 
@@ -137,15 +139,9 @@ concurrency:
137
139
  jobs:
138
140
  publish:
139
141
  # Only runs when:
140
- # - The PR has been merged
141
- # - The PR has the "release" label
142
- # - The PR title starts with ":bricks: chore(release):"
143
- # - The PR was created by a bot
142
+ # - The commit message starts with ":bricks: chore(release):"
144
143
  if: >
145
- github.event.pull_request.merged == true &&
146
- startsWith(github.event.pull_request.title, ':bricks: chore(release):') &&
147
- contains(join(github.event.pull_request.labels.*.name, ','), 'release') &&
148
- github.event.pull_request.user.type == 'Bot'
144
+ startsWith(github.event.head_commit.message, ':bricks: chore(release):')
149
145
  runs-on: ubuntu-latest
150
146
  steps:
151
147
  - name: Checkout repository
@@ -0,0 +1,28 @@
1
+ // Constants used across the release-suite package
2
+
3
+ // Commit types recognized in Conventional Commits
4
+ export const COMMIT_TYPES = [
5
+ 'feat',
6
+ 'fix',
7
+ 'refactor',
8
+ 'docs',
9
+ 'chore',
10
+ 'style',
11
+ 'test',
12
+ 'build',
13
+ 'perf',
14
+ 'ci',
15
+ 'raw',
16
+ 'cleanup',
17
+ 'remove',
18
+ ].join('|');
19
+
20
+ export const COMMIT_RE = new RegExp(
21
+ `^(${COMMIT_TYPES})(\\(.+\\))?(!)?:\\s*`,
22
+ 'i'
23
+ );
24
+
25
+ export const COMMIT_EMOJI_RE = new RegExp(
26
+ `^(:\\S+: )?(${COMMIT_TYPES})(\\(.+\\))?(!)?:`,
27
+ 'i'
28
+ );
package/lib/git.js CHANGED
@@ -1,4 +1,6 @@
1
+ import { COMMIT_RE } from "./constants.js";
1
2
  import { run } from "./utils.js";
3
+ import { normalizeSubject } from "./versioning.js";
2
4
 
3
5
  /* ===========================
4
6
  * Git helpers
@@ -69,5 +71,59 @@ export function getCommits(range, cwd) {
69
71
  */
70
72
  export function parseCommit(line) {
71
73
  const [hash, subject = "", body = ""] = line.split("\x1f");
72
- return { hash, subject, body };
73
- }
74
+ return {
75
+ hash: hash.trim(),
76
+ subject: subject.trim(),
77
+ body: body.trim()
78
+ };
79
+ }
80
+
81
+ function isSquashCommit(commit) {
82
+ return /\n\s*[*-]\s+/.test(commit.body);
83
+ }
84
+
85
+ function extractCommitsFromSquash(commit) {
86
+ const lines = commit.body
87
+ .split("\n")
88
+ .map(l => l.trim())
89
+ .filter(Boolean);
90
+
91
+ const firstBulletIndex = lines.findIndex(
92
+ l => l.startsWith("* ") || l.startsWith("- ")
93
+ );
94
+
95
+ if (firstBulletIndex === -1) {
96
+ return [];
97
+ }
98
+
99
+ return lines
100
+ .slice(firstBulletIndex)
101
+ .filter(l => l.startsWith("* ") || l.startsWith("- "))
102
+ .map(l => normalizeSubject(l))
103
+ .filter(subject => COMMIT_RE.test(subject))
104
+ .map(subject => ({
105
+ hash: commit.hash,
106
+ subject,
107
+ body: ""
108
+ }));
109
+
110
+ }
111
+
112
+ export function normalizeCommits(commits) {
113
+ const normalized = [];
114
+
115
+ for (const commit of commits) {
116
+ if (isSquashCommit(commit)) {
117
+ const extracted = extractCommitsFromSquash(commit);
118
+ if (extracted.length) {
119
+ normalized.push(...extracted);
120
+ continue;
121
+ }
122
+ }
123
+
124
+ // fallback: normal commit
125
+ normalized.push(commit);
126
+ }
127
+
128
+ return normalized;
129
+ }
package/lib/versioning.js CHANGED
@@ -1,10 +1,9 @@
1
+ import { COMMIT_RE } from "./constants.js";
2
+
1
3
  /* ===========================
2
4
  * Semver detection
3
5
  * =========================== */
4
6
 
5
- const COMMIT_RE =
6
- /^(feat|fix|refactor|docs|chore|style|test|build|perf|ci|cleanup|remove)(\(.+\))?(!)?:/i;
7
-
8
7
  /**
9
8
  * Normalize a subject string by removing leading emoji and trimming whitespace.
10
9
  *
@@ -25,12 +24,12 @@ const COMMIT_RE =
25
24
  * @example
26
25
  * normalizeSubject(' :a::b:Multiple emojis at start ') // 'Multiple emojis at start'
27
26
  */
28
- function normalizeSubject(subject) {
27
+ export function normalizeSubject(subject = "") {
29
28
  return subject
30
- // remove emoji at start
31
- .replace(/^:\S+:\s*/, "")
32
- // remove unicode emoji at start
33
- .replace(/^[\u{1F300}-\u{1FAFF}]+\s*/u, "")
29
+ .replace(/^[-*]\s*/, "") // leading list markers
30
+ .replace(/^:\w+:\s*/, "") // :emoji:
31
+ .replace(/^[^\w]+/, "") // leading symbols
32
+ .replace(/^(?:[\u{1F300}-\u{1FAFF}]+\s*)+/u, "") // leading Unicode emoji
34
33
  .trim();
35
34
  }
36
35
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "release-suite",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Semantic versioning tools for Git-based projects, providing automated version computation, changelog generation and release notes creation.",
5
5
  "type": "module",
6
6
  "publishConfig": {