Aligned the archive stage with the new work directory layout

This commit is contained in:
2026-05-18 01:13:51 +00:00
parent cb525c0f72
commit 7dc79e052f
15 changed files with 466 additions and 199 deletions

View File

@@ -33,15 +33,6 @@ var archivePrerequisiteStages = []string{
"analyze",
}
var archiveRunUploadDirs = []string{
"inputs",
"transcripts",
"artifacts",
"reports",
"config",
"logs",
}
func (archiveStage) Name() string { return "archive" }
func (archiveStage) Declares() IODecl {
@@ -87,16 +78,16 @@ func (archiveStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
return nil, fmt.Errorf("archive: remote object store backend is required when archive run upload is enabled")
}
workDir, err := archiveWorkDir(env, m)
runRoot, err := resolveArchiveRunRoot(env, m)
if err != nil {
return nil, fmt.Errorf("archive: resolve local workdir: %w", err)
return nil, fmt.Errorf("archive: resolve run root: %w", err)
}
workDirInfo, err := os.Stat(workDir)
runRootInfo, err := os.Stat(runRoot)
if err != nil {
return nil, fmt.Errorf("archive: local workdir %q: %w", workDir, err)
return nil, fmt.Errorf("archive: run root %q: %w", runRoot, err)
}
if !workDirInfo.IsDir() {
return nil, fmt.Errorf("archive: local workdir %q is not a directory", workDir)
if !runRootInfo.IsDir() {
return nil, fmt.Errorf("archive: run root %q is not a directory", runRoot)
}
runPrefix, err := archiveRunPrefix(env, m)
@@ -116,17 +107,20 @@ func (archiveStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
return nil, fmt.Errorf("archive: run id is required")
}
manifestSource, err := resolveArchiveManifestSource(env, m, workDir)
manifestSource, err := resolveArchiveRunManifestSource(runRoot)
if err != nil {
return nil, fmt.Errorf("archive: resolve manifest source: %w", err)
return nil, fmt.Errorf("archive: resolve run manifest source: %w", err)
}
runFiles, err := collectArchiveRunFiles(workDir, manifestSource)
runFiles, err := collectArchiveRunFiles(runRoot, manifestSource)
if err != nil {
return nil, fmt.Errorf("archive: collect run files: %w", err)
}
workDirCandidates := archiveSourceWorkDirs(env, m, workDir)
promotions, err := resolveArchivePromotions(workDirCandidates, env.Config.Pipeline.Archive.PromoteArtifacts)
sessionRoot, err := resolveArchiveSessionRoot(env, m)
if err != nil {
return nil, fmt.Errorf("archive: resolve session root for promotions: %w", err)
}
promotions, err := resolveArchivePromotions(sessionRoot, env.Config.Pipeline.Archive.PromoteArtifacts)
if err != nil {
return nil, fmt.Errorf("archive: resolve promotion rules: %w", err)
}
@@ -249,39 +243,67 @@ func validateArchivePrerequisites(m *manifest.Manifest) error {
return nil
}
func archiveWorkDir(env *Env, m *manifest.Manifest) (string, error) {
workDir := strings.TrimSpace(m.LocalWorkDir)
if workDir != "" {
cleaned := filepath.Clean(workDir)
if info, err := os.Stat(cleaned); err == nil && info.IsDir() {
return cleaned, nil
}
}
func resolveArchiveRunRoot(env *Env, m *manifest.Manifest) (string, error) {
sessionID := strings.TrimSpace(env.Config.Session.SessionID)
if sessionID == "" {
if sessionID == "" && m != nil {
sessionID = strings.TrimSpace(m.SessionID)
}
campaign := strings.TrimSpace(env.Config.Session.Campaign)
if campaign == "" {
if campaign == "" && m != nil {
campaign = strings.TrimSpace(m.Campaign)
}
runID := strings.TrimSpace(m.RunID)
runID := ""
if m != nil {
runID = strings.TrimSpace(m.RunID)
}
if sessionID == "" || campaign == "" {
return "", fmt.Errorf("campaign and session id are required")
}
if runID == "" {
return "", fmt.Errorf("run id is required")
}
if campaign == "" || sessionID == "" {
return "", fmt.Errorf("campaign and session id are required")
canonical := filepath.Clean(artifacts.SessionRunRootForCampaign(env.Config.Pipeline.Workspace.Root, campaign, sessionID, runID))
legacy := filepath.Clean(artifacts.SessionRunWorkDir(env.Config.Pipeline.Workspace.Root, campaign, sessionID, runID))
canonicalExists, err := directoryExists(canonical)
if err != nil {
return "", fmt.Errorf("check canonical run root %q: %w", canonical, err)
}
runScoped := artifacts.SessionRunWorkDir(env.Config.Pipeline.Workspace.Root, campaign, sessionID, runID)
if info, err := os.Stat(runScoped); err == nil && info.IsDir() {
return runScoped, nil
legacyExists, err := directoryExists(legacy)
if err != nil {
return "", fmt.Errorf("check legacy run root %q: %w", legacy, err)
}
switch {
case canonicalExists && legacyExists && canonical != legacy:
return "", fmt.Errorf("ambiguous run roots for campaign %q session %q run %q: canonical=%q legacy=%q", campaign, sessionID, runID, canonical, legacy)
case canonicalExists:
return canonical, nil
case legacyExists:
return legacy, nil
default:
return "", fmt.Errorf("run root not found for campaign %q session %q run %q (checked canonical=%q legacy=%q)", campaign, sessionID, runID, canonical, legacy)
}
}
func resolveArchiveSessionRoot(env *Env, m *manifest.Manifest) (string, error) {
sessionID := strings.TrimSpace(env.Config.Session.SessionID)
if sessionID == "" && m != nil {
sessionID = strings.TrimSpace(m.SessionID)
}
campaign := strings.TrimSpace(env.Config.Session.Campaign)
if campaign == "" && m != nil {
campaign = strings.TrimSpace(m.Campaign)
}
if sessionID == "" {
return "", fmt.Errorf("session id is required")
}
paths, err := artifacts.NewLocalStore(env.Config.Pipeline.Workspace.Root).ResolveSessionPathsFor(campaign, sessionID)
if err != nil {
return "", err
}
return paths.Root, nil
return filepath.Clean(paths.Root), nil
}
func archiveRunPrefix(env *Env, m *manifest.Manifest) (string, error) {
@@ -334,9 +356,10 @@ func archiveBucket(env *Env, m *manifest.Manifest) string {
return strings.TrimSpace(env.Config.Pipeline.Storage.S3.Bucket)
}
func resolveArchivePromotions(workDirs []string, rules []config.ArchivePromotionRule) ([]archivePromotion, error) {
if len(workDirs) == 0 {
return nil, fmt.Errorf("at least one workdir candidate is required")
func resolveArchivePromotions(sessionRoot string, rules []config.ArchivePromotionRule) ([]archivePromotion, error) {
sessionRoot = filepath.Clean(strings.TrimSpace(sessionRoot))
if sessionRoot == "" {
return nil, fmt.Errorf("session root is required")
}
out := make([]archivePromotion, 0, len(rules))
for _, rule := range rules {
@@ -344,28 +367,16 @@ func resolveArchivePromotions(workDirs []string, rules []config.ArchivePromotion
to := strings.TrimSpace(rule.To)
required := rule.Required == nil || *rule.Required
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
}
resolvedPath, err := resolveWorkDirRelativePath(sessionRoot, from)
if err != nil {
return nil, fmt.Errorf("promotion from %q: %w", from, err)
}
info, err := os.Stat(resolvedPath)
exists := err == nil && !info.IsDir()
if err != nil && !os.IsNotExist(err) {
return nil, fmt.Errorf("promotion source %q: %w", from, err)
}
localPath := resolvedPath
out = append(out, archivePromotion{
From: from,
@@ -378,35 +389,6 @@ func resolveArchivePromotions(workDirs []string, rules []config.ArchivePromotion
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 != "" {
paths, err := artifacts.NewLocalStore(env.Config.Pipeline.Workspace.Root).ResolveSessionPathsFor(strings.TrimSpace(env.Config.Session.Campaign), sessionID)
if err == nil {
if cleanRoot := filepath.Clean(paths.Root); cleanRoot != "" {
candidates = append(candidates, cleanRoot)
}
}
}
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 == "" {
@@ -425,42 +407,40 @@ func resolveWorkDirRelativePath(workDir, rel string) (string, error) {
return cleanedFull, nil
}
func collectArchiveRunFiles(workDir, manifestPath string) ([]archiveUploadFile, error) {
func collectArchiveRunFiles(runRoot, manifestPath string) ([]archiveUploadFile, error) {
files := make([]archiveUploadFile, 0, 64)
for _, dirName := range archiveRunUploadDirs {
fullDir := filepath.Join(workDir, dirName)
info, err := os.Stat(fullDir)
if err != nil {
if os.IsNotExist(err) {
continue
}
return nil, fmt.Errorf("stat %q: %w", fullDir, err)
err := filepath.WalkDir(runRoot, func(path string, d fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if !info.IsDir() {
continue
}
if err := filepath.WalkDir(fullDir, func(path string, d fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if d.IsDir() {
if d.IsDir() {
if path == runRoot {
return nil
}
rel, err := filepath.Rel(workDir, path)
relDir, err := filepath.Rel(runRoot, path)
if err != nil {
return fmt.Errorf("relative path from %q to %q: %w", workDir, path, err)
return fmt.Errorf("relative dir from %q to %q: %w", runRoot, path, err)
}
relDir = filepath.ToSlash(relDir)
// Preserve existing behavior: audio is not uploaded in archive run record.
if relDir == "audio" || strings.HasPrefix(relDir, "audio/") {
return filepath.SkipDir
}
rel = filepath.ToSlash(rel)
files = append(files, archiveUploadFile{
RelativePath: rel,
LocalPath: path,
})
return nil
}); err != nil {
return nil, fmt.Errorf("walk %q: %w", fullDir, err)
}
rel, err := filepath.Rel(runRoot, path)
if err != nil {
return fmt.Errorf("relative path from %q to %q: %w", runRoot, path, err)
}
rel = filepath.ToSlash(rel)
files = append(files, archiveUploadFile{
RelativePath: rel,
LocalPath: path,
})
return nil
})
if err != nil {
return nil, fmt.Errorf("walk %q: %w", runRoot, err)
}
manifestInfo, err := os.Stat(manifestPath)
@@ -477,6 +457,14 @@ func collectArchiveRunFiles(workDir, manifestPath string) ([]archiveUploadFile,
RelativePath: "manifest.json",
LocalPath: manifestPath,
})
seen := map[string]archiveUploadFile{}
for _, file := range files {
seen[file.RelativePath] = file
}
files = files[:0]
for _, file := range seen {
files = append(files, file)
}
sort.Slice(files, func(i, j int) bool {
return files[i].RelativePath < files[j].RelativePath
@@ -484,42 +472,30 @@ func collectArchiveRunFiles(workDir, manifestPath string) ([]archiveUploadFile,
return files, nil
}
func resolveArchiveManifestSource(env *Env, m *manifest.Manifest, workDir string) (string, error) {
candidates := make([]string, 0, 3)
candidates = append(candidates, filepath.Join(workDir, "manifest.json"))
func resolveArchiveRunManifestSource(runRoot string) (string, error) {
path := filepath.Join(filepath.Clean(runRoot), "manifest.json")
info, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return "", fmt.Errorf("manifest.json not found in run root %q", runRoot)
}
return "", fmt.Errorf("stat %q: %w", path, err)
}
if info.IsDir() {
return "", fmt.Errorf("manifest path %q is a directory", path)
}
return path, nil
}
sessionID := strings.TrimSpace(env.Config.Session.SessionID)
if sessionID == "" && m != nil {
sessionID = strings.TrimSpace(m.SessionID)
func directoryExists(path string) (bool, error) {
info, err := os.Stat(path)
if err == nil {
return info.IsDir(), nil
}
if sessionID != "" {
paths, err := artifacts.NewLocalStore(env.Config.Pipeline.Workspace.Root).ResolveSessionPathsFor(strings.TrimSpace(env.Config.Session.Campaign), sessionID)
if err != nil {
return "", err
}
candidates = append(candidates, paths.ManifestPath)
if os.IsNotExist(err) {
return false, nil
}
seen := make(map[string]struct{}, len(candidates))
for _, candidate := range candidates {
clean := filepath.Clean(candidate)
if _, ok := seen[clean]; ok {
continue
}
seen[clean] = struct{}{}
info, err := os.Stat(clean)
if err != nil {
if os.IsNotExist(err) {
continue
}
return "", fmt.Errorf("stat %q: %w", clean, err)
}
if info.IsDir() {
continue
}
return clean, nil
}
return "", fmt.Errorf("manifest.json not found in run workdir or session workdir")
return false, err
}
func writeCurrentManifestSnapshot(m *manifest.Manifest, archiveMetadata map[string]any) (string, error) {