From 87d0e347e393b27170b3a40e2b895d331479dc6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:59:45 -0300 Subject: [PATCH 01/28] feat(genesis): package s390x images --- debs-manifest.conf | 1 + genesis-openembedded/activate | 2 +- genesis-openembedded/build | 2 +- lib/XCAT/GenesisRelease.pm | 13 ++++++++++--- packages-manifest.conf | 1 + 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/debs-manifest.conf b/debs-manifest.conf index 4fd8c93..5ae70b3 100644 --- a/debs-manifest.conf +++ b/debs-manifest.conf @@ -155,3 +155,4 @@ xcat-genesis-openembedded-ppc64le=2.* xcat-genesis-openembedded-armv7hf=2.* xcat-genesis-openembedded-aarch64=2.* xcat-genesis-openembedded-riscv64=2.* +xcat-genesis-openembedded-s390x=2.* diff --git a/genesis-openembedded/activate b/genesis-openembedded/activate index 9a027a4..88d5c20 100755 --- a/genesis-openembedded/activate +++ b/genesis-openembedded/activate @@ -39,7 +39,7 @@ genesis_run_mknb() { genesis_activation_main() { architecture=$1 case "$architecture" in - x86|x86_64|ppc64|ppc64le|armv7hf|aarch64|riscv64) ;; + x86|x86_64|ppc64|ppc64le|armv7hf|aarch64|riscv64|s390x) ;; *) echo "Invalid Genesis architecture: $architecture" >&2 return 0 diff --git a/genesis-openembedded/build b/genesis-openembedded/build index 41fa3ba..f9bd6e7 100755 --- a/genesis-openembedded/build +++ b/genesis-openembedded/build @@ -143,7 +143,7 @@ my @formats = $format eq 'all' ? qw(deb rpm) : ($format); write_binary( "$staging/release.manifest", "format=xcat-genesis-packages\n" - . "version=1\n" + . "version=2\n" . "xcat_version=$version\n" . "xcat_release=$release\n" . "xcat_revision=$revision\n" diff --git a/lib/XCAT/GenesisRelease.pm b/lib/XCAT/GenesisRelease.pm index b07368f..b51499b 100644 --- a/lib/XCAT/GenesisRelease.pm +++ b/lib/XCAT/GenesisRelease.pm @@ -20,8 +20,14 @@ our @EXPORT_OK = qw( verify_release_file ); -my @ARCHITECTURES = qw(x86 x86_64 ppc64 ppc64le armv7hf aarch64 riscv64); +my @RELEASE_V1_ARCHITECTURES = qw(x86 x86_64 ppc64 ppc64le armv7hf aarch64 riscv64); +my @RELEASE_V2_ARCHITECTURES = (@RELEASE_V1_ARCHITECTURES, 's390x'); +my @ARCHITECTURES = @RELEASE_V2_ARCHITECTURES; my %ARCHITECTURE = map { $_ => 1 } @ARCHITECTURES; +my %RELEASE_ARCHITECTURES = ( + 1 => \@RELEASE_V1_ARCHITECTURES, + 2 => \@RELEASE_V2_ARCHITECTURES, +); sub architectures { return @ARCHITECTURES; @@ -161,7 +167,7 @@ sub _validate_release { die "Unsupported Genesis package release format\n" unless $manifest->{format} eq 'xcat-genesis-packages'; die "Unsupported Genesis package release version\n" - unless $manifest->{version} eq '1'; + unless $RELEASE_ARCHITECTURES{ $manifest->{version} }; die "Invalid xCAT version in release manifest\n" unless $manifest->{xcat_version} =~ /^\d+(?:\.\d+){1,3}$/; die "Invalid xCAT release in release manifest\n" @@ -222,7 +228,8 @@ sub validate_complete_release { my ($directory) = @_; my $manifest = validate_release($directory); my %present = map { $_ => 1 } split(/,/, $manifest->{architectures}); - my @missing = grep { !$present{$_} } @ARCHITECTURES; + my @required = @{ $RELEASE_ARCHITECTURES{ $manifest->{version} } }; + my @missing = grep { !$present{$_} } @required; die "Genesis release is missing supported architectures: @missing\n" if @missing; return $manifest; } diff --git a/packages-manifest.conf b/packages-manifest.conf index d97d07a..eacc7bf 100644 --- a/packages-manifest.conf +++ b/packages-manifest.conf @@ -189,3 +189,4 @@ xCAT-genesis-openembedded-ppc64le=>= 2.18.0 xCAT-genesis-openembedded-armv7hf=>= 2.18.0 xCAT-genesis-openembedded-aarch64=>= 2.18.0 xCAT-genesis-openembedded-riscv64=>= 2.18.0 +xCAT-genesis-openembedded-s390x=>= 2.18.0 From 300724af2870c73049f073b605a25cf5838193e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:59:46 -0300 Subject: [PATCH 02/28] test(genesis): cover s390x publication --- .github/workflows/genesis-openembedded.yml | 2 + t/common-repo-gate.t | 113 +++++++++++++++++---- t/genesis_openembedded_consumer.t | 72 +++++-------- t/genesis_openembedded_release.t | 28 ++++- t/lib/XCAT/GenesisReleaseTest.pm | 59 ++++++++++- 5 files changed, 203 insertions(+), 71 deletions(-) diff --git a/.github/workflows/genesis-openembedded.yml b/.github/workflows/genesis-openembedded.yml index cb204ea..e516e42 100644 --- a/.github/workflows/genesis-openembedded.yml +++ b/.github/workflows/genesis-openembedded.yml @@ -48,6 +48,7 @@ jobs: lib/XCAT/GenesisRelease.pm \ mockbuild-all.pl \ t/build_utils.t \ + t/common-repo-gate.t \ t/genesis_openembedded_release.t \ t/genesis_openembedded_consumer.t \ t/lib/XCAT/GenesisReleaseTest.pm @@ -57,3 +58,4 @@ jobs: prove -v t/build_utils.t prove -v -It/lib t/genesis_openembedded_release.t sudo -E prove -v -It/lib t/genesis_openembedded_consumer.t + sudo -E prove -v -It/lib t/common-repo-gate.t diff --git a/t/common-repo-gate.t b/t/common-repo-gate.t index 9658e99..dca440e 100644 --- a/t/common-repo-gate.t +++ b/t/common-repo-gate.t @@ -10,34 +10,68 @@ use strict; use warnings; use Test::More; use FindBin qw($RealBin); +use File::Basename qw(basename); use File::Temp qw(tempdir); use File::Path qw(make_path); use File::Copy qw(copy); use lib "$RealBin/.."; +use lib "$RealBin/../lib"; +use lib "$RealBin/lib"; use MockBuildUtils qw(read_manifest); +use XCAT::BuildUtils qw(command_exists); +use XCAT::GenesisRelease qw(architectures rpm_package_name); +use XCAT::GenesisReleaseTest qw( + build_package_release + copy_tree + write_checksums + write_release_manifest +); my $SCRIPT = "$RealBin/../mockbuild-all.pl"; -my $RELEASE = '/opt/xcat-ci-shared/builds/genesis-openembedded-initial-20260825/release'; +my $PACKAGER = "$RealBin/../genesis-openembedded/package"; plan skip_all => 'mockbuild-all.pl not found' unless -f $SCRIPT; -plan skip_all => 'no Genesis release fixture' unless -d "$RELEASE/rpm"; -# root, like every other test that drives mockbuild-all.pl: the script refuses to run otherwise, -# and the CI builder (XCAT_GENESIS_CI) is root. -plan skip_all => 'rpm tooling and a root Linux builder required' - unless $^O eq 'linux' - && $> == 0 - && !system('sh', '-c', 'command -v rpm >/dev/null 2>&1') - && !system('sh', '-c', 'command -v createrepo_c >/dev/null 2>&1') - && !system('sh', '-c', 'command -v rpmbuild >/dev/null 2>&1'); +my @missing_requirements; +push @missing_requirements, 'Linux' unless $^O eq 'linux'; +push @missing_requirements, 'root' unless $> == 0; +for my $command (qw(createrepo_c gzip rpm rpmbuild tar)) { + push @missing_requirements, $command unless command_exists($command); +} +if (command_exists('tar')) { + my $tar_version = `tar --version 2>/dev/null`; + push @missing_requirements, 'GNU tar' unless $tar_version =~ /GNU tar/; +} +if (@missing_requirements) { + my $message = 'requires ' . join(', ', @missing_requirements); + BAIL_OUT($message) if $ENV{XCAT_GENESIS_CI}; + plan skip_all => $message; +} my $tmp = tempdir(CLEANUP => 1); my $target = 'alma+epel-10-' . do { my $m = `uname -m`; chomp $m; $m }; +my @architectures = architectures(); +my $architecture_count = scalar(@architectures); +my $xcat_version = '2.19.0'; +my $xcat_release = 'snap202609040000'; +my $xcat_revision = 'c' x 40; +my $source_date_epoch = 1788476400; +my $RELEASE = build_package_release( + root => "$tmp/release-fixture", + format => 'rpm', + architectures => \@architectures, + packager => $PACKAGER, + version => $xcat_version, + release => $xcat_release, + revision => $xcat_revision, + epoch => $source_date_epoch, +); # The shipped manifest must describe the shared repo, else nothing can gate it. { my %m = read_manifest("$RealBin/../packages-manifest.conf"); ok($m{common} && %{ $m{common} }, 'the shipped manifest has a [common] section'); - is(scalar(keys %{ $m{common} // {} }), 7, - '... naming every architecture the release must carry'); + my @missing = grep { !exists $m{common}{ rpm_package_name($_) } } @architectures; + is_deeply(\@missing, [], + 'the shared RPM manifest lists every Genesis architecture'); } # fixture_rpm: a minimal noarch rpm, built once, standing in for a compiled dep. @@ -71,7 +105,7 @@ SPEC # run_publish($release_dir) -> ($exit, $output, $common_dir) sub run_publish { - my ($release, $tag) = @_; + my ($release, $tag, $mutate_common) = @_; my $out = "$tmp/$tag"; make_path("$out/root", "$out/collect"); # Something to collect, so the run gets past the "built nothing" guard. It must NOT be an @@ -83,6 +117,7 @@ sub run_publish { print $fh "[$target]\nipmitool-xcat=1.8.18\n"; # the shared repo's own section, copied from the shipped manifest so the test uses the real one my %m = read_manifest("$RealBin/../packages-manifest.conf"); + $mutate_common->($m{common}) if $mutate_common; print $fh "\n[common]\n"; print $fh "$_=$m{common}{$_}\n" for sort keys %{ $m{common} // {} }; close $fh; @@ -97,11 +132,25 @@ sub run_publish { return ($? >> 8, $log, "$out/repo/common"); } +{ + my $package = rpm_package_name('s390x'); + my ($rc, $out, $common) = run_publish( + $RELEASE, + 'unsatisfied-common', + sub { $_[0]->{$package} = '>= 99.0.0' }, + ); + isnt($rc, 0, 'an unsatisfied shared-repository requirement is refused'); + like($out, qr/EVR \Q$package\E: repo has .* manifest requires >= 99\.0\.0/, + 'the shared-repository gate names the unsatisfied requirement'); + ok(!-d $common || !glob("$common/*.rpm"), + 'a failed shared-repository gate publishes nothing'); +} + # ---- a complete release publishes, and says it was gated ----------------------------------------- { my ($rc, $out, $common) = run_publish($RELEASE, 'full'); is($rc, 0, 'a complete release publishes') or diag($out); - is(scalar(grep { !/\.src\.rpm$/ } glob("$common/*.rpm")), 7, + is(scalar(grep { !/\.src\.rpm$/ } glob("$common/*.rpm")), $architecture_count, 'the published shared repo carries every architecture'); like($out, qr/\[verify-repo\] common complete/, 'the shared repo is gated against [common]'); } @@ -109,23 +158,45 @@ sub run_publish { # ---- an incomplete release is refused, and publishes nothing -------------------------------------- { my $partial = "$tmp/partial-release"; + my $missing_architecture = $architectures[-1]; + my $missing_package = rpm_package_name($missing_architecture); + my @partial_architectures = grep { $_ ne $missing_architecture } @architectures; make_path("$partial/rpm", "$partial/srpm"); for my $f (glob("$RELEASE/rpm/*.rpm"), glob("$RELEASE/srpm/*.rpm")) { - next if $f =~ /riscv64/; # drop one architecture + next if basename($f) =~ /^\Q$missing_package-\E/; my ($sub) = $f =~ m{/(rpm|srpm)/[^/]+$}; copy($f, "$partial/$sub/") or die $!; } - copy("$RELEASE/release.manifest", $partial) or die $!; - # SHA256SUMS without the dropped arch, so the release itself still self-describes consistently - open my $in, '<', "$RELEASE/SHA256SUMS" or die $!; - open my $o, '>', "$partial/SHA256SUMS" or die $!; - while (<$in>) { print {$o} $_ unless /riscv64/ } - close $in; close $o; + write_release_manifest( + $partial, $xcat_version, $xcat_release, $xcat_revision, $source_date_epoch, + join(',', @partial_architectures), 'rpm', + ); + write_checksums($partial); my ($rc, $out, $common) = run_publish($partial, 'partial'); isnt($rc, 0, 'a release missing an architecture is refused'); + like($out, qr/missing supported architectures: \Q$missing_architecture\E/, + 'the completeness gate identifies the missing architecture'); ok(!-d $common || !glob("$common/*.rpm"), '... and nothing is published into the shared repository'); } +{ + my $inconsistent = "$tmp/inconsistent-release"; + my $missing_package = rpm_package_name($architectures[-1]); + copy_tree($RELEASE, $inconsistent); + for my $directory (qw(rpm srpm)) { + my @packages = glob("$inconsistent/$directory/$missing_package-*"); + unlink(@packages) == @packages or die "cannot remove package fixture\n"; + } + write_checksums($inconsistent); + + my ($rc, $out, $common) = run_publish($inconsistent, 'inconsistent'); + isnt($rc, 0, 'a release inconsistent with its manifest is refused'); + like($out, qr/Genesis release is missing:/, + 'the release-layout gate reports the missing package'); + ok(!-d $common || !glob("$common/*.rpm"), + 'an inconsistent release publishes nothing'); +} + done_testing(); diff --git a/t/genesis_openembedded_consumer.t b/t/genesis_openembedded_consumer.t index 9574623..d07c5c8 100644 --- a/t/genesis_openembedded_consumer.t +++ b/t/genesis_openembedded_consumer.t @@ -27,11 +27,9 @@ use XCAT::GenesisRelease qw( rpm_package_name ); use XCAT::GenesisReleaseTest qw( - make_export + build_package_release run_capture write_forkmanager_stub - write_checksums - write_release_manifest ); my $repo_root = abs_path("$FindBin::Bin/.."); @@ -397,7 +395,9 @@ sub test_deb_consumer { } architectures(); is_deeply([ genesis_deb_names($shared_pool) ], \@expected_packages, 'shared APT pool contains one complete Genesis release'); - like(read_binary($log), qr/\[verify-repo\] shared pool complete: 7 packages present/, + my $architecture_count = scalar(architectures()); + like(read_binary($log), + qr/\[verify-repo\] shared pool complete: \Q$architecture_count\E packages present/, 'the shared pool is gated against the manifest\'s [shared] section'); my @suite_packages; for my $codename (@APT_SUITES) { @@ -995,6 +995,18 @@ SH is($status, 0, 'the activation helper accepts a local-TFTP node'); is(read_binary($log), "x86_64\n", 'the activation helper runs mknb for one architecture'); + write_binary($log, ''); + $status = run_capture($output, $driver, 's390x'); + is($status, 0, 'the activation helper accepts s390x'); + is(read_binary($log), "s390x\n", 'the activation helper runs mknb for s390x'); + + write_binary($log, ''); + $status = run_capture($output, $driver, 'unsupported'); + is($status, 0, 'an unsupported architecture does not fail the package transaction'); + is(read_binary($log), '', 'an unsupported architecture does not run mknb'); + like(read_binary($output), qr/Invalid Genesis architecture: unsupported/, + 'the activation helper reports an unsupported architecture'); + write_binary($log, ''); local $ENV{XCAT_TEST_SERVICE_NODE} = 1; local $ENV{XCAT_TEST_SHAREDTFTP} = 1; @@ -1011,50 +1023,16 @@ SH sub make_package_release { my ($root, $format, @requested_architectures) = @_; @requested_architectures = architectures() unless @requested_architectures; - my $release_root = "$root/release"; - make_path($release_root); - for my $architecture (@requested_architectures) { - my $export = make_export("$root/exports/$architecture", $architecture); - my $packages = "$root/packages/$architecture"; - die "Cannot package test release for $architecture\n" - if run_capture( - "$root/package-$architecture.log", - $packager, - '--architecture', $architecture, - '--export-dir', $export, - '--output-dir', $packages, - '--version', $version, - '--release', $release, - '--revision', $revision, - '--source-date-epoch', $epoch, - '--format', $format, - ); - if ($format eq 'rpm') { - my $name = rpm_package_name($architecture); - make_path("$release_root/rpm", "$release_root/srpm"); - copy( - "$packages/rpm/$name-$version-$release.noarch.rpm", - "$release_root/rpm/$name-$version-$release.noarch.rpm", - ) or die $!; - copy( - "$packages/srpm/$name-$version-$release.src.rpm", - "$release_root/srpm/$name-$version-$release.src.rpm", - ) or die $!; - } else { - my $name = deb_package_name($architecture); - make_path("$release_root/deb"); - copy( - "$packages/deb/${name}_${version}-${release}_all.deb", - "$release_root/deb/${name}_${version}-${release}_all.deb", - ) or die $!; - } - } - write_release_manifest( - $release_root, $version, $release, $revision, $epoch, - join(',', @requested_architectures), $format, + return build_package_release( + root => $root, + format => $format, + architectures => \@requested_architectures, + packager => $packager, + version => $version, + release => $release, + revision => $revision, + epoch => $epoch, ); - write_checksums($release_root); - return $release_root; } sub genesis_rpm_names { diff --git a/t/genesis_openembedded_release.t b/t/genesis_openembedded_release.t index 365be5b..68cb60a 100644 --- a/t/genesis_openembedded_release.t +++ b/t/genesis_openembedded_release.t @@ -53,7 +53,7 @@ if ($ENV{XCAT_GENESIS_CI}) { is_deeply( [ architectures() ], - [ qw(x86 x86_64 ppc64 ppc64le armv7hf aarch64 riscv64) ], + [ qw(x86 x86_64 ppc64 ppc64le armv7hf aarch64 riscv64 s390x) ], 'supported architectures keep their exact xCAT names', ); is(rpm_package_name('ppc64le'), 'xCAT-genesis-openembedded-ppc64le', @@ -201,6 +201,32 @@ write_release_manifest( write_checksums($complete_release); ok(validate_complete_release($complete_release), 'complete release can be published'); +my $legacy_release = "$tmp/legacy-release"; +copy_tree($complete_release, $legacy_release); +for my $directory (qw(rpm srpm deb)) { + my @s390x_packages = glob("$legacy_release/$directory/*s390x*"); + unlink(@s390x_packages) == @s390x_packages + or die "Cannot remove the s390x package fixture: $!\n"; +} +write_release_manifest( + $legacy_release, $version, $release, $revision, $epoch, + 'x86,x86_64,ppc64,ppc64le,armv7hf,aarch64,riscv64', 'deb,rpm', 1, +); +write_checksums($legacy_release); +ok(validate_complete_release($legacy_release), + 'complete version 1 releases remain publishable'); + +my $unknown_release_version = "$tmp/unknown-release-version"; +copy_tree($complete_release, $unknown_release_version); +write_release_manifest( + $unknown_release_version, $version, $release, $revision, $epoch, + join(',', architectures()), 'deb,rpm', 3, +); +write_checksums($unknown_release_version); +dies_like(sub { validate_release($unknown_release_version) }, + qr/Unsupported Genesis package release version/, + 'unknown release manifest versions fail'); + my $deb_only_release = "$tmp/deb-only-release"; make_path("$deb_only_release/deb"); for my $architecture (architectures()) { diff --git a/t/lib/XCAT/GenesisReleaseTest.pm b/t/lib/XCAT/GenesisReleaseTest.pm index 7660784..7a475e0 100644 --- a/t/lib/XCAT/GenesisReleaseTest.pm +++ b/t/lib/XCAT/GenesisReleaseTest.pm @@ -13,8 +13,10 @@ use XCAT::BuildUtils qw( relative_files write_binary ); +use XCAT::GenesisRelease qw(deb_package_name rpm_package_name); our @EXPORT_OK = qw( + build_package_release copy_tree dies_like make_export @@ -24,6 +26,58 @@ our @EXPORT_OK = qw( write_release_manifest ); +sub build_package_release { + my (%args) = @_; + my $root = $args{root}; + my $format = $args{format}; + my @architectures = @{ $args{architectures} }; + my $release_root = "$root/release"; + + make_path($release_root); + for my $architecture (@architectures) { + my $export = make_export("$root/exports/$architecture", $architecture); + my $packages = "$root/packages/$architecture"; + die "Cannot package test release for $architecture\n" + if run_capture( + "$root/package-$architecture.log", + $args{packager}, + '--architecture', $architecture, + '--export-dir', $export, + '--output-dir', $packages, + '--version', $args{version}, + '--release', $args{release}, + '--revision', $args{revision}, + '--source-date-epoch', $args{epoch}, + '--format', $format, + ); + if ($format eq 'rpm') { + my $name = rpm_package_name($architecture); + make_path("$release_root/rpm", "$release_root/srpm"); + copy( + "$packages/rpm/$name-$args{version}-$args{release}.noarch.rpm", + "$release_root/rpm/$name-$args{version}-$args{release}.noarch.rpm", + ) or die $!; + copy( + "$packages/srpm/$name-$args{version}-$args{release}.src.rpm", + "$release_root/srpm/$name-$args{version}-$args{release}.src.rpm", + ) or die $!; + } else { + my $name = deb_package_name($architecture); + make_path("$release_root/deb"); + copy( + "$packages/deb/${name}_$args{version}-$args{release}_all.deb", + "$release_root/deb/${name}_$args{version}-$args{release}_all.deb", + ) or die $!; + } + } + write_release_manifest( + $release_root, $args{version}, $args{release}, $args{revision}, + $args{epoch}, join(',', @architectures), $format, + ); + write_checksums($release_root); + return $release_root; +} + sub make_export { my ($directory, $architecture) = @_; make_path($directory); @@ -45,11 +99,12 @@ sub make_export { sub write_release_manifest { my ($directory, $xcat_version, $xcat_release, $revision, $epoch, - $architectures, $formats) = @_; + $architectures, $formats, $manifest_version) = @_; + $manifest_version //= 2; write_binary( "$directory/release.manifest", "format=xcat-genesis-packages\n" - . "version=1\n" + . "version=$manifest_version\n" . "xcat_version=$xcat_version\n" . "xcat_release=$xcat_release\n" . "xcat_revision=$revision\n" From 565250246a9f8541e08617495cc39b408fd9455b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:59:46 -0300 Subject: [PATCH 03/28] docs(genesis): list s390x packages --- genesis-openembedded/README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/genesis-openembedded/README.md b/genesis-openembedded/README.md index 33075a0..b67e569 100644 --- a/genesis-openembedded/README.md +++ b/genesis-openembedded/README.md @@ -23,7 +23,7 @@ The packaging scripts use `File::Slurper` and `IPC::Cmd`. Install The default format is `all`, which produces RPM, SRPM, and DEB packages. Use `--format rpm` or `--format deb` when only one package family is needed. The supported image architectures are `x86`, `x86_64`, `ppc64`, `ppc64le`, -`armv7hf`, `aarch64`, and `riscv64`. +`armv7hf`, `aarch64`, `riscv64`, and `s390x`. Use `--architecture` for development builds. Repository publication requires a complete release built with `--all`. @@ -88,8 +88,9 @@ APT metadata is assembled in a side tree and swapped onto the published repository with a single rename, under one global publish lock, so a failed or interrupted publication leaves the previous repository exactly as it was. -Both consumers require all seven architectures and verify package identities -and checksums before publication. A management node can install an image for a +Both consumers verify package identities and checksums before publication. +Version 2 releases require all eight architectures. Seven-architecture version +1 releases remain publishable. A management node can install an image for a different target architecture. Without `--genesis-release`, both builders keep their existing behavior. The @@ -113,4 +114,5 @@ Run the package tests on a Linux builder with RPM, DEB, and repository tools: prove t/build_utils.t prove -It/lib t/genesis_openembedded_release.t sudo -E prove -It/lib t/genesis_openembedded_consumer.t +sudo -E prove -It/lib t/common-repo-gate.t ``` From 5a4be596f4d41576b950e3328e1a34db4d5a3854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 4 Sep 2026 15:22:30 -0300 Subject: [PATCH 04/28] fix(genesis): preserve version 1 publication --- mockbuild-all.pl | 17 +++++++++++++---- sbuild-all.pl | 15 ++++++++++++--- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/mockbuild-all.pl b/mockbuild-all.pl index f108ff0..f2a8339 100755 --- a/mockbuild-all.pl +++ b/mockbuild-all.pl @@ -33,6 +33,8 @@ use XCAT::BuildUtils qw( shell_quote ); use XCAT::GenesisRelease qw( + rpm_package_name + validate_complete_release validated_release_checksums verify_release_file ); @@ -108,6 +110,7 @@ my $skip_createrepo = 0; my $skip_tarball = 0; my $genesis_release = ''; my $genesis_release_checksums; +my @genesis_release_architectures; my $scrub_all_chroots = 0; my $keep_buildroots = 0; # keep per-step mock chroots after build (default: --scrub=chroot each) my $dry_run = 0; @@ -374,6 +377,8 @@ if ($genesis_release ne '') { die "Genesis release changed during verification\n" unless hashes_equal($checksums_before, $checksums_after); $genesis_release_checksums = $checksums_before; + my $manifest = validate_complete_release($genesis_release); + @genesis_release_architectures = split(/,/, $manifest->{architectures}); } # An explicit --target builds just that target; otherwise build the current host @@ -1099,11 +1104,16 @@ sub verify_common_repo { my ($dir) = @_; my $manifest = "$repo_root/packages-manifest.conf"; my %MAN = read_manifest($manifest); - my %req = %{ $MAN{common} // {} }; + my %common = %{ $MAN{common} // {} }; die "FATAL: no [common] section in $manifest -- cannot verify the shared Genesis repository\n" - if !%req; + if !%common; - my @names = sort keys %req; + my @names = map { rpm_package_name($_) } @genesis_release_architectures; + my @missing = grep { !exists($common{$_}) } @names; + die "FATAL: [common] is missing release packages: @missing\n" if @missing; + my %req = map { $_ => $common{$_} } @names; + + @names = sort @names; my %present = repo_present_versions($dir, \@names); my %present_evr = map { $_ => rpm_evr($dir, $_) } @names; my @problems = verify_repo_packages(\%req, \%present, \%present_evr, \&rpm_vercmp_segment); @@ -2150,4 +2160,3 @@ sub slurp_chomp { chomp $line if defined $line; return $line // ''; } - diff --git a/sbuild-all.pl b/sbuild-all.pl index c22aef1..a83edc6 100755 --- a/sbuild-all.pl +++ b/sbuild-all.pl @@ -100,6 +100,7 @@ my $gpg_key_id = 'xcat@megware.com'; my $gpg_home = ''; my $genesis_release = ''; # OpenEmbedded Genesis package release to publish alongside my $genesis_release_checksums; # its verified SHA256SUMS, read once at startup +my @genesis_release_architectures; # The OpenEmbedded Genesis debs are published ONCE, in a pool of their own that every suite indexes. # They are Architecture:all and identical for all suites, so a per-suite copy would multiply hundreds # of megabytes by the number of codenames for no gain. @@ -318,6 +319,8 @@ if ($genesis_release ne '') { die "FATAL: Genesis release changed during verification\n" unless XCAT::BuildUtils::hashes_equal($before, $after); $genesis_release_checksums = $before; + my $release_manifest = XCAT::GenesisRelease::validate_complete_release($genesis_release); + @genesis_release_architectures = split(/,/, $release_manifest->{architectures}); # Every suite's Packages index points into the shared Genesis pool, and publishing a release # replaces that pool -- so a run that rebuilt only some suites would leave the others indexing # files that no longer exist. Publish a release for all of them or for none. @@ -1054,10 +1057,16 @@ sub install_genesis_release_debs { # Completeness only -- the release checksums cover the bytes. sub verify_shared_pool { my ($pool) = @_; - my %req = %{ $MANIFEST{shared} // {} }; + my %shared = %{ $MANIFEST{shared} // {} }; die "FATAL: no [shared] section in $manifest -- cannot verify the shared Genesis pool\n" - if !%req; - my @names = sort keys %req; + if !%shared; + my @names = map { + XCAT::GenesisRelease::deb_package_name($_) + } @genesis_release_architectures; + my @missing = grep { !exists($shared{$_}) } @names; + die "FATAL: [shared] is missing release packages: @missing\n" if @missing; + my %req = map { $_ => $shared{$_} } @names; + @names = sort @names; my %present = map { $_ => deb_version($pool, $_) } @names; my @problems = verify_repo_packages(\%req, \%present); if (@problems) { From ed3455a51b0f82fd0970d44df2b09e038eecfbf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 4 Sep 2026 15:22:30 -0300 Subject: [PATCH 05/28] test(genesis): cover version 1 publication --- t/common-repo-gate.t | 30 ++++++++++++++++++++++++++-- t/genesis_openembedded_consumer.t | 33 ++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/t/common-repo-gate.t b/t/common-repo-gate.t index dca440e..e05e015 100644 --- a/t/common-repo-gate.t +++ b/t/common-repo-gate.t @@ -64,6 +64,22 @@ my $RELEASE = build_package_release( revision => $xcat_revision, epoch => $source_date_epoch, ); +my @version_1_architectures = grep { $_ ne 's390x' } @architectures; +my $VERSION_1_RELEASE = build_package_release( + root => "$tmp/version-1-release-fixture", + format => 'rpm', + architectures => \@version_1_architectures, + packager => $PACKAGER, + version => $xcat_version, + release => $xcat_release, + revision => $xcat_revision, + epoch => $source_date_epoch, +); +write_release_manifest( + $VERSION_1_RELEASE, $xcat_version, $xcat_release, $xcat_revision, + $source_date_epoch, join(',', @version_1_architectures), 'rpm', 1, +); +write_checksums($VERSION_1_RELEASE); # The shipped manifest must describe the shared repo, else nothing can gate it. { @@ -155,10 +171,20 @@ sub run_publish { like($out, qr/\[verify-repo\] common complete/, 'the shared repo is gated against [common]'); } +{ + my ($rc, $out, $common) = run_publish($VERSION_1_RELEASE, 'version-1'); + is($rc, 0, 'a complete version 1 release publishes') or diag($out); + is(scalar(grep { !/\.src\.rpm$/ } glob("$common/*.rpm")), + scalar(@version_1_architectures), + 'the version 1 repository keeps its seven architectures'); + ok(!glob("$common/" . rpm_package_name('s390x') . '-*.rpm'), + 'the version 1 repository does not require s390x'); +} + # ---- an incomplete release is refused, and publishes nothing -------------------------------------- { my $partial = "$tmp/partial-release"; - my $missing_architecture = $architectures[-1]; + my $missing_architecture = 's390x'; my $missing_package = rpm_package_name($missing_architecture); my @partial_architectures = grep { $_ ne $missing_architecture } @architectures; make_path("$partial/rpm", "$partial/srpm"); @@ -183,7 +209,7 @@ sub run_publish { { my $inconsistent = "$tmp/inconsistent-release"; - my $missing_package = rpm_package_name($architectures[-1]); + my $missing_package = rpm_package_name('s390x'); copy_tree($RELEASE, $inconsistent); for my $directory (qw(rpm srpm)) { my @packages = glob("$inconsistent/$directory/$missing_package-*"); diff --git a/t/genesis_openembedded_consumer.t b/t/genesis_openembedded_consumer.t index d07c5c8..aeec525 100644 --- a/t/genesis_openembedded_consumer.t +++ b/t/genesis_openembedded_consumer.t @@ -29,7 +29,9 @@ use XCAT::GenesisRelease qw( use XCAT::GenesisReleaseTest qw( build_package_release run_capture + write_checksums write_forkmanager_stub + write_release_manifest ); my $repo_root = abs_path("$FindBin::Bin/.."); @@ -73,11 +75,12 @@ SKIP: { } SKIP: { - skip 'APT repository tools are not installed', 59 + skip 'APT repository tools are not installed', 62 unless $^O eq 'linux' && command_exists('dpkg-deb') && command_exists('apt-ftparchive'); test_deb_consumer(); + test_version_1_deb_consumer(); test_legacy_deb_consumer(); test_partial_deb_release(); test_publish_lock(); @@ -524,6 +527,34 @@ sub test_deb_consumer { 'pooled package still matches the verified release'); } +sub test_version_1_deb_consumer { + my @release_architectures = grep { $_ ne 's390x' } architectures(); + my $release_root = make_package_release( + "$tmp/deb-version-1", 'deb', @release_architectures, + ); + write_release_manifest( + $release_root, $version, $release, $revision, $epoch, + join(',', @release_architectures), 'deb', 1, + ); + write_checksums($release_root); + + my $apt_root = "$tmp/apt-version-1"; + my $output = "$tmp/deb-version-1-output"; + stage_apt_suites($output, "$tmp/deb-version-1-legacy"); + my $log = "$tmp/deb-version-1.log"; + my $status = run_apt_consumer( + log => $log, output => $output, apt_dir => $apt_root, + extra => [ '--genesis-release', $release_root ], + ); + my $pool = "$apt_root/pool/main/xcat-genesis-openembedded"; + + is($status, 0, 'APT accepts a complete version 1 release'); + is(scalar(genesis_deb_names($pool)), scalar(@release_architectures), + 'the version 1 pool keeps its seven architectures'); + is(scalar(grep { /s390x/ } genesis_deb_names($pool)), 0, + 'the version 1 pool does not require s390x'); +} + sub test_signed_common_rpm_repository { my $release_root = make_package_release("$tmp/rpm-signed", 'rpm'); my $package = "xCAT-genesis-openembedded-x86_64-$version-$release.noarch.rpm"; From 2a434beaadb8325fecf9cabd48eda654660f56f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:16:23 -0300 Subject: [PATCH 06/28] fix(genesis): validate release manifests --- genesis-openembedded/build | 5 +++++ lib/XCAT/GenesisRelease.pm | 4 ++++ mockbuild-all.pl | 24 +++++++++++++++++------- sbuild-all.pl | 33 +++++++++++++++++++++------------ 4 files changed, 47 insertions(+), 19 deletions(-) diff --git a/genesis-openembedded/build b/genesis-openembedded/build index f9bd6e7..68ff556 100755 --- a/genesis-openembedded/build +++ b/genesis-openembedded/build @@ -64,6 +64,11 @@ $xcat_source = abs_path($xcat_source) or die "Cannot resolve xcat-core source\n" for my $path (qw(Version xCAT-genesis-builder/oe/build xCAT-genesis-builder/oe/export)) { die "xcat-core source is missing $path\n" unless -f "$xcat_source/$path"; } +for my $architecture (@requested_architectures) { + my $machine = "xCAT-genesis-builder/oe/kas/$architecture.yml"; + die "xcat-core source does not support Genesis architecture $architecture\n" + unless -f "$xcat_source/$machine"; +} die "xcat-core checkout is not clean\n" if capture_command('git', '-C', $xcat_source, 'status', '--porcelain') ne ''; diff --git a/lib/XCAT/GenesisRelease.pm b/lib/XCAT/GenesisRelease.pm index b51499b..308372c 100644 --- a/lib/XCAT/GenesisRelease.pm +++ b/lib/XCAT/GenesisRelease.pm @@ -178,9 +178,13 @@ sub _validate_release { unless $manifest->{source_date_epoch} =~ /^\d+$/; my @architectures = split(/,/, $manifest->{architectures}); + my %version_architecture = map { $_ => 1 } + @{ $RELEASE_ARCHITECTURES{ $manifest->{version} } }; my %seen_arch; for my $architecture (@architectures) { validate_architecture($architecture); + die "Genesis architecture $architecture is not valid in release version $manifest->{version}\n" + unless $version_architecture{$architecture}; die "Duplicate release architecture: $architecture\n" if $seen_arch{$architecture}++; } die "Release manifest has no architectures\n" unless @architectures; diff --git a/mockbuild-all.pl b/mockbuild-all.pl index f2a8339..50f0a8d 100755 --- a/mockbuild-all.pl +++ b/mockbuild-all.pl @@ -33,8 +33,9 @@ use XCAT::BuildUtils qw( shell_quote ); use XCAT::GenesisRelease qw( + architectures + read_release_manifest rpm_package_name - validate_complete_release validated_release_checksums verify_release_file ); @@ -373,11 +374,11 @@ if ($genesis_release ne '') { # before with the one taken after is what closes that window. my $checksums_before = validated_release_checksums($genesis_release); run_command($^X, $verifier, '--complete', '--format', 'rpm', $genesis_release); + my $manifest = read_release_manifest($genesis_release); my $checksums_after = validated_release_checksums($genesis_release); die "Genesis release changed during verification\n" unless hashes_equal($checksums_before, $checksums_after); $genesis_release_checksums = $checksums_before; - my $manifest = validate_complete_release($genesis_release); @genesis_release_architectures = split(/,/, $manifest->{architectures}); } @@ -1088,9 +1089,9 @@ sub publish_genesis_common_repo { =head3 verify_common_repo - Assert the shared OpenEmbedded Genesis repository carries every package the manifest's [common] - section requires, at a version satisfying its pin. [common] is not a build target: it describes - the one repository published beside the per-EL cells, which no [] section covers. + Assert the shared OpenEmbedded Genesis repository carries every package declared by the + verified release, at a version satisfying the [common] pin. [common] must describe every + currently supported Genesis architecture. Arguments: $dir - the repository to check (the staging directory, before it is swapped into place) @@ -1108,9 +1109,18 @@ sub verify_common_repo { die "FATAL: no [common] section in $manifest -- cannot verify the shared Genesis repository\n" if !%common; + my @supported_names = map { rpm_package_name($_) } architectures(); + my %supported = map { $_ => 1 } @supported_names; + my @manifest_missing = grep { !exists($common{$_}) } @supported_names; + my @manifest_unknown = grep { !$supported{$_} } sort keys %common; + die "FATAL: [common] is missing supported packages: @manifest_missing\n" + if @manifest_missing; + die "FATAL: [common] has unsupported packages: @manifest_unknown\n" + if @manifest_unknown; + + die "FATAL: Genesis release has no architectures\n" + unless @genesis_release_architectures; my @names = map { rpm_package_name($_) } @genesis_release_architectures; - my @missing = grep { !exists($common{$_}) } @names; - die "FATAL: [common] is missing release packages: @missing\n" if @missing; my %req = map { $_ => $common{$_} } @names; @names = sort @names; diff --git a/sbuild-all.pl b/sbuild-all.pl index a83edc6..dc40819 100755 --- a/sbuild-all.pl +++ b/sbuild-all.pl @@ -315,11 +315,11 @@ if ($genesis_release ne '') { require XCAT::GenesisRelease; my $before = XCAT::GenesisRelease::validated_release_checksums($genesis_release); XCAT::BuildUtils::run_command($^X, $verifier, '--complete', '--format', 'deb', $genesis_release); + my $release_manifest = XCAT::GenesisRelease::read_release_manifest($genesis_release); my $after = XCAT::GenesisRelease::validated_release_checksums($genesis_release); die "FATAL: Genesis release changed during verification\n" unless XCAT::BuildUtils::hashes_equal($before, $after); $genesis_release_checksums = $before; - my $release_manifest = XCAT::GenesisRelease::validate_complete_release($genesis_release); @genesis_release_architectures = split(/,/, $release_manifest->{architectures}); # Every suite's Packages index points into the shared Genesis pool, and publishing a release # replaces that pool -- so a run that rebuilt only some suites would leave the others indexing @@ -1050,21 +1050,30 @@ sub install_genesis_release_debs { return scalar(@files); } -# verify_shared_pool($pool): assert the shared Genesis pool carries every package the manifest's -# [shared] section requires, at a version satisfying its pin. [shared] is not a build target: it -# describes the one pool every suite indexes, which no [-] section covers. Run on -# the SIDE TREE, before it is swapped into place, so an incomplete pool is never published. -# Completeness only -- the release checksums cover the bytes. +# verify_shared_pool($pool): assert the pool carries every package declared by the verified release, +# at a version satisfying the [shared] pin. [shared] must cover every supported Genesis architecture. sub verify_shared_pool { my ($pool) = @_; my %shared = %{ $MANIFEST{shared} // {} }; die "FATAL: no [shared] section in $manifest -- cannot verify the shared Genesis pool\n" if !%shared; + + my @supported_names = map { + XCAT::GenesisRelease::deb_package_name($_) + } XCAT::GenesisRelease::architectures(); + my %supported = map { $_ => 1 } @supported_names; + my @manifest_missing = grep { !exists($shared{$_}) } @supported_names; + my @manifest_unknown = grep { !$supported{$_} } sort keys %shared; + die "FATAL: [shared] is missing supported packages: @manifest_missing\n" + if @manifest_missing; + die "FATAL: [shared] has unsupported packages: @manifest_unknown\n" + if @manifest_unknown; + + die "FATAL: Genesis release has no architectures\n" + unless @genesis_release_architectures; my @names = map { XCAT::GenesisRelease::deb_package_name($_) } @genesis_release_architectures; - my @missing = grep { !exists($shared{$_}) } @names; - die "FATAL: [shared] is missing release packages: @missing\n" if @missing; my %req = map { $_ => $shared{$_} } @names; @names = sort @names; my %present = map { $_ => deb_version($pool, $_) } @names; @@ -1480,10 +1489,10 @@ Publish an B alongside the packages this r release is produced separately (see F); this option only verifies it and copies the verified bytes into every selected suite. -The release must be B (every supported Genesis architecture) and must carry C -packages. It is validated before any build or publish: its C is read, the shared -verifier runs, and the checksums are read again -- a release rewritten together with its checksums -while the verifier runs is rejected. +The release must be complete for its manifest version and must carry C packages. Version 1 +requires seven architectures; version 2 also requires C. It is validated before any build or +publish: its C is read, the shared verifier runs, and the checksums are read again -- a +release rewritten together with its checksums while the verifier runs is rejected. The packages are published B, into F, and every suite's C index points at that one copy: they are C and identical everywhere, From be52c514d0d4e435580b69c7a9eeb22ecc0f9319 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:16:23 -0300 Subject: [PATCH 07/28] test(genesis): cover release manifest guards --- t/common-repo-gate.t | 41 +++++++++++++--- t/genesis_openembedded_consumer.t | 78 ++++++++++++++++++++++++++----- t/genesis_openembedded_release.t | 34 ++++++++++++-- 3 files changed, 132 insertions(+), 21 deletions(-) diff --git a/t/common-repo-gate.t b/t/common-repo-gate.t index e05e015..62c8217 100644 --- a/t/common-repo-gate.t +++ b/t/common-repo-gate.t @@ -49,7 +49,6 @@ if (@missing_requirements) { my $tmp = tempdir(CLEANUP => 1); my $target = 'alma+epel-10-' . do { my $m = `uname -m`; chomp $m; $m }; my @architectures = architectures(); -my $architecture_count = scalar(@architectures); my $xcat_version = '2.19.0'; my $xcat_release = 'snap202609040000'; my $xcat_revision = 'c' x 40; @@ -85,8 +84,9 @@ write_checksums($VERSION_1_RELEASE); { my %m = read_manifest("$RealBin/../packages-manifest.conf"); ok($m{common} && %{ $m{common} }, 'the shipped manifest has a [common] section'); - my @missing = grep { !exists $m{common}{ rpm_package_name($_) } } @architectures; - is_deeply(\@missing, [], + my @expected = sort map { rpm_package_name($_) } + qw(x86 x86_64 ppc64 ppc64le armv7hf aarch64 riscv64 s390x); + is_deeply([ sort keys %{ $m{common} } ], \@expected, 'the shared RPM manifest lists every Genesis architecture'); } @@ -166,19 +166,48 @@ sub run_publish { { my ($rc, $out, $common) = run_publish($RELEASE, 'full'); is($rc, 0, 'a complete release publishes') or diag($out); - is(scalar(grep { !/\.src\.rpm$/ } glob("$common/*.rpm")), $architecture_count, + is(scalar(grep { !/\.src\.rpm$/ } glob("$common/*.rpm")), 8, 'the published shared repo carries every architecture'); - like($out, qr/\[verify-repo\] common complete/, 'the shared repo is gated against [common]'); + like($out, qr/\[verify-repo\] common complete: 8 packages present/, + 'the shared repo is gated against [common]'); } { my ($rc, $out, $common) = run_publish($VERSION_1_RELEASE, 'version-1'); is($rc, 0, 'a complete version 1 release publishes') or diag($out); is(scalar(grep { !/\.src\.rpm$/ } glob("$common/*.rpm")), - scalar(@version_1_architectures), + 7, 'the version 1 repository keeps its seven architectures'); ok(!glob("$common/" . rpm_package_name('s390x') . '-*.rpm'), 'the version 1 repository does not require s390x'); + like($out, qr/\[verify-repo\] common complete: 7 packages present/, + 'the version 1 repository is gated against seven packages'); +} + +{ + my ($rc, $out, $common) = run_publish( + $VERSION_1_RELEASE, + 'missing-current-package', + sub { delete $_[0]->{ rpm_package_name('s390x') } }, + ); + isnt($rc, 0, 'a version 1 release does not hide an incomplete current manifest'); + like($out, qr/\[common\] is missing supported packages: .*s390x/, + 'the manifest failure identifies the missing current package'); + ok(!-d $common || !glob("$common/*.rpm"), + 'an incomplete current manifest publishes nothing'); +} + +{ + my ($rc, $out, $common) = run_publish( + $RELEASE, + 'unknown-current-package', + sub { $_[0]->{'xCAT-genesis-openembedded-unknown'} = '>= 2.18.0' }, + ); + isnt($rc, 0, 'an unknown shared manifest package is refused'); + like($out, qr/\[common\] has unsupported packages: xCAT-genesis-openembedded-unknown/, + 'the manifest failure identifies the unknown package'); + ok(!-d $common || !glob("$common/*.rpm"), + 'an unknown shared manifest package publishes nothing'); } # ---- an incomplete release is refused, and publishes nothing -------------------------------------- diff --git a/t/genesis_openembedded_consumer.t b/t/genesis_openembedded_consumer.t index aeec525..72559b1 100644 --- a/t/genesis_openembedded_consumer.t +++ b/t/genesis_openembedded_consumer.t @@ -75,7 +75,7 @@ SKIP: { } SKIP: { - skip 'APT repository tools are not installed', 62 + skip 'APT repository tools are not installed', 69 unless $^O eq 'linux' && command_exists('dpkg-deb') && command_exists('apt-ftparchive'); @@ -301,14 +301,7 @@ sub run_apt_consumer { my $manifest = $args{manifest}; unless ($manifest) { $manifest = "$args{output}/manifest.conf"; - make_path($args{output}); - # plus the shipped [shared] section verbatim: publishing a release gates the shared pool - # against it, and a manifest without it is refused rather than silently ungated. - my $shipped = read_binary("$repo_root/debs-manifest.conf"); - my ($shared) = $shipped =~ /^(\[shared\]\n(?:[^\[]*))/ms; - BAIL_OUT('debs-manifest.conf has no [shared] section') unless $shared; - write_binary($manifest, - join('', map { "[$_-amd64]\nxcat-genesis-base=*\n" } @APT_SUITES) . "\n" . $shared); + write_apt_manifest($manifest); } return run_capture( $args{log}, @@ -326,6 +319,23 @@ sub run_apt_consumer { ); } +sub write_apt_manifest { + my ($path, $mutate) = @_; + my $shipped = read_binary("$repo_root/debs-manifest.conf"); + my ($section) = $shipped =~ /^\[shared\]\n([^\[]*)/ms; + BAIL_OUT('debs-manifest.conf has no [shared] section') unless defined $section; + my %shared = map { /^([^=]+)=(.*)$/ ? ($1 => $2) : () } + grep { length } split(/\n/, $section); + $mutate->(\%shared) if $mutate; + make_path((File::Basename::dirname($path))); + write_binary( + $path, + join('', map { "[$_-amd64]\nxcat-genesis-base=*\n" } @APT_SUITES) + . "\n[shared]\n" + . join('', map { "$_=$shared{$_}\n" } sort keys %shared), + ); +} + # Stage one legacy genesis-base deb for the noble suite only -- enough for the runs that publish # no Genesis release. sub stage_legacy_deb { @@ -398,9 +408,8 @@ sub test_deb_consumer { } architectures(); is_deeply([ genesis_deb_names($shared_pool) ], \@expected_packages, 'shared APT pool contains one complete Genesis release'); - my $architecture_count = scalar(architectures()); like(read_binary($log), - qr/\[verify-repo\] shared pool complete: \Q$architecture_count\E packages present/, + qr/\[verify-repo\] shared pool complete: 8 packages present/, 'the shared pool is gated against the manifest\'s [shared] section'); my @suite_packages; for my $codename (@APT_SUITES) { @@ -549,10 +558,55 @@ sub test_version_1_deb_consumer { my $pool = "$apt_root/pool/main/xcat-genesis-openembedded"; is($status, 0, 'APT accepts a complete version 1 release'); - is(scalar(genesis_deb_names($pool)), scalar(@release_architectures), + is(scalar(genesis_deb_names($pool)), 7, 'the version 1 pool keeps its seven architectures'); is(scalar(grep { /s390x/ } genesis_deb_names($pool)), 0, 'the version 1 pool does not require s390x'); + like(read_binary($log), + qr/\[verify-repo\] shared pool complete: 7 packages present/, + 'the version 1 pool is gated against seven packages'); + + my $missing_manifest = "$tmp/deb-version-1-missing.conf"; + write_apt_manifest( + $missing_manifest, + sub { delete $_[0]->{ deb_package_name('s390x') } }, + ); + my $missing_apt = "$tmp/apt-version-1-missing"; + my $missing_output = "$tmp/deb-version-1-missing-output"; + stage_apt_suites($missing_output, "$tmp/deb-version-1-missing-legacy"); + my $missing_log = "$tmp/deb-version-1-missing.log"; + my $missing_status = run_apt_consumer( + log => $missing_log, output => $missing_output, apt_dir => $missing_apt, + manifest => $missing_manifest, + extra => [ '--genesis-release', $release_root ], + ); + isnt($missing_status, 0, + 'a version 1 release does not hide an incomplete current shared manifest'); + like(read_binary($missing_log), qr/\[shared\] is missing supported packages: .*s390x/, + 'the shared manifest failure identifies the missing current package'); + ok(!-d "$missing_apt/pool/main/xcat-genesis-openembedded", + 'an incomplete shared manifest publishes nothing'); + + my $unknown_manifest = "$tmp/deb-version-1-unknown.conf"; + write_apt_manifest( + $unknown_manifest, + sub { $_[0]->{'xcat-genesis-openembedded-unknown'} = '2.*' }, + ); + my $unknown_apt = "$tmp/apt-version-1-unknown"; + my $unknown_output = "$tmp/deb-version-1-unknown-output"; + stage_apt_suites($unknown_output, "$tmp/deb-version-1-unknown-legacy"); + my $unknown_log = "$tmp/deb-version-1-unknown.log"; + my $unknown_status = run_apt_consumer( + log => $unknown_log, output => $unknown_output, apt_dir => $unknown_apt, + manifest => $unknown_manifest, + extra => [ '--genesis-release', $release_root ], + ); + isnt($unknown_status, 0, 'an unknown shared manifest package is refused'); + like(read_binary($unknown_log), + qr/\[shared\] has unsupported packages: xcat-genesis-openembedded-unknown/, + 'the shared manifest failure identifies the unknown package'); + ok(!-d "$unknown_apt/pool/main/xcat-genesis-openembedded", + 'an unknown shared manifest package publishes nothing'); } sub test_signed_common_rpm_repository { diff --git a/t/genesis_openembedded_release.t b/t/genesis_openembedded_release.t index 68cb60a..b1adf4e 100644 --- a/t/genesis_openembedded_release.t +++ b/t/genesis_openembedded_release.t @@ -216,6 +216,19 @@ write_checksums($legacy_release); ok(validate_complete_release($legacy_release), 'complete version 1 releases remain publishable'); +my $invalid_legacy_release = "$tmp/invalid-legacy-release"; +copy_tree($complete_release, $invalid_legacy_release); +write_release_manifest( + $invalid_legacy_release, $version, $release, $revision, $epoch, + join(',', architectures()), 'deb,rpm', 1, +); +write_checksums($invalid_legacy_release); +dies_like( + sub { validate_release($invalid_legacy_release) }, + qr/Genesis architecture s390x is not valid in release version 1/, + 'version 1 rejects the version 2 architecture vocabulary', +); + my $unknown_release_version = "$tmp/unknown-release-version"; copy_tree($complete_release, $unknown_release_version); write_release_manifest( @@ -259,12 +272,13 @@ dies_like(sub { validate_release($missing_release) }, qr/Genesis release is miss 'incomplete architecture set fails'); SKIP: { - skip 'git is not installed', 2 unless command_exists('git'); + skip 'git is not installed', 4 unless command_exists('git'); my $source = "$tmp/dirty-xcat-core"; - make_path("$source/xCAT-genesis-builder/oe"); + make_path("$source/xCAT-genesis-builder/oe/kas"); write_binary("$source/Version", "$version\n"); write_binary("$source/xCAT-genesis-builder/oe/build", "#!/bin/sh\nexit 99\n"); write_binary("$source/xCAT-genesis-builder/oe/export", "#!/bin/sh\nexit 99\n"); + write_binary("$source/xCAT-genesis-builder/oe/kas/x86_64.yml", "header: {}\n"); for my $command ( [ 'git', '-C', $source, 'init', '-q' ], [ 'git', '-C', $source, 'add', '.' ], @@ -286,6 +300,19 @@ SKIP: { ); like(read_binary($log), qr/xcat-core checkout is not clean/, 'dirty checkout failure is explicit'); + + my $target_log = "$tmp/missing-target.log"; + isnt( + run_capture( + $target_log, $builder, '--xcat-source', $source, + '--architecture', 's390x', + '--output-dir', "$tmp/missing-target-output", + ), + 0, + 'release builder rejects an unsupported xcat-core target', + ); + like(read_binary($target_log), qr/does not support Genesis architecture s390x/, + 'missing target failure identifies the required xcat-core support'); } SKIP: { @@ -484,8 +511,9 @@ sub exercise_packager_from_unsearchable_cwd { sub exercise_builder_tmpdir { my $source = "$tmp/tmpdir-xcat-core"; my $oe = "$source/xCAT-genesis-builder/oe"; - make_path($oe); + make_path("$oe/kas"); write_binary("$source/Version", "$version\n"); + write_binary("$oe/kas/x86_64.yml", "header: {}\n"); write_binary( "$oe/build", <<'BUILD', From a7a7a1898da0bd89404e1fece81eebbe01016996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:16:23 -0300 Subject: [PATCH 08/28] docs(genesis): explain release compatibility --- BUILD.md | 6 +++--- genesis-openembedded/README.md | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/BUILD.md b/BUILD.md index 322095b..4bfbffe 100644 --- a/BUILD.md +++ b/BUILD.md @@ -247,9 +247,9 @@ generation, and signing have succeeded. If a stopped publisher leaves staging or backup directories behind, rerun it with ``--force-unlock`` to recover the previous repository before starting a new publication. -Repository publication requires a release containing every supported Genesis -architecture. The packages are `noarch`, and the common repository contains -the full set of target images. +Repository publication requires a complete release for its manifest version. +Version 1 contains seven architectures; version 2 also requires `s390x`. The +packages are `noarch`, and each publication contains that release's full set. The release checksums cover the unsigned input packages. If repository signing is enabled, `rpmsign` changes the deployed RPM bytes after collection. diff --git a/genesis-openembedded/README.md b/genesis-openembedded/README.md index b67e569..eef1b7e 100644 --- a/genesis-openembedded/README.md +++ b/genesis-openembedded/README.md @@ -90,8 +90,10 @@ interrupted publication leaves the previous repository exactly as it was. Both consumers verify package identities and checksums before publication. Version 2 releases require all eight architectures. Seven-architecture version -1 releases remain publishable. A management node can install an image for a -different target architecture. +1 releases remain publishable. Building a version 2 release requires an +xcat-core revision with the `s390x` OpenEmbedded target. Merge or deploy that +xcat-core change first. A management node can install an image for a different +target architecture. Without `--genesis-release`, both builders keep their existing behavior. The new packages do not provide, replace, or obsolete the old package names. From 2b5ef0ebe78f5d0ec6f00ce042dab81eac866477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:54:13 -0300 Subject: [PATCH 09/28] fix(genesis): use the build architecture contract --- genesis-openembedded/build | 10 ++++++++-- mockbuild-all.pl | 10 +++++++++- sbuild-all.pl | 12 +++++++++++- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/genesis-openembedded/build b/genesis-openembedded/build index 68ff556..e6e7b84 100755 --- a/genesis-openembedded/build +++ b/genesis-openembedded/build @@ -64,10 +64,16 @@ $xcat_source = abs_path($xcat_source) or die "Cannot resolve xcat-core source\n" for my $path (qw(Version xCAT-genesis-builder/oe/build xCAT-genesis-builder/oe/export)) { die "xcat-core source is missing $path\n" unless -f "$xcat_source/$path"; } +my $architecture_reporter = "$xcat_source/xCAT-genesis-builder/oe/build"; +my $supported_output = eval { + capture_command($architecture_reporter, '--list-architectures'); +}; +die "xcat-core source does not report supported Genesis architectures\n" + if !defined($supported_output) || $@; +my %supported_architecture = map { $_ => 1 } split(/\s+/, $supported_output); for my $architecture (@requested_architectures) { - my $machine = "xCAT-genesis-builder/oe/kas/$architecture.yml"; die "xcat-core source does not support Genesis architecture $architecture\n" - unless -f "$xcat_source/$machine"; + unless $supported_architecture{$architecture}; } die "xcat-core checkout is not clean\n" if capture_command('git', '-C', $xcat_source, 'status', '--porcelain') ne ''; diff --git a/mockbuild-all.pl b/mockbuild-all.pl index 50f0a8d..90cc259 100755 --- a/mockbuild-all.pl +++ b/mockbuild-all.pl @@ -112,6 +112,7 @@ my $skip_tarball = 0; my $genesis_release = ''; my $genesis_release_checksums; my @genesis_release_architectures; +my $genesis_release_version; my $scrub_all_chroots = 0; my $keep_buildroots = 0; # keep per-step mock chroots after build (default: --scrub=chroot each) my $dry_run = 0; @@ -380,6 +381,7 @@ if ($genesis_release ne '') { unless hashes_equal($checksums_before, $checksums_after); $genesis_release_checksums = $checksums_before; @genesis_release_architectures = split(/,/, $manifest->{architectures}); + $genesis_release_version = $manifest->{version}; } # An explicit --target builds just that target; otherwise build the current host @@ -1112,7 +1114,9 @@ sub verify_common_repo { my @supported_names = map { rpm_package_name($_) } architectures(); my %supported = map { $_ => 1 } @supported_names; my @manifest_missing = grep { !exists($common{$_}) } @supported_names; - my @manifest_unknown = grep { !$supported{$_} } sort keys %common; + my @manifest_unknown = grep { + /^xCAT-genesis-openembedded-/ && !$supported{$_} + } sort keys %common; die "FATAL: [common] is missing supported packages: @manifest_missing\n" if @manifest_missing; die "FATAL: [common] has unsupported packages: @manifest_unknown\n" @@ -1121,6 +1125,10 @@ sub verify_common_repo { die "FATAL: Genesis release has no architectures\n" unless @genesis_release_architectures; my @names = map { rpm_package_name($_) } @genesis_release_architectures; + my %release_architecture = map { $_ => 1 } @genesis_release_architectures; + my @omitted = grep { !$release_architecture{$_} } architectures(); + print "WARNING: Genesis release version $genesis_release_version omits current architectures: @omitted\n" + if @omitted; my %req = map { $_ => $common{$_} } @names; @names = sort @names; diff --git a/sbuild-all.pl b/sbuild-all.pl index dc40819..4f8ce73 100755 --- a/sbuild-all.pl +++ b/sbuild-all.pl @@ -101,6 +101,7 @@ my $gpg_home = ''; my $genesis_release = ''; # OpenEmbedded Genesis package release to publish alongside my $genesis_release_checksums; # its verified SHA256SUMS, read once at startup my @genesis_release_architectures; +my $genesis_release_version; # The OpenEmbedded Genesis debs are published ONCE, in a pool of their own that every suite indexes. # They are Architecture:all and identical for all suites, so a per-suite copy would multiply hundreds # of megabytes by the number of codenames for no gain. @@ -321,6 +322,7 @@ if ($genesis_release ne '') { unless XCAT::BuildUtils::hashes_equal($before, $after); $genesis_release_checksums = $before; @genesis_release_architectures = split(/,/, $release_manifest->{architectures}); + $genesis_release_version = $release_manifest->{version}; # Every suite's Packages index points into the shared Genesis pool, and publishing a release # replaces that pool -- so a run that rebuilt only some suites would leave the others indexing # files that no longer exist. Publish a release for all of them or for none. @@ -1063,7 +1065,9 @@ sub verify_shared_pool { } XCAT::GenesisRelease::architectures(); my %supported = map { $_ => 1 } @supported_names; my @manifest_missing = grep { !exists($shared{$_}) } @supported_names; - my @manifest_unknown = grep { !$supported{$_} } sort keys %shared; + my @manifest_unknown = grep { + /^xcat-genesis-openembedded-/ && !$supported{$_} + } sort keys %shared; die "FATAL: [shared] is missing supported packages: @manifest_missing\n" if @manifest_missing; die "FATAL: [shared] has unsupported packages: @manifest_unknown\n" @@ -1074,6 +1078,12 @@ sub verify_shared_pool { my @names = map { XCAT::GenesisRelease::deb_package_name($_) } @genesis_release_architectures; + my %release_architecture = map { $_ => 1 } @genesis_release_architectures; + my @omitted = grep { + !$release_architecture{$_} + } XCAT::GenesisRelease::architectures(); + print "WARNING: Genesis release version $genesis_release_version omits current architectures: @omitted\n" + if @omitted; my %req = map { $_ => $shared{$_} } @names; @names = sort @names; my %present = map { $_ => deb_version($pool, $_) } @names; From 7f750638e46f27b39199505e954eb88380cbe769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:54:13 -0300 Subject: [PATCH 10/28] test(genesis): cover release compatibility notices --- t/common-repo-gate.t | 8 +++++++- t/genesis_openembedded_consumer.t | 22 ++++++++++++++++------ t/genesis_openembedded_release.t | 15 ++++++++++++--- 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/t/common-repo-gate.t b/t/common-repo-gate.t index 62c8217..5093e10 100644 --- a/t/common-repo-gate.t +++ b/t/common-repo-gate.t @@ -164,7 +164,11 @@ sub run_publish { # ---- a complete release publishes, and says it was gated ----------------------------------------- { - my ($rc, $out, $common) = run_publish($RELEASE, 'full'); + my ($rc, $out, $common) = run_publish( + $RELEASE, + 'full', + sub { $_[0]->{'xCAT-release'} = '>= 2.0.0' }, + ); is($rc, 0, 'a complete release publishes') or diag($out); is(scalar(grep { !/\.src\.rpm$/ } glob("$common/*.rpm")), 8, 'the published shared repo carries every architecture'); @@ -182,6 +186,8 @@ sub run_publish { 'the version 1 repository does not require s390x'); like($out, qr/\[verify-repo\] common complete: 7 packages present/, 'the version 1 repository is gated against seven packages'); + like($out, qr/WARNING: Genesis release version 1 omits current architectures: s390x/, + 'version 1 publication reports its reduced architecture set'); } { diff --git a/t/genesis_openembedded_consumer.t b/t/genesis_openembedded_consumer.t index 72559b1..fab496a 100644 --- a/t/genesis_openembedded_consumer.t +++ b/t/genesis_openembedded_consumer.t @@ -12,8 +12,10 @@ use POSIX (); use Test::More; use Time::HiRes qw(sleep); +use lib "$FindBin::Bin/.."; use lib "$FindBin::Bin/../lib"; use lib "$FindBin::Bin/lib"; +use BuildUtils qw(read_manifest); use XCAT::BuildUtils qw( capture_command command_exists @@ -75,7 +77,7 @@ SKIP: { } SKIP: { - skip 'APT repository tools are not installed', 69 + skip 'APT repository tools are not installed', 70 unless $^O eq 'linux' && command_exists('dpkg-deb') && command_exists('apt-ftparchive'); @@ -321,11 +323,10 @@ sub run_apt_consumer { sub write_apt_manifest { my ($path, $mutate) = @_; - my $shipped = read_binary("$repo_root/debs-manifest.conf"); - my ($section) = $shipped =~ /^\[shared\]\n([^\[]*)/ms; - BAIL_OUT('debs-manifest.conf has no [shared] section') unless defined $section; - my %shared = map { /^([^=]+)=(.*)$/ ? ($1 => $2) : () } - grep { length } split(/\n/, $section); + my %shipped = read_manifest("$repo_root/debs-manifest.conf"); + BAIL_OUT('debs-manifest.conf has no [shared] section') + unless exists $shipped{shared}; + my %shared = %{ $shipped{shared} }; $mutate->(\%shared) if $mutate; make_path((File::Basename::dirname($path))); write_binary( @@ -386,8 +387,14 @@ sub test_deb_consumer { local $ENV{SOURCE_DATE_EPOCH} = $epoch; my $log = "$tmp/deb-consumer.log"; + my $manifest = "$tmp/deb-consumer.conf"; + write_apt_manifest( + $manifest, + sub { $_[0]->{'xcat-release'} = '2.*' }, + ); my $status = run_apt_consumer( log => $log, output => $output, apt_dir => $apt_root, + manifest => $manifest, extra => [ '--genesis-release', $release_root ], ); my $pool_package = "$shared_pool/$package"; @@ -565,6 +572,9 @@ sub test_version_1_deb_consumer { like(read_binary($log), qr/\[verify-repo\] shared pool complete: 7 packages present/, 'the version 1 pool is gated against seven packages'); + like(read_binary($log), + qr/WARNING: Genesis release version 1 omits current architectures: s390x/, + 'version 1 APT publication reports its reduced architecture set'); my $missing_manifest = "$tmp/deb-version-1-missing.conf"; write_apt_manifest( diff --git a/t/genesis_openembedded_release.t b/t/genesis_openembedded_release.t index b1adf4e..e7653a8 100644 --- a/t/genesis_openembedded_release.t +++ b/t/genesis_openembedded_release.t @@ -274,11 +274,20 @@ dies_like(sub { validate_release($missing_release) }, qr/Genesis release is miss SKIP: { skip 'git is not installed', 4 unless command_exists('git'); my $source = "$tmp/dirty-xcat-core"; - make_path("$source/xCAT-genesis-builder/oe/kas"); + make_path("$source/xCAT-genesis-builder/oe"); write_binary("$source/Version", "$version\n"); - write_binary("$source/xCAT-genesis-builder/oe/build", "#!/bin/sh\nexit 99\n"); + write_binary( + "$source/xCAT-genesis-builder/oe/build", + "#!/bin/sh\n" + . "if [ \"\${1-}\" = --list-architectures ]; then\n" + . " printf '%s\\n' x86_64\n" + . " exit 0\n" + . "fi\n" + . "exit 99\n", + ); + chmod(0755, "$source/xCAT-genesis-builder/oe/build") + or die "Cannot make fixture build executable: $!"; write_binary("$source/xCAT-genesis-builder/oe/export", "#!/bin/sh\nexit 99\n"); - write_binary("$source/xCAT-genesis-builder/oe/kas/x86_64.yml", "header: {}\n"); for my $command ( [ 'git', '-C', $source, 'init', '-q' ], [ 'git', '-C', $source, 'add', '.' ], From ed1b7661e254227930cf062b1766304e42a0dac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:55:45 -0300 Subject: [PATCH 11/28] test(genesis): update builder fixtures --- t/genesis_openembedded_release.t | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/t/genesis_openembedded_release.t b/t/genesis_openembedded_release.t index e7653a8..f14f442 100644 --- a/t/genesis_openembedded_release.t +++ b/t/genesis_openembedded_release.t @@ -520,14 +520,17 @@ sub exercise_packager_from_unsearchable_cwd { sub exercise_builder_tmpdir { my $source = "$tmp/tmpdir-xcat-core"; my $oe = "$source/xCAT-genesis-builder/oe"; - make_path("$oe/kas"); + make_path($oe); write_binary("$source/Version", "$version\n"); - write_binary("$oe/kas/x86_64.yml", "header: {}\n"); write_binary( "$oe/build", <<'BUILD', #!/bin/sh set -eu +if [ "${1-}" = --list-architectures ]; then + printf '%s\n' x86_64 + exit 0 +fi expected=$XCAT_GENESIS_WORK_DIR/build/tmp [ "${TMPDIR:-}" = "$expected" ] || exit 41 mkdir -p "$TMPDIR/deploy" From cd6bcaccbdf3b575819a5894a095187e97ba4f75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:15:23 -0300 Subject: [PATCH 12/28] fix(genesis): refuse incomplete current releases --- genesis-openembedded/build | 23 ++++++++++++---------- mockbuild-all.pl | 26 +++++++++---------------- sbuild-all.pl | 39 ++++++++++++++++---------------------- 3 files changed, 38 insertions(+), 50 deletions(-) diff --git a/genesis-openembedded/build b/genesis-openembedded/build index e6e7b84..e5e3edc 100755 --- a/genesis-openembedded/build +++ b/genesis-openembedded/build @@ -64,16 +64,19 @@ $xcat_source = abs_path($xcat_source) or die "Cannot resolve xcat-core source\n" for my $path (qw(Version xCAT-genesis-builder/oe/build xCAT-genesis-builder/oe/export)) { die "xcat-core source is missing $path\n" unless -f "$xcat_source/$path"; } -my $architecture_reporter = "$xcat_source/xCAT-genesis-builder/oe/build"; -my $supported_output = eval { - capture_command($architecture_reporter, '--list-architectures'); -}; -die "xcat-core source does not report supported Genesis architectures\n" - if !defined($supported_output) || $@; -my %supported_architecture = map { $_ => 1 } split(/\s+/, $supported_output); -for my $architecture (@requested_architectures) { - die "xcat-core source does not support Genesis architecture $architecture\n" - unless $supported_architecture{$architecture}; +if ($requested{s390x}) { + my $architecture_reporter = "$xcat_source/xCAT-genesis-builder/oe/build"; + my $supported_output = eval { + capture_command($architecture_reporter, '--list-architectures'); + }; + my $report_error = $@; + die "xcat-core source does not report supported Genesis architectures: $report_error" + if $report_error; + die "xcat-core source reported no supported Genesis architectures\n" + unless defined($supported_output) && length($supported_output); + my %supported_architecture = map { $_ => 1 } split(/\s+/, $supported_output); + die "xcat-core source does not support Genesis architecture s390x\n" + unless $supported_architecture{s390x}; } die "xcat-core checkout is not clean\n" if capture_command('git', '-C', $xcat_source, 'status', '--porcelain') ne ''; diff --git a/mockbuild-all.pl b/mockbuild-all.pl index 90cc259..dd2456e 100755 --- a/mockbuild-all.pl +++ b/mockbuild-all.pl @@ -111,8 +111,6 @@ my $skip_createrepo = 0; my $skip_tarball = 0; my $genesis_release = ''; my $genesis_release_checksums; -my @genesis_release_architectures; -my $genesis_release_version; my $scrub_all_chroots = 0; my $keep_buildroots = 0; # keep per-step mock chroots after build (default: --scrub=chroot each) my $dry_run = 0; @@ -379,9 +377,12 @@ if ($genesis_release ne '') { my $checksums_after = validated_release_checksums($genesis_release); die "Genesis release changed during verification\n" unless hashes_equal($checksums_before, $checksums_after); + my %release_architecture = map { $_ => 1 } + split(/,/, $manifest->{architectures}); + my @omitted = grep { !$release_architecture{$_} } architectures(); + die "Genesis release version $manifest->{version} omits currently supported architectures: @omitted\n" + if @omitted; $genesis_release_checksums = $checksums_before; - @genesis_release_architectures = split(/,/, $manifest->{architectures}); - $genesis_release_version = $manifest->{version}; } # An explicit --target builds just that target; otherwise build the current host @@ -1091,9 +1092,8 @@ sub publish_genesis_common_repo { =head3 verify_common_repo - Assert the shared OpenEmbedded Genesis repository carries every package declared by the - verified release, at a version satisfying the [common] pin. [common] must describe every - currently supported Genesis architecture. + Assert the shared repository carries every package required by [common]. [common] must + describe every currently supported Genesis architecture. Arguments: $dir - the repository to check (the staging directory, before it is swapped into place) @@ -1122,16 +1122,8 @@ sub verify_common_repo { die "FATAL: [common] has unsupported packages: @manifest_unknown\n" if @manifest_unknown; - die "FATAL: Genesis release has no architectures\n" - unless @genesis_release_architectures; - my @names = map { rpm_package_name($_) } @genesis_release_architectures; - my %release_architecture = map { $_ => 1 } @genesis_release_architectures; - my @omitted = grep { !$release_architecture{$_} } architectures(); - print "WARNING: Genesis release version $genesis_release_version omits current architectures: @omitted\n" - if @omitted; - my %req = map { $_ => $common{$_} } @names; - - @names = sort @names; + my %req = %common; + my @names = sort keys %req; my %present = repo_present_versions($dir, \@names); my %present_evr = map { $_ => rpm_evr($dir, $_) } @names; my @problems = verify_repo_packages(\%req, \%present, \%present_evr, \&rpm_vercmp_segment); diff --git a/sbuild-all.pl b/sbuild-all.pl index 4f8ce73..0d47246 100755 --- a/sbuild-all.pl +++ b/sbuild-all.pl @@ -100,8 +100,6 @@ my $gpg_key_id = 'xcat@megware.com'; my $gpg_home = ''; my $genesis_release = ''; # OpenEmbedded Genesis package release to publish alongside my $genesis_release_checksums; # its verified SHA256SUMS, read once at startup -my @genesis_release_architectures; -my $genesis_release_version; # The OpenEmbedded Genesis debs are published ONCE, in a pool of their own that every suite indexes. # They are Architecture:all and identical for all suites, so a per-suite copy would multiply hundreds # of megabytes by the number of codenames for no gain. @@ -320,9 +318,14 @@ if ($genesis_release ne '') { my $after = XCAT::GenesisRelease::validated_release_checksums($genesis_release); die "FATAL: Genesis release changed during verification\n" unless XCAT::BuildUtils::hashes_equal($before, $after); + my %release_architecture = map { $_ => 1 } + split(/,/, $release_manifest->{architectures}); + my @omitted = grep { + !$release_architecture{$_} + } XCAT::GenesisRelease::architectures(); + die "FATAL: Genesis release version $release_manifest->{version} omits currently supported architectures: @omitted\n" + if @omitted; $genesis_release_checksums = $before; - @genesis_release_architectures = split(/,/, $release_manifest->{architectures}); - $genesis_release_version = $release_manifest->{version}; # Every suite's Packages index points into the shared Genesis pool, and publishing a release # replaces that pool -- so a run that rebuilt only some suites would leave the others indexing # files that no longer exist. Publish a release for all of them or for none. @@ -1052,8 +1055,8 @@ sub install_genesis_release_debs { return scalar(@files); } -# verify_shared_pool($pool): assert the pool carries every package declared by the verified release, -# at a version satisfying the [shared] pin. [shared] must cover every supported Genesis architecture. +# verify_shared_pool($pool): assert the pool satisfies [shared]. [shared] must cover every supported +# Genesis architecture. sub verify_shared_pool { my ($pool) = @_; my %shared = %{ $MANIFEST{shared} // {} }; @@ -1073,19 +1076,8 @@ sub verify_shared_pool { die "FATAL: [shared] has unsupported packages: @manifest_unknown\n" if @manifest_unknown; - die "FATAL: Genesis release has no architectures\n" - unless @genesis_release_architectures; - my @names = map { - XCAT::GenesisRelease::deb_package_name($_) - } @genesis_release_architectures; - my %release_architecture = map { $_ => 1 } @genesis_release_architectures; - my @omitted = grep { - !$release_architecture{$_} - } XCAT::GenesisRelease::architectures(); - print "WARNING: Genesis release version $genesis_release_version omits current architectures: @omitted\n" - if @omitted; - my %req = map { $_ => $shared{$_} } @names; - @names = sort @names; + my %req = %shared; + my @names = sort keys %req; my %present = map { $_ => deb_version($pool, $_) } @names; my @problems = verify_repo_packages(\%req, \%present); if (@problems) { @@ -1499,10 +1491,11 @@ Publish an B alongside the packages this r release is produced separately (see F); this option only verifies it and copies the verified bytes into every selected suite. -The release must be complete for its manifest version and must carry C packages. Version 1 -requires seven architectures; version 2 also requires C. It is validated before any build or -publish: its C is read, the shared verifier runs, and the checksums are read again -- a -release rewritten together with its checksums while the verifier runs is rejected. +The release must carry C packages for every currently supported architecture. Version 1 +remains readable but cannot replace the current eight-architecture repository because it lacks +C. The release is validated before any build or publish: its C is read, the +shared verifier runs, and the checksums are read again -- a release rewritten together with its +checksums while the verifier runs is rejected. The packages are published B, into F, and every suite's C index points at that one copy: they are C and identical everywhere, From f522398456476ecaa0fc30e291f820f34584d771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:15:24 -0300 Subject: [PATCH 13/28] test(genesis): cover repository downgrades --- t/common-repo-gate.t | 36 ++++++++++++---------- t/genesis_openembedded_consumer.t | 50 ++++++++++++++++++++----------- 2 files changed, 52 insertions(+), 34 deletions(-) diff --git a/t/common-repo-gate.t b/t/common-repo-gate.t index 5093e10..dd2b8f7 100644 --- a/t/common-repo-gate.t +++ b/t/common-repo-gate.t @@ -164,11 +164,7 @@ sub run_publish { # ---- a complete release publishes, and says it was gated ----------------------------------------- { - my ($rc, $out, $common) = run_publish( - $RELEASE, - 'full', - sub { $_[0]->{'xCAT-release'} = '>= 2.0.0' }, - ); + my ($rc, $out, $common) = run_publish($RELEASE, 'full'); is($rc, 0, 'a complete release publishes') or diag($out); is(scalar(grep { !/\.src\.rpm$/ } glob("$common/*.rpm")), 8, 'the published shared repo carries every architecture'); @@ -178,21 +174,29 @@ sub run_publish { { my ($rc, $out, $common) = run_publish($VERSION_1_RELEASE, 'version-1'); - is($rc, 0, 'a complete version 1 release publishes') or diag($out); - is(scalar(grep { !/\.src\.rpm$/ } glob("$common/*.rpm")), - 7, - 'the version 1 repository keeps its seven architectures'); - ok(!glob("$common/" . rpm_package_name('s390x') . '-*.rpm'), - 'the version 1 repository does not require s390x'); - like($out, qr/\[verify-repo\] common complete: 7 packages present/, - 'the version 1 repository is gated against seven packages'); - like($out, qr/WARNING: Genesis release version 1 omits current architectures: s390x/, - 'version 1 publication reports its reduced architecture set'); + isnt($rc, 0, 'a version 1 release cannot replace the current repository'); + like($out, qr/Genesis release version 1 omits currently supported architectures: s390x/, + 'version 1 refusal identifies the missing architecture'); + ok(!-d $common || !glob("$common/*.rpm"), + 'a version 1 release publishes nothing'); } { my ($rc, $out, $common) = run_publish( - $VERSION_1_RELEASE, + $RELEASE, + 'missing-non-genesis-package', + sub { $_[0]->{'xCAT-release'} = '>= 2.0.0' }, + ); + isnt($rc, 0, 'every common manifest package is verified'); + like($out, qr/MISSING xCAT-release/, + 'the common gate identifies a missing non-Genesis package'); + ok(!-d $common || !glob("$common/*.rpm"), + 'a missing non-Genesis package prevents publication'); +} + +{ + my ($rc, $out, $common) = run_publish( + $RELEASE, 'missing-current-package', sub { delete $_[0]->{ rpm_package_name('s390x') } }, ); diff --git a/t/genesis_openembedded_consumer.t b/t/genesis_openembedded_consumer.t index fab496a..1a36144 100644 --- a/t/genesis_openembedded_consumer.t +++ b/t/genesis_openembedded_consumer.t @@ -387,14 +387,8 @@ sub test_deb_consumer { local $ENV{SOURCE_DATE_EPOCH} = $epoch; my $log = "$tmp/deb-consumer.log"; - my $manifest = "$tmp/deb-consumer.conf"; - write_apt_manifest( - $manifest, - sub { $_[0]->{'xcat-release'} = '2.*' }, - ); my $status = run_apt_consumer( log => $log, output => $output, apt_dir => $apt_root, - manifest => $manifest, extra => [ '--genesis-release', $release_root ], ); my $pool_package = "$shared_pool/$package"; @@ -564,17 +558,15 @@ sub test_version_1_deb_consumer { ); my $pool = "$apt_root/pool/main/xcat-genesis-openembedded"; - is($status, 0, 'APT accepts a complete version 1 release'); - is(scalar(genesis_deb_names($pool)), 7, - 'the version 1 pool keeps its seven architectures'); - is(scalar(grep { /s390x/ } genesis_deb_names($pool)), 0, - 'the version 1 pool does not require s390x'); + isnt($status, 0, 'APT refuses a version 1 release for the current repository'); like(read_binary($log), - qr/\[verify-repo\] shared pool complete: 7 packages present/, - 'the version 1 pool is gated against seven packages'); - like(read_binary($log), - qr/WARNING: Genesis release version 1 omits current architectures: s390x/, - 'version 1 APT publication reports its reduced architecture set'); + qr/Genesis release version 1 omits currently supported architectures: s390x/, + 'the version 1 refusal identifies the missing architecture'); + ok(!-d $pool, 'a version 1 release publishes no shared pool'); + + my $current_release = make_package_release( + "$tmp/deb-current-manifest", 'deb', architectures(), + ); my $missing_manifest = "$tmp/deb-version-1-missing.conf"; write_apt_manifest( @@ -588,7 +580,7 @@ sub test_version_1_deb_consumer { my $missing_status = run_apt_consumer( log => $missing_log, output => $missing_output, apt_dir => $missing_apt, manifest => $missing_manifest, - extra => [ '--genesis-release', $release_root ], + extra => [ '--genesis-release', $current_release ], ); isnt($missing_status, 0, 'a version 1 release does not hide an incomplete current shared manifest'); @@ -609,7 +601,7 @@ sub test_version_1_deb_consumer { my $unknown_status = run_apt_consumer( log => $unknown_log, output => $unknown_output, apt_dir => $unknown_apt, manifest => $unknown_manifest, - extra => [ '--genesis-release', $release_root ], + extra => [ '--genesis-release', $current_release ], ); isnt($unknown_status, 0, 'an unknown shared manifest package is refused'); like(read_binary($unknown_log), @@ -617,6 +609,28 @@ sub test_version_1_deb_consumer { 'the shared manifest failure identifies the unknown package'); ok(!-d "$unknown_apt/pool/main/xcat-genesis-openembedded", 'an unknown shared manifest package publishes nothing'); + + my $non_genesis_manifest = "$tmp/deb-non-genesis-missing.conf"; + write_apt_manifest( + $non_genesis_manifest, + sub { $_[0]->{'xcat-release'} = '2.*' }, + ); + my $non_genesis_apt = "$tmp/apt-non-genesis-missing"; + my $non_genesis_output = "$tmp/deb-non-genesis-missing-output"; + stage_apt_suites($non_genesis_output, "$tmp/deb-non-genesis-missing-legacy"); + my $non_genesis_log = "$tmp/deb-non-genesis-missing.log"; + my $non_genesis_status = run_apt_consumer( + log => $non_genesis_log, + output => $non_genesis_output, + apt_dir => $non_genesis_apt, + manifest => $non_genesis_manifest, + extra => [ '--genesis-release', $current_release ], + ); + isnt($non_genesis_status, 0, 'every shared manifest package is verified'); + like(read_binary($non_genesis_log), qr/MISSING xcat-release/, + 'the shared gate identifies a missing non-Genesis package'); + ok(!-d "$non_genesis_apt/pool/main/xcat-genesis-openembedded", + 'a missing non-Genesis package prevents APT publication'); } sub test_signed_common_rpm_repository { From 64ecf10b416c04a8b244a5b04629ff386145a2af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:15:25 -0300 Subject: [PATCH 14/28] docs(genesis): define current release completeness --- BUILD.md | 6 +++--- genesis-openembedded/README.md | 11 ++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/BUILD.md b/BUILD.md index 4bfbffe..eb04577 100644 --- a/BUILD.md +++ b/BUILD.md @@ -247,9 +247,9 @@ generation, and signing have succeeded. If a stopped publisher leaves staging or backup directories behind, rerun it with ``--force-unlock`` to recover the previous repository before starting a new publication. -Repository publication requires a complete release for its manifest version. -Version 1 contains seven architectures; version 2 also requires `s390x`. The -packages are `noarch`, and each publication contains that release's full set. +Repository publication requires all eight current architectures. Version 1 +remains readable but cannot replace the current repository because it lacks +`s390x`. The packages are `noarch`, and each publication contains the full set. The release checksums cover the unsigned input packages. If repository signing is enabled, `rpmsign` changes the deployed RPM bytes after collection. diff --git a/genesis-openembedded/README.md b/genesis-openembedded/README.md index eef1b7e..fe9f78a 100644 --- a/genesis-openembedded/README.md +++ b/genesis-openembedded/README.md @@ -89,11 +89,12 @@ repository with a single rename, under one global publish lock, so a failed or interrupted publication leaves the previous repository exactly as it was. Both consumers verify package identities and checksums before publication. -Version 2 releases require all eight architectures. Seven-architecture version -1 releases remain publishable. Building a version 2 release requires an -xcat-core revision with the `s390x` OpenEmbedded target. Merge or deploy that -xcat-core change first. A management node can install an image for a different -target architecture. +Version 2 releases require all eight architectures. Version 1 remains readable, +but current publishers refuse it because replacing the repository would remove +`s390x`. Building `s390x` requires an xcat-core revision that reports the target. +Merge or deploy that xcat-core change first. Builds for the other architectures +still accept older xcat-core revisions. A management node can install an image +for a different target architecture. Without `--genesis-release`, both builders keep their existing behavior. The new packages do not provide, replace, or obsolete the old package names. From 71f4e0d18a69c8f92532be6aef31c99db2eb9046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 4 Sep 2026 18:02:03 -0300 Subject: [PATCH 15/28] fix(genesis): harden release compatibility gates --- genesis-openembedded/build | 12 +++++++++--- mockbuild-all.pl | 30 +++++++++++++++++------------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/genesis-openembedded/build b/genesis-openembedded/build index e5e3edc..62648e5 100755 --- a/genesis-openembedded/build +++ b/genesis-openembedded/build @@ -66,9 +66,15 @@ for my $path (qw(Version xCAT-genesis-builder/oe/build xCAT-genesis-builder/oe/e } if ($requested{s390x}) { my $architecture_reporter = "$xcat_source/xCAT-genesis-builder/oe/build"; - my $supported_output = eval { - capture_command($architecture_reporter, '--list-architectures'); - }; + my $probe_work = tempdir('xcat-genesis-capabilities.XXXXXX', TMPDIR => 1, CLEANUP => 1); + my $supported_output; + { + local $ENV{XCAT_GENESIS_WORK_DIR} = $probe_work; + local $ENV{TMPDIR} = $probe_work; + $supported_output = eval { + capture_command($architecture_reporter, '--list-architectures'); + }; + } my $report_error = $@; die "xcat-core source does not report supported Genesis architectures: $report_error" if $report_error; diff --git a/mockbuild-all.pl b/mockbuild-all.pl index dd2456e..cf81359 100755 --- a/mockbuild-all.pl +++ b/mockbuild-all.pl @@ -382,6 +382,7 @@ if ($genesis_release ne '') { my @omitted = grep { !$release_architecture{$_} } architectures(); die "Genesis release version $manifest->{version} omits currently supported architectures: @omitted\n" if @omitted; + common_repository_requirements(); $genesis_release_checksums = $checksums_before; } @@ -1105,6 +1106,21 @@ sub publish_genesis_common_repo { #-------------------------------------------------------------------------------- sub verify_common_repo { my ($dir) = @_; + my %req = %{ common_repository_requirements() }; + my @names = sort keys %req; + my %present = repo_present_versions($dir, \@names); + my %present_evr = map { $_ => rpm_evr($dir, $_) } @names; + my @problems = verify_repo_packages(\%req, \%present, \%present_evr, \&rpm_vercmp_segment); + if (@problems) { + print " - $_\n" for @problems; + die "FATAL: shared Genesis repo INCOMPLETE at $dir (" . scalar(@problems) . " problem(s))\n"; + } + print "[verify-repo] common complete: " . scalar(@names) + . " packages present + EVR-satisfied in $dir\n"; + return 1; +} + +sub common_repository_requirements { my $manifest = "$repo_root/packages-manifest.conf"; my %MAN = read_manifest($manifest); my %common = %{ $MAN{common} // {} }; @@ -1121,19 +1137,7 @@ sub verify_common_repo { if @manifest_missing; die "FATAL: [common] has unsupported packages: @manifest_unknown\n" if @manifest_unknown; - - my %req = %common; - my @names = sort keys %req; - my %present = repo_present_versions($dir, \@names); - my %present_evr = map { $_ => rpm_evr($dir, $_) } @names; - my @problems = verify_repo_packages(\%req, \%present, \%present_evr, \&rpm_vercmp_segment); - if (@problems) { - print " - $_\n" for @problems; - die "FATAL: shared Genesis repo INCOMPLETE at $dir (" . scalar(@problems) . " problem(s))\n"; - } - print "[verify-repo] common complete: " . scalar(@names) - . " packages present + EVR-satisfied in $dir\n"; - return 1; + return \%common; } sub replace_common_repository { From c1aaba4cd43358c7b57175045a4d3039ac0ca7c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 4 Sep 2026 18:02:03 -0300 Subject: [PATCH 16/28] test(genesis): cover publisher compatibility gates --- t/common-repo-gate.t | 26 +++++++++++++---- t/genesis_openembedded_consumer.t | 8 ++++-- t/genesis_openembedded_release.t | 46 +++++++++++++++++++++++++++---- 3 files changed, 67 insertions(+), 13 deletions(-) diff --git a/t/common-repo-gate.t b/t/common-repo-gate.t index dd2b8f7..e8b6ca3 100644 --- a/t/common-repo-gate.t +++ b/t/common-repo-gate.t @@ -119,9 +119,8 @@ SPEC return $FIXTURE; } -# run_publish($release_dir) -> ($exit, $output, $common_dir) sub run_publish { - my ($release, $tag, $mutate_common) = @_; + my ($release, $tag, $mutate_common, $skip_repo_verification) = @_; my $out = "$tmp/$tag"; make_path("$out/root", "$out/collect"); # Something to collect, so the run gets past the "built nothing" guard. It must NOT be an @@ -137,13 +136,16 @@ sub run_publish { print $fh "\n[common]\n"; print $fh "$_=$m{common}{$_}\n" for sort keys %{ $m{common} // {} }; close $fh; - my $cmd = join(' ', map { my $x = $_; $x =~ s/'/'"'"'/g; "'$x'" } + my @command = ($^X, $SCRIPT, '--repo-root', "$out/root", '--output', "$out/build", '--repo-dep', "$out/repo", '--target', $target, '--run-id', $tag, '--build-timestamp', '1787672536', '--skip-build', '--skip-genesis', '--skip-xcat-dep', '--skip-perl', '--skip-tarball', - '--collect-dir', "$out/collect", '--genesis-release', $release)) . ' 2>&1'; + '--collect-dir', "$out/collect", '--genesis-release', $release); + push @command, '--no-verify-repo' if $skip_repo_verification; + my $cmd = join(' ', map { my $x = $_; $x =~ s/'/'"'"'/g; "'$x'" } @command) + . ' 2>&1'; my $log = `$cmd`; return ($? >> 8, $log, "$out/repo/common"); } @@ -200,13 +202,27 @@ sub run_publish { 'missing-current-package', sub { delete $_[0]->{ rpm_package_name('s390x') } }, ); - isnt($rc, 0, 'a version 1 release does not hide an incomplete current manifest'); + isnt($rc, 0, 'a current release does not hide an incomplete manifest'); like($out, qr/\[common\] is missing supported packages: .*s390x/, 'the manifest failure identifies the missing current package'); ok(!-d $common || !glob("$common/*.rpm"), 'an incomplete current manifest publishes nothing'); } +{ + my ($rc, $out, $common) = run_publish( + $RELEASE, + 'missing-current-package-without-repo-verification', + sub { delete $_[0]->{ rpm_package_name('s390x') } }, + 1, + ); + isnt($rc, 0, 'repository verification cannot disable the common manifest gate'); + like($out, qr/\[common\] is missing supported packages: .*s390x/, + 'the mandatory manifest gate identifies the missing package'); + ok(!-d $common || !glob("$common/*.rpm"), + 'the mandatory manifest gate publishes nothing'); +} + { my ($rc, $out, $common) = run_publish( $RELEASE, diff --git a/t/genesis_openembedded_consumer.t b/t/genesis_openembedded_consumer.t index 1a36144..a778102 100644 --- a/t/genesis_openembedded_consumer.t +++ b/t/genesis_openembedded_consumer.t @@ -103,9 +103,13 @@ done_testing(); sub write_target_manifest { my ($root, $target) = @_; make_path($root); + my %manifest = read_manifest("$repo_root/packages-manifest.conf"); + my $common = join('', map { "$_=$manifest{common}{$_}\n" } + sort keys %{ $manifest{common} // {} }); write_binary( "$root/packages-manifest.conf", - "[$target]\n" . rpm_package_name(capture_command('uname', '-m')) . "=*\n", + "[$target]\n" . rpm_package_name(capture_command('uname', '-m')) + . "=*\n\n[common]\n$common", ); } @@ -583,7 +587,7 @@ sub test_version_1_deb_consumer { extra => [ '--genesis-release', $current_release ], ); isnt($missing_status, 0, - 'a version 1 release does not hide an incomplete current shared manifest'); + 'a current release does not hide an incomplete shared manifest'); like(read_binary($missing_log), qr/\[shared\] is missing supported packages: .*s390x/, 'the shared manifest failure identifies the missing current package'); ok(!-d "$missing_apt/pool/main/xcat-genesis-openembedded", diff --git a/t/genesis_openembedded_release.t b/t/genesis_openembedded_release.t index f14f442..817a57b 100644 --- a/t/genesis_openembedded_release.t +++ b/t/genesis_openembedded_release.t @@ -214,7 +214,7 @@ write_release_manifest( ); write_checksums($legacy_release); ok(validate_complete_release($legacy_release), - 'complete version 1 releases remain publishable'); + 'complete version 1 releases remain readable'); my $invalid_legacy_release = "$tmp/invalid-legacy-release"; copy_tree($complete_release, $invalid_legacy_release); @@ -272,14 +272,16 @@ dies_like(sub { validate_release($missing_release) }, qr/Genesis release is miss 'incomplete architecture set fails'); SKIP: { - skip 'git is not installed', 4 unless command_exists('git'); + skip 'git is not installed', 9 unless command_exists('git'); my $source = "$tmp/dirty-xcat-core"; - make_path("$source/xCAT-genesis-builder/oe"); + my $oe = "$source/xCAT-genesis-builder/oe"; + make_path($oe); write_binary("$source/Version", "$version\n"); write_binary( "$source/xCAT-genesis-builder/oe/build", "#!/bin/sh\n" . "if [ \"\${1-}\" = --list-architectures ]; then\n" + . " mkdir -p \"\${XCAT_GENESIS_WORK_DIR:?}\"\n" . " printf '%s\\n' x86_64\n" . " exit 0\n" . "fi\n" @@ -322,6 +324,38 @@ SKIP: { ); like(read_binary($target_log), qr/does not support Genesis architecture s390x/, 'missing target failure identifies the required xcat-core support'); + ok(!-d "$oe/.work", + 'the capability query does not create a work directory in xcat-core'); + + write_binary("$oe/build", "#!/bin/sh\nexit 23\n"); + chmod(0755, "$oe/build") or die "Cannot update fixture build executable: $!"; + my $failed_log = "$tmp/failed-query.log"; + isnt( + run_capture( + $failed_log, $builder, '--xcat-source', $source, + '--architecture', 's390x', + '--output-dir', "$tmp/failed-query-output", + ), + 0, + 'release builder rejects a failed architecture query', + ); + like(read_binary($failed_log), qr/does not report supported Genesis architectures/, + 'failed architecture queries are reported'); + + write_binary("$oe/build", "#!/bin/sh\nexit 0\n"); + chmod(0755, "$oe/build") or die "Cannot update fixture build executable: $!"; + my $empty_log = "$tmp/empty-query.log"; + isnt( + run_capture( + $empty_log, $builder, '--xcat-source', $source, + '--architecture', 's390x', + '--output-dir', "$tmp/empty-query-output", + ), + 0, + 'release builder rejects an empty architecture query', + ); + like(read_binary($empty_log), qr/reported no supported Genesis architectures/, + 'empty architecture queries are reported'); } SKIP: { @@ -528,7 +562,7 @@ sub exercise_builder_tmpdir { #!/bin/sh set -eu if [ "${1-}" = --list-architectures ]; then - printf '%s\n' x86_64 + printf '%s\n' x86_64 s390x exit 0 fi expected=$XCAT_GENESIS_WORK_DIR/build/tmp @@ -582,7 +616,7 @@ EXPORT $status = run_capture( $log, $builder, '--xcat-source', $source, '--output-dir', $output, '--work-dir', $persistent_work, - '--format', 'deb', + '--format', 'deb', '--architecture', 's390x', ); } is($status, 0, 'release builder isolates the OpenEmbedded tmpdir'); @@ -593,6 +627,6 @@ EXPORT is((stat($output))[2] & oct('07777'), oct('0755'), 'release directory is readable by other users'); my $built = validate_release($output); - is($built->{architectures}, 'x86_64', 'isolated build keeps the target architecture'); + is($built->{architectures}, 's390x', 'isolated build keeps the target architecture'); is($built->{formats}, 'deb', 'isolated build keeps the requested format'); } From ecb9901e266b1822070cf637f6843b40600fcbf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 4 Sep 2026 18:02:04 -0300 Subject: [PATCH 17/28] docs(genesis): explain publisher compatibility --- genesis-openembedded/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/genesis-openembedded/README.md b/genesis-openembedded/README.md index fe9f78a..78871fe 100644 --- a/genesis-openembedded/README.md +++ b/genesis-openembedded/README.md @@ -91,7 +91,9 @@ interrupted publication leaves the previous repository exactly as it was. Both consumers verify package identities and checksums before publication. Version 2 releases require all eight architectures. Version 1 remains readable, but current publishers refuse it because replacing the repository would remove -`s390x`. Building `s390x` requires an xcat-core revision that reports the target. +`s390x`. Builder and publisher hosts must use the same xcat-dep revision when +the release format changes. Building `s390x` requires an xcat-core revision +that reports the target. Merge or deploy that xcat-core change first. Builds for the other architectures still accept older xcat-core revisions. A management node can install an image for a different target architecture. From 58060fd5a4fc2c2390d3a2b10746ba449b3ea132 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 4 Sep 2026 18:02:04 -0300 Subject: [PATCH 18/28] refactor(genesis): remove redundant pool comment --- sbuild-all.pl | 2 -- 1 file changed, 2 deletions(-) diff --git a/sbuild-all.pl b/sbuild-all.pl index 0d47246..afeb035 100755 --- a/sbuild-all.pl +++ b/sbuild-all.pl @@ -1055,8 +1055,6 @@ sub install_genesis_release_debs { return scalar(@files); } -# verify_shared_pool($pool): assert the pool satisfies [shared]. [shared] must cover every supported -# Genesis architecture. sub verify_shared_pool { my ($pool) = @_; my %shared = %{ $MANIFEST{shared} // {} }; From 86e2df00cb2002375e2d91eb59e2cb74af984d08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 4 Sep 2026 18:11:23 -0300 Subject: [PATCH 19/28] refactor(genesis): follow perl style --- genesis-openembedded/build | 21 +++++++++++++-------- mockbuild-all.pl | 6 +++--- sbuild-all.pl | 6 +++--- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/genesis-openembedded/build b/genesis-openembedded/build index 62648e5..5dae90f 100755 --- a/genesis-openembedded/build +++ b/genesis-openembedded/build @@ -3,7 +3,9 @@ use strict; use warnings; +use Carp qw(croak); use Cwd qw(abs_path); +use English qw(-no_match_vars); use File::Basename qw(basename dirname); use File::Copy qw(copy); use File::Path qw(make_path); @@ -75,14 +77,17 @@ if ($requested{s390x}) { capture_command($architecture_reporter, '--list-architectures'); }; } - my $report_error = $@; - die "xcat-core source does not report supported Genesis architectures: $report_error" - if $report_error; - die "xcat-core source reported no supported Genesis architectures\n" - unless defined($supported_output) && length($supported_output); - my %supported_architecture = map { $_ => 1 } split(/\s+/, $supported_output); - die "xcat-core source does not support Genesis architecture s390x\n" - unless $supported_architecture{s390x}; + my $report_error = $EVAL_ERROR; + if ($report_error) { + croak "xcat-core source does not report supported Genesis architectures: $report_error"; + } + if (!defined($supported_output) || !length $supported_output) { + die "xcat-core source reported no supported Genesis architectures\n"; + } + my %supported_architecture = map { $_ => 1 } split m{\s+}xms, $supported_output; + if (!$supported_architecture{s390x}) { + die "xcat-core source does not support Genesis architecture s390x\n"; + } } die "xcat-core checkout is not clean\n" if capture_command('git', '-C', $xcat_source, 'status', '--porcelain') ne ''; diff --git a/mockbuild-all.pl b/mockbuild-all.pl index cf81359..23f6f41 100755 --- a/mockbuild-all.pl +++ b/mockbuild-all.pl @@ -378,7 +378,7 @@ if ($genesis_release ne '') { die "Genesis release changed during verification\n" unless hashes_equal($checksums_before, $checksums_after); my %release_architecture = map { $_ => 1 } - split(/,/, $manifest->{architectures}); + split m{,}xms, $manifest->{architectures}; my @omitted = grep { !$release_architecture{$_} } architectures(); die "Genesis release version $manifest->{version} omits currently supported architectures: @omitted\n" if @omitted; @@ -1129,9 +1129,9 @@ sub common_repository_requirements { my @supported_names = map { rpm_package_name($_) } architectures(); my %supported = map { $_ => 1 } @supported_names; - my @manifest_missing = grep { !exists($common{$_}) } @supported_names; + my @manifest_missing = grep { !exists $common{$_} } @supported_names; my @manifest_unknown = grep { - /^xCAT-genesis-openembedded-/ && !$supported{$_} + m{\AxCAT-genesis-openembedded-}xms && !$supported{$_} } sort keys %common; die "FATAL: [common] is missing supported packages: @manifest_missing\n" if @manifest_missing; diff --git a/sbuild-all.pl b/sbuild-all.pl index afeb035..aff7c9b 100755 --- a/sbuild-all.pl +++ b/sbuild-all.pl @@ -319,7 +319,7 @@ if ($genesis_release ne '') { die "FATAL: Genesis release changed during verification\n" unless XCAT::BuildUtils::hashes_equal($before, $after); my %release_architecture = map { $_ => 1 } - split(/,/, $release_manifest->{architectures}); + split m{,}xms, $release_manifest->{architectures}; my @omitted = grep { !$release_architecture{$_} } XCAT::GenesisRelease::architectures(); @@ -1065,9 +1065,9 @@ sub verify_shared_pool { XCAT::GenesisRelease::deb_package_name($_) } XCAT::GenesisRelease::architectures(); my %supported = map { $_ => 1 } @supported_names; - my @manifest_missing = grep { !exists($shared{$_}) } @supported_names; + my @manifest_missing = grep { !exists $shared{$_} } @supported_names; my @manifest_unknown = grep { - /^xcat-genesis-openembedded-/ && !$supported{$_} + m{\Axcat-genesis-openembedded-}xms && !$supported{$_} } sort keys %shared; die "FATAL: [shared] is missing supported packages: @manifest_missing\n" if @manifest_missing; From bea6826ac5877784fcdc5ccd3178e8a02ee75091 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sun, 6 Sep 2026 10:54:28 -0300 Subject: [PATCH 20/28] fix(genesis): preserve release compatibility --- genesis-openembedded/build | 7 ++++--- lib/XCAT/GenesisRelease.pm | 6 +++--- mockbuild-all.pl | 7 ------- sbuild-all.pl | 16 ++++------------ 4 files changed, 11 insertions(+), 25 deletions(-) diff --git a/genesis-openembedded/build b/genesis-openembedded/build index 5dae90f..3875d54 100755 --- a/genesis-openembedded/build +++ b/genesis-openembedded/build @@ -66,6 +66,8 @@ $xcat_source = abs_path($xcat_source) or die "Cannot resolve xcat-core source\n" for my $path (qw(Version xCAT-genesis-builder/oe/build xCAT-genesis-builder/oe/export)) { die "xcat-core source is missing $path\n" unless -f "$xcat_source/$path"; } +die "xcat-core checkout is not clean\n" + if capture_command('git', '-C', $xcat_source, 'status', '--porcelain') ne ''; if ($requested{s390x}) { my $architecture_reporter = "$xcat_source/xCAT-genesis-builder/oe/build"; my $probe_work = tempdir('xcat-genesis-capabilities.XXXXXX', TMPDIR => 1, CLEANUP => 1); @@ -89,8 +91,6 @@ if ($requested{s390x}) { die "xcat-core source does not support Genesis architecture s390x\n"; } } -die "xcat-core checkout is not clean\n" - if capture_command('git', '-C', $xcat_source, 'status', '--porcelain') ne ''; my $revision = capture_command('git', '-C', $xcat_source, 'rev-parse', 'HEAD'); die "Invalid xcat-core revision: $revision\n" unless $revision =~ /^[0-9a-f]{40}$/; @@ -165,10 +165,11 @@ for my $architecture (@requested_architectures) { } my @formats = $format eq 'all' ? qw(deb rpm) : ($format); +my $manifest_version = $requested{s390x} ? 2 : 1; write_binary( "$staging/release.manifest", "format=xcat-genesis-packages\n" - . "version=2\n" + . "version=$manifest_version\n" . "xcat_version=$version\n" . "xcat_release=$release\n" . "xcat_revision=$revision\n" diff --git a/lib/XCAT/GenesisRelease.pm b/lib/XCAT/GenesisRelease.pm index 308372c..6832940 100644 --- a/lib/XCAT/GenesisRelease.pm +++ b/lib/XCAT/GenesisRelease.pm @@ -232,9 +232,9 @@ sub validate_complete_release { my ($directory) = @_; my $manifest = validate_release($directory); my %present = map { $_ => 1 } split(/,/, $manifest->{architectures}); - my @required = @{ $RELEASE_ARCHITECTURES{ $manifest->{version} } }; - my @missing = grep { !$present{$_} } @required; - die "Genesis release is missing supported architectures: @missing\n" if @missing; + my @missing = grep { !$present{$_} } @ARCHITECTURES; + die "Genesis release version $manifest->{version} omits currently supported architectures: @missing\n" + if @missing; return $manifest; } diff --git a/mockbuild-all.pl b/mockbuild-all.pl index 23f6f41..be8cb28 100755 --- a/mockbuild-all.pl +++ b/mockbuild-all.pl @@ -34,7 +34,6 @@ use XCAT::BuildUtils qw( ); use XCAT::GenesisRelease qw( architectures - read_release_manifest rpm_package_name validated_release_checksums verify_release_file @@ -373,15 +372,9 @@ if ($genesis_release ne '') { # before with the one taken after is what closes that window. my $checksums_before = validated_release_checksums($genesis_release); run_command($^X, $verifier, '--complete', '--format', 'rpm', $genesis_release); - my $manifest = read_release_manifest($genesis_release); my $checksums_after = validated_release_checksums($genesis_release); die "Genesis release changed during verification\n" unless hashes_equal($checksums_before, $checksums_after); - my %release_architecture = map { $_ => 1 } - split m{,}xms, $manifest->{architectures}; - my @omitted = grep { !$release_architecture{$_} } architectures(); - die "Genesis release version $manifest->{version} omits currently supported architectures: @omitted\n" - if @omitted; common_repository_requirements(); $genesis_release_checksums = $checksums_before; } diff --git a/sbuild-all.pl b/sbuild-all.pl index aff7c9b..aa3382f 100755 --- a/sbuild-all.pl +++ b/sbuild-all.pl @@ -314,17 +314,9 @@ if ($genesis_release ne '') { require XCAT::GenesisRelease; my $before = XCAT::GenesisRelease::validated_release_checksums($genesis_release); XCAT::BuildUtils::run_command($^X, $verifier, '--complete', '--format', 'deb', $genesis_release); - my $release_manifest = XCAT::GenesisRelease::read_release_manifest($genesis_release); my $after = XCAT::GenesisRelease::validated_release_checksums($genesis_release); die "FATAL: Genesis release changed during verification\n" unless XCAT::BuildUtils::hashes_equal($before, $after); - my %release_architecture = map { $_ => 1 } - split m{,}xms, $release_manifest->{architectures}; - my @omitted = grep { - !$release_architecture{$_} - } XCAT::GenesisRelease::architectures(); - die "FATAL: Genesis release version $release_manifest->{version} omits currently supported architectures: @omitted\n" - if @omitted; $genesis_release_checksums = $before; # Every suite's Packages index points into the shared Genesis pool, and publishing a release # replaces that pool -- so a run that rebuilt only some suites would leave the others indexing @@ -1490,10 +1482,10 @@ release is produced separately (see F); this opt and copies the verified bytes into every selected suite. The release must carry C packages for every currently supported architecture. Version 1 -remains readable but cannot replace the current eight-architecture repository because it lacks -C. The release is validated before any build or publish: its C is read, the -shared verifier runs, and the checksums are read again -- a release rewritten together with its -checksums while the verifier runs is rejected. +metadata remains readable, but a complete release now requires C. The release is validated +before any build or publish: its C is read, the shared verifier runs, and the checksums +are read again -- a release rewritten together with its checksums while the verifier runs is +rejected. The packages are published B, into F, and every suite's C index points at that one copy: they are C and identical everywhere, From c8def3dc02b6f9c069755f95250678dc81b6c45a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sun, 6 Sep 2026 10:54:29 -0300 Subject: [PATCH 21/28] test(genesis): cover release compatibility --- t/common-repo-gate.t | 2 +- t/genesis_openembedded_consumer.t | 8 ++-- t/genesis_openembedded_release.t | 64 ++++++++++++++++++++++++------- 3 files changed, 55 insertions(+), 19 deletions(-) diff --git a/t/common-repo-gate.t b/t/common-repo-gate.t index e8b6ca3..e52a9ea 100644 --- a/t/common-repo-gate.t +++ b/t/common-repo-gate.t @@ -256,7 +256,7 @@ sub run_publish { my ($rc, $out, $common) = run_publish($partial, 'partial'); isnt($rc, 0, 'a release missing an architecture is refused'); - like($out, qr/missing supported architectures: \Q$missing_architecture\E/, + like($out, qr/omits currently supported architectures: \Q$missing_architecture\E/, 'the completeness gate identifies the missing architecture'); ok(!-d $common || !glob("$common/*.rpm"), '... and nothing is published into the shared repository'); diff --git a/t/genesis_openembedded_consumer.t b/t/genesis_openembedded_consumer.t index a778102..0ba1beb 100644 --- a/t/genesis_openembedded_consumer.t +++ b/t/genesis_openembedded_consumer.t @@ -58,7 +58,7 @@ if ($ENV{XCAT_GENESIS_CI}) { } SKIP: { - skip 'RPM repository tools require a root Linux builder', 64 + skip 'RPM repository tools require a root Linux builder', 65 unless $^O eq 'linux' && $> == 0 && command_exists('rpmbuild') @@ -77,7 +77,7 @@ SKIP: { } SKIP: { - skip 'APT repository tools are not installed', 70 + skip 'APT repository tools are not installed', 63 unless $^O eq 'linux' && command_exists('dpkg-deb') && command_exists('apt-ftparchive'); @@ -792,7 +792,7 @@ sub test_partial_rpm_release { ); isnt($status, 0, 'RPM repository rejects a partial Genesis release'); - like(read_binary($log), qr/Genesis release is missing supported architectures/, + like(read_binary($log), qr/Genesis release version 2 omits currently supported architectures/, 'RPM partial-release failure names the missing architectures'); ok(-f $existing, 'partial release does not remove the deployed package'); } @@ -814,7 +814,7 @@ sub test_partial_deb_release { ); isnt($status, 0, 'APT repository rejects a partial Genesis release'); - like(read_binary($log), qr/Genesis release is missing supported architectures/, + like(read_binary($log), qr/Genesis release version 2 omits currently supported architectures/, 'APT partial-release failure names the missing architectures'); ok(-f $existing, 'partial DEB release does not remove the published package'); } diff --git a/t/genesis_openembedded_release.t b/t/genesis_openembedded_release.t index 817a57b..324934b 100644 --- a/t/genesis_openembedded_release.t +++ b/t/genesis_openembedded_release.t @@ -181,7 +181,7 @@ like( ); dies_like( sub { validate_complete_release($release_dir) }, - qr/Genesis release is missing supported architectures/, + qr/Genesis release version 2 omits currently supported architectures/, 'partial release cannot be published', ); @@ -213,8 +213,12 @@ write_release_manifest( 'x86,x86_64,ppc64,ppc64le,armv7hf,aarch64,riscv64', 'deb,rpm', 1, ); write_checksums($legacy_release); -ok(validate_complete_release($legacy_release), - 'complete version 1 releases remain readable'); +ok(validate_release($legacy_release), 'version 1 releases remain readable'); +dies_like( + sub { validate_complete_release($legacy_release) }, + qr/Genesis release version 1 omits currently supported architectures: s390x/, + 'version 1 releases cannot replace the current repository', +); my $invalid_legacy_release = "$tmp/invalid-legacy-release"; copy_tree($complete_release, $invalid_legacy_release); @@ -275,12 +279,14 @@ SKIP: { skip 'git is not installed', 9 unless command_exists('git'); my $source = "$tmp/dirty-xcat-core"; my $oe = "$source/xCAT-genesis-builder/oe"; + my $capability_marker = "$tmp/capability-query-ran"; make_path($oe); write_binary("$source/Version", "$version\n"); write_binary( "$source/xCAT-genesis-builder/oe/build", "#!/bin/sh\n" . "if [ \"\${1-}\" = --list-architectures ]; then\n" + . " [ -z \"\${XCAT_TEST_CAPABILITY_MARKER-}\" ] || : >\"\$XCAT_TEST_CAPABILITY_MARKER\"\n" . " mkdir -p \"\${XCAT_GENESIS_WORK_DIR:?}\"\n" . " printf '%s\\n' x86_64\n" . " exit 0\n" @@ -299,18 +305,36 @@ SKIP: { die "Cannot prepare test repository\n" if run_capture("$tmp/git-fixture.log", @{$command}); } + my $commit_source = sub { + my ($message) = @_; + for my $command ( + [ 'git', '-C', $source, 'add', 'xCAT-genesis-builder/oe/build' ], + [ 'git', '-C', $source, '-c', 'user.name=xCAT test', + '-c', 'user.email=xcat-test@example.invalid', 'commit', '-qm', $message ], + ) { + die "Cannot update test repository\n" + if run_capture("$tmp/git-fixture.log", @{$command}); + } + }; write_binary("$source/untracked", "not part of the commit\n"); my $log = "$tmp/dirty-source.log"; - isnt( - run_capture( - $log, $builder, '--xcat-source', $source, - '--output-dir', "$tmp/dirty-output", - ), - 0, - 'release builder rejects untracked source files', - ); + { + local $ENV{XCAT_TEST_CAPABILITY_MARKER} = $capability_marker; + isnt( + run_capture( + $log, $builder, '--xcat-source', $source, + '--architecture', 's390x', + '--output-dir', "$tmp/dirty-output", + ), + 0, + 'release builder rejects untracked source files', + ); + } like(read_binary($log), qr/xcat-core checkout is not clean/, 'dirty checkout failure is explicit'); + ok(!-e $capability_marker, + 'dirty source is rejected before its architecture helper runs'); + unlink("$source/untracked") or die "Cannot clean the source fixture: $!\n"; my $target_log = "$tmp/missing-target.log"; isnt( @@ -324,11 +348,10 @@ SKIP: { ); like(read_binary($target_log), qr/does not support Genesis architecture s390x/, 'missing target failure identifies the required xcat-core support'); - ok(!-d "$oe/.work", - 'the capability query does not create a work directory in xcat-core'); write_binary("$oe/build", "#!/bin/sh\nexit 23\n"); chmod(0755, "$oe/build") or die "Cannot update fixture build executable: $!"; + $commit_source->('fail capability query'); my $failed_log = "$tmp/failed-query.log"; isnt( run_capture( @@ -344,6 +367,7 @@ SKIP: { write_binary("$oe/build", "#!/bin/sh\nexit 0\n"); chmod(0755, "$oe/build") or die "Cannot update fixture build executable: $!"; + $commit_source->('empty capability query'); my $empty_log = "$tmp/empty-query.log"; isnt( run_capture( @@ -377,7 +401,7 @@ SKIP: { } SKIP: { - skip 'git and dpkg-deb are not installed', 6 + skip 'git and dpkg-deb are not installed', 9 unless command_exists('git') && command_exists('dpkg-deb'); exercise_builder_tmpdir(); } @@ -629,4 +653,16 @@ EXPORT my $built = validate_release($output); is($built->{architectures}, 's390x', 'isolated build keeps the target architecture'); is($built->{formats}, 'deb', 'isolated build keeps the requested format'); + is($built->{version}, 2, 'a release containing s390x uses format version 2'); + + my $legacy_output = "$tmp/tmpdir-legacy-release"; + my $legacy_log = "$tmp/tmpdir-legacy-builder.log"; + my $legacy_status = run_capture( + $legacy_log, $builder, '--xcat-source', $source, + '--output-dir', $legacy_output, '--work-dir', $persistent_work, + '--format', 'deb', '--architecture', 'x86_64', + ); + is($legacy_status, 0, 'release builder keeps legacy targets buildable'); + my $legacy = validate_release($legacy_output); + is($legacy->{version}, 1, 'a release without s390x uses format version 1'); } From 99d60459e75a249d5b940284097e8b6e450a8e4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sun, 6 Sep 2026 10:54:29 -0300 Subject: [PATCH 22/28] docs(genesis): explain release formats --- BUILD.md | 5 +++-- genesis-openembedded/README.md | 15 +++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/BUILD.md b/BUILD.md index eb04577..7ce76e3 100644 --- a/BUILD.md +++ b/BUILD.md @@ -248,8 +248,9 @@ or backup directories behind, rerun it with ``--force-unlock`` to recover the previous repository before starting a new publication. Repository publication requires all eight current architectures. Version 1 -remains readable but cannot replace the current repository because it lacks -`s390x`. The packages are `noarch`, and each publication contains the full set. +release manifests remain readable, but they cannot replace the current +repository because they lack `s390x`. The packages are `noarch`, and each +publication contains the full set. The release checksums cover the unsigned input packages. If repository signing is enabled, `rpmsign` changes the deployed RPM bytes after collection. diff --git a/genesis-openembedded/README.md b/genesis-openembedded/README.md index 78871fe..d20beb8 100644 --- a/genesis-openembedded/README.md +++ b/genesis-openembedded/README.md @@ -89,14 +89,13 @@ repository with a single rename, under one global publish lock, so a failed or interrupted publication leaves the previous repository exactly as it was. Both consumers verify package identities and checksums before publication. -Version 2 releases require all eight architectures. Version 1 remains readable, -but current publishers refuse it because replacing the repository would remove -`s390x`. Builder and publisher hosts must use the same xcat-dep revision when -the release format changes. Building `s390x` requires an xcat-core revision -that reports the target. -Merge or deploy that xcat-core change first. Builds for the other architectures -still accept older xcat-core revisions. A management node can install an image -for a different target architecture. +Releases containing `s390x` use format version 2. Other releases keep version 1 +and remain readable by older tools. Current publishers require all eight +architectures, so a version 1 release cannot replace the shared repository. +Building `s390x` requires an xcat-core revision that reports the target. Merge +or deploy that xcat-core change first. Builds for the other architectures still +accept older xcat-core revisions. A management node can install an image for a +different target architecture. Without `--genesis-release`, both builders keep their existing behavior. The new packages do not provide, replace, or obsolete the old package names. From 6828df8d67075da7bc2e891a204c89f03ee6d424 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sun, 6 Sep 2026 11:15:33 -0300 Subject: [PATCH 23/28] fix(genesis): order release capability checks --- genesis-openembedded/build | 34 ++++++++++++++++------------------ lib/XCAT/GenesisRelease.pm | 12 ++++++++++++ 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/genesis-openembedded/build b/genesis-openembedded/build index 3875d54..375bd6f 100755 --- a/genesis-openembedded/build +++ b/genesis-openembedded/build @@ -3,9 +3,7 @@ use strict; use warnings; -use Carp qw(croak); use Cwd qw(abs_path); -use English qw(-no_match_vars); use File::Basename qw(basename dirname); use File::Copy qw(copy); use File::Path qw(make_path); @@ -25,6 +23,7 @@ use XCAT::BuildUtils qw( use XCAT::GenesisRelease qw( architectures deb_package_name + minimum_release_version rpm_package_name validate_architecture validate_release @@ -68,6 +67,17 @@ for my $path (qw(Version xCAT-genesis-builder/oe/build xCAT-genesis-builder/oe/e } die "xcat-core checkout is not clean\n" if capture_command('git', '-C', $xcat_source, 'status', '--porcelain') ne ''; + +my $revision = capture_command('git', '-C', $xcat_source, 'rev-parse', 'HEAD'); +die "Invalid xcat-core revision: $revision\n" unless $revision =~ /^[0-9a-f]{40}$/; +if ($xcat_ref ne '') { + die "Invalid xcat-core ref: $xcat_ref\n" unless $xcat_ref =~ /^[A-Za-z0-9][A-Za-z0-9._\/-]*$/; + my $expected = capture_command( + 'git', '-C', $xcat_source, 'rev-parse', '--verify', "$xcat_ref^{commit}", + ); + die "xcat-core HEAD $revision does not match $xcat_ref ($expected)\n" + unless $revision eq $expected; +} if ($requested{s390x}) { my $architecture_reporter = "$xcat_source/xCAT-genesis-builder/oe/build"; my $probe_work = tempdir('xcat-genesis-capabilities.XXXXXX', TMPDIR => 1, CLEANUP => 1); @@ -79,10 +89,9 @@ if ($requested{s390x}) { capture_command($architecture_reporter, '--list-architectures'); }; } - my $report_error = $EVAL_ERROR; - if ($report_error) { - croak "xcat-core source does not report supported Genesis architectures: $report_error"; - } + my $report_error = $@; + die "xcat-core source does not report supported Genesis architectures: $report_error" + if $report_error; if (!defined($supported_output) || !length $supported_output) { die "xcat-core source reported no supported Genesis architectures\n"; } @@ -92,17 +101,6 @@ if ($requested{s390x}) { } } -my $revision = capture_command('git', '-C', $xcat_source, 'rev-parse', 'HEAD'); -die "Invalid xcat-core revision: $revision\n" unless $revision =~ /^[0-9a-f]{40}$/; -if ($xcat_ref ne '') { - die "Invalid xcat-core ref: $xcat_ref\n" unless $xcat_ref =~ /^[A-Za-z0-9][A-Za-z0-9._\/-]*$/; - my $expected = capture_command( - 'git', '-C', $xcat_source, 'rev-parse', '--verify', "$xcat_ref^{commit}", - ); - die "xcat-core HEAD $revision does not match $xcat_ref ($expected)\n" - unless $revision eq $expected; -} - my $version = read_first_line("$xcat_source/Version"); die "Invalid xCAT version: $version\n" unless $version =~ /^\d+(?:\.\d+){1,3}$/; my $source_date_epoch = capture_command( @@ -165,7 +163,7 @@ for my $architecture (@requested_architectures) { } my @formats = $format eq 'all' ? qw(deb rpm) : ($format); -my $manifest_version = $requested{s390x} ? 2 : 1; +my $manifest_version = minimum_release_version(@requested_architectures); write_binary( "$staging/release.manifest", "format=xcat-genesis-packages\n" diff --git a/lib/XCAT/GenesisRelease.pm b/lib/XCAT/GenesisRelease.pm index 6832940..d81325b 100644 --- a/lib/XCAT/GenesisRelease.pm +++ b/lib/XCAT/GenesisRelease.pm @@ -9,6 +9,7 @@ use XCAT::BuildUtils qw(digest_file read_lines relative_files); our @EXPORT_OK = qw( architectures deb_package_name + minimum_release_version read_checksum_manifest read_release_manifest rpm_package_name @@ -53,6 +54,17 @@ sub deb_package_name { return "xcat-genesis-openembedded-$architecture"; } +sub minimum_release_version { + my @architectures = @_; + die "Release format selection requires a Genesis architecture\n" unless @architectures; + validate_architecture($_) for @architectures; + for my $version (sort { $a <=> $b } keys %RELEASE_ARCHITECTURES) { + my %supported = map { $_ => 1 } @{ $RELEASE_ARCHITECTURES{$version} }; + return $version unless grep { !$supported{$_} } @architectures; + } + die "No release format supports the requested Genesis architectures\n"; +} + sub _read_key_values { my ($path, $allowed) = @_; my %values; From 09e068419bab826018ea16acb95855c92efa5705 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sun, 6 Sep 2026 11:15:33 -0300 Subject: [PATCH 24/28] test(genesis): cover release capability ordering --- t/genesis_openembedded_consumer.t | 2 +- t/genesis_openembedded_release.t | 62 ++++++++++++++++++++++++------- 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/t/genesis_openembedded_consumer.t b/t/genesis_openembedded_consumer.t index 0ba1beb..0eb8126 100644 --- a/t/genesis_openembedded_consumer.t +++ b/t/genesis_openembedded_consumer.t @@ -58,7 +58,7 @@ if ($ENV{XCAT_GENESIS_CI}) { } SKIP: { - skip 'RPM repository tools require a root Linux builder', 65 + skip 'RPM repository tools require a root Linux builder', 64 unless $^O eq 'linux' && $> == 0 && command_exists('rpmbuild') diff --git a/t/genesis_openembedded_release.t b/t/genesis_openembedded_release.t index 324934b..bde5201 100644 --- a/t/genesis_openembedded_release.t +++ b/t/genesis_openembedded_release.t @@ -11,6 +11,7 @@ use Test::More; use lib "$FindBin::Bin/../lib"; use lib "$FindBin::Bin/lib"; use XCAT::BuildUtils qw( + capture_command command_exists digest_file read_binary @@ -19,6 +20,7 @@ use XCAT::BuildUtils qw( use XCAT::GenesisRelease qw( architectures deb_package_name + minimum_release_version rpm_package_name validated_release_checksums validate_architecture @@ -60,6 +62,12 @@ is(rpm_package_name('ppc64le'), 'xCAT-genesis-openembedded-ppc64le', 'RPM package keeps ppc64le distinct'); is(deb_package_name('x86_64'), 'xcat-genesis-openembedded-x86-64', 'DEB package uses a legal spelling of x86_64'); +is(minimum_release_version('x86_64'), 1, + 'legacy architectures use release format version 1'); +is(minimum_release_version('x86_64', 's390x'), 2, + 's390x requires release format version 2'); +dies_like(sub { minimum_release_version() }, qr/requires a Genesis architecture/, + 'release format selection requires an architecture'); dies_like(sub { validate_architecture('ppc') }, qr/Unsupported Genesis architecture/, 'legacy ppc alias is rejected'); @@ -276,7 +284,7 @@ dies_like(sub { validate_release($missing_release) }, qr/Genesis release is miss 'incomplete architecture set fails'); SKIP: { - skip 'git is not installed', 9 unless command_exists('git'); + skip 'git is not installed', 13 unless command_exists('git'); my $source = "$tmp/dirty-xcat-core"; my $oe = "$source/xCAT-genesis-builder/oe"; my $capability_marker = "$tmp/capability-query-ran"; @@ -305,10 +313,11 @@ SKIP: { die "Cannot prepare test repository\n" if run_capture("$tmp/git-fixture.log", @{$command}); } + my $previous_revision = capture_command('git', '-C', $source, 'rev-parse', 'HEAD'); my $commit_source = sub { - my ($message) = @_; + my ($path, $message) = @_; for my $command ( - [ 'git', '-C', $source, 'add', 'xCAT-genesis-builder/oe/build' ], + [ 'git', '-C', $source, 'add', $path ], [ 'git', '-C', $source, '-c', 'user.name=xCAT test', '-c', 'user.email=xcat-test@example.invalid', 'commit', '-qm', $message ], ) { @@ -336,22 +345,47 @@ SKIP: { 'dirty source is rejected before its architecture helper runs'); unlink("$source/untracked") or die "Cannot clean the source fixture: $!\n"; + write_binary("$source/revision-marker", "new revision\n"); + $commit_source->('revision-marker', 'advance source revision'); + my $ref_log = "$tmp/ref-mismatch.log"; + { + local $ENV{XCAT_TEST_CAPABILITY_MARKER} = $capability_marker; + isnt( + run_capture( + $ref_log, $builder, '--xcat-source', $source, + '--xcat-ref', $previous_revision, + '--architecture', 's390x', + '--output-dir', "$tmp/ref-mismatch-output", + ), + 0, + 'release builder rejects a mismatched xcat-core revision', + ); + } + like(read_binary($ref_log), qr/xcat-core HEAD .* does not match \Q$previous_revision\E/, + 'revision mismatch failure is explicit'); + ok(!-e $capability_marker, + 'revision mismatch is rejected before the architecture helper runs'); + my $target_log = "$tmp/missing-target.log"; - isnt( - run_capture( - $target_log, $builder, '--xcat-source', $source, - '--architecture', 's390x', - '--output-dir', "$tmp/missing-target-output", - ), - 0, - 'release builder rejects an unsupported xcat-core target', - ); + { + local $ENV{XCAT_TEST_CAPABILITY_MARKER} = $capability_marker; + isnt( + run_capture( + $target_log, $builder, '--xcat-source', $source, + '--architecture', 's390x', + '--output-dir', "$tmp/missing-target-output", + ), + 0, + 'release builder rejects an unsupported xcat-core target', + ); + } like(read_binary($target_log), qr/does not support Genesis architecture s390x/, 'missing target failure identifies the required xcat-core support'); + ok(-e $capability_marker, 'the architecture helper records a successful query'); write_binary("$oe/build", "#!/bin/sh\nexit 23\n"); chmod(0755, "$oe/build") or die "Cannot update fixture build executable: $!"; - $commit_source->('fail capability query'); + $commit_source->('xCAT-genesis-builder/oe/build', 'fail capability query'); my $failed_log = "$tmp/failed-query.log"; isnt( run_capture( @@ -367,7 +401,7 @@ SKIP: { write_binary("$oe/build", "#!/bin/sh\nexit 0\n"); chmod(0755, "$oe/build") or die "Cannot update fixture build executable: $!"; - $commit_source->('empty capability query'); + $commit_source->('xCAT-genesis-builder/oe/build', 'empty capability query'); my $empty_log = "$tmp/empty-query.log"; isnt( run_capture( From d063e86860bbc695cbea0fb1b3dace6ef23b08cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sun, 6 Sep 2026 11:30:42 -0300 Subject: [PATCH 25/28] fix(genesis): validate repository manifests early --- lib/XCAT/GenesisRelease.pm | 13 +++++++++++++ mockbuild-all.pl | 18 +++++++----------- sbuild-all.pl | 33 +++++++++++++++------------------ 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/lib/XCAT/GenesisRelease.pm b/lib/XCAT/GenesisRelease.pm index d81325b..c4b3bee 100644 --- a/lib/XCAT/GenesisRelease.pm +++ b/lib/XCAT/GenesisRelease.pm @@ -17,6 +17,7 @@ our @EXPORT_OK = qw( validate_architecture validate_complete_release validate_export + validate_repository_packages validate_release verify_release_file ); @@ -65,6 +66,18 @@ sub minimum_release_version { die "No release format supports the requested Genesis architectures\n"; } +sub validate_repository_packages { + my ($packages, $section, $prefix, @supported_names) = @_; + my %supported = map { $_ => 1 } @supported_names; + my @missing = grep { !exists $packages->{$_} } @supported_names; + my @unknown = grep { + index($_, $prefix) == 0 && !$supported{$_} + } sort keys %{$packages}; + die "FATAL: [$section] is missing supported packages: @missing\n" if @missing; + die "FATAL: [$section] has unsupported packages: @unknown\n" if @unknown; + return $packages; +} + sub _read_key_values { my ($path, $allowed) = @_; my %values; diff --git a/mockbuild-all.pl b/mockbuild-all.pl index be8cb28..154bc85 100755 --- a/mockbuild-all.pl +++ b/mockbuild-all.pl @@ -35,6 +35,7 @@ use XCAT::BuildUtils qw( use XCAT::GenesisRelease qw( architectures rpm_package_name + validate_repository_packages validated_release_checksums verify_release_file ); @@ -1120,17 +1121,12 @@ sub common_repository_requirements { die "FATAL: no [common] section in $manifest -- cannot verify the shared Genesis repository\n" if !%common; - my @supported_names = map { rpm_package_name($_) } architectures(); - my %supported = map { $_ => 1 } @supported_names; - my @manifest_missing = grep { !exists $common{$_} } @supported_names; - my @manifest_unknown = grep { - m{\AxCAT-genesis-openembedded-}xms && !$supported{$_} - } sort keys %common; - die "FATAL: [common] is missing supported packages: @manifest_missing\n" - if @manifest_missing; - die "FATAL: [common] has unsupported packages: @manifest_unknown\n" - if @manifest_unknown; - return \%common; + return validate_repository_packages( + \%common, + 'common', + 'xCAT-genesis-openembedded-', + map { rpm_package_name($_) } architectures(), + ); } sub replace_common_repository { diff --git a/sbuild-all.pl b/sbuild-all.pl index aa3382f..b008692 100755 --- a/sbuild-all.pl +++ b/sbuild-all.pl @@ -312,6 +312,7 @@ if ($genesis_release ne '') { # the verifier runs would satisfy both the verifier and any single pass taken afterwards. require XCAT::BuildUtils; require XCAT::GenesisRelease; + shared_repository_requirements(); my $before = XCAT::GenesisRelease::validated_release_checksums($genesis_release); XCAT::BuildUtils::run_command($^X, $verifier, '--complete', '--format', 'deb', $genesis_release); my $after = XCAT::GenesisRelease::validated_release_checksums($genesis_release); @@ -1049,24 +1050,7 @@ sub install_genesis_release_debs { sub verify_shared_pool { my ($pool) = @_; - my %shared = %{ $MANIFEST{shared} // {} }; - die "FATAL: no [shared] section in $manifest -- cannot verify the shared Genesis pool\n" - if !%shared; - - my @supported_names = map { - XCAT::GenesisRelease::deb_package_name($_) - } XCAT::GenesisRelease::architectures(); - my %supported = map { $_ => 1 } @supported_names; - my @manifest_missing = grep { !exists $shared{$_} } @supported_names; - my @manifest_unknown = grep { - m{\Axcat-genesis-openembedded-}xms && !$supported{$_} - } sort keys %shared; - die "FATAL: [shared] is missing supported packages: @manifest_missing\n" - if @manifest_missing; - die "FATAL: [shared] has unsupported packages: @manifest_unknown\n" - if @manifest_unknown; - - my %req = %shared; + my %req = %{ shared_repository_requirements() }; my @names = sort keys %req; my %present = map { $_ => deb_version($pool, $_) } @names; my @problems = verify_repo_packages(\%req, \%present); @@ -1078,6 +1062,19 @@ sub verify_shared_pool { return 1; } +sub shared_repository_requirements { + my %shared = %{ $MANIFEST{shared} // {} }; + die "FATAL: no [shared] section in $manifest -- cannot verify the shared Genesis pool\n" + if !%shared; + return XCAT::GenesisRelease::validate_repository_packages( + \%shared, + 'shared', + 'xcat-genesis-openembedded-', + map { XCAT::GenesisRelease::deb_package_name($_) } + XCAT::GenesisRelease::architectures(), + ); +} + sub assemble_into { my ($dir, $expect) = @_; if ($genesis_release ne '') { From 55a9a6968e841a33fa8718b5286e13974ec01e33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sun, 6 Sep 2026 11:30:42 -0300 Subject: [PATCH 26/28] test(genesis): cover early manifest rejection --- t/common-repo-gate.t | 3 ++- t/genesis_openembedded_consumer.t | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/t/common-repo-gate.t b/t/common-repo-gate.t index e52a9ea..eb1fcac 100644 --- a/t/common-repo-gate.t +++ b/t/common-repo-gate.t @@ -86,7 +86,8 @@ write_checksums($VERSION_1_RELEASE); ok($m{common} && %{ $m{common} }, 'the shipped manifest has a [common] section'); my @expected = sort map { rpm_package_name($_) } qw(x86 x86_64 ppc64 ppc64le armv7hf aarch64 riscv64 s390x); - is_deeply([ sort keys %{ $m{common} } ], \@expected, + my @missing = grep { !exists $m{common}{$_} } @expected; + is_deeply(\@missing, [], 'the shared RPM manifest lists every Genesis architecture'); } diff --git a/t/genesis_openembedded_consumer.t b/t/genesis_openembedded_consumer.t index 0eb8126..11cad2d 100644 --- a/t/genesis_openembedded_consumer.t +++ b/t/genesis_openembedded_consumer.t @@ -304,6 +304,7 @@ SH sub run_apt_consumer { my (%args) = @_; my @dists = @{ $args{dists} // \@APT_SUITES }; + my @build_mode = $args{build} ? ('--dry-run') : ('--skip-build'); my $manifest = $args{manifest}; unless ($manifest) { $manifest = "$args{output}/manifest.conf"; @@ -318,7 +319,7 @@ sub run_apt_consumer { '--manifest', $manifest, '--dists', join(' ', @dists), '--arch', 'amd64', - '--skip-build', '--skip-genesis', '--skip-tarball', + @build_mode, '--skip-genesis', '--skip-tarball', '--publish', '--expect-arch', 'amd64 ppc64el', ($args{verify} ? () : ('--no-verify-repo')), @{ $args{extra} // [] }, @@ -584,14 +585,15 @@ sub test_version_1_deb_consumer { my $missing_status = run_apt_consumer( log => $missing_log, output => $missing_output, apt_dir => $missing_apt, manifest => $missing_manifest, + build => 1, extra => [ '--genesis-release', $current_release ], ); isnt($missing_status, 0, 'a current release does not hide an incomplete shared manifest'); like(read_binary($missing_log), qr/\[shared\] is missing supported packages: .*s390x/, 'the shared manifest failure identifies the missing current package'); - ok(!-d "$missing_apt/pool/main/xcat-genesis-openembedded", - 'an incomplete shared manifest publishes nothing'); + unlike(read_binary($missing_log), qr/Ensure sbuild chroots/, + 'an incomplete shared manifest is rejected before building'); my $unknown_manifest = "$tmp/deb-version-1-unknown.conf"; write_apt_manifest( From 0e3e2461c3be358749d1dc15bc05d7618736baee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sun, 6 Sep 2026 11:43:10 -0300 Subject: [PATCH 27/28] fix(genesis): centralize package name prefixes --- lib/XCAT/GenesisRelease.pm | 16 ++++++++++++++-- mockbuild-all.pl | 5 +++-- sbuild-all.pl | 2 +- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/lib/XCAT/GenesisRelease.pm b/lib/XCAT/GenesisRelease.pm index c4b3bee..b05f9a1 100644 --- a/lib/XCAT/GenesisRelease.pm +++ b/lib/XCAT/GenesisRelease.pm @@ -8,10 +8,12 @@ use XCAT::BuildUtils qw(digest_file read_lines relative_files); our @EXPORT_OK = qw( architectures + deb_package_prefix deb_package_name minimum_release_version read_checksum_manifest read_release_manifest + rpm_package_prefix rpm_package_name validated_release_checksums validate_architecture @@ -25,6 +27,8 @@ our @EXPORT_OK = qw( my @RELEASE_V1_ARCHITECTURES = qw(x86 x86_64 ppc64 ppc64le armv7hf aarch64 riscv64); my @RELEASE_V2_ARCHITECTURES = (@RELEASE_V1_ARCHITECTURES, 's390x'); my @ARCHITECTURES = @RELEASE_V2_ARCHITECTURES; +my $RPM_PACKAGE_PREFIX = 'xCAT-genesis-openembedded-'; +my $DEB_PACKAGE_PREFIX = 'xcat-genesis-openembedded-'; my %ARCHITECTURE = map { $_ => 1 } @ARCHITECTURES; my %RELEASE_ARCHITECTURES = ( 1 => \@RELEASE_V1_ARCHITECTURES, @@ -42,17 +46,25 @@ sub validate_architecture { return $architecture; } +sub rpm_package_prefix { + return $RPM_PACKAGE_PREFIX; +} + sub rpm_package_name { my ($architecture) = @_; validate_architecture($architecture); - return "xCAT-genesis-openembedded-$architecture"; + return $RPM_PACKAGE_PREFIX . $architecture; +} + +sub deb_package_prefix { + return $DEB_PACKAGE_PREFIX; } sub deb_package_name { my ($architecture) = @_; validate_architecture($architecture); $architecture =~ tr/_/-/; - return "xcat-genesis-openembedded-$architecture"; + return $DEB_PACKAGE_PREFIX . $architecture; } sub minimum_release_version { diff --git a/mockbuild-all.pl b/mockbuild-all.pl index 154bc85..d4b7b5e 100755 --- a/mockbuild-all.pl +++ b/mockbuild-all.pl @@ -34,6 +34,7 @@ use XCAT::BuildUtils qw( ); use XCAT::GenesisRelease qw( architectures + rpm_package_prefix rpm_package_name validate_repository_packages validated_release_checksums @@ -367,6 +368,7 @@ if ($genesis_release ne '') { unless -d $genesis_release; my $verifier = "$script_dir/genesis-openembedded/verify-release"; die "Genesis release verifier not found: $verifier\n" unless -x $verifier; + common_repository_requirements(); # Checksum, verify, checksum again. The verifier reads the tree it validates, so a # release rewritten together with its SHA256SUMS while the verifier runs would satisfy # both the verifier and any single pass taken afterwards; comparing the pass taken @@ -376,7 +378,6 @@ if ($genesis_release ne '') { my $checksums_after = validated_release_checksums($genesis_release); die "Genesis release changed during verification\n" unless hashes_equal($checksums_before, $checksums_after); - common_repository_requirements(); $genesis_release_checksums = $checksums_before; } @@ -1124,7 +1125,7 @@ sub common_repository_requirements { return validate_repository_packages( \%common, 'common', - 'xCAT-genesis-openembedded-', + rpm_package_prefix(), map { rpm_package_name($_) } architectures(), ); } diff --git a/sbuild-all.pl b/sbuild-all.pl index b008692..60007aa 100755 --- a/sbuild-all.pl +++ b/sbuild-all.pl @@ -1069,7 +1069,7 @@ sub shared_repository_requirements { return XCAT::GenesisRelease::validate_repository_packages( \%shared, 'shared', - 'xcat-genesis-openembedded-', + XCAT::GenesisRelease::deb_package_prefix(), map { XCAT::GenesisRelease::deb_package_name($_) } XCAT::GenesisRelease::architectures(), ); From 043a7f603952a89ca9f71d71ecec464fc3758fdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Sun, 6 Sep 2026 11:43:10 -0300 Subject: [PATCH 28/28] test(genesis): correct packager skip counts --- t/genesis_openembedded_release.t | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/t/genesis_openembedded_release.t b/t/genesis_openembedded_release.t index bde5201..5a35d15 100644 --- a/t/genesis_openembedded_release.t +++ b/t/genesis_openembedded_release.t @@ -417,7 +417,7 @@ SKIP: { } SKIP: { - skip 'rpmbuild and rpm are not installed', 18 + skip 'rpmbuild and rpm are not installed', 19 unless command_exists('rpmbuild') && command_exists('rpm'); exercise_packager('rpm'); } @@ -430,7 +430,7 @@ SKIP: { } SKIP: { - skip 'dpkg-deb is not installed', 12 unless command_exists('dpkg-deb'); + skip 'dpkg-deb is not installed', 14 unless command_exists('dpkg-deb'); exercise_packager('deb'); }