mirror of
https://github.com/daijro/camoufox.git
synced 2026-10-04 16:00:21 +00:00
* Pair each library release with the browser build it was tested with
Nothing tied a library release to a browser build: `camoufox fetch` took the
newest build in a channel, and a launch used whatever config.json marked
active, so an upgraded library could run a browser it was never tested with,
and an old library would pick up a newer, incompatible browser.
A released package now carries browser-pin.json, naming the browser release
built from the same sources. With it, and no explicit choice by the user:
- fetch installs exactly that build (no prerelease prompt: it is the build
this release was tested with, prerelease or not);
- a launch uses exactly that build, whatever else is installed or active,
and reports it as not installed rather than falling back to another;
- the fetcher's automatic install (TypeScript's first run) takes only it.
An explicit `camoufox set` still wins, with a one-time warning at launch;
`camoufox set --release` returns to the paired build, and `camoufox active`
says which is in use. The checked-in pin is `{}`, so development checkouts
follow their channel as before.
Also: prerelease library versions (0.5.8b1, 0.5.8-beta.1) parse as their
release; they were read as 0.5.0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Release a prerelease on every tested merge; promote to stable by tag
Every merge to main whose tests pass now publishes a prerelease of all
three artifacts, and pushing vX.Y.Z on a tested main commit promotes it.
- Build and Release runs after Tests on main. It builds the browser only
when its sources changed (ci.browser_inputs.source_digest: every browser
input, not counting the release number). Each build gets the next unused
beta.N on a release commit beside main -- main is protected -- and is
published as a GitHub prerelease, not a draft, with its source digest in
the notes.
- Publish to pypi follows it: <next>bN on PyPI, then Publish to npm puts
<next>-beta.N under the `next` dist-tag. Both are stamped with the browser
release built from the same sources.
- A vX.Y.Z tag is refused unless the commit is on main and `All tests
passed` succeeded on it. The paired browser prerelease then becomes the
stable, latest release (no rebuild, so users get the tested binaries), and
X.Y.Z goes to PyPI and npm `latest`.
The tested commit travels between workflows as an artifact: a workflow_run
is told main's head, so two quick merges would otherwise publish the second,
untested one. ci/release.py holds the planning, stamping and promotion,
unit-tested in ci/tests/test_release.py.
Also fixes two checks that failed the manual release already: vermin
targeted Python 3.8 exactly, against a package that declares ^3.10 and a
code base that needs 3.9, and check-pack compared npm and PyPI prerelease
versions as strings, although each registry spells them differently.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Test driver-only pull requests against the release paired with their sources
The scope step matched the release tag named by upstream.sh. With release
numbers now allocated per build, that number is a floor, not a release, so
driver-only pull requests would nearly always rebuild, or fetch a build other
than the one their sources produce. It now asks `ci.release paired` for the
release built from exactly this tree's browser sources, and fetch-browser
installs it through the same pin a released package carries.
Documents the release flow in ci/README.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* pythonlib: replace asyncio.to_thread so the 3.8 vermin gate passes
publish-pypi.yml checks the package with
`vermin . --eval-annotations --target=3.8 --violations camoufox/`, and
asyncio.to_thread (Python 3.9+) in _resolve_proxy_geo failed it, stopping
the 0.5.7 release. loop.run_in_executor does the same off-loop lookup.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Pair with releases cut before the digest marker, and test the pairing against the step
The scope step now asks ci.release paired, which only knew releases whose notes
carry a source digest. None does yet: v156.0.1-beta.32, the release built from
main's sources, predates the marker. So every driver-only pull request would have
rebuilt the browser, the first merge would have cut a duplicate beta.33, and the
two scope tests in ci/tests/test_ci.py -- which ran the step in a scratch repo
where ci.release did not import -- failed.
find_paired falls back to the tag upstream.sh names when that release is
published (a prerelease counts; a draft does not) and no browser source changed
since, listing the files that did when they have. browser-plan and promote use
the same lookup. paired takes --root and --releases so the tests run the
workflow's own step against a scratch repo and a fixed release list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Release from one workflow, with trusted publishing
The release chain was four workflows linked by workflow_run, with the
tested commit carried between them as an artifact; a browser release
number committed beside main; pairing data in HTML comments in release
notes; a stored PyPI token; and packages rebuilt at each publish.
release.yml now does all of it with `needs`:
- On a push to main it calls tests.yml on the pushed commit (tests.yml
loses its own push trigger), then builds the browser only when its
sources changed, and publishes a library prerelease only when something
a package ships changed. Docs- and CI-only merges publish nothing.
- A browser release's number lives only in its tag, which points at the
tested main commit; `ci.release set-build` writes it into the build's
working tree. Nothing is committed.
- Each browser release carries a manifest.json asset (source digest,
commit), which is what a library pairs by. Builds are attested with
actions/attest-build-provenance.
- Both packages are built once, in build-library, and the publish jobs
upload exactly those files. PyPI and npm use trusted publishing; no
credential is stored.
- A vX.Y.Z tag builds and checks both packages before promoting the
paired browser and publishing.
- Every published library version is tagged (vX.Y.ZbN for a prerelease),
which is how the next merge tells whether the library changed.
- A failed publish is retried with "Re-run failed jobs"; the retry-only
workflow_dispatch path is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
147 lines
6.0 KiB
Python
147 lines
6.0 KiB
Python
"""A released library runs the browser it was released with, and nothing else by default.
|
|
|
|
Each published package is stamped with `browser-pin.json` naming the browser
|
|
release built from the same sources. These tests hold the three places that
|
|
decide which build is used -- the active install at launch, the asset the
|
|
fetcher accepts, and the explicit-choice escape hatch -- to that pairing.
|
|
"""
|
|
|
|
import json
|
|
import warnings
|
|
|
|
import pytest
|
|
|
|
from camoufox import browser_pin, multiversion, pkgman
|
|
from camoufox.exceptions import CamoufoxNotInstalled
|
|
|
|
PIN = {
|
|
"tag": "v156.0.1-beta.33",
|
|
"repo": "daijro/camoufox",
|
|
"repo_name": "Official",
|
|
"version": "156.0.1",
|
|
"build": "beta.33",
|
|
}
|
|
|
|
|
|
def _setup(tmp_path, monkeypatch, *, pin, installed, config):
|
|
root = tmp_path / "cache"
|
|
root.mkdir()
|
|
(root / ".0.5_FLAG").write_text("")
|
|
(root / "repo_cache.json").write_text("{}")
|
|
(root / "config.json").write_text(json.dumps(config))
|
|
for spec in installed:
|
|
version, build = spec.split("-", 1)
|
|
d = root / "browsers" / "official" / spec
|
|
d.mkdir(parents=True)
|
|
(d / "version.json").write_text(json.dumps({"version": version, "build": build}))
|
|
pin_file = tmp_path / "browser-pin.json"
|
|
pin_file.write_text(json.dumps(pin))
|
|
monkeypatch.setattr(browser_pin, "PIN_FILE", pin_file)
|
|
monkeypatch.setattr(browser_pin, "_warned", False)
|
|
for module in (pkgman, multiversion):
|
|
monkeypatch.setattr(module, "INSTALL_DIR", root)
|
|
monkeypatch.setattr(multiversion, "BROWSERS_DIR", root / "browsers")
|
|
monkeypatch.setattr(multiversion, "CONFIG_FILE", root / "config.json")
|
|
monkeypatch.setattr(multiversion, "COMPAT_FLAG", root / ".0.5_FLAG")
|
|
return root
|
|
|
|
|
|
@pytest.mark.parametrize("content", ["{}", '{"tag": null}', "", "not json"])
|
|
def test_an_empty_or_missing_pin_pins_nothing(tmp_path, content):
|
|
f = tmp_path / "browser-pin.json"
|
|
f.write_text(content)
|
|
assert browser_pin.load_pin(f) is None
|
|
assert browser_pin.load_pin(tmp_path / "absent.json") is None
|
|
|
|
|
|
def test_the_checked_in_pin_is_empty():
|
|
"""main pins nothing; only the release workflow writes a real pin."""
|
|
assert json.loads(browser_pin.PIN_FILE.read_text()) == {}
|
|
|
|
|
|
def test_launch_uses_the_paired_build_even_when_another_is_marked_active(tmp_path, monkeypatch):
|
|
root = _setup(
|
|
tmp_path, monkeypatch, pin=PIN,
|
|
installed=["156.0.1-beta.33", "157.0-beta.34"],
|
|
config={"active_version": "browsers/official/157.0-beta.34"},
|
|
)
|
|
assert multiversion.get_active_path() == root / "browsers/official/156.0.1-beta.33"
|
|
assert pkgman.installed_verstr() == "156.0.1-beta.33"
|
|
|
|
|
|
def test_a_newer_build_is_not_used_when_the_paired_one_is_missing(tmp_path, monkeypatch):
|
|
_setup(tmp_path, monkeypatch, pin=PIN, installed=["157.0-beta.34"], config={})
|
|
assert multiversion.get_active_path() is None
|
|
with pytest.raises(CamoufoxNotInstalled, match="156.0.1-beta.33"):
|
|
pkgman.installed_verstr()
|
|
|
|
|
|
def test_an_explicit_choice_is_kept_and_warned_about(tmp_path, monkeypatch):
|
|
root = _setup(
|
|
tmp_path, monkeypatch, pin=PIN,
|
|
installed=["156.0.1-beta.33", "157.0-beta.34"],
|
|
config={"channel": "official/stable", "active_version": "browsers/official/157.0-beta.34"},
|
|
)
|
|
assert multiversion.get_active_path() == root / "browsers/official/157.0-beta.34"
|
|
with pytest.warns(RuntimeWarning, match="v156.0.1-beta.33"):
|
|
assert pkgman.installed_verstr() == "157.0-beta.34"
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("error")
|
|
pkgman.installed_verstr() # once per process, not per launch
|
|
|
|
|
|
def test_an_explicit_choice_of_the_paired_build_does_not_warn(tmp_path, monkeypatch):
|
|
_setup(
|
|
tmp_path, monkeypatch, pin=PIN, installed=["156.0.1-beta.33"],
|
|
config={"pinned": "156.0.1-beta.33", "channel": "official/prerelease",
|
|
"active_version": "browsers/official/156.0.1-beta.33"},
|
|
)
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("error")
|
|
assert pkgman.installed_verstr() == "156.0.1-beta.33"
|
|
|
|
|
|
def test_without_a_pin_the_channel_logic_is_unchanged(tmp_path, monkeypatch):
|
|
root = _setup(
|
|
tmp_path, monkeypatch, pin={}, installed=["156.0.1-beta.33", "157.0-beta.34"],
|
|
config={"active_version": "browsers/official/157.0-beta.34"},
|
|
)
|
|
assert multiversion.get_active_path() == root / "browsers/official/157.0-beta.34"
|
|
|
|
|
|
@pytest.mark.parametrize("config,accepts_newer", [({}, False), ({"channel": "official/stable"}, True)])
|
|
def test_the_fetcher_accepts_only_the_paired_asset_by_default(tmp_path, monkeypatch, config, accepts_newer):
|
|
_setup(tmp_path, monkeypatch, pin=PIN, installed=[], config=config)
|
|
fetcher = pkgman.CamoufoxFetcher.__new__(pkgman.CamoufoxFetcher)
|
|
fetcher.repo_config = pkgman.RepoConfig.get_default()
|
|
fetcher.pattern = fetcher.repo_config.build_pattern(spoof_os="lin", spoof_arch="x86_64")
|
|
fetcher.installed_sha256 = None
|
|
fetcher.installed_created_at = None
|
|
|
|
def asset(version, build):
|
|
return {"name": f"camoufox-{version}-{build}-lin.x86_64.zip",
|
|
"browser_download_url": f"https://example.invalid/{version}-{build}.zip"}
|
|
|
|
assert fetcher.check_asset(asset("156.0.1", "beta.33"), {"prerelease": True}) is not None
|
|
newer = fetcher.check_asset(asset("157.0", "beta.34"), {"prerelease": False})
|
|
assert (newer is not None) is accepts_newer
|
|
|
|
|
|
@pytest.mark.parametrize("raw,expected", [
|
|
("0.5.8", (0, 5, 8)),
|
|
("0.5.8b1", (0, 5, 8)),
|
|
("0.5.8rc2", (0, 5, 8)),
|
|
("0.5.8.dev3", (0, 5, 8)),
|
|
("0.5.8-beta.1", (0, 5, 8)),
|
|
("^0.5.0", (0, 5, 0)),
|
|
("1", (1, 0, 0)),
|
|
])
|
|
def test_prerelease_library_versions_parse_as_their_release(raw, expected):
|
|
assert pkgman._parse_semver(raw) == expected
|
|
|
|
|
|
def test_a_missing_paired_build_is_reported_as_not_installed_not_outdated(tmp_path, monkeypatch):
|
|
_setup(tmp_path, monkeypatch, pin=PIN, installed=["157.0-beta.34"], config={})
|
|
with pytest.raises(CamoufoxNotInstalled, match="pairs with"):
|
|
pkgman.camoufox_path(download_if_missing=False)
|