Skip to content
Back to skills

Render Toolchain Update

ASecurity

Update Pulp's pinned Skia, Dawn, and optional V8 prebuilts as one milestone-matched render-toolchain release. Use for requests such as "update Skia/Dawn", "move to chrome/mNNN", "find the matching V8", "update the GPU toolchain", or "use the Skia/Dawn/V8 tuple that goes together". Distinguishes milestone-matched V8 releases from v8-builder's newer weekly LKGR releases, verifies exact upstream provenance and asset hashes, updates every Pulp mirror, and runs the provider-identity/ODR gates.

  • 22 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 3, 2026
developmentjavascriptpythonrustgojavac++bashrailsdockergit

Works with

  • terminal
  • api

Security analysis

A100/100

Scanned September 30, 2026

npx -y skills add danielraffel/pulp --skill render-toolchain-update --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Render Toolchain Update?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Render Toolchain Update
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/danielraffel-render-toolchain-update/badge)](https://www.skillsdirectory.com/skills/danielraffel-render-toolchain-update)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: render-toolchain-update
description: |
  Update Pulp's pinned Skia, Dawn, and optional V8 prebuilts as one milestone-matched
  render-toolchain release. Use for requests such as "update Skia/Dawn", "move to
  chrome/mNNN", "find the matching V8", "update the GPU toolchain", or "use the
  Skia/Dawn/V8 tuple that goes together". Distinguishes milestone-matched V8 releases
  from v8-builder's newer weekly LKGR releases, verifies exact upstream provenance and
  asset hashes, updates every Pulp mirror, and runs the provider-identity/ODR gates.
---

# Update the matched render toolchain

This procedure expects `ghapp` and `python3`, and operates on
`tools/deps/manifest.json`, `tools/scripts/fetch_skia_for_release.py`, and
`tools/scripts/fetch_v8_for_release.py`.

Pulp's default is a milestone-matched, provider-validated set: the published
`skia-builder` release, the Dawn revision that Skia itself built from, and the V8
revision from the same Chromium milestone branch. Chromium's raw Skia/Dawn DEPS pins
may differ from the later published Skia branch head; that is valid only when the
v8-builder release records both generations, binds `built_skia`/`built_dawn` to the
published provider, and passes the combined provider-identity/ODR gate. `v8-builder` may also publish
newer weekly LKGR releases; those remain valid opt-in V8 choices, but they are not the
default pin for a milestone update.

## Truth model

Keep these values distinct in notes and manifests:

- `skia`: exact commit at `refs/heads/chrome/mNNN` used by skia-builder.
- `built_dawn`: Dawn from that Skia commit's own `DEPS`; this is the Dawn actually
  compiled into the Skia/Dawn artifacts.
- `v8`: V8 from Chromium `refs/branch-heads/<branch>` DEPS for milestone NNN, after
  recording Chromium's raw Skia revision separately from the provider generation
  validated by the matched release.
- `dawn`: Chromium's Dawn pin. It can differ from `built_dawn`; record the mismatch.
  Do not claim all three are one identical Chromium DEPS tuple when it differs.

`v8-builder/tools/milestone_pin.py` resolves and enforces this contract. For example:

```bash
python3 ../v8-builder/tools/milestone_pin.py 153 \
  --skia-release-tag chrome/m153 > /tmp/m153-render-lock.json
python3 -m json.tool /tmp/m153-render-lock.json
```

The result must have the requested `milestone`, `skia_release_tag`, exact Chromium
`skia`/`v8`/`dawn` SHAs, and exact `built_skia`/`built_dawn` provider SHAs. False
`skia_matches_chromium` or `dawn_matches_chromium` values are disclosed provenance,
not failures: acceptance instead requires `validated_skia_release` to equal the
active Skia version, `built_skia`/`built_dawn` to equal Pulp's active provider, every
asset's embedded pair to agree, and the combined provider-identity/ODR gate to pass.

## Release lanes

- `skia-builder` publishes `chrome/mNNN`, then dispatches
  `skia_milestone_published` to v8-builder.
- v8-builder's `matched-milestone.yml` resolves the lock, skips an exact release that
  already exists, and dispatches the sealed all-platform build with publication on.
- v8-builder's `release-watch.yml` continues its weekly LKGR cadence independently.

For a historical Skia release or recovery, manually start the same idempotent lane:

```bash
ghapp workflow run matched-milestone.yml \
  --repo danielraffel/v8-builder \
  -f milestone=153 \
  -f skia_release_tag=chrome/m153
```

Do not update Pulp's V8 pin until that matched release is published and every requested
platform asset is present. Never substitute the newest weekly V8 merely because it is
newer.

## Matched release collection

Present the Skia/Dawn and V8 release URLs together as the milestone collection, then
download only the assets the user needs:

```bash
ghapp release view chrome/m153 --repo danielraffel/skia-builder --json url
ghapp release list --repo danielraffel/v8-builder --limit 100 --json tagName \
  --jq '.[] | select(.tagName | startswith("v8-m153-")) |
    "https://github.com/danielraffel/v8-builder/releases/tag/\(.tagName)"'
```

The collection is a provenance and compatibility convenience, not one combined archive:
Skia/Dawn, V8, or both may be consumed independently.

## Independent Skia/Dawn advance

When the user explicitly asks to advance Skia/Dawn before the matching V8 build
is complete, keep the lanes separate instead of blocking the render update or
claiming a matched tuple:

1. Update the active Skia entry and its built-Dawn provenance from published
   assets. Leave the V8 version, assets, and its internal `paired_*` metadata at
   their last verified milestone.
2. State the mixed active-provider selection in `DEPENDENCIES.md` and the V8
   manifest notes. Do not rewrite V8's historical `skia_release_tag`,
   `paired_skia`, or `paired_dawn` to the newer active Skia values.
3. Run the sealed V8 provider-identity/ODR gate against the new Skia/Dawn
   provider. Keep this in the required release-path CI surface while the mixed
   selection is active; a successful one-off local or Skia-only build does not
   prove mixed-provider safety.
4. Record a precise follow-up trigger: adopt V8 only after the matched release
   contains every required platform asset and its embedded pair manifest passes
   the normal milestone checks.

This is a bounded compatibility state, not a new default release policy. Return
to a fully milestone-matched selection as soon as the verified V8 release is
available.

## Pulp update checklist

For the canonical executable validation and machine-readable result, run:

```bash
python3 tools/deps/validate_hosts.py --render-toolchain
```

The local native arm64 leg performs the full mixed-provider proof. Configured
Unix remotes populate and verify their own immutable generation, run the
capability probe, and require the second fetch to be a no-download hit. The
underlying `validate_render_update.py` JSON records platform, asset SHA,
generation receipt, capability result, cache result, and mixed-provider result;
retain those fields in the PR/landing evidence.

1. Work from current `origin/main` in a clean worktree. Read release notes from M+1
   through the target and search Pulp for removed APIs.
2. Inspect the published Skia release with `ghapp release view chrome/mNNN --repo
   danielraffel/skia-builder --json assets,publishedAt`. Update every platform URL and
   SHA-256 in `tools/deps/manifest.json`; do not omit Windows, Linux ARM64, Apple device
   and simulator slices, WASM, or XCFramework coverage.
3. Inspect one native archive and `external/skia-build/VERSION.md`. Confirm the exact
   Skia and built-Dawn SHAs, deployment floors, and optional archives such as Skottie,
   `jsonreader`, and `skresources`.
4. Inspect the matched v8-builder release manifests. All assets must agree on
   `pair.pair_kind=chromium-milestone`, `pair.milestone`, `pair.skia`, `pair.v8`,
   `pair.built_dawn`, and `pair.validated_skia_release`.
5. Update the V8 entry, all asset URLs/hashes, `DEPENDENCIES.md`, V8 provider comments,
   `tools/cmake/FindV8.cmake`, `tools/cmake/PulpV8Windows.cmake`, test fixtures, and
   `tools/deps/min_os.json` only from measured release facts.
6. Update the Skia/Dawn mirrors: `external/skia-build/VERSION.md`, visual-harness pins,
   build-script defaults, docs/support matrix, and manifest fixtures.
7. Run the manifest mirror/audit tests and both fetch-script suites. Fetch a real native
   Skia asset and matched V8 asset, configure with GPU + Lottie + V8, and run the
   provider-identity/ODR validation. A pixel-only test is insufficient.
   Configure that validation lane with `PULP_VALIDATE_CAPTURE_STRICT=ON`
   alongside `PULP_VALIDATE_V8_PROVIDER_STRICT=ON`. The default Three.js native
   demo capture tests tolerate a build without V8, or a host without a native
   Dawn adapter, as a skipped PNG assertion, so a toolchain pin that quietly
   broke the V8 link or the Dawn adapter still shows them green. The strict
   variants turn both skips into failures, which is what you want from the one
   lane whose job is to prove the new pins actually render.
   For m153+, run `python3 tools/scripts/verify_skia_m153_capabilities.py
   --platform <matching-native-desktop-platform> --skia-dir
   <materialized-generation>`. Run each architecture on its matching host; the
   compile-and-execute probe intentionally rejects mobile, WASM, Windows, and
   cross-architecture assets. The
   probe must reject a directory unless its verified asset stamp matches that
   platform's manifest digest and both Skia and Dawn archives are materialized.
   It proves the new API and exported-symbol surface against the exact provider;
   actual Graphite executor dispatch belongs to the integration's measured
   behavior gate, not this toolchain probe.
   A `darwin-universal` provider is the deliberate aggregate exception: run it
   on darwin-arm64 so the probe compile/links/runs arm64 natively and x86_64
   explicitly through Rosetta. Its single JSON result binds both records to the
   same universal asset digest, generation receipt, probe source, and Pulp
   source SHA. A universal build/lipo check without that dual-slice receipt is
   not m153 capability evidence.
8. Measure every Apple slice actually selected by the manifest. A same-tag asset can
   leak a higher deployment target than its universal sibling. Measure the exact
   thin archive when the manifest selects a thin archive; evidence from a
   universal sibling is not interchangeable. Use the verified selected asset or
   rebuild rather than publishing a false minimum-OS claim.
9. Re-bake CI goldens when either prebuilt pin changes, then ship only after required CI
   and cross-platform asset coverage are green.
10. Exercise the shared-cache publication path with
    `--cache-lock-timeout`: a pin-stale or cold cache must be populated only by
    the lock owner in a private sibling staging directory, then renamed into an
    immutable platform-plus-asset-SHA-plus-receipt-schema generation. A waiter must recheck the
    winner's exact stamp plus platform library before it skips downloading, and
    a pin bump must preserve the prior generation for bound consumers. Never
    seed the canonical cache by rsyncing a checkout merely because
    `external/skia-build/build` exists. Keep release x64/universal destinations
    isolated from the host arm64 cache.
11. Revalidate the independently pinned three.js runtime used by native WebGPU
    consumers. When `PULP_ENABLE_THREEJS_RUNTIME=ON`, Pulp fetches or accepts
    `PULP_THREEJS_RUNTIME_DIR` and stages the verified payload under
    `share/pulp/threejs`; it is not test-only. A render-toolchain change that
    alters this compatibility or install boundary must update the dependency
    manifest, attribution surfaces, runtime manifest, and installed-SDK proof.
12. After merge, prewarm every active build host through the fetcher's normal
    immutable cache-owner path. For each M3, M5, M1, Mac mini, and Mac Pro host,
    record the exact asset SHA, materialized `libskia.a` plus
    `libdawn_combined.a`, and a second invocation that reports the complete
    generation and performs no download. Skip or defer a host only with an
    explicit offline/retired disposition; never copy a checkout cache between
    machines.
13. Treat missing provenance as a cold cache. A materialized library plus a
    tracked `VERSION.md` digest is not proof that those bytes came from that
    archive: without the exact fetcher-written asset stamp, re-download and
    verify before publication. Source fallbacks must likewise pin the builder
    revision and fail before copying output unless the built Skia checkout HEAD
    equals the manifest's immutable `skia_commit`; a milestone branch name alone
    is never sufficient provenance.
13. Treat any provider release marked `release_immutable: false` as mutable until
    its publishing workflow is terminal. Record the exact publisher run and
    release `updated_at`, collect every asset ID/digest plus metadata and pair
    digests, then re-query the same release immediately before publication. If
    any asset, metadata, pair field, or release timestamp changed, invalidate the
    prior validation, repin the complete platform set, and rerun the provider and
    cache gates. A successful earlier fetch proves only the bytes that existed at
    that time; it does not authorize landing after the release was replaced.

## Common traps

- `release-path-pr-gate.yml`'s darwin runner resolver runs
  `resolve_release_runners.py --apply-class-label pulp-release-pr-gate` after a
  sparse checkout. With `PULP_RELEASE_CLASS_TOKENS` unset the selector is
  verbatim; set, a self-hosted selector gains `pulp-release-pr-gate`, so a gate
  run that never starts may be an unserved class, not a toolchain failure.
- A Skia milestone name alone does not select a V8 revision. Resolve through Chromium's
  milestone branch, preserve its raw Skia SHA, and separately prove the release's
  validated `built_skia`/`built_dawn` pair equals Pulp's active provider.
- Skia's Dawn pin and Chromium's Dawn pin are separate dependency surfaces.
- The SDK's `share/pulp/runtime-pins.json` (embedded in every bundle's
  `pulp-build-info.json`) is DERIVED, not edited: `PulpRuntimePins.cmake` parses
  `**Release:**`, the "Skia branch tip is `<sha>`" sentence, and
  `**skia-builder ref:**` out of `VERSION.md`, decodes `kDawnVersion` from the
  linked `dawn/dawn_version.h`, and takes wgpu-native from the single
  `PULP_WGPU_NATIVE_VERSION` in `PulpDependencies.cmake`. Rewording those
  VERSION.md lines turns the pins into `null`; the
  `cmake-bundle-build-info-contract` ctest compares them with the manifest's
  Skia version and Dawn notes (`DEPS file at <sha>`, `Dawn SHA1 <sha>`), so keep
  those phrasings too. A wgpu-native bump now edits that one variable plus
  `PulpWgpuUniversal.cmake`'s slice digests and `shared-source-contract.txt`.
