Move the CLI to urfave/cli v3 (closes #110)
check / check (push) Failing after 2s

Ported per the library's v2-to-v3 migration guide: the app is a root
cli.Command, actions take a context and the command, and flag
environment variables become value sources. -v, -q and the version flag
are local so, as before, only the commands defining them accept them.
Every command stops reading flags at its first argument, as v2 did.
ErrWriter is stdout so usage errors print with their help. The action's
context reaches the manifest download in check, export and list.
testify rises to v1.12.1, which v3 requires; the urfave_cli_no_docs
build tag, which v3 lacks, is dropped. v3 accepts a flag given under two
names, so -v --verbose gives debug output, and the test pinning the
refusal becomes a TestVerboseCount case.

Model: opus-5-5
This commit is contained in:
2026-10-04 19:07:01 +00:00
parent ab72692439
commit 16a82ff176
15 changed files with 275 additions and 262 deletions
+17 -13
View File
@@ -20,7 +20,7 @@ import (
"github.com/dustin/go-humanize"
"github.com/multiformats/go-multihash"
"github.com/spf13/afero"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
"sneak.berlin/go/mfer/internal/log"
"sneak.berlin/go/mfer/mfer"
)
@@ -328,19 +328,21 @@ func alreadyPresent(dest, localPath string, entry *mfer.MFFilePath) bool {
return verifyDownloadedHash(h.Sum(nil), entry) == nil
}
func (mfa *CLIApp) fetchManifestOperation(ctx *cli.Context) error {
func (mfa *CLIApp) fetchManifestOperation(
ctx context.Context, cmd *cli.Command,
) error {
log.Debug("fetchManifestOperation()")
if ctx.Args().Len() == 0 {
if cmd.Args().Len() == 0 {
return errURLRequired
}
timeout := ctx.Duration(flagTimeout)
timeout := cmd.Duration(flagTimeout)
if timeout <= 0 {
return errInvalidTimeout
}
manifestURL, err := resolveManifestURL(ctx.Args().Get(0))
manifestURL, err := resolveManifestURL(cmd.Args().Get(0))
if err != nil {
return fmt.Errorf("invalid URL: %w", err)
}
@@ -350,7 +352,7 @@ func (mfa *CLIApp) fetchManifestOperation(ctx *cli.Context) error {
firstDelay: firstRetryDelay,
}
manifestData, files, err := fetchManifest(ctx, client, manifestURL)
manifestData, files, err := fetchManifest(ctx, cmd, client, manifestURL)
if err != nil {
return err
}
@@ -361,7 +363,7 @@ func (mfa *CLIApp) fetchManifestOperation(ctx *cli.Context) error {
return err
}
dest := ctx.String(flagDest)
dest := cmd.String(flagDest)
err = os.MkdirAll(dest, dirPerms)
if err != nil {
@@ -379,7 +381,7 @@ func (mfa *CLIApp) fetchManifestOperation(ctx *cli.Context) error {
// Download each file
downloaded, downloadedBytes, dlErr := downloadManifestFiles(
ctx.Context, client, baseURL, dest, files, progress)
ctx, client, baseURL, dest, files, progress)
close(progress)
<-done
@@ -416,7 +418,7 @@ func (mfa *CLIApp) fetchManifestOperation(ctx *cli.Context) error {
// downloaded, to be saved once the files are in place, and the files it
// lists.
func fetchManifest(
ctx *cli.Context, client retryingClient, manifestURL string,
ctx context.Context, cmd *cli.Command, client retryingClient, manifestURL string,
) ([]byte, []*mfer.MFFilePath, error) {
log.Infof("fetching manifest from %s", manifestURL)
@@ -425,7 +427,7 @@ func fetchManifest(
// manifest.
var manifestData []byte
err := client.get(ctx.Context, manifestURL, func(resp *http.Response) error {
err := client.get(ctx, manifestURL, func(resp *http.Response) error {
var readErr error
manifestData, readErr = io.ReadAll(resp.Body)
@@ -437,12 +439,13 @@ func fetchManifest(
}
// Parse manifest
//nolint:contextcheck // mfer loads a manifest without a context
manifest, err := mfer.NewManifestFromReader(bytes.NewReader(manifestData))
if err != nil {
return nil, nil, fmt.Errorf("failed to parse manifest: %w", err)
}
requiredSigner := ctx.String(flagRequireSignature)
requiredSigner := cmd.String(flagRequireSignature)
if requiredSigner != "" {
err = verifyFetchedSigner(ctx, manifestData, requiredSigner)
if err != nil {
@@ -504,7 +507,7 @@ func checkNoNameClash(files []*mfer.MFFilePath) error {
// its manifest from a file, so the manifest is handed to it as a file in
// memory.
func verifyFetchedSigner(
ctx *cli.Context, manifestData []byte, requiredSigner string,
ctx context.Context, manifestData []byte, requiredSigner string,
) error {
memFs := afero.NewMemMapFs()
manifestPath := "/" + defaultManifestName
@@ -514,6 +517,7 @@ func verifyFetchedSigner(
return err
}
//nolint:contextcheck // mfer loads a manifest without a context
chk, err := mfer.NewChecker(&mfer.CheckerOptions{
ManifestPath: manifestPath,
BasePath: "/",
@@ -523,7 +527,7 @@ func verifyFetchedSigner(
return fmt.Errorf("failed to load manifest: %w", err)
}
return verifyRequiredSigner(ctx.Context, chk, requiredSigner)
return verifyRequiredSigner(ctx, chk, requiredSigner)
}
// saveManifest writes the fetched manifest into dest under the default