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
- Variables
- func MigrationsPath(ctx context.Context, f forge.Forge, ref, configured string) (prefix, source string, err error)
- func NormalizePrefix(v string) (string, error)
- type Blamer
- type Cache
- type Commit
- type Comparer
- type Delta
- type DeltaIn
- type DeltaKey
- type Direction
- type LineAge
- type LiveAgeIn
- type ResolveIn
- type Resolver
- type Revision
- type RevisionKey
- type Source
Constants ¶
const ( PrefixFromAppRepo = "app repo" PrefixFromConfig = "config" PrefixFromDefault = "default" )
Where a migrations prefix came from, for the screen header.
const DefaultMigrationsPath = "db/migrate/"
DefaultMigrationsPath is the Rails convention, the default when neither the app repo nor config says otherwise.
const MigrationsDisabled = "none"
MigrationsDisabled is the value, in either the policy file or config, that says "this app has no migrations to look for".
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.
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 ¶
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 ¶
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 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 ¶
Commit is a forge.Commit plus the migration files it added or changed under the delta's prefix (nil when none).
type Comparer ¶
Comparer computes deltas against one app repo's forge.
func (Comparer) Delta ¶
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 ¶
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 LiveAgeIn ¶
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 ¶
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 ¶
Resolver resolves image references against one app repo's forge.
func (Resolver) Resolve ¶
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 ¶
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.
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" )