diff --git a/.github/workflows/build-bluebuild.yml b/.github/workflows/build-bluebuild.yml index e07785a..daf6738 100644 --- a/.github/workflows/build-bluebuild.yml +++ b/.github/workflows/build-bluebuild.yml @@ -1,16 +1,12 @@ name: Build veilor-os OCI (BlueBuild) # v0.7 spike — builds the bootable OCI image used by the bootstrap -# kickstart's `ostreecontainer` directive. Active only on the -# `v0.7-bluebuild-spike` branch until the spike passes success -# criteria (see bluebuild/README.md). +# 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/ -# -# Security note: all `${{ }}` interpolations in this file are restricted -# to vetted GitHub-controlled values (repository_owner, run number, -# secrets, signed action outputs). No `github.event.issue.title` or -# similar untrusted-user-input is read in run blocks. on: push: @@ -33,73 +29,140 @@ permissions: jobs: build: - name: Build + sign + push OCI - runs-on: ubuntu-24.04 - timeout-minutes: 30 + 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 # push to ghcr.io/veilor-org/veilor-os - id-token: write # cosign keyless signing via Sigstore - attestations: write # SLSA build provenance + packages: write + id-token: write # for GH-only cosign keyless (skipped on Forgejo) + attestations: write env: - OCI_IMAGE: ghcr.io/${{ github.repository_owner }}/veilor-os - OCI_TAG: latest + # 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. v4.2+ ships - # node24 which forgejo-runner v6.4.0 (node20) cannot exec. + # Pinned to last v4 tag confirmed to ship on node20. uses: actions/checkout@v4.1.7 - - name: Free up disk + - name: Install build tooling (Fedora) run: | - sudo rm -rf /opt/hostedtoolcache /usr/share/dotnet /usr/local/lib/android - sudo apt-get clean - df -h + set -euxo pipefail + dnf -y upgrade --refresh + dnf -y install \ + podman \ + buildah \ + skopeo \ + jq \ + git \ + curl \ + tar \ + sudo \ + cosign - # BlueBuild action wraps: image build, cosign sign (keyless via - # Sigstore), GHCR push. Pinned to a commit SHA per CI hardening - # agent 8 (2026-05-05 wave). The trailing comment records the - # tag the SHA resolved from, so future bumps stay legible. - - name: Build + push veilor-os OCI - id: bluebuild - uses: blue-build/github-action@24d146df25adc2cf579e918efe2d9bff6adea408 # v1 - with: - recipe: bluebuild/recipe.yml - registry_token: ${{ secrets.GITHUB_TOKEN }} - pr_event_number: ${{ github.event.number }} - maximize_build_space: true + - name: Install BlueBuild CLI + run: | + set -euxo pipefail + BB_VERSION="0.9.10" + BB_URL="https://github.com/blue-build/cli/releases/download/v${BB_VERSION}/bluebuild-x86_64-unknown-linux-gnu.tar.gz" + curl -fsSL "$BB_URL" -o /tmp/bb.tgz + tar -xzf /tmp/bb.tgz -C /usr/local/bin/ + chmod +x /usr/local/bin/bluebuild + bluebuild --version + + - name: Build OCI image with BlueBuild + run: | + set -euxo pipefail + cd bluebuild + # bluebuild build: + # --recipe-path => path to recipe.yml + # --tag => local tag, then we re-tag for registries + # podman driver to avoid Docker socket coupling + bluebuild build \ + --debug \ + --build-driver podman \ + --inspect-driver skopeo \ + --signing-driver cosign \ + recipe.yml + # bluebuild tags as `:latest` locally. Confirm it + # lands and re-tag for both registries. + podman images + podman tag localhost/veilor-os:latest "${FORGEJO_IMAGE}:${OCI_TAG}" + podman tag localhost/veilor-os:latest "${FORGEJO_IMAGE}:latest" + + - 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: | - podman pull "$OCI_IMAGE:$OCI_TAG" - podman run --rm "$OCI_IMAGE:$OCI_TAG" /bin/bash -c ' + 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 present"; which mullvad-browser - echo "-- yggdrasil present"; which yggdrasil - echo "-- tailscale present"; which tailscale + 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 ' - - name: SBOM (SPDX) - if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' - # Pinned to last v0 tag confirmed to ship on node20. The `@v0` - # floating tag rolls forward and will eventually pull a node24 - # release. TODO(infra): SHA pin in a follow-up sweep. + # ── 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.OCI_IMAGE }}:${{ env.OCI_TAG }} + image: ${{ env.GHCR_IMAGE }}:${{ env.OCI_TAG }} format: spdx-json output-file: veilor-os-oci.spdx.json - - name: Build provenance attestation - if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' - # Pinned to last v2 tag confirmed to ship on node20. + - 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.OCI_IMAGE }} + subject-name: ${{ env.GHCR_IMAGE }} subject-digest: ${{ steps.bluebuild.outputs.digest }}