Skip to content

ci: dispatch external CI to the external repositories (led-blinker pilot) - #6328

Open
thomas-bc-autowot wants to merge 22 commits into
nasa:develfrom
JPL-Devin:devin/1793833143-ext-ci-led-blinker
Open

thomas-bc-autowot wants to merge 22 commits into
nasa:develfrom
JPL-Devin:devin/1793833143-ext-ci-led-blinker

Conversation

@thomas-bc-autowot

@thomas-bc-autowot thomas-bc-autowot commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator
Related Issue(s) external CI restructure
Has Unit Tests (y/n) n
Documentation Included (y/n) n
Generative AI was used in this contribution (y/n) AI

Change Description

Dispatch an external project's own ci.yml and report the result on the F´ commit, instead of building the project inside nasa/fprime.

  • ext-ci-trigger.yml: trivial pull_request / merge_group workflow with permissions: {}, so GitHub's fork-approval gate applies before anything privileged runs.
  • ext-ci.yml: runs on workflow_run of the trigger, nightly, on release/** pushes and manual dispatch, always from the default branch (never PR code). One Check Run per external repo on the PR head SHA; dispatches the external ci.yml with {fprime_repository, fprime_ref, fprime_pr_number, dispatch_id}, polls, finalizes (closes open checks, cancels orphaned remote runs). A re-run re-dispatches every repo.
  • .github/actions/ext-ci/orchestrator.py (documented in .github/actions/ext-ci/README.md): standard-library-only Python script doing the above.
  • .github/actions/ext-ci/manifest.json: pilot with fprime-community/fprime-workshop-led-blinker only, failure_policy: notify (a failure shows as a neutral check and does not block). ext-build-led-blinker.yml stays until the new check has run in parallel for a while.

Credentials: one GitHub App ("External CI", registered under fprime-community: Actions rw, Checks rw, Contents ro, Pull requests ro, no webhook) installed on the external repos and on nasa/fprime; repository secrets EXT_CI_APP_ID + EXT_CI_APP_PRIVATE_KEY. The workflow mints two 1-hour installation tokens: one scoped to the external repos (dispatch/cancel), one to nasa/fprime (Check Runs, so they get their own "External CI" group in the PR checks list instead of being filed under another workflow, which is what GITHUB_TOKEN-created checks get). Until the nasa/fprime installation is approved the checks fall back to GITHUB_TOKEN. Dispatched runs have contents: read, no secrets, ccache off; the action-side pip-cache save is closed by nasa/fprime-actions cache-mode: read (separate PR) — see finding 1 below for what that does not cover.

Operation:

  • External ref: the orchestrator uses the external repo's pr-<N> branch when it exists, else its default branch (shown in the check title, e.g. success (… @ devel)); the external ci.yml checks itself out from that ref and builds against the F´ revision it is given.
  • Re-run: re-run the orchestrator run ("External CI", from Actions or via the check's Details → Orchestrator run). Attempt ≥ 2 re-dispatches only the repos whose check on that F´ commit is not green; a new push is always a full run. Re-running the trigger starts a fresh full orchestrator run. App-created checks have no "Re-run" button on the PR row (GitHub offers it for Actions checks only).
  • Display: own check suite per App, so the Checks tab shows <App name> / External CI / led-blinker and the merge box the App avatar + External CI / led-blinker; branch protection matches on the check name.

Rationale

Each ext-*.yml re-implements an external build on nasa/fprime runners and every external breakage is fixed in many workflows; this leaves nasa/fprime with dispatch/poll/report and one manifest entry per project.

Testing/Review Recommendations

Design, evidence and security review: https://github.com/JPL-Devin/fprime-test-ci-fprime/blob/main/docs/ext-ci-dispatch-plan.md. Review focus: ext-ci.yml holds the write permissions and the secret but checks out only the default branch; no pull_request_target. Proven end to end from the JPL-Devin fork: orchestrator run https://github.com/JPL-Devin/fprime/actions/runs/41948640203 dispatched https://github.com/fprime-community/fprime-workshop-led-blinker/actions/runs/41948655128 and closed the External CI / led-blinker check with its result. After merge: a PR touching a non-ignored path should get that check.

Review findings and resolutions (multi-agent review at 6052882)

Full summary review: JPL-Devin#297 (review) (8 lenses, 2 routed out as not applicable; CI safety No-Go / merge readiness No-Go as posted, with the dispositions below).

Boundary verified clean by every lens that traced it: the orchestrator never runs PR code; none of the tokens (GITHUB_TOKEN, App installation tokens) is reachable from the PR; manifest and script are read from the default/release branch; only GitHub-authored workflow_run payload fields are trusted; PR resolution is filtered on base.repo; no pull_request_target; Check Runs can only be written by the orchestrator; concurrency keys are owner-qualified; finalize closes checks and cancels remote runs under cancellation; the dispatched led-blinker workflow has contents: read and no secrets. The context this review assumed: nasa/fprime requires approval for workflow runs from all outside collaborators, and the external repositories hold no secrets.

# Finding (lens, thread) Disposition
1 Security, must fix — PR builds are dispatched onto the external repo's default branch, so the untrusted F´ code gets default-branch Actions-cache write scope (pip cache, ccache); a poisoned entry is restored by the external repo's own CI and by later dispatched runs. Independent of ccache: false / cache-mode: read, which only stop the action's own saves. (JPL-Devin#297 (comment)) Accepted for the led-blinker pilot (hosted runners, approval-gated fork runs, no secrets, example-repo tier: impact is CI integrity of that repo, recoverable by clearing caches). Mitigation before adding repos: dispatched runs neither restore nor save caches (external side), and/or a dedicated non-default branch per external repo as dispatch target. Hard precondition for hardware repos (see below).
2 Design, must fix — design-needs-human-adjudication: a new privileged mechanism (long-lived PAT in a repository secret, external dispatch). (JPL-Devin#297 (comment)) Resolved: GitHub App with short-lived installation tokens from the start (no PAT); each installation limited to selected repositories.
3 Operational, must fix — failure_policy: notify also silences orchestrator-side faults and nightly/release runs: the job exits 0, nobody is alerted. (JPL-Devin#297 (comment)) Planned fix: notify only softens the PR check; dispatch/poll errors and non-PR runs (nightly, release, merge queue) fail the job.
4 Operational + Correctness, promoted to must fix — a lost dispatch response marks the repo dispatch_failed while the remote run proceeds untracked (not polled, not cancelled). (JPL-Devin#297 (comment)) Planned fix: on a dispatch error after the POST, look the run up by its dispatch_id run-name before giving up.
5 Supply chain, suggestions (3) — DISPATCH_TOKEN is job-level env (also visible to actions/checkout / upload-artifact); fork-controlled head_branch written raw to the step summary; on_pr: false bypassed by a manual run naming the repo plus pr_number. Planned in the next push: step-scope the secret, route head_branch through code(), honour on_pr on manual runs.
6 Correctness / Operational / Design / Maintainability / Documentation, below must-fix (≈14) — suggestions and could-fix items on error paths, naming, duplication, docs (CONTRIBUTING.md check description, manifest/ci.yml contract in the script header). Triaged individually on their threads; docstrings and type hints for every function of the orchestrator are in the next push, together with the readability items the maintainer selects.

Rule for external repositories with self-hosted / hardware runners

Compared with the current ext-*.yml model, a dispatched run executes in the external repository's trusted context (default branch, workflow_dispatch) instead of a fork pull_request context: GitHub no longer strips secrets or forces a read-only token, default-branch caches are writable (persistence that survives runner re-imaging), and the App's dispatch token can start runs on those runners without an approval click. Before any such repository is added to the manifest:

  • PR-head code never reaches hardware: on_pr: false for that entry (hardware tests devel nightly, release/** and the merge queue only), or at most same-repo heads via a job-level if: inputs.fprime_repository == 'nasa/fprime' in the external workflow.
  • External workflow: permissions: {} (or contents: read), no ${{ secrets.* }}, no cache restore or save on dispatched runs, ephemeral/JIT runners, SHA-pinned actions, CODEOWNERS on .github/workflows/ including nasa/fprime maintainers.
  • Orchestrator: a manifest flag for self-hosted entries that refuses fork heads even on manual runs.
  • Runners move with the workflow: hardware runners are registered on the external repo (or fprime-community org), label-restricted, no credentials on the host beyond the registration token, "require approval for fork PRs" enabled there too.
  • Second entry point: anyone with write on the external repo can workflow_dispatch its ci.yml with any fprime_ref, outside nasa/fprime's approval gate — the external workflow's own restrictions above are what bound that path, not the orchestrator.
  • Residual, in both the old and the new model: a persistent hardware runner executing approved external code is a trust-the-reviewer model; the new model additionally removes GitHub's automatic fork hardening on that run, which is why on_pr: false is the recommendation.

Future Work

Add the other fprime-community repos as their ci.yml lands; retire the matching ext-*.yml.

Contributor Checklist

AI Usage (see policy)

Devin, under maintainer direction, wrote the workflows, script and manifest and ran the prototype and reviews.

IAMAI

…rator)

Adds ext-ci-trigger.yml (no-op pull_request gate), ext-ci.yml (workflow_run orchestrator:
dispatch, poll, one Check Run per external repo, failed-only re-run, notify/block policy)
and .github/ext-ci-manifest.json onboarding led-blinker, zephyr-reference and
generic-hub-reference in the notify tier. Existing ext-*.yml workflows are untouched and
keep running alongside.
Same behaviour and workflows; folds in review findings: no automatic retry of the
non-idempotent dispatch call (a lost response falls back to dispatch_id correlation),
a failed check-status update is not treated as a failed dispatch, and a check whose
completion update failed is closed by the finalize step with the remote result.
…nifest source, compare 404, concurrency key)

- correlate/poll_all tolerate carried-forward entries (no remote_* keys) on a re-run
- a manual run with fprime_ref reads the manifest and ext ref from the branch the run was
  started from, not from the ref under test (any PR SHA is readable via /contents?ref=)
- a release push whose before SHA was rewritten runs everything instead of aborting
- concurrency group includes the event name so a PR branch named release/* cannot cancel the push run
@thomas-bc thomas-bc closed this Nov 5, 2026
Drop owner notifications (a notify repo is just a neutral check; no pull-requests/issues
write needed any more), failed-only re-run carry-forward (a re-run re-dispatches every repo),
the dispatch_id correlation fallback (the dispatch API returns the run id), manifest lookup
from the base branch and ETag polling.
Comment thread .github/workflows/ext-ci.yml Resolved
Comment thread .github/workflows/ext-ci.yml Resolved
Comment thread .github/actions/ext-ci/manifest.json Resolved
@github-actions

This comment was marked as off-topic.

Comment thread .github/workflows/ext-ci.yml Resolved
…) instead of a PAT

Check Runs created with GITHUB_TOKEN are filed into an arbitrary existing Actions
check suite on the PR head, so the PR shows them as '<other workflow> / ext-ci /
led-blinker'. Creating them with the External CI App's own installation token gives
them their own suite. The same App's fprime-community installation replaces
EXT_CI_DISPATCH_TOKEN; secrets are EXT_CI_APP_ID and EXT_CI_APP_PRIVATE_KEY. Falls
back to GITHUB_TOKEN for the checks while the App is not installed on this repo.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants