first commit

This commit is contained in:
Hamza-Ayed
2026-10-04 00:19:45 +03:00
commit 05f1c9ec6b
93 changed files with 10782 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
# النشر إلى CloudPanel
هذا إعداد أولي للنشر من Git عبر SSH. لا ينشئ موقع CloudPanel أو DNS أو شهادة TLS أو قاعدة MySQL؛ تلك خطوات تجهيز أولي تُنفذ لحساب Site User. يستخدم المنفذ `2101` افتراضيًا حسب إعداد المستخدم.
## إعداد الخادم مرة واحدة
1. أنشئ PHP Site في CloudPanel باسم النطاق المطلوب ومستخدم موقع مستقل. اضبط document root بعد أول نسخة ليشير إلى `<DEPLOY_ROOT>/current/public`.
2. أنشئ قاعدة MySQL ومستخدمًا خاصًا بالموقع من CloudPanel، بصلاحيات قاعدة التطبيق فقط.
3. أنشئ مجلد موقع النشر الذي يملكه Site User، على سبيل المثال `/home/siteuser/htdocs/fitness.example.com`.
4. أضف مفتاح Git Deploy Key للقراءة فقط إلى حساب Git، واختبر أن Site User يستطيع `git ls-remote` على origin.
5. أضف بصمة مفتاح SSH الخاص بالخادم إلى `known_hosts` على جهاز النشر. يعمل السكربت مع `StrictHostKeyChecking=yes` ولا يتجاوز التحقق.
6. أنشئ `<DEPLOY_ROOT>/shared/.env` يدويًا على الخادم، مع إعداد DB ومفاتيح الخدمة. لا ترفع الملف إلى Git. السكربت يوقف النشر إذا كان الملف غير موجود.
7. جهز التطبيق بحيث يحتوي جذر الإصدار `public/index.php`، ويحمّل إعداداته من `shared/.env`. ملفات الرفع الخاصة تحفظ في shared خارج `public/`.
8. ثبّت شهادة TLS ووجّه DNS إلى الخادم من CloudPanel، ثم أضف health endpoint قبل أول نشر إنتاجي.
CloudPanel ينشئ PHP site ومستخدمه، ويمكن تغيير document root إلى مجلد `public` وفق [دليل CloudPanel](https://www.cloudpanel.io/docs/v2/php/applications/other/). ملفات PHP الحالية موضوعة مباشرة داخل `backend/` ولا يوجد `public/` بعد؛ لذلك السكربت سيرفض إصدارها حتى تُنشأ نقطة دخول عامة آمنة.
## إعداد جهاز النشر
أضف remote باسم `origin` إلى repository بعد إنشائه، وادفع الفرع المقصود قبل النشر. عيّن المتغيرات في جلسة طرفية خاصة:
```bash
export DEPLOY_HOST='server.example.com'
export DEPLOY_USER='cloudpanel-site-user'
export DEPLOY_ROOT='/home/cloudpanel-site-user/htdocs/fitness.example.com'
export DEPLOY_DOMAIN='fitness.example.com'
export DEPLOY_PORT='2101'
export DEPLOY_REF='main'
```
تحقق من الإعداد المحلي دون اتصال SSH:
```bash
DEPLOY_DRY_RUN=1 bash deploy/sync-to-server.sh
```
بعد إنشاء public web root وضبط الخادم، نفّذ النشر من جذر المستودع:
```bash
bash deploy/sync-to-server.sh
```
السكريبت يرفض شجرة عمل غير نظيفة، ويتأكد أن `HEAD` يطابق آخر commit منشور للفرع نفسه في Git. يستخدم SSH بتهيئة غير تفاعلية، ثم يجلب ذلك الفرع على الخادم، يستخرج commit إلى مجلد إصدار منفصل، ويفحص بناء PHP نحويًا إن كان PHP CLI متوفرًا. أخيرًا يحول رابط `current` إلى الإصدار الجديد.
أول تشغيل يحتاج مفتاح SSH صالحًا للوصول إلى Site User عبر المنفذ 2101 ومفتاح Git صالحًا على الخادم. مفاتيح SSH لا تمرر كمتغير ولا تحفظ في هذا المستودع.
## الاسترجاع
كل إصدار محفوظ تحت `<DEPLOY_ROOT>/releases/<commit-sha>`. عند الحاجة، أنشئ رابطًا مؤقتًا إلى مجلد الإصدار السابق ثم استبدل `current` ذريًا من جلسة SSH الخاصة بـSite User. لا تحذف مجلدات الإصدار السابقة أثناء فترة المراجعة. استرجاع ملفات التطبيق لا يسترجع قاعدة البيانات؛ migrations تحتاج سياسة رجوع منفصلة ونسخة احتياطية مختبرة.
لا ينفذ السكربت أوامر SQL أو يغيّر صلاحيات قاعدة البيانات أو ينشئ مستخدمين. بعد إضافة migrations، تُدار كخطوة إصدار مراجعة ومختبرة، ولا تُنفّذ تلقائيًا من آلية النشر قبل تحديد سياسة التراجع.
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env bash
set -euo pipefail
die() { printf 'deploy: %s\n' "$*" >&2; exit 1; }
for name in DEPLOY_HOST DEPLOY_USER DEPLOY_ROOT DEPLOY_DOMAIN; do
[[ -n "${!name:-}" ]] || die "set $name before running"
done
command -v git >/dev/null || die 'git is required locally'
command -v ssh >/dev/null || die 'ssh is required locally'
git rev-parse --show-toplevel >/dev/null 2>&1 || die 'run from inside the Git repository'
repo_root="$(git rev-parse --show-toplevel)"
cd "$repo_root"
ref="${DEPLOY_REF:-$(git branch --show-current)}"
[[ -n "$ref" ]] || die 'detached HEAD: set DEPLOY_REF to a branch name'
git check-ref-format --branch "$ref" >/dev/null 2>&1 || die 'DEPLOY_REF is not a valid branch name'
if [[ -n "$(git status --porcelain --untracked-files=normal)" ]]; then
die 'working tree has changes; commit and push the intended release first'
fi
remote_url="$(git remote get-url origin 2>/dev/null)" || die 'Git remote origin is not configured'
remote_sha="$(git ls-remote --heads "$remote_url" "refs/heads/$ref" | awk 'NR == 1 {print $1}')"
[[ "$remote_sha" =~ ^[0-9a-f]{40,64}$ ]] || die "branch '$ref' was not found on origin"
local_sha="$(git rev-parse HEAD)"
[[ "$local_sha" == "$remote_sha" ]] || die 'HEAD must exactly match the commit currently pushed to origin'
port="${DEPLOY_PORT:-2101}"
[[ "$port" =~ ^[0-9]{1,5}$ ]] && (( port > 0 && port < 65536 )) || die 'DEPLOY_PORT must be between 1 and 65535'
[[ "$DEPLOY_HOST" != *$'\n'* && "$DEPLOY_USER" != *$'\n'* && "$DEPLOY_DOMAIN" != *$'\n'* ]] || die 'deployment values cannot contain newlines'
[[ "$DEPLOY_HOST" =~ ^[a-zA-Z0-9._:-]+$ ]] || die 'DEPLOY_HOST must be a hostname or IP address'
[[ "$DEPLOY_USER" =~ ^[a-zA-Z0-9._-]+$ ]] || die 'DEPLOY_USER contains unsupported characters'
[[ "$DEPLOY_DOMAIN" =~ ^[a-zA-Z0-9.-]+$ ]] || die 'DEPLOY_DOMAIN must be a plain domain name'
[[ "$DEPLOY_ROOT" == /home/*/*/* ]] || die 'DEPLOY_ROOT must be a site directory below /home, e.g. /home/siteuser/htdocs/example.com'
[[ "$DEPLOY_ROOT" != *'..'* && "$DEPLOY_ROOT" != *$'\n'* ]] || die 'DEPLOY_ROOT contains an unsafe path component'
printf 'Target: %s@%s:%s domain=%s ref=%s commit=%s\n' \
"$DEPLOY_USER" "$DEPLOY_HOST" "$port" "$DEPLOY_DOMAIN" "$ref" "$remote_sha"
if [[ "${DEPLOY_DRY_RUN:-0}" == 1 ]]; then
printf 'Dry run: local Git and target settings are valid; no SSH connection was made.\n'
exit 0
fi
ssh -p "$port" \
-o BatchMode=yes \
-o ConnectTimeout=10 \
-o StrictHostKeyChecking=yes \
"$DEPLOY_USER@$DEPLOY_HOST" \
bash -s -- "$remote_url" "$ref" "$remote_sha" "$DEPLOY_ROOT" "$DEPLOY_DOMAIN" <<'REMOTE_SCRIPT'
set -euo pipefail
die() { printf 'remote deploy: %s\n' "$*" >&2; exit 1; }
repo_url="$1"
ref="$2"
expected_sha="$3"
root="$4"
domain="$5"
[[ "$root" == /home/*/*/* && "$root" != *'..'* ]] || die 'unsafe deployment root'
[[ "$expected_sha" =~ ^[0-9a-f]{40,64}$ ]] || die 'invalid commit hash'
[[ "$domain" =~ ^[a-zA-Z0-9.-]+$ ]] || die 'invalid domain'
deploy_dir="$root/.deploy"
repo_dir="$deploy_dir/repository.git"
releases_dir="$root/releases"
shared_dir="$root/shared"
release_dir="$releases_dir/$expected_sha"
mkdir -p "$deploy_dir" "$releases_dir" "$shared_dir"
chmod 700 "$deploy_dir" "$shared_dir"
[[ -f "$shared_dir/.env" ]] || die "missing $shared_dir/.env; create it privately before the first deploy"
chmod 600 "$shared_dir/.env"
if [[ ! -d "$repo_dir" ]]; then
git init --bare --quiet "$repo_dir"
git --git-dir="$repo_dir" remote add origin "$repo_url"
fi
git --git-dir="$repo_dir" fetch --quiet --no-tags origin \
"+refs/heads/$ref:refs/remotes/origin/$ref"
actual_sha="$(git --git-dir="$repo_dir" rev-parse "refs/remotes/origin/$ref")"
[[ "$actual_sha" == "$expected_sha" ]] || die 'origin moved during deploy; rerun using the new pushed commit'
[[ ! -e "$release_dir" ]] || die "release already exists: $release_dir"
mkdir "$release_dir"
git --git-dir="$repo_dir" archive "$actual_sha" | tar -x -C "$release_dir"
[[ -d "$release_dir/public" ]] || die 'release has no public/ web root; configure the app layout before deployment'
if command -v php >/dev/null 2>&1 && [[ -d "$release_dir/backend" ]]; then
while IFS= read -r -d '' php_file; do
php -l "$php_file" >/dev/null || die "PHP syntax check failed: $php_file"
done < <(find "$release_dir/backend" -type f -name '*.php' -print0)
fi
printf '%s\n' "$domain" > "$shared_dir/.site-domain"
ln -s "$release_dir" "$root/current.next"
mv -Tf "$root/current.next" "$root/current"
printf 'Published %s to %s\n' "$actual_sha" "$root/current"
printf 'CloudPanel document root must point to: %s/current/public\n' "$root"
REMOTE_SCRIPT