Skip to content

[Phase 2] Make caller_stub_freeze and validate-caller-inputs ingress-aware (job-scoped freeze + if:-as-event-filter check) #1725

Description

@github-actions

Story

As a CI guard maintainer,
I want teach the two in-repo lint.yml guards to validate a multi-job agent-ingress.yml -- job-scoped forwarding-block extraction in caller_stub_freeze, multi-pin-per-file resolution plus an if:-as-event-filter-only assertion in validate-caller-inputs,
so that this repo can collapse its stubs without the ring-0 freeze hard-failing, and the one place ADR-0001 is loosened stays statically checkable.

Acceptance Criteria

  1. caller_stub_freeze.sh's extract_forwarding_block (today a single-on:/single-uses: awk state machine) is replaced by a JOB-SCOPED extractor that selects a named job and captures that job's uses:/with:/permissions: block; CALLER_FREEZE_STUBS and the *.block fixtures move to per-role/per-job granularity; MISSING stays a HARD error for ring-0 roles.
  2. validate-caller-inputs.sh validates each job's forwarded inputs as a subset of its own pinned ref's declared workflow_call.inputs, for MULTIPLE distinct per-job pins within one agent-ingress.yml (the vci_with_keys_for_job indentation contract is preserved).
  3. validate-caller-inputs (or a sibling check wired into the same lint job) enforces the if:-as-event-filter-only boundary: an ingress job's if: may reference only github.event_name/github.event.action/payload predicates and FAILS if it reaches for repo state.
  4. Both guards remain backward-compatible: they still pass on the pre-collapse per-role stub files, so this story lands BEFORE the collapse without breaking current CI.
  5. bats coverage: caller_stub_freeze.bats and a validate-caller-inputs test exercise a multi-job ingress fixture (multi-pin passes; a repo-state if: fails; a pure event-filter if: passes; a drifted single job is caught).

Tasks / Subtasks

Dev Notes

  • caller_stub_freeze.sh: CALLER_FREEZE_STUBS (L46-50) currently holds dev-lead.yml|dev-lead.block, pr-review-trigger.yml|pr-review-trigger.block, ci-failure-analyst.lock.yml. extract_forwarding_block() (L73-88) is an awk state machine that assumes ONE top-level ^on: and the FIRST job-indented uses: up to the first secrets:/permissions: -- it collapses on a multi-job file (one shared on:, many uses:). MISSING is a HARD fail for ring-0 (L125-129). Baselines live in tests/fixtures/caller-stub-freeze/*.block; --update (L165-179) regenerates them. This is the guard that will hard-fail the moment dev-lead.yml/pr-review-trigger.yml become jobs, so it MUST land before Story 6.
  • validate-caller-inputs.sh is the ONE tool already generic: it globs .github/workflows/*.yml (L288-289), parses each uses: per line (vci_parse_uses L48-74), collects with: keys for the specific job by indentation (vci_with_keys_for_job L79-124; job_indent from the uses: line's leading whitespace L87-88), and resolves each uses: at its OWN ref (vci_resolve_reusable L247-278). So multiple distinct per-job pins in one file work IN PRINCIPLE -- the risk to protect is the indentation contract; the NEW work here is the if:-filter assertion, not the input-subset check.
  • The [#1052 A] CI guard: forwarded caller inputs ⊆ declared inputs at the pinned @ref (+ #1034 regression) #1253 channel-skew guard (forwarded caller inputs subset of declared at the pinned @ref) is exactly validate-caller-inputs; ADR-0007 asks it to be taught about 'one file forwarding to several pinned refs' -- confirm per-job resolution rather than assume the whole file shares one ref (guards against the feat: implement issue #1031 — fix(#844): plumb LSP_PILOT_ENABLED env + decouple capture from LSP wiring so the pilot A/B runs in CI #1034/fix(ci): prevent channel-skew workflow breakage — guard forwarded inputs vs pinned refs + codify the pinning rules (post-#1034) #1052 channel-skew defect recurring per-surface).
  • Both guards are invoked from lint.yml: job caller-stub-freeze (L252-272) runs bash scripts/caller_stub_freeze.sh; job validate-caller-inputs (L228-250) runs bash scripts/validate-caller-inputs.sh and MUST keep persist-credentials true (L242-247) for same-repo channel-tag resolution.
  • Testing standard (ADR-0004 pure-logic + bats): keep the extraction/validation pure and cover it in tests/caller_stub_freeze.bats and a validate-caller-inputs bats fixture; assert both the collapsed and the legacy per-role shapes.

Project Structure Notes

Edits two scripts + their bats + fixtures; no reusable or workflow-trigger change. The if:-filter check may live in validate-caller-inputs.sh or a small sibling wired into the same lint job -- keep it pure-logic + bats per ADR-0004.

References

Likely target surface

  • scripts/caller_stub_freeze.sh
  • scripts/validate-caller-inputs.sh
  • tests/caller_stub_freeze.bats
  • tests/fixtures/caller-stub-freeze/
  • tests/test_validate_caller_inputs.bats (new)

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


QA Lead test-risk inputs (folded in 2026-09-08 — these are ACs, not commentary)

  1. Backward-compatibility must be proven by failure, not by passing. AC4 as originally written ("still passes on the pre-collapse per-role stubs") is satisfiable by a guard that silently no-ops on the ingress form — passing is not evidence of checking. Each guard must additionally FAIL on a known-bad ingress fixture, and the test must assert the failure MESSAGE, not merely a non-zero exit code, so a right-answer-for-the-wrong-reason cannot pass.
  2. A malformed ingress must be caught by CI before merge. ADR-0007 names total per-repo blast radius as the cost it is least comfortable with, and no story currently tests it: a syntactically invalid or schema-invalid agent-ingress.yml must fail the lint job. The ADR's own worst-stated failure mode must not be the one with no test.
  3. One parameterised fixture table, not two suites. Legacy-form and ingress-form assertions must be driven by a single table of (form, input, expected) so a newly added case must be answered for BOTH forms or fail to run. Two independent suites will drift apart under delivery pressure; this is cheaper now than reconciling them later.

Activity

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