Skip to content

Commit f8a93c2

Browse files
authored
Merge branch 'dev' into feat/teams-projects
2 parents 74fb9f8 + 099f84c commit f8a93c2

9 files changed

Lines changed: 224 additions & 16 deletions

File tree

.env.example

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
NEXT_PUBLIC_API_BASE_URL=http://localhost:3000/v1
1+
NEXT_PUBLIC_API_BASE_URL=http://localhost:3000/v1/
22
PORT=3000
33
# Next App FRONTEND Instrumentation
44
NEXT_PUBLIC_FARO_URL=http://localhost:12347/collect
@@ -17,7 +17,5 @@ OTEL_SERVICE_NAME=next-backend
1717
## Customize resource attributes, namespace is a recommended attribute
1818
OTEL_RESOURCE_ATTRIBUTES=service.namespace=nextjs-example
1919

20-
# OTel collector
21-
GRAFANA_CLOUD_USERNAME=
22-
GRAFANA_CLOUD_API_KEY=
23-
GRAFANA_CLOUD_ENDPOINT=
20+
# Option special per CI workflow
21+
SKIP_ENV_VALIDATION=true/false

.github/workflows/build.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Build and Push
22

33
on:
44
push:
5-
branches: [dev, main, feat/**]
5+
branches: [ dev, main, feat/** ]
66

77
env:
88
REGISTRY: ghcr.io
@@ -61,6 +61,10 @@ jobs:
6161
file: ./Dockerfile.prod
6262
build-args: |
6363
NEXT_PUBLIC_API_BASE_URL=${{ vars.NEXT_PUBLIC_API_BASE_URL }}
64+
NEXT_PUBLIC_FARO_URL=${{ vars.NEXT_PUBLIC_FARO_URL }}
65+
NEXT_PUBLIC_FARO_APP_VERSION=${{ vars.NEXT_PUBLIC_FARO_APP_VERSION }}
66+
NEXT_PUBLIC_APP_ENV=${{ vars.NEXT_PUBLIC_APP_ENV }}
67+
SKIP_ENV_VALIDATION=true
6468
push: true
6569
tags: ${{ steps.meta.outputs.tags }}
6670
labels: ${{ steps.meta.outputs.labels }}

.github/workflows/ci.yml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ name: CI workflow
22

33
on:
44
pull_request:
5-
branches: ['dev', 'main']
5+
branches: [ "dev", "main" ]
66
paths-ignore:
7-
- '*.md'
7+
- "*.md"
88

99
concurrency:
1010
group: ${{ github.workflow }}-${{ github.ref }}
@@ -25,7 +25,7 @@ jobs:
2525
uses: actions/setup-node@v4
2626
with:
2727
node-version: 22
28-
cache: 'pnpm'
28+
cache: "pnpm"
2929

3030
- name: Install deps
3131
run: pnpm install --frozen-lockfile
@@ -44,9 +44,13 @@ jobs:
4444

4545
- name: Unit tests
4646
run: pnpm test:ci
47+
env:
48+
NODE_ENV: test
4749

4850
- name: Build
4951
run: pnpm build
50-
52+
env:
53+
SKIP_ENV_VALIDATION: true
54+
NODE_ENV: production
5155
# - name: Build Storybook
5256
# run: pnpm build-storybook

.prettierignore

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,29 @@
1+
# Сборка и зависимости
12
node_modules
23
.next
4+
out
5+
build
6+
dist
7+
8+
# Статика и кэш
9+
public
10+
.pnpm-home
11+
.pnpm-store
12+
*.tsbuildinfo
13+
14+
# Логи и отчеты
315
coverage
16+
*.log
17+
.npm
18+
19+
# Конфиги и инфраструктура (обычно их не форматируют так же, как код)
20+
Dockerfile*
421
pnpm-lock.yaml
5-
*.tsbuildinfo
6-
*.log
22+
.dockerignore
23+
.gitignore
24+
25+
# Секреты
26+
.env*
27+
28+
.prettierignore
29+
.github/

Dockerfile.prod

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,20 @@ COPY package.json ./
1414
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --offline --frozen-lockfile
1515

1616
FROM base AS builder
17+
1718
ARG NEXT_PUBLIC_API_BASE_URL
19+
ARG NEXT_PUBLIC_FARO_URL
20+
ARG NEXT_PUBLIC_FARO_APP_VERSION
21+
ARG NEXT_PUBLIC_APP_ENV
22+
ARG SKIP_ENV_VALIDATION=false
1823

1924
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
25+
NEXT_PUBLIC_FARO_URL=$NEXT_PUBLIC_FARO_URL \
26+
NEXT_PUBLIC_FARO_APP_NAME="frontend" \
27+
NEXT_PUBLIC_FARO_APP_NAMESPACE="frontend" \
28+
NEXT_PUBLIC_FARO_APP_VERSION=$NEXT_PUBLIC_FARO_APP_VERSION \
29+
NEXT_PUBLIC_APP_ENV=$NEXT_PUBLIC_APP_ENV \
30+
SKIP_ENV_VALIDATION=$SKIP_ENV_VALIDATION \
2031
NEXT_TELEMETRY_DISABLED=1
2132

2233
COPY --from=deps /app/node_modules ./node_modules
@@ -27,11 +38,14 @@ RUN --mount=type=cache,target=/app/.next/cache pnpm run build
2738
FROM node:20-alpine AS runner
2839
WORKDIR /app
2940

30-
ENV NODE_ENV=production
31-
ENV NEXT_TELEMETRY_DISABLED=1
41+
ENV NODE_ENV=production \
42+
NEXT_TELEMETRY_DISABLED=1 \
43+
PORT=3000 \
44+
HOSTNAME="0.0.0.0"
3245

33-
ENV PORT=${PORT:-3001}
34-
ENV HOSTNAME="0.0.0.0"
46+
ENV OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf \
47+
OTEL_SERVICE_NAME="frontend-ssr" \
48+
OTEL_RESOURCE_ATTRIBUTES="service.namespace=frontend"
3549

3650
RUN addgroup --system --gid 1001 nodejs && \
3751
adduser --system --uid 1001 frontend

next.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import 'shared/config/env';
12
import type { NextConfig } from 'next';
23

34
const nextConfig: NextConfig = {
45
typedRoutes: true,
56
turbopack: {
67
root: __dirname,
78
},
9+
810
output: 'standalone',
911
};
1012

src/shared/config/env.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { Env } from './env';
2+
3+
declare global {
4+
namespace NodeJS {
5+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
6+
interface ProcessEnv extends Env {}
7+
}
8+
}
9+
10+
export {};

src/shared/config/env.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { z } from 'zod/v4';
2+
3+
const isServer = typeof window === 'undefined';
4+
const isBuild = process.env.SKIP_ENV_VALIDATION === 'true';
5+
6+
const envSchemaServer = z.object({
7+
NODE_ENV: z
8+
.enum(['development', 'production', 'test'], {
9+
error: () => ({ message: 'NODE_ENV должен быть: development, production или test' }),
10+
})
11+
.default('development'),
12+
PORT: z.coerce
13+
.number()
14+
.min(1000, 'Порт не может быть ниже 1000')
15+
.max(65535, 'Неверный номер порта')
16+
.default(3000),
17+
OTEL_EXPORTER_OTLP_ENDPOINT: z
18+
.string({
19+
error: 'Эндпоинт OTLP обязателен',
20+
})
21+
.url('OTEL_EXPORTER_OTLP_ENDPOINT должен быть валидным URL (например, http://alloy:4318)'),
22+
OTEL_EXPORTER_OTLP_PROTOCOL: z.enum(['http/protobuf', 'http/json', 'grpc'], {
23+
error: () => ({ message: 'Протокол должен быть http/protobuf, http/json или grpc' }),
24+
}),
25+
OTEL_SERVICE_NAME: z
26+
.string({
27+
error: 'Имя OTEL сервиса обязательно',
28+
})
29+
.min(1, 'Имя сервиса не может быть пустым'),
30+
OTEL_RESOURCE_ATTRIBUTES: z
31+
.string({
32+
error: 'Атрибуты ресурсов (Resource Attributes) обязательны',
33+
})
34+
.includes('service.namespace=', { message: 'Атрибуты должны содержать service.namespace' }),
35+
});
36+
37+
const envSchemaClient = z.object({
38+
NEXT_PUBLIC_API_BASE_URL: z
39+
.string({
40+
error: 'API Base URL обязателен',
41+
})
42+
.url('NEXT_PUBLIC_API_BASE_URL должен быть валидным URL'),
43+
NEXT_PUBLIC_FARO_URL: z
44+
.string({
45+
error: 'URL для Faro (Alloy) обязателен',
46+
})
47+
.url('NEXT_PUBLIC_FARO_URL должен быть валидным URL (например, http://alloy:12347/collect)'),
48+
NEXT_PUBLIC_FARO_APP_NAME: z
49+
.string({
50+
error: 'Имя приложения для Faro обязательно',
51+
})
52+
.min(1, 'Имя приложения не может быть пустым'),
53+
NEXT_PUBLIC_FARO_APP_NAMESPACE: z
54+
.string({
55+
error: 'Namespace приложения обязателен',
56+
})
57+
.min(1, 'Namespace не может быть пустым'),
58+
NEXT_PUBLIC_FARO_APP_VERSION: z.string().default('1.0.0'),
59+
NEXT_PUBLIC_APP_ENV: z
60+
.string({
61+
error: 'Окружение (APP_ENV) обязательно',
62+
})
63+
.min(1, 'Окружение не может быть пустым'),
64+
});
65+
66+
const envSchema = envSchemaClient.extend(envSchemaServer.shape);
67+
68+
const getSchema = () => {
69+
if (!isServer) return envSchemaClient;
70+
71+
if (isBuild) {
72+
return envSchemaClient.extend(envSchemaServer.partial().shape);
73+
}
74+
75+
return envSchema;
76+
};
77+
78+
const _env = getSchema().safeParse(process.env);
79+
80+
if (!_env.success) {
81+
if (isServer) {
82+
console.error('\n\x1b[1;31m[!] CONFIGURATION_ERROR\x1b[0m');
83+
84+
_env.error.issues.forEach((issue) => {
85+
const path = issue.path.join('.') || 'root';
86+
console.error(` \x1b[31m> \x1b[0m \x1b[1m${path}\x1b[0m: \x1b[31m${issue.message}\x1b[0m`);
87+
});
88+
89+
console.error('\n\x1b[33mHint:\x1b[0m Check your .env or Docker build-args\n');
90+
process.exit(1);
91+
} else {
92+
const styles: Record<string, string> = {
93+
badge:
94+
'background: #cc0000; color: white; font-family: monospace; font-weight: bold; padding: 2px 4px; border-radius: 2px;',
95+
text: 'color: #ff4444; font-family: monospace; font-weight: bold;',
96+
};
97+
98+
console.group('%c ELIFECYCLE %c Command failed with exit code 1.', styles.badge, styles.text);
99+
100+
_env.error.issues.forEach((issue) => {
101+
const path = issue.path.join('.') || 'root';
102+
console.error(
103+
`%cerror %c${path}: %c${issue.message}`,
104+
'color: #ff4444; font-weight: bold;',
105+
'color: white; font-weight: bold;',
106+
'color: #aaa;'
107+
);
108+
});
109+
110+
console.groupEnd();
111+
112+
throw new Error('Environment validation failed');
113+
}
114+
} else {
115+
if (isServer) {
116+
if (isBuild) {
117+
console.log(
118+
'\x1b[33m ⚠ SKIP \x1b[0m \x1b[90mServer-side validation skipped for Docker build\x1b[0m'
119+
);
120+
}
121+
122+
console.log(
123+
'\n\x1b[42m\x1b[30m READY \x1b[0m \x1b[32mEnvironment variables validated successfully.\x1b[0m'
124+
);
125+
126+
const entries = Object.entries(_env.data);
127+
const publicEnvs = entries.filter(([key]) => key.startsWith('NEXT_PUBLIC_'));
128+
const privateEnvs = entries.filter(([key]) => !key.startsWith('NEXT_PUBLIC_'));
129+
130+
if (publicEnvs.length > 0) {
131+
console.log('\x1b[36m ○ Client (Public):\x1b[0m');
132+
publicEnvs.forEach(([key, value]) => {
133+
console.log(
134+
`\x1b[32m > \x1b[0m \x1b[90m${key.padEnd(30)}\x1b[0m : \x1b[1m${value}\x1b[0m`
135+
);
136+
});
137+
}
138+
139+
if (privateEnvs.length > 0) {
140+
console.log('\x1b[35m ○ Node (System):\x1b[0m');
141+
privateEnvs.forEach(([key, value]) => {
142+
console.log(
143+
`\x1b[32m > \x1b[0m \x1b[90m${key.padEnd(30)}\x1b[0m : \x1b[1m${value}\x1b[0m`
144+
);
145+
});
146+
}
147+
console.log('');
148+
}
149+
}
150+
151+
export type Env = z.infer<typeof envSchema>;
152+
export const env = _env.data;

tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"next-env.d.ts",
3030
"**/*.ts",
3131
"**/*.tsx",
32+
"src/shared/model/*.d.ts",
3233
".next/types/**/*.ts",
3334
".next/dev/types/**/*.ts",
3435
"**/*.mts"

0 commit comments

Comments
 (0)