fix(ci): make the default success() of a job look at its whole chain of needs
- commit
- 27d907d01ecbf918cbb6e1a6492de273a907644b
- author
- brice · · 2026-10-13 03:21 UTC
- parent
- e893110
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +24 -2
M internal/usecase/ci_plan.go +24 -2
| @@ -133,9 +133,31 @@ func result(insts []domain.CIJob) string { | |||
| 133 | 133 | ||
| 134 | 134 | // needsOf computes the needs context and the status functions of a job from | |
| 135 | 135 | // the jobs it needs, and whether they all have ended. | |
| 136 | - | func (rs *runState) needsOf(j *workflow.Job) (needs *expr.Dictionary, st workflow.StatusFuncs, ready bool) { | |
| 136 | + | // | |
| 137 | + | // The needs context holds the direct needs only, but the status functions look | |
| 138 | + | // at the whole chain (wf.Ancestors): a job whose need ran through always() | |
| 139 | + | // after a failure further up is skipped by its default success() (recorded: | |
| 140 | + | // needs-failed-dependency, chain-after-always), and failure() is true when an | |
| 141 | + | // ancestor failed (documented, not recorded). An ancestor beyond the direct | |
| 142 | + | // needs only changes success() and failure(): cancelled() is not recorded for | |
| 143 | + | // them, and a skipped ancestor behind a need that did run is not either. | |
| 144 | + | func (rs *runState) needsOf(wf *workflow.Workflow, j *workflow.Job) (needs *expr.Dictionary, st workflow.StatusFuncs, ready bool) { | |
| 137 | 145 | needs = expr.NewDictionary() | |
| 138 | 146 | st = workflow.StatusFuncs{Success: true, Cancelled: rs.run.CancelRequested} | |
| 147 | + | for _, n := range wf.Ancestors(j.ID) { | |
| 148 | + | var insts []domain.CIJob | |
| 149 | + | for _, i := range rs.instances(n) { | |
| 150 | + | if rs.jobs[i].Ended() { | |
| 151 | + | insts = append(insts, rs.jobs[i]) | |
| 152 | + | } | |
| 153 | + | } | |
| 154 | + | switch result(insts) { | |
| 155 | + | case "failure": | |
| 156 | + | st.Failure, st.Success = true, false | |
| 157 | + | case "cancelled": | |
| 158 | + | st.Success = false | |
| 159 | + | } | |
| 160 | + | } | |
| 139 | 161 | for _, n := range j.Needs { | |
| 140 | 162 | var insts []domain.CIJob | |
| 141 | 163 | for _, i := range rs.instances(n) { | |
| @@ -305,7 +327,7 @@ func (rs *runState) advance(ctx context.Context) error { | |||
| 305 | 327 | if rec.Planned || rec.Ended() || rec.Phase != domain.CIPhaseWaiting { | |
| 306 | 328 | continue | |
| 307 | 329 | } | |
| 308 | - | needs, st, ready := rs.needsOf(wj) | |
| 330 | + | needs, st, ready := rs.needsOf(wf, wj) | |
| 309 | 331 | if !ready { | |
| 310 | 332 | continue | |
| 311 | 333 | } | |
| @@ -133,9 +133,31 @@ func result(insts []domain.CIJob) string { | |||||
| 133 | 133 | ||||
| 134 | // needsOf computes the needs context and the status functions of a job from | 134 | // needsOf computes the needs context and the status functions of a job from | ||
| 135 | // the jobs it needs, and whether they all have ended. | 135 | // the jobs it needs, and whether they all have ended. | ||
| 136 | - | func (rs *runState) needsOf(j *workflow.Job) (needs *expr.Dictionary, st workflow.StatusFuncs, ready bool) { | 136 | + | // |
| 137 | + | // The needs context holds the direct needs only, but the status functions look | |||
| 138 | + | // at the whole chain (wf.Ancestors): a job whose need ran through always() | |||
| 139 | + | // after a failure further up is skipped by its default success() (recorded: | |||
| 140 | + | // needs-failed-dependency, chain-after-always), and failure() is true when an | |||
| 141 | + | // ancestor failed (documented, not recorded). An ancestor beyond the direct | |||
| 142 | + | // needs only changes success() and failure(): cancelled() is not recorded for | |||
| 143 | + | // them, and a skipped ancestor behind a need that did run is not either. | |||
| 144 | + | func (rs *runState) needsOf(wf *workflow.Workflow, j *workflow.Job) (needs *expr.Dictionary, st workflow.StatusFuncs, ready bool) { | |||
| 137 | needs = expr.NewDictionary() | 145 | needs = expr.NewDictionary() | ||
| 138 | st = workflow.StatusFuncs{Success: true, Cancelled: rs.run.CancelRequested} | 146 | st = workflow.StatusFuncs{Success: true, Cancelled: rs.run.CancelRequested} | ||
| 147 | + | for _, n := range wf.Ancestors(j.ID) { | |||
| 148 | + | var insts []domain.CIJob | |||
| 149 | + | for _, i := range rs.instances(n) { | |||
| 150 | + | if rs.jobs[i].Ended() { | |||
| 151 | + | insts = append(insts, rs.jobs[i]) | |||
| 152 | + | } | |||
| 153 | + | } | |||
| 154 | + | switch result(insts) { | |||
| 155 | + | case "failure": | |||
| 156 | + | st.Failure, st.Success = true, false | |||
| 157 | + | case "cancelled": | |||
| 158 | + | st.Success = false | |||
| 159 | + | } | |||
| 160 | + | } | |||
| 139 | for _, n := range j.Needs { | 161 | for _, n := range j.Needs { | ||
| 140 | var insts []domain.CIJob | 162 | var insts []domain.CIJob | ||
| 141 | for _, i := range rs.instances(n) { | 163 | for _, i := range rs.instances(n) { | ||
| @@ -305,7 +327,7 @@ func (rs *runState) advance(ctx context.Context) error { | |||||
| 305 | if rec.Planned || rec.Ended() || rec.Phase != domain.CIPhaseWaiting { | 327 | if rec.Planned || rec.Ended() || rec.Phase != domain.CIPhaseWaiting { | ||
| 306 | continue | 328 | continue | ||
| 307 | } | 329 | } | ||
| 308 | - | needs, st, ready := rs.needsOf(wj) | 330 | + | needs, st, ready := rs.needsOf(wf, wj) |
| 309 | if !ready { | 331 | if !ready { | ||
| 310 | continue | 332 | continue | ||
| 311 | } | 333 | } | ||