Skip to content

Commit 322cef9

Browse files
authored
Move docker module loading to et-cli (#124)
1 parent 1f5ee01 commit 322cef9

39 files changed

Lines changed: 1528 additions & 204 deletions

.codacy.yaml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
# Codacy's in-repo config can only exclude paths -- it cannot disable individual patterns (dashboard-only).
22
# These paths carry findings the repo already covers elsewhere or has consciously decided against:
33
# - config/semgrep/: our Semgrep rule files; Codacy's own Semgrep misfires on the `- id:` each rule declares.
4-
# - Dockerfile: Codacy's default hadolint re-flags DL4006 (curl | sh pipefail), which config/hadolint.yaml
5-
# deliberately ignores and the repo's own hadolint-check + DeepSource Docker already cover.
64
# - dotnet-data1/Program.cs: Codacy's CA1054 wants URI-typed params, but [JSImport] partial methods can only
75
# marshal `string` across the JS boundary; DeepSource covers the C# there and reports no such issue.
86
# - .github/workflows/ + .github/actions/: Codacy flags GitHub Actions `uses:` not pinned to a commit SHA, but
@@ -38,7 +36,6 @@ exclude_paths:
3836
- ".github/actions/**"
3937
- ".github/workflows/**"
4038
- "CLAUDE.md"
41-
- "Dockerfile"
4239
- "config/semgrep/**"
4340
- "services/ws-modules/dotnet-data1/Program.cs"
4441
- "services/ws-modules/wasi-comm1/src/coverage.rs"

.dockerignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,3 @@ services/ws-server/storage/
5555
**/.git/
5656
**/Dockerfile*
5757
README.md
58-
**/.dockerignore

.github/workflows/coverage.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,14 @@ jobs:
9898

9999
# Fold the instrumented wasm guest modules' coverage into lcov.info so it rides the same `rust` flag.
100100
# The WASI + browser modules dumped their .profraw to target/wasi-cov during the runner tests above.
101+
#
102+
# This and the two merges below carry the same guard as the uploads. The coverage task re-raises a failing
103+
# test status once it has written lcov.info, so without a guard these merges skip on exactly the runs that
104+
# still upload -- publishing a Rust-only report as though it were the whole picture, which reads downstream
105+
# as a large coverage drop rather than as a failed run. `hashFiles` keeps them off a run that produced no
106+
# report at all, where there is nothing to merge into.
101107
- name: Merge wasm guest coverage into lcov.info
108+
if: ${{ !cancelled() && hashFiles('lcov.info') != '' }}
102109
timeout-minutes: 10
103110
run: mise run wasm-cov
104111

@@ -107,6 +114,7 @@ jobs:
107114
# the agent lib's coverage into the same lcov.info. CHROMEWEBDRIVER is the GitHub image's chromedriver that
108115
# matches its preinstalled Chrome; the task falls back to the mise-pinned http:chromedriver when it is unset.
109116
- name: Merge ws-wasm-agent coverage into lcov.info
117+
if: ${{ !cancelled() && hashFiles('lcov.info') != '' }}
110118
timeout-minutes: 20
111119
run: |
112120
export CHROMEDRIVER="${CHROMEWEBDRIVER:+$CHROMEWEBDRIVER/chromedriver}"
@@ -116,12 +124,15 @@ jobs:
116124
# Its native parse tests already ride that run; this drives the module's wasm-bindgen display tests in
117125
# the same headless Chrome as the ws-wasm-agent step above and folds their lcov into lcov.info.
118126
- name: Merge pic-viewer coverage into lcov.info
127+
if: ${{ !cancelled() && hashFiles('lcov.info') != '' }}
119128
timeout-minutes: 20
120129
run: |
121130
export CHROMEDRIVER="${CHROMEWEBDRIVER:+$CHROMEWEBDRIVER/chromedriver}"
122131
mise run pic-viewer-cov
123132
133+
# Python coverage does not depend on the Rust result, so a failing Rust test must not skip it.
124134
- name: Collect Python coverage
135+
if: ${{ !cancelled() }}
125136
timeout-minutes: 15
126137
env:
127138
GITHUB_TOKEN: ${{ github.token }}
@@ -137,7 +148,13 @@ jobs:
137148
coverage-python.xml
138149
if-no-files-found: error
139150

151+
# Every upload below runs on a failed run too, gated on its report actually existing.
152+
# A failing test is when the coverage delta matters most, and the coverage task now writes lcov.info before
153+
# re-raising the test status. The `hashFiles` half keeps a genuinely absent report from turning into a
154+
# second, misleading failure: when an earlier step (a tool install, say) means nothing was ever produced,
155+
# these skip instead of erroring on a missing file and burying the real cause.
140156
- name: Upload Rust coverage to Codecov (OIDC)
157+
if: ${{ !cancelled() && hashFiles('lcov.info') != '' }}
141158
uses: codecov/codecov-action@v7
142159
with:
143160
use_oidc: true
@@ -146,6 +163,7 @@ jobs:
146163
fail_ci_if_error: true
147164

148165
- name: Upload Python coverage to Codecov (OIDC)
166+
if: ${{ !cancelled() && hashFiles('coverage-python.xml') != '' }}
149167
uses: codecov/codecov-action@v7
150168
with:
151169
use_oidc: true
@@ -154,9 +172,11 @@ jobs:
154172
fail_ci_if_error: true
155173

156174
- name: Upload Rust coverage to DeepSource (OIDC)
175+
if: ${{ !cancelled() && hashFiles('lcov.info') != '' }}
157176
run: deepsource report --analyzer test-coverage --key rust --value-file lcov.info --use-oidc
158177

159178
- name: Upload Python coverage to DeepSource (OIDC)
179+
if: ${{ !cancelled() && hashFiles('coverage-python.xml') != '' }}
160180
run: deepsource report --analyzer test-coverage --key python --value-file coverage-python.xml --use-oidc
161181

162182
# Upload test results even when tests failed.

.mise/config.coverage.toml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,14 @@ export __CARGO_LLVM_COV_RUSTC_WRAPPER_RUSTFLAGS="$flags"
114114
# et-ws-web-runner/coverage adds the browser-module capture (shims + the __et_capture_coverage PUT);
115115
# et-ws-wasi-runner/coverage adds the guest `/cov` preopen. ET_TEST_COVERAGE (set by the coverage job) still
116116
# gates capture at runtime within each build.
117-
cargo nextest run --config-file config/nextest.toml --features et-ws-web-runner/coverage,et-ws-wasi-runner/coverage
117+
# A failing test must not cost the whole report: capture the status and re-raise it once lcov.info is written.
118+
# Under `set -e` a nextest failure would abort the task here, so `cargo llvm-cov report` below never runs and
119+
# lcov.info is never produced -- the coverage upload steps then have nothing to send and the run reports no
120+
# coverage at all. A failing run is exactly when the per-test results and the coverage delta matter most, so the
121+
# profile data nextest has already written gets turned into a report either way.
122+
test_status=0
123+
cargo nextest run --config-file config/nextest.toml --features et-ws-web-runner/coverage,et-ws-wasi-runner/coverage ||
124+
test_status=$?
118125
mise run gen-specs
119126
mise run regen-verification
120127
# The runner's host bindings are excluded from the report, not just from analysis.
@@ -128,6 +135,7 @@ mise run regen-verification
128135
# leading `^` matches nothing and the file stays in the report. Match the path tail instead.
129136
cov_ignore='services/ws-wasi-runner/src/bindings\\.rs$'
130137
cargo llvm-cov report --lcov --include-build-script --ignore-filename-regex "$cov_ignore" --output-path lcov.info
138+
exit "$test_status"
131139
"""
132140
shell = "{{ vars.task_shell }}"
133141

.mise/config.python.toml

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,19 @@
1616
"pipx:torch" = { version = "latest", os = ["linux", "macos/arm64", "windows"] }
1717
ruff = "latest"
1818

19-
# Use the GitHub release tarball, not `npm:pyodide`.
20-
# The npm package is only the runtime (pyodide.js, .wasm, stdlib, micropip) ~5 MB -- it's designed for JS apps
21-
# that fetch the rest from JsDelivr's Pyodide custom-CDN namespace (https://www.jsdelivr.com/oss-cdn) at
22-
# runtime. We need the ~200 MB "full" distribution served locally so browser modules work offline; that's
23-
# only published as a tarball on GitHub releases. The extracted tarball contains a top-level `pyodide/`
24-
# directory the modules service picks up -- see `default_modules_folders` in libs/edge-toolkit/src/config.rs.
19+
# Both pyodide distributions are pinned, because a deployment needs one or the other depending on its modules.
20+
# The npm package is only the runtime (pyodide.js, .wasm, stdlib, micropip) -- it's designed for JS apps that
21+
# fetch the rest from JsDelivr's Pyodide custom-CDN namespace (https://www.jsdelivr.com/oss-cdn) at runtime, so
22+
# it suffices for a module that imports the stdlib and nothing more. A module calling `micropip.install` pulls a
23+
# non-stdlib wheel and needs the "full" distribution served locally to work offline; that is published only as a
24+
# tarball on GitHub releases. This workspace serves the full one by default; `et-cli` picks between them per
25+
# scenario. The extracted tarball contains a top-level `pyodide/` directory the modules service picks up -- see
26+
# `default_modules_folders` in libs/edge-toolkit/src/config.rs.
27+
#
28+
# Hold both rows on the same pyodide release when bumping. Note the npm package renumbered after 0.29.x: its
29+
# `latest` is now 314.x, tracking the bundled CPython (314 = Python 3.14) rather than continuing the 0.29 line,
30+
# so an unpinned `latest` here would silently cross a major boundary the http row has not.
31+
"npm:pyodide" = "0.29.3"
2532
[tools."http:pyodide"]
2633
# Keep every pyodide executable OFF the task PATH.
2734
# The dist root carries `python` / `python.bat` / `python.exe` wrapper scripts, and with no bin_path set

.mise/config.toml

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ cargo-binstall = "latest"
7979
cmake = "latest"
8080
"conda:openssl" = "3"
8181
conftest = "latest"
82+
# Compose CLI for the generated scenario stacks under verification/.
83+
# The aqua backend rather than the registry default (`github:docker/compose`): the github backend records SLSA
84+
# provenance in the lockfile, and a mise older than the one that wrote it refuses the entry outright with
85+
# "Lockfile requires slsa provenance for github:docker/compose@<ver> but verification was not performed".
86+
# aqua records `provenance = "github-attestations"` instead, which is what every other pinned tool here uses.
87+
# This is the compose implementation only -- a docker daemon still has to come from the host.
88+
"aqua:docker/compose" = "latest"
8289
"github:nextest-rs/nextest" = { version = "latest", version_prefix = "cargo-nextest-" }
8390
taplo = "latest"
8491
watchexec = "latest"
@@ -607,6 +614,11 @@ task_shell_trace = 'bash -xeuo pipefail "{{ config_root }}/.mise/task-shell.sh"'
607614
task_shell_plain = 'bash "{{ config_root }}/.mise/task-shell.sh"'
608615
# OpenObserve docker image for the openobserve/o2 dev-observability task.
609616
o2_image = "openobserve/openobserve:v0.91.5"
617+
# Port publish spec for that task, bound to loopback rather than every interface.
618+
# config/o2.env's credential is committed, so the collector must not be reachable off the machine. It lives here
619+
# as a var because the mise policy rejects an `X.Y.Z` literal inside a task `run` body, and an IPv4 address trips
620+
# that check the same way a version would.
621+
o2_publish = "127.0.0.1:5080:5080"
610622
# OpenTelemetry Collector image for the o2-hostmetrics / o2-macmon / o2-nvidia / o2-winmetrics tasks.
611623
otelcol_image = "otel/opentelemetry-collector-contrib:0.156.0"
612624
rp_wasm_dir = '''{% if os() == "windows" and env?.MISE_INSTALLS_DIR -%}
@@ -727,6 +739,7 @@ depends = [
727739
"jscpd-check",
728740
"link-check",
729741
"ls-lint-check",
742+
"mise-lock-check",
730743
"readme-mise-version-check",
731744
"regal-check",
732745
"ryl-check",
@@ -808,7 +821,15 @@ run = "semgrep scan --config config/semgrep --autofix --metrics=off ."
808821

809822
[tasks.hadolint-check]
810823
description = "Lint the Dockerfiles with hadolint"
811-
run = "git ls-files '*Dockerfile' '*Dockerfile.*' | xargs hadolint --config config/hadolint.yaml"
824+
# `*Dockerfile.*` also catches a BuildKit per-Dockerfile ignore file, which is not a Dockerfile.
825+
# BuildKit looks for `<path-to-dockerfile>.dockerignore` beside the Dockerfile it is told to build, so the name
826+
# is fixed and cannot be moved out of the glob's way; hadolint parses it as Dockerfile syntax and stops on the
827+
# first ignore pattern with `unexpected '*' expecting ... a pragma, at least one space, or end of input`.
828+
run = """
829+
git ls-files '*Dockerfile' '*Dockerfile.*' | rg -v '\\.dockerignore$' |
830+
xargs hadolint --config config/hadolint.yaml
831+
"""
832+
shell = "{{ vars.task_shell }}"
812833

813834
# Duplication is gated as a fingerprint ratchet, not as a percentage.
814835
# config/jscpd-baseline.json records the fingerprint of every clone that already existed, and only a clone absent
@@ -976,8 +997,12 @@ description = "Cross-check Dockerfiles against mise [tools] pins + heredoc body-
976997
# Each git-tracked Dockerfile* gets both passes, except that the `ignore` pass only visits Dockerfiles that
977998
# actually contain heredocs: conftest 0.68.2's ignore parser panics with `runtime error: slice bounds out of
978999
# range` on plain Dockerfiles (no heredocs), and the body-first-line rule has nothing to check there anyway.
1000+
# A BuildKit per-Dockerfile ignore file is dropped from the list first. `<path-to-dockerfile>.dockerignore` is
1001+
# the name BuildKit demands beside the Dockerfile it builds, so it cannot be moved out of the glob's way, and
1002+
# the dockerfile parser reads its first pattern as an instruction:
1003+
# unknown instruction: **/.claude/
9791004
run = """
980-
files=$(git ls-files '*Dockerfile' '*Dockerfile.*')
1005+
files=$(git ls-files '*Dockerfile' '*Dockerfile.*' | rg -v '\\.dockerignore$')
9811006
# $files is a newline-separated path list; word-splitting is intentional (no spaces in paths).
9821007
# shellcheck disable=SC2086
9831008
conftest test --combine --namespace dockerfile -p config/conftest/policy $files .mise
@@ -1289,6 +1314,11 @@ description = "Regenerate .dockerignore from .gitignore + Docker-only excludes"
12891314
# A bare pattern there is root-anchored -- whereas a slash-less .gitignore pattern matches at any depth.
12901315
# So mirror .gitignore, add the Docker-only excludes (`.git/`, plus the build recipe + docs so editing
12911316
# them doesn't bust the `COPY . .` cache), and `**/`-prefix each non-anchored pattern.
1317+
# `.dockerignore` itself is deliberately NOT excluded any more. Excluding it saved nothing (the file is tiny) and
1318+
# meant it was absent from the build context, so `et-cli` -- which derives each scenario's per-Dockerfile ignore
1319+
# file from it -- could not run inside an image built from this Dockerfile:
1320+
# called `Result::unwrap()` on an `Err` value: Io(... NotFound ... path: "/workspace/.dockerignore")
1321+
# That surfaced only in the docker build.yaml lanes, because every other environment has the file on disk.
12921322
# goawk emits CRLF on Windows, so the whole block is piped through `tr -d '\r'` -- .dockerignore must stay
12931323
# LF-only (editorconfig enforces it, and CRLF would also make dockerignore-check see a spurious diff).
12941324
run = '''
@@ -1299,7 +1329,7 @@ run = '''
12991329
echo
13001330
{
13011331
cat .gitignore
1302-
printf '%s\n' '.git/' 'Dockerfile*' '/README.md' '.dockerignore'
1332+
printf '%s\n' '.git/' 'Dockerfile*' '/README.md'
13031333
} | goawk '
13041334
/^[[:space:]]*#/ { print; next }
13051335
/^[[:space:]]*$/ { print; next }
@@ -1514,7 +1544,8 @@ description = "Run the WebSocket server, restarting on .rs/.toml changes"
15141544
run = "watchexec --restart --clear --exts rs,toml -i '**/tests/**' --shell=none -- cargo run -p et-ws-server"
15151545

15161546
[tasks.ws-server.env]
1517-
OTLP_AUTH_PASSWORD = "1234"
1547+
# Must match ZO_ROOT_USER_PASSWORD in config/o2.env; the server authenticates its OTLP exports as that root user.
1548+
OTLP_AUTH_PASSWORD = "Complexpass#123" # skipcq: SCT-A000 -- local dev-only credential, committed deliberately
15181549
OTLP_AUTH_USERNAME = "root@example.com"
15191550

15201551
[tasks.ws-server-no-otel]
@@ -1753,7 +1784,7 @@ description = "Run both the ws-server and ws-wasm-agent using Chrome"
17531784

17541785
[tasks.openobserve]
17551786
alias = "o2"
1756-
run = "docker run --rm --name openobserve -p 5080:5080 --env-file config/o2.env {{ vars.o2_image }}"
1787+
run = "docker run --rm --name openobserve -p {{ vars.o2_publish }} --env-file config/o2.env {{ vars.o2_image }}"
17571788

17581789
[tasks.demo]
17591790
depends = ["openobserve", "ws-server"]
@@ -2059,6 +2090,32 @@ description = "Install every language toolchain (all .mise/config.<lang>.toml)"
20592090
run = 'MISE_ENV="$ALL_LANGS" mise install'
20602091
shell = "{{ vars.task_shell_plain }}"
20612092

2093+
# Fail when a full install resolves a different tool set or version than the committed lockfiles record.
2094+
# Only the platform-independent fields are compared, because mise fills a lockfile's per-platform rows in lazily on
2095+
# whichever platform runs the install: `checksum`, `url`, `url_api`, `provenance` and `provenance_verified` appear
2096+
# for macos-arm64 on a developer's machine and for linux-x64 on a runner. A whole-file `git diff` therefore can
2097+
# never be clean across the five-platform matrix -- every lane legitimately adds its own rows, and the committed
2098+
# lockfile already carries a checksum for some platforms of a tool and not others. The `[[tools.<name>]]` headers
2099+
# and their `version` / `backend` are identical everywhere, so those are what this gates on: a tool added, removed
2100+
# or bumped without regenerating the lockfiles is caught, while a lane recording its own checksums is not.
2101+
# install-all rather than a bare install, so guest-language tools reach their lockfiles too -- MISE_ENV only
2102+
# loads a subset, and a partial install leaves every unloaded config's lockfile unexamined.
2103+
[tasks.mise-lock-check]
2104+
depends = ["install-all"]
2105+
description = "Fail if .mise/mise*.lock records a different tool set or version than a full install resolves"
2106+
# `|| true` because rg exits 1 on no matches, which under pipefail would abort the whole task on the clean path.
2107+
# `^[-+][-+]` drops git's `+++`/`---` file headers; `@@` hunk headers never match the leading `[-+]`.
2108+
run = """
2109+
drift=$(git diff -U0 -- .mise/mise*.lock | rg "^[-+]" | rg -v "^[-+][-+]" |
2110+
rg "^[-+](version|backend) = |^[-+]\\[\\[tools\\." || true)
2111+
if [ -n "$drift" ]; then
2112+
echo "$drift"
2113+
echo "Lockfile tool set/versions differ from install-all; commit the regenerated .mise/mise*.lock" >&2
2114+
exit 1
2115+
fi
2116+
"""
2117+
shell = "{{ vars.task_shell }}"
2118+
20622119
[tasks.all]
20632120
# Generic stand-in: run any command with every language env enabled.
20642121
# mise appends the task's CLI args after `mise exec --`, e.g. `mise run all zig version`.

.mise/mise.lock

Lines changed: 34 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.mise/mise.python.lock

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)