migrate

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: 13 Imported by: 0

Documentation

Overview

Package migrate answers the question the confirm screens lead with: between the build an env is running and the build about to be written into it, which commits ship, and does any of them migrate the database (R-005 in docs/repo-map.md — on the target repo every Application auto-syncs and the app entrypoint runs db:prepare, so a merge can migrate production as a side effect).

Three activity-shaped pieces (AGENTS.md §4.3: func(ctx, In) (Out, error), no secrets, no internal/ imports), each over a forge.Forge bound to the *app* repo, never the gitops one:

  • Resolver turns an image reference into a git revision: the OCI revision label the build stamped, else a git tag named like the image tag, else a "sha-<prefix>" tag, else unknown. It reports which source answered, because a tag is mutable and a label is not, and the screen should say which it trusted.
  • Comparer lists the commits between two revisions and attributes migration files to the commits that added them, under a per-app path prefix (MigrationsPath decides the prefix: the app repo's own .hoist.yaml, else config, else db/migrate/).
  • Blamer dates one manifest line: how long the reference being replaced has been live.

Everything here informs and nothing blocks (AGENTS.md principle 5): the gitops repo's runbook owns the rule that a promotion is not bundled with a manual migration; this package makes the delta visible. A delta that cannot be computed is a typed ErrUnresolved with a reason, never an empty list that reads as "no commits".

Index

Constants

View Source
const (
	PrefixFromAppRepo = "app repo"
	PrefixFromConfig  = "config"
	PrefixFromDefault = "default"
)

Where a migrations prefix came from, for the screen header.

View Source
const DefaultMigrationsPath = "db/migrate/"

DefaultMigrationsPath is the Rails convention, the default when neither the app repo nor config says otherwise.

View Source
const MigrationsDisabled = "none"

MigrationsDisabled is the value, in either the policy file or config, that says "this app has no migrations to look for".

View Source
const PolicyFile = ".hoist.yaml"

PolicyFile is the file an app repo may carry to describe itself to hoist. Today it holds one key. It lives in the app repo because the app repo is what knows its own migration convention; the gitops repo's config can override per image repo, and the Rails default covers the repos this tool was written for.

View Source
const RevisionLabel = "org.opencontainers.image.revision"

RevisionLabel is the OCI annotation a build stamps with the commit it was built from (docker/metadata-action and buildx both set it by default).

Variables

View Source
var ErrUnresolved = errors.New("migrate: revision unresolved")

ErrUnresolved is returned (wrapped, with the reason) when a delta cannot be computed because one end has no revision. Screens render it as a named gap — "no commit history: v3 has no revision label and me/app has no tag named v3" — never as an empty list.

Functions

func MigrationsPath

func MigrationsPath(ctx context.Context, f forge.Forge, ref, configured string) (prefix, source string, err error)

MigrationsPath decides the migrations prefix for one app repo at ref: the repo's own PolicyFile if it has one and names migrations, else configured (what config.yaml holds for this image repo — already defaulted by config's Normalize, so it is never empty for a mapped repo; "" here means the caller had no config at all), else DefaultMigrationsPath. The returned prefix is cleaned and ends in "/", or is "" when disabled. source is one of the PrefixFrom* constants.

A forge error reading the policy file is returned: it is the same scope gap Compare would hit next, and answering "default" over it would print a default the app repo may have overridden.

func NormalizePrefix

func NormalizePrefix(v string) (string, error)

NormalizePrefix cleans a migrations path to the form Delta matches on: relative, no "..", trailing "/"; or "" for MigrationsDisabled. internal/config's Validate calls it too, so the two agree on what is acceptable.

Types

type Blamer

type Blamer struct {
	Forge forge.Forge
}

Blamer dates manifest lines against the gitops repo's forge.

func (Blamer) LiveAge

func (b Blamer) LiveAge(ctx context.Context, in LiveAgeIn) (map[int]LineAge, error)

LiveAge blames the requested lines. Lines past the end of the file are absent from the result. An ErrUnknownRef on Ref with a FallbackRef set retries once against the fallback.

type Cache

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

Cache memoises deltas and revisions for one process: the tag picker, the deploy confirm and the plan confirm all ask about the same pairs, and a sha pair's delta is immutable. Errors are never cached — a rate-limit 403 must not stick for the rest of the session. Keys carry the app repo even though every Comparer is per-repo, because one Cache serves every mapped image repo.

func (*Cache) Delta

func (c *Cache) Delta(ctx context.Context, key DeltaKey, fill func(context.Context) (Delta, error)) (Delta, error)

Delta returns the cached delta for key or computes it with fill and caches the result.

func (*Cache) Revision

func (c *Cache) Revision(ctx context.Context, key RevisionKey, fill func(context.Context) (Revision, error)) (Revision, error)

Revision is Delta's twin for resolutions. An unresolved Revision (SourceUnknown, nil error) IS cached: it is an answer, not a failure, and re-asking the forge would give the same one.

type Commit

type Commit struct {
	SHA, Subject, Body, Author string
	Date                       time.Time
	Migrations                 []string
}

Commit is a forge.Commit plus the migration files it added or changed under the delta's prefix (nil when none).

type Comparer

type Comparer struct {
	Forge forge.Forge
}

Comparer computes deltas against one app repo's forge.

func (Comparer) Delta

func (c Comparer) Delta(ctx context.Context, in DeltaIn) (Delta, error)

