@@ -55,13 +55,15 @@ type migrationRepairDirtyResult struct {
5555
5656func runMigrations (args []string ) error {
5757 if len (args ) == 0 {
58- return fmt .Errorf ("usage: wfctl migrations <validate|status|ci-check|repair-dirty>" )
58+ return fmt .Errorf ("usage: wfctl migrations <validate|status|up| ci-check|repair-dirty>" )
5959 }
6060 switch args [0 ] {
6161 case "validate" :
6262 return runMigrationsValidate (args [1 :])
6363 case "status" :
6464 return runMigrationsStatus (args [1 :])
65+ case "up" :
66+ return runMigrationsUp (args [1 :])
6567 case "ci-check" :
6668 return runMigrationsCICheck (args [1 :])
6769 case "repair-dirty" :
@@ -107,6 +109,38 @@ func runMigrationsStatus(args []string) error {
107109 return nil
108110}
109111
112+ func runMigrationsUp (args []string ) error {
113+ fs := flag .NewFlagSet ("migrations up" , flag .ContinueOnError )
114+ configFile := fs .String ("config" , "app.yaml" , "Workflow config file" )
115+ fs .StringVar (configFile , "c" , "app.yaml" , "Config file (short for --config)" )
116+ envName := fs .String ("env" , "" , "Environment name" )
117+ pluginDir := fs .String ("plugin-dir" , defaultMigrationPluginDir (), "Plugin directory" )
118+ format := fs .String ("format" , "text" , "Output format: text or json" )
119+ if err := fs .Parse (args ); err != nil {
120+ return err
121+ }
122+
123+ cfg , err := loadMigrationWorkflowConfig (* configFile )
124+ if err != nil {
125+ return fmt .Errorf ("load config: %w" , err )
126+ }
127+ migrations , err := resolveMigrationConfigs (cfg , * envName )
128+ if err != nil {
129+ return err
130+ }
131+ result , err := applyMigrationsForConfigs (context .Background (), migrations , * pluginDir )
132+ if writeErr := writeMigrationStatusOutput (result , * format , "migrations up" ); writeErr != nil {
133+ return writeErr
134+ }
135+ if err != nil {
136+ return err
137+ }
138+ if result .Decision == "fail" {
139+ return errors .New (strings .Join (result .Reasons , "; " ))
140+ }
141+ return nil
142+ }
143+
110144func runMigrationsCICheck (args []string ) error {
111145 fs := flag .NewFlagSet ("migrations ci-check" , flag .ContinueOnError )
112146 configFile := fs .String ("config" , "app.yaml" , "Workflow config file" )
@@ -567,6 +601,66 @@ func collectMigrationStatusForConfigs(ctx context.Context, migrations []resolved
567601 return result , errors .Join (errs ... )
568602}
569603
604+ func applyMigrationsForConfigs (ctx context.Context , migrations []resolvedMigrationConfig , pluginDir string ) (migrationStatusResult , error ) {
605+ result := migrationStatusResult {
606+ Decision : "pass" ,
607+ Destructive : false ,
608+ HumanApprovalRequired : false ,
609+ Migrations : make ([]migrationValidationRecord , 0 , len (migrations )),
610+ }
611+ runner := newMigrationPluginRunner ()
612+ var errs []error
613+ for _ , migration := range migrations {
614+ runCfg := migrationPluginRunConfig {
615+ Plugin : migration .Plugin ,
616+ PluginDir : pluginDir ,
617+ Driver : migration .Driver ,
618+ SourceDir : migration .SourceDir ,
619+ DSN : migration .DSN ,
620+ }
621+ record := migrationValidationRecord {Name : migration .Name , Driver : migration .Driver }
622+ if _ , err := runner .run (ctx , runCfg , "up" ); err != nil {
623+ reason := fmt .Sprintf ("migration %s up failed: %s" , migration .Name , redactMigrationDSN (err .Error (), migration .DSN ))
624+ result .Reasons = append (result .Reasons , reason )
625+ record .Error = reason
626+ result .Migrations = append (result .Migrations , record )
627+ errs = append (errs , errors .New (reason ))
628+ continue
629+ }
630+ statusOutput , err := runner .run (ctx , runCfg , "status" )
631+ if err != nil {
632+ reason := fmt .Sprintf ("migration %s post-up status failed: %s" , migration .Name , redactMigrationDSN (err .Error (), migration .DSN ))
633+ result .Reasons = append (result .Reasons , reason )
634+ record .Error = reason
635+ result .Migrations = append (result .Migrations , record )
636+ errs = append (errs , errors .New (reason ))
637+ continue
638+ }
639+ status , err := parseMigrationStatus (statusOutput .Stdout )
640+ if err != nil {
641+ reason := fmt .Sprintf ("migration %s post-up status failed: %s" , migration .Name , redactMigrationDSN (err .Error (), migration .DSN ))
642+ result .Reasons = append (result .Reasons , reason )
643+ record .Error = reason
644+ result .Migrations = append (result .Migrations , record )
645+ errs = append (errs , errors .New (reason ))
646+ continue
647+ }
648+ record .Current = status .Current
649+ record .Dirty = status .Dirty
650+ record .Pending = status .Pending
651+ for _ , reason := range migrationStatusCleanReasons ([]migrationValidationRecord {record }) {
652+ reason = strings .Replace (reason , " is dirty " , " is dirty after up " , 1 )
653+ result .Reasons = append (result .Reasons , reason )
654+ errs = append (errs , errors .New (reason ))
655+ }
656+ result .Migrations = append (result .Migrations , record )
657+ }
658+ if len (result .Reasons ) > 0 {
659+ result .Decision = "fail"
660+ }
661+ return result , errors .Join (errs ... )
662+ }
663+
570664func migrationCurrentOrUnknown (current string ) string {
571665 current = strings .TrimSpace (current )
572666 if current == "" {
0 commit comments