Skip to content

Commit 7b19d75

Browse files
author
Alessandro Mosca
committed
add github_pages records
1 parent edf47bb commit 7b19d75

6 files changed

Lines changed: 306 additions & 5 deletions

File tree

.github/workflows/deploy_documentation.yml

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,35 @@ jobs:
3535
- name: Create CNAME file
3636
working-directory: ./docs/build
3737
run: echo "docs.opengatellm.etalab.gouv.fr" > CNAME
38-
38+
3939
- name: Deploy to GitHub Pages
4040
uses: peaceiris/actions-gh-pages@v3
4141
with:
4242
github_token: ${{ secrets.GITHUB_TOKEN }}
4343
publish_dir: ./docs/build
4444
publish_branch: gh-pages
4545
user_name: 'github-actions[bot]'
46-
user_email: 'github-actions[bot]@users.noreply.github.com'
46+
user_email: 'github-actions[bot]@users.noreply.github.com'
47+
48+
dns:
49+
runs-on: ubuntu-latest
50+
51+
env:
52+
OVH_APP_KEY: ${{ secrets.OVH_APP_KEY }}
53+
OVH_APP_SECRET: ${{ secrets.OVH_APP_SECRET }}
54+
OVH_CONSUMER_KEY: ${{ secrets.OVH_CONSUMER_KEY }}
55+
56+
steps:
57+
- name: Checkout repository
58+
uses: actions/checkout@v4
59+
60+
- name: Configure Python
61+
uses: actions/setup-python@v5
62+
with:
63+
python-version: '3.12'
64+
65+
- name: Install dependencies
66+
run: pip install ovh
67+
68+
- name: Run DNS management script
69+
run: python scripts/manage_dns.py

.github/workflows/run_tests.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ jobs:
3737
# Activate venv for the test run
3838
. .venv/bin/activate
3939
40+
# Run unit tests (includes api/tests/unit and scripts/test_scripts via Makefile)
41+
make test
42+
43+
# Run integration tests
4044
make test-integ action=run execute=local
4145
4246
mkdir -p .github/badges

Makefile

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ help:
5353
&& sleep 2 \
5454
&& open http://localhost:8000/docs
5555

56-
5756
.start-playground:
5857
@mkdir -p ~/.streamlit/
5958
@echo "[general]" > ~/.streamlit/credentials.toml
@@ -146,7 +145,17 @@ quickstart:
146145

147146
# test -----------------------------------------------------------------------------------------------------------------------------------------------
148147
test:
149-
PYTHONPATH=. pytest api/tests/unit --config-file=pyproject.toml
148+
PYTHONPATH=. pytest api/tests/unit scripts/tests/unit --config-file=pyproject.toml
149+
150+
# test with coverage -------------------------------------------------------------------------------------------------------------------------------
151+
# Generates terminal summary, XML (coverage.xml), and HTML report (htmlcov/)
152+
test-coverage:
153+
PYTHONPATH=. pytest api/tests/unit scripts/tests \
154+
--config-file=pyproject.toml \
155+
--cov=./api --cov=./scripts \
156+
--cov-report=term-missing \
157+
--cov-report=xml \
158+
--cov-report=html
150159

151160
# lint -----------------------------------------------------------------------------------------------------------------------------------------------
152161
lint:

docs/docs/contributing/tests.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,12 @@
1111
3. To run the tests, you can use the following command:
1212

1313
```bash
14-
make test
14+
make test
15+
16+
# Run tests with coverage
17+
make test-coverage
18+
19+
# Open the HTML coverage report
20+
open htmlcov/index.html # macOS
21+
# xdg-open htmlcov/index.html # Linux
1522
```

