Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 0 additions & 43 deletions .github/workflows/pre-release.yml

This file was deleted.

5 changes: 5 additions & 0 deletions .github/workflows/publish-to-npm-on-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ jobs:
env:
NODE_ENV: 'production'

- name: Verify tarball and release tag
run: npm run verify-npm-package
env:
RELEASE_TAG: ${{ github.ref_name }}

- name: Publish opera-devtools-mcp
run: |
npm publish --provenance --access public
14 changes: 8 additions & 6 deletions docs/UPSTREAM.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ a result:
| `scripts/generate-cli.ts` | Opera attribution in the file header and in the generated-file header template | yes |
| `scripts/post-build.ts` | Adds `makeBinScriptsExecutable()` so generated JS bin scripts stay executable (tsc does not preserve file permissions) | yes |
| `scripts/test.js` | Opera env var keys | yes |
| `scripts/verify-npm-package.js` | Reads `npm pack --dry-run` instead of `npm publish --dry-run`, derives entry points from `package.json` `bin`, and checks the release tag names the packed version | yes |
| `tsconfig.json` | `ESNext.Iterator`/`ESNext.Collection` → `ES2025.*` | **temporary** — TS lib workaround |

### Upstream tests we modify
Expand All @@ -122,12 +123,13 @@ resolve by hand, keeping the Opera side. Git leaves upstream's copy in the tree
reflexive `git add -A` brings the file back — `npm run verify-upstream-seam` fails if any path in
this table reappears.

| Path | What we did |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `tests/e2e/chrome-devtools-commands.test.ts`, `tests/e2e/chrome-devtools-disclaimers.test.ts`, `tests/e2e/chrome-devtools-start-stop.test.ts`, `tests/e2e/chrome-devtools-status.test.ts` | Renamed to `opera-devtools-*`; they drive the Opera-named bin |
| `tests/e2e/telemetry.test.ts` | Deleted — Opera forces telemetry off, so there is no upload path left to assert |
| `AGENTS.md` | Deleted in favour of Opera's own agent docs |
| `server.json`, `scripts/verify-server-json-version.ts`, `.github/workflows/publish-to-mcp-registry-on-tag.yml` | Deleted — the fork publishes to npm only, never to the MCP registry |
| Path | What we did |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tests/e2e/chrome-devtools-commands.test.ts`, `tests/e2e/chrome-devtools-disclaimers.test.ts`, `tests/e2e/chrome-devtools-start-stop.test.ts`, `tests/e2e/chrome-devtools-status.test.ts` | Renamed to `opera-devtools-*`; they drive the Opera-named bin |
| `tests/e2e/telemetry.test.ts` | Deleted — Opera forces telemetry off, so there is no upload path left to assert |
| `AGENTS.md` | Deleted in favour of Opera's own agent docs |
| `.github/workflows/pre-release.yml` | Deleted — it triggered on `release-please-*` branches, which no longer exist, and a separate workflow cannot gate `npm publish`; its check now runs inside `publish-to-npm-on-tag.yml` |
| `server.json`, `scripts/verify-server-json-version.ts`, `.github/workflows/publish-to-mcp-registry-on-tag.yml` | Deleted — the fork publishes to npm only, never to the MCP registry |

### Generated — never hand-merge, always regenerate

Expand Down
103 changes: 74 additions & 29 deletions scripts/verify-npm-package.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,85 @@
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*
* Modified by Opera Software AS.
*/

// Verifies the tarball npm would publish, and that it carries the version the
// release tag names. Runs immediately before `npm publish`, which cannot be
// undone: a version published from a broken tarball can only be abandoned.
//
// Uses `npm pack` rather than `npm publish --dry-run` so the check never talks
// to the registry — no auth, and no failure once the version already exists.

import {execSync} from 'node:child_process';
import fs from 'node:fs';

const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'));

/** Strips the leading `./` npm allows in `bin` targets. */
function normalize(entry) {
return entry.replace(/^\.\//, '');
}

function packedFiles() {
const output = execSync('npm pack --dry-run --json', {
encoding: 'utf8',
maxBuffer: 64 * 1024 * 1024,
});
// `prepare` writes to stdout before npm does, so skip to the JSON.
const start = output.indexOf('[');
if (start === -1) {
throw new Error(`npm pack produced no JSON:\n${output}`);
}
const [tarball] = JSON.parse(output.slice(start));
return tarball;
}

// Checks that the select build files are present using `npm publish --dry-run`.
function verifyPackageContents() {
try {
const output = execSync('npm publish --dry-run --json --silent', {
encoding: 'utf8',
});
// skip non-JSON output from prepare.
const data = JSON.parse(output.substring(output.indexOf('{')));
const files = data['chrome-devtools-mcp'].files.map(f => f.path);
// Check some important files.
const requiredPaths = [
'build/src/index.js',
'build/src/third_party/index.js',
];
for (const requiredPath of requiredPaths) {
const hasBuildFolder = files.some(path => path.startsWith(requiredPath));
if (!hasBuildFolder) {
console.error(
`Assertion Failed: "${requiredPath}" not found in tarball.`,
);
process.exit(1);
}
}
console.log(
`npm publish --dry-run contained ${JSON.stringify(requiredPaths)}`,
function verifyContents(tarball) {
const packed = tarball.files.map(file => file.path);
// Derive the entry points from package.json so a renamed bin cannot ship
// missing, and keep the two module roots upstream already checked.
const required = [
'build/src/index.js',
'build/src/third_party/index.js',
...Object.values(packageJson.bin ?? {}).map(normalize),
];

const missing = required.filter(path => !packed.includes(path));
if (missing.length) {
throw new Error(
`tarball is missing ${missing.length} required file(s):\n` +
missing.map(path => ` ${path}`).join('\n') +
`\n It packed ${tarball.entryCount} entries. Did \`npm run bundle\` run?`,
);
} catch (err) {
console.error('failed to parse npm publish output', err);
process.exit(1);
}
console.log(
`tarball ${tarball.filename} contains all ${required.length} required entry point(s) of ${tarball.entryCount} packed.`,
);
}

verifyPackageContents();
function verifyReleaseTag(tarball) {
const tag = process.env['RELEASE_TAG'];
if (!tag) {
return;
}
const expected = `${packageJson.name}-v${packageJson.version}`;
if (tag !== expected) {
throw new Error(
`tag '${tag}' does not name the version being published.\n` +
` package.json is ${packageJson.version}, so the tag must be '${expected}'.\n` +
` Bump package.json, package-lock.json and src/version.ts, then retag.`,
);
}
console.log(`tag ${tag} matches ${tarball.name}@${tarball.version}.`);
}

try {
const tarball = packedFiles();
verifyContents(tarball);
verifyReleaseTag(tarball);
} catch (error) {
console.error(`\n✗ ${error instanceof Error ? error.message : error}\n`);
process.exit(1);
}
Loading