From 924b5d15c6e228e61f1a6bc00fa16fc9b8483d1b Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sun, 17 May 2026 11:08:11 -0500 Subject: [PATCH] Applied a more general bugfix to path-resolution issues in the archive stage --- internal/stage/archive.go | 64 ++++++++++++++++++++++++++++------ internal/stage/archive_test.go | 24 +++++++++++++ 2 files changed, 78 insertions(+), 10 deletions(-) diff --git a/internal/stage/archive.go b/internal/stage/archive.go index 67814e0..97af070 100644 --- a/internal/stage/archive.go +++ b/internal/stage/archive.go @@ -125,7 +125,8 @@ func (archiveStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S if err != nil { return nil, fmt.Errorf("archive: collect run files: %w", err) } - promotions, err := resolveArchivePromotions(workDir, env.Config.Pipeline.Archive.PromoteArtifacts) + workDirCandidates := archiveSourceWorkDirs(env, m, workDir) + promotions, err := resolveArchivePromotions(workDirCandidates, env.Config.Pipeline.Archive.PromoteArtifacts) if err != nil { return nil, fmt.Errorf("archive: resolve promotion rules: %w", err) } @@ -330,21 +331,37 @@ func archiveBucket(env *Env, m *manifest.Manifest) string { return strings.TrimSpace(env.Config.Pipeline.Storage.S3.Bucket) } -func resolveArchivePromotions(workDir string, rules []config.ArchivePromotionRule) ([]archivePromotion, error) { +func resolveArchivePromotions(workDirs []string, rules []config.ArchivePromotionRule) ([]archivePromotion, error) { + if len(workDirs) == 0 { + return nil, fmt.Errorf("at least one workdir candidate is required") + } out := make([]archivePromotion, 0, len(rules)) for _, rule := range rules { from := strings.TrimSpace(rule.From) to := strings.TrimSpace(rule.To) required := rule.Required == nil || *rule.Required - localPath, err := resolveWorkDirRelativePath(workDir, from) - if err != nil { - return nil, fmt.Errorf("promotion from %q: %w", from, err) - } - info, err := os.Stat(localPath) - exists := err == nil && !info.IsDir() - if err != nil && !os.IsNotExist(err) { - return nil, fmt.Errorf("promotion source %q: %w", from, err) + var ( + localPath string + exists bool + ) + for _, candidateRoot := range workDirs { + resolvedPath, err := resolveWorkDirRelativePath(candidateRoot, from) + if err != nil { + return nil, fmt.Errorf("promotion from %q: %w", from, err) + } + info, err := os.Stat(resolvedPath) + if err == nil && !info.IsDir() { + localPath = resolvedPath + exists = true + break + } + if err != nil && !os.IsNotExist(err) { + return nil, fmt.Errorf("promotion source %q: %w", from, err) + } + if localPath == "" { + localPath = resolvedPath + } } out = append(out, archivePromotion{ @@ -358,6 +375,33 @@ func resolveArchivePromotions(workDir string, rules []config.ArchivePromotionRul return out, nil } +func archiveSourceWorkDirs(env *Env, m *manifest.Manifest, runWorkDir string) []string { + candidates := make([]string, 0, 2) + if strings.TrimSpace(runWorkDir) != "" { + candidates = append(candidates, filepath.Clean(runWorkDir)) + } + sessionID := strings.TrimSpace(env.Config.Session.SessionID) + if sessionID == "" && m != nil { + sessionID = strings.TrimSpace(m.SessionID) + } + if sessionID != "" { + sessionWorkDir := filepath.Clean(artifacts.SessionWorkDir(env.Config.Pipeline.Workspace.Root, sessionID)) + if sessionWorkDir != "" { + candidates = append(candidates, sessionWorkDir) + } + } + seen := make(map[string]struct{}, len(candidates)) + out := make([]string, 0, len(candidates)) + for _, c := range candidates { + if _, ok := seen[c]; ok { + continue + } + seen[c] = struct{}{} + out = append(out, c) + } + return out +} + func resolveWorkDirRelativePath(workDir, rel string) (string, error) { rel = filepath.Clean(filepath.FromSlash(strings.TrimSpace(rel))) if rel == "." || rel == "" { diff --git a/internal/stage/archive_test.go b/internal/stage/archive_test.go index 8fd54c2..3a695f1 100644 --- a/internal/stage/archive_test.go +++ b/internal/stage/archive_test.go @@ -3,6 +3,7 @@ package stage import ( "context" "errors" + "os" "path/filepath" "reflect" "strings" @@ -182,6 +183,29 @@ func TestArchiveFailsWhenRequiredPromotionMissing(t *testing.T) { } } +func TestArchivePromotionFallsBackToSessionWorkDir(t *testing.T) { + env, m, runWorkDir := archiveFixture(t) + sessionWorkDir := artifacts.SessionWorkDir(env.Config.Pipeline.Workspace.Root, m.SessionID) + sessionTrimmed := filepath.Join(sessionWorkDir, "transcripts", "trimmed.json") + + if err := os.Remove(filepath.Join(runWorkDir, "transcripts", "trimmed.json")); err != nil { + t.Fatalf("remove run scoped trimmed transcript: %v", err) + } + writeStageTestFile(t, sessionTrimmed, "{}\n") + + result, err := archiveStage{}.Run(context.Background(), env, m) + if err != nil { + t.Fatalf("Run() error = %v", err) + } + if result.Metadata["promoted_files_uploaded"] != 2 { + t.Fatalf("metadata promoted_files_uploaded = %#v, want 2", result.Metadata["promoted_files_uploaded"]) + } + fake := env.ObjectStore.(*storage.FakeBackend) + if _, ok := fake.Objects[m.S3SessionPrefix+"transcripts/trimmed.json"]; !ok { + t.Fatalf("missing promoted trimmed key from session fallback") + } +} + func TestArchiveDoesNotWriteCurrentPointerWhenPromotionUploadFails(t *testing.T) { env, m, _ := archiveFixture(t) fake := env.ObjectStore.(*storage.FakeBackend)