This document describes the state machine that governs staging operations in suve. The staging system uses a Redux-like pattern with pure reducer functions for predictable state transitions.
The staging system manages two types of state:
- Entry State: Tracks value changes (create/update/delete operations)
- Tag State: Tracks tag modifications (add/remove operations)
| State | Description |
|---|---|
NotStaged |
No pending changes for this resource |
Create |
Resource will be created (doesn't exist in AWS) |
Update |
Resource value will be modified |
Delete |
Resource will be deleted |
stateDiagram-v2
[*] --> NotStaged
NotStaged --> Create: add (resource NOT in AWS)
NotStaged --> Update: edit (value != AWS)
NotStaged --> NotStaged: edit (value = AWS) [auto-skip]
NotStaged --> Delete: delete (resource in AWS)
Create --> Create: add (update draft)
Create --> Create: edit (update draft)
Create --> NotStaged: delete [also unstage tags]
Create --> NotStaged: reset
Update --> Update: edit (value != AWS)
Update --> NotStaged: edit (value = AWS) [auto-unstage]
Update --> Delete: delete
Update --> NotStaged: reset
Delete --> NotStaged: reset
Delete --> Delete: delete [no-op]
note right of NotStaged
add (resource in AWS) -> ERROR
delete (resource NOT in AWS) -> ERROR
tag/untag (resource NOT in AWS) -> ERROR
end note
note right of Delete
edit -> ERROR
add -> ERROR
tag/untag -> ERROR
end note
| Current State | Action | Condition | New State | Notes |
|---|---|---|---|---|
| NotStaged | add |
resource NOT in AWS | Create | Stage new resource |
| NotStaged | add |
resource in AWS | ERROR | Use edit instead |
| NotStaged | edit |
value != AWS | Update | Stage modification |
| NotStaged | edit |
value = AWS | NotStaged | Auto-skip (no change needed) |
| NotStaged | delete |
resource in AWS | Delete | Stage deletion |
| NotStaged | delete |
resource NOT in AWS | ERROR | Resource doesn't exist |
| NotStaged | tag/untag |
resource NOT in AWS | ERROR | Resource doesn't exist |
| Create | add |
- | Create | Update draft value |
| Create | edit |
- | Create | Update draft value |
| Create | delete |
- | NotStaged | Unstage + discard tags |
| Create | reset |
- | NotStaged | Unstage only |
| Create | tag/untag |
- | HasChanges | Tags apply on resource creation |
| Update | edit |
value != AWS | Update | Update draft value |
| Update | edit |
value = AWS | NotStaged | Auto-unstage (reverted) |
| Update | delete |
- | Delete | Convert to delete |
| Update | reset |
- | NotStaged | Unstage |
| Delete | edit |
- | ERROR | Must reset first |
| Delete | add |
- | ERROR | Cannot add to delete-staged |
| Delete | delete |
- | Delete | No-op |
| Delete | reset |
- | NotStaged | Unstage |
When editing a resource that is not staged, if the new value matches the current AWS value, the operation is skipped entirely (no staging occurs).
AWS value: "foo"
-> edit "foo" -> Skipped (same as AWS)
When editing a staged resource, if the new value matches the current AWS value, the resource is automatically unstaged.
AWS value: "foo"
-> edit "bar" -> Update staged (value="bar")
-> edit "foo" -> Unstaged (reverted to AWS)
When a Create-staged resource is deleted (unstaged), any associated tag changes are also discarded. This prevents orphaned tag operations that would fail on apply.
-> add /app/new -> Create staged
-> tag env=prod -> Tags staged
-> delete /app/new -> Both entry and tags unstaged
Before staging operations, suve validates that the resource state in AWS is compatible with the requested action:
| Action | Requirement | Error |
|---|---|---|
add |
Resource must NOT exist in AWS | "cannot add: resource already exists, use edit instead" |
delete |
Resource must exist in AWS (or be staged as Create) | "cannot delete: resource not found" |
tag/untag |
Resource must exist in AWS (or be staged as Create) | "cannot tag/untag: resource not found" |
This prevents common mistakes like:
-> add /app/existing -> ERROR (resource exists, use edit)
-> delete /app/missing -> ERROR (resource not found)
-> tag /app/missing -> ERROR (resource not found)
For staged Create resources, delete unstages instead of erroring, and tag/untag is allowed (tags will be applied when the resource is created).
| State | Description |
|---|---|
Empty |
No pending tag changes |
HasChanges |
Has pending tag additions and/or removals |
stateDiagram-v2
[*] --> Empty
Empty --> HasChanges: tag (value != AWS)
Empty --> Empty: tag (value = AWS) [auto-skip]
Empty --> HasChanges: untag (key exists on AWS)
Empty --> Empty: untag (key not on AWS) [auto-skip]
HasChanges --> HasChanges: tag (add/update)
HasChanges --> HasChanges: untag
HasChanges --> Empty: all changes cleared [auto-unstage]
note right of Empty
Resource NOT in AWS
AND NOT staged as Create
-> ERROR for tag/untag
end note
note right of HasChanges
Entry=Delete -> ERROR for tag/untag
end note
Tags are tracked with two sets:
- ToSet: Tags to add or update (key-value pairs)
- ToUnset: Tag keys to remove
| Action | Condition | Result |
|---|---|---|
tag key=value |
key not in AWS or value differs | Add to ToSet |
tag key=value |
key in AWS with same value | Auto-skip |
tag key=value |
key in ToUnset | Remove from ToUnset, add to ToSet |
untag key |
key exists in AWS | Add to ToUnset |
untag key |
key not in AWS | Auto-skip |
untag key |
key in ToSet | Remove from ToSet, add to ToUnset if in AWS |
Tag operations (tag/untag) are blocked when the entry is staged for deletion. This prevents meaningless tag changes on resources that will be deleted.
-> delete /app/config -> Delete staged
-> tag env=prod -> ERROR: cannot modify tags on delete-staged resource
When applying changes, suve checks for conflicts by comparing the BaseModifiedAt timestamp (recorded at staging time) with the current AWS LastModified time.
| Operation | Conflict Condition |
|---|---|
| Create | Resource now exists in AWS |
| Update | AWS modified after staging |
| Delete | AWS modified after staging |
Use --ignore-conflicts to force apply despite conflicts.
The state machine is implemented in internal/staging/transition/:
state.go: State type definitionsaction.go: Action type definitionsreducer.go: Pure reducer functions (ReduceEntry,ReduceTag)executor.go: Persists reducer results to the store
The reducer functions are pure (no side effects) and deterministic, making the staging behavior predictable and testable.