chore: streamline compatibility tracking and documentation
Release / Validate Templates (push) Canceled after 0s
Release / Package & Release (push) Canceled after 0s

This commit is contained in:
KenanZhu committed 2026-10-09 11:43:02 +08:00
1 parent 92d2ba9889
commit 23f0456622
16 files changed
+476 -259

No files matched your search

+1 -1
View File
@@ -19,4 +19,4 @@ DOCS
--- ---
**Full Changelog:** [v1.0.1...v1.27.2](https://github.com/KenanZhu/GiteaMailTemplates/compare/v1.0.1...v1.27.2) **Full Changelog:** [v1.0.1...v1.27.2](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/compare/v1.0.1...v1.27.2)
+1 -1
View File
@@ -19,4 +19,4 @@ DOCS
--- ---
**Full Changelog:** [v1.27.2...v1.27.3](https://github.com/KenanZhu/GiteaMailTemplates/compare/v1.27.2...v1.27.3) **Full Changelog:** [v1.27.2...v1.27.3](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/compare/v1.27.2...v1.27.3)
+1 -1
View File
@@ -18,4 +18,4 @@ DOCS
--- ---
**Full Changelog:** [v1.27.3...v28.0.0](https://github.com/KenanZhu/GiteaMailTemplates/compare/v1.27.3...v28.0.0) **Full Changelog:** [v1.27.3...v28.0.0](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/compare/v1.27.3...v28.0.0)
+137
View File
@@ -0,0 +1,137 @@
"""Offline tests for the compatibility tracker."""
import importlib.util
import io
import json
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
SCRIPT = Path(__file__).with_name("track_gitea_release.py")
SPEC = importlib.util.spec_from_file_location("track_gitea_release", SCRIPT)
TRACKER = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(TRACKER)
class TrackerTests(unittest.TestCase):
def setUp(self):
self.directory = tempfile.TemporaryDirectory(prefix="tracker-test-", dir=TRACKER.ROOT)
self.addCleanup(self.directory.cleanup)
self.root = Path(self.directory.name)
self.paths = ("COMPATIBILITY.md", "README.md", "AGENTS.md", "docs/README.zh-CN.md")
self.originals = {}
for name in self.paths:
destination = self.root / name
destination.parent.mkdir(parents=True, exist_ok=True)
content = (TRACKER.ROOT / name).read_text(encoding="utf-8")
destination.write_text(content, encoding="utf-8")
self.originals[name] = content
def read(self, name):
return (self.root / name).read_text(encoding="utf-8")
def assert_unchanged(self):
for name, content in self.originals.items():
self.assertEqual(content, self.read(name), name)
def test_new_release_adds_only_pending_rows(self):
changed = TRACKER.apply_release(self.root, "28.2.0", "2026-11-01")
self.assertEqual({self.root / name for name in self.paths}, set(changed))
compatibility = self.read("COMPATIBILITY.md")
self.assertIn("| 28.2.0 | [PENDING] Compatibility verification", compatibility)
self.assertIn("| **28.2.0** | 2026-11-01 | [PENDING]", compatibility)
self.assertIn("| **v28.0.0** | **28.0.0** | **28.0.0** | [PASS] Active |", compatibility)
self.assertIn("Latest verified:** Release v28.0.0", compatibility)
self.assertIn("Gitea-28.2.0%20%5BPENDING%5D%20%7C%2028.0.0%20tested-yellow", self.read("README.md"))
self.assertIn("Gitea 28.2.0:** [PENDING]", self.read("README.md"))
self.assertIn("Gitea 28.2.0:** [PENDING]", self.read("docs/README.zh-CN.md"))
self.assertIn("Gitea release: 28.2.0 [PENDING]", self.read("AGENTS.md"))
self.assertIn("Latest tested:** Gitea 28.0.0", self.read("README.md"))
self.assertIn("最新测试:** Gitea 28.0.0", self.read("docs/README.zh-CN.md"))
self.assertEqual([], TRACKER.apply_release(self.root, "28.2.0", "2026-11-01"))
def test_existing_version_is_idempotent(self):
self.assertEqual([], TRACKER.apply_release(self.root, "28.1.0", "2026-10-06"))
self.assert_unchanged()
def test_partial_update_is_repaired(self):
compatibility = self.root / "COMPATIBILITY.md"
partial = self.read("COMPATIBILITY.md").replace("| 28.1.0 | [PENDING] Compatibility verification; no tested template release yet |\n", "")
compatibility.write_text(partial, encoding="utf-8")
self.assertEqual([compatibility], TRACKER.apply_release(self.root, "28.1.0", "2026-10-06"))
self.assertIn("| 28.1.0 | [PENDING] Compatibility verification", self.read("COMPATIBILITY.md"))
def test_older_version_does_not_rewrite_history(self):
self.assertEqual([], TRACKER.apply_release(self.root, "28.0.0", "2026-09-29"))
self.assert_unchanged()
def test_dry_run_does_not_write(self):
self.assertEqual(4, len(TRACKER.apply_release(self.root, "28.2.0", "2026-11-01", dry_run=True)))
self.assert_unchanged()
def test_missing_marker_fails_without_writing(self):
compatibility = self.root / "COMPATIBILITY.md"
compatibility.write_text(self.read("COMPATIBILITY.md").replace("<!-- TRACKER:HISTORY -->", ""), encoding="utf-8")
with self.assertRaises(ValueError):
TRACKER.apply_release(self.root, "28.2.0", "2026-11-01")
self.assertEqual(self.originals["README.md"], self.read("README.md"))
def test_unknown_marker_fails_without_writing(self):
news = self.root / "docs/NEWS.md"
news.write_text("Gitea 28.1.0 [PENDING] <!-- TRACKER:FUTURE-UNKNOWN -->\n", encoding="utf-8")
with self.assertRaisesRegex(ValueError, "Unknown TRACKER marker"):
TRACKER.apply_release(self.root, "28.2.0", "2026-11-01")
self.assert_unchanged()
def test_new_marked_document_is_discovered(self):
news = self.root / "docs/NEWS.md"
news.write_text("Gitea 28.1.0 [PENDING] <!-- TRACKER:UPSTREAM -->\n", encoding="utf-8")
changed = TRACKER.apply_release(self.root, "28.2.0", "2026-11-01")
self.assertIn(news, changed)
self.assertEqual("Gitea 28.2.0 [PENDING] <!-- TRACKER:UPSTREAM -->\n", news.read_text(encoding="utf-8"))
def test_disagreeing_tested_versions_fail(self):
chinese = self.root / "docs/README.zh-CN.md"
chinese.write_text(self.read("docs/README.zh-CN.md").replace("**最新测试:** Gitea 28.0.0", "**最新测试:** Gitea 28.1.0"), encoding="utf-8")
with self.assertRaisesRegex(ValueError, "disagree"):
TRACKER.apply_release(self.root, "28.2.0", "2026-11-01")
def test_after_verification_new_version_becomes_pending(self):
english = self.root / "README.md"
chinese = self.root / "docs/README.zh-CN.md"
agents = self.root / "AGENTS.md"
english.write_text(self.read("README.md")
.replace("**Latest tested:** Gitea 28.0.0", "**Latest tested:** Gitea 28.1.0")
.replace("Gitea-28.1.0%20%5BPENDING%5D%20%7C%2028.0.0%20tested-yellow", "Gitea-28.1.0%20%7C%2028.1.0%20tested-blue")
.replace("**Upstream Gitea 28.1.0:** [PENDING]", "**Upstream Gitea 28.1.0:** [PASS]"), encoding="utf-8")
chinese.write_text(self.read("docs/README.zh-CN.md")
.replace("**最新测试:** Gitea 28.0.0", "**最新测试:** Gitea 28.1.0")
.replace("**上游 Gitea 28.1.0:** [PENDING]", "**上游 Gitea 28.1.0:** [PASS]"), encoding="utf-8")
agents.write_text(self.read("AGENTS.md").replace("28.1.0 [PENDING]", "28.1.0 [PASS]"), encoding="utf-8")
TRACKER.apply_release(self.root, "28.2.0", "2026-11-01")
self.assertIn("Gitea-28.2.0%20%5BPENDING%5D%20%7C%2028.1.0%20tested-yellow", self.read("README.md"))
self.assertIn("Gitea 28.2.0:** [PENDING]", self.read("README.md"))
self.assertIn("Latest tested:** Gitea 28.1.0", self.read("README.md"))
self.assertIn("最新测试:** Gitea 28.1.0", self.read("docs/README.zh-CN.md"))
def test_invalid_version_rejected(self):
with self.assertRaises(ValueError):
TRACKER.apply_release(self.root, "28.2.0-rc1", "2026-11-01")
def test_release_api_uses_published_date(self):
payload = {"tag_name": "v28.2.0", "published_at": "2026-11-01T08:15:00Z", "draft": False, "prerelease": False}
with patch.object(TRACKER, "urlopen", return_value=io.BytesIO(json.dumps(payload).encode("utf-8"))) as mocked:
self.assertEqual(("28.2.0", "2026-11-01"), TRACKER.fetch_release("28.2.0"))
self.assertEqual(f"{TRACKER.RELEASES_API}/tags/v28.2.0", mocked.call_args.args[0].full_url)
def test_prerelease_is_rejected(self):
payload = {"tag_name": "v28.2.0", "published_at": "2026-11-01T08:15:00Z", "prerelease": True}
with patch.object(TRACKER, "urlopen", return_value=io.BytesIO(json.dumps(payload).encode("utf-8"))):
with self.assertRaises(ValueError):
TRACKER.fetch_release(None)
if __name__ == "__main__":
unittest.main()
+189
View File
@@ -0,0 +1,189 @@
#!/usr/bin/env python3
"""Record a new upstream Gitea release without changing verified compatibility."""
import argparse
import json
import os
import re
import sys
from datetime import date
from pathlib import Path
from urllib.request import Request, urlopen
ROOT = Path(__file__).resolve().parents[2]
RELEASES_API = "https://api.github.com/repos/go-gitea/gitea/releases"
VERSION_RE = re.compile(r"v?(\d+)\.(\d+)\.(\d+)\Z")
ROW_RE = re.compile(r"^\|\s*(?:\*\*)?(\d+\.\d+\.\d+)(?:\*\*)?\s*\|")
INLINE_VERSION_RE = re.compile(r"(?<!\d)\d+\.\d+\.\d+(?!\d)")
STATUS_RE = re.compile(r"\[(?:PENDING|PASS|WARN|FAIL|UNSUPPORTED)\]")
MARKER_RE = re.compile(r"<!-- TRACKER:([A-Z][A-Z0-9-]*) -->")
BADGE_RE = re.compile(r"Gitea-(\d+\.\d+\.\d+)(?:%20(?:pending|%5BPENDING%5D))?%20%7C%20(\d+\.\d+\.\d+)%20tested-(?:blue|yellow)")
TABLES = {
"VERSION-MAP": "| Gitea version | Template release |",
"HISTORY": "| Gitea | Release Date | Mail Template Changes | Breaking? |",
}
AUTO_MARKERS = frozenset((*TABLES, "BADGE", "UPSTREAM"))
MANUAL_MARKERS = frozenset(("LATEST-TESTED", "LATEST-VERIFIED"))
REQUIRED_MARKERS = {"VERSION-MAP": 1, "HISTORY": 1, "BADGE": 1, "UPSTREAM": 3, "LATEST-TESTED": 2, "LATEST-VERIFIED": 1}
def version_key(value):
match = VERSION_RE.fullmatch(value)
if not match:
raise ValueError(f"Expected a stable X.Y.Z version, got {value!r}")
return tuple(int(part) for part in match.groups())
def fetch_release(requested_version):
endpoint = f"{RELEASES_API}/tags/v{requested_version}" if requested_version else f"{RELEASES_API}/latest"
request = Request(endpoint, headers={"Accept": "application/vnd.github+json", "User-Agent": "gitea-mail-templates-tracker"})
token = os.environ.get("GITHUB_TOKEN")
if token:
request.add_header("Authorization", f"Bearer {token}")
with urlopen(request, timeout=20) as response:
release = json.load(response)
if release.get("draft") or release.get("prerelease"):
raise ValueError("Draft and prerelease Gitea versions are not tracked")
version = str(release["tag_name"]).removeprefix("v")
version_key(version)
if requested_version and version != requested_version:
raise ValueError(f"Release tag mismatch: expected {requested_version}, got {version}")
published = release.get("published_at")
if not published:
raise ValueError(f"Release v{version} has no published_at date")
release_date = date.fromisoformat(published[:10]).isoformat()
return version, release_date
def table(lines, marker_index, expected_header):
header = marker_index + 1
if header + 1 >= len(lines) or lines[header].strip() != expected_header:
raise ValueError(f"Unexpected table header after {lines[marker_index].strip()}")
if not re.fullmatch(r"\|[\s|:-]+\|", lines[header + 1].strip()):
raise ValueError(f"Missing Markdown table separator after {lines[marker_index].strip()}")
first = header + 2
end = first
while end < len(lines) and lines[end].startswith("|"):
end += 1
versions = set()
for line in lines[first:end]:
match = ROW_RE.match(line)
if not match:
if expected_header == TABLES["HISTORY"] and re.match(r"^\|\s*\*\*≤\s*\d+\.\d+\.x\*\*\s*\|", line):
continue
raise ValueError(f"Malformed version row after {lines[marker_index].strip()}: {line.strip()}")
if match.group(1) in versions:
raise ValueError(f"Duplicate version after {lines[marker_index].strip()}: {match.group(1)}")
versions.add(match.group(1))
if not versions:
raise ValueError(f"Empty version table after {lines[marker_index].strip()}")
return first, versions
def documents(root):
"""Discover Markdown docs; new docs opt in by adding a known TRACKER marker."""
paths = sorted((*root.glob("*.md"), *root.glob("docs/**/*.md")))
result = {path: path.read_text(encoding="utf-8").splitlines(keepends=True) for path in paths}
markers = []
for path, lines in result.items():
for index, line in enumerate(lines):
for match in MARKER_RE.finditer(line):
name = match.group(1)
if name not in AUTO_MARKERS | MANUAL_MARKERS:
raise ValueError(f"Unknown TRACKER marker {name} in {path}")
markers.append((path, index, name))
for name, minimum in REQUIRED_MARKERS.items():
count = sum(item[2] == name for item in markers)
if count < minimum or (name in TABLES or name == "BADGE") and count != minimum:
raise ValueError(f"Expected {minimum} TRACKER:{name} marker(s), found {count}")
return result, markers
def latest_tested(documents_by_path, markers):
versions = set()
for path, index, name in markers:
if name == "LATEST-TESTED":
match = re.search(r"Gitea\s+(\d+\.\d+\.\d+)", documents_by_path[path][index])
if not match:
raise ValueError(f"Missing Gitea version beside TRACKER:LATEST-TESTED in {path}")
versions.add(match.group(1))
if len(versions) != 1:
raise ValueError(f"LATEST-TESTED markers disagree: {sorted(versions)}")
return versions.pop()
def update_marker(lines, index, name, version, release_date, tested):
if name in TABLES:
first, versions = table(lines, index, TABLES[name])
if version not in versions:
if name == "VERSION-MAP":
row = f"| {version} | [PENDING] Compatibility verification; no tested template release yet |\n"
else:
row = f"| **{version}** | {release_date} | [PENDING] Mail-template compatibility verification | TBD |\n"
lines.insert(first, row)
elif name == "UPSTREAM":
versions = list(INLINE_VERSION_RE.finditer(lines[index]))
if len(STATUS_RE.findall(lines[index])) != 1 or len(versions) != 1:
raise ValueError("TRACKER:UPSTREAM needs exactly one status and version on the same line")
line = INLINE_VERSION_RE.sub(version, lines[index], count=1)
lines[index] = STATUS_RE.sub("[PENDING]", line, count=1)
elif name == "BADGE":
match = BADGE_RE.search(lines[index])
if not match or match.group(2) != tested:
raise ValueError("TRACKER:BADGE has an unexpected format or tested version")
badge = f"Gitea-{version}%20%5BPENDING%5D%20%7C%20{tested}%20tested-yellow"
lines[index] = BADGE_RE.sub(badge, lines[index], count=1)
def apply_release(root, version, release_date, dry_run=False):
target = version_key(version)
date.fromisoformat(release_date)
originals, markers = documents(root)
tested = latest_tested(originals, markers)
history_path, history_index, _ = next(item for item in markers if item[2] == "HISTORY")
history_first, _ = table(originals[history_path], history_index, TABLES["HISTORY"])
latest = version_key(ROW_RE.match(originals[history_path][history_first]).group(1))
if target < latest or target <= version_key(tested):
return []
updated = {path: lines.copy() for path, lines in originals.items()}
for path, index, name in sorted(markers, key=lambda item: (str(item[0]), item[1]), reverse=True):
if name in AUTO_MARKERS:
update_marker(updated[path], index, name, version, release_date, tested)
changed = [path for path in updated if updated[path] != originals[path]]
if not dry_run:
for path in changed:
path.write_text("".join(updated[path]), encoding="utf-8")
return changed
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--version", default=os.environ.get("GITEA_VERSION"), help="Specific stable Gitea version; omit for latest release")
parser.add_argument("--release-date", help="YYYY-MM-DD; use with --version for offline checks")
parser.add_argument("--dry-run", action="store_true", help="Check without changing repository documentation")
args = parser.parse_args()
if args.release_date and not args.version:
parser.error("--release-date requires --version")
if args.version:
args.version = args.version.removeprefix("v")
version_key(args.version)
if args.release_date:
version, release_date = args.version, date.fromisoformat(args.release_date).isoformat()
else:
version, release_date = fetch_release(args.version)
changed = apply_release(ROOT, version, release_date, dry_run=args.dry_run)
print(f"Gitea {version}: {', '.join(str(path.relative_to(ROOT)) for path in changed) if changed else 'already tracked'}")
output_path = os.environ.get("GITHUB_OUTPUT")
if output_path:
with open(output_path, "a", encoding="utf-8") as output:
output.write(f"version={version}\nchanged={'true' if changed else 'false'}\n")
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except (KeyError, OSError, ValueError) as error:
sys.exit(f"Tracker failed: {error}")
+23 -95
View File
@@ -2,12 +2,11 @@ name: Gitea Version Tracker
on: on:
schedule: schedule:
# Daily at 08:00 UTC — check for new Gitea releases
- cron: '0 8 * * *' - cron: '0 8 * * *'
workflow_dispatch: workflow_dispatch:
inputs: inputs:
version: version:
description: 'Gitea version (e.g. 1.27.0) — leave empty to auto-detect' description: 'Stable Gitea version (e.g. 28.2.0); empty means latest'
required: false required: false
jobs: jobs:
@@ -20,103 +19,32 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Determine latest Gitea version - uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Test tracker
run: python -B -m unittest discover -s .github/scripts -p 'test_*.py'
- name: Record pending Gitea release
id: version id: version
run: | run: python -B .github/scripts/track_gitea_release.py
if [ -n "${{ github.event.inputs.version }}" ]; then env:
NEW_VERSION="${{ github.event.inputs.version }}" GITEA_VERSION: ${{ github.event.inputs.version }}
else GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Fetch latest release from GitHub API (go-gitea/gitea mirror)
RELEASE_JSON=$(curl -sSf https://api.github.com/repos/go-gitea/gitea/releases/latest)
NEW_VERSION=$(echo "$RELEASE_JSON" | jq -r '.tag_name')
fi
# Strip 'v' prefix if present (API returns v1.27.0, we use 1.27.0)
NEW_VERSION="${NEW_VERSION#v}"
echo "New Gitea version: $NEW_VERSION"
# Parse marker line AND embedded OFFSET to locate the data row
MARKER_INFO=$(grep -n 'TRACKER:QUICK-REF-MAX' COMPATIBILITY.md)
MARKER_LINE=$(echo "$MARKER_INFO" | cut -d: -f1)
OFFSET=$(echo "$MARKER_INFO" | sed 's/.*OFFSET=\([0-9]*\).*/\1/')
CURRENT=$(sed -n "$((MARKER_LINE + OFFSET))p" COMPATIBILITY.md | awk -F'|' '{gsub(/\*| /, "", $4); print $4}')
echo "Current max tested: $CURRENT"
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "current_version=$CURRENT" >> $GITHUB_OUTPUT
if [ "$NEW_VERSION" = "$CURRENT" ]; then
echo "No new Gitea release detected. Skipping."
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "New version detected: $CURRENT → $NEW_VERSION"
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: COMPATIBILITY.md — Quick Reference (max tested version)
if: steps.version.outputs.skip != 'true'
run: |
NEW="${{ steps.version.outputs.new_version }}"
# Parse marker line and embedded OFFSET to locate the data row
MARKER_INFO=$(grep -n 'TRACKER:QUICK-REF-MAX' COMPATIBILITY.md)
MARKER=$(echo "$MARKER_INFO" | cut -d: -f1)
OFFSET=$(echo "$MARKER_INFO" | sed 's/.*OFFSET=\([0-9]*\).*/\1/')
TARGET=$((MARKER + OFFSET))
# Update column 4 (Max Tested) and column 5 (Status) in the pipe-separated row
awk -i inplace -F'|' -v OFS='|' -v ver="${NEW}" '
NR == '$TARGET' { $4 = " **" ver "** "; $5 = " ⏳ Pending Verification " }
{ print }
' COMPATIBILITY.md
- name: COMPATIBILITY.md — Version History table
if: steps.version.outputs.skip != 'true'
run: |
NEW="${{ steps.version.outputs.new_version }}"
TODAY=$(date +%Y-%m-%d)
# Parse marker line and embedded OFFSET for the insertion point
MARKER_INFO=$(grep -n 'TRACKER:VERSION-INSERT' COMPATIBILITY.md)
MARKER=$(echo "$MARKER_INFO" | cut -d: -f1)
OFFSET=$(echo "$MARKER_INFO" | sed 's/.*OFFSET=\([0-9]*\).*/\1/')
INSERT=$((MARKER + OFFSET))
ROW="| **${NEW}** | ${TODAY} | ⏳ Pending Verification | TBD |"
sed -i "${INSERT}a ${ROW}" COMPATIBILITY.md
- name: README.md — pending badge
if: steps.version.outputs.skip != 'true'
run: |
NEW="${{ steps.version.outputs.new_version }}"
# Badge: update version, change label tested→pending, color blue→yellow
sed -i "/TRACKER:BADGE/s/%20[0-9a-z.-]*%20tested-blue/%20${NEW}%20pending-yellow/" README.md
- name: Create Pull Request - name: Create Pull Request
if: steps.version.outputs.skip != 'true' if: steps.version.outputs.changed == 'true'
uses: peter-evans/create-pull-request@v7 uses: peter-evans/create-pull-request@v7
with: with:
branch: track/gitea-${{ steps.version.outputs.new_version }} add-paths: '*.md'
commit-message: "docs: track Gitea ${{ steps.version.outputs.new_version }} — pending verification" branch: track/gitea-${{ steps.version.outputs.version }}
title: "Track Gitea ${{ steps.version.outputs.new_version }} — ⏳ Pending Verification" commit-message: "docs: track Gitea ${{ steps.version.outputs.version }} pending verification"
title: "Track Gitea ${{ steps.version.outputs.version }} compatibility"
body: | body: |
Gitea **${{ steps.version.outputs.new_version }}** has been released. Gitea **${{ steps.version.outputs.version }}** is recorded as [PENDING] across the marked documentation. Verified/tested ranges remain unchanged.
This PR: - [ ] Review upstream mail templates, mailer data, functions, and translation keys.
- Updates the compatibility matrix in `COMPATIBILITY.md` - [ ] Run `cd tools && go test ./...` and `go run . preview all`.
- Marks the new version as pending in the `README.md` badge - [ ] Record findings in `COMPATIBILITY.md`; update tested ranges and status text only after verification.
- Marks the new version as **⏳ Pending Verification** - [ ] If template changes are needed, release them separately with reviewed notes and assets.
## Verification Checklist
- [ ] Check [Gitea changelog](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md) for mail template changes
- [ ] Review diff of official templates: `git diff v${{ steps.version.outputs.current_version }}..v${{ steps.version.outputs.new_version }} -- templates/mail/ services/mailer/`
- [ ] Run `cd tools && go run . preview all` to confirm templates still render
- [ ] Run `cd tools && go test ./...` to verify push-notification data paths
- [ ] Update status from ⏳ to ✅ (compatible) or ❌ (breaking) in COMPATIBILITY.md
- [ ] Update the `Mail Template Changes` and `Breaking?` columns with accurate notes
- [ ] After verification, update `TRACKER:LATEST-VERIFIED` and the English/Chinese `TRACKER:LATEST-TESTED` lines; switch the README badge from pending to tested
## Semantic tracking
Once verified, update the tracked Gitea version:
- [ ] If templates changed, mark the top **Template Release** row `Unreleased` until the fix is tagged; if no change is needed, retain the verified release tag
- [ ] Update the README release status and compatibility matrix to distinguish source fixes from published packages
- [ ] Write `.github/release-notes/vX.Y.Z.md` before tagging; the release workflow uses this file for the published notes
- [ ] Tag a new release when shipping template fixes — `git tag vX.Y.Z && git push origin vX.Y.Z` (the release workflow packages automatically)
labels: compatibility, automated
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+11 -1
View File
@@ -34,16 +34,26 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.21'
- name: Verify release notes - name: Verify release notes
run: test -s ".github/release-notes/${GITHUB_REF_NAME}.md" run: test -s ".github/release-notes/${GITHUB_REF_NAME}.md"
- name: Generate static preview for the archive
working-directory: tools
run: go run . preview all
- name: Package themes/ - name: Package themes/
run: | run: |
VERSION=${GITHUB_REF#refs/tags/} VERSION=${GITHUB_REF#refs/tags/}
ARCHIVE="gitea-mail-templates-${VERSION}" ARCHIVE="gitea-mail-templates-${VERSION}"
mkdir -p "dist/${ARCHIVE}/docs" mkdir -p "dist/${ARCHIVE}/docs" "dist/${ARCHIVE}/preview"
cp -r themes LICENSE README.md CONTRIBUTING.md COMPATIBILITY.md "dist/${ARCHIVE}/" cp -r themes LICENSE README.md CONTRIBUTING.md COMPATIBILITY.md "dist/${ARCHIVE}/"
cp docs/README.zh-CN.md docs/CONTRIBUTING.zh-CN.md "dist/${ARCHIVE}/docs/" cp docs/README.zh-CN.md docs/CONTRIBUTING.zh-CN.md "dist/${ARCHIVE}/docs/"
cp -r docs/images "dist/${ARCHIVE}/docs/"
cp preview/index.html preview/rendered.js "dist/${ARCHIVE}/preview/"
cd dist cd dist
zip -r "${ARCHIVE}.zip" "${ARCHIVE}" zip -r "${ARCHIVE}.zip" "${ARCHIVE}"
tar -czf "${ARCHIVE}.tar.gz" "${ARCHIVE}" tar -czf "${ARCHIVE}.tar.gz" "${ARCHIVE}"
+5 -5
View File
@@ -1,7 +1,10 @@
# Dependencies # Go dependencies
node_modules/
vendor/ vendor/
# Python test cache
__pycache__/
*.pyc
# OS files # OS files
.DS_Store .DS_Store
Thumbs.db Thumbs.db
@@ -18,8 +21,5 @@ Desktop.ini
*.zip *.zip
*.tar.gz *.tar.gz
# Package lock file
package-lock.json
# Preview generated files # Preview generated files
preview/rendered.js preview/rendered.js
+17 -10
View File
@@ -18,22 +18,23 @@ themes/ # Template themes (10 styles, 11 .tmpl each = 110 source fil
neon/ # Cyberpunk / Gaming neon/ # Cyberpunk / Gaming
terminal/ # Developers / Tech terminal/ # Developers / Tech
terra/ # Nature / Sustainability terra/ # Nature / Sustainability
tools/ # Go CLI tooling (modular, zero dependencies) tools/ # Go CLI tooling (modular; uses urfave/cli/v2)
tools.go # Main entry point tools.go # Main entry point
cli/ # CLI subcommands: list, create, delete, preview cli/ # CLI subcommands: list, create, delete, preview
config/ # Config types and templates_config.json loading config/ # Config types and templates_config.json loading
data/ # templates_config.json — single source of truth for template metadata data/ # templates_config.json — single source of truth for template metadata
preview/ # Template rendering engine (funcs, locale, engine) preview/ # Template rendering engine (funcs, locale, engine)
go.mod # Go module (stdlib only, zero dependencies) go.mod # Go module (CLI dependency declared here)
preview/ # Browser-based live preview preview/ # Browser-based live preview
index.html # SPA with style/template/client/viewport switching index.html # SPA with style/template/client/viewport switching
rendered.js # Pre-rendered HTML (generated, committed for clone-and-preview) rendered.js # Pre-rendered HTML (generated, ignored in source clones)
docs/ # Bilingual documentation (English + Simplified Chinese) docs/ # Bilingual documentation (English + Simplified Chinese)
``` ```
## Working With Templates ## Working With Templates
### Template Files ### Template Files
- All `.tmpl` files use Go `html/template` syntax - All `.tmpl` files use Go `html/template` syntax
- Must use only Gitea 28's built-in template functions: `AppUrl`, `DotEscape`, `QueryEscape`, `ShortSha`, `HTMLFormat`, `PathEscapeSegments`, `FormatByteSize` - Must use only Gitea 28's built-in template functions: `AppUrl`, `DotEscape`, `QueryEscape`, `ShortSha`, `HTMLFormat`, `PathEscapeSegments`, `FormatByteSize`
- Must use only official Gitea translation keys (`mail.*` namespace) - Must use only official Gitea translation keys (`mail.*` namespace)
@@ -41,20 +42,23 @@ docs/ # Bilingual documentation (English + Simplified Chinese)
- Each style must have all 11 template types - Each style must have all 11 template types
### Adding a New Theme ### Adding a New Theme
1. Scaffold the new theme: `cd tools && go run . create <name>` — creates the full directory structure with placeholder `.tmpl` files for all 11 email types 1. Scaffold the new theme: `cd tools && go run . create <name>` — creates the full directory structure with placeholder `.tmpl` files for all 11 email types
2. Write all 11 `.tmpl` files with unique visual design 2. Write all 11 `.tmpl` files with unique visual design
3. Run `cd tools && go run . preview all` to regenerate preview data 3. Run `cd tools && go run . preview all` to regenerate preview data
4. Update README.md style gallery table 4. Update README.md style gallery table
### Preview System ### Preview System
- `preview/index.html` loads `preview/rendered.js` (pre-rendered by Go) and displays in iframes - `preview/index.html` loads `preview/rendered.js` (pre-rendered by Go) and displays in iframes
- Supports theme/template switching, view mode (Modern/Source), and viewport toggle (Desktop 1386x780 / Mobile 390x780) - Supports theme/template switching, view mode (Modern/Source), and viewport toggle (Desktop 1386x780 / Mobile 390x780)
- Keyboard navigation: `←→` cycles focus between Theme/Template/View selects, `↑↓` selects within the focused dropdown, `d`/`m` toggles viewport - Keyboard navigation: `←→` cycles focus between Theme/Template/View selects, `↑↓` selects within the focused dropdown, `d`/`m` toggles viewport
- `REGISTRY` and `PARAMS` are auto-generated from `templates_config.json` — no manual syncing needed - `REGISTRY` and `PARAMS` are auto-generated from `templates_config.json` — no manual syncing needed
- Static preview (open `index.html` directly) — works via `file://` protocol with Modern and Source views - Static preview works via `file://` after running `go run . preview all` in a source clone; the updated packaging workflow includes generated preview data and screenshots for future builds (v28.0.0 and older archives include neither)
- Dev server (`go run . dev`) — pure Go HTTP server with SSE live reload; watches `themes/` for `.tmpl` changes, re-renders in-process, and pushes reload events to the browser - Dev server (`go run . dev`) — pure Go HTTP server with SSE live reload; watches `themes/` for `.tmpl` changes, re-renders in-process, and pushes reload events to the browser
### Build Tool ### Build Tool
- `tools/tools.go` is the main entry point for the modular CLI - `tools/tools.go` is the main entry point for the modular CLI
- Subcommands: `list`, `create`, `delete`, `preview`, `dev` - Subcommands: `list`, `create`, `delete`, `preview`, `dev`
- Template metadata lives in `tools/data/templates_config.json` — the single source of truth - Template metadata lives in `tools/data/templates_config.json` — the single source of truth
@@ -67,13 +71,15 @@ docs/ # Bilingual documentation (English + Simplified Chinese)
## Versioning ## Versioning
- Release tags identify actual downloadable packages; the compatibility matrix distinguishes released tags from source-only fixes - Release tags identify actual downloadable packages; the compatibility matrix distinguishes released tags from source-only fixes
- The current release is **v28.0.0**, verified against Gitea 28.0.0. Gitea 28 removes `FileSize` in favor of `FormatByteSize`, so v28.0.0 is not compatible with Gitea 1.25.0–1.27.3; use v1.27.3 for those versions. The quick-reference table in `COMPATIBILITY.md` lists the active release first; the tracker workflow updates that row by position - The current release is **v28.0.0**, verified against Gitea 28.0.0. Gitea 28 removes `FileSize` in favor of `FormatByteSize`, so v28.0.0 is not compatible with Gitea 1.25.0–1.27.3; use v1.27.3 for those versions. The quick-reference table in `COMPATIBILITY.md` lists the active release first
- When a new Gitea version is checked: update the top `COMPATIBILITY.md` row and README badge; keep unreleased fixes distinct from the published release, and update verified/tested wording only after verification - Latest upstream Gitea release: 28.1.0 [PENDING]. <!-- TRACKER:UPSTREAM -->
- Tag a new release (`vX.Y.Z`) only when the template content itself changes — the release workflow packages automatically on tag push - When a new Gitea version appears, the tracker updates pending rows, marked version lines, and the pending README badge. After verification, update the top `COMPATIBILITY.md` tested range and README tested text/badge; keep unreleased fixes distinct from the published release
- Before tagging, add `.github/release-notes/vX.Y.Z.md`; the release workflow validates the file, runs `go test ./...`, and publishes those reviewed notes with the archives - Tag a new release (`vX.Y.Z`) only when the template content itself changes. On the new Gitea host, do not assume tag pushes automatically build or upload archives; verify the Gitea workflow before relying on it
- Keep the `TRACKER:` markers in `COMPATIBILITY.md` / `README.md` adjacent to their rows so the automated workflow keeps parsing them - Before tagging, add `.github/release-notes/vX.Y.Z.md`, run `go test ./...` and `go run . preview all` from `tools/`, then build and upload the archives with the reviewed notes. `.github/workflows/release.yml` retains the earlier GitHub Actions flow as a reference
- Keep `TRACKER:VERSION-MAP` and `TRACKER:HISTORY` above their tables, and inline `TRACKER:BADGE`, `TRACKER:UPSTREAM`, `TRACKER:LATEST-TESTED`, and `TRACKER:LATEST-VERIFIED` markers with their text. The Python tracker updates pending markers; tested/verified markers are manual-only
## Commit Conventions ## Commit Conventions
- `style(name):` — template changes for a specific theme - `style(name):` — template changes for a specific theme
- `preview:` — preview tooling changes - `preview:` — preview tooling changes
- `tools:` — Go CLI/build tooling changes - `tools:` — Go CLI/build tooling changes
@@ -84,7 +90,8 @@ docs/ # Bilingual documentation (English + Simplified Chinese)
- `chore:` — maintenance (config updates, build scripts) - `chore:` — maintenance (config updates, build scripts)
## Constraints ## Constraints
- No JavaScript framework dependencies — preview is vanilla JS - No JavaScript framework dependencies — preview is vanilla JS
- No external Go dependencies — build uses stdlib only - The CLI uses `github.com/urfave/cli/v2`; the preview server itself uses Go's standard HTTP library
- Templates must remain compatible with Gitea's `html/template` execution environment - Templates must remain compatible with Gitea's `html/template` execution environment
- Preview works with `file://` protocol (no server needed) - Preview works with `file://` protocol (no server needed)
+17 -14
View File
@@ -4,14 +4,13 @@ This document tracks the compatibility between **Gitea Mail Templates** releases
## Quick Reference ## Quick Reference
<!-- TRACKER:QUICK-REF-MAX OFFSET=3 -->
| Template Release | Min Gitea | Max Tested Gitea | Status | | Template Release | Min Gitea | Max Tested Gitea | Status |
|-----------------|-----------|-----------------|--------| |-----------------|-----------|-----------------|--------|
| **v28.0.0** | **28.0.0** | **28.0.0** | ✅ Active | | **v28.0.0** | **28.0.0** | **28.0.0** | [PASS] Active |
| **v1.27.3** | **1.25.0** | **1.27.3** | ✅ Superseded; release emails fail on Gitea 28.0.0 (`FileSize` removed) | | **v1.27.3** | **1.25.0** | **1.27.3** | [PASS] Superseded; release emails fail on Gitea 28.0.0 (`FileSize` removed) |
| **v1.27.2** | **1.25.0** | **1.27.3** | ⚠️ Push notices fail in Bloom, Ember, and Heritage on Gitea 1.27.1+ | | **v1.27.2** | **1.25.0** | **1.27.3** | [WARN] Push notices fail in Bloom, Ember, and Heritage on Gitea 1.27.1+ |
| **v1.0.1** | **1.25.0** | **1.27.0** | ✅ Superseded; push notices need newer release on 1.27.1+ | | **v1.0.1** | **1.25.0** | **1.27.0** | [PASS] Superseded; push notices need newer release on 1.27.1+ |
| **v1.0.0** | **1.25.0** | **1.26.4** | ✅ Superseded | | **v1.0.0** | **1.25.0** | **1.26.4** | [PASS] Superseded |
> **Latest verified:** Release v28.0.0 passes the Gitea 28.0.0 mail-template, mailer-context, function, and translation-key source audit plus all-theme rendering tests. This release requires Gitea 28.0.0; use v1.27.3 for Gitea 1.25.0–1.27.3. <!-- TRACKER:LATEST-VERIFIED --> > **Latest verified:** Release v28.0.0 passes the Gitea 28.0.0 mail-template, mailer-context, function, and translation-key source audit plus all-theme rendering tests. This release requires Gitea 28.0.0; use v1.27.3 for Gitea 1.25.0–1.27.3. <!-- TRACKER:LATEST-VERIFIED -->
@@ -19,15 +18,17 @@ This document tracks the compatibility between **Gitea Mail Templates** releases
The release tag identifies the downloadable template package. The supported Gitea version may be appended in parentheses in this compatibility matrix; the parenthesized version is not a Git tag. An **unreleased** row describes fixes available in the repository but not yet in a downloadable release. The release tag identifies the downloadable template package. The supported Gitea version may be appended in parentheses in this compatibility matrix; the parenthesized version is not a Git tag. An **unreleased** row describes fixes available in the repository but not yet in a downloadable release.
<!-- TRACKER:VERSION-MAP -->
| Gitea version | Template release | | Gitea version | Template release |
|---------------|------------------| |---------------|------------------|
| 28.1.0 | [PENDING] Compatibility verification; no tested template release yet |
| 28.0.0 | **v28.0.0**; older releases use the removed `FileSize` function in release emails | | 28.0.0 | **v28.0.0**; older releases use the removed `FileSize` function in release emails |
| 1.27.3 | **v1.27.3**; v1.27.2 has a push-notification issue in three themes | | 1.27.3 | **v1.27.3**; v1.27.2 has a push-notification issue in three themes |
| 1.27.2 | **v1.27.3**; v1.27.2 has the same issue | | 1.27.2 | **v1.27.3**; v1.27.2 has the same issue |
| 1.27.1 | **v1.27.3**; v1.27.2 has the same issue | | 1.27.1 | **v1.27.3**; v1.27.2 has the same issue |
- The [tracker workflow](.github/workflows/gitea-tracker.yml) opens a PR when a new Gitea release appears, marking it ⏳ Pending Verification. - The [tracker workflow](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/src/branch/main/.github/workflows/gitea-tracker.yml) records new upstream versions as [PENDING] in this table, the history below, and the marked README and AGENTS lines. Its automatic PR creation has not been verified on the new Gitea host; check releases manually until Gitea automation is configured.
- After verification, update the top **Template Release** row. Keep fixes on `main` marked **unreleased** until a new tag is published; the [release workflow](.github/workflows/release.yml) packages the archive on tag push. - After verification, update the top **Template Release** row only when that package has been tested against the new Gitea version. Keep fixes on `main` marked **unreleased** until a new tag and downloadable Gitea Release are published; do not assume a tag push uploads archives automatically.
- The older `v1.0.1` tag predates the Gitea 1.27.1 push notification data fix and should not be used for Gitea 1.27.1 or newer. - The older `v1.0.1` tag predates the Gitea 1.27.1 push notification data fix and should not be used for Gitea 1.27.1 or newer.
- Gitea 28.0.0 replaces the mail-template `FileSize` function with `FormatByteSize`. The two functions are not interchangeable across these Gitea versions; use the matching template release. - Gitea 28.0.0 replaces the mail-template `FileSize` function with `FormatByteSize`. The two functions are not interchangeable across these Gitea versions; use the matching template release.
@@ -41,9 +42,10 @@ gitea --version
## Gitea Version History — Mail Template Impact ## Gitea Version History — Mail Template Impact
<!-- TRACKER:VERSION-INSERT OFFSET=2 --> <!-- TRACKER:HISTORY -->
| Gitea | Release Date | Mail Template Changes | Breaking? | | Gitea | Release Date | Mail Template Changes | Breaking? |
|-------|-------------|----------------------|-----------| |-------|-------------|----------------------|-----------|
| **28.1.0** | 2026-10-06 | [PENDING] Mail-template compatibility verification | TBD |
| **28.0.0** | 2026-09-29 | `FileSize` removed and `FormatByteSize` added; mail templates now have `mail/`-prefixed internal names and shared head/footer partials; workflow emails gain status-icon fields. Our custom template paths and referenced data fields remain valid. | **Yes** (release attachment formatting) | | **28.0.0** | 2026-09-29 | `FileSize` removed and `FormatByteSize` added; mail templates now have `mail/`-prefixed internal names and shared head/footer partials; workflow emails gain status-icon fields. Our custom template paths and referenced data fields remain valid. | **Yes** (release attachment formatting) |
| **1.27.3** | 2026-08-29 | No upstream mail template, mailer, or locale changes; an existing push-notification defect in three themes is fixed in template release v1.27.3 | No upstream break | | **1.27.3** | 2026-08-29 | No upstream mail template, mailer, or locale changes; an existing push-notification defect in three themes is fixed in template release v1.27.3 | No upstream break |
| **1.27.2** | 2026-08-14 | None — security + bug fixes | No | | **1.27.2** | 2026-08-14 | None — security + bug fixes | No |
@@ -56,7 +58,7 @@ gitea --version
| **1.26.0** | 2026-04-19 | AppURL cleanup; SanitizeHTML deprecated → use HTMLFormat | No | | **1.26.0** | 2026-04-19 | AppURL cleanup; SanitizeHTML deprecated → use HTMLFormat | No |
| **1.25.5** | 2026-03-10 | None — security + maintenance | No | | **1.25.5** | 2026-03-10 | None — security + maintenance | No |
| **1.25.0** | 2025 | **Directory restructure** — templates moved to `mail/<category>/<type>.tmpl` (PR #35150); subject/body split with `---` separator; template preview support added | **Yes** (structural) | | **1.25.0** | 2025 | **Directory restructure** — templates moved to `mail/<category>/<type>.tmpl` (PR #35150); subject/body split with `---` separator; template preview support added | **Yes** (structural) |
| **≤ 1.24.x** | — | Flat directory structure under `custom/templates/mail/` | ❌ Unsupported | | **≤ 1.24.x** | — | Flat directory structure under `custom/templates/mail/` | [UNSUPPORTED] |
## Template Variable Reference ## Template Variable Reference
@@ -100,7 +102,7 @@ All 11 template types use only Gitea built-in variables and functions. Verified
| `repo/issue/assigned` | `Doer`, `Issue`, `Link`, `IsPull` | | `repo/issue/assigned` | `Doer`, `Issue`, `Link`, `IsPull` |
| `repo/issue/default` | `Doer`, `Issue`, `Link`, `Body`, `ActionName`, `Comment`, `IsPull`, `IsMention`, `ReviewComments`, `CanReply` | | `repo/issue/default` | `Doer`, `Issue`, `Link`, `Body`, `ActionName`, `Comment`, `IsPull`, `IsMention`, `ReviewComments`, `CanReply` |
> ⚠️ **`.DisplayName`** is not available in collaborator, transfer, release, workflow_run, assigned, and default templates — do not reference it. > [WARN] **`.DisplayName`** is not available in collaborator, transfer, release, workflow_run, assigned, and default templates — do not reference it.
### Translation Keys ### Translation Keys
@@ -108,17 +110,18 @@ All templates use Gitea's official `mail.*` translation namespace. Every referen
## How Compatibility Is Verified ## How Compatibility Is Verified
1. **Automated lint** — CI renders all templates via `go run . preview all` on every push; the preview function map mirrors Gitea 28.0.0 1. **Local validation** — run `go test ./...` and `go run . preview all` from `tools/`; the preview function map mirrors Gitea 28.0.0
2. **Source audit** — Template data contexts are cross-referenced against Gitea's `services/mailer/` package 2. **Source audit** — Template data contexts are cross-referenced against Gitea's `services/mailer/` package
3. **Regression tests** — Go tests render push notifications and release attachments in all 10 themes 3. **Regression tests** — Go tests render push notifications and release attachments in all 10 themes
4. **Release checklist** — Each release confirms the max-tested Gitea version in this file 4. **Release checklist** — Each release confirms the max-tested Gitea version in this file
## Automated Tracking ## Version Tracking
A [workflow](.github/workflows/gitea-tracker.yml) runs daily (UTC 08:00) to detect new Gitea releases. When a new version is found, it automatically creates a PR updating the matrix and badges with a **⏳ Pending Verification** status. Manual trigger is also available via `workflow_dispatch`. Once verification passes, update the compatibility matrix; publish a new template tag when template changes require a release (see [Versioning](#versioning)). The [tracker workflow](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/src/branch/main/.github/workflows/gitea-tracker.yml) scans root-level and `docs/` Markdown for `TRACKER:` markers. `VERSION-MAP` and `HISTORY` add pending rows; `UPSTREAM` updates the latest upstream version and status in English/Chinese README and AGENTS; `BADGE` shows the new version as pending while retaining the last tested version. `LATEST-TESTED` and `LATEST-VERIFIED` are manual-only markers and must change only after compatibility verification. The script rejects unknown or missing required markers and is idempotent. Its GitHub Actions schedule and PR creation have not been verified on this Gitea host; until then, check [upstream Gitea releases](https://github.com/go-gitea/gitea/releases) manually. Publish a new template tag only when template content changes (see [Versioning](#versioning)).
## Reporting Issues ## Reporting Issues
If you find a compatibility problem with a specific Gitea version: If you find a compatibility problem with a specific Gitea version:
1. Check the [Gitea changelog](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md) for recent mail template changes 1. Check the [Gitea changelog](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md) for recent mail template changes
2. Open an issue with: your Gitea version, which template, and the error 2. Open an issue with: your Gitea version, which template, and the error
+2 -2
View File
@@ -40,7 +40,7 @@ Documentation updates, preview screenshots, installation guides, and translation
## Development Setup ## Development Setup
- **Go 1.21+** for template rendering and the CLI tool - **Go 1.21+** for template rendering and the CLI tool; Go modules download `github.com/urfave/cli/v2` and its dependencies
### Previewing Locally (Static) ### Previewing Locally (Static)
@@ -56,7 +56,7 @@ cd tools && go run . dev
``` ```
- Watches `themes/**/*.tmpl` — auto-rebuilds on save - Watches `themes/**/*.tmpl` — auto-rebuilds on save
- Pure Go HTTP server with SSE push — no external dependencies - HTTP server and SSE live reload use Go's standard library; the CLI has a Go module dependency
- Re-renders templates in-process and pushes reload events to the browser - Re-renders templates in-process and pushes reload events to the browser
- Terminal output: `themes/aurora/mail/repo/release.tmpl changed` → `[Builder] Rebuild done in 45ms` - Terminal output: `themes/aurora/mail/repo/release.tmpl changed` → `[Builder] Rebuild done in 45ms`
+21 -17
View File
@@ -1,10 +1,10 @@
# Gitea Mail Templates # Gitea Mail Templates
A curated collection of professionally designed, audience-driven email templates for self-hosted [Gitea](https://about.gitea.com) instances. Polished, drop-in email template themes for self-hosted [Gitea](https://about.gitea.com).
[![Gitea](https://img.shields.io/badge/Gitea-28.0.0%20%7C%2028.0.0%20tested-blue)](COMPATIBILITY.md) <!-- TRACKER:BADGE --> [![Gitea](https://img.shields.io/badge/Gitea-28.1.0%20%5BPENDING%5D%20%7C%2028.0.0%20tested-yellow)](COMPATIBILITY.md) <!-- TRACKER:BADGE -->
> **Release v28.0.0 — verified with Gitea 28.0.0 — 110 template files, 10 visual styles, 11 email types each.** > Latest Release: [v28.0.0](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/tag/v28.0.0)
--- ---
@@ -31,9 +31,9 @@ The templates on `main` can replace Gitea's built-in mail templates without patc
| ![Ink](docs/images/ink.png) | **Ink** | Publishing / News / Literature | Editorial print, navy & gold, newspaper layout, drop caps | | ![Ink](docs/images/ink.png) | **Ink** | Publishing / News / Literature | Editorial print, navy & gold, newspaper layout, drop caps |
| ![Aurora](docs/images/aurora.png) | **Aurora** | Premium SaaS / Mindfulness | Ethereal light gradients, deep purple & teal, atmospheric glow | | ![Aurora](docs/images/aurora.png) | **Aurora** | Premium SaaS / Mindfulness | Ethereal light gradients, deep purple & teal, atmospheric glow |
> Images are screenshots from the [live preview](preview/index.html). See [docs/images/README.md](docs/images/README.md) for capture instructions. > Images are screenshots from the [local preview](preview/index.html). See [docs/images/README.md](docs/images/README.md) for capture instructions.
[**Live preview gallery**](preview/index.html) — open in a browser for an interactive style switcher with desktop/mobile viewports and view mode (Modern, Source). [**Local preview gallery**](preview/index.html) — generate the preview data as described below, then open it in a browser for an interactive style switcher with desktop/mobile viewports and view mode (Modern, Source).
--- ---
@@ -82,15 +82,17 @@ reset email — it will render with your custom styles.
## Preview ## Preview
Two modes are available — a zero-dependency static preview for quick checks, and a live-reload dev server for design work. Two modes are available — a static preview that needs no server after generation, and a live-reload dev server for design work. A source clone must generate preview data first. Existing v28.0.0 and earlier archives do not contain the preview or gallery screenshots; the updated packaging workflow includes both for future builds.
### Static Preview ### Static Preview
Generate the preview data once (Go only), then open in a browser: From a source clone, generate the preview data once, then open the HTML file in a browser:
```bash ```bash
cd tools && go run . preview all cd tools
open preview/index.html # no server needed go run . preview all
cd ..
# Open preview/index.html in a browser; no server is needed.
``` ```
### Dev Server (Live Reload) ### Dev Server (Live Reload)
@@ -98,15 +100,16 @@ open preview/index.html # no server needed
Start a pure Go development server that watches for `.tmpl` changes, auto-rebuilds, and pushes live updates to the browser via SSE: Start a pure Go development server that watches for `.tmpl` changes, auto-rebuilds, and pushes live updates to the browser via SSE:
```bash ```bash
cd tools && go run . dev cd tools
open http://localhost:3456 go run . dev
# Open http://localhost:3456 in a browser.
``` ```
| Capability | Static | Dev | | Capability | Static | Dev |
|-----------|--------|-----| |-----------|--------|-----|
| Go template rendering | ✅ | ✅ | | Go template rendering | [YES] | [YES] |
| Theme/template switching | ✅ | ✅ | | Theme/template switching | [YES] | [YES] |
| Live reload on save | — | ✅ | | Live reload on save | [NO] | [YES] |
### Features ### Features
@@ -127,7 +130,7 @@ gitea-mail-templates/
│ ├── ... # Custom styles are added here as separate directories │ ├── ... # Custom styles are added here as separate directories
├── preview/ # Live preview SPA ├── preview/ # Live preview SPA
│ ├── index.html # Style/template/client/viewport switcher │ ├── index.html # Style/template/client/viewport switcher
│ └── rendered.js # Pre-rendered templates (generated by tools/) │ └── rendered.js # Generated by tools/; ignored in source clones
├── tools/ # Modular CLI tooling ├── tools/ # Modular CLI tooling
│ ├── tools.go # Main entry point │ ├── tools.go # Main entry point
│ ├── cli/ # CLI subcommands (list, create, delete, preview) │ ├── cli/ # CLI subcommands (list, create, delete, preview)
@@ -165,7 +168,8 @@ gitea-mail-templates/
- **Gitea 28.0.0** — use template release v28.0.0; earlier Gitea versions require an older package - **Gitea 28.0.0** — use template release v28.0.0; earlier Gitea versions require an older package
- **Latest tested:** Gitea 28.0.0 <!-- TRACKER:LATEST-TESTED --> - **Latest tested:** Gitea 28.0.0 <!-- TRACKER:LATEST-TESTED -->
- **Latest release:** [v28.0.0](https://github.com/KenanZhu/GiteaMailTemplates/releases/tag/v28.0.0) updates all 10 release-notification templates for Gitea's `FormatByteSize` function. Use [v1.27.3](https://github.com/KenanZhu/GiteaMailTemplates/releases/tag/v1.27.3) for Gitea 1.25.0–1.27.3 (see [COMPATIBILITY.md](COMPATIBILITY.md)) - **Latest release:** [v28.0.0](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/tag/v28.0.0) updates all 10 release-notification templates for Gitea's `FormatByteSize` function. Use [v1.27.3](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/tag/v1.27.3) for Gitea 1.25.0–1.27.3 (see [COMPATIBILITY.md](COMPATIBILITY.md))
- **Upstream Gitea 28.1.0:** [PENDING] <!-- TRACKER:UPSTREAM -->
- The current source uses Gitea's official template data paths — see [COMPATIBILITY.md](COMPATIBILITY.md) for release-specific limitations - The current source uses Gitea's official template data paths — see [COMPATIBILITY.md](COMPATIBILITY.md) for release-specific limitations
- Uses only built-in Gitea template functions and official translation keys - Uses only built-in Gitea template functions and official translation keys
- No custom template functions or locale patches required - No custom template functions or locale patches required
@@ -178,7 +182,7 @@ gitea-mail-templates/
2. **Accessible** — 4.5:1 contrast ratios; semantic HTML 2. **Accessible** — 4.5:1 contrast ratios; semantic HTML
3. **Graceful degradation** — Fallback link visible when buttons fail to render 3. **Graceful degradation** — Fallback link visible when buttons fail to render
4. **Logo support** — References `{{AppUrl}}assets/img/favicon.png` by default 4. **Logo support** — References `{{AppUrl}}assets/img/favicon.png` by default
5. **i18n-ready** — All user-facing strings use Gitea's `{{.locale.Tr}}` system 5. **Locale-aware** — Notification text uses Gitea's `{{.locale.Tr}}` system; some decorative labels remain theme-specific English text
--- ---
+7 -4
View File
@@ -34,14 +34,13 @@
## 开发环境 ## 开发环境
- **Go 1.21+** 用于模板渲染和 CLI 工具 - **Go 1.21+** 用于模板渲染和 CLI 工具;Go 模块需下载 `github.com/urfave/cli/v2` 及其依赖
### 本地预览(静态) ### 本地预览(静态)
1. 先生成预览数据:`cd tools && go run . preview all` 1. 先生成预览数据:`cd tools && go run . preview all`
2. 在浏览器中打开 `preview/index.html` — 无需服务器 2. 在浏览器中打开 `preview/index.html` — 无需服务器
### 开发服务器(实时重载) ### 开发服务器(实时重载)
```bash ```bash
@@ -49,8 +48,7 @@ cd tools && go run . dev
# → http://localhost:3456 # → http://localhost:3456
``` ```
修改 `.tmpl` 文件后自动重建并推送至浏览器。 修改 `.tmpl` 文件后自动重建并推送至浏览器。HTTP 服务与 SSE 实时重载使用 Go 标准库实现,CLI 本身依赖 Go 模块。
### 集成测试 ### 集成测试
@@ -75,6 +73,11 @@ cd tools && go run . dev
- `fix:` — Bug 修复 - `fix:` — Bug 修复
- `project:` — README、LICENSE、元文件 - `project:` — README、LICENSE、元文件
## 翻译
- [English CONTRIBUTING](../CONTRIBUTING.md)
- 简体中文(本文)
## 许可协议 ## 许可协议
参与贡献即表示您同意将您的贡献以 MIT 许可证授权。 参与贡献即表示您同意将您的贡献以 MIT 许可证授权。
+41 -13
View File
@@ -1,8 +1,8 @@
# Gitea 邮件模板 # Gitea 邮件模板
为自托管 [Gitea](https://about.gitea.com) 实例精心设计、面向不同受众的邮件模板集合。 为自托管 [Gitea](https://about.gitea.com) 提供精心设计、可直接部署的多风格邮件模板。
> **发布版 v28.0.0 — 已通过 Gitea 28.0.0 兼容性检查 — 110 个模板文件、10 种视觉风格、每种 11 种邮件类型** > 最新发布版:[v28.0.0](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/tag/v28.0.0)
--- ---
@@ -29,9 +29,9 @@
| ![Ink](images/ink.png) | **Ink** | 出版/新闻/文学 | 编辑印刷、深蓝与金色、报纸排版 | | ![Ink](images/ink.png) | **Ink** | 出版/新闻/文学 | 编辑印刷、深蓝与金色、报纸排版 |
| ![Aurora](images/aurora.png) | **Aurora** | 高端SaaS/正念 | 空灵光效渐变、深紫与青绿、大气光晕 | | ![Aurora](images/aurora.png) | **Aurora** | 高端SaaS/正念 | 空灵光效渐变、深紫与青绿、大气光晕 |
> 图片为 600px 宽截图,来自[在线预览](../preview/index.html)。截图方法参见 [images/README.md](images/README.md)。 > 图片为 600px 宽截图,来自[本地预览](../preview/index.html)。截图方法参见 [images/README.md](images/README.md)。
[**在线预览画廊**](../preview/index.html) [**本地预览画廊**](../preview/index.html) — 先按下文生成预览数据,再在浏览器中打开。
--- ---
@@ -46,6 +46,8 @@ systemctl restart gitea
切换风格只需覆盖文件,无需更改配置。 切换风格只需覆盖文件,无需更改配置。
`GITEA_CUSTOM` 决定自定义目录;常见路径为 Linux 二进制部署的 `/var/lib/gitea/custom`、Docker 的 `/data/gitea`,以及 Windows 的 `C:\gitea\custom`。
### 确认生效 ### 确认生效
管理后台的测试邮件不会使用自定义模板。要验证模板是否生效,请触发一次真实的 管理后台的测试邮件不会使用自定义模板。要验证模板是否生效,请触发一次真实的
@@ -56,33 +58,59 @@ systemctl restart gitea
## 预览 ## 预览
从源码克隆时,需先生成被忽略的 `preview/rendered.js`。现有 v28.0.0 及更早的压缩包未包含预览文件和画廊截图;更新后的打包流程会在后续构建中加入两者。
**静态模式:** **静态模式:**
```bash ```bash
cd tools && go run . preview all cd tools
go run . preview all
cd ..
``` ```
然后打开 `preview/index.html`。
然后在浏览器中打开 `preview/index.html`,无需启动服务器。
**开发服务器(实时重载):** **开发服务器(实时重载):**
```bash ```bash
cd tools && go run . dev cd tools
# → http://localhost:3456 go run . dev
# 在浏览器中打开 http://localhost:3456
``` ```
| 功能 | 静态 | Dev | | 功能 | 静态 | Dev |
|-----------|--------|-----| |-----------|--------|-----|
| Go 模板渲染 | ✅ | ✅ | | Go 模板渲染 | [YES] | [YES] |
| 主题/模板切换 | ✅ | ✅ | | 主题/模板切换 | [YES] | [YES] |
| 实时重载 | — | ✅ | | 实时重载 | [NO] | [YES] |
预览支持主题与模板切换、Modern/Source 视图、桌面/移动端尺寸以及模板参数面板。`←→` 可切换选择框,`↑↓` 可切换选项,`d`/`m` 可切换视口。
--- ---
## 兼容性 ## 兼容性
- **Gitea 28.0.0** — 使用模板发布版 v28.0.0;旧版 Gitea 请选用对应的旧版模板 - **Gitea 28.0.0** — 使用模板发布版 v28.0.0;旧版 Gitea 请选用对应的旧版模板
- **最新测试:** Gitea 28.0.0<!-- TRACKER:LATEST-TESTED --> - **最新测试:** Gitea 28.0.0 <!-- TRACKER:LATEST-TESTED -->
- **最新发布版:** [v28.0.0](https://github.com/KenanZhu/GiteaMailTemplates/releases/tag/v28.0.0) 已将全部 10 个主题的发布通知适配为 `FormatByteSize`;Gitea 1.25.0–1.27.3 请使用 [v1.27.3](https://github.com/KenanZhu/GiteaMailTemplates/releases/tag/v1.27.3)(详见[兼容性说明](../COMPATIBILITY.md)) - **最新发布版:** [v28.0.0](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/tag/v28.0.0) 已将全部 10 个主题的发布通知适配为 `FormatByteSize`;Gitea 1.25.0–1.27.3 请使用 [v1.27.3](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/tag/v1.27.3)(详见[兼容性说明](../COMPATIBILITY.md))
- **上游 Gitea 28.1.0:** [PENDING] <!-- TRACKER:UPSTREAM -->
- 当前源码使用 Gitea 官方模板的数据路径;各发布版的限制详见 [兼容性说明](../COMPATIBILITY.md) - 当前源码使用 Gitea 官方模板的数据路径;各发布版的限制详见 [兼容性说明](../COMPATIBILITY.md)
## 模板类型
每种风格都提供 11 种邮件类型:账户激活、邮箱验证、注册通知、密码重置、团队邀请、仓库协作者、仓库转移、新版发布、Actions 工作流、议题/合并请求指派及议题/合并请求更新。具体文件路径参见[英文 README](../README.md#template-types)。
## 设计与翻译
模板采用适合邮件客户端的响应式布局,并提供按钮失效时可用的备用链接。通知正文使用 Gitea 的翻译系统;部分主题装饰性标签仍为英文,并非所有可见文字都已本地化。
## 文档与贡献
- [English README](../README.md)
- [简体中文 README](README.zh-CN.md)
- [English CONTRIBUTING](../CONTRIBUTING.md)
- [简体中文贡献指南](CONTRIBUTING.zh-CN.md)
## 许可证 ## 许可证
MIT — 详见 [LICENSE](../LICENSE)。 MIT — 详见 [LICENSE](../LICENSE)。
+3 -3
View File
@@ -1,6 +1,6 @@
# Style Preview Images # Style Preview Images
Place theme style screenshots of **Desktop** in PNG format here, captured from the [live preview](../preview/index.html). Place theme style screenshots of **Desktop** in PNG format here, captured from the [local preview](../../preview/index.html).
## Naming Convention ## Naming Convention
@@ -14,7 +14,7 @@ neon.png mono.png terra.png ink.png aurora.png
To ensure screenshots can be displayed in the README, please follow these size requirements: To ensure screenshots can be displayed in the README, please follow these size requirements:
- **Maximum:** 50 KiB per image - **Maximum:** 50 KiB per image
- **Recommended:** 30–40 KiB - **Recommended:** 10–20 KiB
- **Format:** PNG, optimised — run through `pngquant` or `optipng` before committing - **Format:** PNG, optimised — run through `pngquant` or `optipng` before committing
## How to Capture ## How to Capture
@@ -24,4 +24,4 @@ To ensure screenshots can be displayed in the README, please follow these size r
3. Take a screenshot of the rendered email (600px width recommended) 3. Take a screenshot of the rendered email (600px width recommended)
4. Save as `<style-name>.png` in this directory 4. Save as `<style-name>.png` in this directory
> For static preview, run `cd tools && go run . preview all` then open `preview/index.html` directly. > For a source clone, run `cd tools && go run . preview all`, return to the repository root, then open `preview/index.html` directly.
@@ -1,92 +0,0 @@
# Spec: Remove Juice & Simplify Preview Module
**Date:** 2026-06-23
**Status:** approved
## Goal
Remove multi-email-client simulation (Gmail/Outlook) from the preview module, eliminate the `juice` CSS-inlining dependency, and rewrite the dev server in pure Go with SSE-based live reload.
## Motivation
- Multi-client CSS simulation has diminishing value — modern email clients render consistently
- `juice` + Node.js adds complexity (npm install, node_modules, separate runtime)
- The project already uses Go for all build tooling; a Node.js dev server is an outlier
- Simplifying to "Modern" and "Source" views covers the real use cases
## Changes
### 1. Delete `tools/server/` (entire directory)
Remove all Node.js artifacts:
- `inliner.mjs` — juice CSS inlining + Gmail/Outlook CSS stripping
- `server.mjs` — Express + WebSocket dev server
- `package.json` / `package-lock.json`
- `node_modules/` — all npm dependencies
### 2. Rewrite `tools/cli/dev.go`
- Remove Node.js dependency check
- Remove `exec.Command("node", "server.mjs")` subprocess
- Instead: create and start a pure Go HTTP server (calling into `tools/preview/server.go`)
- Keep the same CLI interface: `go run . dev [--port <port>]`
### 3. New file: `tools/preview/server.go`
Pure Go dev server with:
- **Static file serving** — serve `preview/` directory (index.html, rendered.js)
- **SSE endpoint** (`GET /events`) — Server-Sent Events for browser reload notifications
- **File watcher** — poll `themes/` every 500ms, compare mod times of `.tmpl` files
- **On change detected** — call `preview.RenderAll()` directly (in-process, no subprocess), write `rendered.js`, broadcast SSE `reload` event
- No external Go dependencies — uses `net/http` stdlib only
### 4. Modify `preview/index.html`
Frontend changes:
- **Client selector** — reduce from 4 options (Modern/Gmail/Outlook/Raw Source) to 2 (Modern/Source)
- **Remove** `renderedGmail` / `renderedOutlook` variables and all Gmail/Outlook JS logic
- **Simplify** `getRendered()` — always returns `rendered`
- **Simplify** `transform()` — only handles `source` mode (HTML escaping)
- **Remove** Client Simulation indicators panel (HTML section + JS in `updatePanel()`)
- **Remove** static-mode warning message ("Gmail / Outlook simulation is approximate...")
- **Replace** WebSocket with SSE (`new EventSource('/events')`)
- **SSE reload handler** — dynamically reload `rendered.js` on `reload` event, re-render iframe without losing current theme/template/viewport selection
- **Simplify** `setDevMode()` — remove multi-client disclaimer
### 5. Update `AGENTS.md`
- Remove references to Juice, Node.js, Gmail/Outlook variants
- Update dev server description to reflect pure Go implementation
## Non-Changes
- `tools/preview/engine.go` — template rendering engine unchanged
- `tools/preview/funcs.go` — template functions unchanged
- `tools/preview/locale.go` — locale data unchanged
- `tools/config/` — config loading unchanged
- `tools/data/templates_config.json` — unchanged
- `tools/cli/preview.go` — preview command unchanged
- `tools/cli/commands.go` — command registration unchanged
- All theme `.tmpl` files — unchanged
## Behavior
### Static preview (open `preview/index.html` directly)
- Works via `file://` protocol as before
- Two view modes: Modern (rendered HTML in iframe) and Source (escaped HTML source)
- No dev-mode warnings needed
### Dev mode (`go run . dev`)
- `go run . dev` starts the Go HTTP server on port 3456 (configurable)
- On startup: runs preview engine once, writes `rendered.js`
- Serves `preview/` as static files
- Watches `themes/` for `.tmpl` changes (500ms polling)
- On change: re-renders affected themes, updates `rendered.js`, pushes SSE event
- Browser auto-reloads preview content without page refresh (preserves UI state)
## Constraints
- No new Go dependencies — SSE and file polling use stdlib only
- No Node.js requirement
- `preview/rendered.js` format stays compatible: `window.__RENDERED__`, `window.__REGISTRY__`, `window.__PARAMS__`
- Preview still works with `file://` protocol (no server needed for basic use)