Skip to content
Merged
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
85 changes: 85 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
version: 2

updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
day: monday
time: "09:00"
timezone: Asia/Shanghai
open-pull-requests-limit: 5
commit-message:
prefix: chore
groups:
framework-patch-minor:
patterns:
- next
- react
- react-dom
- eslint-config-next
- "@types/react"
- "@types/react-dom"
update-types:
- minor
- patch
data-auth-patch-minor:
patterns:
- prisma
- "@prisma/*"
- better-auth
- "@better-auth/*"
update-types:
- minor
- patch
frontend-patch-minor:
patterns:
- tailwindcss
- "@tailwindcss/*"
- tailwind-merge
- radix-ui
- "@radix-ui/*"
update-types:
- minor
- patch
test-quality-patch-minor:
patterns:
- vitest
- "@playwright/test"
- eslint
- "eslint-*"
- typescript
update-types:
- minor
- patch
remaining-patch-minor:
patterns:
- "*"
update-types:
- minor
- patch
ignore:
- dependency-name: typescript
update-types:
- version-update:semver-major
- dependency-name: eslint
update-types:
- version-update:semver-major
- dependency-name: eslint-plugin-simple-import-sort
update-types:
- version-update:semver-major

- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
day: monday
time: "09:30"
timezone: Asia/Shanghai
open-pull-requests-limit: 2
commit-message:
prefix: chore
groups:
github-actions:
patterns:
- "*"
8 changes: 7 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ concurrency:
cancel-in-progress: true

env:
NODE_VERSION: "22.12.0"
NODE_VERSION: "24.18.0"
NEXT_TELEMETRY_DISABLED: 1
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/fulling_test
BETTER_AUTH_URL: http://127.0.0.1:3000
Expand Down Expand Up @@ -54,6 +54,12 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Audit production dependencies
run: npm run audit:prod

- name: Audit all dependencies
run: npm run audit

- name: Deploy database baseline
run: npm run prisma:migrate

Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Read [docs/architecture.md](./docs/architecture.md) before product or architectu

- Next.js 16 (App Router) + React 19 + TypeScript
- Tailwind CSS v4 + Shadcn/UI
- Node.js 22 + Prisma + Better Auth
- Node.js 24 + Prisma + Better Auth
- Kubernetes + PostgreSQL

## Code Conventions
Expand Down
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ work.

## Development Setup

Requirements: Node.js 22.12+ with its bundled npm 10+, PostgreSQL, and GitHub
Requirements: Node.js 24.18+ with its bundled npm 11, PostgreSQL, and GitHub
OAuth credentials.

```bash
Expand All @@ -29,3 +29,5 @@ v2 data migration.

Commits use conventional prefixes such as `feat:`, `fix:`, `docs:`, `refactor:`,
and `chore:`.

Dependency updates follow [the dependency maintenance policy](./docs/dependency-maintenance.md).
12 changes: 4 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# Install dependencies only when needed
FROM node:22-alpine AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat openssl
FROM node:24.18.0-alpine AS deps
WORKDIR /app

# Copy package files and prisma schema
Expand All @@ -12,8 +10,7 @@ COPY prisma ./prisma
RUN npm ci --ignore-scripts

# Rebuild the source code only when needed
FROM node:22-alpine AS builder
RUN apk add --no-cache openssl
FROM node:24.18.0-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/prisma ./prisma
Expand All @@ -28,7 +25,7 @@ ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build

# Production image, copy all the files and run next
FROM node:22-alpine AS runner
FROM node:24.18.0-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production
Expand All @@ -38,11 +35,10 @@ ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# Install runtime dependencies including OpenSSL for Prisma
# Install runtime dependencies used by health checks and outbound TLS.
RUN apk add --no-cache \
curl \
ca-certificates \
openssl \
&& update-ca-certificates

# Automatically leverage output traces to reduce image size
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ ownership model.

## Requirements

- Node.js 22, including its bundled npm 10 or later
- Node.js 24, including its bundled npm 11

The public application does not require PostgreSQL or an OAuth provider. The
legacy authenticated workspace additionally requires PostgreSQL and a GitHub
Expand Down
69 changes: 69 additions & 0 deletions docs/dependency-maintenance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Dependency Maintenance

Fulling uses Dependabot to surface npm and GitHub Actions updates. Dependabot
opens reviewable pull requests; it must not auto-merge dependency changes.

## Cadence

- Apply security updates as soon as validation is complete.
- Review grouped patch and minor updates every week.
- Review major updates quarterly and migrate them separately.
- Coordinate Node.js changes across `package.json`, CI, Docker, Vercel, and
contributor documentation instead of allowing a single-file runtime bump.

## Update boundaries

- Keep one coherent dependency batch per commit. A pull request may contain
multiple independently reviewable batch commits.
- Update coupled packages together, including Next.js with
`eslint-config-next`, React with React DOM, Prisma with `@prisma/client` and
its driver adapter, and Tailwind CSS with `@tailwindcss/postcss`.
- Keep major migrations separate from unrelated dependency changes.
- Never use `npm audit fix --force` as a substitute for compatibility review.

## Lockfile review

- Change dependency requirements in `package.json` and regenerate
`package-lock.json` with npm; never edit the lockfile by hand.
- Use `npm ci` to verify that a clean checkout installs reproducibly.
- Confirm that lockfile churn is attributable to the intended dependency
batch and review new lifecycle scripts, native binaries, and overrides.
- Commit `package.json` and `package-lock.json` together.

## Required validation

Run both production-only and complete vulnerability reviews:

```bash
npm run audit:prod
npm run audit
```

Every dependency batch must also pass the checks relevant to its scope:

```bash
npm ci
npm run lint
npm test
npm run prisma:validate
npm run prisma:migrate
npm run test:e2e
npm run build
```

Runtime, database, framework, or deployment changes also require a Docker image
build, startup smoke test, and the relevant authentication, PostgreSQL, and
user-specific Kubernetes checks.

## Dependabot policy

- Patch and minor npm updates are grouped by framework, data/auth, frontend,
test/quality, and remaining dependencies.
- Major updates remain individual review items. TypeScript 7, ESLint 10, and
`eslint-plugin-simple-import-sort` 13 are explicitly deferred until their
ecosystem compatibility is reviewed.
- GitHub Actions updates are grouped separately.
- Docker updates are intentionally excluded so the Node.js baseline cannot
drift away from CI, package engines, Vercel, and documentation.
- Dependabot alerts and security updates must be enabled in the repository's
GitHub security settings after this configuration reaches the default branch.
2 changes: 1 addition & 1 deletion docs/vercel-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Connect `FullAgent/fulling` to a Vercel project and keep these settings:
- Install command: Vercel default (`npm install` from `package-lock.json`)
- Build command: `npm run build`
- Output directory: Vercel default
- Node.js: 22.x, enforced by `package.json`
- Node.js: 24.x, enforced by `package.json`
- Production branch: `main`

No `vercel.json` file or application environment variables are required for this
Expand Down
5 changes: 3 additions & 2 deletions e2e/global-setup.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { FullConfig } from '@playwright/test'
import { PrismaClient } from '@prisma/client'
import { createHmac } from 'node:crypto'
import { mkdir, writeFile } from 'node:fs/promises'

import { createPrismaClient } from '@/lib/db'

export const E2E_USER_ID = 'e2e-user'
export const E2E_SESSION_ID = 'e2e-session'
export const E2E_SESSION_TOKEN = 'e2e-session-token'
Expand All @@ -24,7 +25,7 @@ export default async function globalSetup(config: FullConfig) {
throw new Error('Playwright requires a configured baseURL.')
}

const prisma = new PrismaClient({ datasourceUrl: databaseUrl })
const prisma = createPrismaClient(databaseUrl)
try {
await prisma.kubeconfig.deleteMany({ where: { userId: E2E_USER_ID } })
await prisma.session.deleteMany({ where: { userId: E2E_USER_ID } })
Expand Down
4 changes: 2 additions & 2 deletions e2e/global-teardown.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PrismaClient } from '@prisma/client'
import { createPrismaClient } from '@/lib/db'

import { E2E_USER_ID } from './global-setup'

Expand All @@ -7,7 +7,7 @@ export default async function globalTeardown() {
return
}

const prisma = new PrismaClient({ datasourceUrl: process.env.DATABASE_URL })
const prisma = createPrismaClient(process.env.DATABASE_URL)
try {
await prisma.user.deleteMany({ where: { id: E2E_USER_ID } })
} finally {
Expand Down
9 changes: 8 additions & 1 deletion e2e/minimal-flow.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,14 @@ test('authenticated pages do not overflow the viewport', async ({ page }) => {

test('sign-out returns the user to login', async ({ page }) => {
await page.route('**/api/auth/sign-out', async (route) => {
await route.fulfill({ status: 200, json: { success: true } })
await route.fulfill({
status: 200,
headers: {
'set-cookie':
'better-auth.session_token=; Max-Age=0; Path=/; HttpOnly; SameSite=Lax',
},
json: { success: true },
})
})
await page.goto('/workspace')

Expand Down
4 changes: 3 additions & 1 deletion lib/auth/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { prismaAdapter } from '@better-auth/prisma-adapter'
import { betterAuth } from 'better-auth'
import { nextCookies } from 'better-auth/next-js'

import { prisma } from '@/lib/db'
import { getPrismaClient } from '@/lib/db'
import { env } from '@/lib/env'

function createAuth() {
Expand All @@ -18,6 +18,8 @@ function createAuth() {
return null
}

const prisma = getPrismaClient()

return betterAuth({
baseURL,
secret,
Expand Down
27 changes: 21 additions & 6 deletions lib/db.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
import { PrismaClient } from '@prisma/client'
import { PrismaPg } from '@prisma/adapter-pg'

import { PrismaClient } from '@/lib/generated/prisma/client'

declare global {
var prisma: PrismaClient | undefined
}

const prismaClientSingleton = () =>
new PrismaClient({
export function createPrismaClient(databaseUrl: string): PrismaClient {
const adapter = new PrismaPg({ connectionString: databaseUrl })

return new PrismaClient({
adapter,
log: process.env.NODE_ENV === 'development' ? ['error', 'warn'] : ['error'],
})
}

export function getPrismaClient(): PrismaClient {
const databaseUrl = process.env.DATABASE_URL
if (!databaseUrl) {
throw new Error('DATABASE_URL is required to initialize Prisma.')
}

const prisma = global.prisma ?? createPrismaClient(databaseUrl)

export const prisma = global.prisma || prismaClientSingleton()
if (process.env.NODE_ENV !== 'production') {
global.prisma = prisma
}

if (process.env.NODE_ENV !== 'production') {
global.prisma = prisma
return prisma
}
3 changes: 2 additions & 1 deletion lib/kubeconfig/get-content.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { prisma } from '@/lib/db'
import { getPrismaClient } from '@/lib/db'

export async function getKubeconfigContent(userId: string): Promise<string | null> {
const prisma = getPrismaClient()
const kubeconfig = await prisma.kubeconfig.findUnique({
where: { userId },
select: { content: true },
Expand Down
3 changes: 2 additions & 1 deletion lib/kubeconfig/get-status.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { prisma } from '@/lib/db'
import { getPrismaClient } from '@/lib/db'

import type { KubeconfigStatus } from './errors'

export async function getKubeconfigStatus(userId: string): Promise<KubeconfigStatus> {
const prisma = getPrismaClient()
const kubeconfig = await prisma.kubeconfig.findUnique({
where: { userId },
select: { updatedAt: true },
Expand Down
3 changes: 2 additions & 1 deletion lib/kubeconfig/remove.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { prisma } from '@/lib/db'
import { getPrismaClient } from '@/lib/db'

export async function removeKubeconfig(userId: string): Promise<void> {
const prisma = getPrismaClient()
await prisma.kubeconfig.deleteMany({ where: { userId } })
}
Loading
Loading