From 500ac837240331bb68b4c0028c1247b7b01b8dd6 Mon Sep 17 00:00:00 2001
From: Faye Chun <faye@lolc.at>
Date: Wed, 18 Dec 2024 04:27:22 -0500
Subject: [PATCH] Add a plugin to poll Gitea pull requests

Based off the existing GithubPulls.pm and GitlabPulls.pm plugins.

Also adds an integration test for the new 'giteapulls' input type to
the existing 'gitea' test.
---
 nixos-tests.nix                    | 73 +++++++++++++++++++++-----
 src/lib/Hydra/Plugin/GiteaPulls.pm | 84 ++++++++++++++++++++++++++++++
 2 files changed, 143 insertions(+), 14 deletions(-)
 create mode 100644 src/lib/Hydra/Plugin/GiteaPulls.pm

diff --git a/nixos-tests.nix b/nixos-tests.nix
index 9efe68c8..2de75eeb 100644
--- a/nixos-tests.nix
+++ b/nixos-tests.nix
@@ -145,10 +145,18 @@ in
             git -C /tmp/repo add .
             git config --global user.email test@localhost
             git config --global user.name test
+
+            # Create initial commit
             git -C /tmp/repo commit -m 'Initial import'
             git -C /tmp/repo remote add origin gitea@machine:root/repo
-            GIT_SSH_COMMAND='ssh -i $HOME/.ssh/privk -o StrictHostKeyChecking=no' \
-              git -C /tmp/repo push origin master
+            export GIT_SSH_COMMAND='ssh -i $HOME/.ssh/privk -o StrictHostKeyChecking=no'
+            git -C /tmp/repo push origin master
+            git -C /tmp/repo log >&2
+
+            # Create PR branch
+            git -C /tmp/repo checkout -b pr
+            git -C /tmp/repo commit --allow-empty -m 'Additional change'
+            git -C /tmp/repo push origin pr
             git -C /tmp/repo log >&2
           '';
 
@@ -185,7 +193,7 @@ in
             cat >data.json <<EOF
             {
               "description": "Trivial",
-              "checkinterval": "60",
+              "checkinterval": "20",
               "enabled": "1",
               "visible": "1",
               "keepnr": "1",
@@ -199,7 +207,12 @@ in
                 "gitea_repo_name": {"value": "repo", "type": "string"},
                 "gitea_repo_owner": {"value": "root", "type": "string"},
                 "gitea_status_repo": {"value": "git", "type": "string"},
-                "gitea_http_url": {"value": "http://localhost:3001", "type": "string"}
+                "gitea_http_url": {"value": "http://localhost:3001", "type": "string"},
+                "pulls": {
+                  "type": "giteapulls",
+                  "value": "localhost:3001 root repo http",
+                  "emailresponsible": false
+                }
               }
             }
             EOF
@@ -227,15 +240,31 @@ in
           };
 
           smallDrv = pkgs.writeText "jobset.nix" ''
-            { trivial = builtins.derivation {
-                name = "trivial";
-                system = "${system}";
-                builder = "/bin/sh";
-                allowSubstitutes = false;
-                preferLocalBuild = true;
-                args = ["-c" "echo success > $out; exit 0"];
+            { pulls, ... }:
+
+            let
+              genDrv = name: builtins.derivation {
+                 inherit name;
+                 system = "${system}";
+                 builder = "/bin/sh";
+                 allowSubstitutes = false;
+                 preferLocalBuild = true;
+                 args = ["-c" "echo success > $out; exit 0"];
               };
-             }
+
+              prs = builtins.fromJSON (builtins.readFile pulls);
+              prJobNames = map (n: "pr-''${n}") (builtins.attrNames prs);
+              prJobset = builtins.listToAttrs (
+                map (
+                  name: {
+                    inherit name;
+                    value = genDrv name;
+                  }
+                ) prJobNames
+              );
+            in {
+              trivial = genDrv "trivial";
+            } // prJobset
           '';
         in
         ''
@@ -279,18 +308,34 @@ in
               + '|  jq .buildstatus | xargs test 0 -eq'
           )
 
+          machine.sleep(3)
+
           data = machine.succeed(
-              'curl -Lf -s "http://localhost:3001/api/v1/repos/root/repo/statuses/$(cd /tmp/repo && git show | head -n1 | awk "{print \\$2}")" '
+              'curl -Lf -s "http://localhost:3001/api/v1/repos/root/repo/statuses/$(cd /tmp/repo && git show master | head -n1 | awk "{print \\$2}")?sort=leastindex" '
               + "-H 'Accept: application/json' -H 'Content-Type: application/json' "
               + f"-H 'Authorization: token ${api_token}'"
           )
 
           response = json.loads(data)
 
