Skip to content

Commit ba3989f

Browse files
authored
Add unit tests (#17)
* initial cut of tests * add run scripts
1 parent 62e3da7 commit ba3989f

13 files changed

+15896
-1844
lines changed

Diff for: .gitignore

+3-1
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,6 @@ pnpm-debug.log*
2121
*.ntvs*
2222
*.njsproj
2323
*.sln
24-
*.sw?
24+
*.sw?
25+
26+
wallaby.conf.js

Diff for: actions.js

+181
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
const debug = require("debug")("action-dashboard:actions");
2+
const _ = require("lodash");
3+
const dayjs = require("dayjs");
4+
const pLimit = require("p-limit");
5+
6+
class Actions {
7+
constructor(gitHub, runStatus, lookbackDays) {
8+
this._gitHub = gitHub;
9+
this._runStatus = runStatus;
10+
// Cache all workflows to speed up refresh
11+
this._runs = [];
12+
this._refreshingRuns = false;
13+
this._lookbackDays = lookbackDays;
14+
}
15+
16+
start() {
17+
debug("Performing initial refreshRuns");
18+
// Load the initial set
19+
this.refreshRuns();
20+
21+
debug("Setting interval to refreshRuns at 15m");
22+
// Refresh by default every fifteeen minutes
23+
setInterval(this.refreshRuns, 1000 * 60 * 15);
24+
}
25+
26+
async getMostRecentRuns(repoOwner, repoName, workflowId) {
27+
try {
28+
const daysAgo = dayjs().subtract(this._lookbackDays, "day");
29+
const runs = await this._gitHub.listWorkflowRuns(
30+
repoOwner,
31+
repoName,
32+
workflowId
33+
);
34+
if (runs.length > 0) {
35+
const groupedRuns = _.groupBy(runs, "head_branch");
36+
const rows = _.reduce(
37+
groupedRuns,
38+
(result, runs, branch) => {
39+
debug(`branch`, branch);
40+
if (daysAgo.isBefore(dayjs(runs[0].created_at))) {
41+
debug(`adding run.id: ${runs[0].id}`);
42+
result.push({
43+
runId: runs[0].id,
44+
repo: runs[0].repository.name,
45+
owner: repoOwner,
46+
workflowId: workflowId,
47+
runNumber: runs[0].run_number,
48+
workflow: runs[0].name,
49+
branch: runs[0].head_branch,
50+
sha: runs[0].head_sha,
51+
message: runs[0].head_commit.message,
52+
committer: runs[0].head_commit.committer.name,
53+
status:
54+
runs[0].status === "completed"
55+
? runs[0].conclusion
56+
: runs[0].status,
57+
createdAt: runs[0].created_at,
58+
updatedAt: runs[0].updated_at,
59+
});
60+
} else {
61+
debug(
62+
`skipping run.id: ${runs[0].id} created_at: ${runs[0].created_at}`
63+
);
64+
}
65+
66+
return result;
67+
},
68+
[]
69+
);
70+
71+
debug(
72+
`getting duration of runs owner: ${repoOwner}, repo: ${repoName}, workflowId: ${workflowId}`
73+
);
74+
75+
// Get durations of runs
76+
const limit = pLimit(10);
77+
const getUsagePromises = rows.map((row) => {
78+
return limit(async () => {
79+
const usage = await this._gitHub.getUsage(
80+
repoOwner,
81+
repoName,
82+
workflowId,
83+
row.runId
84+
);
85+
if (usage?.run_duration_ms) {
86+
row.durationMs = usage.run_duration_ms;
87+
}
88+
89+
return row;
90+
});
91+
});
92+
93+
const rowsWithDuration = await Promise.all(getUsagePromises);
94+
95+
debug(
96+
`most recent runs owner: ${repoOwner}, repo: ${repoName}, workflowId: ${workflowId}`,
97+
rowsWithDuration
98+
);
99+
return rows;
100+
} else {
101+
return [];
102+
}
103+
} catch (e) {
104+
console.error("Error getting runs", e);
105+
return [];
106+
}
107+
}
108+
109+
mergeRuns(runs) {
110+
// Merge into cache
111+
runs.forEach((run) => {
112+
debug(`merging run`, run);
113+
const index = _.findIndex(this._runs, {
114+
workflowId: run.workflowId,
115+
branch: run.branch,
116+
});
117+
if (index >= 0) {
118+
this._runs[index] = run;
119+
} else {
120+
this._runs.push(run);
121+
}
122+
this._runStatus.updatedRun(run);
123+
});
124+
125+
debug("merged runs", this._runs);
126+
}
127+
128+
async refreshRuns() {
129+
// Prevent re-entrant calls
130+
if (this._refreshingRuns) {
131+
return;
132+
}
133+
134+
debug("Starting refreshing runs");
135+
try {
136+
this._refreshingRuns = true;
137+
const repos = await this._gitHub.listRepos();
138+
for (const repo of repos) {
139+
debug(`repo: ${repo.name}`);
140+
const workflows = await this._gitHub.listWorkflowsForRepo(
141+
repo.name,
142+
repo.owner.login
143+
);
144+
if (workflows.length > 0) {
145+
for (const workflow of workflows) {
146+
debug(`workflow: ${workflow.name}`);
147+
const runs = await this.getMostRecentRuns(
148+
repo.owner.login,
149+
repo.name,
150+
workflow.id
151+
);
152+
// Not using apply or spread in case there are a large number of runs returned
153+
this.mergeRuns(runs);
154+
}
155+
}
156+
}
157+
} catch (e) {
158+
console.error("Error getting initial data", e);
159+
} finally {
160+
debug("Finished refreshing runs");
161+
this._refreshingRuns = false;
162+
}
163+
}
164+
165+
async refreshWorkflow(repoOwner, repoName, workflowId) {
166+
const runs = await this.getMostRecentRuns(repoOwner, repoName, workflowId);
167+
this.mergeRuns(runs);
168+
}
169+
170+
getInitialData() {
171+
debug(`getInitialData this._runs.length: ${this._runs.length}`);
172+
if (this._runs.length === 0 && !this._refreshingRuns) {
173+
debug("getInitialData calling refreshRuns");
174+
this.refreshRuns();
175+
}
176+
177+
return this._runs;
178+
}
179+
}
180+
181+
module.exports = Actions;

Diff for: configure.js

+71-23
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,85 @@
1+
const bodyParser = require("body-parser");
2+
const debug = require("debug")("action-dashboard:configure");
3+
const express = require("express");
14
const path = require("path");
5+
const Actions = require("./actions");
6+
const GitHub = require("./github");
7+
const Routes = require("./routes");
8+
const RunStatus = require("./runstatus");
9+
const WebHooks = require("./webhooks");
210

11+
const baseDir = path.basename(process.cwd());
312
// Handle when server is started from vue-cli vs root
4-
if (path.basename(process.cwd()) === "client") {
13+
if (baseDir) {
514
require("dotenv").config({ path: path.resolve(process.cwd(), "../.env") });
6-
} else {
15+
}
16+
// Handle when server is started from
17+
else {
718
require("dotenv").config();
819
}
9-
const debug = require("debug")("action-dashboard:configure");
20+
21+
const {
22+
LOOKBACK_DAYS = 7,
23+
GITHUB_APPID,
24+
GITHUB_APP_CLIENTID,
25+
GITHUB_APP_CLIENTSECRET,
26+
GITHUB_APP_INSTALLATIONID,
27+
GITHUB_APP_WEBHOOK_PORT = 8081,
28+
GITHUB_APP_WEBHOOK_SECRET,
29+
GITHUB_ORG,
30+
GITHUB_USERNAME,
31+
} = process.env;
32+
33+
// Handles newlines \n in private key
34+
const GITHUB_APP_PRIVATEKEY = Buffer.from(
35+
process.env.GITHUB_APP_PRIVATEKEY || "",
36+
"base64"
37+
).toString("utf-8");
38+
39+
// For sharing runStatus across before/after stages
40+
let _runStatus = null;
1041

1142
module.exports = {
1243
before: (app) => {
13-
if (!process.env.DOCKER_BUILD) {
14-
const bodyParser = require("body-parser");
15-
const routes = require("./routes");
16-
17-
debug("configure before");
18-
app.use(bodyParser.json());
19-
app.use("/api", routes);
20-
}
44+
debug("configure before");
45+
46+
const gitHub = new GitHub(
47+
GITHUB_ORG,
48+
GITHUB_USERNAME,
49+
GITHUB_APPID,
50+
GITHUB_APP_PRIVATEKEY,
51+
GITHUB_APP_CLIENTID,
52+
GITHUB_APP_CLIENTSECRET,
53+
GITHUB_APP_INSTALLATIONID
54+
);
55+
_runStatus = new RunStatus();
56+
const actions = new Actions(gitHub, _runStatus, LOOKBACK_DAYS);
57+
const routes = new Routes(
58+
actions,
59+
process.env.GITHUB_ORG || process.env.GITHUB_USERNAME
60+
);
61+
const router = express.Router();
62+
63+
routes.attach(router);
64+
65+
app.use(bodyParser.json());
66+
app.use("/api", router);
67+
68+
const webhooks = new WebHooks(
69+
GITHUB_APP_WEBHOOK_SECRET,
70+
GITHUB_APP_WEBHOOK_PORT,
71+
gitHub,
72+
actions
73+
);
74+
75+
// Start everything
76+
actions.start();
77+
webhooks.start();
2178
},
2279
after: (app, server) => {
23-
if (!process.env.DOCKER_BUILD) {
24-
debug("configure after");
25-
const runStatus = require("./runstatus");
80+
debug("configure after");
2681

27-
// Attach socket.io to server
28-
runStatus.init(server);
29-
}
82+
// Attach socket.io to server
83+
_runStatus.start(server);
3084
},
3185
};
32-
33-
// Loads webhook support if GITHUB_APP_WEBHOOK_SECRET defined
34-
if (!process.env.DOCKER_BUILD) {
35-
debug("loading webhooks");
36-
const webhooks = require("./webhooks");
37-
}

0 commit comments

Comments
 (0)