Skip to content

Commit 1b7ceb1

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 1b7ceb1

3 files changed

Lines changed: 692 additions & 0 deletions

File tree

.gitignore

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

0 commit comments

Comments
 (0)