Applied a more general bugfix to path-resolution issues in the archive stage
This commit is contained in:
@@ -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 == "" {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user