Documentation
¶
Overview ¶
Package requeststatus defines the host-independent request-status model shared by manifest-gen, image-gen, and any future reconciler that wants to tell a requester and a reviewer what a bot-driven change request is doing.
Scope ¶
This package defines values and validation only. It does not render an Update into a Document, call a host API, or import any GitHub package. A separate package per host (e.g. a future githubsurface) implements Surface and owns everything host-specific: entry identity, encoding, transport, and echo suppression. That boundary is enforced by TestImportBoundary, not by convention, so a rehosted or additional surface changes which adapter is bound and nothing in this package.
Model ¶
Phase is the lifecycle state of a running or stopped request: Running, Waiting, NeedsYou, or Failed. Outcome is a terminal result that is not a failure: NoActionNeeded, Complete, or Canceled. An Update carries exactly one of Phase or Outcome, refined by an optional Activity and attempt number, a reason string, and presentation fields. Validator.Validate rejects an incoherent Update before it reaches a renderer or a Surface.
Role names what a Surface is for (the requester's surface or the reviewer's surface), not what host it runs on. EntryKey is the host-independent logical identity of one bot-owned status entry; each Surface adapter encodes that identity in its own host's terms.
Reasons ¶
Reason is a free string rather than an enum. NeedsYou and Failed reasons are owned by each bot's failure table and stay open: Validator only requires them to be nonempty. Running and Waiting reasons are owned by whatever lifecycle authority classifies them (metareconciler today) and are declared at Validator construction with WithReasons; a Running or Waiting reason outside its phase's declared set fails validation. A Validator built with no declared set for a phase accepts no reason for that phase, so a lifecycle authority that forgets to declare its reasons fails closed rather than silently accepting anything.
Example ¶
Example builds a Validator with the Running and Waiting reasons a lifecycle authority owns, then validates a Running and a Complete update.
package main
import (
"fmt"
"chainguard.dev/driftlessaf/requeststatus"
)
func main() {
v, err := requeststatus.NewValidator(
requeststatus.WithReasons(requeststatus.PhaseRunning, "initial", "merge-conflict", "ci-fix"),
requeststatus.WithReasons(requeststatus.PhaseWaiting, "ci", "review"),
requeststatus.WithLinkHosts("github.com"),
)
if err != nil {
fmt.Println("building validator:", err)
return
}
running := requeststatus.Update{
Phase: requeststatus.PhaseRunning,
Reason: "ci-fix",
Activity: requeststatus.ActivityEnrich,
Attempt: 2,
Change: requeststatus.ChangeLink{
Label: "PR #42",
URL: "https://github.com/example/repo/pull/42",
},
}
fmt.Println("running valid:", v.Validate(running) == nil)
complete := requeststatus.Update{
Outcome: requeststatus.OutcomeComplete,
Reason: "merged",
}
fmt.Println("complete valid:", v.Validate(complete) == nil)
// A request surface and a change surface are bound by Role, not by
// host: the same Update can be published to either.
roles := []requeststatus.Role{requeststatus.RoleRequest, requeststatus.RoleChange}
fmt.Println("roles:", roles)
}
Output: running valid: true complete valid: true roles: [request change]
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ValidateSupersededKeys ¶
ValidateSupersededKeys checks that each key in superseded is safe for a Surface to delete by prefix given botKey and the active EntryKey.
A Surface.RemoveSuperseded implementation deletes by prefix match, so a key that is too broad deletes entries the design preserves rather than only the legacy entries it was meant to clean up. ValidateSupersededKeys rejects a key that is:
- empty, which would match every bot-authored entry on a resource;
- not prefixed with "botKey:", which would match another bot's entries; or
- a prefix of active, which would match the active entry this update is publishing.
botKey and active anchor every check above, so both are checked too: a nonempty botKey, and an active key prefixed with "botKey:" and longer.
Example ¶
ExampleValidateSupersededKeys shows that a legacy key for the same bot is safe to delete by prefix, while an empty key, another bot's key, or a prefix of the active key are all rejected because a Surface would delete too much.
package main
import (
"fmt"
"chainguard.dev/driftlessaf/requeststatus"
)
func main() {
const botKey = "manifest-gen"
active := requeststatus.EntryKey("manifest-gen:request-status")
err := requeststatus.ValidateSupersededKeys(botKey, active, "manifest-gen:start", "manifest-gen:failure")
fmt.Println("legacy keys ok:", err == nil)
err = requeststatus.ValidateSupersededKeys(botKey, active, "manifest-gen:")
fmt.Println("bot-key-only key ok:", err == nil)
}
Output: legacy keys ok: true bot-key-only key ok: false
Types ¶
type Activity ¶
type Activity string
Activity refines PhaseRunning with the bot orchestrator's current step. It describes the bot, not the host, and survives every Surface cutover unchanged.
const ( // ActivityAnalyze is understanding the request before generating a // change. ActivityAnalyze Activity = "analyze" // ActivityEnrich is filling in the change's supporting detail. ActivityEnrich Activity = "enrich" // ActivityTestGen is generating the change's test coverage. ActivityTestGen Activity = "testgen" // ActivityValidate is checking the change before it is proposed. ActivityValidate Activity = "validate" )
Activity values.
type ChangeLink ¶
type ChangeLink struct {
// Label is the link text, e.g. "PR #42".
Label string
// URL is the link target. Must be HTTPS on a host declared with
// [WithLinkHosts].
URL string
}
ChangeLink names the proposed change a requester can follow: a pull request on GitHub, a merge request on GitLab. It is presentation only and never routes publication to a Role or a Surface.
type Document ¶
type Document struct{}
Document is the content contract between the model and a Surface adapter: what a status entry says, per audience. It carries no identity, no host encoding, and no timestamp. Omitting a rendered update time keeps two renders of an unchanged Update identical, which is what lets Surface.Publish skip a redundant write; a rendered timestamp would make every publish a distinct body and turn idempotent republication into an unbounded write loop.
The fields that make up a Document's audience-specific content (headline, summary, step list, and so on) are added by the change that builds an Update -> Document renderer; this package defines the contract's properties, not its rendered content.
type EntryKey ¶
type EntryKey string
EntryKey is the host-independent logical identity of one bot-owned status entry, for example "manifest-gen:request-status". Each Surface adapter encodes this key in its own host's terms; the key itself names no host object.
type Option ¶
Option configures a Validator.
func WithLinkHosts ¶
WithLinkHosts declares the hostnames a ChangeLink.URL may target. Calling WithLinkHosts more than once adds to the declared set rather than replacing it. An empty hostname is rejected: a URL like "https:///path" parses with an empty net/url.URL.Hostname, and declaring "" as a host would let that hostless URL through the allowlist it is meant to enforce. Hosts are compared case-insensitively.
func WithReasons ¶
WithReasons declares the reasons Validator.Validate accepts for phase, which must be PhaseRunning or PhaseWaiting. Reasons for PhaseNeedsYou and PhaseFailed are owned by each bot's failure table and stay open: Validate only requires them to be nonempty, and registering them here is an error.
Calling WithReasons for the same phase more than once replaces the previously declared set rather than adding to it.
type Outcome ¶
type Outcome string
Outcome is a terminal result that is not a failure, mutually exclusive with an active Phase.
const ( // OutcomeNoActionNeeded means the request was already satisfied and the // bot made no change. OutcomeNoActionNeeded Outcome = "no_action_needed" // OutcomeComplete means the change merged. OutcomeComplete Outcome = "complete" // OutcomeCanceled means a person stopped the request without merging // it. OutcomeCanceled Outcome = "canceled" )
Outcome values.
type Phase ¶
type Phase string
Phase is the lifecycle state of a request that is still running or has stopped without a terminal Outcome. A string literal converts to Phase implicitly, so Validator.Validate rejects a misspelled value, not the compiler.
const ( // PhaseRunning means the bot is actively generating or regenerating the // change. PhaseRunning Phase = "running" // PhaseWaiting means the bot is waiting on an external signal, such as // checks or human review. PhaseWaiting Phase = "waiting" // PhaseNeedsYou means a person must change the request or make a // decision before the bot can continue. PhaseNeedsYou Phase = "needs_you" // PhaseFailed means the run stopped on an operational or unexpected // error. PhaseFailed Phase = "failed" )
Phase values. This vocabulary is fixed: it does not change when a Surface is rehosted or added.
type Role ¶
type Role string
Role names what a Surface is for — the requester's surface or the reviewer's surface — not what host it runs on. A ChangeLink never selects or implies a Role.
type Surface ¶
type Surface interface {
Publish(ctx context.Context, key EntryKey, doc Document) error
RemoveSuperseded(ctx context.Context, keys ...EntryKey) error
}
Surface is one place a projection publishes a status entry, identified by audience Role rather than by host: the requester's surface, or the reviewer's surface. A concrete adapter (for example a future githubsurface) is the only place a host name appears; this package knows nothing about any host.
Implementations must:
- Upsert exactly one entry per EntryKey: create it on the first Publish for that key, and edit it in place on every later call for the same key.
- Perform no write when the Document to publish is unchanged from the last Document this Surface wrote for that key. Request status is a write-only projection; a Surface that writes on every call turns a benign echo (its own write triggering reconciliation, on a host where the surface is also a reconciliation input) into an unbounded loop.
RemoveSuperseded deletes, best effort, any entries at the given keys. It exists for legacy-comment cleanup and its failure must not fail a concurrent or subsequent Publish. Callers should validate keys with ValidateSupersededKeys before calling RemoveSuperseded, since a too-broad key deletes entries this package's Publish contract requires a Surface to preserve.
Example ¶
ExampleSurface publishes the same Document twice for the same EntryKey and shows that the second call performs no write, per the Surface contract.
package main
import (
"context"
"fmt"
"chainguard.dev/driftlessaf/requeststatus"
)
// memorySurface is a minimal, non-host-backed Surface used only to
// demonstrate the interface contract: one entry per EntryKey, and no write
// when the Document is unchanged.
type memorySurface struct {
published map[requeststatus.EntryKey]requeststatus.Document
writes int
}
func (s *memorySurface) Publish(_ context.Context, key requeststatus.EntryKey, doc requeststatus.Document) error {
if existing, ok := s.published[key]; ok && existing == doc {
return nil
}
s.published[key] = doc
s.writes++
return nil
}
func (s *memorySurface) RemoveSuperseded(_ context.Context, _ ...requeststatus.EntryKey) error {
return nil
}
func main() {
var surface requeststatus.Surface = &memorySurface{published: map[requeststatus.EntryKey]requeststatus.Document{}}
key := requeststatus.EntryKey("manifest-gen:request-status")
doc := requeststatus.Document{}
ctx := context.Background()
_ = surface.Publish(ctx, key, doc)
_ = surface.Publish(ctx, key, doc)
fmt.Println("writes:", surface.(*memorySurface).writes)
}
Output: writes: 1
type Update ¶
type Update struct {
// Phase is set for an active or stopped-without-outcome request. Empty
// when Outcome is set.
Phase Phase
// Outcome is set for a terminal, non-failure result. Empty when Phase
// is set.
Outcome Outcome
// Activity refines PhaseRunning with the orchestrator's current step.
// Empty unless Phase is PhaseRunning.
Activity Activity
// Attempt is the one-based diagnostics-loop iteration for Activity. It
// is positive when Activity is set and zero otherwise; it is not the
// changemanager commit count.
Attempt int
// Reason classifies why Phase or Outcome holds this value. Required
// and validated against a registered set for PhaseRunning and
// PhaseWaiting; required and open for PhaseNeedsYou and PhaseFailed;
// fixed per Outcome. See [Validator.Validate].
Reason string
// Summary is a short, requester-facing explanation, bounded and
// sanitized by the renderer that builds a Document from this Update.
Summary string
// Details are additional bullet-point explanations, bounded and
// sanitized the same way as Summary.
Details []string
// Change links the proposed change a requester can follow. It is
// presentation only and never selects a Role or a Surface.
Change ChangeLink
}
Update is one host-independent statement of request state. It carries exactly one of Phase or Outcome, never both and never neither; see Validator.Validate for the full coherence contract.
type Validator ¶
type Validator struct {
// contains filtered or unexported fields
}
Validator checks Update values for coherence before they reach a renderer or a Surface. Its zero value from NewValidator with no options accepts no Reason for PhaseRunning or PhaseWaiting and no ChangeLink: both are opt-in and fail closed until configured.
func NewValidator ¶
NewValidator builds a Validator from opts.
func (*Validator) Validate ¶
Validate rejects an incoherent Update. It enforces, in order:
- Exactly one of Phase or Outcome is set, and any set Phase, Outcome, or Activity is a known value.
- Activity is set only when Phase is PhaseRunning.
- Activity and a positive Attempt are both set or both empty.
- PhaseNeedsYou and PhaseFailed carry a nonempty Reason.
- OutcomeNoActionNeeded, OutcomeComplete, and OutcomeCanceled carry their fixed Reason.
- A Reason for PhaseRunning or PhaseWaiting is empty or in the set declared for that phase with WithReasons.
- Change.Label and Change.URL are both set or both empty, and a set URL is HTTPS on a host declared with WithLinkHosts.