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-02 17:41:51 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/openQA (Old) and /work/SRC/openSUSE:Factory/.openQA.new.29461 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "openQA" Mon Mar 2 17:41:51 2026 rev:814 rq:1335846 version:5.1772460208.7a4e1e06 Changes: -------- --- /work/SRC/openSUSE:Factory/openQA/openQA.changes 2026-02-27 17:11:32.336169223 +0100 +++ /work/SRC/openSUSE:Factory/.openQA.new.29461/openQA.changes 2026-03-02 17:42:11.455988318 +0100 @@ -1,0 +2,24 @@ +Mon Mar 02 14:07:44 UTC 2026 - [email protected] + +- Update to version 5.1772460208.7a4e1e06: + * docs: Document array-like job settings and `job_setting` parameter + * test: Ensure test of filter params of jobs API fails if code breaks + * feat: Support searching by job settings in API to list jobs + * refactor: Improve `cancel_by_settings` + * fix: Allow filtering by more than one job setting in various routes + * test: Improve checks in `t/api/02-iso.t` + * feat: Allow searching by job settings via overview routes + * style: use consistent q{} syntax for SQL strings in Cache Model + * refactor: streamline IPC::Run usage and signal handling + * test: remove t/25-cache-service.t from unstable_tests.txt + * test: improve robustness of t/25-cache-service.t + * test: refactor InfluxDB subtest to reduce duplication + * test: improve infrastructure for t/25-cache-service.t + * fix: improve database robustness in Cache model + * fix: log rsync stderr in CacheService::Task::Sync + * test: support OPENQA_TEST_WAIT_INTERVAL in wait_for + * fix(cache): capture stderr and handle exit status robustly in Sync task + * test: make SIGCHLD handler selective in OpenQA::Test::Utils + * docs: document aggregate result badges for overview queries + +------------------------------------------------------------------- Old: ---- openQA-5.1772092969.74a39650.obscpio New: ---- openQA-5.1772460208.7a4e1e06.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ openQA-client-test.spec ++++++ --- /var/tmp/diff_new_pack.rt0fYV/_old 2026-03-02 17:42:12.704040395 +0100 +++ /var/tmp/diff_new_pack.rt0fYV/_new 2026-03-02 17:42:12.708040562 +0100 @@ -18,7 +18,7 @@ %define short_name openQA-client Name: %{short_name}-test -Version: 5.1772092969.74a39650 +Version: 5.1772460208.7a4e1e06 Release: 0 Summary: Test package for %{short_name} License: GPL-2.0-or-later ++++++ openQA-devel-test.spec ++++++ --- /var/tmp/diff_new_pack.rt0fYV/_old 2026-03-02 17:42:12.744042064 +0100 +++ /var/tmp/diff_new_pack.rt0fYV/_new 2026-03-02 17:42:12.748042231 +0100 @@ -18,7 +18,7 @@ %define short_name openQA-devel Name: %{short_name}-test -Version: 5.1772092969.74a39650 +Version: 5.1772460208.7a4e1e06 Release: 0 Summary: Test package for %{short_name} License: GPL-2.0-or-later ++++++ openQA-test.spec ++++++ --- /var/tmp/diff_new_pack.rt0fYV/_old 2026-03-02 17:42:12.776043399 +0100 +++ /var/tmp/diff_new_pack.rt0fYV/_new 2026-03-02 17:42:12.780043566 +0100 @@ -18,7 +18,7 @@ %define short_name openQA Name: %{short_name}-test -Version: 5.1772092969.74a39650 +Version: 5.1772460208.7a4e1e06 Release: 0 Summary: Test package for openQA License: GPL-2.0-or-later ++++++ openQA-worker-test.spec ++++++ --- /var/tmp/diff_new_pack.rt0fYV/_old 2026-03-02 17:42:12.816045068 +0100 +++ /var/tmp/diff_new_pack.rt0fYV/_new 2026-03-02 17:42:12.824045402 +0100 @@ -18,7 +18,7 @@ %define short_name openQA-worker Name: %{short_name}-test -Version: 5.1772092969.74a39650 +Version: 5.1772460208.7a4e1e06 Release: 0 Summary: Test package for %{short_name} License: GPL-2.0-or-later ++++++ openQA.spec ++++++ --- /var/tmp/diff_new_pack.rt0fYV/_old 2026-03-02 17:42:12.900048573 +0100 +++ /var/tmp/diff_new_pack.rt0fYV/_new 2026-03-02 17:42:12.904048740 +0100 @@ -99,7 +99,7 @@ %define devel_requires %devel_no_selenium_requires chromedriver Name: openQA -Version: 5.1772092969.74a39650 +Version: 5.1772460208.7a4e1e06 Release: 0 Summary: The openQA web-frontend, scheduler and tools License: GPL-2.0-or-later ++++++ openQA-5.1772092969.74a39650.obscpio -> openQA-5.1772460208.7a4e1e06.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1772092969.74a39650/docs/UsersGuide.asciidoc new/openQA-5.1772460208.7a4e1e06/docs/UsersGuide.asciidoc --- old/openQA-5.1772092969.74a39650/docs/UsersGuide.asciidoc 2026-02-26 09:02:49.000000000 +0100 +++ new/openQA-5.1772460208.7a4e1e06/docs/UsersGuide.asciidoc 2026-03-02 15:03:28.000000000 +0100 @@ -573,18 +573,26 @@ === Test result badges https://github.com/os-autoinst/openQA/pull/5022[gh#5022] -For each job result including the latest job result page, there is a corresponding -route to get an SVG status badge that can eg. be used to build a status dashboard -or for showing the status within a GitHub comment. +For build results or individual job results including the latest job result +page, there is a corresponding route to get an SVG status badge that can e.g. +be used to build a status dashboard or for showing the status within a GitHub +comment. image::images/badges.png[Test result badges] .... http://openqa/tests/123/badge http://openqa/tests/latest/badge +http://openqa/tests/overview/badge?distri=opensuse&groupid=1&result=not_ok .... -There is an optional parameter 'show_build=1' that will +The first two routes refer to a specific job or the latest job of a scenario. +The `/tests/overview/badge` route summarizes the status of multiple jobs based +on overview query parameters, in this example reflecting the most severe +status. It supports the same query parameters as the overview page for example +to filter out states taken into consideration. + +There is an optional parameter 'show_build=1' for single job badges that will prefix the status with the build number. === Build a continuous openQA monitoring dashboard @@ -1087,6 +1095,31 @@ openQA configuration sets `job_settings_max_recent_jobs` which limits the results. +==== Array-like job settings +Settings where the key ends with `[]` are split on `,`. So e.g. +`ISSUES[]=foo,bar,baz` will be stored as `ISSUES[]=foo`, `ISSUES[]=bar` and +`ISSUES[]=baz`. This allows searching for jobs with a certain job setting value +more efficiently, e.g.: + +[source,sh] +---- +openqa-cli api job_settings/jobs key=ISSUES value=foo +---- + +Note that the suffix `[]` is still part of the setting key and always needs to +be specified when referring to the setting. That also means that e.g. `ISSUES` +and `ISSUES[]` are two completely different settings. + +==== Using other API routes +It is also possible to use the `jobs` and `jobs/overview` routes which support +the `job_setting` parameter. More than one `job_setting` parameter can be +specified and it can also be combined with other filtering parameters, e.g.: + +[source,sh] +---- +openqa-cli api jobs result=none job_setting=ISSUES[]={foo,bar} limit=50 +---- + === Triggering tests Tests can be triggered over multiple ways, using `openqa-clone-job`, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1772092969.74a39650/lib/OpenQA/CacheService/Model/Cache.pm new/openQA-5.1772460208.7a4e1e06/lib/OpenQA/CacheService/Model/Cache.pm --- old/openQA-5.1772092969.74a39650/lib/OpenQA/CacheService/Model/Cache.pm 2026-02-26 09:02:49.000000000 +0100 +++ new/openQA-5.1772460208.7a4e1e06/lib/OpenQA/CacheService/Model/Cache.pm 2026-03-02 15:03:28.000000000 +0100 @@ -22,7 +22,7 @@ has [qw(location log sqlite min_free_percentage)]; has limit => 50 * (1024**3); -sub _perform_integrity_check ($self) { $self->sqlite->db->query('pragma integrity_check')->arrays->flatten->to_array } +sub _perform_integrity_check ($self) { $self->sqlite->db->query(q{pragma integrity_check})->arrays->flatten->to_array } sub _check_database_integrity ($self) { my $integrity_errors = $self->_perform_integrity_check; @@ -162,8 +162,8 @@ try { my $db = $self->sqlite->db; my $tx = $db->begin('exclusive'); - my $sql = "INSERT INTO assets (filename, size, last_use) VALUES (?, 0, strftime('%s','now'))" - . 'ON CONFLICT (filename) DO UPDATE SET pending=1'; + my $sql = q{INSERT INTO assets (filename, size, last_use) VALUES (?, 0, strftime('%s','now')) } + . q{ON CONFLICT (filename) DO UPDATE SET pending=1, last_use=strftime('%s','now')}; $db->query($sql, $asset); $tx->commit; } @@ -171,7 +171,7 @@ } sub metrics ($self) { - return {map { $_->{name} => $_->{value} } $self->sqlite->db->query('SELECT * FROM metrics')->hashes->each}; + return {map { $_->{name} => $_->{value} } $self->sqlite->db->query(q{SELECT * FROM metrics})->hashes->each}; } sub _exclusive_query ($self, $sql, @args) { @@ -182,13 +182,13 @@ } sub _update_metric ($self, $name, $value) { - $self->_exclusive_query('INSERT INTO metrics (name, value) VALUES ($1, $2) ON CONFLICT DO UPDATE SET value = $2', + $self->_exclusive_query(q{INSERT INTO metrics (name, value) VALUES ($1, $2) ON CONFLICT DO UPDATE SET value = $2}, $name, $value); } sub _increase_metric ($self, $name, $by_value) { $self->_exclusive_query( - 'INSERT INTO metrics (name, value) VALUES ($1, $2) ON CONFLICT DO UPDATE SET value = value + $2', + q{INSERT INTO metrics (name, value) VALUES ($1, $2) ON CONFLICT DO UPDATE SET value = value + $2}, $name, $by_value); } @@ -197,7 +197,7 @@ sub _update_asset_last_use ($self, $asset) { my $db = $self->sqlite->db; my $tx = $db->begin('exclusive'); - my $sql = "UPDATE assets set last_use = strftime('%s','now'), pending = 0 where filename = ?"; + my $sql = q{UPDATE assets set last_use = strftime('%s','now'), pending = 0 where filename = ?}; $db->query($sql, $asset); $tx->commit; @@ -208,8 +208,9 @@ my $log = $self->log; my $db = $self->sqlite->db; my $tx = $db->begin('exclusive'); - my $sql = "UPDATE assets set etag = ?, size = ?, last_use = strftime('%s','now'), pending = 0 where filename = ?"; - $db->query($sql, $etag, $size, $asset); + my $sql = q{INSERT INTO assets (filename, etag, size, last_use, pending) VALUES (?, ?, ?, strftime('%s','now'), 0) } + . q{ON CONFLICT (filename) DO UPDATE SET etag=excluded.etag, size=excluded.size, last_use=excluded.last_use, pending=0}; + $db->query($sql, $asset, $etag, $size); $tx->commit; my $asset_size = human_readable_size($size); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1772092969.74a39650/lib/OpenQA/CacheService/Task/Sync.pm new/openQA-5.1772460208.7a4e1e06/lib/OpenQA/CacheService/Task/Sync.pm --- old/openQA-5.1772092969.74a39650/lib/OpenQA/CacheService/Task/Sync.pm 2026-02-26 09:02:49.000000000 +0100 +++ new/openQA-5.1772460208.7a4e1e06/lib/OpenQA/CacheService/Task/Sync.pm 2026-03-02 15:03:28.000000000 +0100 @@ -37,13 +37,12 @@ my @cmd = (qw(rsync -avHP --timeout), RSYNC_TIMEOUT, "$from/", qw(--delete), "$to/tests/"); my $cmd = join ' ', @cmd; - $ctx->info("Calling: $cmd"); my $status; my $full_output = ''; for my $retry (1 .. RSYNC_RETRIES) { - my $output = `@cmd`; - $status = $? >> 8; - $full_output .= "Try $retry:\n" . $output . "\n"; + my $res = OpenQA::Utils::run_cmd_with_log_return_error(\@cmd); + $status = $res->{exit_status}; + $full_output .= "Try $retry:\nSTDOUT:\n$res->{stdout}\nSTDERR:\n$res->{stderr}\n"; last unless $status; sleep RSYNC_RETRY_PERIOD; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1772092969.74a39650/lib/OpenQA/Schema/ResultSet/JobSettings.pm new/openQA-5.1772460208.7a4e1e06/lib/OpenQA/Schema/ResultSet/JobSettings.pm --- old/openQA-5.1772092969.74a39650/lib/OpenQA/Schema/ResultSet/JobSettings.pm 2026-02-26 09:02:49.000000000 +0100 +++ new/openQA-5.1772460208.7a4e1e06/lib/OpenQA/Schema/ResultSet/JobSettings.pm 2026-03-02 15:03:28.000000000 +0100 @@ -57,34 +57,18 @@ return \@jobs; } -=head2 query_for_settings - -=over - -=item Return value: ResultSet (to be used as subquery) - -=back - -Given a perl hash, will create a ResultSet of job_settings +sub _cond_for_setting ($key, $value) { + {'me.key' => $key, 'me.value' => ($value =~ /^:\w+:/) ? {like => "$&%"} : $value}; +} -=cut +sub query_for_setting ($self, $key, $value) { + $value + ? ({'me.id' => {-in => $self->search(_cond_for_setting($key, $value))->get_column('job_id')->as_query}}) + : (); +} -sub query_for_settings ($self, $args) { - my @conds; - # Search into the following job_settings - for my $setting (keys %$args) { - next unless $args->{$setting}; - # for dynamic self joins we need to be creative ;( - my $tname = 'me'; - my $setting_value = ($args->{$setting} =~ /^:\w+:/) ? {'like', "$&%"} : $args->{$setting}; - push( - @conds, - { - "$tname.key" => $setting, - "$tname.value" => $setting_value - }); - } - return $self->search({-and => \@conds}); +sub conds_for_settings ($self, $settings) { + {-and => [map { $self->query_for_setting($_, $settings->{$_}) } keys %$settings]}; } 1; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1772092969.74a39650/lib/OpenQA/Schema/ResultSet/Jobs.pm new/openQA-5.1772460208.7a4e1e06/lib/OpenQA/Schema/ResultSet/Jobs.pm --- old/openQA-5.1772092969.74a39650/lib/OpenQA/Schema/ResultSet/Jobs.pm 2026-02-26 09:02:49.000000000 +0100 +++ new/openQA-5.1772460208.7a4e1e06/lib/OpenQA/Schema/ResultSet/Jobs.pm 2026-03-02 15:03:28.000000000 +0100 @@ -12,6 +12,7 @@ use OpenQA::App; use OpenQA::Jobs::Constants; use OpenQA::Constants qw(DEFAULT_MAX_JOB_TIME); +use OpenQA::Jobs::Constants qw(PENDING_STATES EXECUTION_STATES); use OpenQA::Log qw(log_trace log_debug log_info); use OpenQA::Schema::Result::Jobs; use OpenQA::Schema::Result::JobDependencies; @@ -269,6 +270,7 @@ sub _prepare_complex_query_search_args ($self, $args) { my @conds; my @joins; + my $job_settings = $args->{job_settings} // {}; if ($args->{module_re}) { my $modules = $self->_search_modules($args->{module_re}); @@ -336,17 +338,11 @@ push(@conds, -or => \@likes); } else { - my %js_settings; # Check if the settings are between the arguments passed via query url # they come in lowercase, so make sure $key is lc'ed for my $key (qw(ISO HDD_1 WORKER_CLASS)) { - $js_settings{$key} = $args->{lc $key} if defined $args->{lc $key}; + $job_settings->{$key} = $args->{lc $key} if defined $args->{lc $key}; } - if (keys %js_settings) { - my $subquery = $schema->resultset('JobSettings')->query_for_settings(\%js_settings); - push(@conds, {'me.id' => {-in => $subquery->get_column('job_id')->as_query}}); - } - for my $key (qw(distri version flavor arch test machine)) { push(@conds, {'me.' . uc($key) => $args->{$key}}) if $args->{$key}; } @@ -355,6 +351,8 @@ } } + push @conds, $schema->resultset('JobSettings')->conds_for_settings($job_settings) if keys %$job_settings; + if (defined(my $c = $args->{comment_text})) { push @conds, \['(select id from comments where job_id = me.id and text like ? limit 1) is not null', "%$c%"]; } @@ -428,33 +426,24 @@ ) { $deprio_limit //= 100; - my $rsource = $self->result_source; - my $schema = $rsource->schema; - # preserve original settings by deep copy - my %precond = %{$settings}; - my %cond; - - for my $key (OpenQA::Schema::Result::Jobs::MAIN_SETTINGS) { - $cond{$key} = delete $precond{$key} if defined $precond{$key}; - } - if (keys %precond) { - my $subquery = $schema->resultset('JobSettings')->query_for_settings(\%precond); - $cond{'me.id'} = {-in => $subquery->get_column('job_id')->as_query}; - } - $cond{state} = [OpenQA::Jobs::Constants::PENDING_STATES]; - my $jobs = $schema->resultset('Jobs')->search(\%cond); - my $jobs_to_cancel; + my $schema = $self->result_source->schema; + my %settings = %$settings; # make copy to preserve original settings + my %main_conds = ( + state => [PENDING_STATES], + map { defined $settings{$_} ? ($_, delete $settings{$_}) : () } OpenQA::Schema::Result::Jobs::MAIN_SETTINGS, + ); + my @setting_conds = keys %settings ? $schema->resultset('JobSettings')->conds_for_settings(\%settings) : (); + my $jobs = $schema->resultset('Jobs')->search({-and => [\%main_conds, @setting_conds]}); if ($newbuild) { # filter out all jobs that have any comment (they are considered 'important') ... - $jobs_to_cancel = $jobs->search({'comments.job_id' => undef}, {join => 'comments'}); + my $jobs_without_comments = $jobs->search({'comments.job_id' => undef}, {join => 'comments'}); # ... or belong to a tagged build, i.e. is considered important # this might be even the tag 'not important' but not much is lost if # we still not cancel these builds - my $groups_query = $jobs->get_column('group_id')->as_query; - my @important_builds = grep defined, - map { ($_->tag)[0] } $schema->resultset('Comments')->search({'me.group_id' => {-in => $groups_query}}); + my $comments_search = {'me.group_id' => {-in => $jobs->get_column('group_id')->as_query}}; + my @important_builds = map { ($_->tag)[0] // () } $schema->resultset('Comments')->search($comments_search); my @unimportant_jobs; - while (my $j = $jobs_to_cancel->next) { + while (my $j = $jobs_without_comments->next) { # the value we get from that @important_builds search above # could be just BUILD or VERSION-BUILD next if grep ($j->BUILD eq $_, @important_builds); @@ -463,10 +452,7 @@ } # if there are only important jobs there is nothing left for us to do return 0 unless @unimportant_jobs; - $jobs_to_cancel = $jobs_to_cancel->search({'me.id' => {-in => \@unimportant_jobs}}); - } - else { - $jobs_to_cancel = $jobs; + $jobs = $jobs->search({'me.id' => {-in => \@unimportant_jobs}}); } my $cancelled_jobs = 0; my $priority_increment = 10; @@ -486,11 +472,9 @@ return $job->cancel($job_result, $reason) // 0; }; # first scheduled to avoid worker grab - my $scheduled = $jobs_to_cancel->search({state => SCHEDULED}); - $cancelled_jobs += $cancel_or_deprioritize->($_) for $scheduled->all; + $cancelled_jobs += $cancel_or_deprioritize->($_) for $jobs->search({state => SCHEDULED}); # then the rest - my $executing = $jobs_to_cancel->search({state => [OpenQA::Jobs::Constants::EXECUTION_STATES]}); - $cancelled_jobs += $cancel_or_deprioritize->($_) for $executing->all; + $cancelled_jobs += $cancel_or_deprioritize->($_) for $jobs->search({state => [EXECUTION_STATES]}); OpenQA::App->singleton->emit_event(openqa_job_cancel_by_settings => $settings) if $cancelled_jobs; return $cancelled_jobs; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1772092969.74a39650/lib/OpenQA/Utils.pm new/openQA-5.1772460208.7a4e1e06/lib/OpenQA/Utils.pm --- old/openQA-5.1772092969.74a39650/lib/OpenQA/Utils.pm 2026-02-26 09:02:49.000000000 +0100 +++ new/openQA-5.1772460208.7a4e1e06/lib/OpenQA/Utils.pm 2026-03-02 15:03:28.000000000 +0100 @@ -306,6 +306,7 @@ try { my ($stdin, $stdout, $stderr) = ('') x 3; my @out_args = defined $output_file ? ('>', $output_file, '2>', \$stderr) : (\$stdout, \$stderr); + local $SIG{CHLD} = 'DEFAULT'; my $ipc_run_succeeded = IPC::Run::run($cmd, \$stdin, @out_args); my $error_code = $?; my $return_code; @@ -338,6 +339,7 @@ return { status => $ipc_run_succeeded, return_code => $return_code, + exit_status => $ipc_run_succeeded ? 0 : (($return_code // 128 + ($signal // 127)) & 0xFF || 255), stdout => $stdout, stderr => $stderr, signal => $signal, @@ -347,6 +349,7 @@ return { status => 0, return_code => undef, + exit_status => 255, stderr => 'an internal error occurred', stdout => '', }; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1772092969.74a39650/lib/OpenQA/WebAPI/Controller/API/V1/Iso.pm new/openQA-5.1772460208.7a4e1e06/lib/OpenQA/WebAPI/Controller/API/V1/Iso.pm --- old/openQA-5.1772092969.74a39650/lib/OpenQA/WebAPI/Controller/API/V1/Iso.pm 2026-02-26 09:02:49.000000000 +0100 +++ new/openQA-5.1772460208.7a4e1e06/lib/OpenQA/WebAPI/Controller/API/V1/Iso.pm 2026-03-02 15:03:28.000000000 +0100 @@ -265,9 +265,8 @@ $self->emit_event('openqa_iso_delete', {iso => $iso}); my $schema = $self->schema; - my $subquery = $schema->resultset('JobSettings')->query_for_settings({ISO => $iso}); - my @jobs - = $schema->resultset('Jobs')->search({'me.id' => {-in => $subquery->get_column('job_id')->as_query}})->all; + my $settings_conds = $schema->resultset('JobSettings')->conds_for_settings({ISO => $iso}); + my @jobs = $schema->resultset('Jobs')->search($settings_conds)->all; for my $job (@jobs) { $self->emit_event('openqa_job_delete', {id => $job->id}); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1772092969.74a39650/lib/OpenQA/WebAPI/Controller/API/V1/Job.pm new/openQA-5.1772460208.7a4e1e06/lib/OpenQA/WebAPI/Controller/API/V1/Job.pm --- old/openQA-5.1772092969.74a39650/lib/OpenQA/WebAPI/Controller/API/V1/Job.pm 2026-02-26 09:02:49.000000000 +0100 +++ new/openQA-5.1772460208.7a4e1e06/lib/OpenQA/WebAPI/Controller/API/V1/Job.pm 2026-03-02 15:03:28.000000000 +0100 @@ -86,6 +86,7 @@ $validation->optional('offset')->num; $validation->optional('groupid')->num; $validation->optional('not_groupid')->num; + $validation->optional('job_setting', 'not_empty')->like(qr/.+=.*/); my $limits = OpenQA::App->singleton->config->{misc_limits}; my $limit = min($limits->{generic_max_limit}, $validation->param('limit') // $limits->{generic_default_limit}); @@ -99,6 +100,7 @@ my %args; $args{limit} = $limit + 1; $args{offset} = $offset; + $args{job_settings} = $self->every_key_value_param('job_setting'); my @args = qw(build iso distri version flavor scope group groupid not_groupid before after arch hdd_1 test machine worker_class modules modules_result); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1772092969.74a39650/lib/OpenQA/WebAPI/Plugin/Helpers.pm new/openQA-5.1772460208.7a4e1e06/lib/OpenQA/WebAPI/Plugin/Helpers.pm --- old/openQA-5.1772092969.74a39650/lib/OpenQA/WebAPI/Plugin/Helpers.pm 2026-02-26 09:02:49.000000000 +0100 +++ new/openQA-5.1772460208.7a4e1e06/lib/OpenQA/WebAPI/Plugin/Helpers.pm 2026-03-02 15:03:28.000000000 +0100 @@ -285,6 +285,7 @@ $app->helper('reply.validation_error' => \&_validation_error); $app->helper(compose_job_overview_search_args => \&_compose_job_overview_search_args); $app->helper(every_non_empty_param => \&_every_non_empty_param); + $app->helper(every_key_value_param => \&_every_key_value_param); $app->helper(compute_overview_filtering_params => \&_compute_overview_filtering_params); $app->helper(groups_for_globs => \&_groups_for_globs); $app->helper(param_hash => \&_param_hash); @@ -350,6 +351,7 @@ $v->optional('modules', 'comma_separated'); $v->optional('flavor', 'comma_separated'); $v->optional('limit', 'not_empty')->num(1, undef); + $v->optional('job_setting', 'not_empty')->like(qr/.+=.*/); $v->optional('t')->datetime; # add simple query params to search args @@ -442,8 +444,9 @@ # allow filtering by comment text if (my $c = $v->param('comment')) { $search_args{comment_text} = $c } - # allow filtering by several job settings + # allow filtering by job settings $search_args{filters} = $c->compute_overview_filtering_params; + $search_args{job_settings} = $c->every_key_value_param('job_setting'); return (\%search_args, \@groups); } @@ -452,6 +455,10 @@ [map { split ',', $_ } @{$c->every_param($param_key)}] } +sub _every_key_value_param ($c, $param_key) { + return {map { split('=', $_, 2) } @{$c->validation->every_param($param_key)}}; +} + sub _compute_overview_filtering_params ($c) { my $states = $c->every_non_empty_param('state'); my $results = $c->every_non_empty_param('result'); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1772092969.74a39650/t/10-tests_overview.t new/openQA-5.1772460208.7a4e1e06/t/10-tests_overview.t --- old/openQA-5.1772092969.74a39650/t/10-tests_overview.t 2026-02-26 09:02:49.000000000 +0100 +++ new/openQA-5.1772460208.7a4e1e06/t/10-tests_overview.t 2026-03-02 15:03:28.000000000 +0100 @@ -501,6 +501,38 @@ like $summary, qr/Scheduled: 2/i, 'Scheduled jobs remain'; }; +subtest 'Filtering by job settings' => sub { + $schema->txn_begin; + + my @basic_settings = (VERSION => 'test_version', DISTRI => 'test_distri'); + my @search_params = (distri => 'test_distri', version => 'test_version', job_setting => ()); + my @jobs = (create_job(TEST => 'test_job_1', @basic_settings), create_job(TEST => 'test_job_2', @basic_settings)); + $jobs[0]->settings->create({key => 'MY_SETTING', value => 'my_value'}); + $jobs[0]->settings->create({key => 'ANOTHER_SETTING', value => 'another_value'}); + $jobs[1]->settings->create({key => 'MY_SETTING', value => 'other_value'}); + $jobs[1]->settings->create({key => 'ANOTHER_SETTING', value => 'another_value'}); + $jobs[1]->settings->create({key => 'ANOTHER_SETTING', value => 'yet_another_value'}); + + $t->get_ok('/tests/overview', form => {@search_params, 'MY_SETTING=my_value'})->status_is(200); + $t->element_exists('#res-' . $jobs[0]->id, 'job with custom setting found'); + $t->element_exists_not('#res-' . $jobs[1]->id, 'job with different setting NOT found'); + + $t->get_ok('/tests/overview', form => {@search_params, 'MY_SETTING=different_value'})->status_is(200); + $t->element_exists_not('#res-' . $_->id, 'all jobs filtered out') for @jobs; + + $t->get_ok('/tests/overview', form => {@search_params, ['MY_SETTING=my_value', 'ANOTHER_SETTING=another_value']}); + $t->status_is(200, 'can filter by more than one job setting'); + $t->element_exists('#res-' . $jobs[0]->id, 'job with multiple custom setting found'); + $t->element_exists_not('#res-' . $jobs[1]->id, 'job where only one setting applies NOT found'); + + $t->get_ok('/tests/overview', form => {@search_params, [map { "ANOTHER_SETTING=${_}another_value" } '', 'yet_']}); + $t->status_is(200, 'can filter by the same job setting more than one time'); + $t->element_exists_not('#res-' . $jobs[0]->id, 'job where only one setting value applies NOT found'); + $t->element_exists('#res-' . $jobs[1]->id, 'job with multiple custom setting values found'); + + $schema->txn_rollback; +}; + subtest 'Meta-filters' => sub { $t->get_ok('/tests/overview?distri=opensuse&version=13.1&build=0091&result=complete')->status_is(200); my $summary = get_summary; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1772092969.74a39650/t/25-cache-service.t new/openQA-5.1772460208.7a4e1e06/t/25-cache-service.t --- old/openQA-5.1772092969.74a39650/t/25-cache-service.t 2026-02-26 09:02:49.000000000 +0100 +++ new/openQA-5.1772460208.7a4e1e06/t/25-cache-service.t 2026-03-02 15:03:28.000000000 +0100 @@ -16,7 +16,10 @@ $ENV{OPENQA_CACHE_ATTEMPTS} = 3; $ENV{OPENQA_CACHE_ATTEMPT_SLEEP_TIME} = 0; $ENV{OPENQA_RSYNC_RETRY_PERIOD} = 0; + $ENV{OPENQA_RSYNC_RETRIES} = 1; $ENV{OPENQA_METRICS_DOWNLOAD_SIZE} = 1024; + $ENV{OPENQA_BASE_PORT} = 20000 + int(rand(10000)); + $ENV{OPENQA_TEST_WAIT_INTERVAL} = 0.05; $tempdir = tempdir; my $basedir = $tempdir->child('t', 'cache.d'); @@ -79,7 +82,7 @@ $server_instance->set_pipes(0)->separate_err(0)->blocking_stop(1)->channels(0)->restart; $cache_service->set_pipes(0)->separate_err(0)->blocking_stop(1)->channels(0)->restart; perform_minion_jobs($t->app->minion); - wait_for_or_bail_out { $cache_client->info->available } 'cache service'; + wait_for_or_bail_out { $cache_client->info->available } 'cache service', {interval => 0.5}; } sub test_default_usage ($id, $asset) { @@ -137,17 +140,19 @@ } sub test_download ($id, $asset) { - unlink path($cachedir)->child($asset); + unlink path($cachedir, "localhost")->child($asset); my $asset_request = $cache_client->asset_request(id => $id, asset => $asset, type => 'hdd', host => $host); ok !$cache_client->enqueue($asset_request), "enqueued id $id, asset $asset"; my $status = $cache_client->status($asset_request); perform_minion_jobs($t->app->minion); - $status = $cache_client->status($asset_request) until !$status->is_downloading; + wait_for_or_bail_out { !$cache_client->status($asset_request)->is_downloading } "asset"; + $status = $cache_client->status($asset_request); # And then goes to PROCESSED state - ok $status->is_processed, 'only other state is processed'; + ok !$status->has_error, 'no error in status' or die always_explain $status; + ok $status->is_processed, 'only other state is processed' or die always_explain $status; ok exists $status->data->{has_download_error}, 'whether a download error happened is added to status info'; ok !$status->data->{has_download_error}, 'no download error occurred'; @@ -327,13 +332,15 @@ my $tot_proc = $ENV{STRESS_TEST} ? 100 : 3; my $concurrent = $ENV{STRESS_TEST} ? 30 : 2; + my $worker = cache_minion_worker; + $worker->start; + my $q = queue; $q->pool->maximum_processes($concurrent); $q->queue->maximum_processes($tot_proc); my $concurrent_test = sub { if (!$cache_client->enqueue($asset_request)) { - perform_minion_jobs($t->app->minion); wait_for_or_bail_out { $cache_client->status($asset_request)->is_processed } 'asset'; my $ret = $cache_client->asset_exists('localhost', $asset); Devel::Cover::report() if Devel::Cover->can('report'); @@ -344,6 +351,7 @@ $q->add(process($concurrent_test)->set_pipes(0)->internal_pipes(1)) for 1 .. $tot_proc; $q->consume(); + $worker->stop; is $q->done->size, $tot_proc, 'Queue consumed ' . $tot_proc . ' processes'; $q->done->each( sub { @@ -359,7 +367,7 @@ my $asset = '[email protected]'; my $asset_request = $cache_client->asset_request(id => 922756, asset => $asset, type => 'hdd', host => $host); - unlink path($cachedir)->child($asset); + unlink path($cachedir, 'localhost')->child($asset); ok !$cache_client->asset_exists('localhost', $asset), 'Asset absent' or die diag 'Asset already exists - abort test'; @@ -382,7 +390,7 @@ my $asset = '[email protected]'; my $asset_request = $cache_client->asset_request(id => 922756, asset => $asset, type => 'hdd', host => $host); - unlink path($cachedir)->child($asset); + unlink path($cachedir, 'localhost')->child($asset); ok !$cache_client->asset_exists('localhost', $asset), 'Asset absent' or die diag 'Asset already exists - abort test'; @@ -409,9 +417,14 @@ my $worker_4 = cache_minion_worker; $_->start for ($worker_2, $worker_3, $worker_4); + wait_for_or_bail_out { + my $stats = $cache_client->info->data; + ($stats->{active_workers} // 0) + ($stats->{inactive_workers} // 0) >= 3 + } + '3 minion workers'; my @assets = map { "sle-12-SP3-x86_64-0368-200_$_\@64bit.qcow2" } 1 .. $tot_proc; - unlink path($cachedir)->child($_) for @assets; + unlink path($cachedir, "localhost")->child($_) for @assets; my %requests = map { $_ => $cache_client->asset_request(id => 922756, asset => $_, type => 'hdd', host => $host) } @assets; ok !$cache_client->enqueue($requests{$_}), "Download enqueued for $_" for @assets; @@ -422,12 +435,13 @@ 'assets'; for my $asset (@assets) { - ok wait_for(sub { $cache_client->asset_exists('localhost', $asset) }, "Asset $asset downloaded correctly"), - "Asset $asset downloaded correctly"; + my $status = $cache_client->status($requests{$asset}); + ok $status->is_success, "Asset $asset downloaded successfully" or die always_explain $status; + ok $cache_client->asset_exists('localhost', $asset), "Asset $asset exists on disk"; } @assets = map { '[email protected]' } 1 .. $tot_proc; - unlink path($cachedir)->child($_) for @assets; + unlink path($cachedir, "localhost")->child($_) for @assets; %requests = map { $_ => $cache_client->asset_request(id => 922756, asset => $_, type => 'hdd', host => $host) } @assets; ok !$cache_client->enqueue($requests{$_}), "Download enqueued for $_" for @assets; @@ -438,9 +452,10 @@ 'assets'; for my $asset (@assets) { - ok wait_for(sub { $cache_client->asset_exists('localhost', '[email protected]') }, - "Asset $asset downloaded correctly"), - "Asset $asset downloaded correctly"; + my $status = $cache_client->status($requests{$asset}); + ok $status->is_success, "Asset $asset downloaded successfully" or die always_explain $status; + ok $cache_client->asset_exists('localhost', '[email protected]'), + "Asset $asset exists on disk"; } $_->stop for ($worker_2, $worker_3, $worker_4); @@ -504,55 +519,40 @@ is $check_count->(), 0, 'count back at 0'; }; - my $url = $cache_client->url('/influxdb/minion'); + my $cs_port = service_port "cache_service"; + my $url = $cache_client->url("/influxdb/minion"); my $ua = $cache_client->ua; - my $res = $ua->get($url)->result; - is $res->body, <<"EOF", 'three workers still running'; -openqa_minion_jobs,url=http://127.0.0.1:9530 active=0i,delayed=0i,failed=0i,inactive=0i -openqa_minion_workers,url=http://127.0.0.1:9530 active=0i,inactive=2i,registered=2i -openqa_download_count,url=http://127.0.0.1:9530 count=${count}i -openqa_download_rate,url=http://127.0.0.1:9530 bytes=${rate}i + + my $check_influx + = sub ($active_jobs, $inactive_jobs, $failed_jobs, $delayed_jobs, $active_workers, $inactive_workers, + $registered_workers, $msg) + { + my $res = $ua->get($url)->result; + is $res->body, <<"EOF", $msg; +openqa_minion_jobs,url=http://127.0.0.1:${cs_port} active=${active_jobs}i,delayed=${delayed_jobs}i,failed=${failed_jobs}i,inactive=${inactive_jobs}i +openqa_minion_workers,url=http://127.0.0.1:${cs_port} active=${active_workers}i,inactive=${inactive_workers}i,registered=${registered_workers}i +openqa_download_count,url=http://127.0.0.1:${cs_port} count=${count}i +openqa_download_rate,url=http://127.0.0.1:${cs_port} bytes=${rate}i EOF + }; + + $check_influx->(0, 0, 0, 0, 0, 2, 2, 'three workers still running'); my $minion = $app->minion; my $worker = $minion->repair->worker->register; - $res = $ua->get($url)->result; - is $res->body, <<"EOF", 'four workers running now'; -openqa_minion_jobs,url=http://127.0.0.1:9530 active=0i,delayed=0i,failed=0i,inactive=0i -openqa_minion_workers,url=http://127.0.0.1:9530 active=0i,inactive=3i,registered=3i -openqa_download_count,url=http://127.0.0.1:9530 count=${count}i -openqa_download_rate,url=http://127.0.0.1:9530 bytes=${rate}i -EOF + $check_influx->(0, 0, 0, 0, 0, 3, 3, 'four workers running now'); $minion->add_task(test => sub { }); my $job_id = $minion->enqueue('test'); my $job_id2 = $minion->enqueue('test'); my $job = $worker->dequeue(0); - $res = $ua->get($url)->result; - is $res->body, <<"EOF", 'two jobs'; -openqa_minion_jobs,url=http://127.0.0.1:9530 active=1i,delayed=0i,failed=0i,inactive=1i -openqa_minion_workers,url=http://127.0.0.1:9530 active=1i,inactive=2i,registered=3i -openqa_download_count,url=http://127.0.0.1:9530 count=${count}i -openqa_download_rate,url=http://127.0.0.1:9530 bytes=${rate}i -EOF + $check_influx->(1, 1, 0, 0, 1, 2, 3, 'two jobs'); $job->fail('test'); - $res = $ua->get($url)->result; - is $res->body, <<"EOF", 'one job failed'; -openqa_minion_jobs,url=http://127.0.0.1:9530 active=0i,delayed=0i,failed=1i,inactive=1i -openqa_minion_workers,url=http://127.0.0.1:9530 active=0i,inactive=3i,registered=3i -openqa_download_count,url=http://127.0.0.1:9530 count=${count}i -openqa_download_rate,url=http://127.0.0.1:9530 bytes=${rate}i -EOF + $check_influx->(0, 1, 1, 0, 0, 3, 3, 'one job failed'); $job->retry({delay => ONE_HOUR}); - $res = $ua->get($url)->result; - is $res->body, <<"EOF", 'job is being retried'; -openqa_minion_jobs,url=http://127.0.0.1:9530 active=0i,delayed=1i,failed=0i,inactive=2i -openqa_minion_workers,url=http://127.0.0.1:9530 active=0i,inactive=3i,registered=3i -openqa_download_count,url=http://127.0.0.1:9530 count=${count}i -openqa_download_rate,url=http://127.0.0.1:9530 bytes=${rate}i -EOF + $check_influx->(0, 2, 0, 1, 0, 3, 3, 'job is being retried'); }; subtest 'Concurrent downloads of the same file' => sub { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1772092969.74a39650/t/api/02-iso.t new/openQA-5.1772460208.7a4e1e06/t/api/02-iso.t --- old/openQA-5.1772092969.74a39650/t/api/02-iso.t 2026-02-26 09:02:49.000000000 +0100 +++ new/openQA-5.1772460208.7a4e1e06/t/api/02-iso.t 2026-03-02 15:03:28.000000000 +0100 @@ -360,7 +360,7 @@ is $t->tx->res->json->{job}->{state}, 'scheduled', "new job $newid is scheduled"; # cancel the iso -$t->post_ok("/api/v1/isos/$iso/cancel")->status_is(200); +$t->post_ok("/api/v1/isos/$iso/cancel")->status_is(200)->json_is('/result', 16, 'iso was cancelled'); $t->get_ok("/api/v1/jobs/$newid")->status_is(200); is $t->tx->res->json->{job}->{state}, 'cancelled', "job $newid is cancelled"; @@ -378,7 +378,7 @@ $t->delete_ok("/api/v1/isos/$iso")->status_is(403); # switch to admin and continue client($t, apikey => 'ARTHURKEY01', apisecret => 'EXCALIBUR'); -$t->delete_ok("/api/v1/isos/$iso")->status_is(200); +$t->delete_ok("/api/v1/isos/$iso")->status_is(200)->json_is('/count', 23, 'jobs deleted'); # now the jobs should be gone $t->get_ok("/api/v1/jobs/$newid")->status_is(404); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1772092969.74a39650/t/api/04-jobs.t new/openQA-5.1772460208.7a4e1e06/t/api/04-jobs.t --- old/openQA-5.1772092969.74a39650/t/api/04-jobs.t 2026-02-26 09:02:49.000000000 +0100 +++ new/openQA-5.1772460208.7a4e1e06/t/api/04-jobs.t 2026-03-02 15:03:28.000000000 +0100 @@ -86,6 +86,10 @@ my @jobs = @{$t->tx->res->json->{jobs}}; my $jobs_count = scalar @jobs; +sub job_ids { + [sort map { $_->{id} } @{$t->tx->res->json->{jobs}}]; +} + subtest 'initial state of jobs listing' => sub { is $jobs_count, 18; my %jobs = map { $_->{id} => $_ } @jobs; @@ -145,13 +149,17 @@ is scalar @jobs, 4, 'jobs of specified groups are excluded'; }; -subtest 'restricted query' => sub { +subtest 'filtering jobs' => sub { $t->get_ok('/api/v1/jobs?iso=openSUSE-13.1-DVD-i586-Build0091-Media.iso'); - is scalar(@{$t->tx->res->json->{jobs}}), 6, 'query for existing jobs by iso'; + is_deeply job_ids(), [99927, 99928, 99937, 99944, 99945, 99946], 'filtering by iso'; $t->get_ok('/api/v1/jobs?build=0091'); - is scalar(@{$t->tx->res->json->{jobs}}), 11, 'query for existing jobs by build'; + is_deeply job_ids(), [99764, 99927, 99928, 99937, 99944 .. 99946, 99961 .. 99963, 99981], 'filtering by build'; $t->get_ok('/api/v1/jobs?hdd_1=openSUSE-13.1-x86_64.hda'); - is scalar(@{$t->tx->res->json->{jobs}}), 3, 'query for existing jobs by hdd_1'; + is_deeply job_ids(), [99936, 99939, 99946], 'filtering by HDD_1'; + $t->get_ok('/api/v1/jobs?iso=openSUSE-Factory-DVD-x86_64-Build0048-Media.iso&hdd_1=openSUSE-13.1-x86_64.hda'); + is_deeply job_ids(), [99936, 99939], 'filters can be combined'; + $t->get_ok('/api/v1/jobs?distri=opensuse&job_setting=DESKTOP=kde&job_setting=QEMUCPU=qemu64'); + is_deeply job_ids(), [99936, 99938, 99939, 99940], 'filtering by arbitrary job settings'; }; subtest 'argument combinations' => sub { @@ -1397,7 +1405,7 @@ $query->query(worker_class => ':UFP:'); $t->get_ok($query->path_query)->status_is(200); $res = $t->tx->res->json; - ok @{$res->{jobs}} eq 1, 'Known worker class group exists, and returns one job'; + is @{$res->{jobs}}, 1, 'Known worker class group exists, and returns one job'; $t->json_is('/jobs/0/settings/WORKER_CLASS' => ':UFP:NCC1701F', 'Correct worker class'); }; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1772092969.74a39650/t/lib/OpenQA/Test/Utils.pm new/openQA-5.1772460208.7a4e1e06/t/lib/OpenQA/Test/Utils.pm --- old/openQA-5.1772092969.74a39650/t/lib/OpenQA/Test/Utils.pm 2026-02-26 09:02:49.000000000 +0100 +++ new/openQA-5.1772460208.7a4e1e06/t/lib/OpenQA/Test/Utils.pm 2026-03-02 15:03:28.000000000 +0100 @@ -214,8 +214,10 @@ # produces a test failure in case any relevant sub process terminated with a non-zero exit code # note: This function is supposed to be called from the SIGCHLD handler. It seems to have no effect to # call die or BAIL_OUT from that handler so fail and _exit is used instead. - while ((my $pid = waitpid(-1, WNOHANG)) > 0) { - next unless my $child_name = delete $RELEVANT_CHILD_PIDS{$pid}; + for my $pid (keys %RELEVANT_CHILD_PIDS) { + my $wait_pid = waitpid($pid, WNOHANG); + next unless $wait_pid > 0; + my $child_name = delete $RELEVANT_CHILD_PIDS{$pid}; my $exit_status = $?; my $exit_signal = $exit_status & 127; _fail_and_exit "sub process $child_name terminated by signal $exit_signal", $exit_signal if $exit_signal; @@ -571,12 +573,13 @@ sub wait_for : prototype(&*;*) { # `&*;*` allows calling it like `wait_for { 1 } 'foo'` my ($function, $description, $args) = @_; my $timeout = $args->{timeout} // 60; - my $interval = $args->{interval} // .1; + my $interval = $args->{interval} // $ENV{OPENQA_TEST_WAIT_INTERVAL} // .1; note "Waiting for '$description' to become available (timeout: $timeout)"; - while ($timeout > 0) { + my $end = Time::HiRes::time() + $timeout; + while (Time::HiRes::time() < $end) { return 1 if $function->(); - $timeout -= sleep $interval; # uncoverable statement (function might return early one line up) + sleep $interval; } return 0; # uncoverable statement (only invoked if tests would fail) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1772092969.74a39650/tools/unstable_tests.txt new/openQA-5.1772460208.7a4e1e06/tools/unstable_tests.txt --- old/openQA-5.1772092969.74a39650/tools/unstable_tests.txt 2026-02-26 09:02:49.000000000 +0100 +++ new/openQA-5.1772460208.7a4e1e06/tools/unstable_tests.txt 2026-03-02 15:03:28.000000000 +0100 @@ -1,3 +1,2 @@ -t/25-cache-service.t t/ui/26-jobs_restart.t t/ui/13-admin.t ++++++ openQA.obsinfo ++++++ --- /var/tmp/diff_new_pack.rt0fYV/_old 2026-03-02 17:42:39.005124897 +0100 +++ /var/tmp/diff_new_pack.rt0fYV/_new 2026-03-02 17:42:39.017125392 +0100 @@ -1,5 +1,5 @@ name: openQA -version: 5.1772092969.74a39650 -mtime: 1772092969 -commit: 74a396508c9a0468c077a85c120fbdbac5bcb04e +version: 5.1772460208.7a4e1e06 +mtime: 1772460208 +commit: 7a4e1e0611b430dcb2494faa1b0b43bed5899a5c
