8786636f7c
Four issues from the review of the initial repospawner service, none of which change the shape of a request or the file terraform-git receives. - Encode status checks as one --check flag per context on the server-to-job hop, so a separator inside a context can no longer turn one context into several; ban commas (and cap lengths) in Validate as well, since a real context never holds one. - Fail a merged request that has waited five minutes for a Woodpecker token that vanished after acceptance, surfacing "woodpecker token unavailable" through the API, instead of warning in the log forever from enabling-ci. Advance now leaves a terminal request alone so the failure sticks. - Hold a per-name lock from the duplicate checks through the store write, so two concurrent submissions of one name cannot both be accepted. - Cap the description at 500 characters and the status checks at 20 contexts of 100 characters each, and mirror the first two caps in the form.
148 lines
4.1 KiB
Go
148 lines
4.1 KiB
Go
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) {
|
|
// A request that already ended badly stays ended: later observations of the
|
|
// jobs that got it there must not walk it back out of a terminal state.
|
|
if r.State == store.StateFailed || r.State == store.StateClosed {
|
|
return r, ActionNone
|
|
}
|
|
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
|
|
}
|