Skip to content

Commit 2a1ddea

Browse files
authored
feat: Add /health/version endpoint to verify deployed commit
2 parents e3dff04 + 23688b5 commit 2a1ddea

File tree

2 files changed

+164
-1
lines changed

2 files changed

+164
-1
lines changed

mod_health/controllers.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"""Health check endpoints for deployment verification and monitoring."""
22

3+
import os
4+
import subprocess
35
from datetime import datetime
4-
from typing import Any, Dict, Tuple
6+
from typing import Any, Dict, Optional, Tuple
57

68
from flask import Blueprint, current_app, jsonify
79

@@ -112,3 +114,70 @@ def readiness_check() -> Tuple[Any, int]:
112114
:rtype: Tuple[Any, int]
113115
"""
114116
return health_check()
117+
118+
119+
def get_git_info() -> Dict[str, Optional[str]]:
120+
"""
121+
Get git repository information for the deployed version.
122+
123+
:return: Dictionary with git commit hash, short hash, and branch
124+
:rtype: Dict[str, Optional[str]]
125+
"""
126+
result: Dict[str, Optional[str]] = {
127+
'commit': None,
128+
'short': None,
129+
'branch': None,
130+
}
131+
132+
try:
133+
# Get the installation folder from config, fallback to current directory
134+
install_folder = current_app.config.get('INSTALL_FOLDER', os.getcwd())
135+
136+
# Get full commit hash
137+
commit = subprocess.check_output(
138+
['git', 'rev-parse', 'HEAD'],
139+
cwd=install_folder,
140+
stderr=subprocess.DEVNULL
141+
).decode().strip()
142+
result['commit'] = commit
143+
result['short'] = commit[:7]
144+
145+
# Get current branch
146+
branch = subprocess.check_output(
147+
['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
148+
cwd=install_folder,
149+
stderr=subprocess.DEVNULL
150+
).decode().strip()
151+
result['branch'] = branch
152+
153+
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
154+
# Git not available or not a git repository
155+
current_app.logger.warning('Could not retrieve git version information')
156+
157+
return result
158+
159+
160+
@mod_health.route('/health/version')
161+
def version_check() -> Tuple[Any, int]:
162+
"""
163+
Version endpoint to verify deployed commit.
164+
165+
Returns the current git commit hash, useful for verifying
166+
that a deployment has completed successfully.
167+
168+
:return: JSON response with version information
169+
:rtype: Tuple[Any, int]
170+
"""
171+
git_info = get_git_info()
172+
173+
response = {
174+
'timestamp': datetime.utcnow().isoformat() + 'Z',
175+
'git': git_info,
176+
}
177+
178+
# Return 200 if we have version info, 503 if we couldn't get it
179+
if git_info['commit']:
180+
return jsonify(response), 200
181+
else:
182+
response['error'] = 'Could not retrieve version information'
183+
return jsonify(response), 503

tests/test_health/test_controllers.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,97 @@ def test_check_config_missing_keys(self):
130130
result = check_config()
131131
self.assertEqual(result['status'], 'error')
132132
self.assertIn('GITHUB_TOKEN', result['message'])
133+
134+
135+
class TestVersionEndpoint(BaseTestCase):
136+
"""Test version endpoint."""
137+
138+
@mock.patch('mod_health.controllers.get_git_info')
139+
def test_version_endpoint_returns_200_with_git_info(self, mock_git_info):
140+
"""Test that /health/version returns 200 when git info is available."""
141+
mock_git_info.return_value = {
142+
'commit': 'abc123def456789012345678901234567890abcd',
143+
'short': 'abc123d',
144+
'branch': 'master',
145+
}
146+
147+
response = self.app.test_client().get('/health/version')
148+
self.assertEqual(response.status_code, 200)
149+
150+
data = json.loads(response.data)
151+
self.assertIn('timestamp', data)
152+
self.assertIn('git', data)
153+
self.assertEqual(data['git']['commit'], 'abc123def456789012345678901234567890abcd')
154+
self.assertEqual(data['git']['short'], 'abc123d')
155+
self.assertEqual(data['git']['branch'], 'master')
156+
157+
@mock.patch('mod_health.controllers.get_git_info')
158+
def test_version_endpoint_returns_503_when_git_unavailable(self, mock_git_info):
159+
"""Test that /health/version returns 503 when git info unavailable."""
160+
mock_git_info.return_value = {
161+
'commit': None,
162+
'short': None,
163+
'branch': None,
164+
}
165+
166+
response = self.app.test_client().get('/health/version')
167+
self.assertEqual(response.status_code, 503)
168+
169+
data = json.loads(response.data)
170+
self.assertIn('error', data)
171+
self.assertIn('git', data)
172+
self.assertIsNone(data['git']['commit'])
173+
174+
175+
class TestGetGitInfo(BaseTestCase):
176+
"""Test get_git_info function."""
177+
178+
@mock.patch('subprocess.check_output')
179+
def test_get_git_info_success(self, mock_subprocess):
180+
"""Test get_git_info returns correct values when git is available."""
181+
from mod_health.controllers import get_git_info
182+
183+
# Mock both git commands
184+
mock_subprocess.side_effect = [
185+
b'abc123def456789012345678901234567890abcd\n', # git rev-parse HEAD
186+
b'master\n', # git rev-parse --abbrev-ref HEAD
187+
]
188+
189+
with self.app.app_context():
190+
result = get_git_info()
191+
192+
self.assertEqual(result['commit'], 'abc123def456789012345678901234567890abcd')
193+
self.assertEqual(result['short'], 'abc123d')
194+
self.assertEqual(result['branch'], 'master')
195+
196+
@mock.patch('subprocess.check_output')
197+
def test_get_git_info_git_not_available(self, mock_subprocess):
198+
"""Test get_git_info handles missing git gracefully."""
199+
import subprocess
200+
201+
from mod_health.controllers import get_git_info
202+
203+
mock_subprocess.side_effect = FileNotFoundError('git not found')
204+
205+
with self.app.app_context():
206+
result = get_git_info()
207+
208+
self.assertIsNone(result['commit'])
209+
self.assertIsNone(result['short'])
210+
self.assertIsNone(result['branch'])
211+
212+
@mock.patch('subprocess.check_output')
213+
def test_get_git_info_not_a_repo(self, mock_subprocess):
214+
"""Test get_git_info handles non-repo directory gracefully."""
215+
import subprocess
216+
217+
from mod_health.controllers import get_git_info
218+
219+
mock_subprocess.side_effect = subprocess.CalledProcessError(128, 'git')
220+
221+
with self.app.app_context():
222+
result = get_git_info()
223+
224+
self.assertIsNone(result['commit'])
225+
self.assertIsNone(result['short'])
226+
self.assertIsNone(result['branch'])

0 commit comments

Comments
 (0)