From b6c9cd9f0daceab3e02ed582940754b68d9015ea Mon Sep 17 00:00:00 2001 From: Tomas Valenta Date: Fri, 8 May 2026 11:55:43 -0700 Subject: [PATCH 01/10] feat: hash-based manual release workflow Mirror the manual build/release pipeline from e2b-dev/fc-versions, but identify each kernel build by a content hash of its inputs (configs + optional patches). Changing a flag or adding a patch yields a new artifact named vmlinux-_. - scripts/validate.py: resolves version names, computes hashes, builds the matrix and skips arches whose artifact is already in the GitHub release. - build.sh: now accepts [arch], computes the same version_name (or honors VERSION_NAME), supports patches//. - .github/workflows/release.yml: workflow_dispatch with validate -> build -> publish (GH release) -> deploy (GCS), skipping work whose artifacts already exist. - Drops the old build-on-every-push workflow; releases are explicit. --- .github/workflows/fc-kernels.yml | 118 ------------------- .github/workflows/release.yml | 191 +++++++++++++++++++++++++++++++ README.md | 55 ++++++--- build.sh | 163 ++++++++++++++++++-------- scripts/validate.py | 174 ++++++++++++++++++++++++++++ 5 files changed, 518 insertions(+), 183 deletions(-) delete mode 100644 .github/workflows/fc-kernels.yml create mode 100644 .github/workflows/release.yml create mode 100755 scripts/validate.py diff --git a/.github/workflows/fc-kernels.yml b/.github/workflows/fc-kernels.yml deleted file mode 100644 index e8e4013..0000000 --- a/.github/workflows/fc-kernels.yml +++ /dev/null @@ -1,118 +0,0 @@ -name: FC Kernels - -on: - push: - -permissions: - id-token: write - contents: write - -jobs: - build: - name: Build kernels (${{ matrix.arch }}) - runs-on: ubuntu-22.04 - strategy: - matrix: - arch: [x86_64, arm64] - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Build kernels - run: sudo TARGET_ARCH=${{ matrix.arch }} make build - - - name: Upload kernels as artifact - uses: actions/upload-artifact@v4 - with: - name: kernels-${{ matrix.arch }}-${{ github.run_id }} - path: ./builds - retention-days: 7 - - publish: - name: Publish kernels - needs: build - if: github.ref_name == 'main' - runs-on: ubuntu-22.04 - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - uses: actions/create-github-app-token@v1 - id: app-token - with: - app-id: ${{ vars.VERSION_BUMPER_APPID }} - private-key: ${{ secrets.VERSION_BUMPER_SECRET }} - - - name: Get the last release - id: last_release - uses: cardinalby/git-get-release-action@v1 - env: - GITHUB_TOKEN: ${{ github.token }} - with: - latest: true - prerelease: false - draft: false - - - name: Get next version - id: get-version - run: | - version=${{ steps.last_release.outputs.tag_name }} - result=$(echo ${version} | awk -F. -v OFS=. '{$NF += 1 ; print}') - echo "version=$result" >> $GITHUB_OUTPUT - - - name: Download all build artifacts - uses: actions/download-artifact@v4 - with: - path: ./builds - merge-multiple: true - - - name: Setup Service Account - uses: google-github-actions/auth@v1 - with: - workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} - service_account: ${{ secrets.GCP_SERVICE_ACCOUNT_EMAIL }} - - - name: Upload kernels to GCS - uses: "google-github-actions/upload-cloud-storage@v1" - with: - path: "./builds" - destination: ${{ vars.GCP_BUCKET_NAME }}/kernels - gzip: false - parent: false - - - name: Create Git tag - run: | - git config user.name "github-actions" - git config user.email "github-actions@github.com" - git tag ${{ steps.get-version.outputs.version }} - git push origin ${{ steps.get-version.outputs.version }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Prepare release assets - run: | - mkdir -p release-assets - for dir in ./builds/*/; do - name=$(basename "$dir") - # Legacy x86_64 (no arch subdir) - if [ -f "$dir/vmlinux.bin" ]; then - cp "$dir/vmlinux.bin" "release-assets/${name}.bin" - fi - # Per-arch binaries - for archdir in "$dir"/*/; do - [ -d "$archdir" ] || continue - arch=$(basename "$archdir") - if [ -f "$archdir/vmlinux.bin" ]; then - cp "$archdir/vmlinux.bin" "release-assets/${name}-${arch}.bin" - fi - done - done - - - name: Upload Release Asset - uses: softprops/action-gh-release@v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - name: Kernels ${{ steps.get-version.outputs.version }} - tag_name: ${{ steps.get-version.outputs.version }} - files: "./release-assets/*" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c9ac163 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,191 @@ +name: Manual Build & Release + +on: + workflow_dispatch: + inputs: + kernel_versions: + description: 'Comma-separated kernel versions to build (default: kernel_versions.txt)' + required: false + type: string + default: '' + build_amd64: + description: 'Build for amd64 architecture' + required: false + type: boolean + default: true + build_arm64: + description: 'Build for arm64 architecture' + required: false + type: boolean + default: true + +permissions: + contents: write + id-token: write + +jobs: + validate: + runs-on: ubuntu-24.04 + outputs: + build_matrix: ${{ steps.validate.outputs.build_matrix }} + versions: ${{ steps.validate.outputs.versions }} + has_new_artifacts: ${{ steps.validate.outputs.has_new_artifacts }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - id: validate + env: + GH_TOKEN: ${{ github.token }} + run: | + python3 scripts/validate.py \ + --kernel-versions "${{ inputs.kernel_versions }}" \ + --build-amd64 "${{ inputs.build_amd64 }}" \ + --build-arm64 "${{ inputs.build_arm64 }}" + + build: + needs: validate + if: needs.validate.outputs.has_new_artifacts == 'true' + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.validate.outputs.build_matrix) }} + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v4 + + - name: Build kernel ${{ matrix.version_name }} (${{ matrix.arch }}) + env: + VERSION_NAME: ${{ matrix.version_name }} + run: | + target_arch="${{ matrix.arch }}" + [ "$target_arch" = "amd64" ] && target_arch="x86_64" + sudo VERSION_NAME="$VERSION_NAME" ./build.sh "${{ matrix.kernel_version }}" "$target_arch" + + - uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.version_name }}-${{ matrix.arch }} + path: ./builds/${{ matrix.version_name }} + retention-days: 7 + + publish: + needs: [validate, build] + if: needs.validate.outputs.has_new_artifacts == 'true' + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/download-artifact@v4 + with: + path: ./builds + merge-multiple: true + + - name: Show build artifacts + run: find ./builds -type f | head -50 + + - name: Create or update releases + env: + GH_TOKEN: ${{ github.token }} + VERSIONS: ${{ needs.validate.outputs.versions }} + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + echo "$VERSIONS" | jq -c '.[]' | while read -r entry; do + version_name=$(echo "$entry" | jq -r '.version_name') + kernel_version=$(echo "$entry" | jq -r '.kernel_version') + + if ! gh release view "$version_name" >/dev/null 2>&1; then + if ! git rev-parse "refs/tags/$version_name" >/dev/null 2>&1; then + git tag "$version_name" + git push origin "$version_name" + fi + gh release create "$version_name" \ + --title "Kernel $version_name" \ + --notes "Linux kernel $kernel_version built from configs at ${{ github.sha }}" + fi + + existing=$(gh release view "$version_name" --json assets -q '.assets[].name' || true) + + for arch in amd64 arm64; do + local_path="./builds/$version_name/$arch/vmlinux.bin" + [ -f "$local_path" ] || continue + + asset="vmlinux-${arch}.bin" + if echo "$existing" | grep -qx "$asset"; then + echo "Release $version_name: $asset exists, skipping" + else + tmp=$(mktemp -d)/$asset + cp "$local_path" "$tmp" + gh release upload "$version_name" "$tmp" + rm -f "$tmp" + fi + + # Legacy non-arch-suffixed asset (amd64 only) for backwards compat. + if [ "$arch" = "amd64" ] && ! echo "$existing" | grep -qx "vmlinux.bin"; then + tmp=$(mktemp -d)/vmlinux.bin + cp "$local_path" "$tmp" + gh release upload "$version_name" "$tmp" + rm -f "$tmp" + fi + done + done + + deploy: + needs: [validate, publish] + if: needs.validate.outputs.has_new_artifacts == 'true' + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.GCP_SERVICE_ACCOUNT_EMAIL }} + + - uses: google-github-actions/setup-gcloud@v2 + + - name: Download release assets and upload to GCS + env: + GH_TOKEN: ${{ github.token }} + GCP_BUCKET_NAME: ${{ vars.GCP_BUCKET_NAME }} + VERSIONS: ${{ needs.validate.outputs.versions }} + run: | + set -euo pipefail + echo "$VERSIONS" | jq -c '.[]' | while read -r entry; do + version_name=$(echo "$entry" | jq -r '.version_name') + + for arch in amd64 arm64; do + asset="vmlinux-${arch}.bin" + dl_dir="./dl/$version_name/$arch" + mkdir -p "$dl_dir" + if ! gh release download "$version_name" \ + --repo "${{ github.repository }}" \ + --pattern "$asset" \ + --output "$dl_dir/vmlinux.bin" 2>/dev/null; then + continue + fi + + gcs_path="gs://${GCP_BUCKET_NAME}/kernels/${version_name}/${arch}/vmlinux.bin" + if gcloud storage ls "$gcs_path" >/dev/null 2>&1; then + echo "GCS: $gcs_path exists, skipping" + else + gcloud storage cp "$dl_dir/vmlinux.bin" "$gcs_path" + fi + + # Legacy non-arch path (amd64 only) for backwards compat. + if [ "$arch" = "amd64" ]; then + legacy_path="gs://${GCP_BUCKET_NAME}/kernels/${version_name}/vmlinux.bin" + if gcloud storage ls "$legacy_path" >/dev/null 2>&1; then + echo "GCS: $legacy_path exists, skipping" + else + gcloud storage cp "$dl_dir/vmlinux.bin" "$legacy_path" + fi + fi + done + done diff --git a/README.md b/README.md index b1e1579..04e6bee 100644 --- a/README.md +++ b/README.md @@ -2,40 +2,59 @@ ## Overview -This project automates the building of custom Linux kernels for Firecracker microVMs, using the same kernel sources as official Firecracker repo and custom configuration files. It supports building specific kernel versions and uploading the resulting binaries to a Google Cloud Storage (GCS) bucket. +This project builds custom Linux kernels for Firecracker microVMs from the same kernel sources as the official Firecracker repo, using the configuration files (and optional patches) that live in this repo. + +Each kernel build is identified by a content hash of its inputs (configs + patches), so changing a flag or adding a patch produces a new, traceable artifact: + +``` +vmlinux-_ +``` ## Prerequisites - Linux environment (for building kernels) -## Building Kernels +## Building locally 1. **Configure kernel versions:** - - Edit `kernel_versions.txt` to specify which kernel versions to build (one per line, e.g., `6.1.102`). - - Place the corresponding config file in `configs/` (e.g., `configs/6.1.102.config`). + - Edit `kernel_versions.txt` to specify which kernel versions to build (one per line, e.g. `6.1.158`). + - Place the corresponding config(s) in `configs/x86_64/.config` and `configs/arm64/.config`. + - (Optional) Drop `*.patch` files into `patches//` to apply on top of the upstream tree before build. 2. **Build:** ```sh - make build - # or directly - ./build.sh + make build # builds all versions in kernel_versions.txt for x86_64 + make build-arm64 # same, for arm64 + ./build.sh 6.1.158 # build a single version (x86_64) + ./build.sh 6.1.158 arm64 ``` - The built kernels will be placed in `builds/vmlinux-//vmlinux.bin` where `` is `amd64` or `arm64` (Go/OCI convention). For x86_64 backward compatibility, a legacy copy is also placed at `builds/vmlinux-/vmlinux.bin`. -## Development Workflow - - On every push, GitHub Actions will automatically build the kernels and save it as an artifact. + Output: `builds/vmlinux-_//vmlinux.bin` where `` is `amd64` or `arm64` (Go/OCI convention). For x86_64 a legacy copy is also placed at `builds/vmlinux-_/vmlinux.bin`. + +## Releasing + +1. Run the **Manual Build & Release** workflow (Actions → Manual Build & Release → Run workflow). +2. The workflow: + - Computes a content hash for each kernel version from its configs and patches. + - Skips arches whose artifact is already present in the matching GitHub release. + - Builds the missing arches, creates/updates the `vmlinux-_` release, uploads `vmlinux-amd64.bin` / `vmlinux-arm64.bin` (and a legacy `vmlinux.bin` for amd64), and pushes the same files to GCS under `gs://$GCP_BUCKET_NAME/kernels//`. + +### Workflow inputs + +- `kernel_versions` (optional): comma-separated kernel versions. Defaults to all versions in `kernel_versions.txt`. +- `build_amd64` / `build_arm64` (optional, default `true`): which architectures to build. + +## New kernel in E2B's infra +_Note: these steps should give you a new kernel on your self-hosted E2B using https://github.com/e2b-dev/infra_ + +- Run the release workflow to publish the new kernel build. +- Update `DefaultKernelVersion` in [packages/api/internal/cfg/model.go](https://github.com/e2b-dev/infra/blob/main/packages/api/internal/cfg/model.go) to the new `vmlinux-_` name. +- Build and deploy `api`. ## Architecture naming Output directories use Go's `runtime.GOARCH` convention (`amd64`, `arm64`) so they match the infra orchestrator's `TargetArch()` path resolution. The build-time variable `TARGET_ARCH` (`x86_64`, `arm64`) is only used internally for config paths and cross-compilation flags. -## New Kernel in E2B's infra -_Note: these steps should give you new kernel on your self-hosted E2B using https://github.com/e2b-dev/infra_ - - - Copy the kernel build in your project's object storage under `e2b-*-fc-kernels` - - In [packages/api/internal/cfg/model.go](https://github.com/e2b-dev/infra/blob/main/packages/api/internal/cfg/model.go) update `DefaultKernelVersion` - - Build and deploy `api` - ## License -This project is licensed under the Apache License 2.0. See [LICENSE](LICENSE) for details. +This project is licensed under the Apache License 2.0. See [LICENSE](LICENSE) for details. diff --git a/build.sh b/build.sh index 209ee22..0e7391e 100755 --- a/build.sh +++ b/build.sh @@ -3,13 +3,20 @@ set -euo pipefail -# TARGET_ARCH: x86_64 (default) or arm64 -TARGET_ARCH="${TARGET_ARCH:-x86_64}" +# Usage: +# ./build.sh # build all versions in kernel_versions.txt for $TARGET_ARCH +# ./build.sh [arch] # build a single version +# VERSION_NAME=vmlinux-_ ./build.sh +# +# arch is one of: x86_64 (default), arm64 (kernel-style names). +# The output goes to builds///vmlinux.bin where +# is the Go/OCI name (amd64/arm64) used by the orchestrator. +# If VERSION_NAME isn't provided, it's computed deterministically from the +# kernel version's configs and patches: vmlinux-_. + HOST_ARCH="$(uname -m)" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# Go/OCI-normalized arch name for output directory structure. -# The infra orchestrator uses Go's runtime.GOARCH convention (amd64/arm64) -# for path resolution, so output directories must match. normalize_arch() { case "$1" in x86_64) echo "amd64" ;; @@ -17,43 +24,90 @@ normalize_arch() { *) echo "$1" ;; esac } -OUTPUT_ARCH="$(normalize_arch "$TARGET_ARCH")" -function install_dependencies { - local packages=( - bc bison busybox-static cpio curl flex gcc libelf-dev libssl-dev make patch squashfs-tools tree - ) +# Returns vmlinux-_. Hash inputs: configs/{x86_64,arm64}/.config +# and any files under patches//. Must match scripts/validate.py. +compute_version_name() { + local version="$1" + local files=() + for arch in x86_64 arm64; do + local cfg="$SCRIPT_DIR/configs/$arch/${version}.config" + [ -f "$cfg" ] && files+=("$cfg") + done + if [ -d "$SCRIPT_DIR/patches/$version" ]; then + while IFS= read -r -d '' f; do + files+=("$f") + done < <(find "$SCRIPT_DIR/patches/$version" -type f -print0 | sort -z) + fi + if [ "${#files[@]}" -eq 0 ]; then + echo "Error: no configs found for kernel version $version" >&2 + exit 1 + fi + local h + h=$( + for f in "${files[@]}"; do + printf '%s\0' "${f#"$SCRIPT_DIR/"}" + cat "$f" + printf '\0' + done | sha256sum | awk '{print $1}' | cut -c1-7 + ) + echo "vmlinux-${version}_${h}" +} - [[ "${TARGET_ARCH}" == "arm64" && "${HOST_ARCH}" != "aarch64" ]] && packages+=( gcc-aarch64-linux-gnu ) +install_dependencies() { + local target_arch="$1" + local packages=( + bc bison busybox-static cpio curl flex gcc libelf-dev libssl-dev make patch squashfs-tools tree + ) - apt update - apt install -y "${packages[@]}" + [[ "$target_arch" == "arm64" && "$HOST_ARCH" != "aarch64" ]] && packages+=( gcc-aarch64-linux-gnu ) + + apt update + apt install -y "${packages[@]}" } -# prints the git tag corresponding to the newest and best matching the provided kernel version $1 -function get_tag { - local KERNEL_VERSION="${1}" +# Newest-tag matching the requested kernel version. +get_tag() { + local kernel_version="$1" + { + git --no-pager tag -l --sort=-creatordate | grep "microvm-kernel-${kernel_version}-.*\.amzn2" \ + || git --no-pager tag -l --sort=-creatordate | grep "kernel-${kernel_version}-.*\.amzn2" + } | head -n1 +} - # list all tags from newest to oldest - { - git --no-pager tag -l --sort=-creatordate | grep "microvm-kernel-${KERNEL_VERSION}-.*\.amzn2" \ - || git --no-pager tag -l --sort=-creatordate | grep "kernel-${KERNEL_VERSION}-.*\.amzn2" - } | head -n1 +apply_patches() { + local version="$1" + local patches_dir="$SCRIPT_DIR/patches/$version" + [ -d "$patches_dir" ] || return 0 + shopt -s nullglob + local patches=("$patches_dir"/*.patch) + shopt -u nullglob + [ "${#patches[@]}" -gt 0 ] || return 0 + echo "Applying ${#patches[@]} patch(es) for $version" + for p in "${patches[@]}"; do + git apply --check "$p" + git apply "$p" + done } -function build_version { - local version=$1 - echo "Starting build for kernel version: $version (${TARGET_ARCH})" +build_version() { + local version="$1" + local target_arch="$2" + local version_name="$3" + local output_arch + output_arch="$(normalize_arch "$target_arch")" + + echo "Starting build for kernel version: $version (${target_arch}) -> $version_name" - # Configs live in configs/{arch}/ - cp ../configs/"${TARGET_ARCH}/${version}.config" .config + cp "$SCRIPT_DIR/configs/${target_arch}/${version}.config" .config echo "Checking out repo for kernel at version: $version" git checkout "$(get_tag "$version")" - # Set up cross-compilation if building arm64 on x86_64 + apply_patches "$version" + local make_opts="" - if [[ "$TARGET_ARCH" == "arm64" ]]; then + if [[ "$target_arch" == "arm64" ]]; then if [[ "$HOST_ARCH" != "aarch64" ]]; then make_opts="ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-" else @@ -63,39 +117,54 @@ function build_version { echo "Building kernel version: $version" make $make_opts olddefconfig - - if [[ "$TARGET_ARCH" == "arm64" ]]; then + + if [[ "$target_arch" == "arm64" ]]; then make $make_opts Image -j "$(nproc)" else make $make_opts vmlinux -j "$(nproc)" fi echo "Copying finished build to builds directory" - # Output to normalized arch dir (amd64/arm64) matching Go's runtime.GOARCH - mkdir -p "../builds/vmlinux-${version}/${OUTPUT_ARCH}" - if [[ "$TARGET_ARCH" == "arm64" ]]; then - cp arch/arm64/boot/Image "../builds/vmlinux-${version}/${OUTPUT_ARCH}/vmlinux.bin" + local out_dir="$SCRIPT_DIR/builds/${version_name}/${output_arch}" + mkdir -p "$out_dir" + if [[ "$target_arch" == "arm64" ]]; then + cp arch/arm64/boot/Image "$out_dir/vmlinux.bin" else - cp vmlinux "../builds/vmlinux-${version}/${OUTPUT_ARCH}/vmlinux.bin" + cp vmlinux "$out_dir/vmlinux.bin" fi - # x86_64: also copy to legacy path (no arch subdir) for backwards compat - if [[ "$TARGET_ARCH" == "x86_64" ]]; then - cp vmlinux "../builds/vmlinux-${version}/vmlinux.bin" + # x86_64: also copy to legacy path (no arch subdir) for backwards compat. + if [[ "$target_arch" == "x86_64" ]]; then + cp vmlinux "$SCRIPT_DIR/builds/${version_name}/vmlinux.bin" fi } -echo "Building kernels for ${TARGET_ARCH}" +ensure_linux_repo() { + cd "$SCRIPT_DIR" + [ -d linux ] || git clone --no-checkout --filter=tree:0 https://github.com/amazonlinux/linux + cd linux + make distclean || true +} -install_dependencies +main() { + local single_version="${1:-}" + local target_arch="${2:-${TARGET_ARCH:-x86_64}}" -[ -d linux ] || git clone --no-checkout --filter=tree:0 https://github.com/amazonlinux/linux -pushd linux + install_dependencies "$target_arch" -make distclean || true + ensure_linux_repo -grep -v '^ *#' <../kernel_versions.txt | while IFS= read -r version; do - build_version "$version" -done + if [[ -n "$single_version" ]]; then + local version_name="${VERSION_NAME:-$(compute_version_name "$single_version")}" + build_version "$single_version" "$target_arch" "$version_name" + else + grep -v '^ *#' <"$SCRIPT_DIR/kernel_versions.txt" | while IFS= read -r version; do + [ -z "$version" ] && continue + local version_name + version_name="$(compute_version_name "$version")" + build_version "$version" "$target_arch" "$version_name" + done + fi +} -popd +main "$@" diff --git a/scripts/validate.py b/scripts/validate.py new file mode 100755 index 0000000..ff02d73 --- /dev/null +++ b/scripts/validate.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +"""Validate inputs and resolve kernel build matrix. + +Each kernel version is identified by a content hash of its configs and +patches. The version_name is `vmlinux-_`. We skip +arch+version pairs whose artifact already exists in the GitHub release. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import subprocess +import sys +from pathlib import Path +from typing import Iterable + +REPO_ROOT = Path(__file__).resolve().parent.parent +HASH_LEN = 7 + +ARCH_TO_RUNNER = { + "amd64": "ubuntu-24.04", + "arm64": "ubuntu-24.04-arm", +} + + +def read_default_versions() -> list[str]: + versions_file = REPO_ROOT / "kernel_versions.txt" + versions: list[str] = [] + for raw in versions_file.read_text().splitlines(): + line = raw.split("#", 1)[0].strip() + if line: + versions.append(line) + return versions + + +def parse_versions(arg: str) -> list[str]: + if not arg.strip(): + return read_default_versions() + return [v.strip() for v in arg.split(",") if v.strip()] + + +def hash_inputs_for_version(version: str) -> str: + """Hash the configs and any patches that determine this kernel build.""" + paths: list[Path] = [] + for arch in ("x86_64", "arm64"): + cfg = REPO_ROOT / "configs" / arch / f"{version}.config" + if cfg.is_file(): + paths.append(cfg) + patches_dir = REPO_ROOT / "patches" / version + if patches_dir.is_dir(): + paths.extend(sorted(p for p in patches_dir.rglob("*") if p.is_file())) + + if not paths: + raise SystemExit(f"::error::No configs found for kernel version {version}") + + digest = hashlib.sha256() + for path in paths: + rel = path.relative_to(REPO_ROOT).as_posix() + digest.update(rel.encode()) + digest.update(b"\0") + digest.update(path.read_bytes()) + digest.update(b"\0") + return digest.hexdigest()[:HASH_LEN] + + +def gh_release_assets(version_name: str) -> set[str]: + """Return existing asset names for the given release, empty if none.""" + result = subprocess.run( + ["gh", "release", "view", version_name, "--json", "assets", "-q", ".assets[].name"], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + return set() + return {line for line in result.stdout.strip().split("\n") if line} + + +def asset_name(arch: str) -> str: + return f"vmlinux-{arch}.bin" + + +def build_entries( + versions: list[str], + archs: list[str], +) -> tuple[list[dict], list[dict]]: + """Return (build_matrix_entries, all_version_entries).""" + build_entries: list[dict] = [] + version_entries: list[dict] = [] + for version in versions: + version_hash = hash_inputs_for_version(version) + version_name = f"vmlinux-{version}_{version_hash}" + existing = gh_release_assets(version_name) + version_entries.append({ + "kernel_version": version, + "version_hash": version_hash, + "version_name": version_name, + }) + for arch in archs: + if asset_name(arch) in existing: + print( + f"{version_name}/{arch}: artifact exists, skipping build", + file=sys.stderr, + ) + continue + build_entries.append({ + "kernel_version": version, + "arch": arch, + "version_hash": version_hash, + "version_name": version_name, + "runner": ARCH_TO_RUNNER[arch], + }) + return build_entries, version_entries + + +def write_github_output(outputs: dict[str, str]) -> None: + out_path = os.environ.get("GITHUB_OUTPUT") + if not out_path: + for k, v in outputs.items(): + print(f"{k}={v}") + return + with open(out_path, "a") as f: + for k, v in outputs.items(): + f.write(f"{k}={v}\n") + + +def collect_archs(amd64: bool, arm64: bool) -> list[str]: + archs: list[str] = [] + if amd64: + archs.append("amd64") + if arm64: + archs.append("arm64") + return archs + + +def main(argv: Iterable[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="Validate kernel release inputs") + parser.add_argument("--kernel-versions", default="", + help="Comma-separated kernel versions (default: kernel_versions.txt)") + parser.add_argument("--build-amd64", type=lambda x: x.lower() == "true", default=True) + parser.add_argument("--build-arm64", type=lambda x: x.lower() == "true", default=True) + args = parser.parse_args(list(argv) if argv is not None else None) + + archs = collect_archs(args.build_amd64, args.build_arm64) + if not archs: + print("::error::At least one architecture must be selected", file=sys.stderr) + return 1 + + versions = parse_versions(args.kernel_versions) + if not versions: + print("::error::No kernel versions to build", file=sys.stderr) + return 1 + + build_list, version_list = build_entries(versions, archs) + + matrix = {"include": build_list} if build_list else {"include": [{"skip": "true"}]} + + print(f"Versions: {[v['version_name'] for v in version_list]}", file=sys.stderr) + print(f"Build matrix: {json.dumps(matrix)}", file=sys.stderr) + print(f"Has new artifacts: {bool(build_list)}", file=sys.stderr) + + write_github_output({ + "build_matrix": json.dumps(matrix), + "versions": json.dumps(version_list), + "has_new_artifacts": "true" if build_list else "false", + }) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 725cae3c4f253c0e879c915316a21a5c13172e9c Mon Sep 17 00:00:00 2001 From: Tomas Valenta Date: Fri, 8 May 2026 12:46:46 -0700 Subject: [PATCH 02/10] fix: preserve version_name in artifact, force checkout - upload-artifact: keep version_name in the artifact tree so the publish step's ./builds///vmlinux.bin lookup resolves after merge-multiple download. - build.sh: git checkout -f so a dirty tree from a prior iteration's apply_patches doesn't abort multi-version local builds. --- .github/workflows/release.yml | 2 +- build.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c9ac163..be5efcc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,7 +67,7 @@ jobs: - uses: actions/upload-artifact@v4 with: name: ${{ matrix.version_name }}-${{ matrix.arch }} - path: ./builds/${{ matrix.version_name }} + path: ./builds retention-days: 7 publish: diff --git a/build.sh b/build.sh index 0e7391e..92409dc 100755 --- a/build.sh +++ b/build.sh @@ -102,7 +102,7 @@ build_version() { cp "$SCRIPT_DIR/configs/${target_arch}/${version}.config" .config echo "Checking out repo for kernel at version: $version" - git checkout "$(get_tag "$version")" + git checkout -f "$(get_tag "$version")" apply_patches "$version" From 9274f23a0750a867efe34366b41e4793d70271c0 Mon Sep 17 00:00:00 2001 From: Tomas Valenta Date: Fri, 8 May 2026 12:58:27 -0700 Subject: [PATCH 03/10] fix: hash only top-level *.patch files apply_patches only processes patches//*.patch, so the hash inputs must match. Otherwise unrelated/nested files in patches// would silently change the version_name without affecting the build. --- build.sh | 9 ++++++--- scripts/validate.py | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/build.sh b/build.sh index 92409dc..4da93c7 100755 --- a/build.sh +++ b/build.sh @@ -35,9 +35,12 @@ compute_version_name() { [ -f "$cfg" ] && files+=("$cfg") done if [ -d "$SCRIPT_DIR/patches/$version" ]; then - while IFS= read -r -d '' f; do - files+=("$f") - done < <(find "$SCRIPT_DIR/patches/$version" -type f -print0 | sort -z) + shopt -s nullglob + local patches=("$SCRIPT_DIR/patches/$version"/*.patch) + shopt -u nullglob + while IFS= read -r p; do + [ -n "$p" ] && files+=("$p") + done < <(printf '%s\n' "${patches[@]}" | sort) fi if [ "${#files[@]}" -eq 0 ]; then echo "Error: no configs found for kernel version $version" >&2 diff --git a/scripts/validate.py b/scripts/validate.py index ff02d73..db529f8 100755 --- a/scripts/validate.py +++ b/scripts/validate.py @@ -51,7 +51,7 @@ def hash_inputs_for_version(version: str) -> str: paths.append(cfg) patches_dir = REPO_ROOT / "patches" / version if patches_dir.is_dir(): - paths.extend(sorted(p for p in patches_dir.rglob("*") if p.is_file())) + paths.extend(sorted(p for p in patches_dir.glob("*.patch") if p.is_file())) if not paths: raise SystemExit(f"::error::No configs found for kernel version {version}") From 1e82d8dff0104c7d73c2581eb3c42965dd6edb7f Mon Sep 17 00:00:00 2001 From: Tomas Valenta Date: Fri, 8 May 2026 16:39:25 -0700 Subject: [PATCH 04/10] fix: align bash kernel_versions.txt parser with python Strip inline comments and trim whitespace so build.sh and scripts/validate.py produce identical version names for entries like `6.1.158 # stable`. --- build.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/build.sh b/build.sh index 4da93c7..ccf3245 100755 --- a/build.sh +++ b/build.sh @@ -161,12 +161,15 @@ main() { local version_name="${VERSION_NAME:-$(compute_version_name "$single_version")}" build_version "$single_version" "$target_arch" "$version_name" else - grep -v '^ *#' <"$SCRIPT_DIR/kernel_versions.txt" | while IFS= read -r version; do + while IFS= read -r raw; do + local version="${raw%%#*}" + version="${version#"${version%%[![:space:]]*}"}" + version="${version%"${version##*[![:space:]]}"}" [ -z "$version" ] && continue local version_name version_name="$(compute_version_name "$version")" build_version "$version" "$target_arch" "$version_name" - done + done <"$SCRIPT_DIR/kernel_versions.txt" fi } From 6c08a3fe5b93374200cc97f10f6ecbc20bc5fca6 Mon Sep 17 00:00:00 2001 From: Tomas Valenta Date: Fri, 8 May 2026 16:54:41 -0700 Subject: [PATCH 05/10] refactor: simplify to "build everything, calver release per run" - workflow_dispatch with no inputs; pick the branch in the GitHub UI. - Always build every entry in kernel_versions.txt for amd64 and arm64 in parallel (one runner per arch). - One release per run, tagged YYYY.MM.DD (with .N suffix on collision), with every binary uploaded as vmlinux--.bin (plus the legacy vmlinux-.bin for amd64). - Same binaries pushed to GCS under kernels/vmlinux-/... Drops the per-build content-hash version_name machinery and the validate.py helper that fed it. --- .github/workflows/release.yml | 196 +++++++++------------------------- README.md | 29 +++-- build.sh | 55 ++-------- scripts/validate.py | 174 ------------------------------ 4 files changed, 73 insertions(+), 381 deletions(-) delete mode 100755 scripts/validate.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be5efcc..0804804 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,77 +2,39 @@ name: Manual Build & Release on: workflow_dispatch: - inputs: - kernel_versions: - description: 'Comma-separated kernel versions to build (default: kernel_versions.txt)' - required: false - type: string - default: '' - build_amd64: - description: 'Build for amd64 architecture' - required: false - type: boolean - default: true - build_arm64: - description: 'Build for arm64 architecture' - required: false - type: boolean - default: true permissions: contents: write id-token: write jobs: - validate: - runs-on: ubuntu-24.04 - outputs: - build_matrix: ${{ steps.validate.outputs.build_matrix }} - versions: ${{ steps.validate.outputs.versions }} - has_new_artifacts: ${{ steps.validate.outputs.has_new_artifacts }} - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - - id: validate - env: - GH_TOKEN: ${{ github.token }} - run: | - python3 scripts/validate.py \ - --kernel-versions "${{ inputs.kernel_versions }}" \ - --build-amd64 "${{ inputs.build_amd64 }}" \ - --build-arm64 "${{ inputs.build_arm64 }}" - build: - needs: validate - if: needs.validate.outputs.has_new_artifacts == 'true' + name: Build kernels (${{ matrix.arch }}) strategy: fail-fast: false - matrix: ${{ fromJson(needs.validate.outputs.build_matrix) }} + matrix: + include: + - arch: amd64 + target_arch: x86_64 + runner: ubuntu-24.04 + - arch: arm64 + target_arch: arm64 + runner: ubuntu-24.04-arm runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v4 - - name: Build kernel ${{ matrix.version_name }} (${{ matrix.arch }}) - env: - VERSION_NAME: ${{ matrix.version_name }} - run: | - target_arch="${{ matrix.arch }}" - [ "$target_arch" = "amd64" ] && target_arch="x86_64" - sudo VERSION_NAME="$VERSION_NAME" ./build.sh "${{ matrix.kernel_version }}" "$target_arch" + - name: Build kernels + run: sudo TARGET_ARCH=${{ matrix.target_arch }} ./build.sh - uses: actions/upload-artifact@v4 with: - name: ${{ matrix.version_name }}-${{ matrix.arch }} + name: kernels-${{ matrix.arch }} path: ./builds retention-days: 7 publish: - needs: [validate, build] - if: needs.validate.outputs.has_new_artifacts == 'true' + needs: build runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 @@ -84,108 +46,56 @@ jobs: path: ./builds merge-multiple: true - - name: Show build artifacts - run: find ./builds -type f | head -50 + - name: Prepare release assets + run: | + set -euo pipefail + mkdir -p release-assets + for dir in ./builds/vmlinux-*/; do + name=$(basename "$dir") + [ -f "$dir/amd64/vmlinux.bin" ] && cp "$dir/amd64/vmlinux.bin" "release-assets/${name}-amd64.bin" + [ -f "$dir/arm64/vmlinux.bin" ] && cp "$dir/arm64/vmlinux.bin" "release-assets/${name}-arm64.bin" + # Legacy non-arch-suffixed asset (= amd64) for backwards compat. + [ -f "$dir/vmlinux.bin" ] && cp "$dir/vmlinux.bin" "release-assets/${name}.bin" + done + ls -la release-assets/ - - name: Create or update releases + - name: Pick calver tag + id: tag env: GH_TOKEN: ${{ github.token }} - VERSIONS: ${{ needs.validate.outputs.versions }} run: | set -euo pipefail - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - echo "$VERSIONS" | jq -c '.[]' | while read -r entry; do - version_name=$(echo "$entry" | jq -r '.version_name') - kernel_version=$(echo "$entry" | jq -r '.kernel_version') - - if ! gh release view "$version_name" >/dev/null 2>&1; then - if ! git rev-parse "refs/tags/$version_name" >/dev/null 2>&1; then - git tag "$version_name" - git push origin "$version_name" - fi - gh release create "$version_name" \ - --title "Kernel $version_name" \ - --notes "Linux kernel $kernel_version built from configs at ${{ github.sha }}" - fi - - existing=$(gh release view "$version_name" --json assets -q '.assets[].name' || true) - - for arch in amd64 arm64; do - local_path="./builds/$version_name/$arch/vmlinux.bin" - [ -f "$local_path" ] || continue - - asset="vmlinux-${arch}.bin" - if echo "$existing" | grep -qx "$asset"; then - echo "Release $version_name: $asset exists, skipping" - else - tmp=$(mktemp -d)/$asset - cp "$local_path" "$tmp" - gh release upload "$version_name" "$tmp" - rm -f "$tmp" - fi - - # Legacy non-arch-suffixed asset (amd64 only) for backwards compat. - if [ "$arch" = "amd64" ] && ! echo "$existing" | grep -qx "vmlinux.bin"; then - tmp=$(mktemp -d)/vmlinux.bin - cp "$local_path" "$tmp" - gh release upload "$version_name" "$tmp" - rm -f "$tmp" - fi - done + base="$(date -u +%Y.%m.%d)" + tag="$base" + n=1 + while gh release view "$tag" >/dev/null 2>&1; do + n=$((n + 1)) + tag="${base}.${n}" done + echo "tag=$tag" >> "$GITHUB_OUTPUT" - deploy: - needs: [validate, publish] - if: needs.validate.outputs.has_new_artifacts == 'true' - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - - - uses: google-github-actions/auth@v2 - with: - workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} - service_account: ${{ secrets.GCP_SERVICE_ACCOUNT_EMAIL }} - - - uses: google-github-actions/setup-gcloud@v2 - - - name: Download release assets and upload to GCS + - name: Create release env: GH_TOKEN: ${{ github.token }} - GCP_BUCKET_NAME: ${{ vars.GCP_BUCKET_NAME }} - VERSIONS: ${{ needs.validate.outputs.versions }} run: | set -euo pipefail - echo "$VERSIONS" | jq -c '.[]' | while read -r entry; do - version_name=$(echo "$entry" | jq -r '.version_name') - - for arch in amd64 arm64; do - asset="vmlinux-${arch}.bin" - dl_dir="./dl/$version_name/$arch" - mkdir -p "$dl_dir" - if ! gh release download "$version_name" \ - --repo "${{ github.repository }}" \ - --pattern "$asset" \ - --output "$dl_dir/vmlinux.bin" 2>/dev/null; then - continue - fi + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag "${{ steps.tag.outputs.tag }}" + git push origin "${{ steps.tag.outputs.tag }}" + gh release create "${{ steps.tag.outputs.tag }}" \ + --title "Kernels ${{ steps.tag.outputs.tag }}" \ + --notes "Built from commit ${{ github.sha }} on ${{ github.ref_name }}" \ + ./release-assets/* - gcs_path="gs://${GCP_BUCKET_NAME}/kernels/${version_name}/${arch}/vmlinux.bin" - if gcloud storage ls "$gcs_path" >/dev/null 2>&1; then - echo "GCS: $gcs_path exists, skipping" - else - gcloud storage cp "$dl_dir/vmlinux.bin" "$gcs_path" - fi + - uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.GCP_SERVICE_ACCOUNT_EMAIL }} - # Legacy non-arch path (amd64 only) for backwards compat. - if [ "$arch" = "amd64" ]; then - legacy_path="gs://${GCP_BUCKET_NAME}/kernels/${version_name}/vmlinux.bin" - if gcloud storage ls "$legacy_path" >/dev/null 2>&1; then - echo "GCS: $legacy_path exists, skipping" - else - gcloud storage cp "$dl_dir/vmlinux.bin" "$legacy_path" - fi - fi - done - done + - uses: google-github-actions/upload-cloud-storage@v2 + with: + path: ./builds + destination: ${{ vars.GCP_BUCKET_NAME }}/kernels + gzip: false + parent: false diff --git a/README.md b/README.md index 04e6bee..78d1820 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,6 @@ This project builds custom Linux kernels for Firecracker microVMs from the same kernel sources as the official Firecracker repo, using the configuration files (and optional patches) that live in this repo. -Each kernel build is identified by a content hash of its inputs (configs + patches), so changing a flag or adding a patch produces a new, traceable artifact: - -``` -vmlinux-_ -``` - ## Prerequisites - Linux environment (for building kernels) @@ -29,26 +23,27 @@ vmlinux-_ ./build.sh 6.1.158 arm64 ``` - Output: `builds/vmlinux-_//vmlinux.bin` where `` is `amd64` or `arm64` (Go/OCI convention). For x86_64 a legacy copy is also placed at `builds/vmlinux-_/vmlinux.bin`. + Output: `builds/vmlinux-//vmlinux.bin` where `` is `amd64` or `arm64` (Go/OCI convention). For x86_64 a legacy copy is also placed at `builds/vmlinux-/vmlinux.bin`. ## Releasing -1. Run the **Manual Build & Release** workflow (Actions → Manual Build & Release → Run workflow). -2. The workflow: - - Computes a content hash for each kernel version from its configs and patches. - - Skips arches whose artifact is already present in the matching GitHub release. - - Builds the missing arches, creates/updates the `vmlinux-_` release, uploads `vmlinux-amd64.bin` / `vmlinux-arm64.bin` (and a legacy `vmlinux.bin` for amd64), and pushes the same files to GCS under `gs://$GCP_BUCKET_NAME/kernels//`. +1. Pick the branch and run the **Manual Build & Release** workflow (Actions → Manual Build & Release → Run workflow → branch). The workflow takes no inputs. +2. Every kernel version in `kernel_versions.txt` is built for both `amd64` and `arm64` in parallel. +3. A single GitHub release is created per run, tagged with calver `YYYY.MM.DD` (with a `.N` suffix for additional runs the same day). The release contains every binary for that commit: -### Workflow inputs + ``` + vmlinux--amd64.bin + vmlinux--arm64.bin + vmlinux-.bin # legacy (= amd64) for backwards compat + ``` -- `kernel_versions` (optional): comma-separated kernel versions. Defaults to all versions in `kernel_versions.txt`. -- `build_amd64` / `build_arm64` (optional, default `true`): which architectures to build. +4. The same binaries are uploaded to GCS at `gs://$GCP_BUCKET_NAME/kernels/vmlinux-//vmlinux.bin`. ## New kernel in E2B's infra _Note: these steps should give you a new kernel on your self-hosted E2B using https://github.com/e2b-dev/infra_ -- Run the release workflow to publish the new kernel build. -- Update `DefaultKernelVersion` in [packages/api/internal/cfg/model.go](https://github.com/e2b-dev/infra/blob/main/packages/api/internal/cfg/model.go) to the new `vmlinux-_` name. +- Run the release workflow on the branch with the new config/patch. +- Update `DefaultKernelVersion` in [packages/api/internal/cfg/model.go](https://github.com/e2b-dev/infra/blob/main/packages/api/internal/cfg/model.go) if you changed the kernel version. - Build and deploy `api`. ## Architecture naming diff --git a/build.sh b/build.sh index ccf3245..d0ed279 100755 --- a/build.sh +++ b/build.sh @@ -4,15 +4,12 @@ set -euo pipefail # Usage: -# ./build.sh # build all versions in kernel_versions.txt for $TARGET_ARCH -# ./build.sh [arch] # build a single version -# VERSION_NAME=vmlinux-_ ./build.sh +# ./build.sh # build all versions in kernel_versions.txt for $TARGET_ARCH +# ./build.sh [arch] # build a single version # # arch is one of: x86_64 (default), arm64 (kernel-style names). -# The output goes to builds///vmlinux.bin where +# Output: builds/vmlinux-//vmlinux.bin where # is the Go/OCI name (amd64/arm64) used by the orchestrator. -# If VERSION_NAME isn't provided, it's computed deterministically from the -# kernel version's configs and patches: vmlinux-_. HOST_ARCH="$(uname -m)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -25,38 +22,6 @@ normalize_arch() { esac } -# Returns vmlinux-_. Hash inputs: configs/{x86_64,arm64}/.config -# and any files under patches//. Must match scripts/validate.py. -compute_version_name() { - local version="$1" - local files=() - for arch in x86_64 arm64; do - local cfg="$SCRIPT_DIR/configs/$arch/${version}.config" - [ -f "$cfg" ] && files+=("$cfg") - done - if [ -d "$SCRIPT_DIR/patches/$version" ]; then - shopt -s nullglob - local patches=("$SCRIPT_DIR/patches/$version"/*.patch) - shopt -u nullglob - while IFS= read -r p; do - [ -n "$p" ] && files+=("$p") - done < <(printf '%s\n' "${patches[@]}" | sort) - fi - if [ "${#files[@]}" -eq 0 ]; then - echo "Error: no configs found for kernel version $version" >&2 - exit 1 - fi - local h - h=$( - for f in "${files[@]}"; do - printf '%s\0' "${f#"$SCRIPT_DIR/"}" - cat "$f" - printf '\0' - done | sha256sum | awk '{print $1}' | cut -c1-7 - ) - echo "vmlinux-${version}_${h}" -} - install_dependencies() { local target_arch="$1" local packages=( @@ -96,11 +61,10 @@ apply_patches() { build_version() { local version="$1" local target_arch="$2" - local version_name="$3" local output_arch output_arch="$(normalize_arch "$target_arch")" - echo "Starting build for kernel version: $version (${target_arch}) -> $version_name" + echo "Starting build for kernel version: $version (${target_arch})" cp "$SCRIPT_DIR/configs/${target_arch}/${version}.config" .config @@ -128,7 +92,7 @@ build_version() { fi echo "Copying finished build to builds directory" - local out_dir="$SCRIPT_DIR/builds/${version_name}/${output_arch}" + local out_dir="$SCRIPT_DIR/builds/vmlinux-${version}/${output_arch}" mkdir -p "$out_dir" if [[ "$target_arch" == "arm64" ]]; then cp arch/arm64/boot/Image "$out_dir/vmlinux.bin" @@ -138,7 +102,7 @@ build_version() { # x86_64: also copy to legacy path (no arch subdir) for backwards compat. if [[ "$target_arch" == "x86_64" ]]; then - cp vmlinux "$SCRIPT_DIR/builds/${version_name}/vmlinux.bin" + cp vmlinux "$SCRIPT_DIR/builds/vmlinux-${version}/vmlinux.bin" fi } @@ -158,17 +122,14 @@ main() { ensure_linux_repo if [[ -n "$single_version" ]]; then - local version_name="${VERSION_NAME:-$(compute_version_name "$single_version")}" - build_version "$single_version" "$target_arch" "$version_name" + build_version "$single_version" "$target_arch" else while IFS= read -r raw; do local version="${raw%%#*}" version="${version#"${version%%[![:space:]]*}"}" version="${version%"${version##*[![:space:]]}"}" [ -z "$version" ] && continue - local version_name - version_name="$(compute_version_name "$version")" - build_version "$version" "$target_arch" "$version_name" + build_version "$version" "$target_arch" done <"$SCRIPT_DIR/kernel_versions.txt" fi } diff --git a/scripts/validate.py b/scripts/validate.py deleted file mode 100755 index db529f8..0000000 --- a/scripts/validate.py +++ /dev/null @@ -1,174 +0,0 @@ -#!/usr/bin/env python3 -"""Validate inputs and resolve kernel build matrix. - -Each kernel version is identified by a content hash of its configs and -patches. The version_name is `vmlinux-_`. We skip -arch+version pairs whose artifact already exists in the GitHub release. -""" - -from __future__ import annotations - -import argparse -import hashlib -import json -import os -import subprocess -import sys -from pathlib import Path -from typing import Iterable - -REPO_ROOT = Path(__file__).resolve().parent.parent -HASH_LEN = 7 - -ARCH_TO_RUNNER = { - "amd64": "ubuntu-24.04", - "arm64": "ubuntu-24.04-arm", -} - - -def read_default_versions() -> list[str]: - versions_file = REPO_ROOT / "kernel_versions.txt" - versions: list[str] = [] - for raw in versions_file.read_text().splitlines(): - line = raw.split("#", 1)[0].strip() - if line: - versions.append(line) - return versions - - -def parse_versions(arg: str) -> list[str]: - if not arg.strip(): - return read_default_versions() - return [v.strip() for v in arg.split(",") if v.strip()] - - -def hash_inputs_for_version(version: str) -> str: - """Hash the configs and any patches that determine this kernel build.""" - paths: list[Path] = [] - for arch in ("x86_64", "arm64"): - cfg = REPO_ROOT / "configs" / arch / f"{version}.config" - if cfg.is_file(): - paths.append(cfg) - patches_dir = REPO_ROOT / "patches" / version - if patches_dir.is_dir(): - paths.extend(sorted(p for p in patches_dir.glob("*.patch") if p.is_file())) - - if not paths: - raise SystemExit(f"::error::No configs found for kernel version {version}") - - digest = hashlib.sha256() - for path in paths: - rel = path.relative_to(REPO_ROOT).as_posix() - digest.update(rel.encode()) - digest.update(b"\0") - digest.update(path.read_bytes()) - digest.update(b"\0") - return digest.hexdigest()[:HASH_LEN] - - -def gh_release_assets(version_name: str) -> set[str]: - """Return existing asset names for the given release, empty if none.""" - result = subprocess.run( - ["gh", "release", "view", version_name, "--json", "assets", "-q", ".assets[].name"], - capture_output=True, - text=True, - check=False, - ) - if result.returncode != 0: - return set() - return {line for line in result.stdout.strip().split("\n") if line} - - -def asset_name(arch: str) -> str: - return f"vmlinux-{arch}.bin" - - -def build_entries( - versions: list[str], - archs: list[str], -) -> tuple[list[dict], list[dict]]: - """Return (build_matrix_entries, all_version_entries).""" - build_entries: list[dict] = [] - version_entries: list[dict] = [] - for version in versions: - version_hash = hash_inputs_for_version(version) - version_name = f"vmlinux-{version}_{version_hash}" - existing = gh_release_assets(version_name) - version_entries.append({ - "kernel_version": version, - "version_hash": version_hash, - "version_name": version_name, - }) - for arch in archs: - if asset_name(arch) in existing: - print( - f"{version_name}/{arch}: artifact exists, skipping build", - file=sys.stderr, - ) - continue - build_entries.append({ - "kernel_version": version, - "arch": arch, - "version_hash": version_hash, - "version_name": version_name, - "runner": ARCH_TO_RUNNER[arch], - }) - return build_entries, version_entries - - -def write_github_output(outputs: dict[str, str]) -> None: - out_path = os.environ.get("GITHUB_OUTPUT") - if not out_path: - for k, v in outputs.items(): - print(f"{k}={v}") - return - with open(out_path, "a") as f: - for k, v in outputs.items(): - f.write(f"{k}={v}\n") - - -def collect_archs(amd64: bool, arm64: bool) -> list[str]: - archs: list[str] = [] - if amd64: - archs.append("amd64") - if arm64: - archs.append("arm64") - return archs - - -def main(argv: Iterable[str] | None = None) -> int: - parser = argparse.ArgumentParser(description="Validate kernel release inputs") - parser.add_argument("--kernel-versions", default="", - help="Comma-separated kernel versions (default: kernel_versions.txt)") - parser.add_argument("--build-amd64", type=lambda x: x.lower() == "true", default=True) - parser.add_argument("--build-arm64", type=lambda x: x.lower() == "true", default=True) - args = parser.parse_args(list(argv) if argv is not None else None) - - archs = collect_archs(args.build_amd64, args.build_arm64) - if not archs: - print("::error::At least one architecture must be selected", file=sys.stderr) - return 1 - - versions = parse_versions(args.kernel_versions) - if not versions: - print("::error::No kernel versions to build", file=sys.stderr) - return 1 - - build_list, version_list = build_entries(versions, archs) - - matrix = {"include": build_list} if build_list else {"include": [{"skip": "true"}]} - - print(f"Versions: {[v['version_name'] for v in version_list]}", file=sys.stderr) - print(f"Build matrix: {json.dumps(matrix)}", file=sys.stderr) - print(f"Has new artifacts: {bool(build_list)}", file=sys.stderr) - - write_github_output({ - "build_matrix": json.dumps(matrix), - "versions": json.dumps(version_list), - "has_new_artifacts": "true" if build_list else "false", - }) - return 0 - - -if __name__ == "__main__": - sys.exit(main()) From 583d29862cbac3be6be70ca85e33399b601b7bec Mon Sep 17 00:00:00 2001 From: Tomas Valenta Date: Fri, 8 May 2026 17:06:29 -0700 Subject: [PATCH 06/10] fix: calver suffix sequence base, .1, .2 (was skipping .1) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0804804..60c7c29 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,7 +67,7 @@ jobs: set -euo pipefail base="$(date -u +%Y.%m.%d)" tag="$base" - n=1 + n=0 while gh release view "$tag" >/dev/null 2>&1; do n=$((n + 1)) tag="${base}.${n}" From 8eab6747ef9975804b4ac8fa27fc05b8d739da14 Mon Sep 17 00:00:00 2001 From: Tomas Valenta Date: Fri, 8 May 2026 17:19:47 -0700 Subject: [PATCH 07/10] feat: also build on pull_request, gate publish on workflow_dispatch Open PRs now run the build matrix and upload kernel binaries as workflow artifacts so reviewers can grab and inspect them from the PR's checks tab. Release/GCS publishing remains manual-only. --- .github/workflows/release.yml | 4 +++- README.md | 11 +++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 60c7c29..a56c427 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,8 @@ -name: Manual Build & Release +name: Build & Release on: workflow_dispatch: + pull_request: permissions: contents: write @@ -35,6 +36,7 @@ jobs: publish: needs: build + if: github.event_name == 'workflow_dispatch' runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index 78d1820..f7f8ba6 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,14 @@ This project builds custom Linux kernels for Firecracker microVMs from the same Output: `builds/vmlinux-//vmlinux.bin` where `` is `amd64` or `arm64` (Go/OCI convention). For x86_64 a legacy copy is also placed at `builds/vmlinux-/vmlinux.bin`. -## Releasing +## CI / Releasing -1. Pick the branch and run the **Manual Build & Release** workflow (Actions → Manual Build & Release → Run workflow → branch). The workflow takes no inputs. -2. Every kernel version in `kernel_versions.txt` is built for both `amd64` and `arm64` in parallel. -3. A single GitHub release is created per run, tagged with calver `YYYY.MM.DD` (with a `.N` suffix for additional runs the same day). The release contains every binary for that commit: +The **Build & Release** workflow runs in two modes: + +- **On every pull request**: builds every kernel in `kernel_versions.txt` for `amd64` and `arm64` in parallel and uploads the binaries as workflow artifacts (downloadable from the PR's checks tab) so reviewers can inspect them. No release or GCS upload happens. +- **Manually (workflow_dispatch)**: pick the branch in the GitHub UI and run. It does the same build as a PR and additionally creates a GitHub release tagged `YYYY.MM.DD` (with a `.N` suffix for additional runs the same day) containing every binary, and uploads them to GCS. + +Release asset naming for that commit: ``` vmlinux--amd64.bin From c167397e7a8039be25de95d538e3ad1cd563d56e Mon Sep 17 00:00:00 2001 From: Tomas Valenta Date: Fri, 8 May 2026 17:43:21 -0700 Subject: [PATCH 08/10] feat: parallelize build per (kernel_version, arch) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generate the build matrix dynamically from kernel_versions.txt so each (version, arch) pair runs on its own runner. With N versions × 2 archs that's 2N parallel jobs (e.g. 2 versions -> 4 runners). build.sh already supports single-version mode, so no script changes; the matrix job emits a JSON include list that the build job fans out over. --- .github/workflows/release.yml | 40 ++++++++++++++++++++++++----------- README.md | 2 +- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a56c427..2330d08 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,28 +9,44 @@ permissions: id-token: write jobs: + matrix: + runs-on: ubuntu-24.04 + outputs: + build_matrix: ${{ steps.gen.outputs.matrix }} + steps: + - uses: actions/checkout@v4 + - id: gen + run: | + python3 - >> "$GITHUB_OUTPUT" <<'PY' + import json + versions = [] + for line in open("kernel_versions.txt").read().splitlines(): + v = line.split("#", 1)[0].strip() + if v: + versions.append(v) + include = [] + for v in versions: + include.append({"version": v, "arch": "amd64", "target_arch": "x86_64", "runner": "ubuntu-24.04"}) + include.append({"version": v, "arch": "arm64", "target_arch": "arm64", "runner": "ubuntu-24.04-arm"}) + print(f"matrix={json.dumps({'include': include})}") + PY + build: - name: Build kernels (${{ matrix.arch }}) + needs: matrix + name: Build ${{ matrix.version }} (${{ matrix.arch }}) strategy: fail-fast: false - matrix: - include: - - arch: amd64 - target_arch: x86_64 - runner: ubuntu-24.04 - - arch: arm64 - target_arch: arm64 - runner: ubuntu-24.04-arm + matrix: ${{ fromJson(needs.matrix.outputs.build_matrix) }} runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v4 - - name: Build kernels - run: sudo TARGET_ARCH=${{ matrix.target_arch }} ./build.sh + - name: Build kernel + run: sudo ./build.sh "${{ matrix.version }}" "${{ matrix.target_arch }}" - uses: actions/upload-artifact@v4 with: - name: kernels-${{ matrix.arch }} + name: kernel-${{ matrix.version }}-${{ matrix.arch }} path: ./builds retention-days: 7 diff --git a/README.md b/README.md index f7f8ba6..829be4d 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ This project builds custom Linux kernels for Firecracker microVMs from the same The **Build & Release** workflow runs in two modes: -- **On every pull request**: builds every kernel in `kernel_versions.txt` for `amd64` and `arm64` in parallel and uploads the binaries as workflow artifacts (downloadable from the PR's checks tab) so reviewers can inspect them. No release or GCS upload happens. +- **On every pull request**: builds every (kernel version × arch) combination from `kernel_versions.txt` in parallel (one runner per pair) and uploads the binaries as workflow artifacts (downloadable from the PR's checks tab) so reviewers can inspect them. No release or GCS upload happens. - **Manually (workflow_dispatch)**: pick the branch in the GitHub UI and run. It does the same build as a PR and additionally creates a GitHub release tagged `YYYY.MM.DD` (with a `.N` suffix for additional runs the same day) containing every binary, and uploads them to GCS. Release asset naming for that commit: From ef204f42d2224daeabd6793cde91c6222b09f0a2 Mon Sep 17 00:00:00 2001 From: Tomas Valenta Date: Fri, 8 May 2026 17:56:59 -0700 Subject: [PATCH 09/10] fix: scope id-token to publish, harden version handling, idempotent calver - Drop workflow-level id-token: write; only the publish job (which needs OIDC for GCP auth) gets id-token: write. PR builds no longer receive a token they can exchange for cloud creds. - Validate kernel versions against [0-9]+(\.[0-9]+)+ in the matrix job and pass matrix.version to build.sh via env vars instead of inline YAML expression interpolation, so a malicious kernel_versions.txt entry can't shell-inject into the runner. - Calver tag picker now also checks local and remote git tags, not just GH releases, so retries after a partial publish no longer pick a tag that's already been pushed. --- .github/workflows/release.yml | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2330d08..4b7abaa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,12 +5,13 @@ on: pull_request: permissions: - contents: write - id-token: write + contents: read jobs: matrix: runs-on: ubuntu-24.04 + permissions: + contents: read outputs: build_matrix: ${{ steps.gen.outputs.matrix }} steps: @@ -19,11 +20,17 @@ jobs: run: | python3 - >> "$GITHUB_OUTPUT" <<'PY' import json + import re + import sys + VERSION_RE = re.compile(r"^[0-9]+(?:\.[0-9]+)+$") versions = [] for line in open("kernel_versions.txt").read().splitlines(): v = line.split("#", 1)[0].strip() - if v: - versions.append(v) + if not v: + continue + if not VERSION_RE.match(v): + sys.exit(f"::error::invalid kernel version in kernel_versions.txt: {v!r}") + versions.append(v) include = [] for v in versions: include.append({"version": v, "arch": "amd64", "target_arch": "x86_64", "runner": "ubuntu-24.04"}) @@ -38,11 +45,16 @@ jobs: fail-fast: false matrix: ${{ fromJson(needs.matrix.outputs.build_matrix) }} runs-on: ${{ matrix.runner }} + permissions: + contents: read steps: - uses: actions/checkout@v4 - name: Build kernel - run: sudo ./build.sh "${{ matrix.version }}" "${{ matrix.target_arch }}" + env: + KERNEL_VERSION: ${{ matrix.version }} + KERNEL_TARGET_ARCH: ${{ matrix.target_arch }} + run: sudo ./build.sh "$KERNEL_VERSION" "$KERNEL_TARGET_ARCH" - uses: actions/upload-artifact@v4 with: @@ -54,6 +66,9 @@ jobs: needs: build if: github.event_name == 'workflow_dispatch' runs-on: ubuntu-24.04 + permissions: + contents: write + id-token: write steps: - uses: actions/checkout@v4 with: @@ -86,7 +101,9 @@ jobs: base="$(date -u +%Y.%m.%d)" tag="$base" n=0 - while gh release view "$tag" >/dev/null 2>&1; do + while gh release view "$tag" >/dev/null 2>&1 \ + || git rev-parse "refs/tags/$tag" >/dev/null 2>&1 \ + || git ls-remote --exit-code --tags origin "refs/tags/$tag" >/dev/null 2>&1; do n=$((n + 1)) tag="${base}.${n}" done From fd2430a03e607d9d15178d4195515c619abb4d1d Mon Sep 17 00:00:00 2001 From: Tomas Valenta Date: Fri, 8 May 2026 20:32:16 -0700 Subject: [PATCH 10/10] fix: emit Build kernels (x86_64|arm64) checks for branch protection The 'Main' ruleset on this repo requires status checks named exactly 'Build kernels (x86_64)' and 'Build kernels (arm64)' (left over from the previous workflow). The new per-(version, arch) jobs use different names, so those required checks never reported and PRs couldn't merge. Add a tiny aggregator job whose name interpolates to those exact two strings and that gates on the build matrix's combined result, so parallelism stays per-(version, arch) but branch protection is satisfied. --- .github/workflows/release.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4b7abaa..a14f80a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,6 +62,26 @@ jobs: path: ./builds retention-days: 7 + # Aggregator with the exact name required by the `Main` ruleset on this + # repo. Per-(version, arch) jobs above run in parallel; this single check + # gates the whole arch on their combined success. + build-status: + needs: build + if: always() + name: Build kernels (${{ matrix.target_arch }}) + strategy: + matrix: + target_arch: [x86_64, arm64] + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - run: | + if [[ "${{ needs.build.result }}" != "success" ]]; then + echo "build matrix did not succeed (result=${{ needs.build.result }})" + exit 1 + fi + publish: needs: build if: github.event_name == 'workflow_dispatch'