Skip to content

feat(digest): survey default-branch protection in the daily digest - #214

Merged
twistedmelonman merged 1 commit into
mainfrom
claude/feat-digest-protection-survey
Oct 21, 2026
Merged

twistedmelonman merged 1 commit into
mainfrom
claude/feat-digest-protection-survey

Conversation

@twistedmelonman

Copy link
Copy Markdown
Member

Summary

Answers the last open question on #211: the daily digest now flags fleet repos whose default branch is unprotected or does not require standards-check / run-standards-check.

  • standards.sh reads GET repos/{nwo}/branches/{branch} in the existing per-repo loop (no second enumeration) and adds a protection object to each record: required, protected-no-standards, unprotected, exempt (with class), or unreadable.
  • The probe runs before the standards survey, so a repo whose standards read fails still gets a protection result.
  • render.sh adds a "Branch protection" section. Required and exempt repos are counted (exempt by class, with reason and decision date). Unprotected, protected-no-standards and unreadable repos are listed with links. If no repo could be read, the section says protection could not be verified and prints no counts.

Design choices

  • required needs more than .protected == true: it also needs protection.enabled == true, an enforcement_level other than off, and a context matching the existing standards-check name rule. STATUS.md (2026-09-16) records .protected reading true for repos with no effective rules; today's capture of claude-config-backup reads false, so that note may be stale, but the stricter rule costs nothing.
  • Unreadable, never guessed: a failed read, a non-boolean .protected, missing or malformed protection settings, or an unprotected repo whose listing has no fork/private flag all render as UNREADABLE.
  • Exempt classes, not names: forks; private smartwatermelon repos ([infra] Make smartwatermelon/scripts public: secret audit, history rewrite, branch protection #85); private twistedmelonman repos (Free user accounts cannot protect them). Private nightowlstudiollc repos are not exempt. The classes are three predicates, so they are an associative array plus a small function in standards.sh, not a parsed config file.
  • The real unprotected response is protected:false with protection present (enabled:false, enforcement_level:"off", contexts:[]), not an absent required_status_checks. The fixtures use that real shape.

Unverified

  • Whether the digest App token (checks/contents/pull-requests/statuses read, no administration) gets the protection sub-object. Session tokens did. If it does not, every repo reads UNREADABLE and the section says so. No permission was added to the workflow.
  • A protected branch with required_status_checks: null (protection by reviews only) reads UNREADABLE, not protected-no-standards. No fleet repo is in that state today, so the real shape was not captured.

Tests

  • New fixtures: five real branch responses captured 2026-10-21 (trimmed to name/protected/protection), and edge cases built from those captures with one field changed: protection withheld, protected: null, enforcement off, other checks only, NOS private, twistedmelonman private, a public unprotected repo, missing listing flags, and a 403.
  • Each new assertion was checked against a deliberate break of the logic it covers.
  • bash scripts/dependabot-digest/tests/run-tests.sh: classify, render, run-digest and standards pass. Locally, test-upsert-issue.sh (untouched) fails because the session exports a gh shell function (BASH_FUNC_gh%%) whose approval gate overrides the PATH stub. CI has no such function.
  • shellcheck -S info is clean on every touched script.

Closes #211

dev-env#211 asked whether a periodic check should flag fleet repos that
lack protection or a required standards-check. jordan-paddock-pitch went a
week without either, and nothing noticed. The digest already enumerates
every repo the App can read, so the check rides that loop instead of
adding a second enumeration.

standards.sh now reads GET repos/{nwo}/branches/{branch} for each
non-archived repo and adds a "protection" object to its record. The probe
runs before the standards survey, so a repo whose standards read fails
still gets a protection result. States:

- required: protected, enabled, enforcement not "off", and a required
  context matching the existing standards-check name rule.
- protected-no-standards: protected, but standards-check is not
  required or nothing is enforced. .protected == true alone never counts
  as required: STATUS.md records (2026-09-16) that .protected read true
  for repos with no effective rules.
- unprotected: .protected == false and no exempt class applies.
- exempt: unprotected, and in a class exempt by decision.
- unreadable: the read failed, .protected is not a boolean, the
  protection settings are missing or malformed, or the listing gave no
  fork/private flag for an unprotected repo. Whether the App token
  (contents/checks/statuses/pull-requests read, no administration) gets
  the protection object is unverified, so a withheld field must read as
  unreadable, never as a finding or as clean.

Exempt classes are predicates, not repo names: forks, private
smartwatermelon repos (dev-env#85), private twistedmelonman repos (Free
user accounts cannot protect them). Private nightowlstudiollc repos are
not exempt: NOS keeps Team and protection there is a requirement. Three
predicates on fork, owner and visibility live in a commented block in
standards.sh; a parsed config file would need its own mini-language.

render.sh adds a "Branch protection" section: required and exempt repos
are counted (exempt by class, with the reason); unprotected,
protected-no-standards and unreadable repos are listed with links. If no
repo could be read, the section says protection could not be verified
and prints no counts. A record with no protection object is unreadable.

Fixtures for required, exempt and unprotected states are real branch
responses captured 2026-10-21, trimmed to name/protected/protection. The
edge cases are those captures with one field changed. Each new assertion
was checked against a deliberate break of the logic it covers.

Closes #211
@twistedmelonman
twistedmelonman merged commit 9482539 into main Oct 21, 2026
5 checks passed
@twistedmelonman
twistedmelonman deleted the claude/feat-digest-protection-survey branch October 21, 2026 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

jordan-paddock-pitch is outside the fleet baseline

1 participant