Skip to content

Build: /app YAML Schemas #52

Build: /app YAML Schemas

Build: /app YAML Schemas #52

name: "Build: /app YAML Schemas"
# Validates RouterOS /app YAML schemas and all built-in /app entries against them.
# Generates per-version schema files under docs/<version>/ and commits to main.
#
# Triggered manually (with rosver input) or automatically via auto.yaml on new versions.
# Boots RouterOS CHR with extra packages (container/app feature requires them).
#
# Two-part validation:
# Part 1: JSON Schema meta-validation (schemas must be valid per json-schema.org)
# Part 2: Live validation of all ~80 built-in /app YAMLs from CHR against the schema
# If any fail, a GitHub issue is filed listing failing app names.
on:
workflow_dispatch:
inputs:
rosver:
description: 'RouterOS Version (e.g. 7.22, 7.22rc2, 7.22beta4)'
required: true
default: "7.22"
permissions:
contents: write
issues: write
jobs:
validate-app-yaml-schemas:
runs-on: ubuntu-latest
env:
URLBASE: http://localhost:9180/rest
BASICAUTH: "admin:"
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Configure git for automated commits
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
- name: Install QEMU
# qemu-system-x86 provides qemu-system-x86_64; qemu-utils provides qemu-img for disk conversion
run: sudo apt-get update -y && sudo apt-get install -y qemu-system-x86 qemu-utils
- name: Enable KVM
# GitHub hosted ubuntu-latest runners support KVM; this step makes /dev/kvm accessible.
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
ls -la /dev/kvm
- name: Download RouterOS CHR image
run: |
ROUTEROS_VERSION="${{ github.event.inputs.rosver }}"
echo "Downloading RouterOS $ROUTEROS_VERSION CHR image..."
# Try download.mikrotik.com first (stable releases), fall back to cdn.mikrotik.com (beta/rc)
wget -q "https://download.mikrotik.com/routeros/${ROUTEROS_VERSION}/chr-${ROUTEROS_VERSION}.vdi.zip" \
-O "chr-${ROUTEROS_VERSION}.vdi.zip" \
|| (rm -f "chr-${ROUTEROS_VERSION}.vdi.zip" && \
wget -q "https://cdn.mikrotik.com/routeros/${ROUTEROS_VERSION}/chr-${ROUTEROS_VERSION}.vdi.zip" \
-O "chr-${ROUTEROS_VERSION}.vdi.zip")
unzip -q "chr-${ROUTEROS_VERSION}.vdi.zip"
rm -f "chr-${ROUTEROS_VERSION}.vdi.zip"
ls -lh chr-*.vdi
- name: Convert CHR image to qcow2 for virtio disk
run: |
ROUTEROS_VERSION="${{ github.event.inputs.rosver }}"
qemu-img convert -f vdi -O qcow2 \
"chr-${ROUTEROS_VERSION}.vdi" \
"chr-${ROUTEROS_VERSION}.qcow2"
rm -f "chr-${ROUTEROS_VERSION}.vdi"
ls -lh chr-*.qcow2
- name: Start RouterOS CHR in QEMU
# host:9180 → VM:80 (REST API), host:9122 → VM:22 (SSH for extra-packages SCP)
# Extra-package jobs use 1024 MB to avoid memory pressure after package activation.
run: |
ROUTEROS_VERSION="${{ github.event.inputs.rosver }}"
KVM_OPTS=""
if [ -e /dev/kvm ]; then
KVM_OPTS="-enable-kvm -cpu host"
echo "KVM available — using hardware acceleration."
else
echo "::warning::KVM not found — QEMU will run in software emulation mode (slow)."
fi
nohup qemu-system-x86_64 \
${KVM_OPTS} \
-m 1024 \
-nographic \
-drive file=chr-${ROUTEROS_VERSION}.qcow2,format=qcow2,if=virtio \
-netdev user,id=net0,hostfwd=tcp::9180-:80,hostfwd=tcp::9122-:22 \
-device virtio-net-pci,netdev=net0 \
&>/tmp/qemu.log &
QEMU_PID=$!
echo "QEMU started with PID: $QEMU_PID"
echo "$QEMU_PID" > /tmp/qemu.pid
- name: Wait for RouterOS REST API to become available
run: |
echo "Waiting for RouterOS HTTP server to start (up to 5 min)..."
for i in {1..30}; do
if curl -sS -m 5 --fail http://localhost:9180/ > /dev/null 2>&1; then
echo "RouterOS REST API is up after $i attempt(s)."
exit 0
fi
echo "Attempt $i/30: not ready yet, retrying in 10 seconds..."
sleep 10
done
echo "::error::RouterOS did not start within 5 minutes. QEMU log:"
cat /tmp/qemu.log
exit 1
- name: Verify REST API is responding
run: curl -sS --fail http://admin@localhost:9180/rest/ip/address | jq .
- name: Download and install extra packages into CHR
# Extra packages include container/app feature required for /app validation
run: |
mkdir extra
cd extra
# Try download.mikrotik.com first (stable), fall back to cdn.mikrotik.com (beta/rc)
wget -q "https://download.mikrotik.com/routeros/${{ github.event.inputs.rosver }}/all_packages-x86-${{ github.event.inputs.rosver }}.zip" \
|| wget -q "https://cdn.mikrotik.com/routeros/${{ github.event.inputs.rosver }}/all_packages-x86-${{ github.event.inputs.rosver }}.zip"
unzip -q all_packages*.zip
rm all_packages*.zip
cd ..
scp -o 'StrictHostKeyChecking no' -P 9122 extra/* admin@localhost:/
- name: Reboot CHR to activate extra packages
run: |
if curl -sS --fail -X POST "http://admin@localhost:9180/rest/system/reboot" \
--header "Content-Type: application/json"; then
echo "Reboot initiated via REST API"
else
echo "REST API reboot failed, trying SSH..."
ssh -o 'StrictHostKeyChecking no' -p 9122 admin@localhost /system/reboot || true
fi
- name: Setup Bun Runtime
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 'latest'
- name: Wait for RouterOS to come back up after extra-packages reboot
run: |
echo "Waiting for RouterOS to restart with extra packages (up to 5 min)..."
for i in {1..30}; do
if curl -sS -m 5 --fail http://localhost:9180/ > /dev/null 2>&1; then
echo "RouterOS REST API is back up after $i attempt(s)."
exit 0
fi
echo "Attempt $i/30: not ready yet, retrying in 10 seconds..."
sleep 10
done
echo "::error::RouterOS did not restart within 5 minutes after extra-packages reboot. QEMU log:"
cat /tmp/qemu.log
exit 1
- name: List installed RouterOS packages (including extras)
run: curl -sS --fail http://admin@localhost:9180/rest/system/package | jq '[.[] | .name]'
- name: Get RouterOS version from device
id: connection-check
run: |
BUNROSVER=$(bun rest2raml.js --version)
echo "Detected RouterOS version: $BUNROSVER"
echo "rosver=$BUNROSVER" | xargs >> "$GITHUB_OUTPUT"
- name: Install validation dependencies
run: bun install js-yaml ajv ajv-formats
- name: Run /app YAML schema validation
# appyamlvalidate.js:
# Part 1 — validates JSON schemas against json-schema.org meta-schema
# Part 2 — fetches all built-in /app YAMLs from CHR and validates each against the schema
# Exit code 0: all passed; 1: schema meta-validation failed; 2: /app YAML validation failed
id: validate
env:
APP_YAML_FAILURES_FILE: ${{ runner.temp }}/app-yaml-failures.txt
run: |
ROSVER="${{ steps.connection-check.outputs.rosver }}"
rm -f "$APP_YAML_FAILURES_FILE"
set +e
bun appyamlvalidate.js "$ROSVER"
EXIT_CODE=$?
set -e
echo "exit_code=${EXIT_CODE}" >> $GITHUB_OUTPUT
# Exit code 0 or 2 are OK for continuing — 0 means all passed, 2 means schemas
# are valid but some /app YAMLs don't conform. Exit 1 (meta-validation failed)
# means the schemas are invalid JSON Schema and should not be committed.
if [ $EXIT_CODE -eq 1 ]; then
echo "::error::JSON Schema meta-validation failed — schemas are invalid and will not be committed"
exit 1
fi
exit 0
- name: Create GitHub issue if /app YAML validation failed
# Only triggered when exit code 2 (/app YAML validation failures found).
# Lists each failing built-in app so developers know which apps expose schema gaps.
if: steps.validate.outputs.exit_code == '2'
uses: actions/github-script@v9
env:
APP_YAML_FAILURES_FILE: ${{ runner.temp }}/app-yaml-failures.txt
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs')
const rosver = '${{ steps.connection-check.outputs.rosver }}'
const failuresPath = process.env.APP_YAML_FAILURES_FILE
let failureDetails = '(no details file found)'
try {
failureDetails = fs.readFileSync(failuresPath, 'utf8')
} catch (e) {}
const body = [
`## RouterOS ${rosver} /app YAML Schema Validation Failures`,
'',
`RouterOS **${rosver}** built-in \`/app\` YAML entries failed validation against ` +
`\`docs/${rosver}/routeros-app-yaml-schema.json\`.`,
'',
'This typically means MikroTik has introduced new YAML syntax not yet covered by the schema.',
'The schema should be updated to allow the new syntax and re-validated.',
'',
'### Failing /app entries',
'',
failureDetails,
'',
`### Workflow run`,
`[View run logs](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})`,
].join('\n')
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `RouterOS ${rosver}: /app YAML schema validation failures`,
body,
labels: ['bug', 'app-yaml-schema'],
})
console.log('Created GitHub issue for validation failures')
- name: Check app.json publish artifact
if: always() && steps.connection-check.outcome == 'success'
id: app-json-artifact
run: |
ROSVER="${{ steps.connection-check.outputs.rosver }}"
APP_JSON_PATH="docs/${ROSVER}/app.json"
if [ -f "$APP_JSON_PATH" ]; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "path=$APP_JSON_PATH" >> "$GITHUB_OUTPUT"
echo "Found $APP_JSON_PATH"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "No app.json found — /app fetch may have been skipped or failed"
fi
- name: Commit app.json (raw /app data) if fetch succeeded
# app.json is valuable for debugging/review — commit it if we fetched it,
# regardless of whether the schemas passed validation or not.
if: always() && steps.connection-check.outcome == 'success' && steps.app-json-artifact.outputs.exists == 'true'
uses: ./.github/actions/publish-with-retry
with:
publish-paths: |
${{ steps.app-json-artifact.outputs.path }}
commit-message: Add ${{ steps.connection-check.outputs.rosver }} app.json (raw /app data) [${{ github.workflow }}]
docs-index-excludes: |
docs/${{ steps.connection-check.outputs.rosver }}/routeros-app-yaml-schema.json
docs/${{ steps.connection-check.outputs.rosver }}/routeros-app-yaml-store-schema.json
- name: Publish and commit per-version /app YAML schemas
# Only publish/commit schemas if validation passed completely (exit code 0).
# Exit code 2 (live validation failed) means schemas are valid JSON Schema but some
# MikroTik apps don't conform — per user feedback, don't commit until both pass
# so auto.yaml can detect missing schemas and retry the build.
if: steps.validate.outputs.exit_code == '0'
uses: ./.github/actions/publish-with-retry
with:
publish-paths: |
docs/${{ steps.connection-check.outputs.rosver }}/routeros-app-yaml-schema.json
docs/${{ steps.connection-check.outputs.rosver }}/routeros-app-yaml-store-schema.json
commit-message: Publish ${{ steps.connection-check.outputs.rosver }} /app YAML schemas [${{ github.workflow }}]
- name: Save validation artifacts
uses: actions/upload-artifact@v7
if: always()
with:
name: app-yaml-schema-results-${{ github.event.inputs.rosver }}
path: |
docs/${{ steps.connection-check.outputs.rosver }}/routeros-app-yaml-schema.json
docs/${{ steps.connection-check.outputs.rosver }}/routeros-app-yaml-store-schema.json
docs/${{ steps.connection-check.outputs.rosver }}/app.json
- name: Fail build if validation did not pass completely
# Fail the workflow if validation failed (exit code 2 — live validation failures).
# Exit code 1 (meta-validation failed) already failed the workflow earlier.
# This ensures the workflow shows as failed in the UI and the GitHub issue
# created above prompts schema updates.
if: steps.validate.outputs.exit_code == '2'
run: |
echo "::error::/app YAML validation failed — some built-in RouterOS apps don't conform to the schema"
echo "::error::Schemas were NOT committed (waiting for full validation pass)"
echo "::error::app.json WAS committed for debugging"
echo "::error::See the GitHub issue created above for details on which /app entries failed"
exit 1
- name: Cleanup QEMU
if: always()
run: |
if [ -f /tmp/qemu.pid ]; then
kill "$(cat /tmp/qemu.pid)" || true
echo "QEMU process terminated."
fi