Skip to content

feat: add standalone web-based configuration editor for mios.toml #878

feat: add standalone web-based configuration editor for mios.toml

feat: add standalone web-based configuration editor for mios.toml #878

Workflow file for this run

# 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"
'