Skip to content

ci: add machine-readable QA contract + drift validator#3

Merged
Haserjian merged 1 commit intomainfrom
codex/qa-contract-drift
Mar 3, 2026
Merged

ci: add machine-readable QA contract + drift validator#3
Haserjian merged 1 commit intomainfrom
codex/qa-contract-drift

Conversation

@Haserjian
Copy link
Owner

@Haserjian Haserjian commented Mar 3, 2026

Summary

  • add machine-readable QA contract at '.github/qa_contract.yaml'
  • add validator script 'scripts/ci/validate_qa_contract.py'
  • add CI workflow '.github/workflows/qa-contract-drift.yml' to enforce contract drift checks
  • add 'QA_CONTRACT.md' as operator-facing contract doc

Why

This establishes a single, auditable CI contract surface and prevents accidental workflow drift.

Validation

  • python3 -m py_compile scripts/ci/validate_qa_contract.py
  • python3 scripts/ci/validate_qa_contract.py --contract .github/qa_contract.yaml

Copilot AI review requested due to automatic review settings March 3, 2026 08:31
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces a machine-readable “QA contract” for GitHub Actions CI, plus a validator and a dedicated workflow to detect drift between the contract and required workflows.

Changes:

  • Add .github/qa_contract.yaml defining required workflows/jobs/steps/matrix and an action pinning policy.
  • Add scripts/ci/validate_qa_contract.py to validate workflows against the contract.
  • Add .github/workflows/qa-contract-drift.yml and QA_CONTRACT.md to enforce and document the contract.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
scripts/ci/validate_qa_contract.py Implements YAML loading and validation of workflow invariants and uses: policy.
QA_CONTRACT.md Operator-facing documentation pointing to the contract and validator workflow.
.github/workflows/qa-contract-drift.yml CI job that installs PyYAML and runs the validator on PRs and main pushes.
.github/qa_contract.yaml Contract definition for required workflows/jobs/steps/matrix and required action references.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +13 to +15
def _load_yaml(path: Path) -> dict[str, Any]:
data = yaml.safe_load(path.read_text(encoding="utf-8"))
return data if isinstance(data, dict) else {}
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_load_yaml() will raise a stack trace on invalid YAML (PyYAML exceptions) and also silently treats any non-mapping YAML root as {}. For a drift validator, it’s more useful to catch yaml.YAMLError (and similar) and return a clear validation error, and to explicitly fail when the YAML root is not a mapping so contract/workflow corruption is surfaced clearly.

Copilot uses AI. Check for mistakes.
Comment on lines +118 to +120
invariants = contract.get("invariants", {})
policy = contract.get("policy", {})

Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

invariants = contract.get("invariants", {}) and policy = contract.get("policy", {}) assume those fields are mappings. If someone edits the contract and accidentally makes either a list/string, the validator will crash with AttributeError later (e.g., invariants.get(...)). Consider type-checking/coercing these to dicts (or emitting a specific error) to keep failures machine-readable.

Suggested change
invariants = contract.get("invariants", {})
policy = contract.get("policy", {})
raw_invariants = contract.get("invariants")
if raw_invariants is None:
invariants: dict[str, Any] = {}
elif isinstance(raw_invariants, dict):
invariants = raw_invariants
else:
errors.append("contract field 'invariants' must be a mapping")
invariants = {}
raw_policy = contract.get("policy")
if raw_policy is None:
policy: dict[str, Any] = {}
elif isinstance(raw_policy, dict):
policy = raw_policy
else:
errors.append("contract field 'policy' must be a mapping")
policy = {}

Copilot uses AI. Check for mistakes.
Comment on lines +121 to +122
all_uses: set[str] = set()
for wf_contract in invariants.get("required_workflows", []):
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for wf_contract in invariants.get("required_workflows", []) assumes required_workflows is a list of dicts. If it’s accidentally a dict (or contains non-dicts), the validator will crash when calling .get(...) on a non-mapping. Recommend validating the type of required_workflows up front (and each entry) and reporting a contract schema error instead of raising.

Suggested change
all_uses: set[str] = set()
for wf_contract in invariants.get("required_workflows", []):
if not isinstance(invariants, dict):
errors.append("contract 'invariants' section must be a mapping")
invariants = {}
all_uses: set[str] = set()
required_workflows = invariants.get("required_workflows", [])
if not isinstance(required_workflows, list):
errors.append("contract invariants.required_workflows must be a list")
required_workflows = []
for wf_contract in required_workflows:
if not isinstance(wf_contract, dict):
errors.append(
"contract has non-mapping entry in invariants.required_workflows"
)
continue

Copilot uses AI. Check for mistakes.
pull_request:
push:
branches: [main]

Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workflow doesn’t declare explicit permissions, so it will run with the repository’s default GITHUB_TOKEN permissions (which may be broader than necessary). Since this job only needs to read the repo contents to validate YAML, consider adding least-privilege permissions (e.g., contents: read) at the workflow or job level.

Suggested change
permissions:
contents: read

Copilot uses AI. Check for mistakes.
@Haserjian Haserjian merged commit 38c811c into main Mar 3, 2026
8 checks passed
@Haserjian Haserjian deleted the codex/qa-contract-drift branch March 3, 2026 09:16
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.

2 participants