CONTRIBUTING

docs/CONTRIBUTING.md · The full contributor map

← Onboarding

Contributing to ELAS

The one page to read before you open a PR. It ties together how we build (spec-driven), how we test, and how CI/CD ships it. Deep references are linked inline — this is the map, not the territory.


0. The five rules (non-negotiable)

Every change respects these (full detail in BUILDING.md):

  1. Multi-tenant — everything scoped to an Org (orgId); never leak across tenants.
  2. Permission- & license-governed — the server decides who can do what (libs/access.ts, ACCESS.md). The UI only mirrors it.
  3. Agent-ready — if it's an action, expose it as an MCP tool (libs/artie/tools.ts) so Artie can do it too, under the same governance.
  4. Mobile-responsive — works at 375px or it's not done.
  5. Reuse, never recreate — build on existing components/hooks/libs; don't fork a second version of something that exists.

Golden rule: the API/server is the source of truth. npm run build must be green and npm test must pass before you commit. Branch off main — don't commit straight to it.


1. The workflow: spec-driven

Full SOP: SESSION_SUMMARY.md. The short version:

Step 0 — Triage

npx tsx scripts/triage.ts "the change you want to make"

It tells you backend / frontend / both, whether a migration is likely, and which path to follow.

  • Frontend-only (copy, layout, a component, what's shown) → skip the spec. Edit the screen/component, gate with useAccess().can(...), verify at 375px. Done.
  • Backend / both (new data, fields, rules, API, permissions, jobs) → write a spec first (next step).

Step 1 — Write the spec (specs/<feature>/)

One folder, three files — the reviewable source of truth:

specs/<feature>/
  requirements.md   # WHAT & WHY + numbered, testable acceptance criteria
  design.md         # data model + API contract + rules→location + UI hints
  tasks.md          # the ordered build checklist

See specs/org-chart/ and specs/timesheet-settings/ for worked examples, and SESSION_SUMMARY.md for templates.

Step 2 — Build it

Implement what the spec describes. For most new record types this is the ERP-resource shortcut (no migration): register in ERP_RESOURCES → add the type in ResourceTypeMap → map the module in RESOURCE_MODULE. Mechanics + the agent-tool/MCP/integration recipes are all in BUILDING.md.

Step 3 — Test it

Write/extend unit tests for any pure logic you added (see §2). npm test is now a CI gate.

Step 4 — Migrate (only if you added drizzle/000N_*.sql)

npx tsx scripts/pg/migrate.ts   # privileged role via MIGRATE_DATABASE_URL — see BUILDING.md §G

Step 5 — Open a PR

The reviewable unit is the spec + the diff. Migrations get extra scrutiny.


2. Testing

We use Vitest. Tests live next to the code as *.test.ts.

npm test              # run the whole suite once (CI uses this)
npm run test:watch    # watch mode while developing
npm run test:coverage # coverage report (text + html + lcov)

What to test

Pure logic in libs/ — functions that are deterministic given their inputs (no DB, network, or session). That's where bugs hide and where tests pay for themselves: hierarchy/cycle math, permission resolution (can/permFor), date/period boundaries, ingest transforms, money/overtime/accrual calculators, rollups.

Don't unit-test DB-bound or route handlers here — those are covered by the spec's acceptance criteria + the typecheck/build. If a function needs the DB, extract the pure core and test that.

The pattern

import { describe, it, expect } from "vitest";
import { availableParents } from "@/libs/orgchart/hierarchy";

describe("availableParents", () => {
  it("excludes the node and all its descendants", () => {
    expect(availableParents(people, "1").map((p) => p.id)).toEqual(["99"]);
  });
});
  • Import real modules via the @/... alias (configured in vitest.config.ts).
  • Derive expected values from the implementation, not from guesses.
  • A pure function that's hard to test is usually a design smell — pull the logic out.

See libs/orgchart/hierarchy.test.ts and libs/access.test.ts for reference.


3. CI/CD — what runs, and when

Two GitHub Actions workflows. The app lives at the repo root (this repo is the app — there is no shipfast/ subdir; don't add working-directory:).

CI — .github/workflows/ci.yml

Runs on every PR and every push to main:

install → spec:check → lint (advisory) → npm test → build → typecheck

The hard gates are spec:check (committed code must match the spec generator), npm test, build, and typecheck — any of those red makes the PR red. Lint is advisory (continue-on-error) for now: there's a pre-existing ESLint backlog, so it's surfaced but doesn't block. Keep the hard gates green.

CD — .github/workflows/deploy-web.yml

Runs on push to main (path-filtered to app code). It is gated: a verify job (spec:check + advisory lint + npm test) must pass before the image is built — and the image build itself runs next build (which typechecks) — so a red commit can never reach an environment. A merge auto-deploys QC only; staging and prod are one-click promotions of the same tested image (promote-staging.yml / promote-prod.yml, or the in-app /admin/releases screen).

verify (spec:check, lint(advisory), test)
  └─ build image (next build = typecheck) → push to ECR (elas-web :latest+:<sha>) → App Runner rolls QC
       └─ [one-click] Promote to staging (retag :staging) → [one-click] Promote to prod (retag :prod)
  • Auth is GitHub OIDC → the elas-gha-deploy role (no long-lived AWS keys).
  • App Runner has AutoDeploy on, so the ECR push itself starts the rollout; the explicit start-deployment step tolerates the "already deploying" race instead of failing.
  • Runtime config is SSM Parameter Store (/elas/web/*) + Secrets Manager — not a .env. Add a new required var to SSM (+ Terraform env_keys), not just .env.local.
  • If your change adds a migration, apply it before the deploy lands (CI/deploy do not run migrations) — see BUILDING.md §G.

Full hosting runbook: AWS_MIGRATION.md.

Releases — .github/workflows/release-please.yml

Versioning + release notes are automated by release-please. On push to main it keeps a rolling "chore(main): release X.Y.Z" PR that bumps package.json and regenerates CHANGELOG.md from your commit messages; merging that PR tags vX.Y.Z and publishes a GitHub Release with auto-generated notes. (Deploy is fully separate — merging the release PR stamps a version but ships nothing; deploy-web.yml auto-deploys QC on every push to main, and staging/prod are one-click promotions.)

  • Write Conventional Commits so your change shows up: feat: … → minor, fix: … → patch, feat!: / BREAKING CHANGE: → major. Other prefixes are ignored for versioning. (Scopes are fine: feat(orgchart): ….)
  • Roadmap (specced, not yet built): curated release-notes/vX.Y.Z.md → a public /releases page + in-app "What's new" (specs/release-pipeline).

4. Local setup

npm install
npm run dev        # http://localhost:3000  (dev-auth bypass lands you in the demo org)
npm run dev:mock   # same app on MSW fixtures — NO DB/backend (mocks/fixtures.ts)
npm run storybook  # components in isolation at http://localhost:6006
npm run build      # production build — must be green before commit
npm test           # unit tests — must pass before commit

For pure-UI work you don't need a database: dev:mock (sets NEXT_PUBLIC_API_MOCKING=enabled, boots MSW from mocks/handlers.ts) and Storybook both render on the shared fixtures in mocks/fixtures.ts. Add a *.stories.tsx next to a component to isolate it. Minimal .env.local and the dev-loop flags are documented in the README §8.


5. Definition of done

  • Triaged; spec written + reviewed if it's backend/both.
  • Org-scoped; server enforces permission; UI mirrors with useAccess.
  • Action → an MCP tool with a permission (+ undo if reversible).
  • Discoverable: new screen wired into the sidebar + lib/features.ts.
  • Unit tests added for new pure logic; npm test green.
  • Mobile-checked at 375px.
  • npm run build green; committed on a branch (not main) with a Co-Authored-By trailer.
  • Docs updated if you added a pattern/permission/module.