Loading ticks…
Pre-commit checks and CI: what runs, where, and why
At a glance
Every change to the code is checked automatically. If a check fails, the change does not go live.
| When | What is checked, automatically |
|---|---|
| Every commit | No passwords or keys in the code. Code is tidy and follows our rules. Data files are never edited by hand. |
| Before any change is accepted | Five required checks: the code has no errors, 65 test suites pass, the app builds, no secret anywhere in the history, the sample data rebuilds identically. |
| Every night | The whole database is rebuilt from scratch and tested signed in as each role: driver, supervisor, manager, executive. |
What that gives us:
- No secrets leak. Keys are blocked before they leave a laptop, then scanned again in the cloud and in the finished app.
- Nothing broken goes live. The five checks are required on every change. No one can skip them; an emergency override is logged.
- People see only what they are allowed to. The nightly tests try to break the access rules directly, as each role. Every attempt must fail.
- The numbers are right. 1,828 checks confirm each dashboard figure matches the records behind it.
- Security flaws are flagged. Every change is scanned for unsafe code and for packages with known vulnerabilities, and package updates arrive monthly.
The rest of this page is the full catalogue: every check, what it is, and what it protects against.
Background
Prepared 27 Oct 2026, ready to implement once the CI/CD workflow lands in the lucent-axis GitHub
organization. Modelled on Matt's Evidence Lab (dividor/evidencelab): its .pre-commit-config.yaml
and .github/workflows/ci.yml were read in full and every hook was weighed against this repository.
Built 28 Oct 2026: .pre-commit-config.yaml, the three guards in scripts/guards/, the CI jobs,
.github/dependabot.yml and the nightly database job (.github/workflows/nightly-db.yml, running
scripts/ci/nightly_db.sh). This page stays the catalogue; "What was built" below says where the
build departs from the plan.
The short version
- Yes, GitHub Actions, in the lucent-axis organization. It is where the code is moving (SOC 2
OW-1), it already runs our CI on
lucent-axis/scopein about three minutes, and it is what the SOC 2 change-management controls (SC-4, SC-5, SC-7) expect to see as evidence. Vercel keeps deploying; Actions decides whether a change is allowed in. - Three layers, the same checks, different speeds. On commit: only what finishes in a few seconds. On push: the slower code checks. In GitHub Actions: everything, on every pull request, and that is the one that counts.
- Pre-commit is a courtesy, CI is the lock. Anyone can skip a local hook with
--no-verify. The same rule the app follows (the tile is a courtesy, the database policy is the lock) applies here: the hooks save a wasted push, the required checks onmainare what actually stop a bad change. - Use Matt's tool, the
pre-commitframework, so both teams share one way of working. One.pre-commit-config.yamldrives the local hooks, and CI runs the same file withpre-commit run --all-files, exactly as Evidence Lab does. It handles our Python scripts and our TypeScript in one place, which Husky and lint-staged would not. - Two failures a hook would have stopped: on 26 Oct two pushes in a row went red on
main(a lint error, then a missing package). A push-time hook running lint and the type-check catches both before they leave the laptop.
The three layers
| Layer | When it runs | Time budget | What belongs there |
|---|---|---|---|
On commit (pre-commit) |
Every git commit, on the files being committed |
Under 10 seconds | File hygiene, secrets, lint on changed files, the project's own guards |
On push (pre-push stage) |
Every git push |
About 2 minutes | Type-check, all 65 unit suites, the data suites when the generator changed |
| GitHub Actions | Every pull request and every push to main |
About 5 minutes | Everything above, plus the build, dependency scanning and the full-history secret scan |
| Database checks | Nightly and on demand, in a throwaway database | 15-20 minutes | The verify_*.sql scripts and qa_watchlist.mts, which need a loaded database |
The database layer is the equivalent of Evidence Lab's integration job, which starts its whole stack
in Docker. It must never point at the shared database: verify_demo_reset.sql commits, and a reset
reaches every table hanging off its foreign keys (see "ONE DATABASE" in CLAUDE.md).
What Evidence Lab does, and what we take from it
| Evidence Lab check | What it does | For us |
|---|---|---|
| pre-commit-hooks (whitespace, end of file, YAML, JSON, TOML, merge markers, line endings, large files) | Basic file hygiene | Take, with data/csv/ excluded: the CSVs are generated, committed, and the largest is 50 MB |
| Black, isort, flake8 | Python formatting, import order, lint | Take as Ruff, one fast tool doing all three, on scripts/**/*.py only |
| mypy | Python type-checking | Leave: our Python is offline data scripts; the type risk is in the TypeScript, which tsc covers |
| Bandit | Python security scan | Leave: the scripts never take user input or run on a server |
| Hadolint, Trivy | Dockerfile lint, container image scan | Leave: we have no containers; Vercel builds the app |
| detect-secrets and Gitleaks | Secret scanning, twice | Take Gitleaks only, pinned to the version CI already runs. One scanner, no baseline file to maintain |
| code-metrics (complexity, maintainability) | Fails on overly complex files | Take later, report-only: several dashboard components would fail on day one |
| pre-commit run in CI | The local hooks re-run on the server | Take: one config file, two places |
| npm audit, pip-audit | Known-vulnerable packages | Take npm audit, blocking on critical, reporting high |
| CycloneDX SBOM | A list of every package shipped | Take: it is the vendor evidence a SOC 2 auditor and an enterprise buyer ask for |
| Dependabot plus auto-merge | Monthly update pull requests | Take Dependabot for npm and GitHub Actions; auto-merge only after branch protection exists |
| Integration tests in Docker | Whole stack started in CI | Take the idea: Supabase started in the runner, migrations applied, data loaded, verify_*.sql run |
permissions: {} at the top of the workflow |
Each job gets only the access it needs | Take |
Two things in Evidence Lab we would not copy: its security scans all run with continue-on-error,
so they can never fail a change, and trivy-action@master follows a moving branch rather than a
pinned version.
Layer 1: on commit
| Check | What it is | Why it matters here | What it protects against | Status |
|---|---|---|---|---|
| Trailing whitespace, end-of-file newline | Tidies changed text files | Clean diffs in review | Whitespace noise hiding the real change in a pull request | New |
| Mixed line endings (fix to LF) | Forces Unix line endings | The CSVs and SQL are LF by rule; load.sh reads them as such |
A Windows editor turning a file into CRLF and breaking a load or a byte-identical regeneration | New |
| Check YAML, JSON | Parses each changed config file | Workflows, brand.json, tenant files |
A broken workflow or tenant file found only when the build fails | New |
| Merge-conflict markers | Refuses <<<<<<< left in a file |
Two sessions often share one worktree | A half-resolved merge reaching main |
New |
Large files (2 MB, data/csv/ excluded) |
Refuses big binaries | Photos, exports and dumps get dropped in by accident | A 50 MB export committed, permanently in history | New |
| Private key detection | Refuses a committed private key | Supabase and AWS keys are in .env.local |
A key in git history, which means rotating it (SOC 2 SC-1) | New |
| Gitleaks on staged changes | Secret scan of what is about to be committed | CI scans after the push; this scans before | A secret leaving the laptop at all | New locally, in CI already |
ESLint on changed .ts/.tsx |
The same rules CI runs | Lint failures turned main red on 26 Oct |
A push that fails CI for a missing Link or an unused import |
New locally, in CI already |
Ruff on changed scripts/**/*.py |
Python lint (and later, formatting) | The generator and suites are the data set's source of truth | Undefined names and dead code in the generator | New |
| Generated CSV guard | Refuses a commit that changes data/csv/ unless the generator, scenarios.py or window.py changes too |
HARD CONSTRAINT 5: never hand-edit the CSVs | A hand edit that the next regeneration silently undoes | New, project script to write |
| Demo date guard | Checks that the four places holding "today" agree: scripts/window.py, app.today() in the latest migration that defines it, TODAY in packages/core/src/config.ts, FROM/TO in packages/core/src/cofi/facts.ts |
HARD CONSTRAINT 1: three languages, nothing can import from the others | The dashboard reading as historical, or one screen counting a different window from the rest | New, project script to write |
| New table guard | A migration that creates a table must also enable row-level security and call app.add_audit_columns() |
RLS is the access boundary; every table carries the audit columns | A table exposed through the API with no policy (SOC 2 SC-4); verify_audit_columns.sql failing weeks later |
New, project script to write |
| Brand check | apply-tenant.mjs --check-all when anything under tenants/ changes |
Every client build binds a tenant | A tenant with an unreadable colour or a missing icon found at a client's deploy | Exists as a command, new as a hook |
Layer 2: on push
| Check | What it is | Why it matters here | What it protects against | Status |
|---|---|---|---|---|
| Apply the default tenant | node scripts/brand/apply-tenant.mjs |
The app imports gitignored generated files | palette.test.mts failing on a fresh checkout (it does today until this runs) |
In CI already |
| Route types and type-check | next typegen, then tsc --noEmit in apps/web |
Business rules live in packages/core and are shared by every screen |
A changed function signature breaking a screen nobody opened | In CI already |
| Unit suites | npm test: 65 suites, about 85 seconds |
The rules are the product; see the catalogue below | A rule changing without anyone deciding it should | In CI already |
| Data suites, when the generator changed | audit.py, verify.py, coherence.py, precheck_ddl.py: 20 seconds together |
The data set is the demo | A generator change producing broken foreign keys, wrong vocabulary or incoherent records | Run by hand today |
Layer 3: GitHub Actions
| Job | What it is | Why it matters here | What it protects against | Status |
|---|---|---|---|---|
| pre-commit | pre-commit run --all-files |
Same file as the laptops | Someone who never installed the hooks, or skipped them | New |
| check | Type-check, lint, unit suites | The main gate today | Broken code reaching main |
In place (.github/workflows/ci.yml) |
| build | next build with placeholder environment values |
Vercel builds the app; this builds it first | A build that only fails on Vercel, like the missing @scope/spotter dependency on 26 Oct |
New |
| data | Regenerate, run the four suites, then git diff --exit-code data/ |
Regeneration must be byte-identical (determinism broke once, silently, through hash()) |
A generator that no longer reproduces the committed data set | New |
| add-on removable | check-removable.mjs --build: delete every add-on in a scratch copy, then build |
Client repos start without Spotter | Core code quietly depending on an add-on | Exists as a command, new in CI (only when addons/ or packages/spotter change) |
| secrets | Gitleaks over the whole history | Catches anything a laptop missed | A secret already pushed | In place |
| dependencies | npm audit (critical blocks, high reported) and a CycloneDX SBOM kept with the run |
SOC 2 SC-7 and vendor questionnaires | Shipping a package with a known exploit; having no list of what we ship | New |
| Dependabot | Monthly update pull requests for npm and GitHub Actions, majors ignored | Keeps pinned versions current | Old, vulnerable packages and actions | New (.github/dependabot.yml) |
| CodeQL | GitHub's code scanning for TypeScript | SOC 2 SC-7 | Injection and unsafe patterns in the app code | Not bought: Semgrep CE instead, see "CodeQL: the decision" below |
Layer 4: database checks
Run in a throwaway database inside the runner (supabase start, all migrations, load.sh --reset,
the seeds in the order CLAUDE.md gives), nightly and before any client demo. Never against the
shared database.
| Check | What it protects against |
|---|---|
verify.sql |
The planted storylines drifting (7 LTI cases in 2026, the Castleton decline), and any public table without RLS (SOC 2 SC-4) |
verify_raw_writes.sql |
A raw API token doing what the app's functions refuse: signing its own sign-off, closing without a verifier, bypassing the cert gate |
verify_requests.sql, verify_complete_action.sql, verify_operator_gate.sql |
The request-for-input, action sign-off and DVIR cert-gate rules failing for a real login |
verify_training.sql, verify_lms.sql, verify_lms_review.sql, verify_rule_timing.sql |
Training assignment, sessions, outside training and renewal dates going wrong at the database |
verify_dq_documents.sql, verify_site_documents.sql |
Who may file and approve a document, and the dates that drive reminders |
verify_incident_changelog.sql, verify_audit_columns.sql |
The audit trail missing a change, or a table without who-and-when columns |
verify_lost_time.sql, verify_restricted_duty.sql |
Lost-time and restricted-duty counts, which feed DART |
verify_rule_mapping.sql and the three rule-book scripts |
Who may decide a rule mapping, and the trail being append-only |
verify_addon_switch.sql |
An add-on switched off for a tenant still answering a raw token |
verify_demo_reset.sql |
A demo reset failing to put every row back (this one commits, hence the throwaway database) |
qa_watchlist.mts |
A watchlist panel promising four people and opening onto three: 1,828 assertions as each persona's own token |
qa_ask.mts |
Spotter's answers disagreeing with the Reports screen |
The unit suites, one line each
All 65 run in npm test today, in CI on every push. Each is a plain script of assertions, one
process per file.
Incidents and investigations
| Suite | What it checks | What it protects against |
|---|---|---|
incident-kinds |
Ticked kinds on the one incident form and the incident types they file as | A report filed under the wrong type, skewing every count by type |
incident-filter |
The status rail: pills, what each asks the database for, the counts, the query string | A pill counting one population and opening another |
incident-table |
The incidents table's columns, populations and reconciling caption | A table that does not add up to the tile that sent the reader there |
incident-history |
The investigation's history windows across month and year ends | Off-by-one dates at a month or year boundary |
investigation |
Three states, what each requires, what closing does not require | An incident closing without a sign-off, or blocked by something irrelevant |
investigation-queue |
Search and slicers on the queue | A case the owner cannot find |
investigation-request |
Who owes the next move on a request for input, and what blocks a sign-off | Signing off with an open request still waiting on somebody |
changelog |
The audit trail's order, reconstructed rows, and what an export writes | An exported history that misorders or drops changes |
attachment |
File types, the size limit and the storage path the policies rely on | A photo stored where its access policy cannot see it |
capture-draft |
When an unfinished form is kept and how it reads on Tasks | Losing typed work, or Tasks filling with empty drafts |
Actions, tasks and the dashboards
| Suite | What it checks | What it protects against |
|---|---|---|
action-detail |
What a corrective action panel says for each of the four things that can raise one | An action that opens onto nothing |
tasks |
What lands on the Tasks panel, in what order, with whose name | Somebody's work not reaching their landing screen |
dashboard |
Rates against the exposure basis, roll-ups recomputed rather than averaged, month handling, money format | A TRIR or DART that is wrong because rates were averaged |
benchmark |
Comparison bands, with and without one | A tile coloured against a band that does not apply |
watchlist |
Signals, the band rule, ranking, the site thresholds, on real rows | A person in danger ranked below one who is not |
person-detail |
Every watchlist signal has a section to land on | A red flag that opens onto a page saying nothing is wrong |
crew-chase |
Renewals to chase as a reading, not as Tasks rows | Tasks rows nobody can clear |
vehicle-record |
Which defects are open and whether the truck may roll | The record and the watchlist disagreeing about one truck |
vehicle-tree |
Yard, kind of truck, truck, on the real shapes | The vehicle hierarchy drifting from the data |
shop |
The shop supervisor's queue, grouped by who holds each repair | Repairs nobody is holding |
report-finder |
Every report's cells line up with its columns | A blank or shifted column in a CSV a client exports |
search |
The order the people pickers sort into | The right person sinking down the list as a name is typed |
persona-swap |
A demo's own persona names applied to database replies | A demo showing the wrong name, or changing the real identity |
Drivers, the DQ file and equipment
| Suite | What it checks | What it protects against |
|---|---|---|
driver-home |
DQ file status, licence grouping, to-do order, on-duty window | A driver told they are fine when a card has lapsed |
dq-document |
Which buttons a screen offers on the DQ file before it asks the database | A screen offering an action the database will refuse |
dq-file |
The file as a document: summary, "not qualified today", coverage | An auditor's summary that disagrees with the lines beneath it |
coverage |
Licence and company cert read across the workforce | The equipment coverage report overstating who may drive what |
operator-gate |
The DVIR cert gate on the real catalogue and two real cases | A driver filing a DVIR on a machine they are not certified for |
Training
| Suite | What it checks | What it protects against |
|---|---|---|
training-assignment |
Assigning a renewal, and the two-step that finishes it | A renewal that never closes, or closes without its second step |
training-course |
Course relevance; a training names a course and the unsafe line it answers | Remedial training unconnected to its cause |
training-library |
Search, filters, counts, the catalogue editor, who may do what | A course edited by somebody who may not |
training-records |
Wording and rows on the older training screens | Two screens describing the same record differently |
training-rule |
Standing assignment rules and automatic renewal | People missed by a rule, or assigned twice |
training-session |
A class's state machine, roster and single signature | A signed session being changed afterwards |
external-training |
The outside-training form and who may approve it | A learner approving their own certificate |
renewal-plan |
Who needs a renewal, which month, and whether a class has them | The 12-month plan showing red for people already booked |
classes-panel |
Classes ahead and who is due with none | A supervisor not seeing who still needs a class |
site-documents |
Document states, grouping, validation, path | A permit's renewal reminder not firing |
Rule book and rule mapping
| Suite | What it checks | What it protects against |
|---|---|---|
policy |
Which rule-book section a checklist line opens, and search | A supervisor pointed at the wrong paragraph |
rulebook |
The Word reader: structure, chapter numbers, redaction | A new edition read wrongly, or the operator's name surviving redaction |
rule-mapping |
Hazards link the book and the catalogue; decisions override suggestions | A suggested action with no reason behind it |
rule-publish |
What publishing includes, what blocks it, the diff | Publishing a mapping with undecided links |
rule-vocabulary |
A client's own terms over our library | A client's word silently matching nothing |
catalog-import |
A client's course catalogue from CSV or Excel | Rows dropped or courses retired without a word |
Hub, add-ons and the rest
| Suite | What it checks | What it protects against |
|---|---|---|
business-rules |
The rules register, and that every rule still points at a real file and line | The register drifting from the code it describes |
compliance-plan |
The SOC 2 readiness file: parser, ticks, sort orders, malformed rows | A SOC 2 gap silently dropping off the hub page |
questions, data-checklist, hub-activity |
The hub's question board, data checklist form and "new since you last looked" | A question or checklist row not appearing |
sales |
Leads vocabulary matches the database's CHECKs | A screen offering a value the database refuses |
addon-boundary |
Core code never imports an add-on, except through the registry | A client repo failing to build once Spotter is removed |
sync (packages/db) |
The offline outbox: retry or park, order, stop on network loss | A DVIR captured offline being lost or sent twice |
Spotter: ask-log, blocks, model, saved, talk, words |
The question log, the building blocks, what is sent to a model, saved questions, the exchange, and how a VP actually asks | A plausible wrong number; a name or site leaving in a question to the model |
COFI: gaps, keyboard, palette |
The needs-data registry, chart keyboard rules, every colour a Lucent Axis colour | COFI misreporting what it needs, inaccessible charts, off-brand colours |
COFI: model |
The replica's arithmetic against the dashboard's own | Two screens computing one fact two ways. Needs a database dump, so it is skipped in CI by name |
The data suites
They need no database and run in about 20 seconds; regeneration takes another 45. All are standard-library Python.
| Suite | What it checks | What it protects against |
|---|---|---|
generate_sample_data.py, then git diff --exit-code data/ |
The committed data set is exactly what the generator produces | Non-determinism, and generator changes nobody regenerated for |
audit.py |
48 foreign-key checks, 122 logic checks, realism benchmarks (TRIR, DART) | Orphaned rows, impossible combinations, rates outside industry bands |
verify.py |
File hygiene, vocabulary, business-key uniqueness, org structure | A duplicate IN- number, a stray status word, a BOM in a file |
coherence.py |
Each record reads as one event | An incident whose equipment, place and cause contradict each other |
precheck_ddl.py |
Every DDL constraint asserted against the CSVs, and every named CHECK has a test | A load that fails on a constraint, or a constraint nobody tests |
Best practice for the lucent-axis workflow
- Protect
mainwith a ruleset, admins included. Pull request required, one approving review, the CI jobs above as required checks, no force-push, and a written emergency path (SOC 2 SC-5). This ends committing straight tomain, which is how the project works today: that is Dan's call, and the SOC 2 Type I needs it. permissions: {}at the top of every workflow, and each job asks only for what it uses.- Pin every third-party action to a full commit SHA, not a tag or
@master, and let Dependabot move the pins. A tag can be re-pointed by whoever owns the action. - Cancel superseded runs with a
concurrencygroup per branch, so a quick second push does not queue behind the first. - Keep required check names stable. A renamed job silently stops being required.
- Never make a flaky or external check required. Evidence Lab runs its translation tests on every pull request but cannot fail on them, because a third-party outage must not block a merge.
- Secrets at the organization level, and per environment for anything that touches a database, with required reviewers on that environment. Fork pull requests receive no secrets, so jobs that need them are skipped there, as Evidence Lab does. For AWS (Bedrock), use GitHub's OIDC login rather than a stored key.
- Actions gates, Vercel deploys. Keep Vercel's git integration for previews and production, and
make the CI checks required before a merge. Later, if releases need to wait on CI, deploy from
Actions with
vercel buildthenvercel deploy --prebuilt. - Previews must not reach real data. Today every preview reads the one shared database (SOC 2 VE-1, VE-3). The database layer above needs its own throwaway database for the same reason.
- Gitleaks as a pinned binary, not the action. The action needs a paid licence on an organization account; our CI already downloads the binary and checks its SHA-256.
- Cost. GitHub-hosted runners on a private repo draw on the plan's included minutes (3,000 a month on Team). At about 5 minutes a run plus a nightly database job, that is comfortable.
What was built, and where it departs from the plan
- Ruff checks only
E9andF(syntax errors, undefined names, unused imports and variables), set inruff.toml. Its default set reported 378 findings across the scripts, almost all house style; the six real ones (dead imports, an unused variable in the generator) were fixed, and the regenerated data set came out byte-identical. Widenselectone rule family at a time. - The hygiene hooks skip
vendor/(Power BI's own files, CRLF as shipped) and the files scripts write (data/csv/, the suites'data/*.txt,docs/dba/). The one-off whitespace pass they made over the rest is its own commit. - The new-table guard holds new migrations only, those sorting after
20261027130000_lms_switch.sql(and Spotter's0004). The older ones enable RLS in loops and backfill audit columns in bulk, which a text check cannot follow;verify.sqlandverify_audit_columns.sqlalready cover them at the database. - The CI
pre-commitjob skips ESLint, becausechecklints the whole app; everything else in the file runs. npm auditwas not clean on day one: one critical (Next.js 16.0 to 16.4.0, remote code execution innext/og) and four high. Next moved to 16.4.3 and zod to 4.6.7, both inside their major, so the job blocks on critical from its first run.- The data job runs Python 3.11: regeneration was confirmed byte-identical on 3.9 and 3.15.
- The first nightly run found that a database could not be built from empty. Four fixes, none
changing what a migration did to an already-loaded database: two migrations that seed against CSV
rows (
20260901120000,20260909110000) now skip that work on an empty database, wherereseed_reference.sqldoes it after the load;load.sh --resettruncates even a database with no people yet (migrations had already seeded five requirement rows the CSV also carries);gen_demo_user.pywrites the restriction dates its planted case got on the shared database from a backfill; and20261028140000givessite_documentandsite_document_categorythe audit columns they never had, on the shared database too, which is not yet applied there (re-take the demo baseline after applying it). - Semgrep CE is the
code-scanjob (SC-7): the registry's TypeScript, JavaScript, React and Next.js rules, any finding fails it, not required because the rules are fetched at run time. Its first pass found one finding, the hub's own markdown, silenced inline with the reason. - Not built yet: the add-on removable job (path-filtered) and code metrics.
Shipping once main is protected (ORG-07, SOC 2 SC-5)
The ruleset is a file, .github/rulesets/main.json, so what is enforced can be read and diffed:
pull request required, 0 approvals (one developer; Kyle reviews merged changes monthly), merge
commits only, no force-push or deletion, no bypass for admins, and five required checks: check,
secrets, pre-commit, build, data. Not required, because something outside the change can
turn them red: dependencies (a new advisory) and code-scan (new registry rules). Applied by a
repository admin with:
gh api -X POST repos/lucent-axis/scope/rulesets --input .github/rulesets/main.json
From then on nothing pushes to main on lucent-axis/scope. scripts/git/ship.sh is the routine:
push HEAD to the personal repo's main as before and to personal-main on lucent-axis/scope,
open the standing PR into main if none is open, wait for the required checks, merge it.
Dependabot's PRs merge on lucent-axis/scope only, so each is brought home with
git merge origin/main before the next ship; the script refuses to ship until it is. Emergency path: an admin
switches the ruleset to Disabled, which GitHub's audit log records, and back afterwards.
Applied 28 Oct 2026 by Dan's go: ruleset main, id 26059181, active. The first change shipped
through the PR (this paragraph) is the proof that the routine works.
CodeQL: the decision
The question was: pay for GitHub's code scanning on lucent-axis/scope, or not.
- What it adds: static analysis of our own TypeScript (injection, unsafe data handling), as one more CI job with findings in GitHub's Security tab.
- What it costs: the repo is private on the Team plan, so it needs GitHub Code Security, a paid add-on, currently off. About $30 per active committer a month, roughly $60 for two (check the current price before buying).
- Same catch for secret scanning: "turn on secret scanning and push protection" below is also paid on a private repo (Secret Protection, about $19 per committer a month), also off. Gitleaks already covers it for free, on commit and over the whole history in CI.
- Decided (Dan, 28 Oct, "free first",
docs/compliance/README.md): Semgrep Community Edition as a CI job beside ESLint is the SC-7 route. CodeQL is the upgrade, bought only if an auditor or a buyer wants findings tracked in one place. Gitleaks in CI plus the pre-commit hook is the recorded ORG-03 control, so Secret Protection is not bought either. The Semgrep job iscode-scaninci.yml.
Ready to implement
Each line is one piece of work once the CI/CD workflow is in place.
- Install
pre-commit(Python 3.11, to match Evidence Lab) and add.pre-commit-config.yamlwith the Layer 1 hooks,data/csv/excluded - Write the three project guards: generated CSVs, the demo date, new tables
- Add the pre-push stage: tenant, type-check, unit suites, data suites when the generator changed
- Add the
pre-commit,buildanddatajobs to.github/workflows/ci.yml - Add
npm auditand the CycloneDX SBOM to CI - Add
.github/dependabot.ymlfor npm and GitHub Actions - Pin every action in the workflows to a commit SHA
- Turn on secret scanning and push protection on the lucent-axis repo (SOC 2 SC-3)
- Decide on CodeQL (GitHub Code Security is a paid add-on for private repos)
- Build the nightly database job on a throwaway Supabase in the runner
- Protect
mainwith a ruleset and make the CI jobs required checks (SOC 2 SC-5) - Add a "Getting started" line to
README.md:pre-commit install --hook-type pre-commit --hook-type pre-push