Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ Provide steps or a code snippet that reproduces the bug.

### Version:

Run `python -m camoufox version` in your terminal and paste the output here.
Run `python -m camoufox version` (or `npx camoufox version` for the npm package) and paste the output here.
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/camoufox-detected.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,4 @@ These questions will help me diagnose the issue:

### Version:

Run `python -m camoufox version` in your terminal and paste the output here.
Run `python -m camoufox version` (or `npx camoufox version` for the npm package) and paste the output here.
87 changes: 87 additions & 0 deletions .github/workflows/publish-npm.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: Publish to npm

# The npm twin of publish-pypi.yml: dispatched by hand, checks, builds, verifies
# the tarball, publishes. The package version is typescript/package.json's and
# must equal pythonlib's -- scripts/check-pack.mjs refuses to go on otherwise.
#
# Authentication is npm trusted publishing (OIDC): no token is stored anywhere.
# npm accepts this workflow's identity because the package's settings on
# npmjs.com name this repository and this file (publish-npm.yml). Renaming the
# file, or publishing from a fork, is rejected by the registry. The first
# version of a new package has to be published by hand, since the trusted
# publisher is configured on a package that already exists.

on:
workflow_dispatch:
inputs:
dry_run:
description: "Run every check and `npm publish --dry-run`, upload nothing"
type: boolean
default: false

permissions:
contents: read

jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # trusted publishing, and the provenance attestation
defaults:
run:
working-directory: typescript
env:
CAMOUFOX_FPGEN_DATA: ${{ github.workspace }}/.fpgen

steps:
- name: Check out repository
uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.14"

- name: Install pythonlib
# tests/golden-setup.ts records the golden fixtures from it, and the
# build copies its data files into the package.
working-directory: .
run: |
python3 -m venv .venv
.venv/bin/pip install -r ci/requirements.txt -e pythonlib
.venv/bin/python scripts/pin-fpgen-model.py

- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
package_json_file: typescript/package.json

- name: Set up Node
uses: actions/setup-node@v6
with:
# Trusted publishing needs npm >= 11.5.1, which Node 24 ships.
node-version: "24"
registry-url: https://registry.npmjs.org
cache: pnpm
cache-dependency-path: typescript/pnpm-lock.yaml

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Type check and lint
run: |
pnpm typecheck
pnpm check

- name: Test
run: pnpm test

- name: Build package
run: pnpm build

- name: Check package
run: node scripts/check-pack.mjs

- name: Publish to npm
run: npm publish --access public ${{ inputs.dry_run && '--dry-run' || '' }}
176 changes: 167 additions & 9 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,22 +150,48 @@ jobs:
# Only a change that can alter the binary justifies compiling one. A
# pull request that touches pythonlib/ or ci/ is a driver change: it
# still gets the full browser suite, but against the published build its
# users are running, which takes a minute instead of seventy.
# users are running, which takes a minute instead of seventy -- as long
# as that build matches this tree's browser sources (see below).
run: |
if [ "${{ github.event_name }}" != "pull_request" ]; then
echo "browser_changed=true" >> "$GITHUB_OUTPUT"
echo "::notice::Not a pull request -- building, which also refreshes the shared ccache."
exit 0
fi
sources='^(patches/|additions/|settings/|assets/|upstream\.sh|Makefile|scripts/)'
base="${{ github.event.pull_request.base.sha }}"
changed=$(git diff --name-only "$base"...HEAD || echo "")
echo "changed files:"; echo "$changed" | sed 's/^/ /'
if echo "$changed" | grep -qE '^(patches/|additions/|settings/|assets/|upstream\.sh|Makefile|scripts/)'; then
if echo "$changed" | grep -qE "$sources"; then
echo "browser_changed=true" >> "$GITHUB_OUTPUT"
echo "::notice::Browser sources changed -- rebuilding from source."
exit 0
fi
# The pull request leaves the browser alone -- but the published
# release is only the right browser to test it on if it was built from
# the SAME browser sources as this tree. The patch guards and suites
# come from this checkout, so when the base branch has moved past the
# release (a merged browser change that is not published yet), testing
# the release pairs new guards with an old browser, and every guard for
# the unreleased change fails on a pull request that never touched it.
# Compare against the tag the release was cut from; if they differ,
# build -- which restores the base branch's cached browser when its
# compiled half matches, so it costs minutes, not the full build.
. ./upstream.sh
tag="v${version}-${release}"
if ! git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then
echo "browser_changed=true" >> "$GITHUB_OUTPUT"
echo "::notice::No release tag $tag -- the release this tree targets is not published; building."
exit 0
fi
ahead=$(git diff --name-only "$tag" HEAD | grep -E "$sources" || true)
if [ -n "$ahead" ]; then
echo "browser sources that differ from $tag:"; echo "$ahead" | sed 's/^/ /'
echo "browser_changed=true" >> "$GITHUB_OUTPUT"
echo "::notice::The base branch's browser sources are ahead of the published $tag ($(echo "$ahead" | wc -l) files) -- building (a cache hit when the base branch already built it)."
else
echo "browser_changed=false" >> "$GITHUB_OUTPUT"
echo "::notice::No browser sources changed -- testing against the published release."
echo "::notice::No browser sources differ from the published $tag -- testing against it."
fi

