Skip to content

fix(diff): VPC/Droplet/AppPlatform Diff now compares region (#70)#71

Merged
intel352 merged 2 commits intomainfrom
fix/diff-region-detection
May 5, 2026
Merged

fix(diff): VPC/Droplet/AppPlatform Diff now compares region (#70)#71
intel352 merged 2 commits intomainfrom
fix/diff-region-detection

Conversation

@intel352
Copy link
Copy Markdown
Contributor

@intel352 intel352 commented May 5, 2026

Summary

Closes #70. Adds region comparison to VPCDriver, DropletDriver, AppPlatformDriver Diff functions.

Surfaced by core-dump TC2 staging cutover (run) where the cascade-replace plan-shape assertion expected 5 replace actions but got 1 (Volume only). Without the gate, apply would have left VPC + Droplet in nyc3 while moving App + Volume to nyc1 — half-migrated state requiring manual cleanup.

Changes

File Δ Why
internal/drivers/vpc.go +13 VPCDriver.Diff: add region check, ForceNew on drift
internal/drivers/droplet.go +13 DropletDriver.Diff: same pattern
internal/drivers/app_platform.go +28 AppPlatformDriver.Diff: same pattern + add region to appOutput so Diff has a current-side value to compare
internal/drivers/vpc_test.go +59 RegionChangeForcesReplace + RegionEmptyCurrentSkipped
internal/drivers/droplet_test.go +71 same pattern
internal/drivers/app_platform_test.go +71 same pattern
CHANGELOG.md +3 [Unreleased] fixed entry

Total: 6 source files + CHANGELOG, 266 insertions.

Pattern

All three drivers now follow VolumeDriver.Diff's canonical region block (lines 165-173):

if region := strFromConfig(desired.Config, "region", ""); region != "" {
    curRegion, _ := current.Outputs["region"].(string)
    if curRegion != "" && curRegion != region {
        changes = append(changes, interfaces.FieldChange{
            Path: "region", Old: curRegion, New: region, ForceNew: true,
        })
        needsReplace = true
    }
}

The curRegion != "" guard is the upgrade-safe path: resources in state from earlier plugin versions (when their output functions didn't include region) don't false-positive on the first plan after upgrade — they'll Read on next apply to populate the field naturally.

Validation

$ GOWORK=off go build ./...
# clean

$ GOWORK=off go test -count=1 ./internal/drivers/
ok  	github.com/GoCodeAlone/workflow-plugin-digitalocean/internal/drivers	6.151s

$ GOWORK=off go test -count=1 ./...
ok  	github.com/GoCodeAlone/workflow-plugin-digitalocean	1.448s
ok  	github.com/GoCodeAlone/workflow-plugin-digitalocean/internal	2.046s
ok  	github.com/GoCodeAlone/workflow-plugin-digitalocean/internal/drivers	6.151s

Scope discipline

Only the 3 Diff fixes + their regression tests + the appOutput.region addition + CHANGELOG entry. No other Diff changes; no public API changes; no .github/workflows touch.

Test plan

  • Local validation
  • CI green
  • Copilot review pass
  • Admin-merge → tag v0.10.2

Downstream

After v0.10.2 ships, core-dump's .wfctl-lock.yaml + wfctl.yaml bump v0.10.1 → v0.10.2 in a follow-up PR. Re-dispatching tc2-cutover.yml will then emit the expected 5-resource cascade.

🤖 Generated with Claude Code

Previously only VolumeDriver.Diff compared `region` against
current.Outputs["region"]; the other three regional-resource drivers
(VPCDriver, DropletDriver, AppPlatformDriver) silently no-op'd on
region changes despite DO having no in-place region update API for
those resources.

## Bug surfaced by

Core-dump TC2 staging cutover (https://github.com/GoCodeAlone/core-dump/actions/runs/25383166075):
plan against an infra.yaml whose VPC/Volume/Droplet/App regions
moved nyc3 → nyc1. Pre-cutover DO state confirmed all 5 resources
in nyc3. Plan output: only Volume got a `replace` action; VPC and
Droplet were absent from the plan entirely; App got `update`
(should be replace). The plan-shape assertion in
core-dump's tc2-cutover.yml caught the discrepancy before any
cloud mutation.

Without the assertion gate, apply would have updated the firewall
+ App in nyc1 and replaced the Volume to nyc1, while leaving VPC
+ Droplet in nyc3 — producing a half-migrated state requiring
manual cleanup (App's vpc_ref pointing at nyc3 VPC, Volume in nyc1
unable to mount on a nyc3 Droplet, etc.).

## Fix

Add the canonical region-comparison block (mirroring
VolumeDriver.Diff line 165-173) to the three affected drivers:

- internal/drivers/vpc.go::VPCDriver.Diff
- internal/drivers/droplet.go::DropletDriver.Diff
- internal/drivers/app_platform.go::AppPlatformDriver.Diff

Each emits `FieldChange{Path: "region", ForceNew: true}` and sets
`NeedsReplace=true` when desired.region differs from current.region.

## Upgrade-safe guard

The pattern guards on `curRegion != ""` so resources in state from
earlier plugin versions (when their respective output functions
didn't include region) do not false-positive on the first plan
after upgrade. They will Read on next apply to populate the field
naturally.

dropletOutput + vpcOutput already populate `region`. appOutput did
NOT — this PR also adds `region: app.Spec.Region` to the appOutput
map so the Diff comparison has a value to compare against.

## Tests

Added regression tests for each driver:

- TestVPCDriver_Diff_RegionChangeForcesReplace + RegionEmptyCurrentSkipped
- TestDropletDriver_Diff_RegionChangeForcesReplace + RegionEmptyCurrentSkipped
- TestAppPlatformDriver_Diff_RegionChangeForcesReplace + RegionEmptyCurrentSkipped

Each pair covers (a) the change-detection path (current=nyc3,
desired=nyc1, asserts ForceNew + NeedsReplace) and (b) the
upgrade-safe skip path (current.Outputs missing "region", asserts
no false-positive drift).

## Validation

- `go build ./...` (GOWORK=off): clean
- `go test -count=1 ./internal/drivers/`: all 6.1s pass
- `go test -count=1 ./...`: all packages pass

## Scope

Only the 3 Diff fixes + their regression tests + appOutput addition
+ CHANGELOG entry. No other Diff changes; no public API changes;
no .github/workflows touch. Per upstream issue #70 scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 5, 2026 15:17
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a planning blind spot in the DigitalOcean IaC provider by ensuring Diff detects region drift for VPCs, Droplets, and App Platform apps, and marks those changes as replace-only (ForceNew). This prevents incomplete “region migration” plans that would otherwise leave some resources in the old region while dependents move.

Changes:

  • Add region comparisons to VPCDriver.Diff, DropletDriver.Diff, and AppPlatformDriver.Diff, emitting FieldChange{ForceNew: true} on drift.
  • Extend appOutput to include Outputs["region"] so App Platform diffs have a current-side value to compare.
  • Add regression tests for “region change forces replace” and “missing current region is skipped” cases, plus a CHANGELOG entry.

Reviewed changes

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

Show a summary per file
File Description
internal/drivers/vpc.go Adds region drift detection in VPCDriver.Diff and marks drift as ForceNew.
internal/drivers/vpc_test.go Adds tests covering replace-on-region-drift and upgrade-safe empty-current behavior for VPCs.
internal/drivers/droplet.go Adds region drift detection in DropletDriver.Diff and marks drift as ForceNew.
internal/drivers/droplet_test.go Adds tests covering replace-on-region-drift and upgrade-safe empty-current behavior for Droplets.
internal/drivers/app_platform.go Adds region drift detection to Diff (replace-only) and includes region in appOutput.
internal/drivers/app_platform_test.go Adds tests covering replace-on-region-drift and upgrade-safe empty-current behavior for App Platform.
CHANGELOG.md Documents the fix under [Unreleased].

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

Comment thread CHANGELOG.md Outdated
…view)

Copilot caught a contradiction in the CHANGELOG entry: the original
wording said "appOutput, vpcOutput, dropletOutput already populate
region" but this PR is also ADDING region to appOutput.Outputs.
Reworded to: vpcOutput + dropletOutput already had it; appOutput
gets it in this PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@intel352 intel352 merged commit 08a439c into main May 5, 2026
4 of 5 checks passed
@intel352 intel352 deleted the fix/diff-region-detection branch May 5, 2026 16:07
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.

Diff functions miss region change: VPC, Droplet, App Platform drivers don't compare region

2 participants