Add manifest creation command
This commit is contained in:
28
docs/cli.md
28
docs/cli.md
@@ -16,12 +16,14 @@ distributor version [--format text|json]
|
||||
distributor run [--config <path>] [--dry-run] [--force] [--format text|json]
|
||||
distributor validate [--format text|json] <path>
|
||||
distributor inspect [--format text|json] <path>
|
||||
distributor manifest create <bundle-path> --id <bundle-id> [options]
|
||||
```
|
||||
|
||||
- `version`: prints the application name and version. Development builds print `distributor dev`.
|
||||
- `run`: loads a YAML config, discovers source bundles, plans each configured destination, writes selected outputs unless `--dry-run` is set, and prints a final status summary.
|
||||
- `validate`: validates a local source bundle directory or a local tree containing source bundles.
|
||||
- `inspect`: validates local source bundles and prints normalized bundle metadata.
|
||||
- `manifest create`: creates `manifest.json` for a local source bundle directory.
|
||||
|
||||
`validate` and `inspect` accept local paths only. `run` executes `local`, `ssh`, and `s3` backends.
|
||||
|
||||
@@ -45,6 +47,13 @@ Output-producing subcommands:
|
||||
- `--dry-run`: load config, discover bundles, inspect destination state, print planned actions and final status, and do not write output files, destination state, or SSH `known_hosts` entries.
|
||||
- `--force`: allow explicit destructive replacement for supported conflict cases in this run only.
|
||||
|
||||
`manifest create` flags:
|
||||
|
||||
- `--id <bundle-id>`: source bundle id. Required.
|
||||
- `--file <path>`: bundle-relative file to include. Repeatable. If omitted, files are scanned recursively.
|
||||
- `--created <time>`: RFC3339 source created timestamp. If omitted, the current UTC time is used.
|
||||
- `--overwrite`: replace an existing `manifest.json`.
|
||||
|
||||
`run` does not accept positional arguments. `validate` and `inspect` accept at most one path; omitting the path returns a required-path error.
|
||||
|
||||
## Common workflows
|
||||
@@ -61,6 +70,22 @@ Inspect a source bundle:
|
||||
go run ./cmd/distributor inspect examples/source-bundle
|
||||
```
|
||||
|
||||
Create a manifest for a local producer bundle:
|
||||
|
||||
```sh
|
||||
go run ./cmd/distributor manifest create <bundle-path> --id <bundle-id>
|
||||
```
|
||||
|
||||
Create a manifest with explicit file order:
|
||||
|
||||
```sh
|
||||
go run ./cmd/distributor manifest create <bundle-path> \
|
||||
--id <bundle-id> \
|
||||
--created 2026-06-01T11:00:00Z \
|
||||
--file report.md \
|
||||
--file summary.txt
|
||||
```
|
||||
|
||||
Preview local publication without writing:
|
||||
|
||||
```sh
|
||||
@@ -129,10 +154,11 @@ Command-specific JSON results:
|
||||
- `version`: application name and version.
|
||||
- `validate`: bundle count and discovered bundle identifiers.
|
||||
- `inspect`: bundle path, id, created timestamp, digest, file count, total size, and manifest file records.
|
||||
- `manifest create`: manifest path, bundle root, id, created timestamp, digest, file count, and file records.
|
||||
- `run`: dry-run status, pipeline summaries, destination action records, output records, final counters, warnings, and partial failure records.
|
||||
|
||||
## Diagnostics
|
||||
|
||||
Use `validate` before publication when a producer has written a new bundle. Use `inspect` to confirm normalized ids, timestamps, digests, file paths, and file sizes.
|
||||
Use `manifest create` when a local producer has written bundle files but not `manifest.json`. Use `validate` before publication when a producer has written a new bundle. Use `inspect` to confirm normalized ids, timestamps, digests, file paths, and file sizes.
|
||||
|
||||
For symptom-oriented recovery steps, see [troubleshooting](troubleshooting.md). For destination state and retry behavior, see [operations](operations.md). For config fields and defaults, see [configuration](config.md).
|
||||
|
||||
@@ -100,6 +100,15 @@ if err != nil {
|
||||
|
||||
Use `BuildManifest` and `WriteManifest` when a producer already wrote all bundle files into the final root. `BuildManifest` can preserve an explicit file order, or `Scan: true` can recursively include regular files under `Root` in deterministic slash-path order. Scan mode includes dotfiles, excludes files named `manifest.json` or `.distributor.json`, and rejects symlinks.
|
||||
|
||||
Shell producers can create the same manifest through the CLI after writing bundle files:
|
||||
|
||||
```sh
|
||||
go run ./cmd/distributor manifest create <bundle-path> --id reports.example.2026-05-30
|
||||
go run ./cmd/distributor validate <bundle-path>
|
||||
```
|
||||
|
||||
Use repeated `--file` flags to preserve a specific file order. If no `--file` flags are provided, the command scans the bundle directory recursively using the same filtering rules as `pkg/bundle.BuildManifest`.
|
||||
|
||||
## Dry Runs
|
||||
|
||||
`--dry-run` loads and validates config, discovers source bundles, inspects destination state, plans outputs, and prints summary lines. It does not write output files, destination state, or SSH `known_hosts` entries.
|
||||
|
||||
128
internal/app/manifest.go
Normal file
128
internal/app/manifest.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
producerbundle "gitea.maximumdirect.net/eric/distributor/pkg/bundle"
|
||||
)
|
||||
|
||||
type ManifestCreateOptions struct {
|
||||
Root string
|
||||
ID string
|
||||
Created string
|
||||
Files []string
|
||||
Overwrite bool
|
||||
Stdout io.Writer
|
||||
OutputFormat OutputFormat
|
||||
}
|
||||
|
||||
func ManifestCreate(ctx context.Context, options ManifestCreateOptions) error {
|
||||
if err := ValidateOutputFormat(options.OutputFormat); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if options.Root == "" {
|
||||
return fmt.Errorf("manifest create command requires a bundle path")
|
||||
}
|
||||
if options.ID == "" {
|
||||
return fmt.Errorf("manifest create command requires --id")
|
||||
}
|
||||
|
||||
created, err := parseOptionalCreated(options.Created)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
files := normalizeManifestFiles(options.Files)
|
||||
buildOptions := producerbundle.BuildOptions{
|
||||
Root: options.Root,
|
||||
ID: options.ID,
|
||||
Created: created,
|
||||
Files: files,
|
||||
Scan: len(files) == 0,
|
||||
}
|
||||
manifest, err := producerbundle.BuildManifest(buildOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := producerbundle.WriteManifest(options.Root, manifest, producerbundle.WriteManifestOptions{Overwrite: options.Overwrite}); err != nil {
|
||||
return err
|
||||
}
|
||||
loaded, err := producerbundle.LoadManifest(options.Root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := producerbundle.ValidateBundle(options.Root, loaded); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result := manifestCreateResultFromManifest(options.Root, loaded)
|
||||
if IsJSONOutput(options.OutputFormat) {
|
||||
return WriteJSONEnvelope(options.Stdout, "manifest create", true, nil, result, nil)
|
||||
}
|
||||
if options.Stdout != nil {
|
||||
_, err = fmt.Fprintf(options.Stdout, "created %s\nbundle: %s\nfiles: %d\ndigest: %s\n", producerbundle.ManifestName, result.ID, result.FileCount, result.Digest)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func parseOptionalCreated(value string) (time.Time, error) {
|
||||
if value == "" {
|
||||
return time.Time{}, nil
|
||||
}
|
||||
created, err := time.Parse(time.RFC3339, value)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("created must be RFC3339: %w", err)
|
||||
}
|
||||
return created, nil
|
||||
}
|
||||
|
||||
func normalizeManifestFiles(files []string) []string {
|
||||
normalized := make([]string, 0, len(files))
|
||||
for _, file := range files {
|
||||
normalized = append(normalized, filepath.ToSlash(filepath.Clean(strings.ReplaceAll(file, "\\", string(filepath.Separator)))))
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
type manifestCreateResult struct {
|
||||
ManifestPath string `json:"manifest_path"`
|
||||
Root string `json:"root"`
|
||||
ID string `json:"id"`
|
||||
Created string `json:"created"`
|
||||
Digest string `json:"digest"`
|
||||
FileCount int `json:"file_count"`
|
||||
Files []manifestCreateFileResult `json:"files"`
|
||||
}
|
||||
|
||||
type manifestCreateFileResult struct {
|
||||
Path string `json:"path"`
|
||||
SHA256 string `json:"sha256"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
func manifestCreateResultFromManifest(root string, manifest producerbundle.Manifest) manifestCreateResult {
|
||||
result := manifestCreateResult{
|
||||
ManifestPath: filepath.ToSlash(filepath.Join(root, producerbundle.ManifestName)),
|
||||
Root: filepath.ToSlash(root),
|
||||
ID: manifest.ID,
|
||||
Created: manifest.Created.Format(time.RFC3339),
|
||||
Digest: manifest.Digest,
|
||||
FileCount: len(manifest.Files),
|
||||
Files: make([]manifestCreateFileResult, 0, len(manifest.Files)),
|
||||
}
|
||||
for _, file := range manifest.Files {
|
||||
result.Files = append(result.Files, manifestCreateFileResult{
|
||||
Path: file.Path,
|
||||
SHA256: file.SHA256,
|
||||
Size: file.Size,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
139
internal/cli/manifest.go
Normal file
139
internal/cli/manifest.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/distributor/internal/app"
|
||||
)
|
||||
|
||||
func manifestCommand(ctx context.Context, args []string, stdout, stderr io.Writer) int {
|
||||
if len(args) == 0 || args[0] == "-h" || args[0] == "--help" || args[0] == "help" {
|
||||
printManifestHelp(stdout)
|
||||
return exitOK
|
||||
}
|
||||
switch args[0] {
|
||||
case "create":
|
||||
return manifestCreateCommand(ctx, args[1:], stdout, stderr)
|
||||
default:
|
||||
fmt.Fprintf(stderr, "%s: manifest unknown command %q\n\n", app.Name, args[0])
|
||||
printManifestHelp(stderr)
|
||||
return exitUsage
|
||||
}
|
||||
}
|
||||
|
||||
func manifestCreateCommand(ctx context.Context, args []string, stdout, stderr io.Writer) int {
|
||||
if hasHelp(args) {
|
||||
printManifestCreateHelp(stdout)
|
||||
return exitOK
|
||||
}
|
||||
flags := flag.NewFlagSet("manifest create", flag.ContinueOnError)
|
||||
flags.SetOutput(stderr)
|
||||
id := flags.String("id", "", "source bundle id")
|
||||
created := flags.String("created", "", "source created timestamp")
|
||||
overwrite := flags.Bool("overwrite", false, "replace an existing manifest.json")
|
||||
formatFlag := addFormatFlag(flags)
|
||||
var files repeatedFlag
|
||||
flags.Var(&files, "file", "bundle-relative file to include")
|
||||
flagArgs, positionalArgs, ok := splitManifestCreateArgs(stderr, args)
|
||||
if !ok {
|
||||
return exitUsage
|
||||
}
|
||||
if err := flags.Parse(flagArgs); err != nil {
|
||||
return exitUsage
|
||||
}
|
||||
if len(positionalArgs) != 1 {
|
||||
fmt.Fprintf(stderr, "%s: manifest create requires exactly one bundle path\n", app.Name)
|
||||
return exitUsage
|
||||
}
|
||||
format, ok := parseOutputFormat(stderr, "manifest create", *formatFlag)
|
||||
if !ok {
|
||||
return exitUsage
|
||||
}
|
||||
err := app.ManifestCreate(ctx, app.ManifestCreateOptions{
|
||||
Root: positionalArgs[0],
|
||||
ID: *id,
|
||||
Created: *created,
|
||||
Files: []string(files),
|
||||
Overwrite: *overwrite,
|
||||
Stdout: stdout,
|
||||
OutputFormat: format,
|
||||
})
|
||||
if err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
return exitOK
|
||||
}
|
||||
|
||||
func splitManifestCreateArgs(stderr io.Writer, args []string) ([]string, []string, bool) {
|
||||
var flagArgs []string
|
||||
var positionalArgs []string
|
||||
for index := 0; index < len(args); index++ {
|
||||
arg := args[index]
|
||||
switch arg {
|
||||
case "--overwrite":
|
||||
flagArgs = append(flagArgs, arg)
|
||||
case "--id", "--created", "--file", "--format":
|
||||
if index+1 >= len(args) {
|
||||
fmt.Fprintf(stderr, "%s: manifest create %s requires a value\n", app.Name, arg)
|
||||
return nil, nil, false
|
||||
}
|
||||
flagArgs = append(flagArgs, arg, args[index+1])
|
||||
index++
|
||||
default:
|
||||
if strings.HasPrefix(arg, "--id=") ||
|
||||
strings.HasPrefix(arg, "--created=") ||
|
||||
strings.HasPrefix(arg, "--file=") ||
|
||||
strings.HasPrefix(arg, "--format=") {
|
||||
flagArgs = append(flagArgs, arg)
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(arg, "-") {
|
||||
flagArgs = append(flagArgs, arg)
|
||||
continue
|
||||
}
|
||||
positionalArgs = append(positionalArgs, arg)
|
||||
}
|
||||
}
|
||||
return flagArgs, positionalArgs, true
|
||||
}
|
||||
|
||||
type repeatedFlag []string
|
||||
|
||||
func (f *repeatedFlag) String() string {
|
||||
return fmt.Sprint([]string(*f))
|
||||
}
|
||||
|
||||
func (f *repeatedFlag) Set(value string) error {
|
||||
*f = append(*f, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
func printManifestHelp(w io.Writer) {
|
||||
fmt.Fprint(w, `Usage:
|
||||
distributor manifest <command> [options]
|
||||
|
||||
Commands:
|
||||
create Create a source bundle manifest
|
||||
|
||||
Use "distributor manifest <command> --help" for command-specific help.
|
||||
`)
|
||||
}
|
||||
|
||||
func printManifestCreateHelp(w io.Writer) {
|
||||
fmt.Fprint(w, `Usage:
|
||||
distributor manifest create <bundle-path> --id <bundle-id> [options]
|
||||
|
||||
Options:
|
||||
--id <bundle-id> Source bundle id
|
||||
--file <path> Bundle-relative file to include; repeatable
|
||||
--created <time> RFC3339 source created timestamp
|
||||
--overwrite Replace an existing manifest.json
|
||||
--format text|json Output format
|
||||
|
||||
Create manifest.json for a local source bundle directory.
|
||||
`)
|
||||
}
|
||||
@@ -33,6 +33,8 @@ func Execute(ctx context.Context, args []string, stdout, stderr io.Writer) int {
|
||||
return validateCommand(ctx, args[1:], stdout, stderr)
|
||||
case "inspect":
|
||||
return inspectCommand(ctx, args[1:], stdout, stderr)
|
||||
case "manifest":
|
||||
return manifestCommand(ctx, args[1:], stdout, stderr)
|
||||
default:
|
||||
fmt.Fprintf(stderr, "%s: unknown command %q\n\n", app.Name, args[0])
|
||||
printRootHelp(stderr)
|
||||
@@ -51,6 +53,7 @@ Commands:
|
||||
run Run configured distribution pipelines
|
||||
validate Validate a source bundle or bundle tree
|
||||
inspect Inspect bundles or distributor state
|
||||
manifest Create source bundle manifests
|
||||
|
||||
Use "%s <command> --help" for command-specific help.
|
||||
`, app.Name, app.Name, app.Name)
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"gitea.maximumdirect.net/eric/distributor/internal/storage"
|
||||
"gitea.maximumdirect.net/eric/distributor/internal/testutil"
|
||||
producerbundle "gitea.maximumdirect.net/eric/distributor/pkg/bundle"
|
||||
)
|
||||
|
||||
func decodeEnvelope(t *testing.T, stdout *bytes.Buffer) map[string]any {
|
||||
@@ -267,6 +268,206 @@ func TestExecuteInspectArgs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteManifestCreateExplicitFiles(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeCLIFile(t, root, "b.txt", "bravo")
|
||||
writeCLIFile(t, root, "nested/a.txt", "alpha")
|
||||
var stdout, stderr bytes.Buffer
|
||||
|
||||
code := Execute(context.Background(), []string{
|
||||
"manifest", "create", root,
|
||||
"--id", "reports.explicit",
|
||||
"--created", "2026-06-01T11:00:00Z",
|
||||
"--file", "b.txt",
|
||||
"--file", "nested/a.txt",
|
||||
}, &stdout, &stderr)
|
||||
|
||||
if code != exitOK {
|
||||
t.Fatalf("exit code = %d, want %d; stderr = %q", code, exitOK, stderr.String())
|
||||
}
|
||||
for _, want := range []string{
|
||||
"created manifest.json",
|
||||
"bundle: reports.explicit",
|
||||
"files: 2",
|
||||
"digest: sha256:",
|
||||
} {
|
||||
if !strings.Contains(stdout.String(), want) {
|
||||
t.Fatalf("stdout = %q, want substring %q", stdout.String(), want)
|
||||
}
|
||||
}
|
||||
manifest, err := producerbundle.LoadManifest(root)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadManifest() error = %v", err)
|
||||
}
|
||||
if got, want := manifestPaths(manifest), []string{"b.txt", "nested/a.txt"}; !equalStrings(got, want) {
|
||||
t.Fatalf("manifest paths = %v, want %v", got, want)
|
||||
}
|
||||
if err := producerbundle.ValidateBundle(root, manifest); err != nil {
|
||||
t.Fatalf("ValidateBundle() error = %v", err)
|
||||
}
|
||||
var validateStdout, validateStderr bytes.Buffer
|
||||
validateCode := Execute(context.Background(), []string{"validate", root}, &validateStdout, &validateStderr)
|
||||
if validateCode != exitOK {
|
||||
t.Fatalf("validate exit code = %d, want %d; stderr = %q", validateCode, exitOK, validateStderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteManifestCreateScansBundle(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeCLIFile(t, root, "z.txt", "zulu")
|
||||
writeCLIFile(t, root, ".env", "dotfile")
|
||||
writeCLIFile(t, root, "nested/report.md", "# Report\n")
|
||||
writeCLIFile(t, root, storage.StateFileName, "destination state")
|
||||
var stdout, stderr bytes.Buffer
|
||||
|
||||
code := Execute(context.Background(), []string{"manifest", "create", root, "--id", "reports.scan"}, &stdout, &stderr)
|
||||
|
||||
if code != exitOK {
|
||||
t.Fatalf("exit code = %d, want %d; stderr = %q", code, exitOK, stderr.String())
|
||||
}
|
||||
manifest, err := producerbundle.LoadManifest(root)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadManifest() error = %v", err)
|
||||
}
|
||||
if got, want := manifestPaths(manifest), []string{".env", "nested/report.md", "z.txt"}; !equalStrings(got, want) {
|
||||
t.Fatalf("manifest paths = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteManifestCreateJSON(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeCLIFile(t, root, "report.md", "# Report\n")
|
||||
var stdout, stderr bytes.Buffer
|
||||
|
||||
code := Execute(context.Background(), []string{"manifest", "create", root, "--id", "reports.json", "--file", "report.md", "--format", "json"}, &stdout, &stderr)
|
||||
|
||||
if code != exitOK {
|
||||
t.Fatalf("exit code = %d, want %d; stderr = %q", code, exitOK, stderr.String())
|
||||
}
|
||||
envelope := decodeEnvelope(t, &stdout)
|
||||
if envelope["command"] != "manifest create" || envelope["ok"] != true {
|
||||
t.Fatalf("envelope = %#v, want manifest create ok", envelope)
|
||||
}
|
||||
result := envelopeResult(t, envelope)
|
||||
if result["id"] != "reports.json" || result["file_count"] != float64(1) {
|
||||
t.Fatalf("result = %#v, want manifest summary", result)
|
||||
}
|
||||
if stderr.Len() != 0 {
|
||||
t.Fatalf("stderr = %q, want empty", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteManifestCreateOverwrite(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeCLIFile(t, root, "report.md", "old\n")
|
||||
var stdout, stderr bytes.Buffer
|
||||
code := Execute(context.Background(), []string{"manifest", "create", root, "--id", "reports.old", "--file", "report.md"}, &stdout, &stderr)
|
||||
if code != exitOK {
|
||||
t.Fatalf("initial exit code = %d, want %d; stderr = %q", code, exitOK, stderr.String())
|
||||
}
|
||||
writeCLIFile(t, root, "report.md", "new\n")
|
||||
|
||||
stdout.Reset()
|
||||
stderr.Reset()
|
||||
code = Execute(context.Background(), []string{"manifest", "create", root, "--id", "reports.new", "--file", "report.md"}, &stdout, &stderr)
|
||||
if code != exitError {
|
||||
t.Fatalf("overwrite exit code = %d, want %d", code, exitError)
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "write manifest") {
|
||||
t.Fatalf("stderr = %q, want write manifest error", stderr.String())
|
||||
}
|
||||
manifest, err := producerbundle.LoadManifest(root)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadManifest() error = %v", err)
|
||||
}
|
||||
if manifest.ID != "reports.old" {
|
||||
t.Fatalf("manifest id = %q, want reports.old", manifest.ID)
|
||||
}
|
||||
|
||||
stdout.Reset()
|
||||
stderr.Reset()
|
||||
code = Execute(context.Background(), []string{"manifest", "create", root, "--id", "reports.new", "--file", "report.md", "--overwrite"}, &stdout, &stderr)
|
||||
if code != exitOK {
|
||||
t.Fatalf("overwrite exit code = %d, want %d; stderr = %q", code, exitOK, stderr.String())
|
||||
}
|
||||
manifest, err = producerbundle.LoadManifest(root)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadManifest() error = %v", err)
|
||||
}
|
||||
if manifest.ID != "reports.new" {
|
||||
t.Fatalf("manifest id = %q, want reports.new", manifest.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteManifestCreateRejectsSymlink(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeCLIFile(t, root, "target.md", "# Report\n")
|
||||
if err := os.Symlink("target.md", filepath.Join(root, "link.md")); err != nil {
|
||||
t.Skipf("symlink unavailable: %v", err)
|
||||
}
|
||||
var stdout, stderr bytes.Buffer
|
||||
|
||||
code := Execute(context.Background(), []string{"manifest", "create", root, "--id", "reports.link", "--file", "link.md"}, &stdout, &stderr)
|
||||
|
||||
if code != exitError {
|
||||
t.Fatalf("exit code = %d, want %d", code, exitError)
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "regular file") {
|
||||
t.Fatalf("stderr = %q, want regular file error", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteManifestCreateArgs(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeCLIFile(t, root, "report.md", "# Report\n")
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
wantCode int
|
||||
wantStderr string
|
||||
}{
|
||||
{
|
||||
name: "missing path",
|
||||
args: []string{"manifest", "create", "--id", "reports.missing"},
|
||||
wantCode: exitUsage,
|
||||
wantStderr: "requires exactly one bundle path",
|
||||
},
|
||||
{
|
||||
name: "missing id",
|
||||
args: []string{"manifest", "create", root},
|
||||
wantCode: exitError,
|
||||
wantStderr: "requires --id",
|
||||
},
|
||||
{
|
||||
name: "bad created",
|
||||
args: []string{"manifest", "create", root, "--id", "reports.bad", "--created", "June 1"},
|
||||
wantCode: exitError,
|
||||
wantStderr: "created must be RFC3339",
|
||||
},
|
||||
{
|
||||
name: "bad format",
|
||||
args: []string{"manifest", "create", root, "--id", "reports.bad", "--format", "xml"},
|
||||
wantCode: exitUsage,
|
||||
wantStderr: "format must be text or json",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
code := Execute(context.Background(), tt.args, &stdout, &stderr)
|
||||
if code != tt.wantCode {
|
||||
t.Fatalf("exit code = %d, want %d; stderr = %q", code, tt.wantCode, stderr.String())
|
||||
}
|
||||
if stdout.Len() != 0 {
|
||||
t.Fatalf("stdout = %q, want empty", stdout.String())
|
||||
}
|
||||
if !strings.Contains(stderr.String(), tt.wantStderr) {
|
||||
t.Fatalf("stderr = %q, want substring %q", stderr.String(), tt.wantStderr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteRunDryRun(t *testing.T) {
|
||||
sourceRoot := t.TempDir()
|
||||
testutil.WriteSourceBundle(t, sourceRoot, "", testutil.BundleOptions{})
|
||||
@@ -512,3 +713,34 @@ func TestUnknownCommandIsUsageError(t *testing.T) {
|
||||
t.Fatalf("stderr = %q, want unknown command error", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func manifestPaths(manifest producerbundle.Manifest) []string {
|
||||
paths := make([]string, 0, len(manifest.Files))
|
||||
for _, file := range manifest.Files {
|
||||
paths = append(paths, file.Path)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
func equalStrings(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for index := range a {
|
||||
if a[index] != b[index] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func writeCLIFile(t *testing.T, root, relative, body string) {
|
||||
t.Helper()
|
||||
path := filepath.Join(root, filepath.FromSlash(relative))
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatalf("mkdir %s: %v", filepath.Dir(path), err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
||||
t.Fatalf("write %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user