Build: /app YAML Schemas #52
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: /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 |