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
- Variables
- func DeprecationSince(paragraph string) string
- func MetadataBlock(scaffold int) string
- func RemovableFrom(since string) string
- func RemovableFromMajor(since string) string
- func RenderDeprecated(w io.Writer, uses []DeprecatedUse) error
- func RenderNotesDoc(w io.Writer, m *Manifest) error
- func UpcomingReleases(latest string) []string
- func WriteConfigTemplates(workDir string, releases []Release) ([]string, error)
- type Action
- type Applied
- type Area
- type Baseline
- type Bump
- type Change
- type Classification
- type Codemod
- type ConfigCategory
- type ConfigChange
- type ConfigChangeKind
- type ConfigFile
- type ConfigFinding
- type ConfigReport
- type DependencyChange
- type DeprecatedUse
- type FileChange
- type FileFix
- type Fix
- type Framework
- type GoCommand
- type GoCommandError
- type Kind
- type LeftUse
- type Manifest
- func (m *Manifest) CheckPolicy(version string) error
- func (m *Manifest) CheckRelease(tag string) error
- func (m *Manifest) Latest() string
- func (m *Manifest) Path(from, to string) ([]Release, error)
- func (m *Manifest) PathFrom(fw Framework, to string) ([]Release, error)
- func (m *Manifest) Release(version string) (Release, error)
- func (m *Manifest) RenderNotes(w io.Writer, releases []Release) error
- type Plan
- type PlanOptions
- type PlannedAction
- type Release
- type Replacement
- type SchemaChange
- type SchemaChangeKind
- type SchemaFinding
- type SchemaReport
- type Use
Constants ¶
const FrameworkModulePath = frameworkmod.ModulePath
FrameworkModulePath is the module a Gombit application requires.
const ManifestFormat = 1
ManifestFormat is the format of manifest.yaml this framework reads.
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.
const ProjectFile = "gombit.yaml"
ProjectFile is the file the scaffold version is recorded in, at the root of an application.
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.
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 ¶
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") )
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.
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.
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.
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.
var ErrNoUpgradePath = errors.New("upgrade: no upgrade path")
ErrNoUpgradePath: the manifest describes no upgrade between the versions asked for.
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 ¶
DeprecationSince is the release a "Deprecated:" paragraph names as the one that deprecated the API, or "" when it names none.
func MetadataBlock ¶
MetadataBlock is the gombit.yaml block recording scaffold version scaffold, as gombit new writes it.
func RemovableFrom ¶
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 ¶
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 ¶
RenderNotesDoc writes docs/upgrade-notes.md: the notes of every release in m, newest first.
func UpcomingReleases ¶
UpcomingReleases are the releases that can follow latest: its next patch, minor and major.
func WriteConfigTemplates ¶
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 ¶
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.
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 ¶
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 ¶
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.
func RequiredBump ¶
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.
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.
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.
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 ¶
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 ¶
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 ¶
Patch is the rewrite as a unified diff, relative to the application directory: `git apply` (or `patch -p1`) applies it there.
func (*Fix) Write ¶
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 ¶
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 ¶
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 ¶
ParseManifest decodes and validates a manifest. Unknown fields are refused, so a misspelt key cannot drop a change's classification.
func (*Manifest) CheckPolicy ¶
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 ¶
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) Path ¶
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 ¶
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 ¶
Release returns the manifest's entry for version (a release, or Unreleased).
func (*Manifest) RenderNotes ¶
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 ¶
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 ¶
PlanUpgradeWith is PlanUpgrade with options.
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.
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 ¶
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 ¶
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).
Source Files
¶
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). |