jebin2 commited on
Commit
44d1e44
·
1 Parent(s): c163f91

refactor: migrate the web application from Flask to FastAPI.

Browse files
Dockerfile CHANGED
@@ -29,5 +29,5 @@ RUN pip install --no-cache-dir -r github_workflow_app/requirements.txt
29
  # Expose the port
30
  EXPOSE 7860
31
 
32
- # Run the Flask app
33
  CMD ["uvicorn", "github_workflow_app.app:app", "--host", "0.0.0.0", "--port", "7860"]
 
29
  # Expose the port
30
  EXPOSE 7860
31
 
32
+ # Run the FastAPI app with uvicorn
33
  CMD ["uvicorn", "github_workflow_app.app:app", "--host", "0.0.0.0", "--port", "7860"]
github_workflow_app/app.py CHANGED
@@ -1,18 +1,26 @@
1
- from flask import Flask, jsonify, request, send_from_directory
 
 
 
 
2
  import os
3
  import subprocess
4
  import dotenv
5
  import json
6
 
7
- app = Flask(__name__, static_folder='.')
8
-
9
- # Load environment variables to check for GITHUB_TOKEN if present
10
  dotenv.load_dotenv(os.path.join(os.path.dirname(__file__), '..', '.env'))
11
 
12
  import sys
13
  sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
14
  from src.config import get_config_value
15
 
 
 
 
 
 
 
16
  def get_github_token_from_git_credentials():
17
  """Parse ~/.git-credentials to extract GitHub token"""
18
  git_credentials_path = os.path.expanduser('~/.git-credentials')
@@ -25,15 +33,10 @@ def get_github_token_from_git_credentials():
25
  line = line.strip()
26
  # Format: https://username:token@github.com
27
  if 'github.com' in line and ':' in line and '@' in line:
28
- # Extract the token part between : and @
29
- # https://user:token@github.com
30
  try:
31
- # Split by @ first to get the credentials part
32
- creds_part = line.split('@')[0] # https://user:token
33
- # Remove the protocol
34
  if '://' in creds_part:
35
- creds_part = creds_part.split('://')[1] # user:token
36
- # Split by : to get username and token
37
  parts = creds_part.split(':')
38
  if len(parts) >= 2:
39
  token = parts[1]
@@ -46,35 +49,28 @@ def get_github_token_from_git_credentials():
46
 
47
  return None
48
 
 
49
  # Configuration - Priority: env var > git-credentials
50
  GITHUB_TOKEN = get_config_value("GITHUB_TOKEN") or get_config_value("GITHUB_PAT") or get_github_token_from_git_credentials()
51
  REPO_OWNER = "ElvoroLtd"
52
  REPO_NAME = "Elvoro"
53
  WORKFLOW_FILE = "process_csv.yml"
54
 
55
- @app.route('/')
56
- def index():
57
- return send_from_directory('.', 'index.html')
58
 
59
- @app.route('/api/auth/status')
60
- def auth_status():
 
 
 
 
 
61
  """Check if we have a valid token by running `gh auth status` or similar"""
62
- # If GITHUB_TOKEN is in env, gh should Pick it up automatically.
63
- # We can check by running a simple gh command.
64
-
65
- token = None
66
- if GITHUB_TOKEN:
67
- token = GITHUB_TOKEN
68
-
69
- # We can't easily access the client-side token here for the check unless passed,
70
- # but the frontend Logic handles the "Using Local Token" vs "Logged in" display.
71
- # Here we just check the server-side env.
72
 
73
  if not token:
74
- return jsonify({'authenticated': False, 'message': 'Token not found in .env'})
75
 
76
  try:
77
- # Run a simple check
78
  env = os.environ.copy()
79
  env['GITHUB_TOKEN'] = token
80
  cmd = ['gh', 'api', 'user']
@@ -82,14 +78,15 @@ def auth_status():
82
 
83
  if result.returncode == 0:
84
  user_data = json.loads(result.stdout)
85
- return jsonify({'authenticated': True, 'user': user_data.get('login')})
86
  else:
87
- return jsonify({'authenticated': False, 'message': 'Invalid token'})
88
  except Exception as e:
89
- return jsonify({'authenticated': False, 'message': str(e)})
90
 
91
- @app.route('/api/env-vars')
92
- def get_env_vars():
 
93
  """
94
  Parse env file based on selected workflow.
95
  Strategy:
@@ -97,28 +94,22 @@ def get_env_vars():
97
  2. Load keys from template file (publisher.env / video_generate.env)
98
  3. Return keys from template populated with values from .env
99
  """
100
- workflow = request.args.get('workflow', 'process_csv.yml')
101
-
102
  # 1. Load Source of Truth (.env)
103
  root_dir = os.path.join(os.path.dirname(__file__), '..')
104
  dotenv_path = os.path.join(root_dir, '.env')
105
-
106
- # Load into a dict, preserving existing os.environ but prioritizing .env file content for display
107
- # dotenv_values returns a dict of values defined in the file
108
  actual_values = dotenv.dotenv_values(dotenv_path)
109
 
110
  # 2. Determine Template File
111
  if workflow == 'publisher.yml':
112
  template_filename = 'publisher.env'
113
  else:
114
- # For process_csv.yml, use video_generate.env as the template
115
  template_filename = 'video_generate.env'
116
 
117
  template_path = os.path.join(root_dir, template_filename)
118
 
119
  vars_dict = {}
120
 
121
- # 3. specific keys from template, populate with actual values
122
  if os.path.exists(template_path):
123
  try:
124
  with open(template_path, 'r') as f:
@@ -127,37 +118,38 @@ def get_env_vars():
127
  if not line or line.startswith('#'):
128
  continue
129
 
130
- # support both "KEY=VALUE" and just "KEY"
131
  if '=' in line:
132
  key = line.split('=')[0].strip()
133
  else:
134
  key = line.strip()
135
 
136
- # Populate with value from .env, or empty if not set
137
  vars_dict[key] = actual_values.get(key, '')
138
 
139
  except Exception as e:
140
  print(f"Error reading template {template_filename}: {e}")
141
  else:
142
- # Fallback if template missing: just return everything from .env
143
- vars_dict = actual_values
 
 
144
 
145
- return jsonify({'vars': vars_dict})
 
 
 
 
146
 
147
- @app.route('/api/trigger', methods=['POST'])
148
- def trigger_workflow():
149
- data = request.json
150
- token = data.get('token') or GITHUB_TOKEN
151
 
152
  if not token:
153
- return jsonify({'success': False, 'message': 'No GitHub token provided'}), 401
154
 
155
- inputs = data.get('inputs', {})
156
- ref = data.get('ref', 'feature/video-revamp')
157
- workflow_file = data.get('workflow', 'process_csv.yml')
158
-
159
- # Construct input flags for gh
160
- # gh workflow run <workflow_file> --ref <ref> -f key=value -f key2=value2
161
 
162
  cmd = [
163
  'gh', 'workflow', 'run', workflow_file,
@@ -166,7 +158,7 @@ def trigger_workflow():
166
  ]
167
 
168
  for key, value in inputs.items():
169
- if value: # Only add if value is not empty
170
  cmd.extend(['-f', f"{key}={value}"])
171
 
172
  try:
@@ -178,27 +170,30 @@ def trigger_workflow():
178
  result = subprocess.run(cmd, capture_output=True, text=True, env=env)
179
 
180
  if result.returncode == 0:
181
- return jsonify({'success': True, 'message': 'Workflow triggered successfully'})
182
  else:
183
- return jsonify({'success': False, 'message': f'Failed: {result.stderr}'}), 400
184
 
 
 
185
  except Exception as e:
186
- return jsonify({'success': False, 'message': str(e)}), 500
 
187
 
188
- @app.route('/api/runs')
189
- def list_runs():
 
 
 
190
  """List recent workflow runs using gh"""
191
- token = request.args.get('token') or GITHUB_TOKEN
192
- workflow_file = request.args.get('workflow', 'process_csv.yml')
193
 
194
  if not token:
