Working with RPM (dnf / yum)

CodeScoring.Save works as a regular rpm-md repository — one with repodata/repomd.xml in its root. It is understood by dnf and yum on RHEL, Rocky Linux, AlmaLinux, Fedora, CentOS, and other distributions of the family. You can create:

  • a proxy repository — a caching mirror of a distribution repository or a third-party repository (EPEL, vendor repositories, Nexus, Artifactory, another Save);
  • a hosted repository — storage for the company's own packages. There is no need to run createrepo: Save builds the metadata itself after every upload.

Creating a Repository in the Web Interface

  1. In the sidebar, choose Projects and open the project.
  2. Click Create repository.
  3. Fill in the common fields (for details, see Repository management):
    • Name — the repository name, for example Rocky 9 BaseOS. If the name contains the format and the type (for example, my RPM proxy), the Click here to automatically fill the format (rpm) and type (proxy) based on the name link under the field fills in Format, Type, and Proxy URL for you;
    • Key — the identifier used in the URL, for example rocky9-baseos. If left empty, it is generated from the name. It cannot be changed after creation;
    • Description and Color — optional.
  4. In the Format field, choose RPM.
  5. In the Type field, choose Proxy or Hosted.

Proxy Repository Fields

  • Proxy URL — the baseurl of the repository to proxy: the directory that contains repodata/repomd.xml. The autofill link under Name puts an example here — BaseOS for Rocky Linux 9; otherwise the field is empty.

    It is convenient to take the exact URL from a machine where this repository is already configured: dnf repolist -v shows it in the Repo-baseurl line. Keep in mind:

    • the $releasever and $basearch variables are not substituted in Proxy URL. Create a separate proxy repository for every combination of version and architecture (and for every repository: BaseOS, AppStream, EPEL…);
    • a concrete baseurl is required: mirrorlist and metalink URLs do not work;
    • repositories that require a client TLS certificate (for example, the Red Hat subscription CDN) cannot be proxied.
  • Cache TTL, seconds — how often Save revalidates the metadata (repomd.xml) against upstream. Packages never change and are cached permanently. For distribution repositories, 3600 (one hour) is a good value.

  • Authentication — how Save authenticates to upstream. For public mirrors, leave none.

    ValueFieldsWhat upstream receivesWhen to choose
    none—Requests without authenticationPublic distribution mirrors, EPEL
    basicUsername, PasswordAuthorization: Basic ...Upstream accepts a username and a password — just like the username and password options in a .repo file. This is the case for Nexus, Artifactory, and another Save
    bearerTokenAuthorization: Bearer <token>Upstream accepts a token in the Bearer header

    Once saved, the password and the token are never shown in the UI or returned by the API: on the edit form, a stored value appears as ******** — leave it as is to keep it. Save sends the credentials only to the host from Proxy URL (for details, see Upstream access for proxy repositories).

Hosted Repository Fields

  • RPM metadata folder depth — the depth of the indexing root. Leave it empty (or 0) if you need a single repodata/ for the whole repository — this fits most cases. If one repository will hold packages for several distributions, see Multiple indexing roots.
  1. Optionally attach cleanup policies in the Cleanup policy block: move the ones you need from the Available list to Applied.
  2. Click Create repository.

On the page of the created repository, the chain-link icon button shows a ready-made .repo file and commands for this particular repository.

Creating through the API

The same repositories can be created with an API request. Set format to rpm, pass the upstream credentials in the auth block, and the root depth in settings.rpm_root_depth:

# Proxy
curl -X POST https://save.example.com/api/v1/repos \
  -H "Content-Type: application/json" \
  -u "<username>:<password>" \
  -d '{
    "project": "infra",
    "name": "rocky9-baseos",
    "format": "rpm",
    "repository_type": "proxy",
    "remote_url": "https://dl.rockylinux.org/pub/rocky/9/BaseOS/x86_64/os",
    "cache_ttl": 3600
  }'

# Hosted
curl -X POST https://save.example.com/api/v1/repos \
  -H "Content-Type: application/json" \
  -u "<username>:<password>" \
  -d '{
    "project": "infra",
    "name": "rpm-hosted",
    "format": "rpm",
    "repository_type": "hosted"
  }'

For a proxy with authentication, add "auth": {"auth_type": "basic", "username": "<upstream-user>", "password": "<upstream-password>"}. For a hosted repository with several roots, add "settings": {"rpm_root_depth": 2}.

Connecting dnf / yum

The repository URL in Save has the form https://save.example.com/cs-save/rpm/<project>/<repository> — this is the baseurl for the client. Create /etc/yum.repos.d/codescoring.repo:

[codescoring-baseos]
name=Rocky 9 BaseOS (CodeScoring Save)
baseurl=https://save.example.com/cs-save/rpm/<project>/rocky9-baseos
enabled=1
username=<username>
password=<password>
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9
dnf makecache --repo=codescoring-baseos
dnf install <package>

To route every package through Save, disable the original distribution repositories (enabled=0 in their .repo files) or replace baseurl / mirrorlist in them with the Save URL. All .repo file options are described in the dnf documentation.

Robot accounts in CI

For CI/CD, you can use a robot account: username = sa$<robot-login>, password = <api-key> in the .repo file. The file structure remains the same; only values change. For details, see Authentication.

Signature Verification

  • Proxy. Save serves upstream packages and metadata unchanged, including repomd.xml.asc. The distribution keys therefore keep working as before: gpgcheck=1 with the distribution gpgkey, and repo_gpgcheck=1 as well if upstream signs its metadata.

  • Hosted. Save never signs or modifies packages. To use gpgcheck=1, sign the packages before upload (rpmsign --addsign) and distribute your public key to the clients.

  • Hosted repository metadata signature. If metadata signing is enabled on the server (METADATA_SIGNING_ENABLED), Save signs repomd.xml with its own key (repodata/repomd.xml.asc). The public key is available at <baseurl>/repository.key. Enable the verification as follows:

    repo_gpgcheck=1
    gpgkey=https://save.example.com/cs-save/rpm/<project>/rpm-hosted/repository.key

    repo_gpgcheck verifies the metadata signature, and gpgcheck verifies the package signatures. These are different keys: if the packages are signed with your key, list both in gpgkey, separated by a space.

Publishing Packages to a Hosted Repository

dnf has no publish command: as in Nexus or Artifactory, a package is uploaded with a plain HTTP PUT request to the path where it will live in the repository. The -T (--upload-file) option of curl sends the file exactly this way:

curl -u "<username>:<password>" \
  -T mypackage-1.0.0-1.el9.x86_64.rpm \
  https://save.example.com/cs-save/rpm/<project>/rpm-hosted/Packages/m/mypackage-1.0.0-1.el9.x86_64.rpm

After the upload, Save rebuilds repodata/ within a few seconds — there is no need to run createrepo_c. On the client, run dnf makecache --refresh to get the new metadata right away.

What Save checks on upload:

  • the file name follows the NVRA format <name>-<version>-<release>.<arch>.rpm and must match the package header. You can get the correct file name with rpm -qp --queryformat '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}.rpm\n' <file>.rpm;
  • a Packages/... path (as in the example above) or a bare file name in the root is normalized by Save to the standard form Packages/<first-letter>/<file>. Any other directory is kept as is — this is needed for multiple indexing roots;
  • uploading the same package again returns 409.

A package can be deleted in the web interface (Delete artifact on the repository page) — the metadata is rebuilt automatically.

If the metadata needs to be rebuilt manually (for example, after restoring from a backup):

curl -u "<username>:<password>" \
  -X POST https://save.example.com/cs-save/rpm/<project>/rpm-hosted/rebuild-index

The rebuild also removes stale repodata/ files that repomd.xml no longer references.

Multiple Indexing Roots

By default, a hosted repository has a single repodata/ in its root. If one repository holds packages for different distributions or versions, it is more convenient to give each of them its own repodata/. To do this, set RPM metadata folder depth — the counterpart of yumRootDepth in Artifactory and Repodata Depth in Nexus.

The depth is the number of leading directories in the package path that form the root. For example, with depth 2:

