Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0a27dc6
feat(ops): FDD-OPS-001 lines 1+2 — eliminate stale-code-in-workers drift
Apr 23, 2026
a05b370
fix(sec): FDD-SEC-001 — reject squad_key with invalid chars (HTTP 422)
Apr 23, 2026
7905bb0
feat(ops): FDD-OPS-001 lines 3+4 — snapshot drift monitor + deploy wo…
Apr 23, 2026
0abc7c0
test(frontend): Sprint 1.2 step 1 — Vitest + RTL + MSW + Zod foundation
Apr 23, 2026
bb8b445
docs(backfill): FDD-OPS-002 — full Jira description backfill SHIPPED
Apr 23, 2026
0386267
test(frontend): Sprint 1.2 step 2 — Playwright setup + first E2E smoke
Apr 23, 2026
32062c9
test(frontend): Sprint 1.2 step 3 — Zod contracts for 6 metric endpoints
Apr 23, 2026
38accfd
feat(sec): Sprint 1.2 step 5 — Gitleaks secret scanning (pre-commit)
Apr 23, 2026
e6f5c78
test(frontend): Sprint 1.2 step 4 — axe-core a11y gate on 3 critical …
Apr 23, 2026
c631370
ci: Sprint 1.2 step 6 — root-level GitHub Actions with 4 blocking gates
Apr 23, 2026
06266f6
docs(sec): secret rotation runbook + make targets + AI-chat guard in …
Apr 23, 2026
17b2391
fix(ci): add missing @vitest/coverage-v8 dep for test:coverage script
Apr 23, 2026
b7b0cb5
fix(ci): ESLint flat config migration + surface 3 real TS bugs CI found
Apr 23, 2026
6c2c590
test(frontend): FDD-DSH-070 fechamento — regression tests + coverage …
Apr 24, 2026
6cb1248
test(frontend): FDD-DSH-033 fechamento — a11y gate on 10 dashboard ro…
Apr 24, 2026
211d23a
chore(dx): PR#1 — doctor + verify-dev scripts for dev onboarding
Apr 24, 2026
83bd04c
fix(perf): partial index on metrics_snapshots — fixes /metrics/home 5…
Apr 27, 2026
349bcba
docs(quality): close the perf/scale gap exposed by 2026-04-24 incident
Apr 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
#
# PULSE pre-commit hook
# --------------------------------------------------------------------------
# Runs secret-scanning on the staged diff before every commit.
# If a secret is detected, the commit is rejected and the offending
# finding is printed to stderr.
#
# Enable once per clone:
# git config core.hooksPath .githooks
#
# Bypass (only if you are ABSOLUTELY sure it is a false positive and you
# cannot add an allowlist entry in time):
# git commit --no-verify
# (Prefer fixing .gitleaks.toml over bypassing the hook.)
# --------------------------------------------------------------------------

set -euo pipefail

# ------------------------------------------------------------------ colors
if [ -t 2 ]; then
RED=$'\033[31m'; YEL=$'\033[33m'; GRN=$'\033[32m'; DIM=$'\033[2m'; RST=$'\033[0m'
else
RED=""; YEL=""; GRN=""; DIM=""; RST=""
fi

# ------------------------------------------------------------------ gitleaks
if ! command -v gitleaks >/dev/null 2>&1; then
echo "${YEL}[pre-commit]${RST} gitleaks not installed — skipping secret scan."
echo " Install: ${DIM}brew install gitleaks${RST}"
echo " Without it, nothing prevents an API key from entering git history."
exit 0
fi

REPO_ROOT="$(git rev-parse --show-toplevel)"
CONFIG="${REPO_ROOT}/.gitleaks.toml"

CONFIG_ARGS=()
if [ ! -f "${CONFIG}" ]; then
echo "${YEL}[pre-commit]${RST} .gitleaks.toml not found at repo root — running with defaults."
else
CONFIG_ARGS=(--config "${CONFIG}")
fi

echo "${DIM}[pre-commit] scanning staged changes with gitleaks...${RST}"

# `protect --staged` only scans what is in the staged diff — fast and
# scoped to what is about to be committed.
if ! gitleaks protect --staged --redact "${CONFIG_ARGS[@]}" --verbose 2>&1; then
echo ""
echo "${RED}✖ gitleaks found one or more secrets in your staged changes.${RST}"
echo ""
echo "Options:"
echo " 1. Remove the secret from the staged files and rotate it if it was real."
echo " 2. If it is a false positive, add an allowlist entry in .gitleaks.toml"
echo " and commit the config change first."
echo " 3. As a last resort (e.g. offline work, CI will catch it): commit with"
echo " ${DIM}git commit --no-verify${RST} — but you are on the hook if it leaks."
echo ""
exit 1
fi

