-
Notifications
You must be signed in to change notification settings - Fork 0
1792 lines (1673 loc) · 94.7 KB
/
Copy pathinstaller-e2e.yml
File metadata and controls
1792 lines (1673 loc) · 94.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# The 3-OS installer end-to-end contract test (dig_ecosystem#502).
#
# Every other CI job here (ci.yml) proves the installer's PURE logic (asset
# selection, the update decision, the JSON contract) with mocked resolvers and
# service backends. Neither dig-node's nor dig-dns's OWN "service-smoke" CI
# (their repos) covers what a real END USER actually runs: the `dig-installer`
# BINARY, installing BOTH services in one pass and reporting its own aggregate
# "DIG is ready" verdict (#493). This job drives exactly that, against the
# REAL OS service manager, on all three OSes DIG ships for:
#
# build dig-installer -> run it (installing pinned dig-node + dig-dns +
# dig-updater releases) -> assert the #493 fail-loud readiness verdict sees
# BOTH services registered + RUNNING by their canonical id, the Windows SCM
# display names read back correctly (#494), `dig.local` resolves (#140), AND
# the auto-update beacon's daily scheduler artifact is registered by the
# real OS scheduler (#514) -> run --uninstall-dig-node/--uninstall-dig-dns/
# --uninstall-dig-updater -> assert both services are deregistered, the
# hosts entry is gone, and the beacon's scheduler artifact is removed.
#
# dig_ecosystem#524: the dig-node/dig-dns versions below are PINNED to a
# specific released tag (never "latest"), so this job — like the two
# `tests/cli.rs` cases it mirrors — stays deterministic across either repo's
# release-in-progress window instead of racing GitHub's `/releases/latest`.
# Bump these deliberately when validating against a newer release.
# DIG_DNS_VERSION is pinned to 0.15.1, a stable release with all required
# binaries (dig-dns and digd) needed by the installer's default component set.
name: Installer e2e (install -> health -> uninstall)
on:
pull_request:
branches: [main]
paths:
- "src/**"
- "tests/**"
- "Cargo.toml"
- "Cargo.lock"
- ".github/workflows/installer-e2e.yml"
push:
branches: [main]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
DIG_NODE_VERSION: "0.29.0"
DIG_DNS_VERSION: "0.15.1"
# Auto-update beacon (#514) — pinned for the same determinism reason as the
# two above (dig_ecosystem#524): a specific tagged release, never "latest".
DIG_UPDATER_VERSION: "0.6.0"
# Authenticates the installer's GitHub release-API lookups (never the asset
# download itself) so this job doesn't race the unauthenticated 60/hour
# per-IP cap GitHub Actions' shared runner pool routinely exhausts — see
# `download::get_text_with_token` / SPEC.md §1's "Optional GitHub API
# authentication" note.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
e2e:
name: install -> health -> uninstall (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [windows-latest, macos-14, ubuntu-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 15
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install Rust (stable)
shell: bash
run: |
rustup toolchain install stable
rustup default stable
rustc --version
- uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
target
key: ${{ runner.os }}-installer-e2e-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-installer-e2e-
# dig-dns's default gateway loopback IP (127.0.0.5, SPEC S5) has no
# alias on lo0 out of the box on macOS (unlike Linux/Windows, which
# accept the whole 127.0.0.0/8 range) — without it the gateway can't
# bind either its primary or fallback port, so dig-dns never reaches a
# live resolution path (mirrors dig-dns's own service-smoke setup).
- name: Alias the dig-dns loopback IP on lo0 (macOS)
if: runner.os == 'macOS'
run: sudo ifconfig lo0 alias 127.0.0.5 up
- name: Build dig-installer (release)
run: cargo build --release --locked --bin dig-installer
- name: Resolve the built binary + a stable, HOME-independent bin dir
shell: bash
run: |
if [ "${{ runner.os }}" = "Windows" ]; then
echo "INSTALLER=$(pwd)/target/release/dig-installer.exe" >> "$GITHUB_ENV"
else
echo "INSTALLER=$(pwd)/target/release/dig-installer" >> "$GITHUB_ENV"
fi
if [ "${{ runner.os }}" != "Windows" ]; then
# dig-dns's Linux systemd unit runs as a DEDICATED, unprivileged
# service account (CAP_NET_BIND_SERVICE only, dns/linux.rs) —
# `${{ runner.temp }}` lives under `/home/runner`, whose default
# 0750 permissions that account cannot traverse (systemd reports
# this as `status=203/EXEC`, a bare exec failure — found via this
# job). `/opt` is world-traversable, matching what a real Linux
# package (`.deb`) would install into. Root-owned 0755 also satisfies
# the #565 H3 install-root ACL verify (no group/other write).
#
# macOS uses the same directory (#1748): `${{ runner.temp }}` lives
# under `/Users/runner`, owned by uid 501, and an ELEVATED install
# into a directory a non-root account owns is now refused outright —
# correctly, since that account could replace a binary root executes.
#
# `/opt` itself is normalised to 755 FIRST. The GitHub runner image
# ships it mode 777, and since #1748 the install-root verify walks
# EVERY level: write permission on `/opt` is permission to rename
# `/opt/dig-bin` aside and substitute an attacker-owned directory of
# the same name, so a root-owned leaf under a world-writable parent is
# correctly refused. Repairing the image's own `/opt` is the honest
# fix here; the installer deliberately does not re-mode a directory
# the distribution owns.
sudo chmod 755 /opt
# And the veneer: the runner image ships /usr/local/bin world-writable, which the install now
# refuses because it is where root's PATH resolves DIG commands from (#1748 WU3).
sudo chmod 755 /usr/local/bin && sudo chown 0:0 /usr/local/bin
sudo mkdir -p /opt/dig-bin && sudo chmod 755 /opt/dig-bin
echo "BIN_DIR=/opt/dig-bin" >> "$GITHUB_ENV"
else
# #565 H3: a privileged (service-executed) install into a custom
# --bin-dir is now ACL-verified fail-loud, exactly like the protected
# root — a LocalSystem service must not run a binary from a
# user-writable dir. `${{ runner.temp }}` inherits a Users-writable
# ACE, so harden this custom dir admin-only FIRST (mirror Program
# Files' DACL + the root-owned Linux /opt/dig-bin above): SYSTEM +
# Administrators full, Users read+execute, inheritance removed.
# `MSYS_NO_PATHCONV=1` keeps git-bash from mangling icacls's
# `/inheritance:r`/`/grant:r` flags (same pitfall as `schtasks /Query`).
BIN_DIR="${{ runner.temp }}/dig-bin"
mkdir -p "$BIN_DIR"
MSYS_NO_PATHCONV=1 icacls "$(cygpath -w "$BIN_DIR")" /inheritance:r \
/grant:r '*S-1-5-18:(OI)(CI)F' \
/grant:r '*S-1-5-32-544:(OI)(CI)F' \
/grant:r '*S-1-5-32-545:(OI)(CI)RX'
echo "BIN_DIR=$BIN_DIR" >> "$GITHUB_ENV"
fi
# The installer registers OS services and writes the hosts file — the
# SAME Administrator/root requirement a real end-user install needs
# (elevation.rs's pre-install guard, #492). GitHub's hosted Windows
# runner already executes as Administrator (mirrors dig-node's/
# dig-dns's own service-smoke jobs, which need no elevation dance
# either); Linux/macOS need `sudo` (passwordless on hosted runners).
# `-E` preserves the invoking user's HOME/USER so daemon_dir's
# SUDO_USER-scoped read ACL (#501) resolves the real interactive
# account rather than root's.
- name: Elevate on Linux/macOS
if: runner.os != 'Windows'
run: echo "SUDO=sudo -E" >> "$GITHUB_ENV"
# dig-node's own `install` prefers a USER-level systemd unit on Linux
# regardless of privilege (dig-node's service.rs `PREFERS_USER_LEVEL`,
# a deliberate no-elevation-needed design) — but the elevated run above
# makes dig-node's subprocess execute AS ROOT (uid 0), which has no
# systemd/D-Bus user session of its own by default, so `systemctl
# --user` fails ("Failed to connect to bus: Operation not permitted",
# dig_ecosystem#524 finding). Fix: give root its OWN lingering user
# session (mirrors dig-node's own service-smoke.yml, which does the
# same for its unprivileged runner-user run) and point every later
# step's `XDG_RUNTIME_DIR` at it so both the elevated install AND this
# job's own verification/uninstall queries can reach it.
- name: (Linux) bring up a systemd/D-Bus user session for root
# This workaround stays HERE and only here, deliberately. This leg passes
# `--bin-dir`, which #565 FORCES to user scope, so the unit really is
# per-user and reading it really does need root to have a session bus —
# the workaround is load-bearing for THIS leg's shape, not a leftover.
#
# It is the DEFAULT-path install that must never need it, and that is
# asserted in the `system-scope-e2e` job below, which carries no linger
# and fails if the install turns it on. Removing it here would only break
# a leg whose scope is forced, while proving nothing about #526.
if: runner.os == 'Linux'
run: |
sudo loginctl enable-linger root
sudo systemctl start user@0.service
for _ in $(seq 1 10); do
[ -S "/run/user/0/bus" ] && break
sleep 1
done
echo "XDG_RUNTIME_DIR=/run/user/0" >> "$GITHUB_ENV"
# #1748 REGRESSION GATE (part 1 of 2): reproduce the precondition that broke
# dig-app autostart. An earlier PRIVILEGED step leaves root-owned levels in the
# target user's XDG config chain -- `sudo -E` leaks
# XDG_CONFIG_HOME=/home/runner/.config into the root process, and a
# `systemctl --user` run as root then creates `.config/systemd/user` as root:root
# (observed in run 30402180957). `mkdir -p` is then a silent no-op and only the
# redirect fails, so the unit is never written while the install still reports
# ready. Created explicitly here so the ordering is driven by THIS job rather than
# by whatever a future dig-node happens to do.
- name: (Linux) #1748 precondition — an earlier component leaves a root-owned XDG level
if: runner.os == 'Linux'
run: |
sudo mkdir -p /home/runner/.config/systemd/user
sudo chown root:root /home/runner/.config/systemd /home/runner/.config/systemd/user
ls -ldn /home/runner/.config /home/runner/.config/systemd /home/runner/.config/systemd/user
# digstore is out of THIS contract's scope (it has its own installed-
# binary integration tests, CLAUDE.md §3.5) — skipped here to keep the
# run focused on the dig-node + dig-dns service contract #502 tracks.
# A CI runner is not an interactive desktop, and dig_ecosystem#919 makes the installer SKIP
# dig-app's login autostart on a host with no graphical session — correctly, since a tray agent
# has nowhere to appear and an `HKCU\…\Run` value written from Session 0 belongs to nobody who
# logs in. Both facts below are the ones the installer reads, named here so this job exercises
# the REGISTERING path (the write, its ownership, and the artifact location) rather than the
# skip. The skip itself is covered by `autostart::tests` for all three operating systems.
- name: Represent an interactive desktop session, so the autostart write path is exercised
shell: bash
run: |
if [ "$RUNNER_OS" = "Linux" ]; then
echo "DISPLAY=:99" >> "$GITHUB_ENV"
elif [ "$RUNNER_OS" = "Windows" ]; then
echo "SESSIONNAME=Console" >> "$GITHUB_ENV"
fi
- name: Install (dig-node + dig-dns + the auto-update beacon, pinned versions)
run: |
$SUDO "$INSTALLER" \
--no-digstore \
--dig-node-version "$DIG_NODE_VERSION" \
--dig-dns-version "$DIG_DNS_VERSION" \
--dig-updater-version "$DIG_UPDATER_VERSION" \
--bin-dir "$BIN_DIR" \
--json | tee install.json
# Diagnostics only — never gates the job (`if: always()`). Surfaces the
# REAL systemd-level reason if dig-node's `--user` unit reports
# anything other than active, since svc.rs's health check only sees
# is-active's one-word verdict, not WHY. Queries the REAL unit name
# (`dignetwork-dig-node`, see `svc::linux_unit_name` — the
# `service-manager` crate drops the "net" qualifier on Linux, #524),
# never the canonical `net.dignetwork.dig-node` id Windows/macOS use.
- name: (Linux) diagnose the dig-node --user unit's real systemd state
if: always() && runner.os == 'Linux'
run: |
echo "XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR"
sudo -E systemctl --user status dignetwork-dig-node --no-pager -l || true
sudo -E systemctl --user list-units --all --no-pager | grep -i dig || true
sudo -E journalctl --user -u dignetwork-dig-node --no-pager -n 60 || true
- name: (Linux) diagnose the dig-dns system unit's real systemd state
if: always() && runner.os == 'Linux'
run: |
sudo systemctl status dignetwork-dig-dns --no-pager -l || true
sudo journalctl -u dignetwork-dig-dns --no-pager -n 60 || true
sudo ss -ltnp 2>/dev/null | grep -i ':80\|:8053' || true
# #1748 REGRESSION GATE (part 2 of 2): dig-app MUST be registered to start at
# login. Nothing asserted this before, which is exactly why the regression was
# invisible -- autostart is best-effort, so `ready` stayed true while dig-app
# would never start. A tray agent that installs and never launches is the failure
# this contract exists to prevent, one layer over.
- name: Assert dig-app autostart registered (#1748), or a REPORTED skip (#919)
run: |
jq -c '.result.autostart' install.json
# Registered, or skipped for a stated reason — and NEVER "not registered with no reason",
# which is what a genuine registration failure looks like. The step above names a session,
# so a green run here is the REGISTERED arm; the disjunction exists so a runner image
# change surfaces as a legible outcome instead of an unexplained red.
jq -e '.result.autostart
| (.registered == true and .disposition == "register")
or (.registered == false
and (.disposition == "skip-headless" or .disposition == "skip-no-target-user"))' install.json
# This job names a session, so the registering arm is the one that must be taken.
jq -e '.result.autostart.registered == true' install.json
jq -e '.result.autostart.disposition == "register"' install.json
# And on Linux the artifact must really exist AND belong to the invoking user --
# a unit root owns is one `systemd --user` declines to load, so ownership is part
# of the property, not a nicety.
- name: (Linux) Assert the XDG autostart entry exists, is owned by the invoking user, and no stale unit remains
if: runner.os == 'Linux'
run: |
entry=/home/runner/.config/autostart/dig-app.desktop
ls -ldn "$entry" /home/runner/.config/autostart
test -f "$entry"
owner=$(stat -c %u "$entry")
test "$owner" = "$(id -u runner)" || {
echo "entry is owned by uid $owner, not by runner"
exit 1
}
dir_owner=$(stat -c %u /home/runner/.config/autostart)
test "$dir_owner" = "$(id -u runner)" || {
echo "entry DIR is owned by uid $dir_owner, not by runner"
exit 1
}
# #919: the mechanism must be one that STARTS something. A desktop entry is loaded at login
# with no enable step; the systemd user unit earlier versions wrote never was, so it must be
# gone rather than sitting alongside this and producing two agents at the next login.
grep -q '^Exec=' "$entry"
test ! -e /home/runner/.config/systemd/user/dig-app.service || {
echo "the stale systemd user unit is still present — two autostarts at next login"
exit 1
}
- name: Assert the #493 fail-loud readiness verdict — both components ready
run: |
jq -e '.ok == true' install.json
jq -e '.result.ready == true' install.json
jq -e '.result.failures == []' install.json
# #1774: dig-app must resolve a build this host can actually LOAD, on the
# default path — never the GTK-linked build handed over by the old
# shortest-name tiebreak that dies inside `ld.so` before `main` (#1753).
# The variant CHOSEN is incidental here (stock ubuntu-latest may lack
# libxdo.so.3, so even it can fall through to `-headless`) — the property is
# that a loadable, non-refused build was selected. The genuinely GTK-less
# `headless-linux-e2e` job below pins the `-headless` choice explicitly.
- name: Assert dig-app resolved a loadable, non-refused build (#1774)
run: |
DIGAPP='.result.components[] | select(.component=="dig-app")'
jq -e "[$DIGAPP] | length == 1" install.json
jq -e "$DIGAPP | has(\"refused\") | not" install.json
jq -e "$DIGAPP | .selected_variant != null" install.json
- name: (Linux) Assert the selected dig-app build is loadable and its DT_NEEDED resolve (#1774)
if: runner.os == 'Linux'
run: |
DIGAPP='.result.components[] | select(.component=="dig-app")'
jq -e "$DIGAPP | .loadable == true" install.json
dest=$(jq -r "$DIGAPP | .dest" install.json)
echo "placed dig-app at: $dest"
# The install verdict said loadable by PARSING; prove it on the host too —
# no DT_NEEDED of the placed binary may resolve to "not found".
if ldd "$dest" 2>&1 | grep -i "not found"; then
echo "FAIL: the placed dig-app build has unresolved shared libraries"
exit 1
fi
# #565: the install root a privileged service runs from was actually
# ACL-verified admin-only (not merely assumed), and no privileged
# registration's binPath resolves under a legacy/user-writable root.
# The audit + migration now run on this `--bin-dir` privileged leg too
# (#565 H3 completion — the gate is `installs_a_privileged_binary`, not the
# `--bin-dir`-false `installs_a_protected_component`), so the audit array is
# NON-EMPTY here: assert that explicitly so the `under_legacy_root` check is
# meaningful, not vacuously true over an empty list.
- name: Assert the #565 install-root is verified admin-only + the audit ran with no legacy-bound registration
run: |
jq -e '.result.install_root_security.checked == true' install.json
jq -e '.result.install_root_security.secure == true' install.json
jq -e '(.result.registration_audit | length) > 0' install.json
jq -e '[.result.registration_audit[] | select(.under_legacy_root)] | length == 0' install.json
# #623/#858: the installer provisions the privileged TLS root so dig-node can serve HTTPS on
# https://dig.local instead of plaintext. Assert the reported provisioning first (cross-OS, from
# the JSON), then — on unix, where the layout + ownership are directly checkable — that the root
# is REALLY root-owned 0700 with the CA + leaf present and the CA private key owner-only. This
# verifies THIS installer's deliverable; the dig-node-side "HTTPS listening" log depends on the
# pinned dig-node version consuming the root (coordinated separately) and is not asserted here.
- name: Assert the privileged TLS root was provisioned (#623/#858)
run: |
jq -e '.result.tls_root.created == true' install.json
jq -e '.result.tls_root.ca_minted == true' install.json
- name: Assert the TLS root is root-owned 0700 with a CA + leaf (unix, #623/#858)
if: runner.os != 'Windows'
run: |
set -e
root=/etc/dig/tls
# The root is deliberately 0700 root-owned — it holds the CA private key, so an unprivileged
# user must not be able to descend into it. That is exactly why the file checks below run under
# `sudo`: resolving `$root/ca.crt` needs SEARCH permission on the 0700 dir, which only root has.
# A plain `test -f "$root/ca.crt"` fails on permission (not absence) and would falsely read as
# "missing" against a correctly locked-down root. The install itself ran with `$SUDO`, so we
# inspect its output the same way. `stat` on the dir inode only needs search on /etc/dig (0755),
# but running it under sudo too keeps the whole assertion uniform.
sudo test -d "$root" || { echo "TLS root $root missing"; exit 1; }
owner=$(sudo stat -c %u "$root" 2>/dev/null || sudo stat -f %u "$root")
test "$owner" = "0" || { echo "TLS root owned by uid $owner, not root"; exit 1; }
mode=$(sudo stat -c %a "$root" 2>/dev/null || sudo stat -f %Lp "$root")
test "$mode" = "700" || { echo "TLS root mode $mode, expected 700"; exit 1; }
for f in ca.crt ca.key leaf.crt leaf.key; do
sudo test -f "$root/$f" || { echo "missing $root/$f"; exit 1; }
done
keymode=$(sudo stat -c %a "$root/ca.key" 2>/dev/null || sudo stat -f %Lp "$root/ca.key")
test "$keymode" = "600" || { echo "ca.key mode $keymode, expected 600"; exit 1; }
# dig_ecosystem#526: the registration's SCOPE and its reboot survival are now reported, so the
# e2e asserts them rather than only "installed". This leg passes `--bin-dir`, which is FORCED to
# user scope (#565: a machine-wide daemon must never be pointed at a caller-selected path), so
# the expected verdict here is user scope AND `survives_reboot: false` — stated positively, and
# from both sides, so a run that silently registered machine-wide from a caller-chosen root
# would FAIL here rather than look like an improvement.
- name: Assert the reported service scope matches the --bin-dir contract (#526)
if: runner.os != 'Windows'
run: |
jq -e '.result.service.scope == "user"' install.json
jq -e '.result.service.survives_reboot == false' install.json
jq -e '(.result.service.scope_note | length) > 0' install.json
# Windows has no per-user service domain, so the SCM registration is machine-wide and DOES
# survive a reboot with nobody logged in.
- name: Assert the reported service scope is machine-wide on Windows (#526)
if: runner.os == 'Windows'
run: |
jq -e '.result.service.scope == "system"' install.json
jq -e '.result.service.survives_reboot == true' install.json
- name: Assert dig-node registered + RUNNING by canonical id, dig.local resolves
run: |
jq -e '.result.service.installed == true' install.json
jq -e '.result.service.health_checked == true' install.json
jq -e '.result.service.health_ok == true' install.json
jq -e '.result.service.dig_local_resolves == true' install.json
- name: Assert dig-dns registered + RUNNING by canonical id, with a live resolution path
run: |
jq -e '.result.dns.installed == true' install.json
jq -e '.result.dns.service_running == true' install.json
jq -e '(.result.dns.paths_live | length) > 0' install.json
- name: Assert the auto-update beacon's daily scheduler registered (#514)
run: |
jq -e '.result.beacon.applied == true' install.json
if [ "${{ runner.os }}" = "Windows" ]; then
test -f "$BIN_DIR/dig-updater.exe"
test -f "$BIN_DIR/dig-updater-worker.exe"
else
test -f "$BIN_DIR/dig-updater"
test -f "$BIN_DIR/dig-updater-worker"
fi
# `MSYS_NO_PATHCONV=1` is REQUIRED: git-bash's automatic POSIX-path
# conversion mistakes `/Query` for a Unix-style absolute path and
# rewrites it to `<git-install-root>/Query`, so a bare
# `schtasks.exe /Query /TN …` fails with "Invalid argument/option -
# 'C:/Program Files/Git/Query'" — confirmed live on this exact job
# (dig_ecosystem#514). Every OTHER `sc.exe`/`schtasks`-style call in this
# file uses positional (non-`/flag`) args, so this is the first one to
# trip the well-known MSYS argv-mangling pitfall.
- name: Windows — the beacon's Scheduled Task is registered (#514)
if: runner.os == 'Windows'
run: MSYS_NO_PATHCONV=1 schtasks.exe /Query /TN '\DIG\dig-updater'
- name: Linux — the beacon's systemd timer is enabled (#514)
if: runner.os == 'Linux'
run: systemctl is-enabled dig-updater.timer
- name: macOS — the beacon's LaunchDaemon is loaded (#514)
if: runner.os == 'macOS'
run: sudo launchctl print system/net.dignetwork.dig-updater
- name: Windows — verify the SCM display names via `sc qc` (#494)
if: runner.os == 'Windows'
run: |
# `grep` over a HERE-STRING rather than a pipe: a matching `grep -q` exits early and SIGPIPEs its
# producer, which under `pipefail` fails the step after the assertion has already passed.
NODE_QC="$(sc.exe qc net.dignetwork.dig-node)"
echo "$NODE_QC"
grep -q "DIG NETWORK: NODE" <<< "$NODE_QC"
DNS_QC="$(sc.exe qc net.dignetwork.dig-dns)"
echo "$DNS_QC"
grep -q "DIG NETWORK: DNS" <<< "$DNS_QC"
- name: Windows — both services RUNNING per `sc query` (belt-and-braces)
if: runner.os == 'Windows'
run: |
# Captured first, then matched: see the `sc qc` step above for why a pipe into `grep -q` is unsafe.
grep -q "RUNNING" <<< "$(sc.exe query net.dignetwork.dig-node)"
grep -q "RUNNING" <<< "$(sc.exe query net.dignetwork.dig-dns)"
- name: Linux — both units active per `systemctl` (belt-and-braces)
if: runner.os == 'Linux'
run: |
# Real Linux unit names, NOT the canonical dotted id (svc::linux_unit_name /
# dns::plan::service_script_name, #524): the "net" qualifier is dropped and
# {organization}-{application} hyphen-joined for BOTH services (the
# service-manager crate's ServiceLabel::to_script_name(), which every
# dig-node/dig-dns Linux registration in this workspace goes through) --
# dig-node -> dignetwork-dig-node, dig-dns -> dignetwork-dig-dns. dig-node is
# USER-level under root's own session (see the "bring up a systemd/D-Bus
# user session for root" step above); dig-dns is machine-wide (#494). `-E`
# carries this job's XDG_RUNTIME_DIR override to root's shell.
sudo -E systemctl --user is-active dignetwork-dig-node
systemctl is-active dignetwork-dig-dns
- name: macOS — both jobs running per `launchctl` (belt-and-braces)
if: runner.os == 'macOS'
run: |
# `launchctl print` emits multi-KB output and the match is near the top, so a piped `grep -q`
# SIGPIPEs it almost every time — the most likely of these to fail after passing.
grep -q "state = running" <<< "$(sudo launchctl print system/net.dignetwork.dig-node)"
grep -q "state = running" <<< "$(sudo launchctl print system/net.dignetwork.dig-dns)"
- name: Uninstall dig-node via the installer flag + assert deregistered
run: |
$SUDO "$INSTALLER" --uninstall-dig-node --bin-dir "$BIN_DIR" --json | tee uninstall-node.json
jq -e '.result.uninstalled == true' uninstall-node.json
jq -e '.result.dig_local_removed == true' uninstall-node.json
- name: Uninstall dig-dns via the installer flag + assert deregistered
run: |
$SUDO "$INSTALLER" --uninstall-dig-dns --json | tee uninstall-dns.json
jq -e '.result.uninstalled == true' uninstall-dns.json
- name: Uninstall the auto-update beacon's scheduler via the installer flag + assert removed (#514)
run: |
$SUDO "$INSTALLER" --uninstall-dig-updater --bin-dir "$BIN_DIR" --json | tee uninstall-updater.json
jq -e '.result.applied == true' uninstall-updater.json
- name: Windows — the beacon's Scheduled Task is gone
if: runner.os == 'Windows'
run: |
! MSYS_NO_PATHCONV=1 schtasks.exe /Query /TN '\DIG\dig-updater'
- name: Linux — the beacon's systemd timer is gone
if: runner.os == 'Linux'
run: |
! systemctl cat dig-updater.timer
- name: macOS — the beacon's LaunchDaemon is gone
if: runner.os == 'macOS'
run: |
! sudo launchctl print system/net.dignetwork.dig-updater
- name: Windows — both services gone per `sc query`
if: runner.os == 'Windows'
run: |
! sc.exe query net.dignetwork.dig-node
! sc.exe query net.dignetwork.dig-dns
- name: Linux — both units gone per `systemctl`
if: runner.os == 'Linux'
run: |
! sudo -E systemctl --user cat dignetwork-dig-node.service
! systemctl cat dignetwork-dig-dns.service
- name: macOS — both jobs gone per `launchctl`
if: runner.os == 'macOS'
run: |
! sudo launchctl print system/net.dignetwork.dig-node
! sudo launchctl print system/net.dignetwork.dig-dns
- name: Assert the dig.local hosts entry is gone
run: |
if [ "${{ runner.os }}" = "Windows" ]; then
HOSTS_FILE="/c/Windows/System32/drivers/etc/hosts"
else
HOSTS_FILE="/etc/hosts"
fi
! grep -q "dig.local" "$HOSTS_FILE"
- name: Upload diagnostics
if: always()
uses: actions/upload-artifact@v4
with:
name: installer-e2e-${{ matrix.os }}
path: |
install.json
uninstall-node.json
uninstall-dns.json
uninstall-updater.json
if-no-files-found: ignore
# dig_ecosystem#1774: a genuinely GTK-less host must install a dig-app build it
# can actually LOAD.
#
# The `e2e` job runs on stock `ubuntu-latest`, which ships libgtk-3 — so it
# cannot prove the fall-through, only that SOME loadable build was chosen. This
# job runs the whole install inside a `debian:bookworm-slim` container with NO
# GTK3 installed, which is exactly the #1753 headless-server shape: the GTK
# `tray` build's `DT_NEEDED` (libgtk-3.so.0, …) resolve to nothing, so the
# loadability-driven selector MUST fall through to the `-headless` build and
# report it loadable — never place the GTK build that dies before `main`.
#
# The install runs INSIDE the container (not a host-built binary shipped in) so
# the installer's own glibc matches the box it inspects. Only dig-app is
# installed (per-user, no services) to keep the leg fast + focused.
headless-linux-e2e:
name: "#1774 a GTK-less host installs the headless dig-app build (debian:bookworm-slim)"
runs-on: ubuntu-latest
container:
image: debian:bookworm-slim
timeout-minutes: 20
defaults:
run:
shell: bash
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# rustup/cargo live under a writable HOME inside the container.
CARGO_HOME: /root/.cargo
RUSTUP_HOME: /root/.rustup
steps:
# curl + ca-certificates: fetch rustup and the GitHub release assets.
# gcc + build-essential: native deps (ring) compile. git: the Cargo.toml
# `dig-cert` git dependency. jq: the JSON assertions. Deliberately NO
# libgtk-3 — a GTK-less box is the whole point of this leg.
- name: Install build prerequisites (NO GTK)
run: |
apt-get update
apt-get install -y --no-install-recommends \
curl ca-certificates gcc build-essential git jq pkg-config
# Prove the box really lacks GTK3 — if a base-image change ever adds it,
# this leg would silently stop testing the fall-through.
if ldconfig -p | grep -qi 'libgtk-3'; then
echo "FAIL: this container unexpectedly ships libgtk-3 — the fall-through is not exercised"
exit 1
fi
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install Rust (stable)
run: |
curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable
echo "$CARGO_HOME/bin" >> "$GITHUB_PATH"
- name: Build dig-installer (release)
run: cargo build --release --locked --bin dig-installer
- name: A stable, root-owned bin dir under a world-traversable /opt
run: |
# /opt in a fresh debian image is root-owned 0755, satisfying the #1748
# install-root chain verify. --bin-dir places dig-app here.
chmod 755 /opt
mkdir -p /opt/dig-bin && chmod 755 /opt/dig-bin
echo "BIN_DIR=/opt/dig-bin" >> "$GITHUB_ENV"
# dig-app only: per-user, no OS-service registration, so no elevation dance
# and no systemd needed (the container has no init). The run is root (uid 0),
# which satisfies the elevation gate for the nothing-privileged plan.
- name: Install dig-app on the GTK-less host
run: |
./target/release/dig-installer \
--no-digstore --no-dig-node --no-dig-dns --no-auto-update \
--bin-dir "$BIN_DIR" \
--json | tee install.json
- name: Assert dig-app fell through to the -headless build and it is loadable (#1774)
run: |
DIGAPP='.result.components[] | select(.component=="dig-app")'
# Exactly one dig-app component, and it was NOT refused (a refusal would
# mean NO build loaded — the headless build must have).
jq -e "[$DIGAPP] | length == 1" install.json
jq -e "$DIGAPP | has(\"refused\") | not" install.json
# THE property: a GTK-less host selects the headless build, proven loadable.
jq -e "$DIGAPP | .selected_variant == \"headless\"" install.json
jq -e "$DIGAPP | .loadable == true" install.json
# And the chosen asset name really is the -headless artifact.
jq -e "$DIGAPP | .asset | test(\"-headless\")" install.json
- name: Prove the placed headless build's DT_NEEDED resolve on this GTK-less box
run: |
DIGAPP='.result.components[] | select(.component=="dig-app")'
dest=$(jq -r "$DIGAPP | .dest" install.json)
echo "placed dig-app at: $dest"
ldd "$dest" || true
if ldd "$dest" 2>&1 | grep -i "not found"; then
echo "FAIL: the placed headless dig-app build has unresolved shared libraries"
exit 1
fi
- name: Upload diagnostics
if: always()
uses: actions/upload-artifact@v4
with:
name: installer-headless-linux-e2e
path: install.json
if-no-files-found: ignore
# #565 H3 — the seeded-legacy MIGRATION + DEFAULT-protected-root proof.
#
# The `e2e` job above installs with a custom `--bin-dir`, so it verifies the
# audit/ACL on a CUSTOM dir and starts from a clean runner (nothing to migrate).
# Two things that leaves uncovered — and both are the heart of #565: (1) the
# DEFAULT protected root's own ACL (`%ProgramFiles%\DIG\bin`), and (2) the
# MIGRATION of a pre-#565 install whose privileged registration still points at
# a user-writable legacy root. This job covers both on Windows — the canonical
# #565 threat surface the issue names (the SYSTEM `\DIG\dig-updater` beacon task
# executing a binary a non-admin can replace).
#
# It SEEDS that exact pre-#565 shape (a SYSTEM beacon task whose binPath is under
# `%LOCALAPPDATA%\Programs\DIG\bin`), installs the beacon into the DEFAULT
# protected root with NO `--bin-dir`, and asserts the installer DEREGISTERED the
# legacy-bound task, verified the default root admin-only, and re-registered the
# beacon under the protected root. Beacon-only keeps it light + deterministic
# (no long-running network services). This is what makes the migration + audit
# assertions NON-VACUOUS — there is a real legacy registration to vacate.
migrate-e2e:
name: seeded-legacy migration into the default protected root (Windows)
runs-on: windows-latest
timeout-minutes: 15
defaults:
run:
shell: bash
env:
CARGO_TERM_COLOR: always
DIG_UPDATER_VERSION: "0.6.0"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install Rust (stable)
run: |
rustup toolchain install stable
rustup default stable
rustc --version
- uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
target
key: Windows-migrate-e2e-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
Windows-migrate-e2e-
Windows-installer-e2e-
- name: Build dig-installer (release)
run: cargo build --release --locked --bin dig-installer
- name: Resolve the built binary
run: echo "INSTALLER=$(pwd)/target/release/dig-installer.exe" >> "$GITHUB_ENV"
# Seed a PRE-#565 install shape: a SYSTEM beacon scheduled task whose binary
# lives in the user-writable legacy root (%LOCALAPPDATA%\Programs\DIG\bin) —
# exactly the escalation #565 fixes (a non-admin could replace that
# dig-updater.exe and the SYSTEM task would run it). The task id + legacy root
# are the canonical ones the installer's migration reads back and vacates
# (`regaudit::BEACON_WINDOWS_TASK` / `paths::legacy_privileged_roots`).
# `MSYS_NO_PATHCONV=1` stops git-bash mangling schtasks's `/`-flags (the same
# pitfall the beacon steps in the `e2e` job document).
- name: Seed a legacy-bound SYSTEM beacon task (pre-#565 shape)
run: |
LEGACY_DIR="$LOCALAPPDATA/Programs/DIG/bin"
mkdir -p "$LEGACY_DIR"
# A dummy stand-in binary at the legacy path — never executed by the
# installer (#565); only its PRESENCE + the task's binPath matter.
printf '@echo off\r\n' > "$LEGACY_DIR/dig-updater.exe"
LEGACY_WIN="$(cygpath -w "$LEGACY_DIR")\\dig-updater.exe"
echo "LEGACY_DIR=$LEGACY_DIR" >> "$GITHUB_ENV"
MSYS_NO_PATHCONV=1 schtasks.exe /Create /TN '\DIG\dig-updater' \
/TR "\"$LEGACY_WIN\" run" /SC DAILY /ST 03:00 /RU SYSTEM /RL HIGHEST /F
echo "Seeded task now resolves to:"
MSYS_NO_PATHCONV=1 schtasks.exe /Query /TN '\DIG\dig-updater' /XML \
> seeded-task.xml
# NOT `| grep -q`: under `pipefail` a matching `grep -q` exits immediately, SIGPIPEs
# schtasks, and the step fails with 141 having actually PASSED. Read the file instead.
grep -i 'Programs.DIG.bin' seeded-task.xml > /dev/null
# Install the beacon into the DEFAULT protected root (NO --bin-dir). The
# hosted Windows runner is already Administrator (see the `e2e` job's note),
# so no elevation dance is needed. The migration runs FIRST (it now gates on
# `installs_a_privileged_binary`, which is true here), deregisters the seeded
# legacy task, then the install re-registers the beacon from Program Files.
- name: Install the auto-update beacon into the default protected root (no --bin-dir)
run: |
"$INSTALLER" \
--no-digstore --no-dig-node --no-dig-dns \
--dig-updater-version "$DIG_UPDATER_VERSION" \
--json | tee install.json
- name: Assert ready + the DEFAULT protected root is ACL-verified admin-only (#565)
run: |
jq -e '.ok == true' install.json
jq -e '.result.ready == true' install.json
jq -e '.result.failures == []' install.json
jq -e '.result.install_root_security.checked == true' install.json
jq -e '.result.install_root_security.secure == true' install.json
# The verified root is the built-in Program Files root, not a custom dir.
jq -e '.result.install_root_security.root | ascii_downcase | contains("program files")' install.json
- name: Assert the migration DEREGISTERED the legacy-bound beacon task (#565 H1)
run: |
jq -e '.result.migration.migrated == true' install.json
jq -e '[.result.migration.deregistered[] | select(ascii_downcase | contains("beacon"))] | length > 0' install.json
jq -e '.result.migration | has("deregister_failures") and (.deregister_failures | length == 0)' install.json
- name: Assert the audit ran + no privileged registration resolves under a legacy root (#565 H2b)
run: |
jq -e '(.result.registration_audit | length) > 0' install.json
jq -e '[.result.registration_audit[] | select(.under_legacy_root)] | length == 0' install.json
jq -e '.result.beacon.applied == true' install.json
# The re-registered SYSTEM task now points UNDER Program Files, and the
# migration removed the legacy dummy binary — the escalation is closed.
- name: Assert the re-registered task points at the protected root + the legacy binary is gone
run: |
MSYS_NO_PATHCONV=1 schtasks.exe /Query /TN '\DIG\dig-updater' /XML \
> migrated-task.xml
# NOT `| grep -q`: under `pipefail` a matching `grep -q` exits immediately, SIGPIPEs
# schtasks, and the step fails with 141 having actually PASSED. Read the file instead.
grep -i 'Program Files.DIG.bin' migrated-task.xml > /dev/null
test ! -f "$LEGACY_DIR/dig-updater.exe"
- name: Cleanup — uninstall the beacon
if: always()
run: |
"$INSTALLER" --uninstall-dig-updater --json | tee uninstall-updater.json || true
MSYS_NO_PATHCONV=1 schtasks.exe /Delete /TN '\DIG\dig-updater' /F || true
- name: Upload diagnostics
if: always()
uses: actions/upload-artifact@v4
with:
name: installer-migrate-e2e-windows
path: |
install.json
uninstall-updater.json
seeded-task.xml
migrated-task.xml
if-no-files-found: ignore
# ---------------------------------------------------------------------------
# dig_ecosystem#1854: what happens to a host's auto-updates when the #565
# migration vacates a legacy-root beacon schedule on a run that DECLINED the
# beacon?
#
# The Windows `migrate-e2e` job above installs WITH the beacon, so its run
# re-registers the schedule at step 5 and `rearm_beacon_after_migration` returns
# `None` on every CI run — no e2e reached the opt-out path at all, which is how
# the whole branch shipped unexercised end to end.
#
# This leg drives the opt-out shape: a SYSTEM beacon timer whose ExecStart
# resolves under the user-writable legacy root (`~/.dig/bin`), then an install
# that says `--no-auto-update`. The migration MUST still vacate that schedule
# (it is itself the #565 vulnerability), the run MUST stay READY (the beacon was
# not part of what was asked for), and the resulting auto-updates-OFF state MUST
# be REPORTED in `beacon_rearm` rather than swallowed.
#
# Linux only for now — the Windows + macOS parity legs are a sibling ticket.
migrate-optout-e2e:
name: "#1854 a declining run reports the auto-updates it lost (Linux)"
runs-on: ubuntu-latest
timeout-minutes: 20
defaults:
run:
shell: bash
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install Rust (stable)
run: rustup toolchain install stable && rustup default stable
- uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
target
key: ubuntu-1854-optout-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
ubuntu-1854-optout-
ubuntu-1748-reach-
- name: Build the installer
run: cargo build --release --locked --bin dig-installer
# The runner image ships `/opt` world-writable, and the #1748 install-root
# verify walks every ancestor — so an unrepaired image makes the install
# report NOT ready for a reason unrelated to this leg. Repaired + asserted
# exactly as the `#1748` job documents.
- name: Normalise the runner image's world-writable /opt chain
run: |
sudo chmod 755 /opt /usr /usr/local /usr/local/bin
sudo chown 0:0 /usr /usr/local /usr/local/bin
for level in /opt /usr /usr/local /usr/local/bin; do
test -z "$(find "$level" -maxdepth 0 \( -perm -0002 -o -perm -0020 \))" \
|| { echo "FAIL: $level is still group- or world-writable"; exit 1; }
done
# Seed the PRE-#565 shape on Linux: a machine-wide (root-run) dig-updater
# timer whose service ExecStart lives in the INVOKING user's `~/.dig/bin` —
# a directory that user can write, so the root timer would execute a binary
# they can replace. `regaudit::beacon_bin_path` reads that ExecStart back via
# `systemctl show`, which is what makes the migration act on it.
# dig-node's own `install` prefers a USER-level systemd unit regardless of
# privilege, and an elevated run executes it as uid 0, which has no user
# session by default ("Failed to connect to bus: Operation not permitted",
# dig_ecosystem#524). Same recipe the `e2e` job documents: give root its own
# lingering session so the dig-node service registration succeeds and the run
# can reach `ready` — otherwise this leg would red on #524, not on #1854.
- name: Bring up a systemd/D-Bus user session for root
run: |
sudo loginctl enable-linger root
sudo systemctl start user@0.service
for _ in $(seq 1 10); do
[ -S "/run/user/0/bus" ] && break
sleep 1
done
echo "XDG_RUNTIME_DIR=/run/user/0" >> "$GITHUB_ENV"
- name: Seed a legacy-root SYSTEM beacon timer (pre-#565 shape)
run: |
LEGACY_DIR="$HOME/.dig/bin"
mkdir -p "$LEGACY_DIR"
# A stand-in binary at the legacy path — never executed by the installer
# (#565); only its PRESENCE and the unit's ExecStart matter.
printf '#!/bin/sh\nexit 0\n' > "$LEGACY_DIR/dig-updater"
chmod +x "$LEGACY_DIR/dig-updater"
echo "LEGACY_DIR=$LEGACY_DIR" >> "$GITHUB_ENV"
sudo tee /etc/systemd/system/dig-updater.service >/dev/null <<UNIT
[Unit]
Description=DIG auto-update beacon (seeded pre-#565 legacy-root shape)
[Service]
Type=oneshot
ExecStart=$LEGACY_DIR/dig-updater run
UNIT
sudo tee /etc/systemd/system/dig-updater.timer >/dev/null <<'UNIT'
[Unit]
Description=DIG auto-update beacon daily check
[Timer]
OnCalendar=daily
Unit=dig-updater.service
[Install]
WantedBy=timers.target
UNIT
sudo systemctl daemon-reload
sudo systemctl enable --now dig-updater.timer
echo "seeded ExecStart:"
systemctl show -p ExecStart dig-updater.service | tee seeded-execstart.txt
grep -F "$LEGACY_DIR/dig-updater" seeded-execstart.txt > /dev/null
# DECLINE the beacon (`--no-auto-update`) while still installing a privileged
# binary (dig-node, default-on) so the #565 migration gate fires. No
# `--bin-dir`: the privileged root is the protected root, which is the only
# root the re-arm is permitted to consider at all.
- name: Install with the beacon DECLINED into the default protected root
run: |
sudo -E ./target/release/dig-installer \
--no-digstore --no-dig-dns --no-dig-app --no-auto-update \
--json | tee install.json
- name: Assert the migration vacated the legacy-root beacon schedule (#565)
run: |
jq -e '.ok == true' install.json
jq -e '.result.migration.migrated == true' install.json
jq -e '[.result.migration.deregistered[] | select(ascii_downcase | contains("beacon"))] | length > 0' install.json
jq -e '.result.migration | has("deregister_failures") and (.deregister_failures | length == 0)' install.json
# THE #1854 acceptance assertions. `beacon_rearm` non-null is what proves the
# opt-out path RAN and REPORTED — `null` is exactly the silent state the issue
# was filed about, and is what this leg sees if the re-arm block is removed.
- name: Assert the lost auto-updates are REPORTED, and the run is still ready
run: |
jq -e '.result.beacon_rearm != null' install.json \
|| { echo "FAIL: the declining run said nothing about the auto-updates it lost"; exit 1; }
jq -e '.result.beacon_rearm | has("applied") and has("note")' install.json
# Declining the beacon is NOT a readiness failure — the beacon was never
# part of what this run was asked to install.
jq -e '.result.ready == true' install.json
jq -e '.result.failures == []' install.json
# The escalation must be CLOSED, and asserted on the host rather than on the
# report: this run declined the beacon and installs no other machine-wide
# privileged registration, so `registration_audit` is legitimately EMPTY once
# the schedule is vacated — a `select(.under_legacy_root) | length == 0`
# filter would pass vacuously on an empty array and prove nothing. The
# systemd + filesystem checks below cannot pass vacuously.
- name: Assert the legacy-root escalation is closed on the host (#565)
run: |
jq -e '[.result.registration_audit[] | select(.under_legacy_root)] | length == 0' install.json
# The run declined the beacon, so nothing may have re-installed it.
jq -e '.result.beacon == null' install.json
# The seeded root-run timer must no longer resolve at all. `systemctl disable`
# alone leaves the unit file loadable, which is the defect this leg found.
systemctl show -p LoadState dig-updater.timer | tee post-loadstate.txt
grep -qx 'LoadState=loaded' post-loadstate.txt \
&& { echo "FAIL: the beacon timer is still loaded — the schedule was not vacated"; exit 1; }
test ! -f /etc/systemd/system/dig-updater.timer \