plan

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Sep 6, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

Documentation

Overview

Package plan is the plan/confirm screen: it runs discovery + digest resolution + gitops.BuildPlan for one source/target env pair, shows a tickable list of image repos on the left and, on the right, what the promotion ships for the repo under the cursor — the commits between what the target declares and what the source resolved to, and the migrations among them (M10, #85 screen 04/12) — with the unified diff one key away. rows.go derives the rows, the diff and the warning text from a gitops.Plan and resolve.Resolution map with no terminal dependency, so it is unit-testable as plain values (AGENTS.md §4.8); model.go lays that data out with huh fields (MultiSelect for the repo list, Select for the target-env prompt, Confirm for the direct-mode switch) and a bubbles/v2 spinner + viewport, inside the M10 frame.

Index

Constants

View Source
const (
	ModePR     = "pr"
	ModeDirect = "direct"
)

Mode is the write path this plan would use.

Variables

This section is empty.

Functions

func CommonPrefix

func CommonPrefix(rows []Row) string

CommonPrefix is the image-repo prefix every row shares, up to and including its last "/", so the repo names on the rows can drop it and never wrap mid-token (#85: the version you are approving was being split across lines at an arbitrary column). "" when the rows share nothing, or there is only one row (no prefix is shorter than the whole name).

func IsProduction

func IsProduction(env string, envs config.EnvsConfig) bool

IsProduction reports whether env is listed in the repo's envs.production (AGENTS.md §4.5): direct mode is never offered for it, whatever the source env.

func RenderDiff

func RenderDiff(root string, edits []gitops.Edit, ticked map[string]bool) (string, error)

RenderDiff renders a unified diff of every edit whose repo is ticked, reading each file once from disk under root and applying the same read → ApplyBytes → Verify → UnifiedDiff path cmd/hoist's plan --dry-run uses (main.go printPlan) — so the screen's diff is byte-identical to the CLI's for the same ticked set. A NoOp edit (the target already carries the planned ref) is skipped, exactly as printPlan skips it into "Already current" rather than a diff hunk.

func SkippedStaging

func SkippedStaging(source, target string, envs config.EnvsConfig) (staging string, skip bool)

SkippedStaging reports the configured staging env for source when target is production but is not that configured pair — the "deploying straight to production, skipping <staging>" warning (AGENTS.md §4.5). It never blocks (principle 5); skip is false when target is not production, or is exactly the configured pair, or source has no configured pair to skip.

func Summary

func Summary(o ResolveOutcome) []string

Summary is the resolution section: which cluster context and registry credential source were consulted (by name only, AGENTS.md §4.4), then each resolved repo's source and detail, in repo order. It renders the same facts cmd/hoist's resolutionReport.print does.

func TargetsFor

func TargetsFor(r *gitops.Repo, source string) []string

TargetsFor lists the candidate target envs for source: every other discovered env, sorted. It backs the huh.Select shown when the current env has no configured pair.

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: cmd/hoist's `deploy` and the TUI's own deploy screen both call it, because a warning only one of the two entry points attaches is a warning half the operators never see — which is exactly the bug the constant replaced (an ad-hoc string in one screen's View, reaching neither the dry run nor the PR body). 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.

func WarningRepo

func WarningRepo(w gitops.Warning) string

WarningRepo is the repo a warning is about: every Warning built by pkg/gitops and pkg/resolve carries Occurrences for exactly one repo (never a mix), so the first occurrence's ref names it; "" when a warning carries no occurrence at all. Exported so internal/app's filterTicked (app.go) can correlate a Plan's Warnings against the operator's ticked selection the same way DeriveRows already groups them, rather than re-deriving the "first occurrence's ref" convention a second time.

Types

type BackMsg

type BackMsg struct{}

BackMsg is emitted when the screen wants the root to pop back to whatever was underneath it. internal/app/doc.go notes that pop arrives with the first screen pushed on top of the matrix; this is that screen, so the root recognizes BackMsg by its concrete type in its own Update switch — screens still never import app (AGENTS.md §4.8).

type Model

type Model struct {
	// contains filtered or unexported fields
}

Model is the plan screen. It is a value: Update, SetSize and SetStyles return the updated model, matching internal/app/matrix's convention.

func New

func New(repo *gitops.Repo, promotable []string, envs config.EnvsConfig, source, target string, forcePrompt bool, resolveFn ResolveFunc, hist history.Funcs) Model

New builds the plan screen for one source env. target is the configured pair for source (envs.pairs[source]), or "" when there is none; forcePrompt is true when the matrix screen's P (rather than p) opened it, which always prompts even when a pair is configured. resolveFn is nil in "digest sources: none" mode. hist is the commit-history bundle (M10); a zero value degrades every repo to a named gap.

func (Model) CapturesText

func (m Model) CapturesText() bool

CapturesText implements app.Screen (via planScreen's thin delegate in internal/app/screen.go). The root queries this before treating "q" as its own global quit key (round 5, finding 3). huh.Select and huh.MultiSelect both support their own "/" filter-typing mode (GetFiltering), but that mode — like Down/Up/Space navigation generally — is only reachable once huh.Field.WithKeyMap has been called on the field. That normally happens automatically inside a huh.Form/Group; this screen uses both fields standalone (AGENTS.md §4.7, "no layout library", ruled out adopting huh.Form just for its wiring), so buildEnvSelect and buildMultiSelect call WithKeyMap directly at construction time instead — plain component wiring, not a layout dependency. With that in place, query whichever field is actually live for its own real filtering state: the env-select prompt while it's up, or the multiSelect while it holds focus and no huh.Confirm dialog is covering it.

func (Model) Init

func (m Model) Init() tea.Cmd

Init kicks off whatever the starting state needs: the env-select prompt's focus, or the spinner tick plus the async load. Resolution talks to a cluster/registry, so it is a tea.Cmd here, never run inside Update.

func (Model) SetSize

func (m Model) SetSize(width, height int) Model

SetSize lays every huh field and the viewport out inside width × height.

func (Model) SetStyles

func (m Model) SetStyles(s ui.Styles) Model

SetStyles applies the palette (and its dark/light flag to every huh field via huh's own Charm theme — AGENTS.md §4.7: no layout library, but a component's own theming is not one).

func (Model) Update

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd)

Update handles the screen's own keys, the loading messages, and forwards everything else to whichever huh field or bubbles component owns the current state.

func (Model) View

func (m Model) View() string

View renders the current state. Every rendered string passes through redact.Strings once more here, at the output boundary, in addition to each render point that already calls it (Summary's per-repo Detail, the disabled-row Reason, warning messages, viewReady's own fatal-error line) — so a display field that forgets to redact itself, or a credential registered after an earlier call already built its string, is still caught before it reaches the terminal (AGENTS.md §4.4/§4.10, R-002).

func (Model) WithNow

func (m Model) WithNow(now func() time.Time) Model

WithNow fixes the clock relative dates are worded against (tests).

type ResolveFunc

type ResolveFunc func(ctx context.Context, repo *gitops.Repo, source string) (ResolveOutcome, error)

ResolveFunc resolves the source env's promotable occurrences to digests. cmd/hoist supplies it, wrapping whichever cluster and registry adaptors the CLI's own plan command builds (kube context, registry credential chain) — so this package never opens a cluster or registry connection itself (AGENTS.md §4.3) and never imports cmd (AGENTS.md §4.8). It always talks to a cluster/registry when called, so model.go calls it only from inside a tea.Cmd, never from Update directly. A nil ResolveFunc means "digest sources: none" from the start (no config, or resolution deliberately turned off); an error from a non-nil one degrades the same way, with a warning, rather than failing the screen.

type ResolveOutcome

type ResolveOutcome struct {
	Resolutions map[string]resolve.Resolution
	KubeContext string
	// RegistryAuth names the credential source that authenticated, "" when none did.
	RegistryAuth string
	// RegistryConsulted is true when the registry was asked at all (win or lose) —
	// distinct from RegistryAuth == "", which is also true when the registry was never
	// consulted in the first place. Summary uses the two together so "not consulted" and
	// "consulted, every source failed" are never confused, the same distinction
	// cmd/hoist's own resolutionReport.print makes (AGENTS.md §4.10).
	RegistryConsulted bool
	// RegistryAuthTried names the configured credential chain, for the "all failed"
	// wording when RegistryConsulted is true and RegistryAuth is "".
	RegistryAuthTried []string
}

ResolveOutcome is what one ResolveFunc call returns: the resolution per repo plus which kube context and registry auth source were actually consulted, by name only (AGENTS.md §4.4) — the same facts `hoist plan --dry-run` prints in its Resolution section. A zero value is "digest sources: none": BuildPlan then plans from the manifests alone, exactly as M1 did.

type Row

type Row struct {
	Repo string
	// Old and New are the references shown as "old-tag → new-tag"; Old is the first target
	// occurrence's existing ref (occurrences of one repo in one env always share a tag in
	// practice, and a disagreement is its own warning), New is what the plan would write.
	Old, New image.Ref
	// Count is the number of target-env occurrences this repo's edits touch.
	Count int
	// Source names where the digest came from: a resolve.Source ("pods", "manifest",
	// "registry", "override"), "manifest" when no resolution ran at all (digest sources:
	// none, exactly as M1 planned), or "" only for a disabled row.
	Source string
	// Warnings are every warning (resolution or plan) attached to this repo; a non-empty
	// list draws the "!" marker on the row.
	Warnings []gitops.Warning
	// Disabled is true when pkg/resolve could not supply a digest for this repo at all
	// (WarnUnresolved): the row is shown but never offered as a tickable option, since huh
	// has no per-option disable and the row's own reference may still be a stale manifest
	// pin no one has confirmed against the running env.
	Disabled bool
	// Reason explains Disabled; "" otherwise.
	Reason string
	// Files is the number of distinct target-env files this repo's edits touch.
	Files int
}

Row is one image repo in the left pane, derived from a built Plan and (when resolution ran) its Resolutions. It carries no terminal dependency so it is unit-testable as plain values (AGENTS.md §4.8); model.go lays it out and model.go alone talks to huh.

func DeriveRows

func DeriveRows(pl gitops.Plan, res map[string]resolve.Resolution) []Row

DeriveRows groups pl.Edits by image repo into one Row each, sorted by repo. res is the resolve.Resolve output for the source env; a nil or empty map means no resolution ran (digest sources: none) and every row's Source reads "manifest", matching what BuildPlan alone would have written in M1.

func Disabled

func Disabled(rows []Row) []Row

Disabled is the rows shown without a checkbox, each with its reason.

func Selectable

func Selectable(rows []Row) []Row

Selectable is the rows huh.MultiSelect offers a checkbox for.

func (Row) Label

func (r Row) Label() string

Label is the row text: "repo old → new (n occurrences) [source]", with a leading "!" when the row carries warnings.

func (Row) ShortLabel

func (r Row) ShortLabel(prefix string, width int) string

ShortLabel is the row text for the left pane (M10): the repo without the prefix every row shares (CommonPrefix, shown once in the header), the versions, and "!" when the row carries warnings — short enough that the version never wraps mid-token at the pane's width; the occurrence count and source moved to the right pane's own head line.

type StartMsg

type StartMsg struct {
	Plan    gitops.Plan
	Outcome ResolveOutcome
	Mode    string
	// Ticked is the repo set the operator selected in the multiSelect, unmodified — the
	// same set recomputeDiff already filters Plan.Edits by.
	Ticked         []string
	Source, Target string
}

StartMsg is emitted when the operator confirms this plan (Enter, in stateReady) — whichever ticked repos are selected should now start driving as a promotion. It carries plan-shaped data only: this package has no repoFullName (RepoConfig.GitHub), no CI/ approval policy, and no git.Git/forge.Forge adaptor to build a real engine.PromotionState or a flight.DriveFunc from (AGENTS.md §4.3/§4.8 — a screen never imports those adaptor packages). The root recognizes StartMsg by concrete type (AGENTS.md §4.8) and pushes internal/app/flight.

Jump to

Keyboard shortcuts

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