All checks were successful
CI/CD / detect-changes (pull_request) Successful in 4s
CI/CD / test-frontend (pull_request) Has been skipped
CI/CD / test-runners (pull_request) Has been skipped
CI/CD / e2e-smoke (pull_request) Has been skipped
CI/CD / test-go-pg (pull_request) Successful in 2m43s
CI/CD / test-sqlite (pull_request) Successful in 2m52s
CI/CD / build-and-deploy (pull_request) Successful in 2m15s
Consolidate duplicate Go↔Starlark converters into sandbox/convert.go and snapshot parsers into models/snapshot.go. Standardize snapshot creation on wrapped format. Net -393 lines across 21 files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
131 lines
3.7 KiB
Go
131 lines
3.7 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"log"
|
|
|
|
"go.starlark.net/starlark"
|
|
|
|
"armature/sandbox"
|
|
"armature/store"
|
|
)
|
|
|
|
// ── on_advance Hook ─────────────────────────
|
|
//
|
|
// Fires synchronously after a stage transition succeeds.
|
|
// The hook can enrich/transform stage_data or reject the transition.
|
|
//
|
|
// Config in stage_config:
|
|
// {"on_advance": {"package_id": "...", "entry_point": "on_advance"}}
|
|
//
|
|
// Hook receives dict: {stage_data, previous_stage, current_stage, instance_id}
|
|
// Hook returns: {stage_data: {...}} (enriched), None (no change),
|
|
// or {error: "msg"} (reject — caller should handle rollback)
|
|
|
|
// OnAdvanceHookConfig is the on_advance section of stage_config.
|
|
type OnAdvanceHookConfig struct {
|
|
PackageID string `json:"package_id"`
|
|
EntryPoint string `json:"entry_point"`
|
|
}
|
|
|
|
// StageConfigOnAdvance extracts on_advance config from stage_config JSON.
|
|
type StageConfigOnAdvance struct {
|
|
OnAdvance *OnAdvanceHookConfig `json:"on_advance,omitempty"`
|
|
}
|
|
|
|
// OnAdvanceResult is the outcome of firing the on_advance hook.
|
|
type OnAdvanceResult struct {
|
|
EnrichedData json.RawMessage // updated stage_data (nil = no change)
|
|
Error string // non-empty = hook rejected the transition
|
|
}
|
|
|
|
// FireOnAdvanceHook fires the on_advance Starlark hook for a stage transition.
|
|
// Returns nil if no hook is configured or the runner is unavailable.
|
|
func FireOnAdvanceHook(
|
|
ctx context.Context,
|
|
stores store.Stores,
|
|
runner *sandbox.Runner,
|
|
previousStageConfig json.RawMessage,
|
|
instanceID string,
|
|
previousStage, currentStage int,
|
|
stageData json.RawMessage,
|
|
) *OnAdvanceResult {
|
|
if runner == nil || stores.Packages == nil {
|
|
return nil
|
|
}
|
|
|
|
var cfg StageConfigOnAdvance
|
|
if len(previousStageConfig) > 0 {
|
|
_ = json.Unmarshal(previousStageConfig, &cfg)
|
|
}
|
|
if cfg.OnAdvance == nil || cfg.OnAdvance.PackageID == "" || cfg.OnAdvance.EntryPoint == "" {
|
|
return nil
|
|
}
|
|
|
|
pkg, err := stores.Packages.Get(ctx, cfg.OnAdvance.PackageID)
|
|
if err != nil || pkg == nil {
|
|
log.Printf("[workflow-hooks] on_advance: package %s not found", cfg.OnAdvance.PackageID)
|
|
return nil
|
|
}
|
|
|
|
// Build context dict for the hook
|
|
ctxDict := starlark.NewDict(4)
|
|
_ = ctxDict.SetKey(starlark.String("instance_id"), starlark.String(instanceID))
|
|
_ = ctxDict.SetKey(starlark.String("previous_stage"), starlark.MakeInt(previousStage))
|
|
_ = ctxDict.SetKey(starlark.String("current_stage"), starlark.MakeInt(currentStage))
|
|
|
|
// Parse stage_data into Starlark dict
|
|
var dataMap map[string]interface{}
|
|
if json.Unmarshal(stageData, &dataMap) == nil {
|
|
_ = ctxDict.SetKey(starlark.String("stage_data"), sandbox.GoToStarlark(dataMap))
|
|
} else {
|
|
_ = ctxDict.SetKey(starlark.String("stage_data"), starlark.NewDict(0))
|
|
}
|
|
|
|
val, _, err := runner.CallEntryPoint(ctx, pkg, cfg.OnAdvance.EntryPoint,
|
|
starlark.Tuple{ctxDict}, nil, nil)
|
|
if err != nil {
|
|
log.Printf("[workflow-hooks] on_advance hook error: %v", err)
|
|
return nil
|
|
}
|
|
|
|
return parseOnAdvanceResult(val)
|
|
}
|
|
|
|
// parseOnAdvanceResult extracts enriched data or error from the Starlark return value.
|
|
func parseOnAdvanceResult(val starlark.Value) *OnAdvanceResult {
|
|
if val == nil || val == starlark.None {
|
|
return nil
|
|
}
|
|
|
|
d, ok := val.(*starlark.Dict)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
result := &OnAdvanceResult{}
|
|
|
|
// Check for error
|
|
if errVal, found, _ := d.Get(starlark.String("error")); found {
|
|
if s, ok := errVal.(starlark.String); ok {
|
|
result.Error = string(s)
|
|
return result
|
|
}
|
|
}
|
|
|
|
// Check for enriched stage_data
|
|
if sdVal, found, _ := d.Get(starlark.String("stage_data")); found {
|
|
if sd, ok := sdVal.(*starlark.Dict); ok {
|
|
goMap := sandbox.DictToMap(sd)
|
|
if data, err := json.Marshal(goMap); err == nil {
|
|
result.EnrichedData = data
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|