package jobs import ( "git.unkin.net/unkin/repospawner/internal/store" ) // Action is the next Job the reconciler should create for a request. type Action string const ( // ActionNone means the request is either waiting on a running Job or done. ActionNone Action = "" // ActionCreateWatch means the pull request exists and wants following. ActionCreateWatch Action = "watch" // ActionCreateWoodpecker means the merge landed and CI wants enabling. ActionCreateWoodpecker Action = "woodpecker" ) // Advance folds the observed Jobs for one request into its next state and the // Job that should be created next. It is deliberately pure: the reconciler // supplies the observations and performs the action. func Advance(r store.Request, views map[Type]View) (store.Request, Action) { if pr, ok := views[TypePR]; ok { r = applyPR(r, pr) } watch, watching := views[TypeWatch] if watching { r = applyWatch(r, watch) } if r.State == store.StateFailed || r.State == store.StateClosed { return r, ActionNone } // The pull request is open but nothing is following it yet. if r.PRNumber > 0 && !watching && r.State != store.StateMerged && r.State != store.StateEnablingCI && r.State != store.StateReady { r.State = store.StatePROpen return r, ActionCreateWatch } if r.State != store.StateMerged && r.State != store.StateEnablingCI && r.State != store.StateReady { return r, ActionNone } if !r.Woodpecker { r.State = store.StateReady return r, ActionNone } wp, ok := views[TypeWoodpecker] if !ok { if r.State == store.StateReady { // The enablement Job already succeeded and its TTL expired. return r, ActionNone } r.State = store.StateEnablingCI return r, ActionCreateWoodpecker } return applyWoodpecker(r, wp), ActionNone } func applyPR(r store.Request, v View) store.Request { switch { case v.Succeeded: var res PRResult if !DecodeResult(v.Result, &res) || res.PRURL == "" { r.State = store.StateFailed r.Error = "pull request job finished without reporting a pull request" return r } r.PRNumber, r.PRURL = res.PRNumber, res.PRURL if r.State == store.StateOpeningPR || r.State == "" { r.State = store.StatePROpen } case v.Failed: r.State = store.StateFailed r.Error = resultError(v.Result, "pull request job failed") default: if r.State == "" { r.State = store.StateOpeningPR } } return r } func applyWatch(r store.Request, v View) store.Request { switch { case v.Succeeded: var res WatchResult if !DecodeResult(v.Result, &res) { r.State = store.StateFailed r.Error = "watch job finished without reporting an outcome" return r } switch { case res.Merged: if r.State != store.StateEnablingCI && r.State != store.StateReady { r.State = store.StateMerged } case res.Closed: r.State = store.StateClosed default: r.State = store.StateFailed r.Error = resultError(v.Result, "watch job reported neither merge nor close") } case v.Failed: r.State = store.StateFailed r.Error = resultError(v.Result, "watch job failed") default: r.State = store.StatePROpen } return r } func applyWoodpecker(r store.Request, v View) store.Request { switch { case v.Succeeded: var res WoodpeckerResult if !DecodeResult(v.Result, &res) || !res.Enabled { r.State = store.StateFailed r.Error = resultError(v.Result, "woodpecker enablement job reported no activation") return r } r.CIEnabled = true r.State = store.StateReady case v.Failed: r.State = store.StateFailed r.Error = resultError(v.Result, "woodpecker enablement job failed") default: r.State = store.StateEnablingCI } return r } // resultError prefers the job's own error string over the generic fallback. func resultError(raw []byte, fallback string) string { var res struct { Error string `json:"error"` } if DecodeResult(raw, &res) && res.Error != "" { return res.Error } return fallback }