Skip to content

Commit 201dcf1

Browse files
authored
Prepare browser releases matched to consumer Playwright pins (#41)
A consumer Playwright bump can leave the runner without an accepted matching browser image. The watcher checks flowbite-xor main hourly, resolves the selected ref to an immutable commit, validates the exact manifest/lock pin, and proposes a matching browser release. Manual and repository-dispatch triggers support coordinating the release before the consumer bump merges. Each runtime minor owns its npm lock and parity fixture. COPY inputs, fingerprints and validation selection isolate line changes. The watcher captures an accepted application-image digest once and preserves it on retries, even as the catalog grows. Inventory must match the authored Playwright version. Candidates retain RGB rendering and committed screenshot parity. Includes #39's publication isolation: consumer code executes only on a fresh job without repository permissions, persisted checkout credentials or registry login, using controlled Compose configuration and a minimal environment. Separate finalization accepts evidence only for the exact public digest, source, image inputs and fixture. Depends on #39; merge #39 first. The input-layout change allocates revision 1.0.2 rather than reusing 1.0.1. The current consumer remains on 1.58.2. Publication/catalog acceptance and review still gate availability; automatic merge is not enabled. Validation: all 261 combined tests pass, including cross-line isolation, retry idempotence, application-pin stability, evidence binding and publication isolation. Catalog, compilation, shell/YAML and whitespace checks pass. Updated CI is running. Both existing Codex findings have fixes and resolved threads; #40 had no findings.
2 parents 3301179 + 5a4b648 commit 201dcf1

16 files changed

Lines changed: 313 additions & 17 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Match consumer Playwright
2+
on:
3+
schedule:
4+
- cron: '37 * * * *'
5+
workflow_dispatch:
6+
inputs:
7+
consumer_ref:
8+
description: flowbite-xor branch or commit containing the proposed Playwright bump
9+
type: string
10+
default: main
11+
repository_dispatch:
12+
types: [playwright-bump]
13+
permissions:
14+
contents: write
15+
pull-requests: write
16+
concurrency:
17+
group: browser-refresh
18+
cancel-in-progress: false
19+
jobs:
20+
propose:
21+
runs-on: ubuntu-24.04
22+
timeout-minutes: 15
23+
env:
24+
GH_TOKEN: ${{ secrets.LIBRARY_BOT_TOKEN || github.token }}
25+
steps:
26+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
27+
with:
28+
fetch-depth: 0
29+
token: ${{ secrets.LIBRARY_BOT_TOKEN || github.token }}
30+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
31+
with:
32+
node-version: '22'
33+
- run: python3 -m pip install -r requirements-ci.txt
34+
- name: Propose the exact consumer pin
35+
env:
36+
CONSUMER_REF: ${{ inputs.consumer_ref || github.event.client_payload.consumer_ref || 'main' }}
37+
run: python3 scripts/refresh_browser.py --consumer-ref "$CONSUMER_REF" --pr

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ and direct usage pages remains sufficient.
124124
- [Reusable PHP test tasks and per-process coverage](docs/php-tests.md)
125125
- [Prepared Serena v2 REPL, shell access and editing](docs/serena.md)
126126
- [Agent feedback implementation and runner operations](docs/agent-feedback.md)
127+
- [Browser releases matched to consumer Playwright pins](docs/browser-releases.md)
127128
- [Isolated Toolkit validation and fresh Symfony applications](docs/php-toolkit.md)
128129
- [Cloud prerequisites and verification evidence](docs/compatibility.md)
129130
- [Versions, pins, upgrades, and rollback](docs/versioning.md)

‎docs/browser-releases.md‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Browser releases matched to the consuming lock
2+
3+
The flowbite-xor runner selects an accepted browser image whose measured
4+
Playwright version equals the application's exact lock. An authored proposal
5+
does not change availability or existing digest pins.
6+
7+
`Match consumer Playwright` checks flowbite-xor main hourly and opens a PR
8+
when its exact Playwright pin changes. For a coordinated bump before merging
9+
the consumer, manually run that workflow with the consumer branch or commit,
10+
or send its `playwright-bump` repository dispatch with `consumer_ref` in the
11+
payload. The workflow resolves that ref to an immutable commit and requires
12+
the manifest and lock to agree. It never upgrades to an unrelated latest version.
13+
14+
Each runtime minor line has its own npm lock, copied and fingerprinted separately.
15+
Changing one line does not change another line's package inputs. A patch bump
16+
within a runtime line receives a fresh packaging revision; a new runtime minor
17+
starts at 1.0.0. The old exact releases and pins remain usable.
18+
19+
Every candidate keeps RGB subpixel rendering, launches Chromium/Firefox/WebKit
20+
offline, and compares Chromium screenshots with committed baselines at its
21+
own pinned consumer commit and accepted application-image digest. The watcher
22+
records that digest when preparing the proposal; later catalog updates do not
23+
change an existing fixture or retry. Baseline updates and test retries are disabled for
24+
that parity run. A mismatch stops acceptance and retains the report/diff files.
25+
26+
Merge the passing source PR, then review the normal publication/catalog PR.
27+
After catalog acceptance, the runner discovers the matching verified digest
28+
from the consuming Playwright pin; it needs no separate browser-version edit.
29+
Hourly detection supports preparing a release the same day, but publication
30+
still depends on successful CI and timely review/merge. `LIBRARY_BOT_TOKEN`
31+
allows validation of bot-created PRs to start automatically.
32+
33+
To prepare the same proposal locally without pushing:
34+
35+
```sh
36+
python3 scripts/refresh_browser.py --consumer-ref CONSUMER_COMMIT
37+
```
38+
39+
New rendering or packaging fixes belong in a normal feature PR with a fresh
40+
revision. The watcher proposes changed consumer versions, not repeated releases
41+
for every consumer source commit.

‎images/playwright-browser/Dockerfile‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ ARG BASE_IMAGE
33
FROM ${BASE_IMAGE}
44
ARG APT_REFRESH
55
ENV DEBIAN_FRONTEND=noninteractive PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers HOME=/home/dev
6-
COPY images/playwright-browser/package*.json /opt/playwright/
6+
ARG IMAGE_LINE
7+
COPY images/playwright-browser/lines/${IMAGE_LINE}/package*.json /opt/playwright/
78
RUN npm ci --prefix /opt/playwright --ignore-scripts --no-audit --no-fund \
89
&& /opt/playwright/node_modules/.bin/playwright install --with-deps chromium firefox webkit \
910
&& mv /etc/fonts/conf.d/10-sub-pixel-none.conf /etc/fonts/10-sub-pixel-none.conf.disabled \

‎images/playwright-browser/definition.json‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"1.58-trixie": {
3737
"runtime_line": "1.58",
3838
"os": "debian-trixie",
39-
"revision": "1.0.1",
39+
"revision": "1.0.2",
4040
"platforms": [
4141
"linux/amd64"
4242
],
@@ -45,9 +45,10 @@
4545
"digest": "sha256:154ba2f4d6fec323d28e4f4bb86bba4677f1223391a1979cf521304e03a98dfa"
4646
},
4747
"changes": [
48-
"Use RGB subpixel rendering to match the consuming project's Noble Chromium screenshot baselines; verify fontconfig and committed screenshot parity."
48+
"Retain RGB screenshot parity and isolate browser locks and fixtures by runtime line; prepare matched releases when the consumer Playwright pin changes."
4949
],
50-
"migration": "Compatible rendering fix. Select the accepted 1.0.1 digest after publication; existing 1.0.0 digest pins remain unchanged."
50+
"migration": "Select the accepted 1.0.2 digest after publication for isolated browser inputs and RGB rendering. Existing exact release pins remain unchanged.",
51+
"playwright_version": "1.58.2"
5152
}
5253
}
5354
}
File renamed without changes.
File renamed without changes.

