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

ubccpsc / classy / 47029edf-f331-4b4d-bbff-ed4f07561520

19 Dec 2024 09:55PM UTC coverage: 87.102% (-0.2%) from 87.336%
47029edf-f331-4b4d-bbff-ed4f07561520

push

circleci

web-flow
Merge pull request #466 from ubccpsc310/cs310_24w1

Incorporate 310_24w1 improvements.

1108 of 1350 branches covered (82.07%)

Branch coverage included in aggregate %.

3950 of 4457 relevant lines covered (88.62%)

35.81 hits per line

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

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

5
import {Repository, Team} from "../Types";
6
import {DatabaseController} from "./DatabaseController";
1✔
7
import {IGitHubActions} from "./GitHubActions";
8
import {TeamController} from "./TeamController";
1✔
9

10
export interface IGitHubController {
11
    /**
12
     * This is a complex method that provisions an entire repository.
13
     *
14
     * Assumptions: a "staff" repo must also exist.
15
     *
16
     * @param {string} repoName
17
     * @param {Team[]} teams
18
     * @param {string} sourceRepo
19
     * @param {boolean} shouldRelease whether the student team should be added to the repo
20
     * @returns {Promise<boolean>}
21
     */
22
    provisionRepository(repoName: string, teams: Team[], sourceRepo: string, shouldRelease: boolean): Promise<boolean>;
23

24
    createPullRequest(repo: Repository, prName: string): Promise<boolean>;
25

26
    updateBranchProtection(repo: Repository, rules: BranchRule[]): Promise<boolean>;
27

28
    getRepositoryUrl(repo: Repository): Promise<string>;
29

30
    createIssues(repo: Repository, issues: Issue[]): Promise<boolean>;
31

32
    getTeamUrl(team: Team): Promise<string>;
33

34
    releaseRepository(repo: Repository, teams: Team[], asCollaborators?: boolean): Promise<boolean>;
35
}
36

37
export interface GitPersonTuple {
38
    githubId: string;
39
    githubPersonNumber: number;
40
    url: string;
41
}
42

43
export interface GitRepoTuple {
44
    repoName: string;
45
    githubRepoNumber: number;
46
    url: string;
47
}
48

49
export interface GitTeamTuple {
50
    teamName: string;
51
    githubTeamNumber: number;
52
}
53

54
export interface BranchRule {
55
    name: string;
56
    reviews: number;
57
}
58

59
export interface Issue {
60
    title: string;
61
    body: string;
62
    // assignees: string[];
63
}
64

