|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Select the ports local-ci must run for THIS push. |
| 3 | +# |
| 4 | +# `ci-affected-ports.sh` answers "what did this push change?" — which is the wrong |
| 5 | +# question to select lanes on by itself, and the gap is not hypothetical. Across the |
| 6 | +# 1.0.1 cut five lanes failed on c59bb31c0 and were never re-run: the next commit was |
| 7 | +# docs-only, so its run skipped every code lane and reported GREEN, and the red sat |
| 8 | +# unverified under a green tip until the full matrix was dispatched by hand. |
| 9 | +# |
| 10 | +# The question that closes it is "what is not currently known-green on main?", and the |
| 11 | +# answer is a union: |
| 12 | +# |
| 13 | +# affected(this push) ∪ ports whose newest verdict on main is not success |
| 14 | +# |
| 15 | +# Re-running is deliberately chosen over merely reporting. A report that says "java was |
| 16 | +# red three commits ago" leaves the red UNVERIFIED, which is the defect itself; running |
| 17 | +# the lane resolves it either way. So a docs push landing after a red java lane runs |
| 18 | +# java, and it either recovers or stays red loudly. |
| 19 | +# |
| 20 | +# This can only ever WIDEN the affected set — the same direction ci-affected-ports.sh's |
| 21 | +# fail-open policy already commits to, and for the same reason: coverage may only shrink |
| 22 | +# when the shrink is certain. |
| 23 | +# |
| 24 | +# Usage: scripts/ci-ports-to-run.sh <base-sha> <head-sha> |
| 25 | +# Prints a space-separated subset of: ts java python csharp |
| 26 | +set -uo pipefail |
| 27 | + |
| 28 | +ALL="ts java python csharp" |
| 29 | + |
| 30 | +# Resolve the sibling by the script's own location, never by the caller's cwd — the |
| 31 | +# workflow happens to run from the repo root, and a relative call would keep working |
| 32 | +# right up until something invoked this from anywhere else and it fell back to ALL. |
| 33 | +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 34 | + |
| 35 | +# How far back to look for a lane's newest verdict. The nightly full matrix gives every |
| 36 | +# lane a fresh verdict daily, so this window only has to outlive one day of pushes. |
| 37 | +RUN_WINDOW="${MO_PORTS_RUN_WINDOW:-20}" |
| 38 | + |
| 39 | +# Every local-ci job that TESTS a port, mapped to the port it tests. `detect`, `gates` |
| 40 | +# and `notify` are absent on purpose: they run on every push, so they can never be the |
| 41 | +# skipped-and-stale lane this file exists to catch. |
| 42 | +# |
| 43 | +# A new lane missing from this map would be invisible to the known-green check and could |
| 44 | +# sit red forever under green runs — the same defect, one level down. Both directions are |
| 45 | +# asserted against the workflow's real job list by scripts/test-ci-ports-to-run.sh. |
| 46 | +lane_port() { |
| 47 | + case "$1" in |
| 48 | + ts-fast|ts-unit|ts-slow) echo ts ;; |
| 49 | + java-fast|java-slow) echo java ;; |
| 50 | + python) echo python ;; |
| 51 | + csharp) echo csharp ;; |
| 52 | + *) echo "" ;; |
| 53 | + esac |
| 54 | +} |
| 55 | + |
| 56 | +all_lanes() { echo "ts-fast ts-unit ts-slow java-fast java-slow python csharp"; } |
| 57 | + |
| 58 | +# Canonical order, deduplicated, unknown tokens dropped. The order is FIXED rather than |
| 59 | +# sorted because the workflow tests the result with `contains(..., 'ts')` — a substring |
| 60 | +# match on a string that must read the same whichever path produced it. |
| 61 | +union_ports() { |
| 62 | + local want=" $* " out="" p |
| 63 | + for p in $ALL; do |
| 64 | + case "$want" in *" $p "*) out="$out $p" ;; esac |
| 65 | + done |
| 66 | + echo "${out# }" |
| 67 | +} |
| 68 | + |
| 69 | +# stdin: one "<job><TAB><conclusion>" line per job, with runs ordered NEWEST FIRST. |
| 70 | +# Prints the ports that are not known-green. |
| 71 | +# |
| 72 | +# The first non-`skipped` conclusion for a lane is that lane's newest verdict. `skipped` |
| 73 | +# is walked past rather than counted, because it describes the SELECTION and says nothing |
| 74 | +# about the code — treating it as a verdict is precisely the bug. |
| 75 | +# |
| 76 | +# Anything that is not `success` counts as not-green, `cancelled` included: a cancelled |
| 77 | +# lane was never verified, and `cancel-in-progress: true` makes cancellation common |
| 78 | +# enough that counting it as green would reopen the hole from the other side. A lane with |
| 79 | +# no verdict in the window is UNKNOWN, which widens for the same reason. |
| 80 | +not_green_ports() { |
| 81 | + local -A verdict=() |
| 82 | + local job conc |
| 83 | + while IFS=$'\t' read -r job conc; do |
| 84 | + [ -n "$job" ] || continue |
| 85 | + [ "$conc" = "skipped" ] && continue |
| 86 | + [ -n "${verdict[$job]+x}" ] && continue |
| 87 | + verdict[$job]="$conc" |
| 88 | + done |
| 89 | + |
| 90 | + local out="" lane |
| 91 | + for lane in $(all_lanes); do |
| 92 | + [ "${verdict[$lane]:-}" = "success" ] || out="$out $(lane_port "$lane")" |
| 93 | + done |
| 94 | + union_ports $out |
| 95 | +} |
| 96 | + |
| 97 | +# Test seam: `MO_PORTS_TEST=1 source` exposes the pure functions and makes no network |
| 98 | +# or git call. Everything above this line is pure; everything below talks to the world. |
| 99 | +[ "${MO_PORTS_TEST:-0}" = "1" ] && return 0 |
| 100 | + |
| 101 | +# Ask the API for each lane's newest verdict on main, newest run first. |
| 102 | +# |
| 103 | +# curl + jq rather than `gh`: both are in /usr/bin on the self-hosted runner, while `gh` |
| 104 | +# is on a user PATH the runner service does not necessarily inherit. A missing binary |
| 105 | +# here would fail open to the full matrix, which is safe but silently expensive forever. |
| 106 | +fetch_verdicts() { |
| 107 | + local repo="${GITHUB_REPOSITORY:-}" token="${GITHUB_TOKEN:-}" api="${GITHUB_API_URL:-https://api.github.com}" |
| 108 | + [ -n "$repo" ] && [ -n "$token" ] || return 1 |
| 109 | + command -v curl >/dev/null && command -v jq >/dev/null || return 1 |
| 110 | + |
| 111 | + local get=(curl -sS --fail --max-time 30 |
| 112 | + -H "Authorization: Bearer $token" |
| 113 | + -H "Accept: application/vnd.github+json" |
| 114 | + -H "X-GitHub-Api-Version: 2022-11-28") |
| 115 | + |
| 116 | + local runs |
| 117 | + runs="$("${get[@]}" \ |
| 118 | + "$api/repos/$repo/actions/workflows/local-ci.yml/runs?branch=main&status=completed&per_page=$RUN_WINDOW" \ |
| 119 | + | jq -r '.workflow_runs[].id')" || return 1 |
| 120 | + [ -n "$runs" ] || return 0 # no history is not an error; not_green_ports widens on it |
| 121 | + |
| 122 | + local id |
| 123 | + for id in $runs; do |
| 124 | + # The run doing the asking is not completed and so cannot appear here — but a rerun |
| 125 | + # of it could, and its own lanes are exactly the ones with no verdict yet. |
| 126 | + [ "$id" = "${GITHUB_RUN_ID:-}" ] && continue |
| 127 | + "${get[@]}" "$api/repos/$repo/actions/runs/$id/jobs?per_page=50" \ |
| 128 | + | jq -r '.jobs[] | "\(.name)\t\(.conclusion // "none")"' || return 1 |
| 129 | + done |
| 130 | +} |
| 131 | + |
| 132 | +BASE="${1:-}"; HEAD="${2:-HEAD}" |
| 133 | +AFFECTED="$("$HERE/ci-affected-ports.sh" "$BASE" "$HEAD")" |
| 134 | + |
| 135 | +VERDICTS="$(fetch_verdicts)" |
| 136 | +if [ $? -ne 0 ]; then |
| 137 | + # Fail open, loudly. An API blip must not be able to shrink coverage; the cost of |
| 138 | + # widening is one extra matrix on a runner that bills nothing. |
| 139 | + echo "ci-ports-to-run: could not read lane history — selecting all ports" >&2 |
| 140 | + union_ports $ALL |
| 141 | + exit 0 |
| 142 | +fi |
| 143 | + |
| 144 | +STALE="$(printf '%s\n' "$VERDICTS" | not_green_ports)" |
| 145 | +[ -n "$STALE" ] && echo "ci-ports-to-run: not known-green on main: $STALE" >&2 |
| 146 | +union_ports $AFFECTED $STALE |
0 commit comments