Skip to content

configure.sh check mode reports a clean audit over drift it never compares #538

Description

@ptr727

Raised from the fleet re-vendor sweep, where Copilot reviewed the canonical repo-config/configure.sh as it landed in five repos and found the same class of defect from five different angles. Every claim below was re-verified against the hub's own copy on develop at 1ed0cc8 rather than taken from the downstream reviews.

The unifying problem: check is the read-only inverse of apply and is what a maintainer runs to confirm a repo is configured correctly, but it compares a fraction of what apply sets. A live ruleset can drift in ways that matter and check still prints a clean pass.

1. Rule parameters are mostly uncompared

check_ruleset asserts that every rule type in the payload exists live, then compares parameters for exactly two of them: pull_request.allowed_merge_methods and the required_status_checks context list.

Everything else in the payloads is unchecked, including:

  • copilot_code_review.parameters (for example review_on_push)
  • pull_request beyond merge methods (required_review_thread_resolution, dismiss_stale_reviews_on_push)
  • required_status_checks beyond contexts (strict_required_status_checks_policy, do_not_enforce_on_create)

All of these are present in the committed payloads, so the payload already states the intent that check declines to verify.

2. An extra live rule is invisible

The presence loop is one-directional: payload types must exist live, and nothing asserts the reverse. A rule added to a live ruleset that the payload never declared (say required_linear_history on main) passes a clean audit. Drift that a human introduced by hand in the GitHub UI is exactly the drift this script exists to catch.

3. The header over-promises what check verifies

Two adjacent lines disagree:

The check mode is the read-only inverse, where every applied ruleset, setting, and security feature must match.

That also survives the GitHub API normalizing a stored ruleset, comparing rule presence, merge methods and required checks rather than a byte diff.

The second sentence is accurate and the first is not. An operator reads the first.

4. check_settings can silently become a no-op

done < <(jq -r 'to_entries[] | "\(.key)\t\(.value)"' "$settings_file")

If jq fails, for instance on a malformed settings.json, the loop body never runs. set -e does not reliably trip on a failure inside process substitution, so every static setting reports as checked and passing while nothing was compared. This is the worst finding here, because it is a false clean rather than a partial one, and it is the same shape as the prose_lint.py scope floor already tracked in TODO.md: a gate that finds nothing is indistinguishable from a gate with nothing to find. Capture the jq output first and fail loudly if it does not parse.

5. It names hub-only machinery a downstream repo cannot open

Three places point the reader at spec/secrets.json:

  • the header comment
  • the cmd_check comment
  • the note printed to the operator

configure.sh is carried verbatim with appliesTo: "*", and spec/ is hub-only, so in every downstream repo that note points at a path that does not exist. This is a second instance of the TODO.md entry "Decide where a carried file may name hub-only machinery", which currently cites only the scripts/pr_review.py case, so the two should be settled together.

Not a defect, recorded so it is not re-raised

Reviews on two repos flagged that REPO=owner/repo ./repo-config/configure.sh apply no longer works while repo-config/README.md documents it. The hub script does not honor REPO and the hub README does not document it, so this is stale prose in those repos' older carried READMEs rather than a hub defect. It resolves when they take the current canonical.

Sequencing

The fleet re-vendor of this file is in flight and paused on this. Fixing the script here first is cheaper than re-vendoring twice, and four repos additionally need a payload-filename migration (ruleset-*.json to the canonical names) before they can take the canonical at all.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions