feat(api): approvals + ECO workflow API with YAML-configurable workflows

- Add internal/workflow/ package for YAML workflow definitions (Load, LoadAll, Validate)
- Add internal/db/item_approvals.go repository (Create, AddSignature, GetWithSignatures, ListByItemWithSignatures, UpdateState, UpdateSignature)
- Add internal/api/approval_handlers.go with 4 endpoints:
  - GET /{partNumber}/approvals (list approvals with signatures)
  - POST /{partNumber}/approvals (create ECO with workflow + signers)
  - POST /{partNumber}/approvals/{id}/sign (approve or reject)
  - GET /workflows (list available workflow definitions)
- Rule-driven state transitions: any_reject and all_required_approve
- Pack approvals into silo/approvals.json on .kc checkout
- Add WorkflowsConfig to config, load workflows at startup
- Migration 019: add workflow_name column to item_approvals
- Example workflows: engineering-change.yaml, quick-review.yaml
- 7 workflow tests, all passing

Closes #145
This commit is contained in:
Forbes
2026-02-18 19:38:20 -06:00
parent e260c175bf
commit 12ecffdabe
14 changed files with 1091 additions and 10 deletions

View File

@@ -23,6 +23,7 @@ import (
"github.com/kindredsystems/silo/internal/modules"
"github.com/kindredsystems/silo/internal/schema"
"github.com/kindredsystems/silo/internal/storage"
"github.com/kindredsystems/silo/internal/workflow"
"github.com/rs/zerolog"
)
@@ -235,6 +236,19 @@ func main() {
}
}
// Load approval workflow definitions (optional — directory may not exist yet)
var workflows map[string]*workflow.Workflow
if _, err := os.Stat(cfg.Workflows.Directory); err == nil {
workflows, err = workflow.LoadAll(cfg.Workflows.Directory)
if err != nil {
logger.Fatal().Err(err).Str("directory", cfg.Workflows.Directory).Msg("failed to load workflow definitions")
}
logger.Info().Int("count", len(workflows)).Msg("loaded workflow definitions")
} else {
workflows = make(map[string]*workflow.Workflow)
logger.Info().Str("directory", cfg.Workflows.Directory).Msg("workflows directory not found, skipping")
}
// Initialize module registry
registry := modules.NewRegistry()
if err := modules.LoadState(registry, cfg, database.Pool()); err != nil {
@@ -258,7 +272,7 @@ func main() {
// Create API server
server := api.NewServer(logger, database, schemas, cfg.Schemas.Directory, store,
authService, sessionManager, oidcBackend, &cfg.Auth, broker, serverState,
jobDefs, cfg.Jobs.Directory, registry, cfg)
jobDefs, cfg.Jobs.Directory, registry, cfg, workflows)
router := api.NewRouter(server, logger)
// Start background sweepers for job/runner timeouts (only when jobs module enabled)