Skip to content

Commit 8bd01c4

Browse files
committed
[DOCS] Adding documentation and base structure of the repo
1 parent 9e7baf4 commit 8bd01c4

File tree

240 files changed

+15314
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

240 files changed

+15314
-0
lines changed

.github/workflows/ci.yml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: ci
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches: [main]
7+
8+
jobs:
9+
test-and-lint:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- name: Set up Python
14+
uses: actions/setup-python@v5
15+
with:
16+
python-version: '3.13'
17+
- name: Install tooling
18+
run: |
19+
python -m pip install --upgrade pip
20+
pip install pylint isort black pytest pytest-cov pytest-asyncio
21+
- name: Install lib
22+
run: pip install -e lib
23+
- name: Install apps
24+
run: |
25+
for d in apps/*; do
26+
pip install -e "$d/src"
27+
done
28+
- name: Lint
29+
run: |
30+
python -m isort --check-only lib apps
31+
python -m black --check lib apps
32+
python -m pylint lib/src apps/*/src
33+
- name: Tests with coverage
34+
env:
35+
PYTHONPATH: "${{ github.workspace }}/lib/src"
36+
run: |
37+
pytest lib/tests apps/**/tests --maxfail=1 --cov=. --cov-report=term-missing --cov-fail-under=75
38+
39+
build-and-push:
40+
needs: test-and-lint
41+
if: github.ref == 'refs/heads/main'
42+
runs-on: ubuntu-latest
43+
env:
44+
REGISTRY: ghcr.io/${{ github.repository_owner }}
45+
steps:
46+
- uses: actions/checkout@v4
47+
- name: Set up Docker Buildx
48+
uses: docker/setup-buildx-action@v3
49+
- name: Login to GHCR
50+
uses: docker/login-action@v3
51+
with:
52+
registry: ghcr.io
53+
username: ${{ github.repository_owner }}
54+
password: ${{ secrets.GITHUB_TOKEN }}
55+
- name: Build and push images
56+
run: |
57+
for d in apps/*; do
58+
image_name=$(basename "$d")
59+
docker build -t "$REGISTRY/$image_name:latest" -f "$d/src/Dockerfile" "$d"
60+
docker push "$REGISTRY/$image_name:latest"
61+
done

.infra/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Infra CLI quickstart
2+
3+
The CLI in this folder generates Dockerfiles and per-app Bicep modules from templates.
4+
5+
## Prerequisites
6+
- Python 3.13+
7+
- Azure CLI (`az`) if you plan to deploy
8+
- Configure the CLI env once: from the repo root `bash .infra/config-cli.sh` (creates .infra/.venv and installs deps)
9+
10+
To reuse the env later: `source .infra/.venv/bin/activate`.
11+
12+
## Generate Bicep modules
13+
- One app: `python .infra/cli.py generate-bicep --service <app>`
14+
- All apps: `python .infra/cli.py generate-bicep --apply-all`
15+
- Output: `.infra/modules/<app>/<app>.bicep` and `.infra/modules/<app>/<app>-main.bicep` rendered from templates in `.infra/templates/`
16+
17+
## Generate Dockerfiles
18+
- One app: `python .infra/cli.py generate-dockerfile --service <app>`
19+
- All apps: `python .infra/cli.py generate-dockerfile --apply-all`
20+
- Output: `<repo>/apps/<app>/src/Dockerfile` rendered from `.infra/templates/Dockerfile.template`
21+
22+
## Deploy (optional)
23+
- One app: `python .infra/cli.py deploy --service <app> --location <region> [--subscription-id <id>] [--resource-group <name>] [--app-image <image>]`
24+
- All apps: `python .infra/cli.py deploy-all --location <region> [--subscription-id <id>] [--resource-group-prefix <prefix>] [--app-image <image>]`
25+
26+
Notes:
27+
- The default image is `ghcr.io/OWNER/<app>:latest`; override with `--app-image`.
28+
- Templates live in `.infra/templates/`; adjust them before regenerating output files.

.infra/cli.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
"""Typer CLI to deploy services via Bicep and maintain Dockerfiles."""
2+
import subprocess
3+
from pathlib import Path
4+
from typing import Optional
5+
6+
import typer
7+
8+
app = typer.Typer(help="Provision Holiday Peak Hub services")
9+
10+
ROOT = Path(__file__).parent
11+
APPS_DIR = ROOT.parent / "apps"
12+
TEMPLATE_DIR = ROOT / "templates"
13+
14+
def load_template(template_name: str) -> str:
15+
path = TEMPLATE_DIR / template_name
16+
if not path.exists():
17+
raise typer.BadParameter(f"Template not found: {path}")
18+
return path.read_text(encoding="utf-8")
19+
20+
21+
def deploy_file(
22+
file_path: Path,
23+
app_name: str,
24+
resource_group: str,
25+
location: str,
26+
app_image: str,
27+
subscription_id: Optional[str],
28+
) -> None:
29+
typer.echo(
30+
f"Deploying {file_path.name} to {resource_group} in {location}"
31+
)
32+
cmd = [
33+
"az",
34+
"deployment",
35+
"sub",
36+
"create",
37+
"--location",
38+
location,
39+
"--template-file",
40+
str(file_path),
41+
"--parameters",
42+
f"location={location}",
43+
f"resourceGroupName={resource_group}",
44+
f"appName={app_name}",
45+
f"appImage={app_image}",
46+
]
47+
if subscription_id:
48+
cmd.extend(["--subscription", subscription_id, "--parameters", f"subscriptionId={subscription_id}"])
49+
subprocess.run(cmd, check=False)
50+
51+
52+
def default_resource_group(service: str) -> str:
53+
return f"{service}-rg"
54+
55+
56+
def write_bicep_files(app_name: str) -> tuple[Path, Path]:
57+
module_dir = ROOT / "modules" / app_name
58+
module_dir.mkdir(parents=True, exist_ok=True)
59+
app_bicep = module_dir / f"{app_name}.bicep"
60+
main_bicep = module_dir / f"{app_name}-main.bicep"
61+
app_template = load_template("app.bicep.tpl")
62+
main_template = load_template("main.bicep.tpl")
63+
app_bicep.write_text(app_template.replace("{app_name}", app_name), encoding="utf-8")
64+
main_bicep.write_text(main_template.replace("{app_name}", app_name), encoding="utf-8")
65+
typer.echo(f"Wrote {app_bicep} and {main_bicep}")
66+
return app_bicep, main_bicep
67+
68+
69+
@app.command()
70+
def deploy(
71+
service: str,
72+
resource_group: Optional[str] = typer.Option(None, help="Name of the resource group to create/use"),
73+
location: str = typer.Option("eastus", help="Azure region"),
74+
app_image: Optional[str] = typer.Option(None, help="Container image to deploy to AKS"),
75+
subscription_id: Optional[str] = typer.Option(None, help="Azure subscription ID"),
76+
) -> None:
77+
main_bicep = ROOT / "modules" / service / f"{service}-main.bicep"
78+
if not main_bicep.exists():
79+
typer.echo(f"No Bicep found for {service}; generating from template.")
80+
write_bicep_files(service)
81+
rg = resource_group or default_resource_group(service)
82+
image = app_image or f"ghcr.io/OWNER/{service}:latest"
83+
deploy_file(main_bicep, service, rg, location, image, subscription_id)
84+
85+
86+
@app.command()
87+
def deploy_all(
88+
resource_group_prefix: Optional[str] = typer.Option(None, help="Prefix for RG names; defaults to <service>-rg"),
89+
location: str = typer.Option("eastus", help="Azure region"),
90+
app_image: Optional[str] = typer.Option(None, help="Container image to deploy to AKS"),
91+
subscription_id: Optional[str] = typer.Option(None, help="Azure subscription ID"),
92+
) -> None:
93+
for app_path in APPS_DIR.iterdir():
94+
if not app_path.is_dir():
95+
continue
96+
service = app_path.name
97+
main_bicep = ROOT / "modules" / service / f"{service}-main.bicep"
98+
if not main_bicep.exists():
99+
write_bicep_files(service)
100+
rg = resource_group_prefix or default_resource_group(service)
101+
image = app_image or f"ghcr.io/OWNER/{service}:latest"
102+
deploy_file(main_bicep, service, rg, location, image, subscription_id)
103+
104+
105+
@app.command()
106+
def generate_bicep(
107+
service: Optional[str] = typer.Option(None, help="Service name under apps/ (omit to apply --all)"),
108+
apply_all: bool = typer.Option(False, help="Regenerate Bicep for all services"),
109+
) -> None:
110+
if apply_all:
111+
for app_path in APPS_DIR.iterdir():
112+
if app_path.is_dir():
113+
write_bicep_files(app_path.name)
114+
return
115+
if not service:
116+
raise typer.BadParameter("Provide --service or --apply-all")
117+
app_path = APPS_DIR / service
118+
if not app_path.exists():
119+
raise typer.BadParameter(f"Unknown service path: {app_path}")
120+
write_bicep_files(service)
121+
122+
123+
def write_dockerfile(app_path: Path) -> None:
124+
app_name = app_path.name
125+
docker_path = app_path / "src" / "Dockerfile"
126+
template = load_template("Dockerfile.template")
127+
content = template.format(app_name=app_name)
128+
docker_path.write_text(content, encoding="utf-8")
129+
typer.echo(f"Wrote Dockerfile for {app_name} to {docker_path}")
130+
131+
132+
@app.command()
133+
def generate_dockerfile(
134+
service: Optional[str] = typer.Option(None, help="Service name under apps/ (omit to apply --all)"),
135+
apply_all: bool = typer.Option(False, help="Regenerate Dockerfiles for all services"),
136+
) -> None:
137+
if apply_all:
138+
for app_path in APPS_DIR.iterdir():
139+
if app_path.is_dir():
140+
write_dockerfile(app_path)
141+
return
142+
if not service:
143+
raise typer.BadParameter("Provide --service or --apply-all")
144+
app_path = APPS_DIR / service
145+
if not app_path.exists():
146+
raise typer.BadParameter(f"Unknown service path: {app_path}")
147+
write_dockerfile(app_path)
148+
149+
150+
if __name__ == "__main__":
151+
app()

.infra/config-cli.sh

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Configure local venv for the infra CLI
5+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6+
VENV_DIR="$SCRIPT_DIR/.venv"
7+
PYTHON_BIN="${PYTHON:-python3}"
8+
9+
if [ ! -d "$VENV_DIR" ]; then
10+
"$PYTHON_BIN" -m venv "$VENV_DIR"
11+
fi
12+
13+
# shellcheck disable=SC1091
14+
source "$VENV_DIR/bin/activate"
15+
python -m pip install --upgrade pip
16+
python -m pip install typer
17+
18+
echo "Infra CLI environment ready. Activate with: source $VENV_DIR/bin/activate"
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
targetScope = 'subscription'
2+
3+
param subscriptionId string = subscription().subscriptionId
4+
param location string
5+
param appName string
6+
param resourceGroupName string = '${appName}-rg'
7+
param appImage string = 'ghcr.io/OWNER/crm-campaign-intelligence:latest'
8+
9+
resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = {
10+
name: resourceGroupName
11+
location: location
12+
}
13+
14+
module app './crm-campaign-intelligence.bicep' = {
15+
name: 'crm-campaign-intelligence-resources'
16+
scope: resourceGroup(subscriptionId, resourceGroupName)
17+
dependsOn: [
18+
rg
19+
]
20+
params: {
21+
appName: appName
22+
location: location
23+
appImage: appImage
24+
}
25+
}

0 commit comments

Comments
 (0)