From d7d1a5685cfa5a4648818e28f54762b1ae15ab1e Mon Sep 17 00:00:00 2001 From: Jacobinwwey Date: Mon, 23 Mar 2026 03:28:48 -0500 Subject: [PATCH 1/8] feat(ci): add unified ci-required-gate workflow --- .github/workflows/ci-required-gate.yml | 214 +++++++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 .github/workflows/ci-required-gate.yml diff --git a/.github/workflows/ci-required-gate.yml b/.github/workflows/ci-required-gate.yml new file mode 100644 index 0000000000..c2d39fe8c9 --- /dev/null +++ b/.github/workflows/ci-required-gate.yml @@ -0,0 +1,214 @@ +name: CI Required Gate + +on: + pull_request: + branches: [master, dev] + push: + branches: [master] + workflow_dispatch: + +concurrency: + group: ci-required-gate-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + changes: + name: Detect Change Scope + runs-on: ubuntu-latest + outputs: + docs_only: ${{ steps.detect.outputs.docs_only }} + dashboard_changed: ${{ steps.detect.outputs.dashboard_changed }} + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Detect changed files + id: detect + shell: bash + run: | + set -euo pipefail + + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + base_sha="${{ github.event.pull_request.base.sha }}" + head_sha="${{ github.event.pull_request.head.sha }}" + else + base_sha="${{ github.event.before }}" + head_sha="${{ github.sha }}" + fi + + if [[ -z "$base_sha" || "$base_sha" == "0000000000000000000000000000000000000000" ]]; then + base_sha="$(git rev-parse "${head_sha}^" 2>/dev/null || true)" + fi + + if [[ -z "$base_sha" ]]; then + changed_files="$(git ls-tree -r --name-only "$head_sha")" + else + changed_files="$(git diff --name-only "$base_sha" "$head_sha")" + fi + + docs_only=true + dashboard_changed=false + + while IFS= read -r f; do + [[ -z "$f" ]] && continue + + if [[ "$f" == dashboard/* ]]; then + dashboard_changed=true + fi + + if [[ ! "$f" =~ ^docs/ && ! "$f" =~ ^README.*\.md$ && ! "$f" =~ ^changelogs/ ]]; then + docs_only=false + fi + done <<< "$changed_files" + + echo "docs_only=$docs_only" >> "$GITHUB_OUTPUT" + echo "dashboard_changed=$dashboard_changed" >> "$GITHUB_OUTPUT" + + lint: + name: Lint (Ruff) + needs: changes + if: needs.changes.outputs.docs_only != 'true' + runs-on: ubuntu-latest + timeout-minutes: 12 + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: Install uv + run: | + python -m pip install --upgrade pip + python -m pip install uv + + - name: Sync dependencies + run: uv sync --group dev + + - name: Ruff format check + run: uv run ruff format --check . + + - name: Ruff lint check + run: uv run ruff check . + + test: + name: Unit Tests + needs: [changes, lint] + if: needs.changes.outputs.docs_only != 'true' + runs-on: ubuntu-latest + timeout-minutes: 35 + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: Install uv + run: | + python -m pip install --upgrade pip + python -m pip install uv + + - name: Run pytest suite + run: bash ./scripts/run_pytests_ci.sh ./tests + + smoke: + name: Smoke Test + needs: [changes, lint] + if: needs.changes.outputs.docs_only != 'true' + runs-on: ubuntu-latest + timeout-minutes: 12 + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: Install uv + run: | + python -m pip install --upgrade pip + python -m pip install uv + + - name: Sync dependencies + run: uv sync --group dev + + - name: Startup smoke test + shell: bash + run: | + set -euo pipefail + uv run main.py & + app_pid=$! + + cleanup() { + kill "$app_pid" 2>/dev/null || true + wait "$app_pid" 2>/dev/null || true + } + trap cleanup EXIT + + for _ in {1..60}; do + if curl -sf http://localhost:6185 >/dev/null 2>&1; then + exit 0 + fi + sleep 1 + done + + echo "Application failed to start within 60 seconds" + exit 1 + + dashboard: + name: Dashboard Build + needs: changes + if: needs.changes.outputs.dashboard_changed == 'true' + runs-on: ubuntu-latest + timeout-minutes: 18 + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '24.13.0' + + - name: Build dashboard + run: | + cd dashboard + npm install pnpm -g + pnpm install + pnpm i --save-dev @types/markdown-it + pnpm run build + + gate: + name: CI Required Gate + if: always() + needs: [changes, lint, test, smoke, dashboard] + runs-on: ubuntu-latest + steps: + - name: Check upstream job results + shell: bash + run: | + set -euo pipefail + if [[ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" == "true" ]]; then + echo "One or more required jobs failed or were cancelled" + exit 1 + fi + + - name: Print job summary + run: | + echo "changes=${{ needs.changes.result }}" + echo "lint=${{ needs.lint.result }}" + echo "test=${{ needs.test.result }}" + echo "smoke=${{ needs.smoke.result }}" + echo "dashboard=${{ needs.dashboard.result }}" From 68e778743a88970dbf05808b56947f0e19baa1e8 Mon Sep 17 00:00:00 2001 From: Jacobinwwey Date: Mon, 23 Mar 2026 03:43:42 -0500 Subject: [PATCH 2/8] fix(ci): harden dashboard gate toolchain setup --- .github/workflows/ci-required-gate.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-required-gate.yml b/.github/workflows/ci-required-gate.yml index c2d39fe8c9..0a948a7ac7 100644 --- a/.github/workflows/ci-required-gate.yml +++ b/.github/workflows/ci-required-gate.yml @@ -177,18 +177,22 @@ jobs: - name: Checkout uses: actions/checkout@v6 + - name: Setup pnpm + uses: pnpm/action-setup@v4.4.0 + with: + version: 10.28.2 + - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: '24.13.0' + node-version: '22.x' + cache: 'pnpm' + cache-dependency-path: dashboard/pnpm-lock.yaml - name: Build dashboard run: | - cd dashboard - npm install pnpm -g - pnpm install - pnpm i --save-dev @types/markdown-it - pnpm run build + pnpm --dir dashboard install --frozen-lockfile + pnpm --dir dashboard run build gate: name: CI Required Gate From 81e9c70d3503f00e5aafb8455962ae49e9c0e1a4 Mon Sep 17 00:00:00 2001 From: Jacobinwwey Date: Mon, 23 Mar 2026 03:54:23 -0500 Subject: [PATCH 3/8] fix(ci): align dashboard node and clarify test dependency sync --- .github/workflows/ci-required-gate.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-required-gate.yml b/.github/workflows/ci-required-gate.yml index 0a948a7ac7..6a2b67823f 100644 --- a/.github/workflows/ci-required-gate.yml +++ b/.github/workflows/ci-required-gate.yml @@ -118,8 +118,10 @@ jobs: python -m pip install --upgrade pip python -m pip install uv - - name: Run pytest suite - run: bash ./scripts/run_pytests_ci.sh ./tests + - name: Run pytest suite (script performs uv sync --dev) + run: | + # scripts/run_pytests_ci.sh includes dependency sync (`uv sync --dev`) before pytest. + bash ./scripts/run_pytests_ci.sh ./tests smoke: name: Smoke Test @@ -185,7 +187,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: '22.x' + node-version: '24.13.0' cache: 'pnpm' cache-dependency-path: dashboard/pnpm-lock.yaml From f1fb138985bc3cca5a454a0f4dfe9013fdfa2c4c Mon Sep 17 00:00:00 2001 From: Jacobinwwey Date: Mon, 23 Mar 2026 04:24:12 -0500 Subject: [PATCH 4/8] fix(ci): relax node pin and broaden docs-only fast path --- .github/workflows/ci-required-gate.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-required-gate.yml b/.github/workflows/ci-required-gate.yml index 6a2b67823f..b44c8b77b8 100644 --- a/.github/workflows/ci-required-gate.yml +++ b/.github/workflows/ci-required-gate.yml @@ -61,7 +61,7 @@ jobs: dashboard_changed=true fi - if [[ ! "$f" =~ ^docs/ && ! "$f" =~ ^README.*\.md$ && ! "$f" =~ ^changelogs/ ]]; then + if [[ ! "$f" =~ ^docs/ && ! "$f" =~ ^docs-[^/]+/ && ! "$f" =~ ^README.*\.md$ && ! "$f" =~ ^changelogs/ ]]; then docs_only=false fi done <<< "$changed_files" @@ -187,7 +187,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: '24.13.0' + node-version: '24' cache: 'pnpm' cache-dependency-path: dashboard/pnpm-lock.yaml From 73d7bbdbdb53455b994733a0610f7c98002bfad9 Mon Sep 17 00:00:00 2001 From: Jacobinwwey Date: Mon, 23 Mar 2026 06:25:03 -0500 Subject: [PATCH 5/8] refactor(ci): extract shared python+uv setup action --- .github/actions/setup-python-uv/action.yml | 36 +++++++++++++++++++++ .github/workflows/ci-required-gate.yml | 37 ++++++---------------- 2 files changed, 46 insertions(+), 27 deletions(-) create mode 100644 .github/actions/setup-python-uv/action.yml diff --git a/.github/actions/setup-python-uv/action.yml b/.github/actions/setup-python-uv/action.yml new file mode 100644 index 0000000000..b7b8ca8ab6 --- /dev/null +++ b/.github/actions/setup-python-uv/action.yml @@ -0,0 +1,36 @@ +name: Setup Python and uv +description: Set up Python, install uv, and optionally sync dependencies. +inputs: + python-version: + description: Python version to install. + required: false + default: "3.12" + sync-deps: + description: Whether to run dependency sync via uv. + required: false + default: "false" + sync-args: + description: Extra arguments passed to `uv sync`. + required: false + default: "" +runs: + using: composite + steps: + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: ${{ inputs.python-version }} + + - name: Install uv + shell: bash + run: | + set -euo pipefail + python -m pip install --upgrade pip + python -m pip install uv + + - name: Sync dependencies + if: ${{ inputs.sync-deps == 'true' }} + shell: bash + run: | + set -euo pipefail + uv sync ${{ inputs.sync-args }} diff --git a/.github/workflows/ci-required-gate.yml b/.github/workflows/ci-required-gate.yml index b44c8b77b8..fb3ecbbb07 100644 --- a/.github/workflows/ci-required-gate.yml +++ b/.github/workflows/ci-required-gate.yml @@ -79,18 +79,12 @@ jobs: - name: Checkout uses: actions/checkout@v6 - - name: Set up Python - uses: actions/setup-python@v6 + - name: Set up Python and uv + uses: ./.github/actions/setup-python-uv with: python-version: '3.12' - - - name: Install uv - run: | - python -m pip install --upgrade pip - python -m pip install uv - - - name: Sync dependencies - run: uv sync --group dev + sync-deps: 'true' + sync-args: '--group dev' - name: Ruff format check run: uv run ruff format --check . @@ -108,16 +102,11 @@ jobs: - name: Checkout uses: actions/checkout@v6 - - name: Set up Python - uses: actions/setup-python@v6 + - name: Set up Python and uv + uses: ./.github/actions/setup-python-uv with: python-version: '3.12' - - name: Install uv - run: | - python -m pip install --upgrade pip - python -m pip install uv - - name: Run pytest suite (script performs uv sync --dev) run: | # scripts/run_pytests_ci.sh includes dependency sync (`uv sync --dev`) before pytest. @@ -133,18 +122,12 @@ jobs: - name: Checkout uses: actions/checkout@v6 - - name: Set up Python - uses: actions/setup-python@v6 + - name: Set up Python and uv + uses: ./.github/actions/setup-python-uv with: python-version: '3.12' - - - name: Install uv - run: | - python -m pip install --upgrade pip - python -m pip install uv - - - name: Sync dependencies - run: uv sync --group dev + sync-deps: 'true' + sync-args: '--group dev' - name: Startup smoke test shell: bash From b1eb751f3ecdbce974c4010ba6e7244541bad0c5 Mon Sep 17 00:00:00 2001 From: Jacobinwwey Date: Mon, 23 Mar 2026 06:39:21 -0500 Subject: [PATCH 6/8] fix(ci): pin uv and clarify skipped gate states --- .github/actions/setup-python-uv/action.yml | 6 ++++- .github/workflows/ci-required-gate.yml | 26 ++++++++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/.github/actions/setup-python-uv/action.yml b/.github/actions/setup-python-uv/action.yml index b7b8ca8ab6..c17ef14771 100644 --- a/.github/actions/setup-python-uv/action.yml +++ b/.github/actions/setup-python-uv/action.yml @@ -5,6 +5,10 @@ inputs: description: Python version to install. required: false default: "3.12" + uv-version: + description: uv version to install. + required: false + default: "0.10.12" sync-deps: description: Whether to run dependency sync via uv. required: false @@ -26,7 +30,7 @@ runs: run: | set -euo pipefail python -m pip install --upgrade pip - python -m pip install uv + python -m pip install "uv==${{ inputs.uv-version }}" - name: Sync dependencies if: ${{ inputs.sync-deps == 'true' }} diff --git a/.github/workflows/ci-required-gate.yml b/.github/workflows/ci-required-gate.yml index fb3ecbbb07..354e37becb 100644 --- a/.github/workflows/ci-required-gate.yml +++ b/.github/workflows/ci-required-gate.yml @@ -189,13 +189,35 @@ jobs: shell: bash run: | set -euo pipefail - if [[ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" == "true" ]]; then - echo "One or more required jobs failed or were cancelled" + declare -A results=( + [changes]="${{ needs.changes.result }}" + [lint]="${{ needs.lint.result }}" + [test]="${{ needs.test.result }}" + [smoke]="${{ needs.smoke.result }}" + [dashboard]="${{ needs.dashboard.result }}" + ) + + has_blocking=false + for job in "${!results[@]}"; do + case "${results[$job]}" in + failure|cancelled) + echo "::error::${job}=${results[$job]} (blocking)" + has_blocking=true + ;; + skipped) + echo "::notice::${job}=skipped (expected for conditional paths)" + ;; + esac + done + + if [[ "$has_blocking" == "true" ]]; then + echo "One or more required jobs failed or were cancelled." exit 1 fi - name: Print job summary run: | + echo "skipped results are expected for docs-only/dashboard-unchanged paths." echo "changes=${{ needs.changes.result }}" echo "lint=${{ needs.lint.result }}" echo "test=${{ needs.test.result }}" From 123e900577e82da2930957eae4b9862b465ac35e Mon Sep 17 00:00:00 2001 From: Jacobinwwey Date: Mon, 23 Mar 2026 06:43:39 -0500 Subject: [PATCH 7/8] fix(ci): harden change detection and smoke fail-fast --- .github/actions/setup-python-uv/action.yml | 11 +++++------ .github/workflows/ci-required-gate.yml | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/.github/actions/setup-python-uv/action.yml b/.github/actions/setup-python-uv/action.yml index c17ef14771..9f3002ec39 100644 --- a/.github/actions/setup-python-uv/action.yml +++ b/.github/actions/setup-python-uv/action.yml @@ -25,12 +25,11 @@ runs: with: python-version: ${{ inputs.python-version }} - - name: Install uv - shell: bash - run: | - set -euo pipefail - python -m pip install --upgrade pip - python -m pip install "uv==${{ inputs.uv-version }}" + - name: Set up uv + uses: astral-sh/setup-uv@v7.6.0 + with: + version: ${{ inputs.uv-version }} + enable-cache: "true" - name: Sync dependencies if: ${{ inputs.sync-deps == 'true' }} diff --git a/.github/workflows/ci-required-gate.yml b/.github/workflows/ci-required-gate.yml index 354e37becb..7378f49039 100644 --- a/.github/workflows/ci-required-gate.yml +++ b/.github/workflows/ci-required-gate.yml @@ -53,9 +53,11 @@ jobs: docs_only=true dashboard_changed=false + has_changed_files=false while IFS= read -r f; do [[ -z "$f" ]] && continue + has_changed_files=true if [[ "$f" == dashboard/* ]]; then dashboard_changed=true @@ -66,6 +68,11 @@ jobs: fi done <<< "$changed_files" + # Empty diff can happen in edge cases; fail closed to avoid skipping core checks. + if [[ "$has_changed_files" == "false" ]]; then + docs_only=false + fi + echo "docs_only=$docs_only" >> "$GITHUB_OUTPUT" echo "dashboard_changed=$dashboard_changed" >> "$GITHUB_OUTPUT" @@ -143,6 +150,16 @@ jobs: trap cleanup EXIT for _ in {1..60}; do + if ! kill -0 "$app_pid" 2>/dev/null; then + app_exit=0 + wait "$app_pid" || app_exit=$? + if [[ "$app_exit" -eq 0 ]]; then + app_exit=1 + fi + echo "Application exited early with status $app_exit" + exit "$app_exit" + fi + if curl -sf http://localhost:6185 >/dev/null 2>&1; then exit 0 fi From a8f3ac4c0706c71713e56a31816d2e8b38e812e1 Mon Sep 17 00:00:00 2001 From: Jacobinwwey Date: Mon, 23 Mar 2026 06:50:32 -0500 Subject: [PATCH 8/8] fix(ci): harden uv sync arg handling in setup action --- .github/actions/setup-python-uv/action.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/actions/setup-python-uv/action.yml b/.github/actions/setup-python-uv/action.yml index 9f3002ec39..184ac67d5b 100644 --- a/.github/actions/setup-python-uv/action.yml +++ b/.github/actions/setup-python-uv/action.yml @@ -36,4 +36,12 @@ runs: shell: bash run: | set -euo pipefail - uv sync ${{ inputs.sync-args }} + sync_args_raw="${{ inputs.sync-args }}" + if [[ -z "$sync_args_raw" ]]; then + uv sync + exit 0 + fi + + # Split configured sync arguments into an array to avoid glob expansion. + read -r -a sync_args <<< "$sync_args_raw" + uv sync "${sync_args[@]}"