- `PulpDependencies.cmake` sets `CMAKE_DISABLE_PRECOMPILE_HEADERS ON` around
  SDL3's `FetchContent_MakeAvailable` only. SDL3 precompiles `src/SDL_internal.h`
  into every object, and ccache refuses to cache a TU compiled against a PCH
  unless `pch_defines` sloppiness is on (measured on 3.2.12: 188 of 214 SDL3
  calls "could not use precompiled header"). Keep the scope tight when bumping
  SDL3: the save/restore of the previous value is what keeps a later PCH user
  (the Catch2 test carriers in `PulpTestSuite.cmake`) unaffected. The
  `test-pch-wiring` ctest asserts `SDL3-static` compiles with no PCH.
- GitHub release tags containing `/` must remain correctly URL-encoded/handled.
- Linux x64 Skia and V8 assets must retain the portable glibc floor; do not replace
  their portable releases with a normal ubuntu-latest artifact.
- `fetch_skia_for_release.py` platform keys must match the manifest exactly (notably
  `wasm-wasm32`).
- `fetch_skia_for_release.py` retries the asset download, but only for failures a
  second attempt can fix: 408/425/429 and 5xx, plus `URLError`, `TimeoutError`,
  `ConnectionError` and `IncompleteRead`, with exponential backoff from 2s capped at
  30s and a numeric `Retry-After` taking precedence. A 403/404 raises immediately,
  because at this stage that means the pin names an asset that is not there, and
  spending the backoff first buries that error under minutes of silence. A bare
  `OSError` is deliberately not transient either: it is what a full disk raises on
  the write side, and retrying re-downloads hundreds of megabytes to fill the same
  disk. When a pin bump fails here, read which class it was before assuming the
  network.
- Keep release-fetch progress output ASCII-safe. Windows release runners can use a
  cp1252 console, where decorative Unicode arrows raise `UnicodeEncodeError` before
  an asset download starts; exercise the full Windows fetch path with cp1252 stdout.
- Compare extracted-generation receipts in a host-independent canonical path order.
  Windows `Path` ordering is case-insensitive while ZIP member names use POSIX,
  case-sensitive ordering; directly comparing those sorted lists can reject an
  otherwise byte-identical authenticated archive (for example `SkBlendMode.h` and
  `SkBlender.h`). Keep path/hash/size integrity checks exact, but canonicalize both
  projections by their serialized archive path before comparing them.
- JS-engine wording in `tools/deps/manifest.json`, `tools/deps/min_os.json`, and
  `tools/cmake/FindV8.cmake` describes a *selection contract*, not just prose.
  The contract is: `auto`/`quickjs` compile QuickJS only; `jsc` additionally
  compiles `core/view/src/js_jsc_engine.mm` and links
  `JavaScriptCore.framework` on Apple; `v8` selects the sealed prebuilt. JSC is
  **opt-in**, never implied by "Apple". Older text said "default is QuickJS, JSC
  on Apple", which reads as JSC being automatic on Apple platforms and is wrong.
  Likewise iOS is no longer "JSC-only": Pulp has no device/AUv3 V8 runtime
  acceptance or packaging contract. The m153 V8 release includes a jitless
  simulator framework only for provider/header provenance validation; it is not
  selectable as an iOS runtime. QuickJS is the default and JSC stays opt-in.
  When a pin or min-OS note is edited, keep these three files saying the same
  thing — they are the only place the engine contract is written down outside
  the CMake modules.
