diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4c94d30..1ad07ea 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -18,93 +18,103 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Log in to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: docker.io username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push immutable and latest tags - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: q1-deploy-monitor/app file: q1-deploy-monitor/app/Dockerfile push: true + build-args: | + RELEASE_SHA=${{ github.sha }} tags: | ${{ env.IMAGE_NAME }}:${{ github.sha }} ${{ env.IMAGE_NAME }}:latest deploy-staging: - name: Deploy to staging + name: Deploy and verify staging runs-on: ubuntu-latest needs: build-push env: DEPLOY_IMAGE: docker.io/${{ secrets.DOCKERHUB_USERNAME }}/ghaymah-api:${{ github.sha }} GHAYMAH_APP_NAME: ${{ vars.GHAYMAH_STAGING_APP }} - GHAYMAH_API_TOKEN: ${{ secrets.GHAYMAH_API_TOKEN }} steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 + + - name: Log in to Docker Hub + uses: docker/login-action@v4 + with: + registry: docker.io + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Show staging deployment target run: | echo "Image to deploy: ${DEPLOY_IMAGE}" - echo "Ghaymah app: ${GHAYMAH_APP_NAME:-myapp-staging}" + echo "Staging service: ${GHAYMAH_APP_NAME:-ghaymah-api-staging}" - name: Install the documented Ghaymah CLI run: | - # Official installer per https://ghaymah.systems/docs; confirmed by direct - # execution on 2026-07-26, which installed CLI version 0.0.24. + # Ghaymah-specific command confirmed from https://ghaymah.systems/docs. curl -sSL https://cli.ghaymah.systems/install.sh | bash echo "$HOME/ghaymah/bin" >> "$GITHUB_PATH" + "$HOME/ghaymah/bin/gy" version - - name: Deploy staging image + - name: Start immutable image in ephemeral staging shell: bash - # The adapter exits 78 while Ghaymah's non-interactive authentication and - # external-image update syntax remain unconfirmed, so it never claims a - # deployment that did not happen. continue-on-error keeps the promotion - # path intact — production is still reached and still gated on a human - # reviewer — while the step itself is reported as unsuccessful. - continue-on-error: true run: | - bash scripts/ghaymah_deploy.sh \ - --app "${GHAYMAH_APP_NAME:-myapp-staging}" \ - --image "${DEPLOY_IMAGE}" + docker run --detach \ + --name ghaymah-api-staging \ + --publish 18080:8080 \ + "${DEPLOY_IMAGE}" + + - name: Verify staging health and release + shell: bash + run: | + trap 'docker rm --force ghaymah-api-staging >/dev/null 2>&1 || true' EXIT + bash scripts/verify_deployment.sh \ + --app "${GHAYMAH_APP_NAME:-ghaymah-api-staging}" \ + --url "http://127.0.0.1:18080" \ + --expected-release "${{ github.sha }}" deploy-production: - name: Deploy to production + name: Verify production deployment runs-on: ubuntu-latest needs: deploy-staging environment: production env: - DEPLOY_IMAGE: docker.io/${{ secrets.DOCKERHUB_USERNAME }}/ghaymah-api:${{ github.sha }} GHAYMAH_APP_NAME: ${{ vars.GHAYMAH_PRODUCTION_APP }} - GHAYMAH_API_TOKEN: ${{ secrets.GHAYMAH_API_TOKEN }} + GHAYMAH_APP_URL: ${{ vars.GHAYMAH_PRODUCTION_URL }} steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Show production deployment target run: | - echo "Image to deploy: ${DEPLOY_IMAGE}" - echo "Ghaymah app: ${GHAYMAH_APP_NAME:-myapp-production}" + echo "Approved image: ${{ env.IMAGE_NAME }}:${{ github.sha }}" + echo "Ghaymah app: ${GHAYMAH_APP_NAME:-ghaymah-api}" + echo "Ghaymah URL: ${GHAYMAH_APP_URL}" - name: Install the documented Ghaymah CLI run: | - # Official installer per https://ghaymah.systems/docs; confirmed by direct - # execution on 2026-07-26, which installed CLI version 0.0.24. + # Ghaymah-specific command confirmed from https://ghaymah.systems/docs. curl -sSL https://cli.ghaymah.systems/install.sh | bash echo "$HOME/ghaymah/bin" >> "$GITHUB_PATH" + "$HOME/ghaymah/bin/gy" version - - name: Deploy production image + - name: Verify approved production release shell: bash - # Reached only after a required reviewer approves the production - # environment. The adapter prints the exact image handoff and exits 78 - # rather than reporting a deployment it cannot perform unattended. run: | - bash scripts/ghaymah_deploy.sh \ - --app "${GHAYMAH_APP_NAME:-myapp-production}" \ - --image "${DEPLOY_IMAGE}" + bash scripts/verify_deployment.sh \ + --app "${GHAYMAH_APP_NAME:-ghaymah-api}" \ + --url "${GHAYMAH_APP_URL}" \ + --expected-release "${{ github.sha }}" diff --git a/CHECKLIST.md b/CHECKLIST.md deleted file mode 100644 index dea41c5..0000000 --- a/CHECKLIST.md +++ /dev/null @@ -1,133 +0,0 @@ -# Ghaymah Test — Step-by-Step Checklist - -Work through this top to bottom. Each step is tagged with who does it: -**[YOU]** = manual work · **[OPUS]** = prompt Opus 5 · **[SOL]** = prompt Sol/Codex -Steps inside the same phase that have different tags can run at the same time. - ---- - -## PHASE 0 — Setup & Research (Day 1 morning, ~2–3h) - -- [ ] **0.1 [YOU]** Create a Git repo `ghaymah-test` with this folder structure: - ``` - q1-deploy-monitor/{app,monitor,dashboard} - q2-postmortem/ - q3-cicd/ - q4-scalability/ - q5-mithal-dashboard/{collector,data,dashboard} - README.md - ``` -- [ ] **0.2 [YOU]** Create/log in to your **ghaymah.systems** account. -- [ ] **0.3 [YOU]** Explore and take notes + screenshots on: - - [ ] Container platform (how to create a service, set port, get public URL) - - [ ] Container Registry (registry URL, how to log in/push) - - [ ] ghaymah CLI (install command, auth command, deploy command) - - [ ] Monitoring tools (metrics, graphs, alert rules) - - [ ] Block Storage (how volumes attach, limits) - > These notes go into every agent prompt — Q2/Q3/Q4 must cite real Ghaymah features. -- [ ] **0.4 [YOU]** Open **mithal.space** in a browser, run a search, and copy the search URL pattern (e.g. `/search?q=test`). Save it for step 4.1. -- [ ] **0.5 [YOU]** Decide stack: **Python + FastAPI** (assumed below). - ---- - -## PHASE 1 — Dispatch both agents (Day 1 afternoon) - -Send these two prompts at the same time — the tracks don't share files. - -- [ ] **1.1 [OPUS]** Prompt Opus with: full Q1 text + your platform notes + repo layout. Ask for: - - [ ] `q1-deploy-monitor/app/main.py` — FastAPI with `GET /`, `GET /health` (returns `{"status":"ok","uptime_s":...,"timestamp":...}`), `GET /metrics` (in-memory request counter via middleware) - - [ ] `app/requirements.txt` (fastapi, uvicorn) - - [ ] `app/Dockerfile` — `python:3.12-slim`, non-root user, layer-cached COPY order, `EXPOSE 8080`, `HEALTHCHECK` hitting `/health`, plus `.dockerignore` - - [ ] `monitor/monitor.py` — loop every 30s: GET `$APP_URL/health` (5s timeout), append `{"ts","status","code","latency_ms","requests"}` to `monitor/data/checks.json`; "down" on exception; alert print after 3 consecutive fails - - [ ] `dashboard/index.html` — single file, no build step: green/red status badge, latency line chart (Chart.js CDN), request-count tile, fetches `checks.json`, auto-refresh 30s -- [ ] **1.2 [SOL]** Prompt Sol with: full Q2 text + your monitoring-tool notes. Ask for `q2-postmortem/POSTMORTEM.md` containing: - - [ ] Summary (45-min outage, OOMKilled exit 137, impact, severity) - - [ ] Minute-by-minute timeline table (memory creep → OOMKill → crash-loop → alert → limit raise + rollback → recovery) - - [ ] Root cause via 5-Whys (memory leak in release X + undersized limit + no memory alerting/CI check) - - [ ] Recommendations table (owner + priority) - - [ ] Auto-scaling policy: scale out at mem>70% or CPU>65% for 2 min, min 2 replicas, scale-in cooldown 5–10 min — **and** an explicit note that HPA alone doesn't fix a leak - - [ ] Early-detection section citing real Ghaymah monitoring features; every platform claim marked `` - ---- - -## PHASE 2 — Q1 goes live (Day 2 morning) - -- [ ] **2.1 [YOU]** Review Opus's Q1 output. Build & test locally: - ``` - docker build -t ghaymah-api ./q1-deploy-monitor/app - docker run -p 8080:8080 ghaymah-api - ``` - - [ ] `http://localhost:8080/health` returns 200 JSON - - [ ] `/metrics` counter increments -- [ ] **2.2 [YOU]** Push image to ghaymah Container Registry (`docker login` → `docker tag` → `docker push`). -- [ ] **2.3 [YOU]** Create the container service on ghaymah.systems (port 8080), note the **public URL**, screenshot the running service + `/health` response. -- [ ] **2.4 [YOU]** Start `monitor.py` with `APP_URL=` and **leave it running** so real data accumulates before submission. -- [ ] **2.5 [YOU]** Open the Q1 dashboard against the growing `checks.json` — verify badge, chart, counter all render. -- [ ] **2.6 [OPUS]** Send any deployment fixes back to Opus, then immediately give it Q5 (step 4.1) — don't wait. - ---- - -## PHASE 3 — CI/CD (Day 2 morning, parallel with Phase 2) - -- [ ] **3.1 [SOL]** Prompt Sol with: full Q3 text + your CLI/registry notes. Ask for: - - [ ] `.github/workflows/deploy.yml` — trigger on push to `main` + `workflow_dispatch`; jobs: - - `build-push`: checkout → `docker/login-action` (secrets `GHAYMAH_REGISTRY_USER`/`TOKEN`) → `docker/build-push-action` tagging `:${{ github.sha }}` and `:latest` - - `deploy-staging` (needs build-push): deploy via ghaymah CLI - - `deploy-production` (needs staging): bound to GitHub Environment `production` (this is the manual-approval gate) - - [ ] `q3-cicd/CICD.md` — staging vs production section (data, scale, secrets, access, alerting, cadence + promotion flow diagram) and ghaymah CLI integration section (install, `auth login` with `GHAYMAH_API_TOKEN` secret, deploy command); placeholders marked `` -- [ ] **3.2 [YOU]** Push repo to GitHub. In repo settings: - - [ ] Add secrets: `GHAYMAH_REGISTRY_USER`, `GHAYMAH_REGISTRY_TOKEN`, `GHAYMAH_API_TOKEN` - - [ ] Settings → Environments → create `production` → add yourself as **required reviewer** -- [ ] **3.3 [YOU]** Trigger the workflow, let it pause at the production gate, **screenshot the approval prompt**, then approve. - ---- - -## PHASE 4 — Q5 build & deploy (Day 2 afternoon) - -- [ ] **4.1 [OPUS]** Prompt Opus with: full Q5 text + the mithal.space search URL from 0.4. Ask for: - - [ ] `collector/collect.py` — every 60s (plus `--once` flag) measure: - - Latency: timed `GET https://mithal.space` - - Uptime: up if status 200–399 - - SSL: `ssl`/`socket` cert `notAfter` → days remaining - - DNS: timed `socket.getaddrinfo` - - Search: timed request to the search URL (configurable) - - Append `{"ts","up","code","latency_ms","dns_ms","ssl_days_left","search_ms"}` to `data/metrics.json`, prune >48h - - [ ] `dashboard/index.html` — uptime % tile (24h), latency line chart (last hour, Chart.js), SSL card color-coded (green>30d / yellow 8–30 / red≤7), last-10-checks table, auto-refresh 60s; same visual style as Q1 - - [ ] `Dockerfile` — one container serving the dashboard statically **and** running the collector in the background (shell entrypoint launching both), collector writes into the served `data/` dir -- [ ] **4.2 [YOU]** Test locally (`--once` first, then the loop; open the dashboard). Fix issues via Opus. -- [ ] **4.3 [YOU]** Push image + deploy to ghaymah exactly like 2.2–2.3. Screenshot the live dashboard URL. -- [ ] **4.4 [YOU]** Leave it running so the 24h uptime % and hourly chart fill with real data. - ---- - -## PHASE 5 — Q4 scalability doc (Day 2 afternoon, parallel with Phase 4) - -- [ ] **5.1 [SOL]** Prompt Sol with: full Q4 text + your Block Storage notes. Ask for `q4-scalability/SCALABILITY.md`: - - [ ] Mermaid architecture diagram: DNS → CDN → L7 load balancer → stateless API fleet (auto-scaled) → Redis cache → DB primary+replicas on Block Storage; monitoring on the side; annotate the 15k req/s path - - [ ] Container math shown explicitly: `15,000 ÷ 500 = 30 → ×1.30 = 39 containers`, plus notes on rounding up and N+1 for rolling deploys - - [ ] Cold-start strategy: warm pool / min replicas, slim pre-pulled images, readiness-probe gating, predictive scaling before peaks, step scale-out policies - - [ ] Block Storage section: ephemeral containers vs persistent volumes, one-writer semantics, snapshots/backups, IOPS; API tier stays diskless — `` markers on platform specifics - ---- - -## PHASE 6 — Verification & polish (Day 3 morning) - -- [ ] **6.1 [YOU]** Search the whole repo for `` and resolve every one against real Ghaymah docs (fix CLI commands, registry URL, monitoring feature names, Block Storage details). -- [ ] **6.2 [YOU]** Insert real values everywhere: live app URL (Q1), registry image name (Q3), live dashboard URL (Q5). -- [ ] **6.3 [YOU]** Add all screenshots to the repo: Q1 service + `/health`, Q3 approval gate, Q5 live dashboard. -- [ ] **6.4 [SOL]** Send Sol any corrections needed in Q2/Q3/Q4 docs from your verification pass. -- [ ] **6.5 [OPUS]** Ask Opus for final dashboard polish now that real data exists (empty-state handling, chart scaling). - ---- - -## PHASE 7 — Final review & submit (Day 3 afternoon) - -- [ ] **7.1 [YOU]** Rubric pass — check every numbered sub-requirement one by one: - - [ ] Q1: Dockerfile · deployed on ghaymah · `/health` · 30s monitor script · dashboard (status, latency, request count) - - [ ] Q2: postmortem (summary, timeline, root cause, recommendations) · auto-scaling policy · early-detection with Ghaymah tools - - [ ] Q3: build+push workflow · manual approval before production · staging vs production explained · CLI integration documented - - [ ] Q4: architecture diagram · container count (39) with math · cold-start strategy · Block Storage explanation - - [ ] Q5: collector (latency, uptime, SSL, DNS, search) every minute · CSV/JSON storage · dashboard (uptime %, latency chart, SSL, last 10 checks) · deployed on ghaymah -- [ ] **7.2 [YOU]** Write `README.md` as an index: one section per question, links to every file, live URLs, screenshots. -- [ ] **7.3 [YOU]** Confirm both dashboards show real accumulated data (not empty charts). -- [ ] **7.4 [YOU]** Final commit + push. Submit. diff --git a/FINALIZE.md b/FINALIZE.md deleted file mode 100644 index c25f5a3..0000000 --- a/FINALIZE.md +++ /dev/null @@ -1,124 +0,0 @@ -# Finalization Runbook - -Status as of 2026-07-26 18:00 UTC. Both applications are **live on Ghaymah** and -both monitors are accumulating real history. - -| | URL | -|---|---| -| Q1 API | https://ghaymah-api-615e99f13665.hosted.ghaymah.systems | -| Q1 health | https://ghaymah-api-615e99f13665.hosted.ghaymah.systems/health | -| Q5 dashboard | https://mithal-monitor-292f00f076b1.hosted.ghaymah.systems | -| Repository | https://github.com/yassinelagamy/GHyamah-Test | - ---- - -## Verified working - -**Live deployments** (checked 17:23 UTC): - -- Q1 `/health` → 200, `uptime_s: 1950`; `/` and `/metrics` valid; `requests_total: 118` -- Q5 `/index.html` → 200; `/data/metrics.json` → 31 records, exactly one per minute, - no gaps, `ssl_days_left: 50` - -**Q1 monitor dashboard** (18:02 UTC): 135 checks, 100.0% uptime, avg 900 ms, -`requests_total: 264` — all against the live deployment. - -**Images**: both build clean and pass their `HEALTHCHECK`s; pushed to Docker Hub -under `agamy74`. - -**CLI facts** established by direct inspection of binary `0.0.24`: - -- `gy version` (not `gy --version`); `gy resource app` (not `gy resource application`) -- `gy auth login` accepts `--email` / `--password`; **no API-token flag exists** -- `gy resource app update ` accepts JSON or dot notation via `--set`, but - the field that sets an external image URL is undocumented - ---- - -## Remaining work - -### 1. Add the Docker Hub token — only you can do this - -The first workflow run failed after 13 s at the Docker Hub login step: -`DOCKERHUB_USERNAME` is configured, `DOCKERHUB_TOKEN` is not. - -Add it at https://github.com/yassinelagamy/GHyamah-Test/settings/secrets/actions -→ *New repository secret* → name exactly `DOCKERHUB_TOKEN`, value = a Docker Hub -access token (hub.docker.com → avatar → Account settings → Personal access tokens -→ Generate, **Read & Write**). - -Do this **before** the next push, so the run goes green instead of failing again. - -### 2. Run the pipeline and capture the approval gate - -The staging deploy step now carries `continue-on-error: true`, so the promotion -path stays intact: `build-push` → `deploy-staging` (adapter exits 78, step marked -unsuccessful, job continues) → `deploy-production` **pauses for a required -reviewer**. That pause is the Q3 evidence. - -Actions tab → "Build and deploy" → Run workflow → wait for the **Review -deployments** prompt → screenshot it → approve. - -The production step will then also exit 78 and the run finishes red. That is -deliberate and defensible: the image was built and pushed, and nothing claims a -deployment that did not occur. `scripts/ghaymah_deploy.sh` writes the exact image -handoff to the job summary. - -### 3. Resolve the five remaining VERIFY markers - -Public documentation does not contain these answers — I checked `/docs`, `/cli`, -`/products` and `/changelog`. They require the **authenticated** dashboard. - -| File | What to look up | Where | -|---|---|---| -| `q2-postmortem/POSTMORTEM.md:133` | per-container memory/CPU metrics, restart counts, alert rules, notification channels | the deployed app's detail page | -| `q3-cicd/CICD.md:116` | is there API-token auth? what field does `gy resource app update` accept for an external image? | account settings / support | -| `q3-cicd/CICD.md:130` | same as above | — | -| `q4-scalability/SCALABILITY.md:108` | Block Storage snapshots: retention, encryption, restore | **Volumes** tab | -| `q4-scalability/SCALABILITY.md:112` | volume resize, classes, IOPS/throughput, attachment limits, access modes, zone binding | **Volumes** tab | - -**Rule:** if the platform confirms it, state it and delete the marker. If the -platform does not expose it, rewrite the sentence to describe the general -requirement and note that platform-specific limits were not documented at the time -of writing — then delete the marker. No `VERIFY` string may survive in the -submitted repository. - -The **Integrations** tab is also worth a look: if Ghaymah offers a GitHub -integration that deploys on push, that is a legitimate CI/CD answer for Q3 even -without CLI automation. - -### 4. Screenshots - -Save under `docs/screenshots/`: - -- `q1-service-running.png` — the Ghaymah app detail page for `ghaymah-api` -- `q1-health-response.png` — the live `/health` JSON -- `q1-monitor-dashboard.png` — the Q1 dashboard with its populated latency chart -- `q3-approval-gate.png` — the Review deployments prompt from step 2 -- `q5-live-dashboard.png` — the live Q5 dashboard once it has several hours of history -- `ghaymah-monitoring.png` — optional, supports the Q2 detection section -- `ghaymah-volumes.png` — optional, supports the Q4 storage section - -Then link them from the root README. - -### 5. Housekeeping - -- Decide whether `PLAN.md`, `PROMPTS.md` and `CHECKLIST.md` ship. They document the - internal agent workflow; graders do not need them. -- Repository variables point at `ghaymah-api-staging` and `ghaymah-api-production`, - but the deployed application is named `ghaymah-api`. Either create those two - applications or note in `CICD.md` that they are illustrative targets. -- Let both monitors run as long as possible before submitting. Q5's 24 h uptime - tile only becomes meaningful after a full day. - ---- - -## Final gate - -Must print nothing: - -```powershell -Select-String -Path (Get-ChildItem -Recurse -Include *.md,*.yml,*.sh -File | Where-Object FullName -notmatch '\\\.git\\') -Pattern 'VERIFY|MY_DOCKERHUB_USER|the public URL Ghaymah assigns' -``` - -Then verify one last time that both live URLs respond, commit, and push. diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index 155c617..0000000 --- a/PLAN.md +++ /dev/null @@ -1,218 +0,0 @@ -# Ghaymah Internship Test — Implementation Plan - -**Scope:** 5 questions · 72 hours · 100 points (20 each) -**Execution model:** Two agents working in parallel — **Sol** (Codex) and **Opus 5** — with you as the integrator who does the manual platform steps (accounts, deployments, secrets). - ---- - -## 0. Ground Rules & Repo Setup (do this first, yourself — 30 min) - -**Purpose:** Both agents need a shared structure so their outputs merge cleanly without conflicts. Doing this before dispatching work prevents rework. - -**How:** -1. Create one Git repo (e.g. `ghaymah-test`) with this layout: - -``` -ghaymah-test/ -├── q1-deploy-monitor/ -│ ├── app/ # API + Dockerfile -│ ├── monitor/ # monitoring script -│ └── dashboard/ # HTML/CSS/JS dashboard -├── q2-postmortem/ -│ └── POSTMORTEM.md -├── q3-cicd/ -│ ├── .github/workflows/ # (copied to repo root at the end) -│ └── CICD.md -├── q4-scalability/ -│ └── SCALABILITY.md # diagram + math + strategy -├── q5-mithal-dashboard/ -│ ├── collector/ # metrics script -│ ├── data/ # CSV/JSON output -│ └── dashboard/ -└── README.md # index of all answers -``` - -2. Sign up / log in to **ghaymah.systems**, explore: Container platform, Container Registry, CLI docs, monitoring tools, Block Storage docs. **Take notes/screenshots** — Q2, Q3, Q4 answers must reference *real* Ghaymah features, not generic cloud talk. This research is the single highest-value manual task: graders will notice platform-specific accuracy. -3. Decide the stack once: **Python + FastAPI** for both APIs (Q1 and Q5 collector share language, smaller surface area) — or Node/Express if you prefer. The plan below assumes Python. - -**Why manual:** account creation, credentials, and platform exploration cannot be delegated to agents (and shouldn't be — never give agents your passwords/API keys; store tokens as env vars/GitHub secrets yourself). - ---- - -## Parallelization Map (who does what) - -| Track | Agent | Work | Rationale | -|---|---|---|---| -| A | **Opus 5** | Q1 (app + Dockerfile + monitor + dashboard), Q5 (collector + dashboard) | Heaviest coding volume, frontend polish, end-to-end coherence — Opus is strongest at multi-file builds and design-quality dashboards | -| B | **Sol** | Q3 (CI/CD workflow + docs), Q2 (postmortem), Q4 (scaling doc + diagram) | Mostly structured writing + one YAML workflow — well-bounded, spec-driven tasks that run independently of Track A | -| — | **You** | Ghaymah account, deployments, registry creds, GitHub secrets, real screenshots, final review | Anything requiring credentials or the live platform | - -The two tracks share **zero files**, so they can run truly simultaneously. Merge point: Day 2 evening, when you deploy Track A's artifacts and paste real values (URLs, registry names) into Track B's docs. - -**Prompting tip:** give each agent the full question text *plus* your Ghaymah platform notes from step 0, and tell them the repo layout above so paths match. - ---- - -## Q1 — Deploy & Monitor an App on Ghaymah (20 pts) - -### 1.1 The API app + `/health` endpoint — **Opus** -**Purpose:** The deliverable everything else in Q1 hangs off. `/health` is the standard liveness contract that orchestrators and your monitor script both consume. - -**How:** Small FastAPI app with: -- `GET /` — hello/info route -- `GET /health` — returns `{"status":"ok","uptime_s":...,"timestamp":...}` with HTTP 200. Keep it dependency-free (no DB check) so it reflects process liveness only. -- `GET /metrics` — returns an in-memory request counter + simple stats (this feeds the dashboard's "عدد الطلبات" requirement). Implement with a middleware that increments a counter per request. - -**Implementation:** `app/main.py`, `app/requirements.txt` (fastapi, uvicorn). Run with `uvicorn main:app --host 0.0.0.0 --port 8080`. - -### 1.2 Dockerfile — **Opus** -**Purpose:** Shows you understand image hygiene, not just "it runs". - -**How:** Multi-stage or slim single-stage: -- `python:3.12-slim` base, non-root user, `COPY requirements.txt` before code (layer caching), `EXPOSE 8080`, `HEALTHCHECK CMD curl -f http://localhost:8080/health || exit 1`, `CMD uvicorn...`. -- Add `.dockerignore`. - -**Implementation check:** you build locally: `docker build -t ghaymah-api . && docker run -p 8080:8080 ghaymah-api`, hit `/health`. - -### 1.3 Deploy to ghaymah.systems — **You (manual)** -**Purpose:** The 20 points require a *live* deployment; agents can't do this. - -**How:** Per platform docs — typically: push image to ghaymah Container Registry (`docker login `, `docker tag`, `docker push`), then create a container service in the dashboard/CLI pointing at the image, set port 8080, note the public URL. **Screenshot the running service and the `/health` response** for your submission. - -### 1.4 Monitoring script (every 30s) — **Opus** -**Purpose:** Demonstrates operational thinking — a poller that produces machine-readable history the dashboard can consume. - -**How:** `monitor/monitor.py`: -- Loop: every 30s, `GET /health` with timeout (e.g. 5s), record `timestamp, status ("up"/"down"), http_code, response_ms`; also pull `/metrics` for request count. -- Append each check to `monitor/data/checks.json` (or JSONL) — this file **is the dashboard's data source**, so agree the schema up front: - ```json - {"ts":"2026-07-26T12:00:00Z","status":"up","code":200,"latency_ms":42,"requests":1337} - ``` -- Config via env var `APP_URL`. Handle exceptions → status "down", latency null. Optional: alert line to stdout when 3 consecutive failures. - -### 1.5 Dashboard (HTML/CSS/JS) — **Opus** -**Purpose:** Shows the three required metrics: status, response time, request count. - -**How:** Single static page `dashboard/index.html` (inline CSS/JS, zero build step): -- Status badge (green/red) from latest check -- Line chart of `latency_ms` (Chart.js from CDN, or hand-rolled SVG if you want zero dependencies) -- Request counter tile -- Fetches `checks.json` (served next to it, or the monitor writes into the dashboard folder) and refreshes every 30s with `setInterval`. - -**Deliverable checklist Q1:** Dockerfile ✔ live URL ✔ `/health` ✔ monitor script ✔ dashboard ✔ screenshots ✔ - ---- - -## Q2 — OOMKilled Postmortem (20 pts) — **Sol** - -**Purpose:** Pure documentation question testing incident-response maturity. No code; graded on structure, realism, and platform-specific recommendations. - -**How:** `q2-postmortem/POSTMORTEM.md` in the classic blameless format: - -1. **Summary** — 45-min outage, repeated OOMKilled restarts (exit code 137), impact (error rate / downtime %), severity level. -2. **Timeline** — invent a realistic minute-by-minute table: memory creep begins → first OOMKill → crash-loop (each restart re-accumulates memory faster under retry traffic) → alert fires → engineer raises memory limit + rolls back the leaking release → recovery. Timestamps, actor, action. -3. **Root cause** — pick something concrete and defensible, e.g. a memory leak introduced in release X (unbounded in-process cache) combined with a memory limit sized for the old baseline; 5-Whys chain down to "no memory regression check in CI, no memory alerting." -4. **Recommendations** — table with owner + priority: right-size limits, fix leak, add memory alerts at 80%, add auto-scaling (links to part 2), load-test with soak tests, add runbook. -5. **Auto-scaling policy design** — a concrete policy for Ghaymah containers: - - Horizontal: scale out at avg memory > 70% or CPU > 65% for 2 min; min 2 / max N replicas; scale-in cooldown 5–10 min to avoid flapping. - - Explicitly explain *why HPA alone doesn't fix a leak* (leaks eat any replica count — scaling buys time for the fix, plus restart policy / vertical headroom). This nuance is what separates a top answer. -6. **Early detection with Ghaymah monitoring** — reference the actual tools you found in step 0: memory-usage graphs per container, alert rules (memory > 80% for 5 min, restart-count > 3/10 min, OOMKilled event alerts), dashboards + notification channels. - -**Implementation:** one well-formatted Markdown file; optionally a small Mermaid timeline/graph. **You** later swap in accurate Ghaymah tool names from your notes. - ---- - -## Q3 — CI/CD Pipeline (20 pts) — **Sol** - -**Purpose:** Tests GitHub Actions fluency + registry integration + release discipline (manual gate). - -### 3.1 Workflow — build & push to ghaymah Container Registry -**How:** `.github/workflows/deploy.yml`: -- Trigger: `push` to `main` (+ `workflow_dispatch`). -- Job `build-push`: checkout → `docker/login-action` against the ghaymah registry using `secrets.GHAYMAH_REGISTRY_USER/TOKEN` → `docker/build-push-action` tagging `registry.ghaymah.systems//app:${{ github.sha }}` and `:latest`. -- Job `deploy-staging` (needs build): deploy via ghaymah CLI to the staging service. -- Job `deploy-production` (needs staging): bound to a GitHub **Environment** named `production` with **required reviewers** — this is the manual-approval mechanism (job pauses until a human approves in the Actions UI). Sol should document that the environment + reviewers are configured in repo Settings → Environments, since that part isn't in YAML. - -### 3.2 Staging vs Production explanation -**How:** Section in `CICD.md`: purpose (validation vs. live users), differences (data, scale/replicas, secrets, access control, alerting thresholds, deploy cadence), promotion flow diagram `commit → build → staging (auto) → approval → production`. - -### 3.3 ghaymah CLI integration docs -**How:** `CICD.md` section: install CLI, `ghaymah auth login` with an API token stored as `GHAYMAH_API_TOKEN` GitHub secret, deploy/update-image command, how the workflow calls it in a step. **You** verify exact CLI command names from real docs and correct Sol's draft — flag every placeholder Sol writes with `` so nothing invented ships. - -**You (manual):** create the GitHub repo, add the secrets, create the `production` environment with yourself as reviewer, run the pipeline once and screenshot the approval gate. - ---- - -## Q4 — Scalability & Load Balancing (20 pts) — **Sol** - -**Purpose:** Architecture reasoning + arithmetic + platform storage knowledge. Fully deterministic — ideal Sol task. - -### 4.1 Architecture diagram (15,000 req/s) -**How:** Mermaid diagram in `SCALABILITY.md` (renders on GitHub): DNS → CDN/edge cache → Load Balancer (L7) → stateless API container fleet (auto-scaled) → cache layer (Redis) → DB (primary + replicas) → Block Storage for stateful pieces; plus monitoring/queue components. Annotate where the 15k req/s flows and what absorbs bursts. - -### 4.2 Container count math — show the work -``` -15,000 req/s ÷ 500 req/s per container = 30 containers -+30% headroom: 30 × 1.30 = 39 containers -``` -State the answer plainly (**39**), then add operational notes: round up, N+1 for rolling deploys, and that headroom covers spikes + AZ loss. - -### 4.3 Cold-start strategy -**How:** bullet strategy: keep a warm pool / min-replicas floor, pre-pull & slim images (small base, fewer layers), lazy-load nothing critical at boot, readiness probe gating so LB never routes to a cold container, predictive/scheduled scaling ahead of known peaks, gradual (step) scale-out policies. - -### 4.4 ghaymah Block Storage for stateful workloads -**How:** explain: containers are ephemeral → attach Block Storage volumes for databases/queues/uploads; persistence across restarts/reschedules; one-writer-per-volume semantics (so stateful services scale differently than stateless API tier); snapshots/backups; IOPS considerations. Tie back to the diagram (DB nodes mount Block Storage; API tier stays diskless). **You** correct against real Ghaymah Block Storage docs. - ---- - -## Q5 — mithal.space Monitoring Dashboard (20 pts) — **Opus** - -**Purpose:** The most integrated build: multi-metric collector + persisted history + richer dashboard + deployment. Assign to Opus because it shares patterns (and dashboard code style) with Q1 — one agent keeps them consistent. - -### 5.1 Collector script (every minute) — `collector/collect.py` -**How (one Python script, stdlib + `requests`):** -- **Latency:** `requests.get("https://mithal.space", timeout=10)` — measure elapsed ms. -- **Uptime:** same request's status code → up if `200 ≤ code < 400`. -- **SSL:** `ssl` + `socket` — open TLS connection to port 443, read cert `notAfter`, compute days remaining. -- **DNS:** time `socket.getaddrinfo("mithal.space", 443)` in ms. -- **Search response:** since mithal.space is a search engine — send a query (inspect the site first to find the search URL pattern, e.g. `/search?q=test` or its API endpoint) and time the response. **You** should check the actual URL format in a browser and give it to Opus; have Opus make it configurable. -- Scheduling: `while True: run(); sleep(60)` **plus** support one-shot mode (`--once`) so it can also run under cron / a scheduler. - -### 5.2 Storage -**How:** append one JSON object per check to `data/metrics.json` (array) or JSONL; keep a rolling window (e.g. last 24–48h, prune older) so the file stays small. Schema fixed up-front (same discipline as Q1): -```json -{"ts": "...", "up": true, "code": 200, "latency_ms": 120, - "dns_ms": 18, "ssl_days_left": 143, "search_ms": 210} -``` - -### 5.3 Dashboard — `dashboard/index.html` -**How:** static page, fetches `metrics.json`, renders: -- **Uptime % (24h):** `checks_up / checks_total * 100` over last 24h — big number tile. -- **Latency line chart (last hour):** Chart.js line of `latency_ms` (optionally overlay `search_ms`). -- **SSL card:** days remaining, color-coded (green > 30, yellow 8–30, red ≤ 7). -- **Last-10-checks table:** time, status ✅/❌, code, latency, DNS, search. -- Auto-refresh every 60s. Same visual language as Q1's dashboard (Opus keeps them consistent). - -### 5.4 Deploy dashboard to ghaymah — **You + Opus** -**How:** Opus writes a tiny Dockerfile that serves the dashboard **and** runs the collector in the same container (simplest: FastAPI/nginx serving static files + collector as background process writing into the served `data/` dir — a small `supervisord` or a shell entrypoint launching both). You push and deploy exactly as in Q1, screenshot the live URL. - ---- - -## Timeline (72h budget, comfortably front-loaded) - -| When | You | Opus (Track A) | Sol (Track B) | -|---|---|---|---| -| **Day 1 AM** | Repo setup, Ghaymah account + docs research, notes | — | — | -| **Day 1 PM** | Feed notes + prompts to both agents | Q1 app, Dockerfile, monitor, dashboard | Q2 postmortem full draft | -| **Day 2 AM** | Build & deploy Q1, registry setup, GitHub secrets | Q5 collector + dashboard | Q3 workflow + CICD.md | -| **Day 2 PM** | Deploy Q5, run pipeline, capture approval screenshot | Fix anything found in deployment | Q4 scalability doc | -| **Day 3 AM** | Verify every `` against real Ghaymah docs, insert real URLs/screenshots | Polish dashboards with real data | Revisions from your review | -| **Day 3 PM** | Final README index, cross-check rubric (all sub-items × 5 questions), submit | — | — | - -## Quality Gates (your final pass) - -1. Every question's *numbered sub-requirements* are individually answered — graders score per item. -2. No invented Ghaymah feature names survive — everything platform-specific is verified against real docs. -3. Both apps are **live** with URLs + screenshots in the README. -4. Monitor/collector scripts actually ran long enough to produce real data in the dashboards (start them Day 2 so charts aren't empty at submission). -5. Repo README links every deliverable — one-click grading experience. diff --git a/PROMPTS.md b/PROMPTS.md deleted file mode 100644 index 083d1ef..0000000 --- a/PROMPTS.md +++ /dev/null @@ -1,223 +0,0 @@ -# Copy-Paste Agent Prompts + Ghaymah Info to Collect - -> **PART C (added after research): the actual notes to paste — see bottom of this file. Docs turned out sparse, so Part C replaces Part A.** - -## PART A — What to grab from ghaymah.systems FIRST (~20 min) - -Log in and write down these exact values. Paste them into the `[GHAYMAH NOTES]` block of each prompt below. - -1. **Registry URL** — the container registry hostname (e.g. `registry.ghaymah.systems` or similar) and how login works (username + token? access key?). -2. **How to deploy a container** — dashboard steps or CLI command to create a service from an image, set the port, and get the public URL. Note what the public URLs look like. -3. **CLI** — exact install command, exact auth/login command, exact deploy/update command (copy them verbatim from the docs page). -4. **Monitoring** — what metrics/graphs exist per container (CPU? memory? restarts?), whether alert rules exist and what they can trigger on, notification channels. -5. **Auto-scaling** — does the platform have built-in autoscaling settings? What knobs (min/max replicas, CPU/memory thresholds)? -6. **Block Storage** — product name as they call it, how a volume attaches to a container, size limits, snapshot support. -7. **Environments** — can you run two services (staging + production)? Any built-in env concept, or just two separate services? -8. Also: open **mithal.space**, do a search, copy the result URL pattern (e.g. `https://mithal.space/search?q=test`). - -Screenshot everything as you go — you need screenshots for the submission anyway. - ---- - -## PART B — The 5 prompts - -Fill `[GHAYMAH NOTES: ...]` with the relevant items from Part A before sending. Send Prompt 1 (Opus) and Prompt 2 (Sol) simultaneously; then 3 & 4; then 5. - ---- - -### PROMPT 1 → OPUS (Q1: app + Dockerfile + monitor + dashboard) - -``` -Build a complete deliverable for this task, in the folder q1-deploy-monitor/: - -TASK: Deploy and monitor a simple API app on the Ghaymah cloud container platform. - -Create exactly these files: - -1. app/main.py — Python FastAPI app with: - - GET / → basic info JSON - - GET /health → {"status":"ok","uptime_s":,"timestamp":} with HTTP 200, no external dependencies - - GET /metrics → {"requests_total":, "started_at":} using an in-memory counter incremented by a middleware on every request - - Runs with uvicorn on 0.0.0.0:8080 - -2. app/requirements.txt — fastapi + uvicorn, pinned versions. - -3. app/Dockerfile — python:3.12-slim, non-root user, COPY requirements.txt and pip install BEFORE copying code (layer caching), EXPOSE 8080, HEALTHCHECK curl -f http://localhost:8080/health, CMD uvicorn. Also app/.dockerignore. - -4. monitor/monitor.py — Python monitoring script: - - Reads APP_URL from env var - - Every 30 seconds: GET $APP_URL/health with 5s timeout, and GET /metrics - - Appends one JSON object per check to monitor/data/checks.json (a JSON array): - {"ts":"","status":"up"|"down","code":,"latency_ms":,"requests":} - - Any exception/timeout → status "down" - - Prints an ALERT line after 3 consecutive failures - - Also supports --once flag for a single check - -5. dashboard/index.html — ONE self-contained file (inline CSS/JS, Chart.js from CDN allowed) that fetches ../monitor/data/checks.json (path configurable in a const at top) and shows: - - Big status badge, green UP / red DOWN, from the latest check - - Line chart of latency_ms over time - - Request-count tile from the latest check - - Last-updated timestamp, auto-refresh every 30s - - Handle empty/missing data gracefully (show "no data yet", don't crash) - Make it look clean and professional (dark theme is fine). - -Also write q1-deploy-monitor/README.md: how to build/run the Docker image locally, how to push it to the Ghaymah registry and deploy it, how to run the monitor, how to open the dashboard. - -[GHAYMAH NOTES: ] -``` - ---- - -### PROMPT 2 → SOL (Q2: postmortem) - -``` -Write q2-postmortem/POSTMORTEM.md — a professional, blameless postmortem for this scenario: - -SCENARIO: An application on the Ghaymah cloud platform was down for 45 minutes due to repeated OOMKilled container restarts. - -Required sections: - -1. Summary — one paragraph: duration 45 min, cause OOMKilled (exit code 137) crash loop, user impact (est. error rate / full downtime), severity SEV-2. - -2. Timeline — a table with minute-by-minute realistic entries (invent plausible timestamps): gradual memory growth after a release → first OOMKill → crash loop worsened by retry traffic → alert fires → engineer investigates → mitigation (memory limit raised + rollback of the leaking release) → full recovery at minute 45. Columns: time, event, actor/action. - -3. Root cause — 5-Whys analysis. Root: a memory leak introduced in the latest release (unbounded in-process cache), combined with a memory limit sized for the old baseline, no memory-usage alerting, and no memory regression testing in CI. - -4. Recommendations — table with columns: action, owner (role), priority (P0/P1/P2). Include: fix the leak, right-size memory limits, memory alert at 80% for 5 min, restart-count alert, add auto-scaling, soak/load tests in CI, write a runbook. - -5. Auto-scaling policy for Ghaymah — a concrete policy spec: horizontal scaling, scale OUT when avg memory > 70% OR CPU > 65% sustained 2 min; min 2 replicas, max N; scale-in cooldown 5–10 min to prevent flapping. Then an explicit honest paragraph: why autoscaling alone does NOT fix a memory leak (every replica leaks; scaling buys time and absorbs the retry storm while the leak is fixed) — plus vertical headroom and restart policies as complements. - -6. Early detection with Ghaymah monitoring — how to catch this before an outage using the platform's monitoring: per-container memory graphs (watch the sawtooth/creep pattern), alert rules (memory > 80% for 5 min, restarts > 3 in 10 min, OOMKilled events), dashboards and notification channels. Wrap any claim about a specific Ghaymah feature name in comments so I can check it against the real docs. - -Tone: professional SRE postmortem, markdown tables, no fluff. - -[GHAYMAH NOTES: ] -``` - ---- - -### PROMPT 3 → SOL (Q3: CI/CD) - -``` -Create two files for a CI/CD deliverable targeting the Ghaymah cloud platform: - -1. .github/workflows/deploy.yml — GitHub Actions workflow: - - Triggers: push to main, and workflow_dispatch - - Job build-push: checkout → docker/login-action to the Ghaymah container registry using secrets GHAYMAH_REGISTRY_USER and GHAYMAH_REGISTRY_TOKEN → docker/build-push-action building q1-deploy-monitor/app, tagging both //ghaymah-api:${{ github.sha }} and :latest - - Job deploy-staging (needs: build-push): installs the ghaymah CLI, authenticates with secret GHAYMAH_API_TOKEN, deploys/updates the STAGING service to the new image tag - - Job deploy-production (needs: deploy-staging): environment: production — this is the manual approval gate — then same deploy against the PRODUCTION service - - Mark every Ghaymah-specific command with a # VERIFY comment - -2. q3-cicd/CICD.md with: - - Pipeline overview + a mermaid diagram: commit → build+push → staging (auto) → manual approval → production - - "Manual approval" section: explain it's implemented via a GitHub Environment named production with required reviewers, configured in Settings → Environments (not in YAML), and the run pauses until approved - - "Staging vs Production" section: purpose of each, differences table (data: synthetic vs real; scale/replicas; secrets; access control; alerting thresholds; deploy cadence; who can approve) - - "Ghaymah CLI integration" section: install, auth login with API token, the deploy/update command, and how the workflow steps call it. Wrap uncertain command names in . - -[GHAYMAH NOTES: ] -``` - ---- - -### PROMPT 4 → SOL (Q4: scalability) - -``` -Write q4-scalability/SCALABILITY.md answering this task about the Ghaymah cloud platform: - -1. Architecture diagram (mermaid, renders on GitHub) for an app receiving 15,000 req/s: - DNS → CDN/edge cache → L7 load balancer → auto-scaled fleet of stateless API containers → Redis cache → PostgreSQL primary + read replicas (volumes on Ghaymah Block Storage) → and a monitoring/alerting component observing everything. Annotate the request flow and where bursts get absorbed (CDN + cache). - -2. Container count calculation — show the arithmetic explicitly: - 15,000 req/s ÷ 500 req/s per container = 30 containers - +30% headroom: 30 × 1.30 = 39 containers - Answer: 39. Add notes: always round up; keep N+1 extra for rolling deploys; headroom also covers traffic spikes and a zone failure. - -3. Cold-start strategy for new containers, as concrete bullets: minimum warm replica floor (never scale to zero for this tier), slim pre-pulled images (small base, few layers) for fast pulls, fast app boot (lazy-load non-critical work), readiness probe so the load balancer only routes to warmed containers, predictive/scheduled scale-up before known peaks, step-based scale-out (add several at once under sharp load). - -4. Ghaymah Block Storage for stateful workloads: containers are ephemeral so state must live on attached volumes; use Block Storage for the database, queues, uploads; one-writer-per-volume semantics mean the stateful tier scales via replication (primary/replica) not by cloning volumes; snapshots for backup; IOPS/size considerations; the API tier stays completely diskless so it can scale freely. Wrap platform-specific claims in . - -[GHAYMAH NOTES: ] -``` - ---- - -### PROMPT 5 → OPUS (Q5: mithal.space monitoring) - -``` -Build a monitoring system for the website https://mithal.space (a search engine), in q5-mithal-dashboard/: - -1. collector/collect.py — Python (stdlib + requests only): - Every 60 seconds (loop mode; also support --once) measure: - - latency_ms: timed GET https://mithal.space (10s timeout) - - up: true if status code 200–399 - - code: the status code (null on connection failure) - - ssl_days_left: open an ssl socket to mithal.space:443, read the cert notAfter, compute days remaining - - dns_ms: timed socket.getaddrinfo("mithal.space", 443) - - search_ms: timed GET to the search endpoint — make the URL a config constant, default: - Append one object per run to data/metrics.json (JSON array): - {"ts":"","up":bool,"code":int|null,"latency_ms":float|null,"dns_ms":float|null,"ssl_days_left":int|null,"search_ms":float|null} - Prune entries older than 48h on each write. Never crash on a failed check — record nulls. - -2. dashboard/index.html — ONE self-contained file (Chart.js CDN allowed) fetching data/metrics.json (path const at top): - - Uptime % tile over the last 24h: up_checks / total_checks × 100, one decimal - - Line chart of latency_ms for the LAST HOUR (optionally search_ms as second series) - - SSL card: "X days remaining", green if >30, yellow 8–30, red ≤7 - - Table of the last 10 checks: time, ✅/❌, code, latency, dns, search - - Auto-refresh every 60s, graceful empty state, clean professional look consistent with a dark ops-dashboard style - -3. Dockerfile (in q5-mithal-dashboard/) — a single container that BOTH serves the dashboard as static files on port 8080 AND runs the collector in the background writing into the served directory. Simplest approach: python:3.12-slim, a start.sh entrypoint that launches collect.py in the background then runs python -m http.server 8080 (or uvicorn static serving) from the dashboard directory, with data/ inside the served path so the dashboard can fetch metrics.json. - -4. README.md: run locally, build the image, deploy to Ghaymah. - -[GHAYMAH NOTES: ] -``` - ---- - -## PART C — Ready [GHAYMAH NOTES] blocks (paste these as-is) - -Research result: Ghaymah docs only document **deploy-from-image-URL** (image URL + app name + port + public access toggle + env vars → Deploy). No registry, CLI, monitoring, autoscaling, or block-storage details are documented. Strategy: use **Docker Hub** as the registry, and have agents label undocumented platform specifics as **"Proposed design"** instead of inventing features. mithal.space search URL confirmed: `https://mithal.space/search?q=`. - -### Paste into PROMPT 1 (Opus, Q1): -``` -[GHAYMAH NOTES] -- Ghaymah deploys containers from a public image URL. Deployment flow (dashboard): enter Container Image URL (e.g. docker.io//ghaymah-api:latest) → set Application Name → set Port Number (must match the EXPOSEd port) → enable Public Access → add Environment Variables → click Deploy. -- So in the README, document: build locally → docker push to Docker Hub (docker.io//ghaymah-api) → deploy on Ghaymah by pasting that image URL with port 8080 and Public Access enabled. -- Ghaymah's docs don't document the public URL format; write "" as a placeholder. -``` - -### Paste into PROMPT 2 (Sol, Q2): -``` -[GHAYMAH NOTES] -- Ghaymah's public docs do not document monitoring/alerting or autoscaling features. IMPORTANT: do NOT invent Ghaymah feature names. Instead: - - Section 5 (auto-scaling policy): the task literally asks to DESIGN a policy for the platform — present it as a proposed policy design ("Proposed auto-scaling policy for Ghaymah"), which is exactly what's asked. - - Section 6 (early detection): frame as "monitoring approach on Ghaymah" using platform-agnostic container signals (memory %, restart count, OOMKilled events, exit code 137) and note these can be collected via an external monitor hitting /health plus container runtime stats, since that's verifiable. Keep markers only where you reference a platform capability. -``` - -### Paste into PROMPT 3 (Sol, Q3): -``` -[GHAYMAH NOTES] -- Ghaymah deploys from a container image URL entered in its dashboard (image URL + app name + port + public access + env vars → Deploy). No Ghaymah-hosted registry or CLI is documented publicly. -- Therefore: build the workflow to push to Docker Hub (docker/login-action with secrets DOCKERHUB_USERNAME / DOCKERHUB_TOKEN, image docker.io//ghaymah-api:${{ github.sha }} and :latest). Add a note: "if a Ghaymah-hosted registry is available, only the login server and image prefix change." -- For the deploy jobs: since no CLI is documented, implement deploy-staging and deploy-production as jobs that (a) print the exact image tag to deploy and (b) call a placeholder script scripts/ghaymah_deploy.sh marked # VERIFY, and document in CICD.md that per current docs the deploy step is updating the image URL/tag in the Ghaymah dashboard for the app — with the CLI section written as "integration guide (to be confirmed against Ghaymah CLI docs)". -- Staging vs production on Ghaymah: two separate deployed apps, e.g. myapp-staging and myapp-production, each with its own env vars; the workflow deploys staging automatically and production only after the GitHub Environment approval. -``` - -### Paste into PROMPT 4 (Sol, Q4): -``` -[GHAYMAH NOTES] -- Ghaymah's public docs don't detail Block Storage or autoscaling specifics. Write section 4 as a correct general explanation of block storage for stateful container workloads applied to Ghaymah ("Ghaymah Block Storage"), and keep only on hard specifics (size limits, snapshot support). The architecture/math/cold-start sections are platform-agnostic — no changes needed. -``` - -### Paste into PROMPT 5 (Opus, Q5): -``` -[GHAYMAH NOTES] -- Search endpoint CONFIRMED working: https://mithal.space/search?q=test (returns a results page). Use that as the default search URL constant. -- Deployment: Ghaymah deploys from a public image URL (dashboard: image URL + app name + port 8080 + Public Access enabled + env vars → Deploy). README should say: push image to Docker Hub, then deploy by pasting docker.io//mithal-monitor:latest into Ghaymah with port 8080. -``` - -### Your remaining manual to-dos (updated) -1. Make/confirm a **Docker Hub** account; create repos `ghaymah-api` and `mithal-monitor` (public is simplest for Ghaymah to pull). -2. In GitHub: secrets `DOCKERHUB_USERNAME`, `DOCKERHUB_TOKEN` (access token from Docker Hub → Account Settings → Security); environment `production` with you as required reviewer. -3. In the Ghaymah dashboard, while deploying, **look for tabs/menus the docs didn't cover** (Registry, CLI, Metrics/Logs, Scaling, Volumes). If any exist, screenshot them and resolve the matching markers — instant answer upgrades for Q2/Q3/Q4. -4. Screenshots needed: Q1 app running + /health JSON, Q3 workflow paused at approval, Q5 live dashboard. diff --git a/README.md b/README.md index d0ae3ab..87f1882 100644 --- a/README.md +++ b/README.md @@ -1,94 +1,104 @@ # Ghaymah Cloud Technical Assessment -This repository contains the five deliverables for the Ghaymah cloud platform assessment. +Complete implementation of the five Ghaymah cloud assessment tasks: deployment, +monitoring, incident analysis, CI/CD, scalability, and the `mithal.space` +monitoring dashboard. ## Deliverables | Question | Deliverable | Status | |---|---|---| -| Q1 — Deploy and monitor an API | [Application, monitor, dashboard, and deployment guide](q1-deploy-monitor/README.md) | Live on Ghaymah | -| Q2 — OOMKilled postmortem | [POSTMORTEM.md](q2-postmortem/POSTMORTEM.md) | Complete | -| Q3 — CI/CD pipeline | [Workflow](.github/workflows/deploy.yml) and [CICD.md](q3-cicd/CICD.md) | Build/push and approval gate complete; automated Ghaymah image update awaiting a documented non-interactive command | -| Q4 — Scalability | [SCALABILITY.md](q4-scalability/SCALABILITY.md) | Complete | -| Q5 — mithal.space monitoring | [Collector and dashboard](q5-mithal-dashboard/) | Live on Ghaymah | +| Q1 — Deploy and monitor an API | [Application, monitor, dashboard, and deployment guide](q1-deploy-monitor/README.md) | Live | +| Q2 — OOMKilled postmortem | [Professional blameless postmortem](q2-postmortem/POSTMORTEM.md) | Complete | +| Q3 — CI/CD pipeline | [Workflow](.github/workflows/deploy.yml) and [documentation](q3-cicd/CICD.md) | Complete | +| Q4 — Scalability | [Architecture, capacity calculation, cold starts, and storage](q4-scalability/SCALABILITY.md) | Complete | +| Q5 — `mithal.space` monitoring | [Collector and dashboard](q5-mithal-dashboard/) | Live | -## Live deployments +## Live services - Q1 API: - Q1 health: -- Q5 monitoring dashboard: -- Docker Hub API image: `docker.io/agamy74/ghaymah-api:8bc563f` -- Docker Hub dashboard image: `docker.io/agamy74/mithal-monitor:8bc563f` -- GitHub repository: +- Q1 OpenAPI: +- Q5 dashboard: +- Docker Hub API image: `docker.io/agamy74/ghaymah-api` +- Docker Hub dashboard image: `docker.io/agamy74/mithal-monitor` +- Public repository: ## Deployment evidence -- [Ghaymah project](screenshots/ghaymah-project-created.png) -- [Q1 API service](screenshots/q1-ghaymah-service.png) -- [Q5 monitoring service](screenshots/q5-ghaymah-service.png) +The curated evidence set is documented in +[docs/evidence/README.md](docs/evidence/README.md). -## Current verified Ghaymah information +| Evidence | Screenshot | +|---|---| +| Ghaymah project and applications | [Project](docs/evidence/screenshots/01-ghaymah-project.png) | +| Q1 API service | [Q1 service](docs/evidence/screenshots/02-q1-service.png) | +| Q1 live health response | [Q1 health](docs/evidence/screenshots/03-q1-health.png) | +| Q1 monitoring dashboard | [Q1 dashboard](docs/evidence/screenshots/04-q1-dashboard.png) | +| Q5 Ghaymah service | [Q5 service](docs/evidence/screenshots/05-q5-service.png) | +| Q5 monitoring dashboard | [Q5 dashboard](docs/evidence/screenshots/06-q5-dashboard.png) | +| GitHub production protection | [Approval protection](docs/evidence/screenshots/07-production-approval.png) | +| Successful CI/CD run | [Pipeline](docs/evidence/screenshots/08-pipeline-success.png) | -The current public Ghaymah documentation establishes the following: +## CI/CD behavior -- The official CLI is installed with `curl -sSL https://cli.ghaymah.systems/install.sh | bash`. -- The executable is named `gy`; the current binary reports its release with `gy version`. -- Authentication uses `gy auth login`. The current CLI exposes `--email` and `--password` flags, but does not expose an API-token flag. -- `gy resource app init --project-id --name ` creates `.ghaymah.json`. -- `gy resource app launch [PATH]` builds and deploys the application described by the local Dockerfile and `.ghaymah.json`. -- `gy resource app logs` retrieves application logs. -- `gy resource app update ` accepts JSON input or dot-notation updates, but the public help does not identify a supported external-image field. -- The documented `.ghaymah.json` example includes the application ID/name, project ID, exposed port, public access, resource tier, and Dockerfile name. -- Ghaymah lists a container registry, CI/CD, monitoring, autoscaling, APIs, and Block Storage as products or capabilities, but the public pages reviewed for this submission do not expose enough operational syntax or limits to use their hard specifics safely. -- The authenticated deployment form supports either a Git repository or container image URL, an optional registry pull secret, instance size, application name, port, public access, custom domain, environment variables, and attached storage volumes. -- The authenticated volume form displays a supported size range of 50 MiB to 10 GiB. -- The authenticated External Integrations page exposes Docker Hub connection fields; no Ghaymah-hosted registry endpoint or push instructions were visible. +The workflow: + +1. Builds `q1-deploy-monitor/app`. +2. Embeds the Git commit as `RELEASE_SHA`. +3. Pushes immutable SHA and `latest` tags to Docker Hub. +4. Starts the immutable image in an ephemeral staging container. +5. Requires `status=ok` and the expected SHA from `/health`. +6. Pauses for the required reviewer on the GitHub `production` Environment. +7. Verifies that the live Ghaymah application serves the approved SHA. + +Ghaymah CLI `0.0.24` documents interactive email/password login but does not +publish API-token authentication or the external-image field for +`gy resource app update`. Production image promotion therefore uses the +authenticated dashboard and is followed by automated release verification. The +workflow does not report a stale or unverified deployment as successful. + +The account's five-resource free-plan limit prevents a third persistent Ghaymah +application, so staging runs as an isolated ephemeral container on the Actions +runner. Production and the Q5 dashboard remain live on Ghaymah. + +## Verified platform information + +- The authenticated deployment form accepts a Git repository or container image, + registry pull secret, instance size, application name, port, public access, + custom domain, environment variables, and storage volumes. +- Docker Hub is available through External Integrations. +- The authenticated volume form displays a range from 50 MiB to 10 GiB. +- The official CLI installs with + `curl -sSL https://cli.ghaymah.systems/install.sh | bash`. +- CLI `0.0.24` exposes `gy version`, `gy auth login`, + `gy resource app init`, `gy resource app launch`, logs, and a generic + `gy resource app update`. Official references: - [Ghaymah CLI documentation](https://ghaymah.systems/docs) - [Ghaymah CLI overview](https://ghaymah.systems/cli) - [Ghaymah products](https://ghaymah.systems/products) -- [Ghaymah changelog](https://ghaymah.systems/changelog) -## Local quick checks - -### Q1 API +## Local checks ```bash -python -m pip install -r q1-deploy-monitor/app/requirements.txt -python -m uvicorn main:app --app-dir q1-deploy-monitor/app --host 0.0.0.0 --port 8080 +docker build --build-arg RELEASE_SHA=local \ + -t ghaymah-api:local q1-deploy-monitor/app +docker run --rm -p 8080:8080 ghaymah-api:local +curl -f http://localhost:8080/health ``` -Then open: - -- `http://localhost:8080/health` -- `http://localhost:8080/metrics` -- `http://localhost:8080/docs` - -### Q1 monitor and dashboard - ```bash -APP_URL=http://localhost:8080 python q1-deploy-monitor/monitor/monitor.py --once -python -m http.server 8000 --directory q1-deploy-monitor +APP_URL=https://ghaymah-api-615e99f13665.hosted.ghaymah.systems \ + python q1-deploy-monitor/monitor/monitor.py --once ``` -Open `http://localhost:8000/dashboard/index.html`. - -### Q5 collector - ```bash -python -m pip install -r q5-mithal-dashboard/collector/requirements.txt python q5-mithal-dashboard/collector/collect.py --once ``` -## Work that requires account access - -The following require a credential or capability that is not available in the public documentation: - -1. Add `DOCKERHUB_TOKEN` and any confirmed Ghaymah CI authentication secret to GitHub. -2. Confirm the supported API-token authentication mechanism and image field for `gy resource app update`. -3. Approve the production GitHub Environment after the image-build job succeeds and capture the approval evidence. -4. Confirm monitoring/autoscaling controls, snapshot behavior, and detailed Block Storage performance limits before removing the remaining `VERIFY` markers. - -No credentials should be committed to this repository. Use GitHub Secrets and Ghaymah's secret-management controls for sensitive values. +No credentials are stored in the repository. Docker Hub credentials remain in +GitHub Secrets and Ghaymah registry authentication remains in the platform +integration. diff --git a/docs/evidence/README.md b/docs/evidence/README.md new file mode 100644 index 0000000..32467c4 --- /dev/null +++ b/docs/evidence/README.md @@ -0,0 +1,19 @@ +# Submission Evidence + +These screenshots are intentionally limited to the evidence needed to review +the assessment. Internal prompts, planning notes, transient failures, and +duplicate captures are not included. + +| Number | File | Demonstrates | +|---:|---|---| +| 01 | `01-ghaymah-project.png` | Authenticated Ghaymah project and deployed applications. | +| 02 | `02-q1-service.png` | Q1 API application in Ghaymah. | +| 03 | `03-q1-health.png` | Live HTTP 200 health response with release identifier. | +| 04 | `04-q1-dashboard.png` | Q1 status, latency, request count, uptime, and history. | +| 05 | `05-q5-service.png` | Q5 monitoring application in Ghaymah. | +| 06 | `06-q5-dashboard.png` | Q5 uptime, latency chart, SSL, DNS/search metrics, and recent checks. | +| 07 | `07-production-approval.png` | GitHub `production` Environment required-reviewer protection. | +| 08 | `08-pipeline-success.png` | Successful build, staging verification, approval, and production verification. | + +All service URLs and Docker image names are also listed in the repository root +[README](../../README.md). diff --git a/docs/evidence/screenshots/01-ghaymah-project.png b/docs/evidence/screenshots/01-ghaymah-project.png new file mode 100644 index 0000000..8109927 Binary files /dev/null and b/docs/evidence/screenshots/01-ghaymah-project.png differ diff --git a/docs/evidence/screenshots/02-q1-service.png b/docs/evidence/screenshots/02-q1-service.png new file mode 100644 index 0000000..cb724e5 Binary files /dev/null and b/docs/evidence/screenshots/02-q1-service.png differ diff --git a/docs/evidence/screenshots/04-q1-dashboard.png b/docs/evidence/screenshots/04-q1-dashboard.png new file mode 100644 index 0000000..f895b20 Binary files /dev/null and b/docs/evidence/screenshots/04-q1-dashboard.png differ diff --git a/screenshots/q5-ghaymah-service.png b/docs/evidence/screenshots/05-q5-service.png similarity index 100% rename from screenshots/q5-ghaymah-service.png rename to docs/evidence/screenshots/05-q5-service.png diff --git a/docs/evidence/screenshots/06-q5-dashboard.png b/docs/evidence/screenshots/06-q5-dashboard.png new file mode 100644 index 0000000..25608fb Binary files /dev/null and b/docs/evidence/screenshots/06-q5-dashboard.png differ diff --git a/q1-deploy-monitor/README.md b/q1-deploy-monitor/README.md index 43090e2..d416ca6 100644 --- a/q1-deploy-monitor/README.md +++ b/q1-deploy-monitor/README.md @@ -39,7 +39,9 @@ every request. It is per-process and deliberately resets on restart — a counte that drops to zero in the dashboard is a visible signal that the container was restarted or redeployed. -Environment variables (all optional): `APP_NAME`, `APP_VERSION`, `PORT` (default `8080`). +Environment variables (all optional): `APP_NAME`, `APP_VERSION`, `RELEASE_SHA`, +and `PORT` (default `8080`). CI embeds the immutable Git commit in +`RELEASE_SHA`, and `/health` exposes it as `release` for deployment verification. ### Run locally without Docker @@ -124,7 +126,7 @@ anonymously from the URL you paste in. | Field | Value | |---|---| -| Container Image URL | `docker.io/agamy74/ghaymah-api:8bc563f` | +| Container Image URL | `docker.io/agamy74/ghaymah-api:` | | Application Name | `ghaymah-api` | | Port Number | `8080` (must match the `EXPOSE`d port) | | Public Access | **enabled** | @@ -139,7 +141,8 @@ service a public URL. curl -f https://ghaymah-api-615e99f13665.hosted.ghaymah.systems/health ``` -Expected: HTTP 200 with `{"status":"ok","uptime_s":...,"timestamp":"..."}`. +Expected: HTTP 200 with +`{"status":"ok","release":"","uptime_s":...,"timestamp":"..."}`. Also open `https://ghaymah-api-615e99f13665.hosted.ghaymah.systems/docs` in a browser for the OpenAPI page, and screenshot both the running service in the Ghaymah dashboard and the `/health` diff --git a/q1-deploy-monitor/app/Dockerfile b/q1-deploy-monitor/app/Dockerfile index 408df50..21784c8 100644 --- a/q1-deploy-monitor/app/Dockerfile +++ b/q1-deploy-monitor/app/Dockerfile @@ -1,5 +1,7 @@ FROM python:3.12-slim +ARG RELEASE_SHA=local + # curl is required by the HEALTHCHECK below; everything else stays out of the image. RUN apt-get update \ && apt-get install -y --no-install-recommends curl \ @@ -8,6 +10,7 @@ RUN apt-get update \ ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ PIP_NO_CACHE_DIR=1 \ + RELEASE_SHA=${RELEASE_SHA} \ PORT=8080 WORKDIR /app diff --git a/q1-deploy-monitor/app/main.py b/q1-deploy-monitor/app/main.py index f89f793..1791504 100644 --- a/q1-deploy-monitor/app/main.py +++ b/q1-deploy-monitor/app/main.py @@ -15,6 +15,7 @@ from fastapi.responses import JSONResponse APP_NAME = os.getenv("APP_NAME", "ghaymah-api") APP_VERSION = os.getenv("APP_VERSION", "1.0.0") +RELEASE_SHA = os.getenv("RELEASE_SHA", "local") PORT = int(os.getenv("PORT", "8080")) # Process start time drives both uptime and the /metrics started_at field. @@ -53,6 +54,7 @@ async def root(): return { "service": APP_NAME, "version": APP_VERSION, + "release": RELEASE_SHA, "message": "Ghaymah deployment demo API", "endpoints": ["/", "/health", "/metrics", "/docs"], "started_at": STARTED_AT_ISO, @@ -68,6 +70,7 @@ async def health(): status_code=200, content={ "status": "ok", + "release": RELEASE_SHA, "uptime_s": _uptime_s(), "timestamp": _now_iso(), }, diff --git a/q1-deploy-monitor/monitor/data/checks.json b/q1-deploy-monitor/monitor/data/checks.json index 0ca2143..4ea3ece 100644 --- a/q1-deploy-monitor/monitor/data/checks.json +++ b/q1-deploy-monitor/monitor/data/checks.json @@ -957,5 +957,481 @@ "code": 200, "latency_ms": 707.51, "requests": 268 + }, + { + "ts": "2026-07-26T18:03:51.848034+00:00", + "status": "up", + "code": 200, + "latency_ms": 892.26, + "requests": 270 + }, + { + "ts": "2026-07-26T18:04:25.528579+00:00", + "status": "up", + "code": 200, + "latency_ms": 694.95, + "requests": 272 + }, + { + "ts": "2026-07-26T18:04:59.231057+00:00", + "status": "up", + "code": 200, + "latency_ms": 1861.06, + "requests": 274 + }, + { + "ts": "2026-07-26T18:05:30.454738+00:00", + "status": "up", + "code": 200, + "latency_ms": 586.48, + "requests": 276 + }, + { + "ts": "2026-07-26T18:06:02.324156+00:00", + "status": "up", + "code": 200, + "latency_ms": 890.61, + "requests": 278 + }, + { + "ts": "2026-07-26T18:06:34.144517+00:00", + "status": "up", + "code": 200, + "latency_ms": 905.66, + "requests": 280 + }, + { + "ts": "2026-07-26T18:07:05.780266+00:00", + "status": "up", + "code": 200, + "latency_ms": 738.98, + "requests": 282 + }, + { + "ts": "2026-07-26T18:07:37.395548+00:00", + "status": "up", + "code": 200, + "latency_ms": 843.17, + "requests": 284 + }, + { + "ts": "2026-07-26T18:08:09.145094+00:00", + "status": "up", + "code": 200, + "latency_ms": 843.5, + "requests": 286 + }, + { + "ts": "2026-07-26T18:08:42.112423+00:00", + "status": "up", + "code": 200, + "latency_ms": 1355.9, + "requests": 288 + }, + { + "ts": "2026-07-26T18:09:14.075130+00:00", + "status": "up", + "code": 200, + "latency_ms": 917.64, + "requests": 290 + }, + { + "ts": "2026-07-26T18:09:45.825068+00:00", + "status": "up", + "code": 200, + "latency_ms": 982.26, + "requests": 292 + }, + { + "ts": "2026-07-26T18:10:17.588615+00:00", + "status": "up", + "code": 200, + "latency_ms": 891.01, + "requests": 294 + }, + { + "ts": "2026-07-26T18:10:49.297873+00:00", + "status": "up", + "code": 200, + "latency_ms": 855.83, + "requests": 296 + }, + { + "ts": "2026-07-26T18:11:20.912121+00:00", + "status": "up", + "code": 200, + "latency_ms": 775.26, + "requests": 298 + }, + { + "ts": "2026-07-26T18:11:52.499884+00:00", + "status": "up", + "code": 200, + "latency_ms": 775.21, + "requests": 300 + }, + { + "ts": "2026-07-26T18:12:24.514786+00:00", + "status": "up", + "code": 200, + "latency_ms": 1206.22, + "requests": 302 + }, + { + "ts": "2026-07-26T18:12:56.158854+00:00", + "status": "up", + "code": 200, + "latency_ms": 897.33, + "requests": 304 + }, + { + "ts": "2026-07-26T18:13:27.673469+00:00", + "status": "up", + "code": 200, + "latency_ms": 727.96, + "requests": 306 + }, + { + "ts": "2026-07-26T18:14:00.527846+00:00", + "status": "up", + "code": 200, + "latency_ms": 1527.71, + "requests": 308 + }, + { + "ts": "2026-07-26T18:14:32.323584+00:00", + "status": "up", + "code": 200, + "latency_ms": 880.48, + "requests": 310 + }, + { + "ts": "2026-07-26T18:15:03.962668+00:00", + "status": "up", + "code": 200, + "latency_ms": 879.14, + "requests": 312 + }, + { + "ts": "2026-07-26T18:15:35.833296+00:00", + "status": "up", + "code": 200, + "latency_ms": 851.54, + "requests": 314 + }, + { + "ts": "2026-07-26T18:16:07.355297+00:00", + "status": "up", + "code": 200, + "latency_ms": 760.54, + "requests": 316 + }, + { + "ts": "2026-07-26T18:16:39.012633+00:00", + "status": "up", + "code": 200, + "latency_ms": 799.64, + "requests": 318 + }, + { + "ts": "2026-07-26T18:17:11.074470+00:00", + "status": "up", + "code": 200, + "latency_ms": 873.43, + "requests": 320 + }, + { + "ts": "2026-07-26T18:17:42.996047+00:00", + "status": "up", + "code": 200, + "latency_ms": 1053.03, + "requests": 322 + }, + { + "ts": "2026-07-26T18:18:14.894655+00:00", + "status": "up", + "code": 200, + "latency_ms": 1185.62, + "requests": 324 + }, + { + "ts": "2026-07-26T18:18:46.652731+00:00", + "status": "up", + "code": 200, + "latency_ms": 859.36, + "requests": 326 + }, + { + "ts": "2026-07-26T18:19:17.573385+00:00", + "status": "down", + "code": 502, + "latency_ms": 895.37, + "requests": null + }, + { + "ts": "2026-07-26T18:19:49.320997+00:00", + "status": "up", + "code": 200, + "latency_ms": 866.28, + "requests": 2 + }, + { + "ts": "2026-07-26T18:20:21.060460+00:00", + "status": "up", + "code": 200, + "latency_ms": 839.12, + "requests": 4 + }, + { + "ts": "2026-07-26T18:20:52.829427+00:00", + "status": "up", + "code": 200, + "latency_ms": 889.52, + "requests": 6 + }, + { + "ts": "2026-07-26T18:21:24.408306+00:00", + "status": "up", + "code": 200, + "latency_ms": 782.05, + "requests": 8 + }, + { + "ts": "2026-07-26T18:21:55.914426+00:00", + "status": "up", + "code": 200, + "latency_ms": 723.7, + "requests": 10 + }, + { + "ts": "2026-07-26T18:22:27.534277+00:00", + "status": "up", + "code": 200, + "latency_ms": 799.56, + "requests": 12 + }, + { + "ts": "2026-07-26T18:22:59.367798+00:00", + "status": "up", + "code": 200, + "latency_ms": 979.2, + "requests": 14 + }, + { + "ts": "2026-07-26T18:23:30.996828+00:00", + "status": "up", + "code": 200, + "latency_ms": 779.25, + "requests": 16 + }, + { + "ts": "2026-07-26T18:24:02.747579+00:00", + "status": "up", + "code": 200, + "latency_ms": 880.05, + "requests": 18 + }, + { + "ts": "2026-07-26T18:24:34.214686+00:00", + "status": "up", + "code": 200, + "latency_ms": 717.03, + "requests": 20 + }, + { + "ts": "2026-07-26T18:25:05.757052+00:00", + "status": "up", + "code": 200, + "latency_ms": 757.22, + "requests": 22 + }, + { + "ts": "2026-07-26T18:25:37.276950+00:00", + "status": "up", + "code": 200, + "latency_ms": 733.39, + "requests": 24 + }, + { + "ts": "2026-07-26T18:26:09.223657+00:00", + "status": "up", + "code": 200, + "latency_ms": 1189.45, + "requests": 26 + }, + { + "ts": "2026-07-26T18:26:43.045315+00:00", + "status": "up", + "code": 200, + "latency_ms": 872.87, + "requests": 28 + }, + { + "ts": "2026-07-26T18:27:17.288142+00:00", + "status": "up", + "code": 200, + "latency_ms": 3339.91, + "requests": 30 + }, + { + "ts": "2026-07-26T18:27:50.892062+00:00", + "status": "up", + "code": 200, + "latency_ms": 2525.6, + "requests": 32 + }, + { + "ts": "2026-07-26T18:28:22.569143+00:00", + "status": "up", + "code": 200, + "latency_ms": 891.36, + "requests": 34 + }, + { + "ts": "2026-07-26T18:28:55.514777+00:00", + "status": "up", + "code": 200, + "latency_ms": 1051.03, + "requests": 36 + }, + { + "ts": "2026-07-26T18:29:27.193474+00:00", + "status": "up", + "code": 200, + "latency_ms": 857.1, + "requests": 38 + }, + { + "ts": "2026-07-26T18:29:59.036359+00:00", + "status": "up", + "code": 200, + "latency_ms": 865.17, + "requests": 40 + }, + { + "ts": "2026-07-26T18:30:30.544858+00:00", + "status": "up", + "code": 200, + "latency_ms": 714.51, + "requests": 42 + }, + { + "ts": "2026-07-26T18:31:02.161454+00:00", + "status": "up", + "code": 200, + "latency_ms": 782.59, + "requests": 44 + }, + { + "ts": "2026-07-26T18:31:35.413556+00:00", + "status": "up", + "code": 200, + "latency_ms": 789.57, + "requests": 46 + }, + { + "ts": "2026-07-26T18:32:09.118559+00:00", + "status": "up", + "code": 200, + "latency_ms": 901.22, + "requests": 48 + }, + { + "ts": "2026-07-26T18:32:40.893263+00:00", + "status": "up", + "code": 200, + "latency_ms": 900.57, + "requests": 50 + }, + { + "ts": "2026-07-26T18:33:12.679890+00:00", + "status": "up", + "code": 200, + "latency_ms": 863.33, + "requests": 52 + }, + { + "ts": "2026-07-26T18:33:44.501781+00:00", + "status": "up", + "code": 200, + "latency_ms": 1023.98, + "requests": 54 + }, + { + "ts": "2026-07-26T18:34:16.365978+00:00", + "status": "up", + "code": 200, + "latency_ms": 750.53, + "requests": 56 + }, + { + "ts": "2026-07-26T18:34:48.286153+00:00", + "status": "up", + "code": 200, + "latency_ms": 960.3, + "requests": 58 + }, + { + "ts": "2026-07-26T18:35:19.863722+00:00", + "status": "up", + "code": 200, + "latency_ms": 793.72, + "requests": 60 + }, + { + "ts": "2026-07-26T18:35:51.502123+00:00", + "status": "up", + "code": 200, + "latency_ms": 768.31, + "requests": 63 + }, + { + "ts": "2026-07-26T18:36:23.231024+00:00", + "status": "up", + "code": 200, + "latency_ms": 849.43, + "requests": 65 + }, + { + "ts": "2026-07-26T18:36:55.246965+00:00", + "status": "up", + "code": 200, + "latency_ms": 948.16, + "requests": 67 + }, + { + "ts": "2026-07-26T18:37:26.935618+00:00", + "status": "up", + "code": 200, + "latency_ms": 905.51, + "requests": 69 + }, + { + "ts": "2026-07-26T18:38:00.920175+00:00", + "status": "up", + "code": 200, + "latency_ms": 2599.19, + "requests": 71 + }, + { + "ts": "2026-07-26T18:38:32.921709+00:00", + "status": "up", + "code": 200, + "latency_ms": 895.96, + "requests": 73 + }, + { + "ts": "2026-07-26T18:39:05.019900+00:00", + "status": "up", + "code": 200, + "latency_ms": 1029.07, + "requests": 75 + }, + { + "ts": "2026-07-26T18:39:39.166081+00:00", + "status": "up", + "code": 200, + "latency_ms": 881.37, + "requests": 77 } ] diff --git a/q2-postmortem/POSTMORTEM.md b/q2-postmortem/POSTMORTEM.md index 862faf7..77e70a8 100644 --- a/q2-postmortem/POSTMORTEM.md +++ b/q2-postmortem/POSTMORTEM.md @@ -130,5 +130,4 @@ Because Ghaymah's public documentation does not establish specific monitoring, a Operational dashboards should show the current release version alongside memory utilization, limit, restart count, termination reason, health status, latency, and error rate. Alerts should route to the on-call notification channel with the workload, container, release, current memory percentage, restart count, and a link to the OOM runbook. - - +The authenticated application page reviewed for this assessment exposes deployment status and application logs, but no documented configurable memory/restart alert controls were established. The implementation should therefore export container runtime metrics and events to an external monitoring system and retain an external `/health` probe unless Ghaymah support enables equivalent account-level controls. diff --git a/q3-cicd/CICD.md b/q3-cicd/CICD.md index 8a98fd1..3f41f9c 100644 --- a/q3-cicd/CICD.md +++ b/q3-cicd/CICD.md @@ -2,137 +2,147 @@ ## Pipeline overview -The workflow builds the API from `q1-deploy-monitor/app`, publishes two Docker Hub tags, deploys the immutable commit tag to a separate staging application, pauses for manual approval, and then deploys the exact same image to production. Promoting the same immutable `${{ github.sha }}` tag prevents staging and production from running artifacts built from different source. +The workflow builds the API from `q1-deploy-monitor/app`, embeds the Git commit +SHA in the image, and publishes immutable SHA and `latest` tags to Docker Hub. +It then starts that immutable image in an isolated GitHub Actions runner, +validates `/health`, pauses at the protected `production` Environment, and +verifies that the approved Ghaymah application is serving the same SHA. ```mermaid flowchart LR A["Commit to main"] --> B["Build image"] B --> C["Push SHA and latest tags
to Docker Hub"] - C --> D["Deploy staging
(automatic)"] - D --> E{"Manual approval
GitHub Environment"} - E -->|Approved| F["Deploy production"] - E -->|Rejected| G["Stop deployment"] + C --> D["Ephemeral staging container
automatic"] + D --> E["Health and release check"] + E --> F{"Production Environment
required reviewer"} + F -->|Approved| G["Ghaymah production"] + G --> H["Verify /health
and exact release SHA"] + F -->|Rejected| I["Stop promotion"] ``` -The workflow runs on: +The free Ghaymah account used for this assessment permits five total resources. +The two required live applications (`ghaymah-api` and `mithal-monitor`) already +consume the available allocation together with their supporting resources. A +third Ghaymah application was tested and rejected by the platform with the +documented account message `Resource limit exceeded ... maximum 5 resources`. +Staging is therefore an ephemeral container on the GitHub-hosted runner. It runs +the exact image that will be promoted, while production remains the real +Ghaymah service. -- A push to `main`. -- A manual `workflow_dispatch` run. +The workflow runs on a push to `main` and through `workflow_dispatch`. It uses: -It uses these GitHub Actions secrets: - -| Secret | Purpose | +| Setting | Purpose | |---|---| -| `DOCKERHUB_USERNAME` | Docker Hub account/namespace used to authenticate and construct the image name. | -| `DOCKERHUB_TOKEN` | Docker Hub access token used to push images. | -| `GHAYMAH_API_TOKEN` | Reserved for the deployment adapter if a supported API or CLI authentication flow is confirmed. | - -Optional repository or environment variables `GHAYMAH_STAGING_APP` and `GHAYMAH_PRODUCTION_APP` select the target applications. The workflow defaults conceptually to `myapp-staging` and `myapp-production`. +| Secret `DOCKERHUB_USERNAME` | Docker Hub account and image namespace. | +| Secret `DOCKERHUB_TOKEN` | Read/write Docker Hub access token. | +| Variable `GHAYMAH_STAGING_APP` | Display name for the ephemeral staging service. | +| Variable `GHAYMAH_PRODUCTION_APP` | Name of the live Ghaymah application. | +| Variable `GHAYMAH_PRODUCTION_URL` | Public base URL used for the production verification. | Images are published as: ```text -docker.io//ghaymah-api: -docker.io//ghaymah-api:latest +docker.io/agamy74/ghaymah-api: +docker.io/agamy74/ghaymah-api:latest ``` -The SHA tag is used for deployment because it is immutable and auditable. `latest` is a convenience tag and should not be the production source of truth. - -Ghaymah's public documentation describes deployment from a container image URL entered in its dashboard: image URL, application name, port, public-access setting, and environment variables are supplied before selecting **Deploy**. A Ghaymah-hosted registry is not assumed. If one becomes available, only the registry login server and image prefix need to change; the build and promotion design remains the same. - -The authenticated dashboard confirms that manual deployment supports a container image URL and an optional **Registry Pull Secret**, and that **External Integrations** currently offers a Docker Hub connection. This validates Docker Hub as the registry used by this workflow; no Ghaymah-hosted registry endpoint or push syntax was exposed in the dashboard reviewed on 2026-07-26. +Only the SHA tag is promoted. The Dockerfile receives `RELEASE_SHA` as a build +argument and `/health` returns it as `release`. The verification script fails if +production is healthy but still serves an older image. ## Manual approval -Manual approval is implemented with a GitHub Environment named `production`. The workflow declares: +The production gate is implemented through a GitHub Environment named +`production`: ```yaml environment: production ``` -Required reviewers are configured in the GitHub repository UI, not in workflow YAML: +Required reviewers are configured in **Settings → Environments → production**, +not in YAML. This repository has `yassinelagamy` configured as the required +reviewer. The workflow pauses after staging and cannot start the production job +until that reviewer approves it. -1. Open **Settings → Environments**. -2. Create or select the `production` environment. -3. Enable the deployment protection rule for required reviewers. -4. Add the people or teams authorized to approve production deployments. -5. Store production-scoped secrets or variables in this environment where appropriate. +Because Ghaymah CLI `0.0.24` does not document non-interactive token +authentication or the external-image update field, promotion currently uses +this controlled procedure: -After staging succeeds, the `deploy-production` job enters a waiting state. The run pauses until an authorized reviewer approves it; rejection prevents the production deployment. The approval protects only production—staging continues to deploy automatically. +1. The workflow builds and verifies the immutable image in staging. +2. An operator updates the `ghaymah-api` image URL in the authenticated Ghaymah + dashboard to the printed SHA tag. +3. The required reviewer approves the GitHub `production` Environment. +4. The production job calls `/health` and succeeds only when `release` equals + the workflow commit SHA. + +This is intentionally a manual promotion with automated verification. It does +not claim that an undocumented command deployed the application. ## Staging vs Production -Staging and production should be two separate Ghaymah applications, such as `myapp-staging` and `myapp-production`, with independent configuration and environment variables. - | Area | Staging | Production | |---|---|---| -| Purpose | Validate the release in a production-like environment before promotion. | Serve the live customer workload. | -| Data | Synthetic, anonymized, or disposable test data. | Real customer/business data governed by retention and privacy controls. | -| Scale/replicas | Smaller footprint; enough replicas for functional and targeted load tests. | Sized for peak traffic, resilience, and operational headroom. | -| Secrets | Staging-only credentials with limited permissions. | Production-only credentials, tightly scoped and independently rotated. | -| Access control | Engineering and QA access; may be restricted from the public internet. | Least-privilege operational access; public access only where the service requires it. | -| Alerting thresholds | Useful for validation but may be less sensitive or routed to non-paging channels. | SLO-based thresholds with paging for user-impacting failures. | -| Deploy cadence | Automatic after each successful build from `main`. | Only after staging succeeds and a reviewer approves the deployment. | -| Who can approve | No approval required for this pipeline. | Reviewers assigned to the GitHub `production` Environment. | +| Purpose | Validate the exact candidate image before promotion. | Serve the public workload on Ghaymah. | +| Data | Synthetic or disposable test data. | Real application data and traffic. | +| Scale/replicas | One short-lived CI container for this free-tier assessment. | Ghaymah instance sized for availability and measured demand. | +| Secrets | Only build/test credentials; no production secrets. | Production-only values stored in protected platform or environment settings. | +| Access control | Reachable only inside the GitHub Actions runner. | Public endpoint; administrative access restricted to the Ghaymah account. | +| Alerting thresholds | Fast feedback on startup, health, and release identity. | SLO-based uptime, latency, memory, restart, and OOM alerts. | +| Deploy cadence | Automatic on each push to `main`. | Manual promotion after successful staging. | +| Who can approve | No reviewer required. | Required reviewer on the GitHub `production` Environment. | -Both applications receive the same immutable image tag, while their data, secrets, scale, access rules, and environment variables remain isolated. +On a paid allocation, staging should be a second Ghaymah application with its +own variables, secrets, capacity, and URL. The image and release verification +steps remain unchanged. ## Ghaymah CLI integration -### Verified public CLI commands - -Ghaymah now publicly documents the `gy` CLI, including installation, interactive login, project management, Dockerfile-based application initialization and launch, and log retrieval. The currently documented flow is: +The verified public CLI installation and inspection commands are: ```bash +# Ghaymah-specific installation command confirmed from the official documentation. curl -sSL https://cli.ghaymah.systems/install.sh | bash -source ~/.bashrc + +# Ghaymah-specific commands confirmed with CLI 0.0.24. gy version -gy auth login +gy auth login --email '' --password '' gy auth status gy resource project get -gy resource app init --project-id --name -gy resource app launch [PATH] +gy resource app init --project-id '' --name '' +gy resource app launch . gy resource app logs -gy resource app update --set '' +gy resource app update '' --set '' ``` -`app init` creates `.ghaymah.json`; the documented example includes the application ID/name, project ID, exposed port, public-access configuration, resource tier, and Dockerfile name. `app launch` builds the local Dockerfile and deploys that application. +The workflow installs the CLI and runs `gy version` in both deployment jobs so +the integration dependency is continuously checked. It does not place a user +password in CI. Public CLI help for version `0.0.24` exposes email/password +login, but no API-token flag, and its generic `app update --set` help does not +identify the field for an externally built container image. -Official sources: +The authenticated dashboard does provide a verified deployment route: enter a +container image URL, application name, port, public-access choice, environment +variables, and an optional registry pull secret, then deploy or update the +application. That dashboard route is used for production promotion until +Ghaymah publishes a service-account/token flow and an external-image update +schema. + +`scripts/verify_deployment.sh` is the CI boundary after promotion: + +```bash +bash scripts/verify_deployment.sh \ + --app "$GHAYMAH_APP_NAME" \ + --url "$GHAYMAH_PRODUCTION_URL" \ + --expected-release "$GITHUB_SHA" +``` + +It retries `/health`, requires `status=ok`, compares the live `release` to the +approved SHA, writes evidence to the GitHub job summary, and exits nonzero for +an unavailable or stale deployment. + +Official references: - [Ghaymah CLI documentation](https://ghaymah.systems/docs) - [Ghaymah CLI overview](https://ghaymah.systems/cli) - -### CI integration gap - -Direct inspection of CLI version `0.0.24` on 2026-07-26 established that `gy auth login` accepts `--email` and `--password`; it does not expose an API-token flag. It also established that `gy resource app update ` accepts JSON input or dot notation through `--set`. - -The public documentation and CLI help still do **not** specify: - -- A `GHAYMAH_API_TOKEN` login flow suitable for an ephemeral GitHub Actions runner. -- The JSON field used to update an existing application to a specific externally built image URL/tag. -- A login server and push commands for a Ghaymah-hosted registry; the authenticated dashboard instead exposes Docker Hub integration. - - - -Consequently, the workflow safely builds and pushes the immutable Docker Hub image, installs the documented CLI, prints the exact deployment target, and calls `scripts/ghaymah_deploy.sh`. The adapter deliberately exits unsuccessfully after producing a clear deployment handoff; it never reports success for a deployment that did not occur. - -Until the missing CI syntax is confirmed, the image deployment procedure is: - -1. Open the target Ghaymah application in the dashboard. -2. Update its container image URL to `docker.io//ghaymah-api:`. -3. Confirm the application name, exposed port, public-access setting, and environment variables. -4. Select **Deploy** and validate application health. -5. Repeat for production only after the GitHub Environment approval. - -The staging and production workflow steps print the exact SHA-tagged image and target application, then call: - - - -```bash -bash scripts/ghaymah_deploy.sh \ - --app "$GHAYMAH_APP_NAME" \ - --image "$DEPLOY_IMAGE" -``` - -The workflow supplies `GHAYMAH_API_TOKEN` through the environment and never places it in a command-line argument. Once Ghaymah confirms the required syntax, the adapter should authenticate without logging the token, update the application's image URL, wait for rollout completion, validate `/health`, and return a nonzero exit status if deployment or health validation fails. +- [Docker login action](https://github.com/docker/login-action) +- [Docker build and push action](https://github.com/docker/build-push-action) diff --git a/q4-scalability/SCALABILITY.md b/q4-scalability/SCALABILITY.md index 30db2ec..233bd11 100644 --- a/q4-scalability/SCALABILITY.md +++ b/q4-scalability/SCALABILITY.md @@ -103,12 +103,10 @@ This design does not clone and concurrently mount one writable volume across rep ### Backup, performance, and capacity -Crash-consistent or application-consistent volume snapshots can provide a backup building block, but database-aware backups and restore tests are still required. A snapshot should be coordinated with PostgreSQL or combined with its write-ahead log archive so the recovery point is valid. - - +Crash-consistent or application-consistent volume snapshots can provide a backup building block, but database-aware backups and restore tests are still required. A snapshot should be coordinated with PostgreSQL or combined with its write-ahead log archive so the recovery point is valid. Snapshot support, retention, encryption, restore behavior, and cross-zone availability were not specified in the dashboard or public documentation reviewed for this assessment; they must be confirmed with Ghaymah before being used as the backup design. The authenticated Ghaymah dashboard displays a supported volume-size range of **50 MiB minimum to 10 GiB maximum** and allows a volume to be attached from an application's advanced deployment options. Volume sizing must account for the live dataset, indexes, temporary files, write-ahead logs, maintenance operations, expected growth, and free-space safety margin. Performance planning must account for sustained and burst IOPS, throughput, latency, queue depth, and the read/write mix. Load tests should verify database latency at expected peak traffic rather than selecting capacity from size alone. - +The reviewed interfaces also did not publish resize behavior, volume classes, IOPS/throughput guarantees, attachment limits, access modes, or zone-binding rules. Production sizing must treat these as procurement questions for Ghaymah support and validate the answers with load and restore testing. Block Storage makes container replacement compatible with durable state, but it does not replace database replication, tested backups, point-in-time recovery, or a documented failover procedure. diff --git a/screenshots/ghaymah-project-created.png b/screenshots/ghaymah-project-created.png deleted file mode 100644 index 214caf5..0000000 Binary files a/screenshots/ghaymah-project-created.png and /dev/null differ diff --git a/screenshots/q1-ghaymah-service.png b/screenshots/q1-ghaymah-service.png deleted file mode 100644 index b5b36ac..0000000 Binary files a/screenshots/q1-ghaymah-service.png and /dev/null differ diff --git a/scripts/ghaymah_deploy.sh b/scripts/ghaymah_deploy.sh deleted file mode 100644 index f30f315..0000000 --- a/scripts/ghaymah_deploy.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Deployment adapter for the GitHub Actions workflow. -# -# Ghaymah publicly documents `gy resource app launch` for deploying a -# local Dockerfile/.ghaymah.json project. It does not currently document a -# non-interactive API-token login command. The current binary exposes -# email/password login flags and a generic `app update` command, but its public -# help does not identify an external-image field. This adapter therefore -# validates and prints the exact deployment handoff, then fails deliberately -# instead of reporting a deployment that did not happen. - -app="" -image="" - -usage() { - echo "Usage: $0 --app --image " >&2 -} - -while [[ $# -gt 0 ]]; do - case "$1" in - --app) - [[ $# -ge 2 ]] || { usage; exit 64; } - app="$2" - shift 2 - ;; - --image) - [[ $# -ge 2 ]] || { usage; exit 64; } - image="$2" - shift 2 - ;; - *) - echo "Unknown argument: $1" >&2 - usage - exit 64 - ;; - esac -done - -[[ -n "$app" && -n "$image" ]] || { usage; exit 64; } - -echo "Ghaymah deployment handoff" -echo " Application: $app" -echo " Image URL: $image" -echo -echo "Update the target application's container image URL to the immutable tag" -echo "shown above in the Ghaymah dashboard, then validate its health endpoint." -echo -echo "Automated deployment is intentionally blocked because the public CLI docs" -echo "do not specify API-token authentication or the external-image field for" -echo "'gy resource app update'. Replace this adapter only with confirmed syntax." - -if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then - { - echo "### Ghaymah deployment requires confirmed integration" - echo - echo "- Application: \`$app\`" - echo "- Image: \`$image\`" - echo - echo "The image was built and pushed, but no deployment was claimed. Confirm" - echo "the supported API/CLI command or update this image URL in the dashboard." - } >> "$GITHUB_STEP_SUMMARY" -fi - -exit 78 diff --git a/scripts/verify_deployment.sh b/scripts/verify_deployment.sh new file mode 100644 index 0000000..d4c057c --- /dev/null +++ b/scripts/verify_deployment.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Verify that a deployed service is healthy and exposes the expected immutable +# release identifier. This prevents a manually promoted Ghaymah deployment from +# being reported as successful when it is still serving an older image. + +app="" +url="" +expected_release="" +attempts="${DEPLOY_CHECK_ATTEMPTS:-30}" +interval_s="${DEPLOY_CHECK_INTERVAL_S:-2}" + +usage() { + echo "Usage: $0 --app --url --expected-release " >&2 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --app) + [[ $# -ge 2 ]] || { usage; exit 64; } + app="$2" + shift 2 + ;; + --url) + [[ $# -ge 2 ]] || { usage; exit 64; } + url="${2%/}" + shift 2 + ;; + --expected-release) + [[ $# -ge 2 ]] || { usage; exit 64; } + expected_release="$2" + shift 2 + ;; + *) + echo "Unknown argument: $1" >&2 + usage + exit 64 + ;; + esac +done + +[[ -n "$app" && -n "$url" && -n "$expected_release" ]] || { usage; exit 64; } +[[ "$attempts" =~ ^[1-9][0-9]*$ ]] || { echo "DEPLOY_CHECK_ATTEMPTS must be a positive integer" >&2; exit 64; } +[[ "$interval_s" =~ ^[0-9]+$ ]] || { echo "DEPLOY_CHECK_INTERVAL_S must be a non-negative integer" >&2; exit 64; } + +health_url="${url}/health" +echo "Verifying ${app} at ${health_url}" + +for ((attempt = 1; attempt <= attempts; attempt++)); do + body="$(curl --fail --silent --show-error --max-time 5 "$health_url" 2>/dev/null || true)" + + if [[ -n "$body" ]]; then + status="$(sed -n 's/.*"status"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' <<<"$body")" + release="$(sed -n 's/.*"release"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' <<<"$body")" + + if [[ "$status" == "ok" && "$release" == "$expected_release" ]]; then + echo "Deployment verified: status=ok release=${release}" + if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then + { + echo "### Deployment verified" + echo + echo "- Application: \`${app}\`" + echo "- URL: ${url}" + echo "- Release: \`${release}\`" + } >>"$GITHUB_STEP_SUMMARY" + fi + exit 0 + fi + + echo "Attempt ${attempt}/${attempts}: status=${status:-missing}, release=${release:-missing}" + else + echo "Attempt ${attempt}/${attempts}: health endpoint unavailable" + fi + + ((attempt == attempts)) || sleep "$interval_s" +done + +echo "Deployment verification failed for ${app}: expected release ${expected_release}" >&2 +exit 1