| 1 | // SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | |
| 3 | package lifecycle |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | |
| 10 | "github.com/jackc/pgx/v5/pgtype" |
| 11 | |
| 12 | actionsdb "github.com/tenseleyFlow/shithub/internal/actions/sqlc" |
| 13 | reposdb "github.com/tenseleyFlow/shithub/internal/repos/sqlc" |
| 14 | "github.com/tenseleyFlow/shithub/internal/testing/dbtest" |
| 15 | usersdb "github.com/tenseleyFlow/shithub/internal/users/sqlc" |
| 16 | ) |
| 17 | |
| 18 | const fixtureHash = "$argon2id$v=19$m=16384,t=1,p=1$" + |
| 19 | "AAAAAAAAAAAAAAAA$" + |
| 20 | "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" |
| 21 | |
| 22 | func TestCancelRunCancelsQueuedJobsAndCompletesRun(t *testing.T) { |
| 23 | ctx := context.Background() |
| 24 | pool := dbtest.NewTestDB(t) |
| 25 | repoID, userID := setupLifecycleRepo(t, pool) |
| 26 | q := actionsdb.New() |
| 27 | run := insertLifecycleRun(t, pool, repoID, userID, 1) |
| 28 | job, err := q.InsertWorkflowJob(ctx, pool, actionsdb.InsertWorkflowJobParams{ |
| 29 | RunID: run.ID, |
| 30 | JobIndex: 0, |
| 31 | JobKey: "build", |
| 32 | JobName: "Build", |
| 33 | RunsOn: "ubuntu-latest", |
| 34 | NeedsJobs: []string{}, |
| 35 | TimeoutMinutes: 30, |
| 36 | Permissions: []byte(`{}`), |
| 37 | JobEnv: []byte(`{}`), |
| 38 | }) |
| 39 | if err != nil { |
| 40 | t.Fatalf("InsertWorkflowJob: %v", err) |
| 41 | } |
| 42 | step, err := q.InsertWorkflowStep(ctx, pool, actionsdb.InsertWorkflowStepParams{ |
| 43 | JobID: job.ID, |
| 44 | StepIndex: 0, |
| 45 | RunCommand: "go test ./...", |
| 46 | StepEnv: []byte(`{}`), |
| 47 | StepWith: []byte(`{}`), |
| 48 | }) |
| 49 | if err != nil { |
| 50 | t.Fatalf("InsertWorkflowStep: %v", err) |
| 51 | } |
| 52 | |
| 53 | result, err := CancelRun(ctx, Deps{Pool: pool}, run.ID, CancelReasonUser) |
| 54 | if err != nil { |
| 55 | t.Fatalf("CancelRun: %v", err) |
| 56 | } |
| 57 | if len(result.ChangedJobs) != 1 || !result.RunCompleted || result.RunConclusion != actionsdb.CheckConclusionCancelled { |
| 58 | t.Fatalf("result: %+v", result) |
| 59 | } |
| 60 | gotJob, err := q.GetWorkflowJobByID(ctx, pool, job.ID) |
| 61 | if err != nil { |
| 62 | t.Fatalf("GetWorkflowJobByID: %v", err) |
| 63 | } |
| 64 | if gotJob.Status != actionsdb.WorkflowJobStatusCancelled || !gotJob.CancelRequested || |
| 65 | !gotJob.Conclusion.Valid || gotJob.Conclusion.CheckConclusion != actionsdb.CheckConclusionCancelled { |
| 66 | t.Fatalf("job: %+v", gotJob) |
| 67 | } |
| 68 | gotStep, err := q.GetWorkflowStepByID(ctx, pool, step.ID) |
| 69 | if err != nil { |
| 70 | t.Fatalf("GetWorkflowStepByID: %v", err) |
| 71 | } |
| 72 | if gotStep.Status != actionsdb.WorkflowStepStatusCancelled || |
| 73 | !gotStep.Conclusion.Valid || gotStep.Conclusion.CheckConclusion != actionsdb.CheckConclusionCancelled { |
| 74 | t.Fatalf("step: %+v", gotStep) |
| 75 | } |
| 76 | gotRun, err := q.GetWorkflowRunByID(ctx, pool, run.ID) |
| 77 | if err != nil { |
| 78 | t.Fatalf("GetWorkflowRunByID: %v", err) |
| 79 | } |
| 80 | if gotRun.Status != actionsdb.WorkflowRunStatusCompleted || |
| 81 | !gotRun.Conclusion.Valid || gotRun.Conclusion.CheckConclusion != actionsdb.CheckConclusionCancelled { |
| 82 | t.Fatalf("run: %+v", gotRun) |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | func TestCancelJobRequestsRunningJobWithoutTerminalOverwrite(t *testing.T) { |
| 87 | ctx := context.Background() |
| 88 | pool := dbtest.NewTestDB(t) |
| 89 | repoID, userID := setupLifecycleRepo(t, pool) |
| 90 | q := actionsdb.New() |
| 91 | run := insertLifecycleRun(t, pool, repoID, userID, 2) |
| 92 | job, err := q.InsertWorkflowJob(ctx, pool, actionsdb.InsertWorkflowJobParams{ |
| 93 | RunID: run.ID, |
| 94 | JobIndex: 0, |
| 95 | JobKey: "test", |
| 96 | JobName: "Test", |
| 97 | RunsOn: "ubuntu-latest", |
| 98 | NeedsJobs: []string{}, |
| 99 | TimeoutMinutes: 30, |
| 100 | Permissions: []byte(`{}`), |
| 101 | JobEnv: []byte(`{}`), |
| 102 | }) |
| 103 | if err != nil { |
| 104 | t.Fatalf("InsertWorkflowJob: %v", err) |
| 105 | } |
| 106 | runner, err := q.InsertRunner(ctx, pool, actionsdb.InsertRunnerParams{ |
| 107 | Name: "runner-1", |
| 108 | Labels: []string{"ubuntu-latest"}, |
| 109 | Capacity: 1, |
| 110 | }) |
| 111 | if err != nil { |
| 112 | t.Fatalf("InsertRunner: %v", err) |
| 113 | } |
| 114 | if _, err := pool.Exec(ctx, `UPDATE workflow_jobs SET runner_id = $1, status = 'running', started_at = now() WHERE id = $2`, runner.ID, job.ID); err != nil { |
| 115 | t.Fatalf("mark job running: %v", err) |
| 116 | } |
| 117 | |
| 118 | result, err := CancelJob(ctx, Deps{Pool: pool}, job.ID, CancelReasonUser) |
| 119 | if err != nil { |
| 120 | t.Fatalf("CancelJob: %v", err) |
| 121 | } |
| 122 | if len(result.ChangedJobs) != 1 || result.RunCompleted { |
| 123 | t.Fatalf("result: %+v", result) |
| 124 | } |
| 125 | gotJob, err := q.GetWorkflowJobByID(ctx, pool, job.ID) |
| 126 | if err != nil { |
| 127 | t.Fatalf("GetWorkflowJobByID: %v", err) |
| 128 | } |
| 129 | if gotJob.Status != actionsdb.WorkflowJobStatusRunning || !gotJob.CancelRequested || gotJob.Conclusion.Valid { |
| 130 | t.Fatalf("job: %+v", gotJob) |
| 131 | } |
| 132 | gotRun, err := q.GetWorkflowRunByID(ctx, pool, run.ID) |
| 133 | if err != nil { |
| 134 | t.Fatalf("GetWorkflowRunByID: %v", err) |
| 135 | } |
| 136 | if gotRun.Status != actionsdb.WorkflowRunStatusRunning || gotRun.Conclusion.Valid { |
| 137 | t.Fatalf("run: %+v", gotRun) |
| 138 | } |
| 139 | |
| 140 | again, err := CancelJob(ctx, Deps{Pool: pool}, job.ID, CancelReasonUser) |
| 141 | if err != nil { |
| 142 | t.Fatalf("CancelJob repeat: %v", err) |
| 143 | } |
| 144 | if len(again.ChangedJobs) != 0 { |
| 145 | t.Fatalf("repeat was not idempotent: %+v", again) |
| 146 | } |
| 147 | } |
| 148 | |
| 149 | func TestListActiveWorkflowRunsForAdminFiltersActiveRuns(t *testing.T) { |
| 150 | ctx := context.Background() |
| 151 | pool := dbtest.NewTestDB(t) |
| 152 | repoID, userID := setupLifecycleRepo(t, pool) |
| 153 | q := actionsdb.New() |
| 154 | |
| 155 | queued := insertLifecycleRun(t, pool, repoID, userID, 1) |
| 156 | running := insertLifecycleRun(t, pool, repoID, userID, 2) |
| 157 | running, err := q.StartWorkflowRun(ctx, pool, running.ID) |
| 158 | if err != nil { |
| 159 | t.Fatalf("StartWorkflowRun: %v", err) |
| 160 | } |
| 161 | completed := insertLifecycleRun(t, pool, repoID, userID, 3) |
| 162 | if _, err := q.CompleteWorkflowRun(ctx, pool, actionsdb.CompleteWorkflowRunParams{ |
| 163 | ID: completed.ID, |
| 164 | Conclusion: actionsdb.CheckConclusionSuccess, |
| 165 | }); err != nil { |
| 166 | t.Fatalf("CompleteWorkflowRun: %v", err) |
| 167 | } |
| 168 | otherRepoID, otherUserID := setupNamedLifecycleRepo(t, pool, "bob", "other") |
| 169 | otherRepoRun := insertLifecycleRun(t, pool, otherRepoID, otherUserID, 1) |
| 170 | |
| 171 | all, err := q.ListActiveWorkflowRunsForAdmin(ctx, pool, actionsdb.ListActiveWorkflowRunsForAdminParams{ |
| 172 | RepoID: 0, |
| 173 | LimitCount: 10, |
| 174 | }) |
| 175 | if err != nil { |
| 176 | t.Fatalf("ListActiveWorkflowRunsForAdmin all: %v", err) |
| 177 | } |
| 178 | assertRunIDs(t, all, queued.ID, running.ID, otherRepoRun.ID) |
| 179 | |
| 180 | repoOnly, err := q.ListActiveWorkflowRunsForAdmin(ctx, pool, actionsdb.ListActiveWorkflowRunsForAdminParams{ |
| 181 | RepoID: repoID, |
| 182 | LimitCount: 10, |
| 183 | }) |
| 184 | if err != nil { |
| 185 | t.Fatalf("ListActiveWorkflowRunsForAdmin repo: %v", err) |
| 186 | } |
| 187 | assertRunIDs(t, repoOnly, queued.ID, running.ID) |
| 188 | |
| 189 | limited, err := q.ListActiveWorkflowRunsForAdmin(ctx, pool, actionsdb.ListActiveWorkflowRunsForAdminParams{ |
| 190 | RepoID: 0, |
| 191 | LimitCount: 1, |
| 192 | }) |
| 193 | if err != nil { |
| 194 | t.Fatalf("ListActiveWorkflowRunsForAdmin limited: %v", err) |
| 195 | } |
| 196 | assertRunIDs(t, limited, queued.ID) |
| 197 | } |
| 198 | |
| 199 | func setupLifecycleRepo(t *testing.T, db actionsdb.DBTX) (repoID, userID int64) { |
| 200 | t.Helper() |
| 201 | return setupNamedLifecycleRepo(t, db, "alice", "demo") |
| 202 | } |
| 203 | |
| 204 | func setupNamedLifecycleRepo(t *testing.T, db actionsdb.DBTX, username, repoName string) (repoID, userID int64) { |
| 205 | t.Helper() |
| 206 | ctx := context.Background() |
| 207 | user, err := usersdb.New().CreateUser(ctx, db, usersdb.CreateUserParams{ |
| 208 | Username: username, |
| 209 | DisplayName: username, |
| 210 | PasswordHash: fixtureHash, |
| 211 | }) |
| 212 | if err != nil { |
| 213 | t.Fatalf("CreateUser: %v", err) |
| 214 | } |
| 215 | repo, err := reposdb.New().CreateRepo(ctx, db, reposdb.CreateRepoParams{ |
| 216 | OwnerUserID: pgtype.Int8{Int64: user.ID, Valid: true}, |
| 217 | Name: repoName, |
| 218 | DefaultBranch: "trunk", |
| 219 | Visibility: reposdb.RepoVisibilityPublic, |
| 220 | }) |
| 221 | if err != nil { |
| 222 | t.Fatalf("CreateRepo: %v", err) |
| 223 | } |
| 224 | return repo.ID, user.ID |
| 225 | } |
| 226 | |
| 227 | func assertRunIDs(t *testing.T, runs []actionsdb.WorkflowRun, want ...int64) { |
| 228 | t.Helper() |
| 229 | if len(runs) != len(want) { |
| 230 | t.Fatalf("got %d runs, want %d: %+v", len(runs), len(want), runs) |
| 231 | } |
| 232 | for i := range want { |
| 233 | if runs[i].ID != want[i] { |
| 234 | t.Fatalf("run[%d] id=%d, want %d; runs=%+v", i, runs[i].ID, want[i], runs) |
| 235 | } |
| 236 | } |
| 237 | } |
| 238 | |
| 239 | func insertLifecycleRun(t *testing.T, db actionsdb.DBTX, repoID, userID, runIndex int64) actionsdb.WorkflowRun { |
| 240 | t.Helper() |
| 241 | run, err := actionsdb.New().InsertWorkflowRun(context.Background(), db, actionsdb.InsertWorkflowRunParams{ |
| 242 | RepoID: repoID, |
| 243 | RunIndex: runIndex, |
| 244 | WorkflowFile: ".shithub/workflows/ci.yml", |
| 245 | WorkflowName: "CI", |
| 246 | HeadSha: strings.Repeat("a", 40), |
| 247 | HeadRef: "trunk", |
| 248 | Event: actionsdb.WorkflowRunEventPush, |
| 249 | EventPayload: []byte(`{}`), |
| 250 | ActorUserID: pgtype.Int8{Int64: userID, Valid: true}, |
| 251 | }) |
| 252 | if err != nil { |
| 253 | t.Fatalf("InsertWorkflowRun: %v", err) |
| 254 | } |
| 255 | return run |
| 256 | } |
| 257 |