Ghaymah intern

This commit is contained in:
2026-07-28 14:17:17 +03:00
commit 9a49823ced
218 changed files with 29174 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
name: CI/CD - Build, Push to Ghaymah Registry, Deploy
on:
push:
branches:
- develop # staging
- main # production
workflow_dispatch: {}
env:
GHAYMAH_REGISTRY: registry.ghaymah.systems
IMAGE_NAME: motazelalfy/ghyma-cicd-demo
jobs:
build-and-push:
name: Build & Push Docker Image
runs-on: ubuntu-latest
outputs:
image_tag: ${{ steps.vars.outputs.image_tag }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set image tag (short commit SHA)
id: vars
run: echo "image_tag=$(echo $GITHUB_SHA | cut -c1-7)" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Ghaymah Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.GHAYMAH_REGISTRY }}
username: ${{ secrets.GHAYMAH_REGISTRY_USERNAME }}
password: ${{ secrets.GHAYMAH_REGISTRY_TOKEN }}
- name: Build and push image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
${{ env.GHAYMAH_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.image_tag }}
${{ env.GHAYMAH_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
deploy-staging:
name: Deploy to Staging
needs: build-and-push
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging-ghyma-demo.ghaymah.systems
steps:
- name: Install Ghaymah CLI
run: |
curl -fsSL https://cli.ghaymah.systems/install.sh | sh
ghaymah --version
- name: Authenticate with Ghaymah
run: ghaymah login --token "${{ secrets.GHAYMAH_API_TOKEN }}"
- name: Deploy image to staging
run: |
ghaymah deploy \
--app ghyma-cicd-demo-staging \
--image ${{ env.GHAYMAH_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build-and-push.outputs.image_tag }} \
--env staging
deploy-production:
name: Deploy to Production (manual approval required)
needs: build-and-push
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment:
name: production
url: https://ghyma-demo.ghaymah.systems
steps:
- name: Install Ghaymah CLI
run: |
curl -fsSL https://cli.ghaymah.systems/install.sh | sh
ghaymah --version
- name: Authenticate with Ghaymah
run: ghaymah login --token "${{ secrets.GHAYMAH_API_TOKEN }}"
- name: Deploy image to production
run: |
ghaymah deploy \
--app ghyma-cicd-demo-production \
--image ${{ env.GHAYMAH_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build-and-push.outputs.image_tag }} \
--env production
+10
View File
@@ -0,0 +1,10 @@
node_modules/
npm-debug.log*
.env
.env.local
*.log
.DS_Store
Thumbs.db
+17
View File
@@ -0,0 +1,17 @@
FROM node:20-alpine
WORKDIR /usr/src/app
COPY package.json ./
RUN npm install --omit=dev
COPY server.js ./
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "server.js"]
+210
View File
@@ -0,0 +1,210 @@
# Ghyma CI/CD Demo — الدليل الصحيح المبني على توثيق Ghaymah الرسمي
> ⚠️ تحديث مهم: النسخة دي معدّلة بناءً على توثيق رسمي حقيقي من Ghaymah. المنصة **لسه معندهاش CLI** ("coming soon, inshallah") **ولا auto-trigger** عند push. النشر الفعلي **يدوي بالكامل** من لوحة التحكم.
---
## إيه اللي الـ pipeline بيعمله فعليًا (وإيه اللي لسه يدوي)
| المرحلة | مؤتمتة؟ |
|---|---|
| بناء صورة Docker | ✅ أوتوماتيك (GitHub Actions) |
| فحص سريع (smoke test) على `/health` | ✅ أوتوماتيك |
| موافقة يدوية قبل production | ✅ أوتوماتيك (عن طريق GitHub Environments) |
| **النشر الفعلي على Ghaymah** | ❌ **يدوي** — تدخل لوحة التحكم وتحط Commit SHA بنفسك |
يعني الـ workflow مش هينشر لوحده — هو هيبني الصورة، يتأكد إنها شغالة، ويطبعلك بالظبط "انسخ الـ SHA ده وحطه في تطبيق كذا". لما Ghaymah تطلق الـ CLI بتاعها، هنضيف خطوة أخيرة تلقائية بدل النسخ اليدوي.
---
## الخطوة 0: المتطلبات
- Docker Desktop
- حساب GitHub
- حساب Docker Hub
- حساب على Ghaymah Cloud
---
## الخطوة 1: التجربة المحلية
```
npm install
npm start
```
افتح `http://localhost:3000/` و `/health` و `/version`.
## الخطوة 2: التجربة بالـ Docker
```
docker build -t ghyma-cicd-demo:local .
docker run -d -p 3000:3000 --name ghyma-demo ghyma-cicd-demo:local
curl http://localhost:3000/health
docker stop ghyma-demo && docker rm ghyma-demo
```
## الخطوة 3: رفع المشروع على GitHub
```
git init
git add .
git commit -m "Initial commit"
git branch -M main
git remote add origin https://github.com/Moetaz-Alfy/ghyma-cicd-demo.git
git push -u origin main
git checkout -b develop
git push -u origin develop
```
## الخطوة 4: أضف Secrets للـ Registry في GitHub
> هذه الخطوة تحقق شرط "الرفع لـ Registry" في متطلبات المهمة حرفيًا. Docker Hub هنا بيشتغل كبديل مؤقت لحد ما Ghaymah تطلق Container Registry خاص بيها — التفاصيل في قسم "ملاحظة عن خطوة الـ Registry" آخر الملف.
```
Settings → Secrets and variables → Actions → New repository secret
```
| الاسم | القيمة |
|---|---|
| `DOCKERHUB_USERNAME` | اسم مستخدمك على Docker Hub |
| `DOCKERHUB_TOKEN` | Access Token من Docker Hub (Account Settings → Security → New Access Token) |
## الخطوة 5: فعّل بيئة production بموافقة يدوية
```
Settings → Environments → New environment → production
```
فعّل **Required reviewers** واختار نفسك.
### شرح آلية عمل الموافقة اليدوية بالتفصيل
الـ job اسمه `notify-production` في `ci-cd.yml` مربوط بالسطر ده:
```yaml
environment:
name: production
```
لما GitHub تلاقي job مربوط بـ Environment اسمه `production` وعليه Required reviewers مفعّلة، بيحصل الآتي تلقائيًا:
1. الـ job **بيوصل لكل الخطوات اللي قبله عادي** (بناء الصورة، الـ smoke test، الرفع للـ Registry) — دي مش متأثرة بالموافقة خالص
2. لما الدور يجي على `notify-production` نفسه، GitHub **بيوقفه فورًا** قبل أول خطوة جواه، وبيحطه في حالة **"Waiting"**
3. المراجع (Reviewer) المحدد بياخد إشعار على GitHub (وبالإيميل لو مفعّل)، ويشوف في تاب Actions زرار **"Review deployments"**
4. المراجع بيقدر يشوف تفاصيل الـ commit والـ diff قبل ما يوافق
5. لو ضغط **Approve and deploy** → الخطوات جوه الـ job تشتغل فعليًا
6. لو ضغط **Reject** → الـ job بيتوقف نهائيًا وميطبعش أي تعليمات نشر
**ليه الميزة دي مهمة تحديدًا هنا؟** لأن Ghaymah نفسها معندهاش نظام موافقات داخلي (النشر عندها مجرد حقل تكتب فيه SHA وتضغط Save، من غير أي حماية). فالموافقة اليدوية دي بتتم بالكامل على مستوى GitHub **قبل** ما تصل أصلاً لمرحلة "انسخ الـ SHA والصقه في Ghaymah" — يعني حتى لو حد عنده وصول للوحة تحكم Ghaymah، هو مش هياخد SHA صحيح للنشر إلا بعد ما تتم الموافقة على GitHub الأول.
**فرق مهم عن staging:** لاحظ إن job الـ `notify-staging` **مالهوش** سطر `environment: name`، فهو بيشتغل فورًا من غير أي وقفة — ده الفرق الجوهري بين البيئتين في نظام الأتمتة نفسه.
## الخطوة 6: أنشئ المشروع والتطبيقين على Ghaymah (مرة واحدة فقط)
1. سجّل دخول على لوحة تحكم Ghaymah
2. أنشئ **Project** جديد (مثلاً `ghyma-cicd-demo`)
3. جوه المشروع، اعمل **Deploy Application** الأول:
- Deployment method: **Git-Based Deployment → Deploying from Public GitHub**
- Application Name: `ghyma-demo-prod`
- Repository URL: رابط الريبو بتاعك
- Branch/Commit SHA: أول commit على `main`
- Port: `3000`
- Environment Variables: `APP_ENV=production`
4. اعمل **Deploy Application** ثاني بنفس الريبو:
- Application Name: `ghyma-demo-staging`
- Repository URL: نفس الريبو
- Branch/Commit SHA: أول commit على `develop`
- Port: `3000`
- Environment Variables: `APP_ENV=staging`
الآن عندك تطبيقين منفصلين تمامًا (نفس الكود، بيانات وبيئة مختلفة).
## الخطوة 7: التجربة الكاملة للـ pipeline
```
git checkout develop
git commit --allow-empty -m "test staging pipeline"
git push
```
روح لتاب **Actions** → هتلاقي `build-and-push` ثم `notify-staging` بيطبع الـ commit SHA.
انسخ الـ SHA ده، روح للوحة تحكم Ghaymah → طبّق التطبيق `ghyma-demo-staging` → الصق الـ SHA في حقل Branch/Commit → Save.
## الخطوة 8: نشر production (بالموافقة)
```
git checkout main
git merge develop
git push
```
في تاب Actions، `notify-production` هيقف وينتظر موافقتك (**Review deployments → Approve and deploy**). بعد الموافقة، هيطبعلك الـ SHA.
انسخه، وحطه في تطبيق `ghyma-demo-prod` على لوحة تحكم Ghaymah → Save.
---
## الفرق بين staging و production على Ghaymah تحديدًا
| المحور | Staging | Production |
|---|---|---|
| اسم التطبيق في Ghaymah | `ghyma-demo-staging` | `ghyma-demo-prod` |
| الفرع المتابَع | `develop` | `main` |
| موافقة قبل تحديث الـ SHA | لا (الأمر يطبع فورًا) | نعم (GitHub Environment) |
| متغير `APP_ENV` | `staging` | `production` |
| من يحدّث SHA فعليًا | إنت، يدويًا، بعد كل push على develop | إنت، يدويًا، بعد الموافقة فقط |
### ليه فصلناهم كتطبيقين منفصلين تمامًا بدل بيئة واحدة بمتغيرات مختلفة؟
في منصات تانية (زي Heroku أو AWS) ممكن "البيئة" تكون مجرد إعداد داخل نفس التطبيق. لكن بما إن Ghaymah بتنشر مباشرة من الكود في GitHub (Git-Based Deployment)، كل "Application" في لوحة تحكمها هي فعليًا:
- نسخة مبنية بشكل مستقل من commit معين
- عندها رابط عام خاص بيها (Public URL) منفصل تمامًا
- عندها Environment Variables خاصة بيها
- بتنشر وتنطفي بشكل مستقل عن أي تطبيق تاني
فبالتالي أنسب طريقة لعمل "بيئتين" على Ghaymah هي إنشاء **تطبيقين منفصلين فعليًا**، مش تطبيق واحد بيتغيّر سلوكه — وده اللي بيضمن إن أي تجربة أو فشل في staging **مستحيل** يأثر على production، لأنهم فعليًا حاويتين مختلفتين تمامًا وليهم رابط مختلف.
### الفرق العملي وقت الاستخدام اليومي
- **تعمل تعديل وعايز تجربه بسرعة؟** ادفع على `develop` → SHA بيتطبع فورًا → تحطه في `ghyma-demo-staging` → تجربه على رابط staging المنفصل من غير أي انتظار
- **متأكد إن التعديل شغال ومستعد ينزل للمستخدمين؟** ادمج (merge) `develop` في `main` → لازم موافقة يدوية قبل ما تاخد الـ SHA أصلاً → تحطه في `ghyma-demo-prod`
القاعدة العامة: **أي كود بيوصل لـ production لازم يكون مر فعليًا من staging الأول بنفس الآلية بالظبط** — نفس الـ Dockerfile، نفس خطوات الـ CI، الفرق الوحيد هو أي بيئة بتستقبل الكود ومين بيوافق قبلها.
---
## الربط مع Ghaymah CLI — الوضع الحقيقي
Ghaymah **لا توفر CLI في الوقت الحالي**. النص الرسمي:
> "Ghaymah Cloud currently does not have automatic build triggers based on Git commits. Deployments must be manually triggered, or integrated with a CI/CD platform using our CLI (coming soon, inshallah)."
يعني:
- مفيش أوامر `ghaymah login` أو `ghaymah deploy` تقدر تستخدمها دلوقتي
- الطريقة الوحيدة المتاحة حاليًا هي **Git-Based Deployment** من لوحة التحكم مباشرة، مع تحديث يدوي لحقل الـ Commit SHA/Tag لكل تطبيق
- لما الـ CLI يتطلق، هيبقى ممكن نستبدل خطوة `notify-staging`/`notify-production` (اللي بتطبع تعليمات بس) بخطوة فعلية زي `ghaymah deploy --app ghyma-demo-prod --sha <SHA>` تلقائيًا
### إزاي الـ workflow اتصمم عشان "يربط" مع Ghaymah رغم غياب الـ CLI؟
بما إن مفيش API أو CLI نقدر نناديه، الـ workflow بيعمل أقصى حاجة ممكنة أوتوماتيكيًا وبيسيب الجزء المستحيل أتمتته (تحديث حقل الـ SHA في واجهة Ghaymah نفسها) كخطوة يدوية واحدة بس، موثقة بدقة:
```yaml
- name: Print deployment instructions
run: |
echo "Ghaymah dashboard -> Application: mywebapp-prod"
echo "Set Branch/Commit SHA field to: ${{ needs.build-and-push.outputs.commit_sha }}"
```
الخطوة دي مش مجرد رسالة عشوائية — هي **بديل مؤقت** لأمر CLI مستقبلي. لاحظ إن اسم التطبيق والـ SHA بييجوا من متغيرات ديناميكية (`needs.build-and-push.outputs.commit_sha`)، يعني لو غيرت اسم التطبيق أو استخدمت فروع تانية، الرسالة بتتحدث تلقائيًا من غير أي تعديل يدوي في الكود.
### خطة الترقية لما الـ CLI يطلق فعليًا
| دلوقتي (يدوي) | بعد إطلاق CLI (أوتوماتيكي) |
|---|---|
| نسخ الـ SHA من طباعة الـ workflow | استدعاء `ghaymah login --token $TOKEN` |
| فتح لوحة تحكم Ghaymah يدويًا | استدعاء `ghaymah deploy --app <name> --sha <SHA>` مباشرة داخل الـ job |
| الضغط يدويًا على Save | الأمر نفسه بيرجع exit code يوضح نجاح/فشل النشر فورًا داخل GitHub Actions |
التصميم الحالي مبني عمدًا بحيث التحويل يوم ما الـ CLI يتوفر يكون **تغيير بسيط** (استبدال خطوة `Print deployment instructions` بخطوة `ghaymah deploy` فعلية) من غير أي إعادة هيكلة للـ pipeline بالكامل.
## أفضل ممارسات لحد ما الـ CLI يتوفر
- ثابت على تسمية واضحة للتطبيقات (`-prod`, `-staging`) لتفادي الغلط وقت تحديث الـ SHA يدويًا
- استخدم متغيرات بيئة مختلفة تمامًا بين البيئتين (خصوصًا أي مفاتيح API أو روابط قواعد بيانات)
- وثّق كل SHA اتنشر فعليًا على production (تقدر تستخدم GitHub Releases أو Tags لده)
---
## ملاحظة عن خطوة الـ Registry (شفافية كاملة)
متطلبات المهمة الأصلية بتطلب "رفع الصورة إلى ghaymah Container Registry". حاليًا Ghaymah **لا توفر Container Registry عام** — النشر بيتم عن طريق Git-Based Deployment فقط (البند اللي فوق). خطوة الـ `docker push` في الـ workflow بترفع فعليًا على **Docker Hub** كبديل مؤقت، عشان:
1. تستوفي شرط "بناء ورفع الصورة لريجستري" في المهمة حرفيًا
2. تدي نسخة موثقة ومؤرشفة من كل صورة (بالـ commit SHA) تقدر ترجعلها وقت الحاجة
3. تبقى جاهزة تتحول لـ Ghaymah Registry الحقيقي بمجرد ما يتوفر (تغيير قيمة `REGISTRY` في الـ workflow بس)
النشر الفعلي على Ghaymah **مش معتمد على الخطوة دي** — بيعتمد فقط على تحديث الـ Commit SHA في لوحة التحكم زي ما هو موضح في الخطوتين 7 و 8.
+760
View File
@@ -0,0 +1,760 @@
{
"name": "ghyma-cicd-demo",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ghyma-cicd-demo",
"version": "1.0.0",
"dependencies": {
"express": "^4.19.2"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
"node_modules/body-parser": {
"version": "1.20.6",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz",
"integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==",
"dependencies": {
"bytes": "~3.1.2",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "~1.2.0",
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"on-finished": "~2.4.1",
"qs": "~6.15.1",
"raw-body": "~2.5.3",
"type-is": "~1.6.18",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.22.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
"integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "~1.20.5",
"content-disposition": "~0.5.4",
"content-type": "~1.0.4",
"cookie": "~0.7.1",
"cookie-signature": "~1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.3.1",
"fresh": "~0.5.2",
"http-errors": "~2.0.0",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "~2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "~0.1.12",
"proxy-addr": "~2.0.7",
"qs": "~6.15.1",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "~0.19.0",
"serve-static": "~1.16.2",
"setprototypeof": "1.2.0",
"statuses": "~2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/finalhandler": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "~2.4.1",
"parseurl": "~1.3.3",
"statuses": "~2.0.2",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"dependencies": {
"depd": "~2.0.0",
"inherits": "~2.0.4",
"setprototypeof": "~1.2.0",
"statuses": "~2.0.2",
"toidentifier": "~1.0.1"
},
"engines": {
"node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.15.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
"integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
"dependencies": {
"es-define-property": "^1.0.1",
"side-channel": "^1.1.1"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
"dependencies": {
"bytes": "~3.1.2",
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/send": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
"integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "~0.5.2",
"http-errors": "~2.0.1",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "~2.4.1",
"range-parser": "~1.2.1",
"statuses": "~2.0.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/serve-static": {
"version": "1.16.3",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
"integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
"dependencies": {
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "~0.19.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
"node_modules/side-channel": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
"integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.4",
"side-channel-list": "^1.0.1",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.4"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"engines": {
"node": ">= 0.8"
}
}
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"name": "ghyma-cicd-demo",
"version": "1.0.0",
"description": "Small demo app for practicing the Ghaymah CI/CD pipeline",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.19.2"
}
}
+37
View File
@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ghyma CI/CD Demo</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="card">
<h1>Ghyma CI/CD Demo</h1>
<p class="subtitle">تطبيق تجريبي لتطبيق الـ CI/CD Pipeline عليه</p>
<div class="badge" id="envBadge">جاري التحميل...</div>
<div class="links">
<a href="/health" target="_blank">/health</a>
<a href="/version" target="_blank">/version</a>
</div>
</div>
<script>
fetch('/version')
.then(res => res.json())
.then(data => {
const badge = document.getElementById('envBadge');
badge.textContent = 'البيئة الحالية: ' + data.env;
badge.classList.add(data.env === 'production' ? 'prod' : 'staging');
})
.catch(() => {
document.getElementById('envBadge').textContent = 'تعذّر جلب بيانات البيئة';
});
</script>
</body>
</html>
+78
View File
@@ -0,0 +1,78 @@
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Segoe UI', Tahoma, Arial, sans-serif;
background: linear-gradient(135deg, #1e2530, #0f1420);
color: #e7ecf5;
}
.card {
background: #171f30;
border: 1px solid #2a3448;
border-radius: 16px;
padding: 40px;
text-align: center;
max-width: 420px;
width: 90%;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
}
h1 {
margin: 0 0 8px;
font-size: 24px;
}
.subtitle {
margin: 0 0 24px;
color: #8b96ab;
font-size: 14px;
}
.badge {
display: inline-block;
padding: 10px 20px;
border-radius: 999px;
font-size: 14px;
font-weight: 600;
background: #2a3448;
margin-bottom: 24px;
}
.badge.staging {
background: #4f8cff33;
color: #4f8cff;
border: 1px solid #4f8cff;
}
.badge.prod {
background: #2ecc7133;
color: #2ecc71;
border: 1px solid #2ecc71;
}
.links {
display: flex;
gap: 12px;
justify-content: center;
}
.links a {
color: #4f8cff;
text-decoration: none;
font-size: 14px;
padding: 8px 14px;
border: 1px solid #2a3448;
border-radius: 8px;
transition: background 0.2s;
}
.links a:hover {
background: #2a3448;
}
+35
View File
@@ -0,0 +1,35 @@
const express = require('express');
const path = require('path');
const app = express();
const PORT = process.env.PORT || 3000;
const APP_ENV = process.env.APP_ENV || 'local';
const START_TIME = Date.now();
let requestCount = 0;
app.use((req, res, next) => {
requestCount += 1;
next();
});
// Serve the landing page (public/index.html + style.css)
app.use(express.static(path.join(__dirname, 'public')));
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok' });
});
app.get('/version', (req, res) => {
res.status(200).json({
env: APP_ENV,
uptime_seconds: Math.floor((Date.now() - START_TIME) / 1000),
request_count: requestCount,
commit: process.env.GIT_COMMIT || 'unknown',
});
});
app.listen(PORT, '0.0.0.0', () => {
console.log(`[${APP_ENV}] listening on port ${PORT}`);
});