@biffo/cli 0.38.0 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/_skeletons/plugin-template/.github/workflows/ci.yml +124 -0
  2. package/_skeletons/plugin-template/.github/workflows/release.yml +114 -0
  3. package/_skeletons/plugin-template/README.md +232 -0
  4. package/_skeletons/plugin-template/biffo.plugin.json +89 -0
  5. package/_skeletons/plugin-template/pyproject.toml +100 -0
  6. package/_skeletons/plugin-template/registry-schema.json +294 -0
  7. package/_skeletons/plugin-template/src/__init__.py +0 -0
  8. package/_skeletons/plugin-template/src/example_plugin/__init__.py +10 -0
  9. package/_skeletons/plugin-template/src/example_plugin/main.py +63 -0
  10. package/_skeletons/plugin-template/src/example_plugin/manifest.py +30 -0
  11. package/_skeletons/plugin-template/src/example_plugin/plugin.py +92 -0
  12. package/_skeletons/plugin-template/terraform/README.md +146 -0
  13. package/_skeletons/plugin-template/terraform/main.tf +167 -0
  14. package/_skeletons/plugin-template/terraform/outputs.tf +33 -0
  15. package/_skeletons/plugin-template/terraform/variables.tf +133 -0
  16. package/_skeletons/plugin-template/tests/conftest.py +17 -0
  17. package/_skeletons/plugin-template/tests/fakes.py +74 -0
  18. package/_skeletons/plugin-template/tests/test_example_plugin.py +108 -0
  19. package/_skeletons/registry/README.md +27 -0
  20. package/_skeletons/registry/plugins.json +5 -0
  21. package/_skeletons/registry/registry-schema.json +294 -0
  22. package/_skeletons/sibling-template/.github/renovate.json +38 -0
  23. package/_skeletons/sibling-template/.github/workflows/ci.yml +185 -0
  24. package/_skeletons/sibling-template/.github/workflows/codeql.yml +56 -0
  25. package/_skeletons/sibling-template/.github/workflows/deploy.yml +219 -0
  26. package/_skeletons/sibling-template/.github/workflows/destroy-infra.yml +73 -0
  27. package/_skeletons/sibling-template/README.md +164 -0
  28. package/_skeletons/sibling-template/_gitignore +71 -0
  29. package/_skeletons/sibling-template/apps/frontend/.env.example +14 -0
  30. package/_skeletons/sibling-template/apps/frontend/eslint.config.mjs +10 -0
  31. package/_skeletons/sibling-template/apps/frontend/next.config.ts +20 -0
  32. package/_skeletons/sibling-template/apps/frontend/package.json +42 -0
  33. package/_skeletons/sibling-template/apps/frontend/pnpm-lock.yaml +5268 -0
  34. package/_skeletons/sibling-template/apps/frontend/pnpm-workspace.yaml +19 -0
  35. package/_skeletons/sibling-template/apps/frontend/src/app/globals.css +34 -0
  36. package/_skeletons/sibling-template/apps/frontend/src/app/layout.tsx +15 -0
  37. package/_skeletons/sibling-template/apps/frontend/src/app/page.test.tsx +75 -0
  38. package/_skeletons/sibling-template/apps/frontend/src/app/page.tsx +85 -0
  39. package/_skeletons/sibling-template/apps/frontend/src/lib/api-client.ts +58 -0
  40. package/_skeletons/sibling-template/apps/frontend/src/lib/auth.test.ts +135 -0
  41. package/_skeletons/sibling-template/apps/frontend/src/lib/auth.ts +87 -0
  42. package/_skeletons/sibling-template/apps/frontend/src/test-setup.ts +1 -0
  43. package/_skeletons/sibling-template/apps/frontend/tsconfig.json +29 -0
  44. package/_skeletons/sibling-template/apps/frontend/vitest.config.ts +18 -0
  45. package/_skeletons/sibling-template/biffo.sibling.json +7 -0
  46. package/_skeletons/sibling-template/infra/backend.tf +23 -0
  47. package/_skeletons/sibling-template/infra/main.tf +79 -0
  48. package/_skeletons/sibling-template/infra/outputs.tf +17 -0
  49. package/_skeletons/sibling-template/infra/variables.tf +63 -0
  50. package/_skeletons/sibling-template/modules/cloud/aws/api-gateway/main.tf +114 -0
  51. package/_skeletons/sibling-template/modules/cloud/aws/api-gateway/outputs.tf +12 -0
  52. package/_skeletons/sibling-template/modules/cloud/aws/api-gateway/variables.tf +41 -0
  53. package/_skeletons/sibling-template/modules/cloud/aws/compute/main.tf +157 -0
  54. package/_skeletons/sibling-template/modules/cloud/aws/compute/outputs.tf +4 -0
  55. package/_skeletons/sibling-template/modules/cloud/aws/compute/variables.tf +70 -0
  56. package/_skeletons/sibling-template/modules/cloud/aws/storage/main.tf +78 -0
  57. package/_skeletons/sibling-template/modules/cloud/aws/storage/outputs.tf +4 -0
  58. package/_skeletons/sibling-template/modules/cloud/aws/storage/variables.tf +12 -0
  59. package/_skeletons/sibling-template/services/api/pyproject.toml +69 -0
  60. package/_skeletons/sibling-template/services/api/src/api/__init__.py +0 -0
  61. package/_skeletons/sibling-template/services/api/src/api/config.py +31 -0
  62. package/_skeletons/sibling-template/services/api/src/api/core_client.py +64 -0
  63. package/_skeletons/sibling-template/services/api/src/api/main.py +43 -0
  64. package/_skeletons/sibling-template/services/api/src/api/middleware/__init__.py +0 -0
  65. package/_skeletons/sibling-template/services/api/src/api/middleware/auth.py +111 -0
  66. package/_skeletons/sibling-template/services/api/src/api/routers/__init__.py +0 -0
  67. package/_skeletons/sibling-template/services/api/src/api/routers/whoami.py +21 -0
  68. package/_skeletons/sibling-template/services/api/tests/conftest.py +24 -0
  69. package/_skeletons/sibling-template/services/api/tests/test_whoami.py +21 -0
  70. package/_skeletons/sibling-template/services/api/uv.lock +1160 -0
  71. package/core.version +1 -1
  72. package/dist/index.js +113 -87
  73. package/package.json +5 -4
