Skip to content

The dev-tools image

Every Go, Rust, and Svelte component runs on registry.gitlab.com/phpboyscout/images/dev-tools by default. That wasn't always true — earlier versions of these components installed their toolchain tools at job runtime, on nearly every pipeline job.

The problem: paying the install cost on every job

Before dev-tools, a rust-security job would cargo install --locked cargo-binstall then cargo binstall cargo-deny cargo-audit before it could run either scanner. A go-security job would go install golang.org/x/vuln/cmd/govulncheck@latest before it could scan. Multiply that across every MR, every job, every consumer repo, and it's a network fetch plus (for some tools) a compile, paid over and over for tools that don't change between runs.

dev-tools bakes them instead: Go, Rust (via rustup), Node, and the CI tools each language's components need (cargo-nextest, cargo-llvm-cov, cargo-deny, cargo-audit, govulncheck) all live in one image, versioned together. Switching the component image defaults to it and deleting the runtime installs was, in the project's own words, "the single biggest recurring-time win on the self-hosted runner" — 2026-06-22-components-use-dev-tools-v0.14.md.

What actually changed

Component input what dropped
go-security govulncheck_image go install golang.org/x/vuln/cmd/govulncheck@latest
rust-test image cargo install cargo-binstall + cargo binstall cargo-nextest (+ cargo-llvm-cov for the coverage job)
rust-security rust_image the binstall bootstrap for cargo-deny / cargo-audit

The scripts now call the tool directly — govulncheck ./..., cargo nextest run, cargo deny check — because it's already on PATH.

What deliberately stayed

Not everything moved onto dev-tools, and each exception has a reason:

  • rustup component add clippy|rustfmt|llvm-tools-preview stays in the scripts. dev-tools bakes these for its own pinned Rust toolchain, but a consumer's rust-toolchain.toml can make rustup install a different toolchain at runtime — and that toolchain needs its own components. rustup component add is a fast, local no-op when the component is already present, and a correctness safeguard when it isn't. (The cargo binary tools — nextest, deny, audit — are toolchain-independent, so baking them works regardless of which Rust the project pins.)
  • Cross-OS jobs are untouched. rust-test's test-macos / test-windows run on saas-macos / saas-windows runners and bootstrap Rust + tools there — dev-tools is a Linux image, so those jobs keep their own install logic.
  • Security scanners with their own upstream images are untouched. trivy, gitleaks, osv-scanner, and analyze (semgrep) run from their own pinned images regardless of language track — they were never a runtime install to begin with, so there was nothing to bake.
  • Vestigial version inputs stay, ignored, for compatibility. rust-security's cargo_deny_version/cargo_audit_version and rust-test's binstall_version no longer drive anything — the image pins those versions now — but removing the inputs outright would be a breaking change for a consumer who overrode them. They're documented as deprecated and slated for removal in a later major.

Why this belongs to Go/Rust/Svelte and not Tofu

The Terraform/OpenTofu and static-site components run on infra-tools instead — a separate, older image consolidating tofu, tflint, trivy, checkov, terraform-docs, and (for Zensical) Python. infra-tools predates dev-tools and follows the identical philosophy — bake the toolchain once, version it independently of the components — just for a different set of tools. dev-tools exists because the Go/Rust/Svelte tracks needed the same treatment for their own toolchains.

See also