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 \ --security-opt label=disable \ --security-opt seccomp=unconfined \ --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 : 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 }}