Build verified upstream RPMs #109
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build verified upstream RPMs | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| full: | |
| description: Rebuild every package, ignoring what is already published | |
| required: false | |
| default: false | |
| type: boolean | |
| push: | |
| branches: [main] | |
| paths: | |
| - config/** | |
| - packages/** | |
| - tools/source_pipeline.py | |
| - tools/runtime_contract.py | |
| - tests/** | |
| - .github/workflows/rebuild-rpms.yml | |
| pull_request: | |
| paths: | |
| - config/** | |
| - packages/** | |
| - tools/source_pipeline.py | |
| - tools/runtime_contract.py | |
| - tests/** | |
| - .github/workflows/rebuild-rpms.yml | |
| schedule: | |
| - cron: '41 6 * * *' | |
| permissions: | |
| contents: read | |
| id-token: write | |
| packages: write | |
| pages: write | |
| # Build waves may run concurrently. They use run-scoped artifacts and each | |
| # publication re-seeds from the then-current repository, so concurrent work | |
| # cannot lose RPMs. The consumer tag itself remains serialized at `publish`. | |
| jobs: | |
| prepare: | |
| runs-on: ubuntu-24.04 | |
| outputs: | |
| stage0: ${{ steps.matrix.outputs.stage0 }} | |
| stage1: ${{ steps.matrix.outputs.stage1 }} | |
| stage2: ${{ steps.matrix.outputs.stage2 }} | |
| stage3: ${{ steps.matrix.outputs.stage3 }} | |
| stage4: ${{ steps.matrix.outputs.stage4 }} | |
| build_list: ${{ steps.matrix.outputs.build_list }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Validate factory policy and tests | |
| run: | | |
| python3 tools/check_workflow_quoting.py | |
| python3 tools/runtime_contract.py \ | |
| config/bluefin-packages.toml config/runtime-contract.toml --check | |
| python3 -m unittest discover -s tests -v | |
| - name: Install zstandard | |
| run: pip install --quiet zstandard | |
| - id: matrix | |
| env: | |
| FULL: ${{ github.event_name == 'workflow_dispatch' && inputs.full == true && '1' || '0' }} | |
| BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }} | |
| run: | | |
| python3 - <<'PY' | |
| import json, os, re, subprocess, urllib.request, gzip, io, sys | |
| from pathlib import Path | |
| config = json.loads(Path('config/upstream-sources.json').read_text()) | |
| full = os.environ.get('FULL') == '1' | |
| changed = set() | |
| base = os.environ.get('BASE_SHA', '') | |
| if not full and re.fullmatch(r'[0-9a-f]{40}', base or '') and set(base) != {'0'}: | |
| paths = subprocess.check_output( | |
| ['git', 'diff', '--name-only', f'{base}..HEAD'], text=True).splitlines() | |
| changed = { | |
| match.group(1) | |
| for path in paths | |
| if (match := re.match(r'^packages/([^/]+)/', path)) | |
| } | |
| print(f"changed package recipes: {', '.join(sorted(changed)) or 'none'}") | |
| # Fetch the currently published repo so we only rebuild what is new | |
| # or changed. Matching is name + normalized version: Fedora's spec | |
| # Version rewrites the tarball's '.' to '~' (gnome-shell 51.beta -> | |
| # 51~beta), so normalize '~' to '.' on both sides. A mismatch just | |
| # rebuilds a package we could have skipped -- safe, only slower. | |
| published = {} | |
| if not full: | |
| try: | |
| base = "https://projectbluefin.github.io/utah-packages/" | |
| repomd = urllib.request.urlopen(base + "repodata/repomd.xml", timeout=60).read().decode() | |
| href = re.search(r'<location href="([^"]*primary[^"]*)"', repomd).group(1) | |
| raw = urllib.request.urlopen(base + href, timeout=120).read() | |
| if href.endswith('.zst'): | |
| import zstandard | |
| stream = zstandard.ZstdDecompressor().stream_reader(io.BytesIO(raw)) | |
| else: | |
| stream = gzip.GzipFile(fileobj=io.BytesIO(raw)) | |
| text = stream.read() | |
| for m in re.finditer(rb'<package[^>]*>.*?<name>([^<]+)</name>.*?<version[^>]*ver="([^"]+)"', text, re.S): | |
| published[m.group(1).decode()] = m.group(2).decode() | |
| print(f"published repo has {len(published)} packages") | |
| except Exception as error: | |
| print(f"WARNING: could not read published repo, rebuilding all: {error}", file=sys.stderr) | |
| def norm(version: str) -> str: | |
| return version.replace('~', '.') | |
| build = [] | |
| for package in config['packages']: | |
| name = package['name'] | |
| version = package.get('version', '') | |
| if full or name in changed or name not in published or norm(published[name]) != norm(version): | |
| build.append(package) | |
| else: | |
| print(f"skip {name} {version}: already published") | |
| outputs = {'build_list': json.dumps([p['name'] for p in build])} | |
| for n in range(5): | |
| outputs[f'stage{n}'] = json.dumps( | |
| [p['name'] for p in build if (p.get('stage') or 0) == n]) | |
| with open(os.environ['GITHUB_OUTPUT'], 'a') as handle: | |
| for key, value in outputs.items(): | |
| handle.write(f'{key}={value}\n') | |
| print(f"will build {len(build)} of {len(config['packages'])} packages") | |
| PY | |
| preflight: | |
| # Every missing platform dependency has so far been found the slow way: a | |
| # stage fails, we read one "No match for argument" line, add one package, | |
| # and wait another half hour for the next one. gtk4 needed pango >= 1.58.0 | |
| # that Fedora 44 does not carry. This job resolves every recipe's | |
| # BuildRequires in the real build root up front, so one run enumerates all | |
| # of the gaps instead of one per round. | |
| # | |
| # It never gates a build. A requirement listed here may be perfectly fine: | |
| # a later-stage package legitimately BuildRequires something an earlier | |
| # stage has not built yet, and that is indistinguishable from a real gap | |
| # without resolving against the staged output. Read it as a worklist. | |
| needs: prepare | |
| if: needs.prepare.outputs.build_list != '[]' | |
| runs-on: ubuntu-24.04 | |
| continue-on-error: true | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - name: Resolve BuildRequires for packages being built | |
| env: | |
| FACTORY_REPO: ${{ vars.FACTORY_REPO }} | |
| BUILD_LIST: ${{ needs.prepare.outputs.build_list }} | |
| run: | | |
| mkdir -p work/reports | |
| docker run --rm \ | |
| -e FACTORY_REPO -e BUILD_LIST \ | |
| -v "$PWD/work:/work:Z" \ | |
| -v "$PWD/packages:/packages:ro,Z" \ | |
| -v "$PWD/config:/repos:ro,Z" \ | |
| quay.io/fedora/fedora:44 bash -exc ' | |
| disable=--disablerepo=fedora-cisco-openh264 | |
| # The Fedora container images set tsflags=nodocs, so every %doc file is | |
| # dropped at install time. rand_core ships its crate docs that way and | |
| # its lib.rs does #![doc = include_str!("../README.md")], so rust-just | |
| # failed to compile: rustc could not read ../README.md. Nothing was wrong | |
| # with the Fedora package: mock installs docs into a build root, and | |
| # this container was not. Restore that. | |
| sed -i "/^tsflags=nodocs/d" /etc/dnf/dnf.conf | |
| cp /repos/hummingbird.repo /etc/yum.repos.d/ | |
| if [ -n "${FACTORY_REPO:-}" ]; then | |
| printf "[factory]\nname=factory\nbaseurl=%s\nenabled=1\ngpgcheck=0\npriority=5\n" \ | |
| "$FACTORY_REPO" > /etc/yum.repos.d/factory.repo | |
| fi | |
| dnf -y $disable install dnf-plugins-core rpm-build @buildsys-build | |
| rpm -q --qf "buildroot openssl: %{VERSION}-%{RELEASE}\n" openssl-libs || true | |
| : > /work/reports/preflight.txt | |
| # Only resolve BuildRequires for packages this run will actually | |
| # build (the incremental matrix), not the whole tree. | |
| # BUILD_LIST is a JSON array; strip brackets, quotes and commas | |
| # without ever writing a literal quote character. | |
| for package in $(echo "$BUILD_LIST" | sed -e s/\\[// -e s/\\]// -e s/\\\"//g -e s/,/ /g); do | |
| dir="/packages/$package" | |
| test -d "$dir" || { echo "build_list names missing dir: $dir" >> /work/reports/preflight.txt; continue; } | |
| spec=$(find "$dir" -maxdepth 1 -name "*.spec" -print -quit) | |
| test -n "$spec" || continue | |
| # --assumeno resolves the transaction without installing it, | |
| # but it also *declines* the transaction, so dnf exits non-zero | |
| # even when resolution succeeded. Keying off the exit status | |
| # listed all 70 recipes as failures and buried the real ones. | |
| # Judge by the error text instead. | |
| output=$(dnf $disable --assumeno builddep -D "_sourcedir $dir" "$spec" 2>&1 || true) | |
| # Two classes, and they are not equally trustworthy. | |
| # | |
| # "No match for argument" means a capability nothing in the root | |
| # provides. That has been reliable: it is how the real pango gap | |
| # was found. | |
| # | |
| # "cannot install both" is a version conflict between the layered | |
| # repositories and what @System already carries, and it has been | |
| # not a reliable predictor: packages have built successfully after | |
| # reporting one here. The resolution | |
| # this job performs is not the one the build performs, so conflicts | |
| # are recorded separately and must not be treated as gaps. | |
| missing=$(printf "%s\n" "$output" \ | |
| | grep -E "No match for argument|nothing provides" || true) | |
| conflict=$(printf "%s\n" "$output" | grep -E "cannot install" || true) | |
| if [ -n "$missing" ] || [ -n "$conflict" ]; then | |
| printf "%s\n" "$package" >> /work/reports/preflight.txt | |
| if [ -n "$missing" ]; then | |
| printf "%s\n" "$missing" | sed "s/^/ MISSING /" >> /work/reports/preflight.txt | |
| fi | |
| if [ -n "$conflict" ]; then | |
| printf "%s\n" "$conflict" | sed "s/^/ conflict(unreliable) /" >> /work/reports/preflight.txt | |
| fi | |
| fi | |
| done | |
| echo "=== recipes with unresolved BuildRequires ===" | |
| cat /work/reports/preflight.txt | |
| ' | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: preflight-buildrequires | |
| path: work/reports/preflight.txt | |
| if-no-files-found: warn | |
| rebuild0: | |
| needs: prepare | |
| if: needs.prepare.outputs.stage0 != '[]' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| package: ${{ fromJSON(needs.prepare.outputs.stage0) }} | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: ./.github/actions/setup-sccache | |
| - name: Add best-effort swap for Firefox PGO/LTO | |
| if: matrix.package == 'firefox' | |
| run: | | |
| set -euo pipefail | |
| available_gib=$(df --output=avail -BG "$RUNNER_TEMP" | tail -n1 | tr -dc 0-9) | |
| if [ "$available_gib" -lt 20 ]; then | |
| echo "Only ${available_gib} GiB free; skipping extra swap to preserve build disk" >&2 | |
| exit 0 | |
| fi | |
| swapfile="$RUNNER_TEMP/firefox-pgo.swap" | |
| sudo fallocate -l 12G "$swapfile" | |
| sudo chmod 600 "$swapfile" | |
| sudo mkswap "$swapfile" | |
| sudo swapon "$swapfile" || { echo "Host does not permit extra swap" >&2; exit 0; } | |
| free -h | |
| - name: Fetch and verify direct upstream source | |
| env: | |
| PACKAGE: ${{ matrix.package }} | |
| run: | | |
| python3 tools/source_pipeline.py "$PACKAGE" --output work/sources --report-dir work/reports | |
| - name: Build the verified source with its RPM recipe | |
| env: | |
| PACKAGE: ${{ matrix.package }} | |
| FACTORY_REPO: ${{ vars.FACTORY_REPO }} | |
| run: | | |
| test -d "packages/$PACKAGE" | |
| mkdir -p work/result | |
| # Per-package, so it is resolved out here rather than inside the | |
| # container, which only sees config/ and packages/. | |
| DIST_BUMP="$(python3 tools/dist_bump.py "$PACKAGE")" | |
| export DIST_BUMP | |
| docker run --rm --privileged \ | |
| -e PACKAGE -e FACTORY_REPO -e DIST_BUMP \ | |
| -v "$PWD/work:/work:Z" \ | |
| -v "$PWD/packages:/packages:ro,Z" \ | |
| -v "$PWD/config:/repos:ro,Z" \ | |
| quay.io/fedora/fedora:44 bash -exc ' | |
| # The fedora:rawhide image ships fedora-cisco-openh264 enabled, but | |
| # its packages are signed with Cisco key, which the image does not | |
| # trust -- so any builddep graph reaching gstreamer/pipewire dies on | |
| # "Import of the key did not help, wrong key?". openh264 is a runtime | |
| # codec, never a build requirement, and Fedora own noopenh264 provides | |
| # the same libopenh264.so.8 soname, so disabling the repo resolves. | |
| disable=--disablerepo=fedora-cisco-openh264 | |
| # The Fedora container images set tsflags=nodocs, so every %doc file is | |
| # dropped at install time. rand_core ships its crate docs that way and | |
| # its lib.rs does #![doc = include_str!("../README.md")], so rust-just | |
| # failed to compile: rustc could not read ../README.md. Nothing was wrong | |
| # with the Fedora package: mock installs docs into a build root, and | |
| # this container was not. Restore that. | |
| sed -i "/^tsflags=nodocs/d" /etc/dnf/dnf.conf | |
| # Fedora 44 plus Hummingbird, mirroring Hummingbird mock.cfg: | |
| # Fedora release repos with its own Pulp repos shadowing them by | |
| # priority. Proven correct by this job own diagnostic below -- | |
| # openssl 3.5.7 means libcrypto.so.3, the ABI Hummingbird has. | |
| # A Rawhide root produced RPMs needing libcrypto.so.4 instead. | |
| cp /repos/hummingbird.repo /etc/yum.repos.d/ | |
| # RPMs from earlier stages become a local repo, so a later stage | |
| # can satisfy a BuildRequires on something this run just built. | |
| # The guard must recurse: upload-artifact takes the common parent | |
| # of its path globs as the artifact root, so an artifact declaring | |
| # work/result/*.rpm and work/reports/*.json unpacks as | |
| # prior/result/*.rpm, not prior/*.rpm. A non-recursive glob matched | |
| # nothing, this block was silently skipped in every stage, and | |
| # mutter resolved gsettings-desktop-schemas to Fedora 50.1 instead | |
| # of the 51.beta stage 0 had just built. createrepo_c itself walks | |
| # the tree, so only the test needed fixing. | |
| if [ -n "$(find /work/prior -name "*.rpm" -print -quit 2>/dev/null)" ]; then | |
| dnf -y $disable install createrepo_c | |
| createrepo_c /work/prior | |
| printf "[stages]\nname=stages\nbaseurl=file:///work/prior\nenabled=1\ngpgcheck=0\npriority=1\n" \ | |
| > /etc/yum.repos.d/stages.repo | |
| # priority alone does not keep Fedora out. gnome-control-center | |
| # pulled Fedora accountsservice 23.13.9 even though stage 0 had | |
| # built 26.27.3 and [stages] was priority 1: the Fedora main | |
| # package entered the transaction and pinned accountsservice-libs | |
| # to its exact NEVR, so our libs could not be installed and our | |
| # devel, which needs them, was dropped -- | |
| # cannot install both accountsservice-libs-26.27.3 from stages | |
| # and accountsservice-libs-23.13.9-16.fc44 from fedora | |
| # Excluding by name is what settles it: whatever an earlier stage | |
| # built, Fedora must not answer for. Names come from rpm rather | |
| # than from parsing filenames, which stops working the moment a | |
| # disttag changes. | |
| EXCLUDE=$(find /work/prior -name "*.rpm" -type f -print0 \ | |
| | xargs -0 -r rpm -qp --qf "%{NAME}\n" 2>/dev/null \ | |
| | sort -u | paste -sd, -) | |
| echo "excluding from Fedora: $EXCLUDE" | |
| fi | |
| # Hummingbird ships newer versions of some names than Fedora 44 | |
| # does, and the two must never mix in one transaction. The | |
| # conflicts this prevents are real: libicu 78.3 (hum) vs 77.1 | |
| # (fc44) broke samba and evolution-data-server, and Fedora ruby | |
| # 3.3/3.4-default-gems vs Hummingbird ruby4.0-default-gems broke | |
| # webkitgtk, colord, libnotify and zsh. Always prefer the | |
| # Hummingbird copy by excluding these names from Fedora. | |
| # Unconditional: stage 0 has no prior RPMs, so the block above | |
| # never ran and EXCLUDE would otherwise be empty here. | |
| HB_EXCLUDE="ruby-default-gems,ruby3.3-default-gems,ruby3.4-default-gems,libicu,icu,gpgme,qt6-qtbase" | |
| EXCLUDE="${HB_EXCLUDE}${EXCLUDE:+,}${EXCLUDE}" | |
| echo "hummingbird exclusions: $HB_EXCLUDE" | |
| if [ -n "${FACTORY_REPO:-}" ]; then | |
| printf "[factory]\nname=factory\nbaseurl=%s\nenabled=1\ngpgcheck=0\npriority=5\n" \ | |
| "$FACTORY_REPO" > /etc/yum.repos.d/factory.repo | |
| fi | |
| # The plain fedora image is not a build root: it lacks the group mock | |
| # installs, so /usr/bin/echo and friends are missing. Most packages pull | |
| # them in transitively; squashfs-tools calls echo directly from its | |
| # manpage installer and fails without it. | |
| dnf -y $disable install dnf-plugins-core mock rpm-build @buildsys-build | |
| rpm -q --qf "buildroot openssl: %{VERSION}-%{RELEASE}\n" openssl-libs || true | |
| # NO APOSTROPHES IN THIS SCRIPT. It is the body of bash -exc | |
| # a single-quoted string, so one closes it and everything after | |
| # is reparsed. That is what the stilted "Fedora own | |
| # noopenh264" and "this job own diagnostic" above are avoiding. | |
| # Three apostrophes in this very comment took out all 36 stage 0 | |
| # jobs at once, in under two minutes, with no clue in the log. | |
| # | |
| # The AlmaLinux convention, one distro over. They keep the vendor | |
| # release and dist and append to it -- their dnf is | |
| # 4.14.0-34.el9_8.alma.1 against 34.el9_8 from Red Hat, and both | |
| # .alma and .alma.N appear in their repositories. | |
| # | |
| # The vendor here is Hummingbird, not Fedora. These packages are | |
| # built for Hummingbird and installed on Hummingbird; Fedora 44 is | |
| # only the other half of the buildroot, the way a compiler is. An | |
| # earlier version of this tagged them .fc44.bfin, which named the | |
| # distribution they are not for. | |
| # | |
| # The tag is read from the Hummingbird packages present in the | |
| # buildroot rather than hardcoded, so a move to hum2 carries | |
| # itself. A buildroot containing none of them is a repository | |
| # misconfiguration -- the exact failure this factory exists to | |
| # avoid -- so it stops rather than quietly tagging something else. | |
| HUM_TAG="$(rpm -qa --qf "%{RELEASE}\n" | grep -oE "hum[0-9]+$" | sort -u | head -n1)" | |
| if [ -z "$HUM_TAG" ]; then | |
| echo "No Hummingbird package in the buildroot; cannot derive a disttag" >&2 | |
| rpm -qa --qf "%{NAME} %{RELEASE}\n" | sort | head -20 >&2 | |
| exit 1 | |
| fi | |
| DISTTAG=".${HUM_TAG}.bfin${DIST_BUMP:-}" | |
| echo "disttag: $DISTTAG" | |
| # libratbag %check starts ratbagd, which calls | |
| # Gio.bus_get_sync(Gio.BusType.SYSTEM) and dies with "Could not | |
| # connect: No such file or directory" -- a plain container has no | |
| # system bus socket. Seven of its ten suites already pass and the | |
| # failing one is a real test, so give the build root a bus rather | |
| # than disabling the test. Non-fatal: no other package needs it. | |
| dnf -y $disable install dbus-daemon || true | |
| mkdir -p /run/dbus | |
| dbus-daemon --system --fork || true | |
| # mock defines USER in its build root; a bare container does not. | |
| # just 1.57.0 tests/functions.rs:88 calls env::var("USER").unwrap() | |
| # and panicked with NotPresent -- 1823 tests passed, that one did | |
| # not. Same shape as the missing system bus: supply what a real | |
| # build root has rather than disable the test. | |
| export USER="${USER:-root}" | |
| export LOGNAME="${LOGNAME:-$USER}" | |
| spec=$(find "/packages/$PACKAGE" -maxdepth 1 -name "*.spec" -print -quit) | |
| test -n "$spec" | |
| dnf -y $disable ${EXCLUDE:+--setopt=fedora.excludepkgs="$EXCLUDE"} \ | |
| ${EXCLUDE:+--setopt=updates.excludepkgs="$EXCLUDE"} builddep -D "_sourcedir /packages/$PACKAGE" "$spec" | |
| # An imported spec keeps its dist-git PatchN and auxiliary SourceN | |
| # files next to itself, while the verified upstream archive lands in | |
| # /work/sources. rpmbuild takes a single _sourcedir, so stage both: | |
| # recipe files first, then the verified archive, which therefore wins | |
| # over anything of the same name carried in the import. | |
| staged=/work/staged/$PACKAGE | |
| rm -rf "$staged" | |
| mkdir -p "$staged" | |
| cp -a "/packages/$PACKAGE/." "$staged/" | |
| cp -a "/work/sources/$PACKAGE/." "$staged/" | |
| # Packages with %generate_buildrequires -- every Rust one -- compute | |
| # their real BuildRequires during the build, so the spec alone does not | |
| # list them and rpmbuild exits 11 asking to be re-run. Install what the | |
| # generated source RPM declares and retry, bounded so an unsatisfiable | |
| # requirement fails instead of looping. | |
| for _ in 1 2 3 4 5; do | |
| rm -f /root/rpmbuild/SRPMS/*.buildreqs.nosrc.rpm | |
| if rpmbuild -br "$spec" --define "_sourcedir $staged" \ | |
| --define "dist $DISTTAG"; then break; fi | |
| generated=$(ls /root/rpmbuild/SRPMS/*.buildreqs.nosrc.rpm 2>/dev/null | head -1) | |
| test -n "$generated" | |
| dnf -y $disable ${EXCLUDE:+--setopt=fedora.excludepkgs="$EXCLUDE"} \ | |
| ${EXCLUDE:+--setopt=updates.excludepkgs="$EXCLUDE"} builddep -D "_sourcedir /packages/$PACKAGE" "$generated" | |
| done | |
| rpmbuild -ba "$spec" \ | |
| --define "_sourcedir $staged" \ | |
| --define "dist $DISTTAG" \ | |
| --define "_rpmdir /work/result" | |
| find /work/result -name "*.rpm" -type f -print0 | \ | |
| xargs -0 -r rpm -qp --qf "%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\n" | |
| # A build that produced no RPM must fail here. if-no-files-found on | |
| # the upload cannot catch it: the artifact also carries | |
| # work/reports/*.json, so one file always matches and the upload | |
| # reports success while shipping no packages at all. | |
| test -n "$(find /work/result -name "*.rpm" -type f -print -quit)" | |
| ' | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: rpm-s0-${{ matrix.package }} | |
| path: | | |
| work/result/**/*.rpm | |
| work/reports/*.json | |
| if-no-files-found: error | |
| rebuild1: | |
| needs: [prepare, rebuild0] | |
| if: ${{ !cancelled() && needs.prepare.outputs.stage1 != '[]' }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| package: ${{ fromJSON(needs.prepare.outputs.stage1) }} | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: ./.github/actions/setup-sccache | |
| - name: Collect RPMs built by earlier stages | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| pattern: rpm-* | |
| path: work/prior | |
| merge-multiple: true | |
| continue-on-error: true | |
| - name: Fetch and verify direct upstream source | |
| env: | |
| PACKAGE: ${{ matrix.package }} | |
| run: | | |
| python3 tools/source_pipeline.py "$PACKAGE" --output work/sources --report-dir work/reports | |
| - name: Build the verified source with its RPM recipe | |
| env: | |
| PACKAGE: ${{ matrix.package }} | |
| FACTORY_REPO: ${{ vars.FACTORY_REPO }} | |
| run: | | |
| test -d "packages/$PACKAGE" | |
| mkdir -p work/result | |
| # Per-package, so it is resolved out here rather than inside the | |
| # container, which only sees config/ and packages/. | |
| DIST_BUMP="$(python3 tools/dist_bump.py "$PACKAGE")" | |
| export DIST_BUMP | |
| docker run --rm --privileged \ | |
| -e PACKAGE -e FACTORY_REPO -e DIST_BUMP \ | |
| -v "$PWD/work:/work:Z" \ | |
| -v "$PWD/packages:/packages:ro,Z" \ | |
| -v "$PWD/config:/repos:ro,Z" \ | |
| quay.io/fedora/fedora:44 bash -exc ' | |
| # The fedora:rawhide image ships fedora-cisco-openh264 enabled, but | |
| # its packages are signed with Cisco key, which the image does not | |
| # trust -- so any builddep graph reaching gstreamer/pipewire dies on | |
| # "Import of the key did not help, wrong key?". openh264 is a runtime | |
| # codec, never a build requirement, and Fedora own noopenh264 provides | |
| # the same libopenh264.so.8 soname, so disabling the repo resolves. | |
| disable=--disablerepo=fedora-cisco-openh264 | |
| # The Fedora container images set tsflags=nodocs, so every %doc file is | |
| # dropped at install time. rand_core ships its crate docs that way and | |
| # its lib.rs does #![doc = include_str!("../README.md")], so rust-just | |
| # failed to compile: rustc could not read ../README.md. Nothing was wrong | |
| # with the Fedora package: mock installs docs into a build root, and | |
| # this container was not. Restore that. | |
| sed -i "/^tsflags=nodocs/d" /etc/dnf/dnf.conf | |
| # Fedora 44 plus Hummingbird, mirroring Hummingbird mock.cfg: | |
| # Fedora release repos with its own Pulp repos shadowing them by | |
| # priority. Proven correct by this job own diagnostic below -- | |
| # openssl 3.5.7 means libcrypto.so.3, the ABI Hummingbird has. | |
| # A Rawhide root produced RPMs needing libcrypto.so.4 instead. | |
| cp /repos/hummingbird.repo /etc/yum.repos.d/ | |
| # RPMs from earlier stages become a local repo, so a later stage | |
| # can satisfy a BuildRequires on something this run just built. | |
| # The guard must recurse: upload-artifact takes the common parent | |
| # of its path globs as the artifact root, so an artifact declaring | |
| # work/result/*.rpm and work/reports/*.json unpacks as | |
| # prior/result/*.rpm, not prior/*.rpm. A non-recursive glob matched | |
| # nothing, this block was silently skipped in every stage, and | |
| # mutter resolved gsettings-desktop-schemas to Fedora 50.1 instead | |
| # of the 51.beta stage 0 had just built. createrepo_c itself walks | |
| # the tree, so only the test needed fixing. | |
| if [ -n "$(find /work/prior -name "*.rpm" -print -quit 2>/dev/null)" ]; then | |
| dnf -y $disable install createrepo_c | |
| createrepo_c /work/prior | |
| printf "[stages]\nname=stages\nbaseurl=file:///work/prior\nenabled=1\ngpgcheck=0\npriority=1\n" \ | |
| > /etc/yum.repos.d/stages.repo | |
| # priority alone does not keep Fedora out. gnome-control-center | |
| # pulled Fedora accountsservice 23.13.9 even though stage 0 had | |
| # built 26.27.3 and [stages] was priority 1: the Fedora main | |
| # package entered the transaction and pinned accountsservice-libs | |
| # to its exact NEVR, so our libs could not be installed and our | |
| # devel, which needs them, was dropped -- | |
| # cannot install both accountsservice-libs-26.27.3 from stages | |
| # and accountsservice-libs-23.13.9-16.fc44 from fedora | |
| # Excluding by name is what settles it: whatever an earlier stage | |
| # built, Fedora must not answer for. Names come from rpm rather | |
| # than from parsing filenames, which stops working the moment a | |
| # disttag changes. | |
| EXCLUDE=$(find /work/prior -name "*.rpm" -type f -print0 \ | |
| | xargs -0 -r rpm -qp --qf "%{NAME}\n" 2>/dev/null \ | |
| | sort -u | paste -sd, -) | |
| echo "excluding from Fedora: $EXCLUDE" | |
| fi | |
| # Hummingbird ships newer versions of some names than Fedora 44 | |
| # does, and the two must never mix in one transaction. The | |
| # conflicts this prevents are real: libicu 78.3 (hum) vs 77.1 | |
| # (fc44) broke samba and evolution-data-server, and Fedora ruby | |
| # 3.3/3.4-default-gems vs Hummingbird ruby4.0-default-gems broke | |
| # webkitgtk, colord, libnotify and zsh. Always prefer the | |
| # Hummingbird copy by excluding these names from Fedora. | |
| # Unconditional: stage 0 has no prior RPMs, so the block above | |
| # never ran and EXCLUDE would otherwise be empty here. | |
| HB_EXCLUDE="ruby-default-gems,ruby3.3-default-gems,ruby3.4-default-gems,libicu,icu,gpgme,qt6-qtbase" | |
| EXCLUDE="${HB_EXCLUDE}${EXCLUDE:+,}${EXCLUDE}" | |
| echo "hummingbird exclusions: $HB_EXCLUDE" | |
| if [ -n "${FACTORY_REPO:-}" ]; then | |
| printf "[factory]\nname=factory\nbaseurl=%s\nenabled=1\ngpgcheck=0\npriority=5\n" \ | |
| "$FACTORY_REPO" > /etc/yum.repos.d/factory.repo | |
| fi | |
| # The plain fedora image is not a build root: it lacks the group mock | |
| # installs, so /usr/bin/echo and friends are missing. Most packages pull | |
| # them in transitively; squashfs-tools calls echo directly from its | |
| # manpage installer and fails without it. | |
| dnf -y $disable install dnf-plugins-core mock rpm-build @buildsys-build | |
| rpm -q --qf "buildroot openssl: %{VERSION}-%{RELEASE}\n" openssl-libs || true | |
| # NO APOSTROPHES IN THIS SCRIPT. It is the body of bash -exc | |
| # a single-quoted string, so one closes it and everything after | |
| # is reparsed. That is what the stilted "Fedora own | |
| # noopenh264" and "this job own diagnostic" above are avoiding. | |
| # Three apostrophes in this very comment took out all 36 stage 0 | |
| # jobs at once, in under two minutes, with no clue in the log. | |
| # | |
| # The AlmaLinux convention, one distro over. They keep the vendor | |
| # release and dist and append to it -- their dnf is | |
| # 4.14.0-34.el9_8.alma.1 against 34.el9_8 from Red Hat, and both | |
| # .alma and .alma.N appear in their repositories. | |
| # | |
| # The vendor here is Hummingbird, not Fedora. These packages are | |
| # built for Hummingbird and installed on Hummingbird; Fedora 44 is | |
| # only the other half of the buildroot, the way a compiler is. An | |
| # earlier version of this tagged them .fc44.bfin, which named the | |
| # distribution they are not for. | |
| # | |
| # The tag is read from the Hummingbird packages present in the | |
| # buildroot rather than hardcoded, so a move to hum2 carries | |
| # itself. A buildroot containing none of them is a repository | |
| # misconfiguration -- the exact failure this factory exists to | |
| # avoid -- so it stops rather than quietly tagging something else. | |
| HUM_TAG="$(rpm -qa --qf "%{RELEASE}\n" | grep -oE "hum[0-9]+$" | sort -u | head -n1)" | |
| if [ -z "$HUM_TAG" ]; then | |
| echo "No Hummingbird package in the buildroot; cannot derive a disttag" >&2 | |
| rpm -qa --qf "%{NAME} %{RELEASE}\n" | sort | head -20 >&2 | |
| exit 1 | |
| fi | |
| DISTTAG=".${HUM_TAG}.bfin${DIST_BUMP:-}" | |
| echo "disttag: $DISTTAG" | |
| # libratbag %check starts ratbagd, which calls | |
| # Gio.bus_get_sync(Gio.BusType.SYSTEM) and dies with "Could not | |
| # connect: No such file or directory" -- a plain container has no | |
| # system bus socket. Seven of its ten suites already pass and the | |
| # failing one is a real test, so give the build root a bus rather | |
| # than disabling the test. Non-fatal: no other package needs it. | |
| dnf -y $disable install dbus-daemon || true | |
| mkdir -p /run/dbus | |
| dbus-daemon --system --fork || true | |
| # mock defines USER in its build root; a bare container does not. | |
| # just 1.57.0 tests/functions.rs:88 calls env::var("USER").unwrap() | |
| # and panicked with NotPresent -- 1823 tests passed, that one did | |
| # not. Same shape as the missing system bus: supply what a real | |
| # build root has rather than disable the test. | |
| export USER="${USER:-root}" | |
| export LOGNAME="${LOGNAME:-$USER}" | |
| spec=$(find "/packages/$PACKAGE" -maxdepth 1 -name "*.spec" -print -quit) | |
| test -n "$spec" | |
| dnf -y $disable ${EXCLUDE:+--setopt=fedora.excludepkgs="$EXCLUDE"} \ | |
| ${EXCLUDE:+--setopt=updates.excludepkgs="$EXCLUDE"} builddep -D "_sourcedir /packages/$PACKAGE" "$spec" | |
| # An imported spec keeps its dist-git PatchN and auxiliary SourceN | |
| # files next to itself, while the verified upstream archive lands in | |
| # /work/sources. rpmbuild takes a single _sourcedir, so stage both: | |
| # recipe files first, then the verified archive, which therefore wins | |
| # over anything of the same name carried in the import. | |
| staged=/work/staged/$PACKAGE | |
| rm -rf "$staged" | |
| mkdir -p "$staged" | |
| cp -a "/packages/$PACKAGE/." "$staged/" | |
| cp -a "/work/sources/$PACKAGE/." "$staged/" | |
| # Packages with %generate_buildrequires -- every Rust one -- compute | |
| # their real BuildRequires during the build, so the spec alone does not | |
| # list them and rpmbuild exits 11 asking to be re-run. Install what the | |
| # generated source RPM declares and retry, bounded so an unsatisfiable | |
| # requirement fails instead of looping. | |
| for _ in 1 2 3 4 5; do | |
| rm -f /root/rpmbuild/SRPMS/*.buildreqs.nosrc.rpm | |
| if rpmbuild -br "$spec" --define "_sourcedir $staged" \ | |
| --define "dist $DISTTAG"; then break; fi | |
| generated=$(ls /root/rpmbuild/SRPMS/*.buildreqs.nosrc.rpm 2>/dev/null | head -1) | |
| test -n "$generated" | |
| dnf -y $disable ${EXCLUDE:+--setopt=fedora.excludepkgs="$EXCLUDE"} \ | |
| ${EXCLUDE:+--setopt=updates.excludepkgs="$EXCLUDE"} builddep -D "_sourcedir /packages/$PACKAGE" "$generated" | |
| done | |
| rpmbuild -ba "$spec" \ | |
| --define "_sourcedir $staged" \ | |
| --define "dist $DISTTAG" \ | |
| --define "_rpmdir /work/result" | |
| find /work/result -name "*.rpm" -type f -print0 | \ | |
| xargs -0 -r rpm -qp --qf "%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\n" | |
| # A build that produced no RPM must fail here. if-no-files-found on | |
| # the upload cannot catch it: the artifact also carries | |
| # work/reports/*.json, so one file always matches and the upload | |
| # reports success while shipping no packages at all. | |
| test -n "$(find /work/result -name "*.rpm" -type f -print -quit)" | |
| ' | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: rpm-s1-${{ matrix.package }} | |
| path: | | |
| work/result/**/*.rpm | |
| work/reports/*.json | |
| if-no-files-found: error | |
| rebuild2: | |
| needs: [prepare, rebuild0, rebuild1] | |
| if: ${{ !cancelled() && needs.prepare.outputs.stage2 != '[]' }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| package: ${{ fromJSON(needs.prepare.outputs.stage2) }} | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: ./.github/actions/setup-sccache | |
| - name: Collect RPMs built by earlier stages | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| pattern: rpm-* | |
| path: work/prior | |
| merge-multiple: true | |
| continue-on-error: true | |
| - name: Fetch and verify direct upstream source | |
| env: | |
| PACKAGE: ${{ matrix.package }} | |
| run: | | |
| python3 tools/source_pipeline.py "$PACKAGE" --output work/sources --report-dir work/reports | |
| - name: Build the verified source with its RPM recipe | |
| env: | |
| PACKAGE: ${{ matrix.package }} | |
| FACTORY_REPO: ${{ vars.FACTORY_REPO }} | |
| run: | | |
| test -d "packages/$PACKAGE" | |
| mkdir -p work/result | |
| # Per-package, so it is resolved out here rather than inside the | |
| # container, which only sees config/ and packages/. | |
| DIST_BUMP="$(python3 tools/dist_bump.py "$PACKAGE")" | |
| export DIST_BUMP | |
| docker run --rm --privileged \ | |
| -e PACKAGE -e FACTORY_REPO -e DIST_BUMP \ | |
| -v "$PWD/work:/work:Z" \ | |
| -v "$PWD/packages:/packages:ro,Z" \ | |
| -v "$PWD/config:/repos:ro,Z" \ | |
| quay.io/fedora/fedora:44 bash -exc ' | |
| # The fedora:rawhide image ships fedora-cisco-openh264 enabled, but | |
| # its packages are signed with Cisco key, which the image does not | |
| # trust -- so any builddep graph reaching gstreamer/pipewire dies on | |
| # "Import of the key did not help, wrong key?". openh264 is a runtime | |
| # codec, never a build requirement, and Fedora own noopenh264 provides | |
| # the same libopenh264.so.8 soname, so disabling the repo resolves. | |
| disable=--disablerepo=fedora-cisco-openh264 | |
| # The Fedora container images set tsflags=nodocs, so every %doc file is | |
| # dropped at install time. rand_core ships its crate docs that way and | |
| # its lib.rs does #![doc = include_str!("../README.md")], so rust-just | |
| # failed to compile: rustc could not read ../README.md. Nothing was wrong | |
| # with the Fedora package: mock installs docs into a build root, and | |
| # this container was not. Restore that. | |
| sed -i "/^tsflags=nodocs/d" /etc/dnf/dnf.conf | |
| # Fedora 44 plus Hummingbird, mirroring Hummingbird mock.cfg: | |
| # Fedora release repos with its own Pulp repos shadowing them by | |
| # priority. Proven correct by this job own diagnostic below -- | |
| # openssl 3.5.7 means libcrypto.so.3, the ABI Hummingbird has. | |
| # A Rawhide root produced RPMs needing libcrypto.so.4 instead. | |
| cp /repos/hummingbird.repo /etc/yum.repos.d/ | |
| # RPMs from earlier stages become a local repo, so a later stage | |
| # can satisfy a BuildRequires on something this run just built. | |
| # The guard must recurse: upload-artifact takes the common parent | |
| # of its path globs as the artifact root, so an artifact declaring | |
| # work/result/*.rpm and work/reports/*.json unpacks as | |
| # prior/result/*.rpm, not prior/*.rpm. A non-recursive glob matched | |
| # nothing, this block was silently skipped in every stage, and | |
| # mutter resolved gsettings-desktop-schemas to Fedora 50.1 instead | |
| # of the 51.beta stage 0 had just built. createrepo_c itself walks | |
| # the tree, so only the test needed fixing. | |
| if [ -n "$(find /work/prior -name "*.rpm" -print -quit 2>/dev/null)" ]; then | |
| dnf -y $disable install createrepo_c | |
| createrepo_c /work/prior | |
| printf "[stages]\nname=stages\nbaseurl=file:///work/prior\nenabled=1\ngpgcheck=0\npriority=1\n" \ | |
| > /etc/yum.repos.d/stages.repo | |
| # priority alone does not keep Fedora out. gnome-control-center | |
| # pulled Fedora accountsservice 23.13.9 even though stage 0 had | |
| # built 26.27.3 and [stages] was priority 1: the Fedora main | |
| # package entered the transaction and pinned accountsservice-libs | |
| # to its exact NEVR, so our libs could not be installed and our | |
| # devel, which needs them, was dropped -- | |
| # cannot install both accountsservice-libs-26.27.3 from stages | |
| # and accountsservice-libs-23.13.9-16.fc44 from fedora | |
| # Excluding by name is what settles it: whatever an earlier stage | |
| # built, Fedora must not answer for. Names come from rpm rather | |
| # than from parsing filenames, which stops working the moment a | |
| # disttag changes. | |
| EXCLUDE=$(find /work/prior -name "*.rpm" -type f -print0 \ | |
| | xargs -0 -r rpm -qp --qf "%{NAME}\n" 2>/dev/null \ | |
| | sort -u | paste -sd, -) | |
| echo "excluding from Fedora: $EXCLUDE" | |
| fi | |
| # Hummingbird ships newer versions of some names than Fedora 44 | |
| # does, and the two must never mix in one transaction. The | |
| # conflicts this prevents are real: libicu 78.3 (hum) vs 77.1 | |
| # (fc44) broke samba and evolution-data-server, and Fedora ruby | |
| # 3.3/3.4-default-gems vs Hummingbird ruby4.0-default-gems broke | |
| # webkitgtk, colord, libnotify and zsh. Always prefer the | |
| # Hummingbird copy by excluding these names from Fedora. | |
| # Unconditional: stage 0 has no prior RPMs, so the block above | |
| # never ran and EXCLUDE would otherwise be empty here. | |
| HB_EXCLUDE="ruby-default-gems,ruby3.3-default-gems,ruby3.4-default-gems,libicu,icu,gpgme,qt6-qtbase" | |
| EXCLUDE="${HB_EXCLUDE}${EXCLUDE:+,}${EXCLUDE}" | |
| echo "hummingbird exclusions: $HB_EXCLUDE" | |
| if [ -n "${FACTORY_REPO:-}" ]; then | |
| printf "[factory]\nname=factory\nbaseurl=%s\nenabled=1\ngpgcheck=0\npriority=5\n" \ | |
| "$FACTORY_REPO" > /etc/yum.repos.d/factory.repo | |
| fi | |
| # The plain fedora image is not a build root: it lacks the group mock | |
| # installs, so /usr/bin/echo and friends are missing. Most packages pull | |
| # them in transitively; squashfs-tools calls echo directly from its | |
| # manpage installer and fails without it. | |
| dnf -y $disable install dnf-plugins-core mock rpm-build @buildsys-build | |
| rpm -q --qf "buildroot openssl: %{VERSION}-%{RELEASE}\n" openssl-libs || true | |
| # NO APOSTROPHES IN THIS SCRIPT. It is the body of bash -exc | |
| # a single-quoted string, so one closes it and everything after | |
| # is reparsed. That is what the stilted "Fedora own | |
| # noopenh264" and "this job own diagnostic" above are avoiding. | |
| # Three apostrophes in this very comment took out all 36 stage 0 | |
| # jobs at once, in under two minutes, with no clue in the log. | |
| # | |
| # The AlmaLinux convention, one distro over. They keep the vendor | |
| # release and dist and append to it -- their dnf is | |
| # 4.14.0-34.el9_8.alma.1 against 34.el9_8 from Red Hat, and both | |
| # .alma and .alma.N appear in their repositories. | |
| # | |
| # The vendor here is Hummingbird, not Fedora. These packages are | |
| # built for Hummingbird and installed on Hummingbird; Fedora 44 is | |
| # only the other half of the buildroot, the way a compiler is. An | |
| # earlier version of this tagged them .fc44.bfin, which named the | |
| # distribution they are not for. | |
| # | |
| # The tag is read from the Hummingbird packages present in the | |
| # buildroot rather than hardcoded, so a move to hum2 carries | |
| # itself. A buildroot containing none of them is a repository | |
| # misconfiguration -- the exact failure this factory exists to | |
| # avoid -- so it stops rather than quietly tagging something else. | |
| HUM_TAG="$(rpm -qa --qf "%{RELEASE}\n" | grep -oE "hum[0-9]+$" | sort -u | head -n1)" | |
| if [ -z "$HUM_TAG" ]; then | |
| echo "No Hummingbird package in the buildroot; cannot derive a disttag" >&2 | |
| rpm -qa --qf "%{NAME} %{RELEASE}\n" | sort | head -20 >&2 | |
| exit 1 | |
| fi | |
| DISTTAG=".${HUM_TAG}.bfin${DIST_BUMP:-}" | |
| echo "disttag: $DISTTAG" | |
| # libratbag %check starts ratbagd, which calls | |
| # Gio.bus_get_sync(Gio.BusType.SYSTEM) and dies with "Could not | |
| # connect: No such file or directory" -- a plain container has no | |
| # system bus socket. Seven of its ten suites already pass and the | |
| # failing one is a real test, so give the build root a bus rather | |
| # than disabling the test. Non-fatal: no other package needs it. | |
| dnf -y $disable install dbus-daemon || true | |
| mkdir -p /run/dbus | |
| dbus-daemon --system --fork || true | |
| # mock defines USER in its build root; a bare container does not. | |
| # just 1.57.0 tests/functions.rs:88 calls env::var("USER").unwrap() | |
| # and panicked with NotPresent -- 1823 tests passed, that one did | |
| # not. Same shape as the missing system bus: supply what a real | |
| # build root has rather than disable the test. | |
| export USER="${USER:-root}" | |
| export LOGNAME="${LOGNAME:-$USER}" | |
| spec=$(find "/packages/$PACKAGE" -maxdepth 1 -name "*.spec" -print -quit) | |
| test -n "$spec" | |
| dnf -y $disable ${EXCLUDE:+--setopt=fedora.excludepkgs="$EXCLUDE"} \ | |
| ${EXCLUDE:+--setopt=updates.excludepkgs="$EXCLUDE"} builddep -D "_sourcedir /packages/$PACKAGE" "$spec" | |
| # An imported spec keeps its dist-git PatchN and auxiliary SourceN | |
| # files next to itself, while the verified upstream archive lands in | |
| # /work/sources. rpmbuild takes a single _sourcedir, so stage both: | |
| # recipe files first, then the verified archive, which therefore wins | |
| # over anything of the same name carried in the import. | |
| staged=/work/staged/$PACKAGE | |
| rm -rf "$staged" | |
| mkdir -p "$staged" | |
| cp -a "/packages/$PACKAGE/." "$staged/" | |
| cp -a "/work/sources/$PACKAGE/." "$staged/" | |
| # Packages with %generate_buildrequires -- every Rust one -- compute | |
| # their real BuildRequires during the build, so the spec alone does not | |
| # list them and rpmbuild exits 11 asking to be re-run. Install what the | |
| # generated source RPM declares and retry, bounded so an unsatisfiable | |
| # requirement fails instead of looping. | |
| for _ in 1 2 3 4 5; do | |
| rm -f /root/rpmbuild/SRPMS/*.buildreqs.nosrc.rpm | |
| if rpmbuild -br "$spec" --define "_sourcedir $staged" \ | |
| --define "dist $DISTTAG"; then break; fi | |
| generated=$(ls /root/rpmbuild/SRPMS/*.buildreqs.nosrc.rpm 2>/dev/null | head -1) | |
| test -n "$generated" | |
| dnf -y $disable ${EXCLUDE:+--setopt=fedora.excludepkgs="$EXCLUDE"} \ | |
| ${EXCLUDE:+--setopt=updates.excludepkgs="$EXCLUDE"} builddep -D "_sourcedir /packages/$PACKAGE" "$generated" | |
| done | |
| rpmbuild -ba "$spec" \ | |
| --define "_sourcedir $staged" \ | |
| --define "dist $DISTTAG" \ | |
| --define "_rpmdir /work/result" | |
| find /work/result -name "*.rpm" -type f -print0 | \ | |
| xargs -0 -r rpm -qp --qf "%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\n" | |
| # A build that produced no RPM must fail here. if-no-files-found on | |
| # the upload cannot catch it: the artifact also carries | |
| # work/reports/*.json, so one file always matches and the upload | |
| # reports success while shipping no packages at all. | |
| test -n "$(find /work/result -name "*.rpm" -type f -print -quit)" | |
| ' | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: rpm-s2-${{ matrix.package }} | |
| path: | | |
| work/result/**/*.rpm | |
| work/reports/*.json | |
| if-no-files-found: error | |
| rebuild3: | |
| needs: [prepare, rebuild0, rebuild1, rebuild2] | |
| if: ${{ !cancelled() && needs.prepare.outputs.stage3 != '[]' }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| package: ${{ fromJSON(needs.prepare.outputs.stage3) }} | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: ./.github/actions/setup-sccache | |
| - name: Collect RPMs built by earlier stages | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| pattern: rpm-* | |
| path: work/prior | |
| merge-multiple: true | |
| continue-on-error: true | |
| - name: Fetch and verify direct upstream source | |
| env: | |
| PACKAGE: ${{ matrix.package }} | |
| run: | | |
| python3 tools/source_pipeline.py "$PACKAGE" --output work/sources --report-dir work/reports | |
| - name: Build the verified source with its RPM recipe | |
| env: | |
| PACKAGE: ${{ matrix.package }} | |
| FACTORY_REPO: ${{ vars.FACTORY_REPO }} | |
| run: | | |
| test -d "packages/$PACKAGE" | |
| mkdir -p work/result | |
| # Per-package, so it is resolved out here rather than inside the | |
| # container, which only sees config/ and packages/. | |
| DIST_BUMP="$(python3 tools/dist_bump.py "$PACKAGE")" | |
| export DIST_BUMP | |
| docker run --rm --privileged \ | |
| -e PACKAGE -e FACTORY_REPO -e DIST_BUMP \ | |
| -v "$PWD/work:/work:Z" \ | |
| -v "$PWD/packages:/packages:ro,Z" \ | |
| -v "$PWD/config:/repos:ro,Z" \ | |
| quay.io/fedora/fedora:44 bash -exc ' | |
| # The fedora:rawhide image ships fedora-cisco-openh264 enabled, but | |
| # its packages are signed with Cisco key, which the image does not | |
| # trust -- so any builddep graph reaching gstreamer/pipewire dies on | |
| # "Import of the key did not help, wrong key?". openh264 is a runtime | |
| # codec, never a build requirement, and Fedora own noopenh264 provides | |
| # the same libopenh264.so.8 soname, so disabling the repo resolves. | |
| disable=--disablerepo=fedora-cisco-openh264 | |
| # The Fedora container images set tsflags=nodocs, so every %doc file is | |
| # dropped at install time. rand_core ships its crate docs that way and | |
| # its lib.rs does #![doc = include_str!("../README.md")], so rust-just | |
| # failed to compile: rustc could not read ../README.md. Nothing was wrong | |
| # with the Fedora package: mock installs docs into a build root, and | |
| # this container was not. Restore that. | |
| sed -i "/^tsflags=nodocs/d" /etc/dnf/dnf.conf | |
| # Fedora 44 plus Hummingbird, mirroring Hummingbird mock.cfg: | |
| # Fedora release repos with its own Pulp repos shadowing them by | |
| # priority. Proven correct by this job own diagnostic below -- | |
| # openssl 3.5.7 means libcrypto.so.3, the ABI Hummingbird has. | |
| # A Rawhide root produced RPMs needing libcrypto.so.4 instead. | |
| cp /repos/hummingbird.repo /etc/yum.repos.d/ | |
| # RPMs from earlier stages become a local repo, so a later stage | |
| # can satisfy a BuildRequires on something this run just built. | |
| # The guard must recurse: upload-artifact takes the common parent | |
| # of its path globs as the artifact root, so an artifact declaring | |
| # work/result/*.rpm and work/reports/*.json unpacks as | |
| # prior/result/*.rpm, not prior/*.rpm. A non-recursive glob matched | |
| # nothing, this block was silently skipped in every stage, and | |
| # mutter resolved gsettings-desktop-schemas to Fedora 50.1 instead | |
| # of the 51.beta stage 0 had just built. createrepo_c itself walks | |
| # the tree, so only the test needed fixing. | |
| if [ -n "$(find /work/prior -name "*.rpm" -print -quit 2>/dev/null)" ]; then | |
| dnf -y $disable install createrepo_c | |
| createrepo_c /work/prior | |
| printf "[stages]\nname=stages\nbaseurl=file:///work/prior\nenabled=1\ngpgcheck=0\npriority=1\n" \ | |
| > /etc/yum.repos.d/stages.repo | |
| # priority alone does not keep Fedora out. gnome-control-center | |
| # pulled Fedora accountsservice 23.13.9 even though stage 0 had | |
| # built 26.27.3 and [stages] was priority 1: the Fedora main | |
| # package entered the transaction and pinned accountsservice-libs | |
| # to its exact NEVR, so our libs could not be installed and our | |
| # devel, which needs them, was dropped -- | |
| # cannot install both accountsservice-libs-26.27.3 from stages | |
| # and accountsservice-libs-23.13.9-16.fc44 from fedora | |
| # Excluding by name is what settles it: whatever an earlier stage | |
| # built, Fedora must not answer for. Names come from rpm rather | |
| # than from parsing filenames, which stops working the moment a | |
| # disttag changes. | |
| EXCLUDE=$(find /work/prior -name "*.rpm" -type f -print0 \ | |
| | xargs -0 -r rpm -qp --qf "%{NAME}\n" 2>/dev/null \ | |
| | sort -u | paste -sd, -) | |
| echo "excluding from Fedora: $EXCLUDE" | |
| fi | |
| # Hummingbird ships newer versions of some names than Fedora 44 | |
| # does, and the two must never mix in one transaction. The | |
| # conflicts this prevents are real: libicu 78.3 (hum) vs 77.1 | |
| # (fc44) broke samba and evolution-data-server, and Fedora ruby | |
| # 3.3/3.4-default-gems vs Hummingbird ruby4.0-default-gems broke | |
| # webkitgtk, colord, libnotify and zsh. Always prefer the | |
| # Hummingbird copy by excluding these names from Fedora. | |
| # Unconditional: stage 0 has no prior RPMs, so the block above | |
| # never ran and EXCLUDE would otherwise be empty here. | |
| HB_EXCLUDE="ruby-default-gems,ruby3.3-default-gems,ruby3.4-default-gems,libicu,icu,gpgme,qt6-qtbase" | |
| EXCLUDE="${HB_EXCLUDE}${EXCLUDE:+,}${EXCLUDE}" | |
| echo "hummingbird exclusions: $HB_EXCLUDE" | |
| if [ -n "${FACTORY_REPO:-}" ]; then | |
| printf "[factory]\nname=factory\nbaseurl=%s\nenabled=1\ngpgcheck=0\npriority=5\n" \ | |
| "$FACTORY_REPO" > /etc/yum.repos.d/factory.repo | |
| fi | |
| # The plain fedora image is not a build root: it lacks the group mock | |
| # installs, so /usr/bin/echo and friends are missing. Most packages pull | |
| # them in transitively; squashfs-tools calls echo directly from its | |
| # manpage installer and fails without it. | |
| dnf -y $disable install dnf-plugins-core mock rpm-build @buildsys-build | |
| rpm -q --qf "buildroot openssl: %{VERSION}-%{RELEASE}\n" openssl-libs || true | |
| # NO APOSTROPHES IN THIS SCRIPT. It is the body of bash -exc | |
| # a single-quoted string, so one closes it and everything after | |
| # is reparsed. That is what the stilted "Fedora own | |
| # noopenh264" and "this job own diagnostic" above are avoiding. | |
| # Three apostrophes in this very comment took out all 36 stage 0 | |
| # jobs at once, in under two minutes, with no clue in the log. | |
| # | |
| # The AlmaLinux convention, one distro over. They keep the vendor | |
| # release and dist and append to it -- their dnf is | |
| # 4.14.0-34.el9_8.alma.1 against 34.el9_8 from Red Hat, and both | |
| # .alma and .alma.N appear in their repositories. | |
| # | |
| # The vendor here is Hummingbird, not Fedora. These packages are | |
| # built for Hummingbird and installed on Hummingbird; Fedora 44 is | |
| # only the other half of the buildroot, the way a compiler is. An | |
| # earlier version of this tagged them .fc44.bfin, which named the | |
| # distribution they are not for. | |
| # | |
| # The tag is read from the Hummingbird packages present in the | |
| # buildroot rather than hardcoded, so a move to hum2 carries | |
| # itself. A buildroot containing none of them is a repository | |
| # misconfiguration -- the exact failure this factory exists to | |
| # avoid -- so it stops rather than quietly tagging something else. | |
| HUM_TAG="$(rpm -qa --qf "%{RELEASE}\n" | grep -oE "hum[0-9]+$" | sort -u | head -n1)" | |
| if [ -z "$HUM_TAG" ]; then | |
| echo "No Hummingbird package in the buildroot; cannot derive a disttag" >&2 | |
| rpm -qa --qf "%{NAME} %{RELEASE}\n" | sort | head -20 >&2 | |
| exit 1 | |
| fi | |
| DISTTAG=".${HUM_TAG}.bfin${DIST_BUMP:-}" | |
| echo "disttag: $DISTTAG" | |
| # libratbag %check starts ratbagd, which calls | |
| # Gio.bus_get_sync(Gio.BusType.SYSTEM) and dies with "Could not | |
| # connect: No such file or directory" -- a plain container has no | |
| # system bus socket. Seven of its ten suites already pass and the | |
| # failing one is a real test, so give the build root a bus rather | |
| # than disabling the test. Non-fatal: no other package needs it. | |
| dnf -y $disable install dbus-daemon || true | |
| mkdir -p /run/dbus | |
| dbus-daemon --system --fork || true | |
| # mock defines USER in its build root; a bare container does not. | |
| # just 1.57.0 tests/functions.rs:88 calls env::var("USER").unwrap() | |
| # and panicked with NotPresent -- 1823 tests passed, that one did | |
| # not. Same shape as the missing system bus: supply what a real | |
| # build root has rather than disable the test. | |
| export USER="${USER:-root}" | |
| export LOGNAME="${LOGNAME:-$USER}" | |
| spec=$(find "/packages/$PACKAGE" -maxdepth 1 -name "*.spec" -print -quit) | |
| test -n "$spec" | |
| dnf -y $disable ${EXCLUDE:+--setopt=fedora.excludepkgs="$EXCLUDE"} \ | |
| ${EXCLUDE:+--setopt=updates.excludepkgs="$EXCLUDE"} builddep -D "_sourcedir /packages/$PACKAGE" "$spec" | |
| # An imported spec keeps its dist-git PatchN and auxiliary SourceN | |
| # files next to itself, while the verified upstream archive lands in | |
| # /work/sources. rpmbuild takes a single _sourcedir, so stage both: | |
| # recipe files first, then the verified archive, which therefore wins | |
| # over anything of the same name carried in the import. | |
| staged=/work/staged/$PACKAGE | |
| rm -rf "$staged" | |
| mkdir -p "$staged" | |
| cp -a "/packages/$PACKAGE/." "$staged/" | |
| cp -a "/work/sources/$PACKAGE/." "$staged/" | |
| # Packages with %generate_buildrequires -- every Rust one -- compute | |
| # their real BuildRequires during the build, so the spec alone does not | |
| # list them and rpmbuild exits 11 asking to be re-run. Install what the | |
| # generated source RPM declares and retry, bounded so an unsatisfiable | |
| # requirement fails instead of looping. | |
| for _ in 1 2 3 4 5; do | |
| rm -f /root/rpmbuild/SRPMS/*.buildreqs.nosrc.rpm | |
| if rpmbuild -br "$spec" --define "_sourcedir $staged" \ | |
| --define "dist $DISTTAG"; then break; fi | |
| generated=$(ls /root/rpmbuild/SRPMS/*.buildreqs.nosrc.rpm 2>/dev/null | head -1) | |
| test -n "$generated" | |
| dnf -y $disable ${EXCLUDE:+--setopt=fedora.excludepkgs="$EXCLUDE"} \ | |
| ${EXCLUDE:+--setopt=updates.excludepkgs="$EXCLUDE"} builddep -D "_sourcedir /packages/$PACKAGE" "$generated" | |
| done | |
| rpmbuild -ba "$spec" \ | |
| --define "_sourcedir $staged" \ | |
| --define "dist $DISTTAG" \ | |
| --define "_rpmdir /work/result" | |
| find /work/result -name "*.rpm" -type f -print0 | \ | |
| xargs -0 -r rpm -qp --qf "%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\n" | |
| # A build that produced no RPM must fail here. if-no-files-found on | |
| # the upload cannot catch it: the artifact also carries | |
| # work/reports/*.json, so one file always matches and the upload | |
| # reports success while shipping no packages at all. | |
| test -n "$(find /work/result -name "*.rpm" -type f -print -quit)" | |
| ' | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: rpm-s3-${{ matrix.package }} | |
| path: | | |
| work/result/**/*.rpm | |
| work/reports/*.json | |
| if-no-files-found: error | |
| rebuild4: | |
| needs: [prepare, rebuild0, rebuild1, rebuild2, rebuild3] | |
| if: ${{ !cancelled() && needs.prepare.outputs.stage4 != '[]' }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| package: ${{ fromJSON(needs.prepare.outputs.stage4) }} | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: ./.github/actions/setup-sccache | |
| - name: Collect RPMs built by earlier stages | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| pattern: rpm-* | |
| path: work/prior | |
| merge-multiple: true | |
| continue-on-error: true | |
| - name: Fetch and verify direct upstream source | |
| env: | |
| PACKAGE: ${{ matrix.package }} | |
| run: | | |
| python3 tools/source_pipeline.py "$PACKAGE" --output work/sources --report-dir work/reports | |
| - name: Build the verified source with its RPM recipe | |
| env: | |
| PACKAGE: ${{ matrix.package }} | |
| FACTORY_REPO: ${{ vars.FACTORY_REPO }} | |
| run: | | |
| test -d "packages/$PACKAGE" | |
| mkdir -p work/result | |
| # Per-package, so it is resolved out here rather than inside the | |
| # container, which only sees config/ and packages/. | |
| DIST_BUMP="$(python3 tools/dist_bump.py "$PACKAGE")" | |
| export DIST_BUMP | |
| docker run --rm --privileged \ | |
| -e PACKAGE -e FACTORY_REPO -e DIST_BUMP \ | |
| -v "$PWD/work:/work:Z" \ | |
| -v "$PWD/packages:/packages:ro,Z" \ | |
| -v "$PWD/config:/repos:ro,Z" \ | |
| quay.io/fedora/fedora:44 bash -exc ' | |
| # The fedora:rawhide image ships fedora-cisco-openh264 enabled, but | |
| # its packages are signed with Cisco key, which the image does not | |
| # trust -- so any builddep graph reaching gstreamer/pipewire dies on | |
| # "Import of the key did not help, wrong key?". openh264 is a runtime | |
| # codec, never a build requirement, and Fedora own noopenh264 provides | |
| # the same libopenh264.so.8 soname, so disabling the repo resolves. | |
| disable=--disablerepo=fedora-cisco-openh264 | |
| # The Fedora container images set tsflags=nodocs, so every %doc file is | |
| # dropped at install time. rand_core ships its crate docs that way and | |
| # its lib.rs does #![doc = include_str!("../README.md")], so rust-just | |
| # failed to compile: rustc could not read ../README.md. Nothing was wrong | |
| # with the Fedora package: mock installs docs into a build root, and | |
| # this container was not. Restore that. | |
| sed -i "/^tsflags=nodocs/d" /etc/dnf/dnf.conf | |
| # Fedora 44 plus Hummingbird, mirroring Hummingbird mock.cfg: | |
| # Fedora release repos with its own Pulp repos shadowing them by | |
| # priority. Proven correct by this job own diagnostic below -- | |
| # openssl 3.5.7 means libcrypto.so.3, the ABI Hummingbird has. | |
| # A Rawhide root produced RPMs needing libcrypto.so.4 instead. | |
| cp /repos/hummingbird.repo /etc/yum.repos.d/ | |
| # RPMs from earlier stages become a local repo, so a later stage | |
| # can satisfy a BuildRequires on something this run just built. | |
| # The guard must recurse: upload-artifact takes the common parent | |
| # of its path globs as the artifact root, so an artifact declaring | |
| # work/result/*.rpm and work/reports/*.json unpacks as | |
| # prior/result/*.rpm, not prior/*.rpm. A non-recursive glob matched | |
| # nothing, this block was silently skipped in every stage, and | |
| # mutter resolved gsettings-desktop-schemas to Fedora 50.1 instead | |
| # of the 51.beta stage 0 had just built. createrepo_c itself walks | |
| # the tree, so only the test needed fixing. | |
| if [ -n "$(find /work/prior -name "*.rpm" -print -quit 2>/dev/null)" ]; then | |
| dnf -y $disable install createrepo_c | |
| createrepo_c /work/prior | |
| printf "[stages]\nname=stages\nbaseurl=file:///work/prior\nenabled=1\ngpgcheck=0\npriority=1\n" \ | |
| > /etc/yum.repos.d/stages.repo | |
| # priority alone does not keep Fedora out. gnome-control-center | |
| # pulled Fedora accountsservice 23.13.9 even though stage 0 had | |
| # built 26.27.3 and [stages] was priority 1: the Fedora main | |
| # package entered the transaction and pinned accountsservice-libs | |
| # to its exact NEVR, so our libs could not be installed and our | |
| # devel, which needs them, was dropped -- | |
| # cannot install both accountsservice-libs-26.27.3 from stages | |
| # and accountsservice-libs-23.13.9-16.fc44 from fedora | |
| # Excluding by name is what settles it: whatever an earlier stage | |
| # built, Fedora must not answer for. Names come from rpm rather | |
| # than from parsing filenames, which stops working the moment a | |
| # disttag changes. | |
| EXCLUDE=$(find /work/prior -name "*.rpm" -type f -print0 \ | |
| | xargs -0 -r rpm -qp --qf "%{NAME}\n" 2>/dev/null \ | |
| | sort -u | paste -sd, -) | |
| echo "excluding from Fedora: $EXCLUDE" | |
| fi | |
| # Hummingbird ships newer versions of some names than Fedora 44 | |
| # does, and the two must never mix in one transaction. The | |
| # conflicts this prevents are real: libicu 78.3 (hum) vs 77.1 | |
| # (fc44) broke samba and evolution-data-server, and Fedora ruby | |
| # 3.3/3.4-default-gems vs Hummingbird ruby4.0-default-gems broke | |
| # webkitgtk, colord, libnotify and zsh. Always prefer the | |
| # Hummingbird copy by excluding these names from Fedora. | |
| # Unconditional: stage 0 has no prior RPMs, so the block above | |
| # never ran and EXCLUDE would otherwise be empty here. | |
| HB_EXCLUDE="ruby-default-gems,ruby3.3-default-gems,ruby3.4-default-gems,libicu,icu,gpgme,qt6-qtbase" | |
| EXCLUDE="${HB_EXCLUDE}${EXCLUDE:+,}${EXCLUDE}" | |
| echo "hummingbird exclusions: $HB_EXCLUDE" | |
| if [ -n "${FACTORY_REPO:-}" ]; then | |
| printf "[factory]\nname=factory\nbaseurl=%s\nenabled=1\ngpgcheck=0\npriority=5\n" \ | |
| "$FACTORY_REPO" > /etc/yum.repos.d/factory.repo | |
| fi | |
| # The plain fedora image is not a build root: it lacks the group mock | |
| # installs, so /usr/bin/echo and friends are missing. Most packages pull | |
| # them in transitively; squashfs-tools calls echo directly from its | |
| # manpage installer and fails without it. | |
| dnf -y $disable install dnf-plugins-core mock rpm-build @buildsys-build | |
| rpm -q --qf "buildroot openssl: %{VERSION}-%{RELEASE}\n" openssl-libs || true | |
| # NO APOSTROPHES IN THIS SCRIPT. It is the body of bash -exc | |
| # a single-quoted string, so one closes it and everything after | |
| # is reparsed. That is what the stilted "Fedora own | |
| # noopenh264" and "this job own diagnostic" above are avoiding. | |
| # Three apostrophes in this very comment took out all 36 stage 0 | |
| # jobs at once, in under two minutes, with no clue in the log. | |
| # | |
| # The AlmaLinux convention, one distro over. They keep the vendor | |
| # release and dist and append to it -- their dnf is | |
| # 4.14.0-34.el9_8.alma.1 against 34.el9_8 from Red Hat, and both | |
| # .alma and .alma.N appear in their repositories. | |
| # | |
| # The vendor here is Hummingbird, not Fedora. These packages are | |
| # built for Hummingbird and installed on Hummingbird; Fedora 44 is | |
| # only the other half of the buildroot, the way a compiler is. An | |
| # earlier version of this tagged them .fc44.bfin, which named the | |
| # distribution they are not for. | |
| # | |
| # The tag is read from the Hummingbird packages present in the | |
| # buildroot rather than hardcoded, so a move to hum2 carries | |
| # itself. A buildroot containing none of them is a repository | |
| # misconfiguration -- the exact failure this factory exists to | |
| # avoid -- so it stops rather than quietly tagging something else. | |
| HUM_TAG="$(rpm -qa --qf "%{RELEASE}\n" | grep -oE "hum[0-9]+$" | sort -u | head -n1)" | |
| if [ -z "$HUM_TAG" ]; then | |
| echo "No Hummingbird package in the buildroot; cannot derive a disttag" >&2 | |
| rpm -qa --qf "%{NAME} %{RELEASE}\n" | sort | head -20 >&2 | |
| exit 1 | |
| fi | |
| DISTTAG=".${HUM_TAG}.bfin${DIST_BUMP:-}" | |
| echo "disttag: $DISTTAG" | |
| # libratbag %check starts ratbagd, which calls | |
| # Gio.bus_get_sync(Gio.BusType.SYSTEM) and dies with "Could not | |
| # connect: No such file or directory" -- a plain container has no | |
| # system bus socket. Seven of its ten suites already pass and the | |
| # failing one is a real test, so give the build root a bus rather | |
| # than disabling the test. Non-fatal: no other package needs it. | |
| dnf -y $disable install dbus-daemon || true | |
| mkdir -p /run/dbus | |
| dbus-daemon --system --fork || true | |
| # mock defines USER in its build root; a bare container does not. | |
| # just 1.57.0 tests/functions.rs:88 calls env::var("USER").unwrap() | |
| # and panicked with NotPresent -- 1823 tests passed, that one did | |
| # not. Same shape as the missing system bus: supply what a real | |
| # build root has rather than disable the test. | |
| export USER="${USER:-root}" | |
| export LOGNAME="${LOGNAME:-$USER}" | |
| spec=$(find "/packages/$PACKAGE" -maxdepth 1 -name "*.spec" -print -quit) | |
| test -n "$spec" | |
| dnf -y $disable ${EXCLUDE:+--setopt=fedora.excludepkgs="$EXCLUDE"} \ | |
| ${EXCLUDE:+--setopt=updates.excludepkgs="$EXCLUDE"} builddep -D "_sourcedir /packages/$PACKAGE" "$spec" | |
| # An imported spec keeps its dist-git PatchN and auxiliary SourceN | |
| # files next to itself, while the verified upstream archive lands in | |
| # /work/sources. rpmbuild takes a single _sourcedir, so stage both: | |
| # recipe files first, then the verified archive, which therefore wins | |
| # over anything of the same name carried in the import. | |
| staged=/work/staged/$PACKAGE | |
| rm -rf "$staged" | |
| mkdir -p "$staged" | |
| cp -a "/packages/$PACKAGE/." "$staged/" | |
| cp -a "/work/sources/$PACKAGE/." "$staged/" | |
| # Packages with %generate_buildrequires -- every Rust one -- compute | |
| # their real BuildRequires during the build, so the spec alone does not | |
| # list them and rpmbuild exits 11 asking to be re-run. Install what the | |
| # generated source RPM declares and retry, bounded so an unsatisfiable | |
| # requirement fails instead of looping. | |
| for _ in 1 2 3 4 5; do | |
| rm -f /root/rpmbuild/SRPMS/*.buildreqs.nosrc.rpm | |
| if rpmbuild -br "$spec" --define "_sourcedir $staged" \ | |
| --define "dist $DISTTAG"; then break; fi | |
| generated=$(ls /root/rpmbuild/SRPMS/*.buildreqs.nosrc.rpm 2>/dev/null | head -1) | |
| test -n "$generated" | |
| dnf -y $disable ${EXCLUDE:+--setopt=fedora.excludepkgs="$EXCLUDE"} \ | |
| ${EXCLUDE:+--setopt=updates.excludepkgs="$EXCLUDE"} builddep -D "_sourcedir /packages/$PACKAGE" "$generated" | |
| done | |
| rpmbuild -ba "$spec" \ | |
| --define "_sourcedir $staged" \ | |
| --define "dist $DISTTAG" \ | |
| --define "_rpmdir /work/result" | |
| find /work/result -name "*.rpm" -type f -print0 | \ | |
| xargs -0 -r rpm -qp --qf "%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\n" | |
| # A build that produced no RPM must fail here. if-no-files-found on | |
| # the upload cannot catch it: the artifact also carries | |
| # work/reports/*.json, so one file always matches and the upload | |
| # reports success while shipping no packages at all. | |
| test -n "$(find /work/result -name "*.rpm" -type f -print -quit)" | |
| ' | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: rpm-s4-${{ matrix.package }} | |
| path: | | |
| work/result/**/*.rpm | |
| work/reports/*.json | |
| if-no-files-found: error | |
| # A disttag is a convention, not a guarantee. What actually has to be true is | |
| # that every RPM we publish outranks whatever Fedora 44 and Hummingbird offer | |
| # under the same name -- otherwise dnf installs theirs and the rebuild we went | |
| # to the trouble of making is never used. Nothing checked that, and it fails | |
| # silently: the image still resolves, just to the wrong build. | |
| # | |
| # This resolves each name against those repositories, with our own output not | |
| # in the picture, and compares. It reports every offender before failing, so | |
| # one run yields the whole worklist instead of one entry at a time. | |
| precedence: | |
| needs: [prepare, rebuild0, rebuild1, rebuild2, rebuild3, rebuild4] | |
| if: ${{ !cancelled() && needs.prepare.outputs.build_list != '[]' }} | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| pattern: rpm-* | |
| path: built | |
| merge-multiple: true | |
| - name: Assert our builds outrank Fedora and Hummingbird | |
| run: | | |
| docker run --rm \ | |
| -v "$PWD/built:/built:ro,Z" \ | |
| -v "$PWD/config:/repos:ro,Z" \ | |
| quay.io/fedora/fedora:44 bash -exc ' | |
| dnf -y install rpmdevtools >/dev/null | |
| cp /repos/hummingbird.repo /etc/yum.repos.d/ | |
| disable=--disablerepo=fedora-cisco-openh264 | |
| fmt="%|EPOCH?{%{EPOCH}:}|%{VERSION}-%{RELEASE}" | |
| : > /tmp/losers | |
| : > /tmp/overlap | |
| find /built -name "*.rpm" -type f | sort | while read -r rpm; do | |
| case "$rpm" in | |
| *.src.rpm) continue ;; | |
| # The malcontent bootstrap is ranked below the real build on | |
| # purpose and is deleted before publish. It is meant to lose. | |
| *-0.bootstrap.*) continue ;; | |
| esac | |
| name=$(rpm -qp --qf "%{NAME}" "$rpm" 2>/dev/null) || continue | |
| ours=$(rpm -qp --qf "$fmt" "$rpm") | |
| line=$(dnf -q $disable repoquery --latest-limit=1 \ | |
| --qf "$fmt %{reponame}" "$name" 2>/dev/null | tail -n1) | |
| theirs=${line%% *} | |
| repo=${line##* } | |
| # Nothing else ships this name, so there is nothing to outrank. | |
| [ -n "$theirs" ] || continue | |
| rc=0; rpmdev-vercmp "$ours" "$theirs" >/dev/null 2>&1 || rc=$? | |
| # 11 means the first argument is newer, which is what we need. | |
| # 0 (equal) and 12 (theirs newer) both mean ours will not win. | |
| [ "$rc" = 11 ] || echo " $name: ours $ours, $repo has $theirs" >> /tmp/losers | |
| # .hum1.bfin outranks .hum1 at equal release by construction, so | |
| # a package the base OS also ships no longer appears as a loser. | |
| # It is still worth knowing about: this factory is for the | |
| # desktop stack Hummingbird does not ship. | |
| case "$repo" in | |
| *hummingbird*) echo " $name: ours $ours, $repo also has $theirs" >> /tmp/overlap ;; | |
| esac | |
| done | |
| if [ -s /tmp/losers ]; then | |
| echo "These builds do not outrank what the repositories already offer," | |
| echo "so dnf would install the other one:" | |
| cat /tmp/losers | |
| echo | |
| echo "Our disttag outranks both Fedora and Hummingbird at an equal" | |
| echo "release, so a loser here means the thing we forked has moved on:" | |
| echo "its leading release segment is now higher than ours. Rebase the" | |
| echo "spec on the newer dist-git, which raises ours to match." | |
| echo | |
| echo "If the release has NOT moved and we simply need to build it again," | |
| echo "add \"dist_bump\" to the package in config/upstream-sources.json" | |
| echo "instead. That appends .N after .bfin, which is what AlmaLinux does" | |
| echo "with .alma.1. It cannot help with a release bump, because the" | |
| echo "leading segment comes from the spec." | |
| exit 1 | |
| fi | |
| if [ -s /tmp/overlap ]; then | |
| echo "Note: Hummingbird also ships these, and we outrank it:" | |
| cat /tmp/overlap | |
| echo "The remit is the desktop stack Hummingbird does not ship, so each of" | |
| echo "these is worth checking. Not a failure." | |
| fi | |
| echo "every build outranks what Fedora 44 and Hummingbird offer" | |
| ' | |
| publish: | |
| # The consumer tag is atomic. Successful artifacts from a failed matrix are | |
| # still retained by Actions for diagnosis, but they never replace the | |
| # coherent repository Utah consumes. Publication requires every selected | |
| # stage plus precedence and the Hummingbird-only transaction below. | |
| # | |
| # This is no longer main-only. The repository is published as an OCI image | |
| # first and to Pages second, and the OCI push is what consumers actually | |
| # read. Pages can only deploy from the default branch, which meant nothing | |
| # existed to consume until a merge landed -- so an image could never be | |
| # tested against the packages it was meant to use before merging either of | |
| # them. A registry has no such rule. | |
| # | |
| # A pull request from a fork is the exception, and it has to be stated | |
| # rather than assumed: GitHub gives those a read-only GITHUB_TOKEN, so the | |
| # GHCR login and push would fail however the rest of the run went. Skipping | |
| # is honest; failing on a permission that was never going to be granted is | |
| # noise that reads like a broken pipeline. | |
| if: >- | |
| ${{ !cancelled() | |
| && needs.prepare.outputs.build_list != '[]' | |
| && needs.precedence.result == 'success' | |
| && (needs.rebuild0.result == 'success' || needs.rebuild0.result == 'skipped') | |
| && (needs.rebuild1.result == 'success' || needs.rebuild1.result == 'skipped') | |
| && (needs.rebuild2.result == 'success' || needs.rebuild2.result == 'skipped') | |
| && (needs.rebuild3.result == 'success' || needs.rebuild3.result == 'skipped') | |
| && (needs.rebuild4.result == 'success' || needs.rebuild4.result == 'skipped') | |
| && (github.event_name != 'pull_request' | |
| || github.event.pull_request.head.repo.full_name == github.repository) }} | |
| needs: [prepare, rebuild0, rebuild1, rebuild2, rebuild3, rebuild4, precedence] | |
| # Build waves can overlap, but `:latest` is an atomic consumer input. Queue | |
| # only this critical section per ref; the later publisher re-seeds from the | |
| # repository that its predecessor just made current. | |
| concurrency: | |
| group: utah-packages-publish-${{ github.ref }} | |
| cancel-in-progress: false | |
| runs-on: ubuntu-24.04 | |
| outputs: | |
| ref: ${{ steps.oci.outputs.ref }} | |
| digest: ${{ steps.oci.outputs.digest }} | |
| steps: | |
| - name: Seed repository from the last published factory image | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| owner=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") | |
| image="ghcr.io/${owner}/utah-packages:latest" | |
| mkdir -p repository | |
| echo "${GITHUB_TOKEN}" | podman login ghcr.io -u "${{ github.actor }}" --password-stdin | |
| if podman pull "$image"; then | |
| container=$(podman create "$image") | |
| podman cp "$container:/repository/." repository/ | |
| podman rm "$container" | |
| else | |
| echo "No prior factory image; publishing a new repository" | |
| fi | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| pattern: rpm-* | |
| path: repository | |
| merge-multiple: true | |
| - name: Create and keylessly sign repository metadata | |
| run: | | |
| sudo apt-get update && sudo apt-get install -y createrepo-c | |
| # The malcontent bootstrap pass exists only to give the next stage a | |
| # malcontent-libs linked against accountsservice 26, breaking the | |
| # flatpak-libs -> libmalcontent-0.so.0 cycle. It has no parental | |
| # controls UI, so it must not reach anyone's system. Its release is | |
| # 0.bootstrap, below the real build, so this is belt and braces. | |
| find repository -name '*-0.bootstrap.*.rpm' -print -delete | |
| createrepo_c --update repository | |
| curl -fsSL https://github.com/sigstore/cosign/releases/download/v2.4.1/cosign-linux-amd64 -o cosign | |
| install -m 0755 cosign /usr/local/bin/cosign | |
| cosign sign-blob --yes --bundle repository/repodata/repomd.xml.bundle repository/repodata/repomd.xml | |
| - name: Validate Hummingbird-only consumer transaction | |
| run: | | |
| CONTRACT=$(python3 tools/runtime_contract.py \ | |
| config/bluefin-packages.toml config/runtime-contract.toml) | |
| BASE_IMAGE=$(python3 tools/runtime_contract.py \ | |
| config/bluefin-packages.toml config/runtime-contract.toml --base-image) | |
| export CONTRACT | |
| docker run --rm \ | |
| -e CONTRACT \ | |
| -v "$PWD/repository:/repository:ro,Z" \ | |
| -v "$PWD/config:/config:ro,Z" \ | |
| "$BASE_IMAGE" bash -euo pipefail -c ' | |
| cp /config/hummingbird.repo /etc/yum.repos.d/hummingbird.repo | |
| cat >/etc/yum.repos.d/factory.repo <<EOF | |
| [factory] | |
| name=Utah package factory candidate | |
| baseurl=file:///repository | |
| enabled=1 | |
| gpgcheck=0 | |
| EOF | |
| mapfile -t packages <<<"$CONTRACT" | |
| test "${#packages[@]}" -gt 0 | |
| DNF=$(command -v dnf5 || command -v dnf) | |
| output=$($DNF --assumeno --setopt=install_weak_deps=False \ | |
| --disablerepo="*" \ | |
| --enablerepo=factory \ | |
| --enablerepo=public-hummingbird-x86_64-rpms \ | |
| install "${packages[@]}" 2>&1 || true) | |
| printf "%s\n" "$output" | |
| if grep -Eqi "No match for argument|nothing provides|conflicting requests|cannot install both|does not belong to a distupgrade repository" <<<"$output"; then | |
| echo "Hummingbird-only consumer transaction is not resolvable" >&2 | |
| exit 1 | |
| fi | |
| if ! grep -Eq "Transaction Summary|Nothing to do" <<<"$output"; then | |
| echo "DNF did not produce a valid transaction summary" >&2 | |
| exit 1 | |
| fi | |
| echo "Hummingbird-only consumer transaction resolves ${#packages[@]} packages" | |
| ' | |
| # The repository as an OCI image, which is how everything else in this | |
| # ecosystem ships build output: Utah Containerfile already pulls | |
| # projectbluefin/common and ublue-os/brew this way, pinned by digest. | |
| # | |
| # A registry beats a Pages site here on three counts. It works from any | |
| # branch, so an image can be built against a package set before either is | |
| # merged. It is addressable by digest, so an image records exactly which | |
| # packages went into it rather than whatever the site served that day. And | |
| # provenance comes from the registry and a signature over the digest, | |
| # rather than from an unsigned directory of RPMs over HTTPS. | |
| - name: Publish the repository as an OCI image | |
| id: oci | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| DOCKER_CONFIG: ${{ runner.temp }}/.docker | |
| run: | | |
| set -euo pipefail | |
| owner=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") | |
| image="ghcr.io/${owner}/utah-packages" | |
| # main publishes the tag consumers follow; every other ref publishes | |
| # under its own name so it can be tested without touching main. | |
| if [ "${{ github.ref }}" = "refs/heads/main" ]; then | |
| tag=latest | |
| else | |
| tag=$(echo "${{ github.ref_name }}" | tr "/" "-" | tr -cd "[:alnum:]._-" | cut -c1-96) | |
| fi | |
| printf "FROM scratch\nCOPY repository /repository\n" > Containerfile.repo | |
| # cosign signs by pushing to the same registry, but it reads the | |
| # Docker config rather than podman own auth file, so the login has to | |
| # land somewhere both of them look. DOCKER_CONFIG is set on this step | |
| # and on the signing step to the same directory. | |
| mkdir -p "${DOCKER_CONFIG}" | |
| echo "${GITHUB_TOKEN}" | podman login ghcr.io -u "${{ github.actor }}" \ | |
| --password-stdin --authfile "${DOCKER_CONFIG}/config.json" | |
| podman build --tag "${image}:${tag}" --file Containerfile.repo . | |
| podman push "${image}:${tag}" --digestfile /tmp/digest \ | |
| --authfile "${DOCKER_CONFIG}/config.json" | |
| digest=$(cat /tmp/digest) | |
| echo "ref=${image}:${tag}" >> "$GITHUB_OUTPUT" | |
| echo "digest=${digest}" >> "$GITHUB_OUTPUT" | |
| echo "published ${image}:${tag}" | |
| echo "consume it with: ${image}@${digest}" | |
| { | |
| echo "### Package repository published" | |
| echo | |
| echo '```' | |
| echo "${image}:${tag}" | |
| echo "${image}@${digest}" | |
| echo '```' | |
| echo | |
| echo "Consume in a Containerfile:" | |
| echo '```dockerfile' | |
| echo "FROM ${image}@${digest} AS packages" | |
| echo "COPY --from=packages /repository /etc/utah-packages" | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Sign the published image | |
| env: | |
| COSIGN_YES: "true" | |
| DOCKER_CONFIG: ${{ runner.temp }}/.docker | |
| run: | | |
| owner=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") | |
| cosign sign "ghcr.io/${owner}/utah-packages@${{ steps.oci.outputs.digest }}" | |
| # Handed to the Pages job rather than deployed here, so that the registry | |
| # push never depends on the github-pages environment. That environment | |
| # usually carries a deployment branch rule, and inheriting it would have | |
| # blocked this job on exactly the branches the OCI push exists to serve. | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: repository | |
| path: repository | |
| retention-days: 1 | |
| # A convenience mirror for anything wanting a plain HTTP repository. Pages can | |
| # only deploy from the default branch, which is why it is no longer the thing | |
| # consumers depend on. | |
| publish_pages: | |
| if: ${{ !cancelled() && github.ref == 'refs/heads/main' && needs.publish.result == 'success' }} | |
| needs: publish | |
| runs-on: ubuntu-24.04 | |
| environment: | |
| name: github-pages | |
| url: ${{ steps.deployment.outputs.page_url }} | |
| steps: | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: repository | |
| path: repository | |
| - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5 | |
| with: | |
| path: repository | |
| - id: deployment | |
| uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5 |