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
28 changes: 28 additions & 0 deletions .github/actions/prepare-telegram-linkedin-message/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Prepare Telegram LinkedIn notification
description: Prepare a Telegram message reporting whether a LinkedIn release post succeeded or failed.
inputs:
result:
description: Outcome of the LinkedIn publish job (success or failure).
required: true
post-id:
description: Published LinkedIn post URN. Required when result is success.
default: ""
run-url:
description: Workflow run URL. Required when result is failure.
default: ""
outputs:
message:
description: Prepared Telegram message text.
value: ${{ steps.prepare.outputs.message }}
runs:
using: composite
steps:
- id: prepare
shell: bash
env:
REPOSITORY: ${{ github.repository }}
TAG_NAME: ${{ github.event.release.tag_name }}
RESULT: ${{ inputs.result }}
POST_ID: ${{ inputs.post-id }}
RUN_URL: ${{ inputs.run-url }}
run: python3 "${{ github.action_path }}/prepare.py"
55 changes: 55 additions & 0 deletions .github/actions/prepare-telegram-linkedin-message/prepare.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/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 post_url(post_id):
return f"https://www.linkedin.com/feed/update/{post_id}/"


def format_published(repository, tag, post_id):
link = f"[LinkedIn post]({escape_link_url(post_url(post_id))})"
return f"*{escape_markdown(repository)}* • {link} published for {escape_markdown(tag)}"


def format_failed(repository, tag, run_url):
link = f"[LinkedIn post]({escape_link_url(run_url)})"
return f"*{escape_markdown(repository)}* • {link} failed for {escape_markdown(tag)}"


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():
repository = os.environ["REPOSITORY"]
tag = os.environ["TAG_NAME"]
result = os.environ["RESULT"]

if result == "success":
message = format_published(repository, tag, os.environ["POST_ID"])
elif result == "failure":
message = format_failed(repository, tag, os.environ["RUN_URL"])
else:
raise ValueError(f"result must be success or failure, got {result!r}")

write_output(message)
return 0


if __name__ == "__main__":
raise SystemExit(main())
23 changes: 16 additions & 7 deletions .github/workflows/publish-linkedin-release-shared.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,31 @@ jobs:
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.telegram-bot-token }}
run: echo "present=${TELEGRAM_BOT_TOKEN:+true}" >> "$GITHUB_OUTPUT"
- id: prepare-success
if: needs.publish.result == 'success' && steps.check-token.outputs.present == 'true'
uses: $/.github/actions/prepare-telegram-linkedin-message
with:
result: success
post-id: ${{ needs.publish.outputs.post-id }}
- name: Notify success
if: needs.publish.result == 'success' && steps.check-token.outputs.present == 'true'
uses: $/.github/actions/send-telegram-message
with:
message: >-
${{ github.repository }} — LinkedIn post published for
${{ github.event.release.tag_name }}: ${{ needs.publish.outputs.post-id }}
message: ${{ steps.prepare-success.outputs.message }}
telegram-bot-token: ${{ secrets.telegram-bot-token }}
telegram-chat-id: ${{ inputs.telegram-chat-id }}
parse-mode: MarkdownV2
- id: prepare-failure
if: needs.publish.result == 'failure' && steps.check-token.outputs.present == 'true'
uses: $/.github/actions/prepare-telegram-linkedin-message
with:
result: failure
run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: Notify failure
if: needs.publish.result == 'failure' && steps.check-token.outputs.present == 'true'
uses: $/.github/actions/send-telegram-message
with:
message: >-
${{ github.repository }} — failed to publish LinkedIn post for
${{ github.event.release.tag_name }}:
${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
message: ${{ steps.prepare-failure.outputs.message }}
telegram-bot-token: ${{ secrets.telegram-bot-token }}
telegram-chat-id: ${{ inputs.telegram-chat-id }}
parse-mode: MarkdownV2
53 changes: 52 additions & 1 deletion test/test_linkedin_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def load_action_module(action, module):

PREPARE = load_action_module("prepare-linkedin-release-post", "prepare.py")
SEND = load_action_module("send-linkedin-post", "send.py")
PREPARE_NOTIFY = load_action_module("prepare-telegram-linkedin-message", "prepare.py")


class FakeResponse:
Expand Down Expand Up @@ -238,6 +239,46 @@ def test_http_error_diagnostic_does_not_include_token(self):
self.assertEqual(detail, 'LinkedIn API request failed with HTTP 401: {"message":"Expired token"}')


class LinkedInNotifyMessageTest(unittest.TestCase):
def test_formats_success_message_with_post_link(self):
message = PREPARE_NOTIFY.format_published("owner/repo", "v0.18.0", "urn:li:share:123")

self.assertEqual(
message,
r"*owner/repo* • [LinkedIn post](https://www.linkedin.com/feed/update/urn:li:share:123/) "
r"published for v0\.18\.0",
)

def test_formats_failure_message_with_run_link(self):
message = PREPARE_NOTIFY.format_failed(
"owner/repo", "v0.18.0", "https://github.com/owner/repo/actions/runs/123"
)

self.assertEqual(
message,
r"*owner/repo* • [LinkedIn post](https://github.com/owner/repo/actions/runs/123) "
r"failed for v0\.18\.0",
)

def test_escapes_markdown_special_characters_in_repository_and_tag(self):
message = PREPARE_NOTIFY.format_published("owner/my-repo", "v0.18.0-rc.1", "urn:li:share:123")

self.assertIn(r"owner/my\-repo", message)
self.assertIn(r"v0\.18\.0\-rc\.1", message)

def test_rejects_unknown_result(self):
env = {"REPOSITORY": "owner/repo", "TAG_NAME": "v0.18.0", "RESULT": "cancelled"}
with patch.dict(os.environ, env), self.assertRaisesRegex(ValueError, "success or failure"):
PREPARE_NOTIFY.main()

def test_writes_multiline_github_output(self):
with tempfile.NamedTemporaryFile() as output, patch.dict(os.environ, {"GITHUB_OUTPUT": output.name}):
PREPARE_NOTIFY.write_output("first\nsecond")
value = Path(output.name).read_text()

self.assertRegex(value, r"^message<<ghdelim_[0-9a-f]+\nfirst\nsecond\nghdelim_[0-9a-f]+\n$")


class LinkedInWorkflowTest(unittest.TestCase):
def load_workflow(self, name):
with open(BASELINE_ROOT / ".github" / "workflows" / name) as workflow:
Expand Down Expand Up @@ -281,15 +322,25 @@ def test_shared_workflow_notifies_telegram_when_chat_id_is_set(self):
self.assertIn("needs.publish.result == 'failure'", job["if"])
self.assertEqual(job["permissions"], {})

check_token, notify_success, notify_failure = job["steps"]
check_token, prepare_success, notify_success, prepare_failure, notify_failure = job["steps"]
self.assertEqual(check_token["id"], "check-token")
self.assertIn("secrets.telegram-bot-token", check_token["env"]["TELEGRAM_BOT_TOKEN"])

self.assertEqual(prepare_success["uses"], "$/.github/actions/prepare-telegram-linkedin-message")
self.assertEqual(prepare_success["with"]["result"], "success")
self.assertEqual(prepare_success["with"]["post-id"], "${{ needs.publish.outputs.post-id }}")
self.assertEqual(prepare_failure["uses"], "$/.github/actions/prepare-telegram-linkedin-message")
self.assertEqual(prepare_failure["with"]["result"], "failure")
self.assertIn("actions/runs", prepare_failure["with"]["run-url"])

for step in (notify_success, notify_failure):
self.assertEqual(step["uses"], "$/.github/actions/send-telegram-message")
self.assertIn("steps.check-token.outputs.present == 'true'", step["if"])
self.assertEqual(step["with"]["telegram-bot-token"], "${{ secrets.telegram-bot-token }}")
self.assertEqual(step["with"]["telegram-chat-id"], "${{ inputs.telegram-chat-id }}")
self.assertEqual(step["with"]["parse-mode"], "MarkdownV2")
self.assertEqual(notify_success["with"]["message"], "${{ steps.prepare-success.outputs.message }}")
self.assertEqual(notify_failure["with"]["message"], "${{ steps.prepare-failure.outputs.message }}")
self.assertIn("needs.publish.result == 'success'", notify_success["if"])
self.assertIn("needs.publish.result == 'failure'", notify_failure["if"])

Expand Down
1 change: 1 addition & 0 deletions test/test_telegram_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ def test_internal_actions_separate_formatting_and_delivery(self):
telegram_actions,
[
"prepare-telegram-issue-message",
"prepare-telegram-linkedin-message",
"prepare-telegram-pr-message",
"prepare-telegram-release-message",
"send-telegram-message",
Expand Down