Featured image for: SCTP over UDP in the Linux kernel.

Nowadays, Fedora contributors use Packit a lot to keep packages up to date. In general, using the service is extremely easy, but as we will see, integrating it with a complex multi-source project with an extensive cryptographic test suite like the Go FIPS project needed a little tweaking. But the effort will be worth it.

The Go FIPS CI before Packit

In the Go FIPS repository, we use GitHub Actions to test the pull requests before merging them. We run the whole test suite with each pull request's changes in several containers to ensure it works on Red Hat Enterprise Linux (RHEL) 7, 8, and 9. The differences between these three versions of RHEL give us fantastic coverage, but there is a limitation. We can only build on AMD64 if we rely on GitHub Actions without external systems.

Accessing a CI platform that runs on x86_64, aarch64, ppc64le, and s390x is difficult. As we test all those architectures later when we create the package for CentOS Stream and RHEL, we put the idea of finding the perfect CI system on hold. There is no offering like that, so we must create it ourselves, which takes time.

We did something similar for the conversational bot, as we wouldn't rely on an external platform. Our needs were humble, and we didn't want to maintain a server, so I made a straightforward Python bot within the YAML file. It is limited in what you can do, but it works perfectly for our needs. You can check it here if you are curious. With a few more additions to the GitHub Actions, that was everything we were running on the pull request.

All potential architecture-related problems will be caught later in the pipeline when we release a new update for CentOS Stream, so we were not worried about this. However, delaying these tests has two additional issues:

  • First, it is unfair for non-CentOS Stream users because they potentially downloaded a version of our fork that was less tested on non-x86_64 architectures.
  • Second, if we find a problem down the road, going up from CentOS Stream or even RHEL to the GitHub issue is a slow process.

So, it was time to accept the defeat and find an external service to help us here.

Enters Packit and Packit-as-a-Service. The main goal of Packit is to ease the integration of projects with Fedora and other Linux distributions. It can be used in several ways, but there is one that can make our lives easier: It can build packages using GitHub pull requests on COPR repositories, and that is what we are going to do in this article. And Packit-as-a-Service is the already-made and ready-to-be-used bot.

COPR is an automatic build system with a package repository as its output. It supports all the architectures supported by Fedora or other RPM-based Linux distributions (like AmazonLinux, OpenSuse, CentOS Stream, or RHEL). And for Go FIPS, this is great.

Our new goal was clear: to make Packit run every new pull request on a Fedora COPR build.

When you run any stable Fedora release, the Go package contains what comes from upstream with minimal changes. There is no FIPS (Federal Information Processing Standards) patches or RHEL reference whatsoever. But a few months ago, I enabled the ELN branch to run Go FIPS there. The main goal was to reduce the gap between CentOS Stream and Fedora ELN.

Fedora ELN is a special buildroot of Fedora that uses Fedora Rawhide and emulates a Red Hat Enterprise Linux compose. If a package doesn't have an ELN branch in the repository, Fedora ELN will use the rawhide branch to build from it.

But why Fedora ELN instead of RHEL or CentOS Stream?

  • One of the RHEL architectures is missing in COPR at the moment. We would need another buildroot anyway.
  • CentOS Stream is an excellent option, and adding a new buildroot to Packit is simple, but the packages in Fedora ELN and CentOS Stream are slightly different, and that would make the whole project more complex. In any case, this is a change that we'll enable after a while.

Using Fedora ELN covers everything we want in an easy and open way. And this change gives us an additional feature. It allows us to have something bleeding edge but behaves like RHEL, where we can try our Go changes. We can find more issues if we test in a highly dynamic environment.

Integrating a project with Packit is simple; the documentation is excellent (and the Packit team helped me a lot). But integrating Go FIPS is another story. Let's see why.

First attempt

I followed the Packit Onboarding Guide and created the following .packit.yaml file in the repository:

specfile_path: .packit_rpm/golang.spec

files_to_sync:
  - .packit.yaml
  - src: .packit_rpm/golang.spec
    dest: golang.spec

upstream_package_name: golang
downstream_package_name: golang

actions:
  post-upstream-clone:
    - "git clone https://src.fedoraproject.org/rpms/golang.git .packit_rpm --branch eln"

jobs:
  - job: copr_build
    trigger: pull_request
    targets:
    - fedora-eln-aarch64
    - fedora-eln-ppc64le
    - fedora-eln-s390x
    - fedora-eln-x86_64

It will try to build whatever is in the pull request using COPR. As we don't ship the spec file in the GitHub repository and want to use the one in ELN, we also need to clone the repository somewhere during the initial build steps.

After doing this, I found that Packit failed to identify the pull request contents. It wasn't enough to use the spec file from ELN.

