Rename archive config and stage contract to publish

This commit is contained in:
2026-05-23 04:30:45 +00:00
parent f050b9dd54
commit df2c765b7f
24 changed files with 273 additions and 257 deletions

View File

@@ -34,7 +34,7 @@ var archivePrerequisiteStages = []string{
"analyze",
}
func (archiveStage) Name() string { return "archive" }
func (archiveStage) Name() string { return "publish" }
func (archiveStage) Declares() IODecl {
return IODecl{
@@ -46,13 +46,13 @@ func (archiveStage) Declares() IODecl {
func (archiveStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*StageResult, error) {
if env == nil || env.Config == nil || env.Config.Pipeline == nil || env.Config.Session == nil {
return nil, fmt.Errorf("archive: resolved config must include pipeline and session")
return nil, fmt.Errorf("publish: resolved config must include pipeline and session")
}
if archiveDisabled(env) {
return &StageResult{
Metadata: map[string]any{
"stage": "archive",
"stage": "publish",
"skipped": true,
"archive_enabled": false,
"audio_upload_skipped": true,
@@ -63,7 +63,7 @@ func (archiveStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
if archiveRunUploadDisabled(env) {
return &StageResult{
Metadata: map[string]any{
"stage": "archive",
"stage": "publish",
"skipped": true,
"upload_run_enabled": false,
"audio_upload_skipped": true,
@@ -73,76 +73,76 @@ func (archiveStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
}
if err := validateArchivePrerequisites(m); err != nil {
return nil, fmt.Errorf("archive: %w", err)
return nil, fmt.Errorf("publish: %w", err)
}
if env.ObjectStore == nil {
return nil, fmt.Errorf("archive: remote object store backend is required when archive run upload is enabled")
return nil, fmt.Errorf("publish: remote object store backend is required when publish run upload is enabled")
}
runRoot, err := resolveArchiveRunRoot(env, m)
if err != nil {
return nil, fmt.Errorf("archive: resolve run root: %w", err)
return nil, fmt.Errorf("publish: resolve run root: %w", err)
}
runRootInfo, err := os.Stat(runRoot)
if err != nil {
return nil, fmt.Errorf("archive: run root %q: %w", runRoot, err)
return nil, fmt.Errorf("publish: run root %q: %w", runRoot, err)
}
if !runRootInfo.IsDir() {
return nil, fmt.Errorf("archive: run root %q is not a directory", runRoot)
return nil, fmt.Errorf("publish: run root %q is not a directory", runRoot)
}
runPrefix, err := artifacts.ResolveArchiveRunPrefix(env.Config, m)
if err != nil {
return nil, fmt.Errorf("archive: resolve s3 run prefix: %w", err)
return nil, fmt.Errorf("publish: resolve s3 run prefix: %w", err)
}
sessionPrefix, err := artifacts.ResolveArchiveSessionPrefix(env.Config, m)
if err != nil {
return nil, fmt.Errorf("archive: resolve s3 session prefix: %w", err)
return nil, fmt.Errorf("publish: resolve s3 session prefix: %w", err)
}
bucket := artifacts.ResolveArchiveBucket(env.Config, m)
if bucket == "" {
return nil, fmt.Errorf("archive: resolve s3 bucket: bucket is required")
return nil, fmt.Errorf("publish: resolve s3 bucket: bucket is required")
}
runID := strings.TrimSpace(m.RunID)
if runID == "" {
return nil, fmt.Errorf("archive: run id is required")
return nil, fmt.Errorf("publish: run id is required")
}
manifestSource, err := resolveArchiveRunManifestSource(runRoot)
if err != nil {
return nil, fmt.Errorf("archive: resolve run manifest source: %w", err)
return nil, fmt.Errorf("publish: resolve run manifest source: %w", err)
}
runFiles, err := collectArchiveRunFiles(runRoot, manifestSource)
if err != nil {
return nil, fmt.Errorf("archive: collect run files: %w", err)
return nil, fmt.Errorf("publish: collect run files: %w", err)
}
sessionPaths := archiveSessionPaths(env, m)
previousFiles, err := collectArchivePreviousFiles(sessionPaths.PreviousDir)
if err != nil {
return nil, fmt.Errorf("archive: collect previous files: %w", err)
return nil, fmt.Errorf("publish: collect previous files: %w", err)
}
runtimeCatalog, err := buildArchiveRuntimeArtifactCatalog(sessionPaths, env.Config.Pipeline.Scriptorium)
if err != nil {
return nil, fmt.Errorf("archive: build runtime artifact catalog: %w", err)
return nil, fmt.Errorf("publish: build runtime artifact catalog: %w", err)
}
promotions, skippedOptional, skippedUnselected, lockedPromotions, err := resolveArchivePromotions(
sessionPaths,
m,
runtimeCatalog,
env.Config.Pipeline.Archive.PromoteArtifacts,
env.Config.Pipeline.Archive.Locks,
env.Config.Pipeline.Publish.PromoteArtifacts,
env.Config.Pipeline.Publish.Locks,
env.SelectedArtifactKeys,
sessionPrefix,
)
if err != nil {
return nil, fmt.Errorf("archive: resolve promotion rules: %w", err)
return nil, fmt.Errorf("publish: resolve promotion rules: %w", err)
}
runUploaded := make([]string, 0, len(runFiles))
for _, file := range runFiles {
key := artifacts.S3RunRelativeDestinationKey(runPrefix, file.RelativePath)
if _, err := env.ObjectStore.Upload(ctx, file.LocalPath, key, storage.UploadOptions{}); err != nil {
return nil, fmt.Errorf("archive: upload run file %q to %q: %w", file.RelativePath, key, err)
return nil, fmt.Errorf("publish: upload run file %q to %q: %w", file.RelativePath, key, err)
}
runUploaded = append(runUploaded, file.RelativePath)
}
@@ -151,7 +151,7 @@ func (archiveStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
for _, promotion := range promotions {
key := artifacts.S3PromotedArtifactKey(sessionPrefix, promotion.Dest)
if _, err := env.ObjectStore.Upload(ctx, promotion.LocalPath, key, storage.UploadOptions{}); err != nil {
return nil, fmt.Errorf("archive: upload promoted output source %q to %q: %w", promotion.Source, key, err)
return nil, fmt.Errorf("publish: upload promoted output source %q to %q: %w", promotion.Source, key, err)
}
promotedUploaded = append(promotedUploaded, promotion.Dest)
}
@@ -160,7 +160,7 @@ func (archiveStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
for _, file := range previousFiles {
key := artifacts.S3PromotedArtifactKey(sessionPrefix, file.RelativePath)
if _, err := env.ObjectStore.Upload(ctx, file.LocalPath, key, storage.UploadOptions{}); err != nil {
return nil, fmt.Errorf("archive: upload previous file %q to %q: %w", file.RelativePath, key, err)
return nil, fmt.Errorf("publish: upload previous file %q to %q: %w", file.RelativePath, key, err)
}
previousUploaded = append(previousUploaded, file.RelativePath)
}
@@ -179,31 +179,31 @@ func (archiveStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
currentManifestKey,
))
if err != nil {
return nil, fmt.Errorf("archive: build current manifest snapshot: %w", err)
return nil, fmt.Errorf("publish: build current manifest snapshot: %w", err)
}
defer func() { _ = os.Remove(manifestTempPath) }()
if _, err := env.ObjectStore.Upload(ctx, manifestTempPath, currentManifestKey, storage.UploadOptions{
ContentType: "application/json",
}); err != nil {
return nil, fmt.Errorf("archive: upload current manifest to %q: %w", currentManifestKey, err)
return nil, fmt.Errorf("publish: upload current manifest to %q: %w", currentManifestKey, err)
}
runIDTempPath, err := writeCurrentRunIDPointer(runID)
if err != nil {
return nil, fmt.Errorf("archive: build current run id pointer: %w", err)
return nil, fmt.Errorf("publish: build current run id pointer: %w", err)
}
defer func() { _ = os.Remove(runIDTempPath) }()
if _, err := env.ObjectStore.Upload(ctx, runIDTempPath, currentRunPointerKey, storage.UploadOptions{
ContentType: "text/plain; charset=utf-8",
}); err != nil {
return nil, fmt.Errorf("archive: upload current run pointer to %q: %w", currentRunPointerKey, err)
return nil, fmt.Errorf("publish: upload current run pointer to %q: %w", currentRunPointerKey, err)
}
return &StageResult{
Metadata: map[string]any{
"stage": "archive",
"stage": "publish",
"uploaded": true,
"s3_bucket": bucket,
"s3_run_prefix": runPrefix,
@@ -250,7 +250,7 @@ type archiveSkippedUnselectedPromotion struct {
}
func archiveDisabled(env *Env) bool {
cfg := env.Config.Pipeline.Archive
cfg := env.Config.Pipeline.Publish
if cfg == nil {
return true
}
@@ -258,7 +258,7 @@ func archiveDisabled(env *Env) bool {
}
func archiveRunUploadDisabled(env *Env) bool {
cfg := env.Config.Pipeline.Archive
cfg := env.Config.Pipeline.Publish
if cfg == nil {
return true
}
@@ -715,8 +715,8 @@ func writeCurrentManifestSnapshot(m *manifest.Manifest, archiveMetadata map[stri
}
now := time.Now().UTC()
clone.MarkStageSucceeded("archive", now, nil)
if sr := clone.Stages["archive"]; sr != nil {
clone.MarkStageSucceeded("publish", now, nil)
if sr := clone.Stages["publish"]; sr != nil {
sr.Metadata = archiveMetadata
}
@@ -768,7 +768,7 @@ func archiveMetadataPreview(
currentManifestKey string,
) map[string]any {
return map[string]any{
"stage": "archive",
"stage": "publish",
"uploaded": true,
"s3_bucket": bucket,
"s3_run_prefix": runPrefix,

View File

@@ -19,7 +19,7 @@ import (
func TestArchiveSkipsWhenDisabled(t *testing.T) {
env, m, _ := archiveFixture(t)
env.Config.Pipeline.Archive.Enabled = boolPtr(false)
env.Config.Pipeline.Publish.Enabled = boolPtr(false)
result, err := archiveStage{}.Run(context.Background(), env, m)
if err != nil {
@@ -35,7 +35,7 @@ func TestArchiveSkipsWhenDisabled(t *testing.T) {
func TestArchiveSkipsRunUploadWhenDisabled(t *testing.T) {
env, m, _ := archiveFixture(t)
env.Config.Pipeline.Archive.UploadRun = boolPtr(false)
env.Config.Pipeline.Publish.UploadRun = boolPtr(false)
result, err := archiveStage{}.Run(context.Background(), env, m)
if err != nil {
@@ -179,7 +179,7 @@ func TestArchiveToleratesMissingPreviousCache(t *testing.T) {
func TestArchiveUsesCustomPromotionRules(t *testing.T) {
env, m, _ := archiveFixture(t)
env.Config.Pipeline.Archive.PromoteArtifacts = []config.ArchivePromotionRule{
env.Config.Pipeline.Publish.PromoteArtifacts = []config.ArchivePromotionRule{
{Source: "narratio.transcript.final_trimmed", Dest: "published/trimmed.json", Required: boolPtr(true)},
{Source: "narratio.artifact.session_recap", Dest: "published/recap.md", Required: boolPtr(true)},
}
@@ -200,7 +200,7 @@ func TestArchiveUsesCustomPromotionRules(t *testing.T) {
func TestArchiveSkipsOptionalMissingPromotion(t *testing.T) {
env, m, _ := archiveFixture(t)
env.Config.Pipeline.Archive.PromoteArtifacts = []config.ArchivePromotionRule{
env.Config.Pipeline.Publish.PromoteArtifacts = []config.ArchivePromotionRule{
{Source: "narratio.transcript.final_trimmed", Dest: "transcripts/final.trimmed.json", Required: boolPtr(true)},
{Source: "narratio.transcript.base", Dest: "transcripts/base.json", Required: boolPtr(false)},
}
@@ -272,7 +272,7 @@ func TestArchiveSelectedConfiguredPromotionStillFailsWhenMissing(t *testing.T) {
func TestArchiveLockedSelectedPromotionSkipsAsLocked(t *testing.T) {
env, m, _ := archiveFixture(t)
env.SelectedArtifactKeys = []string{"session_recap"}
env.Config.Pipeline.Archive.Locks = []config.ArchiveLockRule{
env.Config.Pipeline.Publish.Locks = []config.ArchiveLockRule{
{Source: "narratio.artifact.session_recap", Reason: "reviewed"},
}
fake := env.ObjectStore.(*storage.FakeBackend)
@@ -301,7 +301,7 @@ func TestArchiveLockedUnselectedConfiguredPromotionSkipsAsUnselected(t *testing.
PromptID: "dnd.player_handout",
OutputPath: "artifacts/player_handout.md",
}
env.Config.Pipeline.Archive.Locks = []config.ArchiveLockRule{
env.Config.Pipeline.Publish.Locks = []config.ArchiveLockRule{
{Source: "narratio.artifact.session_recap", Reason: "reviewed"},
}
@@ -320,7 +320,7 @@ func TestArchiveLockedUnselectedConfiguredPromotionSkipsAsUnselected(t *testing.
func TestArchiveSkipsLockedRequiredPromotionAndCommits(t *testing.T) {
env, m, _ := archiveFixture(t)
env.Config.Pipeline.Archive.Locks = []config.ArchiveLockRule{
env.Config.Pipeline.Publish.Locks = []config.ArchiveLockRule{
{Source: "narratio.transcript.final_trimmed", Reason: "human reviewed"},
}
fake := env.ObjectStore.(*storage.FakeBackend)
@@ -374,7 +374,7 @@ func TestArchiveSkipsLockedRequiredPromotionAndCommits(t *testing.T) {
t.Fatalf("unmarshal current manifest: %v", err)
}
stages := current["stages"].(map[string]any)
archive := stages["archive"].(map[string]any)
archive := stages["publish"].(map[string]any)
meta := archive["metadata"].(map[string]any)
if meta["locked_promotion_count"] != float64(1) {
t.Fatalf("current manifest locked_promotion_count = %#v, want 1", meta["locked_promotion_count"])
@@ -387,10 +387,10 @@ func TestArchiveSkipsLockedRequiredPromotionAndCommits(t *testing.T) {
func TestArchiveLockedRequiredMissingPromotionSucceeds(t *testing.T) {
env, m, _ := archiveFixture(t)
env.Config.Pipeline.Archive.PromoteArtifacts = []config.ArchivePromotionRule{
env.Config.Pipeline.Publish.PromoteArtifacts = []config.ArchivePromotionRule{
{Source: "narratio.transcript.base", Dest: "transcripts/base.json", Required: boolPtr(true)},
}
env.Config.Pipeline.Archive.Locks = []config.ArchiveLockRule{
env.Config.Pipeline.Publish.Locks = []config.ArchiveLockRule{
{Source: "narratio.transcript.base", Reason: "manual merge is locked"},
}
@@ -418,7 +418,7 @@ func TestArchiveLockedRequiredMissingPromotionSucceeds(t *testing.T) {
func TestArchiveLockDoesNotOverwriteExistingPromotion(t *testing.T) {
env, m, _ := archiveFixture(t)
env.Config.Pipeline.Archive.Locks = []config.ArchiveLockRule{
env.Config.Pipeline.Publish.Locks = []config.ArchiveLockRule{
{Source: "narratio.transcript.final_trimmed", Reason: "already published"},
}
fake := env.ObjectStore.(*storage.FakeBackend)
@@ -442,7 +442,7 @@ func TestArchiveLockDoesNotOverwriteExistingPromotion(t *testing.T) {
func TestArchiveFailsWhenRequiredPromotionMissing(t *testing.T) {
env, m, _ := archiveFixture(t)
env.Config.Pipeline.Archive.PromoteArtifacts = []config.ArchivePromotionRule{
env.Config.Pipeline.Publish.PromoteArtifacts = []config.ArchivePromotionRule{
{Source: "narratio.transcript.base", Dest: "transcripts/base.json", Required: boolPtr(true)},
}
@@ -563,7 +563,7 @@ func archiveFixture(t *testing.T) (*Env, *manifest.Manifest, string) {
RootPrefix: "dnd",
},
},
Archive: &config.ArchiveConfig{
Publish: &config.ArchiveConfig{
Enabled: boolPtr(true),
UploadRun: boolPtr(true),
PromoteArtifacts: []config.ArchivePromotionRule{

View File

@@ -61,7 +61,7 @@ func TestStagesReturnExpectedMetadata(t *testing.T) {
RootPrefix: "dnd",
},
},
Archive: &config.ArchiveConfig{
Publish: &config.ArchiveConfig{
Enabled: boolPtr(true),
UploadRun: boolPtr(true),
},
@@ -195,9 +195,9 @@ func TestStagesReturnExpectedMetadata(t *testing.T) {
}
continue
}
if s.Name() == "archive" {
if result.Metadata["stage"] != "archive" {
t.Fatalf("archive metadata = %#v, want stage=archive", result.Metadata)
if s.Name() == "publish" {
if result.Metadata["stage"] != "publish" {
t.Fatalf("archive metadata = %#v, want stage=publish", result.Metadata)
}
if result.Metadata["uploaded"] != true {
t.Fatalf("archive metadata = %#v, want uploaded=true", result.Metadata)