Skip to content

Build: Extra-Packages Schema #200

Build: Extra-Packages Schema

Build: Extra-Packages Schema #200

name: "Build: Extra-Packages Schema"
on:
workflow_dispatch:
inputs:
rosver:
description: 'RouterOS Version (e.g. 7.22, 7.22rc2, 7.22beta4)'
required: true
default: "7.20"
start_path:
description: 'Starting path (space separated, leave empty for full schema)'
required: false
permissions:
contents: write
jobs:
job-using-extra-docker-in-docker:
runs-on: ubuntu-latest
env:
URLBASE: http://localhost:9180/rest
BASICAUTH: "admin:"
MIKROTIK_ACCOUNT: ${{ secrets.MIKROTIK_ACCOUNT }}
MIKROTIK_PASSWORD: ${{ secrets.MIKROTIK_PASSWORD }}
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.
# MODE=0666 is acceptable here because GitHub runners are ephemeral and single-tenant.
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
# qcow2 is the native QEMU format and works well with virtio drivers supported by CHR.
# Converting from VDI here avoids any VirtualBox-format quirks with QEMU.
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
# Run QEMU directly on the runner using user-mode networking with port forwarding.
# host:9180 → VM:80 (REST API), host:9122 → VM:22 (SSH for extra-packages SCP)
# Uses virtio disk (qcow2) and virtio-net — both supported by MikroTik CHR on x86.
# KVM is used when available; falls back to software emulation automatically.
# Extra-package jobs use 1024 MB to match the deep-inspect memory guardrail.
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
# CHR typically boots in 1-3 minutes under KVM. Allow up to 5 minutes before failing.
# -sS: silent but show errors; -m 5: 5-second connect timeout per attempt.
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: Activate CHR trial license
# CHR "free" license caps networking at 1 Mbit/s, which throttles the ~34k
# /console/inspect API calls during enrichment. Trial (p1) allows 1 Gbit/s.
if: env.MIKROTIK_ACCOUNT != ''
run: |
curl -sS -X POST http://admin:@localhost:9180/rest/system/license/renew \
-H 'Content-Type: application/json' \
-d "{\"account\":\"${{ secrets.MIKROTIK_ACCOUNT }}\",\"password\":\"${{ secrets.MIKROTIK_PASSWORD }}\",\"level\":\"p1\"}" \
&& echo "License activated to p1 (trial)" \
|| echo "::warning::License activation failed — enrichment will run at 1 Mbit/s (free)"
# Verify
curl -sS -X POST http://admin:@localhost:9180/rest/system/license/get \
-H 'Content-Type: application/json' \
-d '{"value-name":"level"}' | jq -r '.ret'
- name: Download and install extra packages into CHR
# Extra packages add features like zerotier, iot, container, etc.
# They are uploaded to the root of the CHR filesystem via SCP, then CHR is rebooted.
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
# Use REST API for reboot first; SSH is a fallback if REST is not responding.
# Note: the || true on SSH prevents failure when the connection drops mid-reboot.
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: Setup NodeJS
uses: actions/setup-node@v6
with:
node-version: 18
- name: Wait for RouterOS to come back up after extra-packages reboot
# After installing extra packages and rebooting, wait for CHR to restart.
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: Generate RAML schema from RouterOS REST API (with extra packages)
run: |
bun install
bun rest2raml.js ${{ github.event.inputs.start_path }}
- name: Validate RAML 1.0 using webapi-parser
run: |
npm install webapi-parser
node validraml.cjs ros-rest*.raml
- name: Generate deep-inspect.json and openapi.json
run: |
bun deep-inspect.ts \
--inspect-file ros-inspect-all.json \
--ros-version "${{ steps.connection-check.outputs.rosver }}" \
--output-dir . \
--test-crash-paths \
--transport rest
- name: Validate OpenAPI 3.0 schema
run: bun validate-openapi.ts openapi.json
- name: Enrich OpenAPI schema with documentation from rosetta
if: hashFiles('openapi.json') != ''
run: |
curl -fsSL -o ros-help.db.gz \
https://github.com/tikoci/rosetta/releases/latest/download/ros-help.db.gz
bun -e "
import { gunzipSync } from 'bun';
const gz = new Uint8Array(await Bun.file('ros-help.db.gz').arrayBuffer());
await Bun.write('ros-help.db', gunzipSync(gz));
"
bun enrich-openapi.ts --openapi openapi.json --db ros-help.db
rm -f ros-help.db ros-help.db.gz
- name: Validate enriched OpenAPI 3.0 schema
if: hashFiles('openapi.json') != ''
run: bun validate-openapi.ts openapi.json
- name: Run deep-inspect integration tests
run: bun test deep-inspect.integration.test.ts
- name: Publish schema files to /docs (extra subfolder)
id: publish-to-docs
run: |
ROSVER="${{ steps.connection-check.outputs.rosver }}"
ROS_FILTER=$(echo "${{ github.event.inputs.start_path }}" | tr ' ' '+')
DOCS_PATH="docs/${ROSVER}/extra/${ROS_FILTER}"
echo "Publishing to: $DOCS_PATH"
mkdir -p "$DOCS_PATH"
cp ros-rest*.raml "$DOCS_PATH/schema.raml"
cp ros-inspect*.json "$DOCS_PATH/inspect.json"
if [ -f deep-inspect.json ]; then cp deep-inspect.json "$DOCS_PATH/"; fi
if [ -f openapi.json ]; then cp openapi.json "$DOCS_PATH/"; fi
echo "subpath=$ROS_FILTER" >> $GITHUB_OUTPUT
- name: Commit and push schema to repository
uses: ./.github/actions/publish-with-retry
with:
publish-paths: |
docs/${{ steps.connection-check.outputs.rosver }}/
commit-message: Publish ${{ steps.connection-check.outputs.rosver }} extra/${{ steps.publish-to-docs.outputs.subpath }} [${{ github.workflow }}]
- name: Save build artifacts
uses: actions/upload-artifact@v7
with:
name: build-results
path: |
ros-rest*.raml
ros-inspect*.json
deep-inspect.json
openapi.json
index.html
- name: Cleanup QEMU
if: always()
run: |
if [ -f /tmp/qemu.pid ]; then
kill "$(cat /tmp/qemu.pid)" || true
echo "QEMU process terminated."
fi