diff --git a/UserManual.md b/UserManual.md
index 7831223..0011b25 100644
--- a/UserManual.md
+++ b/UserManual.md
@@ -542,6 +542,18 @@ This directory is also a good place to add any extra assets needed for this cust
---
+# Badges
+
+Laminar will serve a job's current status as a pretty badge at the url `/badge/JOBNAME.svg`. This can be used as a link to your server instance from your Github README.md file or cat blog:
+
+```
+
+
+
+```
+
+---
+
# Reference
## Service configuration file
diff --git a/src/interface.h b/src/interface.h
index fcca41a..a8c596f 100644
--- a/src/interface.h
+++ b/src/interface.h
@@ -135,6 +135,11 @@ struct LaminarInterface {
// proper web server which handles this url.
virtual kj::Own getArtefact(std::string path) = 0;
+ // Given the name of a job, populate the provided string reference with
+ // SVG content describing the last known state of the job. Returns false
+ // if the job is unknown.
+ virtual bool handleBadgeRequest(std::string job, std::string& badge) = 0;
+
// Fetches the content of $LAMINAR_HOME/custom/style.css or an empty
// string. Ideally, this would instead be served by a proper web server
// which handles this url.
diff --git a/src/laminar.cpp b/src/laminar.cpp
index 2a42f24..7e29f0b 100644
--- a/src/laminar.cpp
+++ b/src/laminar.cpp
@@ -924,6 +924,48 @@ kj::Own Laminar::getArtefact(std::string path) {
return kj::heap(fs::path(fs::path(homeDir)/"archive"/path).c_str());
}
+bool Laminar::handleBadgeRequest(std::string job, std::string &badge) {
+ RunState rs = RunState::UNKNOWN;
+ db->stmt("SELECT result FROM builds WHERE name = ? ORDER BY number DESC LIMIT 1")
+ .bind(job)
+ .fetch([&](int result){
+ rs = (RunState) result;
+ });
+ if(rs == RunState::UNKNOWN)
+ return false;
+
+ std::string status = to_string(rs);
+ // Empirical approximation of pixel width. Not particularly stable.
+ const int jobNameWidth = job.size() * 7 + 10;
+ const int statusWidth = status.size() * 7 + 10;
+ const char* gradient1 = (rs == RunState::SUCCESS) ? "#2aff4d" : "#ff2a2a";
+ const char* gradient2 = (rs == RunState::SUCCESS) ? "#24b43c" : "#b42424";
+ char* svg = NULL;
+ asprintf(&svg,
+R"x(
+)x", jobNameWidth+statusWidth, gradient1, gradient2, jobNameWidth, jobNameWidth/2+1, job.data(), jobNameWidth, statusWidth, jobNameWidth+statusWidth/2, status.data());
+ badge = svg;
+ return true;
+}
+
std::string Laminar::getCustomCss() {
MappedFileImpl cssFile(fs::path(fs::path(homeDir)/"custom"/"style.css").c_str());
if(cssFile.address()) {
diff --git a/src/laminar.h b/src/laminar.h
index 8c898b2..07042bf 100644
--- a/src/laminar.h
+++ b/src/laminar.h
@@ -56,6 +56,7 @@ public:
void sendStatus(LaminarClient* client) override;
bool setParam(std::string job, uint buildNum, std::string param, std::string value) override;
kj::Own getArtefact(std::string path) override;
+ bool handleBadgeRequest(std::string job, std::string& badge) override;
std::string getCustomCss() override;
void abortAll() override;
void notifyConfigChanged() override;
diff --git a/src/server.cpp b/src/server.cpp
index 4713193..207c17d 100644
--- a/src/server.cpp
+++ b/src/server.cpp
@@ -311,6 +311,7 @@ private:
} else {
// handle regular HTTP request
const char* start, *end, *content_type;
+ std::string badge;
responseHeaders.clear();
if(resource.compare(0, strlen("/archive/"), "/archive/") == 0) {
kj::Own file = laminar.getArtefact(resource.substr(strlen("/archive/")));
@@ -331,6 +332,11 @@ private:
responseHeaders.add("Content-Transfer-Encoding", "binary");
auto stream = response.send(200, "OK", responseHeaders, end-start);
return stream->write(start, end-start).attach(kj::mv(stream));
+ } else if(url.startsWith("/badge/") && url.endsWith(".svg") && laminar.handleBadgeRequest(resource.substr(7,resource.length()-11), badge)) {
+ responseHeaders.set(kj::HttpHeaderId::CONTENT_TYPE, "image/svg+xml");
+ responseHeaders.add("Cache-Control", "no-cache");
+ auto stream = response.send(200, "OK", responseHeaders, badge.size());
+ return stream->write(badge.data(), badge.size()).attach(kj::mv(badge)).attach(kj::mv(stream));
}
return response.sendError(404, "Not Found", responseHeaders);
}
diff --git a/test/test-server.cpp b/test/test-server.cpp
index e708885..fc2a019 100644
--- a/test/test-server.cpp
+++ b/test/test-server.cpp
@@ -65,6 +65,7 @@ public:
MOCK_METHOD1(sendStatus, void(LaminarClient* client));
MOCK_METHOD4(setParam, bool(std::string job, uint buildNum, std::string param, std::string value));
MOCK_METHOD0(getCustomCss, std::string());
+ MOCK_METHOD2(handleBadgeRequest, bool(std::string, std::string&));
MOCK_METHOD0(abortAll, void());
MOCK_METHOD0(notifyConfigChanged, void());
};