forked from the-distro/ofborg
Initial port from my network
This commit is contained in:
commit
0c7b2f252e
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
config.php
|
||||||
|
vendor
|
||||||
|
*.log
|
||||||
|
test.php
|
57
README.md
Normal file
57
README.md
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
# grahamcofborg
|
||||||
|
|
||||||
|
1. All github events go in to web/index.php, which sends the event to
|
||||||
|
an exchange named for the full name of the repo (ex: nixos/nixpkgs)
|
||||||
|
in lower case. The exchange is set to "fanout"
|
||||||
|
2. build-filter.php creates a queue called build-inputs and binds it
|
||||||
|
to the nixos/nixpkgs exchange. It also creates an exchange,
|
||||||
|
build-jobs, set to fan out. It listens for messages on the
|
||||||
|
build-inputs queue. Issue comments from authorized users on
|
||||||
|
PRs get tokenized and turned in to build instructions. These jobs
|
||||||
|
are then written to the build-jobs exchange.
|
||||||
|
3. builder.php creates a queue called `build-inputs-x86_64-linux`, and
|
||||||
|
binds it to the build-jobs exchange. It then listens for build
|
||||||
|
instructions on the `build-inputs-x86_64-linux` queue. For each
|
||||||
|
job, it uses nix-build to run the build instructions. The status
|
||||||
|
result (pass/fail) and the last ten lines of output are then placed
|
||||||
|
in to the `build-results` queue.
|
||||||
|
4. poster.php declares the build-results queue, and listens for
|
||||||
|
messages on it. It posts the build status and text output on the PR
|
||||||
|
the build is from.
|
||||||
|
|
||||||
|
|
||||||
|
The conspicuously missing config.php looks like:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
use PhpAmqpLib\Connection\AMQPSSLConnection;
|
||||||
|
use PhpAmqpLib\Message\AMQPMessage;
|
||||||
|
|
||||||
|
function rabbitmq_conn() {
|
||||||
|
$connection = new AMQPSSLConnection(
|
||||||
|
'events.nix.gsc.io', 5671,
|
||||||
|
eventsuser, eventspasswordd, '/', array(
|
||||||
|
'verify_peer' => true,
|
||||||
|
'verify_peer_name' => true,
|
||||||
|
'peer_name' => 'events.nix.gsc.io',
|
||||||
|
'verify_depth' => 10,
|
||||||
|
'ca_file' => '/etc/ssl/certs/ca-certificates.crt'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return $connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
function gh_client() {
|
||||||
|
$client = new \Github\Client();
|
||||||
|
$client->authenticate('githubusername',
|
||||||
|
'githubpassword',
|
||||||
|
Github\Client::AUTH_HTTP_PASSWORD);
|
||||||
|
|
||||||
|
return $client;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
118
build-filter.php
Normal file
118
build-filter.php
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
require __DIR__ . '/config.php';
|
||||||
|
use PhpAmqpLib\Message\AMQPMessage;
|
||||||
|
|
||||||
|
# define('AMQP_DEBUG', true);
|
||||||
|
$connection = rabbitmq_conn();
|
||||||
|
$channel = $connection->channel();
|
||||||
|
$channel->basic_qos(null, 1, true);
|
||||||
|
|
||||||
|
$channel->exchange_declare('build-jobs', 'fanout', false, true, false);
|
||||||
|
|
||||||
|
|
||||||
|
list($queueName, , ) = $channel->queue_declare('build-inputs',
|
||||||
|
false, true, false, false);
|
||||||
|
$channel->queue_bind($queueName, 'nixos/nixpkgs');
|
||||||
|
$channel->queue_bind($queueName, 'grahamc/elm-stuff');
|
||||||
|
|
||||||
|
function runner($msg) {
|
||||||
|
$in = json_decode($msg->body);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$etype = \GHE\EventClassifier::classifyEvent($in);
|
||||||
|
|
||||||
|
if ($etype != "issue_comment") {
|
||||||
|
echo "Skipping event type: $etype\n";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (\GHE\EventClassifierUnknownException $e) {
|
||||||
|
echo "Skipping unknown event type\n";
|
||||||
|
print_r($in);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\GHE\ACL::isUserAuthorized($in->comment->user->login)) {
|
||||||
|
echo "Commenter not authorized (" . $in->comment->user->login . ")\n";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\GHE\ACL::isRepoEligible($in->repository->full_name)) {
|
||||||
|
echo "Repo not authorized (" . $in->repository->full_name . ")\n";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($in->issue->pull_request)) {
|
||||||
|
echo "not a PR\n";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
# // We don't get a useful pull_request here, we'd have to fetch it
|
||||||
|
# to know if it is open
|
||||||
|
#if ($in->issue->pull_request->state != "open") {
|
||||||
|
# var_dump($in->issue->pull_request);
|
||||||
|
# echo "PR isn't open\n";
|
||||||
|
# return true;
|
||||||
|
# }
|
||||||
|
|
||||||
|
$cmt = explode(' ', strtolower($in->comment->body));
|
||||||
|
if (!in_array('@grahamcofborg', $cmt)) {
|
||||||
|
echo "not a borgpr\n";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cmt = explode(' ', $in->comment->body);
|
||||||
|
|
||||||
|
$tokens = array_map(function($term) { return trim($term); },
|
||||||
|
array_filter($cmt,
|
||||||
|
function($term) {
|
||||||
|
return !in_array(strtolower($term), [
|
||||||
|
"@grahamcofborg",
|
||||||
|
"",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (count($tokens) == 1 && implode("", $tokens) == "default") {
|
||||||
|
echo "default support is blocked\n";
|
||||||
|
return true;
|
||||||
|
$forward = [
|
||||||
|
'payload' => $in,
|
||||||
|
'build_default' => true,
|
||||||
|
'attrs' => [],
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
$forward = [
|
||||||
|
'payload' => $in,
|
||||||
|
'build_default' => false,
|
||||||
|
'attrs' => $tokens,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "forwarding to build-jobs :)\n";
|
||||||
|
|
||||||
|
$message = new AMQPMessage(json_encode($forward),
|
||||||
|
array('content_type' => 'application/json'));
|
||||||
|
$msg->delivery_info['channel']->basic_publish($message, 'build-jobs');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function outrunner($msg) {
|
||||||
|
try {
|
||||||
|
if (runner($msg) === true) {
|
||||||
|
$msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
|
||||||
|
}
|
||||||
|
} catch (\GHE\ExecException $e) {
|
||||||
|
var_dump($e->getMessage());
|
||||||
|
var_dump($e->getCode());
|
||||||
|
var_dump($e->getOutput());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$consumerTag = 'consumer' . getmypid();
|
||||||
|
$channel->basic_consume($queueName, $consumerTag, false, false, false, false, 'outrunner');
|
||||||
|
while(count($channel->callbacks)) {
|
||||||
|
$channel->wait();
|
||||||
|
}
|
107
builder.php
Normal file
107
builder.php
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
require __DIR__ . '/config.php';
|
||||||
|
use PhpAmqpLib\Message\AMQPMessage;
|
||||||
|
|
||||||
|
# define('AMQP_DEBUG', true);
|
||||||
|
$connection = rabbitmq_conn();
|
||||||
|
$channel = $connection->channel();
|
||||||
|
$channel->basic_qos(null, 1, true);
|
||||||
|
|
||||||
|
list($queueName, , ) = $channel->queue_declare('build-results',
|
||||||
|
false, true, false, false);
|
||||||
|
|
||||||
|
list($queueName, , ) = $channel->queue_declare('build-inputs-x86_64-linux',
|
||||||
|
false, true, false, false);
|
||||||
|
$channel->queue_bind($queueName, 'build-jobs');
|
||||||
|
|
||||||
|
|
||||||
|
function runner($msg) {
|
||||||
|
echo "got a job!\n";
|
||||||
|
$body = json_decode($msg->body);
|
||||||
|
$in = $body->payload;
|
||||||
|
|
||||||
|
$co = new GHE\Checkout("/home/grahamc/.nix-test", "builder");
|
||||||
|
$pname = $co->checkOutRef($in->repository->full_name,
|
||||||
|
$in->repository->clone_url,
|
||||||
|
$in->issue->number,
|
||||||
|
"origin/master"
|
||||||
|
);
|
||||||
|
|
||||||
|
$patch_url = $in->issue->pull_request->patch_url;
|
||||||
|
echo "Building $patch_url\n";
|
||||||
|
$co->applyPatches($pname, $patch_url);
|
||||||
|
|
||||||
|
if ($body->build_default) {
|
||||||
|
echo "building via nix-build .\n";
|
||||||
|
|
||||||
|
$cmd = 'NIX_PATH=nixpkgs=%s nix-build --option restrict-eval true --keep-going .';
|
||||||
|
$args = [$pname];
|
||||||
|
} else {
|
||||||
|
echo "building via nix-build . -A\n";
|
||||||
|
$attrs = array_intersperse(array_values((array)$body->attrs), '-A');
|
||||||
|
var_dump($attrs);
|
||||||
|
|
||||||
|
$fillers = implode(" ", array_fill(0, count($attrs), '%s'));
|
||||||
|
|
||||||
|
$cmd = 'NIX_PATH=nixpkgs=%s nix-build --option restrict-eval true --keep-going . ' . $fillers;
|
||||||
|
$args = $attrs;
|
||||||
|
array_unshift($args, $pname);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$output = GHE\Exec::exec($cmd, $args);
|
||||||
|
$pass = true;
|
||||||
|
} catch (GHE\ExecException $e) {
|
||||||
|
$output = $e->getOutput();
|
||||||
|
$pass = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastlines = array_reverse(
|
||||||
|
array_slice(
|
||||||
|
array_reverse($output),
|
||||||
|
0, 10
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$forward = [
|
||||||
|
'payload' => $in,
|
||||||
|
'output' => $lastlines,
|
||||||
|
'success' => $pass,
|
||||||
|
];
|
||||||
|
|
||||||
|
$message = new AMQPMessage(json_encode($forward),
|
||||||
|
array('content_type' => 'application/json'));
|
||||||
|
$msg->delivery_info['channel']->basic_publish($message, '', 'build-results');
|
||||||
|
$msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
|
||||||
|
|
||||||
|
echo "finished\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
function array_intersperse($array, $val) {
|
||||||
|
return array_reduce($array,
|
||||||
|
function($c, $elem) use ($val) {
|
||||||
|
$c[] = $val;
|
||||||
|
$c[] = $elem;
|
||||||
|
return $c;
|
||||||
|
},
|
||||||
|
array());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function outrunner($msg) {
|
||||||
|
try {
|
||||||
|
return runner($msg);
|
||||||
|
} catch (ExecException $e) {
|
||||||
|
var_dump($e->getMessage());
|
||||||
|
var_dump($e->getCode());
|
||||||
|
var_dump($e->getOutput());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$consumerTag = 'consumer' . getmypid();
|
||||||
|
$channel->basic_consume($queueName, $consumerTag, false, false, false, false, 'outrunner');
|
||||||
|
while(count($channel->callbacks)) {
|
||||||
|
$channel->wait();
|
||||||
|
}
|
14
composer.json
Normal file
14
composer.json
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"require": {
|
||||||
|
"php-amqplib/php-amqplib": ">=2.6.1",
|
||||||
|
"knplabs/github-api": "^2.6@dev",
|
||||||
|
"php-http/guzzle6-adapter": "^1.2@dev"
|
||||||
|
},
|
||||||
|
"minimum-stability": "dev",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {"GHE\\": "src/"}
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^6.4"
|
||||||
|
}
|
||||||
|
}
|
2461
composer.lock
generated
Normal file
2461
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
191
mass-rebuilder.php
Normal file
191
mass-rebuilder.php
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
require __DIR__ . '/config.php';
|
||||||
|
|
||||||
|
use PhpAmqpLib\Message\AMQPMessage;
|
||||||
|
|
||||||
|
# define('AMQP_DEBUG', true);
|
||||||
|
$connection = rabbitmq_conn();
|
||||||
|
$channel = $connection->channel();
|
||||||
|
$channel->basic_qos(null, 1, true);
|
||||||
|
|
||||||
|
|
||||||
|
list($queueName, , ) = $channel->queue_declare('mass-rebuild-checks',
|
||||||
|
false, true, false, false);
|
||||||
|
$channel->queue_bind($queueName, 'nixos/nixpkgs');
|
||||||
|
|
||||||
|
echo "hi\n";
|
||||||
|
|
||||||
|
function outrunner($msg) {
|
||||||
|
try {
|
||||||
|
$ret = runner($msg);
|
||||||
|
var_dump($ret);
|
||||||
|
if ($ret === true) {
|
||||||
|
echo "cool\n";
|
||||||
|
echo "acking\n";
|
||||||
|
$r = $msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
|
||||||
|
var_dump($r);
|
||||||
|
echo "acked\n";
|
||||||
|
} else {
|
||||||
|
echo "Not acking?\n";
|
||||||
|
}
|
||||||
|
} catch (\GHE\ExecException $e) {
|
||||||
|
var_dump($msg);
|
||||||
|
var_dump($e->getMessage());
|
||||||
|
var_dump($e->getCode());
|
||||||
|
var_dump($e->getOutput());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runner($msg) {
|
||||||
|
$in = json_decode($msg->body);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$etype = \GHE\EventClassifier::classifyEvent($in);
|
||||||
|
|
||||||
|
if ($etype != "pull_request") {
|
||||||
|
echo "Skipping event type: $etype\n";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (\GHE\EventClassifierUnknownException $e) {
|
||||||
|
echo "Skipping unknown event type\n";
|
||||||
|
print_r($in);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\GHE\ACL::isRepoEligible($in->repository->full_name)) {
|
||||||
|
echo "Repo not authorized (" . $in->repository->full_name . ")\n";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($in->pull_request->state != "open") {
|
||||||
|
echo "PR isn't open\n";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ok_events = [
|
||||||
|
'created',
|
||||||
|
'edited',
|
||||||
|
'synchronize',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!in_array($in->action, $ok_events)) {
|
||||||
|
echo "Uninteresting event " . $in->action . "\n";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$r = $msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
|
||||||
|
var_dump($r);
|
||||||
|
echo "acked\n";
|
||||||
|
|
||||||
|
|
||||||
|
$against_name = "origin/" . $in->pull_request->base->ref;
|
||||||
|
echo "Building against $against_name\n";
|
||||||
|
$co = new GHE\Checkout("/home/grahamc/.nix-test", "mr-est");
|
||||||
|
$pname = $co->checkOutRef($in->repository->full_name,
|
||||||
|
$in->repository->clone_url,
|
||||||
|
$in->number,
|
||||||
|
$against_name
|
||||||
|
);
|
||||||
|
|
||||||
|
$against = GHE\Exec::exec('git rev-parse %s', [$against_name]);
|
||||||
|
echo " $against_name is $against[0]\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
$co->applyPatches($pname, $in->pull_request->patch_url);
|
||||||
|
} catch (GHE\ExecException $e) {
|
||||||
|
echo "Received ExecException applying patches, likely due to conflicts:\n";
|
||||||
|
var_dump($e->getCode());
|
||||||
|
var_dump($e->getMessage());
|
||||||
|
var_dump($e->getArgs());
|
||||||
|
var_dump($e->getOutput());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$current = GHE\Exec::exec('git rev-parse HEAD');
|
||||||
|
echo " currently at ${current[0]}\n";
|
||||||
|
|
||||||
|
|
||||||
|
reply_to_issue($in, $against[0], $current[0]);
|
||||||
|
$msg->delivery_info['channel']->basic_cancel($msg->delivery_info['consumer_tag']);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reply_to_issue($issue, $prev, $current) {
|
||||||
|
$client = gh_client();
|
||||||
|
|
||||||
|
echo "current labels:\n";
|
||||||
|
$already_there = $client->api('issue')->labels()->all(
|
||||||
|
$issue->repository->owner->login,
|
||||||
|
$issue->repository->name,
|
||||||
|
$issue->number);
|
||||||
|
$already_there = array_map(function($val) { return $val['name']; }, $already_there);
|
||||||
|
var_dump($already_there);
|
||||||
|
|
||||||
|
$output = GHE\Exec::exec('$(nix-instantiate --eval -E %s) %s %s',
|
||||||
|
[
|
||||||
|
'<nixpkgs/maintainers/scripts/rebuild-amount.sh>',
|
||||||
|
$prev,
|
||||||
|
$current
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$labels = [];
|
||||||
|
foreach ($output as $line) {
|
||||||
|
if (preg_match('/^\s*(\d+) (.*)$/', $line, $matches)) {
|
||||||
|
var_dump($matches);
|
||||||
|
# TODO: separate out the rebuild ranges from the rebuild platform and
|
||||||
|
# splice the string together, rather than this ugliness
|
||||||
|
if ($matches[1] > 500) {
|
||||||
|
if ($matches[2] == "x86_64-darwin") {
|
||||||
|
$labels[] = "10.rebuild-darwin: 501+";
|
||||||
|
} else {
|
||||||
|
$labels[] = "10.rebuild-linux: 501+";
|
||||||
|
}
|
||||||
|
} else if ($matches[1] > 100 && $matches[1] <= 500) {
|
||||||
|
if ($matches[2] == "x86_64-darwin") {
|
||||||
|
$labels[] = "10.rebuild-darwin: 101-500";
|
||||||
|
} else {
|
||||||
|
$labels[] = "10.rebuild-linux: 101-500";
|
||||||
|
}
|
||||||
|
} else if ($matches[1] > 10 && $matches[1] <= 100) {
|
||||||
|
if ($matches[2] == "x86_64-darwin") {
|
||||||
|
$labels[] = "10.rebuild-darwin: 11-100";
|
||||||
|
} else {
|
||||||
|
$labels[] = "10.rebuild-linux: 11-100";
|
||||||
|
}
|
||||||
|
} else if ($matches[1] <= 10) {
|
||||||
|
if ($matches[2] == "x86_64-darwin") {
|
||||||
|
$labels[] = "10.rebuild-darwin: 1-10";
|
||||||
|
} else {
|
||||||
|
$labels[] = "10.rebuild-linux: 1-10";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
foreach ($labels as $label) {
|
||||||
|
if (in_array($label, $already_there)) {
|
||||||
|
echo "already labeled $label\n";
|
||||||
|
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
echo "will label +$label\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$client->api('issue')->labels()->add(
|
||||||
|
$issue->repository->owner->login,
|
||||||
|
$issue->repository->name,
|
||||||
|
$issue->number,
|
||||||
|
$label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$consumerTag = 'consumer' . getmypid();
|
||||||
|
$channel->basic_consume($queueName, $consumerTag, false, true, false, false, 'outrunner');
|
||||||
|
while(count($channel->callbacks)) {
|
||||||
|
$channel->wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Bye\n";
|
68
poster.php
Normal file
68
poster.php
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
require __DIR__ . '/config.php';
|
||||||
|
use PhpAmqpLib\Message\AMQPMessage;
|
||||||
|
|
||||||
|
# define('AMQP_DEBUG', true);
|
||||||
|
$connection = rabbitmq_conn();
|
||||||
|
$channel = $connection->channel();
|
||||||
|
$channel->basic_qos(null, 1, true);
|
||||||
|
|
||||||
|
list($queueName, , ) = $channel->queue_declare('build-results',
|
||||||
|
false, true, false, false);
|
||||||
|
|
||||||
|
function runner($msg) {
|
||||||
|
$body = json_decode($msg->body);
|
||||||
|
$in = $body->payload;
|
||||||
|
|
||||||
|
$num = $in->issue->number;
|
||||||
|
if ($body->success) {
|
||||||
|
echo "yay! $num passed!\n";
|
||||||
|
} else {
|
||||||
|
echo "Yikes, $num failede\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
reply_to_issue($in, implode("\n", $body->output), $body->success);
|
||||||
|
|
||||||
|
var_dump($body->success);
|
||||||
|
|
||||||
|
$msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reply_to_issue($issue, $output, $success) {
|
||||||
|
$client = gh_client();
|
||||||
|
$pr = $client->api('pull_request')->show(
|
||||||
|
$issue->repository->owner->login,
|
||||||
|
$issue->repository->name,
|
||||||
|
$issue->issue->number
|
||||||
|
);
|
||||||
|
$sha = $pr['head']['sha'];
|
||||||
|
|
||||||
|
$client->api('pull_request')->reviews()->create(
|
||||||
|
$issue->repository->owner->login,
|
||||||
|
$issue->repository->name,
|
||||||
|
$issue->issue->number,
|
||||||
|
array(
|
||||||
|
'body' => "```\n$output\n```",
|
||||||
|
'event' => $success ? 'APPROVE' : 'COMMENT',
|
||||||
|
'commit_id' => $sha,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function outrunner($msg) {
|
||||||
|
try {
|
||||||
|
return runner($msg);
|
||||||
|
} catch (ExecException $e) {
|
||||||
|
var_dump($e->getMessage());
|
||||||
|
var_dump($e->getCode());
|
||||||
|
var_dump($e->getOutput());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$consumerTag = 'consumer' . getmypid();
|
||||||
|
$channel->basic_consume($queueName, $consumerTag, false, false, false, false, 'outrunner');
|
||||||
|
while(count($channel->callbacks)) {
|
||||||
|
$channel->wait();
|
||||||
|
}
|
15
shell.nix
Normal file
15
shell.nix
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
let
|
||||||
|
pkgs = import <nixpkgs> {};
|
||||||
|
|
||||||
|
inherit (pkgs) stdenv;
|
||||||
|
|
||||||
|
in stdenv.mkDerivation rec {
|
||||||
|
name = "gh-event-forwarder";
|
||||||
|
src = ./.;
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
php
|
||||||
|
phpPackages.composer
|
||||||
|
];
|
||||||
|
|
||||||
|
HISTFILE = "${src}/.bash_hist";
|
||||||
|
}
|
37
src/ACL.php
Normal file
37
src/ACL.php
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
namespace GHE;
|
||||||
|
|
||||||
|
class ACL {
|
||||||
|
static public function getRepos() {
|
||||||
|
return [
|
||||||
|
'grahamc/elm-stuff',
|
||||||
|
'nixos/nixpkgs',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function getUsers() {
|
||||||
|
return [
|
||||||
|
'copumpkin',
|
||||||
|
'domenkozar',
|
||||||
|
'fpletz',
|
||||||
|
'fridh',
|
||||||
|
'globin',
|
||||||
|
'grahamc',
|
||||||
|
'lnl7',
|
||||||
|
'mic92',
|
||||||
|
'shlevy',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function isRepoEligible($repo) {
|
||||||
|
return in_array(strtolower($repo), self::getRepos());
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function isUserAuthorized($user) {
|
||||||
|
return in_array(strtolower($user), self::getUsers());
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function authorizeUserRepo($user, $repo) {
|
||||||
|
return self::isRepoEligible($repo) && self::isUserAuthorized($user);
|
||||||
|
}
|
||||||
|
}
|
117
src/Checkout.php
Normal file
117
src/Checkout.php
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace GHE;
|
||||||
|
|
||||||
|
class Checkout {
|
||||||
|
|
||||||
|
protected $root;
|
||||||
|
protected $type;
|
||||||
|
|
||||||
|
function __construct($root, $type) {
|
||||||
|
$this->root = $root;
|
||||||
|
$this->type = $type;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkOutRef($repo_name, $clone_url, $id, $ref) {
|
||||||
|
$this->prefetchRepoCache($repo_name, $clone_url);
|
||||||
|
|
||||||
|
$pname = $this->pathToRepoCache($repo_name);
|
||||||
|
$bname = $this->pathToBuildDir($repo_name, $id);
|
||||||
|
|
||||||
|
$guard = $this->guard($bname);
|
||||||
|
if (!is_dir($bname)) {
|
||||||
|
echo "Cloning " . $id . " to $bname\n";
|
||||||
|
Exec::exec('git clone --reference-if-able %s %s %s',
|
||||||
|
[
|
||||||
|
$pname,
|
||||||
|
$clone_url,
|
||||||
|
$bname
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chdir($bname)) {
|
||||||
|
throw new CoFailedException("Failed to chdir to $bname\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "fetching " . $id . " in $bname\n";
|
||||||
|
Exec::exec('git fetch origin');
|
||||||
|
try {
|
||||||
|
Exec::exec('git am --abort');
|
||||||
|
} catch (ExecException $e) {
|
||||||
|
// non-zero exit if no am is in progress
|
||||||
|
}
|
||||||
|
Exec::exec('git reset --hard %s', [$ref]);
|
||||||
|
|
||||||
|
|
||||||
|
$this->release($guard);
|
||||||
|
|
||||||
|
return $bname;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPatches($bname, $patch_url) {
|
||||||
|
if (!chdir($bname)) {
|
||||||
|
throw new CoFailedException("Failed to chdir to $bname\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
$guard = $this->guard($pname);
|
||||||
|
Exec::exec('curl -L %s | git am --no-gpg-sign -', [$patch_url]);
|
||||||
|
$this->release($guard);
|
||||||
|
}
|
||||||
|
|
||||||
|
function prefetchRepoCache($name, $clone_url) {
|
||||||
|
if (!chdir($this->root)) {
|
||||||
|
throw new CoFailedException("Failed to chdir to " . $this->root);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pname = $this->pathToRepoCache($name);
|
||||||
|
|
||||||
|
$guard = $this->guard($pname);
|
||||||
|
|
||||||
|
if (!is_dir($pname)) {
|
||||||
|
echo "Cloning " . $name . " to $pname\n";
|
||||||
|
Exec::exec('git clone --bare %s %s',
|
||||||
|
[
|
||||||
|
$clone_url,
|
||||||
|
$pname
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->release($guard);
|
||||||
|
|
||||||
|
if (!chdir($pname)) {
|
||||||
|
throw new CoFailedException("Failed to chdir to $pname");
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Fetching $name to $pname\n";
|
||||||
|
Exec::exec('git fetch origin');
|
||||||
|
}
|
||||||
|
|
||||||
|
function pathToRepoCache($name) {
|
||||||
|
return $this->root . "/repo-" . md5($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pathToBuildDir($repo, $id_number) {
|
||||||
|
$id = (int) $id_number;
|
||||||
|
$repo_hash = md5($repo);
|
||||||
|
$type = $this->type;
|
||||||
|
|
||||||
|
return $this->root . "/$type-$repo_hash-$id";
|
||||||
|
}
|
||||||
|
|
||||||
|
function guard($path) {
|
||||||
|
$res = fopen("$path.lock", 'c');
|
||||||
|
while (!flock($res, LOCK_EX)) {
|
||||||
|
echo "waiting for lock on $path...\n";
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function release($res) {
|
||||||
|
fclose($res);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class CoFailedException extends \Exception {}
|
132
src/EventClassifier.php
Normal file
132
src/EventClassifier.php
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace GHE;
|
||||||
|
|
||||||
|
class EventClassifier {
|
||||||
|
public static function classifyEvent($payload) {
|
||||||
|
if (self::isIssuesEvent($payload)) {
|
||||||
|
return "issues";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::isIssueComment($payload)) {
|
||||||
|
return "issue_comment";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::isCommitComment($payload)) {
|
||||||
|
return "commit_comment";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::isPullRequestReviewComment($payload)) {
|
||||||
|
return "pull_request_review_comment";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::isPullRequestReviewEvent($payload)) {
|
||||||
|
return "pull_request_review";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::isPullRequestEvent($payload)) {
|
||||||
|
return "pull_request";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::isStatusEvent($payload)) {
|
||||||
|
return "status";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::isPushEvent($payload)) {
|
||||||
|
return "push";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::isWatchEvent($payload)) {
|
||||||
|
return "watch";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::isForkEvent($payload)) {
|
||||||
|
return "fork";
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new EventClassifierUnknownException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isIssuesEvent($payload) {
|
||||||
|
return isset($payload->issue)
|
||||||
|
&& !isset($payload->comment)
|
||||||
|
&& isset($payload->action)
|
||||||
|
&& in_array($payload->action,
|
||||||
|
[ "assigned", "unassigned", "labeled",
|
||||||
|
"unlabeled", "opened", "edited",
|
||||||
|
"milestoned", "demilestoned", "closed",
|
||||||
|
"reopened" ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isIssueComment($payload) {
|
||||||
|
return isset($payload->issue)
|
||||||
|
&& isset($payload->comment)
|
||||||
|
&& isset($payload->action)
|
||||||
|
&& in_array($payload->action,
|
||||||
|
['created', 'edited', 'deleted']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isCommitComment($payload) {
|
||||||
|
return !isset($payload->issue)
|
||||||
|
&& !isset($payload->pull_request)
|
||||||
|
&& isset($payload->comment)
|
||||||
|
&& isset($payload->action);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isPullRequestReviewComment($payload) {
|
||||||
|
return !isset($payload->issue)
|
||||||
|
&& isset($payload->pull_request)
|
||||||
|
&& isset($payload->comment)
|
||||||
|
&& isset($payload->action)
|
||||||
|
&& in_array($payload->action,
|
||||||
|
['created', 'edited', 'deleted']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isPullRequestReviewEvent($payload) {
|
||||||
|
return isset($payload->review)
|
||||||
|
&& isset($payload->pull_request)
|
||||||
|
&& isset($payload->action)
|
||||||
|
&& in_array($payload->action,
|
||||||
|
['submitted', 'edited', 'dismissed']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isPullRequestEvent($payload) {
|
||||||
|
return isset($payload->number)
|
||||||
|
&& isset($payload->pull_request)
|
||||||
|
&& isset($payload->action)
|
||||||
|
&& in_array($payload->action,
|
||||||
|
[ "assigned", "unassigned",
|
||||||
|
"review_requested",
|
||||||
|
"review_request_removed", "labeled",
|
||||||
|
"unlabeled", "opened", "edited", "closed",
|
||||||
|
"reopened", "synchronize" ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isStatusEvent($payload) {
|
||||||
|
return isset($payload->sha)
|
||||||
|
&& isset($payload->commit)
|
||||||
|
&& isset($payload->state)
|
||||||
|
&& in_array($payload->state,
|
||||||
|
['pending', 'success', 'failure', 'error']);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isPushEvent($payload) {
|
||||||
|
return isset($payload->head_commit)
|
||||||
|
&& isset($payload->commits)
|
||||||
|
&& isset($payload->compare)
|
||||||
|
&& isset($payload->forced);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isWatchEvent($payload) {
|
||||||
|
return isset($payload->action)
|
||||||
|
&& $payload->action == "started";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isForkEvent($payload) {
|
||||||
|
return isset($payload->forkee);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventClassifierUnknownException extends \Exception{};
|
42
src/Exec.php
Normal file
42
src/Exec.php
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace GHE;
|
||||||
|
|
||||||
|
class Exec {
|
||||||
|
public static function exec($cmd, $args = array()) {
|
||||||
|
$safeArgs = array_map('escapeshellarg', $args);
|
||||||
|
$interiorCmd = vsprintf($cmd, $safeArgs);
|
||||||
|
|
||||||
|
$exteriorCmd = sprintf('/bin/sh -o pipefail -euc %s 2>&1',
|
||||||
|
escapeshellarg($interiorCmd));
|
||||||
|
|
||||||
|
exec($exteriorCmd, $output, $return);
|
||||||
|
|
||||||
|
if ($return > 0) {
|
||||||
|
throw new ExecException($cmd, $args, $output, $return);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExecException extends \Exception {
|
||||||
|
protected $args;
|
||||||
|
protected $output;
|
||||||
|
|
||||||
|
public function __construct($cmd, $args, $output, $return) {
|
||||||
|
$this->args = $args;
|
||||||
|
$this->output = $output;
|
||||||
|
|
||||||
|
parent::__construct("Error calling $cmd", $return);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getArgs() {
|
||||||
|
return $this->args;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOutput() {
|
||||||
|
return $this->output;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
3
src/NixBuild.php
Normal file
3
src/NixBuild.php
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace GHE;
|
68
src/TestExec.php
Normal file
68
src/TestExec.php
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace GHE;
|
||||||
|
|
||||||
|
class TestExec extends \PHPUnit\Framework\TestCase
|
||||||
|
{
|
||||||
|
/** Exec::exec('curl -L %s | git am --no-gpg-sign -');
|
||||||
|
|
||||||
|
*/
|
||||||
|
function testExecBasic() {
|
||||||
|
$this->assertEquals(
|
||||||
|
['oof'],
|
||||||
|
Exec::exec('echo foo | rev')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testExecArgs() {
|
||||||
|
$this->assertEquals(
|
||||||
|
['rab'],
|
||||||
|
Exec::exec('echo %s | rev', ['bar'])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testExecArgsDangerous() {
|
||||||
|
$this->assertEquals(
|
||||||
|
['$(whoami)'],
|
||||||
|
Exec::exec('echo %s', ['$(whoami)'])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testExecFailureExceptions() {
|
||||||
|
$this->expectException(ExecException::class);
|
||||||
|
$this->expectExceptionCode(123);
|
||||||
|
$this->expectExceptionMessage("Error calling exit 123");
|
||||||
|
Exec::exec('exit 123');
|
||||||
|
}
|
||||||
|
|
||||||
|
function testExecFailureExceptionsOutput() {
|
||||||
|
try {
|
||||||
|
Exec::exec('echo %s; exit %s', ["heya", 10]);
|
||||||
|
$this->assertFalse(true, "Should have excepted!");
|
||||||
|
} catch (ExecException $e) {
|
||||||
|
$this->assertEquals(10, $e->getCode());
|
||||||
|
$this->assertEquals(["heya", 10], $e->getArgs());
|
||||||
|
$this->assertEquals(["heya"], $e->getOutput());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function testExecFailureExceptionPipefailEnd() {
|
||||||
|
try {
|
||||||
|
var_dump(Exec::exec('echo "foo" | (exit 2);'));
|
||||||
|
$this->assertFalse(true, "Should have excepted!");
|
||||||
|
} catch (ExecException $e) {
|
||||||
|
$this->assertEquals(2, $e->getCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testExecFailureExceptionPipefailStart() {
|
||||||
|
try {
|
||||||
|
var_dump(Exec::exec('(echo "foo"; exit 3) | rev;'));
|
||||||
|
$this->assertFalse(true, "Should have excepted!");
|
||||||
|
} catch (ExecException $e) {
|
||||||
|
$this->assertEquals(3, $e->getCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
27
web/index.php
Normal file
27
web/index.php
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../config.php';
|
||||||
|
use PhpAmqpLib\Message\AMQPMessage;
|
||||||
|
|
||||||
|
$connection = rabbitmq_conn();
|
||||||
|
$channel = $connection->channel();
|
||||||
|
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
if (!isset($input['repository']['full_name'])) {
|
||||||
|
echo "no full_name set?";
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$name = strtolower($input['repository']['full_name']);
|
||||||
|
if (!GHE\ACL::isRepoEligible($name)) {
|
||||||
|
echo "repo not in ok name list";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$channel->exchange_declare($name, 'fanout', false, true, false);
|
||||||
|
|
||||||
|
$message = new AMQPMessage(json_encode($input),
|
||||||
|
array('content_type' => 'application/json'));
|
||||||
|
|
||||||
|
$channel->basic_publish($message, $name);
|
Loading…
Reference in a new issue