@sebastienrousseau/dotfiles 0.2.518 → 0.2.520
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +191 -0
- package/README.md +1164 -164
- package/docs/ARCHITECTURE.md +117 -0
- package/docs/COPYRIGHT +1 -1
- package/docs/ECOSYSTEM.md +220 -0
- package/docs/GOLD-STANDARD-AUDIT.md +352 -0
- package/docs/GOVERNANCE.md +1 -1
- package/docs/MAINTAINERS.md +1 -1
- package/docs/MINIMUM-TOOLCHAIN.md +100 -0
- package/docs/README.md +1 -1
- package/docs/STRUCTURE.md +1 -1
- package/docs/architecture/ARCHITECTURE.md +11 -108
- package/docs/architecture/REPO_LAYOUT.md +4 -4
- package/docs/guides/MACOS_ICLOUD_SYMLINKS.md +121 -0
- package/docs/index.md +3 -3
- package/docs/manual/00-introduction.md +1 -1
- package/docs/manual/03-reference/01-dot-cli.md +28 -3
- package/docs/manual/command-index.md +24 -4
- package/docs/manual/concept-index.md +2 -0
- package/docs/migration/README.md +81 -0
- package/docs/migration/from-bare-git-repo.md +156 -0
- package/docs/migration/from-gnu-stow.md +165 -0
- package/docs/migration/from-plain-chezmoi.md +148 -0
- package/docs/migration/from-yadm.md +187 -0
- package/docs/operations/PERFORMANCE_BUDGETS.md +196 -0
- package/docs/operations/REGISTRY.md +1 -1
- package/docs/operations/RELEASE_PIPELINE.md +4 -4
- package/docs/operations/TESTING.md +3 -3
- package/docs/operations/TRACEABILITY.md +1 -0
- package/docs/packaging.md +222 -0
- package/docs/reference/FEATURE-MATRIX.md +646 -0
- package/docs/reference/TOOLS.md +1 -1
- package/docs/reference/UTILS.md +1 -0
- package/docs/security/COMPLIANCE.md +1 -1
- package/docs/security/DISCLOSURE.md +4 -4
- package/docs/security/FUZZING.md +113 -18
- package/docs/security/KEY_ROTATION.md +1 -1
- package/docs/security/SCORECARD.md +3 -3
- package/docs/security/VERIFY_RELEASE.md +2 -2
- package/install.sh +6 -6
- package/package.json +2 -2
- package/scripts/ci/check-copyright-headers.sh +1 -1
- package/scripts/ci/check-shell-preamble.sh +1 -1
- package/scripts/ci/guard-gitleaks-checkout.sh +1 -1
- package/scripts/demo/record.sh +1 -1
- package/scripts/diagnostics/a2a-conformance.sh +1 -1
- package/scripts/diagnostics/alias-governance.sh +30 -3
- package/scripts/diagnostics/aliases-cheatsheet.sh +1 -1
- package/scripts/diagnostics/aliases-manifest.sh +1 -1
- package/scripts/diagnostics/attest-verify.sh +147 -0
- package/scripts/diagnostics/benchmark.sh +1 -1
- package/scripts/diagnostics/conflicts.sh +1 -1
- package/scripts/diagnostics/doctor-unified.sh +6 -2
- package/scripts/diagnostics/doctor.sh +56 -10
- package/scripts/diagnostics/drift-dashboard.sh +3 -2
- package/scripts/diagnostics/health.sh +43 -10
- package/scripts/diagnostics/history-analysis.sh +1 -1
- package/scripts/diagnostics/mcp-doctor.sh +2 -2
- package/scripts/diagnostics/perf.sh +1 -1
- package/scripts/diagnostics/scorecard.sh +3 -2
- package/scripts/diagnostics/secret-governance.sh +1 -1
- package/scripts/diagnostics/security-score.sh +1 -1
- package/scripts/diagnostics/smoke-test.sh +1 -1
- package/scripts/diagnostics/snapshot.sh +1 -1
- package/scripts/diagnostics/verify.sh +1 -1
- package/scripts/diagnostics/verify_state.sh +1 -1
- package/scripts/diagnostics/version-locks.sh +1 -1
- package/scripts/diagnostics/workstation-attestation.sh +26 -1
- package/scripts/dot/commands/agent.sh +64 -14
- package/scripts/dot/commands/agents.sh +24 -8
- package/scripts/dot/commands/ai.sh +20 -7
- package/scripts/dot/commands/aliases.sh +1 -1
- package/scripts/dot/commands/appearance.sh +1 -1
- package/scripts/dot/commands/completion.sh +42 -5
- package/scripts/dot/commands/core.sh +1 -1
- package/scripts/dot/commands/diagnostics.sh +1 -1
- package/scripts/dot/commands/env-emit.sh +1 -1
- package/scripts/dot/commands/fleet.sh +41 -18
- package/scripts/dot/commands/init.sh +1 -1
- package/scripts/dot/commands/lint.sh +1 -1
- package/scripts/dot/commands/manual.sh +1 -1
- package/scripts/dot/commands/meta.sh +115 -10
- package/scripts/dot/commands/patterns.sh +1 -1
- package/scripts/dot/commands/registry.sh +43 -7
- package/scripts/dot/commands/restore.sh +1 -1
- package/scripts/dot/commands/secrets.sh +1 -1
- package/scripts/dot/commands/security.sh +1 -1
- package/scripts/dot/commands/tools.sh +21 -7
- package/scripts/fonts/install-nerd-fonts.sh +1 -1
- package/scripts/fonts/patch-fonts.sh +1 -1
- package/scripts/git-hooks/install.sh +1 -1
- package/scripts/git-hooks/pre-commit-audit.sh +2 -2
- package/scripts/lib/secrets_provider.sh +19 -4
- package/scripts/nvim/headless-upgrade.lua +81 -0
- package/scripts/ops/ai-setup.sh +1 -1
- package/scripts/ops/bundle.sh +1 -1
- package/scripts/ops/chaos.sh +1 -1
- package/scripts/ops/chezmoi-apply.sh +1 -1
- package/scripts/ops/chezmoi-diff.sh +1 -1
- package/scripts/ops/chezmoi-remove.sh +3 -3
- package/scripts/ops/chezmoi-update.sh +6 -2
- package/scripts/ops/heal-chezmoi.sh +1 -1
- package/scripts/ops/heal-system.sh +1 -1
- package/scripts/ops/heal-tools.sh +1 -1
- package/scripts/ops/heal.sh +1 -1
- package/scripts/ops/post-apply-repair.sh +1 -1
- package/scripts/ops/prewarm.sh +1 -1
- package/scripts/ops/release.sh +1 -1
- package/scripts/ops/rollback.sh +11 -2
- package/scripts/ops/setup.sh +1 -1
- package/scripts/ops/teleport.sh +1 -1
- package/scripts/qa/check-feature-matrix.sh +296 -0
- package/scripts/qa/check-version-consistency.sh +7 -119
- package/scripts/qa/coverage-baseline.sh +1 -1
- package/scripts/qa/docs-coverage.sh +9 -3
- package/scripts/qa/examples-coverage.sh +1 -1
- package/scripts/qa/reliability-audit.sh +1 -1
- package/scripts/qa/scorecard-snapshot.sh +1 -1
- package/scripts/qa/traceability-coverage.sh +9 -2
- package/scripts/qa/validate-examples.sh +66 -3
- package/scripts/qa/wsl-contract.sh +1 -1
- package/scripts/secrets/age-init.sh +1 -1
- package/scripts/secrets/create-secrets-file.sh +1 -1
- package/scripts/secrets/encrypt-ssh-key.sh +1 -1
- package/scripts/security/backup.sh +1 -1
- package/scripts/security/check-disclosure-key-expiry.sh +1 -1
- package/scripts/security/dns-doh.sh +1 -1
- package/scripts/security/encryption-check.sh +1 -1
- package/scripts/security/enforce-policies.sh +315 -98
- package/scripts/security/firewall.sh +1 -1
- package/scripts/security/lock-configs.sh +1 -1
- package/scripts/security/lock-screen.sh +1 -1
- package/scripts/security/manage-secrets.sh +1 -1
- package/scripts/security/ssh-cert.sh +1 -1
- package/scripts/security/telemetry-kill.sh +1 -1
- package/scripts/security/usb-safety.sh +1 -1
- package/scripts/theme/apply-gnome-theme.sh +1 -1
- package/scripts/theme/extract-heic-frames.sh +1 -1
- package/scripts/theme/extract-theme.py +284 -6
- package/scripts/theme/install-boot-logo.sh +1 -1
- package/scripts/theme/install-catppuccin-themes.sh +1 -1
- package/scripts/theme/install-cursors.sh +1 -1
- package/scripts/theme/install-file-icons.sh +1 -1
- package/scripts/theme/install-grub-theme.sh +1 -1
- package/scripts/theme/install-lock-icon.sh +1 -1
- package/scripts/theme/merge-wallpaper.sh +1 -1
- package/scripts/theme/rebuild-themes.sh +64 -5
- package/scripts/theme/switch.sh +46 -19
- package/scripts/theme/wallpaper-rotate.sh +1 -1
- package/scripts/theme/wallpaper-sync.sh +1 -1
- package/scripts/tools/cmatrix.sh +1 -1
- package/scripts/tools/detect-collisions.py +1 -1
- package/scripts/tools/emoji-picker.sh +1 -1
- package/scripts/tools/figlet-banner.sh +1 -1
- package/scripts/tools/log-rotate.sh +1 -1
- package/scripts/tools/lolcat-wrap.sh +1 -1
- package/scripts/tools/pipes.sh +1 -1
- package/scripts/tuning/linux.sh +1 -1
- package/scripts/tuning/macos.sh +1 -1
- package/scripts/uninstall.sh +1 -1
- package/scripts/verify-release-versions +156 -0
- package/scripts/version-sync.sh +63 -3
- package/LICENSE +0 -21
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
3
3
|
# Copyright (c) 2015-2026 Sebastien Rousseau
|
|
4
4
|
# Dotfiles Pre-Commit Sentinel (Audit Hook)
|
|
5
5
|
# Prevents secrets, hardcoded paths, and shell hygiene violations.
|
|
@@ -141,6 +141,6 @@ if [[ $FAILED -eq 1 ]]; then
|
|
|
141
141
|
printf '%b\\n' " (Use --no-verify to bypass if absolutely necessary)"
|
|
142
142
|
exit 1
|
|
143
143
|
else
|
|
144
|
-
printf '%b\n' "${GREEN}${BOLD}✅ Audit passed.${NC} v0.2.
|
|
144
|
+
printf '%b\n' "${GREEN}${BOLD}✅ Audit passed.${NC} v0.2.520 standards maintained."
|
|
145
145
|
exit 0
|
|
146
146
|
fi
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
3
3
|
# Copyright (c) 2015-2026 Sebastien Rousseau
|
|
4
4
|
# Provider-agnostic secrets bridge for dotfiles.
|
|
5
5
|
|
|
@@ -96,9 +96,24 @@ dot_secrets_store_plain_enc() {
|
|
|
96
96
|
dot_secrets_ensure_layout
|
|
97
97
|
tmp_rec="$(umask 077 && mktemp)"
|
|
98
98
|
file="$DOT_SECRETS_STORE_DIR/${key}.age"
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
|
|
100
|
+
# No RETURN trap here. A RETURN trap set inside a function also fires when
|
|
101
|
+
# its CALLER returns, and by then `tmp_rec` — local to this function — is
|
|
102
|
+
# out of scope: under `set -u` the trap aborted dot_secrets_set with
|
|
103
|
+
# "tmp_rec: unbound variable" AFTER the encrypted file had been written, so
|
|
104
|
+
# `dot secrets set` reported failure on a write that had succeeded. Clean up
|
|
105
|
+
# explicitly on both paths instead.
|
|
106
|
+
if ! age-keygen -y "$DOT_SECRETS_AGE_KEY" >"$tmp_rec"; then
|
|
107
|
+
rm -f "$tmp_rec"
|
|
108
|
+
echo "failed to derive the age recipient from $DOT_SECRETS_AGE_KEY" >&2
|
|
109
|
+
return 1
|
|
110
|
+
fi
|
|
111
|
+
if ! printf "%s" "$value" | age -R "$tmp_rec" -o "$file"; then
|
|
112
|
+
rm -f "$tmp_rec"
|
|
113
|
+
echo "failed to encrypt secret: $key" >&2
|
|
114
|
+
return 1
|
|
115
|
+
fi
|
|
116
|
+
rm -f "$tmp_rec"
|
|
102
117
|
chmod 600 "$file" 2>/dev/null || true
|
|
103
118
|
}
|
|
104
119
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
-- Copyright (c) 2015-2026 Dotfiles. All rights reserved.
|
|
2
|
+
-- headless-upgrade.lua — plugin/tool refresh for `dot upgrade`.
|
|
3
|
+
--
|
|
4
|
+
-- Runs Lazy sync and then waits for Mason's async install queue to drain
|
|
5
|
+
-- before exiting. The naive invocation
|
|
6
|
+
-- nvim --headless "+Lazy! sync" +qa
|
|
7
|
+
-- races on two fronts:
|
|
8
|
+
-- 1. Lazy's own async ops (git fetch/checkout, `build` hooks) can outlive
|
|
9
|
+
-- the `Lazy! sync` command return.
|
|
10
|
+
-- 2. `mason-nvim-dap` and `mason-lspconfig` fire `ensure_installed` on
|
|
11
|
+
-- plugin load; those installs are enqueued to mason.nvim and run
|
|
12
|
+
-- async, so `+qa` aborts them mid-download (leaving codelldb / debugpy
|
|
13
|
+
-- / delve half-installed, per repeated user reports on `dot upgrade`).
|
|
14
|
+
--
|
|
15
|
+
-- We explicitly wait for both queues before quitting.
|
|
16
|
+
|
|
17
|
+
local LAZY_TIMEOUT_MS = 300000 -- 5 min per phase
|
|
18
|
+
local MASON_TIMEOUT_MS = 300000
|
|
19
|
+
|
|
20
|
+
local function log(msg)
|
|
21
|
+
io.stderr:write(("[headless-upgrade] %s\n"):format(msg))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
-- ---------------------------------------------------------------------------
|
|
25
|
+
-- Phase 1: Lazy sync (blocking)
|
|
26
|
+
-- ---------------------------------------------------------------------------
|
|
27
|
+
local ok_lazy, lazy = pcall(require, "lazy")
|
|
28
|
+
if not ok_lazy then
|
|
29
|
+
log("lazy.nvim not available; skipping plugin update")
|
|
30
|
+
vim.cmd("quitall!")
|
|
31
|
+
return
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
lazy.sync({ wait = true, show = false })
|
|
35
|
+
|
|
36
|
+
-- Belt-and-braces: give any residual runner tasks a chance to drain even if
|
|
37
|
+
-- `wait = true` returned early on the last task in a batch.
|
|
38
|
+
pcall(function()
|
|
39
|
+
local ok_runner, runner = pcall(require, "lazy.manage.runner")
|
|
40
|
+
if not ok_runner then return end
|
|
41
|
+
vim.wait(LAZY_TIMEOUT_MS, function()
|
|
42
|
+
return not (runner.running and runner.running())
|
|
43
|
+
end, 200)
|
|
44
|
+
end)
|
|
45
|
+
|
|
46
|
+
-- ---------------------------------------------------------------------------
|
|
47
|
+
-- Phase 2: drain Mason's async install queue
|
|
48
|
+
-- ---------------------------------------------------------------------------
|
|
49
|
+
pcall(function()
|
|
50
|
+
local ok_reg, registry = pcall(require, "mason-registry")
|
|
51
|
+
if not ok_reg then return end
|
|
52
|
+
|
|
53
|
+
-- Refresh the remote registry so ensure_installed picks up latest versions.
|
|
54
|
+
local refreshed = false
|
|
55
|
+
registry.refresh(function() refreshed = true end)
|
|
56
|
+
vim.wait(30000, function() return refreshed end, 200)
|
|
57
|
+
|
|
58
|
+
local function any_installing()
|
|
59
|
+
for _, pkg in ipairs(registry.get_all_packages() or {}) do
|
|
60
|
+
if pkg.is_installing and pkg:is_installing() then
|
|
61
|
+
return true
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
return false
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
-- Small settle time so installs triggered by ensure_installed hooks have
|
|
68
|
+
-- a chance to enter the installing state before we start polling.
|
|
69
|
+
vim.wait(2000, function() return false end, 200)
|
|
70
|
+
|
|
71
|
+
local done = vim.wait(MASON_TIMEOUT_MS, function()
|
|
72
|
+
return not any_installing()
|
|
73
|
+
end, 500)
|
|
74
|
+
|
|
75
|
+
if not done then
|
|
76
|
+
log(("Mason install queue still active after %ds; exiting anyway"):format(
|
|
77
|
+
MASON_TIMEOUT_MS / 1000))
|
|
78
|
+
end
|
|
79
|
+
end)
|
|
80
|
+
|
|
81
|
+
vim.cmd("quitall!")
|
package/scripts/ops/ai-setup.sh
CHANGED
package/scripts/ops/bundle.sh
CHANGED
package/scripts/ops/chaos.sh
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
3
3
|
# Copyright (c) 2015-2026 Sebastien Rousseau
|
|
4
4
|
# Chaos Engineering: Randomly corrupt config files to test self-healing (dot heal)
|
|
5
5
|
# USE WITH CAUTION: This intentionally breaks your environment.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
3
3
|
# Copyright (c) 2015-2026 Sebastien Rousseau
|
|
4
4
|
set -euo pipefail
|
|
5
5
|
|
|
@@ -36,11 +36,11 @@ if [[ $remove_source -eq 0 ]]; then
|
|
|
36
36
|
args+=("--keep-source")
|
|
37
37
|
fi
|
|
38
38
|
|
|
39
|
-
printf "About to run: chezmoi remove %s %s\n" "${args[*]}" "${paths[*]}"
|
|
39
|
+
printf "About to run: chezmoi remove %s %s\n" "${args[*]:-}" "${paths[*]}"
|
|
40
40
|
read -r -p "Proceed? [y/N] " confirm
|
|
41
41
|
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
|
|
42
42
|
echo "Aborted."
|
|
43
43
|
exit 1
|
|
44
44
|
fi
|
|
45
45
|
|
|
46
|
-
chezmoi remove "${args[@]}" "${paths[@]}"
|
|
46
|
+
chezmoi remove ${args[@]+"${args[@]}"} "${paths[@]}"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
3
3
|
# Copyright (c) 2015-2026 Sebastien Rousseau
|
|
4
4
|
set -euo pipefail
|
|
5
5
|
|
|
@@ -45,7 +45,11 @@ NOTICE_FILE="$STATE_DIR/notice"
|
|
|
45
45
|
|
|
46
46
|
run_update() {
|
|
47
47
|
echo "Updating dotfiles..."
|
|
48
|
-
|
|
48
|
+
# `${args[@]+...}`: with DOTFILES_INTERACTIVE_APPLY=1 no flags are added,
|
|
49
|
+
# and bash 3.2 (the system bash on macOS) treats an unguarded empty array
|
|
50
|
+
# expansion under `set -u` as an unbound variable — `dot update` then died
|
|
51
|
+
# instead of running chezmoi.
|
|
52
|
+
chezmoi update ${args[@]+"${args[@]}"}
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
if $ASYNC; then
|
package/scripts/ops/heal.sh
CHANGED
package/scripts/ops/prewarm.sh
CHANGED
package/scripts/ops/release.sh
CHANGED
package/scripts/ops/rollback.sh
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
3
3
|
# Copyright (c) 2015-2026 Sebastien Rousseau
|
|
4
4
|
# shellcheck disable=SC2034
|
|
5
5
|
# =============================================================================
|
|
@@ -35,6 +35,11 @@ BACKUP_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/dotfiles/backups"
|
|
|
35
35
|
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/dotfiles"
|
|
36
36
|
ROLLBACK_LOG="$STATE_DIR/rollback.log"
|
|
37
37
|
MAX_BACKUPS=10
|
|
38
|
+
# sysexits.h EX_TEMPFAIL: the command did nothing because another instance
|
|
39
|
+
# holds the lock. Distinct from 1 (the rollback ran and failed) and from 0
|
|
40
|
+
# (the rollback ran and succeeded) — a caller has to be able to tell those
|
|
41
|
+
# three apart.
|
|
42
|
+
EXIT_BUSY=75
|
|
38
43
|
|
|
39
44
|
# Logging — delegates to shared ui.sh primitives
|
|
40
45
|
ui_init
|
|
@@ -525,8 +530,12 @@ main() {
|
|
|
525
530
|
if command -v flock >/dev/null 2>&1; then
|
|
526
531
|
exec 9>"$LOCK_FILE"
|
|
527
532
|
if ! flock -n 9; then
|
|
533
|
+
# Losing the lock means this invocation did nothing. Exiting 0 made
|
|
534
|
+
# "did nothing" indistinguishable from "rolled back" to any caller or
|
|
535
|
+
# CI step, which is the one thing a recovery tool must never be
|
|
536
|
+
# ambiguous about. 75 is EX_TEMPFAIL: nothing is wrong, try again.
|
|
528
537
|
ui_warn "Already running" "Another rollback instance is active"
|
|
529
|
-
exit
|
|
538
|
+
exit "$EXIT_BUSY"
|
|
530
539
|
fi
|
|
531
540
|
fi
|
|
532
541
|
|
package/scripts/ops/setup.sh
CHANGED
package/scripts/ops/teleport.sh
CHANGED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
3
|
+
# Copyright (c) 2015-2026 Sebastien Rousseau
|
|
4
|
+
#
|
|
5
|
+
# check-feature-matrix.sh — drift gate for docs/reference/FEATURE-MATRIX.md.
|
|
6
|
+
#
|
|
7
|
+
# The matrix is a contract: every user-facing feature of the `dot` CLI has a
|
|
8
|
+
# row naming the regression test, benchmark, example and manual section that
|
|
9
|
+
# cover it. A contract nobody checks rots, so this script checks it, and
|
|
10
|
+
# ci.yml runs it on every push.
|
|
11
|
+
#
|
|
12
|
+
# Four checks:
|
|
13
|
+
#
|
|
14
|
+
# 1. Command coverage — every command the dispatcher can route (bin/dot's
|
|
15
|
+
# route table, which is what `scripts/dot/commands/*.sh` are reached
|
|
16
|
+
# through) and every command in the generated docs/manual/command-index.md
|
|
17
|
+
# has at least one row.
|
|
18
|
+
# 2. Test coverage — every regression test function named in a row is
|
|
19
|
+
# defined by one of tests/regression/test_feature_matrix_*.sh.
|
|
20
|
+
# 3. Benchmark coverage — every benchmark id named in a row is produced by
|
|
21
|
+
# `benches/dot_command_bench.sh --list-ids`.
|
|
22
|
+
# 4. Example coverage — every example file named in a row exists and is
|
|
23
|
+
# executable by scripts/qa/validate-examples.sh.
|
|
24
|
+
#
|
|
25
|
+
# Usage:
|
|
26
|
+
# scripts/qa/check-feature-matrix.sh [--quiet]
|
|
27
|
+
#
|
|
28
|
+
# Exit codes:
|
|
29
|
+
# 0 the matrix is in sync
|
|
30
|
+
# 1 drift detected (details on stderr)
|
|
31
|
+
# 2 bad invocation, or a file the gate depends on is missing
|
|
32
|
+
|
|
33
|
+
set -euo pipefail
|
|
34
|
+
|
|
35
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
36
|
+
REPO_ROOT="${REPO_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
37
|
+
cd "$REPO_ROOT"
|
|
38
|
+
|
|
39
|
+
MATRIX="docs/reference/FEATURE-MATRIX.md"
|
|
40
|
+
DOT_BIN="bin/dot"
|
|
41
|
+
COMMAND_INDEX="docs/manual/command-index.md"
|
|
42
|
+
BENCH="benches/dot_command_bench.sh"
|
|
43
|
+
TEST_GLOB="tests/regression/test_feature_matrix_*.sh"
|
|
44
|
+
|
|
45
|
+
QUIET=0
|
|
46
|
+
case "${1:-}" in
|
|
47
|
+
--quiet | -q) QUIET=1 ;;
|
|
48
|
+
-h | --help)
|
|
49
|
+
sed -n '5,30p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
|
50
|
+
exit 0
|
|
51
|
+
;;
|
|
52
|
+
"") ;;
|
|
53
|
+
*)
|
|
54
|
+
printf 'Unknown option: %s\n' "$1" >&2
|
|
55
|
+
exit 2
|
|
56
|
+
;;
|
|
57
|
+
esac
|
|
58
|
+
|
|
59
|
+
say() { [[ "$QUIET" -eq 1 ]] || printf '%s\n' "$*"; }
|
|
60
|
+
|
|
61
|
+
for required in "$MATRIX" "$DOT_BIN" "$COMMAND_INDEX" "$BENCH"; do
|
|
62
|
+
if [[ ! -f "$required" ]]; then
|
|
63
|
+
printf '::error::required file missing: %s\n' "$required" >&2
|
|
64
|
+
exit 2
|
|
65
|
+
fi
|
|
66
|
+
done
|
|
67
|
+
|
|
68
|
+
failures=0
|
|
69
|
+
fail() {
|
|
70
|
+
printf '::error::%s\n' "$1" >&2
|
|
71
|
+
failures=$((failures + 1))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
work="$(mktemp -d)"
|
|
75
|
+
trap 'rm -rf "$work"' EXIT
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# Parse the matrix.
|
|
79
|
+
#
|
|
80
|
+
# Rows look like:
|
|
81
|
+
# | `dot fleet apply` | --dry-run | `test_fm_x` | `run:y` | `examples/z.sh` | `docs/…` | regression |
|
|
82
|
+
#
|
|
83
|
+
# Field 1 is the command (backticked, "dot " prefix), 3 the test function,
|
|
84
|
+
# 4 the benchmark id, 5 the example path. The two explanatory tables at the
|
|
85
|
+
# top of the document have different shapes and are skipped by requiring a
|
|
86
|
+
# leading "| `dot ".
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
awk '
|
|
90
|
+
# A cell may legitimately contain an escaped pipe (a variant like
|
|
91
|
+
# "set advisory\|strict"). Neutralise those BEFORE splitting on "|", or the
|
|
92
|
+
# row shifts a column and every later check reads the wrong cell.
|
|
93
|
+
/^\| `dot / {
|
|
94
|
+
line = $0
|
|
95
|
+
gsub(/\\\|/, "\002", line)
|
|
96
|
+
n = split(line, cell, "|")
|
|
97
|
+
# 7 content columns become 9 fields: the empty strings either side of the
|
|
98
|
+
# leading and trailing pipes. The document also carries a 3-column summary
|
|
99
|
+
# table of the unmeasurable rows, which must not be parsed as feature rows.
|
|
100
|
+
if (n < 9) next
|
|
101
|
+
for (i = 1; i <= n; i++) {
|
|
102
|
+
gsub(/^[ \t]+|[ \t]+$/, "", cell[i])
|
|
103
|
+
gsub(/`/, "", cell[i])
|
|
104
|
+
gsub(/\002/, "|", cell[i])
|
|
105
|
+
}
|
|
106
|
+
# cell[2] command, cell[4] test, cell[5] bench, cell[6] example, cell[8] coverage
|
|
107
|
+
print cell[2] "\t" cell[4] "\t" cell[5] "\t" cell[6] "\t" cell[8]
|
|
108
|
+
}
|
|
109
|
+
' "$MATRIX" >"$work/rows.tsv"
|
|
110
|
+
|
|
111
|
+
row_count="$(wc -l <"$work/rows.tsv" | tr -d ' ')"
|
|
112
|
+
if [[ "$row_count" -lt 50 ]]; then
|
|
113
|
+
fail "only $row_count rows parsed from $MATRIX — has the table format changed?"
|
|
114
|
+
exit 1
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
# The command cell is "dot <command>"; a row may name a subcommand
|
|
118
|
+
# ("dot fleet apply"), so the routable command is the first word after "dot".
|
|
119
|
+
cut -f1 "$work/rows.tsv" | sed 's/^dot *//' | awk '{print $1}' |
|
|
120
|
+
sort -u >"$work/matrix-commands.txt"
|
|
121
|
+
cut -f2 "$work/rows.tsv" | sort -u | grep -v '^$' >"$work/matrix-tests.txt"
|
|
122
|
+
cut -f3 "$work/rows.tsv" | sort -u | grep -v '^$' >"$work/matrix-benches.txt"
|
|
123
|
+
cut -f4 "$work/rows.tsv" | sort -u | grep -v '^$' >"$work/matrix-examples.txt"
|
|
124
|
+
|
|
125
|
+
say "Feature matrix: $row_count rows, $(wc -l <"$work/matrix-commands.txt" | tr -d ' ') distinct commands"
|
|
126
|
+
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
# 1. Command coverage
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
# bin/dot's route table is the authority on what the dispatcher can reach.
|
|
132
|
+
# Flag aliases (--help/-h/--version/-v) are not commands and are excluded.
|
|
133
|
+
awk '/^_dot_command_routes\(\)/,/^EOF$/' "$DOT_BIN" |
|
|
134
|
+
awk -F'|' '/^[a-z][a-z0-9-]*\|[a-z]+$/ { print $1 }' |
|
|
135
|
+
sort -u >"$work/routed.txt"
|
|
136
|
+
|
|
137
|
+
# The generated command index is the documented surface.
|
|
138
|
+
grep -oE '^\| `dot [a-z][a-z0-9-]*' "$COMMAND_INDEX" |
|
|
139
|
+
sed 's/^| `dot //' | sort -u >"$work/indexed.txt"
|
|
140
|
+
|
|
141
|
+
missing_routed="$(comm -23 "$work/routed.txt" "$work/matrix-commands.txt")"
|
|
142
|
+
if [[ -n "$missing_routed" ]]; then
|
|
143
|
+
while IFS= read -r cmd; do
|
|
144
|
+
[[ -n "$cmd" ]] || continue
|
|
145
|
+
fail "routable command has no FEATURE-MATRIX row: dot $cmd"
|
|
146
|
+
done <<<"$missing_routed"
|
|
147
|
+
else
|
|
148
|
+
say " ✓ every routable command has a row ($(wc -l <"$work/routed.txt" | tr -d ' ') commands)"
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
missing_indexed="$(comm -23 "$work/indexed.txt" "$work/matrix-commands.txt")"
|
|
152
|
+
if [[ -n "$missing_indexed" ]]; then
|
|
153
|
+
while IFS= read -r cmd; do
|
|
154
|
+
[[ -n "$cmd" ]] || continue
|
|
155
|
+
fail "command documented in $COMMAND_INDEX has no FEATURE-MATRIX row: dot $cmd"
|
|
156
|
+
done <<<"$missing_indexed"
|
|
157
|
+
else
|
|
158
|
+
say " ✓ every documented command has a row ($(wc -l <"$work/indexed.txt" | tr -d ' ') commands)"
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
# A row naming a command the dispatcher cannot route is a phantom: the
|
|
162
|
+
# feature was removed but its coverage claim was not.
|
|
163
|
+
phantom="$(comm -13 <(sort -u "$work/routed.txt" "$work/indexed.txt") \
|
|
164
|
+
"$work/matrix-commands.txt")"
|
|
165
|
+
if [[ -n "$phantom" ]]; then
|
|
166
|
+
while IFS= read -r cmd; do
|
|
167
|
+
[[ -n "$cmd" ]] || continue
|
|
168
|
+
fail "FEATURE-MATRIX row names a command that is neither routable nor documented: dot $cmd"
|
|
169
|
+
done <<<"$phantom"
|
|
170
|
+
else
|
|
171
|
+
say " ✓ no phantom command rows"
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
# 2. Test coverage
|
|
176
|
+
# ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
# `|| true`: this runs at top level under `set -euo pipefail`, so without it an
|
|
179
|
+
# unmatched glob (or a grep that simply finds nothing) aborts the whole script
|
|
180
|
+
# with grep's status before the emptiness check below can run — exit 2 and not
|
|
181
|
+
# a word about why, instead of the diagnostic that check exists to print.
|
|
182
|
+
# shellcheck disable=SC2086
|
|
183
|
+
grep -ho '^test_fm_[a-z0-9_]*()' $TEST_GLOB 2>/dev/null |
|
|
184
|
+
sed 's/()//' | sort -u >"$work/defined-tests.txt" || true
|
|
185
|
+
|
|
186
|
+
if [[ ! -s "$work/defined-tests.txt" ]]; then
|
|
187
|
+
fail "no test functions found in $TEST_GLOB"
|
|
188
|
+
else
|
|
189
|
+
undefined="$(comm -23 "$work/matrix-tests.txt" "$work/defined-tests.txt")"
|
|
190
|
+
if [[ -n "$undefined" ]]; then
|
|
191
|
+
while IFS= read -r fn; do
|
|
192
|
+
[[ -n "$fn" ]] || continue
|
|
193
|
+
fail "FEATURE-MATRIX names a test function that does not exist: $fn"
|
|
194
|
+
done <<<"$undefined"
|
|
195
|
+
else
|
|
196
|
+
say " ✓ every named test function exists ($(wc -l <"$work/matrix-tests.txt" | tr -d ' ') referenced, $(wc -l <"$work/defined-tests.txt" | tr -d ' ') defined)"
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
# A defined-but-unreferenced test is not an error — it may be a helper or a
|
|
200
|
+
# cross-cutting invariant — but it is worth surfacing so coverage does not
|
|
201
|
+
# quietly drift out of the table.
|
|
202
|
+
orphan_count="$(comm -13 "$work/matrix-tests.txt" "$work/defined-tests.txt" | grep -c . || true)"
|
|
203
|
+
if [[ "$orphan_count" -gt 0 ]]; then
|
|
204
|
+
say " · $orphan_count test function(s) defined but not referenced by any row"
|
|
205
|
+
fi
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
# Every function the matrix names must also actually be CALLED by its file;
|
|
209
|
+
# a defined-but-never-invoked test passes vacuously.
|
|
210
|
+
uncalled=0
|
|
211
|
+
while IFS= read -r fn; do
|
|
212
|
+
[[ -n "$fn" ]] || continue
|
|
213
|
+
# shellcheck disable=SC2086
|
|
214
|
+
if ! grep -hqE "^[[:space:]]*${fn}([[:space:]]|$)" $TEST_GLOB 2>/dev/null; then
|
|
215
|
+
fail "test function is defined but never called: $fn"
|
|
216
|
+
uncalled=$((uncalled + 1))
|
|
217
|
+
fi
|
|
218
|
+
done <"$work/matrix-tests.txt"
|
|
219
|
+
[[ "$uncalled" -eq 0 ]] && say " ✓ every named test function is invoked by its file"
|
|
220
|
+
|
|
221
|
+
# ---------------------------------------------------------------------------
|
|
222
|
+
# 3. Benchmark coverage
|
|
223
|
+
# ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
if bash "$BENCH" --list-ids >"$work/bench-ids.txt" 2>/dev/null &&
|
|
226
|
+
[[ -s "$work/bench-ids.txt" ]]; then
|
|
227
|
+
sort -u -o "$work/bench-ids.txt" "$work/bench-ids.txt"
|
|
228
|
+
unknown_bench="$(comm -23 "$work/matrix-benches.txt" "$work/bench-ids.txt")"
|
|
229
|
+
if [[ -n "$unknown_bench" ]]; then
|
|
230
|
+
while IFS= read -r id; do
|
|
231
|
+
[[ -n "$id" ]] || continue
|
|
232
|
+
fail "FEATURE-MATRIX names a benchmark id the harness does not produce: $id"
|
|
233
|
+
done <<<"$unknown_bench"
|
|
234
|
+
else
|
|
235
|
+
say " ✓ every named benchmark id exists ($(wc -l <"$work/matrix-benches.txt" | tr -d ' ') referenced, $(wc -l <"$work/bench-ids.txt" | tr -d ' ') available)"
|
|
236
|
+
fi
|
|
237
|
+
|
|
238
|
+
# Cold-start coverage must be total: one help benchmark per routable command.
|
|
239
|
+
missing_bench=0
|
|
240
|
+
while IFS= read -r cmd; do
|
|
241
|
+
[[ -n "$cmd" ]] || continue
|
|
242
|
+
if ! grep -qx "help:$cmd" "$work/bench-ids.txt"; then
|
|
243
|
+
fail "no cold-start benchmark for routable command: dot $cmd"
|
|
244
|
+
missing_bench=$((missing_bench + 1))
|
|
245
|
+
fi
|
|
246
|
+
done <"$work/routed.txt"
|
|
247
|
+
[[ "$missing_bench" -eq 0 ]] &&
|
|
248
|
+
say " ✓ every routable command has a cold-start benchmark"
|
|
249
|
+
else
|
|
250
|
+
fail "could not list benchmark ids: $BENCH --list-ids"
|
|
251
|
+
fi
|
|
252
|
+
|
|
253
|
+
# ---------------------------------------------------------------------------
|
|
254
|
+
# 4. Example coverage
|
|
255
|
+
# ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
missing_example=0
|
|
258
|
+
while IFS= read -r path; do
|
|
259
|
+
[[ -n "$path" ]] || continue
|
|
260
|
+
if [[ ! -f "$path" ]]; then
|
|
261
|
+
fail "FEATURE-MATRIX names an example that does not exist: $path"
|
|
262
|
+
missing_example=$((missing_example + 1))
|
|
263
|
+
elif [[ "$path" != examples/*.sh ]]; then
|
|
264
|
+
fail "example is outside the directory validate-examples.sh runs: $path"
|
|
265
|
+
missing_example=$((missing_example + 1))
|
|
266
|
+
fi
|
|
267
|
+
done <"$work/matrix-examples.txt"
|
|
268
|
+
[[ "$missing_example" -eq 0 ]] &&
|
|
269
|
+
say " ✓ every named example exists ($(wc -l <"$work/matrix-examples.txt" | tr -d ' ') referenced)"
|
|
270
|
+
|
|
271
|
+
# Every command module should be demonstrated by at least one example.
|
|
272
|
+
missing_group=0
|
|
273
|
+
for module in scripts/dot/commands/*.sh; do
|
|
274
|
+
[[ -f "$module" ]] || continue
|
|
275
|
+
name="$(basename "$module" .sh)"
|
|
276
|
+
if ! grep -rqlE "scripts/dot/commands/${name}\.sh" examples/ 2>/dev/null; then
|
|
277
|
+
fail "no example references the command module: $module"
|
|
278
|
+
missing_group=$((missing_group + 1))
|
|
279
|
+
fi
|
|
280
|
+
done
|
|
281
|
+
[[ "$missing_group" -eq 0 ]] &&
|
|
282
|
+
say " ✓ every command module is referenced by an example"
|
|
283
|
+
|
|
284
|
+
# ---------------------------------------------------------------------------
|
|
285
|
+
# Verdict
|
|
286
|
+
# ---------------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
if [[ "$failures" -ne 0 ]]; then
|
|
289
|
+
printf '\nFEATURE-MATRIX drift: %s problem(s).\n' "$failures" >&2
|
|
290
|
+
printf 'Add or correct the row(s) in %s, then re-run this check.\n' "$MATRIX" >&2
|
|
291
|
+
exit 1
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
say ""
|
|
295
|
+
say "FEATURE-MATRIX is in sync."
|
|
296
|
+
exit 0
|