• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ubccpsc / classy / d75e2632-84ef-47f0-968b-c513a1ce335b

12 Aug 2025 10:17PM UTC coverage: 88.309% (+1.1%) from 87.191%
d75e2632-84ef-47f0-968b-c513a1ce335b

push

circleci

web-flow
Merge pull request #480 from ubccpsc310/main

Merge 25w1 changes

1092 of 1321 branches covered (82.66%)

Branch coverage included in aggregate %.

3931 of 4367 relevant lines covered (90.02%)

37.09 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.93
packages/portal/backend/src/controllers/AdminController.ts
1
import Config, { ConfigKey } from "@common/Config";
1✔
2
import Log from "@common/Log";
1✔
3

4
import { GradeReport } from "@common/types/ContainerTypes";
5
import {
6
        AutoTestDashboardTransport,
7
        AutoTestGradeTransport,
8
        AutoTestResultSummaryTransport,
9
        CourseTransport,
10
        DeliverableTransport,
11
        GradeTransport,
12
        ProvisionTransport,
13
        RepositoryTransport,
14
        StudentTransport,
15
        TeamTransport,
16
} from "@common/types/PortalTypes";
17
import Util from "@common/Util";
1✔
18
import { Factory } from "../Factory";
1✔
19
import { AuditLabel, Course, Deliverable, GitHubStatus, Grade, Person, PersonKind, Repository, Result, Team } from "../Types";
1✔
20
import { DatabaseController } from "./DatabaseController";
1✔
21
import { DeliverablesController } from "./DeliverablesController";
1✔
22
import { GitHubActions } from "./GitHubActions";
1✔
23
import { GitHubController, IGitHubController } from "./GitHubController";
1✔
24
import { GradesController } from "./GradesController";
1✔
25
import { PersonController } from "./PersonController";
1✔
26
import { RepositoryController } from "./RepositoryController";
1✔
27
import { ResultsController, ResultsKind } from "./ResultsController";
1✔
28
import { TeamController } from "./TeamController";
1✔
29

