Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87af97514d | ||
|
|
9236c0727c | ||
|
|
4418dc3e97 | ||
|
|
5a0da6da12 | ||
|
|
13a35a986a |
No files matched your search
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
yarn.lock
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"tabWidth": 4,
|
||||||
|
"proseWrap": "always"
|
||||||
|
}
|
||||||
@@ -32,8 +32,8 @@ mkdir /Volumes/usbstick/mybackup
|
|||||||
# (System Settings → Privacy & Security → Full Disk Access), otherwise
|
# (System Settings → Privacy & Security → Full Disk Access), otherwise
|
||||||
# the backup will abort with a permission error on protected directories
|
# the backup will abort with a permission error on protected directories
|
||||||
|
|
||||||
# run your first backup (the default config backs up ~ and /Applications
|
# run your first backup (the default config backs up ~, and /Applications
|
||||||
# with sensible excludes)
|
# on macOS only, with sensible excludes)
|
||||||
vaultik snapshot create
|
vaultik snapshot create
|
||||||
|
|
||||||
# see what you have
|
# see what you have
|
||||||
@@ -957,7 +957,9 @@ development workflow, and the Makefile targets are thin shims that call
|
|||||||
them. We provide:
|
them. We provide:
|
||||||
|
|
||||||
* `script/bootstrap` — install all development dependencies (Go, Go
|
* `script/bootstrap` — install all development dependencies (Go, Go
|
||||||
module download). A host without Go gets the `go.mod` version through
|
module download, and node, yarn and `prettier` for Markdown). Node
|
||||||
|
comes from the host, or else at a pinned version through `nvm`. A
|
||||||
|
host without Go gets the `go.mod` version through
|
||||||
`script/install-go`, in `.tool/go`, which `script/bootstrap` itself,
|
`script/install-go`, in `.tool/go`, which `script/bootstrap` itself,
|
||||||
the `Makefile`, `script/fmt`, `script/fmt-check`, `script/precommit`
|
the `Makefile`, `script/fmt`, `script/fmt-check`, `script/precommit`
|
||||||
and `script/release` add to their `PATH`. It
|
and `script/release` add to their `PATH`. It
|
||||||
@@ -1011,9 +1013,11 @@ them. We provide:
|
|||||||
needs a *local* daemon. It is a developer convenience and never a
|
needs a *local* daemon. It is a developer convenience and never a
|
||||||
gate: no gate reads its exit status. Run `make lint` afterwards to
|
gate: no gate reads its exit status. Run `make lint` afterwards to
|
||||||
find out whether the tree is clean.
|
find out whether the tree is clean.
|
||||||
* `script/fmt` — format all code (writes)
|
* `script/fmt` — format all code (writes): Go with `go fmt`, Markdown
|
||||||
|
with `prettier`
|
||||||
* `script/fmt-check` — check formatting (read-only). It runs `gofmt` on
|
* `script/fmt-check` — check formatting (read-only). It runs `gofmt` on
|
||||||
the host, over every Go file outside `.tool`.
|
the host, over every Go file outside `.tool`, and `prettier --check`
|
||||||
|
over every Markdown file.
|
||||||
* `script/check` — run `script/test`, `script/lint`, and
|
* `script/check` — run `script/test`, `script/lint`, and
|
||||||
`script/fmt-check`.
|
`script/fmt-check`.
|
||||||
* `script/docker` — build the Docker image tagged via
|
* `script/docker` — build the Docker image tagged via
|
||||||
|
|||||||
@@ -24,6 +24,17 @@ owner's; a `v*` tag runs the release build in
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-11-10: Printed the status and progress lines of `remote info`
|
||||||
|
and `snapshot verify` through `internal/ui`, with its marker and
|
||||||
|
color, as the README describes
|
||||||
|
([issue #410](https://git.eeqj.de/sneak/vaultik/issues/410)). Their
|
||||||
|
reports stay plain.
|
||||||
|
|
||||||
|
- 2026-11-10: Made `config init` write the `apps` snapshot, which backs
|
||||||
|
up `/Applications`, only on macOS
|
||||||
|
([issue #408](https://git.eeqj.de/sneak/vaultik/issues/408)). On
|
||||||
|
Linux the first `snapshot create` stopped on the missing directory.
|
||||||
|
|
||||||
- 2026-10-15: Stopped `snapshot create --prune` pruning after a backup
|
- 2026-10-15: Stopped `snapshot create --prune` pruning after a backup
|
||||||
in which a volume mounted below a configured path in an earlier
|
in which a volume mounted below a configured path in an earlier
|
||||||
snapshot of the same name was not mounted
|
snapshot of the same name was not mounted
|
||||||
|
|||||||
+1
-1
@@ -90,7 +90,7 @@ func setupGlobals(
|
|||||||
// blank line. Used both from the fx hook (for subcommand invocations) and
|
// blank line. Used both from the fx hook (for subcommand invocations) and
|
||||||
// from the root cobra Run handler (for `vaultik` with no subcommand).
|
// from the root cobra Run handler (for `vaultik` with no subcommand).
|
||||||
func writeStartupBanner(w *ui.Writer, startTime time.Time, shortCommit string) {
|
func writeStartupBanner(w *ui.Writer, startTime time.Time, shortCommit string) {
|
||||||
w.Bannerf("%s %s by %s (commit %s, built on %s) starting up at %s.",
|
w.Bannerf("%s %s by %s (commit %s, commit date %s) starting up at %s.",
|
||||||
globals.Appname, globals.Version, globals.Author,
|
globals.Appname, globals.Version, globals.Author,
|
||||||
shortCommit, globals.CommitDate,
|
shortCommit, globals.CommitDate,
|
||||||
startTime.Format(time.RFC3339))
|
startTime.Format(time.RFC3339))
|
||||||
|
|||||||
+27
-4
@@ -8,6 +8,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"runtime"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -30,7 +31,7 @@ const configSetArgs = 2
|
|||||||
// parent config dirs (e.g. ~/.config) are conventionally traversable.
|
// parent config dirs (e.g. ~/.config) are conventionally traversable.
|
||||||
const configDirMode = 0o755
|
const configDirMode = 0o755
|
||||||
|
|
||||||
// configYAMLIndent matches the 2-space indentation of defaultConfigTemplate,
|
// configYAMLIndent matches the 2-space indentation of the default config,
|
||||||
// so `config set` writes the file back with the same indentation rather than
|
// so `config set` writes the file back with the same indentation rather than
|
||||||
// yaml.Marshal's 4-space default.
|
// yaml.Marshal's 4-space default.
|
||||||
const configYAMLIndent = 2
|
const configYAMLIndent = 2
|
||||||
@@ -50,7 +51,22 @@ var (
|
|||||||
"so the file was not changed")
|
"so the file was not changed")
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultConfigTemplate = `# vaultik configuration
|
// macOS is the value of runtime.GOOS on macOS.
|
||||||
|
const macOS = "darwin"
|
||||||
|
|
||||||
|
// defaultConfigText returns the file `config init` writes on the operating
|
||||||
|
// system goos, named as runtime.GOOS names it. Only macOS gets the apps
|
||||||
|
// snapshot, since /Applications exists nowhere else.
|
||||||
|
func defaultConfigText(goos string) string {
|
||||||
|
if goos == macOS {
|
||||||
|
return defaultConfigHead + appsSnapshotConfig + defaultConfigTail
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultConfigHead + defaultConfigTail
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultConfigHead is the default config up to the end of the home snapshot.
|
||||||
|
const defaultConfigHead = `# vaultik configuration
|
||||||
# Documentation: https://sneak.berlin/go/vaultik
|
# Documentation: https://sneak.berlin/go/vaultik
|
||||||
|
|
||||||
# ─── REQUIRED ────────────────────────────────────────────────────────────────
|
# ─── REQUIRED ────────────────────────────────────────────────────────────────
|
||||||
@@ -168,7 +184,10 @@ snapshots:
|
|||||||
- "/Movies/ProxyMedia"
|
- "/Movies/ProxyMedia"
|
||||||
- "/Music/iTunes/Album Artwork"
|
- "/Music/iTunes/Album Artwork"
|
||||||
- "/Pictures/iPod Photo Cache"
|
- "/Pictures/iPod Photo Cache"
|
||||||
|
`
|
||||||
|
|
||||||
|
// appsSnapshotConfig is the apps snapshot of the default config.
|
||||||
|
const appsSnapshotConfig = `
|
||||||
# Third-party applications. OS-provided apps live in /System/Applications
|
# Third-party applications. OS-provided apps live in /System/Applications
|
||||||
# on modern macOS and are never in /Applications, but Apple-installed
|
# on modern macOS and are never in /Applications, but Apple-installed
|
||||||
# App Store apps (Safari, GarageBand, iWork, iMovie) are excluded since
|
# App Store apps (Safari, GarageBand, iWork, iMovie) are excluded since
|
||||||
@@ -189,7 +208,10 @@ snapshots:
|
|||||||
- "/Steam.app"
|
- "/Steam.app"
|
||||||
- "/VirtualBox.app"
|
- "/VirtualBox.app"
|
||||||
- "/Utilities/Adobe Installers"
|
- "/Utilities/Adobe Installers"
|
||||||
|
`
|
||||||
|
|
||||||
|
// defaultConfigTail is the default config after the snapshots.
|
||||||
|
const defaultConfigTail = `
|
||||||
# Storage backend (pick ONE of the three forms below).
|
# Storage backend (pick ONE of the three forms below).
|
||||||
#
|
#
|
||||||
# S3-compatible:
|
# S3-compatible:
|
||||||
@@ -311,7 +333,8 @@ on macOS, ~/.config/ on Linux, /etc/vaultik/ as root).`,
|
|||||||
return fmt.Errorf("creating config directory %s: %w", dir, err)
|
return fmt.Errorf("creating config directory %s: %w", dir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.WriteFile(path, []byte(defaultConfigTemplate), configFileMode)
|
err = os.WriteFile(path, []byte(defaultConfigText(runtime.GOOS)),
|
||||||
|
configFileMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("writing config file: %w", err)
|
return fmt.Errorf("writing config file: %w", err)
|
||||||
}
|
}
|
||||||
@@ -560,7 +583,7 @@ func yamlPathOnly(root *yaml.Node, keys []string) (*yaml.Node, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// marshalConfigYAML renders a config document tree with 2-space indentation,
|
// marshalConfigYAML renders a config document tree with 2-space indentation,
|
||||||
// matching defaultConfigTemplate. yaml.Marshal defaults to 4 spaces, which
|
// matching the default config. yaml.Marshal defaults to 4 spaces, which
|
||||||
// would reindent the whole file on the first `config set` despite the promise
|
// would reindent the whole file on the first `config set` despite the promise
|
||||||
// to preserve formatting.
|
// to preserve formatting.
|
||||||
func marshalConfigYAML(root *yaml.Node) ([]byte, error) {
|
func marshalConfigYAML(root *yaml.Node) ([]byte, error) {
|
||||||
|
|||||||
@@ -13,14 +13,15 @@ import (
|
|||||||
"sneak.berlin/go/vaultik/internal/ui"
|
"sneak.berlin/go/vaultik/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestDefaultConfigTemplateParses ensures the init template is valid YAML
|
// TestDefaultConfigParsesOnMacOS ensures the config `config init` writes on
|
||||||
// that unmarshals into the Config struct with the expected snapshots.
|
// macOS is valid YAML that unmarshals into the Config struct with the
|
||||||
func TestDefaultConfigTemplateParses(t *testing.T) {
|
// expected snapshots.
|
||||||
|
func TestDefaultConfigParsesOnMacOS(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
var cfg config.Config
|
var cfg config.Config
|
||||||
|
|
||||||
err := yaml.Unmarshal([]byte(defaultConfigTemplate), &cfg)
|
err := yaml.Unmarshal([]byte(defaultConfigText("darwin")), &cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("default config template is not valid YAML: %v", err)
|
t.Fatalf("default config template is not valid YAML: %v", err)
|
||||||
}
|
}
|
||||||
@@ -58,6 +59,39 @@ func TestDefaultConfigTemplateParses(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestDefaultConfigOnLinux checks that the config `config init` writes on
|
||||||
|
// Linux loads with its home snapshot and without the apps snapshot, whose
|
||||||
|
// /Applications exists only on macOS.
|
||||||
|
func TestDefaultConfigOnLinux(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
path := filepath.Join(t.TempDir(), "config.yml")
|
||||||
|
|
||||||
|
err := os.WriteFile(path, []byte(defaultConfigText("linux")), configFileMode)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("write config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = writeConfigSet(ui.NewWithColor(&bytes.Buffer{}, false),
|
||||||
|
path, "storage_url", "file:///mnt/backups")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("config set storage_url: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := config.Load(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("config.Load: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := cfg.Snapshots["home"]; !ok {
|
||||||
|
t.Error("expected 'home' snapshot in the Linux default config")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := cfg.Snapshots["apps"]; ok {
|
||||||
|
t.Error("the Linux default config should have no 'apps' snapshot")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// testRecipient is a valid age public key.
|
// testRecipient is a valid age public key.
|
||||||
const testRecipient = "age1278m9q7dp3chsh2dcy82qk27v047zywyvtxwnj4cvt0z65jw6a7q5dqhfj"
|
const testRecipient = "age1278m9q7dp3chsh2dcy82qk27v047zywyvtxwnj4cvt0z65jw6a7q5dqhfj"
|
||||||
|
|
||||||
@@ -71,7 +105,7 @@ func TestConfigSetRecipientOnFreshConfig(t *testing.T) {
|
|||||||
|
|
||||||
path := filepath.Join(t.TempDir(), "config.yml")
|
path := filepath.Join(t.TempDir(), "config.yml")
|
||||||
|
|
||||||
err := os.WriteFile(path, []byte(defaultConfigTemplate), configFileMode)
|
err := os.WriteFile(path, []byte(defaultConfigText(macOS)), configFileMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("write config: %v", err)
|
t.Fatalf("write config: %v", err)
|
||||||
}
|
}
|
||||||
@@ -220,7 +254,7 @@ func TestConfigSetKeepsFileThatWouldNotLoad(t *testing.T) {
|
|||||||
{levelReference, "blob_size_limit", "1MB",
|
{levelReference, "blob_size_limit", "1MB",
|
||||||
"blob_size_limit must be at least"},
|
"blob_size_limit must be at least"},
|
||||||
// The type check on a config that does not load yet.
|
// The type check on a config that does not load yet.
|
||||||
{defaultConfigTemplate, "snapshots.apps.paths", "/srv",
|
{defaultConfigText(macOS), "snapshots.apps.paths", "/srv",
|
||||||
"into []string"},
|
"into []string"},
|
||||||
// The type check on a config that cannot be read here.
|
// The type check on a config that cannot be read here.
|
||||||
{configWithUnsetReference, "snapshots.home.paths", "/x",
|
{configWithUnsetReference, "snapshots.home.paths", "/x",
|
||||||
@@ -380,7 +414,7 @@ func loadAfterConfigSet(t *testing.T, key, value string) *config.Config {
|
|||||||
|
|
||||||
path := filepath.Join(t.TempDir(), "config.yml")
|
path := filepath.Join(t.TempDir(), "config.yml")
|
||||||
|
|
||||||
err := os.WriteFile(path, []byte(defaultConfigTemplate), configFileMode)
|
err := os.WriteFile(path, []byte(defaultConfigText(macOS)), configFileMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("write config: %v", err)
|
t.Fatalf("write config: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/adrg/xdg"
|
"github.com/adrg/xdg"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"sneak.berlin/go/vaultik/internal/globals"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Command words and flags used to build argument vectors below. They are
|
// Command words and flags used to build argument vectors below. They are
|
||||||
@@ -112,6 +113,35 @@ func TestBannerStillPrintedWithoutSuppressingFlag(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestBannerLabelsCommitDate checks that the banner prints the commit
|
||||||
|
// and the commit date, each after its own label. Commit and CommitDate
|
||||||
|
// get different values, so printing the wrong one fails.
|
||||||
|
//
|
||||||
|
//nolint:paralleltest // sets globals.Commit and globals.CommitDate
|
||||||
|
func TestBannerLabelsCommitDate(t *testing.T) {
|
||||||
|
const (
|
||||||
|
commit = "abc1234"
|
||||||
|
commitDate = "2026-11-09"
|
||||||
|
)
|
||||||
|
|
||||||
|
previousCommit, previousCommitDate := globals.Commit, globals.CommitDate
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
globals.Commit = previousCommit
|
||||||
|
globals.CommitDate = previousCommitDate
|
||||||
|
})
|
||||||
|
|
||||||
|
globals.Commit = commit
|
||||||
|
globals.CommitDate = commitDate
|
||||||
|
|
||||||
|
var banner bytes.Buffer
|
||||||
|
|
||||||
|
emitStartupBanner(nil, &banner)
|
||||||
|
|
||||||
|
assert.Contains(t, banner.String(),
|
||||||
|
"(commit "+commit+", commit date "+commitDate+")")
|
||||||
|
}
|
||||||
|
|
||||||
// TestBannerSuppressedInArgs covers the suppression scan directly,
|
// TestBannerSuppressedInArgs covers the suppression scan directly,
|
||||||
// including the flags that suppressed the banner before --json joined
|
// including the flags that suppressed the banner before --json joined
|
||||||
// them, so that adding --json cannot regress them.
|
// them, so that adding --json cannot regress them.
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ func writeVersion(out *ui.Writer) {
|
|||||||
w := out.Out()
|
w := out.Out()
|
||||||
|
|
||||||
_, _ = fmt.Fprintf(w, "vaultik %s\n", globals.Version)
|
_, _ = fmt.Fprintf(w, "vaultik %s\n", globals.Version)
|
||||||
_, _ = fmt.Fprintf(w, " commit: %s\n", globals.Commit)
|
_, _ = fmt.Fprintf(w, " commit: %s\n", globals.Commit)
|
||||||
_, _ = fmt.Fprintf(w, " build date: %s\n", globals.CommitDate)
|
_, _ = fmt.Fprintf(w, " commit date: %s\n", globals.CommitDate)
|
||||||
_, _ = fmt.Fprintf(w, " go: %s\n", runtime.Version())
|
_, _ = fmt.Fprintf(w, " go: %s\n", runtime.Version())
|
||||||
_, _ = fmt.Fprintf(w, " os/arch: %s/%s\n", runtime.GOOS, runtime.GOARCH)
|
_, _ = fmt.Fprintf(w, " os/arch: %s/%s\n", runtime.GOOS, runtime.GOARCH)
|
||||||
_, _ = fmt.Fprintf(w, " author: %s\n", globals.Author)
|
_, _ = fmt.Fprintf(w, " author: %s\n", globals.Author)
|
||||||
_, _ = fmt.Fprintf(w, " homepage: %s\n", globals.Homepage)
|
_, _ = fmt.Fprintf(w, " homepage: %s\n", globals.Homepage)
|
||||||
_, _ = fmt.Fprintf(w, " license: %s\n", globals.License)
|
_, _ = fmt.Fprintf(w, " license: %s\n", globals.License)
|
||||||
|
|
||||||
if globals.IsDevVersion(globals.Version) {
|
if globals.IsDevVersion(globals.Version) {
|
||||||
_, _ = fmt.Fprintln(w)
|
_, _ = fmt.Fprintln(w)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package cli_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -49,6 +50,42 @@ func TestVersionCommandReportsBuildVersion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestVersionCommandReportLines checks the label, padding and value of
|
||||||
|
// every line of the report. Commit and CommitDate get different values,
|
||||||
|
// so a line that prints the wrong one fails.
|
||||||
|
//
|
||||||
|
//nolint:paralleltest // sets globals.Commit and globals.CommitDate
|
||||||
|
func TestVersionCommandReportLines(t *testing.T) {
|
||||||
|
const (
|
||||||
|
commit = "abc1234"
|
||||||
|
commitDate = "2026-11-09"
|
||||||
|
)
|
||||||
|
|
||||||
|
previousCommit, previousCommitDate := globals.Commit, globals.CommitDate
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
globals.Commit = previousCommit
|
||||||
|
globals.CommitDate = previousCommitDate
|
||||||
|
})
|
||||||
|
|
||||||
|
globals.Commit = commit
|
||||||
|
globals.CommitDate = commitDate
|
||||||
|
|
||||||
|
want := "vaultik " + globals.Version + "\n" +
|
||||||
|
" commit: " + commit + "\n" +
|
||||||
|
" commit date: " + commitDate + "\n" +
|
||||||
|
" go: " + runtime.Version() + "\n" +
|
||||||
|
" os/arch: " + runtime.GOOS + "/" + runtime.GOARCH + "\n" +
|
||||||
|
" author: " + globals.Author + "\n" +
|
||||||
|
" homepage: " + globals.Homepage + "\n" +
|
||||||
|
" license: " + globals.License + "\n"
|
||||||
|
|
||||||
|
out := runVersionCommand(t)
|
||||||
|
if !strings.HasPrefix(out, want) {
|
||||||
|
t.Errorf("report:\n%s\nwant it to start with:\n%s", out, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestVersionCommandFlagsDevelopmentBuild is the regression test for
|
// TestVersionCommandFlagsDevelopmentBuild is the regression test for
|
||||||
// the thing this command exists to prevent: a build that is not a
|
// the thing this command exists to prevent: a build that is not a
|
||||||
// release must say so. The notice used to be gated on the version
|
// release must say so. The notice used to be gated on the version
|
||||||
|
|||||||
@@ -271,16 +271,18 @@ func (sm *SnapshotManager) CompleteSnapshot(
|
|||||||
return sm.MarkSnapshotComplete(ctx, snapshotID)
|
return sm.MarkSnapshotComplete(ctx, snapshotID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExportSnapshotMetadata exports snapshot metadata to S3
|
// ExportSnapshotMetadata exports snapshot metadata to the backup destination store
|
||||||
//
|
//
|
||||||
// This method executes the complete snapshot metadata export process:
|
// This method executes the complete snapshot metadata export process:
|
||||||
// 1. Creates a temporary directory for working files
|
// 1. Creates a temporary directory for working files
|
||||||
// 2. Copies the main database to preserve its state
|
// 2. Copies the main database to preserve its state
|
||||||
// 3. Cleans the copy to contain only current snapshot data
|
// 3. Cleans the copy to contain only current snapshot data
|
||||||
// 4. Dumps the cleaned database to SQL
|
// 4. Vacuums the copy to remove the deleted data
|
||||||
// 5. Compresses the SQL dump with zstd
|
// 5. Compresses the copy with zstd
|
||||||
// 6. Encrypts the compressed data with age (always; recipients are required)
|
// 6. Encrypts the compressed copy with age (always; recipients are required)
|
||||||
// 7. Uploads to S3 at: snapshots/{snapshot-id}.sql.zst[.age]
|
// 7. Builds the blob manifest from the copy, compressed but not encrypted
|
||||||
|
// 8. Uploads the database to metadata/{remote-key}/db.zst.age
|
||||||
|
// 9. Uploads the manifest to metadata/{remote-key}/manifest.json.zst
|
||||||
//
|
//
|
||||||
// The only caller (finalizeSnapshotMetadata) does not close the main database
|
// The only caller (finalizeSnapshotMetadata) does not close the main database
|
||||||
// before calling this method: the index is copied at dbPath while it is still
|
// before calling this method: the index is copied at dbPath while it is still
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ func (v *Vaultik) RemoteInfo(jsonOutput bool) error {
|
|||||||
v.stdoutf("Type: %s\n", storageInfo.Type)
|
v.stdoutf("Type: %s\n", storageInfo.Type)
|
||||||
v.stdoutf("Location: %s\n", storageInfo.Location)
|
v.stdoutf("Location: %s\n", storageInfo.Location)
|
||||||
v.printlnStdout()
|
v.printlnStdout()
|
||||||
v.stdoutf("Scanning snapshot metadata...\n")
|
v.UI.Beginf("Scanning snapshot metadata...")
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshotMetadata, snapshotIDs, skippedManifestCount, err := v.collectSnapshotMetadata()
|
snapshotMetadata, snapshotIDs, skippedManifestCount, err := v.collectSnapshotMetadata()
|
||||||
@@ -265,7 +265,7 @@ func (v *Vaultik) RemoteInfo(jsonOutput bool) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
v.stdoutf("Downloading %d manifest(s)...\n", manifestCount)
|
v.UI.Beginf("Downloading %d manifest(s)...", manifestCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
referencedBlobs, unreadableManifests := v.collectReferencedBlobsFromManifests(
|
referencedBlobs, unreadableManifests := v.collectReferencedBlobsFromManifests(
|
||||||
@@ -447,7 +447,7 @@ func (v *Vaultik) scanRemoteBlobStorage(
|
|||||||
result *RemoteInfoResult, referencedBlobs map[string]int64, showText bool,
|
result *RemoteInfoResult, referencedBlobs map[string]int64, showText bool,
|
||||||
) error {
|
) error {
|
||||||
if showText {
|
if showText {
|
||||||
v.stdoutf("Scanning blobs...\n")
|
v.UI.Beginf("Scanning blobs...")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use prune's listing, so a file under blobs/ that prune would never
|
// Use prune's listing, so a file under blobs/ that prune would never
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"sneak.berlin/go/vaultik/internal/log"
|
"sneak.berlin/go/vaultik/internal/log"
|
||||||
"sneak.berlin/go/vaultik/internal/snapshot"
|
"sneak.berlin/go/vaultik/internal/snapshot"
|
||||||
|
"sneak.berlin/go/vaultik/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
// testBlobHashB is a blob that the manifest written by addRemote does
|
// testBlobHashB is a blob that the manifest written by addRemote does
|
||||||
@@ -259,3 +260,24 @@ func TestRemoteInfo_DirectoryWithoutManifestLeavesOrphansKnown(t *testing.T) {
|
|||||||
assert.NotContains(t, doc, "unreadable_manifests")
|
assert.NotContains(t, doc, "unreadable_manifests")
|
||||||
assert.NotContains(t, doc, "skipped_manifest_count")
|
assert.NotContains(t, doc, "skipped_manifest_count")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestRemoteInfo_StatusLineUsesUIMarker checks that the remote info
|
||||||
|
// status lines go through internal/ui: marker-prefixed, and silenced by
|
||||||
|
// --quiet.
|
||||||
|
func TestRemoteInfo_StatusLineUsesUIMarker(t *testing.T) {
|
||||||
|
log.Initialize(log.Config{})
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newListEnv(t)
|
||||||
|
env.addRemote(t, listRemoteID, time.Date(2026, 3, 2, 0, 0, 0, 0, time.UTC))
|
||||||
|
|
||||||
|
const statusLine = ui.Marker + " Scanning snapshot metadata...\n"
|
||||||
|
|
||||||
|
require.NoError(t, env.v.RemoteInfo(false))
|
||||||
|
assert.Contains(t, env.stdout.String(), statusLine)
|
||||||
|
|
||||||
|
env.stdout.Reset()
|
||||||
|
env.v.UI.SetQuiet(true)
|
||||||
|
require.NoError(t, env.v.RemoteInfo(false))
|
||||||
|
assert.NotContains(t, env.stdout.String(), "Scanning snapshot metadata")
|
||||||
|
}
|
||||||
@@ -1019,7 +1019,7 @@ func (v *Vaultik) VerifySnapshotWithOptions(
|
|||||||
v.printlnStdout()
|
v.printlnStdout()
|
||||||
|
|
||||||
// Check each blob is present with the size the manifest records.
|
// Check each blob is present with the size the manifest records.
|
||||||
v.stdoutf("Checking blob presence and sizes...\n")
|
v.UI.Beginf("Checking blob presence and sizes...")
|
||||||
}
|
}
|
||||||
|
|
||||||
v.checkSnapshotDatabase(remoteKey, result)
|
v.checkSnapshotDatabase(remoteKey, result)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ var errStorageBindingMismatch = errors.New(
|
|||||||
// nothing and return nil.
|
// nothing and return nil.
|
||||||
// - When the two differ, refuse with an error that tells the user
|
// - When the two differ, refuse with an error that tells the user
|
||||||
// how to recover (revert the config, or run `vaultik database
|
// how to recover (revert the config, or run `vaultik database
|
||||||
// purge` to discard the local index and rebuild against the new
|
// delete` to discard the local index and rebuild against the new
|
||||||
// destination on the next backup).
|
// destination on the next backup).
|
||||||
//
|
//
|
||||||
// Read-only inspection commands (remote info, snapshot list, etc.)
|
// Read-only inspection commands (remote info, snapshot list, etc.)
|
||||||
|
|||||||
+10
-10
@@ -190,7 +190,7 @@ func (v *Vaultik) loadVerificationData(
|
|||||||
log.Info("Downloading manifest", "remote_key", remoteKey)
|
log.Info("Downloading manifest", "remote_key", remoteKey)
|
||||||
|
|
||||||
if !opts.JSON && !v.UI.Quiet() {
|
if !opts.JSON && !v.UI.Quiet() {
|
||||||
v.stdoutf("Downloading manifest...\n")
|
v.UI.Beginf("Downloading manifest...")
|
||||||
}
|
}
|
||||||
|
|
||||||
manifest, err := v.downloadManifestByKey(remoteKey)
|
manifest, err := v.downloadManifestByKey(remoteKey)
|
||||||
@@ -205,9 +205,9 @@ func (v *Vaultik) loadVerificationData(
|
|||||||
"manifest_total_size", ubytes(manifest.TotalCompressedSize))
|
"manifest_total_size", ubytes(manifest.TotalCompressedSize))
|
||||||
|
|
||||||
if !opts.JSON && !v.UI.Quiet() {
|
if !opts.JSON && !v.UI.Quiet() {
|
||||||
v.stdoutf("Manifest loaded: %d blobs (%s)\n",
|
v.UI.Completef("Manifest loaded: %d blobs (%s)",
|
||||||
manifest.BlobCount, ubytes(manifest.TotalCompressedSize))
|
manifest.BlobCount, ubytes(manifest.TotalCompressedSize))
|
||||||
v.stdoutf("Downloading and decrypting database...\n")
|
v.UI.Beginf("Downloading and decrypting database...")
|
||||||
}
|
}
|
||||||
|
|
||||||
tdb, err := v.downloadVerifiedSnapshotDB(
|
tdb, err := v.downloadVerifiedSnapshotDB(
|
||||||
@@ -235,7 +235,7 @@ func (v *Vaultik) loadVerificationData(
|
|||||||
"db_total_size", ubytes(dbTotalSize))
|
"db_total_size", ubytes(dbTotalSize))
|
||||||
|
|
||||||
if !opts.JSON && !v.UI.Quiet() {
|
if !opts.JSON && !v.UI.Quiet() {
|
||||||
v.stdoutf("Database loaded: %d blobs (%s)\n",
|
v.UI.Completef("Database loaded: %d blobs (%s)",
|
||||||
len(dbBlobs), ubytes(dbTotalSize))
|
len(dbBlobs), ubytes(dbTotalSize))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,7 +295,7 @@ func (v *Vaultik) runVerificationSteps(
|
|||||||
identities []age.Identity,
|
identities []age.Identity,
|
||||||
) error {
|
) error {
|
||||||
if !opts.JSON && !v.UI.Quiet() {
|
if !opts.JSON && !v.UI.Quiet() {
|
||||||
v.stdoutf("Verifying manifest against database...\n")
|
v.UI.Beginf("Verifying manifest against database...")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := v.verifyManifestAgainstDatabase(manifest, dbBlobs)
|
err := v.verifyManifestAgainstDatabase(manifest, dbBlobs)
|
||||||
@@ -304,8 +304,8 @@ func (v *Vaultik) runVerificationSteps(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !opts.JSON && !v.UI.Quiet() {
|
if !opts.JSON && !v.UI.Quiet() {
|
||||||
v.stdoutf("Manifest verified.\n")
|
v.UI.Completef("Manifest verified.")
|
||||||
v.stdoutf("Checking blob existence in remote storage...\n")
|
v.UI.Beginf("Checking blob existence in remote storage...")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = v.verifyBlobExistenceFromDB(dbBlobs, result)
|
err = v.verifyBlobExistenceFromDB(dbBlobs, result)
|
||||||
@@ -314,8 +314,8 @@ func (v *Vaultik) runVerificationSteps(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !opts.JSON && !v.UI.Quiet() {
|
if !opts.JSON && !v.UI.Quiet() {
|
||||||
v.stdoutf("All blobs exist.\n")
|
v.UI.Completef("All blobs exist.")
|
||||||
v.stdoutf("Downloading and verifying blob contents (%d blobs, %s)...\n",
|
v.UI.Beginf("Downloading and verifying blob contents (%d blobs, %s)...",
|
||||||
len(dbBlobs), ubytes(totalSize))
|
len(dbBlobs), ubytes(totalSize))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -790,7 +790,7 @@ func (v *Vaultik) performDeepVerificationFromDB(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if !opts.JSON && !v.UI.Quiet() {
|
if !opts.JSON && !v.UI.Quiet() {
|
||||||
v.stdoutf(" Verified %d/%d blobs (%d remaining) - %s/%s - elapsed %s, eta %s\n",
|
v.UI.Progressf("Verified %d/%d blobs (%d remaining) - %s/%s - elapsed %s, eta %s",
|
||||||
i+1, len(blobs), remaining,
|
i+1, len(blobs), remaining,
|
||||||
ubytes(bytesProcessed),
|
ubytes(bytesProcessed),
|
||||||
ubytes(totalBytesExpected),
|
ubytes(totalBytesExpected),
|
||||||
|
|||||||
@@ -100,3 +100,51 @@ func TestVerify_QuietSuppressesReport(t *testing.T) {
|
|||||||
require.Empty(t, stdout.String(),
|
require.Empty(t, stdout.String(),
|
||||||
"a failed verify must write no report under --quiet")
|
"a failed verify must write no report under --quiet")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestVerify_DeepStatusLineUsesUIMarker checks that the deep verify
|
||||||
|
// status lines go through internal/ui: marker-prefixed, and silenced by
|
||||||
|
// --quiet.
|
||||||
|
func TestVerify_DeepStatusLineUsesUIMarker(t *testing.T) {
|
||||||
|
log.Initialize(log.Config{})
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
fs := afero.NewOsFs()
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
dataDir := filepath.Join(tempDir, "source")
|
||||||
|
storeDir := t.TempDir()
|
||||||
|
dbPath := filepath.Join(tempDir, "index.sqlite")
|
||||||
|
|
||||||
|
require.NoError(t, fs.MkdirAll(dataDir, 0o755))
|
||||||
|
require.NoError(t, afero.WriteFile(fs,
|
||||||
|
filepath.Join(dataDir, "data.bin"),
|
||||||
|
bytesPattern("marker-", 64*1024), 0o644))
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
cfg, storer, snapshotID := runFileStorageBackup(
|
||||||
|
ctx, t, fs, dataDir, storeDir, dbPath, 32*1024, 128*1024)
|
||||||
|
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
|
||||||
|
v := &vaultik.Vaultik{
|
||||||
|
Config: cfg,
|
||||||
|
Storage: storer,
|
||||||
|
Fs: fs,
|
||||||
|
Stdout: &stdout,
|
||||||
|
Stderr: io.Discard,
|
||||||
|
UI: ui.NewWithColor(&stdout, false),
|
||||||
|
}
|
||||||
|
v.SetContext(ctx)
|
||||||
|
|
||||||
|
opts := &vaultik.VerifyOptions{Deep: true}
|
||||||
|
|
||||||
|
require.NoError(t, v.VerifySnapshotWithOptions(snapshotID, opts))
|
||||||
|
require.Contains(t, stdout.String(),
|
||||||
|
"\n"+ui.Marker+" Downloading manifest...\n")
|
||||||
|
|
||||||
|
stdout.Reset()
|
||||||
|
v.UI.SetQuiet(true)
|
||||||
|
require.NoError(t, v.VerifySnapshotWithOptions(snapshotID, opts))
|
||||||
|
require.NotContains(t, stdout.String(), "Downloading manifest")
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"prettier": "3.8.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
+80
-1
@@ -3,11 +3,20 @@
|
|||||||
# this repo. Idempotent: every install is guarded by a check so already
|
# this repo. Idempotent: every install is guarded by a check so already
|
||||||
# installed tools are skipped. Base tooling comes from nix, apt, brew,
|
# installed tools are skipped. Base tooling comes from nix, apt, brew,
|
||||||
# or apk (detected in that order); assumes NOTHING is present (not git,
|
# or apk (detected in that order); assumes NOTHING is present (not git,
|
||||||
# make, or go).
|
# make, or go). Node is used directly if installed; otherwise it is
|
||||||
|
# installed at a pinned version via nvm (installing nvm itself first,
|
||||||
|
# from a hash-verified release archive, never curl | sh).
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
|
# Pinned versions, 2026-07-06
|
||||||
|
NODE_VERSION="22.17.0"
|
||||||
|
NVM_VERSION="0.40.3"
|
||||||
|
# sha256 of https://github.com/nvm-sh/nvm/archive/refs/tags/v0.40.3.tar.gz
|
||||||
|
NVM_SHA256="5f4d6aaa04a177dc93c985e31dbc411ab6b8c6e1e21d8015dbc1372625fcd1d0"
|
||||||
|
YARN_VERSION="1.22.22"
|
||||||
|
|
||||||
PKGMGR=""
|
PKGMGR=""
|
||||||
SUDO=""
|
SUDO=""
|
||||||
|
|
||||||
@@ -48,6 +57,69 @@ missing() {
|
|||||||
! command -v "$1" >/dev/null 2>&1
|
! command -v "$1" >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# verify_sha256 <file> <expected-hash>
|
||||||
|
verify_sha256() {
|
||||||
|
if command -v sha256sum >/dev/null 2>&1; then
|
||||||
|
actual="$(sha256sum "$1" | cut -d' ' -f1)"
|
||||||
|
else
|
||||||
|
actual="$(shasum -a 256 "$1" | cut -d' ' -f1)"
|
||||||
|
fi
|
||||||
|
if [ "$actual" != "$2" ]; then
|
||||||
|
echo "bootstrap: sha256 mismatch for $1" >&2
|
||||||
|
echo " expected: $2" >&2
|
||||||
|
echo " actual: $actual" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# nvm is a bash script; run a command in a bash with nvm loaded
|
||||||
|
nvm_sh() {
|
||||||
|
bash -c ". \"\$HOME/.nvm/nvm.sh\" && $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_nvm() {
|
||||||
|
[ -s "$HOME/.nvm/nvm.sh" ] && return 0
|
||||||
|
# nvm prerequisites; nvm itself requires bash
|
||||||
|
if missing bash; then pkg_install bash bash bash bash; fi
|
||||||
|
if missing curl; then pkg_install curl curl curl curl; fi
|
||||||
|
if missing git; then pkg_install git git git git; fi
|
||||||
|
tmp="$(mktemp -d)"
|
||||||
|
curl -fsSL -o "$tmp/nvm.tar.gz" \
|
||||||
|
"https://github.com/nvm-sh/nvm/archive/refs/tags/v${NVM_VERSION}.tar.gz"
|
||||||
|
verify_sha256 "$tmp/nvm.tar.gz" "$NVM_SHA256"
|
||||||
|
mkdir -p "$HOME/.nvm"
|
||||||
|
tar -xzf "$tmp/nvm.tar.gz" -C "$HOME/.nvm" --strip-components=1
|
||||||
|
rm -rf "$tmp"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_node() {
|
||||||
|
if ! missing node; then return 0; fi
|
||||||
|
ensure_nvm
|
||||||
|
nvm_sh "nvm install $NODE_VERSION"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_yarn() {
|
||||||
|
if ! missing yarn; then return 0; fi
|
||||||
|
if ! missing corepack; then
|
||||||
|
corepack enable
|
||||||
|
corepack prepare "yarn@$YARN_VERSION" --activate
|
||||||
|
elif [ -s "$HOME/.nvm/nvm.sh" ]; then
|
||||||
|
nvm_sh "nvm use $NODE_VERSION >/dev/null && corepack enable && \
|
||||||
|
corepack prepare yarn@$YARN_VERSION --activate"
|
||||||
|
else
|
||||||
|
npm install -g "yarn@$YARN_VERSION"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_js_deps() {
|
||||||
|
if missing yarn && [ -s "$HOME/.nvm/nvm.sh" ]; then
|
||||||
|
nvm_sh "nvm use $NODE_VERSION >/dev/null && cd \"$ROOT\" && \
|
||||||
|
yarn install --frozen-lockfile"
|
||||||
|
else
|
||||||
|
yarn install --frozen-lockfile
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# Docker is a hard requirement, not a nice-to-have: script/lint and
|
# Docker is a hard requirement, not a nice-to-have: script/lint and
|
||||||
# script/test build the lint and test phases of the Dockerfile, the only
|
# script/test build the lint and test phases of the Dockerfile, the only
|
||||||
# place the linter and the tests run, and script/check and
|
# place the linter and the tests run, and script/check and
|
||||||
@@ -103,6 +175,8 @@ main() {
|
|||||||
# Base tooling (every repo)
|
# Base tooling (every repo)
|
||||||
if missing git; then pkg_install git git git git; fi
|
if missing git; then pkg_install git git git git; fi
|
||||||
if missing make; then pkg_install gnumake make make make; fi
|
if missing make; then pkg_install gnumake make make make; fi
|
||||||
|
# script/install-go, script/install-goreleaser and nvm download with it.
|
||||||
|
if missing curl; then pkg_install curl curl curl curl; fi
|
||||||
|
|
||||||
# Go toolchain: the host's own, or else the version go.mod names,
|
# Go toolchain: the host's own, or else the version go.mod names,
|
||||||
# hash-verified, in .tool/go. That directory is not on the caller's
|
# hash-verified, in .tool/go. That directory is not on the caller's
|
||||||
@@ -131,6 +205,11 @@ main() {
|
|||||||
|
|
||||||
go mod download
|
go mod download
|
||||||
|
|
||||||
|
# prettier, which script/fmt and script/fmt-check run on Markdown.
|
||||||
|
ensure_node
|
||||||
|
ensure_yarn
|
||||||
|
install_js_deps
|
||||||
|
|
||||||
# Last, so that everything installable is installed before the one
|
# Last, so that everything installable is installed before the one
|
||||||
# thing this script cannot install decides the outcome.
|
# thing this script cannot install decides the outcome.
|
||||||
require_docker
|
require_docker
|
||||||
|
|||||||
+20
@@ -4,11 +4,31 @@ set -eu
|
|||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
|
# Must match the pin in script/bootstrap.
|
||||||
|
NODE_VERSION="22.17.0"
|
||||||
|
|
||||||
|
# script/bootstrap installs node and yarn under nvm and leaves neither
|
||||||
|
# on the PATH of the shell that called it, so resolve the pinned
|
||||||
|
# toolchain here the way bootstrap's own install step does. nvm is a
|
||||||
|
# bash script, hence the subshell.
|
||||||
|
run_yarn() {
|
||||||
|
if command -v yarn >/dev/null 2>&1; then
|
||||||
|
exec yarn "$@"
|
||||||
|
fi
|
||||||
|
if [ ! -s "$HOME/.nvm/nvm.sh" ]; then
|
||||||
|
echo "fmt: no yarn; run script/bootstrap first" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
exec bash -c '. "$HOME/.nvm/nvm.sh" && nvm use "$1" >/dev/null &&
|
||||||
|
shift && exec yarn "$@"' bash "$NODE_VERSION" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
# Where script/bootstrap installs Go when the host has none.
|
# Where script/bootstrap installs Go when the host has none.
|
||||||
PATH="$PATH:$ROOT/.tool/go/bin"
|
PATH="$PATH:$ROOT/.tool/go/bin"
|
||||||
go fmt ./...
|
go fmt ./...
|
||||||
|
run_yarn run prettier --write '**/*.md' --tab-width 4 --prose-wrap always
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
+21
-1
@@ -3,11 +3,30 @@
|
|||||||
# writing. It checks every Go file outside .tool, which is more than
|
# writing. It checks every Go file outside .tool, which is more than
|
||||||
# script/fmt formats: `go fmt ./...` skips `testdata` directories and
|
# script/fmt formats: `go fmt ./...` skips `testdata` directories and
|
||||||
# files and directories whose names start with `.` or `_`. Fix a file
|
# files and directories whose names start with `.` or `_`. Fix a file
|
||||||
# only this reports with `gofmt -w`.
|
# only this reports with `gofmt -w`. Markdown is checked with prettier.
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
|
# Must match the pin in script/bootstrap.
|
||||||
|
NODE_VERSION="22.17.0"
|
||||||
|
|
||||||
|
# script/bootstrap installs node and yarn under nvm and leaves neither
|
||||||
|
# on the PATH of the shell that called it, so resolve the pinned
|
||||||
|
# toolchain here the way bootstrap's own install step does. nvm is a
|
||||||
|
# bash script, hence the subshell.
|
||||||
|
run_yarn() {
|
||||||
|
if command -v yarn >/dev/null 2>&1; then
|
||||||
|
exec yarn "$@"
|
||||||
|
fi
|
||||||
|
if [ ! -s "$HOME/.nvm/nvm.sh" ]; then
|
||||||
|
echo "fmt-check: no yarn; run script/bootstrap first" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
exec bash -c '. "$HOME/.nvm/nvm.sh" && nvm use "$1" >/dev/null &&
|
||||||
|
shift && exec yarn "$@"' bash "$NODE_VERSION" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
# Where script/bootstrap installs Go when the host has none.
|
# Where script/bootstrap installs Go when the host has none.
|
||||||
@@ -23,6 +42,7 @@ main() {
|
|||||||
echo "$unformatted" >&2
|
echo "$unformatted" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
run_yarn run prettier --check '**/*.md' --tab-width 4 --prose-wrap always
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
prettier@3.8.1:
|
||||||
|
version "3.8.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.1.tgz#edf48977cf991558f4fcbd8a3ba6015ba2a3a173"
|
||||||
|
integrity sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==
|
||||||
Reference in new issue
Block a user