From 507415f870fdc0638288cec4088b14d5f92911e3 Mon Sep 17 00:00:00 2001 From: Misha Bragin Date: Thu, 24 Sep 2026 11:06:16 +0200 Subject: [PATCH] [client] Fix RPM metadata for Red Hat certification (#7614) Goreleaser's RPM build is split into one nfpm entry per architecture, each pinned to a single-arch build, with the version substituted from the release job. The deb package, archives, and container images are unaffected. Also fixes rpmlint: incoherent-version-in-changelog, found while investigating: nfpm writes the changelog title straight from semver and never appends the release, so entries read 0.79.0 against a 0.79.0-1 package. --- .github/workflows/release.yml | 11 ++++-- .gitignore | 3 ++ .goreleaser.yaml | 65 +++++++++++++++++++++++++++++++--- release_files/rpm-changelog.sh | 35 ++++++++++++++++++ release_files/rpm-provides.sh | 46 ++++++++++++++++++++++++ 5 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 release_files/rpm-provides.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 51426a7ce..37c6fed6c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -191,6 +191,9 @@ jobs: # requires a changelog. Generated, not committed (see .gitignore). # chglog is a go.mod tool directive, so go.sum pins it and its deps. run: bash release_files/rpm-changelog.sh + - name: Fill the RPM ISA provide version + # nfpm cannot emit rpmbuild's ISA provide and GoReleaser cannot template it. + run: bash release_files/rpm-provides.sh - name: Set up QEMU uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 #v4.1.0 - name: Set up Docker Buildx @@ -230,14 +233,18 @@ jobs: uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2 with: version: ${{ env.GORELEASER_VER }} - args: release --clean ${{ env.flags }} + args: release --config .goreleaser.generated.yaml --clean ${{ env.flags }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} UPLOAD_DEBIAN_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }} UPLOAD_YUM_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }} GPG_RPM_KEY_FILE: ${{ env.GPG_RPM_KEY_FILE }} - NFPM_NETBIRD_RPM_PASSPHRASE: ${{ secrets.GPG_RPM_PASSPHRASE }} + # One per nfpm id: GoReleaser looks the passphrase up as NFPM__PASSPHRASE. + NFPM_NETBIRD_RPM_AMD64_PASSPHRASE: ${{ secrets.GPG_RPM_PASSPHRASE }} + NFPM_NETBIRD_RPM_ARM64_PASSPHRASE: ${{ secrets.GPG_RPM_PASSPHRASE }} + NFPM_NETBIRD_RPM_ARM_PASSPHRASE: ${{ secrets.GPG_RPM_PASSPHRASE }} + NFPM_NETBIRD_RPM_386_PASSPHRASE: ${{ secrets.GPG_RPM_PASSPHRASE }} SKIP_PUBLISH: ${{ env.SKIP_PUBLISH }} SKIP_DOCKER_PUSH: ${{ env.SKIP_DOCKER_PUSH }} - name: Verify RPM signatures diff --git a/.gitignore b/.gitignore index dd7eea76f..5c01f6e60 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,7 @@ management/server/types/testdata/ # generated by chglog in the release workflow, embedded into the RPM changelog.yml + +# generated by rpm-provides.sh, the config GoReleaser actually runs +.goreleaser.generated.yaml .chglog.yml diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 19528f88e..b6c563968 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -40,6 +40,32 @@ builds: tags: - load_wgnt_from_rsrc + # Single-arch builds: nfpm provides is not templated, so the RPM splits per arch. + - &netbird_rpm_build + id: netbird-rpm-amd64 + dir: client + binary: netbird + env: [CGO_ENABLED=0] + goos: [linux] + goarch: [amd64] + ldflags: + - -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser + mod_timestamp: "{{ .CommitTimestamp }}" + tags: + - load_wgnt_from_rsrc + + - <<: *netbird_rpm_build + id: netbird-rpm-arm64 + goarch: [arm64] + + - <<: *netbird_rpm_build + id: netbird-rpm-arm + goarch: [arm] + + - <<: *netbird_rpm_build + id: netbird-rpm-386 + goarch: [386] + - id: netbird-static dir: client binary: netbird @@ -223,17 +249,22 @@ nfpms: postinstall: "release_files/post_install.sh" preremove: "release_files/pre_remove.sh" - - maintainer: Netbird + - &netbird_rpm + maintainer: Netbird description: Netbird client. homepage: https://netbird.io/ license: BSD-3-Clause vendor: NetBird - id: netbird_rpm + id: netbird_rpm_amd64 bindir: /usr/bin - builds: - - netbird + ids: + - netbird-rpm-amd64 formats: - rpm + # Red Hat certification (RPM Version Handling) requires rpmbuild's ISA + # provide, which nfpm does not emit. The version is filled in by the release job. + provides: + - "netbird(x86-64) = @RPM_EVR@" # The client verifies TLS to management and signal against the system trust # store. Red Hat software certification (RPM Dependency Tracking) also # rejects packages that declare no dependencies at all. @@ -263,6 +294,27 @@ nfpms: packager: NetBird signature: key_file: '{{ if index .Env "GPG_RPM_KEY_FILE" }}{{ .Env.GPG_RPM_KEY_FILE }}{{ end }}' + + - <<: *netbird_rpm + id: netbird_rpm_arm64 + ids: + - netbird-rpm-arm64 + provides: + - "netbird(aarch-64) = @RPM_EVR@" + + - <<: *netbird_rpm + id: netbird_rpm_arm + ids: + - netbird-rpm-arm + provides: + - "netbird(armv6hl-32) = @RPM_EVR@" + + - <<: *netbird_rpm + id: netbird_rpm_386 + ids: + - netbird-rpm-386 + provides: + - "netbird(x86-32) = @RPM_EVR@" dockers_v2: - id: netbird disable: "{{ .Env.SKIP_DOCKER_PUSH }}" @@ -513,7 +565,10 @@ uploads: - name: yum skip: "{{ .Env.SKIP_PUBLISH }}" ids: - - netbird_rpm + - netbird_rpm_amd64 + - netbird_rpm_arm64 + - netbird_rpm_arm + - netbird_rpm_386 mode: archive target: https://pkgs.wiretrustee.com/yum/{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }} username: dev@wiretrustee.com diff --git a/release_files/rpm-changelog.sh b/release_files/rpm-changelog.sh index 20af2d415..d9150399e 100755 --- a/release_files/rpm-changelog.sh +++ b/release_files/rpm-changelog.sh @@ -7,6 +7,12 @@ # template headings, review checklists, HTML comments and Co-authored-by # trailers. None of that belongs in a package on Red Hat's catalog, and it is # most of the changelog's size. Keep the subject line and drop the rest. +# +# chglog also records the bare tag as each entry's version, while nfpm writes +# that string into the changelog header verbatim and never appends the release. +# rpmlint then reports incoherent-version-in-changelog, because the entry reads +# 0.79.0 while the package is 0.79.0-1. Rewrite each version the way nfpm +# renders the package EVR. set -eu @@ -20,14 +26,29 @@ path = sys.argv[1] lines = open(path, encoding="utf-8").read().split("\n") NOTE = re.compile(r"^ note: (.*)$") +SEMVER = re.compile(r"^- semver: (.*)$") BLOCK = {"|", "|-", "|+", ">", ">-", ">+"} +# nfpm defaults the RPM release to 1 and the packaging sets no other value. +RELEASE = "1" + def quote(text): """Render text as a YAML single-quoted scalar.""" return " note: '{}'".format(text.replace("'", "''")) +def evr(version): + """Render a semver tag the way nfpm renders the package EVR.""" + version, _, metadata = version.partition("+") + core, _, prerelease = version.partition("-") + if prerelease: + core += "~" + prerelease.replace("-", "_") + if metadata: + core += "+" + metadata + return "{}-{}".format(core, RELEASE) + + def first_line_of_double_quoted(value): """Text of a double-quoted scalar up to its first \\n escape.""" out = [] @@ -52,6 +73,13 @@ seen = 0 i = 0 while i < len(lines): line = lines[i] + + m = SEMVER.match(line) + if m: + out.append("- semver: '{}'".format(evr(m.group(1)))) + i += 1 + continue + m = NOTE.match(line) if not m: out.append(line) @@ -107,4 +135,11 @@ if grep -nE '^ note: ".*\\n' changelog.yml; then exit 1 fi +# Every entry must carry the release, or rpmlint reports the changelog version +# as incoherent with the package again. +if grep -nE "^- semver: " changelog.yml | grep -vE -- "-[0-9]+'$"; then + echo "changelog entries without the RPM release survived the rewrite" >&2 + exit 1 +fi + test -s changelog.yml diff --git a/release_files/rpm-provides.sh b/release_files/rpm-provides.sh new file mode 100644 index 000000000..b1332c18b --- /dev/null +++ b/release_files/rpm-provides.sh @@ -0,0 +1,46 @@ +#!/bin/sh +# +# Write .goreleaser.generated.yaml with the @RPM_EVR@ placeholder filled in. +# +# Red Hat certification (RPM Version Handling) expects rpmbuild's ISA provide, +# netbird(x86-64) = . nfpm does not emit it and GoReleaser does not template +# the provides field, so the version is substituted before GoReleaser runs. +# +# The value has to match what nfpm derives from the same tag: a semver +# prerelease becomes a tilde suffix, and the release defaults to 1. + +set -eu + +OUT=.goreleaser.generated.yaml + +TAG="${GITHUB_REF#refs/tags/}" +case "$TAG" in +v*) ;; +*) TAG=$(git describe --tags --abbrev=0) ;; +esac + +EVR=$(python3 - "$TAG" <<'PYEOF' +import sys + +version = sys.argv[1].lstrip("v") +version, _, metadata = version.partition("+") +core, _, prerelease = version.partition("-") +if prerelease: + core += "~" + prerelease.replace("-", "_") +if metadata: + core += "+" + metadata +print("{}-1".format(core)) +PYEOF +) + +# Written to a separate, ignored file: GoReleaser refuses to release from a +# dirty tree, so .goreleaser.yaml itself must stay untouched. +sed "s/@RPM_EVR@/${EVR}/g" .goreleaser.yaml > "$OUT" + +# A surviving placeholder means the provides entries moved or were renamed. +if grep -n "@RPM_EVR@" "$OUT"; then + echo "unsubstituted @RPM_EVR@ left in $OUT" >&2 + exit 1 +fi + +echo "rpm provides version: ${EVR} -> ${OUT}"