Skip to content

feat: declare ComputePlanVersion v2 + IaCProviderFinalizer.FinalizeApply impl + bump workflow v0.55.0 pin; release v1.3.0#123

Merged
intel352 merged 3 commits into
mainfrom
feat/695-do-finalize
May 17, 2026
Merged

feat: declare ComputePlanVersion v2 + IaCProviderFinalizer.FinalizeApply impl + bump workflow v0.55.0 pin; release v1.3.0#123
intel352 merged 3 commits into
mainfrom
feat/695-do-finalize

Conversation

@intel352

Copy link
Copy Markdown
Contributor

Summary

Phase 2.5 of workflow#640 hard-cutover cascade per ADR 0024 + 0040. DO plugin opts INTO v2 dispatch by declaring ComputePlanVersion="v2" in CapabilitiesResponse + implementing FinalizeApply RPC server-side. wfctl-side OnPlanComplete hook (added in workflow v0.55.0) calls FinalizeApply which inlines the per-driver flush loop from the v1 DOProvider.Apply wrapper — preserves the database trusted_sources deferred-flush regression gate that motivated this cascade.

Changes

  1. go.mod — workflow pin v0.54.0v0.55.0 (go mod tidy regenerated go.sum). Resolves the IaCProviderFinalizerServer types in the generated proto package.
  2. plugin.jsonversion 1.2.0 → 1.3.0; minEngineVersion 0.54.0 → 0.55.0; download URLs updated to v1.3.0 release assets.
  3. internal/iacserver.go (4 sub-edits):
    • doIaCServer struct gains pb.UnimplementedIaCProviderFinalizerServer embed (mustEmbedUnimplemented gRPC forward-compat).
    • Compile-time assert _ pb.IaCProviderFinalizerServer = (*doIaCServer)(nil) added to interface guard block.
    • FinalizeApply method implemented on doIaCServer. Iterates s.provider.drivers (canonical driver registry — captures orphaned deferred entries even when their resource type no longer appears in the current plan, mirroring v1 wrapper semantics). For each driver implementing deferredUpdater with queued state, calls FlushDeferredUpdates(ctx); per-driver failures append to response.errors[] preserving the ActionError{Resource: resourceType, Action: "deferred_update", Error: flushErr.Error()} attribution shape the v1 wrapper used.
    • Capabilities return-struct-literal flipped to add ComputePlanVersion: "v2" (return-literal-only edit per Phase 2's per-plugin universal pattern — signature/receiver/params untouched).

ADR alignment

  • ADR 0024 (no compat shim): plugin opts in via service registration; the gRPC SDK auto-detects pb.IaCProviderFinalizerServer satisfaction at plugin startup (workflow v0.55.0's registerIaCServicesOnly block) and registers the service. Absence of registration would mean no FinalizeApply RPC — but this PR explicitly opts in.
  • ADR 0040 invariants 1 + 2: per-driver attribution preserved via repeated ActionError; wire-status invariant honored (errors[] populated ⇒ gRPC OK; gRPC errors signal transport failure, distinct from per-driver finalize errors).

Test plan

  • GOWORK=off go build ./... → exit 0
  • GOWORK=off go test ./... -race -count=1 → all packages PASS (digitalocean, internal, internal/drivers, internal/statebackend, internal/steps)
  • CI green (will populate post-push)
  • Regression test for FinalizeApply firing under v2 dispatch — adds in Task 8 of the cascade plan (separate commit on this branch, will land before PR merge).

Cascade context

  • Coordinated with workflow PR #697 (workflow v0.55.0 tagged + released → squash sha aac519da; GoReleaser run 25985943298 success; 18 release assets; marked latest).
  • This DO PR's go.mod bump resolves against the already-published v0.55.0 module.

Rollback (per design §Rollback)

Revert this PR. internal/iacserver.go drops ComputePlanVersion="v2" line + FinalizeApply impl + UnimplementedIaCProviderFinalizerServer embed + compile-time assert; go.mod drops to workflow v0.54.0; plugin.json drops to v1.2.0; minEngineVersion drops to 0.54.0. Cut DO v1.3.1 restoring v1.2.0 state. DO consumers continue on workflow v0.54.0 + DO v1.2.0 (v1-dispatched).

Per ADR 0040 matched-pair rollback policy: workflow v0.55.0 stays at HEAD (already shipped); only the DO-side opt-in unwinds.

Plan: docs/plans/2026-05-17-v2-lifecycle-phase2.5-onplancomplete.md (workflow repo).
Design: docs/plans/2026-05-17-v2-lifecycle-phase2.5-onplancomplete-design.md (workflow repo).

Closes workflow#695 (after this plugin's v1.3.0 ships + smoke verification per Task 9).

🤖 Generated with Claude Code

…sion=v2 + workflow v0.55.0 (Phase 2.5)

Phase 2.5 of workflow#640 hard-cutover cascade per ADR 0024 + 0040.
DO plugin opts INTO v2 dispatch by declaring ComputePlanVersion="v2" in
CapabilitiesResponse + implementing FinalizeApply RPC server-side. The
wfctl-side OnPlanComplete hook (added in workflow v0.55.0) calls
FinalizeApply which inlines the per-driver flush loop previously held
in the v1 DOProvider.Apply wrapper — preserves the database
trusted_sources deferred-flush regression-gate that was the original
motivator for this cascade.

Changes:

1. go.mod: workflow pin v0.54.0 → v0.55.0 (resolves
   IaCProviderFinalizer service types in the generated proto package).
2. plugin.json: version 1.2.0 → 1.3.0; minEngineVersion 0.54.0 →
   0.55.0; download URLs updated to v1.3.0 release assets.
3. internal/iacserver.go: doIaCServer struct gains
   pb.UnimplementedIaCProviderFinalizerServer embed (mustEmbedUnimplemented
   gRPC forward-compat). Compile-time assertion
   `_ pb.IaCProviderFinalizerServer = (*doIaCServer)(nil)` added to the
   interface guard block — signature drift fails the build here, not at
   first RPC dispatch.
4. internal/iacserver.go: FinalizeApply method implemented on
   doIaCServer. Iterates s.provider.drivers (the canonical driver
   registry — captures orphaned deferred entries even when their
   resource type no longer appears in the current plan, mirroring v1
   wrapper semantics). For each driver implementing deferredUpdater
   with queued state, calls FlushDeferredUpdates(ctx); per-driver
   failures append to response.errors[] preserving the
   ActionError{Resource: resourceType, Action: "deferred_update",
   Error: flushErr.Error()} attribution shape the v1 wrapper used —
   wfctl-side OnPlanComplete handler propagates each entry to the
   operator-facing result.Errors via the "<plan-finalize>" diagnostic.
5. internal/iacserver.go: Capabilities return-struct-literal flipped to
   add ComputePlanVersion: "v2" (return-literal-only edit per Phase 2's
   per-plugin universal pattern — signature/receiver/params untouched).

Verification:
- GOWORK=off go build ./... → exit 0
- GOWORK=off go test ./... -race -count=1 → all packages PASS
  (digitalocean, internal, internal/drivers, internal/statebackend,
  internal/steps)

Rollback per design §Rollback: revert this commit. iacserver.go drops
ComputePlanVersion="v2" + FinalizeApply impl + UnimplementedIaCProviderFinalizerServer
embed + compile-time assert; go.mod drops to workflow v0.54.0;
plugin.json drops to v1.2.0; minEngineVersion drops to 0.54.0. Cut DO
v1.3.1 restoring v1.2.0 state. DO consumers continue on workflow
v0.54.0 + DO v1.2.0 (v1-dispatched).

Coordinated with workflow v0.55.0 (already tagged + released). Closes
workflow#695 once this plugin's v1.3.0 ships.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 17, 2026 09:02
@intel352

intel352 commented May 17, 2026

Copy link
Copy Markdown
Contributor Author

iac-codemod refactor-apply report

Mode: dry-run
Sites: 1
Errors: 0

Skipped (// wfctl:skip-iac-codemod)

  • internal/provider.go:276 DOProvider.Apply skipped

Full report (90-day retention) attached as workflow artifact.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR opts the DigitalOcean workflow plugin into the workflow engine’s v2 IaC apply lifecycle by advertising ComputePlanVersion="v2" and implementing the new FinalizeApply RPC, while bumping the workflow dependency and plugin release metadata to match the required engine version.

Changes:

  • Bump github.com/GoCodeAlone/workflow dependency to v0.55.0 (and regenerate go.sum accordingly).
  • Declare v2 dispatch capability via ComputePlanVersion: "v2" and add IaCProviderFinalizerServer.FinalizeApply implementation to flush deferred driver updates after apply.
  • Update plugin.json for release v1.3.0, including minEngineVersion and download URLs.

Reviewed changes

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

File Description
internal/iacserver.go Adds IaCProviderFinalizer service wiring, declares ComputePlanVersion="v2", and implements FinalizeApply deferred-flush loop.
go.mod Pins workflow module to v0.55.0 to pick up the finalizer service/protos.
go.sum Updates module checksums after the workflow bump.
plugin.json Bumps plugin version/min engine and updates release asset URLs to v1.3.0.

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

Comment thread internal/iacserver.go
Comment on lines 155 to +184
func (s *doIaCServer) Capabilities(_ context.Context, _ *pb.CapabilitiesRequest) (*pb.CapabilitiesResponse, error) {
caps := s.provider.Capabilities()
out := make([]*pb.IaCCapabilityDeclaration, 0, len(caps))
for _, c := range caps {
// Tier is bounded (1..3) in DOProvider; safe int→int32 conversion.
// Defensive clamp to int32 range avoids G115 drift if the
// declaration list grows.
tier := c.Tier
if tier < math.MinInt32 {
tier = math.MinInt32
} else if tier > math.MaxInt32 {
tier = math.MaxInt32
}
out = append(out, &pb.IaCCapabilityDeclaration{
ResourceType: c.ResourceType,
Tier: int32(tier), //nolint:gosec // G115: clamped above
Operations: append([]string(nil), c.Operations...),
})
}
return &pb.CapabilitiesResponse{Capabilities: out}, nil
return &pb.CapabilitiesResponse{
Capabilities: out,
// ComputePlanVersion="v2" opts this plugin into wfctl's v2 apply
// dispatch (wfctlhelpers.ApplyPlan + per-action hooks). The
// deferred-flush iteration previously held inline in the v1
// DOProvider.Apply wrapper has been hoisted to FinalizeApply (the
// IaCProviderFinalizer optional service implementation below). Per
// workflow#695 Phase 2.5 / ADR 0024 / ADR 0040.
ComputePlanVersion: "v2",
}, nil
}
Comment thread internal/iacserver.go
Comment on lines +204 to +222
func (s *doIaCServer) FinalizeApply(ctx context.Context, _ *pb.FinalizeApplyRequest) (*pb.FinalizeApplyResponse, error) {
var errs []*pb.ActionError
// Iterate the driver registry directly (not plan.Actions) so that
// orphaned deferred entries are flushed even when their resource type
// no longer appears in the current plan (e.g. after a transient flush
// failure on a prior Apply run). Mirrors v1 wrapper semantics.
for resourceType, d := range s.provider.drivers {
du, ok := d.(deferredUpdater)
if !ok || !du.HasDeferredUpdates() {
continue
}
if flushErr := du.FlushDeferredUpdates(ctx); flushErr != nil {
errs = append(errs, &pb.ActionError{
Resource: resourceType,
Action: "deferred_update",
Error: flushErr.Error(),
})
}
}
…preserves per-driver attribution (Phase 2.5)

Adds internal/iacserver_finalize_test.go with 2 regression tests that
exercise the v2 dispatch path's FinalizeApply RPC end-to-end without
going through the v1 DOProvider.Apply wrapper:

1. TestDOIaCServer_FinalizeApply_FlushesDeferredUpdates (happy path)
   — seeds a deferred trusted_sources update via direct
   dbDriver.Create call (first Apps.List returns empty so the driver
   queues the deferred update), then calls FinalizeApply on the gRPC
   server. Asserts:
   - resp.Errors is empty (no per-driver failures)
   - UpdateFirewallRules was invoked with the resolved app UUID
   - Single rule of {type: "app", value: <UUID>} appears in the
     deferred flush request

   Without FinalizeApply firing the per-driver flush loop, v2-
   dispatched plans would silently lose deferred trusted_sources
   resolution — the exact regression workflow#695 Phase 2.5 closes.

2. TestDOIaCServer_FinalizeApply_PreservesPerDriverErrorAttribution
   (failure path) — same fixture but with the new minimalDBMock.flushErr
   field set to a sentinel error. Asserts:
   - gRPC status is OK (per the wire-status invariant — per-driver
     failures live in response.errors[], not the gRPC status)
   - response.errors[] has exactly one entry
   - Entry's Resource is "infra.database"
   - Entry's Action is "deferred_update"
   - Entry's Error contains the sentinel string (substring match
     accommodates the driver's "deferred firewall update <name>:"
     prefix and survives the response.errors[] map-iteration order
     non-determinism for any future multi-driver failure tests)

   Per ADR 0040 invariants 1 + 2; locks the v1 wrapper's
   ActionError{Resource, Action, Error} attribution shape on the wire.

Test scaffolding:
- minimalDBMock gains a flushErr field (existing mock in
  provider_deferred_test.go); UpdateFirewallRules returns it when set.
  Reused by both new tests via the failing-fixture helper.
- setupProviderWithQueuedDBDeferredUpdate(t) helper extracts the seed
  pattern from TestDOProvider_Apply_FlushesDeferred_WhenTypeAbsentFromPlan
  so the new tests are independent of the v1 Apply path.
- setupProviderWithFailingDBDeferredUpdate(t) helper wraps the above
  and sets flushErr; returns the sentinel for assertion.
- fakeAppForDeferred + minimalDBMock + sequencedAppsForDeferred types
  are reused unchanged from provider_deferred_test.go.

Folded T7 follow-up Minors from code-reviewer (PR #123 quality review):

- internal/provider.go: Apply method godoc reworded — the "wfctlhelpers
  does not yet hoist" claim was true pre-Phase-2.5 but is now stale;
  v2 callers route the deferred-flush through FinalizeApply RPC via
  the ApplyPlanHooks.OnPlanComplete hook. Updated to reflect actual
  post-cascade dispatch topology while keeping the v1-callers-only
  legacy note.
- internal/iacserver.go: FinalizeApply godoc gains explicit best-effort
  fan-out semantics block — per-driver flushes that succeed remain
  committed even if later drivers fail; no cross-driver rollback
  coordination. Matches v1 DOProvider.Apply's post-loop block
  semantics; operator-facing reconciliation flows through the per-
  driver errors[] entries surfaced by the wfctl-side OnPlanComplete
  handler.

Verification:
- GOWORK=off go test ./internal/ -run 'TestDOIaCServer_FinalizeApply' -v -count=1
  → 2/2 PASS
- GOWORK=off go test ./... -race -count=1 → all packages PASS:
  digitalocean (2.3s), internal (3.5s), internal/drivers (7.8s),
  internal/statebackend (3.6s), internal/steps (2.9s)
- GOWORK=off go build ./... → exit 0

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ath assert + drop self-referential godoc clause

Three follow-up Minors on T8 (07f4af4) from code-reviewer (PR #123
quality re-review), folded for cascade cleanliness:

1. Add TestDOIaCServer_Capabilities_DeclaresV2 unit lock for the
   ComputePlanVersion="v2" string-literal in the Capabilities RPC
   return. wfctl's DispatchVersionFor reads this string to route the
   plugin through the v2 apply path; a future refactor accidentally
   dropping the literal (or a struct re-init forgetting to set it)
   silently reverts the plugin to v1 dispatch. The compile-time
   assert can't catch a string-literal omission; T9 smoke catches
   end-to-end but unit lock is more direct. ~10 LOC.

2. Add explicit `if dbMock.lastFirewallReq == nil` assert at the top
   of the failure-path test body. The error round-trip below
   implicitly proves the flush chain ran (driver wouldn't queue
   without HasDeferredUpdates returning true; mock wouldn't get
   flushErr injected without UpdateFirewallRules being called), but
   the implicit-only contract masks a future regression where the
   mock's UpdateFirewallRules short-circuits before recording the
   request (e.g., if flushErr moved to a pre-call gate). Belt+
   suspenders — locks "flush was attempted AND failed" directly.

3. Drop self-referential "no longer 'doesn't hoist'" past-tense
   correction clause from internal/provider.go Apply godoc (folded
   in 07f4af4 from T7 Minor #1). The phrasing referenced the
   previous wording, awkward for a future maintainer without
   cascade-history context. Rewords directly: "v2 callers route the
   deferred-flush through the FinalizeApply RPC (see iacserver.go);
   the workflow engine invokes it via the ApplyPlanHooks.OnPlanComplete
   hook. Per workflow#695 Phase 2.5."

Verification:
- GOWORK=off go test ./internal/ -run 'TestDOIaCServer_FinalizeApply|TestDOIaCServer_Capabilities_DeclaresV2' -v -count=1
  → 3/3 PASS
- GOWORK=off go test ./... -race -count=1 → all packages PASS race-clean
  (digitalocean 3.5s, internal 2.1s, internal/drivers 7.9s,
  internal/statebackend 3.5s, internal/steps 2.7s)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 7 changed files in this pull request and generated 1 comment.

Comment thread internal/iacserver.go
Comment on lines +210 to +228
func (s *doIaCServer) FinalizeApply(ctx context.Context, _ *pb.FinalizeApplyRequest) (*pb.FinalizeApplyResponse, error) {
var errs []*pb.ActionError
// Iterate the driver registry directly (not plan.Actions) so that
// orphaned deferred entries are flushed even when their resource type
// no longer appears in the current plan (e.g. after a transient flush
// failure on a prior Apply run). Mirrors v1 wrapper semantics.
for resourceType, d := range s.provider.drivers {
du, ok := d.(deferredUpdater)
if !ok || !du.HasDeferredUpdates() {
continue
}
if flushErr := du.FlushDeferredUpdates(ctx); flushErr != nil {
errs = append(errs, &pb.ActionError{
Resource: resourceType,
Action: "deferred_update",
Error: flushErr.Error(),
})
}
}
@intel352

Copy link
Copy Markdown
Contributor Author

Copilot review acknowledgement (sha 523c795)

Copilot inline at iacserver.go:228 raises duplication of the deferred-flush loop between DOProvider.Apply and doIaCServer.FinalizeApply. This was explicitly considered + decided during the workflow#695 design phase.

Per design doc §E (docs/plans/2026-05-17-v2-lifecycle-phase2.5-onplancomplete-design.md):

Picked: inline the per-driver flush loop directly in doIaCServer.FinalizeApply — do NOT add a new DOProvider.FlushDeferredUpdates public method.

Rationale: extracting a private flushDeferredUpdates helper grows DOProvider's exported (or even internal-package) surface to support a single v1-deletion target. Per design §Out of scope:

Phase 3: delete DOProvider.Apply v1 wrapper as dead code post-Phase-2.5 cutover. Separate cleanup PR; this plan keeps the wrapper in place to minimize blast radius.

So the duplication is BOUNDED to the Phase 2.5+Phase 3 window. Phase 3 deletes DOProvider.Apply entirely → only the FinalizeApply copy remains → no duplication.

Reviewer adversarial-design-review cycle-1 (alternative option 1 in their report) named this exact tradeoff: Inline the per-driver flush in doIaCServer.FinalizeApply directly — picked because it eliminates the 'new method on DOProvider' risk and keeps the v1 Apply wrapper (provider.go:274-310) as the single source of truth for the flush sequence — until Phase 3 deletes it.

Defensible architecturally; tracked for cleanup at Phase 3. CI green; admin-merging.

@intel352
intel352 merged commit d8a203f into main May 17, 2026
8 checks passed
@intel352
intel352 deleted the feat/695-do-finalize branch May 17, 2026 09:48
intel352 added a commit that referenced this pull request May 17, 2026
)

DOProvider.Apply was dead code post-Phase-2.5 v2 cutover (DO declares
ComputePlanVersion=v2; wfctl routes via wfctlhelpers.ApplyPlanWithHooks
+ IaCProviderFinalizer.FinalizeApply, bypassing this method).

Method body replaced with sentinel-error stub returning
ErrApplyV1Removed for diagnostic classification. interfaces.IaCProvider
contract preserved per ADR 0024 — clean deletion not viable without
breaking all 4 plugins simultaneously; interface segregation deferred
to separate refactor design (filed as followup tracking issue).

deferredUpdater interface relocated from provider.go to iacserver.go
(sole consumer is FinalizeApply); preserves Go-level type clarity.

provider_deferred_test.go deleted — regression coverage migrated to
iacserver_finalize_test.go per Phase 2.5 T8 quality-review verdict.

Eliminates FinalizeApply-vs-Apply deferred-flush loop duplication
Copilot flagged on PR #123 (workflow#695 Phase 2.5).

Scope extension (team-lead authorized): delete 11 pre-existing v1 Apply
tests across provider_apply_test.go + provider_test.go that exercise
the now-deleted v1 wrapper dispatch behavior. Equivalent coverage
preserved at wfctlhelpers.ApplyPlan tests + internal/drivers/*_test.go
per-driver tests + iacserver_finalize_test.go (Phase 2.5 T8). Helper
types relocated to internal/deferred_test_helpers_test.go for use by
remaining tests.

Closes workflow#695 followup item 'Phase 3: delete DOProvider.Apply
v1 wrapper'.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants