diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml index 5213b63..e33a9f2 100644 --- a/.github/workflows/cicd.yaml +++ b/.github/workflows/cicd.yaml @@ -74,4 +74,5 @@ jobs: run: $HOME/ghaymah/bin/gy auth login --email "${{ secrets.GHAYMAH_EMAIL }}" --password "${{ secrets.GHAYMAH_PW }}" - name: 🚀 Deploy Application - run: $HOME/ghaymah/bin/gy resource app launch + run: $HOME/ghaymah/bin/gy resource app launch + diff --git a/.gitignore b/.gitignore index 1795c8d..8f5fd87 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -.history \ No newline at end of file +.history diff --git a/CICDpipeline.sh b/CICDpipeline.sh index 2497629..4e8e188 100755 --- a/CICDpipeline.sh +++ b/CICDpipeline.sh @@ -66,18 +66,18 @@ echo "✅ Image pushed: ${DOCKER_USERNAME}/${IMAGENAME}:${new_tag}" # Ghaymah deployment if command -v gy &> /dev/null; then echo "☁️ Setting up Ghaymah deployment..." - + # Create project with the project name echo "🔧 Creating project: ${project_name}" project_id=$(gy resource project create --set .name=${project_name} | awk '/ID:/ {print $NF}') - + if [ -z "$project_id" ]; then echo "❌ Failed to get project ID" exit 1 fi - + echo "✅ Project ID: ${project_id}" - + # Create .ghaymah.json configuration using both names cat > .ghaymah.json < /dev/null; then "resourceTier": "t1" } EOF - + echo "✅ Configuration file created:" echo " - Project: ${project_name} (ID: ${project_id})" echo " - App: ${app_name}" echo " - Image: ${DOCKER_USERNAME}/${IMAGENAME}:${new_tag}" - + # Deploy to Ghaymah echo "🚀 Deploying to Ghaymah..." gy resource app launch - + echo "🎉 Deployment successful!" echo "📊 Project: ${project_name}" echo "📱 App: ${app_name}" diff --git a/Dockerfile b/Dockerfile index 6e77ce3..f0fbcd7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,5 +15,4 @@ RUN echo 'Weather App

EXPOSE 5000 -CMD ["python", "app.py"] - +CMD ["python", "app.py"] diff --git a/README.md b/README.md index b5b0c20..39f79e6 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,24 @@ # 🌤️ Complete GitHub Actions CI/CD Weather Forecast App on Ghaymah Cloud -A simple modern **Flask-based weather forecast application** that fetches real-time weather data from [OpenWeatherMap](https://openweathermap.org/) and provides a beautiful, responsive UI. This project is **Dockerized** and supports **CI/CD deployment** to **Ghaymah Cloud** using the included shell script. +A simple modern **Flask-based weather forecast application** that fetches real-time weather data from [OpenWeatherMap](https://openweathermap.org/) and provides a beautiful, responsive UI. This project is **Dockerized** and supports **CI/CD deployment** to **Ghaymah Cloud** using the included shell script. -## 🚀 Features +## 🚀 Features -- 🌍 Search weather for any city worldwide -- 🌡️ Shows temperature, feels-like, humidity, pressure, wind speed, and visibility -- 🌅 Displays sunrise and sunset times -- 📊 Auto-updating with timestamp -- 🎨 Responsive and modern UI (HTML, CSS, JS embedded in Flask) -- 🐳 Dockerized for portability -- ☁️ CI/CD ready with Ghaymah Cloud +- 🌍 Search weather for any city worldwide +- 🌡️ Shows temperature, feels-like, humidity, pressure, wind speed, and visibility +- 🌅 Displays sunrise and sunset times +- 📊 Auto-updating with timestamp +- 🎨 Responsive and modern UI (HTML, CSS, JS embedded in Flask) +- 🐳 Dockerized for portability +- ☁️ CI/CD ready with Ghaymah Cloud -## 📂 Project Structure +## 📂 Project Structure ``` . -├── .github/workflows/cicd.yaml # GitHub Actions CI/CD workflow for my App +├── .github/workflows/cicd.yaml # GitHub Actions CI/CD workflow for my App ├── app.py # Flask application with weather API integration ├── CICDpipeline.sh # CI/CD pipeline script for Docker + Ghaymah Cloud ├── Dockerfile # Dockerfile to containerize the Flask app @@ -27,14 +27,14 @@ A simple modern **Flask-based weather forecast application** that fetches real- └── README.md # Documentation ``` -## ⚙️ Prerequisites +## ⚙️ Prerequisites -Before running or deploying the app, ensure you have: +Before running or deploying the app, ensure you have: -- [Python 3.9+](https://www.python.org/downloads/) -- [Docker](https://docs.docker.com/get-docker/) -- [Ghaymah CLI (`gy`)](https://docs.ghaymah.io) installed and configured -- An [OpenWeatherMap API key](https://home.openweathermap.org/api_keys) +- [Python 3.9+](https://www.python.org/downloads/) +- [Docker](https://docs.docker.com/get-docker/) +- [Ghaymah CLI (`gy`)](https://docs.ghaymah.io) installed and configured +- An [OpenWeatherMap API key](https://home.openweathermap.org/api_keys) @@ -91,16 +91,16 @@ Before running or deploying the app, ensure you have: -## 🛠️ Local Development +## 🛠️ Local Development -1. Clone this repository: +1. Clone this repository: ```bash git clone https://github.com/your-username/weather-app.git cd weather-app ``` -2. Create and activate a virtual environment: +2. Create and activate a virtual environment: ```bash python -m venv venv @@ -108,71 +108,71 @@ Before running or deploying the app, ensure you have: venv\Scripts\activate # Windows ``` -3. Install dependencies: +3. Install dependencies: ```bash pip install -r requirements.txt ``` -4. Run the app: +4. Run the app: ```bash python app.py ``` -5. Open your browser at: +5. Open your browser at: 👉 http://127.0.0.1:5000 -## 🐳 Running with Docker +## 🐳 Running with Docker -1. Build the Docker image: +1. Build the Docker image: ```bash docker build -t weather-app:1 . ``` -2. Run the container: +2. Run the container: ```bash docker run -p 5000:5000 weather-app:1 ``` -3. Visit the app at: +3. Visit the app at: 👉 http://localhost:5000 -# ☁️ Deployment to Ghaymah Cloud uisng **CICDpipeline.sh** Script +# ☁️ Deployment to Ghaymah Cloud uisng **CICDpipeline.sh** Script -This project includes a **CICDpipeline.sh** script that: -- Builds and tags Docker images -- Pushes them to Docker Hub -- Creates a `.ghaymah.json` config -- Deploys the app to **Ghaymah Cloud** +This project includes a **CICDpipeline.sh** script that: +- Builds and tags Docker images +- Pushes them to Docker Hub +- Creates a `.ghaymah.json` config +- Deploys the app to **Ghaymah Cloud** -### 🔧 Steps +### 🔧 Steps -1. Make the script executable: +1. Make the script executable: ```bash chmod +x CICDpipeline.sh ``` -2. Run the deployment script: +2. Run the deployment script: ```bash ./CICDpipeline.sh ``` -3. Follow the prompts: - - Enter your **Ghaymah Project Name** - - Enter your **App Name** +3. Follow the prompts: + - Enter your **Ghaymah Project Name** + - Enter your **App Name** -4. If Ghaymah CLI is installed, the app will be deployed automatically. +4. If Ghaymah CLI is installed, the app will be deployed automatically. -✅ Your app will be accessible at: +✅ Your app will be accessible at: ``` https://.hosted.ghaymah.systems @@ -180,5 +180,3 @@ https://.hosted.ghaymah.systems ## 🎥 Project Record [Watch the Demo](https://app.gitpasha.com/AhmedGamalYousef/Complete-CI-CD-Project-pipeline-on-Ghaymah-Cloud/src/branch/main/demo/CICDonGhaymahCloud.mp4) - - diff --git a/app.py b/app.py index 778745f..abeb4ba 100644 --- a/app.py +++ b/app.py @@ -1,13 +1,14 @@ -from flask import Flask, request, jsonify, render_template_string -import requests import os from datetime import datetime +import requests +from flask import Flask, jsonify, render_template_string, request + app = Flask(__name__) # Get API key from environment variable (preferred) or use demo key -API_KEY = os.getenv('API_KEY', '6cf356ceb2ec0ff941855d4a43144e0e') -BASE_URL = 'https://api.openweathermap.org/data/2.5/weather' +API_KEY = os.getenv("API_KEY", "6cf356ceb2ec0ff941855d4a43144e0e") +BASE_URL = "https://api.openweathermap.org/data/2.5/weather" # HTML Template with embedded CSS and JavaScript - MUST BE DEFINED BEFORE ROUTES HTML_TEMPLATE = """ @@ -24,7 +25,7 @@ HTML_TEMPLATE = """ box-sizing: border-box; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } - + body { background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%); min-height: 100vh; @@ -33,7 +34,7 @@ HTML_TEMPLATE = """ align-items: center; padding: 20px; } - + .container { max-width: 800px; width: 100%; @@ -42,29 +43,29 @@ HTML_TEMPLATE = """ box-shadow: 0 15px 30px rgba(0, 0, 0, 0.1); overflow: hidden; } - + .header { background: #0984e3; color: white; padding: 30px; text-align: center; } - + .header h1 { font-size: 2.5rem; margin-bottom: 10px; } - + .header p { opacity: 0.9; } - + .search-container { padding: 30px; display: flex; gap: 10px; } - + .search-input { flex: 1; padding: 15px 20px; @@ -74,11 +75,11 @@ HTML_TEMPLATE = """ outline: none; transition: border-color 0.3s; } - + .search-input:focus { border-color: #0984e3; } - + .search-btn { background: #0984e3; color: white; @@ -90,27 +91,27 @@ HTML_TEMPLATE = """ font-weight: 600; transition: background 0.3s; } - + .search-btn:hover { background: #0770c4; } - + .weather-info { padding: 0 30px 30px; display: none; } - + .current-weather { text-align: center; margin-bottom: 30px; } - + .city-name { font-size: 2rem; margin-bottom: 10px; color: #2d3436; } - + .weather-main { display: flex; align-items: center; @@ -118,49 +119,49 @@ HTML_TEMPLATE = """ gap: 20px; margin: 20px 0; } - + .temperature { font-size: 4rem; font-weight: 300; color: #0984e3; } - + .weather-icon { width: 100px; height: 100px; } - + .description { font-size: 1.5rem; color: #636e72; margin-bottom: 20px; } - + .details { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; } - + .detail-card { background: #f8f9fa; padding: 20px; border-radius: 10px; text-align: center; } - + .detail-title { font-size: 0.9rem; color: #636e72; margin-bottom: 5px; } - + .detail-value { font-size: 1.5rem; font-weight: 600; color: #2d3436; } - + .error-message { background: #ff7675; color: white; @@ -170,13 +171,13 @@ HTML_TEMPLATE = """ margin: 0 30px 30px; display: none; } - + .loading { text-align: center; padding: 30px; display: none; } - + .spinner { border: 5px solid #f3f3f3; border-top: 5px solid #0984e3; @@ -186,12 +187,12 @@ HTML_TEMPLATE = """ animation: spin 1s linear infinite; margin: 0 auto 20px; } - + @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } - + .footer { text-align: center; padding: 20px; @@ -199,20 +200,20 @@ HTML_TEMPLATE = """ font-size: 0.9rem; border-top: 1px solid #eee; } - + @media (max-width: 600px) { .header h1 { font-size: 2rem; } - + .search-container { flex-direction: column; } - + .temperature { font-size: 3rem; } - + .weather-icon { width: 80px; height: 80px; @@ -226,19 +227,19 @@ HTML_TEMPLATE = """

Weather Forecast

Get real-time weather information for any city worldwide

- +
- +
- +

Fetching weather data...

- +

-

@@ -248,7 +249,7 @@ HTML_TEMPLATE = """

-

- +
Feels Like
@@ -276,10 +277,10 @@ HTML_TEMPLATE = """
- + @@ -290,7 +291,7 @@ HTML_TEMPLATE = """ const weatherInfo = document.getElementById('weatherInfo'); const errorMessage = document.getElementById('errorMessage'); const loading = document.getElementById('loading'); - + // Elements to update const cityName = document.getElementById('cityName'); const temperature = document.getElementById('temperature'); @@ -303,21 +304,21 @@ HTML_TEMPLATE = """ const visibility = document.getElementById('visibility'); const sunriseSunset = document.getElementById('sunriseSunset'); const timestamp = document.getElementById('timestamp'); - + // Search weather function function searchWeather() { const city = cityInput.value.trim(); - + if (!city) { showError('Please enter a city name'); return; } - + // Show loading, hide weather and error loading.style.display = 'block'; weatherInfo.style.display = 'none'; errorMessage.style.display = 'none'; - + // Fetch weather data fetch(`/weather?city=${encodeURIComponent(city)}`) .then(response => { @@ -342,7 +343,7 @@ HTML_TEMPLATE = """ visibility.textContent = `${(data.visibility / 1000).toFixed(1)} km`; sunriseSunset.textContent = `${data.sunrise} / ${data.sunset}`; timestamp.textContent = data.timestamp; - + // Show weather info, hide loading weatherInfo.style.display = 'block'; loading.style.display = 'none'; @@ -352,23 +353,23 @@ HTML_TEMPLATE = """ loading.style.display = 'none'; }); } - + // Show error message function showError(message) { errorMessage.textContent = message; errorMessage.style.display = 'block'; weatherInfo.style.display = 'none'; } - + // Event listeners searchBtn.addEventListener('click', searchWeather); - + cityInput.addEventListener('keypress', function(e) { if (e.key === 'Enter') { searchWeather(); } }); - + // Try to get weather for a default city on load cityInput.value = 'London'; searchWeather(); @@ -378,33 +379,38 @@ HTML_TEMPLATE = """ """ + def get_weather(city): """Fetch weather data from OpenWeatherMap API""" try: url = f"{BASE_URL}?q={city}&appid={API_KEY}&units=metric" response = requests.get(url, timeout=10) response.raise_for_status() # Raises an HTTPError for bad responses - + data = response.json() - main = data['main'] - weather = data['weather'][0] - sys = data.get('sys', {}) - + main = data["main"] + weather = data["weather"][0] + sys = data.get("sys", {}) + return { - 'city': data['name'], - 'country': sys.get('country', ''), - 'temperature': round(main['temp']), - 'feels_like': round(main['feels_like']), - 'description': weather['description'].title(), - 'humidity': main['humidity'], - 'pressure': main['pressure'], - 'wind_speed': round(data['wind']['speed'], 1), - 'wind_deg': data['wind'].get('deg', 0), - 'visibility': data.get('visibility', 0), - 'icon': weather['icon'], - 'sunrise': datetime.fromtimestamp(sys.get('sunrise', 0)).strftime('%H:%M') if sys.get('sunrise') else 'N/A', - 'sunset': datetime.fromtimestamp(sys.get('sunset', 0)).strftime('%H:%M') if sys.get('sunset') else 'N/A', - 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + "city": data["name"], + "country": sys.get("country", ""), + "temperature": round(main["temp"]), + "feels_like": round(main["feels_like"]), + "description": weather["description"].title(), + "humidity": main["humidity"], + "pressure": main["pressure"], + "wind_speed": round(data["wind"]["speed"], 1), + "wind_deg": data["wind"].get("deg", 0), + "visibility": data.get("visibility", 0), + "icon": weather["icon"], + "sunrise": datetime.fromtimestamp(sys.get("sunrise", 0)).strftime("%H:%M") + if sys.get("sunrise") + else "N/A", + "sunset": datetime.fromtimestamp(sys.get("sunset", 0)).strftime("%H:%M") + if sys.get("sunset") + else "N/A", + "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), } except requests.exceptions.RequestException as e: print(f"API request failed: {e}") @@ -413,29 +419,33 @@ def get_weather(city): print(f"Error parsing weather data: {e}") return None -@app.route('/') + +@app.route("/") def home(): return render_template_string(HTML_TEMPLATE) -@app.route('/weather', methods=['GET', 'POST']) + +@app.route("/weather", methods=["GET", "POST"]) def weather(): - if request.method == 'POST': - city = request.form.get('city') + if request.method == "POST": + city = request.form.get("city") else: - city = request.args.get('city') - + city = request.args.get("city") + if not city: - return jsonify({'error': 'City parameter is required'}), 400 - + return jsonify({"error": "City parameter is required"}), 400 + weather_data = get_weather(city) if weather_data: return jsonify(weather_data) else: - return jsonify({'error': 'City not found or API request failed'}), 404 + return jsonify({"error": "City not found or API request failed"}), 404 -@app.route('/health') + +@app.route("/health") def health(): - return jsonify({'status': 'healthy', 'timestamp': datetime.now().isoformat()}) + return jsonify({"status": "healthy", "timestamp": datetime.now().isoformat()}) -if __name__ == '__main__': - app.run(host='0.0.0.0', port=5000, debug=False) + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000, debug=False)