- Matched V8 cold-fetch validation resolves the immutable builder tag through
  the GitHub API. CI callers must expose their read-only `${{ github.token }}`
  as `GH_TOKEN`; otherwise a valid sealed asset can fail after download when
  the unauthenticated API budget is exhausted. The fetcher deliberately sends
  that credential only to `https://api.github.com` and refuses redirects; it is
  never forwarded to release-asset or cross-origin targets.
- A source dependency pinned by per-file SHA-256 must be checked out with EOL
  conversion off, or Windows breaks it before anything builds. Git for Windows
  defaults to `core.autocrlf=true`, so every LF becomes CRLF on checkout, every
  digest in `tools/cmake/threejs-runtime-manifest.json` mismatches, and
  `PulpDependencies.cmake` hard-fails with "Three.js runtime file does not match
  pinned revision". Three.js opts in by passing `verbatim-eol` to
  `ensure_shared_git_source` in `setup.sh`, which expands to
  `-c core.autocrlf=false -c core.eol=lf` on the clone and pins both keys
  repo-locally; the `FetchContent_Declare` carries a matching `GIT_CONFIG` for
  the no-shared-cache path. Never fix this by normalizing bytes before hashing —
  the digest's whole job is to prove the shipped files are upstream's exact
  bytes. Any future digest-pinned *source* dependency needs the same opt-in.
  Note that `GIT_CONFIG` on `FetchContent_Declare` alone is not enough on the
  release path: `pulp_register_fetchcontent_source` short-circuits FetchContent
  whenever the shared cache exists, and the release leg primes that cache with
  `./setup.sh --ci --deps-only` first.
