@starci/hfs 4.0.0 → 4.0.2
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/CHANGELOG.md +12 -0
- package/bin/hfs.mjs +1 -1
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +8 -8
- package/runtime/knowledge/hfs/slots.yaml +26 -26
- package/runtime/modules/kernel/failure-codes.yaml +8 -8
- package/runtime/scripts/checks/architecture/connection-map.mjs +5 -2
- package/runtime/scripts/checks/architecture/hfs.mjs +2 -2
- package/runtime/scripts/checks/architecture/test-world-files.mjs +3 -1
- package/runtime/scripts/lib/hfs-check.mjs +3 -2
- package/runtime/scripts/lib/hfs-rules/deps.mjs +4 -2
- package/runtime/scripts/lib/hfs-rules/stacks.mjs +3 -3
- package/scaffold/app.mjs +36 -9
- package/sync/hygiene.mjs +5 -5
- package/sync/sonar-key.mjs +3 -5
- package/templates/app/gitignore +26 -0
- package/templates/{be → app}/skeleton/.starcistacks/application-stacks.yaml +1 -0
- /package/templates/{be → app}/skeleton/.sops.yaml +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.0.2 - 2026-10-01
|
|
4
|
+
|
|
5
|
+
- Changed (owner correction): `.starcistacks/` and `.sops.yaml` live at the app root, beside `be/`, `fe/` and `.starciwork`. The slots `be.starcistacks` and `be.sops` are replaced by `app.starcistacks` and `app.sops` (profile app, required); `repo.side-root-forbidden` now lists `.starcistacks/` and `.sops.yaml`, so a side holding either is `HFS_FORBIDDEN_PRESENT`, and the architecture machine reports a `.starcistacks` in either side as `HFS_STACKS_IN_SIDE` (it replaces `HFS_STACKS_IN_FE`). R10 `HFS_STACKS_SHAPE` judges the app root's tree.
|
|
6
|
+
- Changed: the managed `.gitignore` block carries the `.starcistacks` custody rules of `modules/schemas/stacks-layout.yaml` (`custody.gitignoreRules`, in order), rooted at the app root; an app keeps no hand-written custody block.
|
|
7
|
+
- Changed: `hfs scaffold app` writes `.starcistacks/application-stacks.yaml` and `.sops.yaml` at the app root (they were under `be/`); `hfs work-hygiene` guards the app root's `.starcistacks/`; the Sonar key and gate are read from `.starcistacks/application-stacks.yaml` at the app root.
|
|
8
|
+
- Changed: R47 `test-world-files` and R84 `connection-map` read the stack of the app root from the side they judge.
|
|
9
|
+
|
|
10
|
+
## 4.0.1 - 2026-10-01
|
|
11
|
+
|
|
12
|
+
- Fixed: `hfs scaffold app` no longer writes a hand-made lockfile. The 4.0.0 stub held only the root entry, so `npm ci` in a new app failed with EUSAGE ("package.json and package-lock.json are not in sync"). Once the files are written, the scaffold runs `npm install --package-lock-only --ignore-scripts --no-audit --no-fund` in the new app root (registry or npm cache; no node_modules, no scripts), so the lockfile resolves every dependency of the root and its workspaces and `npm ci` accepts it. If npm cannot resolve it, the scaffold exits 2 with `HFS_SCAFFOLD_LOCK_FAILED`, names the step and removes the app it began: no stub lock and no app without a lock is left. There is no switch to skip the step.
|
|
13
|
+
- Fixed (false positive): R14 `HFS_DEP_VERSION_SKEW` does not report a bundled copy in the lockfile (`inBundle`, a package's bundleDependencies, e.g. the tslib inside `@tailwindcss/oxide-wasm32-wasi`): it ships inside its parent's tarball and nothing in the app can move it. A fresh scaffold's real lock has one.
|
|
14
|
+
|
|
3
15
|
## 4.0.0 - 2026-10-01
|
|
4
16
|
|
|
5
17
|
- Added: the scaffolded root `package.json` (and its lockfile root entry) pins `@starci/test-world` from `knowledge/hfs/canon-pins.yaml` (1.0.0), the package whose `starci-test-stack` bin the managed `test:stack` script runs.
|
package/bin/hfs.mjs
CHANGED
|
@@ -215,7 +215,7 @@ export async function main(argv, { stdout = (s) => process.stdout.write(s), stde
|
|
|
215
215
|
const [kind, name, ...extra] = opts.positional;
|
|
216
216
|
if (kind !== 'app' || !name || extra.length || opts.repo !== undefined) throw new Error('hfs scaffold takes `app <name> [--into <dir>]`');
|
|
217
217
|
const { root: created, files } = scaffoldApp({ name, into: path.resolve(opts.into ?? process.cwd()), presets: presets ?? await scaffoldPresets() });
|
|
218
|
-
stdout(`hfs scaffold app: created ${created} (${files.length} files); next: npm
|
|
218
|
+
stdout(`hfs scaffold app: created ${created} (${files.length} files); next: npm ci, then npm run lint\n`);
|
|
219
219
|
return 0;
|
|
220
220
|
}
|
|
221
221
|
if (opts.positional.length !== 1) throw new Error('hfs explain takes exactly one path');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starci/hfs",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.2",
|
|
4
4
|
"description": "The HFS command line of a StarCi app (one repository: the root, be/ and fe/): hfs lint, check, scaffold app, explain, sync and work-hygiene. Self-contained: it carries the runtime files it reads.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -24,19 +24,19 @@ pins:
|
|
|
24
24
|
source: packages/grammar/package.json
|
|
25
25
|
why: nivo-fe pins 0.4.11 and 0.5.0 in one workspace, starci-next-fe 0.5.1, miamia-fe 0.5.0; the runtime source is 0.8.0 (the brand layer sets `--font-sans` and `--font-mono`; the grammar reads them; 0.7.2 added the Input tel kind and IconButton disclosure props).
|
|
26
26
|
'@starci/eslint-canon-be':
|
|
27
|
-
version: 3.0.
|
|
27
|
+
version: 3.0.2
|
|
28
28
|
group: starci
|
|
29
29
|
install: registry
|
|
30
30
|
side: be
|
|
31
31
|
source: packages/eslint/be/package.json
|
|
32
|
-
why: '3.0.0: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side; the project graph is built per side. 2.0.0 (C0 release): starciBeConfig({ hfs: loadHfs(import.meta.url) }) typed factory and the BE-CONVENTION laws.'
|
|
32
|
+
why: '3.0.2: its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root (app.starcistacks, app.sops) and pin hfs 4.0.2. 3.0.1: its bundled canon-pins copy pins hfs 4.0.1. 3.0.0: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side; the project graph is built per side. 2.0.0 (C0 release): starciBeConfig({ hfs: loadHfs(import.meta.url) }) typed factory and the BE-CONVENTION laws.'
|
|
33
33
|
'@starci/eslint-canon-fe':
|
|
34
|
-
version: 8.0.
|
|
34
|
+
version: 8.0.2
|
|
35
35
|
group: starci
|
|
36
36
|
install: registry
|
|
37
37
|
side: fe
|
|
38
38
|
source: packages/eslint/fe/package.json
|
|
39
|
-
why: '8.0.0: `loadHfs(import.meta.url)` of fe/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the fe side; the project graph is built per side. 7.0.0: the front end has no tests (FE_NO_TESTS R97 in hfs); no-vietnamese-in-source (R91).'
|
|
39
|
+
why: '8.0.2: its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root (app.starcistacks, app.sops) and pin hfs 4.0.2. 8.0.1: its bundled canon-pins copy pins hfs 4.0.1. 8.0.0: `loadHfs(import.meta.url)` of fe/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the fe side; the project graph is built per side. 7.0.0: the front end has no tests (FE_NO_TESTS R97 in hfs); no-vietnamese-in-source (R91).'
|
|
40
40
|
'@starci/stylelint-canon':
|
|
41
41
|
version: 2.0.1
|
|
42
42
|
group: starci
|
|
@@ -64,19 +64,19 @@ pins:
|
|
|
64
64
|
source: packages/jest-preset/package.json
|
|
65
65
|
why: '2.2.0: the integration, e2e and contract projects run every spec file in a worker process of its own (world-runner.cjs), so a process-global framework registry (@nestjs/graphql type metadata) never leaks between e2e files; 2.1.0: four projects on the test world, the unit kit (mockEntityManager, fakeTransaction, fakeIds, FakeClock, Outcome matchers) and the recordingOutbox claim side.'
|
|
66
66
|
'@starci/test-world':
|
|
67
|
-
version: 1.0.
|
|
67
|
+
version: 1.0.2
|
|
68
68
|
group: starci
|
|
69
69
|
install: registry
|
|
70
70
|
side: be
|
|
71
71
|
source: packages/test-world/package.json
|
|
72
|
-
why: 'the shared e2e library of every back end (R47, R48): the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs, the outage lock and the starci-test-stack bin behind the managed test:stack script; a devDependency of every back end.'
|
|
72
|
+
why: '1.0.2: every path a declaration names (`stack`, seeds, the realm, Dockerfiles) resolves from the app root (the directory of hfs.json), where .starcistacks lives, never from the be side. The shared e2e library of every back end (R47, R48): the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs, the outage lock and the starci-test-stack bin behind the managed test:stack script; a devDependency of every back end. 1.0.1: world.keycloak.events/sessions (the user events of the realm and live sessions through the admin API) and world.infra.postgresql.connection(name) (the database of one connection down while the others serve, under the outage lock).'
|
|
73
73
|
'@starci/hfs':
|
|
74
|
-
version: 4.0.
|
|
74
|
+
version: 4.0.2
|
|
75
75
|
group: starci
|
|
76
76
|
install: registry
|
|
77
77
|
side: both
|
|
78
78
|
source: packages/hfs/package.json
|
|
79
|
-
why: '4.0.0: the app monorepo standard: one app repository with the root package.json, lockfile and hfs.json of kind app, and the be/ and fe/ sides; `hfs scaffold app <name>` makes it, `hfs lint` at the root lints be/** with the BE canon and fe/** with the FE canon; the standalone be/fe repository kinds and `hfs init` are deleted.'
|
|
79
|
+
why: '4.0.2: .starcistacks and .sops.yaml live at the app root (slots app.starcistacks, app.sops; a side holding either is refused, HFS_STACKS_IN_SIDE), the managed .gitignore block carries the custody rules, the scaffold writes them at the root. 4.0.1: `hfs scaffold app` resolves the real lockfile with npm (`npm install --package-lock-only`) instead of writing a root-only stub that `npm ci` refuses. 4.0.0: the app monorepo standard: one app repository with the root package.json, lockfile and hfs.json of kind app, and the be/ and fe/ sides; `hfs scaffold app <name>` makes it, `hfs lint` at the root lints be/** with the BE canon and fe/** with the FE canon; the standalone be/fe repository kinds and `hfs init` are deleted.'
|
|
80
80
|
# --- tooling
|
|
81
81
|
typescript:
|
|
82
82
|
version: 5.9.3
|
|
@@ -321,6 +321,27 @@ slots:
|
|
|
321
321
|
"features/<feature>/uat/<name>/{seed,cleanup}.sql", "_resources/{identities,environments,fixtures}/<slug>/resource.yaml"]
|
|
322
322
|
forbids: [evidence/, logs/, reports/, captures/, runs/, "kernel-*/", "*.sqlite*", _derived/, "work/node records"]
|
|
323
323
|
rules: [HFS_AGENT_DATA_TRACKED, HFS_WORK_NODE_RETIRED, HFS_IDENTITY_CUSTODY]
|
|
324
|
+
- id: app.starcistacks
|
|
325
|
+
profiles: [app]
|
|
326
|
+
# The stack declarations and the sealed custody of the app, at the app root beside be/, fe/ and .starciwork (never under a side).
|
|
327
|
+
path: ".starcistacks/"
|
|
328
|
+
presence: required
|
|
329
|
+
tracked: tracked
|
|
330
|
+
tier: none
|
|
331
|
+
tests: none
|
|
332
|
+
requires: [application-stacks.yaml]
|
|
333
|
+
allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/infra/{compose,k8s,terraform}/**", "<env>/infra/metadata.json",
|
|
334
|
+
"<env>/runtime/{config,env}/**", "<env>/runtime/env/KEYS.md", "<env>/secrets/<slug>.enc", "<env>/seeds/**"]
|
|
335
|
+
forbids: ["<env>/runtime/files/**", "**/*.enc outside <env>/secrets/", DESIGN.md, deployment.json, "k8s/ at root"]
|
|
336
|
+
rules: [HFS_STACKS_SHAPE, HFS_STACKS_IN_SIDE, HFS_PLAINTEXT_SECRET, HFS_IDENTITY_CUSTODY]
|
|
337
|
+
- id: app.sops
|
|
338
|
+
profiles: [app]
|
|
339
|
+
# The sops custody rule of .starcistacks/<env>/secrets/*.enc, at the app root beside the tree it seals.
|
|
340
|
+
path: .sops.yaml
|
|
341
|
+
presence: required
|
|
342
|
+
tracked: tracked
|
|
343
|
+
tier: none
|
|
344
|
+
tests: none
|
|
324
345
|
- id: app.build-output
|
|
325
346
|
profiles: [app]
|
|
326
347
|
path: "{**/node_modules/,coverage/,reports/,test-results/,**/*.tsbuildinfo}"
|
|
@@ -360,18 +381,18 @@ slots:
|
|
|
360
381
|
tracked: external
|
|
361
382
|
tier: none
|
|
362
383
|
tests: none
|
|
363
|
-
goesTo: "
|
|
384
|
+
goesTo: ".starcistacks/<env>/secrets/<slug>.enc (at the app root); decrypted values only in memory or under %LOCALAPPDATA%/StarCi/secrets/<project>/<env>/ via *_FILE"
|
|
364
385
|
rules: [HFS_PLAINTEXT_SECRET]
|
|
365
386
|
|
|
366
387
|
# ----- side root (both sides): what the old standalone repository root held, less the app-root files ----------
|
|
367
388
|
- id: repo.side-root-forbidden
|
|
368
389
|
profiles: [be, fe]
|
|
369
|
-
path: "{package.json,package-lock.json,hfs.json,README.md,.gitignore,.gitattributes,.husky/,.github/,.starciwork
|
|
390
|
+
path: "{package.json,package-lock.json,hfs.json,README.md,.gitignore,.gitattributes,.husky/,.github/,.starciwork/,.starcistacks/,.sops.yaml,sonar-project.properties,.prettierrc,.prettierignore,scripts/}"
|
|
370
391
|
presence: forbidden
|
|
371
392
|
tracked: external
|
|
372
393
|
tier: none
|
|
373
394
|
tests: none
|
|
374
|
-
goesTo: "the app root: one package.json, lockfile, hfs.json, README, git and CI files, hooks, formatter, Sonar configuration, scripts
|
|
395
|
+
goesTo: "the app root: one package.json, lockfile, hfs.json, README, git and CI files, hooks, formatter, Sonar configuration, scripts/, .starciwork, .starcistacks and .sops.yaml per app"
|
|
375
396
|
- id: be.tool-config
|
|
376
397
|
profiles: [be]
|
|
377
398
|
# Managed files (hfs sync renders them, hfs check compares them): the whole tool configuration of a back end. Each is
|
|
@@ -529,7 +550,7 @@ slots:
|
|
|
529
550
|
tracked: external
|
|
530
551
|
tier: none
|
|
531
552
|
tests: none
|
|
532
|
-
goesTo: "
|
|
553
|
+
goesTo: ".starcistacks/<env>/secrets/<slug>.enc (at the app root); decrypted values only in memory or under %LOCALAPPDATA%/StarCi/secrets/<project>/<env>/ via *_FILE"
|
|
533
554
|
rules: [HFS_PLAINTEXT_SECRET]
|
|
534
555
|
- id: be.root-e2e
|
|
535
556
|
profiles: [be]
|
|
@@ -825,7 +846,7 @@ slots:
|
|
|
825
846
|
infrastructure (containers) and runs apps/migrate's exported bootstrap once; use-test-world.ts exports
|
|
826
847
|
useTestWorld({ apps: { <name>: { module, listen? } } } | { modules: [...] }) -> world.apps.<name>.api,
|
|
827
848
|
world.db.<connection> (the shared EntityManager), world.infra.<service> (latency/cut/restore on a REAL service of the repository's
|
|
828
|
-
own stack, `.starcistacks/<env>`, each behind toxiproxy), world.fake.<provider> (a network-edge fake of an external SaaS started by the world, with
|
|
849
|
+
own stack, the app root's `.starcistacks/<env>`, each behind toxiproxy), world.fake.<provider> (a network-edge fake of an external SaaS started by the world, with
|
|
829
850
|
failNext/replayWebhook/delay), world.waitFor; fakes/<provider>/ holds the fake servers and payload fixtures, kit/ the inlined test helpers, and the world root may hold role-suffixed helpers (<name>.client.ts, <name>.contracts.ts, ...). Nothing is
|
|
830
851
|
overridden in the DI container. It is the only test location that may import typeorm's DataSource or testcontainers,
|
|
831
852
|
call migrate/runMigrations/synchronize, or write process.env.
|
|
@@ -905,27 +926,6 @@ slots:
|
|
|
905
926
|
tests: none
|
|
906
927
|
goesTo: "src/tests/world/ (the only test infrastructure location; e2e/<area>/ holds only *.e2e-spec.ts)"
|
|
907
928
|
|
|
908
|
-
# ----- BE: work and stacks -----------------------------------------------------------------------------
|
|
909
|
-
- id: be.starcistacks
|
|
910
|
-
profiles: [be]
|
|
911
|
-
path: ".starcistacks/"
|
|
912
|
-
presence: required
|
|
913
|
-
tracked: tracked
|
|
914
|
-
tier: none
|
|
915
|
-
tests: none
|
|
916
|
-
requires: [application-stacks.yaml]
|
|
917
|
-
allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/infra/{compose,k8s,terraform}/**", "<env>/infra/metadata.json",
|
|
918
|
-
"<env>/runtime/{config,env}/**", "<env>/runtime/env/KEYS.md", "<env>/secrets/<slug>.enc", "<env>/seeds/**"]
|
|
919
|
-
forbids: ["<env>/runtime/files/**", "**/*.enc outside <env>/secrets/", DESIGN.md, deployment.json, "k8s/ at root"]
|
|
920
|
-
rules: [HFS_STACKS_SHAPE, HFS_PLAINTEXT_SECRET, HFS_IDENTITY_CUSTODY]
|
|
921
|
-
- id: be.sops
|
|
922
|
-
profiles: [be]
|
|
923
|
-
path: .sops.yaml
|
|
924
|
-
presence: required
|
|
925
|
-
tracked: tracked
|
|
926
|
-
tier: none
|
|
927
|
-
tests: none
|
|
928
|
-
|
|
929
929
|
# ----- FE: apps ----------------------------------------------------------------------------------------
|
|
930
930
|
- id: fe.app.next
|
|
931
931
|
profiles: [fe]
|
|
@@ -1503,21 +1503,21 @@ HFS_SRC_LAYOUT_INVALID:
|
|
|
1503
1503
|
owner: op-retry
|
|
1504
1504
|
kind: check-finding
|
|
1505
1505
|
|
|
1506
|
-
|
|
1507
|
-
title: "Stack declarations
|
|
1508
|
-
title_vi: "Khai báo stack nằm trong
|
|
1509
|
-
meaning_vi: "
|
|
1506
|
+
HFS_STACKS_IN_SIDE:
|
|
1507
|
+
title: "Stack declarations under a side (be/ or fe/) instead of the app root"
|
|
1508
|
+
title_vi: "Khai báo stack nằm trong một phía (be/ hoặc fe/) thay vì ở gốc app"
|
|
1509
|
+
meaning_vi: "Một phía của app (be/ hoặc fe/) đang chứa thư mục .starcistacks, trong khi .starcistacks chỉ được đặt ở gốc app, cạnh be/, fe/ và .starciwork."
|
|
1510
1510
|
causes_vi:
|
|
1511
|
-
- "
|
|
1512
|
-
- "Worker tạo stack ở
|
|
1513
|
-
nextStep_vi: "Bộ kiểm kiến trúc từ chối; op phải
|
|
1511
|
+
- "Còn giữ bố cục cũ be/.starcistacks từ trước khi chuyển về gốc app"
|
|
1512
|
+
- "Worker tạo stack trong một phía thay vì ở gốc app"
|
|
1513
|
+
nextStep_vi: "Bộ kiểm kiến trúc từ chối; op phải dời thư mục bằng git mv về .starcistacks ở gốc app và sửa mọi đường dẫn trỏ tới nó."
|
|
1514
1514
|
owner: op-retry
|
|
1515
1515
|
kind: check-finding
|
|
1516
1516
|
|
|
1517
1517
|
HFS_STACKS_SHAPE:
|
|
1518
1518
|
title: "`.starcistacks` has the standard shape and the host Sonar owner"
|
|
1519
1519
|
title_vi: "Hình dạng .starcistacks sai"
|
|
1520
|
-
meaning_vi: "`.starcistacks` có `<path>` ngoài hình dạng chuẩn (danh sách allows của slot
|
|
1520
|
+
meaning_vi: "`.starcistacks` có `<path>` ngoài hình dạng chuẩn (danh sách allows của slot app.starcistacks, gồm cả infra/metadata.json) (hoặc còn trỏ `.stacks`)."
|
|
1521
1521
|
causes_vi:
|
|
1522
1522
|
- "Vi phạm luật R10: `.starcistacks/<env>/` chỉ có `README.md`, `environment.json`, `infra/{compose,k8s,terraform}`, `runtime/{config,env}`, `secrets/`, `seeds/`; khối sonar là `owner: host` trỏ `.claude/ext/sonar`; không còn gốc `.stacks`."
|
|
1523
1523
|
nextStep_vi: "Op vừa tạo ra bản này sẽ được chạy lại để sửa theo lời nhắn của bộ kiểm tra (nợ hàng loạt có codemod của HFS); không cần ai can thiệp thêm."
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { machineKit, pascal, upperSnake } from './machine-ast.mjs';
|
|
4
|
+
import { locateDeclaration } from '../../lib/hfs-slots.mjs';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* R84 `connection-map` (BE_CONNECTION_DUPLICATE), folding RED01 and RED02. One physical database is one connection, one
|
|
@@ -179,11 +180,13 @@ export function checkConnectionMap(input) {
|
|
|
179
180
|
// 4. Stacks: two connections that resolve to one host, port and database are one database.
|
|
180
181
|
let stacksChecked = 0;
|
|
181
182
|
let stacksSkipped = 0;
|
|
182
|
-
|
|
183
|
+
// .starcistacks sits at the app root: the side folder the machine judges reads its app's tree (locateDeclaration).
|
|
184
|
+
const stacksRoot = path.join(locateDeclaration(config.root).appRoot, '.starcistacks');
|
|
183
185
|
let envs = [];
|
|
184
186
|
try { envs = fs.readdirSync(stacksRoot, { withFileTypes: true }).filter(entry => entry.isDirectory()).map(entry => entry.name).sort(); } catch { envs = []; }
|
|
185
187
|
for (const env of envs) {
|
|
186
|
-
|
|
188
|
+
// Relative to the side folder the machine judges (`../.starcistacks/...` for a side): the app-relative path once the side is prefixed.
|
|
189
|
+
const rel = path.relative(config.root, path.join(stacksRoot, env, 'runtime', 'env')).split(path.sep).join('/');
|
|
187
190
|
const values = readEnvFiles(path.join(stacksRoot, env, 'runtime', 'env'));
|
|
188
191
|
if (!values || values.size === 0) { stacksSkipped += 1; continue; }
|
|
189
192
|
const seen = new Map();
|
|
@@ -36,7 +36,7 @@ export const HFS_RULE_IDS = [
|
|
|
36
36
|
'HFS_ROOT_MARKDOWN_FORBIDDEN',
|
|
37
37
|
'HFS_ROOT_SRC_FORBIDDEN_FE',
|
|
38
38
|
'HFS_SRC_LAYOUT_INVALID',
|
|
39
|
-
'
|
|
39
|
+
'HFS_STACKS_IN_SIDE',
|
|
40
40
|
'HFS_TEST_KIND_RETIRED',
|
|
41
41
|
'HFS_WORK_IN_FE',
|
|
42
42
|
];
|
|
@@ -423,9 +423,9 @@ export function checkHfs(config) {
|
|
|
423
423
|
const allowed = slotRootEntries(resolver, profile);
|
|
424
424
|
for (const entry of [...tree.top].sort()) {
|
|
425
425
|
if (NON_NPM_ENTRIES.has(entry) || /\.md$/iu.test(entry)) continue;
|
|
426
|
+
if (entry === '.starcistacks') { finding('HFS_STACKS_IN_SIDE', entry, `The ${profile} side must not hold .starcistacks; stack declarations and sealed custody live in the app root .starcistacks.`); continue; }
|
|
426
427
|
if (frontend && !backend) {
|
|
427
428
|
if (entry === '.starciwork') { finding('HFS_WORK_IN_FE', entry, 'The fe side must not hold a .starciwork tree; Work records live in the app root .starciwork.'); continue; }
|
|
428
|
-
if (entry === '.starcistacks') { finding('HFS_STACKS_IN_FE', entry, 'The fe side must not hold .starcistacks; stack declarations live in be/.starcistacks.'); continue; }
|
|
429
429
|
if (entry === 'src') { finding('HFS_ROOT_SRC_FORBIDDEN_FE', entry, 'The fe side keeps source only under apps/<app>/src; the fe/src/ tree must move.'); continue; }
|
|
430
430
|
}
|
|
431
431
|
if (frontend && !backend && (isFeTestPath(entry) || isFeTestPath(`${entry}/x`))) continue; // a test entry of a front end is FE_NO_TESTS's, the one finding of that path
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { treeOf } from './required-files.mjs';
|
|
2
2
|
import { allowsFile } from '../../lib/hfs-allows.mjs';
|
|
3
|
+
import { locateDeclaration } from '../../lib/hfs-slots.mjs';
|
|
3
4
|
import { DEFAULT_ENVIRONMENT, STACKS_DIRECTORY, STATEFUL_KINDS, namesOfService, readStack } from '../../lib/stack-services.mjs';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -159,7 +160,8 @@ export function checkTestWorldFiles(input) {
|
|
|
159
160
|
const services = [];
|
|
160
161
|
for (const environment of environments) {
|
|
161
162
|
let stack = null;
|
|
162
|
-
|
|
163
|
+
// The stack is the app root's .starcistacks: the side folder the machine judges reads its app's (locateDeclaration).
|
|
164
|
+
try { stack = readStack({ root: locateDeclaration(config.root).appRoot, environment }); } catch { stack = null; }
|
|
163
165
|
for (const service of stack?.services ?? []) if (service.role !== 'service' && !services.some(known => known.name === service.name)) services.push({ ...service, environment });
|
|
164
166
|
}
|
|
165
167
|
const stackHint = `${STACKS_DIRECTORY}/${environments.join(', ')}`;
|
|
@@ -219,7 +219,7 @@ const onSide = (side, p) => (p ? path.posix.normalize(`${side}/${p}`) : p);
|
|
|
219
219
|
|
|
220
220
|
/**
|
|
221
221
|
* The findings of one scope: the app root (profile app; its own files, and the rules of the files only the root holds: the one
|
|
222
|
-
* package.json and lockfile, CI, hooks, .starciwork) or one side (profile be or fe; the side folder is `repoRoot` and every path is
|
|
222
|
+
* package.json and lockfile, CI, hooks, .starciwork, .starcistacks) or one side (profile be or fe; the side folder is `repoRoot` and every path is
|
|
223
223
|
* relative to it, exactly as the standalone repository root was). `files` are the scope's tracked paths; `all` (root only) every
|
|
224
224
|
* tracked path of the app, for the rules that read across it (dependency skew, proof commands).
|
|
225
225
|
*/
|
|
@@ -269,11 +269,12 @@ function scopeFindings({ repoRoot, root, repo, resolver, files, all = files, sco
|
|
|
269
269
|
...testTopologyFindings({ repoRoot, files }),
|
|
270
270
|
...proofCommandFindings({ repoRoot, files: all, resolver, sides: Object.keys(repo.sides ?? {}) }),
|
|
271
271
|
...appFrontendFindings({ repoRoot, files: all, repo }),
|
|
272
|
+
...stacksFindings({ repoRoot, files, resolver }),
|
|
272
273
|
// The tree check of the app root the machine runs per side for a side folder: README, root entries, automatic gates, hooks path.
|
|
273
274
|
...checkAppRoot({ root: repoRoot, resolver, tree: trackedTreeView(all) }).violations.map((item) => ({ code: item.ruleId, level: 'error', path: item.path, line: item.line, column: item.column, source: 'machine', message: `${item.path}: ${item.message}` })),
|
|
274
275
|
);
|
|
275
276
|
} else if (repo.profile === 'be') {
|
|
276
|
-
findings.push(...contractFindings({ files, repo, resolver }), ...
|
|
277
|
+
findings.push(...contractFindings({ files, repo, resolver }), ...testTopologyFindings({ repoRoot, files }));
|
|
277
278
|
} else {
|
|
278
279
|
findings.push(...frontendFindings({ repoRoot, files, repo }), ...feNoTestsFindings({ repoRoot, files: files.filter(inScope) }));
|
|
279
280
|
}
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
// - a dependency the root `overrides` pins to a version (a string that is not a `$name` reference) is declared at that version everywhere
|
|
5
5
|
// it is declared: an override the manifests disagree with is a second version in disguise;
|
|
6
6
|
// - the lockfile (package-lock.json, read, never installed) holds no nested copy of a dependency the workspace declares:
|
|
7
|
-
// `node_modules/<a>/node_modules/<name>` or `apps/<app>/node_modules/<name>` next to the hoisted `node_modules/<name>`.
|
|
7
|
+
// `node_modules/<a>/node_modules/<name>` or `apps/<app>/node_modules/<name>` next to the hoisted `node_modules/<name>`. A
|
|
8
|
+
// bundled copy (`inBundle`: it ships inside its parent's tarball, its bundleDependencies) is not one: no range, override or
|
|
9
|
+
// dedupe of the workspace can move it, so it is the parent's, not a second copy the workspace keeps.
|
|
8
10
|
import { found, readJson } from './read.mjs';
|
|
9
11
|
|
|
10
12
|
export const DEP_VERSION_SKEW = 'HFS_DEP_VERSION_SKEW';
|
|
@@ -54,7 +56,7 @@ export function depFindings({ repoRoot, files }) {
|
|
|
54
56
|
}
|
|
55
57
|
for (const [key, entry] of Object.entries(lock.packages)) {
|
|
56
58
|
const nested = NESTED.exec(key);
|
|
57
|
-
if (!nested || entry.link || HOISTED.test(key) || !declared.has(nested[1])) continue;
|
|
59
|
+
if (!nested || entry.link || entry.inBundle || HOISTED.test(key) || !declared.has(nested[1])) continue;
|
|
58
60
|
const name = nested[1];
|
|
59
61
|
findings.push(found(DEP_VERSION_SKEW, 'package-lock.json', `${key} is a nested copy of ${name}${entry.version ? ` ${entry.version}` : ''}${hoisted.has(name) ? ` next to the hoisted ${hoisted.get(name)}` : ''}; the workspace keeps one copy (align the ranges or add a root override)`, { dependency: name, lockPath: key, version: entry.version }));
|
|
60
62
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// stacks.mjs - HFS_STACKS_SHAPE (R10): `.starcistacks` has the standard shape and the host Sonar owner.
|
|
2
|
-
// - every tracked path under `.starcistacks/` is one the
|
|
2
|
+
// - every tracked path under the app root's `.starcistacks/` is one the app.starcistacks slot allows (its `allows` list is the shape:
|
|
3
3
|
// application-stacks.yaml and <env>/{README.md, environment.json, infra/{compose,k8s,terraform}, runtime/{config,env},
|
|
4
4
|
// secrets/<slug>.enc, seeds}); a `.enc` outside <env>/secrets/ is never allowed, `runtime/files/`, a root `DESIGN.md`,
|
|
5
5
|
// `deployment.json` and `k8s/` are not in the list, so they fall out of it;
|
|
@@ -11,7 +11,7 @@ import { declaredStack, findStackDeclaration, STACK_ROOT, text } from '../stack-
|
|
|
11
11
|
import { found } from './read.mjs';
|
|
12
12
|
|
|
13
13
|
export const STACKS_SHAPE = 'HFS_STACKS_SHAPE';
|
|
14
|
-
export const STACKS_SLOT = '
|
|
14
|
+
export const STACKS_SLOT = 'app.starcistacks';
|
|
15
15
|
export const HOST_SONAR_ROOT = '.claude/ext/sonar';
|
|
16
16
|
const SEALED = /\.enc$/;
|
|
17
17
|
const INSIDE_SECRETS = /^[^/]+\/secrets\/[^/]+\.enc$/;
|
|
@@ -22,7 +22,7 @@ const allowedExpressions = (allows) => allows.flatMap((entry) => braceVariants(e
|
|
|
22
22
|
|
|
23
23
|
const shapeFinding = (file, message, extra) => found(STACKS_SHAPE, file, message, extra);
|
|
24
24
|
|
|
25
|
-
/** The shape findings of the `.starcistacks` tree
|
|
25
|
+
/** The shape findings of the `.starcistacks` tree at the app root (`repoRoot` is the app root, `files` its own tracked paths). */
|
|
26
26
|
export function stacksFindings({ repoRoot, files, resolver }) {
|
|
27
27
|
const slot = resolver.slot(STACKS_SLOT);
|
|
28
28
|
if (!slot) return [];
|
package/scaffold/app.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// hfs scaffold app <name> - the first tree of a new app, the one shape every StarCi product has:
|
|
2
2
|
//
|
|
3
3
|
// <name>/ hfs.json (kind app), package.json (every dependency of both sides at its canon pin, the managed scripts),
|
|
4
|
-
// package-lock.json, README.md, the managed root files (CI, husky, .gitignore block, Sonar, prettier)
|
|
5
|
-
// .starciwork
|
|
4
|
+
// package-lock.json, README.md, the managed root files (CI, husky, .gitignore block, Sonar, prettier),
|
|
5
|
+
// .starciwork, .starcistacks/application-stacks.yaml and .sops.yaml (the stack tree lives at the app root, never
|
|
6
|
+
// under be/); scripts/codegen.mjs, the app's own step of `npm run codegen`
|
|
6
7
|
// <name>/be/ the back-end side: the managed tool configuration and the templates/be/skeleton tree (the api app's
|
|
7
8
|
// entrypoint, platform config/logging/errors/clock/cqrs, the liveness capability and the health feature)
|
|
8
9
|
// <name>/fe/ the front-end side: the managed tool configuration and the templates/fe/skeleton tree (the next-intl
|
|
@@ -11,10 +12,13 @@
|
|
|
11
12
|
//
|
|
12
13
|
// The skeleton files are written once from templates/<app|be|fe>/skeleton ({{project}}, {{app}} and {{appPascal}} filled, a
|
|
13
14
|
// `__app__` folder named after the side's app); the managed files are the render of `hfs sync` (sync/index.mjs), so a fresh app
|
|
14
|
-
// is in sync by construction.
|
|
15
|
-
//
|
|
15
|
+
// is in sync by construction. The lockfile is never written by hand: once the files are written, npm resolves the real one
|
|
16
|
+
// (`npm install --package-lock-only`, no node_modules, no scripts), so `npm ci` installs the new app as it is. When npm cannot
|
|
17
|
+
// resolve it the scaffold fails (HFS_SCAFFOLD_LOCK_FAILED), names the step and removes the app it began, so no app without a lock
|
|
18
|
+
// and no stub lock is ever left. An existing directory is refused, never merged into.
|
|
16
19
|
import fs from 'node:fs';
|
|
17
20
|
import path from 'node:path';
|
|
21
|
+
import { spawnSync } from 'node:child_process';
|
|
18
22
|
import { loadSlotManifest, resolveRepoDeclaration } from '../runtime/scripts/lib/hfs-slots.mjs';
|
|
19
23
|
import { parseYaml } from '../runtime/engine/yaml.mjs';
|
|
20
24
|
import { TEMPLATES_DIR, renderTargets, writeTargets } from '../sync/index.mjs';
|
|
@@ -148,11 +152,30 @@ function skeletonOf(scope, app, vars) {
|
|
|
148
152
|
return files;
|
|
149
153
|
}
|
|
150
154
|
|
|
155
|
+
/** The npm step that resolves the lockfile of a new app, exactly as the error names it. */
|
|
156
|
+
export const LOCK_STEP = 'npm install --package-lock-only --ignore-scripts --no-audit --no-fund';
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Resolves the real package-lock.json of the app at `root` with npm (the registry and the npm cache; no node_modules and no
|
|
160
|
+
* lifecycle script). `{ ok: true }` or `{ ok: false, detail }`.
|
|
161
|
+
*/
|
|
162
|
+
export function npmLock(root) {
|
|
163
|
+
// Through the shell (npm is npm.cmd on Windows, which runs only there); the command is the fixed literal LOCK_STEP.
|
|
164
|
+
const run = spawnSync(LOCK_STEP, { cwd: root, encoding: 'utf8', shell: true, windowsHide: true, timeout: 600_000 });
|
|
165
|
+
if (run.status === 0 && fs.existsSync(path.join(root, 'package-lock.json'))) return { ok: true };
|
|
166
|
+
const said = `${run.stderr ?? ''}\n${run.stdout ?? ''}`.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
|
|
167
|
+
// npm's own error lines (`npm error code ETARGET`, `npm error notarget No matching version found for x@1.2.3.`), without the log pointer.
|
|
168
|
+
const errors = said.filter(line => /^npm (?:error|ERR!)/i.test(line) && !/complete log/i.test(line)).map(line => line.replace(/^npm (?:error|ERR!)\s*/i, ''));
|
|
169
|
+
const reason = run.error ? String(run.error.message) : errors.slice(0, 2).join('; ') || said[0] || 'no output';
|
|
170
|
+
return { ok: false, detail: `exit ${run.status ?? 'none'}: ${reason}` };
|
|
171
|
+
}
|
|
172
|
+
|
|
151
173
|
/**
|
|
152
|
-
* `hfs scaffold app <name>`: writes the new app under `into
|
|
153
|
-
* is what sync loads from the installed @starci/jest-preset (the Sonar
|
|
174
|
+
* `hfs scaffold app <name>`: writes the new app under `into`, resolves its lockfile with npm (`lock`, npmLock), and returns
|
|
175
|
+
* `{ root, files }` (app-relative paths, sorted). `presets` is what sync loads from the installed @starci/jest-preset (the Sonar
|
|
176
|
+
* exclusions); the CLI passes the one it resolves. A failed lock step removes the app and throws HFS_SCAFFOLD_LOCK_FAILED.
|
|
154
177
|
*/
|
|
155
|
-
export function scaffoldApp({ name, into, presets, manifest = loadSlotManifest() }) {
|
|
178
|
+
export function scaffoldApp({ name, into, presets, manifest = loadSlotManifest(), lock = npmLock }) {
|
|
156
179
|
if (!NAME.test(String(name))) throw new ScaffoldError('HFS_SCAFFOLD_NAME_INVALID', `the app name ${name} must be kebab-case (a project name: ${NAME})`);
|
|
157
180
|
const root = path.join(into, name);
|
|
158
181
|
if (fs.existsSync(root)) throw new ScaffoldError('HFS_SCAFFOLD_EXISTS', `${root} already exists; hfs scaffold app never writes into an existing directory`);
|
|
@@ -163,7 +186,6 @@ export function scaffoldApp({ name, into, presets, manifest = loadSlotManifest()
|
|
|
163
186
|
const files = [
|
|
164
187
|
{ path: 'hfs.json', content: jsonText(declaration) },
|
|
165
188
|
{ path: 'package.json', content: jsonText(pkg) },
|
|
166
|
-
{ path: 'package-lock.json', content: jsonText({ name, version: pkg.version, lockfileVersion: 3, requires: true, packages: { '': { name, version: pkg.version, dependencies: pkg.dependencies, devDependencies: pkg.devDependencies } } }) },
|
|
167
189
|
{ path: 'be/nest-cli.json', content: jsonText(nestCli(app)) },
|
|
168
190
|
...app.sides.fe.apps.map(entry => ({ path: `fe/apps/${entry.name}/tsconfig.json`, content: jsonText(nextAppTsconfig()) })),
|
|
169
191
|
...['app', 'be', 'fe'].flatMap(scope => skeletonOf(scope, app, { sonarGate: parseYaml(fs.readFileSync(SONAR_GATE_FILE, 'utf8')).gate.name })),
|
|
@@ -175,5 +197,10 @@ export function scaffoldApp({ name, into, presets, manifest = loadSlotManifest()
|
|
|
175
197
|
}
|
|
176
198
|
const targets = renderTargets(declaration, presets, { manifest });
|
|
177
199
|
writeTargets(root, targets);
|
|
178
|
-
|
|
200
|
+
const locked = lock(root);
|
|
201
|
+
if (!locked.ok) {
|
|
202
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
203
|
+
throw new ScaffoldError('HFS_SCAFFOLD_LOCK_FAILED', `\`${LOCK_STEP}\` could not resolve the lockfile of ${root} (${locked.detail}); the app was removed. Check the network and the npm registry, then run hfs scaffold app ${name} again`);
|
|
204
|
+
}
|
|
205
|
+
return { root, files: [...new Set([...files.map(file => file.path), ...targets.map(target => target.path), 'package-lock.json'])].sort() };
|
|
179
206
|
}
|
package/sync/hygiene.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// hfs work-hygiene: the guard for the two trees an app tracks besides source. A file under the app root's .starciwork must be
|
|
2
|
-
// product content (the .starciwork/.gitignore allowlist admits it, so agent output is refused), and a file under the
|
|
3
|
-
//
|
|
2
|
+
// product content (the .starciwork/.gitignore allowlist admits it, so agent output is refused), and a file under the app root's
|
|
3
|
+
// .starcistacks must not be a plaintext secret (only *.enc is sealed). It is also the secrets guard of the commit: every staged file, in
|
|
4
4
|
// any tree, is read from the index and judged with the one secret judgement of `hfs check` (scripts/lib/hfs-rules/secrets.mjs: a secret by
|
|
5
5
|
// being, an .enc that is no sops envelope, a line that matches a secret pattern), so no plaintext secret reaches the history whatever
|
|
6
6
|
// .gitignore says (`git add -f`, a path tracked before a rule tightened). There is no override. The pre-commit hook judges the staged
|
|
@@ -18,9 +18,9 @@ import { pathToFileURL } from 'node:url';
|
|
|
18
18
|
import { secretFileFindings } from '../runtime/scripts/lib/hfs-rules/secrets.mjs';
|
|
19
19
|
|
|
20
20
|
const PLAINTEXT_NAME = /(^|\/)(\.env(\..*)?|[^/]*\.(pem|key|identity|age))$/;
|
|
21
|
-
/** The app root's work tree and
|
|
21
|
+
/** The app root's work tree and stack tree, app-relative (hfs work-hygiene runs at the app root). */
|
|
22
22
|
const WORK = '.starciwork/';
|
|
23
|
-
const STACKS = '
|
|
23
|
+
const STACKS = '.starcistacks/';
|
|
24
24
|
const GUARDED = file => file.startsWith(WORK) || file.startsWith(STACKS);
|
|
25
25
|
|
|
26
26
|
/** The subset of `files` git ignores (as if untracked), asked in one call: a Set of paths. */
|
|
@@ -44,7 +44,7 @@ export function hygieneFindings(files, ignored) {
|
|
|
44
44
|
}
|
|
45
45
|
if (file.startsWith(STACKS) && !file.endsWith('.enc') && !file.endsWith('.env.example')) {
|
|
46
46
|
if (file.includes('/secrets/')) findings.push({ file, code: 'HFS_PLAINTEXT_SECRET', message: 'sits under secrets/ but is not sealed; only <slug>.enc may be tracked' });
|
|
47
|
-
else if (PLAINTEXT_NAME.test(file)) findings.push({ file, code: 'HFS_PLAINTEXT_SECRET', message: 'is a plaintext secret; seal it to
|
|
47
|
+
else if (PLAINTEXT_NAME.test(file)) findings.push({ file, code: 'HFS_PLAINTEXT_SECRET', message: 'is a plaintext secret; seal it to .starcistacks/<env>/secrets/<slug>.enc' });
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
return findings;
|
package/sync/sonar-key.mjs
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
// The Sonar project key of an app: read from its stack declaration (
|
|
1
|
+
// The Sonar project key of an app: read from its stack declaration (.starcistacks/application-stacks.yaml at the app root,
|
|
2
2
|
// services.sonar.projects[]) when one names the app, so there is one source of the key. Only when no declaration names the app
|
|
3
3
|
// does sync derive the key from the project name.
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { parseYaml as bundledParseYaml } from '../runtime/engine/yaml.mjs';
|
|
7
7
|
|
|
8
|
-
/** The
|
|
9
|
-
export const
|
|
10
|
-
/** The stack declaration, app-relative. */
|
|
11
|
-
export const DECLARATION = path.join(STACKS_SIDE, '.starcistacks', 'application-stacks.yaml');
|
|
8
|
+
/** The stack declaration, app-relative: .starcistacks sits at the app root, beside be/, fe/ and .starciwork. */
|
|
9
|
+
export const DECLARATION = path.join('.starcistacks', 'application-stacks.yaml');
|
|
12
10
|
|
|
13
11
|
/** The repository name the declaration lists its projects under: the app package.json name, else the folder name. */
|
|
14
12
|
export function repositoryName(root) {
|
package/templates/app/gitignore
CHANGED
|
@@ -4,3 +4,29 @@ schema.gql
|
|
|
4
4
|
# fe: Next build output and its generated type file
|
|
5
5
|
.next/
|
|
6
6
|
next-env.d.ts
|
|
7
|
+
# .starcistacks custody at the app root: modules/schemas/stacks-layout.yaml custody.gitignoreRules, in order. Only the
|
|
8
|
+
# declaration, runbooks, key lists, seeds, the rendered environment, the infra definitions and sealed *.enc are tracked.
|
|
9
|
+
.starcistacks/**
|
|
10
|
+
!.starcistacks/**/
|
|
11
|
+
!.starcistacks/**/KEYS.md
|
|
12
|
+
!.starcistacks/**/.gitkeep
|
|
13
|
+
!.starcistacks/application-stacks.yaml
|
|
14
|
+
!.starcistacks/*/README.md
|
|
15
|
+
!.starcistacks/*/environment.json
|
|
16
|
+
!.starcistacks/*/seeds/**
|
|
17
|
+
!.starcistacks/*/infra/metadata.json
|
|
18
|
+
!.starcistacks/*/infra/compose/**
|
|
19
|
+
!.starcistacks/*/infra/k8s/**
|
|
20
|
+
!.starcistacks/*/infra/terraform/**
|
|
21
|
+
.starcistacks/*/infra/**/.env
|
|
22
|
+
.starcistacks/*/infra/**/.env.*
|
|
23
|
+
.starcistacks/*/infra/**/*.env
|
|
24
|
+
.starcistacks/*/infra/**/*.key
|
|
25
|
+
.starcistacks/*/infra/**/*.pem
|
|
26
|
+
.starcistacks/*/infra/**/*.tfvars
|
|
27
|
+
.starcistacks/*/infra/terraform/.terraform/
|
|
28
|
+
.starcistacks/*/infra/terraform/*.tfstate
|
|
29
|
+
.starcistacks/*/infra/terraform/*.tfstate.*
|
|
30
|
+
.starcistacks/*/infra/terraform/files/
|
|
31
|
+
.starcistacks/*/infra/terraform/*.tar.gz
|
|
32
|
+
!.starcistacks/**/*.enc
|
|
File without changes
|