Go FIPS is a set of patches applied to the Go upstream project. It is a multi-source project where Go and the FIPS patches must be aligned. That creates three issues:

  • The pull request content is not a change in Go; it is a change in Go FIPS. We need to notify Packit of this somehow.
  • The version in ELN can be different from the one in the pull request. For example, ELN can be in Go 1.21, and the pull request can be an update to Go 1.22. We cannot trust the content of the spec file.
  • The Fedora ELN spec file can come with patches. And we cannot guarantee that those patches will work on top of the pull request changes. Again, we cannot trust the content of the spec file.

The second attempt

Let's start with the YAML file.

specfile_path: .packit_rpm/golang.spec

files_to_sync:
  - .packit.yaml
  - ./scripts/packit.sh
  - src: .packit_rpm/golang.spec
    dest: golang.spec

srpm_build_deps:
  - golang
  - net-tools
  - openssl-devel
  - glibc-static
  - perl-interpreter
  - procps-ng

upstream_package_name: golang
downstream_package_name: golang

actions:
  create-archive:
    - "bash ./scripts/packit.sh create-archive"
  post-upstream-clone:
    - "git clone https://src.fedoraproject.org/rpms/golang.git .packit_rpm --branch eln"
  fix-spec-file:
    - "bash ./scripts/packit.sh"

jobs:
  - job: copr_build
    trigger: pull_request
    targets:
    - fedora-eln-aarch64
    - fedora-eln-ppc64le
    - fedora-eln-s390x
    - fedora-eln-x86_64

In essence, the file remains the same. The only significant change is the addition of the scripts/packit.sh file. I was reluctant to incorporate an additional script, but it was inevitable. YAML syntax limitations made the whole idea of putting bash commands impossible. You can run simple ones, but when you try to add several quotes or pass variables, it crumbles.

Note: There are better ideas than having everything in one script, but this is a snapshot of what I did at the moment of the writing. It will be different and better if you check the repository in a few months.

So the steps performed are:

  1. Create the archive.
  2. Fetch the ELN spec file.
  3. Fix that spec file.
  4. Build.

Creating the archive means dealing with the first issue: the pull request content is Go FIPS, no Go upstream. Doing this is as easy as listing the file name using a simple ls -1t. But we need to create the archive first.

The version that Go FIPS targets is stored in the config/versions.json file, so if we get that content, we know how to name the tarball. There's an additional problem here: the package release is not in that JSON file. The package release is a number after the project's version that indicates the package's version itself. It is used, for example, when someone wants to modify the spec file with a patch but doesn't need to update the project version it packages. It is unusual to have a lot of package releases within the same Go release. So that is always set as 99. While possible, reaching 99 in a typical scenario would mean I needed to modify and release Go in Fedora ELN 99 times within the same Go release. Highly uncommon.

Here is an example to clarify the versioning: Our tarball for Go 1.21.4 would be go1.21.4-99-openssl-fips.tar.gz. As these builds are not shipped outside COPR, we can reuse the 99 repeatedly.

Fixing this issue also helps us with the second one: we can use the version we detected to modify the spec file later.

We also need to remove the patches from the spec file.

With these two things clear, let's see the content of scripts/packit.sh:

#!/usr/bin/env bash

# Detect the Go version targeted in the PR
version=$(awk '/github.com\/golang\/go/ {gsub(/[: "go]/, "", $2); print $2}' config/versions.json)
# Split the version using '.' as the delimiter
IFS='.' read -ra parts <<< "$version"
# Extract the first two parts and store in go_api
go_api="${parts[0]}.${parts[1]}"
# Extract the third part and store in go_patch
go_patch="${parts[2]}"
# Create a high package release number. This is a dirty hack.
pkg_release="99"

package="go$version-$pkg_release-openssl-fips"

if [ "$1" = "create-archive" ]; then
  git archive --verbose --output $package.tar.gz --prefix go-$package/ HEAD
  ls -1t ./go*-openssl-fips.tar.gz | head -n 1
else
  # Drop fedora.go file
  rm -fv .packit_rpm/fedora.go
  sed -i '/SOURCE2/d' .packit_rpm/golang.spec
  sed -i '/fedora.go/d' .packit_rpm/golang.spec
  # Drop all the patches, we don't know if they can be apply to the new code
  rm -fv .packit_rpm/*.patch
  sed -ri '/[0-9]*:.+$/d' .packit_rpm/golang.spec

  # Update the Go version in golang.spec with the value of $go_api and $go_patch
  sed -i "s/%global go_api .*/%global go_api $go_api/" .packit_rpm/golang.spec
  sed -i "s/%global go_patch .*/%global go_patch $go_patch/" .packit_rpm/golang.spec
  sed -i "s/%global pkg_release .*/%global pkg_release $pkg_release/" .packit_rpm/golang.spec
fi

With these two files, we can have a beautiful bot that runs automatically or under demand by commenting with a /packit copr-build.

Conclusion

With these changes, we will cover those less common architectures earlier in the release process, making it faster and streamlined. And we will also make the people who use Go FIPS directly happier.