Skip to content

Commit 1b6fe1d

Browse files
feat(RELEASE-2482): convert collect-slack-notification-params to python
Convert the collect-slack-notification-params managed task to python. Assisted-by: Claude Code Signed-off-by: Filip Nikolovski <fnikolov@redhat.com>
1 parent 586cbe7 commit 1b6fe1d

3 files changed

Lines changed: 712 additions & 0 deletions

File tree

.coverage

52 KB
Binary file not shown.
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
#!/usr/bin/env python3
2+
"""Collect Slack notification parameters from Release CRs and the data file."""
3+
4+
from __future__ import annotations
5+
6+
import json
7+
import os
8+
from pathlib import Path
9+
from typing import Any, NamedTuple
10+
11+
import tekton
12+
from file import load_json_dict
13+
from logger import logger
14+
15+
PROG = "collect_slack_notification_params.py"
16+
17+
DEFAULT_HAC_URL = "https://console.redhat.com/preview/application-pipeline/workspaces"
18+
19+
20+
class ReleaseMetadata(NamedTuple):
21+
"""Subset of release CR fields needed for the Slack message."""
22+
23+
origin_workspace: str
24+
target_workspace: str
25+
release_name: str
26+
release_pipeline_name: str
27+
28+
29+
def validate_input_files(
30+
data_file: Path,
31+
snapshot_file: Path,
32+
release_file: Path,
33+
) -> None:
34+
"""Raise RuntimeError when any required input file is missing."""
35+
for label, path in [
36+
("data", data_file),
37+
("snapshot", snapshot_file),
38+
("release", release_file),
39+
]:
40+
if not path.is_file():
41+
raise RuntimeError(f"No valid {label} file was provided.")
42+
43+
44+
def _get_slack_field(data: dict[str, Any], key: str) -> str | None:
45+
"""Return a string value from data['slack'][key], or None if absent."""
46+
slack = data.get("slack")
47+
if isinstance(slack, dict) and key in slack:
48+
return str(slack[key])
49+
return None
50+
51+
52+
def extract_slack_secret(data: dict[str, Any]) -> str | None:
53+
"""Return the slack-notification-secret value, or None if absent."""
54+
return _get_slack_field(data, "slack-notification-secret")
55+
56+
57+
def extract_slack_keyname(data: dict[str, Any]) -> str | None:
58+
"""Return the slack-webhook-notification-secret-keyname, or None."""
59+
return _get_slack_field(data, "slack-webhook-notification-secret-keyname")
60+
61+
62+
def extract_release_metadata(release: dict[str, Any]) -> ReleaseMetadata:
63+
"""Extract release metadata needed for the Slack message."""
64+
metadata = release.get("metadata", {})
65+
status = release.get("status", {})
66+
67+
origin_namespace = metadata.get("namespace", "")
68+
target_namespace = status.get("target", "")
69+
70+
return ReleaseMetadata(
71+
origin_workspace=origin_namespace.replace("-tenant", ""),
72+
target_workspace=target_namespace.replace("-tenant", ""),
73+
release_name=metadata.get("name", ""),
74+
release_pipeline_name=status.get("managedProcessing", {}).get("pipelineRun", ""),
75+
)
76+
77+
78+
def build_urls(
79+
hac_url: str,
80+
meta: ReleaseMetadata,
81+
component_group: str,
82+
) -> tuple[str, str]:
83+
"""Build the release detail and pipeline-run URLs."""
84+
release_url = (
85+
f"{hac_url}/{meta.origin_workspace}/applications"
86+
f"/{component_group}/releases/{meta.release_name}"
87+
)
88+
release_plr_url = (
89+
f"{hac_url}/{meta.target_workspace}/applications"
90+
f"/{component_group}/pipelineruns"
91+
f"/{meta.release_pipeline_name}"
92+
)
93+
return release_url, release_plr_url
94+
95+
96+
def build_slack_message(
97+
meta: ReleaseMetadata,
98+
component_group: str,
99+
release_url: str,
100+
release_plr_url: str,
101+
) -> str:
102+
"""Build a Slack Block Kit JSON message string."""
103+
message: dict[str, Any] = {
104+
"blocks": [
105+
{
106+
"type": "header",
107+
"text": {
108+
"type": "plain_text",
109+
"text": "RHTAP Release Service\n",
110+
"emoji": True,
111+
},
112+
},
113+
{"type": "divider"},
114+
{
115+
"type": "rich_text",
116+
"elements": [
117+
{
118+
"type": "rich_text_section",
119+
"elements": [
120+
{
121+
"type": "text",
122+
"text": "Release ",
123+
"style": {"bold": True},
124+
},
125+
{
126+
"type": "text",
127+
"text": (
128+
f"{meta.origin_workspace}"
129+
f"/{component_group}"
130+
f"/{meta.release_name}"
131+
),
132+
},
133+
],
134+
}
135+
],
136+
},
137+
{"type": "divider"},
138+
{
139+
"type": "rich_text",
140+
"elements": [
141+
{
142+
"type": "rich_text_section",
143+
"elements": [
144+
{
145+
"type": "emoji",
146+
"name": "@@CIRCLE_TYPE@@",
147+
},
148+
{"type": "text", "text": " "},
149+
{
150+
"type": "text",
151+
"text": " @@STATUS_TEXT@@ ",
152+
"style": {"bold": True},
153+
},
154+
],
155+
}
156+
],
157+
},
158+
{"type": "divider"},
159+
{
160+
"type": "section",
161+
"text": {
162+
"type": "mrkdwn",
163+
"text": f"<{release_url}|Release Details>",
164+
},
165+
},
166+
{
167+
"type": "section",
168+
"text": {
169+
"type": "mrkdwn",
170+
"text": (f"<{release_plr_url}" "|Release PipelineRun Logs>"),
171+
},
172+
},
173+
{"type": "divider"},
174+
]
175+
}
176+
return json.dumps(message)
177+
178+
179+
def _write_empty(
180+
result_message: Path,
181+
result_secret: Path,
182+
result_keyname: Path,
183+
) -> None:
184+
"""Write empty strings to Tekton result files."""
185+
result_message.write_text("", encoding="utf-8")
186+
result_secret.write_text("", encoding="utf-8")
187+
result_keyname.write_text("", encoding="utf-8")
188+
189+
190+
def collect_params(
191+
*,
192+
data_file: Path,
193+
snapshot_file: Path,
194+
release_file: Path,
195+
hac_url: str,
196+
result_message: Path,
197+
result_secret: Path,
198+
result_keyname: Path,
199+
) -> int:
200+
"""Collect Slack notification parameters and write result files."""
201+
validate_input_files(data_file, snapshot_file, release_file)
202+
203+
data = load_json_dict(data_file)
204+
205+
secret = extract_slack_secret(data)
206+
if secret is None:
207+
logger.info(
208+
"No secret name provided via" " 'slack.slack-notification-secret' key in Data."
209+
)
210+
_write_empty(result_message, result_secret, result_keyname)
211+
return 0
212+
213+
result_secret.write_text(secret, encoding="utf-8")
214+
215+
keyname = extract_slack_keyname(data)
216+
if keyname is None:
217+
logger.info(
218+
"No secret key name provided via"
219+
" 'slack.slack-webhook-notification-secret-keyname'"
220+
" key in Data."
221+
)
222+
_write_empty(result_message, result_secret, result_keyname)
223+
return 0
224+
225+
result_keyname.write_text(keyname, encoding="utf-8")
226+
227+
release = load_json_dict(release_file)
228+
snapshot = load_json_dict(snapshot_file)
229+
230+
meta = extract_release_metadata(release)
231+
component_group = snapshot.get("componentGroup", "")
232+
release_url, release_plr_url = build_urls(hac_url, meta, component_group)
233+
234+
message = build_slack_message(meta, component_group, release_url, release_plr_url)
235+
result_message.write_text(message, encoding="utf-8")
236+
237+
return 0
238+
239+
240+
def main() -> int:
241+
"""Read environment, collect Slack notification params, write results."""
242+
data_dir = Path(tekton.require_env("DATA_DIR"))
243+
data_path = tekton.require_env("DATA_PATH")
244+
snapshot_path = tekton.require_env("SNAPSHOT_PATH")
245+
release_path = tekton.require_env("RELEASE_PATH")
246+
247+
hac_url = os.environ.get("HAC_URL", DEFAULT_HAC_URL).strip()
248+
249+
(
250+
result_message,
251+
result_secret,
252+
result_keyname,
253+
) = tekton.result_paths_from_env(
254+
"RESULT_MESSAGE",
255+
"RESULT_SLACK_NOTIFICATION_SECRET",
256+
"RESULT_SLACK_NOTIFICATION_SECRET_KEYNAME",
257+
)
258+
259+
return collect_params(
260+
data_file=data_dir / data_path,
261+
snapshot_file=data_dir / snapshot_path,
262+
release_file=data_dir / release_path,
263+
hac_url=hac_url,
264+
result_message=result_message,
265+
result_secret=result_secret,
266+
result_keyname=result_keyname,
267+
)
268+
269+
270+
if __name__ == "__main__":
271+
raise SystemExit(main())

0 commit comments

Comments
 (0)