@activade/open-workflows 1.0.2 → 2.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.
Files changed (39) hide show
  1. package/README.md +91 -64
  2. package/actions/doc-sync/action.yml +42 -0
  3. package/actions/doc-sync/skill.md +97 -0
  4. package/actions/issue-label/action.yml +36 -0
  5. package/actions/issue-label/skill.md +113 -0
  6. package/actions/issue-label/src/apply-labels.ts +118 -0
  7. package/actions/pr-review/action.yml +36 -0
  8. package/actions/pr-review/skill.md +223 -0
  9. package/actions/pr-review/src/submit-review.ts +154 -0
  10. package/actions/release/action.yml +59 -0
  11. package/actions/release/src/publish.ts +235 -0
  12. package/dist/cli/index.js +133 -570
  13. package/package.json +11 -21
  14. package/dist/cli/index.d.ts +0 -2
  15. package/dist/cli/installer.d.ts +0 -24
  16. package/dist/cli/templates/auth.d.ts +0 -1
  17. package/dist/cli/templates/doc-sync.d.ts +0 -1
  18. package/dist/cli/templates/index.d.ts +0 -7
  19. package/dist/cli/templates/issue-label.d.ts +0 -1
  20. package/dist/cli/templates/pr-review.d.ts +0 -1
  21. package/dist/cli/templates/release.d.ts +0 -1
  22. package/dist/cli/templates/shared.d.ts +0 -3
  23. package/dist/index.d.ts +0 -3
  24. package/dist/index.js +0 -62
  25. package/dist/skills/doc-sync.d.ts +0 -1
  26. package/dist/skills/index.d.ts +0 -10
  27. package/dist/skills/issue-label.d.ts +0 -1
  28. package/dist/skills/pr-review.d.ts +0 -1
  29. package/dist/skills/release-notes.d.ts +0 -1
  30. package/dist/tools/apply-labels/index.d.ts +0 -2
  31. package/dist/tools/apply-labels/schema.d.ts +0 -12
  32. package/dist/tools/bun-release/index.d.ts +0 -2
  33. package/dist/tools/bun-release/schema.d.ts +0 -4
  34. package/dist/tools/github-release/index.d.ts +0 -2
  35. package/dist/tools/github-release/schema.d.ts +0 -9
  36. package/dist/tools/index.d.ts +0 -2
  37. package/dist/tools/submit-review/index.d.ts +0 -2
  38. package/dist/tools/submit-review/schema.d.ts +0 -24
  39. package/dist/tools/utils/retry.d.ts +0 -7
@@ -0,0 +1,223 @@
1
+ ---
2
+ name: pr-review
3
+ description: AI-powered pull request code review focusing on correctness, security, stability, and maintainability.
4
+ license: MIT
5
+ ---
6
+
7
+ ## What I Do
8
+
9
+ Review pull request changes systematically and post findings as a sticky PR comment. Focus on **real issues that matter** - bugs, security risks, and stability problems that would block a merge in a typical code review.
10
+
11
+ ## Review Philosophy
12
+
13
+ **Ask yourself before flagging any issue**: "Would a senior engineer in a time-limited code review raise this issue?"
14
+
15
+ - Flag issues that represent **real risks** or **actual bugs**
16
+ - Skip style preferences and subjective opinions
17
+ - Prioritize signal over noise - fewer, higher-quality findings
18
+
19
+ ## Workflow
20
+
21
+ 1. **Check prior feedback**: Fetch existing PR comments to understand context
22
+ ```bash
23
+ gh pr view <number> --json comments --jq '.comments[].body'
24
+ ```
25
+
26
+ 2. **Gather PR context**: Get PR metadata and changed files
27
+ ```bash
28
+ gh pr view <number> --json files,title,body,headRefOid
29
+ ```
30
+
31
+ 3. **Create validation todo list**: Track previously-flagged issues (if any) using `todowrite`
32
+ - One item per prior issue: "Validate if [issue] was addressed"
33
+ - One item per changed file for new review
34
+
35
+ 4. **Analyze each file**:
36
+ - Mark todo as `in_progress`
37
+ - Read the file diff and surrounding context
38
+ - Note ONLY issues that pass the "real issue" test (see Review Priorities)
39
+ - Mark todo as `completed`
40
+
41
+ 5. **Synthesize review**: After ALL files are analyzed, determine verdict and summary
42
+ - Acknowledge addressed feedback from prior reviews
43
+ - Only include NEW issues not previously identified
44
+
45
+ 6. **Post review**: Run the submit-review script (see Posting Review section)
46
+
47
+ ## Review Priorities
48
+
49
+ Focus on these areas. Each includes concrete examples of what IS and ISN'T worth flagging.
50
+
51
+ ### 1. Correctness (Logic errors that cause wrong behavior)
52
+
53
+ | Flag This | Skip This |
54
+ |-----------|-----------|
55
+ | Off-by-one errors in loops/bounds | Type confusion that doesn't break runtime |
56
+ | Broken control flow (early returns, missing breaks) | Unused variables (linters catch this) |
57
+ | Incorrect boolean logic | Missing optional chaining on guaranteed-present fields |
58
+ | Wrong comparison operators | Stylistic null checks beyond language guarantees |
59
+ | Missing await on async calls | |
60
+
61
+ ### 2. Security (Exploitable vulnerabilities)
62
+
63
+ | Flag This | Skip This |
64
+ |-----------|-----------|
65
+ | SQL/NoSQL injection | Hypothetical attack scenarios requiring admin access |
66
+ | XSS vulnerabilities | Missing rate limiting (unless auth-related) |
67
+ | Auth bypass possibilities | Theoretical timing attacks |
68
+ | Secrets/credentials in code | Generic "consider security implications" |
69
+ | Unsafe deserialization | |
70
+ | Path traversal | |
71
+
72
+ ### 3. Stability (Issues causing crashes or data loss)
73
+
74
+ | Flag This | Skip This |
75
+ |-----------|-----------|
76
+ | Unhandled promise rejections in critical paths | Adding extra try-catch "just in case" |
77
+ | Race conditions with visible effects | Defensive null checks on typed fields |
78
+ | Resource leaks (unclosed handles, listeners) | Missing error logging (non-critical) |
79
+ | Infinite loops / recursion without base case | Optional timeout additions |
80
+ | Division by zero possibilities | |
81
+
82
+ ### 4. Maintainability (Clarity issues causing confusion)
83
+
84
+ | Flag This | Skip This |
85
+ |-----------|-----------|
86
+ | Misleading function/variable names | Naming preferences (camelCase vs snake_case) |
87
+ | Logic that contradicts its documentation | Minor comment improvements |
88
+ | Dead code that appears intentional | DRY violations without maintenance risk |
89
+ | Inconsistency with adjacent code patterns | Subjective "this could be cleaner" |
90
+
91
+ ## Common Nitpicks to Avoid
92
+
93
+ **DO NOT flag these issues** - they create noise without value:
94
+
95
+ 1. **Coding style preferences** - Spaces vs tabs, quote styles, trailing commas, line length. Let linters handle this.
96
+
97
+ 2. **Defensive programming suggestions** - Adding null checks, optional chaining, or try-catch blocks beyond what the type system or runtime requires.
98
+
99
+ 3. **DRY violations without real cost** - Small code duplication that doesn't create maintenance burden. Not everything needs abstraction.
100
+
101
+ 4. **Missing code comments** - Unless the code is security-critical or intentionally non-obvious, don't require comments.
102
+
103
+ 5. **Subjective complexity concerns** - "This function is too long" or "Consider breaking this up" without identifying a concrete problem it causes.
104
+
105
+ 6. **Minor refactoring suggestions** - Aesthetic improvements, variable renaming preferences, or "cleaner" alternatives that aren't objectively better.
106
+
107
+ ## Context from Prior Reviews
108
+
109
+ Before starting your review:
110
+
111
+ 1. **Fetch existing PR comments** to see prior feedback:
112
+ ```bash
113
+ gh pr view <number> --json comments,reviews --jq '.comments[].body, .reviews[].body'
114
+ ```
115
+
116
+ 2. **If previous issues were flagged**:
117
+ - Create a todo item for each: "Validate: [previous issue title]"
118
+ - Check if the latest commits address each issue
119
+ - Mark as completed with status (resolved/still-present)
120
+
121
+ 3. **In your final review**:
122
+ - Acknowledge issues that were fixed: "Previously flagged X - now resolved"
123
+ - Only flag issues that are NEW or STILL UNRESOLVED
124
+ - Don't re-flag the same issue in slightly different words
125
+
126
+ ## Posting Review
127
+
128
+ The script path is provided in your task message. Run the submit-review script:
129
+
130
+ ```bash
131
+ bun "<script_path>/submit-review.ts" \
132
+ --repo "owner/repo" \
133
+ --pr 123 \
134
+ --commit "abc1234" \
135
+ --verdict "approve" \
136
+ --summary "Your overall assessment" \
137
+ --issues '[{"file":"src/foo.ts","line":42,"severity":"high","title":"Issue title","explanation":"Why this matters","suggestion":"Optional fix"}]'
138
+ ```
139
+
140
+ ### Arguments
141
+
142
+ | Argument | Required | Description |
143
+ |----------|----------|-------------|
144
+ | `--repo` | Yes | Repository in owner/repo format |
145
+ | `--pr` | Yes | Pull request number |
146
+ | `--commit` | No | Commit SHA (first 7 chars shown in review) |
147
+ | `--verdict` | Yes | `approve` or `request_changes` |
148
+ | `--summary` | Yes | 1-3 sentence overall assessment |
149
+ | `--issues` | No | JSON array of issues found |
150
+
151
+ ### Issue Format
152
+
153
+ Each issue in the array:
154
+
155
+ ```json
156
+ {
157
+ "file": "src/auth/login.ts",
158
+ "line": 42,
159
+ "severity": "critical|high|medium|low",
160
+ "title": "Short description (~80 chars)",
161
+ "explanation": "Why this matters, what's wrong",
162
+ "suggestion": "Optional: replacement code"
163
+ }
164
+ ```
165
+
166
+ ## Verdict Rules
167
+
168
+ **Only use `request_changes` for issues that should BLOCK the merge:**
169
+
170
+ - `critical`: Security vulnerabilities, data loss risks, complete feature breakage
171
+ - `high`: Bugs that will affect users, significant logic errors
172
+ - `medium`: Edge cases likely to cause issues, unclear but problematic patterns
173
+
174
+ **Use `approve` generously:**
175
+
176
+ - If issues are `low` severity only - approve with notes
177
+ - If issues are style/preference-based - approve (and don't flag them)
178
+ - If you're unsure whether something is a real issue - lean toward approve
179
+
180
+ **Severity guidance:**
181
+
182
+ - Default to `low` unless the issue clearly meets `medium` or higher criteria
183
+ - `medium` = "This will probably cause a bug in production"
184
+ - `high` = "This will definitely cause problems for users"
185
+ - `critical` = "This is a security hole or will cause data loss"
186
+
187
+ ## Common Mistakes to Avoid
188
+
189
+ - Do NOT run the script more than once per review
190
+ - Do NOT use line numbers from the left (old) side of the diff
191
+ - Do NOT skip the per-file todo workflow
192
+ - Do NOT guess repository, PR number, or commit SHA - derive from git/gh commands
193
+ - Do NOT re-flag issues from prior reviews that were already addressed
194
+ - Do NOT flag style issues that linters or formatters should handle
195
+
196
+ ## Example Issue
197
+
198
+ ```json
199
+ {
200
+ "file": "src/auth/login.ts",
201
+ "line": 42,
202
+ "severity": "high",
203
+ "title": "SQL injection vulnerability in user lookup",
204
+ "explanation": "User input is concatenated directly into the SQL query without sanitization. An attacker could inject malicious SQL to bypass authentication or extract data.",
205
+ "suggestion": "const user = await db.query('SELECT * FROM users WHERE email = $1', [email])"
206
+ }
207
+ ```
208
+
209
+ ## Example of What NOT to Flag
210
+
211
+ ```typescript
212
+ // DON'T flag: "Consider using optional chaining"
213
+ const name = user.profile.name; // If types guarantee profile exists, this is fine
214
+
215
+ // DON'T flag: "Variable could be const"
216
+ let count = 0; // Let linters handle this
217
+
218
+ // DON'T flag: "Function is too long"
219
+ function processOrder() { /* 80 lines */ } // Unless there's a concrete bug
220
+
221
+ // DON'T flag: "Consider adding error handling"
222
+ await saveUser(user); // Unless errors here would cause data loss
223
+ ```
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env bun
2
+ /// <reference types="bun-types" />
3
+
4
+ /**
5
+ * Usage: bun submit-review.ts --repo owner/repo --pr 123 --commit abc1234 \
6
+ * --verdict approve --summary "Clean code" --issues '[...]'
7
+ */
8
+
9
+ import { parseArgs } from "util";
10
+
11
+ const STICKY_MARKER = '<!-- open-workflows:review-sticky -->';
12
+
13
+ type Severity = 'critical' | 'high' | 'medium' | 'low';
14
+ type Verdict = 'approve' | 'request_changes';
15
+
16
+ interface ReviewIssue {
17
+ file: string;
18
+ line: number;
19
+ severity: Severity;
20
+ title: string;
21
+ explanation: string;
22
+ suggestion?: string;
23
+ }
24
+
25
+ async function withRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
26
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
27
+ try {
28
+ return await fn();
29
+ } catch (error) {
30
+ const isLast = attempt === maxRetries - 1;
31
+ const msg = error instanceof Error ? error.message : String(error);
32
+ const isRetryable = msg.includes('rate limit') || msg.includes('timeout') ||
33
+ msg.includes('503') || msg.includes('ECONNRESET');
34
+ if (isLast || !isRetryable) throw error;
35
+ await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
36
+ }
37
+ }
38
+ throw new Error('Max retries exceeded');
39
+ }
40
+
41
+ function formatVerdict(verdict: Verdict): string {
42
+ return verdict === 'request_changes' ? 'REQUEST CHANGES' : 'APPROVE';
43
+ }
44
+
45
+ function buildCommentBody(summary: string, verdict: Verdict, issues: ReviewIssue[], commitSha?: string): string {
46
+ let body = `## AI Review Summary\n\n`;
47
+ body += `**Verdict:** ${formatVerdict(verdict)}\n`;
48
+ if (commitSha) {
49
+ body += `**Commit:** \`${commitSha.slice(0, 7)}\`\n`;
50
+ }
51
+ body += '\n';
52
+
53
+ if (issues.length === 0) {
54
+ body += `### Findings\n\nNo significant issues found.\n\n`;
55
+ } else {
56
+ body += `### Findings\n\n`;
57
+ for (const issue of issues) {
58
+ const location = issue.file && issue.line > 0 ? `${issue.file}:${issue.line}` : issue.file || 'unknown';
59
+ body += `- **[${issue.severity.toUpperCase()}]** \`${location}\` – ${issue.title}\n`;
60
+ body += ` - ${issue.explanation}\n`;
61
+ if (issue.suggestion?.trim()) {
62
+ const suggestion = issue.suggestion.trim();
63
+ if (suggestion.includes('\n')) {
64
+ body += ` - **Suggested fix:**\n\n \`\`\`\n${suggestion.split('\n').map(l => ` ${l}`).join('\n')}\n \`\`\`\n`;
65
+ } else {
66
+ body += ` - **Suggested fix:** ${suggestion}\n`;
67
+ }
68
+ }
69
+ }
70
+ body += '\n';
71
+ }
72
+
73
+ body += `### Overall Assessment\n\n${summary}\n\n`;
74
+ body += STICKY_MARKER;
75
+ return body;
76
+ }
77
+
78
+ async function main() {
79
+ const { values } = parseArgs({
80
+ args: Bun.argv.slice(2),
81
+ options: {
82
+ repo: { type: 'string' },
83
+ pr: { type: 'string' },
84
+ commit: { type: 'string' },
85
+ verdict: { type: 'string' },
86
+ summary: { type: 'string' },
87
+ issues: { type: 'string' },
88
+ },
89
+ strict: true,
90
+ });
91
+
92
+ const { repo, pr, commit, verdict, summary, issues: issuesJson } = values;
93
+
94
+ if (!repo || !pr || !verdict || !summary) {
95
+ console.error('Missing required arguments: --repo, --pr, --verdict, --summary');
96
+ process.exit(1);
97
+ }
98
+
99
+ const prNumber = parseInt(pr, 10);
100
+ if (isNaN(prNumber)) {
101
+ console.error('Invalid PR number');
102
+ process.exit(1);
103
+ }
104
+
105
+ if (verdict !== 'approve' && verdict !== 'request_changes') {
106
+ console.error('Verdict must be "approve" or "request_changes"');
107
+ process.exit(1);
108
+ }
109
+
110
+ let issues: ReviewIssue[] = [];
111
+ if (issuesJson) {
112
+ try {
113
+ issues = JSON.parse(issuesJson);
114
+ } catch {
115
+ console.error('Invalid JSON for --issues');
116
+ process.exit(1);
117
+ }
118
+ }
119
+
120
+ const body = buildCommentBody(summary, verdict as Verdict, issues, commit);
121
+
122
+ let existingCommentId: number | null = null;
123
+ try {
124
+ const result = await withRetry(() =>
125
+ Bun.$`gh api /repos/${repo}/issues/${prNumber}/comments --paginate`.text()
126
+ );
127
+ const comments = JSON.parse(result) as Array<{ id: number; body: string }>;
128
+ const existing = comments.find(c => c.body.includes(STICKY_MARKER));
129
+ if (existing) existingCommentId = existing.id;
130
+ } catch (error) {
131
+ const msg = error instanceof Error ? error.message : String(error);
132
+ console.error('Warning: Failed to fetch existing comments:', msg);
133
+ }
134
+
135
+ const payload = JSON.stringify({ body });
136
+ const input = new Response(payload);
137
+
138
+ if (existingCommentId) {
139
+ await withRetry(() =>
140
+ Bun.$`gh api --method PATCH /repos/${repo}/issues/comments/${existingCommentId} --input - < ${input}`.quiet()
141
+ );
142
+ console.log('Updated existing review comment');
143
+ } else {
144
+ await withRetry(() =>
145
+ Bun.$`gh api --method POST /repos/${repo}/issues/${prNumber}/comments --input - < ${input}`.quiet()
146
+ );
147
+ console.log('Posted new review comment');
148
+ }
149
+ }
150
+
151
+ main().catch(err => {
152
+ console.error('Error:', err.message);
153
+ process.exit(1);
154
+ });
@@ -0,0 +1,59 @@
1
+ name: 'Release'
2
+ description: 'Automated releases with semantic versioning, changelog, and npm provenance'
3
+ author: 'activadee'
4
+
5
+ inputs:
6
+ bump:
7
+ description: 'Version bump type: major, minor, or patch'
8
+ required: false
9
+ version:
10
+ description: 'Override version (e.g., 1.2.3). Takes precedence over bump.'
11
+ required: false
12
+ skip-build:
13
+ description: 'Skip build step (if already built)'
14
+ required: false
15
+ default: 'false'
16
+
17
+ runs:
18
+ using: 'composite'
19
+ steps:
20
+ - name: Validate inputs
21
+ shell: bash
22
+ run: |
23
+ if [ -z "${{ inputs.bump }}" ] && [ -z "${{ inputs.version }}" ]; then
24
+ echo "Error: Either 'bump' or 'version' input is required"
25
+ exit 1
26
+ fi
27
+
28
+ - name: Setup Bun
29
+ uses: oven-sh/setup-bun@v2
30
+
31
+ - name: Setup Node.js
32
+ uses: actions/setup-node@v4
33
+ with:
34
+ node-version: '*'
35
+
36
+ - name: Upgrade npm for provenance support
37
+ shell: bash
38
+ run: npm install -g npm@latest
39
+
40
+ - name: Install dependencies
41
+ shell: bash
42
+ run: bun install --frozen-lockfile
43
+
44
+ - name: Run publish script
45
+ shell: bash
46
+ run: |
47
+ ARGS="--repo ${{ github.repository }}"
48
+ if [ -n "${{ inputs.version }}" ]; then
49
+ ARGS="$ARGS --version ${{ inputs.version }}"
50
+ elif [ -n "${{ inputs.bump }}" ]; then
51
+ ARGS="$ARGS --bump ${{ inputs.bump }}"
52
+ fi
53
+ if [ "${{ inputs.skip-build }}" = "true" ]; then
54
+ ARGS="$ARGS --skip-build"
55
+ fi
56
+ bun "${{ github.action_path }}/src/publish.ts" $ARGS
57
+ env:
58
+ CI: true
59
+ GITHUB_TOKEN: ${{ github.token }}
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env bun
2
+ /// <reference types="bun-types" />
3
+
4
+ import { parseArgs } from "util"
5
+
6
+ const PACKAGE_NAME = process.env.PACKAGE_NAME || ""
7
+
8
+ async function getCurrentNpmVersion(): Promise<string> {
9
+ if (!PACKAGE_NAME) {
10
+ const pkg = await Bun.file("package.json").json()
11
+ return pkg.version || "0.0.0"
12
+ }
13
+
14
+ try {
15
+ const result = await Bun.$`npm view ${PACKAGE_NAME} version`.text()
16
+ return result.trim()
17
+ } catch {
18
+ return "0.0.0"
19
+ }
20
+ }
21
+
22
+ function bumpVersion(version: string, type: "major" | "minor" | "patch"): string {
23
+ const [major, minor, patch] = version.split(".").map(Number)
24
+ switch (type) {
25
+ case "major":
26
+ return `${major + 1}.0.0`
27
+ case "minor":
28
+ return `${major}.${minor + 1}.0`
29
+ case "patch":
30
+ return `${major}.${minor}.${patch + 1}`
31
+ }
32
+ }
33
+
34
+ async function updatePackageVersion(newVersion: string): Promise<void> {
35
+ const pkgPath = "package.json"
36
+ let pkg = await Bun.file(pkgPath).text()
37
+ pkg = pkg.replace(/"version": "[^"]+"/, `"version": "${newVersion}"`)
38
+ await Bun.write(pkgPath, pkg)
39
+ console.log(`Updated package.json to v${newVersion}`)
40
+ }
41
+
42
+ async function generateChangelog(previousVersion: string): Promise<string[]> {
43
+ const notes: string[] = []
44
+
45
+ try {
46
+ const tagExists = await Bun.$`git rev-parse v${previousVersion} 2>/dev/null`.nothrow()
47
+ if (tagExists.exitCode !== 0) {
48
+ console.log("No previous tag found, skipping changelog")
49
+ return notes
50
+ }
51
+
52
+ const log = await Bun.$`git log v${previousVersion}..HEAD --oneline --format="%h %s"`.text()
53
+ const commits = log
54
+ .split("\n")
55
+ .filter((line) => line.trim())
56
+ .filter((line) => !line.match(/^\w+ (chore:|test:|ci:|docs:|release:|Merge )/i))
57
+
58
+ if (commits.length > 0) {
59
+ for (const commit of commits) {
60
+ notes.push(`- ${commit}`)
61
+ }
62
+ }
63
+ } catch (error) {
64
+ console.log("Failed to generate changelog:", error)
65
+ }
66
+
67
+ return notes
68
+ }
69
+
70
+ async function getContributors(previousVersion: string, repo: string): Promise<string[]> {
71
+ const notes: string[] = []
72
+ const excludeUsers = ["github-actions[bot]", "actions-user", "dependabot[bot]"]
73
+
74
+ try {
75
+ const tagExists = await Bun.$`git rev-parse v${previousVersion} 2>/dev/null`.nothrow()
76
+ if (tagExists.exitCode !== 0) return notes
77
+
78
+ const compare = await Bun.$`gh api "/repos/${repo}/compare/v${previousVersion}...HEAD" --jq '.commits[] | {login: .author.login, message: .commit.message}' 2>/dev/null`.text()
79
+ const contributors = new Map<string, string[]>()
80
+
81
+ for (const line of compare.split("\n").filter(Boolean)) {
82
+ try {
83
+ const { login, message } = JSON.parse(line) as { login: string | null; message: string }
84
+ const title = message.split("\n")[0] ?? ""
85
+ if (title.match(/^(chore:|test:|ci:|docs:|release:)/i)) continue
86
+
87
+ if (login && !excludeUsers.includes(login)) {
88
+ if (!contributors.has(login)) contributors.set(login, [])
89
+ contributors.get(login)?.push(title)
90
+ }
91
+ } catch {
92
+ continue
93
+ }
94
+ }
95
+
96
+ if (contributors.size > 0) {
97
+ notes.push("")
98
+ notes.push(`**Contributors:**`)
99
+ for (const [username] of contributors) {
100
+ notes.push(`- @${username}`)
101
+ }
102
+ }
103
+ } catch {}
104
+
105
+ return notes
106
+ }
107
+
108
+ async function buildProject(): Promise<void> {
109
+ console.log("\nBuilding project...")
110
+ await Bun.$`bun run build`
111
+ }
112
+
113
+ async function packProject(): Promise<string> {
114
+ console.log("\nPacking project...")
115
+ const output = await Bun.$`bun pm pack`.text()
116
+ const tarball = output.split("\n").find(line => line.trim().endsWith(".tgz"))?.trim()
117
+ if (!tarball) {
118
+ throw new Error("Failed to get tarball name from bun pm pack")
119
+ }
120
+ console.log(`Created: ${tarball}`)
121
+ return tarball
122
+ }
123
+
124
+ async function publishToNpm(tarball: string): Promise<void> {
125
+ console.log("\nPublishing to npm...")
126
+
127
+ if (process.env.CI) {
128
+ await Bun.$`npm publish ${tarball} --access public --provenance`
129
+ } else {
130
+ await Bun.$`npm publish ${tarball} --access public`
131
+ }
132
+ }
133
+
134
+ async function gitTagAndRelease(newVersion: string, notes: string[], repo: string): Promise<void> {
135
+ if (!process.env.CI) {
136
+ console.log("\nSkipping git operations (not in CI)")
137
+ return
138
+ }
139
+
140
+ console.log("\nCommitting and tagging...")
141
+ await Bun.$`git config user.email "github-actions[bot]@users.noreply.github.com"`
142
+ await Bun.$`git config user.name "github-actions[bot]"`
143
+ await Bun.$`git add package.json`
144
+
145
+ const hasStagedChanges = await Bun.$`git diff --cached --quiet`.nothrow()
146
+ if (hasStagedChanges.exitCode !== 0) {
147
+ await Bun.$`git commit -m "release: v${newVersion} [skip ci]"`
148
+ } else {
149
+ console.log("No changes to commit (version already updated)")
150
+ }
151
+
152
+ const tagExists = await Bun.$`git rev-parse v${newVersion} 2>/dev/null`.nothrow()
153
+ if (tagExists.exitCode !== 0) {
154
+ await Bun.$`git tag v${newVersion}`
155
+ } else {
156
+ console.log(`Tag v${newVersion} already exists`)
157
+ }
158
+
159
+ await Bun.$`git push origin HEAD --tags`
160
+
161
+ console.log("\nCreating GitHub release...")
162
+ const releaseNotes = notes.length > 0 ? notes.join("\n") : "No notable changes"
163
+ const releaseExists = await Bun.$`gh release view v${newVersion} 2>/dev/null`.nothrow()
164
+ if (releaseExists.exitCode !== 0) {
165
+ await Bun.$`gh release create v${newVersion} --title "v${newVersion}" --notes ${releaseNotes}`
166
+ } else {
167
+ console.log(`Release v${newVersion} already exists`)
168
+ }
169
+ }
170
+
171
+ async function main() {
172
+ const { values } = parseArgs({
173
+ args: Bun.argv.slice(2),
174
+ options: {
175
+ bump: { type: "string" },
176
+ version: { type: "string" },
177
+ repo: { type: "string" },
178
+ "skip-build": { type: "boolean", default: false },
179
+ },
180
+ strict: true,
181
+ })
182
+
183
+ const { bump, version: overrideVersion, repo, "skip-build": skipBuild } = values
184
+
185
+ if (!bump && !overrideVersion) {
186
+ console.error("Either --bump (major|minor|patch) or --version is required")
187
+ process.exit(1)
188
+ }
189
+
190
+ if (!repo) {
191
+ console.error("--repo is required (e.g., owner/repo)")
192
+ process.exit(1)
193
+ }
194
+
195
+ const currentVersion = await getCurrentNpmVersion()
196
+ console.log(`Current version: ${currentVersion}`)
197
+
198
+ let newVersion: string
199
+ if (overrideVersion) {
200
+ newVersion = overrideVersion.replace(/^v/, "")
201
+ } else if (bump === "major" || bump === "minor" || bump === "patch") {
202
+ newVersion = bumpVersion(currentVersion, bump)
203
+ } else {
204
+ console.error("Invalid bump type. Use major, minor, or patch")
205
+ process.exit(1)
206
+ }
207
+
208
+ console.log(`New version: ${newVersion}`)
209
+
210
+ await updatePackageVersion(newVersion)
211
+
212
+ const changelog = await generateChangelog(currentVersion)
213
+ const contributors = await getContributors(currentVersion, repo)
214
+ const notes = [...changelog, ...contributors]
215
+
216
+ if (notes.length > 0) {
217
+ console.log("\nRelease notes:")
218
+ console.log(notes.join("\n"))
219
+ }
220
+
221
+ if (!skipBuild) {
222
+ await buildProject()
223
+ }
224
+
225
+ const tarball = await packProject()
226
+ await publishToNpm(tarball)
227
+ await gitTagAndRelease(newVersion, notes, repo)
228
+
229
+ console.log(`\nRelease v${newVersion} complete!`)
230
+ }
231
+
232
+ main().catch((err) => {
233
+ console.error("Release failed:", err.message || err)
234
+ process.exit(1)
235
+ })