upgrade

package
v0.8.0 Latest Latest
Warning

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

Go to latest
Published: Oct 19, 2026 License: MIT Imports: 32 Imported by: 0

Documentation

Overview

Package upgrade is Gombit's application upgrade tooling (UPGRADE-0): it works out where an application stands (its baseline), what moving it to another framework version requires, and proposes those changes for review. It never rewrites user-owned source without an explicit write action.

The baseline is two facts. The framework version comes from the app's go.mod, read offline and never copied anywhere else; gombit contract app reads it the same way (internal/frameworkmod). When a go.work applies to the app, the workspace decides what it builds against, and no version is claimed. The scaffold version, the generation conventions the app was created with, is recorded by gombit new in a versioned block of gombit.yaml:

gombit:
  metadata: 1   # the format of this block
  scaffold: 1   # the scaffold conventions the app was generated with

An app generated before that block existed has none: Detect reports it as scaffold 0, and RecordBaseline (gombit upgrade baseline --write) records that explicitly.

Index

Constants

View Source
const FrameworkModulePath = frameworkmod.ModulePath

FrameworkModulePath is the module a Gombit application requires.

View Source
const ManifestFormat = 1

ManifestFormat is the format of manifest.yaml this framework reads.

View Source
const MetadataVersion = 1

MetadataVersion is the format of the gombit block in gombit.yaml that this framework reads and writes. A block with a newer format is refused (ErrMetadataTooNew): it was written by a newer framework, whose meaning this one cannot know.

View Source
const ProjectFile = "gombit.yaml"

ProjectFile is the file the scaffold version is recorded in, at the root of an application.

View Source
const ScaffoldVersion = 1

ScaffoldVersion is the version of the conventions gombit new generates with (the layout, the framework-owned files, their contents). It moves when those conventions change in a way an upgrade has to account for, not with every framework release. 0 is an app generated before upgrade metadata existed.

View Source
const Unreleased = "unreleased"

Unreleased is the version of the manifest entry collecting the changes of the next release while it is developed. It is renamed to that release's version when the release is cut (docs/releasing.md); it is never an upgrade target.

Variables

View Source
var (
	// ErrNotGombitApp: the directory has no go.mod requiring the framework.
	ErrNotGombitApp = errors.New("upgrade: not a Gombit application")
	// ErrMetadataTooNew: gombit.yaml's metadata format, or its scaffold
	// version, is newer than this framework understands.
	ErrMetadataTooNew = errors.New("upgrade: the upgrade metadata is newer than this gombit understands")
)
View Source
var ErrChangedSinceRun = errors.New("upgrade: a file changed since the codemods read it")

ErrChangedSinceRun: a file the codemods rewrote changed after they read it, so writing their rewrite would discard that change. Nothing was written; run them again.

View Source
var ErrCodemodIncomplete = errors.New("upgrade: the codemod did not cover the whole application")

ErrCodemodIncomplete: a codemod applied as an automatic change wrote what it could, but did not cover the whole application: uses it left for the developer, or files the build configuration excludes that mention its API (rerun under their GOOS, GOARCH or tags). The change is not done.

View Source
var ErrDoesNotLoad = errors.New("upgrade: the application does not type-check")

ErrDoesNotLoad: a package of the scanned graph (the application's packages, their tests, or any package they depend on, directly or not, framework or not) does not load or type-check, so a scan would miss uses; fix the errors first.

View Source
var ErrNoTarget = errors.New("upgrade: name the target release to apply an upgrade to (--to)")

ErrNoTarget: applying an upgrade needs its target named, never a default.

View Source
var ErrNoUpgradePath = errors.New("upgrade: no upgrade path")

ErrNoUpgradePath: the manifest describes no upgrade between the versions asked for.

View Source
var FrameworkModels = []migrations.Model{
	{ImportPath: frameworkmod.ModulePath + "/auth", TypeName: "User"},
	{ImportPath: frameworkmod.ModulePath + "/auth", TypeName: "RefreshToken"},
	{ImportPath: frameworkmod.ModulePath + "/auth", TypeName: "Group"},
	{ImportPath: frameworkmod.ModulePath + "/auth", TypeName: "Permission"},
	{ImportPath: frameworkmod.ModulePath + "/storage/claims", TypeName: "Claim"},
}

FrameworkModels are the framework models whose tables an application's migrations carry, as its model registry names them. A test holds the list to every Models() the framework exports.

Functions

func DeprecationSince

func DeprecationSince(paragraph string) string

DeprecationSince is the release a "Deprecated:" paragraph names as the one that deprecated the API, or "" when it names none.

func MetadataBlock

func MetadataBlock(scaffold int) string

MetadataBlock is the gombit.yaml block recording scaffold version scaffold, as gombit new writes it.

func RemovableFrom

func RemovableFrom(since string) string

RemovableFrom is the first release that may remove an API deprecated in since: before 1.0, a deprecated API stays through the next minor release (deprecated in v0.8.x, still there in v0.9.x, removable from v0.10.0); from 1.0 on, until the next major. Across 1.0: v1.0.0, a major release, may remove what an earlier 0.y deprecated, and it follows a minor line that deprecates nothing, so every pre-1.0 deprecation still gets its next minor line. For a pre-1.0 since, the release is the one returned or v1.0.0, whichever comes first (RemovableFromMajor).

func RemovableFromMajor

func RemovableFromMajor(since string) string

RemovableFromMajor is the major release that may remove an API deprecated in since if it comes before RemovableFrom(since): v1.0.0 for a pre-1.0 deprecation (see RemovableFrom), and "" from 1.0 on.

func RenderDeprecated

func RenderDeprecated(w io.Writer, uses []DeprecatedUse) error

RenderDeprecated writes the uses, each with what to use instead.

func RenderNotesDoc

func RenderNotesDoc(w io.Writer, m *Manifest) error

RenderNotesDoc writes docs/upgrade-notes.md: the notes of every release in m, newest first.

func UpcomingReleases

func UpcomingReleases(latest string) []string

UpcomingReleases are the releases that can follow latest: its next patch, minor and major.

func WriteConfigTemplates

func WriteConfigTemplates(workDir string, releases []Release) ([]string, error)

WriteConfigTemplates upgrades the application's environment templates (.env.example and the like) for the configuration changes of releases, in order, and returns the templates it wrote. A renamed variable is renamed where a template sets or documents it (unless the template already has the new name: then the old line is commented out, as a removed variable's is), with a note above saying why. Every other line is kept as it is, values and line endings included, and each template is written atomically. A file that is not a template (.env, which may hold secrets) is never written. Running it again changes nothing.

Types

type Action

type Action struct {
	// Description says what Apply does.
	Description string
	// Apply applies the change to the application in workDir.
	Apply func(workDir string) (changed bool, err error)
	// Check reports, reading only, whether the application in workDir,
	// whose baseline is b, needs the change (Apply would change something).
	// Nil means it always does. The upgrade plan counts only the automatic
	// changes an app needs, and must write nothing: TestActionChecksWriteNothing
	// runs every Check against an app and compares its files. Check sees the
	// application as it is before the upgrade (b is that baseline), not as
	// the plan's earlier steps would leave it, and a step's need can change
	// once earlier steps apply: whatever applies a plan must re-check each
	// action right before its Apply, not trust the planned list.
	Check func(workDir string, b Baseline) (needed bool, err error)
}

Action is an automatic change this framework implements: Apply carries it out on the application in workDir, reporting whether it changed anything. A manifest change of kind automatic names one, so "automatic" always has code behind it.

func LookupAction

func LookupAction(name string) (Action, bool)

LookupAction returns the automatic action name, and reports whether this framework implements it (it has an Apply). "codemod:<id>" names the codemod id (Codemods).

type Applied

type Applied struct {
	// Plan is the plan the dependency changes came from. Its automatic and
	// manual changes are not applied: they are still to do.
	Plan Plan `json:"plan"`
	// Commands are the go commands run, in order, with their output.
	Commands []GoCommand `json:"commands"`
	// GoMod and GoSum are the lines the commands removed and added.
	GoMod FileChange `json:"go_mod"`
	GoSum FileChange `json:"go_sum"`
	// FollowUp are steps the dependency change leaves to the developer
	// before the app builds again, such as refreshing a vendor directory.
	FollowUp []string `json:"follow_up"`
	// MigrationRequired is whether the upgrade is not complete until the
	// application migrates its database for the framework-schema changes
	// (Plan.Schema.Steps). ApplyDependencies never runs a migration.
	MigrationRequired bool `json:"migration_required"`
}

Applied is what ApplyDependencies did: the plan it carried out, the go commands it ran for it, and the lines of go.mod and go.sum they changed.

func ApplyDependencies

func ApplyDependencies(ctx context.Context, workDir string, m *Manifest, to string) (Applied, error)

ApplyDependencies upgrades the framework dependency of the application in workDir to the release to, as PlanUpgrade plans it: it changes go.mod and go.sum with the go command, and nothing else. Each replace the plan names is edited first (go mod edit -dropreplace, or -replace), so that the require then resolves to the target (go get MODULE@to, last). The go command does the resolution, so the result is Go's own, deterministic for the same module sources; Applied reports every command and changed line.

It refuses what PlanUpgrade refuses, and to must be given. A go command that fails is reported with its full output (GoCommandError), never summarized, and so is a result whose build is not the target release; either way go.mod and go.sum are restored to the bytes they had. The plan's automatic and manual changes are left to do.

func ApplyDependenciesWith

func ApplyDependenciesWith(ctx context.Context, workDir string, m *Manifest, to string, opts PlanOptions) (Applied, error)

ApplyDependenciesWith is ApplyDependencies with the plan's options: the plan, the framework-schema analysis included, is made before anything is changed.

func (Applied) Render

func (a Applied) Render(w io.Writer) error

Render writes what the upgrade did, and what is still to do.

type Area

type Area string

Area is the part of an application a change touches.

type Baseline

type Baseline struct {
	// Framework is what go.mod says about the framework module.
	Framework Framework `json:"framework"`
	// Scaffold is the scaffold version the app was generated with: recorded
	// in gombit.yaml, or 0 when no metadata is recorded (Recorded false).
	Scaffold int `json:"scaffold"`
	// Metadata is the format of the recorded block (0 when there is none).
	Metadata int `json:"metadata"`
	// Recorded is whether gombit.yaml carries the metadata block. An app
	// without it was generated before upgrade metadata existed; its
	// baseline is detected (scaffold 0), and can be recorded with
	// RecordBaseline.
	Recorded bool `json:"recorded"`
}

Baseline is where an application stands: the framework it builds against and the conventions it was generated with.

func Detect

func Detect(workDir string) (Baseline, error)

Detect reads the baseline of the application in workDir: the framework from go.mod, the scaffold version from gombit.yaml. It writes nothing. It asks the go command which go.work applies (`go env GOWORK`), so the go command must be on PATH.

func RecordBaseline

func RecordBaseline(workDir string) (Baseline, bool, error)

RecordBaseline records the detected baseline of an app that has none (one generated before upgrade metadata existed) by appending the metadata block, with scaffold 0, to its gombit.yaml (creating the file if there is none). Nothing else in the file changes: the result must read back as recorded, with every other key meaning what it did, or nothing is written. The file is replaced through a synced temp file and a rename (internal/atomicfile): a reader sees the old file or the new, never a part (on Unix, and on Windows NTFS; a Windows volume without an atomic rename is refused with atomicfile.ErrNoAtomicRename and nothing written), and on Unix the replacement is also synced to disk before RecordBaseline returns; see that package for the rest. It reports whether it wrote; an app that already records its baseline is left as it is. wrote is true whenever gombit.yaml was changed, even with an error: one wrapping atomicfile.ErrNotDurable means the block is in place (and the returned baseline records it), but was not synced to disk.