65
export class GitHubController implements IGitHubController {
1✔
66

67
    private readonly dbc = DatabaseController.getInstance();
70✔
68
    // private readonly tc = new TeamController();
69

70
    private gha: IGitHubActions = null;
70✔
71

72
    constructor(gha: IGitHubActions) {
73
        this.gha = gha;
70✔
74
    }
75

76
    public async getRepositoryUrl(repo: Repository): Promise<string> {
77
        Log.info("GitHubController::GetRepositoryUrl - start");
1✔
78
        const c = Config.getInstance();
1✔
79
        const ghHost = c.getProp(ConfigKey.githubHost) + "/" + c.getProp(ConfigKey.org) + "/"; // valid .org use
1✔
80
        const url = ghHost + repo.id;
1✔
81
        Log.info("GitHubController::GetRepositoryUrl( " + repo.id + " ) - URL: " + url);
1✔
82
        return url;
1✔
83
    }
84

85
    public async getTeamUrl(team: Team): Promise<string> {
86
        const c = Config.getInstance();
8✔
87
        // GET /orgs/:org/teams/:team_slug
88
        const teamUrl = c.getProp(ConfigKey.githubHost) + "/orgs/" + c.getProp(ConfigKey.org) + "/teams/" + team.id;
8✔
89
        Log.info("GitHubController::getTeamUrl( " + team.id + " ) - URL: " + teamUrl);
8✔
90
        return teamUrl;
8✔
91
    }
92

93
    /**
94
     * Creates the given repository on GitHub. Returns the Repository object when it is done (or null if it failed).
95
     *
96
     * Repository.URL should be set once the repo is created successfully
97
     * (this is how we can track that the repo exists on GitHub).
98
     *
99
     * @param {string} repoName The name of the Repository
100
     * @param {string} importUrl The repo it should be imported from (if null, no import should take place)
101
     * @param {string} path The subset of the importUrl repo that should be added to the root of the new repo.
102
     * If this is null, undefined, or "", the whole importUrl is imported.
103
     * @returns {Promise<boolean>}
104
     */
105
    public async createRepository(repoName: string, importUrl: string, path?: string): Promise<boolean> {
106
        Log.info("GitHubController::createRepository( " + repoName + ", ...) - start");
3✔
107

108
        // make sure repoName already exists in the database
109
        await this.checkDatabase(repoName, null);
3✔
110

111
        const config = Config.getInstance();
2✔
112
        const host = config.getProp(ConfigKey.publichostname);
2✔
113
        const WEBHOOKADDR = host + "/portal/githubWebhook";
2✔
114

115
        const startTime = Date.now();
2✔
116

117
        // const gh = GitHubActions.getInstance(true);
118

119
        Log.trace("GitHubController::createRepository( " + repoName + " ) - see if repo already exists on GitHub org");
2✔
120
        const repoVal = await this.gha.repoExists(repoName);
2✔
121
        if (repoVal === true) {
2✔
122
            // unable to create a repository if it already exists!
123
            Log.error("GitHubController::createRepository( " + repoName + " ) - Error: Repository already exists " +
1✔
124
                "on GitHub; unable to create a new repository");
125
            throw new Error("createRepository(..) failed; Repository " + repoName + " already exists on GitHub.");
1✔
126
        }
127

128
        try {
1✔
129
            // create the repository
130
            Log.trace("GitHubController::createRepository( " + repoName + " ) - create GitHub repo");
1✔
131
            const repoCreateVal = await this.gha.createRepo(repoName);
1✔
132
            Log.trace("GitHubController::createRepository( " + repoName + " ) - success; repo: " + repoCreateVal);
1✔
133
        } catch (err) {
134
            /* istanbul ignore next: braces needed for ignore */
135
            {
136
                Log.error("GitHubController::createRepository( " + repoName + " ) - create repo error: " + err);
137
                // repo creation failed; remove if needed (requires createRepo be permissive if already exists)
138
                const res = await this.gha.deleteRepo(repoName);
139
                Log.info("GitHubController::createRepository( " + repoName + " ) - repo removed: " + res);
140
                throw new Error("createRepository(..) failed; Repository " + repoName + " creation failed; ERROR: " + err.message);
141
            }
142
        }
143

144
        try {
1✔
145
            // still add staff team with push, just not students
146
            Log.trace("GitHubController::createRepository( " + repoName + " ) - add staff team to repo");
1✔
147
            // const staffTeamNumber = await this.tc.getTeamNumber(TeamController.STAFF_NAME);
148
            // Log.trace("GitHubController::createRepository(..) - staffTeamNumber: " + staffTeamNumber);
149
            // const staffAdd = await this.gha.addTeamToRepo(staffTeamNumber, repoName, "admin");
150
            const staffAdd = await this.gha.addTeamToRepo(TeamController.STAFF_NAME, repoName, "admin");
1✔
151
            Log.trace("GitHubController::createRepository(..) - team name: " + staffAdd.teamName);
1✔
152

153
            Log.trace("GitHubController::createRepository( " + repoName + " ) - add admin team to repo");
1✔
154
            // const adminTeamNumber = await this.tc.getTeamNumber(TeamController.ADMIN_NAME);
155
            // Log.trace("GitHubController::createRepository(..) - adminTeamNumber: " + adminTeamNumber);
156
            // const adminAdd = await this.gha.addTeamToRepo(adminTeamNumber, repoName, "admin");
157
            const adminAdd = await this.gha.addTeamToRepo(TeamController.ADMIN_NAME, repoName, "admin");
1✔
158
            Log.trace("GitHubController::createRepository(..) - team name: " + adminAdd.teamName);
1✔
159

160
            // add webhooks
161
            Log.trace("GitHubController::createRepository( " + repoName + " ) - add webhook");
1✔
162
            const createHook = await this.gha.addWebhook(repoName, WEBHOOKADDR);
1✔
163
            Log.trace("GitHubController::createRepository(..) - webhook successful: " + createHook);
1✔
164

165
            // perform import
166
            const c = Config.getInstance();
1✔
167
            const targetUrl = c.getProp(ConfigKey.githubHost) + "/" + c.getProp(ConfigKey.org) + "/" + repoName;
1✔
168

169
            Log.trace("GitHubController::createRepository( " + repoName + " ) - importing project (slow)");
1✔
170
            let output;
171
            /* istanbul ignore if */
172
            if (typeof path !== "undefined") {
1✔
173
                output = await this.gha.importRepoFS(importUrl, targetUrl, path);
174
            } else {
175
                output = await this.gha.importRepoFS(importUrl, targetUrl);
1✔
176
            }
177
            Log.trace("GitHubController::createRepository( " + repoName + " ) - import complete; success: " + output);
1✔
178

179
            Log.trace("GithubController::createRepository( " + repoName + " ) - successfully completed; " +
1✔
180
                "took: " + Util.took(startTime));
181

182
            return true;
1✔
183
        } catch (err) {
184
            Log.error("GithubController::createRepository( " + repoName + " ) - ERROR: " + err);
×
185
            return false;
×
186
        }
187
    }
188

189
    /**
190
     * Creates the given repository on GitHub. Returns the Repository object when it is done (or null if it failed).
191
     *
192
     * Repository.URL should be set once the repo is created successfully
193
     * (this is how we can track that the repo exists on GitHub).
194
     *
195
     * @param {string} repoName The name of the Repository.
196
     * @param {string} importUrl The repo it should be imported from (if null, no import will take place).
197
     * @param {string} branchesToKeep The subset of the branches from the imported repo that should exist in the created repo.
198
     * If undefined or [], all branches are retained.
199
     * @returns {Promise<boolean>}
200
     */
201
    public async createRepositoryFromTemplate(repoName: string, importUrl: string, branchesToKeep?: string[]): Promise<boolean> {
202
        Log.info("GitHubController::createRepositoryFromTemplate( " + repoName + ", ...) - start");
×
203

204
        // make sure repoName already exists in the database
205
        await this.checkDatabase(repoName, null);
×
206

207
        const config = Config.getInstance();
×
208
        const host = config.getProp(ConfigKey.publichostname);
×
209
        const WEBHOOKADDR = host + "/portal/githubWebhook";
×
210

211
        const startTime = Date.now();
×
212

213
        if (typeof branchesToKeep === "undefined") {
×
214
            branchesToKeep = [];
×
215
        }
216

217
        // const gh = GitHubActions.getInstance(true);
218

219
        Log.trace("GitHubController::createRepositoryFromTemplate( " + repoName + " ) - see if repo already exists");
×
220
        const repoVal = await this.gha.repoExists(repoName);
×
221
        if (repoVal === true) {
×
222
            // unable to create a repository if it already exists!
223
            Log.error("GitHubController::createRepositoryFromTemplate( " + repoName + " ) - Error: " +
×
224
                "Repository already exists; unable to create a new repository");
225
            throw new Error("createRepositoryFromTemplate( " + repoName + " ) failed; " +
×
226
                "Repository " + repoName + " already exists.");
227
        }
228

229
        try {
×
230
            // create the repository
231
            Log.trace("GitHubController::createRepositoryFromTemplate( " + repoName + " ) - create GitHub repo");
×
232
            const repoCreateVal = await this.gha.createRepo(repoName);
×
233
            Log.trace("GitHubController::createRepositoryFromTemplate( " + repoName + " ) - success; " +
×
234
                "repo: " + repoCreateVal);
235
        } catch (err) {
236
            /* istanbul ignore next: braces needed for ignore */
237
            {
238
                Log.error("GitHubController::createRepositoryFromTemplate( " + repoName + " ) - create repo error: " + err);
239
                // repo creation failed; remove if needed (requires createRepo be permissive if already exists)
240
                const res = await this.gha.deleteRepo(repoName);
241
                Log.info("GitHubController::createRepositoryFromTemplate( " + repoName + " ) - repo removed: " + res);
242
                throw new Error("createRepository( " + repoName + " ) creation failed; ERROR: " + err.message);
243
            }
244
        }
245

246
        if (branchesToKeep.length > 0) {
×
247
            // TODO: remove any branches we do not need
248
        } else {
249
            Log.info("GitHubController::createRepositoryFromTemplate( " + repoName + " ) - all branches included");
×
250
        }
251

252
        try {
×
253
            // still add staff team with push, just not students
254
            Log.trace("GitHubController::createRepositoryFromTemplate( " + repoName + " ) - add staff team to repo");
×
255
            // const staffTeamNumber = await this.tc.getTeamNumber(TeamController.STAFF_NAME);
256
            // Log.trace("GitHubController::createRepository(..) - staffTeamNumber: " + staffTeamNumber);
257
            // const staffAdd = await this.gha.addTeamToRepo(staffTeamNumber, repoName, "admin");
258
            const staffAdd = await this.gha.addTeamToRepo(TeamController.STAFF_NAME, repoName, "admin");
×
259
            Log.trace("GitHubController::createRepositoryFromTemplate(..) - team name: " + staffAdd.teamName);
×
260

261
            Log.trace("GitHubController::createRepositoryFromTemplate( " + repoName + " ) - add admin team to repo");
×
262
            // const adminTeamNumber = await this.tc.getTeamNumber(TeamController.ADMIN_NAME);
263
            // Log.trace("GitHubController::createRepository(..) - adminTeamNumber: " + adminTeamNumber);
264
            // const adminAdd = await this.gha.addTeamToRepo(adminTeamNumber, repoName, "admin");
265
            const adminAdd = await this.gha.addTeamToRepo(TeamController.ADMIN_NAME, repoName, "admin");
×
266
            Log.trace("GitHubController::createRepositoryFromTemplate(..) - team name: " + adminAdd.teamName);
×
267

268
            // add webhooks
269
            Log.trace("GitHubController::createRepositoryFromTemplate( " + repoName + " ) - add webhook");
×
270
            const createHook = await this.gha.addWebhook(repoName, WEBHOOKADDR);
×
271
            Log.trace("GitHubController::createRepositoryFromTemplate(..) - webook successful: " + createHook);
×
272

273
            // perform import
274
            const c = Config.getInstance();
×
275
            const targetUrl = c.getProp(ConfigKey.githubHost) + "/" + c.getProp(ConfigKey.org) + "/" + repoName;
×
276

277
            Log.trace("GitHubController::createRepositoryFromTemplate( " + repoName + " ) - importing project (slow)");
×
278
            const output = await this.gha.importRepoFS(importUrl, targetUrl);
×
279
            Log.trace("GitHubController::createRepositoryFromTemplate( " + repoName + " ) - import complete; " +
×
280
                "success: " + output);
281

282
            Log.trace("GithubController::createRepositoryFromTemplate( " + repoName + " ) - successfully completed; " +
×
283
                "took: " + Util.took(startTime));
284

285
            return true;
×
286
        } catch (err) {
287
            Log.error("GithubController::createRepositoryFromTemplate( " + repoName + " ) - ERROR: " + err);
×
288
            return false;
×
289
        }
290
    }
291

292
    /**
293
     * Releases a repository to a team.
294
     *
295
     * @param {Repository} repo The repository to be released. This must be in the datastore.
296
     * @param {Team[]} teams The teams to be added. These must be in the datastore.
297
     * @param {boolean} asCollaborators Whether the team members should be added as a collaborators
298
     * or whether a GitHub team should be created for them.
299
     * @returns {Promise<Repository | null>}
300
     */
301
    public async releaseRepository(repo: Repository,
302
                                   teams: Team[],
303
                                   asCollaborators: boolean = false): Promise<boolean> {
×
304
        Log.info("GitHubController::releaseRepository( {" + repo.id + ", ...}, ...) - start");
5✔
305
        const start = Date.now();
5✔
306

307
        await this.checkDatabase(repo.id, null);
5✔
308

309
        // const gh = GitHubActions.getInstance(true);
310

311
        for (const team of teams) {
5✔
312
            if (asCollaborators) {
5✔
313
                Log.info("GitHubController::releaseRepository(..) - releasing repository as " +
1✔
314
                    "individual collaborators");
315
                Log.error("GitHubController::releaseRepository(..) - ERROR: Not implemented");
1✔
316
                throw new Error("GitHubController - w/ collaborators NOT IMPLEMENTED");
1✔
317
            } else {
318

319
                await this.checkDatabase(null, team.id);
4✔
320

321
                // const teamNum = await this.tc.getTeamNumber(team.id);
322
                // const res = await this.gha.addTeamToRepo(teamNum, repo.id, "push");
323
                const res = await this.gha.addTeamToRepo(team.id, repo.id, "push");
3✔
324
                // now, add the team to the repository
325
                // const res = await this.gha.addTeamToRepo(team.id, repo.id, "push");
326
                if (res.githubTeamNumber > 0) {
3!
327
                    // keep track of team addition
328
                    team.custom.githubAttached = true;
3✔
329
                } else {
330
                    Log.error("GitHubController::releaseRepository(..) - ERROR adding team to repo: " + JSON.stringify(res));
×
331
                    team.custom.githubAttached = false;
×
332
                }
333

334
                await this.dbc.writeTeam(team); // add new properties to the team
3✔
335
                Log.info("GitHubController::releaseRepository(..) - " +
3✔
336
                    " added team (" + team.id + " ) with push permissions to repository (" + repo.id + ")");
337
            }
338
        }
339

340
        Log.info("GitHubController::releaseRepository( " + repo.id + ", ... ) - done; took: " + Util.took(start));
3✔
341
        return true;
3✔
342
    }
343

344
    public async provisionRepository(repoName: string,
345
                                     teams: Team[],
346
                                     importUrl: string): Promise<boolean> {
347
        Log.info("GitHubController::provisionRepository( " + repoName + ", ...) - start");
8✔
348
        const dbc = DatabaseController.getInstance();
8✔
349

350
        const start = Date.now();
8✔
351

352
        if (teams.length < 1 || teams.length > 1) {
8✔
353
            Log.warn("GitHubController::provisionRepository(..) - only the first team will be added to the repo");
3✔
354
        }
355

356
        Log.info("GitHubController::provisionRepository( " + repoName + " ) - checking to see if repo already exists");
8✔
357
        let repo = await dbc.getRepository(repoName);
8✔
358
        if (repo === null) {
8✔
359
            // repo object should be in datastore before we try to provision it
360
            throw new Error("GitHubController::provisionRepository( " + repoName +
1✔
361
                " ) - repo does not exist in datastore (but should)");
362
        }
363

364
        const repoExists = await this.gha.repoExists(repoName);
7✔
365
        Log.info("GitHubController::provisionRepository( " + repoName + " ) - repo exists: " + repoExists);
7✔
366
        if (repoExists === true) {
7✔
367
            // this is fatal, we cannot provision a repo that already exists
368
            Log.error("GitHubController::provisionRepository( " + repoName + " ) - repo already exists on GitHub; provisioning failed");
1✔
369
            throw new Error("provisionRepository( " + repoName + " ) failed; Repository " + repoName + " already exists.");
1✔
370
        }
371

372
        let repoVal;
373
        try {
6✔
374
            // create a repo
375
            Log.info("GitHubController::provisionRepository( " + repoName + " ) - creating GitHub repo");
6✔
376
            // this is the create and import w/ fs flow
377
            // repoVal = await this.gha.createRepo(repoName); // only creates repo, contents are added later
378

379
            // this is the create and import w/ template flow
380
            // this string munging feels unfortunate, but the data is all there and should always be structured this way
381
            // if there is a problem, fail fast
382
            let importOwner = "";
6✔
383
            let importRepo = "";
6✔
384
            const importBranchesToKeep = [];
6✔
385
            try {
6✔
386
                importOwner = importUrl.split("/")[3];
6✔
387
                importRepo = importUrl.split("/")[4];
6✔
388
                // remove branch from the end of the repo name
389
                if (importRepo.includes("#")) {
6✔
390
                    importRepo = importRepo.split("#")[0];
5✔
391
                }
392
                // remove .git from the end of the repo name
393
                if (importRepo.endsWith(".git")) {
6✔
394
                    importRepo = importRepo.substring(0, importRepo.length - 4);
5✔
395
                }
396

397
                if (importUrl.includes("#")) {
6✔
398
                    // if a branch is specified in the import URL, we need to keep only it
399
                    const splitUrl = importUrl.split("#");
5✔
400
                    importBranchesToKeep.push(splitUrl[1]);
5✔
401
                }
402

403
                // fail if problem encountered
404
                if (importOwner.length < 1) {
6!
405
                    throw new Error("Owner name is empty");
×
406
                }
407
                if (importRepo.length < 1) {
6!
408
                    throw new Error("Repo name is empty");
×
409
                }
410
                if (importBranchesToKeep.length > 0 && importBranchesToKeep[0].length < 1) {
6!
411
                    throw new Error("Invalid branches to keep: " + JSON.stringify(importBranchesToKeep));
×
412
                }
413
            } catch (err) {
414
                Log.error("GitHubController::provisionRepository( " + repoName + " ) - error parsing import URL: " +
×
415
                    importUrl + "; err: " + err.message);
416
                throw new Error("provisionRepository( " + repoName + " ) creating repo failed; ERROR: " + err.message);
×
417
            }
418

419
            Log.info("GitHubController::provisionRepository( " + repoName + " ) - importing: " + importOwner + "/" + importRepo +
6✔
420
                "; branchesToKeep: " + JSON.stringify(importBranchesToKeep));
421

422
            repoVal = await this.gha.createRepoFromTemplate(repoName, importOwner, importRepo); // creates repo and imports contents
6✔
423

424
            if (importBranchesToKeep.length > 0) {
6✔
425
                // prune branches
426
                const branchRemovalSuccess = await this.gha.deleteBranches(repoName, importBranchesToKeep);
5✔
427
                Log.info("GitHubController::provisionRepository( " + repoName + " ) - branch removal success: " + branchRemovalSuccess);
5✔
428

429
                // rename branches
430
                // since we are only keeping one branch, make sure it is renamed to main
431
                if (importBranchesToKeep[0] !== "main") {
5!
432
                    const branchRenameSuccess = await this.gha.renameBranch(repoName, importBranchesToKeep[0], "main");
5✔
433
                    Log.info("GitHubController::provisionRepository( " + repoName + " ) - branch rename success: " + branchRenameSuccess);
5✔
434
                }
435
            } else {
436
                Log.info("GitHubController::provisionRepository( " + repoName + " ) - no branch specified; all branches kept");
1✔
437
            }
438

439
            Log.trace("GitHubController::provisionRepository( " + repoName + " ) - updating repo");
6✔
440
            // since we moved to template provisioning, we need to update the repo to make sure the settings are correct
441
            const updateWorked = await this.gha.updateRepo(repoName);
6✔
442
            Log.trace("GitHubController::provisionRepository( " + repoName + " ) - repo updated: " + updateWorked);
6✔
443

444
            Log.info("GitHubController::provisionRepository( " + repoName + " ) - GitHub repo created");
6✔
445

446
            // we consider the repo to be provisioned once the whole flow is done
447
            // callers of this method should instead set the URL field
448
            repo = await dbc.getRepository(repoName);
6✔
449
            repo.custom.githubCreated = true;
6✔
450
            await dbc.writeRepository(repo);
6✔
451

452
            Log.info("GitHubController::provisionRepository( " + repoName + " ) - val: " + repoVal);
6✔
453
        } catch (err) {
454
            /* istanbul ignore next: braces needed for ignore */
455
            {
456
                Log.error("GitHubController::provisionRepository( " + repoName + " ) - create repo ERROR: " + err);
457
                // repo creation failed; remove if needed (requires createRepo be permissive if already exists)
458
                const res = await this.gha.deleteRepo(repoName);
459
                Log.info("GitHubController::provisionRepository( " + repoName + " ) - repo removed: " + res);
460
                throw new Error("provisionRepository( " + repoName + " ) failed; failed to create repo; ERROR: " + err.message);
461
            }
462
        }
463

464
        const tc = new TeamController();
6✔
465
        try {
6✔
466
            let teamValue = null;
6✔
467
            try {
6✔
468
                Log.info("GitHubController::provisionRepository() - create GitHub team(s): " + JSON.stringify(teams));
6✔
469
                for (const team of teams) {
6✔
470

471
                    Log.trace("GitHubController::provisionRepository() - team: " + JSON.stringify(team));
7✔
472
                    const dbT = await dbc.getTeam(team.id);
7✔
473
                    if (dbT === null) {
7!
474
                        throw new Error("GitHubController::provisionRepository( " + repoName + " ) - " +
×
475
                            "team does not exist in datastore (but should): " + team.id);
476
                    }
477
                    Log.trace("GitHubController::provisionRepository() - dbT: " + JSON.stringify(dbT));
7✔
478

479
                    const teamNum = await tc.getTeamNumber(team.id);
7✔
480
                    Log.trace("GitHubController::provisionRepository() - dbT team Number: " + teamNum);
7✔
481
                    if (team.URL !== null && teamNum !== null) {
7!
482
                        // already exists
483
                        Log.warn("GitHubController::provisionRepository( " + repoName + " ) - team already exists: " +
×
484
                            teamValue.teamName + "; assuming team members on github are correct.");
485
                    } else {
486

487
                        teamValue = await this.gha.createTeam(team.id, "push");
7✔
488
                        Log.info("GitHubController::provisionRepository( " + repoName + " ) - teamCreate: " + teamValue.teamName);
7✔
489

490
                        if (teamValue.githubTeamNumber > 0) {
7!
491
                            // worked
492

493
                            // team.URL = teamValue.URL;
494
                            team.URL = await this.getTeamUrl(team);
7✔
495
                            team.githubId = teamValue.githubTeamNumber;
7✔
496
                            team.custom.githubAttached = false; // attaching happens in release
7✔
497
                            await dbc.writeTeam(team);
7✔
498
                        }
499

500
                        Log.info("GitHubController::provisionRepository( " + repoName + " ) - add members to GitHub team: " + team.id);
7✔
501

502
                        // convert personIds to githubIds
503
                        const memberGithubIds: string[] = [];
7✔
504
                        for (const personId of team.personIds) {
7✔
505
                            const person = await this.dbc.getPerson(personId);
11✔
506
                            memberGithubIds.push(person.githubId);
11✔
507
                        }
508

509
                        const addMembers = await this.gha.addMembersToTeam(teamValue.teamName, memberGithubIds);
7✔
510
                        Log.info("GitHubController::provisionRepository( " + repoName + " ) - addMembers: " + addMembers.teamName);
7✔
511
                    }
512
                }
513
            } catch (err) {
514
                Log.warn("GitHubController::provisionRepository() - create team ERROR: " + err);
×
515
                // swallow these errors and keep going
516
            }
517

518
            // add staff team to repo
519
            Log.trace("GitHubController::provisionRepository() - add staff team to repo");
6✔
520
            const staffAdd = await this.gha.addTeamToRepo(TeamController.STAFF_NAME, repoName, "admin");
6✔
521
            Log.trace("GitHubController::provisionRepository(..) - team name: " + staffAdd.teamName);
6✔
522

523
            // add admin team to repo
524
            Log.trace("GitHubController::provisionRepository() - add admin team to repo");
6✔
525
            const adminAdd = await this.gha.addTeamToRepo(TeamController.ADMIN_NAME, repoName, "admin");
6✔
526
            Log.trace("GitHubController::provisionRepository(..) - team name: " + adminAdd.teamName);
6✔
527

528
            // add webhooks to repo
529
            const host = Config.getInstance().getProp(ConfigKey.publichostname);
6✔
530
            const WEBHOOKADDR = host + "/portal/githubWebhook";
6✔
531
            Log.trace("GitHubController::provisionRepository() - add webhook to: " + WEBHOOKADDR);
6✔
532
            const createHook = await this.gha.addWebhook(repoName, WEBHOOKADDR);
6✔
533
            Log.trace("GitHubController::provisionRepository(..) - webhook successful: " + createHook);
6✔
534

535
            // this was the import from fs flow which is not needed if we are using import from template instead
536
            // perform import
537
            // const c = Config.getInstance();
538
            // const targetUrl = c.getProp(ConfigKey.githubHost) + "/" + c.getProp(ConfigKey.org) + "/" + repoName;
539
            // Log.trace("GitHubController::provisionRepository() - importing project (slow)");
540
            // const output = await this.gha.importRepoFS(importUrl, targetUrl);
541
            // Log.trace("GitHubController::provisionRepository(..) - import complete; success: " + output);
542

543
            Log.trace("GitHubController::provisionRepository(..) - successfully completed for: " +
6✔
544
                repoName + "; took: " + Util.took(start));
545

546
            return true;
6✔
547
        } catch (err) {
548
            Log.error("GitHubController::provisionRepository(..) - ERROR: " + err);
×
549
        }
550
        return false;
×
551
    }
552

553
    public async updateBranchProtection(repo: Repository, rules: BranchRule[]): Promise<boolean> {
554
        if (repo === null) {
2✔
555
            throw new Error("GitHubController::updateBranchProtection(..) - null repo");
1✔
556
        }
557

558
        Log.info("GitHubController::updateBranchProtection(", repo.id, ", ...) - start");
1✔
559
        if (!await this.gha.repoExists(repo.id)) {
1!
560
            throw new Error("GitHubController::updateBranchProtection() - " + repo.id + " did not exist");
×
561
        }
562
        const successes = await Promise.all(rules.map((r) => this.gha.addBranchProtectionRule(repo.id, r)));
1✔
563
        const allSuccess = successes.reduce((a, b) => a && b, true);
1✔
564
        Log.info("GitHubController::updateBranchProtection(", repo.id, ") - All rules added successfully:", allSuccess);
1✔
565
        return allSuccess;
1✔
566
    }
567

568
    public async createIssues(repo: Repository, issues: Issue[]): Promise<boolean> {
569
        if (repo === null) {
2✔
570
            throw new Error("GitHubController::createIssues(..) - null repo");
1✔
571
        }
572

573
        Log.info("GitHubController::createIssues(", repo.id, ", ...) - start");
1✔
574
        if (!await this.gha.repoExists(repo.id)) {
1!
575
            throw new Error("GitHubController::createIssues() - " + repo.id + " did not exist");
×
576
        }
577
        const successes = await Promise.all(issues.map((issue) => this.gha.makeIssue(repo.id, issue)));
1✔
578
        const allSuccess = successes.every((success) => success === true);
1✔
579
        Log.info("GitHubController::createIssues(", repo.id, ") - All issues created successfully:", allSuccess);
1✔
580
        return allSuccess;
1✔
581
    }
582

583
    /**
584
     * Calls the patch tool
585
     * @param {Repository} repo Repo to be patched
586
     * @param {string} prName Name of the patch to apply
587
     * @param {boolean} dryrun Whether to do a practice patch
588
     *        i.e.: if dryrun is false  -> patch is applied to repo
589
     *              elif dryrun is true -> patch is not applied,
590
     *                   but otherwise will behave as if it was
591
     * @param {boolean} root
592
     */
593
    public async createPullRequest(repo: Repository, prName: string, dryrun: boolean = false, root: boolean = false): Promise<boolean> {
×
594
        Log.info(`GitHubController::createPullRequest(..) - Repo: (${repo.id}) start`);
×
595
        throw new Error("Not implemented"); // code below used to work but depended on service that no longer exists
×
596
        // // if (repo.cloneURL === null || repo.cloneURL === undefined) {
597
        // //     Log.error(`GitHubController::createPullRequest(..) - ${repo.id} did not have a valid cloneURL associated with it.`);
598
        // //     return false;
599
        // // }
600
        //
601
        // const baseUrl: string = Config.getInstance().getProp(ConfigKey.patchToolUrl);
602
        // const patchUrl: string = `${baseUrl}/autopatch`;
603
        // const updateUrl: string = `${baseUrl}/update`;
604
        // const qs: string = Util.getQueryStr({
605
        //     patch_id: prName, github_url: `${repo.URL}.git`, dryrun: String(dryrun), from_beginning: String(root)
606
        // });
607
        //
608
        // const options: RequestInit = {
609
        //     method: "POST",
610
        //     agent: new http.Agent()
611
        // };
612
        //
613
        // let result;
614
        //
615
        // try {
616
        //     await fetch(patchUrl + qs, options);
617
        //     Log.info("GitHubController::createPullRequest(..) - Patch applied successfully");
618
        //     return true;
619
        // } catch (err) {
620
        //     result = err;
621
        // }
622
        //
623
        // switch (result.statusCode) {
624
        //     case 424:
625
        //         Log.info(`GitHubController::createPullRequest(..) - ${prName} was not found by the patchtool. Updating patches.`);
626
        //         try {
627
        //             await fetch(updateUrl, options);
628
        //             Log.info(`GitHubController::createPullRequest(..) - Patches updated successfully. Retrying.`);
629
        //             await fetch(patchUrl + qs, {...options});
630
        //             Log.info("GitHubController::createPullRequest(..) - Patch applied successfully on second attempt");
631
        //             return true;
632
        //         } catch (err) {
633
        //             Log.error("GitHubController::createPullRequest(..) - Patch failed on second attempt. "+
634
        //                 "Message from patchtool server:" + result.message);
635
        //             return false;
636
        //         }
637
        //     case 500:
638
        //         Log.error(
639
        //             `GitHubController::createPullRequest(..) - patchtool internal error. " +
640
        //             "Message from patchtool server: ${result.message}`
641
        //         );
642
        //         return false;
643
        //     default:
644
        //         Log.error(
645
        //             `GitHubController::createPullRequest(..) - was not able to make a connection to patchtool. Error: ${result.message}`
646
        //         );
647
        //         return false;
648
        // }
649
    }
650

651
    /**
652
     * Checks to make sure the repoName or teamName (or both, if specified) are in the database.
653
     *
654
     * This is like an assertion that should be picked up by tests, although it should never
655
     * happen in production (if our suite is any good).
656
     *
657
     * NOTE: ASYNC FUNCTION!
658
     *
659
     * @param {string | null} repoName
660
     * @param {string | null} teamName
661
     * @returns {Promise<boolean>}
662
     */
663
    private async checkDatabase(repoName: string | null, teamName: string | null): Promise<boolean> {
664
        Log.trace("GitHubController::checkDatabase( repo:_" + repoName + "_, team:_" + teamName + "_) - start");
12✔
665
        const dbc = DatabaseController.getInstance();
12✔
666
        if (repoName !== null) {
12✔
667
            const repo = await dbc.getRepository(repoName);
8✔
668
            if (repo === null) {
8✔
669
                const msg = "Repository: " + repoName +
1✔
670
                    " does not exist in datastore; make sure you add it before calling this operation";
671
                Log.error("GitHubController::checkDatabase() - repo ERROR: " + msg);
1✔
672
                throw new Error(msg);
1✔
673
            } else {
674
                // ensure custom property is there
675
                /* istanbul ignore if */
676
                if (typeof repo.custom === "undefined" || repo.custom === null || typeof repo.custom !== "object") {
7✔
677
                    const msg = "Repository: " + repoName + " has a non-object .custom property";
678
                    Log.error("GitHubController::checkDatabase() - repo ERROR: " + msg);
679
                    throw new Error(msg);
680
                }
681
            }
682
        }
683

684
        if (teamName !== null) {
11✔
685
            const team = await dbc.getTeam(teamName);
4✔
686
            if (team === null) {
4✔
687
                const msg = "Team: " + teamName +
1✔
688
                    " does not exist in datastore; make sure you add it before calling this operation";
689
                Log.error("GitHubController::checkDatabase() - team ERROR: " + msg);
1✔
690
                throw new Error(msg);
1✔
691
            } else {
692
                // ensure custom property is there
693
                /* istanbul ignore if */
694
                if (typeof team.custom === "undefined" || team.custom === null || typeof team.custom !== "object") {
3✔
695
                    const msg = "Team: " + teamName + " has a non-object .custom property";
696
                    Log.error("GitHubController::checkDatabase() - team ERROR: " + msg);
697
                    throw new Error(msg);
698
                }
699
            }
700
        }
701
        Log.trace("GitHubController::checkDatabase( repo:_" + repoName + "_, team:_" + teamName + "_) - exists");
10✔
702
        return true;
10✔
703
    }
704
}
705