scripts/manage_docs_dns.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import ovh
2+
import os
3+
import sys
4+
5+
# 🔐 Load credentials from environment variables
6+
APP_KEY = os.getenv("OVH_APP_KEY")
7+
APP_SECRET = os.getenv("OVH_APP_SECRET")
8+
CONSUMER_KEY = os.getenv("OVH_CONSUMER_KEY")
9+
10+
# 📌 DNS parameters
11+
DOMAIN = "etalab.gouv.fr"
12+
CNAME_SUBDOMAIN = "docs.opengatellm"
13+
CNAME_TARGET = "etalab-ia.github.io."
14+
TXT_SUBDOMAIN = "_github-pages-challenge-etalab-ia.docs.opengatellm"
15+
TXT_TARGET = "\"fadc7d567d241d7ddbbf351cba47d4\""
16+
17+
def get_client():
18+
return ovh.Client(
19+
endpoint='ovh-eu',
20+
application_key=APP_KEY,
21+
application_secret=APP_SECRET,
22+
consumer_key=CONSUMER_KEY,
23+
)
24+
25+
def find_record(client, domain, subdomain, record_type):
26+
try:
27+
return client.get(f"/domain/zone/{domain}/record", fieldType=record_type, subDomain=subdomain)
28+
except ovh.exceptions.APIError as e:
29+
print(f"❌ Error fetching {record_type} records for {subdomain}: {e}")
30+
return []
31+
32+
def get_record_details(client, domain, record_id):
33+
return client.get(f"/domain/zone/{domain}/record/{record_id}")
34+
35+
def update_or_create_record(client, domain, subdomain, record_type, expected_value):
36+
records = find_record(client, domain, subdomain, record_type)
37+
38+
for record_id in records:
39+
record = get_record_details(client, domain, record_id)
40+
if record["target"] == expected_value:
41+
print(f"✅ {record_type} record for {subdomain} is valid: {expected_value}")
42+
return
43+
else:
44+
print(f"⚠️ Updating invalid {record_type} record for {subdomain} (found: {record['target']})")
45+
client.put(f"/domain/zone/{domain}/record/{record_id}", target=expected_value)
46+
client.post(f"/domain/zone/{domain}/refresh")
47+
return
48+
49+
print(f"➕ Creating new {record_type} record for {subdomain}")
50+
client.post(f"/domain/zone/{domain}/record",
51+
fieldType=record_type,
52+
subDomain=subdomain,
53+
target=expected_value,
54+
ttl=3600
55+
)
56+
client.post(f"/domain/zone/{domain}/refresh")
57+
58+
def main():
59+
if not all([APP_KEY, APP_SECRET, CONSUMER_KEY]):
60+
print("❌ Missing OVH credentials in environment variables.")
61+
sys.exit(1)
62+
63+
client = get_client()
64+
65+
update_or_create_record(client, DOMAIN, CNAME_SUBDOMAIN, "CNAME", CNAME_TARGET)
66+
update_or_create_record(client, DOMAIN, TXT_SUBDOMAIN, "TXT", TXT_TARGET)
67+
68+
if __name__ == "__main__":
69+
main()
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import importlib
2+
import importlib.util
3+
import os
4+
import sys
5+
from types import ModuleType
6+
from unittest.mock import MagicMock
7+
8+
import pytest
9+
10+
11+
def load_manage_docs_dns_with_env(env: dict):
12+
"""
13+
Dynamically load scripts/manage_docs_dns.py while:
14+
- injecting a fake 'ovh' module into sys.modules (to avoid real dependency)
15+
- controlling environment variables at import time (module reads them on import)
16+
Returns the loaded module.
17+
"""
18+
# Backup environment and sys.modules entries we'll override
19+
old_env = os.environ.copy()
20+
try:
21+
# Apply provided environment
22+
os.environ.clear()
23+
os.environ.update(env)
24+
25+
# Inject a fake 'ovh' module with required surface
26+
fake_ovh = ModuleType("ovh")
27+
28+
class _FakeAPIError(Exception):
29+
pass
30+
31+
exceptions_mod = ModuleType("ovh.exceptions")
32+
exceptions_mod.APIError = _FakeAPIError
33+
fake_ovh.exceptions = exceptions_mod
34+
sys.modules["ovh"] = fake_ovh
35+
sys.modules["ovh.exceptions"] = exceptions_mod
36+
37+
# Also provide a placeholder Client type so get_client() can construct it if needed
38+
class _FakeClient:
39+
def __init__(self, *args, **kwargs):
40+
pass
41+
42+
def get(self, *args, **kwargs):
43+
raise NotImplementedError
44+
45+
def post(self, *args, **kwargs):
46+
raise NotImplementedError
47+
48+
def put(self, *args, **kwargs):
49+
raise NotImplementedError
50+
51+
fake_ovh.Client = _FakeClient
52+
53+
# Load the target module by file path
54+
module_path = os.path.abspath(
55+
os.path.join(os.path.dirname(__file__), "..", "..", "manage_docs_dns.py")
56+
)
57+
spec = importlib.util.spec_from_file_location("manage_docs_dns", module_path)
58+
module = importlib.util.module_from_spec(spec)
59+
assert spec and spec.loader
60+
spec.loader.exec_module(module)
61+
return module
62+
finally:
63+
# Restore environment to avoid leaking between tests
64+
os.environ.clear()
65+
os.environ.update(old_env)
66+
# Remove our fake ovh modules if present
67+
for name in ["ovh", "ovh.exceptions"]:
68+
if name in sys.modules:
69+
del sys.modules[name]
70+
71+
72+
def test_main_exits_when_missing_credentials(monkeypatch):
73+
mod = load_manage_docs_dns_with_env({})
74+
75+
# Intercept sys.exit to assert exit code
76+
exit_called = {"code": None}
77+
78+
def fake_exit(code=0):
79+
exit_called["code"] = code
80+
raise SystemExit(code)
81+
82+
monkeypatch.setattr(mod.sys, "exit", fake_exit)
83+
84+
with pytest.raises(SystemExit) as ei:
85+
mod.main()
86+
87+
assert ei.value.code == 1
88+
assert exit_called["code"] == 1
89+
90+
91+
def test_main_calls_update_for_cname_and_txt_when_credentials_present(monkeypatch):
92+
env = {
93+
"OVH_APP_KEY": "k",
94+
"OVH_APP_SECRET": "s",
95+
"OVH_CONSUMER_KEY": "c",
96+
}
97+
mod = load_manage_docs_dns_with_env(env)
98+
99+
# Mock client and functions
100+
fake_client = MagicMock()
101+
monkeypatch.setattr(mod, "get_client", lambda: fake_client)
102+
uocr = MagicMock()
103+
monkeypatch.setattr(mod, "update_or_create_record", uocr)
104+
105+
mod.main()
106+
107+
# Should have been called twice: once for CNAME, once for TXT
108+
assert uocr.call_count == 2
109+
cname_call = uocr.call_args_list[0]
110+
txt_call = uocr.call_args_list[1]
111+
112+
assert cname_call.args == (fake_client, mod.DOMAIN, mod.CNAME_SUBDOMAIN, "CNAME", mod.CNAME_TARGET)
113+
assert txt_call.args == (fake_client, mod.DOMAIN, mod.TXT_SUBDOMAIN, "TXT", mod.TXT_TARGET)
114+
115+
116+
def test_update_or_create_record_valid_no_action(monkeypatch):
117+
env = {
118+
"OVH_APP_KEY": "k",
119+
"OVH_APP_SECRET": "s",
120+
"OVH_CONSUMER_KEY": "c",
121+
}
122+
mod = load_manage_docs_dns_with_env(env)
123+
124+
fake_client = MagicMock()
125+
126+
# Simulate find_record returns one record id and details match expected target
127+
monkeypatch.setattr(mod, "find_record", lambda client, domain, subdomain, rtype: [123])
128+
monkeypatch.setattr(mod, "get_record_details", lambda client, domain, record_id: {"target": "expected"})
129+
130+
mod.update_or_create_record(fake_client, "example.com", "sub", "CNAME", "expected")
131+
132+
# No update or create should be performed
133+
fake_client.put.assert_not_called()
134+
fake_client.post.assert_not_called()
135+
136+
137+
def test_update_or_create_record_updates_existing_record(monkeypatch):
138+
env = {
139+
"OVH_APP_KEY": "k",
140+
"OVH_APP_SECRET": "s",
141+
"OVH_CONSUMER_KEY": "c",
142+
}
143+
mod = load_manage_docs_dns_with_env(env)
144+
145+
fake_client = MagicMock()
146+
147+
monkeypatch.setattr(mod, "find_record", lambda client, domain, subdomain, rtype: [456])
148+
monkeypatch.setattr(mod, "get_record_details", lambda client, domain, record_id: {"target": "old-value"})
149+
150+
mod.update_or_create_record(fake_client, "example.com", "sub", "TXT", "new-value")
151+
152+
fake_client.put.assert_called_once()
153+
assert fake_client.put.call_args[0][0] == "/domain/zone/example.com/record/456"
154+
assert fake_client.put.call_args.kwargs == {"target": "new-value"}
155+
156+
fake_client.post.assert_called_once_with("/domain/zone/example.com/refresh")
157+
158+
159+
def test_update_or_create_record_creates_new_record(monkeypatch):
160+
env = {
161+
"OVH_APP_KEY": "k",
162+
"OVH_APP_SECRET": "s",
163+
"OVH_CONSUMER_KEY": "c",
164+
}
165+
mod = load_manage_docs_dns_with_env(env)
166+
167+
fake_client = MagicMock()
168+
169+
# No existing records
170+
monkeypatch.setattr(mod, "find_record", lambda client, domain, subdomain, rtype: [])
171+
172+
mod.update_or_create_record(fake_client, "example.com", "sub", "TXT", "txt-value")
173+
174+
# First post: create record with proper parameters; Second post: refresh zone
175+
assert fake_client.post.call_count == 2
176+
177+
# Validate first post call arguments
178+
first_call = fake_client.post.call_args_list[0]
179+
assert first_call.args[0] == "/domain/zone/example.com/record"
180+
assert first_call.kwargs == {
181+
"fieldType": "TXT",
182+
"subDomain": "sub",
183+
"target": "txt-value",
184+
"ttl": 3600,
185+
}
186+
187+
# Validate refresh call
188+
second_call = fake_client.post.call_args_list[1]
189+
assert second_call.args == ("/domain/zone/example.com/refresh",)

0 commit comments

Comments
 (0)