Simplified the output of narratio artifacts list --remote and narratio status --session-id
This commit is contained in:
@@ -132,7 +132,7 @@ Valid stage names:
|
|||||||
- `--session <path>`
|
- `--session <path>`
|
||||||
- `--session-id <value>`
|
- `--session-id <value>`
|
||||||
- `--previous-session-id <value>`
|
- `--previous-session-id <value>`
|
||||||
- `--remote`: check remote object availability. Catalog sections use canonical source paths; the `Promoted` section checks configured archive destinations.
|
- `--remote`: check remote availability for configured archive promotion destinations.
|
||||||
|
|
||||||
### `locks`
|
### `locks`
|
||||||
|
|
||||||
@@ -288,8 +288,7 @@ Syntax:
|
|||||||
narratio artifacts list [--config <pipeline.yml>] [--campaign <campaign.yml>] [--session <session.yml>] [--session-id <id>] [--previous-session-id <id>] [--remote]
|
narratio artifacts list [--config <pipeline.yml>] [--campaign <campaign.yml>] [--session <session.yml>] [--session-id <id>] [--previous-session-id <id>] [--remote]
|
||||||
```
|
```
|
||||||
|
|
||||||
`--remote` checks promoted top-level object availability through the storage adapter.
|
`--remote` checks promoted top-level object availability through the storage adapter. Remote markers appear only in the `Promoted` section, which reports each configured archive promotion destination and includes `dest=<path>` when that destination differs from the source's canonical path.
|
||||||
Catalog sections report canonical source-path availability. The `Promoted` section reports each configured archive promotion destination and includes `dest=<path>` when that destination differs from the source's canonical path.
|
|
||||||
|
|
||||||
### `locks`
|
### `locks`
|
||||||
|
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ Dry-run does not write restore report files.
|
|||||||
|
|
||||||
- `status` with no config/session flags still requires explicit `--manifest`.
|
- `status` with no config/session flags still requires explicit `--manifest`.
|
||||||
- `status --session-id <id>` uses normal config/session loading, including remote session fallback.
|
- `status --session-id <id>` uses normal config/session loading, including remote session fallback.
|
||||||
- `status --session-id <id>` includes the same remote output availability view as `artifacts list --remote` when storage is configured: catalog sections use canonical source paths, and promoted outputs use configured archive destinations.
|
- `status --session-id <id>` includes the same promoted remote output availability view as `artifacts list --remote` when storage is configured.
|
||||||
- local and S3 audio input modes are mutually exclusive.
|
- local and S3 audio input modes are mutually exclusive.
|
||||||
- archive publish requires upstream stages through `analyze` to be `succeeded`.
|
- archive publish requires upstream stages through `analyze` to be `succeeded`.
|
||||||
- required promotion rules can fail when selected analyze artifacts did not generate a required file path.
|
- required promotion rules can fail when selected analyze artifacts did not generate a required file path.
|
||||||
|
|||||||
@@ -218,14 +218,12 @@ func Status(ctx context.Context, args []string, out io.Writer) error {
|
|||||||
All: staticArchiveLocks(cfg),
|
All: staticArchiveLocks(cfg),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
remoteState := map[string]string{}
|
|
||||||
promotedRemoteState := map[string]string{}
|
promotedRemoteState := map[string]string{}
|
||||||
if store != nil {
|
if store != nil {
|
||||||
remoteState = remoteArtifactAvailability(ctx, cfg, store, catalog)
|
|
||||||
promotedRemoteState = remotePromotionAvailability(ctx, cfg, store, catalog)
|
promotedRemoteState = remotePromotionAvailability(ctx, cfg, store, catalog)
|
||||||
}
|
}
|
||||||
fmt.Fprintln(out, "Remote outputs:")
|
fmt.Fprintln(out, "Remote outputs:")
|
||||||
writeArtifactList(out, cfg, catalog, catalogLocks, remoteState, promotedRemoteState)
|
writeArtifactList(out, cfg, catalog, catalogLocks, promotedRemoteState)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(out, "Archive locks: error: %v\n", err)
|
fmt.Fprintf(out, "Archive locks: error: %v\n", err)
|
||||||
@@ -390,13 +388,11 @@ func ArtifactsList(ctx context.Context, args []string, out io.Writer) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("artifacts list: %w", err)
|
return fmt.Errorf("artifacts list: %w", err)
|
||||||
}
|
}
|
||||||
remoteState := map[string]string{}
|
|
||||||
promotedRemoteState := map[string]string{}
|
promotedRemoteState := map[string]string{}
|
||||||
if remote && store != nil {
|
if remote && store != nil {
|
||||||
remoteState = remoteArtifactAvailability(ctx, cfg, store, catalog)
|
|
||||||
promotedRemoteState = remotePromotionAvailability(ctx, cfg, store, catalog)
|
promotedRemoteState = remotePromotionAvailability(ctx, cfg, store, catalog)
|
||||||
}
|
}
|
||||||
writeArtifactList(out, cfg, catalog, locks, remoteState, promotedRemoteState)
|
writeArtifactList(out, cfg, catalog, locks, promotedRemoteState)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -797,7 +793,7 @@ func buildHelperArtifactCatalog(cfg *config.Config) (*artifacts.ArtifactCatalog,
|
|||||||
return catalog, nil
|
return catalog, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeArtifactList(out io.Writer, cfg *config.Config, catalog *artifacts.ArtifactCatalog, locks *effectiveLocks, remoteState map[string]string, promotedRemoteState map[string]string) {
|
func writeArtifactList(out io.Writer, cfg *config.Config, catalog *artifacts.ArtifactCatalog, locks *effectiveLocks, promotedRemoteState map[string]string) {
|
||||||
lockSet := lockSourceSet(locks.All)
|
lockSet := lockSourceSet(locks.All)
|
||||||
fmt.Fprintln(out, "Built-in:")
|
fmt.Fprintln(out, "Built-in:")
|
||||||
for _, id := range []string{
|
for _, id := range []string{
|
||||||
@@ -807,11 +803,11 @@ func writeArtifactList(out io.Writer, cfg *config.Config, catalog *artifacts.Art
|
|||||||
artifacts.ArtifactTranscriptTrimmed,
|
artifacts.ArtifactTranscriptTrimmed,
|
||||||
artifacts.ArtifactBoundsSession,
|
artifacts.ArtifactBoundsSession,
|
||||||
} {
|
} {
|
||||||
writeArtifactLine(out, id, lockSet, remoteState)
|
writeArtifactLine(out, id, lockSet)
|
||||||
}
|
}
|
||||||
fmt.Fprintln(out, "Configured:")
|
fmt.Fprintln(out, "Configured:")
|
||||||
for _, entry := range catalog.ListConfigured() {
|
for _, entry := range catalog.ListConfigured() {
|
||||||
writeArtifactLine(out, entry.SourceID, lockSet, remoteState)
|
writeArtifactLine(out, entry.SourceID, lockSet)
|
||||||
}
|
}
|
||||||
fmt.Fprintln(out, "Previous-session:")
|
fmt.Fprintln(out, "Previous-session:")
|
||||||
for _, req := range artifacts.CollectPreviousArtifactRequirements(configuredScriptoriumArtifacts(cfg)) {
|
for _, req := range artifacts.CollectPreviousArtifactRequirements(configuredScriptoriumArtifacts(cfg)) {
|
||||||
@@ -823,14 +819,11 @@ func writeArtifactList(out io.Writer, cfg *config.Config, catalog *artifacts.Art
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeArtifactLine(out io.Writer, source string, lockSet map[string]config.ArchiveLockRule, remoteState map[string]string) {
|
func writeArtifactLine(out io.Writer, source string, lockSet map[string]config.ArchiveLockRule) {
|
||||||
parts := []string{source}
|
parts := []string{source}
|
||||||
if _, ok := lockSet[source]; ok {
|
if _, ok := lockSet[source]; ok {
|
||||||
parts = append(parts, "locked")
|
parts = append(parts, "locked")
|
||||||
}
|
}
|
||||||
if state := remoteState[source]; state != "" {
|
|
||||||
parts = append(parts, state)
|
|
||||||
}
|
|
||||||
fmt.Fprintf(out, "- %s\n", strings.Join(parts, " "))
|
fmt.Fprintf(out, "- %s\n", strings.Join(parts, " "))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -855,26 +848,6 @@ func writePromotedArtifactLine(out io.Writer, rule config.ArchivePromotionRule,
|
|||||||
fmt.Fprintf(out, "- %s\n", strings.Join(parts, " "))
|
fmt.Fprintf(out, "- %s\n", strings.Join(parts, " "))
|
||||||
}
|
}
|
||||||
|
|
||||||
func remoteArtifactAvailability(ctx context.Context, cfg *config.Config, store storage.ObjectStore, catalog *artifacts.ArtifactCatalog) map[string]string {
|
|
||||||
out := map[string]string{}
|
|
||||||
sessionPrefix := artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.SessionID)
|
|
||||||
for _, source := range allCatalogSources(catalog) {
|
|
||||||
entry, ok := catalog.Lookup(source)
|
|
||||||
if !ok || strings.TrimSpace(entry.CanonicalRelPath) == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
key := artifacts.S3PromotedArtifactKey(sessionPrefix, entry.CanonicalRelPath)
|
|
||||||
if exists, err := store.Exists(ctx, key); err == nil && exists {
|
|
||||||
out[source] = "remote=promoted"
|
|
||||||
} else if err != nil {
|
|
||||||
out[source] = "remote=error"
|
|
||||||
} else {
|
|
||||||
out[source] = "remote=missing"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func remotePromotionAvailability(ctx context.Context, cfg *config.Config, store storage.ObjectStore, catalog *artifacts.ArtifactCatalog) map[string]string {
|
func remotePromotionAvailability(ctx context.Context, cfg *config.Config, store storage.ObjectStore, catalog *artifacts.ArtifactCatalog) map[string]string {
|
||||||
out := map[string]string{}
|
out := map[string]string{}
|
||||||
sessionPrefix := artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.SessionID)
|
sessionPrefix := artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.SessionID)
|
||||||
@@ -938,20 +911,6 @@ func promotionRemoteStateKey(source, dest string) string {
|
|||||||
return strings.TrimSpace(source) + "\x00" + strings.TrimSpace(dest)
|
return strings.TrimSpace(source) + "\x00" + strings.TrimSpace(dest)
|
||||||
}
|
}
|
||||||
|
|
||||||
func allCatalogSources(catalog *artifacts.ArtifactCatalog) []string {
|
|
||||||
out := []string{
|
|
||||||
artifacts.ArtifactTranscriptMerged,
|
|
||||||
artifacts.ArtifactTranscriptPolished,
|
|
||||||
artifacts.ArtifactTranscriptFull,
|
|
||||||
artifacts.ArtifactTranscriptTrimmed,
|
|
||||||
artifacts.ArtifactBoundsSession,
|
|
||||||
}
|
|
||||||
for _, entry := range catalog.ListConfigured() {
|
|
||||||
out = append(out, entry.SourceID)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeLocks(out io.Writer, cfg *config.Config, locks *effectiveLocks) {
|
func writeLocks(out io.Writer, cfg *config.Config, locks *effectiveLocks) {
|
||||||
if locks == nil || len(locks.All) == 0 {
|
if locks == nil || len(locks.All) == 0 {
|
||||||
fmt.Fprintln(out, "Archive locks: none")
|
fmt.Fprintln(out, "Archive locks: none")
|
||||||
|
|||||||
@@ -311,6 +311,12 @@ func TestExecuteTopLevelLockAndUnlockAreRemoved(t *testing.T) {
|
|||||||
func TestExecuteArtifactsListRemoteReportsPromotedAvailability(t *testing.T) {
|
func TestExecuteArtifactsListRemoteReportsPromotedAvailability(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
|
addArchivePromotionsToPipeline(t, pipelinePath, `
|
||||||
|
promote_artifacts:
|
||||||
|
- source: narratio.transcript.trimmed
|
||||||
|
dest: transcripts/trimmed.json
|
||||||
|
required: true
|
||||||
|
`)
|
||||||
fake := &storage.FakeBackend{}
|
fake := &storage.FakeBackend{}
|
||||||
trimmedKey := artifacts.S3PromotedArtifactKey(
|
trimmedKey := artifacts.S3PromotedArtifactKey(
|
||||||
artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03"),
|
artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03"),
|
||||||
@@ -369,9 +375,15 @@ func TestExecuteArtifactsListRemoteUsesPromotionDestinations(t *testing.T) {
|
|||||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||||
}
|
}
|
||||||
out := stdout.String()
|
out := stdout.String()
|
||||||
for _, want := range []string{
|
for _, unwanted := range []string{
|
||||||
"narratio.transcript.full remote=missing",
|
"narratio.transcript.full remote=missing",
|
||||||
"narratio.bounds.session remote=missing",
|
"narratio.bounds.session remote=missing",
|
||||||
|
} {
|
||||||
|
if strings.Contains(out, unwanted) {
|
||||||
|
t.Fatalf("stdout = %q, did not want catalog remote marker %q", out, unwanted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, want := range []string{
|
||||||
"narratio.transcript.full dest=transcripts/full.json remote=promoted",
|
"narratio.transcript.full dest=transcripts/full.json remote=promoted",
|
||||||
"narratio.bounds.session dest=transcripts/bounds.json remote=promoted",
|
"narratio.bounds.session dest=transcripts/bounds.json remote=promoted",
|
||||||
} {
|
} {
|
||||||
@@ -426,19 +438,28 @@ func TestExecuteStatusReportsRemoteArtifactCatalog(t *testing.T) {
|
|||||||
"Configured:",
|
"Configured:",
|
||||||
"Previous-session:",
|
"Previous-session:",
|
||||||
"Promoted:",
|
"Promoted:",
|
||||||
|
"narratio.transcript.trimmed locked",
|
||||||
"narratio.transcript.trimmed locked remote=promoted",
|
"narratio.transcript.trimmed locked remote=promoted",
|
||||||
"narratio.transcript.merged remote=missing",
|
|
||||||
"narratio.transcript.full dest=transcripts/full.json remote=promoted",
|
"narratio.transcript.full dest=transcripts/full.json remote=promoted",
|
||||||
} {
|
} {
|
||||||
if !strings.Contains(out, want) {
|
if !strings.Contains(out, want) {
|
||||||
t.Fatalf("stdout = %q, want %q", out, want)
|
t.Fatalf("stdout = %q, want %q", out, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if strings.Contains(out, "narratio.transcript.merged remote=missing") {
|
||||||
|
t.Fatalf("stdout = %q, did not want catalog remote marker", out)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExecuteStatusReportsRemoteArtifactCatalogErrorsWithoutFailing(t *testing.T) {
|
func TestExecuteStatusReportsRemoteArtifactCatalogErrorsWithoutFailing(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
|
addArchivePromotionsToPipeline(t, pipelinePath, `
|
||||||
|
promote_artifacts:
|
||||||
|
- source: narratio.transcript.trimmed
|
||||||
|
dest: transcripts/trimmed.json
|
||||||
|
required: true
|
||||||
|
`)
|
||||||
fake := &storage.FakeBackend{ExistsErr: fmt.Errorf("exists failed")}
|
fake := &storage.FakeBackend{ExistsErr: fmt.Errorf("exists failed")}
|
||||||
var storeInitCalls int
|
var storeInitCalls int
|
||||||
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
|
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
|
||||||
|
|||||||
Reference in New Issue
Block a user