Documentation
¶
Overview ¶
Package service is the one use-case layer both cmd/hoist and the TUI call — application logic that used to live only in package main, duplicated (and quietly diverging) between the CLI's flags-to-render path and the TUI's own wiring.
It imports internal/engine, internal/config and pkg/* freely, and constructs the same pkg/* clients (git, forge, argo, rollout, k8s, registry) either face needs. It never imports internal/app or any Bubble Tea / Lip Gloss package (charm.land/...): a screen may import service's own value types (AGENTS.md §4.8), but service itself must stay renderer-agnostic, or the CLI would start pulling in a terminal UI it never runs. internal/service/imports_test.go enforces this mechanically rather than by convention alone (AGENTS.md §10 meta-rule 5).
The package is built from a few pieces: Settings (what a run resolved from flags and config, once), Deps (constructor seams a caller injects, so a real invocation and a test share the same shape), Service (the lazy, memoized clients built from Deps), and the repo view (a single current read of the GitOps repo, from the operator's own clone or from a cached view of origin, replacing cmd/hoist's own boot-frozen equivalents).
Index ¶
- func AnyRealEdit(edits []gitops.Edit) bool
- func CheckRepoViewCurrent(ctx context.Context, g git.Git, cloneDir, base, viewedSHA string) error
- func Detail(o engine.Observation) string
- func EnsureArgoApps(s *engine.PromotionState, rc config.RepoConfig) error
- func IsProduction(env string, envs config.EnvsConfig) bool
- func RepoConfigFor(cfg *config.Config, repoFullName string) (config.RepoConfig, bool)
- func SkippedStaging(source, target string, envs config.EnvsConfig) (staging string, skip bool)
- func WarnDeployIntoProduction(pl *gitops.Plan, envs config.EnvsConfig)
- func WarnSkippedStagingProduction(pl *gitops.Plan, envs config.EnvsConfig)
- type ActEvent
- type AlreadyCurrentError
- type AmbiguousError
- type Collectable
- type Deps
- type Drive
- type Driver
- type FileStore
- func (FileStore) Archive(id string) error
- func (FileStore) Claim(repoFullName, targetEnv, id string) (func(), error)
- func (FileStore) Delete(id string) error
- func (FileStore) List() ([]*engine.PromotionState, error)
- func (FileStore) ListArchived() ([]*engine.PromotionState, error)
- func (FileStore) Load(id string) (*engine.PromotionState, error)
- func (FileStore) Save(s *engine.PromotionState) error
- func (FileStore) WorktreeDir(id string) (string, error)
- type GCOpts
- type GCReport
- type Hooks
- type InFlightConflictError
- type ListOpts
- type Listed
- type Mode
- type NotFoundError
- type PlanRequest
- type PlannedChange
- type RepoMode
- type RepoView
- type Resolution
- type ResolveOptions
- type ResumeOpts
- type RunHooks
- type Service
- func (s *Service) Abandon(ctx context.Context, id string) ([]string, error)
- func (s *Service) AbandonWith(ctx context.Context, id string, h Hooks) ([]string, error)
- func (s *Service) Argo(kubeContext string) (argo.Argo, error)
- func (s *Service) ArgoNamed(kubeContext string) (argo.Argo, string, error)
- func (s *Service) CheckOverrides(r *gitops.Repo, from string, overrides map[string]image.Ref) error
- func (s *Service) CleanupLanded(ctx context.Context, st *engine.PromotionState) ([]string, error)
- func (s *Service) Cluster(kubeContext string) (k8s.Cluster, string, error)
- func (s *Service) Collectable(ctx context.Context) (Collectable, error)
- func (s *Service) Find(id string) (*engine.PromotionState, error)
- func (s *Service) FindInFlight(ctx context.Context, repoFullName, targetEnv, skipID string) (*engine.PromotionState, engine.StepStatus, error)
- func (s *Service) FindInFlightForEnv(ctx context.Context, env string, h Hooks) (*engine.PromotionState, error)
- func (s *Service) ForgeFor(ownerRepo string) (forge.Forge, error)
- func (s *Service) GC(ctx context.Context, o GCOpts) (GCReport, error)
- func (s *Service) Git() git.Git
- func (s *Service) HistoryDelta(ctx context.Context, from, to image.Ref) (migrate.Delta, error)
- func (s *Service) HistoryMapped(imageRepo string) bool
- func (s *Service) HistoryRevision(ctx context.Context, ref image.Ref) (migrate.Revision, error)
- func (s *Service) List(ctx context.Context, o ListOpts) ([]Listed, error)
- func (s *Service) ListArchived(repoFullName string) ([]*engine.PromotionState, error)
- func (s *Service) LiveAge(ctx context.Context, occ gitops.Occurrence) (migrate.LineAge, error)
- func (s *Service) LoadRepo(ctx context.Context, mode RepoMode) (RepoView, error)
- func (s *Service) OpenTagSource(imageRepo string) *TagSource
- func (s *Service) OpenWatchApp(r *gitops.Repo, app string) (*Watch, error)
- func (s *Service) OpenWatchFamily(env, family string) (*Watch, error)
- func (s *Service) Plan(ctx context.Context, req PlanRequest) (PlannedChange, error)
- func (s *Service) Preflight(target string, m Mode) error
- func (s *Service) RefreshRepo(ctx context.Context) (RepoView, error)
- func (s *Service) RegistryFor(imageRepo string) (registry.Registry, error)
- func (s *Service) Repo() RepoView
- func (s *Service) RestartDo(ctx context.Context, p restart.Plan, at time.Time) ([]gitops.Workload, error)
- func (s *Service) RestartInterval() time.Duration
- func (s *Service) RestartObserve(ctx context.Context, env string, targets []gitops.Workload, at time.Time) ([]restart.Progress, error)
- func (s *Service) RestartRead(ctx context.Context, env string, targets []gitops.Workload) (restart.Plan, error)
- func (s *Service) Resume(ctx context.Context, id string, o ResumeOpts) (Drive, error)
- func (s *Service) Rollout(kubeContext string) (rollout.Rollout, error)
- func (s *Service) RolloutNamed(kubeContext string) (rollout.Rollout, string, error)
- func (s *Service) RunningImages(ctx context.Context, env string) (map[string][]image.Ref, error)
- func (s *Service) Settings() Settings
- func (s *Service) StartPromotion(ctx context.Context, req StartRequest, h Hooks) (Drive, error)
- type Settings
- func (s Settings) ArgoNamespace() string
- func (s Settings) CheckRestartConfirmed(env, confirm string) error
- func (s Settings) KubeContextFor(rc config.RepoConfig) string
- func (s Settings) PairEnvs(from, to string) (string, string, error)
- func (s Settings) PreflightConfig(m Mode) error
- func (s Settings) PreflightGate(target string, m Mode) error
- func (s Settings) ProductionEnvs() []string
- type StartRequest
- type StateStore
- type TagSource
- func (t *TagSource) Err() error
- func (t *TagSource) GitTags(ctx context.Context) (tags []forge.GitTag, mapped bool, err error)
- func (t *TagSource) HasGitTags() bool
- func (t *TagSource) Meta(ctx context.Context, tag string) (registry.ImageMeta, error)
- func (t *TagSource) RegistryTags(ctx context.Context) ([]string, error)
- type Tick
- type UnconfirmedError
- type UsageError
- type Watch
- type WatchSnapshot
- type WatchWorkload
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func AnyRealEdit ¶
AnyRealEdit reports whether edits contains at least one edit that is not a NoOp (the target already carries exactly the planned reference) — the all-NoOp fast-path guard StartPromotion and `hoist plan`'s own branch-id line (cmd/hoist/main.go's runPlan) share, so a plan whose every edit is already satisfied is recognized identically wherever this codebase asks the question. Exported (unlike this file's other helpers) because runPlan's own display-only use has no promotion to start at all — duplicating this one-line predicate there would be exactly the kind of second copy AGENTS.md §8's "layered checks" section warns drifts from the first. Moved from cmd/hoist/promote.go's anyRealEdit, unchanged.
func CheckRepoViewCurrent ¶
CheckRepoViewCurrent refuses a confirm whose plan (built from a view captured at viewedSHA, whatever moment refreshRepoView last ran to produce it — TUI boot or a since F5) no longer matches origin/<base>'s present tip. Deliberately NOT checkCloneCurrentForBase's per-file blob comparison: that function's whole shape assumes cloneDir's working tree tracks refs/heads/<base>, the local branch — true for an ordinary checkout, false by construction for a cached origin view, which is deliberately checked out from origin, never the local branch. What's left to verify is simpler: re-fetch origin/<base> and compare its SHA to viewedSHA.
viewedSHA takes the CALLER'S already-captured RepoView.SHA rather than re-reading viewDir's current on-disk HEAD: repoViewDir is keyed only by cloneDir, so a LATER LoadRepo(RepoFromOrigin) call (an F5 refresh) checks that same directory out to a new ref IN PLACE — re-deriving the "viewed" SHA from disk at check time would therefore always read whatever the most recent refresh just wrote, making every check trivially pass regardless of which view a plan was actually built from (the fail-open this replaces).
viewedSHA == "" (refreshRepoView itself failed and the caller fell back to the clone, or this is a clone-mode view) skips this check entirely — there is nothing cached to compare against.
func Detail ¶
func Detail(o engine.Observation) string
Detail picks whichever of Observation's message fields is set — Blocked takes precedence since it's the more actionable one when both could apply. Moved from cmd/hoist/promote.go's statusDetail, unchanged; exported because it is now shared by service's own inflight.go and by cmd/hoist's remaining (not-yet-moved) abandon.go/resume.go call sites.
func EnsureArgoApps ¶
func EnsureArgoApps(s *engine.PromotionState, rc config.RepoConfig) error
EnsureArgoApps repairs a state file written before M5 added PromotionState.ArgoApps: JSON decoding an older file leaves the field empty, and ArgoRefreshedStep/ArgoSyncedStep both take their `len(apps) == 0` "no Argo Application in this promotion's plan" success path on an empty ArgoApps — so an upgraded, already-in-flight promotion could be reported complete having never actually checked the Application it edited.
An empty ArgoApps alongside a non-empty Edits is unambiguous evidence of exactly that: engine.ArgoAppNames' own contract (see its doc comment) means a real call against a non-empty edit set can never itself return an empty, non-error slice — every edit's directory maps to exactly one family/Application, or the call errors naming the orphan edit. So this only ever fires for a state genuinely predating ArgoApps' introduction, never for a fresh, post-M5 promotion that legitimately touches no Argo Application (which also has no Edits at all, since BuildPlan produces edits only from what an env's own families declare).
s.ArgoApps is otherwise left untouched — state.go's own doc comment ("computed once ... then carried unchanged across every resume") still governs every other case, matching Edits/ CommitMessage/PRTitle/PRBody's own carried-not-recomputed treatment.
s.ArgoNamespace gets the identical treatment in the same pass, for the identical reason: a pre-M5 state file decodes it as the empty string too (the field didn't exist yet), and argoApplications() builds an argo.Application with whatever s.ArgoNamespace holds verbatim — an empty namespace fails Argo.Get's own input validation outright ("application needs a namespace"), rather than merely under-reporting like an empty ArgoApps does. Both fields are always set together at construction time for every post-M5 promotion (StartPromotion), so "ArgoApps is empty and Edits is not" is exactly as unambiguous a legacy signal for ArgoNamespace as it is for ArgoApps itself — this function's own name stays EnsureArgoApps since Applications remain the primary concern, but it closes both gaps a legacy state can have.
s.EditApps gets the same treatment as a third, independent gap (PR #182): a state file saved any time between M5 and EditApps' own introduction has a populated ArgoApps but a nil EditApps, since the two fields were computed together at construction from that point on but ArgoApps alone before it — so "ArgoApps non-empty" cannot stand in for "EditApps populated" the way it does for ArgoNamespace above, and this checks EditApps on its own terms, discovering the repo only once for whichever of the two repairs this state actually needs.
Moved unchanged from cmd/hoist/resume.go's ensureArgoApps, and exported because both List and Resume in this file call it, and a resume_test.go's own legacy-state regression coverage needs it directly too.
func IsProduction ¶
func IsProduction(env string, envs config.EnvsConfig) bool
IsProduction reports whether env is listed in envs.production (AGENTS.md §4.5): direct mode is never offered for it, whatever the source env. Moved from internal/app/plan/rows.go unchanged; it is a thin wrapper over config.EnvsConfig.IsProduction, kept as a function here (rather than inlined at every call site) so a screen deciding whether to offer direct mode and Plan's own deploy-into-production warning read the identical rule.
func RepoConfigFor ¶
RepoConfigFor finds cfg's repos[] entry whose GitHub name matches repoFullName — how both `hoist promotions` and `hoist resume` locate the credential/CloneDir context a stored PromotionState doesn't carry a config reference for (state files are repo-agnostic beyond RepoFullName itself, on purpose — AGENTS.md §4.1: the state file is an index, not a second copy of config).
Moved from cmd/hoist/resume.go's repoConfigFor unchanged.
func SkippedStaging ¶ added in v0.3.0
func SkippedStaging(source, target string, envs config.EnvsConfig) (staging string, skip bool)
SkippedStaging reports the configured pipeline predecessor of target's production env that a promotion from source steps over. With a pair of its own, staging is the env envs.pairs sends source to, and skip is true when target is production but is not that pair. Without one (a qa or preview env), staging is the env(s) envs.pairs reaches target from, and skip is true when source is not one of them. False when target is not production, when source is exactly target's configured predecessor, or when nothing is configured to promote into target (AGENTS.md §4.5). Moved from internal/app/plan unchanged; the plan screen's own sentence and WarnSkippedStagingProduction read this one rule.
func WarnDeployIntoProduction ¶
func WarnDeployIntoProduction(pl *gitops.Plan, envs config.EnvsConfig)
WarnDeployIntoProduction attaches gitops.WarnProductionTarget to a deploy plan whose target env the operator's own config lists as production, and is the single place that decision is made: Plan (below) calls it for every deploy it builds, so the dry run, the confirm screen and the PR body all carry it — a warning only one surface shows is a warning the reviewer of a PR never sees. Informational only; production's real constraint is the PR — always — plus whichever approval mode the repo configures for that env: the comment is the default, but an explicit `approval: auto` is permitted (§4.5), so neither this helper nor the warning it attaches may claim a human comment is unconditional. Both are enforced in internal/engine.
A no-op for anything but a deploy: a promotion into production is what the paired-env config exists to describe, so saying it out loud there is noise, not news. Moved from internal/app/plan/rows.go unchanged.
func WarnSkippedStagingProduction ¶ added in v0.3.0
func WarnSkippedStagingProduction(pl *gitops.Plan, envs config.EnvsConfig)
WarnSkippedStagingProduction attaches gitops.WarnSkippedStaging to a promotion into a production env from a source that is not its configured predecessor: "promoting straight to production, skipping <staging>", whether or not the source has a pair of its own (SkippedStaging). Plan calls it for every promotion it builds, so the dry run, the confirm screen and the PR body all carry it, as WarnDeployIntoProduction does for a deploy (§4.5). It informs and never blocks (principle 5). A deploy has no source env, so it is a no-op there.
Types ¶
type ActEvent ¶
type ActEvent struct {
Step engine.StepName
State engine.PromotionState
}
ActEvent is one step's Act about to run. State is a copy of the promotion's state at that instant — what the Act is about to work on, not what it will produce: the outcome is the History entry the Act's save records, and reaches a caller through Hooks.OnHistory.
type AlreadyCurrentError ¶
type AlreadyCurrentError struct {
SourceEnv, TargetEnv string
// Deploy is true for a deploy plan (SourceEnv is always "" there); Ref is the one reference
// that plan would have written, valid only when Deploy is true.
Deploy bool
Ref image.Ref
}
AlreadyCurrentError is StartPromotion's own no-op signal: every edit in the confirmed plan already carries what it would write, so there is nothing left to commit, push or open a PR for. It carries plain fields rather than one canonical message because the CLI's own wording (which names the deployed ref for a deploy) and the TUI's (which never has, for a deploy — see wiring.go's old buildStartPromotion) have never agreed word for word; unifying that text is out of scope here, so each caller still builds its own final line from these fields. Error() gives a reasonable default for a caller that only wants *a* message — the TUI's own notice line uses exactly it today.
func (*AlreadyCurrentError) Error ¶
func (e *AlreadyCurrentError) Error() string
type AmbiguousError ¶
AmbiguousError is FindInFlightForEnv's own "more than one in-flight promotion targets this env" — AGENTS.md invariant 5 says there should never legitimately be two, so this is always a caller error (name one by id) rather than something FindInFlightForEnv resolves on its own. IDs is sorted before Error() is ever called, so the message is deterministic.
func (*AmbiguousError) Error ¶
func (e *AmbiguousError) Error() string
type Collectable ¶ added in v0.3.0
Collectable is how much a `hoist gc` run would remove of the two kinds that pile up: orphaned worktrees and registry cache entries past the prune age.
type Deps ¶
type Deps struct {
Git func() git.Git
Forge func(ownerRepo string) (forge.Forge, error)
Argo func(kubeContext string) (argo.Argo, string, error)
Rollout func(kubeContext string) (rollout.Rollout, string, error)
Cluster func(kubeContext string) (k8s.Cluster, string, error)
Registry func(registry.AuthConfig) (registry.Registry, error)
Store StateStore
Now func() time.Time
// NoCache disables Service's own success-memoization of Forge/Argo/Rollout (service.go).
// Set only by a test that swaps a seam (adaptors.Forge, adaptors.Argo, ...) BETWEEN calls against one
// long-lived Service instance — the TUI's own session-lifetime Service is exactly that
// shape, and a memoized client built from the seam's first value would otherwise survive
// the swap. Production code never sets this; see service.go's own doc comment for why
// memoizing at all is still correct there.
NoCache bool
}
Deps is the set of constructors a Service is built from — never the clients themselves. cmd/hoist's serviceDeps() wraps its own package-level test seam (adaptors) in closures that read the seam AT CALL TIME, so a Service built before a test reassigns one still reaches the new fake the next time it builds that client (a client the Service already built and memoised, Forge/Argo/Rollout, stays: see Service). Nothing here is a snapshot taken once; the call-time read is pinned by TestServiceDepsReadTheAdaptorsAtCallTime in cmd/hoist.
None of these constructors ever see a secret value directly — that is still resolved inside pkg/k8s and pkg/registry exactly as it is today (AGENTS.md §4.10, R-002); Deps only decides which constructor runs, never what it reads.
type Drive ¶
type Drive interface {
ID() string
State() engine.PromotionState
Step(ctx context.Context) (Tick, error)
OverrideCINone()
Run(ctx context.Context, h RunHooks) error
}
Drive is what a caller — cmd/hoist's own commands and internal/app/flight's screen alike — actually needs from a promotion driver: one poll (Step), the CLI's own run-to-completion loop (Run), the state it is driving (State, ID), and the one operator override a promotion can pick up mid-flight (OverrideCINone). *Driver satisfies it; a test fakes it directly.
type Driver ¶
type Driver struct {
// contains filtered or unexported fields
}
Driver drives one promotion's steps to completion, one engine.DriveStatus walk per Step call (see DriveStatus's own doc comment for the double-observation problem this replaces: a caller that used to run engine.Drive and then engine.Status every poll — cmd/hoist's driveToCompletion and internal/app/flight's own DriveFunc both did — now makes one real walk against the world per poll, whether that poll is a CLI retry, a flight screen tick, or the operator's own R/c gesture). mu serializes every method that touches state: Step, Run (which only ever calls Step) and OverrideCINone, so a screen's manual retry can never race an automatic tick's Step call over the same *engine.PromotionState — the same one-in-flight-per-instance guarantee flight.Model's own busy flag gives at the UI layer, held here too so a caller with no busy flag of its own (session.Controller, which holds a Drive across screens) still gets it for free.
func NewDriver ¶
func NewDriver(steps []engine.Step, state *engine.PromotionState, save func(*engine.PromotionState) error, poll engine.PollIntervals) *Driver
NewDriver builds a Driver over steps, saving through save after every step DriveStatus actually acts on or stops at (exactly as engine.Drive itself already does — save is called from inside DriveStatus, never by Driver directly). state is driven in place: every Step call mutates *state and returns a snapshot of it, so a caller that also holds state directly (a resumed CLI command's own *engine.PromotionState) sees it advance the same way engine.Drive always mutated its own s argument in place.
Exported and taking steps/state/save/poll positionally rather than a settled Deps/Settings shape because internal/service.StartPromotion and Resume are what build a Driver in production, and are expected to unexport this constructor behind their own request/response types once every caller goes through them.
func (*Driver) OverrideCINone ¶
func (d *Driver) OverrideCINone()
OverrideCINone sets CINoneOverride on this Driver's own state — the TUI's `c` gesture and the CLI's `--override-ci-none` both land here. It takes effect on the NEXT Step call, whose own CIGreenStep.Observe reads it and whose own save persists it; OverrideCINone itself never saves, exactly as flight.Model.ApplyCINoneOverride never saved on its own before this change — only a Step (via engine.DriveStatus's own per-step save) does.
func (*Driver) Run ¶
Run drives this promotion to completion, calling Step repeatedly and sleeping Tick.Wait between calls (in Heartbeat-sized slices, so a long wait can still report it is alive) until Step reports Done, a *engine.BlockedError, a terminal (non-Retry) error, or ctx is done. This is cmd/hoist/drive.go's old driveToCompletion, unchanged in outward behavior: the actual waiting still lives here, in the caller's own loop, never inside a Step's Act (AGENTS.md invariant 4) — Run just calls Driver.Step instead of engine.Drive+engine.Status directly, so there is still exactly one walk against the world per poll.
func (*Driver) State ¶
func (d *Driver) State() engine.PromotionState
State returns a value copy of the promotion's current state, safe to hold or render without racing a Step call in flight.
func (*Driver) Step ¶
Step runs one poll iteration: a single engine.DriveStatus walk, and — only when that walk returns a genuine failure, never for Waiting or Blocked — a second engine.Status walk to recompute the full step list, matching exactly what cmd/hoist/wiring.go's old driveFuncFor did unconditionally (call engine.Drive, then always call engine.Status for the rows) on ITS OWN error path: a caller rendering rows after a failure sees precisely what it saw before this change. On every other outcome — done, waiting, blocked — DriveStatus's own statuses are the only walk; the second walk this replaces was pure waste there (AGENTS.md §9 entry 11).
The returned error is never ErrWaiting and never a *BlockedError — those are reported through Tick.Waiting and Tick.Blocked instead, exactly as flight.DriveFunc's own doc comment already described for the old two-call shape. A non-nil error here is always a genuine plumbing or terminal failure, classified by Tick.Retry (engine.Retryable) for whatever the caller's own retry loop needs.
type FileStore ¶
type FileStore struct{}
FileStore is StateStore over the real $XDG_STATE_HOME/hoist files engine/state.go and engine/claim.go already read and write — a thin wrapper, not a reimplementation: every method is exactly the existing engine call, so this changes no on-disk shape and needs no migration.
func (FileStore) Archive ¶
Archive moves id's live state file into the archive subdirectory, per engine.ArchiveState.
func (FileStore) Claim ¶
Claim atomically claims repoFullName/targetEnv for promotion id, per engine.ClaimInFlight.
func (FileStore) Delete ¶
Delete removes one promotion's state file by id, per engine.DeleteState. Idempotent: an already-absent file is not an error.
func (FileStore) List ¶
func (FileStore) List() ([]*engine.PromotionState, error)
List returns every live promotion state file, per engine.ListStates.
func (FileStore) ListArchived ¶
func (FileStore) ListArchived() ([]*engine.PromotionState, error)
ListArchived returns every archived promotion state file, per engine.ListArchivedStates.
func (FileStore) Load ¶
func (FileStore) Load(id string) (*engine.PromotionState, error)
Load reads one promotion's state file by id, per engine.LoadState. A missing file is not an error: it returns (nil, nil), same as engine.LoadState.
type GCOpts ¶
type GCOpts struct {
DryRun bool
}
GCOpts configures one GC call. DryRun makes every check and observation a real run makes and reports what it would remove, removing nothing. "Observation" includes what every landing check does: a fetch of origin's base branch into the clone's remote-tracking ref. A dry run removes nothing; it is not a run that touches nothing.
type GCReport ¶
type GCReport struct {
Removed []string
Kept []string
Failed []string
// OrphanWorktrees and RegistryEntries count the orphaned worktrees and the registry cache
// entries among Removed — what `hoist promotions` weighs against its hint threshold.
OrphanWorktrees int
RegistryEntries int
}
GCReport is what GC did, as lines for a person: Removed is one line per thing removed (worded "would …" under DryRun); Kept is one line per thing GC looked at, could have been expected to remove, and deliberately left, with the reason; Failed is one line per thing it could not decide about or could not remove — an observation or a removal that errored — which is not a decision to keep it and is what makes `hoist gc` exit non-zero.
type Hooks ¶
type Hooks struct {
Progress func(string)
OnWaiting func()
OnAct func(ActEvent)
// OnHistory is called by the Driver for each History entry newly appended by a Step walk, at
// the moment the state save carrying it has landed — oldest first, once per entry — with a
// copy of the state as saved (so e.g. s.PR is populated when the pr-opened entry fires). It
// runs on the walking goroutine under the Driver's lock: it must not block and must not call
// back into the Driver. Entries already in the state when the Driver was built are not
// reported. This, not Progress, is how a step's outcome reaches a screen as it happens.
OnHistory func(e engine.HistoryEntry, s engine.PromotionState)
}
Hooks are optional, nil-safe callbacks StartPromotion invokes as it works through preflight — never anything a caller needs to poll for. Progress reports one short line per preflight stage, mirroring the CLI's own progress lines (runHooksForCLI) and the TUI's own preflight log (buildStartPromotion's report calls) — both now the identical text, since both go through this one function. It carries preflight lines only, never a step's outcome: those are PromotionState.History entries, and echoing them here put every event on the flight screen twice. OnWaiting is passed straight through to engine.StepsFor, for a later Step/Run call's own CommittedStep "waiting for signing approval" wait.
OnAct reports each step's Act as it is about to run (ActEvent) — the one signal that a commit or a push is under way, since History, and so OnHistory below, only hears of an Act once it has returned. Like OnHistory it runs on the walking goroutine under the Driver's lock, so it must not block or call back into the Driver. Only the CLI sets it. AbandonWith and FindInFlightForEnv take a Hooks too and use Progress alone.
type InFlightConflictError ¶
type InFlightConflictError struct {
Conflict *engine.PromotionState
Env string
Status engine.StepStatus
}
InFlightConflictError is AGENTS.md invariant 5's own refusal: another promotion, other than the one being started, already targets the same repo/env and has not yet re-observed as done. Error() is claimTarget's/FindInFlight's own wording, moved unchanged from cmd/hoist/promote.go's inFlightConflictError.
func (*InFlightConflictError) Error ¶
func (e *InFlightConflictError) Error() string
type ListOpts ¶
type ListOpts struct {
RepoFullName string
ArchiveDoneOlderThan time.Duration
Progress func(string)
}
ListOpts scopes and configures one List call: RepoFullName filters to one repo ("" lists every configured repo's promotions, exactly like `hoist promotions` with no --repo), and ArchiveDoneOlderThan is the retention window (state.retain) past which a Done promotion is moved to the archive as part of this same call. 0 (or negative) disables archiving for this call, and only for a caller that passes it directly: the session controller's ListOpts{} does, so the matrix never archives. The config cannot express "never archive" — Normalize makes state.retain 0 or unset mean config.DefaultStateRetain (30 days) and Validate rejects a negative one — so `hoist promotions` always passes a positive window. This comment used to say it matched `hoist promotions`' own "retain: 0 means never archive" reading of the config; there is no such reading (#248).
Progress, when set, hears one line before the re-observation starts, naming how many promotions it covers — the listing prints nothing of its own until every one is observed.
type Listed ¶
type Listed struct {
State engine.PromotionState
Done bool
Statuses []engine.StepStatus
Last engine.StepStatus
Err error
Unconfigured bool
// Archived is true when this same List call just archived this promotion (it was Done and
// older than ArchiveDoneOlderThan) — the CLI's own --archived listing needs this to avoid
// printing an archived-this-run promotion a second time (#181).
Archived bool
// ArchiveErr is set when archiving was attempted (Done, past the retention window) but
// failed — the promotion is still reported Done, with the archive failure alongside it,
// exactly as runPromotions always has.
ArchiveErr error
// Cleaned is one line per thing this same List call removed for a promotion it observed
// landed (its worktree, its local branch — CleanupLanded); CleanupErr is why that removal
// was refused or failed. Neither changes Done: a promotion is no less finished for still
// having a worktree.
Cleaned []string
CleanupErr error
}
Listed is one promotion's own re-observed standing, as `hoist promotions` and the matrix's in-flight pane both now build it from the same List call. Exactly one of Unconfigured/Err/ (Done or a populated Statuses) describes why: Unconfigured means State.RepoFullName names a repo no longer in the config file (nothing was even attempted); Err means a client could not be built, EnsureArgoApps failed, or the re-observation itself errored (already redacted — AGENTS.md §4.10); otherwise Statuses is engine.Status's own full per-step walk and Last is its final entry (the CLI only ever prints Last; the matrix's pane needs every entry to draw per-step glyphs. engine.Status is used here, not engine.ObserveAll, specifically so one walk serves both, and Last is derived from its own final entry rather than a second, separate ObserveAll call).
type Mode ¶
type Mode struct {
Direct bool
Confirmed bool
// OverrideCINone seeds the new (or resumed) PromotionState.CINoneOverride — the CLI's own
// --override-ci-none. The TUI's `c` gesture instead calls a running Drive's own
// OverrideCINone() mid-flight, never this: see Driver.OverrideCINone's own doc comment.
OverrideCINone bool
}
Mode is how the operator asked to run this promotion. Direct/Confirmed are the CLI's own --direct/--confirm-direct pair or the TUI's keypress-then-confirm gesture — either way, Confirmed only ever selects whether a NON-production direct request is honoured; engine.DirectCommitGateStep re-derives the production refusal from ProductionEnvs regardless of Confirmed (AGENTS.md §4.5), and so does Preflight's own call to that same step above.
type NotFoundError ¶
NotFoundError is Find's own "no such promotion" (ID set) and FindInFlightForEnv's own "no in-flight promotion targets this env" (Env set) — one type for both, since both are the same shape of answer ("nothing matched") to the two different questions `hoist resume` can be asked (by id, or by --env). Exactly one of ID/Env is ever set. Error() reproduces cmd/hoist/resume.go's own pre-move wording verbatim, so a caller printing it unchanged (the CLI renderer) prints exactly what it always has.
func (*NotFoundError) Error ¶
func (e *NotFoundError) Error() string
type PlanRequest ¶
type PlanRequest struct {
// Repo is the discovered GitOps repo to plan against; nil uses s.Repo().Repo (the
// service's own current view — LoadRepo/RefreshRepo).
Repo *gitops.Repo
// Source is the env to read digests from; "" iff Deploy is set (a deploy has no source
// env — image.Ref names the reference outright).
Source, Target string
// Deploy is set for `hoist deploy`/the TUI's tag-picker confirm: write this exact
// reference into every occurrence of its repo in Target, rather than resolving Source's
// running digest.
Deploy *image.Ref
// Overrides are the operator's own per-repo digest overrides (`--digest`, the plan
// screen's o dialog): each wins outright over whatever Source resolves to.
Overrides map[string]image.Ref
// Progress, when set, hears one line before digest resolution starts — the one part of
// planning that asks the cluster and the registry. Only the CLI sets it.
Progress func(string)
}
PlanRequest is everything Plan needs to build one gitops.Plan, for a promotion or a deploy — the one shape `hoist plan`, `hoist promote`, `hoist deploy` and both TUI paths (the plan screen's loadCmd, the matrix's deploy confirm) now build and hand to Plan, so a plan built for a confirm screen and a plan built for the CLI's own dry run can never silently diverge again.
type PlannedChange ¶
type PlannedChange struct {
Plan gitops.Plan
Repo *gitops.Repo
// View is the RepoView Plan actually planned against — s.Repo() at the moment Plan ran, when
// req.Repo pointed at that same *gitops.Repo (nil, or an explicit pointer that happens to be
// the service's own current view); a zero RepoView{Repo: r} otherwise (a caller-supplied repo
// Plan never fetched itself, e.g. the CLI's own gitops.Discover). Request below carries this
// into StartRequest.View so StartPromotion's freshness check runs against the view THIS plan
// was built from, never whatever s.Repo() happens to hold when the operator later confirms —
// fixing the fail-open gap where an F5 refresh landing between building the plan and pressing
// Enter let a stale plan's edits pass a freshness check run against the NEW view instead of
// the one that produced them.
//
// When s is in origin mode (cur.FromOrigin — LoadRepo(RepoFromOrigin) has run: TUI boot or a
// later F5), this field is ALWAYS a real FromOrigin view with a captured SHA: Plan itself
// fails closed above rather than ever return a zero or clone-mode View for such a caller.
// StartPromotion enforces the other half of that guarantee at its own end (structurally, not
// just by convention): a StartRequest.View that reaches it nil, or with FromOrigin false, or
// with an empty SHA, while the service's current view is itself origin-mode, is refused
// outright — a caller that lost this field on the way there (a screen that forgot to plumb
// it) gets an error naming the fix, never a silent downgrade to the CLI's looser
// checkCloneCurrentForBase check.
View RepoView
// Resolution is nil for a deploy (which never resolves a digest — the reference is
// caller-supplied) and for a promotion planned with digest sources: none.
Resolution *Resolution
// contains filtered or unexported fields
}
PlannedChange is what Plan returns: the built gitops.Plan (its Warnings already carry resolve.Warnings and, for a deploy into a production env, WarnDeployIntoProduction — never re-attached by the caller), the repo it was planned against, and the resolution section when one ran.
func (PlannedChange) Request ¶
func (p PlannedChange) Request(m Mode) StartRequest
Request builds this PlannedChange's own StartRequest for StartPromotion: the plan and repo exactly as Plan built them, plus this promotion's own resolved digests (fresh) so direct mode's fresh-base check reuses them rather than re-resolving. Only valid for the UNFILTERED plan Plan itself returned — a caller that ticks a subset of edits (the TUI's own confirm) builds its own gitops.Plan with those edits and its own StartRequest directly, passing Fresh: nil, since the resolution behind THIS PlannedChange no longer describes only what was kept.
type RepoMode ¶
type RepoMode int
RepoMode picks how LoadRepo reads the GitOps repo: from the operator's own clone directly (the CLI's own `gitops.Discover(RepoDir)`), or from a cached view of origin/<base> (the TUI's own repoview.go behavior, moved here unchanged).
const ( // RepoFromClone discovers directly from Settings.RepoDir, exactly what `hoist plan`/ // `hoist promote` have always done: a pure local disk read of the operator's own // checkout, no network needed. RepoFromClone RepoMode = iota // RepoFromOrigin fetches origin/Settings.Base into a cached worktree first (refreshRepoView // below) and discovers from THAT — the TUI's own answer to "what does origin actually look // like right now", read without ever touching the operator's own checkout or working tree // (AGENTS.md §4.6). Falls back to RepoFromClone's own local read when origin can't be // reached at all, since browsing the matrix must stay possible offline (principle 5). RepoFromOrigin )
type RepoView ¶
type RepoView struct {
Repo *gitops.Repo
Dir string
// FromOrigin is true when Dir is the cached origin view (refreshRepoView succeeded), false
// when LoadRepo fell back to the clone itself (RepoFromOrigin's fetch failed) or was asked
// for RepoFromClone directly.
FromOrigin bool
// SHA is origin/<base>'s tip at the moment THIS view was built (FromOrigin only; "" for a
// clone-mode view or a fallback). Dir names a cached directory under repoViewDir, keyed only
// by cloneDir — a LATER LoadRepo(RepoFromOrigin) call (an F5 refresh) checks that same
// directory out to a NEW ref in place, so a Dir string alone cannot tell an old view from a
// newer one once the refresh has run: CheckRepoViewCurrent must compare against the SHA this
// view actually captured, never re-derive one by reading Dir's current on-disk HEAD, or a
// refresh that lands between building a plan and confirming it would silently launder a
// stale plan by making the check compare origin's tip against itself.
SHA string
// Fallback is refreshRepoView's own error when RepoFromOrigin had to fall back to the
// clone. nil whenever FromOrigin is true, or for RepoFromClone.
Fallback error
}
RepoView is one snapshot of the GitOps repo: the discovered Repo, the directory it was discovered from, and — for RepoFromOrigin — whether that came from the cached origin view or fell back to the clone, and why.
type Resolution ¶
type Resolution struct {
Order []resolve.Source
AuthTried []registry.AuthSource // the configured chain, for the "all failed" label
KubeContext string // "" when no cluster was opened
AuthUsed string // "" when no credential source authenticated
Consulted bool // true when the registry was asked at all (win or lose)
Res map[string]resolve.Resolution
}
Resolution is one Plan call's resolution section: which sources and credential chain were asked, what actually authenticated, and every resolved repo — the same facts `hoist plan --dry-run` prints and the TUI plan screen renders (AGENTS.md §4.4/§4.10: by name only, never a value). The moved and renamed form of cmd/hoist's own resolutionReport; nil means "digest sources: none" throughout PlannedChange.
type ResolveOptions ¶
type ResolveOptions struct {
KubeContext string
Order []resolve.Source
Auth []registry.AuthSource // "" override; nil = per-repo config decides
ClusterSecret string // "" = per-repo config decides
OpRef string // "" = per-repo config decides
Registries []config.RegistryConfig
}
ResolveOptions is what the flags and the config agree digest resolution does — the moved form of cmd/hoist's own resolveOptions (F4). Auth, ClusterSecret and OpRef are only ever the *explicit* --registry-auth/--cluster-secret/--op-ref override, applied identically to every repo when given; when they are not given (the zero value), each image repo's own registries[] entry decides instead (registryEntryFor/entryAuthConfig, below).
func NewResolveOptions ¶
func NewResolveOptions(cfg *config.Config, rc *config.RepoConfig, digestSources, registryAuth, clusterSecret, opRef, kubeContext string) (ResolveOptions, error)
NewResolveOptions applies the precedence for the digest-resolution flags: a flag given wins outright, for every repo; else the selected repo's config (digest_sources). kubeContext is NOT part of that: it is the context the caller already decided (Settings.KubeContext, from selectRepo — flag if given, else repos[].kube.context, else "" for the kubeconfig's current context), taken as is. Re-deriving it here from the value would read an explicitly empty --kube-context as "not given" and send pods to a different cluster than Argo (#295). Registry credentials (auth, cluster, op) are resolved later, per image repo, in resolveImages — never here as one merged chain (F4). digestSources "none" turns resolution off (M1's plan).
type ResumeOpts ¶
ResumeOpts is Resume's own per-call configuration: OverrideCINone is the CLI's --override-ci-none / the TUI's already-applied `c` gesture (a fresh CINoneOverride to seed onto the state before driving, distinct from Driver.OverrideCINone's own mid-flight version of the same flag); Hooks carries OnWaiting, passed to engine.StepsFor for the returned Drive, and OnHistory, installed on it. Hooks.Progress is preflight-only and Resume has no preflight, so it reports nothing here: a resumed promotion's events reach a caller as OnHistory calls (one per History entry, as its save lands), never as a second stream of progress text.
type RunHooks ¶
type RunHooks struct {
OnTick func(Tick)
OnRetry func(error)
Heartbeat time.Duration
OnHeartbeat func(Tick)
}
RunHooks are the CLI's own reporting hooks for Run — all nil-safe. OnTick fires once per poll that comes back Waiting (mirroring cmd/hoist/drive.go's old waitingReporter.report call on the same condition); OnRetry fires once per poll that came back a genuine, retryable error; OnHeartbeat fires during the sleep between polls, at most every Heartbeat, but only while the most recent poll was Waiting (a retryable-error sleep prints nothing further today, matching driveToCompletion exactly). Heartbeat <= 0 means "no heartbeat slicing" — the whole wait is one sleep.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service is the lazy, memoized client cache built over Deps. A CLI command builds one per invocation (its clients never outlive the command); the TUI builds exactly one for the whole session, which is why memoization has to be success-only (a client that failed to build stays worth retrying on the next call — a `hoist watch`-style boot failure should never wedge every later attempt for the rest of the session) and why Git/Cluster are never memoized at all: Git carries no per-target state to cache (git.Exec{} is stateless, and a test may swap adaptors.Git between calls), and Cluster is opened fresh per call today (the drift column already does this; caching it would be a behavior change).
func New ¶
New builds a Service from settings and deps. Nothing here calls out to git, the forge or the cluster — every client is built lazily, on first use, through the matching provider method below.
func (*Service) Abandon ¶
Abandon retires promotion id for good: releases the state file, removes its worktree and its local branch from the operator's clone and, if it opened a PR, closes it and deletes its remote branch. The returned actions are one human-readable description per real action actually taken, in order (empty when nothing beyond the state file itself needed touching — a direct-mode promotion never opens a PR or pushes a branch to origin at all).
Re-observes first and refuses outright if the promotion has already landed (merged, for the PR path; pushed, for direct mode) — abandoning is not a rollback: a landed promotion needs `hoist deploy` or a fresh promotion to undo, never a state-file delete. The observation is the identical git/forge-only core FindInFlight/FindInFlightForEnv both use (engine.ObserveSteps with Argo/rollout both nil) — "has this promotion's own write happened" is exactly the question that answers, and it is also exactly the three-way intact/superseded/reverted judgment (AGENTS.md §4.1) DirectPushedStep/MergedStep already apply, so Abandon inherits it rather than re-deriving a second opinion.
`done` alone is not "has this landed": MergedStep.Observe (PR path) and DirectPushedStep (direct path) both mutate st in place with the real landed sha the moment their own merge/push is CONFIRMED — even when they go on to report Satisfied:false for something that only ever happens AFTER landing (the PR path's own "merged as X; branch not yet deleted" case: AGENTS.md §6.1 gotcha 7). st.LandedSHA() asks the direct, mode-agnostic question this actually needs answered; `done` stays as a second, independent gate (AGENTS.md §8 "layered checks").
No claim file is touched: a promotion with a state file at all already released its own claim the moment that file was first saved.
Moved unchanged in substance from cmd/hoist/abandon.go's abandonPromotion.
func (*Service) AbandonWith ¶
AbandonWith is Abandon with h.Progress hearing each phase as it starts: the re-observation, then each write. Abandon itself keeps its signature for app.Service, whose caller reports an abandon through the activity log instead.
func (*Service) Argo ¶
Argo returns an Argo client for kubeContext, memoized on success exactly as ForgeFor is.
func (*Service) ArgoNamed ¶ added in v0.3.0
ArgoNamed is Argo plus the kubeconfig context the client resolved to.
func (*Service) CheckOverrides ¶
CheckOverrides refuses a --digest override BuildPlan would never consult: one for a repo outside the promotable prefixes (BuildPlan iterates promotable repos only, so an override for a third-party image would be accepted and change nothing), or one for a repo that has no occurrence in the source env (a typo in the repo name would plan the source env's ref instead of the caller's). Either way -h promises the override is planned, so silence is wrong. An unknown source env and an empty prefix list are left for BuildPlan to report. Moved from cmd/hoist's own checkOverrides unchanged, reading s.settings.Promotable rather than taking it as a parameter.
func (*Service) CleanupLanded ¶
CleanupLanded removes promotion st's worktree and its local branch from the operator's clone once — and only once — st is observed landed: merged with its remote branch deleted (PR path), or pushed and still intact or superseded on the base (direct path). The returned lines are one human-readable description per thing actually removed; nil means there was nothing to do, whether because the promotion has not landed or because it was already clean.
It and cleanupObservedLanded (the same checks, ordered for a caller that polls: List, and the Drive returned by StartPromotion and Resume) are the only automatic cleanups. Both re-observe the landing step themselves rather than accept a caller's word, a recorded Phase or a stored landed sha (AGENTS.md §4.1). And because "landed" is itself only as good as the forge lookup behind it, it then checks the worktree and branch themselves hold nothing that is not on the remote (locallyRemovable, tipIsOnTheRemote) before removing either.
A *engine.CleanupRefusedError means the worktree or branch could not be shown to be hoist's to delete; everything is left in place.
func (*Service) Cluster ¶
Cluster returns a cluster client for kubeContext, and the kubeconfig context name it actually used (an empty kubeContext resolves to the kubeconfig's current context, which callers report by name, never an address — AGENTS.md §4.4/§4.10). Never memoized: the matrix's drift column already opens one per call, and every env it asks about in one pass shares the same kubeContext, so memoizing here would only save a connection this method does not itself hold open anyway.
func (*Service) Collectable ¶ added in v0.3.0
func (s *Service) Collectable(ctx context.Context) (Collectable, error)
Collectable counts what GC would remove, using GC's own dry run so no check is restated: the same orphan evidence and the same prune age. It leaves out the landed-promotion cleanup (cleanupLanded), the one part of GC that asks origin and the forge, because the listing that asks for this has just run it. It removes nothing and talks to no forge or cluster; git is asked only locally.
func (*Service) Find ¶
func (s *Service) Find(id string) (*engine.PromotionState, error)
Find looks up one promotion's state file by id, directly (engine.StatePath(id) is deterministic from id, so this never has to scan every state the way FindInFlightForEnv does) — the one path `hoist resume <id>` and `hoist abandon <id>` both take to turn an operator-given id into a *engine.PromotionState.
engine.StatePath joins id straight into the promotions directory (`id+".json"`), with no containment check of its own — main's own ListStates-based lookup never took an operator-given string anywhere near a path join, so this join-by-id shortcut (moved here from cmd/hoist's own runResume/runAbandon) is a NEW path-traversal surface: an id containing a path separator or ".." reaches outside the promotions directory (`../../etc/passwd`), or sideways into the archive subdirectory (`archive/<real-id>`), letting `hoist resume`/`hoist abandon` act on an archived or aged-out promotion `hoist promotions` never lists as live. Find refuses any id containing a path separator or ".." outright, and — once a state file does load — refuses one whose own ID does not match id, so a file that happens to sit at the expected path but was never saved as this id (or was moved/archived since) is treated as not found rather than silently acted on.
func (*Service) FindInFlight ¶
func (s *Service) FindInFlight(ctx context.Context, repoFullName, targetEnv, skipID string) (*engine.PromotionState, engine.StepStatus, error)
FindInFlight looks for a promotion state other than skipID targeting repoFullName/targetEnv that engine.ObserveAll reports as not yet done — AGENTS.md §4.1's own re-observe rule, applied to "is there already a promotion running for this env" rather than trusted from the state file's own Phase or presence alone (invariant 5: one in-flight promotion per target env). found is nil when no conflicting in-flight promotion exists. An error re-observing a candidate is treated conservatively — reported rather than silently skipped — since a promotion this call can't verify is done must not be treated as safely finished.
Deliberately observes only the git/forge core (through Merged for the PR path, through the push for a direct one — engine.ObserveSteps picks by the state's own mode, since a direct state can never satisfy the PR path's steps and would otherwise be in flight forever), not the full engine.AllSteps/AllDirectSteps — a considered call, not an oversight. Invariant 5 exists to prevent exactly one thing: two promotions racing to create separate branches/PRs/merges for the same target env (a real git/forge conflict). That risk is fully retired the moment a merge (or a direct push) lands — a second promotion for the same env gets its own id, its own branch and its own PR (§4.1's deterministic id is keyed on the image set), so nothing about this promotion's own Argo refresh/sync or rollout convergence can still collide with it. See AGENTS.md §9 entry 11 for why this needs a genuinely three-way landed verdict (intact/ superseded/reverted), not a plain ancestry or blob check, to avoid wedging a real env for days (#165, #166) — that verdict lives in engine.observeLanded, called from inside the steps ObserveSteps builds here, not from this function itself.
Moved unchanged from cmd/hoist/drive.go's findInFlight — g and f are now built from Deps rather than taken as parameters: repoFullName is always the SAME forge identity every step list built from prev would use, and s.Git()/s.ForgeFor give exactly that without a caller having to already hold one.
The scan is made through a scanGit first — one listing of origin's branches and one fetch of each base, shared by every candidate — and its answer stands only if origin's branches are still where that snapshot had them once the scan is over (scanGit.unchanged). Otherwise, or if the pass failed, the scan is made again through the live git.Git. Either way every call asks origin afresh: the snapshot is built here and dropped on return.
func (*Service) FindInFlightForEnv ¶
func (s *Service) FindInFlightForEnv(ctx context.Context, env string, h Hooks) (*engine.PromotionState, error)
FindInFlightForEnv answers `hoist resume --env <target-env>`'s own question: which single, still-in-flight promotion (per AGENTS.md invariant 5, there should never legitimately be more than one) targets env. Every live state file targeting env is re-observed (AGENTS.md §4.1, never trusted from its own recorded Phase) through the git/forge-only core (Argo/rollout are both nil — the same core FindInFlight and abandon's own landed-check use: "is this promotion's own write still pending" does not need the cluster).
A candidate whose repo is no longer in the config file, whose forge/Argo-rollout clients cannot be built, or whose own re-observation errors, is never silently excluded — that would let a transient forge/git failure misleadingly read as "no in-flight promotion" (or resolve an otherwise-ambiguous set down to one candidate without ever confirming the choice was actually unambiguous). Every such candidate is instead collected into a *UnconfirmedError, returned once the scan finishes, naming every candidate it could not confirm.
Moved unchanged in substance from cmd/hoist/resume.go's own --env matching loop.
h.Progress hears one line before the scan starts.
func (*Service) ForgeFor ¶
ForgeFor returns a forge client for ownerRepo, memoized on success: once a forge for ownerRepo has been built, every later call for the same ownerRepo returns the same value without calling Deps.Forge again. A failed build is never cached, so the next call retries it (a lapsed `gh` login fixed mid-session should not need a restart).
func (*Service) GC ¶
GC removes what finished promotions leave on this machine, and nothing else:
- the worktree and local branch of every promotion with a live state file that is observed landed — the same CleanupLanded the listing and the drivers call, so this is where a promotion that landed before that cleanup existed gets tidied;
- orphaned worktrees: a directory under <CacheDir>/worktrees that no live state file names, left by a promotion abandoned or archived before cleanup existed, or whose state file was deleted by hand;
- repo-view directories (<CacheDir>/repo-view/<hash>) of clones that are no longer configured (#247): see removeStaleRepoViews;
- registry cache entries not used for registryCacheMaxAge.
An orphan has no state file, so nothing can be re-observed about it; what stands in for that is evidence it is hoist's own worktree and holds nothing uncommitted: its name is a promotion id, it is a real directory (engine.PromotionWorktree), it is registered as a worktree of a configured clone on that id's own hoist/<env>/<id> branch, and `git status` shows nothing in it. Anything short of that is kept and named in the report. A commit that exists only on an orphan's branch is NOT a reason to keep it: that is exactly what an abandoned promotion leaves — a one-line-per-occurrence image edit that promoting again makes again, as a new commit. This is the one place hoist deletes a commit that may exist nowhere else, which is why orphans are removed only here, on an explicit command with a dry run, and never automatically. (Automatic cleanup of a landed promotion checks the opposite: service.tipIsOnTheRemote.)
The worktrees directory is read BEFORE the state files are listed: a promotion saves its state file before it creates its worktree, so every worktree seen in that first read already has its state file by the time the second read happens, and a promotion starting mid-GC can never be mistaken for an orphan.
func (*Service) Git ¶
Git returns a git client. Never memoized (see Service's own doc comment) — s.deps.Git is called every time.
func (*Service) HistoryDelta ¶ added in v0.3.0
HistoryDelta is the commits and migrations between two builds of one image repo: everything between resolving both ends, fetching their labels, choosing the migrations prefix and caching happens inside it. An unresolvable end is a migrate.ErrUnresolved a screen renders as a named gap; an unmapped image repo is the same error with a reason naming repos[].apps.
func (*Service) HistoryMapped ¶ added in v0.3.0
HistoryMapped answers, without a network call, whether repos[].apps maps imageRepo to an app repo — so a screen can say "no app repo in repos[].apps" before spending a spinner on it.
func (*Service) HistoryRevision ¶ added in v0.3.0
HistoryRevision resolves which app-repo commit built ref.
func (*Service) List ¶
List re-observes every live promotion state file — `hoist promotions`' own listing and the matrix's in-flight pane both drive from this one call now (previously two separate re- observation loops, cmd/hoist/resume.go's runPromotions and cmd/hoist/wiring.go's observeForList, which could in principle disagree about the very same state file). Never reads a state file's own recorded Phase (AGENTS.md §4.1) and never returns early on one candidate's own error — a promotion that cannot be confirmed is reported as such (Listed.Err), never dropped.
func (*Service) ListArchived ¶
func (s *Service) ListArchived(repoFullName string) ([]*engine.PromotionState, error)
ListArchived returns every archived promotion state file (state.retain's own destination), optionally scoped to one repo — `hoist promotions --archived`'s own listing, called AFTER List so that a promotion List just archived (Listed.Archived) can be excluded from a caller's own combined rendering instead of appearing twice in the same invocation's output.
func (*Service) LiveAge ¶ added in v0.3.0
LiveAge dates one manifest occurrence in the gitops repo: when the line last changed. It blames the gitops repo's own forge, which needs no app mapping at all — a repo with an empty repos[].apps still gets "declares v1 · since 4 weeks ago".
func (*Service) LoadRepo ¶
LoadRepo reads the GitOps repo per mode and stores the result as s's current view (Repo() below reads it back). Called at TUI boot and by RefreshRepo (F5); a CLI command that only ever discovers once may ignore the stored view and just use the returned RepoView.
func (*Service) OpenTagSource ¶ added in v0.3.0
OpenTagSource builds the tag source for imageRepo. The registry client is the one registries[] entry covering it (RegistryFor, F4: never another entry's credentials). A registry that cannot be built is not fatal here: every call that needs it reports the real error, so the picker opens and says why. The forge client comes from ForgeFor, which remembers only a success, so a `gh` login fixed mid-session is picked up the next time the picker opens.
func (*Service) OpenWatchApp ¶ added in v0.3.0
OpenWatchApp opens the Application named app (its metadata.name) in r, the way `hoist watch --app` does. The repo is the caller's: the CLI reads its working tree, the TUI the service's current view.
func (*Service) OpenWatchFamily ¶ added in v0.3.0
OpenWatchFamily opens one family of one env in the service's current repo view (Repo), so a family an F5 refresh just added is watchable at once. It resolves the family's Application (gitops.Family.App, the wrapper OpenWatchApp finds by name) and reads from the same place.
func (*Service) Plan ¶
func (s *Service) Plan(ctx context.Context, req PlanRequest) (PlannedChange, error)
Plan builds one gitops.Plan: BuildDeployPlan for req.Deploy, else digest resolution (when s.Settings().Resolve.Order is non-empty) followed by BuildPlanWith — the exact sequence `hoist plan`/`hoist promote`/the TUI plan screen each used to run for themselves. Resolution errors are a whole-operation failure (AGENTS.md §4.10): a per-repo registry miss is a resolve.Resolution that failed to resolve, never an error return, so there is nothing left here to plan from without a resolution attempt that could not even run at all.
func (*Service) Preflight ¶
Preflight is StartPromotion's own early gate, called before any freshness check, claim or state save, and the one place a start's prerequisites are stated: the CLI calls its two halves (PreflightConfig, PreflightGate) around its own --confirm-direct match, so `hoist promote`, `hoist deploy` and the TUI refuse the same shapes in the same words (AR-M8, #201.5).
Reaching engine.DirectCommitGateStep here, first, is what closes the TUI's old hole: its buildStartPromotion only reached the gate once Drive first ran the step — AFTER the claim and the initial state save. This does not make the gate any less the SOLE enforcement point (AGENTS.md §4.5): deleting this call cannot make a production direct commit possible, because StepsFor's direct step list carries the same step, checked again the moment Drive first runs (§8, layered checks — the deletion test). ProductionEnvs is always the settings' own, unfiltered, exactly as engine.StepsFor's doc comment demands.
func (*Service) RefreshRepo ¶
RefreshRepo is F5's own re-read of origin: RepoFromOrigin, so the matrix and a plan built moments later never disagree about which origin/<base> either was reading.
func (*Service) RegistryFor ¶
RegistryFor builds a registry.Registry client for one image repo, scoped to whichever registries[] entry covers it (or the default chain, for one none does) — the credential lookup M6's tag picker and commit history need for a direct call that skips Plan's own resolution entirely (AGENTS.md §4.10: F4's per-repo scoping applies here too, never a merged chain). Moved from cmd/hoist's own buildTagsFunc, which built this inline per call.
func (*Service) Repo ¶
Repo returns the current RepoView — whatever the most recent LoadRepo/RefreshRepo stored. The zero value (Repo == nil) means LoadRepo has never been called.
func (*Service) RestartDo ¶ added in v0.3.0
func (s *Service) RestartDo(ctx context.Context, p restart.Plan, at time.Time) ([]gitops.Workload, error)
RestartDo stamps the restart annotation on every target in p at at, stopping at the first genuine failure and returning the workloads it got through, in order (internal/restart.Do).
func (*Service) RestartInterval ¶ added in v0.3.0
RestartInterval is how often a running restart's rollout is re-read: poll.rollout, or three seconds when none is configured.
func (*Service) RestartObserve ¶ added in v0.3.0
func (s *Service) RestartObserve(ctx context.Context, env string, targets []gitops.Workload, at time.Time) ([]restart.Progress, error)
RestartObserve reads how far each named workload's rollout has got against the stamp a restart wrote at at (internal/restart.Observe). An OnDelete workload comes back Manual, never Done.
func (*Service) RestartRead ¶ added in v0.3.0
func (s *Service) RestartRead(ctx context.Context, env string, targets []gitops.Workload) (restart.Plan, error)
RestartRead reads every named workload's current state in env — Deployments, StatefulSets and DaemonSets alike (rollout.RestartableKinds) — so a caller can show what would roll, and every reason it would not be graceful, before anything is written (internal/restart.Read). Restart is the one use case that writes to the cluster rather than to git, so it has no plan, branch, PR or state file: these three calls are the whole of it.
Each of the three asks for the rollout client itself, at call time, rather than holding one: Rollout memoises only a success, so a cluster that was unreachable when the TUI opened is retried on the next R, and the error a failed call returns is the real one from the client.
func (*Service) Resume ¶
Resume re-derives a drivable promotion from a persisted state file, honouring exactly the same carry-forward rules cmd/hoist/resume.go's runResume and cmd/hoist/wiring.go's buildInFlightFuncs.Resume both applied by hand (and had drifted on: the TUI passed no progress, no onWaiting, and the CLI's own ctx already wraps a deadline the TUI's call site never threaded through either — this is the one path both now take):
- CINone/CIGrace/CISettle/CIIgnore/Approval/Approvers/Collaborators are NEVER re-read from the current config file — PromotionState's own doc comment states the invariant that these are policy "as of when this promotion started", so a promotion never straddles two different policies mid-flight even if the operator edits the config file while it is in flight. Only o.OverrideCINone is a one-shot, explicit override for this call.
- ArgoNamespace IS re-read from the current config on every resume: it only names where a live Get for this env's Argo Applications lands, not a decision replayed against historical events, so re-reading it fails loudly (Application not found) rather than misjudging anything quietly if it moved.
- EnsureArgoApps repairs a legacy state file missing ArgoApps/ArgoNamespace/EditApps.
- engine.StepsFor is asked for the mode this promotion actually IS (a direct promotion never pushed its branch and has no PR, so driving it through the PR-path steps would push a branch and open one — the exact shape --direct exists to avoid); Confirmed is always true, because this promotion's own existence on disk is the confirmation (the state file only exists because the operator already passed --confirm-direct, or the TUI's own gesture, when it first started) — DirectCommitGateStep still re-derives the production refusal independently of Confirmed (AGENTS.md §4.5), so resuming can never reach an env the original run would have been refused.
func (*Service) Rollout ¶
Rollout returns a rollout client for kubeContext, memoized on success exactly as ForgeFor and Argo are.
func (*Service) RolloutNamed ¶ added in v0.3.0
RolloutNamed is Rollout plus the kubeconfig context the client resolved to.
func (*Service) RunningImages ¶ added in v0.3.0
RunningImages is the matrix's cluster question: the raw pod observations for one env (the env is the namespace, AGENTS.md §1), straight from pkg/k8s, keyed by canonical image repo with every distinct running reference kept — never the planning resolver, which picks one digest per repo and would collapse a partial rollout, and which grafts the manifest's tag onto the digest it picked (#122). Pods only, whatever digest_sources the config orders for planning: a manifest or registry answer is not "what the cluster runs". Each running reference carries the pod's digest and, when the pod reported the image it was started from, that image's tag — so a bare manifest tag is compared with what the pod pulled, not with a tag the resolver assumed.
The cluster is opened per call, so an unreachable one is one env's "cluster not asked" sentence, never a reason the TUI fails to open.
func (*Service) StartPromotion ¶
StartPromotion takes a confirmed plan the rest of the way to a drivable promotion: preflight, freshness, the one-in-flight claim, the first durable state save and the claim's release, all in one place — the CLI's own runPromote/runDeploy and the TUI's own buildStartPromotion used to each assemble this sequence by hand, in two different orders, which is what let the TUI reach engine.DirectCommitGateStep only after its own claim and save, and let the two faces run the claim/save order and the preflight order differently. This function is the one, canonical order both faces now call through:
Preflight → freshness (by View) → [direct] fresh-base occurrence check → no-op (*AlreadyCurrentError) → forge identity → ArgoAppNames/EditApps → Argo/Rollout(KubeContext) → claimTarget → newState (Direct set) → Store.Save → release → Driver.
Two behaviour changes fall out of unifying on this order (accepted divergences, not defects):
- The claim is now released only after the state's first successful save lands (the TUI's own order) — the CLI used to release inside its save wrapper's own closure, which is observably the same EXCEPT when Drive's very first Observe fails before its own first save: a state file now remains in that case (Phase ""), blocking the env until `resume`/ `abandon` — already true for the TUI before this change.
- A direct no-op whose origin has an occurrence the local checkout has never seen is now refused with that specific error, on both faces, rather than the TUI's old "already current" (the fresh-base check now always runs before the no-op check, the CLI's own order).
type Settings ¶
type Settings struct {
RepoDir, AppsRoot string
Base string
Promotable []string
// KubeContext is the already-reconciled context for the CURRENTLY selected repo: the
// flag when given, else Repo's own kube.context, else "" (the kubeconfig's current
// context). Every cluster-touching call for THIS run reads this field directly — never
// re-derives it from Repo.Kube.Context itself, which is what collapses cmd/hoist's five
// duplicate fallbacks (resolution.go, deploy.go, restart.go, history.go, main.go) into
// the one place that computes it (selectRepo, cmd/hoist).
KubeContext string
// KubeOverride is the flag alone, never a config fallback — what `hoist promotions`/
// `hoist resume --env` and the TUI's in-flight pane use to observe a promotion against a
// DIFFERENT repo's own state file, where Repo (below) does not name that other repo's
// config entry. KubeContextFor is how a caller applies it against some other RepoConfig.
KubeOverride string
// KubeOverrideSet says the flag was given at all. A given-but-empty --kube-context means
// the kubeconfig's current context, not "the config decides", so "" alone cannot carry it
// (#295; AGENTS.md §9 entry 20).
KubeOverrideSet bool
// Repo is the selected repo's own config entry, nil when running on flags alone (no
// repos[] configured, or --repo named an unconfigured path). Config is the whole loaded
// file, needed for cfg.Registries and for repoConfigFor-style repo lookups by GitHub name
// when observing some other promotion's state.
Repo *config.RepoConfig
Config *config.Config
// Resolve is the digest-resolution chain for THIS run: order, registry auth, cluster
// secret, op ref (NewResolveOptions, resolve.go).
Resolve ResolveOptions
Poll engine.PollIntervals
Deadline time.Duration
Retain time.Duration
}
Settings is what one invocation resolved from flags and the config file, once, at the boundary — cmd/hoist's selectRepo builds it from the flag/config precedence; the TUI shares the exact same struct rather than rebuilding an equivalent one, which is what let the two faces' kube-context fallback drift into five separate copies.
func (Settings) ArgoNamespace ¶ added in v0.3.0
ArgoNamespace is where the Application custom resources live for the selected repo (RepoConfig.Kube.ArgoNamespace), or the default when running from flags alone.
func (Settings) CheckRestartConfirmed ¶ added in v0.3.0
CheckRestartConfirmed is production's gate for a restart. §4.5's PR-and-approval-comment gate cannot apply to an operation that commits nothing, so what stands in its place is a second, distinct acknowledgement repeating the env's exact name: a production restart must never be one keystroke or one line of shell history. confirm is that acknowledgement, "" when none was given. A refusal is a *UsageError; an env the config does not list as production needs none.
With no repos[] entry selected there is no envs.production list, so every env would look non-production by omission. That fails closed, as the direct-commit preflight does (§8: never provide a fallback default for required configuration) — though not outright, since restarting against an unconfigured repo is a legitimate thing to want: it is "we cannot tell, so acknowledge it".
func (Settings) KubeContextFor ¶
func (s Settings) KubeContextFor(rc config.RepoConfig) string
KubeContextFor is the one precedence a caller applies when it holds some OTHER RepoConfig than the currently selected one (an in-flight promotion for a different repos[] entry): the operator's own --kube-context override wins outright, exactly as it does for the selected repo in selectRepo, even when it was given empty (the kubeconfig's current context, KubeOverrideSet); otherwise that repo's own configured context. The one place this precedence is applied.
func (Settings) PairEnvs ¶ added in v0.3.0
PairEnvs fills in a promotion's source from envs.pairs (source env -> target env), so `hoist plan` and `hoist promote` need only name where to promote TO (#173): --to is the required end, and --from is optional, defaulting to the one env envs.pairs promotes into it (the target's immediate ancestor). The target is never derived: a lone --from does not say where to promote, and filling it in would let a command line that never names an env write into it (production included), so it is refused, as it was before pairs were read by the CLI at all. With both given, or neither, from and to are returned as they are; neither names no promotion (a config with several pairs does not say which is meant) and the caller refuses it as it always did.
Given only to, the source is the one env envs.pairs promotes into it. Anything short of exactly one is a *UsageError that says what the config could not decide and which flag to give, so a defaulting that is ambiguous is refused, never guessed (principle 5 warns, but a wrong source env reads the wrong digests).
func (Settings) PreflightConfig ¶ added in v0.3.0
PreflightConfig checks the configuration a start needs, before anything else is asked. Every promotion needs repos[].github (the forge identity); a direct one needs a configured repo too. Refusals are *UsageError.
func (Settings) PreflightGate ¶ added in v0.3.0
PreflightGate runs engine.DirectCommitGateStep for a direct start into target: a production env is refused outright, and an unconfirmed direct commit is blocked (m.Confirmed). It is the step's own Observe, not a copy of its rule.
func (Settings) ProductionEnvs ¶
ProductionEnvs is Repo.Envs.Production, unfiltered — flags-only runs (Repo nil) have no production list at all, which AGENTS.md §4.5 treats as "nothing is known to be production", never as "everything is", since a flags-only invocation has no config to have declared one.
type StartRequest ¶
type StartRequest struct {
Plan gitops.Plan
Mode Mode
// Repo is the discovered GitOps repo ArgoAppNames/EditApps read from. nil reads s.Repo().Repo
// AT CALL TIME, not when this request was built — this fixes FB-M1: the TUI's old
// buildStartPromotion closed over a boot-time *gitops.Repo that F5 never updated, so an Argo
// Application renamed after boot was still looked up under its stale name. The CLI passes its
// own freshly discovered repo here instead, since a one-shot process only ever discovers once
// anyway.
Repo *gitops.Repo
// View picks the freshness check: nil reads s.Repo() at call time (same reasoning as Repo
// above). A view whose FromOrigin is true is checked with CheckRepoViewCurrent (the TUI's own
// origin-cache check); everything else — including the CLI's own zero RepoView, since it
// never calls LoadRepo — is checked with checkCloneCurrentForBase (the local-disk check
// against a freshly fetched origin/<base>).
View *RepoView
// Fresh carries the digests/reasons this plan was actually resolved with, for direct mode's
// own fresh-base occurrence check (checkFreshBase). nil recovers them from Plan.Edits instead
// — the TUI's rule, unchanged: a filtered, ticked-down plan has no single resolution
// behind it any more.
Fresh *freshInputs
}
StartRequest is everything StartPromotion needs to take a confirmed plan the rest of the way: claim the target env, build (or resume) its PromotionState, save it and hand back a Drive.
type StateStore ¶
type StateStore interface {
List() ([]*engine.PromotionState, error)
ListArchived() ([]*engine.PromotionState, error)
Load(id string) (*engine.PromotionState, error)
Save(*engine.PromotionState) error
Delete(id string) error
Archive(id string) error
Claim(repoFullName, targetEnv, id string) (release func(), err error)
WorktreeDir(id string) (string, error)
}
StateStore is what a Service reads and writes promotion state through — the interface FileStore below implements over engine's own state.go/claim.go functions, and the one a test's fake can implement instead without touching $XDG_STATE_HOME.
type TagSource ¶ added in v0.3.0
type TagSource struct {
// Mapped is the static answer the picker starts from: repos[].apps names an app repo AND a
// forge client for it could be built. False when it is not mapped, when the forge could not
// be built (warn, don't block — principle 5: the picker still opens, in registry order) and
// when the registry client itself could not.
Mapped bool
// contains filtered or unexported fields
}
TagSource is what the tag picker reads for one image repo: its registry tags, the mapped app repo's git tags when repos[].apps maps it, and one tag's registry metadata. OpenTagSource builds it; each method then asks the registry or forge itself, so the picker can render the registry's fast answer without waiting on the forge's slow N+1 crawl (forge.Forge.Tags).
func (*TagSource) Err ¶ added in v0.3.0
Err is why the registry client could not be built, nil when it was. A caller that has no use for the other methods without one (the picker shows the error on the registry list and has no metadata to fetch) checks it.
func (*TagSource) GitTags ¶ added in v0.3.0
GitTags lists the mapped app repo's git tags (commit dates, for invariant 3's ordering). mapped is this call's own observed answer: true with the tags, false with the forge's error when the lookup fails at run time, so the picker degrades to registry order and a caller that wants to can say why. Before this reported the error it was dropped, and a forge failure was indistinguishable from an unmapped repo.
func (*TagSource) HasGitTags ¶ added in v0.3.0
HasGitTags reports whether GitTags has anything to ask: the repo is mapped to an app repo with a working forge client.
type Tick ¶
type Tick struct {
State engine.PromotionState
Done bool
Statuses []engine.StepStatus
Waiting bool
Blocked *engine.BlockedError
Retry bool
Wait time.Duration
}
Tick is one poll iteration's result — the CLI's own retry/heartbeat loop and the flight screen's tick loop both drive off exactly this shape, so neither reimplements the other's retry/wait decision (AGENTS.md §9 entry 11's own lesson: a decision made twice, even correctly today, drifts). State is a value copy of the promotion's state as it stood the instant this Step call returned — safe for a caller to hold or render without racing a later Step call on the same Drive. Done and Statuses mirror engine.DriveStatus's own return shape exactly; Waiting and Blocked are the same distinction engine.Observation makes, surfaced here because Step's own err is never ErrWaiting or a *BlockedError — see Driver.Step's own doc comment for why. Retry and Wait are meaningful together: Retry is engine.Retryable(err) — true only when Step actually returned a genuine error and the failing step is one of engine.RetryableStep's five — and Wait is engine.PollInterval for whichever step is active (the failing step, when there was one, else the promotion's own current phase), always populated regardless of err so a caller scheduling the NEXT poll never has to re-derive it from statuses itself.
type UnconfirmedError ¶
UnconfirmedError is FindInFlightForEnv's own refusal to silently exclude a candidate it could not re-observe (a transient forge/git error, a repo removed from config since the promotion started): every candidate targeting Env that could not be confirmed either way is named in Errs (sorted before this error is built), rather than treated as "not in flight" — which could misleadingly report NotFoundError when the real reason is that the answer is simply unknown.
func (*UnconfirmedError) Error ¶
func (e *UnconfirmedError) Error() string
type UsageError ¶
type UsageError struct{ Msg string }
UsageError is a caller-fixable misconfiguration Preflight/StartPromotion refuse before ever touching git, the forge or the cluster — a missing repos[].github, or --direct without a configured repo at all (AGENTS.md §4.5/§8: never provide a fallback default for required configuration). Its own type, rather than a plain fmt.Errorf, so a caller (cmd/hoist) can map it to its own "usage" exit code without string-matching the message.
func (*UsageError) Error ¶
func (e *UsageError) Error() string
type Watch ¶ added in v0.3.0
type Watch struct {
// App and Namespace name what is watched; KubeContext is the kubeconfig context the clients
// resolved to, by name.
App, Namespace, KubeContext string
// Interval is the cadence both faces poll at: the tighter of poll.argo and poll.rollout,
// since one tick refreshes an Argo status and a workload's, so neither reading is staler than
// its own configured cadence already promises. There is no dedicated watch knob.
Interval time.Duration
// contains filtered or unexported fields
}
Watch is a read-only handle on one Argo Application and the workloads its family declares, opened by OpenWatchApp or OpenWatchFamily. It holds the two cluster clients it reads through, and nothing here can write: Read calls Argo.Get, Rollout.Deployment and Rollout.JobLike only — never Argo.Refresh — which is the whole of watch's read-only guarantee (argo.Argo does have a Refresh method, so it holds by the calls below and by the tests that assert no Refresh ever reaches a fake, not by the type).
type WatchSnapshot ¶ added in v0.3.0
type WatchSnapshot struct {
// App is the Application's metadata.name; Namespace its destination namespace (the env).
App, Namespace string
// SyncStatus, HealthStatus, Revision and OperationPhase are Argo's own status words, ""
// when Argo has not filled one in; ReconciledAt is zero when it never has.
SyncStatus, HealthStatus, Revision, OperationPhase string
ReconciledAt time.Time
Workloads []WatchWorkload
}
WatchSnapshot is one read of an Argo Application and the workloads its family declares: plain values, no cluster types, so the watch screen and `hoist watch` render the same thing from the same read. Workloads lists every Deployment, then every Job and CronJob.
type WatchWorkload ¶ added in v0.3.0
type WatchWorkload struct {
Kind, Name string
// Replicas is a Deployment's spec.replicas; 0 for a Job/CronJob, which has none.
Replicas int32
// Complete and DeadlineExceeded are kubectl's own rollout verdict, Deployments only.
Complete, DeadlineExceeded bool
// Detail is the kubectl-style line: what is still pending, or that it rolled out.
Detail string
// Images are "container name=ref" pairs, initContainers marked.
Images []string
}
WatchWorkload is one Deployment, Job or CronJob's current state.