feat: ci/cd
CI / 🧪 Tests Laravel (push) Successful in 2m49s
CI / 🐳 Build & Push Image (push) Successful in 2m41s

This commit is contained in:
2026-07-16 15:19:50 +02:00
parent ea0d625bf9
commit 5cf0510aa5
15 changed files with 127 additions and 520 deletions
-1
View File
@@ -1,6 +1,5 @@
.git .git
.ssh .ssh
.kamal/secrets*
vendor vendor
node_modules node_modules
storage/logs storage/logs
+46
View File
@@ -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
+81
View File
@@ -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 }}
-165
View File
@@ -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
'
-1
View File
@@ -23,4 +23,3 @@ Homestead.json
Homestead.yaml Homestead.yaml
Thumbs.db Thumbs.db
.env.prod .env.prod
.kamal/secrets
-3
View File
@@ -1,3 +0,0 @@
#!/bin/sh
echo "Docker set up on $KAMAL_HOSTS..."
-3
View File
@@ -1,3 +0,0 @@
#!/bin/sh
echo "Booted app version $KAMAL_VERSION on $KAMAL_HOSTS..."
-14
View File
@@ -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"
-3
View File
@@ -1,3 +0,0 @@
#!/bin/sh
echo "Rebooted kamal-proxy on $KAMAL_HOSTS"
-3
View File
@@ -1,3 +0,0 @@
#!/bin/sh
echo "Booting app version $KAMAL_VERSION on $KAMAL_HOSTS..."
-51
View File
@@ -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
-47
View File
@@ -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 ]
-122
View File
@@ -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
-3
View File
@@ -1,3 +0,0 @@
#!/bin/sh
echo "Rebooting kamal-proxy on $KAMAL_HOSTS..."
-104
View File
@@ -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"