@activade/open-workflows 1.0.2 → 2.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.
- package/README.md +91 -64
- package/actions/doc-sync/action.yml +42 -0
- package/actions/doc-sync/skill.md +97 -0
- package/actions/issue-label/action.yml +36 -0
- package/actions/issue-label/skill.md +113 -0
- package/actions/issue-label/src/apply-labels.ts +118 -0
- package/actions/pr-review/action.yml +36 -0
- package/actions/pr-review/skill.md +223 -0
- package/actions/pr-review/src/submit-review.ts +154 -0
- package/actions/release/action.yml +59 -0
- package/actions/release/src/publish.ts +235 -0
- package/dist/cli/index.js +133 -570
- package/package.json +11 -21
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/installer.d.ts +0 -24
- package/dist/cli/templates/auth.d.ts +0 -1
- package/dist/cli/templates/doc-sync.d.ts +0 -1
- package/dist/cli/templates/index.d.ts +0 -7
- package/dist/cli/templates/issue-label.d.ts +0 -1
- package/dist/cli/templates/pr-review.d.ts +0 -1
- package/dist/cli/templates/release.d.ts +0 -1
- package/dist/cli/templates/shared.d.ts +0 -3
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -62
- package/dist/skills/doc-sync.d.ts +0 -1
- package/dist/skills/index.d.ts +0 -10
- package/dist/skills/issue-label.d.ts +0 -1
- package/dist/skills/pr-review.d.ts +0 -1
- package/dist/skills/release-notes.d.ts +0 -1
- package/dist/tools/apply-labels/index.d.ts +0 -2
- package/dist/tools/apply-labels/schema.d.ts +0 -12
- package/dist/tools/bun-release/index.d.ts +0 -2
- package/dist/tools/bun-release/schema.d.ts +0 -4
- package/dist/tools/github-release/index.d.ts +0 -2
- package/dist/tools/github-release/schema.d.ts +0 -9
- package/dist/tools/index.d.ts +0 -2
- package/dist/tools/submit-review/index.d.ts +0 -2
- package/dist/tools/submit-review/schema.d.ts +0 -24
- 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}`.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}`.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}'`.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}`.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}`.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
|
+
})
|