IMPL 0007: Additional Rule Types and Ignore Lists
Status: Completed Author: Donald Gifford Date: 2026-03-15
Objective
Implement global and per-rule ignore lists with glob pattern matching,
rule "setting" and rule "branch_protection" types, and the label_sync,
branch_protection, and workflow_sync reconciler implementations.
Implements: DESIGN-0008
Scope
In Scope
- Global ignore lists (skip repos for all rules)
- Per-rule ignore lists (skip specific rules for specific repos)
- Glob pattern matching via
path.Match rule "setting"type for repo settingsrule "branch_protection"type for branch protection configurationlabel_syncreconcilerbranch_protectionreconcilerworkflow_syncreconciler- New GitHub client interface methods
- New Prometheus metrics
Out of Scope
- Multi-org support
- Custom rule type plugins
- Ignore lists based on repo metadata (topics, visibility, team)
- Hot-reloadable config
Implementation Phases
Each phase builds on the previous one. A phase is complete when all its tasks are checked off and its success criteria are met.
Phase 1: Ignore Lists
Implement global and per-rule ignore lists with glob pattern matching. This is the lowest-risk addition and immediately useful.
Tasks
- [x] Populate
IgnoreConfigstruct ininternal/policy/types.go(currently a placeholder from IMPL-0005): Repos []stringfield with HCL struct tags- [x] Create
internal/policy/ignore.gowith: (ic *IgnoreConfig) Matches(ownerRepo string) bool- Use
path.Matchfor glob pattern matching - Input is always
owner/repoformat - [x] Add
ignore {}block support to HCL parser for: - Top-level global ignore (in
PolicyConfig.IgnoreList) - Per-rule ignore (in
FileRuleConfig.Ignore) - [x] Integrate into checker engine
CheckRepo: - Check global ignore list once per repo (before any rules)
- Check per-rule ignore list before each rule evaluation
- Increment
repo_guardian_ignored_total{scope="global"}or{scope="rule"}metric - [x] Add
repo_guardian_ignored_totalcounter metric withscopelabel - [x] Write unit tests:
- Exact match:
myorg/my-repomatchesmyorg/my-repo - Glob wildcard:
myorg/terraform-vpcmatchesmyorg/terraform-* - No match:
myorg/my-repodoesn't matchmyorg/other-* - Character set:
myorg/repo-[abc]matches correctly - Global ignore skips all rules
- Per-rule ignore skips only that rule
- Empty ignore list: no repos skipped
- Case sensitivity: matching is case-sensitive
Success Criteria
- Global ignore list prevents all rules from running on matched repos
- Per-rule ignore list prevents only that rule on matched repos
- Glob patterns work with
*,?,[abc]syntax - Ignored repos are counted in metrics
make testandmake lintpass
Phase 2: GitHub Client Extensions
Add new methods to the GitHub client interface needed by setting rules, branch protection rules, and reconcilers.
Tasks
- [x] Add to
github.Clientinterface: GetVulnerabilityAlertsEnabled(ctx, owner, repo) (bool, error)EnableVulnerabilityAlerts(ctx, owner, repo) errorDisableVulnerabilityAlerts(ctx, owner, repo) errorUpdateRepository(ctx, owner, repo, opts) errorGetFileContent(ctx, owner, repo, path) (string, error)- [x] Add to
github.Clientinterface (rulesets): ListRepositoryRulesets(ctx, owner, repo) ([]*Ruleset, error)GetRepositoryRuleset(ctx, owner, repo, rulesetID) (*Ruleset, error)CreateRepositoryRuleset(ctx, owner, repo, ruleset) (*Ruleset, error)UpdateRepositoryRuleset(ctx, owner, repo, rulesetID, ruleset) (*Ruleset, error)- [x] Add to
github.Clientinterface (labels): ListLabels(ctx, owner, repo) ([]*Label, error)CreateLabel(ctx, owner, repo, label) errorUpdateLabel(ctx, owner, repo, name, label) errorDeleteLabel(ctx, owner, repo, name) error- [x] Define
RulesetandLabeltypes ininternal/github/types.go - [x] Implement all methods in
internal/github/client.go - [x] Update mock client in tests to implement new interface methods
- [x] Write unit tests for each new method using
httptest.Server
Success Criteria
- All new methods compile and implement the interface
- Mock client updated for tests
make testandmake lintpass
Phase 3: Setting Rules
Implement rule "setting" for checking and optionally remediating GitHub
repository settings.
Tasks
- [x] Add
SettingRuleConfigtointernal/policy/types.go: - Name, Enabled, Property, Expected (interface{}), Remediate, Ignore
- HCL struct tags for the
rule "setting"block - [x] Add
SettingRules []SettingRuleConfigtoPolicyConfig - [x] Update HCL parser to handle
rule "setting" "<name>" {}blocks - [x] Update validation to check:
propertyis one of the supported propertiesexpectedtype matches property type (bool or string)- [x] Implement setting rule evaluation in checker engine:
- Read current value from GitHub API
- Compare against expected
- Match → log, increment
settings_checked_total - Mismatch +
remediate=false→ log, incrementsettings_mismatched_total - Mismatch +
remediate=true+ not dry_run → set via API, incrementsettings_remediated_total - Mismatch +
remediate=true+ dry_run → log "would remediate" - [x] Add supported properties:
vulnerability_alerts_enabled(bool)default_branch(string)has_issues(bool)has_wiki(bool)delete_branch_on_merge(bool)allow_merge_commit(bool)allow_squash_merge(bool)allow_rebase_merge(bool)- [x] Add Prometheus metrics:
repo_guardian_settings_checked_total{rule_name}repo_guardian_settings_remediated_total{rule_name}repo_guardian_settings_mismatched_total{rule_name}- [x] Write unit tests:
- Setting matches expected: no action
- Setting mismatches, remediate=false: logs mismatch
- Setting mismatches, remediate=true: calls API to fix
- Setting mismatches, remediate=true, dry_run: logs "would remediate"
- Unsupported property: validation error at load time
- Each supported property type works correctly
Success Criteria
- Setting rules check and optionally remediate repo settings
- All 8 supported properties work
- Metrics track checks, mismatches, and remediations
- Validation catches unsupported properties
make testandmake lintpass
Phase 4: Branch Protection Rules
Implement rule "branch_protection" for checking and optionally enforcing
branch protection settings using the repository rulesets API.
Tasks
- [x] Add
BranchProtectionRuleConfigtointernal/policy/types.go: - Name, Enabled, Branch, RequirePR, RequiredApprovals, DismissStaleReviews, RequireStatusChecks, EnforceAdmins, RequireLinearHistory, Ignore, Reconcilers
- HCL struct tags for
rule "branch_protection"block - [x] Add
BranchProtectionRules []BranchProtectionRuleConfigtoPolicyConfig - [x] Update HCL parser to handle
rule "branch_protection" "<name>" {}blocks - [x] Implement branch protection rule evaluation:
- Read current protection via rulesets API
- Compare each field against expected
- All match → log, increment
branch_protection_checked_total - Mismatch → remediate flow (same pattern as setting rules)
- [x] Map
BranchProtectionRuleConfigfields to ruleset API request format - [x] Handle "branch doesn't exist" case: log warning, no error
- [x] Add Prometheus metrics:
repo_guardian_branch_protection_checked_total{rule_name}repo_guardian_branch_protection_remediated_total{rule_name}- [x] Write unit tests:
- Protection matches: no action
- Protection mismatches: logs differences
- Protection with remediation: updates via API
- Branch doesn't exist: logs warning
- Multiple protection fields checked
- Dry run mode
Success Criteria
- Branch protection rules check settings via rulesets API
- Remediation creates/updates rulesets
- Missing branches handled gracefully
make testandmake lintpass
Phase 5: Label Sync Reconciler
Implement the label_sync reconciler that syncs repository labels from a
YAML configuration file.
Tasks
- [x] Create
internal/reconciler/label_sync.goimplementingReconciler: NewLabelSyncReconciler(config ReconcilerConfig) (Reconciler, error)Name()returns"label_sync"Reconcile(ctx, params)implements the sync flow- [x] Define label file YAML schema:
labels: [{name, color, description, renamed_from}]- [x] Implement reconcile flow:
- Parse file content as YAML label list
- List current repo labels via GitHub API
- Process renames first (
renamed_fromwhere old name exists) - Diff desired vs current
- Create missing labels
- Update changed labels (color or description mismatch)
- If
delete_extra = true, delete labels not in the file - [x] Add
LabelSyncConfigfields toReconcilerConfig: DeleteExtra bool- [x] Register
label_syncfactory inNewRegistry() - [x] Write unit tests:
- Creates missing labels
- Updates labels with changed color/description
- Renames labels via
renamed_from - Deletes extra labels when
delete_extra = true - Keeps extra labels when
delete_extra = false - Handles empty label file
- Handles invalid YAML
- Dry run mode: logs changes without making them
Success Criteria
- Label sync creates, updates, renames, and optionally deletes labels
renamed_frompreserves issue/PR associationsmake testandmake lintpass
Phase 6: Branch Protection Reconciler
Implement the branch_protection reconciler that reads protection settings
from a YAML file and applies them via the rulesets API.
Tasks
- [x] Create
internal/reconciler/branch_protection.goimplementingReconciler: NewBranchProtectionReconciler(config ReconcilerConfig) (Reconciler, error)Name()returns"branch_protection"Reconcile(ctx, params)reads YAML file and applies settings- [x] Define branch protection YAML schema (mirrors
BranchProtectionRuleConfigfields) - [x] Implement reconcile flow:
- Parse file content as YAML
- Read current rulesets via API
- Diff desired vs current
- Create or update rulesets as needed
- [x] Register
branch_protectionfactory inNewRegistry() - [x] Write unit tests:
- Valid YAML → correct rulesets created
- Existing rulesets updated when settings change
- No changes when settings match
- Invalid YAML produces clear error
- Dry run mode
Success Criteria
- Branch protection reconciler reads YAML and applies via rulesets API
make testandmake lintpass
Phase 7: Workflow Sync Reconciler
Implement the workflow_sync reconciler that detects workflow file changes
and triggers rescans.
Tasks
- [x] Create
internal/reconciler/workflow_sync.goimplementingReconciler: NewWorkflowSyncReconciler(config ReconcilerConfig) (Reconciler, error)Name()returns"workflow_sync"Reconcile(ctx, params)detects file mismatch- [x] The reconciler is primarily useful for the
watchcapability (push event → rescan → template comparison viaexactcheck mode) - [x] Register
workflow_syncfactory inNewRegistry() - [x] Write unit tests:
- Reconciler runs without error when file matches template
- Reconciler logs when file differs from template
- Dry run mode
Success Criteria
- Workflow sync reconciler integrates with
watch+exactmode make testandmake lintpass
Phase 8: Integration and Cleanup
End-to-end testing and documentation updates.
Tasks
- [x] Write integration test: HCL config with ignore lists + setting rules
- branch protection → engine → mock GitHub client → correct API calls
- [x] Write integration test: label_sync reconciler end-to-end with mock GitHub client
- [x] Verify all new metrics appear in
/metricsendpoint - [x] Update
CLAUDE.mdarchitecture section if needed - [x] Run
make ci(lint + test + build) - [x] Verify backward compatibility: no HCL config → identical behavior
Success Criteria
make cipasses- All new features work end-to-end
- Backward compatibility preserved
- Metrics observable
File Changes
| File | Action | Description |
|---|---|---|
internal/policy/types.go |
Modify | Add IgnoreConfig, SettingRuleConfig, BranchProtectionRuleConfig |
internal/policy/ignore.go |
Create | Ignore list matching with glob |
internal/policy/ignore_test.go |
Create | Ignore list tests |
internal/policy/loader.go |
Modify | Parse setting/branch_protection rule blocks |
internal/policy/validate.go |
Modify | Validate new rule types |
internal/github/client.go |
Modify | Add new API methods |
internal/github/types.go |
Modify | Add Ruleset, Label types |
internal/checker/engine.go |
Modify | Integrate ignore lists, setting rules, branch protection rules |
internal/metrics/metrics.go |
Modify | Add new counters |
internal/reconciler/label_sync.go |
Create | Label sync reconciler |
internal/reconciler/branch_protection.go |
Create | Branch protection reconciler |
internal/reconciler/workflow_sync.go |
Create | Workflow sync reconciler |
internal/reconciler/label_sync_test.go |
Create | Label sync tests |
internal/reconciler/branch_protection_test.go |
Create | Branch protection tests |
internal/reconciler/workflow_sync_test.go |
Create | Workflow sync tests |
Testing Plan
- [x] Unit tests for ignore list matching (exact, glob, edge cases)
- [x] Unit tests for all new GitHub client methods
- [x] Unit tests for setting rule evaluation (all supported properties)
- [x] Unit tests for branch protection rule evaluation
- [x] Unit tests for label sync reconciler (create, update, rename, delete)
- [x] Unit tests for branch protection reconciler
- [x] Unit tests for workflow sync reconciler
- [x] Integration tests for ignore lists in CheckRepo flow
- [x] Integration tests for end-to-end reconciler flows
Dependencies
- IMPL-0005 (HCL Policy Configuration) — must be completed first
- IMPL-0006 (Reconciler Interface) — must be completed first
- DESIGN-0008 — design document (completed)
- GitHub rulesets API documentation
Resolved Questions
-
Ruleset API vs legacy branch protection API: Rulesets only. GitHub's recommended path forward. If an org doesn't have access, that's a problem for later.
-
Label sync conflict resolution: Skip the rename and log a warning. No destructive action on conflicts — if both the source and target label names exist, the rename is silently skipped with a warning log.
-
Setting rule property extensibility: Curated list with type checking. Properties are validated against known types at config load time, similar to Terraform variable validation blocks. Keeps things safe and typed.
-
Ignore list case sensitivity: Normalize to lowercase before matching. GitHub repo names are case-insensitive, so
path.Matchinput is lowercased to match that behavior.
References
- DESIGN-0008: Additional Rule Types and Ignore Lists
- RFC-0002: HCL-driven Policy Engine
- GitHub Repository Rulesets API
- GitHub Labels API
- Current GitHub client:
internal/github/client.go - Current metrics:
internal/metrics/metrics.go