12 Commits

Author SHA1 Message Date
Yury Semikhatsky 655530f6d0 devops: restore npm publishing from GitHub Actions (#459)
## Summary
- Release publishing goes back to the GitHub Actions workflow with OIDC
trusted publishing and provenance.
- The ESRP pipeline stays available for manual on-demand runs only (no
automatic triggers).
2026-09-03 12:04:51 -07:00
Yury Semikhatsky 397ee39c83 chore: mark v0.1.19 (#458) 2026-09-01 08:38:23 -07:00
Yury Semikhatsky 58d9406780 chore: roll Playwright to 1.63.0-alpha-2026-08-31 (#457) 2026-09-01 08:36:57 -07:00
Yury Semikhatsky cbc09311c4 devops: publish all npm versions via ESRP pipeline (#456)
The ESRP pipeline now publishes @next with current timestamp on manual
runs and @latest from v* release tags. The GitHub Actions npm publish
workflow is removed.
2026-08-27 15:21:41 -07:00
Mert Can Altin 60cb176373 fix(cli): call defaultRegistryDirectory() when resolving the cache file (#454)
`registry.defaultRegistryDirectory` became a function upstream in
microsoft/playwright#41942 and arrived here with the roll to
1.63.0-alpha-2026-08-05. `cacheFile()` still used it as a string, so
`path.join()` threw, `readCache()`/`writeCache()` swallowed the
TypeError, and the update check was never cached: every single CLI
invocation fetched the npm registry and re-ran the installed-skill
check.

Locally that is ~480ms per command instead of ~125ms.

Tests always set PLAYWRIGHT_CLI_INSTALLATION_FOR_TEST, so the default
branch was never exercised. Add a regression test that points HOME at a
temp directory and asserts the cache file is written.

Also hoist `cacheFile()` out of the try blocks so only I/O and parse
failures are swallowed there, instead of masking a path-computation bug
as "no cache".
2026-08-24 16:15:19 -06:00
dependabot[bot] 2f85a94b7b chore(deps): bump the github-actions group with 2 updates (#451) 2026-08-19 12:49:25 -07:00
Dan Fiedler 578643330f Pin GitHub Actions to full-length commit SHAs (#450) 2026-08-19 09:33:15 -07:00
Pavel Feldman 4f9d85a947 fix(cli): apply update-check policy to installed skill check (#447) 2026-08-13 11:39:25 -07:00
Yury Semikhatsky e2d8f311f3 devops: add ESRP pipeline for publishing npm alpha versions (#446) 2026-08-07 11:18:33 -07:00
Yury Semikhatsky ca196c2971 chore: mark v0.1.18 (#444) 2026-08-05 17:12:10 -07:00
Yury Semikhatsky 40783a0063 chore: roll Playwright to 1.63.0-alpha-2026-08-05 (#442) 2026-08-05 15:44:50 -07:00
Dmitry Gozman eee5a185c9 fix(skills): ignore line ending differences when checking installed skill (#439)
Fixes: https://github.com/microsoft/playwright/issues/41760
2026-07-15 17:22:37 +01:00
14 changed files with 264 additions and 65 deletions
+141
View File
@@ -0,0 +1,141 @@
# Publishes @playwright/cli via ESRP. Manual trigger only, regular publishing
# is done from GitHub Actions, see .github/workflows/publish.yml.
# Depending on the selected ref, a manual run publishes:
# - @next (alpha with current timestamp) from main
# - @latest from v* release tags
trigger: none
pr: none
resources:
repositories:
- repository: 1esPipelines
type: git
name: 1ESPipelineTemplates/1ESPipelineTemplates
ref: refs/tags/release
extends:
template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines
parameters:
pool:
name: DevDivPlaywrightAzurePipelinesUbuntu2204
os: linux
sdl:
sourceAnalysisPool:
# SDL tools require windows, see https://aka.ms/AAo6v8e
name: DevDivPlaywrightAzurePipelinesWindows2022
os: windows
stages:
- stage: Stage
jobs:
- job: Build
displayName: "Build npm package"
templateContext:
outputs:
- output: pipelineArtifact
path: $(Build.ArtifactStagingDirectory)/esrp-build
artifact: esrp-build
steps:
- checkout: self
displayName: "Checkout code"
- task: Bash@3
displayName: "Check the branch is main or a v* tag"
inputs:
targetType: "inline"
script: |
if [[ "$BUILD_SOURCE_BRANCH" != "refs/heads/main" && "$BUILD_SOURCE_BRANCH" != refs/tags/v* ]]; then
echo "Can only publish from main or v* tags."
echo "Unexpected branch: $BUILD_SOURCE_BRANCH"
exit 1
fi
env:
BUILD_SOURCE_BRANCH: $(Build.SourceBranch)
- task: UseNode@1
inputs:
version: '24.x'
displayName: "Install Node.js"
- task: Bash@3
displayName: "setup .npmrc"
inputs:
targetType: "inline"
script: |
echo "registry=https://devdiv.pkgs.visualstudio.com/DevDiv/_packaging/DevDiv_PublicPackages/npm/registry/" >> .npmrc
- task: npmAuthenticate@0
displayName: "authenticate the private npm registry"
inputs:
workingFile: .npmrc
- script: npm ci
displayName: "npm ci"
- task: Bash@3
name: setVersion
displayName: "Set version and dist-tag"
inputs:
targetType: "inline"
script: |
set -e
BASE_VERSION=$(node -p "require('./package.json').version.split('-')[0]")
if [[ "$BUILD_SOURCE_BRANCH" == refs/tags/v* ]]; then
# Release version is already checked in, only publish what the tag points at.
NPM_DIST_TAG="latest"
if [[ "$BUILD_SOURCE_BRANCH" != "refs/tags/v$BASE_VERSION" ]]; then
echo "ERROR: version '$BASE_VERSION' does not match tag '$BUILD_SOURCE_BRANCH'"
exit 1
fi
else
NPM_DIST_TAG="next"
npm version "${BASE_VERSION}-alpha-$(date +%s)000" --no-git-tag-version
fi
echo "Publishing version $(node -p "require('./package.json').version") with dist-tag $NPM_DIST_TAG"
echo "##vso[task.setvariable variable=npmDistTag;isOutput=true]$NPM_DIST_TAG"
env:
BUILD_SOURCE_BRANCH: $(Build.SourceBranch)
- task: Bash@3
displayName: "Pack the package"
inputs:
targetType: "inline"
script: |
set -e
mkdir -p "$(Build.ArtifactStagingDirectory)/esrp-build"
npm pack --pack-destination="$(Build.ArtifactStagingDirectory)/esrp-build"
ls -la "$(Build.ArtifactStagingDirectory)/esrp-build"
- job: Publish
displayName: "ESRP Release to npm"
dependsOn: Build
variables:
npmDistTag: $[ dependencies.Build.outputs['setVersion.npmDistTag'] ]
templateContext:
type: releaseJob
isProduction: true
inputs:
- input: pipelineArtifact
artifactName: esrp-build
targetPath: $(Build.ArtifactStagingDirectory)/esrp-build
steps:
- checkout: none
- task: EsrpRelease@11
inputs:
connectedservicename: 'Playwright-ESRP-PME'
usemanagedidentity: true
keyvaultname: 'playwright-esrp-pme'
signcertname: 'ESRP-Release-Sign'
clientid: '13434a40-7de4-4c23-81a3-d843dc81c2c5'
intent: 'PackageDistribution'
contenttype: 'npm'
# npm dist-tag to publish with.
productstate: '$(npmDistTag)'
folderlocation: '$(Build.ArtifactStagingDirectory)/esrp-build'
waitforreleasecompletion: true
owners: 'yurys@microsoft.com'
approvers: 'yurys@microsoft.com'
serviceendpointurl: 'https://api.esrp.microsoft.com'
mainpublisher: 'Playwright'
domaintenantid: '975f013f-7f24-47e8-a7d3-abc4752bf346'
displayName: 'ESRP Release to npm'
+2 -1
View File
@@ -6,8 +6,9 @@
`npm info playwright@next version`
2. **Update Playwright packages** in `package.json`:
- Update `playwright` (dependency) and `@playwright/test` (devDependency) to the target version.
- Update `playwright` and `playwright-core` (dependencies) and `@playwright/test` (devDependency) to the target version.
- Run `npm install` to update `package-lock.json`.
- Verify with `npm ls playwright-core` that the root `playwright-core` actually rolled — it is pinned directly in `package.json` and is what `playwright-cli.js` runs; forgetting it leaves the CLI on the old version while tests pass against the new `@playwright/test`.
3. **Run the update script** to sync skills and README:
```bash
+11
View File
@@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
groups:
github-actions:
patterns: ["*"]
schedule:
interval: "weekly"
cooldown:
default-days: 7
+2 -2
View File
@@ -14,9 +14,9 @@ jobs:
os: [ubuntu-latest, macos-15, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Use Node.js 20
uses: actions/setup-node@v5
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '20'
cache: 'npm'
+2 -2
View File
@@ -12,8 +12,8 @@ jobs:
contents: read
id-token: write # Required for OIDC npm publishing
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
registry-url: https://registry.npmjs.org/
+3
View File
@@ -4,3 +4,6 @@ node_modules/
/.playwright-cli/
# Ignore self-skill which is a build artifact
.claude/skills/playwright-cli/
.npmrc
# Playwright CLI output (may contain credentials)
.playwright-cli/
+2
View File
@@ -248,6 +248,8 @@ playwright-cli run-code <code> # run playwright code snippet
playwright-cli run-code --filename=f # run playwright code from a file
playwright-cli tracing-start # start trace recording
playwright-cli tracing-stop # stop trace recording
playwright-cli recording-start # record user actions in the browser
playwright-cli recording-stop # stop recording, print actions as Playwright code
playwright-cli video-start [filename] # start video recording
playwright-cli video-chapter <title> # add a chapter marker to the video
playwright-cli video-show-actions # annotate each action with a callout in the video
+16 -33
View File
@@ -1,22 +1,22 @@
{
"name": "@playwright/cli",
"version": "0.1.17",
"version": "0.1.19",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@playwright/cli",
"version": "0.1.17",
"version": "0.1.19",
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.62.0-alpha-1783623505000",
"playwright-core": "1.62.0-alpha-1783623505000"
"playwright": "1.63.0-alpha-2026-08-31",
"playwright-core": "1.63.0-alpha-2026-08-31"
},
"bin": {
"playwright-cli": "playwright-cli.js"
},
"devDependencies": {
"@playwright/test": "1.62.0-alpha-1783623505000",
"@playwright/test": "1.63.0-alpha-2026-08-31",
"@types/node": "^25.2.1"
},
"engines": {
@@ -24,13 +24,13 @@
}
},
"node_modules/@playwright/test": {
"version": "1.62.0-alpha-1783623505000",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.0-alpha-1783623505000.tgz",
"integrity": "sha512-6aj9UWRXnS2amfs+8BHPRqQNTyiq91MF8Pl0UechaJkW0TZfvLxEjvhBnSrs6Lm3jcLmkkv/QTpW5NyslKZpTw==",
"version": "1.63.0-alpha-2026-08-31",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0-alpha-2026-08-31.tgz",
"integrity": "sha512-sNAYSkzbTC67x/v4ZekJ5ZzBFhSaHGc5t8+/3lqZI9LSvtYWVC+L2bHDbfFIhVXBjtl+1v+rg9Fdx+Rx3w2E8g==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.62.0-alpha-1783623505000"
"playwright": "1.63.0-alpha-2026-08-31"
},
"bin": {
"playwright": "cli.js"
@@ -49,42 +49,25 @@
"undici-types": "~7.16.0"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.62.0-alpha-1783623505000",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.0-alpha-1783623505000.tgz",
"integrity": "sha512-6KV9h4PP3hqu4NaGdxxcijWfYh9LJcFI/R2sP4TTC4I5cFo3oRawN0ETlW5MkE3cQEgKhhoj0KUNz4sfpCT0Tg==",
"version": "1.63.0-alpha-2026-08-31",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0-alpha-2026-08-31.tgz",
"integrity": "sha512-3XAsuznfu8jBVJ4QxdGvBkt0+b8ZFwuwJYyOfiIw5ZjUOrNLNRhKxzLzLuydou3gJ9c6eMwVqgzdiOwhy54Kzw==",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.62.0-alpha-1783623505000"
"playwright-core": "1.63.0-alpha-2026-08-31"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=20"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.62.0-alpha-1783623505000",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.0-alpha-1783623505000.tgz",
"integrity": "sha512-CPJZdsA/KGT2QQlekiV6Wt+QlQrZHVSZ6oiNtOI/bYYOIVLM8jfKGWTM4zQiyd4UN+40Cq4cA6lxmZHZbtPvJQ==",
"version": "1.63.0-alpha-2026-08-31",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0-alpha-2026-08-31.tgz",
"integrity": "sha512-1ek0Lyr12h6jcs/WTcNoVtzZkQp7D/90PsMuBW/Rm6h3AsWAbzpqj0geMv8+8Tzzr9CSUYvg9kznrVZINQMXXw==",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@playwright/cli",
"version": "0.1.17",
"version": "0.1.19",
"description": "Playwright CLI",
"repository": {
"type": "git",
@@ -18,12 +18,12 @@
"test": "playwright test"
},
"devDependencies": {
"@playwright/test": "1.62.0-alpha-1783623505000",
"@playwright/test": "1.63.0-alpha-2026-08-31",
"@types/node": "^25.2.1"
},
"dependencies": {
"playwright": "1.62.0-alpha-1783623505000",
"playwright-core": "1.62.0-alpha-1783623505000"
"playwright": "1.63.0-alpha-2026-08-31",
"playwright-core": "1.63.0-alpha-2026-08-31"
},
"bin": {
"playwright-cli": "playwright-cli.js"
+14 -14
View File
@@ -32,14 +32,11 @@ const ONE_DAY_MS = 24 * 60 * 60 * 1000;
main();
async function main() {
const command = process.argv.slice(2).find(arg => !arg.startsWith('-'));
if (command !== 'install')
checkInstalledSkills();
await notifyAboutUpdate().catch(() => {});
await checkForUpdates().catch(() => {});
program({ embedderVersion: packageJson.version });
}
async function notifyAboutUpdate() {
async function checkForUpdates() {
if (process.env.NO_UPDATE_NOTIFIER || process.env.CI)
return;
@@ -47,13 +44,14 @@ async function notifyAboutUpdate() {
const stale = !cache || (Date.now() - cache.lastCheck) > ONE_DAY_MS;
if (!stale)
return;
writeCache({ lastCheck: Date.now() });
const command = process.argv.slice(2).find(arg => !arg.startsWith('-'));
if (command !== 'install')
checkInstalledSkills();
const latest = await fetchLatestVersion();
if (!latest)
return;
writeCache({ lastCheck: Date.now(), latestVersion: latest });
if (tools.compareSemver(latest, packageJson.version) > 0)
if (latest && tools.compareSemver(latest, packageJson.version) > 0)
printNotice(packageJson.version, latest);
}
@@ -89,13 +87,15 @@ function printNotice(current, latest) {
}
function cacheFile() {
return path.join(registry.defaultRegistryDirectory, 'cli-update-check.json');
const dir = process.env.PLAYWRIGHT_CLI_INSTALLATION_FOR_TEST || registry.defaultRegistryDirectory();
return path.join(dir, 'cli-update-check.json');
}
function readCache() {
const file = cacheFile();
try {
const data = JSON.parse(fs.readFileSync(cacheFile(), 'utf8'));
if (typeof data.lastCheck === 'number' && typeof data.latestVersion === 'string')
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
if (typeof data.lastCheck === 'number')
return data;
} catch {
}
@@ -106,8 +106,8 @@ function readCache() {
* @param {*} data
*/
function writeCache(data) {
const file = cacheFile();
try {
const file = cacheFile();
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, JSON.stringify(data));
} catch {
+4 -3
View File
@@ -20,8 +20,8 @@ const fs = require('fs');
const path = require('path');
function bundledSkillFile() {
const programPath = require.resolve('playwright-core/lib/tools/cli-client/program');
return path.join(path.dirname(programPath), 'skill', 'SKILL.md');
const corePath = require.resolve('playwright-core/package.json');
return path.join(path.dirname(corePath), 'lib', 'tools', 'skills', 'playwright-cli', 'SKILL.md');
}
function installedSkillTargets() {
@@ -37,7 +37,8 @@ function installedSkillTargets() {
* @returns
*/
function readSkill(file) {
return fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null;
// Normalize line endings, they could be affected by git or editor settings.
return fs.existsSync(file) ? fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n') : null;
}
/**
+5
View File
@@ -165,6 +165,11 @@ playwright-cli run-code "async page => await page.context().grantPermissions(['g
playwright-cli run-code --filename=script.js
playwright-cli tracing-start
playwright-cli tracing-stop
# record user actions in the browser, print them as Playwright code on stop
playwright-cli recording-start
playwright-cli recording-stop
playwright-cli video-start video.webm
playwright-cli video-chapter "Chapter Title" --description="Details" --duration=2000
playwright-cli video-stop
+1 -1
View File
@@ -19,7 +19,7 @@ playwright-cli tracing-stop
## Trace Output Files
When you start tracing, Playwright creates a `traces/` directory with several files:
When you start tracing, Playwright creates a `.playwright-cli/traces/` directory with several files:
### `trace-{timestamp}.trace`
+57 -5
View File
@@ -25,7 +25,7 @@ type CliResult = {
exitCode: number | null;
};
async function runCli(...args: string[]): Promise<CliResult> {
async function runCli(args: string[], env: Record<string, string> = {}): Promise<CliResult> {
const cliPath = path.join(__dirname, '../playwright-cli.js');
return new Promise<CliResult>((resolve, reject) => {
@@ -36,6 +36,7 @@ async function runCli(...args: string[]): Promise<CliResult> {
env: {
...process.env,
PLAYWRIGHT_CLI_INSTALLATION_FOR_TEST: test.info().outputPath(),
...env,
},
cwd: test.info().outputPath(),
});
@@ -61,26 +62,77 @@ async function runCli(...args: string[]): Promise<CliResult> {
}
test('open data URL', async ({}) => {
expect(await runCli('open', 'data:text/html,hello', '--persistent')).toEqual(expect.objectContaining({
expect(await runCli(['open', 'data:text/html,hello', '--persistent'])).toEqual(expect.objectContaining({
output: expect.stringContaining('hello'),
exitCode: 0,
}));
expect(await runCli('delete-data')).toEqual(expect.objectContaining({
expect(await runCli(['delete-data'])).toEqual(expect.objectContaining({
output: expect.stringContaining('Deleted user data for'),
exitCode: 0,
}));
});
test('warns when installed skill is out of date', async ({}) => {
expect(await runCli('install', '--skills')).toEqual(expect.objectContaining({
expect(await runCli(['install', '--skills'], { NO_UPDATE_NOTIFIER: '1' })).toEqual(expect.objectContaining({
exitCode: 0,
}));
const skillFile = path.join(test.info().outputPath(), '.claude', 'skills', 'playwright-cli', 'SKILL.md');
fs.appendFileSync(skillFile, 'x');
expect(await runCli('--help')).toEqual(expect.objectContaining({
const env = { CI: '', NO_UPDATE_NOTIFIER: '' };
expect(await runCli(['--help'], env)).toEqual(expect.objectContaining({
error: expect.stringContaining('does not match the tool version'),
}));
expect(await runCli(['--help'], env)).toEqual(expect.objectContaining({
error: expect.not.stringContaining('does not match the tool version'),
}));
});
test('does not warn when installed skill only differs in line endings', async ({}) => {
expect(await runCli(['install', '--skills'], { NO_UPDATE_NOTIFIER: '1' })).toEqual(expect.objectContaining({
exitCode: 0,
}));
const skillFile = path.join(test.info().outputPath(), '.claude', 'skills', 'playwright-cli', 'SKILL.md');
fs.writeFileSync(skillFile, fs.readFileSync(skillFile, 'utf8').replace(/\n/g, '\r\n'));
expect(await runCli(['--help'], { CI: '', NO_UPDATE_NOTIFIER: '' })).toEqual(expect.objectContaining({
error: expect.not.stringContaining('does not match the tool version'),
}));
});
test('caches the update check in the default registry directory', async ({}) => {
// Redirect the home/cache directories so the real user cache is untouched, and
// leave PLAYWRIGHT_CLI_INSTALLATION_FOR_TEST empty so the default path is used.
const home = test.info().outputPath('home');
fs.mkdirSync(home, { recursive: true });
const env = {
CI: '',
NO_UPDATE_NOTIFIER: '',
PLAYWRIGHT_CLI_INSTALLATION_FOR_TEST: '',
HOME: home,
USERPROFILE: home,
XDG_CACHE_HOME: path.join(home, '.cache'),
LOCALAPPDATA: path.join(home, 'AppData', 'Local'),
};
expect(await runCli(['--version'], env)).toEqual(expect.objectContaining({ exitCode: 0 }));
const found: string[] = [];
const walk = (dir: string) => {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory())
walk(full);
else if (entry.name === 'cli-update-check.json')
found.push(full);
}
};
walk(home);
expect(found).toHaveLength(1);
expect(JSON.parse(fs.readFileSync(found[0], 'utf8')).lastCheck).toEqual(expect.any(Number));
});