Skip to content

Decouple admin UI from Go binary: serve via static.fileserver with external assets#42

Merged
intel352 merged 3 commits intomainfrom
copilot/decouple-admin-ui-static-assets
Feb 22, 2026
Merged

Decouple admin UI from Go binary: serve via static.fileserver with external assets#42
intel352 merged 3 commits intomainfrom
copilot/decouple-admin-ui-static-assets

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 22, 2026

The admin UI was embedded into the Go binary via go:embed, requiring a full binary rebuild to update UI assets and bloating the binary with static files.

Changes

module/api_workflow_ui.go

  • Removed //go:embed all:ui_dist, uiAssets embed.FS, and ExtractUIAssets()
  • Removed embedded file server registration from RegisterRoutes()static.fileserver module handles static serving

admin/admin.go

  • Removed WriteUIAssets() (wrote embedded assets to a temp dir on startup)
  • Removed InjectUIRoot() (moved to a local helper in cmd/server/main.go)
  • Dropped os and module imports

cmd/server/main.go

  • Added --admin-ui-dir flag; env override ADMIN_UI_DIR wired into applyEnvOverrides
  • mergeAdminConfig now calls injectUIRoot only when the flag/env is explicitly set, leaving the path in config.yaml as the default
  • mergeAdminConfig no longer takes *serverApp (temp dir cleanup no longer needed)

admin/config.yaml

  • static.fileserver root changed from "module/ui_dist" to "./ui/dist" (standard build output path)

Usage

# Point to a separately-built admin UI artifact
ADMIN_UI_DIR=/path/to/ui/dist ./workflow-server

# or via flag
./workflow-server --admin-ui-dir /path/to/ui/dist

The admin UI can now be built, versioned, and deployed independently of the Go binary. Updating the UI no longer requires a binary release.

Original prompt

This section details on the original issue you should resolve

<issue_title>Decouple Admin UI from Go Binary: Serve as Static Asset via static.fileserver</issue_title>
<issue_description>Decouple the admin UI (workflow/ui/) from the workflow server binary so it is built and served via static.fileserver just like any application UI.

Tasks

  • Remove go:embed from module/ui_dist/ and module/api_workflow_ui.go.
  • Build admin UI separately in CI; produce artifact for Docker and deployment.
  • Allow admin/config.yaml (or flag/env) to specify root path for admin UI assets.
  • Remove admin.WriteUIAssets and admin.InjectUIRoot.
  • Ensure static.fileserver config is clear and matches admin config.
  • Update deployment docs to describe standalone admin UI build and serve process.

Acceptance

  • Admin UI can be deployed and updated independent of Go binary.
  • Serving mechanism matches that of application UIs (including SPA fallback).

Copilot Agent assigned (can be tackled in parallel).

Linked to broader UI refactor effort.</issue_description>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 2 commits February 22, 2026 09:36
… flag

Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
…ternal assets

Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
Copilot AI changed the title [WIP] Decouple Admin UI from Go binary for static file serving Decouple admin UI from Go binary: serve via static.fileserver with external assets Feb 22, 2026
Copilot AI requested a review from intel352 February 22, 2026 09:39
@intel352 intel352 marked this pull request as ready for review February 22, 2026 09:47
@intel352 intel352 merged commit 35ed0e5 into main Feb 22, 2026
10 of 11 checks passed
@intel352 intel352 deleted the copilot/decouple-admin-ui-static-assets branch February 22, 2026 09:49
intel352 added a commit that referenced this pull request Apr 24, 2026
…egistry, typed gRPC args, migrate image, teardown

Five features bundled into v0.19.0 for shared config-file shape (wfctl.yaml +
.wfctl-lock.yaml) and release boundary. Each addresses architectural debt
surfaced during BMW tonight's deploy blocker chain.

Features:
- A. Plugin manifest + lockfile split (tasks #42/#43)
- B. Multi-registry + IaCProvider.EnsureRegistryAuth (task #48)
- C. Typed-args refactor for IaCProvider gRPC (task #41)
- D. Official workflow-migrate Docker image (task #49)
- E. wfctl infra teardown with mandatory dry-run + --approve flag (new)

Non-goals: constraint-based plugin resolution (v0.20.0), transitive plugin
deps, OCI chart/artifact registries, cross-registry mirroring.

Autonomous pipeline target: v0.19.0 after BMW post-teardown stabilizes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
intel352 added a commit that referenced this pull request Apr 24, 2026
* docs: v0.19.0 architectural cleanup design — plugin manifest, multi-registry, typed gRPC args, migrate image, teardown

Five features bundled into v0.19.0 for shared config-file shape (wfctl.yaml +
.wfctl-lock.yaml) and release boundary. Each addresses architectural debt
surfaced during BMW tonight's deploy blocker chain.

Features:
- A. Plugin manifest + lockfile split (tasks #42/#43)
- B. Multi-registry + IaCProvider.EnsureRegistryAuth (task #48)
- C. Typed-args refactor for IaCProvider gRPC (task #41)
- D. Official workflow-migrate Docker image (task #49)
- E. wfctl infra teardown with mandatory dry-run + --approve flag (new)

Non-goals: constraint-based plugin resolution (v0.20.0), transitive plugin
deps, OCI chart/artifact registries, cross-registry mirroring.

Autonomous pipeline target: v0.19.0 after BMW post-teardown stabilizes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: v0.19.0 design — add Features F, G, H (outputs, verify, secret sinks)

Scope expanded from 5 to 7 features per user feedback on BMW CI gap audit:
- F. wfctl infra outputs with masked-by-default sensitivity + GHA ::add-mask::
- G. wfctl deploy verify with multi-target healthcheck + retry/timeout gate
- H. Declarative secret sinks (outputs.<field>.sinks[]) — plaintext never
  leaves wfctl process; built-in github_secret + github_env handlers;
  aws/gcp/azure sinks via plugin fan-out in v0.19.x

Motivation: BMW's Capture staging DB URL step uses doctl + awk + gh secret
set shell pipeline, leaking DATABASE_URL plaintext through stdout/env/argv.
Declarative sink pattern (like terraform's output-to-secret-manager) writes
the value in-process directly to the GitHub secrets API with libsodium
encryption. Matches user's stated principle: "if BMW CI has provider-specific
shell, fix it in workflow/wfctl so the CI stays declarative."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: v0.19.0 implementation plan — 7 features × 9 phases

Matches design doc 2026-04-24-v0.19.0-architectural-cleanup-design.md:
- Phase 1 alpha.1: Feature A (plugin manifest + lockfile)
- Phase 2 alpha.2: Feature C client-side (typed gRPC args)
- Phase 3 (DO plugin v0.8.0): Feature C server-side + integration tests
- Phase 4 alpha.3: Feature B (multi-registry)
- Phase 5 (DO plugin v0.8.1): Feature B server-side (EnsureRegistryAuth)
- Phase 6a rc1: Feature D (workflow-migrate image)
- Phase 6b rc2: Feature E (wfctl infra teardown)
- Phase 6c rc3: Features F + G + H (outputs + verify + sinks)
- Phase 7: v0.19.0 final + changelog + docs
- Phase 8: Plugin fan-out (aws/gcp/azure/tofu) in parallel
- Phase 9: BMW migration PR (after v0.19.0 stabilizes)

Timing: all phases can merge independently; final v0.19.0 tag and Phase 9
hold until BMW's tonight deploy chain reaches prod /healthz green (task #26).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: address PR #474 review — reconcile feature count, flag naming, source task column

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
intel352 added a commit that referenced this pull request Apr 24, 2026
…nfra_output (#476)

* docs: v0.19.0 architectural cleanup design — plugin manifest, multi-registry, typed gRPC args, migrate image, teardown

Five features bundled into v0.19.0 for shared config-file shape (wfctl.yaml +
.wfctl-lock.yaml) and release boundary. Each addresses architectural debt
surfaced during BMW tonight's deploy blocker chain.

Features:
- A. Plugin manifest + lockfile split (tasks #42/#43)
- B. Multi-registry + IaCProvider.EnsureRegistryAuth (task #48)
- C. Typed-args refactor for IaCProvider gRPC (task #41)
- D. Official workflow-migrate Docker image (task #49)
- E. wfctl infra teardown with mandatory dry-run + --approve flag (new)

Non-goals: constraint-based plugin resolution (v0.20.0), transitive plugin
deps, OCI chart/artifact registries, cross-registry mirroring.

Autonomous pipeline target: v0.19.0 after BMW post-teardown stabilizes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: v0.19.0 design — add Features F, G, H (outputs, verify, secret sinks)

Scope expanded from 5 to 7 features per user feedback on BMW CI gap audit:
- F. wfctl infra outputs with masked-by-default sensitivity + GHA ::add-mask::
- G. wfctl deploy verify with multi-target healthcheck + retry/timeout gate
- H. Declarative secret sinks (outputs.<field>.sinks[]) — plaintext never
  leaves wfctl process; built-in github_secret + github_env handlers;
  aws/gcp/azure sinks via plugin fan-out in v0.19.x

Motivation: BMW's Capture staging DB URL step uses doctl + awk + gh secret
set shell pipeline, leaking DATABASE_URL plaintext through stdout/env/argv.
Declarative sink pattern (like terraform's output-to-secret-manager) writes
the value in-process directly to the GitHub secrets API with libsodium
encryption. Matches user's stated principle: "if BMW CI has provider-specific
shell, fix it in workflow/wfctl so the CI stays declarative."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: v0.19.0 implementation plan — 7 features × 9 phases

Matches design doc 2026-04-24-v0.19.0-architectural-cleanup-design.md:
- Phase 1 alpha.1: Feature A (plugin manifest + lockfile)
- Phase 2 alpha.2: Feature C client-side (typed gRPC args)
- Phase 3 (DO plugin v0.8.0): Feature C server-side + integration tests
- Phase 4 alpha.3: Feature B (multi-registry)
- Phase 5 (DO plugin v0.8.1): Feature B server-side (EnsureRegistryAuth)
- Phase 6a rc1: Feature D (workflow-migrate image)
- Phase 6b rc2: Feature E (wfctl infra teardown)
- Phase 6c rc3: Features F + G + H (outputs + verify + sinks)
- Phase 7: v0.19.0 final + changelog + docs
- Phase 8: Plugin fan-out (aws/gcp/azure/tofu) in parallel
- Phase 9: BMW migration PR (after v0.19.0 stabilizes)

Timing: all phases can merge independently; final v0.19.0 tag and Phase 9
hold until BMW's tonight deploy chain reaches prod /healthz green (task #26).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: address PR #474 review — reconcile feature count, flag naming, source task column

* docs: v0.18.9 phase-continuation design — env-resolution consistency

BMW deploy run 24888583717 created a duplicate DO App Platform app because
wfctl infra apply used env-resolved name "bmw-staging" while wfctl ci run
--phase deploy used base module name "bmw-app". Both paths call driver.Read
by name; with different names they find different resources (or none) and
each calls Create, producing duplicates.

Root cause: cmd/wfctl/deploy_providers.go:769 reads m.Name directly after
ResolveForEnv has been applied. Same class as v0.18.7's Task #32 fix but
in the deploy-phase code path.

Fix: refactor resolveModCfg closure to return *ResolvedModule, use
resolved.Name at call sites. Audit + patch infra_output source resolution
(task #56) with the same pattern. Ship as v0.18.9.

Does not require state-sharing between IaC and CI phases; the bug is about
names, not state. Both phases use driver.Read by name; aligning the names
aligns the lookups.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: v0.18.9 phase-continuation implementation plan

9 tasks across Phase 1 (core fixes: deploy_providers.go + infra_secrets.go
+ regression tests) and Phase 2 (release + BMW unblock: PR, merge, tag,
BMW bump, teardown, redeploy).

Same-class fix as v0.18.7 Task #32: env-resolved Name used consistently
wherever modules are consumed. Target: v0.18.9 hotfix; unblocks BMW
staging deploy from run 24888583717 duplicate-resource failure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(wfctl): ci run deploy uses env-resolved module name (not base)

Refactored resolveModCfg closure in deploy_providers.go to return
*config.ResolvedModule so callers see both resolved.Name (env-override
lifted from Config["name"]) and resolved.Config. All three call sites
(iac.provider lookup, findByType, fallback loop) now read resolved.Name
instead of m.Name.

Same class as v0.18.7 Task #32 fix for ResourceSpec.Name — env override
of Config["name"] was lifted into ResolvedModule.Name but deploy_providers.go
read m.Name directly, ignoring the override. Caused BMW deploy run
24888583717 to create duplicate DO apps (bmw-app vs bmw-staging).

Regression tested via:
- TestPluginDeployProvider_UsesEnvResolvedName (new, was failing)
- TestPluginDeployProvider_FallsBackToModuleNameWhenNoEnv (new, baseline)

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

* fix(wfctl): infra_output source module name flows through env resolution

Introduces resolveInfraOutput(wfCfg, source, envName, stateOutputs)
which translates the base module name in a "module.field" source string
to its env-resolved name before looking up state. State is persisted
under the env-resolved name (e.g. "bmw-staging-db"), so "bmw-database.uri"
with --env staging now correctly finds the state entry.

syncInfraOutputSecrets now accepts wfCfg and envName so the new
resolution is applied for every infra_output secret in the generate list.
The call site in infra.go (runInfraApply) loads the workflow config and
passes it through.

Closes task #56. Regression tested via:
- TestInfraOutput_EnvResolvesModuleSource (new, was failing)
- TestInfraOutput_NoEnvUsesBaseName (new, baseline)

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

* docs: CHANGELOG v0.18.9 entry

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

* fix(wfctl): stateKeys actually sorts keys (comment matched implementation)

Agent-Logs-Url: https://github.com/GoCodeAlone/workflow/sessions/a0429849-a053-4485-914d-ccb115be94e8

Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>

* fix(wfctl): address 4 Copilot round-1 findings on v0.18.9 (#476)

- resolveInfraOutput: ResolveForEnv ok=false now errors (config error)
  instead of silently falling back to base module name — prevents
  the env-resolution fix from being bypassed on misconfigured envs
- stateKeys: add sort.Strings so error messages list available modules
  in deterministic order (comment already said "sorted")
- infra.go: surface config.LoadFromFile error instead of discarding it —
  silent failure would regress env resolution to the pre-fix nil-wfCfg path
- CHANGELOG: replace "Closes task #60" (ambiguous GitHub issue ref) with
  "Root cause from BMW deploy run 24888583717"

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

* fix(wfctl): accurate error message + test for explicitly-disabled module in resolveInfraOutput

Agent-Logs-Url: https://github.com/GoCodeAlone/workflow/sessions/3accbfdf-259b-4b98-a44e-8b538d3f5857

Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>

* fix(wfctl): gate LoadFromFile on envName + infra_output presence (#476)

Skip config.LoadFromFile when env resolution is not needed:
- envName="" → no env resolution, wfCfg=nil is correct
- no infra_output generators → syncInfraOutputSecrets ignores wfCfg

Avoids unnecessary file I/O on every infra apply when the caller
has no infra_output secrets or is not running with --env.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
intel352 added a commit that referenced this pull request Apr 24, 2026
…face) (#477)

* design: wfctl deploy-log observability (v0.18.10)

Introduces optional Troubleshooter interface on ResourceDriver.  Drivers that
can explain their own failures (DO App Platform in v0.7.8, other clouds
later) implement Troubleshoot(ctx, ref, failureMsg) returning structured
[]Diagnostic. wfctl invokes it automatically after a health-check timeout or
deploy error and renders diagnostics in CI-provider-agnostic group blocks
plus a Markdown summary to $GITHUB_STEP_SUMMARY (and equivalents).

Scope:
- workflow v0.18.10 — interface, wfctl wiring, gRPC default-UNIMPLEMENTED,
  CI group emitter, tests.
- workflow-plugin-digitalocean v0.7.8 — AppPlatformDriver.Troubleshoot via
  godo deployments + per-phase logs.
- BMW bump — single PR after both upstream tags.

Non-goals v0.18.10: generic StreamLogs API; AWS/GCP/Azure implementations;
real-time streaming. All deferred to v0.19.0 alongside tasks #42 and #63.

Includes initial uncommitted draft of the Troubleshooter/Diagnostic types in
interfaces/iac_resource_driver.go (from impl-migrations' pre-pipeline draft;
adopted as design anchor).

Closes task #64.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(wfctl): observability pass — progress logging + auto-troubleshoot on health-check timeout

Add live progress output to the plugin health-check poll loop so users
can see what's happening during long deploys instead of silence followed
by "timed out":

- Print start message: "→ health poll: waiting for <name> (timeout: 10m)"
- Every poll: emit timestamped status when message changes
- Heartbeat every 30 s when no new status arrives (silent-wait bug fix)
- On success: "✓ healthy (elapsed)"

On timeout, emit a structured failure block:
  ❌ Deploy health check timed out for "bmw-staging" after 10m0s
     Last observed status: no deployment found
     Recent deployments (via provider API):
       • dep-abc  phase=ERROR          14:45:03  — image pull failed: ...

The failure block is populated by the new optional interfaces.Troubleshooter
interface (added in b7d0ac1). Drivers that implement Troubleshoot() return
[]interfaces.Diagnostic; wfctl calls it automatically with a 15 s deadline.
Drivers that don't implement it produce no extra output (graceful degradation).

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

* plan: wfctl deploy-log observability implementation (v0.18.10 + DO v0.7.8 + BMW bump)

19-task implementation plan covering:
- Phase 1 (tasks 1-10): workflow v0.18.10 — Troubleshooter interface, gRPC
  dispatch, CI emitters (GHA/GitLab/Jenkins/CircleCI), step-summary writer,
  failure-path wiring in deploy_providers + infra_apply, full test coverage,
  PR review, merge, tag.
- Phase 2 (tasks 11-17): workflow-plugin-digitalocean v0.7.8 —
  AppPlatformDriver.Troubleshoot via godo deployments + per-phase logs,
  cause extraction, gRPC dispatch, tests, PR, merge, tag.
- Phase 3 (tasks 18-19): BMW bump setup-wfctl + DO plugin pins.

Ownership: impl-migrations (workflow core), impl-digitalocean-2 (DO plugin),
impl-bmw-2 (BMW bump), team-lead (tags + BMW merge), spec+code reviewers.

TDD with frequent commits; each task has failing-test-first steps + exact
code samples. Dependency: Task 14 blocked by Task 10; Task 18 blocked by
Task 17.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* interfaces: add Detail field + compile-time Troubleshooter check

* wfctl: Troubleshoot gRPC dispatch with Unimplemented fallback

* wfctl: CIGroupEmitter with provider detection (GHA/GitLab/Jenkins/CircleCI)

* wfctl: step-summary Markdown writer with golden tests

* wfctl: call Troubleshoot after deploy health-check failure

* wfctl: call Troubleshoot after infra apply failure

* wfctl: e2e tests for Troubleshoot wiring

* docs: CHANGELOG v0.18.10

* fix(wfctl): address 3 Copilot findings on health-poll observability

- lastProgress initialized to start (not zero) so heartbeat fires after
  healthPollProgressInterval, not immediately on first loop iteration
- pollCtx.Done() now checks Err() to distinguish context.Canceled
  (parent Ctrl-C / pipeline abort) from context.DeadlineExceeded
  (our own timeout), returning a plain cancelled error in the former case
- healthPollTimeout restores legacy returned error text format
  (no 'after N' suffix) for grep-based parser compatibility; elapsed
  duration is now only in the human-readable stderr print block

* fix(wfctl): address 5 Copilot findings on observability re-review

- ci_output: gitlabEmitter now stores sectionID in GroupStart and reuses
  it in GroupEnd so GitLab section folds close correctly (was generating
  a new random ID on each call)
- ci_output_test: verify start/end section IDs match via regex capture
- infra_apply: applyWithProviderAndStore accepts io.Writer for diagnostic
  output (replaces hardcoded os.Stderr) enabling test capture
- infra_apply: derive meaningful ResourceRef (name+type from plan.Actions
  or specs) before calling troubleshootAfterFailure instead of empty ref
- infra_apply_troubleshoot_test: add plainFailProvider (no Troubleshooter)
  to actually exercise the non-troubleshooter no-op path; assert stderr
  output contains group markers and diagnostic cause
- interfaces/troubleshooter_test: TestDiagnostic_JSONRoundtrip now
  marshals/unmarshals and asserts all fields including At and Detail

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

* fix(wfctl): document inert troubleshoot hook in infra_apply

Hook 2 (applyWithProviderAndStore failure path) passes an IaCProvider to
troubleshootAfterFailure whose type assertion against Troubleshooter always
returns false — the call is currently a no-op. Add a TODO(v0.18.11) comment
documenting the gap so it's explicit rather than silent.

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

* fix(wfctl): use 0o600 for GITHUB_STEP_SUMMARY file (gosec G302)

golangci-lint gosec rule G302 requires file permissions ≤ 0600.
The summary file is written to a CI-runner-managed path from
GITHUB_STEP_SUMMARY; 0644 had world-read which triggered the lint.
Change to 0600 and add nolint comment explaining the path is env-controlled.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
intel352 added a commit that referenced this pull request Apr 24, 2026
…ry (#480)

* design: typed ProviderID validation at wfctl ↔ plugin boundaries (v0.18.11)

Optional ProviderIDValidator interface on ResourceDriver declares the
shape of provider-specific identifiers (UUID, domain name, ARN, freeform).
wfctl validates at two points: soft-warn before driver.Update/Delete to
give the driver's self-heal a chance, and hard-fail before state write
when a driver returns a malformed ProviderID for its declared format.

This is Level 1 of the layered validation space: bounded, backward-
compatible, ships in one pair of PRs (workflow v0.18.11 +
workflow-plugin-digitalocean v0.7.9). Levels 2-4 (typed wrapper, proto
oneof, JSON Schema on Config) fold into v0.19.0 with tasks #41 + #42.

BMW's state corruption pattern becomes impossible to reintroduce: if any
future driver bug returns a name-shaped value for a UUID-declared
ProviderID, wfctl fails loudly on output validation within one apply
cycle instead of the bug sticking around silently.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* plan: typed ProviderID validation implementation (v0.18.11 + v0.7.9 + BMW bump)

16-task implementation plan covering:

Phase 1 (Tasks 1-8, workflow v0.18.11, impl-migrations):
- ProviderIDFormat enum + optional ProviderIDValidator interface
- ValidateProviderID + per-format validators (UUID, DomainName, ARN)
- wfctl input-side soft-warn validation
- wfctl output-side hard-fail validation before state write
- 4 integration tests covering warn/reject/freeform/backward-compat
- CHANGELOG + PR + merge + tag

Phase 2 (Tasks 9-14, DO plugin v0.7.9, impl-digitalocean-2):
- go.mod bump to v0.18.11
- Every DO driver declares ProviderIDFormat (UUID / DomainName / Freeform)
- State-heal pattern replicated from AppPlatformDriver (v0.7.8) across
  remaining UUID drivers with per-driver integration tests
- CHANGELOG + PR + merge + tag

Phase 3 (Tasks 15-16, BMW bump, impl-bmw-2 + team-lead):
- Single PR: setup-wfctl v0.18.10.1 → v0.18.11 + DO plugin v0.7.8 → v0.7.9

TDD discipline: each task has exact files, failing-test-first steps,
regression-verification step, commit messages. DRY via shared isUUIDLike
helper (from v0.7.8) and shared integration-test harness (parameterized
across drivers).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* interfaces: add ProviderIDFormat enum + ProviderIDValidator interface

* interfaces: ValidateProviderID + per-format validators (UUID, domain, ARN)

Add idformat.go with ValidateProviderID dispatch function and three
unexported validators: validateUUID (positional, no-alloc), validateDomainName
(RFC 1035 relaxed), and validateARN (6-segment colon split). Unknown and
unrecognized formats always return true for forward compatibility. 49
table-driven test cases in idformat_test.go cover all formats and edge cases.

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

* wfctl: input-side (soft-warn) + output-side (hard-fail) ProviderID validation

Add infra_validation.go with two helpers:
- validateInputProviderIDs: iterates update/delete plan actions, probes
  ResourceDriver for ProviderIDValidator, and logs WARN when the current
  state ProviderID is malformed. Soft-warn so the driver's self-heal path
  can recover.
- validateOutputProviderID: called in the result loop before state write;
  rejects malformed ProviderIDs for strict formats (UUID/DomainName/ARN)
  with a hard error, preventing corrupt data from reaching the state store.

Both helpers no-op when the driver does not implement ProviderIDValidator.

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

* wfctl: integration tests for ProviderID validation wiring (7 cases)

Add infra_apply_validation_test.go covering:
- InputValidation_Warns: stale name-as-ProviderID triggers WARN log
- InputValidation_NoWarnForCreate: create actions are not validated
- InputValidation_ValidUUIDNoWarn: well-formed UUID does not trigger WARN
- OutputValidation_RejectsBadProviderID: malformed ProviderID returns error
- OutputValidation_SkipsFreeform: freeform format passes any non-empty value
- NoValidator_BackwardCompat: drivers without ProviderIDValidator pass through
- FakeValidationProvider_ResourceDriverReturnsDriver: harness sanity check

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

* docs: CHANGELOG v0.18.11 — typed ProviderID validation

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

* fix(lint): address 4 golangci-lint findings on v0.18.11 validation code

- infra_validation.go: range-over-slice by index to avoid 136-byte copy
  per PlanAction (gocritic rangeValCopy)
- infra_validation.go: log swallowed ResourceDriver error in
  validateOutputProviderID (nilerr)
- idformat.go: validateUUID iterates bytes directly (s[i]) instead of
  runes to eliminate byte(c) G115 conversion risk
- idformat.go: rewrite validateDomainName char check via De Morgan's law
  to satisfy staticcheck QF1001

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <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.

Decouple Admin UI from Go Binary: Serve as Static Asset via static.fileserver

2 participants