diff --git a/.bin/scripts/build-images.sh b/.bin/scripts/build-images.sh index cedefda2d..14ad780dc 100755 --- a/.bin/scripts/build-images.sh +++ b/.bin/scripts/build-images.sh @@ -24,7 +24,7 @@ if [[ $# == "0" ]]; then fi; set +e -docker buildx create --name mna --driver docker-container --bootstrap --use 2> /dev/null +docker buildx create --name mna-tdb --driver docker-container --config "$SCRIPT_DIR/buildkitd.toml" 2> /dev/null set -e if [[ ! -z "${CI:-}" ]]; then @@ -36,4 +36,6 @@ fi export CHANNEL=$(get_channel $VERSION) # "$@" is the list of environements -docker buildx bake --builder mna --${mode} "$@" +docker buildx bake --builder mna-tdb --${mode} "$@" +docker builder prune --builder mna-tdb --keep-storage 20GB --force +docker buildx stop --builder mna-tdb diff --git a/.bin/scripts/buildkitd.toml b/.bin/scripts/buildkitd.toml new file mode 100644 index 000000000..ae44e38aa --- /dev/null +++ b/.bin/scripts/buildkitd.toml @@ -0,0 +1,7 @@ +[worker.oci] + max-parallelism = 2 + +[[worker.oci.gcpolicy]] + all = true + keepBytes = "20GB" + keepDuration = "72h" diff --git a/.bin/scripts/seed-update.sh b/.bin/scripts/seed-update.sh index 3b216e3b8..90e6ca408 100755 --- a/.bin/scripts/seed-update.sh +++ b/.bin/scripts/seed-update.sh @@ -9,6 +9,8 @@ else shift fi +echo "base de donnée cible: $TARGET_DB" + read -p "La base de donnée contient-elle des données sensible ? [Y/n]: " response case $response in [nN][oO]|[nN]) diff --git a/.bin/scripts/setup-local-env.sh b/.bin/scripts/setup-local-env.sh index 0ccf354ab..38052c664 100755 --- a/.bin/scripts/setup-local-env.sh +++ b/.bin/scripts/setup-local-env.sh @@ -10,6 +10,12 @@ ANSIBLE_CONFIG="${ROOT_DIR}/.infra/ansible/ansible.cfg" ansible all \ --extra-vars "@${ROOT_DIR}/.infra/vault/vault.yml" \ --vault-password-file="${SCRIPT_DIR}/get-vault-password-client.sh" +echo "PUBLIC_VERSION=0.0.0-local" >> "${ROOT_DIR}/server/.env" + echo "NEXT_PUBLIC_ENV=local" >> "${ROOT_DIR}/ui/.env" echo "NEXT_PUBLIC_VERSION=0.0.0-local" >> "${ROOT_DIR}/ui/.env" echo "NEXT_PUBLIC_API_PORT=5001" >> "${ROOT_DIR}/ui/.env" + +yarn build:dev +yarn cli migrations:up +yarn cli indexes:create diff --git a/.dockerignore b/.dockerignore index 30b260e8f..761c1851c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,14 +1,18 @@ * -!/server -/server/.env -/server/.env.local +!/server/src +!/server/static +!/server/package.json +!/server/tsconfig.json +!/server/tsoa.json +!/server/tsup.config.ts !/shared !/ui /ui/.env /ui/.next +/ui/.eslintrc.js !/package.json !/.yarn/cache diff --git a/.github/workflows/deploy_preview.yml b/.github/workflows/deploy_preview.yml new file mode 100644 index 000000000..66e5910ed --- /dev/null +++ b/.github/workflows/deploy_preview.yml @@ -0,0 +1,138 @@ +name: Deploy Preview +on: + issue_comment: + types: [created] + +jobs: + debug: + runs-on: ubuntu-latest + steps: + - uses: hmarr/debug-action@v2 + + deploy_preview: + if: github.event.comment.body == ':rocket:' && github.event.issue.pull_request + concurrency: + group: ${{ github.workflow }}-${{ github.event.issue.id }} + cancel-in-progress: true + name: Deploy Preview ${{ github.event.issue.number }} + runs-on: ubuntu-latest + steps: + - name: React to comment + uses: dkershner6/reaction-action@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commentId: ${{ github.event.comment.id }} + reaction: "+1" + + - id: "get-branch" + run: echo "branch=$(gh pr view $PR_NO --repo $REPO --json headRefName --jq '.headRefName')" >> $GITHUB_OUTPUT + env: + REPO: ${{ github.repository }} + PR_NO: ${{ github.event.issue.number }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ steps.get-branch.outputs.branch }} + + - name: Create LFS file list + run: git lfs ls-files --long | cut -d ' ' -f1 | sort > .lfs-assets-id + + - name: LFS Cache + uses: actions/cache@v3 + with: + path: .git/lfs/objects + key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }} + restore-keys: | + ${{ runner.os }}-lfs- + + - name: Git LFS Pull + run: git lfs pull + + - name: Install SSH key + uses: shimataro/ssh-key-action@v2 + with: + name: github_actions + key: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }} + known_hosts: ${{ vars.SSH_KNOWN_HOSTS }} + config: | + Host * + IdentityFile ~/.ssh/github_actions + + - name: Create vault pwd file + run: echo ${{ secrets.VAULT_PWD }} > .infra/.vault_pwd.txt + + - name: Install jmespath + run: | + sudo pipx inject ansible-core jmespath + + - name: Run playbook + run: .bin/mna-lba deploy preview "${{ github.event.issue.number }}" + env: + ANSIBLE_VAULT_PASSWORD_FILE: .infra/.vault_pwd.txt + ANSIBLE_REMOTE_USER: deploy + ANSIBLE_BECOME_PASS: ${{ secrets.DEPLOY_PASS }} + + - name: Encrypt Error log on failure + run: .bin/mna-lba deploy:log:encrypt + if: failure() + env: + ANSIBLE_VAULT_PASSWORD_FILE: .infra/.vault_pwd.txt + + - name: Upload failure artifacts on failure + if: failure() + uses: actions/upload-artifact@v3 + with: + name: error-logs + path: /tmp/deploy_error.log.gpg + + - name: Preview Summary when failed + if: failure() + run: echo 'You can get error logs using `.bin/mna-lba deploy:log:decrypt ${{ github.run_id }}`' >> $GITHUB_STEP_SUMMARY + + - name: Preview Summary + run: echo 'https://${{ github.event.issue.number }}.labonnealternance-preview.apprentissage.beta.gouv.fr/ 🚀' >> $GITHUB_STEP_SUMMARY + + - name: Comment PR Preview + if: github.event.issue.state != 'closed' + uses: thollander/actions-comment-pull-request@v2 + with: + message: | + ### :rocket: Prévisualisation + https://${{ github.event.issue.number }}.labonnealternance-preview.apprentissage.beta.gouv.fr/ + + To re-deploy just add a comment with :rocket: + comment_tag: deployment + mode: recreate + pr_number: ${{ github.event.issue.number }} + + - name: Comment PR Preview when failed + if: failure() && github.event.issue.state != 'closed' + uses: thollander/actions-comment-pull-request@v2 + with: + message: | + ### :ambulance: Prévisualisation failed + + https://${{ github.event.issue.number }}.labonnealternance-preview.apprentissage.beta.gouv.fr/ + + You can get error logs using `.bin/mna-lba deploy:log:decrypt ${{ github.run_id }}` + + To re-deploy just add a comment with :rocket: + comment_tag: deployment + mode: recreate + pr_number: ${{ github.event.issue.number }} + + - name: Comment PR Preview when cancelled + if: cancelled() && github.event.issue.state != 'closed' + uses: thollander/actions-comment-pull-request@v2 + with: + message: | + ### :ambulance: Prévisualisation cancelled + + https://${{ github.event.issue.number }}.labonnealternance-preview.apprentissage.beta.gouv.fr/ + + To re-deploy just add a comment with :rocket: + comment_tag: deployment + mode: recreate + pr_number: ${{ github.event.issue.number }} diff --git a/.github/workflows/gitguardian.yml b/.github/workflows/gitguardian.yml new file mode 100644 index 000000000..de2c1838d --- /dev/null +++ b/.github/workflows/gitguardian.yml @@ -0,0 +1,21 @@ +name: GitGuardian scan + +on: [push, pull_request] + +jobs: + scanning: + name: GitGuardian scan + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 # fetch all history so multiple commits can be scanned + - name: GitGuardian scan + uses: GitGuardian/ggshield-action@v1.16.0 + env: + GITHUB_PUSH_BEFORE_SHA: ${{ github.event.before }} + GITHUB_PUSH_BASE_SHA: ${{ github.event.base }} + GITHUB_PULL_BASE_SHA: ${{ github.event.pull_request.base.sha }} + GITHUB_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GITGUARDIAN_API_KEY: ${{ secrets.GITGUARDIAN_API_KEY }} diff --git a/.github/workflows/merge_queue.yml b/.github/workflows/merge_queue.yml new file mode 100644 index 000000000..d0cde7c1d --- /dev/null +++ b/.github/workflows/merge_queue.yml @@ -0,0 +1,10 @@ +name: Merge Queue +on: + merge_group: + types: [checks_requested] + +jobs: + tests: + uses: "./.github/workflows/ci.yml" + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 293238fe5..83ac4c289 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -10,69 +10,15 @@ jobs: secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - deploy: - if: github.event.pull_request.draft == false - concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - name: Deploy Preview ${{ github.event.pull_request.number }} + deploy_comment: + name: Add deploy comment runs-on: ubuntu-latest steps: - - name: Checkout project - uses: actions/checkout@v4 - with: - lfs: true - - - name: Install SSH key - uses: shimataro/ssh-key-action@v2 - with: - name: github_actions - key: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }} - known_hosts: ${{ vars.SSH_KNOWN_HOSTS }} - config: | - Host * - IdentityFile ~/.ssh/github_actions - - - name: Create vault pwd file - run: echo ${{ secrets.VAULT_PWD }} > .infra/.vault_pwd.txt - - - name: Install jmespath - run: | - sudo pipx inject ansible-core jmespath - - - name: Run playbook - run: .bin/mna-tdb deploy preview "${{ github.event.pull_request.number }}" - env: - ANSIBLE_VAULT_PASSWORD_FILE: .infra/.vault_pwd.txt - ANSIBLE_REMOTE_USER: deploy - ANSIBLE_BECOME_PASS: ${{ secrets.DEPLOY_PASS }} - - - name: Encrypt Error log on failure - run: .bin/mna-tdb deploy:log:encrypt - if: failure() - env: - ANSIBLE_VAULT_PASSWORD_FILE: .infra/.vault_pwd.txt - - - name: Upload failure artifacts on failure - if: failure() - uses: actions/upload-artifact@v3 - with: - name: error-logs - path: /tmp/deploy_error.log.gpg - - - name: Preview Summary when failed - if: failure() - run: echo 'You can get error logs using `.bin/mna-tdb deploy:log:decrypt ${{ github.run_id }}`' >> $GITHUB_STEP_SUMMARY - - - name: Preview Summary - run: echo 'https://${{ github.event.pull_request.number }}.tdb-preview.apprentissage.beta.gouv.fr/ 🚀' >> $GITHUB_STEP_SUMMARY - - name: Comment PR Preview if: github.event.pull_request.state != 'closed' uses: thollander/actions-comment-pull-request@v2 with: message: | - ### :rocket: Prévisualisation - https://${{ github.event.pull_request.number }}.tdb-preview.apprentissage.beta.gouv.fr/ - comment_tag: execution - mode: recreate + To deploy this PR just add a comment with a simple :rocket: + comment_tag: deployment_instructions + mode: upsert diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ef722f413..765e86629 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,6 +10,8 @@ jobs: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} release: + concurrency: + group: "release-${{ github.workflow }}-${{ github.ref }}" permissions: write-all outputs: VERSION: ${{ steps.get-version.outputs.VERSION }} @@ -66,7 +68,7 @@ jobs: deploy: concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: "deploy-${{ github.workflow }}-${{ github.ref }}" needs: ["release"] name: Deploy ${{ needs.release.outputs.VERSION }} on recette uses: "./.github/workflows/_deploy.yml" diff --git a/.infra/ansible/tasks/preview_pr.yml b/.infra/ansible/tasks/preview_pr.yml index f9d53f75b..09f046be5 100644 --- a/.infra/ansible/tasks/preview_pr.yml +++ b/.infra/ansible/tasks/preview_pr.yml @@ -68,7 +68,7 @@ - name: "[{{ pr_number }}] Build local images 0.0.0-{{ pr_number }}" shell: - cmd: ".bin/mna-tdb preview:build 0.0.0-{{ pr_number }} load preview" + cmd: "flock --verbose --close /tmp/deployment_build.lock .bin/mna-tdb preview:build 0.0.0-{{ pr_number }} load preview" chdir: "/opt/app/projects/{{ pr_number }}/repository" async: 900 # max 15 minutes poll: 15 # check every 15s @@ -81,18 +81,20 @@ - name: "[{{ pr_number }}] Trigger ACME companion" shell: chdir: /opt/app - cmd: docker exec nginx-proxy-acme /app/force_renew + cmd: docker exec nginx-proxy-acme /app/signal_le_service + + - name: "[{{ pr_number }}] Seed database" + shell: + chdir: "/opt/app" + cmd: "flock --verbose --close /tmp/deployment_seed.lock /opt/app/scripts/seed.sh preview_{{ pr_number | default('00') }}" + async: 900 # max 15 minutes + poll: 15 # check every 15s - name: "[{{ pr_number }}] Execute MongoDB migrations" shell: chdir: "/opt/app/projects/{{ pr_number }}" cmd: "docker compose run --rm server yarn cli migrations:up" - - name: "[{{ pr_number }}] Seed database" - shell: - chdir: "/opt/app" - cmd: "/opt/app/scripts/seed.sh preview_{{ pr_number | default('00') }}" - - name: "[{{ pr_number }}] Preview URL" debug: msg: "{{ vault[env_type].MNA_TDB_PUBLIC_URL }}" diff --git a/.infra/docker-compose.preview-system.yml b/.infra/docker-compose.preview-system.yml index 28f8b2ef9..4a05a5b5d 100644 --- a/.infra/docker-compose.preview-system.yml +++ b/.infra/docker-compose.preview-system.yml @@ -4,7 +4,7 @@ x-default: &default deploy: resources: limits: - memory: 256m + memory: 1g restart: always networks: - mna_network diff --git a/.infra/docker-compose.preview.yml b/.infra/docker-compose.preview.yml index 82e8c19f6..88739f791 100644 --- a/.infra/docker-compose.preview.yml +++ b/.infra/docker-compose.preview.yml @@ -4,7 +4,7 @@ x-default: &default deploy: resources: limits: - memory: 256m + memory: 2g restart: always networks: - mna_network diff --git a/.infra/docker-compose.production.yml b/.infra/docker-compose.production.yml index ae9427cb0..490c5efa6 100644 --- a/.infra/docker-compose.production.yml +++ b/.infra/docker-compose.production.yml @@ -9,9 +9,11 @@ x-deploy-default: &deploy-default parallelism: 1 delay: 10s rollback_config: - parallelism: 0 + parallelism: 1 + delay: 10s restart_policy: window: 360s + delay: 30s # Max 24hours max_attempts: 240 @@ -27,7 +29,7 @@ services: <<: *deploy-default resources: limits: - memory: 1g + memory: 2g replicas: 2 env_file: .env_server volumes: @@ -52,7 +54,7 @@ services: <<: *deploy-default resources: limits: - memory: 1g + memory: 2g command: ["yarn", "cli", "queue_processor:start"] env_file: .env_server volumes: @@ -71,7 +73,7 @@ services: <<: *deploy-default resources: limits: - memory: 1g + memory: 2g command: ["yarn", "cli", "job_processor:start"] env_file: .env_server volumes: diff --git a/.infra/docker-compose.recette.yml b/.infra/docker-compose.recette.yml index b79369e97..665a9dc87 100644 --- a/.infra/docker-compose.recette.yml +++ b/.infra/docker-compose.recette.yml @@ -9,8 +9,11 @@ services: memory: 128m update_config: failure_action: rollback + parallelism: 1 + delay: 10s rollback_config: - parallelism: 0 + parallelism: 1 + delay: 10s restart_policy: window: 360s # Max 24hours diff --git a/.infra/files/scripts/cli.sh b/.infra/files/scripts/cli.sh new file mode 100755 index 000000000..f286141d4 --- /dev/null +++ b/.infra/files/scripts/cli.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail +#Needs to be run as sudo + +docker compose run --rm --no-deps server yarn cli "$@" diff --git a/.prettierignore b/.prettierignore index 7edff856e..79f80ccb7 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,3 @@ .yarn .talismanrc +.infra/files/configs/mongodb/seed.gpg diff --git a/.talismanrc b/.talismanrc index 8758e3d09..3aeab5135 100644 --- a/.talismanrc +++ b/.talismanrc @@ -13,6 +13,10 @@ fileignoreconfig: checksum: 920950cddadd6683cfeb4b678051e935d9e9bdf62a557a9119b45a2e03cf155f - filename: .github/workflows/ci.yml checksum: 9e279ec421f82277240a958eec081cdb098a7e602bfd41e023013362ad365cfb +- filename: .github/workflows/deploy_preview.yml + checksum: 461208e3c293062c405f4dacdda6bf3b87153cb43da80698f7c97a4be76375f0 +- filename: .github/workflows/gitguardian.yml + checksum: 6570536d75569b3826026d9126cff26fc4de473afbab8adbfc9fab833362fb79 - filename: .github/workflows/preview.yml checksum: dbe369785961ffb27ed90dbdd9842d14cedc621a2d32f430710515457b05fa51 - filename: .github/workflows/preview_cleanup.yml diff --git a/Dockerfile b/Dockerfile index 98f3b13e6..e733eea6b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ COPY ui/package.json ui/package.json COPY server/package.json server/package.json COPY shared/package.json shared/package.json -RUN yarn install --immutable +RUN --mount=type=cache,target=/app/.yarn/cache yarn install --immutable FROM builder_root as root WORKDIR /app @@ -27,14 +27,12 @@ COPY ./shared ./shared RUN yarn --cwd server build # Removing dev dependencies -RUN yarn workspaces focus --all --production -# Cache is not needed anymore -RUN rm -rf .yarn/cache +RUN --mount=type=cache,target=/app/.yarn/cache yarn workspaces focus --all --production # Production image, copy all the files and run next FROM node:20-alpine AS server WORKDIR /app -RUN apk add --update \ +RUN --mount=type=cache,target=/var/cache/apk apk add --update \ curl \ && rm -rf /var/cache/apk/* @@ -73,8 +71,7 @@ ARG PUBLIC_ENV ENV NEXT_PUBLIC_ENV=$PUBLIC_ENV RUN yarn --cwd ui build -# Cache is not needed anymore -RUN rm -rf .yarn/cache +# RUN --mount=type=cache,target=/app/ui/.next/cache yarn --cwd ui build # Production image, copy all the files and run next FROM node:20-alpine AS ui @@ -84,13 +81,19 @@ ENV NODE_ENV production # Uncomment the following line in case you want to disable telemetry during runtime. ENV NEXT_TELEMETRY_DISABLED 1 +ARG PUBLIC_VERSION +ENV NEXT_PUBLIC_VERSION=$PUBLIC_VERSION + +ARG PUBLIC_ENV +ENV NEXT_PUBLIC_ENV=$PUBLIC_ENV + RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs # You only need to copy next.config.js if you are NOT using the default configuration -COPY --from=builder_ui /app/ui/next.config.js /app/ -COPY --from=builder_ui /app/ui/public /app/ui/public -COPY --from=builder_ui /app/ui/package.json /app/ui/package.json +COPY --from=builder_ui --chown=nextjs:nodejs /app/ui/next.config.js /app/ +COPY --from=builder_ui --chown=nextjs:nodejs /app/ui/public /app/ui/public +COPY --from=builder_ui --chown=nextjs:nodejs /app/ui/package.json /app/ui/package.json # Automatically leverage output traces to reduce image size # https://nextjs.org/docs/advanced-features/output-file-tracing diff --git a/package.json b/package.json index 79fe63e48..002669c94 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,16 @@ "author": "MNA", "license": "MIT", "private": true, + "packageManager": "yarn@3.6.1", "engines": { "node": ">=20", "npm": "please-use-yarn" }, + "workspaces": [ + "ui", + "server", + "shared" + ], "scripts": { "setup": ".bin/mna-tdb init:env", "dev": "yarn services:start; yarn foreach:parallel run dev", @@ -26,12 +32,13 @@ "services:stop": "docker compose down", "services:clean": "yarn services:stop; docker system prune --volumes", "seed:update": "./.bin/mna-tdb seed:update", - "lint": "eslint --ignore-path .gitignore --cache .", + "lint": "eslint --ignore-path .gitignore --cache --ext .js,.jsx,.ts,.tsx .", + "lint:fix": "yarn lint --fix", "prettier:fix": "prettier --write -u .", "prettier:check": "prettier --check -u .", "release": "semantic-release", "release:interactive": "./.bin/mna-tdb release:interactive", - "prepare": "husky install", + "postinstall": "husky install", "talisman:add-exception": "yarn node-talisman --githook pre-commit -i", "test": "cross-env NODE_NO_WARNINGS=1 NODE_OPTIONS=--experimental-vm-modules jest", "test:ci": "yarn test --ci -w 2", @@ -43,11 +50,9 @@ "foreach:parallel": "yarn foreach:seq -pi", "foreach:ci": "yarn foreach:seq -p" }, - "workspaces": [ - "ui", - "server", - "shared" - ], + "dependencies": { + "husky": "^8.0.3" + }, "devDependencies": { "@commitlint/cli": "^17.7.1", "@commitlint/config-conventional": "^17.7.0", @@ -67,7 +72,6 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-unused-imports": "^3.0.0", - "husky": "^8.0.3", "jest": "^29.6.4", "jest-environment-jsdom": "^29.6.4", "lint-staged": "^14.0.1", @@ -94,7 +98,6 @@ "prettier --write -u" ] }, - "packageManager": "yarn@3.6.1", "resolutions": { "zod@3.21.4": "patch:zod@npm%3A3.21.4#./.yarn/patches/zod-npm-3.21.4-9f570b215c.patch" } diff --git a/release.config.js b/release.config.js index c2e3fc01d..2f2cb80f1 100644 --- a/release.config.js +++ b/release.config.js @@ -1,5 +1,5 @@ module.exports = { - branches: ["master", { name: "next", channel: "next", prerelease: "rc" }], + branches: ["master", { name: "hotfix", channel: "hotfix", prerelease: "hotfix" }], repositoryUrl: "https://github.com/mission-apprentissage/flux-retour-cfas.git", plugins: [ "@semantic-release/commit-analyzer", diff --git a/server/package.json b/server/package.json index 65ee53be6..155dcb3af 100644 --- a/server/package.json +++ b/server/package.json @@ -6,12 +6,16 @@ "author": "MNA", "license": "MIT", "type": "module", + "engines": { + "node": ">=20", + "npm": "please-use-yarn" + }, "scripts": { "cli": "node dist/index.js", + "build:dev": "tsup-node", + "dev": "tsup-node --env.TSUP_WATCH true", "init:dev": "yarn cli init:dev", - "dev": "tsup-node --watch", "build": "tsup-node --env.NODE_ENV production", - "build:dev": "tsup-node", "typecheck": "tsc --noEmit" }, "dependencies": { @@ -100,10 +104,6 @@ "zod-express-middleware": "1.4.0", "zod-to-json-schema": "3.21.4" }, - "engines": { - "node": ">=20", - "npm": "please-use-yarn" - }, "prettier": { "printWidth": 120, "bracketSpacing": true, diff --git a/server/src/commands.ts b/server/src/commands.ts index adc70855c..22ba76afc 100644 --- a/server/src/commands.ts +++ b/server/src/commands.ts @@ -18,10 +18,11 @@ import { updateUserPassword } from "./jobs/users/update-user-password"; async function startJobProcessor(signal: AbortSignal) { logger.info(`Process jobs queue - start`); - if (config.env !== "local") { + if (config.env !== "local" && config.env !== "preview") { await addJob({ name: "crons:init", queued: true, + payload: {}, }); } @@ -60,6 +61,7 @@ program .configureHelp({ sortSubcommands: true, }) + .showSuggestionAfterError() .hook("preAction", (_, actionCommand) => { const command = actionCommand.name(); // on définit le module du logger en global pour distinguer les logs des jobs diff --git a/server/src/common/actions/job.actions.ts b/server/src/common/actions/job.actions.ts index 1110a6597..51c0ccfa0 100644 --- a/server/src/common/actions/job.actions.ts +++ b/server/src/common/actions/job.actions.ts @@ -1,48 +1,97 @@ -import { Filter, FindOptions, MatchKeysAndValues, ObjectId, WithoutId } from "mongodb"; +import Boom from "boom"; +import { MatchKeysAndValues, ObjectId, FindOptions, Filter } from "mongodb"; import { jobsDb } from "../model/collections"; -import { IJob } from "../model/job.model"; -import { getDbCollection } from "../mongodb"; +import { IJob, IJobsCron, IJobsCronTask, IJobsSimple } from "../model/job.model"; -type CreateJobParam = Pick; +type CreateJobSimpleParams = Pick; -/** - * Création d'un job - */ -export const createJob = async ({ +export const createJobSimple = async ({ name, - type = "simple", payload, scheduled_for = new Date(), sync = false, +}: CreateJobSimpleParams): Promise => { + const job: IJobsSimple = { + _id: new ObjectId(), + name, + type: "simple", + status: sync ? "will_start" : "pending", + payload, + updated_at: new Date(), + created_at: new Date(), + scheduled_for, + sync, + }; + await jobsDb().insertOne(job); + return job; +}; + +type CreateJobCronParams = Pick; + +export const createJobCron = async ({ + name, cron_string, -}: CreateJobParam): Promise => { - const job: WithoutId = { + scheduled_for = new Date(), + sync = false, +}: CreateJobCronParams): Promise => { + const job: IJobsCron = { + _id: new ObjectId(), name, - type, + type: "cron", status: sync ? "will_start" : "pending", - ...(payload ? { payload } : {}), - ...(cron_string ? { cron_string } : {}), + cron_string, updated_at: new Date(), created_at: new Date(), scheduled_for, sync, }; - const { insertedId: _id } = await getDbCollection("jobs").insertOne(job); - return { ...job, _id }; + await jobsDb().insertOne(job); + return job; +}; + +export const updateJobCron = async (id: ObjectId, cron_string: IJobsCron["cron_string"]): Promise => { + const data = { + status: "pending", + cron_string, + updated_at: new Date(), + }; + const job = await jobsDb().findOneAndUpdate(id, data, { returnDocument: "after" }); + if (!job.value || job.value.type !== "cron") { + throw Boom.internal("Not found"); + } + return job.value; +}; + +type CreateJobCronTaskParams = Pick; + +export const createJobCronTask = async ({ name, scheduled_for }: CreateJobCronTaskParams): Promise => { + const job: IJobsCronTask = { + _id: new ObjectId(), + name, + type: "cron_task", + status: "pending", + updated_at: new Date(), + created_at: new Date(), + scheduled_for, + sync: false, + }; + await jobsDb().insertOne(job); + return job; }; -export const findJob = async (filter: Filter, options?: FindOptions): Promise => { +export const findJob = async (filter: Filter, options?: FindOptions): Promise => { return await jobsDb().findOne(filter, options); }; -export const findJobs = async (filter: Filter, options?: FindOptions): Promise => { - return await jobsDb().find(filter, options).toArray(); +export const findJobs = async (filter: Filter, options?: FindOptions): Promise => { + // @ts-expect-error + return await jobsDb().find(filter, options).toArray(); }; /** * Mise à jour d'un job */ export const updateJob = async (_id: ObjectId, data: MatchKeysAndValues) => { - return getDbCollection("jobs").updateOne({ _id }, { $set: { ...data, updated_at: new Date() } }); + return jobsDb().updateOne({ _id }, { $set: { ...data, updated_at: new Date() } }); }; diff --git a/server/src/common/model/job.model.ts b/server/src/common/model/job.model.ts index 2ac2005f3..64343b80b 100644 --- a/server/src/common/model/job.model.ts +++ b/server/src/common/model/job.model.ts @@ -1,6 +1,8 @@ import { Jsonify } from "type-fest"; import { z } from "zod"; +import { CronName, CronsMap } from "@/jobs/jobs"; + import { IModelDescriptor, zObjectId } from "./common"; const collectionName = "jobs" as const; @@ -11,29 +13,71 @@ const indexes: IModelDescriptor["indexes"] = [ [{ ended_at: 1 }, { expireAfterSeconds: 3600 * 24 * 90 }], // 3 mois ]; -export const ZJob = z +export const ZJobSimple = z .object({ _id: zObjectId, name: z.string().describe("Le nom de la tâche"), - type: z.enum(["simple", "cron", "cron_task"]).describe("Type du job simple ou cron"), + type: z.literal("simple"), status: z .enum(["pending", "will_start", "running", "finished", "blocked", "errored"]) .describe("Statut courant du job"), - sync: z.boolean().optional().describe("Si le job est synchrone"), - payload: z.record(z.unknown()).optional().describe("La donnée liéé à la tâche"), - output: z.record(z.unknown()).optional().describe("Les valeurs de retours du job"), - cron_string: z.string().optional().describe("standard cron string exemple: '*/2 * * * *'"), + sync: z.boolean().describe("Si le job est synchrone"), + payload: z.record(z.unknown()).nullish().describe("La donnée liéé à la tâche"), + output: z.record(z.unknown()).nullish().describe("Les valeurs de retours du job"), + scheduled_for: z.date().describe("Date de lancement programmée"), + started_at: z.date().nullish().describe("Date de lancement"), + ended_at: z.date().nullish().describe("Date de fin d'execution"), + updated_at: z.date().describe("Date de mise à jour en base de données"), + created_at: z.date().describe("Date d'ajout en base de données"), + }) + .strict(); + +export const ZJobCron = z + .object({ + _id: zObjectId, + name: z + .enum>(Object.keys(CronsMap) as any) + .describe("Le nom de la tâche"), + type: z.literal("cron"), + status: z + .enum(["pending", "will_start", "running", "finished", "blocked", "errored"]) + .describe("Statut courant du job"), + sync: z.boolean().describe("Si le job est synchrone"), + cron_string: z.string().describe("standard cron string exemple: '*/2 * * * *'"), + scheduled_for: z.date().describe("Date de lancement programmée"), + updated_at: z.date().describe("Date de mise à jour en base de données"), + created_at: z.date().describe("Date d'ajout en base de données"), + }) + .strict(); + +export const ZJobCronTask = z + .object({ + _id: zObjectId, + name: z + .enum>(Object.keys(CronsMap) as any) + .describe("Le nom de la tâche"), + type: z.literal("cron_task"), + status: z + .enum(["pending", "will_start", "running", "finished", "blocked", "errored"]) + .describe("Statut courant du job"), + sync: z.boolean().describe("Si le job est synchrone"), scheduled_for: z.date().describe("Date de lancement programmée"), started_at: z.date().optional().describe("Date de lancement"), ended_at: z.date().optional().describe("Date de fin d'execution"), - updated_at: z.date().optional().describe("Date de mise à jour en base de données"), - created_at: z.date().optional().describe("Date d'ajout en base de données"), + updated_at: z.date().describe("Date de mise à jour en base de données"), + created_at: z.date().describe("Date d'ajout en base de données"), }) .strict(); +const ZJob = z.discriminatedUnion("type", [ZJobSimple, ZJobCron, ZJobCronTask]); + export type IJob = z.output; export type IJobJson = Jsonify>; +export type IJobsSimple = z.output; +export type IJobsCron = z.output; +export type IJobsCronTask = z.output; + export default { zod: ZJob, indexes, diff --git a/server/src/common/utils/asyncUtils.ts b/server/src/common/utils/asyncUtils.ts index cbe3ceedd..1d25cd40d 100644 --- a/server/src/common/utils/asyncUtils.ts +++ b/server/src/common/utils/asyncUtils.ts @@ -5,9 +5,11 @@ export const asyncForEach = async (array, callback) => { }; export function timeout(promise, millis) { - const timeout = new Promise((resolve, reject) => setTimeout(() => reject(`Timed out after ${millis} ms.`), millis)); - // @ts-expect-error - return Promise.race([promise, timeout]).finally(() => clearTimeout(timeout)); + let timeout: NodeJS.Timeout; + const timeoutPromise = new Promise((resolve, reject) => { + timeout = setTimeout(() => reject(`Timed out after ${millis} ms.`), millis); + }); + return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timeout)); } export async function sleep(durationMs: number, signal?: AbortSignal): Promise { diff --git a/server/src/common/utils/cacheUtils.ts b/server/src/common/utils/cacheUtils.ts index 36aba1f3a..cafc1c320 100644 --- a/server/src/common/utils/cacheUtils.ts +++ b/server/src/common/utils/cacheUtils.ts @@ -19,7 +19,7 @@ export async function tryCachedExecution( setTimeout(() => { logger.debug({ cacheKey, expiration }, "clear cache"); delete cache[cacheKey]; - }, expiration); + }, expiration).unref(); } return await cachedResult; } diff --git a/server/src/db/migrations/20231023100539-clear-jobs.ts b/server/src/db/migrations/20231023100539-clear-jobs.ts new file mode 100644 index 000000000..9b9921c3b --- /dev/null +++ b/server/src/db/migrations/20231023100539-clear-jobs.ts @@ -0,0 +1,5 @@ +import { Db } from "mongodb"; + +export const up = async (db: Db) => { + await db.collection("jobs").deleteMany({}); +}; diff --git a/server/src/jobs/crons_actions.ts b/server/src/jobs/crons_actions.ts index 16cc3d781..8cd06ed10 100644 --- a/server/src/jobs/crons_actions.ts +++ b/server/src/jobs/crons_actions.ts @@ -1,9 +1,19 @@ import cronParser from "cron-parser"; import logger from "@/common/logger"; +import { IJobsCron } from "@/common/model/job.model"; import { getDbCollection } from "@/common/mongodb"; - -import { createJob, findJob, findJobs, updateJob } from "../common/actions/job.actions"; +import config from "@/config"; + +import { + createJobCron, + createJobCronTask, + createJobSimple, + findJob, + findJobs, + updateJob, + updateJobCron, +} from "../common/actions/job.actions"; import { CRONS } from "./jobs"; import { addJob } from "./jobs_actions"; @@ -16,6 +26,10 @@ function parseCronString(cronString: string, options: { currentDate: string } | } export async function cronsInit() { + if (config.env === "preview") { + return; + } + logger.info(`Crons - initialise crons in DB`); const crons = Object.values(CRONS); @@ -27,7 +41,7 @@ export async function cronsInit() { }); await getDbCollection("jobs").deleteMany({ name: { $nin: crons.map((c) => c.name) }, - status: "pending", + status: { $in: ["pending", "will_start"] }, type: "cron_task", }); @@ -38,30 +52,33 @@ export async function cronsInit() { }); if (!cronJob) { - await createJob({ + await createJobCron({ name: cron.name, - type: "cron", cron_string: cron.cron_string, scheduled_for: new Date(), + sync: true, }); schedulerRequired = true; - } else if (cronJob.cron_string !== cron.cron_string) { - await updateJob(cronJob._id, { - cron_string: cron.cron_string, + } else if (cronJob.type === "cron" && cronJob.cron_string !== cron.cron_string) { + await updateJobCron(cronJob._id, cron.cron_string); + await getDbCollection("jobs").deleteMany({ + name: cronJob.name, + status: { $in: ["pending", "will_start"] }, + type: "cron_task", }); schedulerRequired = true; } } if (schedulerRequired) { - await addJob({ name: "crons:scheduler", queued: true }); + await addJob({ name: "crons:scheduler", queued: true, payload: {} }); } } export async function cronsScheduler(): Promise { logger.info(`Crons - Check and run crons`); - const crons = await findJobs( + const crons = await findJobs( { type: "cron", scheduled_for: { $lte: new Date() }, @@ -73,8 +90,7 @@ export async function cronsScheduler(): Promise { const next = parseCronString(cron.cron_string ?? "", { currentDate: cron.scheduled_for, }).next(); - await createJob({ - type: "cron_task", + await createJobCronTask({ name: cron.name, scheduled_for: next.toDate(), }); @@ -93,9 +109,10 @@ export async function cronsScheduler(): Promise { if (!cron) return; cron.scheduled_for.setSeconds(cron.scheduled_for.getSeconds() + 1); // add DELTA of 1 sec - await createJob({ - type: "simple", + await createJobSimple({ name: "crons:scheduler", scheduled_for: cron.scheduled_for, + sync: false, + payload: {}, }); } diff --git a/server/src/jobs/jobs.ts b/server/src/jobs/jobs.ts index 357902e6b..4e6bcf9d9 100644 --- a/server/src/jobs/jobs.ts +++ b/server/src/jobs/jobs.ts @@ -1,5 +1,5 @@ import logger from "@/common/logger"; -import { IJob } from "@/common/model/job.model"; +import { IJobsCronTask, IJobsSimple } from "@/common/model/job.model"; import { create as createMigration, status as statusMigration, up as upMigration } from "@/jobs/migrations/migrations"; import { clear, clearUsers } from "./clear/clear-all"; @@ -50,15 +50,8 @@ import { } from "./users/generate-password-update-token"; import { updateUsersApiSeeders } from "./users/update-apiSeeders"; -interface CronDef { - name: string; - cron_string: string; - handler: () => Promise; -} - -export const CRONS: Record = { +export const CronsMap = { "Run daily jobs each day at 02h30": { - name: "Run daily jobs each day at 02h30", cron_string: "30 2 * * *", handler: async () => { // # Remplissage des organismes issus du référentiel @@ -98,7 +91,6 @@ export const CRONS: Record = { }, "Send reminder emails at 7h": { - name: "Send reminder emails at 7h", cron_string: "0 7 * * *", handler: async () => { await addJob({ name: "send-reminder-emails", queued: true }); @@ -107,7 +99,6 @@ export const CRONS: Record = { }, "Run hydrate contrats DECA job each day at 19h45": { - name: "Run hydrate contrats DECA job each day at 19h45", cron_string: "45 19 * * *", handler: async () => { // # Remplissage des contrats DECA @@ -119,19 +110,31 @@ export const CRONS: Record = { // TODO : Checker si coté métier l'archivage est toujours prévu ? // "Run archive dossiers apprenants & effectifs job each first day of month at 12h45": { - // name: "Run archive dossiers apprenants & effectifs job each first day of month at 12h45", // cron_string: "45 12 1 * *", // handler: async () => { // // run-archive-job.sh yarn cli archive:dossiersApprenantsEffectifs // return 0; // }, // }, -}; +} satisfies Record>; + +export type CronName = keyof typeof CronsMap; + +interface CronDef { + name: CronName; + cron_string: string; + handler: () => Promise; +} + +export const CRONS: CronDef[] = Object.entries(CronsMap).map(([name, cronDef]) => ({ + ...cronDef, + name: name as CronName, +})); -export async function runJob(job: IJob): Promise { +export async function runJob(job: IJobsCronTask | IJobsSimple): Promise { return executeJob(job, async () => { if (job.type === "cron_task") { - return CRONS[job.name].handler(); + return CronsMap[job.name].handler(); } switch (job.name) { case "init:dev": diff --git a/server/src/jobs/jobs_actions.ts b/server/src/jobs/jobs_actions.ts index 0da6bba99..371e5a93f 100644 --- a/server/src/jobs/jobs_actions.ts +++ b/server/src/jobs/jobs_actions.ts @@ -1,26 +1,27 @@ import { captureException, getCurrentHub, runWithAsyncContext } from "@sentry/node"; +import Boom from "boom"; import { formatDuration, intervalToDuration } from "date-fns"; import logger from "@/common/logger"; import { jobsDb } from "@/common/model/collections"; -import { IJob } from "@/common/model/job.model"; +import { IJob, IJobsSimple } from "@/common/model/job.model"; import { sleep } from "@/common/utils/asyncUtils"; -import { createJob, updateJob } from "../common/actions/job.actions"; +import { createJobSimple, updateJob } from "../common/actions/job.actions"; import { runJob } from "./jobs"; +type AddJobSimpleParams = Pick & + Partial> & { queued?: boolean }; + export async function addJob({ name, - type = "simple", - payload = {}, + payload, scheduled_for = new Date(), queued = false, -}: Pick & - Partial> & { queued?: boolean }): Promise { - const job = await createJob({ +}: AddJobSimpleParams): Promise { + const job = await createJobSimple({ name, - type, payload, scheduled_for, sync: !queued, @@ -50,6 +51,10 @@ export async function processor(signal: AbortSignal): Promise { ); if (nextJob) { + if (nextJob.type === "cron") { + throw Boom.internal("Unexpected"); + } + logger.info({ job: nextJob.name }, "job will start"); await runJob(nextJob); } else { @@ -107,11 +112,11 @@ const runner = async (job: IJob, jobFunc: () => Promise): Promise Promise) { return runWithAsyncContext(async () => { const hub = getCurrentHub(); - const transaction = hub.startTransaction({ + const transaction = hub?.startTransaction({ name: `JOB: ${job.name}`, op: "processor.job", }); - hub.configureScope((scope) => { + hub?.configureScope((scope) => { scope.setSpan(transaction); scope.setTag("job", job.name); scope.setContext("job", job); @@ -120,8 +125,8 @@ export function executeJob(job: IJob, jobFunc: () => Promise) { try { return await runner(job, jobFunc); } finally { - transaction.setMeasurement("job.execute", Date.now() - start, "millisecond"); - transaction.finish(); + transaction?.setMeasurement("job.execute", Date.now() - start, "millisecond"); + transaction?.finish(); } }); } diff --git a/server/tests/jobs/jobs_actions.test.ts b/server/tests/jobs/jobs_actions.test.ts index 8b1d40cdf..fc676a263 100644 --- a/server/tests/jobs/jobs_actions.test.ts +++ b/server/tests/jobs/jobs_actions.test.ts @@ -3,7 +3,7 @@ import * as Sentry from "@sentry/node"; import { advanceTo, clear } from "jest-date-mock"; import sentryTestkit from "sentry-testkit"; -import { createJob, findJob } from "@/common/actions/job.actions"; +import { createJobSimple, findJob } from "@/common/actions/job.actions"; import { getSentryOptions } from "@/common/services/sentry/sentry"; import { executeJob } from "@/jobs/jobs_actions"; import { useMongo } from "@tests/jest/setupMongo"; @@ -39,9 +39,8 @@ describe("executeJob", () => { useMongo(); describe("when job is success", () => { it("should return exit code", async () => { - const job = await createJob({ + const job = await createJobSimple({ name: "success:job", - type: "simple", payload: { name: "Moroine" }, scheduled_for: timings.scheduled_for, sync: true, @@ -89,9 +88,8 @@ describe("executeJob", () => { describe("when job fails", () => { it("should return exit code", async () => { - const job = await createJob({ + const job = await createJobSimple({ name: "failure:job", - type: "simple", payload: { name: "Moroine" }, scheduled_for: timings.scheduled_for, sync: true, diff --git a/server/tsconfig.json b/server/tsconfig.json index 04ff57a1a..525d27ca8 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -1,7 +1,5 @@ { "files": ["src/types.d.ts"], - "include": ["./**/*.ts", "./**/*.js"], - "exclude": ["**/node_modules", "coverage", "dist"], "compilerOptions": { /* Visit https://aka.ms/tsconfig.json to read more about this file */ /* Projects */ @@ -14,11 +12,10 @@ /* Modules */ "module": "ESNext", // TODO: maybe nodenext ? "moduleResolution": "node", - "baseUrl": "./", "paths": { "@/*": ["./src/*"], "@tests/*": ["./tests/*"], - "shared": ["../shared"] + "shared/*": ["../shared/*"] }, "resolveJsonModule": true, /* JavaScript Support */ @@ -28,6 +25,7 @@ /* Interop Constraints */ "esModuleInterop": true, "isolatedModules": true, + "forceConsistentCasingInFileNames": true, /* Type Checking */ "strict": true, "noImplicitAny": false, @@ -35,5 +33,7 @@ "skipLibCheck": true, /* Output Formatting */ "pretty": true - } + }, + "include": ["./**/*.ts", "./**/*.js"], + "exclude": ["**/node_modules", "coverage", "dist"] } diff --git a/server/tsup.config.ts b/server/tsup.config.ts index 8236794ff..d747122f3 100644 --- a/server/tsup.config.ts +++ b/server/tsup.config.ts @@ -4,22 +4,23 @@ import { basename } from "node:path"; import { defineConfig } from "tsup"; export default defineConfig((options) => { - const files = fs.readdirSync("./src/db/migrations"); - const isDev = options.env?.NODE_ENV !== "production"; + const isWatched = options.env?.TSUP_WATCH === "true"; + const migrationFiles = fs.readdirSync("./src/db/migrations"); const entry: Record = { index: isDev ? "src/dev.ts" : "src/main.ts", }; - for (const file of files) { + for (const file of migrationFiles) { entry[`db/migrations/${basename(file, ".ts")}`] = `src/db/migrations/${file}`; } return { entry, - watch: isDev && options.watch ? ["./src", "../shared/src"] : false, - onSuccess: isDev && options.watch ? "yarn cli start --withProcessor" : "", + watch: isWatched ? ["./src", "../shared"] : false, + onSuccess: isWatched ? "yarn cli start --withProcessor" : "", + ignoreWatch: ["../shared/node_modules/**"], // In watch mode doesn't exit cleanly as it causes EADDRINUSE error killSignal: "SIGKILL", target: "es2022", diff --git a/shared/tsconfig.json b/shared/tsconfig.json index 1adac3132..0271fc6b5 100644 --- a/shared/tsconfig.json +++ b/shared/tsconfig.json @@ -1,13 +1,14 @@ { - "include": ["."], - "exclude": ["**/node_modules", ".next/*"], + "include": ["**/*.ts"], + "exclude": ["**/node_modules"], "compilerOptions": { + "incremental": true, "strictPropertyInitialization": false, "declarationMap": true, "listEmittedFiles": false, "listFiles": false, "pretty": true, - "isolatedModules": false, + "isolatedModules": true, "lib": ["ES2023"] /* Emit ECMAScript-standard-compliant class fields. */, "module": "ESNext" /* Specify what module code is generated. */, "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, @@ -20,7 +21,6 @@ "declaration": true, "noEmit": true, "forceConsistentCasingInFileNames": true, - "allowJs": false, - "baseUrl": "./" + "allowJs": false } } diff --git a/ui/config.public.ts b/ui/config.public.ts index 121dd7712..a76f7f4f5 100644 --- a/ui/config.public.ts +++ b/ui/config.public.ts @@ -2,7 +2,8 @@ export interface PublicConfig { sentry_dsn: string; baseUrl: string; host: string; - env: "local" | "dev" | "recette" | "production" | "preview"; + env: "local" | "recette" | "production" | "preview"; + version: string; } const SENTRY_DSN = "https://362c29c6acbe4a599640109d87e77beb@o4504570758561792.ingest.sentry.io/4504570760265728"; @@ -15,6 +16,7 @@ function getProductionPublicConfig(): PublicConfig { env: "production", host, baseUrl: `https://${host}`, + version: getVersion(), }; } @@ -26,17 +28,7 @@ function getRecettePublicConfig(): PublicConfig { env: "recette", host, baseUrl: `https://${host}`, - }; -} - -function getDevPublicConfig(): PublicConfig { - const host = "cfas-dev.apprentissage.beta.gouv.fr"; - - return { - sentry_dsn: SENTRY_DSN, - env: "dev", - host, - baseUrl: `https://${host}`, + version: getVersion(), }; } @@ -55,6 +47,7 @@ function getPreviewPublicConfig(): PublicConfig { env: "preview", host, baseUrl: `https://${host}`, + version: getVersion(), }; } @@ -66,6 +59,7 @@ function getLocalPublicConfig(): PublicConfig { env: "local", host, baseUrl: `http://${host}:${process.env.NEXT_PUBLIC_API_PORT}`, + version: getVersion(), }; } @@ -84,7 +78,6 @@ function getEnv(): PublicConfig["env"] { switch (env) { case "production": case "recette": - case "dev": case "preview": case "local": return env; @@ -99,8 +92,6 @@ function getPublicConfig(): PublicConfig { return getProductionPublicConfig(); case "recette": return getRecettePublicConfig(); - case "dev": - return getDevPublicConfig(); case "preview": return getPreviewPublicConfig(); case "local": diff --git a/ui/next.config.js b/ui/next.config.js index 4f006fd36..ad2643b91 100644 --- a/ui/next.config.js +++ b/ui/next.config.js @@ -29,11 +29,11 @@ const nextConfig = { transpilePackages: ["shared"], poweredByHeader: false, swcMinify: true, - output: "standalone", experimental: { appDir: false, typedRoutes: true, }, + output: "standalone", eslint: { dirs: ["."], }, diff --git a/ui/sentry.client.config.ts b/ui/sentry.client.config.ts index c4f5cc7fc..2b391ce82 100644 --- a/ui/sentry.client.config.ts +++ b/ui/sentry.client.config.ts @@ -10,10 +10,10 @@ import { publicConfig } from "./config.public"; Sentry.init({ dsn: publicConfig.sentry_dsn, tracesSampleRate: publicConfig.env === "production" ? 0.1 : 1.0, - tracePropagationTargets: [/\.apprentissage\.beta\.gouv\.fr$/], + tracePropagationTargets: [/^https:\/\/[^/]*\.apprentissage\.beta\.gouv\.fr/, publicConfig.baseUrl, /^\//], environment: publicConfig.env, enabled: publicConfig.env !== "local", - // debug: true, + release: publicConfig.version, normalizeDepth: 8, // replaysOnErrorSampleRate: 1.0, // replaysSessionSampleRate: 0.1, @@ -22,6 +22,7 @@ Sentry.init({ // maskAllText: true, // blockAllMedia: true, // }), + // new Sentry.BrowserTracing(), // @ts-ignore new ExtraErrorData({ depth: 8 }), // @ts-ignore diff --git a/ui/sentry.edge.config.ts b/ui/sentry.edge.config.ts index a661bed10..280fcb1b2 100644 --- a/ui/sentry.edge.config.ts +++ b/ui/sentry.edge.config.ts @@ -11,10 +11,10 @@ import { publicConfig } from "./config.public"; Sentry.init({ dsn: publicConfig.sentry_dsn, tracesSampleRate: publicConfig.env === "production" ? 0.1 : 1.0, - tracePropagationTargets: [/\.apprentissage\.beta\.gouv\.fr$/], + tracePropagationTargets: [/^https:\/\/[^/]*\.apprentissage\.beta\.gouv\.fr/, publicConfig.baseUrl], environment: publicConfig.env, enabled: publicConfig.env !== "local", - // debug: true, + release: publicConfig.version, normalizeDepth: 8, integrations: [ new Sentry.Integrations.Http({ tracing: true }), diff --git a/ui/sentry.server.config.ts b/ui/sentry.server.config.ts index 139e05c8d..08bd2a823 100644 --- a/ui/sentry.server.config.ts +++ b/ui/sentry.server.config.ts @@ -10,10 +10,10 @@ import { publicConfig } from "./config.public"; Sentry.init({ dsn: publicConfig.sentry_dsn, tracesSampleRate: publicConfig.env === "production" ? 0.1 : 1.0, - tracePropagationTargets: [/\.apprentissage\.beta\.gouv\.fr$/], + tracePropagationTargets: [/^https:\/\/[^/]*\.apprentissage\.beta\.gouv\.fr/, publicConfig.baseUrl], environment: publicConfig.env, enabled: publicConfig.env !== "local", - // debug: true, + release: publicConfig.version, normalizeDepth: 8, integrations: [ new Sentry.Integrations.Http({ tracing: true }), diff --git a/ui/tsconfig.json b/ui/tsconfig.json index c21fe2353..cbf84c9a3 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -18,7 +18,7 @@ "isolatedModules": true, "jsx": "preserve", "paths": { - "@/*": ["*"], + "@/*": ["./*"], "shared": ["../shared"] }, "plugins": [ @@ -28,5 +28,5 @@ ] }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "exclude": ["node_modules", "public"] }