Skip to content

feat(wfctl): add DigitalOcean App Platform deploy provider#423

Closed
intel352 wants to merge 2 commits into
mainfrom
feat/deploy-provider-digitalocean
Closed

feat(wfctl): add DigitalOcean App Platform deploy provider#423
intel352 wants to merge 2 commits into
mainfrom
feat/deploy-provider-digitalocean

Conversation

@intel352

Copy link
Copy Markdown
Contributor

Summary

  • Adds digitalocean (and alias do) to newDeployProvider in deploy_providers.go
  • Deploy upserts to DO App Platform REST API: GET to find existing app, POST to create, PUT to update. Translates ServiceConfig (expose ports, scaling replicas, secrets) to a minimal doAppSpec
  • HealthCheck fetches live_url from GET /v2/apps/{id} and passes the full URL to pollHealthCheck
  • Auth via DIGITALOCEAN_TOKEN env var; clear error when unset
  • No new external dependencies — godo is already in go.mod

Tests

Five httptest.NewServer-based tests in deploy_providers_test.go:

  • TestDigitalOceanProvider_NewProvider — alias "do" and "digitalocean" both resolve
  • TestDigitalOceanProvider_MissingToken — clear error when token unset
  • TestDigitalOceanProvider_Deploy_CreatesNewApp — POST /v2/apps called with app name
  • TestDigitalOceanProvider_Deploy_UpdatesExistingApp — PUT /v2/apps/{id} called
  • TestDigitalOceanProvider_HealthCheck — live_url fetched and polled

All pass: GOWORK=off go test ./cmd/wfctl/... -run DigitalOcean

Context

BMW's infra.yaml declares provider: digitalocean; wfctl was erroring with "unsupported deploy provider". This unblocks BMW deploys.

🤖 Generated with Claude Code

Adds `digitalocean` (alias `do`) to the wfctl deploy provider registry.

- `digitaloceanProvider.Deploy` upserts to the DO App Platform REST API:
  GET /v2/apps?name=X to detect existing; POST to create, PUT to update.
  Converts ServiceConfig (expose, scaling, secrets) into a minimal doAppSpec.
- `digitaloceanProvider.HealthCheck` fetches live_url from GET /v2/apps/{id}
  and delegates to the shared pollHealthCheck helper.
- Auth via DIGITALOCEAN_TOKEN env var; returns a clear error when unset.
- Five tests covering: provider registration, missing token, create-new,
  update-existing, and health check — all using httptest.NewServer stubs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 20, 2026 19:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds a new digitalocean (alias do) deploy provider to wfctl so CI deployments can target DigitalOcean App Platform, including app upsert behavior and post-deploy health checks.

Changes:

  • Extend newDeployProvider to recognize digitalocean/do.
  • Implement DigitalOcean App Platform deploy flow (list/find, create, update) and health check live URL resolution.
  • Add httptest-based unit tests covering provider resolution, missing token, create/update deploy, and health check.

Reviewed changes

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

File Description
cmd/wfctl/deploy_providers.go Adds the DigitalOcean deploy provider implementation and wires it into provider selection.
cmd/wfctl/deploy_providers_test.go Adds unit tests for DigitalOcean provider behavior using mock HTTP servers.

Comment thread cmd/wfctl/deploy_providers.go Outdated
Comment on lines +748 to +758
if err == nil && liveURL != "" {
hcPath := cfg.Env.HealthCheck.Path
fullURL := strings.TrimRight(liveURL, "/") + "/" + strings.TrimLeft(hcPath, "/")
hcCopy := *cfg.Env.HealthCheck
hcCopy.Path = fullURL
envCopy := *cfg.Env
envCopy.HealthCheck = &hcCopy
cfgCopy := cfg
cfgCopy.Env = &envCopy
return pollHealthCheck(ctx, cfgCopy)
}

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

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

HealthCheck ignores errors from fetchLiveURL (it only proceeds when err == nil). If the DO API call fails, the code falls back to pollHealthCheck with a relative path, which pollHealthCheck rewrites to http://localhost/... and will almost certainly fail for DigitalOcean deploys. Prefer returning an error (or at least surfacing a warning) when fetchLiveURL fails so the health check doesn’t silently target the wrong host.

Suggested change
if err == nil && liveURL != "" {
hcPath := cfg.Env.HealthCheck.Path
fullURL := strings.TrimRight(liveURL, "/") + "/" + strings.TrimLeft(hcPath, "/")
hcCopy := *cfg.Env.HealthCheck
hcCopy.Path = fullURL
envCopy := *cfg.Env
envCopy.HealthCheck = &hcCopy
cfgCopy := cfg
cfgCopy.Env = &envCopy
return pollHealthCheck(ctx, cfgCopy)
}
if err != nil {
return fmt.Errorf("fetch DigitalOcean live URL for health check: %w", err)
}
if liveURL == "" {
return fmt.Errorf("fetch DigitalOcean live URL for health check: empty live URL")
}
hcPath := cfg.Env.HealthCheck.Path
fullURL := strings.TrimRight(liveURL, "/") + "/" + strings.TrimLeft(hcPath, "/")
hcCopy := *cfg.Env.HealthCheck
hcCopy.Path = fullURL
envCopy := *cfg.Env
envCopy.HealthCheck = &hcCopy
cfgCopy := cfg
cfgCopy.Env = &envCopy
return pollHealthCheck(ctx, cfgCopy)

Copilot uses AI. Check for mistakes.
Comment on lines +418 to +431
var postBody []byte
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/v2/apps"):
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(doListAppsResponse{Apps: []doApp{}})
case r.Method == http.MethodPost && r.URL.Path == "/v2/apps":
postBody, _ = io.ReadAll(r.Body)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(doAppResponse{App: doApp{ID: "new-app-1"}})
default:
http.Error(w, "unexpected: "+r.Method+" "+r.URL.Path, http.StatusInternalServerError)
}

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

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

This test captures postBody from the httptest handler goroutine and then reads it from the test goroutine without synchronization. With go test -race this will be flagged as a data race. Use a channel, sync.Mutex, or atomic.Value to safely pass the captured body back to the test.

Copilot uses AI. Check for mistakes.
Comment on lines +454 to +468
var putPath string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/v2/apps"):
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(doListAppsResponse{Apps: []doApp{
{ID: "existing-1", Spec: doAppSpec{Name: "myapp"}},
}})
case r.Method == http.MethodPut:
putPath = r.URL.Path
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(doAppResponse{App: doApp{ID: "existing-1"}})
default:
http.Error(w, "unexpected: "+r.Method+" "+r.URL.Path, http.StatusInternalServerError)
}

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

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

This test captures putPath inside the httptest handler goroutine and reads it in the test goroutine without synchronization. This is a data race under go test -race. Use a channel / mutex / atomic to transfer the observed request path safely.

Copilot uses AI. Check for mistakes.
Comment on lines +638 to +640
func (p *digitaloceanProvider) findApp(ctx context.Context, token, name string) (string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, p.doBase()+"/v2/apps?name="+name, nil)
if err != nil {

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

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

findApp builds the request URL by concatenating "?name="+name without URL-escaping. App names containing spaces or reserved characters will produce an invalid request (and it also allows query injection). Build the URL using net/url (url.Values / QueryEscape) instead of string concatenation.

Copilot uses AI. Check for mistakes.
Comment on lines +585 to +602
instanceCount := 1
if len(cfg.Services) == 1 {
for _, svc := range cfg.Services {
if svc.Scaling != nil && svc.Scaling.Replicas > 0 {
instanceCount = svc.Scaling.Replicas
}
}
}

httpPort := 8080
if len(cfg.Services) == 1 {
for _, svc := range cfg.Services {
if len(svc.Expose) > 0 {
httpPort = svc.Expose[0].Port
}
}
}

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

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

buildAppSpec silently falls back to a single service spec (using defaults for port/replicas) when len(cfg.Services) > 1. In multi-service CI configs this will deploy an incomplete/incorrect app without any warning. Either implement multi-service mapping for DigitalOcean App Platform or return a clear error when multiple services are provided.

Copilot uses AI. Check for mistakes.
@github-actions

github-actions Bot commented Apr 20, 2026

Copy link
Copy Markdown

⏱ Benchmark Results

No significant performance regressions detected.

benchstat comparison (baseline → PR)
## benchstat: baseline → PR
baseline-bench.txt:245: parsing iteration count: invalid syntax
baseline-bench.txt:299772: parsing iteration count: invalid syntax
baseline-bench.txt:623503: parsing iteration count: invalid syntax
baseline-bench.txt:951806: parsing iteration count: invalid syntax
baseline-bench.txt:1285542: parsing iteration count: invalid syntax
baseline-bench.txt:1561990: parsing iteration count: invalid syntax
benchmark-results.txt:245: parsing iteration count: invalid syntax
benchmark-results.txt:272385: parsing iteration count: invalid syntax
benchmark-results.txt:583777: parsing iteration count: invalid syntax
benchmark-results.txt:895247: parsing iteration count: invalid syntax
benchmark-results.txt:1209747: parsing iteration count: invalid syntax
benchmark-results.txt:1507108: parsing iteration count: invalid syntax
goos: linux
goarch: amd64
pkg: github.com/GoCodeAlone/workflow/dynamic
cpu: AMD EPYC 7763 64-Core Processor                
                            │ baseline-bench.txt │        benchmark-results.txt        │
                            │       sec/op       │    sec/op      vs base              │
InterpreterCreation-4              3.316m ± 190%   3.391m ± 186%       ~ (p=0.394 n=6)
ComponentLoad-4                    3.543m ±   1%   3.645m ±   1%  +2.86% (p=0.002 n=6)
ComponentExecute-4                 1.926µ ±   1%   1.964µ ±   2%  +1.97% (p=0.002 n=6)
PoolContention/workers-1-4         1.079µ ±   3%   1.112µ ±   2%  +3.01% (p=0.004 n=6)
PoolContention/workers-2-4         1.077µ ±   2%   1.116µ ±   2%  +3.57% (p=0.002 n=6)
PoolContention/workers-4-4         1.079µ ±   1%   1.126µ ±   0%  +4.31% (p=0.002 n=6)
PoolContention/workers-8-4         1.085µ ±   1%   1.124µ ±   2%  +3.64% (p=0.002 n=6)
PoolContention/workers-16-4        1.093µ ±   1%   1.129µ ±   2%  +3.30% (p=0.002 n=6)
ComponentLifecycle-4               3.591m ±   4%   3.901m ±   2%  +8.63% (p=0.002 n=6)
SourceValidation-4                 2.228µ ±   1%   2.312µ ±   1%  +3.75% (p=0.002 n=6)
RegistryConcurrent-4               781.5n ±   3%   814.8n ±   1%  +4.26% (p=0.002 n=6)
LoaderLoadFromString-4             3.581m ±   1%   3.718m ±   3%  +3.84% (p=0.002 n=6)
geomean                            17.37µ          18.02µ         +3.77%

                            │ baseline-bench.txt │        benchmark-results.txt         │
                            │        B/op        │     B/op      vs base                │
InterpreterCreation-4               2.027Mi ± 0%   2.027Mi ± 0%       ~ (p=1.000 n=6)
ComponentLoad-4                     2.180Mi ± 0%   2.180Mi ± 0%       ~ (p=0.961 n=6)
ComponentExecute-4                  1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-1-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-2-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-4-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-8-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-16-4         1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
ComponentLifecycle-4                2.183Mi ± 0%   2.183Mi ± 0%       ~ (p=0.790 n=6)
SourceValidation-4                  1.984Ki ± 0%   1.984Ki ± 0%       ~ (p=1.000 n=6) ¹
RegistryConcurrent-4                1.133Ki ± 0%   1.133Ki ± 0%       ~ (p=1.000 n=6) ¹
LoaderLoadFromString-4              2.182Mi ± 0%   2.182Mi ± 0%       ~ (p=0.123 n=6)
geomean                             15.25Ki        15.25Ki       +0.00%
¹ all samples are equal

                            │ baseline-bench.txt │        benchmark-results.txt        │
                            │     allocs/op      │  allocs/op   vs base                │
InterpreterCreation-4                15.68k ± 0%   15.68k ± 0%       ~ (p=1.000 n=6)
ComponentLoad-4                      18.02k ± 0%   18.02k ± 0%       ~ (p=1.000 n=6)
ComponentExecute-4                    25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-1-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-2-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-4-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-8-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-16-4           25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
ComponentLifecycle-4                 18.07k ± 0%   18.07k ± 0%       ~ (p=1.000 n=6) ¹
SourceValidation-4                    32.00 ± 0%    32.00 ± 0%       ~ (p=1.000 n=6) ¹
RegistryConcurrent-4                  2.000 ± 0%    2.000 ± 0%       ~ (p=1.000 n=6) ¹
LoaderLoadFromString-4               18.06k ± 0%   18.06k ± 0%       ~ (p=1.000 n=6) ¹
geomean                               183.3         183.3       +0.00%
¹ all samples are equal

pkg: github.com/GoCodeAlone/workflow/middleware
                                  │ baseline-bench.txt │       benchmark-results.txt       │
                                  │       sec/op       │   sec/op     vs base              │
CircuitBreakerDetection-4                  283.2n ± 0%   287.2n ± 1%  +1.38% (p=0.002 n=6)
CircuitBreakerExecution_Success-4          21.53n ± 0%   21.54n ± 1%       ~ (p=0.933 n=6)
CircuitBreakerExecution_Failure-4          65.89n ± 2%   65.92n ± 1%       ~ (p=1.000 n=6)
geomean                                    73.79n        74.15n       +0.48%

                                  │ baseline-bench.txt │       benchmark-results.txt        │
                                  │        B/op        │    B/op     vs base                │
CircuitBreakerDetection-4                 144.0 ± 0%     144.0 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Success-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Failure-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                              ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                  │ baseline-bench.txt │       benchmark-results.txt        │
                                  │     allocs/op      │ allocs/op   vs base                │
CircuitBreakerDetection-4                 1.000 ± 0%     1.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Success-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Failure-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                              ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/module
                                 │ baseline-bench.txt │       benchmark-results.txt        │
                                 │       sec/op       │    sec/op     vs base              │
JQTransform_Simple-4                     870.1n ± 26%   882.4n ± 29%       ~ (p=0.310 n=6)
JQTransform_ObjectConstruction-4         1.428µ ±  0%   1.474µ ±  1%  +3.26% (p=0.002 n=6)
JQTransform_ArraySelect-4                3.261µ ±  0%   3.458µ ±  2%  +6.04% (p=0.002 n=6)
JQTransform_Complex-4                    37.61µ ±  1%   38.70µ ±  0%  +2.89% (p=0.002 n=6)
JQTransform_Throughput-4                 1.755µ ±  1%   1.830µ ±  1%  +4.25% (p=0.002 n=6)
SSEPublishDelivery-4                     74.49n ±  1%   73.07n ±  1%  -1.91% (p=0.002 n=6)
geomean                                  1.646µ         1.690µ        +2.63%

                                 │ baseline-bench.txt │        benchmark-results.txt         │
                                 │        B/op        │     B/op      vs base                │
JQTransform_Simple-4                   1.273Ki ± 0%     1.273Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ObjectConstruction-4       1.773Ki ± 0%     1.773Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ArraySelect-4              2.625Ki ± 0%     2.625Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Complex-4                  16.22Ki ± 0%     16.22Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Throughput-4               1.984Ki ± 0%     1.984Ki ± 0%       ~ (p=1.000 n=6) ¹
SSEPublishDelivery-4                     0.000 ± 0%       0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                             ²                 +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                 │ baseline-bench.txt │       benchmark-results.txt        │
                                 │     allocs/op      │ allocs/op   vs base                │
JQTransform_Simple-4                     10.00 ± 0%     10.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ObjectConstruction-4         15.00 ± 0%     15.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ArraySelect-4                30.00 ± 0%     30.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Complex-4                    324.0 ± 0%     324.0 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Throughput-4                 17.00 ± 0%     17.00 ± 0%       ~ (p=1.000 n=6) ¹
SSEPublishDelivery-4                     0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                             ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/schema
                                    │ baseline-bench.txt │       benchmark-results.txt       │
                                    │       sec/op       │   sec/op     vs base              │
SchemaValidation_Simple-4                    1.141µ ± 5%   1.129µ ± 4%       ~ (p=0.818 n=6)
SchemaValidation_AllFields-4                 1.673µ ± 3%   1.698µ ± 5%       ~ (p=0.240 n=6)
SchemaValidation_FormatValidation-4          1.597µ ± 3%   1.599µ ± 1%       ~ (p=0.699 n=6)
SchemaValidation_ManySchemas-4               1.821µ ± 2%   1.797µ ± 3%       ~ (p=0.221 n=6)
geomean                                      1.535µ        1.532µ       -0.19%

                                    │ baseline-bench.txt │       benchmark-results.txt        │
                                    │        B/op        │    B/op     vs base                │
SchemaValidation_Simple-4                   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_AllFields-4                0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_FormatValidation-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_ManySchemas-4              0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                                ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                    │ baseline-bench.txt │       benchmark-results.txt        │
                                    │     allocs/op      │ allocs/op   vs base                │
SchemaValidation_Simple-4                   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_AllFields-4                0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_FormatValidation-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_ManySchemas-4              0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                                ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/store
                                   │ baseline-bench.txt │       benchmark-results.txt        │
                                   │       sec/op       │    sec/op     vs base              │
EventStoreAppend_InMemory-4                1.216µ ±  8%   1.353µ ± 23%       ~ (p=0.310 n=6)
EventStoreAppend_SQLite-4                  1.413m ±  5%   1.302m ±  3%  -7.89% (p=0.002 n=6)
GetTimeline_InMemory/events-10-4           13.59µ ±  1%   14.03µ ±  2%  +3.21% (p=0.002 n=6)
GetTimeline_InMemory/events-50-4           75.15µ ± 13%   79.09µ ± 21%       ~ (p=0.065 n=6)
GetTimeline_InMemory/events-100-4          119.0µ ±  1%   123.9µ ±  3%  +4.13% (p=0.002 n=6)
GetTimeline_InMemory/events-500-4          611.8µ ±  0%   634.6µ ±  1%  +3.73% (p=0.002 n=6)
GetTimeline_InMemory/events-1000-4         1.252m ±  2%   1.314m ±  1%  +4.97% (p=0.002 n=6)
GetTimeline_SQLite/events-10-4             106.2µ ±  1%   109.8µ ±  1%  +3.44% (p=0.002 n=6)
GetTimeline_SQLite/events-50-4             244.3µ ±  0%   253.6µ ±  2%  +3.79% (p=0.002 n=6)
GetTimeline_SQLite/events-100-4            412.2µ ±  1%   423.8µ ±  1%  +2.81% (p=0.002 n=6)
GetTimeline_SQLite/events-500-4            1.769m ±  1%   1.801m ±  1%  +1.85% (p=0.002 n=6)
GetTimeline_SQLite/events-1000-4           3.437m ±  1%   3.514m ±  0%  +2.24% (p=0.002 n=6)
geomean                                    216.1µ         222.9µ        +3.15%

                                   │ baseline-bench.txt │         benchmark-results.txt         │
                                   │        B/op        │     B/op       vs base                │
EventStoreAppend_InMemory-4                  804.5 ± 7%     769.5 ± 10%       ~ (p=0.567 n=6)
EventStoreAppend_SQLite-4                  1.984Ki ± 1%   1.985Ki ±  2%       ~ (p=0.461 n=6)
GetTimeline_InMemory/events-10-4           7.953Ki ± 0%   7.953Ki ±  0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-50-4           46.62Ki ± 0%   46.62Ki ±  0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-100-4          94.48Ki ± 0%   94.48Ki ±  0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-500-4          472.8Ki ± 0%   472.8Ki ±  0%       ~ (p=0.567 n=6)
GetTimeline_InMemory/events-1000-4         944.3Ki ± 0%   944.3Ki ±  0%       ~ (p=0.429 n=6)
GetTimeline_SQLite/events-10-4             16.74Ki ± 0%   16.74Ki ±  0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-50-4             87.14Ki ± 0%   87.14Ki ±  0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-100-4            175.4Ki ± 0%   175.4Ki ±  0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-500-4            846.1Ki ± 0%   846.1Ki ±  0%       ~ (p=0.123 n=6)
GetTimeline_SQLite/events-1000-4           1.639Mi ± 0%   1.639Mi ±  0%       ~ (p=0.405 n=6)
geomean                                    67.45Ki        67.20Ki        -0.37%
¹ all samples are equal

                                   │ baseline-bench.txt │        benchmark-results.txt        │
                                   │     allocs/op      │  allocs/op   vs base                │
EventStoreAppend_InMemory-4                  7.000 ± 0%    7.000 ± 0%       ~ (p=1.000 n=6) ¹
EventStoreAppend_SQLite-4                    53.00 ± 0%    53.00 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-10-4             125.0 ± 0%    125.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-50-4             653.0 ± 0%    653.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-100-4           1.306k ± 0%   1.306k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-500-4           6.514k ± 0%   6.514k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-1000-4          13.02k ± 0%   13.02k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-10-4               382.0 ± 0%    382.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-50-4              1.852k ± 0%   1.852k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-100-4             3.681k ± 0%   3.681k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-500-4             18.54k ± 0%   18.54k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-1000-4            37.29k ± 0%   37.29k ± 0%       ~ (p=1.000 n=6) ¹
geomean                                     1.162k        1.162k       +0.00%
¹ all samples are equal

Benchmarks run with go test -bench=. -benchmem -count=6.
Regressions ≥ 20% are flagged. Results compared via benchstat.

…w fixes)

- Add injectable *http.Client field to digitaloceanProvider with 2-min default
- Replace all http.DefaultClient.Do calls with p.httpClient().Do
- Use url.QueryEscape for app name in GET /v2/apps query param
- Warn when fetchLiveURL fails in HealthCheck instead of silently falling back
- Log warning when deploying multiple services via DO App Platform

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@intel352

Copy link
Copy Markdown
Contributor Author

Pivoting: Task 5 reimplemented DO App Platform logic that already exists in workflow-plugin-digitalocean's AppPlatformDriver. The correct architectural path is to make wfctl ci run --phase deploy plugin-aware so workflow-plugin-digitalocean's IaCProvider + DeployDriverProvider extension is the source of truth. Closing this PR; follow-up task will do the plugin-delegation refactor.

@intel352 intel352 closed this Apr 20, 2026
@intel352 intel352 deleted the feat/deploy-provider-digitalocean branch April 20, 2026 19:34
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