Skip to content
Open
Show file tree
Hide file tree
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
110 changes: 110 additions & 0 deletions .github/workflows/downstream-gate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Reverse-dependency gate: on every libneo PR, build and fast-test each
# downstream code against this PR's libneo and gate on green. Keeps libneo main
# always working with the downstreams, with breakage attributed to the PR that
# caused it.
#
# ci/downstreams is the single source of the gated set: one row per downstream.
# Add a downstream by appending a row there; nothing in this file changes. Each
# row's workflow is dispatched with `-f libneo_ref=<sha>` for the fast tier.
# Label the libneo PR `full-ci` to also run each downstream's slow suite,
# dispatched per that downstream's own convention (the row's `full` column, see
# ci/downstreams). Fast-only downstreams keep their slow suite on their own PRs.
#
# Needs RELEASE_BOT_TOKEN with actions:write on the downstreams (dispatch + read
# their runs). Same-repo PRs only: a fork's head SHA is not on origin, so the
# downstream's FetchContent could not resolve it.

name: downstream-gate

on:
pull_request:
types: [opened, synchronize, reopened, labeled]

permissions:
contents: read

concurrency:
group: downstream-gate-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
gate:
runs-on: ubuntu-24.04
# Skip drafts and forks. On a `labeled` event run only when the label is
# `full-ci`, so unrelated label changes do not re-dispatch the whole gate.
if: >-
github.event.pull_request.draft == false &&
github.event.pull_request.head.repo.full_name == github.repository &&
(github.event.action != 'labeled' || github.event.label.name == 'full-ci')
steps:
- uses: actions/checkout@v4

- name: Dispatch downstream CI against this PR's libneo and gate on green
env:
GH_TOKEN: ${{ secrets.RELEASE_BOT_TOKEN }}
REF: ${{ github.event.pull_request.head.sha }}
FULL: ${{ contains(github.event.pull_request.labels.*.name, 'full-ci') }}
run: |
set -u
fail=0

# Dispatch one workflow against this PR's libneo and wait for its run.
# Always passes -f libneo_ref=$REF; extra -f args are appended. Returns
# non-zero if dispatch, discovery, or the run itself fails.
run_wf() {
local repo=$1 wf=$2; shift 2
local before run_id
before=$(gh run list -R "itpplasma/$repo" --workflow "$wf" \
--event workflow_dispatch --limit 1 --json databaseId \
-q '.[0].databaseId // 0')
if ! gh workflow run "$wf" -R "itpplasma/$repo" -f "libneo_ref=$REF" "$@" 2>&1; then
echo "::error::failed to dispatch $repo ($wf) — workflow may lack workflow_dispatch trigger"
return 1
fi
run_id=0
for _ in $(seq 1 12); do
sleep 5
run_id=$(gh run list -R "itpplasma/$repo" --workflow "$wf" \
--event workflow_dispatch --limit 1 --json databaseId \
-q '.[0].databaseId // 0')
[ "$run_id" != "$before" ] && [ "$run_id" != 0 ] && break
done
if [ "$run_id" = "$before" ] || [ "$run_id" = 0 ]; then
echo "::error::could not find dispatched run for $repo ($wf)"; return 1
fi
gh run watch "$run_id" -R "itpplasma/$repo" --exit-status >/dev/null 2>&1
}

# FULL=true (libneo PR labeled 'full-ci') also runs each downstream's
# slow suite, dispatched per that downstream's own convention (the
# ci/downstreams full column); otherwise only the sub-minute fast tier.
while read -r repo wf blocking full_ci; do
[ -z "${repo:-}" ] && continue
# blocking=no rows are dispatched and reported but never set fail.
blocks=1
[ "${blocking:-yes}" != yes ] && blocks=0
echo "::group::$repo ($wf) against libneo $REF (full=$FULL, blocking=$blocks)"
rc=0
if [ "$FULL" = true ] && [ "${full_ci:-no}" = yes ]; then
# self: the downstream's one workflow carries its own slow tier.
run_wf "$repo" "$wf" -f full=true || rc=1
else
run_wf "$repo" "$wf" || rc=1
if [ "$FULL" = true ] && [ "${full_ci:-no}" != no ]; then
# extra workflows: the downstream keeps its slow suite separate.
IFS=',' read -ra extra <<< "$full_ci"
for ewf in "${extra[@]}"; do
run_wf "$repo" "$ewf" || rc=1
done
fi
fi
if [ "$rc" = 0 ]; then
echo "$repo: green"
elif [ "$blocks" = 1 ]; then
echo "::error::$repo CI red against libneo $REF"; fail=1
else
echo "::warning::$repo CI red against libneo $REF — report-only, not failing the gate"
fi
echo "::endgroup::"
done < <(grep -vE '^[[:space:]]*#|^[[:space:]]*$' ci/downstreams)
exit $fail
36 changes: 36 additions & 0 deletions ci/downstreams
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Downstream codes gated on every libneo PR. This file is the single source of
# the gated set: add a downstream by appending one row; the gate workflow needs
# no edit. The chosen workflow must accept a `libneo_ref` input.
#
# Columns: <repo> <workflow> <blocking> <full>
# workflow dispatched with `-f libneo_ref=<sha>` to build + fast-test the
# downstream against this PR's libneo. Must accept that input on
# the downstream's default branch.
# blocking yes = a red run fails the libneo PR. no = report-only.
# full what to also run when the libneo PR is labeled `full-ci`. The
# downstream picks its own convention; the gate does not force one:
# no fast-only; the slow suite runs on the
# downstream's own PRs instead.
# yes re-dispatch <workflow> with `-f full=true`
# (the workflow carries its own slow tier).
# <wf>[,<wf>...] also dispatch these extra workflows, each with
# `-f libneo_ref=<sha>` (the downstream keeps its
# slow suite in separate workflows).
#
# NEO-2 splits fast and slow CI: unit-tests.yml is the always-on fast tier the
# gate triggers; its golden-record/performance/PAR suite stays on NEO-2's own
# PRs, so NEO-2 is full=no here.
#
# rabe keeps its slow suite in its own golden.yml; on full-ci the gate dispatches
# that against the candidate libneo. benchmark.yml is a rabe perf trend keyed to
# rabe's own commits, so it stays on rabe's PRs and is not gate-triggered.
#
# NEO-RT is report-only: it reaches libneo transitively through NEO-2 (already
# gated), and its own build does not yet compile the candidate libneo, so a
# direct gate would not exercise it. Promote once that propagation is wired.
SIMPLE main.yml yes yes
NEO-2 unit-tests.yml yes no
MEPHIT main.yml yes yes
KAMEL ci.yml yes yes
rabe test.yml yes golden.yml
NEO-RT test.yml no no
Loading