pull/208/merge
Maximilian Seesslen 2 months ago committed by GitHub
commit afa0f3ada3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -60,6 +60,7 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wno-unused-parameter -Wno-sign-compare")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-error=deprecated-declarations" )
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Werror -DDEBUG")
# Allow passing in the version string, for e.g. patched/packaged versions
@ -142,6 +143,7 @@ set(LAMINARD_CORE_SOURCES
src/run.cpp
src/server.cpp
src/version.cpp
src/json.cpp
laminar.capnp.c++
index_html_size.h
)
@ -206,4 +208,4 @@ if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
install(FILES etc/laminar.conf DESTINATION ${CMAKE_INSTALL_PREFIX}/etc)
configure_file(etc/laminar.service.in laminar.service @ONLY)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/laminar.service DESTINATION ${CMAKE_INSTALL_PREFIX}${SYSTEMD_UNITDIR})
endif()
endif()

@ -494,6 +494,22 @@ TIMEOUT=120
---
# Adding metadata to runs
With the `laminarc tag` command the build scripts can augment runs with metadata.
This can be SCM-oriented information like the used branch or release-version.
The metadata is a list of key/value pairs.
The metadata is automatically shown in the run-view of a job in the web UI.
Each call of `laminarc tag` can assign one key/value pair.
Example:
```
laminarc tag $JOBNAME $NUMBER Branch "$(git -C $WORKSPACE branch --show-current)"
```
---
# Contexts
In Laminar, each run of a job is associated with a context. The context defines an integer number of *executors*, which is the amount of runs which the context will accept simultaneously. A context may also provide additional environment variables.

@ -8,7 +8,7 @@ VERSION=$(cd "$SOURCE_DIR" && git describe --tags --abbrev=8 --dirty)-1~upstream
DOCKER_TAG=$(docker build -q - <<EOS
FROM debian:11-slim
RUN apt-get update && apt-get install -y wget cmake g++ capnproto libcapnp-dev rapidjson-dev libsqlite3-dev libboost-dev zlib1g-dev
RUN apt-get update && apt-get install -y wget cmake g++ capnproto libcapnp-dev rapidjson-dev libsqlite3-dev libboost-dev zlib1g-dev pkg-config
EOS
)
@ -34,6 +34,8 @@ Depends: libcapnp-0.7.0, libsqlite3-0, zlib1g
Description: Lightweight Continuous Integration Service
EOF
echo /etc/laminar.conf > laminar/DEBIAN/conffiles
mkdir -p laminar/etc
mv laminar/usr/etc/laminar.conf laminar/etc/laminar.conf
cat <<EOF > laminar/DEBIAN/postinst
#!/bin/bash
echo Creating laminar user with home in /var/lib/laminar

