Skip to content

ci: fewer jobs, less rebuilding, pinned actions - #111

Closed
Christopher Obbard (obbardc) wants to merge 28 commits into
mainfrom
wip/obbardc/ci-job-restructure
Closed

Christopher Obbard (obbardc) wants to merge 28 commits into
mainfrom
wip/obbardc/ci-job-restructure

Conversation

@obbardc

Copy link
Copy Markdown
Contributor

Restructures how the build legs are arranged, then pins what they call.

The resolve job is dropped and the legs are gated directly; the build path is
branched on in the caller rather than inside the build; prepare's values are
passed as step outputs instead of an uploaded tree, which also lets the checkout
drop its credentials. The builder image is pulled rather than rebuilt in every
job. Jobs are renamed for what they produce. The questing and bookworm suites
are dropped from CI.

Then the action pinning: actions/checkout and actions/upload-artifact to
v7.0.1, actions/download-artifact to v8.0.1, upload-private-artifact-action
to its aws-v4 commit, and GITHUB_TOKEN scoped per job with each permission
explained. These are here rather than in the repo-hygiene branch because the
workflow files they pin do not exist on main -- this branch creates them.

Based on #110; review or merge that first.

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>

This branch was successfully deployed

No deployments
Staging eab41c3b Deployed Sep 15, 2026 by obbardc via qcom-arduino-forky / Build package (Debusine) #56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant