- 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
344 lines
13 KiB
YAML
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
|