@ -90,17 +90,19 @@ static void usage(std::ostream& out) {
out << "Usage: laminarc [-h|--help] COMMAND\n";
out << " -h|--help show this help message\n";
out << "where COMMAND is:\n";
out << " queue JOB_LIST... queues one or more jobs for execution and returns immediately.\n";
out << " start JOB_LIST... queues one or more jobs for execution and blocks until it starts.\n";
out << " run JOB_LIST... queues one or more jobs for execution and blocks until it finishes.\n";
out << " JOB_LIST may be prepended with --next, in this case the job will\n";
out << " be pushed to the front of the queue instead of the end.\n";
out << " set PARAMETER_LIST... sets the given parameters as environment variables in the currently\n";
out << " running job. Fails if run outside of a job context.\n";
out << " abort NAME NUMBER aborts the run identified by NAME and NUMBER.\n";
out << " show-jobs lists all known jobs.\n";
out << " show-queued lists currently queued jobs.\n";
out << " show-running lists currently running jobs.\n";
out << " queue JOB_LIST... queues one or more jobs for execution and returns immediately.\n";
out << " start JOB_LIST... queues one or more jobs for execution and blocks until it starts.\n";
out << " run JOB_LIST... queues one or more jobs for execution and blocks until it finishes.\n";
out << " JOB_LIST may be prepended with --next, in this case the job will\n";
out << " be pushed to the front of the queue instead of the end.\n";
out << " set PARAMETER_LIST... sets the given parameters as environment variables in the currently\n";
out << " running job. Fails if run outside of a job context.\n";
out << " abort NAME NUMBER aborts the run identified by NAME and NUMBER.\n";
out << " tag NAME NUMBER KEY VALUE sets an key/value pair in the metadata for an run by NAME and NUMBER.\n";
out << " This is intended to be used by the build scripts, not the user.\n";
out << " show-jobs lists all known jobs.\n";
out << " show-queued lists currently queued jobs.\n";
out << " show-running lists currently running jobs.\n";
out << "JOB_LIST is of the form:\n";
out << " [JOB_NAME [PARAMETER_LIST...]]...\n";
out << "PARAMETER_LIST is of the form:\n";
@ -244,6 +246,20 @@ int main(int argc, char** argv) {
for(auto it : running.getResult()) {
printf("%s:%d\n", it.getJob().cStr(), it.getBuildNum());
}
} else if(strcmp(argv[1], "tag") == 0) {
if(argc != 6) {
fprintf(stderr, "Usage %s tag <jobName> <jobNumber> <metaKey> <metaValue>\n", argv[0]);
return EXIT_BAD_ARGUMENT;
}
auto req = laminar.tagRequest();
req.getRun().setJob(argv[2]);
req.getRun().setBuildNum(atoi(argv[3]));
req.setMetaKey( argv[4] );
req.setMetaValue( argv[5] );
ts.add(req.send().then([&ret](capnp::Response<LaminarCi::TagResults> resp){
if(resp.getResult() != LaminarCi::MethodResult::SUCCESS)
ret = EXIT_OPERATION_FAILED;
}));
} else {
fprintf(stderr, "Unknown command %s\n", argv[1]);
return EXIT_BAD_ARGUMENT;

@ -0,0 +1,39 @@
///
/// Copyright 2015-2022 Oliver Giles
///
/// This file is part of Laminar
///
/// Laminar is free software: you can redistribute it and/or modify
/// it under the terms of the GNU General Public License as published by
/// the Free Software Foundation, either version 3 of the License, or
/// (at your option) any later version.
///
/// Laminar is distributed in the hope that it will be useful,
/// but WITHOUT ANY WARRANTY; without even the implied warranty of
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/// GNU General Public License for more details.
///
/// You should have received a copy of the GNU General Public License
/// along with Laminar. If not, see <http://www.gnu.org/licenses/>
///
#include "json.h"
template<> Json& Json::set(const char* key, double value) { String(key); Double(value); return *this; }
template<> Json& Json::set(const char* key, const char* value) { String(key); String(value); return *this; }
template<> Json& Json::set(const char* key, std::string value) { String(key); String(value.c_str()); return *this; }
Json& Json::setJsonObject(const char* key, const std::string& object)
{
String(key);
if( object.length() )
{
RawValue(object.c_str(), object.length(), rapidjson::kObjectType);
}
else
{
StartObject();
EndObject();
}
return *this;
}

@ -0,0 +1,47 @@
///
/// Copyright 2015-2022 Oliver Giles
///
/// This file is part of Laminar
///
/// Laminar is free software: you can redistribute it and/or modify
/// it under the terms of the GNU General Public License as published by
/// the Free Software Foundation, either version 3 of the License, or
/// (at your option) any later version.
///
/// Laminar is distributed in the hope that it will be useful,
/// but WITHOUT ANY WARRANTY; without even the implied warranty of
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/// GNU General Public License for more details.
///
/// You should have received a copy of the GNU General Public License
/// along with Laminar. If not, see <http://www.gnu.org/licenses/>
///
#ifndef LAMINAR_JSON_H_
#define LAMINAR_JSON_H_
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
#include <string>
// rapidjson::Writer with a StringBuffer is used a lot in Laminar for
// preparing JSON messages to send to HTTP clients. A small wrapper
// class here reduces verbosity later for this common use case.
class Json : public rapidjson::Writer<rapidjson::StringBuffer> {
public:
Json() : rapidjson::Writer<rapidjson::StringBuffer>(buf) { StartObject(); }
template<typename T>
Json& set(const char* key, T value) { String(key); Int64(value); return *this; }
Json& startObject(const char* key) { String(key); StartObject(); return *this; }
Json& startArray(const char* key) { String(key); StartArray(); return *this; }
Json& setJsonObject(const char* key, const std::string& object);
const char* str() { EndObject(); return buf.GetString(); }
private:
rapidjson::StringBuffer buf;
};
template<> Json& Json::set(const char* key, double value);
template<> Json& Json::set(const char* key, const char* value);
template<> Json& Json::set(const char* key, std::string value);
#endif // ? ! LAMINAR_LAMINAR_H_

@ -9,6 +9,7 @@ interface LaminarCi {
listRunning @4 () -> (result :List(Run));
listKnown @5 () -> (result :List(Text));
abort @6 (run :Run) -> (result :MethodResult);
tag @7 (run :Run, metaKey :Text, metaValue: Text) -> (result :MethodResult);
struct Run {
job @0 :Text;

@ -22,6 +22,7 @@
#include "log.h"
#include "http.h"
#include "rpc.h"
#include "json.h"
#include <sys/wait.h>
#include <sys/mman.h>
@ -34,32 +35,11 @@
#define COMPRESS_LOG_MIN_SIZE 1024
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
// FNM_EXTMATCH isn't supported under musl
#if !defined(FNM_EXTMATCH)
#define FNM_EXTMATCH 0
#endif
// rapidjson::Writer with a StringBuffer is used a lot in Laminar for
// preparing JSON messages to send to HTTP clients. A small wrapper
// class here reduces verbosity later for this common use case.
class Json : public rapidjson::Writer<rapidjson::StringBuffer> {
public:
Json() : rapidjson::Writer<rapidjson::StringBuffer>(buf) { StartObject(); }
template<typename T>
Json& set(const char* key, T value) { String(key); Int64(value); return *this; }
Json& startObject(const char* key) { String(key); StartObject(); return *this; }
Json& startArray(const char* key) { String(key); StartArray(); return *this; }
const char* str() { EndObject(); return buf.GetString(); }
private:
rapidjson::StringBuffer buf;
};
template<> Json& Json::set(const char* key, double value) { String(key); Double(value); return *this; }
template<> Json& Json::set(const char* key, const char* value) { String(key); String(value); return *this; }
template<> Json& Json::set(const char* key, std::string value) { String(key); String(value.c_str()); return *this; }
// short syntax helpers for kj::Path
template<typename T>
inline kj::Path operator/(const kj::Path& p, const T& ext) {
@ -100,7 +80,7 @@ Laminar::Laminar(Server &server, Settings settings) :
"name TEXT, number INT UNSIGNED, node TEXT, queuedAt INT, "
"startedAt INT, completedAt INT, result INT, output TEXT, "
"outputLen INT, parentJob TEXT, parentBuild INT, reason TEXT, "
"PRIMARY KEY (name, number DESC))";
"metadata TEXT, PRIMARY KEY (name, number DESC))";
db->exec(create_table_stmt);
// Migrate from (name, number) primary key to (name, number DESC).
@ -123,6 +103,18 @@ Laminar::Laminar(Server &server, Settings settings) :
db->exec("CREATE INDEX IF NOT EXISTS idx_completion_time ON builds("
"completedAt DESC)");
// Update 'metadata' if not existing;
db->stmt(
"SELECT COUNT(*) AS CNT FROM pragma_table_info('builds')"
" WHERE name='metadata'")
.fetch<int>([&](int has_metadata) {
if( !has_metadata )
{
LLOG(WARNING, "Updating database to contain 'metadata'");
db->exec( "ALTER TABLE 'builds' ADD COLUMN 'metadata' TEXT");
}
});
// retrieve the last build numbers
db->stmt("SELECT name, MAX(number) FROM builds GROUP BY name")
.fetch<str,uint>([this](str name, uint build){
@ -250,17 +242,21 @@ std::string Laminar::getStatus(MonitorScope scope) {
j.set("time", time(nullptr));
j.startObject("data");
if(scope.type == MonitorScope::RUN) {
db->stmt("SELECT queuedAt,startedAt,completedAt,result,reason,parentJob,parentBuild,q.lr IS NOT NULL,q.lr FROM builds "
db->stmt("SELECT queuedAt,startedAt,completedAt,result,reason,metadata,parentJob,parentBuild,q.lr IS NOT NULL,q.lr FROM builds "
"LEFT JOIN (SELECT name n, MAX(number), completedAt-startedAt lr FROM builds WHERE result IS NOT NULL GROUP BY n) q ON q.n = name "
"WHERE name = ? AND number = ?")
.bind(scope.job, scope.num)
.fetch<time_t, time_t, time_t, int, std::string, std::string, uint, uint, uint>([&](time_t queued, time_t started, time_t completed, int result, std::string reason, std::string parentJob, uint parentBuild, uint lastRuntimeKnown, uint lastRuntime) {
.fetch<time_t, time_t, time_t, int, std::string, std::string, std::string, uint, uint, uint>(
[&](time_t queued, time_t started, time_t completed, int result,
std::string reason, std::string metadata,std::string parentJob,
uint parentBuild, uint lastRuntimeKnown, uint lastRuntime) {
j.set("queued", queued);
j.set("started", started);
if(completed)
j.set("completed", completed);
j.set("result", to_string(completed ? RunState(result) : started ? RunState::RUNNING : RunState::QUEUED));
j.set("reason", reason);
j.setJsonObject("metadata", metadata);
j.startObject("upstream").set("name", parentJob).set("num", parentBuild).EndObject(2);
if(lastRuntimeKnown)
j.set("etc", started + lastRuntime);
@ -287,18 +283,19 @@ std::string Laminar::getStatus(MonitorScope scope) {
order_by = "(completedAt-startedAt) " + direction + ", number DESC";
else
order_by = "number DESC";
std::string stmt = "SELECT number,startedAt,completedAt,result,reason FROM builds "
std::string stmt = "SELECT number,startedAt,completedAt,result,reason,metadata FROM builds "
"WHERE name = ? AND result IS NOT NULL ORDER BY "
+ order_by + " LIMIT ?,?";
db->stmt(stmt.c_str())
.bind(scope.job, scope.page * runsPerPage, runsPerPage)
.fetch<uint,time_t,time_t,int,str>([&](uint build,time_t started,time_t completed,int result,str reason){
.fetch<uint,time_t,time_t,int,str,str>([&](uint build,time_t started,time_t completed,int result,str reason,str metadata){
j.StartObject();
j.set("number", build)
.set("completed", completed)
.set("started", started)
.set("result", to_string(RunState(result)))
.set("reason", reason)
.setJsonObject("metadata", metadata)
.EndObject();
});
j.EndArray();
@ -323,6 +320,7 @@ std::string Laminar::getStatus(MonitorScope scope) {
j.set("started", run->startedAt);
j.set("result", to_string(RunState::RUNNING));
j.set("reason", run->reason());
j.setJsonObject("metadata", run->getMetaDataJsonString());
j.EndObject();
}
j.EndArray();
@ -333,6 +331,7 @@ std::string Laminar::getStatus(MonitorScope scope) {
j.set("number", run->build);
j.set("result", to_string(RunState::QUEUED));
j.set("reason", run->reason());
j.setJsonObject("metadata", run->getMetaDataJsonString());
j.EndObject();
}
}
@ -358,9 +357,10 @@ std::string Laminar::getStatus(MonitorScope scope) {
j.set("description", desc == jobDescriptions.end() ? "" : desc->second);
} else if(scope.type == MonitorScope::ALL) {
j.startArray("jobs");
db->stmt("SELECT name, number, startedAt, completedAt, result, reason "
db->stmt("SELECT name, number, startedAt, completedAt, result, reason, metadata "
"FROM builds GROUP BY name HAVING number = MAX(number)")
.fetch<str,uint,time_t,time_t,int,str>([&](str name,uint number, time_t started, time_t completed, int result, str reason){
.fetch<str,uint,time_t,time_t,int,str,str>([&](str name,uint number,
time_t started, time_t completed, int result, str reason, str metadata){
j.StartObject();
j.set("name", name);
j.set("number", number);
@ -368,6 +368,7 @@ std::string Laminar::getStatus(MonitorScope scope) {
j.set("started", started);
j.set("completed", completed);
j.set("reason", reason);
j.setJsonObject("metadata", metadata);
j.EndObject();
});
j.EndArray();
@ -387,8 +388,9 @@ std::string Laminar::getStatus(MonitorScope scope) {
j.EndObject();
} else { // Home page
j.startArray("recent");
db->stmt("SELECT name,number,node,queuedAt,startedAt,completedAt,result,reason FROM builds WHERE completedAt IS NOT NULL ORDER BY completedAt DESC LIMIT 20")
.fetch<str,uint,str,time_t,time_t,time_t,int,str>([&](str name,uint build,str context,time_t queued,time_t started,time_t completed,int result,str reason){
db->stmt("SELECT name,number,node,queuedAt,startedAt,completedAt,result,reason,metadata"
" FROM builds WHERE completedAt IS NOT NULL ORDER BY completedAt DESC LIMIT 20")
.fetch<str,uint,str,time_t,time_t,time_t,int,str,str>([&](str name,uint build,str context,time_t queued,time_t started,time_t completed,int result,str reason,str metadata){
j.StartObject();
j.set("name", name)
.set("number", build)
@ -398,6 +400,7 @@ std::string Laminar::getStatus(MonitorScope scope) {
.set("completed", completed)
.set("result", to_string(RunState(result)))
.set("reason", reason)
.setJsonObject("metadata", metadata)
.EndObject();
});
j.EndArray();
@ -620,8 +623,9 @@ std::shared_ptr<Run> Laminar::queueJob(std::string name, ParamMap params, bool f
else
queuedJobs.push_back(run);
db->stmt("INSERT INTO builds(name,number,queuedAt,parentJob,parentBuild,reason) VALUES(?,?,?,?,?,?)")
.bind(run->name, run->build, run->queuedAt, run->parentName, run->parentBuild, run->reason())
db->stmt("INSERT INTO builds(name,number,queuedAt,parentJob,parentBuild,reason,metadata) VALUES(?,?,?,?,?,?,?)")
.bind(run->name, run->build, run->queuedAt, run->parentName, run->parentBuild,
run->reason(), run->getMetaDataJsonString())
.exec();
// notify clients
@ -633,6 +637,7 @@ std::shared_ptr<Run> Laminar::queueJob(std::string name, ParamMap params, bool f
.set("result", to_string(RunState::QUEUED))
.set("queueIndex", frontOfQueue ? 0 : (queuedJobs.size() - 1))
.set("reason", run->reason())
.setJsonObject("metadata", run->getMetaDataJsonString())
.EndObject();
http->notifyEvent(j.str(), name.c_str());
@ -652,6 +657,18 @@ void Laminar::abortAll() {
}
}
bool Laminar::tag(std::string job, uint buildNum, std::string key, std::string value)
{
if(Run* run = activeRun(job, buildNum))
return run->tag(key, value);
else
{
LLOG(WARNING, "No active run with ", job, buildNum);
}
return true;
}
bool Laminar::canQueue(const Context& ctx, const Run& run) const {
if(ctx.busyExecutors >= ctx.numExecutors)
return false;
@ -722,7 +739,8 @@ bool Laminar::tryStartRun(std::shared_ptr<Run> run, int queueIndex) {
.set("queued", run->queuedAt)
.set("started", run->startedAt)
.set("number", run->build)
.set("reason", run->reason());
.set("reason", run->reason())
.setJsonObject("metadata", run->getMetaDataJsonString());
db->stmt("SELECT completedAt - startedAt FROM builds WHERE name = ? ORDER BY completedAt DESC LIMIT 1")
.bind(run->name)
.fetch<uint>([&](uint etc){
@ -768,8 +786,8 @@ void Laminar::handleRunFinished(Run * r) {
}
}
db->stmt("UPDATE builds SET completedAt = ?, result = ?, output = ?, outputLen = ? WHERE name = ? AND number = ?")
.bind(completedAt, int(r->result), maybeZipped, logsize, r->name, r->build)
db->stmt("UPDATE builds SET completedAt = ?, result = ?, output = ?, outputLen = ?, metadata = ? WHERE name = ? AND number = ?")
.bind(completedAt, int(r->result), maybeZipped, logsize, r->getMetaDataJsonString(), r->name, r->build)
.exec();
// notify clients
@ -782,7 +800,8 @@ void Laminar::handleRunFinished(Run * r) {
.set("completed", completedAt)
.set("started", r->startedAt)
.set("result", to_string(r->result))
.set("reason", r->reason());
.set("reason", r->reason())
.setJsonObject("metadata", r->getMetaDataJsonString());
j.startArray("artifacts");
populateArtifacts(j, r->name, r->build);
j.EndArray();

@ -96,6 +96,9 @@ public:
// Abort all running jobs
void abortAll();
// Store metadata for an run
bool tag(std::string job, uint buildNum, std::string key, std::string value);
private:
bool loadConfiguration();
void loadCustomizations();

@ -131,6 +131,9 @@
<canvas id="chartBt"></canvas>
</div>
<div style="grid-column: 1/-1">
<div v-show="false">{{metaDataKeys=["XBranch", "XVersion"]}}</div>
<div v-show="false">{{allJobs=jobsQueued.concat(jobsRunning).concat(jobsRecent)}}</div>
<div v-show="false">{{metaDataKeys=uniteMetadataKeys(allJobs)}}</div>
<table class="striped">
<thead><tr>
<th><a class="sort" :class="(sort.field=='result'?sort.order:'')" v-on:click="do_sort('result')">&nbsp;</a></th>
@ -138,13 +141,15 @@
<th class="text-center">Started <a class="sort" :class="(sort.field=='started'?sort.order:'')" v-on:click="do_sort('started')">&nbsp;</a></th>
<th class="text-center">Duration <a class="sort" :class="(sort.field=='duration'?sort.order:'')" v-on:click="do_sort('duration')">&nbsp;</a></th>
<th class="text-center vp-sm-hide">Reason <a class="sort" :class="(sort.field=='reason'?sort.order:'')" v-on:click="do_sort('reason')">&nbsp;</a></th>
<th class="text-center vp-sm-hide" v-for="(value, key) in metaDataKeys">{{key}}<a class="sort" :class="(sort.field=='reason'?sort.order:'')" v-on:click="do_sort('reason')">&nbsp;</a></th>
</tr></thead>
<tr v-for="job in jobsQueued.concat(jobsRunning).concat(jobsRecent)" track-by="$index">
<tr v-for="job in allJobs" track-by="$index">
<td style="width:1px"><span v-html="runIcon(job.result)"></span></td>
<td><router-link :to="'jobs/'+route.params.name+'/'+job.number">#{{job.number}}</router-link></td>
<td class="text-center"><span v-if="job.result!='queued'">{{formatDate(job.started)}}</span></td>
<td class="text-center"><span v-if="job.result!='queued'">{{formatDuration(job.started, job.completed)}}</span></td>
<td class="text-center vp-sm-hide">{{job.reason}}</td>
<td class="text-center vp-sm-hide" v-for="(value, key) in metaDataKeys">{{job.metadata[key]}}</td>
</tr>
</table>
<div style="float: right; margin: 15px; display: inline-grid; grid-auto-flow: column; gap: 10px; align-items: center">
@ -176,13 +181,17 @@
<dt v-show="runComplete(job)">Completed</dt><dd v-show="job.completed">{{formatDate(job.completed)}}</dd>
<dt v-show="job.started">Duration</dt><dd v-show="job.started">{{formatDuration(job.started, job.completed)}}</dd>
</dl>
<dl v-show="job.artifacts.length">
<dt>Artifacts</dt>
<dd>
<dl>
<dt v-show="job.artifacts.length">Artifacts</dt>
<dd v-show="job.artifacts.length">
<ul style="margin-bottom: 0">
<li v-for="art in job.artifacts"><a :href="art.url" target="_self">{{art.filename}}</a> [{{ art.size | iecFileSize }}]</li>
</ul>
</dd>
<template v-show="Object.keys(job.metadata).length" v-for="(value, key) in job.metadata">
<dt>{{key}}</dt>
<dd>{{value}}</dd>
</template>
</dl>
</div>
</div>

@ -105,6 +105,22 @@ Vue.mixin({
return Math.floor((end-start)/60) + ' minutes, ' + ((end-start)%60) + ' seconds';
else
return (end-start) + ' seconds';
},
// collect all keys in available in the metadata of a list of given jobs
uniteMetadataKeys: function(jobs) {
let keyList = {};
for(let i = 0; i < jobs.length; i++) {
let metadata = jobs[i].metadata;
for(var key in metadata)
{
if(! keyList.hasOwnProperty(key) )
{
keyList[key]=0
}
keyList[key]+=1
}
}
return( keyList );
}
}
});
@ -579,7 +595,7 @@ const All = templateId => {
};
};
// Component for the /job/:name endpoint
// Component for the /jobs/:name endpoint
const Job = templateId => {
const state = {
description: '',

@ -318,6 +318,7 @@ button:not([disabled]) { cursor: pointer; color: var(--main-fg); }
#page-run-detail {
display: grid;
grid-template-columns: minmax(400px, auto) 1fr;
align-items: start;
gap: 5px;
}
@media (max-width: 780px) {

@ -143,6 +143,19 @@ public:
return kj::READY_NOW;
}
kj::Promise<void> tag(TagContext context) override {
std::string jobName = context.getParams().getRun().getJob();
uint buildNum = context.getParams().getRun().getBuildNum();
std::string metaKey = context.getParams().getMetaKey();
std::string metaValue = context.getParams().getMetaValue();
LLOG(INFO, "RPC tag", jobName, buildNum, metaKey, metaValue);
LaminarCi::MethodResult result = laminar.tag(jobName, buildNum, metaKey, metaValue)
? LaminarCi::MethodResult::SUCCESS
: LaminarCi::MethodResult::FAILED;
context.getResults().setResult(result);
return kj::READY_NOW;
}
private:
// Helper to convert an RPC parameter list to a hash map
ParamMap params(const capnp::List<LaminarCi::JobParam>::Reader& paramReader) {

@ -208,3 +208,20 @@ bool Run::abort() {
}
return false;
}
bool Run::tag(const std::string& key, const std::string& value)
{
LLOG(INFO, "Setting tag ", key, value );
metaDataMap[key]=value;
return true;
}
std::string Run::getMetaDataJsonString()
{
Json j;
for (const auto& [key, value] : metaDataMap)
{
j.set(key.c_str(), value);
}
return( std::string(j.str()) );
}

@ -19,7 +19,10 @@
#ifndef LAMINAR_RUN_H_
#define LAMINAR_RUN_H_
#include "json.h"
#include <string>
#include <map>
#include <queue>
#include <list>
#include <functional>
@ -62,6 +65,11 @@ public:
// aborts this run
bool abort();
// store metadata into run
bool tag(const std::string& key, const std::string& value);
// Return the meta as JSON text
std::string getMetaDataJsonString();
std::string reason() const;
kj::Promise<void> whenStarted() { return startedFork.addBranch(); }
@ -101,6 +109,8 @@ private:
kj::ForkedPromise<void> startedFork;
kj::PromiseFulfillerPair<RunState> finished;
kj::ForkedPromise<RunState> finishedFork;
std::map<std::string, std::string> metaDataMap;
};
// All this below is a somewhat overengineered method of keeping track of

Loading…
Cancel
Save