@theihtisham/ai-release-notes 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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1493 -0
  3. package/__tests__/analyzer.test.js +63 -0
  4. package/__tests__/categorizer.test.js +93 -0
  5. package/__tests__/config.test.js +92 -0
  6. package/__tests__/formatter.test.js +63 -0
  7. package/__tests__/formatters.test.js +394 -0
  8. package/__tests__/migration.test.js +322 -0
  9. package/__tests__/semver.test.js +94 -0
  10. package/__tests__/tones.test.js +252 -0
  11. package/action.yml +113 -0
  12. package/index.js +73 -0
  13. package/jest.config.js +10 -0
  14. package/package.json +41 -0
  15. package/src/ai-writer.js +108 -0
  16. package/src/analyzer.js +232 -0
  17. package/src/analyzers/migration.js +355 -0
  18. package/src/categorizer.js +182 -0
  19. package/src/config.js +162 -0
  20. package/src/constants.js +137 -0
  21. package/src/contributor.js +144 -0
  22. package/src/diff-analyzer.js +202 -0
  23. package/src/formatter.js +336 -0
  24. package/src/formatters/discord.js +174 -0
  25. package/src/formatters/html.js +195 -0
  26. package/src/formatters/index.js +42 -0
  27. package/src/formatters/markdown.js +123 -0
  28. package/src/formatters/slack.js +176 -0
  29. package/src/formatters/twitter.js +242 -0
  30. package/src/formatters/types.js +48 -0
  31. package/src/generator.js +297 -0
  32. package/src/integrations/changelog.js +125 -0
  33. package/src/integrations/discord.js +96 -0
  34. package/src/integrations/github-release.js +75 -0
  35. package/src/integrations/indexer.js +119 -0
  36. package/src/integrations/slack.js +112 -0
  37. package/src/integrations/twitter.js +128 -0
  38. package/src/logger.js +52 -0
  39. package/src/prompts.js +210 -0
  40. package/src/rate-limiter.js +92 -0
  41. package/src/semver.js +129 -0
  42. package/src/tones/casual.js +114 -0
  43. package/src/tones/humorous.js +164 -0
  44. package/src/tones/index.js +38 -0
  45. package/src/tones/professional.js +125 -0
  46. package/src/tones/technical.js +164 -0
  47. package/src/tones/types.js +26 -0
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ const { analyzeCommits } = require('../src/analyzer');
4
+ const { DEFAULT_CATEGORIES } = require('../src/constants');
5
+
6
+ describe('analyzer', () => {
7
+ describe('analyzeCommits', () => {
8
+ test('returns empty analysis for no commits', () => {
9
+ const result = analyzeCommits([], { categories: DEFAULT_CATEGORIES });
10
+ expect(result.stats.total_commits).toBe(0);
11
+ expect(result.categories).toEqual({});
12
+ expect(result.contributors).toEqual([]);
13
+ });
14
+
15
+ test('returns empty analysis for null commits', () => {
16
+ const result = analyzeCommits(null, { categories: DEFAULT_CATEGORIES });
17
+ expect(result.stats.total_commits).toBe(0);
18
+ });
19
+
20
+ test('categorizes conventional commits', () => {
21
+ const commits = [
22
+ { sha: 'abc123', commit: { message: 'feat: add new login page', author: { name: 'Alice', email: 'alice@test.com' } } },
23
+ { sha: 'def456', commit: { message: 'fix: resolve login timeout', author: { name: 'Bob', email: 'bob@test.com' } } },
24
+ ];
25
+ const result = analyzeCommits(commits, { categories: DEFAULT_CATEGORIES });
26
+ expect(result.stats.total_commits).toBe(2);
27
+ expect(result.contributors.length).toBeGreaterThan(0);
28
+ });
29
+
30
+ test('detects breaking changes', () => {
31
+ const commits = [
32
+ { sha: 'abc123', commit: { message: 'feat!: completely new API\n\nBREAKING CHANGE: old API removed', author: { name: 'Alice', email: 'alice@test.com' } } },
33
+ ];
34
+ const result = analyzeCommits(commits, { categories: DEFAULT_CATEGORIES });
35
+ expect(result.breaking.length).toBeGreaterThan(0);
36
+ });
37
+
38
+ test('extracts scopes from commits', () => {
39
+ const commits = [
40
+ { sha: 'abc123', commit: { message: 'feat(auth): add OAuth support', author: { name: 'Alice', email: 'alice@test.com' } } },
41
+ ];
42
+ const result = analyzeCommits(commits, { categories: DEFAULT_CATEGORIES });
43
+ expect(result.stats.total_commits).toBe(1);
44
+ });
45
+
46
+ test('handles commits without author info', () => {
47
+ const commits = [
48
+ { sha: 'abc123', commit: { message: 'chore: update deps' } },
49
+ ];
50
+ const result = analyzeCommits(commits, { categories: DEFAULT_CATEGORIES });
51
+ expect(result.stats.total_commits).toBe(1);
52
+ });
53
+
54
+ test('counts total commits correctly', () => {
55
+ const commits = Array.from({ length: 10 }, (_, i) => ({
56
+ sha: `abc${i}`,
57
+ commit: { message: `feat: feature ${i}`, author: { name: 'Dev', email: 'dev@test.com' } },
58
+ }));
59
+ const result = analyzeCommits(commits, { categories: DEFAULT_CATEGORIES });
60
+ expect(result.stats.total_commits).toBe(10);
61
+ });
62
+ });
63
+ });
@@ -0,0 +1,93 @@
1
+ 'use strict';
2
+
3
+ const { categorizeCommits, deduplicateChanges } = require('../src/categorizer');
4
+ const { DEFAULT_CATEGORIES } = require('../src/constants');
5
+
6
+ describe('categorizer', () => {
7
+ describe('categorizeCommits', () => {
8
+ test('returns empty categories for no commits', () => {
9
+ const result = categorizeCommits([], DEFAULT_CATEGORIES);
10
+ expect(Object.keys(result).length).toBeGreaterThanOrEqual(0);
11
+ });
12
+
13
+ test('categorizes feat commits', () => {
14
+ const commits = [
15
+ { sha: 'abc123', commit: { message: 'feat: add new dashboard' } },
16
+ ];
17
+ const result = categorizeCommits(commits, DEFAULT_CATEGORIES);
18
+ const allChanges = Object.values(result).flat();
19
+ expect(allChanges.length).toBeGreaterThan(0);
20
+ });
21
+
22
+ test('categorizes fix commits', () => {
23
+ const commits = [
24
+ { sha: 'abc123', commit: { message: 'fix: resolve crash on startup' } },
25
+ ];
26
+ const result = categorizeCommits(commits, DEFAULT_CATEGORIES);
27
+ const allChanges = Object.values(result).flat();
28
+ expect(allChanges.length).toBeGreaterThan(0);
29
+ });
30
+
31
+ test('handles non-conventional commits as Other', () => {
32
+ const commits = [
33
+ { sha: 'abc123', commit: { message: 'updated something' } },
34
+ ];
35
+ const result = categorizeCommits(commits, DEFAULT_CATEGORIES);
36
+ const allChanges = Object.values(result).flat();
37
+ expect(allChanges.length).toBeGreaterThan(0);
38
+ });
39
+
40
+ test('handles commit with message directly on object', () => {
41
+ const commits = [
42
+ { sha: 'abc123', message: 'feat: direct message field' },
43
+ ];
44
+ const result = categorizeCommits(commits, DEFAULT_CATEGORIES);
45
+ const allChanges = Object.values(result).flat();
46
+ expect(allChanges.length).toBeGreaterThan(0);
47
+ });
48
+
49
+ test('removes empty categories', () => {
50
+ const commits = [
51
+ { sha: 'abc123', commit: { message: 'feat: only feature' } },
52
+ ];
53
+ const result = categorizeCommits(commits, DEFAULT_CATEGORIES);
54
+ for (const changes of Object.values(result)) {
55
+ expect(changes.length).toBeGreaterThan(0);
56
+ }
57
+ });
58
+ });
59
+
60
+ describe('deduplicateChanges', () => {
61
+ test('removes duplicate descriptions across categories', () => {
62
+ const categorized = {
63
+ 'Features': [
64
+ { description: 'same thing', commit: 'abc' },
65
+ { description: 'new feature', commit: 'ghi' },
66
+ ],
67
+ 'Bug Fixes': [
68
+ { description: 'same thing', commit: 'def' },
69
+ ],
70
+ };
71
+ const result = deduplicateChanges(categorized);
72
+ const allChanges = Object.values(result).flat();
73
+ const descs = allChanges.map(c => c.description.toLowerCase().trim());
74
+ const uniqueDescs = [...new Set(descs)];
75
+ expect(uniqueDescs.length).toBe(descs.length);
76
+ });
77
+
78
+ test('returns all unique changes', () => {
79
+ const categorized = {
80
+ 'Features': [{ description: 'a', commit: '1' }],
81
+ 'Bug Fixes': [{ description: 'b', commit: '2' }],
82
+ };
83
+ const result = deduplicateChanges(categorized);
84
+ const allChanges = Object.values(result).flat();
85
+ expect(allChanges.length).toBeGreaterThanOrEqual(2);
86
+ });
87
+
88
+ test('handles empty object', () => {
89
+ const result = deduplicateChanges({});
90
+ expect(Object.keys(result).length).toBe(0);
91
+ });
92
+ });
93
+ });
@@ -0,0 +1,92 @@
1
+ 'use strict';
2
+
3
+ const { createConfig } = require('../src/config');
4
+
5
+ describe('config', () => {
6
+ const validInputs = {
7
+ 'github-token': 'ghp_test123',
8
+ };
9
+
10
+ test('creates config with defaults', () => {
11
+ const config = createConfig(validInputs);
12
+ expect(config).toBeTruthy();
13
+ expect(config.githubToken).toBe('ghp_test123');
14
+ });
15
+
16
+ test('throws for missing github token', () => {
17
+ expect(() => createConfig({})).toThrow();
18
+ });
19
+
20
+ test('throws for empty github token', () => {
21
+ expect(() => createConfig({ 'github-token': '' })).toThrow();
22
+ });
23
+
24
+ test('accepts valid template', () => {
25
+ const config = createConfig({ ...validInputs, template: 'default' });
26
+ expect(config).toBeTruthy();
27
+ expect(config.template).toBe('default');
28
+ });
29
+
30
+ test('sets default template when not provided', () => {
31
+ const config = createConfig(validInputs);
32
+ expect(config.template).toBe('default');
33
+ });
34
+
35
+ test('accepts valid commit mode', () => {
36
+ const config = createConfig({ ...validInputs, 'commit-mode': 'auto' });
37
+ expect(config.commitMode).toBe('auto');
38
+ });
39
+
40
+ test('accepts valid language', () => {
41
+ const config = createConfig({ ...validInputs, language: 'en' });
42
+ expect(config.language).toBe('en');
43
+ });
44
+
45
+ test('parses max commits', () => {
46
+ const config = createConfig({ ...validInputs, 'max-commits': '50' });
47
+ expect(config.maxCommits).toBe(50);
48
+ });
49
+
50
+ test('uses default max commits when not provided', () => {
51
+ const config = createConfig(validInputs);
52
+ expect(config.maxCommits).toBe(200);
53
+ });
54
+
55
+ test('parses boolean include-breaking', () => {
56
+ const config = createConfig({ ...validInputs, 'include-breaking': 'false' });
57
+ expect(config.includeBreaking).toBe(false);
58
+ });
59
+
60
+ test('parses boolean dry-run', () => {
61
+ const config = createConfig({ ...validInputs, 'dry-run': 'true' });
62
+ expect(config.dryRun).toBe(true);
63
+ });
64
+
65
+ test('defaults dry-run to false', () => {
66
+ const config = createConfig(validInputs);
67
+ expect(config.dryRun).toBe(false);
68
+ });
69
+
70
+ test('throws for invalid commit mode', () => {
71
+ expect(() => createConfig({ ...validInputs, 'commit-mode': 'invalid' })).toThrow();
72
+ });
73
+
74
+ test('throws for invalid language', () => {
75
+ expect(() => createConfig({ ...validInputs, language: 'xx' })).toThrow();
76
+ });
77
+
78
+ test('detects AI availability', () => {
79
+ const config = createConfig({ ...validInputs, 'api-key': 'sk-test' });
80
+ expect(config.hasAI).toBe(true);
81
+ });
82
+
83
+ test('detects no AI when api-key missing', () => {
84
+ const config = createConfig(validInputs);
85
+ expect(config.hasAI).toBe(false);
86
+ });
87
+
88
+ test('returns frozen object', () => {
89
+ const config = createConfig(validInputs);
90
+ expect(Object.isFrozen(config)).toBe(true);
91
+ });
92
+ });
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ const { formatReleaseNotes, generateSummary } = require('../src/formatter');
4
+
5
+ describe('formatter', () => {
6
+ describe('formatReleaseNotes', () => {
7
+ test('formats release notes with version', () => {
8
+ const analysis = {
9
+ categories: {},
10
+ breaking: [],
11
+ contributors: [],
12
+ stats: { total_commits: 2 },
13
+ };
14
+ const diffSummary = {};
15
+ const result = formatReleaseNotes(analysis, diffSummary, '1.0.0', '0.9.0', {});
16
+ expect(result).toContain('1.0.0');
17
+ expect(typeof result).toBe('string');
18
+ expect(result.length).toBeGreaterThan(0);
19
+ });
20
+
21
+ test('handles empty analysis with default version', () => {
22
+ const analysis = {
23
+ categories: {},
24
+ breaking: [],
25
+ contributors: [],
26
+ stats: { total_commits: 0 },
27
+ };
28
+ const result = formatReleaseNotes(analysis, {}, '', '', {});
29
+ expect(typeof result).toBe('string');
30
+ expect(result.length).toBeGreaterThan(0);
31
+ });
32
+
33
+ test('uses v0.0.0 when no version provided', () => {
34
+ const analysis = {
35
+ categories: {},
36
+ breaking: [],
37
+ contributors: [],
38
+ stats: { total_commits: 0 },
39
+ };
40
+ const result = formatReleaseNotes(analysis, {}, '', '', {});
41
+ expect(result).toContain('0.0.0');
42
+ });
43
+ });
44
+
45
+ describe('generateSummary', () => {
46
+ test('generates summary from analysis', () => {
47
+ const analysis = {
48
+ stats: { total_commits: 10, additions: 500, deletions: 200 },
49
+ categories: { 'Features': [{}, {}], 'Bug Fixes': [{}] },
50
+ breaking: [],
51
+ contributors: [{ name: 'Alice' }, { name: 'Bob' }],
52
+ };
53
+ const summary = generateSummary(analysis, '1.0.0');
54
+ expect(typeof summary).toBe('string');
55
+ expect(summary.length).toBeGreaterThan(0);
56
+ });
57
+
58
+ test('handles null analysis', () => {
59
+ const summary = generateSummary(null, '1.0.0');
60
+ expect(typeof summary).toBe('string');
61
+ });
62
+ });
63
+ });