type Bump

type Bump int

Bump is the size of a version change.

const (
	// BumpPatch: X.Y.Z+1.
	BumpPatch Bump = iota
	// BumpMinor: X.Y+1.0.
	BumpMinor
	// BumpMajor: X+1.0.0.
	BumpMajor
)

func RequiredBump

func RequiredBump(prev string, changes []Change) (Bump, []string)

RequiredBump is the smallest version change a release with changes may be, released after prev: a breaking change needs a minor release before 1.0 (where minors may break) and a major one from 1.0 on. The security exception: a breaking change in the security area may ship in any release, so a vulnerability is closed without waiting for the next minor or major. It also returns the breaking changes that set the bump.

func (Bump) String

func (b Bump) String() string

type Change

type Change struct {
	// ID names the change, unique across the manifest (kebab-case).
	ID string `yaml:"id" json:"id"`
	// Kind is what moving across it takes.
	Kind Kind `yaml:"kind" json:"kind"`
	// Breaking is whether an app can stop building or behave differently
	// across it without acting.
	Breaking bool `yaml:"breaking" json:"breaking"`
	// Area is the part of an application it touches.
	Area Area `yaml:"area" json:"area"`
	// Summary is one line: what changed.
	Summary string `yaml:"summary" json:"summary"`
	// Details is Markdown: what to do (required for a manual change), and
	// why.
	Details string `yaml:"details,omitempty" json:"details,omitempty"`
	// Action names the automation applying an automatic change.
	Action string `yaml:"action,omitempty" json:"action,omitempty"`
	// Refs are the GitHub issues or pull requests of the change.
	Refs []int `yaml:"refs,omitempty" json:"refs,omitempty"`
	// Config are the configuration variables the change renames, removes,
	// re-defaults, requires or newly refuses values of, and those it adds
	// that another entry needs or that a security change introduces: what
	// the upgrade tooling checks the application's environment files
	// against (AnalyzeConfig). A change in the security area makes each of
	// them, an addition included, a security review.
	Config []ConfigChange `yaml:"config,omitempty" json:"config,omitempty"`
	// Schema are the framework-owned tables the change adds, alters or
	// removes (area schema): the migrations an application generates,
	// reviews and applies for it (AnalyzeSchema). In a manual change they
	// are required; in an informational one, they are for an application
	// that adopts the feature.
	Schema []SchemaChange `yaml:"schema,omitempty" json:"schema,omitempty"`
	// Deprecates are the framework APIs the change deprecates, each named
	// as gombit upgrade check names it (database.KeepStoredZeros). A
	// "Deprecated: since vX.Y.Z" paragraph must name a release with a
	// change declaring its API here, so its window (RemovableFrom) starts
	// where the deprecation was announced.
	Deprecates []string `yaml:"deprecates,omitempty" json:"deprecates,omitempty"`
}

Change is one upgrade-relevant change of a release.

type Classification

type Classification struct {
	Automatic     []Change `json:"automatic"`
	Manual        []Change `json:"manual"`
	Informational []Change `json:"informational"`
	Breaking      []Change `json:"breaking"`
}

Classification groups changes by Kind; Breaking holds the breaking ones of every kind.

func Classify

func Classify(releases []Release) Classification

Classify groups the changes of releases (a Path) by kind, in release order.

type Codemod

type Codemod struct {
	// ID names it (kebab-case); a manifest change applies it as action
	// "codemod:<ID>".
	ID string
	// Description says what it rewrites, in one line.
	Description string
	// Package is the import path of the framework package declaring the
	// API it rewrites.
	Package string
	// Symbol is that API in Package: Name for a function, type, constant
	// or variable, Type.Name for a method or a struct field that Type's own
	// declaration declares. A member Type only reaches is refused: one
	// promoted from an embedded type or interface, or Base's when Type is
	// defined from it (type Type Base) or an alias of it. It is the
	// declaring type's, and every use of it would be rewritten, however
	// reached.
	Symbol string

	// Rename renames each use to this name. With MoveTo, the empty Rename
	// keeps the name.
	Rename string
	// MoveTo is the import path of the package the replacement of a
	// package-level API lives in: pkg.Name becomes newpkg.Name (or
	// newpkg.Rename), the file importing newpkg if it does not, and no
	// longer importing pkg if nothing else uses it.
	MoveTo string

	// Rewrite rewrites one use, for a change Rename and MoveTo cannot
	// express (reordered arguments, a new parameter): it returns the
	// expression replacing u.Node(), or nil (u.Skip) to leave the use for
	// the developer. It runs bottom-up, so u.Call's arguments are already
	// rewritten; give new nodes the position of what they replace. The
	// replacement must not use the API it replaces, or a second run would
	// rewrite it again: RunCodemods refuses one that does.
	Rewrite func(u *Use) ast.Expr
}

Codemod is a mechanical rewrite of an application's uses of one framework API, for a change whose replacement is deterministic: a rename or a move (Rename, MoveTo), or any other rewrite of a use (Rewrite).

A codemod rewrites uses, found by type information exactly as ScanDeprecated finds them (the same object, never the same text), and never the application's own declarations: renaming an interface method the application implements still needs its implementations renamed.

func Codemods

func Codemods() []Codemod

Codemods are the codemods this framework implements, by ID.

type ConfigCategory

type ConfigCategory string

ConfigCategory is how a configuration change concerns an application.

const (
	// ConfigCategoryRenamed: a variable it may set has a new name.
	ConfigCategoryRenamed ConfigCategory = "renamed"
	// ConfigCategoryDefaultChanged: a variable it may leave unset defaults
	// to something else.
	ConfigCategoryDefaultChanged ConfigCategory = "default-changed"
	// ConfigCategoryActionRequired: a variable is removed, now required,
	// or refuses values it accepted.
	ConfigCategoryActionRequired ConfigCategory = "action-required"
	// ConfigCategorySecurityReview: the change is security-sensitive.
	ConfigCategorySecurityReview ConfigCategory = "security-review"
)

type ConfigChange

type ConfigChange struct {
	// Var is the variable, e.g. GOMBIT_HTTP_ADDR.
	Var string `yaml:"var" json:"var"`
	// Change is what the release does to it.
	Change ConfigChangeKind `yaml:"change" json:"change"`
	// To is, for a rename, the new name; for a default change, the new
	// default.
	To string `yaml:"to,omitempty" json:"to,omitempty"`
	// From is, for a default change, the old default.
	From string `yaml:"from,omitempty" json:"from,omitempty"`
	// Environment limits a default change, a requirement or a refusal to
	// one runtime environment (development, test, production); empty is
	// every one.
	Environment string `yaml:"environment,omitempty" json:"environment,omitempty"`
}

ConfigChange is one configuration variable a release changes.

func (ConfigChange) Describe

func (cc ConfigChange) Describe() string

Describe is the change in one line of Markdown, e.g. "`GOMBIT_A` is renamed `GOMBIT_B`".

type ConfigChangeKind

type ConfigChangeKind string

ConfigChangeKind is what a release does to a configuration variable.

const (
	// ConfigRenamed: the variable is read under another name (To) now; the
	// old one is ignored.
	ConfigRenamed ConfigChangeKind = "renamed"
	// ConfigRemoved: the variable is no longer read.
	ConfigRemoved ConfigChangeKind = "removed"
	// ConfigDefault: the variable's default (when unset) changed, From to
	// To.
	ConfigDefault ConfigChangeKind = "default"
	// ConfigRequired: the variable must now be set (in Environment, or
	// everywhere).
	ConfigRequired ConfigChangeKind = "required"
	// ConfigRefused: values the variable accepted before are now refused
	// (in Environment, or everywhere); the change's details say which.
	ConfigRefused ConfigChangeKind = "refused"
	// ConfigAdded: the variable is new, read from this release on. It is
	// declared when a later declaration (a requirement, a refusal, a
	// default change, a rename) needs it to exist, or when a security
	// change introduces it. An ordinary addition asks nothing of an
	// application; a security one is a security review.
	ConfigAdded ConfigChangeKind = "added"
)

type ConfigFile

type ConfigFile struct {
	// Path is relative to the application directory.
	Path string `json:"path"`
	// Template is whether it is a template (.env.example and the like),
	// which WriteConfigTemplates may upgrade. Any other environment file
	// (.env, .env.production) may hold secrets: only the names it sets are
	// read, and it is never written.
	Template bool `json:"template"`
}

ConfigFile is an environment file of the application the analysis read.

type ConfigFinding

type ConfigFinding struct {
	ConfigChange
	// Release and ChangeID say where the manifest declares it.
	Release  string `json:"release"`
	ChangeID string `json:"change_id"`
	// Category is how it concerns the application.
	Category ConfigCategory `json:"category"`
	// NeedsAction is whether the developer must act (or review): every
	// rename, removal, requirement, refusal and security change, and a
	// default change the application's files do not override.
	NeedsAction bool `json:"needs_action"`
	// SetIn are the environment files (not templates) that set the
	// variable; TemplateSetIn the templates that set it, and DocumentedIn
	// those that mention it commented out: each template as the path's
	// earlier changes leave it (WriteConfigTemplates rewrites it), every
	// other file as it is (see AnalyzeConfig). They are mentions: only
	// .env, for local development runs, is a runtime override gombit loads.
	SetIn         []string `json:"set_in"`
	TemplateSetIn []string `json:"template_set_in"`
	DocumentedIn  []string `json:"documented_in"`
	// SetAs are the files (not templates) that set the variable under an
	// earlier name of a rename on the path, each with that name: renaming
	// A to B, then B to C, a .env setting A is listed for B. It proves
	// nothing about the new name (a default change still needs action),
	// but the file needs the renames.
	SetAs map[string]string `json:"set_as,omitempty"`
	// Fixable is whether WriteConfigTemplates would change a template for
	// it (false once it has).
	Fixable bool `json:"fixable"`
	// Advice says what to do.
	Advice string `json:"advice"`
}

ConfigFinding is a configuration change of the upgrade, and what it means for the application: where its environment files mention the variable, and what to do. It never carries a value from those files.

type ConfigReport

type ConfigReport struct {
	// Files are the environment files read.
	Files []ConfigFile `json:"files"`
	// Unreadable are the environment files that could not be read (no
	// permission, not a regular file): what they set is unknown, so check
	// them by hand.
	Unreadable []string `json:"unreadable"`
	// Findings are the upgrade's configuration changes, in release order.
	Findings []ConfigFinding `json:"findings"`
}

ConfigReport is the configuration side of an upgrade for an application.

func AnalyzeConfig

func AnalyzeConfig(workDir string, releases []Release) (ConfigReport, error)

