Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 67 additions & 13 deletions .github/actions/lava-test-plans/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ inputs:
description: "Pull request URL. When set, it is added to the LAVA job metadata as PR_URL"
required: false
default: ""
distro:
description: "Distro name without the kernel suffix. Added to the LAVA job metadata"
required: false
default: ""

outputs:
jobmatrix:
Expand Down Expand Up @@ -65,6 +69,14 @@ runs:
run-id: ${{ inputs.build_id }}
pattern: build-url_*_${{ inputs.distro_name }}

- name: 'Download build metadata'
continue-on-error: true
uses: actions/download-artifact@v7
with:
github-token: ${{ inputs.gh_token }}
run-id: ${{ inputs.build_id }}
pattern: build-meta_*_${{ inputs.distro_name }}

- name: Run lava-test-plans
id: lava-test-plans
shell: bash
Expand All @@ -78,6 +90,9 @@ runs:
INPUTS_TESTKIT_REF: ${{ inputs.testkit_ref }}
INPUTS_PR_NUMBER: ${{ inputs.pr_number }}
INPUTS_PR_URL: ${{ inputs.pr_url }}
INPUTS_REF: ${{ inputs.ref }}
INPUTS_BUILD_ID: ${{ inputs.build_id }}
INPUTS_DISTRO: ${{ inputs.distro }}
run: |
set -euo pipefail
cd lava-test-plans && pip install .
Expand Down Expand Up @@ -119,6 +134,32 @@ runs:
echo "GITHUB_WORKFLOW_URL=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" >> "${VARS_OUT_PATH}/gh-variables.ini"
echo "GITHUB_WORKFLOW_RUN_ID=${GITHUB_RUN_ID}" >> "${VARS_OUT_PATH}/gh-variables.ini"
echo "GITHUB_WORKFLOW_RUN_ATTEMPT=${GITHUB_RUN_ATTEMPT}" >> "${VARS_OUT_PATH}/gh-variables.ini"
SOURCE_SHA="${GITHUB_SHA}"
SOURCE_BRANCH="${GITHUB_REF_NAME}"
BUILD_TYPE="${GITHUB_EVENT_NAME^^}"
if [ "${GITHUB_EVENT_NAME}" = "workflow_run" ]; then
BUILD_TYPE="PR"
if [ -f "${GITHUB_EVENT_PATH:-}" ]; then
# github.sha is the target branch here, not the built commit
SOURCE_SHA=$(jq -r '.workflow_run.head_sha // ""' "${GITHUB_EVENT_PATH}")
SOURCE_BRANCH=$(jq -r '.workflow_run.head_branch // ""' "${GITHUB_EVENT_PATH}")
fi
fi
COMMON_META="${VARS_OUT_PATH}/gh-metadata.ini"
{
echo "[EXTRA_METADATA]"
echo "build-type = \"${BUILD_TYPE}\""
echo "build-run-id = \"${INPUTS_BUILD_ID}\""
echo "source-repo = \"${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}\""
echo "source-branch = \"${SOURCE_BRANCH}\""
echo "source-sha = \"${SOURCE_SHA}\""
echo "distro = \"${INPUTS_DISTRO}\""
echo "build-config = \"${INPUTS_DISTRO_NAME}\""
echo "testplan = \"${INPUTS_TESTPLAN}\""
echo "testkit-ref = \"${INPUTS_TESTKIT_REF}\""
echo "lava-test-plans-ref = \"${INPUTS_REF}\""
echo "gh-workflow-name = \"${GITHUB_WORKFLOW}\""
} > "${COMMON_META}"
IMAGE_TYPE="core-image-base"
case "${INPUTS_DISTRO_NAME}" in
"qcom-distro"*)
Expand All @@ -127,22 +168,35 @@ runs:
echo "AUTO_LOGIN_PASSWORD=oelinux123" >> "${VARS_OUT_PATH}/gh-variables.ini"
;;
esac
echo "image = \"${IMAGE_TYPE}\"" >> "${COMMON_META}"

