@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.
- package/_skeletons/plugin-template/.github/workflows/ci.yml +124 -0
- package/_skeletons/plugin-template/.github/workflows/release.yml +114 -0
- package/_skeletons/plugin-template/README.md +232 -0
- package/_skeletons/plugin-template/biffo.plugin.json +89 -0
- package/_skeletons/plugin-template/pyproject.toml +100 -0
- package/_skeletons/plugin-template/registry-schema.json +294 -0
- package/_skeletons/plugin-template/src/__init__.py +0 -0
- package/_skeletons/plugin-template/src/example_plugin/__init__.py +10 -0
- package/_skeletons/plugin-template/src/example_plugin/main.py +63 -0
- package/_skeletons/plugin-template/src/example_plugin/manifest.py +30 -0
- package/_skeletons/plugin-template/src/example_plugin/plugin.py +92 -0
- package/_skeletons/plugin-template/terraform/README.md +146 -0
- package/_skeletons/plugin-template/terraform/main.tf +167 -0
- package/_skeletons/plugin-template/terraform/outputs.tf +33 -0
- package/_skeletons/plugin-template/terraform/variables.tf +133 -0
- package/_skeletons/plugin-template/tests/conftest.py +17 -0
- package/_skeletons/plugin-template/tests/fakes.py +74 -0
- package/_skeletons/plugin-template/tests/test_example_plugin.py +108 -0
- package/_skeletons/registry/README.md +27 -0
- package/_skeletons/registry/plugins.json +5 -0
- package/_skeletons/registry/registry-schema.json +294 -0
- package/_skeletons/sibling-template/.github/renovate.json +38 -0
- package/_skeletons/sibling-template/.github/workflows/ci.yml +185 -0
- package/_skeletons/sibling-template/.github/workflows/codeql.yml +56 -0
- package/_skeletons/sibling-template/.github/workflows/deploy.yml +219 -0
- package/_skeletons/sibling-template/.github/workflows/destroy-infra.yml +73 -0
- package/_skeletons/sibling-template/README.md +164 -0
- package/_skeletons/sibling-template/_gitignore +71 -0
- package/_skeletons/sibling-template/apps/frontend/.env.example +14 -0
- package/_skeletons/sibling-template/apps/frontend/eslint.config.mjs +10 -0
- package/_skeletons/sibling-template/apps/frontend/next.config.ts +20 -0
- package/_skeletons/sibling-template/apps/frontend/package.json +42 -0
- package/_skeletons/sibling-template/apps/frontend/pnpm-lock.yaml +5268 -0
- package/_skeletons/sibling-template/apps/frontend/pnpm-workspace.yaml +19 -0
- package/_skeletons/sibling-template/apps/frontend/src/app/globals.css +34 -0
- package/_skeletons/sibling-template/apps/frontend/src/app/layout.tsx +15 -0
- package/_skeletons/sibling-template/apps/frontend/src/app/page.test.tsx +75 -0
- package/_skeletons/sibling-template/apps/frontend/src/app/page.tsx +85 -0
- package/_skeletons/sibling-template/apps/frontend/src/lib/api-client.ts +58 -0
- package/_skeletons/sibling-template/apps/frontend/src/lib/auth.test.ts +135 -0
- package/_skeletons/sibling-template/apps/frontend/src/lib/auth.ts +87 -0
- package/_skeletons/sibling-template/apps/frontend/src/test-setup.ts +1 -0
- package/_skeletons/sibling-template/apps/frontend/tsconfig.json +29 -0
- package/_skeletons/sibling-template/apps/frontend/vitest.config.ts +18 -0
- package/_skeletons/sibling-template/biffo.sibling.json +7 -0
- package/_skeletons/sibling-template/infra/backend.tf +23 -0
- package/_skeletons/sibling-template/infra/main.tf +79 -0
- package/_skeletons/sibling-template/infra/outputs.tf +17 -0
- package/_skeletons/sibling-template/infra/variables.tf +63 -0
- package/_skeletons/sibling-template/modules/cloud/aws/api-gateway/main.tf +114 -0
- package/_skeletons/sibling-template/modules/cloud/aws/api-gateway/outputs.tf +12 -0
- package/_skeletons/sibling-template/modules/cloud/aws/api-gateway/variables.tf +41 -0
- package/_skeletons/sibling-template/modules/cloud/aws/compute/main.tf +157 -0
- package/_skeletons/sibling-template/modules/cloud/aws/compute/outputs.tf +4 -0
- package/_skeletons/sibling-template/modules/cloud/aws/compute/variables.tf +70 -0
- package/_skeletons/sibling-template/modules/cloud/aws/storage/main.tf +78 -0
- package/_skeletons/sibling-template/modules/cloud/aws/storage/outputs.tf +4 -0
- package/_skeletons/sibling-template/modules/cloud/aws/storage/variables.tf +12 -0
- package/_skeletons/sibling-template/services/api/pyproject.toml +69 -0
- package/_skeletons/sibling-template/services/api/src/api/__init__.py +0 -0
- package/_skeletons/sibling-template/services/api/src/api/config.py +31 -0
- package/_skeletons/sibling-template/services/api/src/api/core_client.py +64 -0
- package/_skeletons/sibling-template/services/api/src/api/main.py +43 -0
- package/_skeletons/sibling-template/services/api/src/api/middleware/__init__.py +0 -0
- package/_skeletons/sibling-template/services/api/src/api/middleware/auth.py +111 -0
- package/_skeletons/sibling-template/services/api/src/api/routers/__init__.py +0 -0
- package/_skeletons/sibling-template/services/api/src/api/routers/whoami.py +21 -0
- package/_skeletons/sibling-template/services/api/tests/conftest.py +24 -0
- package/_skeletons/sibling-template/services/api/tests/test_whoami.py +21 -0
- package/_skeletons/sibling-template/services/api/uv.lock +1160 -0
- package/core.version +1 -1
- package/dist/index.js +113 -87
- 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 }}'
|