Build: Extra-Packages Schema #200
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: "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 |