|
| 1 | +--- |
| 2 | +name: artifacts-builder |
| 3 | +description: Automates the release process for the ts-oauth2-server project. |
| 4 | +license: Complete terms in LICENSE |
| 5 | +--- |
| 6 | + |
| 7 | +# Release Creator Skill |
| 8 | + |
| 9 | +## Purpose |
| 10 | +Automates the release process for the ts-oauth2-server project by: |
| 11 | +1. Bumping version numbers in both `package.json` and `jsr.json` |
| 12 | +2. Generating changelog entries based on git commits since the last tagged version |
| 13 | +3. Updating the CHANGELOG.md file with proper formatting |
| 14 | +4. Maintaining Keep a Changelog format and Semantic Versioning compliance |
| 15 | + |
| 16 | +## When to Use This Skill |
| 17 | +Use this skill when the user requests: |
| 18 | +- Creating a new release |
| 19 | +- Bumping the version (patch, minor, major, premajor, preminor, prepatch, prerelease) |
| 20 | +- Updating the changelog |
| 21 | +- Preparing for a release |
| 22 | +- Publishing a new version |
| 23 | + |
| 24 | +## Prerequisites |
| 25 | +- Git repository with commit history |
| 26 | +- Existing tags for version tracking |
| 27 | +- `package.json` and `jsr.json` files present |
| 28 | +- `CHANGELOG.md` file exists |
| 29 | + |
| 30 | +## Core Functionality |
| 31 | + |
| 32 | +### Version Bump Types |
| 33 | +Supported version bump types (following semver): |
| 34 | +- **patch**: Bug fixes (1.0.0 → 1.0.1) |
| 35 | +- **minor**: New features (1.0.0 → 1.1.0) |
| 36 | +- **major**: Breaking changes (1.0.0 → 2.0.0) |
| 37 | +- **prepatch**: Pre-release patch (1.0.0 → 1.0.1-0) |
| 38 | +- **preminor**: Pre-release minor (1.0.0 → 1.1.0-0) |
| 39 | +- **premajor**: Pre-release major (1.0.0 → 2.0.0-0) |
| 40 | +- **prerelease**: Increment pre-release (1.0.0-0 → 1.0.0-1) |
| 41 | + |
| 42 | +### Implementation Steps |
| 43 | + |
| 44 | +#### 1. Get Current Version |
| 45 | +```typescript |
| 46 | +import { readFileSync } from 'fs'; |
| 47 | + |
| 48 | +function getCurrentVersion(): string { |
| 49 | + const pkg = JSON.parse(readFileSync('package.json', 'utf-8')); |
| 50 | + return pkg.version; |
| 51 | +} |
| 52 | +``` |
| 53 | + |
| 54 | +#### 2. Calculate New Version |
| 55 | +```typescript |
| 56 | +function bumpVersion(current: string, bumpType: string): string { |
| 57 | + const parts = current.split('-'); |
| 58 | + const [major, minor, patch] = parts[0].split('.').map(Number); |
| 59 | + const prerelease = parts[1]; |
| 60 | + |
| 61 | + switch (bumpType) { |
| 62 | + case 'major': |
| 63 | + return `${major + 1}.0.0`; |
| 64 | + case 'minor': |
| 65 | + return `${major}.${minor + 1}.0`; |
| 66 | + case 'patch': |
| 67 | + return `${major}.${minor}.${patch + 1}`; |
| 68 | + case 'premajor': |
| 69 | + return `${major + 1}.0.0-0`; |
| 70 | + case 'preminor': |
| 71 | + return `${major}.${minor + 1}.0-0`; |
| 72 | + case 'prepatch': |
| 73 | + return `${major}.${minor}.${patch + 1}-0`; |
| 74 | + case 'prerelease': |
| 75 | + if (prerelease) { |
| 76 | + const num = parseInt(prerelease) || 0; |
| 77 | + return `${parts[0]}-${num + 1}`; |
| 78 | + } |
| 79 | + return `${major}.${minor}.${patch + 1}-0`; |
| 80 | + default: |
| 81 | + throw new Error(`Invalid bump type: ${bumpType}`); |
| 82 | + } |
| 83 | +} |
| 84 | +``` |
| 85 | + |
| 86 | +#### 3. Get Git Commits Since Last Tag |
| 87 | +```bash |
| 88 | +# Get the last tag |
| 89 | +last_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "") |
| 90 | + |
| 91 | +# Get commits since last tag |
| 92 | +if [ -z "$last_tag" ]; then |
| 93 | + # No previous tag, get all commits |
| 94 | + commits=$(git log --pretty=format:"%s" --no-merges) |
| 95 | +else |
| 96 | + # Get commits since last tag |
| 97 | + commits=$(git log ${last_tag}..HEAD --pretty=format:"%s" --no-merges) |
| 98 | +fi |
| 99 | +``` |
| 100 | + |
| 101 | +#### 4. Categorize Commits |
| 102 | +Parse commits and categorize them based on conventional commit format: |
| 103 | +- `feat:` or `feature:` → Added section |
| 104 | +- `fix:` → Fixed section |
| 105 | +- `BREAKING CHANGE:` or `!:` → Changed section (breaking) |
| 106 | +- `docs:` → Skip (or note in documentation) |
| 107 | +- `chore:`, `refactor:`, `test:` → Skip or group under "Changed" |
| 108 | +- `security:` → Security section |
| 109 | + |
| 110 | +#### 5. Generate Changelog Entry |
| 111 | +Follow Keep a Changelog format: |
| 112 | +```markdown |
| 113 | +## [Version] - YYYY-MM-DD |
| 114 | + |
| 115 | +### Added |
| 116 | +- New feature A |
| 117 | +- New feature B |
| 118 | + |
| 119 | +### Changed |
| 120 | +- **BREAKING**: Changed behavior X |
| 121 | +- Updated Y |
| 122 | + |
| 123 | +### Fixed |
| 124 | +- Fixed bug in Z |
| 125 | +- Resolved issue with W |
| 126 | + |
| 127 | +### Security |
| 128 | +- Security fix for vulnerability V |
| 129 | +``` |
| 130 | + |
| 131 | +#### 6. Update Files |
| 132 | +Update both `package.json` and `jsr.json` with the new version, and prepend the new changelog entry to CHANGELOG.md under the `## [Unreleased]` section. |
| 133 | + |
| 134 | +## Usage Examples |
| 135 | + |
| 136 | +### Example 1: Patch Release |
| 137 | +```typescript |
| 138 | +// User request: "Create a patch release" |
| 139 | +const currentVersion = "4.1.1"; |
| 140 | +const newVersion = "4.1.2"; |
| 141 | + |
| 142 | +// 1. Get commits since v4.1.1 |
| 143 | +// 2. Categorize commits |
| 144 | +// 3. Generate changelog entry |
| 145 | +// 4. Update package.json, jsr.json, CHANGELOG.md |
| 146 | +``` |
| 147 | + |
| 148 | +### Example 2: Minor Release with Features |
| 149 | +```typescript |
| 150 | +// User request: "Bump minor version and update changelog" |
| 151 | +const currentVersion = "4.1.1"; |
| 152 | +const newVersion = "4.2.0"; |
| 153 | + |
| 154 | +// Changelog generated from commits: |
| 155 | +// - feat: Add new grant type support |
| 156 | +// - feat: Enhance token validation |
| 157 | +// - fix: Resolve refresh token issue |
| 158 | +``` |
| 159 | + |
| 160 | +### Example 3: Major Release with Breaking Changes |
| 161 | +```typescript |
| 162 | +// User request: "Create major release for breaking changes" |
| 163 | +const currentVersion = "4.1.1"; |
| 164 | +const newVersion = "5.0.0"; |
| 165 | + |
| 166 | +// Identify BREAKING CHANGE commits |
| 167 | +// Place them under ### Changed section |
| 168 | +``` |
| 169 | + |
| 170 | +## Implementation Script |
| 171 | + |
| 172 | +Here's a complete implementation approach: |
| 173 | + |
| 174 | +```typescript |
| 175 | +interface ChangelogEntry { |
| 176 | + added: string[]; |
| 177 | + changed: string[]; |
| 178 | + deprecated: string[]; |
| 179 | + removed: string[]; |
| 180 | + fixed: string[]; |
| 181 | + security: string[]; |
| 182 | +} |
| 183 | + |
| 184 | +async function createRelease(bumpType: string): Promise<void> { |
| 185 | + // 1. Read current version |
| 186 | + const currentVersion = getCurrentVersion(); |
| 187 | + console.log(`Current version: ${currentVersion}`); |
| 188 | + |
| 189 | + // 2. Calculate new version |
| 190 | + const newVersion = bumpVersion(currentVersion, bumpType); |
| 191 | + console.log(`New version: ${newVersion}`); |
| 192 | + |
| 193 | + // 3. Get last tag and commits |
| 194 | + const lastTag = await getLastTag(); |
| 195 | + const commits = await getCommitsSinceTag(lastTag); |
| 196 | + console.log(`Found ${commits.length} commits since ${lastTag || 'beginning'}`); |
| 197 | + |
| 198 | + // 4. Parse and categorize commits |
| 199 | + const changelog = categorizeCommits(commits); |
| 200 | + |
| 201 | + // 5. Generate changelog entry |
| 202 | + const changelogEntry = generateChangelogEntry(newVersion, changelog); |
| 203 | + console.log('Generated changelog entry'); |
| 204 | + |
| 205 | + // 6. Update files |
| 206 | + await updatePackageJson(newVersion); |
| 207 | + await updateJsrJson(newVersion); |
| 208 | + await updateChangelog(changelogEntry); |
| 209 | + |
| 210 | + console.log(`✓ Successfully prepared release ${newVersion}`); |
| 211 | + console.log('Next steps:'); |
| 212 | + console.log(' 1. Review the changes'); |
| 213 | + console.log(' 2. Commit: git add -A && git commit -m "chore: release v' + newVersion + '"'); |
| 214 | + console.log(' 3. Tag: git tag v' + newVersion); |
| 215 | + console.log(' 4. Push: git push && git push --tags'); |
| 216 | +} |
| 217 | + |
| 218 | +function categorizeCommits(commits: string[]): ChangelogEntry { |
| 219 | + const entry: ChangelogEntry = { |
| 220 | + added: [], |
| 221 | + changed: [], |
| 222 | + deprecated: [], |
| 223 | + removed: [], |
| 224 | + fixed: [], |
| 225 | + security: [], |
| 226 | + }; |
| 227 | + |
| 228 | + for (const commit of commits) { |
| 229 | + const lower = commit.toLowerCase(); |
| 230 | + |
| 231 | + if (lower.includes('breaking change') || lower.includes('!:')) { |
| 232 | + entry.changed.push(commit.replace(/^[^:]+:\s*/, '')); |
| 233 | + } else if (lower.startsWith('feat:') || lower.startsWith('feature:')) { |
| 234 | + entry.added.push(commit.replace(/^[^:]+:\s*/, '')); |
| 235 | + } else if (lower.startsWith('fix:')) { |
| 236 | + entry.fixed.push(commit.replace(/^[^:]+:\s*/, '')); |
| 237 | + } else if (lower.startsWith('security:')) { |
| 238 | + entry.security.push(commit.replace(/^[^:]+:\s*/, '')); |
| 239 | + } else if (lower.startsWith('deprecate:')) { |
| 240 | + entry.deprecated.push(commit.replace(/^[^:]+:\s*/, '')); |
| 241 | + } else if (lower.startsWith('remove:')) { |
| 242 | + entry.removed.push(commit.replace(/^[^:]+:\s*/, '')); |
| 243 | + } |
| 244 | + // Skip chore, docs, test, refactor, style, etc. |
| 245 | + } |
| 246 | + |
| 247 | + return entry; |
| 248 | +} |
| 249 | + |
| 250 | +function generateChangelogEntry(version: string, changelog: ChangelogEntry): string { |
| 251 | + const date = new Date().toISOString().split('T')[0]; |
| 252 | + let entry = `## [${version}] - ${date}\n\n`; |
| 253 | + |
| 254 | + if (changelog.added.length > 0) { |
| 255 | + entry += '### Added\n'; |
| 256 | + for (const item of changelog.added) { |
| 257 | + entry += `- ${item}\n`; |
| 258 | + } |
| 259 | + entry += '\n'; |
| 260 | + } |
| 261 | + |
| 262 | + if (changelog.changed.length > 0) { |
| 263 | + entry += '### Changed\n'; |
| 264 | + for (const item of changelog.changed) { |
| 265 | + entry += `- ${item}\n`; |
| 266 | + } |
| 267 | + entry += '\n'; |
| 268 | + } |
| 269 | + |
| 270 | + if (changelog.deprecated.length > 0) { |
| 271 | + entry += '### Deprecated\n'; |
| 272 | + for (const item of changelog.deprecated) { |
| 273 | + entry += `- ${item}\n`; |
| 274 | + } |
| 275 | + entry += '\n'; |
| 276 | + } |
| 277 | + |
| 278 | + if (changelog.removed.length > 0) { |
| 279 | + entry += '### Removed\n'; |
| 280 | + for (const item of changelog.removed) { |
| 281 | + entry += `- ${item}\n`; |
| 282 | + } |
| 283 | + entry += '\n'; |
| 284 | + } |
| 285 | + |
| 286 | + if (changelog.fixed.length > 0) { |
| 287 | + entry += '### Fixed\n'; |
| 288 | + for (const item of changelog.fixed) { |
| 289 | + entry += `- ${item}\n`; |
| 290 | + } |
| 291 | + entry += '\n'; |
| 292 | + } |
| 293 | + |
| 294 | + if (changelog.security.length > 0) { |
| 295 | + entry += '### Security\n'; |
| 296 | + for (const item of changelog.security) { |
| 297 | + entry += `- ${item}\n`; |
| 298 | + } |
| 299 | + entry += '\n'; |
| 300 | + } |
| 301 | + |
| 302 | + return entry; |
| 303 | +} |
| 304 | +``` |
| 305 | + |
| 306 | +## Best Practices |
| 307 | + |
| 308 | +1. **Always verify versions match**: Ensure package.json and jsr.json versions are in sync before and after bumping |
| 309 | +2. **Review commits carefully**: Not all commits should appear in changelog |
| 310 | +3. **Use conventional commits**: Encourage the team to use conventional commit format |
| 311 | +4. **Breaking changes**: Always highlight breaking changes prominently |
| 312 | +5. **Date format**: Use ISO date format (YYYY-MM-DD) for consistency |
| 313 | +6. **Manual review**: Always review generated changelog before committing |
| 314 | + |
| 315 | +## Error Handling |
| 316 | + |
| 317 | +- **No git repository**: Warn user and exit gracefully |
| 318 | +- **Uncommitted changes**: Warn user to commit or stash changes first |
| 319 | +- **Invalid bump type**: Show available options and exit |
| 320 | +- **Version mismatch**: If package.json and jsr.json versions don't match, warn user |
| 321 | +- **No commits**: If no commits since last tag, warn user and ask if they want to proceed |
| 322 | + |
| 323 | +## Output Format |
| 324 | + |
| 325 | +The skill should provide clear feedback: |
| 326 | +``` |
| 327 | +📦 Current version: 4.1.1 |
| 328 | +🔼 Bumping version: patch |
| 329 | +📝 New version: 4.1.2 |
| 330 | +🔍 Found 5 commits since v4.1.1 |
| 331 | +📋 Categorized commits: |
| 332 | + - Added: 2 |
| 333 | + - Fixed: 3 |
| 334 | +✍️ Generated changelog entry |
| 335 | +✅ Updated package.json |
| 336 | +✅ Updated jsr.json |
| 337 | +✅ Updated CHANGELOG.md |
| 338 | +
|
| 339 | +✓ Successfully prepared release v4.1.2 |
| 340 | +
|
| 341 | +Next steps: |
| 342 | + 1. Review the changes |
| 343 | + 2. Commit: git add -A && git commit -m "chore: release v4.1.2" |
| 344 | + 3. Tag: git tag v4.1.2 |
| 345 | + 4. Push: git push && git push --tags |
| 346 | +``` |
| 347 | + |
| 348 | +## Integration with Project |
| 349 | + |
| 350 | +This skill respects the project's: |
| 351 | +- Semantic versioning (as stated in CHANGELOG.md) |
| 352 | +- Keep a Changelog format (as shown in existing CHANGELOG.md) |
| 353 | +- Git workflow and tagging conventions |
| 354 | +- Dual package manager support (npm and JSR) |
| 355 | + |
| 356 | +## Notes |
| 357 | + |
| 358 | +- The skill does NOT automatically commit, tag, or push changes |
| 359 | +- It prepares all files for review before manual commit |
| 360 | +- This allows the developer to review and adjust if needed |
| 361 | +- The skill is idempotent - can be run multiple times safely |
| 362 | +- ALWAYS Ensure package.json and jsr.json versions are in sync before and after bumping |
0 commit comments