@link-assistant/web-capture 1.1.2 → 1.7.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 (60) hide show
  1. package/CHANGELOG.md +57 -13
  2. package/Dockerfile +26 -21
  3. package/README.md +290 -160
  4. package/bin/web-capture.js +622 -17
  5. package/package.json +15 -4
  6. package/src/animation.js +283 -0
  7. package/src/archive.js +225 -0
  8. package/src/batch.js +180 -0
  9. package/src/browser.js +150 -111
  10. package/src/docx.js +182 -0
  11. package/src/extract-images.js +120 -0
  12. package/src/figures.js +208 -0
  13. package/src/gdocs.js +335 -0
  14. package/src/image.js +65 -8
  15. package/src/index.js +15 -0
  16. package/src/latex.js +127 -0
  17. package/src/lib.js +281 -1
  18. package/src/localize-images.js +202 -0
  19. package/src/markdown.js +7 -2
  20. package/src/metadata.js +333 -0
  21. package/src/pdf.js +76 -0
  22. package/src/popups.js +171 -0
  23. package/src/postprocess.js +263 -0
  24. package/src/retry.js +48 -0
  25. package/src/themed-image.js +167 -0
  26. package/src/verify.js +312 -0
  27. package/tests/e2e/docker.test.js +51 -12
  28. package/tests/e2e/process.test.js +34 -10
  29. package/tests/fixtures/habr-articles.js +45 -0
  30. package/tests/integration/api-endpoints.test.js +231 -0
  31. package/tests/integration/habr-article.test.js +337 -0
  32. package/tests/mock/index.test.js +5 -3
  33. package/tests/unit/animation.test.js +33 -0
  34. package/tests/unit/batch.test.js +114 -0
  35. package/tests/unit/browser.test.js +38 -2
  36. package/tests/unit/extract-images.test.js +233 -0
  37. package/tests/unit/figures.test.js +74 -0
  38. package/tests/unit/gdocs.test.js +278 -0
  39. package/tests/unit/heading-image.test.js +49 -0
  40. package/tests/unit/latex.test.js +123 -0
  41. package/tests/unit/localize-images.test.js +78 -0
  42. package/tests/unit/metadata.test.js +140 -0
  43. package/tests/unit/postprocess.test.js +110 -0
  44. package/tests/unit/retry.test.js +111 -0
  45. package/tests/unit/verify.test.js +182 -0
  46. package/.github/workflows/release.yml +0 -294
  47. package/ARCHITECTURE.md +0 -982
  48. package/LICENSE +0 -24
  49. package/apply.sh +0 -166
  50. package/docs/market-analysis-2025-05-13.md +0 -64
  51. package/scripts/changeset-version.mjs +0 -38
  52. package/scripts/create-github-release.mjs +0 -93
  53. package/scripts/create-manual-changeset.mjs +0 -88
  54. package/scripts/format-github-release.mjs +0 -83
  55. package/scripts/format-release-notes.mjs +0 -218
  56. package/scripts/instant-version-bump.mjs +0 -121
  57. package/scripts/publish-to-npm.mjs +0 -128
  58. package/scripts/setup-npm.mjs +0 -37
  59. package/scripts/validate-changeset.mjs +0 -106
  60. package/scripts/version-and-commit.mjs +0 -237
