Initial commit

This commit is contained in:
Markus Koch 2022-01-18 21:04:35 +01:00
commit 3cc2460a9e
4 changed files with 395 additions and 0 deletions

4
README.MD Normal file
View File

@ -0,0 +1,4 @@
# Micobu
Mini Continous Build
A tiny continuous build system written in bash and php.

82
gitea.php Normal file
View File

@ -0,0 +1,82 @@
<?php
// Micobu Gitea Hook
$secret_key = 'mykey';
$micobu_pipe = '/var/tmp/micobu_pipe';
$micobu_key = "mykey";
// check for POST request
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
error_log('GITEA HOOK: FAILED - not POST - '. $_SERVER['REQUEST_METHOD']);
exit();
}
// get content type
$content_type = isset($_SERVER['CONTENT_TYPE']) ? strtolower(trim($_SERVER['CONTENT_TYPE'])) : '';
if ($content_type != 'application/json') {
error_log('GITEA HOOK: FAILED - not application/json - '. $content_type);
exit();
}
// get payload
$payload = trim(file_get_contents("php://input"));
if (empty($payload)) {
error_log('GITEA HOOK: FAILED - no payload');
exit();
}
// get header signature
$header_signature = isset($_SERVER['HTTP_X_GITEA_SIGNATURE']) ? $_SERVER['HTTP_X_GITEA_SIGNATURE'] : '';
if (empty($header_signature)) {
error_log('GITEA HOOK: FAILED - header signature missing');
exit();
}
// calculate payload signature
$payload_signature = hash_hmac('sha256', $payload, $secret_key, false);
// check payload signature against header signature
if ($header_signature !== $payload_signature) {
error_log('GITEA HOOK: FAILED - payload signature');
exit();
}
// convert json to array
$decoded = json_decode($payload);
// check for json decode errors
if (json_last_error() !== JSON_ERROR_NONE) {
error_log('GITEA HOOK: FAILED - json decode - '. json_last_error());
exit();
}
// success, do something
$name = $decoded->repository->name;
$url = $decoded->repository->html_url;
$hash = $decoded->after;
preg_match("/^[_\-A-Za-z0-9]{1,30}$/", $name, $matches);
if (!$matches) {
error_log('GITEA HOOK: FAILED - name verification');
exit();
}
preg_match("/^[_\-A-Za-z0-9:\/\.]{1,150}$/", $url, $matches);
if (!$matches) {
error_log('GITEA HOOK: FAILED - url verification');
exit();
}
preg_match("/^[A-Za-z0-9]{1,50}$/", $hash, $matches);
if (!$matches) {
error_log('GITEA HOOK: FAILED - hash verification');
exit();
}
file_put_contents($micobu_pipe, "$micobu_key $name $url $hash\n");
?>

192
index.php Normal file
View File

