feat: add standalone web-based configuration editor for mios.toml #878
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
| # AI-hint: GitHub Actions workflow for building, tagging, and pushing MiOS OCI images to ghcr.io using podman to ensure bit-for-bit parity between GitHub and Forgejo build environments. | |
| # AI-related: /etc/mios/install.env, /usr/lib/mios/agent-pipe/server.py, mios-ci, mios-dev, mios-pc-control, mios-launcher-daemon, mios-flatpak-icon-sanitize, mios-wsl-interop-priority, mios-wsl-interop-priority.service | |
| 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: | |
| # WS-0A drift gate: source-tree fitness-functions that need NO built image. | |
| # Runs first and fast so drift (un-wired SSOT keys, broken orchestration | |
| # leaves) fails the PR before the expensive OCI bake. Mirrors the post-build | |
| # sub-phases in automation/build.sh and the `just drift-gate` target. | |
| drift-gate: | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: SSOT-render conformance lint (38-ssot-lint.sh) | |
| run: bash ./automation/38-ssot-lint.sh | |
| - name: Agent-pipe unit tests (test_mios_*.py) | |
| run: | | |
| cd ./usr/lib/mios/agent-pipe | |
| fails=0 | |
| for t in test_mios_*.py; do | |
| if python3 "$t" >/dev/null 2>&1; then | |
| echo " [ OK ] $t" | |
| else | |
| echo " [FAIL] $t"; python3 "$t" || true; fails=$((fails + 1)) | |
| fi | |
| done | |
| if [ "$fails" -gt 0 ]; then | |
| echo "::error::${fails} agent-pipe test script(s) failed"; exit 1 | |
| fi | |
| - name: Doc-generator unit test (test_mios_docgen.py) | |
| run: | | |
| cd ./usr/libexec/mios | |
| if python3 test_mios_docgen.py >/dev/null 2>&1; then | |
| echo " [ OK ] test_mios_docgen.py" | |
| else | |
| python3 test_mios_docgen.py || true | |
| echo "::error::test_mios_docgen.py failed"; exit 1 | |
| fi | |
| - name: AI-plane source drift checks (38-drift-checks.sh) | |
| run: bash ./automation/38-drift-checks.sh | |
| build: | |
| needs: drift-gate | |
| 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 | |
| # VERSION must yield ONE clean token. Strip comment lines (e.g. a | |
| # stray AI-hint header) + all whitespace so a multi-line value can't | |
| # write a bare line to $GITHUB_OUTPUT ("Invalid format" failure 2026-06-13). | |
| run: | | |
| ver="$(grep -vE '^[[:space:]]*#' VERSION | tr -d '[:space:]')" | |
| echo "version=${ver:-0.0.0}" >> "$GITHUB_OUTPUT" | |
| - name: Resolve image tag (VERSION + UTC timestamp + short SHA) | |
| id: tag | |
| run: | | |
| VER="$(grep -vE '^[[:space:]]*#' VERSION 2>/dev/null | tr -d '[:space:]')"; VER="${VER:-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: Configure host podman storage (disable metacopy) | |
| run: | | |
| sudo mkdir -p /etc/containers /mnt/tmp | |
| # runroot + graphroot are REQUIRED (a [storage] table with only `driver` | |
| # makes `podman system reset` abort with "runroot must be set"). Put | |
| # graphroot on /mnt -- the GHA runner's LARGE ephemeral disk (~65GB+ | |
| # free) vs / (~21GB even after jlumbroso frees ~30GB). The MiOS image | |
| # bakes 21 large bound-images (~50GB incl AI lanes); committing that | |
| # layer on / exhausts the disk and the layer copy's pipe closes ("io: | |
| # read/write on closed pipe", exit 125, 2026-06-21). runroot stays on | |
| # tmpfs /run (small runtime state only). install-robustness 2026-06-21. | |
| echo -e '[storage]\ndriver = "overlay"\ngraphroot = "/mnt/containers-storage"\nrunroot = "/run/containers/storage"\n[storage.options.overlay]\nmountopt = "nodev"' | sudo tee /etc/containers/storage.conf | |
| # The GHA runner ships a pre-seeded containers store whose libpod DB | |
| # may record an empty/foreign graph driver. `podman system reset` | |
| # reads that DB FIRST and aborts with 'database graph driver "" does | |
| # not match our graph driver "overlay"' BEFORE it can wipe. Remove | |
| # the stale store (rootful + rootless) so reset starts from a clean | |
| # slate, then make reset itself non-fatal (install-robustness | |
| # 2026-06-21). | |
| sudo rm -rf /var/lib/containers/storage /run/containers/storage /mnt/containers-storage | |
| rm -rf "${HOME}/.local/share/containers/storage" 2>/dev/null || true | |
| sudo podman system reset -f || true | |
| - 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: true | |
| android: true | |
| dotnet: true | |
| haskell: true | |
| large-packages: true | |
| docker-images: true | |
| swap-storage: true | |
| - 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). | |
| # TMPDIR on /mnt too -- buildah's commit scratch must not spill onto | |
| # the small / (install-robustness 2026-06-21). | |
| sudo TMPDIR=/mnt/tmp 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: github.event_name != 'pull_request' | |
| uses: sigstore/cosign-installer@v3 | |
| # Sign EVERY pushed image (including :latest from main), not just v* tags | |
| # -- closes the "unsigned :latest" gap and makes the SECURITY.md "CI signs | |
| # every push with cosign keyless" claim true. Keyless OIDC; needs the | |
| # job's `permissions: id-token: write` (already set -- the v-tag .sig | |
| # objects in GHCR prove keyless signing works). Operator 2026-06-06. | |
| - name: Cosign keyless sign (all pushes incl. :latest) | |
| if: github.event_name != 'pull_request' | |
| 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: Configure host podman storage (disable metacopy) | |
| run: | | |
| sudo mkdir -p /etc/containers /mnt/tmp | |
| # runroot + graphroot are REQUIRED (a [storage] table with only `driver` | |
| # makes `podman system reset` abort with "runroot must be set"). Put | |
| # graphroot on /mnt -- the GHA runner's LARGE ephemeral disk (~65GB+ | |
| # free) vs / (~21GB even after jlumbroso frees ~30GB). The MiOS image | |
| # bakes 21 large bound-images (~50GB incl AI lanes); committing that | |
| # layer on / exhausts the disk and the layer copy's pipe closes ("io: | |
| # read/write on closed pipe", exit 125, 2026-06-21). runroot stays on | |
| # tmpfs /run (small runtime state only). install-robustness 2026-06-21. | |
| echo -e '[storage]\ndriver = "overlay"\ngraphroot = "/mnt/containers-storage"\nrunroot = "/run/containers/storage"\n[storage.options.overlay]\nmountopt = "nodev"' | sudo tee /etc/containers/storage.conf | |
| # The GHA runner ships a pre-seeded containers store whose libpod DB | |
| # may record an empty/foreign graph driver. `podman system reset` | |
| # reads that DB FIRST and aborts with 'database graph driver "" does | |
| # not match our graph driver "overlay"' BEFORE it can wipe. Remove | |
| # the stale store (rootful + rootless) so reset starts from a clean | |
| # slate, then make reset itself non-fatal (install-robustness | |
| # 2026-06-21). | |
| sudo rm -rf /var/lib/containers/storage /run/containers/storage /mnt/containers-storage | |
| rm -rf "${HOME}/.local/share/containers/storage" 2>/dev/null || true | |
| sudo podman system reset -f || true | |
| - 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: true | |
| android: true | |
| dotnet: true | |
| haskell: true | |
| large-packages: true | |
| docker-images: true | |
| swap-storage: true | |
| - 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 TMPDIR=/mnt/tmp podman build -t mios:smoke -f Containerfile . | |
| - name: Smoke RUN (image runs; key MiOS components present + agent-pipe compiles) | |
| run: | | |
| # A real runtime smoke (not just a build/lint): boot the image in | |
| # podman, compile the agent-pipe, and assert the load-bearing shims + | |
| # units shipped. Catches a build that lints clean but is missing | |
| # components or has a broken server.py. Operator 2026-06-06 (closes | |
| # the "CI has no run/boot test" gap from the maturity review). | |
| set -euo pipefail | |
| sudo podman run --rm mios:smoke bash -lc ' | |
| set -e | |
| python3 -m py_compile /usr/lib/mios/agent-pipe/server.py && echo "agent-pipe compile: OK" | |
| for b in flatpak-launch mios-pc-control mios-launcher-daemon \ | |
| mios-flatpak-icon-sanitize; do | |
| test -e "/usr/libexec/mios/$b" || { echo "MISSING: /usr/libexec/mios/$b"; exit 1; } | |
| done | |
| echo "libexec shims: OK" | |
| test -f /usr/lib/systemd/system/mios-wsl-interop-priority.service \ | |
| && echo "interop-priority svc: OK" || { echo "MISSING interop svc"; exit 1; } | |
| echo "SMOKE_RUN_PASS" | |
| ' |