Skip to content

build(deps-dev): bump selenium-webdriver from 4.50.0 to 4.51.0 in /site #1195

build(deps-dev): bump selenium-webdriver from 4.50.0 to 4.51.0 in /site

build(deps-dev): bump selenium-webdriver from 4.50.0 to 4.51.0 in /site #1195

Workflow file for this run

name: CI
on:
push:
branches: [main]
# Pure-docs / site / packaging-template changes don't affect Rust
# behavior, and skipping them here avoids a full 3-OS test matrix on
# a typo fix in a README. The site has its own pages.yml workflow
# that handles its own changes; packaging templates are validated
# at release time by publish.yml when they're actually used.
paths-ignore:
- '**/*.md'
- 'docs/**'
- 'site/**'
- 'packaging/**'
- 'LICENSE'
- '.gitignore'
# NOTE: deliberately no `paths-ignore` here, unlike the push trigger
# above. `CI Gate` at the bottom of this file is a required status
# check, and a required check that never *reports* leaves a PR stuck on
# "Expected — waiting for status" forever. Filtering at the workflow
# level would do exactly that for any PR touching only site/docs/
# packaging. So the workflow always runs, and the expensive jobs skip
# themselves via the `changes` job below. A skipped job satisfies a
# required check; an absent one does not.
pull_request:
branches: [main]
# Let me run CI manually from the Actions tab, useful for re-running a
# cache-warm build after tweaking the workflow without having to push.
workflow_dispatch:
# A new push to the same PR cancels any still-running job from that PR.
# Saves runner minutes and gives fresher feedback on force-pushes.
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Least-privilege by default; individual jobs can opt into more.
permissions:
contents: read
jobs:
# Every job below sets `timeout-minutes`. GitHub's default is 360, and on
# 2026-08-19 a stalled `apt-get` in "Install system dependencies" sat there
# for the full six hours on five branches at once, then failed the gate --
# burning a day of runner time to report a problem visible in the first
# minute.
#
# The values are sized against measured runs, not guessed: with a warm
# cache the slowest job here is ~3 minutes, and a fully cold build of the
# workspace (GTK/WebKit + Tauri, release profile with LTO) is well under
# 20. Each limit leaves roughly an order of magnitude of headroom over
# what the job actually needs, so a timeout means something is wrong
# rather than merely slow.
# Classify the diff so the expensive jobs can skip themselves without
# the workflow disappearing from the PR's check list. Done with plain
# git rather than a paths-filter action to avoid adding another
# third-party action to a repo that signs and publishes binaries.
changes:
name: Detect changes
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
code: ${{ steps.filter.outputs.code }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0
- id: filter
shell: bash
# `github.event.pull_request.*` is attacker-reachable on a fork PR.
# These two fields are commit SHAs, so hex, so not exploitable today —
# but interpolating an event field straight into a `run:` script is
# the shape of the bug, not the field's contents, and this was the
# last place in the repo still doing it. Through `env:` the shell
# receives them as data that no quoting mistake can turn into code.
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
if [[ "${{ github.event_name }}" != "pull_request" ]]; then
# push / manual runs always do the full build.
echo "code=true" >> "$GITHUB_OUTPUT"
exit 0
fi
files=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA")
echo "Changed files:"
echo "$files" | sed 's/^/ /'
# Anything that is NOT purely docs/site/packaging counts as code.
# Mirrors the push trigger's paths-ignore list above; keep the two
# in step.
code=$(echo "$files" | grep -vE '(\.md$|^docs/|^site/|^packaging/|^LICENSE$|^\.gitignore$)' || true)
if [[ -n "$code" ]]; then
echo "code=true" >> "$GITHUB_OUTPUT"
else
echo "code=false" >> "$GITHUB_OUTPUT"
echo "No code changes — Rust/GUI jobs will skip."
fi
maintainability:
name: Maintainability
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
# 22, matching pages.yml / release.yml / site.yml / site-preview.yml.
# Node 20 left standard support in April 2026, and the split meant
# the GUI was tested on one runtime and shipped from another.
node-version-file: .nvmrc
- name: File size guard
run: node scripts/check-file-size.mjs
# Contrast is invisible until someone cannot read something, and the
# light theme gets far less use than the dark one, so a regression there
# can sit for months. This also fails if design-tokens.json has drifted
# from tokens.css, which is the only thing keeping the generated file
# honest.
- name: Design token contrast
run: node scripts/design-tokens.mjs
# The app links at the docs site, and neither project knows what the
# other publishes. Runs in site.yml too, since either side can break it.
- name: App doc links
run: node scripts/check-app-doc-links.mjs
# Installer scripts aren't exercised by any other job, so lint them
# directly. shellcheck and PSScriptAnalyzer are both preinstalled on
# ubuntu-latest/GitHub-hosted runners.
script-lint:
name: Installer Script Lint
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# install.sh is user-facing; the release/ scripts hold GH_TOKEN and
# push to three package registries. Both are worth linting, and
# neither is exercised by any other job.
- name: shellcheck install.sh
run: shellcheck scripts/install.sh scripts/install_test.sh
# What install.sh checks before it installs, against a fake release:
# a wrong checksum or a signature that does not verify must stop it,
# and a download cut short must run nothing.
- name: test install.sh
run: bash scripts/install_test.sh
- name: shellcheck release scripts
run: shellcheck scripts/release/*.sh
# The publish path only executes during a release, so a regression in
# it stays invisible until the worst possible moment. lib_test.sh
# covers the two things shellcheck cannot see: that verified_sha's
# stdout carries the digest and nothing else, and that validate_tag
# rejects the shell metacharacters that reach sed.
- name: test release script helpers
run: bash scripts/release/lib_test.sh
# msstore-submit.sh runs once per release against an API CI cannot
# reach, so its test answers in the API's place.
- name: test Store submission script
run: bash scripts/release/msstore-submit_test.sh
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .nvmrc
# The last check before a release goes public, and the builder of the
# in-app updater's latest.json. Both run only during a release, and a
# mistake in the first one would let an incomplete release out.
- name: test release asset and updater manifest checks
run: node --test scripts/release/*.test.mjs
- name: PSScriptAnalyzer install.ps1 and the release scripts
shell: pwsh
# Write-Host is intentional here (colored installer UX output, which
# Write-Output can't do); the empty catch blocks are documented
# best-effort fallbacks (e.g. TLS 1.2 opt-in on older PowerShell);
# and BOM-less UTF-8 is the existing, working file encoding. None of
# these are correctness bugs, so they're excluded rather than
# rewriting a working installer for style alone.
#
# scripts/release/*.ps1 too. sign-windows-pe.ps1 and
# check-msi-signed.ps1 run only during a release, inside the Windows
# GUI build, and a failure in the first is what left v0.3.4 without
# Windows files for 1 h 38 min. Linting is the part of that path CI
# can reach without the signing credentials.
run: |
Install-Module -Name PSScriptAnalyzer -Force -Scope CurrentUser
$paths = @('scripts/install.ps1') + @(Get-ChildItem scripts/release/*.ps1 | ForEach-Object FullName)
$results = @($paths | ForEach-Object {
Invoke-ScriptAnalyzer -Path $_ -Severity Warning,Error `
-ExcludeRule PSAvoidUsingWriteHost, PSAvoidUsingEmptyCatchBlock, PSUseBOMForUnicodeEncodedFile
})
$results | Format-Table -AutoSize
if ($results.Count -gt 0) { exit 1 }
# Fast lint + format gate. Runs first because if `cargo fmt --check` is
# going to fail there's no point firing up the matrix below.
lint:
name: Format + Clippy
needs: [changes, maintainability]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# libpcap-dev: netscli-core pcap feature. GTK/WebKit: netscli-gui
# (Tauri 2) — clippy --all-targets compiles the Tauri shell, which
# won't link without these even when we only intend to lint Rust.
- name: Install system dependencies
run: |
# `|| true`, deliberately. GitHub's Ubuntu image ships third-party
# apt sources (Google Chrome among them) that this job does not use,
# and a broken index on any one of them makes `apt-get update` exit
# 100 and kill the job before Rust is even invoked. That is not a
# hypothetical: on 2026-09-09 a `Hash Sum mismatch` on
# dl.google.com failed this step across three re-runs.
#
# The install below is the step whose failure means something, and
# it still fails hard: if libpcap genuinely cannot be fetched,
# `apt-get install` exits non-zero and the job stops there.
sudo apt-get update || true
sudo apt-get install -y \
libpcap-dev pkg-config \
libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev \
librsvg2-dev libsoup-3.0-dev libjavascriptcoregtk-4.1-dev
- name: Install Rust
# Pinned by commit SHA; the trailing `# stable` records which
# action-side ref that SHA came from, and is deliberately not a
# version number. dtolnay/rust-toolchain's numeric refs (@1.96.0)
# are Rust *toolchain* versions rather than action releases, so a
# version-shaped comment here invites Dependabot to bump the
# compiler while pretending to bump an action. The toolchain we
# actually want comes from the explicit `toolchain` input below,
# and .github/dependabot.yml ignores this action as a second guard.
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
with:
toolchain: "1.96.0"
components: rustfmt, clippy
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
shared-key: lint
- name: cargo fmt --check
run: cargo fmt --check
- name: cargo clippy (default features)
run: cargo clippy --all-targets -- -D warnings
- name: cargo clippy (pcap feature)
run: cargo clippy --all-targets --features pcap -- -D warnings
# The desktop app's hand-written TypeScript types are checked against
# these generated ones at compile time
# (apps/netscli-gui/src/types/netscli.contract.ts). That check is only
# as good as the generated files, so they must match the Rust structs.
- name: TypeScript bindings match the Rust types
run: |
rm -rf apps/netscli-gui/src/types/generated
TS_RS_EXPORT_DIR="$PWD/apps/netscli-gui/src/types/generated" TS_RS_LARGE_INT=number cargo test -p netscli-core --features ts,mdns --lib export_bindings
git add --intent-to-add apps/netscli-gui/src/types/generated
git diff --exit-code -- apps/netscli-gui/src/types/generated || { echo "::error::Regenerate with the command in apps/netscli-gui/src/types/netscli.contract.ts"; exit 1; }
test:
name: Test (${{ matrix.os }})
needs: [changes, lint]
if: needs.changes.outputs.code == 'true'
runs-on: ${{ matrix.os }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# Linux needs libpcap for netscli-core's pcap feature and GTK/WebKit
# for netscli-gui (Tauri 2). `cargo test --all` compiles the whole
# workspace including the GUI, so both are required to link.
- name: Install system dependencies (Linux)
if: runner.os == 'Linux'
run: |
# `|| true` so a broken third-party apt index cannot fail this job;
# the install below still fails hard. See the first occurrence in
# ci.yml for the full reasoning.
sudo apt-get update || true
sudo apt-get install -y \
libpcap-dev pkg-config \
libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev \
librsvg2-dev libsoup-3.0-dev libjavascriptcoregtk-4.1-dev
- name: Install Rust
# See the pin note on the lint job above.
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
with:
toolchain: "1.96.0"
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
shared-key: test-${{ matrix.os }}
- name: cargo test --all
run: cargo test --all --no-fail-fast
- name: cargo test netscli-core with pcap
if: runner.os == 'Linux'
run: cargo test -p netscli-core --features pcap --no-fail-fast
- name: cargo test netscli-mcp with pcap
if: runner.os == 'Linux'
run: cargo test -p netscli-mcp --features pcap --no-fail-fast
# The Windows pcap paths used to be covered by nothing, anywhere.
#
# Both steps above are Linux-only, and the feature cannot be built on a
# Windows runner without the Npcap SDK -- the link step fails with
# `LNK1181: cannot open input file 'wpcap.lib'`. So the Npcap code, which
# is the entire reason the feature exists on Windows and is what the
# `-pcap` Windows release asset ships, was exercised on no platform.
# SECURITY.md said so; this is the half of it that CI can close.
#
# release.yml already installs this SDK to build that asset, so the
# step below is its twin -- same pinned URL, same pinned digest, same
# reason for verifying before extracting. Re-pin both together.
- name: Install Npcap SDK (Windows, pcap)
if: runner.os == 'Windows'
shell: pwsh
run: |
$sdkUrl = "https://npcap.com/dist/npcap-sdk-1.13.zip"
$sdkSha256 = "dad1f2bf1b02b787be08ca4862f99e39a876c1f274bac4ac0cedc9bbc58f94fd"
$zipPath = Join-Path $env:RUNNER_TEMP "npcap-sdk.zip"
$destPath = Join-Path $env:RUNNER_TEMP "npcap-sdk"
Invoke-WebRequest -Uri $sdkUrl -OutFile $zipPath
$actual = (Get-FileHash -Algorithm SHA256 -Path $zipPath).Hash.ToLowerInvariant()
if ($actual -ne $sdkSha256) {
Write-Error "Npcap SDK checksum mismatch. Expected $sdkSha256, got $actual"
exit 1
}
Write-Host "Npcap SDK checksum verified."
Expand-Archive -Path $zipPath -DestinationPath $destPath -Force
"LIB=$(Join-Path $destPath 'Lib\x64');$env:LIB" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
"INCLUDE=$(Join-Path $destPath 'Include');$env:INCLUDE" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
# Build, not test, and the difference is forced rather than chosen.
#
# The SDK above provides headers and `wpcap.lib`, which is everything
# the *link* needs. Running is a different matter: the test binary
# imports `wpcap.dll`, which ships with the Npcap runtime driver, not
# the SDK. Windows resolves imports at load time, so without that DLL
# the executable does not start and every test in it fails at once --
# measured, not predicted: `cargo test` here exited 0xc0000135,
# STATUS_DLL_NOT_FOUND, after compiling cleanly in 1m35s.
#
# Installing the Npcap runtime is not the fix. Its silent installer is
# licensed, and a CI job that installs a packet-capture driver to run
# unit tests is a poor trade.
#
# `--tests` still compiles the test targets, so a Windows-only break in
# the pcap code -- a cfg that stops matching, an API that moved, a type
# that only differs on this platform -- fails here. That is the part
# that was covered by nothing. Actually capturing a packet needs a live
# adapter and stays a manual check.
- name: cargo build netscli-core with pcap (Windows)
if: runner.os == 'Windows'
run: cargo build -p netscli-core --features pcap --tests
- name: cargo build netscli-mcp with pcap (Windows)
if: runner.os == 'Windows'
run: cargo build -p netscli-mcp --features pcap --tests
gui-build:
name: GUI Build (TypeScript + Vite)
needs: [changes, lint]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
# 22 — see the note on the maintainability job. This one is also
# load-bearing for jsdom: jsdom 30 pulls an undici that calls
# `worker_threads.markAsUncloneable`, added in Node 22.10.0 and
# never backported to 20, so the whole test suite failed to start
# its workers on the old runtime.
node-version-file: .nvmrc
cache: npm
cache-dependency-path: apps/netscli-gui/package-lock.json
- name: npm ci
run: npm ci
working-directory: apps/netscli-gui
- name: GUI lint
# C-22: there was no lint config or script here at all, which
# AGENTS.md acknowledged. `react-hooks` is the rule set that catches
# stale closures and wrong dependency arrays -- the shape of A-13,
# B-16 and B-18.
run: npm run lint
working-directory: apps/netscli-gui
- name: GUI unit tests
run: npm run test:unit
working-directory: apps/netscli-gui
- name: Source size guard
run: npm run test:maintainability
working-directory: apps/netscli-gui
- name: tsc + vite build
run: npm run build
working-directory: apps/netscli-gui
# Drive `netscli serve` with the official MCP TypeScript SDK client.
#
# The Rust tests check the server's replies against what the server itself
# expects. Every reply carried both `result` and `error` for months with all
# of them green, while the SDK most MCP clients are built on dropped those
# replies and timed out waiting for `initialize`. This asks the SDK instead.
mcp-smoke:
name: MCP SDK smoke test
needs: [changes, lint]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install Rust
# See the pin note on the lint job above.
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
with:
toolchain: "1.96.0"
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
shared-key: mcp-smoke
- name: cargo build -p netscli
run: cargo build -p netscli
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .nvmrc
- name: npm ci
run: npm ci --ignore-scripts
working-directory: scripts/mcp-smoke
- name: Connect, list tools and call one
run: node scripts/mcp-smoke/smoke.mjs target/debug/netscli
# Release-config binary build. This catches regressions that only show
# up under optimizations / lto that debug builds miss, and the artifact
# upload makes it possible to grab a "tip of main" binary straight from
# the Actions run for quick smoke testing without cutting a release.
#
# Matrix is conditional on event:
# - pull_request → ubuntu-latest only. The `test` job already runs on
# all 3 OS, so cross-platform compile errors are caught there.
# Release-profile-specific bugs (LTO, opt-level=3, strip) are rare,
# and the actual release.yml rebuilds across 5 OS/arch on every tag.
# - push to main → full ubuntu+windows+macos. macOS is a 10x billing
# multiplier vs. ubuntu, so paying it once per main-merge instead
# of once per PR cuts release-build cost by ~95% with effectively
# equivalent coverage.
release-build:
name: Release build (${{ matrix.os }})
needs: [changes, test]
if: needs.changes.outputs.code == 'true'
runs-on: ${{ matrix.os }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
os: ${{ github.event_name == 'pull_request' && fromJSON('["ubuntu-latest"]') || fromJSON('["ubuntu-latest", "windows-latest", "macos-latest"]') }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install system dependencies (Linux)
if: runner.os == 'Linux'
run: |
# `|| true` so a broken third-party apt index cannot fail this job;
# the install below still fails hard. See the first occurrence in
# ci.yml for the full reasoning.
sudo apt-get update || true
sudo apt-get install -y libpcap-dev pkg-config
- name: Install Rust
# See the pin note on the lint job above.
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
with:
toolchain: "1.96.0"
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
shared-key: release-${{ matrix.os }}
- name: cargo build --release -p netscli
run: cargo build --release -p netscli
- name: Upload artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: netscli-${{ matrix.os }}
path: |
target/release/netscli
target/release/netscli.exe
if-no-files-found: ignore
retention-days: 14
# Prove the crates still package, long before release day.
#
# `cargo publish` is the one step of a release that cannot be undone:
# crates.io has no unpublish, only yank, and a yanked version still holds
# its number forever. Until now nothing exercised packaging until the
# release was already public and the upload was running.
#
# Dry-running all three together is what makes this possible at all. The
# dependent crates require `netscli-core = "^0.3.0"`, which does not exist
# on the index before release, so packaging them individually fails on
# resolution -- with or without `--no-verify`. Passing the whole set lets
# cargo resolve them from the workspace instead, and it builds each
# packaged copy to confirm it compiles standing alone.
#
# Push and dispatch only, not pull_request. It compiles the packaged copy
# of the workspace from scratch, which is minutes, and a packaging break
# caught at merge is still caught a long way from a release.
publish-dry-run:
name: Crate packaging (publish dry run)
needs: [changes, lint]
if: needs.changes.outputs.code == 'true' && github.event_name != 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install system dependencies
run: |
# `|| true` so a broken third-party apt index cannot fail this job;
# the install below still fails hard. See the first occurrence in
# ci.yml for the full reasoning.
sudo apt-get update || true
sudo apt-get install -y libpcap-dev pkg-config
- name: Install Rust
# See the pin note on the lint job above. This was the one `uses:` in
# the repository still on a moving ref.
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
with:
toolchain: "1.96.0"
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
shared-key: publish-dry-run
# Mirrors the real publish in publish.yml exactly, minus the upload.
# If these two ever diverge, this check stops meaning anything.
- name: cargo publish --dry-run (all three crates)
run: cargo publish --dry-run -p netscli-core -p netscli-mcp -p netscli
# ─── Required status check ────────────────────────────────────────────
#
# This is the ONLY job from this workflow that should be marked required
# in branch protection. It runs unconditionally (`if: always()`, no path
# filter), so it always reports — which is the whole point. Marking the
# individual jobs required instead would deadlock any PR that legitimately
# skips them.
#
# `needs` result values: success | failure | cancelled | skipped.
# Skipped is a pass here: it means the `changes` job decided this PR does
# not touch code, which is a real answer, not a missing one.
ci-gate:
name: CI Gate
if: always()
needs:
- changes
- maintainability
- script-lint
- lint
- test
- gui-build
- mcp-smoke
- release-build
- publish-dry-run
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Verify no upstream job failed
shell: bash
env:
NEEDS: ${{ toJSON(needs) }}
run: |
set -euo pipefail
echo "$NEEDS" | jq -r 'to_entries[] | " \(.key): \(.value.result)"'
# Allowlist the two good outcomes rather than denylisting the bad
# ones. This used to fail only on "failure" or "cancelled", and
# GitHub has more result states than that.
#
# Observed on PR #187: codeload started returning 429 for action
# downloads, the ubuntu leg of the test matrix died before running
# anything, and its result came back as "abandoned" — which matched
# neither arm, so this gate reported success over a test job that
# had visibly failed. A required status check that can go green
# while a test job is red is worse than no gate, because branch
# protection is trusting it.
#
# Anything that is not success or skipped now fails the gate,
# including result states GitHub may add later.
bad=$(echo "$NEEDS" | jq -r '
to_entries
| map(select(.value.result != "success" and .value.result != "skipped"))
| map("\(.key)=\(.value.result)")
| join(", ")
')
if [[ -n "$bad" ]]; then
echo "::error::CI Gate failed — these jobs did not succeed: ${bad}"
exit 1
fi
echo "CI Gate passed (all jobs succeeded or were legitimately skipped)."