zenith-language 0.3.0 → 0.4.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.
@@ -0,0 +1,254 @@
1
+ # =============================================================================
2
+ # Zenith Automated Release Workflow
3
+ # =============================================================================
4
+ # This workflow handles automated releases for all Zenith repositories.
5
+ #
6
+ # TRIGGERS:
7
+ # - Push to 'main' branch (analyzes commits for version bump)
8
+ # - Manual trigger via workflow_dispatch (with optional dry-run mode)
9
+ # - Tag creation (v*) for explicit version releases
10
+ #
11
+ # FEATURES:
12
+ # - Conventional Commits parsing for automatic version determination
13
+ # - Automatic CHANGELOG.md generation
14
+ # - GitHub Release creation
15
+ # - Optional NPM publishing
16
+ # - Commits updated files back to repo
17
+ # - Dry-run mode for testing
18
+ # - Monorepo support (detects changed packages)
19
+ #
20
+ # REQUIRED SECRETS:
21
+ # - NPM_TOKEN: For publishing to NPM (if enabled)
22
+ # - GITHUB_TOKEN: Automatically provided by GitHub Actions
23
+ # =============================================================================
24
+
25
+ name: Release
26
+
27
+ on:
28
+ push:
29
+ branches:
30
+ - main
31
+ tags:
32
+ - 'v*'
33
+ paths-ignore:
34
+ - '**.md'
35
+ - '.github/**'
36
+ - '!.github/workflows/release.yml'
37
+
38
+ workflow_dispatch:
39
+ inputs:
40
+ dry_run:
41
+ description: 'Dry run mode (no actual release)'
42
+ required: false
43
+ default: false
44
+ type: boolean
45
+ package:
46
+ description: 'Specific package to release (for monorepo, leave empty for auto-detect)'
47
+ required: false
48
+ default: ''
49
+ type: string
50
+ bump_type:
51
+ description: 'Force version bump type (leave empty for auto-detect from commits)'
52
+ required: false
53
+ default: ''
54
+ type: choice
55
+ options:
56
+ - ''
57
+ - patch
58
+ - minor
59
+ - major
60
+ publish_npm:
61
+ description: 'Publish to NPM'
62
+ required: false
63
+ default: true
64
+ type: boolean
65
+
66
+ # Prevent concurrent releases
67
+ concurrency:
68
+ group: release-${{ github.ref }}
69
+ cancel-in-progress: false
70
+
71
+ env:
72
+ BUN_VERSION: '1.1.38'
73
+
74
+ jobs:
75
+ # ==========================================================================
76
+ # Detect Changes (for monorepo support)
77
+ # ==========================================================================
78
+ detect-changes:
79
+ name: Detect Changed Packages
80
+ runs-on: ubuntu-latest
81
+ outputs:
82
+ packages: ${{ steps.detect.outputs.packages }}
83
+ has_changes: ${{ steps.detect.outputs.has_changes }}
84
+ steps:
85
+ - name: Checkout Repository
86
+ uses: actions/checkout@v4
87
+ with:
88
+ fetch-depth: 0
89
+ token: ${{ secrets.GITHUB_TOKEN }}
90
+
91
+ - name: Setup Bun
92
+ uses: oven-sh/setup-bun@v2
93
+ with:
94
+ bun-version: ${{ env.BUN_VERSION }}
95
+
96
+ - name: Detect Changed Packages
97
+ id: detect
98
+ run: |
99
+ # First, check if this is a single-package repo (package.json in root)
100
+ if [ -f "./package.json" ]; then
101
+ # Count subdirectories with package.json (excluding node_modules)
102
+ SUB_PACKAGES=$(find . -mindepth 2 -name "package.json" -not -path "*/node_modules/*" | wc -l)
103
+
104
+ if [ "$SUB_PACKAGES" -eq 0 ]; then
105
+ # Single package repo - always release from root
106
+ echo "Single package repository detected"
107
+ echo "packages=[\".\"]" >> $GITHUB_OUTPUT
108
+ echo "has_changes=true" >> $GITHUB_OUTPUT
109
+ exit 0
110
+ fi
111
+ fi
112
+
113
+ # Monorepo detection
114
+ PACKAGES=$(find . -name "package.json" -not -path "*/node_modules/*" -not -path "*/.git/*" | xargs -I {} dirname {} | sed 's|^\./||' | grep -v "^$" | sort -u)
115
+
116
+ # For monorepos, detect which packages changed
117
+ CHANGED_PACKAGES="[]"
118
+ if [ "${{ github.event.inputs.package }}" != "" ]; then
119
+ CHANGED_PACKAGES="[\"${{ github.event.inputs.package }}\"]"
120
+ else
121
+ # Get changed files since last tag or in the current push
122
+ LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
123
+ if [ -z "$LAST_TAG" ]; then
124
+ CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || git ls-files)
125
+ else
126
+ CHANGED_FILES=$(git diff --name-only $LAST_TAG HEAD)
127
+ fi
128
+
129
+ # Match changed files to packages
130
+ CHANGED_PKGS=""
131
+ for pkg in $PACKAGES; do
132
+ if echo "$CHANGED_FILES" | grep -q "^$pkg/"; then
133
+ if [ -z "$CHANGED_PKGS" ]; then
134
+ CHANGED_PKGS="\"$pkg\""
135
+ else
136
+ CHANGED_PKGS="$CHANGED_PKGS, \"$pkg\""
137
+ fi
138
+ fi
139
+ done
140
+ CHANGED_PACKAGES="[$CHANGED_PKGS]"
141
+ fi
142
+
143
+ echo "packages=$CHANGED_PACKAGES" >> $GITHUB_OUTPUT
144
+ if [ "$CHANGED_PACKAGES" = "[]" ]; then
145
+ echo "has_changes=false" >> $GITHUB_OUTPUT
146
+ else
147
+ echo "has_changes=true" >> $GITHUB_OUTPUT
148
+ fi
149
+
150
+
151
+ # ==========================================================================
152
+ # Release Job
153
+ # ==========================================================================
154
+ release:
155
+ name: Release
156
+ needs: detect-changes
157
+ if: needs.detect-changes.outputs.has_changes == 'true'
158
+ runs-on: ubuntu-latest
159
+ permissions:
160
+ contents: write
161
+ packages: write
162
+
163
+ strategy:
164
+ fail-fast: false
165
+ matrix:
166
+ package: ${{ fromJson(needs.detect-changes.outputs.packages) }}
167
+
168
+ steps:
169
+ - name: Checkout Repository
170
+ uses: actions/checkout@v4
171
+ with:
172
+ fetch-depth: 0
173
+ token: ${{ secrets.GITHUB_TOKEN }}
174
+
175
+ - name: Setup Bun
176
+ uses: oven-sh/setup-bun@v2
177
+ with:
178
+ bun-version: ${{ env.BUN_VERSION }}
179
+
180
+ - name: Configure Git
181
+ run: |
182
+ git config user.name "github-actions[bot]"
183
+ git config user.email "github-actions[bot]@users.noreply.github.com"
184
+
185
+ - name: Install Dependencies
186
+ working-directory: ${{ matrix.package }}
187
+ run: bun install
188
+
189
+ - name: Run Release Script
190
+ id: release
191
+ working-directory: ${{ matrix.package }}
192
+ env:
193
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
194
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
195
+ DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
196
+ BUMP_TYPE: ${{ github.event.inputs.bump_type || '' }}
197
+ PUBLISH_NPM: ${{ github.event.inputs.publish_npm || 'true' }}
198
+ run: |
199
+ # Run the Bun release script
200
+ bun run scripts/release.ts
201
+
202
+ - name: Build Package
203
+ if: steps.release.outputs.should_release == 'true'
204
+ working-directory: ${{ matrix.package }}
205
+ run: |
206
+ if bun run build 2>/dev/null; then
207
+ echo "Build completed successfully"
208
+ else
209
+ echo "No build script found or build not required"
210
+ fi
211
+
212
+ - name: Commit Changes
213
+ if: steps.release.outputs.should_release == 'true' && github.event.inputs.dry_run != 'true'
214
+ working-directory: ${{ matrix.package }}
215
+ run: |
216
+ git add CHANGELOG.md package.json
217
+ git commit -m "chore(release): v${{ steps.release.outputs.new_version }} [skip ci]" || echo "No changes to commit"
218
+ git push
219
+
220
+ - name: Create GitHub Release
221
+ if: steps.release.outputs.should_release == 'true' && github.event.inputs.dry_run != 'true'
222
+ uses: softprops/action-gh-release@v2
223
+ with:
224
+ tag_name: v${{ steps.release.outputs.new_version }}
225
+ name: Release v${{ steps.release.outputs.new_version }}
226
+ body_path: ${{ matrix.package }}/RELEASE_NOTES.md
227
+ draft: false
228
+ prerelease: false
229
+ token: ${{ secrets.GITHUB_TOKEN }}
230
+
231
+ - name: Publish to NPM
232
+ if: steps.release.outputs.should_release == 'true' && github.event.inputs.dry_run != 'true' && (github.event.inputs.publish_npm == 'true' || github.event.inputs.publish_npm == '')
233
+ working-directory: ${{ matrix.package }}
234
+ run: |
235
+ # Check if package is not private
236
+ PRIVATE=$(cat package.json | bun -e "console.log(JSON.parse(await Bun.stdin.text()).private || false)")
237
+ if [ "$PRIVATE" = "false" ]; then
238
+ echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
239
+ bun publish --access public || npm publish --access public
240
+ else
241
+ echo "Package is private, skipping NPM publish"
242
+ fi
243
+
244
+ - name: Summary
245
+ run: |
246
+ echo "## Release Summary" >> $GITHUB_STEP_SUMMARY
247
+ echo "" >> $GITHUB_STEP_SUMMARY
248
+ if [ "${{ github.event.inputs.dry_run }}" = "true" ]; then
249
+ echo "āš ļø **DRY RUN MODE** - No actual release was created" >> $GITHUB_STEP_SUMMARY
250
+ fi
251
+ echo "" >> $GITHUB_STEP_SUMMARY
252
+ echo "- **Package**: ${{ matrix.package }}" >> $GITHUB_STEP_SUMMARY
253
+ echo "- **Version**: ${{ steps.release.outputs.new_version }}" >> $GITHUB_STEP_SUMMARY
254
+ echo "- **Bump Type**: ${{ steps.release.outputs.bump_type }}" >> $GITHUB_STEP_SUMMARY
@@ -0,0 +1,73 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "types": {
4
+ "feat": {
5
+ "title": "✨ Features",
6
+ "bump": "minor",
7
+ "description": "New features or functionality"
8
+ },
9
+ "fix": {
10
+ "title": "šŸ› Bug Fixes",
11
+ "bump": "patch",
12
+ "description": "Bug fixes and corrections"
13
+ },
14
+ "perf": {
15
+ "title": "⚔ Performance Improvements",
16
+ "bump": "patch",
17
+ "description": "Performance optimizations"
18
+ },
19
+ "refactor": {
20
+ "title": "ā™»ļø Code Refactoring",
21
+ "bump": "patch",
22
+ "description": "Code changes that neither fix bugs nor add features"
23
+ },
24
+ "docs": {
25
+ "title": "šŸ“š Documentation",
26
+ "bump": null,
27
+ "description": "Documentation only changes"
28
+ },
29
+ "style": {
30
+ "title": "šŸ’„ Styles",
31
+ "bump": null,
32
+ "description": "Code style changes (formatting, whitespace)"
33
+ },
34
+ "test": {
35
+ "title": "āœ… Tests",
36
+ "bump": null,
37
+ "description": "Adding or updating tests"
38
+ },
39
+ "build": {
40
+ "title": "šŸ“¦ Build System",
41
+ "bump": "patch",
42
+ "description": "Build system or dependency changes"
43
+ },
44
+ "ci": {
45
+ "title": "šŸ”§ CI Configuration",
46
+ "bump": null,
47
+ "description": "CI/CD configuration changes"
48
+ },
49
+ "chore": {
50
+ "title": "šŸ”Ø Chores",
51
+ "bump": null,
52
+ "description": "Maintenance tasks and other changes"
53
+ },
54
+ "revert": {
55
+ "title": "āŖ Reverts",
56
+ "bump": "patch",
57
+ "description": "Reverting previous commits"
58
+ }
59
+ },
60
+ "skipCI": [
61
+ "[skip ci]",
62
+ "[ci skip]",
63
+ "[no ci]",
64
+ "chore(release)"
65
+ ],
66
+ "tagPrefix": "v",
67
+ "branches": {
68
+ "main": "latest",
69
+ "next": "next",
70
+ "beta": "beta",
71
+ "alpha": "alpha"
72
+ }
73
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,44 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.4.0] - 2026-01-16
9
+
10
+ ### ✨ Features
11
+
12
+ - **language**: update syntax grammar for new directives and reactive bindings (cb65c98)
13
+
14
+ ### šŸ› Bug Fixes
15
+
16
+ - **release**: use appendFileSync for GitHub Actions output (c953f51)
17
+
18
+ ### šŸ“ Other Changes
19
+
20
+ -
21
+ c1aa285dac910ec64f2240c849ff6c0d18b7cd2e ()
22
+ -
23
+ dc90df4092e298ffdce221f8127ae87b0aeed45c ()
24
+ -
25
+ 18982c541782091455f32bb5c354e66a06c2938a ()
26
+ -
27
+ 5a5046880d2afbc7df70abce0062c6a4be21859e ()
28
+ -
29
+ 8627c68faf8cd28521675a6216dc7462c7deb2b2 ()
30
+ -
31
+ e06fdd9e3167f30671c559e98c3fb75088c7e1b6 ()
32
+ - 0.2.9 (de391df)
33
+ -
34
+ e0ad1cd02292af51ac321e7580ae9e534abd6c1b ()
35
+ -
36
+ e4ca2b1d2af81e1cf9876b0932cd3178b7d1bf7e ()
37
+ -
38
+ cff8202737008d97c6527703f51783583eae7e6f ()
39
+ -
40
+ 645d159ba240aec6cb6cab5bf332743f6eed4fcd ()
41
+ -
42
+ 52507461378cb8f2d87245b84924790a191879ad ()
43
+ - ()
44
+
package/README.md CHANGED
@@ -12,7 +12,7 @@ VS Code extension providing world-class development support for the Zenith frame
12
12
  - **IntelliSense**: Smart completions for Zenith components, hooks, and reactive state.
