Skip to content

fix: authenticate camoufox fetch with GITHUB_TOKEN - #309

Draft
l2ysho wants to merge 1 commit into
masterfrom
claude/flake-triage-d3c8fe
Draft

fix: authenticate camoufox fetch with GITHUB_TOKEN#309
l2ysho wants to merge 1 commit into
masterfrom
claude/flake-triage-d3c8fe

Conversation

@l2ysho

@l2ysho l2ysho commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Note

I was testing my skill for flake job detection, I found this one and fixed it with /fix-flaky-test. Seems legit, feel free to take a look, cancel or whatever. cc @vladfrangu


Problem

camoufox fetch lists releases from api.github.com. Unauthenticated, every leg of the image matrix shares one runner IP's 60 requests/hour budget, so a leg occasionally loses the race and gets a 403.

On the Python side that failure is silent. _do_sync catches the per-repo exception, camoufox fetch returns without an error code, and the image ships with no browser. It then dies one step later in Test image:

Official... Error: 403 Client Error: rate limit exceeded for url:
https://api.github.com/repos/camoufox/camoufox/releases
Synced 0 versions from 0 repos.
Version 'official' not found in cache. Run 'camoufox sync'.
camoufox.exceptions.CamoufoxNotInstalled: official/stable is not installed.
Please run `camoufox fetch` to install.

Seen in run 34317591187py 3.14 / pw 1.60.0 red while 24 of the 25 camoufox legs on the same SHA passed. Nothing about that combination relates to a GitHub API quota, so it reads like a platform break but is not one.

Change

  • Pass GITHUB_TOKEN into both camoufox builds as a BuildKit secret, lifting the budget to 1000 requests/hour per repository. A secret rather than a build arg keeps the token out of the image layers and out of docker history. Builds that pass no secret (local, forks) stay unauthenticated and still work.
  • mode=0444 on the node mount only — that stage runs as myuser, and secrets default to root-owned 0400. The Python stage runs as root, so its default is correct.
  • Assert on the Python side that the browser really landed. Any future fetch failure is now a red build naming the cause, not a green build shipping a broken image.

camoufox-js throws on a failed fetch, so the node image was never silently broken — it gets the token but needs no guard.

Verification

The original 403 can't be reproduced on demand, so the same code path was driven with a deliberately invalid token:

Case Result
Invalid token 401 Client Error: Unauthorized → guard fails the build. Proves the secret reaches camoufox's request headers.
No secret Synced 30 versions from 3 repos.Camoufox installed: 152.0.4-beta.30 → green
docker run image test All browser tests passed.
Node image build green
Guard with browser missing fails the build, reproducing the exact CI traceback at build time
mode=0444 removed (control) non-root stage reads the token as empty — the flag is load-bearing

hadolint and actionlint both match baseline exactly (zero new findings). Token absent from docker history.

Notes for review

  • The changed RUN instructions invalidate the GHA layer cache once; the first run after merge rebuilds the camoufox layer on every leg.
  • The guard calls camoufox.pkgman.installed_verstr, the same function the runtime calls on launch (also present in 0.4.11, so older matrix legs are fine). An upstream rename would fail the build loudly rather than silently.
  • The secret is mounted on the pull_request build steps too, so a PR editing the Dockerfile could read /run/secrets/github_token at build time. Fork PRs get a read-only token by design and same-repo branches already imply write access. build-main has no explicit permissions: block; scoping one down would be a separate change.
  • Local make test-*-camoufox targets can't pass a token, so heavy local iteration still shares the 60/hour budget. Left alone deliberately — wiring an optional secret through four Makefile targets needs $(COMMA) escaping and full image builds to validate.

🤖 Generated with Claude Code

`camoufox fetch` lists releases from api.github.com. Unauthenticated,
every leg of the image matrix shares one runner IP's 60 requests/hour
budget, so a leg occasionally loses the race and gets a 403.

On the Python side that failure is silent: the sync swallows the error,
`camoufox fetch` still exits 0, and the image ships with no browser. It
then dies one step later in "Test image" with CamoufoxNotInstalled. Seen
in run 34317591187, where 24 of the 25 camoufox legs on the same SHA
passed.

