Add Jenkins CI workflow for Renode unit tests #12
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
| # Triggers the Jenkins Renode unit-test job and waits for its result. | |
| # The GH Action's pass/fail mirrors the Jenkins build, so the check on | |
| # the PR is the commit status -- no separate Jenkins -> GitHub callback | |
| # is required. | |
| # | |
| # Required repository secrets: | |
| # JENKINS_URL Jenkins instance base URL (no trailing slash) | |
| # JENKINS_WEBHOOK_TOKEN Generic Webhook Trigger token on the Jenkins job | |
| # JENKINS_USER Jenkins service-account username (for API polling) | |
| # JENKINS_API_TOKEN Jenkins API token for that user | |
| # CF_ACCESS_CLIENT_ID Cloudflare Access service token client ID | |
| # CF_ACCESS_CLIENT_SECRET Cloudflare Access service token client secret | |
| name: Trigger Jenkins Renode Unit Tests | |
| on: | |
| push: | |
| branches: | |
| - main | |
| pull_request: | |
| branches: | |
| - main | |
| workflow_dispatch: | |
| inputs: | |
| adamant_xmera_components_ref: | |
| description: "adamant-xmera-components ref (default: PR head SHA / push SHA)" | |
| required: false | |
| type: string | |
| adamant_ref: | |
| description: "adamant ref (default: Jenkins job's hardcoded fallback)" | |
| required: false | |
| type: string | |
| adamant_example_ref: | |
| description: "adamant_example ref (default: Jenkins job's hardcoded fallback)" | |
| required: false | |
| type: string | |
| fp32_fsw_xmera_ref: | |
| description: "fp32-fsw-xmera ref (default: Jenkins job's hardcoded fallback)" | |
| required: false | |
| type: string | |
| project_ref: | |
| description: "downstream project repo ref (default: Jenkins job's hardcoded fallback)" | |
| required: false | |
| type: string | |
| jobs: | |
| trigger_job: | |
| name: trigger_jenkins_renode | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 120 | |
| env: | |
| JENKINS_URL: ${{ secrets.JENKINS_URL }} | |
| CF_ACCESS_CLIENT_ID: ${{ secrets.CF_ACCESS_CLIENT_ID }} | |
| CF_ACCESS_CLIENT_SECRET: ${{ secrets.CF_ACCESS_CLIENT_SECRET }} | |
| # === Per-CI-run pins ============================================ | |
| # Uncomment a line below to pin a sibling repo's ref for THIS | |
| # branch's CI runs (e.g. one-off DROP / integration testing without | |
| # touching Jenkins UI). workflow_dispatch inputs still win over a | |
| # PIN; revert (or comment back) before merging. | |
| # ADAMANT_REF_PIN: 'feature/your-branch-here' | |
| # ADAMANT_EXAMPLE_REF_PIN: 'feature/your-branch-here' | |
| # FP32_FSW_XMERA_REF_PIN: 'feature/your-branch-here' | |
| # PROJECT_REF_PIN: 'feature/your-branch-here' | |
| steps: | |
| - run: echo "Starting job triggered by a ${{ github.event_name }} event on a ${{ runner.os }} server hosted by GitHub." | |
| - name: Resolve commit SHA | |
| id: sha | |
| run: | | |
| set -euo pipefail | |
| SHA="${{ github.event.pull_request.head.sha || github.sha }}" | |
| if [ -z "$SHA" ]; then | |
| echo "Could not resolve a commit SHA from this event." | |
| exit 1 | |
| fi | |
| echo "Resolved SHA: $SHA" | |
| echo "sha=$SHA" >> "$GITHUB_OUTPUT" | |
| - name: Trigger Jenkins generic-webhook | |
| id: trigger | |
| env: | |
| JENKINS_WEBHOOK_TOKEN: ${{ secrets.JENKINS_WEBHOOK_TOKEN }} | |
| COMMIT_SHA: ${{ steps.sha.outputs.sha }} | |
| # AXC has no PIN: PR builds must always test the PR head SHA. | |
| AXC_REF_OVERRIDE: ${{ inputs.adamant_xmera_components_ref || '' }} | |
| ADAMANT_REF_OVERRIDE: ${{ inputs.adamant_ref || env.ADAMANT_REF_PIN || '' }} | |
| ADAMANT_EXAMPLE_REF_OVERRIDE: ${{ inputs.adamant_example_ref || env.ADAMANT_EXAMPLE_REF_PIN || '' }} | |
| FP32_FSW_XMERA_REF_OVERRIDE: ${{ inputs.fp32_fsw_xmera_ref || env.FP32_FSW_XMERA_REF_PIN || '' }} | |
| PROJECT_REF_OVERRIDE: ${{ inputs.project_ref || env.PROJECT_REF_PIN || '' }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${JENKINS_URL:-}" ]; then | |
| echo "JENKINS_URL secret is not set on this repository." | |
| exit 1 | |
| fi | |
| if [ -z "${JENKINS_WEBHOOK_TOKEN:-}" ]; then | |
| echo "JENKINS_WEBHOOK_TOKEN secret is not set on this repository." | |
| exit 1 | |
| fi | |
| # adamant-xmera-components ref defaults to the resolved PR/push SHA; | |
| # workflow_dispatch can override it explicitly. | |
| AXC_REF="${AXC_REF_OVERRIDE:-$COMMIT_SHA}" | |
| # Build the payload, omitting any optional override that wasn't set. | |
| BODY=$(jq -n \ | |
| --arg axc "$AXC_REF" \ | |
| --arg adamant "$ADAMANT_REF_OVERRIDE" \ | |
| --arg ax_ex "$ADAMANT_EXAMPLE_REF_OVERRIDE" \ | |
| --arg fp32 "$FP32_FSW_XMERA_REF_OVERRIDE" \ | |
| --arg project "$PROJECT_REF_OVERRIDE" \ | |
| '{adamant_xmera_components_ref: $axc} | |
| + (if $adamant != "" then {adamant_ref: $adamant} else {} end) | |
| + (if $ax_ex != "" then {adamant_example_ref: $ax_ex} else {} end) | |
| + (if $fp32 != "" then {fp32_fsw_xmera_ref: $fp32} else {} end) | |
| + (if $project != "" then {project_ref: $project} else {} end)') | |
| TRIGGER_URL="${JENKINS_URL}/generic-webhook-trigger/invoke" | |
| echo "POST ${TRIGGER_URL}?token=***" | |
| echo "Body: ${BODY}" | |
| RESPONSE=$(curl --max-time 120 \ | |
| --retry 5 --retry-delay 5 --retry-max-time 630 \ | |
| -sS -w '\n__HTTP_STATUS__:%{http_code}' \ | |
| -X POST \ | |
| -H "Content-Type: application/json" \ | |
| -H "CF-Access-Client-Id: ${CF_ACCESS_CLIENT_ID}" \ | |
| -H "CF-Access-Client-Secret: ${CF_ACCESS_CLIENT_SECRET}" \ | |
| --data-binary "${BODY}" \ | |
| "${TRIGGER_URL}?token=${JENKINS_WEBHOOK_TOKEN}") | |
| HTTP_CODE="${RESPONSE##*__HTTP_STATUS__:}" | |
| RESP_BODY="${RESPONSE%__HTTP_STATUS__:*}" | |
| RESP_BODY="${RESP_BODY%$'\n'}" | |
| echo "HTTP ${HTTP_CODE}" | |
| echo "${RESP_BODY}" | |
| if [ "${HTTP_CODE}" != "200" ]; then | |
| echo "Jenkins returned non-200 status." | |
| exit 1 | |
| fi | |
| QUEUE_PATH=$(echo "${RESP_BODY}" | jq -r '[.jobs | to_entries[] | select(.value.triggered == true)][0].value.url // empty') | |
| JOB_NAME=$(echo "${RESP_BODY}" | jq -r '[.jobs | to_entries[] | select(.value.triggered == true)][0].key // empty') | |
| if [ -z "${QUEUE_PATH}" ]; then | |
| echo "Jenkins accepted the request but no jobs reported triggered=true." | |
| echo " Likely a regexpFilter mismatch in the Generic Webhook Trigger config." | |
| exit 1 | |
| fi | |
| QUEUE_URL="${JENKINS_URL}/${QUEUE_PATH}" | |
| echo "Triggered: ${JOB_NAME}" | |
| echo "Queue: ${QUEUE_URL}" | |
| echo "queue_url=${QUEUE_URL}" >> "$GITHUB_OUTPUT" | |
| echo "job_name=${JOB_NAME}" >> "$GITHUB_OUTPUT" | |
| { | |
| echo "### Jenkins job triggered" | |
| echo "" | |
| echo "- **Job:** \`${JOB_NAME}\`" | |
| echo "- **Commit:** \`${COMMIT_SHA}\`" | |
| echo "- **Queue:** ${QUEUE_URL}" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Wait for Jenkins build to start | |
| id: wait_start | |
| env: | |
| JENKINS_USER: ${{ secrets.JENKINS_USER }} | |
| JENKINS_API_TOKEN: ${{ secrets.JENKINS_API_TOKEN }} | |
| QUEUE_URL: ${{ steps.trigger.outputs.queue_url }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${JENKINS_USER:-}" ] || [ -z "${JENKINS_API_TOKEN:-}" ]; then | |
| echo "JENKINS_USER and JENKINS_API_TOKEN secrets must be set to poll build status." | |
| exit 1 | |
| fi | |
| if [ -z "${CF_ACCESS_CLIENT_ID:-}" ] || [ -z "${CF_ACCESS_CLIENT_SECRET:-}" ]; then | |
| echo "CF_ACCESS_CLIENT_ID and CF_ACCESS_CLIENT_SECRET must be set to reach Jenkins API through Cloudflare Access." | |
| exit 1 | |
| fi | |
| echo "Polling queue: ${QUEUE_URL}" | |
| # Wait up to 45 minutes for an executor to pick up the build (540 * 5s). | |
| # Renode builds can't run in parallel on this Jenkins, so concurrent | |
| # PR triggers from this and sibling repos serialize through the queue. | |
| for i in $(seq 1 540); do | |
| Q=$(curl -sS --max-time 30 \ | |
| -H "CF-Access-Client-Id: ${CF_ACCESS_CLIENT_ID}" \ | |
| -H "CF-Access-Client-Secret: ${CF_ACCESS_CLIENT_SECRET}" \ | |
| --user "${JENKINS_USER}:${JENKINS_API_TOKEN}" \ | |
| "${QUEUE_URL}api/json") | |
| CANCELLED=$(echo "$Q" | jq -r '.cancelled // false') | |
| if [ "$CANCELLED" = "true" ]; then | |
| echo "Build was cancelled while in queue." | |
| exit 1 | |
| fi | |
| EXEC_URL=$(echo "$Q" | jq -r '.executable.url // empty') | |
| if [ -n "$EXEC_URL" ]; then | |
| # Jenkins advertises itself with its internal address; rewrite to the | |
| # Cloudflare-fronted host so the runner can actually reach it. | |
| EXEC_PATH="${EXEC_URL#http*://*/}" | |
| BUILD_URL="${JENKINS_URL}/${EXEC_PATH}" | |
| echo "Build started: ${BUILD_URL}" | |
| echo "build_url=${BUILD_URL}" >> "$GITHUB_OUTPUT" | |
| { | |
| echo "" | |
| echo "### Build started" | |
| echo "${BUILD_URL}" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| exit 0 | |
| fi | |
| REASON=$(echo "$Q" | jq -r '.why // "queued"') | |
| echo "Waiting for executor ($i/540): ${REASON}" | |
| sleep 5 | |
| done | |
| echo "Build never started within 45 minutes." | |
| exit 1 | |
| - name: Wait for Jenkins build to finish | |
| env: | |
| JENKINS_USER: ${{ secrets.JENKINS_USER }} | |
| JENKINS_API_TOKEN: ${{ secrets.JENKINS_API_TOKEN }} | |
| BUILD_URL: ${{ steps.wait_start.outputs.build_url }} | |
| run: | | |
| set -euo pipefail | |
| # Poll the build's result field. Cap total wait at 60 minutes (120 * 30s). | |
| for i in $(seq 1 120); do | |
| B=$(curl -sS --max-time 30 \ | |
| -H "CF-Access-Client-Id: ${CF_ACCESS_CLIENT_ID}" \ | |
| -H "CF-Access-Client-Secret: ${CF_ACCESS_CLIENT_SECRET}" \ | |
| --user "${JENKINS_USER}:${JENKINS_API_TOKEN}" \ | |
| "${BUILD_URL}api/json") | |
| RESULT=$(echo "$B" | jq -r '.result // "null"') | |
| if [ "$RESULT" != "null" ]; then | |
| echo "Build finished: ${RESULT}" | |
| { | |
| echo "" | |
| echo "### Build finished: ${RESULT}" | |
| echo "${BUILD_URL}" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| case "$RESULT" in | |
| SUCCESS) | |
| exit 0 | |
| ;; | |
| *) | |
| echo "--- Tail of Jenkins console log ---" | |
| curl -sS --max-time 60 \ | |
| -H "CF-Access-Client-Id: ${CF_ACCESS_CLIENT_ID}" \ | |
| -H "CF-Access-Client-Secret: ${CF_ACCESS_CLIENT_SECRET}" \ | |
| --user "${JENKINS_USER}:${JENKINS_API_TOKEN}" \ | |
| "${BUILD_URL}consoleText" | tail -200 || true | |
| exit 1 | |
| ;; | |
| esac | |
| fi | |
| echo "Building ($i/120)..." | |
| sleep 30 | |
| done | |
| echo "Build did not finish within 60 minutes." | |
| exit 1 | |
| - run: echo "Finished with status - ${{ job.status }}." | |
| if: always() |