veilor-os/.github/workflows/build-bluebuild.yml

263 lines
11 KiB
YAML

name: Build veilor-os OCI (BlueBuild)
# v0.7 spike — builds the bootable OCI image used by the bootstrap
# kickstart's `ostreecontainer` directive. Runs on the Forgejo
# self-hosted runner (label `nullstone`); GitHub-side cosign/SBOM/
# attest steps are gated off because Forgejo has no Sigstore Fulcio-
# trusted OIDC issuer (see docs/PROOF-OF-WORK.md, build-iso.yml fix).
#
# Reference: https://blue-build.org/how-to/setup-build-action/
on:
push:
branches: [v0.7-bluebuild-spike]
paths:
- 'bluebuild/**'
- 'overlay/**'
- 'assets/**'
- 'scripts/**'
- '.github/workflows/build-bluebuild.yml'
pull_request:
branches: [main, v0.7-bluebuild-spike]
schedule:
# Rebuild weekly so we pick up upstream secureblue + Fedora updates.
- cron: '0 6 * * 1'
workflow_dispatch:
permissions:
contents: read
jobs:
build:
name: Build + push OCI
# nullstone label resolves to veilor-build:43 (fedora43 + nodejs)
# via runner config. Privileged + userns=host + sock pass-through
# already wired in the runner config (see infra/forgejo/).
runs-on: nullstone
timeout-minutes: 60
permissions:
contents: read
packages: write
id-token: write # for GH-only cosign keyless (skipped on Forgejo)
attestations: write
env:
# Forgejo container registry path. PAT in FORGEJO_REGISTRY_TOKEN
# secret has package:write on veilor-org.
FORGEJO_REGISTRY: git.s8n.ru
FORGEJO_IMAGE: git.s8n.ru/veilor-org/veilor-os
OCI_TAG: "43"
# GH parallel target — only used when run on github.com.
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/veilor-os
steps:
- name: Checkout
# Pinned to last v4 tag confirmed to ship on node20.
uses: actions/checkout@v4.1.7
- name: Fix sudo perms (userns=host artefact)
run: |
# Daemon has userns-remap=default; the act job container is
# launched with --userns=host. The image was pulled under
# remap so /etc/sudo.conf + /etc/sudoers ship as uid 100000.
# sudo refuses to read either unless owned by uid 0. Restore.
chown -R 0:0 /etc/sudo.conf /etc/sudoers /etc/sudoers.d 2>/dev/null || true
ls -la /etc/sudo.conf /etc/sudoers 2>&1 | head -5
- name: Install build tooling (Fedora)
run: |
set -euxo pipefail
dnf -y upgrade --refresh
# veilor-build:43 already ships git, curl, tar, sudo, nodejs.
# cosign is not packaged in Fedora 43; we install it from the
# upstream release tarball below in a separate step.
dnf -y install --skip-unavailable \
podman \
buildah \
skopeo \
jq
# blue-build/github-action shells out to `docker`; Fedora ships
# podman. Symlink so the action finds the CLI.
if ! command -v docker >/dev/null; then
ln -sf "$(command -v podman)" /usr/local/bin/docker
docker --version
fi
- name: Install cosign binary (upstream release)
run: |
set -euxo pipefail
# Fedora 43 has no cosign rpm. Pull static x86_64 binary
# from sigstore/cosign GitHub releases. Pinned to v2.4.1.
COSIGN_VERSION="2.4.1"
curl -fsSL \
"https://github.com/sigstore/cosign/releases/download/v${COSIGN_VERSION}/cosign-linux-amd64" \
-o /usr/local/bin/cosign
chmod +x /usr/local/bin/cosign
cosign version
- name: Pre-pull secureblue base image
env:
GHCR_PULL_TOKEN: ${{ secrets.GHCR_PULL_TOKEN }}
run: |
set -euxo pipefail
# GHCR rate-limits anonymous CI pulls (403 on bearer-token).
# Login with a read-only PAT (forgejo secret GHCR_PULL_TOKEN)
# so bluebuild's buildah inside the CLI container also sees a
# valid auth.json via shared storage bind-mount below.
if [ -n "${GHCR_PULL_TOKEN:-}" ]; then
echo "$GHCR_PULL_TOKEN" | podman login \
--username s8n-ru \
--password-stdin ghcr.io
else
echo "[WARN] GHCR_PULL_TOKEN secret empty; trying anonymous pull"
fi
podman pull ghcr.io/secureblue/kinoite-main-hardened:latest
- name: Stage cosign private key for signing module
env:
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
run: |
set -euo pipefail
if [ -z "${COSIGN_PRIVATE_KEY:-}" ]; then
echo "[ERR] COSIGN_PRIVATE_KEY secret missing"
exit 1
fi
# bluebuild signing module reads from this env var when
# building the cosign.key bind stage. Also write to bluebuild/
# so it sits next to cosign.pub for local reproducible runs.
mkdir -p bluebuild
printf '%s' "$COSIGN_PRIVATE_KEY" > bluebuild/cosign.key
chmod 600 bluebuild/cosign.key
# bluebuild's generated Containerfile uses `FROM scratch as
# stage-keys; COPY cosign.pub /keys/`. Buildah's build context
# is the cwd ($PWD) — symlink the keys to repo root so COPY
# finds them there too.
ln -sf bluebuild/cosign.pub cosign.pub
ln -sf bluebuild/cosign.key cosign.key
ls -la cosign.pub cosign.key 2>&1 | head -4
- name: Build OCI image with BlueBuild CLI container
id: bluebuild
# blue-build/github-action requires docker buildx which podman
# doesn't ship. Run the official BlueBuild CLI container with
# buildah driver instead — works against rootless or rootful
# podman, no docker dependency.
run: |
set -euxo pipefail
# Pull cli image; pinned to v0.9.x at action time.
podman pull ghcr.io/blue-build/cli:latest
# Mount the repo + podman socket; build with buildah driver.
# Bind host /var/lib/containers/storage into the bluebuild
# CLI container so buildah inside it can see the pre-pulled
# secureblue base layer (avoids GHCR auth round-trip during
# templating).
# podman login writes to $XDG_RUNTIME_DIR/containers/auth.json
# by default, which is volatile. Find it + copy to a stable
# path that we then bind into the bluebuild container.
AUTH_SRC=""
for cand in \
"${XDG_RUNTIME_DIR:-/run/user/0}/containers/auth.json" \
"/run/containers/0/auth.json" \
"/root/.config/containers/auth.json" \
"/root/.docker/config.json"; do
if [ -f "$cand" ]; then AUTH_SRC="$cand"; break; fi
done
if [ -z "$AUTH_SRC" ]; then
echo "[ERR] no podman/docker auth.json found post-login"
find / -name auth.json -o -name 'config.json' 2>/dev/null | head -10
exit 1
fi
mkdir -p /root/.config/containers
cp "$AUTH_SRC" /root/.config/containers/auth.json
ls -la /root/.config/containers/auth.json
# Diagnostic: confirm the keypair landed where bluebuild expects.
ls -la bluebuild/
head -1 bluebuild/cosign.pub
head -1 bluebuild/cosign.key | cut -c1-30
podman run --rm \
--privileged \
--entrypoint /usr/bin/bluebuild \
-v "$PWD:/work" \
-v /var/lib/containers/storage:/var/lib/containers/storage \
-v /root/.config/containers/auth.json:/root/.config/containers/auth.json:ro \
-w /work \
-e BB_BUILD_DRIVER=buildah \
ghcr.io/blue-build/cli:latest \
build \
--build-driver buildah \
-vv \
bluebuild/recipe.yml
# bluebuild CLI tags as <recipe-name>:<tag> in local podman
# storage. List + verify, then re-tag for the registries.
podman images
podman tag localhost/veilor-os:latest "${FORGEJO_IMAGE}:${OCI_TAG}" || true
podman tag localhost/veilor-os:latest "${FORGEJO_IMAGE}:latest" || true
- name: Push to Forgejo registry (primary)
if: success() && github.event_name != 'pull_request' && github.server_url != 'https://github.com'
env:
FORGEJO_REGISTRY_TOKEN: ${{ secrets.FORGEJO_REGISTRY_TOKEN }}
FORGEJO_REGISTRY_USER: ${{ secrets.FORGEJO_REGISTRY_USER }}
run: |
set -euo pipefail
if [ -z "${FORGEJO_REGISTRY_TOKEN:-}" ]; then
echo "[WARN] FORGEJO_REGISTRY_TOKEN secret is empty; skipping push"
exit 0
fi
echo "$FORGEJO_REGISTRY_TOKEN" | podman login \
--username "${FORGEJO_REGISTRY_USER:-veilor-org}" \
--password-stdin "$FORGEJO_REGISTRY"
podman push "${FORGEJO_IMAGE}:${OCI_TAG}"
podman push "${FORGEJO_IMAGE}:latest"
echo "[OK] pushed ${FORGEJO_IMAGE}:{${OCI_TAG},latest}"
- name: Push to GHCR (mirror, GitHub-only)
if: success() && github.event_name != 'pull_request' && github.server_url == 'https://github.com'
run: |
set -euo pipefail
podman tag localhost/veilor-os:latest "${GHCR_IMAGE}:${OCI_TAG}"
podman tag localhost/veilor-os:latest "${GHCR_IMAGE}:latest"
echo "${{ secrets.GITHUB_TOKEN }}" | podman login \
--username "${{ github.repository_owner }}" \
--password-stdin ghcr.io
podman push "${GHCR_IMAGE}:${OCI_TAG}"
podman push "${GHCR_IMAGE}:latest"
- name: Smoke-test OCI image
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
run: |
set -euxo pipefail
podman run --rm "localhost/veilor-os:latest" /bin/bash -c '
set -e
echo "-- os-release"
head -5 /etc/os-release
echo "-- sudo present"; which sudo
echo "-- mullvad-browser path"; rpm -q mullvad-browser || echo "not installed"
echo "-- yggdrasil"; rpm -q yggdrasil || echo "not installed"
echo "-- tailscale"; rpm -q tailscale || echo "not installed"
echo "-- veilor-firstboot unit"; ls -la /etc/systemd/system/veilor-firstboot.service 2>&1 || true
'
# ── GitHub-only signing/SBOM/attest ────────────────────────────
# cosign keyless needs Sigstore Fulcio-trusted OIDC. Forgejo
# has none, so these are GH-only. v0.7+ TODO: cosign key-pair
# signing for Forgejo using a stored secret.
- name: SBOM (SPDX, GitHub-only)
if: github.event_name == 'push' && github.server_url == 'https://github.com'
# Pinned to last v0.17 release that ships node20.
uses: anchore/sbom-action@v0.17.2
with:
image: ${{ env.GHCR_IMAGE }}:${{ env.OCI_TAG }}
format: spdx-json
output-file: veilor-os-oci.spdx.json
- name: Build provenance attestation (GitHub-only)
if: github.event_name == 'push' && github.server_url == 'https://github.com'
# Pinned to last v2.2 release that ships node20.
uses: actions/attest-build-provenance@v2.2.3
with:
subject-name: ${{ env.GHCR_IMAGE }}
subject-digest: ${{ steps.bluebuild.outputs.digest }}