Version release-candidate .ipk packages so opkg sorts them below the release

A release candidate is tagged vX.Y.Z-rcN, and the OpenWrt workflow
wrote that tag unchanged as the .ipk control Version. opkg compares
versions with dpkg's algorithm and reads the text after the last '-' as
a revision, so v0.5.3-rc1 sorted above v0.5.3 and a router that had
installed the candidate was never upgraded by the release.

The "Derive package version" step now also emits ipk_version, which
maps a candidate tag's -alphaN/-betaN/-preN/-rcN suffix to '~' (the
same mapping the .deb uses), giving v0.5.3~rc1. The leading 'v' is
kept: every released .ipk carries it, and under opkg 0.5.4 would sort
below v0.5.3, so dropping it would stop future releases upgrading.
Release tags and branch builds are unchanged.

build-ipk.sh takes the control Version from IPK_VERSION, falling back
to PKG_VERSION, and still names the file from PKG_VERSION so the asset
name keeps the tag's -rcN (a GitHub release renames assets with '~').

check-package-versions.sh gains an .ipk section that runs the step's
text for a release tag, an rc and a beta candidate tag and a branch,
checks the ordering against v0.5.2, v0.5.3 and v0.5.4 and of the beta
below the rc, and checks the job output and build step wiring.
package-test.sh builds the .ipk with IPK_VERSION removed from the
build's environment and with it set, and reads the control Version
back. It removes the first package before the second build and records
a missing file as a failed check, so a build-ipk.sh that named its file
from IPK_VERSION cannot pass by reading the first build's package, and
its exit trap also removes a package whose name carries the '~' form.
This commit is contained in:
Johnathan Corgan
2026-10-01 22:41:14 +00:00
parent 86fdb99890
commit 7671f0d59a
4 changed files with 131 additions and 5 deletions
+15
View File
@@ -25,6 +25,7 @@ jobs:
outputs: outputs:
package_version: ${{ steps.version.outputs.package_version }} package_version: ${{ steps.version.outputs.package_version }}
apk_version: ${{ steps.version.outputs.apk_version }} apk_version: ${{ steps.version.outputs.apk_version }}
ipk_version: ${{ steps.version.outputs.ipk_version }}
release_channel: ${{ steps.channel.outputs.release_channel }} release_channel: ${{ steps.channel.outputs.release_channel }}
steps: steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
@@ -41,15 +42,28 @@ jobs:
# in the .apk metadata. apk_version is built directly from the same # in the .apk metadata. apk_version is built directly from the same
# structured inputs (tag, or commit height) — no reparse of the # structured inputs (tag, or commit height) — no reparse of the
# flattened package_version. See packaging/openwrt-apk/apk-version.sh. # flattened package_version. See packaging/openwrt-apk/apk-version.sh.
#
# ipk_version is the .ipk control Version. opkg compares versions as
# dpkg does, so it reads a tag's vX.Y.Z-rcN as revision rcN and sorts
# it above the release; vX.Y.Z~rcN sorts below it. git refuses '~' in
# a ref name, so the tag carries '-' and this maps it. The leading 'v'
# stays: every released .ipk carries it, and under opkg 0.5.4 sorts
# below v0.5.3. testing/check-package-versions.sh runs this step's text.
if [[ "$GITHUB_REF" == refs/tags/* ]]; then if [[ "$GITHUB_REF" == refs/tags/* ]]; then
echo "package_version=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" echo "package_version=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
echo "apk_version=$(sh packaging/openwrt-apk/apk-version.sh tag "${GITHUB_REF_NAME}")" >> "$GITHUB_OUTPUT" echo "apk_version=$(sh packaging/openwrt-apk/apk-version.sh tag "${GITHUB_REF_NAME}")" >> "$GITHUB_OUTPUT"
IPK_VERSION="${GITHUB_REF_NAME}"
if [[ "$GITHUB_REF_NAME" =~ ^(v[0-9]+\.[0-9]+\.[0-9]+)-((alpha|beta|pre|rc)[0-9]*)$ ]]; then
IPK_VERSION="${BASH_REMATCH[1]}~${BASH_REMATCH[2]}"
fi
echo "ipk_version=${IPK_VERSION}" >> "$GITHUB_OUTPUT"
else else
BRANCH=$(echo "$GITHUB_REF_NAME" | sed 's|/|-|g') BRANCH=$(echo "$GITHUB_REF_NAME" | sed 's|/|-|g')
HEIGHT=$(git rev-list --count HEAD) HEIGHT=$(git rev-list --count HEAD)
HASH=$(git rev-parse --short HEAD) HASH=$(git rev-parse --short HEAD)
echo "package_version=${BRANCH}.${HEIGHT}.${HASH}" >> "$GITHUB_OUTPUT" echo "package_version=${BRANCH}.${HEIGHT}.${HASH}" >> "$GITHUB_OUTPUT"
echo "apk_version=$(sh packaging/openwrt-apk/apk-version.sh dev "${HEIGHT}")" >> "$GITHUB_OUTPUT" echo "apk_version=$(sh packaging/openwrt-apk/apk-version.sh dev "${HEIGHT}")" >> "$GITHUB_OUTPUT"
echo "ipk_version=${BRANCH}.${HEIGHT}.${HASH}" >> "$GITHUB_OUTPUT"
fi fi
- name: Determine release channel - name: Determine release channel
@@ -332,6 +346,7 @@ jobs:
- name: Build .ipk - name: Build .ipk
env: env:
PKG_VERSION: ${{ needs.determine-versioning.outputs.package_version }} PKG_VERSION: ${{ needs.determine-versioning.outputs.package_version }}
IPK_VERSION: ${{ needs.determine-versioning.outputs.ipk_version }}
run: ./packaging/openwrt-ipk/build-ipk.sh --arch ${{ matrix.build_arch }} --bin-dir "$GITHUB_WORKSPACE/bins" run: ./packaging/openwrt-ipk/build-ipk.sh --arch ${{ matrix.build_arch }} --bin-dir "$GITHUB_WORKSPACE/bins"
- name: Install shellcheck (if missing) - name: Install shellcheck (if missing)
+13 -2
View File
@@ -14,7 +14,13 @@
# arm 32-bit ARM routers (Cortex-A7) # arm 32-bit ARM routers (Cortex-A7)
# x86_64 x86 routers / VMs # x86_64 x86 routers / VMs
# #
# Output: dist/fips_<version>_<openwrt-arch>.ipk # Output: dist/fips_<PKG_VERSION>_<openwrt-arch>.ipk
#
# Environment:
# PKG_VERSION label for the file name (default: git describe)
# IPK_VERSION control file Version (default: PKG_VERSION). CI passes a
# release candidate as vX.Y.Z~rcN, which opkg sorts below the
# release, while PKG_VERSION keeps the tag's vX.Y.Z-rcN.
# #
# Prerequisites: # Prerequisites:
# cargo install cargo-zigbuild # cargo install cargo-zigbuild
@@ -86,8 +92,13 @@ DIST_DIR="$PROJECT_ROOT/dist"
PKG_NAME="fips" PKG_NAME="fips"
PKG_VERSION="${PKG_VERSION:-$(cd "$PROJECT_ROOT" && git describe --tags --always --dirty 2>/dev/null || echo "0.1.0")}" PKG_VERSION="${PKG_VERSION:-$(cd "$PROJECT_ROOT" && git describe --tags --always --dirty 2>/dev/null || echo "0.1.0")}"
IPK_VERSION="${IPK_VERSION:-$PKG_VERSION}"
if [ "$IPK_VERSION" = "$PKG_VERSION" ]; then
echo "==> Building $PKG_NAME $PKG_VERSION for $OPENWRT_ARCH ($RUST_TARGET)" echo "==> Building $PKG_NAME $PKG_VERSION for $OPENWRT_ARCH ($RUST_TARGET)"
else
echo "==> Building $PKG_NAME $PKG_VERSION (control Version $IPK_VERSION) for $OPENWRT_ARCH ($RUST_TARGET)"
fi
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 1. Obtain binaries # 1. Obtain binaries
@@ -192,7 +203,7 @@ PKG_SIZE=$(du -sk "$DATA_DIR" | cut -f1)
cat > "$CONTROL_DIR/control" <<EOF cat > "$CONTROL_DIR/control" <<EOF
Package: $PKG_NAME Package: $PKG_NAME
Version: $PKG_VERSION Version: $IPK_VERSION
Architecture: $OPENWRT_ARCH Architecture: $OPENWRT_ARCH
Maintainer: FIPS Network Maintainer: FIPS Network
Section: net Section: net
+60
View File
@@ -23,6 +23,26 @@
# would leave checksums-linux.txt naming a file the release does not have). # would leave checksums-linux.txt naming a file the release does not have).
# Those three are read from the text, not executed. # Those three are read from the text, not executed.
# #
# OpenWrt's opkg compares versions with dpkg's algorithm (libopkg/pkg.c
# verrevcmp in openwrt/opkg-lede, read at 80503d94) and splits a revision at
# the last '-', so the .ipk has the same defect. package-openwrt.yml's "Derive
# package version" step maps the tag for the .ipk control Version, keeping the
# leading 'v' every released .ipk carries: under opkg, 0.5.4 sorts below
# v0.5.3, so dropping it would stop releases upgrading. dpkg stands in for
# opkg as the ordering oracle; it warns about the 'v' and still compares.
#
# ipk cases:
# refs/tags/v0.5.3 package_version and ipk_version v0.5.3
# refs/tags/v0.5.3-rc1 ipk_version v0.5.3~rc1, package_version (the file
# label) v0.5.3-rc1, and v0.5.2 < v0.5.3~rc1 < v0.5.3
# < v0.5.4
# refs/tags/v0.5.3-beta1 ipk_version v0.5.3~beta1, which sorts between
# v0.5.2 and v0.5.3~rc1
# refs/heads/maint ipk_version equals package_version
# and the wiring: the job declares ipk_version, and the "Build .ipk" step
# passes it as IPK_VERSION. build-ipk.sh's use of IPK_VERSION is executed by
# testing/openwrt/package-test.sh.
#
# Exit 0 = clean. Exit 1 = a case or a wiring check failed, including a # Exit 0 = clean. Exit 1 = a case or a wiring check failed, including a
# derivation that ran but did not write a declared output. Exit 2 = the check # derivation that ran but did not write a declared output. Exit 2 = the check
# could not run (no dpkg, no PyYAML, a step not found, the derivation failed, # could not run (no dpkg, no PyYAML, a step not found, the derivation failed,
@@ -200,6 +220,46 @@ expect_text "$WORK/deb.build.sh" \
expect_text "$WORK/deb.build.sh" "tr '~' '-'" \ expect_text "$WORK/deb.build.sh" "tr '~' '-'" \
"the .deb build renames a '~' in the package file name" "the .deb build renames a '~' in the package file name"
# ── OpenWrt .ipk, package-openwrt.yml ─────────────────────────────────────
extract ipk package-openwrt.yml determine-versioning "Derive package version" \
build "Build .ipk"
echo "ipk: release tag refs/tags/v0.5.3"
derive ipk refs/tags/v0.5.3 package_version ipk_version
expect_eq "package_version" "${OUT[package_version]}" "v0.5.3"
expect_eq "ipk_version" "${OUT[ipk_version]}" "v0.5.3"
echo "ipk: candidate tag refs/tags/v0.5.3-rc1"
derive ipk refs/tags/v0.5.3-rc1 package_version ipk_version
expect_eq "package_version" "${OUT[package_version]}" "v0.5.3-rc1"
expect_eq "ipk_version" "${OUT[ipk_version]}" "v0.5.3~rc1"
expect_order "${OUT[ipk_version]}" lt v0.5.3
expect_order "${OUT[ipk_version]}" gt v0.5.2
expect_order v0.5.4 gt "${OUT[ipk_version]}"
RC1_IPK="${OUT[ipk_version]}"
echo "ipk: candidate tag refs/tags/v0.5.3-beta1"
derive ipk refs/tags/v0.5.3-beta1 package_version ipk_version
expect_eq "package_version" "${OUT[package_version]}" "v0.5.3-beta1"
expect_eq "ipk_version" "${OUT[ipk_version]}" "v0.5.3~beta1"
expect_order "${OUT[ipk_version]}" lt "$RC1_IPK"
expect_order "${OUT[ipk_version]}" gt v0.5.2
echo "ipk: branch refs/heads/maint"
derive ipk refs/heads/maint package_version ipk_version
expect_eq "package_version" "${OUT[package_version]}" "maint.${HEIGHT}.${HASH}"
expect_eq "ipk_version" "${OUT[ipk_version]}" "${OUT[package_version]}"
echo "ipk: wiring"
# shellcheck disable=SC2016 # the ${{ }} expressions are workflow text, not shell
expect_text -x "$WORK/ipk.outputs" \
'ipk_version=${{ steps.version.outputs.ipk_version }}' \
"determine-versioning declares ipk_version from the derivation step"
# shellcheck disable=SC2016
expect_text -x "$WORK/ipk.build.sh" \
'IPK_VERSION: ${{ needs.determine-versioning.outputs.ipk_version }}' \
"the .ipk build passes ipk_version as IPK_VERSION"
echo echo
if [ "$FAILED" -eq 0 ]; then if [ "$FAILED" -eq 0 ]; then
echo "check-package-versions: all checks passed" echo "check-package-versions: all checks passed"
+42 -2
View File
@@ -47,7 +47,7 @@ fi
PKG_VERSION="pkgtest.$$" PKG_VERSION="pkgtest.$$"
TMP="$(mktemp -d)" || { echo "package-test: mktemp failed" >&2; exit 2; } TMP="$(mktemp -d)" || { echo "package-test: mktemp failed" >&2; exit 2; }
trap 'rm -rf "$TMP"; rm -f "$PROJECT_ROOT/dist/fips_${PKG_VERSION}_"*' EXIT trap 'rm -rf "$TMP"; rm -f "$PROJECT_ROOT/dist/fips_${PKG_VERSION}"[_~]*' EXIT
harness_fail() { harness_fail() {
echo "package-test: $*" >&2 echo "package-test: $*" >&2
@@ -227,7 +227,9 @@ done
# ── Build the .ipk ────────────────────────────────────────────────────────── # ── Build the .ipk ──────────────────────────────────────────────────────────
echo "==> build-ipk.sh" echo "==> build-ipk.sh"
if ! PKG_VERSION="$PKG_VERSION" \ # The first build is V1's "IPK_VERSION unset" case, so it must not inherit an
# IPK_VERSION from the caller's environment.
if ! env -u IPK_VERSION PKG_VERSION="$PKG_VERSION" \
bash "$PROJECT_ROOT/packaging/openwrt-ipk/build-ipk.sh" --arch x86_64 --bin-dir "$BINS" \ bash "$PROJECT_ROOT/packaging/openwrt-ipk/build-ipk.sh" --arch x86_64 --bin-dir "$BINS" \
> "$TMP/build-ipk.log" 2>&1; then > "$TMP/build-ipk.log" 2>&1; then
cat "$TMP/build-ipk.log" >&2 cat "$TMP/build-ipk.log" >&2
@@ -240,6 +242,44 @@ tar -xzf "$IPK" -O ./data.tar.gz | tar -tzf - > "$TMP/ipk-data" \
tar -xzf "$IPK" -O ./control.tar.gz | tar -tzf - > "$TMP/ipk-control" \ tar -xzf "$IPK" -O ./control.tar.gz | tar -tzf - > "$TMP/ipk-control" \
|| harness_fail "cannot list control.tar.gz in $IPK" || harness_fail "cannot list control.tar.gz in $IPK"
# Print the Version field of an .ipk's control file.
ipk_version() {
tar -xzf "$1" -O ./control.tar.gz | tar -xzOf - ./control \
| sed -n 's/^Version: //p'
}
# ── V1 and V2. The control Version comes from IPK_VERSION ───────────────────
# The workflow passes a candidate's opkg-sortable version (vX.Y.Z~rcN) as
# IPK_VERSION and keeps the tag's form in PKG_VERSION for the file name, since
# a GitHub release renames an asset with '~' in its name. Unset, IPK_VERSION
# falls back to PKG_VERSION, as a local build expects.
got="$(ipk_version "$IPK")" || harness_fail "cannot read the control file in $IPK"
if [[ "$got" == "$PKG_VERSION" ]]; then
ok "V1 with IPK_VERSION unset the control Version is PKG_VERSION ($got)"
else
bad "V1 with IPK_VERSION unset the control Version is '$got', want $PKG_VERSION"
fi
# The first build's package is removed so a second build that names its file
# from anything but PKG_VERSION cannot be passed by reading the stale one.
RC_VERSION="${PKG_VERSION}~rc1"
rm -f "$IPK" || harness_fail "cannot remove $IPK before the second build"
if ! PKG_VERSION="$PKG_VERSION" IPK_VERSION="$RC_VERSION" \
bash "$PROJECT_ROOT/packaging/openwrt-ipk/build-ipk.sh" --arch x86_64 --bin-dir "$BINS" \
> "$TMP/build-ipk-rc.log" 2>&1; then
cat "$TMP/build-ipk-rc.log" >&2
harness_fail "build-ipk.sh failed with IPK_VERSION set"
fi
if [[ ! -f "$IPK" ]]; then
bad "V2 with IPK_VERSION set build-ipk.sh wrote no $IPK; the file name must come from PKG_VERSION"
else
got="$(ipk_version "$IPK")" || harness_fail "cannot read the control file in $IPK"
if [[ "$got" == "$RC_VERSION" ]]; then
ok "V2 with IPK_VERSION set the control Version is IPK_VERSION ($got), file named from PKG_VERSION"
else
bad "V2 with IPK_VERSION set the control Version is '$got', want $RC_VERSION"
fi
fi
# ── P1 and P2. Neither package ships the dnsmasq drop-in ──────────────────── # ── P1 and P2. Neither package ships the dnsmasq drop-in ────────────────────
# OpenWrt's dnsmasq builds its config from UCI and reads no directory under # OpenWrt's dnsmasq builds its config from UCI and reads no directory under
# /etc, so .fips forwarding comes from the UCI entry 90-fips-setup adds. The # /etc, so .fips forwarding comes from the UCI entry 90-fips-setup adds. The