ci: add gh-aw agentic workflows #284
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
| # Copyright 2026 ResQ Software | |
| # SPDX-License-Identifier: Apache-2.0 | |
| # | |
| # Comprehensive CI. Three parallel streams converge into the `required` | |
| # aggregator that the org ruleset `default-branch-baseline` watches: | |
| # | |
| # gates — org-wide reusable: dotnet build/test/format + security scan | |
| # client — org-wide reusable: node-ci (install + tsc + vite build) | |
| # client-budget — bundle-size enforcement + wwwroot artifact upload | |
| # | |
| # The org's `node-ci.yml` gained `cache-dependency-path` support in | |
| # resq-software/.github#19, which unblocked viz's subdirectory lockfile | |
| # (src/ResQ.Viz.Web/package-lock.json). Prior hand-rolled inline job | |
| # replaced with the shared reusable so viz inherits the org's | |
| # SHA-pinned actions + step-security/harden-runner + unified timeout | |
| # policy automatically. | |
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| # Workflow-level baseline is intentionally read-only. Each job below | |
| # grants the narrowest scope it actually needs (zizmor: excessive-permissions). | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # ── Backend (C#) + security ────────────────────────────────────────── | |
| gates: | |
| permissions: | |
| contents: read | |
| security-events: write # required.yml dispatches CodeQL + uploads SARIF | |
| pull-requests: read # security-scan reads PR metadata for diff scans | |
| uses: resq-software/.github/.github/workflows/required.yml@23ce94eabddf963835624451e89baca7ac9db541 | |
| with: | |
| lang: dotnet | |
| dotnet-solution: ResQ.Viz.sln | |
| codeql-languages: '["csharp"]' | |
| submodules: recursive | |
| secrets: inherit | |
| # ── Frontend (TypeScript / Vite) — via org reusable ────────────────── | |
| # Consumes node-ci.yml directly (not via required.yml) because | |
| # required.yml dispatches based on `lang:` — viz already calls it as | |
| # `lang: dotnet` for the backend stream, and a second `lang: node` | |
| # dispatch would duplicate the security-scan job. Direct use of | |
| # node-ci.yml skips that. | |
| client: | |
| permissions: | |
| contents: read | |
| uses: resq-software/.github/.github/workflows/node-ci.yml@23ce94eabddf963835624451e89baca7ac9db541 | |
| with: | |
| package-manager: npm | |
| node-version: "22" | |
| working-directory: src/ResQ.Viz.Web | |
| cache-dependency-path: src/ResQ.Viz.Web/package-lock.json | |
| install-cmd: npm ci --legacy-peer-deps | |
| typecheck-cmd: npx tsc --noEmit | |
| build-cmd: npx vite build | |
| timeout-minutes: 10 | |
| secrets: inherit | |
| # ── Bundle-size budget + artifact upload (companion) ───────────────── | |
| # The reusable node-ci doesn't expose bundle outputs or artifact | |
| # upload, so this job rebuilds the client on a cache-warm runner | |
| # (~15 s) and enforces the ceiling. Depends on `client` so a failing | |
| # tsc/build stops the pipeline before wasting a second build. | |
| # | |
| # Bundle budgets in bytes. Current build is ~776 KB JS + 24 KB CSS; | |
| # 800 KB / 50 KB gives measured headroom. Bumping is a policy decision. | |
| client-budget: | |
| needs: client | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| defaults: | |
| run: | |
| working-directory: src/ResQ.Viz.Web | |
| env: | |
| BUNDLE_JS_BUDGET_BYTES: '819200' # 800 KB | |
| BUNDLE_CSS_BUDGET_BYTES: '51200' # 50 KB | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2 | |
| with: | |
| egress-policy: audit | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| # Don't leave a credentialed `.git/config` on disk; the artifact | |
| # upload step below would otherwise risk packing the token | |
| # alongside wwwroot/ (zizmor: artipacked). | |
| persist-credentials: false | |
| - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| cache-dependency-path: src/ResQ.Viz.Web/package-lock.json | |
| - name: Install dependencies | |
| run: npm ci --legacy-peer-deps | |
| - name: Run client tests | |
| # Vitest unit tests for the host-side WebGPU primitives (ray | |
| # packing roundtrip, LidarScan validation). No browser / GPU — | |
| # ~350 ms on a cache-warm runner. Stops the pipeline before | |
| # the bundle measure if a primitive regressed. | |
| run: npm test | |
| - name: Build (cache-warm) | |
| run: npx vite build | |
| - name: Measure + enforce bundle-size budget | |
| run: | | |
| set -euo pipefail | |
| js=$(stat -c%s wwwroot/assets/index-*.js) | |
| css=$(stat -c%s wwwroot/assets/index-*.css) | |
| { | |
| echo "## Bundle size" | |
| echo "" | |
| echo "| Asset | Bytes | KB | Budget KB | Used |" | |
| echo "|---|---:|---:|---:|---:|" | |
| echo "| JS | $js | $((js / 1024)) | $((BUNDLE_JS_BUDGET_BYTES / 1024)) | $((js * 100 / BUNDLE_JS_BUDGET_BYTES))% |" | |
| echo "| CSS | $css | $((css / 1024)) | $((BUNDLE_CSS_BUDGET_BYTES / 1024)) | $((css * 100 / BUNDLE_CSS_BUDGET_BYTES))% |" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| if [ "$js" -gt "$BUNDLE_JS_BUDGET_BYTES" ]; then | |
| echo "::error::JS bundle $js bytes exceeds $BUNDLE_JS_BUDGET_BYTES budget" | |
| exit 1 | |
| fi | |
| if [ "$css" -gt "$BUNDLE_CSS_BUDGET_BYTES" ]; then | |
| echo "::error::CSS bundle $css bytes exceeds $BUNDLE_CSS_BUDGET_BYTES budget" | |
| exit 1 | |
| fi | |
| echo "::notice::JS $(((js * 100) / BUNDLE_JS_BUDGET_BYTES))% of budget · CSS $(((css * 100) / BUNDLE_CSS_BUDGET_BYTES))% of budget" | |
| - name: Advisory audit (non-blocking) | |
| # Surfaces high-severity npm advisories for visibility without | |
| # blocking PRs. Socket Security (via security-scan.yml) is the | |
| # enforcing gate for supply chain. | |
| continue-on-error: true | |
| run: npm audit --audit-level=high --legacy-peer-deps | |
| - name: Upload build artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: viz-wwwroot-${{ github.sha }} | |
| path: src/ResQ.Viz.Web/wwwroot/ | |
| retention-days: 7 | |
| # ── Aggregate gate watched by the org ruleset ─────────────────────── | |
| required: | |
| name: required | |
| runs-on: ubuntu-latest | |
| needs: [gates, client, client-budget] | |
| permissions: {} # pure aggregator: only reads needs.*.result env vars | |
| if: always() | |
| steps: | |
| - name: Assert all upstream jobs passed or were skipped | |
| env: | |
| GATES: ${{ needs.gates.result }} | |
| CLIENT: ${{ needs.client.result }} | |
| BUDGET: ${{ needs.client-budget.result }} | |
| run: | | |
| set -eu | |
| ok() { case "$1" in success|skipped|"") return 0;; *) return 1;; esac; } | |
| fail=0 | |
| ok "$GATES" || { echo "::error::gates=$GATES"; fail=1; } | |
| ok "$CLIENT" || { echo "::error::client=$CLIENT"; fail=1; } | |
| ok "$BUDGET" || { echo "::error::client-budget=$BUDGET"; fail=1; } | |
| if [ "$fail" -ne 0 ]; then exit 1; fi | |
| echo "ok: gates=$GATES client=$CLIENT budget=$BUDGET" |