Skip to content

ci: batch Telegram push notifications #270

ci: batch Telegram push notifications

ci: batch Telegram push notifications #270

Workflow file for this run

name: CI
on:
push:
branches: [main, dev]
pull_request:
branches: [main]
# Cancel superseded runs on the same ref (rapid pushes / PR force-pushes).
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
# Least privilege. The notify job authenticates to Telegram via its own secrets,
# not GITHUB_TOKEN, so read-only repo access is enough everywhere.
permissions:
contents: read
jobs:
# Node-version-independent static analysis — run once, not per matrix entry.
checks:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- name: Install dependencies
run: npm ci
- name: Verify runtime contract
run: npm run check:runtime
- name: Build SDK workspace
run: npm run build -w packages/sdk
- name: Type check
run: npm run typecheck
- name: Lint
run: npm run lint
- name: Format check
run: npm run format:check
- name: Dead code check
run: npm run knip
- name: Circular dependency check
run: npm run circular
- name: Duplicate code check
run: npm run dupcheck
- name: Security audit
run: npm run audit:ci
# Build + test on each supported Node version.
test:
runs-on: ubuntu-latest
timeout-minutes: 25
strategy:
matrix:
node-version: ["22.22.2", "24.15.0"]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm
cache-dependency-path: |
package-lock.json
web/package-lock.json
- name: Install dependencies
run: npm ci
- name: Install WebUI dependencies
run: cd web && npm ci
- name: Build SDK workspace
run: npm run build -w packages/sdk
- name: Build
run: npm run build
- name: Test with coverage
run: npm run test:coverage
- name: CLI smoke test
run: node dist/cli/index.js --help
# ---- Telegram notification (one summary per push, pushes only) ----
notify:
needs: [checks, test]
if: always() && github.event_name == 'push'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Notify Telegram
env:
TG_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TG_CHAT: ${{ secrets.TELEGRAM_CHAT_ID }}
BRANCH: ${{ github.ref_name }}
BEFORE_SHA: ${{ github.event.before }}
AFTER_SHA: ${{ github.event.after }}
CHECKS_RESULT: ${{ needs.checks.result }}
TEST_RESULT: ${{ needs.test.result }}
run: |
set -uo pipefail
NULL_SHA="0000000000000000000000000000000000000000"
if [ -z "$TG_TOKEN" ] || [ -z "$TG_CHAT" ]; then
echo "Telegram secrets not configured, skipping"
exit 0
fi
# Send one message, retrying until Telegram accepts it. Do not use
# curl --fail: a 429 body contains the retry_after value we need.
send() {
local text="$1" attempt resp ra
for attempt in 1 2 3 4 5 6; do
resp=$(curl -sS -m 25 -X POST "https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \
-d chat_id="${TG_CHAT}" -d parse_mode=HTML -d disable_web_page_preview=true \
--data-urlencode "text=${text}" 2>/dev/null || true)
case "$resp" in
*'"ok":true'*) return 0 ;;
esac
# 429 -> honor retry_after; other transient error -> small backoff.
ra=$(printf '%s' "$resp" | grep -o '"retry_after":[0-9]*' | grep -o '[0-9]*' || true)
sleep "$(( ${ra:-2} + 1 ))"
done
echo "WARN: gave up sending after retries: ${text%%$'\n'*}"
return 1
}
REPO_URL="${{ github.server_url }}/${{ github.repository }}"
# One summary notification per push (avoid per-commit spam).
if [ -n "$BRANCH" ]; then
if [ "$BEFORE_SHA" = "$NULL_SHA" ] || ! git merge-base --is-ancestor "$BEFORE_SHA" "$AFTER_SHA" 2>/dev/null; then
COUNT=$(git rev-list --count "$AFTER_SHA")
RANGE="$AFTER_SHA"
URL="$REPO_URL/commits/$BRANCH"
else
COUNT=$(git rev-list --count "$BEFORE_SHA..$AFTER_SHA")
RANGE="$BEFORE_SHA..$AFTER_SHA"
URL="$REPO_URL/compare/$BEFORE_SHA...$AFTER_SHA"
fi
NOUN=commits; [ "$COUNT" = "1" ] && NOUN=commit
REPO_NAME=${REPO_URL##*/}
LABEL=$(printf '%s' "$BRANCH" | tr '[:lower:]' '[:upper:]')
TEXT="<b>[$LABEL] $REPO_NAME</b>"
if [ "$CHECKS_RESULT" = "success" ] && [ "$TEST_RESULT" = "success" ]; then
printf -v TEXT '%s\n%s %s pushed to %s' "$TEXT" "$COUNT" "$NOUN" "$BRANCH"
else
printf -v TEXT '%s\nCI failed on %s (%s %s)' "$TEXT" "$BRANCH" "$COUNT" "$NOUN"
fi
# Commit titles (HTML-escaped), oldest first, capped to avoid huge messages.
MAX=50
TITLES=$(git log --reverse --max-count="$MAX" --pretty=format:'%s' "$RANGE" \
| sed -e 's/&/\&amp;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g')
if [ "$COUNT" -gt 1 ]; then
if [ "$COUNT" -gt "$MAX" ]; then
printf -v TITLES '%s\n… and %s more' "$TITLES" "$((COUNT - MAX))"
fi
# Multiple commits: collapse the title list into an expandable quote.
printf -v TEXT '%s\n\n<blockquote expandable>%s</blockquote>' "$TEXT" "$TITLES"
elif [ -n "$TITLES" ]; then
printf -v TEXT '%s\n\n<blockquote>%s</blockquote>' "$TEXT" "$TITLES"
fi
printf -v TEXT '%s\n\n<a href="%s">View commits</a>' "$TEXT" "$URL"
send "$TEXT"
fi