Pass GITHUB_TOKEN into both camoufox builds as a BuildKit secret, which
lifts the budget to 1000 requests/hour per repository. A secret rather
than a build arg keeps the token out of the image layers and out of
`docker history`. Builds that pass no secret stay unauthenticated and
still work. mode=0444 on the node mount because that stage runs as
myuser and secrets default to root-owned 0400.

Also assert on the Python side that the browser really landed, so any
future fetch failure is a red build naming the cause instead of a green
build shipping a broken image.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added this to the 149th sprint - Tooling team milestone Sep 9, 2026
@github-actions github-actions Bot added the t-tooling Issues with this label are in the ownership of the tooling team. label Sep 9, 2026
@l2ysho
l2ysho requested a review from vladfrangu September 9, 2026 12:27
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

📦 Image size report

Built images compared against the currently published rolling tag for the same runtime version (e.g. apify/actor-node:22; -slim variants against the -slim tag). Sizes are the uncompressed on-disk size reported by docker image inspect, so they will be larger than the compressed download size shown on Docker Hub. Only workflows triggered by this PR's changes report a section.

Python + Playwright images — 25 images (at 5d99792)
Image Variant Current New Δ
apify/actor-python-playwright-camoufox:3.10 playwright 1.62.0, camoufox 0.5.6 2257.9 MiB 2257.9 MiB 🔺 +0.0 MiB (+0.0%)
apify/actor-python-playwright-camoufox:3.11 playwright 1.62.0, camoufox 0.5.6 2270.1 MiB 2270.1 MiB 🔺 +0.0 MiB (+0.0%)
apify/actor-python-playwright-camoufox:3.12 playwright 1.62.0, camoufox 0.5.6 2261.5 MiB 2261.5 MiB 🔺 +0.0 MiB (+0.0%)
apify/actor-python-playwright-camoufox:3.13 playwright 1.62.0, camoufox 0.5.6 2249.8 MiB 2249.8 MiB 🔺 +0.0 MiB (+0.0%)
apify/actor-python-playwright-camoufox:3.14 playwright 1.62.0, camoufox 0.5.6 2254.2 MiB 2254.2 MiB 🔺 +0.0 MiB (+0.0%)
apify/actor-python-playwright-chrome:3.10 playwright 1.62.0, camoufox 0.5.6 1846.8 MiB 1846.8 MiB ➖ 0.0 MiB (0.0%)
apify/actor-python-playwright-chrome:3.11 playwright 1.62.0, camoufox 0.5.6 1853.0 MiB 1853.0 MiB ➖ 0.0 MiB (0.0%)
apify/actor-python-playwright-chrome:3.12 playwright 1.62.0, camoufox 0.5.6 1847.7 MiB 1847.7 MiB ➖ 0.0 MiB (0.0%)
apify/actor-python-playwright-chrome:3.13 playwright 1.62.0, camoufox 0.5.6 1836.2 MiB 1836.2 MiB ➖ 0.0 MiB (0.0%)
apify/actor-python-playwright-chrome:3.14 playwright 1.62.0, camoufox 0.5.6 1838.5 MiB 1838.5 MiB ➖ 0.0 MiB (0.0%)
apify/actor-python-playwright-firefox:3.10 playwright 1.62.0, camoufox 0.5.6 1163.2 MiB 1163.2 MiB ➖ 0.0 MiB (0.0%)
apify/actor-python-playwright-firefox:3.11 playwright 1.62.0, camoufox 0.5.6 1169.3 MiB 1169.3 MiB ➖ 0.0 MiB (0.0%)
apify/actor-python-playwright-firefox:3.12 playwright 1.62.0, camoufox 0.5.6 1164.0 MiB 1164.0 MiB ➖ 0.0 MiB (0.0%)
apify/actor-python-playwright-firefox:3.13 playwright 1.62.0, camoufox 0.5.6 1152.6 MiB 1152.6 MiB ➖ 0.0 MiB (0.0%)
apify/actor-python-playwright-firefox:3.14 playwright 1.62.0, camoufox 0.5.6 1154.9 MiB 1154.9 MiB ➖ 0.0 MiB (0.0%)
apify/actor-python-playwright-webkit:3.10 playwright 1.62.0, camoufox 0.5.6 1425.5 MiB 1425.5 MiB ➖ 0.0 MiB (0.0%)
apify/actor-python-playwright-webkit:3.11 playwright 1.62.0, camoufox 0.5.6 1431.6 MiB 1431.6 MiB ➖ 0.0 MiB (0.0%)
apify/actor-python-playwright-webkit:3.12 playwright 1.62.0, camoufox 0.5.6 1426.3 MiB 1426.3 MiB ➖ 0.0 MiB (0.0%)
apify/actor-python-playwright-webkit:3.13 playwright 1.62.0, camoufox 0.5.6 1414.9 MiB 1414.9 MiB ➖ 0.0 MiB (0.0%)
apify/actor-python-playwright-webkit:3.14 playwright 1.62.0, camoufox 0.5.6 1417.2 MiB 1417.2 MiB ➖ 0.0 MiB (0.0%)
apify/actor-python-playwright:3.10 playwright 1.62.0, camoufox 0.5.6 2828.2 MiB 2828.2 MiB ➖ 0.0 MiB (0.0%)
apify/actor-python-playwright:3.11 playwright 1.62.0, camoufox 0.5.6 2834.3 MiB 2834.3 MiB ➖ 0.0 MiB (0.0%)
apify/actor-python-playwright:3.12 playwright 1.62.0, camoufox 0.5.6 2829.0 MiB 2829.0 MiB ➖ 0.0 MiB (0.0%)
apify/actor-python-playwright:3.13 playwright 1.62.0, camoufox 0.5.6 2817.5 MiB 2817.5 MiB ➖ 0.0 MiB (0.0%)
apify/actor-python-playwright:3.14 playwright 1.62.0, camoufox 0.5.6 2819.9 MiB 2819.9 MiB ➖ 0.0 MiB (0.0%)
Node + Playwright images — 30 images (at 5d99792)
Image Variant Current New Δ
apify/actor-node-playwright-camoufox:22 playwright 1.60.0, camoufox ^0.12.0 2545.1 MiB 2545.2 MiB 🔺 +0.1 MiB (+0.0%)
apify/actor-node-playwright-camoufox:22-slim playwright 1.60.0, camoufox ^0.12.0, slim 2226.8 MiB 2226.8 MiB 🔺 +0.0 MiB (+0.0%)
apify/actor-node-playwright-camoufox:24 playwright 1.60.0, camoufox ^0.12.0 2548.4 MiB 2548.7 MiB 🔺 +0.3 MiB (+0.0%)
apify/actor-node-playwright-camoufox:24-slim playwright 1.60.0, camoufox ^0.12.0, slim 2229.1 MiB 2229.1 MiB 🔺 +0.0 MiB (+0.0%)
apify/actor-node-playwright-camoufox:26 playwright 1.60.0, camoufox ^0.12.0 2563.5 MiB 2563.8 MiB 🔺 +0.3 MiB (+0.0%)
apify/actor-node-playwright-camoufox:26-slim playwright 1.60.0, camoufox ^0.12.0, slim 2244.2 MiB 2244.2 MiB 🔺 +0.0 MiB (+0.0%)
apify/actor-node-playwright-chrome:22 playwright 1.63.0, camoufox ^0.12.0 2203.9 MiB 2204.2 MiB 🔺 +0.3 MiB (+0.0%)
apify/actor-node-playwright-chrome:22-slim playwright 1.63.0, camoufox ^0.12.0, slim 1852.3 MiB 1852.3 MiB 🔺 +0.0 MiB (+0.0%)
apify/actor-node-playwright-chrome:24 playwright 1.63.0, camoufox ^0.12.0 2206.3 MiB 2206.5 MiB 🔺 +0.2 MiB (+0.0%)
apify/actor-node-playwright-chrome:24-slim playwright 1.63.0, camoufox ^0.12.0, slim 1854.7 MiB 1854.7 MiB 🔺 +0.0 MiB (+0.0%)
apify/actor-node-playwright-chrome:26 playwright 1.63.0, camoufox ^0.12.0 2221.4 MiB 2221.6 MiB 🔺 +0.2 MiB (+0.0%)
apify/actor-node-playwright-chrome:26-slim playwright 1.63.0, camoufox ^0.12.0, slim 1869.8 MiB 1869.8 MiB 🔻 −0.0 MiB (−0.0%)
apify/actor-node-playwright-firefox:22 playwright 1.63.0, camoufox ^0.12.0 1521.7 MiB 1522.0 MiB 🔺 +0.3 MiB (+0.0%)
apify/actor-node-playwright-firefox:22-slim playwright 1.63.0, camoufox ^0.12.0, slim 1170.1 MiB 1170.1 MiB 🔻 −0.0 MiB (−0.0%)
apify/actor-node-playwright-firefox:24 playwright 1.63.0, camoufox ^0.12.0 1524.1 MiB 1524.4 MiB 🔺 +0.3 MiB (+0.0%)
apify/actor-node-playwright-firefox:24-slim playwright 1.63.0, camoufox ^0.12.0, slim 1172.5 MiB 1172.5 MiB 🔺 +0.0 MiB (+0.0%)
apify/actor-node-playwright-firefox:26 playwright 1.63.0, camoufox ^0.12.0 1539.2 MiB 1539.5 MiB 🔺 +0.3 MiB (+0.0%)
apify/actor-node-playwright-firefox:26-slim playwright 1.63.0, camoufox ^0.12.0, slim 1187.6 MiB 1187.6 MiB 🔻 −0.0 MiB (−0.0%)
apify/actor-node-playwright-webkit:22 playwright 1.63.0, camoufox ^0.12.0 1778.6 MiB 1778.9 MiB 🔺 +0.3 MiB (+0.0%)
apify/actor-node-playwright-webkit:22-slim playwright 1.63.0, camoufox ^0.12.0, slim 1427.1 MiB 1427.1 MiB 🔻 −0.0 MiB (−0.0%)
apify/actor-node-playwright-webkit:24 playwright 1.63.0, camoufox ^0.12.0 1781.0 MiB 1781.3 MiB 🔺 +0.3 MiB (+0.0%)
apify/actor-node-playwright-webkit:24-slim playwright 1.63.0, camoufox ^0.12.0, slim 1429.4 MiB 1429.4 MiB 🔻 −0.0 MiB (−0.0%)
apify/actor-node-playwright-webkit:26 playwright 1.63.0, camoufox ^0.12.0 1796.1 MiB 1796.4 MiB 🔺 +0.3 MiB (+0.0%)
apify/actor-node-playwright-webkit:26-slim playwright 1.63.0, camoufox ^0.12.0, slim 1444.5 MiB 1444.5 MiB 🔻 −0.0 MiB (−0.0%)
apify/actor-node-playwright:22 playwright 1.63.0, camoufox ^0.12.0 3427.0 MiB 3427.2 MiB 🔺 +0.2 MiB (+0.0%)
apify/actor-node-playwright:22-slim playwright 1.63.0, camoufox ^0.12.0, slim 3091.4 MiB 3091.4 MiB 🔻 −0.0 MiB (−0.0%)
apify/actor-node-playwright:24 playwright 1.63.0, camoufox ^0.12.0 3240.8 MiB 3241.0 MiB 🔺 +0.3 MiB (+0.0%)
apify/actor-node-playwright:24-slim playwright 1.63.0, camoufox ^0.12.0, slim 2905.1 MiB 2905.1 MiB 🔻 −0.0 MiB (−0.0%)
apify/actor-node-playwright:26 playwright 1.63.0, camoufox ^0.12.0 3387.1 MiB 3387.4 MiB 🔺 +0.3 MiB (+0.0%)
apify/actor-node-playwright:26-slim playwright 1.63.0, camoufox ^0.12.0, slim 3051.5 MiB 3051.5 MiB 🔻 −0.0 MiB (−0.0%)

\
# Install Camoufox browser
&& PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=0 npx camoufox-js fetch \
&& GITHUB_TOKEN="$(cat /run/secrets/github_token 2>/dev/null || true)" \

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we should unset the env variable if the file is not present

Comment on lines +139 to +142
# `camoufox-js fetch` reads api.github.com, where the whole matrix shares one 60/hour budget.
# GITHUB_TOKEN lifts it to 1000/hour; a secret, not a build arg, keeps it out of the layers.
# mode=0444 because this stage runs as myuser and secrets default to root-owned 0400.
# Without it the build stays unauthenticated and still works.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

atClaude tldr this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

t-tooling Issues with this label are in the ownership of the tooling team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants