mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-06 07:57:04 +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:
co-authored by
Claude Opus 4.7
parent
a5b64e1dc9
commit
296ddb8619
@@ -0,0 +1,3 @@
|
|||||||
|
config.json
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
@@ -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
@@ -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()
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
name: Deploy Docs
|
name: Deploy Docs
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_run:
|
||||||
paths:
|
workflows: ["Tag Build CI"]
|
||||||
- "vitepress-docs/**"
|
types:
|
||||||
tags:
|
- completed
|
||||||
- "*"
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
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:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
@@ -18,6 +21,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
ref: ${{ github.event.workflow_run.head_sha || github.ref }}
|
||||||
|
|
||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
@@ -31,34 +35,16 @@ jobs:
|
|||||||
version: 10
|
version: 10
|
||||||
run_install: false
|
run_install: false
|
||||||
|
|
||||||
- name: check github release done
|
- name: Deploy Docs
|
||||||
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
|
|
||||||
env:
|
env:
|
||||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
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>
|
<a href="CHANGELOG_EN.md">English</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
## v1.8.0(main)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
|
||||||
## v1.7.0(main)
|
## v1.7.0(main)
|
||||||
|
|
||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
|
|||||||
@@ -6,6 +6,14 @@
|
|||||||
<a href="CHANGELOG_EN.md">English</a>
|
<a href="CHANGELOG_EN.md">English</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
## v1.8.0(main)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
|
||||||
## v1.7.0(main)
|
## v1.7.0(main)
|
||||||
|
|
||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cloudflare_temp_email",
|
"name": "cloudflare_temp_email",
|
||||||
"version": "1.7.0",
|
"version": "1.8.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Generated
+4
-4
@@ -2654,8 +2654,8 @@ packages:
|
|||||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
safe-array-concat@1.1.3:
|
safe-array-concat@1.1.4:
|
||||||
resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==}
|
resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==}
|
||||||
engines: {node: '>=0.4'}
|
engines: {node: '>=0.4'}
|
||||||
|
|
||||||
safe-buffer@5.1.2:
|
safe-buffer@5.1.2:
|
||||||
@@ -5133,7 +5133,7 @@ snapshots:
|
|||||||
object.assign: 4.1.7
|
object.assign: 4.1.7
|
||||||
own-keys: 1.0.1
|
own-keys: 1.0.1
|
||||||
regexp.prototype.flags: 1.5.4
|
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-push-apply: 1.0.0
|
||||||
safe-regex-test: 1.1.0
|
safe-regex-test: 1.1.0
|
||||||
set-proto: 1.0.0
|
set-proto: 1.0.0
|
||||||
@@ -5953,7 +5953,7 @@ snapshots:
|
|||||||
'@rollup/rollup-win32-x64-msvc': 4.60.2
|
'@rollup/rollup-win32-x64-msvc': 4.60.2
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
safe-array-concat@1.1.3:
|
safe-array-concat@1.1.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.9
|
call-bind: 1.0.9
|
||||||
call-bound: 1.0.4
|
call-bound: 1.0.4
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "temp-email-pages",
|
"name": "temp-email-pages",
|
||||||
"version": "1.7.0",
|
"version": "1.8.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "temp-mail-docs",
|
"name": "temp-mail-docs",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.7.0",
|
"version": "1.8.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^25.6.0",
|
"@types/node": "^25.6.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cloudflare_temp_email",
|
"name": "cloudflare_temp_email",
|
||||||
"version": "1.7.0",
|
"version": "1.8.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export const CONSTANTS = {
|
export const CONSTANTS = {
|
||||||
VERSION: 'v' + '1.7.0',
|
VERSION: 'v' + '1.8.0',
|
||||||
|
|
||||||
// DB Version
|
// DB Version
|
||||||
DB_VERSION_KEY: 'db_version',
|
DB_VERSION_KEY: 'db_version',
|
||||||
|
|||||||
Reference in New Issue
Block a user