Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: CI

on:
pull_request:
push:
branches:
- main

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Set explicit least-privilege GITHUB_TOKEN permissions.

This workflow performs read/build/test actions only. Add explicit read-only permissions to avoid inheriting broader defaults.

Proposed hardening
 name: CI
@@
 concurrency:
   group: ci-${{ github.workflow }}-${{ github.ref }}
   cancel-in-progress: true
+
+permissions:
+  contents: read
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml at line 13, Add an explicit least-privilege
GITHUB_TOKEN permissions block to the workflow (instead of relying on defaults)
to restrict token scope for the read/build/test job(s); update the top-level or
job-level definition referenced by jobs to include a permissions mapping (e.g.,
set contents: read and packages: read and any other minimal read permissions
your CI needs) so the GITHUB_TOKEN is not granted broader rights.

build:
name: Lint, type-check, build
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'

- name: Restore turbo cache
uses: actions/cache@v4
with:
path: |
.turbo
node_modules/.cache/turbo
key: ${{ runner.os }}-turbo-${{ hashFiles('pnpm-lock.yaml') }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-${{ hashFiles('pnpm-lock.yaml') }}-
${{ runner.os }}-turbo-

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Lint
run: pnpm lint

- name: Type-check
run: pnpm check-types

- name: Build
run: pnpm build

docker:
name: Docker stack health
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build images
run: docker compose build

- name: Start stack and wait for healthchecks
run: docker compose up -d --wait --wait-timeout 180

- name: Dump compose state on failure
if: failure()
run: |
docker compose ps
docker compose logs --no-color

- name: Tear down stack
if: always()
run: docker compose down -v
3 changes: 3 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Verify workspace (lint, types, build)
run: pnpm lint && pnpm check-types && pnpm build

- name: Configure Git user
run: |
git config --global user.name "github-actions[bot]"
Expand Down
242 changes: 124 additions & 118 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,119 +1,125 @@
# ==========================================
# STAGE 1: Pruner
# ==========================================
FROM node:20-alpine AS pruner
RUN apk add --no-cache libc6-compat
WORKDIR /app
RUN npm install -g turbo
COPY . .
RUN turbo prune --scope=web --scope=@repo/backend --docker

# ==========================================
# STAGE 2: Base & Builder
# ==========================================
FROM node:20-alpine AS builder
RUN apk add --no-cache libc6-compat sqlite
WORKDIR /app

RUN corepack enable && corepack prepare pnpm@10.27.0 --activate

# Copy pruned files
COPY --from=pruner /app/out/json/ .
COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml

RUN pnpm install --frozen-lockfile

COPY --from=pruner /app/out/full/ .

ENV DB_FILE_NAME=file:/app/apps/backend/local.db

# Web app environment variables (build-time)
ARG VITE_BACKEND_URL

ENV VITE_BACKEND_URL=${VITE_BACKEND_URL:-}


RUN pnpm build --filter=web... --filter=@repo/backend...

RUN pnpm --filter @repo/backend --prod deploy --legacy pruned-backend

# ==========================================
# STAGE 3: Backend Runner
# ==========================================
FROM node:20-alpine AS backend
WORKDIR /app

RUN apk add --no-cache sqlite libc6-compat

RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nodejs

COPY --from=builder /app/pruned-backend .
COPY --from=builder /app/apps/backend/dist ./dist
COPY --from=builder /app/apps/backend/drizzle ./drizzle
COPY --from=builder /app/apps/backend/src/scripts/run-migrations.mjs ./run-migrations.mjs

RUN mkdir -p storage && chown -R nodejs:nodejs storage

USER nodejs

ENV NODE_ENV=production
ENV PORT=3000
ENV DB_FILE_NAME=file:storage/local.db
ENV CLIENT_URL=http://localhost:5173

EXPOSE 3000

CMD ["sh", "-c", "node run-migrations.mjs && node dist/index.js"]

# ==========================================
# STAGE 4: Web Runner (Nginx)
# ==========================================
FROM nginx:alpine AS web
WORKDIR /usr/share/nginx/html
RUN rm -rf ./*
COPY --from=builder /app/apps/web/dist .

RUN echo 'server { \
listen 80; \
\
# Enable Gzip compression for faster loading \
gzip on; \
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; \
\
location / { \
root /usr/share/nginx/html; \
index index.html index.htm; \
try_files $uri $uri/ /index.html; \
} \
\
# Proxy API requests to the backend container \
location /api/ { \
proxy_pass http://backend:3000; \
proxy_set_header Host $host; \
proxy_set_header X-Real-IP $remote_addr; \
} \
\
# Proxy storage requests to the backend container \
location /storage/ { \
proxy_pass http://backend:3000; \
proxy_set_header Host $host; \
proxy_set_header X-Real-IP $remote_addr; \
client_max_body_size 10M; \
} \
\
# Proxy WebSocket connections for Socket.io \
location /socket.io/ { \
proxy_pass http://backend:3000; \
proxy_http_version 1.1; \
proxy_set_header Upgrade $http_upgrade; \
proxy_set_header Connection "upgrade"; \
proxy_set_header Host $host; \
proxy_set_header X-Real-IP $remote_addr; \
proxy_read_timeout 86400; \
proxy_send_timeout 86400; \
} \
}' > /etc/nginx/conf.d/default.conf

EXPOSE 80
# ==========================================
# STAGE 1: Pruner
# ==========================================
FROM node:20-alpine AS pruner
RUN apk add --no-cache libc6-compat
WORKDIR /app
RUN npm install -g turbo
COPY . .
RUN turbo prune --scope=web --scope=@repo/backend --docker

# ==========================================
# STAGE 2: Base & Builder
# ==========================================
FROM node:20-alpine AS builder
RUN apk add --no-cache libc6-compat sqlite
WORKDIR /app

RUN corepack enable && corepack prepare pnpm@10.27.0 --activate

# Copy pruned files
COPY --from=pruner /app/out/json/ .
COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml

RUN pnpm install --frozen-lockfile

COPY --from=pruner /app/out/full/ .

ENV DB_FILE_NAME=file:/app/apps/backend/local.db

# Web app environment variables (build-time)
ARG VITE_BACKEND_URL

ENV VITE_BACKEND_URL=${VITE_BACKEND_URL:-}


RUN pnpm build --filter=web... --filter=@repo/backend...

RUN pnpm --filter @repo/backend --prod deploy --legacy pruned-backend

# ==========================================
# STAGE 3: Backend Runner
# ==========================================
FROM node:20-alpine AS backend
WORKDIR /app

RUN apk add --no-cache sqlite libc6-compat

RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nodejs

COPY --from=builder /app/pruned-backend .
COPY --from=builder /app/apps/backend/dist ./dist
COPY --from=builder /app/apps/backend/drizzle ./drizzle
COPY --from=builder /app/apps/backend/src/scripts/run-migrations.mjs ./run-migrations.mjs

RUN mkdir -p storage && chown -R nodejs:nodejs storage

USER nodejs

ENV NODE_ENV=production
ENV PORT=3000
ENV DB_FILE_NAME=file:storage/local.db
ENV CLIENT_URL=http://localhost:5173

EXPOSE 3000

CMD ["sh", "-c", "node run-migrations.mjs && node dist/index.js"]

# ==========================================
# STAGE 4: Web Runner (Nginx)
# ==========================================
FROM nginx:alpine AS web
WORKDIR /usr/share/nginx/html
RUN rm -rf ./*
COPY --from=builder /app/apps/web/dist .

RUN cat <<'EOF' > /etc/nginx/conf.d/default.conf
server {
listen 80;

# Enable Gzip compression for faster loading
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}

# Proxy API requests to the backend container
location /api/ {
proxy_pass http://backend:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}

# Proxy storage requests to the backend container
location /storage/ {
proxy_pass http://backend:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
client_max_body_size 10M;
}

# Proxy WebSocket connections for Socket.io
location /socket.io/ {
proxy_pass http://backend:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 86400;
proxy_send_timeout 86400;
}
}
EOF

EXPOSE 80

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget -qO- http://127.0.0.1/ >/dev/null 2>&1 || exit 1

CMD ["nginx", "-g", "daemon off;"]
22 changes: 12 additions & 10 deletions packages/eslint-config/react-internal.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import js from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier";
import tseslint from "typescript-eslint";
import pluginReactHooks from "eslint-plugin-react-hooks";
import pluginReact from "eslint-plugin-react";
import globals from "globals";
import { config as baseConfig } from "./base.js";
import js from '@eslint/js';
import eslintConfigPrettier from 'eslint-config-prettier';
import tseslint from 'typescript-eslint';
import pluginReactHooks from 'eslint-plugin-react-hooks';
import pluginReact from 'eslint-plugin-react';
import globals from 'globals';
import { config as baseConfig } from './base.js';

/**
* A custom ESLint configuration for libraries that use React.
Expand All @@ -32,13 +32,15 @@ export const config = [
},
{
plugins: {
"react-hooks": pluginReactHooks,
'react-hooks': pluginReactHooks,
},
settings: { react: { version: "detect" } },
// Use an explicit version (not "detect") so eslint-plugin-react does not call
// context.getFilename(), which was removed in ESLint 10 (plugin not yet fully compatible).
settings: { react: { version: '19' } },
rules: {
...pluginReactHooks.configs.recommended.rules,
// React scope no longer necessary with new JSX transform.
"react/react-in-jsx-scope": "off",
'react/react-in-jsx-scope': 'off',
},
},
];
Loading
Loading