diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d0dcd7c..30ad478 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,3 +26,11 @@ jobs: - name: Run site-monitor tests run: bash scripts/site-monitor/tests/run-tests.sh + + # Not hermetic: reads claude-config's conf at main (claude-config#725). + # curl -f and the checker both fail on a failed or empty download. + - name: Check every site has a copy-paths.conf entry + run: | + curl -fsSL --retry 3 -o "${RUNNER_TEMP}/copy-paths.conf" \ + https://raw.githubusercontent.com/smartwatermelon/claude-config/main/copy-paths.conf + bash scripts/site-monitor/check-copy-paths.sh "${RUNNER_TEMP}/copy-paths.conf" diff --git a/docs/STATUS.md b/docs/STATUS.md index d93bf5f..eb46645 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -2,7 +2,7 @@ **As of 2026-09-16, with 2026-10-01, 2026-10-02 (three), 2026-10-05 (three), 2026-10-06, 2026-10-06/07, 2026-10-07 evening, 2026-10-08, -2026-10-12 and 2026-10-13 to 10-16 updates below.** +2026-10-12, 2026-10-13 to 10-16 and 2026-10-19 updates below.** Point-in-time snapshot of the infrastructure backlog (`docs/superpowers/specs/2026-09-01-infrastructure-backlog-design.md`). The design doc is authoritative on *what* each item is and why; this file records @@ -512,7 +512,39 @@ or be explicitly parked with a resume note, by then — see the tracker. - **Auto mode pilot on ASIAGO** (`claude-wrapper#139`, open), with per-machine settings layered over the shared file (`claude-wrapper#138`). - **Open:** `claude-config#731` (the secret hook misses - `op item get --format json`). + `op item get --format json`). Closed 2026-10-19, below. + +**2026-10-19 update (Pacific):** + +- **Hooks, all live on ASIAGO:** + - `claude-config#745` closes `#731`: the secret-leak hook denies + `op item get` forms that print values (`--format json`, `OP_FORMAT=json`, + `--reveal`, `--fields`, `--otp`) and `op document get` without + `--out-file`; the redactor replaces such output. No rotation needed for + the 10-15 exposure (prefixes only). + - `#746` closes `#742`: a quoted `cd` in a literal `printf`/`echo` + argument no longer denies. + - `#749` closes `#637`: commits on main through `env -C` or `xargs` are + blocked. Remaining parse gaps: `#750`. + - `#751` closes `#744`: the Pangram copy guard accepts the synced + `personify~gN` path (the `~gN` suffix is the sync generation and will + change) and no longer reads `sort -k2` as a kind flag. + - `#748` closes `#681`: codebase review runs with `--tools`, + `--allowedTools` and `--strict-mcp-config`, like the arbiter. + - `dotfiles#442` closes `#434`: the `gh` wrapper finds the `gh api` + endpoint past value-taking flags and blocks when it cannot. +- **github-workflows auto-merges its own Dependabot PRs** (`#197`), pinned to + `@dependabot-auto-merge-v2`. Every fleet caller pins a hand-moved tag, so a + merge there reaches no other repo. +- **CodeQL default setup on 28 more repos** (`dev-env#166`, open for two + decisions: Code Security for the 8 private NOS repos, and a digest check). +- **"Require branches to be up to date" is off on all 37 protected repos**, + by decision: it cost a CI re-run per merge. This reverses the W0 exemplar's + `strict: true`. Required checks are unchanged. +- **dev-env CI checks `sites.json` against claude-config's + `copy-paths.conf`** (`dev-env#210`, closes `claude-config#725`). +- **Open:** `claude-config#747` (a quoted `'; cd '` moves the gate's + route), `#750`, `#732` (fast CI for `known-runtime.txt`-only PRs). **Kanban tracker: GitHub Issues + a Project board in `smartwatermelon/dev-env`**, using the org's `Priority`/`Target date`/`Effort` issue fields. Board columns diff --git a/scripts/site-monitor/check-copy-paths.sh b/scripts/site-monitor/check-copy-paths.sh new file mode 100755 index 0000000..9901946 --- /dev/null +++ b/scripts/site-monitor/check-copy-paths.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Fails when a sites.json repo has no include line in copy-paths.conf. Exit 0 ok, 1 repo missing, 2 bad input. +set -uo pipefail +unset CDPATH + +HERE="$(CDPATH='' cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONF="${1:-}" +SITES="${2:-${HERE}/sites.json}" + +die() { + echo "check-copy-paths: $1" >&2 + exit 2 +} + +[[ -n "${CONF}" ]] || die "usage: check-copy-paths.sh [sites.json]" +[[ -r "${CONF}" ]] || die "cannot read ${CONF}" +[[ -r "${SITES}" ]] || die "cannot read ${SITES}" + +# Fail closed: a truncated or wrong download must not pass, so the conf needs at least one owner/name include line. +included="$(awk '$1 ~ /^[^#=\/]+\/[^\/]+$/ && $2 == "include" { print $1 }' "${CONF}" | sort -u)" +[[ -n "${included}" ]] || die "${CONF} has no owner/name include lines; refusing to pass on an empty or unreadable conf" + +if ! jq -e 'type == "array" and length > 0 and all(.[]; (.repo | type) == "string")' "${SITES}" >/dev/null 2>&1; then + die "${SITES} is not a non-empty array of objects with a string .repo" +fi +repos="$(jq -r '[.[].repo] | unique | .[]' "${SITES}")" || die "cannot list repos in ${SITES}" + +missing=0 +while IFS= read -r repo; do + [[ -n "${repo}" ]] || continue + if ! grep -qxF -- "${repo}" <<<"${included}"; then + echo "check-copy-paths: ${repo} is in ${SITES##*/} but has no include line in ${CONF##*/}" >&2 + missing=1 + fi +done <<<"${repos}" + +if [[ "${missing}" -ne 0 ]]; then + echo "check-copy-paths: add include lines for the repos above to smartwatermelon/claude-config copy-paths.conf" >&2 + exit 1 +fi +echo "check-copy-paths: every repo in ${SITES##*/} has a copy-paths.conf entry" +exit 0 diff --git a/scripts/site-monitor/tests/fixtures/copy-paths/covered.conf b/scripts/site-monitor/tests/fixtures/copy-paths/covered.conf new file mode 100644 index 0000000..49705b2 --- /dev/null +++ b/scripts/site-monitor/tests/fixtures/copy-paths/covered.conf @@ -0,0 +1,6 @@ +# Every repo in sites.json has an include line. +public=* include **/README* + +owner-a/site-a include index.html +owner-a/site-a exclude vendor/** +owner-b/site-b include src/** diff --git a/scripts/site-monitor/tests/fixtures/copy-paths/missing.conf b/scripts/site-monitor/tests/fixtures/copy-paths/missing.conf new file mode 100644 index 0000000..32aa2a7 --- /dev/null +++ b/scripts/site-monitor/tests/fixtures/copy-paths/missing.conf @@ -0,0 +1,7 @@ +# owner-b/site-b is missing. Its only lines are an exclude, a comment, and a near-miss name, none of which count. +public=* include **/README* + +owner-a/site-a include index.html +owner-b/site-b exclude vendor/** +# owner-b/site-b include index.html +owner-b/site-b-old include index.html diff --git a/scripts/site-monitor/tests/fixtures/copy-paths/no-entries.conf b/scripts/site-monitor/tests/fixtures/copy-paths/no-entries.conf new file mode 100644 index 0000000..bb83e1b --- /dev/null +++ b/scripts/site-monitor/tests/fixtures/copy-paths/no-entries.conf @@ -0,0 +1,2 @@ +# Comments and public=* lines only: what a truncated download could look like. +public=* include **/README* diff --git a/scripts/site-monitor/tests/fixtures/copy-paths/sites.json b/scripts/site-monitor/tests/fixtures/copy-paths/sites.json new file mode 100644 index 0000000..93335ce --- /dev/null +++ b/scripts/site-monitor/tests/fixtures/copy-paths/sites.json @@ -0,0 +1,5 @@ +[ + { "url": "https://a.example", "repo": "owner-a/site-a" }, + { "url": "https://b.example", "repo": "owner-b/site-b" }, + { "url": "https://b2.example", "repo": "owner-b/site-b" } +] diff --git a/scripts/site-monitor/tests/test-check-copy-paths.sh b/scripts/site-monitor/tests/test-check-copy-paths.sh new file mode 100755 index 0000000..4019489 --- /dev/null +++ b/scripts/site-monitor/tests/test-check-copy-paths.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# check-copy-paths.sh against fixture confs: covered, missing, and each input that must fail closed. +set -uo pipefail +unset CDPATH + +HERE="$(CDPATH='' cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCRIPT="${HERE}/../check-copy-paths.sh" +FIX="${HERE}/fixtures/copy-paths" +WORK="/tmp/site-monitor-copy-paths-test-$$" +mkdir -p "${WORK}" +trap 'rm -f "${WORK}"/*; rmdir "${WORK}"' EXIT + +fail=0 +_pass() { echo " PASS: $1"; } +_fail() { echo " FAIL: $1" >&2; fail=1; } +check() { + local name="$1" + shift + if "$@"; then _pass "${name}"; else _fail "${name}"; fi +} +check_not() { + local name="$1" + shift + if "$@"; then _fail "${name}"; else _pass "${name}"; fi +} +# expect_exit WANT ARGS...: run the checker with ARGS and compare its exit status. +expect_exit() { + local want="$1" rc=0 + shift + "${CHECK_TEST_BASH:-bash}" "${SCRIPT}" "$@" >"${WORK}/out" 2>"${WORK}/err" || rc=$? + if [[ "${rc}" -eq "${want}" ]]; then _pass "exit ${want}"; else _fail "exit ${rc}, want ${want}"; fi +} + +echo "-- every repo has an include line: exit 0" +expect_exit 0 "${FIX}/covered.conf" "${FIX}/sites.json" +check "says all covered" grep -q 'every repo' "${WORK}/out" + +echo "-- a repo with only an exclude, a comment, or a near-miss name: exit 1, named once" +expect_exit 1 "${FIX}/missing.conf" "${FIX}/sites.json" +check "names the missing repo" grep -qF 'owner-b/site-b is in' "${WORK}/err" +named="$(grep -cF 'owner-b/site-b is in' "${WORK}/err")" || named=0 +check "named once despite two sites" test "${named}" -eq 1 +check_not "covered repo not named" grep -qF 'owner-a/site-a' "${WORK}/err" + +echo "-- a conf with no owner/name include lines fails closed: exit 2" +expect_exit 2 "${FIX}/no-entries.conf" "${FIX}/sites.json" +check "says why" grep -q 'no owner/name include lines' "${WORK}/err" + +echo "-- an empty conf (a failed download) fails closed: exit 2" +: >"${WORK}/empty.conf" +expect_exit 2 "${WORK}/empty.conf" "${FIX}/sites.json" + +echo "-- a missing conf file: exit 2" +expect_exit 2 "${WORK}/absent.conf" "${FIX}/sites.json" +check "says why" grep -q 'cannot read' "${WORK}/err" + +echo "-- no conf argument: exit 2" +expect_exit 2 + +echo "-- a malformed sites.json: exit 2" +printf '{"repo":"owner-a/site-a"}\n' >"${WORK}/bad-sites.json" +expect_exit 2 "${FIX}/covered.conf" "${WORK}/bad-sites.json" + +echo "-- an empty sites.json array: exit 2" +printf '[]\n' >"${WORK}/empty-sites.json" +expect_exit 2 "${FIX}/covered.conf" "${WORK}/empty-sites.json" + +echo "-- the default sites.json is the real one next to the script" +expect_exit 1 "${FIX}/covered.conf" +check "reads real sites.json" grep -qF 'sites.json but has no include line' "${WORK}/err" + +# MIMOLETTE runs this directory under macOS /bin/bash 3.2, so repeat every case there when this host has it. +if [[ -z "${CHECK_TEST_BASH:-}" ]] && /bin/bash -c '[[ ${BASH_VERSINFO[0]} -lt 4 ]]' 2>/dev/null; then + echo "-- every case again under /bin/bash" + CHECK_TEST_BASH=/bin/bash bash "${BASH_SOURCE[0]}" || fail=1 +fi + +exit "${fail}" diff --git a/scripts/site-monitor/tests/test-run-monitor.sh b/scripts/site-monitor/tests/test-run-monitor.sh index 7b7cb3e..124658e 100755 --- a/scripts/site-monitor/tests/test-run-monitor.sh +++ b/scripts/site-monitor/tests/test-run-monitor.sh @@ -3,6 +3,8 @@ set -uo pipefail unset CDPATH unset BASH_ENV GH_TOKEN GH_HOST GITHUB_TOKEN NTFY_TOPIC +# An exported gh function (the dotfiles wrapper) beats the stub on PATH in run-monitor.sh, so drop it. +unset -f gh HERE="$(CDPATH='' cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DIR="${HERE}/.."