13
13
  - **Emmet Support**: Accelerated HTML development inside `.zen` templates.
14
14
  - **Project Scaffolding**: Integrated support for starting new projects.
15
- - **LSP Integration**: Leverages `@zenith/language-server` for powerful diagnostics and refactoring.
15
+ - **LSP Integration**: Leverages `@zenithbuild/language-server` for powerful diagnostics and refactoring.
16
16
 
17
17
  ## Supported Extensions
18
18
 
package/package.json CHANGED
@@ -1,102 +1,110 @@
1
1
  {
2
- "name": "zenith-language",
3
- "displayName": "Zenith Language Support",
4
- "description": "Syntax highlighting, IntelliSense, and editor support for Zenith Framework (.zen files)",
5
- "version": "0.3.0",
6
- "publisher": "ZenithBuild",
7
- "engines": {
8
- "vscode": "^1.80.0"
9
- },
10
- "main": "./out/extension.js",
11
- "scripts": {
12
- "build:server": "cd ../zenith-language-server && bun run build",
13
- "compile": "bun x esbuild src/extension.ts --bundle --outfile=out/extension.js --external:vscode --format=cjs --platform=node && cp ../zenith-language-server/dist/server.js out/server.js",
14
- "watch": "bun run compile -- --watch",
15
- "build:marketplace": "bun run build:server && bun run compile && node scripts/build.js marketplace",
16
- "build:openvsx": "bun run build:server && bun run compile && node scripts/build.js openvsx",
17
- "build:all": "bun run build:server && bun run compile && node scripts/build.js all"
18
- },
19
- "devDependencies": {
20
- "@types/node": "^20.0.0",
21
- "@types/vscode": "^1.80.0",
22
- "esbuild": "^0.19.0",
23
- "typescript": "^5.0.0"
24
- },
25
- "categories": [
26
- "Programming Languages"
27
- ],
28
- "icon": "assets/logo.png",
29
- "keywords": [
30
- "zenith",
31
- "zen",
32
- "syntax",
33
- "highlighting",
34
- "intellisense",
35
- "framework"
36
- ],
37
- "contributes": {
38
- "languages": [
39
- {
40
- "id": "zenith",
41
- "aliases": [
42
- "Zenith",
43
- "zenith"
44
- ],
45
- "extensions": [
46
- ".zen",
47
- ".zen.html",
48
- ".zenx"
49
- ],
50
- "configuration": "./language-configuration.json"
51
- }
2
+ "name": "zenith-language",
3
+ "displayName": "Zenith Language Support",
4
+ "description": "Syntax highlighting, IntelliSense, and editor support for Zenith Framework (.zen files)",
5
+ "version": "0.4.1",
6
+ "publisher": "ZenithBuild",
7
+ "engines": {
8
+ "vscode": "^1.80.0"
9
+ },
10
+ "main": "./out/extension.js",
11
+ "scripts": {
12
+ "build:server": "cd ../zenith-language-server && bun run build",
13
+ "compile": "bun x esbuild src/extension.ts --bundle --outfile=out/extension.js --external:vscode --format=cjs --platform=node && cp ../zenith-language-server/dist/server.js out/server.js",
14
+ "watch": "bun run compile -- --watch",
15
+ "build:marketplace": "bun run build:server && bun run compile && node scripts/build.js marketplace",
16
+ "build:openvsx": "bun run build:server && bun run compile && node scripts/build.js openvsx",
17
+ "build:all": "bun run build:server && bun run compile && node scripts/build.js all",
18
+ "release": "bun run scripts/release.ts",
19
+ "release:dry": "bun run scripts/release.ts --dry-run",
20
+ "release:patch": "bun run scripts/release.ts --bump=patch",
21
+ "release:minor": "bun run scripts/release.ts --bump=minor",
22
+ "release:major": "bun run scripts/release.ts --bump=major"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^20.0.0",
26
+ "@types/vscode": "^1.80.0",
27
+ "esbuild": "^0.19.0",
28
+ "typescript": "^5.0.0"
29
+ },
30
+ "categories": [
31
+ "Programming Languages"
32
+ ],
33
+ "icon": "assets/logo.png",
34
+ "keywords": [
35
+ "zenith",
36
+ "zen",
37
+ "syntax",
38
+ "highlighting",
39
+ "intellisense",
40
+ "framework"
41
+ ],
42
+ "contributes": {
43
+ "languages": [
44
+ {
45
+ "id": "zenith",
46
+ "aliases": [
47
+ "Zenith",
48
+ "zenith"
52
49
  ],
53
- "grammars": [
54
- {
55
- "language": "zenith",
56
- "scopeName": "text.html.zenith",
57
- "path": "./syntaxes/zenith.tmLanguage.json",
58
- "embeddedLanguages": {
59
- "source.js": "javascript",
60
- "source.ts": "typescript",
61
- "source.css": "css",
62
- "text.html.basic": "html",
63
- "meta.embedded.block.javascript": "javascript",
64
- "meta.embedded.block.typescript": "typescript",
65
- "meta.embedded.block.css": "css"
66
- }
67
- }
50
+ "extensions": [
51
+ ".zen",
52
+ ".zen.html",
53
+ ".zenx"
68
54
  ],
69
- "configurationDefaults": {
70
- "[zenith]": {
71
- "editor.formatOnSave": true,
72
- "editor.wordBasedSuggestions": "off",
73
- "editor.suggest.insertMode": "replace",
74
- "editor.semanticHighlighting.enabled": true,
75
- "editor.quickSuggestions": {
76
- "other": true,
77
- "comments": false,
78
- "strings": true
79
- },
80
- "editor.autoClosingBrackets": "always"
81
- },
82
- "emmet.includeLanguages": {
83
- "zenith": "html"
84
- },
85
- "emmet.syntaxProfiles": {
86
- "zenith": "html"
87
- }
55
+ "configuration": "./language-configuration.json"
56
+ }
57
+ ],
58
+ "grammars": [
59
+ {
60
+ "language": "zenith",
61
+ "scopeName": "text.html.zenith",
62
+ "path": "./syntaxes/zenith.tmLanguage.json",
63
+ "embeddedLanguages": {
64
+ "source.js": "javascript",
65
+ "source.ts": "typescript",
66
+ "source.css": "css",
67
+ "text.html.basic": "html",
68
+ "meta.embedded.block.javascript": "javascript",
69
+ "meta.embedded.block.typescript": "typescript",
70
+ "meta.embedded.block.css": "css"
88
71
  }
89
- },
90
- "repository": {
91
- "type": "git",
92
- "url": "https://github.com/zenithbuild/zenith"
93
- },
94
- "homepage": "https://github.com/zenithbuild/zenith#readme",
95
- "bugs": {
96
- "url": "https://github.com/zenithbuild/zenith/issues"
97
- },
98
- "license": "MIT",
99
- "dependencies": {
100
- "vscode-languageclient": "^9.0.1"
72
+ }
73
+ ],
74
+ "configurationDefaults": {
75
+ "[zenith]": {
76
+ "editor.formatOnSave": true,
77
+ "editor.wordBasedSuggestions": "off",
78
+ "editor.suggest.insertMode": "replace",
79
+ "editor.semanticHighlighting.enabled": true,
80
+ "editor.quickSuggestions": {
81
+ "other": true,
82
+ "comments": false,
83
+ "strings": true
84
+ },
85
+ "editor.autoClosingBrackets": "always"
86
+ },
87
+ "emmet.includeLanguages": {
88
+ "zenith": "html"
89
+ },
90
+ "emmet.syntaxProfiles": {
91
+ "zenith": "html"
92
+ }
101
93
  }
102
- }
94
+ },
95
+ "repository": {
96
+ "type": "git",
97
+ "url": "https://github.com/zenithbuild/zenith"
98
+ },
99
+ "homepage": "https://github.com/zenithbuild/zenith#readme",
100
+ "bugs": {
101
+ "url": "https://github.com/zenithbuild/zenith/issues"
102
+ },
103
+ "publishConfig": {
104
+ "access": "public"
105
+ },
106
+ "license": "MIT",
107
+ "dependencies": {
108
+ "vscode-languageclient": "^9.0.1"
109
+ }
110
+ }
@@ -0,0 +1,554 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * =============================================================================
4
+ * Zenith Release Script
5
+ * =============================================================================
6
+ *
7
+ * Automated release script using Bun for all Zenith repositories.
8
+ * Handles:
9
+ * - Conventional Commit parsing for automatic version determination
10
+ * - CHANGELOG.md generation
11
+ * - package.json version updates
12
+ * - Release notes generation for GitHub releases
13
+ *
14
+ * Usage:
15
+ * bun run scripts/release.ts # Normal release
16
+ * bun run scripts/release.ts --dry-run # Test without making changes
17
+ * bun run scripts/release.ts --bump=major # Force major version bump
18
+ * bun run scripts/release.ts --bump=minor # Force minor version bump
19
+ * bun run scripts/release.ts --bump=patch # Force patch version bump
20
+ *
21
+ * Environment Variables:
22
+ * DRY_RUN - Set to 'true' for dry run mode
23
+ * BUMP_TYPE - Force bump type (patch, minor, major)
24
+ * GITHUB_TOKEN - GitHub token for API calls (optional)
25
+ *
26
+ * =============================================================================
27
+ */
28
+
29
+ import { $ } from "bun";
30
+ import { existsSync } from "fs";
31
+ import { join } from "path";
32
+
33
+ // =============================================================================
34
+ // Types
35
+ // =============================================================================
36
+
37
+ interface Commit {
38
+ hash: string;
39
+ type: string;
40
+ scope: string | null;
41
+ subject: string;
42
+ body: string;
43
+ breaking: boolean;
44
+ raw: string;
45
+ }
46
+
47
+ interface PackageJson {
48
+ name: string;
49
+ version: string;
50
+ private?: boolean;
51
+ [key: string]: unknown;
52
+ }
53
+
54
+ interface ReleaseConfig {
55
+ types: {
56
+ [key: string]: {
57
+ title: string;
58
+ bump: "patch" | "minor" | "major" | null;
59
+ hidden?: boolean;
60
+ };
61
+ };
62
+ skipCI: string[];
63
+ tagPrefix: string;
64
+ }
65
+
66
+ type BumpType = "patch" | "minor" | "major";
67
+
68
+ // =============================================================================
69
+ // Configuration
70
+ // =============================================================================
71
+
72
+ const DEFAULT_CONFIG: ReleaseConfig = {
73
+ types: {
74
+ feat: { title: "✨ Features", bump: "minor" },
75
+ fix: { title: "šŸ› Bug Fixes", bump: "patch" },
76
+ perf: { title: "⚔ Performance Improvements", bump: "patch" },
77
+ refactor: { title: "ā™»ļø Code Refactoring", bump: "patch" },
78
+ docs: { title: "šŸ“š Documentation", bump: null },
79
+ style: { title: "šŸ’„ Styles", bump: null },
80
+ test: { title: "āœ… Tests", bump: null },
81
+ build: { title: "šŸ“¦ Build System", bump: "patch" },
82
+ ci: { title: "šŸ”§ CI Configuration", bump: null },
83
+ chore: { title: "šŸ”Ø Chores", bump: null },
84
+ revert: { title: "āŖ Reverts", bump: "patch" },
85
+ },
86
+ skipCI: ["[skip ci]", "[ci skip]", "[no ci]"],
87
+ tagPrefix: "v",
88
+ };
89
+
90
+ // =============================================================================
91
+ // Utility Functions
92
+ // =============================================================================
93
+
94
+ function log(message: string, type: "info" | "success" | "warn" | "error" = "info"): void {
95
+ const colors = {
96
+ info: "\x1b[36m", // Cyan
97
+ success: "\x1b[32m", // Green
98
+ warn: "\x1b[33m", // Yellow
99
+ error: "\x1b[31m", // Red
100
+ };
101
+ const reset = "\x1b[0m";
102
+ const prefix = {
103
+ info: "ℹ",
104
+ success: "āœ“",
105
+ warn: "⚠",
106
+ error: "āœ–",
107
+ };
108
+ console.log(`${colors[type]}${prefix[type]}${reset} ${message}`);
109
+ }
110
+
111
+ function parseArgs(): { dryRun: boolean; bumpType: BumpType | null } {
112
+ const args = process.argv.slice(2);
113
+ let dryRun = process.env.DRY_RUN === "true";
114
+ let bumpType: BumpType | null = (process.env.BUMP_TYPE as BumpType) || null;
115
+
116
+ for (const arg of args) {
117
+ if (arg === "--dry-run") {
118
+ dryRun = true;
119
+ } else if (arg.startsWith("--bump=")) {
120
+ const bump = arg.split("=")[1] as BumpType;
121
+ if (["patch", "minor", "major"].includes(bump)) {
122
+ bumpType = bump;
123
+ }
124
+ }
125
+ }
126
+
127
+ return { dryRun, bumpType };
128
+ }
129
+
130
+ // =============================================================================
131
+ // Git Functions
132
+ // =============================================================================
133
+
134
+ async function getLastTag(): Promise<string | null> {
135
+ try {
136
+ const result = await $`git describe --tags --abbrev=0 2>/dev/null`.text();
137
+ return result.trim() || null;
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+
143
+ async function tagExists(version: string): Promise<boolean> {
144
+ try {
145
+ const result = await $`git tag -l "v${version}"`.text();
146
+ return result.trim() !== "";
147
+ } catch {
148
+ return false;
149
+ }
150
+ }
151
+
152
+
153
+ async function getCommitsSinceTag(tag: string | null): Promise<string[]> {
154
+ try {
155
+ let result: string;
156
+ if (tag) {
157
+ result = await $`git log ${tag}..HEAD --pretty=format:"%H|%s|%b|||"`.text();
158
+ } else {
159
+ result = await $`git log --pretty=format:"%H|%s|%b|||"`.text();
160
+ }
161
+ return result.split("|||").filter((c) => c.trim());
162
+ } catch {
163
+ return [];
164
+ }
165
+ }
166
+
167
+ function parseCommit(rawCommit: string, config: ReleaseConfig): Commit | null {
168
+ const parts = rawCommit.trim().split("|");
169
+ if (parts.length < 2) return null;
170
+
171
+ const hash = parts[0];
172
+ const subject = parts[1];
173
+ const body = parts.slice(2).join("|").trim();
174
+
175
+ // Skip CI commits
176
+ if (config.skipCI.some((skip) => subject.includes(skip))) {
177
+ return null;
178
+ }
179
+
180
+ // Parse conventional commit format: type(scope): subject
181
+ const conventionalMatch = subject.match(/^(\w+)(?:\(([^)]+)\))?!?:\s*(.+)$/);
182
+
183
+ if (!conventionalMatch) {
184
+ // Non-conventional commit, treat as misc
185
+ return {
186
+ hash,
187
+ type: "other",
188
+ scope: null,
189
+ subject,
190
+ body,
191
+ breaking: false,
192
+ raw: rawCommit,
193
+ };
194
+ }
195
+
196
+ const [, type, scope, message] = conventionalMatch;
197
+ const breaking = subject.includes("!:") ||
198
+ body.toLowerCase().includes("breaking change") ||
199
+ body.toLowerCase().includes("breaking-change");
200
+
201
+ return {
202
+ hash,
203
+ type: type.toLowerCase(),
204
+ scope: scope || null,
205
+ subject: message.trim(),
206
+ body,
207
+ breaking,
208
+ raw: rawCommit,
209
+ };
210
+ }
211
+
212
+ // =============================================================================
213
+ // Version Functions
214
+ // =============================================================================
215
+
216
+ function bumpVersion(version: string, bumpType: BumpType): string {
217
+ const parts = version.replace(/^v/, "").split(".");
218
+ const major = parseInt(parts[0] || "0", 10);
219
+ const minor = parseInt(parts[1] || "0", 10);
220
+ const patch = parseInt(parts[2] || "0", 10);
221
+
222
+ switch (bumpType) {
223
+ case "major":
224
+ return `${major + 1}.0.0`;
225
+ case "minor":
226
+ return `${major}.${minor + 1}.0`;
227
+ case "patch":
228
+ return `${major}.${minor}.${patch + 1}`;
229
+ }
230
+ }
231
+
232
+ function determineBumpType(commits: Commit[], config: ReleaseConfig): BumpType | null {
233
+ let bump: BumpType | null = null;
234
+ const priority: Record<BumpType, number> = { patch: 1, minor: 2, major: 3 };
235
+
236
+ for (const commit of commits) {
237
+ // Breaking changes always result in major bump
238
+ if (commit.breaking) {
239
+ return "major";
240
+ }
241
+
242
+ const typeConfig = config.types[commit.type];
243
+ if (typeConfig?.bump) {
244
+ if (!bump || priority[typeConfig.bump] > priority[bump]) {
245
+ bump = typeConfig.bump;
246
+ }
247
+ }
248
+ }
249
+
250
+ return bump;
251
+ }
252
+
253
+ // =============================================================================
254
+ // Changelog Generation
255
+ // =============================================================================
256
+
257
+ function generateChangelog(
258
+ commits: Commit[],
259
+ newVersion: string,
260
+ config: ReleaseConfig
261
+ ): string {
262
+ const date = new Date().toISOString().split("T")[0];
263
+ const groupedCommits: Record<string, Commit[]> = {};
264
+
265
+ // Group commits by type
266
+ for (const commit of commits) {
267
+ const type = commit.type;
268
+ if (!groupedCommits[type]) {
269
+ groupedCommits[type] = [];
270
+ }
271
+ groupedCommits[type].push(commit);
272
+ }
273
+
274
+ let changelog = `## [${newVersion}] - ${date}\n\n`;
275
+
276
+ // Breaking changes section
277
+ const breakingChanges = commits.filter((c) => c.breaking);
278
+ if (breakingChanges.length > 0) {
279
+ changelog += `### āš ļø BREAKING CHANGES\n\n`;
280
+ for (const commit of breakingChanges) {
281
+ const scope = commit.scope ? `**${commit.scope}**: ` : "";
282
+ changelog += `- ${scope}${commit.subject} (${commit.hash.slice(0, 7)})\n`;
283
+ }
284
+ changelog += "\n";
285
+ }
286
+
287
+ // Regular changes by type
288
+ for (const [type, typeConfig] of Object.entries(config.types)) {
289
+ if (typeConfig.hidden) continue;
290
+
291
+ const typeCommits = groupedCommits[type];
292
+ if (!typeCommits || typeCommits.length === 0) continue;
293
+
294
+ changelog += `### ${typeConfig.title}\n\n`;
295
+ for (const commit of typeCommits) {
296
+ const scope = commit.scope ? `**${commit.scope}**: ` : "";
297
+ changelog += `- ${scope}${commit.subject} (${commit.hash.slice(0, 7)})\n`;
298
+ }
299
+ changelog += "\n";
300
+ }
301
+
302
+ // Other/uncategorized commits
303
+ if (groupedCommits.other && groupedCommits.other.length > 0) {
304
+ changelog += `### šŸ“ Other Changes\n\n`;
305
+ for (const commit of groupedCommits.other) {
306
+ changelog += `- ${commit.subject} (${commit.hash.slice(0, 7)})\n`;
307
+ }
308
+ changelog += "\n";
309
+ }
310
+
311
+ return changelog;
312
+ }
313
+
314
+ function generateReleaseNotes(
315
+ commits: Commit[],
316
+ newVersion: string,
317
+ packageName: string,
318
+ config: ReleaseConfig
319
+ ): string {
320
+ const changelog = generateChangelog(commits, newVersion, config);
321
+
322
+ return `# ${packageName} v${newVersion}
323
+
324
+ ${changelog}
325
+
326
+ ## Installation
327
+
328
+ \`\`\`bash
329
+ bun add ${packageName}@${newVersion}
330
+ \`\`\`
331
+
332
+ or with npm:
333
+
334
+ \`\`\`bash
335
+ npm install ${packageName}@${newVersion}
336
+ \`\`\`
337
+ `;
338
+ }
339
+
340
+ async function updateChangelogFile(
341
+ newChangelog: string,
342
+ dryRun: boolean
343
+ ): Promise<void> {
344
+ const changelogPath = join(process.cwd(), "CHANGELOG.md");
345
+ let existingChangelog = "";
346
+
347
+ if (existsSync(changelogPath)) {
348
+ existingChangelog = await Bun.file(changelogPath).text();
349
+ // Remove the header if it exists
350
+ existingChangelog = existingChangelog.replace(
351
+ /^# Changelog\n\n(?:.*\n)*?(?=## \[)/,
352
+ ""
353
+ );
354
+ }
355
+
356
+ const fullChangelog = `# Changelog
357
+
358
+ All notable changes to this project will be documented in this file.
359
+
360
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
361
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
362
+
363
+ ${newChangelog}${existingChangelog}`;
364
+
365
+ if (dryRun) {
366
+ log("Would write CHANGELOG.md:", "info");
367
+ console.log(fullChangelog.slice(0, 500) + "...");
368
+ } else {
369
+ await Bun.write(changelogPath, fullChangelog);
370
+ log("Updated CHANGELOG.md", "success");
371
+ }
372
+ }
373
+
374
+ // =============================================================================
375
+ // Package.json Functions
376
+ // =============================================================================
377
+
378
+ async function readPackageJson(): Promise<PackageJson> {
379
+ const packagePath = join(process.cwd(), "package.json");
380
+ const content = await Bun.file(packagePath).text();
381
+ return JSON.parse(content);
382
+ }
383
+
384
+ async function updatePackageJson(
385
+ newVersion: string,
386
+ dryRun: boolean
387
+ ): Promise<void> {
388
+ const packagePath = join(process.cwd(), "package.json");
389
+ const packageJson = await readPackageJson();
390
+ packageJson.version = newVersion;
391
+
392
+ if (dryRun) {
393
+ log(`Would update package.json version to ${newVersion}`, "info");
394
+ } else {
395
+ await Bun.write(packagePath, JSON.stringify(packageJson, null, 2) + "\n");
396
+ log(`Updated package.json version to ${newVersion}`, "success");
397
+ }
398
+ }
399
+
400
+ // =============================================================================
401
+ // GitHub Actions Output
402
+ // =============================================================================
403
+
404
+ async function setGitHubOutput(name: string, value: string): Promise<void> {
405
+ const outputFile = process.env.GITHUB_OUTPUT;
406
+ if (outputFile) {
407
+ const output = `${name}=${value}\n`;
408
+ // Use Node.js appendFileSync for reliable GitHub Actions output
409
+ const { appendFileSync } = await import("fs");
410
+ appendFileSync(outputFile, output);
411
+ log(`Set output: ${name}=${value}`, "info");
412
+ }
413
+ }
414
+
415
+ // =============================================================================
416
+ // Load Custom Config
417
+ // =============================================================================
418
+
419
+ async function loadConfig(): Promise<ReleaseConfig> {
420
+ const configPaths = [
421
+ join(process.cwd(), ".releaserc.json"),
422
+ join(process.cwd(), "release.config.json"),
423
+ join(process.cwd(), ".release.json"),
424
+ ];
425
+
426
+ for (const configPath of configPaths) {
427
+ if (existsSync(configPath)) {
428
+ const content = await Bun.file(configPath).text();
429
+ const customConfig = JSON.parse(content);
430
+ return { ...DEFAULT_CONFIG, ...customConfig };
431
+ }
432
+ }
433
+
434
+ return DEFAULT_CONFIG;
435
+ }
436
+
437
+ // =============================================================================
438
+ // Main Release Function
439
+ // =============================================================================
440
+
441
+ async function main(): Promise<void> {
442
+ console.log("\nšŸš€ Zenith Release Script\n");
443
+ console.log("=".repeat(50) + "\n");
444
+
445
+ const { dryRun, bumpType: forcedBumpType } = parseArgs();
446
+
447
+ if (dryRun) {
448
+ log("Running in DRY RUN mode - no changes will be made", "warn");
449
+ console.log();
450
+ }
451
+
452
+ // Load configuration
453
+ const config = await loadConfig();
454
+ log("Loaded release configuration", "success");
455
+
456
+ // Read package.json
457
+ const packageJson = await readPackageJson();
458
+ log(`Package: ${packageJson.name}`, "info");
459
+ log(`Current version: ${packageJson.version}`, "info");
460
+
461
+ // Get last tag
462
+ const lastTag = await getLastTag();
463
+ log(`Last tag: ${lastTag || "none"}`, "info");
464
+
465
+ // Get commits since last tag
466
+ const rawCommits = await getCommitsSinceTag(lastTag);
467
+ log(`Found ${rawCommits.length} commits since last release`, "info");
468
+
469
+ if (rawCommits.length === 0) {
470
+ log("No commits since last release. Nothing to do.", "warn");
471
+ await setGitHubOutput("should_release", "false");
472
+ return;
473
+ }
474
+
475
+ // Parse commits
476
+ const commits = rawCommits
477
+ .map((c) => parseCommit(c, config))
478
+ .filter((c): c is Commit => c !== null);
479
+
480
+ log(`Parsed ${commits.length} conventional commits`, "info");
481
+
482
+ // Determine bump type
483
+ const determinedBumpType = forcedBumpType || determineBumpType(commits, config);
484
+
485
+ if (!determinedBumpType) {
486
+ log("No version bump required based on commits", "warn");
487
+ await setGitHubOutput("should_release", "false");
488
+ return;
489
+ }
490
+
491
+ log(`Version bump type: ${determinedBumpType}`, "info");
492
+
493
+ // Calculate new version
494
+ const newVersion = bumpVersion(packageJson.version, determinedBumpType);
495
+ log(`New version: ${newVersion}`, "success");
496
+
497
+ // Check if this version already exists as a tag (fallback to prevent duplicates)
498
+ if (await tagExists(newVersion)) {
499
+ log(`Version v${newVersion} already exists as a tag. Skipping release.`, "warn");
500
+ await setGitHubOutput("should_release", "false");
501
+ return;
502
+ }
503
+
504
+ console.log("\n" + "-".repeat(50) + "\n");
505
+
506
+ // Generate changelog
507
+ const changelog = generateChangelog(commits, newVersion, config);
508
+ log("Generated changelog", "success");
509
+
510
+ // Generate release notes
511
+ const releaseNotes = generateReleaseNotes(
512
+ commits,
513
+ newVersion,
514
+ packageJson.name,
515
+ config
516
+ );
517
+
518
+ // Update files
519
+ await updateChangelogFile(changelog, dryRun);
520
+ await updatePackageJson(newVersion, dryRun);
521
+
522
+ // Write release notes for GitHub Action
523
+ if (!dryRun) {
524
+ await Bun.write(join(process.cwd(), "RELEASE_NOTES.md"), releaseNotes);
525
+ log("Written RELEASE_NOTES.md", "success");
526
+ }
527
+
528
+ // Set GitHub Actions outputs
529
+ await setGitHubOutput("should_release", "true");
530
+ await setGitHubOutput("new_version", newVersion);
531
+ await setGitHubOutput("bump_type", determinedBumpType);
532
+ await setGitHubOutput("package_name", packageJson.name);
533
+
534
+ console.log("\n" + "=".repeat(50));
535
+ console.log("\nāœ… Release preparation complete!\n");
536
+
537
+ if (dryRun) {
538
+ console.log("DRY RUN - Summary of what would happen:");
539
+ console.log(` • Version: ${packageJson.version} → ${newVersion}`);
540
+ console.log(` • CHANGELOG.md would be updated`);
541
+ console.log(` • Release notes would be created`);
542
+ console.log(` • Git tag: v${newVersion} would be created`);
543
+ console.log(` • GitHub release would be published`);
544
+ if (!packageJson.private) {
545
+ console.log(` • Package would be published to NPM`);
546
+ }
547
+ }
548
+ }
549
+
550
+ // Run the script
551
+ main().catch((error) => {
552
+ log(`Release failed: ${error.message}`, "error");
553
+ process.exit(1);
554
+ });