- Repairing an already-converted cache with `git checkout --force -- .` silently
  does nothing. Git decides a file is current from the index's cached stat data
  and never reads the bytes, and the bad checkout recorded its own stat when it
  was written; `--force` means "overwrite local modifications", and by the stat
  cache there are none. `git checkout-index -a -f` and `git read-tree --reset -u`
  fail the same way. The primitive that works is dropping `.git/index` and then
  `git reset --hard` (gitattributes(5)'s own renormalisation recipe), which needs
  no network even on a `--filter=blob:none` partial clone. This trap is not
  specific to line endings — any repair phrased as "check the worktree out again"
  hits it. A test written the obvious way (poison, immediately repair, assert)
  will certify the broken primitive, because git distrusts a stat inside its own
  timestamp granularity and falls back to comparing content; settle the fixture
  with `touch -t 202001010000` plus `git update-index --refresh` or the test
  grades its own homework.
- The visual harness has two raster pins on two different version lines, and
  "re-bake CI goldens" only means one of them. The C++ Skia archive rasterizes
  nothing in `tools/harness/visual/`: its committed PNG golden is produced by the
  `skia-python` wheel, pinned separately as `determinism.skia_python_smoke_version`
  in `tools/deps/manifest.json` (mirrored into `pins.SKIA_PYTHON_SMOKE_VERSION`
  and the Dockerfile `ARG`, cross-checked by `check_skia_pin.py`). That wheel
  deliberately trails the C++ milestone, so a Skia/Dawn milestone bump leaves the
  PNG golden and `pins.RASTER_GOLDEN_SHA256` correct and untouched, while bumping
  only the wheel invalidates both without moving a single release-asset digest.
  When changing the wheel, regenerate through
  `python3 -m tools.harness.visual.runner --generate --all --surface canvas2d`
  and update the recorded sha256 in the same commit: a golden regenerated without
  its digest fails `tests/test_raster_golden.py` before any raster runs.
  `pins.RASTER_GOLDEN_VERIFIED_PLATFORMS` records which hosts that identity was
  actually measured on, so add a platform key only after a run on that platform
  reported the matching digest.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…