Share command output projections
This commit is contained in:
@@ -204,6 +204,8 @@ Run helpers are grouped by responsibility:
|
||||
- `run_destination.go`: destination-scoped planning, execution, action
|
||||
recording, and failure bookkeeping.
|
||||
- `run_output.go`: `RunReport`, action/output records, and text/JSON report projection.
|
||||
- `output_projection.go`: shared bundle and manifest-file result projection for
|
||||
command JSON output.
|
||||
- `run_summary.go`: summary counters.
|
||||
- `run_failures.go`: destination failure aggregation and partial-result detection.
|
||||
- `run_selection.go`: destination bundle selection, path mapping decisions, and fixed-path warnings.
|
||||
|
||||
@@ -65,55 +65,19 @@ func writeInspectResult(options InspectOptions, selection sourceSelection) error
|
||||
}
|
||||
|
||||
type inspectResult struct {
|
||||
PipelineID string `json:"pipeline_id,omitempty"`
|
||||
SourceBackend string `json:"source_backend,omitempty"`
|
||||
BundleCount int `json:"bundle_count"`
|
||||
Bundles []inspectBundleResult `json:"bundles"`
|
||||
}
|
||||
|
||||
type inspectBundleResult struct {
|
||||
Path string `json:"path"`
|
||||
ID string `json:"id"`
|
||||
Created string `json:"created"`
|
||||
Digest string `json:"digest"`
|
||||
FileCount int `json:"file_count"`
|
||||
TotalSize int64 `json:"total_size"`
|
||||
Files []inspectFileResult `json:"files"`
|
||||
}
|
||||
|
||||
type inspectFileResult struct {
|
||||
Path string `json:"path"`
|
||||
SHA256 string `json:"sha256"`
|
||||
Size int64 `json:"size"`
|
||||
PipelineID string `json:"pipeline_id,omitempty"`
|
||||
SourceBackend string `json:"source_backend,omitempty"`
|
||||
BundleCount int `json:"bundle_count"`
|
||||
Bundles []bundleDetailResult `json:"bundles"`
|
||||
}
|
||||
|
||||
func inspectResultFromSelection(selection sourceSelection) inspectResult {
|
||||
result := inspectResult{
|
||||
return inspectResult{
|
||||
PipelineID: selection.PipelineID,
|
||||
SourceBackend: selection.SourceBackend,
|
||||
BundleCount: len(selection.Bundles),
|
||||
Bundles: make([]inspectBundleResult, 0, len(selection.Bundles)),
|
||||
Bundles: bundleDetailsFromBundles(selection.Bundles),
|
||||
}
|
||||
for _, sourceBundle := range selection.Bundles {
|
||||
bundleResult := inspectBundleResult{
|
||||
Path: storage.DisplayPath(sourceBundle.RootRelativePath),
|
||||
ID: sourceBundle.Manifest.ID,
|
||||
Created: sourceBundle.Manifest.Created.Format("2006-01-02T15:04:05Z07:00"),
|
||||
Digest: sourceBundle.Manifest.Digest,
|
||||
FileCount: len(sourceBundle.Manifest.Files),
|
||||
Files: make([]inspectFileResult, 0, len(sourceBundle.Manifest.Files)),
|
||||
}
|
||||
for _, file := range sourceBundle.Manifest.Files {
|
||||
bundleResult.TotalSize += file.Size
|
||||
bundleResult.Files = append(bundleResult.Files, inspectFileResult{
|
||||
Path: file.Path,
|
||||
SHA256: file.SHA256,
|
||||
Size: file.Size,
|
||||
})
|
||||
}
|
||||
result.Bundles = append(result.Bundles, bundleResult)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func writeInspection(w io.Writer, selection sourceSelection) error {
|
||||
@@ -134,7 +98,7 @@ func writeInspection(w io.Writer, selection sourceSelection) error {
|
||||
"- path=%s id=%s created=%s digest=%s files=%d\n",
|
||||
storage.DisplayPath(sourceBundle.RootRelativePath),
|
||||
sourceBundle.Manifest.ID,
|
||||
sourceBundle.Manifest.Created.Format("2006-01-02T15:04:05Z07:00"),
|
||||
formatManifestCreated(sourceBundle.Manifest.Created),
|
||||
sourceBundle.Manifest.Digest,
|
||||
len(sourceBundle.Manifest.Files),
|
||||
); err != nil {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/distributor/internal/testutil"
|
||||
)
|
||||
@@ -87,6 +88,49 @@ func TestInspectConfiguredSourceJSON(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectJSONPreservesCreatedOffsetAndFileMetadata(t *testing.T) {
|
||||
sourceRoot := t.TempDir()
|
||||
created := time.Date(2026, 6, 1, 6, 30, 0, 0, time.FixedZone("CDT", -5*60*60))
|
||||
testutil.WriteSourceBundle(t, sourceRoot, "daily", testutil.BundleOptions{
|
||||
ID: "reports.offset",
|
||||
Created: created,
|
||||
Files: []testutil.SourceFile{
|
||||
{Path: "report.md", Data: "# Report\n"},
|
||||
},
|
||||
})
|
||||
var stdout bytes.Buffer
|
||||
|
||||
err := Inspect(context.Background(), InspectOptions{
|
||||
Path: sourceRoot,
|
||||
Stdout: &stdout,
|
||||
OutputFormat: OutputFormatJSON,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Inspect() error = %v", err)
|
||||
}
|
||||
result := decodeAppResult(t, stdout.String())
|
||||
bundles, ok := result["bundles"].([]any)
|
||||
if !ok || len(bundles) != 1 {
|
||||
t.Fatalf("bundles = %#v, want one bundle", result["bundles"])
|
||||
}
|
||||
bundle, ok := bundles[0].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("bundle = %#v, want object", bundles[0])
|
||||
}
|
||||
if bundle["created"] != "2026-06-01T06:30:00-05:00" || bundle["file_count"] != float64(1) {
|
||||
t.Fatalf("bundle = %#v, want offset timestamp and file count", bundle)
|
||||
}
|
||||
files, ok := bundle["files"].([]any)
|
||||
if !ok || len(files) != 1 {
|
||||
t.Fatalf("files = %#v, want one file", bundle["files"])
|
||||
}
|
||||
file, ok := files[0].(map[string]any)
|
||||
if !ok || file["path"] != "report.md" || file["sha256"] == "" || file["size"] != float64(9) {
|
||||
t.Fatalf("file = %#v, want projected file metadata", file)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectConfiguredSourceJSONIncludesSecretConflictWarningWithoutValues(t *testing.T) {
|
||||
name := "DISTRIBUTOR_TEST_INSPECT_SECRET"
|
||||
t.Setenv(name, "process-value")
|
||||
|
||||
@@ -92,37 +92,23 @@ func normalizeManifestFiles(files []string) []string {
|
||||
}
|
||||
|
||||
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"`
|
||||
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 []manifestFileResult `json:"files"`
|
||||
}
|
||||
|
||||
func manifestCreateResultFromManifest(root string, manifest producerbundle.Manifest) manifestCreateResult {
|
||||
result := manifestCreateResult{
|
||||
return manifestCreateResult{
|
||||
ManifestPath: filepath.ToSlash(filepath.Join(root, producerbundle.ManifestName)),
|
||||
Root: filepath.ToSlash(root),
|
||||
ID: manifest.ID,
|
||||
Created: manifest.Created.Format(time.RFC3339),
|
||||
Created: formatManifestCreated(manifest.Created),
|
||||
Digest: manifest.Digest,
|
||||
FileCount: len(manifest.Files),
|
||||
Files: make([]manifestCreateFileResult, 0, len(manifest.Files)),
|
||||
Files: manifestFileResults(manifest.Files),
|
||||
}
|
||||
for _, file := range manifest.Files {
|
||||
result.Files = append(result.Files, manifestCreateFileResult{
|
||||
Path: file.Path,
|
||||
SHA256: file.SHA256,
|
||||
Size: file.Size,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
42
internal/app/manifest_test.go
Normal file
42
internal/app/manifest_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestManifestCreateJSONPreservesCreatedOffsetAndFileMetadata(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(root, "report.md"), []byte("# Report\n"), 0o600); err != nil {
|
||||
t.Fatalf("write report: %v", err)
|
||||
}
|
||||
var stdout bytes.Buffer
|
||||
|
||||
err := ManifestCreate(context.Background(), ManifestCreateOptions{
|
||||
Root: root,
|
||||
ID: "reports.offset",
|
||||
Created: "2026-06-01T06:30:00-05:00",
|
||||
Files: []string{"report.md"},
|
||||
Stdout: &stdout,
|
||||
OutputFormat: OutputFormatJSON,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("ManifestCreate() error = %v", err)
|
||||
}
|
||||
result := decodeAppResult(t, stdout.String())
|
||||
if result["id"] != "reports.offset" || result["created"] != "2026-06-01T06:30:00-05:00" || result["file_count"] != float64(1) {
|
||||
t.Fatalf("result = %#v, want manifest metadata", result)
|
||||
}
|
||||
files, ok := result["files"].([]any)
|
||||
if !ok || len(files) != 1 {
|
||||
t.Fatalf("files = %#v, want one file", result["files"])
|
||||
}
|
||||
file, ok := files[0].(map[string]any)
|
||||
if !ok || file["path"] != "report.md" || file["sha256"] == "" || file["size"] != float64(9) {
|
||||
t.Fatalf("file = %#v, want projected file metadata", file)
|
||||
}
|
||||
}
|
||||
83
internal/app/output_projection.go
Normal file
83
internal/app/output_projection.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/distributor/internal/bundle"
|
||||
"gitea.maximumdirect.net/eric/distributor/internal/storage"
|
||||
)
|
||||
|
||||
type bundleSummaryResult struct {
|
||||
Path string `json:"path"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type bundleDetailResult struct {
|
||||
Path string `json:"path"`
|
||||
ID string `json:"id"`
|
||||
Created string `json:"created"`
|
||||
Digest string `json:"digest"`
|
||||
FileCount int `json:"file_count"`
|
||||
TotalSize int64 `json:"total_size"`
|
||||
Files []manifestFileResult `json:"files"`
|
||||
}
|
||||
|
||||
type manifestFileResult struct {
|
||||
Path string `json:"path"`
|
||||
SHA256 string `json:"sha256"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
func bundleSummaryFromBundle(sourceBundle bundle.Bundle) bundleSummaryResult {
|
||||
return bundleSummaryResult{
|
||||
Path: storage.DisplayPath(sourceBundle.RootRelativePath),
|
||||
ID: sourceBundle.Manifest.ID,
|
||||
}
|
||||
}
|
||||
|
||||
func bundleSummariesFromBundles(sourceBundles []bundle.Bundle) []bundleSummaryResult {
|
||||
results := make([]bundleSummaryResult, 0, len(sourceBundles))
|
||||
for _, sourceBundle := range sourceBundles {
|
||||
results = append(results, bundleSummaryFromBundle(sourceBundle))
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func bundleDetailFromBundle(sourceBundle bundle.Bundle) bundleDetailResult {
|
||||
result := bundleDetailResult{
|
||||
Path: storage.DisplayPath(sourceBundle.RootRelativePath),
|
||||
ID: sourceBundle.Manifest.ID,
|
||||
Created: formatManifestCreated(sourceBundle.Manifest.Created),
|
||||
Digest: sourceBundle.Manifest.Digest,
|
||||
FileCount: len(sourceBundle.Manifest.Files),
|
||||
Files: manifestFileResults(sourceBundle.Manifest.Files),
|
||||
}
|
||||
for _, file := range sourceBundle.Manifest.Files {
|
||||
result.TotalSize += file.Size
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func bundleDetailsFromBundles(sourceBundles []bundle.Bundle) []bundleDetailResult {
|
||||
results := make([]bundleDetailResult, 0, len(sourceBundles))
|
||||
for _, sourceBundle := range sourceBundles {
|
||||
results = append(results, bundleDetailFromBundle(sourceBundle))
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func manifestFileResults(files []bundle.ManifestFile) []manifestFileResult {
|
||||
results := make([]manifestFileResult, 0, len(files))
|
||||
for _, file := range files {
|
||||
results = append(results, manifestFileResult{
|
||||
Path: file.Path,
|
||||
SHA256: file.SHA256,
|
||||
Size: file.Size,
|
||||
})
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func formatManifestCreated(created time.Time) string {
|
||||
return created.Format(time.RFC3339)
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"io"
|
||||
|
||||
"gitea.maximumdirect.net/eric/distributor/internal/config"
|
||||
"gitea.maximumdirect.net/eric/distributor/internal/storage"
|
||||
)
|
||||
|
||||
type ValidateOptions struct {
|
||||
@@ -73,29 +72,17 @@ func writeValidateResult(options ValidateOptions, selection sourceSelection) err
|
||||
}
|
||||
|
||||
type validateResult struct {
|
||||
PipelineID string `json:"pipeline_id,omitempty"`
|
||||
SourceBackend string `json:"source_backend,omitempty"`
|
||||
BundleCount int `json:"bundle_count"`
|
||||
Bundles []validateBundleResult `json:"bundles"`
|
||||
}
|
||||
|
||||
type validateBundleResult struct {
|
||||
Path string `json:"path"`
|
||||
ID string `json:"id"`
|
||||
PipelineID string `json:"pipeline_id,omitempty"`
|
||||
SourceBackend string `json:"source_backend,omitempty"`
|
||||
BundleCount int `json:"bundle_count"`
|
||||
Bundles []bundleSummaryResult `json:"bundles"`
|
||||
}
|
||||
|
||||
func validateResultFromSelection(selection sourceSelection) validateResult {
|
||||
result := validateResult{
|
||||
return validateResult{
|
||||
PipelineID: selection.PipelineID,
|
||||
SourceBackend: selection.SourceBackend,
|
||||
BundleCount: len(selection.Bundles),
|
||||
Bundles: make([]validateBundleResult, 0, len(selection.Bundles)),
|
||||
Bundles: bundleSummariesFromBundles(selection.Bundles),
|
||||
}
|
||||
for _, sourceBundle := range selection.Bundles {
|
||||
result.Bundles = append(result.Bundles, validateBundleResult{
|
||||
Path: storage.DisplayPath(sourceBundle.RootRelativePath),
|
||||
ID: sourceBundle.Manifest.ID,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user