- name: May the stealth check run?
Expand Down Expand Up @@ -307,6 +333,73 @@ jobs:
include-hidden-files: true
if-no-files-found: warn

# ---------------------------------------------------------------------------
typescript:
name: typescript
# Tier 1, beside pythonlib. The npm package's type check, lint and vitest
# suite, including the golden tests that hold it to pythonlib's output byte
# for byte -- so a pythonlib change that typescript/ does not mirror fails
# here, in a minute, not after the build.
needs: [resolve, static]
runs-on: ubuntu-24.04
permissions:
contents: read
env:
# The fpgen model the TS port downloads (sha256-pinned by
# scripts/data/fpgen-model.json). Kept in the workspace so it can be cached.
CAMOUFOX_FPGEN_DATA: ${{ github.workspace }}/.ci-work/fpgen
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: actions/setup-python@v5
with:
# Not PYTHON_VERSION: the golden fixtures are regenerated from this
# interpreter, and pycompat.ts's pySum() reproduces sum() as 3.14
# computes it (3.12/3.13 round mixed int/float sums differently in
# the last bit).
python-version: "3.14"
- run: |
# A venv at the repo root: tests/golden-setup.ts records the golden
# fixtures from its pythonlib before the suite runs.
python3 -m venv .venv
.venv/bin/pip install -r ci/requirements.txt -e pythonlib
# fpgen downloads its model on first import with TLS verification
# OFF and no checksum, and its release picker can only ever reach the
# April-2025 model. Install the pinned one first: see
# scripts/pin-fpgen-model.py.
.venv/bin/python scripts/pin-fpgen-model.py
- name: Test prerequisites
# Everything tests/prereq.ts may ask for. In CI a missing prerequisite
# FAILS its tests rather than skipping them, so this list is the job's
# contract. xvfb: the virtual-display lifecycle.
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends xvfb
- uses: pnpm/action-setup@v4
with:
package_json_file: typescript/package.json
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: pnpm
cache-dependency-path: typescript/pnpm-lock.yaml
- uses: actions/cache@v4
with:
path: .ci-work/fpgen
key: fpgen-model-${{ hashFiles('scripts/data/fpgen-model.json') }}
- name: Run
env:
GITHUB_TOKEN: ${{ github.token }}
run: .venv/bin/python -m ci.run_typescript
- uses: actions/upload-artifact@v4
if: always()
with:
name: results-typescript
path: .ci-work/results/
include-hidden-files: true
if-no-files-found: warn

# ---------------------------------------------------------------------------
build:
name: Build (linux x86_64)
Expand Down Expand Up @@ -595,7 +688,11 @@ jobs:
run: |
set -euo pipefail
python -m camoufox fetch
install_dir="$(python -m camoufox path)"
# The ACTIVE build's directory, resolved the way the launcher resolves
# it. `camoufox path` prints the cache root, and multiversion installs
# each build under browsers/<channel>/<version>/ -- so reading the
# binary from the root failed every driver-only run since #772.
install_dir="$(python -c 'from camoufox.pkgman import camoufox_path; print(camoufox_path(download_if_missing=False))')"
echo "install dir: $install_dir"
# Which browser did we actually get? This path does not build, it
# downloads the current release -- correct for a driver change, since
Expand Down Expand Up @@ -833,6 +930,67 @@ jobs:
include-hidden-files: true
if-no-files-found: ignore

