create-raredays-app 0.1.3 → 0.1.5
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/bin/compose-allow-builds.mjs +47 -0
- package/bin/index.js +15 -0
- package/package.json +7 -3
- package/templates/client/.node-version +1 -0
- package/templates/client/.semgrepignore +12 -0
- package/templates/client/_github/workflows/ci.yml +63 -22
- package/templates/client/_github/workflows/coverage.yml +41 -0
- package/templates/client/_github/workflows/sast.yml +42 -0
- package/templates/client/_github/workflows/security.yml +34 -0
- package/templates/client/_github/workflows/zizmor.yml +43 -0
- package/templates/client/package.json +4 -1
- package/templates/client/pnpm-workspace.yaml +9 -0
- package/templates/full-stack/.node-version +1 -0
- package/templates/full-stack/.semgrepignore +12 -0
- package/templates/full-stack/_github/workflows/ci.yml +63 -23
- package/templates/full-stack/_github/workflows/coverage.yml +41 -0
- package/templates/full-stack/_github/workflows/db-check.yml +8 -3
- package/templates/full-stack/_github/workflows/db-migrate.yml +8 -3
- package/templates/full-stack/_github/workflows/sast.yml +42 -0
- package/templates/full-stack/_github/workflows/security.yml +34 -0
- package/templates/full-stack/_github/workflows/zizmor.yml +43 -0
- package/templates/full-stack/package.json +4 -1
- package/templates/full-stack/pnpm-workspace.yaml +9 -0
- package/templates/static/.node-version +1 -0
- package/templates/static/.semgrepignore +12 -0
- package/templates/static/_github/workflows/ci.yml +58 -19
- package/templates/static/_github/workflows/sast.yml +42 -0
- package/templates/static/_github/workflows/security.yml +34 -0
- package/templates/static/_github/workflows/zizmor.yml +43 -0
- package/templates/static/package.json +3 -1
- package/templates/static/pnpm-workspace.yaml +8 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Merge per-module build-script approvals into a template's pnpm-workspace.yaml.
|
|
2
|
+
//
|
|
3
|
+
// pnpm 11 blocks dependency build scripts by default and only honors approvals
|
|
4
|
+
// declared as `allowBuilds` in pnpm-workspace.yaml (the package.json `pnpm`
|
|
5
|
+
// field and .npmrc are ignored). Each scaffold fragment (module or vendor) may
|
|
6
|
+
// declare its own `allowBuilds` map in module.json; the scaffolder unions those
|
|
7
|
+
// onto the template's base so a generated app pre-approves exactly the build
|
|
8
|
+
// scripts its selected modules need — no matter which modules are chosen.
|
|
9
|
+
//
|
|
10
|
+
// Pure string transform over the template's pnpm-workspace.yaml: parse the
|
|
11
|
+
// existing ` <pkg>: true|false` entries, union the fragment-contributed ones
|
|
12
|
+
// (later wins, so a fragment can also opt a build OUT with `false`), and
|
|
13
|
+
// re-emit a sorted block while preserving the file's header comment. We avoid a
|
|
14
|
+
// YAML parser dependency on purpose — the block is a flat name->boolean map.
|
|
15
|
+
|
|
16
|
+
const ENTRY = /^ {2}["']?([^"':\s]+)["']?:\s*(true|false)\s*$/gm;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} yamlSource - the template's pnpm-workspace.yaml contents
|
|
20
|
+
* @param {Record<string, boolean>} extra - build approvals contributed by selected fragments
|
|
21
|
+
* @returns {string} the rewritten pnpm-workspace.yaml contents
|
|
22
|
+
*/
|
|
23
|
+
export function mergeAllowBuilds(yamlSource, extra) {
|
|
24
|
+
const merged = {};
|
|
25
|
+
ENTRY.lastIndex = 0;
|
|
26
|
+
let match;
|
|
27
|
+
while ((match = ENTRY.exec(yamlSource)) !== null) {
|
|
28
|
+
merged[match[1]] = match[2] === "true";
|
|
29
|
+
}
|
|
30
|
+
for (const [name, value] of Object.entries(extra)) {
|
|
31
|
+
merged[name] = value === true || value === "true";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Preserve everything before `allowBuilds:` (the header comment block).
|
|
35
|
+
const marker = yamlSource.indexOf("allowBuilds:");
|
|
36
|
+
const header = marker > 0 ? yamlSource.slice(0, marker) : "";
|
|
37
|
+
|
|
38
|
+
const lines = Object.keys(merged)
|
|
39
|
+
.sort()
|
|
40
|
+
.map((name) => {
|
|
41
|
+
// Quote names that aren't bare identifiers (e.g. scoped "@sentry/cli").
|
|
42
|
+
const key = /^[a-zA-Z0-9_-]+$/.test(name) ? name : `"${name}"`;
|
|
43
|
+
return ` ${key}: ${merged[name] ? "true" : "false"}`;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return `${header}allowBuilds:\n${lines.join("\n")}\n`;
|
|
47
|
+
}
|
package/bin/index.js
CHANGED
|
@@ -7,6 +7,8 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { parseArgs } from "node:util";
|
|
8
8
|
import * as p from "@clack/prompts";
|
|
9
9
|
|
|
10
|
+
import { mergeAllowBuilds } from "./compose-allow-builds.mjs";
|
|
11
|
+
|
|
10
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
13
|
const packageRoot = resolve(__dirname, "..");
|
|
12
14
|
const templatesDir = resolve(packageRoot, "templates");
|
|
@@ -520,6 +522,7 @@ async function scaffold(opts) {
|
|
|
520
522
|
// Load + apply selected fragments. Static templates don't have a
|
|
521
523
|
// composition root, so skip module composition entirely there.
|
|
522
524
|
const collectedDeps = {};
|
|
525
|
+
const collectedBuilds = {};
|
|
523
526
|
const envVars = [];
|
|
524
527
|
const vendorMocks = [];
|
|
525
528
|
if (opts.template !== "static" && opts.selectedModules.length > 0) {
|
|
@@ -528,6 +531,7 @@ async function scaffold(opts) {
|
|
|
528
531
|
|
|
529
532
|
for (const f of valid) {
|
|
530
533
|
Object.assign(collectedDeps, f.deps ?? {});
|
|
534
|
+
Object.assign(collectedBuilds, f.allowBuilds ?? {});
|
|
531
535
|
if (f.filesDir) await cp(f.filesDir, targetDir, { recursive: true });
|
|
532
536
|
}
|
|
533
537
|
|
|
@@ -537,6 +541,7 @@ async function scaffold(opts) {
|
|
|
537
541
|
const vendor = f?.vendors?.find((v) => v.key === vendorKey);
|
|
538
542
|
if (!vendor) continue;
|
|
539
543
|
Object.assign(collectedDeps, vendor.deps ?? {});
|
|
544
|
+
Object.assign(collectedBuilds, vendor.allowBuilds ?? {});
|
|
540
545
|
for (const v of vendor.env ?? []) envVars.push(v);
|
|
541
546
|
if (vendor.mocks) {
|
|
542
547
|
vendorMocks.push(vendor.mocks);
|
|
@@ -550,6 +555,16 @@ async function scaffold(opts) {
|
|
|
550
555
|
await writeFile(runtimePath, composeRuntime(source, valid));
|
|
551
556
|
}
|
|
552
557
|
|
|
558
|
+
// Union module/vendor build-script approvals onto the template's
|
|
559
|
+
// pnpm-workspace.yaml so the generated app pre-approves exactly what its
|
|
560
|
+
// selected modules need under pnpm 11. No-op when no fragment declares any.
|
|
561
|
+
if (Object.keys(collectedBuilds).length > 0) {
|
|
562
|
+
const wsPath = resolve(targetDir, "pnpm-workspace.yaml");
|
|
563
|
+
if (existsSync(wsPath)) {
|
|
564
|
+
await writeFile(wsPath, mergeAllowBuilds(await readFile(wsPath, "utf8"), collectedBuilds));
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
553
568
|
// Compose mocks/handlers.ts for any template that ships it (full-stack + client).
|
|
554
569
|
if (opts.template !== "static") {
|
|
555
570
|
const handlersPath = resolve(targetDir, "mocks/handlers.ts");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-raredays-app",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Scaffold a new RareDays Next.js app.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"author": "Jeremy Harper <jeremy@raredays.com>",
|
|
@@ -14,13 +14,16 @@
|
|
|
14
14
|
"skillex"
|
|
15
15
|
],
|
|
16
16
|
"type": "module",
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
17
20
|
"dependencies": {
|
|
18
21
|
"@clack/prompts": "1.4.0"
|
|
19
22
|
},
|
|
20
23
|
"devDependencies": {
|
|
21
24
|
"typescript": "5.9.3",
|
|
22
|
-
"vitest": "4.1.
|
|
23
|
-
"@raredays/lint": "0.
|
|
25
|
+
"vitest": "4.1.9",
|
|
26
|
+
"@raredays/lint": "0.2.0"
|
|
24
27
|
},
|
|
25
28
|
"engines": {
|
|
26
29
|
"node": ">=20"
|
|
@@ -34,6 +37,7 @@
|
|
|
34
37
|
"style": "raredays style",
|
|
35
38
|
"style:fix": "raredays style:fix",
|
|
36
39
|
"fix": "raredays fix",
|
|
40
|
+
"publint": "publint run --strict",
|
|
37
41
|
"check": "raredays check",
|
|
38
42
|
"test": "vitest run",
|
|
39
43
|
"test:watch": "vitest",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
24.15.0
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Semgrep ignore — paths excluded from SAST scans.
|
|
2
|
+
#
|
|
3
|
+
# Add specific paths or globs here when a rule produces unavoidable
|
|
4
|
+
# false positives (e.g. mock servers using http://localhost, generated
|
|
5
|
+
# code that trips security patterns).
|
|
6
|
+
#
|
|
7
|
+
# Example:
|
|
8
|
+
# src/mocks/*.test.ts
|
|
9
|
+
#
|
|
10
|
+
# Prefer inline `// nosemgrep: <rule-id> -- <reason>` over path-level
|
|
11
|
+
# exclusions when the case is local; reserve this file for whole files
|
|
12
|
+
# that legitimately need to be skipped.
|
|
@@ -6,6 +6,9 @@ on:
|
|
|
6
6
|
pull_request:
|
|
7
7
|
branches: [main]
|
|
8
8
|
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
9
12
|
concurrency:
|
|
10
13
|
group: ci-${{ github.ref }}
|
|
11
14
|
cancel-in-progress: true
|
|
@@ -20,9 +23,11 @@ jobs:
|
|
|
20
23
|
runs-on: ubuntu-latest
|
|
21
24
|
timeout-minutes: 10
|
|
22
25
|
steps:
|
|
23
|
-
- uses: actions/checkout@
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
27
|
+
with:
|
|
28
|
+
persist-credentials: false
|
|
29
|
+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
30
|
+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
26
31
|
with: { node-version: 24, cache: pnpm }
|
|
27
32
|
- run: pnpm install --frozen-lockfile
|
|
28
33
|
- run: pnpm lint
|
|
@@ -32,9 +37,11 @@ jobs:
|
|
|
32
37
|
runs-on: ubuntu-latest
|
|
33
38
|
timeout-minutes: 10
|
|
34
39
|
steps:
|
|
35
|
-
- uses: actions/checkout@
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
41
|
+
with:
|
|
42
|
+
persist-credentials: false
|
|
43
|
+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
44
|
+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
38
45
|
with: { node-version: 24, cache: pnpm }
|
|
39
46
|
- run: pnpm install --frozen-lockfile
|
|
40
47
|
- run: pnpm format:check
|
|
@@ -44,9 +51,11 @@ jobs:
|
|
|
44
51
|
runs-on: ubuntu-latest
|
|
45
52
|
timeout-minutes: 10
|
|
46
53
|
steps:
|
|
47
|
-
- uses: actions/checkout@
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
55
|
+
with:
|
|
56
|
+
persist-credentials: false
|
|
57
|
+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
58
|
+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
50
59
|
with: { node-version: 24, cache: pnpm }
|
|
51
60
|
- run: pnpm install --frozen-lockfile
|
|
52
61
|
- run: pnpm style
|
|
@@ -56,9 +65,11 @@ jobs:
|
|
|
56
65
|
runs-on: ubuntu-latest
|
|
57
66
|
timeout-minutes: 10
|
|
58
67
|
steps:
|
|
59
|
-
- uses: actions/checkout@
|
|
60
|
-
|
|
61
|
-
|
|
68
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
69
|
+
with:
|
|
70
|
+
persist-credentials: false
|
|
71
|
+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
72
|
+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
62
73
|
with: { node-version: 24, cache: pnpm }
|
|
63
74
|
- run: pnpm install --frozen-lockfile
|
|
64
75
|
- run: pnpm theme:build
|
|
@@ -69,9 +80,11 @@ jobs:
|
|
|
69
80
|
runs-on: ubuntu-latest
|
|
70
81
|
timeout-minutes: 15
|
|
71
82
|
steps:
|
|
72
|
-
- uses: actions/checkout@
|
|
73
|
-
|
|
74
|
-
|
|
83
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
84
|
+
with:
|
|
85
|
+
persist-credentials: false
|
|
86
|
+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
87
|
+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
75
88
|
with: { node-version: 24, cache: pnpm }
|
|
76
89
|
- run: pnpm install --frozen-lockfile
|
|
77
90
|
- run: pnpm theme:build
|
|
@@ -82,9 +95,11 @@ jobs:
|
|
|
82
95
|
runs-on: ubuntu-latest
|
|
83
96
|
timeout-minutes: 15
|
|
84
97
|
steps:
|
|
85
|
-
- uses: actions/checkout@
|
|
86
|
-
|
|
87
|
-
|
|
98
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
99
|
+
with:
|
|
100
|
+
persist-credentials: false
|
|
101
|
+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
102
|
+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
88
103
|
with: { node-version: 24, cache: pnpm }
|
|
89
104
|
- run: pnpm install --frozen-lockfile
|
|
90
105
|
- run: pnpm build
|
|
@@ -93,12 +108,38 @@ jobs:
|
|
|
93
108
|
name: Storybook build
|
|
94
109
|
runs-on: ubuntu-latest
|
|
95
110
|
timeout-minutes: 15
|
|
96
|
-
if: hashFiles('.storybook/main.ts', '.storybook/main.js') != ''
|
|
97
111
|
steps:
|
|
98
|
-
- uses: actions/checkout@
|
|
99
|
-
|
|
100
|
-
|
|
112
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
113
|
+
with:
|
|
114
|
+
persist-credentials: false
|
|
115
|
+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
116
|
+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
101
117
|
with: { node-version: 24, cache: pnpm }
|
|
102
118
|
- run: pnpm install --frozen-lockfile
|
|
103
119
|
- run: pnpm theme:build
|
|
104
120
|
- run: pnpm storybook:build
|
|
121
|
+
|
|
122
|
+
# Single aggregation gate — make THIS the only required status check in
|
|
123
|
+
# branch protection so you don't have to enumerate every job (skipped
|
|
124
|
+
# jobs are tolerated by the gate).
|
|
125
|
+
ci-result:
|
|
126
|
+
name: ci-result
|
|
127
|
+
needs: [lint, format-check, style, typecheck, test, build, storybook]
|
|
128
|
+
if: ${{ !cancelled() }}
|
|
129
|
+
runs-on: ubuntu-latest
|
|
130
|
+
permissions: {}
|
|
131
|
+
steps:
|
|
132
|
+
- name: Aggregate CI results
|
|
133
|
+
env:
|
|
134
|
+
NEEDS: ${{ toJson(needs) }}
|
|
135
|
+
run: |
|
|
136
|
+
echo "$NEEDS" | jq -r 'to_entries[] | "\(.key)\t\(.value.result)"'
|
|
137
|
+
failed=$(echo "$NEEDS" | jq -r '
|
|
138
|
+
to_entries[]
|
|
139
|
+
| select(.value.result == "failure" or .value.result == "cancelled")
|
|
140
|
+
| .key')
|
|
141
|
+
if [ -n "$failed" ]; then
|
|
142
|
+
echo "::error::Failed or cancelled jobs: $failed"
|
|
143
|
+
exit 1
|
|
144
|
+
fi
|
|
145
|
+
echo "All required CI jobs passed (or were skipped)."
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: Coverage
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: coverage-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
# Runs Vitest with --coverage and uploads the HTML report as an artifact.
|
|
17
|
+
# Coverage thresholds are not enforced by default — opt in by configuring
|
|
18
|
+
# `test.coverage.thresholds` in vitest.config.ts when your suite is ready.
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
coverage:
|
|
22
|
+
name: Coverage
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
timeout-minutes: 15
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
27
|
+
with:
|
|
28
|
+
persist-credentials: false
|
|
29
|
+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
30
|
+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
31
|
+
with: { node-version: 24, cache: pnpm }
|
|
32
|
+
- run: pnpm install --frozen-lockfile
|
|
33
|
+
- run: pnpm theme:build
|
|
34
|
+
- run: pnpm coverage
|
|
35
|
+
- name: Upload coverage report
|
|
36
|
+
if: always()
|
|
37
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
38
|
+
with:
|
|
39
|
+
name: coverage-report
|
|
40
|
+
path: coverage/
|
|
41
|
+
retention-days: 14
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: SAST
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: sast-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
# Static Application Security Testing via Semgrep. Scans first-party
|
|
17
|
+
# source against TypeScript, JavaScript, React, and OWASP top-ten
|
|
18
|
+
# rulesets. Findings fail the build — silence false positives with
|
|
19
|
+
# `.semgrepignore` (path-level) or inline `// nosemgrep: <rule-id>` with
|
|
20
|
+
# a justification.
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
semgrep:
|
|
24
|
+
name: Semgrep
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
timeout-minutes: 10
|
|
27
|
+
if: github.actor != 'dependabot[bot]'
|
|
28
|
+
container:
|
|
29
|
+
image: semgrep/semgrep:1.168.0@sha256:59fbed6127ea7c5dde3ba6a85142733bb20ea9aaa36120c953904f1539aaf66e
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
32
|
+
with:
|
|
33
|
+
persist-credentials: false
|
|
34
|
+
- name: Run Semgrep
|
|
35
|
+
run: |
|
|
36
|
+
semgrep scan \
|
|
37
|
+
--config=p/typescript \
|
|
38
|
+
--config=p/javascript \
|
|
39
|
+
--config=p/react \
|
|
40
|
+
--config=p/owasp-top-ten \
|
|
41
|
+
--error \
|
|
42
|
+
--metrics=off
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: Security
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: security-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
# Audits production dependencies for known vulnerabilities. Fails the
|
|
17
|
+
# build on high or critical severity findings; moderate and low findings
|
|
18
|
+
# appear in the log but don't block merges.
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
audit:
|
|
22
|
+
name: pnpm audit
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
timeout-minutes: 5
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
27
|
+
with:
|
|
28
|
+
persist-credentials: false
|
|
29
|
+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
30
|
+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
31
|
+
with: { node-version: 24, cache: pnpm }
|
|
32
|
+
- run: pnpm install --frozen-lockfile
|
|
33
|
+
- name: Audit production dependencies
|
|
34
|
+
run: pnpm audit --audit-level=high --prod
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: Zizmor
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
paths: [".github/**"]
|
|
7
|
+
pull_request:
|
|
8
|
+
branches: [main]
|
|
9
|
+
paths: [".github/**"]
|
|
10
|
+
|
|
11
|
+
permissions: {}
|
|
12
|
+
|
|
13
|
+
concurrency:
|
|
14
|
+
group: zizmor-${{ github.ref }}
|
|
15
|
+
cancel-in-progress: true
|
|
16
|
+
|
|
17
|
+
# Static security analysis of this app's GitHub Actions workflows
|
|
18
|
+
# (template-injection, unpinned actions, excessive token permissions,
|
|
19
|
+
# credential persistence). Free runner-hosted SAST — no GitHub Advanced
|
|
20
|
+
# Security needed. Findings surface as inline PR annotations. Starts at
|
|
21
|
+
# min-severity: high; lower to medium/low once your workflows are clean.
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
zizmor:
|
|
25
|
+
name: GitHub Actions security (zizmor)
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
timeout-minutes: 10
|
|
28
|
+
permissions:
|
|
29
|
+
contents: read
|
|
30
|
+
actions: read
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
33
|
+
with:
|
|
34
|
+
persist-credentials: false
|
|
35
|
+
- uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
|
|
36
|
+
with:
|
|
37
|
+
min-severity: high
|
|
38
|
+
min-confidence: medium
|
|
39
|
+
# This app's repo may not have GitHub Advanced Security; disable the
|
|
40
|
+
# SARIF/code-scanning upload (mutually exclusive with annotations) and
|
|
41
|
+
# surface findings as inline PR annotations instead.
|
|
42
|
+
advanced-security: false
|
|
43
|
+
annotations: true
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"theme:build": "node ./scripts/build-theme.mjs",
|
|
13
13
|
"test": "vitest run",
|
|
14
14
|
"test:watch": "vitest",
|
|
15
|
+
"coverage": "vitest run --coverage",
|
|
15
16
|
"storybook": "storybook dev -p 6006",
|
|
16
17
|
"storybook:build": "storybook build -o storybook-static",
|
|
17
18
|
"lint": "raredays lint",
|
|
@@ -48,6 +49,7 @@
|
|
|
48
49
|
"@types/react": "19.2.14",
|
|
49
50
|
"@types/react-dom": "19.2.3",
|
|
50
51
|
"@vitejs/plugin-react": "6.0.1",
|
|
52
|
+
"@vitest/coverage-v8": "4.1.5",
|
|
51
53
|
"happy-dom": "15.11.7",
|
|
52
54
|
"storybook": "10.3.6",
|
|
53
55
|
"tailwindcss": "4.2.4",
|
|
@@ -57,5 +59,6 @@
|
|
|
57
59
|
},
|
|
58
60
|
"engines": {
|
|
59
61
|
"node": ">=24"
|
|
60
|
-
}
|
|
62
|
+
},
|
|
63
|
+
"packageManager": "pnpm@11.9.0"
|
|
61
64
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# pnpm 11 blocks dependency build scripts by default, and its pre-run deps
|
|
2
|
+
# check then aborts every pnpm command (install/build/exec) with
|
|
3
|
+
# ERR_PNPM_IGNORED_BUILDS. Pre-approve the builds this app actually needs so a
|
|
4
|
+
# fresh scaffold works out of the box. (In pnpm 11 `allowBuilds` replaced
|
|
5
|
+
# `onlyBuiltDependencies`; values are booleans, not a list.)
|
|
6
|
+
allowBuilds:
|
|
7
|
+
esbuild: true
|
|
8
|
+
msw: true
|
|
9
|
+
sharp: true
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
24.15.0
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Semgrep ignore — paths excluded from SAST scans.
|
|
2
|
+
#
|
|
3
|
+
# Add specific paths or globs here when a rule produces unavoidable
|
|
4
|
+
# false positives (e.g. mock servers using http://localhost, generated
|
|
5
|
+
# code that trips security patterns).
|
|
6
|
+
#
|
|
7
|
+
# Example:
|
|
8
|
+
# src/mocks/*.test.ts
|
|
9
|
+
#
|
|
10
|
+
# Prefer inline `// nosemgrep: <rule-id> -- <reason>` over path-level
|
|
11
|
+
# exclusions when the case is local; reserve this file for whole files
|
|
12
|
+
# that legitimately need to be skipped.
|
|
@@ -6,6 +6,9 @@ on:
|
|
|
6
6
|
pull_request:
|
|
7
7
|
branches: [main]
|
|
8
8
|
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
9
12
|
concurrency:
|
|
10
13
|
group: ci-${{ github.ref }}
|
|
11
14
|
cancel-in-progress: true
|
|
@@ -24,9 +27,11 @@ jobs:
|
|
|
24
27
|
runs-on: ubuntu-latest
|
|
25
28
|
timeout-minutes: 10
|
|
26
29
|
steps:
|
|
27
|
-
- uses: actions/checkout@
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
31
|
+
with:
|
|
32
|
+
persist-credentials: false
|
|
33
|
+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
34
|
+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
30
35
|
with: { node-version: 24, cache: pnpm }
|
|
31
36
|
- run: pnpm install --frozen-lockfile
|
|
32
37
|
- run: pnpm lint
|
|
@@ -36,9 +41,11 @@ jobs:
|
|
|
36
41
|
runs-on: ubuntu-latest
|
|
37
42
|
timeout-minutes: 10
|
|
38
43
|
steps:
|
|
39
|
-
- uses: actions/checkout@
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
45
|
+
with:
|
|
46
|
+
persist-credentials: false
|
|
47
|
+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
48
|
+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
42
49
|
with: { node-version: 24, cache: pnpm }
|
|
43
50
|
- run: pnpm install --frozen-lockfile
|
|
44
51
|
- run: pnpm format:check
|
|
@@ -48,9 +55,11 @@ jobs:
|
|
|
48
55
|
runs-on: ubuntu-latest
|
|
49
56
|
timeout-minutes: 10
|
|
50
57
|
steps:
|
|
51
|
-
- uses: actions/checkout@
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
59
|
+
with:
|
|
60
|
+
persist-credentials: false
|
|
61
|
+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
62
|
+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
54
63
|
with: { node-version: 24, cache: pnpm }
|
|
55
64
|
- run: pnpm install --frozen-lockfile
|
|
56
65
|
- run: pnpm style
|
|
@@ -60,9 +69,11 @@ jobs:
|
|
|
60
69
|
runs-on: ubuntu-latest
|
|
61
70
|
timeout-minutes: 10
|
|
62
71
|
steps:
|
|
63
|
-
- uses: actions/checkout@
|
|
64
|
-
|
|
65
|
-
|
|
72
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
73
|
+
with:
|
|
74
|
+
persist-credentials: false
|
|
75
|
+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
76
|
+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
66
77
|
with: { node-version: 24, cache: pnpm }
|
|
67
78
|
- run: pnpm install --frozen-lockfile
|
|
68
79
|
- run: pnpm theme:build
|
|
@@ -73,9 +84,11 @@ jobs:
|
|
|
73
84
|
runs-on: ubuntu-latest
|
|
74
85
|
timeout-minutes: 15
|
|
75
86
|
steps:
|
|
76
|
-
- uses: actions/checkout@
|
|
77
|
-
|
|
78
|
-
|
|
87
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
88
|
+
with:
|
|
89
|
+
persist-credentials: false
|
|
90
|
+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
91
|
+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
79
92
|
with: { node-version: 24, cache: pnpm }
|
|
80
93
|
- run: pnpm install --frozen-lockfile
|
|
81
94
|
- run: pnpm theme:build
|
|
@@ -86,9 +99,11 @@ jobs:
|
|
|
86
99
|
runs-on: ubuntu-latest
|
|
87
100
|
timeout-minutes: 15
|
|
88
101
|
steps:
|
|
89
|
-
- uses: actions/checkout@
|
|
90
|
-
|
|
91
|
-
|
|
102
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
103
|
+
with:
|
|
104
|
+
persist-credentials: false
|
|
105
|
+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
106
|
+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
92
107
|
with: { node-version: 24, cache: pnpm }
|
|
93
108
|
- run: pnpm install --frozen-lockfile
|
|
94
109
|
- run: pnpm build
|
|
@@ -97,13 +112,38 @@ jobs:
|
|
|
97
112
|
name: Storybook build
|
|
98
113
|
runs-on: ubuntu-latest
|
|
99
114
|
timeout-minutes: 15
|
|
100
|
-
# Only run if storybook is configured.
|
|
101
|
-
if: hashFiles('.storybook/main.ts', '.storybook/main.js') != ''
|
|
102
115
|
steps:
|
|
103
|
-
- uses: actions/checkout@
|
|
104
|
-
|
|
105
|
-
|
|
116
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
117
|
+
with:
|
|
118
|
+
persist-credentials: false
|
|
119
|
+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
120
|
+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
106
121
|
with: { node-version: 24, cache: pnpm }
|
|
107
122
|
- run: pnpm install --frozen-lockfile
|
|
108
123
|
- run: pnpm theme:build
|
|
109
124
|
- run: pnpm storybook:build
|
|
125
|
+
|
|
126
|
+
# Single aggregation gate — make THIS the only required status check in
|
|
127
|
+
# branch protection so you don't have to enumerate every job (skipped
|
|
128
|
+
# jobs are tolerated by the gate).
|
|
129
|
+
ci-result:
|
|
130
|
+
name: ci-result
|
|
131
|
+
needs: [lint, format-check, style, typecheck, test, build, storybook]
|
|
132
|
+
if: ${{ !cancelled() }}
|
|
133
|
+
runs-on: ubuntu-latest
|
|
134
|
+
permissions: {}
|
|
135
|
+
steps:
|
|
136
|
+
- name: Aggregate CI results
|
|
137
|
+
env:
|
|
138
|
+
NEEDS: ${{ toJson(needs) }}
|
|
139
|
+
run: |
|
|
140
|
+
echo "$NEEDS" | jq -r 'to_entries[] | "\(.key)\t\(.value.result)"'
|
|
141
|
+
failed=$(echo "$NEEDS" | jq -r '
|
|
142
|
+
to_entries[]
|
|
143
|
+
| select(.value.result == "failure" or .value.result == "cancelled")
|
|
144
|
+
| .key')
|
|
145
|
+
if [ -n "$failed" ]; then
|
|
146
|
+
echo "::error::Failed or cancelled jobs: $failed"
|
|
147
|
+
exit 1
|
|
148
|
+
fi
|
|
149
|
+
echo "All required CI jobs passed (or were skipped)."
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: Coverage
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: coverage-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
# Runs Vitest with --coverage and uploads the HTML report as an artifact.
|
|
17
|
+
# Coverage thresholds are not enforced by default — opt in by configuring
|
|
18
|
+
# `test.coverage.thresholds` in vitest.config.ts when your suite is ready.
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
coverage:
|
|
22
|
+
name: Coverage
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
timeout-minutes: 15
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
27
|
+
with:
|
|
28
|
+
persist-credentials: false
|
|
29
|
+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
30
|
+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
31
|
+
with: { node-version: 24, cache: pnpm }
|
|
32
|
+
- run: pnpm install --frozen-lockfile
|
|
33
|
+
- run: pnpm theme:build
|
|
34
|
+
- run: pnpm coverage
|
|
35
|
+
- name: Upload coverage report
|
|
36
|
+
if: always()
|
|
37
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
38
|
+
with:
|
|
39
|
+
name: coverage-report
|
|
40
|
+
path: coverage/
|
|
41
|
+
retention-days: 14
|
|
@@ -19,13 +19,18 @@ on:
|
|
|
19
19
|
- "package.json"
|
|
20
20
|
- ".github/workflows/db-check.yml"
|
|
21
21
|
|
|
22
|
+
permissions:
|
|
23
|
+
contents: read
|
|
24
|
+
|
|
22
25
|
jobs:
|
|
23
26
|
db-check:
|
|
24
27
|
runs-on: ubuntu-latest
|
|
25
28
|
steps:
|
|
26
|
-
- uses: actions/checkout@
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
30
|
+
with:
|
|
31
|
+
persist-credentials: false
|
|
32
|
+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
33
|
+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
29
34
|
with:
|
|
30
35
|
node-version: 24
|
|
31
36
|
cache: pnpm
|
|
@@ -15,6 +15,9 @@ on:
|
|
|
15
15
|
- ".github/workflows/db-migrate.yml"
|
|
16
16
|
workflow_dispatch:
|
|
17
17
|
|
|
18
|
+
permissions:
|
|
19
|
+
contents: read
|
|
20
|
+
|
|
18
21
|
concurrency:
|
|
19
22
|
# Advisory lock at the workflow layer too — defense in depth alongside
|
|
20
23
|
# safeMigrate's per-database advisory lock.
|
|
@@ -26,9 +29,11 @@ jobs:
|
|
|
26
29
|
runs-on: ubuntu-latest
|
|
27
30
|
environment: production
|
|
28
31
|
steps:
|
|
29
|
-
- uses: actions/checkout@
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
33
|
+
with:
|
|
34
|
+
persist-credentials: false
|
|
35
|
+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
36
|
+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
32
37
|
with:
|
|
33
38
|
node-version: 24
|
|
34
39
|
cache: pnpm
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: SAST
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: sast-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
# Static Application Security Testing via Semgrep. Scans first-party
|
|
17
|
+
# source against TypeScript, JavaScript, React, and OWASP top-ten
|
|
18
|
+
# rulesets. Findings fail the build — silence false positives with
|
|
19
|
+
# `.semgrepignore` (path-level) or inline `// nosemgrep: <rule-id>` with
|
|
20
|
+
# a justification.
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
semgrep:
|
|
24
|
+
name: Semgrep
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
timeout-minutes: 10
|
|
27
|
+
if: github.actor != 'dependabot[bot]'
|
|
28
|
+
container:
|
|
29
|
+
image: semgrep/semgrep:1.168.0@sha256:59fbed6127ea7c5dde3ba6a85142733bb20ea9aaa36120c953904f1539aaf66e
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
32
|
+
with:
|
|
33
|
+
persist-credentials: false
|
|
34
|
+
- name: Run Semgrep
|
|
35
|
+
run: |
|
|
36
|
+
semgrep scan \
|
|
37
|
+
--config=p/typescript \
|
|
38
|
+
--config=p/javascript \
|
|
39
|
+
--config=p/react \
|
|
40
|
+
--config=p/owasp-top-ten \
|
|
41
|
+
--error \
|
|
42
|
+
--metrics=off
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: Security
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: security-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
# Audits production dependencies for known vulnerabilities. Fails the
|
|
17
|
+
# build on high or critical severity findings; moderate and low findings
|
|
18
|
+
# appear in the log but don't block merges.
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
audit:
|
|
22
|
+
name: pnpm audit
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
timeout-minutes: 5
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
27
|
+
with:
|
|
28
|
+
persist-credentials: false
|
|
29
|
+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
30
|
+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
31
|
+
with: { node-version: 24, cache: pnpm }
|
|
32
|
+
- run: pnpm install --frozen-lockfile
|
|
33
|
+
- name: Audit production dependencies
|
|
34
|
+
run: pnpm audit --audit-level=high --prod
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: Zizmor
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
paths: [".github/**"]
|
|
7
|
+
pull_request:
|
|
8
|
+
branches: [main]
|
|
9
|
+
paths: [".github/**"]
|
|
10
|
+
|
|
11
|
+
permissions: {}
|
|
12
|
+
|
|
13
|
+
concurrency:
|
|
14
|
+
group: zizmor-${{ github.ref }}
|
|
15
|
+
cancel-in-progress: true
|
|
16
|
+
|
|
17
|
+
# Static security analysis of this app's GitHub Actions workflows
|
|
18
|
+
# (template-injection, unpinned actions, excessive token permissions,
|
|
19
|
+
# credential persistence). Free runner-hosted SAST — no GitHub Advanced
|
|
20
|
+
# Security needed. Findings surface as inline PR annotations. Starts at
|
|
21
|
+
# min-severity: high; lower to medium/low once your workflows are clean.
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
zizmor:
|
|
25
|
+
name: GitHub Actions security (zizmor)
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
timeout-minutes: 10
|
|
28
|
+
permissions:
|
|
29
|
+
contents: read
|
|
30
|
+
actions: read
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
33
|
+
with:
|
|
34
|
+
persist-credentials: false
|
|
35
|
+
- uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
|
|
36
|
+
with:
|
|
37
|
+
min-severity: high
|
|
38
|
+
min-confidence: medium
|
|
39
|
+
# This app's repo may not have GitHub Advanced Security; disable the
|
|
40
|
+
# SARIF/code-scanning upload (mutually exclusive with annotations) and
|
|
41
|
+
# surface findings as inline PR annotations instead.
|
|
42
|
+
advanced-security: false
|
|
43
|
+
annotations: true
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"theme:build": "node ./scripts/build-theme.mjs",
|
|
13
13
|
"test": "vitest run",
|
|
14
14
|
"test:watch": "vitest",
|
|
15
|
+
"coverage": "vitest run --coverage",
|
|
15
16
|
"storybook": "storybook dev -p 6006",
|
|
16
17
|
"storybook:build": "storybook build -o storybook-static",
|
|
17
18
|
"db:generate": "drizzle-kit generate",
|
|
@@ -58,6 +59,7 @@
|
|
|
58
59
|
"@storybook/react-vite": "10.3.6",
|
|
59
60
|
"@tailwindcss/vite": "4.2.4",
|
|
60
61
|
"@vitejs/plugin-react": "6.0.1",
|
|
62
|
+
"@vitest/coverage-v8": "4.1.5",
|
|
61
63
|
"drizzle-kit": "0.31.10",
|
|
62
64
|
"happy-dom": "15.11.7",
|
|
63
65
|
"storybook": "10.3.6",
|
|
@@ -68,5 +70,6 @@
|
|
|
68
70
|
},
|
|
69
71
|
"engines": {
|
|
70
72
|
"node": ">=24"
|
|
71
|
-
}
|
|
73
|
+
},
|
|
74
|
+
"packageManager": "pnpm@11.9.0"
|
|
72
75
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# pnpm 11 blocks dependency build scripts by default, and its pre-run deps
|
|
2
|
+
# check then aborts every pnpm command (install/build/exec) with
|
|
3
|
+
# ERR_PNPM_IGNORED_BUILDS. Pre-approve the builds this app actually needs so a
|
|
4
|
+
# fresh scaffold works out of the box. (In pnpm 11 `allowBuilds` replaced
|
|
5
|
+
# `onlyBuiltDependencies`; values are booleans, not a list.)
|
|
6
|
+
allowBuilds:
|
|
7
|
+
esbuild: true
|
|
8
|
+
msw: true
|
|
9
|
+
sharp: true
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
24.15.0
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Semgrep ignore — paths excluded from SAST scans.
|
|
2
|
+
#
|
|
3
|
+
# Add specific paths or globs here when a rule produces unavoidable
|
|
4
|
+
# false positives (e.g. mock servers using http://localhost, generated
|
|
5
|
+
# code that trips security patterns).
|
|
6
|
+
#
|
|
7
|
+
# Example:
|
|
8
|
+
# src/mocks/*.test.ts
|
|
9
|
+
#
|
|
10
|
+
# Prefer inline `// nosemgrep: <rule-id> -- <reason>` over path-level
|
|
11
|
+
# exclusions when the case is local; reserve this file for whole files
|
|
12
|
+
# that legitimately need to be skipped.
|
|
@@ -6,6 +6,9 @@ on:
|
|
|
6
6
|
pull_request:
|
|
7
7
|
branches: [main]
|
|
8
8
|
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
9
12
|
concurrency:
|
|
10
13
|
group: ci-${{ github.ref }}
|
|
11
14
|
cancel-in-progress: true
|
|
@@ -20,9 +23,11 @@ jobs:
|
|
|
20
23
|
runs-on: ubuntu-latest
|
|
21
24
|
timeout-minutes: 10
|
|
22
25
|
steps:
|
|
23
|
-
- uses: actions/checkout@
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
27
|
+
with:
|
|
28
|
+
persist-credentials: false
|
|
29
|
+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
30
|
+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
26
31
|
with: { node-version: 24, cache: pnpm }
|
|
27
32
|
- run: pnpm install --frozen-lockfile
|
|
28
33
|
- run: pnpm lint
|
|
@@ -32,9 +37,11 @@ jobs:
|
|
|
32
37
|
runs-on: ubuntu-latest
|
|
33
38
|
timeout-minutes: 10
|
|
34
39
|
steps:
|
|
35
|
-
- uses: actions/checkout@
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
41
|
+
with:
|
|
42
|
+
persist-credentials: false
|
|
43
|
+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
44
|
+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
38
45
|
with: { node-version: 24, cache: pnpm }
|
|
39
46
|
- run: pnpm install --frozen-lockfile
|
|
40
47
|
- run: pnpm format:check
|
|
@@ -44,9 +51,11 @@ jobs:
|
|
|
44
51
|
runs-on: ubuntu-latest
|
|
45
52
|
timeout-minutes: 10
|
|
46
53
|
steps:
|
|
47
|
-
- uses: actions/checkout@
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
55
|
+
with:
|
|
56
|
+
persist-credentials: false
|
|
57
|
+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
58
|
+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
50
59
|
with: { node-version: 24, cache: pnpm }
|
|
51
60
|
- run: pnpm install --frozen-lockfile
|
|
52
61
|
- run: pnpm style
|
|
@@ -56,9 +65,11 @@ jobs:
|
|
|
56
65
|
runs-on: ubuntu-latest
|
|
57
66
|
timeout-minutes: 10
|
|
58
67
|
steps:
|
|
59
|
-
- uses: actions/checkout@
|
|
60
|
-
|
|
61
|
-
|
|
68
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
69
|
+
with:
|
|
70
|
+
persist-credentials: false
|
|
71
|
+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
72
|
+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
62
73
|
with: { node-version: 24, cache: pnpm }
|
|
63
74
|
- run: pnpm install --frozen-lockfile
|
|
64
75
|
- run: pnpm theme:build
|
|
@@ -69,9 +80,11 @@ jobs:
|
|
|
69
80
|
runs-on: ubuntu-latest
|
|
70
81
|
timeout-minutes: 15
|
|
71
82
|
steps:
|
|
72
|
-
- uses: actions/checkout@
|
|
73
|
-
|
|
74
|
-
|
|
83
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
84
|
+
with:
|
|
85
|
+
persist-credentials: false
|
|
86
|
+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
87
|
+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
75
88
|
with: { node-version: 24, cache: pnpm }
|
|
76
89
|
- run: pnpm install --frozen-lockfile
|
|
77
90
|
- run: pnpm build
|
|
@@ -80,12 +93,38 @@ jobs:
|
|
|
80
93
|
name: Storybook build
|
|
81
94
|
runs-on: ubuntu-latest
|
|
82
95
|
timeout-minutes: 15
|
|
83
|
-
if: hashFiles('.storybook/main.ts', '.storybook/main.js') != ''
|
|
84
96
|
steps:
|
|
85
|
-
- uses: actions/checkout@
|
|
86
|
-
|
|
87
|
-
|
|
97
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
98
|
+
with:
|
|
99
|
+
persist-credentials: false
|
|
100
|
+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
101
|
+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
88
102
|
with: { node-version: 24, cache: pnpm }
|
|
89
103
|
- run: pnpm install --frozen-lockfile
|
|
90
104
|
- run: pnpm theme:build
|
|
91
105
|
- run: pnpm storybook:build
|
|
106
|
+
|
|
107
|
+
# Single aggregation gate — make THIS the only required status check in
|
|
108
|
+
# branch protection so you don't have to enumerate every job (skipped
|
|
109
|
+
# jobs are tolerated by the gate).
|
|
110
|
+
ci-result:
|
|
111
|
+
name: ci-result
|
|
112
|
+
needs: [lint, format-check, style, typecheck, build, storybook]
|
|
113
|
+
if: ${{ !cancelled() }}
|
|
114
|
+
runs-on: ubuntu-latest
|
|
115
|
+
permissions: {}
|
|
116
|
+
steps:
|
|
117
|
+
- name: Aggregate CI results
|
|
118
|
+
env:
|
|
119
|
+
NEEDS: ${{ toJson(needs) }}
|
|
120
|
+
run: |
|
|
121
|
+
echo "$NEEDS" | jq -r 'to_entries[] | "\(.key)\t\(.value.result)"'
|
|
122
|
+
failed=$(echo "$NEEDS" | jq -r '
|
|
123
|
+
to_entries[]
|
|
124
|
+
| select(.value.result == "failure" or .value.result == "cancelled")
|
|
125
|
+
| .key')
|
|
126
|
+
if [ -n "$failed" ]; then
|
|
127
|
+
echo "::error::Failed or cancelled jobs: $failed"
|
|
128
|
+
exit 1
|
|
129
|
+
fi
|
|
130
|
+
echo "All required CI jobs passed (or were skipped)."
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: SAST
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: sast-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
# Static Application Security Testing via Semgrep. Scans first-party
|
|
17
|
+
# source against TypeScript, JavaScript, React, and OWASP top-ten
|
|
18
|
+
# rulesets. Findings fail the build — silence false positives with
|
|
19
|
+
# `.semgrepignore` (path-level) or inline `// nosemgrep: <rule-id>` with
|
|
20
|
+
# a justification.
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
semgrep:
|
|
24
|
+
name: Semgrep
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
timeout-minutes: 10
|
|
27
|
+
if: github.actor != 'dependabot[bot]'
|
|
28
|
+
container:
|
|
29
|
+
image: semgrep/semgrep:1.168.0@sha256:59fbed6127ea7c5dde3ba6a85142733bb20ea9aaa36120c953904f1539aaf66e
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
32
|
+
with:
|
|
33
|
+
persist-credentials: false
|
|
34
|
+
- name: Run Semgrep
|
|
35
|
+
run: |
|
|
36
|
+
semgrep scan \
|
|
37
|
+
--config=p/typescript \
|
|
38
|
+
--config=p/javascript \
|
|
39
|
+
--config=p/react \
|
|
40
|
+
--config=p/owasp-top-ten \
|
|
41
|
+
--error \
|
|
42
|
+
--metrics=off
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: Security
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: security-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
# Audits production dependencies for known vulnerabilities. Fails the
|
|
17
|
+
# build on high or critical severity findings; moderate and low findings
|
|
18
|
+
# appear in the log but don't block merges.
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
audit:
|
|
22
|
+
name: pnpm audit
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
timeout-minutes: 5
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
27
|
+
with:
|
|
28
|
+
persist-credentials: false
|
|
29
|
+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
30
|
+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
31
|
+
with: { node-version: 24, cache: pnpm }
|
|
32
|
+
- run: pnpm install --frozen-lockfile
|
|
33
|
+
- name: Audit production dependencies
|
|
34
|
+
run: pnpm audit --audit-level=high --prod
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: Zizmor
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
paths: [".github/**"]
|
|
7
|
+
pull_request:
|
|
8
|
+
branches: [main]
|
|
9
|
+
paths: [".github/**"]
|
|
10
|
+
|
|
11
|
+
permissions: {}
|
|
12
|
+
|
|
13
|
+
concurrency:
|
|
14
|
+
group: zizmor-${{ github.ref }}
|
|
15
|
+
cancel-in-progress: true
|
|
16
|
+
|
|
17
|
+
# Static security analysis of this app's GitHub Actions workflows
|
|
18
|
+
# (template-injection, unpinned actions, excessive token permissions,
|
|
19
|
+
# credential persistence). Free runner-hosted SAST — no GitHub Advanced
|
|
20
|
+
# Security needed. Findings surface as inline PR annotations. Starts at
|
|
21
|
+
# min-severity: high; lower to medium/low once your workflows are clean.
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
zizmor:
|
|
25
|
+
name: GitHub Actions security (zizmor)
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
timeout-minutes: 10
|
|
28
|
+
permissions:
|
|
29
|
+
contents: read
|
|
30
|
+
actions: read
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
33
|
+
with:
|
|
34
|
+
persist-credentials: false
|
|
35
|
+
- uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
|
|
36
|
+
with:
|
|
37
|
+
min-severity: high
|
|
38
|
+
min-confidence: medium
|
|
39
|
+
# This app's repo may not have GitHub Advanced Security; disable the
|
|
40
|
+
# SARIF/code-scanning upload (mutually exclusive with annotations) and
|
|
41
|
+
# surface findings as inline PR annotations instead.
|
|
42
|
+
advanced-security: false
|
|
43
|
+
annotations: true
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"@raredays/components": "^0.1.0",
|
|
26
26
|
"@raredays/core": "^0.1.0",
|
|
27
27
|
"@raredays/style": "^0.1.0",
|
|
28
|
+
"lucide-react": "1.14.0",
|
|
28
29
|
"next": "16.2.4",
|
|
29
30
|
"react": "19.2.5",
|
|
30
31
|
"react-dom": "19.2.5"
|
|
@@ -49,5 +50,6 @@
|
|
|
49
50
|
},
|
|
50
51
|
"engines": {
|
|
51
52
|
"node": ">=24"
|
|
52
|
-
}
|
|
53
|
+
},
|
|
54
|
+
"packageManager": "pnpm@11.9.0"
|
|
53
55
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# pnpm 11 blocks dependency build scripts by default, and its pre-run deps
|
|
2
|
+
# check then aborts every pnpm command (install/build/exec) with
|
|
3
|
+
# ERR_PNPM_IGNORED_BUILDS. Pre-approve the builds this app actually needs so a
|
|
4
|
+
# fresh scaffold works out of the box. (In pnpm 11 `allowBuilds` replaced
|
|
5
|
+
# `onlyBuiltDependencies`; values are booleans, not a list.)
|
|
6
|
+
allowBuilds:
|
|
7
|
+
esbuild: true
|
|
8
|
+
sharp: true
|