RESULT_JSON='{"target": []}'
for machine in ${INPUTS_MACHINES//,/ }; do
echo "Rendering test job templates for ${machine}"
cp "${VARS_OUT_PATH}/gh-variables.ini" "${VARS_OUT_PATH}/${machine}.ini"
echo "IMAGE_FILE_NAME=${IMAGE_TYPE}-${machine}.rootfs.qcomflash.tar.gz" >> "${VARS_OUT_PATH}/${machine}.ini"
echo "DEVICE_TYPE=${machine}" >> "${VARS_OUT_PATH}/${machine}.ini"
# machines whose artifacts are published under a different build
ARTIFACT_MACHINE="${machine}"
case "${machine}" in
dragonboard-410c|dragonboard-820c) ARTIFACT_MACHINE="qcom-armv8a" ;;
esac
MACHINE_META="${VARS_OUT_PATH}/${machine}-metadata.ini"
cp "${COMMON_META}" "${MACHINE_META}"
echo "machine = \"${machine}\"" >> "${MACHINE_META}"
BUILD_META="${GITHUB_WORKSPACE}/build-meta_${ARTIFACT_MACHINE}_${INPUTS_DISTRO_NAME}/build-meta_${ARTIFACT_MACHINE}_${INPUTS_DISTRO_NAME}"
if [ -f "${BUILD_META}" ]; then
jq -r 'to_entries[]
| select(.value | type == "string") | select(.value != "")
| "\(.key) = \"\(.value)\""' \
"${BUILD_META}" >> "${MACHINE_META}"
else
echo "No build metadata for ${machine}"
fi

export BUILD_URL=""
# dir name and file name are the same as we're downloading multiple files
export BUILD_URL_FILE="$GITHUB_WORKSPACE/build-url_${machine}_${INPUTS_DISTRO_NAME}/build-url_${machine}_${INPUTS_DISTRO_NAME}"
if [ "${machine}" = "dragonboard-410c" ]; then
export BUILD_URL_FILE="$GITHUB_WORKSPACE/build-url_qcom-armv8a_${INPUTS_DISTRO_NAME}/build-url_qcom-armv8a_${INPUTS_DISTRO_NAME}"
fi
if [ "${machine}" = "dragonboard-820c" ]; then
export BUILD_URL_FILE="$GITHUB_WORKSPACE/build-url_qcom-armv8a_${INPUTS_DISTRO_NAME}/build-url_qcom-armv8a_${INPUTS_DISTRO_NAME}"
fi
export BUILD_URL_FILE="$GITHUB_WORKSPACE/build-url_${ARTIFACT_MACHINE}_${INPUTS_DISTRO_NAME}/build-url_${ARTIFACT_MACHINE}_${INPUTS_DISTRO_NAME}"
echo "${BUILD_URL_FILE}"
if [ -f "$BUILD_URL_FILE" ]; then
export BUILD_URL=$(cat "${BUILD_URL_FILE}")
Expand All @@ -158,19 +212,19 @@ runs:
echo "ROOTFS_URL=${BUILD_URL}/${INPUTS_DISTRO_NAME}/${machine}/${IMAGE_TYPE}-${machine}.rootfs.qcomflash.tar.gz" >> "${VARS_OUT_PATH}/${machine}.ini"

if [ "${machine}" = "dragonboard-410c" ]; then
echo "ROOTFS_IMG_FILE=${IMAGE_TYPE}-qcom-armv8a.rootfs.ext4" >> "${VARS_OUT_PATH}/${machine}.ini"
echo "BOOT_IMG_FILE=boot-apq8016-sbc-qcom-armv8a.img" >> "${VARS_OUT_PATH}/${machine}.ini"
echo "ROOTFS_IMG_FILE=${IMAGE_TYPE}-${ARTIFACT_MACHINE}.rootfs.ext4" >> "${VARS_OUT_PATH}/${machine}.ini"
echo "BOOT_IMG_FILE=boot-apq8016-sbc-${ARTIFACT_MACHINE}.img" >> "${VARS_OUT_PATH}/${machine}.ini"
echo "BUILD_OS=${INPUTS_DISTRO_NAME}${INPUTS_KERNEL}/" >> "${VARS_OUT_PATH}/${machine}.ini"
fi
if [ "${machine}" = "dragonboard-820c" ]; then
echo "ROOTFS_IMG_FILE=${IMAGE_TYPE}-qcom-armv8a.rootfs.ext4" >> "${VARS_OUT_PATH}/${machine}.ini"
echo "BOOT_IMG_FILE=boot-apq8096-db820c-qcom-armv8a.img" >> "${VARS_OUT_PATH}/${machine}.ini"
echo "ROOTFS_IMG_FILE=${IMAGE_TYPE}-${ARTIFACT_MACHINE}.rootfs.ext4" >> "${VARS_OUT_PATH}/${machine}.ini"
echo "BOOT_IMG_FILE=boot-apq8096-db820c-${ARTIFACT_MACHINE}.img" >> "${VARS_OUT_PATH}/${machine}.ini"
echo "BUILD_OS=${INPUTS_DISTRO_NAME}${INPUTS_KERNEL}/" >> "${VARS_OUT_PATH}/${machine}.ini"
fi

cat "${VARS_OUT_PATH}/${machine}.ini"
cat "${VARS_OUT_PATH}/${machine}.ini" "${MACHINE_META}"
# INPUTS_TESTPLAN contains distro name
lava-test-plans --dry-run --variables "${VARS_OUT_PATH}/${machine}.ini" --test-plan "${PROJECT}/${INPUTS_TESTPLAN}" --device-type "projects/${PROJECT}/devices/${machine}" --dry-run-path "${JOBS_OUT_PATH}/${machine}-${INPUTS_DISTRO_NAME}-${INPUTS_TESTPLAN}" || true
lava-test-plans --dry-run --variables "${VARS_OUT_PATH}/${machine}.ini" "${MACHINE_META}" --test-plan "${PROJECT}/${INPUTS_TESTPLAN}" --device-type "projects/${PROJECT}/devices/${machine}" --dry-run-path "${JOBS_OUT_PATH}/${machine}-${INPUTS_DISTRO_NAME}-${INPUTS_TESTPLAN}" || true
# create the JSON file that is used for the job submission matrix
find "${JOBS_OUT_PATH}/${machine}-${INPUTS_DISTRO_NAME}-${INPUTS_TESTPLAN}" -name "*.yaml"
for JOB_PATH in $( find "${JOBS_OUT_PATH}/${machine}-${INPUTS_DISTRO_NAME}-${INPUTS_TESTPLAN}" -name "*.yaml");
Expand Down
36 changes: 36 additions & 0 deletions .github/workflows/compile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,34 @@ jobs:
INPUTS_MACHINE: ${{ inputs.machine }}
INPUTS_DISTRO_NAME: ${{ inputs.distro_name }}
INPUTS_KERNEL_DIRNAME: ${{ inputs.kernel_dirname }}
- name: Extract build metadata from the image SBOM

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

turn it into action so you can install python dependencies properly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theres no python dependencies to install, all are in stdlib I believe

