High-signal, durable realizations from building dig-installer. Concise facts with context — not a change diary. See CLAUDE.md → §4.5 for how this is maintained.
Auto-update beacon registration (#514): dig-updater schedule install is idempotent — unlike dig-node's install
dig-node's own install verb is NOT idempotent (task #232's whole reason for the stop-before-write/
Skip-doesn't-reinstall dance): re-running it over an already-registered service hard-fails on
Windows SCM / macOS launchd ("already exists"), so register_dig_node tolerates that failure and
relies on start as the real signal. dig-updater schedule install/schedule uninstall are the
OPPOSITE: schtasks /Create … /F always overwrites, systemctl enable --now is idempotent, and
launchd's own registration path bootouts any prior registration before rebootstrapping — so a
re-install always succeeds cleanly. This installer's beacon::register/unregister therefore call
the scheduler unconditionally on every Install/Update/Skip decision for dig-updater (never
gated on the version-decide outcome the way dig-node/dig-dns's registration is) — a genuinely
different, simpler contract than every other delegated-subcommand component in this crate, worth
keeping in mind before copying the dig-node pattern onto a new component by reflex.
Testing the detect→compare→decide pipeline (src/update.rs) end-to-end (not just the pure
decide() matrix) needs a fake binary at the EXACT path resolve_component would place the real
one at — bin_dir.join(target.exe_name(stem)), i.e. literally digstore.exe on Windows, not some
test-chosen name. That kills the doctor.rs/service.rs "write a .cmd/shell-script stub" trick
for a genuine present-and-PARSEABLE (Skip/Update-by-version) integration test: Windows' CreateProcess
only special-cases .bat/.cmd/.exe-associated extensions for that shim, and a plain file named
digstore.exe containing batch-script text is NOT dispatched through cmd.exe — it's read as a
(broken) PE and fails to launch. A cross-platform-safe integration test can therefore only cheaply
prove absent → Install (no file at all) and present-but-unrunnable → Update (any garbage file
at the exact dest fails to spawn on every OS, landing in the "unreadable" reinstall branch) — the
Skip/Update-by-real-version-compare cells stay covered by update.rs's pure decide() unit tests
(which take DetectedVersion directly, no process spawn), not a full-pipeline integration test. A
genuine end-to-end Skip test would need a real compiled per-OS stub binary — not worth the CI weight
for what the pure matrix already proves.
Every DIG-Network release tag is a bare git-cliff vMAJOR.MINOR.PATCH (no pre-release/build
metadata in practice), so update.rs's comparator is a hand-rolled 3-part SimpleVersion rather
than pulling in the semver crate: it is (a) all this installer will ever need, (b) trivially
correct/testable, and (c) keeps the module dependency-free for its planned #504-B extraction into
dig-release-resolver. A version string that doesn't fit X.Y.Z (a real pre-release tag, a
foreign/garbled --version output) deliberately fails to parse rather than being approximated —
decide() treats "can't parse" as "reinstall to be safe" either way, so under-parsing costs nothing.
Register dig-dns's OWN run-service SCM entrypoint DIRECTLY — a host-shim caused the 1053 (task #494/#499)
The field bug (dig_ecosystem#499): installing dig-dns as a Windows service failed with SCM error
1053 ("the service did not respond to the start request in a timely fashion"). ROOT CAUSE was an
indirection: the installer registered its OWN binary as the service, running a hidden
run-dig-dns-service host-shim that child-spawned dig-dns serve. The host process's
StartServiceCtrlDispatcher/RUNNING handshake was gated behind spawning the child, so the SCM's
start-timeout could elapse before RUNNING was reported.
FIX: dig-dns v0.9.0+ ships its OWN Service Control Protocol entrypoint, dig-dns run-service, which
reports SERVICE_RUNNING to the SCM before any slow startup work. The installer now registers the
SCM service to run dig-dns.exe run-service directly (program = the dig-dns binary, args =
["run-service"]) — no host shim (src/dns/service_host.rs + the run-dig-dns-service subcommand
DELETED, windows-service dep dropped). One coherent service process. An explicit dig-node override
is baked into the service ENVIRONMENT as DIG_NODE_URL (dig-dns config::ENV_NODE_URL, a byte-identical
cross-repo contract), which run-service reads; ServiceInstallCtx.environment carries it.
Scope note: dig-dns (v0.9.0+) NOW has its own install/uninstall/start/stop/status/run-service
verbs (it previously had none — old entries elsewhere calling dig-dns "a plain CLI with no service code"
are superseded). But this installer STILL owns the surrounding per-OS wiring — the .dig NRPT rule /
split-DNS resolver, the Chrome/Edge DoH policy, and dig-dns doctor self-verification — plus the
canonical dns::plan::SERVICE_LABEL/SERVICE_DISPLAY_NAME it registers under. On macOS/Linux the
service runs dig-dns serve directly (no SCM timeout there).
Also: service-manager v0.7's ScServiceManager::install (Windows) ALWAYS sets displayname= to
the qualified service name at create time — ServiceInstallCtx has no field to override it. A
custom human-friendly display name (e.g. "DIG NETWORK: DNS") must be applied as a follow-up
sc config <name> displayname= "<display>" call, and VERIFIED by reading it back via
sc qc <id> DISPLAY_NAME (svc::verify_display_name) — sc config can appear to succeed while the
panel still shows the raw service id (the #499 display-name symptom).
The installer's DEFAULT is the full 3-component stack, and boot-start is delegated vs owned (task #301)
dig-installer installs digstore + dig-node + dig-dns by default (opt out with
--no-<component>); dig-relay + DIG Browser stay opt-in. The default lives in
ONE place — InstallPlan::default() in src/lib.rs — and main.rs just maps
--no-*/--with-* onto it (with_x = cli.with_x || !cli.no_x, so the --with-*
flags are redundant-but-accepted). help_json()'s components[].default mirrors
this and is the machine-readable contract an agent reads.
Boot-start is registered two different ways, and that split matters:
- dig-node owns its own service lifecycle, so the installer just runs
dig-node install— which itself setsautostart: true(dig-node-service'sservice::install). We must NOT invent a manual-start variant; boot-start is the delegated default. The installer-side contract is only "invoke plaininstall" (service::install_args() == ["install"]). - dig-dns ships NO service verbs, so the installer registers it directly via
the
service-managercrate. Boot-start is the single shared flagdns::plan::DNS_SERVICE_AUTOSTART(true) threaded intoServiceInstallCtx.autostarton all three OS modules — which maps to Windows SCMstart= auto, systemdenable, launchd load. The declarative systemdWantedBy=multi-user.targetand launchdRunAtLoadin the hand-rolled unit/plist bodies (dns::plan) are the belt to that suspenders. One named const keeps a manual-start regression a one-line, test-caught change.
The user-facing installer is "DIG Installer" (Tauri productName, window
title, TitleBar.jsx, identifier net.dig.installer) — but "DigStore" /
digstore legitimately stays as the CLI component it installs. A blanket
find-replace of "DigStore" would be wrong (and would break
tests::gui_copy_uses_canonical_ecosystem_vocabulary, which asserts "DigStore"
still appears in the wizard copy). The rebrand target is the two-word phrase
"DigStore Installer" only; the internal crate/lib identifiers
(digstore-installer, digstore_installer_lib) are deliberately left as-is
(not user-visible). tests::installer_is_branded_dig_installer_not_digstore_installer
guards the identity surfaces.
--dig-node-port defaulted to 8080 here (src/main.rs, src/service.rs) long
after dig-node itself moved its own default to 9778 (task #132 — an uncommon
high port, sibling of the dig-wallet HTTP API's 9777). Nothing failed: the
installer still ran, dig-node still started — it just silently registered the
service on the wrong port relative to what the extension / DIG Browser / the
§5.3 localhost tier now expect by default. A duplicated literal default (here:
the installer's own ServiceConfig::default() mirroring dig-node's
config::DEFAULT_PORT by convention rather than by reference, since they're
different binaries/repos) needs an explicit cross-repo grep whenever the
canonical value moves — SYSTEM.md recording the canonical port is necessary
but not sufficient; every consumer's own default literal has to be swept too.
hosts::resolve_dig_local() asks the real OS resolver (getaddrinfo/the Windows
equivalent, via std::net::ToSocketAddrs) whether dig.local maps to
127.0.0.2 — a genuine post-install verification, not a re-parse of the
installer's own hosts-file write (which would trivially always "pass"). The
pure comparison logic (hosts::resolve_host) is unit-tested by feeding it bare
IP literals ("127.0.0.2", "127.0.0.1") instead of hostnames: ToSocketAddrs
parses a literal directly with no I/O, so the success/mismatch branches are
deterministic and CI-safe. The "doesn't resolve" branch is tested with a
.invalid-TLD hostname (RFC 2606 reserved, guaranteed never to resolve) rather
than a made-up name, which could theoretically hit a search-domain suffix on
some networks. The real dig.local resolution itself is only exercised as a
manual/integration check post-install (mirrors how write_dig_local()'s actual
system-hosts-file write was never unit-tested either — see hosts.rs's
_at-suffixed pure-path variants for the testable core).
Both dig-node-service and this installer's own dig-dns wiring register OS
services via the service-manager crate pinned at 0.7.1, with
ServiceInstallCtx.contents: None (letting the crate generate the systemd
unit / launchd plist / SCM entry) and no explicit restart config. Checked the
crate source at tag v0.7.1 (GitHub API, since it isn't vendored locally) to
learn what that actually produces:
- systemd —
SystemdConfig::default().restartisSystemdServiceRestartType::OnFailure; the generated unit getsRestart=on-failureautomatically. Auto-restart-on-crash "just works" on Linux. - launchd —
LaunchdInstallConfig::default().keep_aliveistrue; the generated plist getsKeepAlive: true(+RunAtLoad: truefromServiceInstallCtx.autostart). Auto-restart-on-crash "just works" on macOS too. - Windows (SCM) —
src/sc.rs'sinstall()only shellssc create …; it never callssc failure/ChangeServiceConfig2to set recovery actions. Windows services do NOT restart on crash by default — this is a REAL gap, not a documentation gap. Filed as DIG-Network/dig_ecosystem#224 (indig-node-service, out of scope for this repo).
Lesson: "delegates to the service-manager crate" is not one behavior — its
per-OS default differs, and the only way to know which is to read that crate's
actual per-backend source for the pinned version (docs.rs/the crate's own docs
don't spell this out; ServiceInstallCtx's fields are the same across OSes,
but the manager's own config struct, which this installer/dig-node-service
never touch, is what carries the OS-specific default).
Before adding the stop-before-write/start-after-write install lifecycle,
audited dig-node-service's and dig-relay's actual install/uninstall/
start/stop/status implementations (both just thinly shell out to the
service-manager crate's sc/systemd/launchd backends via ? — no
"already installed"/"already running" pre-checks of their own):
installon an already-registered service hard-fails on Windows SCM ("already exists") and (typically) macOS launchd; systemd tends to succeed as a no-op. So a plain re-installduring an upgrade is NOT safe to treat as fatal — the installer now tolerates aninstallfailure and still attemptsstart(the registration still points at the same on-disk path this run just wrote, sostartpicks up the new binary regardless of whetherinstallitself succeeded).start/stopare also not idempotent —starton an already-running service andstopon a stopped one both commonly hard-fail on Windows/ macOS (systemd tends to tolerate both). Never assume any of these four verbs no-ops safely; onlystatusis safe to call unconditionally.status --json's envelope shape differs between the two binaries: dig-node returns a FLAT{"serving": bool, ...}; dig-relay returns a NESTED{"result": {"serving": bool, ...}}.statusnever hard-fails (alwaysOk, exit 0 when serving / 1 when not) but cannot distinguish "not installed" from "installed but stopped" — both read asserving: false. Neither binary exposes an "is it registered" verb, so the installer's stop-before-write step treats "binary absent at the destination path" (not "service not registered") as its "first install, nothing to stop" signal instead.- No OS-tool error string ("already exists", "not loaded", …) is a literal
constant in dig-node/dig-relay's own source — it's whatever
sc.exe/systemctl/launchctlprinted, passed through verbatim. Don't string-match those messages from a caller; branch onstatus'sservingboolean and treat everything else as an opaque, best-effort outcome recorded in a note.
bd4860a (2026-06-29) deliberately re-skinned the GUI from its original dark
cosmic surface to the clean white DIG product theme, citing SYSTEM.md →
"Canonical terminology & branding", which (as of this writing) still lists
"the installer GUI" among the product surfaces using the white theme
(dig.net/docs.dig.net are the only stated dark exceptions). Task #233
reverted it back to dark per an explicit user bug report. This leaves
SYSTEM.md's canonical-branding text and the installer's actual shipped
theme in direct disagreement — flagged for the orchestrator to resolve
(either add the installer GUI to the sanctioned-dark-exception list, or
this reversion needs revisiting) rather than silently drift again. Whoever
touches this theme next should check which way SYSTEM.md reads FIRST.
gui/app/src-tauri deliberately declares its own empty [workspace] table
(isolating it from the root workspace so the CLI never drags in Tauri), which
also meant no root-level cargo invocation in ci.yml ever touched it — it
was only ever compiled by release.yml's build-gui job, AFTER a version
tag was already pushed. Added gui-fmt/gui-clippy/gui-test/
gui-build-os-matrix/gui-frontend jobs scoped via --manifest-path.
Findings from actually turning these on:
- The checked-in
Cargo.lockfor a path-dependency drifts silently when nothing ever builds with--locked. BothCargo.locks (root and GUI) had the path-depdig-installerentry pinned at a version behind the liveCargo.toml(the GUI's lock still said0.4.0/0.5.0after a root version bump nobody re-locked against).cargo build --lockeddoesn't care about this drift normally — but it fails outright the moment the lock's recorded version differs from what the path dep's ownCargo.tomlreports, because--lockedforbids the resync. Direct, reproducible proof of the exact gap #238 closes: this had clearly been broken for at least one prior version bump and nothing caught it. Fix is a 1-line hand-edit of theversion = "..."field in the lock entry (path deps carry no checksum/source to reconcile) — far more minimal thancargo update -p dig-installer, which cascades into unrelated transitive version churn (observed: severalwindows-sys/getrandom/tempfiletransitive versions shifted) because unlocking one package still lets the resolver re-pick anything downstream of it. - A
#[cfg(windows)]-gated use site does NOT make an un-gatedconstdeclaration warning-free cross-platform.install.rs'sDIG_ICON_ICO(an embedded.icofor the Windows ProgID icon) was a plain top-levelconst, but its only reader was inside a#[cfg(windows)]block — invisible on ubuntu-latest/macos-14, so-D warnings(dead_code) failed there despite the crate being perfectly clean on native Windows. This is the exact class of bug the root crate'sbuild-os-matrixjob's own header comment warns about, now caught in the GUI crate too. Fix: cfg-gate the const itself (mirrors the pre-existingDIG_ICON_PNGsibling one line below it). - The
ERROR_ELEVATION_REQUIRED(Windows os error 740) quirk running this crate's compiled test binary does NOT reproduce on GitHub's hostedwindows-latestrunner — reproduced locally on a non-elevated local Windows console, but the experimentalgui-test-windowsCI job (added non-blocking specifically to observe this) passed clean on the hosted runner. Whatever local heuristic triggers it (binary name containing "installer"?) either doesn't apply, or the hosted runner's default console privilege context differs.gui-teststill runs its required copy on ubuntu-latest/macos-14 (this crate's only real test content is OS-agnostic pure logic), but this is a useful data point if the elevation question resurfaces — it is NOT a hosted-CI blocker. - Tauri/
wryon Linux needslibwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelffrom apt just to compile (not just bundle) —gui-clippy/gui-teston ubuntu-latest install these first, mirroringrelease.yml'sbuild-guiLinux step.cargo build/cargo test --no-runagainst this crate do NOT require the frontenddist/to exist first —tauri.conf.json'sbeforeBuildCommandonly fires under thetauriCLI (tauri build/tauri dev), never under plaincargo. - CORRECTION (#424):
cargo clippyis the exception to the bullet above — it DOES needdist/on a cold cache.src-tauri/src/lib.rs'stauri::generate_context!()readsfrontendDist("../dist") and PANICS at macro-expansion time if it's absent.cargo build/cargo test --no-runtolerate a missingdist/because a warm target dir reuses the cached rustc artifact without re-expanding the macro — butclippy-driverkeeps its OWN separate metadata and always re-expands it fresh, sogui-clippyfails the moment its cache is cold (verified directly:rm -rf target && cargo clippyfails withoutdist/present;cargo build/cargo test --no-runon the same cleantarget/do not).gui-clippy's cache key is keyed ongui/app/src-tauri/Cargo.lockalone, so ANY change that touches the rootdig-installercrate (a path-dependency of this one — a new field, a new module, a new upstream dep) forces a fresh recompile of this crate too, exposing the gap. Fixed by adding annpm ci && npm run buildstep togui-clippybefore itscargo clippycall (mirrorsrelease.yml'sbuild-guijob, which already builds the frontend vianpx tauri build'sbeforeBuildCommand).gui-fmt(pure parsing, no macro expansion) andgui-build-os-matrix/gui-test's compile step are unaffected — confirmed on a truly cleantarget/for both.
- A "success" line must be earned, not printed. The real bug: an un-elevated
run masked a
dig-node installexit-6 with a ✓, hitCreateService 1073, yet ended✓ DIG is ready. Lesson: the aggregate readiness verdict (ready/failuresonInstallReport) is computed from VERIFIED post-conditions, and the green line + zero exit are gated on it. A component-level failure that was only logged (never propagated) is the classic false-success trap. - Verify the SERVICE, not the port. The old post-install health check probed
rpc.discoveron 9778 — a dig-node started by ANYTHING (a manualserve, a stale process) answered, so the check passed without this run registering a service. Fix: query the OS service manager by the canonical service id (net.dignetwork.dig-node/-dns) —sc querySTATE=RUNNING /systemctl is-active/launchctl print state=running. The port probe is secondary detail only.svc.rsowns the pure parsers. - Enforce elevation FIRST, before any write. Registering a service / writing
hosts needs admin; check it before downloading/writing so an un-elevated run
fails fast (
NOT_ELEVATED) with zero partial state, rather than half-installing then failing on the privileged step.InstallPlan::requires_elevation()scopes it (dry-run / digstore-only never trips it). Detection: Windowsnet session, Unixid -u== 0. - "On PATH" means resolvable from a FRESH shell, not just "a file exists".
pathcheckspawns each CLI by BARE NAME with PATH augmented to include the install bin dir, so it proves name-resolution the way the user's next shell will see it. On Windows the PATH write is followed by aWM_SETTINGCHANGEbroadcast so new shells pick it up without a reboot.
run_report's release RESOLUTION always runs; only the DOWNLOAD/WRITE is gated on--dry-run.resolve_component/resolve_dig_nodecall the injectedReleaseResolverunconditionally;download_component(&c, plan.dry_run)is the only dry-run check. So a--dry-runinvocation that leaves a component SELECTED (dig-node/dig-dns default ON, #301) still makes a real GitHub API call to resolve its "latest" release —--dry-runmeans "don't write", not "don't touch the network". Twotests/cli.rse2e cases (the firewall-intent tests) learned this the hard way: passing no--dig-node-versionleft them racing/releases/lateston dig-node's own release timeline, reddening dig-installer's CI during ANY dig-node release-in-progress window (dig_ecosystem#524, surfaced by U7/#309's PR). Fix: PIN--dig-node-versionto a specific, permanently-published tag in any e2e test/CI job that leaves a tracked component selected under--dry-run— a tagged release's asset list never changes shape after publish, sorelease_by_tagis deterministic where/releases/latestis not. Applies equally to the 3-OS installer e2e job (#502): itsDIG_NODE_VERSION/DIG_DNS_VERSIONare pinned constants, never "latest".- Running the installer as a real end user does (elevated) needs
sudo -E, not baresudo, on Linux/macOS.daemon_dir.rs's Unix ACL step readsSUDO_USER(whichsudoalways exports regardless) to grant the real interactive account read access to the machine-wide state dir, but-Eadditionally keeps$HOMEpointed at the invoking user rather than root's — matching what a real UAC-elevated-as-yourself Windows run does (elevation.rsexplicitly refuses a SYSTEM-token run for the identical reason, #499). Usesudo -Ewhen scripting a real (non--dry-run) install/uninstall in CI or locally. - dig-dns's default gateway loopback IP (127.0.0.5) has no macOS alias out
of the box. Unlike Linux/Windows (which accept the whole 127.0.0.0/8
range on the loopback interface), macOS only aliases 127.0.0.1 on
lo0by default — withoutsudo ifconfig lo0 alias 127.0.0.5 upfirst, dig-dns's gateway can't bind either its primary or fallback port, sodns.paths_livestays empty and the aggregatereadyverdict false even though the service process itself is registered/running fine (mirrors the same gotcha dig-dns's own CI already documents).
Running the actual dig-installer binary end-to-end for the first time — the whole point of the
3-OS installer-e2e job — found THREE real, previously-invisible bugs in one pass, none catchable by
the existing mocked-resolver/mocked-service-backend unit suite:
- A delegated subcommand's INHERITED stdio corrupts
--jsonmode.service::run_dig_node/run_relayused.status()(inheriting the child's stdio "so the user sees dig-node's own messages"). That's fine in PRETTY mode, but in--jsonmode dig-installer's OWN progress goes to STDERR (eprintln!) so ONLY its final JSON line reaches stdout — except the child's inherited stdio bypasses that routing entirely, writing its own prose directly onto the SAME stdout fd, ahead of the JSON line. Every existing--jsone2e test happened to be--dry-run(which never spawns the subprocess), so this was invisible until a REAL install ran for the first time. Fixed:run_capturing(Command::output(), never.status()) captures unconditionally; a failure folds the captured text into theErr(nothing lost) and a success just discards it (dig-installer's own confirmation line already covers the event). Lesson: an inherited-stdio child process is incompatible with ANY "stdout is machine-readable" contract — capture always, surface via your OWN reporting layer instead. - Linux service-health check was scope-blind.
svc::service_run_state_on(Os::Linux, ...)ran a baresystemctl is-active <id>(system scope only) — but dig-node's owninstallunconditionally prefers a USER-level unit (PREFERS_USER_LEVELin dig-node-service, a deliberate no-elevation design), while dig-installer's dig-dns wiring is machine-wide. A single system-scoped query could NEVER see a genuinely-running dig-node, permanently reporting "registered but NOT running" even on a perfectly healthy install. Fixed: query BOTHsystemctl --user is-activeandsystemctl is-active, Running wins if either says so (combine_systemctl_states) — scope-agnostic rather than hardcoding which service registers where. - The canonical reverse-DNS id is NOT the real systemd unit name, on Linux only — for BOTH
services. Even after fixing the scope-blindness above, dig-node STILL read "registered but NOT
running" — a direct
systemctl --user status net.dignetwork.dig-nodediagnostic step (added to the e2e job specifically to chase this) revealed the REAL unit wasdignetwork-dig-node.service, notnet.dignetwork.dig-node.service. Root cause: theservice-managercrate (v0.7.1) names Linux units viaServiceLabel::to_script_name(), which DROPS the reverse-DNS qualifier ("net") and hyphen-joins{organization}-{application}—net.dignetwork.dig-node→dignetwork-dig-node. Windows (sc.rs) and macOS (launchd.rs) both useto_qualified_name()instead (dots preserved verbatim), so ONLY Linux drifts from the canonical id — an easy thing to miss since the id LOOKS platform-neutral. This bit dig-dns too, and worse: dig-installer's OWNdns/linux.rsALSO registers dig-dns through this SAMEServiceLabelmachinery (net.dignetwork.dig-dnsparsed + installed viaservice-manager), yet a SEPARATE hardcoded constant (dns::plan::SERVICE_SCRIPT_NAME = "dig-dns") — which LOOKED like the obvious dashed form — was used everywhere ELSE (existence checks, uninstall, notes) to refer to it. The REAL registered name isdignetwork-dig-dns, sounit_registered()'s clean-reinstall detection had ALSO been silently checking a unit that was never actually written, this whole time, entirely independent of the health-check bug. Fixed BOTH:dns::plan::service_script_name()now DERIVES the name (same transformation dig-dns's own registration applies, so the two can't drift apart again), andsvc::linux_unit_namegenerically parses ANY canonical id through the sameServiceLabel+to_script_name()rather than hardcoding either result. Lesson: when a health check OR an existence check crosses a THIRD-PARTY library's own naming transformation, verify it against a REAL running instance on EVERY platform it claims to support, and DERIVE shared identifiers from ONE source rather than hand-copying a name that "looks right" into a second constant — a mock/unit test that supplies the parser with hand-written "active\n" text, or asserts a hardcoded constant equals itself, can never catch "we're asking systemctl about a unit that was never registered under that name in the first place." - Root has no systemd
--user/D-Bus session by default (Linux, NOT yet fixed — dig_ecosystem#526). Because dig-installer's elevation gate runs the WHOLE process as root whenever dig-node/dig-dns are selected, and dig-node'sinstallalways targets--userscope, a realsudo dig-installerrun genuinely CANNOT register dig-node on Linux today (systemctl --userfails with "Failed to connect to bus: Operation not permitted" — root has no session unless one is explicitly provisioned, e.g.loginctl enable-linger root+XDG_RUNTIME_DIR=/run/user/0, the workaround the e2e CI job itself now applies). This is a genuine cross-repo design gap (dig-installer + dig-node), not merely a CI artifact — filed as dig_ecosystem#526 rather than silently worked around in production code. - Child
Commands flash a console window on Windows unlessCREATE_NO_WINDOWis set (#564). A GUI/no-console parent (the Tauri installer) that spawns a console-subsystem child (sc,net,netsh,powershell,icacls,whoami,cmd, or a delegated dig-node/dig-dns/dig-updater verb) gets a brand-new console allocated for that child, which flashes on screen + steals focus for the child's lifetime — a storm of blinking boxes across a 15+-spawn install. Fix: the Win32CREATE_NO_WINDOW(0x08000000) creation flag on EVERY spawn, applied crate-wide via the oneproc::HideConsole::hide_console()helper (no-op off Windows) rather than a literal per site. The flag hides ONLY the console —.output()stdio capture and exit codes are untouched — andstd::process::Commandexposes no getter for its creation flags, so the helper is tested behaviourally (a hidden child still runs + its stdout is still captured), not by reading the flag back. Same defect lives indig-updater-broker's own schtasks/service spawns — its own lane. - Adding a footer action can clip the primary at the window's min width (#564). The Done screen's
footer is a fixed-height flex row; adding the Close button pushed its content to ~567px, which
overflowed (and clipped Launch Terminal) at the old 880px
minWidtheven though it was perfect at the 1080px default.overflowupstream hid the clip rather than scrolling, sodocumentElementshowed no horizontal scroll — measurefooter.scrollWidth > footer.clientWidth, not the document, to catch it. Fix: raisetauri.conf.jsonminWidth880 → 980 so the three-action footer always has room. Lesson: a new nav/footer control must be re-verified at the window's MINIMUM size, not just its default.
select_asset matches by OS/arch + extension only — the stem param is a TIE-BREAKER, not a filter (#548)
Adding the dign/digd alias binaries surfaced a real (and, on reflection, desirable) property of
asset.rs::select_asset: the stem argument never DISQUALIFIES a candidate — it only orders
multiple candidates that already matched on OS/arch token + accepted extension (stem_rank in the
scoring tuple). Consequently, resolving an alias whose OWN asset is genuinely absent from a release
(an old/pinned tag published before the alias existed) does NOT raise ASSET_NOT_FOUND as long as
the release has ANY OTHER raw-binary asset for that OS/arch — select_asset silently returns the
PRIMARY's own asset under the alias's query. This is harmless-by-design here (an alias and its
primary are byte-for-byte the same shape, so downloading the primary's asset and placing it at the
alias's dest is a correct fallback, not a bug) — but it means "no dedicated <alias>-* asset
published for this tag" is not independently testable via select_asset/resolve_component
returning None; it can only be exercised by making the WHOLE release lookup fail for the alias's
Repo (a different repo entirely, e.g. dign's pre-rename dig-companion fallback divergence from
Repo::dign()). Two tests that assumed the former (giving a release with only primary-stem assets
and asserting the alias resolves to None) were flawed and had to be deleted — asset name matching
in this crate is genuinely permissive by design, not per-stem-strict.