@@ -3,6 +3,7 @@ |
| 3 | 3 | package repo |
| 4 | 4 | |
| 5 | 5 | import ( |
| 6 | + "bytes" |
| 6 | 7 | "context" |
| 7 | 8 | "net/http" |
| 8 | 9 | "net/http/httptest" |
@@ -15,6 +16,7 @@ import ( |
| 15 | 16 | "github.com/jackc/pgx/v5/pgtype" |
| 16 | 17 | |
| 17 | 18 | actionsdb "github.com/tenseleyFlow/shithub/internal/actions/sqlc" |
| 19 | + "github.com/tenseleyFlow/shithub/internal/infra/storage" |
| 18 | 20 | "github.com/tenseleyFlow/shithub/internal/web/middleware" |
| 19 | 21 | ) |
| 20 | 22 | |
@@ -130,6 +132,232 @@ func TestRepoTabActionsPaginatesTwentyRuns(t *testing.T) { |
| 130 | 132 | } |
| 131 | 133 | } |
| 132 | 134 | |
| 135 | +func TestRepoActionRunRendersWorkflowRunJobsAndSteps(t *testing.T) { |
| 136 | + t.Parallel() |
| 137 | + f := newRepoFixture(t) |
| 138 | + now := time.Date(2026, 5, 11, 12, 0, 0, 0, time.UTC) |
| 139 | + runID := f.insertWorkflowRun(t, workflowRunFixture{ |
| 140 | + RunIndex: 7, |
| 141 | + WorkflowFile: ".shithub/workflows/ci.yml", |
| 142 | + WorkflowName: "CI", |
| 143 | + HeadRef: "trunk", |
| 144 | + Event: actionsdb.WorkflowRunEventPush, |
| 145 | + Status: actionsdb.WorkflowRunStatusCompleted, |
| 146 | + Conclusion: actionsdb.CheckConclusionFailure, |
| 147 | + ActorUserID: f.owner.ID, |
| 148 | + CreatedOffset: -20 * time.Minute, |
| 149 | + StartedOffset: -19 * time.Minute, |
| 150 | + DoneOffset: -10 * time.Minute, |
| 151 | + }, now) |
| 152 | + buildID := f.insertWorkflowJob(t, workflowJobFixture{ |
| 153 | + RunID: runID, |
| 154 | + JobIndex: 0, |
| 155 | + JobKey: "build", |
| 156 | + JobName: "Build", |
| 157 | + RunsOn: "ubuntu-latest", |
| 158 | + Status: actionsdb.WorkflowJobStatusCompleted, |
| 159 | + Conclusion: actionsdb.CheckConclusionSuccess, |
| 160 | + StartedAt: now.Add(-19 * time.Minute), |
| 161 | + CompletedAt: now.Add(-15 * time.Minute), |
| 162 | + }) |
| 163 | + testID := f.insertWorkflowJob(t, workflowJobFixture{ |
| 164 | + RunID: runID, |
| 165 | + JobIndex: 1, |
| 166 | + JobKey: "test", |
| 167 | + JobName: "Test", |
| 168 | + RunsOn: "ubuntu-latest", |
| 169 | + Needs: []string{"build"}, |
| 170 | + Status: actionsdb.WorkflowJobStatusCompleted, |
| 171 | + Conclusion: actionsdb.CheckConclusionFailure, |
| 172 | + StartedAt: now.Add(-14 * time.Minute), |
| 173 | + CompletedAt: now.Add(-10 * time.Minute), |
| 174 | + }) |
| 175 | + f.insertWorkflowStep(t, workflowStepFixture{ |
| 176 | + JobID: buildID, |
| 177 | + StepIndex: 0, |
| 178 | + StepName: "Checkout", |
| 179 | + UsesAlias: "actions/checkout@v4", |
| 180 | + Status: actionsdb.WorkflowStepStatusCompleted, |
| 181 | + Conclusion: actionsdb.CheckConclusionSuccess, |
| 182 | + CompletedAt: now.Add(-18 * time.Minute), |
| 183 | + }) |
| 184 | + f.insertWorkflowStep(t, workflowStepFixture{ |
| 185 | + JobID: testID, |
| 186 | + StepIndex: 0, |
| 187 | + RunCommand: "go test ./...", |
| 188 | + Status: actionsdb.WorkflowStepStatusCompleted, |
| 189 | + Conclusion: actionsdb.CheckConclusionFailure, |
| 190 | + CompletedAt: now.Add(-10 * time.Minute), |
| 191 | + }) |
| 192 | + |
| 193 | + resp := httptest.NewRecorder() |
| 194 | + req := httptest.NewRequest(http.MethodGet, "/alice/public-repo/actions/runs/7", nil) |
| 195 | + f.actionsMux(viewerFor(f.owner)).ServeHTTP(resp, req) |
| 196 | + if resp.Code != http.StatusOK { |
| 197 | + t.Fatalf("status=%d body=%s", resp.Code, resp.Body.String()) |
| 198 | + } |
| 199 | + body := resp.Body.String() |
| 200 | + for _, want := range []string{ |
| 201 | + "RUN=CI:#7:push:alice:failure;", |
| 202 | + "SUMMARY=2:2:1:0;", |
| 203 | + "JOB=Build:success::ubuntu-latest;", |
| 204 | + "STEP=Checkout:success:/alice/public-repo/actions/runs/7/jobs/0/steps/0;", |
| 205 | + "JOB=Test:failure:build:ubuntu-latest;", |
| 206 | + "STEP=go test ./...:failure:/alice/public-repo/actions/runs/7/jobs/1/steps/0;", |
| 207 | + } { |
| 208 | + if !strings.Contains(body, want) { |
| 209 | + t.Fatalf("body missing %q in %s", want, body) |
| 210 | + } |
| 211 | + } |
| 212 | +} |
| 213 | + |
| 214 | +func TestRepoActionRunStatusRendersPollingFragment(t *testing.T) { |
| 215 | + t.Parallel() |
| 216 | + f := newRepoFixture(t) |
| 217 | + now := time.Date(2026, 5, 11, 12, 0, 0, 0, time.UTC) |
| 218 | + f.insertWorkflowRun(t, workflowRunFixture{ |
| 219 | + RunIndex: 8, |
| 220 | + WorkflowFile: ".shithub/workflows/deploy.yml", |
| 221 | + WorkflowName: "Deploy", |
| 222 | + HeadRef: "trunk", |
| 223 | + Event: actionsdb.WorkflowRunEventWorkflowDispatch, |
| 224 | + Status: actionsdb.WorkflowRunStatusRunning, |
| 225 | + ActorUserID: f.owner.ID, |
| 226 | + CreatedOffset: -5 * time.Minute, |
| 227 | + StartedOffset: -4 * time.Minute, |
| 228 | + }, now) |
| 229 | + |
| 230 | + resp := httptest.NewRecorder() |
| 231 | + req := httptest.NewRequest(http.MethodGet, "/alice/public-repo/actions/runs/8/status", nil) |
| 232 | + f.actionsMux(viewerFor(f.owner)).ServeHTTP(resp, req) |
| 233 | + if resp.Code != http.StatusOK { |
| 234 | + t.Fatalf("status=%d body=%s", resp.Code, resp.Body.String()) |
| 235 | + } |
| 236 | + want := "STATUS=running:false:/alice/public-repo/actions/runs/8/status;" |
| 237 | + if body := resp.Body.String(); !strings.Contains(body, want) { |
| 238 | + t.Fatalf("status fragment missing %q in %s", want, body) |
| 239 | + } |
| 240 | +} |
| 241 | + |
| 242 | +func TestRepoActionStepLogRendersSQLChunks(t *testing.T) { |
| 243 | + t.Parallel() |
| 244 | + f := newRepoFixture(t) |
| 245 | + now := time.Date(2026, 5, 11, 12, 0, 0, 0, time.UTC) |
| 246 | + runID := f.insertWorkflowRun(t, workflowRunFixture{ |
| 247 | + RunIndex: 9, |
| 248 | + WorkflowFile: ".shithub/workflows/ci.yml", |
| 249 | + WorkflowName: "CI", |
| 250 | + HeadRef: "trunk", |
| 251 | + Event: actionsdb.WorkflowRunEventPush, |
| 252 | + Status: actionsdb.WorkflowRunStatusRunning, |
| 253 | + ActorUserID: f.owner.ID, |
| 254 | + CreatedOffset: -5 * time.Minute, |
| 255 | + StartedOffset: -4 * time.Minute, |
| 256 | + }, now) |
| 257 | + jobID := f.insertWorkflowJob(t, workflowJobFixture{ |
| 258 | + RunID: runID, |
| 259 | + JobIndex: 0, |
| 260 | + JobKey: "build", |
| 261 | + JobName: "Build", |
| 262 | + RunsOn: "ubuntu-latest", |
| 263 | + Status: actionsdb.WorkflowJobStatusRunning, |
| 264 | + StartedAt: now.Add(-4 * time.Minute), |
| 265 | + }) |
| 266 | + stepID := f.insertWorkflowStep(t, workflowStepFixture{ |
| 267 | + JobID: jobID, |
| 268 | + StepIndex: 0, |
| 269 | + StepName: "Run tests", |
| 270 | + RunCommand: "go test ./...", |
| 271 | + Status: actionsdb.WorkflowStepStatusRunning, |
| 272 | + StartedAt: now.Add(-3 * time.Minute), |
| 273 | + }) |
| 274 | + f.insertStepLogChunk(t, stepID, 0, "hello\n") |
| 275 | + f.insertStepLogChunk(t, stepID, 1, "world\n") |
| 276 | + |
| 277 | + resp := httptest.NewRecorder() |
| 278 | + req := httptest.NewRequest(http.MethodGet, "/alice/public-repo/actions/runs/9/jobs/0/steps/0", nil) |
| 279 | + f.actionsMux(viewerFor(f.owner)).ServeHTTP(resp, req) |
| 280 | + if resp.Code != http.StatusOK { |
| 281 | + t.Fatalf("status=%d body=%s", resp.Code, resp.Body.String()) |
| 282 | + } |
| 283 | + body := resp.Body.String() |
| 284 | + for _, want := range []string{ |
| 285 | + "STEPLOG=Build:Run tests:SQL chunks::false;", |
| 286 | + "LOG=hello\nworld\n;", |
| 287 | + } { |
| 288 | + if !strings.Contains(body, want) { |
| 289 | + t.Fatalf("body missing %q in %s", want, body) |
| 290 | + } |
| 291 | + } |
| 292 | +} |
| 293 | + |
| 294 | +func TestRepoActionStepLogRendersArchivedObject(t *testing.T) { |
| 295 | + t.Parallel() |
| 296 | + f := newRepoFixture(t) |
| 297 | + now := time.Date(2026, 5, 11, 12, 0, 0, 0, time.UTC) |
| 298 | + runID := f.insertWorkflowRun(t, workflowRunFixture{ |
| 299 | + RunIndex: 10, |
| 300 | + WorkflowFile: ".shithub/workflows/ci.yml", |
| 301 | + WorkflowName: "CI", |
| 302 | + HeadRef: "trunk", |
| 303 | + Event: actionsdb.WorkflowRunEventPush, |
| 304 | + Status: actionsdb.WorkflowRunStatusCompleted, |
| 305 | + Conclusion: actionsdb.CheckConclusionSuccess, |
| 306 | + ActorUserID: f.owner.ID, |
| 307 | + CreatedOffset: -5 * time.Minute, |
| 308 | + StartedOffset: -4 * time.Minute, |
| 309 | + DoneOffset: -1 * time.Minute, |
| 310 | + }, now) |
| 311 | + jobID := f.insertWorkflowJob(t, workflowJobFixture{ |
| 312 | + RunID: runID, |
| 313 | + JobIndex: 0, |
| 314 | + JobKey: "build", |
| 315 | + JobName: "Build", |
| 316 | + RunsOn: "ubuntu-latest", |
| 317 | + Status: actionsdb.WorkflowJobStatusCompleted, |
| 318 | + Conclusion: actionsdb.CheckConclusionSuccess, |
| 319 | + StartedAt: now.Add(-4 * time.Minute), |
| 320 | + CompletedAt: now.Add(-1 * time.Minute), |
| 321 | + }) |
| 322 | + stepID := f.insertWorkflowStep(t, workflowStepFixture{ |
| 323 | + JobID: jobID, |
| 324 | + StepIndex: 0, |
| 325 | + StepName: "Archive", |
| 326 | + RunCommand: "printf archived", |
| 327 | + Status: actionsdb.WorkflowStepStatusCompleted, |
| 328 | + Conclusion: actionsdb.CheckConclusionSuccess, |
| 329 | + StartedAt: now.Add(-3 * time.Minute), |
| 330 | + CompletedAt: now.Add(-1 * time.Minute), |
| 331 | + }) |
| 332 | + key := "actions/runs/" + strconv.FormatInt(runID, 10) + "/jobs/" + strconv.FormatInt(jobID, 10) + "/steps/" + strconv.FormatInt(stepID, 10) + ".log" |
| 333 | + if _, err := f.objectStore.Put(context.Background(), key, bytes.NewReader([]byte("archived\n")), storage.PutOpts{ContentType: "text/plain; charset=utf-8"}); err != nil { |
| 334 | + t.Fatalf("put log object: %v", err) |
| 335 | + } |
| 336 | + if _, err := actionsdb.New().UpdateWorkflowStepLogObject(context.Background(), f.pool, actionsdb.UpdateWorkflowStepLogObjectParams{ |
| 337 | + LogObjectKey: pgtype.Text{String: key, Valid: true}, |
| 338 | + LogByteCount: int64(len("archived\n")), |
| 339 | + ID: stepID, |
| 340 | + }); err != nil { |
| 341 | + t.Fatalf("update log object: %v", err) |
| 342 | + } |
| 343 | + |
| 344 | + resp := httptest.NewRecorder() |
| 345 | + req := httptest.NewRequest(http.MethodGet, "/alice/public-repo/actions/runs/10/jobs/0/steps/0", nil) |
| 346 | + f.actionsMux(viewerFor(f.owner)).ServeHTTP(resp, req) |
| 347 | + if resp.Code != http.StatusOK { |
| 348 | + t.Fatalf("status=%d body=%s", resp.Code, resp.Body.String()) |
| 349 | + } |
| 350 | + body := resp.Body.String() |
| 351 | + for _, want := range []string{ |
| 352 | + "STEPLOG=Build:Archive:object storage:mem://actions/runs/", |
| 353 | + "LOG=archived\n;", |
| 354 | + } { |
| 355 | + if !strings.Contains(body, want) { |
| 356 | + t.Fatalf("body missing %q in %s", want, body) |
| 357 | + } |
| 358 | + } |
| 359 | +} |
| 360 | + |
| 133 | 361 | func (f *repoFixture) actionsMux(viewer middleware.CurrentUser) http.Handler { |
| 134 | 362 | mux := chi.NewRouter() |
| 135 | 363 | mux.Use(func(next http.Handler) http.Handler { |
@@ -137,6 +365,9 @@ func (f *repoFixture) actionsMux(viewer middleware.CurrentUser) http.Handler { |
| 137 | 365 | next.ServeHTTP(w, r.WithContext(middleware.WithCurrentUserForTest(r.Context(), viewer))) |
| 138 | 366 | }) |
| 139 | 367 | }) |
| 368 | + mux.Get("/{owner}/{repo}/actions/runs/{runIndex}/jobs/{jobIndex}/steps/{stepIndex}", f.handlers.repoActionStepLog) |
| 369 | + mux.Get("/{owner}/{repo}/actions/runs/{runIndex}/status", f.handlers.repoActionRunStatus) |
| 370 | + mux.Get("/{owner}/{repo}/actions/runs/{runIndex}", f.handlers.repoActionRun) |
| 140 | 371 | mux.Get("/{owner}/{repo}/actions", f.handlers.repoTabActions) |
| 141 | 372 | return mux |
| 142 | 373 | } |
@@ -153,10 +384,15 @@ type workflowRunFixture struct { |
| 153 | 384 | CreatedOffset time.Duration |
| 154 | 385 | StartedOffset time.Duration |
| 155 | 386 | DoneOffset time.Duration |
| 387 | + RepoID int64 |
| 156 | 388 | } |
| 157 | 389 | |
| 158 | | -func (f *repoFixture) insertWorkflowRun(t *testing.T, fx workflowRunFixture, base time.Time) { |
| 390 | +func (f *repoFixture) insertWorkflowRun(t *testing.T, fx workflowRunFixture, base time.Time) int64 { |
| 159 | 391 | t.Helper() |
| 392 | + repoID := fx.RepoID |
| 393 | + if repoID == 0 { |
| 394 | + repoID = f.publicRepo.ID |
| 395 | + } |
| 160 | 396 | createdAt := base.Add(fx.CreatedOffset) |
| 161 | 397 | startedAt := pgtype.Timestamptz{} |
| 162 | 398 | completedAt := pgtype.Timestamptz{} |
@@ -170,7 +406,8 @@ func (f *repoFixture) insertWorkflowRun(t *testing.T, fx workflowRunFixture, bas |
| 170 | 406 | if fx.Conclusion != "" { |
| 171 | 407 | conclusion = actionsdb.NullCheckConclusion{CheckConclusion: fx.Conclusion, Valid: true} |
| 172 | 408 | } |
| 173 | | - _, err := f.pool.Exec(context.Background(), ` |
| 409 | + var id int64 |
| 410 | + err := f.pool.QueryRow(context.Background(), ` |
| 174 | 411 | INSERT INTO workflow_runs ( |
| 175 | 412 | repo_id, run_index, workflow_file, workflow_name, |
| 176 | 413 | head_sha, head_ref, event, event_payload, actor_user_id, |
@@ -179,8 +416,9 @@ func (f *repoFixture) insertWorkflowRun(t *testing.T, fx workflowRunFixture, bas |
| 179 | 416 | $1, $2, $3, $4, |
| 180 | 417 | $5, $6, $7, '{}'::jsonb, $8, |
| 181 | 418 | $9, $10, $11, $12, $13, $14 |
| 182 | | - )`, |
| 183 | | - f.publicRepo.ID, |
| 419 | + ) |
| 420 | + RETURNING id`, |
| 421 | + repoID, |
| 184 | 422 | fx.RunIndex, |
| 185 | 423 | fx.WorkflowFile, |
| 186 | 424 | fx.WorkflowName, |
@@ -194,12 +432,166 @@ func (f *repoFixture) insertWorkflowRun(t *testing.T, fx workflowRunFixture, bas |
| 194 | 432 | completedAt, |
| 195 | 433 | createdAt, |
| 196 | 434 | createdAt, |
| 197 | | - ) |
| 435 | + ).Scan(&id) |
| 198 | 436 | if err != nil { |
| 199 | 437 | t.Fatalf("insert workflow run %d: %v", fx.RunIndex, err) |
| 200 | 438 | } |
| 439 | + return id |
| 201 | 440 | } |
| 202 | 441 | |
| 203 | 442 | func strconvDigit(n int64) string { |
| 204 | 443 | return strconv.FormatInt(n%10, 10) |
| 205 | 444 | } |
| 445 | + |
| 446 | +type workflowJobFixture struct { |
| 447 | + RunID int64 |
| 448 | + JobIndex int32 |
| 449 | + JobKey string |
| 450 | + JobName string |
| 451 | + RunsOn string |
| 452 | + Needs []string |
| 453 | + Status actionsdb.WorkflowJobStatus |
| 454 | + Conclusion actionsdb.CheckConclusion |
| 455 | + StartedAt time.Time |
| 456 | + CompletedAt time.Time |
| 457 | +} |
| 458 | + |
| 459 | +func (f *repoFixture) insertWorkflowJob(t *testing.T, fx workflowJobFixture) int64 { |
| 460 | + t.Helper() |
| 461 | + status := fx.Status |
| 462 | + if status == "" { |
| 463 | + status = actionsdb.WorkflowJobStatusQueued |
| 464 | + } |
| 465 | + conclusion := actionsdb.NullCheckConclusion{} |
| 466 | + if fx.Conclusion != "" { |
| 467 | + conclusion = actionsdb.NullCheckConclusion{CheckConclusion: fx.Conclusion, Valid: true} |
| 468 | + } |
| 469 | + startedAt := pgtype.Timestamptz{} |
| 470 | + if !fx.StartedAt.IsZero() { |
| 471 | + startedAt = pgtype.Timestamptz{Time: fx.StartedAt, Valid: true} |
| 472 | + } |
| 473 | + completedAt := pgtype.Timestamptz{} |
| 474 | + if !fx.CompletedAt.IsZero() { |
| 475 | + completedAt = pgtype.Timestamptz{Time: fx.CompletedAt, Valid: true} |
| 476 | + } |
| 477 | + needs := fx.Needs |
| 478 | + if needs == nil { |
| 479 | + needs = []string{} |
| 480 | + } |
| 481 | + runnerID := pgtype.Int8{} |
| 482 | + if status == actionsdb.WorkflowJobStatusRunning || status == actionsdb.WorkflowJobStatusCompleted { |
| 483 | + runnerID = pgtype.Int8{Int64: f.insertWorkflowRunner(t), Valid: true} |
| 484 | + } |
| 485 | + var id int64 |
| 486 | + err := f.pool.QueryRow(context.Background(), ` |
| 487 | + INSERT INTO workflow_jobs ( |
| 488 | + run_id, job_index, job_key, job_name, runs_on, needs_jobs, |
| 489 | + runner_id, status, conclusion, started_at, completed_at |
| 490 | + ) VALUES ( |
| 491 | + $1, $2, $3, $4, $5, $6, |
| 492 | + $7, $8, $9, $10, $11 |
| 493 | + ) |
| 494 | + RETURNING id`, |
| 495 | + fx.RunID, |
| 496 | + fx.JobIndex, |
| 497 | + fx.JobKey, |
| 498 | + fx.JobName, |
| 499 | + fx.RunsOn, |
| 500 | + needs, |
| 501 | + runnerID, |
| 502 | + status, |
| 503 | + conclusion, |
| 504 | + startedAt, |
| 505 | + completedAt, |
| 506 | + ).Scan(&id) |
| 507 | + if err != nil { |
| 508 | + t.Fatalf("insert workflow job %s: %v", fx.JobKey, err) |
| 509 | + } |
| 510 | + return id |
| 511 | +} |
| 512 | + |
| 513 | +func (f *repoFixture) insertWorkflowRunner(t *testing.T) int64 { |
| 514 | + t.Helper() |
| 515 | + var id int64 |
| 516 | + err := f.pool.QueryRow(context.Background(), ` |
| 517 | + INSERT INTO workflow_runners (name, labels, status) |
| 518 | + VALUES ($1, ARRAY['ubuntu-latest']::text[], 'busy') |
| 519 | + RETURNING id`, |
| 520 | + "runner-"+strconv.FormatInt(time.Now().UnixNano(), 10), |
| 521 | + ).Scan(&id) |
| 522 | + if err != nil { |
| 523 | + t.Fatalf("insert workflow runner: %v", err) |
| 524 | + } |
| 525 | + return id |
| 526 | +} |
| 527 | + |
| 528 | +type workflowStepFixture struct { |
| 529 | + JobID int64 |
| 530 | + StepIndex int32 |
| 531 | + StepName string |
| 532 | + RunCommand string |
| 533 | + UsesAlias string |
| 534 | + Status actionsdb.WorkflowStepStatus |
| 535 | + Conclusion actionsdb.CheckConclusion |
| 536 | + StartedAt time.Time |
| 537 | + CompletedAt time.Time |
| 538 | +} |
| 539 | + |
| 540 | +func (f *repoFixture) insertWorkflowStep(t *testing.T, fx workflowStepFixture) int64 { |
| 541 | + t.Helper() |
| 542 | + status := fx.Status |
| 543 | + if status == "" { |
| 544 | + status = actionsdb.WorkflowStepStatusQueued |
| 545 | + } |
| 546 | + runCommand := fx.RunCommand |
| 547 | + if runCommand == "" && fx.UsesAlias == "" { |
| 548 | + runCommand = "true" |
| 549 | + } |
| 550 | + conclusion := actionsdb.NullCheckConclusion{} |
| 551 | + if fx.Conclusion != "" { |
| 552 | + conclusion = actionsdb.NullCheckConclusion{CheckConclusion: fx.Conclusion, Valid: true} |
| 553 | + } |
| 554 | + startedAt := pgtype.Timestamptz{} |
| 555 | + if !fx.StartedAt.IsZero() { |
| 556 | + startedAt = pgtype.Timestamptz{Time: fx.StartedAt, Valid: true} |
| 557 | + } |
| 558 | + completedAt := pgtype.Timestamptz{} |
| 559 | + if !fx.CompletedAt.IsZero() { |
| 560 | + completedAt = pgtype.Timestamptz{Time: fx.CompletedAt, Valid: true} |
| 561 | + } |
| 562 | + var id int64 |
| 563 | + err := f.pool.QueryRow(context.Background(), ` |
| 564 | + INSERT INTO workflow_steps ( |
| 565 | + job_id, step_index, step_name, run_command, uses_alias, |
| 566 | + status, conclusion, started_at, completed_at |
| 567 | + ) VALUES ( |
| 568 | + $1, $2, $3, $4, $5, |
| 569 | + $6, $7, $8, $9 |
| 570 | + ) |
| 571 | + RETURNING id`, |
| 572 | + fx.JobID, |
| 573 | + fx.StepIndex, |
| 574 | + fx.StepName, |
| 575 | + runCommand, |
| 576 | + fx.UsesAlias, |
| 577 | + status, |
| 578 | + conclusion, |
| 579 | + startedAt, |
| 580 | + completedAt, |
| 581 | + ).Scan(&id) |
| 582 | + if err != nil { |
| 583 | + t.Fatalf("insert workflow step %d: %v", fx.StepIndex, err) |
| 584 | + } |
| 585 | + return id |
| 586 | +} |
| 587 | + |
| 588 | +func (f *repoFixture) insertStepLogChunk(t *testing.T, stepID int64, seq int32, chunk string) { |
| 589 | + t.Helper() |
| 590 | + if _, err := actionsdb.New().AppendStepLogChunk(context.Background(), f.pool, actionsdb.AppendStepLogChunkParams{ |
| 591 | + StepID: stepID, |
| 592 | + Seq: seq, |
| 593 | + Chunk: []byte(chunk), |
| 594 | + }); err != nil { |
| 595 | + t.Fatalf("insert step log chunk %d: %v", seq, err) |
| 596 | + } |
| 597 | +} |