# Locked CI toolchains

Check out this repository, then use `./.github/actions/setup-pm`. The runner's
preinstalled Python bootstraps PM with the standard library only. PM downloads
and verifies the exact native artifacts from `pm/lock.json`. The action does
not resolve a version range, install another setup action, or modify the lock.

```yaml
- uses: useblacksmith/checkout@25227e61ff9dafe400e22fa487b673eac4e4409a  # v1
- uses: ./.github/actions/setup-pm
  with:
    toolchain: all
    extras: '[]'
    test-environment: 'true'
- run: python --version && node --version && npm --version
```

`toolchain` defaults to `python` (Python and PM's private installer). `node` installs Node and npm;
`all` installs both pairs. There are no version overrides. `extras` is a JSON
array because GitHub action inputs are strings. Omit it for tools only; `[]`
installs the core Python dependencies. Set `test-environment: 'true'` to build an
independent interpreter with the `dev` and `test` dependency groups; any `extras`
then select runtime features in that interpreter. PM checks `uv.lock`, installs
the requested dependencies, and validates the environment. Only non-test installs
publish an application selection. It does not enable plugins.

Subsequent steps get `python`, `python3`, `node`, `npm` and `npx`
for the selected toolchain on PATH. The pinned npm precedes Node's bundled npm.
On Windows, PM selects the host architecture even if the bootstrap interpreter
runs under x64 emulation. A disposable command environment supplies the missing
`python3.exe` alias without changing the verified interpreter store.

For Python dependencies, the action exports `HERMES_PYTHON` and `VIRTUAL_ENV`.
Use `scripts/run_tests.sh`; do not activate `.venv`. The environment belongs to
PM under the runner's temporary home, not the checkout. No installer executable
or `UV_*` policy variables are exposed. Tool-only jobs can prepare small isolated
CI environments with `python -m scripts.ci.python_packages package==version`.
This exports the selected Python and tool entrypoints for subsequent steps;
append `-- -m module ...` to run a Python command directly and preserve its exit
status. Neither route modifies the cached tool store. Native builds still need their system libraries
and compiler, such as OpenSSL for Windows ARM64 cryptography.

## Caches

The official, SHA-pinned `actions/cache` transports three independent caches:

| Cache | Contents | Identity |
| --- | --- | --- |
| Tools | PM store and installed facts | Native target, toolchain and PM lock hash |
| Python | PM's actual uv download/build cache | Native target, OS version, Python version, prune policy and dependency-file hash |
| Node | `npm config get cache` | Runner OS, native architecture and npm dependency-lock hash |

All restores use the exact primary key, without fallback prefixes, matching
setup-uv and setup-node's npm behavior. Successful jobs save at teardown;
exact hits are not saved again. Only dependency-carrying callers
(`extras` set or `test-environment: 'true'`) auto-save the uv cache: a tool-only job never runs a
dependency operation, so letting it save would freeze an empty cache under
the production key, where an immutable exact hit blocks real saves forever.
PM re-verifies restored tools before use.
Dependency caches never contain `node_modules` or virtual environments.
Keep an installed-tree cache in the caller if that job needs one.

`cache`, `cache-python` and `cache-node` independently disable the store, uv,
and npm caches. Each defaults to `true`; language-specific caches run only for
that toolchain. `python-cache-dependency-glob` defaults to `pyproject.toml` and
`uv.lock`. `node-cache-dependency-path` defaults to `package-lock.json`; use
`website/package-lock.json` for site jobs. Both accept multiline glob strings.
An npm cache without a matching lockfile fails, rather than caching an
unversioned dependency set.

`prune-python-cache: true` registers PM's lock-exact cache-pruning operation at teardown,
after the caller's installs and before the cache save. It is skipped on an
exact hit. The default is `false`, as in setup-uv v9; migrated v8 callers opt in
to retain their former policy. The small nested JavaScript action exists only
because GitHub composite actions cannot declare their own post step. It uses
Node's standard library and has no bundled dependencies. Pruning deletes cache
entries the project's `uv.lock` cannot resolve (the same exactness contract the
bundle ship gate enforces) and keeps downloaded wheels the lock still needs —
`uv cache prune --ci` would discard them, leaving a snapshot that warms almost
nothing.

Outputs include `python-version`, `uv-version`, `node-version`, `npm-version`,
`python-path`, `venv`, `target`, and the three `*-cache-hit` flags.
Use the version outputs in installed-tree cache keys instead of repeating pins.

The tools cache key hashes `pm/**`, this action, and `scripts/ci/setup_toolchain.py`,
so a provisioning change re-runs the cold path on every lane that uses the action
(`tests-os.yml` covers macOS and both Windows architectures). `cache-suffix` is an
optional namespace for isolated cache experiments; it changes lookup, not storage.
