veilor-os/.github/workflows/build-iso.yml
veilor-org 2782b72ead sre: release pipeline w/ ISO split, GPG sig, MOK signing scaffold
- build-iso.yml: on tag push (v*.*.*), split ISO into 1.9G parts, GPG-sign
  the sha256 with GPG_PRIVATE_KEY secret, and auto-create release with
  softprops/action-gh-release@v2 attaching part files + sig + reassembly
  instructions. Falls back to legacy release.published path.
- build-iso.yml: optional EFI Secure Boot signing step. If MOK_PRIVATE_KEY
  + MOK_CERT secrets are present, sbsign each .efi inside the ISO and
  repack with xorriso; otherwise warn and ship unsigned. Refresh sha256.
- release-checksums.yml: new PR-time gate. Validates source + generated
  CI kickstart, shellchecks scripts, parses every workflow YAML, and
  asserts the split size stays under GitHub'''s 2 GiB asset cap.
- scripts/gen-mok-key.sh: idempotent MOK keypair generator (RSA-4096,
  10y), outputs to gitignored build/keys/. Header documents mokutil
  enrollment and gh secret upload. exec bit set in index.
- .gitignore: add build/keys/, *.priv, *.der.

User must add GitHub secrets before the next tagged release:
  GPG_PRIVATE_KEY  — armored private key for sha256 signing
  MOK_PRIVATE_KEY  — sbsign EFI signing key (PEM)
  MOK_CERT         — public cert (DER) for sbsign + mokutil enrollment
2026-05-01 23:39:19 +01:00

344 lines
13 KiB
YAML

name: Build veilor-os ISO
on:
push:
branches: [main]
paths:
- 'kickstart/**'
- 'overlay/**'
- 'scripts/**'
- 'assets/**'
- 'build/**'
- '.github/workflows/build-iso.yml'
tags:
- 'v*.*.*'
workflow_dispatch:
inputs:
releasever:
description: 'Fedora release version'
required: false
default: '43'
release:
types: [published]
jobs:
build:
name: Build live ISO
runs-on: ubuntu-24.04
timeout-minutes: 90
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Free up disk
run: |
sudo rm -rf /opt/hostedtoolcache /usr/share/dotnet /usr/local/lib/android /usr/local/share/boost
sudo apt-get clean
df -h
- name: Run build inside Fedora 43 container
uses: addnab/docker-run-action@v3
with:
image: registry.fedoraproject.org/fedora:43
options: |
--privileged
-v ${{ github.workspace }}:/work
-v /dev:/dev
--tmpfs /tmp:rw,nosuid,nodev,exec,size=16G
run: |
set -euxo pipefail
# Update Fedora image to latest packages — guarantees pcre2 +
# libselinux + selinux-policy are matched (the local build's
# core problem). CI runners always start fresh, no version skew.
dnf -y upgrade --refresh
# Install build tooling. sbsigntools provides sbsign for EFI
# binary signing; pesign is the alternate path. xorriso is
# required for ISO repack after EFI signing.
dnf -y install \
lorax \
livecd-tools \
pykickstart \
python3-imgcreate \
anaconda-tui \
squashfs-tools \
xorriso \
createrepo_c \
git \
which \
shadow-utils \
sbsigntools \
openssl \
dosfstools \
mtools
cd /work
# PATCH: livecd-creator bug — __get_efi_image_stanza writes
# `root=live:LABEL=...` instead of `live:CDLABEL=...` for dracut.
# Result: dracut hangs on parse-livenet looking for non-CD label.
# Fix in-place before running build.
LIVE_PY=$(python3 -c 'import imgcreate, os; print(os.path.dirname(imgcreate.__file__))')/live.py
sed -i 's|"live:LABEL=%(fslabel)s"|"live:CDLABEL=%(fslabel)s"|g' "$LIVE_PY"
grep -n 'CDLABEL=%(fslabel)s' "$LIVE_PY" || { echo "[ERR] patch failed"; exit 1; }
echo "[OK] livecd-creator patched: LABEL= → CDLABEL= for EFI dracut stanza"
# CI uses ks-ci.ks (no local fix-repo line). Generated from main ks.
# Also strip flags livecd-creator doesn't recognize.
sed -e '/veilor-fix/d' \
-e '/^shutdown$/d' \
kickstart/veilor-os.ks > kickstart/veilor-os-ci.ks
ksvalidator kickstart/veilor-os-ci.ks
mkdir -p build/out
# livecd-creator (livecd-tools) — purpose-built for live ISOs.
# Handles EFI/BOOT + isohybrid + grafting that livemedia-creator
# --make-iso --no-virt does not. Produces UEFI+BIOS bootable ISO.
# --tmpdir /var/lmc to avoid GitHub Actions /tmp tmpfs constraints.
# /var on the runner is the host's ext4 (~80GB free post-disk-cleanup).
mkdir -p /var/lmc /var/lmc-cache
livecd-creator \
--verbose \
--config kickstart/veilor-os-ci.ks \
--fslabel "veilor-os-43" \
--title "veilor-os" \
--product "veilor-os" \
--releasever "${{ github.event.inputs.releasever || '43' }}" \
--tmpdir /var/lmc \
--cache /var/lmc-cache 2>&1 | tee build/out/build.log
# Move output ISO to expected dir
mv ./veilor-os-43.iso build/out/ 2>/dev/null || mv ./*.iso build/out/ 2>/dev/null || true
# Rename. For tag builds use the tag as the version stamp so
# release artifacts have a stable, predictable name. For
# branch / dispatch builds use a timestamp.
REF_NAME="${{ github.ref_name }}"
if [[ "${{ github.ref_type }}" == "tag" ]]; then
VERSION_TAG="$REF_NAME"
else
VERSION_TAG="$(date +%Y%m%d-%H%M%S)"
fi
ISO_NAME="veilor-os-${{ github.event.inputs.releasever || '43' }}-${VERSION_TAG}.iso"
cd build/out
for f in *.iso; do
[[ -f $f && $f != $ISO_NAME ]] && mv "$f" "$ISO_NAME"
done
sha256sum "$ISO_NAME" > "$ISO_NAME.sha256"
ls -lh "$ISO_NAME"
# Stash final ISO name for later steps that run *outside* the
# container (signing, splitting, release). The runner sees
# /work/build/out, host sees ${{ github.workspace }}/build/out.
echo "$ISO_NAME" > /work/build/out/.iso-name
- name: Resolve ISO filename
id: iso
run: |
ISO_NAME=$(cat build/out/.iso-name)
echo "name=$ISO_NAME" >> "$GITHUB_OUTPUT"
echo "path=build/out/$ISO_NAME" >> "$GITHUB_OUTPUT"
echo "[INFO] ISO resolved: $ISO_NAME"
# ─── EFI signing (Secure Boot) ──────────────────────────────────────
# Only runs if MOK_PRIVATE_KEY + MOK_CERT secrets are present.
# Repacks the ISO in-place with sbsign'd BOOTX64.EFI / grubx64.efi.
# Without the secrets, ISO is shipped unsigned (testing builds).
- name: Sign EFI binaries (if MOK secrets present)
id: sbsign
env:
MOK_PRIVATE_KEY: ${{ secrets.MOK_PRIVATE_KEY }}
MOK_CERT: ${{ secrets.MOK_CERT }}
run: |
set -euo pipefail
if [[ -z "${MOK_PRIVATE_KEY:-}" || -z "${MOK_CERT:-}" ]]; then
echo "::warning::[INFO] MOK secrets absent — ISO unsigned for testing"
echo "signed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
sudo apt-get update
sudo apt-get install -y sbsigntool xorriso mtools dosfstools
# Materialise key + cert from secrets to a tmp dir wiped on exit.
KEYDIR=$(mktemp -d)
trap 'rm -rf "$KEYDIR"' EXIT
printf '%s' "$MOK_PRIVATE_KEY" > "$KEYDIR/MOK.priv"
printf '%s' "$MOK_CERT" | base64 -d > "$KEYDIR/MOK.der" 2>/dev/null \
|| printf '%s' "$MOK_CERT" > "$KEYDIR/MOK.der"
# sbsign prefers PEM cert input — derive on the fly.
openssl x509 -inform DER -in "$KEYDIR/MOK.der" -outform PEM -out "$KEYDIR/MOK.pem" \
|| cp "$KEYDIR/MOK.der" "$KEYDIR/MOK.pem"
chmod 600 "$KEYDIR/MOK.priv"
ISO_PATH="${{ steps.iso.outputs.path }}"
WORKDIR=$(mktemp -d)
trap 'rm -rf "$KEYDIR" "$WORKDIR"' EXIT
# Extract EFI ESP image from ISO. xorriso prints embedded image
# locations; we pull the EFI/BOOT directory contents into a
# writable scratch dir, sign each .efi, and graft back in.
mkdir -p "$WORKDIR/iso-extract"
xorriso -osirrox on -indev "$ISO_PATH" \
-extract /EFI "$WORKDIR/iso-extract/EFI" 2>&1 | tail -20
SIGNED_ANY=0
for efi in $(find "$WORKDIR/iso-extract/EFI" -type f -iname '*.efi' 2>/dev/null); do
echo "[*] Signing $efi"
sbsign \
--key "$KEYDIR/MOK.priv" \
--cert "$KEYDIR/MOK.pem" \
--output "${efi}.signed" \
"$efi"
mv "${efi}.signed" "$efi"
SIGNED_ANY=1
done
if [[ $SIGNED_ANY -eq 0 ]]; then
echo "::warning::No EFI binaries found in ISO — skipping repack"
echo "signed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# Repack: graft the signed EFI tree back into the ISO. Use
# xorriso's update mode so other ISO contents (squashfs, isolinux)
# remain untouched.
xorriso \
-indev "$ISO_PATH" \
-outdev "${ISO_PATH}.signed" \
-boot_image any keep \
-map "$WORKDIR/iso-extract/EFI" /EFI \
-commit
mv "${ISO_PATH}.signed" "$ISO_PATH"
# Refresh sha256 — content changed.
(cd build/out && sha256sum "${{ steps.iso.outputs.name }}" > "${{ steps.iso.outputs.name }}.sha256")
echo "[OK] EFI binaries signed; ISO repacked + checksum refreshed."
echo "signed=true" >> "$GITHUB_OUTPUT"
- name: Upload ISO artifact
if: success()
uses: actions/upload-artifact@v4
with:
name: veilor-os-iso
path: |
build/out/*.iso
build/out/*.sha256
retention-days: 14
- name: Upload build log on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: veilor-os-buildlog
path: |
build/out/build.log
build/out/build/anaconda/
# ─── Release artefact pipeline ──────────────────────────────────────
# Triggered on tag push (v*.*.*). The 2.86 GB ISO exceeds GitHub's
# 2 GB single-asset cap, so we split into 1.9 GB parts and let users
# reassemble with `cat`. Checksum is GPG-signed so users can verify
# the assembled ISO end-to-end.
- name: Split ISO for release
if: github.event_name == 'push' && github.ref_type == 'tag'
id: split
run: |
set -euo pipefail
cd build/out
ISO="${{ steps.iso.outputs.name }}"
# 1900M keeps each part safely below GitHub's 2 GB asset limit
# (limit is 2 GiB = 2147483648 bytes; 1900 MiB = 1992294400).
split -b 1900M -d -a 2 "$ISO" "${ISO}.part-"
# Rename suffix to letter form so it matches the documented
# convention .part-aa, .part-ab. -d uses 00/01; we want aa/ab
# to make the cat glob unambiguous (no leading zero confusion).
mv "${ISO}.part-00" "${ISO}.part-aa"
mv "${ISO}.part-01" "${ISO}.part-ab" 2>/dev/null || true
# In rare 3-part case (>3.8 GB ISO), handle .part-02 → .part-ac
[[ -f "${ISO}.part-02" ]] && mv "${ISO}.part-02" "${ISO}.part-ac" || true
ls -lh "${ISO}".part-*
echo "iso=$ISO" >> "$GITHUB_OUTPUT"
- name: GPG-sign sha256 checksum
if: github.event_name == 'push' && github.ref_type == 'tag'
env:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
run: |
set -euo pipefail
if [[ -z "${GPG_PRIVATE_KEY:-}" ]]; then
echo "::warning::GPG_PRIVATE_KEY secret missing — checksum will not be signed"
exit 0
fi
GNUPGHOME=$(mktemp -d)
export GNUPGHOME
chmod 700 "$GNUPGHOME"
# Trust the imported key automatically — CI is non-interactive.
printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import
KEYID=$(gpg --list-secret-keys --with-colons | awk -F: '/^sec:/ {print $5; exit}')
echo "[*] Signing with key $KEYID"
cd build/out
ISO="${{ steps.iso.outputs.name }}"
gpg --batch --yes --pinentry-mode loopback \
--local-user "$KEYID" \
--armor --detach-sign \
--output "${ISO}.sha256.asc" \
"${ISO}.sha256"
ls -lh "${ISO}.sha256" "${ISO}.sha256.asc"
- name: Create release and attach assets
if: github.event_name == 'push' && github.ref_type == 'tag'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
name: veilor-os ${{ github.ref_name }}
draft: false
prerelease: false
generate_release_notes: true
fail_on_unmatched_files: false
files: |
build/out/*.iso.part-*
build/out/*.iso.sha256
build/out/*.iso.sha256.asc
body: |
## veilor-os ${{ github.ref_name }}
ISO is split into 1.9 GB parts to fit within GitHub's 2 GB
per-asset upload cap. Reassemble after download:
```bash
cat ${{ steps.iso.outputs.name }}.part-* > ${{ steps.iso.outputs.name }}
sha256sum -c ${{ steps.iso.outputs.name }}.sha256
```
### Verifying the checksum signature
```bash
# Import the veilor-os release signing key (one-time):
gpg --keyserver keys.openpgp.org --recv-keys <KEY-ID-FROM-RELEASE-NOTES>
# Verify:
gpg --verify ${{ steps.iso.outputs.name }}.sha256.asc \
${{ steps.iso.outputs.name }}.sha256
```
### Secure Boot
EFI binaries are signed with the veilor-os MOK if signing
secrets were configured at build time. To enroll the public
cert post-install, see `scripts/gen-mok-key.sh` header.
# Legacy hook: keeps the manual `release.published` workflow path
# working. Tag-driven flow above is the new canonical entry point.
- name: Attach to release (legacy release event)
if: github.event_name == 'release'
uses: softprops/action-gh-release@v2
with:
files: |
build/out/*.iso
build/out/*.sha256