Skip to content

ci: add gh-aw agentic workflows #284

ci: add gh-aw agentic workflows

ci: add gh-aw agentic workflows #284

Workflow file for this run

# 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"