Skip to content

Commit b0f37c0

Browse files
committed
πŸ™ chore: enhance CI/CD, versioning, and testing setup
β€’ use importlib.metadata for version and description in Settings β€’ add CI/CD pipeline on main branch for testing and auto-release pypi and docker β€’ add new test for UVX functionality to ensure reliability
1 parent 83f3a8b commit b0f37c0

File tree

18 files changed

+221
-109
lines changed

18 files changed

+221
-109
lines changed

β€Ž.github/workflows/1_tests.ymlβ€Ž

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
name: Tests
22

33
on:
4-
push:
5-
branches:
6-
- main
74
pull_request:
85
types:
96
- opened
107
- synchronize
8+
workflow_call:
119

1210
permissions: {} # deny all by default
1311

β€Ž.github/workflows/2_release.ymlβ€Ž

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
name: Release Python Package and Docker Image
22

33
on:
4+
workflow_call:
5+
inputs:
6+
publish:
7+
description: 'Whether to publish the Python package and Docker image (true/false)'
8+
required: false
9+
default: 'false'
10+
type: string
411
workflow_dispatch:
512
inputs:
613
publish:
@@ -25,18 +32,6 @@ jobs:
2532
run: echo "$GITHUB_CONTEXT"
2633
- id: lower
2734
run: echo "github_repository_lowercase=${GITHUB_REPOSITORY,,}" >> $GITHUB_OUTPUT
28-
29-
build-python-package:
30-
needs: params
31-
uses: ./.github/workflows/2.1_build-python-package.yml
32-
permissions:
33-
id-token: write
34-
contents: read
35-
with:
36-
publish: ${{ github.event.inputs.publish }}
37-
upload-github-release: 'false'
38-
secrets: inherit
39-
4035
build-docker-image:
4136
needs: [params, build-python-package]
4237
uses: ./.github/workflows/2.2_build-docker-image.yml
@@ -50,7 +45,16 @@ jobs:
5045
dockerfile: Dockerfile
5146
push: ${{ github.event.inputs.publish }}
5247
secrets: inherit
53-
48+
build-python-package:
49+
needs: params
50+
uses: ./.github/workflows/2.1_build-python-package.yml
51+
permissions:
52+
id-token: write
53+
contents: read
54+
with:
55+
publish: ${{ github.event.inputs.publish }}
56+
upload-github-release: 'false'
57+
secrets: inherit
5458
create-github-release:
5559
needs: [params, build-python-package, build-docker-image]
5660
if: ${{ github.event.inputs.publish == 'true' && needs.build-python-package.result == 'success' && needs.build-docker-image.result == 'success' }}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: CI/CD
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
permissions: {}
9+
10+
jobs:
11+
tests:
12+
name: Run full test suite
13+
uses: ./.github/workflows/1_tests.yml
14+
permissions:
15+
contents: read
16+
17+
release:
18+
name: Release Python Package and Docker Image
19+
needs: tests
20+
if: needs.tests.result == 'success'
21+
uses: ./.github/workflows/2_release.yml
22+
with:
23+
publish: 'true' # change to 'false' if you only want to build without publishing
24+
secrets: inherit

β€Ž.github/workflows/README.mdβ€Ž

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## πŸ“‚ Workflow Overview
44

55
This repository uses modular, clearly named workflows for CI, integration tests, packaging, Docker, and releases.
6+
A top-level **`0_ci-cd.yaml`** orchestrates the process for pushes to `main`, running **Tests** first and triggering the **Release** workflow only if they pass.
67

78
1. πŸ§ͺ **Tests** β€” lint, types checks, unit, integration and e2e tests.
89
- 🧩 **Unit Tests** β€” Run unit tests
@@ -12,6 +13,7 @@ This repository uses modular, clearly named workflows for CI, integration tests,
1213
- πŸ“¦ **Build & Publish Python Package** β€” Build and (optionally) publish the Python package
1314
- πŸ‹ **Build & Push Docker Image** β€” Build and (optionally) push the Docker image
1415
- πŸ“ **Create GitHub Release Only** β€” Create a GitHub Release from already published artifacts
16+
3. πŸ”„ **CI/CD Orchestration** (`3_ci-cd.yaml`) β€” Runs Tests β†’ Release when pushing to `main`.
1517

