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 # 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