Delta implements the algorithm described on the package: compare, detect a rollback and re-compare the other way, then attribute migrations with as few calls as the answer needs — none when the forge's own range file list is complete and holds no migration, else one CommitsTouching plus one CommitFiles per commit that touched the prefix.

type Delta

type Delta struct {
	From, To  Revision
	Direction Direction
	Commits   []Commit
	Total     int
	Truncated bool
	// MigrationsIncomplete is set when the migration attribution could not see everything:
	// the compare was truncated and the attribution had to run (so it is bounded by the
	// oldest returned commit), or a commit that touches the migrations path had its file
	// list capped by the forge. Migrations is then a floor and the screens word it as one —
	// "unknown" when the floor is zero. Truncated alone does not set it: a complete file
	// list with nothing under the prefix proves zero migrations however many commits the
	// compare left out.
	MigrationsIncomplete bool
	Migrations           []string
	MigrationCommits     int
	// Prefix and PrefixSource record which migrations path applied and where it came from
	// (MigrationsPath), so the screen can say "migrations under db/migrate/ · from
	// me/app's .hoist.yaml". Prefix is "" when migrations are disabled for this app.
	Prefix       string
	PrefixSource string
}

Delta is what ships between two revisions. Commits are newest first, the order every screen lists them. Migrations is the distinct migration paths across the whole range, sorted, and MigrationCommits how many commits carry one. Total and Truncated come from the forge; when Truncated, Commits is the newest len(Commits) of Total and Migrations was attributed through CommitsTouching, which is bounded by the oldest *returned* commit's date — so under truncation the migration count is a floor, and the screen says so.

type DeltaIn

type DeltaIn struct {
	From, To   Revision
	Migrations string
}

DeltaIn is Comparer.Delta's input. Migrations is the path prefix that marks a migration file ("db/migrate/"); "" disables attribution (MigrationsDisabled). A prefix, not a glob: the field is a string so a glob form later is an additive change.

type DeltaKey

type DeltaKey struct {
	AppRepo, FromSHA, ToSHA, Prefix string
}

DeltaKey identifies one delta: app repo, both shas, and the prefix it was attributed under.

type Direction

type Direction string

Direction is the relationship between From and To.

const (
	// DirectionForward means To is ahead of From; Commits are what ships.
	DirectionForward Direction = "forward"
	// DirectionRollback means To is behind From; Commits are what is being UN-applied, and
	// Migrations the migrations being reverted — the more dangerous case, which a plain
	// forward compare would report as zero commits.
	DirectionRollback Direction = "rollback"
	// DirectionDiverged means neither contains the other; Commits are those only in To.
	DirectionDiverged Direction = "diverged"
	// DirectionSame means identical revisions.
	DirectionSame Direction = "same"
)

type LineAge

type LineAge struct {
	SHA         string
	Since       time.Time
	Approximate bool
}

LineAge is when one line last changed: the commit and its committer date.

type LiveAgeIn

type LiveAgeIn struct {
	Ref         string
	FallbackRef string
	Path        string
	Lines       []int
}

LiveAgeIn names lines of one file at one ref of the gitops repo. FallbackRef is tried when Ref does not resolve on the forge — the operator's checkout may be ahead of anything pushed — and an answer from it is marked Approximate, since the line numbers were read from Ref's tree, not FallbackRef's.

type ResolveIn

type ResolveIn struct {
	Ref    image.Ref
	Labels map[string]string
}

ResolveIn is Resolver.Resolve's input. Labels is the image's config-blob labels (registry.ImageMeta.Labels); nil means the caller did not fetch them, and the label source is skipped rather than treated as absent.

type Resolver

type Resolver struct {
	Forge forge.Forge
}

Resolver resolves image references against one app repo's forge.

func (Resolver) Resolve

func (r Resolver) Resolve(ctx context.Context, in ResolveIn) (Revision, error)

Resolve tries each Source in order and returns the first that answers. A forge error stops the walk and is returned: a token scope gap must never read as "unknown" (the forge's own ok=false is the only thing that means "not there"). A full-length label sha is trusted as is — verifying it against the repo costs a call the common case does not need, and Compare will report ErrUnknownRef if it is not actually in the repo.

type Revision

type Revision struct {
	Ref    image.Ref
	SHA    string
	Source Source
	Detail string
}

Revision is one image reference resolved to a commit in its app repo. Detail is the tag or prefix that answered, for the screen to show beside Source; empty for a label.

func (Revision) Resolved

func (r Revision) Resolved() bool

Resolved reports whether a commit was found.

type RevisionKey

type RevisionKey struct {
	AppRepo, Ref string
}

RevisionKey identifies one resolution: app repo and the image reference as written.

type Source

type Source string

Source names which evidence resolved an image reference to a git revision, in the order Resolver tries them. The screens print it: a label is the build's own claim about itself, a tag is whatever the tag points at today.

const (
	// SourceLabel means the image's org.opencontainers.image.revision label.
	SourceLabel Source = "image label"
	// SourceGitTag means the app repo has a git tag named exactly like the image tag.
	SourceGitTag Source = "git tag"
	// SourceShaTag means the image tag is "sha-<prefix>" and the prefix names a commit.
	SourceShaTag Source = "sha tag"
	// SourceUnknown means nothing answered. Revision.SHA is empty.
	SourceUnknown Source = "unknown"
)

Jump to

Keyboard shortcuts

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