Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/actions/prepare-telegram-release-message/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Prepare Telegram release message
description: Prepare a Telegram message for a published release.
outputs:
message:
description: Prepared message.
value: ${{ steps.prepare.outputs.message }}
runs:
using: composite
steps:
- id: prepare
shell: bash
env:
REPOSITORY: ${{ github.repository }}
TAG_NAME: ${{ github.event.release.tag_name }}
RELEASE_NAME: ${{ github.event.release.name }}
RELEASE_URL: ${{ github.event.release.html_url }}
ACTOR: ${{ github.event.release.author.login }}
run: python3 "${{ github.action_path }}/prepare.py"
50 changes: 50 additions & 0 deletions .github/actions/prepare-telegram-release-message/prepare.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env python3
import os
import uuid

MARKDOWN_V2_SPECIAL_CHARACTERS = frozenset("_*[]()~`>#+-=|{}.!\\")


def escape_markdown(value):
return "".join(f"\\{character}" if character in MARKDOWN_V2_SPECIAL_CHARACTERS else character for character in str(value))


def escape_link_url(value):
return str(value).replace("\\", "\\\\").replace(")", "\\)")


def format_published(repository, release):
tag = release["tag"]
label = escape_markdown(f"release {tag}")
link = f"[{label}]({escape_link_url(release['url'])})"
parts = [f"*{escape_markdown(repository)}*", f"{link} published"]

name = release.get("name")
if name and name != tag:
parts.append(f"*{escape_markdown(name)}*")

parts.append(escape_markdown(release.get("actor") or "ghost"))
return " · ".join(parts)


def write_output(message):
delimiter = f"ghdelim_{uuid.uuid4().hex}"
with open(os.environ["GITHUB_OUTPUT"], "a") as output:
print(f"message<<{delimiter}", file=output)
print(message, file=output)
print(delimiter, file=output)


def main():
release = {
"tag": os.environ["TAG_NAME"],
"name": os.environ.get("RELEASE_NAME") or "",
"url": os.environ["RELEASE_URL"],
"actor": os.environ.get("ACTOR") or "ghost",
}
write_output(format_published(os.environ["REPOSITORY"], release))
return 0


if __name__ == "__main__":
raise SystemExit(main())
25 changes: 25 additions & 0 deletions .github/workflows/notify-telegram-release-shared.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Notify Telegram release (shared)
on:
workflow_call:
secrets:
TELEGRAM_BOT_TOKEN:
required: true
TELEGRAM_CHAT_ID:
required: true
jobs:
notify:
if: >-
github.event_name == 'release' &&
github.event.action == 'published'
runs-on: ubuntu-slim
permissions: {}
steps:
- id: prepare
uses: $/.github/actions/prepare-telegram-release-message
- name: Send
uses: $/.github/actions/send-telegram-message
with:
message: ${{ steps.prepare.outputs.message }}
telegram-bot-token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
telegram-chat-id: ${{ secrets.TELEGRAM_CHAT_ID }}
parse-mode: MarkdownV2
10 changes: 10 additions & 0 deletions .github/workflows/notify-telegram-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
name: Notify Telegram release
on:
release:
types: [published]
jobs:
notify:
uses: ./.github/workflows/notify-telegram-release-shared.yml
secrets:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ installed in the developer environment.
- `.github/workflows/lint.yml`, `github.yml`, and `embedder.yml` — local self-check callers
- `.github/workflows/pr.yml` — validates Baseline pull request titles against Conventional Commits
- `.github/workflows/release.yml` — maintains the release PR and publishes merged releases
- `.github/workflows/notify-telegram-pr.yml` — Baseline's own Telegram pull request notification caller
- `.github/workflows/notify-telegram-pr.yml` and `notify-telegram-release.yml` — Baseline's own Telegram notification callers
- `.github/workflows/test.yml` — runs the Python test suite
- `.pre-commit-hooks.yaml` — hook definitions for pre-commit

Expand Down Expand Up @@ -124,6 +124,9 @@ The caller owns any label or other notification condition; the shared workflow
does not modify issues. Baseline does not call it because this repository does
not use issue notifications.

`notify-telegram-release-shared.yml` exports release publication notifications.
Baseline calls it locally through `notify-telegram-release.yml`.

Pre-commit hook pins in `.pre-commit-config.yaml` are updated by Dependabot
(`package-ecosystem: pre-commit` in `.github/dependabot.yml`), not by a custom
workflow.
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,31 @@ workflow code from the default branch. The shared workflow does not check out
or execute pull request code. Pass the two secrets explicitly rather than using
`secrets: inherit`.

## Notify Telegram about releases

Create `.github/workflows/notify-telegram-release.yml`:

<!-- x-release-please-start-version -->

```yaml
name: Notify Telegram release
on:
release:
types: [published]
jobs:
notify:
uses: rubykatzen/baseline/.github/workflows/notify-telegram-release-shared.yml@v0.15.0
secrets:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
```

<!-- x-release-please-end -->

Baseline sends one notification when a release or prerelease is published. The
release tag links to the GitHub Release. Pass the two secrets explicitly rather
than using `secrets: inherit`.

## Notify Telegram about closed issues

Issue notifications are optional and can use a different Telegram channel from
Expand Down
80 changes: 72 additions & 8 deletions test/test_telegram_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def load_action_module(name):

PR_TELEGRAM = load_action_module("prepare-telegram-pr-message")
ISSUE_TELEGRAM = load_action_module("prepare-telegram-issue-message")
RELEASE_TELEGRAM = load_action_module("prepare-telegram-release-message")


class TelegramPullRequestMessageTest(unittest.TestCase):
Expand Down Expand Up @@ -189,6 +190,37 @@ def test_closed_issue_includes_leading_heading_and_first_paragraph(self):
self.assertNotIn("Ignored", message)


class TelegramReleaseMessageTest(unittest.TestCase):
def test_formats_release_without_repeating_tag_as_name(self):
release = {
"tag": "v1.2.3",
"name": "v1.2.3",
"url": "https://github.com/owner/repo/releases/tag/v1.2.3",
"actor": "octocat",
}

message = RELEASE_TELEGRAM.format_published("owner/repo", release)

self.assertEqual(
message,
"*owner/repo* · [release v1\\.2\\.3](https://github.com/owner/repo/releases/tag/v1.2.3) "
"published · octocat",
)

def test_formats_distinct_release_name_and_escapes_markdown(self):
release = {
"tag": "v1.2.3",
"name": "Summer [release]",
"url": "https://github.com/owner/repo/releases/tag/v1.2.3",
"actor": "dependabot[bot]",
}

message = RELEASE_TELEGRAM.format_published("owner/repo", release)

self.assertIn(r"*Summer \[release\]*", message)
self.assertTrue(message.endswith(r"dependabot\[bot\]"))


class TelegramWorkflowTest(unittest.TestCase):
def load_workflow(self, name):
with open(BASELINE_ROOT / ".github" / "workflows" / name) as workflow:
Expand Down Expand Up @@ -228,30 +260,62 @@ def test_issue_workflow_only_sends_closed_issue_notification(self):
)
self.assertEqual(job["steps"][-1]["with"]["parse-mode"], "MarkdownV2")

def test_baseline_caller_passes_only_explicit_telegram_secrets(self):
path = BASELINE_ROOT / ".github" / "workflows" / "notify-telegram-pr.yml"
content = path.read_text()
workflow = yaml.safe_load(content)
secrets = workflow["jobs"]["notify"]["secrets"]
def test_release_workflow_only_sends_published_release_notification(self):
workflow = self.load_workflow("notify-telegram-release-shared.yml")
job = workflow["jobs"]["notify"]

self.assertIn("github.event_name == 'release'", job["if"])
self.assertIn("github.event.action == 'published'", job["if"])
self.assertEqual(job["permissions"], {})
self.assertEqual(
[step["uses"] for step in job["steps"]],
[
"$/.github/actions/prepare-telegram-release-message",
"$/.github/actions/send-telegram-message",
],
)
self.assertEqual(job["steps"][-1]["with"]["parse-mode"], "MarkdownV2")

def test_baseline_callers_pass_only_explicit_telegram_secrets(self):
for name in ("notify-telegram-pr.yml", "notify-telegram-release.yml"):
path = BASELINE_ROOT / ".github" / "workflows" / name
content = path.read_text()
workflow = yaml.safe_load(content)
secrets = workflow["jobs"]["notify"]["secrets"]

with self.subTest(name=name):
self.assertEqual(set(secrets), {"TELEGRAM_BOT_TOKEN", "TELEGRAM_CHAT_ID"})
self.assertNotIn("secrets: inherit", content)

self.assertEqual(set(secrets), {"TELEGRAM_BOT_TOKEN", "TELEGRAM_CHAT_ID"})
self.assertNotIn("secrets: inherit", content)
def test_baseline_release_caller_uses_published_event(self):
content = (BASELINE_ROOT / ".github" / "workflows" / "notify-telegram-release.yml").read_text()

self.assertIn("release:\n types: [published]", content)
self.assertIn("uses: ./.github/workflows/notify-telegram-release-shared.yml", content)

def test_internal_actions_separate_formatting_and_delivery(self):
action_root = BASELINE_ROOT / ".github" / "actions"
telegram_actions = sorted(path.parent.name for path in action_root.glob("*telegram*/action.yml"))
pr_action = (action_root / "prepare-telegram-pr-message" / "action.yml").read_text()
issue_action = (action_root / "prepare-telegram-issue-message" / "action.yml").read_text()
release_action = (action_root / "prepare-telegram-release-message" / "action.yml").read_text()
send_action = (action_root / "send-telegram-message" / "action.yml").read_text()

self.assertEqual(
telegram_actions,
["prepare-telegram-issue-message", "prepare-telegram-pr-message", "send-telegram-message"],
[
"prepare-telegram-issue-message",
"prepare-telegram-pr-message",
"prepare-telegram-release-message",
"send-telegram-message",
],
)
self.assertIn('run: python3 "${{ github.action_path }}/prepare.py"', pr_action)
self.assertIn('run: python3 "${{ github.action_path }}/prepare.py"', issue_action)
self.assertIn('run: python3 "${{ github.action_path }}/prepare.py"', release_action)
self.assertNotIn("api.telegram.org", pr_action)
self.assertNotIn("api.telegram.org", issue_action)
self.assertNotIn("api.telegram.org", release_action)
self.assertIn("inputs:\n message:", send_action)
self.assertIn("parse-mode:", send_action)
self.assertIn("parse_mode", send_action)
Expand Down