From ac2d296c8aef17b54a5ad6216467839f8f59ee0e Mon Sep 17 00:00:00 2001 From: MDA2AV Date: Sun, 16 Aug 2026 12:22:25 +0100 Subject: [PATCH] ci: validate compose-profile entries on the self-hosted box, one at a time validate (fulmine) has been failing on three profiles for reasons that have nothing to do with the entry. The gateway and production compose files pin cpusets from the benchmark host - 0-15,64-79 for the edge, 17-20,81-84 for authsvc, 21-31,85-95 for the server - and a GitHub runner has four cores, so docker refuses to create the containers. Every gateway-subscribed entry has this: aspnet-minimal_caddy, aspnet-minimal_nginx, fulmine, sark-gateway, sark-production, trillium, trillium-tuned. Unsetting the cpusets on the runner would have let the containers start, but the stack would then be unpinned and contending for four cores with the load generator. That is not the profile being validated. These belong on the box that has the cores. detect now emits two lists. An entry that ships a compose.*.yml goes to `validate-gateway` on self-hosted; everything else keeps running in parallel on ubuntu-latest exactly as before. The split is per entry rather than per profile because validate.sh runs an entry's whole `tests` array in one process, so a gateway-subscribed entry has to run its isolated profiles there too. Serialization, since the stacks bind the host's ports directly - edge 8443, authsvc 9090, server 8080 - and two of them on one box fight over them: - the job shares the `benchmark` concurrency group, so a validation never runs while a measured run holds the machine, and waits rather than replacing it - it iterates the entries in one job instead of a matrix. A matrix job enters the concurrency group once per entry, and GitHub keeps only one run pending per group, so three gateway entries in one PR would have the third cancel the second while it waited. One job holding the slot for all of them is sequential by construction. Every entry runs even if an earlier one fails, so a PR touching several gets one report rather than one failure and a blank. Also drops two entries the old detection would have tried to validate: a directory deleted in the PR, and frameworks/_shared, which is the authsvc sidecar rather than an entry and has no meta.json. Both used to reach validate.sh and fail there. Verified the detection against the real tree for six cases: a single gateway entry, a mixed PR, plain entries only, frameworks/_shared, a deleted framework, and a PR that touches no entry at all. --- .github/workflows/validate.yml | 76 +++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index adcebad5f..202a02ab3 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -12,18 +12,43 @@ jobs: runs-on: ubuntu-latest outputs: frameworks: ${{ steps.find.outputs.frameworks }} + gateway: ${{ steps.find.outputs.gateway }} steps: - uses: actions/checkout@v5 with: fetch-depth: 0 - id: find + # Two lists, because the two halves cannot run on the same machine. + # + # An entry that ships a compose.*.yml subscribes to gateway-64, + # gateway-h3 or production-stack, and those compose files pin cpusets + # from the benchmark host — 0-15,64-79 for the edge, 21-31,85-95 for + # the server. A GitHub runner has four cores, so Docker refuses to + # create the container and the profile fails for reasons that have + # nothing to do with the entry. Those go to the self-hosted box. + # + # The split is per entry, not per profile: validate.sh runs everything + # in an entry's `tests` array in one process, so a gateway-subscribed + # entry has to run its isolated profiles there too. run: | - frameworks=$(git diff --name-only origin/${{ github.base_ref }}...HEAD \ - | grep '^frameworks/' \ - | cut -d'/' -f2 \ - | sort -u \ - | jq -R -s -c 'split("\n") | map(select(length > 0))') - echo "frameworks=$frameworks" >> "$GITHUB_OUTPUT" + changed=$(git diff --name-only origin/${{ github.base_ref }}...HEAD \ + | grep '^frameworks/' | cut -d'/' -f2 | sort -u || true) + std=(); gw=() + for fw in $changed; do + # Deleted in this PR, or the shared authsvc rather than an entry. + [ -d "frameworks/$fw" ] || continue + [ -f "frameworks/$fw/meta.json" ] || continue + if compgen -G "frameworks/$fw/compose.*.yml" > /dev/null; then + gw+=("$fw") + else + std+=("$fw") + fi + done + as_json() { printf '%s\n' "${@}" | jq -R -s -c 'split("\n") | map(select(length > 0))'; } + echo "frameworks=$(as_json "${std[@]:-}")" >> "$GITHUB_OUTPUT" + echo "gateway=$(as_json "${gw[@]:-}")" >> "$GITHUB_OUTPUT" + echo "standard: $(as_json "${std[@]:-}")" + echo "gateway: $(as_json "${gw[@]:-}")" validate: needs: detect @@ -38,3 +63,42 @@ jobs: - uses: actions/checkout@v5 - name: Validate ${{ matrix.framework }} run: ./scripts/validate.sh "${{ matrix.framework }}" + + validate-gateway: + needs: detect + if: needs.detect.outputs.gateway != '[]' + runs-on: self-hosted + timeout-minutes: 90 + # One at a time, and never alongside a benchmark. Every gateway and + # production stack binds the host's ports directly — edge 8443, authsvc + # 9090, server 8080 — so two stacks on one box fight over them and the + # loser dies on bind. Sharing the `benchmark` group is what keeps a + # validation off the box while a measured run is using it, and matches + # those workflows' cancel-in-progress: false: a queued run waits rather + # than replacing the one holding the machine. + concurrency: + group: benchmark + cancel-in-progress: false + steps: + - uses: actions/checkout@v5 + # Deliberately a loop rather than a matrix. A matrix job enters the + # concurrency group once per entry, and GitHub keeps only one run pending + # per group — so three gateway entries in one PR would have the third + # cancel the second while it waited. One job holding the slot for all of + # them is sequential by construction and cannot cancel itself. + # + # Every entry runs even if an earlier one fails, so a PR touching several + # gets one report instead of one failure and a blank. + - name: Validate compose-profile entries (self-hosted, one at a time) + run: | + failed=() + for fw in $(echo '${{ needs.detect.outputs.gateway }}' | jq -r '.[]'); do + echo "::group::validate $fw" + ./scripts/validate.sh "$fw" || failed+=("$fw") + echo "::endgroup::" + done + if [ ${#failed[@]} -gt 0 ]; then + echo "::error::validation failed: ${failed[*]}" + exit 1 + fi + echo "all compose-profile entries validated"