tenseleyflow/shithub / 6e7ec09

Browse files

Wire shithubd migrate + seed subcommands (replace S00 stubs)

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
6e7ec09befcd1876a2ade5d94b4b3677f3cabd11
Parents
75713ce
Tree
58df215

3 changed files

StatusFile+-
A cmd/shithubd/migrate.go 35 0
M cmd/shithubd/root.go 2 2
A cmd/shithubd/seed.go 71 0
cmd/shithubd/migrate.goadded
@@ -0,0 +1,35 @@
1
+// SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+package main
4
+
5
+import (
6
+	"github.com/spf13/cobra"
7
+
8
+	"github.com/tenseleyFlow/shithub/internal/infra/db"
9
+	_ "github.com/tenseleyFlow/shithub/internal/migrationsfs" // registers the embedded migrations FS with db
10
+)
11
+
12
+var migrateCmd = &cobra.Command{
13
+	Use:   "migrate",
14
+	Short: "Run database migrations",
15
+	Long:  "Run database migrations against SHITHUB_DATABASE_URL using goose. Subcommands: up, down, status, version, redo, reset.",
16
+}
17
+
18
+func newMigrateActionCmd(use, short string, action db.MigrateAction) *cobra.Command {
19
+	return &cobra.Command{
20
+		Use:   use,
21
+		Short: short,
22
+		RunE: func(cmd *cobra.Command, args []string) error {
23
+			return db.Migrate(cmd.Context(), db.Defaults(), action)
24
+		},
25
+	}
26
+}
27
+
28
+func init() {
29
+	migrateCmd.AddCommand(newMigrateActionCmd("up", "Apply all pending migrations", db.MigrateUp))
30
+	migrateCmd.AddCommand(newMigrateActionCmd("down", "Roll back the most recent migration", db.MigrateDown))
31
+	migrateCmd.AddCommand(newMigrateActionCmd("status", "Show applied/pending migrations", db.MigrateStatus))
32
+	migrateCmd.AddCommand(newMigrateActionCmd("version", "Show current schema version", db.MigrateVersion))
33
+	migrateCmd.AddCommand(newMigrateActionCmd("redo", "Re-apply the most recent migration", db.MigrateRedo))
34
+	migrateCmd.AddCommand(newMigrateActionCmd("reset", "Roll back all migrations (DANGEROUS)", db.MigrateReset))
35
+}
cmd/shithubd/root.gomodified
@@ -28,11 +28,11 @@ func Execute() {
2828
 func init() {
2929
 	rootCmd.AddCommand(versionCmd)
3030
 	rootCmd.AddCommand(webCmd)
31
+	rootCmd.AddCommand(migrateCmd)
32
+	rootCmd.AddCommand(seedCmd)
3133
 
3234
 	// Stubs for subcommands implemented in later sprints. They surface in
3335
 	// `--help` so the operator-facing interface is discoverable from day one.
34
-	rootCmd.AddCommand(stubCmd("migrate", "Run database migrations", "S01"))
35
-	rootCmd.AddCommand(stubCmd("seed", "Load development seed data", "S01"))
3636
 	rootCmd.AddCommand(stubCmd("worker", "Run background workers", "S14"))
3737
 	rootCmd.AddCommand(stubCmd("ssh-authkeys", "AuthorizedKeysCommand handler", "S07"))
3838
 	rootCmd.AddCommand(stubCmd("ssh-shell", "Forced SSH shell dispatcher", "S07/S13"))
cmd/shithubd/seed.goadded
@@ -0,0 +1,71 @@
1
+// SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+package main
4
+
5
+import (
6
+	"context"
7
+	"fmt"
8
+	"os"
9
+	"path/filepath"
10
+	"sort"
11
+	"strings"
12
+
13
+	"github.com/spf13/cobra"
14
+
15
+	"github.com/tenseleyFlow/shithub/internal/infra/db"
16
+)
17
+
18
+var seedCmd = &cobra.Command{
19
+	Use:   "seed",
20
+	Short: "Load development seed SQL files from seeds/dev/ in lex order",
21
+	RunE: func(cmd *cobra.Command, args []string) error {
22
+		dir, _ := cmd.Flags().GetString("dir")
23
+		return runSeed(cmd.Context(), dir)
24
+	},
25
+}
26
+
27
+func init() {
28
+	seedCmd.Flags().StringP("dir", "d", "seeds/dev", "Directory containing .sql seed files")
29
+}
30
+
31
+func runSeed(ctx context.Context, dir string) error {
32
+	pool, err := db.Open(ctx, db.Defaults())
33
+	if err != nil {
34
+		return err
35
+	}
36
+	defer pool.Close()
37
+
38
+	entries, err := os.ReadDir(dir)
39
+	if err != nil {
40
+		return fmt.Errorf("seed: read %s: %w", dir, err)
41
+	}
42
+	var files []string
43
+	for _, e := range entries {
44
+		if e.IsDir() {
45
+			continue
46
+		}
47
+		if !strings.HasSuffix(e.Name(), ".sql") {
48
+			continue
49
+		}
50
+		files = append(files, filepath.Join(dir, e.Name()))
51
+	}
52
+	sort.Strings(files)
53
+
54
+	if len(files) == 0 {
55
+		fmt.Fprintln(os.Stderr, "seed: no .sql files in", dir, "(nothing to do)")
56
+		return nil
57
+	}
58
+
59
+	for _, f := range files {
60
+		body, err := os.ReadFile(f) //nolint:gosec // operator-supplied path
61
+		if err != nil {
62
+			return fmt.Errorf("seed: read %s: %w", f, err)
63
+		}
64
+		fmt.Fprintln(os.Stderr, "seed: applying", f)
65
+		if _, err := pool.Exec(ctx, string(body)); err != nil {
66
+			return fmt.Errorf("seed: %s: %w", f, err)
67
+		}
68
+	}
69
+	fmt.Fprintln(os.Stderr, "seed: done")
70
+	return nil
71
+}