diff --git a/buildrpms.pl b/buildrpms.pl
index b4461ad4a..f6d3ed4db 100755
--- a/buildrpms.pl
+++ b/buildrpms.pl
@@ -134,6 +134,7 @@ my @PACKAGES = qw(
xCAT-server
xCAT-test
xCAT-vlan
+ xCAT-release
);
my @TARGETS = (
@@ -628,6 +629,11 @@ sub createrepo_dir {
# src.rpm enters the binary repomd, and index the SRPMS repo separately.
sub index_repo {
my ($repodir) = @_;
+ my $alias = "$repodir/xCAT-release-latest.noarch.rpm";
+
+ # The stable bootstrap filename is a direct-download convenience, not a
+ # second package. Keep it out of repository metadata.
+ unlink $alias if -f $alias;
say "Creating repository $repodir";
# Drop the top-level stray src.rpm and the mock logs (build.log/root.log/...)
# that mock leaves in the resultdir, so the dir is directly deployable (upstream
@@ -640,7 +646,19 @@ sub index_repo {
sub update_repo {
my ($target) = @_;
- index_repo("dist/$target/rpms");
+ my $repodir = "dist/$target/rpms";
+ index_repo($repodir);
+}
+
+sub write_release_alias {
+ my ($repodir) = @_;
+ my $alias = "$repodir/xCAT-release-latest.noarch.rpm";
+
+ my @release_rpms = glob("$repodir/xCAT-release-$VERSION-$RELEASE.noarch.rpm");
+ if (@release_rpms == 1) {
+ cp $release_rpms[0], $alias;
+ chmod 0644, $alias;
+ }
}
sub sign_rpms {
@@ -764,6 +782,7 @@ sub finalize_core {
sign_repo_dir($dir, $opts{gpg_key_name});
}
write_repo_metadata_dir($dir);
+ write_release_alias($dir);
return 0;
}
@@ -816,6 +835,12 @@ sub main {
write_repo_metadata($target);
}
+ # Signing regenerates repository metadata, so create the direct-download
+ # alias only after the final metadata pass.
+ for my $target ($opts{targets}->@*) {
+ write_release_alias("dist/$target/rpms");
+ }
+
exit(0);
}
diff --git a/docs/source/guides/install-guides/yum/configure_xcat.rst b/docs/source/guides/install-guides/yum/configure_xcat.rst
index 2f387d155..fba2ec140 100644
--- a/docs/source/guides/install-guides/yum/configure_xcat.rst
+++ b/docs/source/guides/install-guides/yum/configure_xcat.rst
@@ -6,13 +6,14 @@ xCAT software and repo files can be obtained from: ``_, navigate to the correct subdirectory for the target machine and download the ``xcat-dep.repo`` file and copy it to ``/etc/yum.repos.d``.
+The dependency repository is selected automatically from the operating-system
+release and architecture reported by DNF. The installed repository files are
+marked as configuration files, so package upgrades preserve local changes.
Continue to the next section to install xCAT.
@@ -28,4 +29,3 @@ Local Repository
.. include:: ../common_sections.rst
:start-after: BEGIN_configure_xcat_local_repo_xcat-dep_RPM
:end-before: END_configure_xcat_local_repo_xcat-dep_RPM
-
diff --git a/xCAT-release/RPM-GPG-KEY-xCAT b/xCAT-release/RPM-GPG-KEY-xCAT
new file mode 100644
index 000000000..d5071b497
--- /dev/null
+++ b/xCAT-release/RPM-GPG-KEY-xCAT
@@ -0,0 +1,89 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+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
+G9DsOI3iK1B2pWUQZYMnWLCJfzQ=
+=b6W6
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/xCAT-release/xCAT-release.spec b/xCAT-release/xCAT-release.spec
new file mode 100644
index 000000000..27e5792ed
--- /dev/null
+++ b/xCAT-release/xCAT-release.spec
@@ -0,0 +1,35 @@
+Summary: xCAT DNF repository configuration
+Name: xCAT-release
+Version: %{?version:%{version}}%{!?version:%(cat Version)}
+Release: %{?release:%{release}}%{!?release:%(cat Release)}
+License: EPL
+URL: https://xcat.org/
+Source0: xCAT-release-%{version}.tar.gz
+BuildArch: noarch
+Requires: dnf
+
+%description
+Installs the xCAT core and dependency DNF repository definitions and the public
+signing key used to verify their packages and repository metadata.
+
+%prep
+%setup -q -n xCAT-release
+
+%build
+
+%install
+rm -rf %{buildroot}
+mkdir -p %{buildroot}%{_sysconfdir}/yum.repos.d
+mkdir -p %{buildroot}%{_sysconfdir}/pki/rpm-gpg
+install -m 0644 xcat-core.repo %{buildroot}%{_sysconfdir}/yum.repos.d/xcat-core.repo
+install -m 0644 xcat-dep.repo %{buildroot}%{_sysconfdir}/yum.repos.d/xcat-dep.repo
+install -m 0644 RPM-GPG-KEY-xCAT %{buildroot}%{_sysconfdir}/pki/rpm-gpg/RPM-GPG-KEY-xCAT
+
+%files
+%config(noreplace) %{_sysconfdir}/yum.repos.d/xcat-core.repo
+%config(noreplace) %{_sysconfdir}/yum.repos.d/xcat-dep.repo
+%{_sysconfdir}/pki/rpm-gpg/RPM-GPG-KEY-xCAT
+
+%changelog
+* Sat Jul 11 2026 xCAT Project
+- Add the xCAT repository bootstrap package
diff --git a/xCAT-release/xcat-core.repo b/xCAT-release/xcat-core.repo
new file mode 100644
index 000000000..c6add8aa4
--- /dev/null
+++ b/xCAT-release/xcat-core.repo
@@ -0,0 +1,7 @@
+[xcat-core]
+name=xCAT Core packages
+baseurl=https://xcat.org/files/xcat/repos/yum/latest/xcat-core
+enabled=1
+gpgcheck=1
+repo_gpgcheck=1
+gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-xCAT
diff --git a/xCAT-release/xcat-dep.repo b/xCAT-release/xcat-dep.repo
new file mode 100644
index 000000000..6da572b2d
--- /dev/null
+++ b/xCAT-release/xcat-dep.repo
@@ -0,0 +1,7 @@
+[xcat-dep]
+name=xCAT dependencies
+baseurl=https://xcat.org/files/xcat/repos/yum/latest/xcat-dep/rh$releasever/$basearch
+enabled=1
+gpgcheck=1
+repo_gpgcheck=1
+gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-xCAT
diff --git a/xCAT-test/unit/xcat_release_package.t b/xCAT-test/unit/xcat_release_package.t
new file mode 100644
index 000000000..f81b44148
--- /dev/null
+++ b/xCAT-test/unit/xcat_release_package.t
@@ -0,0 +1,100 @@
+#!/usr/bin/env perl
+use strict;
+use warnings;
+
+use Digest::SHA qw(sha256_hex);
+use File::Spec;
+use FindBin;
+use Test::More;
+
+my $repo_root = File::Spec->rel2abs(File::Spec->catdir($FindBin::Bin, '..', '..'));
+
+my $spec = read_file('xCAT-release/xCAT-release.spec');
+like($spec, qr/^Name:\s+xCAT-release$/m, 'package has the expected name');
+like($spec, qr/^Source0:\s+xCAT-release-%\{version\}\.tar\.gz$/m, 'source archive follows the package name');
+like($spec, qr/^BuildArch:\s+noarch$/m, 'package is architecture independent');
+like($spec, qr/^Requires:\s+dnf$/m, 'package is limited to DNF-based systems');
+like($spec, qr/^%config\(noreplace\) .*xcat-core\.repo$/m, 'core repo preserves local changes');
+like($spec, qr/^%config\(noreplace\) .*xcat-dep\.repo$/m, 'dependency repo preserves local changes');
+like($spec, qr{RPM-GPG-KEY-xCAT}, 'package installs the signing key');
+
+my $core = read_file('xCAT-release/xcat-core.repo');
+assert_repo_security($core, 'core');
+like(
+ $core,
+ qr{^baseurl=https://xcat\.org/files/xcat/repos/yum/latest/xcat-core$}m,
+ 'core repo uses the stable HTTPS endpoint'
+);
+
+my $dep = read_file('xCAT-release/xcat-dep.repo');
+assert_repo_security($dep, 'dependency');
+like(
+ $dep,
+ qr{^baseurl=https://xcat\.org/files/xcat/repos/yum/latest/xcat-dep/rh\$releasever/\$basearch$}m,
+ 'dependency repo follows the DNF release and architecture variables'
+);
+
+my $key = read_file('xCAT-release/RPM-GPG-KEY-xCAT');
+like($key, qr/^-----BEGIN PGP PUBLIC KEY BLOCK-----$/m, 'signing key is ASCII armored');
+is(
+ sha256_hex($key),
+ '72076f25ce4929d34a67e305327a37f89c964d3cbf1821e3afad4907c9d91249',
+ 'packaged key matches the published xCAT signing key'
+);
+
+my $builder = read_file('buildrpms.pl');
+like($builder, qr/^\s+xCAT-release\s*$/m, 'default RPM build includes xCAT-release');
+like(
+ $builder,
+ qr{\$repodir/xCAT-release-latest\.noarch\.rpm},
+ 'stable bootstrap alias follows the package name'
+);
+like(
+ $builder,
+ qr{\$repodir/xCAT-release-\$VERSION-\$RELEASE\.noarch\.rpm},
+ 'stable bootstrap alias selects the xCAT-release RPM'
+);
+like(
+ $builder,
+ qr/unlink \$alias.*?createrepo_dir\(\$repodir/s,
+ 'stable bootstrap alias is excluded from repository metadata'
+);
+like(
+ $builder,
+ qr/cp \$release_rpms\[0\], \$alias/,
+ 'repository export creates the stable bootstrap filename'
+);
+my $sign_call = rindex($builder, 'sign_rpms($target)');
+my $alias_call = rindex($builder, 'write_release_alias("dist/$target/rpms")');
+ok(
+ $sign_call >= 0 && $alias_call > $sign_call,
+ 'stable bootstrap alias is created after signed metadata is finalized'
+);
+like(
+ $builder,
+ qr/sub finalize_core \{.*?write_repo_metadata_dir\(\$dir\);.*?write_release_alias\(\$dir\);/s,
+ 'assembled core repository creates the stable alias after final metadata'
+);
+
+done_testing();
+
+sub assert_repo_security {
+ my ($content, $label) = @_;
+ like($content, qr/^enabled=1$/m, "$label repo is enabled");
+ like($content, qr/^gpgcheck=1$/m, "$label repo verifies packages");
+ like($content, qr/^repo_gpgcheck=1$/m, "$label repo verifies repository metadata");
+ like(
+ $content,
+ qr{^gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-xCAT$}m,
+ "$label repo uses the packaged signing key"
+ );
+}
+
+sub read_file {
+ my ($file) = @_;
+ my $path = File::Spec->catfile($repo_root, split m{/}, $file);
+ open(my $fh, '<', $path) or die "open $path: $!";
+ my $contents = do { local $/; <$fh> };
+ close($fh) or die "close $path: $!";
+ return $contents;
+}