# Default config: always loaded regardless of MISE_ENV. # Holds the Rust toolchain (the substrate -- every guest-language module build # shells out to `cargo run -p et-cli`), the Node static-workspace infra, the # universal repo-wide linters, and the cross-language orchestration tasks below. # # Per-language toolchains live in .mise/config..toml, selected via MISE_ENV (dart, dotnet, java, # python, zig). config.rust.toml currently holds only the Rust WASM module builds; the rest of the Rust # tooling still lives here, pending a later pass that moves it behind MISE_ENV=rust. Examples: # Install one language's toolchain: MISE_ENV=dart mise install # Run a check across everything: mise run check-all (or install-all) # Make a selection sticky: export MISE_ENV=dart # Pinned at the newest mise taiki-e/install-action can fetch; the floor also covers auto_env in .miserc.toml. # 2026.9.1 exists but is absent from install-action's manifest, so requesting it drops CI into a cargo-binstall # source build. Coming off 2026.8.5 took three workarounds, each carried at its own call site: osv-scanner-npm # installs node ahead of any npm: tool, install-mise-tools stops restoring the GHCR store, and every task # declares a `shell` that routes its body through a wrapper script. A warm checkout needs `mise install` re-run # once -- a tool this mise cannot resolve still reports as installed, so nothing refetches it on its own. min_version = "2026.10.15" [settings] # Disable mise's two auto-install paths so tasks fail fast on a missing tool. # exec_auto_install covers `mise exec`, task.run_auto_install covers `mise run`. cargo.binstall = true # Disable mise backends we never use. # `asdf` is kept enabled because several of our bare-name tools (conftest, dprint, hadolint, pipx, rclone, # ruff, ...) list `asdf:` as the registry fallback after `aqua:`. `vfox` was the original target -- killed # the persistent `DEBUG [vfox] Getting metadata for yarn` noise that mise's `load_tools` produces by # proactively iterating every registry tool with `idiomatic_files` declared. The rest (forgejo, gem, gitlab, # pkgx, s3, spm, ubi) are belt-and-braces. disable_backends = ["forgejo", "gem", "gitlab", "pkgx", "s3", "spm", "ubi", "vfox"] exec_auto_install = false experimental = true http_retries = 10 http_timeout = "2m" # Explicit empty list (matches mise's default) keeps version-file detection off. # This disables idiomatic-version-file detection for every tool. We declare versions in `.mise/config*.toml`; # no need to parse `.tool-versions`/`.node-version`/etc. on top of that. idiomatic_version_file_enable_tools = [] # Resolve a bare `java = "X.Y.Z"` to the OpenJDK build that java_home in config.java.toml is written against. # mise's own default vendor is Temurin, and leaving it implicit writes a second `java` entry per vendor into the # lockfiles. java.shorthand_vendor = "openjdk" # Lock every tool resolution to the committed .mise/mise.lock. # Reading the lockfile is mise's default; `lockfile = true` additionally keeps it updated on installs and # upgrades. The lock pins each `latest` to a concrete version + checksum, so composing a task/exec env # resolves entirely offline -- without it, every `mise x`/`mise run` on a cold-cache CI runner re-resolves # every `latest` tool against api.github.com (one /releases list per tool), burning the shared rate-limit # bucket for tools that are not even being installed. Refresh with `mise lock` (all five platforms below); # `mise lock --bump` re-resolves the `latest` selectors when a deliberate tool refresh is wanted. lockfile = true lockfile_platforms = ["linux-arm64", "linux-x64", "macos-arm64", "macos-x64", "windows-x64"] task.run_auto_install = false # Hard-verify tool signatures via gpg on every platform. # Propagates to node.gpg_verify and swift.gpg_verify per mise/src/config/settings.rs. mise's node plugin # runs `gpg --import` + `gpg --verify` against Node's signed SHASUMS; with this enabled that path hard-fails # instead of soft-skipping (`warn!("gpg not found, skipping verification")` in mise/src/plugins/core/node.rs) # when gpg is absent. The full gnupg suite (gpg + agent + dirmngr) is installed up-front in _setup_all so # any gpg-verified install finds it. gpg_verify = true [tools] # aqua/github ship no Windows build for action-validator, so scope the aqua entry off Windows. # config.windows.toml installs cargo:action-validator (cargo-quickinstall msvc prebuilt) there instead. action-validator = { version = "latest", os = ["linux", "macos"] } # jaq: a Rust jq-compatible alternative implementation with prebuilt binaries for every platform. "aqua:01mf02/jaq" = "latest" "aqua:EmbarkStudios/cargo-deny" = "latest" "aqua:StyraInc/regal" = "latest" # The secret scanner Codacy and DeepSource both run against a pushed branch. "aqua:gitleaks/gitleaks" = "latest" "aqua:koalaman/shellcheck" = "latest" # shfmt (the `sh` repo's formatter binary) formats the multi-line bash bodies of mise tasks. "aqua:mvdan/sh" = "latest" "aqua:rhysd/actionlint" = "latest" "aqua:rustwasm/wasm-pack" = "latest" # Vector ships no darwin/amd64 prebuilt, so second-tier macos/x64 builds the same pin from source below. # The aqua error was: `unsupported env: darwin/amd64 (supported: ["linux", "darwin/arm64", "windows/amd64"])`. "aqua:vectordotdev/vector" = { version = "0.60.0", os = ["linux", "macos/arm64", "windows"] } # Validates the generated Kubernetes manifests against the API schemas, which the drift check cannot do. "aqua:yannh/kubeconform" = "latest" # k3d runs a real k3s cluster in Docker, paired with kubectl to drive it. # That is what lets the generated manifests be applied and asserted against rather than only schema-checked. "aqua:k3d-io/k3d" = "latest" "aqua:kubernetes/kubernetes/kubectl" = "latest" # Reproduces locally the Kubernetes hardening findings Codacy reports on the generated manifests. # Codacy's three -- privilege escalation, writable root filesystem, absent security context -- are trivy's # KSV-0001, KSV-0014 and KSV-0118, and trivy additionally covers seccomp, capabilities, resource bounds and # UID/GID, so the local check is a superset of the pushed one rather than a mirror of it. "aqua:aquasecurity/trivy" = "latest" ast-grep = "latest" cargo-binstall = "latest" "cargo:cargo-expand" = { version = "latest", os = ["linux", "macos"] } "cargo:open" = "latest" "cargo:wasm-opt" = { version = "latest", os = ["linux", "macos"] } cmake = "latest" "conda:openssl" = "3" conftest = "latest" # Compose CLI for the generated scenario stacks under verification/. # The aqua backend rather than the registry default (`github:docker/compose`): the github backend records SLSA # provenance in the lockfile, and a mise older than the one that wrote it refuses the entry outright with # "Lockfile requires slsa provenance for github:docker/compose@ but verification was not performed". # aqua records `provenance = "github-attestations"` instead, which is what every other pinned tool here uses. # This is the compose implementation only -- a docker daemon still has to come from the host. "aqua:docker/compose" = "latest" dprint = "latest" editorconfig-checker = "latest" "github:nextest-rs/nextest" = { version = "latest", version_prefix = "cargo-nextest-" } taplo = "latest" watchexec = "latest" # ripgrep (rg): the Rust grep that tasks use in place of the host's grep. ripgrep = "latest" # Apple Silicon resource monitor (CPU/GPU/ANE/power) for the o2-macmon task; macOS-only, so os-scoped. "github:vladkens/macmon" = { version = "0.7.2", os = ["macos"] } # NVIDIA GPU Prometheus exporter (nvidia-smi) for the o2-nvidia task; Linux-only, so os-scoped. # Windows GPU coverage is windows_exporter's gpu collector; Apple Silicon is macmon. "github:utkuozdemir/nvidia_gpu_exporter" = { version = "1.10.0", os = ["linux"] } # Windows host + GPU Prometheus exporter for the o2-winmetrics task; Windows-only, so os-scoped. "github:prometheus-community/windows_exporter" = { version = "0.31.7", os = ["windows"] } # OpenTelemetry Collector (contrib distro) binary for the native, non-Docker o2-winmetrics-native / o2-winperf tasks. # The releases repo ships several distros in one tag; matching=contrib selects the contrib archive -- the only one # carrying the prometheus/hostmetrics/windowsperfcounters receivers + basicauth extension those configs use. "github:open-telemetry/opentelemetry-collector-releases[matching=contrib]" = { version = "0.163.1" } # NATS server with JetStream, the broker several hub instances share as their backplane. # A hub routes through it when `WS_BACKPLANE` names it, and the horizontal-scaling tests start one of their own. # Upstream ships prebuilt archives for all five platforms. "github:nats-io/nats-server" = { version = "2.15.1" } # uutils findutils on aarch64-linux, built from source rather than taken from the github: entry further down. # Its 0.8.0 release had no asset for that platform, so it builds via cargo: on the same pin as the github: entry, # so every platform runs the same version. "cargo:findutils" = { version = "0.10.0", os = ["linux/arm64"] } # agent-lint: the linter behind agent-lint-check, over this repository's Claude configuration. # Scoped to the three targets upstream publishes a binary for -- macOS arm64 and Linux x86_64/aarch64, all # three musl or native -- because there is no Windows or macOS x86_64 asset in the release to install. The # task skips where the tool cannot be installed rather than failing on a missing binary. "github:zhupanov/agent-lint" = { version = "4.0.3", os = ["linux", "macos/arm64"] } # goawk: the awk that tasks invoke as `goawk` in place of the host's awk. "github:benhoyt/goawk" = "latest" "github:caldempsey/parfit" = "latest" "github:grok-rs/waitup" = "latest" # jscpd: the cross-language copy/paste detector behind jscpd-check. # Upstream ships prebuilt tarballs for every tier-1 and tier-2 triple, so the github (ubi) backend covers all five # platforms with no os-scoping. Clone fingerprints are tokenizer-derived, so a new jscpd release can rewrite them # and redden config/jscpd-baseline.json; the lockfile holds the version until a refresh moves it. When it moves, # regenerate the baseline and set `declared_clones` in the jscpd conftest policy to its new count in the same change. "github:kucherenko/jscpd" = "latest" # ONNX Runtime prebuilt -- ort-sys 2.0.0-rc.10 binds to ORT 1.22.x. "cargo:ryl" = { version = "latest", os = ["macos/x64"] } "github:microsoft/onnxruntime" = "1.22.0" "github:owenlamont/ryl" = { version = "latest", os = ["linux", "macos/arm64", "windows"] } "github:wasm-bindgen/wasm-bindgen" = "0.2.129" # go-containerregistry's crane fetches the per-platform ghcr.io mise-tools stores. # Its `crane export` rootfs-flatten is what the pull-mise-tools task and CI's install-mise-tools restore # step extract tool trees with. crane = "latest" # Sole consumer right now is the `go:` backend that source-builds jqfmt. # (noperator/jqfmt ships no release assets); add Go users here as they arrive. go = "latest" # Version-pinned rather than `latest` so installing it needs no Go-proxy version lookup. # Resolving `latest` queries the proxy for the tag list, and that request is bound by # fetch_remote_versions_timeout (20s), which the Windows lanes' third-party egress overran: # mise ERROR Failed to install go:github.com/noperator/jqfmt/cmd/jqfmt@latest: timed out after 20.00s # on commit 12bfe419 at https://github.com/edge-toolkit/core/actions/runs/30797697389/job/91634929057. # Pinning removes the lookup entirely instead of widening the timeout. upstream has exactly one release, so # this is also what `latest` already resolved to -- revisit only when noperator/jqfmt tags a second version. "go:github.com/noperator/jqfmt/cmd/jqfmt" = "0.1.0" hadolint = "latest" ls-lint = "latest" # dekit, mprocs under its new name: `dekit mprocs` runs the repo-root mprocs.yaml. # The github backend rather than the registry's `aqua:pvolok/dekit`, whose manifest still names the pre-rename # assets, so every platform fails with # aqua:pvolok/dekit@0.10.0: no asset found: mprocs-0.10.0-darwin-aarch64.tar.gz, mprocs-0.10.0-darwin-universal.tar.gz # on mise 2026.9.18. Return to the registry name once its aqua entry knows the `dekit-` assets. "github:pvolok/dekit" = "latest" node = "22" osv-scanner = "latest" # Aqua has no pipx build for windows/amd64. # On Windows the workflow runs `python -m pip install pipx` separately so the # `pipx:*` entries below still resolve through whatever pipx is already on PATH. pipx = { version = "latest", os = ["linux", "macos"] } # Pure-Python fixture for the et-ws-pyo3-runner `cowsay` test. # Proves a mise-installed python package lands on the embedded interpreter's # sys.path (see `edge_toolkit::config::mise_python_site_packages`). gh = "latest" "pipx:cowsay" = "latest" "pipx:semgrep" = "latest" # Aqua has no pnpm darwin/amd64 prebuilt. # The `npm:pnpm` fallback for macos/x64 lives in the `js` env (config.js.toml), held to the same major. # # Held at pnpm 11 because pnpm 12 cannot install on Nano Server. pnpm 12 resolves its store operation lock directory # only through `SHGetKnownFolderPath(FOLDERID_LocalAppData)`, with no setting or variable to override it, and Nano's # shell32 lacks that entry point, so every install there aborts before resolving a single package: # # Error: ERR_PNPM_STORE_DIR_OPEN_OPERATION_LOCK # x installing dependencies # `-> Failed to open the store operation lock at "pnpm-store-operation-locks": # failed to resolve the local application data directory: HRESULT 0x80004005 # # Seen in `prefetch:node` on the `build (windows-2022, nanoserver)` lane at commit # https://github.com/edge-toolkit/core/commit/4daebb043af06dfd3b0f81ef301f5b0ead6d50b0, # https://github.com/edge-toolkit/core/actions/runs/36950377535/job/110661783062. pnpm 11 runs on node and never # calls the API. Held everywhere rather than on Nano alone so every lane installs the workspace with one pnpm. pnpm = { version = "11", os = ["linux", "macos/arm64", "windows"] } protoc = "latest" # Full version triple, not a minor pin. # mise installs python under a per-tool dir named after the request and only symlinks the `3.x` minor alias # -- and that symlink isn't created on the Windows runner, so the py3_* interpreter paths must resolve to the # exact `3.x.y` dir. Bump in lockstep with py3_unix/py3_win/pylib. python = "3.13.16" rclone = "latest" typos = "latest" uv = "0.13.3" wasm-tools = "latest" yq = "latest" zizmor = "latest" # Rust toolchains -- Linux/macOS only. # Windows installs the gnullvm-host toolchains directly via config.windows.toml's own `[[tools.rust]]` # entries, which skips the install-msvc-then-flip-to-gnullvm dance the old preinstall did. Both entries use # `profile = "minimal"` to skip the ~1 GB rust-docs HTML tree per toolchain (the Nano docker build otherwise # filled its disk during the second toolchain install -- crashing inside an HCS re-exec on the rust-docs HTML # write under share/doc/rust/html/ with "There is not enough space on the disk"). Array-of-tables form # rather than inline so each entry's fields sit on their own lines alongside `os` + the component / target # lists; must come at the end of [tools] (TOML can't re-open the table after the [[ ... ]] array-of-tables # headers below). [[tools.rust]] components = "clippy,rust-analyzer" os = ["linux", "macos"] profile = "minimal" targets = "wasm32-unknown-unknown,wasm32-wasip2" # Use `stable`, not `latest`, so every lane resolves through the same rustup channel. # mise's `latest` picks the max of rust-lang/rust's GitHub Releases, which are published by separate # automation that lags the `stable` rustup channel the Windows [[tools.rust]] entries resolve through. That # skew once shipped a newer clippy (with a new restriction lint) to Windows than Linux/macOS, breaking only # that lane. version = "stable" [[tools.rust]] # llvm-tools ships the version-matched llc + llvm-cov + llvm-profdata the wasm-coverage pipeline drives. # The instrumented guests build on nightly for -Zno-profiler-runtime, so the matching LLVM tools are nightly's. components = "rust-src,rustfmt,llvm-tools" os = ["linux", "macos"] profile = "minimal" targets = "wasm32-unknown-unknown,wasm32-wasip2" version = "{{ vars.rust_nightly }}" # The macos/x64 half of vector (see the aqua:vectordotdev/vector note above). # A git `tag:` spec rather than the crates.io crate so this platform builds the exact source the other # platforms' 0.60.0 prebuilts are cut from -- bump it with the aqua entry. A source build is acceptable here # because macos/x64 is second-tier. `crate` names the package to build, and is required rather than tidy: a # git install searches the whole repo, so cargo refuses with `error: multiple packages with binaries found` # on any workspace carrying more than one binary rather than choosing between them. # # `--cap-lints=warn` is what makes the source build survive a toolchain newer than the one upstream cut the # release with. vector-config carries `#![deny(warnings)]`, so a future-compat warning is fatal there: # # error: a method with this name may be added to the standard library in the future # --> lib/vector-config/src/schema/visitors/merge.rs:265:17 # = note: `#[deny(unstable_name_collisions)]` implied by `#[deny(warnings)]` # # Capping every lint to `warn` for this one install leaves the deny in place for our own crates. It has to # ride on plain RUSTFLAGS rather than joining config.macos.toml's CARGO_TARGET_X86_64_APPLE_DARWIN_RUSTFLAGS, # because install_env values are passed through literally -- no `{{ vars.x }}`, no `$HOME` -- so the rpath # flag that value is built from cannot be restated here. Plain RUSTFLAGS wins over the target-specific form, # so this install alone links without the conda OpenSSL rpath. [tools."cargo:vectordotdev/vector"] crate = "vector" install_env = { RUSTFLAGS = "--cap-lints=warn" } os = ["macos/x64"] version = "tag:v0.60.0" # The macos/x64 half of rustfs, the one platform upstream ships no prebuilt for. # A git spec rather than a bare `cargo:rustfs`: the crates.io crate is a stale 0.0.2 placeholder while real # releases are 1.0.0-beta.x on GitHub only, so installing the crate would give this platform different software # than every other one. `tag:` builds the exact source the prebuilt is cut from, keeping them aligned -- bump it # with the `http:rustfs` entry. A source build is acceptable here because macos/x64 is second-tier. [tools."cargo:rustfs/rustfs"] crate = "rustfs" os = ["macos/x64"] version = "tag:1.0.0-beta.12" # uutils coreutils: the Rust multicall binary, invoked as `coreutils ` in tasks. # Pinned to 0.6.0 because uutils 0.7.0+ ships the Windows release as per-utility .exes (see # uutils/coreutils#11268). 0.12.0 restores a multicall coreutils.exe but still ships ~90 per-utility .exes beside # it, all of which would land on PATH -- `link.exe` among them, shadowing MSVC's linker. 0.6.0 keeps the layout # uniform across Linux/macOS/Windows, so tasks can keep using `coreutils ` everywhere. Bump cautiously: # confirm the Windows zip layout hasn't reverted before raising the pin. # # On Linux it takes the musl build. The gnu one needs GLIBC_2.39, so on the ubuntu:22.04 (2.35) and # debian:bookworm (2.36) image bases it cannot start, and every `coreutils` call there fails -- first the preinstall # libicu probe, as `preinstall: missing required build prerequisite: the versioned libicu runtime` on the # `build (debian:bookworm)` and `build (ubuntu:22.04)` lanes of # https://github.com/edge-toolkit/core/actions/runs/36933521606. mise picks the gnu build for a glibc platform # unless told otherwise, and the aqua registry entry offers no way to tell it, hence the github backend. # # The pattern is one template for every platform rather than a per-platform `asset_pattern`: mise keys a lock # entry on its options, so per-platform patterns become separate entries that an install on any other platform # prunes, and `mise-lock-check` then reports the lockfile as drifted. [tools."github:uutils/coreutils"] asset_pattern = "{{ vars.musl_pattern }}" version = "0.6.0" # uutils findutils (find + xargs), pinned rather than "latest" because 0.9.0 shipped no binaries. # Separate binaries, not a multicall, so the mise shims shadow the host's find / xargs in tasks. Its musl build on # Linux for the same reason as coreutils above: from 0.10.0 the gnu one needs GLIBC_2.39, and `find` failing on the # older image bases took down dotnet-install, which calls it. [tools."github:uutils/findutils"] asset_pattern = "{{ vars.musl_pattern }}" os = ["linux/x64", "macos", "windows"] version = "0.10.0" # lychee takes its musl build on Linux for the same reason as coreutils above: the gnu one needs GLIBC_2.39. [tools."github:lycheeverse/lychee"] asset_pattern = "{{ vars.musl_pattern }}" version = "latest" # The feature-powerset driver behind cargo-hack-check; musl on Linux so the older-glibc image bases can run it. [tools."github:taiki-e/cargo-hack"] asset_pattern = "{{ vars.musl_pattern }}" version = "latest" # The unused-dependency finder behind cargo-shear-check; musl on Linux for the same reason as cargo-hack. [tools."github:Boshen/cargo-shear"] asset_pattern = "{{ vars.musl_pattern }}" version = "latest" # Native rustpython binary built from upstream RustPython main. # Built with `freeze-stdlib` (so the binary embeds the Python stdlib and doesn't need a RUSTPYTHONPATH dance) # + the upstream pip audit-hook fix (RustPython#7956). Per-platform url + checksum, os-scoped to only those # platforms whose triple's tarball is already in the rp-v release; consumers on every other platform have # no `rustpython` on PATH (no CI/build path depends on it today). To add a platform: dispatch # .github/workflows/upstream-cache.yaml -- it builds + uploads the tarball, then auto-edits this block to add # the platforms. entry on the maintainer's next local run. `bin = "rustpython"` because the tool short # name (`et-rp`) doesn't match the binary name. Short SHA inlined in each platform's url because mise's tera # context doesn't expose top-level `version` into platforms..url; the publish task updates both # `version` and the platform url together. [tools."http:et-rp"] bin = "rustpython" os = ["linux", "macos", "windows"] version = "08a1d6f" [tools."http:et-rp".platforms.linux-arm64] checksum = "sha256:c26df56bf3c28cb9e134abe95dd0fcf927f05487069a492a6f54ffa1c0f3dc12" url = "https://github.com/edge-toolkit/core/releases/download/rp-v1/rustpython-08a1d6f-aarch64-unknown-linux-gnu.tar.gz" [tools."http:et-rp".platforms.linux-x64] checksum = "sha256:75b74fd71ba338d542d9c8a512aa6e9475695f31d5e9e3d159b44d1445b9ba21" url = "https://github.com/edge-toolkit/core/releases/download/rp-v1/rustpython-08a1d6f-x86_64-unknown-linux-gnu.tar.gz" [tools."http:et-rp".platforms.macos-arm64] checksum = "sha256:5cdb5ed45e925ac01d12feeae453be75112d5e4d4725d44c869d63e502fcf393" url = "https://github.com/edge-toolkit/core/releases/download/rp-v1/rustpython-08a1d6f-aarch64-apple-darwin.tar.gz" [tools."http:et-rp".platforms.macos-x64] checksum = "sha256:2a161b533e6eab988b7a16d400b05216c37be371a142e761b917044c14cacad2" url = "https://github.com/edge-toolkit/core/releases/download/rp-v1/rustpython-08a1d6f-x86_64-apple-darwin.tar.gz" [tools."http:et-rp".platforms.windows-x64] checksum = "sha256:30b96e2d50ec15f7da5563b5bbce4170ee23f5fbf8565a8973889d8f25c6279a" url = "https://github.com/edge-toolkit/core/releases/download/rp-v1/rustpython-08a1d6f-x86_64-pc-windows-msvc.tar.gz" # augtool (from augeas) is used by the maint publish-* tasks. # Those config.maint.toml tasks edit .mise/config.toml + config/upstream-cache/data.toml in place via # augeas's Toml lens (preserves comments and structure, unlike pure stringly TOML rewrites). Source-built # from the jayvdb/augeas `win` branch on every platform by the augeas job in upstream-cache.yaml -- one # branch, one set of portability patches, identical binaries. URLs inline the asset filename rather than # templating via {{ vars.* }} because mise's http backend doesn't run tera against per-platform url fields # (works for top-level tools..url, breaks here). The matching `augeas__asset` vars stay in # [vars] below so checksums.rego's cross-reference policy (.mise vars <-> data.toml [asset.*]) still passes. # Noarch rustpython_wasm tarball (the prebuilt wasm-pack output that build-ws-pywasm1-module consumes). # Single platform-agnostic URL -- mise extracts to `installs/http-rp-wasm//`, then the module # build task copies what it needs into pkg/. Built + uploaded by `publish-rp-wasm-to-release` in # config.maint.toml; matching `[asset."rustpython-wasm-.tar.gz"]` row sits in config/upstream-cache/data.toml. # Version segment is the upstream RustPython commit's 7-char short SHA -- bump in lockstep with the asset # filename in data.toml and the url here. [tools."http:rp-wasm"] checksum = "sha256:7b81d53f5b3a63c60fc2bc1717e87466026d796ec522b925f009532c7b0291f3" url = "https://github.com/edge-toolkit/core/releases/download/{{ vars.a_rp_release }}/{{ vars.a_rp_wasm_asset }}" version = "08a1d6f" # OpenObserve open-source binary for the o2-native task. # The same build the o2 docker image runs (its Dockerfile is just `CMD ["/openobserve"]`), pinned to the same # version. Direct upstream, per-version immutable URLs, so no checksum. Every supported platform has a prebuilt: # the Windows asset is a `.zip` (holding `openobserve.exe`) rather than the `.tar.gz` the others use -- probing the # `.tar.gz` name is what previously read as "no Windows build". windows-arm64 has no prebuilt but is not a target. # # The linux URLs take upstream's `-musl` static builds, not the default gnu ones: the gnu binary imports # GLIBC_2.39 symbols, so on every docker-linux matrix distro with glibc <= 2.38 (ubuntu:22.04, # debian:bookworm, amazonlinux:2023, opensuse/leap:15.6, azurelinux 3.0) it dies at exec and the # ws-wasi-runner o2_ingest_search test times out with the literal panic # # openobserve never bound its HTTP port :36657 # # observed on commit https://github.com/edge-toolkit/core/commit/1f544590b473887015754f67c3af843f970d8dbc at # https://github.com/edge-toolkit/core/actions/runs/31559765133/job/94267330524 (and the three sibling # old-glibc lanes of that run), while every glibc >= 2.39 lane passed. The musl build has no glibc floor. [tools."http:openobserve"] bin = "openobserve" version = "1.1.1" [tools."http:openobserve".platforms.linux-arm64] url = "https://downloads.openobserve.ai/releases/openobserve/v1.1.1/openobserve-v1.1.1-linux-arm64-musl.tar.gz" [tools."http:openobserve".platforms.linux-x64] url = "https://downloads.openobserve.ai/releases/openobserve/v1.1.1/openobserve-v1.1.1-linux-amd64-musl.tar.gz" [tools."http:openobserve".platforms.macos-arm64] url = "https://downloads.openobserve.ai/releases/openobserve/v1.1.1/openobserve-v1.1.1-darwin-arm64.tar.gz" [tools."http:openobserve".platforms.macos-x64] url = "https://downloads.openobserve.ai/releases/openobserve/v1.1.1/openobserve-v1.1.1-darwin-amd64.tar.gz" [tools."http:openobserve".platforms.windows-x64] url = "https://downloads.openobserve.ai/releases/openobserve/v1.1.1/openobserve-v1.1.1-windows-amd64.zip" # rustfs is the S3-compatible server the et-storage-service S3-backend test runs against. # Version-pinned rather than `latest` because upstream has published only prereleases so far, which mise's # version list filters out -- `latest` resolves to nothing. This entry names an upstream asset per platform; # macos/x64 is the one platform with no prebuilt at all and builds from source via `cargo:rustfs/rustfs` # above. Bump both in lockstep. # # Naming a URL directly is what keeps this installable. The aqua/github entry this replaced resolved the same # release through GitHub's API, which meant mise also read the release's `rustfs-.provenance.json`, # recorded `provenance.slsa` in mise.lock, and then refused to install because that file is a bare unsigned # in-toto Statement (no DSSE envelope, no sigstore material) that can never verify: # # mise ERROR Failed to install github:rustfs/rustfs@1.0.0-beta.12: Lockfile requires slsa provenance for # github:rustfs/rustfs@1.0.0-beta.12 but verification was not performed. This may indicate a downgrade # attack. Enable the corresponding verification setting or update the lockfile. # # observed on macos-arm64 and windows-x64 with mise 2026.8.5 at commit # https://github.com/edge-toolkit/core/commit/20b91714e47f52e7fcd898fc3df3dbf35bb53242. The http backend # performs no provenance lookup, so nothing unverifiable lands in the lockfile. It also records no # cross-platform checksums the way the github backend did: `mise lock` only hashes the asset for the platform # it runs on, so each platform's checksum appears the first time the lockfile is refreshed there. # # Linux takes the static musl assets rather than the gnu ones aqua used to select, which import GLIBC_2.39 # symbols -- on every docker-linux matrix distro with glibc <= 2.38 the gnu build dies at exec and the # et-storage-service s3_backend test times out with the literal panic # # rustfs did not start listening on port 33649 # # observed on commit https://github.com/edge-toolkit/core/commit/1f544590b473887015754f67c3af843f970d8dbc at # https://github.com/edge-toolkit/core/actions/runs/31559765133/job/94267330524 (and the three sibling # old-glibc lanes of that run), while every glibc >= 2.39 lane passed. The musl build has no glibc floor; # macOS and Windows have only the one build each. Direct upstream, per-version immutable URLs, so no checksum. [tools."http:rustfs"] bin = "rustfs" os = ["linux", "macos/arm64", "windows"] version = "1.0.0-beta.12" [tools."http:rustfs".platforms.linux-arm64] url = "https://github.com/rustfs/rustfs/releases/download/{{version}}/rustfs-linux-aarch64-musl-v{{version}}.zip" [tools."http:rustfs".platforms.linux-x64] url = "https://github.com/rustfs/rustfs/releases/download/{{version}}/rustfs-linux-x86_64-musl-v{{version}}.zip" [tools."http:rustfs".platforms.macos-arm64] url = "https://github.com/rustfs/rustfs/releases/download/{{version}}/rustfs-macos-aarch64-v{{version}}.zip" [tools."http:rustfs".platforms.windows-x64] url = "https://github.com/rustfs/rustfs/releases/download/{{version}}/rustfs-windows-x86_64-v{{version}}.zip" # chromedriver from Google's Chrome for Testing CDN, for the headless-browser wasm-bindgen tests. # The `os` list uses mise's os/arch compound syntax (mise >= 2026.4, jdx/mise#9088) to skip linux-arm64: Chrome # for Testing publishes no linux-arm64 chromedriver, and a bare `linux` entry would hard-fail `mise install` on # the linux-arm64 lanes ("No URL for platform linux-arm64"). `linux/x64` matches only that arch; bare `macos` # matches both. Immutable per-version URLs mean no checksum -- the same direct-upstream posture as # http:openobserve. mise auto-strips the single `chromedriver-/` archive dir, leaving the `chromedriver` # binary at the install root. Consumers: test-ws-wasm-agent-chrome / ws-e2e-chrome and the wasm-agent-cov coverage # task (which in CI drives Chrome via the runner's matched CHROMEWEBDRIVER instead). # The version is the current Stable release from Chrome for Testing's channel feed rather than a fixed pin. A # chromedriver more than one major away from the browser refuses the session, and a desktop Chrome auto-updates along # that same Stable channel, so a pin went stale on its own -- 152.0.7977.75 against Chrome 154 failed with # session not created: This version of ChromeDriver only supports Chrome version 152 # locally, on commit https://github.com/edge-toolkit/core/commit/e669c188d458a4d1b931fb7268f1131603f4d7b6. [tools."http:chromedriver"] bin = "chromedriver" os = ["linux/x64", "macos"] version = "latest" version_expr = "[fromJSON(body).channels.Stable.version]" version_list_url = "https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions.json" [tools."http:chromedriver".platforms.linux-x64] url = "https://storage.googleapis.com/chrome-for-testing-public/{{version}}/linux64/chromedriver-linux64.zip" [tools."http:chromedriver".platforms.macos-arm64] url = "https://storage.googleapis.com/chrome-for-testing-public/{{version}}/mac-arm64/chromedriver-mac-arm64.zip" [tools."http:chromedriver".platforms.macos-x64] url = "https://storage.googleapis.com/chrome-for-testing-public/{{version}}/mac-x64/chromedriver-mac-x64.zip" [tools."http:augeas"] bin = "augtool" version = "1.14.1" [tools."http:augeas".platforms.linux-arm64] checksum = "sha256:3303e8022294b04fe4ed567b0d88c10c1c7d5b51e443485251a6d4a606b1433a" url = "https://github.com/edge-toolkit/core/releases/download/augeas-v1/1.14.1-aarch64-unknown-linux-gnu.tar.xz" [tools."http:augeas".platforms.linux-x64] checksum = "sha256:76eb80c33355abe9f6ea01ac90a47a70d5917d940a0a843aed9d0b61feed7714" url = "https://github.com/edge-toolkit/core/releases/download/augeas-v1/1.14.1-x86_64-unknown-linux-gnu.tar.xz" [tools."http:augeas".platforms.macos-arm64] checksum = "sha256:10fc4aff94a5c6016c51b4dbbc224277a340a083c315ca0a05e9f5ef9a869403" url = "https://github.com/edge-toolkit/core/releases/download/augeas-v1/1.14.1-aarch64-apple-darwin.tar.xz" [tools."http:augeas".platforms.macos-x64] checksum = "sha256:28b26d680158f9480cf5e01cd7d47e57b7b05a7ac8646c76aa9c58537ea80422" url = "https://github.com/edge-toolkit/core/releases/download/augeas-v1/1.14.1-x86_64-apple-darwin.tar.xz" [tools."http:augeas".platforms.windows-x64] checksum = "sha256:6d97229c53cc0af09635bfd85b15169704ab9c07d5e23090252ac80746260036" url = "https://github.com/edge-toolkit/core/releases/download/augeas-v1/1.14.1-x86_64-pc-windows-mingw.tar.gz" [vars] # Rust nightly channel, pinned to a single version used for every nightly toolchain in the repo. # One value drives the coverage build + its llvm-tools, cargo-fmt, cargo-doc-check, the et-rest-client fmt, and # the Windows/mingw/msvc host toolchains, so no lane silently floats to a different nightly. # # The version is chosen by its LLVM, which has to match the `minicov` in [workspace.dependencies]: minicov # writes the guests' profraw and this toolchain's llvm-tools reads it, so a mismatch traps the guest with # `wasm trap: out of bounds memory access` in `coverage::dump`. `nightly-2026-08-05` (rustc 1ed2df61a) carries # LLVM 22.1.8, which is the profraw-10 format minicov 0.3.8 emits. Bump this and minicov together, never one # alone, and check `rustc + --version --verbose` reports an LLVM the minicov release names. rust_nightly = "nightly-2026-08-05" # The rustup host triple for this machine's arch, used as both the toolchain spec and CARGO_BUILD_TARGET. # x64 keeps gnullvm: llvm-mingw supplies a full compiler + CRT from mise, so nothing depends on a system MSVC # install, which is what lets the Nano Server image build at all. arm64 has no such option -- llvm-mingw # publishes ucrt-x86_64 only, and every other native prerequisite the gnullvm lane leans on (conda:m2-make, # conda:m2-gnupg, conda:libclang) is x86_64-only in its channel too. The windows-11-arm runner does ship # Visual Studio with the arm64 MSVC toolset, so msvc is the one triple that can build there today; it is also # rustup's own host default on that arch, so this asks for exactly the toolchain rustup would pick unaided. # # Defined here rather than beside the Windows tools that read it, and that placement is load-bearing. # `lockfile_platforms` makes any lockfile-writing command resolve the `windows-x64` tools from every host, so # `config.windows.toml`'s `[tools]` get rendered on macOS and Linux -- where that file is not an active config # and its own `[vars]` are therefore absent from the context. Declaring it there failed every `mise upgrade` # that had work to do, with `Field win_rust_host is not defined`; this file is active everywhere. win_rust_host = "{% if arch() == 'arm64' %}aarch64-pc-windows-msvc{% else %}x86_64-pc-windows-gnullvm{% endif %}" # The github-backend `asset_pattern` that takes a Rust-triple release's musl build on Linux. # `raw` keeps the config's own render from evaluating it, so the backend renders it per target platform and the # lock records the one template rather than a host-specific string. The pattern is anchored and the shortest match # wins, which is what the trailing `*` leans on to take `.tar.gz` / `.zip` over their `.sha256` sidecars. musl_pattern = '{% raw %}*-{{ x86_64_arch }}-*{{ os(linux="musl.t", macos="darwin.t", windows="msvc.z") }}*{% endraw %}' # Tier-`a_` prefix marks leaf values referenced by later vars. # a_* vars are referenced by later vars (face1_src), and mise renders [vars] in alphabetical order; `a_` sorts # before every non-prefixed var name so the reference resolves. Shared GitHub release tag for the HF-asset # mirror; referenced by face1_src below + by config.maint.toml's publish-*-to-hf-cache task body. Bump # (`hf-cache-v2`, ...) when an upstream asset changes -- updating this single var rolls both sides. a_hf_cache_tag = "hf-cache-v1" # Shared GitHub release tag for both the rustpython_wasm tarball and the natives. # Covers the wasm-pack rustpython_wasm tarball and the per-platform native rustpython binaries (both built # from the same upstream RustPython commit, stored together). Referenced by rp_* vars below + by # config.maint.toml's publish-rp-* task bodies. a_rp_release = "rp-v1" # Home directory, used to compose the Unix-style mise install paths in the vars below. # On Unix, mise's tera context always has env.HOME. On Windows it does not (only USERPROFILE is guaranteed), and # a bare `env.HOME` reference aborts the entire render with "Variable `env.HOME` not found in context" -- so branch # on os() and fall back to USERPROFILE (forward-slashed) there. The Windows value only has to render without # erroring: every var built from it (mise_data_dir, conda_openssl, py3_unix, pylib_flag, ort_loc_unix) is Unix-only # and unused on Windows, where mise uses %LOCALAPPDATA%\mise and config.windows.toml supplies the real paths. a_home = '''{% if os() == "windows" -%} {{- (env?.USERPROFILE or "") | replace(from="\\", to="/") -}} {%- else -%} {{- env.HOME -}} {%- endif -%} ''' # augeas asset filenames, one per supported platform. # Each matches an `[asset.""]` row in config/upstream-cache/data.toml so the checksums.rego policy # can enforce the cross-reference. All five tarballs are uploaded to the augeas-v1 release by the # upstream-cache `augeas` job. augeas_linux_arm64_asset = "1.14.1-aarch64-unknown-linux-gnu.tar.xz" augeas_linux_x64_asset = "1.14.1-x86_64-unknown-linux-gnu.tar.xz" augeas_macos_arm64_asset = "1.14.1-aarch64-apple-darwin.tar.xz" augeas_macos_x64_asset = "1.14.1-x86_64-apple-darwin.tar.xz" augeas_windows_x64_asset = "1.14.1-x86_64-pc-windows-mingw.tar.gz" # mise data dir -- the parent of `installs/-/`. # Used to compose absolute paths into a tool's install dir from [env] (e.g. AUGEAS_LENS_LIB). Honours # MISE_DATA_DIR if set; otherwise mise's default of $XDG_DATA_HOME (~/.local/share/mise on Linux/macOS). mise_data_dir = '{{ env?.MISE_DATA_DIR or (vars.a_home ~ "/.local/share/mise") }}' # The Rust/Python/Dart/Java half of the lockfile set osv-scanner scans, one file per ecosystem. # Split from the npm half (and composed into osv_locks below) so no single line exceeds the editorconfig line # length. The `a_` prefix puts both halves ahead of osv_locks in mise's alphabetical [vars] render order, so # they are defined before osv_locks interpolates them. a_osv_locks_base = "--lockfile Cargo.lock --lockfile uv.lock --lockfile pubspec.lock --lockfile pom.xml" # The npm half: the repo-root pnpm workspace lockfile, covering every member listed in pnpm-workspace.yaml. a_osv_locks_node = "--lockfile pnpm-lock.yaml" osv_locks = "{{ vars.a_osv_locks_base }} {{ vars.a_osv_locks_node }}" # clang-tidy's resource-dir arg (points clang at its builtin headers, e.g. # stddef.h). Empty default; config.linux.toml sets it from conda:clangxx. clang_resource_arg = "" conda_openssl = "{{ vars.a_home }}/.local/share/mise/installs/conda-openssl/3" # Forward-slashed config_root for safe substitution into bash command lines. # Windows mise returns a backslash-form path like `D:\a\core\core`; substituting that verbatim into a bash # command line trips `\a` -> alert, `\c` -> "suppress output" string mangling inside busybox ash, so paths # like `D:\a\core\core/target/...` become `D:acorecore/target/...`). Forward slashes Just Work on both # shells. Used as the root of every workspace-relative var below. config_root_fwd = '{{ config_root | replace(from="\\", to="/") }}' # Google dart-archive base URLs for config.dart.toml's `[tools.dart]` url/version_list_url. # They live here (always-loaded), not in the dart config, because mise resolves tool URLs during # `mise upgrade`/`install` without that env config's own [vars] in scope -- the same reason augeas_* assets are here. dart_bucket = "https://storage.googleapis.com/storage/v1/b/dart-archive/o" dart_release = "https://storage.googleapis.com/dart-archive/channels/stable/release" # Absolute path to the workspace-built et-cli binary. # The parallel build-ws-*-module tasks each invoke `et-cli module-package-json`; using this var (rather than # `cargo run -p et-cli`) means every invocation execs the same pre-built binary instead of re-entering cargo, # which avoids a race seen on macOS where one cargo's link step transiently unlinks `target/debug/et-cli` # while another cargo's `cargo run` tries to exec it # (`error: ... (never executed) ... No such file or directory (os error 2)`). The `build-et-cli` task is the # dependency that puts the binary in place; every et-cli-using task must (directly or transitively) depend # on it. `.exe` suffix on Windows matches cargo's output naming. a_et_cli_exe = "et-cli{% if os() == 'windows' %}.exe{% endif %}" a_et_cli_target_dir = "target{% if os() == 'windows' %}/x86_64-pc-windows-gnullvm{% endif %}/debug" # RUSTC_WORKSPACE_WRAPPER assignment for the coverage build; leaf var referenced by web_cov_wrapper below. # `a_` prefix so mise's alphabetical [vars] render defines it before web_cov_wrapper uses it. Split out so # web_cov_wrapper only adds the ET_TEST_COVERAGE conditional around this value. a_web_cov_wrapper_env = 'RUSTC_WORKSPACE_WRAPPER={{ config_root }}/target/debug/int-wasm-cov-wrapper ' et_cli = "{{ vars.config_root_fwd }}/{{ vars.a_et_cli_target_dir }}/{{ vars.a_et_cli_exe }}" # Oldest macOS SDK macos-sdk-check accepts, held as high as CI can actually satisfy. # This is a track-the-latest policy rather than a floor the build forces: the deployment target (11.0 on # arm64) is the only figure the repo itself requires, and every SDK since is technically sufficient. Pinning # near the newest keeps contributors on one known SDK, so a build difference is never "which Xcode is this". # # 26.0 is that policy capped by the runners. GitHub's macOS images are macOS 26 -- macos-latest is macOS 26 # arm64, macos-26-intel its x64 twin -- so they carry a 26.x SDK, and 27.0 would fail every macOS lane rather # than any real defect. Raise this to 27.0 once those images ship Xcode 27; a contributor already on 27 keeps # passing meanwhile, since the check is a floor and not an equality. macos_min_sdk = "26.0" # mise-managed CPython's bin dir (Linux/macOS), named to sort ahead of py3_unix, which builds on it. # The key order is the formatter's, and a var can only reference one defined above it. py3_bin_unix = "{{ vars.a_home }}/.local/share/mise/installs/python/3.13.16/bin" # mise-managed CPython (Linux/macOS); the PYO3_PYTHON default below. # Windows overrides PYO3_PYTHON with its own py3_win in config.windows.toml. The version # segment must equal the `python` [tools] pin (conftest enforces both). py3_unix = "{{ vars.py3_bin_unix }}/python3" # RUSTFLAGS fragments shared by the CARGO_TARGET_* env in config..toml rpath_flag = "-C link-arg=-Wl,-rpath,{{ vars.conda_openssl }}/lib" # rpath to the mise CPython lib dir so the runner finds libpython at runtime. # This lets the et-ws-pyo3-runner binary find libpython at runtime (libpython3.13.so on Linux, # libpython3.13.dylib on macOS via its @rpath install name). Points at the exact `3.x.y` install dir (the # `python` pin), since the minor-version alias dir isn't created everywhere. pylib_flag = "-C link-arg=-Wl,-rpath,{{ vars.a_home }}/.local/share/mise/installs/python/3.13.16/lib" wasm_rustflags = "-C target-cpu=mvp -C target-feature=+mutable-globals,+sign-ext,+nontrapping-fptoint,+reference-types" # Instrumentation flags (leaf var) shared by the wasm coverage builds. # The nightly -Z flag adds branch regions to the covmap so the lcov exports carry BRDA records. `a_` prefix so # mise's alphabetical [vars] render defines it before wasm_cov_rustflags uses it. a_wasm_cov_instr = "-Cinstrument-coverage -Zcoverage-options=branch" # RUSTFLAGS for the wasm coverage builds: instrument + emit LLVM IR, no profiler runtime (minicov provides it). # Single codegen unit + LTO off so each crate emits one .ll the wasm-cov task can gut and feed to llc/llvm-cov. wasm_cov_rustflags = "--emit=llvm-ir {{ vars.a_wasm_cov_instr }} -Ccodegen-units=1 -Clto=off -Zno-profiler-runtime" # Coverage fragments the wasm module build commands append unconditionally (empty unless ET_TEST_COVERAGE is set). # wasm_cov is an inline RUSTFLAGS env prefix; the nightly toolchain + conda clang come from the coverage [env]. # The *_feat vars turn on minicov: cargo `--features` for the WASI cdylibs, wasm-pack `-- --features` for browsers # (comm1 pulls et-web only under this feature). wasi_cov_feat = '{% if env?.ET_TEST_COVERAGE == "true" %} --features coverage{% endif %}' wasm_cov = '{% if env?.ET_TEST_COVERAGE == "true" %}RUSTFLAGS="{{ vars.wasm_cov_rustflags }}"{% endif %}' web_cov_feat = '{% if env?.ET_TEST_COVERAGE == "true" %} -- --features et-web/coverage{% endif %}' # Command prefix routing a browser module's wasm-pack build through the workspace-only coverage wrapper. # Instruments our crates, never dependency cdylibs. Empty unless ET_TEST_COVERAGE is set, so normal builds get # no wrapper (no fingerprint churn). The wrapper binary is built by the build-wasm-cov-wrapper task dependency. web_cov_wrapper = '{% if env?.ET_TEST_COVERAGE == "true" %}{{ vars.a_web_cov_wrapper_env }}{% endif %}' # Owner scope every module package is published under, as wasm-pack's flag for it. # GitHub Packages only accepts a package scoped to the repository owner, so an unscoped module cannot be # published at all. One var rather than the flag repeated per module, so the scope is stated once and a new # module build picks it up by using this like every other. It costs nothing on a module that is never # published: the hub drops the scope when it names a module, so a scoped and an unscoped build of the same # module are served under one name, at one URL, and are indistinguishable to a deployment. Defined above its # consumers because mise resolves `[vars]` in file order -- a var referenced before it is defined is simply # absent, which surfaces as `Field ... is not defined` rather than as an empty string. wasm_scope = "--scope edge-toolkit " # The module this repository's server serves at `/`, named as its own package.json declares it. # The server has no default: which module is a deployment's front page is that deployment's business, and a # name defaulted there would be one project's carried by every other. Stated once here because all three # ws-server tasks need it and a copy per task is a copy that drifts. hub_root_module = "@edge-toolkit/et-ws-server-static" # Override-free head of a browser module's wasm-pack build: coverage wrapper prefix plus fixed build args. # Used by the two modules that also run et-cli module-package-json (har1, face-detection). It excludes no_opt / # web_cov_feat -- those are appended inline at the call site because no_opt carries a per-platform override that a # nested var would freeze at this file's value. web_pack_cov = '{{ vars.web_cov_wrapper }}wasm-pack build . --target web {{ vars.wasm_scope }}' # Extra flag for the wasm-pack module builds; empty so wasm-opt runs. # config.windows.toml overrides it to --no-opt where wasm-opt can't execute. no_opt = "" # Workspace members to exclude from the build-based cargo checks; empty on Linux/macOS. # config.windows.toml overrides it to `--exclude et-ws-web-runner` -- that crate pulls deno -> v8, whose # rusty_v8 gnullvm prebuilt is broken, so the workspace cargo check/clippy/doc can't build it on Windows. cargo_ws_excludes = "" # The targets and scope cargo-clippy-check lints and cargo-clippy-fix rewrites, which must be the same set. clippy_scope = "--keep-going --workspace --all-targets" # Flags for cargo-hack-check's feature powerset. # `cuda` is left out because it downloads a CUDA build of ONNX Runtime that exists only for some host triples. # `docs` only renders the feature list into rustdoc, which cargo-doc-check builds, so it would double every run. cargo_hack_flags = "--all-targets --feature-powerset --exclude-features cuda,docs" # Linux/macOS install root for the github:microsoft/onnxruntime tool. # The ORT_LIB_LOCATION env below resolves to it via the OS conditional. Version # segment must match the [tools] pin (conftest's version_drift enforces this). ort_loc_unix = "{{ vars.a_home }}/.local/share/mise/installs/github-microsoft-onnxruntime/1.22.0" # Source/dest paths for the fetch-*-rclone model downloads, kept as vars. # Keeping them as vars lets each `rclone copyto` command stay on a single line without continuations. # face1's RetinaFace_int.onnx is mirrored to edge-toolkit/core's hf-cache-v release because HuggingFace # 429s the CI runner IP pool during prefetch (every docker-linux lane that hits amd/retinaface concurrently # runs into the rate limit). face1_asset pins the upstream amd/retinaface commit SHA (short); # config/upstream-cache/data.toml records the matching SHA256; the `publish-face1-to-hf-cache` task in # config.maint.toml (MISE_ENV=maint) refreshes both sides + the release asset together. # config/conftest/policy/checksums.rego cross-checks them. face1_asset = "RetinaFace_int-7b69507.onnx" face1_dst = "data/model-modules/model-face1/pkg/video_cv.onnx" face1_src = ":http:edge-toolkit/core/releases/download/{{ vars.a_hf_cache_tag }}/{{ vars.face1_asset }}" # eye1 is pyeye1's MediaPipe FaceLandmarker model bundle (Apache-2.0), used as-is (no conversion). # It is Google's face_landmarker.task, mirrored from the mediapipe-models bucket to the same hf-cache-v # release by publish-eye1-to-hf-cache in config.maint.toml. data.toml records the SHA256; checksums.rego # cross-checks both sides. eye1_asset = "face_landmarker.task" eye1_dst = "data/model-modules/model-eye1/pkg/face_landmarker.task" eye1_src = ":http:edge-toolkit/core/releases/download/{{ vars.a_hf_cache_tag }}/{{ vars.eye1_asset }}" har_dst = "data/model-modules/model-har-motion1/pkg/har-motion1.onnx" har_repo = ":http:acd17sk/MET-Metabolic-Equivalent-of-Task-AI-Android-APP" har_src = "{{ vars.har_repo }}/raw/refs/heads/main/Models/onnx/hybrid_met.onnx" # llm1's chat model: SmolLM2-135M-Instruct (Apache-2.0), fetched from HuggingFaceTB at a pinned commit. # transformers.js resolves a local model as `//`, so every file keeps its upstream # name under model-llm1/pkg/ and the weights keep their `onnx/` subdir. The q4f16 export is ~118 MB -- the # smallest instruct model that still holds a chat turn; point llm1_repo and the asset names at # SmolLM2-360M-Instruct for noticeably better replies at ~273 MB. Each asset name carries the upstream short # SHA; config/upstream-cache/data.toml records the matching SHA256s and checksums.rego cross-checks both sides. llm1_config_asset = "llm1-12fd25f-config.json" llm1_dir = "data/model-modules/model-llm1/pkg" llm1_gen_asset = "llm1-12fd25f-generation_config.json" llm1_repo = ":http:HuggingFaceTB/SmolLM2-135M-Instruct/resolve/12fd25f" llm1_tokenizer_asset = "llm1-12fd25f-tokenizer.json" llm1_tokenizer_config_asset = "llm1-12fd25f-tokenizer_config.json" llm1_weights_asset = "llm1-12fd25f-model_q4f16.onnx" mnist_dst = "services/ws-modules/wasi-graphics-info/pkg/mnist-12.onnx" mnist_src = ":http:onnx/models/raw/main/validated/vision/classification/mnist/model/mnist-12.onnx" speech1_asset = "speech1-84f0d86.onnx" speech1_dst = "data/model-modules/model-speech1/pkg/speech1.onnx" speech1_src = ":http:onnx-community/silero-vad/resolve/84f0d86/onnx/model_fp16.onnx" # Pip 26.0.1 wheel via PyPI, pinned to stay compatible with RustPython. # Pinned because pip 26.1.1+ calls # `sys.addaudithook` in `_prevent_further_imports`, which RustPython 0.5.0 # doesn't implement (RustPython#7956). Used to bootstrap pipx on rustpython # (config.windows.toml's preinstall, the Nano-Server arm) and to probe pip # + pipx behaviour generally (config.maint.toml's rustpython-investigation). # `_sha` is the SHA256-prefix dir PyPI uses for `packages/`; rclone composes # the full URL via `--http-url https://files.pythonhosted.org`. pip_for_rustpython = "26.0.1" pip_for_rustpython_sha = "de/f0/c81e05b613866b76d2d1066490adf1a3dbc4ee9d9c839961c3fc8a6997af" pip_for_rustpython_wheel = "pip-{{ vars.pip_for_rustpython }}-py3-none-any.whl" pip_for_rustpython_wheel_url = ":http:packages/{{ vars.pip_for_rustpython_sha }}/{{ vars.pip_for_rustpython_wheel }}" # Pre-built rustpython_wasm tarball from upstream RustPython main. # Built with wasm-pack --target web; `freeze-stdlib` is the default crates/wasm feature so the .wasm # embeds the Python stdlib (no RUSTPYTHONPATH dance needed at consumer-side). Tracks main HEAD so the # upstream `pip` audit-hook fix (RustPython#7956) is in. a_rp_wasm_asset pins the upstream short SHA; # config/upstream-cache/data.toml records the matching SHA256; built + uploaded by config.maint.toml's # `publish-rp-wasm-to-release` (sharing the rp-v release tag with the native rustpython binary builds). # `a_rp_wasm_short` is the canonical version source -- the upstream RustPython commit's 7-char short SHA. # rp_wasm_dir templates against it, and the publish-rp-wasm-to-release maint task bumps it (plus # a_rp_wasm_asset, the tool entry's version + checksum, and the data.toml asset row) in one go. # a_rp_wasm_asset stays a literal string because checksums.rego reads the raw TOML and needs the # filename verbatim to cross-check the data.toml [asset.*] entry. a_rp_wasm_asset = "rustpython-wasm-08a1d6f.tar.gz" a_rp_wasm_short = "08a1d6f" # Install dir for the [tools."http:rp-wasm"] tarball. # mise extracts to `installs/http-rp-wasm//`. Picks the per-OS install root inline: on Windows # mise uses %LOCALAPPDATA%\mise (config.toml's [vars] is rendered before config.windows.toml's [vars] is # in scope, so we can't cross-reference vars.mise_installs here); on Unix it uses MISE_DATA_DIR or # $HOME/.local/share/mise. On Windows vars.mise_data_dir still renders (via a_home's USERPROFILE fallback) but # resolves to a Unix-style path mise ignores there, so reusing it would be wrong -- the conditional is load-bearing. # Forward slashes throughout the Windows suffix: GHA's LOCALAPPDATA arrives already-forward-slashed # ("C:/Users/runneradmin/AppData/Local"), and a mixed-slash path trips uutils-coreutils' Windows path # normalizer (`cp: The system cannot find the path specified. (os error 3)`). # Extra actionlint flags for actionlint-check; empty everywhere except the config.windows.toml override. actionlint_args = "" # The one actionlint error actionlint-check ignores: a local action written in GitHub's self-repository syntax. # That syntax (`uses: $/.github/actions/`) arrived in July 2026, and actionlint 1.7.12, the latest release, # predates it, so it reports every such ref as # specifying action "$/.github/actions/install-mise" in invalid format because ref is missing. # available formats are "{owner}/{repo}@{ref}" or "{owner}/{repo}/{path}@{ref}" [action] # The pattern matches only that message for a `$/.github/actions/` ref, so a malformed remote ref is still caught. # Drop it once an actionlint release understands `$/`. actionlint_ignore = "-ignore 'specifying action ..[/][.]github/actions/[^ ]+ in invalid format because ref is missing'" gh_http = "--http-url https://github.com --progress --ignore-existing" # Shared shfmt flags for the shfmt-mise-check / shfmt-mise-fmt pair. # `-ln bash` because a split-out body carries no shebang for shfmt to sniff, `-i 2` to match the indent_size # .editorconfig sets for every file in the repo, and `-ci` to keep `case` arms indented under their `case` # (shfmt's default pulls them back to the `case` column, which every body in these configs disagrees with). shfmt_args = "-ln bash -i 2 -ci" # Shell every task runs its body under. # The trailing operand is a wrapper script rather than `-c`: mise hands the body over as the argument after the # shell operands either way, and a script file is the only one of the two forms that can run anything before the # body does. Double-quoted so a checkout path containing a space still arrives as one operand -- mise splits a # `shell` string on whitespace, honouring double quotes but not single. task_shell = 'bash -euo pipefail "{{ config_root }}/.mise/task-shell.sh"' # The TOML files the taplo tasks act on, NUL-separated: every tracked or new-but-not-ignored `*.toml`. # Named explicitly because taplo's own discovery walks the tree following links before it applies `exclude`, and pnpm # links an optional package it skips on the host to `../../../../..`, the repo root -- so `taplo lint` and # `taplo format --check` recursed through the repo inside node_modules forever instead of failing. toml_files = "git ls-files -z --cached --others --exclude-standard '*.toml'" # `set -x` variant, for a body whose every command should be traced. task_shell_trace = 'bash -xeuo pipefail "{{ config_root }}/.mise/task-shell.sh"' # Unguarded variant, for a body that checks its own exit codes and must not abort on the first failure. task_shell_plain = 'bash "{{ config_root }}/.mise/task-shell.sh"' # OpenObserve docker image for the openobserve/o2 dev-observability task. o2_image = "openobserve/openobserve:v1.1.1" # Port publish spec for that task, bound to loopback rather than every interface. # config/o2.env's credential is committed, so the collector must not be reachable off the machine. It lives here # as a var because the mise policy rejects an `X.Y.Z` literal inside a task `run` body, and an IPv4 address trips # that check the same way a version would. o2_publish = "127.0.0.1:5080:5080" # OpenTelemetry Collector image for the o2-hostmetrics / o2-macmon / o2-nvidia / o2-winmetrics tasks. otelcol_image = "otel/opentelemetry-collector-contrib:0.163.1" rp_wasm_dir = '''{% if os() == "windows" and env?.MISE_INSTALLS_DIR -%} {{- env.MISE_INSTALLS_DIR | replace(from="\\", to="/") -}}/http-rp-wasm/{{- vars.a_rp_wasm_short -}} {%- elif os() == "windows" -%} {%- set base = (env?.LOCALAPPDATA or "") | replace(from="\\", to="/") -%} {{- base -}}/mise/installs/http-rp-wasm/{{- vars.a_rp_wasm_short -}} {%- else -%} {{- vars.mise_data_dir -}}/installs/http-rp-wasm/{{- vars.a_rp_wasm_short -}} {%- endif -%} ''' [env] # Comma-separated list of every language env (one per .mise/config..toml). # Used as MISE_ENV by the *-all tasks to act on every language at once (mise has # no MISE_ENV=all). Hardcoded rather than shell-discovered so it works on Windows # too -- keep in sync when adding/removing a config..toml. ALL_LANGS = "dart,dotnet,java,js,kotlin,python,r,rust,zig" CLIPPY_CONF_DIR = "{{ config_root }}/config" TAPLO_CONFIG = "{{ config_root }}/config/taplo.toml" # augtool's lens search path is compile-time pinned to the build-time --prefix. # That --prefix (i.e. the GHA runner's $RUNNER_TEMP) means a freshly mise-installed augtool can't find # Toml.lns. Point it at the bundled lens dir in our tarball; the version segment is the [tools."http:augeas"] # pin -- bump them together. (A binary-self-locate patch in the jayvdb/augeas `win` branch is the proper # fix; until then, this.) AUGEAS_LENS_LIB = "{{ vars.mise_data_dir }}/installs/http-augeas/1.14.1/share/augeas/lenses" # Use the conda:openssl install for Rust's OPENSSL_DIR so openssl-sys crate builds. OPENSSL_DIR = "{{ vars.conda_openssl }}" # pyo3-ffi (et-ws-pyo3-runner) links its embedded interpreter at build time. # It links from PYO3_PYTHON, else the first python on PATH -- under mise that's the 32-bit Pyodide shim # ("target architecture (64-bit) does not match ... (32-bit)"). Pin it to the mise-managed CPython. # Always-loaded so `cargo check --workspace` builds the pyo3 runner without MISE_ENV=python. This is the # Linux/macOS path; config.windows.toml overrides it with py3_win. PYO3_PYTHON = "{{ vars.py3_unix }}" RCLONE_CONFIG = "{{ config_root }}/config/rclone.ini" RCLONE_RETRIES = "1" # Make a bare `rg` search the tracked hidden dirs (esp. `.mise/`) instead of skipping them. See the config file. RIPGREP_CONFIG_PATH = "{{ config_root }}/config/ripgrep.rc" # Scope to the wasm32 target only. # The env value is honoured by every `wasm-pack`/`cargo` invocation that targets wasm32-unknown-unknown but # doesn't leak to host builds (e.g. wasm-pack's auto-install of wasm-bindgen-cli would otherwise reject # `target-cpu=mvp` for the host). Hoisted from `[tasks.build-ws-wasm-agent.env]` to workspace [env] # because a task-level env override on Windows made mise's child-env composition drop the `%LOCALAPPDATA%\mise\shims` # dir from PATH, so `'wasm-pack' is not recognized` in the cmd subprocess. Captured at # https://github.com/edge-toolkit/core/actions/runs/27798859316/job/82264573380 # on 3a1e0c84. CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS = "{{ vars.wasm_rustflags }}" # Direct ort-sys (workspace `ort` dep) at the mise-staged ONNX Runtime install. # That install is github:microsoft/onnxruntime above, used instead of fetching at build time. The # workspace dep dropped the `download-binaries` feature, so SKIP_DOWNLOAD is # belt-and-braces (the download code path is gone). ORT_LIB_LOCATION = "{% if os() == 'windows' %}{{ vars.ort_loc_win }}{% else %}{{ vars.ort_loc_unix }}{% endif %}" ORT_SKIP_DOWNLOAD = "1" [tasks] ec = "editorconfig-checker" editorconfig-check = "editorconfig-checker" [tasks.dprint-check] env = { DPRINT_CACHE_DIR = "{{ config_root }}/target/dprint-cache" } run = "dprint check" [tasks.dprint-fmt] run = "dprint fmt" [tasks.jq-fmt] description = "Format every checked-in .jq file with jqfmt (in-place)" # jqfmt prints the formatted body to stdout (no -w flag). # Pipe it through goawk to strip the trailing space jqfmt's `-op pipe` mode leaves at the # end of each broken-pipe line, then rewrite via tmp + rename. # NOTE: jqfmt strips every `#` comment, so no aggregate runs this: the tracked .jq files carry documentation # comments it would delete. Run it by hand only on a file whose comments have been lifted elsewhere first. run = """ git ls-files '*.jq' | while read -r f; do jqfmt -f "$f" -op pipe -ob -ar | goawk '{ sub(/[ \\t]+$/, ""); print }' >"$f.tmp" coreutils mv "$f.tmp" "$f" done """ shell = "{{ vars.task_shell }}" [tasks."fmt:rust"] # Rust + universal formatters. Always-loaded, so `fmt`'s glob always matches it. depends = ["cargo-clippy-fix", "cargo-fmt", "dprint-fmt", "shfmt-mise-fmt", "taplo-fmt"] description = "Run the Rust + universal repo-wide formatters" [tasks.fmt] # Native `depends` glob (not a nested `mise run`) so it works on Windows. # It activates each task's tools. fmt:rust is always loaded, so the glob never # matches zero (which mise would error on); guest fmt: join when loaded. depends = ["fmt:*"] description = "Run all formatters: fmt:rust + any loaded guest fmt:" [tasks."check:rust"] # Rust + universal repo-wide checks. # Lives in the always-loaded default config, # so `check`'s `check:*` glob always matches at least this one (Rust is in the # default config this pass; the guest `check:` tasks come from their # loaded MISE_ENV configs). depends = [ "action-validator-check", "actionlint-check", "agent-lint-check", "ast-grep-check", "ast-grep-check-rule-tests", "branch-up-to-date-check", "cargo-check", "cargo-clippy-check", "cargo-clippy-wasm-check", "cargo-doc-check", "cargo-fmt-check", "cargo-shear-check", "checks-md-check", "conftest-check", "dockerignore-check", "dprint-check", "editorconfig-check", "gen-help-all", "gen-help-check", "gen-specs-check", "gitleaks-check", "hadolint-check", "jscpd-check", "kubeconform-check", "link-check", "ls-lint-check", "mise-lock-check", "readme-mise-version-check", "regal-check", "repo-check", "ryl-check", "semgrep-check", "semgrep-check-rule-tests", "shellcheck-check", "shellcheck-mise-check", "shfmt-mise-check", "taplo-check", "taplo-fmt-check", "trivy-check", "typos-check", "verification-check", "zizmor-check", ] description = "Run the Rust + universal repo-wide checks" [tasks.check] # Native `depends` glob over check:rust + every loaded guest check:. # They run as parallel siblings (all report, none gated), with each task's tools activated. check:rust is # always loaded so the glob never matches zero. (Avoid a nested `mise run 'check:*'` -- that breaks tool # activation and the `[env]` exec on Windows.) depends = ["check:*"] description = "Run all checks: check:rust + any loaded guest check:" [tasks."fix:rust"] # Rust + universal lint-fix passes. # Distinct from fmt:rust (style/whitespace # formatters) -- these apply auto-rewrites for actual lint violations. depends = ["ast-grep-fix", "cargo-clippy-fix", "cargo-shear-fix", "regal-fix", "semgrep-fix", "typos-fix"] description = "Apply Rust + universal lint-fix passes" [tasks.fix] # Runs every fix pass, then fmt, since the passes' rewrites need reformatting. # Same glob shape as fmt/check: fix:rust is always loaded, so the glob never matches zero, and guest fix: # tasks join when their MISE_ENV is loaded. depends = ["fix:*"] depends_post = ["fmt"] description = "Apply every lint-fix pass (fix:rust + any loaded guest fix:), then every formatter (fmt)" [tasks.action-validator-check] description = "Validate GitHub Actions workflow + composite-action YAML" run = "action-validator .github/workflows/*.yaml .github/actions/*/action.yaml" [tasks.agent-lint-check] description = "Lint this repository's agent configuration (AGENTS.md, .claude/) with agent-lint" # Skips where upstream ships no binary rather than failing on a missing one. # That is the one case this repo treats a missing tool as expected: the `[tools]` entry is os-scoped to the # targets the release actually carries, so on any other platform there is nothing to install and nothing this # could have run. macOS x86_64 is the case the os scope alone cannot express, since it scopes by OS and arch # while a task body is the only place the two can be read together on a host that does have mise. run = """ if [ "$(coreutils uname -s)" = "Darwin" ] && [ "$(coreutils uname -m)" != "arm64" ]; then echo "agent-lint-check: upstream publishes no macOS x86_64 binary; nothing to check on this host" exit 0 fi agent-lint . """ shell = "{{ vars.task_shell }}" [tasks.actionlint-check] description = "Lint GitHub Actions workflows (actionlint): local-action input validation, expression syntax, shellcheck" # Lint every workflow with actionlint's native checks plus its shellcheck integration over each `run:` script. # shellcheck is a mise [tool], so it is on PATH here and actionlint auto-detects and invokes it (except under # the Windows vars.actionlint_args override). The most load-bearing native rule is `[action]`, which fails # when a workflow step passes a `with:` key the referenced local action does not declare (GHA itself only # warns). actionlint lints workflows only, so it does NOT see composite-action -> action calls; the gha_uses # conftest rule covers that direction. Intentional word-splitting sites carry an inline # `# shellcheck disable=SC2086` directive in the run script, as the mise-task bodies do. run = "actionlint {{ vars.actionlint_args }} {{ vars.actionlint_ignore }} .github/workflows/*.yaml" [tasks.zizmor-check] description = "Audit GitHub Actions workflows + composite actions for security issues (zizmor)" # --offline so it needs no GH token and stays deterministic. # config/zizmor.yaml turns off the hash-pin policy (we pin actions by tag, not commit). run = "zizmor --offline --no-progress -c config/zizmor.yaml .github/workflows .github/actions" [tasks.ls-lint-check] description = "Lint file and directory naming conventions (ls-lint)" run = "ls-lint --config config/ls-lint.yaml" # Verify the host's macOS SDK before anything tries to link against it. # Two failures, and the first is the common one. `xcrun` reporting no SDK at all means the command-line tools # are absent or the active developer dir points at nothing -- config.macos.toml's `SDKROOT` is computed by # exactly this command, so that state poisons every cc-rs build script with "'stdlib.h' file not found" # rather than saying what is wrong. Catching it here names it once, up front. The second is an SDK older than # the deployment target the build asks for, which is incoherent: you cannot target macOS 11 with a 10.x SDK. # # Lives in the always-loaded config rather than config.macos.toml, and no-ops elsewhere. # `check:rust` and `test:rust` both depend on it, and their depends lists are static -- a task defined only in # the macOS config would be an unresolvable dependency on every other platform. # # This does NOT exist to demand a recent Xcode. Apple's `ld` reads its own SDK whatever its vintage, so there # is no upper-bound problem to guard against; the floor is only a floor. [tasks.macos-sdk-check] description = "Fail if the macOS SDK is missing or older than the minimum this repo builds against" run = """ if [ "$(coreutils uname -s)" != "Darwin" ]; then echo "macos-sdk-check: not macOS; nothing to verify" exit 0 fi sdk="$(xcrun --show-sdk-version 2>/dev/null || true)" if [ -z "$sdk" ]; then echo "macos-sdk-check: xcrun reported no SDK version." >&2 echo "Install the Xcode command-line tools with: xcode-select --install" >&2 exit 1 fi min="{{ vars.macos_min_sdk }}" # `sort -V` puts the lower version first, so the minimum staying first means the SDK is at least it. # Equal versions sort to the same value, which lands on the pass side, as an exact match should. lowest="$(printf '%s\\n%s\\n' "$min" "$sdk" | coreutils sort -V | coreutils head -n 1)" if [ "$lowest" != "$min" ]; then echo "macos-sdk-check: SDK $sdk is older than the required $min." >&2 echo "Update the Xcode command-line tools via Software Update, or: xcode-select --install" >&2 exit 1 fi echo "macos-sdk-check: SDK $sdk (minimum $min)" """ shell = "{{ vars.task_shell }}" [tasks.ast-grep-check] # --no-ignore hidden so the gha-* YAML rules reach .github/workflows. # ast-grep skips dot-dirs by default; gitignored paths like target/ stay skipped. description = "Run ast-grep structural-search rules" run = "ast-grep scan --error --no-ignore hidden -c config/ast-grep/sgconfig.yaml" # The valid/invalid cases in config/ast-grep/tests, one file per rule id. [tasks.ast-grep-check-rule-tests] description = "Run the ast-grep rule tests against their valid and invalid cases (ast-grep test)" run = "ast-grep test -c config/ast-grep/sgconfig.yaml --skip-snapshot-tests" [tasks.ast-grep-fix] description = "Apply autofix for ast-grep rules that define a `fix:` field" run = "ast-grep scan --update-all --no-ignore hidden -c config/ast-grep/sgconfig.yaml" [tasks.semgrep-check] description = "Run Semgrep code and configuration rules" run = "semgrep scan --config config/semgrep --error --metrics=off ." # Each case file in config/semgrep-tests is `..yaml`, its annotated target under `code:`. # The target is wrapped in YAML so the repo carries no loose source file in that language; it is unwrapped into # target/ under its real name, where `semgrep --test` pairs it with the rule file of the same stem. [tasks.semgrep-check-rule-tests] description = "Run the Semgrep rule tests against their annotated cases (semgrep --test)" run = """ work=target/scratch/semgrep-rule-tests coreutils rm -rf "$work" coreutils mkdir -p "$work" for case in config/semgrep-tests/*.yaml; do yq -r '.code' "$case" >"$work/$(coreutils basename "$case" .yaml)" done semgrep --test --metrics=off --config config/semgrep "$work" """ shell = "{{ vars.task_shell }}" [tasks.semgrep-fix] description = "Apply autofix for Semgrep rules that define a `fix:` field" run = "semgrep scan --config config/semgrep --autofix --metrics=off ." # `--ignore-gitleaks-allow` is deliberate. # Codacy runs its own gitleaks and does not read `gitleaks:allow`, so a marker that satisfies a bare local run # still fails the pushed branch. Honouring markers here would make this check weaker than the one it exists to # predict, which is the whole point of running it locally. [tasks.gitleaks-check] alias = "gitleaks" description = "Scan the working tree for committed secrets with gitleaks" run = "gitleaks dir . --config config/gitleaks.toml --no-banner --ignore-gitleaks-allow" [tasks.hadolint-check] description = "Lint the Dockerfiles with hadolint" # `*Dockerfile.*` also catches a BuildKit per-Dockerfile ignore file, which is not a Dockerfile. # BuildKit looks for `.dockerignore` beside the Dockerfile it is told to build, so the name # is fixed and cannot be moved out of the glob's way; hadolint parses it as Dockerfile syntax and stops on the # first ignore pattern with `unexpected '*' expecting ... a pragma, at least one space, or end of input`. run = """ git ls-files '*Dockerfile' '*Dockerfile.*' | rg -v '\\.dockerignore$' | xargs hadolint --config config/hadolint.yaml """ shell = "{{ vars.task_shell }}" # Duplication is gated as a fingerprint ratchet, not as a percentage. # config/jscpd-baseline.json records the fingerprint of every clone that already existed, and only a clone absent # from it fails, so nothing is permanently silenced and the ledger shrinks as duplication is removed -- # `--update-baseline` prints the added/removed counts. A percentage gate was rejected because removing duplicated # code also shrinks the denominator: this change deleted 653 duplicated lines and moved the total only 9.11% -> # 9.00%. The baseline lives on the task rather than in config/jscpd.json so jscpd-report, which shares that # config, still shows the whole picture instead of filtering to new clones. That config's `ignore` list covers # only generator-owned output nobody hand-writes -- note the files under a module's pkg/ other than package.json # are hand-written shims, so they stay in scope. # # The one `ignorePattern` entry, whose reason has to live here because that config is plain JSON. # It matches the `actions/checkout` step, which every workflow is obliged to write out in full: a composite # action is referenced by a path inside the repo, so it cannot run until the repo has been checked out, which # makes this the one step that can never be factored into `.github/actions/`. Six workflows therefore repeat the # same five lines with the same `fetch-depth` / `persist-credentials` pins, and reported as a clone it asks for a # refactor that the platform does not permit. `ignorePattern` drops those tokens before detection, which is # narrower than excluding the workflow files: everything else in them -- job bodies, step sequences, run blocks # -- is still compared. Delete this the day a checkout can be shared, and expect the clone count to rise by the # handful of pairs it is currently hiding. # # Skipped on Windows, with the operator's sign-off; the Linux and macOS lanes still enforce it. Under mise # 2026.10.15, jscpd (5.1.1 and 5.4.2 alike) dies on its first write of the report to stdout there: # # thread 'main' panicked at /rustc/.../library\std\src\io\stdio.rs:1166:9: # failed printing to stdout: failed to write the buffered data # sh: C:/i\jscpd-vk6ezlee/jscpd.exe: The system detected an overrun of a stack-based buffer in this # application. This overrun could potentially allow a malicious user to gain control of this application. # Error 0xc0000409 # # on commit https://github.com/edge-toolkit/core/commit/c91d332c72d0c94f9fd4aa915c1d485fee251fd0 at # https://github.com/edge-toolkit/core/actions/runs/39857810525/job/120100470709 (check windows-latest lane). The # same jscpd 5.1.1 printed its report there under mise 2026.9.17, so the pipe mise now hands a task's stdout on # Windows is the suspect. Lift the skip once a Windows run gets through. [tasks.jscpd-check] description = "Detect copy/pasted code across every language (jscpd)" run = """ {% if os() == "windows" -%} echo "jscpd-check: skipped on Windows, where jscpd crashes writing its report; Linux and macOS enforce it" {%- else -%} jscpd . --config config/jscpd.json --baseline config/jscpd-baseline.json --fail-on-new-clones {%- endif -%} """ shell = "{{ vars.task_shell }}" [tasks.jscpd-baseline-update] alias = "update-jscpd-baseline" description = "Rewrite config/jscpd-baseline.json from the current tree, printing added/removed clone counts" run = "jscpd . --config config/jscpd.json --baseline config/jscpd-baseline.json --update-baseline" # No baseline here on purpose -- a human reading the report wants every clone, not just the new ones. [tasks.jscpd-report] description = "Write an HTML duplication report to target/jscpd for browsing" run = "jscpd . --config config/jscpd.json --reporters html,console --output target/jscpd" [tasks.cargo-check] run = "cargo check --workspace {{ vars.cargo_ws_excludes }}" # Lints every crate under every combination of its features with the workspace clippy set. # A `#[cfg(feature)]` that only compiles, or only lints clean, alongside some other feature then fails here rather # than for the downstream user who picked that combination. cargo-clippy-check covers the default features alone. # Only the check workflow runs it, as a lane of its own: no aggregate does, because a clippy build per feature # combination of every crate takes far longer than anyone running `mise run check` locally should have to wait. [tasks.cargo-hack-check] description = "cargo clippy every crate's feature powerset" run = "cargo hack clippy --workspace {{ vars.cargo_hack_flags }} {{ vars.cargo_ws_excludes }}" # Fails on a dependency no source file names, and on one only tests name that is not a dev-dependency. # The scan is syntactic, so a crate reached only through a macro expansion reads as unused: such a crate goes in # that manifest's `[package.metadata.cargo-shear] ignored`, with the macro named beside it. [tasks.cargo-shear-check] description = "Find unused and misplaced Cargo dependencies with cargo-shear" run = "cargo shear --locked --deny-warnings" [tasks.cargo-shear-fix] description = "Remove the unused Cargo dependencies cargo-shear finds" run = "cargo shear --fix" # A real codegen + link pass, where cargo-check stops after type-checking. # That distinction is the whole point on a platform being brought up: the linker, the CRT and the -sys crates' # build scripts are exactly what a new host triple gets wrong, and `cargo check` reaches none of them. [tasks.cargo-build] run = "cargo build --workspace {{ vars.cargo_ws_excludes }}" [tasks.cargo-fmt] run = "cargo +{{ vars.rust_nightly }} fmt" [tasks.parfit-fmt] description = "Reflow Rust comment prose to the 120-column width with parfit (in-place)" # Reflow Rust comment prose with parfit, skipping the constructs it would otherwise flatten. # # Rust only: parfit's `toml` / `yaml` language modes emit an empty file, and its plain-text mode joins keys and # table headers into the surrounding comment prose, so TOML, YAML and markdown stay hand-wrapped. The width matches # .rustfmt.toml's max_width. The skip regexes pass rustdoc `# Heading` lines and list items (`*`, `-`, `1.`) through # verbatim -- without them parfit folds `# Panics` into the sentence below it and a bulleted list into one paragraph. # It also breaks a line after a hyphen, inside backticks and URLs included, and has no switch for that: it reflows # a whole paragraph before re-breaking it, so a skip regex cannot protect a URL that starts mid-paragraph. Check # the diff for comment lines ending in `-` and rejoin the word by hand; a split # `https://github.com/edge-toolkit/core/...` leaves a dead link behind. # Given file paths, reflows just those; with none, every tracked Rust file except the generator-owned ones. The # usage-spec is what carries the paths: mise appends bare task arguments to the body text, which the eval in # task-shell.sh then rejects, whereas a `var` arg arrives space-joined in $usage_file for xargs to split. run = """ heading='^\\s*//[/!]?\\s*#' item='^\\s*//[/!]?\\s*([-*]|[0-9]+\\.)\\s' if [ -z "${usage_file:-}" ]; then git ls-files '*.rs' ':!generated/**' ':!services/ws-wasi-runner/src/bindings.rs' else printf '%s\\n' "$usage_file" fi | xargs -r parfit -w 120 -s "$heading" -s "$item" """ shell = "{{ vars.task_shell }}" usage = 'arg "[file]..." var=#true help="Rust files to reflow (default: every tracked hand-written .rs file)"' [tasks.cargo-fmt-check] run = "cargo +{{ vars.rust_nightly }} fmt -- --check" [tasks.cargo-clippy-check] alias = ["cargo-clippy", "clippy", "clippy-check"] run = "cargo clippy {{ vars.clippy_scope }} {{ vars.cargo_ws_excludes }}" # Lints the wasm crates on the targets they actually build for, which cargo-clippy-check's host build never sees. # The crate sets come from `cargo metadata`, by what each crate binds to, so a new module joins without an edit: # a `wasm-bindgen` dependent is a browser crate, a `wit-bindgen` or `et-wasi-guest` dependent is a WASI guest. [tasks.cargo-clippy-wasm-check] description = "cargo clippy the browser and WASI crates on their wasm targets" run = """ meta="$(cargo metadata --format-version 1 --no-deps)" # `$dep` and `$names` are jaq variables bound inside the filter, so the shell must not expand them. # shellcheck disable=SC2016 pick='.packages[] | select(any(.dependencies[].name; . as $dep | any($names[]; . == $dep))) | "-p", .name' browser="$(printf '%s' "$meta" | jaq -r --argjson names '["wasm-bindgen"]' "$pick")" wasi="$(printf '%s' "$meta" | jaq -r --argjson names '["wit-bindgen", "et-wasi-guest"]' "$pick")" rc=0 # Both package lists are newline-separated `-p` / `` words, and rely on word-splitting. # shellcheck disable=SC2086 cargo clippy --keep-going --all-targets --target wasm32-unknown-unknown $browser || rc=1 # shellcheck disable=SC2086 cargo clippy --keep-going --all-targets --target wasm32-wasip2 $wasi || rc=1 exit "$rc" """ shell = "{{ vars.task_shell }}" [tasks.cargo-clippy-check-pkg] alias = ["clippy-pkg"] description = "cargo clippy on one or more workspace packages (narrow inner-loop check)" # Same flags as cargo-clippy-check minus --workspace, plus --no-deps. # Only the named packages are linted, not the workspace crates they depend on. Those still build, but their lints # are cargo-clippy-check's to report. # Usage-spec captures the package names and exposes them space-joined in $usage_package, split here into one `-p` # each; a missing arg fails up front with mise's auto-generated usage message instead of clippy's generic # "--package not found" error. run = """ set -- # shellcheck disable=SC2086 # $usage_package is a space-joined package list, split by design. for package in $usage_package; do set -- "$@" -p "$package" done cargo clippy --keep-going --all-targets --no-deps "$@" """ shell = "{{ vars.task_shell }}" usage = 'arg "..." var=#true help="Workspace package names (e.g. edge-toolkit et-ws-service)"' [tasks.cargo-doc-check] description = "Type-check rustdoc with -D warnings to fail on any doc issues" # Uses nightly rustdoc's `--check` (gated behind -Z unstable-options). # The task runs the front-end -- intra-doc link resolution + doc-comment type-check -- but skips HTML # emission. No `target/doc/` churn; faster than a full doc build. Nightly is already in [tools] for # `cargo +nightly fmt`. Drop `-Z unstable-options --check` and switch back to plain `cargo doc` once # rustdoc stabilises `--check`. env = { RUSTDOCFLAGS = "-Z unstable-options --check -D warnings" } # The mkdir works around ort-sys assuming a fixed OUT_DIR depth; it must precede any nightly cargo build. # Nightly cargo (1.99.0-nightly, 7c83d4cc0, 2026-07-29) writes build-script output to `build///out` # where stable still uses `build/-/out`. ort-sys 2.0.0-rc.10's `copy_libraries` locates the profile # dir via `out_dir.ancestors().nth(3)`, so that extra path segment lands it on `/debug/build` instead of # `/debug`. It then symlinks the ONNX Runtime dylibs into that dir plus `examples/` and `deps/` beneath # it, and the unwrapped symlink aborts on the first of those that does not exist: # thread 'main' panicked at .../ort-sys-2.0.0-rc.10/build.rs:129:66: # called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" } # Seen on commit https://github.com/edge-toolkit/core/commit/9440a53b3f632307d5a41b654ff526981021759f in the # macos + ubuntu check lanes, e.g. # https://github.com/edge-toolkit/core/actions/runs/30782416445/job/91589563319 -- and reproducible locally. # The feature cannot simply be switched off: wasmtime-wasi-nn declares `features = ["copy-dylibs"]` on its own # ort dependency, so feature unification keeps it enabled regardless of this workspace's ort entry. Creating the # dirs is inert under the classic layout. Drop this once ort stops hardcoding the depth -- 2.0.0-rc.13 still # does, at build/dynamic_link.rs:18. run = """ ort_build_dir="${CARGO_TARGET_DIR:-target}${CARGO_BUILD_TARGET:+/$CARGO_BUILD_TARGET}/debug/build" coreutils mkdir -p "$ort_build_dir/examples" "$ort_build_dir/deps" tc="{{ vars.rust_nightly }}" # `docs` is the feature docs.rs builds with, which renders each crate's feature list. features="--features docs" # shellcheck disable=SC2086 cargo "+$tc" doc --keep-going --workspace --no-deps --document-private-items $features {{ vars.cargo_ws_excludes }} """ shell = "{{ vars.task_shell }}" [tasks.cargo-clippy-fix] run = "cargo clippy --fix --allow-dirty --allow-staged {{ vars.clippy_scope }} {{ vars.cargo_ws_excludes }}" [tasks.taplo-fmt] alias = "toml-fmt" run = "{{ vars.toml_files }} | xargs -0 taplo format" [tasks.taplo-fmt-check] alias = "toml-fmt-check" run = "{{ vars.toml_files }} | xargs -0 taplo format --check" [tasks.conftest-check] depends = ["conftest-check-*"] description = "Run all conftest policy checks (one per parsed file format)" [tasks.conftest-check-toml] description = "Run conftest OPA/Rego policies over the TOML config + lock files" # --combine feeds every listed file to one policy evaluation. # It arrives as an array of {path, contents}, so a policy can both check each file and compare across files # (the mise wasm-bindgen pin vs Cargo.lock; the .mise/config*.toml *_asset vars vs config/upstream-cache/data.toml; # the mise python pin vs config/pyrefly.toml's python-version). --parser toml: Cargo.lock has no .toml suffix. The # TOML rule namespaces are listed explicitly, so the per-file `gha` (YAML) namespace is never evaluated against this # combined TOML input. run = """ ns_list="cross cargo mise mise_lock pyproject checksums advisories" named="Cargo.lock config/upstream-cache/data.toml config/deny.toml config/pyrefly.toml" # Both lists rely on word-splitting when they expand. # $ns expands to repeated `--namespace ` pairs; $named is a space-separated path list. # shellcheck disable=SC2086 ns=$(for n in $ns_list; do echo -n " --namespace $n"; done) # shellcheck disable=SC2086 git ls-files '*Cargo.toml' '.mise/config*.toml' '.mise/mise*.lock' '*pyproject.toml' $named | xargs conftest test --combine --parser toml $ns -p config/conftest/policy # The trailing-backslash ban holds for every TOML file, not only the ones the namespaces above are written for. {{ vars.toml_files }} | xargs -0 conftest test --combine --parser toml --namespace no_trailing_backslash -p config/conftest/policy """ shell = "{{ vars.task_shell }}" # Hold every value mise renders free of the Tera comment opener, over each config mise loads. # The generated scenario `mise.toml` files are rendered by mise too, which is why they are listed here and not in # `conftest-check-toml`, whose other namespaces are written for the repository's own configs. [tasks.conftest-check-mise-templates] description = "Check no mise config value contains Tera's comment opener (conftest)" run = """ files=$(git ls-files '.mise/config*.toml' '*mise.toml') # $files is a newline-separated path list, split into one argument per path by design. # shellcheck disable=SC2086 conftest test --combine --parser toml --namespace tera_comment -p config/conftest/policy $files """ shell = "{{ vars.task_shell }}" # Hold the README's checks badge to the rules count CHECKS.md records. # `--parser ignore` because Markdown has no parser; it hands each file over as its lines, which is all this reads. [tasks.conftest-check-checks-badge] description = "Check the README checks badge keeps step with CHECKS.md's rules count (conftest)" run = """ if [ ! -f README.md ]; then echo "conftest-check-checks-badge: README.md absent (docker build context); skipping -- native lanes enforce it" exit 0 fi conftest test --combine --parser ignore --namespace checks_badge -p config/conftest/policy CHECKS.md README.md """ shell = "{{ vars.task_shell }}" # Ratchet the jscpd duplication baseline against the count declared in the policy. # One file, no --combine, so the policy reads the parsed baseline as `input` directly. [tasks.conftest-check-jscpd] description = "Check the jscpd baseline has not grown (conftest)" run = "conftest test --namespace jscpd -p config/conftest/policy config/jscpd-baseline.json" # Rego unit tests living beside the policies, for invariants the real policy runs cannot reach from one OS. # Named into the conftest-check-* family so the `check` aggregate always picks it up. [tasks.conftest-check-policy-tests] description = "Run the Rego unit tests beside the conftest policies (conftest verify)" run = "conftest verify -p config/conftest/policy" # The policy tree checked against itself: every policy must have a test file beside it, or a recorded reason. # `--parser ignore` because .rego has no parser and only the paths are read; the policy contents are never # inspected, so the entries the parser produces per line go unused. [tasks.conftest-check-policy-pairs] description = "Check every conftest policy has a test file beside it (conftest)" run = """ find config/conftest/policy -name '*.rego' -print0 | xargs -0 conftest test --combine --parser ignore --namespace policy_pairing -p config/conftest/policy """ shell = "{{ vars.task_shell }}" [tasks.conftest-check-generated-trees] description = "Cross-check config/generated-trees.toml against the linter configs it governs (conftest)" # --combine over a deliberately mixed file set, with conftest auto-detecting each parser (TOML, YAML, JSON). # The declaration and every governed config must arrive in one evaluation so a rule can compare across them. run = """ files="config/generated-trees.toml config/jscpd.json config/pyrefly.toml config/ast-grep/rules config/semgrep" # shellcheck disable=SC2086 # $files is a word-split path list by design; do not quote it. conftest test --combine --namespace generated_trees -p config/conftest/policy $files """ shell = "{{ vars.task_shell }}" [tasks.conftest-check-branch-size] description = "Fail when the branch modifies more files than CodeRabbit reviews in one pull request (conftest)" # The list is every tracked file changed since the merge base with origin/main, plus new untracked files. # The merge base is the pull request's base, so the count is the one CodeRabbit sees. A clone with no origin/main # has nothing to compare against, and skips as branch-up-to-date-check does. run = """ if ! git rev-parse --verify --quiet origin/main >/dev/null; then echo "conftest-check-branch-size: origin/main not available (shallow clone?); skipping" exit 0 fi base="$(git merge-base origin/main HEAD)" { git diff --name-only "$base"; git ls-files --others --exclude-standard; } | coreutils sort -u | conftest test --combine --parser ignore --namespace branch_size -p config/conftest/policy - """ shell = "{{ vars.task_shell }}" [tasks.conftest-check-no-exemptions] description = "Check the rules held to every file carry no path exemption, and their inline guard (conftest)" # --combine over every rule file, so the policy can hold the inline guard to the same list as the rules it guards. run = """ files="config/ast-grep/rules config/semgrep" # shellcheck disable=SC2086 # $files is a word-split path list by design; do not quote it. conftest test --combine --namespace no_exemptions -p config/conftest/policy $files """ shell = "{{ vars.task_shell }}" [tasks.conftest-check-yaml] description = "Run conftest OPA/Rego policies over the GitHub Actions YAML, and the backslash ban over all YAML" # Five passes over the workflow + action YAML, then one over every YAML file. # per-file rules (gha) over workflow YAML; --combine rules (gha_combined) over # workflow YAML for cross-cutting invariants where the rule has to compare each workflow's parsed body # against its own filename (the `paths:` self-reference check; conftest's per-file mode has no `input.path`); # gha_action over the composite actions under .github/actions/*/action.yaml (different schema, so its # own namespace); gha_mise over the workflow YAML combined with .mise (auto-detected parsers) so a rule can # cross-check a hard-coded action-input version against the matching mise [tools] pin (setup-dotnet's # dotnet-version vs the `dotnet` pin); and gha_uses over the workflow YAML combined with the composite actions so a # rule can resolve each `uses: ./.github/actions/` against that action's declared `inputs:` (GHA only warns on # an undeclared input key, silently dropping the value). no_trailing_backslash reads `input` as a file list, so it needs # --combine like the TOML pass in conftest-check-toml. run = """ conftest test --parser yaml --namespace gha -p config/conftest/policy .github/workflows conftest test --parser yaml --combine --namespace gha_combined -p config/conftest/policy .github/workflows conftest test --parser yaml --namespace gha_action -p config/conftest/policy .github/actions/*/action.yaml conftest test --combine --namespace gha_mise -p config/conftest/policy .github/workflows .mise conftest test --parser yaml --combine --namespace gha_uses -p config/conftest/policy .github/workflows .github/actions git ls-files -z '*.yaml' '*.yml' | xargs -0 conftest test --parser yaml --combine --namespace no_trailing_backslash -p config/conftest/policy """ shell = "{{ vars.task_shell }}" [tasks.conftest-check-dockerfile] description = "Cross-check Dockerfiles against mise [tools] pins + heredoc body-first-line invariants" # Two passes over each Dockerfile. # 1. `dockerfile` parser (default) for AST-shape rules: version drift, pipx-disable, RUN heredoc must invoke bash. # Combined with .mise so the AST rules can compare against [tools] pins. # 2. `ignore` parser, which preserves every line of the file as a separate entry -- conftest's dockerfile parser # flattens heredoc bodies out of the AST, so the body-first-line `set -euo pipefail` check lives in its # own namespace + parser. # Each git-tracked Dockerfile* gets both passes, except that the `ignore` pass only visits Dockerfiles that # actually contain heredocs: conftest 0.68.2's ignore parser panics with `runtime error: slice bounds out of # range` on plain Dockerfiles (no heredocs), and the body-first-line rule has nothing to check there anyway. # A BuildKit per-Dockerfile ignore file is dropped from the list first. `.dockerignore` is # the name BuildKit demands beside the Dockerfile it builds, so it cannot be moved out of the glob's way, and # the dockerfile parser reads its first pattern as an instruction: # unknown instruction: **/.claude/ run = """ files=$(git ls-files '*Dockerfile' '*Dockerfile.*' | rg -v '\\.dockerignore$') # $files is a newline-separated path list; word-splitting is intentional (no spaces in paths). # shellcheck disable=SC2086 conftest test --combine --namespace dockerfile -p config/conftest/policy $files .mise # A published scenario's `--from=` names must be stages the same file declared, one file per input. published=$(git ls-files 'verification/published/**/Dockerfile') # shellcheck disable=SC2086 conftest test --namespace dockerfile_stages -p config/conftest/policy $published # Filter to Dockerfiles that actually use heredocs before the `ignore` parser. for f in $files; do if rg -q '^RUN[^\\n]*<<' "$f"; then conftest test --parser ignore --namespace dockerfile_heredoc -p config/conftest/policy "$f" fi done """ shell = "{{ vars.task_shell }}" [tasks.regal-check] description = "Lint Rego policy files with Regal (style, idiomatic, bugs)" # config/regal.yaml carves out conftest-specific patterns. # These are multiple `deny` rules per file, cross-package data references, no OPA entrypoints. run = "regal lint -c config/regal.yaml config/conftest/policy/" [tasks.regal-fix] description = "Auto-fix the regal violations that have an auto-fixer (e.g. opa-fmt)" run = "regal fix -c config/regal.yaml config/conftest/policy/" [tasks.shellcheck-check] description = "shellcheck the standalone shell fragments committed under .mise/" # The sibling task below reads shell out of the TOML; this one covers the files that are already shell on disk. # Nothing was linting them, so an external analyzer reported their findings first -- a `.sh` under `.mise/` is # exactly as much shell as a task body is, and had none of the same coverage. # No `--shell` override and no `--exclude`: each file declares its own interpreter through a shebang or a # `shell=` directive, and the exclusions the sibling task carries exist for extracted bodies, whose # Tera-masked names read as unassigned variables. Applying either here would hide real findings. # `-x` follows what a fragment sources, resolved from the repo root, which is the base a task body's relative # `source` path resolves against when it pulls one of these in. run = 'shellcheck -x --source-path="{{ vars.config_root_fwd }}" .mise/*.sh' [tasks.shellcheck-mise-check] description = "shellcheck every bash-shell mise task body across .mise/config*.toml" # Extract each multi-line bash-shell task body and shellcheck it in isolation. # For each `[tasks.X] run = """..."""` whose `shell` starts with bash and whose `run` is multi-line, drop # it into target/scratch/shellcheck-mise/ with a synthetic shebang + `set -euo pipefail`, then shellcheck # the lot. Tera tokens `{{ ... }}` are masked to `MISEVAR` (a valid bash word) and `{% ... %}` to empty so # shellcheck doesn't choke on template syntax. The awk regex uses character classes (`[{][{]` not `{{`) so # mise's own Tera render of this task body doesn't itself trip on the awk source. The jq filter at # .mise/shellcheck-mise.jq emits one ` ` line per eligible task (base64 so newlines + # quotes survive the pipe). goawk emits CRLF on Windows, so `tr -d '\r'` normalises each generated script # back to LF (a no-op on Unix) -- without it shellcheck flags every body with SC1017 (literal carriage return). # `-x` with the repo root as source path follows what a body sources, so a shared fragment is linted with it # rather than reported unread; the extracted scripts sit under target/, so a relative source needs that root. run = ''' boot="{{ vars.config_root_fwd }}/target/scratch/shellcheck-mise" coreutils rm -rf "$boot" coreutils mkdir -p "$boot" mask='{ gsub(/[{][{][^}]*[}][}]/, "MISEVAR"); gsub(/[{][%][^%]*[%][}]/, ""); print }' for cfg in .mise/config*.toml; do stem=$(coreutils basename "$cfg" .toml) yq -p toml -o json "$cfg" | jaq -r -f .mise/shellcheck-mise.jq | while read -r name b64; do out="$boot/${stem}__${name}.sh" { echo '#!/usr/bin/env bash' echo 'set -euo pipefail' printf '%s' "$b64" | coreutils base64 -d | goawk "$mask" } | coreutils tr -d '\r' >"$out" done done shellcheck --shell=bash -x --source-path="{{ vars.config_root_fwd }}" --exclude=SC2034,SC2154 "$boot"/*.sh ''' shell = "{{ vars.task_shell }}" [tasks.shfmt-mise-check] description = "Check that every multi-line bash mise task body is shfmt-formatted" # Run the shfmt-mise-fmt pipeline against scratch copies and compare, rather than over the configs themselves. # The `-d` pass is what makes a failure actionable -- shfmt prints the offending body's own diff -- while the # hash compare additionally catches whatever the merge pass would rewrite outside shfmt's view (the TOML # escape normalisation .mise/shfmt-mise.awk performs on a `"""` body). run = """ work="{{ vars.config_root_fwd }}/target/scratch/shfmt-mise-check" coreutils rm -rf "$work" status=0 for cfg in .mise/config*.toml; do d="$work/$(coreutils basename "$cfg" .toml)" coreutils mkdir -p "$d" goawk -N raw -v mode=split -v dir="$d" -f .mise/shfmt-mise.awk "$cfg" done shfmt {{ vars.shfmt_args }} -d "$work"/*/*.sh || status=1 shfmt {{ vars.shfmt_args }} -w "$work"/*/*.sh for cfg in .mise/config*.toml; do d="$work/$(coreutils basename "$cfg" .toml)" goawk -N raw -v mode=merge -v dir="$d" -f .mise/shfmt-mise.awk "$cfg" >"$d/merged.toml" have=$(coreutils sha256sum -b "$cfg" | coreutils cut -c1-64) want=$(coreutils sha256sum -b "$d/merged.toml" | coreutils cut -c1-64) if [ "$have" != "$want" ]; then echo "$cfg: task bodies need reformatting; run 'mise run shfmt-mise-fmt'" >&2 status=1 fi done exit $status """ shell = "{{ vars.task_shell }}" [tasks.shfmt-mise-fmt] description = "Format every multi-line bash mise task body with shfmt" # Split each config's task bodies into scratch scripts, shfmt them, then merge them back over the config. # .mise/shfmt-mise.awk owns both halves of that trip; the two loops here are split around one shfmt call so # the glob always has something to match (config.java.toml carries no multi-line body of its own). run = """ work="{{ vars.config_root_fwd }}/target/scratch/shfmt-mise-fmt" coreutils rm -rf "$work" for cfg in .mise/config*.toml; do d="$work/$(coreutils basename "$cfg" .toml)" coreutils mkdir -p "$d" goawk -N raw -v mode=split -v dir="$d" -f .mise/shfmt-mise.awk "$cfg" done shfmt {{ vars.shfmt_args }} -w "$work"/*/*.sh for cfg in .mise/config*.toml; do d="$work/$(coreutils basename "$cfg" .toml)" goawk -N raw -v mode=merge -v dir="$d" -f .mise/shfmt-mise.awk "$cfg" >"$d/merged.toml" coreutils cp "$d/merged.toml" "$cfg" done """ shell = "{{ vars.task_shell }}" # taplo-fmt rewrites every .mise/config*.toml in full, and so does this task. # fmt:rust runs its dependencies as parallel siblings, so with no ordering edge between the two the second # writer silently drops the first one's work. `wait_for` rather than `depends` because the edge only matters # when both are already scheduled -- `mise run shfmt-mise-fmt` alone should stay one task, not two. wait_for = ["taplo-fmt"] [tasks.taplo-check] alias = "toml-check" # `taplo lint` reads config/taplo.toml's `[[rule]] schema` entries. # It uses them for editor / LSP-style validation, but silently ignores their nested constraints in CLI # mode (verified: injecting `path = "../foo"` into a member Cargo.toml doesn't fire `no-path-deps` via the # rule alone). We work around it by applying each schema explicitly with `taplo lint --schema file://...`. # conftest-check-toml (config/conftest/policy) intentionally duplicates these lints; running both is fine. run = """ {{ vars.toml_files }} | xargs -0 taplo lint # The --schema lints need a file:// URL, built cross-platform as file:///${PWD#/}. # taplo rejects a bare path. On Linux $PWD is /abs, so the form yields file:///abs; on Windows $PWD is C:/dir # (forward slashes, from busybox), yielding file:///C:/dir. A plain file://$PWD puts the drive letter in the # URL authority and breaks on Windows. conftest-check-toml duplicates these lints as a cross-check. # All member Cargo.tomls -- exclude workspace root and any build output. # `-prune` target/ and .git so find never descends into them: `-not -path` alone still walks the whole # tree, and target/debug/deps/ churns constantly under a concurrent `cargo-check`, racing find into # "No such file" errors. POSIX sh (no bash arrays / process substitution -- the tasks run on busybox ash # on Windows): collect the paths into a temp file once and feed each schema run via xargs. members="$(mktemp)" trap 'rm -f "$members"' EXIT find . -path ./target -prune -o -path ./.git -prune -o -name Cargo.toml ! -path ./Cargo.toml -print >"$members" # Banned deps only need to be checked at the workspace root. # Member crates are required to reference deps via `workspace = true` (the `require-workspace-deps` schema # below), so any banned dep can only enter the build through `[workspace.dependencies]` here. taplo lint --schema "file:///${PWD#/}/config/taplo/no-banned-deps.schema.json" Cargo.toml # Schemas that apply to every member crate. xargs taplo lint --schema "file:///${PWD#/}/config/taplo/no-path-deps.schema.json" <"$members" xargs taplo lint --schema "file:///${PWD#/}/config/taplo/no-wildcard-or-git-deps.schema.json" <"$members" xargs taplo lint --schema "file:///${PWD#/}/config/taplo/require-workspace-deps.schema.json" <"$members" xargs taplo lint --schema "file:///${PWD#/}/config/taplo/require-lib-doctest-false.schema.json" <"$members" xargs taplo lint --schema "file:///${PWD#/}/config/taplo/no-lib-name.schema.json" <"$members" # `require-lints-section` exempts generated/rust-rest/Cargo.toml. # It is exempted because progenitor's emitted `src/lib.rs` contains patterns our workspace lint table # flags (e.g. doc-comment shapes that trip rustdoc's invalid-code-block lint). Inheriting `[lints] workspace = true` # would fail the build on the generated source. The exemption is about the lint table, not about the # deps -- its `[dependencies]` still inherits from `[workspace.dependencies]` like every other crate. rg -vxF './generated/rust-rest/Cargo.toml' "$members" | xargs taplo lint --schema "file:///${PWD#/}/config/taplo/require-lints-section.schema.json" taplo lint --schema "file:///${PWD#/}/config/taplo/mise-run-not-array.schema.json" .mise/config*.toml taplo lint --schema "file:///${PWD#/}/config/taplo/mise-no-ubi-backend.schema.json" .mise/config*.toml # config.maint.toml is exempted from the cargo:-backend allowlist. # One-off maint tools can be slow cargo source-builds; see mise.rego's matching note. non_maint=$(find .mise -maxdepth 1 -name 'config*.toml' ! -name 'config.maint.toml') # $non_maint holds a space-separated path list; word-splitting is intentional. # shellcheck disable=SC2086 taplo lint --schema "file:///${PWD#/}/config/taplo/mise-cargo-backend-allowlist.schema.json" $non_maint """ shell = "{{ vars.task_shell }}" [tasks.readme-mise-version-check] # Keep the README's stated mise version in sync with the enforced `min_version` in .mise/config.toml. # No structural linter spans a markdown prose value and a TOML value, so this is a small goawk/coreutils compare. # The README states the same version as a floor ("or later"), so the match takes the first version on its # install line and compares that; the trailing words are not part of the comparison. description = "Fail if README.md's mise version differs from min_version in .mise/config.toml" run = """ # README.md is excluded from the docker build context, so it is absent when `check` runs inside an image. # gen:dockerignore adds /README.md. Skip there -- the native check lanes (which have README.md) enforce the sync. if [ ! -f README.md ]; then echo "readme-mise-version-check: README.md absent (docker build context); skipping -- native lanes enforce it" exit 0 fi config_version="$(goawk -F'"' '/^min_version/{print $2; exit}' .mise/config.toml)" # shellcheck disable=SC2016 # $0/RSTART/RLENGTH are awk fields, not shell vars -- single quotes are intended readme_prog='/Please install/ && match($0,/[0-9]{4}\\.[0-9]+\\.[0-9]+/){print substr($0,RSTART,RLENGTH);exit}' readme_version="$(goawk "$readme_prog" README.md)" if [ "$config_version" != "$readme_version" ]; then echo "README mise version ($readme_version) != .mise/config.toml min_version ($config_version)" >&2 exit 1 fi """ shell = "{{ vars.task_shell }}" [tasks.typos-check] alias = "typos" run = "typos --config config/typos.toml" [tasks.typos-fix] description = "Auto-correct typos detected by the typos linter" run = "typos --config config/typos.toml --write-changes" # lychee makes live network requests, so config/lychee.toml is tuned to keep it from being flaky. # Retries, 429-tolerance and a disk cache. It checks every URL in the repo's Markdown + Rust sources; lychee # extracts URLs from .rs comments and string literals too. # Validates the generated Kubernetes manifests against the real API schemas. # `verification-check` only proves the manifests are stable across regenerations, not that a cluster would # accept them, so a generator that emitted a malformed object would drift-check clean forever. `-strict` also # rejects unknown fields, which is what turns a mistyped manifest key into a failure here rather than at # `kubectl apply`. The schemas are fetched on first use and cached under target/; it is a handful of files # from raw.githubusercontent.com, far inside the rate limit that makes larger fetches worth mirroring. [tasks.kubeconform-check] alias = "kubeconform" description = "Validate the generated Kubernetes manifests against the Kubernetes API schemas" run = """ cache=target/kubeconform-cache coreutils mkdir -p "$cache" find verification -name k3s.yaml -print0 | xargs -0 -r kubeconform -strict -summary -cache "$cache" """ shell = "{{ vars.task_shell }}" # End-to-end check that a published deployment runs against released artifacts and nothing local. # The sibling scenario checks all read this working tree: the mise and compose ones build the runners out of the # workspace, and k3s-verify imports images the caller built. None of them can tell whether what was published is # usable, which is a different question and one only a release can answer. This installs the hub, the runner and # the module packages from crates.io and GitHub Packages, runs the math1 exchange on them, and asserts the twin # stored the model the other deployments are held to. # # Expected to fail for a stretch after any change to a published crate, and that is not a defect in this task. # What it runs is whatever the registries currently hold, so between a change landing here and the release that # carries it, this reports the old artifacts -- correctly. It is therefore deliberately absent from the `check` # aggregate and marked continue-on-error in CI: a red result asks "is a release outstanding?", not "is the tree # broken?". Run it after publishing, not before. # # Needs a GITHUB_TOKEN because GitHub Packages requires auth even to read a public package, and Docker for the # collector the scenario starts alongside the hub. [tasks.published-verify] description = "Run the published math1 scenario against released crates and module packages, end to end" run = """ scenario=math1 out="verification/published/output/$scenario" if [ -z "${GITHUB_TOKEN:-}" ]; then echo "published-verify: GITHUB_TOKEN is unset; GitHub Packages rejects an unauthenticated read" >&2 exit 1 fi # Storage is where the deployment itself keeps it, beside its `mise.toml`, and is emptied first. # The deployment's own task env names that directory, which outranks anything exported into this process, so it is # read from there rather than redirected. Emptying it keeps a stale model from an earlier run from satisfying the # poll below; the directory is gitignored beside the deployment. storage="$out/storage" coreutils rm -rf "$storage" scenario_pid="" cleanup() { status=$? if [ -n "$scenario_pid" ]; then kill "$scenario_pid" 2>/dev/null || true; fi # The collector outlives the task that started it. # mise's child is the docker client, and killing a client leaves the container running, to be adopted by the # next run as a name collision. docker rm -f openobserve >/dev/null 2>&1 || true coreutils rm -rf "$storage" exit "$status" } trap cleanup EXIT mise trust "$out/mise.toml" >/dev/null # The lockfile is removed rather than honoured, which is the opposite of what a deployment usually wants. # `lockfile = true` is set repository-wide, so a scenario directory acquires one on its first install and from # then on pins whatever was resolved that day -- `specifiers = ["latest"]` alongside a fixed version, so a # newer release is not consulted at all. That is precisely the release this task exists to exercise, and the # pin is silent: the install succeeds, and only the running binary's version gives it away. The file is # gitignored, so nothing downstream depends on it surviving. coreutils rm -f "$out/mise.lock" # The npm registry config is exported here, for every mise call below, rather than left to the deployment's `[env]`. # mise computes that entry but does not apply it to its own tool resolution, so the embedded npm client reads # no `NPM_CONFIG_USERCONFIG`, falls back to registry.npmjs.org, and answers `package not found` for a scoped # package that only GitHub Packages holds. Exported into the process, the same file resolves against # npm.pkg.github.com and installs. Measured both ways on one cold cache: the deployment's `[env]` alone # fetched https://registry.npmjs.org/@edge-toolkit%2Fet-ws-math1 and installed nothing. export NPM_CONFIG_USERCONFIG="$PWD/$out/npmrc" (cd "$out" && mise install) # `mise lock` after the install records each npm module's dependency graph, which the install alone leaves out. # mise files an npm install under a directory named for that graph (`~aube~`), but a fresh # `mise install` writes the module's lock entry with no graph, and `mise ls --current` then names the bare # `` directory that was never created. The hub finds its modules through that listing, so it served # nothing and each runner's request for its module's `package.json` was a 404. Reproduced with any `latest` npm tool # on the mise `min_version` names and on its next release: the install lands in `~aube~` while the # listing names ``, until `mise lock` adds the graph. Seen as # `GET /modules/@edge-toolkit/et-ws-math1/package.json` 404s at # https://github.com/edge-toolkit/core/commit/b19fbf4790a38c77000dd888d513ff27b0f35d16, job # https://github.com/edge-toolkit/core/actions/runs/36968983438/job/110718777886. (cd "$out" && mise lock) (cd "$out" && mise run generated-scenario) & scenario_pid=$! # The model is what says the exchange ran, rather than the processes being up. # The runners retry until the hub answers, so both being alive proves only that neither has given up yet. stored="" for _ in $(coreutils seq 1 60); do for bucket in "$storage"/*/math1-output.json; do if [ -f "$bucket" ]; then stored=$(coreutils cat "$bucket") break fi done if [ -n "$stored" ]; then break; fi coreutils sleep 5 done if [ -z "$stored" ]; then echo "published-verify: no math1-output.json appeared under $storage" >&2 echo "published-verify: check whether the crates and module packages this scenario names are published yet" >&2 # Print where the hub was told each module is installed, beside what is actually there. # The hub serves each npm module from the install path `mise ls --current --json` names, and is silent when that # path is missing, so a module it never found shows up only as a runner's 404 -- this makes a listing that names a # directory the install never created visible. listing=$(cd "$out" && mise ls --current --json) || listing="{}" printf '%s' "$listing" | jaq -r 'to_entries[] | select(.key | startswith("npm:@edge-toolkit/")) | "\\(.key) \\(.value[0].install_path)"' | while read -r tool path; do echo "published-verify: $tool is listed at $path" >&2 coreutils ls -la "$(coreutils dirname "$path")" >&2 || true done # A module whose lock entry has no `aube` graph is listed at the bare version, so show which entries carry one. echo "published-verify: aube graphs recorded in $out/mise.lock:" >&2 rg -n -A6 '^\\[\\[tools\\."npm:@edge-toolkit/' "$out/mise.lock" >&2 || true exit 1 fi printf '%s' "$stored" | cargo run -q -p et-ws-test-server --bin verify-math1 """ shell = "{{ vars.task_shell }}" # End-to-end check that the generated k3s manifests actually run the math1 scenario on a real cluster. # `kubeconform-check` proves the manifests are valid and `verification-check` proves they are stable, and # neither applies them -- a manifest that is both can still describe a deployment that does not work. This # stands up a throwaway k3s cluster (k3d runs one in Docker, so no root and no systemd), applies the scenario, # and asserts the twin stored the same model the mise and compose deployments are held to. # # The images are the caller's to build: they come from this repository rather than a registry, and building # them dwarfs the run itself, so a missing one fails immediately rather than starting a deployment whose pods # can only sit in ErrImagePull. Deliberately not part of the `check` aggregate -- it needs Docker and those # images, which no other check does. # # `--scenario` picks another math1-shaped scenario. One that names machines gets a k3d node per machine, the # first as the server and the rest as agents, each carrying the node label the manifests pin its pods to, and # the run then checks every pod landed on its own machine's node. k3d's nodes are full k3s nodes joined to the # server by token, so the scheduling and the cross-node traffic are real even though one host runs them all. [tasks.k3s-verify] description = "Apply a generated math1 scenario's k3s manifests to a throwaway cluster and verify the stored model" run = """ scenario="${usage_scenario:-math1}" source="${usage_source:-local}" ns="et-$scenario" cluster="et-$scenario-verify" out="verification/$source/output/$scenario" input="verification/$source/input/$scenario.yaml" hub_image="et-ws-server-$scenario:latest" manifest_wait=300s machine_label=edge-toolkit/machine # One name per line; each is an RFC 1123 label, so the unquoted word-splitting loops over it below are safe. machines=$(yq '(.machines // [])[].name' "$input") server_machine=$(yq '.machines[0].name // ""' "$input") # The machines running a hub besides the server, each of which runs `ws-server-`. # Any at all means the hubs share NATS and an object store, which run on the server. other_hubs=$(server="$server_machine" yq '(.services.hub.machines // [])[] | select(. != strenv(server))' "$input") # The runner is the one image that differs between the two sources. # A published deployment names the released one and so has it pulled here rather than built; the scenario # image is built either way, because no release can carry a module set particular to one deployment. Pulling # rather than leaving it to the cluster is what keeps the guard below meaningful: `k3d image import` reads the # local daemon, so an image only the registry has would fail there instead of at the check. if [ "$source" = "published" ]; then runner_image="ghcr.io/edge-toolkit/core/et-ws-web-runner:latest" docker pull "$runner_image" else runner_image="et-ws-web-runner:latest" fi for image in "$hub_image" "$runner_image"; do if ! docker image inspect "$image" >/dev/null 2>&1; then echo "k3s-verify: $image is not built; $out/README.md has the build and import steps" >&2 exit 1 fi done # Generate into a throwaway directory, purely to obtain `secrets.env`. # That file is deliberately uncommitted, so the Secret has no source until it is produced -- and its password # is derived from the scenario input, so a copy generated anywhere carries the same value. Generating beside # the committed manifests instead would rewrite their README with a k3s-only one and show up as drift, and # regenerating the whole tree would drag in the guest toolchains the other scenarios' module paths need. The # manifest applied below is the committed one either way. secrets_dir=$(coreutils mktemp -d) # Diagnostics run before the teardown, and on any failure rather than only a missing model. # A rollout that never completes exits the task under `set -e` several steps before the model is looked for, # so without this the only output is `timed out waiting for the condition` -- and the cluster that could have # explained it is already gone. dump_state() { echo "k3s-verify: cluster state at failure" >&2 kubectl get pods -n "$ns" >&2 || true kubectl describe pods -n "$ns" >&2 || true # A label selector, because `kubectl logs` needs a resource and every pod here carries an `app` label. kubectl logs -n "$ns" --all-containers --tail=40 --prefix --selector app >&2 || true # Each k3d node's own log, which is the only record of a node that never joined the cluster. # kubectl cannot see such a node at all. Container ids rather than a `--format` template, because a Go template's # braces would reach Tera, which renders this body first. for node in $(docker ps --all --quiet --filter "label=k3d.cluster=$cluster"); do echo "k3s-verify: log of k3d node container $node" >&2 docker logs --tail 60 "$node" >&2 || true done } # Installed before the credential is generated, not after. # The trap is what removes the directory holding it, so generating first would leave the password on disk for # any failure between the two. trap 'status=$?; if [ "$status" -ne 0 ]; then dump_state; fi k3d cluster delete "$cluster" >/dev/null 2>&1 || true coreutils rm -rf "$secrets_dir"' EXIT cargo run -q -p et-cli -- generate-deployment --input-file "$input" --output-dir "$secrets_dir" # The first machine is the server node and every other one an agent. # Each is labelled as the README's join commands label them. A scenario naming no machines gets no flags, which # is the single-node cluster it always had. # The flags are collected as positional parameters, the one list every task shell has. set -- agents=0 node="server:0" for machine in $machines; do set -- "$@" --k3s-node-label "$machine_label=$machine@$node" if [ "$node" != "server:0" ]; then agents=$((agents + 1)) fi node="agent:$agents" done # Every node gets the containerd template a scenario ships when its limits need the container runtime plugins. # The template turns on the check refusing a container they did not adjust, and the generated README has every # machine install it. template="$out/containerd-config-v3.toml.tmpl" if [ -f "$template" ]; then set -- "$@" --volume "$PWD/$template:/var/lib/rancher/k3s/agent/etc/containerd/config-v3.toml.tmpl@all" fi k3d cluster delete "$cluster" >/dev/null 2>&1 || true # Bounded, so a node that never comes up fails the task with its log dumped rather than running out the CI step. # Unbounded, an agent hung at `Starting node 'k3d-et-math1-multi-verify-agent-0'` until the step's 20-minute limit # on commit https://github.com/edge-toolkit/core/commit/e569b7f337314b70c60b76f9bfd668c8cf094417 at # https://github.com/edge-toolkit/core/actions/runs/38818439394/job/116708831502, with nothing logged to say why. # `--no-rollback` keeps the node containers when the timeout fires: k3d's own rollback would otherwise delete them # before the exit trap reads their logs, and the trap deletes the cluster itself once it has. k3d cluster create "$cluster" --wait --timeout 300s --no-rollback --agents "$agents" "$@" # `--wait` returns once k3s logs that it started, which can be before its containerd and API server serve. # Imported then, the image never lands -- k3d logs the failure and still reports success -- and the next kubectl # call is refused, as on commit https://github.com/edge-toolkit/core/commit/9d140c69a519d2221d41848e09a303311d72205b # at https://github.com/edge-toolkit/core/actions/runs/38880115734/job/116904536884: # ctr: cannot access socket /run/k3s/containerd/containerd.sock: stat /run/k3s/containerd/containerd.sock: no # such file or directory # Error from server (ServiceUnavailable): apiserver not ready # A node reports Ready only once its kubelet is running against containerd, so both are up after this. for _ in $(coreutils seq 1 60); do if kubectl get --raw=/readyz >/dev/null 2>&1; then break fi coreutils sleep 5 done kubectl wait --for=condition=Ready node --all --timeout="$manifest_wait" # `k3d image import` loads every node, so a pod finds its image whichever machine it is pinned to. k3d image import -c "$cluster" "$hub_image" "$runner_image" # --save-config so the `kubectl apply` below does not warn about adopting a namespace it did not create. kubectl create namespace "$ns" --save-config kubectl create secret generic "$ns-secrets" --from-env-file="$secrets_dir/secrets.env" -n "$ns" kubectl apply -f "$out/k3s.yaml" deployments="openobserve ws-server" if [ -n "$other_hubs" ]; then deployments="$deployments nats rustfs" for machine in $other_hubs; do deployments="$deployments ws-server-$machine" done fi for deployment in $deployments; do kubectl rollout status "deploy/$deployment" -n "$ns" --timeout="$manifest_wait" done # The runners restart until the hub answers, so the model is what says the exchange ran -- not pod readiness. # # Only the first bucket is read. Both agents register and each gets its own storage bucket, so the glob matches # two identical `math1-output.json` files once the exchange completes, and `cat`ting both yields two # concatenated JSON documents rather than one. The sibling mise and compose test reads the first match for the # same reason. stored="" # The glob, `$1` and the `cat` are all for the pod's shell, so they must reach it unexpanded. # That shell runs in the hub image, where no mise-managed coreutils exists. # shellcheck disable=SC2016 # nosemgrep: config.semgrep.mise-task-coreutils-prefix read_model='set -- /app/storage/*/math1-output.json; cat "$1"' if [ -n "$other_hubs" ]; then # Hubs that share storage keep it in the object store, so the server's hub reads the model back through S3. # It uses the credentials it was given, listing the bucket for the first key that holds one. # The twin stored it through the other hub, so reading it here is what shows the store is shared. # rustfs refuses a request without `x-amz-content-sha256`, which the image's curl does not send on its own: # InvalidRequestmissing header: x-amz-content-sha256 # shellcheck disable=SC2016 # nosemgrep: config.semgrep.mise-task-coreutils-prefix read_model='s3() { login="$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" unsigned="x-amz-content-sha256: UNSIGNED-PAYLOAD" curl -fsS --aws-sigv4 "aws:amz:$AWS_DEFAULT_REGION:s3" --user "$login" -H "$unsigned" "$AWS_ENDPOINT/et-storage/$1" } key=$(s3 "?list-type=2" | grep -o "[^>]*/math1-output[.]json" | head -n 1) [ -n "$key" ] && s3 "$key"' fi for _ in $(coreutils seq 1 60); do stored=$(kubectl exec -n "$ns" deploy/ws-server -- sh -c "$read_model" 2>/dev/null || true) if [ -n "$stored" ]; then break fi coreutils sleep 5 done if [ -z "$stored" ]; then echo "k3s-verify: no math1-output.json appeared in the hub's storage" >&2 exit 1 fi printf '%s' "$stored" | cargo run -q -p et-ws-test-server --bin verify-math1 if [ -f "$template" ]; then mise run verify-limits "$scenario" k3s fi # Every pod has to be on the node labelled as its machine. # The collector, the server's hub and what several hubs share belong on the server, any other machine's hub on # that machine, and each listed agent's runner on the machine listing it. # Checked after the exchange, when every pod is scheduled. listed='.machines[] | select(.name == strenv(machine)) | .agents[]' for machine in $machines; do apps=$(machine="$machine" yq "$listed" "$input") if [ "$machine" = "$server_machine" ]; then apps="openobserve ws-server $apps" if [ -n "$other_hubs" ]; then apps="nats rustfs $apps" fi fi for hub_machine in $other_hubs; do if [ "$machine" = "$hub_machine" ]; then apps="ws-server-$machine $apps" fi done expected=$(kubectl get nodes -l "$machine_label=$machine" -o jsonpath='{.items[0].metadata.name}') for app in $apps; do placed=$(kubectl get pods -n "$ns" -l "app=$app" -o jsonpath='{.items[0].spec.nodeName}') if [ "$placed" != "$expected" ]; then echo "k3s-verify: $app runs on $placed, not on $expected, the node of machine $machine" >&2 exit 1 fi done done """ shell = "{{ vars.task_shell }}" usage = """ arg "[source]" help="Which verification tree to apply: local (default) or published" flag "--scenario " help="Which math1-shaped scenario to apply" default="math1" """ # End-to-end check that a generated compose deployment runs the math1 exchange, one machine at a time. # Each machine's profile is brought up by its own `docker compose up`, the server's first, which is what the # README tells each machine to run -- so the runners off the server take the paths only a multi-machine stack # has: the server's address instead of `localhost`, no healthy-hub gate, and a restart until the hub answers. The # scenario's machines are all at loopback, which is what lets one host play every one of them. A scenario naming # no machines comes up in one `up`. Not part of `check`, for the reasons `k3s-verify` gives. [tasks.compose-verify] description = "Start a generated math1 compose scenario one machine at a time and verify the stored model" run = """ scenario="${usage_scenario:-math1-multi}" out="verification/local/output/$scenario" input="verification/local/input/$scenario.yaml" # One name per line; each is an RFC 1123 label, so the unquoted word-splitting loops over it below are safe. machines=$(yq '(.machines // [])[].name' "$input") secrets_dir=$(coreutils mktemp -d) compose() { docker compose --project-name "et-$scenario-verify" -f "$out/compose.yaml" "$@" } # Every machine's profile ahead of the given arguments, for the commands that address the whole stack. # `down` needs them too, since a service in an inactive profile is otherwise left running. compose_all() { for machine in $machines; do set -- --profile "$machine" "$@" done compose "$@" } dump_state() { echo "compose-verify: stack state at failure" >&2 compose_all ps --all >&2 || true compose_all logs --tail=40 >&2 || true } trap 'status=$?; if [ "$status" -ne 0 ]; then dump_state; fi compose_all down --volumes >/dev/null 2>&1 || true coreutils rm -rf "$secrets_dir"' EXIT # The stack reads its credential from `secrets.env` beside the compose file, which is uncommitted. # It is generated in a throwaway directory and copied in, for the reason `k3s-verify` generates its own. It is # left there afterwards: it is the same derived value `regen-verification` writes into that directory, and # gitignored there. cargo run -q -p et-cli -- generate-deployment --input-file "$input" --output-dir "$secrets_dir" coreutils cp "$secrets_dir/secrets.env" "$out/secrets.env" if [ -z "$machines" ]; then compose up --detach --build fi for machine in $machines; do compose --profile "$machine" up --detach --build done stored="" # The glob, `$1` and the `cat` are all for the container's shell, so they must reach it unexpanded. # That shell runs in the hub image, where no mise-managed coreutils exists. # shellcheck disable=SC2016 # nosemgrep: config.semgrep.mise-task-coreutils-prefix read_model='set -- /app/storage/*/math1-output.json; cat "$1"' for _ in $(coreutils seq 1 60); do stored=$(compose_all exec -T ws-server sh -c "$read_model" 2>/dev/null || true) if [ -n "$stored" ]; then break fi coreutils sleep 5 done if [ -z "$stored" ]; then echo "compose-verify: no math1-output.json appeared under /app/storage" >&2 exit 1 fi printf '%s' "$stored" | cargo run -q -p et-ws-test-server --bin verify-math1 # Hold the running containers to the limits the k3s runtime plugins would write, when the scenario sets any. # The template is written exactly when it does, which is when there is something for compose to be held to. if [ -f "$out/containerd-config-v3.toml.tmpl" ]; then mise run verify-limits "$scenario" compose fi """ shell = "{{ vars.task_shell }}" usage = """ flag "--scenario " help="Which math1-shaped local scenario to start" default="math1-multi" """ # Assert a running scenario's containers hold every limit its k3s manifest has the runtime plugins write. # The manifest's plugin annotations are the expectation for both formats. They are exactly what k3s writes into each # container's cgroup and rlimits, so a compose container reading back the same values is the parity check itself, made # on a live kernel rather than in generated text. `k3s-verify` and `compose-verify` run it against their own stack # while it is still up; on its own it has nothing to read. A manifest naming no plugin fails it, since a check that # compares nothing would pass for a scenario whose limits had silently stopped rendering. [tasks.verify-limits] description = "Check a running scenario's containers hold the cgroup values and rlimits its k3s manifest declares" run = """ scenario="$usage_scenario" format="$usage_format" out="verification/local/output/$scenario" manifest="$out/k3s.yaml" ns="et-$scenario" project="et-$scenario-verify" # The values are read from a probe of each limited process rather than from the process itself. # A runner exits once its module has run, so by the time the exchange is verified there may be no container left to # read; the probe is the same service definition -- the same limits and, under k3s, the same plugin annotations -- # with its command replaced by a sleep, and so lives as long as the check needs it. Under k3s it is a bare Pod cut from # the Deployment's template, relabelled so the Deployment's ReplicaSet does not adopt it as a surplus replica. probe_pod='select(.kind == "Deployment" and .metadata.name == strenv(app)) | .spec.template | .apiVersion = "v1" | .kind = "Pod" | .metadata.name = strenv(app) + "-limits-probe" | .metadata.labels.app = strenv(app) + "-limits-probe" | .spec.restartPolicy = "Never" | .spec.containers[0].command = ["sleep", "600"] | del(.spec.containers[0].args) | del(.spec.containers[0].readinessProbe)' limited='select(.kind == "Deployment") | select((.spec.template.metadata.annotations // {}) | keys | any_c(test("memory-qos|ulimits"))) | .metadata.name' # `--no-doc` throughout, because yq otherwise separates each document's lines with a `---` of its own. apps=$(yq --no-doc "$limited" "$manifest") cleanup() { for app in $apps; do if [ "$format" = "k3s" ]; then kubectl delete pod -n "$ns" "$app-limits-probe" --wait=false >/dev/null 2>&1 || true else docker rm --force "$project-$app-limits-probe" >/dev/null 2>&1 || true fi done } trap cleanup EXIT for app in $apps; do if [ "$format" = "k3s" ]; then app="$app" yq --no-doc "$probe_pod" "$manifest" | kubectl apply -n "$ns" -f - >/dev/null kubectl wait -n "$ns" --for=condition=Ready "pod/$app-limits-probe" --timeout=120s >/dev/null else set -- run --detach --no-deps --name "$project-$app-limits-probe" --entrypoint sleep "$app" 600 docker compose --project-name "$project" -f "$out/compose.yaml" "$@" >/dev/null fi done # Run a command in the probe of the named process. # Stdin is closed so the command cannot drain the loop feeding this function its expectations. in_container() { app="$1" shift if [ "$format" = "k3s" ]; then kubectl exec -n "$ns" "$app-limits-probe" -- "$@" ` line per plugin annotation, from every Deployment of the manifest. # `$app` is yq's own variable, bound by `as`, so it must reach yq unexpanded. # shellcheck disable=SC2016 annotations='select(.kind == "Deployment") | .metadata.name as $app | (.spec.template.metadata.annotations // {}) | to_entries[] | $app + " " + .key + " " + .value' # The same for each rlimit, its value the soft and hard limits the ulimits annotation lists. # shellcheck disable=SC2016 rlimits='select(.kind == "Deployment") | .metadata.name as $app | (.spec.template.metadata.annotations // {}) | to_entries[] | select(.key | contains("ulimits.noderesource.dev/")) | .value | from_yaml | .[] | $app + " " + (.type | downcase | sub("rlimit_"; "")) + " " + (.soft | tostring) + " " + (.hard | tostring)' checked=0 while read -r app key expected; do case "$key" in *.memory-qos.nri.io/*) ;; *) continue ;; esac file="${key%%.memory-qos.nri.io/*}" actual=$(in_container "$app" cat "/sys/fs/cgroup/$file") if [ "$actual" != "$expected" ]; then echo "verify-limits: $app has $file '$actual' under $format, not the '$expected' its manifest declares" >&2 exit 1 fi checked=$((checked + 1)) done < <(yq --no-doc "$annotations" "$manifest") while read -r app resource soft hard; do actual=$(in_container "$app" prlimit --pid 1 "--$resource" --output SOFT,HARD --noheadings | coreutils tr -s ' ') actual="${actual# }" if [ "$actual" != "$soft $hard" ]; then echo "verify-limits: $app has $resource '$actual' under $format, not the '$soft $hard' its manifest declares" >&2 exit 1 fi checked=$((checked + 1)) done < <(yq --no-doc "$rlimits" "$manifest") if [ "$checked" -eq 0 ]; then echo "verify-limits: $manifest names no limit for the runtime plugins to write, so there was nothing to check" >&2 exit 1 fi echo "verify-limits: $checked limits hold under $format" """ shell = "{{ vars.task_shell }}" usage = """ arg "" help="The local scenario whose running containers to check" arg "" help="Which deployment is running it: k3s or compose" """ # Hardening checks over the generated Kubernetes manifests, mirroring what Codacy reports on a pushed branch. # Codacy's three findings are trivy's KSV-0001 (privilege escalation), KSV-0014 (writable root filesystem) and # KSV-0118 (no security context); trivy adds seccomp, capabilities, resource bounds and UID/GID on top, so this # is a superset of the external check rather than a copy of it. `kubeconform-check` answers whether a cluster # would accept these manifests; this answers whether it should. # # Dockerfiles are covered as well as the manifests. hadolint already lints them, but for different things: # hadolint is style and shell correctness, trivy is image posture -- running as root, an unpinned tag, a # missing healthcheck. The two overlap on almost nothing. # # One scan of the whole repository rather than a list of files, so a Dockerfile or manifest added later is # covered without anyone remembering to extend a glob. It also keeps the reported paths repo-relative, which # is what lets config/trivyignore.yaml scope a suppression to a directory: scanning a single file makes trivy # report that file as the scan root, so any path pattern longer than its basename matches nothing. # # `target` has to be skipped explicitly -- trivy does not read .gitignore, and walking tens of gigabytes of # build output takes the scan from 23 seconds to five minutes without finding one extra file. [tasks.trivy-check] alias = "trivy" description = "Check every Dockerfile and generated Kubernetes manifest for hardening findings" run = """ set -- --skip-dirs target --skip-dirs '**/node_modules' trivy config --quiet --exit-code 1 --ignorefile config/trivyignore.yaml "$@" . """ shell = "{{ vars.task_shell }}" [tasks.repo-check] description = "Repository-wide checks that need to read git itself (commit hashes, ...)" run = "cargo run -q -p et-int-repo-check" [tasks.link-check] description = "Check that URLs in prose, source and config files are reachable (network)" # The .rs glob is scoped to the source dirs, not a bare `**/*.rs`. # A recursive `**/*.rs` walks target/'s churning rustc temp files, and lychee expands globs before applying # `exclude_path`, so it aborts with a GlobError when one of those temps vanishes mid-iteration. # # The config, workflow and Dockerfile globs are here because those files carry links nothing else validates. # Every commit a workaround comment cites is written as a `/commit/` URL, and most of them live in a mise # config, a workflow or a Dockerfile rather than in prose -- so with only the .md and .rs globs the majority of # this repo's commit references were checked by nothing at all. run = """ cfg=config/lychee.toml # The README's Codecov dashboard link is skipped while Codecov's Cloudflare-fronted hosts refuse TLS handshakes. # That outage is https://github.com/codecov/codecov-action/issues/1975, open since 2026-10-05. Every dashboard URL # redirects to app.codecov.io, one of those hosts, so the check reports # [ERROR] https://codecov.io/gh/edge-toolkit/core (at 3:32) | Network error: received fatal alert: # HandshakeFailure | Followed 1 redirect. # on GitHub's runners at https://github.com/edge-toolkit/core/actions/runs/37251999021/job/111581371873 and # locally alike. Drop the skip once that issue is resolved. Only that exact URL is skipped: the badge beside it is # served by codecov.io itself and names the same project, so it still fails if the project moves or disappears. # It lives here rather than in the lychee config because the service may only be named in the coverage tooling. dashboard='^https://codecov\\.io/gh/edge-toolkit/core$' # Links to this repo's own files on main are checked against the working tree instead of github.com. # A file the branch adds 404s there until it merges, and one the branch deletes still resolves there, so the live # check reports the first and misses the second. The file URL takes the same cross-platform form as taplo-check's. blob='^https://github\\.com/edge-toolkit/core/blob/main/(.*)$ '"file:///${PWD#/}/\\$1" set -- --exclude "$dashboard" --remap "$blob" lychee --config "$cfg" "$@" '**/*.md' 'libs/**/*.rs' 'services/**/*.rs' 'utilities/**/*.rs' # The other languages' sources are named by git rather than globbed, which keeps their build output out as well. src=$(git ls-files -- libs services utilities | rg '\\.(py|js|mjs|ts|dart|kt|cs|java|zig|c|cpp|h)$') # $src is a newline-separated path list, split into one argument per path by design. # shellcheck disable=SC2086 lychee --config "$cfg" "$@" $src # Everything except the commit URLs is excluded on the second pass, by `--exclude .*` with `--include` winning. # Those files hold links this repo does not otherwise own -- release-asset bases joined to a filename at run # time, doc placeholders, a regex quoted inside a comment -- and none of them is a URL a reader could follow. # Checking the whole set reports 21 such fragments as broken. `--scheme https` drops the `file:` and # `docker-image:` values for the same reason, and every commit URL is https. conf=".mise/config*.toml .mise/*.sh config/*.toml Cargo.toml .deepsource.toml" conf="$conf $(git ls-files 'config/*.yaml' 'config/*.rego' | coreutils tr '\\n' ' ')" gha=".github/workflows/*.yaml .github/actions/*/*.yaml" # k3s.yaml is skipped as a whole because one value in it cannot be filtered any other way. # It passes a BuildKit named-context reference to `docker build`, and the `:latest` tag on the end reads as # an invalid port number, so lychee fails to parse the token -- which happens before `--scheme` or # `--exclude` get a say, and is why neither can reach it. Naming the value here would reproduce the problem # in this file. The workflow carries no commit URLs today; one added there would go unchecked, which is the # cost of the skip. skip="--exclude-path .github/workflows/k3s.yaml" # The flag patterns stay quoted while the glob lists are deliberately split and expanded by the shell. # `--exclude .*` unquoted is a glob the shell resolves against the working directory, which hands lychee every # dotfile as an input -- including its own `.lycheecache`, whose every line then reads as a broken URL. # shellcheck disable=SC2086 lychee --config "$cfg" --scheme https --exclude '.*' --include '/commit/[0-9a-f]{40}' $skip $conf $gha Dockerfile* """ shell = "{{ vars.task_shell }}" [tasks.ryl-check] description = "Lint YAML with ryl (a yamllint-compatible Rust linter)" run = "ryl check -c config/ryl.toml ." [tasks._setup_all] description = "Private shared cross-platform preinstall" hide = true run = """ # gpg must land BEFORE `mise install node` below. # node's gpg-verify path (mise/src/plugins/core/node.rs) hard-fails when gpg_verify is enabled and gpg # isn't reachable. Per-platform package: conda:gnupg on Linux/macOS (conda-forge has no win-64 build), # conda:m2-gnupg on Windows (msys2 mirror package; config.windows.toml adds its bin to `_.path` so # subprocess PATH picks up gpg.exe -- otherwise mise's per-tool .mise-bins misses the nested `Library/usr/bin` layout). {% if os() == 'windows' %}mise install conda:m2-gnupg{% else %}mise install conda:gnupg{% endif %} mise install gh mise install cargo-binstall mise install node mise install conda:openssl mise install github:uutils/coreutils mise install ripgrep # goawk lands here in _setup_all so it's available to every later step. # Steps that may need `awk` -- third-party scripts (dotnet-install.sh on Linux, others) hard-code bare # `awk`, and the per-OS preinstalls below symlink mise's `goawk` into `awk` on hosts where the base image # lacks one (Azure Linux base/core notably). Installing here, not in the per-OS preinstall body, keeps the # `mise which goawk` call those use guaranteed to resolve. mise install github:benhoyt/goawk # rclone is used by the rustpython pipx bootstrap in config.windows.toml's preinstall. # That preinstall runs before the main `mise install` pass. Land it here in _setup_all so the bootstrap # doesn't trip with `rclone: command was not found`. mise install rclone """ shell = "{{ vars.task_shell }}" [tasks.osv-scanner-check] # Scans every committed lockfile across languages for known vulnerabilities. # The scanned set is vars.osv_locks. No build dependency, so the same task serves both ad-hoc local use and the # dependencies workflow. alias = "osv-scanner" run = "osv-scanner {{ vars.osv_locks }} --config config/osv-scanner.toml" [tasks."gen:osv-scanner"] description = "Regenerate config/osv-scanner.toml from config/deny.toml's [advisories].ignore list" # osv-scanner and cargo-deny must ignore the same advisory IDs. # config/deny.toml is the source of truth (it carries the per-ID rationale). Both RUSTSEC-YYYY-NNNN and # GHSA-xxxx-xxxx-xxxx ids are extracted, so a GHSA-only advisory (one RustSec hasn't assigned) still filters. # # rg + coreutils ONLY, and that is a hard constraint rather than a preference. # The `dependencies` workflow does no `mise install`: it names its six tools outright in the install-mise # step, so anything outside that list is simply absent. A goawk rewrite of this task looked tidier and died # there with `goawk: command not found`, and goawk cannot join that list either -- it is a Go binary, absent # from taiki-e/install-action's manifest and not a crate cargo-binstall could fetch. Keep to the tools the # workflow already installs, or add the tool to the workflow first and prove it resolves. # # One pattern, matching an id and the review date beside it, because every ignore entry carries one: the # advisories conftest policy fails the build for any that does not. The date becomes osv-scanner's native # `ignoreUntil`, which it enforces itself, so a lapsed entry stops being honoured rather than silently # outliving its review. run = """ { # backticks in the echoed line are literal markdown, not command substitution. # shellcheck disable=SC2016 echo '# AUTO-GENERATED from config/deny.toml by `mise run gen:osv-scanner`.' echo '# The list spans every language, so "has unused ignores" warnings are expected: agents must ignore them.' echo 'IgnoredVulns = [' id_re='"(RUSTSEC-[0-9]{4}-[0-9]{4}|GHSA(?:-[0-9a-z]{4}){3})"' date_re='reason = "expires ([0-9]{4}-[0-9]{2}-[0-9]{2})"' rg -oN -r ' { id = "$1", ignoreUntil = $2 },' "$id_re, $date_re" config/deny.toml | coreutils sort -u echo ']' } >config/osv-scanner.toml """ shell = "{{ vars.task_shell }}" [tasks.cargo-deny-check] description = "Audit dependencies with cargo-deny (advisories, bans, licenses, sources)" # Escalate `no-license-field` to a hard error, except where clarify-allowlisted. # The exception is crates with an explicit clarify entry in config/deny.toml. cargo-deny has no per-crate # diagnostic-level config, so we post-filter its output: any `warning[no-license-field]` whose crate isn't # allowlisted (currently just `saffron`) fails the task. A new transitive dep without an SPDX `license` # expression must then land in clarify intentionally, not silently. run = """ rc=0 out="$(cargo deny --config config/deny.toml check 2>&1)" || rc=$? echo "$out" extra="$(echo "$out" | rg 'warning\\[no-license-field\\]' | rg -v 'saffron' || true)" if [ -n "$extra" ]; then echo "ERROR: no-license-field warning for crate not in clarify allowlist:" >&2 echo "$extra" >&2 exit 1 fi exit "$rc" """ shell = "{{ vars.task_shell }}" [tasks.cargo-unmaintained-check] description = "Flag dependencies whose upstream repo is archived or unmaintained" # cargo-unmaintained is deliberately NOT a mise [tool]. # It has no prebuilt and a from-source `cargo install` is too heavy to pay on every `mise install`. The # `dependencies` workflow installs it via taiki-e/install-action; this task just runs the binary so the # audit command lives beside the others. cargo-unmaintained needs a GitHub token to check each upstream # repo's archival status: the CI workflow already sets GITHUB_TOKEN, so the `:=` default is a no-op there; # locally (where the env var is unset) it falls back to `gh auth token`, so a dev never has to export one by # hand. `set -u` is satisfied because `${VAR:=...}` assigns before the reference. run = """ : "${GITHUB_TOKEN:=$(gh auth token)}" export GITHUB_TOKEN cargo unmaintained """ shell = "{{ vars.task_shell }}" [tasks.cargo-unmaintained-check-pkg] description = "Check one dependency with cargo-unmaintained (cheap re-check of a single finding)" # Only the named package's upstream is queried, rather than one GitHub lookup per crate in the lockfile. run = """ : "${GITHUB_TOKEN:=$(gh auth token)}" export GITHUB_TOKEN cargo unmaintained -p "$usage_package" """ shell = "{{ vars.task_shell }}" usage = 'arg "" help="Dependency package name as it appears in Cargo.lock (e.g. chrono-tz)"' [tasks."gen:dockerignore"] description = "Regenerate .dockerignore from .gitignore + Docker-only excludes" # Docker reads only the root .dockerignore (never .gitignore or nested ignore files). # A bare pattern there is root-anchored -- whereas a slash-less .gitignore pattern matches at any depth. # So mirror .gitignore, add the Docker-only excludes (`.git/`, plus the build recipe + docs so editing # them doesn't bust the `COPY . .` cache), and `**/`-prefix each non-anchored pattern. # `.dockerignore` itself is deliberately NOT excluded any more. Excluding it saved nothing (the file is tiny) and # meant it was absent from the build context, so `et-cli` -- which derives each scenario's per-Dockerfile ignore # file from it -- could not run inside an image built from this Dockerfile: # called `Result::unwrap()` on an `Err` value: Io(... NotFound ... path: "/workspace/.dockerignore") # That surfaced only in the docker build.yaml lanes, because every other environment has the file on disk. # goawk emits CRLF on Windows, so the whole block is piped through `tr -d '\r'` -- .dockerignore must stay # LF-only (editorconfig enforces it, and CRLF would also make dockerignore-check see a spurious diff). run = ''' { echo "# AUTO-GENERATED from .gitignore by 'mise run gen:dockerignore' -- do not edit." echo "# Docker reads only this file; patterns are **/-prefixed to match at any depth" echo "# like .gitignore. Edit .gitignore and regenerate." echo { coreutils cat .gitignore printf '%s\n' '.git/' 'Dockerfile*' '/README.md' } | goawk ' /^[[:space:]]*#/ { print; next } /^[[:space:]]*$/ { print; next } { neg = ""; line = $0 if (substr(line, 1, 1) == "!") { neg = "!"; line = substr(line, 2) } if (substr(line, 1, 1) == "/") { print neg substr(line, 2); next } core = line; sub(/\/$/, "", core) if (index(core, "/") > 0) print neg line; else print neg "**/" line } ' } | coreutils tr -d '\r' >.dockerignore ''' shell = "{{ vars.task_shell }}" [tasks.dockerignore-check] depends = ["gen:dockerignore"] description = "Fail if .dockerignore is stale vs .gitignore (run `mise run gen:dockerignore`)" run = "git diff --exit-code -- .dockerignore" # Fail when HEAD does not already contain origin/main, because a stale branch makes every other check untrustworthy. # A branch behind main runs its lints against code the rest of the repo has already moved past, so failures land in # files the branch never touched and read as though the branch broke them. No linter can express this -- it is git # topology rather than file content -- so it is a small git compare, in the same spirit as dockerignore-check above. # Deliberately does not fetch: checks make no network calls, and a stale origin/main can only cause a missed warning, # never a spurious failure. Skips when it cannot tell rather than guessing. CI checks out with `fetch-depth: 1`, so # origin/main is simply absent there and the honest answer is "unknown" -- GitHub's own up-to-date-branch protection # governs merging. This guard exists for developer machines, which is where the confusing failures happen. [tasks.branch-up-to-date-check] description = "Fail if the current branch does not already contain origin/main (rebase needed)" run = """ if ! git rev-parse --verify --quiet origin/main >/dev/null; then echo "branch-up-to-date-check: origin/main not available (shallow clone?); skipping" exit 0 fi if git merge-base --is-ancestor origin/main HEAD; then exit 0 fi behind="$(git rev-list --count HEAD..origin/main)" echo "branch-up-to-date-check: HEAD is behind origin/main by $behind commit(s)." echo "Other checks may fail in files this branch never touched. Rebase first: git pull --rebase origin main" exit 1 """ shell = "{{ vars.task_shell }}" # The repository's GitHub code-scanning list must be empty; every open alert, listed one per line, fails this. # `{owner}/{repo}` is filled in by gh from the checkout's remote. [tasks.code-scanning-check] description = "Fail if the GitHub repository has any open code-scanning alert" run = """ url='repos/{owner}/{repo}/code-scanning/alerts?state=open&per_page=100' where='.most_recent_instance.location | "\\(.path):\\(.start_line)"' line='.[] | "#\\(.number) \\(.rule.id) " + ('"$where"')' alerts="$(gh api --paginate "$url" --jq "$line")" if [ -z "$alerts" ]; then exit 0 fi echo "code-scanning-check: open alerts, from gh api '$url':" echo "$alerts" exit 1 """ shell = "{{ vars.task_shell }}" [tasks.prefetch-ci] # `build-ws-wasm-agent` produces `services/ws-wasm-agent/pkg/`. # The static workspace references it via a `link:` path. Run it before `pnpm install` so the link target # exists. Only `fetch-mnist-rclone` is pulled here -- CI tests don't exercise the face1 / har-motion1 # models, and skipping those avoids the HuggingFace rate-limit that bites Windows runners. # # `prefetch:rust` + `prefetch:node` live in this default config (always loaded), so the `prefetch:*` glob # never matches zero; guest-language `prefetch:` tasks join from their loaded MISE_ENV configs. # Native `depends` glob (not a nested `mise run`) so each task's tools are activated -- a nested run left # `dart`/`zig` off PATH on Windows. depends = ["fetch-mnist-rclone", "prefetch:*"] description = "Pre-download CI-required dependencies + the MNIST model" [tasks.prefetch] # "dprint-check" is here to trigger download of the dprint plugin WASM files. depends = ["dprint-check", "fetch-eye1-rclone", "fetch-face1-rclone", "fetch-har-motion1-rclone", "prefetch-ci"] description = "Pre-download all dependencies and models (superset of prefetch-ci)" [tasks."prefetch:rust"] description = "Prefetch the Rust (cargo) dependencies" # `cargo fetch` downloads the full Cargo.lock graph (all targets) without compiling. # The actual build happens in build-modules / the test step. Avoids # the redundant `cargo check --workspace` compile (and its large target/ dir). # # Declaring a bash `shell` is load-bearing on Windows: as a base-config task, mise's child-cmd PATH composition # drops the shim-dir / `~/.cargo/bin` entries (the same dropout the build-et-cli task declares one to # avoid). Captured on 3ac0d60b, windows job 82277109482 -- bare `cargo` resolved via pwsh `where.exe` # in the parent step but the cmd subprocess mise spawned didn't see it. Bash (ash via MISE_BASH_PATH on # Windows) inherits the right PATH. run = "cargo fetch" shell = "{{ vars.task_shell }}" [tasks.build-ws-wasm-agent] description = "Build the WebSocket WASM client" dir = "services/ws-wasm-agent" # {{ vars.no_opt }} is --no-opt on Windows (wasm-opt can't run on Nano), empty elsewhere. # The served, size-optimised artifact is built on Linux/macOS. Stable toolchain build: drops the nightly # `-Z build-std=std,panic_abort`. Without build-std the precompiled std doesn't get the mvp target-feature # treatment, but the agent doesn't need that for correctness. Lives in the global config (not config.rust.toml) # so other globally-loaded tasks -- prefetch-ci's transitive deps via `prefetch:node` -- can reference it # without forcing `rust` into MISE_ENV. A bash `shell` for the same base-config PATH-dropout reason # `prefetch:rust` carries one. run = "wasm-pack build . --target web {{ vars.wasm_scope }}{{ vars.no_opt }}" shell = "{{ vars.task_shell }}" [tasks."prefetch:node"] # Needs `build-ws-wasm-agent` first. # The static member `link:`s its `pkg/`, so the link target must exist before `pnpm install`. depends = ["build-ws-wasm-agent"] description = "Prefetch the Node (pnpm) workspace" # pnpm refuses non-TTY destructive node_modules cleanup unless CI=true. # That cleanup fires when an earlier install left a different layout. Treat as a CI batch. env = { CI = "true" } # Installs the whole workspace from the repo root, so no --dir. # The members come from pnpm-workspace.yaml and the resolution lands in the single root pnpm-lock.yaml. run = "pnpm install --ignore-scripts" # A bash `shell` on Windows for the same base-config-task PATH dropout. # Same dropout as `prefetch:rust` and `build-et-cli` -- cmd subprocess can't find pnpm. Captured on # 3ac0d60b windows test job 82282265424 (after the prefetch:rust fix, the same failure surfaced one step # downstream in prefetch:node). shell = "{{ vars.task_shell }}" [tasks.regenerate-verification] alias = "regen-verification" description = "Regenerate checked-in verification output files" run = "cargo run -p et-cli -- regen-verification" [tasks.verification-check] depends = ["regenerate-verification"] description = "Fail if generated scenario output files are stale" run = "git diff --exit-code -- verification/" [tasks.gen-help-all] description = "Regenerate HELP.md for every binary crate using clap or reading serde-env config" run = "cargo run -q -p et-int-gen --bin et-int-gen -- help-md" [tasks."gen:checks"] description = "Regenerate CHECKS.md, the catalogue of every check across every MISE_ENV, stamped with today's date" run = "cargo run -q -p et-int-gen --bin et-int-gen -- checks" # Holds CHECKS.md to its generation date rather than re-rendering it, so it costs no build on any lane. # `--parser ignore` because Markdown has no parser; it hands the file over as its lines, which is all this reads. [tasks.checks-md-check] description = "Fail if CHECKS.md was last generated more than 7 days ago (run `mise run gen:checks`) (conftest)" run = "conftest test --combine --parser ignore --namespace checks_md -p config/conftest/policy CHECKS.md" [tasks.gen-help-check] # Ordered after the workspace builds so its `cargo run` reuses their artifacts. # Run beside them it is a fourth concurrent build, which doubled every cargo task's time on the three-core macOS # runner and took the macOS check lane past its budget. depends = ["cargo-check", "cargo-clippy-check", "cargo-doc-check", "regenerate-verification"] description = "Fail if any checked-in HELP.md is stale vs its clap tree or env config (run `mise run gen-help-all`)" run = "cargo run -q -p et-int-gen --bin et-int-gen -- help-md --check" [tasks.fetch-wit-deps] description = "Fetch upstream WASI WIT packages into generated/specs/wit/ at pinned versions (run by hand on bumps)" run = "cargo run -q -p et-int-gen --bin et-int-gen -- fetch-deps" [tasks."gen:ws-spec"] description = "Run et-int-gen: emit the language-agnostic WS specs (YAML/WIT/KDL/JSON) + the Rust REST client" # et-int-gen runs progenitor + prettyplease to produce generated/rust-rest/src/lib.rs. # prettyplease's style disagrees with our nightly rustfmt (different import grouping + line width), so # the workspace `mise run fmt` would reformat it and `mise run gen-specs-check` would see drift on the # next regen. Make rustfmt the canonical formatter by running it here as a post-process -- the committed # file is then a fixed point of both tools. # # `generate core` emits the language-agnostic specs every per-language `gen:*` task consumes (ws.kdl, # *.schema.json, rest.yaml), so this task stays in the always-loaded default config (its `gen:` name also # anchors the `gen:*` glob) and every `gen:` depends on it. The Zig client (`gen:zig-rest`) moved # to config.zig.toml since it needs `zig fmt`. run = """ cargo run -q -p et-int-gen --bin et-int-gen -- generate core cargo run -q -p et-int-gen --bin et-int-gen -- generate rust cargo +{{ vars.rust_nightly }} fmt -p et-rest-client """ shell = "{{ vars.task_shell }}" [tasks."gen:wasmtime-bindings"] depends = ["gen:ws-spec"] description = "Run et-int-gen: emit the ws-wasi-runner host bindings for the `runner` world" # Output lands in the consuming crate, not generated/. # `cargo package` only includes files under a crate's own directory, so bindings that live anywhere else # leave et-ws-wasi-runner unable to build from its own tarball -- which is what retired the # `wasmtime::component::bindgen!` macro that read WIT from `../../generated/specs/wit`. # # Depends on gen:ws-spec for the `et:ws-messages` WIT its `generate core` step emits. # Unlike gen:ws-spec there is no rustfmt post-process; refer to int-gen bindings.rs for details. run = "cargo run -q -p et-int-gen --bin et-int-gen -- generate bindings" [tasks.gen-specs] # Native `depends` glob anchored by gen:ws-spec (core + Rust client, always loaded). # That anchor keeps the glob from matching zero; guest `gen:` tasks join from their # loaded MISE_ENV configs. Run with MISE_ENV covering every language (e.g. # `mise run gen-specs-all`) to regenerate everything. depends = ["gen:*"] description = "Regenerate every loaded spec artifact under generated/ (core + Rust + any loaded guest gen)" [tasks.gen-specs-check] depends = ["gen-specs"] description = "Fail if any checked-in et-int-gen artifact is stale" # Nearly every et-int-gen output lands under generated/. # The runner's wit/deps/et-ws-messages is a symlink into that tree, so a single diff against generated/ # covers those. The runner's host bindings are the exception: they must sit inside the crate for # `cargo package` to include them, so that path is diffed explicitly. run = "git diff --exit-code -- generated services/ws-wasi-runner/src/bindings.rs" # `cargo-watch` is unmaintained; `watchexec` is its successor. # watchexec is what `cargo-watch` always called under the hood. Restart on .rs/.toml changes so a # workspace-wide edit (any library, any service the server depends on) triggers a rebuild. `--clear` # wipes the terminal between runs so cargo output starts at the top of the screen; `--restart` kills the # running `cargo run` before kicking off a fresh one. Respects `.gitignore`, so `target/` and friends # are skipped. `--shell=none` runs the command directly as a program instead of wrapping it in a shell: # without it, watchexec 2.x defaults to shell mode, and on Windows the mise task runs under MSYS `sh`, which # watchexec invokes as `sh -c cargo run -p et-ws-server` -- `cargo` runs with no args (run/-p/et-ws-server # become sh positional params $0/$1/$2), so it just prints `cargo`'s help and the server never starts # (with `--clear`, the screen simply blanks). The command has no shell features, so program mode is correct # on every OS. This is upstream watchexec#918 (open); drop --shell=none once it is fixed. Verified on 2.5.1. [tasks.ws-server] alias = "hub" description = "Run the WebSocket server, restarting on .rs/.toml changes" run = "watchexec --restart --clear --exts rs,toml -i '**/tests/**' --shell=none -- cargo run -p et-ws-server" [tasks.ws-server.env] MODULES_ROOT = "{{ vars.hub_root_module }}" # Must match ZO_ROOT_USER_PASSWORD in config/o2.env; the server authenticates its OTLP exports as that root user. OTLP_AUTH_PASSWORD = "Complexpass#123" # skipcq: SCT-A000 -- local dev-only credential, committed deliberately OTLP_AUTH_USERNAME = "root@example.com" [tasks.ws-server-no-otel] description = "Run the WebSocket server without OTLP/o2 observability (OpenTelemetry init is skipped)" # Same as ws-server but sets no OTLP_* env, so Config.otlp deserialises to None and et_otlp::init is skipped. # Use this when you don't want the o2/OpenObserve stack (`mise run o2`) running alongside the server. env = { MODULES_ROOT = "{{ vars.hub_root_module }}" } run = "watchexec --restart --clear --exts rs,toml -i '**/tests/**' --shell=none -- cargo run -p et-ws-server" [tasks.ws-server-offline-demo] description = "Run the WebSocket server for a hotspot demo: no OTLP, no watchexec, cargo --offline" # Demo the server from a machine that shares its own Wi-Fi hotspot and has no internet uplink. # Sets no OTLP_* env, runs cargo directly (no watchexec restart-on-change), and `--offline` makes cargo # resolve and build from the crates already in the local cache instead of touching the network. env = { MODULES_ROOT = "{{ vars.hub_root_module }}", NET_LOG_INTERFACE = "bridge100" } run = "cargo run --offline -p et-ws-server" [tasks.ws-wasi-runner] description = "Run the WebSocket WASI runner (set RUNNER_MODULE, e.g. wasi-graphics-info; RUNNER_FEATURES=cuda on CUDA)" dir = "services/ws-wasi-runner" run = """ if [ -n "${RUNNER_FEATURES:-}" ]; then cargo run --features "$RUNNER_FEATURES" else cargo run fi """ shell = "{{ vars.task_shell }}" [tasks.build-et-cli] # Pre-build et-cli once. # The parallel build-ws-* module tasks each call `cargo run -p et-cli -- module-package-json`; without # this they race rebuilding et-cli, and one execs the binary mid-replace (ENOENT -- surfaced on macOS). # Stays in the base config (not a per-lang config) because dart, js, python, and rust env tasks all # depend on it. # `shell = "bash"` is load-bearing on Windows: mise's child-env composition drops the `%LOCALAPPDATA%\mise\shims` # PATH entry when spawning the default cmd shell for a base-config task under a bash-parent GHA step, so # bare `cargo` fails with `'cargo' is not recognized`. Git Bash sources its own PATH from mise's shim # dir, so `cargo` resolves cleanly. See windows job 82267796152 on d5bb7e0b for the captured # cmd-subprocess failure. description = "Build et-cli (prereq for the module-package-json steps)" run = "cargo build -p et-cli" shell = "{{ vars.task_shell }}" # Install the tools the GHCR-store tasks call, so they work on a checkout that never ran a full `mise install`. # gh is only needed by the maint tasks, but one shared helper beats three near-copies. # # Installing is all this buys: PATH does NOT follow. mise composes a task's PATH from what is installed when # the `mise run` starts, and a `depends` install lands after that snapshot just as an in-body `mise install` # does -- so a cold runner still gets "bash: line 1: coreutils: command not found" (first seen with the in-body # form at https://github.com/edge-toolkit/core/actions/runs/31670112517/job/94352839713 on commit # https://github.com/edge-toolkit/core/commit/d8ad8cb99cf58a1e505cdbebc5b94f9893532b4f, then again through # `depends` at https://github.com/edge-toolkit/core/actions/runs/31773896309/job/94685230924 on commit # https://github.com/edge-toolkit/core/commit/0dd8cb009e2b81f9013d7a647092adf383391284). Every consumer # therefore resolves these tools with `mise which` and calls them by absolute path, which needs no PATH entry # at all. # # Keep the tool specs unquoted. This body is a single line, so on Windows it runs under mise's default # `cmd /c` -- the repo's bash inline-shell default is set by `preinstall`, which has not run yet when CI # restores the store. cmd.exe does not strip quotes, so a quoted spec reaches mise verbatim and it tries to # clone a plugin from the literal name: `mise plugin:01mf02/jaq' clone https://github.com/01mf02/jaq'.git`, # ending in `Failed to install 'aqua:01mf02/jaq'@latest`, which stranded the Windows lane's restore on commit # https://github.com/edge-toolkit/core/commit/0dd8cb009e2b81f9013d7a647092adf383391284 at # https://github.com/edge-toolkit/core/actions/runs/31773896309/job/94685231006. [tasks._setup-store-tools] description = "Install the tools the mise-tools store tasks depend on (cold-checkout helper)" run = "mise install github:uutils/coreutils crane gh aqua:01mf02/jaq" # Build the OCI layout for a mise-tools store, carrying only relocatable tools. # Shared by every push-mise-tools* task (native and docker) so all platforms' stores hold the same set of # backends, and lives here rather than in config.maint.toml because the docker publisher runs it INSIDE the # build container, where only the always-loaded configs exist. # # A tool is relocatable when its install tree carries no machine-specific absolute paths. aqua, github, http # and ubi unpack an archive and stop, so moving the tree to another machine's data dir works. The go, java, # node and zig core runtimes are the same shape -- a plain toolchain-archive extract, each locating its own # libs relative to the running binary (or, for java, to JAVA_HOME) -- except for one absolute artifact, the # macOS Contents/Home convenience symlink mise creates at java install time, which the body below repoints # to a relative target. cargo: tools ride along too -- cargo-binstall drops plain prebuilt binaries under # bin/, and the .crates.toml / .crates2.json metadata beside them records only crate names and versions -- as # do go: tools, which are single self-contained static executables. The rust core runtime can NOT join them: # mise never materialises rust under installs/ at all -- installs/rust/ is an absolute symlink into # $CARGO_HOME/bin and the toolchain itself lives in $RUSTUP_HOME (both outside the data dir), so a rust layer # would carry nothing but a dangling publisher-path symlink. Every other backend writes the publishing # machine's absolute paths into the tree at install time, which then breaks on a consumer whose $HOME differs # -- conda emits `.mise-bins/` wrappers holding # # export CONDA_PREFIX='/home//.local/share/mise/installs/conda-/' # exec '/home//.local/share/mise/installs/conda-//bin/' # # so a restored `clang-format` dies with `exec: /home//...: not found` (exit 127), and pipx does # the same through venv shebangs + pyvenv.cfg. `mise install` cannot repair those: it treats a tool with an # install dir as installed, so a poisoned tree survives every later install. aqua/github/http/ubi earn their # place by sparing api.github.com the per-tool release/attestation lookups CI would otherwise repeat; the # core runtimes and the cargo:/go: tools never touch that API but are carried because their cold installs are # the slow ones (source-build fallbacks, toolchain downloads). [tasks.oci-build-store] depends = ["_setup-store-tools"] description = "Build the mise-tools OCI layout for the current toolset, relocatable backends only" run = """ coreutils="$(mise which coreutils)" jaq="$(mise which jaq)" out="${usage_out:?output directory required}" # Prefixed names carry their own backend; bare ones are registry short names to resolve. # `mise registry ` prints every backend for the name, preferred first, so take the first field. disabled="" for tool in $(mise ls --current --json | "$jaq" -r 'keys[]'); do case "$tool" in *:*) backend="$tool" ;; *) backend="$(mise registry "$tool" | "$coreutils" head -n1 | "$coreutils" cut -d' ' -f1)" ;; esac case "$backend" in aqua:* | cargo:* | core:go | core:java | core:node | core:zig | github:* | go:* | http:* | ubi:*) ;; *) disabled="${disabled:+$disabled,}$tool" ;; esac done echo "store excludes non-relocatable tools: ${disabled:-}" # Repoint the macOS java Contents/Home symlink at its version root relatively. # The link mise creates at install time is absolute (the publisher's data dir), so it dangles on any consumer # whose data dir differs; the relative target resolves identically everywhere. No-op on linux/windows JDK # layouts, which have no Contents/. installs="${MISE_DATA_DIR:-$HOME/.local/share/mise}/installs" for home in "$installs"/java/*/Contents/Home; do if [ -L "$home" ]; then case "$("$coreutils" readlink "$home")" in /*) "$coreutils" ln -sfn .. "$home" ;; esac fi done MISE_DISABLE_TOOLS="$disabled" MISE_EXPERIMENTAL=1 mise oci build -o "$out" """ shell = "{{ vars.task_shell }}" usage = 'arg "" help="Output directory for the OCI image layout"' # Pre-populate the local mise data dir from this platform's GHCR tool store. # A maintainer publishes each platform's tool tree to ghcr.io//mise-tools/- with the maint # env's push-mise-tools task (`mise oci push`, one content-addressed layer per tool); this flattens that # image with `crane export` straight into the local data dir's installs/, so a fresh `mise install` # afterwards only fills the gaps and regenerates shims instead of downloading everything. The store carries # only relocatable backends, so a restored tree needs no repair; everything else installs cold as usual. # Private packages need a one-time `docker login ghcr.io` (or `crane auth login ghcr.io`). [tasks.pull-mise-tools] depends = ["_setup-store-tools"] description = "Pre-populate the local mise data dir from this platform's GHCR tool store" run = """ coreutils="$(mise which coreutils)" crane="$(mise which crane)" jaq="$(mise which jaq)" detected=$("$coreutils" uname -sm) case "$detected" in Darwin*arm64) def_plat=macos-arm64 ;; Darwin*x86_64) def_plat=macos-x64 ;; Linux*aarch64 | Linux*arm64) def_plat=linux-arm64 ;; Linux*x86_64) def_plat=linux-x64 ;; Windows*) def_plat=windows-x64 ;; *) echo "unrecognised platform '$detected'; pass one explicitly" >&2 && exit 1 ;; esac plat="${usage_platform:-$def_plat}" ref="ghcr.io/edge-toolkit/core/mise-tools/${plat}:latest" if [ "$plat" = "windows-x64" ]; then data="$LOCALAPPDATA/mise" else data="${MISE_DATA_DIR:-$HOME/.local/share/mise}" fi # Pull the image as an OCI layout, then extract its layers locally into scratch. # One `crane export` stream of the whole multi-GB rootfs is fragile -- GHCR killed it mid-transfer with # # Error: http2: server sent GOAWAY and closed the connection; ... debug="idle timeout" # ...: Truncated tar archive # # (observed locally). `crane pull` instead fetches each digest-verified layer as its own retried request. # The layers land in scratch rather than straight onto the data dir, both to survive a mid-pull failure # and because tar refuses to write through existing symlinks on a populated data dir ("Cannot extract # through symlink installs/http-augeas//bin", observed locally). tmp="target/scratch/pull-mise-tools" "$coreutils" rm -rf "$tmp" "$coreutils" mkdir -p "$tmp" "$data/installs" echo "pulling $ref" "$crane" pull --format=oci "$ref" "$tmp/oci" manifest=$("$jaq" -r '.manifests[0].digest | ltrimstr("sha256:")' "$tmp/oci/index.json") layers=$("$jaq" -r '.layers[].digest | ltrimstr("sha256:")' "$tmp/oci/blobs/sha256/$manifest") # Layers that carry no mise/installs entries (the base image's) make tar exit non-zero; that miss is fine. for layer in $layers; do tar -xzf "$tmp/oci/blobs/sha256/$layer" -C "$tmp" --strip-components=1 mise/installs 2>/dev/null || true done restored=0 for d in "$tmp"/installs/*/*/; do [ -d "$d" ] || continue rel="${d#"$tmp"/installs/}" dest="$data/installs/$rel" if [ ! -e "$dest" ]; then "$coreutils" mkdir -p "$("$coreutils" dirname "$dest")" "$coreutils" mv "$d" "$dest" restored=$((restored + 1)) fi done "$coreutils" rm -rf "$tmp" echo "restored $restored tool versions into $data/installs" echo "done; run 'mise install' to fill gaps and regenerate shims" """ shell = "{{ vars.task_shell }}" usage = 'arg "[platform]" help="CI platform store to pull (default: the current platform)" default=""' [tasks.build-modules] # Native `depends` glob over `build-ws-*`. # That matches `build-ws-wasm-agent` (always loaded here, no `-module` suffix -- so the glob never # matches zero) plus every loaded `build-ws-*-module`. depends = ["build-ws-*"] description = "Build all loaded WebAssembly modules" [tasks.test-ws-wasm-agent-firefox] description = "Run headless Firefox tests for the WebSocket WASM client" dir = "services/ws-wasm-agent" run = "wasm-pack test --headless --firefox" [tasks.ws-e2e-firefox] depends = ["test-ws-wasm-agent-firefox", "ws-server"] description = "Run both the ws-server and ws-wasm-agent using Firefox" [tasks.test-ws-wasm-agent-chrome] description = "Run headless Chrome tests for the WebSocket WASM client" dir = "services/ws-wasm-agent" run = "env CHROMEDRIVER=\"$(mise which chromedriver)\" wasm-pack test --headless --chrome" [tasks.test-pic-viewer-firefox] description = "Run headless Firefox tests for the pic-viewer module (image display onto the canvas)" dir = "services/ws-modules/pic-viewer" run = "wasm-pack test --headless --firefox" [tasks.test-pic-viewer-chrome] description = "Run headless Chrome tests for the pic-viewer module (image display onto the canvas)" dir = "services/ws-modules/pic-viewer" run = "env CHROMEDRIVER=\"$(mise which chromedriver)\" wasm-pack test --headless --chrome" [tasks.ws-e2e-chrome] depends = ["test-ws-wasm-agent-chrome", "ws-server"] description = "Run both the ws-server and ws-wasm-agent using Chrome" [tasks.openobserve] alias = "o2" run = "docker run --rm --name openobserve -p {{ vars.o2_publish }} --env-file config/o2.env {{ vars.o2_image }}" [tasks.demo] depends = ["openobserve", "ws-server"] [tasks.open-o2] run = "open http://localhost:5080/" # Native OpenObserve, an alternative to the docker `o2` task. # Same binary and config/o2.env, data under the gitignored target/. Binds :5080 like docker o2, so run one or # the other, not both. [tasks.o2-native] description = "Run OpenObserve natively (same binary the o2 docker image runs) with data under target/o2-data" run = """ set -a # shellcheck source=/dev/null . ./config/o2.env set +a export ZO_DATA_DIR="$PWD/target/o2-data" openobserve init-dir -p "$ZO_DATA_DIR" openobserve """ shell = "{{ vars.task_shell }}" [tasks.o2-native-clean] description = "Delete native OpenObserve data under target/o2-data (stop o2-native first if it is running)" run = """ coreutils rm -rf target/o2-data """ shell = "{{ vars.task_shell }}" [tasks.o2-hostmetrics] description = "Ship host CPU/memory/disk/network metrics to the local o2 via an OpenTelemetry Collector" run = """ cfg="$PWD/config/otelcol-hostmetrics.yaml:/etc/otelcol-contrib/config.yaml:ro" img="{{ vars.otelcol_image }}" docker run --rm --name o2-hostmetrics --network host -v /:/hostfs:ro -v "$cfg" --env-file config/o2.env "$img" """ shell = "{{ vars.task_shell }}" [tasks.o2-macmon] description = "Ship Apple Silicon CPU/GPU/power metrics to the local o2 (native macmon scraped by a collector)" run = """ macmon serve -p 9090 & mpid=$! trap 'kill "$mpid" 2>/dev/null || true' EXIT cfg="$PWD/config/otelcol-macmon.yaml:/etc/otelcol-contrib/config.yaml:ro" img="{{ vars.otelcol_image }}" docker run --rm --name o2-macmon -v "$cfg" --env-file config/o2.env "$img" """ shell = "{{ vars.task_shell }}" [tasks.o2-nvidia] description = "Ship NVIDIA GPU metrics to the local o2 (native nvidia_gpu_exporter scraped by a collector)" run = """ nvidia_gpu_exporter --web.listen-address=:9835 & npid=$! trap 'kill "$npid" 2>/dev/null || true' EXIT cfg="$PWD/config/otelcol-nvidia.yaml:/etc/otelcol-contrib/config.yaml:ro" img="{{ vars.otelcol_image }}" # --network host: exporter + o2 both run on the host; Linux host network lets the collector reach them at localhost. docker run --rm --name o2-nvidia --network host -v "$cfg" --env-file config/o2.env "$img" """ shell = "{{ vars.task_shell }}" [tasks.o2-nvidia-native] description = "Non-Docker o2-nvidia: native collector binary scrapes native nvidia_gpu_exporter into o2" run = """ set -a # shellcheck disable=SC1091 . ./config/o2.env set +a nvidia_gpu_exporter --web.listen-address=:9835 & npid=$! trap 'kill "$npid" 2>/dev/null || true' EXIT export NVIDIA_METRICS_TARGET=localhost:9835 export O2_METRICS_ENDPOINT=http://localhost:5080/api/default/v1/metrics otelcol-contrib --config config/otelcol-nvidia.yaml """ shell = "{{ vars.task_shell }}" [tasks.o2-winmetrics] description = "Ship Windows host CPU/mem/disk/net + GPU metrics to the local o2 (native windows_exporter + collector)" run = """ windows_exporter --collectors.enabled=cpu,memory,logical_disk,physical_disk,net,os,system,gpu & wpid=$! trap 'kill "$wpid" 2>/dev/null || true' EXIT cfg="$PWD/config/otelcol-winmetrics.yaml:/etc/otelcol-contrib/config.yaml:ro" img="{{ vars.otelcol_image }}" docker run --rm --name o2-winmetrics -v "$cfg" --env-file config/o2.env "$img" """ shell = "{{ vars.task_shell }}" [tasks.o2-winmetrics-native] description = "Non-Docker o2-winmetrics: native collector binary scrapes native windows_exporter (host + GPU) into o2" run = """ set -a # shellcheck disable=SC1091 . ./config/o2.env set +a windows_exporter --collectors.enabled=cpu,memory,logical_disk,physical_disk,net,os,system,gpu & wpid=$! trap 'kill "$wpid" 2>/dev/null || true' EXIT export WIN_METRICS_TARGET=localhost:9182 export O2_METRICS_ENDPOINT=http://localhost:5080/api/default/v1/metrics otelcol-contrib --config config/otelcol-winmetrics.yaml """ shell = "{{ vars.task_shell }}" [tasks.o2-winperf] description = "Self-contained non-Docker Windows metrics: one native collector reads host + GPU perf counters into o2" run = """ set -a # shellcheck disable=SC1091 . ./config/o2.env set +a otelcol-contrib --config config/otelcol-winperf.yaml """ shell = "{{ vars.task_shell }}" # Note: bare `yq` / `coreutils` would fail inside the nanoserver docker build. # There mise's shims aren't on PATH for busybox-w32 ash (see the AGENTS.md "Nano Server exception"). # `mise exec --` is the documented escape hatch; works the same on every host. yq stays bare because it # already runs cleanly on every other lane and the nanoserver-shim TODO (AGENTS.md) plans to put all # shims on PATH there too. [tasks.fetch-face1-rclone] run = """ rclone copyto {{ vars.face1_src }} {{ vars.face1_dst }} {{ vars.gh_http }} # Windows-safe checksum verify; behaviour is unchanged on Unix. # Two Windows-only traps are avoided: (1) the inline `bash -c` body strips the double quotes from # `yq '.asset.""'`, so the dotted key `*.onnx` mis-parses to `null` (passing the key via env + strenv() # needs no quotes); (2) uutils `sha256sum -c` mis-hashes binary files on Windows (text-mode read) even though # a direct hash is correct, so we compare the direct hash ourselves instead of piping a checklist through `-c`. export asset='{{ vars.face1_asset }}' sha=$(mise exec -- yq -p toml -oy '.asset[strenv(asset)].sha256' config/upstream-cache/data.toml) actual=$(mise exec -- coreutils sha256sum -b "{{ vars.face1_dst }}" | mise exec -- coreutils cut -c1-64) [ "$actual" = "$sha" ] || { echo "face1 checksum mismatch: expected=$sha actual=$actual" >&2 exit 1 } """ shell = "{{ vars.task_shell }}" [tasks.fetch-eye1-rclone] # Same shape as fetch-face1-rclone (rclone copy + Windows-safe SHA256 verify against data.toml). # The FaceMesh .task is mirrored to the hf-cache-v release by `MISE_ENV=maint mise run # publish-eye1-to-hf-cache`; refresh it there on an upstream bump. run = """ rclone copyto {{ vars.eye1_src }} {{ vars.eye1_dst }} {{ vars.gh_http }} export asset='{{ vars.eye1_asset }}' sha=$(mise exec -- yq -p toml -oy '.asset[strenv(asset)].sha256' config/upstream-cache/data.toml) actual=$(mise exec -- coreutils sha256sum -b "{{ vars.eye1_dst }}" | mise exec -- coreutils cut -c1-64) [ "$actual" = "$sha" ] || { echo "eye1 checksum mismatch: expected=$sha actual=$actual" >&2 exit 1 } """ shell = "{{ vars.task_shell }}" [tasks.fetch-har-motion1-rclone] run = "rclone copyto {{ vars.har_src }} {{ vars.har_dst }} {{ vars.gh_http }}" shell = "{{ vars.task_shell }}" [tasks.fetch-mnist-rclone] # MNIST classifier used by services/ws-modules/wasi-graphics-info to exercise wasi-nn. # Exercises wasi-nn end-to-end. Sourced from the canonical ONNX Model Zoo; size 26143 bytes. Lands under # the module's gitignored pkg/ so et-modules-service can serve it statically alongside the .wasm. run = "rclone copyto {{ vars.mnist_src }} {{ vars.mnist_dst }} {{ vars.gh_http }}" shell = "{{ vars.task_shell }}" [tasks.fetch-llm1-rclone] # llm1's SmolLM2 weights, tokenizer and configs, fetched straight from HuggingFace. # The five files land under the model module's gitignored pkg/, which et-modules-service then serves as # /modules/et-model-llm1/... -- exactly the layout transformers.js expects of a local model. Deliberately not # in prefetch-ci: at ~118 MB the weights are a developer-side download, and only llm1 reads them. run = """ opts="--http-url https://huggingface.co --progress --ignore-existing" files=(config.json generation_config.json tokenizer.json tokenizer_config.json onnx/model_q4f16.onnx) assets=( '{{ vars.llm1_config_asset }}' '{{ vars.llm1_gen_asset }}' '{{ vars.llm1_tokenizer_asset }}' '{{ vars.llm1_tokenizer_config_asset }}' '{{ vars.llm1_weights_asset }}' ) for index in "${!files[@]}"; do dst="{{ vars.llm1_dir }}/${files[index]}" # shellcheck disable=SC2086 rclone copyto "{{ vars.llm1_repo }}/${files[index]}" "$dst" $opts export asset="${assets[index]}" sha=$(mise exec -- yq -p toml -oy '.asset[strenv(asset)].sha256' config/upstream-cache/data.toml) actual=$(mise exec -- coreutils sha256sum -b "$dst" | mise exec -- coreutils cut -c1-64) [ "$actual" = "$sha" ] || { echo "llm1 checksum mismatch for ${files[index]}: expected=$sha actual=$actual" >&2 exit 1 } done """ shell = "{{ vars.task_shell }}" [tasks.fetch-speech1-rclone] run = """ opts="--http-url https://huggingface.co --progress --ignore-existing" # shellcheck disable=SC2086 rclone copyto {{ vars.speech1_src }} {{ vars.speech1_dst }} $opts export asset='{{ vars.speech1_asset }}' sha=$(mise exec -- yq -p toml -oy '.asset[strenv(asset)].sha256' config/upstream-cache/data.toml) actual=$(mise exec -- coreutils sha256sum -b "{{ vars.speech1_dst }}" | mise exec -- coreutils cut -c1-64) [ "$actual" = "$sha" ] || { echo "speech1 checksum mismatch: expected=$sha actual=$actual" >&2 exit 1 } """ shell = "{{ vars.task_shell }}" [tasks.download-models] depends = [ "fetch-eye1-rclone", "fetch-face1-rclone", "fetch-har-motion1-rclone", "fetch-llm1-rclone", "fetch-mnist-rclone", "fetch-speech1-rclone", ] [tasks.cargo-test] alias = "cargo-nextest" description = "Run every Rust test via nextest (web-runner excluded on gnullvm)" run = "cargo nextest run --workspace {{ vars.cargo_ws_excludes }} --config-file config/nextest.toml" [tasks.test-recovery-supervisor] description = "Test recovery policy, Kubernetes observations, persistence, and hub lifecycle transport" run = "cargo test -p et-recovery-supervisor" tools = { "aqua:kubernetes/kubernetes/kubectl" = "latest" } [tasks.recovery-supervisor] description = "Run the opt-in Core workload recovery supervisor" run = "cargo run -p et-recovery-supervisor" # The self-contained slice of the suite, for platforms still being brought up. # These three crates' tests need nothing the host has to provide: no built WASM module, no ws-server, no # docker, no network. That is what makes them meaningful on a lane whose job is to prove the toolchain # works rather than to gate the product -- a failure here is a real defect on that platform, not a missing # fixture. Everything else in the suite wants at least one of those, so widening this list means teaching # the lane to build modules and start servers, which is the full `test` task by another name. [tasks.cargo-test-libs] description = "Run the tests that need no server, module or network (toolchain bring-up lanes)" run = "cargo nextest run --config-file config/nextest.toml -p edge-toolkit -p et-path -p et-ws-runner-common" [tasks."test:rust"] # Rust tests. Always-loaded, so `test`'s glob always matches it. depends = ["cargo-test"] description = "Run the Rust tests" [tasks.test] # Native `depends` glob; test:rust is always loaded so it never matches zero. depends = ["test:*"] description = "Run all tests: test:rust + any loaded guest test:" # Detect which rp-v assets are MISSING from the GitHub release. # The assets are the native binary for this host's triple + the platform-agnostic wasm tarball. Lives # outside config.maint.toml because callers (notably .github/workflows/test.yaml) need the answer before # deciding whether to load the maint env; the detection itself only needs gh (host) + yq/rg/coreutils # (mise tools). Reads .mise/config.toml's pinned `tools."http:et-rp".version` to derive the expected # asset names. When $GITHUB_OUTPUT is set (GHA), emits step outputs the workflow can branch on; otherwise # prints to stdout. [tasks.rp-detect-missing] description = "Print whether rp-v has native+wasm assets for this host (GHA outputs)" run = """ short=$(key='http:et-rp' yq -p toml -oy '.tools[strenv(key)].version' .mise/config.toml) triple=$(rustc -vV | rg '^host: ' | coreutils cut -c7-) native_asset="rustpython-${short}-${triple}.tar.gz" wasm_asset="rustpython-wasm-${short}.tar.gz" tag="{{ vars.a_rp_release }}" assets=$(gh release view "$tag" --repo edge-toolkit/core --json assets --jq '.assets[].name' 2>/dev/null || true) native_missing=true wasm_missing=true if echo "$assets" | rg -Fxq "$native_asset"; then native_missing=false; fi if echo "$assets" | rg -Fxq "$wasm_asset"; then wasm_missing=false; fi out="${GITHUB_OUTPUT:-/dev/stdout}" { echo "short=$short" echo "triple=$triple" echo "native_missing=$native_missing" echo "wasm_missing=$wasm_missing" } >>"$out" echo "[rp-detect-missing] triple=$triple short=$short native_missing=$native_missing wasm_missing=$wasm_missing" >&2 """ shell = "{{ vars.task_shell }}" # Build the pywasm1 ws-module from the prebuilt rustpython_wasm tarball. # The tarball is installed by mise as [tools."http:rp-wasm"] (download + sha256 + extract handled by # mise; install dir referenced via vars.rp_wasm_dir). Picked up by `build-modules`'s glob (`build-ws-*`) # like every other module task -- using the prebuilt tarball from the rp-v release rather than # recompiling RustPython keeps the cost at one ~10 MB download, not a multi-minute upstream wasm-pack build. [tasks.build-ws-pywasm1-module] depends = ["build-et-cli"] description = "Build the pywasm1 module (fetched rustpython_wasm + main.py)" dir = "services/ws-modules/pywasm1" run = """ rp_pkg={{ vars.rp_wasm_dir }} coreutils rm -rf pkg coreutils mkdir -p pkg coreutils cp "$rp_pkg"/rustpython_wasm.js "$rp_pkg"/rustpython_wasm.d.ts pkg/ coreutils cp "$rp_pkg"/rustpython_wasm_bg.wasm "$rp_pkg"/rustpython_wasm_bg.wasm.d.ts pkg/ coreutils cp -R "$rp_pkg/snippets" pkg/ coreutils cp index.js main.py pkg/ {{ vars.et_cli }} module-package-json """ shell = "{{ vars.task_shell }}" # Cross-language orchestration (stand-ins for the MISE_ENV=all mise lacks). # `$ALL_LANGS` is auto-discovered in `[env]` above from the config..toml filenames, so adding a # language needs no edits here. [tasks.print-all-langs] description = "Print $ALL_LANGS -- the comma-separated list of every language env" run = 'echo "$ALL_LANGS"' shell = "{{ vars.task_shell_plain }}" [tasks.install-all] description = "Install every language toolchain (all .mise/config..toml)" run = 'MISE_ENV="$ALL_LANGS" mise install' shell = "{{ vars.task_shell_plain }}" # Fail when a full install resolves a different tool set or version than the committed lockfiles record. # Only the platform-independent fields are compared, because mise fills a lockfile's per-platform rows in lazily on # whichever platform runs the install: `checksum`, `url`, `url_api`, `provenance` and `provenance_verified` appear # for macos-arm64 on a developer's machine and for linux-x64 on a runner. A whole-file `git diff` therefore can # never be clean across the five-platform matrix -- every lane legitimately adds its own rows, and the committed # lockfile already carries a checksum for some platforms of a tool and not others. The `[[tools.]]` headers # and their `version` / `backend` are identical everywhere, so those are what this gates on: a tool added, removed # or bumped without regenerating the lockfiles is caught, while a lane recording its own checksums is not. # install-all rather than a bare install, so guest-language tools reach their lockfiles too -- MISE_ENV only # loads a subset, and a partial install leaves every unloaded config's lockfile unexamined. # # The .mise/locks/ sidecars -- the npm and PyPI tools' dependency graphs, which the lockfiles reference by path and # digest -- carry no per-platform rows, so they are compared whole: any change, or a graph directory git does not # track yet, is drift. [tasks.mise-lock-check] depends = ["install-all"] description = "Fail if .mise/mise*.lock or .mise/locks/ differ from what a full install resolves" # `|| true` because rg exits 1 on no matches, which under pipefail would abort the whole task on the clean path. # `^[-+][-+]` drops git's `+++`/`---` file headers; `@@` hunk headers never match the leading `[-+]`. run = """ drift=$(git diff -U0 -- .mise/mise*.lock | rg "^[-+]" | rg -v "^[-+][-+]" | rg "^[-+](version|backend) = |^[-+]\\[\\[tools\\." || true) sidecars=$( git diff --name-only -- .mise/locks git ls-files --others --exclude-standard -- .mise/locks ) if [ -n "$drift" ] || [ -n "$sidecars" ]; then [ -z "$drift" ] || echo "$drift" [ -z "$sidecars" ] || echo "$sidecars" echo "Lockfiles differ from install-all; commit the regenerated .mise/mise*.lock and .mise/locks/" >&2 exit 1 fi """ shell = "{{ vars.task_shell }}" [tasks.all] # Generic stand-in: run any command with every language env enabled. # mise appends the task's CLI args after `mise exec --`, e.g. `mise run all zig version`. description = "Run a command with every language env enabled (MISE_ENV=all)" run = 'MISE_ENV="$ALL_LANGS" mise exec --' shell = "{{ vars.task_shell_plain }}" [tasks.check-all] description = "Run check across every language env" run = 'MISE_ENV="$ALL_LANGS" mise run check' shell = "{{ vars.task_shell_plain }}" [tasks.fmt-all] description = "Run fmt across every language env" run = 'MISE_ENV="$ALL_LANGS" mise run fmt' shell = "{{ vars.task_shell_plain }}" [tasks.test-all] description = "Run tests across every language env" run = 'MISE_ENV="$ALL_LANGS" mise run test' shell = "{{ vars.task_shell_plain }}" [tasks.build-modules-all] description = "Build modules across every language env" run = 'MISE_ENV="$ALL_LANGS" mise run build-modules' shell = "{{ vars.task_shell_plain }}" [tasks.gen-specs-all] description = "Regenerate every spec artifact across every language env" run = 'MISE_ENV="$ALL_LANGS" mise run gen-specs' shell = "{{ vars.task_shell_plain }}"