@ -0,0 +1,192 @@
<?php
define("MICOBU_BASE", "/opt/micobu/rootfs/home/builduser/artefacts");
define("MICOBU_BASE_LINK", "./artefacts");
class micobu_build {
public $hash;
public $result;
public $time_start;
public $time_stop;
public $project;
}
class micobu_project {
public $name;
public $builds;
}
function glob_sorted_mtime($dir) {
$ar = [];
array_multisort(array_map('filemtime', ($files = glob("$dir/*", GLOB_ONLYDIR))), SORT_DESC, $files);
foreach($files as $filename)
{
$ar[] = $filename;
}
return $ar;
}
function scan_build(&$project, $hash) {
$build = new micobu_build();
$build->hash = $hash;
$build->result = -1;
$build->project = $project;
$file = MICOBU_BASE . "/". $project->name . "/" . $hash . "/meta.json";
if (file_exists($file)) {
$json = json_decode(file_get_contents($file));
$build->hash = $json->hash;
$build->result = $json->result;
$build->time_start = $json->time_start;
$build->time_stop = $json->time_stop;
$build->description = $json->description;
}
return $build;
}
function scan_project($name) {
$project = new micobu_project();
$project->name = $name;
$project->builds = array();
$dirs = glob_sorted_mtime(MICOBU_BASE . "/" . $name);
foreach ($dirs as $dir) {
$hash = basename($dir);
$project->builds += array($hash => scan_build($project, $hash));
}
return $project;
}
function micobu_scan() {
$projects = array();
foreach (glob(MICOBU_BASE . "/*", GLOB_ONLYDIR) as $project_path) {
$project_name = basename($project_path);
$projects += array($project_name => scan_project($project_name));
}
return $projects;
}
function micobu_get_trend($project) {
foreach ($project->builds as $build) {
if ($build->result > 0)
return "<span title='failed'>❌</span>";
else
return "<span title='success'>✅</span>";
}
return "";
return "🔼";
return "";
}
function micobu_get_build_icon($build) {
if ($build->result > 0)
return "<span title='Build failed with return code $build->result'>❌</span>";
else
return "<span title='Build succeeded'>✅</span>";
}
function micobu_get_build_time($build) {
return gmdate("i\m s\s", $build->time_stop - $build->time_start);
}
function micobu_get_weblink($build, $filename) {
return MICOBU_BASE_LINK . "/" . $build->project->name . "/" . $build->hash . "/" . $filename;
}
$projects = micobu_scan();
if (isset($_GET['p'])) {
$selected_project = $_GET['p'];
preg_match("/^[_\-A-Za-z0-9]{1,20}$/", $selected_project, $matches);
if (!$matches)
die("Invalid request.");
if (!isset($projects[$selected_project]))
die("Invalid project.");
if (isset($_GET['h'])) {
$selected_hash = $_GET['h'];
preg_match("/^[_\-A-Za-z0-9]{1,40}$/", $selected_hash, $matches);
if (!$matches)
die("Invalid request.");
if (!isset($projects[$selected_project]->builds[$selected_hash]))
die("Invalid hash.");
}
}
?>
<!DOCTYPE html>
<html>
<!--
Hi there, curious person!
This is the source code of Micobu's web dashboard.
You can find more information on this system on:
<link TBD>
-->
<head>
<style>
body {
font-family: sans-serif;
background-color: #111;
color: #CCC;
}
table {
background-color: #222;
border-radius: 8px;
font-size: 20pt;
border: 1px solid black;
width: 100%;
}
tr:hover {
background-color: #555;
}
td {
border: 1px dotted black;
}
a {
color: inherit;
text-decoration: none;
}
.footer {
margin: 8px;
color: #666;
text-align: right;
}
</style>
</head>
<body>
<?php
$dpath = (isset($selected_project) ? " / <a href='?p=$selected_project'>$selected_project</a>" : "");
$dpath .= (isset($selected_hash) ? " / $selected_hash" : "");
echo "<h1><a href='?'>Micobu CI Dashboard</a>$dpath</h1>";
?>
<div>
<?php
if (isset($selected_hash)) {
//var_dump($projects[$selected_project]->builds[$selected_hash]);
$build = $projects[$selected_project]->builds[$selected_hash];
echo "<table>";
echo "<tr><td>Build status</td><td>" . micobu_get_build_icon($build). "</td></tr>";
echo "<tr><td>Build time</td><td>" . micobu_get_build_time($build) . "</td></tr>";
echo "</table>";
} else if (isset($selected_project)) {
echo "<table>";
foreach ($projects[$selected_project]->builds as $hash => $build) {
echo "<tr><td><a href='?p=$selected_project&h=$hash'>" . date(DATE_RFC2822, $build->time_start) . "</a></td><td>" . micobu_get_build_icon($build) . "</td><td><a title='$build->hash'href='?p=$selected_project&h=$hash'>" . $build->description . "</a></td><td><a title='Show log file' href='" . micobu_get_weblink($build, "log.txt") . "'>📰</a> <a title='Show meta info' href='" . micobu_get_weblink($build, "meta.json") . "'>🛈</a> <a title='Show artefacts' href='" . micobu_get_weblink($build, "") . "'>📂</a></td></tr>";
}
echo "</table>";
} else {
echo "<table>";
foreach ($projects as $project) {
echo "<tr><td><a href='?p=" . $project->name ."'>" . $project->name . "</a></td><td>" . micobu_get_trend($project) . "</td></tr>";
}
echo "</table>";
}
?>
</div>
<div class="footer">
<?php
echo "Processed " . count($projects) . " repositories in " . round((microtime(true) - $_SERVER["REQUEST_TIME_FLOAT"]) * 1000, 1) . " ms.";
?>
</div>

