File Coverage

File:lib/CheckSpelling/Apply.pm
Coverage:4.6%

linestmtbrancondsubtimecode
1package CheckSpelling::Apply;
2sub tear_here {
3
0
  my ($exit) = @_;
4
0
  our $exited;
5
0
  return if defined $exited;
6
0
  print STDERR "\n<<<TEAR HERE<<<exit: $exit\n";
7
0
  print STDOUT "\n<<<TEAR HERE<<<exit: $exit\n";
8
0
  $exited = $exit;
9}
10#!/usr/bin/env perl
11":" || q@<<"=END_OF_PERL"@;
12
13
1
1
1
114480
2
33
use Symbol 'gensym';
14
1
1
1
156
1219
25
use IPC::Open3;
15
1
1
1
3
1
24
use File::Basename qw(dirname);
16
1
1
1
2
0
18
use File::Path qw(make_path);
17
1
1
1
154
294
30
use File::Spec::Functions qw(catfile path);
18
1
1
1
2
1
20
use File::Temp qw/ tempfile tempdir /;
19
1
1
1
2
0
18
use JSON::PP;
20
1
1
1
4
1
2460
use warnings;
21
22my @safe_path = qw(
23    /opt/homebrew/bin
24    /opt/homebrew/sbin
25    /usr/local/bin
26    /usr/bin
27    /bin
28    /usr/sbin
29    /sbin
30);
31
32my $bin = glob("~/bin");
33push @safe_path, $bin if -d $bin;
34
35my $ua = 'check-spelling-agent/0.0.4';
36
37$ENV{'PATH'} = join ':', @safe_path unless defined $ENV{SYSTEMROOT};
38
39sub check_exists_command {
40
0
    my ($program) = @_;
41
42
0
    my @path = path;
43
0
    my @pathext = ('');
44
45
0
    if ($^O eq 'MSWin32') {
46
0
0
        push @pathext, map { lc } split /;/, $ENV{PATHEXT};
47    }
48
49
0
    for my $dir (@path) {
50
0
        for my $suffix (@pathext) {
51
0
            my $f = catfile $dir, "$program$suffix";
52
0
            return $f if -x $f;
53        }
54    }
55}
56
57sub needs_command_because {
58
0
    my ($program, $reason) = @_;
59
0
    return if check_exists_command($program);
60
0
    die 'Please install `'.$program.'` - it is needed to '.$reason;
61}
62
63sub check_basic_tools {
64
0
    needs_command_because('git', 'interact with git repositories');
65
0
    needs_command_because('curl', 'download other tools');
66
0
    needs_command_because('gh', 'interact with github');
67    #needs_command_because('magic-magic', 'debugging');
68}
69
70sub get_token {
71
0
    our $token;
72
0
    return $token if defined $token && $token ne '';
73
0
    $token = $ENV{'GH_TOKEN'} || $ENV{'GITHUB_TOKEN'};
74
0
    return $token if defined $token && $token ne '';
75
0
    $token = `gh auth token`;
76
0
    chomp $token;
77
0
    return $token;
78};
79
80sub download_with_curl {
81
0
    my ($url, $dest, $flags) = @_;
82
0
    $flags = '-fsL' unless defined $flags;
83
0
    system('curl',
84        '--connect-timeout', 3,
85        '-A', $ua,
86        $flags,
87        '-o', $dest,
88        $url
89    );
90}
91
92sub tempfile_name {
93
0
    my ($fh, $filename) = tempfile();
94
0
    close $fh;
95
0
    return $filename;
96}
97
98sub strip_comments {
99
0
    my ($file) = @_;
100
0
    my ($fh, $filename) = tempfile();
101
0
    open INPUT, '<', $file;
102
0
    while (<INPUT>) {
103
0
        next if /^\s*(?:#.*)/;
104
0
        print $fh $_;
105    }
106
0
    close INPUT;
107
0
    close $fh;
108
0
    return $filename;
109}
110
111sub capture_system {
112
0
    my @args = @_;
113
0
    my $pid = open3(my $child_in, my $child_out, my $child_err = gensym, @args);
114
0
    my (@err, @out);
115
0
    while (my $output = <$child_out>) {
116
0
        push @out, $output;
117    }
118
0
    while (my $error = <$child_err>) {
119
0
        push @err, $error;
120    }
121
0
    waitpid( $pid, 0 );
122
0
    my $child_exit_status = $?;
123
0
    my $output_joined = join '', @out;
124
0
    my $error_joined = join '', @err;
125
0
    return ($output_joined, $error_joined, $child_exit_status);
126}
127
128sub capture_merged_system {
129
0
    my ($output_joined, $error_joined, $child_exit_status) = capture_system(@_);
130
0
    my $joiner = ($output_joined ne '') ? "\n" : '';
131
0
    return ($output_joined.$joiner.$error_joined, $child_exit_status);
132}
133
134sub compare_files {
135
0
    my ($one, $two) = @_;
136
0
    my $one_stripped = strip_comments($one);
137
0
    my $two_stripped = strip_comments($two);
138
0
    my $exit_code;
139
0
    (undef, undef, $exit_code) = capture_system(
140            'diff',
141            '-qwB',
142            $one_stripped, $two_stripped
143        );
144
0
    if ($? == -1) {
145
0
        print "could not compare '$one' and '$two': $!\n";
146
0
        return 0;
147    }
148
0
    if ($? & 127) {
149
0
        printf "child died with signal %d, %s core dump\n",
150        ($? & 127),  ($? & 128) ? 'with' : 'without';
151
0
        return 0;
152    }
153
0
    return 0 if $? == 0;
154
0
    return 1;
155}
156
157my $bash_script=q{
158=END_OF_PERL@
159# bash
160set -e
161if [ "$OUTPUT" = "$ERROR" ]; then
162    ("$@" 2>&1) > "$OUTPUT"
163else
164    "$@" > "$OUTPUT" 2> "$ERROR"
165fi
166exit
167};
168
169sub check_current_script {
170
0
    if ("$0" eq '-') {
171
0
        my ($bash_script) = @_;
172
0
        my $fh;
173
0
        ($fh, $0) = tempfile();
174
0
        $bash_script =~ s/^=.*\@$//m;
175
0
        print $fh $bash_script;
176
0
        close $fh;
177
0
        return;
178    }
179
0
    my $filename = tempfile_name();
180
0
    my $source = 'https://raw.githubusercontent.com/check-spelling/check-spelling/prerelease/apply.pl';
181
0
    download_with_curl($source, $filename);
182
0
    if ($? == 0) {
183
0
        if (compare_files($filename, $0)) {
184
0
            print "Current apply script differs from '$source' (locally downloaded to `$filename`). You may wish to upgrade.\n";
185        }
186    }
187}
188
189sub die_with_message {
190
0
    our $program;
191
0
    my ($gh_err_text) = @_;
192
0
    if ($gh_err_text =~ /error connecting to / && $gh_err_text =~ /check your internet connection/) {
193
0
        print "$program: Internet access may be limited. Check your connection (this often happens with lousy cable internet service providers where their CG-NAT or whatever strands the modem).\n\n$gh_err_text";
194
0
0
        tear_here(5); return -1000;;
195    }
196
0
    if ($gh_err_text =~ /proxyconnect tcp:.*connect: connection refused/) {
197
0
        print "$program: Proxy is not accepting connections.\n";
198
0
        for my $proxy (qw(http_proxy HTTP_PROXY https_proxy HTTPS_PROXY)) {
199
0
            if (defined $ENV{$proxy}) {
200
0
                print "  $proxy: '$ENV{$proxy}'\n";
201            }
202        }
203
0
        print "\n$gh_err_text";
204
0
0
        tear_here(6); return -1000;;
205    }
206
0
    if ($gh_err_text =~ /dial unix .*: connect: .*/) {
207
0
        print "$program: Unix http socket is not working.\n";
208
0
        my $gh_http_unix_socket = `gh config get http_unix_socket`;
209
0
        print "  http_unix_socket: $gh_http_unix_socket\n";
210
0
        print "\n$gh_err_text";
211
0
0
        tear_here(7); return -1000;;
212    }
213}
214
215sub gh_is_happy_internal {
216
0
    my ($output, $exit_code) = capture_merged_system(qw(gh api /installation/repositories));
217
0
    return ($exit_code, $output) if $exit_code == 0;
218
0
    ($output, $exit_code) = capture_merged_system(qw(gh api /user));
219
0
    return ($exit_code, $output);
220}
221
222sub gh_is_happy {
223
0
    my ($program) = @_;
224
0
    my ($gh_auth_status, $gh_status_lines) = gh_is_happy_internal();
225
0
    return 1 if $gh_auth_status == 0;
226
0
    die_with_message($gh_status_lines);
227
228
0
    my @problematic_env_variables;
229
0
    for my $variable (qw(GH_TOKEN GITHUB_TOKEN GITHUB_ACTIONS CI)) {
230
0
        if (defined $ENV{$variable}) {
231
0
            delete $ENV{$variable};
232
0
            push @problematic_env_variables, $variable;
233
0
            ($gh_auth_status, $gh_status_lines) = gh_is_happy_internal();
234
0
            if ($gh_auth_status == 0) {
235
0
                print STDERR "$0: gh program did not like these environment variables: ".join(', ', @problematic_env_variables)." -- consider unsetting them.\n";
236
0
                return 1;
237            }
238        }
239    }
240
241
0
    print $gh_status_lines;
242
0
    return 0;
243}
244
245sub tools_are_ready {
246
0
    my ($program) = @_;
247
0
    unless (gh_is_happy($program)) {
248
0
        $! = 1;
249
0
        my $or_gh_token = (defined $ENV{CI} && $ENV{CI}) ? ' or set the GH_TOKEN environment variable' : '';
250
0
        die "$program requires a happy gh, please try 'gh auth login'$or_gh_token\n";
251    }
252}
253
254sub maybe_unlink {
255
0
    unlink($_[0]) if $_[0];
256}
257
258sub run_pipe {
259
0
    my @args = @_;
260
0
    my ($out, undef, $exit_code) = capture_system(@args);
261
0
    return $out;
262}
263
264sub unzip_pipe {
265
0
    my ($artifact, $file) = @_;
266
0
    return run_pipe(
267        'unzip',
268        '-p', $artifact,
269        $file
270    );
271}
272
273sub retrieve_spell_check_this {
274
0
    my ($artifact, $config_ref) = @_;
275
0
    my $spell_check_this_config = unzip_pipe($artifact, 'spell_check_this.json');
276
0
    return unless $spell_check_this_config =~ /\{.*\}/s;
277
0
    my %config;
278
0
0
0
    eval { %config = %{decode_json $spell_check_this_config}; } || die "decode_json failed in retrieve_spell_check_this with '$spell_check_this_config'";
279
0
    my ($repo, $branch, $destination, $path) = ($config{url}, $config{branch}, $config{config}, $config{path});
280
0
    my $spell_check_this_dir = tempdir();
281
0
    my $exit_code;
282
0
    (undef, undef, $exit_code) = capture_system(
283            'git', 'clone',
284            '--depth', '1',
285            '--no-tags',
286            $repo,
287            '--branch', $branch,
288            $spell_check_this_dir
289        );
290
0
    if ($?) {
291
0
        die "git clone $repo#$branch failed";
292    }
293
294
0
    make_path($destination);
295
0
    system('cp', '-i', '-R', glob("$spell_check_this_dir/$path/*"), $destination);
296
0
    system('git', 'add', '-f', $destination);
297}
298
299sub case_biased {
300
0
    lc($a)."-".$a cmp lc($b)."-".$b;
301}
302
303sub add_to_excludes {
304
0
    my ($artifact, $config_ref) = @_;
305
0
0
    my %config = %{$config_ref};
306
0
    my $excludes = $config{"excludes_file"};
307
0
    my $should_exclude_patterns = unzip_pipe($artifact, 'should_exclude.patterns');
308
0
    unless ($should_exclude_patterns =~ /\w/) {
309
0
        $should_exclude_patterns = unzip_pipe($artifact, 'should_exclude.txt');
310
0
        return unless $should_exclude_patterns =~ /\w/;
311
0
        $should_exclude_patterns =~ s{^(.*)}{^\\Q$1\\E\$}gm;
312    }
313
0
    my $need_to_add_excludes;
314    my %excludes;
315
0
    if (-f $excludes) {
316
0
        open EXCLUDES, '<', $excludes;
317
0
        while (<EXCLUDES>) {
318
0
            chomp;
319
0
            next unless /./;
320
0
            $excludes{$_."\n"} = 1;
321        }
322
0
        close EXCLUDES;
323    } else {
324
0
        $need_to_add_excludes = 1;
325    }
326
0
    for $pattern (split /\n/, $should_exclude_patterns) {
327
0
        next unless $pattern =~ /./;
328
0
        $excludes{$pattern."\n"} = 1;
329    }
330
0
    open EXCLUDES, '>', $excludes;
331
0
    print EXCLUDES join "", sort case_biased keys %excludes;
332
0
    close EXCLUDES;
333
0
    system('git', 'add', '--', $excludes) if $need_to_add_excludes;
334}
335
336sub remove_stale {
337
0
    my ($artifact, $config_ref) = @_;
338
0
    my @stale = split /\s+/s, unzip_pipe($artifact, 'remove_words.txt');
339
0
    return unless @stale;
340
0
0
    my %config = %{$config_ref};
341
0
0
    my @expect_files = @{$config{"expect_files"}};
342    @expect_files = grep {
343
0
0
        print STDERR "Could not find $_\n" unless -f $_;
344
0
        -f $_;
345    } @expect_files;
346
0
    unless (@expect_files) {
347
0
        die "Could not find any of the processed expect files, are you on the wrong branch?";
348    }
349
350
0
    my $re = join "|", @stale;
351
0
    for my $file (@expect_files) {
352
0
        open INPUT, '<', $file;
353
0
        my @keep;
354
0
        while (<INPUT>) {
355
0
            next if /^(?:$re)(?:(?:\r|\n)*$|[# ].*)/;
356
0
            push @keep, $_;
357        }
358
0
        close INPUT;
359
360
0
        open OUTPUT, '>', $file;
361
0
        print OUTPUT join '', @keep;
362
0
        close OUTPUT;
363    };
364}
365
366sub add_expect {
367
0
    my ($artifact, $config_ref) = @_;
368
0
    my @add = split /\s+/s, (unzip_pipe($artifact, 'tokens.txt'));
369
0
    return unless @add;
370
0
0
    my %config = %{$config_ref};
371
0
    my $new_expect_file = $config{"new_expect_file"};
372
0
    my @words;
373
0
    make_path (dirname($new_expect_file));
374
0
    if (-s $new_expect_file) {
375
0
        open FILE, q{<}, $new_expect_file;
376
0
        local $/ = undef;
377
0
        @words = split /\s+/, <FILE>;
378
0
        close FILE;
379    }
380
0
    my %items;
381
0
    @items{@words} = @words x (1);
382
0
    @items{@add} = @add x (1);
383
0
    @words = sort case_biased keys %items;
384
0
    open FILE, q{>}, $new_expect_file;
385
0
    for my $word (@words) {
386
0
        print FILE "$word\n" if $word =~ /\S/;
387    };
388
0
    close FILE;
389
0
    system("git", "add", $new_expect_file);
390}
391
392sub get_artifact_metadata {
393
0
    my ($url) = @_;
394
0
    my $json_file = tempfile_name();
395
0
    my ($curl_stdout, $curl_stderr, $curl_result);
396
0
    my @curl_args = (
397        'curl',
398        $url,
399        '-A',
400        $ua,
401        '-s',
402        '--fail-with-body',
403    );
404
0
    my $gh_token = get_token();
405
0
    push @curl_args, '-u', "token:$gh_token" if defined $gh_token;
406
0
    push @curl_args, (
407        '-o',
408        $json_file
409    );
410
0
    ($curl_stdout, $curl_stderr, $curl_result) = capture_system(
411        @curl_args
412    );
413
0
    unless ($curl_result == 0) {
414
0
        if ($curl_stdout eq '') {
415
0
            local $/;
416
0
            open my $error_fh, '<', $json_file;
417
0
            $curl_stdout = <$error_fh>;
418
0
            close $error_fh;
419        }
420        return (
421
0
            out    => $curl_stdout,
422            err    => $curl_stderr,
423            result => $curl_result,
424        );
425    }
426
0
    my $link;
427
0
    open my $json_file_fh, '<', $json_file;
428
0
    my ($id, $download_url, $count);
429    {
430
0
0
        local $/;
431
0
        my $content = <$json_file_fh>;
432
0
        my $json = decode_json $content;
433
0
        my $artifact = $json->{'artifacts'}->[0];
434
0
        $id = $artifact->{'id'};
435
0
        $download_url = $artifact->{'archive_download_url'};
436
0
        $count = $json->{'total_count'};
437    }
438
0
    close $json_file_fh;
439
0
    if ($count == 0) {
440        return (
441
0
            out => '',
442            err => 'no artifact matches any of the names or patterns provided',
443            result => (3 << 8),
444        );
445    }
446    return (
447
0
        id       => $id,
448        download => $download_url,
449        count    => $count,
450    );
451}
452
453sub get_latest_artifact_metadata {
454
0
    my ($artifact_dir, $repo, $run, $artifact_name) = @_;
455
0
    my $page = 1;
456
0
    my $url = "$ENV{GITHUB_API_URL}/repos/$repo/actions/runs/$run/artifacts?name=$artifact_name&per_page=1&page=";
457
0
    my %first = get_artifact_metadata($url.$page);
458
0
    $page = $first{'count'};
459
0
    if (defined $page) {
460
0
        my %second = get_artifact_metadata($url.$page);
461
0
        my ($id_1, $id_2) = ($first{'id'}, $second{'id'});
462
0
        if (defined $id_1 && defined $id_2) {
463
0
            if ($id_2 > $id_1) {
464                return (
465
0
                    download => $second{'download'},
466                );
467            }
468        }
469    }
470
0
    my $download = $first{'download'};
471
0
    if (defined $download) {
472        return (
473
0
            download => $download,
474        );
475    }
476
0
    return %first;
477}
478
479sub download_latest_artifact {
480
0
    my %maybe_download = get_latest_artifact_metadata(@_);
481
0
    my $download = $maybe_download{'download'};
482
0
    my $zip_file = tempfile_name();
483
0
    if (defined $download) {
484
0
        my @curl_args = (
485            'curl',
486            $download,
487            '-L',
488            '-A',
489            $ua,
490            '-s',
491            '--fail-with-body',
492        );
493
0
        my $gh_token = get_token();
494
0
        push @curl_args, '-u', "token:$gh_token" if defined $gh_token;
495
0
        push @curl_args, (
496            '-o',
497            $zip_file
498        );
499
0
        ($curl_stdout, $curl_stderr, $curl_result) = capture_system(
500            @curl_args
501        );
502
0
        if ($curl_result != 0) {
503
0
            if ($curl_stdout eq '') {
504
0
                local $/;
505
0
                open my $error_fh, '<', $zip_file;
506
0
                $curl_stdout = <$error_fh>;
507
0
                close $error_fh;
508            }
509
0
            return ("$curl_stdout\n$curl_stderr", $curl_result);
510        }
511
0
        my ($artifact_dir, $repo, $run, $artifact_name) = @_;
512
0
        ($out, $err, $result) = capture_system(
513            'unzip',
514            '-q',
515            $zip_file,
516            '-d',
517            $artifact_dir,
518            );
519
0
        return ("$out\n$err", $result);
520    }
521
0
    my ($out, $err, $result) = ($maybe_download{'out'}, $maybe_download{'err'}, $maybe_download{'result'});
522
0
    return ("$out\n$err", $result);
523}
524
525sub get_artifacts {
526
0
    my ($repo, $run, $suffix) = @_;
527
0
    our $program;
528
0
    my $artifact_dir = tempdir(CLEANUP => 1);
529
0
    my $gh_err_text;
530
0
    my $artifact_name = 'check-spelling-comment';
531
0
    if ($suffix) {
532
0
        $artifact_name .= "-$suffix";
533    }
534
0
    my $retries_remaining = 3;
535
0
    while ($retries_remaining-- > 0) {
536
0
        ($gh_err_text, $ret) = download_latest_artifact(
537            $artifact_dir,
538            $repo,
539            $run,
540            $artifact_name
541        );
542
0
        return glob("$artifact_dir/artifact*.zip") unless ($ret >> 8);
543
544
0
        die_with_message($gh_err_text);
545
0
        if ($gh_err_text =~ /no valid artifacts found to download|"Artifact has expired"/) {
546
0
            my $expired_json = run_pipe(
547                'gh', 'api',
548                "/repos/$repo/actions/runs/$run/artifacts",
549                '-q',
550                '.artifacts.[]|select(.name=="'.$artifact_name.'")|.expired'
551            );
552
0
            if ($expired_json ne '') {
553
0
                chomp $expired_json;
554
0
                my $expired;
555
0
0
                eval { $expired = decode_json $expired_json } || die "decode_json failed in update_repository with '$expired_json'";
556
0
                if ($expired) {
557
0
                    print "$program: GitHub Run Artifact expired. You will need to trigger a new run.\n";
558
0
0
                    tear_here(1); return -1000;;
559                }
560            }
561
0
            print "$program: GitHub Run may not have completed. If so, please wait for it to finish and try again.\n";
562
0
0
            tear_here(2); return -1000;;
563        }
564
0
        if ($gh_err_text =~ /no artifact matches any of the names or patterns provided/) {
565
0
            $github_server_url = $ENV{GITHUB_SERVER_URL} || '';
566
0
            my $run_link;
567
0
            if ($github_server_url) {
568
0
                $run_link = "[$run]($github_server_url/$repo/actions/runs/$run)";
569            } else {
570
0
                $run_link = "$run";
571            }
572
0
            print "$program: The referenced repository ($repo) run ($run_link) does not have a corresponding artifact ($artifact_name). If it was deleted, that's unfortunate. Consider pushing a change to the branch to trigger a new run?\n";
573
0
            print "If you don't think anyone deleted the artifact, please file a bug to https://github.com/check-spelling/check-spelling/issues/new including as much information about how you triggered this error as possible.\n";
574
0
0
            tear_here(3); return -1000;;
575        }
576
0
        if ($gh_err_text =~ /HTTP 404: Not Found|"status":"404"/) {
577
0
            print "$program: The referenced repository ($repo) may not exist, perhaps you do not have permission to see it. If the repository is hosted by GitHub Enterprise, check-spelling does not know how to integrate with it.\n";
578
0
0
            tear_here(8); return -1000;;
579        }
580
0
        if ($gh_err_text =~ /HTTP 403: API rate limit exceeded for .*?./) {
581        } elsif ($gh_err_text =~ m{dial tcp \S+:\d+: i/o timeout$}) {
582
0
            if ($retries_remaining <= 0) {
583
0
                print "$program: Timeout connecting to GitHub. This is probably caused by an outage of sorts.\nCheck https://www.githubstatus.com/history\nTry again later.";
584
0
0
                tear_here(9); return -1000;;
585            }
586        } else {
587
0
            print "$program: Unknown error, please check the list of known issues https://github.com/check-spelling/check-spelling/issues?q=is%3Aissue%20apply.pl and file a bug to https://github.com/check-spelling/check-spelling/issues/new?title=%60apply.pl%60%20scenario&body=Please%20provide%20details+preferably%20including%20a%20link%20to%20a%20workflow%20run,%20the%20configuration%20of%20the%20repository,%20and%20anything%20else%20you%20may%20know%20about%20the%20problem%2e\n";
588
0
            print $gh_err_text;
589
0
0
            tear_here(4); return -1000;;
590        }
591
0
        my $request_id = $1 if ($gh_err_text =~ /\brequest ID\s+(\S+)/);
592
0
        my $timestamp = $1 if ($gh_err_text =~ /\btimestamp\s+(.*? UTC)/);
593
0
        my $has_gh_token = defined $ENV{GH_TOKEN} || defined $ENV{GITHUB_TOKEN};
594
0
        my $meta_url = 'https://api.github.com/meta';
595
0
        while (1) {
596
0
            my @curl_args = qw(curl);
597
0
            unless ($has_gh_token) {
598
0
                my $gh_token = get_token();
599
0
                push @curl_args, '-u', "token:$gh_token" if defined $gh_token;
600            }
601
0
            push @curl_args, '-I', $meta_url;
602
0
            my ($curl_stdout, $curl_stderr, $curl_result);
603
0
            ($curl_stdout, $curl_stderr, $curl_result) = capture_system(@curl_args);
604
0
            my $delay = 1;
605
0
            if ($curl_stdout =~ m{^HTTP/\S+\s+200}) {
606
0
                if ($curl_stdout =~ m{^x-ratelimit-remaining:\s+(\d+)$}m) {
607
0
                    my $ratelimit_remaining = $1;
608
0
                    last if ($ratelimit_remaining > 10);
609
610
0
                    $delay = 5;
611
0
                    print STDERR "Sleeping for $delay seconds because $ratelimit_remaining is close to 0\n";
612                } else {
613
0
                    print STDERR "Couldn't find x-ratelimit-remaining, will sleep for $delay\n";
614                }
615            } elsif ($curl_stdout =~ m{^HTTP/\S+\s+403}) {
616
0
                if ($curl_stdout =~ /^retry-after:\s+(\d+)/m) {
617
0
                    $delay = $1;
618
0
                    print STDERR "Sleeping for $delay seconds (presumably due to API rate limit)\n";
619                } else {
620
0
                    print STDERR "Couldn't find retry-after, will sleep for $delay\n";
621                }
622            } else {
623
0
                my $response = $1 if $curl_stdout =~ m{^(HTTP/\S+)};
624
0
                print STDERR "Unexpected response ($response) from $meta_url; sleeping for $delay\n";
625            }
626
0
            sleep $delay;
627        }
628    }
629}
630
631sub update_repository {
632
0
    my ($artifact) = @_;
633
0
    die if $artifact =~ /'/;
634
0
    our $program;
635
0
    my $apply = unzip_pipe($artifact, 'apply.json');
636
0
    unless ($apply =~ /\{.*\}/s) {
637
0
        print STDERR "$program: Could not retrieve valid apply.json from artifact\n";
638
0
        $apply = '{
639            "expect_files": [".github/actions/spelling/expect.txt"],
640            "new_expect_file": ".github/actions/spelling/expect.txt",
641            "excludes_file": ".github/actions/spelling/excludes.txt",
642            "spelling_config": ".github/actions/spelling"
643        }';
644    }
645
0
    my $config_ref;
646
0
0
    eval { $config_ref = decode_json($apply); } ||
647        die "$program: decode_json failed in update_repository with '$apply'";
648
649
0
    my $git_repo_root = run_pipe('git', 'rev-parse', '--show-toplevel');
650
0
    chomp $git_repo_root;
651
0
    die "$program: Could not find git repo root..." unless $git_repo_root =~ /\w/;
652
0
    chdir $git_repo_root;
653
654
0
    retrieve_spell_check_this($artifact, $config_ref);
655
0
    remove_stale($artifact, $config_ref);
656
0
    add_expect($artifact, $config_ref);
657
0
    add_to_excludes($artifact, $config_ref);
658
0
    system('git', 'add', '-u', '--', $config_ref->{'spelling_config'});
659}
660
661sub main {
662
0
    our $program;
663
0
    my ($bash_script, $first, $run);
664
0
    ($program, $bash_script, $first, $run) = @_;
665
0
    my $syntax = "$program <RUN_URL | OWNER/REPO RUN | ARTIFACT.zip>";
666    # Stages
667    # - 1 check for tools basic
668
0
    check_basic_tools();
669    # - 2 check for current
670    # -> 1 download the latest version to a temp file
671    # -> 2. parse current and latest (stripping comments) and compare (whitespace insensitively)
672    # -> 3. offer to update if the latest version is different
673
0
    check_current_script($bash_script);
674    # - 4 parse arguments
675
0
    die $syntax unless defined $first;
676
0
    $ENV{'GITHUB_API_URL'} ||= 'https://api.github.com';
677
0
    my $repo;
678    my @artifacts;
679
0
    if (-s $first) {
680
0
        my $artifact = $first;
681
0
        open my $artifact_reader, '-|', 'unzip', '-l', $artifact;
682
0
        my ($has_artifact, $only_file) = (0, 0);
683
0
        while (my $line = <$artifact_reader>) {
684
0
            chomp $line;
685
0
            if ($line =~ /\s+artifact\.zip$/) {
686
0
                $has_artifact = 1;
687
0
                next;
688            }
689
0
            if ($line =~ /\s+1 file$/) {
690
0
                $only_file = 1;
691
0
                next;
692            }
693
0
            $only_file = 0 if $only_file;
694        }
695
0
        close $artifact_reader;
696
0
        if ($has_artifact && $only_file) {
697
0
            my $artifact_dir = tempdir(CLEANUP => 1);
698
0
            my ($fh, $gh_err) = tempfile();
699
0
            close $fh;
700
0
            system('unzip', '-q', '-d', $artifact_dir, $artifact, 'artifact.zip');
701
0
            @artifacts = ("$artifact_dir/artifact.zip");
702        } else {
703
0
            @artifacts = ($artifact);
704        }
705    } else {
706
0
        my $suffix;
707
0
        if ($first =~ m{^\s*https://.*/([^/]+/[^/]+)/actions/runs/(\d+)(?:/attempts/\d+|)(?:#(\S+)|)\s*$}) {
708
0
            ($repo, $run, $suffix) = ($1, $2, $3);
709        } else {
710
0
            $repo = $first;
711        }
712
0
        die $syntax unless defined $repo && defined $run;
713        # - 3 check for tool readiness (is `gh` working)
714
0
        tools_are_ready($program);
715
0
        @artifacts = get_artifacts($repo, $run, $suffix);
716    }
717
718    # - 5 do work
719
0
    for my $artifact (@artifacts) {
720
0
        update_repository($artifact);
721    }
722}
723