hotfix

package
v0.13.0-rc.0 Latest Latest
Warning

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

Go to latest
Published: Jul 9, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Overview

Package hotfix plans and validates per-environment hotfixes that apply a trunk commit onto an environment pinned to an older trunk base.

The plan verb computes and validates only; the cherry-pick, build, deploy, and state write happen in the generated workflow. A plan with --dry-run mutates nothing.

Index

Constants

View Source
const EnvBranchPrefix = "env/"

EnvBranchPrefix is the prefix of the per-environment integration branches a hotfix creates (for example env/test). A branch carrying this prefix exists only while its environment is diverged; once the env rejoins trunk the branch is deleted.

Variables

This section is empty.

Functions

func EnvBranchName added in v0.12.0

func EnvBranchName(component, env string) string

EnvBranchName returns the integration branch name for env within component. The default (empty) component yields env/<env>, byte-identical to the historical single-component form; a named component yields env/<component>/<env> so each component's integration branches occupy a disjoint namespace and a hotfix on one component can never touch another's.

func HealOrphanEnvBranches added in v0.6.0

func HealOrphanEnvBranches(component string, branches []string, state map[string]*config.EnvState, remote string, del func(remote, branch string) error) ([]string, error)

HealOrphanEnvBranches deletes every integration branch that OrphanEnvBranches flags for component as having no matching divergence, calling del to remove each branch on remote. In production del is git.DeleteRemoteBranch, whose delete of an absent branch is a no-op success, so HealOrphanEnvBranches is idempotent: re-running it, or running it against an orphan that is already gone, deletes nothing further and never errors.

Only orphans in component's own namespace are deleted. A branch backing a diverged environment, and every sibling component's branch, is never touched, because the deletion set comes from OrphanEnvBranches, which excludes them by the component filter and the same IsDiverged() predicate the hotfix preflight and status consistency classify on. The returned slice lists the branches deleted, in input order, and is nil when nothing was orphaned. On the first delete error the heal stops and returns that error with a nil healed slice.

func HotfixTagsForBase

func HotfixTagsForBase(spec taggrammar.Spec, baseVersion string, tags []string) []string

HotfixTagsForBase returns the hotfix tags in tags that belong to the pre-release base of baseVersion under spec. A hotfix tag has the dotted shape <base>-<token><sep>N.hotfix.M (for example vX.Y.Z-rc.N.hotfix.M); it shares the pre-release base of the version the environment held while diverged. The pre-release-shaped cleanup in internal/release deliberately cannot see these tags, so divergence-end cleanup must collect them explicitly.

baseVersion may itself be a hotfix version; it is normalized to its pre-release base before matching. Parsing uses spec so a custom pre-release token still resolves. Tags that do not parse, are not hotfix tags, or belong to a different pre-release base are excluded. The result is nil when nothing matches.

func NewCommand

func NewCommand() *cobra.Command

NewCommand creates the `cascade hotfix` command and its subcommands.

func OrphanEnvBranches

func OrphanEnvBranches(component string, branches []string, state map[string]*config.EnvState) []string

OrphanEnvBranches returns the integration branches in branches that belong to component and have no matching divergence in state. state is component's own env subtree (state.components.<component>.<env>), and for the default empty component it is the historical env-keyed state map. A branch is healthy only while state[<env>] reports IsDiverged(); a branch with no diverged env behind it is an orphan left over from an interrupted hotfix or manual meddling and is flagged.

Only branches whose parsed component equals component are considered, so a component never inspects, and never flags, a sibling's env/<other>/<env> branch, and the default component ignores every component-nested branch. Non env/* branches are ignored. The returned slice preserves the input order and is nil when nothing is orphaned, so callers can treat a nil result as "consistent".

func ParseEnvBranch added in v0.12.0

func ParseEnvBranch(branch string) (component, env string, ok bool)

ParseEnvBranch splits an integration branch name into its component and env, the inverse of EnvBranchName. It reports ok=false for any branch that does not carry EnvBranchPrefix or whose remainder is not a well-formed env/<env> or env/<component>/<env>.

env/<env> parses to an empty component and <env>, preserving the historical single-component reading. env/<component>/<env> parses to <component> and <env>. The segment count after the prefix disambiguates the two forms, so a nested branch never has its component folded into the env: env/web/staging parses to component "web" and env "staging", not the naive strings.TrimPrefix("env/") result of env "web/staging". A bare prefix or a more deeply nested name is malformed and reports ok=false.

Types

type EnvPlan added in v0.3.0

type EnvPlan struct {
	Env     string `json:"env"`
	Branch  string `json:"branch"`
	BaseSHA string `json:"base_sha"`

	// Commits are the fix SHAs still to cherry-pick into this environment, in
	// the caller's requested ref order, after skipping commits already present.
	Commits []string `json:"commits"`

	// NoOp is true when the whole requested set is already present in this env.
	NoOp bool `json:"no_op"`

	// BranchReset is true when the remote env/<env> branch existed at a stale tip
	// but was force-reset back to BaseSHA by the orphan self-heal. It is set only
	// when the env is not diverged and the single-flight gate ran with a real
	// checker that found no open resolution PR.
	BranchReset bool `json:"branch_reset"`

	// ConflictExpected hints whether a cherry-pick is likely to conflict. The
	// plan verb does not run the cherry-pick, so this is best-effort and false
	// by default; the workflow is authoritative.
	ConflictExpected bool `json:"conflict_expected"`
}

EnvPlan is the plan for one environment in the chain: the env branch, its recorded base SHA, and the ordered list of commits still to apply after per-(commit,env) idempotency skips. NoOp is true when every requested commit is already present in that environment.

type FinalizeOption

type FinalizeOption func(*Finalizer)

FinalizeOption configures optional, additive Finalizer behavior.

func WithComponent added in v0.12.0

func WithComponent(name string) FinalizeOption

WithComponent scopes the finalize to a declared component. It records state under state.components.<name>.<env>, resolves the hotfix version and tag in the component's tag namespace, and names the integration branch env/<name>/<env>. An empty name (the default) keeps the single-component behavior byte-identical.

func WithFinalizeDryRun

func WithFinalizeDryRun(dryRun bool) FinalizeOption

WithFinalizeDryRun computes the finalize plan without writing state, tags, or release objects.

func WithReleaseManager

func WithReleaseManager(m releaseManager) FinalizeOption

WithReleaseManager injects the release operations. When unset, finalize builds a *release.Manager from GITHUB_REPOSITORY and the release token at run time.

func WithStatePusher

func WithStatePusher(p statePusher) FinalizeOption

WithStatePusher injects the manifest commit/push. The default reuses the shared rebase-retry helper.

func WithTagLister

func WithTagLister(l tagLister) FinalizeOption

WithTagLister injects the existing-tag lookup used for version allocation.

func WithTipReader added in v0.5.0

func WithTipReader(r gitTipReader) FinalizeOption

WithTipReader injects the reader Finalize uses to cross-check the merge SHA against the resolution branch tip. The default reads the local env-branch tip via git. The what-if simulator injects a record-only reader so finalize can run without a git checkout.

func WithTrunkStateReader

func WithTrunkStateReader(r trunkStateReader) FinalizeOption

WithTrunkStateReader injects the reader that returns the manifest as it exists on the trunk branch. Finalize uses it to read prior env state from trunk, the source of truth, rather than from the lagging env branch the hotfix merged into. The default reads via the GitHub Contents API on real GitHub and plain git under act.

type Finalizer

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

Finalizer writes the diverged state, tag, and release object for a completed hotfix. It mirrors the inputs of promote's Finalizer but targets one env on its integration branch rather than a trunk promotion.

func NewFinalizer

func NewFinalizer(opts FinalizerOptions, options ...FinalizeOption) (*Finalizer, error)

NewFinalizer constructs a Finalizer over the manifest at opts.ConfigPath.

func (*Finalizer) Finalize

func (f *Finalizer) Finalize(targetEnv, mergeSHA string, fixSHAs []string, baseSHA string) error

Finalize writes the diverged state for a completed hotfix on env/<targetEnv>.

targetEnv is the environment being hotfixed; mergeSHA is the tip of env/<targetEnv> after the resolution PR merged; fixSHAs are the trunk commits the hotfix carries (every cherry-picked commit, in apply order); baseSHA is the trunk anchor the integration branch diverged from. It cross-checks the merge SHA against the env-branch tip, allocates the next free hotfix version, snapshots the prior state into the Previous ring, writes the divergence fields and substates, commits the manifest to trunk, and creates the hotfix tag and release object. Every commit in fixSHAs is appended to the env's recorded patch set so a multi-commit hotfix records all of its commits, not just the first.

Finalize is idempotent on identical inputs: a rerun after the state already records the merge SHA is a no-op that neither double-applies patches nor re-snapshots Previous.

func (*Finalizer) SetBuildResult

func (f *Finalizer) SetBuildResult(name, result string)

SetBuildResult records the conclusion of a build job. Valid results mirror SetDeployResult; only successful builds update per-build state.

func (*Finalizer) SetDeployResult

func (f *Finalizer) SetDeployResult(name, result string)

SetDeployResult records the result of a deploy job, mirroring promote.Finalizer.SetDeployResult. Valid results: "success", "failure", "skipped", "cancelled". Only successful deploys update per-deploy state.

type FinalizerOptions

type FinalizerOptions struct {
	ConfigPath  string
	ManifestKey string
	Actor       string
}

FinalizerOptions carries the required inputs for NewFinalizer.

type OpenPR

type OpenPR struct {
	Number int    `json:"number"`
	URL    string `json:"url"`
}

OpenPR is a minimal view of an open pull request returned by a PRChecker.

type Option

type Option func(*Planner)

Option configures optional, additive Planner behavior.

func WithDryRun

func WithDryRun(dryRun bool) Option

WithDryRun controls whether the planner mutates anything. When true the env branch is computed but not created.

func WithPRChecker

func WithPRChecker(c PRChecker) Option

WithPRChecker injects the single-flight PR lookup. The default reports no open PRs.

func WithPlanComponent added in v0.12.0

func WithPlanComponent(name string) Option

WithPlanComponent scopes the plan to a declared component. It names the integration branch env/<component>/<env> so the plan agrees with the component-aware finalize path and each component's branches occupy a disjoint namespace. An empty name (the default) keeps the single-component behavior byte-identical, naming the branch env/<env>. This mirrors the finalize package option WithComponent; the two names differ only because the plan and finalize option types are distinct.

func WithRemote

func WithRemote(remote string) Option

WithRemote overrides the git remote env branches live on (default "origin").

type PRChecker

type PRChecker interface {
	// OpenHotfixPRs returns open PRs labeled cascade-hotfix whose base is baseBranch.
	OpenHotfixPRs(baseBranch string) ([]OpenPR, error)
}

PRChecker reports open hotfix PRs targeting a given base branch. It is the single-flight gate: the plan verb refuses to proceed while a hotfix PR is already open against the target env branch. The default implementation is a no-op that reports no open PRs, so callers without GitHub context (and unit tests) are not forced to provide one.

type PlanChainResult added in v0.3.0

type PlanChainResult struct {
	// Envs are the per-environment plans in bottom-up chain order (the first
	// environment is excluded; the target environment is the last entry).
	Envs []EnvPlan `json:"envs"`
}

PlanChainResult is the computed, validated plan for elevating a set of trunk commits across the bottom-up environment chain up to and including a target environment. It is the multi-commit, multi-env companion to PlanResult and is additive: the single-env Plan and PlanResult are unchanged.

type PlanResult

type PlanResult struct {
	TargetEnv string `json:"target_env"`
	FixSHA    string `json:"fix_sha"`
	Branch    string `json:"branch"`
	BaseSHA   string `json:"base_sha"`

	// NoOp is true when the fix is already contained in the target state SHA.
	NoOp bool `json:"no_op"`

	// BranchCreated is true when env/<target> was (or, in dry-run, would be)
	// created at BaseSHA. False when it already existed at the expected tip.
	BranchCreated bool `json:"branch_created"`

	// BranchReset is true when env/<target> existed at a stale tip but was (or, in
	// dry-run, would be) force-reset back to BaseSHA by the orphan self-heal. It is
	// set only when the env is not diverged and the single-flight gate ran with a
	// real checker that found no open resolution PR.
	BranchReset bool `json:"branch_reset"`

	// HotfixVersionCandidate is the next free hotfix version over the target
	// env's current version base (e.g. v1.0.0-rc.1 -> v1.0.0-rc.1.hotfix.1).
	HotfixVersionCandidate string `json:"hotfix_version_candidate"`

	// ConflictExpected hints whether the cherry-pick is likely to conflict.
	// The plan verb does not run the cherry-pick, so this is best-effort and
	// false by default; the workflow is authoritative.
	ConflictExpected bool `json:"conflict_expected"`

	// ProtectionSuggestions are ready-to-run gh/gh api commands an operator can
	// paste to establish env/* branch protection. cascade never applies these.
	ProtectionSuggestions []string `json:"protection_suggestions"`

	DryRun bool `json:"dry_run"`
}

PlanResult is the computed, validated hotfix plan emitted as JSON and GHA outputs. It records only what the workflow needs; no mutation has happened beyond the optional env-branch creation.

type Planner

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

Planner validates and computes a hotfix plan for one environment.

func NewPlanner

func NewPlanner(opts PlannerOptions, options ...Option) (*Planner, error)

NewPlanner constructs a Planner from the manifest at opts.ConfigPath.

func (*Planner) Plan

func (p *Planner) Plan(fixRef, targetEnv string) (*PlanResult, error)

Plan validates the hotfix request and computes the env-branch plan.

fixRef is the trunk commit (or ref/short SHA) to apply; targetEnv is the environment to hotfix. It enforces, in order: trunk ancestry of the fix, target-env eligibility, no-op detection, the single-flight open-PR gate, and env-branch reconciliation. The single-flight gate runs before any branch mutation, so a blocked plan leaves no git state changes.

func (*Planner) PlanChain added in v0.3.0

func (p *Planner) PlanChain(refs []string, targetEnv string) (*PlanChainResult, error)

PlanChain validates and computes the per-environment plan for elevating a set of trunk commits across the bottom-up environment chain up to and including targetEnv. Commits are kept in the caller's ref order; environments run bottom-up. Each ref must resolve and be a trunk ancestor; each (commit, env) pair is skipped when the commit is already present in that environment (an ancestor of its state SHA or already in its patch list). An environment whose whole requested set is already present is reported as a no-op.

PlanChain is additive: it does not create branches or mutate state, and the single-env Plan and PlanResult are unchanged.

type PlannerOptions

type PlannerOptions struct {
	ConfigPath  string
	ManifestKey string
	Actor       string
}

PlannerOptions carries the required inputs for NewPlanner.

Jump to

Keyboard shortcuts

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