30
export class AdminController {
1✔
31
        protected dbc = DatabaseController.getInstance();
55✔
32
        protected pc = new PersonController();
55✔
33
        protected rc = new RepositoryController();
55✔
34
        protected tc = new TeamController();
55✔
35
        protected gc = new GradesController();
55✔
36
        protected resC = new ResultsController();
55✔
37
        // protected cc: ICourseController;
38
        protected gh: IGitHubController = null;
55✔
39

40
        constructor(ghController: IGitHubController) {
41
                Log.trace("AdminController::<init>");
55✔
42
                this.gh = ghController;
55✔
43
        }
44

45
        /**
46
         * Returns the name for this instance. Not defensive: If name is null or something goes wrong there will be errors all over.
47
         *
48
         * @returns {string | null}
49
         */
50
        public static getName(): string | null {
51
                return Config.getInstance().getProp(ConfigKey.name);
1✔
52
        }
53

54
        /**
55
         * Validates the CourseTransport object.
56
         *
57
         * @param {CourseTransport} courseTrans
58
         * @returns {string | null} null if object is valid; string description of error if not.
59
         */
60
        public static validateCourseTransport(courseTrans: CourseTransport): string | null {
61
                if (typeof courseTrans === "undefined" || courseTrans === null) {
6✔
62
                        const msg = "Course not populated.";
1✔
63
                        Log.error("AdminController::validateCourseTransport(..) - ERROR: " + msg);
1✔
64
                        throw new Error(msg);
1✔
65
                }
66

67
                // noinspection SuspiciousTypeOfGuard
68
                if (typeof courseTrans.id !== "string") {
5✔
69
                        const msg = "Course.id not specified";
1✔
70
                        Log.error("AdminController::validateCourseTransport(..) - ERROR: " + msg);
1✔
71
                        throw new Error(msg);
1✔
72
                }
73

74
                // noinspection SuspiciousTypeOfGuard
75
                if (typeof courseTrans.defaultDeliverableId !== "string") {
4✔
76
                        const msg = "defaultDeliverableId not specified";
1✔
77
                        Log.error("AdminController::validateCourseTransport(..) - ERROR: " + msg);
1✔
78
                        return msg;
1✔
79
                }
80

81
                // noinspection SuspiciousTypeOfGuard
82
                if (typeof courseTrans.custom !== "object") {
3✔
83
                        const msg = "custom not specified";
1✔
84
                        Log.error("AdminController::validateCourseTransport(..) - ERROR: " + msg);
1✔
85
                        return msg;
1✔
86
                }
87

88
                return null;
2✔
89
        }
90

91
        /**
92
         * Returns null if the object is valid. This API is terrible.
93
         *
94
         * @param {ProvisionTransport} obj
95
         * @returns {ProvisionTransport | null}
96
         */
97
        public static validateProvisionTransport(obj: ProvisionTransport): ProvisionTransport | null {
98
                if (typeof obj === "undefined" || obj === null) {
5✔
99
                        const msg = "Transport not populated.";
1✔
100
                        Log.error("AdminController::validateProvisionTransport(..) - ERROR: " + msg);
1✔
101
                        throw new Error(msg);
1✔
102
                }
103

104
                // noinspection SuspiciousTypeOfGuard
105
                if (typeof obj.delivId !== "string") {
4✔
106
                        const msg = "Provision.id not specified";
1✔
107
                        Log.error("AdminController::validateProvisionTransport(..) - ERROR: " + msg);
1✔
108
                        throw new Error(msg);
1✔
109
                }
110

111
                // noinspection SuspiciousTypeOfGuard
112
                if (typeof obj.formSingle !== "boolean") {
3✔
113
                        const msg = "formSingle not specified";
1✔
114
                        Log.error("AdminController::validateProvisionTransport(..) - ERROR: " + msg);
1✔
115
                        throw new Error(msg);
1✔
116
                }
117

118
                return null;
2✔
119
        }
120

121
        /**
122
         * Processes the new autotest grade. Only returns true if the grade was accepted and saved.
123
         *
124
         * @param {AutoTestGradeTransport} grade
125
         * @returns {Promise<boolean>} Whether the new grade was saved
126
         */
127
        public async processNewAutoTestGrade(grade: AutoTestGradeTransport): Promise<boolean> {
128
                Log.trace("AdminController::processNewAutoTestGrade(..) - start");
4✔
129

130
                const cc = await Factory.getCourseController(this.gh);
4✔
131

132
                try {
4✔
133
                        Log.trace("AdminController::processNewAutoTestGrade(..) - payload: " + JSON.stringify(grade));
4✔
134
                        const repo = await this.rc.getRepository(grade.repoId);
4✔
135
                        if (repo === null) {
4✔
136
                                // sanity check
137
                                Log.error("AdminController::processNewAutoTestGrade(..) - invalid repo name: " + grade.repoId);
1✔
138
                                return false;
1✔
139
                        }
140

141
                        const peopleIds = await this.rc.getPeopleForRepo(grade.repoId);
3✔
142
                        if (peopleIds.length < 1) {
3!
143
                                // sanity check
144
                                Log.error("AdminController::processNewAutoTestGrade(..) - no people to associate grade record with.");
×
145
                                return false;
×
146
                        }
147

148
                        Log.trace("AdminController::processNewAutoTestGrade(..) - getting deliv"); // NOTE: for hangup debugging
3✔
149

150
                        const delivController = new DeliverablesController();
3✔
151
                        const deliv = await delivController.getDeliverable(grade.delivId);
3✔
152

153
                        let saved = false;
3✔
154

155
                        for (const personId of peopleIds) {
3✔
156
                                const newGrade: Grade = {
6✔
157
                                        personId: personId,
158
                                        delivId: grade.delivId,
159
                                        score: grade.score,
160
                                        comment: grade.comment,
161
                                        urlName: grade.urlName,
162
                                        URL: grade.URL,
163
                                        timestamp: grade.timestamp,
164
                                        custom: grade.custom,
165
                                };
166

167
                                Log.trace("AdminController::processNewAutoTestGrade(..) - getting grade for " + personId);
6✔
168
                                const existingGrade = await this.gc.getGrade(personId, grade.delivId);
6✔
169
                                const existingGradeScore = existingGrade?.score ? existingGrade.score : "N/A";
6✔
170
                                Log.trace(
6✔
171
                                        "AdminController::processNewAutoTestGrade(..) - handling grade for " +
172
                                                personId +
173
                                                "; repo: " +
174
                                                grade.repoId +
175
                                                "; existingGrade: " +
176
                                                existingGradeScore +
177
                                                "; newGrade: " +
178
                                                newGrade.score
179
                                );
180
                                const shouldSave = await cc.handleNewAutoTestGrade(deliv, newGrade, existingGrade);
6✔
181
                                // Log.trace("AdminController::processNewAutoTestGrade(..) - handled grade for " + personId +
182
                                //     "; shouldSave: " + shouldSave); // NOTE: for hangup debugging
183

184
                                Log.trace(
6✔
185
                                        "AdminController::processNewAutoTestGrade(..) - grade: " +
186
                                                JSON.stringify(newGrade) +
187
                                                "; repoId: " +
188
                                                grade.repoId +
189
                                                "; shouldSave: " +
190
                                                shouldSave
191
                                );
192

193
                                if (shouldSave === true) {
6✔
194
                                        Log.info(
4✔
195
                                                "AdminController::processNewAutoTestGrade(..) - saving grade for deliv: " +
196
                                                        newGrade.delivId +
197
                                                        "; repo: " +
198
                                                        grade.repoId
199
                                        );
200
                                        await this.dbc.writeAudit(AuditLabel.GRADE_AUTOTEST, "AutoTest", existingGrade, newGrade, { repoId: grade.repoId });
4✔
201
                                        await this.gc.saveGrade(newGrade);
4✔
202
                                        saved = true;
4✔
203
                                }
204
                        }
205
                        return saved;
3✔
206
                } catch (err) {
207
                        Log.error("AdminController::processNewAutoTestGrade(..) - ERROR: " + err);
×
208
                        return false;
×
209
                }
210
        }
211

212
        public async getCourse(): Promise<Course> {
213
                let record: Course = await this.dbc.getCourseRecord();
8✔
214
                if (record === null) {
8✔
215
                        // create default and write it
216
                        record = {
3✔
217
                                id: Config.getInstance().getProp(ConfigKey.name),
218
                                defaultDeliverableId: null,
219
                                custom: {},
220
                        };
221
                        await this.dbc.writeCourseRecord(record);
3✔
222
                }
223
                return record;
8✔
224
        }
225

226
        public async saveCourse(course: Course): Promise<boolean> {
227
                const record: Course = await this.dbc.getCourseRecord();
5✔
228
                if (record !== null) {
5!
229
                        // merge the new with the old
230
                        record.defaultDeliverableId = course.defaultDeliverableId;
5✔
231
                        record.custom = Object.assign({}, record.custom, course.custom); // merge custom properties
5✔
232
                }
233
                return await this.dbc.writeCourseRecord(record);
5✔
234
        }
235

236
        /**
237
         * Gets the students associated with the course. Admins, staff, and withdrawn students are not included.
238
         *
239
         * @returns {Promise<StudentTransport[]>}
240
         */
241
        public async getStudents(): Promise<StudentTransport[]> {
242
                const people = await this.pc.getAllPeople();
5✔
243

244
                const students: StudentTransport[] = [];
5✔
245
                for (const person of people) {
5✔
246
                        if (person.kind === PersonKind.STUDENT || person.kind === null) {
60✔
247
                                // null should be set on first login
248
                                const studentTransport = {
36✔
249
                                        id: person.id,
250
                                        firstName: person.fName,
251
                                        lastName: person.lName,
252
                                        githubId: person.githubId,
253
                                        userUrl: Config.getInstance().getProp(ConfigKey.githubHost) + "/" + person.githubId,
254
                                        studentNum: person.studentNumber,
255
                                        labId: person.labId,
256
                                };
257
                                students.push(studentTransport);
36✔
258
                        }
259
                }
260
                return students;
5✔
261
        }
262

263
        /**
264
         * Gets the staff associated with the course.
265
         *
266
         * @returns {Promise<StudentTransport[]>}
267
         */
268
        public async getStaff(): Promise<StudentTransport[]> {
269
                const people = await this.pc.getAllPeople();
1✔
270

271
                const adminStaff: StudentTransport[] = [];
1✔
272
                for (const person of people) {
1✔
273
                        if (person.kind === PersonKind.ADMIN || person.kind === PersonKind.STAFF || person.kind === PersonKind.ADMINSTAFF) {
12✔
274
                                const isAdmin = person.kind === PersonKind.ADMIN || person.kind === PersonKind.ADMINSTAFF;
3✔
275
                                const isStaff = person.kind === PersonKind.STAFF || person.kind === PersonKind.ADMINSTAFF;
3✔
276

277
                                const studentTransport = {
3✔
278
                                        id: person.id,
279
                                        firstName: person.fName,
280
                                        lastName: person.lName,
281
                                        githubId: person.githubId,
282
                                        userUrl: Config.getInstance().getProp(ConfigKey.githubHost) + "/" + person.githubId,
283
                                        studentNum: person.studentNumber,
284
                                        labId: person.labId,
285
                                        kind: person.kind,
286
                                        isAdmin,
287
                                        isStaff,
288
                                };
289
                                adminStaff.push(studentTransport);
3✔
290
                        }
291
                }
292
                return adminStaff;
1✔
293
        }
294

295
        /**
296
         * Gets the teams associated with the course.
297
         *
298
         * @returns {Promise<TeamTransport[]>}
299
         */
300
        public async getTeams(): Promise<TeamTransport[]> {
301
                const allTeams = await this.tc.getAllTeams();
2✔
302
                const teams: TeamTransport[] = [];
2✔
303
                for (const team of allTeams) {
2✔
304
                        const teamTransport: TeamTransport = {
4✔
305
                                id: team.id,
306
                                delivId: team.delivId,
307
                                people: team.personIds,
308
                                URL: team.URL,
309
                        };
310
                        teams.push(teamTransport);
4✔
311
                }
312
                return teams;
2✔
313
        }
314

315
        /**
316
         * Gets the repos associated with the course.
317
         *
318
         * @returns {Promise<RepositoryTransport[]>}
319
         */
320
        public async getRepositories(): Promise<RepositoryTransport[]> {
321
                const allRepos = await this.rc.getAllRepos();
2✔
322
                const repos: RepositoryTransport[] = [];
2✔
323
                for (const repo of allRepos) {
2✔
324
                        const repoTransport: RepositoryTransport = {
4✔
325
                                id: repo.id,
326
                                URL: repo.URL,
327
                                delivId: repo.delivId,
328
                                gitHubStatus: repo.gitHubStatus.toString(),
329
                        };
330
                        repos.push(repoTransport);
4✔
331
                }
332
                return repos;
2✔
333
        }
334

335
        /**
336
         * Gets the grades associated with the course.
337
         *
338
         * @returns {Promise<GradeTransport[]>}
339
         */
340
        public async getGrades(): Promise<GradeTransport[]> {
341
                Log.info("AdminController::getGrades() - start");
2✔
342
                const start = Date.now();
2✔
343
                const allGrades = await this.gc.getAllGrades();
2✔
344
                Log.trace("AdminController::getGrades() - getting grades took: " + Util.took(start));
2✔
345

346
                let part = Date.now();
2✔
347
                const grades: GradeTransport[] = [];
2✔
348
                const pc = new PersonController();
2✔
349
                const allPeople = await pc.getAllPeople(); // just make this query once
2✔
350
                Log.trace("AdminController::getGrades() - getting people took: " + Util.took(part));
2✔
351

352
                part = Date.now();
2✔
353
                for (const grade of allGrades) {
2✔
354
                        const p = allPeople.find((person) => person.id === grade.personId);
6✔
355
                        const gradeTrans: GradeTransport = {
4✔
356
                                personId: grade.personId,
357
                                personURL: Config.getInstance().getProp(ConfigKey.githubHost) + "/" + p.githubId,
358
                                delivId: grade.delivId,
359
                                score: grade.score,
360
                                comment: grade.comment,
361
                                urlName: grade.urlName,
362
                                URL: grade.URL,
363
                                timestamp: grade.timestamp,
364
                                custom: grade.custom,
365
                        };
366
                        grades.push(gradeTrans);
4✔
367
                }
368

369
                Log.trace("AdminController::getGrades() - post-processing took: " + Util.took(part));
2✔
370

371
                Log.info("AdminController::getGrades() - done; took: " + Util.took(start));
2✔
372
                return grades;
2✔
373
        }
374

375
        /**
376
         * Gets the results associated with the course.
377
         * @param reqDelivId ("any" for *)
378
         * @param reqRepoId ("any" for *)
379
         * @param maxNumResults (optional, default 500)
380
         * @param kind
381
         * @returns {Promise<AutoTestGradeTransport[]>}
382
         */
383
        public async getDashboard(
384
                reqDelivId: string,
385
                reqRepoId: string,
386
                maxNumResults?: number,
387
                kind: ResultsKind = ResultsKind.ALL
6✔
388
        ): Promise<AutoTestDashboardTransport[]> {
389
                Log.info("AdminController::getDashboard( " + reqDelivId + ", " + reqRepoId + ", " + maxNumResults + " ) - start");
8✔
390
                const start = Date.now();
8✔
391
                const NUM_RESULTS = maxNumResults ? maxNumResults : 500; // max # of records
8✔
392

393
                const repoIds: string[] = [];
8✔
394
                const results: AutoTestDashboardTransport[] = [];
8✔
395
                const allResults = await this.matchResults(reqDelivId, reqRepoId, kind);
8✔
396
                for (const result of allResults) {
8✔
397
                        const repoId = result.input.target.repoId;
88✔
398
                        if (results.length < NUM_RESULTS) {
88✔
399
                                const resultTrans = await this.createDashboardTransport(result);
83✔
400
                                // just return the first result for a repo, unless they are specified
401
                                if (reqRepoId !== "any" || repoIds.indexOf(repoId) < 0) {
83✔
402
                                        results.push(resultTrans);
32✔
403
                                        repoIds.push(repoId);
32✔
404
                                }
405
                        } else {
406
                                // result does not match filter
407
                        }
408
                }
409
                Log.info("AdminController::getDashboard(..) - # results: " + results.length + "; took: " + Util.took(start));
8✔
410
                return results;
8✔
411
        }
412

413
        public async matchResults(reqDelivId: string, reqRepoId: string, kind: ResultsKind): Promise<Result[]> {
414
                Log.trace("AdminController::matchResults(..) - start");
12✔
415
                const start = Date.now();
12✔
416
                const WILDCARD = "any";
12✔
417

418
                let allResults: Result[];
419
                if (reqRepoId !== WILDCARD) {
12✔
420
                        // if both are not "any" just use this one too
421
                        // ResultsKind not supported for getAllResults(..)
422
                        allResults = await this.resC.getResultsForRepo(reqRepoId);
5✔
423
                } else if (reqDelivId !== WILDCARD) {
7✔
424
                        allResults = await this.resC.getResultsForDeliverable(reqDelivId, kind);
2✔
425
                } else {
426
                        // ResultsKind not supported for getAllResults(..)
427
                        allResults = await this.resC.getAllResults();
5✔
428
                }
429
                Log.trace("AdminController::matchResults(..) - search done; # results: " + allResults.length + "; took: " + Util.took(start));
12✔
430

431
                const NUM_RESULTS = 1000;
12✔
432

433
                const results: Result[] = [];
12✔
434
                for (const result of allResults) {
12✔
435
                        // const repo = await rc.getRepository(result.repoId); // this happens a lot and ends up being too slow
436
                        const delivId = result.delivId;
146✔
437
                        const repoId = result.input.target.repoId;
146✔
438

439
                        if (
146!
440
                                (reqDelivId === WILDCARD || delivId === reqDelivId) &&
519✔
441
                                (reqRepoId === WILDCARD || repoId === reqRepoId) &&
442
                                results.length <= NUM_RESULTS
443
                        ) {
444
                                results.push(result);
146✔
445
                        } else {
446
                                // result does not match filter
447
                        }
448
                }
449

450
                Log.trace("AdminController::matchResults(..) - done; # results: " + results.length + "; took: " + Util.took(start));
12✔
451
                return results;
12✔
452
        }
453

454
        /**
455
         * Gets the list of GitHub ids associated with the "students" team on GitHub
456
         * and marks them as PersonKind.WITHDRAWN. Does nothing if the students team
457
         * does not exist or is empty.
458
         *
459
         * @returns {Promise<string>} A message summarizing the outcome of the operation.
460
         */
461
        public async performStudentWithdraw(): Promise<string> {
462
                Log.info("AdminController::performStudentWithdraw() - start");
2✔
463
                const gha = GitHubActions.getInstance(true);
2✔
464
                // const tc = new TeamController();
465
                // const teamNum = await tc.getTeamNumber("students"); // await gha.getTeamNumber("students");
466
                // const registeredGithubIds = await gha.getTeamMembers(teamNum);
467
                const registeredGithubIds = await gha.getTeamMembers("students");
2✔
468

469
                if (registeredGithubIds.length > 0) {
2!
470
                        const pc = new PersonController();
2✔
471
                        const msg = await pc.markStudentsWithdrawn(registeredGithubIds);
2✔
472
                        Log.info("AdminController::performStudentWithdraw() - done; msg: " + msg);
2✔
473
                        return msg;
2✔
474
                } else {
475
                        throw new Error("No students specified in the students team on GitHub; operation aborted.");
×
476
                }
477
        }
478

479
        /**
480
         * Gets the results associated with the course.
481
         * @param reqDelivId ("any" for *)
482
         * @param reqRepoId ("any" for *)
483
         * @param kind
484
         * @returns {Promise<AutoTestGradeTransport[]>}
485
         */
486
        public async getResults(
487
                reqDelivId: string,
488
                reqRepoId: string,
489
                kind: ResultsKind = ResultsKind.ALL
4✔
490
        ): Promise<AutoTestResultSummaryTransport[]> {
491
                Log.info("AdminController::getResults( " + reqDelivId + ", " + reqRepoId + ", " + kind + " ) - start");
4✔
492
                const start = Date.now();
4✔
493
                const NUM_RESULTS = 1000; // max # of records
4✔
494

495
                const results: AutoTestResultSummaryTransport[] = [];
4✔
496
                const allResults = await this.matchResults(reqDelivId, reqRepoId, kind);
4✔
497
                for (const result of allResults) {
4✔
498
                        // const repo = await rc.getRepository(result.repoId); // this happens a lot and ends up being too slow
499
                        // const repoId = result.input.target.repoId;
500
                        if (results.length <= NUM_RESULTS) {
58!
501
                                const resultTrans = await this.clipAutoTestResult(result);
58✔
502
                                results.push(resultTrans);
58✔
503
                        } else {
504
                                // result does not match filter
505
                        }
506
                }
507
                Log.info(
4✔
508
                        "AdminController::getResults( " +
509
                                reqDelivId +
510
                                ", " +
511
                                reqRepoId +
512
                                ", " +
513
                                kind +
514
                                ") - done; # results: " +
515
                                results.length +
516
                                "; took: " +
517
                                Util.took(start)
518
                );
519
                return results;
4✔
520
        }
521

522
        /**
523
         * Gets the deliverables associated with the course.
524
         *
525
         * @returns {Promise<DeliverableTransport[]>}
526
         */
527
        public async getDeliverables(): Promise<DeliverableTransport[]> {
528
                const deliverables = await this.dbc.getDeliverables();
4✔
529
                const start = Date.now();
4✔
530
                Log.trace("AdminController::getDeliverables() - start");
4✔
531

532
                let delivs: DeliverableTransport[] = [];
4✔
533
                for (const deliv of deliverables) {
4✔
534
                        const delivTransport = DeliverablesController.deliverableToTransport(deliv);
20✔
535

536
                        delivs.push(delivTransport);
20✔
537
                }
538

539
                delivs = delivs.sort(function (d1: DeliverableTransport, d2: DeliverableTransport) {
4✔
540
                        return d1.id.localeCompare(d2.id);
16✔
541
                });
542

543
                Log.trace("AdminController::getDeliverables() - done; # delivs: " + delivs.length + "; took: " + Util.took(start));
4✔
544
                return delivs;
4✔
545
        }
546

547
        /**
548
         * This plans the repo provisioning process. Planning is separated from doing so
549
         * that course staff can look at the repos being proposed and have the opportunity
550
         * to provision a subset of repos if they wish (e.g., for testing before creating
551
         * all of them).
552
         *
553
         * @param {Deliverable} deliv
554
         * @param {boolean} formSingleTeams specify whether singletons should be allocated into teams.
555
         * Choose false if you want to wait for the students to specify, choose true if you want to
556
         * let them work individually. (Note: if your teams are of max size 1, you still need to say
557
         * yes to make this happen.)
558
         *
559
         * @returns {Promise<RepositoryTransport[]>}
560
         */
561
        public async planProvision(deliv: Deliverable, formSingleTeams: boolean): Promise<RepositoryTransport[]> {
562
                Log.info("AdminController::planProvision( " + deliv.id + ", " + formSingleTeams + " ) - start");
5✔
563
                const cc = await Factory.getCourseController(this.gh);
5✔
564

565
                let allPeople: Person[] = await this.pc.getAllPeople();
5✔
566
                Log.info("AdminController::planProvision( .. ) - # people (all): " + allPeople.length);
5✔
567

568
                // remove all withdrawn people, we do not need to provision these
569
                allPeople = allPeople.filter((person) => person.kind !== PersonKind.WITHDRAWN);
33✔
570
                Log.info("AdminController::planProvision( .. ) - # people (not withdrawn): " + allPeople.length);
5✔
571

572
                // teams were either formed by students (or the admin in the UI)
573
                // _or_ the deliv is for single students and we will form them below
574
                let allTeams: Team[] = await this.tc.getAllTeams();
5✔
575
                Log.info("AdminController::planProvision( .. ) - # teams: " + allTeams.length);
5✔
576

577
                // just for logging, will remove with filter below
578
                for (const team of allTeams) {
5✔
579
                        if (team.personIds.length < 1) {
9!
580
                                Log.warn("AdminController::planProvision(..) - team has no people: " + team.id);
×
581
                        }
582
                }
583

584
                // remove teams that have no people
585
                allTeams = allTeams.filter((team) => team.personIds.length > 0);
9✔
586
                Log.info("AdminController::planProvision(..) - # teams after removing teams without people: " + allTeams.length);
5✔
587

588
                if (deliv.teamMaxSize === 1) {
5✔
589
                        formSingleTeams = true;
3✔
590
                        Log.info("AdminController::planProvision(..) - team maxSize 1: formSingleTeams forced to true");
3✔
591
                } else {
592
                        Log.info("AdminController::planProvision(..) - team maxSize > 1: formSingleTeams not forced");
2✔
593
                }
594

595
                const delivTeams: Team[] = [];
5✔
596
                for (const team of allTeams) {
5✔
597
                        if (team === null || deliv === null || team.id === null || deliv.id === null) {
9!
598
                                // seeing this during 310 provisioning, need to figure this out
599
                                Log.error(
×
600
                                        "AdminController::planProvision(..) - ERROR! null team: " + JSON.stringify(team) + " or deliv: " + JSON.stringify(deliv)
601
                                );
602
                        } else {
603
                                if (team.delivId === deliv.id) {
9✔
604
                                        Log.info("AdminController::planProvision(..) - adding team: " + team.id + " to delivTeams");
5✔
605
                                        delivTeams.push(team);
5✔
606
                                }
607
                        }
608
                }
609
                Log.info("AdminController::planProvision(..) - # deliv teams: " + delivTeams.length);
5✔
610

611
                // remove any people who are already on teams
612
                for (const team of delivTeams) {
5✔
613
                        for (const personId of team.personIds) {
5✔
614
                                const index = allPeople
7✔
615
                                        .map(function (p: Person) {
616
                                                return p.id;
18✔
617
                                        })
618
                                        .indexOf(personId);
619
                                if (index >= 0) {
7!
620
                                        Log.info("AdminController::planProvision(..) - person already on team: " + personId + " ( team: " + team.id + " )");
7✔
621
                                        allPeople.splice(index, 1);
7✔
622
                                } else {
623
                                        Log.warn("AdminController::planProvision(..) - allPeople does not contain: " + personId);
×
624
                                        const person = await this.pc.getPerson(personId);
×
625
                                        if (person !== null) {
×
626
                                                Log.warn("AdminController::planProvision(..) - person details: " + JSON.stringify(person));
×
627
                                        } else {
628
                                                Log.warn("AdminController::planProvision(..) - person is not in database");
×
629
                                        }
630
                                }
631
                        }
632
                }
633
                Log.trace("AdminController::planProvision(..) - # people not on teams: " + allPeople.length);
5✔
634

635
                if (formSingleTeams === true) {
5✔
636
                        // now create teams for individuals
637
                        Log.info("AdminController::planProvision(..) - handling single teams");
3✔
638
                        for (const individual of allPeople) {
3✔
639
                                try {
5✔
640
                                        const name = await cc.computeNames(deliv, [individual]);
5✔
641
                                        const team = await this.tc.formTeam(name.teamName, deliv, [individual], false);
5✔
642
                                        delivTeams.push(team);
5✔
643
                                } catch (err) {
644
                                        Log.error("AdminController::planProvision(..) - single team creation ERROR: " + err.message);
×
645
                                }
646
                        }
647
                        Log.info("AdminController::planProvision(..) - single teams done");
3✔
648
                }
649

650
                Log.info("AdminController::planProvision(..) - # delivTeams after individual teams added: " + delivTeams.length);
5✔
651

652
                const reposToProvision: Repository[] = [];
5✔
653
                // now process the teams to create their repos
654
                for (const delivTeam of delivTeams) {
5✔
655
                        Log.info("AdminController::planProvision(..) - preparing to provision team: " + delivTeam.id);
10✔
656

657
                        const people: Person[] = [];
10✔
658
                        for (const pId of delivTeam.personIds) {
10✔
659
                                people.push(await this.pc.getPerson(pId));
12✔
660
                        }
661
                        Log.trace("AdminController::planProvision(..) - preparing to provision pIds: " + JSON.stringify(delivTeam.personIds));
10✔
662
                        if (delivTeam.personIds.length !== people.length) {
10!
663
                                Log.warn("AdminController::planProvision(..) - preparing to provision missing people; people: " + JSON.stringify(people));
×
664
                        }
665

666
                        const names = await cc.computeNames(deliv, people);
10✔
667

668
                        Log.info(
10✔
669
                                "AdminController::planProvision(..) - delivTeam: " +
670
                                        delivTeam.id +
671
                                        "; computed team: " +
672
                                        names.teamName +
673
                                        "; computed repo: " +
674
                                        names.repoName
675
                        );
676

677
                        const team = await this.tc.getTeam(names.teamName);
10✔
678
                        let repo = await this.rc.getRepository(names.repoName);
10✔
679

680
                        if (team === null) {
10!
681
                                // sanity checking team must not be null given what we have done above (should never happen)
682
                                throw new Error("AdminController::planProvision(..) - team unexpectedly null: " + name); // s.teamName);
×
683
                        }
684

685
                        if (repo === null) {
10✔
686
                                repo = await this.rc.createRepository(names.repoName, deliv, [team], {});
7✔
687
                        }
688

689
                        if (repo === null) {
10!
690
                                // sanity checking repo must not be null given what we have done above (should never happen)
691
                                throw new Error("AdminController::planProvision(..) - repo unexpectedly null: " + names.repoName); // names.repoName);
×
692
                        }
693

694
                        // /* istanbul ignore if */
695
                        // if (typeof repo.custom.githubCreated !== "undefined" && repo.custom.githubCreated === true && repo.URL === null) {
696
                        //     // HACK: this is just for dealing with inconsistent databases
697
                        //     // This whole block should be removed in the future
698
                        //     Log.warn("AdminController::planProvision(..) - repo URL should not be null: " + repo.id);
699
                        //     const config = Config.getInstance();
700
                        //     repo.URL = config.getProp(ConfigKey.githubHost) + "/" + config.getProp(ConfigKey.org) + "/" + repo.id;
701
                        //     await this.dbc.writeRepository(repo);
702
                        // }
703

704
                        reposToProvision.push(repo);
10✔
705
                        Log.info("AdminController::planProvision(..) - team planning done for team: " + delivTeam.id);
10✔
706
                }
707

708
                Log.info("AdminController::planProvision(..) - # repos to provision: " + reposToProvision.length);
5✔
709

710
                const repoTrans: RepositoryTransport[] = [];
5✔
711
                for (const repo of reposToProvision) {
5✔
712
                        const newRepo = { delivId: deliv.id, id: repo.id, URL: repo.URL, gitHubStatus: repo.gitHubStatus.toString() };
10✔
713
                        repoTrans.push(newRepo);
10✔
714
                }
715

716
                return repoTrans;
5✔
717
        }
718

719
        /**
720
         * Creates the GitHub side of the provided repositories. Only provisions those that
721
         * have not already been configured (e.g., their URL field is null).
722
         *
723
         * Does not release the repos to the students (e.g., the student team is not attached
724
         * to the repository; this should be done with performRelease). Released repos will
725
         * have their Team.URL fields set. e.g., creating the repo sets Repository.URL; releasing
726
         * the repo sets Team.URL (for the student teams associated with the repo).
727
         *
728
         * @param {Repository[]} repos
729
         * @param {string} importURL
730
         * @returns {Promise<Repository[]>}
731
         */
732
        public async performProvision(repos: Repository[], importURL: string): Promise<RepositoryTransport[]> {
733
                const gha = GitHubActions.getInstance(true);
4✔
734
                const ghc = new GitHubController(gha);
4✔
735
                const cc = await Factory.getCourseController(this.gh);
4✔
736

737
                const config = Config.getInstance();
4✔
738
                const dbc = DatabaseController.getInstance();
4✔
739

740
                Log.info("AdminController::performProvision(..) - start; # repos: " + repos.length + "; importURL: " + importURL);
4✔
741
                const provisionedRepos: Repository[] = [];
4✔
742

743
                for (const repo of repos) {
4✔
744
                        try {
8✔
745
                                const start = Date.now();
8✔
746
                                Log.info("AdminController::performProvision(..) ***** START *****; repo: " + repo.id);
8✔
747
                                if (repo.gitHubStatus === GitHubStatus.NOT_PROVISIONED) {
8✔
748
                                        const futureTeams: Array<Promise<Team>> = repo.teamIds.map((teamId) => this.dbc.getTeam(teamId));
5✔
749
                                        const teams: Team[] = await Promise.all(futureTeams);
5✔
750
                                        Log.trace("AdminController::performProvision(..) - about to provision: " + repo.id);
5✔
751
                                        let success = await ghc.provisionRepository(repo.id, teams, importURL);
5✔
752
                                        success = success && (await cc.finalizeProvisionedRepo(repo, teams));
5✔
753
                                        Log.trace("AdminController::performProvision(..) - provisioned: " + repo.id + "; success: " + success);
5✔
754

755
                                        if (success === true) {
5!
756
                                                Log.trace("AdminController::performProvision(..) - success: " + repo.id + "; URL: " + repo.URL);
5✔
757
                                                provisionedRepos.push(repo);
5✔
758
                                        } else {
759
                                                Log.warn("AdminController::performProvision(..) - provision FAILED: " + repo.id + "; URL: " + repo.URL);
×
760
                                        }
761

762
                                        // forced wait unnecessary with the transition to creating repo from template
763
                                        // Log.trace("AdminController::performProvision(..) - done provisioning: " + repo.id + "; forced wait");
764
                                        // await Util.delay(2 * 1000); // after any provisioning wait a bit
765
                                        // Log.info("AdminController::performProvision(..) - done for repo: " + repo.id + "; wait complete");
766
                                        Log.info("AdminController::performProvision(..) ***** DONE *****; repo: " + repo.id + "; took: " + Util.took(start));
5✔
767
                                } else {
768
                                        Log.info("AdminController::performProvision(..) - skipped; already provisioned: " + repo.id + "; URL: " + repo.URL);
3✔
769
                                }
770
                        } catch (err) {
771
                                Log.error("AdminController::performProvision(..) - FAILED: " + repo.id + "; URL: " + repo.URL + "; ERROR: " + err.message);
×
772
                                // would prefer not to rethrow, but the extra logging can be helpful
773
                                throw err;
×
774
                        }
775
                }
776

777
                const provisionedRepositoryTransport: RepositoryTransport[] = [];
4✔
778
                for (const repo of provisionedRepos) {
4✔
779
                        provisionedRepositoryTransport.push(RepositoryController.repositoryToTransport(repo));
5✔
780
                }
781
                return provisionedRepositoryTransport;
4✔
782
        }
783

784
        /**
785
         * Plans the releasing activity for attaching teams to their respective provisioned repositories.
786
         *
787
         * NOTE: this does _not_ provision the repos, or release them. It just creates a plan.
788
         *
789
         * @param {Deliverable} deliv
790
         * @returns {Promise<RepositoryTransport[]>}
791
         */
792
        public async planRelease(deliv: Deliverable): Promise<Repository[]> {
793
                Log.info("AdminController::planRelease( " + deliv.id + " ) - start");
2✔
794
                const cc = await Factory.getCourseController(this.gh);
2✔
795

796
                let allTeams: Team[] = await this.tc.getAllTeams();
2✔
797
                Log.trace("AdminController::planRelease( " + deliv.id + " ) - # teams: " + allTeams.length);
2✔
798

799
                // remove teams that have no people as they don't need to be released
800
                // just for logging, will remove with filter below
801
                for (const team of allTeams) {
2✔
802
                        if (team.personIds.length < 1) {
3!
803
                                Log.warn("AdminController::planRelease(..) - team has no people: " + team.id);
×
804
                        }
805
                }
806

807
                // remove teams that have no people
808
                allTeams = allTeams.filter((team) => team.personIds.length > 0);
3✔
809
                Log.info("AdminController::planRelease(..) - # teams after removing teams without people: " + allTeams.length);
2✔
810

811
                const delivTeams: Team[] = [];
2✔
812
                for (const team of allTeams) {
2✔
813
                        if (team === null || deliv === null || team.id === null || deliv.id === null) {
3!
814
                                // seeing this during 310 provisioning, need to figure this out
815
                                Log.error(
×
816
                                        "AdminController::planRelease(..) - ERROR! null team: " + JSON.stringify(team) + " or deliv: " + JSON.stringify(deliv)
817
                                );
818
                        } else {
819
                                if (team.delivId === deliv.id) {
3✔
820
                                        Log.trace("AdminController::planRelease(..) - adding team: " + team.id + " to delivTeams");
1✔
821
                                        delivTeams.push(team);
1✔
822
                                }
823
                        }
824
                }
825

826
                Log.info("AdminController::planRelease( " + deliv.id + " ) - # deliv teams: " + delivTeams.length);
2✔
827
                const reposToRelease: Repository[] = [];
2✔
828
                const reposAlreadyReleased: Repository[] = [];
2✔
829
                for (const team of delivTeams) {
2✔
830
                        try {
1✔
831
                                Log.trace("AdminController::planRelease( " + deliv.id + " ) - processing team: " + team.id);
1✔
832

833
                                // get repo for team
834
                                const people: Person[] = [];
1✔
835
                                for (const pId of team.personIds) {
1✔
836
                                        people.push(await this.dbc.getPerson(pId));
2✔
837
                                }
838
                                const names = await cc.computeNames(deliv, people);
1✔
839
                                const repo = await this.dbc.getRepository(names.repoName);
1✔
840

841
                                /* istanbul ignore else */
842
                                // if (typeof team.custom.githubAttached === "undefined" || team.custom.githubAttached === false) {
843
                                if (team.gitHubStatus === GitHubStatus.PROVISIONED_UNLINKED) {
1✔
844
                                        /* istanbul ignore else */
845
                                        // if (repo !== null && typeof repo.custom.githubCreated !== "undefined" && repo.custom.githubCreated === true) {
846
                                        if (repo !== null && repo.gitHubStatus !== GitHubStatus.NOT_PROVISIONED) {
1✔
847
                                                // repo exists and has been provisioned: this is important as teams may have formed that have not been provisioned
848
                                                // aka only release provisioned repos
849
                                                reposToRelease.push(repo);
1✔
850
                                        } else {
851
                                                Log.info(
852
                                                        "AdminController::planRelease( " + deliv.id + " ) - repo not provisioned yet: " + JSON.stringify(team.personIds)
853
                                                );
854
                                        }
855
                                } else {
856
                                        Log.info("AdminController::planRelease( " + deliv.id + " ) - skipping team: " + team.id + "; already attached");
857
                                        reposAlreadyReleased.push(repo);
858
                                }
859
                        } catch (err) {
860
                                /* istanbul ignore next: curlies needed for ignore */
861
                                {
862
                                        Log.error("AdminController::planRelease(..) - ERROR: " + err.message);
863
                                        Log.exception(err);
864
                                }
865
                        }
866
                        Log.trace("AdminController::planRelease( " + deliv.id + " ) - done team processing: " + team.id);
1✔
867
                }
868

869
                Log.info("AdminController::planRelease( " + deliv.id + " ) - # repos in release plan: " + reposToRelease.length);
2✔
870

871
                // we want to know all repos whether they are released or not
872
                const allRepos: Repository[] = reposAlreadyReleased;
2✔
873
                for (const toReleaseRepo of reposToRelease) {
2✔
874
                        // toReleaseRepo.URL = null; // HACK, but denotes that it has not been released yet
875
                        toReleaseRepo.gitHubStatus = GitHubStatus.PROVISIONED_UNLINKED; // denotes that repo has not been released yet
1✔
876
                        allRepos.push(toReleaseRepo);
1✔
877
                }
878
                return allRepos;
2✔
879
        }
880

881
        public async performRelease(repos: Repository[]): Promise<RepositoryTransport[]> {
882
                const gha = GitHubActions.getInstance(true);
2✔
883
                const ghc = new GitHubController(gha);
2✔
884

885
                Log.info("AdminController::performRelease(..) - start; # repos: " + repos.length);
2✔
886
                const start = Date.now();
2✔
887

888
                const releasedRepos = [];
2✔
889
                for (const repo of repos) {
2✔
890
                        try {
2✔
891
                                const startRepo = Date.now();
2✔
892
                                // if (repo.URL !== null) {
893
                                // can only release repos that are provisioned
894
                                if (repo.gitHubStatus !== GitHubStatus.NOT_PROVISIONED) {
2!
895
                                        const teams: Team[] = [];
2✔
896
                                        for (const teamId of repo.teamIds) {
2✔
897
                                                teams.push(await this.dbc.getTeam(teamId));
2✔
898
                                        }
899

900
                                        // actually release the repo
901
                                        const success = await ghc.releaseRepository(repo, teams, false);
2✔
902

903
                                        if (success === true) {
2!
904
                                                Log.info("AdminController::performRelease(..) - success: " + repo.id + "; took: " + Util.took(startRepo));
2✔
905
                                                releasedRepos.push(repo);
2✔
906
                                        } else {
907
                                                Log.warn("AdminController::performRelease(..) - FAILED: " + repo.id);
×
908
                                        }
909

910
                                        await Util.delay(200); // after any releasing wait a short bit
2✔
911
                                } else {
912
                                        Log.info("AdminController::performRelease(..) - skipped; repo not yet provisioned: " + repo.id); // + "; URL: " + repo.URL);
×
913
                                }
914
                        } catch (err) {
915
                                Log.error("AdminController::performRelease(..) - FAILED: " + repo.id + "; URL: " + repo.URL + "; ERROR: " + err.message);
×
916
                        }
917
                }
918

919
                const releasedRepositoryTransport: RepositoryTransport[] = [];
2✔
920
                for (const repo of releasedRepos) {
2✔
921
                        releasedRepositoryTransport.push(RepositoryController.repositoryToTransport(repo));
2✔
922
                }
923
                Log.info(
2✔
924
                        "AdminController::performRelease(..) - complete; # released: " +
925
                                releasedRepositoryTransport.length +
926
                                "; took: " +
927
                                Util.took(start)
928
                );
929

930
                return releasedRepositoryTransport;
2✔
931
        }
932

933
        public async makeReposReadOnly(deliv: Deliverable): Promise<RepositoryTransport[]> {
934
                Log.info("AdminController::makeReposReadOnly( " + deliv.id + " ) - start");
×
935
                return [];
×
936
        }
937

938
        public async makeReposWriteable(deliv: Deliverable): Promise<RepositoryTransport[]> {
939
                Log.info("AdminController::makeReposReadOnly( " + deliv.id + " ) - start");
×
940
                return [];
×
941
        }
942

943
        /* istanbul ignore next */
944
        /**
945
         * Synchronizes the database objects with GitHub. Does _NOT_ remove any DB objects, just makes
946
         * sure their properties match those in the GitHub org. This is useful if manual changes are made
947
         * to the org that you want to have updated in the repo as well.
948
         *
949
         * NOTE: team membership is _NOT_ currently read from GitHub and will not be synced.
950
         *
951
         * @param {boolean} dryRun
952
         * @returns {Promise<void>}
953
         */
954
        public async dbSanityCheck(dryRun: boolean): Promise<void> {
955
                Log.info("AdminController::dbSanityCheck() - start");
956
                const start = Date.now();
957

958
                const gha = GitHubActions.getInstance(true);
959
                const tc = new TeamController();
960
                const config = Config.getInstance();
961

962
                let repos = await this.dbc.getRepositories();
963
                for (const repo of repos) {
964
                        Log.info("AdminController::dbSanityCheck() - start; repo: " + repo.id);
965
                        const repoExists = await gha.repoExists(repo.id);
966
                        if (repoExists === true) {
967
                                // make sure repo is consistent
968
                                repo.URL = config.getProp(ConfigKey.githubHost) + "/" + config.getProp(ConfigKey.org) + "/" + repo.id;
969
                                // if (repo.custom.githubCreated !== true) {
970
                                //         Log.warn("AdminController::dbSanityCheck() - repo.custom.githubCreated should not be false for created: " + repo.id);
971
                                //         repo.custom.githubCreated = true;
972
                                // }
973
                                if (repo.gitHubStatus === GitHubStatus.NOT_PROVISIONED) {
974
                                        Log.warn("AdminController::dbSanityCheck() - gitHubStatus should be PROVISIONED for created: " + repo.id);
975
                                        repo.gitHubStatus = GitHubStatus.PROVISIONED_UNLINKED; // linking does not matter for repos
976
                                }
977
                        } else {
978
                                // if (repo.custom.githubCreated !== false) {
979
                                //         Log.warn("AdminController::dbSanityCheck() - repo.custom.githubCreated should not be true for !created: " + repo.id);
980
                                //         repo.custom.githubCreated = false; // does not exist, must not be created
981
                                // }
982
                                //
983
                                // if (repo.custom.githubReleased !== false) {
984
                                //         Log.warn("AdminController::dbSanityCheck() - repo.custom.githubReleased should not be true for !created: " + repo.id);
985
                                //         repo.custom.githubReleased = false; // does not exist, must not be released
986
                                // }
987

988
                                // repo does not exist
989
                                if (repo.gitHubStatus !== GitHubStatus.NOT_PROVISIONED) {
990
                                        Log.warn("AdminController::dbSanityCheck() - gitHubStatus can only be NOT_PROVISIONED for !created: " + repo.id);
991
                                        repo.gitHubStatus = GitHubStatus.NOT_PROVISIONED;
992
                                }
993

994
                                if (repo.URL !== null) {
995
                                        Log.warn("AdminController::dbSanityCheck() - repo.URL should be null for: " + repo.id);
996
                                        repo.URL = null;
997
                                }
998
                        }
999

1000
                        if (dryRun === false) {
1001
                                await this.dbc.writeRepository(repo);
1002
                        }
1003
                        Log.trace("AdminController::dbSanityCheck() - done; repo: " + repo.id);
1004
                }
1005

1006
                let teams = await tc.getAllTeams(); // not DBC because we want special teams filtered out
1007
                for (const team of teams) {
1008
                        Log.info("AdminController::dbSanityCheck() - start; team: " + team.id);
1009

1010
                        let teamNumber: number = -1;
1011
                        if (team.githubId !== null) {
1012
                                // use the cached team id if it exists and is correct (much faster)
1013
                                const tuple = await gha.getTeam(team.githubId);
1014
                                if (tuple !== null && tuple.githubTeamNumber === team.githubId && tuple.teamName === team.id) {
1015
                                        Log.info("AdminController::dbSanityCheck() - using cached gitHubId for team: " + team.id);
1016
                                        teamNumber = team.githubId;
1017
                                }
1018
                        }
1019

1020
                        if (teamNumber <= 0) {
1021
                                Log.info("AdminController::dbSanityCheck() - not using cached gitHubId for team: " + team.id);
1022
                                teamNumber = await gha.getTeamNumber(team.id);
1023
                        }
1024

1025
                        if (teamNumber >= 0) {
1026
                                if (team.githubId !== teamNumber) {
1027
                                        Log.warn("AdminController::dbSanityCheck() - team.githubId should match the GitHub id for: " + team.id);
1028
                                        team.githubId = teamNumber;
1029
                                }
1030
                        } else {
1031
                                if (team.githubId !== null) {
1032
                                        Log.warn("AdminController::dbSanityCheck() - team.githubId should be null: " + team.id);
1033
                                        team.githubId = null; // does not exist, must not have a number
1034
                                }
1035

1036
                                // if (team.custom.githubAttached !== false) {
1037
                                //         Log.warn("AdminController::dbSanityCheck() - team.custom.githubAttached should be false: " + team.id);
1038
                                //         team.custom.githubAttached = false; // does not exist, must not be attached
1039
                                // }
1040

1041
                                if (team.gitHubStatus !== GitHubStatus.NOT_PROVISIONED) {
1042
                                        Log.warn("AdminController::dbSanityCheck() - team should not exist: " + team.id);
1043
                                        team.gitHubStatus = GitHubStatus.NOT_PROVISIONED; // does not exist, must not be attached
1044
                                }
1045
                        }
1046

1047
                        if (dryRun === false) {
1048
                                await this.dbc.writeTeam(team);
1049
                        }
1050
                        Log.trace("AdminController::dbSanityCheck() - done; team: " + team.id);
1051
                }
1052

1053
                repos = await this.dbc.getRepositories();
1054
                const checkedTeams: Team[] = [];
1055
                for (const repo of repos) {
1056
                        Log.info("AdminController::dbSanityCheck() - start; repo second pass: " + repo.id);
1057
                        let repoHasBeenChecked = false;
1058

1059
                        for (const teamId of repo.teamIds) {
1060
                                const team = await this.dbc.getTeam(teamId);
1061

1062
                                const teamsOnRepo = await gha.getTeamsOnRepo(repo.id);
1063
                                let isTeamOnRepo = false;
1064
                                for (const teamOnRepo of teamsOnRepo) {
1065
                                        if (teamOnRepo.teamName === teamId) {
1066
                                                // team is on repo
1067
                                                isTeamOnRepo = true;
1068
                                                repoHasBeenChecked = true;
1069
                                                checkedTeams.push(team);
1070
                                        }
1071
                                }
1072

1073
                                if (isTeamOnRepo === true) {
1074
                                        // if (repo.custom.githubReleased !== true) {
1075
                                        //         repo.custom.githubReleased = true;
1076
                                        //         Log.warn("AdminController::dbSanityCheck() - repo2.custom.githubReleased should be true: " + repo.id);
1077
                                        // }
1078

1079
                                        // if a team is on a repo, it must be provisioned and linked
1080
                                        if (repo.gitHubStatus !== GitHubStatus.PROVISIONED_LINKED) {
1081
                                                // repo.custom.githubReleased = true;
1082
                                                repo.gitHubStatus = GitHubStatus.PROVISIONED_LINKED;
1083
                                                Log.warn("AdminController::dbSanityCheck() - repo2.custom.githubReleased should be true: " + repo.id);
1084
                                        }
1085

1086
                                        // if (team.custom.githubAttached !== true) {
1087
                                        //         team.custom.githubAttached = true;
1088
                                        //         Log.warn("AdminController::dbSanityCheck() - team2.custom.githubAttached should be true: " + team.id);
1089
                                        // }
1090

1091
                                        if (team.gitHubStatus !== GitHubStatus.PROVISIONED_LINKED) {
1092
                                                team.gitHubStatus = GitHubStatus.PROVISIONED_LINKED;
1093
                                                Log.warn("AdminController::dbSanityCheck() - team.gitHubStatus should be PROVISIONED_LINKED: " + team.id);
1094
                                        }
1095

1096
                                        if (dryRun === false) {
1097
                                                await this.dbc.writeRepository(repo);
1098
                                                await this.dbc.writeTeam(team);
1099
                                        }
1100
                                }
1101
                        }
1102

1103
                        if (repoHasBeenChecked === false) {
1104
                                // repos that were not found to have teams must not be released
1105
                                // if (repo.custom.githubReleased !== false) {
1106
                                //         repo.custom.githubReleased = false; // was not found above, must be unreleased
1107
                                if (repo.gitHubStatus !== GitHubStatus.PROVISIONED_UNLINKED) {
1108
                                        repo.gitHubStatus = GitHubStatus.PROVISIONED_UNLINKED;
1109
                                        Log.warn("AdminController::dbSanityCheck() - repo.gitHubStatus should be PROVISIONED_UNLINKED: " + repo.gitHubStatus);
1110

1111
                                        if (dryRun === false) {
1112
                                                await this.dbc.writeRepository(repo);
1113
                                        }
1114
                                }
1115
                        }
1116
                }
1117

1118
                teams = await tc.getAllTeams(); // not DBC because we want special teams filtered out
1119
                for (const team of teams) {
1120
                        let checked = false;
1121
                        for (const checkedTeam of checkedTeams) {
1122
                                if (checkedTeam.id === team.id) {
1123
                                        checked = true;
1124
                                }
1125
                        }
1126
                        if (checked === false) {
1127
                                // teams that were not found with repos must not be attached
1128
                                // if (team.custom.githubAttached !== false) {
1129
                                //         team.custom.githubAttached = false;
1130
                                if (team.gitHubStatus !== GitHubStatus.PROVISIONED_UNLINKED) {
1131
                                        team.gitHubStatus = GitHubStatus.PROVISIONED_UNLINKED;
1132
                                        Log.warn("AdminController::dbSanityCheck() - team.gitHubStatus should be PROVISIONED_UNLINKED: " + team.id);
1133

1134
                                        if (dryRun === false) {
1135
                                                await this.dbc.writeTeam(team);
1136
                                        }
1137
                                }
1138
                        }
1139
                }
1140

1141
                Log.info("AdminController::dbSanityCheck() - done; took: " + Util.took(start));
1142
        }
1143

1144
        private async createDashboardTransport(result: Result): Promise<AutoTestDashboardTransport> {
1145
                const resultSummary = await this.clipAutoTestResult(result);
83✔
1146

1147
                let testPass: string[] = [];
83✔
1148
                let testFail: string[] = [];
83✔
1149
                let testSkip: string[] = [];
83✔
1150
                let testError: string[] = [];
83✔
1151

1152
                if (typeof result.output !== "undefined" && typeof result.output.report !== "undefined") {
83!
1153
                        const report: GradeReport = result.output.report;
83✔
1154
                        if (typeof report.passNames !== "undefined") {
83!
1155
                                testPass = report.passNames;
83✔
1156
                        }
1157
                        if (typeof report.failNames !== "undefined") {
83!
1158
                                testFail = report.failNames;
83✔
1159
                        }
1160
                        if (typeof report.skipNames !== "undefined") {
83!
1161
                                testSkip = report.skipNames;
83✔
1162
                        }
1163
                        if (typeof report.errorNames !== "undefined") {
83!
1164
                                testError = report.errorNames;
83✔
1165
                        }
1166
                }
1167

1168
                return {
83✔
1169
                        ...resultSummary,
1170
                        testPass: testPass,
1171
                        testFail: testFail,
1172
                        testError: testError,
1173
                        testSkip: testSkip,
1174
                        custom: {},
1175
                };
1176
        }
1177

1178
        /**
1179
         * Transforms a Result into an AutoTestResultSummaryTransport
1180
         */
1181
        private async clipAutoTestResult(result: Result): Promise<AutoTestResultSummaryTransport> {
1182
                const repoId = result.input.target.repoId;
141✔
1183
                const repoURL =
1184
                        Config.getInstance().getProp(ConfigKey.githubHost) + "/" + Config.getInstance().getProp(ConfigKey.org) + "/" + repoId;
141✔
1185

1186
                let scoreOverall = null;
141✔
1187
                let scoreCover = null;
141✔
1188
                let scoreTest = null;
141✔
1189

1190
                if (typeof result.output !== "undefined" && typeof result.output.report !== "undefined") {
141!
1191
                        const report = result.output.report;
141✔
1192
                        if (typeof report.scoreOverall !== "undefined") {
141!
1193
                                scoreOverall = report.scoreOverall;
141✔
1194
                        }
1195
                        if (typeof report.scoreTest !== "undefined") {
141!
1196
                                scoreTest = report.scoreTest;
141✔
1197
                        }
1198
                        if (typeof report.scoreCover !== "undefined") {
141!
1199
                                scoreCover = report.scoreCover;
141✔
1200
                        }
1201
                }
1202

1203
                const state = this.selectState(result);
141✔
1204

1205
                return {
141✔
1206
                        repoId: repoId,
1207
                        repoURL: repoURL,
1208
                        delivId: result.delivId,
1209
                        state: state,
1210
                        timestamp: result.output.timestamp,
1211
                        commitSHA: result.input.target.commitSHA,
1212
                        commitURL: result.input.target.commitURL,
1213
                        scoreOverall: scoreOverall,
1214
                        scoreCover: scoreCover,
1215
                        scoreTests: scoreTest,
1216
                        custom: {},
1217
                };
1218
        }
1219

1220
        // NOTE: the default implementation is currently broken; do not use it.
1221
        /**
1222
         * This is a method that subtypes can call from computeNames if they do not want to implement it themselves.
1223
         *
1224
         * @param {Deliverable} deliv
1225
         * @param {Person[]} people
1226
         * @returns {Promise<{teamName: string | null; repoName: string | null}>}
1227
         */
1228
        // public async computeNames(deliv: Deliverable, people: Person[]): Promise<{teamName: string | null, repoName: string | null}> {
1229
        //     Log.info("AdminController::computeNames(..) - start; # people: " + people.length);
1230
        //
1231
        //     // TODO: this code has a fatal flaw; if the team/repo exists already for the specified people,
1232
        //     // it is correct to return those.
1233
        //
1234
        //     let repoPrefix = "";
1235
        //     if (deliv.repoPrefix.length > 0) {
1236
        //         repoPrefix = deliv.repoPrefix;
1237
        //     } else {
1238
        //         repoPrefix = deliv.id;
1239
        //     }
1240
        //
1241
        //     let teamPrefix = "";
1242
        //     if (deliv.teamPrefix.length > 0) {
1243
        //         teamPrefix = deliv.teamPrefix;
1244
        //     } else {
1245
        //         teamPrefix = deliv.id;
1246
        //     }
1247
        //     // the repo name and the team name should be the same, so just use the repo name
1248
        //     const repos = await this.dbc.getRepositories();
1249
        //     let repoCount = 0;
1250
        //     for (const repo of repos) {
1251
        //         if (repo.id.startsWith(repoPrefix)) {
1252
        //             repoCount++;
1253
        //         }
1254
        //     }
1255
        //     let repoName = "";
1256
        //     let teamName = "";
1257
        //
1258
        //     let ready = false;
1259
        //     while (!ready) {
1260
        //         repoName = repoPrefix + "_" + repoCount;
1261
        //         teamName = teamPrefix + "_" + repoCount;
1262
        //         const r = await this.dbc.getRepository(repoName);
1263
        //         const t = await this.dbc.getTeam(teamName);
1264
        //         if (r === null && t === null) {
1265
        //             ready = true;
1266
        //         } else {
1267
        //             Log.warn("AdminController::computeNames(..) - name not available; r: " + repoName + "; t: " + teamName);
1268
        //             repoCount++; // try the next one
1269
        //         }
1270
        //     }
1271
        //     Log.info("AdminController::computeNames(..) - done; r: " + repoName + "; t: " + teamName);
1272
        //     return {teamName: teamName, repoName: repoName};
1273
        // }
1274

1275
        /**
1276
         * Takes a result, and if the VM was successful picks the state of the report.
1277
         *     else returns the state of the VM
1278
         * @param result
1279
         */
1280
        private selectState(result: Result): string {
1281
                // if the VM state is SUCCESS, return the report state
1282
                let state = "UNDEFINED";
141✔
1283
                if (typeof result.output !== "undefined" && typeof result.output.state !== "undefined") {
141!
1284
                        state = result.output.state.toString();
141✔
1285
                }
1286
                if (state === "SUCCESS" && typeof result.output.report.result !== "undefined") {
141!
1287
                        state = result.output.report.result;
141✔
1288
                }
1289
                return state;
141✔
1290
        }
1291
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc