Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ jobs:
- uses: actions/checkout@v7
- uses: actions/setup-go@v6
with:
go-version: "1.26.x"
go-version: "1.26.6"
check-latest: true
- name: govulncheck
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<p align="center">
<a href="https://pkg.go.dev/github.com/theworker02/shiftlock"><img src="https://pkg.go.dev/badge/github.com/theworker02/shiftlock.svg" alt="Go Reference"/></a>
<a href="https://github.com/theworker02/shiftlock/actions/workflows/ci.yml"><img src="https://github.com/theworker02/shiftlock/actions/workflows/ci.yml/badge.svg" alt="CI"/></a>
<a href="https://github.com/theworker02/shiftlock/releases/tag/v0.10.0"><img src="https://img.shields.io/github/v/release/theworker02/shiftlock?include_prereleases&sort=semver" alt="Release"/></a>
<a href="https://github.com/theworker02/shiftlock/releases/tag/v0.11.0"><img src="https://img.shields.io/github/v/release/theworker02/shiftlock?include_prereleases&sort=semver" alt="Release"/></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-blue.svg" alt="License"/></a>
</p>

Expand All @@ -22,7 +22,7 @@
·
<a href="https://pkg.go.dev/github.com/theworker02/shiftlock"><strong>Go module docs</strong></a>
·
<a href="https://github.com/theworker02/shiftlock/releases/tag/v0.10.0">v0.10.0</a>
<a href="https://github.com/theworker02/shiftlock/releases/tag/v0.11.0">v0.11.0</a>
·
<a href="docs/architecture.md">Architecture</a>
·
Expand All @@ -41,11 +41,11 @@ workflows, databases, queues, and APIs.
| | |
|---|---|
| **Module path** | [`github.com/theworker02/shiftlock`](https://pkg.go.dev/github.com/theworker02/shiftlock) |
| **Latest release** | [`v0.10.0`](https://pkg.go.dev/github.com/theworker02/shiftlock@v0.10.0) |
| **Latest release** | [`v0.11.0`](https://pkg.go.dev/github.com/theworker02/shiftlock@v0.11.0) |
| **Repository** | [github.com/theworker02/shiftlock](https://github.com/theworker02/shiftlock) |

```bash
go get github.com/theworker02/shiftlock@v0.10.0
go get github.com/theworker02/shiftlock@v0.11.0
```

Supports the current and previous stable Go releases. Core stays stdlib-first;
Expand Down
17 changes: 12 additions & 5 deletions cmd/shiftlock-inspect/impact.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,23 @@ func runImpactPlan(args []string) {

plan := sampleHealthReport().PlanImpact(*failed)
if *jsonOut {
payload := struct {
health.ImpactPlan
Waves []health.Wave `json:"waves"`
}{ImpactPlan: plan, Waves: plan.Waves()}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
_ = enc.Encode(plan)
_ = enc.Encode(payload)
return
}

fmt.Printf("failed=%s blast_radius=%v actions=%d issues=%d\n",
plan.Failed, plan.BlastRadius, len(plan.Actions), len(plan.Issues))
for _, action := range plan.Actions {
fmt.Printf(" [%d] %s %s — %s\n", action.Priority, action.Node, action.Kind, action.Reason)
fmt.Printf("failed=%s blast_radius=%v actions=%d issues=%d waves=%d\n",
plan.Failed, plan.BlastRadius, len(plan.Actions), len(plan.Issues), len(plan.Waves()))
for _, wave := range plan.Waves() {
fmt.Printf("wave %d (priority %d)\n", wave.Index, wave.Priority)
for _, action := range wave.Actions {
fmt.Printf(" [%d] %s %s — %s\n", action.Priority, action.Node, action.Kind, action.Reason)
}
}
}

Expand Down
21 changes: 21 additions & 0 deletions docs/releases/v0.11.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# ShiftLock v0.11.0 — Health Impact Execution Waves

ShiftLock v0.11.0 turns impact-plan actions into ordered execution waves so operators drain, quarantine, and observe in the sequence the planner already ranked.

## Added

- `health.Wave` with index, shared priority, and the actions in that slice.
- `health.ImpactPlan.Waves()` groups planned actions by priority. Lower-index waves should complete first.
- `shiftlock-inspect impact-plan` JSON now includes `waves`; text output prints each wave.

## Compatibility

- v0.10.0 `PlanImpact` action lists, priorities, and JSON fields are unchanged.
- `Waves()` is additive and derived from the existing sorted action list.
- Go 1.25 remains the module toolchain declaration.

## Upgrade

```bash
go get github.com/theworker02/shiftlock@v0.11.0
```
29 changes: 29 additions & 0 deletions health/impact.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,40 @@ type ImpactPlan struct {
Issues []ValidationIssue `json:"issues,omitempty"`
}

// Wave is one execution slice of an impact plan. Actions that share a
// priority run together; lower-index waves should complete first.
type Wave struct {
Index int `json:"index"`
Priority int `json:"priority"`
Actions []PlannedAction `json:"actions"`
}

// Empty reports whether the plan carries no failure context or work items.
func (p ImpactPlan) Empty() bool {
return p.Failed == "" && len(p.BlastRadius) == 0 && len(p.Actions) == 0 && len(p.Issues) == 0
}

// Waves groups planned actions into priority-ordered execution waves.
// Actions are already sorted by PlanImpact; grouping is stable.
func (p ImpactPlan) Waves() []Wave {
if len(p.Actions) == 0 {
return nil
}
waves := make([]Wave, 0)
current := Wave{Index: 0, Priority: p.Actions[0].Priority}
for _, action := range p.Actions {
if action.Priority != current.Priority && len(current.Actions) > 0 {
waves = append(waves, current)
current = Wave{Index: len(waves), Priority: action.Priority}
}
current.Actions = append(current.Actions, action)
}
if len(current.Actions) > 0 {
waves = append(waves, current)
}
return waves
}

// PlanImpact derives blast radius and recommended actions for a failed node.
// Validation issues from the report are included; planning stops early when
// the failed node is missing or unnamed.
Expand Down
37 changes: 37 additions & 0 deletions health/impact_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,43 @@ func TestImpactPlanEmpty(t *testing.T) {
if !plan.Empty() {
t.Fatal("zero plan should be empty")
}
if waves := plan.Waves(); waves != nil {
t.Fatalf("empty plan waves = %#v", waves)
}
}

func TestImpactPlanWavesGroupByPriority(t *testing.T) {
plan := sampleReport().PlanImpact("database")
waves := plan.Waves()
if len(waves) < 2 {
t.Fatalf("expected multiple waves, got %#v", waves)
}
if waves[0].Index != 0 || waves[0].Priority != plan.Actions[0].Priority {
t.Fatalf("first wave = %#v", waves[0])
}
seen := 0
lastPriority := -1
for i, wave := range waves {
if wave.Index != i {
t.Fatalf("wave %d index = %d", i, wave.Index)
}
if lastPriority >= 0 && wave.Priority < lastPriority {
t.Fatalf("waves are not priority-ordered: %#v", waves)
}
lastPriority = wave.Priority
if len(wave.Actions) == 0 {
t.Fatalf("wave %d has no actions", i)
}
for _, action := range wave.Actions {
if action.Priority != wave.Priority {
t.Fatalf("action %#v not in matching wave %#v", action, wave)
}
seen++
}
}
if seen != len(plan.Actions) {
t.Fatalf("wave actions %d != plan actions %d", seen, len(plan.Actions))
}
}

func TestPlanImpactUpstreamObserve(t *testing.T) {
Expand Down
Loading