-
Notifications
You must be signed in to change notification settings - Fork 0
feat(wfctl): deployment state tracking, config diff, and resource correlation #166
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,193 @@ | ||||||||||||||||
| package main | ||||||||||||||||
|
|
||||||||||||||||
| import ( | ||||||||||||||||
| "crypto/sha256" | ||||||||||||||||
| "encoding/json" | ||||||||||||||||
| "fmt" | ||||||||||||||||
| "os" | ||||||||||||||||
| "time" | ||||||||||||||||
|
|
||||||||||||||||
| "github.com/GoCodeAlone/workflow/config" | ||||||||||||||||
| ) | ||||||||||||||||
|
|
||||||||||||||||
| const deployStateSchemaVersion = 1 | ||||||||||||||||
|
|
||||||||||||||||
| // DeployedModuleState records what was deployed for a single module. | ||||||||||||||||
| type DeployedModuleState struct { | ||||||||||||||||
| // Type is the module type string (e.g. "storage.sqlite"). | ||||||||||||||||
| Type string `json:"type"` | ||||||||||||||||
| // Stateful indicates whether this module manages persistent state. | ||||||||||||||||
| Stateful bool `json:"stateful"` | ||||||||||||||||
| // ResourceID is the infrastructure resource identifier generated for this | ||||||||||||||||
| // module (e.g. "database/prod-orders-db"). | ||||||||||||||||
| ResourceID string `json:"resourceId,omitempty"` | ||||||||||||||||
| // Config is a snapshot of the module's config at deploy time. | ||||||||||||||||
| Config map[string]any `json:"config,omitempty"` | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // DeployedPipelineState records what was deployed for a single pipeline. | ||||||||||||||||
| type DeployedPipelineState struct { | ||||||||||||||||
| // Trigger is the pipeline trigger type (e.g. "http"). | ||||||||||||||||
| Trigger string `json:"trigger"` | ||||||||||||||||
| // Path is the HTTP path if the trigger is HTTP-based. | ||||||||||||||||
| Path string `json:"path,omitempty"` | ||||||||||||||||
| // Method is the HTTP method if the trigger is HTTP-based. | ||||||||||||||||
| Method string `json:"method,omitempty"` | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // DeployedResources is the top-level resource map inside a DeploymentState. | ||||||||||||||||
| type DeployedResources struct { | ||||||||||||||||
| Modules map[string]DeployedModuleState `json:"modules,omitempty"` | ||||||||||||||||
| Pipelines map[string]DeployedPipelineState `json:"pipelines,omitempty"` | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // DeploymentState is the full state manifest written after a successful deploy. | ||||||||||||||||
| // It is serialised to deployment.state.json alongside the workflow config. | ||||||||||||||||
| type DeploymentState struct { | ||||||||||||||||
| // Version is the manifest format version (currently "1"). | ||||||||||||||||
| Version string `json:"version"` | ||||||||||||||||
| // ConfigHash is a SHA-256 hex digest of the config file contents at deploy time. | ||||||||||||||||
| ConfigHash string `json:"configHash"` | ||||||||||||||||
| // DeployedAt is the RFC 3339 timestamp of the deployment. | ||||||||||||||||
| DeployedAt time.Time `json:"deployedAt"` | ||||||||||||||||
| // ConfigFile is the path to the workflow config file that was deployed. | ||||||||||||||||
| ConfigFile string `json:"configFile"` | ||||||||||||||||
| // Resources contains per-module and per-pipeline state records. | ||||||||||||||||
| Resources DeployedResources `json:"resources"` | ||||||||||||||||
| // SchemaVersion is an integer version for the state file format. | ||||||||||||||||
| SchemaVersion int `json:"schemaVersion"` | ||||||||||||||||
| // Migrations lists migration IDs that have been applied. | ||||||||||||||||
| Migrations []string `json:"migrations,omitempty"` | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // SaveState writes the DeploymentState to a JSON file at path. | ||||||||||||||||
| func SaveState(state *DeploymentState, path string) error { | ||||||||||||||||
| data, err := json.MarshalIndent(state, "", " ") | ||||||||||||||||
| if err != nil { | ||||||||||||||||
| return fmt.Errorf("marshal deployment state: %w", err) | ||||||||||||||||
| } | ||||||||||||||||
| if err := os.WriteFile(path, data, 0640); err != nil { //nolint:gosec // G306: deploy state file | ||||||||||||||||
| return fmt.Errorf("write deployment state: %w", err) | ||||||||||||||||
| } | ||||||||||||||||
| return nil | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // LoadState reads and deserialises a DeploymentState from a JSON file at path. | ||||||||||||||||
| // Returns an error if the file does not exist or cannot be parsed. | ||||||||||||||||
| func LoadState(path string) (*DeploymentState, error) { | ||||||||||||||||
| data, err := os.ReadFile(path) | ||||||||||||||||
| if err != nil { | ||||||||||||||||
| return nil, fmt.Errorf("read deployment state: %w", err) | ||||||||||||||||
| } | ||||||||||||||||
| var state DeploymentState | ||||||||||||||||
| if err := json.Unmarshal(data, &state); err != nil { | ||||||||||||||||
| return nil, fmt.Errorf("parse deployment state: %w", err) | ||||||||||||||||
| } | ||||||||||||||||
| return &state, nil | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // BuildStateFromConfig constructs a DeploymentState from a loaded WorkflowConfig. | ||||||||||||||||
| // configFile is the original config file path (used for display and hashing). | ||||||||||||||||
| // namespace is used when generating resource IDs (may be empty). | ||||||||||||||||
| // migrations is the optional list of already-applied migration IDs. | ||||||||||||||||
| func BuildStateFromConfig(cfg *config.WorkflowConfig, configFile, namespace string, migrations []string) (*DeploymentState, error) { | ||||||||||||||||
| // Hash the config file if it exists. | ||||||||||||||||
| configHash := "" | ||||||||||||||||
| if configFile != "" { | ||||||||||||||||
| h, err := hashFile(configFile) | ||||||||||||||||
| if err == nil { | ||||||||||||||||
| configHash = "sha256:" + h | ||||||||||||||||
| } | ||||||||||||||||
|
Comment on lines
+98
to
+100
|
||||||||||||||||
| if err == nil { | |
| configHash = "sha256:" + h | |
| } | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to compute hash for config file %q: %w", configFile, err) | |
| } | |
| configHash = "sha256:" + h |
Copilot
AI
Feb 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment says “Deep-copy the config map”, but the implementation only shallow-copies the top-level map; nested maps/slices remain shared and can still be mutated from the original config. Either update the comment to match reality or perform a true deep copy (e.g., via JSON round-trip) to ensure the snapshot is isolated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR description and the comment here claim the deployment state is written “after a successful deploy” / “alongside the workflow config”, but the current
deploycommand implementation doesn’t callBuildStateFromConfig/SaveStateanywhere (and these functions are otherwise unused). Either wire state generation intowfctl deploy ...or adjust the PR description/docs to match what’s actually delivered.