Skip to content

docs(#856): architecture plan for controller resolution refactor #2374

docs(#856): architecture plan for controller resolution refactor

docs(#856): architecture plan for controller resolution refactor #2374

Workflow file for this run

# Actions are pinned to commit SHAs (not version tags) to prevent supply chain attacks.
# Version comments (e.g., "# v6") allow Dependabot to track and update to newer versions.
name: Tests
on:
workflow_dispatch:
pull_request:
push:
workflow_call:
jobs:
security:
name: Security Scan
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
enable-cache: true
- name: Install bandit
run: uv tool install bandit[toml]
- name: Run bandit security scan
run: bandit -c pyproject.toml -r nac_test/ -ll -f json -o bandit-security-report.json
- name: Upload security report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
with:
name: bandit-security-report
path: bandit-security-report.json
lint:
name: Lint
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write # Required for dependabot to push lock file updates
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
# For fork PRs, use merge commit; for same-repo PRs, use head ref
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
enable-cache: true
- name: Install mypy
run: uv tool install mypy
- name: Verify uv.lock is up to date
if: github.actor != 'dependabot[bot]'
run: |
set -euo pipefail
uv lock
git diff --exit-code uv.lock
- name: Update lock file
# Only run for dependabot on same-repo PRs (not forks)
if: github.actor == 'dependabot[bot]' && github.event.pull_request.head.repo.full_name == github.repository
run: |
set -euo pipefail
echo "=== uv lock diagnostics ==="
pwd
ls -la
test -f pyproject.toml && echo "pyproject.toml: present" || (echo "pyproject.toml: MISSING" >&2; exit 1)
uv --version
echo "actor=$GITHUB_ACTOR ref=$GITHUB_REF head_ref=${GITHUB_HEAD_REF:-<unset>}"
uv lock
if [[ -n $(git status --porcelain uv.lock) ]]; then
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add uv.lock
git commit -m "chore: update uv.lock [dependabot skip]"
git push origin HEAD:refs/heads/${{ github.head_ref }}
fi
- name: Check License Headers
run: bash scripts/license-headers.sh
- name: Pre-commit Checks
uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
test-linux:
name: Tests (Linux)
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
strategy:
matrix:
python:
- "3.10"
- "3.11"
- "3.12"
- "3.13"
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
enable-cache: true
- name: Set up Python ${{ matrix.python }}
run: uv python install ${{ matrix.python }}
- name: Test
run: |
uv sync --frozen --extra dev
# Run tests in parallel (--dist loadscope keeps test classes together, this is critical for e2e tests)
uv run pytest -n auto --dist loadscope tests/
test-windows:
name: Tests (Windows)
runs-on: windows-latest
timeout-minutes: 15
permissions:
contents: read
strategy:
matrix:
python:
- "3.10"
- "3.11"
- "3.12"
- "3.13"
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
enable-cache: true
- name: Set up Python ${{ matrix.python }}
run: uv python install ${{ matrix.python }}
- name: Test
run: |
uv sync --frozen --extra dev
# Explicit file list: Windows shell doesn't expand globs, and other integration
# tests import pyats which isn't available on Windows
uv run pytest -m windows -n auto --dist loadscope tests/e2e tests/integration/test_cli_basic.py tests/integration/test_cli_rendering.py tests/integration/test_cli_ordering.py tests/integration/test_cli_extra_args.py
uv run nac-test --version
test-uv-tool:
name: Tests (uv tool install)
runs-on: ${{ matrix.os }}
timeout-minutes: 10
permissions:
contents: read
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
enable-cache: true
- name: Set up Python 3.12
run: uv python install 3.12
- name: Install nac-test via uv tool install
run: uv tool install --python 3.12 .
- name: Smoke test uv tool install (Linux)
# Smoke-tests the render→pabot→robot path in a clean uv tool env (no dev
# venv on PATH), catching regressions like #718. PyATS is not covered here
# as it requires a live API endpoint — see #722.
if: runner.os == 'Linux'
run: |
TMPDIR=$(mktemp -d)
nac-test -d tests/integration/fixtures/data -t tests/integration/fixtures/templates -o "$TMPDIR"
- name: Smoke test uv tool install (Windows)
if: runner.os == 'Windows'
env:
PYTHONUTF8: 1 # CI stdout is piped/captured → falls back to cp1252 (#723)
run: |
$TMPDIR = New-TemporaryFile | ForEach-Object { Remove-Item $_; New-Item -ItemType Directory -Path $_ }
nac-test -d tests/integration/fixtures/data -t tests/integration/fixtures/templates -o "$TMPDIR"
shell: pwsh
notification:
name: Notification
if: always() && github.event_name != 'pull_request' && github.event_name != 'workflow_call' && github.actor != 'dependabot[bot]'
needs: [security, lint, test-linux, test-windows, test-uv-tool]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions: {}
steps:
- name: Check Job Success
run: |
if [ ${{ needs.security.result }} == 'success' ] && [ ${{ needs.lint.result }} == 'success' ] && [ ${{ needs.test-linux.result }} == 'success' ] && [ ${{ needs.test-windows.result }} == 'success' ] && [ ${{ needs.test-uv-tool.result }} == 'success' ]; then
echo "All jobs succeeded"
echo "JOB_SUCCESS=success" >> $GITHUB_ENV
else
echo "Not all jobs succeeded"
echo "JOB_SUCCESS=fail" >> $GITHUB_ENV
fi
id: print_status
- name: Sanitize notification variables
env:
# Pass through env to avoid shell injection from server-side expression expansion
RAW_COMMIT_MSG: ${{ github.event.head_commit.message }}
RAW_PR_TITLE: ${{ github.event.pull_request.title }}
run: |
# Truncate to stay within Webex message size limits (500 chars for commit
# messages, 200 chars for PR titles are generous practical limits)
COMMIT_MSG=$(echo "$RAW_COMMIT_MSG" | head -c 500)
PR_TITLE=$(echo "$RAW_PR_TITLE" | head -c 200)
{
echo "SAFE_COMMIT_MSG<<EOF"
echo "$COMMIT_MSG"
echo "EOF"
echo "SAFE_PR_TITLE<<EOF"
echo "$PR_TITLE"
echo "EOF"
} >> "$GITHUB_ENV"
# Using direct curl to Webex API instead of a GitHub Action because:
# - qsnyder/action-wxt is unmaintained (last commit 2022, no releases)
# - Direct API call has no third-party action dependency to maintain
# - Gives full control over error handling and message format
- name: Webex Notification
if: always()
env:
# Pass sanitized env vars and static context values through env to avoid
# server-side expression expansion inside the shell script
JOB_SUCCESS: ${{ env.JOB_SUCCESS }}
SAFE_COMMIT_MSG: ${{ env.SAFE_COMMIT_MSG }}
SAFE_PR_TITLE: ${{ env.SAFE_PR_TITLE }}
WEBEX_TOKEN: ${{ secrets.WEBEX_TOKEN }}
WEBEX_ROOM_ID: ${{ secrets.WEBEX_ROOM_ID }}
COMMIT_URL: ${{ github.event.head_commit.url }}
AUTHOR: ${{ github.event.sender.login }}
REF: ${{ github.ref }}
HEAD_REF: ${{ github.head_ref }}
EVENT: ${{ github.event_name }}
REPO: ${{ github.repository }}
RUN_NUM: ${{ github.run_number }}
RUN_ID: ${{ github.run_id }}
run: |
# jq -n --arg handles all JSON escaping; avoids double-encoding from jq -Rs
PAYLOAD=$(jq -n \
--arg room_id "$WEBEX_ROOM_ID" \
--arg status "$JOB_SUCCESS" \
--arg repo "$REPO" \
--arg run_num "$RUN_NUM" \
--arg run_url "https://github.com/$REPO/actions/runs/$RUN_ID" \
--arg commit_msg "$SAFE_COMMIT_MSG" \
--arg commit_url "$COMMIT_URL" \
--arg pr_title "$SAFE_PR_TITLE" \
--arg author "$AUTHOR" \
--arg ref "$REF" \
--arg head_ref "$HEAD_REF" \
--arg event "$EVENT" \
'{roomId: $room_id, markdown: ("[**[\($status)] \($repo) #\($run_num)**](\($run_url))\n* Commit: [\($commit_msg)](\($commit_url))\($pr_title)\n* Author: \($author)\n* Branch: \($ref) \($head_ref)\n* Event: \($event)")}')
# --fail-with-body: exit non-zero on HTTP 4xx/5xx so the || warning fires
curl --silent --fail-with-body --max-time 30 \
--request POST \
--url https://webexapis.com/v1/messages \
--header "Authorization: Bearer $WEBEX_TOKEN" \
--header "Content-Type: application/json" \
--data "$PAYLOAD" \
|| echo "::warning::Webex notification failed"