Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/dependabot-digest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ jobs:
permission-contents: read
permission-pull-requests: read
permission-statuses: read
# Reads CodeQL default setup (dev-env#166).
permission-administration: read

- name: Mint a token for nightowlstudiollc
id: token-nightowlstudiollc
Expand All @@ -69,6 +71,8 @@ jobs:
permission-contents: read
permission-pull-requests: read
permission-statuses: read
# Reads CodeQL default setup (dev-env#166).
permission-administration: read

- name: Mint a token for twistedmelonman
id: token-twistedmelonman
Expand All @@ -81,6 +85,8 @@ jobs:
permission-contents: read
permission-pull-requests: read
permission-statuses: read
# Reads CodeQL default setup (dev-env#166).
permission-administration: read

- name: Build and publish the digest
env:
Expand Down
4 changes: 4 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@
docs/data/corpus-440/files/
docs/data/corpus-440/results/
docs/data/corpus-440/files.tar.gz

# Deliberately truncated JSON: the digest test that a malformed response is
# unreadable (dev-env#166). Prettier cannot parse it and blocks the commit.
scripts/dependabot-digest/tests/fixtures/standards/repos_smartwatermelon_withheld_code-scanning_default-setup.json
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ This repo is the **dev-env infrastructure repository** — it contains documenta
- `docs/token-rotation.md` — Retired 2026-10-12: no repo runs Claude in CI and `CLAUDE_CODE_OAUTH_TOKEN` is deleted; never contains a token
- `docs/runbooks/fleet-probe-token-scopes.md` — The two fine-grained-PAT properties a fleet probe needs (`Administration: Read-only` + All-repositories), and why an under-scoped token returns wrong numbers instead of errors
- `scripts/org-migration/` — Snapshot/transfer/verify tooling for the 2026-09 org migration; tests in `scripts/org-migration/tests/run-tests.sh`
- `scripts/dependabot-digest/` — Collects open Dependabot PRs across all three owners and upserts one digest issue describing the queue, plus each repo's standards-check warnings and default-branch protection (`standards.sh`); run by `.github/workflows/dependabot-digest.yml`. Credentials (a GitHub App) are installed by hand: see `docs/runbooks/dependabot-digest-credentials.md`
- `scripts/dependabot-digest/` — Collects open Dependabot PRs across all three owners and upserts one digest issue describing the queue, plus each repo's standards-check warnings, default-branch protection and CodeQL default setup (`standards.sh`); run by `.github/workflows/dependabot-digest.yml`. Credentials (a GitHub App) are installed by hand: see `docs/runbooks/dependabot-digest-credentials.md`
- `scripts/site-monitor/` — Every 15 minutes, checks each production Netlify site in `sites.json` and its latest `site-check` run; keeps one `site-down` issue and pushes ntfy.sh alerts. Run by `.github/workflows/site-monitor.yml`, which a launchd job on MIMOLETTE starts through `dispatch-monitor.sh` because GitHub's cron never fired. The `NTFY_TOPIC` secret and the dispatch token are installed by hand: see `docs/runbooks/site-monitor-ntfy.md`
- `.project-hooks/pre-commit` and `.project-hooks/pre-push` — Project-specific git hook extensions, run by the global hooks at `~/.config/git/hooks/` when executable
- `.claude/` is ignored, as in every fleet repo (dev-env#178). The scaffold the post-checkout hook drops there (`config.sh.template`, `README.md`) comes from `smartwatermelon/dotfiles` `git/template/.claude-template/`; edit it there.
Expand Down
9 changes: 7 additions & 2 deletions docs/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -911,8 +911,13 @@ scheduled run passed 2026-10-02, and plan step 7 (delete the three
the digest also carries a "Standards warnings" section.
Since 2026-10-21 it also carries a "Branch protection" section (closes
`dev-env#211`): repos unprotected or not requiring standards-check, with
exempt classes counted. Whether the App token can read `.protection` is
unverified until the first run after merge.
exempt classes counted. The App token reads `.protection`: run
39661428846 (2026-10-21) surveyed 40 repos, 38 required, 2 exempt, 0
unreadable. A "CodeQL" section (advances `dev-env#166`) reports repos with
default setup off, and lists by name the private repos where scanning is
off because paid Code Security is not bought (cost decision still open on
`#166`). Forks and repos with no scannable language are exempt classes. It
needs the App's Administration: read on all three installations.

Earlier, all daily runs from 09-26 failed on `nightowlstudiollc` check-run
access, so nothing was published.
Expand Down
9 changes: 8 additions & 1 deletion docs/runbooks/dependabot-digest-credentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,14 @@ proof that this path works: a published digest means every check was read.
- **Name**: `dependabot-digest-swm`, installed on `smartwatermelon`,
`nightowlstudiollc` and `twistedmelonman`, **All repositories** each.
- **Repository permissions**, all read-only: Checks, Contents, Metadata,
Pull requests, Commit statuses. Nothing else. Webhook inactive.
Pull requests, Commit statuses, Administration. Nothing else. Webhook
inactive. Administration is there only for the CodeQL section:
`GET repos/{owner}/{repo}/code-scanning/default-setup` needs
Administration: read (GitHub's "Permissions required for GitHub Apps"
table; dev-env#166, approved 2026-10-21). The workflow asks for it
when it mints each token. GitHub rejects a token request for a
permission the installation has not granted, so accept the grant on all
three installations before the workflow runs.
- The private key lives in 1Password vault `Automation`, item `DIGEST_APP`
(fields `client_id`, `private_key`). Use the attached
`dependabot-digest-swm.2026-10-01.private-key.pem`, not the `private_key`
Expand Down
92 changes: 92 additions & 0 deletions scripts/dependabot-digest/render.sh
Original file line number Diff line number Diff line change
Expand Up @@ -348,9 +348,101 @@ render_protection() {
fi
}

# dev-env#166. As for protection: unreadable is never on or off, and an
# all-unreadable survey says so.
render_codeql() {
local file="$1" recs surveyed configured exempt flagged unreadable by_class owners rows
echo "## CodeQL"
echo
if [[ ! -r "${file}" ]] || ! jq -e -s 'type == "array"' "${file}" >/dev/null 2>&1; then
echo "**Not checked.** The survey produced no readable result, so no repository's CodeQL setup was verified."
echo
return 0
fi
# A record without a codeql object (an older or crashed survey) is
# unreadable, not clean.
if ! recs="$(jq -cs '
[.[] | select(type == "object" and (.repo | type == "string") and .state != "archived")
| .c = (if (.codeql | type) == "object" and (.codeql.state | type) == "string"
then .codeql
else {state: "unreadable", detail: "no CodeQL result recorded", exemptClass: null} end)]
' "${file}" 2>/dev/null)" || [[ -z "${recs}" ]]; then
echo "**Not checked.** The survey output could not be summarized, so no repository's CodeQL setup was verified."
echo
return 0
fi
owners="$(jq -rs '[.[] | select(type == "object" and .repo == null and .state == "unlisted") | .owner] | unique | join(", ")' "${file}" 2>/dev/null)"
surveyed="$(jq 'length' <<<"${recs}")"
configured="$(jq '[.[] | select(.c.state == "configured")] | length' <<<"${recs}")"
exempt="$(jq '[.[] | select(.c.state == "exempt")] | length' <<<"${recs}")"
flagged="$(jq '[.[] | select(.c.state == "not-configured")] | length' <<<"${recs}")"
nocs="$(jq '[.[] | select(.c.state == "no-code-security")] | length' <<<"${recs}")"
unreadable=$((surveyed - configured - exempt - flagged - nocs))

echo "Whether each repository has CodeQL default setup turned on. Exempt repositories are left off by a recorded decision for their class; they are counted, not listed."
echo
if [[ "${surveyed}" -eq 0 ]]; then
echo "**No repository was surveyed**, so no CodeQL setup was verified."
echo
elif [[ "${unreadable}" -eq "${surveyed}" ]]; then
# Zero gaps out of zero reads is not "all configured".
echo "**CodeQL setup could not be verified for any of ${surveyed} repositories.** Every read failed or returned no usable state. The likely cause is a token without \`Administration: Read\`, which the default-setup endpoint needs. Nothing here means CodeQL is on or off anywhere."
echo
else
echo "**${surveyed} repositories surveyed**: ${configured} configured, ${exempt} exempt, **${flagged} not configured, ${nocs} without Code Security, ${unreadable} unreadable.**"
echo
if [[ "${exempt}" -gt 0 ]]; then
by_class="$(jq -r '[.[] | select(.c.state == "exempt")] | group_by(.c.exemptClass)
| map("- \(.[0].c.exemptClass // "unknown class"): \(length) — \(.[0].c.detail // "no reason recorded")") | join("\n")' <<<"${recs}")"
echo "Exempt by class:"
echo
echo "${by_class}"
echo
fi
if [[ "${flagged}" -eq 0 && "${unreadable}" -eq 0 && "${nocs}" -eq 0 ]]; then
echo "No gaps: every surveyed repository has default setup on or is exempt."
echo
fi
fi
if [[ "${flagged}" -gt 0 || "${unreadable}" -gt 0 ]]; then
if ! rows="$(jq -r '
def cell: tostring | gsub("[\r\n]+"; " ") | gsub("\\|"; "\\|");
def name: if .c.state == "not-configured" then "NOT CONFIGURED" else "UNREADABLE" end;
sort_by((if .c.state == "not-configured" then 0 else 1 end), .repo)[]
| select(.c.state | IN("configured", "exempt", "no-code-security") | not)
| "| [" + .repo + "](https://github.com/" + .repo + ") | " + name
+ (if .c.detail then ": " + (.c.detail | cell) else "" end) + " |"
' <<<"${recs}")" || [[ -z "${rows}" ]]; then
echo "**Some repositories need attention, but listing them failed.** See the run log."
echo
else
echo "| Repo | CodeQL default setup |"
echo "| --- | --- |"
echo "${rows}"
echo
fi
fi
# Listed by name, not exempt: the cost decision is open (dev-env#166).
if [[ "${nocs}" -gt 0 ]]; then
rows="$(jq -r '[.[] | select(.c.state == "no-code-security") | .repo] | sort[]
| "- [" + . + "](https://github.com/" + . + ")"' <<<"${recs}")"
echo "### Scanning off: paid Code Security not bought"
echo
echo "GitHub refuses code scanning on these private repositories until the paid GitHub Code Security add-on is enabled. Whether to buy it is an open cost decision (dev-env#166)."
echo
echo "${rows:-**${nocs} repositories, but listing them failed.** See the run log.}"
echo
fi
if [[ -n "${owners}" ]]; then
echo "**Not listed, so not surveyed:** ${owners}. CodeQL setup of their repositories is unknown."
echo
fi
}

if [[ -n "${standards_file}" ]]; then
render_standards "${standards_file}"
render_protection "${standards_file}"
render_codeql "${standards_file}"
fi

echo "---"
Expand Down
83 changes: 78 additions & 5 deletions scripts/dependabot-digest/standards.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# Usage: standards.sh <owner>. Prints one JSON record per repo: standards-check
# annotations, or the reason none were read, plus the default branch's
# protection state (dev-env#211).
# protection state (dev-env#211) and CodeQL default setup (dev-env#166).
set -uo pipefail
unset CDPATH

Expand Down Expand Up @@ -35,18 +35,18 @@ emit() {
--arg detail "$3" --arg source "${4-}" --arg url "${5-}" \
--argjson ann "${6:-[]}" --arg runAt "${7-}" \
--arg rulesAt "${RULES_AT}" --arg rulesErr "${RULES_ERR}" \
--argjson prot "${PROT_JSON:-null}" \
--argjson prot "${PROT_JSON:-null}" --argjson cq "${CQ_JSON:-null}" \
'{owner: $owner, repo: (if $repo == "" then null else $repo end),
state: $state, detail: $detail,
source: (if $source == "" then null else $source end),
runUrl: (if $url == "" then null else $url end),
runAt: (if $runAt == "" then null else $runAt end),
rulesAt: (if $rulesAt == "" then null else $rulesAt end),
rulesErr: (if $rulesErr == "" then null else $rulesErr end),
annotations: $ann, protection: $prot}' 2>/dev/null)"
# If jq fails, the repo must still appear. Its protection is unknown.
annotations: $ann, protection: $prot, codeql: $cq}' 2>/dev/null)"
# If jq fails, the repo must still appear. Protection and CodeQL are unknown.
if [[ -z "${line}" ]]; then
printf '{"owner":"%s","repo":"%s","state":"unreadable","detail":"could not encode the result","source":null,"runUrl":null,"runAt":null,"rulesAt":null,"rulesErr":null,"annotations":[],"protection":null}\n' \
printf '{"owner":"%s","repo":"%s","state":"unreadable","detail":"could not encode the result","source":null,"runUrl":null,"runAt":null,"rulesAt":null,"rulesErr":null,"annotations":[],"protection":null,"codeql":null}\n' \
"${owner}" "$1"
return 0
fi
Expand Down Expand Up @@ -214,6 +214,75 @@ probe_protection() {
set_prot "${state}" "${detail}" "${cls}"
}

# dev-env#166: CodeQL default setup. Exempt classes are counted, not
# flagged. A private repo without paid Code Security is NOT exempt.
declare -A CQ_EXEMPT_REASON=(
[fork]="a fork is not ours to scan (decided 2026-10-21, dev-env#166)"
[no-supported-language]="GitHub detects no language default setup can scan (2026-10-19, dev-env#166)"
)
# The 403 body when the paid product is off. Any other 403 is a token gap.
# Matched on the body: stderr can carry noise.
CQ_NO_PRODUCT='^(Code|Advanced) Security must be enabled for this repository to use code scanning\.$'

# Prints "state<TAB>detail". An odd field is unreadable, never a result.
read -r -d '' cq_verdict <<'JQ'
if type != "object" then "unreadable\tthe default-setup response is malformed"
elif .state == "configured" then "configured\t"
elif .state != "not-configured" then "unreadable\tunknown default-setup state: " + (.state | tostring)
elif (.languages | type) != "array" then "unreadable\tnot configured, and no language list was returned"
elif (.languages | length) == 0 then "no-language\t"
else "not-configured\tdefault setup is off; languages: " + (.languages | map(tostring) | join(", "))
end
JQ

CQ_JSON=""
set_cq() {
# set_cq <state> <detail> [exempt class]
CQ_JSON="$(jq -cn --arg s "$1" --arg d "$2" --arg c "${3-}" \
'{state: $s, detail: (if $d == "" then null else $d end),
exemptClass: (if $c == "" then null else $c end)}' 2>/dev/null)"
if [[ -z "${CQ_JSON}" ]]; then
CQ_JSON='{"state":"unreadable","detail":"could not encode the CodeQL result","exemptClass":null}'
fi
}
set_cq_exempt() { set_cq exempt "${CQ_EXEMPT_REASON[$1]-no reason recorded}" "$1"; }

# Sets CQ_JSON for one repo. Never fails: a failed read is a state.
probe_codeql() {
local nwo="$1" fork="$2" private="$3" body why msg verdict state detail
if ! body="$(api "repos/${nwo}/code-scanning/default-setup")"; then
why="$(last_err "no detail")"
msg="$(jq -r 'if type == "object" then .message // "" else "" end' <<<"${body}" 2>/dev/null)"
# Only the exact product message, on a repo the listing says is
# private: a public repo has no paywall to explain it.
if [[ "${why}" == *"(HTTP 403)"* && "${msg}" =~ ${CQ_NO_PRODUCT} \
&& "${private}" == "true" ]]; then
set_cq no-code-security "${msg}"
else
set_cq unreadable "cannot read default setup: ${why}"
fi
return 0
fi
verdict="$(jq -r "${cq_verdict}" <<<"${body}" 2>/dev/null)"
state="${verdict%%$'\t'*}"
detail="${verdict#*$'\t'}"
case "${state}" in
configured | unreadable) set_cq "${state}" "${detail}" ;;
no-language) set_cq_exempt no-supported-language ;;
not-configured)
# A fork is exempt; without a fork flag, exemption cannot be decided.
if [[ "${fork}" == "true" ]]; then
set_cq_exempt fork
elif [[ "${fork}" != "false" ]]; then
set_cq unreadable "not configured, but the repository listing gave no fork flag, so exemption cannot be decided"
else
set_cq not-configured "${detail}"
fi
;;
*) set_cq unreadable "the default-setup response is malformed" ;;
esac
}