@@ -0,0 +1,110 @@
1
+ import {
2
+ postProcessMarkdown,
3
+ applyUnicodeNormalization,
4
+ applyLatexSpacingFixes,
5
+ applyPercentSignFix,
6
+ applyBoldFormattingFixes,
7
+ } from '../../src/postprocess.js';
8
+
9
+ describe('postprocess module', () => {
10
+ describe('applyUnicodeNormalization', () => {
11
+ it('preserves non-breaking spaces as   entities', () => {
12
+ expect(applyUnicodeNormalization('hello\u00A0world')).toBe(
13
+ 'hello world'
14
+ );
15
+ });
16
+
17
+ it('normalizes curly quotes to straight quotes', () => {
18
+ expect(applyUnicodeNormalization('\u2018hello\u2019')).toBe("'hello'");
19
+ expect(applyUnicodeNormalization('\u201Chello\u201D')).toBe('"hello"');
20
+ });
21
+
22
+ it('normalizes en-dash to hyphen', () => {
23
+ expect(applyUnicodeNormalization('2\u20133')).toBe('2-3');
24
+ });
25
+
26
+ it('normalizes ellipsis', () => {
27
+ expect(applyUnicodeNormalization('wait\u2026')).toBe('wait...');
28
+ });
29
+ });
30
+
31
+ describe('applyLatexSpacingFixes', () => {
32
+ it('adds space before formula after word character', () => {
33
+ const result = applyLatexSpacingFixes('where$x$is');
34
+ expect(result).toBe('where $x$ is');
35
+ });
36
+
37
+ it('trims whitespace inside formula delimiters', () => {
38
+ const result = applyLatexSpacingFixes('$ x + y $');
39
+ expect(result).toBe('$x + y$');
40
+ });
41
+
42
+ it('does not modify block formulas', () => {
43
+ const result = applyLatexSpacingFixes('$$x + y$$');
44
+ expect(result).toBe('$$x + y$$');
45
+ });
46
+
47
+ it('does not modify blockquote block formulas', () => {
48
+ const result = applyLatexSpacingFixes('> $$x + y$$');
49
+ expect(result).toBe('> $$x + y$$');
50
+ });
51
+
52
+ it('handles multiple formulas on one line', () => {
53
+ const result = applyLatexSpacingFixes('$a$and$b$');
54
+ expect(result).toBe('$a$ and $b$');
55
+ });
56
+ });
57
+
58
+ describe('applyPercentSignFix', () => {
59
+ it('fixes percent sign in inline formulas', () => {
60
+ const result = applyPercentSignFix('$50\\%$');
61
+ expect(result).toBe('$50\\\\%$');
62
+ });
63
+
64
+ it('fixes \\text{%} notation', () => {
65
+ const result = applyPercentSignFix('$50\\text{%}$');
66
+ expect(result).toBe('$50\\\\%$');
67
+ });
68
+ });
69
+
70
+ describe('applyBoldFormattingFixes', () => {
71
+ it('removes empty bold markers', () => {
72
+ const result = applyBoldFormattingFixes('hello ** ** world');
73
+ expect(result).toBe('hello world');
74
+ });
75
+
76
+ it('trims content inside bold markers', () => {
77
+ const result = applyBoldFormattingFixes('** hello **');
78
+ expect(result).toBe('**hello**');
79
+ });
80
+
81
+ it('adds space between word and bold', () => {
82
+ const result = applyBoldFormattingFixes('word**bold**next');
83
+ expect(result).toBe('word **bold** next');
84
+ });
85
+ });
86
+
87
+ describe('postProcessMarkdown', () => {
88
+ it('applies all transformations by default', () => {
89
+ const input = 'hello\u00A0$x$world';
90
+ const result = postProcessMarkdown(input);
91
+ expect(result).toContain('hello');
92
+ expect(result).toContain('$x$');
93
+ expect(result).not.toContain('\u00A0');
94
+ });
95
+
96
+ it('respects option flags', () => {
97
+ const input = 'hello\u00A0world';
98
+ const result = postProcessMarkdown(input, {
99
+ normalizeUnicode: false,
100
+ });
101
+ expect(result).toContain('\u00A0');
102
+ });
103
+
104
+ it('removes stray standalone $ signs', () => {
105
+ const input = 'line1\n$\nline2';
106
+ const result = postProcessMarkdown(input);
107
+ expect(result).toBe('line1\n\nline2');
108
+ });
109
+ });
110
+ });
@@ -0,0 +1,111 @@
1
+ import { jest } from '@jest/globals';
2
+ import { retry } from '../../src/retry.js';
3
+
4
+ jest.setTimeout(30000);
5
+
6
+ describe('retry', () => {
7
+ it('returns result on first success', async () => {
8
+ const fn = jest.fn().mockResolvedValue('ok');
9
+ const result = await retry(fn, { retries: 3 });
10
+ expect(result).toBe('ok');
11
+ expect(fn).toHaveBeenCalledTimes(1);
12
+ });
13
+
14
+ it('retries on failure and succeeds eventually', async () => {
15
+ let calls = 0;
16
+ const fn = jest.fn().mockImplementation(async () => {
17
+ calls++;
18
+ if (calls < 3) {
19
+ throw new Error('fail');
20
+ }
21
+ return 'success';
22
+ });
23
+
24
+ const result = await retry(fn, { retries: 3, baseDelay: 50 });
25
+ expect(result).toBe('success');
26
+ expect(fn).toHaveBeenCalledTimes(3);
27
+ });
28
+
29
+ it('throws after exhausting retries', async () => {
30
+ const fn = jest.fn().mockRejectedValue(new Error('persistent failure'));
31
+
32
+ await expect(retry(fn, { retries: 2, baseDelay: 50 })).rejects.toThrow(
33
+ 'persistent failure'
34
+ );
35
+ expect(fn).toHaveBeenCalledTimes(3); // initial + 2 retries
36
+ });
37
+
38
+ it('calls onRetry callback before each retry', async () => {
39
+ let calls = 0;
40
+ const fn = jest.fn().mockImplementation(async () => {
41
+ calls++;
42
+ if (calls < 2) {
43
+ throw new Error('fail');
44
+ }
45
+ return 'ok';
46
+ });
47
+ const onRetry = jest.fn();
48
+
49
+ await retry(fn, { retries: 3, baseDelay: 50, onRetry });
50
+ expect(onRetry).toHaveBeenCalledTimes(1);
51
+ expect(onRetry).toHaveBeenCalledWith(expect.any(Error), 1, 50);
52
+ });
53
+
54
+ it('uses exponential backoff delays', async () => {
55
+ const delays = [];
56
+ const onRetry = jest.fn((err, attempt, delay) => {
57
+ delays.push(delay);
58
+ });
59
+
60
+ const fn = jest.fn().mockRejectedValue(new Error('fail'));
61
+
62
+ await expect(
63
+ retry(fn, {
64
+ retries: 3,
65
+ baseDelay: 100,
66
+ factor: 2,
67
+ maxDelay: 1000,
68
+ onRetry,
69
+ })
70
+ ).rejects.toThrow();
71
+
72
+ // Delays should be: 100, 200, 400
73
+ expect(delays).toEqual([100, 200, 400]);
74
+ });
75
+
76
+ it('caps delay at maxDelay', async () => {
77
+ const delays = [];
78
+ const onRetry = jest.fn((err, attempt, delay) => {
79
+ delays.push(delay);
80
+ });
81
+
82
+ const fn = jest.fn().mockRejectedValue(new Error('fail'));
83
+
84
+ await expect(
85
+ retry(fn, {
86
+ retries: 3,
87
+ baseDelay: 100,
88
+ factor: 10,
89
+ maxDelay: 500,
90
+ onRetry,
91
+ })
92
+ ).rejects.toThrow();
93
+
94
+ // Delays: min(100, 500)=100, min(1000, 500)=500, min(10000, 500)=500
95
+ expect(delays).toEqual([100, 500, 500]);
96
+ });
97
+
98
+ it('passes attempt number to fn', async () => {
99
+ const attempts = [];
100
+ const fn = jest.fn().mockImplementation(async (attempt) => {
101
+ attempts.push(attempt);
102
+ if (attempt < 2) {
103
+ throw new Error('fail');
104
+ }
105
+ return 'ok';
106
+ });
107
+
108
+ await retry(fn, { retries: 3, baseDelay: 50 });
109
+ expect(attempts).toEqual([0, 1, 2]);
110
+ });
111
+ });
@@ -0,0 +1,182 @@
1
+ import {
2
+ normalizeText,
3
+ normalizeCode,
4
+ verifyMarkdownContent,
5
+ } from '../../src/verify.js';
6
+
7
+ describe('verify module', () => {
8
+ describe('normalizeText', () => {
9
+ it('lowercases text', () => {
10
+ expect(normalizeText('Hello World')).toBe('hello world');
11
+ });
12
+
13
+ it('normalizes unicode spaces', () => {
14
+ expect(normalizeText('hello\u00A0world')).toBe('hello world');
15
+ });
16
+
17
+ it('normalizes curly quotes', () => {
18
+ expect(normalizeText('\u201Chello\u201D')).toBe('"hello"');
19
+ });
20
+
21
+ it('removes LaTeX delimiters', () => {
22
+ expect(normalizeText('$x + y$')).toBe('x + y');
23
+ expect(normalizeText('$$x + y$$')).toBe('x + y');
24
+ });
25
+
26
+ it('normalizes arrows', () => {
27
+ expect(normalizeText('\u2192')).toBe('->');
28
+ expect(normalizeText('\\to')).toBe('->');
29
+ });
30
+
31
+ it('removes \\displaystyle', () => {
32
+ expect(normalizeText('\\displaystyle x')).toBe('x');
33
+ });
34
+
35
+ it('extracts text from \\text{}', () => {
36
+ expect(normalizeText('\\text{hello}')).toBe('hello');
37
+ });
38
+ });
39
+
40
+ describe('normalizeCode', () => {
41
+ it('lowercases and collapses whitespace', () => {
42
+ expect(normalizeCode(' Hello World ')).toBe('hello world');
43
+ });
44
+
45
+ it('normalizes multiplication sign', () => {
46
+ expect(normalizeCode('2\u00D73')).toBe('2x3');
47
+ });
48
+ });
49
+
50
+ describe('verifyMarkdownContent', () => {
51
+ it('verifies title presence', () => {
52
+ const webContent = {
53
+ title: 'My Article',
54
+ headings: [],
55
+ paragraphs: [],
56
+ codeBlocks: [],
57
+ formulas: [],
58
+ blockquoteFormulas: [],
59
+ listItems: [],
60
+ links: [],
61
+ figures: [],
62
+ };
63
+ const markdown = '# My Article\n\nSome content here.';
64
+ const result = verifyMarkdownContent(webContent, markdown);
65
+ expect(result.missing.title).toBe(false);
66
+ expect(result.passedChecks).toBeGreaterThan(0);
67
+ });
68
+
69
+ it('detects missing title', () => {
70
+ const webContent = {
71
+ title: 'Missing Title',
72
+ headings: [],
73
+ paragraphs: [],
74
+ codeBlocks: [],
75
+ formulas: [],
76
+ blockquoteFormulas: [],
77
+ listItems: [],
78
+ links: [],
79
+ figures: [],
80
+ };
81
+ const markdown = '# Different Title\n\nContent.';
82
+ const result = verifyMarkdownContent(webContent, markdown);
83
+ expect(result.missing.title).toBe(true);
84
+ });
85
+
86
+ it('verifies headings', () => {
87
+ const webContent = {
88
+ title: '',
89
+ headings: [
90
+ { level: 'h2', text: 'Introduction' },
91
+ { level: 'h3', text: 'Background' },
92
+ ],
93
+ paragraphs: [],
94
+ codeBlocks: [],
95
+ formulas: [],
96
+ blockquoteFormulas: [],
97
+ listItems: [],
98
+ links: [],
99
+ figures: [],
100
+ };
101
+ const markdown = '## Introduction\n\n### Background\n\nText.';
102
+ const result = verifyMarkdownContent(webContent, markdown);
103
+ expect(result.missing.headings).toHaveLength(0);
104
+ });
105
+
106
+ it('detects missing headings', () => {
107
+ const webContent = {
108
+ title: '',
109
+ headings: [{ level: 'h2', text: 'Unique Heading XYZ' }],
110
+ paragraphs: [],
111
+ codeBlocks: [],
112
+ formulas: [],
113
+ blockquoteFormulas: [],
114
+ listItems: [],
115
+ links: [],
116
+ figures: [],
117
+ };
118
+ const markdown = '## Different Heading\n\nText.';
119
+ const result = verifyMarkdownContent(webContent, markdown);
120
+ expect(result.missing.headings).toContain('Unique Heading XYZ');
121
+ });
122
+
123
+ it('verifies code blocks with fuzzy matching', () => {
124
+ const webContent = {
125
+ title: '',
126
+ headings: [],
127
+ paragraphs: [],
128
+ codeBlocks: [
129
+ 'function hello() {\n console.log("hello");\n return true;\n}',
130
+ ],
131
+ formulas: [],
132
+ blockquoteFormulas: [],
133
+ listItems: [],
134
+ links: [],
135
+ figures: [],
136
+ };
137
+ const markdown =
138
+ '```javascript\nfunction hello() {\n console.log("hello");\n return true;\n}\n```';
139
+ const result = verifyMarkdownContent(webContent, markdown);
140
+ expect(result.missing.codeBlocks).toHaveLength(0);
141
+ });
142
+
143
+ it('calculates pass rate', () => {
144
+ const webContent = {
145
+ title: 'Test',
146
+ headings: [{ level: 'h2', text: 'Found' }],
147
+ paragraphs: [],
148
+ codeBlocks: [],
149
+ formulas: [],
150
+ blockquoteFormulas: [],
151
+ listItems: [],
152
+ links: [],
153
+ figures: [],
154
+ };
155
+ const markdown = '# Test\n\n## Found\n\nContent.';
156
+ const result = verifyMarkdownContent(webContent, markdown);
157
+ expect(result.passRate).toBe(1.0);
158
+ expect(result.success).toBe(true);
159
+ });
160
+
161
+ it('checks figure images when hasLocalImages is set', () => {
162
+ const webContent = {
163
+ title: '',
164
+ headings: [],
165
+ paragraphs: [],
166
+ codeBlocks: [],
167
+ formulas: [],
168
+ blockquoteFormulas: [],
169
+ listItems: [],
170
+ links: [],
171
+ figures: [1, 2, 3],
172
+ };
173
+ const markdown =
174
+ '![Figure 1](images/figure-1.png)\n![Figure 2](images/figure-2.png)\n![Figure 3](images/figure-3.png)';
175
+ const result = verifyMarkdownContent(webContent, markdown, {
176
+ hasLocalImages: true,
177
+ expectedFigures: 3,
178
+ });
179
+ expect(result.missing.images).toBe(0);
180
+ });
181
+ });
182
+ });
@@ -1,294 +0,0 @@
1
- name: Checks and release
2
-
3
- on:
4
- push:
5
- branches:
6
- - main
7
- pull_request:
8
- types: [opened, synchronize, reopened]
9
- # Manual release support - consolidated here to work with npm trusted publishing
10
- # npm only allows ONE workflow file as trusted publisher, so all publishing
11
- # must go through this workflow (release.yml)
12
- workflow_dispatch:
13
- inputs:
14
- release_mode:
15
- description: 'Manual release mode'
16
- required: true
17
- type: choice
18
- default: 'instant'
19
- options:
20
- - instant
21
- - changeset-pr
22
- bump_type:
23
- description: 'Manual release type'
24
- required: true
25
- type: choice
26
- options:
27
- - patch
28
- - minor
29
- - major
30
- description:
31
- description: 'Manual release description (optional)'
32
- required: false
33
- type: string
34
-
35
- concurrency: ${{ github.workflow }}-${{ github.ref }}
36
-
37
- jobs:
38
- # Changeset check - only runs on PRs
39
- changeset-check:
40
- name: Check for Changesets
41
- runs-on: ubuntu-latest
42
- if: github.event_name == 'pull_request'
43
- steps:
44
- - uses: actions/checkout@v4
45
- with:
46
- fetch-depth: 0
47
-
48
- - name: Setup Node.js
49
- uses: actions/setup-node@v4
50
- with:
51
- node-version: '20.x'
52
-
53
- - name: Install dependencies
54
- run: npm install
55
-
56
- - name: Check for changesets
57
- run: |
58
- # Skip changeset check for automated version PRs
59
- if [[ "${{ github.head_ref }}" == "changeset-release/"* ]]; then
60
- echo "Skipping changeset check for automated release PR"
61
- exit 0
62
- fi
63
-
64
- # Run changeset validation script
65
- node scripts/validate-changeset.mjs
66
-
67
- # Linting and formatting - runs after changeset check on PRs, immediately on main
68
- lint:
69
- name: Lint and Format Check
70
- runs-on: ubuntu-latest
71
- needs: [changeset-check]
72
- if: always() && (github.event_name == 'push' || needs.changeset-check.result == 'success')
73
- steps:
74
- - uses: actions/checkout@v4
75
-
76
- - name: Setup Node.js
77
- uses: actions/setup-node@v4
78
- with:
79
- node-version: '20.x'
80
-
81
- - name: Install dependencies
82
- run: npm install
83
-
84
- - name: Run ESLint
85
- run: npm run lint
86
-
87
- - name: Check formatting
88
- run: npm run format:check
89
-
90
- - name: Check code duplication
91
- run: npm run check:duplication
92
-
93
- # Test matrix: Node.js on Ubuntu with unit tests and e2e tests
94
- test:
95
- name: Test (Node.js on ${{ matrix.os }})
96
- runs-on: ${{ matrix.os }}
97
- needs: [changeset-check]
98
- if: always() && (github.event_name == 'push' || needs.changeset-check.result == 'success')
99
- strategy:
100
- fail-fast: false
101
- matrix:
102
- os: [ubuntu-latest]
103
- steps:
104
- - uses: actions/checkout@v4
105
-
106
- - name: Setup Node.js
107
- uses: actions/setup-node@v4
108
- with:
109
- node-version: '22.x'
110
-
111
- - name: Install dependencies
112
- run: npm install
113
-
114
- - name: Install Playwright browsers
115
- run: npx playwright install --with-deps chromium
116
-
117
- - name: Install Puppeteer browsers
118
- run: npx puppeteer browsers install chrome
119
-
120
- - name: Run unit, integration, and e2e process tests
121
- run: npm test -- --testPathIgnorePatterns="docker.test.js"
122
-
123
- - name: Build Docker image for e2e tests
124
- run: docker compose build
125
-
126
- - name: Run e2e Docker tests
127
- run: npm test -- tests/e2e/docker.test.js
128
-
129
- # Release - only runs on main after tests pass (for push events)
130
- release:
131
- name: Release
132
- needs: [lint, test]
133
- # Use always() to ensure this job runs even if changeset-check was skipped
134
- # This is needed because lint/test jobs have a transitive dependency on changeset-check
135
- if: always() && github.ref == 'refs/heads/main' && github.event_name == 'push' && needs.lint.result == 'success' && needs.test.result == 'success'
136
- runs-on: ubuntu-latest
137
- # Permissions required for npm OIDC trusted publishing
138
- permissions:
139
- contents: write
140
- pull-requests: write
141
- id-token: write
142
- steps:
143
- - uses: actions/checkout@v4
144
- with:
145
- fetch-depth: 0
146
-
147
- - name: Setup Node.js
148
- uses: actions/setup-node@v4
149
- with:
150
- node-version: '22.x'
151
- registry-url: 'https://registry.npmjs.org'
152
-
153
- - name: Install dependencies
154
- run: npm install
155
-
156
- - name: Update npm for OIDC trusted publishing
157
- run: node scripts/setup-npm.mjs
158
-
159
- - name: Check for changesets
160
- id: check_changesets
161
- run: |
162
- # Count changeset files (excluding README.md and config.json)
163
- CHANGESET_COUNT=$(find .changeset -name "*.md" ! -name "README.md" | wc -l)
164
- echo "Found $CHANGESET_COUNT changeset file(s)"
165
- echo "has_changesets=$([[ $CHANGESET_COUNT -gt 0 ]] && echo 'true' || echo 'false')" >> $GITHUB_OUTPUT
166
-
167
- - name: Version packages and commit to main
168
- if: steps.check_changesets.outputs.has_changesets == 'true'
169
- id: version
170
- run: node scripts/version-and-commit.mjs --mode changeset
171
-
172
- - name: Publish to npm
173
- # Run if version was committed OR if a previous attempt already committed (for re-runs)
174
- if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true'
175
- id: publish
176
- run: node scripts/publish-to-npm.mjs --should-pull
177
-
178
- - name: Create GitHub Release
179
- if: steps.publish.outputs.published == 'true'
180
- env:
181
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
182
- run: node scripts/create-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}"
183
-
184
- - name: Format GitHub release notes
185
- if: steps.publish.outputs.published == 'true'
186
- env:
187
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
188
- run: node scripts/format-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --commit-sha "${{ github.sha }}"
189
-
190
- # Manual Instant Release - triggered via workflow_dispatch with instant mode
191
- # This job is in release.yml because npm trusted publishing
192
- # only allows one workflow file to be registered as a trusted publisher
193
- instant-release:
194
- name: Instant Release
195
- if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_mode == 'instant'
196
- runs-on: ubuntu-latest
197
- # Permissions required for npm OIDC trusted publishing
198
- permissions:
199
- contents: write
200
- pull-requests: write
201
- id-token: write
202
- steps:
203
- - uses: actions/checkout@v4
204
- with:
205
- fetch-depth: 0
206
-
207
- - name: Setup Node.js
208
- uses: actions/setup-node@v4
209
- with:
210
- node-version: '22.x'
211
- registry-url: 'https://registry.npmjs.org'
212
-
213
- - name: Install dependencies
214
- run: npm install
215
-
216
- - name: Update npm for OIDC trusted publishing
217
- run: node scripts/setup-npm.mjs
218
-
219
- - name: Version packages and commit to main
220
- id: version
221
- run: node scripts/version-and-commit.mjs --mode instant --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}"
222
-
223
- - name: Publish to npm
224
- # Run if version was committed OR if a previous attempt already committed (for re-runs)
225
- if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true'
226
- id: publish
227
- run: node scripts/publish-to-npm.mjs
228
-
229
- - name: Create GitHub Release
230
- if: steps.publish.outputs.published == 'true'
231
- env:
232
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
233
- run: node scripts/create-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}"
234
-
235
- - name: Format GitHub release notes
236
- if: steps.publish.outputs.published == 'true'
237
- env:
238
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
239
- run: node scripts/format-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --commit-sha "${{ github.sha }}"
240
-
241
- # Manual Changeset PR - creates a pull request with the changeset for review
242
- changeset-pr:
243
- name: Create Changeset PR
244
- if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_mode == 'changeset-pr'
245
- runs-on: ubuntu-latest
246
- permissions:
247
- contents: write
248
- pull-requests: write
249
- steps:
250
- - uses: actions/checkout@v4
251
- with:
252
- fetch-depth: 0
253
-
254
- - name: Setup Node.js
255
- uses: actions/setup-node@v4
256
- with:
257
- node-version: '22.x'
258
-
259
- - name: Install dependencies
260
- run: npm install
261
-
262
- - name: Create changeset file
263
- run: node scripts/create-manual-changeset.mjs --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}"
264
-
265
- - name: Format changeset with Prettier
266
- run: |
267
- # Run Prettier on the changeset file to ensure it matches project style
268
- npx prettier --write ".changeset/*.md" || true
269
-
270
- echo "Formatted changeset files"
271
-
272
- - name: Create Pull Request
273
- uses: peter-evans/create-pull-request@v7
274
- with:
275
- token: ${{ secrets.GITHUB_TOKEN }}
276
- commit-message: 'chore: add changeset for manual ${{ github.event.inputs.bump_type }} release'
277
- branch: changeset-manual-release-${{ github.run_id }}
278
- delete-branch: true
279
- title: 'chore: manual ${{ github.event.inputs.bump_type }} release'
280
- body: |
281
- ## Manual Release Request
282
-
283
- This PR was created by a manual workflow trigger to prepare a **${{ github.event.inputs.bump_type }}** release.
284
-
285
- ### Release Details
286
- - **Type:** ${{ github.event.inputs.bump_type }}
287
- - **Description:** ${{ github.event.inputs.description || 'Manual release' }}
288
- - **Triggered by:** @${{ github.actor }}
289
-
290
- ### Next Steps
291
- 1. Review the changeset in this PR
292
- 2. Merge this PR to main
293
- 3. The automated release workflow will create a version PR
294
- 4. Merge the version PR to publish to npm and create a GitHub release