echo "${GRN}✓ no secrets detected${RST}"
42 changes: 42 additions & 0 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# GitHub Actions workflows — root vs pulse/

This repo has workflows in **two** locations. The split is intentional.

## `/.github/workflows/` (this directory — **ACTIVE**)

Runs on every push + PR. These are the real gates enforced by branch
protection. Scope is the full monorepo (root-level).

| File | Trigger | What it does |
|---|---|---|
| `ci.yml` | PR + push to main/develop | Gitleaks secrets scan, ESLint + TSC pulse-web, Vitest (139+ tests incl. contract), Vite build |
| `e2e-a11y.yml` | manual + nightly cron | Playwright smoke + axe-core a11y. No-op until backend CI infra is wired — see testing-playbook.md §8.8 |

## `/pulse/.github/workflows/` (sub-directory — **DORMANT**)

Workflows prepared for the day `pulse/` is extracted into its own git
repo (SaaS productization). They expect `pulse/` to be the repo root, so
`cd packages/...` works directly. They do **not** run today because
GitHub Actions only looks at `.github/workflows/` at the actual repo
root.

| File | Purpose |
|---|---|
| `ci.yml` | Full backend + frontend CI (Jest, Pytest with anti-surveillance gate, Docker builds) — runs when pulse/ is standalone |
| `deploy.yml` | Release rollout template (manual dispatch) — TODO steps for kubectl/ECS |

When you extract `pulse/` to its own repo, `git mv pulse/.github/workflows/*.yml
.github/workflows/` and delete these root workflows.

## Branch protection (set once in GitHub Settings)

For `ci.yml` to actually block merges, turn on branch protection for
`main` (and `develop` if used) with these required status checks:

- `Secrets scan (gitleaks)`
- `Lint & typecheck (pulse-web)`
- `Unit tests (pulse-web Vitest)`
- `Build (pulse-web Vite)`

UI path: Settings → Branches → Branch protection rules → Add rule →
"Require status checks to pass before merging" → pick the 4 above.
173 changes: 173 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
name: CI

# Root-level GitHub Actions workflow. Runs on every PR and push to
# main/develop. Matches the Sprint 1.2 quality gates established locally
# (Vitest unit + contract, ESLint, Gitleaks secrets scan) so regressions
# are caught before merge.
#
# Scope note: THIS workflow is frontend + repo-wide only. Backend CI
# (pulse-api Jest, pulse-data Pytest, anti-surveillance gate, Docker builds)
# lives at pulse/.github/workflows/ci.yml and runs when pulse/ is extracted
# into its own repo. See .github/workflows/README.md in the commit message
# for the divided-ownership rationale.
#
# E2E + a11y specs need a live backend (docker compose) and are triggered
# manually via the separate `.github/workflows/e2e-a11y.yml` workflow
# (workflow_dispatch). Once backend CI infra is ready, they'll join this
# pipeline.

on:
pull_request:
branches: [main, develop]
push:
branches: [main, develop]

concurrency:
# Cancel in-progress runs for the same branch on new pushes, but NEVER
# cancel runs on main/develop (they produce artifacts and deploy signals).
group: ci-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }}

env:
NODE_VERSION: "20"

permissions:
contents: read

jobs:
# --------------------------------------------------------------------------
# Secrets scanning — runs first, fast, against full repo (not just diff)
# --------------------------------------------------------------------------
secrets-scan:
name: Secrets scan (gitleaks)
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout (full history)
uses: actions/checkout@v4
with:
# Full history so gitleaks can scan all commits, not just the shallow diff.
fetch-depth: 0

- name: Run gitleaks
# Pinned to a major tag for reproducibility. The action uses the
# .gitleaks.toml at repo root (our config with PULSE-specific rules
# and allowlist for .env / lockfiles / tests/fixtures).
uses: gitleaks/gitleaks-action@v2
env:
# GITHUB_TOKEN is used only to comment on PRs if findings exist —
# no write permissions needed beyond that.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_CONFIG: ./.gitleaks.toml

# --------------------------------------------------------------------------
# Frontend lint (ESLint + TypeScript)
# --------------------------------------------------------------------------
lint-web:
name: Lint & typecheck (pulse-web)
runs-on: ubuntu-latest
timeout-minutes: 10
defaults:
run:
working-directory: pulse/packages/pulse-web
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
cache-dependency-path: pulse/packages/pulse-web/package-lock.json

- name: Install pulse-shared (sibling dep)
working-directory: pulse/packages/pulse-shared
run: npm ci

- name: Install pulse-web
run: npm ci

- name: ESLint
run: npm run lint

- name: TypeScript (strict, no emit)
# `tsc -b` validates the project references tree.
run: npx tsc -b --noEmit