117
micobu.sh Executable file
View File

@ -0,0 +1,117 @@
#!/bin/bash
# Chroot instructions
# ArchLinux root via pacstrap, configured with build tools
# User: builduser 1000
BUILD_BASE='/opt/micobu/rootfs/home/builduser/base'
ARTEFACT_BASE='/opt/micobu/rootfs/home/builduser/artefacts'
BUILD_TIMEOUT='600'
API_KEY='mykey'
SERVER_PIPE='/var/tmp/micobu_pipe'
function build() {
NAME="$1"
REPO="$2"
HASH="$3"
BUILD_DIR="$BUILD_BASE/$NAME"
ARTEFACT_DIR="$ARTEFACT_BASE/$NAME/$HASH"
MICOBU_TEMP_NAME="micobu_$NAME_$HASH" # Relative to $BUILD_BASE
if [ -d "$ARTEFACT_DIR" ]; then
echo "$NAME revision $HASH has already been built."
echo "Removing old version"
rm -rf $ARTEFACT_DIR
#return
fi
echo "[env] Building in $BUILD_DIR."
if [ ! -d "$BUILD_DIR" ]; then
echo "[git] Performing initial clone"
mkdir -p $BUILD_DIR
git clone --recurse-submodules "$REPO" "$BUILD_DIR"
cd $BUILD_DIR
else
echo "[git] Updating existing repo."
cd $BUILD_DIR
git fetch
fi
git clean -xdf
git reset --hard
git checkout $HASH
DESCRIPTION="`git describe --all`"
mkdir -p $BUILD_BASE/$MICOBU_TEMP_NAME
cat <<EOF > $BUILD_BASE/micobu_run.sh
#!/bin/bash
cd ~/$NAME
./run.py --no-color
RET=\$?
cd vunit_out/test_output
tar cvJf ~/$MICOBU_TEMP_NAME/artefacts.tar.xz *
exit \$RET
EOF
chmod +x $BUILD_BASE/micobu_run.sh
echo "[sec] Creating jail"
META_DATE_START="`date '+%s'`"
set +e
set -o pipefail
firejail --noprofile --chroot=/opt/micobu-rootfs --net=none --read-write="$BUILD_DIR" env -i HOME=/home/builduser PATH=/bin:/usr/bin "bash" "-c" "timeout $BUILD_TIMEOUT ~/micobu_run.sh" | tee $BUILD_BASE/$MICOBU_TEMP_NAME/log.txt
RET=$?
set +o pipefail
set -e
META_DATE_STOP="`date '+%s'`"
rm $BUILD_BASE/micobu_run.sh
cat <<EOF > $BUILD_BASE/$MICOBU_TEMP_NAME/meta.json
{
"name" : "$NAME",
"url" : "$REPO",
"hash" : "$HASH",
"description" : "$DESCRIPTION",
"time_start" : "$META_DATE_START",
"time_stop" : "$META_DATE_STOP",
"result" : "$RET"
}
EOF
mkdir -p $ARTEFACT_DIR
mv -T $BUILD_BASE/$MICOBU_TEMP_NAME $ARTEFACT_DIR
echo "Stored artefacts in $ARTEFACT_DIR"
find $ARTEFACT_BASE
cat $ARTEFACT_DIR/meta.json
echo ""
if [ $RET -eq 0 ]; then
echo 'BUILD SUCCEEDED!'
else
echo "BUILD FAILED (exit code $RET)!"
fi
return 0
}
function build_s()
{
if [ "$1" != "$API_KEY" ]; then
echo "error: invalid key"
return
fi
shift
echo Starting build of $@
build $@
}
set -e
[ ! -p "$SERVER_PIPE" ] && mkfifo "$SERVER_PIPE"
while true; do
args=`cat $SERVER_PIPE | sed -n 's/\([a-zA-Z0-9]\+\) \([a-zA-Z0-9]\+\) \([a-zA-Z0-9:\-_\/\.]\+\) \([a-z0-9]\+\)/\1 \2 \3 \4/p'`
build_s $args
done