Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
248 changes: 248 additions & 0 deletions .github/workflows/ext-fprime-stm32-baremetal-reference.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
# Dispatch the HIL/cross-compile workflow for
# https://github.com/fprime-community/fprime-stm32-baremetal-reference (STM32H753XI-EVAL2,
# ARM Cortex-M7, Os::Baremetal single-threaded cyclic executive). The self-hosted Pi runner
# and the actual build/flash/test steps live in that repo's own `hil.yml` - this workflow only
# fires workflow_dispatch there with the exact F´ revision under test, polls the remote run,
# and reports the result back as a Check Run on this commit/PR.

name: "External Repo: STM32 Baremetal Reference"

on:
schedule:
- cron: "0 21 * * *" # Nightly at 2:00 PM US/Pacific (PDT)
push:
branches: [ release/** ]
pull_request:
branches: [ devel, release/** ]
paths-ignore:
- 'docs/**'
- '**.md'
- '.github/actions/spelling/**'
- '.github/ISSUE_TEMPLATE/**'
- '.github/agents/**'
workflow_dispatch:
inputs:
fprime_ref:
description: "F´ ref/SHA to dispatch for build+test (default: this run's own ref)"
type: string
required: false

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ !contains(github.ref, 'devel') && !contains(github.ref, 'release/') }}

permissions:
contents: read
checks: write

env:
TARGET_REPOSITORY: fprime-community/fprime-stm32-baremetal-reference
TARGET_WORKFLOW: hil.yml

jobs:
dispatch-hil:
name: "Dispatch HIL run on fprime-community/fprime-stm32-baremetal-reference"
runs-on: ubuntu-22.04
# Generous enough to cover the remote cross-compile + HIL job (today's remote
# HIL-integration alone carries a 30-minute timeout) plus poll/correlate overhead.
timeout-minutes: 90
# Fork PRs are never dispatched: a fork's head is untrusted code, and the real trust
# gate against it lives on the receiving side (the target repo's `hil-hardware`
# environment requires human approval before a dispatched ref reaches the board) -
# there is still no reason to spend a remote run on an untrusted PR head at all.
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
env:
DISPATCH_TOKEN: ${{ secrets.HIL_DISPATCH_TOKEN }}
steps:
- name: "Resolve F´ revision and PR number to dispatch"
id: target
env:
INPUT_FPRIME_REF: ${{ inputs.fprime_ref }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then
echo "sha=${PR_HEAD_SHA}" >> "${GITHUB_OUTPUT}"
echo "pr_number=${PR_NUMBER}" >> "${GITHUB_OUTPUT}"
else
echo "sha=${INPUT_FPRIME_REF:-${GITHUB_SHA}}" >> "${GITHUB_OUTPUT}"
echo "pr_number=" >> "${GITHUB_OUTPUT}"
fi
shell: bash
- name: "Dispatch and create the hil-tests Check Run"
id: dispatch
uses: actions/github-script@v7
env:
TARGET_REPOSITORY: ${{ env.TARGET_REPOSITORY }}
TARGET_WORKFLOW: ${{ env.TARGET_WORKFLOW }}
FPRIME_SHA: ${{ steps.target.outputs.sha }}
FPRIME_PR_NUMBER: ${{ steps.target.outputs.pr_number }}
with:
script: |
// actions/github-script's bundled github-token only has permissions on this repo;
// dispatching into the external repo needs its own token (actions:write there).
async function extApi(method, path, body) {
const res = await fetch(`https://api.github.com${path}`, {
method,
headers: {
authorization: `Bearer ${process.env.DISPATCH_TOKEN}`,
accept: 'application/vnd.github+json',
'content-type': 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
});
const text = await res.text();
if (!res.ok) throw new Error(`${method} ${path} -> ${res.status} ${text.slice(0, 300)}`);
return text ? JSON.parse(text) : null;
}

const [owner, repo] = process.env.TARGET_REPOSITORY.split('/');
const fprimeSha = process.env.FPRIME_SHA;
const dispatchId = `${context.runId}-${context.runNumber}`;
const orchestratorUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;

const check = await github.rest.checks.create({
owner: context.repo.owner,
repo: context.repo.repo,
name: 'hil-tests',
head_sha: fprimeSha,
status: 'queued',
details_url: orchestratorUrl,
output: {
title: `Dispatching ${process.env.TARGET_REPOSITORY}`,
summary: `Dispatching \`${process.env.TARGET_WORKFLOW}\` on \`${process.env.TARGET_REPOSITORY}\` with F´ \`${context.repo.owner}/${context.repo.repo}@${fprimeSha.slice(0, 7)}\`.\n\nOrchestrator: ${orchestratorUrl}`,
},
});
core.setOutput('check_run_id', check.data.id);
core.setOutput('dispatch_id', dispatchId);

await extApi('POST', `/repos/${owner}/${repo}/actions/workflows/${process.env.TARGET_WORKFLOW}/dispatches`, {
ref: 'devel',
inputs: {
fprime_repository: `${context.repo.owner}/${context.repo.repo}`,
fprime_ref: fprimeSha,
fprime_pr_number: process.env.FPRIME_PR_NUMBER || '',
dispatch_id: dispatchId,
},
});
- name: "Poll the remote run and report its result"
uses: actions/github-script@v7
env:
TARGET_REPOSITORY: ${{ env.TARGET_REPOSITORY }}
CHECK_RUN_ID: ${{ steps.dispatch.outputs.check_run_id }}
DISPATCH_ID: ${{ steps.dispatch.outputs.dispatch_id }}
with:
script: |
async function extApi(method, path, body) {
const res = await fetch(`https://api.github.com${path}`, {
method,
headers: {
authorization: `Bearer ${process.env.DISPATCH_TOKEN}`,
accept: 'application/vnd.github+json',
'content-type': 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
});
const text = await res.text();
if (!res.ok) throw new Error(`${method} ${path} -> ${res.status} ${text.slice(0, 300)}`);
return text ? JSON.parse(text) : null;
}

const [owner, repo] = process.env.TARGET_REPOSITORY.split('/');
const checkRunId = Number(process.env.CHECK_RUN_ID);
const dispatchId = process.env.DISPATCH_ID;
const orchestratorUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

// Dispatch doesn't return a run id, so correlate by matching the dispatch_id this
// run stamped into the dispatched workflow's run-name.
let remoteRun = null;
const correlateDeadline = Date.now() + 5 * 60 * 1000;
while (!remoteRun && Date.now() < correlateDeadline) {
const since = new Date(Date.now() - 10 * 60 * 1000).toISOString();
const runs = await extApi('GET', `/repos/${owner}/${repo}/actions/runs?event=workflow_dispatch&created=%3E%3D${since}&per_page=20`);
remoteRun = runs.workflow_runs.find((r) => (r.display_title || r.name || '').includes(dispatchId));
if (!remoteRun) await sleep(15_000);
}

if (!remoteRun) {
await github.rest.checks.update({
owner: context.repo.owner,
repo: context.repo.repo,
check_run_id: checkRunId,
status: 'completed',
conclusion: 'failure',
output: {
title: 'Dispatched run never appeared',
summary: `No run on \`${process.env.TARGET_REPOSITORY}\` matched dispatch id \`${dispatchId}\` within 5 minutes.\n\nOrchestrator: ${orchestratorUrl}`,
},
});
core.setFailed(`no remote run found for dispatch_id ${dispatchId}`);
return;
}

await github.rest.checks.update({
owner: context.repo.owner,
repo: context.repo.repo,
check_run_id: checkRunId,
status: 'in_progress',
details_url: remoteRun.html_url,
output: {
title: `Running on ${process.env.TARGET_REPOSITORY}`,
summary: `Remote run: ${remoteRun.html_url}\n\nOrchestrator: ${orchestratorUrl}`,
},
});

let conclusion = null;
while (!conclusion) {
await sleep(60_000);
const run = await extApi('GET', `/repos/${owner}/${repo}/actions/runs/${remoteRun.id}`);
if (run.status === 'completed') conclusion = run.conclusion;
}

await github.rest.checks.update({
owner: context.repo.owner,
repo: context.repo.repo,
check_run_id: checkRunId,
status: 'completed',
conclusion,
details_url: remoteRun.html_url,
output: {
title: `${conclusion} on ${process.env.TARGET_REPOSITORY}`,
summary: `Remote run: ${remoteRun.html_url}\n\nOrchestrator: ${orchestratorUrl}`,
},
});

if (conclusion !== 'success') core.setFailed(`HIL run concluded: ${conclusion}`);
- name: "Finalize: close the Check Run if this job was cancelled or crashed"
if: always()
uses: actions/github-script@v7
env:
TARGET_REPOSITORY: ${{ env.TARGET_REPOSITORY }}
CHECK_RUN_ID: ${{ steps.dispatch.outputs.check_run_id }}
JOB_STATUS: ${{ job.status }}
with:
script: |
const checkRunIdStr = process.env.CHECK_RUN_ID;
if (!checkRunIdStr) return; // dispatch step never created one
const checkRunId = Number(checkRunIdStr);
const current = await github.rest.checks.get({
owner: context.repo.owner,
repo: context.repo.repo,
check_run_id: checkRunId,
});
if (current.data.status === 'completed') return; // already reported by the poll step
const cancelled = process.env.JOB_STATUS === 'cancelled';
await github.rest.checks.update({
owner: context.repo.owner,
repo: context.repo.repo,
check_run_id: checkRunId,
status: 'completed',
conclusion: cancelled ? 'cancelled' : 'failure',
output: {
title: cancelled ? 'Cancelled' : 'Dispatcher error',
summary: cancelled
? 'The dispatcher run was cancelled before the remote HIL run finished.'
: 'The dispatcher run ended before the remote HIL run was reported (dispatcher error or timeout).',
},
});
Loading