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
17 changes: 17 additions & 0 deletions .github/actions/prepare-telegram-issue-message/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Prepare Telegram issue message
description: Prepare a Telegram message for a closed issue.
outputs:
message:
description: Prepared message.
value: ${{ steps.prepare.outputs.message }}
runs:
using: composite
steps:
- id: prepare
shell: bash
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
NUMBER: ${{ github.event.issue.number }}
ACTOR: ${{ github.actor }}
run: python3 "${{ github.action_path }}/prepare.py"
59 changes: 59 additions & 0 deletions .github/actions/prepare-telegram-issue-message/prepare.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
import json
import os
import subprocess
import uuid

ISSUE_BODY_LIMIT = 500


def gh_json(*arguments):
result = subprocess.run(["gh", *arguments], stdout=subprocess.PIPE, text=True, check=True)
return json.loads(result.stdout)


def truncate(value, limit):
value = value.strip()
if len(value) <= limit:
return value
return value[: limit - 3].rstrip() + "..."


def format_closed(repository, issue, actor):
lines = [
f"🟒 {repository} β€” issue closed",
f"#{issue['number']} {issue['title']}",
]
if body := truncate(issue.get("body") or "", ISSUE_BODY_LIMIT):
lines.append(body)
lines.append(f"{actor} Β· {issue['url']}")
return "\n".join(lines)


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"]
number = os.environ["NUMBER"]
actor = os.environ["ACTOR"]
issue = gh_json(
"issue",
"view",
number,
"--repo",
repository,
"--json",
"number,title,url,body",
)
write_output(format_closed(repository, issue, actor))
return 0


if __name__ == "__main__":
raise SystemExit(main())
19 changes: 19 additions & 0 deletions .github/actions/prepare-telegram-pr-message/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Prepare Telegram PR message
description: Prepare a Telegram message for the current pull request event or digest.
outputs:
message:
description: Prepared message, or an empty string when no message is needed.
value: ${{ steps.prepare.outputs.message }}
runs:
using: composite
steps:
- id: prepare
shell: bash
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
EVENT_NAME: ${{ github.event_name }}
EVENT_ACTION: ${{ github.event.action }}
NUMBER: ${{ github.event.pull_request.number }}
MERGED: ${{ github.event.pull_request.merged }}
run: python3 "${{ github.action_path }}/prepare.py"
136 changes: 136 additions & 0 deletions .github/actions/prepare-telegram-pr-message/prepare.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#!/usr/bin/env python3
import json
import os
import subprocess
import uuid
from datetime import datetime, timezone

DIGEST_PR_LIMIT = 10
TELEGRAM_MESSAGE_LIMIT = 4096


def gh_json(*arguments):
result = subprocess.run(["gh", *arguments], stdout=subprocess.PIPE, text=True, check=True)
return json.loads(result.stdout)


def author_login(pull_request):
return (pull_request.get("author") or {}).get("login") or "ghost"


def format_opened(repository, pull_request, event_action):
if pull_request.get("isDraft"):
return ""

events = {
"ready_for_review": ("πŸ†—", "PR ready for review"),
"reopened": ("πŸ†™", "PR reopened"),
}
emoji, event = events.get(event_action, ("πŸ†•", "PR opened"))
return (
f"{emoji} {repository} β€” {event}\n"
f"#{pull_request['number']} {pull_request['title']}\n"
f"{author_login(pull_request)} Β· {pull_request['url']}"
)


def format_merged(repository, pull_request):
return (
f"πŸ”€ {repository} β€” PR merged\n"
f"#{pull_request['number']} {pull_request['title']}\n"
f"{author_login(pull_request)} Β· {pull_request['url']}"
)


def parse_github_time(value):
return datetime.fromisoformat(value.replace("Z", "+00:00"))


def format_digest(repository, pull_requests, now=None):
pull_requests = [pull_request for pull_request in pull_requests if not pull_request.get("isDraft")]
if not pull_requests:
return ""

now = now or datetime.now(timezone.utc)
header = f"πŸ”  {repository} β€” {len(pull_requests)} open PR(s)"
lines = []
for pull_request in pull_requests[:DIGEST_PR_LIMIT]:
age = max(0, (now - parse_github_time(pull_request["createdAt"])).days)
line = (
f"#{pull_request['number']} {pull_request['title']}\n"
f"{author_login(pull_request)} Β· {age}d Β· {pull_request['url']}"
)
remaining = len(pull_requests) - len(lines) - 1
suffix = f"...and {remaining} more Β· https://github.com/{repository}/pulls" if remaining else ""
candidate = "\n".join((header, *lines, line, suffix)).rstrip()
if len(candidate) > TELEGRAM_MESSAGE_LIMIT:
break
lines.append(line)

remaining = len(pull_requests) - len(lines)
if remaining:
lines.append(f"...and {remaining} more Β· https://github.com/{repository}/pulls")

return "\n".join((header, *lines))


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"]
event_name = os.environ["EVENT_NAME"]
event_action = os.environ.get("EVENT_ACTION") or None
number = os.environ.get("NUMBER") or None
merged = os.environ.get("MERGED") == "true"

if event_name in ("schedule", "workflow_dispatch"):
pull_requests = gh_json(
"pr",
"list",
"--repo",
repository,
"--state",
"open",
"--limit",
"100",
"--json",
"number,title,author,isDraft,url,createdAt",
)
message = format_digest(repository, pull_requests)
elif event_name == "pull_request_target" and event_action in ("opened", "ready_for_review", "reopened"):
pull_request = gh_json(
"pr",
"view",
number,
"--repo",
repository,
"--json",
"number,title,author,url,isDraft",
)
message = format_opened(repository, pull_request, event_action)
elif event_name == "pull_request_target" and event_action == "closed" and merged:
pull_request = gh_json(
"pr",
"view",
number,
"--repo",
repository,
"--json",
"number,title,author,url",
)
message = format_merged(repository, pull_request)
else:
raise ValueError(f"Unsupported pull request notification event: {event_name}/{event_action}")

write_output(message)
return 0


if __name__ == "__main__":
raise SystemExit(main())
31 changes: 31 additions & 0 deletions .github/actions/send-telegram-message/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Send Telegram message
description: Send a plain-text message to a Telegram chat.
inputs:
message:
description: Message text.
required: true
telegram-bot-token:
description: Telegram bot token.
required: true
telegram-chat-id:
description: Telegram chat ID.
required: true
runs:
using: composite
steps:
- shell: bash
env:
MESSAGE: ${{ inputs.message }}
BOT_TOKEN: ${{ inputs.telegram-bot-token }}
CHAT_ID: ${{ inputs.telegram-chat-id }}
run: |
set -euo pipefail
curl -fsSL \
--output /dev/null \
-X POST \
"https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg c "$CHAT_ID" \
--arg t "$MESSAGE" \
'{chat_id: $c, text: $t, link_preview_options: {is_disabled: true}}')"
25 changes: 25 additions & 0 deletions .github/workflows/notify-telegram-issue-shared.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Notify Telegram issue (shared)
on:
workflow_call:
secrets:
TELEGRAM_BOT_TOKEN:
required: true
TELEGRAM_CHAT_ID:
required: true
jobs:
notify-closed:
if: >-
github.event_name == 'issues' &&
github.event.action == 'closed'
runs-on: ubuntu-latest
permissions:
issues: read
steps:
- id: prepare
uses: $/.github/actions/prepare-telegram-issue-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 }}
29 changes: 29 additions & 0 deletions .github/workflows/notify-telegram-pr-shared.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Notify Telegram PR (shared)
on:
workflow_call:
secrets:
TELEGRAM_BOT_TOKEN:
required: true
TELEGRAM_CHAT_ID:
required: true
jobs:
notify:
if: >-
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request_target' &&
(contains(fromJSON('["opened","ready_for_review","reopened"]'), github.event.action) ||
(github.event.action == 'closed' && github.event.pull_request.merged == true)))
runs-on: ubuntu-latest
permissions:
pull-requests: read
steps:
- id: prepare
uses: $/.github/actions/prepare-telegram-pr-message
- name: Send
if: steps.prepare.outputs.message != ''
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 }}
13 changes: 13 additions & 0 deletions .github/workflows/notify-telegram-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: Notify Telegram PR
on:
pull_request_target:
types: [opened, ready_for_review, reopened, closed]
schedule:
- cron: "0 10 * * *"
workflow_dispatch:
jobs:
notify:
uses: ./.github/workflows/notify-telegram-pr-shared.yml
secrets:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
9 changes: 0 additions & 9 deletions .github/workflows/notify-telegram-unreleased.yml

This file was deleted.

17 changes: 14 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,17 @@ tools to already be installed in the developer environment.
- `.github/actions/detect-linters/` β€” composite action that selects applicable linters from tracked files
- `.github/actions/check-precommit/` β€” composite action: verifies pre-commit hooks match detected CI linters
- `.github/actions/setup-runtimes/` β€” installs Python packages, Ruby, and standalone binaries for requested linters; Python is provided by the runner
- `.github/actions/prepare-telegram-pr-message/` β€” formats pull request and open-PR digest messages
- `.github/actions/prepare-telegram-issue-message/` β€” formats closed-issue messages
- `.github/actions/send-telegram-message/` β€” sends plain-text messages through the Telegram Bot API
- `.github/workflows/lint-shared.yml` β€” reusable workflow exported for consuming repos: setup + lint
- `.github/workflows/embedder-shared.yml` β€” reusable workflow exported for required content validation
- `.github/workflows/notify-telegram-pr-shared.yml` β€” reusable pull request notifications and open-PR digest
- `.github/workflows/notify-telegram-issue-shared.yml` β€” optional reusable issue-closure notifications
- `.github/workflows/pr.yml` β€” validates Baseline pull request titles against Conventional Commits
- `.github/workflows/lint.yml` β€” baseline self-lint (uses local `./` references, not `@vX`)
- `.github/workflows/release-please.yml` β€” maintains the release PR and publishes merged releases
- `.github/workflows/notify-telegram-unreleased.yml` β€” baseline's own caller (delegates to `rubykatzen/releaser`)
- `.github/workflows/notify-telegram-pr.yml` β€” Baseline's own Telegram pull request notification caller
- `.pre-commit-hooks.yaml` β€” hook definitions for pre-commit
- `.pre-commit-config.yaml.example` β€” example for consuming repos (all hooks, prune as needed)

Expand All @@ -99,8 +104,14 @@ Pre-commit hooks expect tools to already be on PATH in the developer environment

## Workflows

`notify-telegram-unreleased.yml` is baseline's own caller that delegates to
`rubykatzen/releaser`; it is not exported for external use.
`notify-telegram-pr-shared.yml` exports pull request notifications and the
open-pull-request digest. Baseline calls it locally through
`notify-telegram-pr.yml`.

`notify-telegram-issue-shared.yml` exports optional issue-closure notifications.
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.

`lint-shared.yml` is the primary export β€” consuming repos call it via
`uses: rubykatzen/baseline/.github/workflows/lint-shared.yml@VERSION`.
Expand Down
Loading