706
// /* istanbul ignore next */
707
//
708
// // tslint:disable-next-line
709
// export class TestGitHubController implements IGitHubController {
710
//
711
//     public async getRepositoryUrl(repo: Repository): Promise<string> {
712
//         Log.warn("TestGitHubController::getRepositoryUrl(..) - TEST");
713
//         return "TestGithubController_URL";
714
//     }
715
//
716
//     public async getTeamUrl(team: Team): Promise<string> {
717
//         Log.warn("TestGitHubController::getTeamUrl(..) - TEST");
718
//         // const URL = this.gha.getTeamNumber()
719
//         return "TestGithubController_TeamName";
720
//     }
721
//
722
//     public async provisionRepository(repoName: string,
723
//                                      teams: Team[],
724
//                                      sourceRepo: string): Promise<boolean> {
725
//         Log.warn("TestGitHubController::provisionRepository(..) - TEST");
726
//         return true;
727
//     }
728
//
729
//     public async createPullRequest(repo: Repository, prName: string): Promise<boolean> {
730
//         Log.warn("TestGitHubController::createPullRequest(..) - TEST");
731
//         return true;
732
//     }
733
//
734
//     public async updateBranchProtection(repo: Repository, rules: BranchRule[]): Promise<boolean> {
735
//         Log.warn("TestGitHubController::updateBranchProtection(..) - TEST");
736
//         return true;
737
//     }
738
//
739
//     public async createIssues(repo: Repository, issues: Issue[]): Promise<boolean> {
740
//         Log.warn("TestGitHubController::createIssues(..) - TEST");
741
//         return true;
742
//     }
743
//
744
//     public async releaseRepository(repo: Repository,
745
//                                    teams: Team[],
746
//                                    asCollaborators: boolean = false): Promise<boolean> {
747
//         Log.warn("TestGitHubController::releaseRepository(..) - TEST");
748
//         return true;
749
//     }
750
// }
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