snipe-pr 1.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/.github/workflows/snipe-pr.yml +15 -0
- package/LICENSE +21 -0
- package/README.md +85 -0
- package/__tests__/analyzer.test.ts +107 -0
- package/action.yml +39 -0
- package/dist/index.js +32287 -0
- package/jest.config.js +5 -0
- package/package.json +24 -0
- package/src/ai.ts +60 -0
- package/src/analyzer.ts +273 -0
- package/src/index.ts +156 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
name: Snipe PR
|
|
2
|
+
on:
|
|
3
|
+
pull_request:
|
|
4
|
+
types: [opened, synchronize]
|
|
5
|
+
|
|
6
|
+
permissions:
|
|
7
|
+
pull-requests: write
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
describe:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: klawgulp-ship-it/snipe-pr@v1
|
|
14
|
+
with:
|
|
15
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SnipeLink LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Snipe PR โก
|
|
2
|
+
|
|
3
|
+
**Auto-generated PR descriptions that actually make sense.**
|
|
4
|
+
|
|
5
|
+
Snipe PR analyzes your pull request diffs and generates structured, readable descriptions โ so your team spends less time writing and more time shipping.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
Add to `.github/workflows/snipe-pr.yml`:
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
name: Snipe PR
|
|
13
|
+
on:
|
|
14
|
+
pull_request:
|
|
15
|
+
types: [opened, synchronize]
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
describe:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
permissions:
|
|
21
|
+
pull-requests: write
|
|
22
|
+
steps:
|
|
23
|
+
- uses: klawgulp-ship-it/snipe-pr@v1
|
|
24
|
+
with:
|
|
25
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
That's it. Every PR gets an auto-generated description with:
|
|
29
|
+
- Change type detection (feature, bugfix, refactor, docs, etc.)
|
|
30
|
+
- Smart file categorization (API, UI, Tests, Config, etc.)
|
|
31
|
+
- Visual stats bar showing additions vs deletions
|
|
32
|
+
- Key highlights (new files, large changes, renames)
|
|
33
|
+
|
|
34
|
+
## Example Output
|
|
35
|
+
|
|
36
|
+
> ## โจ **New Feature** โ 5 files changed (+120/-15)
|
|
37
|
+
>
|
|
38
|
+
> ### Highlights
|
|
39
|
+
> - Added 2 new files: `UserService.ts`, `types.ts`
|
|
40
|
+
> - Largest change: `UserService.ts` (+80/-0)
|
|
41
|
+
>
|
|
42
|
+
> ### Changes
|
|
43
|
+
> - ๐ **API**: `src/routes/users.ts`, `src/controllers/auth.ts`
|
|
44
|
+
> - ๐ผ๏ธ **UI Components**: `src/components/UserCard.tsx`
|
|
45
|
+
> - ๐งช **Tests**: `src/__tests__/users.test.ts`
|
|
46
|
+
> - โ๏ธ **Configuration**: `tsconfig.json`
|
|
47
|
+
>
|
|
48
|
+
> ๐ฉ๐ฉ๐ฉ๐ฉ๐ฉ๐ฉ๐ฉ๐ฉ๐ฉ๐ฉ๐ฉ๐ฉ๐ฉ๐ฉ๐ฉ๐ฉ๐ฉ๐ฉ๐ฅ๐ฅ +120 / -15 (net +105)
|
|
49
|
+
|
|
50
|
+
## Options
|
|
51
|
+
|
|
52
|
+
| Input | Description | Default |
|
|
53
|
+
|-------|-------------|---------|
|
|
54
|
+
| `github-token` | GitHub token for API access | `${{ github.token }}` |
|
|
55
|
+
| `snipelink-key` | SnipeLink API key for AI-powered descriptions | โ |
|
|
56
|
+
| `mode` | `comment` (PR comment) or `body` (update PR body) | `comment` |
|
|
57
|
+
| `include-stats` | Include file change statistics | `true` |
|
|
58
|
+
| `max-diff-size` | Max diff characters to analyze | `10000` |
|
|
59
|
+
|
|
60
|
+
## AI-Powered Descriptions (Pro)
|
|
61
|
+
|
|
62
|
+
Want smarter, context-aware descriptions? Add a SnipeLink API key:
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
- uses: klawgulp-ship-it/snipe-pr@v1
|
|
66
|
+
with:
|
|
67
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
68
|
+
snipelink-key: ${{ secrets.SNIPELINK_KEY }}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Get your API key at [snipelink.com/tools](https://snipelink.com/tools)
|
|
72
|
+
|
|
73
|
+
## How It Works
|
|
74
|
+
|
|
75
|
+
1. PR is opened or updated
|
|
76
|
+
2. Snipe PR reads the diff and changed files
|
|
77
|
+
3. Files are categorized by type (API, UI, Tests, etc.)
|
|
78
|
+
4. Change type is detected from diff content
|
|
79
|
+
5. A structured description is generated and posted
|
|
80
|
+
|
|
81
|
+
No external API calls needed for the free tier โ everything runs locally in the GitHub Action.
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
MIT โ by [SnipeLink LLC](https://snipelink.com)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { analyzeChanges, formatDescription, FileChange } from '../src/analyzer';
|
|
2
|
+
|
|
3
|
+
describe('analyzeChanges', () => {
|
|
4
|
+
it('detects feature changes', () => {
|
|
5
|
+
const files: FileChange[] = [
|
|
6
|
+
{ filename: 'src/newFeature.ts', status: 'added', additions: 50, deletions: 0 },
|
|
7
|
+
{ filename: 'src/index.ts', status: 'modified', additions: 5, deletions: 2 },
|
|
8
|
+
];
|
|
9
|
+
const diff = 'add new feature implement create';
|
|
10
|
+
const result = analyzeChanges(files, diff);
|
|
11
|
+
|
|
12
|
+
expect(result.changeType).toBe('feature');
|
|
13
|
+
expect(result.stats.filesChanged).toBe(2);
|
|
14
|
+
expect(result.stats.additions).toBe(55);
|
|
15
|
+
expect(result.stats.deletions).toBe(2);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('detects bugfix changes', () => {
|
|
19
|
+
const files: FileChange[] = [
|
|
20
|
+
{ filename: 'src/auth.ts', status: 'modified', additions: 3, deletions: 5 },
|
|
21
|
+
];
|
|
22
|
+
const diff = 'fix bug error crash issue';
|
|
23
|
+
const result = analyzeChanges(files, diff);
|
|
24
|
+
|
|
25
|
+
expect(result.changeType).toBe('bugfix');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('detects test-only changes', () => {
|
|
29
|
+
const files: FileChange[] = [
|
|
30
|
+
{ filename: 'src/__tests__/auth.test.ts', status: 'added', additions: 40, deletions: 0 },
|
|
31
|
+
{ filename: 'src/auth.spec.ts', status: 'modified', additions: 10, deletions: 5 },
|
|
32
|
+
];
|
|
33
|
+
const result = analyzeChanges(files, '');
|
|
34
|
+
|
|
35
|
+
expect(result.changeType).toBe('test');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('detects docs-only changes', () => {
|
|
39
|
+
const files: FileChange[] = [
|
|
40
|
+
{ filename: 'README.md', status: 'modified', additions: 20, deletions: 5 },
|
|
41
|
+
{ filename: 'docs/setup.md', status: 'added', additions: 50, deletions: 0 },
|
|
42
|
+
];
|
|
43
|
+
const result = analyzeChanges(files, '');
|
|
44
|
+
|
|
45
|
+
expect(result.changeType).toBe('docs');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('categorizes files correctly', () => {
|
|
49
|
+
const files: FileChange[] = [
|
|
50
|
+
{ filename: 'src/components/Button.tsx', status: 'modified', additions: 10, deletions: 5 },
|
|
51
|
+
{ filename: 'src/api/routes/users.ts', status: 'added', additions: 30, deletions: 0 },
|
|
52
|
+
{ filename: 'package.json', status: 'modified', additions: 2, deletions: 1 },
|
|
53
|
+
{ filename: 'src/__tests__/button.test.tsx', status: 'added', additions: 20, deletions: 0 },
|
|
54
|
+
];
|
|
55
|
+
const result = analyzeChanges(files, '');
|
|
56
|
+
|
|
57
|
+
const categoryNames = result.categories.map((c) => c.name);
|
|
58
|
+
expect(categoryNames).toContain('Tests');
|
|
59
|
+
expect(categoryNames).toContain('Configuration');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('highlights new files', () => {
|
|
63
|
+
const files: FileChange[] = [
|
|
64
|
+
{ filename: 'src/newService.ts', status: 'added', additions: 100, deletions: 0 },
|
|
65
|
+
{ filename: 'src/types.ts', status: 'added', additions: 30, deletions: 0 },
|
|
66
|
+
];
|
|
67
|
+
const result = analyzeChanges(files, '');
|
|
68
|
+
|
|
69
|
+
expect(result.highlights.some((h) => h.includes('Added 2 new files'))).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('highlights large changes', () => {
|
|
73
|
+
const files: FileChange[] = [
|
|
74
|
+
{ filename: 'src/bigRefactor.ts', status: 'modified', additions: 200, deletions: 150 },
|
|
75
|
+
];
|
|
76
|
+
const result = analyzeChanges(files, '');
|
|
77
|
+
|
|
78
|
+
expect(result.highlights.some((h) => h.includes('Largest change'))).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('formatDescription', () => {
|
|
83
|
+
it('generates valid markdown', () => {
|
|
84
|
+
const files: FileChange[] = [
|
|
85
|
+
{ filename: 'src/index.ts', status: 'modified', additions: 10, deletions: 5 },
|
|
86
|
+
];
|
|
87
|
+
const analysis = analyzeChanges(files, 'fix a bug in auth');
|
|
88
|
+
const md = formatDescription(analysis, 'Fix auth bug');
|
|
89
|
+
|
|
90
|
+
expect(md).toContain('##');
|
|
91
|
+
expect(md).toContain('Changes');
|
|
92
|
+
expect(md).toContain('Snipe PR');
|
|
93
|
+
expect(md).toContain('snipelink.com');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('includes stats bar', () => {
|
|
97
|
+
const files: FileChange[] = [
|
|
98
|
+
{ filename: 'src/a.ts', status: 'modified', additions: 50, deletions: 10 },
|
|
99
|
+
];
|
|
100
|
+
const analysis = analyzeChanges(files, '');
|
|
101
|
+
const md = formatDescription(analysis, 'Test');
|
|
102
|
+
|
|
103
|
+
expect(md).toContain('๐ฉ');
|
|
104
|
+
expect(md).toContain('+50');
|
|
105
|
+
expect(md).toContain('-10');
|
|
106
|
+
});
|
|
107
|
+
});
|
package/action.yml
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: 'Snipe PR โ AI PR Descriptions'
|
|
2
|
+
author: 'SnipeLink LLC'
|
|
3
|
+
description: 'Auto-generates smart PR descriptions from your diffs. Free tier included, AI-powered with SnipeLink Pro.'
|
|
4
|
+
|
|
5
|
+
inputs:
|
|
6
|
+
github-token:
|
|
7
|
+
description: 'GitHub token for API access (usually secrets.GITHUB_TOKEN)'
|
|
8
|
+
required: true
|
|
9
|
+
default: ${{ github.token }}
|
|
10
|
+
snipelink-key:
|
|
11
|
+
description: 'SnipeLink API key for AI-powered descriptions (optional, get one at snipelink.com)'
|
|
12
|
+
required: false
|
|
13
|
+
default: ''
|
|
14
|
+
mode:
|
|
15
|
+
description: 'Output mode: "comment" posts a PR comment, "body" updates the PR body'
|
|
16
|
+
required: false
|
|
17
|
+
default: 'comment'
|
|
18
|
+
include-stats:
|
|
19
|
+
description: 'Include file change statistics'
|
|
20
|
+
required: false
|
|
21
|
+
default: 'true'
|
|
22
|
+
max-diff-size:
|
|
23
|
+
description: 'Max diff characters to analyze (larger = more detail)'
|
|
24
|
+
required: false
|
|
25
|
+
default: '10000'
|
|
26
|
+
|
|
27
|
+
outputs:
|
|
28
|
+
description:
|
|
29
|
+
description: 'The generated PR description'
|
|
30
|
+
comment-id:
|
|
31
|
+
description: 'ID of the posted comment (if mode=comment)'
|
|
32
|
+
|
|
33
|
+
runs:
|
|
34
|
+
using: 'node20'
|
|
35
|
+
main: 'dist/index.js'
|
|
36
|
+
|
|
37
|
+
branding:
|
|
38
|
+
icon: 'zap'
|
|
39
|
+
color: 'green'
|