195
- return jsonify({'runs': []})
196
-
197
- # gh run list --workflow <workflow_file> --repo owner/repo --limit 5 --json databaseId,status,conclusion,createdAt,url,name
198
-
199
  cmd = [
200
  'gh', 'run', 'list',
201
- '--workflow', workflow_file,
202
  '--repo', f"{REPO_OWNER}/{REPO_NAME}",
203
  '--limit', '5',
204
  '--json', 'number,status,conclusion,createdAt,url,name'
@@ -212,10 +207,6 @@ def list_runs():
212
 
213
  if result.returncode == 0:
214
  runs = json.loads(result.stdout)
215
- # Normalize fields to match previous API response for frontend compatibility
216
- # Previous API (GitHub REST) uses: run_number, name, status, conclusion, created_at, html_url
217
- # gh json uses: number, name, status, conclusion, createdAt, url
218
-
219
  normalized_runs = []
220
  for run in runs:
221
  normalized_runs.append({
@@ -227,14 +218,15 @@ def list_runs():
227
  'html_url': run.get('url')
228
  })
229
 
230
- return jsonify({'workflow_runs': normalized_runs})
231
  else:
232
- return jsonify({'runs': [], 'error': result.stderr})
233
  except Exception as e:
234
- return jsonify({'runs': [], 'error': str(e)})
235
 
236
- if __name__ == '__main__':
237
- print(f"GitHub Workflow Runner (gh CLI)")
238
- print(f"Open your browser to: http://localhost:5002")
239
- app.run(debug=True, port=5002)
240
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException, Query
2
+ from fastapi.staticfiles import StaticFiles
3
+ from fastapi.responses import FileResponse, JSONResponse
4
+ from pydantic import BaseModel
5
+ from typing import Optional, Dict, Any
6
  import os
7
  import subprocess
8
  import dotenv
9
  import json
10
 
11
+ # Load environment variables
 
 
12
  dotenv.load_dotenv(os.path.join(os.path.dirname(__file__), '..', '.env'))
13
 
14
  import sys
15
  sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
16
  from src.config import get_config_value
17
 
18
+ app = FastAPI(title="GitHub Workflow Runner")
19
+
20
+ # Mount static files
21
+ app.mount("/static", StaticFiles(directory=os.path.dirname(__file__)), name="static")
22
+
23
+
24
  def get_github_token_from_git_credentials():
25
  """Parse ~/.git-credentials to extract GitHub token"""
26
  git_credentials_path = os.path.expanduser('~/.git-credentials')
 
33
  line = line.strip()
34
  # Format: https://username:token@github.com
35
  if 'github.com' in line and ':' in line and '@' in line:
 
 
36
  try:
37
+ creds_part = line.split('@')[0]
 
 
38
  if '://' in creds_part:
39
+ creds_part = creds_part.split('://')[1]
 
40
  parts = creds_part.split(':')
41
  if len(parts) >= 2:
42
  token = parts[1]
 
49
 
50
  return None
51
 
52
+
53
  # Configuration - Priority: env var > git-credentials
54
  GITHUB_TOKEN = get_config_value("GITHUB_TOKEN") or get_config_value("GITHUB_PAT") or get_github_token_from_git_credentials()
55
  REPO_OWNER = "ElvoroLtd"
56
  REPO_NAME = "Elvoro"
57
  WORKFLOW_FILE = "process_csv.yml"
58
 
 
 
 
59
 
60
+ @app.get("/")
61
+ async def index():
62
+ return FileResponse(os.path.join(os.path.dirname(__file__), 'index.html'))
63
+
64
+
65
+ @app.get("/api/auth/status")
66
+ async def auth_status():
67
  """Check if we have a valid token by running `gh auth status` or similar"""
68
+ token = GITHUB_TOKEN
 
 
 
 
 
 
 
 
 
69
 
70
  if not token:
71
+ return {"authenticated": False, "message": "Token not found in .env"}
72
 
73
  try:
 
74
  env = os.environ.copy()
75
  env['GITHUB_TOKEN'] = token
76
  cmd = ['gh', 'api', 'user']
 
78
 
79
  if result.returncode == 0:
80
  user_data = json.loads(result.stdout)
81
+ return {"authenticated": True, "user": user_data.get('login')}
82
  else:
83
+ return {"authenticated": False, "message": "Invalid token"}
84
  except Exception as e:
85
+ return {"authenticated": False, "message": str(e)}
86
 
87
+
88
+ @app.get("/api/env-vars")
89
+ async def get_env_vars(workflow: str = Query(default="process_csv.yml")):
90
  """
91
  Parse env file based on selected workflow.
92
  Strategy:
 
94
  2. Load keys from template file (publisher.env / video_generate.env)
95
  3. Return keys from template populated with values from .env
96
  """
 
 
97
  # 1. Load Source of Truth (.env)
98
  root_dir = os.path.join(os.path.dirname(__file__), '..')
99
  dotenv_path = os.path.join(root_dir, '.env')
 
 
 
100
  actual_values = dotenv.dotenv_values(dotenv_path)
101
 
102
  # 2. Determine Template File
103
  if workflow == 'publisher.yml':
104
  template_filename = 'publisher.env'
105
  else:
 
106
  template_filename = 'video_generate.env'
107
 
108
  template_path = os.path.join(root_dir, template_filename)
109
 
110
  vars_dict = {}
111
 
112
+ # 3. Specific keys from template, populate with actual values
113
  if os.path.exists(template_path):
114
  try:
115
  with open(template_path, 'r') as f:
 
118
  if not line or line.startswith('#'):
119
  continue
120
 
 
121
  if '=' in line:
122
  key = line.split('=')[0].strip()
123
  else:
124
  key = line.strip()
125
 
 
126
  vars_dict[key] = actual_values.get(key, '')
127
 
128
  except Exception as e:
129
  print(f"Error reading template {template_filename}: {e}")
130
  else:
131
+ vars_dict = dict(actual_values)
132
+
133
+ return {"vars": vars_dict}
134
+
135
 
136
+ class TriggerRequest(BaseModel):
137
+ token: Optional[str] = None
138
+ inputs: Optional[Dict[str, Any]] = {}
139
+ ref: Optional[str] = "feature/video-revamp"
140
+ workflow: Optional[str] = "process_csv.yml"
141
 
142
+
143
+ @app.post("/api/trigger")
144
+ async def trigger_workflow(data: TriggerRequest):
145
+ token = data.token or GITHUB_TOKEN
146
 
147
  if not token:
148
+ raise HTTPException(status_code=401, detail="No GitHub token provided")
149
 
150
+ inputs = data.inputs or {}
151
+ ref = data.ref
152
+ workflow_file = data.workflow
 
 
 
153
 
154
  cmd = [
155
  'gh', 'workflow', 'run', workflow_file,
 
158
  ]
159
 
160
  for key, value in inputs.items():
161
+ if value:
162
  cmd.extend(['-f', f"{key}={value}"])
163
 
164
  try:
 
170
  result = subprocess.run(cmd, capture_output=True, text=True, env=env)
171
 
172
  if result.returncode == 0:
173
+ return {"success": True, "message": "Workflow triggered successfully"}
174
  else:
175
+ raise HTTPException(status_code=400, detail=f"Failed: {result.stderr}")
176
 
177
+ except HTTPException:
178
+ raise
179
  except Exception as e:
180
+ raise HTTPException(status_code=500, detail=str(e))
181
+
182
 
183
+ @app.get("/api/runs")
184
+ async def list_runs(
185
+ token: Optional[str] = Query(default=None),
186
+ workflow: str = Query(default="process_csv.yml")
187
+ ):
188
  """List recent workflow runs using gh"""
189
+ token = token or GITHUB_TOKEN
 
190
 
191
  if not token:
192
+ return {"runs": []}
193
+
 
 
194
  cmd = [
195
  'gh', 'run', 'list',
196
+ '--workflow', workflow,
197
  '--repo', f"{REPO_OWNER}/{REPO_NAME}",
198
  '--limit', '5',
199
  '--json', 'number,status,conclusion,createdAt,url,name'
 
207
 
208
  if result.returncode == 0:
209
  runs = json.loads(result.stdout)
 
 
 
 
210
  normalized_runs = []
211
  for run in runs:
212
  normalized_runs.append({
 
218
  'html_url': run.get('url')
219
  })
220
 
221
+ return {"workflow_runs": normalized_runs}
222
  else:
223
+ return {"runs": [], "error": result.stderr}
224
  except Exception as e:
225
+ return {"runs": [], "error": str(e)}
226
 
 
 
 
 
227
 
228
+ if __name__ == '__main__':
229
+ import uvicorn
230
+ print("GitHub Workflow Runner (FastAPI)")
231
+ print("Open your browser to: http://localhost:5002")
232
+ uvicorn.run(app, host="127.0.0.1", port=5002)
github_workflow_app/requirements.txt CHANGED
@@ -1,3 +1,4 @@
1
- flask
2
- requests
3
  python-dotenv
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
  python-dotenv
4
+ pydantic