diff --git a/.dockerignore b/.dockerignore index c5fd9e6..7b1a025 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,5 @@ .git .ssh -.kamal/secrets* vendor node_modules storage/logs diff --git a/.gitea/workflows/cd.yml b/.gitea/workflows/cd.yml new file mode 100644 index 0000000..67687c1 --- /dev/null +++ b/.gitea/workflows/cd.yml @@ -0,0 +1,46 @@ +name: CD +on: + workflow_run: + workflows: + - CI + branches: + - main + types: + - completed +jobs: + deploy: + name: Déploiement production + if: ${{ github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + steps: + - name: Configuration SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.DEPLOY_SSH_KEY }}" \ + > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + ssh-keyscan \ + -H "${{ secrets.DEPLOY_HOST }}" \ + >> ~/.ssh/known_hosts + + + - name: 🚀 Deploy Docker + run: | + ssh \ + -i ~/.ssh/id_ed25519 \ + ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} << 'EOF' + set -e + + echo "Projet" + cd /data/stacks/daily-meal-api + + echo "Pull nouvelle image" + docker compose pull + + echo "Redémarrage" + docker compose up -d + + echo "Migration Laravel" + docker compose exec -T app php artisan migrate --force + + EOF diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..0badb1a --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,81 @@ +name: CI + +on: + push: + branches: + - main + - develop + + pull_request: + branches: + - main +jobs: + test: + name: 🧪 Tests Laravel + runs-on: ubuntu-latest + env: + DB_CONNECTION: sqlite + DB_DATABASE: database/database.sqlite + steps: + - name: 📥 Checkout + uses: actions/checkout@v4 + - name: 🐘 Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + extensions: bcmath,ctype,curl,dom,fileinfo,intl,mbstring,openssl,pdo,tokenizer,xml,zip + coverage: none + + - name: Cache Composer + uses: actions/cache@v4 + with: + path: ~/.composer/cache + key: composer-cache + + - name: Install Composer dependencies + run: | + composer install \ + --no-interaction \ + --prefer-dist \ + --no-progress + + - name: ⚙️ Prepare Laravel + run: | + cp .env.example .env + php artisan key:generate + touch database/database.sqlite + php artisan migrate --force + + + - name: 🧪 Run PHPUnit + run: | + php artisan test + + + + docker: + name: 🐳 Build & Push Image + needs: test + if: ${{ github.ref == 'refs/heads/main' }} + runs-on: ubuntu-latest + steps: + - name: 📥 Checkout + uses: actions/checkout@v4 + + - name: 🔐 Login Registry Gitea + run: | + docker login git.leonmorival.xyz \ + -u "${{ secrets.REGISTRY_USER }}" \ + -p "${{ secrets.REGISTRY_PASSWORD }}" + + - name: 🏗️ Build image + run: | + docker build \ + -t git.leonmorival.xyz/leonm/daily-meal-api:latest \ + -t git.leonmorival.xyz/leonm/daily-meal-api:${{ github.sha }} \ + . + + - name: 📤 Push image + run: | + docker push git.leonmorival.xyz/leonm/daily-meal-api:latest + docker push git.leonmorival.xyz/leonm/daily-meal-api:${{ github.sha }} diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml deleted file mode 100644 index fe6ed5a..0000000 --- a/.gitea/workflows/deploy.yml +++ /dev/null @@ -1,165 +0,0 @@ -name: Laravel CI-CD - -on: - push: - branches: ["main"] - -env: - KAMAL_IMAGE: ghcr.io/basecamp/kamal:v2.11.0 - -jobs: - test: - name: Tests Unitaires - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Run Tests - uses: docker://laravelsail/php84-composer:latest - env: - APP_ENV: testing - APP_KEY: base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - DB_CONNECTION: sqlite - DB_DATABASE: ":memory:" - with: - args: > - bash -lc "apt-get update - && apt-get install -y --no-install-recommends libicu-dev - && docker-php-ext-install intl - && php -m | grep -qi '^intl$' - && composer install --no-interaction --prefer-dist - && php -d memory_limit=512M artisan test --compact" - - deploy: - name: Deploy with Kamal - needs: test - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Deploy - env: - GITEA_TOKEN: ${{ secrets.TOKEN_GITEA }} - APP_KEY: ${{ secrets.APP_KEY }} - DB_DATABASE: ${{ secrets.DB_DATABASE }} - DB_USERNAME: ${{ secrets.DB_USERNAME }} - DB_PASSWORD: ${{ secrets.DB_PASSWORD }} - MEILISEARCH_KEY: ${{ secrets.MEILISEARCH_KEY }} - GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} - STRIPE_KEY: ${{ secrets.STRIPE_KEY }} - STRIPE_SECRET: ${{ secrets.STRIPE_SECRET }} - STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }} - SSH_HOST: ${{ secrets.SSH_HOST }} - SSH_USER: ${{ secrets.SSH_USER }} - SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} - STRAVA_CLIENT_ID: ${{ secrets.STRAVA_CLIENT_ID }} - STRAVA_CLIENT_SECRET: ${{ secrets.STRAVA_CLIENT_SECRET }} - RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - - run: | - set -eu - : "${GEMINI_API_KEY:?GEMINI_API_KEY secret is required}" - : "${STRIPE_KEY:?STRIPE_KEY secret is required}" - : "${STRIPE_SECRET:?STRIPE_SECRET secret is required}" - : "${STRIPE_WEBHOOK_SECRET:?STRIPE_WEBHOOK_SECRET secret is required}" - : "${AWS_ACCESS_KEY_ID:?AWS_ACCESS_KEY_ID secret is required}" - : "${AWS_SECRET_ACCESS_KEY:?AWS_SECRET_ACCESS_KEY secret is required}" - - WORKSPACE="${GITHUB_WORKSPACE:-$PWD}" - test -f "$WORKSPACE/config/deploy.yml" - - tar \ - --exclude=.kamal/secrets \ - --exclude=.kamal/secrets.* \ - --exclude=.env \ - --exclude=.env.* \ - -C "$WORKSPACE" -cf - . | docker run --rm -i \ - -v /var/run/docker.sock:/var/run/docker.sock \ - -e GITEA_TOKEN \ - -e APP_KEY \ - -e DB_DATABASE \ - -e DB_USERNAME \ - -e DB_PASSWORD \ - -e MEILISEARCH_KEY \ - -e GEMINI_API_KEY \ - -e STRIPE_KEY \ - -e STRIPE_SECRET \ - -e STRIPE_WEBHOOK_SECRET \ - -e SSH_HOST \ - -e SSH_USER \ - -e SSH_PRIVATE_KEY \ - -e STRAVA_CLIENT_ID \ - -e STRAVA_CLIENT_SECRET \ - -e RESEND_API_KEY \ - -e AWS_ACCESS_KEY_ID \ - -e AWS_SECRET_ACCESS_KEY \ - --entrypoint /bin/sh \ - "$KAMAL_IMAGE" -lc ' - set -eu - - mkdir -p /workdir /root/.ssh - tar -xf - -C /workdir - cd /workdir - - chmod 700 /root/.ssh - printf "%s\n" "$SSH_PRIVATE_KEY" | tr -d "\r" > /root/.ssh/id_ed25519 - chmod 600 /root/.ssh/id_ed25519 - - REMOTE_HOST="${SSH_HOST:-89.167.35.217}" - REMOTE_USER="${SSH_USER:-root}" - - mkdir -p /workdir/.ssh - cat > /workdir/.ssh/config <<'"'"'SSH_CONFIG'"'"' - Host * - IdentityFile /root/.ssh/id_ed25519 - IdentitiesOnly yes - StrictHostKeyChecking yes - UserKnownHostsFile /workdir/.ssh/known_hosts - GlobalKnownHostsFile /dev/null - CheckHostIP no - SSH_CONFIG - ssh-keyscan -H "$REMOTE_HOST" > /workdir/.ssh/known_hosts - chmod 600 /workdir/.ssh/config /workdir/.ssh/known_hosts - - mkdir -p .kamal - cat > .kamal/secrets <<'"'"'SECRETS'"'"' - GITEA_TOKEN=$GITEA_TOKEN - APP_KEY=$APP_KEY - DB_DATABASE=$DB_DATABASE - DB_USERNAME=$DB_USERNAME - DB_PASSWORD=$DB_PASSWORD - MEILISEARCH_KEY=$MEILISEARCH_KEY - GEMINI_API_KEY=$GEMINI_API_KEY - STRIPE_KEY=$STRIPE_KEY - STRIPE_SECRET=$STRIPE_SECRET - STRIPE_WEBHOOK_SECRET=$STRIPE_WEBHOOK_SECRET - SSH_PRIVATE_KEY=$SSH_PRIVATE_KEY - STRAVA_CLIENT_ID=$STRAVA_CLIENT_ID - STRAVA_CLIENT_SECRET=$STRAVA_CLIENT_SECRET - RESEND_API_KEY=$RESEND_API_KEY - AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID - AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY - SECRETS - chmod 600 .kamal/secrets - - ssh -F /workdir/.ssh/config "$REMOTE_USER@$REMOTE_HOST" <<'"'"'REMOTE'"'"' - set -eu - - docker network inspect bemeal_public >/dev/null 2>&1 || docker network create bemeal_public - docker network inspect bemeal_internal >/dev/null 2>&1 || docker network create bemeal_internal - - for container in bemeal-pgsql bemeal-redis bemeal-meilisearch bemeal-adminer; do - if docker inspect "$container" >/dev/null 2>&1; then - docker network connect bemeal_internal "$container" >/dev/null 2>&1 || true - fi - done - REMOTE - - /kamal/bin/kamal deploy - ' diff --git a/.gitignore b/.gitignore index 568cf9d..278fa39 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,3 @@ Homestead.json Homestead.yaml Thumbs.db .env.prod -.kamal/secrets diff --git a/.kamal/hooks/docker-setup.sample b/.kamal/hooks/docker-setup.sample deleted file mode 100755 index 2fb07d7..0000000 --- a/.kamal/hooks/docker-setup.sample +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -echo "Docker set up on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/post-app-boot.sample b/.kamal/hooks/post-app-boot.sample deleted file mode 100755 index 70f9c4b..0000000 --- a/.kamal/hooks/post-app-boot.sample +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -echo "Booted app version $KAMAL_VERSION on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/post-deploy.sample b/.kamal/hooks/post-deploy.sample deleted file mode 100755 index fd364c2..0000000 --- a/.kamal/hooks/post-deploy.sample +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh - -# A sample post-deploy hook -# -# These environment variables are available: -# KAMAL_RECORDED_AT -# KAMAL_PERFORMER -# KAMAL_VERSION -# KAMAL_HOSTS -# KAMAL_ROLES (if set) -# KAMAL_DESTINATION (if set) -# KAMAL_RUNTIME - -echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds" diff --git a/.kamal/hooks/post-proxy-reboot.sample b/.kamal/hooks/post-proxy-reboot.sample deleted file mode 100755 index 1435a67..0000000 --- a/.kamal/hooks/post-proxy-reboot.sample +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -echo "Rebooted kamal-proxy on $KAMAL_HOSTS" diff --git a/.kamal/hooks/pre-app-boot.sample b/.kamal/hooks/pre-app-boot.sample deleted file mode 100755 index 45f7355..0000000 --- a/.kamal/hooks/pre-app-boot.sample +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -echo "Booting app version $KAMAL_VERSION on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/pre-build.sample b/.kamal/hooks/pre-build.sample deleted file mode 100755 index c5a5567..0000000 --- a/.kamal/hooks/pre-build.sample +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/sh - -# A sample pre-build hook -# -# Checks: -# 1. We have a clean checkout -# 2. A remote is configured -# 3. The branch has been pushed to the remote -# 4. The version we are deploying matches the remote -# -# These environment variables are available: -# KAMAL_RECORDED_AT -# KAMAL_PERFORMER -# KAMAL_VERSION -# KAMAL_HOSTS -# KAMAL_ROLES (if set) -# KAMAL_DESTINATION (if set) - -if [ -n "$(git status --porcelain)" ]; then - echo "Git checkout is not clean, aborting..." >&2 - git status --porcelain >&2 - exit 1 -fi - -first_remote=$(git remote) - -if [ -z "$first_remote" ]; then - echo "No git remote set, aborting..." >&2 - exit 1 -fi - -current_branch=$(git branch --show-current) - -if [ -z "$current_branch" ]; then - echo "Not on a git branch, aborting..." >&2 - exit 1 -fi - -remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1) - -if [ -z "$remote_head" ]; then - echo "Branch not pushed to remote, aborting..." >&2 - exit 1 -fi - -if [ "$KAMAL_VERSION" != "$remote_head" ]; then - echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2 - exit 1 -fi - -exit 0 diff --git a/.kamal/hooks/pre-connect.sample b/.kamal/hooks/pre-connect.sample deleted file mode 100755 index 77744bd..0000000 --- a/.kamal/hooks/pre-connect.sample +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env ruby - -# A sample pre-connect check -# -# Warms DNS before connecting to hosts in parallel -# -# These environment variables are available: -# KAMAL_RECORDED_AT -# KAMAL_PERFORMER -# KAMAL_VERSION -# KAMAL_HOSTS -# KAMAL_ROLES (if set) -# KAMAL_DESTINATION (if set) -# KAMAL_RUNTIME - -hosts = ENV["KAMAL_HOSTS"].split(",") -results = nil -max = 3 - -elapsed = Benchmark.realtime do - results = hosts.map do |host| - Thread.new do - tries = 1 - - begin - Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME) - rescue SocketError - if tries < max - puts "Retrying DNS warmup: #{host}" - tries += 1 - sleep rand - retry - else - puts "DNS warmup failed: #{host}" - host - end - end - - tries - end - end.map(&:value) -end - -retries = results.sum - hosts.size -nopes = results.count { |r| r == max } - -puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ] diff --git a/.kamal/hooks/pre-deploy.sample b/.kamal/hooks/pre-deploy.sample deleted file mode 100755 index 05b3055..0000000 --- a/.kamal/hooks/pre-deploy.sample +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env ruby - -# A sample pre-deploy hook -# -# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds. -# -# Fails unless the combined status is "success" -# -# These environment variables are available: -# KAMAL_RECORDED_AT -# KAMAL_PERFORMER -# KAMAL_VERSION -# KAMAL_HOSTS -# KAMAL_COMMAND -# KAMAL_SUBCOMMAND -# KAMAL_ROLES (if set) -# KAMAL_DESTINATION (if set) - -# Only check the build status for production deployments -if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production" - exit 0 -end - -require "bundler/inline" - -# true = install gems so this is fast on repeat invocations -gemfile(true, quiet: true) do - source "https://rubygems.org" - - gem "octokit" - gem "faraday-retry" -end - -MAX_ATTEMPTS = 72 -ATTEMPTS_GAP = 10 - -def exit_with_error(message) - $stderr.puts message - exit 1 -end - -class GithubStatusChecks - attr_reader :remote_url, :git_sha, :github_client, :combined_status - - def initialize - @remote_url = github_repo_from_remote_url - @git_sha = `git rev-parse HEAD`.strip - @github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"]) - refresh! - end - - def refresh! - @combined_status = github_client.combined_status(remote_url, git_sha) - end - - def state - combined_status[:state] - end - - def first_status_url - first_status = combined_status[:statuses].find { |status| status[:state] == state } - first_status && first_status[:target_url] - end - - def complete_count - combined_status[:statuses].count { |status| status[:state] != "pending"} - end - - def total_count - combined_status[:statuses].count - end - - def current_status - if total_count > 0 - "Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..." - else - "Build not started..." - end - end - - private - def github_repo_from_remote_url - url = `git config --get remote.origin.url`.strip.delete_suffix(".git") - if url.start_with?("https://github.com/") - url.delete_prefix("https://github.com/") - elsif url.start_with?("git@github.com:") - url.delete_prefix("git@github.com:") - else - url - end - end -end - - -$stdout.sync = true - -begin - puts "Checking build status..." - - attempts = 0 - checks = GithubStatusChecks.new - - loop do - case checks.state - when "success" - puts "Checks passed, see #{checks.first_status_url}" - exit 0 - when "failure" - exit_with_error "Checks failed, see #{checks.first_status_url}" - when "pending" - attempts += 1 - end - - exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS - - puts checks.current_status - sleep(ATTEMPTS_GAP) - checks.refresh! - end -rescue Octokit::NotFound - exit_with_error "Build status could not be found" -end diff --git a/.kamal/hooks/pre-proxy-reboot.sample b/.kamal/hooks/pre-proxy-reboot.sample deleted file mode 100755 index 061f805..0000000 --- a/.kamal/hooks/pre-proxy-reboot.sample +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -echo "Rebooting kamal-proxy on $KAMAL_HOSTS..." diff --git a/config/deploy.yml b/config/deploy.yml deleted file mode 100644 index c99bb71..0000000 --- a/config/deploy.yml +++ /dev/null @@ -1,104 +0,0 @@ -service: bemeal -image: daily_meal/bemeal-api -deploy_timeout: 120 - -x-hosts: &hosts - - <%= ENV.fetch("SSH_HOST", "89.167.35.217") %> - -x-web-networks: &web-networks - network: - - bemeal_public - - bemeal_internal - -x-worker-networks: &worker-networks - network: - - bemeal_internal - -servers: - web: - hosts: *hosts - options: *web-networks - horizon: - hosts: *hosts - cmd: php artisan horizon - options: *worker-networks - scheduler: - hosts: *hosts - cmd: php artisan schedule:work - options: *worker-networks - -proxy: - host: bemeal.leonmorival.com - ssl: false - app_port: 80 - run: - http_port: 8081 - https_port: 8443 - healthcheck: - path: /api/health - -registry: - server: gitea.leonmorival.com - username: leon.morival@gmail.com - password: - - GITEA_TOKEN - -ssh: - user: <%= ENV.fetch("SSH_USER", "root") %> - keys_only: true - key_data: - - SSH_PRIVATE_KEY - config: false - -builder: - arch: amd64 - -env: - clear: - APP_LOCALE: fr - APP_ENV: production - APP_DEBUG: "false" - APP_NAME: "Bowly" - APP_URL: https://bemeal.leonmorival.com - APP_MOBILE_SCHEME: bowly - SERVER_NAME: ":80" - APP_RUNTIME: "Laravel\\FrankenPHP\\Runtime" - FILESYSTEM_DISK: s3 - AWS_DEFAULT_REGION: eu-north-1 - AWS_BUCKET: bowli - AWS_USE_PATH_STYLE_ENDPOINT: "false" - DB_CONNECTION: pgsql - DB_HOST: bemeal-pgsql - DB_PORT: "5432" - REDIS_HOST: bemeal-redis - REDIS_CLIENT: predis - QUEUE_CONNECTION: redis - SCOUT_DRIVER: meilisearch - MEILISEARCH_HOST: http://bemeal-meilisearch:7700 - MAIL_MAILER: resend - MAIL_FROM_ADDRESS: onboarding@resend.dev - MAIL_FROM_NAME: Bowli - CASHIER_CURRENCY: eur - secret: - - APP_KEY - - DB_DATABASE - - DB_USERNAME - - DB_PASSWORD - - MEILISEARCH_KEY - - GEMINI_API_KEY - - STRIPE_KEY - - STRIPE_SECRET - - STRIPE_WEBHOOK_SECRET - - STRAVA_CLIENT_ID - - STRAVA_CLIENT_SECRET - - RESEND_API_KEY - - AWS_ACCESS_KEY_ID - - AWS_SECRET_ACCESS_KEY - -volumes: - - bemeal_storage_data:/var/www/html/storage/app - - bemeal_storage_public_data:/var/www/html/storage/app/public - -aliases: - artisan: app exec --reuse "php artisan" - shell: app exec --interactive --reuse "sh"