@link-assistant/web-capture 1.10.8 → 1.10.9
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
CHANGED
package/jest.config.mjs
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,174 @@
|
|
|
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
|
+
const repoPath = mkdtempSync(join(tmpdir(), 'web-capture-detect-'));
|
|
44
|
+
|
|
45
|
+
initializeRepo(repoPath);
|
|
46
|
+
|
|
47
|
+
writeTrackedFile(repoPath, 'README.md', '# fixture\n');
|
|
48
|
+
commitAll(repoPath, 'initial commit');
|
|
49
|
+
|
|
50
|
+
git(repoPath, ['checkout', '-b', 'feature']);
|
|
51
|
+
writeTrackedFile(repoPath, 'rust/src/lib.rs', 'pub fn capture() {}\n');
|
|
52
|
+
commitAll(repoPath, 'add rust code');
|
|
53
|
+
|
|
54
|
+
writeTrackedFile(repoPath, 'docs/notes.md', '# docs only tail commit\n');
|
|
55
|
+
commitAll(repoPath, 'document rust change');
|
|
56
|
+
|
|
57
|
+
git(repoPath, ['checkout', 'main']);
|
|
58
|
+
git(repoPath, ['merge', '--no-ff', 'feature', '-m', 'merge feature']);
|
|
59
|
+
|
|
60
|
+
return repoPath;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function createSingleCommitRepo() {
|
|
64
|
+
return createSingleCommitRepoWithFiles({
|
|
65
|
+
'js/src/index.js': 'export const ok = true;\n',
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function createSingleCommitRepoWithFiles(files) {
|
|
70
|
+
const repoPath = mkdtempSync(join(tmpdir(), 'web-capture-detect-'));
|
|
71
|
+
|
|
72
|
+
initializeRepo(repoPath);
|
|
73
|
+
for (const [relativePath, contents] of Object.entries(files)) {
|
|
74
|
+
writeTrackedFile(repoPath, relativePath, contents);
|
|
75
|
+
}
|
|
76
|
+
commitAll(repoPath, 'initial commit');
|
|
77
|
+
|
|
78
|
+
return repoPath;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function initializeRepo(repoPath) {
|
|
82
|
+
git(repoPath, ['init']);
|
|
83
|
+
git(repoPath, ['config', 'user.email', 'ci@example.test']);
|
|
84
|
+
git(repoPath, ['config', 'user.name', 'CI Test']);
|
|
85
|
+
git(repoPath, ['checkout', '-b', 'main']);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseOutputs(outputFile) {
|
|
89
|
+
return Object.fromEntries(
|
|
90
|
+
readFileSync(outputFile, 'utf8')
|
|
91
|
+
.trim()
|
|
92
|
+
.split('\n')
|
|
93
|
+
.filter(Boolean)
|
|
94
|
+
.map((line) => {
|
|
95
|
+
const separatorIndex = line.indexOf('=');
|
|
96
|
+
return [line.slice(0, separatorIndex), line.slice(separatorIndex + 1)];
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function runDetector(repoPath, eventName) {
|
|
102
|
+
const outputFile = join(repoPath, `github-output-${eventName}.txt`);
|
|
103
|
+
const env = {
|
|
104
|
+
...process.env,
|
|
105
|
+
GITHUB_EVENT_NAME: eventName,
|
|
106
|
+
GITHUB_OUTPUT: outputFile,
|
|
107
|
+
};
|
|
108
|
+
delete env.GITHUB_BASE_SHA;
|
|
109
|
+
delete env.GITHUB_HEAD_SHA;
|
|
110
|
+
|
|
111
|
+
run(process.execPath, [detectorPath], repoPath, { env });
|
|
112
|
+
|
|
113
|
+
return parseOutputs(outputFile);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
describe('detect-code-changes', () => {
|
|
117
|
+
let repoPath;
|
|
118
|
+
|
|
119
|
+
afterEach(() => {
|
|
120
|
+
if (repoPath) {
|
|
121
|
+
rmSync(repoPath, { recursive: true, force: true });
|
|
122
|
+
repoPath = undefined;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('detects code introduced by a real merge commit pushed to main', () => {
|
|
127
|
+
repoPath = createMergedFeatureRepo();
|
|
128
|
+
|
|
129
|
+
const outputs = runDetector(repoPath, 'push');
|
|
130
|
+
|
|
131
|
+
expect(outputs['rust-code-changed']).toBe('true');
|
|
132
|
+
expect(outputs['rust-changed']).toBe('true');
|
|
133
|
+
expect(outputs['any-rust-code-changed']).toBe('true');
|
|
134
|
+
expect(outputs['any-code-changed']).toBe('true');
|
|
135
|
+
expect(outputs['docs-changed']).toBe('true');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('keeps pull request merge commits scoped to the PR head commit', () => {
|
|
139
|
+
repoPath = createMergedFeatureRepo();
|
|
140
|
+
|
|
141
|
+
const outputs = runDetector(repoPath, 'pull_request');
|
|
142
|
+
|
|
143
|
+
expect(outputs['rust-code-changed']).toBe('false');
|
|
144
|
+
expect(outputs['rust-changed']).toBe('false');
|
|
145
|
+
expect(outputs['any-rust-code-changed']).toBe('false');
|
|
146
|
+
expect(outputs['any-code-changed']).toBe('false');
|
|
147
|
+
expect(outputs['docs-changed']).toBe('true');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('detects files when a push only has one commit', () => {
|
|
151
|
+
repoPath = createSingleCommitRepo();
|
|
152
|
+
|
|
153
|
+
const outputs = runDetector(repoPath, 'push');
|
|
154
|
+
|
|
155
|
+
expect(outputs['js-code-changed']).toBe('true');
|
|
156
|
+
expect(outputs['js-changed']).toBe('true');
|
|
157
|
+
expect(outputs['any-js-code-changed']).toBe('true');
|
|
158
|
+
expect(outputs['any-code-changed']).toBe('true');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('runs JS checks for CI tests without requiring a changeset', () => {
|
|
162
|
+
repoPath = createSingleCommitRepoWithFiles({
|
|
163
|
+
'js/jest.config.mjs': 'export default {};\n',
|
|
164
|
+
'js/tests/ci/workflow-policy.test.js': 'test("ok", () => {});\n',
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const outputs = runDetector(repoPath, 'push');
|
|
168
|
+
|
|
169
|
+
expect(outputs['js-code-changed']).toBe('false');
|
|
170
|
+
expect(outputs['js-changed']).toBe('true');
|
|
171
|
+
expect(outputs['any-js-code-changed']).toBe('true');
|
|
172
|
+
expect(outputs['any-code-changed']).toBe('true');
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
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@v6');
|
|
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@v5');
|
|
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
|
+
});
|