-
Notifications
You must be signed in to change notification settings - Fork 22
/
openqa-clone-and-monitor-job-from-pr
executable file
·221 lines (192 loc) · 9 KB
/
openqa-clone-and-monitor-job-from-pr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
#!/usr/bin/env perl
# Copyright SUSE LLC
=head1 NAME
openqa-clone-and-monitor-job-from-pr
=head1 SYNOPSIS
Clones and monitors openQA jobs mentioned in a PR description or PR comment as
CI action. This script is supposed to be used via the GitHub action defined in
C<actions/clone-job/action.yaml> like this:
=begin text
---
name: Run openQA tests
on:
pull_request_target:
workflow_dispatch:
env:
OPENQA_HOST: ${{ secrets.OPENQA_URL }}
OPENQA_API_KEY: ${{ secrets.OPENQA_API_KEY }}
OPENQA_API_SECRET: ${{ secrets.OPENQA_API_SECRET }}
GH_REPO: ${{ github.event.pull_request.head.repo.full_name }}
GH_REF: ${{ github.event.pull_request.head.ref }}
GH_PR_BODY: ${{ github.event.pull_request.body }}
jobs:
clone_and_monitor_job_from_pr:
runs-on: ubuntu-latest
container:
image: registry.opensuse.org/devel/openqa/containers/tumbleweed:client
steps:
- uses: os-autoinst/scripts/actions/clone-job@master
=end text
It will then clone and monitor an openQA job when a PR mentioning one via e.g.
C<@openqa: Clone https://openqa.opensuse.org/tests/123456 FOO=bar> is created.
By default it will clone the job on o3 into the "Development / GitHub" group. To
use a different openQA instance and group, set the environment variables
C<OPENQA_HOST> and C<OPENQA_SCHEDULE_GROUP_ID> accordingly.
For jobs mentioned in PR comments one can use the same GitHub action like this:
=begin text
---
name: Clone an openQA test mentioned in a PR comment
on:
issue_comment:
types: [created, edited]
env:
OPENQA_HOST: ${{ vars.OPENQA_URL }}
OPENQA_API_KEY: ${{ secrets.OPENQA_API_KEY }}
OPENQA_API_SECRET: ${{ secrets.OPENQA_API_SECRET }}
GH_PR_URL: ${{ github.event.issue.pull_request.url }}
GH_COMMENT_BODY: ${{ github.event.comment.body }}
GH_COMMENT_AUTHOR: ${{ github.event.comment.user.login }}
GH_COMMENT_URL: ${{ github.event.comment.html_url }}
RESTRICT_ORGA: os-autoinst
RESTRICT_TEAM: tests-maintainer
jobs:
clone_mentioned_job:
runs-on: ubuntu-latest
if: "github.event.issue.pull_request && contains(github.event.comment.body, 'openqa: Clone ')"
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN_FOR_ACTIONS }}
container:
image: registry.opensuse.org/devel/openqa/containers/tumbleweed:client
steps:
- name: Clone and monitor job mentioned in PR comment
uses: os-autoinst/scripts/actions/clone-job@master
=end text
So it works very similar to the usage for PR descriptions. The most important
difference is that you have to supply a GitHub token manually that is able to
use the statuses API on the relevant repository (`repo:status` permission). You
probably also want to restrict the cloning to members of a team via the
`RESTRICT_` variables as shown in this example so the token also needs access to
that (`read:org` permission).
For local testing you may also invoke the script manually. Have a look at the
handling of environment variables at the beginning of the script code as you
need to set certain additional environment variables. The test
`test/05-clone-and-monitor.t` can also clarify the behavior of this script from
user perspective.
=back
=cut
package openqa_clone_and_monitor_job_from_pr; # for testing
use Mojo::Base -strict, -signatures;
use Mojo::JSON qw(decode_json);
use Mojo::JSON::Pointer;
use Mojo::UserAgent;
use Text::ParseWords qw(shellwords);
my $expected_url = $ENV{OPENQA_HOST} // 'https://openqa.opensuse.org';
my $group_id = $ENV{OPENQA_SCHEDULE_GROUP_ID} // 118;
my $pr_body = $ENV{GH_PR_BODY} // $ENV{GH_COMMENT_BODY} // '';
my $gh_repo = $ENV{GH_REPO} ? "$ENV{GH_REPO}.git" : '';
my $gh_pr_url = $ENV{GH_PR_URL};
my $gh_statuses_url = $ENV{GH_STATUSES_URL};
my $gh_ref = $ENV{GH_REF};
my $gh_srv = $ENV{GITHUB_SERVER_URL} or die 'GITHUB_SERVER_URL must be set';
my $gh_api = $ENV{GITHUB_API_URL};
my $gh_token = $ENV{GITHUB_TOKEN} // '';
my $api_key = $ENV{OPENQA_API_KEY} or die 'OPENQA_API_KEY must be set';
my $api_secret = $ENV{OPENQA_API_SECRET} or die 'OPENQA_API_SECRET must be set';
my $user = $ENV{GH_COMMENT_AUTHOR} // '';
my $comment_url = $ENV{GH_COMMENT_URL} // '';
my $cloned_by_url = $comment_url // $ENV{GH_PR_HTML_URL};
my $restrict_orga = $ENV{RESTRICT_ORGA} // '';
my $restrict_team = $ENV{RESTRICT_TEAM} // '';
my @secrets = ('--apikey', $api_key, '--apisecret', $api_secret);
my @cloned_by_vars = $cloned_by_url ? ("CLONED_BY=$cloned_by_url") : ();
my $gh_clone_url = $gh_repo ? "$gh_srv/$gh_repo" : undef;
my %gh_headers = (Accept => 'application/vnd.github+json', Authorization => "Bearer $gh_token");
my $ua = Mojo::UserAgent->new;
die 'GH_REF or GH_PR_URL must be set' unless $gh_ref || $gh_pr_url;
die 'GH_REPO or GH_PR_URL must be set' unless $gh_repo || $gh_pr_url;
die 'GITHUB_TOKEN and GITHUB_API_URL must be set when RESTRICT_ variables are set'
if (!$gh_token || !$gh_api) && ($restrict_orga || $restrict_team);
# use `shellwords` to split arguments like a shell would (so one can use quotes to avoid splitting)
# use `grep` to filter out arguments starting with `-`/`--` to avoid users passing unexpected flags/arguments
sub _split_and_filter_arguments ($args) { grep { $_ && $_ !~ qr/^\s*-/ } shellwords $args }
sub _done ($log = undef, $status = 0) {
print $log if defined $log;
exit $status;
}
sub _parse_clone_args ($text) {
my @clone_calls;
while ($text =~ /(openqa:\s+Clone\s+(https?:[^\s]+))(.*)/ig) {
push @clone_calls, [$2, _split_and_filter_arguments $3] if index($2, $expected_url) == 0;
}
return \@clone_calls if @clone_calls;
print 'No test cloned; the PR description does not contain '; # uncoverable statement
print "a command like 'openqa: Clone $expected_url/tests/<JOB_ID>'.\n"; # uncoverable statement
_done;
}
sub _handle_cmd_error ($command, @args) {
if ($? == -1) { die "Failed to execute '$command @args': $!\n" }
elsif ($? & 127) { die sprintf("'$command' received signal %d\n", $? & 127) }
elsif ($? >> 8) { die sprintf("'$command' exited with non-zero exist status %d\n", $? >> 8) }
}
sub _run_cmd ($command, @args) { system $command, @args; _handle_cmd_error $command, @args }
sub _run_cmd_capturing_output ($command, @args) {
open my $fh, '-|', $command, @args or die "Failed to execute '$command @args': $!\n";
my $output = do { local $/; <$fh> };
close $fh;
_handle_cmd_error $command, @args;
return $output;
}
sub _clone_job ($args, $vars) {
my @args = (@secrets, qw(--json-output --skip-chained-deps --within-instance), @$args, @$vars);
my $json = _run_cmd_capturing_output 'openqa-clone-job', @args;
return values %{decode_json($json)};
}
sub _restrict_to_team_members () {
return undef unless $restrict_orga || $restrict_team;
my $url = "$gh_api/orgs/$restrict_orga/teams/$restrict_team/memberships/$user";
my $res = $ua->get($url, \%gh_headers)->res;
my $body = $res->body // '';
my $state = eval { $res->json('/state') } // '';
_done "No test cloned; the user '$user' is not member of the team '$restrict_team' within '$restrict_orga'.\n$body"
if $state ne 'active';
}
sub _determine_gh_ref_and_clone_url () {
return undef if $gh_ref;
my $res = $ua->get($gh_pr_url, \%gh_headers)->res;
my $body = $res->body // '';
my $json = Mojo::JSON::Pointer->new(eval { $res->json } // {});
$gh_ref = $json->get('/head/sha');
$gh_clone_url = $json->get('/head/repo/clone_url');
$gh_statuses_url = $json->get('/statuses_url');
_done "No test cloned; unable to determine ref and clone URL of PR via '$gh_pr_url'.\n$body"
unless defined $gh_ref && defined $gh_clone_url;
$gh_repo = Mojo::URL->new($gh_clone_url)->path->leading_slash(0);
}
sub _update_status ($status) {
my $repo = $ENV{GITHUB_REPOSITORY};
my $run_id = $ENV{GITHUB_RUN_ID};
return undef unless $gh_statuses_url && $repo && $run_id;
my $target_url = "$gh_srv/$repo/actions/runs/$run_id";
my $ctx = "Run openQA test mentioned in comment '$comment_url' by '$user'";
my $description = $status eq 'pending' ? 'Monitoring cloned job(s)' : 'Job(s) have finished';
my %payload = (state => $status, target_url => $target_url, context => $ctx, description => $description);
my $res = $ua->post($gh_statuses_url, \%gh_headers, json => \%payload)->res;
my $body = $res->body;
print "Unable to update status on GitHub via '$gh_statuses_url'.\n$body\n" unless $res->is_success;
}
sub run () {
_restrict_to_team_members;
_determine_gh_ref_and_clone_url;
my $clone_args = _parse_clone_args($pr_body);
my @vars = ("BUILD=$gh_repo#$gh_ref", "_GROUP_ID=$group_id", "CASEDIR=$gh_clone_url#$gh_ref", @cloned_by_vars);
my @job_ids = map { _clone_job $_, \@vars } @$clone_args;
my @quoted_url_list = join(', ', map { "'$_->[0]'" } @$clone_args);
my @job_url_list = map { "- $expected_url/tests/$_\n" } @job_ids;
print "Cloned @quoted_url_list into:\n @job_url_list";
_update_status 'pending';
eval { _run_cmd 'openqa-cli', 'monitor', '--host', $expected_url, @secrets, @job_ids };
my $error = $@;
_update_status $error ? 'failure' : 'success';
_done $error, $error ? 1 : 0;
}
run unless caller;