feat: Add Grafana auto-update & improve classifier
Add auto-update for Grafana on Synology via SSH + docker compose, with review examples and a context.txt describing the specific setup (Prometheus + Node Exporter, no plugins) so the classifier can ignore plugin-only breaking changes. The classifier now loads an optional context.txt per service to tailor decisions to the actual deployment. It also truncates large release notes intelligently, keeping important sections (breaking changes, deprecations, migration notes) while dropping long PR lists that add no signal. test_classifier.py now also loads context.txt and accepts any service name as argument.
This commit is contained in:
parent
67a9509eaa
commit
ea90b8569b
10 changed files with 185 additions and 1 deletions
33
services/grafana/README.md
Normal file
33
services/grafana/README.md
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Grafana auto-update setup
|
||||||
|
|
||||||
|
Grafana runs in Docker on a Synology NAS (192.168.1.30) via Container Manager.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
The auto-update script SSHes into the Synology and runs `docker compose pull && up -d`. For this to work without sudo, the SSH user needs to be in the `docker` group.
|
||||||
|
|
||||||
|
### One-time Synology setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh jeena@192.168.1.30
|
||||||
|
sudo synogroup --add docker
|
||||||
|
sudo synogroup --memberadd docker jeena
|
||||||
|
sudo chown root:docker /var/run/docker.sock
|
||||||
|
exit
|
||||||
|
```
|
||||||
|
|
||||||
|
Log out and back in (new SSH session) for the group to take effect. This persists through DSM updates and reboots.
|
||||||
|
|
||||||
|
### Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh jeena@192.168.1.30 '/usr/local/bin/docker ps'
|
||||||
|
```
|
||||||
|
|
||||||
|
Should list containers without permission errors.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Docker is at `/usr/local/bin/docker` on Synology (not in PATH for non-interactive SSH)
|
||||||
|
- The compose project is at `/volume1/docker/monitoring/`
|
||||||
|
- `context.txt` tells the AI classifier this is a simple Prometheus + Node Exporter setup with no plugins, so plugin-only breaking changes are classified as auto
|
||||||
4
services/grafana/auto-update.sh
Executable file
4
services/grafana/auto-update.sh
Executable file
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ssh jeena@192.168.1.30 'cd /volume1/docker/monitoring && /usr/local/bin/docker compose pull && /usr/local/bin/docker compose up -d' 2>&1 | tail -5
|
||||||
1
services/grafana/context.txt
Normal file
1
services/grafana/context.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Simple setup with Prometheus and Node Exporter data sources only. No third-party plugins installed. Basic dashboards for system monitoring. Running in Docker on Synology NAS.
|
||||||
25
services/grafana/review-examples/auto-1.txt
Normal file
25
services/grafana/review-examples/auto-1.txt
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
action: auto
|
||||||
|
reason: Patch release with security fixes, minor enhancements, and bug fixes. No breaking changes or manual steps required.
|
||||||
|
---
|
||||||
|
[Download page](https://grafana.com/grafana/download/12.4.2)
|
||||||
|
[What's new highlights](https://grafana.com/docs/grafana/latest/whatsnew/)
|
||||||
|
|
||||||
|
### Features and enhancements
|
||||||
|
|
||||||
|
- **Analytics tab:** Improve voice over accessibility (Enterprise)
|
||||||
|
- **Dashboards a11y:** Do not open time zonemenu on focus
|
||||||
|
- **Dashboards:** Resolve display names by identity in version history
|
||||||
|
- **Plugins:** Forward AWS SDK credential chain env vars to external AWS plugins
|
||||||
|
- **Public Dashboards:** Prevent unintended CRUD operations from different orgs
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- **IAM:** Handle NULL team_member.external column to fix dashboard loading
|
||||||
|
- **Plugins:** Fix installer IsDisabled condition
|
||||||
|
- **Plugins:** Forward PLUGIN_UNIX_SOCKET_DIR to plugin processes to fix tmp dir in restricted environments
|
||||||
|
- **Security:** Fixes CVE-2026-27876
|
||||||
|
- **Security:** Fixes CVE-2026-27877
|
||||||
|
- **Security:** Fixes CVE-2026-28375
|
||||||
|
- **Security:** Fixes CVE-2026-27879
|
||||||
|
- **Security:** Fixes CVE-2026-27880
|
||||||
|
- **Security:** Fixes CVE-2026-33375
|
||||||
18
services/grafana/review-examples/auto-2.txt
Normal file
18
services/grafana/review-examples/auto-2.txt
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
action: auto
|
||||||
|
reason: Security-only patch release. No breaking changes, no configuration changes, no manual steps required.
|
||||||
|
---
|
||||||
|
[Download page](https://grafana.com/grafana/download/12.3.6)
|
||||||
|
[What's new highlights](https://grafana.com/docs/grafana/latest/whatsnew/)
|
||||||
|
|
||||||
|
### Features and enhancements
|
||||||
|
|
||||||
|
- **Public Dashboards:** Prevent unintended CRUD operations from different orgs
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- **Security:** Fixes CVE-2026-27876
|
||||||
|
- **Security:** Fixes CVE-2026-27877
|
||||||
|
- **Security:** Fixes CVE-2026-28375
|
||||||
|
- **Security:** Fixes CVE-2026-27879
|
||||||
|
- **Security:** Fixes CVE-2026-27880
|
||||||
|
- **Security:** Fixes CVE-2026-27876
|
||||||
20
services/grafana/review-examples/auto-3.txt
Normal file
20
services/grafana/review-examples/auto-3.txt
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
action: auto
|
||||||
|
reason: The only breaking change is about plugin environment variables, which does not affect a simple Prometheus + Node Exporter setup with no third-party plugins installed.
|
||||||
|
---
|
||||||
|
[Download page](https://grafana.com/grafana/download/12.4.0)
|
||||||
|
[What's new highlights](https://grafana.com/docs/grafana/latest/whatsnew/)
|
||||||
|
|
||||||
|
### Breaking changes
|
||||||
|
|
||||||
|
- **Plugins:** Prevent passing host environment variables to plugin processes by default
|
||||||
|
|
||||||
|
### Features and enhancements
|
||||||
|
|
||||||
|
- **Alerting:** Add Alert Rules tabs navigation with feature toggle
|
||||||
|
- **Dashboards:** Add ability to share dashboards externally
|
||||||
|
- **Plugins:** Forward AWS SDK credential chain env vars to external AWS plugins
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- **Dashboards:** Fix panel rendering issue with long time ranges
|
||||||
|
- **Prometheus:** Fix label matching in metric browser
|
||||||
17
services/grafana/review-examples/manual-1.txt
Normal file
17
services/grafana/review-examples/manual-1.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
action: manual
|
||||||
|
reason: Major version bump that removes the viewers_can_edit setting and changes anonymous auth enforcement. These configuration changes could affect dashboard access. Admin should review config before upgrading.
|
||||||
|
---
|
||||||
|
[Download page](https://grafana.com/grafana/download/12.0.0)
|
||||||
|
[What's new highlights](https://grafana.com/docs/grafana/latest/whatsnew/)
|
||||||
|
|
||||||
|
### Breaking changes
|
||||||
|
|
||||||
|
- **Alerting:** Make $value return the query value in case when a single datasource is used
|
||||||
|
- **Anonymous:** Enforce org role Viewer setting
|
||||||
|
- **Config:** Removes setting `viewers_can_edit`
|
||||||
|
- **Frontend:** Remove Angular (Enterprise)
|
||||||
|
- **Chore:** Enabling failWrongDSUID by default in Grafana 12
|
||||||
|
|
||||||
|
### Deprecations
|
||||||
|
|
||||||
|
- **Alerting:** Deprecate OpsGenie integration
|
||||||
16
services/grafana/review-examples/manual-2.txt
Normal file
16
services/grafana/review-examples/manual-2.txt
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
action: manual
|
||||||
|
reason: Major version changes secret encryption so data source credentials created with v9+ cannot be decrypted by older versions. Rollback becomes difficult. Admin should back up the database before upgrading. Also changes log format which may affect monitoring.
|
||||||
|
---
|
||||||
|
[Download page](https://grafana.com/grafana/download/9.0.0)
|
||||||
|
[What's new highlights](https://grafana.com/docs/grafana/latest/whatsnew/)
|
||||||
|
|
||||||
|
### Breaking changes
|
||||||
|
|
||||||
|
* Any secret (data sources credential, alert manager credential, etc, etc) created or modified with Grafana v9.0 won't be decryptable from any previous version (by default) because the way encrypted secrets are stored into the database has changed. Although secrets created or modified with previous versions will still be decryptable by Grafana v9.0.
|
||||||
|
* If required, although generally discouraged, the `disableEnvelopeEncryption` feature toggle can be enabled to keep envelope encryption disabled once updating to Grafana v9.0.
|
||||||
|
* In case of need to rollback to an earlier version of Grafana (i.e. Grafana v8.x) for any reason, after being created or modified any secret with Grafana v9.0, the `envelopeEncryption` feature toggle will need to be enabled to keep backwards compatibility.
|
||||||
|
* Alternatively, backing up the Grafana database before updating could be a good idea to prevent disasters.
|
||||||
|
|
||||||
|
The format of log messages have been updated, `lvl` is now `level` and `eror` and `dbug` has been replaced with `error` and `debug`. The precision of timestamps has been increased. To smooth the transition, it is possible to opt-out of the new log format by enabling the feature toggle `oldlog`. This option will be removed in a future minor release.
|
||||||
|
|
||||||
|
The `Rename by regex` transformation has been improved to allow global patterns of the form `/<stringToReplace>/g`. Depending on the regex match used, this may cause some transformations to behave slightly differently.
|
||||||
|
|
@ -66,12 +66,20 @@ def load_examples(directory: Path) -> list[dict]:
|
||||||
|
|
||||||
|
|
||||||
def build_messages(examples: list[dict], target_notes: str, current: str = "1.0.0", latest: str = "2.0.0") -> list[dict]:
|
def build_messages(examples: list[dict], target_notes: str, current: str = "1.0.0", latest: str = "2.0.0") -> list[dict]:
|
||||||
|
context_file = EXAMPLES_DIR.parent / "context.txt"
|
||||||
|
context = context_file.read_text(encoding="utf-8").strip() if context_file.is_file() else None
|
||||||
|
|
||||||
system = (
|
system = (
|
||||||
f"You are an update safety classifier for self-hosted {SERVICE_NAME} instances.\n"
|
f"You are an update safety classifier for self-hosted {SERVICE_NAME} instances.\n"
|
||||||
f"Given release notes for a {SERVICE_NAME} update, decide whether the update can be\n"
|
f"Given release notes for a {SERVICE_NAME} update, decide whether the update can be\n"
|
||||||
"applied automatically (action: auto) or requires the admin to intervene\n"
|
"applied automatically (action: auto) or requires the admin to intervene\n"
|
||||||
"manually before or during the update (action: manual).\n\n"
|
"manually before or during the update (action: manual).\n\n"
|
||||||
|
)
|
||||||
|
if context:
|
||||||
|
system += f"Instance context: {context}\n\n"
|
||||||
|
system += (
|
||||||
"Rules:\n"
|
"Rules:\n"
|
||||||
|
"- Only flag as manual if the breaking changes actually affect this specific setup.\n"
|
||||||
"- Classify only THIS release, not future or past releases mentioned in the notes.\n"
|
"- Classify only THIS release, not future or past releases mentioned in the notes.\n"
|
||||||
"- Warnings or announcements about breaking changes in upcoming versions do NOT\n"
|
"- Warnings or announcements about breaking changes in upcoming versions do NOT\n"
|
||||||
" make the current release manual.\n"
|
" make the current release manual.\n"
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,10 @@ class Classifier:
|
||||||
if not examples:
|
if not examples:
|
||||||
return {"action": "manual", "reason": "No review examples configured; defaulting to manual review."}
|
return {"action": "manual", "reason": "No review examples configured; defaulting to manual review."}
|
||||||
|
|
||||||
messages = self._build_messages(service_dir.name, examples, release_notes, current, latest)
|
release_notes = self._truncate(release_notes)
|
||||||
|
context_file = service_dir / "context.txt"
|
||||||
|
context = context_file.read_text(encoding="utf-8").strip() if context_file.is_file() else None
|
||||||
|
messages = self._build_messages(service_dir.name, examples, release_notes, current, latest, context)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
payload = json.dumps({
|
payload = json.dumps({
|
||||||
|
|
@ -85,13 +88,19 @@ class Classifier:
|
||||||
release_notes: str,
|
release_notes: str,
|
||||||
current: str,
|
current: str,
|
||||||
latest: str,
|
latest: str,
|
||||||
|
context: Optional[str] = None,
|
||||||
) -> List[Dict[str, str]]:
|
) -> List[Dict[str, str]]:
|
||||||
system_prompt = (
|
system_prompt = (
|
||||||
f"You are an update safety classifier for self-hosted {service_name} instances.\n"
|
f"You are an update safety classifier for self-hosted {service_name} instances.\n"
|
||||||
"Given release notes for a software update, decide whether the update can be applied\n"
|
"Given release notes for a software update, decide whether the update can be applied\n"
|
||||||
"automatically (action: auto) or requires the admin to intervene manually before or\n"
|
"automatically (action: auto) or requires the admin to intervene manually before or\n"
|
||||||
"during the update (action: manual).\n\n"
|
"during the update (action: manual).\n\n"
|
||||||
|
)
|
||||||
|
if context:
|
||||||
|
system_prompt += f"Instance context: {context}\n\n"
|
||||||
|
system_prompt += (
|
||||||
"Rules:\n"
|
"Rules:\n"
|
||||||
|
"- Only flag as manual if the breaking changes actually affect this specific setup.\n"
|
||||||
"- Classify only THIS release, not future or past releases mentioned in the notes.\n"
|
"- Classify only THIS release, not future or past releases mentioned in the notes.\n"
|
||||||
"- Warnings or announcements about breaking changes in upcoming versions do NOT\n"
|
"- Warnings or announcements about breaking changes in upcoming versions do NOT\n"
|
||||||
" make the current release manual.\n"
|
" make the current release manual.\n"
|
||||||
|
|
@ -113,3 +122,36 @@ class Classifier:
|
||||||
"content": f"Upgrading from {current} to {latest}.\n\nRelease notes:\n{release_notes}",
|
"content": f"Upgrading from {current} to {latest}.\n\nRelease notes:\n{release_notes}",
|
||||||
})
|
})
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _truncate(text: str, max_chars: int = 16000) -> str:
|
||||||
|
if len(text) <= max_chars:
|
||||||
|
return text
|
||||||
|
|
||||||
|
# Split into sections by markdown headers
|
||||||
|
import re
|
||||||
|
sections = re.split(r'(?=^#{1,4}\s)', text, flags=re.MULTILINE)
|
||||||
|
|
||||||
|
important_keywords = re.compile(
|
||||||
|
r'breaking|deprecat|migrat|important|caution|warning|manual|required|must|before.*upgrad',
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Always keep the intro (first section) and any important sections
|
||||||
|
kept = []
|
||||||
|
skipped = 0
|
||||||
|
for i, section in enumerate(sections):
|
||||||
|
if i == 0 or important_keywords.search(section[:500]):
|
||||||
|
kept.append(section)
|
||||||
|
else:
|
||||||
|
skipped += 1
|
||||||
|
|
||||||
|
result = "".join(kept)
|
||||||
|
if skipped:
|
||||||
|
result += f"\n\n[... {skipped} sections of features/bug fixes omitted ...]"
|
||||||
|
|
||||||
|
# Final safety truncation if still too long
|
||||||
|
if len(result) > max_chars:
|
||||||
|
result = result[:max_chars] + "\n\n[... truncated ...]"
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue