Skip to content

[Phase 2] Cross-repo remediation PR driver (DRY_RUN-gated, idempotent) #1150

Closed

Description

@github-actions

Story

As a fleet platform maintainer,
I want a DRY_RUN-aware network driver that consumes the remediation plan and, per consumer repo, creates a branch, writes the canonical stub bytes, and opens exactly one PR,
so that detected drift is auto-corrected as an auditable, deterministic proposal instead of manual per-issue toil.

Acceptance Criteria

  1. Given the Phase-1 plan (from fleet_stub_drift.json), for each remediable {repo, stub_file}: fetch the canonical bytes from CANONICAL_STUB_REPO/standards/workflows/.yml (the source whose SHA drift was measured against), create a branch on the consumer repo, write the content via the contents API PUT, and open ONE PR per repo grouping all that repo's drifted stubs.
  2. DRY_RUN-aware: when DRY_RUN is truthy the driver logs the intended branch/PUT/PR per repo and makes zero write API calls (mirrors seed-repo-template.sh _is_dry/_seed_repo). DRY_RUN is the default in this story.
  3. Idempotent: if a remediation branch or an open remediation PR already exists for the repo, the driver skips re-creating it (mirrors seed-repo-template.sh's existing-PR check via gh pr list --head).
  4. The PR is opened against the consumer repo's default branch as a proposal — never a direct push to the default branch — with a deterministic title/body citing the canonical source and the drift.
  5. Post-write, the driver asserts per repo that the resulting file blob SHA equals the canonical SHA (byte-identity ⇒ drift closed) and fails that repo on mismatch rather than opening a misleading PR; the fleet-level verification summary is Phase 3's responsibility.
  6. bats tests stub gh and assert: dry-run makes no writes, the branch+PUT+PR sequence for a drifted repo, the idempotent skip, and one-PR-per-repo grouping; shellcheck passes.
  7. Fail-closed blast-radius bound: the driver refuses to run live (DRY_RUN=false) unless an explicit repo scope is supplied — with no scope it stays dry-run and emits a ::warning::. This guarantees a bound even if the driver is invoked before the Phase-3 pilot scope / REMEDIATE_MAX_REPOS ceiling lands.

Tasks / Subtasks

Dev Notes

  • Reuse the cross-repo PR machinery in shape from seed-repo-template.sh: default_branch lookup, git/ref/heads base sha, git/refs POST to create the branch, _put_file (contents PUT with the existing file sha when overwriting), gh pr list --head to detect an already-open PR, and a single gh pr create. [Source: scripts/seed-repo-template.sh]
  • The canonical content fetch mirrors fleet_monitor.sh's canonical SHA read (repos/${CANONICAL_STUB_REPO}/contents/${canonical_path}); fetch .content and base64 -d for the bytes. Because the stub is deployed verbatim, PUTting these bytes makes the consumer blob SHA equal the canonical SHA — closing drift deterministically. [Source: scripts/fleet_monitor.sh]
  • DRY_RUN convention: follow _is_dry()/DRY_RUN from seed-repo-template.sh and the repo's dry-run-first canaries so the first wiring is inert. [Source: scripts/seed-repo-template.sh, AGENTS.md]
  • Blast-radius: enforce at most one PR per consumer repo per run (group by repo). The per-run max-repos ceiling and pilot scoping land in Phase 3.
  • Supply-chain posture (idea's adversarial review): PRs are proposals through the normal PR-review pipeline, content is deterministic and byte-identical to canon, and the Phase-1 allowlist protects customized files. Encode 'never push to default branch' as an AC.
  • Testing: bats with a gh shim on PATH asserting the API call sequence, matching how the repo's cross-repo scripts are exercised. [Source: tests/fleet_stub_drift.bats, AGENTS.md]

Project Structure Notes

The network driver lives in the same scripts/fleet_stub_remediate.sh added in Phase 1 (pure helpers + a source-guarded main()), matching how seed-repo-template.sh co-locates pure helpers with a network main().

References

  • scripts/seed-repo-template.sh
  • scripts/fleet_monitor.sh
  • scripts/fleet_stub_drift.sh
  • AGENTS.md

Likely target surface

  • scripts/fleet_stub_remediate.sh (network main)
  • tests/fleet_stub_remediate.bats (new)

Story prepared by the BMAD Scrum Master (Bob) for epic #1148. Status: ready-for-dev.

Activity

  1. don-petry commented on Jul 15, 2026

    @don-petry
    Collaborator

    Dev-Lead Implementation Plan

    Issue: #1150 — [Phase 2] Cross-repo remediation PR driver (DRY_RUN-gated, idempotent)

    Scope

    Add a DRY_RUN-gated, idempotent, fail-closed network driver to scripts/fleet_stub_remediate.sh that consumes the Phase-1 remediation plan (from fleet_stub_drift.json), and per consumer repo fetches the canonical stub bytes, creates a branch, PUTs the content, verifies byte-identity (blob SHA == canonical SHA), and opens exactly one PR against the repo's default branch.

    Implementation Checklist

    • scripts/fleet_stub_remediate.sh: add DRY_RUN (default true) + _is_dry helper (mirror seed-repo-template.sh).
    • Add _canonical_bytes / _canonical_sha helpers (mirror fleet_monitor.sh canonical read: contents/<path> → .content|base64 -d and .sha).
    • Add _put_file (contents PUT with existing-file sha when overwriting) — reuse seed-repo-template.sh shape.
    • Add repo-scope helpers (_scope_list, _in_scope) reading REMEDIATE_REPOS + --repo flag(s).
    • Add _remediate_repo network function: idempotent open-PR skip, branch create-or-reuse, per-stub fetch+PUT+SHA-verify, one gh pr create. Fail the repo on SHA mismatch (no misleading PR).
    • Rewrite main() as the network driver: build plan via existing pure build_remediation_plan, group entries by repo (one branch + one PR per repo), apply the fail-closed scope gate, iterate repos.
    • Keep the pure helpers (build_remediation_plan, resolve_canonical_path, remediation_allowlisted) unchanged; source-guard preserved.

    Tests to Write (append to tests/fleet_stub_remediate.bats)

    • DRY_RUN (default) makes zero write API calls and logs intended branch/PUT/PR per repo.
    • Live run for a drifted repo performs the branch POST + contents PUT + one gh pr create sequence (asserted via a gh shim on PATH logging calls).
    • Idempotent skip: an already-open remediation PR (or existing branch) causes the repo to be skipped with no writes.
    • One-PR-per-repo grouping: a repo with two drifted stubs yields one branch, two PUTs, and exactly one PR.
    • Post-write SHA-verify: a written blob SHA != canonical SHA fails the repo and opens no PR.
    • Fail-closed: DRY_RUN=false with no repo scope stays dry-run, emits a ::warning::, and makes no writes.

    Test Command

    bats tests/fleet_stub_remediate.bats and bash .dev-lead/scripts/dev-lead-lint.sh

  2. don-petry commented on Jul 15, 2026

    @don-petry
    Collaborator

    Dev-Lead: Implementation Complete

    Plan Execution

    Test Results

    1..22
    ok 1 resolve_canonical_path: planner stub resolves to its canonical path
    ok 2 resolve_canonical_path: driver stub resolves to its canonical path
    ok 3 resolve_canonical_path: an unknown stub_file is unresolved (non-zero)
    ok 4 remediation_allowlisted: a whole-repo allowlist entry matches any stub in that repo
    ok 5 remediation_allowlisted: a repo+stub_file entry matches only that file
    ok 6 remediation_allowlisted: a non-allowlisted repo returns non-zero
    ok 7 build_remediation_plan: selects only DRIFTED entries (ALIGNED/MISSING excluded)
    ok 8 build_remediation_plan: each entry names repo, stub, stub_file, and canonical source
    ok 9 build_remediation_plan: an allowlisted repo never appears in the plan (AC #3)
    ok 10 build_remediation_plan: an allowlisted repo+stub_file excludes only that file (AC #3)
    ok 11 build_remediation_plan: an empty array yields an empty plan
    ok 12 build_remediation_plan: an absent file yields an empty plan
    ok 13 build_remediation_plan: malformed (non-array) input fails loud, not silently empty
    ok 14 module is sourced (not executed): pure helpers are available as functions
    ok 15 driver: DRY_RUN is the default — logs intent and makes ZERO write API calls (AC #2)
    ok 16 driver: --dry-run also forces dry-run even when DRY_RUN=false in env
    ok 17 driver: a live drifted repo runs branch → PUT → one PR (AC #1, #4)
    ok 18 driver: PR title/body cite the canonical source of record (AC #4)
    ok 19 driver: an already-open remediation PR is skipped — no writes (AC #3)
    ok 20 driver: two drifted stubs in one repo yield ONE branch, two PUTs, ONE PR (AC #1)
    ok 21 driver: a post-PUT blob SHA that != canonical SHA fails the repo and opens NO PR (AC #5)
    ok 22 driver: DRY_RUN=false with NO repo scope stays dry-run + warns (fail-closed, AC #7)
    

    shellcheck --severity=warning -x clean; .dev-lead/scripts/dev-lead-lint.sh → all checks passed. No regressions in fleet_stub_drift.bats / template_stub_drift.bats.

    Files Changed

    • scripts/fleet_stub_remediate.sh: added the co-located Phase-2 network driver (helpers + _remediate_repo + run_remediation + rewritten main()); pure plan-builder section untouched.
    • tests/fleet_stub_remediate.bats: added a gh-shim harness (setup/teardown) and 8 network-driver tests covering dry-run, live branch+PUT+PR, idempotent skip, one-PR-per-repo grouping, SHA byte-identity verify, and the fail-closed scope guard.

    Notes

    • Idempotency is implemented as: skip the repo entirely if an open remediation PR exists for the head branch; otherwise create-or-reuse the branch (never re-create). This mirrors seed-repo-template.sh's gh pr list --head check.
    • Post-PUT verification fails a repo (non-zero, no PR) if the written blob SHA != canonical SHA, or if the canonical bytes/SHA cannot be read — never opening a misleading PR (AC feat: add Copilot engine support via REVIEW_ENGINE toggle #5). Per-repo failure surfaces as an overall non-zero exit; the fleet-level verification summary remains Phase 3's scope.
    • Blast-radius bound (AC Workflow should skip no-op PRs in MAX_PRS count #7): live writes require an explicit --repo/REMEDIATE_REPOS scope; with none the driver stays dry-run and emits a ::warning::. The per-run REMEDIATE_MAX_REPOS ceiling and pilot scoping land in Phase 3.
    • tests/fleet_stub_remediate.bats is already registered in lint.yml's bats list (from Phase 1) — no workflow wiring change needed.
  3. added a commit that references this issue on Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    dev-leadFor dev-lead agent pickupinitiativeEpic / initiative tracking issue

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions