mirror of
https://github.com/juanfont/headscale.git
synced 2026-10-02 21:03:36 +09:00
tools/bump: add the version bump tool
Keeps the interlocked pins current and reports what it could not move. Areas apply, gate and commit one at a time, so a dependency that breaks the build costs one commit rather than the whole pull request.
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
)
|
||||
|
||||
type state string
|
||||
|
||||
const (
|
||||
stateApplied state = "applied"
|
||||
statePartial state = "partial"
|
||||
stateEmpty state = "unchanged"
|
||||
stateDropped state = "dropped"
|
||||
stateSkipped state = "skipped"
|
||||
)
|
||||
|
||||
// change is what an area did, in a shape the report can render directly.
|
||||
type change struct {
|
||||
Summary string
|
||||
Detail []string
|
||||
// Drops are sub-units the area rewound on its own, such as a single
|
||||
// dependency that failed to build.
|
||||
Drops []dropped
|
||||
Empty bool
|
||||
}
|
||||
|
||||
// area is one independently revertible unit of work. Areas are the granularity
|
||||
// at which the bot succeeds or fails: each becomes its own commit, so a broken
|
||||
// one can be dropped without disturbing the others.
|
||||
type area struct {
|
||||
Name string
|
||||
Needs []string
|
||||
// Message renders the commit subject, in the repository's
|
||||
// "package: imperative description" style.
|
||||
Message func(change) string
|
||||
Apply func(ctx context.Context, r *repo) (change, error)
|
||||
// Gate is a cheap structural check. Anything that needs a build belongs
|
||||
// in the final gate instead.
|
||||
Gate func(ctx context.Context, r *repo) error
|
||||
}
|
||||
|
||||
type result struct {
|
||||
Area string
|
||||
State state
|
||||
Change change
|
||||
Reason string
|
||||
Log string
|
||||
Commit string
|
||||
}
|
||||
|
||||
func coreAreas() []area {
|
||||
return []area{
|
||||
{
|
||||
Name: "flake",
|
||||
Apply: applyFlake,
|
||||
Gate: gateFlake,
|
||||
Message: func(c change) string { return "flake.lock: update " + c.Summary },
|
||||
},
|
||||
{
|
||||
Name: "gomod",
|
||||
Needs: []string{"flake"},
|
||||
Apply: applyGoMod,
|
||||
Gate: gateGoMod,
|
||||
Message: func(c change) string { return "go.mod: " + c.Summary },
|
||||
},
|
||||
{
|
||||
Name: "docker-go",
|
||||
Needs: []string{"flake"},
|
||||
Apply: applyDockerGo,
|
||||
Gate: gateDockerGo,
|
||||
Message: func(c change) string { return "Dockerfile: bump " + c.Summary },
|
||||
},
|
||||
{
|
||||
Name: "generate",
|
||||
Needs: []string{"gomod"},
|
||||
Apply: applyGenerate,
|
||||
Gate: gateGenerate,
|
||||
Message: func(change) string { return "all: regenerate generated files" },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// runAreas applies each area in order, committing the ones that hold and
|
||||
// rewinding the ones that do not. A dropped area leaves no residue because the
|
||||
// rewind is a hard reset to a commit that is known good.
|
||||
func runAreas(ctx context.Context, r *repo, areas []area, want func(string) bool) ([]result, error) {
|
||||
states := make(map[string]state, len(areas))
|
||||
results := make([]result, 0, len(areas))
|
||||
|
||||
record := func(res result) {
|
||||
states[res.Area] = res.State
|
||||
results = append(results, res)
|
||||
}
|
||||
|
||||
for _, a := range areas {
|
||||
if !want(a.Name) {
|
||||
record(result{Area: a.Name, State: stateSkipped, Reason: "not selected"})
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if blocker, ok := blockedBy(a, states); ok {
|
||||
record(result{Area: a.Name, State: stateSkipped, Reason: "depends on dropped " + blocker})
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
res, err := runArea(ctx, r, a)
|
||||
if err != nil {
|
||||
return results, err
|
||||
}
|
||||
|
||||
record(res)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// blockedBy reports the first dependency that did not survive.
|
||||
func blockedBy(a area, states map[string]state) (string, bool) {
|
||||
for _, need := range a.Needs {
|
||||
if states[need] == stateDropped {
|
||||
return need, true
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
func runArea(ctx context.Context, r *repo, a area) (result, error) {
|
||||
snapshot, err := headSHA(ctx, r)
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
|
||||
log.Printf("area %s: applying", a.Name)
|
||||
|
||||
rewind := func(res result) (result, error) {
|
||||
err := resetTo(ctx, r, snapshot)
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
ch, err := a.Apply(ctx, r)
|
||||
if err != nil {
|
||||
return rewind(result{
|
||||
Area: a.Name, State: stateDropped,
|
||||
Reason: reasonOf(err), Log: logOf(err),
|
||||
})
|
||||
}
|
||||
|
||||
if ch.Empty {
|
||||
return rewind(result{Area: a.Name, State: stateEmpty, Change: ch})
|
||||
}
|
||||
|
||||
if a.Gate != nil {
|
||||
err := a.Gate(ctx, r)
|
||||
if err != nil {
|
||||
return rewind(result{
|
||||
Area: a.Name, State: stateDropped, Change: ch,
|
||||
Reason: reasonOf(err), Log: logOf(err),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
err = commitAll(ctx, r, a.Message(ch))
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
|
||||
sha, err := headSHA(ctx, r)
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
|
||||
st := stateApplied
|
||||
if len(ch.Drops) > 0 {
|
||||
st = statePartial
|
||||
}
|
||||
|
||||
return result{Area: a.Name, State: st, Change: ch, Commit: sha}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user