-          assert len(response) == 2, "Expected exactly three status updates for latest commit (queued, finished)!"
+          assert len(response) == 2, "Expected exactly two status updates for latest commit (queued, finished)!"
           assert response[0]['status'] == "success", "Expected finished status to be success!"
           assert response[1]['status'] == "pending", "Expected queued status to be pending!"
 
+          # giteapulls test
+
+          machine.succeed(
+              "curl --fail -X POST http://localhost:3001/api/v1/repos/root/repo/pulls "
+              + "-H 'Accept: application/json' -H 'Content-Type: application/json' "
+              + f"-H 'Authorization: token ${api_token}'"
+              + ' -d \'{"title":"Test PR", "base":"master", "head": "pr"}\'''
+          )
+
+          machine.wait_until_succeeds(
+              'curl -Lf -s http://localhost:3000/build/2 -H "Accept: application/json" '
+              + '|  jq .buildstatus | xargs test 0 -eq'
+          )
+
           machine.shutdown()
         '';
     });
diff --git a/src/lib/Hydra/Plugin/GiteaPulls.pm b/src/lib/Hydra/Plugin/GiteaPulls.pm
new file mode 100644
index 00000000..c43d207d
--- /dev/null
+++ b/src/lib/Hydra/Plugin/GiteaPulls.pm
@@ -0,0 +1,84 @@
+# Allow building based on Gitea pull requests.
+#
+# Example input:
+#   "pulls": {
+#     "type": "giteapulls",
+#     "value": "example.com alice repo"
+#     "emailresponsible": false
+#   }
+
+package Hydra::Plugin::GiteaPulls;
+
+use strict;
+use warnings;
+use parent 'Hydra::Plugin';
+use HTTP::Request;
+use LWP::UserAgent;
+use JSON::MaybeXS;
+use Hydra::Helper::CatalystUtils;
+use File::Temp;
+use POSIX qw(strftime);
+
+sub supportedInputTypes {
+    my ($self, $inputTypes) = @_;
+    $inputTypes->{'giteapulls'} = 'Open Gitea Pull Requests';
+}
+
+sub _iterate {
+    my ($url, $auth, $pulls, $ua) = @_;
+
+    my $req = HTTP::Request->new('GET', $url);
+    $req->header('Authorization' => 'token ' . $auth) if defined $auth;
+
+    my $res = $ua->request($req);
+    my $content = $res->decoded_content;
+    die "Error pulling from the gitea pulls API: $content\n"
+	unless $res->is_success;
+
+    my $pulls_list = decode_json $content;
+
+    foreach my $pull (@$pulls_list) {
+	$pulls->{$pull->{number}} = $pull;
+    }
+
+    # TODO Make Link header parsing more robust!!!
+    my @links = split ',', ($res->header("Link") // "");
+    my $next = "";
+    foreach my $link (@links) {
+        my ($url, $rel) = split ";", $link;
+        if (trim($rel) eq 'rel="next"') {
+            $next = substr trim($url), 1, -1;
+            last;
+        }
+    }
+    _iterate($next, $auth, $pulls, $ua) unless $next eq "";
+}
+
+sub fetchInput {
+    my ($self, $type, $name, $value, $project, $jobset) = @_;
+    return undef if $type ne "giteapulls";
+
+    my ($baseUrl, $owner, $repo, $proto) = split ' ', $value;
+    if (not defined $proto) { # the protocol handler is exposed as an option in order to do integration testing
+	$proto = "https"
+    }
+    my $auth = $self->{config}->{gitea_authorization}->{$owner};
+
+    my $ua = LWP::UserAgent->new();
+    my %pulls;
+    _iterate("$proto://$baseUrl/api/v1/repos/$owner/$repo/pulls?limit=100", $auth, \%pulls, $ua);
+
+    my $tempdir = File::Temp->newdir("gitea-pulls" . "XXXXX", TMPDIR => 1);
+    my $filename = "$tempdir/gitea-pulls.json";
+    open(my $fh, ">", $filename) or die "Cannot open $filename for writing: $!";
+    print $fh encode_json \%pulls;
+    close $fh;
+
+    my $storePath = trim(`nix-store --add "$filename"`
+        or die "cannot copy path $filename to the Nix store.\n");
+    chomp $storePath;
+    my $timestamp = time;
+    return { storePath => $storePath, revision => strftime "%Y%m%d%H%M%S", gmtime($timestamp) };
+}
+
+1;