diff options
| -rw-r--r-- | .env.sample | 2 | ||||
| -rw-r--r-- | package-lock.json | 78 | ||||
| -rw-r--r-- | package.json | 2 | ||||
| -rw-r--r-- | src/index.ts | 286 | ||||
| -rw-r--r-- | src/metadata.ts | 54 |
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; |
