Linux service / process / container verbs #545
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: mios-ci | |
| # In-repo build: MiOS owns the Containerfile and Justfile, so CI builds | |
| # straight from the repository tree -- no cross-repo fetch. | |
| # | |
| # Parity with .forgejo/workflows/build-mios.yml: both pipelines use | |
| # `podman build` (NOT docker/build-push-action) so the OCI manifests, | |
| # layer digests, labels, and provenance match bit-for-bit between the | |
| # self-hosted Forgejo Runner closure and the GitHub Actions cloud | |
| # closure. An operator pulling ghcr.io/mios-dev/mios:latest gets the | |
| # same image whether the runner was forgejo-side or github-side. | |
| on: | |
| push: | |
| branches: [main] | |
| tags: ['v*'] | |
| pull_request: | |
| branches: [main] | |
| workflow_dispatch: | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: mios-dev/mios | |
| jobs: | |
| build: | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| contents: read | |
| packages: write | |
| id-token: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Read VERSION | |
| id: ver | |
| run: echo "version=$(cat VERSION)" >> "$GITHUB_OUTPUT" | |
| - name: Resolve image tag (VERSION + UTC timestamp + short SHA) | |
| id: tag | |
| run: | | |
| VER="$(cat VERSION 2>/dev/null || echo 'v0.0.0')" | |
| SHA="$(git rev-parse --short=12 HEAD)" | |
| TS="$(date -u +%Y%m%d-%H%M%S)" | |
| TAG="${VER#v}-${TS}-${SHA}" | |
| echo "image_tag=${TAG}" >> "$GITHUB_OUTPUT" | |
| echo "Building tag: ${TAG}" | |
| - name: Install podman | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install -y podman skopeo | |
| - name: Free disk space on the GHA runner | |
| # GitHub-hosted ubuntu-24.04 runners ship with ~14 GB free on | |
| # /. The MiOS Containerfile bakes 16+ container images into | |
| # /usr/lib/containers/storage at OCI build time | |
| # (BOUND-IMAGES law -- runtime pulls are bugs), and the bake | |
| # blew through the runner's free space mid-pull at the 16th | |
| # image (open-webui's NotoSansSC font blob, ~30 MB) on | |
| # 2026-05-15. jlumbroso/free-disk-space reclaims ~30 GB by | |
| # removing pre-installed Android SDK, .NET, GHC, Haskell, | |
| # CodeQL, large /opt entries, etc. -- none of which the OCI | |
| # build needs. Forgejo's self-hosted runner has plenty of | |
| # disk and skips this; keeping the steps in parity here | |
| # matters less than building successfully. | |
| uses: jlumbroso/free-disk-space@main | |
| with: | |
| tool-cache: false | |
| android: true | |
| dotnet: true | |
| haskell: true | |
| large-packages: true | |
| docker-images: false | |
| swap-storage: false | |
| - name: Login to GHCR (raise base-image pull rate limit) | |
| # Anonymous GHCR pulls hit "503 Egress is over the account limit" | |
| # mid-pull on big multi-layer images like ublue-os/ucore-hci | |
| # (~70 blobs, all anonymous-quota-counted). Authenticated pulls | |
| # don't share the anonymous pool, so logging in even with the | |
| # auto-provided GITHUB_TOKEN (which has GHCR read on public | |
| # images) bypasses the rate limit. Operator-confirmed CI failure | |
| # 2026-05-15: Containerfile FROM ghcr.io/ublue-os/ucore-hci died | |
| # at blob 67/70 with the anonymous-quota error. | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| echo "$GH_TOKEN" | sudo podman login ghcr.io \ | |
| -u "${{ github.actor }}" --password-stdin | |
| - name: Build OCI image (parity with Forgejo Runner) | |
| run: | | |
| set -euo pipefail | |
| # Source build-args from /etc/mios/install.env if present | |
| # (matches Forgejo runner behavior). On GitHub-hosted runners | |
| # there's no install.env so the build uses Containerfile | |
| # defaults — which is the correct vendor-neutral build. | |
| BUILD_ARGS=() | |
| if [[ -r /etc/mios/install.env ]]; then | |
| # shellcheck source=/dev/null | |
| source /etc/mios/install.env | |
| [[ -n "${MIOS_LINUX_USER:-}" ]] && BUILD_ARGS+=(--build-arg "MIOS_USER=${MIOS_LINUX_USER}") | |
| [[ -n "${MIOS_HOSTNAME:-}" ]] && BUILD_ARGS+=(--build-arg "MIOS_HOSTNAME=${MIOS_HOSTNAME}") | |
| [[ -n "${MIOS_AI_MODEL:-}" ]] && BUILD_ARGS+=(--build-arg "MIOS_AI_MODEL=${MIOS_AI_MODEL}") | |
| [[ -n "${MIOS_AI_EMBED_MODEL:-}" ]] && BUILD_ARGS+=(--build-arg "MIOS_AI_EMBED_MODEL=${MIOS_AI_EMBED_MODEL}") | |
| [[ -n "${MIOS_OLLAMA_BAKE_MODELS:-}" ]] && BUILD_ARGS+=(--build-arg "MIOS_OLLAMA_BAKE_MODELS=${MIOS_OLLAMA_BAKE_MODELS}") | |
| fi | |
| # sudo: rootful podman avoids the user-namespace UID exhaustion | |
| # that breaks the bound-images bake step. The bake's inner | |
| # `podman --root /usr/lib/containers/storage pull` unpacks | |
| # images that contain files owned by high-numbered system | |
| # GIDs (e.g. /etc/gshadow gid=42); rootless podman remaps via | |
| # /etc/subuid + /etc/subgid which the GHA runner user has only | |
| # 65k entries for, and the chown then fails with "lchown | |
| # /etc/gshadow: invalid argument" -- "potentially insufficient | |
| # UIDs or GIDs available in user namespace". Rootful podman | |
| # has full UID range and skips the remap. Operator-confirmed | |
| # CI failure 2026-05-15 (qdrant + 13 other bound images failed | |
| # with this exact error mid-bake). | |
| sudo podman build \ | |
| "${BUILD_ARGS[@]}" \ | |
| -f Containerfile \ | |
| -t "localhost/mios:latest" \ | |
| -t "localhost/mios:${{ steps.tag.outputs.image_tag }}" \ | |
| . | |
| - name: Verify bootc lint passes (Architectural Law 4) | |
| run: | | |
| # Containerfile already runs `bootc container lint` as the | |
| # final build step. Belt-and-suspenders check: confirm the | |
| # freshly-built image still carries the required bootc labels. | |
| # Identical check to .forgejo/workflows/build-mios.yml. | |
| # sudo: read from rootful storage where the build placed the image | |
| sudo podman image inspect localhost/mios:latest \ | |
| --format '{{ index .Config.Labels "containers.bootc" }}' \ | |
| | grep -qx '1' || { | |
| echo "ERROR: built image missing containers.bootc=1 label" >&2 | |
| exit 1 | |
| } | |
| sudo podman image inspect localhost/mios:latest \ | |
| --format '{{ index .Config.Labels "ostree.bootable" }}' \ | |
| | grep -qx '1' || { | |
| echo "ERROR: built image missing ostree.bootable=1 label" >&2 | |
| exit 1 | |
| } | |
| - name: Rechunk for smaller Day-2 deltas (every push) | |
| run: | | |
| set -euo pipefail | |
| # hhd-dev/rechunk produces 5-10x smaller bootc-upgrade | |
| # deltas. Forgejo Runner runs this on every push (closing | |
| # the self-replication loop with optimal pull bandwidth); | |
| # GitHub Actions does the same so cloud-pulled images | |
| # match self-hosted images bit-for-bit. | |
| IMAGE_TAG="localhost/mios:${{ steps.tag.outputs.image_tag }}" | |
| MAX_LAYERS="${MIOS_RECHUNK_MAX_LAYERS:-67}" | |
| # sudo: rechunk reads from + writes to rootful storage | |
| sudo podman run --rm \ | |
| --security-opt label=type:unconfined_t \ | |
| -v /var/lib/containers/storage:/var/lib/containers/storage \ | |
| "${IMAGE_TAG}" \ | |
| /usr/libexec/bootc-base-imagectl rechunk \ | |
| --max-layers "${MAX_LAYERS}" \ | |
| "containers-storage:${IMAGE_TAG}" \ | |
| "containers-storage:${IMAGE_TAG}-rechunked" || { | |
| echo "WARN: rechunk failed; pushing un-rechunked image" >&2 | |
| exit 0 | |
| } | |
| sudo podman tag "${IMAGE_TAG}-rechunked" "${IMAGE_TAG}" | |
| sudo podman tag "${IMAGE_TAG}" "localhost/mios:latest" | |
| - name: Compute registry tags | |
| id: meta | |
| if: github.event_name != 'pull_request' | |
| run: | | |
| BASE="${REGISTRY}/${IMAGE_NAME}" | |
| TAGS=() | |
| if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then | |
| TAGS+=("${BASE}:latest") | |
| TAGS+=("${BASE}:${{ steps.ver.outputs.version }}") | |
| fi | |
| if [[ "${{ github.ref }}" == refs/tags/v* ]]; then | |
| SEMVER="${GITHUB_REF#refs/tags/v}" | |
| TAGS+=("${BASE}:${SEMVER}") | |
| TAGS+=("${BASE}:latest") | |
| fi | |
| TAGS+=("${BASE}:${{ steps.tag.outputs.image_tag }}") | |
| { | |
| echo "tags<<EOF" | |
| printf '%s\n' "${TAGS[@]}" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Log in to GHCR (push step) | |
| if: github.event_name != 'pull_request' | |
| run: | | |
| echo "${{ secrets.GITHUB_TOKEN }}" | \ | |
| sudo podman login -u "${{ github.actor }}" --password-stdin "${REGISTRY}" | |
| - name: Push to GHCR | |
| if: github.event_name != 'pull_request' | |
| run: | | |
| set -euo pipefail | |
| while IFS= read -r tag; do | |
| [[ -z "$tag" ]] && continue | |
| sudo podman tag localhost/mios:latest "$tag" | |
| sudo podman push "$tag" | |
| echo "Pushed: $tag" | |
| done <<< "${{ steps.meta.outputs.tags }}" | |
| - name: Install cosign | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| uses: sigstore/cosign-installer@v3 | |
| - name: Cosign keyless sign (tag pushes) | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| env: | |
| COSIGN_EXPERIMENTAL: '1' | |
| run: | | |
| set -euo pipefail | |
| while IFS= read -r tag; do | |
| [[ -z "$tag" ]] && continue | |
| cosign sign --yes "$tag" | |
| done <<< "${{ steps.meta.outputs.tags }}" | |
| smoke-test: | |
| runs-on: ubuntu-24.04 | |
| needs: build | |
| if: github.event_name == 'pull_request' | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Install podman | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install -y podman | |
| - name: Free disk space on the GHA runner (smoke job) | |
| # Same rationale as the main build job -- runner needs ~30 GB | |
| # extra room for the 16+ bound-image bakes during smoke build. | |
| uses: jlumbroso/free-disk-space@main | |
| with: | |
| tool-cache: false | |
| android: true | |
| dotnet: true | |
| haskell: true | |
| large-packages: true | |
| docker-images: false | |
| swap-storage: false | |
| - name: Login to GHCR (raise base-image pull rate limit, smoke step) | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| echo "$GH_TOKEN" | sudo podman login ghcr.io \ | |
| -u "${{ github.actor }}" --password-stdin | |
| - name: Smoke build (lint via Containerfile final RUN) | |
| run: | | |
| # sudo: rootful podman avoids user-namespace UID exhaustion in | |
| # the bake step (same fix as the main build step above). | |
| sudo podman build -t mios:smoke -f Containerfile . |