diff --git a/BUILD.md b/BUILD.md index a9c866f..6c7dd1d 100644 --- a/BUILD.md +++ b/BUILD.md @@ -438,8 +438,21 @@ parsing/resolution), separate from the disk/gpg I/O. - **Duplicate = hard error:** a package appearing in the index with **two distinct versions** (a stale `.deb` not cleaned from the pool) makes `parse_packages_index` **die loudly** rather than keep one — identical behaviour to the EL `rpm_version` gate. -- **Version pins** are the manifest's *upstream* version; the published Debian version's epoch/revision - is stripped (`deb_upstream_version`) before the pin compare. +- **Version pins** match the **full** Debian version — `[epoch:]upstream[-revision]` — exactly as it + appears in the built `.deb` and in the published index. The revision is the *packaging* revision + (`elilo-xcat` 3.14-5 and 3.14-6 are different builds of the same upstream 3.14) and the epoch + overrides version comparison outright, so an upstream-only pin would accept a deb that the gate + calls good and `apt install xCAT` then refuses — xCAT's own `debian/control` declares versioned + dependencies (`goconserver (>= 0.3.3-snap…)`, `ipmitool-xcat (>= 1.8.17-1)`, `grub2-xcat (>= 2.02-…)`). + Two pins stay globbed on purpose: `goconserver=0.3.3-snap*` (upstream exact; the revision is the CD + stamp, which changes every run — the glob still *requires* a snap stamp) and `xcat-genesis-base=2.*` + (its version is not owned by xcat-dep, and each arch is converted from its own genesis rpm). +- **Only build output is collected.** Some package directories carry prebuilt `.deb` files in the + checkout — `elilo/` ships `elilo-xcat_3.14-5_all.deb` and `gnu-efi_3.0v-5_amd64.deb`. Those arrive + with the copied source tree and are **not** output of the build, so the collector snapshots the + `.deb`s present before the build and subtracts them afterwards (a file the build overwrites changes + size/mtime and still counts). Without this, a stale checked-in artifact is republished under the + current run's name — and with full-version pins it also collides with the freshly built one. # Packages notes diff --git a/BuildUtils.pm b/BuildUtils.pm index 964cb2f..52071fc 100644 --- a/BuildUtils.pm +++ b/BuildUtils.pm @@ -30,7 +30,7 @@ our @EXPORT_OK = qw( codename_to_version version_to_codename known_codenames chroot_name chroot_sources_list chroot_is_disposable chroot_build_script control_field genesis_deb_control - deb_field deb_version deb_upstream_version deb_hash cross_copy_genesis_deb + deb_field deb_version deb_hash cross_copy_genesis_deb build_deb_in_chroot ); @@ -253,7 +253,7 @@ sub parse_release_architectures { # parse_packages_index($text) -> \%{ package_name => version } # Parse a Debian 'Packages' index: RFC822 stanzas separated by blank line(s); each carries a # 'Package:' and a 'Version:'. Returns name => version (the FULL Debian version verbatim, epoch + -# revision included -- the caller strips to the upstream part with deb_upstream_version). A stanza +# revision included -- which is exactly what the manifest pin is compared against). A stanza # lacking either field is skipped; malformed/empty input yields an empty hash. # DUPLICATE = LOUD ERROR: apt-ftparchive emits one stanza per package, so a name appearing twice with # DISTINCT versions means a stale .deb was not cleaned from the pool before assemble -- a version pin @@ -322,10 +322,13 @@ sub skip_arch_all_on { return ((control_binary_arch($control_text, $binpkg) // '') eq 'all') ? 1 : 0; } -# resolve_present_names(\%parsed, $arch, \@names) -> \%present (name => upstream version | undef) +# resolve_present_names(\%parsed, $arch, \@names) -> \%present (name => FULL version | undef) # PURE. Resolves each manifest package NAME to the version actually in the parsed index (\%parsed from -# parse_packages_index), reducing to the upstream version so it compares against the manifest's -# upstream pins. Resolution order: +# parse_packages_index), returning the FULL Debian version -- [epoch:]upstream[-revision], verbatim -- +# because that is what the manifest pins. Comparing only the upstream part would accept a package with +# the right upstream version but the wrong epoch or a stale packaging revision, which is exactly what +# xCAT's own versioned Depends (goconserver >= 0.3.3-snap..., ipmitool-xcat >= 1.8.17-1) care about. +# Resolution order: # - exact index key (e.g. ipmitool-xcat), else # - the arch-suffixed key for THIS cell's arch (e.g. xcat-genesis-base -> xcat-genesis-base-). # It deliberately does NOT fall back to a DIFFERENT-arch suffix: the xcat-genesis-base- debs are @@ -338,7 +341,7 @@ sub resolve_present_names { my $full; if (exists $parsed->{$name}) { $full = $parsed->{$name}; } elsif (exists $parsed->{"$name-$arch"}) { $full = $parsed->{"$name-$arch"}; } - $present{$name} = defined $full ? deb_upstream_version($full) : undef; + $present{$name} = $full; } return \%present; } @@ -479,36 +482,38 @@ sub deb_field { return defined $v ? $v : ''; } -# deb_upstream_version: the UPSTREAM part of a Debian version — strip a leading "epoch:" and the -# trailing "-" (dpkg splits the revision at the LAST dash). So '1.8.18-4' -> '1.8.18', -# '2:0.3.3-snap202608101400.57' -> '0.3.3'. This is what the manifest pins (the Release/revision -# carries the codename/snap stamp and is intentionally NOT pinned), mirroring EL rpm_version's %{version}. -sub deb_upstream_version { - my ($v) = @_; - return $v unless defined $v; - $v =~ s/^\d+://; # drop epoch - $v =~ s/-[^-]*$//; # drop the last -revision segment - return $v; -} - -# deb_version: the UPSTREAM Version of the built binary .deb named _*.deb under $dir (undef if -# absent). $pkg is the binary package name (e.g. 'ipmitool-xcat', 'goconserver', or the logical -# 'xcat-genesis-base' which matches the arch-suffixed xcat-genesis-base-amd64/-ppc64el). Dies if the -# dir holds more than one DISTINCT upstream version of the package (a stale artifact not cleaned -# before the build — a version pin could otherwise pass against the wrong deb and both could ship). +# deb_version: the FULL Version ([epoch:]upstream[-revision]) of the built binary .deb named +# _*.deb under $dir (undef if absent) -- the same string the published index carries, so +# build-time validation and publish-time verification compare like with like against the manifest pin. +# $pkg is the binary package name (e.g. 'ipmitool-xcat', 'goconserver') or the logical +# 'xcat-genesis-base'. +# +# $arch (optional) disambiguates that logical name: the genesis debs are arch-SUFFIXED +# (xcat-genesis-base-amd64 / -ppc64el) and are legitimately built from two DIFFERENT genesis rpms, so +# their revisions differ (2.19.0-snap202607261133 vs ...271832). On the amd64 host both are staged +# (the cross-arch ppc one for #7610), so without $arch the two would look like a version conflict. +# Pass the target arch and only that arch's genesis is considered -- matching resolve_present_names, +# which likewise never borrows another arch's genesis. +# +# Dies if the dir holds more than one DISTINCT version of the package (a stale artifact not cleaned +# before the build -- a version pin could otherwise pass against the wrong deb and both could ship). # Mirrors MockBuildUtils::rpm_version. sub deb_version { - my ($dir, $pkg) = @_; - my $glob = ($pkg eq 'xcat-genesis-base') - ? "$dir/xcat-genesis-base-*_*.deb" + my ($dir, $pkg, $arch) = @_; + my $is_genesis = ($pkg eq 'xcat-genesis-base'); + my $glob = $is_genesis + ? ((defined $arch && $arch ne '') ? "$dir/xcat-genesis-base-$arch\_*.deb" + : "$dir/xcat-genesis-base-*_*.deb") : "$dir/${pkg}_*.deb"; my %vers; for my $f (sort glob($glob)) { my $n = deb_field($f, 'Package'); - my $match = ($pkg eq 'xcat-genesis-base') - ? ($n =~ /^xcat-genesis-base-/) : ($n eq $pkg); + my $match = $is_genesis + ? ($n =~ /^xcat-genesis-base-/ + && (!defined $arch || $arch eq '' || $n eq "xcat-genesis-base-$arch")) + : ($n eq $pkg); next unless $match; - my $v = deb_upstream_version(deb_field($f, 'Version')); + my $v = deb_field($f, 'Version'); $vers{$v} = 1 if defined $v && $v ne ''; } return undef unless %vers; @@ -614,6 +619,13 @@ W=$(mktemp -d) cp -a "$PKGSRC" "$W/pkg" cd "$W/pkg" +# Some package directories carry PREBUILT .deb files in the checkout (elilo/ ships +# elilo-xcat_3.14-5_all.deb and gnu-efi_3.0v-5_amd64.deb). Those arrive with the copied source tree +# and are NOT output of this build -- collecting them republishes a stale, unrelated artifact under +# this run's name. Record what is already present so the collector can subtract it. A file the build +# OVERWRITES (elilo-xcat_3.14-6_all.deb) changes size/mtime, so it still counts as build output. +find "$W" -maxdepth 3 -name '*.deb' -printf '%s %T@ %p\n' | sort > "$W/.debs-before" + # Declared Build-Depends, resolved by mk-build-deps: it hands debian/control's relationships to apt # verbatim, so versions/alternatives/arch-qualifiers are honoured. FATAL on failure -- an unsatisfied # build dependency must stop the build, never be papered over by whatever the chroot already carries. @@ -626,14 +638,19 @@ fi printf '%s' "$B64" | base64 -d > "$W/pkgbuild.sh" ( cd "$W/pkg" && bash "$W/pkgbuild.sh" ) -# Collect the built binaries. The mk-build-deps dummy package (-build-deps_*.deb) and debug -# symbols are not build output and must never reach the repo. -mapfile -t found < <(find "$W" -maxdepth 3 -name '*.deb' \ - ! -name '*-dbgsym_*' ! -name '*-build-deps_*' -print | sort) +# Collect ONLY what this build produced: everything that is new or changed since the snapshot taken +# before the build. Prebuilt debs that came in with the checkout are subtracted, and the mk-build-deps +# dummy package (-build-deps_*.deb) and debug symbols are excluded outright -- none of the three +# is build output, and none may reach the repo. +find "$W" -maxdepth 3 -name '*.deb' ! -name '*-dbgsym_*' ! -name '*-build-deps_*' \ + -printf '%s %T@ %p\n' | sort > "$W/.debs-after" +mapfile -t found < <(comm -13 "$W/.debs-before" "$W/.debs-after" | sed 's/^[^ ]* [^ ]* //') if [ "${#found[@]}" -eq 0 ]; then - echo "FATAL: the package build produced no .deb" >&2 + echo "FATAL: the package build produced no .deb (only prebuilt artifacts from the checkout)" >&2 exit 1 fi +comm -12 "$W/.debs-before" "$W/.debs-after" | sed 's/^[^ ]* [^ ]* //' \ + | while read -r f; do echo "prebuilt in the checkout, NOT collected: $f"; done mkdir -p "$OUT" for d in "${found[@]}"; do cp -v "$d" "$OUT/"; done INNER diff --git a/debs-manifest.conf b/debs-manifest.conf index 8db959d..6129fb3 100644 --- a/debs-manifest.conf +++ b/debs-manifest.conf @@ -5,12 +5,28 @@ # where is the produced .deb's binary name (or the logical 'xcat-genesis-base', # which matches the arch-suffixed xcat-genesis-base-amd64 / xcat-genesis-base-ppc64el) and # is one of: -# - an exact upstream version (e.g. 1.8.18) -- the build must produce exactly it; -# - a shell-style glob (e.g. 2.*) -- the built upstream version must match it (* and ?); -# - '*' -- any version accepted. -# Only the UPSTREAM version is matched, never the Debian revision (which carries the -snap. CD -# stamp and the per-codename build). Bump an exact pin here when the in-tree debian/changelog upstream -# version is bumped. +# - an exact version (e.g. 1.8.18-4) -- the build must produce exactly it; +# - a shell-style glob (e.g. 2.*) -- the built version must match it (* and ?); +# - '*' -- any version accepted. +# +# The pin is matched against the FULL Debian version -- [epoch:]upstream[-revision] -- exactly as it +# appears in the built .deb and in the published Packages index. That is deliberate: a package can +# carry the right upstream version and still be wrong for xCAT, because +# * the debian_revision is the PACKAGING revision (elilo-xcat 3.14-5 vs 3.14-6 are different builds +# of the same upstream 3.14), and +# * the epoch overrides version comparison outright (2:2.18.0 outranks any un-epoched 2.19.0). +# xCAT's own debian/control declares versioned dependencies on these packages -- goconserver +# (>= 0.3.3-snap...), ipmitool-xcat (>= 1.8.17-1), grub2-xcat (>= 2.02-...) -- so an upstream-only pin +# would accept a deb the gate calls good and `apt install xCAT` then refuses. Bump the pin here when +# the in-tree debian/changelog is bumped (the revision is part of the pin now, so a changelog bump +# that only moves the revision must be reflected here too). +# +# Two pins are deliberately NOT fully exact, because their revision is not owned by the checkout: +# * goconserver=0.3.3-snap* -- upstream is pinned exactly; the revision is the CD stamp +# snap, which changes every run. The glob still REQUIRES a snap-stamped +# revision, so an unstamped or hand-built deb is rejected. +# * xcat-genesis-base=2.* -- its version is not owned by xcat-dep at all (see the note below); +# and the two arches legitimately differ, since each is converted from its own genesis rpm. # # PER-ARCH SETS (review concern #3 -- the arch matrix must be valid): # * Compiled, arch-specific deps that genuinely build on BOTH arches are listed for amd64 AND @@ -27,7 +43,10 @@ # * xcat-genesis-base is arch:all but arch-SPECIFIC content (the netboot image for that arch), so # each arch produces its own (xcat-genesis-base-); the finalize step cross-populates both # into every codename pool for cross-arch netboot (issue #7610). Pinned 2.* -- its Version walks -# with the paired xcat-core (not owned by xcat-dep), exactly as on the EL side. +# with the paired xcat-core (not owned by xcat-dep), exactly as on the EL side. The gate still +# enforces that EXACTLY ONE genesis is present FOR THIS ARCH: the name resolves to +# xcat-genesis-base- and never borrows the other arch's, and two distinct versions of it in +# one staging dir is a hard error. # # sbuild-all.pl reads this file and, per target, builds ONLY the listed packages and then validates # that EVERY listed package was produced at the pinned version -- any missing or mismatched package @@ -36,84 +55,84 @@ # ============================ focal (ubuntu20.04) ============================ [focal-amd64] -ipmitool-xcat=1.8.18 -conserver-xcat=8.2.1 -goconserver=0.3.3 -syslinux-xcat=3.86 -grub2-xcat=2.12 -elilo-xcat=3.14 -xnba-undi=1.21.1 +ipmitool-xcat=1.8.18-4 +conserver-xcat=8.2.1-1 +goconserver=0.3.3-snap* +syslinux-xcat=3.86-2 +grub2-xcat=2.12-1 +elilo-xcat=3.14-6 +xnba-undi=1.21.1-1 xcat-genesis-base=2.* [focal-ppc64el] -ipmitool-xcat=1.8.18 -conserver-xcat=8.2.1 -goconserver=0.3.3 -syslinux-xcat=3.86 -grub2-xcat=2.12 -elilo-xcat=3.14 -xnba-undi=1.21.1 +ipmitool-xcat=1.8.18-4 +conserver-xcat=8.2.1-1 +goconserver=0.3.3-snap* +syslinux-xcat=3.86-2 +grub2-xcat=2.12-1 +elilo-xcat=3.14-6 +xnba-undi=1.21.1-1 xcat-genesis-base=2.* # ============================ jammy (ubuntu22.04) ============================ [jammy-amd64] -ipmitool-xcat=1.8.18 -conserver-xcat=8.2.1 -goconserver=0.3.3 -syslinux-xcat=3.86 -grub2-xcat=2.12 -elilo-xcat=3.14 -xnba-undi=1.21.1 +ipmitool-xcat=1.8.18-4 +conserver-xcat=8.2.1-1 +goconserver=0.3.3-snap* +syslinux-xcat=3.86-2 +grub2-xcat=2.12-1 +elilo-xcat=3.14-6 +xnba-undi=1.21.1-1 xcat-genesis-base=2.* [jammy-ppc64el] -ipmitool-xcat=1.8.18 -conserver-xcat=8.2.1 -goconserver=0.3.3 -syslinux-xcat=3.86 -grub2-xcat=2.12 -elilo-xcat=3.14 -xnba-undi=1.21.1 +ipmitool-xcat=1.8.18-4 +conserver-xcat=8.2.1-1 +goconserver=0.3.3-snap* +syslinux-xcat=3.86-2 +grub2-xcat=2.12-1 +elilo-xcat=3.14-6 +xnba-undi=1.21.1-1 xcat-genesis-base=2.* # ============================ noble (ubuntu24.04) ============================ [noble-amd64] -ipmitool-xcat=1.8.18 -conserver-xcat=8.2.1 -goconserver=0.3.3 -syslinux-xcat=3.86 -grub2-xcat=2.12 -elilo-xcat=3.14 -xnba-undi=1.21.1 +ipmitool-xcat=1.8.18-4 +conserver-xcat=8.2.1-1 +goconserver=0.3.3-snap* +syslinux-xcat=3.86-2 +grub2-xcat=2.12-1 +elilo-xcat=3.14-6 +xnba-undi=1.21.1-1 xcat-genesis-base=2.* [noble-ppc64el] -ipmitool-xcat=1.8.18 -conserver-xcat=8.2.1 -goconserver=0.3.3 -syslinux-xcat=3.86 -grub2-xcat=2.12 -elilo-xcat=3.14 -xnba-undi=1.21.1 +ipmitool-xcat=1.8.18-4 +conserver-xcat=8.2.1-1 +goconserver=0.3.3-snap* +syslinux-xcat=3.86-2 +grub2-xcat=2.12-1 +elilo-xcat=3.14-6 +xnba-undi=1.21.1-1 xcat-genesis-base=2.* # ============================ resolute (ubuntu26.04) ========================= [resolute-amd64] -ipmitool-xcat=1.8.18 -conserver-xcat=8.2.1 -goconserver=0.3.3 -syslinux-xcat=3.86 -grub2-xcat=2.12 -elilo-xcat=3.14 -xnba-undi=1.21.1 +ipmitool-xcat=1.8.18-4 +conserver-xcat=8.2.1-1 +goconserver=0.3.3-snap* +syslinux-xcat=3.86-2 +grub2-xcat=2.12-1 +elilo-xcat=3.14-6 +xnba-undi=1.21.1-1 xcat-genesis-base=2.* [resolute-ppc64el] -ipmitool-xcat=1.8.18 -conserver-xcat=8.2.1 -goconserver=0.3.3 -syslinux-xcat=3.86 -grub2-xcat=2.12 -elilo-xcat=3.14 -xnba-undi=1.21.1 +ipmitool-xcat=1.8.18-4 +conserver-xcat=8.2.1-1 +goconserver=0.3.3-snap* +syslinux-xcat=3.86-2 +grub2-xcat=2.12-1 +elilo-xcat=3.14-6 +xnba-undi=1.21.1-1 xcat-genesis-base=2.* diff --git a/sbuild-all.pl b/sbuild-all.pl index 41efb3c..5059a0a 100755 --- a/sbuild-all.pl +++ b/sbuild-all.pl @@ -46,7 +46,7 @@ use BuildUtils qw(sh_quote print_step version_matches required_pkgs read_manifes codename_to_version known_codenames chroot_name chroot_sources_list chroot_is_disposable control_field genesis_deb_control - deb_field deb_version deb_upstream_version deb_hash cross_copy_genesis_deb); + deb_field deb_version deb_hash cross_copy_genesis_deb); my $script_dir = abs_path(dirname(__FILE__)); my $repo_root = $script_dir; @@ -640,7 +640,10 @@ sub validate_manifest { # presence on this arch is verified later against the PUBLISHED index (verify_assembled_repo). next if pkg_skip_on_arch($pkg, $arch); my $want = $MANIFEST{$tgt}{$pkg}; - my $got = deb_version($dir, $pkg); + # $arch disambiguates the logical 'xcat-genesis-base': both arch-suffixed genesis debs are + # staged on the amd64 host (the cross-arch ppc one for #7610) and they carry DIFFERENT + # revisions, so only this target's arch may be considered. + my $got = deb_version($dir, $pkg, $arch); if (!defined $got) { push @fail, "[$tgt] MISSING $pkg"; next; } push @fail, "[$tgt] $pkg: built $got, manifest pins $want" unless version_matches($got, $want); @@ -661,13 +664,14 @@ sub validate_manifest { # --------------------------------------------------------------------------------------------------- # repo_present_from_index($idx, @names): parse the PUBLISHED $idx (a binary-/Packages file) and -# resolve each required manifest @names against it, returning %present = (reqname => upstream-version | +# resolve each required manifest @names against it, returning %present = (reqname => FULL version | # undef). Name-resolution mirrors deb_version/validate_manifest: try an EXACT index key first (most # packages -- ipmitool-xcat, goconserver, grub2-xcat, the Architecture:all boot bits keep their plain # names), else exactly - for THIS cell's arch (the arch-suffixed xcat-genesis-base -> -# xcat-genesis-base-, never a different arch's). The published Version carries epoch+revision, so -# it is reduced to the UPSTREAM part (what the manifest pins) via deb_upstream_version before the pure -# comparator. (Resolution itself lives in the pure BuildUtils::resolve_present_names.) +# xcat-genesis-base-, never a different arch's). The version is passed to the comparator WHOLE -- +# [epoch:]upstream[-revision] -- because the manifest pins the whole thing, so a stale packaging +# revision or a wrong epoch is caught. (Resolution itself lives in the pure +# BuildUtils::resolve_present_names.) sub repo_present_from_index { my ($idx, $arch, @names) = @_; my $text = do { local $/; open my $fh, '<', $idx or die "FATAL: cannot read $idx: $!\n"; <$fh> }; diff --git a/t/sbuild-all.t b/t/sbuild-all.t index b53d9c1..78fdc93 100644 --- a/t/sbuild-all.t +++ b/t/sbuild-all.t @@ -19,7 +19,7 @@ use BuildUtils qw(required_pkgs version_matches read_manifest standard_options codename_to_version version_to_codename known_codenames chroot_name chroot_sources_list chroot_is_disposable chroot_build_script control_field genesis_deb_control - deb_field deb_version deb_upstream_version deb_hash cross_copy_genesis_deb + deb_field deb_version deb_hash cross_copy_genesis_deb build_deb_in_chroot); # Run a printing sub with STDOUT muted so its progress lines do not pollute TAP. @@ -215,10 +215,28 @@ PKG is(($m2 && $m2->{a}), '1-1', 'an identical repeated version is kept, not an error'); } -# ---- deb_upstream_version: strip epoch + debian revision ---------------------------------------- -is(deb_upstream_version('1.8.18-4'), '1.8.18', 'strip -revision'); -is(deb_upstream_version('2:0.3.3-snap202608101400.57'), '0.3.3', 'strip epoch and -revision'); -is(deb_upstream_version('3.86'), '3.86', 'native-ish version returned as-is'); +# ---- pins bite on the FULL version: revision and epoch are part of the comparison ----------------- +# A package can carry the right upstream version and still be wrong for xCAT -- the debian_revision is +# the packaging revision (elilo-xcat 3.14-5 vs 3.14-6) and the epoch overrides version comparison +# outright. These assert version_matches over WHOLE Debian versions, which is what the manifest pins. +{ + ok( version_matches('3.14-6', '3.14-6'), 'exact full pin matches the same revision'); + ok(!version_matches('3.14-5', '3.14-6'), + 'a STALE packaging revision is rejected (3.14-5 vs pin 3.14-6) -- upstream-only would pass'); + ok(!version_matches('1.8.18-3', '1.8.18-4'), 'an older ipmitool revision is rejected'); + ok(!version_matches('2:2.18.0-1', '2.18.0-1'), + 'an EPOCH that the pin does not name is rejected (2:2.18.0-1 vs pin 2.18.0-1)'); + ok( version_matches('2:2.18.0-1', '2:2.18.0-1'), 'an epoch IS accepted when the pin names it'); + + # the two pins that must stay globbed, and what they still enforce + ok( version_matches('0.3.3-snap202608212338', '0.3.3-snap*'), 'goconserver CD stamp matches 0.3.3-snap*'); + ok(!version_matches('0.3.3-1', '0.3.3-snap*'), + '... but an UNSTAMPED goconserver revision is rejected (the glob still demands a snap stamp)'); + ok(!version_matches('0.3.4-snap202608212338', '0.3.3-snap*'), + '... and upstream is still pinned exactly under the glob'); + ok( version_matches('2.19.0-snap202607261133', '2.*'), 'genesis 2.* walks with xcat-core'); + ok(!version_matches('1.9.0-snap202607261133', '2.*'), '... but not below 2'); +} # ---- .deb inspection + cross-arch genesis provisioning (needs dpkg-deb for real debs) ----------- SKIP: { @@ -248,15 +266,38 @@ SKIP: { { my $dir = "$tmp/vdir1"; make_path($dir); system("cp '$ipmi' '$dir/'"); - is(deb_version($dir, 'ipmitool-xcat'), '1.8.18', 'deb_version returns UPSTREAM version'); + is(deb_version($dir, 'ipmitool-xcat'), '1.8.18-4', + 'deb_version returns the FULL version, revision included'); } - # deb_version dies when a dir holds two DIFFERENT upstream versions of the same package. + # deb_version dies when a dir holds two DIFFERENT versions of the same package -- now including two + # revisions of the SAME upstream version, which upstream-only comparison used to collapse into one. { my $dir = "$tmp/vdir2"; make_path($dir); system("cp '" . $mk->('ipmitool-xcat', '1.8.18-4', 'amd64') . "' '$dir/'"); system("cp '" . $mk->('ipmitool-xcat', '1.8.19-1', 'amd64') . "' '$dir/'"); my $died = !eval { deb_version($dir, 'ipmitool-xcat'); 1 }; - ok($died, 'deb_version dies on multiple distinct upstream versions (stale artifact)'); + ok($died, 'deb_version dies on multiple distinct versions (stale artifact)'); + } + { + my $dir = "$tmp/vdir3"; make_path($dir); + system("cp '" . $mk->('elilo-xcat', '3.14-5', 'all') . "' '$dir/'"); + system("cp '" . $mk->('elilo-xcat', '3.14-6', 'all') . "' '$dir/'"); + my $died = !eval { deb_version($dir, 'elilo-xcat'); 1 }; + ok($died, 'deb_version dies on two REVISIONS of one upstream version (the elilo 3.14-5/-6 case)'); + } + # The logical 'xcat-genesis-base' spans two arch-suffixed packages that legitimately carry + # different revisions (each converted from its own genesis rpm) and are both staged on the amd64 + # host. $arch selects one; without it the pair would look like a version conflict. + { + my $dir = "$tmp/gdir"; make_path($dir); + system("cp '" . $mk->('xcat-genesis-base-amd64', '2.19.0-snap202607261133', 'all') . "' '$dir/'"); + system("cp '" . $mk->('xcat-genesis-base-ppc64el', '2.19.0-snap202607271832', 'all') . "' '$dir/'"); + is(deb_version($dir, 'xcat-genesis-base', 'amd64'), '2.19.0-snap202607261133', + 'deb_version picks THIS arch genesis when both are staged'); + is(deb_version($dir, 'xcat-genesis-base', 'ppc64el'), '2.19.0-snap202607271832', + '... and the other arch resolves to its own'); + my $died = !eval { deb_version($dir, 'xcat-genesis-base'); 1 }; + ok($died, 'without an arch the two genesis revisions are a conflict (so callers must pass it)'); } # genesis cross-copy: content identity by deb_hash, refresh-stale + idempotent + sign callback. @@ -316,11 +357,20 @@ SKIP: { my @names = ('ipmitool-xcat', 'xcat-genesis-base'); my $amd = resolve_present_names(\%parsed, 'amd64', \@names); - is($amd->{'ipmitool-xcat'}, '1.8.18', 'resolve: exact name reduced to upstream'); - is($amd->{'xcat-genesis-base'}, '2.19.0', 'resolve: genesis -> amd64-suffixed for the amd64 cell'); + is($amd->{'ipmitool-xcat'}, '1.8.18-snap202601010000', + 'resolve: exact name, FULL version handed to the comparator (revision kept)'); + is($amd->{'xcat-genesis-base'}, '2.19.0-snap202601010000', + 'resolve: genesis -> amd64-suffixed for the amd64 cell'); my $ppc = resolve_present_names(\%parsed, 'ppc64el', \@names); - is($ppc->{'xcat-genesis-base'}, '2.19.0', 'resolve: genesis -> ppc64el-suffixed for the ppc64el cell'); + is($ppc->{'xcat-genesis-base'}, '2.19.0-snap202601010000', + 'resolve: genesis -> ppc64el-suffixed for the ppc64el cell'); + + # The point of keeping the revision: a stale packaging revision in the published index is now a + # VERSION problem, where upstream-only comparison reported the repo as complete. + my @stale = verify_repo_packages({ 'elilo-xcat' => '3.14-6' }, { 'elilo-xcat' => '3.14-5' }); + like($stale[0], qr/^VERSION elilo-xcat: repo has 3\.14-5, manifest pins 3\.14-6$/, + 'a stale REVISION in the published index is caught (was a false PASS on upstream 3.14)'); # The false-PASS the reviewer caught: only the amd64 genesis is published. The ppc64el cell MUST # NOT resolve to it (that would mask a missing native ppc genesis, #7610) -> undef -> later MISSING. diff --git a/t/verify-repo.t b/t/verify-repo.t index 931698b..d3294e6 100644 --- a/t/verify-repo.t +++ b/t/verify-repo.t @@ -53,9 +53,9 @@ sub arch_all_stanzas { } sub native_stanzas { - my ($a) = @_; - return stanza('ipmitool-xcat', "1.8.18-snap202608211200", $a) - . stanza('goconserver', "0.3.3-snap202608211200", $a); + my ($a, %o) = @_; + return stanza('ipmitool-xcat', $o{ipmi_version} // '1.8.18-snap202608211200', $a) + . stanza('goconserver', '0.3.3-snap202608211200', $a); } # make_repo(%opt): a fixture apt tree for codename 'noble'. @@ -70,7 +70,8 @@ sub make_repo { my %native = map { $_ => 1 } @{ $o{native} // [@arches] }; my $dir = "$tmp/repo" . (++$repo_seq); for my $a (@arches) { - my $body = ($native{$a} ? native_stanzas($a) : '') . arch_all_stanzas($a); + my $body = ($native{$a} ? native_stanzas($a, ipmi_version => $o{ipmi_version}) : '') + . arch_all_stanzas($a); write_file("$dir/dists/noble/main/binary-$a/Packages", $body); } write_file("$dir/dists/noble/Release", @@ -82,16 +83,17 @@ sub make_repo { # The manifest is the source of truth for what each codename x arch must carry. my $manifest = "$tmp/debs-manifest.conf"; +# Pins are matched against the FULL Debian version, so the fixtures pin revisions too. write_file($manifest, <<'MAN'); [noble-amd64] -ipmitool-xcat=1.8.18 -goconserver=0.3.3 +ipmitool-xcat=1.8.18-snap202608211200 +goconserver=0.3.3-snap* grub2-xcat=* xcat-genesis-base=2.* [noble-ppc64el] -ipmitool-xcat=1.8.18 -goconserver=0.3.3 +ipmitool-xcat=1.8.18-snap202608211200 +goconserver=0.3.3-snap* grub2-xcat=* xcat-genesis-base=2.* MAN @@ -194,6 +196,27 @@ sub run_gate { '... and the log names the flag that turned it off'); } +# ---- a stale PACKAGING REVISION is caught, not just a wrong upstream version ---------------------- +# The manifest pin is matched against the FULL [epoch:]upstream[-revision]. An upstream-only gate +# reported this repo as complete: upstream 1.8.18 is what the pin used to say, and it matches -- while +# the published deb is an older packaging revision than xCAT's own Depends require. +{ + my $repo = make_repo(ipmi_version => '1.8.18-snap202501010000'); # older revision, same upstream + my ($rc, $out) = run_gate($repo, '--no-verify-signature'); + isnt($rc, 0, 'a stale packaging revision FAILS the gate') or diag($out); + like($out, qr/VERSION ipmitool-xcat: repo has 1\.8\.18-snap202501010000, manifest pins 1\.8\.18-snap202608211200/, + '... reported as a VERSION problem naming both revisions'); +} + +# An EPOCH the pin does not name is likewise a mismatch -- the epoch outranks the version entirely, +# so silently accepting it would let an un-pinned epoch bump through. +{ + my $repo = make_repo(ipmi_version => '2:1.8.18-snap202608211200'); + my ($rc, $out) = run_gate($repo, '--no-verify-signature'); + isnt($rc, 0, 'an epoch the pin does not name FAILS the gate') or diag($out); + like($out, qr/VERSION ipmitool-xcat: repo has 2:1\.8\.18-snap202608211200/, '... naming the epoch'); +} + # ---- a cell with no manifest section is a configuration error, not a free pass -------------------- { my $thin = "$tmp/thin-manifest.conf";