@@ -1 +0,0 @@ -npm run build \ No newline at end of file @@ -2,7 +2,7 @@ import * as Sentry from "@sentry/nextjs"; Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, - integrations: [Sentry.replayIntegration({ maskAllText: false, blockAllMedia: false })], + integrations: [Sentry.replayIntegration({ maskAllText: false, maskAllInputs: false, blockAllMedia: false })], replaysSessionSampleRate: 0.1, replaysOnErrorSampleRate: 1.0, sendDefaultPii: true, @@ -8,7 +8,6 @@ export async function register() { if (process.env.NEXT_RUNTIME === "edge") { await import("../sentry.edge.config"); } - } export const onRequestError = Sentry.captureRequestError; @@ -3,96 +3,62 @@ stages: - Deploy default: - image: docker:latest + image: docker:cli before_script: - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" $CI_REGISTRY --password-stdin -build_staging: +build: stage: Build - image: docker:cli script: - - export RELEASE="$(date -Iseconds)" - - echo -e "\nRELEASE=$RELEASE\nNEXT_PUBLIC_RELEASE=$RELEASE\nCI=true" >> $ENV - - cp $ENV .env - - docker compose --env-file $ENV build - - docker compose push - environment: - name: Staging - url: https://staging.air.fail - deployment_tier: staging - services: - - docker:dind + - cp $BUILD_ENV .env + - docker compose build --push only: + - main - staging + when: on_success -deploy_staging: +.deploy_template: &default_deploy_job stage: Deploy - image: docker:cli + services: + - docker:dind variables: - DOCKER_HOST: tcp://$STAGING_CLUSTER_HOST:2376 + ENVIRONMENT: $CI_ENVIRONMENT_TIER DOCKER_TLS_VERIFY: 1 DOCKER_CERT_PATH: "/certs" - services: - - docker:dind - environment: - name: Staging - url: https://staging.air.fail - deployment_tier: staging - only: - - staging + COMPOSE_ENV_FILES: $ENV + COMPOSE_REMOVE_ORPHANS: true + COMPOSE_PROJECT_NAME: $CI_PROJECT_NAME + when: on_success before_script: - - export RELEASE="$(date -Iseconds)" - - echo -e "\nRELEASE=$RELEASE\nNEXT_PUBLIC_RELEASE=$RELEASE" >> $ENV - - cp $ENV .env - mkdir -p $DOCKER_CERT_PATH - - echo "$STAGING_CLUSTER_CA" > $DOCKER_CERT_PATH/ca.pem - - echo "$STAGING_CLUSTER_CERT" > $DOCKER_CERT_PATH/cert.pem - - echo "$STAGING_CLUSTER_KEY" > $DOCKER_CERT_PATH/key.pem + - export ENVNAME=$(echo ${ENVIRONMENT} | tr '[:lower:]' '[:upper:]') + - export DOCKER_HOST="tcp://$(printenv "${ENVNAME}_CLUSTER_HOST"):2376" + - cp "$(printenv ${ENVNAME}_CLUSTER_CA)" $DOCKER_CERT_PATH/ca.pem + - cp "$(printenv ${ENVNAME}_CLUSTER_CERT)" $DOCKER_CERT_PATH/cert.pem + - cp "$(printenv ${ENVNAME}_CLUSTER_KEY)" $DOCKER_CERT_PATH/key.pem - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" $CI_REGISTRY --password-stdin script: - - docker compose pull - - docker compose --env-file $ENV --project-name air-ui-web up -d + - sort -u -t '=' -k 1,1 $ENV $BUILD_ENV <(echo "RELEASE=$(echo -n $(date '+%D %X'))") > .env + - if [ "$(docker info --format '{{.Swarm.LocalNodeState}}' 2>/dev/null)" = "active" ]; then + docker stack deploy --prune --with-registry-auth -c <(docker compose config | grep -v "^name:") $COMPOSE_PROJECT_NAME; + else + docker compose up -d; + fi -build_production: - stage: Build - script: - - export RELEASE="$(date -Iseconds)" - - echo -e "\nRELEASE=$RELEASE\nNEXT_PUBLIC_RELEASE=$RELEASE\nCI=true" >> $ENV - - cp $ENV .env - - docker compose -f stack.yml --env-file $ENV build - - docker compose -f stack.yml push +deploy_staging: + <<: *default_deploy_job environment: - name: Production - url: https://app.air.fail - deployment_tier: production - services: - - docker:dind + name: Staging + deployment_tier: staging + url: $DOMAIN only: - - main + - staging deploy_production: - stage: Deploy - image: docker:cli - variables: - DOCKER_HOST: tcp://$PRODUCTION_CLUSTER_HOST:2376 - DOCKER_TLS_VERIFY: 1 - DOCKER_CERT_PATH: "/certs" - services: - - docker:dind + <<: *default_deploy_job environment: name: Production - url: https://app.air.fail deployment_tier: production + url: $DOMAIN only: - - main - before_script: - - export RELEASE="$(date -Iseconds)" - - echo -e "\nRELEASE=$RELEASE\nNEXT_PUBLIC_RELEASE=$RELEASE" >> $ENV - - cp $ENV .env - - mkdir -p $DOCKER_CERT_PATH - - echo "$PRODUCTION_CLUSTER_CA" > $DOCKER_CERT_PATH/ca.pem - - echo "$PRODUCTION_CLUSTER_CERT" > $DOCKER_CERT_PATH/cert.pem - - echo "$PRODUCTION_CLUSTER_KEY" > $DOCKER_CERT_PATH/key.pem - - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" $CI_REGISTRY --password-stdin - script: - - docker stack deploy --prune --with-registry-auth --resolve-image=always --compose-file stack.yml --detach frontend \ No newline at end of file + - main \ No newline at end of file @@ -1,3 +0,0 @@ - -[auth] -token=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 @@ -1,26 +1,26 @@ -FROM node:alpine as dependencies +FROM node:22.22-alpine3.22 AS dependencies WORKDIR /app -COPY package.json ./package.json -RUN --mount=type=cache,target=/root/.npm npm install +COPY package.json package-lock.json ./ +RUN npm ci --no-audit --no-fund -FROM node:alpine as builder +FROM node:22.22-alpine3.22 AS builder WORKDIR /app COPY . . COPY --from=dependencies /app/node_modules ./node_modules RUN npm run build -FROM node:alpine as final +FROM node:22.22-alpine3.22 WORKDIR /app COPY --from=builder /app/public ./public COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/package-lock.json ./package-lock.json COPY --from=builder /app/.next ./.next COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/next.config.js ./next.config.js COPY --from=builder /app/next-i18next.config.js ./next-i18next.config.js - CMD ["npm", "run", "start"] \ No newline at end of file @@ -1,9 +0,0 @@ -FROM node:alpine as dependencies - -WORKDIR /app - -COPY . . -RUN npm install - - -CMD ["npm", "run", "dev"] \ No newline at end of file @@ -1,23 +0,0 @@ -.DEFAULT_GOAL=start - -# Setup environment commands by specific shell (Powershell or Bash) -ifeq ($(OS),Windows_NT) - COPY_ENV=xcopy .env.dist .env /D /I - WSL=wsl -else - COPY_ENV=cp -n .env.dist .env -endif - -start: - $(COPY_ENV) - $(WSL) docker compose -f docker-compose.debug.yml up -.PHONY=start - -rebuild: - $(COPY_ENV) - $(WSL) docker compose -f docker-compose.debug.yml up --build -.PHONY=rebuild - -stop: - $(WSL) docker compose -f docker-compose.debug.yml down --remove-orphans -v -.PHONY=stop \ No newline at end of file @@ -1,17 +0,0 @@ -services: - app: - build: - context: . - dockerfile: Dockerfile.dev - container_name: frontend - restart: unless-stopped - volumes: - - .:/app - ports: - - '3000:3000' - env_file: - - .env - -networks: - default: - name: 'air' @@ -1,34 +1,48 @@ services: app: - image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + image: ${CI_REGISTRY_IMAGE:-air/ui/web}:${CI_COMMIT_SHA:-latest} build: context: . dockerfile: Dockerfile + env_file: ${ENV:-.env} restart: unless-stopped - networks: - - infrastructure - env_file: - - .env - labels: + labels: &app-labels - traefik.enable=true - - traefik.docker.network=infrastructure + - traefik.${PROVIDER:-docker}.network=${PROXY_NETWORK:-infrastructure} - - traefik.http.routers.frontend-web-http.rule=Host(`$DOMAIN`) + - traefik.http.routers.frontend-web-http.rule=HostRegexp(`$DOMAIN`) - traefik.http.routers.frontend-web-http.entrypoints=web - traefik.http.routers.frontend-web-http.tls=false - traefik.http.routers.frontend-web-http.service=frontend - traefik.http.routers.frontend-web-http.middlewares=sts-header@file,https-redirect@file - - traefik.http.routers.frontend-web-https.rule=Host(`$DOMAIN`) + - traefik.http.routers.frontend-web-https.rule=HostRegexp(`$DOMAIN`) - traefik.http.routers.frontend-web-https.entrypoints=websecure - traefik.http.routers.frontend-web-https.tls=true - traefik.http.routers.frontend-web-https.tls.certresolver=defaultresolver - traefik.http.routers.frontend-web-https.service=frontend - traefik.http.services.frontend.loadbalancer.server.port=3000 + deploy: + replicas: ${REPLICAS:-1} + labels: *app-labels + update_config: + parallelism: 1 + delay: 10s + order: start-first + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + window: 30s + placement: + constraints: + - node.role == worker + - node.labels.type != observer + networks: + - infrastructure networks: infrastructure: + name: ${PROXY_NETWORK:-infrastructure} external: true - tracing: - external: true \ No newline at end of file @@ -50,5 +50,6 @@ const { withSentryConfig } = require("@sentry/nextjs"); module.exports = withSentryConfig(analyzerConfig, { silent: !process.env.CI, widenClientFileUpload: true, - tunnelRoute: "/monitoring" + tunnelRoute: process.env.SENTRY_TUNNEL_PATH ?? '/monitoring', + debug: process.env.SENTRY_DEBUG ?? false }); @@ -9,8 +9,7 @@ "start": "next start", "build": "next build", "dev": "next dev", - "lint": "next lint", - "prepare": "husky" + "lint": "next lint" }, "engines": { "node": ">=12.0.0" @@ -1,48 +0,0 @@ -services: - app: - image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - build: - context: . - dockerfile: Dockerfile - networks: - - infrastructure - deploy: - replicas: 1 - update_config: - parallelism: 1 - delay: 10s - order: start-first - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 30s - placement: - constraints: - - node.role == worker - labels: - - traefik.enable=true - - traefik.swarm.network=infrastructure - - - traefik.http.routers.frontend-web-http.rule=Host(`$DOMAIN`) - - traefik.http.routers.frontend-web-http.entrypoints=web - - traefik.http.routers.frontend-web-http.tls=false - - traefik.http.routers.frontend-web-http.service=frontend - - traefik.http.routers.frontend-web-http.middlewares=sts-header@file,https-redirect@file,fallback@file - - - traefik.http.routers.frontend-web-https.rule=Host(`$DOMAIN`) - - traefik.http.routers.frontend-web-https.entrypoints=websecure - - traefik.http.routers.frontend-web-https.tls=true - - traefik.http.routers.frontend-web-https.tls.certresolver=defaultresolver - - traefik.http.routers.frontend-web-https.service=frontend - - - traefik.http.routers.frontend-web-https.middlewares=fallback@file - - - traefik.http.services.frontend.loadbalancer.server.port=3000 - env_file: - - .env - -networks: - infrastructure: - name: infrastructure - external: true