feat: add CI/CD pipeline, production Dockerfile, and deployment architecture

- Add production Dockerfile with bun build --compile, multi-stage Alpine build
- Add .dockerignore
- Swap bcrypt -> bcryptjs (pure JS, no native addons)
- Add programmatic migrations on startup via drizzle migrator
- Add /v1/version endpoint with APP_VERSION baked in at build time
- Add .gitea/workflows/ci.yml (lint + test with postgres/valkey services)
- Add .gitea/workflows/build.yml (version bump, build, push to registry)
- Update CLAUDE.md and docs/architecture.md to remove multi-tenancy
- Add docs/deployment.md covering DOKS + ArgoCD architecture

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ryan Moon
2026-04-01 19:50:37 -05:00
parent ffef4c8727
commit c2b1073fef
15 changed files with 419 additions and 26 deletions

View File

@@ -23,7 +23,8 @@
"@fastify/multipart": "^9.4.0",
"@fastify/rate-limit": "^10.3.0",
"@lunarfront/shared": "workspace:*",
"bcrypt": "^6",
"@types/bcryptjs": "^3.0.0",
"bcryptjs": "^3.0.3",
"drizzle-orm": "^0.38",
"fastify": "^5",
"fastify-plugin": "^5",
@@ -32,10 +33,9 @@
"zod": "^4"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^22",
"drizzle-kit": "^0.30",
"pino-pretty": "^13",
"@types/node": "^22",
"@types/bcrypt": "^5"
"typescript": "^5"
}
}

View File

@@ -42,8 +42,8 @@ async function seed() {
const adminPassword = process.env.ADMIN_PASSWORD ?? 'admin1234'
const [adminUser] = await sql`SELECT id FROM "user" WHERE email = 'admin@lunarfront.dev'`
if (!adminUser) {
const bcrypt = await import('bcrypt')
const hashedPw = await (bcrypt.default || bcrypt).hash(adminPassword, 10)
const bcrypt = await import('bcryptjs')
const hashedPw = await bcrypt.hash(adminPassword, 10)
const [user] = await sql`INSERT INTO "user" (email, password_hash, first_name, last_name, role) VALUES ('admin@lunarfront.dev', ${hashedPw}, 'Admin', 'User', 'admin') RETURNING id`
const [adminRole] = await sql`SELECT id FROM role WHERE slug = 'admin' LIMIT 1`
if (adminRole) {

View File

@@ -1,4 +1,7 @@
import Fastify from 'fastify'
import { drizzle } from 'drizzle-orm/postgres-js'
import { migrate } from 'drizzle-orm/postgres-js/migrator'
import postgres from 'postgres'
import rateLimit from '@fastify/rate-limit'
import { databasePlugin } from './plugins/database.js'
import { redisPlugin } from './plugins/redis.js'
@@ -8,6 +11,7 @@ import { authPlugin } from './plugins/auth.js'
import { devAuthPlugin } from './plugins/dev-auth.js'
import { storagePlugin } from './plugins/storage.js'
import { healthRoutes } from './routes/v1/health.js'
import { versionRoutes } from './routes/v1/version.js'
import { authRoutes } from './routes/v1/auth.js'
import { accountRoutes } from './routes/v1/accounts.js'
import { inventoryRoutes } from './routes/v1/inventory.js'
@@ -92,6 +96,7 @@ export async function buildApp() {
// Core routes — always available
await app.register(healthRoutes, { prefix: '/v1' })
await app.register(versionRoutes, { prefix: '/v1' })
await app.register(authRoutes, { prefix: '/v1' })
await app.register(accountRoutes, { prefix: '/v1' })
await app.register(rbacRoutes, { prefix: '/v1' })
@@ -138,7 +143,20 @@ export async function buildApp() {
return app
}
async function runMigrations() {
const connectionString = process.env.DATABASE_URL
if (!connectionString) throw new Error('DATABASE_URL is required')
const migrationsFolder = process.env.MIGRATIONS_DIR ?? './src/db/migrations'
const sql = postgres(connectionString, { max: 1 })
const db = drizzle(sql)
await migrate(db, { migrationsFolder })
await sql.end()
}
async function start() {
await runMigrations()
const app = await buildApp()
const port = parseInt(process.env.PORT ?? '8000', 10)

View File

@@ -1,5 +1,5 @@
import { eq } from 'drizzle-orm'
import bcrypt from 'bcrypt'
import bcrypt from 'bcryptjs'
import { users } from '../db/schema/users.js'
import { RbacService } from '../services/rbac.service.js'
import type { FastifyRequest, FastifyReply, FastifyInstance } from 'fastify'

View File

@@ -1,6 +1,6 @@
import type { FastifyPluginAsync } from 'fastify'
import { eq } from 'drizzle-orm'
import bcrypt from 'bcrypt'
import bcrypt from 'bcryptjs'
import { RegisterSchema, LoginSchema } from '@lunarfront/shared/schemas'
import { users } from '../../db/schema/users.js'

View File

@@ -0,0 +1,7 @@
import type { FastifyPluginAsync } from 'fastify'
export const versionRoutes: FastifyPluginAsync = async (app) => {
app.get('/version', async (_request, reply) => {
reply.send({ version: process.env.APP_VERSION ?? 'dev' })
})
}

View File

@@ -5,7 +5,7 @@ import { userRoles } from '../db/schema/rbac.js'
import { withPagination, withSort, buildSearchCondition, paginatedResponse } from '../utils/pagination.js'
import type { PaginationInput } from '@lunarfront/shared/schemas'
import { randomBytes, createCipheriv, createDecipheriv, pbkdf2Sync } from 'crypto'
import bcrypt from 'bcrypt'
import bcrypt from 'bcryptjs'
// --- Encryption key held in memory ---
let derivedKey: Buffer | null = null