From 2e914fc2e487cd576843b97a3cb04a80e814393e Mon Sep 17 00:00:00 2001 From: Bjordis Collaku Date: Wed, 2 Sep 2026 14:04:33 -0700 Subject: [PATCH 1/3] feat(ci): append a local version suffix to resolute-qcom-devel builds Packages built from resolute-qcom-devel currently carry the same version as whatever Canonical tag they were last rebased onto, regardless of how many Qualcomm commits have landed on top since. A build with five Qualcomm patches and a build with eight are versioned identically, so the version string alone cannot distinguish them, and apt/dpkg have no way to see a newer patched build as an upgrade over an older one. - Adds scripts/apply-local-version-suffix.sh, which appends +qcom.g to the topmost debian/changelog entry, where N is the commit count past the last Canonical sync tag and is the short commit hash. N sorts numerically ahead of the SHA, so ordering between successive builds is always correct, and the leading + sorts above the bare Canonical version per Debian Policy's convention for local modifications. - Only applies to resolute-qcom-devel builds with commits past the last sync tag. Skipped for resolute-qcom mirror builds and exact kernel_version-pinned builds, which must stay byte-identical to the real Canonical upload, and skipped when HEAD is exactly a sync tag. - Finds the nearest Ubuntu-qcom-* tag through the GitHub Compare API rather than local git tags: the checkout is shallow and never carries tag refs, deepening commit history alone does not fetch tags, and this repository mirrors Ubuntu's full upstream history, so a blanket "git fetch --tags" is unsafe on every build. Verified empirically: a real depth-1 clone fails git describe outright ("No names found"), while the Compare API gives an identical, correct answer with no local tag fetch at all. - Never fails the kernel build itself. The script writes a full replacement changelog to a temp file and swaps it in with mv at the end, so a failure partway through leaves the real changelog untouched, and the calling workflow step downgrades any non-zero exit to a warning. - Adds a "Checkout CI scripts" step, its own path (matching kernel-src and docker-pkg-build, since checking out to the workspace root would clean the whole workspace and delete kernel-src), pinned to main: this repository's own CI scripts must always come from the trusted default branch, never from whatever ref is being built, including when this workflow is called via workflow_call from premerge-pr.yml, where github.sha reflects the caller's resolute-qcom-devel merge commit rather than main. - Sets persist-credentials: false on this and the two other checkout steps in this file (kernel source, docker-pkg-build), per zizmor's credential-persistence finding. Neither needs the checkout token afterward: the devel-PR merge step already authenticates its own git remote independently. - Surfaces the resolved version in the existing build summary table. Verified against the real repository before writing any workflow change: constructed the actual changelog prepend against the real fetched debian.qcom/changelog content and confirmed dpkg-parsechangelog parses the full result cleanly, including the untouched older entry beneath it; confirmed abi_release and uname -r are unaffected, since both derive only from the portion of the version before the appended suffix; confirmed worst-case filename length stays well under filesystem limits; and confirmed no script in qcom-distro-images parses package versions via a fragile filename regex that this could break, since all of it goes through dpkg-deb metadata instead. Signed-off-by: Bjordis Collaku --- .github/workflows/build-kernel.yml | 32 ++++++ scripts/apply-local-version-suffix.sh | 135 ++++++++++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100755 scripts/apply-local-version-suffix.sh diff --git a/.github/workflows/build-kernel.yml b/.github/workflows/build-kernel.yml index 829abe35e4b21..3fc1207580d0d 100644 --- a/.github/workflows/build-kernel.yml +++ b/.github/workflows/build-kernel.yml @@ -207,6 +207,7 @@ jobs: with: ref: ${{ inputs.ref != '' && inputs.ref || (inputs.kernel_version != '' && format('{0}-{1}', env.UPSTREAM_PREFIX, inputs.kernel_version) || inputs.suite || 'resolute-qcom-devel') }} path: kernel-src + persist-credentials: false # ----------------------------------------------------------------------- # 4b. Merge additional PRs against resolute-qcom-devel into the @@ -283,6 +284,35 @@ jobs: echo "::endgroup::" done + # ----------------------------------------------------------------------- + # 4c. Local version suffix for resolute-qcom-devel builds with commits + # past the last Canonical sync tag, so they're distinguishable from + # a plain rebuild of that tag. Skipped for resolute-qcom mirror and + # kernel_version-pinned builds. Never fails the kernel build itself. + # + # Checked out at main: CI scripts must come from the trusted + # default branch, not whatever ref is being built. Matters most + # for workflow_call from premerge-pr.yml, where github.sha is the + # caller's resolute-qcom-devel merge commit, not main. + # ----------------------------------------------------------------------- + - name: Checkout CI scripts + uses: actions/checkout@v6 + with: + ref: main + # Own path, matching kernel-src and docker-pkg-build: checking out + # to the workspace root would clean the whole workspace, deleting + # kernel-src. + path: ci-scripts + persist-credentials: false + + - name: Apply Qualcomm local version suffix + env: + KERNEL_VERSION: ${{ inputs.kernel_version }} + GH_TOKEN: ${{ github.token }} + run: | + bash ./ci-scripts/scripts/apply-local-version-suffix.sh || \ + echo "::warning::Local version suffix step failed; proceeding without it." + # ----------------------------------------------------------------------- # 5. Checkout docker-pkg-build # ----------------------------------------------------------------------- @@ -292,6 +322,7 @@ jobs: repository: qualcomm-linux/docker-pkg-build ref: main path: docker-pkg-build + persist-credentials: false # ----------------------------------------------------------------------- # 6. Build the base-suite-matched pkg-builder docker image. @@ -480,6 +511,7 @@ jobs: echo "| Architecture | \`${ARCH}\` |" echo "| Flavour | \`${FLAVOURS}\` |" echo "| Dbgsym | \`${DBGSYM}\` |" + echo "| Kernel version | \`${KERNEL_LOCAL_VERSION:-unmodified}\` |" echo "| Container | \`ghcr.io/qualcomm-linux/pkg-builder:${BASE_SUITE}\` |" echo "| Runner | \`${RUNNER_NAME}\` |" if [ -n "${DEVEL_PRS}" ]; then diff --git a/scripts/apply-local-version-suffix.sh b/scripts/apply-local-version-suffix.sh new file mode 100755 index 0000000000000..a27cc4248189b --- /dev/null +++ b/scripts/apply-local-version-suffix.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause +# +# Appends a local Debian version suffix (+qcom.) to the topmost +# debian/changelog entry for resolute-qcom-devel builds that carry commits +# past the last Canonical sync tag, so the package version differs from a +# plain rebuild of that tag. N is the commit count past the tag and sorts +# numerically, so later builds always compare as newer. Skipped for +# resolute-qcom mirror builds and kernel_version-pinned builds, which must +# stay byte-identical to Canonical. Nothing here is ever committed to git. +# +# Uses the GitHub Compare API to find the nearest Ubuntu-qcom-* tag rather +# than local git tags: the checkout is shallow, deepening history alone +# doesn't fetch tags, and this repo's full tag namespace is too large to +# fetch blindly on every build. +# +# Never fails the build itself. Writes a full replacement changelog to a +# temp file and swaps it in with mv at the end, so a failure partway +# through never leaves debian/changelog partially written; the calling +# workflow step also downgrades any non-zero exit to a warning. +set -euo pipefail + +: "${SUITE:?SUITE is required}" +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" +KERNEL_VERSION="${KERNEL_VERSION:-}" + +emit_kernel_version() { + echo "KERNEL_LOCAL_VERSION=$1" >> "$GITHUB_ENV" +} + +if [[ "$SUITE" != "resolute-qcom-devel" || -n "$KERNEL_VERSION" ]]; then + echo "Not a resolute-qcom-devel development build (suite=${SUITE}, kernel_version=${KERNEL_VERSION:-}); no local version suffix needed." + emit_kernel_version "unmodified" + exit 0 +fi + +cd kernel-src/ + +DEBIAN_DIR="$(awk -F= '($1 == "DEBIAN") { print $2 }' debian/debian.env 2>/dev/null || true)" +if [[ -z "$DEBIAN_DIR" ]]; then + echo "::warning::Could not resolve DEBIAN directory from debian/debian.env; skipping local version suffix." + emit_kernel_version "unmodified" + exit 0 +fi + +CHANGELOG="${DEBIAN_DIR}/changelog" +if [[ ! -f "$CHANGELOG" ]]; then + echo "::warning::${CHANGELOG} not found; skipping local version suffix." + emit_kernel_version "unmodified" + exit 0 +fi + +HEAD_SHA="$(git rev-parse HEAD)" +SHORT_SHA="$(git rev-parse --short=12 HEAD)" + +if [[ ! "$HEAD_SHA" =~ ^[0-9a-f]{40}$ ]] || [[ ! "$SHORT_SHA" =~ ^[0-9a-f]{12}$ ]]; then + echo "::warning::Unexpected HEAD SHA format; skipping local version suffix." + emit_kernel_version "unmodified" + exit 0 +fi + +if ! TAGS_RAW="$( + gh api --paginate "repos/${GITHUB_REPOSITORY}/tags" \ + -X GET -f per_page=100 \ + --jq '[.[] | select(.name | test("^Ubuntu-qcom-[0-9]+\\.[0-9]+\\.[0-9]+-[0-9]+\\.[0-9]+$")) | .name]' \ + 2>&1 +)"; then + echo "::warning::Unable to list Ubuntu-qcom-* tags; skipping local version suffix. ${TAGS_RAW}" + emit_kernel_version "unmodified" + exit 0 +fi + +TAGS_JSON="$(jq -s 'add // []' <<< "$TAGS_RAW")" +if [[ "$(jq 'length' <<< "$TAGS_JSON")" == "0" ]]; then + echo "::warning::No Ubuntu-qcom-* tags returned; skipping local version suffix." + emit_kernel_version "unmodified" + exit 0 +fi + +# Find the tag with the smallest ahead_by among all tags that are actual +# ancestors of HEAD (status "identical" or "ahead"). That is the nearest +# sync point, independent of whatever order the API happens to list tags in. +BEST_AHEAD="" +BEST_TAG="" +LAST_COMPARE_ERROR="" +for tag in $(jq -r '.[]' <<< "$TAGS_JSON"); do + if ! cmp="$(gh api "repos/${GITHUB_REPOSITORY}/compare/${tag}...${HEAD_SHA}" \ + --jq '{status, ahead_by} | @json' 2>&1)"; then + LAST_COMPARE_ERROR="$cmp" + continue + fi + status="$(jq -r '.status' <<< "$cmp")" + ahead="$(jq -r '.ahead_by' <<< "$cmp")" + [[ "$status" == "identical" || "$status" == "ahead" ]] || continue + [[ "$ahead" =~ ^[0-9]+$ ]] || continue + if [[ -z "$BEST_AHEAD" || "$ahead" -lt "$BEST_AHEAD" ]]; then + BEST_AHEAD="$ahead" + BEST_TAG="$tag" + fi +done + +if [[ -z "$BEST_AHEAD" ]]; then + echo "::warning::No Ubuntu-qcom-* tag found as an ancestor of ${HEAD_SHA}; skipping local version suffix. ${LAST_COMPARE_ERROR}" + emit_kernel_version "unmodified" + exit 0 +fi + +if [[ "$BEST_AHEAD" == "0" ]]; then + echo "HEAD is exactly ${BEST_TAG}; pure Canonical content, no local version suffix needed." + emit_kernel_version "unmodified" + exit 0 +fi + +PACKAGE="$(dpkg-parsechangelog -l "$CHANGELOG" -SSource)" +BASE_VERSION="$(dpkg-parsechangelog -l "$CHANGELOG" -SVersion)" +DISTRIBUTION="$(dpkg-parsechangelog -l "$CHANGELOG" -SDistribution)" +NEW_VERSION="${BASE_VERSION}+qcom${BEST_AHEAD}.${SHORT_SHA}" + +TMP_CHANGELOG="$(mktemp)" +{ + printf '%s (%s) %s; urgency=medium\n\n' "$PACKAGE" "$NEW_VERSION" "$DISTRIBUTION" + printf ' * Qualcomm downstream build: %s commit(s) past %s.\n\n' "$BEST_AHEAD" "$BEST_TAG" + printf ' -- Qualcomm Linux CI %s\n\n' "$(LC_ALL=C date -R)" + cat "$CHANGELOG" +} > "$TMP_CHANGELOG" + +# The rewritten file must parse cleanly before it ever replaces the real +# changelog; a malformed entry must never reach the actual build. +dpkg-parsechangelog -l "$TMP_CHANGELOG" -SVersion >/dev/null + +mv "$TMP_CHANGELOG" "$CHANGELOG" + +echo "Applied local version suffix: ${BASE_VERSION} -> ${NEW_VERSION} (${BEST_AHEAD} commit(s) past ${BEST_TAG})" +emit_kernel_version "$NEW_VERSION" From 13301f1e8660a33a7b70f19dc2d0b1bd73070119 Mon Sep 17 00:00:00 2001 From: Bjordis Collaku Date: Tue, 8 Sep 2026 09:56:04 -0700 Subject: [PATCH 2/3] ci(local-version): preflight the host tools the suffix step needs The suffix step ran green on every build while silently doing nothing. Build 33690758951 shows why: apply-local-version-suffix.sh: line 68: gh: command not found gh is not installed on the self-hosted runner. The existing handler caught it, warned, emitted KERNEL_LOCAL_VERSION=unmodified and exited 0, so the step passed and an hour-long build shipped the plain Canonical version. gh was only the first missing tool. jq and dpkg-parsechangelog run on the runner host as well, not inside the build container, so the container's dpkg-dev does not cover them. dpkg-parsechangelog is reached after every guard in this script, where a failure aborts under set -euo pipefail and produces the same silent "unmodified" outcome from a different cause. Check all three up front and name every missing one in a single warning, so a runner that is short a tool costs one line of log rather than one build per tool. The check sits after the suite early-exit, so builds that skip the suffix entirely still need nothing installed. This does not by itself install anything; build-kernel.yml has to provide gh and dpkg-dev on the runner. Signed-off-by: Bjordis Collaku --- scripts/apply-local-version-suffix.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/scripts/apply-local-version-suffix.sh b/scripts/apply-local-version-suffix.sh index a27cc4248189b..14d429cb63be7 100755 --- a/scripts/apply-local-version-suffix.sh +++ b/scripts/apply-local-version-suffix.sh @@ -35,6 +35,21 @@ if [[ "$SUITE" != "resolute-qcom-devel" || -n "$KERNEL_VERSION" ]]; then exit 0 fi +# Report every missing host tool at once rather than one build at a time. All +# three run on the runner host, not inside the build container, so they have to +# be installed by the workflow; the container's dpkg-dev does not help here. A +# build costs over an hour, and without this the first missing tool aborts the +# script mid-way and looks identical to a clean skip. +MISSING_TOOLS=() +for tool in gh jq dpkg-parsechangelog; do + command -v "$tool" >/dev/null 2>&1 || MISSING_TOOLS+=("$tool") +done +if (( ${#MISSING_TOOLS[@]} > 0 )); then + echo "::warning::Missing required tool(s) on the runner: ${MISSING_TOOLS[*]}; skipping local version suffix." + emit_kernel_version "unmodified" + exit 0 +fi + cd kernel-src/ DEBIAN_DIR="$(awk -F= '($1 == "DEBIAN") { print $2 }' debian/debian.env 2>/dev/null || true)" From 59985041ceebafaa646b8bc427b6adf9586d6268 Mon Sep 17 00:00:00 2001 From: Bjordis Collaku Date: Tue, 8 Sep 2026 10:09:54 -0700 Subject: [PATCH 3/3] ci(local-version): install the host tools the suffix step needs Completes the preflight check in the prior commit: gh, jq, and dpkg-dev (for dpkg-parsechangelog) are not present on the self-hosted runner host, only inside the build container, which the local version suffix step never touches. Installs all three before that step runs, matching the existing "Install GitHub CLI" pattern already used in premerge-distro-validation.yml. Signed-off-by: Bjordis Collaku --- .github/workflows/build-kernel.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/build-kernel.yml b/.github/workflows/build-kernel.yml index 3fc1207580d0d..f85fdf7caef44 100644 --- a/.github/workflows/build-kernel.yml +++ b/.github/workflows/build-kernel.yml @@ -305,6 +305,12 @@ jobs: path: ci-scripts persist-credentials: false + - name: Install host tools for the local version suffix step + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y --no-install-recommends gh jq dpkg-dev + - name: Apply Qualcomm local version suffix env: KERNEL_VERSION: ${{ inputs.kernel_version }}