Skip to content

fix(server): implement multi-workflow mode API wiring#131

Merged
intel352 merged 3 commits intomainfrom
fix/issue-71-dead-api-wiring
Feb 23, 2026
Merged

fix(server): implement multi-workflow mode API wiring#131
intel352 merged 3 commits intomainfrom
fix/issue-71-dead-api-wiring

Conversation

@intel352
Copy link
Contributor

Summary

  • Replace dead TODO block in cmd/server/main.go with working multi-workflow mode implementation
  • The TODO listed 7 steps for PostgreSQL-backed multi-tenant mode — all underlying code already existed in store/, api/, and engine_manager.go packages
  • Wires up: PG connection, migrations, stores, admin bootstrap, WorkflowEngineManager, API router, and service registration
  • Prevents single-config path from also executing when --database-dsn is provided

Test plan

  • go build ./cmd/server/ compiles cleanly
  • go test ./cmd/server/ passes all tests
  • golangci-lint run passes with 0 issues
  • Only modifies cmd/server/main.go (1 file, 99 insertions, 29 deletions)

Closes #71

🤖 Generated with Claude Code

Replace the dead TODO block in cmd/server/main.go with a working
implementation that connects to PostgreSQL via store.NewPGStore, runs
database migrations, bootstraps an optional admin user on first run,
starts the api.NewRouter on a dedicated port (--multi-workflow-addr,
default :8090), and shuts down cleanly alongside the single-config
engine.

New flags: --multi-workflow-addr
New imports: apihandler (api package), bcrypt, time

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 23, 2026 11:37
Copy link
Contributor

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

Implements the previously TODO multi-workflow (PostgreSQL-backed) startup path in cmd/server/main.go, wiring database initialization and starting the REST API server when --database-dsn is provided.

Changes:

  • Adds multi-workflow mode wiring: PG store creation, migrations, and optional bootstrap admin creation.
  • Starts the multi-workflow REST API server on a dedicated address/port (--multi-workflow-addr).
  • Introduces WorkflowEngineManager construction for multi-workflow mode and adds related imports/flags.

Comment on lines +1266 to +1269
if *adminEmail != "" && *adminPassword != "" {
_, lookupErr := pgStore.Users().GetByEmail(context.Background(), *adminEmail)
if lookupErr != nil {
hash, hashErr := bcrypt.GenerateFromPassword([]byte(*adminPassword), bcrypt.DefaultCost)
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

Admin bootstrap treats any error from GetByEmail as “user not found” and attempts creation. This can mask real DB/query errors and lead to misleading logs. Please only bootstrap when errors.Is(lookupErr, evstore.ErrNotFound); otherwise handle/return the unexpected error.

Copilot uses AI. Check for mistakes.
Comment on lines +1322 to +1327
go func() {
logger.Info("multi-workflow API listening", "addr", *multiWorkflowAddr)
if sErr := apiServer.ListenAndServe(); sErr != nil && sErr != http.ErrServerClosed {
logger.Error("multi-workflow API server error", "error", sErr)
}
}()
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

ListenAndServe runs in a goroutine and errors are only logged. If the multi-workflow API port is already in use, startup will continue in single-config mode even though multi-workflow mode was requested. Consider failing fast on serve/bind errors (e.g., net.Listen first and exit on failure, or propagate the error via a channel).

Copilot uses AI. Check for mistakes.
Comment on lines +1247 to +1249
// Multi-workflow mode: connect to PostgreSQL, run migrations, start the
// REST API router on a dedicated port alongside the single-config engine.
logger.Info("Starting in multi-workflow mode",
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

PR description mentions mounting the API router at /api/v1 alongside existing routes, but the implementation starts a separate HTTP server on --multi-workflow-addr (dedicated port). Please align the description with the actual behavior, or adjust the implementation to mount the router into the existing server if that’s the intended UX.

Copilot uses AI. Check for mistakes.
Comment on lines +1249 to 1254
logger.Info("Starting in multi-workflow mode",
"database_dsn_set", true,
"jwt_secret_set", *jwtSecret != "",
"admin_email_set", *adminEmail != "",
"api_addr", *multiWorkflowAddr,
)
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

Multi-workflow mode starts the REST API even when --jwt-secret is empty. In this codebase the API signs/validates JWTs with the configured HMAC secret; an empty secret makes tokens forgeable. Please fail fast when *jwtSecret is empty (or generate a secure random secret explicitly for dev-only) before starting the API server.

Copilot uses AI. Check for mistakes.
@intel352
Copy link
Contributor Author

@copilot open a new pull request to apply changes based on the comments in this thread

Copy link
Contributor

Copilot AI commented Feb 23, 2026

@intel352 I've opened a new pull request, #143, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI and others added 2 commits February 23, 2026 09:33
* Initial plan

* fix(server): address review comments on multi-workflow mode wiring

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
Copilot AI review requested due to automatic review settings February 23, 2026 16:21
@intel352 intel352 merged commit 23b07f8 into main Feb 23, 2026
5 checks passed
@intel352 intel352 deleted the fix/issue-71-dead-api-wiring branch February 23, 2026 16:22
Copy link
Contributor

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

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

Comments suppressed due to low confidence (2)

api/workflow_handler.go:286

  • Deploy calls h.engine.DeployWorkflow(...) and then independently updates the workflow status in the store. If the injected engine is workflow.WorkflowEngineManager, it already updates the workflow’s DB status internally, so this causes duplicate updates/version bumps and can create inconsistencies if one update succeeds and the other fails. Prefer a single source of truth: either let the engine runner own the status update and re-fetch/return the record, or remove the engine manager’s internal store mutation and keep the handler update.
	if h.engine != nil {
		if err := h.engine.DeployWorkflow(r.Context(), id); err != nil {
			WriteError(w, http.StatusInternalServerError, "failed to deploy workflow engine")
			return
		}
	}
	wf.Status = store.WorkflowStatusActive
	wf.UpdatedAt = time.Now()
	if err := h.workflows.Update(r.Context(), wf); err != nil {
		WriteError(w, http.StatusInternalServerError, "internal error")

api/workflow_handler.go:317

  • Stop has the same double-write issue as Deploy: calling the engine runner and then updating the workflow status in the handler can lead to duplicate DB updates/version bumps and mismatched state if one step fails. Consolidate status persistence into either the engine runner or the handler (and return the store’s canonical record).
	if h.engine != nil {
		if err := h.engine.StopWorkflow(r.Context(), id); err != nil {
			WriteError(w, http.StatusInternalServerError, "failed to stop workflow engine")
			return
		}
	}
	wf.Status = store.WorkflowStatusStopped
	wf.UpdatedAt = time.Now()
	if err := h.workflows.Update(r.Context(), wf); err != nil {
		WriteError(w, http.StatusInternalServerError, "internal error")

Comment on lines 1556 to 1557
secret = "dev-secret-change-me"
logger.Error("No JWT secret configured — using insecure default; set JWT_SECRET env var or -jwt-secret flag")
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

runMultiWorkflow still exists and is no longer referenced from main(), but it’s being kept partially updated (e.g., switching to apihandler.*). This risks the inlined multi-workflow startup logic and runMultiWorkflow drifting further apart (and runMultiWorkflow currently allows an insecure default JWT secret). Consider deleting runMultiWorkflow or refactoring main() to call it so there’s a single implementation to maintain.

Suggested change
secret = "dev-secret-change-me"
logger.Error("No JWT secret configured — using insecure default; set JWT_SECRET env var or -jwt-secret flag")
log.Fatal("No JWT secret configured — refusing to start; set JWT_SECRET env var or -jwt-secret flag")

Copilot uses AI. Check for mistakes.
"encoding/json"
"errors"
"flag"
"errors"
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

The import block includes errors twice, which will make this file fail to compile (imported and not used/duplicate import). Remove the duplicate and keep a single errors import.

Suggested change
"errors"

Copilot uses AI. Check for mistakes.
Comment on lines +1363 to +1366
if *adminEmail != "" && *adminPassword != "" {
_, lookupErr := pgStore.Users().GetByEmail(context.Background(), *adminEmail)
switch {
case errors.Is(lookupErr, evstore.ErrNotFound):
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

Admin bootstrap looks up the user with context.Background() and uses the raw --admin-email value. Consider reusing the existing timed context (dbCtx) and normalizing the email (trim + lower-case) to match the auth handlers’ behavior, otherwise the bootstrap user may not be able to log in if the stored email casing differs.

Copilot uses AI. Check for mistakes.
Comment on lines +1381 to +1382
if createErr := pgStore.Users().Create(context.Background(), adminUser); createErr != nil {
logger.Warn("multi-workflow mode: failed to create admin user (may already exist)", "error", createErr)
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

Admin bootstrap creates the user with context.Background(), so these DB operations ignore the connection/migration timeout and any shutdown cancellation. Use the same bounded context used for startup (or a derived context) so a hung DB call can’t block shutdown indefinitely.

Copilot uses AI. Check for mistakes.
Comment on lines 22 to 29

// OAuth providers keyed by provider name (e.g. "google", "okta").
OAuthProviders map[string]*OAuthProviderConfig

// Engine is an optional engine lifecycle manager used by the workflow
// deploy/stop endpoints to actually start and stop workflow engines.
Engine EngineRunner
}
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

The PR description says only cmd/server/main.go is modified, but this PR also changes the api/ package (router + workflow handler). Please update the PR description/test plan to reflect the additional files/behavior being changed, since it impacts review scope and release notes.

Copilot uses AI. Check for mistakes.
Comment on lines +277 to +282
if h.engine != nil {
if err := h.engine.DeployWorkflow(r.Context(), id); err != nil {
WriteError(w, http.StatusInternalServerError, "failed to deploy workflow engine")
return
}
}
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

New behavior when h.engine != nil (calling DeployWorkflow/StopWorkflow and handling errors) isn’t covered by existing tests in api/workflow_handler_test.go (which only exercises the nil-engine path). Add tests for the engine-runner path, including success and failure cases, to prevent regressions and to validate HTTP status mapping.

Copilot generated this review using guidance from organization custom instructions.
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.

Audit and resolve dead API wiring TODO in cmd/server/main.go

3 participants