Skip to content

Add Jenkins CI workflow for Renode unit tests #12

Add Jenkins CI workflow for Renode unit tests

Add Jenkins CI workflow for Renode unit tests #12

# 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()