Expose pipeline profile selection and provenance

This commit is contained in:
2026-08-30 13:41:26 +00:00
parent f86b17045d
commit 4e991fa21d
22 changed files with 298 additions and 29 deletions

View File

@@ -31,6 +31,14 @@ func (f *singletonStringFlag) Set(value string) error {
return nil
}
func (f singletonStringFlag) pointer() *string {
if !f.set {
return nil
}
value := f.value
return &value
}
type singletonBoolFlag struct {
name string
value bool

View File

@@ -57,6 +57,7 @@ func TestBoundedRunParsingRejectsDuplicateSingletons(t *testing.T) {
{name: "through mixed", args: []string{"session", "--through", "analyze", "--through=publish"}, want: "--through may be specified only once"},
{name: "force separate", args: []string{"session", "--force", "--force"}, want: "--force may be specified only once"},
{name: "force equals", args: []string{"session", "--force=true", "--force=false"}, want: "--force may be specified only once"},
{name: "profile", args: []string{"session", "--profile", "production", "--profile=testing"}, want: "--profile may be specified only once"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
@@ -68,6 +69,32 @@ func TestBoundedRunParsingRejectsDuplicateSingletons(t *testing.T) {
}
}
func TestBoundedRunParsingRetainsExplicitProfilePresence(t *testing.T) {
withoutProfile, err := parseBoundedRunRequest("run", []string{"session"}, io.Discard)
if err != nil {
t.Fatal(err)
}
if got := withoutProfile.Config.sessionOptions().Profile; got != nil {
t.Fatalf("omitted profile = %#v, want nil", got)
}
withProfile, err := parseBoundedRunRequest("run", []string{"session", "--profile", "testing"}, io.Discard)
if err != nil {
t.Fatal(err)
}
if got := withProfile.Config.sessionOptions().Profile; got == nil || *got != "testing" {
t.Fatalf("explicit profile = %#v, want testing", got)
}
explicitEmpty, err := parseBoundedRunRequest("run", []string{"session", "--profile", ""}, io.Discard)
if err != nil {
t.Fatal(err)
}
if got := explicitEmpty.Config.sessionOptions().Profile; got == nil || *got != "" {
t.Fatalf("explicit empty profile = %#v, want non-nil empty", got)
}
}
func TestBoundedRunParsingUsesSharedRangeValidation(t *testing.T) {
args := []string{"session", "--from", "publish", "--through", "render"}
runRequest, runErr := parseBoundedRunRequest("run", args, io.Discard)

View File

@@ -88,7 +88,7 @@ func cleanAllLocal(flags commonConfigFlags, dryRun, clearCache bool, out io.Writ
strings.TrimSpace(flags.previousSessionID) != "" {
return fmt.Errorf("clean: --all cannot be combined with --campaign, --campaign-file, --session, a session_id, or --previous-session-id")
}
_, pipelineCfg, err := loadPipelineConfig(flags.pipelinePath)
_, pipelineCfg, err := loadPipelineConfig(flags.pipelinePath, config.PipelineLoadOptions{Profile: flags.profile.pointer()})
if err != nil {
return fmt.Errorf("clean: %w", err)
}

View File

@@ -17,7 +17,7 @@ import (
type pipelineCampaignConfig = config.LoadedPipelineCampaign
var downloadObjectToTempFn = storage.DownloadObjectToTemp
var loadPipelineConfigFn = config.LoadPipeline
var loadPipelineConfigFn = config.LoadPipelineWithOptions
type commandConfig struct {
Config *config.Config
@@ -48,7 +48,7 @@ func loadCommandConfig(ctx context.Context, pipelineFlag, campaignFlag, campaign
}
}()
base, err := loadPipelineCampaignConfig(pipelineFlag, campaignFlag, campaignFileFlag)
base, err := loadPipelineCampaignConfig(pipelineFlag, campaignFlag, campaignFileFlag, config.PipelineLoadOptions{Profile: sessionOpts.Profile})
if err != nil {
return nil, err
}
@@ -137,8 +137,8 @@ func loadCommandConfig(ctx context.Context, pipelineFlag, campaignFlag, campaign
return loaded, nil
}
func loadPipelineCampaignConfig(pipelineFlag, campaignFlag, campaignFileFlag string) (*pipelineCampaignConfig, error) {
loadedPipelinePath, pipelineCfg, err := loadPipelineConfig(pipelineFlag)
func loadPipelineCampaignConfig(pipelineFlag, campaignFlag, campaignFileFlag string, pipelineOpts config.PipelineLoadOptions) (*pipelineCampaignConfig, error) {
loadedPipelinePath, pipelineCfg, err := loadPipelineConfig(pipelineFlag, pipelineOpts)
if err != nil {
return nil, err
}
@@ -163,12 +163,12 @@ func loadPipelineCampaignConfig(pipelineFlag, campaignFlag, campaignFileFlag str
}, nil
}
func loadPipelineConfig(pipelineFlag string) (string, *config.PipelineConfig, error) {
func loadPipelineConfig(pipelineFlag string, opts config.PipelineLoadOptions) (string, *config.PipelineConfig, error) {
resolvedPipelinePath, err := resolvePipelineConfigPath(pipelineFlag)
if err != nil {
return "", nil, err
}
pipelineCfg, err := loadPipelineConfigFn(resolvedPipelinePath)
pipelineCfg, err := loadPipelineConfigFn(resolvedPipelinePath, opts)
if err != nil {
return "", nil, err
}

View File

@@ -15,8 +15,8 @@ func TestLoadCommandConfigRetainsInitiallyLoadedPipeline(t *testing.T) {
originalLoader := loadPipelineConfigFn
loadCalls := 0
loadPipelineConfigFn = func(path string) (*config.PipelineConfig, error) {
loaded, err := originalLoader(path)
loadPipelineConfigFn = func(path string, opts config.PipelineLoadOptions) (*config.PipelineConfig, error) {
loaded, err := originalLoader(path, opts)
if err != nil {
return nil, err
}

View File

@@ -0,0 +1,70 @@
package app
import (
"fmt"
"gitea.maximumdirect.net/eric/narratio/internal/config"
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
)
func effectiveConfigProvenance(cfg *config.Config) *manifest.EffectiveConfigProvenance {
if cfg == nil || cfg.Pipeline == nil {
return nil
}
provenance := &manifest.EffectiveConfigProvenance{
EffectiveConfigDigest: config.EffectivePipelineDigest(cfg.Pipeline),
}
if selected, ok := config.SelectedPipelineProfile(cfg.Pipeline); ok {
provenance.SelectedProfile = &manifest.SelectedProfileProvenance{
Name: selected.Name,
Source: selected.Source,
}
}
if provenance.SelectedProfile == nil && provenance.EffectiveConfigDigest == "" {
return nil
}
return provenance
}
func applyEffectiveConfigProvenance(m *manifest.Manifest, cfg *config.Config) {
if m != nil {
m.EffectiveConfig = effectiveConfigProvenance(cfg)
}
}
func applyEffectiveConfigProvenanceToRun(m *manifest.RunManifest, cfg *config.Config) {
if m != nil {
m.EffectiveConfig = effectiveConfigProvenance(cfg)
}
}
func effectiveConfigSummary(cfg *config.Config) string {
provenance := effectiveConfigProvenance(cfg)
if provenance == nil {
return "profile=none digest=unavailable"
}
profile := "none"
if provenance.SelectedProfile != nil {
profile = provenance.SelectedProfile.Name + " (" + provenance.SelectedProfile.Source + ")"
}
digest := provenance.EffectiveConfigDigest
if digest == "" {
digest = "unavailable"
}
return fmt.Sprintf("profile=%s digest=%s", profile, digest)
}
func persistedEffectiveConfigSummary(provenance *manifest.EffectiveConfigProvenance) string {
if provenance == nil {
return "profile=none digest=unavailable"
}
profile := "none"
if provenance.SelectedProfile != nil {
profile = provenance.SelectedProfile.Name + " (" + provenance.SelectedProfile.Source + ")"
}
digest := provenance.EffectiveConfigDigest
if digest == "" {
digest = "unavailable"
}
return fmt.Sprintf("profile=%s digest=%s", profile, digest)
}

View File

@@ -20,6 +20,7 @@ type commonConfigFlags struct {
sessionPath string
sessionID string
previousSessionID string
profile singletonStringFlag
}
func addCommonConfigFlags(fs *flag.FlagSet, flags *commonConfigFlags) {
@@ -29,12 +30,15 @@ func addCommonConfigFlags(fs *flag.FlagSet, flags *commonConfigFlags) {
fs.StringVar(&flags.sessionPath, "session", "", "path to session.yml")
fs.StringVar(&flags.sessionID, "session-id", "", "session identifier")
fs.StringVar(&flags.previousSessionID, "previous-session-id", "", "expected previous session identifier")
flags.profile.name = "profile"
fs.Var(&flags.profile, "profile", "named pipeline profile")
}
func (f commonConfigFlags) sessionOptions() config.SessionLoadOptions {
return config.SessionLoadOptions{
SessionID: f.sessionID,
PreviousSessionID: f.previousSessionID,
Profile: f.profile.pointer(),
}
}

View File

@@ -22,6 +22,7 @@ func SessionInit(ctx context.Context, args []string, out io.Writer) error {
fs := flag.NewFlagSet("session init", flag.ContinueOnError)
fs.SetOutput(io.Discard)
var pipelinePath, campaignPath, campaignFilePath, sessionID, previousSessionID, date, title, output, audioS3Prefix, audioDir string
var profile singletonStringFlag
var remote, force bool
fs.StringVar(&pipelinePath, "config", "", "path to pipeline.yml (optional; defaults searched)")
fs.StringVar(&campaignPath, "campaign", "", "campaign ID")
@@ -33,6 +34,8 @@ func SessionInit(ctx context.Context, args []string, out io.Writer) error {
fs.StringVar(&output, "output", "", "local output session.yml path")
fs.StringVar(&audioS3Prefix, "audio-s3-prefix", "", "session audio S3 prefix")
fs.StringVar(&audioDir, "audio-dir", "", "local audio directory")
profile.name = "profile"
fs.Var(&profile, "profile", "named pipeline profile")
fs.BoolVar(&remote, "remote", false, "write session.yml to S3 session prefix")
fs.BoolVar(&force, "force", false, "overwrite existing target")
if err := parseSessionAwareFlags("session init", fs, args, &sessionID); err != nil {
@@ -48,7 +51,7 @@ func SessionInit(ctx context.Context, args []string, out io.Writer) error {
return fmt.Errorf("session init: --audio-dir and --audio-s3-prefix are mutually exclusive")
}
base, err := loadPipelineCampaignConfig(pipelinePath, campaignPath, campaignFilePath)
base, err := loadPipelineCampaignConfig(pipelinePath, campaignPath, campaignFilePath, config.PipelineLoadOptions{Profile: profile.pointer()})
if err != nil {
return fmt.Errorf("session init: %w", err)
}

View File

@@ -40,6 +40,7 @@ func Status(ctx context.Context, args []string, out io.Writer) error {
fmt.Fprintf(out, "Campaign: %s\n", cfg.Session.Campaign)
fmt.Fprintf(out, "Workspace: %s\n", paths.Root)
fmt.Fprintf(out, "Session config: %s\n", sessionSourceSummary(cfg))
fmt.Fprintf(out, "Configuration (current): %s\n", effectiveConfigSummary(cfg))
writeStatusStableInputs(out, inspectStableInputs(cfg))
writeStatusLocalAudio(out, inspectLocalAudioPresence(cfg))
@@ -51,6 +52,7 @@ func Status(ctx context.Context, args []string, out io.Writer) error {
} else {
localManifest = m
fmt.Fprintf(out, "Local manifest: %s\n", paths.ManifestPath)
fmt.Fprintf(out, "Configuration (last persisted): %s\n", persistedEffectiveConfigSummary(m.EffectiveConfig))
writeStageStatuses(out, m)
}

View File

@@ -61,7 +61,7 @@ func Plan(ctx context.Context, args []string, out io.Writer) error {
runCount := 0
skipCount := 0
if _, err := fmt.Fprintf(out, "narratio session plan: read-only workdir at %s\n", paths.Root); err != nil {
if _, err := fmt.Fprintf(out, "narratio session plan: read-only workdir at %s; %s\n", paths.Root, effectiveConfigSummary(cfg)); err != nil {
return err
}
for _, selectedStage := range stages {

View File

@@ -0,0 +1,72 @@
package app
import (
"context"
"io"
"os"
"path/filepath"
"strings"
"testing"
"gitea.maximumdirect.net/eric/narratio/internal/config"
)
func TestRunProfileSelectionFlowsThroughSharedLoader(t *testing.T) {
workspaceRoot := t.TempDir()
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
enableCommandTestProfiles(t, pipelinePath)
original := executeStagesFn
t.Cleanup(func() { executeStagesFn = original })
var language string
executeStagesFn = func(_ context.Context, cfg *config.Config, _ BoundedPlan, _ RunOptions) (*RunSummary, error) {
language = cfg.Pipeline.WhisperX.Language
return &RunSummary{SessionID: cfg.Session.SessionID, ManifestPath: "manifest.json"}, nil
}
if err := Run(context.Background(), []string{
"2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--profile", "testing",
}, io.Discard); err != nil {
t.Fatal(err)
}
if language != "fr" {
t.Fatalf("explicit profile language = %q, want fr", language)
}
if err := Run(context.Background(), []string{
"2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath,
}, io.Discard); err != nil {
t.Fatal(err)
}
if language != "en" {
t.Fatalf("default profile language = %q, want en", language)
}
for _, profile := range []string{"unknown", ""} {
err := Run(context.Background(), []string{
"2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--profile", profile,
}, io.Discard)
if err == nil || !strings.Contains(err.Error(), "profile") {
t.Fatalf("profile %q error = %v, want selection rejection", profile, err)
}
}
}
func enableCommandTestProfiles(t *testing.T, pipelinePath string) {
t.Helper()
data, err := os.ReadFile(pipelinePath)
if err != nil {
t.Fatal(err)
}
composition := "composition:\n default_profile: production\n profiles:\n production:\n overlay: production.yml\n testing:\n overlay: testing.yml\n"
if err := os.WriteFile(pipelinePath, append([]byte(composition), data...), 0o644); err != nil {
t.Fatal(err)
}
dir := filepath.Dir(pipelinePath)
if err := os.WriteFile(filepath.Join(dir, "production.yml"), []byte("whisperx:\n language: en\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "testing.yml"), []byte("whisperx:\n language: fr\n"), 0o644); err != nil {
t.Fatal(err)
}
}

View File

@@ -24,6 +24,7 @@ func TestRegenerateArtifactsForwardsExactCanonicalRunArguments(t *testing.T) {
"--artifacts=player_handout",
"--config", "pipeline.yml",
"--campaign", "sample-campaign",
"--profile", "testing",
}, io.Discard, io.Discard)
if code != 0 {
t.Fatalf("Execute() code = %d, want 0", code)
@@ -34,6 +35,7 @@ func TestRegenerateArtifactsForwardsExactCanonicalRunArguments(t *testing.T) {
"--artifacts=player_handout",
"--config", "pipeline.yml",
"--campaign", "sample-campaign",
"--profile", "testing",
}
if !reflect.DeepEqual(captured, want) {
t.Fatalf("forwarded args = %#v, want %#v", captured, want)

View File

@@ -44,11 +44,12 @@ func Run(ctx context.Context, args []string, out io.Writer) error {
_, err = fmt.Fprintf(
out,
"narratio run: session %s; executed=%d skipped=%d; manifest=%s\n",
"narratio run: session %s; executed=%d skipped=%d; manifest=%s; %s\n",
summary.SessionID,
len(summary.Executed),
len(summary.Skipped),
summary.ManifestPath,
effectiveConfigSummary(cfg),
)
return err
}

View File

@@ -147,6 +147,7 @@ func executePlan(ctx context.Context, cfg *config.Config, plan BoundedPlan, opts
return nil, fmt.Errorf("validate bounded run prerequisites under session lock: %w", err)
}
identity.applyToSessionManifest(m)
applyEffectiveConfigProvenance(m, cfg)
if err := env.ManifestStore.Save(ctx, manifestPath, m); err != nil {
return nil, fmt.Errorf("save manifest identity %q: %w", manifestPath, err)
}
@@ -172,6 +173,7 @@ func executePlan(ctx context.Context, cfg *config.Config, plan BoundedPlan, opts
return nil, fmt.Errorf("create run manifest: %w", err)
}
identity.applyToRunManifest(runManifest, manifestPath)
applyEffectiveConfigProvenanceToRun(runManifest, cfg)
if err := runManifestStore.SaveRun(ctx, runManifestPath, runManifest); err != nil {
return nil, persistTerminalFailure(
ctx, env.ManifestStore, manifestPath, m, runManifestStore, runManifestPath, runManifest,