‎schemas/definition.schema.json‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@
8282
"runtime_line": {
8383
"type": "string"
8484
},
85+
"playwright_version": {
86+
"type": "string",
87+
"pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$"
88+
},
8589
"os": {
8690
"const": "debian-trixie"
8791
},

‎scripts/build.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def build(line, image, source, parent=None, cache=None):
3232
d = definitions()[line]
3333
tools = read(ROOT / "images/tools.json")
3434
args = {"BASE_IMAGE": parent or pinned(d["base"]), "SOURCE_COMMIT": source,
35-
"IMAGE_VERSION": d["revision"], "APT_REFRESH": d["revision"]}
35+
"IMAGE_VERSION": d["revision"], "APT_REFRESH": d["revision"], "IMAGE_LINE": d["line"]}
3636
if d["family"] in ("php-dev", "php-frankenphp"):
3737
args.update(COMPOSER_IMAGE=pinned(tools["composer"]), REDIS_VERSION=tools["redis_version"],
3838
XDEBUG_VERSION=tools["xdebug_version"], PCOV_VERSION=tools["pcov_version"],

‎scripts/image_inputs.py‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@
1717
}
1818

1919

20-
def sources(family, root):
20+
def sources(family, root, line=None):
2121
"""Read COPY sources; stage copies are already represented by pinned inputs."""
2222
recipe = Path(root) / "images" / family / "Dockerfile"
2323
result = {str(recipe.relative_to(root)), ".dockerignore"}
2424
text = recipe.read_text().replace("\\\n", " ")
25-
for line in text.splitlines():
26-
instruction = re.match(r"^\s*(COPY|ADD)\s+(.+)$", line, re.I)
25+
for recipe_line in text.splitlines():
26+
instruction = re.match(r"^\s*(COPY|ADD)\s+(.+)$", recipe_line, re.I)
2727
if not instruction:
2828
continue
2929
if instruction[1].upper() == "ADD":
@@ -37,20 +37,24 @@ def sources(family, root):
3737
continue
3838
tokens = json.loads(value) if value.startswith("[") else shlex.split(value)
3939
for source in tokens[:-1]:
40+
if "${IMAGE_LINE}" in source:
41+
if line is not None and not re.fullmatch(r"[a-zA-Z0-9][a-zA-Z0-9._-]*", line):
42+
raise ValueError(f"Unsupported image line: {line}")
43+
source = source.replace("${IMAGE_LINE}", line if line is not None else "*")
4044
if source.startswith("/") or ".." in Path(source).parts or "$" in source:
4145
raise ValueError(f"Unsupported COPY source: {source}")
4246
result.add(source.rstrip("/"))
4347
return sorted(result)
4448

4549

46-
def consumes(family, path, root):
50+
def consumes(family, path, root, line=None):
4751
return any(path == source or path.startswith(source + "/")
48-
or Path(path).match(source) for source in sources(family, root))
52+
or Path(path).match(source) for source in sources(family, root, line))
4953

5054

51-
def files(family, root):
55+
def files(family, root, line=None):
5256
result = set()
53-
for source in sources(family, root):
57+
for source in sources(family, root, line):
5458
for path in Path(root).glob(source):
5559
if path.is_dir():
5660
result.update(item for item in path.rglob("*") if item.is_file())

0 commit comments

Comments
 (0)