name: Build and release ISO # Builds the BOS ISO on the hestia self-hosted runner (native Arch container). # Stages bakery desktop apps from the *minisign-verified* stable index at # https://dl.breadway.dev/index.json (see iso/bread-lockfile.toml), then runs # build-local.sh and uploads the ISO to a Forgejo release. A matching GitHub # release is created best-effort and points at 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 # GPG_PRIVATE_KEY — armoured secret key for the dedicated "BOS Release Signing" # identity (releases@breadway.dev); public half is committed # at KEYS.asc for verifying ISO SHA256SUMS only. That key # does not sign the [breadway] pacman repo. No passphrase # (CI-only key, access controlled via the Forgejo secret # store). 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: # Floating tag: this environment cannot pin a reproducible digest of # archlinux:latest. Do not invent one. 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: | # grub is required by profiledef.sh bootmodes=('uefi.grub'): # mkarchiso validates grub-install on the *builder*, not the image. # archiso pulls syslinux/squashfs-tools/libisoburn; it does not pull grub. pacman -Syu --noconfirm archiso grub curl python git minisign - 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: Stage bakery ecosystem from signed stable index run: | set -euo pipefail cd /bos LAPTOP_HOME=/build-home python3 scripts/ci-stage-bakery.py - name: Verify staged bakery bake inputs run: | set -euo pipefail cd /bos LAPTOP_HOME=/build-home bash scripts/ci-verify-bake.sh - 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 \ || echo "skip: GitHub release failed (MIRROR_TOKEN historically broken)" # `stable` is a marker branch only — CI fast-forwards it to whatever # commit the latest real (non-RC) release tag points at. Never merged # into by hand, so unlike the old dev/beta/main model it can't rot: # nobody has to remember to move it, a bot always does. Lets you # `git diff stable..main` before a build to see what's new since the # last release, without a human-maintained promotion step. - name: Fast-forward stable branch to this tag if: ${{ !contains(steps.vars.outputs.tag, '-rc.') }} env: FORGEJO_TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | set -euo pipefail cd /bos git push "https://oauth2:${FORGEJO_TOKEN}@git.breadway.dev/${GITHUB_REPOSITORY}.git" \ "HEAD:refs/heads/stable" --force