Package pathRootbaseurl for the client
centos/7/Packages/hello-1.0-1.el7.x86_64.rpmcentos/7https://save.example.com/cs-save/rpm/<project>/rpm-dists/centos/7
centos/8/Packages/hello-1.0-1.el8.x86_64.rpmcentos/8https://save.example.com/cs-save/rpm/<project>/rpm-dists/centos/8

Every root has its own independent repodata/: packages from centos/7 do not get into the centos/8 index.

  • 0 (default) — a single repodata/ for the whole repository.
  • 1–10 — a separate repodata/ for every root of this depth.
  • The setting is available only for hosted repositories: a proxy serves upstream metadata as is.

A package is uploaded into the directory of its root:

curl -u "<username>:<password>" \
  -T hello-1.0-1.el7.x86_64.rpm \
  https://save.example.com/cs-save/rpm/<project>/rpm-dists/centos/7/Packages/hello-1.0-1.el7.x86_64.rpm

If the depth is greater than 0, the package path must contain at least as many directories as the depth and must not start with Packages/. Otherwise Save responds with 400 and a hint on where to upload the package. Artifactory stores such a package without indexing it, while Save rejects the upload on purpose: otherwise the package is "uploaded" but dnf cannot see it. The same package can be uploaded to different roots.

The depth of an existing repository can be changed on the Edit repository form. Save reindexes the repository in the background: it creates repodata/ for the new roots and removes the metadata of the old layout. Until reindexing finishes, the new repomd.xml files may not be available yet.

If metadata signing is enabled, repomd.xml.asc is published in every root, while the key is shared by the whole repository: it is available both at <repo>/repository.key and at <root>/repository.key. rebuild-index rebuilds the metadata of all roots.

Repository URL Migration

Use case: migrating an RPM repository from Nexus / Artifactory to CodeScoring.Save.

Sourcebaseurl before migrationbaseurl after migration
Nexushttps://nexus.host.ru/repository/yum-hostedhttps://save.example.com/cs-save/rpm/<project>/rpm-hosted
Artifactoryhttps://jfrog.host.ru/artifactory/rpm-localhttps://save.example.com/cs-save/rpm/<project>/rpm-hosted
Official mirrorhttps://dl.rockylinux.org/pub/rocky/9/BaseOS/x86_64/oshttps://save.example.com/cs-save/rpm/<project>/rocky9-baseos

Authentication parameters (username / password) in the .repo file remain unchanged. If Nexus or Artifactory used a repodata depth (Repodata Depth / yumRootDepth), set the same value in RPM metadata folder depth and upload the packages to the same paths — the root baseurl values stay the same.

Troubleshooting

What dnf Does

dnf clean metadata --repo=codescoring-baseos
dnf -v makecache --repo=codescoring-baseos

With -v, dnf shows the URLs it requests and the cause of the error. A 401 means the username / password in the .repo file are wrong.

Checking repomd.xml

curl -u "<username>:<password>" \
  https://save.example.com/cs-save/rpm/<project>/rpm-hosted/repodata/repomd.xml

The response is XML with the primary, filelists, and other sections. A 404 for a hosted repository with a depth greater than 0 is expected: the metadata lives in the root directories (<root>/repodata/repomd.xml).

A Package Is Uploaded but dnf Cannot See It

Wait a few seconds (the metadata is built asynchronously), then run dnf makecache --refresh or rebuild-index. To check whether the package is in the index:

# the primary index href is taken from repomd.xml
curl -s -u "<username>:<password>" \
  https://save.example.com/cs-save/rpm/<project>/rpm-hosted/repodata/<hash>-primary.xml.gz \
  | gunzip | grep '<location'

Checking the Public Key

curl -u "<username>:<password>" \
  https://save.example.com/cs-save/rpm/<project>/rpm-hosted/repository.key
# Expected: -----BEGIN PGP PUBLIC KEY BLOCK-----
# A 404 means metadata signing is not enabled on the server

Service Status

curl https://save.example.com/health

Repository Audit

curl -u "<username>:<password>" \
  "https://save.example.com/api/v1/admin/audit?resource_type=repository&q=rpm-hosted&limit=50"
Was this page helpful?