From 600a7d00e1bdb89bd5ab99cce8c046b8a7cf7658 Mon Sep 17 00:00:00 2001 From: Rahul Chandrasekaran Date: Mon, 21 Sep 2026 11:57:15 +0530 Subject: [PATCH 1/7] feat: implement release notifier for Slack and Teams --- .github/scripts/notify_release.py | 202 +++++++++++++++++++++++++++ .github/workflows/notify-release.yml | 24 ++++ 2 files changed, 226 insertions(+) create mode 100644 .github/scripts/notify_release.py create mode 100644 .github/workflows/notify-release.yml diff --git a/.github/scripts/notify_release.py b/.github/scripts/notify_release.py new file mode 100644 index 00000000..727bd92e --- /dev/null +++ b/.github/scripts/notify_release.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +"""Posts a "new release" notification to Slack and/or Microsoft Teams for a +just-published debricked/cli GitHub release. + +Run by .github/workflows/notify-release.yml on every `release: published` +event. Mirrors the formatting of the internal-tools/deploy-notifier-cli Rust +tool: a "What's Changed" pull request list plus a "Full Changelog" link, both +taken from GitHub's own release-notes generator. +""" + +import json +import os +import re +import sys +import urllib.error +import urllib.request + +GITHUB_API = "https://api.github.com" + +# PR bullet lines look like: +# "* by @<author> in https://github.com/<owner>/<repo>/pull/<number>" +PR_LINE_RE = re.compile( + r"^\*\s+(?P<title>.+?)\s+by\s+@[\w-]+\s+in\s+" + r"(?P<url>https://github\.com/\S+/pull/(?P<number>\d+))\s*$", + re.MULTILINE, +) +CHANGELOG_RE = re.compile(r"\*\*Full Changelog\*\*:\s*(\S+)") + + +def env(name: str) -> str: + value = os.environ.get(name, "").strip() + if not value: + print(f"::error::{name} is required", file=sys.stderr) + sys.exit(1) + return value + + +def http_json(method: str, url: str, headers: dict, body: dict | None = None) -> dict: + data = json.dumps(body).encode() if body is not None else None + request = urllib.request.Request(url, data=data, method=method, headers=headers) + try: + with urllib.request.urlopen(request) as response: + return json.load(response) + except urllib.error.HTTPError as error: + detail = error.read().decode(errors="replace") + print(f"::error::{method} {url} returned {error.code}: {detail}", file=sys.stderr) + raise + + +def generate_release_notes(repo: str, token: str, tag: str) -> dict: + # GitHub auto-detects the previous tag itself; this is the same content + # shown by the "Generate release notes" button in the GitHub UI. + return http_json( + "POST", + f"{GITHUB_API}/repos/{repo}/releases/generate-notes", + headers={ + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {token}", + "X-GitHub-Api-Version": "2022-11-28", + }, + body={"tag_name": tag}, + ) + + +def parse_pull_requests(body: str) -> list[dict]: + return [ + {"number": int(m["number"]), "title": m["title"], "url": m["url"]} + for m in (match.groupdict() for match in PR_LINE_RE.finditer(body)) + ] + + +def parse_changelog_url(body: str) -> str | None: + match = CHANGELOG_RE.search(body) + return match.group(1) if match else None + + +def slack_payload(tag: str, release_url: str, prs: list[dict], changelog_url: str | None) -> dict: + if prs: + pr_text = "".join(f"\u2022 <{pr['url']}|{pr['title']}>\n" for pr in prs) + else: + pr_text = "_No pull requests in this release_\n" + if changelog_url: + pr_text += f"\n<{changelog_url}|Full Changelog>\n" + + return { + "text": f"New Debricked CLI release: {tag}!", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ( + f":sparkles: *New Debricked CLI release: {tag}* :sparkles:\n" + f"<{release_url}|View release on GitHub>\n" + ), + }, + }, + { + "type": "section", + "text": {"type": "mrkdwn", "text": f"*What's Changed*\n{pr_text}"}, + }, + ], + } + + +def teams_payload(tag: str, release_url: str, prs: list[dict], changelog_url: str | None) -> dict: + if prs: + pr_text = "\n".join(f"- [{pr['title']}]({pr['url']})" for pr in prs) + else: + pr_text = "_No pull requests in this release_" + if changelog_url: + pr_text += f"\n\n[Full Changelog]({changelog_url})" + + pr_word = "pull request" if len(prs) == 1 else "pull requests" + + body = [ + { + "type": "TextBlock", + "text": f"\u2728 New Debricked CLI Release: {tag} \u2728", + "wrap": True, + "weight": "Bolder", + "size": "Large", + }, + {"type": "TextBlock", "text": "<at>channel</at>", "wrap": True}, + { + "type": "TextBlock", + "text": f"This release includes {len(prs)} {pr_word}. [View release on GitHub]({release_url})", + "wrap": True, + "size": "Small", + "isSubtle": True, + }, + { + "type": "TextBlock", + "text": "What's Changed", + "wrap": True, + "weight": "Bolder", + "size": "Medium", + "separator": True, + }, + {"type": "TextBlock", "text": pr_text, "wrap": True}, + ] + + return { + "type": "message", + "attachments": [ + { + "contentType": "application/vnd.microsoft.card.adaptive", + "contentUrl": None, + "content": { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.4", + "body": body, + "msteams": { + "entities": [ + { + "type": "mention", + "text": "<at>channel</at>", + "mentioned": {"id": "channel", "name": "channel"}, + } + ] + }, + }, + } + ], + } + + +def post_webhook(url: str, payload: dict) -> None: + http_json("POST", url, headers={"Content-Type": "application/json"}, body=payload) + + +def main() -> None: + repo = env("GITHUB_REPOSITORY") + token = env("GH_TOKEN") + tag = env("RELEASE_TAG") + release_url = env("RELEASE_URL") + slack_webhook = os.environ.get("RELEASE_BOT_SLACK_WEBHOOK", "").strip() + teams_webhook = os.environ.get("RELEASE_BOT_TEAMS_WEBHOOK", "").strip() + + if not slack_webhook and not teams_webhook: + print( + "::error::configure the RELEASE_BOT_SLACK_WEBHOOK or RELEASE_BOT_TEAMS_WEBHOOK repository secret", + file=sys.stderr, + ) + sys.exit(1) + + notes = generate_release_notes(repo, token, tag) + prs = parse_pull_requests(notes["body"]) + changelog_url = parse_changelog_url(notes["body"]) + print(f"Found {len(prs)} pull request(s) for release {tag}") + + if slack_webhook: + post_webhook(slack_webhook, slack_payload(tag, release_url, prs, changelog_url)) + print("Posted to Slack") + if teams_webhook: + post_webhook(teams_webhook, teams_payload(tag, release_url, prs, changelog_url)) + print("Posted to Teams") + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/notify-release.yml b/.github/workflows/notify-release.yml new file mode 100644 index 00000000..955b7934 --- /dev/null +++ b/.github/workflows/notify-release.yml @@ -0,0 +1,24 @@ +name: Notify release + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + notify: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Notify Slack and Teams + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_TAG: ${{ github.event.release.tag_name }} + RELEASE_URL: ${{ github.event.release.html_url }} + SLACK_WEBHOOK_URL: ${{ secrets.RELEASE_BOT_SLACK_WEBHOOK}} + RELEASE_BOT_TEAMS_WEBHOOK: ${{ secrets.RELEASE_BOT_TEAMS_WEBHOOK }} + run: python3 .github/scripts/notify_release.py From dd87cb9420b8cf4f3d8f7601e49a424807a118c7 Mon Sep 17 00:00:00 2001 From: Rahul Chandrasekaran <rc2658535@gmail.com> Date: Mon, 21 Sep 2026 13:29:04 +0530 Subject: [PATCH 2/7] feat: enhance release notifier to include asset details in Slack and Teams notifications --- .github/scripts/notify_release.py | 141 ++++++++++++++++++++++----- .github/workflows/notify-release.yml | 3 +- 2 files changed, 119 insertions(+), 25 deletions(-) diff --git a/.github/scripts/notify_release.py b/.github/scripts/notify_release.py index 727bd92e..7304d4a2 100644 --- a/.github/scripts/notify_release.py +++ b/.github/scripts/notify_release.py @@ -35,18 +35,25 @@ def env(name: str) -> str: return value -def http_json(method: str, url: str, headers: dict, body: dict | None = None) -> dict: - data = json.dumps(body).encode() if body is not None else None +def http_request(method: str, url: str, headers: dict, data: bytes | None = None) -> bytes: request = urllib.request.Request(url, data=data, method=method, headers=headers) try: with urllib.request.urlopen(request) as response: - return json.load(response) + return response.read() except urllib.error.HTTPError as error: detail = error.read().decode(errors="replace") print(f"::error::{method} {url} returned {error.code}: {detail}", file=sys.stderr) raise +def http_json(method: str, url: str, headers: dict, body: dict | None = None) -> dict: + data = json.dumps(body).encode() if body is not None else None + # Webhook endpoints (Slack, Teams/Power Automate) commonly reply with an + # empty or plain-text body on success, not JSON, so only parse if present. + raw = http_request(method, url, headers, data) + return json.loads(raw) if raw.strip() else {} + + def generate_release_notes(repo: str, token: str, tag: str) -> dict: # GitHub auto-detects the previous tag itself; this is the same content # shown by the "Generate release notes" button in the GitHub UI. @@ -74,7 +81,36 @@ def parse_changelog_url(body: str) -> str | None: return match.group(1) if match else None -def slack_payload(tag: str, release_url: str, prs: list[dict], changelog_url: str | None) -> dict: +def human_size(num_bytes: float) -> str: + # Matches GitHub's own release page style, e.g. "612 Bytes", "1.51 KB", "31.0 MB". + for unit in ("Bytes", "KB", "MB", "GB"): + if num_bytes < 1024 or unit == "GB": + return f"{num_bytes:.0f} {unit}" if unit == "Bytes" else f"{num_bytes:.2f} {unit}" + num_bytes /= 1024 + return f"{num_bytes:.2f} GB" + + +def parse_assets(raw_assets_json: str) -> list[dict]: + # Comes straight from the `release` event payload (`github.event.release.assets`), + # so no extra GitHub API call is needed to list them. + assets = json.loads(raw_assets_json or "[]") + return [ + { + "name": asset["name"], + "url": asset["browser_download_url"], + "size": human_size(asset["size"]), + } + for asset in assets + ] + + +def slack_payload( + tag: str, + release_url: str, + prs: list[dict], + changelog_url: str | None, + assets: list[dict], +) -> dict: if prs: pr_text = "".join(f"\u2022 <{pr['url']}|{pr['title']}>\n" for pr in prs) else: @@ -82,28 +118,65 @@ def slack_payload(tag: str, release_url: str, prs: list[dict], changelog_url: st if changelog_url: pr_text += f"\n<{changelog_url}|Full Changelog>\n" + blocks = [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ( + f":sparkles: *New Debricked CLI release: {tag}* :sparkles:\n" + f"<{release_url}|View release on GitHub>\n" + ), + }, + }, + { + "type": "section", + "text": {"type": "mrkdwn", "text": f"*What's Changed*\n{pr_text}"}, + }, + ] + blocks.extend(asset_blocks(assets)) + return { "text": f"New Debricked CLI release: {tag}!", - "blocks": [ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": ( - f":sparkles: *New Debricked CLI release: {tag}* :sparkles:\n" - f"<{release_url}|View release on GitHub>\n" - ), - }, - }, + "blocks": blocks, + } + + +# Slack rejects section blocks whose text exceeds 3000 characters, so a long +# asset list (this repo publishes 25+ per release) is split across blocks. +MAX_BLOCK_TEXT_LEN = 2900 + + +def asset_blocks(assets: list[dict]) -> list[dict]: + if not assets: + return [ { "type": "section", - "text": {"type": "mrkdwn", "text": f"*What's Changed*\n{pr_text}"}, - }, - ], - } + "text": {"type": "mrkdwn", "text": "*Assets (0)*\n_No assets attached to this release_\n"}, + } + ] + + heading = f"*Assets ({len(assets)})*\n" + blocks = [] + current = heading + for asset in assets: + line = f"\u2022 <{asset['url']}|{asset['name']}> ({asset['size']})\n" + if len(current) + len(line) > MAX_BLOCK_TEXT_LEN and current != heading: + blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": current}}) + current = "" + current += line + if current: + blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": current}}) + return blocks -def teams_payload(tag: str, release_url: str, prs: list[dict], changelog_url: str | None) -> dict: +def teams_payload( + tag: str, + release_url: str, + prs: list[dict], + changelog_url: str | None, + assets: list[dict], +) -> dict: if prs: pr_text = "\n".join(f"- [{pr['title']}]({pr['url']})" for pr in prs) else: @@ -111,6 +184,11 @@ def teams_payload(tag: str, release_url: str, prs: list[dict], changelog_url: st if changelog_url: pr_text += f"\n\n[Full Changelog]({changelog_url})" + if assets: + assets_text = "\n".join(f"- [{a['name']}]({a['url']}) ({a['size']})" for a in assets) + else: + assets_text = "_No assets attached to this release_" + pr_word = "pull request" if len(prs) == 1 else "pull requests" body = [ @@ -138,6 +216,15 @@ def teams_payload(tag: str, release_url: str, prs: list[dict], changelog_url: st "separator": True, }, {"type": "TextBlock", "text": pr_text, "wrap": True}, + { + "type": "TextBlock", + "text": f"Assets ({len(assets)})", + "wrap": True, + "weight": "Bolder", + "size": "Medium", + "separator": True, + }, + {"type": "TextBlock", "text": assets_text, "wrap": True}, ] return { @@ -167,7 +254,12 @@ def teams_payload(tag: str, release_url: str, prs: list[dict], changelog_url: st def post_webhook(url: str, payload: dict) -> None: - http_json("POST", url, headers={"Content-Type": "application/json"}, body=payload) + http_request( + "POST", + url, + headers={"Content-Type": "application/json"}, + data=json.dumps(payload).encode(), + ) def main() -> None: @@ -175,6 +267,7 @@ def main() -> None: token = env("GH_TOKEN") tag = env("RELEASE_TAG") release_url = env("RELEASE_URL") + assets = parse_assets(os.environ.get("RELEASE_ASSETS_JSON", "")) slack_webhook = os.environ.get("RELEASE_BOT_SLACK_WEBHOOK", "").strip() teams_webhook = os.environ.get("RELEASE_BOT_TEAMS_WEBHOOK", "").strip() @@ -188,13 +281,13 @@ def main() -> None: notes = generate_release_notes(repo, token, tag) prs = parse_pull_requests(notes["body"]) changelog_url = parse_changelog_url(notes["body"]) - print(f"Found {len(prs)} pull request(s) for release {tag}") + print(f"Found {len(prs)} pull request(s) and {len(assets)} asset(s) for release {tag}") if slack_webhook: - post_webhook(slack_webhook, slack_payload(tag, release_url, prs, changelog_url)) + post_webhook(slack_webhook, slack_payload(tag, release_url, prs, changelog_url, assets)) print("Posted to Slack") if teams_webhook: - post_webhook(teams_webhook, teams_payload(tag, release_url, prs, changelog_url)) + post_webhook(teams_webhook, teams_payload(tag, release_url, prs, changelog_url, assets)) print("Posted to Teams") diff --git a/.github/workflows/notify-release.yml b/.github/workflows/notify-release.yml index 955b7934..9cc3669c 100644 --- a/.github/workflows/notify-release.yml +++ b/.github/workflows/notify-release.yml @@ -19,6 +19,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_TAG: ${{ github.event.release.tag_name }} RELEASE_URL: ${{ github.event.release.html_url }} - SLACK_WEBHOOK_URL: ${{ secrets.RELEASE_BOT_SLACK_WEBHOOK}} + RELEASE_ASSETS_JSON: ${{ toJson(github.event.release.assets) }} + RELEASE_BOT_SLACK_WEBHOOK: ${{ secrets.RELEASE_BOT_SLACK_WEBHOOK }} RELEASE_BOT_TEAMS_WEBHOOK: ${{ secrets.RELEASE_BOT_TEAMS_WEBHOOK }} run: python3 .github/scripts/notify_release.py From 8e1004a3cb77033f988652c86adea2cb2272cf4f Mon Sep 17 00:00:00 2001 From: Rahul Chandrasekaran <rc2658535@gmail.com> Date: Mon, 21 Sep 2026 14:07:32 +0530 Subject: [PATCH 3/7] feat: implement release notifier enhancements for workflow completion and asset retrieval --- .github/scripts/notify_release.py | 19 ++++++++++++++++++- .github/workflows/notify-release.yml | 12 +++++++----- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/.github/scripts/notify_release.py b/.github/scripts/notify_release.py index 7304d4a2..ecf3c38b 100644 --- a/.github/scripts/notify_release.py +++ b/.github/scripts/notify_release.py @@ -69,6 +69,18 @@ def generate_release_notes(repo: str, token: str, tag: str) -> dict: ) +def get_release(repo: str, token: str, tag: str) -> dict: + return http_json( + "GET", + f"{GITHUB_API}/repos/{repo}/releases/tags/{tag}", + headers={ + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {token}", + "X-GitHub-Api-Version": "2022-11-28", + }, + ) + + def parse_pull_requests(body: str) -> list[dict]: return [ {"number": int(m["number"]), "title": m["title"], "url": m["url"]} @@ -267,7 +279,12 @@ def main() -> None: token = env("GH_TOKEN") tag = env("RELEASE_TAG") release_url = env("RELEASE_URL") - assets = parse_assets(os.environ.get("RELEASE_ASSETS_JSON", "")) + assets_json = os.environ.get("RELEASE_ASSETS_JSON", "").strip() + if assets_json: + assets = parse_assets(assets_json) + else: + release = get_release(repo, token, tag) + assets = parse_assets(json.dumps(release.get("assets", []))) slack_webhook = os.environ.get("RELEASE_BOT_SLACK_WEBHOOK", "").strip() teams_webhook = os.environ.get("RELEASE_BOT_TEAMS_WEBHOOK", "").strip() diff --git a/.github/workflows/notify-release.yml b/.github/workflows/notify-release.yml index 9cc3669c..51e9ae8e 100644 --- a/.github/workflows/notify-release.yml +++ b/.github/workflows/notify-release.yml @@ -1,14 +1,16 @@ name: Notify release on: - release: - types: [published] + workflow_run: + workflows: [Release] + types: [completed] permissions: contents: read jobs: notify: + if: ${{ github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest steps: - name: Check out repository @@ -17,9 +19,9 @@ jobs: - name: Notify Slack and Teams env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ github.event.release.tag_name }} - RELEASE_URL: ${{ github.event.release.html_url }} - RELEASE_ASSETS_JSON: ${{ toJson(github.event.release.assets) }} + RELEASE_TAG: ${{ github.event.workflow_run.head_branch }} + RELEASE_URL: https://github.com/${{ github.repository }}/releases/tag/${{ github.event.workflow_run.head_branch }} + RELEASE_ASSETS_JSON: "" RELEASE_BOT_SLACK_WEBHOOK: ${{ secrets.RELEASE_BOT_SLACK_WEBHOOK }} RELEASE_BOT_TEAMS_WEBHOOK: ${{ secrets.RELEASE_BOT_TEAMS_WEBHOOK }} run: python3 .github/scripts/notify_release.py From d732048beb909c5800d2b6c4cd7dde18278eea2e Mon Sep 17 00:00:00 2001 From: Rahul Chandrasekaran <rc2658535@gmail.com> Date: Tue, 22 Sep 2026 14:16:24 +0530 Subject: [PATCH 4/7] feat: implement release notifier in Go and update workflow to use it --- .github/scripts/notify_release.py | 312 ------------------------- .github/workflows/notify-release.yml | 7 +- cmd/release-notifier/main.go | 328 +++++++++++++++++++++++++++ cmd/release-notifier/main_test.go | 54 +++++ 4 files changed, 388 insertions(+), 313 deletions(-) delete mode 100644 .github/scripts/notify_release.py create mode 100644 cmd/release-notifier/main.go create mode 100644 cmd/release-notifier/main_test.go diff --git a/.github/scripts/notify_release.py b/.github/scripts/notify_release.py deleted file mode 100644 index ecf3c38b..00000000 --- a/.github/scripts/notify_release.py +++ /dev/null @@ -1,312 +0,0 @@ -#!/usr/bin/env python3 -"""Posts a "new release" notification to Slack and/or Microsoft Teams for a -just-published debricked/cli GitHub release. - -Run by .github/workflows/notify-release.yml on every `release: published` -event. Mirrors the formatting of the internal-tools/deploy-notifier-cli Rust -tool: a "What's Changed" pull request list plus a "Full Changelog" link, both -taken from GitHub's own release-notes generator. -""" - -import json -import os -import re -import sys -import urllib.error -import urllib.request - -GITHUB_API = "https://api.github.com" - -# PR bullet lines look like: -# "* <title> by @<author> in https://github.com/<owner>/<repo>/pull/<number>" -PR_LINE_RE = re.compile( - r"^\*\s+(?P<title>.+?)\s+by\s+@[\w-]+\s+in\s+" - r"(?P<url>https://github\.com/\S+/pull/(?P<number>\d+))\s*$", - re.MULTILINE, -) -CHANGELOG_RE = re.compile(r"\*\*Full Changelog\*\*:\s*(\S+)") - - -def env(name: str) -> str: - value = os.environ.get(name, "").strip() - if not value: - print(f"::error::{name} is required", file=sys.stderr) - sys.exit(1) - return value - - -def http_request(method: str, url: str, headers: dict, data: bytes | None = None) -> bytes: - request = urllib.request.Request(url, data=data, method=method, headers=headers) - try: - with urllib.request.urlopen(request) as response: - return response.read() - except urllib.error.HTTPError as error: - detail = error.read().decode(errors="replace") - print(f"::error::{method} {url} returned {error.code}: {detail}", file=sys.stderr) - raise - - -def http_json(method: str, url: str, headers: dict, body: dict | None = None) -> dict: - data = json.dumps(body).encode() if body is not None else None - # Webhook endpoints (Slack, Teams/Power Automate) commonly reply with an - # empty or plain-text body on success, not JSON, so only parse if present. - raw = http_request(method, url, headers, data) - return json.loads(raw) if raw.strip() else {} - - -def generate_release_notes(repo: str, token: str, tag: str) -> dict: - # GitHub auto-detects the previous tag itself; this is the same content - # shown by the "Generate release notes" button in the GitHub UI. - return http_json( - "POST", - f"{GITHUB_API}/repos/{repo}/releases/generate-notes", - headers={ - "Accept": "application/vnd.github+json", - "Authorization": f"Bearer {token}", - "X-GitHub-Api-Version": "2022-11-28", - }, - body={"tag_name": tag}, - ) - - -def get_release(repo: str, token: str, tag: str) -> dict: - return http_json( - "GET", - f"{GITHUB_API}/repos/{repo}/releases/tags/{tag}", - headers={ - "Accept": "application/vnd.github+json", - "Authorization": f"Bearer {token}", - "X-GitHub-Api-Version": "2022-11-28", - }, - ) - - -def parse_pull_requests(body: str) -> list[dict]: - return [ - {"number": int(m["number"]), "title": m["title"], "url": m["url"]} - for m in (match.groupdict() for match in PR_LINE_RE.finditer(body)) - ] - - -def parse_changelog_url(body: str) -> str | None: - match = CHANGELOG_RE.search(body) - return match.group(1) if match else None - - -def human_size(num_bytes: float) -> str: - # Matches GitHub's own release page style, e.g. "612 Bytes", "1.51 KB", "31.0 MB". - for unit in ("Bytes", "KB", "MB", "GB"): - if num_bytes < 1024 or unit == "GB": - return f"{num_bytes:.0f} {unit}" if unit == "Bytes" else f"{num_bytes:.2f} {unit}" - num_bytes /= 1024 - return f"{num_bytes:.2f} GB" - - -def parse_assets(raw_assets_json: str) -> list[dict]: - # Comes straight from the `release` event payload (`github.event.release.assets`), - # so no extra GitHub API call is needed to list them. - assets = json.loads(raw_assets_json or "[]") - return [ - { - "name": asset["name"], - "url": asset["browser_download_url"], - "size": human_size(asset["size"]), - } - for asset in assets - ] - - -def slack_payload( - tag: str, - release_url: str, - prs: list[dict], - changelog_url: str | None, - assets: list[dict], -) -> dict: - if prs: - pr_text = "".join(f"\u2022 <{pr['url']}|{pr['title']}>\n" for pr in prs) - else: - pr_text = "_No pull requests in this release_\n" - if changelog_url: - pr_text += f"\n<{changelog_url}|Full Changelog>\n" - - blocks = [ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": ( - f":sparkles: *New Debricked CLI release: {tag}* :sparkles:\n" - f"<{release_url}|View release on GitHub>\n" - ), - }, - }, - { - "type": "section", - "text": {"type": "mrkdwn", "text": f"*What's Changed*\n{pr_text}"}, - }, - ] - blocks.extend(asset_blocks(assets)) - - return { - "text": f"New Debricked CLI release: {tag}!", - "blocks": blocks, - } - - -# Slack rejects section blocks whose text exceeds 3000 characters, so a long -# asset list (this repo publishes 25+ per release) is split across blocks. -MAX_BLOCK_TEXT_LEN = 2900 - - -def asset_blocks(assets: list[dict]) -> list[dict]: - if not assets: - return [ - { - "type": "section", - "text": {"type": "mrkdwn", "text": "*Assets (0)*\n_No assets attached to this release_\n"}, - } - ] - - heading = f"*Assets ({len(assets)})*\n" - blocks = [] - current = heading - for asset in assets: - line = f"\u2022 <{asset['url']}|{asset['name']}> ({asset['size']})\n" - if len(current) + len(line) > MAX_BLOCK_TEXT_LEN and current != heading: - blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": current}}) - current = "" - current += line - if current: - blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": current}}) - return blocks - - -def teams_payload( - tag: str, - release_url: str, - prs: list[dict], - changelog_url: str | None, - assets: list[dict], -) -> dict: - if prs: - pr_text = "\n".join(f"- [{pr['title']}]({pr['url']})" for pr in prs) - else: - pr_text = "_No pull requests in this release_" - if changelog_url: - pr_text += f"\n\n[Full Changelog]({changelog_url})" - - if assets: - assets_text = "\n".join(f"- [{a['name']}]({a['url']}) ({a['size']})" for a in assets) - else: - assets_text = "_No assets attached to this release_" - - pr_word = "pull request" if len(prs) == 1 else "pull requests" - - body = [ - { - "type": "TextBlock", - "text": f"\u2728 New Debricked CLI Release: {tag} \u2728", - "wrap": True, - "weight": "Bolder", - "size": "Large", - }, - {"type": "TextBlock", "text": "<at>channel</at>", "wrap": True}, - { - "type": "TextBlock", - "text": f"This release includes {len(prs)} {pr_word}. [View release on GitHub]({release_url})", - "wrap": True, - "size": "Small", - "isSubtle": True, - }, - { - "type": "TextBlock", - "text": "What's Changed", - "wrap": True, - "weight": "Bolder", - "size": "Medium", - "separator": True, - }, - {"type": "TextBlock", "text": pr_text, "wrap": True}, - { - "type": "TextBlock", - "text": f"Assets ({len(assets)})", - "wrap": True, - "weight": "Bolder", - "size": "Medium", - "separator": True, - }, - {"type": "TextBlock", "text": assets_text, "wrap": True}, - ] - - return { - "type": "message", - "attachments": [ - { - "contentType": "application/vnd.microsoft.card.adaptive", - "contentUrl": None, - "content": { - "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", - "type": "AdaptiveCard", - "version": "1.4", - "body": body, - "msteams": { - "entities": [ - { - "type": "mention", - "text": "<at>channel</at>", - "mentioned": {"id": "channel", "name": "channel"}, - } - ] - }, - }, - } - ], - } - - -def post_webhook(url: str, payload: dict) -> None: - http_request( - "POST", - url, - headers={"Content-Type": "application/json"}, - data=json.dumps(payload).encode(), - ) - - -def main() -> None: - repo = env("GITHUB_REPOSITORY") - token = env("GH_TOKEN") - tag = env("RELEASE_TAG") - release_url = env("RELEASE_URL") - assets_json = os.environ.get("RELEASE_ASSETS_JSON", "").strip() - if assets_json: - assets = parse_assets(assets_json) - else: - release = get_release(repo, token, tag) - assets = parse_assets(json.dumps(release.get("assets", []))) - slack_webhook = os.environ.get("RELEASE_BOT_SLACK_WEBHOOK", "").strip() - teams_webhook = os.environ.get("RELEASE_BOT_TEAMS_WEBHOOK", "").strip() - - if not slack_webhook and not teams_webhook: - print( - "::error::configure the RELEASE_BOT_SLACK_WEBHOOK or RELEASE_BOT_TEAMS_WEBHOOK repository secret", - file=sys.stderr, - ) - sys.exit(1) - - notes = generate_release_notes(repo, token, tag) - prs = parse_pull_requests(notes["body"]) - changelog_url = parse_changelog_url(notes["body"]) - print(f"Found {len(prs)} pull request(s) and {len(assets)} asset(s) for release {tag}") - - if slack_webhook: - post_webhook(slack_webhook, slack_payload(tag, release_url, prs, changelog_url, assets)) - print("Posted to Slack") - if teams_webhook: - post_webhook(teams_webhook, teams_payload(tag, release_url, prs, changelog_url, assets)) - print("Posted to Teams") - - -if __name__ == "__main__": - main() diff --git a/.github/workflows/notify-release.yml b/.github/workflows/notify-release.yml index 51e9ae8e..9248ba78 100644 --- a/.github/workflows/notify-release.yml +++ b/.github/workflows/notify-release.yml @@ -16,6 +16,11 @@ jobs: - name: Check out repository uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.27.1" + - name: Notify Slack and Teams env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -24,4 +29,4 @@ jobs: RELEASE_ASSETS_JSON: "" RELEASE_BOT_SLACK_WEBHOOK: ${{ secrets.RELEASE_BOT_SLACK_WEBHOOK }} RELEASE_BOT_TEAMS_WEBHOOK: ${{ secrets.RELEASE_BOT_TEAMS_WEBHOOK }} - run: python3 .github/scripts/notify_release.py + run: go run ./cmd/release-notifier diff --git a/cmd/release-notifier/main.go b/cmd/release-notifier/main.go new file mode 100644 index 00000000..c2efb9f0 --- /dev/null +++ b/cmd/release-notifier/main.go @@ -0,0 +1,328 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "regexp" + "strings" +) + +const ( + githubAPI = "https://api.github.com" + maxBlockLength = 2900 +) + +var ( + pullRequestLine = regexp.MustCompile(`(?m)^\*\s+(.+?)\s+by\s+@[\w-]+\s+in\s+(https://github\.com/\S+/pull/\d+)\s*$`) + changelogLine = regexp.MustCompile(`\*\*Full Changelog\*\*:\s*(\S+)`) +) + +type release struct { + Assets []releaseAsset `json:"assets"` + Body string `json:"body"` +} + +type releaseAsset struct { + Name string `json:"name"` + BrowserDownloadURL string `json:"browser_download_url"` + Size int64 `json:"size"` +} + +type pullRequest struct { + Title string + URL string +} + +type asset struct { + Name string + URL string + Size string +} + +type slackText struct { + Type string `json:"type"` + Text string `json:"text"` +} + +type slackBlock struct { + Type string `json:"type"` + Text slackText `json:"text"` +} + +type slackPayload struct { + Text string `json:"text"` + Blocks []slackBlock `json:"blocks"` +} + +type teamsPayload struct { + Type string `json:"type"` + Attachments []teamsAttachment `json:"attachments"` +} + +type teamsAttachment struct { + ContentType string `json:"contentType"` + ContentURL *string `json:"contentUrl"` + Content teamsCard `json:"content"` +} + +type teamsCard struct { + Schema string `json:"$schema"` + Type string `json:"type"` + Version string `json:"version"` + Body []interface{} `json:"body"` +} + +func main() { + repo := requiredEnv("GITHUB_REPOSITORY") + token := requiredEnv("GH_TOKEN") + tag := requiredEnv("RELEASE_TAG") + releaseURL := requiredEnv("RELEASE_URL") + slackWebhook := strings.TrimSpace(os.Getenv("RELEASE_BOT_SLACK_WEBHOOK")) + teamsWebhook := strings.TrimSpace(os.Getenv("RELEASE_BOT_TEAMS_WEBHOOK")) + + if slackWebhook == "" && teamsWebhook == "" { + fail("configure the RELEASE_BOT_SLACK_WEBHOOK or RELEASE_BOT_TEAMS_WEBHOOK repository secret") + } + + release := getRelease(repo, token, tag) + prs := parsePullRequests(release.Body) + assets := parseAssets(release.Assets) + changelogURL := parseChangelogURL(release.Body) + fmt.Printf("Found %d pull request(s) and %d asset(s) for release %s\n", len(prs), len(assets), tag) + + if slackWebhook != "" { + postWebhook(slackWebhook, slackMessage(tag, releaseURL, prs, changelogURL, assets)) + fmt.Println("Posted to Slack") + } + if teamsWebhook != "" { + postWebhook(teamsWebhook, teamsMessage(tag, releaseURL, prs, changelogURL, assets)) + fmt.Println("Posted to Teams") + } +} + +func requiredEnv(name string) string { + value := strings.TrimSpace(os.Getenv(name)) + if value == "" { + fail(name + " is required") + } + return value +} + +func getRelease(repo, token, tag string) release { + var release release + githubRequest(http.MethodGet, fmt.Sprintf("%s/repos/%s/releases/tags/%s", githubAPI, repo, tag), token, nil, &release) + return release +} + +func githubRequest(method, url, token string, body interface{}, target interface{}) { + var requestBody io.Reader + if body != nil { + encoded, err := json.Marshal(body) + if err != nil { + fail(fmt.Sprintf("encode GitHub request: %v", err)) + } + requestBody = bytes.NewReader(encoded) + } + + request, err := http.NewRequest(method, url, requestBody) + if err != nil { + fail(fmt.Sprintf("create GitHub request: %v", err)) + } + request.Header.Set("Accept", "application/vnd.github+json") + request.Header.Set("Authorization", "Bearer "+token) + request.Header.Set("X-GitHub-Api-Version", "2022-11-28") + request.Header.Set("Content-Type", "application/json") + + response := doRequest(request) + defer response.Body.Close() + if err := json.NewDecoder(response.Body).Decode(target); err != nil { + fail(fmt.Sprintf("decode GitHub response: %v", err)) + } +} + +func parsePullRequests(body string) []pullRequest { + matches := pullRequestLine.FindAllStringSubmatch(body, -1) + prs := make([]pullRequest, 0, len(matches)) + for _, match := range matches { + prs = append(prs, pullRequest{Title: match[1], URL: match[2]}) + } + return prs +} + +func parseChangelogURL(body string) string { + match := changelogLine.FindStringSubmatch(body) + if len(match) == 0 { + return "" + } + return match[1] +} + +func parseAssets(releaseAssets []releaseAsset) []asset { + assets := make([]asset, 0, len(releaseAssets)) + for _, releaseAsset := range releaseAssets { + assets = append(assets, asset{ + Name: releaseAsset.Name, + URL: releaseAsset.BrowserDownloadURL, + Size: humanSize(releaseAsset.Size), + }) + } + return assets +} + +func humanSize(size int64) string { + if size < 1024 { + return fmt.Sprintf("%d Bytes", size) + } + units := []string{"KB", "MB", "GB"} + value := float64(size) + for _, unit := range units { + value /= 1024 + if value < 1024 || unit == "GB" { + return fmt.Sprintf("%.2f %s", value, unit) + } + } + return fmt.Sprintf("%.2f GB", value) +} + +func slackMessage(tag, releaseURL string, prs []pullRequest, changelogURL string, assets []asset) slackPayload { + changed := "_No pull requests in this release_\n" + if len(prs) > 0 { + var lines strings.Builder + for _, pr := range prs { + fmt.Fprintf(&lines, "• <%s|%s>\n", pr.URL, pr.Title) + } + changed = lines.String() + } + if changelogURL != "" { + changed += fmt.Sprintf("\n<%s|Full Changelog>\n", changelogURL) + } + + blocks := []slackBlock{ + section(fmt.Sprintf(":sparkles: *New Debricked CLI release: %s* :sparkles:\n<%s|View release on GitHub>\n", tag, releaseURL)), + section("*What's Changed*\n" + changed), + } + blocks = append(blocks, slackAssetBlocks(assets)...) + return slackPayload{Text: fmt.Sprintf("New Debricked CLI release: %s!", tag), Blocks: blocks} +} + +func section(text string) slackBlock { + return slackBlock{Type: "section", Text: slackText{Type: "mrkdwn", Text: text}} +} + +func slackAssetBlocks(assets []asset) []slackBlock { + if len(assets) == 0 { + return []slackBlock{section("*Assets (0)*\n_No assets attached to this release_\n")} + } + + heading := fmt.Sprintf("*Assets (%d)*\n", len(assets)) + current := heading + blocks := []slackBlock{} + for _, asset := range assets { + line := fmt.Sprintf("• <%s|%s> (%s)\n", asset.URL, asset.Name, asset.Size) + if len(current)+len(line) > maxBlockLength && current != heading { + blocks = append(blocks, section(current)) + current = "" + } + current += line + } + return append(blocks, section(current)) +} + +func teamsMessage(tag, releaseURL string, prs []pullRequest, changelogURL string, assets []asset) teamsPayload { + changed := "_No pull requests in this release_" + if len(prs) > 0 { + lines := make([]string, 0, len(prs)) + for _, pr := range prs { + lines = append(lines, fmt.Sprintf("- [%s](%s)", pr.Title, pr.URL)) + } + changed = strings.Join(lines, "\n") + } + if changelogURL != "" { + changed += fmt.Sprintf("\n\n[Full Changelog](%s)", changelogURL) + } + + assetText := "_No assets attached to this release_" + if len(assets) > 0 { + lines := make([]string, 0, len(assets)) + for _, asset := range assets { + lines = append(lines, fmt.Sprintf("- [%s](%s) (%s)", asset.Name, asset.URL, asset.Size)) + } + assetText = strings.Join(lines, "\n") + } + + prWord := "pull requests" + if len(prs) == 1 { + prWord = "pull request" + } + body := []interface{}{ + textBlock(fmt.Sprintf("✨ New Debricked CLI Release: %s ✨", tag), "Bolder", "Large", false), + map[string]interface{}{"type": "TextBlock", "text": fmt.Sprintf("This release includes %d %s. [View release on GitHub](%s)", len(prs), prWord, releaseURL), "wrap": true, "size": "Small", "isSubtle": true}, + textBlock("What's Changed", "Bolder", "Medium", true), + textBlock(changed, "", "", false), + textBlock(fmt.Sprintf("Assets (%d)", len(assets)), "Bolder", "Medium", true), + textBlock(assetText, "", "", false), + } + + return teamsPayload{ + Type: "message", + Attachments: []teamsAttachment{{ + ContentType: "application/vnd.microsoft.card.adaptive", + Content: teamsCard{ + Schema: "http://adaptivecards.io/schemas/adaptive-card.json", + Type: "AdaptiveCard", + Version: "1.4", + Body: body, + }, + }}, + } +} + +func textBlock(text, weight, size string, separator bool) map[string]interface{} { + block := map[string]interface{}{"type": "TextBlock", "text": text, "wrap": true} + if weight != "" { + block["weight"] = weight + } + if size != "" { + block["size"] = size + } + if separator { + block["separator"] = true + } + return block +} + +func postWebhook(url string, payload interface{}) { + body, err := json.Marshal(payload) + if err != nil { + fail(fmt.Sprintf("encode webhook payload: %v", err)) + } + request, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) + if err != nil { + fail(fmt.Sprintf("create webhook request: %v", err)) + } + request.Header.Set("Content-Type", "application/json") + response := doRequest(request) + response.Body.Close() +} + +func doRequest(request *http.Request) *http.Response { + response, err := http.DefaultClient.Do(request) + if err != nil { + fail(fmt.Sprintf("%s %s failed: %v", request.Method, request.URL, err)) + } + if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices { + defer response.Body.Close() + body, _ := io.ReadAll(response.Body) + fail(fmt.Sprintf("%s %s returned HTTP %d: %s", request.Method, request.URL, response.StatusCode, string(body))) + } + return response +} + +func fail(message string) { + fmt.Fprintln(os.Stderr, "::error::"+message) + os.Exit(1) +} diff --git a/cmd/release-notifier/main_test.go b/cmd/release-notifier/main_test.go new file mode 100644 index 00000000..5c6f1b8e --- /dev/null +++ b/cmd/release-notifier/main_test.go @@ -0,0 +1,54 @@ +package main + +import ( + "encoding/json" + "strings" + "testing" +) + +func TestParseReleaseContent(t *testing.T) { + body := "## What's Changed\n* SPM support by @octocat in https://github.com/debricked/cli/pull/336\n\n**Full Changelog**: https://github.com/debricked/cli/compare/v26.3.4...v26.3.5\n" + + prs := parsePullRequests(body) + if len(prs) != 1 { + t.Fatalf("expected one pull request, got %d", len(prs)) + } + if prs[0].Title != "SPM support" || prs[0].URL != "https://github.com/debricked/cli/pull/336" { + t.Fatalf("unexpected pull request: %#v", prs[0]) + } + if got := parseChangelogURL(body); got != "https://github.com/debricked/cli/compare/v26.3.4...v26.3.5" { + t.Fatalf("unexpected changelog URL: %s", got) + } +} + +func TestAssetPayloadsIncludeCountAndSizes(t *testing.T) { + assets := parseAssets([]releaseAsset{ + {Name: "checksums.txt", BrowserDownloadURL: "https://example.test/checksums.txt", Size: 612}, + {Name: "cli_linux_amd64.apk", BrowserDownloadURL: "https://example.test/cli_linux_amd64.apk", Size: 31000000}, + }) + if assets[0].Size != "612 Bytes" || assets[1].Size != "29.56 MB" { + t.Fatalf("unexpected asset sizes: %#v", assets) + } + + slack := slackMessage("v26.3.5", "https://example.test/release", nil, "", assets) + if !strings.Contains(slack.Blocks[2].Text.Text, "*Assets (2)*") { + t.Fatalf("Slack payload omits asset count: %s", slack.Blocks[2].Text.Text) + } + if _, err := json.Marshal(slack); err != nil { + t.Fatalf("Slack payload does not serialize: %v", err) + } + + teams := teamsMessage("v26.3.5", "https://example.test/release", nil, "", assets) + body := teams.Attachments[0].Content.Body + assetsHeading := body[4].(map[string]interface{})["text"] + if assetsHeading != "Assets (2)" { + t.Fatalf("Teams payload has unexpected asset heading: %v", assetsHeading) + } + encoded, err := json.Marshal(teams) + if err != nil { + t.Fatalf("Teams payload does not serialize: %v", err) + } + if strings.Contains(string(encoded), "<at>") || strings.Contains(string(encoded), "msteams") { + t.Fatalf("Teams payload contains an unsupported placeholder mention: %s", encoded) + } +} From d50f5ce239cc5b7ccf9b1410c4ccefa3dadccdd1 Mon Sep 17 00:00:00 2001 From: Rahul Chandrasekaran <rc2658535@gmail.com> Date: Tue, 22 Sep 2026 14:29:38 +0530 Subject: [PATCH 5/7] fix: improve error handling and resource management in GitHub API requests --- cmd/release-notifier/main.go | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/cmd/release-notifier/main.go b/cmd/release-notifier/main.go index c2efb9f0..9a56a1a5 100644 --- a/cmd/release-notifier/main.go +++ b/cmd/release-notifier/main.go @@ -109,12 +109,14 @@ func requiredEnv(name string) string { if value == "" { fail(name + " is required") } + return value } func getRelease(repo, token, tag string) release { var release release githubRequest(http.MethodGet, fmt.Sprintf("%s/repos/%s/releases/tags/%s", githubAPI, repo, tag), token, nil, &release) + return release } @@ -128,17 +130,23 @@ func githubRequest(method, url, token string, body interface{}, target interface requestBody = bytes.NewReader(encoded) } - request, err := http.NewRequest(method, url, requestBody) + request, err := http.NewRequest(method, url, requestBody) //nolint:gosec // GitHub API URL is built from the configured repository. if err != nil { fail(fmt.Sprintf("create GitHub request: %v", err)) } + request.Header.Set("Accept", "application/vnd.github+json") request.Header.Set("Authorization", "Bearer "+token) request.Header.Set("X-GitHub-Api-Version", "2022-11-28") request.Header.Set("Content-Type", "application/json") response := doRequest(request) - defer response.Body.Close() + defer func() { + if err := response.Body.Close(); err != nil { + fail(fmt.Sprintf("close GitHub response: %v", err)) + } + }() + if err := json.NewDecoder(response.Body).Decode(target); err != nil { fail(fmt.Sprintf("decode GitHub response: %v", err)) } @@ -150,6 +158,7 @@ func parsePullRequests(body string) []pullRequest { for _, match := range matches { prs = append(prs, pullRequest{Title: match[1], URL: match[2]}) } + return prs } @@ -300,25 +309,32 @@ func postWebhook(url string, payload interface{}) { if err != nil { fail(fmt.Sprintf("encode webhook payload: %v", err)) } - request, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) + request, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) //nolint:gosec // Webhook URL is supplied through a repository secret. if err != nil { fail(fmt.Sprintf("create webhook request: %v", err)) } request.Header.Set("Content-Type", "application/json") response := doRequest(request) - response.Body.Close() + if err := response.Body.Close(); err != nil { + fail(fmt.Sprintf("close webhook response: %v", err)) + } } func doRequest(request *http.Request) *http.Response { - response, err := http.DefaultClient.Do(request) + response, err := http.DefaultClient.Do(request) //nolint:gosec // URLs are the configured GitHub API or webhook endpoints. if err != nil { fail(fmt.Sprintf("%s %s failed: %v", request.Method, request.URL, err)) } + if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices { - defer response.Body.Close() body, _ := io.ReadAll(response.Body) + if err := response.Body.Close(); err != nil { + fail(fmt.Sprintf("close error response: %v", err)) + } + fail(fmt.Sprintf("%s %s returned HTTP %d: %s", request.Method, request.URL, response.StatusCode, string(body))) } + return response } From bc17e9b9eb680f254df15541a6df1aba37cdc183 Mon Sep 17 00:00:00 2001 From: Rahul Chandrasekaran <rc2658535@gmail.com> Date: Tue, 22 Sep 2026 14:44:07 +0530 Subject: [PATCH 6/7] fix: remove unnecessary blank lines in parsing functions --- cmd/release-notifier/main.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/release-notifier/main.go b/cmd/release-notifier/main.go index 9a56a1a5..6ba44261 100644 --- a/cmd/release-notifier/main.go +++ b/cmd/release-notifier/main.go @@ -167,6 +167,7 @@ func parseChangelogURL(body string) string { if len(match) == 0 { return "" } + return match[1] } @@ -179,6 +180,7 @@ func parseAssets(releaseAssets []releaseAsset) []asset { Size: humanSize(releaseAsset.Size), }) } + return assets } @@ -194,6 +196,7 @@ func humanSize(size int64) string { return fmt.Sprintf("%.2f %s", value, unit) } } + return fmt.Sprintf("%.2f GB", value) } From e5bc5e462faaa48c5f4ccd3644673ecfb75fad27 Mon Sep 17 00:00:00 2001 From: Rahul Chandrasekaran <rc2658535@gmail.com> Date: Tue, 22 Sep 2026 14:49:03 +0530 Subject: [PATCH 7/7] fix: remove unnecessary blank lines in slack message functions --- cmd/release-notifier/main.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/release-notifier/main.go b/cmd/release-notifier/main.go index 6ba44261..b40b8183 100644 --- a/cmd/release-notifier/main.go +++ b/cmd/release-notifier/main.go @@ -218,6 +218,7 @@ func slackMessage(tag, releaseURL string, prs []pullRequest, changelogURL string section("*What's Changed*\n" + changed), } blocks = append(blocks, slackAssetBlocks(assets)...) + return slackPayload{Text: fmt.Sprintf("New Debricked CLI release: %s!", tag), Blocks: blocks} } @@ -241,6 +242,7 @@ func slackAssetBlocks(assets []asset) []slackBlock { } current += line } + return append(blocks, section(current)) } @@ -304,6 +306,7 @@ func textBlock(text, weight, size string, separator bool) map[string]interface{} if separator { block["separator"] = true } + return block }