Repository navigation
build(deps-dev): bump selenium-webdriver from 4.50.0 to 4.51.0 in /site #1195
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)." |