CI usage¶
CI with ixt is simple: commit
ixt.tomlandixt.lock, check the lock, runixt tool apply --locked --strict, then prune the cache at the end.
ixt gives CI one toolchain path for Python packages, npm packages, and GitHub/GitLab release binaries:
- one declarative file:
ixt.toml; - one generated exact-resolution file:
ixt.lock; - one materialization command:
ixt tool apply --locked --strict --remove --yes; - lazy runtimes: reuse
uv/bunwhen present, bootstrap only what is missing; - cached binary resolution: learned GitHub asset patterns and downloaded
archives live in
$IXT_CACHE_HOME; - portable binary metadata:
ixt asset-index exportcan turn learned GitHub release asset knowledge into a reusable CI artifact.
This means the optimized CI path is not "install everything globally". It is:
- restore
$IXT_CACHE_HOMEand uv's cache/tool dirs; - install the
ixtcommand withuv tool install ixt-cli; - preload
asset_index.jsonif one exists; - run
ixt lock check; - run
ixt tool apply --locked --strict --remove --yes; - run the job using the shims in
$IXT_HOME/installed/bin; - export a fresh
asset_index.json; - run
ixt cache pruneso the cache stays bounded.
Minimal job¶
Use this when you just want ixt in one CI job.
image: ghcr.io/astral-sh/uv:python3.13-trixie-slim
variables:
IXT_HOME: "$CI_PROJECT_DIR/.ixt/home"
IXT_CACHE_HOME: "$CI_PROJECT_DIR/.ixt/cache"
UV_CACHE_DIR: "$CI_PROJECT_DIR/.ixt/uv/cache"
UV_TOOL_DIR: "$CI_PROJECT_DIR/.ixt/uv/tools"
UV_TOOL_BIN_DIR: "$CI_PROJECT_DIR/.ixt/uv/bin"
cache:
key: ixt-tools
paths:
- .ixt/cache/
- .ixt/uv/
- .ixt/asset_index.json
before_script:
- apt-get update -qq
- apt-get install -y -qq --no-install-recommends git ca-certificates
- export PATH="$UV_TOOL_BIN_DIR:$IXT_HOME/installed/bin:$PATH"
- uv tool install ixt-cli
- test ! -f .ixt/asset_index.json || export IXT_ASSET_INDEX="$PWD/.ixt/asset_index.json"
- ixt lock check
- ixt tool apply --locked --strict --remove --yes
lint:
script:
- ruff check .
after_script:
- export PATH="$UV_TOOL_BIN_DIR:$IXT_HOME/installed/bin:$PATH"
- mkdir -p .ixt
- ixt asset-index export > .ixt/asset_index.json || true
- ixt cache prune || true
ixt lock check verifies that the committed ixt.lock still matches the
committed ixt.toml using only local files; it never checks registries or
release APIs for a newer version. ixt tool apply --locked --strict then
installs missing tools, re-links changed shims, updates injections/exposure,
and removes tools not in the manifest because --remove --yes is set.
Do not cache $IXT_HOME/installed/envs. Rebuild installed tools from
ixt.lock; cache the downloads and resolver metadata instead.
Optimized workflow¶
For a larger pipeline, split tool resolution into a warmup job and reuse the learned asset index in downstream jobs.
stages:
- tools
- test
.ixt:
image: ghcr.io/astral-sh/uv:python3.13-trixie-slim
variables:
IXT_HOME: "$CI_PROJECT_DIR/.ixt/home"
IXT_CACHE_HOME: "$CI_PROJECT_DIR/.ixt/cache"
UV_CACHE_DIR: "$CI_PROJECT_DIR/.ixt/uv/cache"
UV_TOOL_DIR: "$CI_PROJECT_DIR/.ixt/uv/tools"
UV_TOOL_BIN_DIR: "$CI_PROJECT_DIR/.ixt/uv/bin"
cache:
key: ixt-tools
paths:
- .ixt/cache/
- .ixt/uv/
- .ixt/asset_index.json
before_script:
- apt-get update -qq
- apt-get install -y -qq --no-install-recommends git ca-certificates
- export PATH="$UV_TOOL_BIN_DIR:$IXT_HOME/installed/bin:$PATH"
- uv tool install ixt-cli
- test ! -f .ixt/asset_index.json || export IXT_ASSET_INDEX="$PWD/.ixt/asset_index.json"
tools:warmup:
extends: .ixt
stage: tools
script:
- ixt lock check
- ixt tool apply --locked --strict --remove --yes
- ixt asset-index export > .ixt/asset_index.json
- ixt cache prune
- ixt cache info
artifacts:
paths:
- .ixt/asset_index.json
expire_in: 1 week
lint:
extends: .ixt
stage: test
needs:
- job: tools:warmup
artifacts: true
script:
- export IXT_ASSET_INDEX="$PWD/.ixt/asset_index.json"
- export IXT_GITHUB_API=never
- ixt lock check
- ixt tool apply --locked --strict --remove --yes
- ruff check .
after_script:
- export PATH="$UV_TOOL_BIN_DIR:$IXT_HOME/installed/bin:$PATH"
- mkdir -p .ixt
- ixt asset-index export > .ixt/asset_index.json || true
- ixt cache prune || true
What this buys you:
| Step | What ixt optimizes |
|---|---|
ixt lock check |
Fails early if ixt.toml and ixt.lock diverge. |
ixt tool apply --locked --strict --remove --yes |
Rebuilds the job toolchain from ixt.lock, across Python, npm, and binary tools. |
$IXT_CACHE_HOME cache |
Reuses downloads, learned GitHub asset patterns, and resolver metadata between jobs. |
ixt asset-index export |
Publishes portable GitHub binary asset metadata for downstream jobs or later pipelines. |
IXT_ASSET_INDEX |
Preloads that metadata before binary resolution starts. |
IXT_GITHUB_API=never |
Forbids the GitHub Releases API fallback when the asset index/cache should be enough. |
ixt cache prune |
Keeps old binary downloads bounded without touching installed tools or metadata needed for resolution. |
IXT_ASSET_INDEX and IXT_GITHUB_API=never are performance and network-policy
tools, not reproducibility guarantees. Reproducibility comes from ixt.lock
plus --locked --strict. IXT_GITHUB_API=never only disables
api.github.com release API fallback; latest redirects and direct release
downloads still work.
Binary downloads are keyed by release tag and exact URL. After upgrading from 0.9.0, an existing flat download is deliberately ignored once and fetched into the safe cache namespace; subsequent jobs can reuse that exact identity.
ixt.toml and ixt.lock in CI¶
ixt.toml stays human-authored. Use exact pins when the project wants to
declare that a tool must stay on one release:
[tools]
"@pypi:ruff" = { version = "==0.13.0" }
"@npm:@biomejs/biome" = { version = "==1.9.4" }
"@gh:sharkdp/fd" = { version = "==10.2.0" }
Use ranges only when the job intentionally accepts compatible updates:
ixt.lock freezes the current resolution, including tools declared with
ranges or no version constraint. Update it intentionally:
In CI, always check and replay the committed lock:
Runtime behavior¶
| Base CI image | Recommendation | ixt reuses | ixt may bootstrap |
|---|---|---|---|
ghcr.io/astral-sh/uv:* |
Recommended default | uv, Python |
bun if npm backend is used |
| Ubuntu minimal | Install standalone uv, then ixt |
uv after install |
bun if npm backend is used |
python:* |
OK for wheel/pip validation | Python/pip | uv, then bun if needed |
node:* |
Use only for real Node/npm jobs | node/npm | uv if Python backend is used |
oven/bun:* |
Good for npm-heavy jobs | bun |
uv if Python backend is used |
| Binary-only job | No external runtime needed | none | none |
ixt first looks for a valid runtime on PATH. If a backend needs a runtime
that is missing, ixt bootstraps only that runtime.
Cache maintenance¶
Cache these:
| Path | Why |
|---|---|
$IXT_CACHE_HOME |
Downloaded archives, learned asset patterns, metadata, resolve cache. |
UV_CACHE_DIR |
Wheels and metadata used by uv. |
UV_TOOL_DIR |
The uv tool install ixt-cli environment. |
Do not cache these in normal CI:
| Path | Why |
|---|---|
$IXT_HOME/installed/envs |
Rebuild from ixt.toml; envs are job-local state. |
$IXT_HOME/installed/runtimes |
Recreate quickly from cache when needed. |
Recommended end-of-job cleanup:
ixt asset-index export > .ixt/asset_index.json
ixt cache prune
# If your CI cache, runner, or image layer keeps $IXT_HOME/installed/runtimes:
ixt runtime prune
ixt cache info
ixt cache prune keeps the newest two indexed download artifacts per binary
repository by default: current + previous. It does not touch installed tools,
shims, config, runtimes, or metadata needed for future resolution.
Use ixt runtime prune when $IXT_HOME/installed/runtimes is part of what
survives the job: a broad CI cache, a long-lived runner, or a Docker image
layer. It removes unused ixt-managed runtimes; later installs can bootstrap
them again.
Docker multi-stage images¶
For a final Docker image that already contains ixt-installed tools, build the
tool layer once and copy only $IXT_HOME/installed into the final image.
FROM ghcr.io/astral-sh/uv:python3.13-trixie-slim AS tools
ENV IXT_HOME=/opt/ixt \
IXT_CACHE_HOME=/var/cache/ixt \
PATH="/opt/ixt/installed/bin:${PATH}"
RUN uv tool install ixt-cli
WORKDIR /tmp
COPY ixt.toml ixt.lock ./
RUN ixt lock check
RUN ixt tool apply --locked --strict --remove --yes
RUN ixt cache prune
RUN ixt runtime prune
FROM ghcr.io/astral-sh/uv:python3.13-trixie-slim
ENV IXT_HOME=/opt/ixt \
PATH="/opt/ixt/installed/bin:${PATH}"
COPY --from=tools /opt/ixt/installed /opt/ixt/installed
The final stage needs $IXT_HOME/installed/bin and
$IXT_HOME/installed/envs. $IXT_CACHE_HOME, uv caches, build temp files, and
$IXT_HOME/config stay in the build stage.
For binary-only images, the final stage can be a smaller base such as
debian:trixie-slim, as long as the extracted binaries' shared-library
requirements are satisfied. Do not use the binary-only pattern for Python tools
unless the final image also preserves or repairs the venv's runtime paths.
Diagnostics¶
Use these at the start of a CI job when debugging the environment:
Use resolver verbosity only when you need to understand network/cache behavior:
ixt tool upgrade --all -v # compact resolver summary
ixt tool upgrade --all -vv # per-package resolver traces
For runtime bootstrap regressions in this repo, use the manual Docker matrix:
The matrix checks that each backend bootstraps only what it needs.