Skip to content

Commit 71b29d4

Browse files
Merge pull request #698 from STX-Boot/feat/issues-138-140-145-150-graceful-shutdown-eslint-sanity-codeql
feat: graceful shutdown/readiness split, ESLint backend, repo sanity …
2 parents c62aec6 + ff0ffce commit 71b29d4

6 files changed

Lines changed: 146 additions & 7 deletions

File tree

.github/workflows/backend-ci.yml

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,8 @@ jobs:
2525
- name: Install dependencies
2626
run: npm ci
2727

28-
- name: Run backend lint (if available)
29-
run: |
30-
if npm --workspace=backend run | rg -q " lint"; then
31-
npm run lint --workspace=backend
32-
else
33-
echo "No backend lint script found; skipping."
34-
fi
28+
- name: Run backend lint
29+
run: npm run lint --workspace=backend
3530

3631
- name: Run backend typecheck (if available)
3732
run: |

.github/workflows/codeql.yml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
name: CodeQL
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
schedule:
9+
# Weekly on Sunday at 02:00 UTC
10+
- cron: '0 2 * * 0'
11+
12+
jobs:
13+
analyze-javascript:
14+
name: Analyze JavaScript
15+
runs-on: ubuntu-latest
16+
permissions:
17+
security-events: write
18+
actions: read
19+
contents: read
20+
21+
steps:
22+
- name: Check out repository
23+
uses: actions/checkout@v4
24+
25+
- name: Initialize CodeQL
26+
uses: github/codeql-action/init@v3
27+
with:
28+
languages: javascript
29+
queries: security-extended
30+
31+
- name: Perform CodeQL Analysis
32+
uses: github/codeql-action/analyze@v3
33+
with:
34+
category: /language:javascript
35+
36+
analyze-rust:
37+
name: Analyze Rust
38+
runs-on: ubuntu-latest
39+
permissions:
40+
security-events: write
41+
actions: read
42+
contents: read
43+
44+
steps:
45+
- name: Check out repository
46+
uses: actions/checkout@v4
47+
48+
- name: Install Rust toolchain
49+
uses: dtolnay/rust-toolchain@stable
50+
51+
- name: Initialize CodeQL
52+
uses: github/codeql-action/init@v3
53+
with:
54+
languages: rust
55+
queries: security-extended
56+
57+
- name: Build Rust workspace
58+
run: cargo build --workspace
59+
timeout-minutes: 20
60+
61+
- name: Perform CodeQL Analysis
62+
uses: github/codeql-action/analyze@v3
63+
with:
64+
category: /language:rust

.github/workflows/repo-sanity.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: Repo Sanity
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
push:
7+
branches: [main]
8+
9+
jobs:
10+
sanity:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Check out repository
15+
uses: actions/checkout@v4
16+
17+
- name: Set up Node.js
18+
uses: actions/setup-node@v4
19+
with:
20+
node-version: 20
21+
cache: npm
22+
23+
- name: Workspace install (npm ci)
24+
run: npm ci
25+
26+
- name: Verify lockfile is up to date
27+
run: |
28+
npm install --package-lock-only --ignore-scripts
29+
git diff --exit-code package-lock.json || {
30+
echo "package-lock.json is out of date. Run 'npm install' locally and commit the updated lockfile."
31+
exit 1
32+
}
33+
34+
- name: npm audit
35+
run: npm audit --audit-level=high
36+
37+
- name: Workspace dependency check
38+
run: npm ls --workspaces --depth=0

backend/eslint.config.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import js from '@eslint/js';
2+
import globals from 'globals';
3+
4+
export default [
5+
js.configs.recommended,
6+
{
7+
languageOptions: {
8+
ecmaVersion: 2022,
9+
sourceType: 'module',
10+
globals: {
11+
...globals.node,
12+
},
13+
},
14+
rules: {
15+
'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
16+
'no-console': 'off',
17+
},
18+
},
19+
{
20+
ignores: ['node_modules/', 'coverage/'],
21+
},
22+
];

backend/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"test": "node --test src/**/*.test.js",
1111
"test:integration": "node --test src/integration/*.test.js",
1212
"test:contract": "node --test src/integration/openapi-contract.test.js",
13+
"lint": "eslint src/",
1314
"typecheck": "tsc --noEmit --project jsconfig.json",
1415
"db:migrate": "node src/db/migrate.js",
1516
"openapi:validate": "node scripts/validateOpenApi.js",
@@ -46,6 +47,9 @@
4647
"zod": "^3.23.0"
4748
},
4849
"devDependencies": {
50+
"@eslint/js": "^9.0.0",
51+
"eslint": "^9.0.0",
52+
"globals": "^15.0.0",
4953
"@readme/openapi-parser": "^2.6.0",
5054
"@redocly/cli": "^1.34.5",
5155
"@types/cors": "^2.8.0",

backend/src/index.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,11 +668,20 @@ export async function createApp(options = {}) {
668668
}
669669
}
670670

671+
let isShuttingDown = false;
672+
671673
app.get('/health', async (_req, res) => {
672674
const payload = await buildHealthPayload();
673675
res.json(payload);
674676
});
675677

678+
app.get('/ready', (_req, res) => {
679+
if (isShuttingDown) {
680+
return res.status(503).json({ status: 'shutting_down', ready: false });
681+
}
682+
return res.json({ status: 'ok', ready: true });
683+
});
684+
676685
const siteOrigin =
677686
process.env.SITE_ORIGIN ?? allowedOrigins.find((origin) => origin !== '*') ?? '';
678687

@@ -786,6 +795,7 @@ export async function createApp(options = {}) {
786795
prefix: API_V1_PREFIX,
787796
endpoints: {
788797
health: 'GET /health',
798+
ready: 'GET /ready',
789799
healthRpc: 'GET /health/rpc',
790800
metrics: 'GET /metrics',
791801
info: `GET ${API_V1_PREFIX}`,
@@ -2116,6 +2126,11 @@ export async function createApp(options = {}) {
21162126
// Central error handler — must be registered after all routes
21172127
app.use(errorHandler);
21182128

2129+
app._close = () => {
2130+
isShuttingDown = true;
2131+
try { dal.db.close(); } catch (_) {}
2132+
};
2133+
21192134
return app;
21202135
}
21212136

@@ -2152,6 +2167,7 @@ export async function startServer(options = {}) {
21522167
async function gracefulShutdown(signal) {
21532168
if (shuttingDown) return;
21542169
shuttingDown = true;
2170+
app._close?.();
21552171
log.info({ signal, graceMs: SHUTDOWN_GRACE_MS }, 'graceful shutdown started');
21562172

21572173
const forceTimer = setTimeout(() => {

0 commit comments

Comments
 (0)