Files
repospawner/internal/jobs/state.go
T
unkin-agent f1bcb8cd3a
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Add the initial repospawner service
repospawner turns JSON new-repo requests into terraform-git pull requests
via kubernetes Jobs, follows those PRs to merge and optionally activates
the repository in Woodpecker.
2026-08-30 14:33:31 +10:00

143 lines
3.8 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) {
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
}