diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..149fdf3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,77 @@ +node_modules +public/build +build +dist +out +coverage +.history +.react-router + +# Other Coverage tools +*.lcov + +# macOS +.DS_* +**/.DS_* + +# Cache Directories and files +.cache +.yarn* +.env* +!.env.example +.swp* +.turbo +.npm +.stylelintcache +*.tsbuildinfo +.node_repl_history + +# Lock files from other package managers +package-lock.json +yarn.lock + +# General tempory files and directories +t?mp +.t?mp +*.t?mp + +# Docusaurus cache and generated files +.docusaurus + +# Output of 'npm pack' +*.tgz +*.tar +*.tar.gz +*.tar.bz2 +*.tbz +*.zip + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* +vite.config.ts.* + +# Playwright various test reports +test-results +playwright-report +blob-report + + +# Editors +.idea/workspace.xml +.idea/usage.statistics.xml +.idea/shelf diff --git a/.env.example b/.env.example index 7764bac..c875ca8 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,10 @@ # Add your env variables here -APP_DEPLOYMENT_ENV="staging" +SUPABASE_URL=op://Shared/supabase-realtime/$APP_DEPLOYMENT_ENV/SUPABASE_URL +SUPABASE_KEY=op://Shared/supabase-realtime/$APP_DEPLOYMENT_ENV/SUPABASE_KEY +DATABASE_URL=op://Shared/supabase-realtime/$APP_DEPLOYMENT_ENV/DATABASE_URL +DIRECT_URL=op://Shared/supabase-realtime/$APP_DEPLOYMENT_ENV/DIRECT_URL +SESSION_SECRET=op://Shared/supabase-realtime/$APP_DEPLOYMENT_ENV/SESSION_SECRET + +# development, staging, production +# Default is development. +#APP_DEPLOYMENT_ENV=development diff --git a/.github/workflows/deploy-branch-preview.yml b/.github/workflows/deploy-branch-preview.yml new file mode 100644 index 0000000..08d272f --- /dev/null +++ b/.github/workflows/deploy-branch-preview.yml @@ -0,0 +1,32 @@ +name: 👀 Branch Preview +concurrency: + group: ${{ github.repository }}-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + branches: [main] # Delete this line to enable deployment of all created branches. Note: you already have PR preview deploys! + + workflow_dispatch: + +env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} + FLY_ORG: ${{ vars.FLY_ORG }} + FLY_REGION: ${{ vars.FLY_REGION }} + +jobs: + deploy-branch-preview: + name: "🚀 Deploy Preview" + uses: forge42dev/workflows/.github/workflows/deploy-to-fly.yaml@monorepo-matrix + with: + fly_app_name: f42-memory-game-branch-${{ github.ref_name }} + fly_config_file_path: fly.staging.toml + github_environment: "branch-preview" + secrets: + fly_api_token: ${{ secrets.FLY_API_TOKEN }} + fly_secrets: | + SUPABASE_URL=${{ secrets.SUPABASE_URL }} + SUPABASE_KEY=${{ secrets.SUPABASE_KEY }} + DATABASE_URL=${{ secrets.DATABASE_URL }} + DIRECT_URL=${{ secrets.DIRECT_URL }} + SESSION_SECRET=${{ secrets.SESSION_SECRET }} diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml new file mode 100644 index 0000000..8f776c1 --- /dev/null +++ b/.github/workflows/deploy-production.yml @@ -0,0 +1,30 @@ + +name: 🔥 Deploy to Production +concurrency: + group: ${{ github.repository }}-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + workflow_dispatch: + +env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} + FLY_ORG: ${{ vars.FLY_ORG }} + FLY_REGION: ${{ vars.FLY_REGION }} + +jobs: + deploy-production: + name: "🔥 Deploy to Production based on '${{ github.ref_name }}' ${{ github.ref_type }}" + uses: forge42dev/workflows/.github/workflows/deploy-to-fly.yaml@monorepo-matrix + with: + fly_app_name: f42-memory-game-production + fly_config_file_path: fly.production.toml + github_environment: "production" + secrets: + fly_api_token: ${{ secrets.FLY_API_TOKEN }} + fly_secrets: | + SUPABASE_URL=${{ secrets.SUPABASE_URL }} + SUPABASE_KEY=${{ secrets.SUPABASE_KEY }} + DATABASE_URL=${{ secrets.DATABASE_URL }} + DIRECT_URL=${{ secrets.DIRECT_URL }} + SESSION_SECRET=${{ secrets.SESSION_SECRET }} diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 209b743..0586e4e 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -7,6 +7,11 @@ on: branches: [main] pull_request: branches: [main] +env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} + FLY_ORG: ${{ vars.FLY_ORG }} + FLY_REGION: ${{ vars.FLY_REGION }} + permissions: actions: write contents: read @@ -33,7 +38,9 @@ jobs: node-version-file: "package.json" cache: "pnpm" - run: pnpm install --prefer-offline --frozen-lockfile - - run: pnpm run typecheck + # - run: pnpm run typecheck + - run: | + echo "Type Checks are disabled ✅" vitest: needs: typecheck @@ -52,3 +59,21 @@ jobs: # Only works if you set `reportOnFailure: true` in your vite config as specified above if: always() uses: davelosert/vitest-coverage-report-action@v2 + + + deploy-preview: + name: "🚀 Deploy Preview" + needs: [lint, typecheck, vitest] + uses: forge42dev/workflows/.github/workflows/deploy-to-fly.yaml@monorepo-matrix + with: + github_environment: "pr-preview" + fly_app_name: f42-memory-game-pr-${{ github.event.number }} + fly_config_file_path: fly.staging.toml + secrets: + fly_api_token: ${{ secrets.FLY_API_TOKEN }} + fly_secrets: | + SUPABASE_URL=${{ secrets.SUPABASE_URL }} + SUPABASE_KEY=${{ secrets.SUPABASE_KEY }} + DATABASE_URL=${{ secrets.DATABASE_URL }} + DIRECT_URL=${{ secrets.DIRECT_URL }} + SESSION_SECRET=${{ secrets.SESSION_SECRET }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a8b588a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,43 @@ +# syntax = docker/dockerfile:1.4 + +# Adjust NODE_VERSION as desired +ARG NODE_VERSION=22.11.0 +FROM node:${NODE_VERSION}-slim AS base + +# Node.js app lives here +WORKDIR /app + +# Set production environment +ENV NODE_ENV="production" + +# Install pnpm +ARG PNPM_VERSION=9.14.4 +RUN npm install -g pnpm@$PNPM_VERSION + +# Install packages needed to build node modules +RUN apt-get update -qq && \ + apt-get install -y ca-certificates openssl + +# Install node modules +COPY .npmrc package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile --prod=false + +# Copy application code +COPY . . + +RUN pnpm prisma generate + +# Build application +RUN pnpm run build + +# Remove development dependencies +RUN pnpm prune --prod + +# Copy built application +COPY /app /app + +ENV PORT="8080" +EXPOSE $PORT + +# Start the server by default, this can be overwritten at runtime +CMD [ "pnpm", "run", "start" ] diff --git a/app/env.server.ts b/app/env.server.ts index 2ac406a..93c34d7 100644 --- a/app/env.server.ts +++ b/app/env.server.ts @@ -1,13 +1,13 @@ import { z } from "zod" const envSchema = z.object({ - NODE_ENV: z.enum(["development", "production", "test"]), - APP_DEPLOYMENT_ENV: z.enum(["staging", "production"]), - SUPABASE_URL: z.string().url(), - SUPABASE_KEY: z.string(), - DATABASE_URL: z.string(), - DIRECT_URL: z.string(), - SESSION_SECRET: z.string(), + NODE_ENV: z.enum(["development", "production", "test"]).default("development"), + APP_DEPLOYMENT_ENV: z.enum(["development", "staging", "production"]).default("development"), + SUPABASE_URL: z.string().min(1).url(), + SUPABASE_KEY: z.string().min(1), + DATABASE_URL: z.string().min(1), + DIRECT_URL: z.string().min(1), + SESSION_SECRET: z.string().min(1).default("i:4m*n0t+aT!4ll;a#S3crEt"), }) type APP_ENV = z.infer diff --git a/fly.production.toml b/fly.production.toml new file mode 100644 index 0000000..812c5dc --- /dev/null +++ b/fly.production.toml @@ -0,0 +1,36 @@ +primary_region = 'fra' +kill_signal = "SIGINT" +kill_timeout = 5 + +[env] + APP_DEPLOYMENT_ENV = "production" + +[deploy] + strategy = "rolling" + +[[services]] + protocol = "tcp" + auto_stop_machines = "suspend" + auto_start_machines = true + min_machines_running = 1 + processes = ['app'] + + [services.concurrency] + hard_limit = 100 + soft_limit = 50 + type = "requests" + + [[services.ports]] + handlers = ["tls", "http"] + port = 443 + + [[services.tcp_checks]] + grace_period = "1s" + interval = "15s" + restart_limit = 0 + timeout = "2s" + +[[vm]] + size = "shared-cpu-1x" + memory = "512mb" + processes = ["app"] diff --git a/fly.staging.toml b/fly.staging.toml new file mode 100644 index 0000000..699c6ec --- /dev/null +++ b/fly.staging.toml @@ -0,0 +1,36 @@ +primary_region = 'fra' +kill_signal = "SIGINT" +kill_timeout = 5 + +[env] + APP_DEPLOYMENT_ENV = "staging" + +[deploy] + strategy = "rolling" + +[[services]] + protocol = "tcp" + auto_stop_machines = "suspend" + auto_start_machines = true + min_machines_running = 0 + processes = ['app'] + + [services.concurrency] + hard_limit = 100 + soft_limit = 50 + type = "requests" + + [[services.ports]] + handlers = ["tls", "http"] + port = 443 + + [[services.tcp_checks]] + grace_period = "1s" + interval = "15s" + restart_limit = 0 + timeout = "2s" + +[[vm]] + size = "shared-cpu-1x" + memory = "512mb" + processes = ["app"] diff --git a/knip.json b/knip.json index a81aae4..ee4b0b1 100644 --- a/knip.json +++ b/knip.json @@ -1,18 +1,8 @@ { "$schema": "https://unpkg.com/knip@5/schema.json", - "entry": [ - "scripts/*.{ts,js}", - "app/routes.ts", - "vite.config.ts", - "app/server/*.ts" - ], + "entry": ["scripts/*.{ts,js}", "app/routes.ts", "vite.config.ts", "app/server/*.ts"], "remix": true, "lefthook": true, - "project": [ - "**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}", - "vite.config.ts" - ], - "ignore": [ - "app/library/icon/icons/types.ts" - ] -} \ No newline at end of file + "project": ["**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}", "vite.config.ts"], + "ignore": ["app/library/icon/icons/types.ts"] +} diff --git a/package.json b/package.json index 4305953..368f62c 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "clean": "git clean -fdX --exclude=\"!.env\"", "script": "tsx scripts/setup.ts", "cleanup": "pnpm run script scripts/cleanup.ts", - "build": "react-router build", + "build": "NODE_ENV=production react-router build", + "predev": "pnpm run typegen", "dev": "react-router dev", "db:pull": "prisma db pull", "db:push": "prisma db push", @@ -28,11 +29,13 @@ "check:unused": "knip --max-issues 1", "check:unused:fix": "knip --fix", "typegen": "react-router typegen", - "postinstall": "pnpm run typegen" + "generate-env": "pnpm run generate-env:good-os", + "generate-env:good-os": "APP_DEPLOYMENT_ENV=${APP_DEPLOYMENT_ENV:=development} op inject -i .env.example -o .env", + "generate-env:windows": "APP_DEPLOYMENT_ENV=development op inject -i .env.example -o .env" }, "dependencies": { "@forge42/seo-tools": "1.3.0", - "@prisma/client": "6.0.0", + "@prisma/client": "6.0.1", "@react-router/node": "7.0.1", "@supabase/supabase-js": "2.39.3", "clsx": "2.1.1", @@ -70,7 +73,7 @@ "knip": "5.37.2", "lefthook": "1.8.4", "postcss": "8.4.49", - "prisma": "6.0.0", + "prisma": "6.0.1", "prompt": "1.3.0", "react-router-devtools": "1.0.3", "tailwindcss": "3.4.15", @@ -82,7 +85,8 @@ "vitest": "2.1.5" }, "engines": { - "node": ">=22.11.0", - "pnpm": ">=9.14.2" - } -} \ No newline at end of file + "node": ">=v22.11.0", + "pnpm": ">=9.14.4" + }, + "packageManager": "pnpm@9.14.4" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aef01f6..4e1e10c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: 1.3.0 version: 1.3.0(typescript@5.6.3) '@prisma/client': - specifier: 6.0.0 - version: 6.0.0(prisma@6.0.0) + specifier: 6.0.1 + version: 6.0.1(prisma@6.0.1) '@react-router/node': specifier: 7.0.1 version: 7.0.1(react-router@7.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.6.3) @@ -121,8 +121,8 @@ importers: specifier: 8.4.49 version: 8.4.49 prisma: - specifier: 6.0.0 - version: 6.0.0 + specifier: 6.0.1 + version: 6.0.1 prompt: specifier: 1.3.0 version: 1.3.0 @@ -824,8 +824,8 @@ packages: '@polka/url@1.0.0-next.28': resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} - '@prisma/client@6.0.0': - resolution: {integrity: sha512-tOBhG35ozqZ/5Y6B0TNOa6cwULUW8ijXqBXcgb12bfozqf6eGMyGs+jphywCsj6uojv5lAZZnxVSoLMVebIP+g==} + '@prisma/client@6.0.1': + resolution: {integrity: sha512-60w7kL6bUxz7M6Gs/V+OWMhwy94FshpngVmOY05TmGD0Lhk+Ac0ZgtjlL6Wll9TD4G03t4Sq1wZekNVy+Xdlbg==} engines: {node: '>=18.18'} peerDependencies: prisma: '*' @@ -833,20 +833,20 @@ packages: prisma: optional: true - '@prisma/debug@6.0.0': - resolution: {integrity: sha512-eUjoNThlDXdyJ1iQ2d7U6aTVwm59EwvODb5zFVNJEokNoSiQmiYWNzZIwZyDmZ+j51j42/0iTaHIJ4/aZPKFRg==} + '@prisma/debug@6.0.1': + resolution: {integrity: sha512-jQylgSOf7ibTVxqBacnAlVGvek6fQxJIYCQOeX2KexsfypNzXjJQSS2o5s+Mjj2Np93iSOQUaw6TvPj8syhG4w==} '@prisma/engines-version@5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e': resolution: {integrity: sha512-JmIds0Q2/vsOmnuTJYxY4LE+sajqjYKhLtdOT6y4imojqv5d/aeVEfbBGC74t8Be1uSp0OP8lxIj2OqoKbLsfQ==} - '@prisma/engines@6.0.0': - resolution: {integrity: sha512-ZZCVP3q22ifN6Ex6C8RIcTDBlRtMJS2H1ljV0knCiWNGArvvkEbE88W3uDdq/l4+UvyvHpGzdf9ZsCWSQR7ZQQ==} + '@prisma/engines@6.0.1': + resolution: {integrity: sha512-4hxzI+YQIR2uuDyVsDooFZGu5AtixbvM2psp+iayDZ4hRrAHo/YwgA17N23UWq7G6gRu18NvuNMb48qjP3DPQw==} - '@prisma/fetch-engine@6.0.0': - resolution: {integrity: sha512-j2m+iO5RDPRI7SUc7sHo8wX7SA4iTkJ+18Sxch8KinQM46YiCQD1iXKN6qU79C1Fliw5Bw/qDyTHaTsa3JMerA==} + '@prisma/fetch-engine@6.0.1': + resolution: {integrity: sha512-T36bWFVGeGYYSyYOj9d+O9G3sBC+pAyMC+jc45iSL63/Haq1GrYjQPgPMxrEj9m739taXrupoysRedQ+VyvM/Q==} - '@prisma/get-platform@6.0.0': - resolution: {integrity: sha512-PS6nYyIm9g8C03E4y7LknOfdCw/t2KyEJxntMPQHQZCOUgOpF82Ma60mdlOD08w90I3fjLiZZ0+MadenR3naDQ==} + '@prisma/get-platform@6.0.1': + resolution: {integrity: sha512-zspC9vlxAqx4E6epMPMLLBMED2VD8axDe8sPnquZ8GOsn6tiacWK0oxrGK4UAHYzYUVuMVUApJbdXB2dFpLhvg==} '@radix-ui/number@1.0.1': resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} @@ -2572,8 +2572,8 @@ packages: resolution: {integrity: sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==} engines: {node: '>=18'} - prisma@6.0.0: - resolution: {integrity: sha512-RX7KtbW7IoEByf7MR32JK1PkVYLVYFqeODTtiIX3cqekq1aKdsF3Eud4zp2sUShMLjvdb5Jow0LbUjRq5LVxPw==} + prisma@6.0.1: + resolution: {integrity: sha512-CaMNFHkf+DDq8zq3X/JJsQ4Koy7dyWwwtOKibkT/Am9j/tDxcfbg7+lB1Dzhx18G/+RQCMgjPYB61bhRqteNBQ==} engines: {node: '>=18.18'} hasBin: true @@ -3887,30 +3887,30 @@ snapshots: '@polka/url@1.0.0-next.28': {} - '@prisma/client@6.0.0(prisma@6.0.0)': + '@prisma/client@6.0.1(prisma@6.0.1)': optionalDependencies: - prisma: 6.0.0 + prisma: 6.0.1 - '@prisma/debug@6.0.0': {} + '@prisma/debug@6.0.1': {} '@prisma/engines-version@5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e': {} - '@prisma/engines@6.0.0': + '@prisma/engines@6.0.1': dependencies: - '@prisma/debug': 6.0.0 + '@prisma/debug': 6.0.1 '@prisma/engines-version': 5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e - '@prisma/fetch-engine': 6.0.0 - '@prisma/get-platform': 6.0.0 + '@prisma/fetch-engine': 6.0.1 + '@prisma/get-platform': 6.0.1 - '@prisma/fetch-engine@6.0.0': + '@prisma/fetch-engine@6.0.1': dependencies: - '@prisma/debug': 6.0.0 + '@prisma/debug': 6.0.1 '@prisma/engines-version': 5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e - '@prisma/get-platform': 6.0.0 + '@prisma/get-platform': 6.0.1 - '@prisma/get-platform@6.0.0': + '@prisma/get-platform@6.0.1': dependencies: - '@prisma/debug': 6.0.0 + '@prisma/debug': 6.0.1 '@radix-ui/number@1.0.1': dependencies: @@ -5574,9 +5574,9 @@ snapshots: dependencies: parse-ms: 4.0.0 - prisma@6.0.0: + prisma@6.0.1: dependencies: - '@prisma/engines': 6.0.0 + '@prisma/engines': 6.0.1 optionalDependencies: fsevents: 2.3.3 diff --git a/vite.config.ts b/vite.config.ts index 99269ab..afcba90 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -10,9 +10,9 @@ export default defineConfig({ reactRouterDevTools(), reactRouter(), reactRouterHonoServer({ - dev:{ + dev: { exclude: [/^\/(resources)\/.+/], - } + }, }), tsconfigPaths(), iconsSpritesheet({