AnalyzeConfig reports what the configuration changes of releases (an upgrade's path, in order) mean for the application in workDir: per change, its category, whether the developer must act, the environment files that set or document the variable, and what to do. It reads workDir's .env and .env.* files and never reports a value from them: from a file that is not a template, only the names it sets are read. Every rename and removal is reported whether or not a file mentions the variable (a deployment sets its environment elsewhere), and every change of a security change is a security review. Each change is judged against the templates as the path's earlier changes leave them, in the order WriteConfigTemplates applies them, and against the other files as they are, since nothing writes them.

func (ConfigReport) NeedsAction

func (r ConfigReport) NeedsAction() bool

NeedsAction reports whether any finding needs the developer.

func (ConfigReport) Render

func (r ConfigReport) Render(w io.Writer) error

Render writes the report as text: each finding with its category, where it is declared and what to do, then the environment files read.

type DependencyChange

type DependencyChange struct {
	Module string `json:"module"`
	// Directive is the directive that changes: "require" or "replace".
	Directive string `json:"directive"`
	// ForVersion is, for a replace, the version it replaces (its left
	// side); empty for a replace of every version.
	ForVersion string `json:"for_version,omitempty"`
	// From is the directive's right side now: the required version, or the
	// replacement as go.mod writes it ("path version", or a directory).
	From string `json:"from"`
	// To is the version it moves to; empty for a replace that must be
	// dropped instead.
	To string `json:"to,omitempty"`
	// Note says why the directive must change, when the reason is not
	// plain.
	Note string `json:"note,omitempty"`
}

DependencyChange is a go.mod directive about a module that the upgrade changes.

type DeprecatedUse

type DeprecatedUse struct {
	// File is the using file, relative to the application directory when
	// it is inside it.
	File string `json:"file"`
	// Line and Column are where the use is.
	Line   int `json:"line"`
	Column int `json:"column"`
	// Symbol is the API used: an import path (a deprecated package),
	// pkg.Name, pkg.Type.Field, or (*pkg.Type).Method.
	Symbol string `json:"symbol"`
	// Package is the import path of the package that declares it.
	Package string `json:"package"`
	// Deprecation is the declaration's "Deprecated:" paragraph, which names
	// what to use instead.
	Deprecation string `json:"deprecation"`
	// Since is the release that deprecated it, as the paragraph says
	// ("Deprecated: since vX.Y.Z, ..."), and RemovableFrom the first
	// release of its line the compatibility policy lets remove it;
	// RemovableFromMajor is the major release that may remove it if it
	// comes first (v1.0.0, for a pre-1.0 deprecation). All empty when the
	// paragraph names no release.
	Since              string `json:"since,omitempty"`
	RemovableFrom      string `json:"removable_from,omitempty"`
	RemovableFromMajor string `json:"removable_from_major,omitempty"`
}

DeprecatedUse is one use, in the application, of a framework API that the framework marks deprecated.

func ScanDeprecated

func ScanDeprecated(ctx context.Context, workDir string) ([]DeprecatedUse, error)

ScanDeprecated finds every use, in the application in workDir (all its packages, tests included), of a framework API whose doc comment carries a "Deprecated:" paragraph, the Go convention (go doc, gopls and pkg.go.dev show it too): a deprecated package (its import), or a deprecated function, method, type, constant, variable, struct field (named or embedded) or interface method.

It works on type information, never text, in one graph: the app's packages, their tests and every package they depend on are loaded and type-checked from source together (go/packages with NeedDeps; no export data), so a framework declaration and the app's use of it are the same go/types object. Every framework package anywhere in that graph is indexed, including one the app reaches only through another dependency (a promoted method of an embedded framework type): each deprecated declaration under the object it defines (TypesInfo.Defs). A use is found by that object's identity, never by name, key or file position: an identifier denoting it (a call, a selector, a composite literal key, a dot-imported name), an element of an unkeyed struct literal (which names no field), or the import of a deprecated package. One token can be two uses (an unkeyed element that is itself a deprecated name). So a name that merely matches, a local declaration of the same name, a comment or a string never counts. The framework's own packages are not scanned.

It never returns a clean result it cannot vouch for: a package anywhere in the graph that does not load or type-check is ErrDoesNotLoad, and a used framework object from a package outside the indexed graph is an error.

type FileChange

type FileChange struct {
	Removed []string `json:"removed"`
	Added   []string `json:"added"`
}

FileChange is the lines a file lost and gained, each in file order.

type FileFix

type FileFix struct {
	// Path is the file, relative to the application directory, with
	// forward slashes.
	Path string `json:"path"`
	// Before and After are its content before and after the rewrite.
	Before []byte `json:"-"`
	After  []byte `json:"-"`
	// contains filtered or unexported fields
}

FileFix is one file's rewrite.

type Fix

type Fix struct {
	// Files are the files it changes, by path.
	Files []FileFix `json:"files"`
	// Left are the uses no codemod could rewrite: the developer changes
	// them.
	Left []LeftUse `json:"left"`
	// Unchecked are the application's Go files the build configuration
	// excludes (build constraints) that may use a codemod's API (they
	// mention its name): they are not type-checked, so not rewritten. They
	// are found by walking the tree as ./... does, so a directory whose
	// every file is excluded is covered too. Rerun with their GOOS, GOARCH
	// or -tags (GOFLAGS) to cover them.
	Unchecked []string `json:"unchecked"`
}

Fix is the rewrite the codemods propose for an application: nothing is written until Write.

func RunCodemods

func RunCodemods(ctx context.Context, workDir string, mods []Codemod) (*Fix, error)

RunCodemods runs mods on the application in workDir (all its packages, tests included) and returns the rewrite they propose, writing nothing.

It loads and type-checks the application and the framework in one graph, as ScanDeprecated does (ErrDoesNotLoad when that fails), and rewrites the syntax tree of each use, never its text. A use it cannot rewrite safely (the new name would not resolve to the new API there, a generated or cgo-processed file, an embedded field whose name would change) is left for the developer, unchanged. Changed files are printed with go/format, so gofmt-formatted, keeping CRLF line endings. The codemods run in passes, each on the application as the last one left it (so a codemod may rewrite another's output), until a pass changes nothing: that pass type-checks the rewritten application, and shows that running the codemods on it changes nothing. A rewrite that does not type-check, or does not settle, is an error, and nothing is proposed.

func (*Fix) Patch

func (f *Fix) Patch() string

Patch is the rewrite as a unified diff, relative to the application directory: `git apply` (or `patch -p1`) applies it there.

func (*Fix) Write

func (f *Fix) Write() ([]string, error)

Write writes the rewrite, each file atomically (whole or not at all), and returns the paths it wrote. It first checks that every file still holds exactly the bytes the codemods parsed, and writes nothing otherwise (ErrChangedSinceRun). That check is not a lock: Write does not coordinate with other writers, so a change made between it and a file's rename is lost. A failure writing one file leaves the ones before it written: the returned paths say which.

type Framework

type Framework struct {
	// Module is the framework's module path.
	Module string `json:"module"`
	// Version is the framework release the app builds against: the require
	// directive's version, or the version of a replace by the framework
	// module itself. It is empty when there is no framework release to
	// name: the framework is replaced by a local directory (Local) or by
	// another module (a fork: see Replace), or a go.work decides the build
	// (Workspace). Upgrade planning starts from Version, so it never names
	// what is not a release of the framework.
	Version string `json:"version,omitempty"`
	// Required is the require directive's version.
	Required string `json:"required"`
	// Replace is the replace directive in go.mod that applies to Required,
	// as the go command picks it, if any. Nil when a go.work applies
	// (Workspace): the workspace's replaces count then, and are not read.
	Replace *Replacement `json:"replace,omitempty"`
	// Local is true when go.mod replaces the framework by a local directory
	// (a framework checkout): there is no published version to upgrade
	// from. False when a go.work applies (Workspace), which decides instead.
	Local bool `json:"local,omitempty"`
	// Workspace is the go.work the go command uses for the app (its own
	// answer, `go env GOWORK`), if any. The workspace then decides the
	// framework the app builds against: its other modules' requirements and
	// replaces count too. This package does not resolve that, so only
	// Required (go.mod's) is set, and Version, Replace and Local are empty;
	// with GOWORK=off the app builds from its go.mod alone.
	Workspace string `json:"workspace,omitempty"`
}

Framework is the framework dependency as the app's go.mod declares it.

type GoCommand

type GoCommand struct {
	// Args are its arguments, after "go".
	Args []string `json:"args"`
	// Output is what it printed (stdout and stderr).
	Output string `json:"output,omitempty"`
}

GoCommand is one go command run for an upgrade.

type GoCommandError

type GoCommandError struct {
	Args   []string
	Output string
	Err    error
}

GoCommandError is a go command that failed while applying an upgrade: its arguments, its error, and everything it printed, unedited. The upgrade restores go.mod and go.sum before returning it.

func (*GoCommandError) Error

func (e *GoCommandError) Error() string

func (*GoCommandError) Unwrap

func (e *GoCommandError) Unwrap() error

type Kind

type Kind string

Kind classifies a change by what moving across it takes.

const (
	// KindAutomatic changes are applied by the upgrade tooling (its Action),
	// as a reviewable change.
	KindAutomatic Kind = "automatic"
	// KindManual changes need the developer to act (Details says how).
	KindManual Kind = "manual"
	// KindInformational changes need nothing done, but are worth knowing
	// when moving across them.
	KindInformational Kind = "informational"
)

type LeftUse

type LeftUse struct {
	File    string `json:"file"`
	Line    int    `json:"line"`
	Column  int    `json:"column"`
	Codemod string `json:"codemod"`
	Symbol  string `json:"symbol"`
	Reason  string `json:"reason"`
}

LeftUse is a use a codemod could not rewrite, and why. Its position is in the file as the rewrite leaves it (as after Write or git apply).

type Manifest

type Manifest struct {
	// Format is the manifest's format (ManifestFormat).
	Format int `yaml:"format" json:"format"`
	// Releases are in ascending version order. The first is the baseline:
	// the first release the manifest covers, which upgrades start from, so
	// it declares no changes. Unreleased, if present, is last.
	Releases []Release `yaml:"releases" json:"releases"`
}

Manifest is the compatibility manifest.

func LoadManifest

func LoadManifest() (*Manifest, error)

LoadManifest returns the manifest embedded in this framework: the releases up to its own. Each call returns its own copy, free to modify.

func ParseManifest

func ParseManifest(data []byte) (*Manifest, error)

ParseManifest decodes and validates a manifest. Unknown fields are refused, so a misspelt key cannot drop a change's classification.

func (*Manifest) CheckPolicy

func (m *Manifest) CheckPolicy(version string) error

CheckPolicy reports whether the listed release version is at least the version change its changes need (RequiredBump) after the latest stable release before it (a pre-release is not one: v0.9.0 after v0.9.0-rc.1 is still a minor release after v0.8.x). The release gate (CheckRelease) holds the release being cut to it, and a repository test every listed release but the exceptions. Parsing the manifest does not: a correction to a published release must stay representable.

func (*Manifest) CheckRelease

func (m *Manifest) CheckRelease(tag string) error

CheckRelease reports whether the manifest is ready to ship as the stable release tag: the tag is listed, it is the newest release (Latest), and no change is left under Unreleased (absent, or empty), and the release is the version change its changes need (CheckPolicy). A manifest that lists the tag but keeps the tag's changes under Unreleased, or lists releases past it, would ship notes and an upgrade plan saying the release changes nothing; the release workflow refuses it.

func (*Manifest) Latest

func (m *Manifest) Latest() string

Latest is the newest release the manifest describes (never Unreleased).

func (*Manifest) Path

func (m *Manifest) Path(from, to string) ([]Release, error)

Path returns the releases an upgrade from version from to version to moves across, in order: every release after from, up to and including to. It returns none when from and to are the same, and ErrNoUpgradePath, saying why, when the manifest cannot describe the upgrade: a version it does not list (before the first release it covers, newer than this framework, a pseudo-version or unlisted pre-release, or Unreleased), or a downgrade. It trusts that the versions are the framework's; PathFrom plans from an application's baseline.

func (*Manifest) PathFrom

func (m *Manifest) PathFrom(fw Framework, to string) ([]Release, error)

PathFrom is Path from the framework an application builds against (as Detect reports it), refusing with the reason a baseline that names no framework release: a go.work decides the build, or go.mod replaces the framework by a local directory or by another module (a fork). Planning from an application goes through here, so no caller can plan from a version that is not the framework's.

func (*Manifest) Release

func (m *Manifest) Release(version string) (Release, error)

Release returns the manifest's entry for version (a release, or Unreleased).

func (*Manifest) RenderNotes

func (m *Manifest) RenderNotes(w io.Writer, releases []Release) error

RenderNotes writes the upgrade notes of releases (of m), in the order given, as Markdown: per release, its changes grouped by kind (manual first), breaking ones marked. This is what docs/upgrade-notes.md and the GitHub release body carry, rendered from the manifest rather than written separately.

type Plan

type Plan struct {
	// Baseline is where the application stands (Detect).
	Baseline Baseline `json:"baseline"`
	// Current is the framework release the application builds against.
	Current string `json:"current"`
	// Target is the framework release the plan moves it to.
	Target string `json:"target"`
	// Dependencies are the go.mod directives about the framework module the
	// upgrade changes: its require, and a replace of it by the framework
	// module itself, which decides the build too. None when the application
	// already builds against Target.
	Dependencies []DependencyChange `json:"dependencies"`
	// Releases are the releases the upgrade moves across, in order.
	Releases []Release `json:"releases"`
	// Automatic are the automatic changes the application needs, each with
	// the action that applies it. An automatic change whose action reports
	// the application does not need it is left out.
	Automatic []PlannedAction `json:"automatic"`
	// Manual are the changes the developer must act on: every one the
	// manifest declares between Current and Target, with what to do.
	Manual []Change `json:"manual"`
	// Breaking are the breaking changes this application faces: every
	// breaking manual and informational change, and the breaking automatic
	// changes it needs. An automatic change its action reports the app does
	// not need is left out here too, as in Automatic, so the plan never
	// counts a change the app has already absorbed.
	Breaking []Change `json:"breaking"`
	// Informational are the changes worth knowing, with nothing to do.
	Informational []Change `json:"informational"`
	// Config is what the upgrade's configuration changes mean for the
	// application's environment files (AnalyzeConfig): names only, never
	// a value.
	Config ConfigReport `json:"config"`
	// Schema is what the upgrade's framework-schema changes (auth tables,
	// storage claims) mean for the application's migrations
	// (AnalyzeSchema, from its model registry in database/migrations): the
	// migration steps the upgrade is not complete without. The upgrade
	// tooling never generates or applies a migration.
	Schema SchemaReport `json:"schema"`
	// Contract are the changes on the path to the generated contract (the
	// OpenAPI document and TypeScript client: area client). Whatever they
	// are, the framework renders the generated artifacts, so after the
	// dependency change gombit upgrade contract checks them.
	Contract []Change `json:"contract"`
}

Plan is what upgrading an application to a target framework release takes, worked out without changing anything (gombit upgrade --dry-run).

func PlanUpgrade

func PlanUpgrade(workDir string, m *Manifest, to string) (Plan, error)

PlanUpgrade works out the plan for upgrading the application in workDir to the framework release to (the newest release m lists when to is empty). It writes nothing: it reads the baseline, plans with m.PathFrom, which refuses with the reason an application that names no framework release (a workspace, a local checkout, a fork) or a version m does not list, and asks each automatic action whether the application needs it.

func PlanUpgradeWith

func PlanUpgradeWith(workDir string, m *Manifest, to string, opts PlanOptions) (Plan, error)

PlanUpgradeWith is PlanUpgrade with options.

func (Plan) Render

func (p Plan) Render(w io.Writer) error

Render writes the plan as text, for a terminal or a CI log.

type PlanOptions

type PlanOptions struct {
	// MigrationDir is the application's migration directory, which holds
	// its model registry, relative to its directory unless absolute (as
	// gombit db --dir; default database/migrations). The framework-schema
	// section is judged against it, and its steps act on it.
	MigrationDir string
}

PlanOptions configures PlanUpgradeWith.

type PlannedAction

type PlannedAction struct {
	Change Change `json:"change"`
	// Description says what the action does.
	Description string `json:"description"`
}

PlannedAction is an automatic change an application needs, and the action that applies it.

type Release

type Release struct {
	// Version is the release's version (canonical semver), or Unreleased.
	Version string `yaml:"version" json:"version"`
	// Changes are its upgrade-relevant changes; none for a release with
	// nothing to declare.
	Changes []Change `yaml:"changes,omitempty" json:"changes"`
}

Release is the changes of moving to one release from the one before it.

type Replacement

type Replacement = frameworkmod.Replacement

Replacement is a replace directive's target: a module path and version, or a local directory (no version).

type SchemaChange

type SchemaChange struct {
	// Model is the framework model, by its package within the framework
	// module and its type: auth.RefreshToken, storage/claims.Claim.
	Model string `yaml:"model" json:"model"`
	// Table is the table: the model's own, or a many-to-many join table
	// the model declares (Join).
	Table string `yaml:"table" json:"table"`
	// Join: Table is a join table Model declares (auth_user_groups for
	// auth.User). Its change alters Model; it never adds or removes it. A
	// table that is not the own table of a model this gombit has is read
	// as a join table even without the flag.
	Join bool `yaml:"join,omitempty" json:"join,omitempty"`
	// Change is what happens to it.
	Change SchemaChangeKind `yaml:"change" json:"change"`
	// Destructive: the change drops or narrows something, so the migration
	// loses data unless the application acts first (gombit db plan then
	// asks for --allow).
	Destructive bool `yaml:"destructive,omitempty" json:"destructive,omitempty"`
}

SchemaChange is one framework-owned table a change adds, alters or removes.

func (SchemaChange) Describe

func (sc SchemaChange) Describe() string

Describe is the change in one phrase: "auth.RefreshToken (refresh_tokens): altered".

func (SchemaChange) RegistryModel

func (sc SchemaChange) RegistryModel() migrations.Model

RegistryModel is the model as an application's model registry names it.

type SchemaChangeKind

type SchemaChangeKind string

SchemaChangeKind is how a release changes a framework-owned table.

An entry is about one table. Only the model's own table being added or removed adds or removes the model (the registry's --model and --forget-model); a join table it declares (Join) changing, appearing or going away alters the model, as a column would.

const (
	// SchemaAdded: a new framework model, and its table.
	SchemaAdded SchemaChangeKind = "added"
	// SchemaAltered: the model's table changes (columns, types, indexes).
	SchemaAltered SchemaChangeKind = "altered"
	// SchemaRemoved: the framework no longer has the model; its table is
	// the application's to drop.
	SchemaRemoved SchemaChangeKind = "removed"
)

type SchemaFinding

type SchemaFinding struct {
	SchemaChange
	// Release and ChangeID say where the manifest declares it.
	Release  string `json:"release"`
	ChangeID string `json:"change_id"`
	// Required is whether the application must migrate for it: the change
	// is manual, and its model is one the application has (or, added, one
	// every application needs). An informational change's table is for an
	// application that adopts the feature it belongs to.
	Required bool `json:"required"`
	// Registered is whether the application's model registry lists the
	// model, so its migrations carry the table.
	Registered bool `json:"registered"`
	// Advice says what to do.
	Advice string `json:"advice"`
}

SchemaFinding is a framework-owned table change on an upgrade's path, as it concerns the application.

type SchemaReport

type SchemaReport struct {
	// Registry is the application's model registry file, relative to the
	// application directory, and RegistryFound whether it exists: without
	// one the application has generated no migration yet.
	Registry      string `json:"registry"`
	RegistryFound bool   `json:"registry_found"`
	// Findings are the framework-schema changes on the path, in order.
	Findings []SchemaFinding `json:"findings"`
	// Steps are the migration steps the upgrade needs, in order: the
	// upgrade is not complete until they are done. Empty when no change
	// requires one. They are for the developer: gombit upgrade never
	// generates or applies a migration.
	Steps []string `json:"steps"`
}

SchemaReport is what an upgrade's framework-schema changes mean for an application's migrations.

func AnalyzeSchema

func AnalyzeSchema(workDir, migrationDir string, releases []Release) (SchemaReport, error)

AnalyzeSchema reports what the framework-schema changes of releases (an upgrade's path, in order) mean for the application in workDir, whose migrations are in migrationDir (relative to workDir unless absolute; default database/migrations, as gombit db --dir): per change, whether the application's model registry lists the model and what to do, and the migration steps the upgrade needs, each command with the flags the next one needs to act on the same schema. It reads the registry only, and never touches a database.

func (SchemaReport) MigrationRequired

func (r SchemaReport) MigrationRequired() bool

MigrationRequired reports whether the upgrade needs a migration.

func (SchemaReport) Render

func (r SchemaReport) Render(w io.Writer) error

Render writes the report as text.

type Use

type Use struct {
	// Ident is the identifier denoting the API.
	Ident *ast.Ident
	// Expr is the use: pkg.Name; x.Name (a method, a field, a method
	// expression); or a bare Name (a dot import, a composite literal key).
	Expr ast.Expr
	// Call is the call, or conversion, whose function Expr is, possibly
	// parenthesized and instantiated ((f)(x), f[T](x)); nil when Expr is
	// not called. CallFun builds a new call's Fun keeping those wrappers.
	Call *ast.CallExpr
	// Path is the enclosing nodes, Ident first and the file last.
	Path []ast.Node
	// Info is the type information of the use's package, for the source
	// as it was before any rewrite (new nodes have none).
	Info *types.Info
	// contains filtered or unexported fields
}

Use is one use of a codemod's API, handed to its Rewrite.

func (*Use) CallFun

func (u *Use) CallFun(e ast.Expr) ast.Expr

CallFun is u.Call's function with Expr replaced by e, keeping the parentheses and type arguments around it: the Fun of a Rewrite's new call. Without a call, it is e.

func (*Use) Import

func (u *Use) Import(path string) (string, bool)

Import is the name the use's file refers to the package at path by, adding its import when the file has none; false when the package does not load or that name is shadowed at the use (nothing could refer to it there).

func (*Use) Node

func (u *Use) Node() ast.Expr

Node is what a Rewrite replaces: Call when Expr is called, else Expr.

func (*Use) Skip

func (u *Use) Skip(reason string) ast.Expr

Skip leaves the use for the developer, saying why: a Rewrite returns u.Skip(reason) for a use it cannot rewrite.

Directories

Path Synopsis
Package contractcheck checks an application's generated contract artifacts (the resource *.gen.go, the OpenAPI document, the TypeScript client) after an upgrade, with the existing drift gates' own checks: gombit upgrade contract (UPGRADE-8).
Package contractcheck checks an application's generated contract artifacts (the resource *.gen.go, the OpenAPI document, the TypeScript client) after an upgrade, with the existing drift gates' own checks: gombit upgrade contract (UPGRADE-8).

Jump to

Keyboard shortcuts

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