1618
## ⚑ Quick Start
1719

@@ -101,6 +103,14 @@ Create a GitHub Release from already published artifacts:
101103
act -W .github/workflows/2.3_create-github-release.yml -P ubuntu-latest=catthehacker/ubuntu:act-latest --rm
102104
```
103105

106+
## 3. πŸ”„ CI/CD Orchestration
107+
108+
Run the combined CI + Release process (push to main simulation):
109+
110+
```sh
111+
act -W .github/workflows/3_ci-cd.yaml -P ubuntu-latest=catthehacker/ubuntu:act-latest --rm
112+
```
113+
104114
## πŸ’‘ Notes
105115

106116
- 🐳 You need Docker running.

β€Žconfig.yamlβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
package_name: mcp-selenium-grid # pyproject.toml
12
project_name: MCP Selenium Grid
23
deployment_mode: docker # one of: docker, kubernetes (DeploymentMode enum values)
34
api_v1_str: /api/v1

β€Žpyproject.tomlβ€Ž

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
[project]
1515
name = "mcp-selenium-grid"
16-
version = "0.1.0.dev5"
16+
version = "0.1.0.dev6"
1717
description = "MCP Server for managing Selenium Grid"
1818
readme = "README.md"
1919
license = { file = "LICENSE" }
@@ -58,6 +58,7 @@ test = [
5858
"pytest-asyncio>=1.0.0", # Parallel test execution
5959
"pytest-sugar>=1.0.0",
6060
"coverage>=7.10.2",
61+
"pytest-timeout>=2.4.0",
6162
]
6263

6364
[build-system]

β€Žsrc/app/common/toml.pyβ€Ž

Lines changed: 0 additions & 55 deletions
This file was deleted.

β€Žsrc/app/core/settings.pyβ€Ž

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,29 @@
11
"""Core settings for MCP Server."""
22

3-
from pydantic import Field, SecretStr, field_validator
3+
from importlib.metadata import metadata, version
4+
5+
from pydantic import Field, SecretStr
46

5-
from app.common.toml import load_value_from_toml
67
from app.services.selenium_hub.models.general_settings import SeleniumHubGeneralSettings
78

89

910
class Settings(SeleniumHubGeneralSettings):
1011
"""MCP Server settings."""
1112

12-
# API Settings
13+
# Project Settings
14+
PACKAGE_NAME: str = "mcp-selenium-grid"
1315
PROJECT_NAME: str = "MCP Selenium Grid"
14-
VERSION: str = ""
1516

16-
@field_validator("VERSION", mode="before")
17-
@classmethod
18-
def load_version_from_pyproject(cls, v: str) -> str:
19-
return v or load_value_from_toml(["project", "version"])
17+
@property
18+
def VERSION(self) -> str:
19+
return version(self.PACKAGE_NAME)
2020

21-
API_V1_STR: str = "/api/v1"
21+
@property
22+
def DESCRIPTION(self) -> str:
23+
return metadata(self.PACKAGE_NAME).get("Summary", "").strip()
2224

23-
# API Token
25+
# API Settings
26+
API_V1_STR: str = "/api/v1"
2427
API_TOKEN: SecretStr = SecretStr("CHANGE_ME")
2528

2629
# Security Settings

β€Žsrc/app/main.pyβ€Ž

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,12 @@
1515

1616
from app.common.fastapi_mcp import handle_fastapi_request
1717
from app.common.logger import logger
18-
from app.common.toml import load_value_from_toml
1918
from app.dependencies import get_settings, verify_token
2019
from app.models import HealthCheckResponse, HealthStatus, HubStatusResponse
2120
from app.routers.browsers import router as browsers_router
2221
from app.routers.selenium_proxy import router as selenium_proxy_router
2322
from app.services.selenium_hub import SeleniumHub
2423

25-
DESCRIPTION = load_value_from_toml(["project", "description"])
2624
SETTINGS = get_settings()
2725
MCP_HTTP_PATH = "/mcp"
2826
MCP_SSE_PATH = "/sse"
@@ -65,7 +63,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, Any]:
6563
app = FastAPI(
6664
title=SETTINGS.PROJECT_NAME,
6765
version=SETTINGS.VERSION,
68-
description=DESCRIPTION,
66+
description=SETTINGS.DESCRIPTION,
6967
lifespan=lifespan,
7068
)
7169

@@ -119,15 +117,14 @@ async def get_hub_stats(
119117
# Get app_state.browsers_instances using lock to ensure thread safety
120118
app_state = request.app.state
121119
async with app_state.browsers_instances_lock:
122-
browsers = [browser.model_dump() for browser in app_state.browsers_instances.values()]
123-
124-
return HubStatusResponse(
125-
hub_running=is_running,
126-
hub_healthy=is_healthy,
127-
deployment_mode=SETTINGS.DEPLOYMENT_MODE,
128-
max_instances=SETTINGS.selenium_grid.MAX_BROWSER_INSTANCES,
129-
browsers=browsers,
130-
)
120+
return HubStatusResponse(
121+
hub_running=is_running,
122+
hub_healthy=is_healthy,
123+
deployment_mode=SETTINGS.DEPLOYMENT_MODE,
124+
max_instances=SETTINGS.selenium_grid.MAX_BROWSER_INSTANCES,
125+
browsers=app_state.browsers_instances,
126+
webdriver_remote_url=hub.WEBDRIVER_REMOTE_URL,
127+
)
131128

132129
# Include browser management endpoints
133130
app.include_router(browsers_router, prefix=SETTINGS.API_V1_STR)
@@ -137,8 +134,8 @@ async def get_hub_stats(
137134
# --- MCP Integration ---
138135
mcp = FastApiMCP(
139136
app,
140-
name="MCP Selenium Grid",
141-
description=DESCRIPTION,
137+
name=SETTINGS.PROJECT_NAME,
138+
description=SETTINGS.DESCRIPTION,
142139
describe_full_response_schema=True,
143140
describe_all_responses=True,
144141
auth_config=AuthConfig(
@@ -148,7 +145,7 @@ async def get_hub_stats(
148145
mcp.mount_http(mount_path=MCP_HTTP_PATH)
149146
mcp.mount_sse(mount_path=MCP_SSE_PATH)
150147

151-
@app.api_route("/", methods=["GET", "POST"])
148+
@app.api_route("/", methods=["GET", "POST"], include_in_schema=False)
152149
async def root_proxy(
153150
request: Request,
154151
credentials: HTTPAuthorizationCredentials = Depends(verify_token),

β€Žsrc/app/models.pyβ€Ž

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
"""Response models for MCP Server."""
22

33
from enum import Enum
4-
from typing import Any
54

65
from pydantic import BaseModel, Field
76

87
from app.services.selenium_hub.models import DeploymentMode
8+
from app.services.selenium_hub.models.browser import BrowserInstance
99

1010

1111
class HealthStatus(str, Enum):
@@ -38,4 +38,7 @@ class HubStatusResponse(BaseModel):
3838
examples=[DeploymentMode.DOCKER, DeploymentMode.KUBERNETES],
3939
)
4040
max_instances: int = Field(description="Maximum allowed browser instances")
41-
browsers: list[dict[str, Any]] = Field(description="List of current browser instances")
41+
browsers: dict[str, BrowserInstance] = Field(
42+
description="Dict of current browser instances with id as dict key"
43+
)
44+
webdriver_remote_url: str = Field(description="URL to connect to the Grid's Hub or Router")

0 commit comments

Comments
Β (0)