ci: build every package from one source package - #113
Closed
Christopher Obbard (obbardc) wants to merge 53 commits into
Closed
Christopher Obbard (obbardc) wants to merge 53 commits into
Christopher Obbard (obbardc) wants to merge 53 commits into
Conversation
build-kernel.sh defaulted to the SSH clone URL while CI and the matrix use HTTPS, so a local build needed a GitHub key that a read-only clone does not. The same URL is what the changelog records as the source, so the default now matches what ci/build-matrix.yaml says. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
Three scripts derived LOCALVERSION from a tag, each its own way. CI used ci/scripts/derive-localversion.sh. prepare-source.sh carried a copy of the same rule. build-kernel.sh carried an older one that produced the pre-plus, SHA-less form (qcom-next-20260210), passed no snapshot, and so gave a local build a Debian version with no +git component at all -- a different version from the one CI gives the same commit. prepare-source.sh now calls derive-localversion.sh, with the exact tag or the branch HEAD sits on as the ref and HEAD's committer date for the branch-tip case, which is precisely what the prepare action feeds it. It takes a --flavour for the identity part of LOCALVERSION, defaulting to qcom-next, and build-kernel.sh passes that through and derives nothing of its own. A local build of a commit now produces the version CI would, which is the precondition for a source package built locally being interchangeable with one built in CI. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
--latest-tag picked the tag with the highest version sort, which puts an older-dated release candidate of a newer kernel above the newest snapshot. CI resolves by trailing date through ci/scripts/resolve-kernel-ref.sh, so a local build now calls the same script and lands on the same tag. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
The changelog names the repository and ref a build was cut from, but only CI passed them, so a local build's changelog read "Source: unknown unknown". The origin remote and the tag or branch HEAD answers to are the same facts, read from the checkout, so they are now the defaults for --git-clone and --git-ref. Naming the checkout once also serves the version derivation, which used the same tag-or-branch rule of its own. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
The changelog entry carried a date fixed in the template, so every build of every kernel was dated 14 November 2025. The date is now a prepare input, CHANGELOG_DATE, and prepare-source.sh fills it with HEAD's committer date: the same instant the snapshot comes from, so the entry is dated by the source it describes and two builds of one commit write one changelog. That date does more than read well. dpkg-source takes the debian tarball's mtimes from the changelog date and dpkg-buildpackage exports it as SOURCE_DATE_EPOCH, so dating the changelog from the commit is part of what makes the source package a function of the commit alone. Its git lookups now tolerate a checkout git refuses to read, and only run for a value the caller did not supply. A prepare run by hand without one gets today's date. CI passing CHANGELOG_DATE in explicitly, and why, is a separate change: the action that would read it does not exist yet here. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
Both build paths made the orig tarball with tar czf on a fresh clone, so every run wrote new mtimes and a new gzip header and the same upstream version got a different orig each time. Two suites sharing a source package and upstream version -- trixie and forky, differing only in Debian revision -- therefore could not share a pool, and a rerun could not reproduce what it had built. build-source-package.sh takes a tree prepare-source.sh has prepared and writes the .orig.tar.gz, .debian.tar.xz, .dsc and .changes into an output directory. The orig comes from git archive of the commit, whose entries carry the commit's timestamp and root ownership, through gzip -n, which writes no timestamp: two runs on one commit give one tarball, byte for byte. Verified here by building trixie and forky into one directory, where the second run rebuilt the orig and compared it with the first. The upstream version names the commit as ~g<sha>, so the script checks that the commit it archives is that one, and refuses a tree that differs from the commit outside debian/ -- dpkg-source would fold the difference into an automatic patch and the package would no longer describe the commit its version names. --write-fields hands the result to a caller as KEY=VALUE lines, for CI. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
--source-package stops after build-source-package.sh has written the .orig.tar.gz, .dsc and .changes into the output directory, and --dsc builds binaries from an existing source package with no clone and no prepare. Together they let a local build follow the path CI takes: source package first, binaries from it. Native mode unpacks the .dsc into the output directory and runs dpkg-buildpackage -b there, so the .deb files land where every other mode puts them. sbuild takes a .dsc as it stands. Docker mode cannot go through docker_deb_build.py, which builds from a tree only, so it runs sbuild in the same pkg-builder image with the same flags docker_deb_build.py uses, building the image through docker_deb_build.py --rebuild if it is absent. The default path from a tree is unchanged. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
Docker mode required docker_deb_build.py up front, because building from a tree goes through it. Building from a .dsc runs sbuild in the image directly and needs the script only to build the image when it is absent, so that path now looks for it without insisting, and says what to do if the image is missing and the script is too. A run from a .dsc also stops printing the tree-preparation settings it has no use for. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
debian/README.md now lists the two scripts beside build-kernel.sh, shows the source-package flow (--source-package, --dsc, and the two scripts run by hand), and says what makes the orig tarball reproducible and what the script refuses. It also stops claiming that build-kernel.sh derives LOCALVERSION from the tag and that debian/rules recovers it from a package name; neither has been true for a while. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
The delivery matrix described build legs indirectly. A row carried a list of suites that resolve-matrix.sh flattened into one leg per suite, and each leg's Debian revision was assembled at resolve time from a per-row debian_version_stub, a matrix-wide suite_suffix_mapping, and a suffix implied by the row's type. Reading the matrix told you which packages existed only after you had run the flattening and the revision formula in your head, and the row's debian_version_suffix field existed purely to write half of that formula down again so a validator could check the two agreed. Split the rows so that one entry is one generated package: a single kernel_variant, type and suite, with its debian_revision stated outright. suite_suffix_mapping and the stub/suffix pair are gone, and with them derive-debian-revision.sh, whose whole job was to apply the formula they encoded. There is nothing left to expand or derive at resolve time, so what an entry says is what gets built. The cost is duplication: nine entries repeat their variant's git_clone, srcpkg, binpkg and kernel_config. They are written out in full rather than sharing YAML anchors, so each entry can be read, grepped and changed on its own, and the resolver takes on the invariants that duplication puts at risk. A variant's entries must agree on srcpkg, binpkg and kernel_config, which decide what the package is; its entries of one type must agree on the kernel ref, so a release ref bump cannot skip a suite and quietly ship one suite a different kernel from its siblings; no two entries may build the same srcpkg at the same revision; and a suite's Daily revision must be its Release revision plus the trailing ~ that sorts it below. Move the matrix to YAML and rewrite the resolver in Python. The resolver was 372 lines whose validation was a single jq program built from elif chains, where each check had to fall through to the next and a new rule meant extending an expression rather than adding a function. The Python collects every problem in one pass and reports them together, so a bad matrix yields its full list of errors instead of the first one jq happened to reach. Both files drop out of the change naturally: the schema is being rewritten anyway, and YAML lets each entry carry the comment that explains it. Unknown fields are now rejected at both entry and root level. Anything left over from this schema change - a stray suites: or suite_suffix_mapping: - fails loudly rather than sitting in the file looking authoritative while nothing reads it. The dkms list moves across with the rest of the schema, validated by resolve-matrix.py the same way kernel_config is and joined into the same comma-separated workflow input. Every entry keeps the kgsl, camx and iris-vpu the row it came from carried, including the ones the Ubuntu override in build-kernel-deb.yml still trims at build time; moving that policy into the matrix is a separate change. Verified against the old resolver across all fifteen filter combinations the workflows can produce: the emitted JSON is identical, so every build leg keeps its inputs and its debian_revision. build-kernel-deb.yml's direct-dispatch path loses the mapping it used to consult, so its debian-version-stub input becomes debian-revision. Left empty it looks up the Daily entry for the variant and suite it was given and builds at the revision the daily build would have used. Dispatching a combination the matrix does not configure now fails instead of inventing a version for it; the error names the override that builds it ad hoc. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
Bring the READMEs onto the schema the previous commit introduced. The Matrix Model section documented suites, debian_version_stub and debian_version_suffix as fields and derive-debian-revision.sh as the single implementation of the revision formula, none of which exist now. Replace the field table with the flattened one, set out the invariants the resolver enforces across entries, and rewrite the maintenance instructions: adding a variant is now one entry per package it should produce rather than exactly two rows, and adding a suite no longer starts with a suite_suffix_mapping entry. The ordering discussion keeps its Daily-sorts-below-Release argument, which the trailing ~ still carries, but now attributes it to the revisions themselves rather than to a mapping. The example matrix becomes YAML and shows one Daily and one Release entry for the same suite, which is what the pairing rule now means. debian/README.md's pointer to the dkms field follows the file to its new name; that field is still unimplemented and is wired up separately. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
camx-dkms and iris-vpu-dkms are not packaged for Ubuntu, so build-kernel-deb.yml overwrote the resolved DKMS list with kgsl on Ubuntu-family legs. That was delivery policy sitting in workflow YAML, put there because a matrix row spanned several suites and could not say anything about one of them. A row no longer does: one entry is one generated package for one suite, so the resolute entry can carry its own dkms list and does. Revert the override, along with the "needs: resolve" and SUITE_FAMILY plumbing that fed it, and pass the resolved list to prepare-source.sh unchanged. dkms stops being a variant identity field. It was checked alongside srcpkg, binpkg and kernel_config, on the reasoning that those decide what the package is and only the destination varies. That holds for the others, which describe the kernel itself, but not for dkms: it names packages that have to exist in the target archive, so it is a property of the variant in a suite rather than of the variant. A variant's entries for one suite must still agree, which keeps the property worth having - a Daily that bundled a different module set from the Release it precedes would not be testing what ships - while letting the suites differ from each other. What each leg bundles is unchanged: the Debian suites get kgsl, camx and iris-vpu, resolute gets kgsl. It is now stated in the matrix rather than applied to it at build time. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
The matrix build job was named "<variant> / <suite>". GitHub composes a called workflow's job names as "<caller> / <callee>" and renders the result as a tree, so the separator inside the caller's own name added a level that means nothing: every run grew a qcom-next hub and a qcom-next-debug hub, each holding its suites, when the thing being run is one leg per suite. Name the job "<variant> (<suite>)" instead, which is how GitHub writes its own matrix job names, so a leg reads as one entry. The jobs themselves are unchanged. The remaining nesting under each leg is the reusable workflow's own jobs, which GitHub always surfaces individually. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
kernel_variant was doing two unrelated jobs. It named the build in CI - the Actions job, the prepared-source artifact, the Debusine child workspace, the S3 path - and it was also handed to derive-localversion.sh, which folded it into LOCALVERSION and so into the kernel release string and the versioned linux-image package name. Renaming a build leg therefore renamed the kernel that leg installs, which is not something a CI label should be able to do. Split the two. Add a flavour field: the kernel's own identity, the part of the release string that distinguishes two kernels built from one ref with different configuration so their linux-image packages coexist. derive-localversion.sh takes --flavour in place of --variant, and build-kernel-deb.yml feeds it from the new flavour input rather than kernel-variant. kernel_variant keeps every CI-facing use and reaches nothing that ends up in a package. Everything the matrix asserts about what is built is regrouped onto flavour, since that is now what identifies a package: the srcpkg, binpkg and kernel_config agreement, the per-suite dkms agreement, the kernel ref agreement within a delivery type, the Daily-and-Release requirement, the srcpkg and binpkg exclusivity, and the Daily/Release revision pairing. kernel_variant keeps one check of its own, that it does not repeat for a type and suite, which is what makes job names and artifact scopes unique. The flavours are qcom-next and qcom-next-debug, the values kernel_variant already held, so every leg builds the same kernel release it did before: verified leg by leg against the old script, LOCALVERSION identical for all nine. kernel_variant is now free. Renaming the legs to qcom-next-trixie and so on validates cleanly and leaves LOCALVERSION untouched, which it could not have done before this change. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
kernel_variant repeated: three Daily entries all called qcom-next, distinguished only by their suite. The Actions job had to be named from two fields to say which leg it was, and a dispatch had to take a variant and a suite and offer scopes for each combination of the two. Give every entry a kernel_variant of its own - qcom-next-trixie, qcom-next-debug-forky - so a variant names exactly one build. The job name is now just the variant, a dispatch asks for builds by name, and resolve-matrix.py rejects a matrix that uses one variant twice in a delivery type. Uniqueness is per type rather than global: a run only ever resolves one type, so a Daily and the Release that supersedes it keep the same name, which is what makes them recognisable as the same leg. The dispatch filters take comma-separated lists, so a run can name several legs. --flavour joins them, because "release qcom-next everywhere it ships" is the normal release action and naming each suite individually would be a worse way to ask for it. A name matching no entry is now an error listing what is available, rather than a silently smaller build set - with per-leg names there is much more to mistype. Daily scopes become Full matrix, Selected variants, and Selected flavour; Release becomes Selected flavour and Selected variants. Renaming the legs would have moved every published artifact, since the S3 path, Debusine workspace and prepared-source artifact were all keyed on kernel_variant. They describe which package is at that location, not which CI job wrote it, so they are keyed on flavour now and their values are unchanged: qcom-next/trixie stays qcom-next/trixie. Verified leg by leg, along with LOCALVERSION. build-kernel-debusine.yml and build-kernel-ubuntu.yml take flavour in place of kernel-variant and no longer see the CI label at all. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
The two keys were named for what the matrix used to be. "deliveries"
described rows that were expanded into build legs, and "kernel_variant"
identified a kernel variant that several rows shared. Neither is true
any more: an entry is one build, and its identifier labels that one
build and nothing else, having been separated from flavour and from
every published path.
Rename them to builds and name, which is what a YAML list of things
each carrying an identifier normally looks like, and reads as such:
builds:
- name: qcom-next-trixie
type: Daily
suite: trixie
flavour: qcom-next
The resolver's filter follows: --kernel-variant becomes --build, and
build-kernel-deb.yml's kernel-variant input becomes build. The dispatch
inputs on daily.yml and release.yml become "builds", taking the same
comma-separated list, and their scopes read "Selected builds".
Both old spellings now fail rather than being ignored: deliveries is
rejected as an unknown top-level key, and kernel_variant as an unknown
field on an entry whose name is missing.
Names only. No build changes what it produces: the job names, S3 paths,
Debusine workspaces, revisions and LOCALVERSIONs are all as they were.
Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
Three kernels join the delivery matrix, each built for trixie and forky: mainline from the tip of Linus's tree, next from the newest next-YYYYMMDD tag of linux-next, and qcom-arduino from the early/hwe/arduino topic branch of kernel-topics. mainline and next give early warning of upstream breakage; qcom-arduino delivers hardware enablement that has not reached qcom-next. All three track something moving - two branch tips and a tag cut every night - so none of them has a Release entry: a Release must name an immutable ref, and neither a branch tip nor tonight's tag is one. They are built and published daily and never promoted to qli. That makes the "every flavour defines at least one of each delivery type" invariant wrong, so it becomes the half that carries the meaning: a Release entry requires the Daily that tests it, but a Daily needs no Release. The symmetric rule was not protecting anything - a flavour built daily and never promoted is a coherent thing to configure, and the rule only prevented saying so. mainline and next bundle no DKMS modules. kgsl, camx and iris-vpu are built against the Qualcomm tree, and a listed module is a presence contract: naming one whose BUILD_EXCLUSIVE gates exclude the kernel fails the build rather than shipping without it. qcom-arduino is a Qualcomm branch and carries the same three its qcom-next siblings do on the Debian suites. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
The resolve job took a whole ubuntu-latest runner to turn inputs.suite into the string "debian" or "ubuntu" via a static case statement, which the two build legs then compared against. A job-level if: expression answers the same question for free, so every run paid for a runner spin-up and a queue wait ahead of both legs to learn something already fixed at dispatch time. Both legs now test the suite themselves, the Ubuntu one negating what the Debian one asserts, so the two cannot drift into an overlap or a gap. The list is spelled out twice because GitHub Actions does not expand YAML anchors and there is no other way to share it without reintroducing a job. The trixie default is unchanged. One behaviour change comes with it: GitHub's contains() compares strings case-insensitively, so a suite of "Trixie" now classifies as debian where the case statement sent it to ubuntu. Neither spelling reaches here from daily.yml or release.yml, whose suites come from the matrix in lowercase. The build-name check moves to the Resolve Debian revision step, the only place the name is consumed, and becomes an ::error:: so it shows in the run summary rather than only in the log. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
A full daily run rendered 21 skipped jobs against 11 real build legs, and a PR the same. Every leg started both family backends and skipped one; every Debian leg started both Debusine tails and skipped one. Release was worse per leg: all four of its legs are Debian, so the Ubuntu backend never once ran. Both branches were decided by data the caller already knew. A suite's family follows from the suite, which the matrix states; publish-versus-promote is fixed by which workflow is running, since a daily never promotes and a release always does. Passing both down into one shared workflow and re-deciding them there with if: is what minted the skipped jobs, and GitHub offers no way to take that back: a job is conditional or it is not, an empty dynamic matrix is an error rather than an empty job list, and the guard that avoids the error is another skipped job. So decide in the caller and give each decision its own workflow. resolve-matrix.py derives a family from each entry's suite and filters on it, daily.yml and pr-build.yml split one selection into a debian and an ubuntu matrix, and each leg calls only the workflow that builds it. build-kernel-deb.yml, which existed to make the choice, is gone. The two Debian workflows are the same build with different tails -- build-kernel-debian.yml publishes to S3, release-kernel-debian.yml promotes to a target workspace -- so neither carries the other's tail as a job to skip. Release entries only ever reach the second: promotion runs through Debusine, which builds no Ubuntu suite, so resolve-matrix.py now rejects a Release entry for one. That was previously accepted and would have built the package, taken the Ubuntu path, published to the daily S3 location and reported success without releasing anything. The shared steps move into two composite actions rather than a common parent workflow. A reusable workflow would have put its name into every job's, which is the nesting that "qcom-next-resolute / Build (Ubuntu, docker) / Build kernel package" already suffered from; a composite action adds no segment. That name is now "qcom-next-resolute / Build kernel package", and a full daily run is 32 jobs across 11 legs with nothing skipped. Suite stays a free-text dispatch input, so a new suite still needs no workflow edit. Since the workflow now implies the family, prepare-kernel-source takes the family its caller builds and rejects a suite belonging to the other before anything is cloned. Its family list is the one resolve-matrix.py routes by. Two dispatch scopes were repaired in passing: daily.yml and release.yml both still matched "Selected variants" in a case whose input has offered "Selected builds" since the matrix keys were renamed, so choosing it failed the run with "unsupported build scope". No build changes what it produces. The job names, S3 paths, Debusine workspaces, revisions and LOCALVERSIONs are all as they were. The staging build-dependency workspaces come along in the same move. They were picked by a ternary on target-workspace, which was the only thing separating a daily run from a release one while both lived in one workflow. That separation is now the choice of workflow, so debusine-build takes the list as an input and each caller states its own: build-kernel-debian.yml reads qli and qli-staging, release-kernel-debian.yml reads qli alone. Same two answers, no longer derived from a field that no longer distinguishes anything. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
zizmor raised two findings against the new action and workflows. prepare-kernel-source wrote its intermediate values to GITHUB_ENV, which puts them in the environment of every later step in the job rather than only the steps that want them, and lets a value containing a newline define environment variables of its own -- LD_PRELOAD or NODE_OPTIONS being the interesting ones. Not all of these values are ours to trust: the kernel ref is whatever the remote's tag list offers, and on pr-build.yml the inputs come from the build matrix as the pull request wrote it, since a PR is built against its own ci/build-matrix.yaml. So the steps now hand their values on as step outputs, and each consumer names in its own env: block what it reads. That is the same data flowing the same way, but scoped to the steps that asked for it. The resolved kernel ref is also rejected outright if it contains a newline, because that one is chosen furthest from us and step outputs are injectable in the same manner if less usefully. The checkouts leave the GitHub token in .git/config, which the prepare job then packs a sibling directory into an artifact next to. None of these checkouts is fetched from or pushed to afterwards: the kernel is cloned separately with its own remote, and the PR-merge steps fetch inside that clone rather than this one. So they all set persist-credentials: false, as release-kernel-debian.yml's debusine-action checkout and pr-build.yml and release.yml already did. daily.yml's configure-matrix checkout gets the same treatment. zizmor did not flag it, having only reviewed the diff, but it is the same checkout doing the same job as the two that were already set that way. No build changes what it produces, and the job graph is as it was: 32 jobs across 11 legs for a daily or PR run, 12 across 4 for a release, none skipped. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
"Build kernel package" said what every build job in the repository does, so next to "Build (Debusine)" it read as the generic one rather than as the other half of a pair. Both jobs build a kernel package; what separates them is where. Name it "Build (Docker)", which is what the rest of the repository already calls this path -- build-kernel.sh --build-mode docker, the README's Docker path -- and the two now differ by the word that actually differs: qcom-next-forky / Build (Debusine) qcom-next-resolute / Build (Docker) The step inside it keeps the name, as the Debusine steps do; it is the job listing that had the ambiguity. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
Every job ran docker_deb_build.py --rebuild, which deletes any existing pkg-builder image and builds a fresh one from docker-pkg-build's Dockerfile. The name it tags looks like a registry reference, ghcr.io/qualcomm-linux/ pkg-builder:<suite>, but nothing here ever pushed or pulled it: the docker run that follows was using a local image the previous step had just made. That image is published. docker-pkg-build's container-build-and-upload.yml builds trixie, noble and resolute, validates each by building a test package in it, and pushes them on every push to main and again weekly. So each of our jobs was rebuilding, from the same Dockerfile, an image that upstream had already built and tested -- once for a Debian leg, twice for an Ubuntu one, on ephemeral runners that keep no layer cache between them. Pull trixie and resolute, which covers all eleven daily legs: the ten Debian ones use the trixie image, and resolute is the only Ubuntu one. The package is not public, so the pull logs in with GITHUB_TOKEN under the packages: read permission these workflows already declared and had no use for until now. Suites with no published image still build one on the runner, so the path stays. In prepare-kernel-source both that build and the docker-pkg-build checkout it needs are now conditional, and the Ubuntu build job needs no step at all for them: docker_deb_build.py builds the image itself when it finds it missing, so not pulling is instruction enough. forky is worth a note, having no image of its own in the registry and no Dockerfile in docker-pkg-build to build one from. It uses the trixie image and therefore pulls it, which costs nothing today: Debusine performs the actual suite-specific build, and this container only runs prepare-source.sh, which generates packaging rather than compiling anything. The comment says so, so that the day a forky image appears it is clear what to change. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
"Prepare kernel source" and "Build (Debusine)" named a stage and a place, so
the pair read as different kinds of thing and neither said what came out of
it. Name all three for their product instead, and the sequence states itself
in the checks list:
qcom-next-forky / Generate source package
/ Build package (Debusine)
/ Publish to S3
qcom-next-resolute / Generate source package
/ Build package (Docker)
The parenthesis keeps doing what it did before: source package generation is
one job whichever family runs it, and only the binary build differs, so that
is the only name that has to say where it happens.
Display names only. The job ids are untouched, so needs:, the comments and the
README all still refer to prepare, build, publish and release as they did.
Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
Both were listed as buildable and neither was. questing is skipped by docker_deb_build.py's SKIP_REBUILD_DISTROS and has no published image, so a questing run would build nothing and then fail pulling one. bookworm has no Dockerfile in docker-pkg-build at all, and no published image either. Neither appears in ci/build-matrix.yaml, so both failures were only ever waiting for someone to dispatch them by hand -- which build-kernel-ubuntu.yml invited by naming questing in its suite description. bookworm goes further than the description: it was in resolve-matrix.py's DEBIAN_SUITES, which decides which workflow builds an entry, and it was the only Debian suite mapping to an image of its own name. Without it every Debian-family suite maps to trixie, so the two case arms that did that become one arm saying so. The packaging keeps questing. prepare-source.sh accepts it in VALID_DISTROS and debian/README.md lists it as a supported distribution, which is a claim about the packaging rather than about CI, and it is true: what is missing is a container image to build it in, not support for the suite. bookworm was never in either, so nothing there to remove. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
Both were floating on a major-version tag, and two checkouts were already pinned to a commit while thirteen others were not, so the same action ran at whatever main had moved to depending on which file invoked it. A tag is a mutable ref: it can be repointed at any commit, which is the supply-chain hole pinning closes. Pin every use to the commit the release tag names, with the version in a comment so the next reader knows what is pinned without resolving a SHA: actions/checkout 3d3c42e5aac5ba805825da76410c181273ba90b1 v7.0.1 actions/upload-artifact 043fb46d1a93c77aae656e7c1c64a875d1fc6a0a v7.0.1 Both SHAs were checked against the tags they claim to be rather than taken on trust; each is the commit v7 and v7.0.1 both point at today. This raises a runner requirement. Both actions run on Node.js 24 from v6, which needs Actions Runner 2.327.1 or newer, and the prepare and build jobs run on the self-hosted arm64 runners. Those need to be at that version before this merges, or every job that checks out will fail on them. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
Every workflow granted contents: read and packages: read to all of its jobs,
so a job that only reads the repository still held a token that could read
packages, and nothing recorded which job needed which.
Give each job the permissions it uses and name the consumer in a comment, so
the next person to add a step can tell whether the token already covers it:
permissions:
contents: read # actions/checkout
packages: read # docker pull ghcr.io/qualcomm-linux/pkg-builder
Publish to S3 is the one job that narrows: it checks out debusine-action and
reads the built packages out of Debusine over HTTP, and pulls no image, so it
keeps contents: read alone. Every other job either pulls the pkg-builder image
or runs in the debusine-pkg-builder container, and the comment says which.
Job-level permissions replace the workflow-level ones rather than adding to
them, so each block lists everything its job needs. On the jobs that call a
reusable workflow the block is a ceiling for the whole call, which is why
daily.yml, pr-build.yml and release.yml grant packages: read there: the
workflow they call pulls images even though the calling job runs nothing
itself.
Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
Four majors behind, and floating on a tag. It is the counterpart of the upload-artifact pinned alongside it: prepare uploads the prepared source tree, and both build paths download it again. v8's inputs are a superset of v4's, adding digest-mismatch and skip-decompress and dropping nothing, so the name and path this repository passes still mean what they meant. Like the actions pinned before it, v8 runs on Node.js 24 and so needs Actions Runner 2.327.1 or newer. Both callers are self-hosted jobs. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
Pinned, not bumped: aws-v5 exists but this stays on aws-v4. The two new workflows named the aws-v4 tag, which is at least a tag. The legacy build-kernel.yml named aws, which is a branch, so it ran whatever had last landed there -- the mutable ref this repository otherwise no longer has, and one publishing artifacts to S3 at that. All three now name the commit aws-v4 points at, so the branch stops deciding what the legacy workflow runs and all three uses agree on one version. For build-kernel.yml that settles it at aws-v4 rather than at the branch head, which currently carries the aws-v5 interface. Nothing it passes is affected: s3_bucket, path and destination are inputs of both, and the inputs aws-v5 adds are ones no caller here sets. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
Every manual build already had to name something the matrix defines, but each workflow asked for it differently, and the two build workflows asked for things the matrix had long since taken over. daily.yml offered a Build scope dropdown whose three options -- full matrix, named builds, one flavour -- were three ways of writing a set of entries, one of which needed a second input to say which set. release.yml offered the same idea under a different name and a different default. build-kernel-debian.yml and build-kernel-ubuntu.yml offered neither: their dispatch predates one entry being one package, and still asked a person to type git-clone, srcpkg, binpkg, kernel-config, dkms and a Debian revision, with defaults frozen at whatever qcom-next/trixie looked like when the form was written. Dispatching one of those built whatever the form said, not what the matrix says, and nothing reconciled the two. So there is now one input, in the two workflows that resolve a matrix: builds, defaulting to all, otherwise a comma-separated list of names. A scheduled run carries no inputs and lands on the same default, which is what it wanted anyway. Selecting a flavour across its suites is naming its entries; the --flavour filter stays in resolve-matrix.py for local use. A name matching no entry of that delivery type still fails the run with the names that do, so a typo cannot quietly narrow a release. daily.yml is the manual entry point that build-kernel-* stops being. It resolves the selection into both families, so one dispatch can name Debian and Ubuntu builds together and each is called by the workflow that builds it -- which is the thing a dispatch of a single-family workflow could never do. The build workflows are workflow_call only now: one run of either is one matrix entry, and a reusable workflow cannot call itself to fan out over a list. The three inputs they offered that the matrix deliberately does not carry -- debug-build, qcom-next-pr, kernel-topics-pr -- move to daily.yml, where they apply to every selected build. They describe a one-off validation run rather than a delivery target, which is why they are inputs and not entry fields. The rest of what that form asked for is gone: it is in the entry, and typing it again was the way to disagree with it. resolve-matrix.py now rejects a build named all, which the dispatch form would otherwise read as every entry, leaving that build unreachable by name. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
A release was a second build. release.yml took a pinned tag, built it from scratch in a fresh CI workspace, and promoted that -- bits the daily build had already produced from the same tag and tested. Everything else followed from the duplicate build: a second set of matrix entries to describe it, a type field to tell the two sets apart, ref_strategy rules per type, a release ref pinned in a file so that releasing meant editing the matrix, and a revision convention whose only job was to keep the two builds' versions apart. What shipped was never quite the artifact that passed the daily. So the daily build now promotes. An entry naming a target_workspace is copied into it once its build succeeds, and release.yml promotes onward from there and builds nothing at all: it names a version that already exists and asks Debusine to publish it into the release workspace. lib/release was already built for this -- it promotes between workspaces given a source package and a version, and defaults its target to qli-staging. That collapses the matrix to one kind of entry, 15 to 11. type is gone, and with it the per-type ref and revision rules; target_workspace is now an optional field on any Debian entry rather than a Release-only one, and the entries that name none -- mainline, next, the topic branch -- are built for early warning and published to S3 exactly as before. Releasing changes no file here: there is no ref to pin, because the version being released has already been built. The trailing ~ therefore stays on the version through release. Promoting the tested artifact means the archive gets the version it was built with, and a release that dropped the ~ would have to be a different build, which is the thing being removed. Nothing new upstream is now a green run rather than a failure. The version is a function of the resolved ref and the entry's revision, so a night on which the tracked tag has not moved rebuilds a version the archive already holds and Debusine rejects the duplicate. prepare reads the version out of the changelog it just generated and asks the target workspace whether it has it, and the build, the S3 publication and the promotion are skipped when it does. An unreachable workspace fails the run instead: a configuration problem must not look like a quiet night. Three kinds of run promote nowhere by construction. A PR build never forwards a target-workspace. A dispatch setting debug-build, qcom-next-pr or kernel-topics-pr builds something the matrix does not describe, so build-kernel-debian.yml declines to promote it -- guarded in the callee, where it holds for every caller rather than for the ones that remembered. And an entry naming no workspace has nowhere to go. release-kernel-debian.yml is deleted. It was the release tail of a build that no longer happens. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
release.yml promoted out of qli-staging into qli, which meant the pipeline needed qli-staging to exist at all: a workflow that runs days after the build has to read the packages from somewhere durable, and the workspace the build actually ran in is not that. lib/build names it <parent>-gh-<repo>-<run>-<attempt>-<leg> and creates it per run, so by the time anyone dispatches a release there is neither a name to look up nor, eventually, a workspace behind it. qli-staging was there to outlive the run. Promoting inside the build removes the need for it. build-kernel-debian.yml already had the promote job, reading the CI workspace while the run still holds it, so there was never anything for a second workflow to do that this one could not do first. release.yml is deleted, and with it the upstream-version field that existed only so a later run could name a version it had not built. The destination stays qli-staging for now. Nothing about where packages land changes with this commit: the same workspace receives the same artifacts from the same job, and only the workflow that could later move them onward is gone. Pointing DEBUSINE_STAGING_WORKSPACE at qli would publish every night straight into the released archive, so that is a decision to take separately, together with moving the promote job to the Production environment so an approval still stands between a nightly build and qli. README says so where the variable is documented. Nothing reads DEBUSINE_RELEASE_TOKEN any more, and the Production environment is unused. Both are left configured rather than removed, because they are what a gated promotion into qli will want. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
The promote job in build-kernel-debian.yml runs when target-workspace is not empty, and pr-build.yml kept a pull request's kernel out of the archives by not passing the input at all -- relying on its default being empty, with the reason recorded in a comment two dozen lines above the block it applied to. That is a lot of weight for an absence to carry. A PR is unreviewed by definition and its packages are versioned -pr<number>, so promoting one would put a kernel nobody approved into an archive people install from, at a version no later build supersedes. Nothing in the with: block said so; a change to the input's default, or a copy of the block into a workflow that should promote, would have flipped it silently. So the value is written where it takes effect, with the reason next to it. Nothing changes at runtime: the input already defaulted to empty and still does. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
The trailing ~ existed to sort a daily build below the release built from the same ref: two builds of one kernel needed two versions, and ~ is what made the daily the lesser of them. resolve-matrix.py enforced the pairing, requiring a Daily revision to be its Release revision plus a ~. There is no second build any more. A kernel is built once, and the artifact that build produced is what any archive holds, so nothing exists for a ~ to sort against. All it does now is make every published package look permanently provisional -- a version that will never be superseded by the non-~ one it implies is coming. So trixie builds at 0qli1~bpo13+1, forky at 0qli1, resolute at 0qli1~26.04.1. The remaining ~ in the trixie and resolute revisions is the backports convention and stays: it is what sorts a trixie backport below a forky build of the same kernel, and the cross-suite order the README documents is unchanged -- resolute < trixie < forky, verified with dpkg --compare-versions. Upgrades are unaffected. Every new version sorts above the ~-suffixed one it replaces, so a machine tracking qli-staging moves onto the next nightly by itself. The first run after this builds and publishes rather than skipping, because the version it produces is genuinely one the archive has not seen. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
pr-build.yml claimed that kver-extra marks the version so PR artifacts never collide with the daily build's. It does not: debian/rules appends KVER_EXTRA to KVER and derives the package version separately, so a PR build shares the nightly's source version and differs only in uname -r and the versioned linux-image name. That is enough where it is -- a PR build has its own throwaway workspace and promotes nowhere -- but it is not a version stamp, and the comment should not invite anything to start relying on it as one. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
Releasing has had no workflow since the promotion was folded into the nightly build. That commit removed release.yml because its model could not work: it promoted out of qli-staging days after the fact, which meant the pipeline had to keep a durable staging workspace for it to read, since the workspace a build actually ran in is named after that run and does not outlive it. This one does not promote a previous run's build. It builds the ref it releases, in the same workflow the nightly calls, and promotes that build while the run still holds its workspace -- so what reaches qli is the artifact this run produced and tested, and the awkward dependency on a workspace outliving the run that filled it never arises. What is released is written down rather than typed in. ci/build-matrix.yaml gains a releases list beside builds: same shape, plus a target_workspace, and ref_strategy restricted to pinned_ref so that two dispatches of one entry cannot ship different kernels. Updating a release is a pull request that changes branch_or_tag, which puts the ref that ships under review before the run that ships it -- where an upstream-version dispatch field put it at the mercy of whoever typed it. The refs come from main, which is where releasing last worked: qcom-next and qcom-next-debug, trixie and forky, at qcom-next-7.2-20260826. target_workspace stays rejected on a builds entry. Where a nightly is published follows from why it is running and remains the calling workflow's to decide; a release is the one case where the destination belongs to the entry, because putting one ref into one archive is the whole of what it is. The two lists are validated together however few of them a caller selects, so a broken release entry fails a nightly run rather than lying in wait for whoever next tries to release. Releases are Debian-only, enforced rather than assumed: promotion runs through Debusine and the Ubuntu path has no workspace to promote into, so an Ubuntu entry there would name a destination nothing could deliver it to. Which environment the promote job runs in becomes the caller's, through a new promote-environment input defaulting to Staging. The nightly keeps promoting into staging unattended; a release passes Production, so whatever approval that environment requires stands in front of qli. The run summary lists the refs, revisions and destinations before that gate, so the approval is given against what is shipping rather than against a run number. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
prepare-kernel-source took qcom-next-pr and kernel-topics-pr and merged or applied them after the clone, but no workflow passes either: the build workflows have no input to carry them, so the two steps could never run. They could not have been wired up as they stood, either. The commit SHA and date are read from HEAD before the patches would be applied, so a patched build would name a commit it did not build. And a merge commit made at build time has no stable SHA, so the version could never be reproduced from it. An orig tarball keyed on the commit, which the source package is about to be, rules such builds out for good. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
The action looked the revision up in ci/build-matrix.yaml when its input was empty, for a caller with no matrix entry in hand. No such caller exists: the build workflows are workflow_call only, are called with a matrix entry, and give the input a default of their own, so the lookup could not be reached. The revision is now a required input, and a caller that forgets it fails at the action boundary rather than building at a revision nobody chose. resolve-matrix.py keeps --field, which is still a useful way to ask the matrix a question from a shell. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
The if: guards on the build jobs look removable, and are not: an empty matrix
is a workflow error rather than an empty job list, so a family the run selects
nothing on has to be skipped rather than started with nothing to do.
Skipping one has a visible cost that looks like a bug. GitHub never expands a
job it skips, so it cannot evaluate name: ${{ matrix.name }} and shows the
expression's source text -- a check literally called "matrix.name". A ||
fallback does not help, printing its own source text just the same, and the
only name that would read well is one never mentioning matrix, which would mean
renaming every check in every workflow to tidy up one that appears when a
family is empty. Both facts are written down so the next reader reaches that
conclusion without rediscovering it.
Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
Split out of "packaging: date debian/changelog from the kernel commit": the packaging half of that change landed early, alongside the reproducible source package work, before this action existed to call it. The action reads the date on the runner and passes it in, as it does the SHA and the ref: prepare-source.sh runs in a container as root and git will not read a checkout owned by someone else. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
Every input naming what a build is -- the entry, its suite and flavour, the kernel repository and ref, the package names and the Debian revision -- had a default spelling out the qcom-next entry. No caller relied on one: daily.yml, pr-build.yml and release.yml pass all of them from the matrix. What the defaults did was let a caller that dropped one build the default entry's kernel under another entry's name without anything noticing. They are now required, so an omission is a workflow error at parse time. The inputs that keep a default are the ones for which empty is a value: tag-pattern outside latest_tag, an empty fragment or module list, and the overrides and destinations the caller chooses rather than the entry. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
prepare-kernel-source now runs build-source-package.sh after prepare-source.sh, in the same pkg-builder container, and uploads the resulting .changes set as source-package-<flavour>-<suite>. Nothing consumes it yet: the prepared-tree artifact is still uploaded and still what both build paths read, so this commit changes what a run produces and not what it builds. The next commits move each family onto it. The version the caller is told now comes from the fields file the script writes, read from the .dsc it built, rather than from a sed over the changelog on the runner host. The action also reports the orig tarball's name and SHA-256, which a caller can hold against what an archive already has for this upstream version. The container is told to trust the checkout, which the runner owns and root inside the container would otherwise refuse to read; it is this job's own clone. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
The build job downloaded the prepared tree, unpacked it, and had debusine-action's generate-source-package assemble an orig tarball from it with tar czf -- a different tarball on every run. It now downloads the .changes set the prepare job built, reads the version from the .dsc, and gives lib/build that .dsc: the source package Debusine builds is the one prepare-kernel-source wrote and reported, with the reproducible orig. DEBUSINE_ASSEMBLE_ORIG in debusine-action stays for its other callers; nothing here uses it any more. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
The build job downloaded the prepared tree and ran build-kernel.sh --local-source --skip-prepare, which had docker_deb_build.py make an orig tarball of its own with tar czf before handing sbuild a .dsc. It now downloads the .changes set prepare built and passes its .dsc to build-kernel.sh --dsc, which runs sbuild on it in the suite's pkg-builder image. The Ubuntu family therefore builds the same kind of thing Debusine builds for the Debian family: the source package the run reported, with the reproducible orig, and nothing is prepared twice. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
Both build paths now start from the source-package artifact, so the tarball of the prepared tree has no reader. It was the larger of the two uploads, and the reason the README explained why an artifact had to be a tar.gz: the tree carried execute bits that upload-artifact's zip would strip. A .changes set is plain files, so that note goes with it, and the pipeline diagrams now show the source package as the handoff. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
The orig tarball is now a function of the kernel commit, so the archive can hold a different one for the same upstream version only if something about how it was written differs -- and that is what this check is for. The already-published step, which already reads the target workspace's source index, now also finds every stanza naming this run's orig and compares its SHA-256 with the one prepare reported. A mismatch fails the run with both checksums, before anything is built, rather than surfacing as a file conflict at promotion or, worse, not at all. The case that matters is the packaging rebuild: 0qli1 to 0qli2 of one snapshot, where the archive requires the two revisions to share one file. The check reads this suite's index only, since apt indexes are per suite; across suites the reproducibility of the tarball itself is the guarantee. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
The prepare job and the build job ran on the same runner label, so the only thing the artifact between them did was cross a job boundary that had no other reason to exist: uploaded by one ephemeral runner to be downloaded by the next. One job now clones, prepares, builds the source package and builds the binaries from it in place. The action still uploads the .changes set, as the record of what was built, but nothing downloads it. The build job's own image-fetch step goes with it. It repeated the action's pull-or-build decision for one suite; the action makes that decision for every suite before prepare-source.sh runs, and leaves the docker-pkg-build checkout where build-kernel.sh looks for it when it had to build the image. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
The .deb files reach S3 under one run's directory; the source package they were built from now sits in source/ beneath it, on both paths. The .changes records every checksum, the .orig.tar.gz is the exact source, and the .dsc is enough to rebuild the packages anywhere. The Actions artifact holding the same files expires after a week; this is the copy that stays with the binaries. The Debian publish job downloads the artifact for it, since the source package was built on another runner; the Ubuntu job uploads it from the workspace it was built in. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
The orig comparison ran first, so a night on which the tracked tag had not moved failed instead of skipping: the version was already published, and the orig in the archive predates git archive, so the checksums differ. That is a run with nothing to do reported as a conflict. A run that publishes nothing uploads no files, so nothing it built can conflict with anything. The already-published test therefore comes first and exits, and the orig comparison guards only a version that is actually going to be promoted. That leaves it doing the job it was added for: a packaging rebuild publishing a second Debian revision of one upstream version, where the archive requires both revisions to name one file. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
The action had a single caller and paid for being one. The vars and secrets contexts do not reach a composite action, so the Debusine host, scope, user and token were all declared as inputs whose only purpose was to carry values the calling workflow could already see. Its steps now sit in the build job and read vars and secrets directly, and six inputs and their plumbing go. Two more things fall out. The job checked out pkg-linux-qcom solely to reach the action, so that checkout goes too: the package comes from the artifact and the tooling from debusine-action, and nothing of this repository is needed in that job at all. And workspace_url was an action output that only ever fed a step summary, so it is now a local variable in the step that writes it. Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
Christopher Obbard (obbardc)
deployed
to
Staging
September 15, 2026 02:12 — with
GitHub Actions
Active
Christopher Obbard (obbardc)
deployed
to
Staging
September 15, 2026 02:15 — with
GitHub Actions
Active
Christopher Obbard (obbardc)
deployed
to
Staging
September 15, 2026 02:15 — with
GitHub Actions
Active
Christopher Obbard (obbardc)
had a problem deploying
to
Staging
September 15, 2026 02:15 — with
GitHub Actions
Error
Christopher Obbard (obbardc)
had a problem deploying
to
Staging
September 15, 2026 02:16 — with
GitHub Actions
Error
Christopher Obbard (obbardc)
had a problem deploying
to
Staging
September 15, 2026 02:16 — with
GitHub Actions
Error
Christopher Obbard (obbardc)
had a problem deploying
to
Staging
September 15, 2026 02:16 — with
GitHub Actions
Error
Christopher Obbard (obbardc)
had a problem deploying
to
Staging
September 15, 2026 02:17 — with
GitHub Actions
Error
Christopher Obbard (obbardc)
had a problem deploying
to
Staging
September 15, 2026 02:17 — with
GitHub Actions
Error
Christopher Obbard (obbardc)
had a problem deploying
to
Staging
September 15, 2026 02:18 — with
GitHub Actions
Error
This branch was successfully deployed
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Both build paths now start from the same source package, built once.
preparebuilds the.dscand hands it to Debusine and to the Ubuntu leg,instead of each path preparing its own tree -- so the prepared tree no longer
needs uploading, and the Ubuntu leg collapses into a single job. The source
package is published to S3 beside the binaries.
Because the
orig.tar.gzis reproducible, a rebuild of the same snapshot at anew packaging revision produces a byte-identical orig; the run now checks the
archive for a published version first and fails if it holds a different orig
for that version, rather than discovering the clash at upload.
The
debusine-buildcomposite action is inlined intobuild-kernel-debian.yml,its only caller.
Based on #112; review or merge that first.