fix(deployment): untangle execution + domain layers, fix strategy bug - #281
Merged
Conversation
Deployment carried three separate, unreconciled attempts at "dispatch
a Provider from an Intent" plus two unreconciled domain result/error
models. Audited every file (full diffs or repo-wide grep confirming
zero external references) before deleting anything — nothing here
had existing test coverage to break.
Real bug fixed: K8SProvider.Execute (pkg/apis/deployment/api/kubernetes.go)
had a parameter named `intent` shadowing the `intent` package, so
`switch intent.Strategy { case intent.Strategy: ... }` compared the
value to itself — the first case always matched and executeBlueGreen
was unreachable. Every Kubernetes deployment silently executed as
Rolling regardless of configured Strategy. Renamed the parameter to
dIntent and fixed the case labels to reference the real package
constants; added a table-driven test proving the two branches are
now actually distinguishable.
Deleted as confirmed dead code:
- pkg/apis/deployment/api/application/ (5 files) — the oldest
BackendSelector.ForIntent design: panics instead of returning
errors, no GitOps awareness beyond a hardcoded Flux special-case,
and its intent_builder/mapper/status siblings were missing
ManifestsRepo/ReconciliationStrategy wiring, a validation check,
and RetryOnConflict that the surviving application/ package has.
- pkg/apis/deployment/application/backend_selector.go +
gitops_decorator.go — a second, later redesign (ProviderRegistry +
Provider.Supports() + GitOpsDecorator) that was never wired to
DeploymentService, and whose one piece of real logic
(GitOpsDecorator.Execute) was a single comment with no actual
implementation — while the GitOps path actually in use
(KustomizeStrategyProvider.ReconcileKustomization) is fully real.
- pkg/apis/deployment/domain/result.go, domain/errors.go, and the
Result-consuming half of domain/state.go (DeploymentState,
ServiceUnitState, StateFromResult, ServiceUnitStateFromResult) — a
third, richer domain model with zero references anywhere outside
these three files; the execution path uses model.go's simpler
DeploymentResult/DeploymentPhase/ServiceUnitPhase instead.
Also promotes Intent to match the per-CR floor layout every other
piece of this codebase uses (pkg/apis/packages/intent/ already does
this): moved pkg/intent/deployment/ (9 files) to
pkg/apis/deployment/intent/, renaming `package deployment` to
`package intent` to match — this also drops the explicit `intent`
import alias every caller previously needed to avoid colliding with
the package's old name.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Amends this branch's earlier move — pkg/apis/deployment/intent/ was still nested under deployment's own tree, which doesn't match the actual target: Intent as a real 4th architectural pillar, sibling to resolution/, cache/, and core/, not tucked inside pkg/apis/. Moved intent/deployment/ to the repo root and renamed package intent back to package deployment (matching cache/deployment's convention: package name = directory's own name). Every caller re-adds the explicit `intent` import alias to keep every existing call site (intent.DeploymentIntent, intent.StrategyRolling, etc.) unchanged. pkg/apis/packages/intent/ (nested) is now the odd one out against this new top-level convention — flagged as a likely follow-up, not done here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reverses the previous top-level intent/ placement — pkg/apis/ is where all the per-CR domain code already lives, so Intent belongs alongside it under pkg/, not out at the repo root next to resolution/cache/core. - pkg/intent/deployment/ — back to its original location (this is where it started before any of today's moves), package deployment kept as-is from the last relocation. Every caller keeps its explicit `intent` import alias. - pkg/apis/packages/intent/ -> pkg/intent/package/ — the other CR with its own intent subpackage, now matching the same pkg/intent/ convention. Kept `package intent` (directory is named "package", which can't be a Go package name — it's a reserved word), so the 5 existing importers already resolve to the `intent` identifier with no alias needed, same as before the move. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Neo: "serviceunit must cater for its own resolution... I have the
intent resolving serviceunit, not correct, it must resolve itself."
Same principle as the earlier resolution/domain/application work for
ServiceUnit, now extended to the intent layer: pkg/intent/deployment
owned ServiceUnitIntent, RouteIntent, and WorkloadIntent, and built
them via ResolveServiceUnitIntent — all logic that's actually about
ServiceUnit, not Deployment.
New pkg/intent/serviceunit/ (matching the pkg/intent/<cr> convention
already established for deployment and package):
- ServiceUnitIntent + ResolveServiceUnitIntent (moved from
pkg/intent/deployment/serviceunit.go)
- RouteIntent (moved from route.go) and WorkloadIntent (moved from
workload.go) — both were only ever used by ServiceUnitIntent, so
they move with it; zero circular-dependency risk confirmed before
moving.
- ErrBuildNotReady/ErrInvalidServiceUnit (moved from errors.go) —
the two ServiceUnit-specific errors; ErrServiceUnitNotFound and
ErrInvalidDeployment stay in deployment's errors.go since they're
genuinely Deployment's own.
pkg/intent/deployment.DeploymentIntent.ServiceUnits is now
[]serviceunitIntent.ServiceUnitIntent; every caller across
resolve.go, pkg/apis/deployment/application/intent_builder.go,
pkg/apis/deployment/render/builders/builder.go,
pkg/apis/deployment/api/{kubernetes,ecs}.go updated accordingly.
Also relocates IntentBuilder (pkg/apis/deployment/application ->
pkg/intent/deployment/intent_builder.go) per a follow-up request —
the intent-building logic belongs in the intent package itself, not
the application layer. Note for a later pass: this package now has
two overlapping ways to build a DeploymentIntent — ResolveDeploymentIntent
(resolve.go) and IntentBuilder.Build (intent_builder.go) — the latter
is more complete (sets Runtime/Strategy/ReconciliationStrategy/
ManifestsRepo, which the former doesn't), not reconciled here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
7 tasks
ntlaletsi70
added a commit
that referenced
this pull request
Jul 19, 2026
) * ci: move SLSA provenance to its own standalone workflow (#272) Reverts the release-assets.yml integration in favor of a dedicated slsa-provenance.yml matching the upstream generator's own build+ provenance shape. Scopes provenance to the one artifact this repo actually builds (the gomarkdoc docs bundle) rather than also attesting install/CRDs/CLI assets that are just re-bundled from environments-install and environments-cli's own releases — those repos should generate their own provenance for their own output. The build job rebuilds the docs bundle itself so the hash it feeds to the provenance generator is guaranteed to match what it built, rather than assuming byte-identical reproducibility against release-assets.yml's separate docs job. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> * fix(ci): match code-docs.yml GPG committer identity to the signing key (#275) Every push to main has been failing at "Import GPG key" with: Committer email "github-actions[bot]@users.noreply.github.com" does not match GPG private key email "actions@github.com" code-docs.yml was the only workflow using a different committer identity (github-actions[bot] / users.noreply.github.com) than the one the shared GPG_PRIVATE_KEY is actually issued for. release.yml and finalize-release.yml already use github-actions / actions@github.com and sign correctly — code-docs.yml now matches. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> * fix(ci): heal stale vendor-snapshot cache and simplify coverage summary (#276) * fix(ci): heal stale vendor-snapshot cache and simplify coverage summary Vendor-snapshot fix (validate, coverage in ci.yml; govulncheck in security.yml): go.sum only changes when a required module's version changes. A test newly importing an already-required module's not-yet-vendored subpackage (e.g. pkg/secrets/git/build/build_test.go importing sigs.k8s.io/controller-runtime/pkg/client/fake) leaves go.sum untouched, so the sha256(go.sum) cache tag never busts — the cached snapshot built before that import existed keeps getting restored as current forever. Confirmed via the failed develop CI run (#270, 29609870897): "Restore vendor from snapshot" succeeds, then go test -mod=vendor fails with "cannot find module providing package .../client/fake: import lookup disabled by -mod=vendor". Each of the three jobs now verifies the restored vendor with a plain go build -mod=vendor right after restoring, and if that fails, rebuilds vendor locally and pushes a corrected snapshot under the same tag + :latest before continuing — turning a silent, permanent staleness bug into a self-healing one-time rebuild. Coverage summary (ci.yml coverage job): go tool cover -func emits one row per function, not per package — hundreds of rows in a repo this size. Rewritten to aggregate coverage.out's statement counts by package directory instead, producing one row per package, sorted weakest-first, and collapsed into a <details> block so the step summary stays short by default. Also fixed the module-prefix strip, which referenced a stale github.com/BlanketOps/blanketops-environments/ path that doesn't match this module's actual github.com/blanketops/environments. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat(ci): generate a gocov-html coverage report alongside coverage.html Adds beauty-report.html (gocov convert + gocov-html) as a richer, styled coverage report next to the existing plain go tool cover -html output, uploaded in the same coverage-report-<sha> artifact. gocov's tagged releases and current master (last touched 2024-10-11) pin golang.org/x/tools@v0.13.0, which fails to compile under this repo's Go toolchain — confirmed directly (go install fails identically on both the latest tag and master; a plain golang.org/x/tools@v0.13.0 build fails the same way outside of gocov entirely, while @latest builds fine). This is stale upstream, not fixable by tracking a newer gocov version, so the only working path is building gocov from source with a forced newer x/tools via `go mod edit -replace` — verified end-to-end against this repo's real coverage.out before wiring it in. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(ci): staleness probe must include go vet, not just go build Caught live on the first real run of the previous commit: the probe used `go build -mod=vendor ./...`, but build doesn't type-check _test.go files — and the exact missing package this whole fix exists for (sigs.k8s.io/controller-runtime/pkg/client/fake) is only ever imported from a test file. Build reported success, the rebuild/heal steps got skipped as a no-op, and Vet (which does check test files) then failed on the same stale vendor right after — same bug, just moved one step later instead of actually being caught. All three probes (validate, coverage in ci.yml; govulncheck in security.yml) now run go vet -mod=vendor alongside go build before deciding whether to rebuild. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(ci): reset GOPROXY before the vendor-heal fallback's go mod vendor Live failure on the first real govulncheck run of this fix: the rebuild step's go mod vendor hit honnef.co/go/tools/cmd/staticcheck: unrecognized import path "honnef.co/go/tools": https fetch: ... TLS handshake timeout validate's equivalent run (same commit) succeeded, so this may have been one-off network flakiness rather than guaranteed — but the GOPROXY=direct these three jobs set is real and relevant either way: it forces every module, including vanity-domain ones like honnef.co/go/tools, through direct fetch instead of the module proxy. vendor-snapshot (ci.yml) — the job actually designed to run a full `go mod vendor` — never sets GOPROXY at all, relying on the default proxy-first behavior. The heal steps in validate/coverage/govulncheck now reset GOPROXY to that same default immediately before go mod vendor, matching the job that's actually proven to do this reliably. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> * fix(deployment): untangle execution + domain layers, fix strategy bug (#281) * fix(deployment): untangle execution + domain layers, fix strategy bug Deployment carried three separate, unreconciled attempts at "dispatch a Provider from an Intent" plus two unreconciled domain result/error models. Audited every file (full diffs or repo-wide grep confirming zero external references) before deleting anything — nothing here had existing test coverage to break. Real bug fixed: K8SProvider.Execute (pkg/apis/deployment/api/kubernetes.go) had a parameter named `intent` shadowing the `intent` package, so `switch intent.Strategy { case intent.Strategy: ... }` compared the value to itself — the first case always matched and executeBlueGreen was unreachable. Every Kubernetes deployment silently executed as Rolling regardless of configured Strategy. Renamed the parameter to dIntent and fixed the case labels to reference the real package constants; added a table-driven test proving the two branches are now actually distinguishable. Deleted as confirmed dead code: - pkg/apis/deployment/api/application/ (5 files) — the oldest BackendSelector.ForIntent design: panics instead of returning errors, no GitOps awareness beyond a hardcoded Flux special-case, and its intent_builder/mapper/status siblings were missing ManifestsRepo/ReconciliationStrategy wiring, a validation check, and RetryOnConflict that the surviving application/ package has. - pkg/apis/deployment/application/backend_selector.go + gitops_decorator.go — a second, later redesign (ProviderRegistry + Provider.Supports() + GitOpsDecorator) that was never wired to DeploymentService, and whose one piece of real logic (GitOpsDecorator.Execute) was a single comment with no actual implementation — while the GitOps path actually in use (KustomizeStrategyProvider.ReconcileKustomization) is fully real. - pkg/apis/deployment/domain/result.go, domain/errors.go, and the Result-consuming half of domain/state.go (DeploymentState, ServiceUnitState, StateFromResult, ServiceUnitStateFromResult) — a third, richer domain model with zero references anywhere outside these three files; the execution path uses model.go's simpler DeploymentResult/DeploymentPhase/ServiceUnitPhase instead. Also promotes Intent to match the per-CR floor layout every other piece of this codebase uses (pkg/apis/packages/intent/ already does this): moved pkg/intent/deployment/ (9 files) to pkg/apis/deployment/intent/, renaming `package deployment` to `package intent` to match — this also drops the explicit `intent` import alias every caller previously needed to avoid colliding with the package's old name. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(deployment): relocate intent to top-level intent/deployment/ Amends this branch's earlier move — pkg/apis/deployment/intent/ was still nested under deployment's own tree, which doesn't match the actual target: Intent as a real 4th architectural pillar, sibling to resolution/, cache/, and core/, not tucked inside pkg/apis/. Moved intent/deployment/ to the repo root and renamed package intent back to package deployment (matching cache/deployment's convention: package name = directory's own name). Every caller re-adds the explicit `intent` import alias to keep every existing call site (intent.DeploymentIntent, intent.StrategyRolling, etc.) unchanged. pkg/apis/packages/intent/ (nested) is now the odd one out against this new top-level convention — flagged as a likely follow-up, not done here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(deployment,packages): relocate intent under pkg/, not top-level Reverses the previous top-level intent/ placement — pkg/apis/ is where all the per-CR domain code already lives, so Intent belongs alongside it under pkg/, not out at the repo root next to resolution/cache/core. - pkg/intent/deployment/ — back to its original location (this is where it started before any of today's moves), package deployment kept as-is from the last relocation. Every caller keeps its explicit `intent` import alias. - pkg/apis/packages/intent/ -> pkg/intent/package/ — the other CR with its own intent subpackage, now matching the same pkg/intent/ convention. Kept `package intent` (directory is named "package", which can't be a Go package name — it's a reserved word), so the 5 existing importers already resolve to the `intent` identifier with no alias needed, same as before the move. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(serviceunit,deployment): give ServiceUnit its own intent layer Neo: "serviceunit must cater for its own resolution... I have the intent resolving serviceunit, not correct, it must resolve itself." Same principle as the earlier resolution/domain/application work for ServiceUnit, now extended to the intent layer: pkg/intent/deployment owned ServiceUnitIntent, RouteIntent, and WorkloadIntent, and built them via ResolveServiceUnitIntent — all logic that's actually about ServiceUnit, not Deployment. New pkg/intent/serviceunit/ (matching the pkg/intent/<cr> convention already established for deployment and package): - ServiceUnitIntent + ResolveServiceUnitIntent (moved from pkg/intent/deployment/serviceunit.go) - RouteIntent (moved from route.go) and WorkloadIntent (moved from workload.go) — both were only ever used by ServiceUnitIntent, so they move with it; zero circular-dependency risk confirmed before moving. - ErrBuildNotReady/ErrInvalidServiceUnit (moved from errors.go) — the two ServiceUnit-specific errors; ErrServiceUnitNotFound and ErrInvalidDeployment stay in deployment's errors.go since they're genuinely Deployment's own. pkg/intent/deployment.DeploymentIntent.ServiceUnits is now []serviceunitIntent.ServiceUnitIntent; every caller across resolve.go, pkg/apis/deployment/application/intent_builder.go, pkg/apis/deployment/render/builders/builder.go, pkg/apis/deployment/api/{kubernetes,ecs}.go updated accordingly. Also relocates IntentBuilder (pkg/apis/deployment/application -> pkg/intent/deployment/intent_builder.go) per a follow-up request — the intent-building logic belongs in the intent package itself, not the application layer. Note for a later pass: this package now has two overlapping ways to build a DeploymentIntent — ResolveDeploymentIntent (resolve.go) and IntentBuilder.Build (intent_builder.go) — the latter is more complete (sets Runtime/Strategy/ReconciliationStrategy/ ManifestsRepo, which the former doesn't), not reconciled here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> * test: add coverage for pkg/apis/serviceunit and the cache/* layer (#280) * test: add coverage for pkg/apis/serviceunit and the cache/* layer Both pkg/apis/serviceunit/{domain,application} and every cache/* per-CR package (build, deployment, domain, environment, githubevent, gitrepository, packages, route, serviceunit) plus the shared cache.ObjectCache/NewExternal primitives were at 0% coverage. Total repo coverage: 35.9% -> 48.3%. Adds cache/internal/testutil.FakeExternalCache — an in-memory, JSON-serializing core/cache.ExternalCache fake shared across cache/* tests, mirroring pkg/secrets/internal/testutil's existing pattern. Real bug found and fixed while writing the serviceunit cache tests: cache/serviceunit/serviceunit.go's PublishResolved switched on r.Spec.Type.String(), comparing against literal "static"/"build" — but the generated proto String() returns the full constant name (e.g. "SERVICE_UNIT_TYPE_STATIC"), so neither case ever matched and the image/buildRef field was silently never cached for any ServiceUnit. Fixed to switch on the enum value directly, matching the pattern already used in resolution/serviceunit/resolve.go. Not covered here: cache/adapter's Redis/Memcached backends need a live connection to test meaningfully (and per setup.go's own TODOs, neither is actually wired up yet — NewExternal always returns NoopExternalCache regardless of the configured backend). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat(serviceunit): wire ServiceUnitCache into Reconcile Closes the gap flagged after the cache/* coverage pass: every per-CR cache was fully built and tested but never called from any CR's application/Reconcile code. Wires ServiceUnit end to end as the first pattern to prove the integration point, before deciding whether to repeat it for the other 8 CRs. Reconcile now calls cache.PublishResolved after the status write, using the CR's own namespace/name/generation. Best-effort per the cache layer's existing contract (see PublishResolved's doc comment) — its error is discarded, never fails Reconcile. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> * refactor(deployment): split strategy/reconcile dispatch out of api api/ was carrying both infra (materializing k8s objects, GitOps commits) and dispatch (which reconciliation mode, which runtime/strategy) in one flat package. Split dispatch into two new packages: - reconcile/: ReconciliationExecutor, the Imperative-vs-GitOps axis. - strategy/: RuntimeProvider + K8SStrategy (the Rolling/BlueGreen switch pulled out of K8SProvider.Execute), plus the ECS/Knative placeholder reconcilers. Kubernetes, ECS, and Knative are deployment strategies in this domain, not a separate runtime layer, so they live together here. api/ keeps only the infra that actually materializes objects: K8SProvider's apply/teardown (ApplyServiceUnit now exported for strategy.K8SStrategy to call) and KustomizeStrategyProvider's GitOps commit path. Also persists the go-run-driver verification recipe used to confirm this and the earlier structural cleanup didn't regress the Deployment pipeline. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * test(deployment): cover the api/application/strategy/reconcile split Adds tests for the packages touched by the reconcile/strategy split: K8SProvider's apply/teardown infra and ProviderRegistry (api), DeploymentService.Reconcile end-to-end and StatusWriter (application), ReconciliationExecutor's imperative/GitOps dispatch (reconcile), RuntimeProvider + deriveDeploymentPhase (strategy), and the Deployment/ Service builders (render/builders) — all previously at 0%. pkg/apis/deployment/application/mapper.go is left untested: Mapper / MapResolvedToDomain have no callers anywhere (DeploymentService.Reconcile goes through IntentBuilder, not this Mapper), so it's dead code rather than a coverage gap — flagged separately for a cleanup follow-up. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * docs(deployment,intent): add package doc comments api, application, domain, reconcile, strategy (pkg/apis/deployment) and pkg/intent/{deployment,serviceunit,package} had no package-level doc comment at all — browsing the freshly regenerated docs/code output made this obvious, since these packages rendered with no overview text next to ones that do have it (e.g. pkg/apis/serviceunit). domain/model.go's existing "DOMAIN PRINCIPLES" block was mid-file, after the package clause, so godoc/gomarkdoc never picked it up as the package doc — folded into a proper package comment instead of leaving a duplicate. pkg/intent/deployment's comment also documents in one place what was only previously flagged in a private note: ResolveDeploymentIntent (resolve.go) is a superseded, partial constructor with no real callers — IntentBuilder.Build is the one DeploymentService actually uses. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
ntlaletsi70
added a commit
that referenced
this pull request
Jul 19, 2026
* ci: move SLSA provenance to its own standalone workflow (#272) Reverts the release-assets.yml integration in favor of a dedicated slsa-provenance.yml matching the upstream generator's own build+ provenance shape. Scopes provenance to the one artifact this repo actually builds (the gomarkdoc docs bundle) rather than also attesting install/CRDs/CLI assets that are just re-bundled from environments-install and environments-cli's own releases — those repos should generate their own provenance for their own output. The build job rebuilds the docs bundle itself so the hash it feeds to the provenance generator is guaranteed to match what it built, rather than assuming byte-identical reproducibility against release-assets.yml's separate docs job. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> * fix(ci): match code-docs.yml GPG committer identity to the signing key (#275) Every push to main has been failing at "Import GPG key" with: Committer email "github-actions[bot]@users.noreply.github.com" does not match GPG private key email "actions@github.com" code-docs.yml was the only workflow using a different committer identity (github-actions[bot] / users.noreply.github.com) than the one the shared GPG_PRIVATE_KEY is actually issued for. release.yml and finalize-release.yml already use github-actions / actions@github.com and sign correctly — code-docs.yml now matches. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> * fix(ci): heal stale vendor-snapshot cache and simplify coverage summary (#276) * fix(ci): heal stale vendor-snapshot cache and simplify coverage summary Vendor-snapshot fix (validate, coverage in ci.yml; govulncheck in security.yml): go.sum only changes when a required module's version changes. A test newly importing an already-required module's not-yet-vendored subpackage (e.g. pkg/secrets/git/build/build_test.go importing sigs.k8s.io/controller-runtime/pkg/client/fake) leaves go.sum untouched, so the sha256(go.sum) cache tag never busts — the cached snapshot built before that import existed keeps getting restored as current forever. Confirmed via the failed develop CI run (#270, 29609870897): "Restore vendor from snapshot" succeeds, then go test -mod=vendor fails with "cannot find module providing package .../client/fake: import lookup disabled by -mod=vendor". Each of the three jobs now verifies the restored vendor with a plain go build -mod=vendor right after restoring, and if that fails, rebuilds vendor locally and pushes a corrected snapshot under the same tag + :latest before continuing — turning a silent, permanent staleness bug into a self-healing one-time rebuild. Coverage summary (ci.yml coverage job): go tool cover -func emits one row per function, not per package — hundreds of rows in a repo this size. Rewritten to aggregate coverage.out's statement counts by package directory instead, producing one row per package, sorted weakest-first, and collapsed into a <details> block so the step summary stays short by default. Also fixed the module-prefix strip, which referenced a stale github.com/BlanketOps/blanketops-environments/ path that doesn't match this module's actual github.com/blanketops/environments. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat(ci): generate a gocov-html coverage report alongside coverage.html Adds beauty-report.html (gocov convert + gocov-html) as a richer, styled coverage report next to the existing plain go tool cover -html output, uploaded in the same coverage-report-<sha> artifact. gocov's tagged releases and current master (last touched 2024-10-11) pin golang.org/x/tools@v0.13.0, which fails to compile under this repo's Go toolchain — confirmed directly (go install fails identically on both the latest tag and master; a plain golang.org/x/tools@v0.13.0 build fails the same way outside of gocov entirely, while @latest builds fine). This is stale upstream, not fixable by tracking a newer gocov version, so the only working path is building gocov from source with a forced newer x/tools via `go mod edit -replace` — verified end-to-end against this repo's real coverage.out before wiring it in. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(ci): staleness probe must include go vet, not just go build Caught live on the first real run of the previous commit: the probe used `go build -mod=vendor ./...`, but build doesn't type-check _test.go files — and the exact missing package this whole fix exists for (sigs.k8s.io/controller-runtime/pkg/client/fake) is only ever imported from a test file. Build reported success, the rebuild/heal steps got skipped as a no-op, and Vet (which does check test files) then failed on the same stale vendor right after — same bug, just moved one step later instead of actually being caught. All three probes (validate, coverage in ci.yml; govulncheck in security.yml) now run go vet -mod=vendor alongside go build before deciding whether to rebuild. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(ci): reset GOPROXY before the vendor-heal fallback's go mod vendor Live failure on the first real govulncheck run of this fix: the rebuild step's go mod vendor hit honnef.co/go/tools/cmd/staticcheck: unrecognized import path "honnef.co/go/tools": https fetch: ... TLS handshake timeout validate's equivalent run (same commit) succeeded, so this may have been one-off network flakiness rather than guaranteed — but the GOPROXY=direct these three jobs set is real and relevant either way: it forces every module, including vanity-domain ones like honnef.co/go/tools, through direct fetch instead of the module proxy. vendor-snapshot (ci.yml) — the job actually designed to run a full `go mod vendor` — never sets GOPROXY at all, relying on the default proxy-first behavior. The heal steps in validate/coverage/govulncheck now reset GOPROXY to that same default immediately before go mod vendor, matching the job that's actually proven to do this reliably. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> * fix(deployment): untangle execution + domain layers, fix strategy bug (#281) * fix(deployment): untangle execution + domain layers, fix strategy bug Deployment carried three separate, unreconciled attempts at "dispatch a Provider from an Intent" plus two unreconciled domain result/error models. Audited every file (full diffs or repo-wide grep confirming zero external references) before deleting anything — nothing here had existing test coverage to break. Real bug fixed: K8SProvider.Execute (pkg/apis/deployment/api/kubernetes.go) had a parameter named `intent` shadowing the `intent` package, so `switch intent.Strategy { case intent.Strategy: ... }` compared the value to itself — the first case always matched and executeBlueGreen was unreachable. Every Kubernetes deployment silently executed as Rolling regardless of configured Strategy. Renamed the parameter to dIntent and fixed the case labels to reference the real package constants; added a table-driven test proving the two branches are now actually distinguishable. Deleted as confirmed dead code: - pkg/apis/deployment/api/application/ (5 files) — the oldest BackendSelector.ForIntent design: panics instead of returning errors, no GitOps awareness beyond a hardcoded Flux special-case, and its intent_builder/mapper/status siblings were missing ManifestsRepo/ReconciliationStrategy wiring, a validation check, and RetryOnConflict that the surviving application/ package has. - pkg/apis/deployment/application/backend_selector.go + gitops_decorator.go — a second, later redesign (ProviderRegistry + Provider.Supports() + GitOpsDecorator) that was never wired to DeploymentService, and whose one piece of real logic (GitOpsDecorator.Execute) was a single comment with no actual implementation — while the GitOps path actually in use (KustomizeStrategyProvider.ReconcileKustomization) is fully real. - pkg/apis/deployment/domain/result.go, domain/errors.go, and the Result-consuming half of domain/state.go (DeploymentState, ServiceUnitState, StateFromResult, ServiceUnitStateFromResult) — a third, richer domain model with zero references anywhere outside these three files; the execution path uses model.go's simpler DeploymentResult/DeploymentPhase/ServiceUnitPhase instead. Also promotes Intent to match the per-CR floor layout every other piece of this codebase uses (pkg/apis/packages/intent/ already does this): moved pkg/intent/deployment/ (9 files) to pkg/apis/deployment/intent/, renaming `package deployment` to `package intent` to match — this also drops the explicit `intent` import alias every caller previously needed to avoid colliding with the package's old name. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(deployment): relocate intent to top-level intent/deployment/ Amends this branch's earlier move — pkg/apis/deployment/intent/ was still nested under deployment's own tree, which doesn't match the actual target: Intent as a real 4th architectural pillar, sibling to resolution/, cache/, and core/, not tucked inside pkg/apis/. Moved intent/deployment/ to the repo root and renamed package intent back to package deployment (matching cache/deployment's convention: package name = directory's own name). Every caller re-adds the explicit `intent` import alias to keep every existing call site (intent.DeploymentIntent, intent.StrategyRolling, etc.) unchanged. pkg/apis/packages/intent/ (nested) is now the odd one out against this new top-level convention — flagged as a likely follow-up, not done here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(deployment,packages): relocate intent under pkg/, not top-level Reverses the previous top-level intent/ placement — pkg/apis/ is where all the per-CR domain code already lives, so Intent belongs alongside it under pkg/, not out at the repo root next to resolution/cache/core. - pkg/intent/deployment/ — back to its original location (this is where it started before any of today's moves), package deployment kept as-is from the last relocation. Every caller keeps its explicit `intent` import alias. - pkg/apis/packages/intent/ -> pkg/intent/package/ — the other CR with its own intent subpackage, now matching the same pkg/intent/ convention. Kept `package intent` (directory is named "package", which can't be a Go package name — it's a reserved word), so the 5 existing importers already resolve to the `intent` identifier with no alias needed, same as before the move. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(serviceunit,deployment): give ServiceUnit its own intent layer Neo: "serviceunit must cater for its own resolution... I have the intent resolving serviceunit, not correct, it must resolve itself." Same principle as the earlier resolution/domain/application work for ServiceUnit, now extended to the intent layer: pkg/intent/deployment owned ServiceUnitIntent, RouteIntent, and WorkloadIntent, and built them via ResolveServiceUnitIntent — all logic that's actually about ServiceUnit, not Deployment. New pkg/intent/serviceunit/ (matching the pkg/intent/<cr> convention already established for deployment and package): - ServiceUnitIntent + ResolveServiceUnitIntent (moved from pkg/intent/deployment/serviceunit.go) - RouteIntent (moved from route.go) and WorkloadIntent (moved from workload.go) — both were only ever used by ServiceUnitIntent, so they move with it; zero circular-dependency risk confirmed before moving. - ErrBuildNotReady/ErrInvalidServiceUnit (moved from errors.go) — the two ServiceUnit-specific errors; ErrServiceUnitNotFound and ErrInvalidDeployment stay in deployment's errors.go since they're genuinely Deployment's own. pkg/intent/deployment.DeploymentIntent.ServiceUnits is now []serviceunitIntent.ServiceUnitIntent; every caller across resolve.go, pkg/apis/deployment/application/intent_builder.go, pkg/apis/deployment/render/builders/builder.go, pkg/apis/deployment/api/{kubernetes,ecs}.go updated accordingly. Also relocates IntentBuilder (pkg/apis/deployment/application -> pkg/intent/deployment/intent_builder.go) per a follow-up request — the intent-building logic belongs in the intent package itself, not the application layer. Note for a later pass: this package now has two overlapping ways to build a DeploymentIntent — ResolveDeploymentIntent (resolve.go) and IntentBuilder.Build (intent_builder.go) — the latter is more complete (sets Runtime/Strategy/ReconciliationStrategy/ ManifestsRepo, which the former doesn't), not reconciled here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> * test: add coverage for pkg/apis/serviceunit and the cache/* layer (#280) * test: add coverage for pkg/apis/serviceunit and the cache/* layer Both pkg/apis/serviceunit/{domain,application} and every cache/* per-CR package (build, deployment, domain, environment, githubevent, gitrepository, packages, route, serviceunit) plus the shared cache.ObjectCache/NewExternal primitives were at 0% coverage. Total repo coverage: 35.9% -> 48.3%. Adds cache/internal/testutil.FakeExternalCache — an in-memory, JSON-serializing core/cache.ExternalCache fake shared across cache/* tests, mirroring pkg/secrets/internal/testutil's existing pattern. Real bug found and fixed while writing the serviceunit cache tests: cache/serviceunit/serviceunit.go's PublishResolved switched on r.Spec.Type.String(), comparing against literal "static"/"build" — but the generated proto String() returns the full constant name (e.g. "SERVICE_UNIT_TYPE_STATIC"), so neither case ever matched and the image/buildRef field was silently never cached for any ServiceUnit. Fixed to switch on the enum value directly, matching the pattern already used in resolution/serviceunit/resolve.go. Not covered here: cache/adapter's Redis/Memcached backends need a live connection to test meaningfully (and per setup.go's own TODOs, neither is actually wired up yet — NewExternal always returns NoopExternalCache regardless of the configured backend). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat(serviceunit): wire ServiceUnitCache into Reconcile Closes the gap flagged after the cache/* coverage pass: every per-CR cache was fully built and tested but never called from any CR's application/Reconcile code. Wires ServiceUnit end to end as the first pattern to prove the integration point, before deciding whether to repeat it for the other 8 CRs. Reconcile now calls cache.PublishResolved after the status write, using the CR's own namespace/name/generation. Best-effort per the cache layer's existing contract (see PublishResolved's doc comment) — its error is discarded, never fails Reconcile. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> * docs: regenerate code documentation (#284) docs/code/ predated the resolution/* adapter/contract/resolve split, so it was structurally stale, not just missing new packages — e.g. resolution/build.md was a flat file where the source is now a resolution/build/ directory of sub-packages. Regenerated wholesale via the same gomarkdoc invocation code-docs.yml uses, rather than patching in just the newest packages, since the drift went back further than this session's changes. Picks up: the reconcile/ and strategy/ split, pkg/apis/serviceunit, pkg/intent/{deployment,serviceunit,package}, core's per-file docs, and the resolution/*/{adapter,contract,resolve} layout. Drops docs for deleted code: pkg/apis/deployment/api/application/ and the old pkg/apis/packages/intent location. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> * ci(code-docs): commit generated docs via the Git Data API, drop GPG (#285) git commit -S required GPG_PRIVATE_KEY/GPG_PASSPHRASE just to get a "Verified" badge on an automated docs commit. GitHub marks API-created commits as Verified automatically (same mechanism as an edit made in the web UI) when made with an authenticated GitHub App token — which this workflow already generates for checkout. Replaced the GPG-import + `git commit -S && git push` steps with a Git Data API call (getRef/getCommit/createTree/createCommit/updateRef) via actions/github-script, with one retry if main moved underneath it. Verified locally: `act push -j godoc -W .github/workflows/code-docs.yml -e <(echo '{"ref":"refs/heads/main"}') -n` dry-runs the full step sequence cleanly, and the change-detection + tree-building logic was exercised against a real gomarkdoc regen of this repo's docs/code/ (83 changed paths, correctly split into adds/modifies vs. deletions) via a standalone harness with the GitHub API calls stubbed — real API calls weren't made since that would create a real commit. No more GPG_PRIVATE_KEY/GPG_PASSPHRASE dependency for this workflow. APP_ID/APP_PRIVATE_KEY remain required; the App's Contents permission must be Read & write (already relied on for the existing push). Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
ntlaletsi70
added a commit
that referenced
this pull request
Jul 19, 2026
* test: add full coverage for resolution/* packages (#262)
* test: add full coverage for resolution/* packages
Every resolution/* package (build, deployment, environment, githubevent,
gitrepository, packages, route, domain, serviceunit) had zero tests despite
being pure decode/validate logic with no external dependencies — the
highest-value, lowest-effort target after core/*.
Each ResolveX function gets a resolve_test.go covering: nil input, empty/
missing contract, invalid JSON, every required-field-missing branch, every
wrong-JSON-type branch on required and optional fields, and the full valid
path. Each ToXContract projection gets a contract_adapter_test.go covering
nil-input safety and every enum/wrapper mapping branch, including the
default-to-UNSPECIFIED fallback paths that are non-fatal by design.
resolution/serviceunit/resolve.go is a documented stub (returns nil, nil) —
only its contract_adapter.go has real logic, so that's what's tested; the
stub itself gets one smoke test recording its current no-op behavior.
Two tests deliberately document existing gaps rather than desired behavior,
flagged inline rather than silently fixed:
- resolution/environment: the "domain" contract field resolves into
spec.Route instead of a (nonexistent) spec.Domain field, silently
overwriting whatever "route" resolved to.
- resolution/contract_resolution.go: Adapter constructs a packages.Adapter
but the type switch has no case for *environmentv1alpha1.Package, so
Package objects always hit the "unsupported object type" default —
the packages adapter is currently dead weight.
Coverage: all 10 packages (including the top-level resolution/ dispatcher)
at 100%. go build/vet/gofmt clean. Overall repo coverage 14.5% -> 35.7%.
* refactor: split resolution/* into resolve/adapter/contract subpackages
Each resolution/<CR> package (build, deployment, environment, githubevent,
gitrepository, packages, route, domain, serviceunit) now separates its
resolve.go, adapter.go, and contract_adapter.go into their own resolve/,
adapter/, and contract/ subpackages, matching the one-file-one-package
convention already used under pkg/secrets/. Contract-layer methods defined
on Resolved*Spec types are converted to free functions taking the type as
a parameter, since a method's receiver must live in the same package as
the type. All consumers across pkg/apis, pkg/secrets, pkg/intent,
pkg/serviceaccounts, and cache/* are repointed to the new import paths.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* chore(release): update changelog for v0.7.3 (#263)
Co-authored-by: github-actions <actions@github.com>
* feat(serviceunit): give ServiceUnit its own resolution + domain floor (#265)
ResolveServiceUnit was a dead stub (commented out, returning nil, nil)
and the top-level resolution dispatch never routed ServiceUnit CRs at
all, so no code path in the repo could produce a ResolvedServiceUnit.
ServiceUnit also had no pkg/apis/serviceunit floor of its own — its
lifecycle state and shape were borrowed inside Deployment's packages.
Adds a real ResolveServiceUnit (canonical raw-JSON decode, matching
Build/Route/Deployment), wires it into the dispatch switch, and gives
ServiceUnit its own domain (model/state/result/errors) and application
(mapper/service/status) floor so it resolves and reports its own
status independently, rather than Deployment absorbing its modeling
by accretion.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* ci(release-assets): generate SLSA provenance for release assets (#268)
Fixes the OpenSSF Scorecard Signed-Releases check (0/10) — it scans
release assets for signature/provenance files, and none of this
repo's release assets carried any.
Adds two jobs gated via `needs:` on the four existing asset jobs (no
race with a second `release:` trigger): download everything just
uploaded to the release, hash it, and generate SLSA level-3 build
provenance via slsa-framework/slsa-github-generator, uploaded back
onto the same release as *.intoto.jsonl.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* ci: move SLSA provenance to its own standalone workflow (#272) (#274)
* ci: move SLSA provenance to its own standalone workflow (#272)
Reverts the release-assets.yml integration in favor of a dedicated
slsa-provenance.yml matching the upstream generator's own build+
provenance shape. Scopes provenance to the one artifact this repo
actually builds (the gomarkdoc docs bundle) rather than also
attesting install/CRDs/CLI assets that are just re-bundled from
environments-install and environments-cli's own releases — those
repos should generate their own provenance for their own output.
The build job rebuilds the docs bundle itself so the hash it feeds
to the provenance generator is guaranteed to match what it built,
rather than assuming byte-identical reproducibility against
release-assets.yml's separate docs job.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* fix(ci): match code-docs.yml GPG committer identity to the signing key (#275)
Every push to main has been failing at "Import GPG key" with:
Committer email "github-actions[bot]@users.noreply.github.com"
does not match GPG private key email "actions@github.com"
code-docs.yml was the only workflow using a different committer
identity (github-actions[bot] / users.noreply.github.com) than the
one the shared GPG_PRIVATE_KEY is actually issued for. release.yml
and finalize-release.yml already use github-actions / actions@github.com
and sign correctly — code-docs.yml now matches.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* fix(ci): heal stale vendor-snapshot cache and simplify coverage summary (#276)
* fix(ci): heal stale vendor-snapshot cache and simplify coverage summary
Vendor-snapshot fix (validate, coverage in ci.yml; govulncheck in
security.yml):
go.sum only changes when a required module's version changes. A test
newly importing an already-required module's not-yet-vendored
subpackage (e.g. pkg/secrets/git/build/build_test.go importing
sigs.k8s.io/controller-runtime/pkg/client/fake) leaves go.sum
untouched, so the sha256(go.sum) cache tag never busts — the cached
snapshot built before that import existed keeps getting restored as
current forever. Confirmed via the failed develop CI run (#270,
29609870897): "Restore vendor from snapshot" succeeds, then
go test -mod=vendor fails with "cannot find module providing package
.../client/fake: import lookup disabled by -mod=vendor".
Each of the three jobs now verifies the restored vendor with a plain
go build -mod=vendor right after restoring, and if that fails,
rebuilds vendor locally and pushes a corrected snapshot under the
same tag + :latest before continuing — turning a silent, permanent
staleness bug into a self-healing one-time rebuild.
Coverage summary (ci.yml coverage job):
go tool cover -func emits one row per function, not per package —
hundreds of rows in a repo this size. Rewritten to aggregate
coverage.out's statement counts by package directory instead,
producing one row per package, sorted weakest-first, and collapsed
into a <details> block so the step summary stays short by default.
Also fixed the module-prefix strip, which referenced a stale
github.com/BlanketOps/blanketops-environments/ path that doesn't
match this module's actual github.com/blanketops/environments.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* feat(ci): generate a gocov-html coverage report alongside coverage.html
Adds beauty-report.html (gocov convert + gocov-html) as a richer,
styled coverage report next to the existing plain go tool cover
-html output, uploaded in the same coverage-report-<sha> artifact.
gocov's tagged releases and current master (last touched 2024-10-11)
pin golang.org/x/tools@v0.13.0, which fails to compile under this
repo's Go toolchain — confirmed directly (go install fails identically
on both the latest tag and master; a plain golang.org/x/tools@v0.13.0
build fails the same way outside of gocov entirely, while @latest
builds fine). This is stale upstream, not fixable by tracking a newer
gocov version, so the only working path is building gocov from source
with a forced newer x/tools via `go mod edit -replace` — verified
end-to-end against this repo's real coverage.out before wiring it in.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(ci): staleness probe must include go vet, not just go build
Caught live on the first real run of the previous commit: the probe
used `go build -mod=vendor ./...`, but build doesn't type-check
_test.go files — and the exact missing package this whole fix exists
for (sigs.k8s.io/controller-runtime/pkg/client/fake) is only ever
imported from a test file. Build reported success, the rebuild/heal
steps got skipped as a no-op, and Vet (which does check test files)
then failed on the same stale vendor right after — same bug, just
moved one step later instead of actually being caught.
All three probes (validate, coverage in ci.yml; govulncheck in
security.yml) now run go vet -mod=vendor alongside go build before
deciding whether to rebuild.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(ci): reset GOPROXY before the vendor-heal fallback's go mod vendor
Live failure on the first real govulncheck run of this fix: the
rebuild step's go mod vendor hit
honnef.co/go/tools/cmd/staticcheck: unrecognized import path
"honnef.co/go/tools": https fetch: ... TLS handshake timeout
validate's equivalent run (same commit) succeeded, so this may have
been one-off network flakiness rather than guaranteed — but the
GOPROXY=direct these three jobs set is real and relevant either way:
it forces every module, including vanity-domain ones like
honnef.co/go/tools, through direct fetch instead of the module proxy.
vendor-snapshot (ci.yml) — the job actually designed to run a full
`go mod vendor` — never sets GOPROXY at all, relying on the default
proxy-first behavior. The heal steps in validate/coverage/govulncheck
now reset GOPROXY to that same default immediately before go mod
vendor, matching the job that's actually proven to do this reliably.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Develop (#277)
* ci: move SLSA provenance to its own standalone workflow (#272)
Reverts the release-assets.yml integration in favor of a dedicated
slsa-provenance.yml matching the upstream generator's own build+
provenance shape. Scopes provenance to the one artifact this repo
actually builds (the gomarkdoc docs bundle) rather than also
attesting install/CRDs/CLI assets that are just re-bundled from
environments-install and environments-cli's own releases — those
repos should generate their own provenance for their own output.
The build job rebuilds the docs bundle itself so the hash it feeds
to the provenance generator is guaranteed to match what it built,
rather than assuming byte-identical reproducibility against
release-assets.yml's separate docs job.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* fix(ci): match code-docs.yml GPG committer identity to the signing key (#275)
Every push to main has been failing at "Import GPG key" with:
Committer email "github-actions[bot]@users.noreply.github.com"
does not match GPG private key email "actions@github.com"
code-docs.yml was the only workflow using a different committer
identity (github-actions[bot] / users.noreply.github.com) than the
one the shared GPG_PRIVATE_KEY is actually issued for. release.yml
and finalize-release.yml already use github-actions / actions@github.com
and sign correctly — code-docs.yml now matches.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* fix(ci): heal stale vendor-snapshot cache and simplify coverage summary (#276)
* fix(ci): heal stale vendor-snapshot cache and simplify coverage summary
Vendor-snapshot fix (validate, coverage in ci.yml; govulncheck in
security.yml):
go.sum only changes when a required module's version changes. A test
newly importing an already-required module's not-yet-vendored
subpackage (e.g. pkg/secrets/git/build/build_test.go importing
sigs.k8s.io/controller-runtime/pkg/client/fake) leaves go.sum
untouched, so the sha256(go.sum) cache tag never busts — the cached
snapshot built before that import existed keeps getting restored as
current forever. Confirmed via the failed develop CI run (#270,
29609870897): "Restore vendor from snapshot" succeeds, then
go test -mod=vendor fails with "cannot find module providing package
.../client/fake: import lookup disabled by -mod=vendor".
Each of the three jobs now verifies the restored vendor with a plain
go build -mod=vendor right after restoring, and if that fails,
rebuilds vendor locally and pushes a corrected snapshot under the
same tag + :latest before continuing — turning a silent, permanent
staleness bug into a self-healing one-time rebuild.
Coverage summary (ci.yml coverage job):
go tool cover -func emits one row per function, not per package —
hundreds of rows in a repo this size. Rewritten to aggregate
coverage.out's statement counts by package directory instead,
producing one row per package, sorted weakest-first, and collapsed
into a <details> block so the step summary stays short by default.
Also fixed the module-prefix strip, which referenced a stale
github.com/BlanketOps/blanketops-environments/ path that doesn't
match this module's actual github.com/blanketops/environments.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* feat(ci): generate a gocov-html coverage report alongside coverage.html
Adds beauty-report.html (gocov convert + gocov-html) as a richer,
styled coverage report next to the existing plain go tool cover
-html output, uploaded in the same coverage-report-<sha> artifact.
gocov's tagged releases and current master (last touched 2024-10-11)
pin golang.org/x/tools@v0.13.0, which fails to compile under this
repo's Go toolchain — confirmed directly (go install fails identically
on both the latest tag and master; a plain golang.org/x/tools@v0.13.0
build fails the same way outside of gocov entirely, while @latest
builds fine). This is stale upstream, not fixable by tracking a newer
gocov version, so the only working path is building gocov from source
with a forced newer x/tools via `go mod edit -replace` — verified
end-to-end against this repo's real coverage.out before wiring it in.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(ci): staleness probe must include go vet, not just go build
Caught live on the first real run of the previous commit: the probe
used `go build -mod=vendor ./...`, but build doesn't type-check
_test.go files — and the exact missing package this whole fix exists
for (sigs.k8s.io/controller-runtime/pkg/client/fake) is only ever
imported from a test file. Build reported success, the rebuild/heal
steps got skipped as a no-op, and Vet (which does check test files)
then failed on the same stale vendor right after — same bug, just
moved one step later instead of actually being caught.
All three probes (validate, coverage in ci.yml; govulncheck in
security.yml) now run go vet -mod=vendor alongside go build before
deciding whether to rebuild.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(ci): reset GOPROXY before the vendor-heal fallback's go mod vendor
Live failure on the first real govulncheck run of this fix: the
rebuild step's go mod vendor hit
honnef.co/go/tools/cmd/staticcheck: unrecognized import path
"honnef.co/go/tools": https fetch: ... TLS handshake timeout
validate's equivalent run (same commit) succeeded, so this may have
been one-off network flakiness rather than guaranteed — but the
GOPROXY=direct these three jobs set is real and relevant either way:
it forces every module, including vanity-domain ones like
honnef.co/go/tools, through direct fetch instead of the module proxy.
vendor-snapshot (ci.yml) — the job actually designed to run a full
`go mod vendor` — never sets GOPROXY at all, relying on the default
proxy-first behavior. The heal steps in validate/coverage/govulncheck
now reset GOPROXY to that same default immediately before go mod
vendor, matching the job that's actually proven to do this reliably.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* ci: move SLSA provenance to its own standalone workflow (#278)
Reverts the release-assets.yml integration in favor of a dedicated
slsa-provenance.yml matching the upstream generator's own build+
provenance shape. Scopes provenance to the one artifact this repo
actually builds (the gomarkdoc docs bundle) rather than also
attesting install/CRDs/CLI assets that are just re-bundled from
environments-install and environments-cli's own releases — those
repos should generate their own provenance for their own output.
The build job rebuilds the docs bundle itself so the hash it feeds
to the provenance generator is guaranteed to match what it built,
rather than assuming byte-identical reproducibility against
release-assets.yml's separate docs job.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* refactor(deployment): split strategy/reconcile dispatch out of api (#283)
* ci: move SLSA provenance to its own standalone workflow (#272)
Reverts the release-assets.yml integration in favor of a dedicated
slsa-provenance.yml matching the upstream generator's own build+
provenance shape. Scopes provenance to the one artifact this repo
actually builds (the gomarkdoc docs bundle) rather than also
attesting install/CRDs/CLI assets that are just re-bundled from
environments-install and environments-cli's own releases — those
repos should generate their own provenance for their own output.
The build job rebuilds the docs bundle itself so the hash it feeds
to the provenance generator is guaranteed to match what it built,
rather than assuming byte-identical reproducibility against
release-assets.yml's separate docs job.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* fix(ci): match code-docs.yml GPG committer identity to the signing key (#275)
Every push to main has been failing at "Import GPG key" with:
Committer email "github-actions[bot]@users.noreply.github.com"
does not match GPG private key email "actions@github.com"
code-docs.yml was the only workflow using a different committer
identity (github-actions[bot] / users.noreply.github.com) than the
one the shared GPG_PRIVATE_KEY is actually issued for. release.yml
and finalize-release.yml already use github-actions / actions@github.com
and sign correctly — code-docs.yml now matches.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* fix(ci): heal stale vendor-snapshot cache and simplify coverage summary (#276)
* fix(ci): heal stale vendor-snapshot cache and simplify coverage summary
Vendor-snapshot fix (validate, coverage in ci.yml; govulncheck in
security.yml):
go.sum only changes when a required module's version changes. A test
newly importing an already-required module's not-yet-vendored
subpackage (e.g. pkg/secrets/git/build/build_test.go importing
sigs.k8s.io/controller-runtime/pkg/client/fake) leaves go.sum
untouched, so the sha256(go.sum) cache tag never busts — the cached
snapshot built before that import existed keeps getting restored as
current forever. Confirmed via the failed develop CI run (#270,
29609870897): "Restore vendor from snapshot" succeeds, then
go test -mod=vendor fails with "cannot find module providing package
.../client/fake: import lookup disabled by -mod=vendor".
Each of the three jobs now verifies the restored vendor with a plain
go build -mod=vendor right after restoring, and if that fails,
rebuilds vendor locally and pushes a corrected snapshot under the
same tag + :latest before continuing — turning a silent, permanent
staleness bug into a self-healing one-time rebuild.
Coverage summary (ci.yml coverage job):
go tool cover -func emits one row per function, not per package —
hundreds of rows in a repo this size. Rewritten to aggregate
coverage.out's statement counts by package directory instead,
producing one row per package, sorted weakest-first, and collapsed
into a <details> block so the step summary stays short by default.
Also fixed the module-prefix strip, which referenced a stale
github.com/BlanketOps/blanketops-environments/ path that doesn't
match this module's actual github.com/blanketops/environments.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* feat(ci): generate a gocov-html coverage report alongside coverage.html
Adds beauty-report.html (gocov convert + gocov-html) as a richer,
styled coverage report next to the existing plain go tool cover
-html output, uploaded in the same coverage-report-<sha> artifact.
gocov's tagged releases and current master (last touched 2024-10-11)
pin golang.org/x/tools@v0.13.0, which fails to compile under this
repo's Go toolchain — confirmed directly (go install fails identically
on both the latest tag and master; a plain golang.org/x/tools@v0.13.0
build fails the same way outside of gocov entirely, while @latest
builds fine). This is stale upstream, not fixable by tracking a newer
gocov version, so the only working path is building gocov from source
with a forced newer x/tools via `go mod edit -replace` — verified
end-to-end against this repo's real coverage.out before wiring it in.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(ci): staleness probe must include go vet, not just go build
Caught live on the first real run of the previous commit: the probe
used `go build -mod=vendor ./...`, but build doesn't type-check
_test.go files — and the exact missing package this whole fix exists
for (sigs.k8s.io/controller-runtime/pkg/client/fake) is only ever
imported from a test file. Build reported success, the rebuild/heal
steps got skipped as a no-op, and Vet (which does check test files)
then failed on the same stale vendor right after — same bug, just
moved one step later instead of actually being caught.
All three probes (validate, coverage in ci.yml; govulncheck in
security.yml) now run go vet -mod=vendor alongside go build before
deciding whether to rebuild.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(ci): reset GOPROXY before the vendor-heal fallback's go mod vendor
Live failure on the first real govulncheck run of this fix: the
rebuild step's go mod vendor hit
honnef.co/go/tools/cmd/staticcheck: unrecognized import path
"honnef.co/go/tools": https fetch: ... TLS handshake timeout
validate's equivalent run (same commit) succeeded, so this may have
been one-off network flakiness rather than guaranteed — but the
GOPROXY=direct these three jobs set is real and relevant either way:
it forces every module, including vanity-domain ones like
honnef.co/go/tools, through direct fetch instead of the module proxy.
vendor-snapshot (ci.yml) — the job actually designed to run a full
`go mod vendor` — never sets GOPROXY at all, relying on the default
proxy-first behavior. The heal steps in validate/coverage/govulncheck
now reset GOPROXY to that same default immediately before go mod
vendor, matching the job that's actually proven to do this reliably.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* fix(deployment): untangle execution + domain layers, fix strategy bug (#281)
* fix(deployment): untangle execution + domain layers, fix strategy bug
Deployment carried three separate, unreconciled attempts at "dispatch
a Provider from an Intent" plus two unreconciled domain result/error
models. Audited every file (full diffs or repo-wide grep confirming
zero external references) before deleting anything — nothing here
had existing test coverage to break.
Real bug fixed: K8SProvider.Execute (pkg/apis/deployment/api/kubernetes.go)
had a parameter named `intent` shadowing the `intent` package, so
`switch intent.Strategy { case intent.Strategy: ... }` compared the
value to itself — the first case always matched and executeBlueGreen
was unreachable. Every Kubernetes deployment silently executed as
Rolling regardless of configured Strategy. Renamed the parameter to
dIntent and fixed the case labels to reference the real package
constants; added a table-driven test proving the two branches are
now actually distinguishable.
Deleted as confirmed dead code:
- pkg/apis/deployment/api/application/ (5 files) — the oldest
BackendSelector.ForIntent design: panics instead of returning
errors, no GitOps awareness beyond a hardcoded Flux special-case,
and its intent_builder/mapper/status siblings were missing
ManifestsRepo/ReconciliationStrategy wiring, a validation check,
and RetryOnConflict that the surviving application/ package has.
- pkg/apis/deployment/application/backend_selector.go +
gitops_decorator.go — a second, later redesign (ProviderRegistry +
Provider.Supports() + GitOpsDecorator) that was never wired to
DeploymentService, and whose one piece of real logic
(GitOpsDecorator.Execute) was a single comment with no actual
implementation — while the GitOps path actually in use
(KustomizeStrategyProvider.ReconcileKustomization) is fully real.
- pkg/apis/deployment/domain/result.go, domain/errors.go, and the
Result-consuming half of domain/state.go (DeploymentState,
ServiceUnitState, StateFromResult, ServiceUnitStateFromResult) — a
third, richer domain model with zero references anywhere outside
these three files; the execution path uses model.go's simpler
DeploymentResult/DeploymentPhase/ServiceUnitPhase instead.
Also promotes Intent to match the per-CR floor layout every other
piece of this codebase uses (pkg/apis/packages/intent/ already does
this): moved pkg/intent/deployment/ (9 files) to
pkg/apis/deployment/intent/, renaming `package deployment` to
`package intent` to match — this also drops the explicit `intent`
import alias every caller previously needed to avoid colliding with
the package's old name.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(deployment): relocate intent to top-level intent/deployment/
Amends this branch's earlier move — pkg/apis/deployment/intent/ was
still nested under deployment's own tree, which doesn't match the
actual target: Intent as a real 4th architectural pillar, sibling to
resolution/, cache/, and core/, not tucked inside pkg/apis/.
Moved intent/deployment/ to the repo root and renamed package intent
back to package deployment (matching cache/deployment's convention:
package name = directory's own name). Every caller re-adds the
explicit `intent` import alias to keep every existing call site
(intent.DeploymentIntent, intent.StrategyRolling, etc.) unchanged.
pkg/apis/packages/intent/ (nested) is now the odd one out against
this new top-level convention — flagged as a likely follow-up, not
done here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(deployment,packages): relocate intent under pkg/, not top-level
Reverses the previous top-level intent/ placement — pkg/apis/ is
where all the per-CR domain code already lives, so Intent belongs
alongside it under pkg/, not out at the repo root next to
resolution/cache/core.
- pkg/intent/deployment/ — back to its original location (this is
where it started before any of today's moves), package deployment
kept as-is from the last relocation. Every caller keeps its
explicit `intent` import alias.
- pkg/apis/packages/intent/ -> pkg/intent/package/ — the other CR
with its own intent subpackage, now matching the same pkg/intent/
convention. Kept `package intent` (directory is named "package",
which can't be a Go package name — it's a reserved word), so the 5
existing importers already resolve to the `intent` identifier with
no alias needed, same as before the move.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(serviceunit,deployment): give ServiceUnit its own intent layer
Neo: "serviceunit must cater for its own resolution... I have the
intent resolving serviceunit, not correct, it must resolve itself."
Same principle as the earlier resolution/domain/application work for
ServiceUnit, now extended to the intent layer: pkg/intent/deployment
owned ServiceUnitIntent, RouteIntent, and WorkloadIntent, and built
them via ResolveServiceUnitIntent — all logic that's actually about
ServiceUnit, not Deployment.
New pkg/intent/serviceunit/ (matching the pkg/intent/<cr> convention
already established for deployment and package):
- ServiceUnitIntent + ResolveServiceUnitIntent (moved from
pkg/intent/deployment/serviceunit.go)
- RouteIntent (moved from route.go) and WorkloadIntent (moved from
workload.go) — both were only ever used by ServiceUnitIntent, so
they move with it; zero circular-dependency risk confirmed before
moving.
- ErrBuildNotReady/ErrInvalidServiceUnit (moved from errors.go) —
the two ServiceUnit-specific errors; ErrServiceUnitNotFound and
ErrInvalidDeployment stay in deployment's errors.go since they're
genuinely Deployment's own.
pkg/intent/deployment.DeploymentIntent.ServiceUnits is now
[]serviceunitIntent.ServiceUnitIntent; every caller across
resolve.go, pkg/apis/deployment/application/intent_builder.go,
pkg/apis/deployment/render/builders/builder.go,
pkg/apis/deployment/api/{kubernetes,ecs}.go updated accordingly.
Also relocates IntentBuilder (pkg/apis/deployment/application ->
pkg/intent/deployment/intent_builder.go) per a follow-up request —
the intent-building logic belongs in the intent package itself, not
the application layer. Note for a later pass: this package now has
two overlapping ways to build a DeploymentIntent — ResolveDeploymentIntent
(resolve.go) and IntentBuilder.Build (intent_builder.go) — the latter
is more complete (sets Runtime/Strategy/ReconciliationStrategy/
ManifestsRepo, which the former doesn't), not reconciled here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* test: add coverage for pkg/apis/serviceunit and the cache/* layer (#280)
* test: add coverage for pkg/apis/serviceunit and the cache/* layer
Both pkg/apis/serviceunit/{domain,application} and every cache/*
per-CR package (build, deployment, domain, environment, githubevent,
gitrepository, packages, route, serviceunit) plus the shared
cache.ObjectCache/NewExternal primitives were at 0% coverage. Total
repo coverage: 35.9% -> 48.3%.
Adds cache/internal/testutil.FakeExternalCache — an in-memory,
JSON-serializing core/cache.ExternalCache fake shared across cache/*
tests, mirroring pkg/secrets/internal/testutil's existing pattern.
Real bug found and fixed while writing the serviceunit cache tests:
cache/serviceunit/serviceunit.go's PublishResolved switched on
r.Spec.Type.String(), comparing against literal "static"/"build" —
but the generated proto String() returns the full constant name
(e.g. "SERVICE_UNIT_TYPE_STATIC"), so neither case ever matched and
the image/buildRef field was silently never cached for any
ServiceUnit. Fixed to switch on the enum value directly, matching
the pattern already used in resolution/serviceunit/resolve.go.
Not covered here: cache/adapter's Redis/Memcached backends need a
live connection to test meaningfully (and per setup.go's own TODOs,
neither is actually wired up yet — NewExternal always returns
NoopExternalCache regardless of the configured backend).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* feat(serviceunit): wire ServiceUnitCache into Reconcile
Closes the gap flagged after the cache/* coverage pass: every
per-CR cache was fully built and tested but never called from any
CR's application/Reconcile code. Wires ServiceUnit end to end as the
first pattern to prove the integration point, before deciding whether
to repeat it for the other 8 CRs.
Reconcile now calls cache.PublishResolved after the status write,
using the CR's own namespace/name/generation. Best-effort per the
cache layer's existing contract (see PublishResolved's doc comment)
— its error is discarded, never fails Reconcile.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* refactor(deployment): split strategy/reconcile dispatch out of api
api/ was carrying both infra (materializing k8s objects, GitOps commits)
and dispatch (which reconciliation mode, which runtime/strategy) in one
flat package. Split dispatch into two new packages:
- reconcile/: ReconciliationExecutor, the Imperative-vs-GitOps axis.
- strategy/: RuntimeProvider + K8SStrategy (the Rolling/BlueGreen switch
pulled out of K8SProvider.Execute), plus the ECS/Knative placeholder
reconcilers. Kubernetes, ECS, and Knative are deployment strategies in
this domain, not a separate runtime layer, so they live together here.
api/ keeps only the infra that actually materializes objects:
K8SProvider's apply/teardown (ApplyServiceUnit now exported for
strategy.K8SStrategy to call) and KustomizeStrategyProvider's GitOps
commit path.
Also persists the go-run-driver verification recipe used to confirm this
and the earlier structural cleanup didn't regress the Deployment pipeline.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* test(deployment): cover the api/application/strategy/reconcile split
Adds tests for the packages touched by the reconcile/strategy split:
K8SProvider's apply/teardown infra and ProviderRegistry (api),
DeploymentService.Reconcile end-to-end and StatusWriter (application),
ReconciliationExecutor's imperative/GitOps dispatch (reconcile),
RuntimeProvider + deriveDeploymentPhase (strategy), and the Deployment/
Service builders (render/builders) — all previously at 0%.
pkg/apis/deployment/application/mapper.go is left untested: Mapper /
MapResolvedToDomain have no callers anywhere (DeploymentService.Reconcile
goes through IntentBuilder, not this Mapper), so it's dead code rather
than a coverage gap — flagged separately for a cleanup follow-up.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* docs(deployment,intent): add package doc comments
api, application, domain, reconcile, strategy (pkg/apis/deployment) and
pkg/intent/{deployment,serviceunit,package} had no package-level doc
comment at all — browsing the freshly regenerated docs/code output made
this obvious, since these packages rendered with no overview text next to
ones that do have it (e.g. pkg/apis/serviceunit).
domain/model.go's existing "DOMAIN PRINCIPLES" block was mid-file, after
the package clause, so godoc/gomarkdoc never picked it up as the package
doc — folded into a proper package comment instead of leaving a duplicate.
pkg/intent/deployment's comment also documents in one place what was only
previously flagged in a private note: ResolveDeploymentIntent (resolve.go)
is a superseded, partial constructor with no real callers — IntentBuilder.Build
is the one DeploymentService actually uses.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Develop (#282)
* ci: move SLSA provenance to its own standalone workflow (#272)
Reverts the release-assets.yml integration in favor of a dedicated
slsa-provenance.yml matching the upstream generator's own build+
provenance shape. Scopes provenance to the one artifact this repo
actually builds (the gomarkdoc docs bundle) rather than also
attesting install/CRDs/CLI assets that are just re-bundled from
environments-install and environments-cli's own releases — those
repos should generate their own provenance for their own output.
The build job rebuilds the docs bundle itself so the hash it feeds
to the provenance generator is guaranteed to match what it built,
rather than assuming byte-identical reproducibility against
release-assets.yml's separate docs job.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* fix(ci): match code-docs.yml GPG committer identity to the signing key (#275)
Every push to main has been failing at "Import GPG key" with:
Committer email "github-actions[bot]@users.noreply.github.com"
does not match GPG private key email "actions@github.com"
code-docs.yml was the only workflow using a different committer
identity (github-actions[bot] / users.noreply.github.com) than the
one the shared GPG_PRIVATE_KEY is actually issued for. release.yml
and finalize-release.yml already use github-actions / actions@github.com
and sign correctly — code-docs.yml now matches.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* fix(ci): heal stale vendor-snapshot cache and simplify coverage summary (#276)
* fix(ci): heal stale vendor-snapshot cache and simplify coverage summary
Vendor-snapshot fix (validate, coverage in ci.yml; govulncheck in
security.yml):
go.sum only changes when a required module's version changes. A test
newly importing an already-required module's not-yet-vendored
subpackage (e.g. pkg/secrets/git/build/build_test.go importing
sigs.k8s.io/controller-runtime/pkg/client/fake) leaves go.sum
untouched, so the sha256(go.sum) cache tag never busts — the cached
snapshot built before that import existed keeps getting restored as
current forever. Confirmed via the failed develop CI run (#270,
29609870897): "Restore vendor from snapshot" succeeds, then
go test -mod=vendor fails with "cannot find module providing package
.../client/fake: import lookup disabled by -mod=vendor".
Each of the three jobs now verifies the restored vendor with a plain
go build -mod=vendor right after restoring, and if that fails,
rebuilds vendor locally and pushes a corrected snapshot under the
same tag + :latest before continuing — turning a silent, permanent
staleness bug into a self-healing one-time rebuild.
Coverage summary (ci.yml coverage job):
go tool cover -func emits one row per function, not per package —
hundreds of rows in a repo this size. Rewritten to aggregate
coverage.out's statement counts by package directory instead,
producing one row per package, sorted weakest-first, and collapsed
into a <details> block so the step summary stays short by default.
Also fixed the module-prefix strip, which referenced a stale
github.com/BlanketOps/blanketops-environments/ path that doesn't
match this module's actual github.com/blanketops/environments.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* feat(ci): generate a gocov-html coverage report alongside coverage.html
Adds beauty-report.html (gocov convert + gocov-html) as a richer,
styled coverage report next to the existing plain go tool cover
-html output, uploaded in the same coverage-report-<sha> artifact.
gocov's tagged releases and current master (last touched 2024-10-11)
pin golang.org/x/tools@v0.13.0, which fails to compile under this
repo's Go toolchain — confirmed directly (go install fails identically
on both the latest tag and master; a plain golang.org/x/tools@v0.13.0
build fails the same way outside of gocov entirely, while @latest
builds fine). This is stale upstream, not fixable by tracking a newer
gocov version, so the only working path is building gocov from source
with a forced newer x/tools via `go mod edit -replace` — verified
end-to-end against this repo's real coverage.out before wiring it in.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(ci): staleness probe must include go vet, not just go build
Caught live on the first real run of the previous commit: the probe
used `go build -mod=vendor ./...`, but build doesn't type-check
_test.go files — and the exact missing package this whole fix exists
for (sigs.k8s.io/controller-runtime/pkg/client/fake) is only ever
imported from a test file. Build reported success, the rebuild/heal
steps got skipped as a no-op, and Vet (which does check test files)
then failed on the same stale vendor right after — same bug, just
moved one step later instead of actually being caught.
All three probes (validate, coverage in ci.yml; govulncheck in
security.yml) now run go vet -mod=vendor alongside go build before
deciding whether to rebuild.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(ci): reset GOPROXY before the vendor-heal fallback's go mod vendor
Live failure on the first real govulncheck run of this fix: the
rebuild step's go mod vendor hit
honnef.co/go/tools/cmd/staticcheck: unrecognized import path
"honnef.co/go/tools": https fetch: ... TLS handshake timeout
validate's equivalent run (same commit) succeeded, so this may have
been one-off network flakiness rather than guaranteed — but the
GOPROXY=direct these three jobs set is real and relevant either way:
it forces every module, including vanity-domain ones like
honnef.co/go/tools, through direct fetch instead of the module proxy.
vendor-snapshot (ci.yml) — the job actually designed to run a full
`go mod vendor` — never sets GOPROXY at all, relying on the default
proxy-first behavior. The heal steps in validate/coverage/govulncheck
now reset GOPROXY to that same default immediately before go mod
vendor, matching the job that's actually proven to do this reliably.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* fix(deployment): untangle execution + domain layers, fix strategy bug (#281)
* fix(deployment): untangle execution + domain layers, fix strategy bug
Deployment carried three separate, unreconciled attempts at "dispatch
a Provider from an Intent" plus two unreconciled domain result/error
models. Audited every file (full diffs or repo-wide grep confirming
zero external references) before deleting anything — nothing here
had existing test coverage to break.
Real bug fixed: K8SProvider.Execute (pkg/apis/deployment/api/kubernetes.go)
had a parameter named `intent` shadowing the `intent` package, so
`switch intent.Strategy { case intent.Strategy: ... }` compared the
value to itself — the first case always matched and executeBlueGreen
was unreachable. Every Kubernetes deployment silently executed as
Rolling regardless of configured Strategy. Renamed the parameter to
dIntent and fixed the case labels to reference the real package
constants; added a table-driven test proving the two branches are
now actually distinguishable.
Deleted as confirmed dead code:
- pkg/apis/deployment/api/application/ (5 files) — the oldest
BackendSelector.ForIntent design: panics instead of returning
errors, no GitOps awareness beyond a hardcoded Flux special-case,
and its intent_builder/mapper/status siblings were missing
ManifestsRepo/ReconciliationStrategy wiring, a validation check,
and RetryOnConflict that the surviving application/ package has.
- pkg/apis/deployment/application/backend_selector.go +
gitops_decorator.go — a second, later redesign (ProviderRegistry +
Provider.Supports() + GitOpsDecorator) that was never wired to
DeploymentService, and whose one piece of real logic
(GitOpsDecorator.Execute) was a single comment with no actual
implementation — while the GitOps path actually in use
(KustomizeStrategyProvider.ReconcileKustomization) is fully real.
- pkg/apis/deployment/domain/result.go, domain/errors.go, and the
Result-consuming half of domain/state.go (DeploymentState,
ServiceUnitState, StateFromResult, ServiceUnitStateFromResult) — a
third, richer domain model with zero references anywhere outside
these three files; the execution path uses model.go's simpler
DeploymentResult/DeploymentPhase/ServiceUnitPhase instead.
Also promotes Intent to match the per-CR floor layout every other
piece of this codebase uses (pkg/apis/packages/intent/ already does
this): moved pkg/intent/deployment/ (9 files) to
pkg/apis/deployment/intent/, renaming `package deployment` to
`package intent` to match — this also drops the explicit `intent`
import alias every caller previously needed to avoid colliding with
the package's old name.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(deployment): relocate intent to top-level intent/deployment/
Amends this branch's earlier move — pkg/apis/deployment/intent/ was
still nested under deployment's own tree, which doesn't match the
actual target: Intent as a real 4th architectural pillar, sibling to
resolution/, cache/, and core/, not tucked inside pkg/apis/.
Moved intent/deployment/ to the repo root and renamed package intent
back to package deployment (matching cache/deployment's convention:
package name = directory's own name). Every caller re-adds the
explicit `intent` import alias to keep every existing call site
(intent.DeploymentIntent, intent.StrategyRolling, etc.) unchanged.
pkg/apis/packages/intent/ (nested) is now the odd one out against
this new top-level convention — flagged as a likely follow-up, not
done here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(deployment,packages): relocate intent under pkg/, not top-level
Reverses the previous top-level intent/ placement — pkg/apis/ is
where all the per-CR domain code already lives, so Intent belongs
alongside it under pkg/, not out at the repo root next to
resolution/cache/core.
- pkg/intent/deployment/ — back to its original location (this is
where it started before any of today's moves), package deployment
kept as-is from the last relocation. Every caller keeps its
explicit `intent` import alias.
- pkg/apis/packages/intent/ -> pkg/intent/package/ — the other CR
with its own intent subpackage, now matching the same pkg/intent/
convention. Kept `package intent` (directory is named "package",
which can't be a Go package name — it's a reserved word), so the 5
existing importers already resolve to the `intent` identifier with
no alias needed, same as before the move.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(serviceunit,deployment): give ServiceUnit its own intent layer
Neo: "serviceunit must cater for its own resolution... I have the
intent resolving serviceunit, not correct, it must resolve itself."
Same principle as the earlier resolution/domain/application work for
ServiceUnit, now extended to the intent layer: pkg/intent/deployment
owned ServiceUnitIntent, RouteIntent, and WorkloadIntent, and built
them via ResolveServiceUnitIntent — all logic that's actually about
ServiceUnit, not Deployment.
New pkg/intent/serviceunit/ (matching the pkg/intent/<cr> convention
already established for deployment and package):
- ServiceUnitIntent + ResolveServiceUnitIntent (moved from
pkg/intent/deployment/serviceunit.go)
- RouteIntent (moved from route.go) and WorkloadIntent (moved from
workload.go) — both were only ever used by ServiceUnitIntent, so
they move with it; zero circular-dependency risk confirmed before
moving.
- ErrBuildNotReady/ErrInvalidServiceUnit (moved from errors.go) —
the two ServiceUnit-specific errors; ErrServiceUnitNotFound and
ErrInvalidDeployment stay in deployment's errors.go since they're
genuinely Deployment's own.
pkg/intent/deployment.DeploymentIntent.ServiceUnits is now
[]serviceunitIntent.ServiceUnitIntent; every caller across
resolve.go, pkg/apis/deployment/application/intent_builder.go,
pkg/apis/deployment/render/builders/builder.go,
pkg/apis/deployment/api/{kubernetes,ecs}.go updated accordingly.
Also relocates IntentBuilder (pkg/apis/deployment/application ->
pkg/intent/deployment/intent_builder.go) per a follow-up request —
the intent-building logic belongs in the intent package itself, not
the application layer. Note for a later pass: this package now has
two overlapping ways to build a DeploymentIntent — ResolveDeploymentIntent
(resolve.go) and IntentBuilder.Build (intent_builder.go) — the latter
is more complete (sets Runtime/Strategy/ReconciliationStrategy/
ManifestsRepo, which the former doesn't), not reconciled here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* test: add coverage for pkg/apis/serviceunit and the cache/* layer (#280)
* test: add coverage for pkg/apis/serviceunit and the cache/* layer
Both pkg/apis/serviceunit/{domain,application} and every cache/*
per-CR package (build, deployment, domain, environment, githubevent,
gitrepository, packages, route, serviceunit) plus the shared
cache.ObjectCache/NewExternal primitives were at 0% coverage. Total
repo coverage: 35.9% -> 48.3%.
Adds cache/internal/testutil.FakeExternalCache — an in-memory,
JSON-serializing core/cache.ExternalCache fake shared across cache/*
tests, mirroring pkg/secrets/internal/testutil's existing pattern.
Real bug found and fixed while writing the serviceunit cache tests:
cache/serviceunit/serviceunit.go's PublishResolved switched on
r.Spec.Type.String(), comparing against literal "static"/"build" —
but the generated proto String() returns the full constant name
(e.g. "SERVICE_UNIT_TYPE_STATIC"), so neither case ever matched and
the image/buildRef field was silently never cached for any
ServiceUnit. Fixed to switch on the enum value directly, matching
the pattern already used in resolution/serviceunit/resolve.go.
Not covered here: cache/adapter's Redis/Memcached backends need a
live connection to test meaningfully (and per setup.go's own TODOs,
neither is actually wired up yet — NewExternal always returns
NoopExternalCache regardless of the configured backend).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* feat(serviceunit): wire ServiceUnitCache into Reconcile
Closes the gap flagged after the cache/* coverage pass: every
per-CR cache was fully built and tested but never called from any
CR's application/Reconcile code. Wires ServiceUnit end to end as the
first pattern to prove the integration point, before deciding whether
to repeat it for the other 8 CRs.
Reconcile now calls cache.PublishResolved after the status write,
using the CR's own namespace/name/generation. Best-effort per the
cache layer's existing contract (see PublishResolved's doc comment)
— its error is discarded, never fails Reconcile.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* docs: regenerate code documentation (#284)
docs/code/ predated the resolution/* adapter/contract/resolve split, so
it was structurally stale, not just missing new packages — e.g.
resolution/build.md was a flat file where the source is now a
resolution/build/ directory of sub-packages. Regenerated wholesale via
the same gomarkdoc invocation code-docs.yml uses, rather than patching
in just the newest packages, since the drift went back further than
this session's changes.
Picks up: the reconcile/ and strategy/ split, pkg/apis/serviceunit,
pkg/intent/{deployment,serviceunit,package}, core's per-file docs, and
the resolution/*/{adapter,contract,resolve} layout. Drops docs for
deleted code: pkg/apis/deployment/api/application/ and the old
pkg/apis/packages/intent location.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* ci(code-docs): commit generated docs via the Git Data API, drop GPG (#285)
git commit -S required GPG_PRIVATE_KEY/GPG_PASSPHRASE just to get a
"Verified" badge on an automated docs commit. GitHub marks API-created
commits as Verified automatically (same mechanism as an edit made in the
web UI) when made with an authenticated GitHub App token — which this
workflow already generates for checkout. Replaced the GPG-import +
`git commit -S && git push` steps with a Git Data API call
(getRef/getCommit/createTree/createCommit/updateRef) via
actions/github-script, with one retry if main moved underneath it.
Verified locally: `act push -j godoc -W .github/workflows/code-docs.yml
-e <(echo '{"ref":"refs/heads/main"}') -n` dry-runs the full step
sequence cleanly, and the change-detection + tree-building logic was
exercised against a real gomarkdoc regen of this repo's docs/code/ (83
changed paths, correctly split into adds/modifies vs. deletions) via a
standalone harness with the GitHub API calls stubbed — real API calls
weren't made since that would create a real commit.
No more GPG_PRIVATE_KEY/GPG_PASSPHRASE dependency for this workflow.
APP_ID/APP_PRIVATE_KEY remain required; the App's Contents permission
must be Read & write (already relied on for the existing push).
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: github-actions <actions@github.com>
ntlaletsi70
added a commit
that referenced
this pull request
Jul 21, 2026
* ci: move SLSA provenance to its own standalone workflow (#272) Reverts the release-assets.yml integration in favor of a dedicated slsa-provenance.yml matching the upstream generator's own build+ provenance shape. Scopes provenance to the one artifact this repo actually builds (the gomarkdoc docs bundle) rather than also attesting install/CRDs/CLI assets that are just re-bundled from environments-install and environments-cli's own releases — those repos should generate their own provenance for their own output. The build job rebuilds the docs bundle itself so the hash it feeds to the provenance generator is guaranteed to match what it built, rather than assuming byte-identical reproducibility against release-assets.yml's separate docs job. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> * fix(ci): match code-docs.yml GPG committer identity to the signing key (#275) Every push to main has been failing at "Import GPG key" with: Committer email "github-actions[bot]@users.noreply.github.com" does not match GPG private key email "actions@github.com" code-docs.yml was the only workflow using a different committer identity (github-actions[bot] / users.noreply.github.com) than the one the shared GPG_PRIVATE_KEY is actually issued for. release.yml and finalize-release.yml already use github-actions / actions@github.com and sign correctly — code-docs.yml now matches. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> * fix(ci): heal stale vendor-snapshot cache and simplify coverage summary (#276) * fix(ci): heal stale vendor-snapshot cache and simplify coverage summary Vendor-snapshot fix (validate, coverage in ci.yml; govulncheck in security.yml): go.sum only changes when a required module's version changes. A test newly importing an already-required module's not-yet-vendored subpackage (e.g. pkg/secrets/git/build/build_test.go importing sigs.k8s.io/controller-runtime/pkg/client/fake) leaves go.sum untouched, so the sha256(go.sum) cache tag never busts — the cached snapshot built before that import existed keeps getting restored as current forever. Confirmed via the failed develop CI run (#270, 29609870897): "Restore vendor from snapshot" succeeds, then go test -mod=vendor fails with "cannot find module providing package .../client/fake: import lookup disabled by -mod=vendor". Each of the three jobs now verifies the restored vendor with a plain go build -mod=vendor right after restoring, and if that fails, rebuilds vendor locally and pushes a corrected snapshot under the same tag + :latest before continuing — turning a silent, permanent staleness bug into a self-healing one-time rebuild. Coverage summary (ci.yml coverage job): go tool cover -func emits one row per function, not per package — hundreds of rows in a repo this size. Rewritten to aggregate coverage.out's statement counts by package directory instead, producing one row per package, sorted weakest-first, and collapsed into a <details> block so the step summary stays short by default. Also fixed the module-prefix strip, which referenced a stale github.com/BlanketOps/blanketops-environments/ path that doesn't match this module's actual github.com/blanketops/environments. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat(ci): generate a gocov-html coverage report alongside coverage.html Adds beauty-report.html (gocov convert + gocov-html) as a richer, styled coverage report next to the existing plain go tool cover -html output, uploaded in the same coverage-report-<sha> artifact. gocov's tagged releases and current master (last touched 2024-10-11) pin golang.org/x/tools@v0.13.0, which fails to compile under this repo's Go toolchain — confirmed directly (go install fails identically on both the latest tag and master; a plain golang.org/x/tools@v0.13.0 build fails the same way outside of gocov entirely, while @latest builds fine). This is stale upstream, not fixable by tracking a newer gocov version, so the only working path is building gocov from source with a forced newer x/tools via `go mod edit -replace` — verified end-to-end against this repo's real coverage.out before wiring it in. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(ci): staleness probe must include go vet, not just go build Caught live on the first real run of the previous commit: the probe used `go build -mod=vendor ./...`, but build doesn't type-check _test.go files — and the exact missing package this whole fix exists for (sigs.k8s.io/controller-runtime/pkg/client/fake) is only ever imported from a test file. Build reported success, the rebuild/heal steps got skipped as a no-op, and Vet (which does check test files) then failed on the same stale vendor right after — same bug, just moved one step later instead of actually being caught. All three probes (validate, coverage in ci.yml; govulncheck in security.yml) now run go vet -mod=vendor alongside go build before deciding whether to rebuild. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(ci): reset GOPROXY before the vendor-heal fallback's go mod vendor Live failure on the first real govulncheck run of this fix: the rebuild step's go mod vendor hit honnef.co/go/tools/cmd/staticcheck: unrecognized import path "honnef.co/go/tools": https fetch: ... TLS handshake timeout validate's equivalent run (same commit) succeeded, so this may have been one-off network flakiness rather than guaranteed — but the GOPROXY=direct these three jobs set is real and relevant either way: it forces every module, including vanity-domain ones like honnef.co/go/tools, through direct fetch instead of the module proxy. vendor-snapshot (ci.yml) — the job actually designed to run a full `go mod vendor` — never sets GOPROXY at all, relying on the default proxy-first behavior. The heal steps in validate/coverage/govulncheck now reset GOPROXY to that same default immediately before go mod vendor, matching the job that's actually proven to do this reliably. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> * fix(deployment): untangle execution + domain layers, fix strategy bug (#281) * fix(deployment): untangle execution + domain layers, fix strategy bug Deployment carried three separate, unreconciled attempts at "dispatch a Provider from an Intent" plus two unreconciled domain result/error models. Audited every file (full diffs or repo-wide grep confirming zero external references) before deleting anything — nothing here had existing test coverage to break. Real bug fixed: K8SProvider.Execute (pkg/apis/deployment/api/kubernetes.go) had a parameter named `intent` shadowing the `intent` package, so `switch intent.Strategy { case intent.Strategy: ... }` compared the value to itself — the first case always matched and executeBlueGreen was unreachable. Every Kubernetes deployment silently executed as Rolling regardless of configured Strategy. Renamed the parameter to dIntent and fixed the case labels to reference the real package constants; added a table-driven test proving the two branches are now actually distinguishable. Deleted as confirmed dead code: - pkg/apis/deployment/api/application/ (5 files) — the oldest BackendSelector.ForIntent design: panics instead of returning errors, no GitOps awareness beyond a hardcoded Flux special-case, and its intent_builder/mapper/status siblings were missing ManifestsRepo/ReconciliationStrategy wiring, a validation check, and RetryOnConflict that the surviving application/ package has. - pkg/apis/deployment/application/backend_selector.go + gitops_decorator.go — a second, later redesign (ProviderRegistry + Provider.Supports() + GitOpsDecorator) that was never wired to DeploymentService, and whose one piece of real logic (GitOpsDecorator.Execute) was a single comment with no actual implementation — while the GitOps path actually in use (KustomizeStrategyProvider.ReconcileKustomization) is fully real. - pkg/apis/deployment/domain/result.go, domain/errors.go, and the Result-consuming half of domain/state.go (DeploymentState, ServiceUnitState, StateFromResult, ServiceUnitStateFromResult) — a third, richer domain model with zero references anywhere outside these three files; the execution path uses model.go's simpler DeploymentResult/DeploymentPhase/ServiceUnitPhase instead. Also promotes Intent to match the per-CR floor layout every other piece of this codebase uses (pkg/apis/packages/intent/ already does this): moved pkg/intent/deployment/ (9 files) to pkg/apis/deployment/intent/, renaming `package deployment` to `package intent` to match — this also drops the explicit `intent` import alias every caller previously needed to avoid colliding with the package's old name. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(deployment): relocate intent to top-level intent/deployment/ Amends this branch's earlier move — pkg/apis/deployment/intent/ was still nested under deployment's own tree, which doesn't match the actual target: Intent as a real 4th architectural pillar, sibling to resolution/, cache/, and core/, not tucked inside pkg/apis/. Moved intent/deployment/ to the repo root and renamed package intent back to package deployment (matching cache/deployment's convention: package name = directory's own name). Every caller re-adds the explicit `intent` import alias to keep every existing call site (intent.DeploymentIntent, intent.StrategyRolling, etc.) unchanged. pkg/apis/packages/intent/ (nested) is now the odd one out against this new top-level convention — flagged as a likely follow-up, not done here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(deployment,packages): relocate intent under pkg/, not top-level Reverses the previous top-level intent/ placement — pkg/apis/ is where all the per-CR domain code already lives, so Intent belongs alongside it under pkg/, not out at the repo root next to resolution/cache/core. - pkg/intent/deployment/ — back to its original location (this is where it started before any of today's moves), package deployment kept as-is from the last relocation. Every caller keeps its explicit `intent` import alias. - pkg/apis/packages/intent/ -> pkg/intent/package/ — the other CR with its own intent subpackage, now matching the same pkg/intent/ convention. Kept `package intent` (directory is named "package", which can't be a Go package name — it's a reserved word), so the 5 existing importers already resolve to the `intent` identifier with no alias needed, same as before the move. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(serviceunit,deployment): give ServiceUnit its own intent layer Neo: "serviceunit must cater for its own resolution... I have the intent resolving serviceunit, not correct, it must resolve itself." Same principle as the earlier resolution/domain/application work for ServiceUnit, now extended to the intent layer: pkg/intent/deployment owned ServiceUnitIntent, RouteIntent, and WorkloadIntent, and built them via ResolveServiceUnitIntent — all logic that's actually about ServiceUnit, not Deployment. New pkg/intent/serviceunit/ (matching the pkg/intent/<cr> convention already established for deployment and package): - ServiceUnitIntent + ResolveServiceUnitIntent (moved from pkg/intent/deployment/serviceunit.go) - RouteIntent (moved from route.go) and WorkloadIntent (moved from workload.go) — both were only ever used by ServiceUnitIntent, so they move with it; zero circular-dependency risk confirmed before moving. - ErrBuildNotReady/ErrInvalidServiceUnit (moved from errors.go) — the two ServiceUnit-specific errors; ErrServiceUnitNotFound and ErrInvalidDeployment stay in deployment's errors.go since they're genuinely Deployment's own. pkg/intent/deployment.DeploymentIntent.ServiceUnits is now []serviceunitIntent.ServiceUnitIntent; every caller across resolve.go, pkg/apis/deployment/application/intent_builder.go, pkg/apis/deployment/render/builders/builder.go, pkg/apis/deployment/api/{kubernetes,ecs}.go updated accordingly. Also relocates IntentBuilder (pkg/apis/deployment/application -> pkg/intent/deployment/intent_builder.go) per a follow-up request — the intent-building logic belongs in the intent package itself, not the application layer. Note for a later pass: this package now has two overlapping ways to build a DeploymentIntent — ResolveDeploymentIntent (resolve.go) and IntentBuilder.Build (intent_builder.go) — the latter is more complete (sets Runtime/Strategy/ReconciliationStrategy/ ManifestsRepo, which the former doesn't), not reconciled here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> * test: add coverage for pkg/apis/serviceunit and the cache/* layer (#280) * test: add coverage for pkg/apis/serviceunit and the cache/* layer Both pkg/apis/serviceunit/{domain,application} and every cache/* per-CR package (build, deployment, domain, environment, githubevent, gitrepository, packages, route, serviceunit) plus the shared cache.ObjectCache/NewExternal primitives were at 0% coverage. Total repo coverage: 35.9% -> 48.3%. Adds cache/internal/testutil.FakeExternalCache — an in-memory, JSON-serializing core/cache.ExternalCache fake shared across cache/* tests, mirroring pkg/secrets/internal/testutil's existing pattern. Real bug found and fixed while writing the serviceunit cache tests: cache/serviceunit/serviceunit.go's PublishResolved switched on r.Spec.Type.String(), comparing against literal "static"/"build" — but the generated proto String() returns the full constant name (e.g. "SERVICE_UNIT_TYPE_STATIC"), so neither case ever matched and the image/buildRef field was silently never cached for any ServiceUnit. Fixed to switch on the enum value directly, matching the pattern already used in resolution/serviceunit/resolve.go. Not covered here: cache/adapter's Redis/Memcached backends need a live connection to test meaningfully (and per setup.go's own TODOs, neither is actually wired up yet — NewExternal always returns NoopExternalCache regardless of the configured backend). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat(serviceunit): wire ServiceUnitCache into Reconcile Closes the gap flagged after the cache/* coverage pass: every per-CR cache was fully built and tested but never called from any CR's application/Reconcile code. Wires ServiceUnit end to end as the first pattern to prove the integration point, before deciding whether to repeat it for the other 8 CRs. Reconcile now calls cache.PublishResolved after the status write, using the CR's own namespace/name/generation. Best-effort per the cache layer's existing contract (see PublishResolved's doc comment) — its error is discarded, never fails Reconcile. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> * docs: regenerate code documentation (#284) docs/code/ predated the resolution/* adapter/contract/resolve split, so it was structurally stale, not just missing new packages — e.g. resolution/build.md was a flat file where the source is now a resolution/build/ directory of sub-packages. Regenerated wholesale via the same gomarkdoc invocation code-docs.yml uses, rather than patching in just the newest packages, since the drift went back further than this session's changes. Picks up: the reconcile/ and strategy/ split, pkg/apis/serviceunit, pkg/intent/{deployment,serviceunit,package}, core's per-file docs, and the resolution/*/{adapter,contract,resolve} layout. Drops docs for deleted code: pkg/apis/deployment/api/application/ and the old pkg/apis/packages/intent location. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> * ci(code-docs): commit generated docs via the Git Data API, drop GPG (#285) git commit -S required GPG_PRIVATE_KEY/GPG_PASSPHRASE just to get a "Verified" badge on an automated docs commit. GitHub marks API-created commits as Verified automatically (same mechanism as an edit made in the web UI) when made with an authenticated GitHub App token — which this workflow already generates for checkout. Replaced the GPG-import + `git commit -S && git push` steps with a Git Data API call (getRef/getCommit/createTree/createCommit/updateRef) via actions/github-script, with one retry if main moved underneath it. Verified locally: `act push -j godoc -W .github/workflows/code-docs.yml -e <(echo '{"ref":"refs/heads/main"}') -n` dry-runs the full step sequence cleanly, and the change-detection + tree-building logic was exercised against a real gomarkdoc regen of this repo's docs/code/ (83 changed paths, correctly split into adds/modifies vs. deletions) via a standalone harness with the GitHub API calls stubbed — real API calls weren't made since that would create a real commit. No more GPG_PRIVATE_KEY/GPG_PASSPHRASE dependency for this workflow. APP_ID/APP_PRIVATE_KEY remain required; the App's Contents permission must be Read & write (already relied on for the existing push). Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> * test(resolution): add native Go fuzz tests for CR contract resolvers Resolve{GitRepository,Route,Domain} decode attacker-controlled Contract.Raw JSON from CRs and manually walk nested maps/type assertions beyond what encoding/json validates. Fuzzing the existing xWithContract(raw) test helpers exercises that parsing directly, satisfying the OpenSSF Scorecard Fuzzing check via Go's native testing.F support (go.dev/doc/security/fuzz). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * refactor(deployment): split strategy/reconcile dispatch out of api (#289) * refactor(deployment): split strategy/reconcile dispatch out of api api/ was carrying both infra (materializing k8s objects, GitOps commits) and dispatch (which reconciliation mode, which runtime/strategy) in one flat package. Split dispatch into two new packages: - reconcile/: ReconciliationExecutor, the Imperative-vs-GitOps axis. - strategy/: RuntimeProvider + K8SStrategy (the Rolling/BlueGreen switch pulled out of K8SProvider.Execute), plus the ECS/Knative placeholder reconcilers. Kubernetes, ECS, and Knative are deployment strategies in this domain, not a separate runtime layer, so they live together here. api/ keeps only the infra that actually materializes objects: K8SProvider's apply/teardown (ApplyServiceUnit now exported for strategy.K8SStrategy to call) and KustomizeStrategyProvider's GitOps commit path. Also persists the go-run-driver verification recipe used to confirm this and the earlier structural cleanup didn't regress the Deployment pipeline. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * test(deployment): cover the api/application/strategy/reconcile split Adds tests for the packages touched by the reconcile/strategy split: K8SProvider's apply/teardown infra and ProviderRegistry (api), DeploymentService.Reconcile end-to-end and StatusWriter (application), ReconciliationExecutor's imperative/GitOps dispatch (reconcile), RuntimeProvider + deriveDeploymentPhase (strategy), and the Deployment/ Service builders (render/builders) — all previously at 0%. pkg/apis/deployment/application/mapper.go is left untested: Mapper / MapResolvedToDomain have no callers anywhere (DeploymentService.Reconcile goes through IntentBuilder, not this Mapper), so it's dead code rather than a coverage gap — flagged separately for a cleanup follow-up. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * docs(deployment,intent): add package doc comments api, application, domain, reconcile, strategy (pkg/apis/deployment) and pkg/intent/{deployment,serviceunit,package} had no package-level doc comment at all — browsing the freshly regenerated docs/code output made this obvious, since these packages rendered with no overview text next to ones that do have it (e.g. pkg/apis/serviceunit). domain/model.go's existing "DOMAIN PRINCIPLES" block was mid-file, after the package clause, so godoc/gomarkdoc never picked it up as the package doc — folded into a proper package comment instead of leaving a duplicate. pkg/intent/deployment's comment also documents in one place what was only previously flagged in a private note: ResolveDeploymentIntent (resolve.go) is a superseded, partial constructor with no real callers — IntentBuilder.Build is the one DeploymentService actually uses. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> * feat(environment): provision Vault ClusterSecretStore/SecretStore; expand fuzz coverage SecretStoreReconciler provisions the ESO ClusterSecretStore/SecretStore backing an Environment's declared secretStore.provider, dispatched by provider string with a builder per provider (vault.go fully implemented via Kubernetes auth; aws/azure/gcp stubbed pending their own ResolvedXConfig). Unlike the create-once composed-CR secret reconcilers under pkg/secrets, this always server-side-applies the desired spec so it converges on later changes, matching K8SProvider.applyDeployment's pattern. resolve.go validates contract.secretStore.vault (address/path/role required; mountPath/version defaulted) at resolution time rather than deep inside ESO reconciliation. Also adds native Go fuzz targets (testing.F) for the five remaining CR contract resolvers that decode attacker-controlled Contract.Raw JSON (Build, Deployment, Environment, GitHubEvent, Package), rounding out the OpenSSF Scorecard Fuzzing coverage started on fix/native-go-fuzzing to every Resolve* that parses a raw JSON contract. fuzzing.yml's matrix now runs all eight targets. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: github-actions <actions@github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Deletes two confirmed-dead, unreconciled attempts at Deployment's Provider-dispatch design and a third unreconciled domain result/error model, fixes a real strategy-dispatch bug in the surviving path, and promotes
pkg/intent/deploymenttopkg/apis/deployment/intentto match the per-CR floor layout the rest of the codebase uses (pkg/apis/packages/intent/already does this).Why
Audited
pkg/apis/deployment/file-by-file after it was flagged as cluttered. Found three separate historical attempts at "dispatch a Provider from an Intent," only one of whichNewDeploymentServiceactually constructs, plus a second domain model (Result/DeploymentState/ServiceUnitState/DeploymentError) with zero references anywhere outside its own three files. Every deletion below was verified via fulldiffor repo-widegrepbefore removal — nothing here had existing test coverage to break (find pkg/apis/deployment -name "*_test.go"was empty before this PR).Real bug found and fixed along the way:
K8SProvider.Execute(pkg/apis/deployment/api/kubernetes.go) had a parameter namedintent, shadowing theintentpackage:Both case labels resolved to the parameter's own field, not the package constants —
switch X { case X: }— so the first case always matched andexecuteBlueGreenwas unreachable. Every Kubernetes deployment silently executed as Rolling regardless of configured Strategy. This compiles cleanly since it's not a constant comparison, so Go's duplicate-case check never caught it.Domain
environmentseventssourcesnetworkscommonAPI impact
Checklist
mage verifypasses locally — no Magefile present in this checkout; rango build ./...,go vet ./...,gofmt -l, andgo test ./...instead — all clean, zero failures repo-widebuf breakingreviewed — N/A, no.protochangespanic()calls added; deleted code that used to panicgen/go/blanketops/...for contract types — N/A, no contract import changescore.SetConditionat each domain pipeline stage — N/A, no conditions logic changedcore.EventRecorderfor terminal outcomes — N/ANotes for reviewer
api/provider.go'sProviderRegistrybecomes unused onceapplication/backend_selector.gois deleted (its only caller), but is left in place — it's small, self-contained, reasonable plumbing for whenever Knative/ECS providers get built out, and isn't confusingly duplicated (only oneProviderRegistryexists, unlike the twoBackendSelectors this PR removes down to one).intentimport alias, since the package's real name now matches (previously it waspackage deployment, colliding with siblingdeploymentpackages, hence the alias).🤖 Generated with Claude Code