-
Notifications
You must be signed in to change notification settings - Fork 1
Fix: Jira dashboard config params not reaching notifier #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
1e5bed4
e451758
aabf89b
27bd5f3
678d928
b267d42
5ec0cb5
35a5e75
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| #!/usr/bin/env python3 | ||
| """ | ||
| Verify that dashboard-provided Jira config (via Socket API) flows into JiraNotifier. | ||
|
|
||
| This script does NOT call Jira. It only loads Socket Basics config, builds the | ||
| NotificationManager, and inspects JiraNotifier params. | ||
|
|
||
| Expected env vars: | ||
| - SOCKET_SECURITY_API_KEY or SOCKET_SECURITY_API_TOKEN (required) | ||
| - SOCKET_ORG or SOCKET_ORG_SLUG (recommended; auto-discovery is attempted) | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import json | ||
| import logging | ||
| import os | ||
| import sys | ||
| from pathlib import Path | ||
|
|
||
| from socket_basics.core.config import merge_json_and_env_config | ||
| from socket_basics.core.notification.manager import NotificationManager | ||
|
|
||
|
|
||
| def _summarize_jira(notifier) -> dict: | ||
| return { | ||
| "server": getattr(notifier, "server", None), | ||
| "project": getattr(notifier, "project", None), | ||
| "email": getattr(notifier, "email", None), | ||
| "api_token_present": bool(getattr(notifier, "api_token", None)), | ||
| } | ||
|
|
||
|
|
||
| def _load_dotenv(dotenv_path: Path) -> None: | ||
| if not dotenv_path.exists(): | ||
| return | ||
| try: | ||
| for raw_line in dotenv_path.read_text().splitlines(): | ||
| line = raw_line.strip() | ||
| if not line or line.startswith("#"): | ||
| continue | ||
| if line.startswith("export "): | ||
| line = line[len("export "):].strip() | ||
| if "=" not in line: | ||
| continue | ||
| key, val = line.split("=", 1) | ||
| key = key.strip() | ||
| val = val.strip().strip("'").strip('"') | ||
| if key and key not in os.environ: | ||
| os.environ[key] = val | ||
| except Exception: | ||
| # Best-effort; do not fail if .env parsing is imperfect | ||
| pass | ||
|
|
||
|
|
||
| def main() -> int: | ||
| logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s") | ||
|
|
||
| repo_root = Path(__file__).resolve().parents[1] | ||
| _load_dotenv(repo_root / ".env") | ||
|
|
||
| try: | ||
| config_dict = merge_json_and_env_config() | ||
| except Exception as exc: | ||
| print(f"ERROR: failed to load config: {exc}") | ||
| return 2 | ||
|
|
||
| # Load notifications.yaml and build manager | ||
| try: | ||
| import yaml | ||
|
|
||
| cfg_path = repo_root / "socket_basics" / "notifications.yaml" | ||
| notif_cfg = None | ||
| if cfg_path.exists(): | ||
| with open(cfg_path, "r") as f: | ||
| notif_cfg = yaml.safe_load(f) | ||
| except Exception as exc: | ||
| print(f"ERROR: failed to load notifications.yaml: {exc}") | ||
| return 2 | ||
|
|
||
| nm = NotificationManager(notif_cfg, app_config=config_dict) | ||
| nm.load_from_config() | ||
|
|
||
| jira_notifiers = [n for n in nm.notifiers if getattr(n, "name", "").lower() == "jira"] | ||
| if not jira_notifiers: | ||
| print("Jira notifier not enabled. Check that dashboard config includes jira_url or env provides JIRA_URL.") | ||
| return 1 | ||
|
|
||
| # Print details for first Jira notifier | ||
| summary = _summarize_jira(jira_notifiers[0]) | ||
| print("Jira notifier config summary:") | ||
| print(json.dumps(summary, indent=2)) | ||
|
|
||
| missing = [k for k, v in summary.items() if k in ("server", "project", "email") and not v] | ||
| if missing: | ||
| print(f"WARNING: missing expected fields: {', '.join(missing)}") | ||
| return 1 | ||
|
|
||
| print("OK: Jira dashboard config appears to be wired into JiraNotifier.") | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(main()) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,20 +21,23 @@ class JiraNotifier(BaseNotifier): | |
| def __init__(self, params: Dict[str, Any] | None = None): | ||
| super().__init__(params or {}) | ||
| # JIRA configuration from params, env variables, or app config | ||
| # Parameter names match dashboard config keys (jira_url, jira_project) | ||
| self.server = ( | ||
| self.config.get('server') or | ||
| self.config.get('jira_url') or | ||
| get_jira_url() | ||
| ) | ||
| self.project = ( | ||
| self.config.get('project') or | ||
| self.config.get('jira_project') or | ||
| get_jira_project() | ||
| ) | ||
| self.email = ( | ||
| self.config.get('email') or | ||
| self.config.get('jira_email') or | ||
| self.config.get('auth', {}).get('email') or | ||
| get_jira_email() | ||
| ) | ||
| self.api_token = ( | ||
| self.config.get('api_token') or | ||
| self.config.get('jira_api_token') or | ||
| self.config.get('auth', {}).get('api_token') or | ||
| get_jira_api_token() | ||
|
Comment on lines
33
to
41
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same note here regarding the legacy keys ( If it's worth including for maximum compatibility, we could do something like: self.config.get('jira_email') or
self.config.get('email') or
self.config.get('auth', {}).get('email') or
get_jira_email()And repeat the same pattern for |
||
| ) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| # Tests | ||
|
|
||
| Quick-start for running tests locally: | ||
|
|
||
| ```bash | ||
| ./venv/bin/python -m pytest | ||
| ``` | ||
|
|
||
| Notes: | ||
| - Tests are lightweight and should not require external services. | ||
| - Use the local config-wiring script for Jira setup validation: | ||
| `./venv/bin/python scripts/verify_jira_dashboard_config.py` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| from socket_basics.core.notification.jira_notifier import JiraNotifier | ||
| from socket_basics.core.config import normalize_api_config | ||
|
|
||
|
|
||
| def test_jira_notifier_reads_new_param_names(): | ||
| n = JiraNotifier( | ||
| { | ||
| "jira_url": "https://acme.atlassian.net", | ||
| "jira_project": "SEC", | ||
| "jira_email": "bot@acme.example", | ||
| "jira_api_token": "token123", | ||
| } | ||
| ) | ||
| assert n.server == "https://acme.atlassian.net" | ||
| assert n.project == "SEC" | ||
| assert n.email == "bot@acme.example" | ||
| assert n.api_token == "token123" | ||
|
|
||
|
|
||
| def test_jira_notifier_falls_back_to_auth_dict(): | ||
| n = JiraNotifier( | ||
| { | ||
| "jira_url": "https://acme.atlassian.net", | ||
| "jira_project": "SEC", | ||
| "auth": {"email": "auth@acme.example", "api_token": "auth-token"}, | ||
| } | ||
| ) | ||
| assert n.email == "auth@acme.example" | ||
| assert n.api_token == "auth-token" | ||
|
|
||
|
|
||
| def test_normalize_api_config_maps_jira_keys(): | ||
| normalized = normalize_api_config( | ||
| { | ||
| "jiraUrl": "https://acme.atlassian.net", | ||
| "jiraProject": "SEC", | ||
| "jiraEmail": "bot@acme.example", | ||
| "jiraApiToken": "token123", | ||
| } | ||
| ) | ||
| assert normalized["jira_url"] == "https://acme.atlassian.net" | ||
| assert normalized["jira_project"] == "SEC" | ||
| assert normalized["jira_email"] == "bot@acme.example" | ||
| assert normalized["jira_api_token"] == "token123" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| import os | ||
|
|
||
| from socket_basics.core.notification.manager import NotificationManager | ||
|
|
||
|
|
||
| def _base_cfg(): | ||
| return { | ||
| "notifiers": { | ||
| "jira": { | ||
| "module_path": "socket_basics.core.notification.jira_notifier", | ||
| "class": "JiraNotifier", | ||
| "parameters": [ | ||
| {"name": "jira_url", "env_variable": "INPUT_JIRA_URL", "type": "str"}, | ||
| {"name": "jira_project", "env_variable": "INPUT_JIRA_PROJECT", "type": "str"}, | ||
| {"name": "jira_email", "env_variable": "INPUT_JIRA_EMAIL", "type": "str"}, | ||
| {"name": "jira_api_token", "env_variable": "INPUT_JIRA_API_TOKEN", "type": "str"}, | ||
| ], | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
| def test_param_precedence_app_config_over_env_and_default(monkeypatch): | ||
| cfg = _base_cfg() | ||
| # default should be overridden by env, then app_config should win | ||
| cfg["notifiers"]["jira"]["parameters"][0]["default"] = "https://default.example" | ||
| monkeypatch.setenv("INPUT_JIRA_URL", "https://env.example") | ||
|
|
||
| nm = NotificationManager(cfg, app_config={"jira_url": "https://app.example"}) | ||
| nm.load_from_config() | ||
|
|
||
| jira = next(n for n in nm.notifiers if getattr(n, "name", "") == "jira") | ||
| assert jira.server == "https://app.example" | ||
|
|
||
|
|
||
| def test_param_precedence_env_over_default(monkeypatch): | ||
| cfg = _base_cfg() | ||
| cfg["notifiers"]["jira"]["parameters"][0]["default"] = "https://default.example" | ||
| monkeypatch.setenv("INPUT_JIRA_URL", "https://env.example") | ||
|
|
||
| nm = NotificationManager(cfg, app_config={}) | ||
| nm.load_from_config() | ||
|
|
||
| jira = next(n for n in nm.notifiers if getattr(n, "name", "") == "jira") | ||
| assert jira.server == "https://env.example" | ||
|
|
||
|
|
||
| def test_param_precedence_default_used_when_no_env_or_app_config(monkeypatch): | ||
| cfg = _base_cfg() | ||
| # Use default for jira_project while enabling via app_config jira_url | ||
| cfg["notifiers"]["jira"]["parameters"][1]["default"] = "DEFAULTPROJ" | ||
| monkeypatch.delenv("INPUT_JIRA_PROJECT", raising=False) | ||
|
|
||
| nm = NotificationManager(cfg, app_config={"jira_url": "https://app.example"}) | ||
| nm.load_from_config() | ||
|
|
||
| jira = next(n for n in nm.notifiers if getattr(n, "name", "") == "jira") | ||
| assert jira.project == "DEFAULTPROJ" | ||
|
|
||
|
|
||
| def test_jira_enabled_via_app_config(monkeypatch): | ||
| cfg = _base_cfg() | ||
| monkeypatch.delenv("INPUT_JIRA_URL", raising=False) | ||
|
|
||
| nm = NotificationManager(cfg, app_config={"jira_url": "https://app.example"}) | ||
| nm.load_from_config() | ||
|
|
||
| jira = next(n for n in nm.notifiers if getattr(n, "name", "") == "jira") | ||
| assert jira.server == "https://app.example" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the params are being renamed, do you know if any customers use custom configs that may rely on the old JIRA keys (
serverorproject)? If so, we may need to include some backwards compatibility fallback mechanism.