Swadhin Biswas

[email protected]
2026-06-04 18:12:12 +0600 committed NaN years ago
ab57b8a
commit ab57b8a
6 files changed
+96 -32
▼.github/workflows/ci.yml
+26-1
176
176
runs-on: ubuntu-latest
177
177
needs: test
178
178
timeout-minutes: 20
179
+ services:
180
+ postgres:
181
+ image: postgres:16-alpine
182
+ env:
183
+ POSTGRES_USER: test
184
+ POSTGRES_PASSWORD: test
185
+ POSTGRES_DB: test
186
+ ports:
187
+ - 5432:5432
188
+ options: >-
189
+ --health-cmd pg_isready
190
+ --health-interval 10s
191
+ --health-timeout 5s
192
+ --health-retries 5
193
+
179
194
steps:
180
195
- uses: actions/checkout@v4
181
196
196
211
- name: Install Playwright browsers
197
212
run: bunx playwright install --with-deps chromium
198
213
214
+ - name: Push Drizzle schema to test database
215
+ env:
216
+ DATABASE_URL: postgresql://test:test@localhost:5432/test
217
+ DATABASE_DRIVER: postgres
218
+ run: bunx drizzle-kit push --force
219
+
199
220
- name: Run E2E tests
200
221
run: bunx playwright test
201
222
env:
202
- DATABASE_URL: "file:./test.db"
223
+ DATABASE_URL: postgresql://test:test@localhost:5432/test
224
+ DATABASE_DRIVER: postgres
203
225
JWT_SECRET: "ci-test-secret-key-for-e2e-tests"
204
226
SITE_URL: "http://localhost:4321"
227
+ INTERNAL_HOOK_SECRET: "ci-test-internal-hook-secret"
228
+ CSRF_SKIP_DEV: "true"
229
+ RATE_LIMIT_SKIP_DEV: "true"
205
230
206
231
- name: Upload Playwright report
207
232
if: always()
208
233
▼playwright.config.ts
+6-2
23
23
reuseExistingServer: !process.env.CI,
24
24
timeout: 120_000,
25
25
env: {
26
- // E2E tests need these to bypass security middleware and ensure the
27
- // pre-receive git hook can reach the dev server on localhost.
26
+ // Inherit the parent process env so CI can inject DATABASE_URL, JWT_SECRET,
27
+ // SITE_URL, etc. without us having to mirror them here.
28
+ ...process.env,
29
+ // E2E-specific overrides: bypass security middleware so tests don't get
30
+ // rate-limited, and pin the site URL to the dev server so the
31
+ // pre-receive git hook can reach it.
28
32
RATE_LIMIT_SKIP_DEV: 'true',
29
33
CSRF_SKIP_DEV: 'true',
30
34
SITE_URL: 'http://localhost:4321',
31
35
▼scripts/security/audit.mjs
+34-16
15
15
* 1 — fail (disallowed high/critical vulnerability present)
16
16
* 2 — script error
17
17
*/
18
-import { existsSync, readFileSync, statSync } from "node:fs";
18
+import { existsSync, readFileSync, statSync, writeFileSync, unlinkSync } from "node:fs";
19
19
import { resolve } from "node:path";
20
20
import { spawnSync } from "node:child_process";
21
21
29
29
30
30
function regenerateLockfile() {
31
31
log("info", "Regenerating package-lock.json from package.json (--package-lock-only)…");
32
- const r = spawnSync(
33
- "npm",
34
- [
35
- "install",
36
- "--package-lock-only",
37
- "--no-audit",
38
- "--no-fund",
39
- `--registry=${AUDIT_REGISTRY}`,
40
- ],
41
- { stdio: "inherit" }
42
- );
43
- if (r.status !== 0) {
44
- log("error", "Failed to regenerate package-lock.json");
45
- process.exit(2);
32
+ // Some npm versions crash with `Cannot read properties of null (reading 'matches')`
33
+ // when the existing lockfile is stale or contains shapes they don't expect.
34
+ // Try the simple invocation first; fall back to deleting and regenerating
35
+ // from scratch; finally fall back to auditing whatever lockfile exists.
36
+ const attempts = [
37
+ ["npm", ["install", "--package-lock-only", "--no-audit", "--no-fund", `--registry=${AUDIT_REGISTRY}`]],
38
+ ["npm", ["install", "--package-lock-only", "--no-audit", "--no-fund", "--ignore-scripts", "--legacy-peer-deps", `--registry=${AUDIT_REGISTRY}`]],
39
+ ];
40
+ for (let i = 0; i < attempts.length; i++) {
41
+ const [cmd, args] = attempts[i];
42
+ const r = spawnSync(cmd, args, { stdio: "inherit" });
43
+ if (r.status === 0) return;
44
+ log("warn", `Lockfile regen attempt ${i + 1} failed (exit ${r.status})`);
45
+ }
46
+ if (existsSync(LOCKFILE)) {
47
+ log("warn", "Falling back to existing package-lock.json (may be stale)");
48
+ return;
46
49
}
50
+ log("error", "Failed to regenerate package-lock.json and no fallback exists");
51
+ process.exit(2);
47
52
}
48
53
49
54
function lockfileIsFresh() {
65
70
66
71
function runAudit() {
67
72
log("info", `Running npm audit (registry=${AUDIT_REGISTRY})…`);
73
+ if (!existsSync(LOCKFILE)) {
74
+ log("error", "No package-lock.json to audit against");
75
+ process.exit(2);
76
+ }
68
77
const r = spawnSync(
69
78
"npm",
70
79
["audit", `--registry=${AUDIT_REGISTRY}`, "--json"],
71
80
{ encoding: "utf8" }
72
81
);
73
82
74
- const report = JSON.parse(r.stdout || "null") ?? {};
83
+ // npm may exit non-zero when vulnerabilities are found AND when the lockfile
84
+ // is malformed. We rely on the JSON report regardless of exit code.
85
+ let report = {};
86
+ try {
87
+ report = JSON.parse(r.stdout || "{}");
88
+ } catch (e) {
89
+ log("error", `Could not parse npm audit output: ${e.message}`);
90
+ if (r.stderr) console.error(r.stderr);
91
+ process.exit(2);
92
+ }
75
93
const meta = report.metadata?.vulnerabilities ?? {};
76
94
const summary = {
77
95
critical: meta.critical ?? 0,
78
96
▼src/db/index.ts
+19-4
26
26
| NodePgDatabase<typeof schema>
27
27
| null = null;
28
28
29
+/**
30
+ * Infer database driver from connection URL when no explicit driver is set.
31
+ * Keeps `DATABASE_DRIVER` authoritative when present (production safety) but
32
+ * makes `file:./test.db` work in CI without extra config.
33
+ */
34
+function inferDriverFromUrl(url: string): DatabaseDriver {
35
+ if (url.startsWith("file:")) return "sqlite";
36
+ if (url.startsWith("libsql:") || url.startsWith("https:")) return "libsql";
37
+ if (url.startsWith("postgres://") || url.startsWith("postgresql://")) return "postgres";
38
+ return "postgres";
39
+}
40
+
29
41
/**
30
42
* Get database configuration from environment
31
43
*/
32
44
function getDbConfig(): { driver: DatabaseDriver; url: string } {
33
- // Prefer Astro/Vite env (loaded from .env), then fallback to process.env for scripts.
45
+ // process.env wins when explicitly set (CI runners, scripts, systemd),
46
+ // then Astro/Vite import.meta.env (loaded from .env), then a sensible default.
47
+ // This way CI can set `DATABASE_URL=file:./test.db` without a .env override.
34
48
const envDriver =
35
- import.meta.env?.DATABASE_DRIVER || process.env.DATABASE_DRIVER;
36
- const envUrl = import.meta.env?.DATABASE_URL || process.env.DATABASE_URL;
49
+ process.env.DATABASE_DRIVER || import.meta.env?.DATABASE_DRIVER;
50
+ const envUrl =
51
+ process.env.DATABASE_URL || import.meta.env?.DATABASE_URL;
37
52
38
53
// Default to PostgreSQL since all schemas use pgTable
39
- const driver = (envDriver || "postgres") as DatabaseDriver;
40
54
const url =
41
55
envUrl || "postgresql://opencodehub:opencodehub@localhost:5432/opencodehub";
56
+ const driver = (envDriver || inferDriverFromUrl(url)) as DatabaseDriver;
42
57
43
58
return { driver, url };
44
59
}
45
60
▼src/lib/auth.ts
+6-6
40
40
let _jwtSecret: Uint8Array | null = null;
41
41
function getJwtSecret(): Uint8Array {
42
42
if (!_jwtSecret) {
43
+ // process.env wins when explicitly set (CI runners, scripts, systemd).
43
44
// Vite/Astro exposes .env values on import.meta.env at build time and
44
- // only injects vars prefixed with PUBLIC_ into the client bundle. Server
45
- // code can read the full set via import.meta.env, but process.env is
46
- // not populated for unprefixed keys. Fall back to process.env for
47
- // production-style deployments.
48
- const fromMeta = (import.meta as ImportMeta & { env?: Record<string, string | undefined> }).env?.JWT_SECRET;
49
- const raw = fromMeta || process.env.JWT_SECRET;
45
+ // only injects vars prefixed with PUBLIC_ into the client bundle, so
46
+ // server code must fall back to import.meta.env for unprefixed keys
47
+ // when .env is the only source.
48
+ const metaEnv = (import.meta as ImportMeta & { env?: Record<string, string | undefined> }).env;
49
+ const raw = process.env.JWT_SECRET || metaEnv?.JWT_SECRET;
50
50
if (!raw) {
51
51
throw new Error("JWT_SECRET environment variable is required");
52
52
}
53
53
▼src/lib/git.ts
+5-3
1562
1562
}
1563
1563
1564
1564
// Use environment variable for site URL, fallback to localhost for development
1565
+ // process.env wins when explicitly set (CI runners, scripts, systemd);
1566
+ // otherwise fall back to import.meta.env (loaded from .env).
1567
+ const metaEnv = (import.meta as ImportMeta & { env?: Record<string, string | undefined> }).env;
1565
1568
const siteUrl =
1566
- (import.meta as ImportMeta & { env?: Record<string, string | undefined> }).env?.SITE_URL ||
1567
1569
process.env.SITE_URL ||
1570
+ metaEnv?.SITE_URL ||
1568
1571
process.env.PUBLIC_URL ||
1569
1572
"http://localhost:3000";
1570
- const metaEnv = (import.meta as ImportMeta & { env?: Record<string, string | undefined> }).env;
1571
- const hookSecret = metaEnv?.INTERNAL_HOOK_SECRET || process.env.INTERNAL_HOOK_SECRET;
1573
+ const hookSecret = process.env.INTERNAL_HOOK_SECRET || metaEnv?.INTERNAL_HOOK_SECRET;
1572
1574
if (!hookSecret) {
1573
1575
throw new Error(
1574
1576
"INTERNAL_HOOK_SECRET environment variable is required to install git hooks",
1575
1577