mirror of
https://github.com/juanfont/headscale.git
synced 2026-10-03 05:13: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,149 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// repo is a checkout the bump operates on. Every helper runs from the
|
||||
// repository root so relative paths in the Makefile recipes and the
|
||||
// //go:generate directives resolve the way they do for a developer.
|
||||
type repo struct {
|
||||
Root string
|
||||
// System is the Nix system double (e.g. x86_64-linux) used to address
|
||||
// flake checks.
|
||||
System string
|
||||
}
|
||||
|
||||
// tailLines is how much of a failing command's output is carried into the
|
||||
// report. Enough to see a compiler error, short enough to paste into a pull
|
||||
// request body.
|
||||
const tailLines = 40
|
||||
|
||||
// cmdError carries the tail of a failed command's output so the reason an
|
||||
// area was dropped survives all the way into the pull request body.
|
||||
type cmdError struct {
|
||||
Argv []string
|
||||
Tail string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *cmdError) Error() string {
|
||||
return fmt.Sprintf("%s: %v\n%s", strings.Join(e.Argv, " "), e.Err, e.Tail)
|
||||
}
|
||||
|
||||
func (e *cmdError) Unwrap() error { return e.Err }
|
||||
|
||||
func openRepo(ctx context.Context) (*repo, error) {
|
||||
out, err := exec.CommandContext(ctx, "git", "rev-parse", "--show-toplevel").Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("locating repository root: %w", err)
|
||||
}
|
||||
|
||||
r := &repo{Root: strings.TrimSpace(string(out))}
|
||||
|
||||
sys, err := r.run(ctx, "nix", "eval", "--impure", "--raw", "--expr", "builtins.currentSystem")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.System = strings.TrimSpace(sys)
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// run executes argv in the repository root and returns its combined output.
|
||||
func (r *repo) run(ctx context.Context, argv ...string) (string, error) {
|
||||
return r.runIn(ctx, r.Root, argv...)
|
||||
}
|
||||
|
||||
// runIn executes argv in dir, which may be relative to the repository root.
|
||||
func (r *repo) runIn(ctx context.Context, dir string, argv ...string) (string, error) {
|
||||
if !filepath.IsAbs(dir) {
|
||||
dir = filepath.Join(r.Root, dir)
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, argv[0], argv[1:]...) //nolint:gosec // argv is built from repo state, not user input
|
||||
cmd.Dir = dir
|
||||
|
||||
// Streams stay separate: nix writes progress and a dirty-tree warning to
|
||||
// stderr, and folding that into stdout corrupts every value parsed out of
|
||||
// a command run through the devShell.
|
||||
var stdout, stderr bytes.Buffer
|
||||
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
cmd.Env = append(os.Environ(), "GOWORK=off", "NIX_CONFIG=warn-dirty = false")
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return stdout.String(), &cmdError{
|
||||
Argv: argv,
|
||||
Tail: tail(stderr.String() + stdout.String()),
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
return stdout.String(), nil
|
||||
}
|
||||
|
||||
// nixRun executes argv inside `nix develop`. It re-enters the shell on every
|
||||
// call on purpose: a single long-lived shell would pin every later command to
|
||||
// the toolchain that was locked before the flake area ran.
|
||||
func (r *repo) nixRun(ctx context.Context, argv ...string) (string, error) {
|
||||
return r.nixRunIn(ctx, r.Root, argv...)
|
||||
}
|
||||
|
||||
func (r *repo) nixRunIn(ctx context.Context, dir string, argv ...string) (string, error) {
|
||||
full := append([]string{"nix", "develop", "--fallback", "--command"}, argv...)
|
||||
|
||||
return r.runIn(ctx, dir, full...)
|
||||
}
|
||||
|
||||
// path joins a repository-relative path onto the root.
|
||||
func (r *repo) path(rel ...string) string {
|
||||
return filepath.Join(append([]string{r.Root}, rel...)...)
|
||||
}
|
||||
|
||||
// readFile reads a repository-relative file.
|
||||
func (r *repo) readFile(rel string) (string, error) {
|
||||
b, err := os.ReadFile(r.path(rel))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("reading %s: %w", rel, err)
|
||||
}
|
||||
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// writeFile writes a repository-relative file, preserving its current mode.
|
||||
func (r *repo) writeFile(rel, content string) error {
|
||||
name := r.path(rel)
|
||||
|
||||
mode := os.FileMode(0o644)
|
||||
if fi, err := os.Stat(name); err == nil { //nolint:noinlineerr
|
||||
mode = fi.Mode().Perm()
|
||||
}
|
||||
|
||||
err := os.WriteFile(name, []byte(content), mode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing %s: %w", rel, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// tail returns the last tailLines lines of s.
|
||||
func tail(s string) string {
|
||||
lines := strings.Split(strings.TrimRight(s, "\n"), "\n")
|
||||
if len(lines) > tailLines {
|
||||
lines = lines[len(lines)-tailLines:]
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
Reference in New Issue
Block a user