Skip to content

Instantly share code, notes, and snippets.

@antifuchs
Last active November 24, 2023 16:45
Show Gist options
  • Save antifuchs/5c2394eb88e0ca436ba9113e2aaeaed7 to your computer and use it in GitHub Desktop.
Save antifuchs/5c2394eb88e0ca436ba9113e2aaeaed7 to your computer and use it in GitHub Desktop.
GithubEvalNotify.pm - a hydra plugin that pushes evaluation status of each commit to your nix flake's github repo
# GithubEvalNotify.pm - a hydra plugin that pushes evaluation status of each commit
# to your nix flake's github repo
#
# Note that this file must live under the path "ci/hydra-plugins/Hydra/Plugin/GithubEvalNotify.pm"
# The "ci/hydra-plugins" can be changed, but you have to adjust it in the hydra.nix config below.
# The "Hydra/Plugin/" directory must be kept intact though, otherwise perl won't load this module.
package Hydra::Plugin::GithubEvalNotify;
use strict;
use warnings;
use parent 'Hydra::Plugin';
use HTTP::Request;
use JSON::MaybeXS;
use LWP::UserAgent;
sub isEnabled {
my ($self) = @_;
return defined $self->{config}->{githubstatus};
}
## Evaluation has begun:
##
## Sadly, useless - isn't called at the right time & doesn't get the
## necessary IDs to say anything about the evaluation.
# sub evalStarted {
# my ($self, $traceID, $jobset) = @_;
# print STDERR "evalStarted was called!\n";
# }
## Same for evalFailed, but thankfully evalAdded is able to handle that case.
sub common {
my ($self, $traceID, $jobset, $eval, $cached) = @_;
my $baseurl = $self->{config}->{'base_uri'} || "http://localhost:3000";
my $jsId = $jobset->id;
my $jsName = $jobset->name;
my $evalId = $eval->id;
my $fl = $eval->flake;
if (!$fl) {
print STDERR "Evaluation finalized, but TODO - determine github ref: $evalId for jobset $jsId\n";
return 0;
}
my $job_name = $jobset->get_column('project') . ":" . $jobset->get_column('name');
my $github_job_name = $job_name =~ s/-pr-\d+//r;
my $context = "ci/hydra-eval:" . $github_job_name;
my $error = $eval->evaluationerror;
my $body = encode_json(
{
state => $error->errormsg ne "" ? "failure" : "success",
target_url => "$baseurl/eval/" . $eval->id,
description => "Hydra evaluation #" . $eval->id . " of $jsName",
context => $context,
});
print STDERR "Evaluation finalized (cached: $cached): $evalId, $github_job_name $fl\n";
if ($cached) {
return 0;
}
# Actually post status to github (sendStatus & the repo ref computation taken from GithubStatus.pm):
my $ua = LWP::UserAgent->new();
my $sendStatus = sub {
my ($input, $owner, $repo, $rev) = @_;
my $key = $owner . "-" . $repo . "-" . $rev;
my $url = "https://api.github.com/repos/$owner/$repo/statuses/$rev";
my $req = HTTP::Request->new('POST', $url);
$req->header('Content-Type' => 'application/json');
$req->header('Accept' => 'application/vnd.github.v3+json');
$req->header('Authorization' => $self->{config}->{github_authorization}->{$owner});
$req->content($body);
my $res = $ua->request($req);
print STDERR $res->status_line, ": ", $res->decoded_content, "\n" unless $res->is_success;
my $limit = $res->header("X-RateLimit-Limit");
my $limitRemaining = $res->header("X-RateLimit-Remaining");
my $limitReset = $res->header("X-RateLimit-Reset");
my $now = time();
my $diff = $limitReset - $now;
my $delay = (($limit - $limitRemaining) / $diff) * 5;
if ($limitRemaining < 1000) {
$delay = max(1, $delay);
}
if ($limitRemaining < 2000) {
print STDERR "GithubEvalNotify ratelimit $limitRemaining/$limit, resets in $diff, sleeping $delay\n";
sleep $delay;
} else {
print STDERR "GithubEvalNotify ratelimit $limitRemaining/$limit, resets in $diff\n";
}
};
if ($eval->flake =~ m!github:([^/]+)/([^/]+)/([[:xdigit:]]{40})$! or $eval->flake =~ m!git\+ssh://git\@github.com/([^/]+)/([^/]+)\?.*rev=([[:xdigit:]]{40})$!) {
$sendStatus->("src", $1, $2, $3);
} else {
print STDERR "Can't parse flake, skipping evaluation GitHub status update for $evalId, js:$jsId ev:$evalId $fl\n";
}
}
## Evaluation has finished adding builds:
sub evalAdded {
common(@_, 0);
}
## Evaluation didn't happen, as it is already cached:
sub evalCached {
common(@_, 1);
}
1;
{pkgs, config, lib, ...}: {
services.hydra = {
# The rest of your hydra config, and then:
extraEnv = {
PERL5LIB = let
hydraPlugins = builtins.path {
path = ./ci/hydra-plugins;
name = "hydra-plugins";
};
in "${hydraPlugins}";
};
extraConfig = ''
# Make sure you configure github authorization under <githubstatus>!
'';
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment