Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package openQA for openSUSE:Factory checked in at 2026-03-27 16:50:10 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/openQA (Old) and /work/SRC/openSUSE:Factory/.openQA.new.8177 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "openQA" Fri Mar 27 16:50:10 2026 rev:829 rq:1343060 version:5.1774510397.efec10a7 Changes: -------- --- /work/SRC/openSUSE:Factory/openQA/openQA.changes 2026-03-27 06:45:30.436072607 +0100 +++ /work/SRC/openSUSE:Factory/.openQA.new.8177/openQA.changes 2026-03-27 16:52:17.826440000 +0100 @@ -2 +2 @@ -Wed Mar 25 23:07:42 UTC 2026 - [email protected] +Thu Mar 26 08:07:45 UTC 2026 - [email protected] @@ -4 +4 @@ -- Update to version 5.1774473623.838c74ef: +- Update to version 5.1774510397.efec10a7: @@ -5,0 +6,3 @@ + * docs(userguide): fix position of job_templates unique id + * docs(amqp): cover usage examples of amqp and slack integration + * feat: Allow resuming asset downloads with `openqa-clone-job` Old: ---- openQA-5.1774473623.838c74ef.obscpio New: ---- openQA-5.1774510397.efec10a7.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ openQA-client-test.spec ++++++ --- /var/tmp/diff_new_pack.rmEv1r/_old 2026-03-27 16:52:18.982488337 +0100 +++ /var/tmp/diff_new_pack.rmEv1r/_new 2026-03-27 16:52:18.982488337 +0100 @@ -18,7 +18,7 @@ %define short_name openQA-client Name: %{short_name}-test -Version: 5.1774473623.838c74ef +Version: 5.1774510397.efec10a7 Release: 0 Summary: Test package for %{short_name} License: GPL-2.0-or-later ++++++ openQA-devel-test.spec ++++++ --- /var/tmp/diff_new_pack.rmEv1r/_old 2026-03-27 16:52:19.014489675 +0100 +++ /var/tmp/diff_new_pack.rmEv1r/_new 2026-03-27 16:52:19.018489842 +0100 @@ -18,7 +18,7 @@ %define short_name openQA-devel Name: %{short_name}-test -Version: 5.1774473623.838c74ef +Version: 5.1774510397.efec10a7 Release: 0 Summary: Test package for %{short_name} License: GPL-2.0-or-later ++++++ openQA-test.spec ++++++ --- /var/tmp/diff_new_pack.rmEv1r/_old 2026-03-27 16:52:19.046491013 +0100 +++ /var/tmp/diff_new_pack.rmEv1r/_new 2026-03-27 16:52:19.046491013 +0100 @@ -18,7 +18,7 @@ %define short_name openQA Name: %{short_name}-test -Version: 5.1774473623.838c74ef +Version: 5.1774510397.efec10a7 Release: 0 Summary: Test package for openQA License: GPL-2.0-or-later ++++++ openQA-worker-test.spec ++++++ --- /var/tmp/diff_new_pack.rmEv1r/_old 2026-03-27 16:52:19.074492183 +0100 +++ /var/tmp/diff_new_pack.rmEv1r/_new 2026-03-27 16:52:19.074492183 +0100 @@ -18,7 +18,7 @@ %define short_name openQA-worker Name: %{short_name}-test -Version: 5.1774473623.838c74ef +Version: 5.1774510397.efec10a7 Release: 0 Summary: Test package for %{short_name} License: GPL-2.0-or-later ++++++ openQA.spec ++++++ --- /var/tmp/diff_new_pack.rmEv1r/_old 2026-03-27 16:52:19.110493689 +0100 +++ /var/tmp/diff_new_pack.rmEv1r/_new 2026-03-27 16:52:19.110493689 +0100 @@ -99,7 +99,7 @@ %define devel_requires %devel_no_selenium_requires chromedriver Name: openQA -Version: 5.1774473623.838c74ef +Version: 5.1774510397.efec10a7 Release: 0 Summary: The openQA web-frontend, scheduler and tools License: GPL-2.0-or-later ++++++ openQA-5.1774473623.838c74ef.obscpio -> openQA-5.1774510397.efec10a7.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1774473623.838c74ef/docs/Installing.asciidoc new/openQA-5.1774510397.efec10a7/docs/Installing.asciidoc --- old/openQA-5.1774473623.838c74ef/docs/Installing.asciidoc 2026-03-25 22:20:23.000000000 +0100 +++ new/openQA-5.1774510397.efec10a7/docs/Installing.asciidoc 2026-03-26 08:33:17.000000000 +0100 @@ -1074,8 +1074,9 @@ The messages consist of a topic and a body. The body contains json encoded info about the event. See https://github.com/openSUSE/suse_msg/blob/master/amqp_infra.md[amqp_infra.md] -for more info about the server and the message topic format. -There you will find instructions how to configure the AMQP server as well. +for more info about the server and the message topic format or take a look at +<<UsersGuide.asciidoc#amqp_events,Consuming AMQP events from openQA>> to find +detailed instructions how to configure the AMQP server. To let openQA send messages to an AMQP message bus, first make sure that the `perl-Mojo-RabbitMQ-Client` RPM is installed. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1774473623.838c74ef/docs/UsersGuide.asciidoc new/openQA-5.1774510397.efec10a7/docs/UsersGuide.asciidoc --- old/openQA-5.1774473623.838c74ef/docs/UsersGuide.asciidoc 2026-03-25 22:20:23.000000000 +0100 +++ new/openQA-5.1774510397.efec10a7/docs/UsersGuide.asciidoc 2026-03-26 08:33:17.000000000 +0100 @@ -13,9 +13,8 @@ <<Installing.asciidoc#installing,Installation Guide>> first to understand the structure of components as well as the configuration of an installed instance. - -== Using job templates to automate jobs creation [id="job_templates"] +== Using job templates to automate jobs creation === The problem @@ -1698,6 +1697,58 @@ systemctl mask openqa-enqueue-asset-cleanup.timer ---- +[id="amqp_events"] +== Consuming AMQP events from openQA +The message topic follows the format `SCOPE.APPLICATION.OBJECT.ACTION`, for +example `opensuse.openqa.job.done` or `opensuse.openqa.comment.create`. +The `topic_prefix` setting controls the `SCOPE` part; if left empty the topic +starts with `openqa.` directly. +Consumers can use `#` to match any number of words or `*` to match exactly one +word. `opensuse.openqa.#` subscribes to all openQA events, for instance. + +Here are the events triggered and published by openQA: + +1. `openqa.job.create` when a job is created +2. `openqa.job.delete` when a job is deleted +3. `openqa.job.cancel` when a job is cancelled +4. `openqa.job.restart` when a job is restarted or duplicated +5. `openqa.job.update_result` when a job result is updated +6. `openqa.job.done` when a job finishes + +Job event bodies include the job settings (e.g. `BUILD`, `TEST`, `ARCH`, +`MACHINE`, `FLAVOR`, asset fields like `ISO` or `HDD_1`) plus `id`, `group_id`, +and `remaining` (number of pending jobs for the same build). Finished jobs +additionally include `result`, `reason`, `newbuild`, `failedmodules`, `bugref`, +and `bugurl` (only present when a bug reference exists). + +Example of a `*.openqa.job.done` message body (at the time of writing): + +[source,json] +-------------------------------------------------------------------------------- +{"ARCH":"x86_64","BUILD":"N.487.1","FLAVOR":"Staging-DVD","ISO":"openSUSE-Staging:N-Tumbleweed-DVD-x86_64-Build487.1-Media.iso","MACHINE":"64bit","TEST":"autoyast_mini","bugref":null,"failedmodules":[],"group_id":2,"id":5735489,"newbuild":null,"reason":null,"remaining":13,"result":"passed"} +-------------------------------------------------------------------------------- + +Event for comments are also published: + +1. `openqa.comment.create` +2. `openqa.comment.update` +3. `openqa.comment.delete` + +Comment event bodies include: `id`, `job_id`, `group_id`, `parent_group_id`, +`user`, `text`, `created`, `updated`. + +Example `*.openqa.comment.create` message body: + +[source,json] +-------------------------------------------------------------------------------- +{"created":"2026-03-11T14:35:23Z","group_id":null,"id":865726,"job_id":5735489,"parent_group_id":null,"text":"amqp sent","updated":"2026-03-11T14:35:23Z","user":"demo"} +-------------------------------------------------------------------------------- + +A real-world example of an AMQP consumer built on top of openQA events is +https://github.com/dirkmueller/slacky[slacky], a bot that monitors CI/build +pipelines and posts failure notifications to Slack. It subscribes to openQA job +events and tracks job results per build and job group. + == CLI interface Beside the `daemon` argument to run the actual web service the openQA startup script `/usr/share/openqa/script/openqa` supports further arguments. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1774473623.838c74ef/lib/OpenQA/Script/CloneJob.pm new/openQA-5.1774510397.efec10a7/lib/OpenQA/Script/CloneJob.pm --- old/openQA-5.1774473623.838c74ef/lib/OpenQA/Script/CloneJob.pm 2026-03-25 22:20:23.000000000 +0100 +++ new/openQA-5.1774510397.efec10a7/lib/OpenQA/Script/CloneJob.pm 2026-03-26 08:33:17.000000000 +0100 @@ -8,7 +8,6 @@ use Cpanel::JSON::XS; use Data::Dump 'pp'; use Exporter 'import'; -use LWP::UserAgent; use OpenQA::Client; use OpenQA::Jobs::Constants; use Mojo::File 'path'; @@ -36,6 +35,8 @@ _GROUP_ID => '_GROUP', }; +use constant CURL => $ENV{OPENQA_CLI_CURL_PATH} // 'curl'; + my $TEST_NAME = TEST_NAME_ALLOWED_CHARS; my $TEST_NAME_PLUS_MINUS = TEST_NAME_ALLOWED_CHARS_PLUS_MINUS; my $SETTINGS_REGEX = qr|([A-Z0-9_]+(\[\])?)(:([$TEST_NAME]+(?:[$TEST_NAME_PLUS_MINUS]+[$TEST_NAME])?))?(\+)?=(.*)|; @@ -148,10 +149,29 @@ die "The following assets are missing:\n - $relevant_missing_assets\n$note\n"; } +sub _format_cmd_error ($command) { + return "Failed to execute '$command': $!" if $? == -1; + return ($? & 127) + ? sprintf "'$command' received signal %d", $? & 127 + : sprintf "'$command' exited with non-zero exit status %d", $? >> 8; +} + +sub _run_cmd ($command, @args) { system $command, @args; return $? == 0 ? '' : _format_cmd_error($command) } + +sub mirror ($url_handler, $from, $dst) { + my @curl_args = @{$url_handler->{curl_args}}; + my $secrets = $url_handler->{secrets}; + my $headers = Mojo::Headers->new; + OpenQA::UserAgent::add_auth_headers($headers, $from, @$secrets) if $secrets; + for my $name (@{$headers->names}) { + push @curl_args, '-H', "$name: " . $_ for @{$headers->every_header($name)}; + } + _run_cmd CURL, @curl_args, qw(--continue-at - --output), $dst, $from; +} + sub clone_job_download_assets ($jobid, $job, $url_handler, $options) { my $parents = _get_chained_parents($job, $url_handler, $options); _check_for_missing_assets($job, $parents, $options); - my $ua = $url_handler->{ua}; my $remote_url = $url_handler->{remote_url}; for my $type (keys %{$job->{assets}}) { next if $type eq 'repo'; # we can't download repos @@ -170,18 +190,13 @@ $dst =~ s,.*/,,; my $dst_dir = path($options->{dir}, $type)->make_path; $dst = $dst_dir->child($dst)->to_string; + die "Cannot write $dst_dir\n" unless -w $dst_dir; my $from = $remote_url->clone; $from->path(sprintf '/tests/%d/asset/%s/%s', $jobid, $type, $file); - $from = $from->to_string; - - die "Cannot write $dst_dir\n" unless -w $dst_dir; - print STDERR "downloading\n$from\nto\n$dst\n"; - my $r = $ua->mirror($from, $dst); - unless ($r->is_success || $r->code == HTTP_NOT_MODIFIED) { - print STDERR "$jobid failed: $file, ", $r->status_line, "\n"; - die "Can't clone due to missing assets: ", $r->status_line, " \n" - unless $options->{'ignore-missing-assets'}; + if (my $error = mirror($url_handler, $from, $dst)) { + my $msg = "\nCloning aborted during asset download: $error\n"; + $options->{'ignore-missing-assets'} ? print STDERR $msg : die $msg; } # ensure the asset cleanup preserves the asset the configured amount of days starting from the time @@ -203,21 +218,18 @@ return ($host_url, $jobid); } -sub create_lwp_user_agent ($host, $options) { - my $ua = LWP::UserAgent->new; - $ua->timeout(10); - $ua->env_proxy; - $ua->show_progress(1) if $options->{'show-progress'}; - return $ua unless my $cfg = OpenQA::UserAgent::open_config_file($host); +sub make_curl_arguments ($options) { + my @args = ('--follow'); + push @args, '--no-progress-meter' unless $options->{'show-progress'}; + push @args, '--verbose' if $options->{verbose}; + return \@args; +} +sub read_secrets ($host) { + return undef unless my $cfg = OpenQA::UserAgent::open_config_file($host); my $apikey = ($cfg->val($host, 'key'))[-1]; my $apisecret = ($cfg->val($host, 'secret'))[-1]; - $ua->add_handler( - request_prepare => sub ($request, $ua, $handler) { - OpenQA::UserAgent::add_auth_headers($request, Mojo::URL->new($request->uri), $apikey, $apisecret); - }) if $apikey && $apisecret; - - return $ua; + return $apikey && $apisecret ? [$apikey, $apisecret] : undef; } sub create_url_handler ($options) { @@ -234,9 +246,14 @@ # configure user agents for the source host (usually a remote host) my $remote_url = OpenQA::Client::url_from_host($options->{from}); $remote_url->path('/api/v1/jobs'); - my $remote = OpenQA::Client->new(api => $remote_url->host); - my $ua = create_lwp_user_agent($remote_url->host, $options); - return {ua => $ua, local => $local, local_url => $local_url, remote => $remote, remote_url => $remote_url}; + return { + curl_args => make_curl_arguments($options), + secrets => read_secrets($remote_url->host), + local => $local, + local_url => $local_url, + remote => OpenQA::Client->new(api => $remote_url->host), + remote_url => $remote_url + }; } sub openqa_baseurl ($local_url) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1774473623.838c74ef/t/35-script_clone_job.t new/openQA-5.1774510397.efec10a7/t/35-script_clone_job.t --- old/openQA-5.1774473623.838c74ef/t/35-script_clone_job.t 2026-03-25 22:20:23.000000000 +0100 +++ new/openQA-5.1774510397.efec10a7/t/35-script_clone_job.t 2026-03-26 08:33:17.000000000 +0100 @@ -19,8 +19,7 @@ use Mojo::Transaction; use Scalar::Util qw(looks_like_number); -# define fake client -package Test::FakeLWPUserAgentMirrorResult { +package Test::FakeResult { use Mojo::Base -base, -signatures; has is_success => 1; has code => 304; @@ -28,31 +27,6 @@ has json => undef; } # uncoverable statement -package Test::FakeLWPUserAgentMirrorTxn { - use Mojo::Base -base, -signatures; - has error => undef; - has res => sub { Test::FakeLWPUserAgentMirrorResult->new(is_success => 0, code => 404) }; -} # uncoverable statement - -package Test::FakeLWPUserAgent { - use Mojo::Base -base, -signatures; - has mirrored => sub { {} }; - has missing => 0; - has max_redirects => undef; - has fake_txn_args => sub { [] }; - - sub get ($self, $url) { Test::FakeLWPUserAgentMirrorTxn->new(@{$self->fake_txn_args}) } - - sub mirror ($self, $from, $dest) { - my @res - = ($self->missing || $from !~ qr{http://foo/tests/1/asset/iso/(foo|bar)\.iso}) - ? (is_success => 0, code => 404) - : (); - $self->mirrored->{$from} = $dest; - Test::FakeLWPUserAgentMirrorResult->new(@res); - } -} # uncoverable statement - my @argv = ( qw(WORKER_CLASS=local HDD_1=new.qcow2 HDDSIZEGB=40 FOO=value:with:colon), 'WORKER_CLASS:cre:ate+hpc#foo@bar+=-parent' @@ -73,15 +47,28 @@ WORKER_CLASS => 'qemu_x86_64', ); +subtest 'running command' => sub { + my $run = \&OpenQA::Script::CloneJob::_run_cmd; + combined_like { + like $run->('does-not-exist'), qr/Failed.*does-not-exist.*No such/i, 'error if command does not exist' + } + qr/can't exec/i, 'exec error logged'; + like $run->('false'), qr/false.*exited.*1/i, 'error if command fails'; + is $run->('true'), '', 'no error if command succeeds'; +}; + subtest 'getting job' => sub { my $clone_mock = Test::MockModule->new('OpenQA::Script::CloneJob'); $clone_mock->redefine(_handle_unexpected_return_code => sub ($tx) { die 'unexpected return code' }); - my $url_handler = {remote => Test::FakeLWPUserAgent->new, remote_url => Mojo::URL->new('foo')}; + my $fake_res = Test::FakeResult->new(is_success => 0, code => 400, json => {FOO => 'bar'}); + my $fake_txn = Test::MockObject->new->set_always(res => $fake_res)->set_always(error => undef); + my $fake_ua = Test::MockObject->new->set_always(get => $fake_txn); + $fake_ua->set_always(max_redirects => $fake_ua); + my $url_handler = {remote => $fake_ua, remote_url => Mojo::URL->new('foo')}; my $options = {'ignore-missing-assets' => 1, reproduce => 1}; throws_ok { clone_job_get_job(42, $url_handler, $options) } qr/unexpected return code/, 'unexpected return code handled'; - my $fake_res = Test::FakeLWPUserAgentMirrorResult->new(is_success => 1, code => 200, json => {FOO => 'bar'}); - $url_handler->{remote}->fake_txn_args([res => $fake_res]); + $fake_res->is_success(1)->code(200); my $job = clone_job_get_job(42, $url_handler, $options); is_deeply $job, {vars => {FOO => 'bar'}}, 'vars assigned' or always_explain $job; }; @@ -133,8 +120,10 @@ subtest 'asset download' => sub { my $temp_assetdir = tempdir; - my $fake_ua = Test::FakeLWPUserAgent->new; - my %url_handler = (remote_url => Mojo::URL->new('http://foo'), ua => $fake_ua); + my $clone_mock = Test::MockModule->new('OpenQA::Script::CloneJob'); + my %url_handler = (remote_url => Mojo::URL->new('http://foo'), curl_args => []); + my %mirrored; + my $fake_download_error = 'fake-failure'; my %options = (dir => $temp_assetdir); my $job_id = 1; my @missing_assets; @@ -150,7 +139,7 @@ missing_assets => \@missing_assets ); $temp_assetdir->child($_)->make_path->chmod(0555) for qw(iso hdd); # test with unwritable asset folders - my $clone_mock = Test::MockModule->new('OpenQA::Script::CloneJob'); + $clone_mock->redefine(mirror => sub ($url_handler, $from, $dst) { $mirrored{$from} = $dst; $fake_download_error }); $clone_mock->redefine( clone_job_get_job => sub ($job_id, $url_handler, $options) { return {id => 2, settings => {}, parents => {Chained => [3, 4]}} if $job_id eq 2; @@ -170,12 +159,11 @@ # assume an asset download fails $temp_assetdir->child($_)->remove_tree for qw(iso hdd); # test with missing asset folders (will be created) - $fake_ua->missing(1); throws_ok { combined_like { clone_job_download_assets($job_id, \%job, \%url_handler, \%options) } - qr/downloading.*foo.*to.*failed.*some status/s, 'download error logged'; + qr/downloading foobar/s, 'download logged'; } - qr/Can't clone due to missing assets: some status/, 'error if asset does not exist'; + qr|Cloning aborted during asset download:.*fake-failure|, 'error if asset does not exist'; # assume openQA reports that an asset is missing @missing_assets = ('iso/foo.iso', 'hdd/some.qcow2'); @@ -197,35 +185,35 @@ $options{'skip-deps'} = 0; $options{'ignore-missing-assets'} = 1; combined_like { clone_job_download_assets($job_id, \%job, \%url_handler, \%options) } - qr/downloading.*foo.*to.*failed.*some status.*downloading.*bar.*failed/s, 'download error logged but ignored'; + qr/Cloning aborted during asset download:.*fake-failure/s, 'download error logged but ignored'; - $fake_ua->mirrored({})->missing(0); + %mirrored = (); combined_like { clone_job_download_assets($job_id, \%job, \%url_handler, \%options) } qr{downloading.*http://.*foo.iso.*to.*foo.iso.*downloading.*http://.*bar.iso.*to.*bar.iso}s, 'download logged'; - is_deeply $fake_ua->mirrored, \%expected_downloads, + is_deeply \%mirrored, \%expected_downloads, 'assets downloadeded except HDDs which are generated by parent job anyways' - or always_explain $fake_ua->mirrored; + or always_explain \%mirrored; ok -f "$temp_assetdir/iso/foo.iso", 'foo touched'; ok -f "$temp_assetdir/iso/bar.iso", 'foo touched'; - $fake_ua->mirrored({}); + %mirrored = (); $options{'skip-deps'} = 1; $expected_downloads{"http://foo/tests/$job_id/asset/hdd/some.qcow2"} = "$temp_assetdir/hdd/some.qcow2"; $expected_downloads{"http://foo/tests/$job_id/asset/hdd/uefi-vars.qcow2"} = "$temp_assetdir/hdd/uefi-vars.qcow2"; combined_like { clone_job_download_assets($job_id, \%job, \%url_handler, \%options) } qr/downloading/, 'downloading logged (1)'; - is_deeply $fake_ua->mirrored, \%expected_downloads, + is_deeply \%mirrored, \%expected_downloads, 'assets downloadeded including HDDs because we skip cloning the parent job' - or always_explain $fake_ua->mirrored; + or always_explain \%mirrored; - $fake_ua->mirrored({}); + %mirrored = (); $job{parents} = {Chained => [3, 5]}; delete $expected_downloads{"http://foo/tests/$job_id/asset/hdd/uefi-vars.qcow2"}; combined_like { clone_job_download_assets($job_id, \%job, \%url_handler, \%options) } qr/downloading/, 'downloading logged (2)'; - is_deeply $fake_ua->mirrored, \%expected_downloads, + is_deeply \%mirrored, \%expected_downloads, 'assets downloadeded except uefi-vars because no parent produces it anyways' - or always_explain $fake_ua->mirrored; + or always_explain \%mirrored; }; subtest 'get 2 nodes HA cluster with get_deps' => sub { @@ -496,17 +484,30 @@ }; }; -subtest 'auth with lwp' => sub { - note 'config path: ' . ($ENV{OPENQA_CONFIG} = "$FindBin::Bin/data"); - my $ua = OpenQA::Script::CloneJob::create_lwp_user_agent('testapi', {}); - $ua->add_handler( - request_send => sub ($request, $ua, $handler) { - ok looks_like_number($request->header('X-API-Microtime')), 'microtime set'; - is $request->header('X-API-Key'), 'PERCIVALKEY02', 'api key set'; - is length($request->header('X-API-Hash')), 40, 'hash set'; - return HTTP::Response->new(200); # terminate the processing +subtest 'invoking curl passing auth headers' => sub { + my $clone_mock = Test::MockModule->new('OpenQA::Script::CloneJob'); + my @invoked_cmds; + $clone_mock->redefine( + _run_cmd => sub (@args) { + push @invoked_cmds, map { m/(.*hash:|.*time:)/i ? "$1 ?" : "$_" } @args; + ''; }); - $ua->get('http://foobar/some/path'); + note 'config path: ' . ($ENV{OPENQA_CONFIG} = "$FindBin::Bin/data"); + + my $args = OpenQA::Script::CloneJob::make_curl_arguments({}); + is_deeply $args, [qw(--follow --no-progress-meter)], 'default arguments correct'; + my $secrets = OpenQA::Script::CloneJob::read_secrets('testapi'); + is_deeply $secrets, [qw(PERCIVALKEY02 PERCIVALSECRET02)], 'key and secret as expected for host "testapi"'; + + my %url_handler = (curl_args => $args, secrets => $secrets); + my $error = OpenQA::Script::CloneJob::mirror(\%url_handler, Mojo::URL->new('url'), 'path'); + my @expected_cmds = ( + qw(curl --follow --no-progress-meter), + (map { -H => $_ } ('X-API-Hash: ?', 'X-API-Key: PERCIVALKEY02', 'X-API-Microtime: ?')), + qw(--continue-at - --output path url), + ); + is $error, '', 'no error returned'; + is_deeply \@invoked_cmds, \@expected_cmds, 'invoked expected commands' or always_explain \@invoked_cmds; }; subtest 'determining base URL' => sub { ++++++ openQA.obsinfo ++++++ --- /var/tmp/diff_new_pack.rmEv1r/_old 2026-03-27 16:52:32.251043119 +0100 +++ /var/tmp/diff_new_pack.rmEv1r/_new 2026-03-27 16:52:32.295044959 +0100 @@ -1,5 +1,5 @@ name: openQA -version: 5.1774473623.838c74ef -mtime: 1774473623 -commit: 838c74ef0288862796840795d66bd78058e57ee5 +version: 5.1774510397.efec10a7 +mtime: 1774510397 +commit: efec10a7ca29ca75df50219ea57f9617419139f3