id: build-metadata
if: inputs.target
shell: bash
env:
INPUTS_MACHINE: ${{ inputs.machine }}
INPUTS_DISTRO_NAME: ${{ inputs.distro_name }}
INPUTS_KERNEL_DIRNAME: ${{ inputs.kernel_dirname }}
run: |
set -euo pipefail
uploads_dir="./uploads/${INPUTS_DISTRO_NAME}${INPUTS_KERNEL_DIRNAME}/${INPUTS_MACHINE}"
FILENAME="build-meta_${INPUTS_MACHINE}_${INPUTS_DISTRO_NAME}${INPUTS_KERNEL_DIRNAME}"
echo "filename=${FILENAME}" >> "$GITHUB_OUTPUT"
SPDX=()
for f in "${uploads_dir}"/*.spdx.json; do
if [ -e "${f}" ]; then
SPDX+=("${f}")
fi
done
if [ "${#SPDX[@]}" -eq 0 ]; then
echo "::warning::no image SBOM found under ${uploads_dir}"
echo '{}' > "${FILENAME}"
elif ! python3 ci/build-metadata.py "${SPDX[@]}" > "${FILENAME}"; then
echo "::warning::could not read build metadata from ${uploads_dir}"
echo '{}' > "${FILENAME}"
fi
cat "${FILENAME}"

- name: Set S3 object tag
run: |
if [[ "$GITHUB_EVENT_NAME" = "pull_request" ]]; then
Expand Down Expand Up @@ -248,3 +276,11 @@ jobs:
name: ${{ steps.print-output.outputs.filename }}
path: ${{ steps.print-output.outputs.filename }}

- name: Upload build metadata

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might take us above the allowed artifact threshold (again) quickly. I'm not sure how to prevent the problem. Maybe upload to S3?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Size or count? I can bundle this into the other artifact to keep the count down?

if: inputs.target
uses: actions/upload-artifact@v6
with:
overwrite: true
name: ${{ steps.build-metadata.outputs.filename }}
path: ${{ steps.build-metadata.outputs.filename }}

2 changes: 2 additions & 0 deletions .github/workflows/test-distro.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ jobs:
testkit_ref: ${{ inputs.testkit_ref }}
pr_number: ${{ inputs.pr_number }}
pr_url: ${{ inputs.pr_url }}
distro: ${{ inputs.distro_name }}

- name: Print output ID
env:
Expand Down Expand Up @@ -208,6 +209,7 @@ jobs:
testkit_ref: ${{ inputs.testkit_ref }}
pr_number: ${{ inputs.pr_number }}
pr_url: ${{ inputs.pr_url }}
distro: ${{ inputs.distro_name }}

- name: Print output ID
env:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ on:
description: "Token used to submit jobs to the LAVA lab"
env:
# git sha, tag or branch in lava-test-plans repository
LAVA_TEST_PLANS_REF: "6070e41a1472e3fd595ea7ac849f7c30f86a2282"
LAVA_TEST_PLANS_REF: "2f2900cc3e7f2215ce7db6b5bf67955a0782ea31"
# CalVer tag in qcom-linux-testkit repository (format: testkit-YYYY.MM.DD)
TESTKIT_REF: "testkit-2026.08.14"

Expand Down
116 changes: 116 additions & 0 deletions ci/build-metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#!/usr/bin/env python3
# Copyright (c) 2026 Qualcomm Innovation Center, Inc. All rights reserved.
# SPDX-License-Identifier: MIT

"""Collect build metadata from the image SPDX documents of a build.

oe-core inherits create-spdx by default, so every image build already writes
an SBOM recording each recipe's source URI and resolved revision. Reading the
metadata back out of it costs nothing at build time and avoids having the
build record the same facts twice.

Currently this reports the kernel: its recipe, version, release, repository
and commit.

Nothing here is specific to a kernel recipe: the anchor is the package named
by KERNEL_PACKAGE_NAME ("kernel" unless a layer overrides it), which
kernel.bbclass produces for every provider, qcom or not.

A build writes one document per image and not all of them describe a kernel:
an initramfs image installs kernel modules but no kernel package. Every
document given is therefore examined and the most complete answer wins.
"""
import json, re, sys

KNAME = "kernel"

def provenance(path):
doc = json.load(open(path))
graph = doc["@graph"]
packages = [e for e in graph if e.get("type") == "software_Package"]

kernel = next((p for p in packages if p.get("name") == KNAME), None)
if kernel is None:
# An image with no kernel package, such as an initramfs.
return {}

recipe = recipe_of(kernel)
mine = [p for p in packages if recipe_of(p) == recipe]
return describe(kernel, recipe, mine)


def recipe_of(entry):
"""The recipe whose SPDX document an entry belongs to."""
m = re.search(r"/spdxdocs/(.+?)-[0-9a-f]{8}-[0-9a-f]{4}-", entry.get("spdxId", ""))
return m.group(1) if m else None


def source_index(p):
"""Position of a source within SRC_URI.

create-spdx numbers sources by their 1-based index in SRC_URI, so a
recipe's own repository is source/1. linux-yocto fetches yocto-kernel-cache
as a second git source; the ordering separates them without having to
recognise the cache by name.
"""
m = re.search(r"/source/(\d+)$", p.get("spdxId", ""))
return int(m.group(1)) if m else 1 << 30


def describe(kernel, recipe, mine):
# Patches carry the purpose "patch" rather than "source", so what is left
# here is a real source and the lowest index is the recipe's own
# repository. Every source is still reported, so a surprise stays visible.
sources = sorted((p for p in mine
if p.get("software_primaryPurpose") == "source"),
key=source_index)
primary = sources[0] if sources else None

repo = commit = ""
if primary:
loc = primary.get("software_downloadLocation", "")
if loc.startswith("git+"):
# git+<url>@<full sha>; a tarball kernel has no revision to report.
repo, _, commit = loc[4:].partition("@")
else:
repo = loc

# kernel.bbclass names one package "<KERNEL_PACKAGE_NAME>-<KERNEL_VERSION>".
# Keying off that rather than "kernel-image-<type>" keeps this independent of
# KERNEL_IMAGETYPE, which differs per machine (Image, Image.gz, zImage).
release = ""
for p in mine:
m = re.fullmatch(re.escape(KNAME) + r"-(\d.*)", p.get("name") or "")
if m:
release = m.group(1)
break

return {
"kernel-recipe": recipe or "",
"kernel-version": kernel.get("software_packageVersion", ""),
"kernel-release": release,
"kernel-repo": repo,
"kernel-commit": commit,
**({"kernel-all-sources": [p.get("software_downloadLocation", "") for p in sources]}
if len(sources) > 1 else {}),
}


def completeness(result):
"""A result with a commit beats one with only a release, beats nothing."""
return (bool(result.get("kernel-commit")), bool(result.get("kernel-release")))


best = {}
for path in sorted(sys.argv[1:]):
try:
result = provenance(path)
except (OSError, ValueError, KeyError):
continue
if completeness(result) > completeness(best):
best = result
if best.get("kernel-commit"):
# Nothing better to find; skip parsing the remaining documents.
break

print(json.dumps(best, indent=2))
Loading