@link-assistant/web-capture 2.0.0 → 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.
- package/CHANGELOG.md +11 -0
- package/bin/web-capture.js +2 -5
- package/package.json +22 -11
- package/src/archive.js +0 -1
- package/src/binary-download.js +0 -1
- package/src/docx.js +13 -10
- package/src/fetch.js +0 -1
- package/src/gdocs-images.js +0 -1
- package/src/gdocs.js +0 -1
- package/src/github.js +0 -1
- package/src/lib.js +0 -1
- package/src/markdown.js +0 -1
- package/src/shared-dialog.js +0 -1
- package/src/stream.js +0 -1
- package/src/transport.js +23 -5
- package/src/txt.js +0 -1
- package/.changeset/README.md +0 -8
- package/.changeset/config.json +0 -11
- package/.dockerignore +0 -6
- package/.jscpd.json +0 -19
- package/.lenv +0 -8
- package/.prettierignore +0 -8
- package/.prettierrc +0 -10
- package/Dockerfile +0 -52
- package/docker-compose.yml +0 -11
- package/eslint.config.js +0 -112
- package/examples/js/capture_cache.js +0 -12
- package/examples/js/engine_comparison.js +0 -39
- package/examples/js/html_download.js +0 -23
- package/examples/js/image_download.js +0 -36
- package/examples/js/markdown_download.js +0 -23
- package/examples/js/playwright_example.js +0 -23
- package/examples/python/capture_client.py +0 -20
- package/examples/python/html_download.py +0 -3
- package/examples/python/image_download.py +0 -16
- package/examples/python/markdown_download.py +0 -3
- package/examples/python/playwright_example.py +0 -22
- package/jest.config.mjs +0 -17
- package/tests/ci/dependency-freshness.test.js +0 -74
- package/tests/ci/detect-code-changes.test.js +0 -187
- package/tests/ci/docker-install.test.js +0 -31
- package/tests/ci/rust-dockerfile.test.js +0 -54
- package/tests/ci/workflow-policy.test.js +0 -79
- package/tests/e2e/docker.test.js +0 -186
- package/tests/e2e/process.test.js +0 -215
- package/tests/fixtures/bold-coalesce-and-leakage.html +0 -27
- package/tests/fixtures/deepwiki/deepwiki-full-page.png +0 -0
- package/tests/fixtures/deepwiki/index.md +0 -1500
- package/tests/fixtures/deepwiki/part-1.md +0 -776
- package/tests/fixtures/habr-articles.js +0 -45
- package/tests/fixtures/heading-numbering.html +0 -19
- package/tests/fixtures/list-item-with-br.html +0 -8
- package/tests/fixtures/ol-continuous-numbering.html +0 -12
- package/tests/fixtures/paragraph-vs-line-break.html +0 -11
- package/tests/helpers/nock.js +0 -10
- package/tests/helpers/screenshot.js +0 -10
- package/tests/helpers/search-fixture.cjs +0 -17
- package/tests/helpers/temporary-directory.js +0 -12
- package/tests/integration/api-endpoints.test.js +0 -418
- package/tests/integration/archive-default-layout.test.js +0 -37
- package/tests/integration/browser-engines.test.js +0 -80
- package/tests/integration/browser-packaged-path.test.js +0 -45
- package/tests/integration/deepwiki-download.test.js +0 -144
- package/tests/integration/deepwiki-rendered-page.mock.test.js +0 -121
- package/tests/integration/formalai-contract.test.js +0 -214
- package/tests/integration/gdocs-public-doc.test.js +0 -315
- package/tests/integration/github-readme.test.js +0 -133
- package/tests/integration/google-drive-image.test.js +0 -246
- package/tests/integration/habr-article.test.js +0 -337
- package/tests/integration/search-endpoint.test.js +0 -88
- package/tests/integration/stackoverflow-download.test.js +0 -173
- package/tests/integration/wikipedia-download.test.js +0 -134
- package/tests/jest.setup.mjs +0 -7
- package/tests/mock/index.test.js +0 -225
- package/tests/unit/animation.test.js +0 -33
- package/tests/unit/batch.test.js +0 -114
- package/tests/unit/bold-coalesce-and-leakage.test.js +0 -54
- package/tests/unit/browser-launch-options.test.js +0 -42
- package/tests/unit/browser.test.js +0 -172
- package/tests/unit/cache.test.js +0 -182
- package/tests/unit/cli.test.js +0 -399
- package/tests/unit/convertGoogleDriveUrl.test.js +0 -89
- package/tests/unit/convertRelativeUrls.test.js +0 -34
- package/tests/unit/extract-images.test.js +0 -259
- package/tests/unit/fetch-html.test.js +0 -54
- package/tests/unit/figures.test.js +0 -74
- package/tests/unit/gdocs-export-headers.test.js +0 -28
- package/tests/unit/gdocs-preprocess.test.js +0 -36
- package/tests/unit/gdocs.test.js +0 -1490
- package/tests/unit/github.test.js +0 -156
- package/tests/unit/heading-image.test.js +0 -49
- package/tests/unit/heading-numbering.test.js +0 -28
- package/tests/unit/html2md-br-in-list-item.test.js +0 -23
- package/tests/unit/html2md-ol-numbering.test.js +0 -20
- package/tests/unit/html2md.test.js +0 -695
- package/tests/unit/image-mode-defaults.test.js +0 -54
- package/tests/unit/kreuzberg.test.js +0 -249
- package/tests/unit/latex.test.js +0 -123
- package/tests/unit/localize-images.test.js +0 -114
- package/tests/unit/markdown.test.js +0 -36
- package/tests/unit/metadata.test.js +0 -140
- package/tests/unit/package-runtime-dependencies.test.js +0 -74
- package/tests/unit/paragraph-vs-line-break.test.js +0 -56
- package/tests/unit/postprocess.test.js +0 -110
- package/tests/unit/proxy-headers.test.js +0 -100
- package/tests/unit/publish-verification.test.js +0 -54
- package/tests/unit/retry.test.js +0 -111
- package/tests/unit/search.test.js +0 -339
- package/tests/unit/shared-dialog.test.js +0 -173
- package/tests/unit/stackoverflow.test.js +0 -32
- package/tests/unit/stream.test.js +0 -83
- package/tests/unit/verify.test.js +0 -182
- package/tests/xpaste/xpaste-screenshot-markdown.mock.test.js +0 -375
- package/tests/xpaste/xpaste.e2e.test.js +0 -36
- package/tests/xpaste/xpaste.integration.test.js +0 -41
- package/tests/xpaste/xpaste.mock.test.js +0 -261
- package/tests/xpaste/xpaste.unit.test.js +0 -119
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
|
|
5
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
-
const url = process.argv[2] || 'https://example.com';
|
|
7
|
-
const endpoint = `http://localhost:3000/image?url=${encodeURIComponent(url)}`;
|
|
8
|
-
|
|
9
|
-
fetch(endpoint)
|
|
10
|
-
.then((res) => {
|
|
11
|
-
console.log('Status:', res.status);
|
|
12
|
-
console.log('Content-Type:', res.headers.get('content-type'));
|
|
13
|
-
if (!res.ok) {
|
|
14
|
-
throw new Error(`HTTP error! status: ${res.status}`);
|
|
15
|
-
}
|
|
16
|
-
return res.arrayBuffer();
|
|
17
|
-
})
|
|
18
|
-
.then((buffer) => {
|
|
19
|
-
const buf = Buffer.from(buffer);
|
|
20
|
-
const pngSignature = Buffer.from([
|
|
21
|
-
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
22
|
-
]);
|
|
23
|
-
if (buf.slice(0, 8).equals(pngSignature)) {
|
|
24
|
-
const outputPath = path.join(__dirname, 'downloaded.png');
|
|
25
|
-
fs.writeFileSync(outputPath, buf);
|
|
26
|
-
console.log(`Image saved to ${outputPath} (valid PNG)`);
|
|
27
|
-
} else {
|
|
28
|
-
console.log(
|
|
29
|
-
'Response is not a valid PNG! First bytes:',
|
|
30
|
-
buf.slice(0, 16)
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
})
|
|
34
|
-
.catch((err) => {
|
|
35
|
-
console.error('Error:', err);
|
|
36
|
-
});
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
|
|
5
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
-
const url = process.argv[2] || 'https://example.com';
|
|
7
|
-
const endpoint = `http://localhost:3000/markdown?url=${encodeURIComponent(url)}`;
|
|
8
|
-
|
|
9
|
-
fetch(endpoint)
|
|
10
|
-
.then((res) => {
|
|
11
|
-
if (!res.ok) {
|
|
12
|
-
throw new Error(`HTTP error! status: ${res.status}`);
|
|
13
|
-
}
|
|
14
|
-
return res.text();
|
|
15
|
-
})
|
|
16
|
-
.then((text) => {
|
|
17
|
-
const outputPath = path.join(__dirname, 'downloaded.md');
|
|
18
|
-
fs.writeFileSync(outputPath, text, 'utf-8');
|
|
19
|
-
console.log(`Markdown saved to ${outputPath}`);
|
|
20
|
-
})
|
|
21
|
-
.catch((err) => {
|
|
22
|
-
console.error('Error:', err);
|
|
23
|
-
});
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import fetch from 'node-fetch';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
|
|
4
|
-
const baseUrl = 'http://localhost:3000';
|
|
5
|
-
const targetUrl = 'https://example.com';
|
|
6
|
-
|
|
7
|
-
console.log('Fetching HTML using Playwright engine...');
|
|
8
|
-
const htmlResponse = await fetch(
|
|
9
|
-
`${baseUrl}/html?url=${encodeURIComponent(targetUrl)}&engine=playwright`
|
|
10
|
-
);
|
|
11
|
-
const html = await htmlResponse.text();
|
|
12
|
-
fs.writeFileSync('output/playwright_html.html', html);
|
|
13
|
-
console.log('HTML saved to output/playwright_html.html');
|
|
14
|
-
|
|
15
|
-
console.log('Fetching screenshot using Playwright engine...');
|
|
16
|
-
const imageResponse = await fetch(
|
|
17
|
-
`${baseUrl}/image?url=${encodeURIComponent(targetUrl)}&engine=playwright`
|
|
18
|
-
);
|
|
19
|
-
const imageBuffer = Buffer.from(await imageResponse.arrayBuffer());
|
|
20
|
-
fs.writeFileSync('output/playwright_screenshot.png', imageBuffer);
|
|
21
|
-
console.log('Screenshot saved to output/playwright_screenshot.png');
|
|
22
|
-
|
|
23
|
-
console.log('Done! Both requests used Playwright engine.');
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
"""Shared request policy for the capture-service examples."""
|
|
2
|
-
import os
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
import sys
|
|
5
|
-
import requests
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def capture(kind, url=None, **options):
|
|
9
|
-
if url is None:
|
|
10
|
-
url = sys.argv[1] if len(sys.argv) > 1 else 'https://example.com'
|
|
11
|
-
endpoint = os.environ.get('WEB_CAPTURE_URL', 'http://localhost:3000').rstrip('/')
|
|
12
|
-
response = requests.get(f'{endpoint}/{kind}', params={'url': url, **options}, timeout=30)
|
|
13
|
-
response.raise_for_status()
|
|
14
|
-
return response
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def download_text(kind, filename):
|
|
18
|
-
output = Path(__file__).with_name(filename)
|
|
19
|
-
output.write_text(capture(kind).text, encoding='utf-8')
|
|
20
|
-
print(f'{kind.title()} saved to {output}')
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
from capture_client import capture
|
|
2
|
-
import os
|
|
3
|
-
|
|
4
|
-
response = capture('image')
|
|
5
|
-
print('Status:', response.status_code)
|
|
6
|
-
print('Content-Type:', response.headers.get('content-type'))
|
|
7
|
-
|
|
8
|
-
# PNG signature: first 8 bytes
|
|
9
|
-
png_signature = b'\x89PNG\r\n\x1a\n'
|
|
10
|
-
if response.content[:8] == png_signature:
|
|
11
|
-
output_path = os.path.join(os.path.dirname(__file__), 'downloaded.png')
|
|
12
|
-
with open(output_path, 'wb') as f:
|
|
13
|
-
f.write(response.content)
|
|
14
|
-
print(f'Image saved to {output_path} (valid PNG)')
|
|
15
|
-
else:
|
|
16
|
-
print('Response is not a valid PNG! First bytes:', response.content[:16])
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
from capture_client import capture
|
|
3
|
-
import os
|
|
4
|
-
|
|
5
|
-
target_url = sys.argv[1] if len(sys.argv) > 1 else 'https://example.com'
|
|
6
|
-
|
|
7
|
-
# Create output directory if it doesn't exist
|
|
8
|
-
os.makedirs('output', exist_ok=True)
|
|
9
|
-
|
|
10
|
-
print('Fetching HTML using Playwright engine...')
|
|
11
|
-
html_response = capture('html', target_url, engine='playwright')
|
|
12
|
-
with open('output/playwright_html.html', 'w', encoding='utf-8') as f:
|
|
13
|
-
f.write(html_response.text)
|
|
14
|
-
print('HTML saved to output/playwright_html.html')
|
|
15
|
-
|
|
16
|
-
print('Fetching screenshot using Playwright engine...')
|
|
17
|
-
image_response = capture('image', target_url, engine='playwright')
|
|
18
|
-
with open('output/playwright_screenshot.png', 'wb') as f:
|
|
19
|
-
f.write(image_response.content)
|
|
20
|
-
print('Screenshot saved to output/playwright_screenshot.png')
|
|
21
|
-
|
|
22
|
-
print('Done! Both requests used Playwright engine.')
|
package/jest.config.mjs
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export default {
|
|
2
|
-
transform: {},
|
|
3
|
-
moduleNameMapper: {
|
|
4
|
-
'^(\\.{1,2}/.*)\\.js$': '$1',
|
|
5
|
-
},
|
|
6
|
-
testEnvironment: 'node',
|
|
7
|
-
verbose: true,
|
|
8
|
-
testMatch: [
|
|
9
|
-
'**/tests/ci/**/*.test.js',
|
|
10
|
-
'**/tests/unit/**/*.test.js',
|
|
11
|
-
'**/tests/mock/**/*.test.js',
|
|
12
|
-
'**/tests/e2e/**/*.test.js',
|
|
13
|
-
'**/tests/integration/**/*.test.js',
|
|
14
|
-
'**/tests/xpaste/**/*.test.js',
|
|
15
|
-
],
|
|
16
|
-
setupFiles: ['./tests/jest.setup.mjs'],
|
|
17
|
-
};
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { jest } from '@jest/globals';
|
|
2
|
-
import { checkVersions } from '../../../scripts/check-dependency-freshness.mjs';
|
|
3
|
-
|
|
4
|
-
const latest = async () => '2.0.0';
|
|
5
|
-
const dependencies = [{ name: 'example', current: '1.0.0' }];
|
|
6
|
-
const blockers = {
|
|
7
|
-
example: {
|
|
8
|
-
issue: 'https://github.com/example/repo/issues/1',
|
|
9
|
-
reason: 'Upstream API breaks packaged consumers',
|
|
10
|
-
},
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
test('freshness rejects old releases, missing packages and undocumented exceptions', async () => {
|
|
14
|
-
for (const current of ['1.0.0', '(missing)']) {
|
|
15
|
-
await expect(
|
|
16
|
-
checkVersions([{ name: 'example', current }], {}, latest, jest.fn())
|
|
17
|
-
).rejects.toThrow('behind 2.0.0');
|
|
18
|
-
}
|
|
19
|
-
});
|
|
20
|
-
test('freshness permits only documented open issues', async () => {
|
|
21
|
-
await expect(
|
|
22
|
-
checkVersions(dependencies, blockers, latest, async () => ({
|
|
23
|
-
state: 'open',
|
|
24
|
-
}))
|
|
25
|
-
).resolves.toBeUndefined();
|
|
26
|
-
await expect(
|
|
27
|
-
checkVersions(dependencies, blockers, latest, async () => ({
|
|
28
|
-
state: 'closed',
|
|
29
|
-
}))
|
|
30
|
-
).rejects.toThrow('is closed');
|
|
31
|
-
await expect(
|
|
32
|
-
checkVersions(
|
|
33
|
-
dependencies,
|
|
34
|
-
{ example: { ...blockers.example, reason: '' } },
|
|
35
|
-
latest,
|
|
36
|
-
jest.fn()
|
|
37
|
-
)
|
|
38
|
-
).rejects.toThrow('document an open issue');
|
|
39
|
-
await expect(
|
|
40
|
-
checkVersions(
|
|
41
|
-
dependencies,
|
|
42
|
-
{ example: { ...blockers.example, reason: ' ' } },
|
|
43
|
-
latest,
|
|
44
|
-
jest.fn()
|
|
45
|
-
)
|
|
46
|
-
).rejects.toThrow('document an open issue');
|
|
47
|
-
});
|
|
48
|
-
test('registry and issue failures fail the check rather than treating dependencies as fresh', async () => {
|
|
49
|
-
await expect(
|
|
50
|
-
checkVersions(
|
|
51
|
-
dependencies,
|
|
52
|
-
{},
|
|
53
|
-
async () => {
|
|
54
|
-
throw new Error('registry down');
|
|
55
|
-
},
|
|
56
|
-
jest.fn()
|
|
57
|
-
)
|
|
58
|
-
).rejects.toThrow('registry down');
|
|
59
|
-
await expect(
|
|
60
|
-
checkVersions(dependencies, blockers, latest, async () => {
|
|
61
|
-
throw new Error('issue unavailable');
|
|
62
|
-
})
|
|
63
|
-
).rejects.toThrow('issue unavailable');
|
|
64
|
-
});
|
|
65
|
-
test('current releases need no exception', async () => {
|
|
66
|
-
const getIssue = jest.fn();
|
|
67
|
-
await checkVersions(
|
|
68
|
-
[{ name: 'example', current: '2.0.0' }],
|
|
69
|
-
{},
|
|
70
|
-
latest,
|
|
71
|
-
getIssue
|
|
72
|
-
);
|
|
73
|
-
expect(getIssue).not.toHaveBeenCalled();
|
|
74
|
-
});
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from 'child_process';
|
|
2
|
-
import {
|
|
3
|
-
mkdirSync,
|
|
4
|
-
mkdtempSync,
|
|
5
|
-
readFileSync,
|
|
6
|
-
rmSync,
|
|
7
|
-
writeFileSync,
|
|
8
|
-
} from 'fs';
|
|
9
|
-
import { tmpdir } from 'os';
|
|
10
|
-
import { dirname, join, resolve } from 'path';
|
|
11
|
-
import { fileURLToPath } from 'url';
|
|
12
|
-
|
|
13
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
-
const __dirname = dirname(__filename);
|
|
15
|
-
const repoRoot = resolve(__dirname, '../../..');
|
|
16
|
-
const detectorPath = resolve(repoRoot, 'scripts/detect-code-changes.mjs');
|
|
17
|
-
|
|
18
|
-
function run(command, args, cwd, options = {}) {
|
|
19
|
-
return execFileSync(command, args, {
|
|
20
|
-
cwd,
|
|
21
|
-
encoding: 'utf8',
|
|
22
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
23
|
-
...options,
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function git(repoPath, args) {
|
|
28
|
-
return run('git', args, repoPath);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function writeTrackedFile(repoPath, relativePath, contents) {
|
|
32
|
-
const filePath = join(repoPath, relativePath);
|
|
33
|
-
mkdirSync(dirname(filePath), { recursive: true });
|
|
34
|
-
writeFileSync(filePath, contents);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function commitAll(repoPath, message) {
|
|
38
|
-
git(repoPath, ['add', '.']);
|
|
39
|
-
git(repoPath, ['commit', '-m', message]);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function createMergedFeatureRepo({
|
|
43
|
-
featureCode = true,
|
|
44
|
-
baseCode = false,
|
|
45
|
-
} = {}) {
|
|
46
|
-
const repoPath = mkdtempSync(join(tmpdir(), 'web-capture-detect-'));
|
|
47
|
-
|
|
48
|
-
initializeRepo(repoPath);
|
|
49
|
-
|
|
50
|
-
writeTrackedFile(repoPath, 'README.md', '# fixture\n');
|
|
51
|
-
commitAll(repoPath, 'initial commit');
|
|
52
|
-
|
|
53
|
-
git(repoPath, ['checkout', '-b', 'feature']);
|
|
54
|
-
if (featureCode) {
|
|
55
|
-
writeTrackedFile(repoPath, 'rust/src/lib.rs', 'pub fn capture() {}\n');
|
|
56
|
-
commitAll(repoPath, 'add rust code');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
writeTrackedFile(repoPath, 'docs/notes.md', '# docs only tail commit\n');
|
|
60
|
-
commitAll(repoPath, 'document rust change');
|
|
61
|
-
|
|
62
|
-
git(repoPath, ['checkout', 'main']);
|
|
63
|
-
if (baseCode) {
|
|
64
|
-
writeTrackedFile(repoPath, 'rust/src/lib.rs', 'pub fn base_capture() {}\n');
|
|
65
|
-
commitAll(repoPath, 'base branch code change');
|
|
66
|
-
}
|
|
67
|
-
git(repoPath, ['merge', '--no-ff', 'feature', '-m', 'merge feature']);
|
|
68
|
-
|
|
69
|
-
return repoPath;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function createSingleCommitRepo() {
|
|
73
|
-
return createSingleCommitRepoWithFiles({
|
|
74
|
-
'js/src/index.js': 'export const ok = true;\n',
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function createSingleCommitRepoWithFiles(files) {
|
|
79
|
-
const repoPath = mkdtempSync(join(tmpdir(), 'web-capture-detect-'));
|
|
80
|
-
|
|
81
|
-
initializeRepo(repoPath);
|
|
82
|
-
for (const [relativePath, contents] of Object.entries(files)) {
|
|
83
|
-
writeTrackedFile(repoPath, relativePath, contents);
|
|
84
|
-
}
|
|
85
|
-
commitAll(repoPath, 'initial commit');
|
|
86
|
-
|
|
87
|
-
return repoPath;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function initializeRepo(repoPath) {
|
|
91
|
-
git(repoPath, ['init']);
|
|
92
|
-
git(repoPath, ['config', 'user.email', 'ci@example.test']);
|
|
93
|
-
git(repoPath, ['config', 'user.name', 'CI Test']);
|
|
94
|
-
git(repoPath, ['checkout', '-b', 'main']);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function parseOutputs(outputFile) {
|
|
98
|
-
return Object.fromEntries(
|
|
99
|
-
readFileSync(outputFile, 'utf8')
|
|
100
|
-
.trim()
|
|
101
|
-
.split('\n')
|
|
102
|
-
.filter(Boolean)
|
|
103
|
-
.map((line) => {
|
|
104
|
-
const separatorIndex = line.indexOf('=');
|
|
105
|
-
return [line.slice(0, separatorIndex), line.slice(separatorIndex + 1)];
|
|
106
|
-
})
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function runDetector(repoPath, eventName) {
|
|
111
|
-
const outputFile = join(repoPath, `github-output-${eventName}.txt`);
|
|
112
|
-
const env = {
|
|
113
|
-
...process.env,
|
|
114
|
-
GITHUB_EVENT_NAME: eventName,
|
|
115
|
-
GITHUB_OUTPUT: outputFile,
|
|
116
|
-
};
|
|
117
|
-
delete env.GITHUB_BASE_SHA;
|
|
118
|
-
delete env.GITHUB_HEAD_SHA;
|
|
119
|
-
|
|
120
|
-
run(process.execPath, [detectorPath], repoPath, { env });
|
|
121
|
-
|
|
122
|
-
return parseOutputs(outputFile);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
describe('detect-code-changes', () => {
|
|
126
|
-
let repoPath;
|
|
127
|
-
|
|
128
|
-
afterEach(() => {
|
|
129
|
-
if (repoPath) {
|
|
130
|
-
rmSync(repoPath, { recursive: true, force: true });
|
|
131
|
-
repoPath = undefined;
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test.each(['push', 'pull_request'])(
|
|
136
|
-
'detects code before a docs-only final commit for %s',
|
|
137
|
-
(eventName) => {
|
|
138
|
-
repoPath = createMergedFeatureRepo();
|
|
139
|
-
|
|
140
|
-
expect(runDetector(repoPath, eventName)).toMatchObject({
|
|
141
|
-
'rust-code-changed': 'true',
|
|
142
|
-
'rust-changed': 'true',
|
|
143
|
-
'any-rust-code-changed': 'true',
|
|
144
|
-
'any-code-changed': 'true',
|
|
145
|
-
'docs-changed': 'true',
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
test.each([
|
|
151
|
-
['docs-only', { featureCode: false }],
|
|
152
|
-
['base branch code only', { featureCode: false, baseCode: true }],
|
|
153
|
-
])('keeps code checks disabled for %s PRs', (_name, options) => {
|
|
154
|
-
repoPath = createMergedFeatureRepo(options);
|
|
155
|
-
|
|
156
|
-
expect(runDetector(repoPath, 'pull_request')).toMatchObject({
|
|
157
|
-
'rust-code-changed': 'false',
|
|
158
|
-
'any-code-changed': 'false',
|
|
159
|
-
'docs-changed': 'true',
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
test('detects files when a push only has one commit', () => {
|
|
164
|
-
repoPath = createSingleCommitRepo();
|
|
165
|
-
|
|
166
|
-
const outputs = runDetector(repoPath, 'push');
|
|
167
|
-
|
|
168
|
-
expect(outputs['js-code-changed']).toBe('true');
|
|
169
|
-
expect(outputs['js-changed']).toBe('true');
|
|
170
|
-
expect(outputs['any-js-code-changed']).toBe('true');
|
|
171
|
-
expect(outputs['any-code-changed']).toBe('true');
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
test('runs JS checks for CI tests without requiring a changeset', () => {
|
|
175
|
-
repoPath = createSingleCommitRepoWithFiles({
|
|
176
|
-
'js/jest.config.mjs': 'export default {};\n',
|
|
177
|
-
'js/tests/ci/workflow-policy.test.js': 'test("ok", () => {});\n',
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
const outputs = runDetector(repoPath, 'push');
|
|
181
|
-
|
|
182
|
-
expect(outputs['js-code-changed']).toBe('false');
|
|
183
|
-
expect(outputs['js-changed']).toBe('true');
|
|
184
|
-
expect(outputs['any-js-code-changed']).toBe('true');
|
|
185
|
-
expect(outputs['any-code-changed']).toBe('true');
|
|
186
|
-
});
|
|
187
|
-
});
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { spawnSync } from 'node:child_process';
|
|
3
|
-
|
|
4
|
-
test.each(['js', 'rust'])(
|
|
5
|
-
'%s Docker build fails when every browser package installation attempt fails',
|
|
6
|
-
(implementation) => {
|
|
7
|
-
const dockerfile = readFileSync(
|
|
8
|
-
new globalThis.URL(
|
|
9
|
-
`../../../${implementation}/Dockerfile`,
|
|
10
|
-
import.meta.url
|
|
11
|
-
),
|
|
12
|
-
'utf8'
|
|
13
|
-
);
|
|
14
|
-
const loop = dockerfile.match(/for i in 1 2 3; do[\s\S]*?done/)[0];
|
|
15
|
-
// Execute only the finite retry loop; stub installation and waiting.
|
|
16
|
-
const result = spawnSync(
|
|
17
|
-
'bash',
|
|
18
|
-
[
|
|
19
|
-
'-c',
|
|
20
|
-
`
|
|
21
|
-
apt-get() { printf '%s\\n' "$1"; [ "$1" = update ]; }
|
|
22
|
-
sleep() { return 0; }
|
|
23
|
-
${loop}
|
|
24
|
-
`,
|
|
25
|
-
],
|
|
26
|
-
{ encoding: 'utf8' }
|
|
27
|
-
);
|
|
28
|
-
expect(result.stdout.match(/^install$/gm)).toHaveLength(3);
|
|
29
|
-
expect(result.status).toBe(1);
|
|
30
|
-
}
|
|
31
|
-
);
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'fs';
|
|
2
|
-
import { dirname, resolve } from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
|
|
5
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
-
const __dirname = dirname(__filename);
|
|
7
|
-
const repoRoot = resolve(__dirname, '../../..');
|
|
8
|
-
|
|
9
|
-
function parseVersion(version) {
|
|
10
|
-
return version.split('.').map((part) => Number(part));
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function compareVersions(left, right) {
|
|
14
|
-
const leftParts = parseVersion(left);
|
|
15
|
-
const rightParts = parseVersion(right);
|
|
16
|
-
const maxLength = Math.max(leftParts.length, rightParts.length);
|
|
17
|
-
|
|
18
|
-
for (let index = 0; index < maxLength; index += 1) {
|
|
19
|
-
const leftPart = leftParts[index] || 0;
|
|
20
|
-
const rightPart = rightParts[index] || 0;
|
|
21
|
-
|
|
22
|
-
if (leftPart !== rightPart) {
|
|
23
|
-
return leftPart - rightPart;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return 0;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
describe('Rust Dockerfile', () => {
|
|
31
|
-
test('uses a Rust builder image that satisfies Cargo.toml rust-version', () => {
|
|
32
|
-
const cargoToml = readFileSync(
|
|
33
|
-
resolve(repoRoot, 'rust/Cargo.toml'),
|
|
34
|
-
'utf8'
|
|
35
|
-
);
|
|
36
|
-
const dockerfile = readFileSync(
|
|
37
|
-
resolve(repoRoot, 'rust/Dockerfile'),
|
|
38
|
-
'utf8'
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
const requiredRustVersion = cargoToml.match(
|
|
42
|
-
/^rust-version = "([^"]+)"/m
|
|
43
|
-
)?.[1];
|
|
44
|
-
const dockerRustVersion = dockerfile.match(
|
|
45
|
-
/^FROM rust:(\d+(?:\.\d+){1,2})[-\s]/m
|
|
46
|
-
)?.[1];
|
|
47
|
-
|
|
48
|
-
expect(requiredRustVersion).toBeDefined();
|
|
49
|
-
expect(dockerRustVersion).toBeDefined();
|
|
50
|
-
expect(
|
|
51
|
-
compareVersions(dockerRustVersion, requiredRustVersion)
|
|
52
|
-
).toBeGreaterThanOrEqual(0);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'fs';
|
|
2
|
-
import { dirname, resolve } from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
|
|
5
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
-
const __dirname = dirname(__filename);
|
|
7
|
-
const repoRoot = resolve(__dirname, '../../..');
|
|
8
|
-
|
|
9
|
-
const workflowFiles = [
|
|
10
|
-
'.github/workflows/js.yml',
|
|
11
|
-
'.github/workflows/rust.yml',
|
|
12
|
-
'.github/workflows/parity.yml',
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
function readRootFile(relativePath) {
|
|
16
|
-
return readFileSync(resolve(repoRoot, relativePath), 'utf8');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function readWorkflow(relativePath) {
|
|
20
|
-
return readRootFile(relativePath);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
describe('workflow policy', () => {
|
|
24
|
-
test.each(workflowFiles)(
|
|
25
|
-
'%s uses release-safe concurrency cancellation',
|
|
26
|
-
(workflowFile) => {
|
|
27
|
-
const workflow = readWorkflow(workflowFile);
|
|
28
|
-
|
|
29
|
-
expect(workflow).toContain(
|
|
30
|
-
"cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}"
|
|
31
|
-
);
|
|
32
|
-
expect(workflow).not.toContain(
|
|
33
|
-
"cancel-in-progress: ${{ github.ref == 'refs/heads/main' }}"
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
test.each(workflowFiles)(
|
|
39
|
-
'%s uses the current checkout action',
|
|
40
|
-
(workflowFile) => {
|
|
41
|
-
const workflow = readWorkflow(workflowFile);
|
|
42
|
-
|
|
43
|
-
expect(workflow).toContain('uses: actions/checkout@v7.0.1');
|
|
44
|
-
expect(workflow).not.toContain('uses: actions/checkout@v4');
|
|
45
|
-
}
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
test('Rust workflow uses the current cache action', () => {
|
|
49
|
-
const workflow = readWorkflow('.github/workflows/rust.yml');
|
|
50
|
-
|
|
51
|
-
expect(workflow).toContain('uses: actions/cache@v6.1.0');
|
|
52
|
-
expect(workflow).not.toContain('uses: actions/cache@v4');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test('parity check ignores repository-level CI tests', () => {
|
|
56
|
-
const parityScript = readRootFile('scripts/check-js-rust-parity.mjs');
|
|
57
|
-
|
|
58
|
-
expect(parityScript).toContain('"js/tests/ci/"');
|
|
59
|
-
expect(parityScript).toContain(
|
|
60
|
-
'matchingFiles(files, JS_PARITY_PATHS, JS_PARITY_IGNORE_PATHS)'
|
|
61
|
-
);
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test('release workflows require dependency health gates', () => {
|
|
66
|
-
const js = readWorkflow('.github/workflows/js.yml');
|
|
67
|
-
expect(js).toContain('npm audit --package-lock-only --audit-level=moderate');
|
|
68
|
-
expect(js).toContain('check-dependency-freshness.mjs npm');
|
|
69
|
-
expect(js).toContain('needs: [lint, test, dependency-health]');
|
|
70
|
-
const rust = readWorkflow('.github/workflows/rust.yml');
|
|
71
|
-
expect(rust).toContain('check-dependency-freshness.mjs cargo');
|
|
72
|
-
expect(rust).toContain('cargo update --dry-run');
|
|
73
|
-
expect(rust).toContain('needs: [lint, test, build, dependency-health]');
|
|
74
|
-
for (const workflow of [js, rust]) {
|
|
75
|
-
expect(
|
|
76
|
-
workflow.match(/needs\.dependency-health\.result == 'success'/g)
|
|
77
|
-
).toHaveLength(2);
|
|
78
|
-
}
|
|
79
|
-
});
|