@@ -0,0 +1,294 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "Biffo Plugin Manifest",
4
+ "description": "Schema for biffo.plugin.json — declares what a plugin provides and needs.",
5
+ "type": "object",
6
+ "required": ["name", "version", "tables", "api_routes", "ui_components"],
7
+ "properties": {
8
+ "name": {
9
+ "type": "string",
10
+ "pattern": "^[a-z][a-z0-9-]*$",
11
+ "description": "Unique plugin slug (lowercase, kebab-case)."
12
+ },
13
+ "version": {
14
+ "type": "string",
15
+ "pattern": "^\\d+\\.\\d+\\.\\d+$",
16
+ "description": "Semver version of this plugin release."
17
+ },
18
+ "description": {
19
+ "type": "string",
20
+ "maxLength": 500,
21
+ "description": "Human-readable summary shown in the marketplace."
22
+ },
23
+ "author": {
24
+ "type": "string",
25
+ "default": "Biffo Team",
26
+ "description": "Publisher name."
27
+ },
28
+ "tags": {
29
+ "type": "array",
30
+ "items": { "type": "string" },
31
+ "description": "Categorisation tags for filtering/search."
32
+ },
33
+ "required_core_version": {
34
+ "type": "string",
35
+ "default": ">=0.0.0",
36
+ "description": "Minimum Biffo core version (npm-style range)."
37
+ },
38
+ "tables": {
39
+ "type": "array",
40
+ "items": {
41
+ "type": "object",
42
+ "required": ["name"],
43
+ "properties": {
44
+ "name": {
45
+ "type": "string",
46
+ "pattern": "^[a-z][a-z0-9_]*$",
47
+ "description": "PostgreSQL table name (snake_case)."
48
+ },
49
+ "columns": {
50
+ "type": "array",
51
+ "description": "Additional columns beyond id/tenant_id/created_at/updated_at, which are auto-injected on every table per ADR-0001 and must NOT be declared here — doing so fails validation (ColumnDefinition/PluginTableDefinition in services/api/src/api/models/plugin_table.py).",
52
+ "items": {
53
+ "type": "object",
54
+ "required": ["name", "type"],
55
+ "properties": {
56
+ "name": {
57
+ "type": "string",
58
+ "not": { "enum": ["id", "tenant_id", "created_at", "updated_at"] },
59
+ "description": "Column name. The names id, tenant_id, created_at, and updated_at are reserved (auto-injected) and rejected here."
60
+ },
61
+ "type": {
62
+ "type": "string",
63
+ "pattern": "^(String|Integer|Text|Boolean|Float|DateTime)(\\(.*\\))?$",
64
+ "description": "SQLAlchemy column type constructor string, e.g. 'String(255)', 'Integer', or 'DateTime(timezone=True)'. Must be one of the base types plugin_table.py's _TYPE_MAP resolves (String, Integer, Text, Boolean, Float, DateTime) — anything else silently falls back to String, so this schema rejects it up front rather than allowing a PostgreSQL-style type name (e.g. UUID, TIMESTAMP) that looks valid but isn't."
65
+ },
66
+ "primary_key": {
67
+ "type": "boolean",
68
+ "default": false,
69
+ "description": "Whether this is the primary key."
70
+ },
71
+ "nullable": {
72
+ "type": "boolean",
73
+ "default": false,
74
+ "description": "Whether NULL values are allowed. Defaults to false (NOT NULL)."
75
+ },
76
+ "index": {
77
+ "type": "boolean",
78
+ "default": false,
79
+ "description": "Create a database index on this column."
80
+ },
81
+ "default": { "type": "string", "description": "SQL default value expression." },
82
+ "description": {
83
+ "type": "string",
84
+ "default": "",
85
+ "description": "Human-readable column description."
86
+ }
87
+ },
88
+ "additionalProperties": false
89
+ }
90
+ },
91
+ "indexes": {
92
+ "type": "array",
93
+ "description": "Explicit (optionally multi-column or unique) indexes on the table, beyond any single-column indexes already requested via a column's index: true.",
94
+ "items": {
95
+ "type": "object",
96
+ "required": ["name", "columns"],
97
+ "properties": {
98
+ "name": { "type": "string", "description": "Index name in the database." },
99
+ "columns": {
100
+ "type": "array",
101
+ "items": { "type": "string" },
102
+ "minItems": 1,
103
+ "description": "Column names included in the index."
104
+ },
105
+ "unique": {
106
+ "type": "boolean",
107
+ "default": false,
108
+ "description": "Whether the index enforces uniqueness."
109
+ }
110
+ },
111
+ "additionalProperties": false
112
+ }
113
+ },
114
+ "permissions": {
115
+ "type": "object",
116
+ "additionalProperties": false,
117
+ "description": "Declarative per-operation generic-CRUD permissions (ADR-0004; TablePermissions in services/api/src/api/models/plugin_table.py). Default-deny: an absent permissions block, an absent operation key, or an operation with allowed:false ALL make the table invisible to the generic CRUD layer for that operation. required_role is an any-of allow-list matched against the caller's roles (cognito:groups); an empty list means any authenticated caller. Tenant scoping (ADR-0001) is always applied and is deliberately not configurable here. Only the five operation keys below are permitted.",
118
+ "properties": {
119
+ "list": {
120
+ "type": "object",
121
+ "additionalProperties": false,
122
+ "properties": {
123
+ "allowed": {
124
+ "type": "boolean",
125
+ "default": false,
126
+ "description": "Whether the generic CRUD layer exposes this operation at all."
127
+ },
128
+ "required_role": {
129
+ "type": "array",
130
+ "items": { "type": "string" },
131
+ "default": [],
132
+ "description": "Any-of role allow-list; empty means any authenticated caller."
133
+ }
134
+ }
135
+ },
136
+ "read": {
137
+ "type": "object",
138
+ "additionalProperties": false,
139
+ "properties": {
140
+ "allowed": {
141
+ "type": "boolean",
142
+ "default": false,
143
+ "description": "Whether the generic CRUD layer exposes this operation at all."
144
+ },
145
+ "required_role": {
146
+ "type": "array",
147
+ "items": { "type": "string" },
148
+ "default": [],
149
+ "description": "Any-of role allow-list; empty means any authenticated caller."
150
+ }
151
+ }
152
+ },
153
+ "create": {
154
+ "type": "object",
155
+ "additionalProperties": false,
156
+ "properties": {
157
+ "allowed": {
158
+ "type": "boolean",
159
+ "default": false,
160
+ "description": "Whether the generic CRUD layer exposes this operation at all."
161
+ },
162
+ "required_role": {
163
+ "type": "array",
164
+ "items": { "type": "string" },
165
+ "default": [],
166
+ "description": "Any-of role allow-list; empty means any authenticated caller."
167
+ }
168
+ }
169
+ },
170
+ "update": {
171
+ "type": "object",
172
+ "additionalProperties": false,
173
+ "properties": {
174
+ "allowed": {
175
+ "type": "boolean",
176
+ "default": false,
177
+ "description": "Whether the generic CRUD layer exposes this operation at all."
178
+ },
179
+ "required_role": {
180
+ "type": "array",
181
+ "items": { "type": "string" },
182
+ "default": [],
183
+ "description": "Any-of role allow-list; empty means any authenticated caller."
184
+ }
185
+ }
186
+ },
187
+ "delete": {
188
+ "type": "object",
189
+ "additionalProperties": false,
190
+ "properties": {
191
+ "allowed": {
192
+ "type": "boolean",
193
+ "default": false,
194
+ "description": "Whether the generic CRUD layer exposes this operation at all."
195
+ },
196
+ "required_role": {
197
+ "type": "array",
198
+ "items": { "type": "string" },
199
+ "default": [],
200
+ "description": "Any-of role allow-list; empty means any authenticated caller."
201
+ }
202
+ }
203
+ }
204
+ }
205
+ }
206
+ },
207
+ "additionalProperties": false
208
+ },
209
+ "description": "Database tables the plugin creates via Alembic migrations."
210
+ },
211
+ "api_routes": {
212
+ "type": "array",
213
+ "items": {
214
+ "type": "object",
215
+ "required": ["method", "path", "table", "operation"],
216
+ "properties": {
217
+ "method": {
218
+ "enum": ["GET", "POST", "PUT", "PATCH", "DELETE"],
219
+ "description": "HTTP method. Must be compatible with `operation` (list/read=GET, create=POST, update=PUT/PATCH, delete=DELETE) — see plugin_route.py's _OPERATION_METHODS."
220
+ },
221
+ "path": {
222
+ "type": "string",
223
+ "pattern": "^/[a-zA-Z0-9/_{}-]+$",
224
+ "description": "Path relative to the plugin's mount point (/api/v1/plugins/<name>), e.g. '/widgets' or '/widgets/{id}'. Must start with '/'. Single-row operations (read/update/delete) address a row via an '{id}' path parameter; collection operations (list/create) must not."
225
+ },
226
+ "table": {
227
+ "type": "string",
228
+ "description": "Name of a table declared in this manifest's `tables` that this route exposes. A route cannot reference another plugin's table (RouteDefinition / parse_plugin_routes_from_manifest in services/api/src/api/models/plugin_route.py)."
229
+ },
230
+ "operation": {
231
+ "enum": ["list", "read", "create", "update", "delete"],
232
+ "description": "Generic CRUD operation the Core API synthesizes for this route against `table`, scoped to the caller's tenant_id (ADR-0001). A plugin manifest cannot ship executable handler code the Core API imports and runs (ADR-0002); routes are declarative (issue #19)."
233
+ },
234
+ "description": { "type": "string" }
235
+ }
236
+ },
237
+ "description": "Declarative generic-CRUD routes the Core API synthesizes against this plugin's own tables (issue #19 / ADR-0002), mounted under /api/v1/plugins/<name>."
238
+ },
239
+ "event_subscriptions": {
240
+ "type": "array",
241
+ "items": {
242
+ "type": "object",
243
+ "required": ["detail_type"],
244
+ "properties": {
245
+ "source": { "type": "string", "default": "biffo.core" },
246
+ "detail_type": { "type": "string" },
247
+ "handler": { "type": "string" }
248
+ }
249
+ },
250
+ "description": "EventBridge events the plugin subscribes to."
251
+ },
252
+ "infra_modules": {
253
+ "type": "array",
254
+ "items": {
255
+ "enum": [
256
+ "compute",
257
+ "storage",
258
+ "events",
259
+ "cdn",
260
+ "database",
261
+ "auth",
262
+ "networking",
263
+ "oidc",
264
+ "api-gateway"
265
+ ]
266
+ },
267
+ "description": "Terraform module categories the plugin provisions."
268
+ },
269
+ "ui_components": {
270
+ "type": "array",
271
+ "items": {
272
+ "type": "object",
273
+ "required": ["type", "label", "path"],
274
+ "properties": {
275
+ "type": {
276
+ "enum": ["nav-link", "page", "dashboard-widget", "modal", "dialog"],
277
+ "description": "UI component type."
278
+ },
279
+ "label": { "type": "string" },
280
+ "path": { "type": "string" },
281
+ "icon": { "type": "string" },
282
+ "requires_auth": { "type": "boolean", "default": true }
283
+ }
284
+ },
285
+ "description": "Portal UI elements the plugin adds."
286
+ },
287
+ "dependencies": {
288
+ "type": "object",
289
+ "additionalProperties": { "type": "string" },
290
+ "description": "Python package dependencies (including biffo-plugin-sdk)."
291
+ }
292
+ },
293
+ "additionalProperties": false
294
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
+ "extends": ["config:recommended", "security:openssf-scorecard"],
4
+ "schedule": ["before 6am on Monday"],
5
+ "timezone": "UTC",
6
+ "prConcurrentLimit": 5,
7
+ "prHourlyLimit": 2,
8
+ "automerge": false,
9
+ "labels": ["dependencies"],
10
+ "packageRules": [
11
+ {
12
+ "matchUpdateTypes": ["patch"],
13
+ "matchDepTypes": ["devDependencies"],
14
+ "automerge": true
15
+ },
16
+ {
17
+ "matchManagers": ["terraform"],
18
+ "groupName": "terraform providers",
19
+ "schedule": ["before 6am on the first day of the month"]
20
+ },
21
+ {
22
+ "matchManagers": ["pip_requirements", "pyproject"],
23
+ "groupName": "python dependencies"
24
+ },
25
+ {
26
+ "matchPackageNames": ["next", "react", "react-dom"],
27
+ "groupName": "Next.js / React"
28
+ },
29
+ {
30
+ "matchDepTypes": ["action"],
31
+ "pinDigests": true
32
+ }
33
+ ],
34
+ "vulnerabilityAlerts": {
35
+ "labels": ["security"],
36
+ "automerge": true
37
+ }
38
+ }
@@ -0,0 +1,185 @@
1
+ # Scaled down from biffo-template's own root .github/workflows/ci.yml for a
2
+ # single sibling app (ADR-0007): one frontend package (apps/frontend), one
3
+ # backend service (services/api), one infra/ dir — no monorepo/Turborepo
4
+ # filtering needed since there's exactly one of each. Jobs are consolidated by
5
+ # toolchain (one JS job, one Python job) rather than one per check: GitHub bills
6
+ # each job's wall time rounded up to a full minute and re-installs deps per job,
7
+ # so a dozen sub-minute jobs are wasteful. Job *names* below match the core
8
+ # project's consolidated CI — `biffo sibling create` configures this repo's
9
+ # branch protection with that same status-check list (see
10
+ # cli/src/adapters/source-control/github/index.ts's DEFAULT_STATUS_CHECKS).
11
+ name: CI
12
+
13
+ on:
14
+ push:
15
+ branches: [main, dev, staging]
16
+ pull_request:
17
+ branches: [main, dev, staging]
18
+ # Manual re-run, matching the core project's CI. Without this there is no way
19
+ # to trigger CI on a protected branch without pushing to it — and a sibling
20
+ # repo has the same branch protection, so the same recovery gap. See #293.
21
+ workflow_dispatch:
22
+
23
+ concurrency:
24
+ group: ${{ github.workflow }}-${{ github.ref }}
25
+ cancel-in-progress: true
26
+
27
+ env:
28
+ NODE_VERSION: '22'
29
+ PYTHON_VERSION: '3.13'
30
+
31
+ jobs:
32
+ js:
33
+ # This job also runs a Build step. The name is deliberately NOT extended to
34
+ # say so: it is a required status check context, matched by string against
35
+ # DEFAULT_STATUS_CHECKS and against the branch protection already written
36
+ # onto every sibling repo created so far. Renaming it would leave those
37
+ # repos waiting forever on a context nothing reports.
38
+ name: JS (lint, types, test, audit)
39
+ runs-on: ${{ vars.RUNNER_LABEL || 'ubuntu-latest' }}
40
+ defaults:
41
+ run:
42
+ working-directory: apps/frontend
43
+ steps:
44
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
45
+ - uses: pnpm/action-setup@v4
46
+ with:
47
+ version: 9.15.9
48
+ - uses: actions/setup-node@v4
49
+ with:
50
+ node-version: ${{ env.NODE_VERSION }}
51
+ cache: pnpm
52
+ cache-dependency-path: apps/frontend/pnpm-lock.yaml
53
+ - run: pnpm install --frozen-lockfile
54
+ # Check steps use `if: ${{ !cancelled() }}` so a failure in one still runs
55
+ # the rest — every failure surfaces in a single run, not just the first.
56
+ - name: Lint
57
+ if: ${{ !cancelled() }}
58
+ run: pnpm run lint
59
+ - name: Type check
60
+ if: ${{ !cancelled() }}
61
+ run: pnpm run typecheck
62
+ - name: Test
63
+ if: ${{ !cancelled() }}
64
+ run: pnpm run test
65
+ # Deliberately run with NO NEXT_PUBLIC_CORE_COGNITO_* in scope (issue
66
+ # #286). `next build` prerenders `/`, which imports src/lib/auth.ts; when
67
+ # that module built its Cognito pool eagerly the build blew up on any
68
+ # machine without the real pool ids exported, and nothing in CI caught it
69
+ # — the first person to find out was whoever had just scaffolded the
70
+ # sibling, debugging template code they did not write. A sibling's build
71
+ # must not need auth credentials, so this step is the assertion of that.
72
+ # Provide the real values in deploy.yml, where they are inlined into the
73
+ # artifact that actually ships.
74
+ - name: Build
75
+ if: ${{ !cancelled() }}
76
+ run: pnpm run build
77
+ - name: Dependency audit
78
+ if: ${{ !cancelled() }}
79
+ run: pnpm audit --audit-level=high
80
+
81
+ python:
82
+ name: Python (lint, types, test, security)
83
+ runs-on: ${{ vars.RUNNER_LABEL || 'ubuntu-latest' }}
84
+ defaults:
85
+ run:
86
+ working-directory: services/api
87
+ steps:
88
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
89
+ - uses: astral-sh/setup-uv@v5
90
+ with:
91
+ enable-cache: true
92
+ - run: uv sync --all-groups
93
+ - name: Lint
94
+ if: ${{ !cancelled() }}
95
+ run: uv run ruff check .
96
+ - name: Format check
97
+ if: ${{ !cancelled() }}
98
+ run: uv run ruff format --check .
99
+ - name: Type check
100
+ if: ${{ !cancelled() }}
101
+ run: uv run pyright
102
+ - name: Test
103
+ if: ${{ !cancelled() }}
104
+ run: uv run pytest --cov --cov-report=xml
105
+ - name: Coverage upload
106
+ if: ${{ !cancelled() }}
107
+ uses: codecov/codecov-action@v5
108
+ with:
109
+ files: services/api/coverage.xml
110
+ - name: SAST (Bandit)
111
+ if: ${{ !cancelled() }}
112
+ run: uv run bandit -r src/ -ll --format json -o bandit-report.json
113
+ - name: Upload Bandit report
114
+ if: ${{ !cancelled() }}
115
+ uses: actions/upload-artifact@v4
116
+ with:
117
+ name: bandit-report
118
+ path: services/api/bandit-report.json
119
+ - name: Dependency audit
120
+ if: ${{ !cancelled() }}
121
+ run: uv run pip-audit
122
+
123
+ security-secrets:
124
+ name: Secret Scan
125
+ runs-on: ${{ vars.RUNNER_LABEL || 'ubuntu-latest' }}
126
+ permissions:
127
+ contents: read
128
+ pull-requests: read
129
+ steps:
130
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
131
+ with:
132
+ fetch-depth: 0
133
+ # gitleaks/gitleaks-action@v2 requires a paid GITLEAKS_LICENSE for
134
+ # organization-owned repos (tabsii-com), which isn't provisioned here.
135
+ # Install the free, open-source gitleaks CLI directly instead.
136
+ - name: Install gitleaks
137
+ run: |
138
+ curl -sSfL -o gitleaks.tar.gz https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz
139
+ echo "551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb gitleaks.tar.gz" | sha256sum -c -
140
+ tar -xzf gitleaks.tar.gz -C /usr/local/bin gitleaks
141
+ gitleaks version
142
+ # First pass: scan git history (fetch-depth: 0 above), catching secrets
143
+ # committed then removed within the same history.
144
+ #
145
+ # --log-opts is load-bearing (issue #258). gitleaks' default git log
146
+ # arguments include `--all`, so with fetch-depth: 0 — which fetches every
147
+ # remote ref, not just the one being built — the scan covers commits that
148
+ # live only on *unmerged* branches, letting anyone's open branch redden
149
+ # the integration branch and every open PR at once. Scoping to HEAD
150
+ # restricts the scan to commits reachable from the ref under test without
151
+ # weakening detection: the full reachable history is still walked, so a
152
+ # secret committed then removed within the branch is still caught, and on
153
+ # pull_request HEAD is the merge commit so the PR's own commits are in
154
+ # scope.
155
+ - name: gitleaks git history scan
156
+ run: gitleaks detect --redact -v --exit-code=2 --log-opts="--full-history HEAD"
157
+ # Second pass: scan every file in the working tree (no-git mode).
158
+ # This mirrors what generated repos see on their first CI run — the initial
159
+ # commit includes every template file, so secrets in unchanged files would
160
+ # pass the history scan above but fail there. Running both here closes the gap.
161
+ - name: gitleaks filesystem scan
162
+ run: gitleaks detect --no-git --redact -v --exit-code=2
163
+
164
+ infra-validate:
165
+ name: Terraform Validate & Security
166
+ runs-on: ${{ vars.RUNNER_LABEL || 'ubuntu-latest' }}
167
+ steps:
168
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
169
+ - uses: hashicorp/setup-terraform@v4
170
+ with:
171
+ terraform_version: '~> 1.9'
172
+ - name: Terraform fmt check
173
+ run: terraform fmt -check -recursive infra/ modules/
174
+ - name: Validate infra
175
+ working-directory: infra
176
+ run: |
177
+ terraform init -backend=false
178
+ terraform validate
179
+ - name: Checkov (Terraform SAST)
180
+ uses: bridgecrewio/checkov-action@v12
181
+ with:
182
+ directory: modules/
183
+ quiet: true
184
+ soft_fail: true
185
+ framework: terraform
@@ -0,0 +1,56 @@
1
+ name: 'CodeQL'
2
+
3
+ on:
4
+ push:
5
+ branches: [main, dev, staging]
6
+ pull_request:
7
+ branches: [main, dev, staging]
8
+ schedule:
9
+ # Weekly catch-up scan (Tuesday 05:17 UTC) for issues that land outside
10
+ # PR review (e.g. direct merges onto a branch).
11
+ - cron: '17 5 * * 2'
12
+
13
+ jobs:
14
+ analyze:
15
+ name: Analyze (${{ matrix.language }})
16
+ runs-on: ${{ vars.RUNNER_LABEL || 'ubuntu-latest' }}
17
+ # CodeQL code scanning requires GitHub Advanced Security (GHAS) on private
18
+ # repos — the analysis runs but the SARIF upload fails without it. So this
19
+ # is opt-in: enable GHAS on the repo, then set the repo variable
20
+ # ENABLE_CODE_SCANNING=true. Left dormant (not failing) otherwise.
21
+ if: vars.ENABLE_CODE_SCANNING == 'true'
22
+ timeout-minutes: 15
23
+ permissions:
24
+ # Required for CodeQL to upload SARIF results.
25
+ security-events: write
26
+ contents: read
27
+ actions: read
28
+
29
+ strategy:
30
+ fail-fast: false
31
+ matrix:
32
+ include:
33
+ - language: javascript-typescript
34
+ build-mode: none
35
+ - language: python
36
+ build-mode: none
37
+
38
+ steps:
39
+ - name: Checkout repository
40
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
41
+
42
+ - name: Initialize CodeQL
43
+ uses: github/codeql-action/init@v4
44
+ with:
45
+ languages: ${{ matrix.language }}
46
+ build-mode: ${{ matrix.build-mode }}
47
+ queries: security-extended
48
+
49
+ # autobuild intentionally omitted (build-mode 'none'):
50
+ # - the frontend (apps/frontend) is TypeScript that CodeQL analyses
51
+ # directly, no build required.
52
+ # - the sibling's Python (services/api) has no build step CodeQL needs.
53
+ - name: Perform CodeQL Analysis
54
+ uses: github/codeql-action/analyze@v4
55
+ with:
56
+ category: '/language:${{ matrix.language }}'