Verified against the actual cached mkinitcpio package (41-4), not assumed: its shipped /etc/mkinitcpio.conf template is HOOKS=(base systemd autodetect microcode modconf kms keyboard sd-vconsole block filesystems fsck) — there's no "udev" token at all on a stock install anymore, systemd is the base hook instead. This silently broke two things: - The existing plymouth-hook insertion (`sed 's/\budev\b/\0 plymouth/'`) has been a no-op on every fresh install using a current mkinitcpio package — the boot splash was never actually getting wired into the initramfs, just failing quietly (the script's best-effort `|| echo WARN` pattern doesn't catch a sed that "succeeds" by matching nothing). - My own new encrypt-hook insertion from the previous commit had the same flaw, and would have used the wrong hook regardless: `encrypt` is the udev-based hook, systemd-based initramfs needs `sd-encrypt` instead. post-install.sh now detects which base hook (systemd or udev) is actually in HOOKS once, and uses the matching hook name for both plymouth (unaffected by the choice, just needed the right anchor to insert after) and LUKS unlocking (encrypt vs sd-encrypt — genuinely different hooks). Also: avoid a redundant pacman -Sy in release-iso.yml's signing step — the job already synced repos earlier in the same container.
239 lines
10 KiB
YAML
239 lines
10 KiB
YAML
name: Build and release ISO
|
|
|
|
# Builds the BOS ISO on the hestia self-hosted runner (native Arch container),
|
|
# downloads all bakery ecosystem binaries from their GitHub releases, compiles
|
|
# bread-theme from source, and uploads the resulting ISO to a Forgejo pre-release.
|
|
# A matching GitHub release is created that points to Forgejo for the download
|
|
# (GitHub releases cannot host files larger than 2 GB).
|
|
#
|
|
# Required secrets:
|
|
# RELEASE_TOKEN — Forgejo API token with write:repository scope
|
|
# MIRROR_TOKEN — GitHub personal access token with repo scope (already used by mirror.yml)
|
|
# GPG_PRIVATE_KEY — armoured secret key for the dedicated "BOS Release Signing"
|
|
# identity (releases@breadway.dev); public half is committed
|
|
# at KEYS.asc for verification. No passphrase (CI-only key,
|
|
# access controlled via the Forgejo secret store, not a
|
|
# passphrase nobody could type non-interactively anyway).
|
|
|
|
on:
|
|
push:
|
|
tags: ['v*']
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: 'Git tag to build (e.g. v0.4.0)'
|
|
required: true
|
|
|
|
jobs:
|
|
release-iso:
|
|
runs-on: [self-hosted, hestia]
|
|
container:
|
|
image: archlinux:latest
|
|
# --privileged: mkarchiso needs CAP_SYS_ADMIN for loop mounts + mknod
|
|
# --network=host: gives localhost:3002 access to Forgejo (avoids the
|
|
# public git.breadway.dev → Aegis → Tailscale round-trip for pacman)
|
|
options: --privileged --network=host
|
|
|
|
steps:
|
|
- name: Install build dependencies
|
|
run: |
|
|
pacman -Syu --noconfirm archiso curl python git rust
|
|
|
|
- name: Determine tag and version
|
|
id: vars
|
|
run: |
|
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
TAG="${{ github.event.inputs.tag }}"
|
|
else
|
|
TAG="${{ github.ref_name }}"
|
|
fi
|
|
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
|
echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Clone repository at tag
|
|
run: |
|
|
git clone --branch "${{ steps.vars.outputs.tag }}" --depth 1 \
|
|
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" /bos
|
|
|
|
- name: Download bakery ecosystem binaries
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p /build-home/.local/bin \
|
|
/build-home/.local/state/bakery \
|
|
/build-home/.cache/bakery
|
|
|
|
# Fetch the canonical bakery index
|
|
curl -fsSL "https://dl.breadway.dev/index.json" \
|
|
-o /build-home/.cache/bakery/index.json
|
|
|
|
# Download each binary from dl.breadway.dev (canonical source; github_url
|
|
# is not always published for dev/patch releases) and generate the
|
|
# installed.json that bakery expects in ~/.local/state.
|
|
python3 << 'PYEOF'
|
|
import json, urllib.request, os
|
|
|
|
with open('/build-home/.cache/bakery/index.json') as f:
|
|
idx = json.load(f)
|
|
|
|
BIN_DIR = '/build-home/.local/bin'
|
|
installed = {}
|
|
|
|
for pkg_name, pkg in idx['packages'].items():
|
|
bins = []
|
|
for b in pkg['binaries']:
|
|
dest_name = b['name'].removesuffix('-x86_64')
|
|
dest = os.path.join(BIN_DIR, dest_name)
|
|
url = b['dl_url']
|
|
print(f' {dest_name} <- {url}', flush=True)
|
|
urllib.request.urlretrieve(url, dest)
|
|
os.chmod(dest, 0o755)
|
|
bins.append(dest_name)
|
|
|
|
# installed.json services field is a flat list of unit-name strings
|
|
services = [
|
|
(s['unit'] if isinstance(s, dict) else s)
|
|
for s in pkg.get('services', [])
|
|
]
|
|
installed[pkg_name] = {
|
|
'name': pkg_name,
|
|
'version': pkg['version'],
|
|
'binaries': bins,
|
|
'services': services,
|
|
'installed_at': '2024-01-01T00:00:00+00:00',
|
|
}
|
|
|
|
with open('/build-home/.local/state/bakery/installed.json', 'w') as f:
|
|
json.dump({'packages': installed}, f, indent=2)
|
|
print('installed.json written', flush=True)
|
|
PYEOF
|
|
|
|
- name: Build bread-theme from source
|
|
run: |
|
|
set -euo pipefail
|
|
# bread-theme is not in the bakery index; build it at the tag pinned
|
|
# in bos-settings/Cargo.toml so the CLI matches the library version.
|
|
THEME_TAG=$(grep 'bread-theme.*tag' /bos/bos-settings/Cargo.toml \
|
|
| grep -oP '"v[^"]+"' | tr -d '"')
|
|
echo "Building bread-theme @ $THEME_TAG"
|
|
git clone --branch "$THEME_TAG" --depth 1 \
|
|
https://github.com/Breadway/bread-ecosystem /bread-ecosystem
|
|
cd /bread-ecosystem
|
|
cargo build --release -p bread-theme
|
|
install -m 755 target/release/bread-theme /build-home/.local/bin/bread-theme
|
|
echo "bread-theme built OK"
|
|
|
|
- name: Build ISO
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p /bos-work /bos-out
|
|
cd /bos
|
|
LAPTOP_HOME=/build-home \
|
|
WORK=/bos-work \
|
|
OUT=/bos-out \
|
|
CI_BUILD=1 \
|
|
bash build-local.sh
|
|
ls -lh /bos-out/*.iso
|
|
|
|
- name: Checksum and sign
|
|
env:
|
|
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
|
run: |
|
|
set -euo pipefail
|
|
VERSION="${{ steps.vars.outputs.version }}"
|
|
ISO=$(ls /bos-out/*.iso | head -1)
|
|
ISO_NAME="bos-${VERSION}-x86_64.iso"
|
|
cd /bos-out
|
|
mv "$(basename "$ISO")" "$ISO_NAME"
|
|
|
|
sha256sum "$ISO_NAME" > SHA256SUMS
|
|
cat SHA256SUMS
|
|
|
|
pacman -S --noconfirm --needed gnupg
|
|
export GNUPGHOME=/tmp/gnupg-release
|
|
mkdir -m 700 -p "$GNUPGHOME"
|
|
echo "$GPG_PRIVATE_KEY" | gpg --batch --import
|
|
gpg --batch --yes --local-user releases@breadway.dev \
|
|
--detach-sign --armor -o SHA256SUMS.asc SHA256SUMS
|
|
echo "Signed SHA256SUMS -> SHA256SUMS.asc"
|
|
|
|
- name: Create Forgejo release and upload assets
|
|
env:
|
|
FORGEJO_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
TAG="${{ steps.vars.outputs.tag }}"
|
|
VERSION="${{ steps.vars.outputs.version }}"
|
|
ISO_NAME="bos-${VERSION}-x86_64.iso"
|
|
|
|
# Use an existing release for this tag if one exists (e.g. created
|
|
# manually or by a prior re-run), otherwise create a fresh one.
|
|
EXISTING=$(curl -sf \
|
|
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
|
"http://localhost:3002/api/v1/repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}" \
|
|
2>/dev/null || true)
|
|
RELEASE_ID=$(echo "${EXISTING}" | python3 -c \
|
|
"import json,sys; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true)
|
|
|
|
if [ -z "${RELEASE_ID}" ]; then
|
|
RELEASE=$(curl -fsS -X POST \
|
|
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
"http://localhost:3002/api/v1/repos/${GITHUB_REPOSITORY}/releases" \
|
|
-d "{
|
|
\"tag_name\": \"${TAG}\",
|
|
\"name\": \"BOS ${TAG}\",
|
|
\"prerelease\": false,
|
|
\"body\": \"ISO image attached below. Verify with SHA256SUMS + SHA256SUMS.asc (signed by the BOS Release Signing key — see KEYS.asc in the repo).\\n\\nSee the [README](https://github.com/Breadway/bos#testing-in-a-vm) for VM testing instructions.\"
|
|
}")
|
|
RELEASE_ID=$(echo "${RELEASE}" | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")
|
|
fi
|
|
echo "Using release ID: ${RELEASE_ID}"
|
|
|
|
upload_asset() {
|
|
local file="$1" name
|
|
name="$(basename "$file")"
|
|
local asset_id
|
|
asset_id=$(curl -sf \
|
|
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
|
"http://localhost:3002/api/v1/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets" \
|
|
| python3 -c "
|
|
import json,sys
|
|
assets=json.load(sys.stdin)
|
|
match=[a['id'] for a in assets if a['name']=='${name}']
|
|
print(match[0] if match else '')
|
|
" 2>/dev/null || true)
|
|
if [ -n "${asset_id}" ]; then
|
|
curl -fsS -X DELETE \
|
|
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
|
"http://localhost:3002/api/v1/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets/${asset_id}"
|
|
echo "Removed existing ${name} asset"
|
|
fi
|
|
curl -fsS -X POST \
|
|
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
|
-F "attachment=@${file};filename=${name}" \
|
|
"http://localhost:3002/api/v1/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets"
|
|
echo "Uploaded: ${name}"
|
|
}
|
|
|
|
upload_asset "/bos-out/${ISO_NAME}"
|
|
upload_asset "/bos-out/SHA256SUMS"
|
|
upload_asset "/bos-out/SHA256SUMS.asc"
|
|
|
|
- name: Create GitHub release
|
|
env:
|
|
GH_TOKEN: ${{ secrets.MIRROR_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
TAG="${{ steps.vars.outputs.tag }}"
|
|
VERSION="${{ steps.vars.outputs.version }}"
|
|
FORGEJO_URL="https://git.breadway.dev/${GITHUB_REPOSITORY}/releases/tag/${TAG}"
|
|
|
|
printf '**Download ISO:** %s\n\nGitHub releases cannot host files >2 GB; the `bos-%s-x86_64.iso` (~2.5 GB), SHA256SUMS, and SHA256SUMS.asc (signed by the BOS Release Signing key — public half at [KEYS.asc](https://github.com/Breadway/bos/blob/main/KEYS.asc)) are all on Forgejo.\n\nSee the [README](https://github.com/Breadway/bos#testing-in-a-vm) for VM testing instructions.' \
|
|
"${FORGEJO_URL}" "${VERSION}" > /tmp/gh-release-notes.md
|
|
|
|
gh release create "${TAG}" \
|
|
--repo "Breadway/bos" \
|
|
--title "BOS ${TAG}" \
|
|
\
|
|
--notes-file /tmp/gh-release-notes.md \
|
|
2>/dev/null || echo "GitHub release already exists — skipping"
|