Monorepo for Tangled
1package main
2
3import (
4 "database/sql"
5 "flag"
6 "fmt"
7 "log"
8 "math/rand"
9 "time"
10
11 "github.com/bluesky-social/indigo/atproto/syntax"
12 _ "github.com/mattn/go-sqlite3"
13)
14
15var (
16 dbPath = flag.String("db", "appview.db", "Path to SQLite database")
17 count = flag.Int("count", 10, "Number of pipeline runs to generate")
18 repo = flag.String("repo", "", "Repository name (e.g., 'did:plc:xyz/myrepo')")
19 knot = flag.String("knot", "localhost:8100", "Knot hostname")
20)
21
22// StatusKind represents the status of a workflow
23type StatusKind string
24
25const (
26 StatusKindPending StatusKind = "pending"
27 StatusKindRunning StatusKind = "running"
28 StatusKindFailed StatusKind = "failed"
29 StatusKindTimeout StatusKind = "timeout"
30 StatusKindCancelled StatusKind = "cancelled"
31 StatusKindSuccess StatusKind = "success"
32)
33
34var finishStatuses = []StatusKind{
35 StatusKindFailed,
36 StatusKindTimeout,
37 StatusKindCancelled,
38 StatusKindSuccess,
39}
40
41// generateRandomSha generates a random 40-character SHA
42func generateRandomSha() string {
43 const hexChars = "0123456789abcdef"
44 sha := make([]byte, 40)
45 for i := range sha {
46 sha[i] = hexChars[rand.Intn(len(hexChars))]
47 }
48 return string(sha)
49}
50
51// generateRkey generates a TID-like rkey
52func generateRkey() string {
53 // Simple timestamp-based rkey
54 now := time.Now().UnixMicro()
55 return fmt.Sprintf("%d", now)
56}
57
58func main() {
59 flag.Parse()
60
61 if *repo == "" {
62 log.Fatal("--repo is required (format: did:plc:xyz/reponame)")
63 }
64
65 // Parse repo into owner and name
66 did, repoName, ok := parseRepo(*repo)
67 if !ok {
68 log.Fatalf("Invalid repo format: %s (expected: did:plc:xyz/reponame)", *repo)
69 }
70
71 db, err := sql.Open("sqlite3", *dbPath)
72 if err != nil {
73 log.Fatalf("Failed to open database: %v", err)
74 }
75 defer db.Close()
76
77 rand.Seed(time.Now().UnixNano())
78
79 branches := []string{"main", "develop", "feature/auth", "fix/bugs"}
80 workflows := []string{"test", "build", "lint", "deploy"}
81
82 log.Printf("Generating %d pipeline runs for %s...\n", *count, *repo)
83
84 for i := 0; i < *count; i++ {
85 // Random trigger type
86 isPush := rand.Float32() > 0.3 // 70% push, 30% PR
87
88 var triggerId int64
89 if isPush {
90 triggerId, err = createPushTrigger(db, branches)
91 } else {
92 triggerId, err = createPRTrigger(db, branches)
93 }
94 if err != nil {
95 log.Fatalf("Failed to create trigger: %v", err)
96 }
97
98 // Create pipeline
99 pipelineRkey := generateRkey()
100 sha := generateRandomSha()
101 createdTime := time.Now().Add(-time.Duration(rand.Intn(7*24*60)) * time.Minute) // Random time in last week
102
103 _, err = db.Exec(`
104 INSERT INTO pipelines (knot, rkey, repo_owner, repo_name, sha, created, trigger_id)
105 VALUES (?, ?, ?, ?, ?, ?, ?)
106 `, *knot, pipelineRkey, did, repoName, sha, createdTime.Format(time.RFC3339), triggerId)
107
108 if err != nil {
109 log.Fatalf("Failed to create pipeline: %v", err)
110 }
111
112 // Create workflow statuses
113 numWorkflows := rand.Intn(len(workflows)-1) + 2 // 2-4 workflows
114 selectedWorkflows := make([]string, numWorkflows)
115 perm := rand.Perm(len(workflows))
116 for j := 0; j < numWorkflows; j++ {
117 selectedWorkflows[j] = workflows[perm[j]]
118 }
119
120 for _, workflow := range selectedWorkflows {
121 err = createWorkflowStatuses(db, *knot, pipelineRkey, workflow, createdTime)
122 if err != nil {
123 log.Fatalf("Failed to create workflow statuses: %v", err)
124 }
125 }
126
127 log.Printf("Created pipeline %d/%d (rkey: %s)\n", i+1, *count, pipelineRkey)
128
129 // Small delay to ensure unique rkeys
130 time.Sleep(2 * time.Millisecond)
131 }
132
133 log.Println("✓ Pipeline population complete!")
134}
135
136func parseRepo(repo string) (syntax.DID, string, bool) {
137 // Simple parser for "did:plc:xyz/reponame"
138 for i := 0; i < len(repo); i++ {
139 if repo[i] == '/' {
140 did := syntax.DID(repo[:i])
141 name := repo[i+1:]
142 if did != "" && name != "" {
143 return did, name, true
144 }
145 }
146 }
147 return "", "", false
148}
149
150func createPushTrigger(db *sql.DB, branches []string) (int64, error) {
151 branch := branches[rand.Intn(len(branches))]
152 oldSha := generateRandomSha()
153 newSha := generateRandomSha()
154
155 result, err := db.Exec(`
156 INSERT INTO triggers (kind, push_ref, push_new_sha, push_old_sha)
157 VALUES (?, ?, ?, ?)
158 `, "push", "refs/heads/"+branch, newSha, oldSha)
159
160 if err != nil {
161 return 0, err
162 }
163
164 return result.LastInsertId()
165}
166
167func createPRTrigger(db *sql.DB, branches []string) (int64, error) {
168 targetBranch := branches[0] // Usually main
169 sourceBranch := branches[rand.Intn(len(branches)-1)+1]
170 sourceSha := generateRandomSha()
171 actions := []string{"opened", "synchronize", "reopened"}
172 action := actions[rand.Intn(len(actions))]
173
174 result, err := db.Exec(`
175 INSERT INTO triggers (kind, pr_source_branch, pr_target_branch, pr_source_sha, pr_action)
176 VALUES (?, ?, ?, ?, ?)
177 `, "pull_request", sourceBranch, targetBranch, sourceSha, action)
178
179 if err != nil {
180 return 0, err
181 }
182
183 return result.LastInsertId()
184}
185
186func createWorkflowStatuses(db *sql.DB, knot, pipelineRkey, workflow string, startTime time.Time) error {
187 // Generate a progression of statuses for the workflow
188 statusProgression := []StatusKind{StatusKindPending, StatusKindRunning}
189
190 // Randomly choose a final status (80% success, 10% failed, 5% timeout, 5% cancelled)
191 roll := rand.Float32()
192 var finalStatus StatusKind
193 switch {
194 case roll < 0.80:
195 finalStatus = StatusKindSuccess
196 case roll < 0.90:
197 finalStatus = StatusKindFailed
198 case roll < 0.95:
199 finalStatus = StatusKindTimeout
200 default:
201 finalStatus = StatusKindCancelled
202 }
203
204 statusProgression = append(statusProgression, finalStatus)
205
206 currentTime := startTime
207 for i, status := range statusProgression {
208 rkey := fmt.Sprintf("%s-%s-%d", pipelineRkey, workflow, i)
209
210 // Add some realistic time progression (10-60 seconds between statuses)
211 if i > 0 {
212 currentTime = currentTime.Add(time.Duration(rand.Intn(50)+10) * time.Second)
213 }
214
215 var errorMsg *string
216 var exitCode int
217
218 if status == StatusKindFailed {
219 msg := "Command exited with non-zero status"
220 errorMsg = &msg
221 exitCode = rand.Intn(100) + 1
222 } else if status == StatusKindTimeout {
223 msg := "Workflow exceeded maximum execution time"
224 errorMsg = &msg
225 exitCode = 124
226 }
227
228 _, err := db.Exec(`
229 INSERT INTO pipeline_statuses (
230 spindle, rkey, pipeline_knot, pipeline_rkey,
231 created, workflow, status, error, exit_code
232 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
233 `, "spindle.example.com", rkey, knot, pipelineRkey,
234 currentTime.Format(time.RFC3339), workflow, string(status), errorMsg, exitCode)
235
236 if err != nil {
237 return err
238 }
239 }
240
241 return nil
242}