Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions config/golden-image-container-integration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,6 @@ toolchains:
archive: pnpm-11.22.0.tgz
download_url: https://registry.npmjs.org/pnpm/-/pnpm-11.22.0.tgz
archive_sha256: 57a97e6f23a3faffc03153a4ef8c770a0552612b8640aebe39bfdd5754d0ebdc
- name: rust
version: 1.97.1
archive: rust-1.97.1-x86_64-unknown-linux-gnu.tar.xz
download_url: https://static.rust-lang.org/dist/rust-1.97.1-x86_64-unknown-linux-gnu.tar.xz
archive_sha256: 88f28fa9af20594179f85d6df67078dfd6fa93e2f6da5e1e9b0ac4997988ca4f
- name: rustup
version: 1.29.0
archive: rustup-init
Expand Down
5 changes: 0 additions & 5 deletions config/golden-image-container.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,6 @@ toolchains:
archive: pnpm-11.22.0.tgz
download_url: https://registry.npmjs.org/pnpm/-/pnpm-11.22.0.tgz
archive_sha256: 57a97e6f23a3faffc03153a4ef8c770a0552612b8640aebe39bfdd5754d0ebdc
- name: rust
version: 1.97.1
archive: rust-1.97.1-x86_64-unknown-linux-gnu.tar.xz
download_url: https://static.rust-lang.org/dist/rust-1.97.1-x86_64-unknown-linux-gnu.tar.xz
archive_sha256: 88f28fa9af20594179f85d6df67078dfd6fa93e2f6da5e1e9b0ac4997988ca4f
- name: rustup
version: 1.29.0
archive: rustup-init
Expand Down
5 changes: 0 additions & 5 deletions config/golden-image-integration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,6 @@ toolchains:
archive: pnpm-11.22.0.tgz
download_url: https://registry.npmjs.org/pnpm/-/pnpm-11.22.0.tgz
archive_sha256: 57a97e6f23a3faffc03153a4ef8c770a0552612b8640aebe39bfdd5754d0ebdc
- name: rust
version: 1.97.1
archive: rust-1.97.1-x86_64-unknown-linux-gnu.tar.xz
download_url: https://static.rust-lang.org/dist/rust-1.97.1-x86_64-unknown-linux-gnu.tar.xz
archive_sha256: 88f28fa9af20594179f85d6df67078dfd6fa93e2f6da5e1e9b0ac4997988ca4f
- name: rustup
version: 1.29.0
archive: rustup-init
Expand Down
5 changes: 0 additions & 5 deletions config/golden-image.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,6 @@ toolchains:
archive: pnpm-11.22.0.tgz
download_url: https://registry.npmjs.org/pnpm/-/pnpm-11.22.0.tgz
archive_sha256: 57a97e6f23a3faffc03153a4ef8c770a0552612b8640aebe39bfdd5754d0ebdc
- name: rust
version: 1.97.1
archive: rust-1.97.1-x86_64-unknown-linux-gnu.tar.xz
download_url: https://static.rust-lang.org/dist/rust-1.97.1-x86_64-unknown-linux-gnu.tar.xz
archive_sha256: 88f28fa9af20594179f85d6df67078dfd6fa93e2f6da5e1e9b0ac4997988ca4f
- name: rustup
version: 1.29.0
archive: rustup-init
Expand Down
33 changes: 7 additions & 26 deletions internal/imagebuild/assets/provision.sh
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,9 @@ toolchain_manifest="$(printf '%s' "${GHA_TOOLCHAINS_B64}" | base64 --decode)"
jq -e 'type == "array"' <<<"${toolchain_manifest}" >/dev/null
mapfile -t toolchain_names < <(jq -r '.[].name' <<<"${toolchain_manifest}")
toolchain_set="$(printf '%s\n' "${toolchain_names[@]}" | LC_ALL=C sort | paste -sd, -)"
if [[ "${toolchain_set}" != bun,gh,go,rust,rustup,uv \
&& "${toolchain_set}" != bun,gh,go,node22,node24,node25,pnpm,rust,rustup,uv,yarn \
&& "${toolchain_set}" != bun,flutter,gh,go,node22,node24,node25,pnpm,rust,rustup,uv,yarn ]]; then
if [[ "${toolchain_set}" != bun,gh,go,rustup,uv \
&& "${toolchain_set}" != bun,gh,go,node22,node24,node25,pnpm,rustup,uv,yarn \
&& "${toolchain_set}" != bun,flutter,gh,go,node22,node24,node25,pnpm,rustup,uv,yarn ]]; then
echo "toolchain manifest does not pin the exact baked set" >&2
exit 1
fi
Expand Down Expand Up @@ -332,28 +332,6 @@ for toolchain_name in "${toolchain_names[@]}"; do
ln -sfn "${package_root}/bin/pnpx.cjs" /usr/local/bin/pnpx
[[ "$(pnpm --version)" == "${toolchain_version}" ]]
;;
rust)
# The standalone archive alone is not what the estate asks for. Every
# setup-system pins rust-toolchain.toml to a channel with rustfmt and
# clippy, and actions-rust-lang/setup-rust-toolchain resolves that through
# rustup -- so a system-wide rustc with no rustup, no clippy and no
# rustfmt is invisible to it and every job downloads a toolchain instead.
# Measured across the seven public anchors: 28 workflow requests for
# 1.98.0 and 7 for the 1.89 MSRV, against a baked 1.97.1.
#
# rustup owns the layout, so the archive installs into it rather than into
# /usr/local, and the extra channels are installed beside it. RUSTUP_HOME
# and CARGO_HOME are system paths so the toolchains are shared rather than
# re-fetched per job, and the shims land on PATH for jobs that never call
# the action at all.
tar --extract --xz --file "${toolchain_archive}" \
--directory "${toolchain_scratch}" --no-same-owner --no-same-permissions
"${toolchain_scratch}/rust-${toolchain_version}-x86_64-unknown-linux-gnu/install.sh" \
--prefix=/usr/local \
--components=rustc,cargo,rust-std-x86_64-unknown-linux-gnu >/dev/null
[[ "$(rustc --version)" == "rustc ${toolchain_version} "* ]]
[[ "$(cargo --version)" == "cargo ${toolchain_version} "* ]]
;;
rustup)
install -o root -g root -m 0755 "${toolchain_archive}" "${toolchain_scratch}/rustup-init"
export RUSTUP_HOME=/usr/local/rustup CARGO_HOME=/usr/local/cargo
Expand All @@ -365,7 +343,10 @@ for toolchain_name in "${toolchain_names[@]}"; do
# rust-toolchain.toml files name, so the action finds them already present.
mapfile -t rust_channels < <(jq -r '.channels[]?' <<<"${entry}")
rust_default="$(jq -r '.default_channel // ""' <<<"${entry}")"
[[ ${#rust_channels[@]} -gt 0 && -n "${rust_default}" ]]
if [[ ${#rust_channels[@]} -eq 0 || -z "${rust_default}" ]]; then
echo "rustup entry carries no channels or no default channel: the manifest names them, so the provisioning contract dropped them" >&2
exit 1
fi
for channel in "${rust_channels[@]}"; do
"${CARGO_HOME}/bin/rustup" toolchain install "${channel}" \
--profile minimal --component clippy --component rustfmt >/dev/null
Expand Down
25 changes: 21 additions & 4 deletions internal/imagebuild/assets/smoke-integration.sh
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ runner_tool_cache="$(jq -er .runner_tool_cache /etc/nddev/image-build.json)"
[[ "${runner_tool_cache}" == /home/runner/actions-runner/_work/_tool ]]
smoke_toolchains="$(printf '%s' "${GHA_TOOLCHAINS_B64}" | base64 --decode)"
mapfile -t smoke_toolchain_names < <(jq -r '.[].name' <<<"${smoke_toolchains}")
[[ "$(printf '%s\n' "${smoke_toolchain_names[@]}" | LC_ALL=C sort | paste -sd, -)" == bun,gh,go,node22,node24,node25,pnpm,rust,uv,yarn ]]
[[ "$(printf '%s\n' "${smoke_toolchain_names[@]}" | LC_ALL=C sort | paste -sd, -)" == bun,gh,go,node22,node24,node25,pnpm,rustup,uv,yarn ]]
for smoke_toolchain in "${smoke_toolchain_names[@]}"; do
entry="$(jq -ce --arg name "${smoke_toolchain}" '.[] | select(.name == $name)' <<<"${smoke_toolchains}")"
expected_version="$(jq -er .version <<<"${entry}")"
Expand Down Expand Up @@ -128,9 +128,26 @@ for smoke_toolchain in "${smoke_toolchain_names[@]}"; do
fi
;;
pnpm) [[ "$(pnpm --version)" == "${expected_version}" ]] ;;
rust)
[[ "$(rustc --version)" == "rustc ${expected_version} "* ]]
[[ "$(cargo --version)" == "cargo ${expected_version} "* ]]
rustup)
# rustup owns the whole Rust surface: the estate pins channels in
# rust-toolchain.toml, actions-rust-lang/setup-rust-toolchain resolves
# them through rustup, and the shims on PATH serve jobs that never call
# the action. Every pinned channel must already hold clippy and rustfmt,
# or the per-job download this image exists to remove comes back.
[[ "$(rustup --version 2>/dev/null)" == "rustup ${expected_version} "* ]]
[[ "$(stat --format='%U' -- /usr/local/rustup)" == root ]]
[[ "$(stat --format='%U' -- /usr/local/cargo)" == root ]]
grep -q '^RUSTUP_HOME=/usr/local/rustup$' /etc/environment
mapfile -t smoke_rust_channels < <(jq -r '.channels[]?' <<<"${entry}")
smoke_rust_default="$(jq -er '.default_channel' <<<"${entry}")"
[[ ${#smoke_rust_channels[@]} -gt 0 ]]
for smoke_rust_channel in "${smoke_rust_channels[@]}"; do
[[ "$(rustup run "${smoke_rust_channel}" rustc --version)" == "rustc ${smoke_rust_channel}"* ]]
[[ "$(rustup run "${smoke_rust_channel}" cargo clippy --version)" == clippy* ]]
[[ "$(rustup run "${smoke_rust_channel}" rustfmt --version)" == rustfmt* ]]
done
[[ "$(rustc --version)" == "rustc ${smoke_rust_default}"* ]]
[[ "$(cargo --version)" == "cargo "* ]]
;;
uv)
[[ "$(uv --version)" == "uv ${expected_version}"* ]]
Expand Down
29 changes: 23 additions & 6 deletions internal/imagebuild/assets/smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ runner_tool_cache="$(jq -er .runner_tool_cache /etc/nddev/image-build.json)"
smoke_toolchains="$(printf '%s' "${GHA_TOOLCHAINS_B64}" | base64 --decode)"
mapfile -t smoke_toolchain_names < <(jq -r '.[].name' <<<"${smoke_toolchains}")
smoke_toolchain_set="$(printf '%s\n' "${smoke_toolchain_names[@]}" | LC_ALL=C sort | paste -sd, -)"
[[ "${smoke_toolchain_set}" == bun,gh,go,rust,uv \
|| "${smoke_toolchain_set}" == bun,gh,go,node22,node24,node25,pnpm,rust,uv,yarn \
|| "${smoke_toolchain_set}" == bun,flutter,gh,go,node22,node24,node25,pnpm,rust,uv,yarn ]]
[[ "${smoke_toolchain_set}" == bun,gh,go,rustup,uv \
|| "${smoke_toolchain_set}" == bun,gh,go,node22,node24,node25,pnpm,rustup,uv,yarn \
|| "${smoke_toolchain_set}" == bun,flutter,gh,go,node22,node24,node25,pnpm,rustup,uv,yarn ]]
for smoke_toolchain in "${smoke_toolchain_names[@]}"; do
entry="$(jq -ce --arg name "${smoke_toolchain}" '.[] | select(.name == $name)' <<<"${smoke_toolchains}")"
expected_version="$(jq -er .version <<<"${entry}")"
Expand Down Expand Up @@ -115,9 +115,26 @@ for smoke_toolchain in "${smoke_toolchain_names[@]}"; do
fi
;;
pnpm) [[ "$(pnpm --version)" == "${expected_version}" ]] ;;
rust)
[[ "$(rustc --version)" == "rustc ${expected_version} "* ]]
[[ "$(cargo --version)" == "cargo ${expected_version} "* ]]
rustup)
# rustup owns the whole Rust surface: the estate pins channels in
# rust-toolchain.toml, actions-rust-lang/setup-rust-toolchain resolves
# them through rustup, and the shims on PATH serve jobs that never call
# the action. Every pinned channel must already hold clippy and rustfmt,
# or the per-job download this image exists to remove comes back.
[[ "$(rustup --version 2>/dev/null)" == "rustup ${expected_version} "* ]]
[[ "$(stat --format='%U' -- /usr/local/rustup)" == root ]]
[[ "$(stat --format='%U' -- /usr/local/cargo)" == root ]]
grep -q '^RUSTUP_HOME=/usr/local/rustup$' /etc/environment
mapfile -t smoke_rust_channels < <(jq -r '.channels[]?' <<<"${entry}")
smoke_rust_default="$(jq -er '.default_channel' <<<"${entry}")"
[[ ${#smoke_rust_channels[@]} -gt 0 ]]
for smoke_rust_channel in "${smoke_rust_channels[@]}"; do
[[ "$(rustup run "${smoke_rust_channel}" rustc --version)" == "rustc ${smoke_rust_channel}"* ]]
[[ "$(rustup run "${smoke_rust_channel}" cargo clippy --version)" == clippy* ]]
[[ "$(rustup run "${smoke_rust_channel}" rustfmt --version)" == rustfmt* ]]
done
[[ "$(rustc --version)" == "rustc ${smoke_rust_default}"* ]]
[[ "$(cargo --version)" == "cargo "* ]]
;;
uv)
[[ "$(uv --version)" == "uv ${expected_version}"* ]]
Expand Down
35 changes: 24 additions & 11 deletions internal/imagebuild/orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"strings"
"time"

"github.com/NDDev-OpenNetwork/github-actions/internal/imagemanifest"
"github.com/NDDev-OpenNetwork/github-actions/internal/imageplan"
)

Expand Down Expand Up @@ -296,6 +297,26 @@ type guestToolchain struct {
Version string `json:"version"`
Archive string `json:"archive"`
ArchiveSHA256 string `json:"archive_sha256"`
// Channels and DefaultChannel ride along for version managers. Dropping
// them here once produced a rustup with no toolchains: the manifest named
// the channels, every manifest gate passed, and the builder still died
// because this projection was the one place they were not carried.
Channels []string `json:"channels,omitempty"`
DefaultChannel string `json:"default_channel,omitempty"`
}

// guestToolchainFromPlan is the single projection from a manifest toolchain to
// the guest contract. Provision receives the pushed archive path; smoke
// receives none, because it verifies installed state rather than archives.
func guestToolchainFromPlan(toolchain imagemanifest.Toolchain, archivePath string) guestToolchain {
return guestToolchain{
Name: toolchain.Name,
Version: toolchain.Version,
Archive: archivePath,
ArchiveSHA256: toolchain.ArchiveSHA256,
Channels: toolchain.Channels,
DefaultChannel: toolchain.DefaultChannel,
}
}

func encodeToolchains(toolchains []guestToolchain) (string, error) {
Expand Down Expand Up @@ -573,12 +594,8 @@ exit 1`); err != nil {
if _, err = o.Runner.Run(ctx, o.incus(plan.Project, "file", "push", artifacts.Toolchains[toolchain.Name], destination, "--mode", "0600")...); err != nil {
return "", "", fmt.Errorf("push verified %s toolchain archive: %w", toolchain.Name, err)
}
toolchainRequests = append(toolchainRequests, guestToolchain{
Name: toolchain.Name,
Version: toolchain.Version,
Archive: "/var/tmp/" + toolchain.Archive,
ArchiveSHA256: toolchain.ArchiveSHA256,
})
toolchainRequests = append(toolchainRequests,
guestToolchainFromPlan(toolchain, "/var/tmp/"+toolchain.Archive))
}
encodedToolchains, err := encodeToolchains(toolchainRequests)
if err != nil {
Expand Down Expand Up @@ -800,11 +817,7 @@ func (o Orchestrator) smoke(ctx context.Context, plan imageplan.Plan, fingerprin
smoke, _ := scripts.ReadFile(smokeName)
smokeToolchains := make([]guestToolchain, 0, len(plan.Toolchains))
for _, toolchain := range plan.Toolchains {
smokeToolchains = append(smokeToolchains, guestToolchain{
Name: toolchain.Name,
Version: toolchain.Version,
ArchiveSHA256: toolchain.ArchiveSHA256,
})
smokeToolchains = append(smokeToolchains, guestToolchainFromPlan(toolchain, ""))
}
encodedToolchains, err := encodeToolchains(smokeToolchains)
if err != nil {
Expand Down
41 changes: 40 additions & 1 deletion internal/imagebuild/orchestrator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ func TestRecipeFingerprintIsDeterministic(t *testing.T) {
// alias is part of the recipe, so a manifest whose contents changed under an
// unchanged alias would otherwise ask the builder to produce different bytes
// for a name that is already promoted.
if first != "sha256:3d89d31c7c5a423238a6888b2b4c4d894f22fcb91e0d6ea76fd689d0c5a09b9b" {
if first != "sha256:6b92afb396a9d31a6772e489343006f2ce05c93a59a641701e2bf49ff8fe23fc" {
t.Fatalf("deployed standard recipe fingerprint drifted: %q", first)
}
smoke, err := SmokeFingerprint(plan)
Expand Down Expand Up @@ -689,3 +689,42 @@ func TestFailedBuildPreservesTheBuilderOnlyWhenAsked(t *testing.T) {
}
}
}

// TestGuestToolchainContractCarriesChannels guards the projection between the
// manifest and the guest scripts. The channels were once present in the
// manifest, validated by imagemanifest tests, and still absent inside the
// builder, because this projection dropped them; provision then failed with no
// toolchain installed. Both the provisioning and the smoke contract must carry
// them.
func TestGuestToolchainContractCarriesChannels(t *testing.T) {
t.Parallel()

pinned := imagemanifest.Toolchain{
Name: "rustup",
Version: "1.29.0",
Archive: "rustup-init",
ArchiveSHA256: strings.Repeat("a", 64),
Channels: []string{"1.98.0", "1.89"},
DefaultChannel: "1.98.0",
}

for name, projected := range map[string]guestToolchain{
"provision": guestToolchainFromPlan(pinned, "/var/tmp/"+pinned.Archive),
"smoke": guestToolchainFromPlan(pinned, ""),
} {
encoded, err := encodeToolchains([]guestToolchain{projected})
if err != nil {
t.Fatalf("%s: encode: %v", name, err)
}
decoded, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
t.Fatalf("%s: decode: %v", name, err)
}
contract := string(decoded)
for _, needle := range []string{`"channels":["1.98.0","1.89"]`, `"default_channel":"1.98.0"`} {
if !strings.Contains(contract, needle) {
t.Fatalf("%s contract %s lost %s", name, contract, needle)
}
}
}
}
3 changes: 1 addition & 2 deletions internal/imagemanifest/manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func TestRepositoryManifestIsValidAndPinned(t *testing.T) {
if !strings.HasPrefix(fingerprint, "sha256:") || len(fingerprint) != len("sha256:")+64 {
t.Fatalf("unexpected fingerprint %q", fingerprint)
}
if fingerprint != "sha256:02591ff082938633b0435312ef80f7a397021f80fe155fa62ece7cc6026e8e70" {
if fingerprint != "sha256:d7fdc01a61f2afc9991ef658511ea3cf73f73c251d0043c04c392f06b6397d1a" {
t.Fatalf("standard manifest fingerprint drifted: %q", fingerprint)
}
}
Expand Down Expand Up @@ -77,7 +77,6 @@ func assertBakedToolchains(t *testing.T, manifest Manifest) {
"node24": {"24.19.0", "14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647"},
"node25": {"25.9.0", "1d8db7d6e291d167e8c467ae4094be175e1a0b3969c7ae1f8955b9f7824f7b2e"},
"pnpm": {"11.22.0", "57a97e6f23a3faffc03153a4ef8c770a0552612b8640aebe39bfdd5754d0ebdc"},
"rust": {"1.97.1", "88f28fa9af20594179f85d6df67078dfd6fa93e2f6da5e1e9b0ac4997988ca4f"},
"rustup": {"1.29.0", "4acc9acc76d5079515b46346a485974457b5a79893cfb01112423c89aeb5aa10"},
"uv": {"0.11.30", "04bc7d180d6138bf6dc08387acf507a823f397a98fea55da36b0ccc7fbce3b68"},
"yarn": {"4.18.0", "606e7e2dfc8bcc24e1b3a70a1043288a271ad2cc71cf42248fadc25f5938a497"},
Expand Down
13 changes: 7 additions & 6 deletions internal/imagemanifest/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,13 @@ type BrowserSmoke struct {
// already on PATH, and actions/setup-go resolves a pre-seeded runner tool cache,
// so a complete set turns per-job toolchain installation into a no-op.
func BakedToolchains() []string {
// rustup is baked beside rust, not instead of it. A system-wide rustc
// satisfies a job that calls cargo directly; it is invisible to
// actions-rust-lang/setup-rust-toolchain, which resolves the channel in
// rust-toolchain.toml through rustup. Both are needed because the estate
// does both.
return []string{"bun", "gh", "go", "node22", "node24", "node25", "pnpm", "rust", "rustup", "uv", "yarn"}
// rustup owns the whole Rust surface. A standalone rust tarball was baked
// beside it once, and the rustup shims overwrote its /usr/local/bin
// binaries during provisioning, so the "system" rust the recipe promised
// was unreachable in the shipped image. The default channel serves jobs
// that call cargo directly, and actions-rust-lang/setup-rust-toolchain
// resolves rust-toolchain.toml channels through rustup either way.
return []string{"bun", "gh", "go", "node22", "node24", "node25", "pnpm", "rustup", "uv", "yarn"}
}

// OptionalToolchains may be pinned by an image that needs them and omitted by
Expand Down
2 changes: 1 addition & 1 deletion internal/imageplan/plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func TestBuildProducesDeterministicBoundedPlan(t *testing.T) {
}
// The baked set plus the optional Flutter SDK this manifest pins, still
// sorted: the plan's ordering is what makes the recipe fingerprint stable.
if !slices.Equal(names, []string{"bun", "flutter", "gh", "go", "node22", "node24", "node25", "pnpm", "rust", "rustup", "uv", "yarn"}) {
if !slices.Equal(names, []string{"bun", "flutter", "gh", "go", "node22", "node24", "node25", "pnpm", "rustup", "uv", "yarn"}) {
t.Fatalf("plan toolchains are not the canonical sorted set: %v", names)
}
if first.CompilerCache.Name != "sccache" || first.CompilerCache.Version != "v0.17.0" ||
Expand Down
2 changes: 1 addition & 1 deletion internal/repositorycontract/external_downloads_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func TestExternalDownloadFallbackMarkersRemainBounded(t *testing.T) {
"--retry 2", "verify_candidate", "nddev_tool_cache_event=",
},
"scripts/install-benchmark-toolchain.sh": {
"--retry 2", "toolchain SHA-256 mismatch", "rustc 1.97.1",
"--retry 2", "toolchain SHA-256 mismatch", "rustc 1.98.0",
},
"internal/garmproviderincus/provider/incus.go": {
"--retry 2", "validateRunnerTool",
Expand Down
Loading