diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000000..db59dc94a0a53 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000000..d61fb1ab7ce6c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE/pr_template.md b/.github/PULL_REQUEST_TEMPLATE/pr_template.md new file mode 100644 index 0000000000000..2a2767f6f43f5 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pr_template.md @@ -0,0 +1,27 @@ +## Pull Request + +**Description** +A clear and concise description of what this pull request does. + +**Related Issue** +Link to the issue that this pull request addresses (e.g., `Fixes #123`). + +**Type of Change** +Please delete options that are not relevant. +- Bug fix (non-breaking change which fixes an issue) +- New feature (non-breaking change which adds functionality) +- Breaking change (fix or feature that would cause existing functionality to not work as expected) +- Documentation update + +**Checklist** +- [ ] My code follows the style guidelines of this project +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes +- [ ] Any dependent changes have been merged and published in downstream modules + +**Additional Context** +Add any other context or screenshots about the pull request here. diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000000000..13150bcfe23d7 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "github-actions" # See documentation for possible values + directory: "/" # This points to .github/workflows + schedule: + interval: "daily" diff --git a/.github/workflows/Readme.md b/.github/workflows/Readme.md new file mode 100644 index 0000000000000..8dc5e2c687383 --- /dev/null +++ b/.github/workflows/Readme.md @@ -0,0 +1,6 @@ +# List of workflows and actions +This folder contains workflows that are helpful for maintaining a smooth and secure development process. The workflows should be enabled for open-source projects. + +Workflows: +1. `qcom-preflight-checks.yml` - This workflow runs several preflight checks, including copyight, email, repolinter, and security checks. See [qualcomm/qcom-actions](https://github.com/qualcomm/qcom-actions) +2. `stale-issues.yaml` - This workflow will periodically run every 30 days to check for stalled issues and PRs. If the workflow detects any stalled issues and/or PRs, it will automatically leave just a comment to draw attention. diff --git a/.github/workflows/bootstrap-history.yml b/.github/workflows/bootstrap-history.yml new file mode 100644 index 0000000000000..fd1c9a3f2a84d --- /dev/null +++ b/.github/workflows/bootstrap-history.yml @@ -0,0 +1,186 @@ +# SPDX-License-Identifier: BSD-3-Clause +# +# bootstrap-history.yml +# +# One-time, automated bootstrap of a suite branch with the COMPLETE Canonical +# kernel history from Launchpad. Run this ONCE per suite (via "Run workflow"); +# thereafter fetch-source-pkg.yml keeps it current incrementally. +# +# This exists because the steady-state sync is incremental and therefore needs a +# real-history base to exist first, but the branch starts with no usable history +# (the legacy pipeline stored flattened snapshots with no upstream ancestry). +# +# How it works (single shot) +# ────────────────────────── +# scripts/seed-history.sh does a FULL `git clone` of the upstream history +# (Launchpad's shallow/deepen path is broken, but it can serve a full clone), +# then pushes the seed in <2 GB slices (GitHub caps a single push at 2 GB) with +# the upstream upload tags mirrored verbatim. +# +# * The seed is pushed to a -seed branch (e.g. resolute-qcom-seed), +# NEVER the live branch -- a human reviews and promotes it. +# * The cloned repo is cached (actions/cache); if the publish fails, a manual +# re-dispatch restores the cache and skips re-downloading the history. + +name: "Bootstrap: Seed Canonical Kernel History" + +on: + workflow_dispatch: + inputs: + suite: + description: "Suite to seed (e.g. resolute-qcom, noble, questing, resolute). Pushed to -seed." + required: false + default: "resolute-qcom" + type: string + custom_git_url: + description: "Git URL to seed from. Leave empty to use the official Ubuntu kernel repo for the base suite." + required: false + default: "" + type: string + +permissions: + contents: write # push the seeded branch and tags + +jobs: + bootstrap: + name: "Seed ${{ inputs.suite }}-seed" + runs-on: ubuntu-24.04-arm + timeout-minutes: 350 + + steps: + # ----------------------------------------------------------------------- + # 1. Free disk space -- a full kernel history is multi-GB. + # ----------------------------------------------------------------------- + - name: Free up runner disk space + run: | + sudo rm -rf \ + /usr/share/dotnet /usr/local/lib/android /opt/ghc \ + /opt/hostedtoolcache/CodeQL /usr/local/share/boost \ + "$AGENT_TOOLSDIRECTORY" 2>/dev/null || true + sudo apt-get clean + df -h / + + # ----------------------------------------------------------------------- + # 2. Check out this repo just for the scripts/ helpers. + # ----------------------------------------------------------------------- + - name: Checkout CI scripts + uses: actions/checkout@v6 + + # ----------------------------------------------------------------------- + # 3. Resolve the seed configuration (URL, tag prefix, seed branch). + # ----------------------------------------------------------------------- + - name: Resolve configuration + id: cfg + env: + SUITE: ${{ inputs.suite || 'resolute-qcom' }} + CUSTOM_GIT_URL: ${{ inputs.custom_git_url }} + run: | + BASE_SUITE="$(echo "${SUITE}" | cut -d'-' -f1)" + + if [ -n "${CUSTOM_GIT_URL}" ]; then + UPSTREAM_URL="${CUSTOM_GIT_URL}" + UPSTREAM_PREFIX="Ubuntu-qcom" + elif echo "${SUITE}" | grep -q '-'; then + # A custom (suffixed) suite with no URL given defaults to the + # Qualcomm-Ubuntu carmel-team repository for that base suite. + UPSTREAM_URL="https://git.launchpad.net/~carmel-team/ubuntu/+source/linux/+git/${BASE_SUITE}" + UPSTREAM_PREFIX="Ubuntu-qcom" + else + UPSTREAM_URL="https://git.launchpad.net/~ubuntu-kernel/ubuntu/+source/linux/+git/${BASE_SUITE}" + UPSTREAM_PREFIX="Ubuntu" + fi + + { + echo "suite=${SUITE}" + echo "upstream_url=${UPSTREAM_URL}" + echo "upstream_prefix=${UPSTREAM_PREFIX}" + echo "seed_branch=${SUITE}-seed" + echo "cache_path=${RUNNER_TEMP}/history-seed" + } >> "$GITHUB_OUTPUT" + + # Seed pushed to -seed (NOT the live branch); tags mirrored verbatim. + echo "Suite ${SUITE} -> seed branch ${SUITE}-seed from ${UPSTREAM_URL} (tags ${UPSTREAM_PREFIX}-* verbatim)" + + # ----------------------------------------------------------------------- + # 4. Restore a previously-cloned seed (so a retry after a failed publish + # skips re-downloading the multi-GB history). + # ----------------------------------------------------------------------- + - name: Restore cached clone + uses: actions/cache/restore@v4 + with: + path: ${{ steps.cfg.outputs.cache_path }} + key: history-seed-${{ steps.cfg.outputs.suite }}-${{ github.run_id }} + restore-keys: | + history-seed-${{ steps.cfg.outputs.suite }}- + + # ----------------------------------------------------------------------- + # 5. Full-clone the history and publish the seed. Capture the exit code so + # the clone can be cached before the job is failed on a publish error. + # ----------------------------------------------------------------------- + - name: Seed history + id: seed + env: + MIRROR_URL: https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.git + UPSTREAM_URL: ${{ steps.cfg.outputs.upstream_url }} + BRANCH: ${{ steps.cfg.outputs.seed_branch }} + UPSTREAM_PREFIX: ${{ steps.cfg.outputs.upstream_prefix }} + WORKDIR: ${{ steps.cfg.outputs.cache_path }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + + set +e + bash scripts/seed-history.sh + rc=$? + set -e + + echo "rc=${rc}" >> "$GITHUB_OUTPUT" + if [ "${rc}" -eq 0 ]; then + echo "status=complete" >> "$GITHUB_OUTPUT" + else + echo "status=failed" >> "$GITHUB_OUTPUT" + fi + + # ----------------------------------------------------------------------- + # 6. On failure, cache the cloned history so a manual re-dispatch can reuse + # it instead of re-downloading from the slow Launchpad server. + # ----------------------------------------------------------------------- + - name: Cache clone for retry + if: steps.seed.outputs.status == 'failed' + uses: actions/cache/save@v4 + with: + path: ${{ steps.cfg.outputs.cache_path }} + key: history-seed-${{ steps.cfg.outputs.suite }}-${{ github.run_id }} + + # ----------------------------------------------------------------------- + # 7. Surface a publish/clone failure as a red job, AFTER the clone is cached. + # ----------------------------------------------------------------------- + - name: Fail on seed error + if: steps.seed.outputs.status == 'failed' + env: + SEED_RC: ${{ steps.seed.outputs.rc }} + run: | + echo "::error::seed-history.sh failed (rc=${SEED_RC})." \ + "The clone is cached; investigate and re-dispatch to retry the publish." + exit 1 + + # ----------------------------------------------------------------------- + # 8. Summary. + # ----------------------------------------------------------------------- + - name: Print summary + if: always() + env: + SEED_BRANCH: ${{ steps.cfg.outputs.seed_branch }} + SEED_STATUS: ${{ steps.seed.outputs.status || 'failed' }} + run: | + { + echo "## Bootstrap Summary" + echo "" + echo "| Field | Value |" + echo "|-------|-------|" + echo "| Seed branch | \`${SEED_BRANCH}\` |" + echo "| Status | \`${SEED_STATUS}\` |" + if [ "${SEED_STATUS}" = "complete" ]; then + echo "| Result | Seed pushed -- review and promote \`${SEED_BRANCH}\` to the live branch |" + fi + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/build-kernel.yml b/.github/workflows/build-kernel.yml new file mode 100644 index 0000000000000..07fddad7bd2f0 --- /dev/null +++ b/.github/workflows/build-kernel.yml @@ -0,0 +1,500 @@ +# SPDX-License-Identifier: BSD-3-Clause +# +# build-kernel.yml +# +# Builds Ubuntu kernel .deb packages from a branch +# (e.g. "noble", "resolute-qcom") that was populated by fetch-source-pkg.yml. +# +# The build always runs inside the base-suite-matched +# ghcr.io/qualcomm-linux/pkg-builder: container via +# docker-pkg-build, ensuring a clean, reproducible environment +# that exactly matches the target distro. +# +# Output .deb packages are uploaded ONLY to the S3 bucket +# (qli-prd-lecore-gh-artifacts), on the lecore-production runner. No GitHub +# Actions artifacts and no GitHub Releases are produced. + +name: "Build: Canonical Kernel .deb Packages" + +on: + # Daily build at 15:00 PST (23:00 UTC; GitHub cron runs in UTC). A scheduled run + # passes no inputs, so it builds resolute-qcom-devel HEAD (the SUITE default) on + # the lecore-production runner and uploads the .deb packages to S3 (skip_s3 is + # unset, so the upload step runs). + schedule: + - cron: "0 23 * * *" + + workflow_dispatch: + inputs: + suite: + description: "Branch to build from (e.g. resolute-qcom-devel, resolute-qcom)" + required: true + default: "resolute-qcom-devel" + type: string + kernel_version: + description: "Version (e.g. 7.0.0-1006.8) - builds the exact Ubuntu-qcom- tag. Empty = branch HEAD." + required: false + type: string + devel_prs: + description: "Space-separated PR numbers against resolute-qcom-devel to merge before building (e.g. 42 43)" + required: false + default: "" + type: string + dbgsym: + description: "Build the -dbgsym.ddeb debug symbol packages alongside the .deb packages." + required: false + default: true + type: boolean + flavours: + description: "ALL builds every flavour. Otherwise a space-separated list of flavour names (e.g. 'qcom', 'qcom qcom-rt'), no 'binary-' prefix." + required: false + default: "ALL" + type: string + + workflow_call: + inputs: + suite: + description: "Branch to build from" + required: false + default: "resolute-qcom-devel" + type: string + kernel_version: + description: "Version tag to build. Empty = branch HEAD." + required: false + default: "" + type: string + devel_prs: + description: "Space-separated PR numbers against resolute-qcom-devel to merge before building" + required: false + default: "" + type: string + dbgsym: + description: "Build the -dbgsym.ddeb debug symbol packages alongside the .deb packages." + required: false + default: true + type: boolean + ref: + description: "Exact git ref to checkout. Overrides suite/kernel_version when set (used by premerge-pr.yml to build the PR merge ref)." + required: false + default: "" + type: string + skip_s3: + description: "Skip uploading built .deb packages to S3 (used by premerge-pr.yml for build-only checks)." + required: false + default: false + type: boolean + flavours: + description: "ALL builds every flavour. Otherwise a space-separated list of flavour names (e.g. 'qcom', 'qcom qcom-rt'), no 'binary-' prefix." + required: false + default: "ALL" + type: string + s3_prefix: + description: "S3 path prefix under pkg/ (e.g. 'premerge', 'temp'). Callers must pass this explicitly." + required: false + default: "temp" + type: string + +permissions: + contents: read # checkout + tag ls-remote only; output goes to S3 + +jobs: + # -------------------------------------------------------------------------- + # Job: build + # -------------------------------------------------------------------------- + build: + name: "Build" + # Runner is always lecore-production for this repo. + runs-on: ["self-hosted", "lecore-prd-u2404-arm64-xlrg-od-ephem"] + timeout-minutes: 360 + + env: + SUITE: ${{ inputs.suite || 'resolute-qcom-devel' }} + ARCH: arm64 + FLAVOURS: ${{ inputs.flavours || 'ALL' }} + DBGSYM: ${{ github.event_name == 'schedule' && 'true' || (inputs.dbgsym && 'true' || 'false') }} + + steps: + # ----------------------------------------------------------------------- + # 1. Free up disk space + # GitHub-hosted ubuntu-24.04-arm runners ship with ~14 GB free. + # The kernel build needs ~20 GB; removing unused toolchains gives + # enough headroom. + # ----------------------------------------------------------------------- + - name: Free up runner disk space + run: | + echo "Disk before cleanup:" + df -h / + sudo rm -rf \ + /usr/share/dotnet \ + /usr/local/lib/android \ + /opt/ghc \ + /opt/hostedtoolcache/CodeQL \ + /usr/local/share/boost \ + "$AGENT_TOOLSDIRECTORY" \ + 2>/dev/null || true + sudo apt-get clean + echo "Disk after cleanup:" + df -h / + + # ----------------------------------------------------------------------- + # 2. Extract org/repo for S3 upload path + # ----------------------------------------------------------------------- + - name: Extract org and repo name + run: | + echo "ORG_NAME=${GITHUB_REPOSITORY%%/*}" >> "$GITHUB_ENV" + echo "REPO_NAME=${GITHUB_REPOSITORY#*/}" >> "$GITHUB_ENV" + + # ----------------------------------------------------------------------- + # 2b. Derive base suite (for Docker image) and upstream tag prefix. + # BASE_SUITE strips any custom suffix so the correct pkg-builder image + # is used (resolute-qcom → resolute). UPSTREAM_PREFIX is the verbatim + # Canonical tag prefix the source branch was mirrored with + # (custom/qcom suites → Ubuntu-qcom; official suites → Ubuntu). + # ----------------------------------------------------------------------- + - name: Derive base suite and tag prefix + run: | + BASE_SUITE=$(echo "${SUITE}" | cut -d'-' -f1) + echo "BASE_SUITE=${BASE_SUITE}" >> "$GITHUB_ENV" + if echo "${SUITE}" | grep -q '-'; then + echo "UPSTREAM_PREFIX=Ubuntu-qcom" >> "$GITHUB_ENV" + else + echo "UPSTREAM_PREFIX=Ubuntu" >> "$GITHUB_ENV" + fi + echo "Suite: ${SUITE} → Base suite: ${BASE_SUITE}" + + # ----------------------------------------------------------------------- + # 3. Validate kernel_version against existing tags (fail fast) + # Must run BEFORE checkout so a bad version fails immediately with a + # clear error rather than a cryptic git "ref not found" from checkout. + # ----------------------------------------------------------------------- + - name: Validate kernel_version tag exists + if: inputs.kernel_version != '' + env: + GH_TOKEN: ${{ github.token }} + KERNEL_VERSION: ${{ inputs.kernel_version }} + run: | + TAG="${UPSTREAM_PREFIX}-${KERNEL_VERSION}" + echo "Verifying tag '${TAG}' exists in repository..." + + if git ls-remote --exit-code --tags \ + "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" \ + "refs/tags/${TAG}" >/dev/null 2>&1; then + echo "✓ Tag '${TAG}' exists — proceeding with build" + else + echo "ERROR: Tag '${TAG}' does not exist in this repository." >&2 + echo "" >&2 + echo "The kernel_version input must match a version that has been" >&2 + echo "synced to the ${SUITE} branch by fetch-source-pkg.yml." >&2 + echo "" >&2 + echo "Existing tags: https://github.com/${{ github.repository }}/tags" >&2 + exit 1 + fi + + # ----------------------------------------------------------------------- + # 4. Checkout the kernel source + # - If kernel_version is provided: checkout the exact verbatim Canonical + # tag - (e.g. Ubuntu-qcom-7.0.0-1006.8) + # so the build uses the precise synced source. + # - If kernel_version is empty: checkout the suite branch HEAD + # (test/dev builds). + # Note: actions/checkout checks out a ref of THIS repository + # (pkg-linux-qcom-canonical). The resolute-qcom/noble/... branches of + # this repo contain the full kernel source tree mirrored by + # fetch-source-pkg.yml — they are not the main branch. + # ----------------------------------------------------------------------- + - name: Checkout kernel source + uses: actions/checkout@v6 + with: + ref: ${{ inputs.ref != '' && inputs.ref || (inputs.kernel_version != '' && format('{0}-{1}', env.UPSTREAM_PREFIX, inputs.kernel_version) || inputs.suite || 'resolute-qcom-devel') }} + path: kernel-src + + # ----------------------------------------------------------------------- + # 4b. Merge additional PRs against resolute-qcom-devel into the + # checked-out source tree (engineering builds only). + # Each PR number is validated as numeric before use. Conflicts abort + # the build immediately with a clear error. + # ----------------------------------------------------------------------- + - name: Merge devel PRs into source tree + if: inputs.devel_prs != '' + env: + DEVEL_PRS: ${{ inputs.devel_prs }} + GH_TOKEN: ${{ github.token }} + run: | + cd kernel-src/ + git config user.email "github-actions[bot]@users.noreply.github.com" + git config user.name "github-actions[bot]" + git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" + + echo "PRs to merge: ${DEVEL_PRS}" + for pr in ${DEVEL_PRS}; do + [[ "${pr}" =~ ^[0-9]+$ ]] || { + echo "ERROR: invalid PR number '${pr}' -- must be numeric" >&2 + exit 1 + } + + # Validate: the PR must exist, be open, and target the suite branch. + # A closed or wrong-base PR number would otherwise be merged silently. + info="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${pr}" \ + --jq '[.state, .base.ref] | @tsv')" || { + echo "ERROR: PR #${pr} not found in ${GITHUB_REPOSITORY}." >&2 + exit 1 + } + state="$(printf '%s' "${info}" | cut -f1)" + base="$(printf '%s' "${info}" | cut -f2)" + [ "${state}" = "open" ] || { + echo "ERROR: PR #${pr} is '${state}' -- only open PRs can be merged." >&2 + exit 1 + } + [ "${base}" = "${SUITE}" ] || { + echo "ERROR: PR #${pr} targets '${base}', not '${SUITE}'." >&2 + exit 1 + } + + echo "::group::Merging ${SUITE} PR #${pr}" + # The build checkout is shallow (depth 1), so the merge base with the + # PR is not present. Fetch the PR head and deepen BOTH histories until + # a merge base exists - PRs fork from a recent tip, so this stays a + # small fetch and never pulls the full ~1.43M-commit history. + git fetch --no-tags --depth=1 origin "refs/pull/${pr}/head:refs/pr/${pr}" + step=100; total=0; max=6400 + until git merge-base HEAD "refs/pr/${pr}" >/dev/null 2>&1; do + if [ "${total}" -ge "${max}" ]; then + echo "ERROR: no common ancestor with PR #${pr} within ${max} commits." >&2 + echo "Rebase the PR onto ${SUITE} and retry." >&2 + exit 1 + fi + echo " deepening history by ${step} commits to find the merge base..." + git fetch --deepen="${step}" origin "refs/heads/${SUITE}" "refs/pull/${pr}/head" + total=$((total + step)); step=$((step * 2)) + done + + if ! git merge --no-ff --no-commit "refs/pr/${pr}"; then + echo "ERROR: merge conflict while merging PR #${pr}. Aborting." >&2 + git merge --abort || true + exit 1 + fi + if git diff --cached --quiet; then + echo "PR #${pr} already present in tree -- nothing to merge." + git merge --abort >/dev/null 2>&1 || true + else + git commit -m "Merged ${SUITE} PR #${pr}" + echo "PR #${pr} merged successfully." + fi + echo "::endgroup::" + done + + # ----------------------------------------------------------------------- + # 5. Checkout docker-pkg-build + # ----------------------------------------------------------------------- + - name: Checkout docker-pkg-build + uses: actions/checkout@v6 + with: + repository: qualcomm-linux/docker-pkg-build + ref: main + path: docker-pkg-build + + # ----------------------------------------------------------------------- + # 6. Build the base-suite-matched pkg-builder docker image. + # Uses BASE_SUITE (e.g. resolute) not SUITE (e.g. resolute-qcom) + # because pkg-builder images are tagged by Ubuntu distro name only. + # ----------------------------------------------------------------------- + - name: Build docker image for suite (${{ inputs.suite || 'resolute-qcom' }}) + run: | + ./docker-pkg-build/docker_deb_build.py --rebuild -d "${BASE_SUITE}" + docker image ls + + # ----------------------------------------------------------------------- + # 7. Build kernel packages inside the suite-matched container + # The workspace is bind-mounted so output .deb files land on the host. + # ----------------------------------------------------------------------- + - name: Build kernel packages (ghcr.io/qualcomm-linux/pkg-builder:${{ inputs.suite || 'resolute-qcom' }}) + run: | + JOBS=$(nproc) + FLAVOURS_LC=$(echo "${FLAVOURS}" | tr '[:upper:]' '[:lower:]') + # binary-indep is always built alongside the selected flavours — + # linux-headers-* and linux-tools-* packages depend on it, so it + # can't be made optional here. + if [ "${FLAVOURS_LC}" = "all" ]; then + TARGET="binary" + else + TARGET="binary-indep" + for f in ${FLAVOURS_LC}; do + TARGET="${TARGET} binary-${f}" + done + fi + + echo "Building: suite=${SUITE} base_suite=${BASE_SUITE} target=${TARGET} arch=${ARCH} jobs=${JOBS}" + + # IMPORTANT: use single quotes for bash -c so the outer shell does NOT + # expand $(...) or ${VAR} before passing the string to docker. + # JOBS, TARGET, and WS are passed as -e env vars and expanded inside + # the container by the container's own bash. + docker run -i --privileged --rm \ + -v "${GITHUB_WORKSPACE}:${GITHUB_WORKSPACE}" \ + --workdir="${GITHUB_WORKSPACE}" \ + -e JOBS="${JOBS}" \ + -e TARGET="${TARGET}" \ + -e WS="${GITHUB_WORKSPACE}" \ + -e DBGSYM="${DBGSYM}" \ + "ghcr.io/qualcomm-linux/pkg-builder:${BASE_SUITE}" \ + bash -c ' + set -euo pipefail + + # Enable deb-src so apt-get build-dep can resolve build dependencies. + # pkg-builder:noble is Ubuntu 24.04 which uses DEB822 format. + sed -i "s/^Types: deb$/Types: deb deb-src/" /etc/apt/sources.list.d/ubuntu.sources + apt-get update -qq + apt-get build-dep -y linux + + cd kernel-src/ + echo "debian/debian.env: $(cat debian/debian.env 2>/dev/null || echo NOT FOUND)" + ls -la debian/rules && echo "debian/rules: OK" || { echo "debian/rules: NOT FOUND"; exit 1; } + + # Heartbeat every 2 min — kernel build takes 60-90 min with minimal output + while true; do echo "[$(date -u +%H:%M:%S)] kernel build still running..."; sleep 120; done & + HEARTBEAT=$! + + # Run the clean target to set up the full build environment before + # compilation. This is the standard Ubuntu kernel build setup path: + # - runs debian/control as a dependency, generating + # debian/canonical-certs.pem (required by certs/x509_certificate_list) + # and debian/control + # - creates debian/changelog → debian.master/changelog symlink + # (required by dh_installchangelogs at the end of binary-generic) + # - removes any stale build artifacts + # + # Use "fakeroot make -f debian/rules" rather than "fakeroot debian/rules": + # fakeroot is a shell script that execs the command via /bin/sh (dash); + # dash resolves the shebang (#!/usr/bin/make -f) and reports "not found" + # if make is not yet on PATH at exec time. Invoking make explicitly + # bypasses the shebang lookup entirely. + echo "=== Setting up build environment (debian/rules clean) ===" + fakeroot make -f debian/rules clean + + # Safety net: ensure debian/changelog symlink exists. + # The clean target should create it as part of standard setup, + # but if it does not (container environment difference), the + # build would fail at dh_installchangelogs after 2+ hours. + # This guard is a no-op when clean already created the symlink. + if [ ! -f debian/changelog ] && [ -f debian.master/changelog ]; then + ln -sf ../debian.master/changelog debian/changelog + echo "Safety net: created debian/changelog → debian.master/changelog" + fi + + echo "=== Starting: fakeroot debian/rules ${TARGET} (jobs=${JOBS}) ===" + export DEB_BUILD_OPTIONS="parallel=${JOBS} nocheck" + # do_skip_checks=true skips the config policy check that requires + # CONFIG_RUST_IS_AVAILABLE=y — bindgen-0.65 is not available in + # the container so Rust support is unavailable, causing the check + # to fail. This is the standard approach for non-official builds. + # do_dbgsym_package=true additionally builds the unstripped + # -dbgsym.ddeb (vmlinux + modules with full debug symbols). + DBGSYM_OPT="do_dbgsym_package=false" + if [ "${DBGSYM}" = "true" ]; then DBGSYM_OPT="do_dbgsym_package=true"; fi + fakeroot debian/rules ${TARGET} do_skip_checks=true "${DBGSYM_OPT}" do_fitimage=false + + kill "${HEARTBEAT}" 2>/dev/null || true + + echo "=== Locating built .deb files ===" + find "${WS}" -name "*.deb" -o -name "*.ddeb" | sort || true + ' + + # ----------------------------------------------------------------------- + # 8. Collect output packages + # The Ubuntu kernel build drops .deb files one level above the + # source tree (i.e. in GITHUB_WORKSPACE, not inside kernel-src/). + # qcom-rt flavour packages are split into output/rt/ so consumers can + # tell the two flavours apart; qcom flavour packages stay in output/. + # output/rt/ is only created on demand (when a qcom-rt artifact is + # actually found below) so a build that didn't select qcom-rt + # doesn't produce an empty/misleading rt directory. + # + # linux-qcom-headers-* and linux-qcom-tools-* are "Architecture: all" + # at the source level, so there is a single shared .deb per ABI + # rather than one per flavour, and they land in output/ (they don't + # match the -qcom-rt filename pattern above). linux-headers-*-qcom-rt + # depends on linux-qcom-headers-*, and a future linux-tools-*-qcom-rt + # would similarly depend on linux-qcom-tools-*, so both are also + # copied (not moved — qcom needs its own copy in output/ too) into + # output/rt/ to keep that folder self-contained — but again, only + # if output/rt/ already exists (i.e. qcom-rt was actually built). + # ----------------------------------------------------------------------- + - name: Collect built packages + run: | + mkdir -p output/ + + find "${GITHUB_WORKSPACE}" -maxdepth 1 \ + \( -name "*.deb" -o -name "*.ddeb" -o -name "*.changes" -o -name "*.buildinfo" \) \ + -print0 | while IFS= read -r -d '' f; do + case "$(basename "$f")" in + *-qcom-rt_*|*-qcom-rt-dbgsym_*) mkdir -p output/rt/; cp "$f" output/rt/ ;; + *) cp "$f" output/ ;; + esac + done + + if [ -d output/rt ]; then + find output -maxdepth 1 \ + \( -name "linux-qcom-headers-*" -o -name "linux-qcom-tools-*" \) \ + -print0 | while IFS= read -r -d '' f; do + cp "$f" output/rt/ + done + fi + + echo "Built packages (qcom):" + ls -lh output/ 2>/dev/null | grep -v '^total\|^d' || echo "(no files found)" + if [ -d output/rt ]; then + echo "Built packages (qcom-rt):" + ls -lh output/rt/ || echo "(no files found)" + fi + + # ----------------------------------------------------------------------- + # 9. Upload to S3 (lecore-production runner only) + # Skipped when skip_s3 is set (premerge-pr.yml build-only checks). + # Destination prefix is the caller-supplied s3_prefix input, not + # inferred from github.event_name: a reusable workflow inherits the + # caller's original event, so it is never actually "workflow_call". + # ----------------------------------------------------------------------- + - name: Upload kernel .deb packages to S3 + if: inputs.skip_s3 != true + uses: qualcomm-linux/upload-private-artifact-action@aws-v4 + with: + s3_bucket: qli-prd-lecore-gh-artifacts + path: ${{ github.workspace }}/output + destination: ${{ env.ORG_NAME }}/pkg/${{ inputs.s3_prefix || 'temp' }}/${{ env.REPO_NAME }}/${{ github.run_id }}-${{ github.run_attempt }}/ + + # ----------------------------------------------------------------------- + # 10. Summary + # ----------------------------------------------------------------------- + - name: Print summary + if: always() + env: + DEVEL_PRS: ${{ inputs.devel_prs }} + RUNNER_NAME: ${{ runner.name }} + run: | + { + echo "## Kernel Build Summary" + echo "" + echo "| Field | Value |" + echo "|-------|-------|" + echo "| Branch | \`${SUITE}\` |" + echo "| Architecture | \`${ARCH}\` |" + echo "| Flavour | \`${FLAVOURS}\` |" + echo "| Dbgsym | \`${DBGSYM}\` |" + echo "| Container | \`ghcr.io/qualcomm-linux/pkg-builder:${BASE_SUITE}\` |" + echo "| Runner | \`${RUNNER_NAME}\` |" + if [ -n "${DEVEL_PRS}" ]; then + echo "| Devel PRs | \`${DEVEL_PRS}\` |" + fi + echo "" + echo "### Built packages (qcom)" + echo '```' + ls -lh output/ 2>/dev/null | grep -v '^total\|^d' || echo "(none)" + echo '```' + if [ -d output/rt ]; then + echo "" + echo "### Built packages (qcom-rt)" + echo '```' + ls -lh output/rt/ + echo '```' + fi + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/fetch-source-pkg.yml b/.github/workflows/fetch-source-pkg.yml new file mode 100644 index 0000000000000..9b5eb4aaa35ef --- /dev/null +++ b/.github/workflows/fetch-source-pkg.yml @@ -0,0 +1,165 @@ +# SPDX-License-Identifier: BSD-3-Clause +# +# fetch-source-pkg.yml +# +# Mirrors the latest Qualcomm-Ubuntu kernel upload from the carmel-team Launchpad +# repository into the resolute-qcom branch (preserving full upstream history), +# then triggers a kernel build. +# +# This repo mirrors exactly ONE source into ONE branch, so the workflow takes no +# inputs -- everything is fixed: +# upstream : git.launchpad.net/~carmel-team/ubuntu/+source/linux/+git/resolute +# branch : resolute-qcom +# tags : Ubuntu-qcom-X.Y.Z-A.B (mirrored verbatim from upstream) +# +# History-preserving "mirror-repoint" model +# * resolute-qcom is a movable "latest Canonical" pointer. +# * The upstream Ubuntu-qcom-X.Y.Z-A.B tag is mirrored verbatim as the immutable +# per-upload record. +# * A sync is pure fetch + repoint (scripts/sync-mirror.sh): it never merges or +# rebases, so it cannot conflict. +# +# Prerequisite: resolute-qcom must first be seeded with full history by the +# "Bootstrap: Seed Canonical Kernel History" workflow. This job is incremental +# and refuses to run against an un-seeded branch. + +name: "Sync: Canonical Kernel Sources to Branch" +run-name: "Mirror latest Ubuntu-qcom upload into resolute-qcom" + +on: + workflow_dispatch: # manual only; always mirrors the latest carmel-team upload + +permissions: + contents: read + +# Only one sync at a time -- a second run's lease-pinned push would be rejected. +concurrency: + group: mirror-sync-resolute-qcom + cancel-in-progress: false + +env: + BRANCH: resolute-qcom + UPSTREAM_URL: "https://git.launchpad.net/~carmel-team/ubuntu/+source/linux/+git/resolute" + UPSTREAM_PREFIX: Ubuntu-qcom + +jobs: + # ========================================================================== + # Job 1 - cheap "is there a new upload?" gate (no clone). + # ========================================================================== + check-version: + name: "Check for new uploads" + runs-on: ubuntu-24.04-arm + timeout-minutes: 10 + outputs: + should_sync: ${{ steps.gate.outputs.should_sync }} + steps: + - name: Gate on un-mirrored uploads + id: gate + env: + MIRROR_URL: https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.git + run: | + # Compare upstream upload tags against our mirrored tags via two cheap + # ls-remote calls (no clone). should_sync is true iff any upstream + # upload has no matching tag here. Capture ls-remote first so a transport + # failure is not mistaken for "upstream has no tags". + up_raw="$(git ls-remote --tags "${UPSTREAM_URL}" "refs/tags/${UPSTREAM_PREFIX}-*")" \ + || { echo "::error::git ls-remote failed for upstream (network/auth?)"; exit 1; } + mapfile -t UP < <( + printf '%s\n' "${up_raw}" \ + | grep -v '\^{}' | sed -E "s#.*refs/tags/${UPSTREAM_PREFIX}-##" | sort -V + ) + [ "${#UP[@]}" -gt 0 ] || { echo "No ${UPSTREAM_PREFIX}-* tags upstream"; exit 1; } + LATEST="${UP[-1]}" + + OURS="$( + git ls-remote --tags "${MIRROR_URL}" "refs/tags/${UPSTREAM_PREFIX}-*" \ + | grep -v '\^{}' | sed -E "s#.*refs/tags/${UPSTREAM_PREFIX}-##" || true + )" + + # -F: match the version as a fixed string (dots are not wildcards). + SHOULD_SYNC=false + for ver in "${UP[@]}"; do + if ! grep -qxF "${ver}" <<<"${OURS}"; then SHOULD_SYNC=true; break; fi + done + + echo "should_sync=${SHOULD_SYNC}" >> "$GITHUB_OUTPUT" + echo "Latest upstream: ${LATEST}; new uploads to mirror: ${SHOULD_SYNC}" + + # ========================================================================== + # Job 2 - mirror every new upload (history-preserving), via sync-mirror.sh. + # ========================================================================== + sync: + name: "Mirror" + runs-on: ubuntu-24.04-arm + timeout-minutes: 60 + needs: check-version + if: needs.check-version.outputs.should_sync == 'true' + permissions: + contents: write + + outputs: + synced_version: ${{ steps.sync.outputs.synced_version }} + synced_count: ${{ steps.sync.outputs.synced_count }} + + steps: + - name: Free up runner disk space + run: | + sudo rm -rf \ + /usr/share/dotnet /usr/local/lib/android /opt/ghc \ + /opt/hostedtoolcache/CodeQL /usr/local/share/boost \ + "$AGENT_TOOLSDIRECTORY" 2>/dev/null || true + sudo apt-get clean + + - name: Checkout CI scripts + uses: actions/checkout@v6 + + # BRANCH, UPSTREAM_URL and UPSTREAM_PREFIX come from the workflow-level env + # above; sync-mirror.sh reads them directly. + - name: Mirror new uploads + id: sync + env: + MIRROR_URL: https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.git + WORKDIR: ${{ runner.temp }}/mirror-sync + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + bash scripts/sync-mirror.sh + + - name: Print summary + if: always() + env: + SYNCED_COUNT: ${{ steps.sync.outputs.synced_count || 0 }} + SYNCED_VERSION: ${{ steps.sync.outputs.synced_version }} + run: | + { + echo "## Sync Summary" + echo "" + echo "| Field | Value |" + echo "|-------|-------|" + echo "| Branch | \`${BRANCH}\` |" + echo "| Uploads mirrored | ${SYNCED_COUNT} |" + echo "| Latest version | \`${SYNCED_VERSION}\` |" + } >> "$GITHUB_STEP_SUMMARY" + + # ========================================================================== + # Job 3 - trigger a kernel build for the newest mirrored upload. + # ========================================================================== + trigger-build: + name: "Trigger kernel build" + runs-on: ubuntu-24.04-arm + needs: [check-version, sync] + if: needs.sync.result == 'success' && needs.sync.outputs.synced_count != '0' + permissions: + actions: write + + steps: + - name: Dispatch build-kernel workflow + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ needs.sync.outputs.synced_version }} + run: | + echo "Building ${BRANCH} ${VERSION} (flavour qcom)" + gh workflow run build-kernel.yml \ + --repo "${{ github.repository }}" \ + --field suite="${BRANCH}" \ + --field kernel_version="${VERSION}" diff --git a/.github/workflows/premerge-distro-validation.yml b/.github/workflows/premerge-distro-validation.yml new file mode 100644 index 0000000000000..3c530610dfc73 --- /dev/null +++ b/.github/workflows/premerge-distro-validation.yml @@ -0,0 +1,257 @@ +# SPDX-License-Identifier: BSD-3-Clause + +name: "Pre-merge distro image validation" + +on: # zizmor: ignore[dangerous-triggers] + workflow_run: + workflows: + - "Pre-merge PR build" + types: + - completed + + repository_dispatch: + types: + - canonical-premerge-distro-result + +permissions: + contents: read + +concurrency: + group: >- + premerge-distro-validation-${{ + github.event_name == 'repository_dispatch' && 'callback' || 'request' + }}-${{ + github.event.client_payload.pr_number || + github.event.workflow_run.head_repository.full_name || + github.repository + }}-${{ + github.event.workflow_run.head_branch || + github.run_id + }} + cancel-in-progress: true + +jobs: + workflow-context: + name: "Resolve kernel validation context" + if: ${{ github.event_name == 'workflow_run' }} + runs-on: [self-hosted, lecore-prd-u2404-arm64-xlrg-od-ephem] + outputs: + should-validate: ${{ steps.context.outputs.should-validate }} + kernel-conclusion: ${{ steps.context.outputs.kernel-conclusion }} + kernel-run-id: ${{ steps.context.outputs.kernel-run-id }} + kernel-run-attempt: ${{ steps.context.outputs.kernel-run-attempt }} + kernel-build-id: ${{ steps.context.outputs.kernel-build-id }} + kernel-s3-prefix: ${{ steps.context.outputs.kernel-s3-prefix }} + pr-number: ${{ steps.context.outputs.pr-number }} + head-sha: ${{ steps.context.outputs.head-sha }} + request-id: ${{ steps.context.outputs.request-id }} + steps: + - name: Checkout orchestration helpers + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + + - name: Install GitHub CLI + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y --no-install-recommends gh + + - id: context + name: Validate triggering workflow context + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + bash ./scripts/resolve-distro-validation-context.sh + + request-validation: + name: "Request distro image validation" + needs: workflow-context + if: ${{ needs.workflow-context.outputs.should-validate == 'true' }} + runs-on: [self-hosted, lecore-prd-u2404-arm64-xlrg-od-ephem] + permissions: + checks: write + contents: read + statuses: write + steps: + - name: Checkout orchestration helpers + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + + - name: Install GitHub CLI + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y --no-install-recommends gh + + - id: check + name: Start distro validation Check Run + env: + GH_TOKEN: ${{ github.token }} + MODE: start + REPOSITORY: ${{ github.repository }} + COMMIT_SHA: ${{ needs.workflow-context.outputs.head-sha }} + PR_NUMBER: ${{ needs.workflow-context.outputs.pr-number }} + REQUEST_ID: ${{ needs.workflow-context.outputs.request-id }} + KERNEL_BUILD_ID: ${{ needs.workflow-context.outputs.kernel-build-id }} + IMAGE_S3_PREFIX: qualcomm-linux/${{ needs.workflow-context.outputs.kernel-s3-prefix }}/${{ needs.workflow-context.outputs.kernel-build-id }} + DETAILS_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + set -euo pipefail + bash ./scripts/report-distro-validation-check.sh + + - name: Report validation pending + if: ${{ steps.check.outputs.should-dispatch == 'true' }} + env: + GH_TOKEN: ${{ github.token }} + REPOSITORY: ${{ github.repository }} + COMMIT_SHA: ${{ needs.workflow-context.outputs.head-sha }} + STATE: pending + DESCRIPTION: "Building Canonical server and desktop distro images" + TARGET_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + set -euo pipefail + bash ./scripts/report-distro-validation-status.sh + + - name: Dispatch trusted distro validation request + if: ${{ steps.check.outputs.should-dispatch == 'true' }} + env: + GH_TOKEN: ${{ secrets.DEB_PKG_BOT_CI_TOKEN }} + DISTRO_REPOSITORY: qualcomm-linux/qcom-distro-images + KERNEL_REPOSITORY: ${{ github.repository }} + KERNEL_RUN_ID: ${{ needs.workflow-context.outputs.kernel-run-id }} + KERNEL_RUN_ATTEMPT: ${{ needs.workflow-context.outputs.kernel-run-attempt }} + KERNEL_BUILD_ID: ${{ needs.workflow-context.outputs.kernel-build-id }} + KERNEL_S3_PREFIX: ${{ needs.workflow-context.outputs.kernel-s3-prefix }} + PR_NUMBER: ${{ needs.workflow-context.outputs.pr-number }} + HEAD_SHA: ${{ needs.workflow-context.outputs.head-sha }} + REQUEST_ID: ${{ needs.workflow-context.outputs.request-id }} + run: | + set -euo pipefail + bash ./scripts/send-distro-validation-request.sh + + kernel-failure: + name: "Report kernel validation failure" + needs: workflow-context + if: ${{ needs.workflow-context.outputs.kernel-conclusion != 'success' }} + runs-on: [self-hosted, lecore-prd-u2404-arm64-xlrg-od-ephem] + permissions: + contents: read + statuses: write + steps: + - name: Checkout status helper + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + + - name: Install GitHub CLI + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y --no-install-recommends gh + + - name: Publish failure status + env: + GH_TOKEN: ${{ github.token }} + REPOSITORY: ${{ github.repository }} + COMMIT_SHA: ${{ needs.workflow-context.outputs.head-sha }} + STATE: failure + DESCRIPTION: "Canonical kernel build failed before distro validation" + TARGET_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + set -euo pipefail + bash ./scripts/report-distro-validation-status.sh + exit 1 + + callback-context: + name: "Validate distro callback" + if: ${{ github.event_name == 'repository_dispatch' }} + runs-on: [self-hosted, lecore-prd-u2404-arm64-xlrg-od-ephem] + outputs: + state: ${{ steps.callback.outputs.state }} + description: ${{ steps.callback.outputs.description }} + check-conclusion: ${{ steps.callback.outputs.check-conclusion }} + pr-number: ${{ steps.callback.outputs.pr-number }} + head-sha: ${{ steps.callback.outputs.head-sha }} + request-id: ${{ steps.callback.outputs.request-id }} + kernel-build-id: ${{ steps.callback.outputs.kernel-build-id }} + distro-build-id: ${{ steps.callback.outputs.distro-build-id }} + image-s3-prefix: ${{ steps.callback.outputs.image-s3-prefix }} + distro-run-url: ${{ steps.callback.outputs.distro-run-url }} + steps: + - name: Checkout callback validator + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + + - name: Install GitHub CLI + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y --no-install-recommends gh + + - id: callback + name: Validate returned distro result + env: + GH_TOKEN: ${{ secrets.DEB_PKG_BOT_CI_TOKEN }} + BUCKET: qli-prd-lecore-gh-artifacts + run: | + set -euo pipefail + bash ./scripts/validate-distro-validation-callback.sh + + callback-status: + name: "Report distro validation result" + needs: callback-context + if: ${{ always() && needs.callback-context.result == 'success' }} + runs-on: [self-hosted, lecore-prd-u2404-arm64-xlrg-od-ephem] + permissions: + checks: write + contents: read + statuses: write + steps: + - name: Checkout status helper + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + + - name: Install GitHub CLI + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y --no-install-recommends gh + + - name: Publish final status and summary + env: + GH_TOKEN: ${{ github.token }} + MODE: complete + REPOSITORY: ${{ github.repository }} + COMMIT_SHA: ${{ needs.callback-context.outputs.head-sha }} + PR_NUMBER: ${{ needs.callback-context.outputs.pr-number }} + REQUEST_ID: ${{ needs.callback-context.outputs.request-id }} + KERNEL_BUILD_ID: ${{ needs.callback-context.outputs.kernel-build-id }} + CONCLUSION: ${{ needs.callback-context.outputs.check-conclusion }} + DETAILS_URL: ${{ needs.callback-context.outputs.distro-run-url }} + DISTRO_BUILD_ID: ${{ needs.callback-context.outputs.distro-build-id }} + IMAGE_S3_PREFIX: ${{ needs.callback-context.outputs.image-s3-prefix }} + STATE: ${{ needs.callback-context.outputs.state }} + DESCRIPTION: ${{ needs.callback-context.outputs.description }} + TARGET_URL: ${{ needs.callback-context.outputs.distro-run-url }} + run: | + set -euo pipefail + { + echo "## Canonical Premerge Distro Validation" + echo + echo "| Field | Value |" + echo "|-------|-------|" + echo "| Pull request | \`#${PR_NUMBER}\` |" + echo "| Result | \`${STATE}\` |" + echo "| Kernel build ID | \`${KERNEL_BUILD_ID}\` |" + echo "| Distro build ID | \`${DISTRO_BUILD_ID}\` |" + echo "| Image S3 prefix | \`${IMAGE_S3_PREFIX}\` |" + } >> "$GITHUB_STEP_SUMMARY" + + bash ./scripts/report-distro-validation-check.sh + bash ./scripts/report-distro-validation-status.sh + [[ "$STATE" == "success" ]] diff --git a/.github/workflows/stale-issues.yaml b/.github/workflows/stale-issues.yaml new file mode 100644 index 0000000000000..f0bb240dc2d5a --- /dev/null +++ b/.github/workflows/stale-issues.yaml @@ -0,0 +1,22 @@ +name: 'Close stale issues and pull requests with no recent activity' +on: + schedule: + - cron: "30 1 * * *" + +permissions: + issues: write + pull-requests: write + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v10 + with: + stale-issue-message: 'Remove the stale label or add a comment to reset the inactivity timer.' + stale-pr-message: 'Remove the stale label or add a comment to reset the inactivity timer' + days-before-stale: 30 + days-before-close: -1 + remove-stale-when-updated: true + remove-issue-stale-when-updated: true + remove-pr-stale-when-updated: true diff --git a/CODE-OF-CONDUCT.md b/CODE-OF-CONDUCT.md new file mode 100644 index 0000000000000..82a5fc677fbc2 --- /dev/null +++ b/CODE-OF-CONDUCT.md @@ -0,0 +1,132 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[GitHub.CoC](mailto:github.coc@qti.qualcomm.com?subject=GitHub%20Qualcomm%20Code%20of%20Conduct%20Report). +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000000..cc503a4a9554e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,7 @@ +# Contributing to pkg-linux-qcom-canonical + +This project is not accepting contributions. + +The repository mirrors the Canonical kernel optimized for Qualcomm and includes Qualcomm contributions and workflow automation used by the maintainers. + +External pull requests and issue-based contributions are not accepted in this repository. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000000000..389b5477129b0 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,26 @@ +Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000000000..bf8c7871d4ccc --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# pkg-linux-qcom-canonical + +Mirrors the Canonical kernel optimized for Qualcomm on `resolute-qcom`, from +[Launchpad](https://git.launchpad.net/~carmel-team/ubuntu/+source/linux/+git/resolute/log/?h=master-next), +with Qualcomm contributions. + +This is not a product or actively supported by Qualcomm. We are not accepting +contributions in this repository. + +> [!TIP] +> Latest upload: see the **[tags page](https://github.com/qualcomm-linux/pkg-linux-qcom-canonical/tags)**. + +## At a glance + +| | | +|---|---| +| **Upstream** | [https://git.launchpad.net/~carmel-team/ubuntu/+source/linux/+git/resolute](https://git.launchpad.net/~carmel-team/ubuntu/+source/linux/+git/resolute) | +| **`resolute-qcom`** | Mirror of that kernel - sync-only, do not commit here | +| **`resolute-qcom-devel`** | Integration branch - Qualcomm contributions (via PR) | +| **Output** | Kernel `.deb` packages, uploaded to S3 | + +> [!NOTE] +> `main` is the **CI orchestrator** - the workflows, scripts, and docs that drive the sync and build. + +## Documentation + +| Doc | For | +|-----|-----| +| **[docs/INTEGRATION.md](docs/INTEGRATION.md)** | Qualcomm developers - working on `resolute-qcom-devel` | +| **[docs/PIPELINE.md](docs/PIPELINE.md)** | Maintainers - sync, build, and mirror operations | +| **[CONTRIBUTING.md](CONTRIBUTING.md)** | This project is not accepting contributions | +| **[SECURITY.md](SECURITY.md)** | This project is not actively maintained. It is a mirror. | + +## License + +| Scope | License | +|-------|---------| +| Workflows, scripts, and documentation on `main` | BSD 3-Clause - see **[LICENSE.txt](LICENSE.txt)** | diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000000..32b724fd311d1 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,4 @@ +# Security Policy + +This project is not actively maintained. It is a mirror. + diff --git a/docs/FORK_PREMERGE_SMOKE_TEST.md b/docs/FORK_PREMERGE_SMOKE_TEST.md new file mode 100644 index 0000000000000..1e5b3e1a3549e --- /dev/null +++ b/docs/FORK_PREMERGE_SMOKE_TEST.md @@ -0,0 +1,3 @@ +# Fork Premerge Validation Smoke Test + +This temporary marker exercises the fork-based premerge kernel and distro image validation path. It is not intended for merge. diff --git a/docs/INTEGRATION.md b/docs/INTEGRATION.md new file mode 100644 index 0000000000000..1f14212b9e9f9 --- /dev/null +++ b/docs/INTEGRATION.md @@ -0,0 +1,39 @@ +# Developing on `resolute-qcom-devel` + +Qualcomm contributions live on the `resolute-qcom-devel` branch, on +top of the `resolute-qcom` mirror of the Canonical kernel. This guide is for +developers working on those contributions. + +For how the mirror and build pipeline work, see [PIPELINE.md](PIPELINE.md). + +## The branches + +| Branch | Role | +|--------|------| +| `resolute-qcom` | The mirror of the upstream Canonical kernel. Only the automated sync writes to it - **do not commit here**. | +| `resolute-qcom-devel` | The integration branch: Qualcomm contributions on top of `resolute-qcom`. **Use pull requests for changes here.** | + +The `-devel` suffix follows Canonical's own `devel` naming convention. + +## Working on Qualcomm contributions + +Qualcomm contributions land on `resolute-qcom-devel` through a **pull request** - +work on a feature branch and open a PR into `resolute-qcom-devel`. Never commit to +`resolute-qcom`: the sync is its only writer and force-advances it on every new +upload. + +```bash +git clone https://github.com/qualcomm-linux/pkg-linux-qcom-canonical.git +cd pkg-linux-qcom-canonical +git checkout -b my-feature origin/resolute-qcom-devel # feature branch off the integration branch +# add your patches, commit DCO-signed (git commit -s), then: +git push origin my-feature +``` + +Then open a pull request from your feature branch **into `resolute-qcom-devel`**. +Commits must carry a DCO `Signed-off-by` line (`git commit -s`) as required for Qualcomm contribution review. + +## Building + +To build `resolute-qcom-devel` (or any mirrored upload) into `.deb` packages, see +[PIPELINE.md](PIPELINE.md#manual-build-triggers). diff --git a/docs/PIPELINE.md b/docs/PIPELINE.md new file mode 100644 index 0000000000000..606922a3b80ed --- /dev/null +++ b/docs/PIPELINE.md @@ -0,0 +1,87 @@ +# Pipeline operations (maintainers) + +How the mirror and build pipeline work. To contribute patches, see +[INTEGRATION.md](INTEGRATION.md). + +## Repository branch layout + +``` +pkg-linux-qcom-canonical +│ +├── main branch ← CI orchestrator: workflows, scripts, docs +│ ├── .github/workflows/ +│ │ ├── fetch-source-pkg.yml ← manual incremental mirror sync +│ │ ├── bootstrap-history.yml ← one-time history seed +│ │ ├── build-kernel.yml ← build .deb packages (+ reusable workflow_call) +│ │ └── premerge-distro-validation.yml ← trusted distro image validation orchestrator +│ ├── scripts/ ← sync-mirror.sh, seed-history.sh (self-documenting) +│ └── README.md +│ +├── resolute-qcom branch ← upstream Ubuntu kernel mirror (SYNC-ONLY) +│ └── immutable tag per upload: Ubuntu-qcom-X.Y.Z-A.B +│ +├── resolute-qcom-devel branch ← developer integration branch (see INTEGRATION.md) +│ └── .github/workflows/premerge-pr.yml ← pre-merge PR build check (lives here, not on main) +│ +└── resolute-qcom-seed branch ← transient bootstrap staging (only during a seed) +``` + +`resolute-qcom` shares no history with `main` (the CI orchestrator); it holds the +upstream kernel tree the packages are built from. + +## How it works + +`resolute-qcom` mirrors the upstream Ubuntu kernel: each upload is fetched and +frozen under an immutable `Ubuntu-qcom-X.Y.Z-A.B` tag. A one-time bootstrap seeds +the branch; thereafter an incremental sync advances it per new upstream upload and +triggers a build. Builds produce `.deb` packages uploaded to a private S3 bucket +(no GitHub artifacts or releases). Qualcomm contributions land on `resolute-qcom-devel`, +never on the mirror. + +Upstream source: [https://git.launchpad.net/~carmel-team/ubuntu/+source/linux/+git/resolute](https://git.launchpad.net/~carmel-team/ubuntu/+source/linux/+git/resolute). + +## Running it + +The maintenance workflows below are manual (`Actions → … → Run workflow`, or via `gh`): + +```bash +# Sync the mirror to the latest upstream upload (no inputs; idempotent). +gh workflow run fetch-source-pkg.yml --repo qualcomm-linux/pkg-linux-qcom-canonical + +# Build .deb packages. Defaults to resolute-qcom-devel HEAD; set the suite input to +# resolute-qcom for the mirror, or kernel_version for an exact tag. The dbgsym +# input (default true) also builds the unstripped -dbgsym.ddeb. +gh workflow run build-kernel.yml --repo qualcomm-linux/pkg-linux-qcom-canonical + +# One-time only, before the first sync: seed resolute-qcom with history (into +# resolute-qcom-seed, which a human then promotes to the live branch). +gh workflow run bootstrap-history.yml --repo qualcomm-linux/pkg-linux-qcom-canonical +``` + +PRs into `resolute-qcom-devel` get a pre-merge build check (`premerge-pr.yml` on +that branch), which calls `build-kernel.yml` with `flavours=qcom`, +`dbgsym=false`, and `s3_prefix=premerge` (binary-indep is always built +regardless of `flavours`). Its packages are uploaded to S3 under +`pkg/premerge/`, separate from the `pkg/temp/` prefix used by nightly and +manual `workflow_dispatch` runs. + +After the pre-merge kernel workflow completes, `premerge-distro-validation.yml` +runs from the trusted `main` branch through `workflow_run`. It resolves the PR +from the triggering kernel run and dispatches a validated request to the internal +qcom-distro-images repository. The internal receiver calls its local reusable +workflow and builds the fixed Resolute IoT server and desktop matrix using only +the Canonical kernel packages from that exact premerge build. + +The two image tarballs are uploaded alongside the kernel packages under the same +`pkg/premerge/pkg-linux-qcom-canonical/-/` directory. A +`distro-validation.json` completion marker is written only after both image +uploads are verified. qcom-distro-images returns the distro build ID and result +through a repository dispatch callback. The Canonical callback handler verifies +the kernel run, distro run, PR identity, and S3 completion marker before reporting +the final result on the PR head commit. A Check Run named +`qcom-distro-images/canonical-premerge` starts before the distro request and is +completed only after the callback is validated. The request ID is stored as the +Check Run external ID so retries update the same validation and superseded runs +cannot overwrite a newer result. The existing commit status is published in +parallel during the transition. The untrusted PR workflow receives no repository +secrets, and neither side polls the other workflow. diff --git a/scripts/build-kernel-deb.sh b/scripts/build-kernel-deb.sh new file mode 100755 index 0000000000000..a63dde74ba9d8 --- /dev/null +++ b/scripts/build-kernel-deb.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause +# +# build-kernel-deb.sh - Build Ubuntu kernel .deb packages from a Canonical +# source tree (as checked out from a series branch) +# +# Usage: +# build-kernel-deb.sh [SOURCE_DIR] [ARCH] [FLAVOR] [JOBS] +# +# Arguments: +# SOURCE_DIR Root of the kernel source tree containing debian/ (default: .) +# ARCH Target Debian architecture: arm64 | amd64 (default: arm64) +# FLAVOR Kernel flavour: generic | lowlatency | all (default: generic) +# JOBS Parallel make jobs (default: nproc) +# +# Output: +# Built .deb packages are placed in ./output/ relative to the working +# directory from which this script is invoked. +# +# Notes: +# • Designed for native arm64 builds (Ubuntu 24.04 arm64 host). +# • The Ubuntu kernel build needs ~20 GB of free disk space. +# • A full build (all flavours) can take 2+ hours; 'generic' is ~1 hour. +# • Run as a normal user; sudo is used only for apt-get. + +set -euo pipefail + +SOURCE_DIR="${1:-.}" +ARCH="${2:-arm64}" +FLAVOR="${3:-generic}" +JOBS="${4:-$(nproc)}" + +OUTPUT_DIR="$(pwd)/output" + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- +log() { printf '[%s] %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$*" >&2; } +die() { log "ERROR: $*"; exit 1; } +hr() { log "$(printf '%0.s─' {1..60})"; } + +hr +log "Ubuntu kernel .deb build" +log " Source dir : ${SOURCE_DIR}" +log " Arch : ${ARCH}" +log " Flavour : ${FLAVOR}" +log " Jobs : ${JOBS}" +log " Output dir : ${OUTPUT_DIR}" +hr + +# --------------------------------------------------------------------------- +# 1. Validate source tree +# --------------------------------------------------------------------------- +[ -f "${SOURCE_DIR}/debian/rules" ] \ + || die "No debian/rules found in '${SOURCE_DIR}' – is this a kernel source tree?" + +# --------------------------------------------------------------------------- +# 2. Install build dependencies +# --------------------------------------------------------------------------- +hr +log "Installing build dependencies (requires sudo)..." +sudo apt-get update -qq +sudo apt-get build-dep -y "${SOURCE_DIR}" \ + || die "apt-get build-dep failed" + +# --------------------------------------------------------------------------- +# 3. Build +# --------------------------------------------------------------------------- +hr +log "Starting kernel build (flavour=${FLAVOR}, arch=${ARCH}, jobs=${JOBS})..." + +# Determine the debian/rules target +if [ "${FLAVOR}" = "all" ]; then + RULES_TARGET="binary" +else + RULES_TARGET="binary-${FLAVOR}" +fi + +# The Ubuntu kernel build system reads DEB_BUILD_OPTIONS for parallelism +export DEB_BUILD_OPTIONS="parallel=${JOBS} nocheck" + +# Run the build (native arm64 – no cross-compilation flags needed) +( + cd "${SOURCE_DIR}" + fakeroot debian/rules "${RULES_TARGET}" \ + || die "debian/rules ${RULES_TARGET} failed" +) + +# --------------------------------------------------------------------------- +# 4. Collect output packages +# --------------------------------------------------------------------------- +hr +mkdir -p "${OUTPUT_DIR}" + +# The Ubuntu kernel build drops .deb files one level above the source tree +PARENT_DIR=$(dirname "$(realpath "${SOURCE_DIR}")") +find "${PARENT_DIR}" -maxdepth 1 \ + \( -name "*.deb" -o -name "*.changes" -o -name "*.buildinfo" \) \ + | while read -r f; do + cp "${f}" "${OUTPUT_DIR}/" + log " Collected: $(basename "${f}")" + done + +hr +log "Build complete." +log "" +log "Output packages:" +ls -lh "${OUTPUT_DIR}"/*.deb 2>/dev/null \ + || log " (no .deb files found — check build log above)" diff --git a/scripts/check-version.sh b/scripts/check-version.sh new file mode 100755 index 0000000000000..95b0fcebb28b7 --- /dev/null +++ b/scripts/check-version.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause +# +# check-version.sh - Query the latest published kernel version from Launchpad +# +# Usage: +# check-version.sh [SUITE] [SOURCE_NAME] +# +# Arguments: +# SUITE Ubuntu suite (default: noble) +# SOURCE_NAME Source package name (default: linux) +# +# Output: +# Prints the latest version string to stdout (e.g. "6.8.0-51.52") +# +# Exit codes: +# 0 Version found +# 1 Version not found or API error + +set -euo pipefail + +SUITE="${1:-noble}" +SOURCE_NAME="${2:-linux}" + +LAUNCHPAD_API="https://api.launchpad.net/1.0" + +die() { echo "ERROR: $*" >&2; exit 1; } + +RESPONSE=$(curl -fsSL \ + "${LAUNCHPAD_API}/ubuntu/+archive/primary?ws.op=getPublishedSources\ +&source_name=${SOURCE_NAME}\ +&distro_series=/ubuntu/${SUITE}\ +&status=Published\ +&order_by_date=true") \ + || die "Failed to query Launchpad API" + +# Filter by exact source_package_name (source_name= is a prefix match on Launchpad) +VERSION=$(echo "$RESPONSE" | jq -r \ + --arg name "${SOURCE_NAME}" \ + '[.entries[] | select(.source_package_name == $name)] | .[0].source_package_version // empty') + +[ -n "$VERSION" ] || die "No exact match for '${SOURCE_NAME}' in '${SUITE}'" + +echo "${VERSION}" diff --git a/scripts/fetch-source-pkg.sh b/scripts/fetch-source-pkg.sh new file mode 100755 index 0000000000000..e821ece3452c4 --- /dev/null +++ b/scripts/fetch-source-pkg.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause +# +# fetch-source-pkg.sh - Clone the Canonical Ubuntu kernel source from the +# Launchpad git repository at the version tag matching +# the latest published source package. +# +# Usage: +# fetch-source-pkg.sh [SUITE] [SOURCE_NAME] [OUTPUT_DIR] +# +# Arguments: +# SUITE Ubuntu suite (default: noble) +# SOURCE_NAME Source package name (default: linux) +# OUTPUT_DIR Directory to clone into (default: .) +# +# Why git instead of the source package (.dsc/.orig.tar.gz/.diff.gz)? +# The Ubuntu kernel source package (format 1.0) ships only debian.master/ +# with rules.d/ fragments — debian/rules is NOT included. The complete +# debian/ directory (with rules, scripts/, templates/, etc.) lives only in +# the Launchpad git repository. Cloning from git gives a buildable tree. +# +# Output: +# A shallow clone of the kernel source at tag Ubuntu- is placed +# in OUTPUT_DIR/. A version.env metadata file is also written. + +set -euo pipefail + +SUITE="${1:-noble}" +SOURCE_NAME="${2:-linux}" +OUTPUT_DIR="${3:-.}" + +LAUNCHPAD_API="${LAUNCHPAD_API:-https://api.launchpad.net/1.0}" +LAUNCHPAD_GIT="https://git.launchpad.net/~ubuntu-kernel/ubuntu/+source/linux/+git" + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- +log() { printf '[%s] %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$*" >&2; } +die() { log "ERROR: $*"; exit 1; } +hr() { log "$(printf '%0.s─' {1..60})"; } + +# --------------------------------------------------------------------------- +# 1. Query Launchpad for the latest published version +# --------------------------------------------------------------------------- +hr +log "Querying Launchpad for latest '${SOURCE_NAME}' in Ubuntu ${SUITE}..." + +API_URL="${LAUNCHPAD_API}/ubuntu/+archive/primary" +API_URL+="?ws.op=getPublishedSources" +API_URL+="&source_name=${SOURCE_NAME}" +API_URL+="&distro_series=/ubuntu/${SUITE}" +API_URL+="&status=Published" +API_URL+="&order_by_date=true" + +RESPONSE=$(curl -fsSL "${API_URL}") \ + || die "Launchpad API request failed" + +# Filter by exact source_package_name (source_name= is a prefix match) +VERSION=$(echo "$RESPONSE" | jq -r \ + --arg name "${SOURCE_NAME}" \ + '[.entries[] | select(.source_package_name == $name)] | .[0].source_package_version // empty') + +[ -n "$VERSION" ] || die "No published source found for '${SOURCE_NAME}' (exact) in '${SUITE}'" + +UPSTREAM_VERSION=$(echo "${VERSION}" | cut -d'-' -f1) +GIT_TAG="Ubuntu-${VERSION}" + +log "Found: ${SOURCE_NAME} ${VERSION} (upstream: ${UPSTREAM_VERSION})" +log "Git tag: ${GIT_TAG}" + +# --------------------------------------------------------------------------- +# 2. Clone from Launchpad git at the version tag (shallow) +# --------------------------------------------------------------------------- +hr +CLONE_URL="${LAUNCHPAD_GIT}/${SUITE}" +log "Cloning ${CLONE_URL} at tag ${GIT_TAG} (shallow)..." + +mkdir -p "${OUTPUT_DIR}" + +git clone --depth=1 --branch "${GIT_TAG}" "${CLONE_URL}" "${OUTPUT_DIR}" \ + || die "git clone failed" + +FILE_COUNT=$(find "${OUTPUT_DIR}" -type f | wc -l) +log "Cloned ${FILE_COUNT} files" + +[ "${FILE_COUNT}" -gt 5000 ] || \ + die "Too few files cloned (${FILE_COUNT}) — expected >5000" + +# --------------------------------------------------------------------------- +# 3. Write version metadata +# --------------------------------------------------------------------------- +hr +cat > "${OUTPUT_DIR}/version.env" <> "$GITHUB_OUTPUT" +} + +[[ "$MODE" =~ ^(start|complete)$ ]] || { echo "::error::Invalid Check Run mode: ${MODE}" >&2; exit 1; } +[[ "$REPOSITORY" == "qualcomm-linux/pkg-linux-qcom-canonical" ]] || { echo "::error::Unexpected repository: ${REPOSITORY}" >&2; exit 1; } +[[ "$COMMIT_SHA" =~ ^[0-9a-f]{40}$ ]] || { echo "::error::Invalid commit SHA: ${COMMIT_SHA}" >&2; exit 1; } +[[ "$PR_NUMBER" =~ ^[0-9]+$ ]] || { echo "::error::Invalid pull request number: ${PR_NUMBER}" >&2; exit 1; } +[[ "$KERNEL_BUILD_ID" =~ ^[0-9]+-[0-9]+$ ]] || { echo "::error::Invalid kernel build ID: ${KERNEL_BUILD_ID}" >&2; exit 1; } +[[ "$REQUEST_ID" == "${KERNEL_BUILD_ID}-${COMMIT_SHA}" ]] || { echo "::error::Request ID does not match the kernel build and commit SHA." >&2; exit 1; } +[[ "$IMAGE_S3_PREFIX" == "qualcomm-linux/pkg/premerge/pkg-linux-qcom-canonical/${KERNEL_BUILD_ID}" ]] || { echo "::error::Unexpected image S3 prefix: ${IMAGE_S3_PREFIX}" >&2; exit 1; } +[[ "$DETAILS_URL" =~ ^https://github\.com/qualcomm-linux/(pkg-linux-qcom-canonical|qcom-distro-images)/actions/runs/[0-9]+$ ]] || { echo "::error::Unexpected Check Run details URL: ${DETAILS_URL}" >&2; exit 1; } + +if [[ "$MODE" == "complete" ]]; then + [[ "$CONCLUSION" =~ ^(success|failure|cancelled|skipped)$ ]] || { echo "::error::Invalid Check Run conclusion: ${CONCLUSION}" >&2; exit 1; } + [[ "$DISTRO_BUILD_ID" =~ ^[0-9]+-[0-9]+$ ]] || { echo "::error::Invalid distro build ID: ${DISTRO_BUILD_ID}" >&2; exit 1; } +fi + +checks="$({ + gh api \ + --paginate \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "repos/${REPOSITORY}/commits/${COMMIT_SHA}/check-runs?filter=all&per_page=100" +} | jq -s --arg name "$CHECK_NAME" '[.[].check_runs[] | select(.name == $name)]')" + +matching_checks="$(jq --arg request_id "$REQUEST_ID" '[.[] | select((.external_id // "") == $request_id)]' <<< "$checks")" +matching_count="$(jq 'length' <<< "$matching_checks")" + +if [[ "$matching_count" -gt 1 ]]; then + echo "::error::Multiple Check Runs use request ID ${REQUEST_ID}." >&2 + exit 1 +fi + +check_title="Canonical premerge distro validation" +check_summary="Building Canonical server and desktop images for PR #${PR_NUMBER}." +check_text="$IMAGE_S3_PREFIX" + +if [[ "$MODE" == "start" ]]; then + if [[ "$matching_count" == "1" ]]; then + existing_status="$(jq -r '.[0].status' <<< "$matching_checks")" + existing_url="$(jq -r '.[0].html_url' <<< "$matching_checks")" + if [[ "$existing_status" =~ ^(in_progress|completed)$ ]]; then + emit_output should-dispatch false + echo "[INFO] Reusing Check Run for ${REQUEST_ID}: ${existing_url}" + exit 0 + fi + echo "::error::Check Run for ${REQUEST_ID} has unexpected status ${existing_status}." >&2 + exit 1 + fi + + while IFS=$'\t' read -r check_id external_id status; do + [[ -n "$check_id" ]] || continue + [[ "$status" == "in_progress" ]] || continue + [[ "$external_id" != "$REQUEST_ID" ]] || continue + [[ "$external_id" =~ ^[0-9]+-[0-9]+-${COMMIT_SHA}$ ]] || continue + + superseded_payload="$(jq -n \ + --arg details_url "$DETAILS_URL" \ + --arg title "$check_title" \ + --arg summary "Superseded by a newer Canonical premerge validation request." \ + --arg text "Superseding request ID: \`${REQUEST_ID}\`" \ + '{ + status: "completed", + conclusion: "cancelled", + details_url: $details_url, + output: { + title: $title, + summary: $summary, + text: $text + } + }')" + gh api \ + --method PATCH \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "repos/${REPOSITORY}/check-runs/${check_id}" \ + --input - <<< "$superseded_payload" >/dev/null + done < <(jq -r '.[] | [.id, (.external_id // ""), .status] | @tsv' <<< "$checks") + + create_payload="$(jq -n \ + --arg name "$CHECK_NAME" \ + --arg head_sha "$COMMIT_SHA" \ + --arg external_id "$REQUEST_ID" \ + --arg details_url "$DETAILS_URL" \ + --arg title "$check_title" \ + --arg summary "$check_summary" \ + --arg text "$check_text" \ + '{ + name: $name, + head_sha: $head_sha, + external_id: $external_id, + status: "in_progress", + details_url: $details_url, + output: { + title: $title, + summary: $summary, + text: $text + } + }')" + check_run_url="$(gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "repos/${REPOSITORY}/check-runs" \ + --input - <<< "$create_payload" \ + --jq '.html_url')" + emit_output should-dispatch true + echo "[INFO] Started Check Run for ${REQUEST_ID}: ${check_run_url}" + exit 0 +fi + +if [[ "$matching_count" != "1" ]]; then + echo "::error::No Check Run exists for request ID ${REQUEST_ID}." >&2 + exit 1 +fi + +check_id="$(jq -r '.[0].id' <<< "$matching_checks")" +existing_status="$(jq -r '.[0].status' <<< "$matching_checks")" +existing_conclusion="$(jq -r '.[0].conclusion // ""' <<< "$matching_checks")" +existing_url="$(jq -r '.[0].html_url' <<< "$matching_checks")" + +case "$CONCLUSION" in + success) + check_summary="Canonical server and desktop distro images passed for PR #${PR_NUMBER}." + ;; + failure) + check_summary="Canonical distro image validation failed for PR #${PR_NUMBER}." + ;; + cancelled) + check_summary="Canonical distro image validation was cancelled for PR #${PR_NUMBER}." + ;; + skipped) + check_summary="Canonical distro image validation was skipped for PR #${PR_NUMBER}." + ;; +esac + +if [[ "$existing_status" == "completed" ]]; then + existing_details_url="$(jq -r '.[0].details_url // ""' <<< "$matching_checks")" + existing_title="$(jq -r '.[0].output.title // ""' <<< "$matching_checks")" + existing_summary="$(jq -r '.[0].output.summary // ""' <<< "$matching_checks")" + existing_text="$(jq -r '.[0].output.text // ""' <<< "$matching_checks")" + if [[ "$existing_conclusion" == "$CONCLUSION" && + "$existing_details_url" == "$DETAILS_URL" && + "$existing_title" == "$check_title" && + "$existing_summary" == "$check_summary" && + "$existing_text" == "$check_text" ]]; then + echo "[INFO] Check Run already completed for ${REQUEST_ID}: ${existing_url}" + exit 0 + fi + echo "::error::Completed Check Run for ${REQUEST_ID} does not match the validated callback result." >&2 + exit 1 +fi + +[[ "$existing_status" == "in_progress" ]] || { echo "::error::Check Run for ${REQUEST_ID} has unexpected status ${existing_status}." >&2; exit 1; } + +complete_payload="$(jq -n \ + --arg conclusion "$CONCLUSION" \ + --arg details_url "$DETAILS_URL" \ + --arg title "$check_title" \ + --arg summary "$check_summary" \ + --arg text "$check_text" \ + '{ + status: "completed", + conclusion: $conclusion, + details_url: $details_url, + output: { + title: $title, + summary: $summary, + text: $text + } + }')" +check_run_url="$(gh api \ + --method PATCH \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "repos/${REPOSITORY}/check-runs/${check_id}" \ + --input - <<< "$complete_payload" \ + --jq '.html_url')" + +echo "[INFO] Completed Check Run for ${REQUEST_ID}: ${check_run_url}" diff --git a/scripts/report-distro-validation-status.sh b/scripts/report-distro-validation-status.sh new file mode 100755 index 0000000000000..704008d18d3b4 --- /dev/null +++ b/scripts/report-distro-validation-status.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause +# +set -euo pipefail + +: "${REPOSITORY:?REPOSITORY is required}" +: "${COMMIT_SHA:?COMMIT_SHA is required}" +: "${STATE:?STATE is required}" +: "${DESCRIPTION:?DESCRIPTION is required}" +: "${TARGET_URL:?TARGET_URL is required}" + +STATUS_CONTEXT="${STATUS_CONTEXT:-qcom-distro-images/canonical-premerge}" + +[[ "$REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || { echo "::error::Invalid repository: ${REPOSITORY}" >&2; exit 1; } +[[ "$COMMIT_SHA" =~ ^[0-9a-f]{40}$ ]] || { echo "::error::Invalid commit SHA: ${COMMIT_SHA}" >&2; exit 1; } +[[ "$STATE" =~ ^(error|failure|pending|success)$ ]] || { echo "::error::Invalid status state: ${STATE}" >&2; exit 1; } + +description="${DESCRIPTION:0:140}" + +gh api \ + --method POST \ + "repos/${REPOSITORY}/statuses/${COMMIT_SHA}" \ + -f state="$STATE" \ + -f target_url="$TARGET_URL" \ + -f description="$description" \ + -f context="$STATUS_CONTEXT" \ + >/dev/null + +echo "[INFO] Reported ${STATUS_CONTEXT}=${STATE} for ${COMMIT_SHA}." diff --git a/scripts/resolve-distro-validation-context.sh b/scripts/resolve-distro-validation-context.sh new file mode 100755 index 0000000000000..e34b147ef2775 --- /dev/null +++ b/scripts/resolve-distro-validation-context.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause +# +set -euo pipefail + +: "${GITHUB_EVENT_NAME:?GITHUB_EVENT_NAME is required}" + +emit_output() { + local name="$1" + local value="$2" + echo "${name}=${value}" >> "$GITHUB_OUTPUT" +} + +validate_common() { + local run_id="$1" + local run_attempt="$2" + local pr_number="$3" + local head_sha="$4" + + [[ "$run_id" =~ ^[0-9]+$ ]] || { echo "::error::Invalid kernel workflow run ID: ${run_id}" >&2; exit 1; } + [[ "$run_attempt" =~ ^[0-9]+$ ]] || { echo "::error::Invalid kernel workflow run attempt: ${run_attempt}" >&2; exit 1; } + [[ "$pr_number" =~ ^[0-9]+$ ]] || { echo "::error::Invalid pull request number: ${pr_number}" >&2; exit 1; } + [[ "$head_sha" =~ ^[0-9a-f]{40}$ ]] || { echo "::error::Invalid pull request head SHA: ${head_sha}" >&2; exit 1; } + + emit_output kernel-build-id "${run_id}-${run_attempt}" + emit_output kernel-run-id "$run_id" + emit_output kernel-run-attempt "$run_attempt" + emit_output kernel-s3-prefix "pkg/premerge/pkg-linux-qcom-canonical" + emit_output pr-number "$pr_number" + emit_output head-sha "$head_sha" + emit_output request-id "${run_id}-${run_attempt}-${head_sha}" +} + +if [[ "$GITHUB_EVENT_NAME" == "workflow_run" ]]; then + : "${GITHUB_EVENT_PATH:?GITHUB_EVENT_PATH is required}" + + action="$(jq -r '.action' "$GITHUB_EVENT_PATH")" + event="$(jq -r '.workflow_run.event' "$GITHUB_EVENT_PATH")" + workflow_path="$(jq -r '.workflow_run.path' "$GITHUB_EVENT_PATH")" + repository="$(jq -r '.workflow_run.repository.full_name' "$GITHUB_EVENT_PATH")" + + [[ "$event" == "pull_request" ]] || { echo "::error::Unexpected triggering event: ${event}" >&2; exit 1; } + [[ "$workflow_path" == ".github/workflows/premerge-pr.yml" ]] || { echo "::error::Unexpected triggering workflow path: ${workflow_path}" >&2; exit 1; } + [[ "$repository" == "$GITHUB_REPOSITORY" ]] || { echo "::error::Unexpected triggering repository: ${repository}" >&2; exit 1; } + + [[ "$action" == "completed" ]] || { echo "::error::Unexpected workflow_run activity: ${action}" >&2; exit 1; } + + run_id="$(jq -r '.workflow_run.id' "$GITHUB_EVENT_PATH")" + run_attempt="$(jq -r '.workflow_run.run_attempt' "$GITHUB_EVENT_PATH")" + conclusion="$(jq -r '.workflow_run.conclusion' "$GITHUB_EVENT_PATH")" + head_sha="$(jq -r '.workflow_run.head_sha' "$GITHUB_EVENT_PATH")" + + pull_requests="$( + gh api "repos/${GITHUB_REPOSITORY}/commits/${head_sha}/pulls" \ + -H "Accept: application/vnd.github+json" \ + --jq '[.[] | select(.base.ref == "resolute-qcom-devel" and .head.sha == "'"$head_sha"'")]' + )" + pr_count="$(jq 'length' <<< "$pull_requests")" + [[ "$pr_count" == "1" ]] || { + echo "::error::Expected exactly one resolute-qcom-devel pull request for ${head_sha}; found ${pr_count}." >&2 + exit 1 + } + pr_number="$(jq -r '.[0].number' <<< "$pull_requests")" + + validate_common "$run_id" "$run_attempt" "$pr_number" "$head_sha" + emit_output kernel-conclusion "$conclusion" + if [[ "$conclusion" == "success" ]]; then + emit_output should-validate "true" + else + emit_output should-validate "false" + fi + exit 0 +fi + +if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then + : "${KERNEL_RUN_ID:?KERNEL_RUN_ID is required}" + : "${KERNEL_RUN_ATTEMPT:?KERNEL_RUN_ATTEMPT is required}" + : "${PR_NUMBER:?PR_NUMBER is required}" + : "${PR_HEAD_SHA:?PR_HEAD_SHA is required}" + + validate_common "$KERNEL_RUN_ID" "$KERNEL_RUN_ATTEMPT" "$PR_NUMBER" "$PR_HEAD_SHA" + emit_output kernel-conclusion "success" + emit_output should-validate "true" + exit 0 +fi + +echo "::error::Unsupported event: ${GITHUB_EVENT_NAME}" >&2 +exit 1 diff --git a/scripts/seed-history.sh b/scripts/seed-history.sh new file mode 100755 index 0000000000000..5806c9c5c9939 --- /dev/null +++ b/scripts/seed-history.sh @@ -0,0 +1,188 @@ +#!/usr/bin/env bash +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause +# +# seed-history.sh - One-time automated bootstrap: seed a branch of this repo with +# the FULL Canonical kernel history from Launchpad, pushing the +# upstream upload tags (e.g. Ubuntu-qcom-X.Y.Z-A.B) verbatim. +# +# Why a bootstrap is needed +# ───────────────────────── +# The steady-state sync (sync-mirror.sh) is incremental: it only works once the +# branch already holds real upstream history, so each new upload is a small +# delta. The branch starts empty, so the FULL history must be transferred from +# Launchpad exactly once. This script does that. +# +# How the history is transferred (both points proven empirically) +# ─────────────────────────────────────────────────────────────── +# * A FULL `git clone` is used, NOT shallow + `git fetch --deepen`: Launchpad's +# shallow-deepen path is broken (it stalls and throws "error processing +# shallow info"). Launchpad CAN serve a full clone, but spends many minutes +# server-side computing the pack (the client sees ~0 bytes meanwhile), so the +# git low-speed abort is relaxed to tolerate that quiet phase. +# * GitHub caps a single push at 2 GB, so the seed is pushed in <2 GB slices. +# +# Single shot (no resume loop): the full clone either completes within the job's +# time budget or fails. On failure, re-dispatch -- the caller caches the cloned +# repo, so a retry skips re-downloading it and just re-attempts the publish. +# +# Usage: +# seed-history.sh +# +# Required environment: +# MIRROR_URL Authenticated push URL of THIS repo +# UPSTREAM_URL Canonical/Launchpad git URL to seed from +# BRANCH Seed branch to create (e.g. resolute-qcom-seed) +# UPSTREAM_PREFIX Upstream tag prefix, mirrored verbatim (e.g. Ubuntu-qcom) +# WORKDIR Scratch dir (cached across runs by the caller) +# +# Optional environment: +# PUSH_SLICE_COMMITS Commits per push slice, to stay under GitHub's 2 GB +# per-push limit (default: 20000) +# +# Exit codes: +# 0 Seed complete and published +# 1 Hard error + +set -euo pipefail + +log() { printf '[%s] %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$*" >&2; } +die() { log "ERROR: $*"; exit 1; } +hr() { log "────────────────────────────────────────────────────────────"; } +redact() { sed -E 's#(https?://)[^@/]*@#\1***@#g' <<<"$1"; } + +: "${MIRROR_URL:?MIRROR_URL is required}" +: "${UPSTREAM_URL:?UPSTREAM_URL is required}" +: "${BRANCH:?BRANCH is required}" +: "${UPSTREAM_PREFIX:?UPSTREAM_PREFIX is required}" +: "${WORKDIR:?WORKDIR is required (cached across runs by the caller)}" + +PUSH_SLICE_COMMITS="${PUSH_SLICE_COMMITS:-20000}" +SEED="${WORKDIR}/seed.git" + +# Abort only a TRULY dead transfer. Launchpad spends many minutes server-side +# computing the pack for a full kernel history (the client sees ~0 bytes during +# "Counting/Compressing objects"); too short a low-speed window kills that +# legitimate compute phase. Tolerate a long quiet period, abort only if nothing +# moves for the full window. +export GIT_HTTP_LOW_SPEED_LIMIT="${GIT_HTTP_LOW_SPEED_LIMIT:-1000}" +export GIT_HTTP_LOW_SPEED_TIME="${GIT_HTTP_LOW_SPEED_TIME:-2400}" # 40 min + +hr +log "Canonical kernel history bootstrap" +log " Seed branch : ${BRANCH}" +log " Upstream : $(redact "${UPSTREAM_URL}")" +log " Tags : ${UPSTREAM_PREFIX}-* (verbatim)" +log " Workdir : ${WORKDIR}" +hr + +mkdir -p "${WORKDIR}" + +# --------------------------------------------------------------------------- +# 1. Full-clone the upstream history (fresh), or reuse a cached clone (retry). +# A full clone is used rather than shallow + `git fetch --deepen`: Launchpad's +# shallow-deepen path is broken, but it can serve a full clone. Retried a few +# times to ride out transient stalls. +# --------------------------------------------------------------------------- +if [ ! -d "${SEED}" ]; then + log "No cached clone found -- full-cloning upstream history (this is slow)..." + for attempt in 1 2 3; do + if git clone --bare "${UPSTREAM_URL}" "${SEED}"; then break; fi + log "Full clone attempt ${attempt} failed; cleaning up and backing off..." + rm -rf "${SEED}" + sleep "$((attempt * 30))" + done + [ -d "${SEED}" ] || die "Full clone of ${UPSTREAM_URL} failed after retries." +else + log "Reusing cached clone at ${SEED}." +fi + +cd "${SEED}" +git remote get-url upstream >/dev/null 2>&1 || git remote add upstream "${UPSTREAM_URL}" + +# A full clone is never shallow; assert it defensively, since GitHub rejects a +# shallow push outright. +[ ! -f "${SEED}/shallow" ] \ + || die "Clone is unexpectedly shallow -- cannot publish a shallow history." +log "Full history cloned: $(git rev-list --all --count) commits." + +# --------------------------------------------------------------------------- +# 3. Fetch EVERY upload tag (full; the shared base is already local, so these +# are cheap deltas) so each upload -- including any that upstream rebased onto +# a divergent line -- is preserved under its own immutable tag. +# --------------------------------------------------------------------------- +log "Fetching all ${UPSTREAM_PREFIX}-* upload tags for full preservation..." +# On a resume, these tags may already be local; a re-pointed upstream tag then +# yields a non-zero "would clobber" instead of a silent overwrite (G10). Surface +# it clearly rather than letting `set -e` report an opaque failure. +if ! git fetch --no-tags upstream \ + "refs/tags/${UPSTREAM_PREFIX}-*:refs/tags/${UPSTREAM_PREFIX}-*" 2>fetch.err; then + if grep -q 'would clobber existing tag' fetch.err; then + die "An upstream ${UPSTREAM_PREFIX}-* tag was re-pointed since a previous \ +attempt (immutability violation upstream). Refusing to clobber a preserved tag." + fi + cat fetch.err >&2 + die "Failed to fetch upstream upload tags." +fi + +mapfile -t VERSIONS < <( + git tag --list "${UPSTREAM_PREFIX}-*" \ + | sed -E "s#^${UPSTREAM_PREFIX}-##" | sort -V +) +[ "${#VERSIONS[@]}" -gt 0 ] || die "No upload tags present after fetch." +LATEST_VERSION="${VERSIONS[-1]}" +log "Uploads to preserve: ${VERSIONS[*]}" + +# The upstream ${UPSTREAM_PREFIX}-* tags fetched above ARE the preservation tags, +# mirrored verbatim (Canonical's tag names) -- no re-tagging. Point the seed +# branch at the latest upload. +git update-ref "refs/heads/${BRANCH}" \ + "$(git rev-parse "${UPSTREAM_PREFIX}-${LATEST_VERSION}^{commit}")" + +# --------------------------------------------------------------------------- +# 4. Sliced push: GitHub caps a single push at 2 GB, so push the latest branch's +# history in commit-count checkpoints (each slice carries only the objects +# between checkpoints), then the branch ref and every preservation tag. +# --------------------------------------------------------------------------- +# `git clone` set origin to the UPSTREAM (Launchpad) URL; repoint it at our +# mirror so the seed is pushed to GitHub, not back to Launchpad. +if git remote get-url origin >/dev/null 2>&1; then + git remote set-url origin "${MIRROR_URL}" +else + git remote add origin "${MIRROR_URL}" +fi + +# The scratch ref is a DISPOSABLE chunking aid, never history. It is force-pushed +# and cleared on every exit so a leftover from a failed prior attempt can never +# wedge a resume into a non-fast-forward (which would otherwise make the bootstrap +# non-convergent without manual intervention). +SCRATCH="refs/heads/_seed-progress" +cleanup_scratch() { git push origin ":${SCRATCH}" >/dev/null 2>&1 || true; } +trap cleanup_scratch EXIT +cleanup_scratch # clear any leftover before we start + +log "Slicing history into <2 GB pushes (every ${PUSH_SLICE_COMMITS} commits)..." +mapfile -t CHECKPOINTS < <( + git rev-list --first-parent --reverse "refs/heads/${BRANCH}" \ + | awk -v n="${PUSH_SLICE_COMMITS}" 'NR % n == 0' +) +for cp in "${CHECKPOINTS[@]}"; do + log " push checkpoint ${cp:0:12} -> ${SCRATCH}" + git push --force origin "${cp}:${SCRATCH}" +done + +log "Pushing branch ${BRANCH} and ${#VERSIONS[@]} preservation tag(s)..." +git push origin "refs/heads/${BRANCH}" +git push origin "refs/tags/${UPSTREAM_PREFIX}-*" +# scratch ref is removed by the EXIT trap + +hr +log "Bootstrap complete." +log " Branch ${BRANCH} seeded with $(git rev-list --count "${BRANCH}") commits." +log " Preserved uploads: ${VERSIONS[*]}" +log " Latest: ${LATEST_VERSION}" + +if [ -n "${GITHUB_OUTPUT:-}" ]; then + echo "latest_version=${LATEST_VERSION}" >> "${GITHUB_OUTPUT}" + echo "preserved_count=${#VERSIONS[@]}" >> "${GITHUB_OUTPUT}" +fi diff --git a/scripts/send-distro-validation-request.sh b/scripts/send-distro-validation-request.sh new file mode 100755 index 0000000000000..a2dee529c5d16 --- /dev/null +++ b/scripts/send-distro-validation-request.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause +# +set -euo pipefail + +: "${DISTRO_REPOSITORY:?DISTRO_REPOSITORY is required}" +: "${KERNEL_REPOSITORY:?KERNEL_REPOSITORY is required}" +: "${KERNEL_RUN_ID:?KERNEL_RUN_ID is required}" +: "${KERNEL_RUN_ATTEMPT:?KERNEL_RUN_ATTEMPT is required}" +: "${KERNEL_BUILD_ID:?KERNEL_BUILD_ID is required}" +: "${KERNEL_S3_PREFIX:?KERNEL_S3_PREFIX is required}" +: "${PR_NUMBER:?PR_NUMBER is required}" +: "${HEAD_SHA:?HEAD_SHA is required}" +: "${REQUEST_ID:?REQUEST_ID is required}" + +[[ "$DISTRO_REPOSITORY" == "qualcomm-linux/qcom-distro-images" ]] || { echo "::error::Unexpected distro repository." >&2; exit 1; } +[[ "$KERNEL_REPOSITORY" == "qualcomm-linux/pkg-linux-qcom-canonical" ]] || { echo "::error::Unexpected kernel repository." >&2; exit 1; } +[[ "$KERNEL_RUN_ID" =~ ^[0-9]+$ ]] || { echo "::error::Invalid kernel run ID." >&2; exit 1; } +[[ "$KERNEL_RUN_ATTEMPT" =~ ^[0-9]+$ ]] || { echo "::error::Invalid kernel run attempt." >&2; exit 1; } +[[ "$KERNEL_BUILD_ID" == "${KERNEL_RUN_ID}-${KERNEL_RUN_ATTEMPT}" ]] || { echo "::error::Kernel build ID does not match its run identity." >&2; exit 1; } +[[ "$KERNEL_S3_PREFIX" == "pkg/premerge/pkg-linux-qcom-canonical" ]] || { echo "::error::Unexpected kernel S3 prefix." >&2; exit 1; } +[[ "$PR_NUMBER" =~ ^[0-9]+$ ]] || { echo "::error::Invalid pull request number." >&2; exit 1; } +[[ "$HEAD_SHA" =~ ^[0-9a-f]{40}$ ]] || { echo "::error::Invalid pull request head SHA." >&2; exit 1; } +[[ "$REQUEST_ID" == "${KERNEL_BUILD_ID}-${HEAD_SHA}" ]] || { echo "::error::Request ID does not match the build and head SHA." >&2; exit 1; } + +payload="$(jq -n \ + --arg event_type "canonical-premerge-validation" \ + --arg kernel_repository "$KERNEL_REPOSITORY" \ + --arg kernel_run_id "$KERNEL_RUN_ID" \ + --arg kernel_run_attempt "$KERNEL_RUN_ATTEMPT" \ + --arg kernel_build_id "$KERNEL_BUILD_ID" \ + --arg kernel_s3_prefix "$KERNEL_S3_PREFIX" \ + --arg pr_number "$PR_NUMBER" \ + --arg head_sha "$HEAD_SHA" \ + --arg request_id "$REQUEST_ID" \ + '{ + event_type: $event_type, + client_payload: { + kernel_repository: $kernel_repository, + kernel_run_id: $kernel_run_id, + kernel_run_attempt: $kernel_run_attempt, + kernel_build_id: $kernel_build_id, + kernel_s3_prefix: $kernel_s3_prefix, + pr_number: $pr_number, + head_sha: $head_sha, + request_id: $request_id + } + }')" + +gh api --method POST "repos/${DISTRO_REPOSITORY}/dispatches" --input - <<< "$payload" + +echo "[INFO] Dispatched distro validation request ${REQUEST_ID}." diff --git a/scripts/sync-mirror.sh b/scripts/sync-mirror.sh new file mode 100755 index 0000000000000..51e948c32086e --- /dev/null +++ b/scripts/sync-mirror.sh @@ -0,0 +1,278 @@ +#!/usr/bin/env bash +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause +# +# sync-mirror.sh - Incrementally mirror new Canonical kernel upload tags into a +# branch of this repository while PRESERVING full upstream +# history. +# +# This is the steady-state "mirror-repoint" sync. It replaces the legacy +# shallow-clone + rsync + squash approach, which discarded all upstream history +# (one flattened snapshot commit per upload). Here, the branch becomes a faithful +# mirror of the upstream kernel tree: each upload is fetched with its real +# ancestry and frozen under an immutable per-upload tag. +# +# Mental model +# ──────────── +# * BRANCH (e.g. resolute-qcom) -- a MOVABLE "latest Canonical" +# pointer; disposable by design. +# * TAG (e.g. Ubuntu-qcom-7.0.0-1006.8) -- the upstream Canonical tag, +# mirrored VERBATIM. IMMUTABLE +# per-upload record; the sole +# anchor that preserves history. +# +# A sync is PURE FETCH + REPOINT -- it never merges or rebases, so it can never +# conflict and can never be blocked by developer patches layered on the branch. +# Developer patches are deliberately out of scope: the branch is force-advanced +# past them (they live on developers' own branches and are re-applied manually). +# +# Operational guardrails (each empirically validated by the history-preservation +# stress test; numbers refer to that test's findings): +# G1 Pin the lease: --force-with-lease=:, never bare +# --force-with-lease (a pre-push fetch silently defeats the bare form) and +# never blind --force. +# G2 Push the tag and the branch ATOMICALLY (--atomic) so they land together +# or not at all -- prevents tag-lands/branch-rejected split-brain. +# G3 Fail fast: abort the whole run on any rejected push. Never let the next +# idempotent run mask an incomplete one. +# G4 Tag every missing upload, ascending, and create the tag BEFORE moving the +# branch. Latest-only silently drops unique commits from rebased uploads. +# G5 Never use --depth on the incremental fetch -- the closure must be complete +# and non-shallow. +# G8 Operate on a BARE clone (no worktree) so the branch ref can be updated +# without the "branch used by worktree" failure. +# G10 Do not assume `git fetch` always exits 0 (a re-pointed upstream tag yields +# a non-zero "would clobber existing tag" -- handled explicitly). +# +# Usage: +# sync-mirror.sh +# +# Required environment: +# MIRROR_URL Authenticated push URL of THIS repo +# (e.g. https://x-access-token:TOKEN@github.com/org/repo.git) +# UPSTREAM_URL Canonical/Launchpad git URL to mirror from +# BRANCH Branch to advance (e.g. resolute-qcom) +# UPSTREAM_PREFIX Upstream tag prefix, mirrored verbatim (e.g. Ubuntu-qcom) +# +# Optional environment: +# WORKDIR Scratch directory for the bare mirror (default: mktemp) +# MIN_HISTORY_COMMITS Bootstrap sentinel: refuse to sync if BRANCH has fewer +# than this many commits, i.e. it has not been seeded with +# real history yet (default: 1000). Run bootstrap first. +# GITHUB_OUTPUT If set, the latest synced version is written as +# `synced_version=` and `synced_count=` for the caller. +# +# Exit codes: +# 0 Up to date or one-or-more uploads synced successfully +# 1 Hard error (bootstrap required, push rejected, etc.) + +set -euo pipefail + +# --------------------------------------------------------------------------- +# Logging helpers +# --------------------------------------------------------------------------- +log() { printf '[%s] %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$*" >&2; } +die() { log "ERROR: $*"; exit 1; } +hr() { log "────────────────────────────────────────────────────────────"; } + +# --------------------------------------------------------------------------- +# Validate inputs +# --------------------------------------------------------------------------- +: "${MIRROR_URL:?MIRROR_URL is required}" +: "${UPSTREAM_URL:?UPSTREAM_URL is required}" +: "${BRANCH:?BRANCH is required}" +: "${UPSTREAM_PREFIX:?UPSTREAM_PREFIX is required}" + +MIN_HISTORY_COMMITS="${MIN_HISTORY_COMMITS:-1000}" +WORKDIR="${WORKDIR:-$(mktemp -d)}" +MIRROR="${WORKDIR}/mirror.git" + +# Redact credentials from any URL before printing it. +redact() { sed -E 's#(https?://)[^@/]*@#\1***@#g' <<<"$1"; } + +hr +log "Canonical kernel mirror sync (history-preserving)" +log " Branch : ${BRANCH}" +log " Upstream : $(redact "${UPSTREAM_URL}")" +log " Tags (verbatim) : ${UPSTREAM_PREFIX}-*" +hr + +# --------------------------------------------------------------------------- +# 1. Clone OUR mirror as a BARE repo (G8: no worktree -> clean ref updates). +# --filter=blob:none keeps the clone small: we need the commit/tree graph for +# fetch negotiation and ref moves, not the file blobs (the upstream fetch +# brings the new blobs, and the push only sends objects the mirror lacks). +# --single-branch limits it to BRANCH so unrelated suite branches are not +# pulled. +# --------------------------------------------------------------------------- +log "Cloning mirror (bare, blobless, single-branch ${BRANCH})..." +rm -rf "${MIRROR}" # self-heal if an operator reuses a fixed WORKDIR +git clone --bare --filter=blob:none --single-branch --branch "${BRANCH}" \ + "${MIRROR_URL}" "${MIRROR}" \ + || die "Mirror does not yet contain branch '${BRANCH}'. Run bootstrap-history first." +cd "${MIRROR}" + +# --------------------------------------------------------------------------- +# 2. Bootstrap sentinel: the incremental model only works if BRANCH already +# carries real upstream history (so the upstream fetch transfers a small +# delta, not the whole tree). A freshly created or legacy-squashed branch has +# a handful of commits -- refuse, and point the operator at the bootstrap. +# --------------------------------------------------------------------------- +HISTORY_COUNT="$(git rev-list --count "${BRANCH}")" +log "Branch '${BRANCH}' currently has ${HISTORY_COUNT} commits." +if [ "${HISTORY_COUNT}" -lt "${MIN_HISTORY_COMMITS}" ]; then + die "Branch '${BRANCH}' has only ${HISTORY_COUNT} commits (< ${MIN_HISTORY_COMMITS}); \ +it has not been seeded with full Canonical history. Run the 'Bootstrap' workflow first." +fi + +# A bare mirror must never be shallow (G5): a shallow base breaks negotiation and +# GitHub rejects shallow pushes outright. +if [ -f shallow ]; then + die "Mirror clone is shallow -- refusing to sync. The seed must be fully \ +unshallowed before incremental syncs can run." +fi + +# --------------------------------------------------------------------------- +# 3. Discover which upstream uploads we have not mirrored yet. +# Missing = upstream ${UPSTREAM_PREFIX}- tags not yet present in our +# mirror (we mirror Canonical's tag names verbatim). Sorted ascending so a +# rebased middle upload is preserved before the branch advances past it (G4). +# --------------------------------------------------------------------------- +git remote add upstream "${UPSTREAM_URL}" + +# Capture ls-remote first so a transport failure (network/auth/5xx) is not +# silently flattened to an empty list and misreported as "no tags upstream". +upstream_raw="$(git ls-remote --tags upstream "refs/tags/${UPSTREAM_PREFIX}-*")" \ + || die "git ls-remote failed for upstream (network/auth?)." +mapfile -t UPSTREAM_VERSIONS < <( + printf '%s\n' "${upstream_raw}" \ + | grep -v '\^{}' \ + | sed -E "s#.*refs/tags/${UPSTREAM_PREFIX}-##" \ + | sort -V +) +[ "${#UPSTREAM_VERSIONS[@]}" -gt 0 ] \ + || die "No ${UPSTREAM_PREFIX}-* tags found upstream." + +# "Missing" is judged against the tags ACTUALLY ON THE MIRROR (ls-remote origin), +# NOT the local clone: the clone is --single-branch, so it only holds tags +# reachable from the branch tip. A rebased/divergent upload we already mirrored is +# invisible locally, and re-listing it here would make the atomic push below fail +# with "tag already exists". This matches how the check-version gate decides what +# is new (fetch-source-pkg.yml). +mirror_raw="$(git ls-remote --tags origin "refs/tags/${UPSTREAM_PREFIX}-*")" \ + || die "git ls-remote failed for the mirror (origin)." +mirrored_versions="$( + printf '%s\n' "${mirror_raw}" \ + | grep -v '\^{}' \ + | sed -E "s#.*refs/tags/${UPSTREAM_PREFIX}-##" || true +)" + +MISSING=() +for ver in "${UPSTREAM_VERSIONS[@]}"; do + # -F: the version is a fixed string (dots are literal, not globs). + grep -qxF "${ver}" <<<"${mirrored_versions}" || MISSING+=("${ver}") +done + +if [ "${#MISSING[@]}" -eq 0 ]; then + log "Already up to date -- no new uploads to mirror." + [ -n "${GITHUB_OUTPUT:-}" ] && { + echo "synced_version=" >> "${GITHUB_OUTPUT}" + echo "synced_count=0" >> "${GITHUB_OUTPUT}" + } + exit 0 +fi + +log "Uploads to mirror (ascending): ${MISSING[*]}" + +# --------------------------------------------------------------------------- +# 4. Mirror each missing upload in order: fetch the upstream tag (delta) -> +# advance branch -> atomic, lease-pinned push of branch + the upstream tag. +# Halt on the first failure (G3). +# --------------------------------------------------------------------------- +SYNCED=0 +LAST_VERSION="" +for ver in "${MISSING[@]}"; do + hr + log "Mirroring upload ${ver}" + + upstream_tag="${UPSTREAM_PREFIX}-${ver}" + + # Lease baseline (G1): the branch value we are advancing FROM. Empty if the + # branch somehow vanished between clone and now (treated as a create). + old_sha="$(git rev-parse -q --verify "refs/heads/${BRANCH}" || true)" + + # Fetch ONLY this upload's tag, full depth (G5), into the SAME ref name so the + # upstream annotated tag object becomes our preservation tag verbatim -- we + # mirror Canonical's tag names exactly. Because the mirror already holds the + # shared base, negotiation transfers just the new objects. A re-pointed + # upstream tag returns non-zero "would clobber" (G10) -- surface it clearly + # rather than letting `set -e` report a generic failure. + if ! git fetch --no-tags upstream \ + "refs/tags/${upstream_tag}:refs/tags/${upstream_tag}" 2>fetch.err; then + if grep -q 'would clobber existing tag' fetch.err; then + die "Upstream tag '${upstream_tag}' was re-pointed (immutability violation \ +upstream). Refusing to move our preservation tag. Manual review required." + fi + cat fetch.err >&2 + die "Failed to fetch upstream tag '${upstream_tag}'." + fi + + new_sha="$(git rev-parse "${upstream_tag}^{commit}")" + + # Advance the movable "latest" pointer (bare repo: update-ref == branch -f, + # with no worktree guard, G8). The upstream tag fetched above is already the + # immutable per-upload record (G4); no re-tagging needed. + git update-ref "refs/heads/${BRANCH}" "${new_sha}" + + # Push the branch and the tag ATOMICALLY (G2) with a PINNED lease (G1). + # An empty old_sha means "create" -- assert the remote ref is absent. + # Array (not a bare string) so a BRANCH with unexpected characters cannot + # word-split or glob the push arguments. + if [ -n "${old_sha}" ]; then + lease=("--force-with-lease=refs/heads/${BRANCH}:${old_sha}") + else + lease=("--force-with-lease=refs/heads/${BRANCH}:") + fi + + log "Pushing branch + tag atomically (lease pinned to ${old_sha:-})..." + if ! git push --atomic "${lease[@]}" origin \ + "refs/heads/${BRANCH}" \ + "refs/tags/${upstream_tag}"; then + die "Atomic push rejected for ${ver} (stale lease or protected ref). \ +Halting so the next run does not mask a partial sync (G3)." + fi + + log "Mirrored ${ver}: branch ${BRANCH} -> ${new_sha:0:12}, tag ${upstream_tag}" + SYNCED=$((SYNCED + 1)) + LAST_VERSION="${ver}" +done + +# --------------------------------------------------------------------------- +# 5. Guarantee the branch ends at the NEWEST upload. The loop advances the branch +# as it mirrors, but a late-arriving OLDER upload (backfill) or a prior partial +# run can leave the branch behind the newest tag. Force it forward so the mirror +# HEAD is always the latest upload (a no-op when the loop already ended there). +# --------------------------------------------------------------------------- +newest="${UPSTREAM_VERSIONS[-1]}" +newest_tag="${UPSTREAM_PREFIX}-${newest}" +git rev-parse -q --verify "${newest_tag}^{commit}" >/dev/null 2>&1 \ + || git fetch --no-tags origin "refs/tags/${newest_tag}:refs/tags/${newest_tag}" 2>/dev/null \ + || git fetch --no-tags upstream "refs/tags/${newest_tag}:refs/tags/${newest_tag}" \ + || die "Could not obtain newest tag ${newest_tag} to position the branch." +newest_sha="$(git rev-parse "${newest_tag}^{commit}")" +current_sha="$(git rev-parse -q --verify "refs/heads/${BRANCH}" || true)" +if [ "${newest_sha}" != "${current_sha}" ]; then + log "Advancing branch ${BRANCH} to newest upload ${newest} (${current_sha:0:12} -> ${newest_sha:0:12})..." + git update-ref "refs/heads/${BRANCH}" "${newest_sha}" + git push --force-with-lease="refs/heads/${BRANCH}:${current_sha}" origin "refs/heads/${BRANCH}" \ + || die "Failed to advance branch to newest upload ${newest} (stale lease or protected ref). Halting (G3)." +fi +LAST_VERSION="${newest}" + +hr +log "Sync complete: ${SYNCED} upload(s) mirrored; branch now at ${LAST_VERSION}." + +if [ -n "${GITHUB_OUTPUT:-}" ]; then + echo "synced_version=${LAST_VERSION}" >> "${GITHUB_OUTPUT}" + echo "synced_count=${SYNCED}" >> "${GITHUB_OUTPUT}" +fi diff --git a/scripts/validate-distro-validation-callback.sh b/scripts/validate-distro-validation-callback.sh new file mode 100755 index 0000000000000..224a9b4036508 --- /dev/null +++ b/scripts/validate-distro-validation-callback.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause +# +set -euo pipefail + +: "${GITHUB_EVENT_PATH:?GITHUB_EVENT_PATH is required}" +: "${BUCKET:?BUCKET is required}" + +emit_output() { + echo "$1=$2" >> "$GITHUB_OUTPUT" +} + +payload="$(jq '.client_payload' "$GITHUB_EVENT_PATH")" +request_id="$(jq -r '.request_id' <<< "$payload")" +kernel_build_id="$(jq -r '.kernel_build_id' <<< "$payload")" +kernel_s3_prefix="$(jq -r '.kernel_s3_prefix' <<< "$payload")" +pr_number="$(jq -r '.pr_number' <<< "$payload")" +head_sha="$(jq -r '.head_sha' <<< "$payload")" +distro_result="$(jq -r '.distro_result' <<< "$payload")" +distro_run_id="$(jq -r '.distro_run_id' <<< "$payload")" +distro_run_attempt="$(jq -r '.distro_run_attempt' <<< "$payload")" + +[[ "$(jq -r '.action' "$GITHUB_EVENT_PATH")" == "canonical-premerge-distro-result" ]] || { echo "::error::Unexpected callback event type." >&2; exit 1; } +[[ "$kernel_build_id" =~ ^[0-9]+-[0-9]+$ ]] || { echo "::error::Invalid kernel build ID." >&2; exit 1; } +[[ "$kernel_s3_prefix" == "pkg/premerge/pkg-linux-qcom-canonical" ]] || { echo "::error::Unexpected kernel S3 prefix." >&2; exit 1; } +[[ "$pr_number" =~ ^[0-9]+$ ]] || { echo "::error::Invalid pull request number." >&2; exit 1; } +[[ "$head_sha" =~ ^[0-9a-f]{40}$ ]] || { echo "::error::Invalid pull request head SHA." >&2; exit 1; } +[[ "$distro_result" =~ ^(success|failure|cancelled|skipped)$ ]] || { echo "::error::Invalid distro result." >&2; exit 1; } +[[ "$distro_run_id" =~ ^[0-9]+$ && "$distro_run_attempt" =~ ^[0-9]+$ ]] || { echo "::error::Invalid distro run identity." >&2; exit 1; } +[[ "$request_id" == "${kernel_build_id}-${head_sha}" ]] || { echo "::error::Request ID does not match the kernel build and head SHA." >&2; exit 1; } + +IFS=- read -r kernel_run_id kernel_run_attempt <<< "$kernel_build_id" +kernel_run="$(gh api "repos/qualcomm-linux/pkg-linux-qcom-canonical/actions/runs/${kernel_run_id}")" +jq -e \ + --argjson attempt "$kernel_run_attempt" \ + --arg head_sha "$head_sha" \ + '.event == "pull_request" and + .path == ".github/workflows/premerge-pr.yml" and + .run_attempt == $attempt and + .conclusion == "success" and + .head_sha == $head_sha' \ + <<< "$kernel_run" >/dev/null || { + echo "::error::Kernel workflow run does not match the callback context." >&2 + exit 1 + } + +pull_requests="$( + gh api "repos/qualcomm-linux/pkg-linux-qcom-canonical/commits/${head_sha}/pulls" \ + -H "Accept: application/vnd.github+json" \ + --jq '[.[] | select(.base.ref == "resolute-qcom-devel" and .head.sha == "'"$head_sha"'")]' +)" +[[ "$(jq 'length' <<< "$pull_requests")" == "1" ]] || { echo "::error::Unable to identify one matching Canonical pull request." >&2; exit 1; } +[[ "$(jq -r '.[0].number' <<< "$pull_requests")" == "$pr_number" ]] || { echo "::error::Callback pull request number does not match the kernel commit." >&2; exit 1; } + +distro_run="$(gh api "repos/qualcomm-linux/qcom-distro-images/actions/runs/${distro_run_id}")" +jq -e \ + --argjson attempt "$distro_run_attempt" \ + '.event == "repository_dispatch" and + .path == ".github/workflows/canonical-premerge-validation.yml" and + .run_attempt == $attempt' \ + <<< "$distro_run" >/dev/null || { + echo "::error::Distro workflow run does not match the callback context." >&2 + exit 1 + } + +distro_build_id="${distro_run_id}-${distro_run_attempt}" +distro_ref="$(jq -r '.head_sha' <<< "$distro_run")" +image_s3_prefix="qualcomm-linux/${kernel_s3_prefix}/${kernel_build_id}" +[[ "$distro_ref" =~ ^[0-9a-f]{40}$ ]] || { echo "::error::Invalid distro source SHA." >&2; exit 1; } + +distro_run_url="$(jq -r '.html_url' <<< "$distro_run")" + +state=failure +description="Canonical distro image validation failed" +check_conclusion="$distro_result" +if [[ "$distro_result" == "success" ]]; then + marker="$(mktemp)" + trap 'rm -f "$marker"' EXIT + aws s3 cp "s3://${BUCKET}/${image_s3_prefix}/distro-validation.json" "$marker" >/dev/null + jq -e \ + --arg request_id "$request_id" \ + --arg distro_build_id "$distro_build_id" \ + --arg distro_ref "$distro_ref" \ + --arg kernel_build_id "$kernel_build_id" \ + --arg kernel_s3_prefix "$kernel_s3_prefix" \ + '.status == "success" and + .request_id == $request_id and + .distro_build_id == $distro_build_id and + .distro_ref == $distro_ref and + .kernel_build_id == $kernel_build_id and + .kernel_s3_prefix == $kernel_s3_prefix and + (.images | sort) == ([ + "qcom-ubuntu-iot-resolute-desktop-canonical.images.tar.gz", + "qcom-ubuntu-iot-resolute-server-canonical.images.tar.gz" + ] | sort)' "$marker" >/dev/null || { + echo "::error::Distro completion marker does not match the callback context." >&2 + exit 1 + } + state=success + description="Canonical server and desktop distro images passed" +fi + +emit_output state "$state" +emit_output description "$description" +emit_output check-conclusion "$check_conclusion" +emit_output pr-number "$pr_number" +emit_output head-sha "$head_sha" +emit_output request-id "$request_id" +emit_output kernel-build-id "$kernel_build_id" +emit_output distro-build-id "$distro_build_id" +emit_output image-s3-prefix "$image_s3_prefix" +emit_output distro-run-url "$distro_run_url"