requeststatus

package
v0.10.150 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 1, 2026 License: Apache-2.0 Imports: 4 Imported by: 0

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

func ValidateSupersededKeys(botKey string, active EntryKey, superseded ...EntryKey) error

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 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

type Option func(*Validator) error

Option configures a Validator.

func WithLinkHosts

func WithLinkHosts(hosts ...string) Option

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

func WithReasons(phase Phase, reasons ...string) Option

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.

const (
	// RoleRequest is the requester's and operator's surface, e.g. the
	// stereo issue.
	RoleRequest Role = "request"
	// RoleChange is the reviewer's surface, e.g. the stereo pull request.
	RoleChange Role = "change"
)

Role values.

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

func NewValidator(opts ...Option) (*Validator, error)

NewValidator builds a Validator from opts.

func (*Validator) Validate

func (v *Validator) Validate(u Update) error

Validate rejects an incoherent Update. It enforces, in order:

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL