Documentation
¶
Overview ¶
Package version resolves what a branch is allowed to release, and what the next tag on it should be.
This is the code that decides version numbers. Everything here is a pure function of a branch name and a set of tags, so it can be tested without a repository and without a network.
Index ¶
- func CheckSeries(series string) error
- type Bump
- type Classification
- type GitRepo
- func (g *GitRepo) AllTags(pattern string) ([]string, error)
- func (g *GitRepo) CommitOf(tag string) (string, error)
- func (g *GitRepo) CountCommits(from string) (int, error)
- func (g *GitRepo) CurrentBranch() (string, error)
- func (g *GitRepo) Dir() string
- func (g *GitRepo) Head() (string, error)
- func (g *GitRepo) IsAncestor(commit string) (bool, error)
- func (g *GitRepo) ReachableTags(pattern string) ([]string, error)
- func (g *GitRepo) Subject(commit string) (string, error)
- func (g *GitRepo) TagExists(tag string) (bool, error)
- type Mode
- type Policy
- type Repo
- type Request
- type Result
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CheckSeries ¶
CheckSeries reports whether a series is well formed, as X.Y with no leading zeros and no leading v.
Exported so the commands can reject a malformed series before spending a workflow dispatch to learn the same thing, while the regex and the message stay defined once. The resolver applies it too, where getting it wrong mints a number another branch owns.
Types ¶
type Classification ¶
type Classification struct {
// FromMain says whether this release soaks on unbounded-stable.
FromMain bool
// Latest says whether the GitHub release is marked Latest.
Latest bool
// Report is the reasoning, for the operator.
Report []string
}
Classification is what happens to a release once it has been built.
func Classify ¶
func Classify(repo Repo, tag string) (*Classification, error)
Classify decides whether a release soaks, and whether it is Latest.
There are two questions here and they are NOT the same question. An earlier design answered both with one version comparison and got the first one wrong.
FromMain Should this soak on unbounded-stable?
That cluster soaks main, and only main. A release cut from a
release-X.Y branch has its own soak story, and deploying it to
stable would replace whatever main last put there.
Deliberately a question about PROVENANCE, not version ordering.
Ordering cannot answer it: stable can be running a candidate newer
than the newest final, so "is this version the highest" says
deploy when the honest answer is that this release has no business
on that cluster at all.
Assumes release branches are never merged back into main. The flow
is one-way: fixes land on main and are cherry-picked down, so a
branch's commits stay off main and its tags stay unreachable.
Latest Should the GitHub release be marked Latest?
This one IS about version ordering, because Latest is what
releases/latest/download resolves to, which is the install command
in README.md and every guide. Publishing v0.3.1 after v0.5.0 must
not repoint those at v0.3.1.
GitHub defaults make_latest to true on any newly published
release, so the answer must be passed explicitly on every publish.
Expects repo to be a checkout of the DEFAULT BRANCH with full history and tags, so HEAD is main.
type GitRepo ¶
type GitRepo struct {
// contains filtered or unexported fields
}
GitRepo answers resolution questions from a real clone.
func NewGitRepo ¶
NewGitRepo binds a resolver to a working tree. An empty dir uses the process working directory.
func (*GitRepo) AllTags ¶
AllTags lists tags matching a glob anywhere in the repository.
Errors are returned for the same reason as ReachableTags, and it matters more here: this feeds the Latest decision, where an empty result means "nothing outranks this release". `git tag --list <glob>` exits 0 even in a repository with no commits at all, so an error from it is always a real failure.
func (*GitRepo) CountCommits ¶
CountCommits counts commits in from..HEAD.
func (*GitRepo) CurrentBranch ¶
CurrentBranch returns the branch HEAD is on, or empty when detached.
Used to default relctl's --branch, so the bare command answers about the branch you are actually on. A detached HEAD reports empty rather than guessing: it is what a CI checkout of a tag looks like, and there is no honest answer.
func (*GitRepo) IsAncestor ¶
IsAncestor reports whether commit is an ancestor of HEAD.
`merge-base --is-ancestor` documents the same contract as `rev-parse -q --verify` above: "exit with status 0 if true, or with status 1 if not. Errors are signaled by a non-zero status that is not 1." So exit 1 is an answer and anything else is a failure to answer.
The distinction is not cosmetic, because the two callers fail in opposite directions. In the resolver a swallowed error becomes "not an ancestor", which refuses to tag and is safe. In Classify it becomes FromMain=false, and release-upgrade publishes a release whose from_main is not 'true' with deploy, Orca and smoke all skipped. A broken git command would ship an unsoaked release, which is what the workflow's own notice describes as publishing without a soak.
func (*GitRepo) ReachableTags ¶
ReachableTags lists tags matching a glob that are ancestors of HEAD.
A tag whose commit later leaves the branch's history stops being seen. That fails safe, since the resolver would recompute a version whose tag already exists and refuse it, and it is the correct reading anyway: a tag on a commit that is no longer on the branch is not part of its history.
Errors are returned, not swallowed. The shell this replaces ended the same query with `|| true`, which looks like it is absorbing "no tags match" - but `git tag --merged HEAD --list <glob>` exits 0 with empty output when nothing matches, and 128 only for an unborn HEAD, a missing repository or a canceled context. So there is no legitimate absence to absorb, and swallowing here would turn a real failure into "no tags", which is a floor of v0.0.0 that the resolver would then compute a version from.
func (*GitRepo) TagExists ¶
TagExists reports whether a tag exists anywhere in the repository.
rev-parse exits non-zero for a tag that does not exist, which is the answer rather than a failure, so the distinction is made on the message: anything that is not a resolution failure is reported as an error. Reporting a genuine git failure as "the tag does not exist" would let validate fall through its duplicate-tag guard.
type Policy ¶
type Policy struct {
// Bump is the component that advances.
Bump Bump
// Series is the X.Y a release branch is confined to, empty on main.
Series string
}
Policy is what a branch may cut.
func ForBranch ¶
ForBranch decides what branch may release, and how much it may advance.
The versioning rule (#627):
main cuts vX.Y.0 - a minor, or a major when explicitly asked release-X.Y cuts vX.Y.Z - a patch, and nothing else
Every minor's patch space therefore belongs to exactly one branch, and main never enters it. That is what makes a release branch possible at all: without it, main and release-0.2 would both compute v0.2.5 and compete for the same tag, which pre-1.0 they would do for months, since a minor series here runs twenty-odd releases.
major is never derived. A major is always a deliberate human decision, and deriving one from commit contents would make it an accident waiting for the right commit message.
type Repo ¶
type Repo interface {
// ReachableTags lists tags matching a glob that are ancestors of HEAD.
ReachableTags(pattern string) ([]string, error)
// AllTags lists tags matching a glob ANYWHERE, reachable or not.
//
// Separate from ReachableTags because they answer different questions, and
// conflating them is a real bug in both directions. Discovery must be
// reachability-scoped so a stray tag on someone's branch cannot drive the
// numbering; the Latest decision must NOT be, because a release branch's
// own tags are invisible from main and are exactly what could outrank it.
AllTags(pattern string) ([]string, error)
// TagExists reports whether a tag exists ANYWHERE, not only on this branch.
TagExists(tag string) (bool, error)
// Head returns the commit HEAD points at.
Head() (string, error)
// CommitOf resolves a tag to its commit.
CommitOf(tag string) (string, error)
// IsAncestor reports whether commit is an ancestor of HEAD.
IsAncestor(commit string) (bool, error)
// CountCommits counts commits in from..HEAD.
CountCommits(from string) (int, error)
// Subject returns a commit's subject line.
Subject(commit string) (string, error)
}
Repo is the git history a resolution is computed against.
An interface because every question here is about tags and ancestry, and answering them from a fixture is what makes the resolver testable without a repository per case.
type Request ¶
type Request struct {
Mode Mode
// Bump only applies when STARTING a train; it is ignored when continuing one.
Bump Bump
// Series confines the result to X.Y, set when cutting from a release branch.
Series string
// Pre is an explicit prerelease suffix, e.g. rc.3.
Pre string
// Version is an explicit final for promote.
Version string
// AllowConcurrentTrains permits starting a second live train.
AllowConcurrentTrains bool
}
Request is what the caller is asking for.
type Result ¶
type Result struct {
// Tag is the tag to create.
Tag string
// Base is the commit to create it at.
Base string
// LatestFinal is the highest final reachable from HEAD, or v0.0.0.
LatestFinal string
// Live are cores still in flight.
Live []string
// Stale are cores whose candidates were abandoned.
Stale []string
// Report is the human-facing state report, in order.
Report []string
// Warnings are conditions worth surfacing that do not refuse.
Warnings []string
}
Result is the resolution, plus everything the operator needs to see.
func Resolve ¶
Resolve works out which tag a release should mint, and at which commit.
Why Base exists: promote finalizes a candidate that was already built, deployed and smoke-tested, so tagging HEAD would ship a DIFFERENT tree under a version whose only claim to being trustworthy is that soak. promote resolves the candidate's commit; release and prerelease resolve HEAD.