survey_repo() {
local nwo="$1" branch="$2" head pulls pr pr_head run source ann count url
if ! head="$(api "repos/${nwo}/commits/${branch}" --jq '.sha')" \
Expand Down Expand Up @@ -313,13 +382,17 @@ fi
while IFS=$'\t' read -r nwo archived fork private branch; do
[[ -z "${nwo}" ]] && continue
PROT_JSON=""
CQ_JSON=""
if [[ "${archived}" == "true" ]]; then
emit "${nwo}" archived "archived; not surveyed"
elif [[ -z "${branch}" ]]; then
set_prot unreadable "no default branch reported"
# Default setup is per repo, not per branch, so it is still read.
probe_codeql "${nwo}" "${fork}" "${private}"
emit "${nwo}" unreadable "no default branch reported"
else
probe_protection "${nwo}" "${branch}" "${fork}" "${private}"
probe_codeql "${nwo}" "${fork}" "${private}"
survey_repo "${nwo}" "${branch}"
fi
done <<<"${repo_list}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"message": "Code Security must be enabled for this repository to use code scanning.",
"documentation_url": "https://docs.github.com/rest/code-scanning/code-scanning#get-a-code-scanning-default-setup-configuration",
"status": "403"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"message": "Resource not accessible by integration",
"documentation_url": "https://docs.github.com/rest/code-scanning/code-scanning#get-a-code-scanning-default-setup-configuration",
"status": "403"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"state": "not-configured",
"languages": [],
"query_suite": "default",
"threat_model": "remote",
"updated_at": "2026-10-19T23:22:09Z",
"schedule": "weekly",
"runner_type": "standard",
"runner_label": ""
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"state": "enabled",
"languages": ["actions"],
"query_suite": "default",
"threat_model": "remote",
"updated_at": "2026-10-19T23:22:09Z",
"schedule": "weekly",
"runner_type": "standard",
"runner_label": ""
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"state": "not-configured",
"languages": ["actions"],
"query_suite": "default",
"threat_model": "remote",
"updated_at": "2026-10-19T23:22:09Z",
"schedule": "weekly",
"runner_type": "standard",
"runner_label": ""
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"state": "not-configured",
"languages": ["actions"],
"query_suite": "default",
"threat_model": "remote",
"updated_at": "2026-10-19T23:22:09Z",
"schedule": "weekly",
"runner_type": "standard",
"runner_label": ""
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"message": "Advanced Security must be enabled for this repository to use code scanning.",
"documentation_url": "https://docs.github.com/rest/code-scanning/code-scanning#get-a-code-scanning-default-setup-configuration",
"status": "403"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"state": "configured",
"languages": ["actions"],
"query_suite": "default",
"threat_model": "remote",
"updated_at": "2026-10-19T23:22:09Z",
"schedule": "weekly",
"runner_type": "standard",
"runner_label": ""
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"message": "Code Security must be enabled for this repository to use code scanning.",
"documentation_url": "https://docs.github.com/rest/code-scanning/code-scanning#get-a-code-scanning-default-setup-configuration",
"status": "403"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"state":"configured","languages":["acti
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"state": "not-configured",
"languages": ["actions"],
"query_suite": "default",
"threat_model": "remote",
"updated_at": "2026-10-19T23:22:09Z",
"schedule": "weekly",
"runner_type": "standard",
"runner_label": ""
}
Loading
Loading