# --------------------------------------------------------------------------
# Frontend unit tests (Vitest) — includes component + hook + contract +
# anti-surveillance meta-test. 139+ tests as of Sprint 1.2 step 3.
# --------------------------------------------------------------------------
test-unit-web:
name: Unit tests (pulse-web Vitest)
runs-on: ubuntu-latest
timeout-minutes: 10
defaults:
run:
working-directory: pulse/packages/pulse-web
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
cache-dependency-path: pulse/packages/pulse-web/package-lock.json

- name: Install pulse-shared (sibling dep)
working-directory: pulse/packages/pulse-shared
run: npm ci

- name: Install pulse-web
run: npm ci

- name: Vitest (run mode, coverage)
# `--run` = no watch, `--coverage` = v8 coverage, output to coverage/.
# Contract tests skip cleanly when backend is offline (CI has none).
run: npm run test:coverage

- name: Upload coverage
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-pulse-web
path: pulse/packages/pulse-web/coverage/
retention-days: 7

# --------------------------------------------------------------------------
# Frontend build (Vite) — catches type errors that only surface at build time
# --------------------------------------------------------------------------
build-web:
name: Build (pulse-web Vite)
runs-on: ubuntu-latest
timeout-minutes: 10
needs: [lint-web, test-unit-web]
defaults:
run:
working-directory: pulse/packages/pulse-web
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
cache-dependency-path: pulse/packages/pulse-web/package-lock.json

- name: Install pulse-shared + build (generates dist/ used by pulse-web)
working-directory: pulse/packages/pulse-shared
run: |
npm ci
npm run build

- name: Install pulse-web
run: npm ci

- name: Build
run: npm run build

- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: pulse-web-dist
path: pulse/packages/pulse-web/dist/
retention-days: 3
109 changes: 109 additions & 0 deletions .github/workflows/e2e-a11y.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
name: E2E + A11y (manual)

# Playwright E2E smoke + axe-core a11y gate. These tests need a live
# backend (docker compose) and are heavier to run, so they're NOT wired
# as blocking PR gates yet — promote to ci.yml once the backend-in-CI
# infrastructure is ready (docker compose up, migrations, DevLake seed,
# secret plumbing).
#
# Triggered:
# - workflow_dispatch (manually from the Actions tab)
# - schedule (nightly at 03:00 UTC — proves the suite stays green)
#
# When green, the specs under tests/e2e/ exercise:
# - home-dashboard-smoke.spec.ts (1 smoke E2E)
# - a11y/{home,dora,cycle-time}.spec.ts (WCAG 2.1 AA gate, 3 pages)

on:
workflow_dispatch:
inputs:
suite:
description: "Which suite to run"
required: true
default: "all"
type: choice
options:
- all
- smoke
- a11y
schedule:
# 03:00 UTC daily — off-hours for the ops team.
- cron: "0 3 * * *"

concurrency:
group: e2e-a11y-${{ github.ref }}
cancel-in-progress: true

env:
NODE_VERSION: "20"

permissions:
contents: read

jobs:
playwright:
name: Playwright (${{ github.event.inputs.suite || 'all' }})
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: pulse/packages/pulse-web
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
cache-dependency-path: pulse/packages/pulse-web/package-lock.json

- name: Install pulse-shared
working-directory: pulse/packages/pulse-shared
run: npm ci

- name: Install pulse-web
run: npm ci

- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('pulse/packages/pulse-web/package-lock.json') }}

- name: Install Playwright browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: npx playwright install --with-deps chromium firefox

# TODO: start backend (docker compose up -d) once CI-backend infra is wired.
# Until then, the specs detect an unreachable dev server and skip gracefully
# (see devServerIsDown helper). The run still "passes" but effectively
# no-ops — surfacing a warning in the summary below keeps it honest.
- name: Skip notice (backend not yet provisioned in CI)
run: |
echo "::warning ::E2E + a11y specs will skip because no backend is running in this CI context."
echo "::warning ::Promote this workflow once docker compose is wired. See testing-playbook.md §8.8."

- name: Playwright — smoke
if: github.event.inputs.suite == 'smoke' || github.event.inputs.suite == 'all' || github.event.inputs.suite == ''
run: npx playwright test tests/e2e/platform --project=chromium

- name: Playwright — a11y
if: github.event.inputs.suite == 'a11y' || github.event.inputs.suite == 'all' || github.event.inputs.suite == ''
run: npm run test:a11y

- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: pulse/packages/pulse-web/playwright-report/
retention-days: 14

- name: Upload test-results (traces, screenshots)
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-test-results
path: pulse/packages/pulse-web/test-results/
retention-days: 7
Loading
Loading