browser-commander 0.26.0 → 0.26.2
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/.jscpd-baseline.json +6 -4
- package/CHANGELOG.md +30 -0
- package/package.json +2 -2
- package/scripts/check-duplication.mjs +176 -0
- package/scripts/create-manual-changeset.mjs +3 -1
- package/scripts/format-release-notes.mjs +8 -3
- package/scripts/merge-changesets.mjs +4 -1
- package/scripts/validate-changeset.mjs +23 -10
- package/scripts/version-and-commit.mjs +8 -10
- package/src/browser/webdriver.js +69 -3
- package/src/elements/selectors.js +31 -14
- package/src/parity/harness.js +4 -1
- package/src/tests/index.js +12 -1
- package/tests/e2e/engine-matrix.e2e.test.js +2 -1
- package/tests/e2e/safari.e2e.test.js +2 -1
- package/tests/e2e/webdriver.e2e.test.js +2 -1
- package/tests/helpers/fixture-server.js +18 -1
- package/tests/unit/browser/webdriver.test.js +100 -0
- package/tests/unit/elements/selectors.test.js +75 -0
- package/tests/unit/helpers/fixture-server.test.js +60 -0
- package/tests/unit/playwright-protocol-coverage.test.js +9 -6
- package/tests/unit/puppeteer-bindings-coverage.test.js +28 -7
- package/tests/unit/scripts/bot-commit-attribution.test.js +70 -0
- package/tests/unit/scripts/check-duplication.test.js +139 -0
- package/tests/unit/scripts/check-pipeline-status.test.js +264 -52
- package/tests/unit/scripts/check-version-modification.test.js +107 -1
- package/tests/unit/scripts/ci-timeout-budgets.test.js +15 -2
- package/tests/unit/scripts/ci-workflow-policy.test.js +91 -7
- package/tests/unit/scripts/command-stream-errexit.test.js +83 -41
- package/tests/unit/scripts/e2e-suite-coverage.test.js +174 -0
- package/tests/unit/scripts/github-actions-log.test.js +99 -0
- package/tests/unit/scripts/links-workflow.test.js +42 -0
- package/tests/unit/scripts/preflight-credentials.test.js +555 -0
- package/tests/unit/scripts/run-with-budget-warning.test.js +212 -6
- package/tests/unit/scripts/trusted-publishing.test.js +78 -0
- package/tests/unit/scripts/use-module-integration.test.js +2 -9
- package/tests/unit/scripts/use-module.test.js +20 -0
- package/tests/unit/scripts/validate-changeset.test.js +127 -0
- package/tests/unit/scripts/zizmor-version.test.js +82 -0
- package/tests/unit/tests/index.test.js +43 -0
package/.jscpd-baseline.json
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
"fingerprints": {
|
|
4
4
|
"00d381dd611ae8ac": 1,
|
|
5
5
|
"029a62ef1f48e856": 1,
|
|
6
|
-
"03241c1f33f40d14": 1,
|
|
7
6
|
"038170e72e0af1f5": 1,
|
|
7
|
+
"0413f601c8911a92": 1,
|
|
8
8
|
"04b28d01d0bb47dc": 1,
|
|
9
9
|
"04f605be138bc350": 1,
|
|
10
10
|
"0796d358ad2562c3": 1,
|
|
11
|
+
"080e47b73f48a044": 1,
|
|
11
12
|
"08e28c7706cdddd6": 1,
|
|
12
13
|
"090a69d9528e4cdb": 1,
|
|
13
14
|
"09694dc8fb9d683c": 1,
|
|
@@ -30,6 +31,7 @@
|
|
|
30
31
|
"17c26c0db2d35ad1": 1,
|
|
31
32
|
"193b46c0e7f72053": 1,
|
|
32
33
|
"19ff70fc80fad524": 1,
|
|
34
|
+
"1a9f0f0f68ed445c": 1,
|
|
33
35
|
"1ad4dc7e19a77f68": 1,
|
|
34
36
|
"1c1baf1bc8ea0eae": 1,
|
|
35
37
|
"1d61f5c49f12d2dc": 1,
|
|
@@ -79,7 +81,6 @@
|
|
|
79
81
|
"4715a17d690865ee": 1,
|
|
80
82
|
"4b0d3cc5c27227e7": 1,
|
|
81
83
|
"4c3b94caa28045d0": 1,
|
|
82
|
-
"4e090d6bb30e6551": 1,
|
|
83
84
|
"4f3c541b3f3e3f23": 1,
|
|
84
85
|
"4f93b074bc2ac9e8": 1,
|
|
85
86
|
"5085765e1f103f3e": 1,
|
|
@@ -114,6 +115,7 @@
|
|
|
114
115
|
"6880d620e0e67229": 1,
|
|
115
116
|
"69d50e6f6a5f2ee9": 1,
|
|
116
117
|
"6a7ed9ad3fc00a39": 1,
|
|
118
|
+
"6a9854bcdc9f71d7": 1,
|
|
117
119
|
"6af3c307939ef75e": 1,
|
|
118
120
|
"6b35b48512756192": 1,
|
|
119
121
|
"6cd00d6b6a039022": 1,
|
|
@@ -182,7 +184,7 @@
|
|
|
182
184
|
"acc95d694eb2f581": 1,
|
|
183
185
|
"ad4cfa76daa1b6fc": 1,
|
|
184
186
|
"ad50b48938af8aef": 1,
|
|
185
|
-
"
|
|
187
|
+
"add45ff3644fbeeb": 1,
|
|
186
188
|
"aee1e5c65f783848": 1,
|
|
187
189
|
"aee45e8b58c2d87f": 1,
|
|
188
190
|
"af702b20ebd45ef2": 1,
|
|
@@ -246,13 +248,13 @@
|
|
|
246
248
|
"e5feedc0c24e103e": 1,
|
|
247
249
|
"e67726a913675120": 1,
|
|
248
250
|
"e686288f2e9b8a6d": 1,
|
|
251
|
+
"e7d7bb7e163d2327": 1,
|
|
249
252
|
"ea1195048ded896c": 1,
|
|
250
253
|
"ea88711c5deb1fe5": 1,
|
|
251
254
|
"eb9a70c2a21094ff": 1,
|
|
252
255
|
"ebe03c5f8c2f5147": 1,
|
|
253
256
|
"ee19c8b9b332ee98": 1,
|
|
254
257
|
"eeca0ccf8d821f21": 1,
|
|
255
|
-
"f022b763ff342b2d": 1,
|
|
256
258
|
"f07f979f8c2706e8": 1,
|
|
257
259
|
"f186f73c118b8e8a": 1,
|
|
258
260
|
"f38f72095a67c95e": 1,
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.26.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- e2ef59f: Verify crates.io trusted publishing before releases and mark repository fixture manifests as non-publishable. Document the account setup required for PyPI and crates.io.
|
|
8
|
+
|
|
9
|
+
## 0.26.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- f2988a3: Close the parity probe server only after it stops listening, so a connection
|
|
14
|
+
that arrives during shutdown can no longer keep `close()` waiting. Test fixture
|
|
15
|
+
servers now drop idle browser preconnections too, which stopped the Safari
|
|
16
|
+
smoke test from timing out during cleanup on Node 24.
|
|
17
|
+
|
|
18
|
+
Parse Playwright `:has-text("…")` and `:text-is("…")` selectors with string
|
|
19
|
+
operations instead of a backtracking regular expression. A long selector that
|
|
20
|
+
did not match took quadratic time (16 seconds for 550,000 characters); it is
|
|
21
|
+
now rejected in linear time. CodeQL reported it as `js/polynomial-redos`.
|
|
22
|
+
|
|
23
|
+
Failure artifact names are trimmed of leading and trailing dashes by index, so
|
|
24
|
+
a test name with a long run of dashes no longer takes quadratic time.
|
|
25
|
+
|
|
26
|
+
A Safari launch right after a previous Safari session closed starts a fresh
|
|
27
|
+
`safaridriver`, up to three attempts, when the driver exits before it is ready
|
|
28
|
+
or refuses the connection. safaridriver serves one automation session at a
|
|
29
|
+
time, and CI saw both failures on back-to-back launches. A refused connection
|
|
30
|
+
now also names the driver server and whether it had exited. Authorization
|
|
31
|
+
errors are still reported at once. Set `VERBOSE=1` to log each retry.
|
|
32
|
+
|
|
3
33
|
## 0.26.0
|
|
4
34
|
|
|
5
35
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "browser-commander",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.2",
|
|
4
4
|
"description": "Universal browser automation library that supports Playwright, Puppeteer, and Selenium with a unified API",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"format": "prettier --write .",
|
|
33
33
|
"format:check": "prettier --check .",
|
|
34
34
|
"docs:api": "jsdoc -c jsdoc.json",
|
|
35
|
-
"check:duplication": "
|
|
35
|
+
"check:duplication": "node scripts/check-duplication.mjs",
|
|
36
36
|
"check": "npm run lint && npm run format:check && npm run check:duplication",
|
|
37
37
|
"changeset": "changeset",
|
|
38
38
|
"changeset:version": "node scripts/changeset-version.mjs",
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Duplication gate that prints the clones it fails on, and only those.
|
|
5
|
+
*
|
|
6
|
+
* With the console reporter, `jscpd --baseline … --fail-on-new-clones` lists
|
|
7
|
+
* every clone it finds, accepted or not. A passing run printed 272 "Clone
|
|
8
|
+
* found" blocks, about 840 lines, for clones the baseline already accepts. The
|
|
9
|
+
* one clone that failed a run was marked only by a `[NEW]` tag somewhere in
|
|
10
|
+
* that list (issue #128). This runs jscpd with the JSON reporter instead. It
|
|
11
|
+
* prints the new clones from the report (as error annotations in GitHub
|
|
12
|
+
* Actions) and a one-line total.
|
|
13
|
+
*
|
|
14
|
+
* `npx jscpd .` still prints every clone, and `.jscpd.json` keeps the console
|
|
15
|
+
* reporter for that.
|
|
16
|
+
*
|
|
17
|
+
* Usage (from js/): node scripts/check-duplication.mjs
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { spawnSync } from 'node:child_process';
|
|
21
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
22
|
+
import { createRequire } from 'node:module';
|
|
23
|
+
import { tmpdir } from 'node:os';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
|
|
27
|
+
const BASELINE = '.jscpd-baseline.json';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Path to jscpd's CLI entry point, or null when it is not installed. Running
|
|
31
|
+
* it through `process.execPath` avoids the `.bin` shims, which Windows cannot
|
|
32
|
+
* execute directly.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveJscpdCli() {
|
|
35
|
+
const require = createRequire(import.meta.url);
|
|
36
|
+
let manifestPath;
|
|
37
|
+
try {
|
|
38
|
+
manifestPath = require.resolve('jscpd/package.json');
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const { bin } = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
43
|
+
const entry = typeof bin === 'string' ? bin : bin?.jscpd;
|
|
44
|
+
const cli = entry && path.join(path.dirname(manifestPath), entry);
|
|
45
|
+
return cli && existsSync(cli) ? cli : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function escapeProperty(value) {
|
|
49
|
+
return String(value)
|
|
50
|
+
.replaceAll('%', '%25')
|
|
51
|
+
.replaceAll('\r', '%0D')
|
|
52
|
+
.replaceAll('\n', '%0A')
|
|
53
|
+
.replaceAll(':', '%3A')
|
|
54
|
+
.replaceAll(',', '%2C');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function escapeMessage(value) {
|
|
58
|
+
return String(value)
|
|
59
|
+
.replaceAll('%', '%25')
|
|
60
|
+
.replaceAll('\r', '%0D')
|
|
61
|
+
.replaceAll('\n', '%0A');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The new clones in a jscpd JSON report, one line each.
|
|
66
|
+
*
|
|
67
|
+
* jscpd names code embedded in Markdown `README.md:javascript`, so the
|
|
68
|
+
* language suffix is dropped before the name is used as a path. In GitHub
|
|
69
|
+
* Actions each clone becomes an `::error` annotation on the
|
|
70
|
+
* repository-relative path. `cwd` is where jscpd ran, and `workspace` is the
|
|
71
|
+
* checkout root.
|
|
72
|
+
*
|
|
73
|
+
* @param {{duplicates: Array<object>}} report
|
|
74
|
+
* @param {{githubActions?: boolean, cwd?: string, workspace?: string}} [options]
|
|
75
|
+
*/
|
|
76
|
+
export function formatNewClones(report, options = {}) {
|
|
77
|
+
const {
|
|
78
|
+
githubActions = false,
|
|
79
|
+
cwd = process.cwd(),
|
|
80
|
+
workspace = cwd,
|
|
81
|
+
} = options;
|
|
82
|
+
const fileOf = (side) => {
|
|
83
|
+
const name = side.name.replace(/:[a-z]+$/, '');
|
|
84
|
+
return githubActions
|
|
85
|
+
? path
|
|
86
|
+
.relative(workspace, path.resolve(cwd, name))
|
|
87
|
+
.split(path.sep)
|
|
88
|
+
.join('/')
|
|
89
|
+
: name;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return report.duplicates
|
|
93
|
+
.filter((duplicate) => duplicate.isNew)
|
|
94
|
+
.map(({ firstFile, secondFile, lines }) => {
|
|
95
|
+
const first = fileOf(firstFile);
|
|
96
|
+
const second = `${fileOf(secondFile)}:${secondFile.start}-${secondFile.end}`;
|
|
97
|
+
if (!githubActions) {
|
|
98
|
+
return `New clone: ${first}:${firstFile.start}-${firstFile.end} ~ ${second} (${lines} lines)\n`;
|
|
99
|
+
}
|
|
100
|
+
const properties = [
|
|
101
|
+
`file=${escapeProperty(first)}`,
|
|
102
|
+
`line=${firstFile.start}`,
|
|
103
|
+
`endLine=${firstFile.end}`,
|
|
104
|
+
'title=New duplicated code',
|
|
105
|
+
].join(',');
|
|
106
|
+
const message =
|
|
107
|
+
`${lines} lines duplicate ${second}. Extract the shared code, or ` +
|
|
108
|
+
'accept the clone with `npm run check:duplication:update`.';
|
|
109
|
+
return `::error ${properties}::${escapeMessage(message)}\n`;
|
|
110
|
+
})
|
|
111
|
+
.join('');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function main() {
|
|
115
|
+
const cli = resolveJscpdCli();
|
|
116
|
+
if (!cli) {
|
|
117
|
+
console.error('jscpd is not installed; run npm ci in js/.');
|
|
118
|
+
return 2;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const output = mkdtempSync(path.join(tmpdir(), 'jscpd-report-'));
|
|
122
|
+
try {
|
|
123
|
+
const jscpd = spawnSync(
|
|
124
|
+
process.execPath,
|
|
125
|
+
[
|
|
126
|
+
cli,
|
|
127
|
+
'.',
|
|
128
|
+
'--baseline',
|
|
129
|
+
BASELINE,
|
|
130
|
+
'--fail-on-new-clones',
|
|
131
|
+
'--reporters',
|
|
132
|
+
'json',
|
|
133
|
+
'--output',
|
|
134
|
+
output,
|
|
135
|
+
'--no-colors',
|
|
136
|
+
],
|
|
137
|
+
{ encoding: 'utf8' }
|
|
138
|
+
);
|
|
139
|
+
const reportPath = path.join(output, 'jscpd-report.json');
|
|
140
|
+
if (jscpd.error || !existsSync(reportPath)) {
|
|
141
|
+
// Fail closed: a run that produced no report proved nothing.
|
|
142
|
+
process.stdout.write(jscpd.stdout ?? '');
|
|
143
|
+
process.stderr.write(jscpd.stderr ?? '');
|
|
144
|
+
console.error(
|
|
145
|
+
`jscpd wrote no report (exit ${jscpd.status}): ${jscpd.error?.message ?? reportPath}`
|
|
146
|
+
);
|
|
147
|
+
return jscpd.status || 2;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const report = JSON.parse(readFileSync(reportPath, 'utf8'));
|
|
151
|
+
const newCount = report.duplicates.filter((d) => d.isNew).length;
|
|
152
|
+
process.stdout.write(
|
|
153
|
+
formatNewClones(report, {
|
|
154
|
+
githubActions: process.env.GITHUB_ACTIONS === 'true',
|
|
155
|
+
workspace: process.env.GITHUB_WORKSPACE || process.cwd(),
|
|
156
|
+
})
|
|
157
|
+
);
|
|
158
|
+
const total = report.duplicates.length;
|
|
159
|
+
console.log(
|
|
160
|
+
`jscpd: ${total} clone${total === 1 ? '' : 's'}, ${newCount} new ` +
|
|
161
|
+
`(the rest are accepted in ${BASELINE}).`
|
|
162
|
+
);
|
|
163
|
+
if (jscpd.status !== 0 && newCount === 0) {
|
|
164
|
+
// jscpd failed for a reason other than new clones; show what it said.
|
|
165
|
+
process.stdout.write(jscpd.stdout);
|
|
166
|
+
process.stderr.write(jscpd.stderr);
|
|
167
|
+
}
|
|
168
|
+
return jscpd.status ?? 2;
|
|
169
|
+
} finally {
|
|
170
|
+
rmSync(output, { recursive: true, force: true });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
175
|
+
process.exitCode = main();
|
|
176
|
+
}
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
import { writeFileSync } from 'fs';
|
|
14
14
|
import { randomBytes } from 'crypto';
|
|
15
15
|
|
|
16
|
+
import { printUntrusted } from '../../scripts/github-actions-log.mjs';
|
|
17
|
+
|
|
16
18
|
const PACKAGE_NAME = 'browser-commander';
|
|
17
19
|
|
|
18
20
|
import {
|
|
@@ -69,7 +71,7 @@ ${description}
|
|
|
69
71
|
|
|
70
72
|
console.log(`Created changeset: ${changesetFile}`);
|
|
71
73
|
console.log('Content:');
|
|
72
|
-
|
|
74
|
+
printUntrusted(content);
|
|
73
75
|
|
|
74
76
|
// Format with Prettier
|
|
75
77
|
console.log('\nFormatting with Prettier...');
|
|
@@ -70,6 +70,9 @@ if (!releaseId || !version || !repository) {
|
|
|
70
70
|
process.exit(1);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/** Start of the npm badge appended below, which marks a formatted body. */
|
|
74
|
+
const NPM_BADGE_PREFIX = '[. The
|
|
86
|
+
// badge's alt text marks it: matching the image host would also skip a
|
|
87
|
+
// body whose changelog text mentions that host.
|
|
88
|
+
if (currentBody.includes(NPM_BADGE_PREFIX)) {
|
|
84
89
|
console.log('ℹ️ Release notes already formatted');
|
|
85
90
|
process.exit(0);
|
|
86
91
|
}
|
|
@@ -186,7 +191,7 @@ try {
|
|
|
186
191
|
|
|
187
192
|
// Build formatted release notes
|
|
188
193
|
const versionWithoutV = version.replace(/^v/, '');
|
|
189
|
-
const npmBadge =
|
|
194
|
+
const npmBadge = `${NPM_BADGE_PREFIX}https://img.shields.io/badge/npm-${versionWithoutV}-blue.svg)](https://www.npmjs.com/package/${PACKAGE_NAME}/v/${versionWithoutV})`;
|
|
190
195
|
|
|
191
196
|
let formattedBody = `${cleanDescription}`;
|
|
192
197
|
|
|
@@ -23,6 +23,8 @@ import {
|
|
|
23
23
|
} from 'fs';
|
|
24
24
|
import { join } from 'path';
|
|
25
25
|
|
|
26
|
+
import { printUntrusted } from '../../scripts/github-actions-log.mjs';
|
|
27
|
+
|
|
26
28
|
const PACKAGE_NAME = 'browser-commander';
|
|
27
29
|
const CHANGESET_DIR = '.changeset';
|
|
28
30
|
|
|
@@ -254,7 +256,8 @@ function main() {
|
|
|
254
256
|
}
|
|
255
257
|
|
|
256
258
|
console.log('\nChangeset merge completed successfully');
|
|
257
|
-
console.log(
|
|
259
|
+
console.log('\nMerged changeset content:');
|
|
260
|
+
printUntrusted(mergedContent);
|
|
258
261
|
}
|
|
259
262
|
|
|
260
263
|
main();
|
|
@@ -7,13 +7,17 @@
|
|
|
7
7
|
* - Only checks changeset files ADDED by the current PR (not pre-existing ones)
|
|
8
8
|
* - Uses git diff to compare PR head against base branch
|
|
9
9
|
* - Validates that the PR adds exactly one changeset with proper format
|
|
10
|
-
* -
|
|
10
|
+
* - Fails when a pull request's diff cannot be computed (issue #128): checking
|
|
11
|
+
* every changeset on disk instead would let a stale one satisfy the check
|
|
12
|
+
* - Outside a pull request (local runs), checks the changesets on disk
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
15
|
import { execSync } from 'child_process';
|
|
14
16
|
import { readFileSync, readdirSync, existsSync } from 'fs';
|
|
15
17
|
import { join } from 'path';
|
|
16
18
|
|
|
19
|
+
import { printUntrusted } from '../../scripts/github-actions-log.mjs';
|
|
20
|
+
|
|
17
21
|
const PACKAGE_NAME = 'browser-commander';
|
|
18
22
|
// When running from js/ directory, the changeset dir is local
|
|
19
23
|
const CHANGESET_DIR = '.changeset';
|
|
@@ -113,13 +117,11 @@ function tryBaseBranchComparison(prBase) {
|
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
/**
|
|
116
|
-
*
|
|
120
|
+
* Outside a pull request: get all changesets in directory
|
|
117
121
|
* @returns {string[]} Array of all changeset file names
|
|
118
122
|
*/
|
|
119
123
|
function getAllChangesets() {
|
|
120
|
-
console.log(
|
|
121
|
-
'Warning: Could not determine PR diff, checking all changesets in directory'
|
|
122
|
-
);
|
|
124
|
+
console.log('No pull request to compare; checking all changesets on disk');
|
|
123
125
|
if (!existsSync(CHANGESET_DIR)) {
|
|
124
126
|
return [];
|
|
125
127
|
}
|
|
@@ -136,6 +138,8 @@ function getAddedChangesetFiles() {
|
|
|
136
138
|
const baseSha = process.env.GITHUB_BASE_SHA || process.env.BASE_SHA;
|
|
137
139
|
const headSha = process.env.GITHUB_HEAD_SHA || process.env.HEAD_SHA;
|
|
138
140
|
|
|
141
|
+
const prBase = process.env.GITHUB_BASE_REF;
|
|
142
|
+
|
|
139
143
|
// Try explicit SHAs first
|
|
140
144
|
if (baseSha && headSha) {
|
|
141
145
|
const result = tryExplicitShaComparison(baseSha, headSha);
|
|
@@ -145,7 +149,6 @@ function getAddedChangesetFiles() {
|
|
|
145
149
|
}
|
|
146
150
|
|
|
147
151
|
// Try base branch comparison
|
|
148
|
-
const prBase = process.env.GITHUB_BASE_REF;
|
|
149
152
|
if (prBase) {
|
|
150
153
|
const result = tryBaseBranchComparison(prBase);
|
|
151
154
|
if (result !== null) {
|
|
@@ -153,7 +156,14 @@ function getAddedChangesetFiles() {
|
|
|
153
156
|
}
|
|
154
157
|
}
|
|
155
158
|
|
|
156
|
-
//
|
|
159
|
+
// In a pull request, every changeset on disk includes ones already on the
|
|
160
|
+
// base branch, so falling back to them would pass a PR that adds none.
|
|
161
|
+
if (prBase || baseSha || headSha) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
'Could not determine which changesets this pull request adds, because git could not diff it against its base'
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
157
167
|
return getAllChangesets();
|
|
158
168
|
}
|
|
159
169
|
|
|
@@ -252,7 +262,9 @@ try {
|
|
|
252
262
|
console.error(`::error::${validation.error}`);
|
|
253
263
|
console.error(`\nFile content of ${changesetFile}:`);
|
|
254
264
|
try {
|
|
255
|
-
|
|
265
|
+
printUntrusted(readFileSync(changesetFile, 'utf-8'), {
|
|
266
|
+
stream: process.stderr,
|
|
267
|
+
});
|
|
256
268
|
} catch {
|
|
257
269
|
console.error('(could not read file)');
|
|
258
270
|
}
|
|
@@ -261,9 +273,10 @@ try {
|
|
|
261
273
|
|
|
262
274
|
console.log('Changeset validation passed');
|
|
263
275
|
console.log(` Type: ${validation.type}`);
|
|
264
|
-
console.log(
|
|
276
|
+
console.log(' Description:');
|
|
277
|
+
printUntrusted(validation.description);
|
|
265
278
|
} catch (error) {
|
|
266
|
-
console.error(
|
|
279
|
+
console.error(`::error::${error.message}`);
|
|
267
280
|
if (process.env.DEBUG) {
|
|
268
281
|
console.error('Stack trace:', error.stack);
|
|
269
282
|
}
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
loadCommandStream,
|
|
25
25
|
loadLinoArguments,
|
|
26
26
|
} from '../../scripts/use-module.mjs';
|
|
27
|
+
import { printUntrusted } from '../../scripts/github-actions-log.mjs';
|
|
27
28
|
|
|
28
29
|
const { $ } = await loadCommandStream();
|
|
29
30
|
const { makeConfig } = await loadLinoArguments();
|
|
@@ -53,11 +54,9 @@ const config = makeConfig({
|
|
|
53
54
|
const { mode, bumpType, description } = config;
|
|
54
55
|
|
|
55
56
|
// Debug: Log parsed configuration
|
|
56
|
-
console.log('Parsed configuration:', {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
description: description || '(none)',
|
|
60
|
-
});
|
|
57
|
+
console.log('Parsed configuration:', { mode, bumpType });
|
|
58
|
+
console.log('Description:');
|
|
59
|
+
printUntrusted(description || '(none)');
|
|
61
60
|
|
|
62
61
|
// Detect if positional arguments were used (common mistake)
|
|
63
62
|
const args = process.argv.slice(2);
|
|
@@ -145,7 +144,7 @@ async function main() {
|
|
|
145
144
|
try {
|
|
146
145
|
// Configure git
|
|
147
146
|
await $`git config user.name "github-actions[bot]"`;
|
|
148
|
-
await $`git config user.email "github-actions[bot]@users.noreply.github.com"`;
|
|
147
|
+
await $`git config user.email "41898282+github-actions[bot]@users.noreply.github.com"`;
|
|
149
148
|
|
|
150
149
|
// Check if remote main has advanced (handles re-runs after partial success)
|
|
151
150
|
console.log('Checking for remote changes...');
|
|
@@ -231,10 +230,9 @@ async function main() {
|
|
|
231
230
|
// Stage all changes (package.json, package-lock.json, CHANGELOG.md, deleted changesets)
|
|
232
231
|
await $`git add -A`;
|
|
233
232
|
|
|
234
|
-
// Commit with version number as message
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
await $`git commit -m "${escapedMessage}"`;
|
|
233
|
+
// Commit with version number as message. command-stream quotes the
|
|
234
|
+
// interpolated value itself; escaping it here would add backslashes.
|
|
235
|
+
await $`git commit -m ${newVersion}`;
|
|
238
236
|
|
|
239
237
|
// Push directly to main.
|
|
240
238
|
//
|
package/src/browser/webdriver.js
CHANGED
|
@@ -28,6 +28,7 @@ import { createRequire } from 'node:module';
|
|
|
28
28
|
import path from 'node:path';
|
|
29
29
|
|
|
30
30
|
import { createWebDriverPage } from '../core/webdriver-page.js';
|
|
31
|
+
import { isVerboseEnabled } from '../core/logger.js';
|
|
31
32
|
import {
|
|
32
33
|
runCommand as defaultRunCommand,
|
|
33
34
|
startProcess as defaultStartProcess,
|
|
@@ -484,6 +485,68 @@ async function teardown({
|
|
|
484
485
|
* @returns {Promise<{driver: Object, page: Object, close: function(): Promise<void>, serverUrl: string, driverProcess: Object, driverPath: string, driverSource: string, browser: string, userDataDir: string, temporaryProfile: boolean, args: Array<string>, bidi: boolean}>}
|
|
485
486
|
*/
|
|
486
487
|
export async function launchWebDriver(options = {}, dependencies = {}) {
|
|
488
|
+
if (!isSafariChannel(options.browser ?? 'chrome')) {
|
|
489
|
+
return await launchWebDriverOnce(options, dependencies);
|
|
490
|
+
}
|
|
491
|
+
const delay = dependencies.safariRetryDelay ?? SAFARI_RETRY_DELAY;
|
|
492
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
493
|
+
try {
|
|
494
|
+
return await launchWebDriverOnce(options, dependencies);
|
|
495
|
+
} catch (error) {
|
|
496
|
+
if (
|
|
497
|
+
attempt >= SAFARI_LAUNCH_ATTEMPTS ||
|
|
498
|
+
!TRANSIENT_DRIVER_START_ERROR.test(error?.message ?? '')
|
|
499
|
+
) {
|
|
500
|
+
throw error;
|
|
501
|
+
}
|
|
502
|
+
if (isVerboseEnabled()) {
|
|
503
|
+
console.warn(
|
|
504
|
+
`[webdriver] Safari launch attempt ${attempt} failed, retrying in ${delay}ms: ${error.message}`
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* safaridriver serves one Safari automation session at a time. Launched right
|
|
514
|
+
* after a previous session closed, CI saw a new driver exit with code 1 before
|
|
515
|
+
* it was ready, and a driver that was ready refuse the new-session request
|
|
516
|
+
* (issue #128). Both are start-up failures of a driver this process owns, so a
|
|
517
|
+
* Safari launch starts a fresh driver for them, a bounded number of times.
|
|
518
|
+
*/
|
|
519
|
+
const SAFARI_LAUNCH_ATTEMPTS = 3;
|
|
520
|
+
const SAFARI_RETRY_DELAY = 1000;
|
|
521
|
+
const TRANSIENT_DRIVER_START_ERROR =
|
|
522
|
+
/ECONNREFUSED|ECONNRESET|socket hang up|exited with code \d+ before it was ready/;
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Name the driver server behind a connection error from selenium, which only
|
|
526
|
+
* says which port refused: whether the server had exited, and its last output.
|
|
527
|
+
*/
|
|
528
|
+
function withDriverContext(error, { serverUrl, exitCode, output }) {
|
|
529
|
+
if (
|
|
530
|
+
!serverUrl ||
|
|
531
|
+
!/ECONNREFUSED|ECONNRESET|socket hang up/.test(error?.message ?? '')
|
|
532
|
+
) {
|
|
533
|
+
return error;
|
|
534
|
+
}
|
|
535
|
+
const state =
|
|
536
|
+
exitCode === null || exitCode === undefined
|
|
537
|
+
? 'was still running'
|
|
538
|
+
: `had exited with code ${exitCode}`;
|
|
539
|
+
const tail = output.trim();
|
|
540
|
+
return Object.assign(
|
|
541
|
+
new Error(
|
|
542
|
+
`${error.message} (WebDriver server at ${serverUrl} ${state}${tail ? `: ${tail}` : ''})`,
|
|
543
|
+
{ cause: error }
|
|
544
|
+
),
|
|
545
|
+
{ code: error.code }
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function launchWebDriverOnce(options, dependencies) {
|
|
487
550
|
const {
|
|
488
551
|
browser = 'chrome',
|
|
489
552
|
executablePath,
|
|
@@ -533,15 +596,16 @@ export async function launchWebDriver(options = {}, dependencies = {}) {
|
|
|
533
596
|
|
|
534
597
|
let driverProcess;
|
|
535
598
|
let driver;
|
|
599
|
+
let serverUrl;
|
|
600
|
+
let output = '';
|
|
536
601
|
try {
|
|
537
602
|
const port = await reservePort();
|
|
538
|
-
|
|
603
|
+
serverUrl = `http://${LOOPBACK_HOST}:${port}`;
|
|
539
604
|
driverProcess = startProcess(
|
|
540
605
|
resolved.driverPath,
|
|
541
606
|
buildDriverServerArgs(browser, port),
|
|
542
607
|
{ env: childEnv, forwardOutput: verbose }
|
|
543
608
|
);
|
|
544
|
-
let output = '';
|
|
545
609
|
for (const stream of [driverProcess.stdout, driverProcess.stderr]) {
|
|
546
610
|
stream?.on('data', (chunk) => {
|
|
547
611
|
output = `${output}${chunk}`.slice(-4000);
|
|
@@ -602,8 +666,10 @@ export async function launchWebDriver(options = {}, dependencies = {}) {
|
|
|
602
666
|
bidi,
|
|
603
667
|
};
|
|
604
668
|
} catch (error) {
|
|
669
|
+
const exitCode = driverProcess?.exitCode;
|
|
605
670
|
await teardown({ driver, driverProcess, temporaryProfile, userDataDir });
|
|
606
|
-
|
|
671
|
+
const failure = withDriverContext(error, { serverUrl, exitCode, output });
|
|
672
|
+
throw safari ? safariLaunchError(failure, browser) : failure;
|
|
607
673
|
}
|
|
608
674
|
}
|
|
609
675
|
|
|
@@ -117,6 +117,31 @@ function isPlaywrightTextSelector(selector) {
|
|
|
117
117
|
return selector.includes(':has-text(') || selector.includes(':text-is(');
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Split `base:<pseudo>("text")` into its base selector and text.
|
|
122
|
+
*
|
|
123
|
+
* String operations rather than a regular expression: the former
|
|
124
|
+
* /^(.+?):has-text\("(.+?)"\)$/ backtracked quadratically on long selectors
|
|
125
|
+
* that do not match (CodeQL js/polynomial-redos). The base is everything before
|
|
126
|
+
* the first `:<pseudo>("`, as the lazy `(.+?)` made it; base and text must be
|
|
127
|
+
* non-empty.
|
|
128
|
+
* @param {string} selector - Selector to split
|
|
129
|
+
* @param {string} pseudo - Pseudo-class name, e.g. 'has-text'
|
|
130
|
+
* @returns {{baseSelector: string, text: string}|null} - Parts, or null
|
|
131
|
+
*/
|
|
132
|
+
function splitTextPseudo(selector, pseudo) {
|
|
133
|
+
const open = `:${pseudo}("`;
|
|
134
|
+
const start = selector.indexOf(open, 1);
|
|
135
|
+
if (start === -1 || !selector.endsWith('")')) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const text = selector.slice(start + open.length, -2);
|
|
139
|
+
if (!text) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
return { baseSelector: selector.slice(0, start), text };
|
|
143
|
+
}
|
|
144
|
+
|
|
120
145
|
/**
|
|
121
146
|
* Parse a Playwright text selector to extract base selector and text
|
|
122
147
|
* @param {string} selector - Playwright text selector like 'a:has-text("text")'
|
|
@@ -124,22 +149,14 @@ function isPlaywrightTextSelector(selector) {
|
|
|
124
149
|
*/
|
|
125
150
|
function parsePlaywrightTextSelector(selector) {
|
|
126
151
|
// Match patterns like 'a:has-text("text")' or 'button:text-is("exact text")'
|
|
127
|
-
const
|
|
128
|
-
if (
|
|
129
|
-
return {
|
|
130
|
-
baseSelector: hasTextMatch[1],
|
|
131
|
-
text: hasTextMatch[2],
|
|
132
|
-
exact: false,
|
|
133
|
-
};
|
|
152
|
+
const hasText = splitTextPseudo(selector, 'has-text');
|
|
153
|
+
if (hasText) {
|
|
154
|
+
return { ...hasText, exact: false };
|
|
134
155
|
}
|
|
135
156
|
|
|
136
|
-
const
|
|
137
|
-
if (
|
|
138
|
-
return {
|
|
139
|
-
baseSelector: textIsMatch[1],
|
|
140
|
-
text: textIsMatch[2],
|
|
141
|
-
exact: true,
|
|
142
|
-
};
|
|
157
|
+
const textIs = splitTextPseudo(selector, 'text-is');
|
|
158
|
+
if (textIs) {
|
|
159
|
+
return { ...textIs, exact: true };
|
|
143
160
|
}
|
|
144
161
|
|
|
145
162
|
return null;
|
package/src/parity/harness.js
CHANGED
|
@@ -109,8 +109,11 @@ export async function startProbeServer(probeSource) {
|
|
|
109
109
|
});
|
|
110
110
|
},
|
|
111
111
|
async close() {
|
|
112
|
+
// Stop listening before dropping connections, so none can slip in
|
|
113
|
+
// between and keep close() waiting (issue #128).
|
|
114
|
+
const closed = new Promise((resolve) => server.close(resolve));
|
|
112
115
|
server.closeAllConnections?.();
|
|
113
|
-
await
|
|
116
|
+
await closed;
|
|
114
117
|
},
|
|
115
118
|
};
|
|
116
119
|
}
|