mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-05-07 05:02:50 +08:00
chore: bump v1.8.0, add release-notify skill, optimize docs deploy (#992)
- Upgrade version to 1.8.0 in all package.json files - Add cf-temp-mail-release-notify skill with MarkdownV2 Telegram posting - Optimize docs_deploy.yml to auto-trigger on Tag Build CI completion - Add v1.8.0 placeholder in CHANGELOG.md and CHANGELOG_EN.md Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
3
.claude/skills/cf-temp-mail-release-notify/.gitignore
vendored
Normal file
3
.claude/skills/cf-temp-mail-release-notify/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
config.json
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
30
.claude/skills/cf-temp-mail-release-notify/SKILL.md
Normal file
30
.claude/skills/cf-temp-mail-release-notify/SKILL.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: cf-temp-mail-release-notify
|
||||
description: Announce a cloudflare_temp_email GitHub release to the project's Telegram channel topic. Use when the user asks to notify/announce/broadcast a release to Telegram, push release notes to the channel, or send a release to the topic after running cf-temp-mail-release. Posts bilingual (中文 + English) changelog excerpts plus the release URL.
|
||||
---
|
||||
|
||||
# Release Notify Workflow
|
||||
|
||||
Post an existing GitHub release's notes to the project's Telegram channel topic.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `config.json` exists in this skill directory with `token`, `chat_id`, `message_thread_id` (gitignored, never commit).
|
||||
- `gh` CLI authenticated.
|
||||
- `uv` installed (`brew install uv` / `curl -LsSf https://astral.sh/uv/install.sh | sh`). Script uses PEP 723 inline metadata; `uv` auto-installs deps.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Resolve tag**: If the user didn't give one, use the latest release: `gh release list --limit 1 --json tagName --jq '.[0].tagName'`.
|
||||
2. **Run the script**:
|
||||
```bash
|
||||
uv run scripts/send_release_to_telegram.py vX.Y.Z
|
||||
```
|
||||
The script fetches the release via `gh`, splits the body into zh/en sections, strips PR collapsibles and the cache-clearing link, truncates to fit Telegram's 4096-char limit, and posts to the configured `chat_id` + `message_thread_id`.
|
||||
3. **Verify**: The script prints `ok: message_id=<id>` on success. Report the message id.
|
||||
|
||||
## Notes
|
||||
|
||||
- Message uses `parse_mode: MarkdownV2`; all content is escaped (via `md_escape`) to avoid parse errors on reserved chars `_ * [ ] ( ) ~ \` > # + - = | { } . !`.
|
||||
- Only the zh/en changelog sections are posted. PRs list and the cache-clearing discussion link are stripped to keep the message concise.
|
||||
- For very long release bodies, zh and en are each truncated to ~half of the 3500-char body budget.
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"token": "<telegram bot token>",
|
||||
"chat_id": "@cloudflare_temp_email",
|
||||
"message_thread_id": 82
|
||||
}
|
||||
221
.claude/skills/cf-temp-mail-release-notify/scripts/send_release_to_telegram.py
Executable file
221
.claude/skills/cf-temp-mail-release-notify/scripts/send_release_to_telegram.py
Executable file
@@ -0,0 +1,221 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = ["httpx>=0.27"]
|
||||
# ///
|
||||
"""Send a cloudflare_temp_email release announcement to a Telegram channel topic.
|
||||
|
||||
Usage:
|
||||
uv run scripts/send_release_to_telegram.py <tag>
|
||||
|
||||
Reads skill config from ../config.json (relative to this script):
|
||||
{
|
||||
"token": "...",
|
||||
"chat_id": "@channel_or_-100...",
|
||||
"message_thread_id": 82
|
||||
}
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
|
||||
TG_API = "https://api.telegram.org"
|
||||
TG_HARD_LIMIT = 4096
|
||||
BODY_BUDGET = 3500 # leave room for header + footer
|
||||
EN_MARKER_RE = re.compile(r"<details>\s*<summary>English</summary>", re.IGNORECASE)
|
||||
MDV2_ESCAPE_RE = re.compile(r"([_*\[\]()~`>#+\-=|{}.!\\])")
|
||||
MDV2_CODE_ESCAPE_RE = re.compile(r"([`\\])")
|
||||
MD_INLINE_RE = re.compile(r"\*\*(.+?)\*\*|`([^`]+)`")
|
||||
MD_HEADING_RE = re.compile(r"^(#{1,6})\s+(.*)$")
|
||||
|
||||
|
||||
def die(msg: str) -> None:
|
||||
print(f"error: {msg}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def md_escape(text: str) -> str:
|
||||
"""Escape all MarkdownV2 reserved characters."""
|
||||
return MDV2_ESCAPE_RE.sub(r"\\\1", text)
|
||||
|
||||
|
||||
def md_render(text: str) -> str:
|
||||
"""Convert source Markdown (changelog) to Telegram MarkdownV2.
|
||||
|
||||
Handles:
|
||||
- `### Heading` -> bold line
|
||||
- `**bold**` -> `*bold*`
|
||||
- `` `code` `` -> `` `code` `` (only ` and \\ escaped inside)
|
||||
- everything else: literal text with MDV2 specials escaped
|
||||
"""
|
||||
out: list[str] = []
|
||||
for raw in text.splitlines():
|
||||
m = MD_HEADING_RE.match(raw)
|
||||
if m:
|
||||
out.append(f"*{md_escape(m.group(2).strip())}*")
|
||||
continue
|
||||
segments: list[str] = []
|
||||
last = 0
|
||||
for im in MD_INLINE_RE.finditer(raw):
|
||||
segments.append(md_escape(raw[last:im.start()]))
|
||||
if im.group(1) is not None:
|
||||
segments.append(f"*{md_escape(im.group(1))}*")
|
||||
else:
|
||||
segments.append(f"`{MDV2_CODE_ESCAPE_RE.sub(r'\\\\\1', im.group(2))}`")
|
||||
last = im.end()
|
||||
segments.append(md_escape(raw[last:]))
|
||||
out.append("".join(segments))
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def load_config() -> dict:
|
||||
cfg_path = Path(__file__).resolve().parent.parent / "config.json"
|
||||
if not cfg_path.exists():
|
||||
die(f"config missing: {cfg_path}")
|
||||
try:
|
||||
cfg = json.loads(cfg_path.read_text())
|
||||
except json.JSONDecodeError as e:
|
||||
die(f"config.json is not valid JSON: {e}")
|
||||
for k in ("token", "chat_id", "message_thread_id"):
|
||||
if k not in cfg:
|
||||
die(f"config.json missing key: {k}")
|
||||
return cfg
|
||||
|
||||
|
||||
def fetch_release(tag: str) -> dict:
|
||||
out = subprocess.run(
|
||||
["gh", "release", "view", tag, "--json", "tagName,name,body,url"],
|
||||
capture_output=True, text=True, check=False,
|
||||
)
|
||||
if out.returncode != 0:
|
||||
die(f"gh release view failed: {out.stderr.strip()}")
|
||||
return json.loads(out.stdout)
|
||||
|
||||
|
||||
def extract_sections(body: str) -> tuple[str, str]:
|
||||
m = EN_MARKER_RE.search(body)
|
||||
if not m:
|
||||
return body.strip(), ""
|
||||
zh = body[: m.start()]
|
||||
rest = body[m.end():]
|
||||
close = rest.find("</details>")
|
||||
if close < 0:
|
||||
die("malformed release body: missing </details> after English marker")
|
||||
en = rest[:close]
|
||||
return zh.strip(), en.strip()
|
||||
|
||||
|
||||
def strip_noise(text: str) -> str:
|
||||
"""Drop PR collapsibles, cache-clearing link, and Full Changelog line."""
|
||||
lines = text.splitlines()
|
||||
out: list[str] = []
|
||||
depth = 0
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("<details>"):
|
||||
depth += 1
|
||||
continue
|
||||
if stripped.startswith("</details>"):
|
||||
depth = max(0, depth - 1)
|
||||
continue
|
||||
if depth > 0:
|
||||
continue
|
||||
if "discussions/487" in stripped:
|
||||
continue
|
||||
if stripped.startswith("**Full Changelog**"):
|
||||
continue
|
||||
out.append(line)
|
||||
result: list[str] = []
|
||||
blanks = 0
|
||||
for line in out:
|
||||
if not line.strip():
|
||||
blanks += 1
|
||||
if blanks <= 1:
|
||||
result.append(line)
|
||||
else:
|
||||
blanks = 0
|
||||
result.append(line)
|
||||
return "\n".join(result).strip()
|
||||
|
||||
|
||||
def truncate(text: str, limit: int) -> str:
|
||||
if len(text) <= limit:
|
||||
return text
|
||||
return text[: limit - 3].rstrip() + "..."
|
||||
|
||||
|
||||
def _budget(zh: str, en: str, total: int) -> tuple[int, int]:
|
||||
"""Split budget between zh and en based on actual length. Short side keeps full, long side absorbs the rest."""
|
||||
if not en:
|
||||
return total, 0
|
||||
if len(zh) + len(en) <= total:
|
||||
return len(zh), len(en)
|
||||
half = total // 2
|
||||
if len(zh) <= half:
|
||||
return len(zh), total - len(zh)
|
||||
if len(en) <= half:
|
||||
return total - len(en), len(en)
|
||||
return half, total - half
|
||||
|
||||
|
||||
def build_message(tag: str, name: str, url: str, body: str) -> str:
|
||||
zh, en = extract_sections(body)
|
||||
zh = strip_noise(zh)
|
||||
en = strip_noise(en)
|
||||
|
||||
zh_limit, en_limit = _budget(zh, en, BODY_BUDGET)
|
||||
zh = truncate(zh, zh_limit)
|
||||
en = truncate(en, en_limit) if en else ""
|
||||
|
||||
title = md_escape(name or tag)
|
||||
header = f"🚀 *{title} 已发布 / Released*"
|
||||
parts = [header, "", md_render(zh)]
|
||||
if en:
|
||||
parts.extend(["", "__English__", "", md_render(en)])
|
||||
parts.extend(["", f"🔗 {md_escape(url)}"])
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def send(cfg: dict, text: str) -> None:
|
||||
payload = {
|
||||
"chat_id": cfg["chat_id"],
|
||||
"message_thread_id": cfg["message_thread_id"],
|
||||
"text": text,
|
||||
"parse_mode": "MarkdownV2",
|
||||
"disable_web_page_preview": False,
|
||||
}
|
||||
try:
|
||||
resp = httpx.post(
|
||||
f"{TG_API}/bot{cfg['token']}/sendMessage",
|
||||
json=payload,
|
||||
timeout=30,
|
||||
)
|
||||
except httpx.HTTPError as e:
|
||||
die(f"Telegram network error: {e}")
|
||||
try:
|
||||
data = resp.json()
|
||||
except ValueError:
|
||||
die(f"Telegram API returned non-JSON ({resp.status_code}): {resp.text[:200]!r}")
|
||||
if resp.status_code != 200 or not data.get("ok"):
|
||||
die(f"Telegram API rejected ({resp.status_code}): {data}")
|
||||
print(f"ok: message_id={data['result'].get('message_id')}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) != 2:
|
||||
die("usage: send_release_to_telegram.py <tag>")
|
||||
cfg = load_config()
|
||||
rel = fetch_release(sys.argv[1])
|
||||
text = build_message(rel["tagName"], rel.get("name", ""), rel["url"], rel.get("body", ""))
|
||||
send(cfg, text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
52
.github/workflows/docs_deploy.yml
vendored
52
.github/workflows/docs_deploy.yml
vendored
@@ -1,16 +1,19 @@
|
||||
name: Deploy Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "vitepress-docs/**"
|
||||
tags:
|
||||
- "*"
|
||||
workflow_run:
|
||||
workflows: ["Tag Build CI"]
|
||||
types:
|
||||
- completed
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event.workflow_run.conclusion == 'success' &&
|
||||
startsWith(github.event.workflow_run.head_branch, 'v'))
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
@@ -18,6 +21,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.workflow_run.head_sha || github.ref }}
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
@@ -31,34 +35,16 @@ jobs:
|
||||
version: 10
|
||||
run_install: false
|
||||
|
||||
- name: check github release done
|
||||
run: |
|
||||
for ((attempt=1; attempt<=10; attempt++)); do
|
||||
if wget -q --spider "https://github.com/dreamhunter2333/cloudflare_temp_email/releases/latest/download/frontend.zip"; then
|
||||
echo "frontend.zip found."
|
||||
break
|
||||
else
|
||||
if [ $attempt -eq 10 ]; then
|
||||
echo "Exceeded maximum retries. frontend.zip not found."
|
||||
else
|
||||
echo "frontend.zip not found. Retrying in 30 seconds..."
|
||||
sleep 30
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Deploy Docs for ${{github.ref_name}}
|
||||
run: |
|
||||
cd vitepress-docs/
|
||||
wget https://github.com/dreamhunter2333/cloudflare_temp_email/releases/latest/download/frontend.zip -O docs/public/ui_install/frontend.zip
|
||||
pnpm install --no-frozen-lockfile
|
||||
if [[ ${{github.ref}} == refs/tags/* ]]; then
|
||||
export TAG_NAME=${{github.ref_name}}
|
||||
else
|
||||
export TAG_NAME=$(git describe --tags --abbrev=0)
|
||||
fi
|
||||
echo "Deploying docs for tag $TAG_NAME"
|
||||
pnpm run deploy
|
||||
- name: Deploy Docs
|
||||
env:
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
cd vitepress-docs/
|
||||
wget https://github.com/dreamhunter2333/cloudflare_temp_email/releases/latest/download/frontend.zip -O docs/public/ui_install/frontend.zip
|
||||
pnpm install --no-frozen-lockfile
|
||||
TAG_NAME=$(gh release view --json tagName --jq '.tagName')
|
||||
echo "Deploying docs for tag $TAG_NAME"
|
||||
export TAG_NAME
|
||||
pnpm run deploy
|
||||
|
||||
@@ -6,6 +6,14 @@
|
||||
<a href="CHANGELOG_EN.md">English</a>
|
||||
</p>
|
||||
|
||||
## v1.8.0(main)
|
||||
|
||||
### Features
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
### Improvements
|
||||
|
||||
## v1.7.0(main)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
@@ -6,6 +6,14 @@
|
||||
<a href="CHANGELOG_EN.md">English</a>
|
||||
</p>
|
||||
|
||||
## v1.8.0(main)
|
||||
|
||||
### Features
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
### Improvements
|
||||
|
||||
## v1.7.0(main)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cloudflare_temp_email",
|
||||
"version": "1.7.0",
|
||||
"version": "1.8.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
8
frontend/pnpm-lock.yaml
generated
8
frontend/pnpm-lock.yaml
generated
@@ -2654,8 +2654,8 @@ packages:
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
|
||||
safe-array-concat@1.1.3:
|
||||
resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==}
|
||||
safe-array-concat@1.1.4:
|
||||
resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==}
|
||||
engines: {node: '>=0.4'}
|
||||
|
||||
safe-buffer@5.1.2:
|
||||
@@ -5133,7 +5133,7 @@ snapshots:
|
||||
object.assign: 4.1.7
|
||||
own-keys: 1.0.1
|
||||
regexp.prototype.flags: 1.5.4
|
||||
safe-array-concat: 1.1.3
|
||||
safe-array-concat: 1.1.4
|
||||
safe-push-apply: 1.0.0
|
||||
safe-regex-test: 1.1.0
|
||||
set-proto: 1.0.0
|
||||
@@ -5953,7 +5953,7 @@ snapshots:
|
||||
'@rollup/rollup-win32-x64-msvc': 4.60.2
|
||||
fsevents: 2.3.3
|
||||
|
||||
safe-array-concat@1.1.3:
|
||||
safe-array-concat@1.1.4:
|
||||
dependencies:
|
||||
call-bind: 1.0.9
|
||||
call-bound: 1.0.4
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "temp-email-pages",
|
||||
"version": "1.7.0",
|
||||
"version": "1.8.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "temp-mail-docs",
|
||||
"private": true,
|
||||
"version": "1.7.0",
|
||||
"version": "1.8.0",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.6.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cloudflare_temp_email",
|
||||
"version": "1.7.0",
|
||||
"version": "1.8.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const CONSTANTS = {
|
||||
VERSION: 'v' + '1.7.0',
|
||||
VERSION: 'v' + '1.8.0',
|
||||
|
||||
// DB Version
|
||||
DB_VERSION_KEY: 'db_version',
|
||||
|
||||
Reference in New Issue
Block a user