Monorepo for Tangled
tangled.org
1package spindle
2
3import (
4 "context"
5 "database/sql"
6 "errors"
7 "fmt"
8 "log/slog"
9 "os"
10
11 _ "github.com/mattn/go-sqlite3"
12 "tangled.org/core/spindle/db"
13)
14
15const forceTapResyncFlag = "force-tap-repo-resync-v1"
16
17func runStartupMigrations(ctx context.Context, d *db.DB, tapEmbed bool, tapDBPath string, logger *slog.Logger) error {
18 if err := cleanupOrphanRepos(ctx, d, logger); err != nil {
19 return fmt.Errorf("cleanup orphan repos: %w", err)
20 }
21 if !tapEmbed {
22 logger.Warn("tap not embedded: legacy repos won't auto-resync; trigger external tap resync to migrate secrets/casbin")
23 return nil
24 }
25 if err := nudgeTapForResync(ctx, d, tapDBPath, logger); err != nil {
26 return fmt.Errorf("nudge tap for resync: %w", err)
27 }
28 return nil
29}
30
31func cleanupOrphanRepos(ctx context.Context, d *db.DB, logger *slog.Logger) error {
32 res, err := d.ExecContext(ctx, `
33 delete from repos
34 where coalesce(repo_did, '') = ''
35 and exists (
36 select 1 from repos r2
37 where r2.owner = repos.owner
38 and coalesce(r2.repo_did, '') <> ''
39 )
40 `)
41 if err != nil {
42 return fmt.Errorf("delete orphan repos: %w", err)
43 }
44 n, _ := res.RowsAffected()
45 if n > 0 {
46 logger.Info("cleaned up orphan repos missing repo_did", "deleted", n)
47 }
48 return nil
49}
50
51func nudgeTapForResync(ctx context.Context, d *db.DB, tapDBPath string, logger *slog.Logger) error {
52 if tapDBPath == "" {
53 return fmt.Errorf("tap db path empty in embed mode")
54 }
55 var exists bool
56 if err := d.QueryRowContext(ctx,
57 `select exists (select 1 from migrations where name = ?)`,
58 forceTapResyncFlag,
59 ).Scan(&exists); err != nil {
60 return fmt.Errorf("check %s flag: %w", forceTapResyncFlag, err)
61 }
62 if exists {
63 logger.Warn("skipped migration, already applied", "migration", forceTapResyncFlag)
64 return nil
65 }
66
67 markDone := func() error {
68 if _, err := d.ExecContext(ctx,
69 `insert or ignore into migrations (name) values (?)`,
70 forceTapResyncFlag,
71 ); err != nil {
72 return fmt.Errorf("mark %s done: %w", forceTapResyncFlag, err)
73 }
74 return nil
75 }
76
77 if _, err := os.Stat(tapDBPath); errors.Is(err, os.ErrNotExist) {
78 logger.Info("tap db not yet created, marking resync nudge done", "migration", forceTapResyncFlag, "path", tapDBPath)
79 return markDone()
80 } else if err != nil {
81 return fmt.Errorf("stat tap db: %w", err)
82 }
83
84 tdb, err := sql.Open("sqlite3", tapDBPath+"?_busy_timeout=5000")
85 if err != nil {
86 return fmt.Errorf("open tap db: %w", err)
87 }
88 defer tdb.Close()
89
90 if _, err := tdb.ExecContext(ctx, `delete from repo_records`); err != nil {
91 return fmt.Errorf("clear tap repo_records: %w", err)
92 }
93 res, err := tdb.ExecContext(ctx,
94 `update repos set state = 'desynchronized', retry_after = 0 where state in ('active','error')`,
95 )
96 if err != nil {
97 return fmt.Errorf("desync tap repos: %w", err)
98 }
99 n, _ := res.RowsAffected()
100
101 if err := markDone(); err != nil {
102 return err
103 }
104 logger.Info("nudged tap to resync", "migration", forceTapResyncFlag, "repos_desynced", n)
105 return nil
106}