aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.env.sample2
-rw-r--r--package-lock.json78
-rw-r--r--package.json2
-rw-r--r--src/index.ts286
-rw-r--r--src/metadata.ts54
5 files changed, 318 insertions, 104 deletions
diff --git a/.env.sample b/.env.sample
index 281d0ec..b3fdc29 100644
--- a/.env.sample
+++ b/.env.sample
@@ -2,3 +2,5 @@ PORT=3000
TOKEN=
HOST_DIR=/dist
UPLOAD_LIMIT=4194304
+MONGO_URI=mongodb://localhost:27017
+MONGO_DB=ao-coverage \ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 0479af0..84cbad6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "ao-coverage",
- "version": "1.0.0",
+ "version": "0.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -13,6 +13,14 @@
"@types/node": "12.12.7"
}
},
+ "@types/bson": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.1.tgz",
+ "integrity": "sha512-K6VAEdLVJFBxKp8m5cRTbUfeZpuSvOuLKJLrgw9ANIXo00RiyGzgH4BKWWR4F520gV4tWmxG7q9sKQRVDuzrBw==",
+ "requires": {
+ "@types/node": "12.12.7"
+ }
+ },
"@types/connect": {
"version": "3.4.32",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz",
@@ -62,6 +70,15 @@
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz",
"integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw=="
},
+ "@types/mongodb": {
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.3.12.tgz",
+ "integrity": "sha512-gWIdrA8YKC4OetBk4eT5Zsp4p3oy/BJQKt80tXfgPnfBuLigumcmwNZseVSkLQJ3XkN/1OR0/kIunGWlew3rmQ==",
+ "requires": {
+ "@types/bson": "4.0.1",
+ "@types/node": "12.12.7"
+ }
+ },
"@types/node": {
"version": "12.12.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.7.tgz",
@@ -215,6 +232,11 @@
"resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz",
"integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw=="
},
+ "bson": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.3.tgz",
+ "integrity": "sha512-TdiJxMVnodVS7r0BdL42y/pqC9cL2iKynVwA0Ho3qbsQYr428veL3l7BQyuqiw+Q5SqqoT0m4srSY/BlZ9AxXg=="
+ },
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
@@ -670,6 +692,12 @@
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
+ "memory-pager": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
+ "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
+ "optional": true
+ },
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
@@ -698,6 +726,17 @@
"mime-db": "1.42.0"
}
},
+ "mongodb": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.3.5.tgz",
+ "integrity": "sha512-6NAv5gTFdwRyVfCz+O+KDszvjpyxmZw+VlmqmqKR2GmpkeKrKFRv/ZslgTtZba2dc9JYixIf99T5Gih7TIWv7Q==",
+ "requires": {
+ "bson": "1.1.3",
+ "require_optional": "1.0.1",
+ "safe-buffer": "5.1.2",
+ "saslprep": "1.0.3"
+ }
+ },
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -892,6 +931,20 @@
}
}
},
+ "require_optional": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz",
+ "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==",
+ "requires": {
+ "resolve-from": "2.0.0",
+ "semver": "5.7.1"
+ }
+ },
+ "resolve-from": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
+ "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c="
+ },
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -902,6 +955,15 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
+ "saslprep": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz",
+ "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==",
+ "optional": true,
+ "requires": {
+ "sparse-bitfield": "3.0.3"
+ }
+ },
"saxes": {
"version": "3.1.11",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz",
@@ -910,6 +972,11 @@
"xmlchars": "2.2.0"
}
},
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
+ },
"send": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
@@ -959,6 +1026,15 @@
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"optional": true
},
+ "sparse-bitfield": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
+ "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=",
+ "optional": true,
+ "requires": {
+ "memory-pager": "1.5.0"
+ }
+ },
"sshpk": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
diff --git a/package.json b/package.json
index 9699d44..818b361 100644
--- a/package.json
+++ b/package.json
@@ -16,10 +16,12 @@
"dependencies": {
"@types/express": "^4.17.2",
"@types/jsdom": "^12.2.4",
+ "@types/mongodb": "^3.3.12",
"badgen": "3.0.1",
"dotenv": "8.2.0",
"express": "4.17.1",
"jsdom": "^15.2.1",
+ "mongodb": "^3.3.5",
"typescript": "^3.7.2"
},
"devDependencies": {
diff --git a/src/index.ts b/src/index.ts
index b6f41e5..4955ba6 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -2,16 +2,24 @@ import dotenv from "dotenv";
import express from "express";
import { JSDOM } from "jsdom";
import { badgen } from "badgen";
+import { MongoClient } from "mongodb";
import path from "path";
import fs from "fs";
import formats, { Format } from "./formats";
+import Metadata from "./metadata";
// Start-up configuration
dotenv.config();
const PORT = Number(process.env.PORT || 3000);
const TOKEN = process.env.TOKEN || "";
const UPLOAD_LIMIT = Number(process.env.UPLOAD_LIMIT || 4194304);
+const MONGO_URI =
+ process.env.MONGO_URI ||
+ (() => {
+ throw Error("MONGO_URI must be defined");
+ })();
+const MONGO_DB = process.env.MONGO_DB || "ao-coverage";
const HOST_DIR =
process.env.HOST_DIR ||
(() => {
@@ -23,111 +31,183 @@ if (!path.isAbsolute(HOST_DIR)) {
throw Error("HOST_DIR must be an absolute path");
}
-const app: express.Application = express();
-
-// Upload HTML file
-app.post("/v1/:org/:repo/:branch/:commit.html", (req, res) => {
- const { org, repo, branch, commit } = req.params;
- console.info(
- "POST request to /v1/%s/%s/%s/%s.html",
- org,
- repo,
- branch,
- commit
- );
-
- const { token, format } = req.query;
- //TODO @Metadata token should come from metadata
- if (token != TOKEN) {
- return res.status(401).send("Invalid token");
- }
+new MongoClient(MONGO_URI, { useUnifiedTopology: true }).connect(
+ (err, mongo) => {
+ if (err !== null) {
+ throw err;
+ }
- if (!formats.list_formats().includes(format)) {
- return res.status(406).send("Report format unknown");
- }
+ const app: express.Application = express();
+ const metadata = new Metadata(mongo.db(MONGO_DB));
+
+ // Upload HTML file
+ app.post("/v1/:org/:repo/:branch/:commit.html", (req, res) => {
+ const { org, repo, branch, commit } = req.params;
+ console.info(
+ "POST request to /v1/%s/%s/%s/%s.html",
+ org,
+ repo,
+ branch,
+ commit
+ );
+
+ const { token, format } = req.query;
+ //TODO @Metadata token should come from metadata
+ if (token != TOKEN) {
+ return res.status(401).send("Invalid token");
+ }
+
+ if (!formats.list_formats().includes(format)) {
+ return res.status(406).send("Report format unknown");
+ }
+
+ var contents = "";
+ req.on("data", raw => {
+ if (contents.length + raw.length > UPLOAD_LIMIT) {
+ res.status(413).send("Uploaded file is too large");
+ } else {
+ contents += raw;
+ }
+ });
+ req.on("end", () => {
+ let formatter: Format, coverage: number;
+ try {
+ const doc = new JSDOM(contents).window.document;
+ formatter = formats.get_format(format);
+ coverage = formatter.parse_coverage(doc);
+ } catch {
+ return res.status(400).send("Invalid report document");
+ }
+
+ const badge = badgen({
+ label: "coverage",
+ status: Math.floor(coverage).toString() + "%",
+ //TODO @Metadata stage values should come from metadata
+ color: formatter.match_color(coverage, 95, 80)
+ });
+
+ const report_path = path.join(HOST_DIR, org, repo, branch, commit);
+
+ fs.promises
+ .mkdir(report_path, { recursive: true })
+ .then(() =>
+ fs.promises.writeFile(path.join(report_path, "badge.svg"), badge)
+ )
+ .then(() =>
+ fs.promises.writeFile(
+ path.join(report_path, "index.html"),
+ contents
+ )
+ )
+ .then(() =>
+ metadata.updateBranch({ org, repo, name: branch, head: commit })
+ )
+ .then(result =>
+ result
+ ? res.status(200).send()
+ : res.status(500).send("Unknown error occurred")
+ );
+ });
+ });
- var contents = "";
- req.on("data", raw => {
- if (contents.length + raw.length > UPLOAD_LIMIT) {
- res.status(413).send("Uploaded file is too large");
- } else {
- contents += raw;
- }
- });
- req.on("end", () => {
- let formatter: Format, coverage: number;
- try {
- const doc = new JSDOM(contents).window.document;
- formatter = formats.get_format(format);
- coverage = formatter.parse_coverage(doc);
- } catch {
- return res.status(400).send("Invalid report document");
- }
+ app.get("/v1/:org/:repo/:branch.svg", (req, res) => {
+ const { org, repo, branch } = req.params;
+ console.info("GET request to /v1/%s/%s/%s.svg", org, repo, branch);
+
+ metadata.getHeadCommit(org, repo, branch).then(
+ commit => {
+ console.debug(
+ "Found commit %s for ORB %s/%s/%s",
+ commit,
+ org,
+ repo,
+ branch
+ );
+
+ res.sendFile(
+ path.join(HOST_DIR, org, repo, branch, commit, "badge.svg")
+ );
+ },
+ () => {
+ res.status(500).send("Unknown error occurred");
+ }
+ );
+ });
+
+ app.get("/v1/:org/:repo/:branch.html", (req, res) => {
+ const { org, repo, branch } = req.params;
+ console.info("GET request to /v1/%s/%s/%s.html", org, repo, branch);
+
+ metadata.getHeadCommit(org, repo, branch).then(
+ commit => {
+ console.debug(
+ "Found commit %s for ORB %s/%s/%s",
+ commit,
+ org,
+ repo,
+ branch
+ );
+
+ res.sendFile(
+ path.join(HOST_DIR, org, repo, branch, commit, "index.html")
+ );
+ },
+ () => {
+ res.status(500).send("Unknown error occurred");
+ }
+ );
+ });
+
+ // provide hard link for commit
+ app.get("/v1/:org/:repo/:branch/:commit.svg", (req, res) => {
+ const { org, repo, branch, commit } = req.params;
+ console.info(
+ "GET request to /v1/%s/%s/%s/%s.svg",
+ org,
+ repo,
+ branch,
+ commit
+ );
+
+ res.sendFile(path.join(HOST_DIR, org, repo, branch, commit, "badge.svg"));
+ });
+
+ // provide hard link for commit
+ app.get("/v1/:org/:repo/:branch/:commit.html", (req, res) => {
+ const { org, repo, branch, commit } = req.params;
+ console.info(
+ "GET request to /v1/%s/%s/%s/%s.html",
+ org,
+ repo,
+ branch,
+ commit
+ );
+
+ res.sendFile(
+ path.join(HOST_DIR, org, repo, branch, commit, "index.html")
+ );
+ });
- const badge = badgen({
- label: "coverage",
- status: Math.floor(coverage).toString() + "%",
- //TODO @Metadata stage values should come from metadata
- color: formatter.match_color(coverage, 95, 80)
+ const server = app.listen(PORT, () => {
+ console.log("Express started on port " + PORT);
});
- const report_path = path.join(HOST_DIR, org, repo, branch, commit);
-
- fs.promises
- .mkdir(report_path, { recursive: true })
- .then(() =>
- fs.promises.writeFile(path.join(report_path, "badge.svg"), badge)
- )
- .then(() =>
- fs.promises.writeFile(path.join(report_path, "index.html"), contents)
- )
- //TODO @Metadata set branch alias for badge / report file
- .then(() => res.status(200).send());
- });
-});
-
-app.get("/v1/:org/:repo/:branch.svg", (req, res) => {
- const { org, repo, branch } = req.params;
- console.info("GET request to /v1/%s/%s/%s.svg", org, repo, branch);
-
- //TODO @Metadata get the commit @@ via metadata
- const commit = "";
-
- return res.status(501).send();
-});
-
-app.get("/v1/:org/:repo/:branch.html", (req, res) => {
- const { org, repo, branch } = req.params;
- console.info("GET request to /v1/%s/%s/%s.html", org, repo, branch);
-
- //TODO @Metadata get the commit @@ via metadata
- const commit = "";
-
- return res.status(501).send();
-});
-
-// provide hard link for commit
-app.get("/v1/:org/:repo/:branch/:commit.svg", (req, res) => {
- const { org, repo, branch, commit } = req.params;
- console.info("GET request to /v1/%s/%s/%s/%s.svg", org, repo, branch, commit);
-
- res.sendFile(path.join(HOST_DIR, org, repo, branch, commit, "badge.svg"));
-});
-
-// provide hard link for commit
-app.get("/v1/:org/:repo/:branch/:commit.html", (req, res) => {
- const { org, repo, branch, commit } = req.params;
- console.info(
- "GET request to /v1/%s/%s/%s/%s.html",
- org,
- repo,
- branch,
- commit
- );
-
- res.sendFile(path.join(HOST_DIR, org, repo, branch, commit, "index.html"));
-});
-
-app.listen(PORT, () => {
- console.log("Express started on port " + PORT);
-});
+ // application exit handling
+ const handle_closure = (signal: NodeJS.Signals) => {
+ console.log("%s signal received. Closing shop.", signal);
+
+ mongo.close().then(() => {
+ console.log("Mongo client connection closed.");
+ server.close(() => {
+ console.log("Express down.");
+ process.exit();
+ });
+ });
+ };
+
+ const handle_codes: NodeJS.Signals[] = ["SIGTERM", "SIGHUP", "SIGINT"];
+ const process_event = (code: NodeJS.Signals) =>
+ process.on(code, handle_closure);
+ handle_codes.map(process_event);
+ }
+);
diff --git a/src/metadata.ts b/src/metadata.ts
new file mode 100644
index 0000000..1eb7f1a
--- /dev/null
+++ b/src/metadata.ts
@@ -0,0 +1,54 @@
+import { Db } from "mongodb";
+
+/** //FIXME fix document schema
+ * Rather than using branches as the core, this should be adopted into the following document model:
+ * repo:
+ * - org
+ * - name
+ * - token
+ * - branches: {
+ * [branchname]: {
+ * head
+ * }
+ * }
+ */
+export interface Branch {
+ org: string;
+ repo: string;
+ name: string;
+ head: string;
+}
+
+class Metadata {
+ database: Db;
+
+ constructor(client: Db) {
+ this.database = client;
+ }
+
+ async getHeadCommit(
+ org: string,
+ repo: string,
+ branch: string
+ ): Promise<string> {
+ const result = await this.database
+ .collection<Branch>("branch")
+ .findOne({ org, repo, name: branch });
+
+ if (result !== null) {
+ return result.head;
+ } else {
+ throw Error("Branch not found");
+ }
+ }
+
+ async updateBranch(branch: Branch): Promise<boolean> {
+ const { head, ...matcher } = branch;
+ const { result } = await this.database
+ .collection<Branch>("branch")
+ .replaceOne(matcher, branch, { upsert: true });
+ return result.ok === 1;
+ }
+}
+
+export default Metadata;