Sign each ISO chunk with cosign keyless OIDC, generate an SPDX SBOM of the build output, and attach an in-toto build-provenance attestation. Sigs/certs/SBOM are uploaded alongside the ISO parts in the ci-latest rolling prerelease so the test/auto-install.sh path can verify before reassembling. Action versions are major-version tags (@v3, @v0, @v2). SHA-pinning is tracked separately to keep this PR small and avoid the long web lookups that stalled the previous attempt.
280 lines
12 KiB
YAML
280 lines
12 KiB
YAML
name: Build veilor-os ISO
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
paths:
|
|
- 'kickstart/**'
|
|
- 'overlay/**'
|
|
- 'scripts/**'
|
|
- 'assets/**'
|
|
- 'build/**'
|
|
- '.github/workflows/build-iso.yml'
|
|
workflow_dispatch:
|
|
inputs:
|
|
releasever:
|
|
description: 'Fedora release version'
|
|
required: false
|
|
default: '43'
|
|
release:
|
|
types: [published]
|
|
|
|
permissions:
|
|
contents: write # needed for action-gh-release to create+update ci-latest
|
|
id-token: write # cosign keyless OIDC + attest-build-provenance
|
|
attestations: write # attest-build-provenance writes the attestation
|
|
|
|
jobs:
|
|
build:
|
|
name: Build live ISO
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 90
|
|
|
|
steps:
|
|
- name: Checkout
|
|
# Pinned to last v4 tag confirmed to ship on node20. v4.2+ ships
|
|
# node24 which forgejo-runner v6.4.0 (node20) cannot exec.
|
|
uses: actions/checkout@v4.1.7
|
|
|
|
- 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
|
|
# v3 is composite/docker-based — no node runtime in the action
|
|
# itself. Safe under node20 forgejo-runner. TODO(infra): consider
|
|
# SHA pinning in a follow-up sweep.
|
|
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
|
|
dnf -y install \
|
|
lorax \
|
|
livecd-tools \
|
|
pykickstart \
|
|
python3-imgcreate \
|
|
anaconda-tui \
|
|
squashfs-tools \
|
|
xorriso \
|
|
createrepo_c \
|
|
git \
|
|
which \
|
|
shadow-utils \
|
|
syslinux \
|
|
tar \
|
|
curl
|
|
|
|
# Vendor gum binary onto the ISO so the TTY1 installer can use
|
|
# Charm.sh TUI primitives. gum is not packaged in Fedora repos,
|
|
# so pull the upstream release tarball pinned by sha256.
|
|
GUM_VERSION="0.17.0"
|
|
GUM_URL="https://github.com/charmbracelet/gum/releases/download/v${GUM_VERSION}/gum_${GUM_VERSION}_Linux_x86_64.tar.gz"
|
|
GUM_SHA256="69ee169bd6387331928864e94d47ed01ef649fbfe875baed1bbf27b5377a6fdb"
|
|
mkdir -p /work/overlay/usr/local/bin
|
|
curl -fsSL "$GUM_URL" -o /tmp/gum.tgz
|
|
echo "$GUM_SHA256 /tmp/gum.tgz" | sha256sum -c -
|
|
tar -xzf /tmp/gum.tgz -C /tmp/
|
|
install -m 0755 "/tmp/gum_${GUM_VERSION}_Linux_x86_64/gum" /work/overlay/usr/local/bin/gum
|
|
/work/overlay/usr/local/bin/gum --version
|
|
echo "[OK] gum ${GUM_VERSION} vendored into overlay/usr/local/bin/"
|
|
|
|
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.
|
|
# Drop `updates` repo: 3 consecutive builds 404'd on its
|
|
# repodata zchunk file across all mirrors — Fedora infra issue
|
|
# mid-push window. Original reason for `updates` (selinux-policy
|
|
# 43.7 pcre2 fix) is no longer needed; base 43 ships fixed.
|
|
sed -e '/veilor-fix/d' \
|
|
-e '/^shutdown$/d' \
|
|
-e '/repo --name=updates/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
|
|
|
|
# Graft veilor source tree onto the ISO so the installer-generated
|
|
# kickstart's `%post --nochroot` can find SRC at
|
|
# /run/install/repo/veilor/{overlay,scripts,assets}/ when the user
|
|
# promotes the live ISO into a real install.
|
|
ISO_FILE=$(ls /work/*.iso 2>/dev/null | head -1)
|
|
[ -n "$ISO_FILE" ] || { echo "[ERR] no ISO produced by livecd-creator"; exit 1; }
|
|
echo "[INFO] grafting /veilor/ onto $ISO_FILE"
|
|
|
|
# Extract original ISO's exact boot stanza so the rebuild matches
|
|
# livecd-creator's layout byte-for-byte. This is immune to upstream
|
|
# Fedora layout changes (e.g. images/ vs isolinux/ for efiboot.img,
|
|
# partition geometry flags, hybrid MBR/GPT options).
|
|
xorriso -indev "$ISO_FILE" -report_el_torito as_mkisofs 2>&1 | tee /tmp/iso-boot.txt || true
|
|
ORIG_FLAGS=$(xorriso -indev "$ISO_FILE" -report_el_torito as_mkisofs 2>/dev/null | \
|
|
grep -v '^xorriso :' | grep -E '^-' | tr '\n' ' ')
|
|
[ -n "$ORIG_FLAGS" ] || { echo "[ERR] could not extract boot stanza from $ISO_FILE"; exit 1; }
|
|
echo "[INFO] re-pack flags from original ISO: $ORIG_FLAGS"
|
|
|
|
mkdir -p /tmp/iso-mod
|
|
xorriso -osirrox on -indev "$ISO_FILE" -extract / /tmp/iso-mod
|
|
chmod -R u+w /tmp/iso-mod
|
|
mkdir -p /tmp/iso-mod/veilor
|
|
cp -a /work/overlay /work/scripts /work/assets /tmp/iso-mod/veilor/
|
|
|
|
# Replay the exact stanza captured above. eval is needed because
|
|
# ORIG_FLAGS contains multiple flag/value pairs that must word-split.
|
|
eval xorriso -as mkisofs \
|
|
-volid "veilor-os-43" \
|
|
$ORIG_FLAGS \
|
|
-o "${ISO_FILE}.tmp" /tmp/iso-mod
|
|
mv "${ISO_FILE}.tmp" "$ISO_FILE"
|
|
rm -rf /tmp/iso-mod
|
|
echo "[OK] /veilor/ grafted onto $ISO_FILE"
|
|
|
|
# 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 + checksum
|
|
ISO_NAME="veilor-os-${{ github.event.inputs.releasever || '43' }}-$(date +%Y%m%d-%H%M%S).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"
|
|
|
|
# ── ISO publish ────────────────────────────────────────────────────
|
|
# GH Release asset size limit = 2 GiB. Our ISO ~2.8 GiB. Split into
|
|
# chunks before upload. Reassemble client-side via `cat *.part-* > x.iso`.
|
|
# Squashfs is already near-incompressible (zstd -19 → 96%) so split,
|
|
# not compress.
|
|
- name: Split ISO into 2GiB chunks
|
|
if: success() && github.ref == 'refs/heads/main'
|
|
run: |
|
|
# ISO + sidecars created by Fedora container as root. Reclaim
|
|
# ownership so this step (running as runner user) can write.
|
|
sudo chown -R "$(id -u):$(id -g)" build/out
|
|
cd build/out
|
|
ISO=$(ls *.iso | head -1)
|
|
[ -n "$ISO" ] || { echo "[ERR] no ISO"; exit 1; }
|
|
# Split with 1900M chunks (under 2 GiB safe). Suffix .part-aa, .part-ab, ...
|
|
split -b 1900M -d --suffix-length=2 "$ISO" "${ISO}.part-"
|
|
ls -lh
|
|
# Drop the original ISO so it doesn't try to upload (over limit)
|
|
rm -f "$ISO"
|
|
# Generate sha256 of all parts so reassembly is verifiable
|
|
sha256sum *.part-* > "${ISO}.parts.sha256"
|
|
echo "[OK] split into:"
|
|
ls "${ISO}".part-*
|
|
|
|
- name: Install cosign
|
|
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
|
uses: sigstore/cosign-installer@v3
|
|
|
|
- name: Sign ISO parts (keyless)
|
|
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
|
run: |
|
|
cd build/out
|
|
for f in *.part-*; do
|
|
cosign sign-blob --yes "$f" \
|
|
--output-signature "$f.sig" \
|
|
--output-certificate "$f.pem"
|
|
done
|
|
|
|
- name: Generate SBOM (SPDX)
|
|
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
|
uses: anchore/sbom-action@v0
|
|
with:
|
|
path: build/out
|
|
format: spdx-json
|
|
output-file: build/out/veilor-os.spdx.json
|
|
|
|
- name: Build provenance attestation
|
|
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
|
uses: actions/attest-build-provenance@v2
|
|
with:
|
|
subject-path: 'build/out/*.iso.part-*'
|
|
|
|
- name: Publish to ci-latest rolling prerelease
|
|
if: success() && github.ref == 'refs/heads/main'
|
|
# Pinned to last v2 tag confirmed to ship on node20.
|
|
uses: softprops/action-gh-release@v2.0.4
|
|
with:
|
|
tag_name: ci-latest
|
|
name: "ci-latest (auto)"
|
|
body: |
|
|
Rolling auto-build from `main`. Latest commit: ${{ github.sha }}.
|
|
|
|
**ISO is split into chunks (GH release 2 GiB asset limit).**
|
|
Reassemble:
|
|
```
|
|
cat veilor-os-*.iso.part-* > veilor-os.iso
|
|
sha256sum -c veilor-os-*.iso.parts.sha256
|
|
```
|
|
Or use `test/auto-install.sh` which handles reassembly automatically.
|
|
|
|
Not a stable release — for testing only.
|
|
prerelease: true
|
|
make_latest: false
|
|
files: |
|
|
build/out/*.iso.part-*
|
|
build/out/*.sha256
|
|
build/out/*.sig
|
|
build/out/*.pem
|
|
build/out/*.spdx.json
|
|
|
|
# Build log on failure: print inline + skip artifact upload to avoid
|
|
# quota wall. Job log retains everything anyway.
|
|
- name: Print build log on failure
|
|
if: failure()
|
|
run: |
|
|
echo "─── build/out/build.log ───"
|
|
tail -200 build/out/build.log 2>/dev/null || echo "(no build.log)"
|
|
echo "─── anaconda program.log ───"
|
|
find build/out/build/anaconda -name 'program.log' -exec tail -100 {} \; 2>/dev/null || echo "(no anaconda log)"
|
|
|
|
- name: Attach to release on tag
|
|
if: github.event_name == 'release'
|
|
# Pinned to last v2 tag confirmed to ship on node20.
|
|
uses: softprops/action-gh-release@v2.0.4
|
|
with:
|
|
files: |
|
|
build/out/*.iso
|
|
build/out/*.sha256
|