IMPL 0006: Reconciler Interface and Push Event Handler
Status: Completed Author: Donald Gifford Date: 2026-03-15
Objective
Implement the Reconciler interface, reconciler registry, and the
custom_properties reconciler (migrating existing logic from
internal/checker/properties.go). Add a push event handler that triggers
re-evaluation when watched files are modified on the default branch.
Implements: DESIGN-0007
Scope
In Scope
Reconcilerinterface withReconcileParamsRegistrywith factory pattern for reconciler type registrationcustom_propertiesreconciler migrating logic frominternal/checker/properties.gopushevent handler ininternal/webhook/handler.go- Watched file path extraction from
PolicyConfig TriggerPushtrigger type- Backward compatibility with
CUSTOM_PROPERTIES_MODEenv var - Integration with checker engine (reconcilers run after assertions pass)
Out of Scope
label_sync,branch_protection,workflow_syncreconcilers (IMPL-0007)- Ignore lists (IMPL-0007)
rule "setting"andrule "branch_protection"types (IMPL-0007)
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: Reconciler Interface and Registry
Define the core interface and registry that all reconciler types implement.
Tasks
- [x] Create
internal/reconciler/reconciler.gowith: Reconcilerinterface (Name() string,Reconcile(ctx, *ReconcileParams) error)ReconcileParamsstruct (Client, Owner, Repo, DefaultBranch, Content, OpenPRs, DryRun, Logger)- [x] Create
internal/reconciler/registry.gowith: Registrystruct withfactories map[string]FactoryFactorytype:func(config ReconcilerConfig) (Reconciler, error)NewRegistry() *RegistryRegister(name string, factory Factory)Build(config ReconcilerConfig) (Reconciler, error)- [x] Add
ReconcilerConfigtype tointernal/policy/types.gowith HCL struct tags (type, mode, watch, and type-specific fields) - [x] Write unit tests:
- Registry registers and builds reconcilers correctly
- Registry returns error for unknown reconciler type
ReconcileParamsconstruction- Interface compliance (compile-time check)
Success Criteria
Reconcilerinterface is defined and documented- Registry can register and build reconcilers
go build ./...succeedsmake lintandmake testpass
Phase 2: Custom Properties Reconciler
Migrate the existing custom properties logic from
internal/checker/properties.go into a custom_properties reconciler.
Tasks
- [x] Create
internal/reconciler/custom_properties.goimplementingReconciler: NewCustomPropertiesReconciler(config ReconcilerConfig) (Reconciler, error)Name() stringreturns"custom_properties"Reconcile(ctx, params)mirrors currentCheckCustomPropertiesflow- [x] Extract from
internal/checker/properties.go: - YAML parsing logic (catalog-info extraction)
- Custom property diff logic
- API mode: set properties directly, PR if file missing
- GHA mode: create PR with workflow
- [x] Use
CustomPropertiesConfigfields for YAML path mappings (owner, component, jira_project, jira_label) - [x] Support configurable defaults (owner, component default values)
- [x] Preserve all existing metrics:
properties_checked_totalproperties_set_totalproperties_already_correct_total- [x] Register
custom_propertiesfactory inNewRegistry() - [x] Write unit tests:
- Reconcile with valid catalog-info content: extracts correct values,
diffs against current, calls
SetCustomPropertyValues - Reconcile with missing fields: uses configured defaults
- Reconcile with unparseable content: uses defaults
- Reconcile in dry-run mode: logs but doesn't call API
- Reconcile in api mode: sets properties directly
- Reconcile in github-action mode: creates PR with GHA workflow
- Metrics are incremented correctly
Success Criteria
custom_propertiesreconciler produces identical behavior to currentCheckCustomProperties- All existing properties-related tests pass or are migrated
- Metrics are preserved
make testandmake lintpass
Phase 3: Checker Engine Integration
Wire reconcilers into the checker engine's CheckRepo flow so they run
after file assertions pass.
Tasks
- [x] Add
Reconcilers []Reconcilerfield to the engine's internal rule representation (built fromFileRuleConfig.Reconcilersvia Registry) - [x] Build reconcilers from
PolicyConfigat engine construction time using theRegistry - [x] Modify
CheckRepoflow: - After file existence + assertions pass → read file content once →
run each reconciler with
ReconcileParams existsmode: reconcilers run when file is presentcontainsmode: reconcilers run when assertions passexactmode: reconcilers run when file matches template- [x] Remove
checkCustomPropertiesIfEnabledfromCheckRepo(replaced by reconciler invocation in policy path; legacy path retained until Phase 7) - [x] Remove
customPropertiesModefield fromEnginestruct (removed from policy path; legacy field retained until Phase 7) - [x] Update
NewEnginesignature (no morecustomPropertiesModeparam) (policy engine uses registry; legacy NewEngine unchanged until Phase 7) - [x] Write unit tests:
- Reconciler runs when file present (exists mode)
- Reconciler runs when assertions pass (contains mode)
- Reconciler does not run when file missing
- Reconciler does not run when assertions fail
- Multiple reconcilers run in order
- Reconciler error is logged but doesn't fail the check
- DryRun flag propagated to ReconcileParams
Success Criteria
- Reconcilers run at the correct point in the CheckRepo flow
- Custom properties behavior is unchanged
checkCustomPropertiesIfEnabledis removedmake testandmake lintpass
Phase 4: Backward Compatibility Layer
Ensure CUSTOM_PROPERTIES_MODE env var continues to work when no HCL config
is present.
Tasks
- [x] In
internal/policy/defaults.go: whenCUSTOM_PROPERTIES_MODEis set and no HCL config is loaded, add acustom_propertiesreconciler to thecatalog_infofile rule in built-in defaults - [x] Map env var mode value to reconciler config fields:
mode = os.Getenv("CUSTOM_PROPERTIES_MODE")watch = false(no push handler in legacy mode)- Field mappings match current hardcoded values in
properties.go - [x] Write backward compatibility test:
- No HCL config +
CUSTOM_PROPERTIES_MODE=api→ engine behavior identical to current - No HCL config +
CUSTOM_PROPERTIES_MODE=""→ no reconciler attached - HCL config present →
CUSTOM_PROPERTIES_MODEenv var is ignored (HCL takes precedence)
Success Criteria
CUSTOM_PROPERTIES_MODE=apiwithout HCL produces identical behaviorCUSTOM_PROPERTIES_MODE=""disables custom properties (same as today)- HCL config takes precedence over env var
make testpasses
Phase 5: Push Event Handler
Add push event handling to the webhook handler that triggers re-evaluation
when watched files are modified on the default branch.
Tasks
- [x] Add
TriggerPushconstant tointernal/checker/queue.go - [x] Add
watchedPaths map[string]boolfield towebhook.Handler - [x] Update
webhook.NewHandlersignature to acceptwatchedPaths - [x] Add
case *gh.PushEvent:toServeHTTPswitch - [x] Implement
handlePushEvent: - Check
e.GetRef()matches"refs/heads/" + defaultBranch - Log tag pushes at debug level
- Call
hasWatchedFileChanges - If matched, enqueue with
TriggerPush - [x] Implement
hasWatchedFileChanges: - Check
AddedandModifiedpaths in all commits - Return true if any path is in
watchedPaths - Do NOT check
Removedpaths - [x] Update
main.goto passwatchedPathstoNewHandler - [x] Write unit tests:
- Push to default branch with watched file in
added: enqueues - Push to default branch with watched file in
modified: enqueues - Push to default branch with unrelated files: does not enqueue
- Push to non-default branch: does not enqueue
- Push with watched file in
removedonly: does not enqueue - Push with no watched paths configured: does not enqueue
- Multiple commits, watched file in later commit: enqueues
- Tag push: logged at debug, not enqueued
Success Criteria
- Push events for watched files on default branch trigger rescans
- Non-matching pushes are silently ignored
repos_checked_total{trigger="push"}metric works- No new GitHub API calls from push handler (payload only)
make testandmake lintpass
Phase 6: Watched Path Extraction
Build the utility that extracts watched file paths from the policy config.
Tasks
- [x] Create
internal/policy/watch.gowithExtractWatchedPaths(config *PolicyConfig) map[string]bool - [x] For each file rule with a reconciler where
watch = true, add all rule paths to the watched set - [x] Call
ExtractWatchedPathsinmain.goand pass result towebhook.NewHandler - [x] When no HCL config and no
watch = truereconcilers, return empty map (push events silently ignored) - [x] Write unit tests:
- Rule with
watch = truereconciler: all rule paths in set - Rule with
watch = falsereconciler: no paths in set - Rule with no reconciler: no paths in set
- Multiple rules with watch: union of all paths
- Empty config: empty map
Success Criteria
- Watched paths correctly extracted from config
- Empty map when no watched reconcilers
make testandmake lintpass
Phase 7: Cleanup and Integration Testing
Remove the old custom properties code path and verify end-to-end behavior.
Tasks
- [x] Remove
internal/checker/properties.go(logic migrated to reconciler) - [x] Remove
internal/checker/properties_test.go - [x] Remove
customPropertiesModefromconfig.Config - [x] Update any remaining references to the old properties code path
- [x] Write integration test: HCL config with
custom_propertiesreconciler → engine creation → mock GitHub client → expected API calls - [x] Write integration test: push event → handler → enqueue → engine → reconciler runs
- [x] Run
make ci(lint + test + build) - [x] Update
cmd/repo-guardian/main.goimports
Success Criteria
- No references to old
properties.gocode path make cipasses- Integration tests verify end-to-end reconciler flow
- Integration tests verify end-to-end push event flow
File Changes
| File | Action | Description |
|---|---|---|
internal/reconciler/reconciler.go |
Create | Interface and ReconcileParams |
internal/reconciler/registry.go |
Create | Factory registry |
internal/reconciler/custom_properties.go |
Create | Custom properties reconciler |
internal/reconciler/reconciler_test.go |
Create | Interface and registry tests |
internal/reconciler/custom_properties_test.go |
Create | Custom properties tests |
internal/policy/types.go |
Modify | Add ReconcilerConfig type |
internal/policy/defaults.go |
Modify | Add CUSTOM_PROPERTIES_MODE compat |
internal/policy/watch.go |
Create | Watched path extraction |
internal/policy/watch_test.go |
Create | Watched path tests |
internal/checker/engine.go |
Modify | Integrate reconcilers into CheckRepo |
internal/checker/engine_test.go |
Modify | Add reconciler integration tests |
internal/checker/properties.go |
Delete | Migrated to reconciler |
internal/checker/properties_test.go |
Delete | Migrated to reconciler |
internal/checker/queue.go |
Modify | Add TriggerPush constant |
internal/webhook/handler.go |
Modify | Add push event handler |
internal/webhook/handler_test.go |
Modify | Add push event tests |
internal/config/config.go |
Modify | Remove customPropertiesMode |
cmd/repo-guardian/main.go |
Modify | Wire reconcilers and watched paths |
Testing Plan
- [x] Unit tests for reconciler interface and registry
- [x] Unit tests for custom properties reconciler (mirrors existing tests)
- [x] Unit tests for push event handler (all scenarios)
- [x] Unit tests for watched path extraction
- [x] Integration test: config → engine → reconciler flow
- [x] Integration test: push event → enqueue → check flow
- [x] Backward compatibility test: CUSTOM_PROPERTIES_MODE without HCL
Dependencies
- IMPL-0005 (HCL Policy Configuration) — must be completed first
- DESIGN-0007 — design document (completed)
- Current custom properties code:
internal/checker/properties.go - Current webhook handler:
internal/webhook/handler.go
Resolved Questions
-
Reconciler error handling: Configurable per reconciler, defaulting to fail the entire
CheckRepocall. Reconcilers can opt into log-and-continue behavior via config. Fail loud by default. -
Push event handler testing: Use both approaches — real JSON fixtures from GitHub's webhook docs for realistic payload parsing, and Go structs constructed directly for targeted edge cases.
-
Reconciler content reading: Add a new
client.GetFileContentmethod that returns the file content as a string. Leave the existingclient.GetContents(existence check only) unchanged.
References
- DESIGN-0007: Reconciler Interface and Push Event Handler
- RFC-0002: HCL-driven Policy Engine
- Current custom properties:
internal/checker/properties.go - Current webhook handler:
internal/webhook/handler.go - Current queue:
internal/checker/queue.go