# ---------------------------------------------------------------------------
typescript-browser:
name: typescript (browser)
# Tier 3a. Launches the browser under test through the TS API -- headless,
# persistent context, and launchServer -- and checks that a page sees the
# same identity pythonlib's launch of it shows.
needs: [resolve, build, fetch-browser, typescript]
if: >-
always() && needs.typescript.result == 'success' &&
(needs.build.result == 'success' || needs.fetch-browser.result == 'success')
runs-on: ubuntu-24.04
timeout-minutes: 45
permissions:
contents: read
env:
CAMOUFOX_FPGEN_DATA: ${{ github.workspace }}/.ci-work/fpgen
# The browser under test is the build job's unpackaged dist/bin, which
# packages en-US only: scripts/package.py adds the langpacks, and CI never
# runs it. A de-DE/fr-FR identity therefore presents en-US here though a
# packaged release presents de-DE. The e2e locale assertions skip on that
# (named) gap; everything else in the page-vs-config checks still runs.
# Remove this once the test artifact carries the langpacks.
CAMOUFOX_TEST_ALLOW_MISSING: packaged-locales
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/prepare-browser
with:
python-version: ${{ env.PYTHON_VERSION }}
- run: |
pip install -e pythonlib
# fpgen downloads its model on first import with TLS verification
# OFF and no checksum, and its release picker can only ever reach the
# April-2025 model. Install the pinned one first: see
# scripts/pin-fpgen-model.py.
python3 scripts/pin-fpgen-model.py
- uses: pnpm/action-setup@v4
with:
package_json_file: typescript/package.json
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: pnpm
cache-dependency-path: typescript/pnpm-lock.yaml
- uses: actions/cache@v4
with:
path: .ci-work/fpgen
key: fpgen-model-${{ hashFiles('scripts/data/fpgen-model.json') }}
- name: Run
env:
GITHUB_TOKEN: ${{ github.token }}
run: xvfb-run -a python3 -m ci.run_typescript --browser "$CAMOUFOX_BINARY"
- uses: actions/upload-artifact@v4
if: always()
with:
name: results-typescript-browser
path: .ci-work/results/
include-hidden-files: true
if-no-files-found: warn

# ---------------------------------------------------------------------------
growth:
name: Memory growth (scheduled)
Expand Down Expand Up @@ -927,8 +1085,8 @@ jobs:
# ---------------------------------------------------------------------------
summary:
name: Summary
needs: [resolve, static, pythonlib, build, fetch-browser, playwright,
patch-guards, build-tester, native, sundial]
needs: [resolve, static, pythonlib, typescript, build, fetch-browser, playwright,
patch-guards, build-tester, native, typescript-browser, sundial]
if: always() && needs.resolve.result == 'success'
runs-on: ubuntu-24.04
permissions:
Expand Down Expand Up @@ -968,7 +1126,7 @@ jobs:
# summarize report "produced no result file" and fail the gate on
# every pull request that did not touch the browser. Which is most of
# them, and exactly the cheap path this pipeline advertises.
required="pythonlib native_rules patch_guards skiplist_audit build_tester playwright native_browser"
required="pythonlib typescript typescript_browser native_rules patch_guards skiplist_audit build_tester playwright native_browser"
if [ "${{ needs.resolve.outputs.browser_changed }}" = "true" ]; then
required="$required build"
fi
Expand Down Expand Up @@ -1033,8 +1191,8 @@ jobs:
# that is not `success`, including `skipped`, fails the gate: a suite that
# did not run has not passed, and silently skipping one is the cheapest way
# to a green tick.
needs: [resolve, static, pythonlib, build, fetch-browser, playwright,
patch-guards, build-tester, native, sundial, summary]
needs: [resolve, static, pythonlib, typescript, build, fetch-browser, playwright,
patch-guards, build-tester, native, typescript-browser, sundial, summary]
if: always()
runs-on: ubuntu-24.04
permissions:
Expand Down
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ The engineering rules in [`AGENTS.md`](AGENTS.md) apply to every change, whether

## Testing Requirements

**CI runs everything, on every pull request.** [`.github/workflows/tests.yml`](.github/workflows/tests.yml) builds the browser from your branch when you touch browser sources (and tests against the published release when you do not), then runs the Python package tests, the patch guards, build-tester, the upstream Playwright suite, the leak suite and the stealth check. Branch protection requires exactly one check, **`All tests passed`**, which is green only when every applicable suite is.
**CI runs everything, on every pull request.** [`.github/workflows/tests.yml`](.github/workflows/tests.yml) builds the browser from your branch when you touch browser sources (and tests against the published release when you do not), then runs the Python and TypeScript package tests, the patch guards, build-tester, the upstream Playwright suite, the leak suite and the stealth check. Branch protection requires exactly one check, **`All tests passed`**, which is green only when every applicable suite is.

So there is nothing to attach to the pull request by hand. The old process — run the suites locally, screenshot the output, paste it in — was unenforceable: nothing checked that the browser in the screenshot was built from the branch under review. If you want a report in the description anyway, CI leaves one as a comment on the pull request.

Expand All @@ -50,6 +50,7 @@ python3 -m ci.run_build_tester --binary /path/to/camoufox-bin
python3 -m ci.run_playwright --binary /path/to/camoufox-bin # or --shard 3/6
python3 -m ci.run_skiplist_audit --binary /path/to/camoufox-bin
python3 -m ci.run_pythonlib # pythonlib/
python3 -m ci.run_typescript # typescript/
python3 -m pytest ci/tests -q # the pipeline's own tests
```

Expand Down
Loading
Loading