Compare commits

...
7 Commits
Author SHA1 Message Date
stefankoelle 9789eedc9e README.md 2026-08-14 23:37:05 +02:00
Stefan Koelle 8472475a2c Merge pull request #13 from skoelle/renovate/python-3.14-slim
docker Update python:3.14-slim Docker digest to a7fb1e6
2026-08-10 20:19:08 +02:00
renovate[bot] 10d1a71ab7 docker Update python:3.14-slim Docker digest to a7fb1e6 2026-08-10 18:17:40 +00:00
stefankoelle 8260d1a098 renovate no docker-compoes 2026-08-10 20:15:59 +02:00
stefankoelle 523ee24b0b renovate 2026-08-10 19:56:34 +02:00
stefankoelle 3a2d803788 documentation: profiles feature 2026-08-07 21:36:08 +02:00
stefankoelle b90b2f9ba1 new feature: profiles 2026-08-07 21:18:42 +02:00
11 changed files with 200 additions and 104 deletions
+6
View File
@@ -0,0 +1,6 @@
title: "MVG Departures"
emoji: "🚇"
category: code
subcategory: "Smart Home Apps"
status: active
stack: [Python, FastAPI, Uvicorn, Jinja2]
+4 -1
View File
@@ -11,7 +11,7 @@ MVG Departures Monitor: FastAPI-App für Münchner U-/S-Bahn-Abfahrtsanzeige. Mo
| `app/main.py` | FastAPI-App, Endpunkte, Caching, MVG-API-Aufrufe | | `app/main.py` | FastAPI-App, Endpunkte, Caching, MVG-API-Aufrufe |
| `app/config.py` | Config-Loader: YAML → Dataclasses | | `app/config.py` | Config-Loader: YAML → Dataclasses |
| `app/templates/index.html` | Jinja2-HTML-Template (Mobile-UI) | | `app/templates/index.html` | Jinja2-HTML-Template (Mobile-UI) |
| `config.yaml` | Stationskonfiguration (User-Input) | | `config.yaml` | Profil- und Stationskonfiguration (User-Input) |
| `requirements.txt` | Python-Dependencies | | `requirements.txt` | Python-Dependencies |
| `Dockerfile` | Container-Build | | `Dockerfile` | Container-Build |
| `.github/workflows/build.yml` | CI/CD | | `.github/workflows/build.yml` | CI/CD |
@@ -60,6 +60,9 @@ Kein Linter konfiguriert. Bei Bedarf: `ruff check app/` oder `black app/`
### Neue Station hinzufügen ### Neue Station hinzufügen
→ Nur `config.yaml` editieren, kein Code nötig → Nur `config.yaml` editieren, kein Code nötig
### Neues Profil hinzufügen
`config.yaml` unter `profiles:` neuen Eintrag mit `name:` und `stations:` ergänzen
### Neuen Transporttyp hinzufügen ### Neuen Transporttyp hinzufügen
`TYPE_MAP` und `ICON_MAP` in `main.py` erweitern, CSS-Klasse `.icon-X` in `index.html` hinzufügen `TYPE_MAP` und `ICON_MAP` in `main.py` erweitern, CSS-Klasse `.icon-X` in `index.html` hinzufügen
+1 -1
View File
@@ -1,4 +1,4 @@
FROM python:3.14-slim@sha256:cea0e6040540fb2b965b6e7fb5ffa00871e632eef63719f0ea54bca189ce14a6 FROM python:3.14-slim@sha256:a7fb1e634c4a578f9e0bd6327f11a3cde11b7a9395f48e24360c0988bcc5c2bc
WORKDIR /app WORKDIR /app
+64 -53
View File
@@ -1,21 +1,24 @@
# mvg-departures # 🚇 mvg-departures
Kompakter Abfahrtsmonitor fuer MVG U-Bahn und S-Bahn (Muenchen), primaer fuer mobile Nutzung optimiert. Kompakter Abfahrtsmonitor fuer MVG U-Bahn und S-Bahn (Muenchen), primaer fuer mobile Nutzung optimiert.
## Features
- Konfigurierbare Stationsliste (`config.yaml`) mit Typ (UBAHN/SBAHN/TRAM/BUS)
- Ausschluss-Filter pro Station: nur unerwuenschte Zielrichtungen werden ausgeblendet,
neue/unbekannte Ziele werden automatisch weiter angezeigt
- Zwei Endpunkte:
- `/` — mobile-optimierte HTML-Ansicht mit Auto-Refresh (alle 60s per Meta-Refresh)
- `/api/departures` — JSON-Ausgabe derselben Daten
- Anzeige von Linie, Ziel, Abfahrtszeit, Verspaetung und Stoerungsmeldungen
- Visuelle Unterscheidung von U-Bahn (blau) und S-Bahn (gruen) per Icon
![Screenshot der Webseite](docs/screenshot.png) ![Screenshot der Webseite](docs/screenshot.png)
## Konfiguration ## ✨ Features
- 🗺️ Konfigurierbare Stationsliste (`config.yaml`) mit Typ (UBAHN/SBAHN/TRAM/BUS)
- 🚫 Ausschluss-Filter pro Station: nur unerwuenschte Zielrichtungen werden ausgeblendet,
neue/unbekannte Ziele werden automatisch weiter angezeigt
- 👤 Profile zur Gruppierung von Stationen (z.B. "Hinfahrt" und "Rückfahrt")
- 🖱️ Klickbare Kacheln zum Profilwechsel (nur bei >1 Profil)
- 🔗 URL-Param `?profile=<name>` + Auto-Refresh
- 🌐 Zwei Endpunkte:
- `/` — mobile-optimierte HTML-Ansicht mit Auto-Refresh (alle 60s per Meta-Refresh)
- `/api/departures` — JSON-Ausgabe derselben Daten
- 📊 Anzeige von Linie, Ziel, Abfahrtszeit, Verspaetung und Stoerungsmeldungen
- 🎨 Visuelle Unterscheidung von U-Bahn (blau) und S-Bahn (gruen) per Icon
## ⚙️ Konfiguration
Siehe `config.yaml`: Siehe `config.yaml`:
@@ -24,39 +27,45 @@ refresh_seconds: 60
cache_seconds: 20 cache_seconds: 20
departures_limit: 10 departures_limit: 10
stations: profiles:
- name: "Josephsburg, München" - name: "Hinfahrt"
type: "UBAHN" stations:
exclude_destinations: - name: "Josephsburg, München"
- "Messestadt Ost" type: "UBAHN"
- "Messestadt West" exclude_destinations:
- "Messestadt Ost"
- name: "Berg am Laim, München" - "Messestadt West"
type: "SBAHN" - name: "Berg am Laim, München"
exclude_destinations: type: "SBAHN"
- "Erding" exclude_destinations:
- "Markt Schwaben" - "Erding"
- "Markt Schwaben"
- name: "Rückfahrt"
stations:
- name: "Grosshadern, München"
type: "UBAHN"
exclude_destinations: []
``` ```
`exclude_destinations` filtert per exaktem `destination`-Stringvergleich. Alles was `exclude_destinations` filtert per exaktem `destination`-Stringvergleich. Alles was
NICHT in der Liste steht, wird angezeigt — so fallen Linienaenderungen/Umleitungen NICHT in der Liste steht, wird angezeigt — so fallen Linienaenderungen/Umleitungen
sofort auf, statt lautlos verschwunden zu sein. sofort auf, statt lautlos verschwunden zu sein.
## Lokal starten ## 🏃 Lokal starten
```bash ```bash
pip install -r requirements.txt pip install -r requirements.txt
uvicorn app.main:app --reload uvicorn app.main:app --reload
``` ```
## Docker ## 🐳 Docker
```bash ```bash
docker build -t mvg-departures . docker build -t mvg-departures .
docker run -p 8000:8000 -e TZ=Europe/Berlin -v $(pwd)/config.yaml:/app/config.yaml mvg-departures docker run -p 8000:8000 -e TZ=Europe/Berlin -v $(pwd)/config.yaml:/app/config.yaml mvg-departures
``` ```
## Deployment ## 🚀 Deployment
Siehe `docker-compose.yml` im Repo als Referenz. Beispiel (Owner anpassen): Siehe `docker-compose.yml` im Repo als Referenz. Beispiel (Owner anpassen):
@@ -76,36 +85,38 @@ services:
- "com.centurylinklabs.watchtower.enable=true" - "com.centurylinklabs.watchtower.enable=true"
``` ```
## CI/CD ## 🔄 CI/CD
`.github/workflows/build.yml`: `.github/workflows/build.yml`:
- Baut bei jedem Push auf `main` das Docker-Image und pusht es nach - 🏗️ Baut bei jedem Push auf `main` das Docker-Image und pusht es nach
`ghcr.io/<owner>/<repo>:latest` sowie mit Short-SHA-Tag `ghcr.io/<owner>/<repo>:latest` sowie mit Short-SHA-Tag
- Anschliessender Cleanup-Job loescht alte Image-Versionen in der GHCR-Package-Registry - 🧹 Anschliessender Cleanup-Job loescht alte Image-Versionen in der GHCR-Package-Registry
und behaelt nur die letzten 4 Versionen (`min-versions-to-keep: 4`) und behaelt nur die letzten 4 Versionen (`min-versions-to-keep: 4`)
### Voraussetzung ### 📋 Voraussetzung
Repo-Settings → Actions → General → Workflow permissions auf Repo-Settings → Actions → General → Workflow permissions auf
"Read and write permissions" stellen, sonst schlaegt der Push nach GHCR fehl. "Read and write permissions" stellen, sonst schlaegt der Push nach GHCR fehl.
## mvg api ## 📡 mvg api
https://www.mvg.de/api/bgw-pt/v3/locations?query=Josephsburg \ https://www.mvg.de/api/bgw-pt/v3/locations?query=Josephsburg \
https://www.mvg.de/api/bgw-pt/v3/locations?query=Berg%20am \ https://www.mvg.de/api/bgw-pt/v3/locations?query=Berg%20am \
https://www.mvg.de/api/bgw-pt/v3/departures?globalId=de:09162:1220&limit=10&transportTypes=UBAHN \ https://www.mvg.de/api/bgw-pt/v3/departures?globalId=de:09162:1220&limit=10&transportTypes=UBAHN \
https://www.mvg.de/api/bgw-pt/v3/departures?globalId=de:09162:910&limit=10&transportTypes=SBAHN https://www.mvg.de/api/bgw-pt/v3/departures?globalId=de:09162:910&limit=10&transportTypes=SBAHN
## Endpunkte ## 🔌 Endpunkte
| Route | Beschreibung | | Route | Beschreibung |
|---|---| |---|---|
| `/` | HTML-Ansicht, mobile-optimiert, Auto-Refresh alle 60s | | `/` | HTML-Ansicht, mobile-optimiert, Auto-Refresh alle 60s |
| `/api/departures` | JSON-Liste aller gefilterten Abfahrten | | `/?profile=<name>` | HTML-Ansicht für ein bestimmtes Profil |
| `/api/departures` | JSON-Liste aller gefilterten Abfahrten (erstes Profil als Fallback) |
| `/api/departures?profile=<name>` | JSON-Liste für ein bestimmtes Profil |
| `/healthz` | Healthcheck fuer Docker/Watchtower | | `/healthz` | Healthcheck fuer Docker/Watchtower |
## Projektstruktur ## 📁 Projektstruktur
``` ```
mvg-departures/ mvg-departures/
@@ -128,28 +139,28 @@ mvg-departures/
└── build.yml # CI/CD: Docker-Build + GHCR-Push └── build.yml # CI/CD: Docker-Build + GHCR-Push
``` ```
## Tech-Stack ## 🛠️ Tech-Stack
- **Runtime**: Python 3.14 - **Runtime**: Python 3.14
- **Framework**: FastAPI + Uvicorn - 🚀 **Framework**: FastAPI + Uvicorn
- **Templating**: Jinja2 (server-seitig) - 🎨 **Templating**: Jinja2 (server-seitig)
- **MVG-API**: `mvg` Python-Client (oeffentlich, kein Auth noetig) - 🚇 **MVG-API**: `mvg` Python-Client (oeffentlich, kein Auth noetig)
- **Config**: YAML via `pyyaml` - ⚙️ **Config**: YAML via `pyyaml`
- **Container**: Docker (python:3.14-slim) - 🐳 **Container**: Docker (python:3.14-slim)
## Caching ## 💾 Caching
- **Station-ID**: In-memory Dict, lifetime=Session (loest Station-Namen auf) - 🆔 **Station-ID**: In-memory Dict, lifetime=Session (loest Station-Namen auf)
- **Abfahrten**: In-memory Dict, TTL=`cache_seconds` (Default: 20s) pro Station+Typ - ⏱️ **Abfahrten**: In-memory Dict, TTL=`cache_seconds` (Default: 20s) pro Station+Typ
- Keine Persistenz → Neustart = Cache-Verlust - 🔄 Keine Persistenz → Neustart = Cache-Verlust
## Einschraenkungen ## ⚠️ Einschraenkungen
- Kein WebSocket/Live-Updates (nur periodischer Meta-Refresh) - 📡 Kein WebSocket/Live-Updates (nur periodischer Meta-Refresh)
- Keine Datenbank (nur In-Memory) - 💽 Keine Datenbank (nur In-Memory)
- Kein Test-Framework vorhanden - 🧪 Kein Test-Framework vorhanden
- Ein `config.yaml` pro Deployment (kein Multi-Tenancy) - 📄 Ein `config.yaml` pro Deployment (kein Multi-Tenancy)
## Lizenz ## 📜 Lizenz
MIT License - Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de) MIT License - Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
+19 -7
View File
@@ -42,7 +42,9 @@ config.yaml
load_config() → AppConfig (Dataclass) load_config() → AppConfig (Dataclass)
Für jede Station: Profile auswählen (?profile= param oder erstes Profil)
Für jede Station im aktiven Profil:
resolve_station_id(name) → station_id (mit Memory-Cache) resolve_station_id(name) → station_id (mit Memory-Cache)
MvgApi(station_id).departures(limit, transport_types) MvgApi(station_id).departures(limit, transport_types)
@@ -61,7 +63,9 @@ Response: JSON oder HTML-Template
| Route | Methode | Beschreibung | | Route | Methode | Beschreibung |
|---|---|---| |---|---|---|
| `/` | GET | Mobile HTML-Ansicht, Auto-Refresh alle 60s | | `/` | GET | Mobile HTML-Ansicht, Auto-Refresh alle 60s |
| `/api/departures` | GET | JSON-Liste aller gefilterten Abfahrten | | `/?profile=<name>` | GET | HTML-Ansicht für ein bestimmtes Profil |
| `/api/departures` | GET | JSON-Liste aller gefilterten Abfahrten (erstes Profil als Fallback) |
| `/api/departures?profile=<name>` | GET | JSON-Liste für ein bestimmtes Profil |
| `/healthz` | GET | Healthcheck (`{"status": "ok"}`) | | `/healthz` | GET | Healthcheck (`{"status": "ok"}`) |
## 6. Konfiguration (`config.yaml`) ## 6. Konfiguration (`config.yaml`)
@@ -71,11 +75,18 @@ refresh_seconds: 60 # Meta-Refresh-Intervall (HTML)
cache_seconds: 20 # API-Cache pro Station cache_seconds: 20 # API-Cache pro Station
departures_limit: 10 # Max. Abfahrten pro Station departures_limit: 10 # Max. Abfahrten pro Station
stations: profiles:
- name: "Station Name, München" - name: "Hinfahrt" # Profilname (einzigartig)
type: "UBAHN" # UBAHN | SBAHN | TRAM | BUS stations:
exclude_destinations: # Exakter Stringvergleich - name: "Station Name, München"
- "Zielstation" type: "UBAHN" # UBAHN | SBAHN | TRAM | BUS
exclude_destinations: # Exakter Stringvergleich
- "Zielstation"
- name: "Rückfahrt"
stations:
- name: "Andere Station"
type: "SBAHN"
exclude_destinations: []
``` ```
### Filter-Logik ### Filter-Logik
@@ -93,6 +104,7 @@ stations:
## 8. UI ## 8. UI
- Dunkles Theme (`#111417` Background) - Dunkles Theme (`#111417` Background)
- Profil-Kacheln (nur bei >1 Profil): 50% Breite, Basis `#1e242b`, aktiv `#005ca9` (MVG-Blau)
- Farbliche Unterscheidung: U-Bahn (blau `#005ca9`), S-Bahn (grün `#00933b`), Tram (rot `#e2001a`), Bus (grau `#55545a`) - Farbliche Unterscheidung: U-Bahn (blau `#005ca9`), S-Bahn (grün `#00933b`), Tram (rot `#e2001a`), Bus (grau `#55545a`)
- Anzeige: Linie, Ziel, Abfahrtszeit, Verspätung (gelb/rot), Entfall, Störungsmeldungen - Anzeige: Linie, Ziel, Abfahrtszeit, Verspätung (gelb/rot), Entfall, Störungsmeldungen
- Responsive, für Smartphone-First optimiert - Responsive, für Smartphone-First optimiert
+19 -10
View File
@@ -13,12 +13,18 @@ class StationConfig:
exclude_destinations: List[str] = field(default_factory=list) exclude_destinations: List[str] = field(default_factory=list)
@dataclass
class ProfileConfig:
name: str
stations: List[StationConfig] = field(default_factory=list)
@dataclass @dataclass
class AppConfig: class AppConfig:
refresh_seconds: int refresh_seconds: int
cache_seconds: int cache_seconds: int
departures_limit: int departures_limit: int
stations: List[StationConfig] profiles: List[ProfileConfig]
def load_config(path: str = None) -> AppConfig: def load_config(path: str = None) -> AppConfig:
@@ -26,18 +32,21 @@ def load_config(path: str = None) -> AppConfig:
with open(path, "r", encoding="utf-8") as f: with open(path, "r", encoding="utf-8") as f:
raw = yaml.safe_load(f) raw = yaml.safe_load(f)
stations = [ profiles = []
StationConfig( for p in raw.get("profiles", []):
name=s["name"], stations = [
type=s["type"].upper(), StationConfig(
exclude_destinations=s.get("exclude_destinations", []) or [], name=s["name"],
) type=s["type"].upper(),
for s in raw.get("stations", []) exclude_destinations=s.get("exclude_destinations", []) or [],
] )
for s in p.get("stations", [])
]
profiles.append(ProfileConfig(name=p["name"], stations=stations))
return AppConfig( return AppConfig(
refresh_seconds=int(raw.get("refresh_seconds", 60)), refresh_seconds=int(raw.get("refresh_seconds", 60)),
cache_seconds=int(raw.get("cache_seconds", 20)), cache_seconds=int(raw.get("cache_seconds", 20)),
departures_limit=int(raw.get("departures_limit", 10)), departures_limit=int(raw.get("departures_limit", 10)),
stations=stations, profiles=profiles,
) )
+20 -7
View File
@@ -12,7 +12,7 @@ from fastapi.staticfiles import StaticFiles
from mvg import MvgApi, TransportType from mvg import MvgApi, TransportType
from app.config import load_config, StationConfig from app.config import load_config, StationConfig, ProfileConfig
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("mvg-departures") logger = logging.getLogger("mvg-departures")
@@ -98,27 +98,40 @@ def fetch_departures_for_station(station_cfg: StationConfig) -> List[Dict[str, A
return result return result
def get_all_departures() -> List[Dict[str, Any]]: def get_profile_by_name(profile_name: str) -> ProfileConfig:
for profile in config.profiles:
if profile.name == profile_name:
return profile
return config.profiles[0] if config.profiles else None
def get_departures_for_profile(profile: ProfileConfig) -> List[Dict[str, Any]]:
all_deps: List[Dict[str, Any]] = [] all_deps: List[Dict[str, Any]] = []
for station_cfg in config.stations: for station_cfg in profile.stations:
all_deps.extend(fetch_departures_for_station(station_cfg)) all_deps.extend(fetch_departures_for_station(station_cfg))
all_deps.sort(key=lambda d: d["time_epoch"] or 0) all_deps.sort(key=lambda d: d["time_epoch"] or 0)
return all_deps return all_deps
@app.get("/api/departures") @app.get("/api/departures")
def api_departures(): def api_departures(profile: str = None):
return JSONResponse(content={"departures": get_all_departures()}) active_profile = get_profile_by_name(profile) if profile else config.profiles[0] if config.profiles else None
if not active_profile:
return JSONResponse(content={"departures": []})
return JSONResponse(content={"departures": get_departures_for_profile(active_profile)})
@app.get("/") @app.get("/")
def index(request: Request): def index(request: Request, profile: str = None):
departures = get_all_departures() active_profile = get_profile_by_name(profile) if profile else config.profiles[0] if config.profiles else None
departures = get_departures_for_profile(active_profile) if active_profile else []
return templates.TemplateResponse( return templates.TemplateResponse(
request, request,
"index.html", "index.html",
{ {
"departures": departures, "departures": departures,
"profiles": config.profiles,
"active_profile": active_profile,
"refresh_seconds": config.refresh_seconds, "refresh_seconds": config.refresh_seconds,
"generated_at": datetime.now().strftime("%H:%M:%S"), "generated_at": datetime.now().strftime("%H:%M:%S"),
}, },
+13 -1
View File
@@ -5,11 +5,23 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Abfahrten</title> <title>Abfahrten</title>
<meta http-equiv="refresh" content="{{ refresh_seconds }}"> <meta http-equiv="refresh" content="{{ refresh_seconds }};url=/?profile={{ active_profile.name }}">
</head> </head>
<body> <body>
<div style="max-width: 375px; margin: 0 auto; width: 100%;"> <div style="max-width: 375px; margin: 0 auto; width: 100%;">
<h1>Abfahrten <span class="updated">{{ generated_at }}</span></h1> <h1>Abfahrten <span class="updated">{{ generated_at }}</span></h1>
{% if profiles|length > 1 %}
<div style="display: flex; gap: 8px; margin-bottom: 16px;">
{% for p in profiles %}
<a href="/?profile={{ p.name }}"
style="flex: 1; padding: 12px; text-align: center; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 18px; color: {{ 'white' if p.name == active_profile.name else '#9aa5b1' }}; background: {{ '#005ca9' if p.name == active_profile.name else '#1e242b' }};">
{{ p.name }}
</a>
{% endfor %}
</div>
{% endif %}
{% if departures %} {% if departures %}
<table> <table>
{% for d in departures %} {% for d in departures %}
+15 -12
View File
@@ -7,15 +7,18 @@ refresh_seconds: 60
cache_seconds: 20 cache_seconds: 20
departures_limit: 10 departures_limit: 10
stations: profiles:
- name: "Josephsburg, München" - name: "Hinfahrt"
type: "UBAHN" stations:
exclude_destinations: - name: "Josephsburg, München"
- "Messestadt Ost" type: "UBAHN"
- "Messestadt West" exclude_destinations:
- "Messestadt Ost"
- name: "Berg am Laim, München" - "Messestadt West"
type: "SBAHN" - name: "Berg am Laim, München"
exclude_destinations: type: "SBAHN"
- "Erding" exclude_destinations:
- "Markt Schwaben" - "Erding"
- "Markt Schwaben"
- name: "Rückfahrt"
stations: []
+1 -1
View File
@@ -1,6 +1,6 @@
services: services:
mvg-departures: mvg-departures:
image: ghcr.io/skoelle/mvg-departures:latest@sha256:c93ca797aa5dea84611a44d9463dd557816b664d30cdc7e956335f3e7628dcb0 image: ghcr.io/skoelle/mvg-departures:latest
container_name: mvg-departures container_name: mvg-departures
ports: ports:
- "8000:8000" - "8000:8000"
+38 -11
View File
@@ -3,23 +3,50 @@
"extends": [ "extends": [
"config:recommended", "config:recommended",
"docker:pinDigests", "docker:pinDigests",
"helpers:pinGitHubActionDigests", "helpers:pinGitHubActionDigests"
":configMigration",
":pinDevDependencies",
"abandonments:recommended",
"security:minimumReleaseAgeNpm",
":dependencyDashboard"
], ],
"schedule": ["before 6am on monday"],
"timezone": "Europe/Berlin",
"automergeStrategy": "squash", "automergeStrategy": "squash",
"ignorePaths": ["docker-compose.yml"],
"packageRules": [ "packageRules": [
{ {
"matchUpdateTypes": ["minor", "patch"], "description": "GitHub Actions - grouped automerge",
"groupName": "{{depName}}", "matchManagers": ["github-actions"],
"commitMessagePrefix": "fix(deps)" "groupName": "github-actions",
"commitMessagePrefix": "ci",
"automerge": true
}, },
{ {
"description": "Docker Digests - automerge (security patches)",
"matchManagers": ["dockerfile"],
"matchUpdateTypes": ["digest"],
"commitMessagePrefix": "docker",
"automerge": true
},
{
"description": "Docker Major/Minor - manual review",
"matchManagers": ["dockerfile"],
"matchUpdateTypes": ["major", "minor"],
"groupName": "docker-base-images",
"commitMessagePrefix": "docker",
"automerge": false
},
{
"description": "Python minor/patch - automerge",
"matchManagers": ["pip_requirements"],
"matchUpdateTypes": ["minor", "patch"],
"commitMessagePrefix": "fix(deps)",
"automerge": true
},
{
"description": "Python major - no automerge, label",
"matchManagers": ["pip_requirements"],
"matchUpdateTypes": ["major"], "matchUpdateTypes": ["major"],
"groupName": "major-{{depName}}" "commitMessagePrefix": "chore(deps)",
"automerge": false,
"labels": ["major-update"]
} }
] ],
"ignoreDeps": ["node", "express", "node-cron", "nodemailer", "actions/setup-node"]
} }