9 Commits
Author SHA1 Message Date
KenanZhu f4d96de79e chore: refresh docs and adapt workflows for Gitea
Release / Validate Templates (push) Successful in 3m1s
Release / Package & Release (push) Skipped
Release / Update Latest Release Documentation (push) Skipped
2026-10-09 22:02:33 +08:00
KenanZhu fec3ace600 chore: migrate mail themes to shared framework and locked upstream inputs
Release / Validate Templates (push) Canceled after 0s
Release / Package & Release (push) Canceled after 0s
Release / Update Latest Release Documentation (push) Canceled after 0s
2026-10-09 18:34:36 +08:00
KenanZhu 5c0589f6f6 docs: clarify per-version Gitea compatibility
Release / Validate Templates (push) Canceled after 0s
Release / Package & Release (push) Canceled after 0s
Release / Update Latest Release Documentation (push) Canceled after 0s
2026-10-09 12:27:00 +08:00
KenanZhu 14dc37046d refactor: unify markdown release tracking blocks
Release / Validate Templates (push) Canceled after 0s
Release / Package & Release (push) Canceled after 0s
Release / Update Latest Release Documentation (push) Canceled after 0s
2026-10-09 12:10:12 +08:00
KenanZhu 23f0456622 chore: streamline compatibility tracking and documentation
Release / Validate Templates (push) Canceled after 0s
Release / Package & Release (push) Canceled after 0s
2026-10-09 11:43:02 +08:00
KenanZhu 92d2ba9889 fix: support Gitea 28.0.0 mail templates
Release / Validate Templates (push) Canceled after 0s
Release / Package & Release (push) Canceled after 0s
2026-10-02 19:44:20 +08:00
KenanZhu ffb674669b chore: prepare v1.27.3 release and guard template publishing
Release / Validate Templates (push) Canceled after 0s
Release / Package & Release (push) Canceled after 0s
2026-09-23 14:15:47 +08:00
KenanZhu 4ff008e564 docs: clarify Gitea 1.27.3 fix is on main, not yet released 2026-09-23 14:02:59 +08:00
KenanZhu dfb5024dfb fix: verify Gitea 1.27.3 and repair push mail templates 2026-09-23 13:51:22 +08:00
232 changed files with 6245 additions and 16067 deletions

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
* text=auto
# Generated lock and framework sources use stable line endings.
gitea.lock.json text eol=lf
framework/** text eol=lf
themes/** text eol=lf
tools/** text eol=lf
preview/** text eol=lf
.github/** text eol=lf
+16
View File
@@ -0,0 +1,16 @@
# v1.27.2
> **Correction (2026-09-23):** The push-notification fix in this release is incomplete. Bloom, Ember and Heritage still use the old commit ID path and can fail to render push notifications on Gitea 1.27.1+. Use v1.27.3 for the complete fix; see the [compatibility matrix](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/src/branch/main/COMPATIBILITY.md#compatibility-matrix).
## Changes
- Add the `email-text` class to Aurora release paragraphs for consistent typography.
- Update push commit paths in seven themes for Gitea 1.27.1 compatibility (`.ID` → `.UserCommit.GitCommit.ID`). The three remaining themes are covered by the correction above.
- Replace `rgba()` colors with `#RRGGBBAA` notation across all ten themes.
## Documentation
- Record compatibility for Gitea 1.27.0, 1.27.1 and 1.27.2.
- Maintain English and Simplified Chinese README and contributor guides.
[Full changelog: v1.0.1…v1.27.2](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/compare/v1.0.1...v1.27.2)
+20
View File
@@ -0,0 +1,20 @@
# v1.27.3
This release completes the push-notification fix for Gitea 1.27.1–1.27.3. See the [compatibility matrix](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/src/branch/main/COMPATIBILITY.md#compatibility-matrix) for recommended release combinations.
## Fixes
- Fix pull request push-notification links and abbreviated hashes in Bloom, Ember and Heritage by reading `.UserCommit.GitCommit.ID`.
- Add a push-notification regression test covering all ten themes and run Go tests before packaging.
## Packaging
- Include English and Simplified Chinese README and contributor guides in release archives.
- Use reviewed release notes in the publishing workflow.
## Documentation
- Record compatibility with Gitea 1.27.3, whose mail templates, mailer and locale files are unchanged from 1.27.2.
- Distinguish the affected v1.27.2 archive from the complete fix in this release.
[Full changelog: v1.27.2…v1.27.3](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/compare/v1.27.2...v1.27.3)
+18
View File
@@ -0,0 +1,18 @@
# v28.0.0
This release targets Gitea 28.0.0 and includes ten themes with eleven mail types each. For earlier Gitea versions, use the packages listed in the [compatibility matrix](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/src/branch/main/COMPATIBILITY.md#compatibility-matrix).
## Fixes
- Replace the removed `FileSize` function with `FormatByteSize` in release-attachment emails across all ten themes.
- Update the preview's file-size function for Gitea 28.0.0 and include release attachments in the example data.
- Add an all-theme regression test for release attachments and the removed function.
## Documentation
- Update both READMEs and the compatibility matrix for Gitea 28.0.0.
- Document the Gitea 28.0.0 requirement and direct users of earlier versions to the compatibility matrix.
The snapshot-based source architecture now on `main` is separate, unreleased work. It is not included in the existing v28.0.0 archives.
[Full changelog: v1.27.3…v28.0.0](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/compare/v1.27.3...v28.0.0)
+152
View File
@@ -0,0 +1,152 @@
"""Gitea API operations for the tracker and release workflows (Python stdlib only)."""
import argparse
import json
import os
from pathlib import Path
import re
import subprocess
from urllib.error import HTTPError
from urllib.parse import quote, urlencode, urlsplit
from urllib.request import HTTPRedirectHandler, Request, build_opener
class NoRedirects(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
# Never forward an instance token to a redirect destination.
return None
class GiteaAPI:
def __init__(self, server, repository, token):
parsed = urlsplit(server)
if parsed.scheme != "https" or not parsed.netloc or parsed.username or parsed.password or parsed.query or parsed.fragment:
raise ValueError("GITEA_SERVER_URL must be an HTTPS instance URL")
if not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", repository):
raise ValueError("GITEA_REPOSITORY must be owner/repository")
if not token:
raise ValueError("GITEA_TOKEN is required")
self.base = server.rstrip("/") + "/api/v1/repos/" + repository
self.token = token
self.opener = build_opener(NoRedirects())
def request(self, method, path, data=None, content_type="application/json", allow_missing=False):
if data is not None and not isinstance(data, bytes):
data = json.dumps(data).encode("utf-8")
req = Request(self.base + path, data=data, method=method, headers={
"Authorization": "token " + self.token,
"Accept": "application/json",
"Content-Type": content_type,
"User-Agent": "GiteaMailTemplates-actions",
})
try:
with self.opener.open(req, timeout=120) as response:
return json.load(response)
except HTTPError as error:
status = error.code
error.close()
if allow_missing and status == 404:
return None
raise RuntimeError(f"Gitea API {method} {path}: HTTP {status}") from None
def git(*args):
return subprocess.check_output(["git", *args], text=True).strip()
def create_pull_request(api, version):
if not re.fullmatch(r"\d+\.\d+\.\d+", version):
raise ValueError("Expected a stable X.Y.Z upstream version")
branch = "track/gitea-" + version
title = f"Track Gitea {version} compatibility"
changed = git("diff", "--name-only", "HEAD", "--").splitlines()
if any(not name.endswith(".md") for name in changed):
raise ValueError("Tracker may commit only Markdown changes")
if not changed:
print("[PASS] No tracked documentation changes")
return
# Repeated scheduled runs can find a branch from an earlier pending PR.
# Reuse identical content, but never force-push over a changed branch.
remote = git("ls-remote", "--heads", "origin", "refs/heads/" + branch)
if remote:
git("fetch", "origin", "refs/heads/" + branch)
if git("diff", "--name-only", "FETCH_HEAD", "--"):
raise ValueError(f"Existing {branch} differs; review it before updating the tracking PR")
else:
git("switch", "-c", branch)
git("add", "--", *changed)
git("-c", "user.name=release-bot", "-c", "user.email=release-bot@users.noreply.local",
"commit", "-m", f"docs: track Gitea {version} pending verification")
git("push", "origin", "HEAD:refs/heads/" + branch)
page = 1
while True:
pulls = api.request("GET", f"/pulls?state=open&base_branch=main&limit=50&page={page}")
for pull in pulls:
if (pull["head"]["ref"] == branch and pull["base"]["ref"] == "main"
and pull["head"]["repo"]["full_name"] == pull["base"]["repo"]["full_name"]):
print(f"[PASS] Tracking PR already exists: {pull['html_url']}")
return
if not pulls:
break
page += 1
body = f"""Gitea **{version}** is recorded as [PENDING]. Verified/tested versions remain unchanged.
- [ ] Review upstream mail templates, mailer data, functions and translation keys.
- [ ] Run `go test ./...` and `go run . preview all` from `tools/`.
- [ ] Record results in `COMPATIBILITY.md`; update verified status only after testing.
- [ ] Release template changes separately with reviewed notes and assets.
"""
pull = api.request("POST", "/pulls", {"title": title, "head": branch, "base": "main", "body": body})
print(f"[PASS] Created tracking PR: {pull['html_url']}")
def publish_release(api, version, root=Path(".")):
if not re.fullmatch(r"v\d+\.\d+\.\d+", version):
raise ValueError("Expected a stable vX.Y.Z release tag")
root = Path(root)
lock = json.loads((root / "gitea.lock.json").read_text(encoding="utf-8"))
if lock["tag"] != version:
raise ValueError("Release tag must match gitea.lock.json")
notes = (root / ".github" / "release-notes" / (version + ".md")).read_text(encoding="utf-8")
if not notes.strip():
raise ValueError("Release notes are empty")
assets = [root / "dist" / ("gitea-mail-templates-" + version + ext) for ext in (".zip", ".tar.gz")]
for asset in assets:
if asset.is_symlink() or not asset.is_file() or not asset.stat().st_size:
raise ValueError(f"Missing or invalid release archive: {asset}")
existing = api.request("GET", "/releases/tags/" + quote(version, safe=""), allow_missing=True)
if existing is not None:
raise ValueError("Release already exists; refusing to replace its notes or assets")
release = api.request("POST", "/releases", {
"tag_name": version, "name": version, "body": notes, "draft": True, "prerelease": False,
})
release_id = int(release["id"])
# Gitea accepts raw attachment data with the filename in the query string.
# Publish only after both uploads succeed; failures leave a draft for review.
for asset in assets:
api.request("POST", f"/releases/{release_id}/assets?" + urlencode({"name": asset.name}),
asset.read_bytes(), content_type="application/octet-stream")
release = api.request("PATCH", f"/releases/{release_id}", {"draft": False})
print(f"[PASS] Published {release['html_url']}")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("operation", choices=("create-pull-request", "publish-release"))
parser.add_argument("--version", required=True)
args = parser.parse_args()
try:
api = GiteaAPI(os.environ.get("GITEA_SERVER_URL", ""), os.environ.get("GITEA_REPOSITORY", ""), os.environ.get("GITEA_TOKEN", ""))
if args.operation == "create-pull-request":
create_pull_request(api, args.version)
else:
publish_release(api, args.version)
except (ValueError, KeyError, OSError, RuntimeError, subprocess.CalledProcessError) as error:
parser.exit(1, f"[FAIL] {error}\n")
if __name__ == "__main__":
main()
+130
View File
@@ -0,0 +1,130 @@
"""Package verified generated mail overrides; never copy stale build directories."""
import argparse
import hashlib
import io
import json
from pathlib import Path, PurePosixPath
import re
import tarfile
import zipfile
def read_file(path):
path = Path(path)
if path.is_symlink() or not path.is_file():
raise ValueError(f"Expected regular file: {path}")
return path.read_bytes()
def checked_path(base, name):
parts = PurePosixPath(name)
if parts.is_absolute() or ".." in parts.parts or "\\" in name or ":" in name:
raise ValueError(f"Unsafe package path: {name}")
path = base.joinpath(*parts.parts)
if not path.resolve().is_relative_to(base.resolve()):
raise ValueError(f"Path escapes package root: {name}")
return path
def digest(data):
return hashlib.sha256(data).hexdigest()
def collect(root, version):
root = Path(root)
if not re.fullmatch(r"v[0-9]+\.[0-9]+\.[0-9]+", version):
raise ValueError("Expected a stable vX.Y.Z package version")
lock = json.loads(read_file(root / "gitea.lock.json"))
if version != lock["tag"]:
raise ValueError("Package version must match the locked Gitea tag")
files = {}
names = sorted(p.name for p in (root / "themes").iterdir() if p.is_dir() and not p.is_symlink())
if not names:
raise ValueError("No source theme manifests")
for name in names:
if not re.fullmatch(r"[a-z][a-z0-9-]*", name):
raise ValueError(f"Invalid theme name: {name}")
source = root / "themes" / name
theme = json.loads(read_file(source / "theme.json"))
if theme["name"] != name or theme["mode"] not in ("shared", "framed"):
raise ValueError(f"Invalid theme metadata: {name}")
built = root / "build" / "themes" / name
meta = json.loads(read_file(built / "build.json"))
if (meta["theme"], meta["mode"], meta["gitea_tag"], meta["gitea_commit"]) != (name, theme["mode"], version, lock["commit"]):
raise ValueError(f"Stale build identity: {name}")
if not {"theme.json", "theme.css"}.issubset(meta.get("sources") or {}) or not meta.get("files"):
raise ValueError(f"Missing source/file provenance: {name}")
expected_files = {"mail/base/head.tmpl", "mail/base/footer.tmpl"}
if theme["mode"] == "framed":
expected_files.update(path.removeprefix("templates/") for path in lock["files"] if path.startswith("templates/mail/") and path.endswith(".tmpl"))
expected_files.update(path.relative_to(root / "framework").as_posix() for path in (root / "framework" / "mail").rglob("*.tmpl"))
if set(meta["files"]) != expected_files:
raise ValueError(f"Incomplete generated install package: {name}")
for path, expected in meta["sources"].items():
base = root if path.startswith(("framework/", "tools/")) else source
if digest(read_file(checked_path(base, path))) != expected:
raise ValueError(f"Source changed since build: {name}/{path}")
for path, expected in meta["files"].items():
if not path.startswith("mail/") or not path.endswith(".tmpl"):
raise ValueError(f"Unexpected generated file: {path}")
data = read_file(checked_path(built, path))
if digest(data) != expected:
raise ValueError(f"Generated checksum mismatch: {name}/{path}")
files[f"themes/{name}/{path}"] = data
files[f"themes/{name}/build.json"] = read_file(built / "build.json")
manifest_data = read_file(root / "preview" / "rendered.js")
prefix = b"window.__MAIL_PREVIEW__ = "
manifest = json.loads(manifest_data.split(prefix, 1)[1].strip().removesuffix(b";"))
languages = sorted(path.removeprefix("options/locale/locale_").removesuffix(".json") for path in lock["files"] if path.startswith("options/locale/locale_") and path.endswith(".json"))
if manifest["upstream"] != version or manifest.get("upstream_commit") != lock["commit"] or sorted(manifest["themes"]) != names or sorted(manifest["languages"]) != languages:
raise ValueError("Preview does not match lock/all source themes and languages; run preview all")
files["preview/rendered.js"] = manifest_data
for lang in languages:
data = read_file(root / "preview" / "rendered" / (lang + ".js"))
marker = ('window.__MAIL_PREVIEW_DATA__[' + json.dumps(lang) + '] = ').encode()
payload = json.loads(data.split(marker, 1)[1].strip().removesuffix(b";"))
if sorted(payload) != names or any(set(payload[name]) != set(manifest["registry"]) for name in names):
raise ValueError(f"Incomplete preview language bundle: {lang}")
files[f"preview/rendered/{lang}.js"] = data
for name in ["LICENSE", "README.md", "CONTRIBUTING.md", "COMPATIBILITY.md", "THIRD_PARTY_NOTICES.md", "docs/README.zh-CN.md", "docs/CONTRIBUTING.zh-CN.md", "preview/index.html"]:
files[name] = read_file(root / name)
for path in sorted((root / "docs" / "images").iterdir()):
if path.is_file() and (path.suffix == ".png" or path.name == "README.md"):
files["docs/images/" + path.name] = read_file(path)
license_data = read_file(root / "build" / "upstream" / "LICENSE")
if digest(license_data) != lock["files"]["LICENSE"]:
raise ValueError("Upstream license checksum mismatch")
files["GITEA-LICENSE"] = license_data
files["upstream-lock.json"] = read_file(root / "gitea.lock.json")
return files
def package(root, version, output):
files = collect(root, version)
archive = f"gitea-mail-templates-{version}"
output = Path(output)
targets = [output / (archive + ext) for ext in (".zip", ".tar.gz")]
if any(path.exists() for path in targets):
raise ValueError("Refusing to overwrite existing release archives; use a new output directory")
output.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(targets[0], "w", zipfile.ZIP_DEFLATED, compresslevel=9) as zipped, tarfile.open(targets[1], "w:gz") as tar:
for name, data in sorted(files.items()):
path = archive + "/" + name
zipped.writestr(path, data)
info = tarfile.TarInfo(path)
info.size, info.mode = len(data), 0o644
tar.addfile(info, io.BytesIO(data))
print(f"[PASS] Packaged {len(files)} verified files: {targets[0]}, {targets[1]}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--version", required=True)
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[2])
parser.add_argument("--output", type=Path, default=Path("dist"))
args = parser.parse_args()
try:
package(args.root, args.version, args.output)
except (ValueError, KeyError, IndexError, OSError) as error:
parser.exit(1, f"[FAIL] {error}\n")
+160
View File
@@ -0,0 +1,160 @@
"""Offline Gitea API and disposable-Git workflow regression tests."""
import importlib.util
import json
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
from unittest.mock import Mock
from urllib.error import HTTPError
SPEC = importlib.util.spec_from_file_location("gitea_actions", Path(__file__).with_name("gitea_actions.py"))
ACTIONS = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(ACTIONS)
class APITests(unittest.TestCase):
def test_instance_url_token_and_raw_asset_request(self):
api = ACTIONS.GiteaAPI("https://git.example/subpath/", "owner/repo", "test-token")
response = Mock()
response.__enter__ = Mock(return_value=response)
response.__exit__ = Mock(return_value=False)
response.read.return_value = b'{"id": 1}'
api.opener = Mock()
api.opener.open.return_value = response
self.assertEqual({"id": 1}, api.request("POST", "/releases/1/assets?name=test.zip", b"archive", "application/octet-stream"))
request = api.opener.open.call_args.args[0]
self.assertEqual("https://git.example/subpath/api/v1/repos/owner/repo/releases/1/assets?name=test.zip", request.full_url)
self.assertEqual("token test-token", request.get_header("Authorization"))
self.assertEqual(b"archive", request.data)
self.assertEqual("application/octet-stream", request.get_header("Content-type"))
def test_only_explicit_404_is_missing_and_redirects_are_refused(self):
api = ACTIONS.GiteaAPI("https://git.example", "owner/repo", "test-token")
api.opener = Mock()
for status in (401, 403, 500, 302):
api.opener.open.side_effect = HTTPError(api.base, status, "error", {}, None)
with self.assertRaisesRegex(RuntimeError, f"HTTP {status}"):
api.request("GET", "/releases/tags/v28.0.0", allow_missing=True)
api.opener.open.side_effect = HTTPError(api.base, 404, "missing", {}, None)
self.assertIsNone(api.request("GET", "/releases/tags/v28.0.0", allow_missing=True))
self.assertIsNone(ACTIONS.NoRedirects().redirect_request(None, None, 302, "", {}, "https://elsewhere.example"))
def test_rejects_invalid_configuration(self):
for server, repository, token in [("http://git.example", "owner/repo", "x"),
("https://user:password@git.example", "owner/repo", "x"),
("https://git.example", "../owner/repo", "x"), ("https://git.example", "owner/repo", "")]:
with self.assertRaises(ValueError):
ACTIONS.GiteaAPI(server, repository, token)
class ReleaseTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
(self.root / "gitea.lock.json").write_text(json.dumps({"tag": "v28.0.0"}))
notes = self.root / ".github/release-notes/v28.0.0.md"
notes.parent.mkdir(parents=True)
notes.write_text("Reviewed notes", encoding="utf-8")
(self.root / "dist").mkdir()
for ext in (".zip", ".tar.gz"):
(self.root / "dist" / ("gitea-mail-templates-v28.0.0" + ext)).write_bytes(b"archive")
def test_existing_release_is_not_modified(self):
api = Mock()
api.request.return_value = {"id": 5}
with self.assertRaisesRegex(ValueError, "already exists"):
ACTIONS.publish_release(api, "v28.0.0", self.root)
self.assertEqual(["GET"], [call.args[0] for call in api.request.call_args_list])
def test_publish_only_after_both_uploads_succeed(self):
api = Mock()
api.request.side_effect = [None, {"id": 5}, {"id": 6}, {"id": 7}, {"html_url": "https://git.example/release"}]
ACTIONS.publish_release(api, "v28.0.0", self.root)
calls = api.request.call_args_list
self.assertEqual(["GET", "POST", "POST", "POST", "PATCH"], [c.args[0] for c in calls])
self.assertTrue(calls[1].args[2]["draft"])
self.assertEqual("Reviewed notes", calls[1].args[2]["body"])
self.assertEqual("/releases/5/assets?name=gitea-mail-templates-v28.0.0.zip", calls[2].args[1])
self.assertEqual("/releases/5/assets?name=gitea-mail-templates-v28.0.0.tar.gz", calls[3].args[1])
self.assertEqual({"draft": False}, calls[4].args[2])
def test_failed_upload_leaves_draft_unpublished(self):
api = Mock()
api.request.side_effect = [None, {"id": 5}, RuntimeError("upload failed")]
with self.assertRaisesRegex(RuntimeError, "upload failed"):
ACTIONS.publish_release(api, "v28.0.0", self.root)
self.assertNotIn("PATCH", [c.args[0] for c in api.request.call_args_list])
def test_bad_version_or_missing_archive_fails_before_api_call(self):
api = Mock()
for version in ("v28.0.1", "v28.0.0-rc1", "../../x"):
with self.assertRaises(ValueError):
ACTIONS.publish_release(api, version, self.root)
(self.root / "dist/gitea-mail-templates-v28.0.0.zip").unlink()
with self.assertRaisesRegex(ValueError, "archive"):
ACTIONS.publish_release(api, "v28.0.0", self.root)
api.request.assert_not_called()
class PullRequestTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.previous = Path.cwd()
os.chdir(self.root)
self.addCleanup(os.chdir, self.previous)
subprocess.run(["git", "init", "--bare", "remote.git"], check=True, capture_output=True)
subprocess.run(["git", "init", "-b", "main", "work"], check=True, capture_output=True)
os.chdir(self.root / "work")
ACTIONS.git("config", "user.name", "test")
ACTIONS.git("config", "user.email", "test@example.invalid")
ACTIONS.git("remote", "add", "origin", str(self.root / "remote.git"))
Path("README.md").write_text("Baseline\n")
Path("source.txt").write_text("Source\n")
ACTIONS.git("add", ".")
ACTIONS.git("commit", "-m", "baseline")
ACTIONS.git("push", "origin", "main")
Path("README.md").write_text("Pending 28.1.0\n")
def test_create_then_reuse_branch_and_paginated_pr(self):
api = Mock()
api.request.side_effect = [[], {"html_url": "https://git.example/pulls/1"}]
ACTIONS.create_pull_request(api, "28.1.0")
self.assertEqual("track/gitea-28.1.0", ACTIONS.git("branch", "--show-current"))
payload = api.request.call_args.args[2]
self.assertEqual("main", payload["base"])
self.assertEqual("track/gitea-28.1.0", payload["head"])
self.assertIn("[PENDING]", payload["body"])
original_head = ACTIONS.git("rev-parse", "HEAD")
ACTIONS.git("switch", "main")
Path("README.md").write_text("Pending 28.1.0\n")
api.reset_mock()
api.request.side_effect = [[{"head": {"ref": "unrelated"}}], [{
"head": {"ref": "track/gitea-28.1.0", "repo": {"full_name": "owner/repo"}},
"base": {"ref": "main", "repo": {"full_name": "owner/repo"}},
"html_url": "https://git.example/pulls/1",
}]]
ACTIONS.create_pull_request(api, "28.1.0")
self.assertEqual(["GET", "GET"], [c.args[0] for c in api.request.call_args_list])
self.assertEqual(original_head, ACTIONS.git("rev-parse", "FETCH_HEAD"))
Path("README.md").write_text("Different pending content\n")
with self.assertRaisesRegex(ValueError, "Existing.*differs"):
ACTIONS.create_pull_request(api, "28.1.0")
def test_non_documentation_change_is_not_committed(self):
Path("source.txt").write_text("Changed source\n")
api = Mock()
with self.assertRaisesRegex(ValueError, "only Markdown"):
ACTIONS.create_pull_request(api, "28.1.0")
api.request.assert_not_called()
self.assertEqual("baseline", ACTIONS.git("log", "-1", "--format=%s"))
if __name__ == "__main__":
unittest.main()
+77
View File
@@ -0,0 +1,77 @@
"""Offline artifact/provenance tests independent of generated repository output."""
import importlib.util
import json
from pathlib import Path
import tempfile
import unittest
import zipfile
SPEC = importlib.util.spec_from_file_location("package_release", Path(__file__).with_name("package_release.py"))
PACKER = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(PACKER)
class PackagingTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.tag = "v28.0.0"
self.commit = "a" * 40
theme = json.dumps({"name": "demo", "mode": "shared"}).encode()
css = b"a { color:blue; }"
self.write("themes/demo/theme.json", theme)
self.write("themes/demo/theme.css", css)
files = {"mail/base/head.tmpl": b"<title>test</title>", "mail/base/footer.tmpl": b"<p>footer</p>"}
meta = {"theme": "demo", "mode": "shared", "gitea_tag": self.tag, "gitea_commit": self.commit, "sources": {"theme.json": PACKER.digest(theme), "theme.css": PACKER.digest(css)}, "files": {p: PACKER.digest(data) for p, data in files.items()}}
for path, data in files.items():
self.write("build/themes/demo/" + path, data)
self.write_json("build/themes/demo/build.json", meta)
self.write_json("gitea.lock.json", {"tag": self.tag, "commit": self.commit, "files": {"LICENSE": PACKER.digest(b"MIT"), "options/locale/locale_en-US.json": PACKER.digest(b"{}")}})
self.write("build/upstream/LICENSE", b"MIT")
manifest = {"upstream": self.tag, "upstream_commit": self.commit, "themes": ["demo"], "languages": ["en-US"], "registry": {"activate": {}}}
self.write("preview/rendered.js", ("window.__MAIL_PREVIEW__ = " + json.dumps(manifest) + ";").encode())
self.write("preview/rendered/en-US.js", ('window.__MAIL_PREVIEW_DATA__["en-US"] = ' + json.dumps({"demo": {"activate": "<html></html>"}}) + ";").encode())
for name in ["LICENSE", "README.md", "CONTRIBUTING.md", "COMPATIBILITY.md", "THIRD_PARTY_NOTICES.md", "docs/README.zh-CN.md", "docs/CONTRIBUTING.zh-CN.md", "preview/index.html", "docs/images/README.md"]:
self.write(name, b"test")
def write(self, name, data):
path = self.root / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(data)
def write_json(self, name, data):
self.write(name, json.dumps(data).encode())
def test_packages_declared_outputs_only_and_never_overwrites(self):
self.write("build/themes/stale/mail/stale.tmpl", b"stale")
out = self.root / "artifacts"
PACKER.package(self.root, self.tag, out)
with zipfile.ZipFile(next(out.glob("*.zip"))) as archive:
names = archive.namelist()
self.assertTrue(any(name.endswith("themes/demo/mail/base/head.tmpl") for name in names))
self.assertFalse(any("stale" in name or "/build/" in name for name in names))
with self.assertRaises(ValueError):
PACKER.package(self.root, self.tag, out)
def test_refuses_stale_source_tampered_output_and_wrong_version(self):
with self.assertRaises(ValueError):
PACKER.collect(self.root, "v28.1.0")
self.write("themes/demo/theme.css", b"changed")
with self.assertRaisesRegex(ValueError, "Source changed"):
PACKER.collect(self.root, self.tag)
def test_refuses_missing_language_bundle(self):
(self.root / "preview/rendered/en-US.js").unlink()
with self.assertRaises(ValueError):
PACKER.collect(self.root, self.tag)
def test_refuses_modified_generated_files(self):
self.write("build/themes/demo/mail/base/head.tmpl", b"modified")
with self.assertRaisesRegex(ValueError, "checksum mismatch"):
PACKER.collect(self.root, self.tag)
if __name__ == "__main__":
unittest.main()
+285
View File
@@ -0,0 +1,285 @@
"""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)
RELEASE_URL = "https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/latest"
FIXTURES = {
"COMPATIBILITY.md": """# Gitea Compatibility
<!-- DOC-TAGS: {"TRACKER":["LATEST-VERIFIED","VERSION-MAP","HISTORY"]} -->
<!-- TRACKER:LATEST-VERIFIED -->
> **Latest verified:** Release v28.0.0 passes tests.
<!-- /TRACKER:LATEST-VERIFIED -->
<!-- TRACKER:VERSION-MAP -->
| Gitea version | Recommended template release | Status | Notes |
|---|---|---|---|
| 28.1.0 | — | [PENDING] | Compatibility verification pending |
| 28.0.0 | **v28.0.0** | [PASS] | Tested |
<!-- /TRACKER:VERSION-MAP -->
<!-- TRACKER:HISTORY -->
| Gitea | Release Date | Mail Template Changes | Impact |
|---|---|---|---|
| **28.1.0** | 2026-10-06 | [PENDING] Review of mail-template changes | TBD |
| **28.0.0** | 2026-09-29 | FormatByteSize | Yes |
| **≤ 1.24.x** | — | Old directory | [UNSUPPORTED] |
<!-- /TRACKER:HISTORY -->
""",
"README.md": f"""# Gitea Mail Templates
<!-- DOC-TAGS: {{"TRACKER":["BADGE","LATEST-TESTED","UPSTREAM"],"RELEASE":["HEADER","SUMMARY"]}} -->
<!-- RELEASE:HEADER -->
> Latest Release: [v28.0.0]({RELEASE_URL})
<!-- /RELEASE:HEADER -->
<!-- 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 -->
<!-- TRACKER:LATEST-TESTED -->
- **Latest tested:** Gitea 28.0.0
<!-- /TRACKER:LATEST-TESTED -->
<!-- RELEASE:SUMMARY -->
- **Latest release:** [v28.0.0]({RELEASE_URL})
<!-- /RELEASE:SUMMARY -->
<!-- TRACKER:UPSTREAM -->
- **Upstream Gitea 28.1.0:** [PENDING]
<!-- /TRACKER:UPSTREAM -->
""",
"AGENTS.md": """# Agents
<!-- DOC-TAGS: {"TRACKER":["UPSTREAM"],"RELEASE":["CURRENT"]} -->
<!-- RELEASE:CURRENT -->
- Current template release: **v28.0.0**.
<!-- /RELEASE:CURRENT -->
<!-- TRACKER:UPSTREAM -->
- Latest upstream Gitea release: 28.1.0 [PENDING].
<!-- /TRACKER:UPSTREAM -->
""",
"docs/README.zh-CN.md": f"""# Gitea 邮件模板
<!-- DOC-TAGS: {{"TRACKER":["LATEST-TESTED","UPSTREAM"],"RELEASE":["HEADER","SUMMARY"]}} -->
<!-- RELEASE:HEADER -->
> 最新发布版:[v28.0.0]({RELEASE_URL})
<!-- /RELEASE:HEADER -->
<!-- TRACKER:LATEST-TESTED -->
- **最新测试:** Gitea 28.0.0
<!-- /TRACKER:LATEST-TESTED -->
<!-- RELEASE:SUMMARY -->
- **最新发布版:** [v28.0.0]({RELEASE_URL})
<!-- /RELEASE:SUMMARY -->
<!-- TRACKER:UPSTREAM -->
- **上游 Gitea 28.1.0:** [PENDING]
<!-- /TRACKER:UPSTREAM -->
""",
}
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 = FIXTURES[name]
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 pending |", compatibility)
self.assertIn("| **28.2.0** | 2026-11-01 | [PENDING]", compatibility)
self.assertIn("| 28.0.0 | **v28.0.0** | [PASS] | Tested |", 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_repository_markers_are_valid(self):
documents, blocks = TRACKER.documents(TRACKER.ROOT)
self.assertTrue({TRACKER.ROOT / name for name in self.paths}.issubset(documents))
self.assertIn(("TRACKER", "HISTORY"), {(block.subject, block.content) for block in blocks})
self.assertIn(("RELEASE", "HEADER"), {(block.subject, block.content) for block in blocks})
TRACKER.latest_tested(documents, blocks)
matrix = next(block for block in blocks if (block.subject, block.content) == ("TRACKER", "VERSION-MAP"))
history = next(block for block in blocks if (block.subject, block.content) == ("TRACKER", "HISTORY"))
self.assertEqual(
TRACKER.table(TRACKER.body(documents[matrix.path], matrix), "VERSION-MAP"),
TRACKER.table(TRACKER.body(documents[history.path], history), "HISTORY"),
)
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 pending |\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 pending |", 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 -->\n", ""), 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('<!-- DOC-TAGS: {"TRACKER":["FUTURE-UNKNOWN"]} -->\n', encoding="utf-8")
with self.assertRaisesRegex(ValueError, "Unknown TRACKER content"):
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 / "misc/notes/NEWS.md"
news.parent.mkdir(parents=True)
news.write_text('<!-- DOC-TAGS: {"TRACKER":["UPSTREAM"]} -->\n<!-- TRACKER:UPSTREAM -->\nGitea 28.1.0 [PENDING]\n<!-- /TRACKER:UPSTREAM -->\n', encoding="utf-8")
changed = TRACKER.apply_release(self.root, "28.2.0", "2026-11-01")
self.assertIn(news, changed)
self.assertIn("Gitea 28.2.0 [PENDING]", news.read_text(encoding="utf-8"))
def test_duplicate_block_uses_first_in_document_order(self):
english = self.root / "README.md"
duplicate = "<!-- TRACKER:UPSTREAM -->\nGitea 28.1.0 [PENDING]\n<!-- /TRACKER:UPSTREAM -->\n"
english.write_text(self.read("README.md") + duplicate, encoding="utf-8")
TRACKER.apply_release(self.root, "28.2.0", "2026-11-01")
self.assertIn("**Upstream Gitea 28.2.0:** [PENDING]", self.read("README.md"))
self.assertTrue(self.read("README.md").endswith(duplicate))
def test_unclosed_block_rejected_without_writing(self):
english = self.root / "README.md"
english.write_text(self.read("README.md").replace("<!-- /TRACKER:UPSTREAM -->", ""), encoding="utf-8")
with self.assertRaisesRegex(ValueError, "Unclosed tag block"):
TRACKER.apply_release(self.root, "28.2.0", "2026-11-01")
self.assertEqual(self.originals["COMPATIBILITY.md"], self.read("COMPATIBILITY.md"))
def test_undeclared_block_rejected(self):
english = self.root / "README.md"
english.write_text(self.read("README.md") + "<!-- RELEASE:CURRENT -->\nv28.0.0\n<!-- /RELEASE:CURRENT -->\n", encoding="utf-8")
with self.assertRaisesRegex(ValueError, "Undeclared RELEASE:CURRENT"):
TRACKER.apply_template_release(self.root, "v28.0.1")
def test_misspelled_subject_rejected(self):
english = self.root / "README.md"
english.write_text(self.read("README.md").replace("<!-- TRACKER:UPSTREAM -->", "<!-- TRACER:UPSTREAM -->"), encoding="utf-8")
with self.assertRaisesRegex(ValueError, "Undeclared TRACER:UPSTREAM"):
TRACKER.apply_release(self.root, "28.2.0", "2026-11-01")
def test_block_without_manifest_rejected(self):
news = self.root / "NEWS.md"
news.write_text("<!-- TRACKER:UPSTREAM -->\nGitea 28.1.0 [PENDING]\n<!-- /TRACKER:UPSTREAM -->\n", encoding="utf-8")
with self.assertRaisesRegex(ValueError, "without DOC-TAGS"):
TRACKER.apply_release(self.root, "28.2.0", "2026-11-01")
def test_mismatched_close_rejected(self):
english = self.root / "README.md"
english.write_text(self.read("README.md").replace("<!-- /RELEASE:SUMMARY -->", "<!-- /RELEASE:HEADER -->"), encoding="utf-8")
with self.assertRaisesRegex(ValueError, "Unmatched closing tag"):
TRACKER.apply_template_release(self.root, "v28.0.1")
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_template_release_updates_all_marked_labels(self):
changed = TRACKER.apply_template_release(self.root, "v28.0.1")
self.assertEqual({self.root / name for name in ("README.md", "docs/README.zh-CN.md", "AGENTS.md")}, set(changed))
for name in ("README.md", "docs/README.zh-CN.md"):
self.assertEqual(2, self.read(name).count("[v28.0.1](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/latest)"))
self.assertNotIn("/releases/tag/v28.0.1", self.read(name))
self.assertIn("Current template release: **v28.0.1**", self.read("AGENTS.md"))
self.assertEqual(self.originals["COMPATIBILITY.md"], self.read("COMPATIBILITY.md"))
self.assertEqual([], TRACKER.apply_template_release(self.root, "v28.0.1"))
self.assertEqual([], TRACKER.apply_template_release(self.root, "v28.0.0"))
def test_template_release_dry_run_does_not_write(self):
self.assertEqual(3, len(TRACKER.apply_template_release(self.root, "v28.0.1", dry_run=True)))
self.assert_unchanged()
def test_template_release_rejects_mismatched_labels(self):
english = self.root / "README.md"
english.write_text(self.read("README.md").replace("Latest Release: [v28.0.0]", "Latest Release: [v28.0.1]"), encoding="utf-8")
with self.assertRaisesRegex(ValueError, "disagree"):
TRACKER.apply_template_release(self.root, "v28.0.2")
def test_template_release_rejects_tagged_link(self):
english = self.root / "README.md"
english.write_text(self.read("README.md").replace("[v28.0.0](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/latest)", "[v28.0.0](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/tag/v28.0.0)", 1), encoding="utf-8")
with self.assertRaisesRegex(ValueError, "/releases/latest"):
TRACKER.apply_template_release(self.root, "v28.0.1")
def test_template_release_rejects_invalid_tag(self):
with self.assertRaises(ValueError):
TRACKER.apply_template_release(self.root, "v28.0.1-rc1")
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()
+311
View File
@@ -0,0 +1,311 @@
#!/usr/bin/env python3
"""Update marker-driven documentation for upstream and template releases."""
import argparse
import json
import os
import re
import sys
from datetime import date
from pathlib import Path
from typing import NamedTuple
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)\]")
MANIFEST_RE = re.compile(r"^<!-- DOC-TAGS: (\{.*\}) -->\s*$")
TAG_RE = re.compile(r"^<!-- (/?)([A-Z][A-Z0-9-]*):([A-Z][A-Z0-9-]*) -->\s*$")
BADGE_RE = re.compile(r"Gitea-(\d+\.\d+\.\d+)(?:%20(?:pending|%5BPENDING%5D))?%20%7C%20(\d+\.\d+\.\d+)%20tested-(?:blue|yellow)")
RELEASE_VERSION_RE = re.compile(r"\bv\d+\.\d+\.\d+\b")
RELEASE_LINK_RE = re.compile(r"\[v\d+\.\d+\.\d+\]\(https://[^)]+/releases/latest\)")
TABLES = {
"VERSION-MAP": "| Gitea version | Recommended template release | Status | Notes |",
"HISTORY": "| Gitea | Release Date | Mail Template Changes | Impact |",
}
SKIP_DIRECTORIES = frozenset((".git", "node_modules", "vendor", "dist"))
class TagBlock(NamedTuple):
path: Path
subject: str
content: str
start: int
end: int
def body(lines, block):
return lines[block.start + 1:block.end]
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, name):
if len(lines) < 3 or lines[0].strip() != TABLES[name]:
raise ValueError(f"Unexpected table header in TRACKER:{name}")
if not re.fullmatch(r"\|[\s|:-]+\|", lines[1].strip()):
raise ValueError(f"Missing Markdown table separator in TRACKER:{name}")
if any(not line.startswith("|") for line in lines[2:]):
raise ValueError(f"Non-table line in TRACKER:{name}")
versions = set()
for line in lines[2:]:
match = ROW_RE.match(line)
if not match:
if name == "HISTORY" and re.match(r"^\|\s*\*\*≤\s*\d+\.\d+\.x\*\*\s*\|", line):
continue
raise ValueError(f"Malformed version row in TRACKER:{name}: {line.strip()}")
if match.group(1) in versions:
raise ValueError(f"Duplicate version in TRACKER:{name}: {match.group(1)}")
versions.add(match.group(1))
if not versions:
raise ValueError(f"Empty version table in TRACKER:{name}")
return versions
def documents(root):
"""Scan every Markdown file; each participant declares its paired tag blocks."""
result = {}
blocks = []
for path in sorted(root.rglob("*.md")):
if any(part in SKIP_DIRECTORIES or part.startswith("tracker-test-") for part in path.relative_to(root).parts[:-1]):
continue
lines = path.read_text(encoding="utf-8").splitlines(keepends=True)
declarations = [MANIFEST_RE.match(line) for line in lines]
declarations = [match for match in declarations if match]
if len(declarations) > 1:
raise ValueError(f"Multiple DOC-TAGS declarations in {path}")
if not declarations:
if any(TAG_RE.match(line) for line in lines):
raise ValueError(f"Tag block without DOC-TAGS declaration in {path}")
continue
manifest = json.loads(declarations[0].group(1))
if not isinstance(manifest, dict) or not manifest:
raise ValueError(f"DOC-TAGS must be a non-empty object in {path}")
declared = set()
for subject, contents in manifest.items():
if subject not in HANDLERS or not isinstance(contents, list) or not contents:
raise ValueError(f"Unknown or empty DOC-TAGS subject {subject!r} in {path}")
for content in contents:
if content not in HANDLERS[subject]:
raise ValueError(f"Unknown {subject} content {content!r} in {path}")
declared.add((subject, content))
result[path] = lines
opened = None
found = set()
for index, line in enumerate(lines):
match = TAG_RE.match(line)
if not match:
continue
closing, subject, content = match.groups()
key = (subject, content)
if key not in declared:
raise ValueError(f"Undeclared {subject}:{content} block in {path}")
if closing:
if opened is None or opened[0] != key:
raise ValueError(f"Unmatched closing tag {subject}:{content} in {path}")
if key not in found:
blocks.append(TagBlock(path, subject, content, opened[1], index))
found.add(key)
opened = None
elif opened is not None:
raise ValueError(f"Nested tag block in {path}")
else:
opened = (key, index)
if opened is not None:
raise ValueError(f"Unclosed tag block {opened[0]} in {path}")
if missing := declared - found:
raise ValueError(f"Missing declared tag block(s) {sorted(missing)} in {path}")
return result, blocks
def latest_tested(documents_by_path, blocks):
versions = set()
for block in blocks:
if (block.subject, block.content) == ("TRACKER", "LATEST-TESTED"):
match = re.search(r"Gitea\s+(\d+\.\d+\.\d+)", "".join(body(documents_by_path[block.path], block)))
if not match:
raise ValueError(f"Missing Gitea version in TRACKER:LATEST-TESTED in {block.path}")
versions.add(match.group(1))
if len(versions) != 1:
raise ValueError(f"LATEST-TESTED markers disagree: {sorted(versions)}")
return versions.pop()
def update_version_map(content, version, release_date, tested):
if version not in table(content, "VERSION-MAP"):
content.insert(2, f"| {version} | — | [PENDING] | Compatibility verification pending |\n")
return content
def update_history(content, version, release_date, tested):
if version not in table(content, "HISTORY"):
content.insert(2, f"| **{version}** | {release_date} | [PENDING] Review of mail-template changes | TBD |\n")
return content
def update_upstream(content, version, release_date, tested):
if len(content) != 1 or len(STATUS_RE.findall(content[0])) != 1 or len(INLINE_VERSION_RE.findall(content[0])) != 1:
raise ValueError("TRACKER:UPSTREAM needs exactly one status and version on one body line")
line = INLINE_VERSION_RE.sub(version, content[0], count=1)
content[0] = STATUS_RE.sub("[PENDING]", line, count=1)
return content
def update_badge(content, version, release_date, tested):
match = BADGE_RE.search(content[0]) if len(content) == 1 else None
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"
content[0] = BADGE_RE.sub(badge, content[0], count=1)
return content
def keep_manual(content, version, release_date, tested):
return content
def update_release(content, tag, path):
line = "".join(content)
matches = RELEASE_VERSION_RE.findall(line)
if len(content) != 1 or len(matches) != 1 or ("[v" in line and len(RELEASE_LINK_RE.findall(line)) != 1):
raise ValueError(f"RELEASE block needs one vX.Y.Z label and a /releases/latest link when linked: {path}")
return [RELEASE_VERSION_RE.sub(tag, content[0], count=1)]
HANDLERS = {
"TRACKER": {
"VERSION-MAP": update_version_map,
"HISTORY": update_history,
"UPSTREAM": update_upstream,
"BADGE": update_badge,
"LATEST-TESTED": keep_manual,
"LATEST-VERIFIED": keep_manual,
},
"RELEASE": {"HEADER": update_release, "SUMMARY": update_release, "CURRENT": update_release},
}
def dispatch(originals, blocks, subject, handler):
updated = {path: lines.copy() for path, lines in originals.items()}
for block in sorted((item for item in blocks if item.subject == subject), key=lambda item: (str(item.path), item.start), reverse=True):
lines = updated[block.path]
lines[block.start + 1:block.end] = handler(block, body(lines, block))
return updated
def save_changes(originals, updated, dry_run):
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 apply_release(root, version, release_date, dry_run=False):
target = version_key(version)
date.fromisoformat(release_date)
originals, blocks = documents(root)
tested = latest_tested(originals, blocks)
history = [block for block in blocks if (block.subject, block.content) == ("TRACKER", "HISTORY")]
if len(history) != 1:
raise ValueError(f"Expected one TRACKER:HISTORY block, found {len(history)}")
history_lines = body(originals[history[0].path], history[0])
latest = max(map(version_key, table(history_lines, "HISTORY")))
if target < latest or target <= version_key(tested):
return []
updated = dispatch(originals, blocks, "TRACKER", lambda block, content: HANDLERS["TRACKER"][block.content](content, version, release_date, tested))
return save_changes(originals, updated, dry_run)
def apply_template_release(root, tag, dry_run=False):
if not re.fullmatch(r"v\d+\.\d+\.\d+", tag):
raise ValueError(f"Expected a stable vX.Y.Z release tag, got {tag!r}")
originals, blocks = documents(root)
release_blocks = [block for block in blocks if block.subject == "RELEASE"]
if not release_blocks:
raise ValueError("No RELEASE blocks declared")
versions = set()
for block in release_blocks:
content = body(originals[block.path], block)
line = "".join(content)
matches = RELEASE_VERSION_RE.findall(line)
if len(content) != 1 or len(matches) != 1 or ("[v" in line and len(RELEASE_LINK_RE.findall(line)) != 1):
raise ValueError(f"RELEASE block needs one vX.Y.Z label and a /releases/latest link when linked: {block.path}")
versions.add(matches[0])
if len(versions) != 1:
raise ValueError(f"TRACKER:RELEASE labels disagree: {sorted(versions)}")
current = versions.pop()
if version_key(tag) <= version_key(current):
return []
updated = dispatch(originals, blocks, "RELEASE", lambda block, content: HANDLERS["RELEASE"][block.content](content, tag, block.path))
return save_changes(originals, updated, dry_run)
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("--template-release", help="Published template tag vX.Y.Z; update RELEASE blocks only")
parser.add_argument("--dry-run", action="store_true", help="Check without changing repository documentation")
args = parser.parse_args()
if args.template_release and (args.version or args.release_date):
parser.error("--template-release cannot be combined with upstream release options")
if args.release_date and not args.version:
parser.error("--release-date requires --version")
if args.template_release:
version = args.template_release
changed = apply_template_release(ROOT, version, dry_run=args.dry_run)
label = "Template release"
else:
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)
label = "Gitea"
print(f"{label} {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}")
+24 -114
View File
@@ -2,136 +2,46 @@ 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:
track: track:
name: Track Gitea Release name: Track Gitea Release
runs-on: ubuntu-latest runs-on: linux-amd64-docker-small
permissions: permissions:
contents: write contents: write
pull-requests: write pull-requests: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
ref: main
token: ${{ secrets.GITEA_TOKEN }}
- 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 # Optional GitHub token for upstream API rate limits; never use the Gitea job token.
# Fetch latest release from GitHub API (go-gitea/gitea mirror) GITHUB_TOKEN: ${{ secrets.UPSTREAM_GITHUB_TOKEN }}
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: COMPATIBILITY.md — latest verified note
if: steps.version.outputs.skip != 'true'
run: |
NEW="${{ steps.version.outputs.new_version }}"
# Update version inline on the line with TRACKER:LATEST-VERIFIED
sed -i "/TRACKER:LATEST-VERIFIED/s/Gitea [0-9.]*/Gitea ${NEW}/" COMPATIBILITY.md
- name: README.md — badge and latest tested
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
# Latest tested: update version on the TRACKER:LATEST-TESTED line
sed -i "/TRACKER:LATEST-TESTED/s/Gitea [0-9.]*/Gitea ${NEW}/" README.md
- name: README translations — latest tested
if: steps.version.outputs.skip != 'true'
run: |
NEW="${{ steps.version.outputs.new_version }}"
for f in docs/README.zh-CN.md; do
sed -i "/TRACKER:LATEST-TESTED/s/Gitea [0-9.]*/Gitea ${NEW}/" "$f"
done
- 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 run: python -B .github/scripts/gitea_actions.py create-pull-request --version "$UPSTREAM_VERSION"
with:
branch: track/gitea-${{ steps.version.outputs.new_version }}
commit-message: "docs: track Gitea ${{ steps.version.outputs.new_version }} — pending verification"
title: "Track Gitea ${{ steps.version.outputs.new_version }} — ⏳ Pending Verification"
body: |
Gitea **${{ steps.version.outputs.new_version }}** has been released.
This PR:
- Updates the compatibility matrix in `COMPATIBILITY.md`
- Updates version badges in `README.md` and translations
- Marks the new version as **⏳ Pending Verification**
## 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
- [ ] Update status from ⏳ to ✅ (compatible) or ❌ (breaking) in COMPATIBILITY.md
- [ ] Update the `Mail Template Changes` and `Breaking?` columns with accurate notes
## Semantic tracking
Once verified, update the tracked Gitea version (release numbers stay unchanged):
- [ ] Update the parenthesized Gitea version on the active **Template Release** row in `COMPATIBILITY.md` (e.g. `v1.0.1(v${{ steps.version.outputs.new_version }})`)
- [ ] Update the README release line and badge to the new version
- [ ] Tag a new release only if the template content itself changed — `git tag vX.Y.Z && git push origin vX.Y.Z` (the release workflow packages automatically)
labels: compatibility, automated
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} UPSTREAM_VERSION: ${{ steps.version.outputs.version }}
GITEA_SERVER_URL: ${{ github.server_url }}
GITEA_REPOSITORY: ${{ github.repository }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
+89 -17
View File
@@ -8,15 +8,32 @@ on:
jobs: jobs:
validate: validate:
name: Validate Templates name: Validate Templates
runs-on: ubuntu-latest runs-on: linux-amd64-docker-small
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version: '1.21' go-version: '1.25.x'
- name: Lint — render all templates - uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Test documentation tracker
run: python -B -m unittest discover -s .github/scripts -p 'test_*.py'
- name: Verify pinned official snapshot
working-directory: tools
run: |
go run . upstream prepare
go run . upstream verify
- name: Test — framework alignment and multilingual notification parity
working-directory: tools
run: go test ./...
- name: Build and render all themes and official languages
working-directory: tools working-directory: tools
run: go run . preview all run: go run . preview all
@@ -24,26 +41,81 @@ jobs:
name: Package & Release name: Package & Release
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
needs: validate needs: validate
runs-on: ubuntu-latest runs-on: linux-amd64-docker-small
permissions: permissions:
contents: write contents: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
token: ${{ secrets.GITEA_TOKEN }}
- name: Package themes/ - uses: actions/setup-go@v5
with:
go-version: '1.25.x'
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Verify release tag matches the official snapshot
run: | run: |
VERSION=${GITHUB_REF#refs/tags/} python -c 'import json, os; lock=json.load(open("gitea.lock.json")); assert lock["tag"] == os.environ["GITHUB_REF_NAME"], "Release tag must match the pinned Gitea tag"'
ARCHIVE="gitea-mail-templates-${VERSION}"
mkdir -p "dist/${ARCHIVE}" - name: Verify release notes
cp -r themes LICENSE README.md COMPATIBILITY.md "dist/${ARCHIVE}/" run: test -s ".github/release-notes/${GITHUB_REF_NAME}.md"
cd dist
zip -r "${ARCHIVE}.zip" "${ARCHIVE}" - name: Update release labels in the archive
tar -czf "${ARCHIVE}.tar.gz" "${ARCHIVE}" run: python -B .github/scripts/track_gitea_release.py --template-release "$TEMPLATE_RELEASE"
env:
TEMPLATE_RELEASE: ${{ github.ref_name }}
- name: Generate static preview for the archive
working-directory: tools
run: go run . preview all
- name: Package generated installable themes and multilingual preview
run: python -B .github/scripts/package_release.py --version "$TEMPLATE_RELEASE"
env:
TEMPLATE_RELEASE: ${{ github.ref_name }}
- name: Create Release - name: Create Release
uses: softprops/action-gh-release@v2 run: python -B .github/scripts/gitea_actions.py publish-release --version "$TEMPLATE_RELEASE"
with:
files: dist/*.zip,dist/*.tar.gz
generate_release_notes: true
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TEMPLATE_RELEASE: ${{ github.ref_name }}
GITEA_SERVER_URL: ${{ github.server_url }}
GITEA_REPOSITORY: ${{ github.repository }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
sync-release-docs:
name: Update Latest Release Documentation
if: startsWith(github.ref, 'refs/tags/v')
needs: package
runs-on: linux-amd64-docker-small
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: main
token: ${{ secrets.GITEA_TOKEN }}
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Update marked release labels on main
run: python -B .github/scripts/track_gitea_release.py --template-release "$TEMPLATE_RELEASE"
env:
TEMPLATE_RELEASE: ${{ github.ref_name }}
- name: Commit documentation update
run: |
git add -- ':(glob)**/*.md'
if git diff --cached --quiet; then
echo "Release labels are already current."
exit 0
fi
git -c user.name='release-bot' -c user.email='release-bot@users.noreply.local' commit -m "docs: mark ${TEMPLATE_RELEASE} as latest release"
git push origin HEAD:main
env:
TEMPLATE_RELEASE: ${{ github.ref_name }}
+10 -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,10 @@ 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
preview/rendered/
build/
/upstream/
dist/
node_modules/
+52 -69
View File
@@ -1,89 +1,72 @@
# AGENTS.md — Gitea Mail Templates # AGENTS.md — Gitea Mail Templates
<!-- DOC-TAGS: {"TRACKER":["UPSTREAM"],"RELEASE":["CURRENT"]} -->
## Project Overview ## Project Overview
A curated collection of email template themes (10 visual styles) for self-hosted Gitea instances. Each theme contains 11 Go `html/template` files covering all Gitea notification email types. Theme presentation for self-hosted Gitea, driven by locked official inputs and a reusable control framework. New source architecture supports Gitea 28+; historical release tags retain their original layouts and compatibility.
## Repository Layout ## Repository Layout
``` ```
themes/ # Template themes (10 styles, 11 .tmpl each = 110 source files) gitea.lock.json # Generated official version, commit and file SHA-256; no vendored upstream source
aurora/ # Ethereal / Dreamlike framework/ # Shared header/action/fallback/footer controls and structural layout presets
bloom/ # Creative / Startup (glassmorphism) themes/<name>/ # theme.json and theme.css only; no mail data/logic/translation definitions
ember/ # Community / Open Source tools/ # Go 1.24+ CLI; urfave/cli and x/net HTML validation
heritage/ # Education / Research upstream/ # Explicit sync, checksum/adapter verification and key discovery
horizon/ # Enterprise / Corporate builder/ # One official-context alignment layer and theme package generation
ink/ # Editorial / Publishing config/, data/ # Names, descriptions and mock contexts; not a template inventory
mono/ # Minimal / Swiss design preview/ # Official-compatible rendering, locale adapter and SSE dev server
neon/ # Cyberpunk / Gaming cli/ # list, create, delete, build, preview, dev, upstream
terminal/ # Developers / Tech build/themes/ # Generated installable overrides; ignored
terra/ # Nature / Sustainability build/upstream/ # Downloaded immutable official mail/locales/assets/license; ignored
tools/ # Go CLI tooling (modular, zero dependencies) preview/ # Vanilla JS UI, generated manifest and per-language scripts
tools.go # Main entry point docs/ # English/Simplified Chinese documentation and gallery images
cli/ # CLI subcommands: list, create, delete, preview
config/ # Config types and templates_config.json loading
data/ # templates_config.json — single source of truth for template metadata
preview/ # Template rendering engine (funcs, locale, engine)
go.mod # Go module (stdlib only, zero dependencies)
preview/ # Browser-based live preview
index.html # SPA with style/template/client/viewport switching
rendered.js # Pre-rendered HTML (generated, committed for clone-and-preview)
docs/ # Bilingual documentation (English + Simplified Chinese)
``` ```
## Working With Templates ## Source and Theme Rules
### Template Files - Never commit or hand-maintain official input files. `upstream prepare` downloads missing inputs to the ignored cache. `upstream sync --tag vX.Y.Z` explicitly regenerates the lock after reviewing a new version.
- All `.tmpl` files use Go `html/template` syntax - Build/preview bootstrap a missing cache, then verify it offline against the lock's commit and per-file SHA-256. Corruption is an error, never silently repaired.
- Must use only Gitea's built-in template functions: `AppUrl`, `DotEscape`, `QueryEscape`, `ShortSha`, `HTMLFormat`, `PathEscapeSegments`, `FileSize` - Root `gitea.lock.json` is mandatory for prepare/verify/build/preview/dev; a cached lock is not a substitute. Restore the tracked lock for normal clones; intentional initialization uses explicit `upstream sync --tag v28.0.0`. There is no automatic latest-version selection.
- Must use only official Gitea translation keys (`mail.*` namespace) - Run upstream commands from `tools/`, or provide the subcommand's `--root` (default `..`). `verify` is read-only; `prepare` never rewrites the root lock; `sync` requires network and stable 28+ tags, replaces cache/lock but does not change docs or publish. See CONTRIBUTING for recovery; preserve invalid caches before rebuilding and avoid concurrent cache writers.
- Never reference `.DisplayName` in templates where the data context lacks it (collaborator, transfer, release, workflow_run, assigned, default) - Changed reviewed translation/mail-renderer references require an adapter review before updating reference hashes.
- Each style must have all 11 template types - Official templates own notification data, conditions, subject sections and functional URLs. The shared framework may reorganize presentation and add controls using those values and official translations; themes may not add business logic.
- `theme.json` has `name`, `description`, `mode` (`shared` or `framed`) and optional `layout`. New themes default to `framed` with the `standard` framework preset. `shared` remains a minimal CSS-only mode without framework controls.
- All framed themes use the same header/action/fallback/footer partials and single adapter. Structural presets live in `framework/layouts/`, not in theme directories; color/font/spacing rules live in theme CSS.
- Layout fragments form balanced presentation markup. `__HEADER__` inserts the shared brand control; `__MAIL_TYPE__` expands to the discovered mail ID. Deployment logos use `{{AppUrl}}assets/img/favicon.png`; the static preview uses the downloaded icon in its generated data only.
- CSS must not add text, external resources or hide official content. Use literal email-compatible CSS; browser preview is not a mail-client emulator.
- All referenced translation keys must exist in the official English catalog, including keys outside `mail.*`. Other languages use official English fallback.
- The reviewed Polish v28.0.0 invitation placeholder defect is reported as `[UPSTREAM-WARN]`; its exact source text is preserved.
- Mail entrypoints come from the snapshot; new types need fixtures. Keep JSON fixture integers as integers for Go formatting.
### Adding a New Theme ## Development
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
3. Run `cd tools && go run . preview all` to regenerate preview data
4. Update README.md style gallery table
### Preview System 1. `cd tools && go run . create <name>`.
- `preview/index.html` loads `preview/rendered.js` (pre-rendered by Go) and displays in iframes 2. Edit only theme metadata/CSS. Shared controls or layout presets are framework changes.
- Supports theme/template switching, view mode (Modern/Source), and viewport toggle (Desktop 1386x780 / Mobile 390x780) 3. Run `go run . upstream prepare`, `go test ./...` and `go run . preview all`.
- Keyboard navigation: `←→` cycles focus between Theme/Template/View selects, `↑↓` selects within the focused dropdown, `d`/`m` toggles viewport 4. Check desktop/mobile layouts and languages; update the gallery if appropriate.
- `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
- 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 all` generates `build/themes/<name>/mail/`. `preview all` builds and renders every official language, writing `preview/rendered.js` and `preview/rendered/<locale>.js`. Static preview must work on `file://` via script loading. Keep generated output ignored.
- `tools/tools.go` is the main entry point for the modular CLI
- Subcommands: `list`, `create`, `delete`, `preview`, `dev` `dev` uses Go HTTP/SSE on loopback and watches theme resources, framework, lock/cache and fixture data. The preview remains vanilla JS with theme/template/language/view switching, desktop/mobile viewports, panel controls and keyboard navigation.
- Template metadata lives in `tools/data/templates_config.json` — the single source of truth
- `tools/config/` handles config loading and data flattening Tests compare subjects, notification text and functional links across every language/theme and relevant branch, accounting explicitly for framework-added controls/branding. Verify button labels and identical button/fallback URLs separately. Before release, capture real mail using an isolated Gitea matching the locked version.
- `tools/preview/` implements the rendering engine (template funcs, locale, engine, markSafeHTML)
- `tools/cli/` implements CLI subcommands using `github.com/urfave/cli/v2`
- `tools/preview/server.go` pure Go dev server with SSE live reload and in-process template re-rendering
- Uses Go's native `html/template` package for template rendering
## Versioning ## Versioning
- Release numbers stay on the project's own scheme; the supported Gitea version is appended in parentheses — `v1.0.1(v1.27.2)` means release `v1.0.1` tracks Gitea `1.27.2` <!-- RELEASE:CURRENT -->
- The current release is **v1.0.1** (tracks Gitea 1.27.2); the quick-reference table in `COMPATIBILITY.md` lists it first — the tracker workflow updates that row by position - Current template release: **v28.0.0**.
- When a new Gitea version is verified compatible: update the parenthesized Gitea version in the active `COMPATIBILITY.md` row, plus the README release line and badge <!-- /RELEASE:CURRENT -->
- Tag a new release (`vX.Y.Z`) only when the template content itself changes — the release workflow packages automatically on tag push - The snapshot-driven source refactor is unreleased. Its baseline is Gitea v28.0.0; do not replace the existing v28.0.0 assets.
- Keep the `TRACKER:` markers in `COMPATIBILITY.md` / `README.md` adjacent to their rows so the automated workflow keeps parsing them - Historical compatibility is documented in `COMPATIBILITY.md`, including the incomplete v1.27.2 push fix. New source architecture does not extend support to earlier Gitea versions.
<!-- TRACKER:UPSTREAM -->
- Latest upstream Gitea release: 28.1.0 [PENDING].
<!-- /TRACKER:UPSTREAM -->
- Upstream discovery only adds pending documentation; it must not implicitly update snapshots or mark versions verified.
- Release tags match the locked Gitea version. Review notes in `.github/release-notes/vX.Y.Z.md`, test, build and complete the matching-instance smoke test before publication.
- The workflow packages generated overrides, preview language bundles, docs and upstream license/provenance. Verify Actions/write permissions on the configured Gitea host before relying on automatic upload.
- Markdown managed blocks declare pairs in `DOC-TAGS`. Opening `<!-- SUBJECT:CONTENT -->` and closing `<!-- /SUBJECT:CONTENT -->` delimit managed text; the first repeated pair wins. TRACKER manages pending upstream labels; RELEASE manages published labels. LATEST-TESTED and LATEST-VERIFIED remain manual-only.
## Commit Conventions ## Commit Conventions
- `style(name):` — template changes for a specific theme
- `preview:` — preview tooling changes
- `tools:` — Go CLI/build tooling changes
- `docs:` — documentation and translations
- `fix:` — bug fixes
- `project:` — README, LICENSE, AGENTS.md, meta
- `refactor:` — code restructuring (e.g. modularization)
- `chore:` — maintenance (config updates, build scripts)
## Constraints Use `style(name):`, `preview:`, `tools:`, `docs:`, `fix:`, `project:`, `refactor:` or `chore:` as appropriate. Preserve unrelated user changes and keep generated files out of commits.
- No JavaScript framework dependencies — preview is vanilla JS
- No external Go dependencies — build uses stdlib only
- Templates must remain compatible with Gitea's `html/template` execution environment
- Preview works with `file://` protocol (no server needed)
+100 -50
View File
@@ -1,29 +1,61 @@
# Gitea Compatibility # Gitea Compatibility
<!-- DOC-TAGS: {"TRACKER":["LATEST-VERIFIED","VERSION-MAP","HISTORY"]} -->
This document tracks the compatibility between **Gitea Mail Templates** releases and **Gitea** versions. This document lists supported release combinations, known limitations and the checks used to assess compatibility.
## Quick Reference [Project overview](README.md) · [Contributor guide](CONTRIBUTING.md) · [简体中文使用说明](docs/README.zh-CN.md)
<!-- TRACKER:QUICK-REF-MAX OFFSET=3 --> ## Compatibility Matrix
| Template Release | Min Gitea | Max Tested Gitea | Status |
|-----------------|-----------|-----------------|--------|
| **v1.0.1(v1.27.2)** | **1.25.0** | **1.27.2** | ✅ Active |
| **v1.0.0** | **1.25.0** | **1.26.4** | ✅ Superseded |
> **Latest verified:** All 11 templates pass validation against Gitea 1.27.2 data contexts. <!-- TRACKER:LATEST-VERIFIED --> Select a template release for the Gitea version you run. `[PASS]` records a compatible combination; `[PENDING]` means verification is incomplete. Legacy entries reflect the project's recorded compatibility assessments rather than a fresh test of every patch release.
## Versioning <!-- TRACKER:VERSION-MAP -->
| Gitea version | Recommended template release | Status | Notes |
|---------------|------------------------------|--------|-------|
| 28.1.0 | — | [PENDING] | Compatibility verification pending |
| 28.0.0 | **v28.0.0** | [PASS] | Earlier template releases use the removed `FileSize` mail function |
| 1.27.3 | **v1.27.3** | [PASS] | Push-to-PR commit links fixed in every theme |
| 1.27.2 | **v1.27.3** | [PASS] | Matching v1.27.2 has a push-notification defect in Bloom, Ember, and Heritage |
| 1.27.1 | **v1.27.3** | [PASS] | Older v1.0.x templates use obsolete push-commit fields; v1.27.2 is only partially fixed |
| 1.27.0 | — | [PENDING] | New commit data shape breaks older push templates; v1.27.3 is a plausible fix but was not release-tested here |
| 1.26.4 | **v1.0.1** | [PASS] | — |
| 1.26.3 | **v1.0.1** | [PASS] | — |
| 1.26.2 | **v1.0.1** | [PASS] | — |
| 1.26.1 | **v1.0.1** | [PASS] | — |
| 1.26.0 | **v1.0.1** | [PASS] | — |
| 1.25.5 | **v1.0.1** | [PASS] | — |
| 1.25.0 | **v1.0.1** | [PASS] | — |
<!-- /TRACKER:VERSION-MAP -->
From **1.27.1** onward, template releases are **semantically tracked** against Gitea: the release keeps its own number, and the supported Gitea version is appended in parentheses — `v1.0.1(v1.27.2)` means release `v1.0.1` is the version to use with Gitea `1.27.2`. v1.0.1 retains the pre-1.27 push-commit fields used by the listed Gitea 1.25/1.26 releases. Gitea 1.24.x and earlier use a different custom-mail-template layout and are [UNSUPPORTED]. The v1.0.0 template tag is superseded by v1.0.1; it is not a separate recommendation.
| Gitea version | Template release | <!-- 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. For other Gitea versions, follow the per-version matrix above.
| 1.27.2 | **v1.0.1(v1.27.2)** | <!-- /TRACKER:LATEST-VERIFIED -->
| 1.27.1 | **v1.0.1(v1.27.1)** |
- The [tracker workflow](.github/workflows/gitea-tracker.yml) opens a PR when a new Gitea release appears, marking it ⏳ Pending Verification. ## Snapshot-Driven Source Status
- After verification, update the parenthesized Gitea version on the active **Template Release** row. A new release (`vX.Y.Z`) is only tagged when the template content itself changes — the [release workflow](.github/workflows/release.yml) packages the archive automatically on tag push.
- Releases before Gitea 1.27.1 predate semantic tracking and keep their plain version numbers in the matrix. The source architecture on `main` is **unreleased**. Its lock pins Gitea **v28.0.0**, commit `15b8a5805adf57c5189602008d38cccfd3c795e0`, with 13 mail files (11 entrypoints and 2 shared partials) and 28 locale files. It targets Gitea 28 and later, with each new snapshot requiring review. Historical archives retain the compatibility recorded above.
Themes contain CSS and metadata. The shared framework adapts official templates into reusable headers, action buttons, fallback links and footers while preserving notification conditions, subjects and functional URLs. Official inputs are downloaded to `build/upstream/` and verified against the committed `gitea.lock.json`. Builds reject mismatched checksums, missing English keys, changed adapter references and unreviewed action anchors. New mail types require framework support and fixtures.
### Translation Behavior
Missing translations in other languages follow the official English fallback and are reported as `[FALLBACK]`. Gitea v28.0.0's Polish `mail.team_invite.text_1` starts with malformed `%[1]z…` instead of `%[1]s`; preview preserves this official defect and reports `[UPSTREAM-WARN]`. The exception matches the exact source text; other formatting defects fail rendering.
### Validation Scope
Source validation covers rendering and notification parity across all themes and languages. Optional browser checks cover static and HTTP previews, language switching and mobile layouts. An isolated Gitea smoke test checks template loading and captures a password-reset email. These checks do not establish rendering compatibility with Gmail, Outlook or Apple Mail; test those clients for your deployment. The release workflow does not automatically run the optional browser or real-Gitea suites.
Snapshot preparation, cache verification and version updates are separate operations. `upstream prepare` uses the root lock to create an absent cache; `upstream verify` checks an existing cache offline. `upstream sync --tag vX.Y.Z` explicitly replaces the snapshot and lock. A successful sync does not certify compatibility or update this matrix. See [command usage and cache recovery](CONTRIBUTING.md#official-snapshot-updates).
## Versioning and Known Exceptions
Release tags identify downloadable packages. Use the matrix to select a supported combination: v1.27.2 has a known defect despite its matching version number, and there is no v1.27.1 template tag. Changes on `main` become part of a release only when a new tag and archive are published.
- Gitea 1.27.0 changed the push-to-PR commit data shape. Gitea 1.27.1 fixed its **bundled** mail template, but custom overrides still need the new `.UserCommit.GitCommit` path. v1.27.2 updated seven themes; v1.27.3 completed the remaining three. Older v1.0.x templates use the pre-1.27 path. See the [upstream regression report](https://github.com/go-gitea/gitea/issues/38469), [upstream fix](https://github.com/go-gitea/gitea/pull/38467), and [v1.27.2 correction](.github/release-notes/v1.27.2.md).
- Gitea 28.0.0 removed the mail-template `FileSize` function in favor of `FormatByteSize` ([mail function map](https://raw.githubusercontent.com/go-gitea/gitea/v28.0.0/modules/templates/mail.go)). v28.0.0 uses the new function; older template releases can fail when rendering release attachments. Conversely, v28.0.0 is not a drop-in replacement for earlier Gitea mail contexts.
- A source-only fix is **unreleased** until its tag and downloadable archive exist. New upstream versions remain [PENDING] until their compatibility is checked.
## Check Your Gitea Version ## Check Your Gitea Version
@@ -35,29 +67,35 @@ gitea --version
## Gitea Version History — Mail Template Impact ## Gitea Version History — Mail Template Impact
<!-- TRACKER:VERSION-INSERT OFFSET=2 --> This table describes changes in **Gitea**, not fixes in this template repository. `None` means no relevant new mail-template impact for that Gitea release; the recommended archive is listed in the matrix above.
| Gitea | Release Date | Mail Template Changes | Breaking? |
|-------|-------------|----------------------|-----------| <!-- TRACKER:HISTORY -->
| **1.27.2** | 2026-08-14 | None — security + bug fixes | No | | Gitea | Release Date | Mail Template Changes | Impact |
| **1.27.1** | 2026-07-27 | Fixed push commit data paths: .ID → .UserCommit.GitCommit.ID (#38467) | No | |-------|-------------|-----------------------|--------|
| **1.27.0** | 2026-07-13 | None — no mail template changes | No | | **28.1.0** | 2026-10-06 | [PENDING] Review of mail-template changes | TBD |
| **1.26.4** | 2026-06-21 | None — hotfix release | No | | **28.0.0** | 2026-09-29 | `FileSize` replaced by `FormatByteSize`; mail template internals reorganized | **Breaking:** legacy release-attachment templates can fail |
| **1.26.3** | 2026-06-20 | None — security release | No | | **1.27.3** | 2026-08-29 | None | None |
| **1.26.2** | 2026-05-20 | None — security + bug fixes | No | | **1.27.2** | 2026-08-14 | None | None |
| **1.26.1** | 2026-04-22 | None — bug fixes | No | | **1.27.1** | 2026-07-27 | Bundled push template updated to `.UserCommit.GitCommit` ([fix](https://github.com/go-gitea/gitea/pull/38467)) | **Existing incompatibility persists:** old custom push-to-PR templates can fail |
| **1.26.0** | 2026-04-19 | AppURL cleanup; SanitizeHTML deprecated → use HTMLFormat | No | | **1.27.0** | 2026-07-13 | Push commit data moved under `.UserCommit.GitCommit` ([report](https://github.com/go-gitea/gitea/issues/38469)) | **Breaking:** old push-to-PR templates can fail |
| **1.25.5** | 2026-03-10 | None — security + maintenance | No | | **1.26.4** | 2026-06-21 | None | None |
| **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.26.3** | 2026-06-20 | None | None |
| **≤ 1.24.x** | — | Flat directory structure under `custom/templates/mail/` | ❌ Unsupported | | **1.26.2** | 2026-05-20 | None | None |
| **1.26.1** | 2026-04-22 | None | None |
| **1.26.0** | 2026-04-19 | None | None |
| **1.25.5** | 2026-03-10 | None | None |
| **1.25.0** | 2025 | Custom-mail paths and subject/body format changed ([refactor](https://github.com/go-gitea/gitea/pull/35150)) | **Breaking:** older template layout is not supported |
| **≤ 1.24.x** | — | Legacy custom-mail layout | [UNSUPPORTED] |
<!-- /TRACKER:HISTORY -->
## Template Variable Reference ## Template Variable Reference
All 11 template types use only Gitea built-in variables and functions. Verified against Gitea source (`services/mailer/` + `modules/templates/mail.go`). Official templates define each mail context. The tables below summarize functions and example data relevant to development; they are not a complete API reference. Check the locked official sources and framework adapter when changing a template or fixture.
### Template Functions (available in all templates) ### Relevant Template Functions
| Function | Since Gitea | Notes | | Function | Gitea version | Notes |
|----------|------------|-------| |----------|---------------|-------|
| `AppName` | ≤ 1.21 | Application name | | `AppName` | ≤ 1.21 | Application name |
| `AppUrl` | ≤ 1.21 | Application base URL | | `AppUrl` | ≤ 1.21 | Application base URL |
| `AppDomain` | ≤ 1.21 | Server domain | | `AppDomain` | ≤ 1.21 | Server domain |
@@ -65,7 +103,8 @@ All 11 template types use only Gitea built-in variables and functions. Verified
| `QueryEscape` | ≤ 1.21 | URL query encoding | | `QueryEscape` | ≤ 1.21 | URL query encoding |
| `PathEscapeSegments` | ≤ 1.21 | Per-segment path encoding | | `PathEscapeSegments` | ≤ 1.21 | Per-segment path encoding |
| `ShortSha` | ≤ 1.21 | Truncated commit hash | | `ShortSha` | ≤ 1.21 | Truncated commit hash |
| `FileSize` | ≤ 1.21 | Human-readable file size | | `FileSize` | Before 28.0.0 | Used by older template releases for attachment sizes; unavailable in Gitea 28 mail templates |
| `FormatByteSize` | 28.0.0 | Used by v28.0.0 for attachment sizes; unavailable in earlier Gitea mail templates |
| `HTMLFormat` | ≤ 1.21 | Render string as safe HTML | | `HTMLFormat` | ≤ 1.21 | Render string as safe HTML |
| `Iif` | ≤ 1.21 | Inline conditional | | `Iif` | ≤ 1.21 | Inline conditional |
| `dict` | ≤ 1.21 | Build maps from key-value pairs | | `dict` | ≤ 1.21 | Build maps from key-value pairs |
@@ -74,7 +113,6 @@ All 11 template types use only Gitea built-in variables and functions. Verified
| `SliceUtils` | ≤ 1.21 | Slice manipulation helpers | | `SliceUtils` | ≤ 1.21 | Slice manipulation helpers |
| `JsonUtils` | ≤ 1.21 | JSON helpers | | `JsonUtils` | ≤ 1.21 | JSON helpers |
| `DumpVar` | ≤ 1.21 | Debug variable dump | | `DumpVar` | ≤ 1.21 | Debug variable dump |
| `SanitizeHTML` | ≤ 1.21 | **Deprecated in 1.26** — use `HTMLFormat` |
### Data Contexts by Template ### Data Contexts by Template
@@ -86,30 +124,42 @@ All 11 template types use only Gitea built-in variables and functions. Verified
| `user/auth/reset_passwd` | `DisplayName`, `Code`, `ResetPwdCodeLives` | | `user/auth/reset_passwd` | `DisplayName`, `Code`, `ResetPwdCodeLives` |
| `org/team_invite` | `Inviter`, `Team`, `Organization`, `InviteURL`, `Invite` | | `org/team_invite` | `Inviter`, `Team`, `Organization`, `InviteURL`, `Invite` |
| `repo/collaborator` | `Subject`, `RepoName`, `Link` | | `repo/collaborator` | `Subject`, `RepoName`, `Link` |
| `repo/transfer` | `Doer`, `User`, `Repo`, `Link`, `Destination` | | `repo/transfer` | `Subject`, `Repo`, `Link` |
| `repo/release` | `Release` (with `Publisher`, `TagName`, `Title`, `RenderedNote`, `Attachments`), `Link` | | `repo/release` | `Release` (with `Publisher`, `TagName`, `Title`, `RenderedNote`, `Attachments`), `Link` |
| `repo/actions/workflow_run` | `Run` (with `WorkflowID`, `HTMLURL`), `Jobs`, `RunStatusText` | | `repo/actions/workflow_run` | `Subject`, `Run` (with `WorkflowID`, `HTMLURL`), `Jobs` (with status class, icon CID/alt, attempt, URL and duration) |
| `repo/issue/assigned` | `Doer`, `Issue`, `Link`, `IsPull` | | `repo/issue/assigned` | `Subject`, `Doer`, `Issue`, `Link`, `IsPull`, `CanReply` |
| `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. `.DisplayName` is available in the authentication mail contexts. Collaborator, transfer, release, workflow, assignment and issue-update templates use the context-specific fields listed above.
### Translation Keys ### Translation Keys
All templates use Gitea's official `mail.*` translation namespace. Keys are stable across Gitea 1.25+. Official templates reference `mail.*` keys and `actions.runs.attempt`. AST-based checks inspect `.locale.Tr`, `$.locale.Tr` and both plural keys in `TrN`, including nested pipelines. Every referenced key must exist in the locked English catalog. Preview loads full official locale JSON files rather than a copied Go dictionary; formatting, escaping and plural selection follow the reviewed Gitea adapter.
## How Compatibility Is Verified ## How Compatibility Is Verified
1. **Automated lint** — CI renders all templates via `go run . preview all` on every push 1. **Locked input verification** — run `go run . upstream prepare` then `go run . upstream verify` from `tools/`; first preparation downloads missing cache files, verification checks hashes, adapter references and English keys offline.
2. **Source audit** — Template data contexts are cross-referenced against Gitea's `services/mailer/` package 2. **Framework alignment** — `go test ./...` checks deterministic shared adaptation/generation and fails on changed primary-action anchors; theme sources cannot own business templates.
3. **Release checklist** — Each release confirms the max-tested Gitea version in this file 3. **Notification semantics and controls** — tests compare subjects, notification text and functional links for every theme/language and critical branches, accounting explicitly for added controls/branding. Browser QA checks translated button labels, identical fallback targets, logos and mobile layout. `go run . preview all` builds all language bundles and reports fallback/upstream defects.
4. **Matching-instance smoke test** — before publication, load generated overrides in an isolated Gitea matching the snapshot and capture a real notification or password-reset mail. The admin test email does not exercise custom templates.
5. **Release identity** — the release tag must match the snapshot tag. Only mark the new release verified after its tests and smoke check; published tags/assets remain unchanged.
## 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, release the templates under the same version number (see [Versioning](#versioning)). The [tracker workflow](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/src/branch/main/.github/workflows/gitea-tracker.yml) records upstream releases as pending. Snapshot updates and compatibility verification require a separate review.
Participating Markdown files declare managed subject/content pairs in a `DOC-TAGS` JSON comment. Each body is enclosed by `<!-- TRACKER:CONTENT -->` and `<!-- /TRACKER:CONTENT -->`, or the corresponding `RELEASE` pair. Only the first occurrence of a repeated pair in a document is processed. Undeclared, unknown or unclosed blocks, and missing declared blocks, cause validation to fail.
| Managed blocks | Update policy |
|---|---|
| `TRACKER:VERSION-MAP`, `TRACKER:HISTORY` | Add pending rows for new upstream releases |
| `TRACKER:UPSTREAM` | Update the upstream version and pending status |
| `TRACKER:BADGE` | Update the upstream version while retaining the last tested version |
| `TRACKER:LATEST-TESTED`, `TRACKER:LATEST-VERIFIED` | Updated manually after verification |
| `RELEASE:HEADER`, `RELEASE:SUMMARY`, `RELEASE:CURRENT` | Updated by the release workflow after publication; archive labels are prepared during packaging |
The [release workflow](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/src/branch/main/.github/workflows/release.yml) keeps release links at `/releases/latest` and leaves published tags unchanged. Actions execution and write permissions have not been verified on the configured Gitea host. Until confirmed, check [upstream releases](https://github.com/go-gitea/gitea/releases) and published assets manually. New template tags are published when template content changes; see [versioning and known exceptions](#versioning-and-known-exceptions).
## Reporting Issues ## Reporting Issues
If you find a compatibility problem with a specific Gitea version: Open an issue with the Gitea version, template release or source commit, affected theme and mail type, language, reproduction steps and error output. Include the mail client for display problems. See the [reporting guide](CONTRIBUTING.md#reporting-problems) for source-build diagnostics.
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
+200 -81
View File
@@ -1,100 +1,219 @@
# Contributing to Gitea Mail Templates # Contributing to Gitea Mail Templates
Thanks for your interest in contributing! This project aims to provide a diverse, well-maintained collection of email templates for the Gitea ecosystem. [简体中文](docs/CONTRIBUTING.zh-CN.md) · [Project overview](README.md) · [Compatibility](COMPATIBILITY.md)
--- Contributions to themes, tooling, tests, documentation and translations are welcome. This guide covers the current source architecture; installation and release selection are described in the [README](README.md#installation).
## Ways to Contribute ## Local Development
### Adding a New Style Use **Go 1.24 or later**. From the repository root, prepare the dependencies and locked official inputs, then run the checks and generate the preview:
1. Scaffold the new style: `cd tools && go run . create <your-style-name>` — this creates the directory structure with placeholder `.tmpl` files for all 11 email types
2. Edit each `.tmpl` file in `themes/<your-style-name>/` with your unique visual design
3. Regenerate the preview: `cd tools && go run . preview all` — the build script auto-discovers all theme directories under `themes/` and generates the theme selector dynamically
4. Submit a PR with screenshots of rendered emails (≤ 50 KiB each, 10–20 KiB recommended)
### Style Guidelines
- Each style must include **all 11 template types** listed in the README
- Use only Gitea's built-in template functions — check the [Gitea source](https://github.com/go-gitea/gitea) for reference
- Translation keys must come from Gitea's official locale files (`mail.*` namespace)
- **Never reference `.DisplayName`** in templates where the data context lacks it (collaborator, transfer, release, workflow_run, assigned, default)
- Design for 600px max-width email clients
- Test against major email clients (Apple Mail, Gmail, Outlook) when possible
### Bug Reports
If a template doesn't render correctly:
1. Check that all referenced Go template variables exist — compare against the Gitea source mail templates
2. Verify translation keys match Gitea's locale files
3. Confirm `.DisplayName` isn't used in templates that lack it
4. Regenerate the preview: `cd tools && go run . preview all`
5. Open an issue with: the style name, which email type, and the error or unexpected output
### Documentation Improvements
Documentation updates, preview screenshots, installation guides, and translations are always welcome.
---
## Development Setup
- **Go 1.21+** for template rendering and the CLI tool
### Previewing Locally (Static)
1. Run `cd tools && go run . preview all` to generate rendered data
2. Open `preview/index.html` directly in a browser — no server needed
3. Use the theme switcher, template selector, and view mode toggles
### Dev Server (Live Reload)
```bash ```bash
cd tools && go run . dev cd tools
# → http://localhost:3456 go mod download
go run . upstream prepare
go test ./...
go run . preview all
``` ```
- Watches `themes/**/*.tmpl` — auto-rebuilds on save Open `preview/index.html` in a browser, or run `go run . dev` from `tools/` and visit [http://127.0.0.1:3456](http://127.0.0.1:3456) for live reload. The development server watches themes, the framework, the lock/cache and preview fixtures.
- Pure Go HTTP server with SSE push — no external dependencies
- 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`
### Integration Testing The CLI uses urfave/cli and the x/net HTML parser. Python 3.11 or later is used by the documentation and packaging checks. Node.js and the dependencies in `tools/qa` are needed only for browser QA.
Deploy the templates to a Gitea instance and verify with real transactional emails. ### Common Commands
The admin test email (**Site Administration > Configuration > Mailer > Send Test Email**)
does not use custom mail templates — it follows a built-in code path.
The most reliable method is to trigger a real notification. For example, the password Run these commands from `tools/`. Place flags before positional arguments.
reset flow:
1. Log out and click **"Forgot password"** on the login page | Command | Result |
2. Enter your account email and submit |---|---|
3. Check the password reset email — it will render with your custom mail templates | `go run . list` | List available themes |
| `go run . create my-theme` | Create theme metadata and CSS |
| `go run . build my-theme` | Generate one theme's installable templates |
| `go run . build all` | Generate every theme |
| `go run . preview all` | Build every theme and render all official languages |
| `go run . dev` | Start the development server on port 3456 |
| `go run . dev --port 3457` | Use a different local port |
| `go run . delete my-theme` | Remove the named theme's source directory |
--- `build` writes to `build/themes/<name>/mail/` and records file and source hashes in `build.json`. `preview` also builds the templates, then writes `preview/rendered.js` and `preview/rendered/<locale>.js`. These generated files and the downloaded inputs are ignored by Git. Keep them out of contributions.
## Adding a Theme
1. Run `go run . create my-theme` from `tools/`.
2. Edit `themes/my-theme/theme.json` and `theme.css`.
3. Run `go test ./...` and `go run . preview all` from `tools/`.
4. Check desktop and mobile layouts in English, Simplified Chinese and at least one other official language. Include screenshots with the pull request.
Theme names start with a lowercase letter and may contain lowercase letters, digits and hyphens. A theme directory contains only `theme.json` and `theme.css`.
```json
{
"name": "my-theme",
"description": "A short description of the theme",
"mode": "framed",
"layout": "standard"
}
```
| Mode | Generated presentation |
|---|---|
| `framed` | Shared controls and a framework layout, styled with the theme's CSS. New themes use this mode with the `standard` layout. |
| `shared` | CSS in the official head partial and the official footer partial; no additional framework controls or per-mail overrides. |
### Shared Framework
Headers, action buttons, fallback links, sidebars and footers live in `framework/mail/base/`. Layout presets live in `framework/layouts/`; the optional `layout` field selects a preset for framed themes. Colors, fonts and spacing belong in theme CSS.
`tools/builder/framework.go` adapts official action values and translation keys for the shared controls. Official templates retain responsibility for notification conditions, subjects, attachments and contextual links. Changes to controls or layout structure belong in the framework so all themes use the same adaptation logic.
Layout fragments must form balanced presentation markup. `__HEADER__` inserts the shared header, `__MAIL_TYPE__` expands to the discovered mail ID, and `__SIDEBAR__` inserts the shared sidebar in footer fragments. Deployment logos use `{{AppUrl}}assets/img/favicon.png`; generated preview data embeds the downloaded icon for offline display.
## Design Guidelines
- Use email-compatible CSS. Theme CSS must not load external resources, generate text or hide official content.
- Use presentation markup for layout fragments. The validator accepts `table`, `tbody`, `tr`, `td`, `div` and `span` with supported presentation attributes.
- When editing an existing theme, preserve its visual identity: palette, typography, borders, header, buttons and layout.
- Check desktop and 390px mobile previews, including long text and URLs. Test Gmail, Outlook and Apple Mail where available; the browser preview does not emulate mail clients.
- Keep gallery images in PNG format, at most 50 KiB each; 10–20 KiB is preferred. Follow the [capture guide](docs/images/README.md) for consistent images.
## Official Snapshot Updates
`gitea.lock.json` records a stable Gitea 28+ tag, its commit and per-file SHA-256 hashes. Official mail templates, locale catalogs, the favicon, license and reviewed adapter references are downloaded into `build/upstream/`. Only the generated root lock is committed; official files are not maintained in the source tree.
### Command Reference
Run from `tools/`. Each upstream subcommand accepts `--root <repository-root>`, defaulting to `..` relative to the working directory. Place the flag after the subcommand and quote paths containing spaces:
```powershell
go run . upstream verify --root "C:\Projects\GiteaMailTemplates"
```
| Command | Behavior |
|---|---|
| `go run . upstream prepare` | Read the root lock. Download and validate its exact inputs if the cache is absent; otherwise verify the existing cache offline. Leaves the root lock unchanged. |
| `go run . upstream verify` | Check the existing cache against the root lock, offline and read-only. Reports `[PASS]` and lists non-English missing keys as `[FALLBACK]`. |
| `go run . upstream sync --tag vX.Y.Z` | Resolve an explicit stable Gitea 28+ tag through the GitHub API, download and validate its files, then replace the cache and generate the root lock. |
`sync` uses `api.github.com`; file downloads use `raw.githubusercontent.com`. These commands do not accept a local Gitea checkout, authentication tokens or an implicit `latest` version. Network errors and API rate limits stop the operation. Go may separately need network access to download its toolchain or modules.
### Existing Clone and Offline Use
`build`, `preview` and `dev` prepare inputs automatically. They require the committed root lock and validate cached files before use. Once the cache, Go toolchain and dependencies are available, builds can run offline.
Use `upstream verify` to check the cache independently. It validates official file hashes, reviewed adapter references and official template keys in the English catalog. Build and rendering checks also cover framework translation keys and primary-action anchors, so cache verification is only one part of compatibility testing.
### Missing Lock and Explicit Version Updates
For an existing clone, restore the tracked `gitea.lock.json` if it is missing. The copy at `build/upstream/lock.json` cannot replace it. When intentionally initializing the lock, run:
```bash
cd tools
go run . upstream sync --tag v28.0.0
go run . upstream verify
go test ./...
go run . preview all
```
To review a different version, specify its tag explicitly. Review the lock diff, update the framework adapter and fixtures as needed, and complete the checks and matching-instance smoke test before updating compatibility records. `sync` changes the cache and lock only; documentation and release publication are separate steps. Use `prepare` for a missing cache.
Changes to the reviewed translation or mail-renderer sources require an adapter review before their reference hashes can be updated. Official English must contain all referenced keys; other languages use English fallback. New mail types require framework alignment and fixtures in `tools/data/templates_config.json`, which supplies preview metadata and example contexts. Keep integer values as integers in JSON fixtures for Go formatting.
Network or validation failures before cache replacement leave existing inputs unchanged. Cache replacement and root-lock writing are separate operations; if a filesystem error interrupts them, inspect both before retrying.
### Cache Recovery
`prepare` reports corrupt, incomplete or lock-mismatched caches without repairing them. `sync` also refuses to replace a non-empty invalid snapshot. Inspect the error and move the generated cache aside before preparing it again from the reviewed root lock.
For example, in PowerShell **from the repository root**, with an unused backup name:
```powershell
Move-Item -LiteralPath .\build\upstream -Destination .\build\upstream.backup
Set-Location tools
go run . upstream prepare
go run . upstream verify
```
Preserve the rest of `build/` and leave official cached files unedited. To return to a previous snapshot, restore its reviewed root lock and rebuild the cache in the same way. Stop `dev` before replacing inputs, and avoid concurrent processes writing to the same cache.
### Known Upstream Formatting Issue
Gitea v28.0.0 contains a Polish `mail.team_invite.text_1` placeholder defect. Preview preserves the official output and reports `[UPSTREAM-WARN]`. The exception matches the exact reviewed source text; other formatting errors fail rendering. Report upstream defects without modifying the cached locale files.
## Verification and Release
### Checks for a Pull Request
From `tools/`, run `go test ./...` and `go run . preview all`. Tests cover deterministic generation, changes to action anchors, translation keys, and notification subjects, text and links across all themes and languages. Fixtures include push, review, reply, workflow and attachment branches. Shared controls and branding are accounted for separately from official notification content.
For documentation, tracker or packaging changes, run from the repository root:
```bash
python -B -m unittest discover -s .github/scripts -p 'test_*.py'
```
Keep English and Simplified Chinese guides aligned when changing shared instructions. Include a description of the change, relevant checks and screenshots for visual changes in the pull request.
### Browser and Gitea Checks
After generating preview data, run the optional browser suite:
```bash
cd tools/qa
npm install
npx playwright install chromium
npm test
```
To use installed Chrome or Edge, set `BROWSER_EXECUTABLE_PATH` instead of installing Chromium. Set `PREVIEW_DEV_URL` to include a running HTTP preview in the checks. `npm test -- --update-gallery` also refreshes gallery screenshots.
Before a release, test generated overrides in an isolated Gitea instance matching the locked version and capture a real password-reset or notification email. The administration test-email button bypasses custom templates.
The optional `tools/integration` test uses disposable SQLite data, users, Git/SSH paths and loopback SMTP. Set `GITEA_SMOKE_BINARY` to a checksum-verified official binary matching the lock, then run `go test ./integration -v -count=1` from `tools/`. It captures mail locally without sending to real users. Without this variable, the test is skipped. The release workflow does not run the optional browser or real-Gitea checks automatically.
### Preparing a Release
Release tags must match the locked Gitea version. The current source refactor is unreleased; retain existing v28.0.0 and historical assets.
1. Complete automated checks, preview review and the matching-instance smoke test.
2. Add reviewed notes at `.github/release-notes/vX.Y.Z.md` and update compatibility records with the verification results.
3. Generate all themes and language bundles with `go run . preview all` from `tools/`.
4. Package the release and verify its contents before publication.
For manual packaging, run from the repository root:
```bash
python .github/scripts/package_release.py --version vX.Y.Z --output <new-output-directory>
```
Replace the version and output placeholder with the reviewed tag and a new directory. The script checks source and generated hashes, includes all language bundles, excludes stale theme builds, and refuses to overwrite existing archives.
The release workflow uses the same script to package templates, previews, documentation, the upstream license and provenance. It also updates managed release labels. Confirm Actions support and write permissions on the configured Gitea host before relying on automated publication. See [version tracking](COMPATIBILITY.md#version-tracking) for the managed Markdown blocks.
### Gitea Workflow Configuration
Both workflows use the `linux-amd64-docker-small` runner label. The job image must support the Node.js actions used for checkout and toolchain setup, as well as Git and a POSIX shell. Go and Python are installed by the setup steps.
PR creation and release uploads use `.github/scripts/gitea_actions.py` and the instance's `/api/v1` API. The workflows pass the instance URL, repository and built-in `GITEA_TOKEN`; repository settings must allow the requested code, release and pull-request writes. The optional `UPSTREAM_GITHUB_TOKEN` secret is used only to query official releases on GitHub. Without it, those queries use GitHub's unauthenticated API limit.
The tracker reuses an existing branch when its content matches and reuses an open PR for that branch. A differing branch requires manual review; the workflow does not force-push. Release publication refuses any existing release, including a draft. New releases remain drafts until both archives upload successfully. If an upload fails, inspect the draft before retrying; published releases and assets must remain unchanged.
## Reporting Problems
Include the Gitea version, template release or source commit, theme, mail type, language and steps to reproduce. For source-build problems, include the snapshot tag/commit and output from `go run . upstream verify` or `go run . preview all`. For display problems, include the mail client and a screenshot with personal data removed.
## Commit Conventions ## Commit Conventions
Any readable commit message in semantic format is welcome. Such as: | Prefix | Scope |
|---|---|
| `style(<name>):` | Theme presentation |
| `preview:` | Browser preview |
| `tools:` | CLI, build and snapshot tools |
| `docs:` | Documentation and translations |
| `fix:` | Bug fixes |
| `project:` | Repository configuration and project structure |
| `refactor:` | Code restructuring |
| `chore:` | Maintenance |
- `style(horizon|terminal|ember|bloom|heritage|neon|mono|terra|ink|aurora):` — template changes ## Translations and License
- `preview(*):` — preview tooling changes
- `tools(*):` — Go build script changes
- `docs(*):` — documentation and translations
- `fix(*):` — bug fixes
- `project(*):` — README, LICENSE, AGENTS.md, meta
--- The [Simplified Chinese guide](docs/CONTRIBUTING.zh-CN.md) covers the same workflow. Contributions are licensed under MIT. Retain Gitea copyright and the upstream license when distributing derived templates; see [third-party notices](THIRD_PARTY_NOTICES.md).
## Translations
- English
- [简体中文](docs/CONTRIBUTING.zh-CN.md)
---
## License
By contributing, you agree that your contributions will be licensed under the MIT License.
+124 -135
View File
@@ -1,204 +1,193 @@
# Gitea Mail Templates # Gitea Mail Templates
<!-- DOC-TAGS: {"TRACKER":["BADGE","LATEST-TESTED","UPSTREAM"],"RELEASE":["HEADER","SUMMARY"]} -->
A curated collection of professionally designed, audience-driven email templates for self-hosted [Gitea](https://about.gitea.com) instances. Email themes for self-hosted [Gitea](https://about.gitea.com), with a local preview and tools for building custom mail templates.
[![Gitea](https://img.shields.io/badge/Gitea-1.25+%20%7C%201.27.2%20tested-blue)](COMPATIBILITY.md) <!-- TRACKER:BADGE --> [简体中文](docs/README.zh-CN.md) · [Installation](#installation) · [Preview](#preview) · [Compatibility](COMPATIBILITY.md) · [Contributing](CONTRIBUTING.md)
> **Release v1.0.1 — tracks Gitea 1.27.2 — 110 template files, 10 visual styles, 11 email types each** <!-- 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:HEADER -->
> Latest release: [v28.0.0](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/latest)
<!-- /RELEASE:HEADER -->
## Philosophy The repository includes ten themes for account, repository, issue and workflow notifications. Gitea supplies notification content and translations; themes provide the visual presentation.
Most self-hosted Gitea instances use the default plain email templates. This project provides **ready-to-deploy, visually polished alternatives** — each designed for a specific community or audience, so you can pick the one that feels right for your users. The source architecture on `main` is **unreleased** and uses Gitea v28.0.0 as its baseline. It builds templates from locked official inputs and a shared layout framework. Published archives retain their original contents and compatibility. Use the [compatibility matrix](COMPATIBILITY.md#compatibility-matrix) to choose an archive for your Gitea version.
Every template is a drop-in replacement. All Go template variables, translation keys, and Gitea data contexts are fully compatible. **No patches, no plugins, no forks required.**
---
## Style Gallery ## Style Gallery
| Preview | Style | Audience | Character | | Preview | Theme | Appearance |
|---|---|---|---| |---|---|---|
| ![Horizon](docs/images/horizon.png) | **Horizon** | Enterprise / Corporate | Blue accent, slate typography, centered cards | | ![Horizon](docs/images/horizon.png) | **Horizon** | Blue accents, gray text and a centered white card |
| ![Terminal](docs/images/terminal.png) | **Terminal** | Developers / Tech | Dark mode, monospace, green CLI accents | | ![Terminal](docs/images/terminal.png) | **Terminal** | Dark background, monospace text and green accents |
| ![Ember](docs/images/ember.png) | **Ember** | Community / Open Source | Warm amber, rounded, humanist, inclusive | | ![Ember](docs/images/ember.png) | **Ember** | Warm orange palette, serif headings and rounded buttons |
| ![Bloom](docs/images/bloom.png) | **Bloom** | Creative / Startup | Frosted glass, soft blue light, iridescent accents | | ![Bloom](docs/images/bloom.png) | **Bloom** | Light blue gradients, rounded cards and buttons |
| ![Heritage](docs/images/heritage.png) | **Heritage** | Education / Research | Navy and gold, serif, classic, authoritative | | ![Heritage](docs/images/heritage.png) | **Heritage** | Navy and gold accents, double borders and serif text |
| ![Neon](docs/images/neon.png) | **Neon** | Gaming / Web3 / Creative Tech | Cyberpunk neon glow, hot pink & cyan, synthwave energy | | ![Neon](docs/images/neon.png) | **Neon** | Dark background, pink and cyan accents and glow effects |
| ![Mono](docs/images/mono.png) | **Mono** | Design Studios / Editorial | Swiss brutalist, black & white, red accent, zero radius | | ![Mono](docs/images/mono.png) | **Mono** | Black and white, red accents and square borders |
| ![Terra](docs/images/terra.png) | **Terra** | Sustainability / Wellness | Warm earth tones, organic textures, humanist serif | | ![Terra](docs/images/terra.png) | **Terra** | Earth tones, terracotta buttons and serif text |
| ![Ink](docs/images/ink.png) | **Ink** | Publishing / News / Literature | Editorial print, navy & gold, newspaper layout, drop caps | | ![Ink](docs/images/ink.png) | **Ink** | Newspaper layout, sidebar, serif text and 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** | Dark purple background, teal accents and soft glow effects |
> Images are screenshots from the [live preview](preview/index.html). See [docs/images/README.md](docs/images/README.md) for capture instructions. Images show the current source build. For other mail types and languages, generate the [local preview](#preview). See the [capture guide](docs/images/README.md) when updating screenshots.
[**Live preview gallery**](preview/index.html) — open in a browser for an interactive style switcher with desktop/mobile viewports and view mode (Modern, Source).
---
## Installation ## Installation
### Quick Start ### Choose a Package
Choose a style, then copy the `mail/` directory into your Gitea custom templates path: Check your Gitea version with `gitea --version`, then select a template release from the [compatibility matrix](COMPATIBILITY.md#compatibility-matrix).
| Source | Mail template directory | Preparation |
|---|---|---|
| Release archive | `themes/<name>/mail/` | Download and extract the archive for the recommended release |
| Source checkout | `build/themes/<name>/mail/` | Build with Go 1.24 or later |
For a source checkout, run from the repository root:
```bash ```bash
# Locate your Gitea custom directory cd tools
# (set by GITEA_CUSTOM; defaults shown below) go run . build all
cd ..
```
# Copy templates (example: Horizon style) The first build downloads the official files pinned by `gitea.lock.json` if the cache is absent. Later builds verify the cache before use. The root lock file is required; missing or damaged inputs are covered in the [setup and recovery guide](CONTRIBUTING.md#official-snapshot-updates).
cp -r themes/horizon/mail/* /var/lib/gitea/custom/templates/mail/
# Restart Gitea ### Install a Theme
Copy the chosen theme's `mail/` contents into `<GITEA_CUSTOM>/templates/mail/`, then restart Gitea. Confirm your instance's custom directory before copying files. Common deployment paths include:
| Deployment | Example custom directory |
|---|---|
| Linux binary | `/var/lib/gitea/custom` |
| Docker | `/data/gitea` |
| Windows | `C:\gitea\custom` |
For example, from an extracted release archive on a Linux host managed by systemd:
```bash
mkdir -p /var/lib/gitea/custom/templates/mail
cp -r themes/horizon/mail/. /var/lib/gitea/custom/templates/mail/
systemctl restart gitea systemctl restart gitea
``` ```
### Custom Directory Location For a source build, use `build/themes/horizon/mail/.` as the copy source. Docker and Windows installations should restart Gitea using their deployment's service or container controls.
| Platform | Default Path |
|---|---|
| Linux (binary) | `/var/lib/gitea/custom` |
| Linux (Docker) | `/data/gitea` |
| Windows | `C:\gitea\custom` |
### Switching Styles ### Switching Styles
Overwrite the files with a different style. All templates share the exact same variable structure — no configuration changes needed. Back up existing mail overrides before replacing a theme. Remove the previous theme's installed files, then copy the new theme's complete output. Current source builds include a `build.json` file listing the generated files; for historical archives, refer to the archive contents. Preserve unrelated custom templates.
```bash Removing old overrides is especially important when switching from `framed` to `shared` mode: files left behind can keep the previous theme's layout.
cp -r themes/terminal/mail/* /var/lib/gitea/custom/templates/mail/
systemctl restart gitea
```
### Confirming It Works ### Confirming It Works
The admin test email does not use custom mail templates. To verify your templates Trigger a notification that uses Gitea's mail templates, such as a password-reset email for a test account, and check its appearance and links. The administration test-email button does not use custom mail templates.
are active, trigger a real email notification. The quickest way is the password
reset flow: log out, click **"Forgot password"** on the login page, and check the
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. The preview supports theme, mail type and language selection, rendered HTML and source views, desktop/mobile viewports, and a panel showing the example data. The v28.0.0 snapshot contains 11 mail types and 28 languages.
### Static Preview ### Static Preview
Generate the preview data once (Go only), then open in a browser: From a source checkout, generate the preview data:
```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](preview/index.html) in a browser. Generated language bundles load on demand and work over `file://`, so no server is needed. Archives produced by the current packaging script include these bundles; historical archives retain their original preview contents.
### Dev Server (Live Reload) ### Dev Server (Live Reload)
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
``` ```
| Capability | Static | Dev | Open [http://127.0.0.1:3456](http://127.0.0.1:3456). The Go server watches theme files, the shared framework, the lock/cache and preview fixtures. Changes trigger a rebuild and browser refresh through server-sent events (SSE).
|-----------|--------|-----|
| Go template rendering | ✅ | ✅ |
| Theme/template switching | ✅ | ✅ |
| Live reload on save | — | ✅ |
### Features | Control | Options or shortcut |
|---|---|
| Theme, template, language and view | `←` / `→` moves between selectors; `↑` / `↓` selects an option |
| View | **Modern** for rendered HTML; **Source** for generated HTML text |
| Viewport | **Desktop** (1386 × 780), **Mobile** (390 × 780); `d` / `m` |
| Information panel | `p` toggles the panel |
- Theme switcher — browse all 10 visual styles The preview renders templates using example data. Check your target mail clients separately; browser rendering does not reproduce their CSS support.
- Template switcher — all 11 email types
- View mode — Modern (rendered preview), Source (raw HTML)
- Viewport toggle — Desktop 1386×780 / Mobile 390×780
- Parameter panel — mock data per email type
- Keyboard shortcuts — `←→` tab between Theme/Template/View, `↑↓` select within, `d`/`m` viewport
--- ## Compatibility
<!-- TRACKER:LATEST-TESTED -->
- **Latest tested:** Gitea 28.0.0
<!-- /TRACKER:LATEST-TESTED -->
<!-- RELEASE:SUMMARY -->
- **Latest release:** [v28.0.0](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/latest)
<!-- /RELEASE:SUMMARY -->
<!-- TRACKER:UPSTREAM -->
- **Upstream Gitea 28.1.0:** [PENDING]
<!-- /TRACKER:UPSTREAM -->
The current source architecture targets Gitea 28 and later, with compatibility verified against the locked version. A newer upstream release remains pending until reviewed. Earlier Gitea versions require the packages listed in the [compatibility matrix](COMPATIBILITY.md#compatibility-matrix), which also records the incomplete push-notification fix in v1.27.2.
Generated templates use Gitea's built-in functions and official translation keys. Missing translations fall back to English. The locked v28.0.0 catalog has a known Polish invitation formatting defect; preview reports `[UPSTREAM-WARN]` and preserves the official output. See [known limitations](COMPATIBILITY.md#snapshot-driven-source-status).
## Directory Structure ## Directory Structure
```text
gitea.lock.json # Official tag, commit and file checksums
framework/ # Shared mail controls and layout presets
themes/<name>/ # Theme metadata (theme.json) and CSS (theme.css)
tools/ # Go CLI, build tools and tests
cli/ # Command definitions
upstream/ # Snapshot downloads, verification and key discovery
builder/ # Official template adaptation and theme generation
preview/ # Mail rendering, locale adapter and development server
config/, data/ # Preview metadata and example contexts
integration/, qa/ # Optional Gitea and browser checks
preview/ # Browser UI; generated manifest and language bundles
docs/ # Simplified Chinese guides and gallery images
.github/ # Workflows, release notes and packaging/tracking scripts
build/upstream/ # Downloaded official inputs (ignored)
build/themes/ # Generated installable templates (ignored)
``` ```
gitea-mail-templates/
├── themes/ # 10 visual styles, 11 .tmpl files each Gitea's official templates define notification data, conditions, subjects and URLs. The shared framework arranges these into headers, action buttons, fallback links and footers. Themes define colors, typography and spacing. See [theme development](CONTRIBUTING.md#adding-a-theme) for the `framed` and `shared` modes.
│ ├── ... # Custom styles are added here as separate directories
├── preview/ # Live preview SPA
│ ├── index.html # Style/template/client/viewport switcher
│ └── rendered.js # Pre-rendered templates (generated by tools/)
├── tools/ # Modular CLI tooling
│ ├── tools.go # Main entry point
│ ├── cli/ # CLI subcommands (list, create, delete, preview)
│ ├── config/ # Config types and templates_config.json loading
│ ├── data/ # templates_config.json — single source of truth
│ ├── preview/ # Template rendering engine
│ └── go.mod
├── docs/ # Bilingual documentation (English + Simplified Chinese)
├── AGENTS.md # AI agent guidance
├── CONTRIBUTING.md
├── LICENSE
├── README.md
└── .gitignore
```
### Template Types ### Template Types
| File | Email Trigger | Mail types are discovered from the locked snapshot. The v28.0.0 entrypoints are:
| File | Notification |
|---|---| |---|---|
| `mail/user/auth/activate.tmpl` | Account activation | | `mail/user/auth/activate.tmpl` | Account activation |
| `mail/user/auth/activate_email.tmpl` | Email address verification | | `mail/user/auth/activate_email.tmpl` | Email address verification |
| `mail/user/auth/register_notify.tmpl` | New registration notification | | `mail/user/auth/register_notify.tmpl` | Registration notification |
| `mail/user/auth/reset_passwd.tmpl` | Password reset | | `mail/user/auth/reset_passwd.tmpl` | Password reset |
| `mail/org/team_invite.tmpl` | Team invitation | | `mail/org/team_invite.tmpl` | Team invitation |
| `mail/repo/collaborator.tmpl` | Repository collaborator added | | `mail/repo/collaborator.tmpl` | Repository collaborator added |
| `mail/repo/transfer.tmpl` | Repository ownership transfer | | `mail/repo/transfer.tmpl` | Repository ownership transfer |
| `mail/repo/release.tmpl` | New release published | | `mail/repo/release.tmpl` | Release published |
| `mail/repo/actions/workflow_run.tmpl` | Actions workflow run | | `mail/repo/actions/workflow_run.tmpl` | Actions workflow run |
| `mail/repo/issue/assigned.tmpl` | Issue / Pull Request assigned | | `mail/repo/issue/assigned.tmpl` | Issue or pull request assigned |
| `mail/repo/issue/default.tmpl` | Issue / Pull Request updates | | `mail/repo/issue/default.tmpl` | Issue or pull request activity |
---
## Compatibility
- **Gitea 1.25+** — matches the refactored mail template directory structure (v1.25)
- **Latest tested:** Gitea 1.27.2 <!-- TRACKER:LATEST-TESTED -->
- **Versioning:** releases are semantically tracked — Gitea 1.27.2 → template release v1.0.1(v1.27.2) (see [COMPATIBILITY.md](COMPATIBILITY.md))
- 100% variable-compatible with official Gitea templates — see [COMPATIBILITY.md](COMPATIBILITY.md) for the full matrix
- Uses only built-in Gitea template functions and official translation keys
- No custom template functions or locale patches required
---
## Design Principles
1. **Responsive** — Max-width 600px cards; works in all email clients
2. **Accessible** — 4.5:1 contrast ratios; semantic HTML
3. **Graceful degradation** — Fallback link visible when buttons fail to render
4. **Logo support** — References `{{AppUrl}}assets/img/favicon.png` by default
5. **i18n-ready** — All user-facing strings use Gitea's `{{.locale.Tr}}` system
---
## Documentation
- [English](README.md)
- [简体中文](docs/README.zh-CN.md)
---
## Contributing ## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. A Simplified Chinese translation is available in [docs/](docs/). Contributions to themes, tooling, documentation and translations are welcome. Start with [CONTRIBUTING.md](CONTRIBUTING.md) for local setup, design guidelines, checks and release procedures.
## Documentation
- [简体中文使用说明](docs/README.zh-CN.md)
- [Contributor guide](CONTRIBUTING.md) · [简体中文贡献指南](docs/CONTRIBUTING.zh-CN.md)
- [Compatibility and template reference](COMPATIBILITY.md)
- [Gallery capture guide](docs/images/README.md)
## License ## License
MIT — see [LICENSE](LICENSE). Free to use, modify, and distribute in any Gitea deployment. This project is licensed under [MIT](LICENSE). Generated release archives retain Gitea's license and snapshot provenance; see [third-party notices](THIRD_PARTY_NOTICES.md).
--- This project is not affiliated with Gitea.
<p align="center">
<sub>Not affiliated with the Gitea project. Gitea is a community-managed lightweight code hosting solution written in Go.</sub>
</p>
+18
View File
@@ -0,0 +1,18 @@
# Third-Party Notices
## Gitea
The official mail templates, locale catalogs, preview favicon and implementation references used by the adapter come from [Gitea](https://github.com/go-gitea/gitea), which is licensed under MIT.
The build tools download these files into the ignored `build/upstream/` directory. The committed `gitea.lock.json` records their tag, commit and SHA-256 hashes. The upstream license is retained without modification at `build/upstream/LICENSE`.
Archives generated by the current packaging script include:
- `GITEA-LICENSE`: the upstream license.
- `upstream-lock.json`: the provenance of the official inputs used to generate templates and translated previews.
Deployed email logos reference the Gitea instance's asset URL. Generated preview data embeds the official icon for offline display.
## Project and Dependencies
Theme presentation and tooling are covered by this repository's [MIT license](LICENSE). Go dependencies are recorded in `tools/go.mod` and `tools/go.sum`; optional browser QA dependencies are declared in `tools/qa/package.json`. Each dependency retains its own license.
+200 -61
View File
@@ -1,80 +1,219 @@
# 贡献指南 — Gitea 邮件模板 # 贡献指南
## 参与方式 [English](../CONTRIBUTING.md) · [项目概览](README.zh-CN.md) · [兼容性](../COMPATIBILITY.md)
### 添加新风格 欢迎改进主题、工具、测试、文档和翻译。本指南介绍当前源码架构的开发流程;安装和版本选择请参阅 [README](README.zh-CN.md#安装)。
1. 使用工具脚手架:`cd tools && go run . create <风格名称>` — 这会创建完整的目录结构和全部 11 种邮件类型的占位 `.tmpl` 文件 ## 本地开发
2. 在 `themes/<风格名称>/` 中编写每个 `.tmpl` 文件,应用独特的视觉设计
3. 重新生成预览:`cd tools && go run . preview all`(构建脚本会自动发现 `themes/` 下的所有主题目录并动态生成主题选择器)
4. 提交包含截图渲染效果的 PR(单张图片 ≤ 50 KiB,建议 10–20 KiB)
### 风格指南 使用 **Go 1.24 或更高版本**。从仓库根目录开始,准备依赖和锁定的官方文件,再运行检查并生成预览:
- 每个风格必须包含全部 **11 种模板类型**
- 只使用 Gitea 内置模板函数
- 翻译键必须来自 Gitea 官方语言文件(`mail.*` 命名空间)
- **不要在以下模板中使用 `.DisplayName`**:collaborator、transfer、release、workflow_run、assigned、default
- 面向 600px 宽度的邮件客户端设计
- 尽可能在 Gmail、Outlook、Apple Mail 中测试
### 报告 Bug
1. 检查引用的 Go 模板变量是否存在
2. 验证翻译键是否与 Gitea 语言文件匹配
3. 确认 `.DisplayName` 未在不支持的模板中使用
4. 重新生成预览:`cd tools && go run . preview all`
5. 提交 issue,注明风格名称、邮件类型及错误描述
### 改进文档
文档改进、预览截图、安装指南和翻译始终欢迎。
---
## 开发环境
- **Go 1.21+** 用于模板渲染和 CLI 工具
### 本地预览(静态)
1. 先生成预览数据:`cd tools && go run . preview all`
2. 在浏览器中打开 `preview/index.html` — 无需服务器
### 开发服务器(实时重载)
```bash ```bash
cd tools && go run . dev cd tools
# → http://localhost:3456 go mod download
go run . upstream prepare
go test ./...
go run . preview all
``` ```
修改 `.tmpl` 文件后自动重建并推送至浏览器。 在浏览器中打开 `preview/index.html`,或在 `tools/` 中运行 `go run . dev`,访问 [http://127.0.0.1:3456](http://127.0.0.1:3456) 使用实时预览。开发服务器监听主题、框架、锁文件与缓存、预览测试数据的变化。
CLI 使用 urfave/cli 和 x/net HTML 解析器。文档与打包检查使用 Python 3.11 或更高版本;只有浏览器检查需要 Node.js 和 `tools/qa` 中的依赖。
### 集成测试 ### 常用命令
将模板部署到 Gitea 实例后,请通过真实的邮件通知进行验证。管理后台的测试邮件 以下命令均在 `tools/` 中执行。带有位置参数时,将选项放在位置参数之前。
(**Site Administration > Configuration > Mailer > Send Test Email**)不会使用
自定义邮件模板——它走的是内置代码路径。
最可靠的验证方式是触发一次真实的邮件通知,推荐使用密码重置流程: | 命令 | 用途 |
|---|---|
| `go run . list` | 列出可用主题 |
| `go run . create my-theme` | 创建主题元数据和 CSS |
| `go run . build my-theme` | 生成指定主题的安装模板 |
| `go run . build all` | 生成全部主题 |
| `go run . preview all` | 构建全部主题并渲染所有官方语言 |
| `go run . dev` | 在端口 3456 启动开发服务器 |
| `go run . dev --port 3457` | 指定其他本地端口 |
| `go run . delete my-theme` | 删除指定主题的源码目录 |
1. 退出登录,点击登录页的**"忘记密码"** `build` 将模板写入 `build/themes/<名称>/mail/`,并在 `build.json` 中记录文件和源码哈希。`preview` 同时完成构建,随后写入 `preview/rendered.js` 和 `preview/rendered/<语言>.js`。这些生成文件和下载的官方文件均被 Git 忽略,无需随贡献提交。
2. 输入账户邮箱并提交
3. 查看密码重置邮件——它将使用你的自定义邮件模板渲染
--- ## 添加主题
1. 在 `tools/` 中运行 `go run . create my-theme`。
2. 编辑 `themes/my-theme/theme.json` 和 `theme.css`。
3. 在 `tools/` 中运行 `go test ./...` 和 `go run . preview all`。
4. 检查英文、简体中文及至少另一种官方语言的桌面与移动端布局,在拉取请求中附上截图。
主题名称以小写字母开头,可包含小写字母、数字和连字符。主题目录只包含 `theme.json` 和 `theme.css`。
```json
{
"name": "my-theme",
"description": "主题的简短说明",
"mode": "framed",
"layout": "standard"
}
```
| 模式 | 生成的展示结构 |
|---|---|
| `framed` | 使用共享控件和框架布局,由主题 CSS 定义样式。新主题默认使用此模式及 `standard` 布局。 |
| `shared` | 在官方页头片段中加入 CSS,并保留官方页脚片段;不增加框架控件或各类邮件的覆盖模板。 |
### 共享框架
页头、操作按钮、备用链接、侧栏和页脚位于 `framework/mail/base/`。布局预设位于 `framework/layouts/`,`framed` 主题可通过可选的 `layout` 字段选择预设。配色、字体和间距由主题 CSS 定义。
`tools/builder/framework.go` 将官方操作值和翻译键适配到共享控件。通知条件、主题行、附件和上下文链接由官方模板定义。控件或布局结构的修改应放在框架中,使各主题使用同一套适配逻辑。
布局片段组合后必须形成标签配对完整的展示结构。`__HEADER__` 插入共享页头,`__MAIL_TYPE__` 展开为发现的邮件 ID,页脚片段中的 `__SIDEBAR__` 插入共享侧栏。部署模板的 Logo 使用 `{{AppUrl}}assets/img/favicon.png`;生成的预览数据嵌入下载的图标,供离线显示。
## 设计规范
- 使用兼容邮件客户端的 CSS。主题 CSS 不得加载外部资源、生成文字或隐藏官方内容。
- 布局片段使用展示性标记。校验器接受 `table`、`tbody`、`tr`、`td`、`div`、`span` 及支持的展示属性。
- 修改已有主题时,保留其配色、字体、边框、页头、按钮和布局特征。
- 检查桌面和 390px 移动端预览,覆盖长文本和长 URL。条件允许时测试 Gmail、Outlook 和 Apple Mail;浏览器预览不能模拟邮件客户端。
- 画廊图片使用 PNG,单张不超过 50 KiB,建议为 10–20 KiB。按[截图指南](images/README.md)保持截图一致。
## 更新官方快照
`gitea.lock.json` 记录稳定的 Gitea 28+ 标签、对应提交和每个文件的 SHA-256。官方邮件模板、语言文件、图标、许可证和已评审的适配参考源码下载到 `build/upstream/`。仓库仅提交生成的根锁文件,不维护官方文件副本。
### 命令参考
在 `tools/` 中执行。每个上游子命令均支持 `--root <仓库根目录>`,默认值为相对当前工作目录的 `..`。参数放在子命令之后,包含空格的路径需要加引号:
```powershell
go run . upstream verify --root "C:\Projects\GiteaMailTemplates"
```
| 命令 | 行为 |
|---|---|
| `go run . upstream prepare` | 读取根锁文件。缓存不存在时下载并校验对应文件;已有缓存则离线校验。不修改根锁文件。 |
| `go run . upstream verify` | 对照根锁文件离线、只读检查已有缓存。输出 `[PASS]`,并以 `[FALLBACK]` 列出非英文语言缺少的键。 |
| `go run . upstream sync --tag vX.Y.Z` | 通过 GitHub API 解析明确指定的稳定 Gitea 28+ 标签,下载并校验文件,然后替换缓存并生成根锁文件。 |
`sync` 使用 `api.github.com`,文件下载使用 `raw.githubusercontent.com`。这些命令不接受本地 Gitea 克隆、认证令牌或隐式的 `latest` 版本。网络错误和 API 限流会中止操作;Go 工具链或模块可能仍需单独联网下载。
### 已有克隆与离线使用
`build`、`preview` 和 `dev` 会自动准备输入,需要已提交的根锁文件,并在使用缓存前完成校验。缓存、Go 工具链和依赖齐备后,可以离线构建。
需要单独检查缓存时,使用 `upstream verify`。它校验官方文件哈希、已评审的适配参考源码,以及官方模板引用的英文翻译键。构建和渲染还会检查框架翻译键及主要操作链接的锚点,因此缓存校验只是兼容性测试的一部分。
### 锁文件缺失与显式更新版本
已有克隆缺少 `gitea.lock.json` 时,应恢复受版本控制的文件。`build/upstream/lock.json` 中的副本不能替代根锁文件。需要有意初始化锁文件时,执行:
```bash
cd tools
go run . upstream sync --tag v28.0.0
go run . upstream verify
go test ./...
go run . preview all
```
评审其他版本时,明确指定对应标签。检查锁文件差异,按需更新框架适配逻辑和测试数据,完成检查及同版本实例冒烟测试后,再更新兼容性记录。`sync` 只修改缓存和锁文件,文档更新与版本发布需单独完成。仅缺少缓存时使用 `prepare`。
已评审的翻译或邮件渲染参考源码发生变化时,须先评审适配逻辑,再更新参考哈希。官方英文必须包含所有引用的翻译键;其他语言缺少的翻译回退为英文。新增邮件类型需要补充框架适配和 `tools/data/templates_config.json` 中的测试数据,该文件提供预览元数据和示例上下文。JSON 测试数据中的整数应保持整数形式,以便 Go 正确格式化。
缓存替换前发生网络或校验错误时,原有文件保持不变。缓存替换和根锁文件写入是两个操作;文件系统错误中断操作后,重试前应检查二者是否一致。
### 缓存恢复
`prepare` 会报告损坏、不完整或与锁文件不匹配的缓存,不会自动修复。`sync` 同样拒绝替换非空的无效快照。检查错误后,将生成的缓存移至备份位置,再按已评审的根锁文件重新准备。
例如,在**仓库根目录的 PowerShell** 中,使用尚不存在的备份名称:
```powershell
Move-Item -LiteralPath .\build\upstream -Destination .\build\upstream.backup
Set-Location tools
go run . upstream prepare
go run . upstream verify
```
保留 `build/` 中的其他内容,不直接编辑缓存中的官方文件。需要回退快照时,恢复此前已评审的根锁文件,再按同样方式重建缓存。替换输入前先停止 `dev`,避免多个进程同时写入同一缓存。
### 已知上游格式问题
Gitea v28.0.0 的波兰语 `mail.team_invite.text_1` 存在占位符缺陷。预览保留官方输出并报告 `[UPSTREAM-WARN]`。该例外仅匹配已评审的完整原文,其他格式错误会使渲染失败。发现上游问题时应报告问题,不修改缓存中的语言文件。
## 验证与发布
### 拉取请求检查
在 `tools/` 中运行 `go test ./...` 和 `go run . preview all`。测试覆盖生成结果的确定性、操作锚点变化、翻译键,以及全部主题和语言的通知主题行、正文与链接。测试数据包含推送、评审、回复、工作流和附件分支;共享控件与品牌展示作为附加内容单独处理。
修改文档、追踪脚本或打包流程时,在仓库根目录执行:
```bash
python -B -m unittest discover -s .github/scripts -p 'test_*.py'
```
调整通用说明时,同步更新英文和简体中文指南。拉取请求应说明改动内容及相关检查结果,涉及视觉变化时附上截图。
### 浏览器与 Gitea 实例检查
生成预览数据后,可运行浏览器检查:
```bash
cd tools/qa
npm install
npx playwright install chromium
npm test
```
如需使用已安装的 Chrome 或 Edge,设置 `BROWSER_EXECUTABLE_PATH`,无需再安装 Chromium。设置 `PREVIEW_DEV_URL` 可同时检查运行中的 HTTP 预览。`npm test -- --update-gallery` 会一并更新画廊截图。
发布前,在与锁定版本一致的隔离 Gitea 实例中加载生成的模板,捕获真实的密码重置或通知邮件。管理后台的测试邮件按钮不使用自定义模板。
可选的 `tools/integration` 测试使用临时 SQLite 数据、用户、Git/SSH 路径和回环 SMTP。将 `GITEA_SMOKE_BINARY` 设为已校验官方校验值、版本与锁文件一致的 Gitea 可执行文件,再在 `tools/` 中运行 `go test ./integration -v -count=1`。邮件仅在本地捕获,不发送给真实用户。未设置该变量时会跳过此测试。发布工作流不会自动运行可选的浏览器和真实 Gitea 实例检查。
### 准备发行版
发行标签必须与锁定的 Gitea 版本一致。当前源码重构尚未发布,已有 v28.0.0 和历史版本的附件应保留。
1. 完成自动化检查、预览检查和同版本实例冒烟测试。
2. 在 `.github/release-notes/vX.Y.Z.md` 添加已评审的发行说明,并按验证结果更新兼容性记录。
3. 在 `tools/` 中运行 `go run . preview all`,生成全部主题和语言数据。
4. 打包并检查发行包内容后再发布。
手动打包时,在仓库根目录执行:
```bash
python .github/scripts/package_release.py --version vX.Y.Z --output <new-output-directory>
```
将版本和输出目录占位符替换为已评审的标签及新的目录。脚本会检查源码与产物哈希,包含所有语言数据包,排除残留的旧主题构建,并拒绝覆盖已有压缩包。
发布工作流使用同一脚本打包模板、预览、文档、官方许可证和来源信息,并更新受管理的版本标记。依赖自动发布前,请确认所用 Gitea 主机的 Actions 支持和写入权限。Markdown 管理区块的说明见[版本追踪](../COMPATIBILITY.md#version-tracking)。
### Gitea 工作流配置
两个工作流均使用 `linux-amd64-docker-small` Runner 标签。任务镜像需要支持检出和工具链安装步骤使用的 Node.js Action,并提供 Git 和 POSIX Shell。Go 与 Python 由安装步骤准备。
PR 创建和发行附件上传通过 `.github/scripts/gitea_actions.py` 调用实例的 `/api/v1` API。工作流传入实例地址、仓库名称和内置 `GITEA_TOKEN`,仓库设置需要允许所请求的代码、发行版及拉取请求写入权限。可选的 `UPSTREAM_GITHUB_TOKEN` Secret 仅用于查询 GitHub 上的官方发行版;未配置时使用 GitHub 的匿名 API 限额。
追踪工作流会复用内容相同的已有分支及对应的未关闭 PR。分支内容不同时需要人工检查,工作流不会强制推送。发布流程拒绝修改任何已有发行版,包括草稿;新版本在两个压缩包上传成功后才从草稿转为发布状态。上传失败时,重试前先检查草稿;已发布版本及附件应保持不变。
## 报告问题
请提供 Gitea 版本、模板发行版或源码提交、主题、邮件类型、语言和复现步骤。源码构建问题还应包含快照标签与提交,以及 `go run . upstream verify` 或 `go run . preview all` 的输出。显示问题请注明邮件客户端,并附上移除个人信息后的截图。
## 提交规范 ## 提交规范
- `style(<名称>):` — 特定风格的模板变更 | 前缀 | 范围 |
- `preview:` — 预览工具变更 |---|---|
- `tools:` — Go 构建脚本变更 | `style(<名称>):` | 主题展示样式 |
- `docs:` — 文档和翻译 | `preview:` | 浏览器预览 |
- `fix:` — Bug 修复 | `tools:` | CLI、构建与快照工具 |
- `project:` — README、LICENSE、元文件 | `docs:` | 文档和翻译 |
| `fix:` | 问题修复 |
| `project:` | 仓库配置和项目结构 |
| `refactor:` | 代码重构 |
| `chore:` | 日常维护 |
## 许可协议 ## 翻译与许可证
参与贡献即表示您同意将您的贡献以 MIT 许可证授权。 [英文指南](../CONTRIBUTING.md)介绍相同的开发流程。贡献采用 MIT 许可证;分发派生模板时,请保留 Gitea 版权和官方许可证,详见[第三方声明](../THIRD_PARTY_NOTICES.md)。
+154 -53
View File
@@ -1,88 +1,189 @@
# Gitea 邮件模板 # Gitea 邮件模板
<!-- DOC-TAGS: {"TRACKER":["LATEST-TESTED","UPSTREAM"],"RELEASE":["HEADER","SUMMARY"]} -->
为自托管 [Gitea](https://about.gitea.com) 实例精心设计、面向不同受众的邮件模板集合。 为自托管 [Gitea](https://about.gitea.com) 提供邮件主题、本地预览和自定义邮件模板构建工具。
> **110 个模板文件 — 10 种视觉风格,每种 11 种邮件类型** [English](../README.md) · [安装](#安装) · [预览](#预览) · [兼容性](../COMPATIBILITY.md) · [贡献指南](CONTRIBUTING.zh-CN.md)
--- <!-- RELEASE:HEADER -->
> 最新发布版:[v28.0.0](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/latest)
<!-- /RELEASE:HEADER -->
## 设计理念 仓库包含十种主题,覆盖账户、仓库、议题和工作流等邮件通知。通知内容和翻译由 Gitea 提供,主题负责展示样式。
大多数自托管 Gitea 实例使用默认的纯文本邮件模板。本项目提供了**开箱即用、视觉精美的替代方案**——每种方案都针对特定社区或受众设计,您可以选择最适合您用户的风格。 `main` 分支的源码架构**尚未发布**,以 Gitea v28.0.0 为基线,通过锁定的官方文件和共享布局框架构建模板。已发布的压缩包保留原有内容和兼容范围。安装前,请根据 Gitea 版本查阅[兼容矩阵](../COMPATIBILITY.md#compatibility-matrix),选择对应的发行包。
每个模板都是直接替换。所有 Go 模板变量、翻译键和 Gitea 数据上下文完全兼容。**无需补丁、无需插件、无需 fork。**
---
## 风格画廊 ## 风格画廊
| 预览 | 风格 | 受众 | 特点 | | 预览 | 主题 | 样式特点 |
|---|---|---|---| |---|---|---|
| ![Horizon](images/horizon.png) | **Horizon** | 企业/公司 | 蓝色强调色、石板灰排版、居中卡片 | | ![Horizon](images/horizon.png) | **Horizon** | 蓝色强调色、灰色文字、居中白色卡片 |
| ![Terminal](images/terminal.png) | **Terminal** | 开发者/技术 | 暗色模式、等宽字体、绿色命令行风格 | | ![Terminal](images/terminal.png) | **Terminal** | 深色背景、等宽字体、绿色强调色 |
| ![Ember](images/ember.png) | **Ember** | 社区/开源 | 暖琥珀色、圆角、人文主义、包容 | | ![Ember](images/ember.png) | **Ember** | 暖橙色调、衬线标题、圆角按钮 |
| ![Bloom](images/bloom.png) | **Bloom** | 创意/初创 | 磨砂玻璃、柔和蓝色光线、虹彩点缀 | | ![Bloom](images/bloom.png) | **Bloom** | 浅蓝色渐变、圆角卡片与按钮 |
| ![Heritage](images/heritage.png) | **Heritage** | 教育/研究 | 海军蓝与金色、衬线字体、经典、权威 | | ![Heritage](images/heritage.png) | **Heritage** | 藏蓝与金色、双线边框、衬线字体 |
| ![Neon](images/neon.png) | **Neon** | 游戏/Web3/创意科技 | 赛博朋克霓虹、粉红与青色、合成波能量 | | ![Neon](images/neon.png) | **Neon** | 深色背景、粉红与青色、发光效果 |
| ![Mono](images/mono.png) | **Mono** | 设计工作室/编辑 | 瑞士粗野主义、黑白红强调、零圆角 | | ![Mono](images/mono.png) | **Mono** | 黑白配色、红色强调色、直角边框 |
| ![Terra](images/terra.png) | **Terra** | 可持续/健康 | 温暖大地色调、有机质感、人文衬线 | | ![Terra](images/terra.png) | **Terra** | 大地色调、陶土色按钮、衬线字体 |
| ![Ink](images/ink.png) | **Ink** | 出版/新闻/文学 | 编辑印刷、深蓝与金色、报纸排版 | | ![Ink](images/ink.png) | **Ink** | 报刊式布局、侧栏、衬线字体与首字下沉 |
| ![Aurora](images/aurora.png) | **Aurora** | 高端SaaS/正念 | 空灵光效渐变、深紫与青绿、大气光晕 | | ![Aurora](images/aurora.png) | **Aurora** | 深紫色背景、青绿色强调色、柔和光晕 |
> 图片为 600px 宽截图,来自[在线预览](../preview/index.html)。截图方法参见 [images/README.md](images/README.md)。 截图展示当前源码的构建结果。其他邮件类型和语言可通过[本地预览](#预览)查看;更新截图请参阅[截图指南](images/README.md)。
[**在线预览画廊**](../preview/index.html)
---
## 安装 ## 安装
选择一种风格,将 `mail/` 目录复制到 Gitea 自定义模板路径: ### 选择安装来源
运行 `gitea --version` 确认实例版本,再按[兼容矩阵](../COMPATIBILITY.md#compatibility-matrix)选择模板版本。
| 来源 | 邮件模板目录 | 准备步骤 |
|---|---|---|
| 发行压缩包 | `themes/<名称>/mail/` | 下载并解压推荐版本的发行包 |
| 源码仓库 | `build/themes/<名称>/mail/` | 使用 Go 1.24 或更高版本构建 |
从源码构建时,在仓库根目录执行:
```bash ```bash
cp -r themes/horizon/mail/* /var/lib/gitea/custom/templates/mail/ cd tools
go run . build all
cd ..
```
缓存不存在时,首次构建会下载 `gitea.lock.json` 锁定的官方文件;后续构建会先校验缓存。构建需要根目录的锁文件,文件缺失或缓存损坏的处理方式见[准备与恢复说明](CONTRIBUTING.zh-CN.md#更新官方快照)。
### 安装主题
将所选主题 `mail/` 目录中的内容复制到 `<GITEA_CUSTOM>/templates/mail/`,然后重启 Gitea。复制前请确认实例实际使用的自定义目录。以下是常见部署路径示例:
| 部署方式 | 自定义目录示例 |
|---|---|
| Linux 二进制部署 | `/var/lib/gitea/custom` |
| Docker | `/data/gitea` |
| Windows | `C:\gitea\custom` |
例如,在使用 systemd 管理 Gitea 的 Linux 主机上,从已解压的发行包目录执行:
```bash
mkdir -p /var/lib/gitea/custom/templates/mail
cp -r themes/horizon/mail/. /var/lib/gitea/custom/templates/mail/
systemctl restart gitea systemctl restart gitea
``` ```
切换风格只需覆盖文件,无需更改配置。 使用源码构建产物时,将复制来源改为 `build/themes/horizon/mail/.`。Docker 和 Windows 部署请使用相应的容器或服务管理方式重启。
### 切换主题
先备份已有邮件模板,再移除上一主题安装的文件,复制新主题的完整产物。当前源码构建会在 `build.json` 中记录生成的文件;历史发行包可参照压缩包内容确认文件范围。保留其他自定义模板。
从 `framed` 切换到 `shared` 模式时,尤其需要清理旧主题文件,否则残留的覆盖模板可能继续使用原有布局。
### 确认生效 ### 确认生效
管理后台的测试邮件不会使用自定义模板。要验证模板是否生效,请触发一次真实的 使用测试账户触发密码重置等邮件通知,检查邮件样式和链接。管理后台的测试邮件按钮不使用自定义邮件模板。
邮件通知。最快的方式是密码重置:退出登录,点击登录页的**"忘记密码"**,查看
重置邮件即可——它将使用你的自定义样式渲染。
---
## 预览 ## 预览
**静态模式:** 预览支持切换主题、邮件类型和语言,查看渲染结果或 HTML 源码,切换桌面与移动端视口,以及查看示例数据面板。v28.0.0 快照包含 11 种邮件类型和 28 种语言。
```bash
cd tools && go run . preview all ### 静态预览
```
然后打开 `preview/index.html`。 在源码仓库中生成预览数据:
**开发服务器(实时重载):**
```bash ```bash
cd tools && go run . dev cd tools
# → http://localhost:3456 go run . preview all
cd ..
``` ```
| 功能 | 静态 | Dev | 在浏览器中打开 [preview/index.html](../preview/index.html)。语言数据按需加载,支持 `file://`,无需启动服务器。当前打包脚本生成的发行包包含这些数据;历史发行包保留其原有预览内容。
|-----------|--------|-----|
| Go 模板渲染 | ✅ | ✅ |
| 主题/模板切换 | ✅ | ✅ |
| 实时重载 | — | ✅ |
--- ### 开发服务器
```bash
cd tools
go run . dev
```
打开 [http://127.0.0.1:3456](http://127.0.0.1:3456)。Go 服务器监听主题文件、共享框架、锁文件与缓存、预览测试数据的变化,重新构建后通过服务器发送事件(SSE)刷新页面。
| 控件 | 选项或快捷键 |
|---|---|
| 主题、模板、语言和视图 | `←` / `→` 切换选择框,`↑` / `↓` 选择选项 |
| 视图 | **Modern** 显示渲染结果,**Source** 显示生成的 HTML 文本 |
| 视口 | **Desktop**(1386 × 780)、**Mobile**(390 × 780);快捷键 `d` / `m` |
| 信息面板 | `p` 展开或收起面板 |
预览使用示例数据渲染模板。浏览器与邮件客户端对 CSS 的支持不同,部署前还需在目标邮件客户端中检查效果。
## 兼容性 ## 兼容性
- **Gitea 1.25+** — v1.25 引入的邮件模板目录结构 <!-- TRACKER:LATEST-TESTED -->
- **最新测试:** Gitea 1.27.2<!-- TRACKER:LATEST-TESTED --> - **最新测试:** Gitea 28.0.0
- **版本号:** 发布版本语义化追踪 Gitea 版本 — Gitea 1.27.2 → 模板发布版 v1.0.1(v1.27.2)(详见 [COMPATIBILITY.md](COMPATIBILITY.md)) <!-- /TRACKER:LATEST-TESTED -->
- 与 Gitea 官方模板 100% 兼容 — 详见 [COMPATIBILITY.md](COMPATIBILITY.md) <!-- RELEASE:SUMMARY -->
- **最新发布版:** [v28.0.0](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/latest)
<!-- /RELEASE:SUMMARY -->
<!-- TRACKER:UPSTREAM -->
- **上游 Gitea 28.1.0:** [PENDING]
<!-- /TRACKER:UPSTREAM -->
当前源码架构面向 Gitea 28 及更高版本,兼容性按锁定版本验证。新的上游版本在完成评审前保持待验证状态。早期 Gitea 版本请使用[兼容矩阵](../COMPATIBILITY.md#compatibility-matrix)推荐的发行包;其中也记录了 v1.27.2 推送通知修复不完整的问题。
生成的模板使用 Gitea 内置函数和官方翻译键,缺少的翻译回退为英文。锁定的 v28.0.0 语言文件存在已知的波兰语邀请文案格式缺陷,预览会报告 `[UPSTREAM-WARN]` 并保留官方输出。详见[已知限制](../COMPATIBILITY.md#snapshot-driven-source-status)。
## 目录结构
```text
gitea.lock.json # 官方标签、提交和文件校验值
framework/ # 共享邮件控件和布局预设
themes/<名称>/ # 主题元数据(theme.json)和样式(theme.css)
tools/ # Go 命令行工具、构建工具和测试
cli/ # 命令定义
upstream/ # 快照下载、校验和翻译键发现
builder/ # 官方模板适配与主题生成
preview/ # 邮件渲染、语言适配和开发服务器
config/, data/ # 预览元数据和示例上下文
integration/, qa/ # 可选的 Gitea 实例与浏览器检查
preview/ # 浏览器界面;生成的清单和语言数据包
docs/ # 简体中文指南和画廊图片
.github/ # 工作流、发行说明、打包与版本追踪脚本
build/upstream/ # 下载的官方文件,不纳入版本控制
build/themes/ # 生成的安装模板,不纳入版本控制
```
官方模板定义通知数据、条件、主题行和 URL。共享框架将其组织为页头、操作按钮、备用链接和页脚,主题定义配色、字体与间距。`framed` 和 `shared` 模式的说明见[主题开发指南](CONTRIBUTING.zh-CN.md#添加主题)。
### 模板类型
邮件类型从锁定的快照中发现。v28.0.0 的邮件入口如下:
| 文件 | 通知类型 |
|---|---|
| `mail/user/auth/activate.tmpl` | 账户激活 |
| `mail/user/auth/activate_email.tmpl` | 邮箱验证 |
| `mail/user/auth/register_notify.tmpl` | 注册通知 |
| `mail/user/auth/reset_passwd.tmpl` | 密码重置 |
| `mail/org/team_invite.tmpl` | 团队邀请 |
| `mail/repo/collaborator.tmpl` | 添加仓库协作者 |
| `mail/repo/transfer.tmpl` | 仓库所有权转移 |
| `mail/repo/release.tmpl` | 发布新版本 |
| `mail/repo/actions/workflow_run.tmpl` | Actions 工作流运行 |
| `mail/repo/issue/assigned.tmpl` | 议题或合并请求指派 |
| `mail/repo/issue/default.tmpl` | 议题或合并请求动态 |
## 参与贡献
欢迎改进主题、工具、文档和翻译。[贡献指南](CONTRIBUTING.zh-CN.md)介绍了本地环境准备、设计规范、检查要求和发布流程。
## 相关文档
- [English README](../README.md)
- [简体中文贡献指南](CONTRIBUTING.zh-CN.md) · [English contributor guide](../CONTRIBUTING.md)
- [兼容性与模板参考](../COMPATIBILITY.md)
- [画廊截图指南](images/README.md)
## 许可证 ## 许可证
MIT — 详见 [LICENSE](../LICENSE)。 本项目采用 [MIT 许可证](../LICENSE)。生成的发行包保留 Gitea 许可证和快照来源信息,详见[第三方声明](../THIRD_PARTY_NOTICES.md)。
本项目与 Gitea 官方无隶属关系。
+35 -18
View File
@@ -1,27 +1,44 @@
# Style Preview Images # Gallery Images
Place theme style screenshots of **Desktop** in PNG format here, captured from the [live preview](../preview/index.html). Gallery images show the current source build. Historical release archives retain their original templates.
## Naming Convention ## Capture Settings
``` Use the same settings for every theme so images remain comparable:
horizon.png terminal.png ember.png bloom.png heritage.png
neon.png mono.png terra.png ink.png aurora.png | Setting | Value |
|---|---|
| Template | **Register Notify** |
| Language | **en-US** |
| View | **Modern** |
| Viewport | **Desktop** |
| Information panel | Collapsed |
| Output | PNG, 600px wide recommended |
Save images as `<theme-name>.png` in this directory, matching the directory name under `themes/`. Each file must be at most **50 KiB**; **10–20 KiB** is preferred. Optimize larger PNGs with tools such as `pngquant` or `optipng`.
## Automated Capture
Generate preview data from the repository root:
```bash
cd tools
go run . preview all
cd qa
npm install
npx playwright install chromium
npm test -- --update-gallery
``` ```
## Image Size Requirements To use installed Chrome or Edge, set `BROWSER_EXECUTABLE_PATH` instead of installing Chromium. The script checks language loading, controls and mobile overflow, saves captures in `build/screenshots/`, then copies them here after enforcing the size limit. Review the resulting images before committing.
To ensure screenshots can be displayed in the README, please follow these size requirements: Run `npm test` without `--update-gallery` to perform the checks and capture images without changing the committed gallery.
- **Maximum:** 50 KiB per image ## Manual Capture
- **Recommended:** 30–40 KiB
- **Format:** PNG, optimised — run through `pngquant` or `optipng` before committing
## How to Capture 1. Run `go run . dev` from `tools/` and open [http://127.0.0.1:3456](http://127.0.0.1:3456). Alternatively, run `go run . preview all` and open [preview/index.html](../../preview/index.html) directly.
2. Select a theme and apply the capture settings above.
3. Capture the rendered email, excluding the preview toolbar and information panel.
4. Save the PNG as `<theme-name>.png`, check its size and repeat for the remaining themes.
1. Start the dev server: `cd tools && go run . dev` (opens http://localhost:3456 in your browser) When adding a theme, update its gallery entry in both the [English README](../../README.md#style-gallery) and [Simplified Chinese README](../README.zh-CN.md#风格画廊).
2. For each style, select the **"Activate Account"** template and **"Modern"** client mode in **"Desktop"**
3. Take a screenshot of the rendered email (600px width recommended)
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.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 17 KiB

@@ -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)
@@ -0,0 +1,3 @@
</td></tr>
</table>
@@ -0,0 +1,3 @@
</td></tr>
<tr><td class="email-footer">
+4
View File
@@ -0,0 +1,4 @@
<table role="presentation" width="100%" align="center" cellpadding="0" cellspacing="0" border="0" class="email-wrap">
<tr><td class="email-header">__HEADER__</td></tr>
<tr><td class="email-card card-body mail-type-__MAIL_TYPE__">
+3
View File
@@ -0,0 +1,3 @@
</td></tr>
</table>
@@ -0,0 +1,3 @@
</td></tr>
<tr><td class="email-footer">
+4
View File
@@ -0,0 +1,4 @@
<table role="presentation" width="100%" align="center" cellpadding="0" cellspacing="0" border="0" class="email-wrap email-card">
<tr><td class="email-header">__HEADER__</td></tr>
<tr><td class="email-body card-body mail-type-__MAIL_TYPE__">
+3
View File
@@ -0,0 +1,3 @@
</td></tr>
</table>
@@ -0,0 +1,3 @@
</td></tr>
<tr><td class="email-footer">
+4
View File
@@ -0,0 +1,4 @@
<table role="presentation" width="100%" align="center" cellpadding="0" cellspacing="0" border="0" class="email-wrap email-card">
<tr><td class="email-header">__HEADER__</td></tr>
<tr><td class="email-body card-body mail-type-__MAIL_TYPE__">
@@ -0,0 +1,3 @@
</td></tr>
</table>
@@ -0,0 +1,3 @@
</td></tr>
<tr><td class="email-footer">
@@ -0,0 +1,4 @@
<table role="presentation" width="100%" align="center" cellpadding="0" cellspacing="0" border="0" class="email-wrap email-card">
<tr><td class="email-header">__HEADER__</td></tr>
<tr><td class="email-body card-body mail-type-__MAIL_TYPE__">
@@ -0,0 +1,3 @@
</td></tr>
</table>
@@ -0,0 +1,3 @@
</td></tr>
<tr><td class="email-footer">
@@ -0,0 +1,4 @@
<table role="presentation" width="100%" align="center" cellpadding="0" cellspacing="0" border="0" class="email-wrap">
<tr><td class="email-header">__HEADER__</td></tr>
<tr><td class="email-card card-body mail-type-__MAIL_TYPE__">
+3
View File
@@ -0,0 +1,3 @@
</td></tr>
</table>
+3
View File
@@ -0,0 +1,3 @@
</td><td width="36%" class="email-sidebar resp-hide">__SIDEBAR__</td></tr>
<tr><td colspan="2" class="email-footer">
+4
View File
@@ -0,0 +1,4 @@
<table role="presentation" width="100%" align="center" cellpadding="0" cellspacing="0" border="0" class="email-wrap">
<tr><td colspan="2" class="email-header">__HEADER__</td></tr>
<tr><td width="64%" class="email-card card-body mail-type-__MAIL_TYPE__">
+3
View File
@@ -0,0 +1,3 @@
</td></tr>
</table>
+3
View File
@@ -0,0 +1,3 @@
</td></tr>
<tr><td class="email-footer">
+4
View File
@@ -0,0 +1,4 @@
<table role="presentation" width="100%" align="center" cellpadding="0" cellspacing="0" border="0" class="email-wrap">
<tr><td class="email-header">__HEADER__</td></tr>
<tr><td class="email-card card-body mail-type-__MAIL_TYPE__">
+3
View File
@@ -0,0 +1,3 @@
</td></tr>
</table>
+3
View File
@@ -0,0 +1,3 @@
</td></tr>
<tr><td class="email-footer">
+4
View File
@@ -0,0 +1,4 @@
<table role="presentation" width="100%" align="center" cellpadding="0" cellspacing="0" border="0" class="email-wrap">
<tr><td class="email-header">__HEADER__</td></tr>
<tr><td class="email-card card-body mail-type-__MAIL_TYPE__">
@@ -0,0 +1 @@
</td></tr></table>
@@ -0,0 +1 @@
</td></tr><tr><td class="email-footer">
@@ -0,0 +1 @@
<table role="presentation" width="100%" align="center" cellpadding="0" cellspacing="0" border="0" class="email-wrap"><tr><td class="email-header">__HEADER__</td></tr><tr><td class="email-card card-body mail-type-__MAIL_TYPE__">
@@ -0,0 +1,3 @@
</td></tr>
</table>
@@ -0,0 +1,3 @@
</td></tr>
<tr><td class="email-footer">
@@ -0,0 +1,4 @@
<table role="presentation" width="100%" align="center" cellpadding="0" cellspacing="0" border="0" class="email-wrap">
<tr><td class="email-header">__HEADER__</td></tr>
<tr><td class="email-card card-body mail-type-__MAIL_TYPE__">
+3
View File
@@ -0,0 +1,3 @@
</td></tr>
</table>
@@ -0,0 +1,3 @@
</td></tr>
<tr><td class="email-footer">
+4
View File
@@ -0,0 +1,4 @@
<table role="presentation" width="100%" align="center" cellpadding="0" cellspacing="0" border="0" class="email-wrap">
<tr><td class="email-header">__HEADER__</td></tr>
<tr><td class="email-card card-body mail-type-__MAIL_TYPE__">
+7
View File
@@ -0,0 +1,7 @@
<div class="email-action">
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" class="email-btn-table framework-extra"><tr><td class="email-btn-row"><a href="{{.URL}}" class="email-btn">{{.Label}}</a></td></tr></table>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" class="email-link-box"><tr><td class="email-link-cell">
<p class="email-link-label framework-extra">{{.Hint}}</p>
<p class="email-link-url"><a href="{{.URL}}">{{.URL}}</a></p>
</td></tr></table>
</div>
+1
View File
@@ -0,0 +1 @@
<p class="email-footer-text"><span class="framework-extra">&copy; </span><a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a></p>
+11
View File
@@ -0,0 +1,11 @@
<div class="framework-extra">
{{if eq .Variant "terminal"}}<span class="email-brand">&gt; {{AppName}}</span>
{{else if eq .Variant "mono"}}<span class="email-brand">{{AppName}}<span class="email-brand-star">*</span></span>
{{else if eq .Variant "neon"}}<span class="email-brand">{{AppName}}</span>
{{else if eq .Variant "heritage"}}<img src="{{AppUrl}}assets/img/favicon.png" alt="Gitea" width="40" height="40" class="email-logo" /><p class="email-brand">{{AppName}}</p>
{{else if eq .Variant "bloom"}}<img src="{{AppUrl}}assets/img/favicon.png" alt="Gitea" width="32" height="32" class="email-logo" /><p class="email-brand">{{AppName}}</p>
{{else if eq .Variant "ink"}}<img src="{{AppUrl}}assets/img/favicon.png" alt="Gitea" width="24" height="24" class="email-logo" /><span class="email-brand">{{AppName}}</span><br /><span class="email-tagline">Git with a cup of tea</span>
{{else if eq .Variant "terra"}}<img src="{{AppUrl}}assets/img/favicon.png" alt="Gitea" width="28" height="28" class="email-logo" /><span class="email-brand">{{AppName}}</span><div class="email-brand-rule"></div>
{{else if eq .Variant "aurora"}}<img src="{{AppUrl}}assets/img/favicon.png" alt="Gitea" width="24" height="24" class="email-logo" /><span class="email-brand">{{AppName}}</span>
{{else}}<img src="{{AppUrl}}assets/img/favicon.png" alt="Gitea" width="32" height="32" class="email-logo" /><span class="email-brand">{{AppName}}</span>{{end}}
</div>
+6
View File
@@ -0,0 +1,6 @@
<div class="framework-extra sidebar-info">
<p class="sidebar-label">{{AppName}}</p>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" class="sidebar-info-table"><tr><td>
<strong>{{AppDomain}}</strong><br /><a href="{{AppUrl}}">{{AppUrl}}</a>
</td></tr></table>
</div>
+55
View File
@@ -0,0 +1,55 @@
{
"schema": 1,
"repository": "https://github.com/go-gitea/gitea",
"tag": "v28.0.0",
"commit": "15b8a5805adf57c5189602008d38cccfd3c795e0",
"files": {
"LICENSE": "ed2f10a9d78b8c6c9ef33f1420d0eb266981891caf2f15d630553f02dc60d3ae",
"modules/templates/mail.go": "fb8204b79f700f88c7c3f0da16e50c3d678a2216e62e98f712805ed1d42ab3a9",
"modules/translation/i18n/format.go": "f440236eaf1f73fe0a9aa36a2c036a4b34c6ab28c9e4cf971bef7fceee701cb4",
"modules/translation/i18n/localestore.go": "e55a7832d01753622a7d47e904de95e5aed7ad3535c59bf6ac3fec4346e40e9d",
"modules/translation/translation.go": "4217cbd9beb79c0e1be52dd9a7d7705b9ff4becc2d9515d49236db126da0f7ee",
"options/locale/locale_cs-CZ.json": "816c4a0cfc6112c8cc9a63e63b3de0bc623819a23d2f2bbb800b09c45d23da66",
"options/locale/locale_de-DE.json": "593ec4e3652c0faf9ecb5d5b27a649c1c0a933030ecdfbf4dc810ea4ad69bfb8",
"options/locale/locale_el-GR.json": "dd03e6e083d6286d8eb2045a37bcf5c578fc339ab87a72a7397e01733078d89e",
"options/locale/locale_en-US.json": "bb58779f10b2cd0ee94f71297b2231a621d4d30f2a56a2b223f3c586f635f592",
"options/locale/locale_es-ES.json": "cbb1cd7bc4709a189b8b189d26a83a06fed0c32e0b5ac5602370fb0f03a39d04",
"options/locale/locale_fa-IR.json": "ad85f5829335844883608ed5ed09dee5bc5f4800cda7ef9277200aa4a7f35f32",
"options/locale/locale_fi-FI.json": "3b811b0ee794548399bcb64a887711a26cc0c26e04dfd0305b0846742d065952",
"options/locale/locale_fr-FR.json": "a725246762b129e6323ff2e74baa75eb4c63e893ef0e3e226d3a020d60e1fd87",
"options/locale/locale_ga-IE.json": "670c916a233c83eea5efd7e73971763dd2cd5886a832064724fccfdef87d7bf9",
"options/locale/locale_hu-HU.json": "39fc4beb3a602bbb8d41d8ab6c2cbfa7220d0c52c7cf9d631e5d2304209618a5",
"options/locale/locale_id-ID.json": "1916c9d9f600ddbeba53d2566cb3687c28a57920b2e66a5ef67ea7989535932b",
"options/locale/locale_is-IS.json": "fc1a98822acc354d0915ff6e221722a4d6d26215d52a77d4b4b90259b22f7447",
"options/locale/locale_it-IT.json": "c67559a29021731cb7ffab0fe25a4300025c7e45d9d61488f1a1c99752360854",
"options/locale/locale_ja-JP.json": "e5e64b955a358630b43b6f4cb79b1790175f685a90d46f7071d93dafbdbce9cb",
"options/locale/locale_ko-KR.json": "5f4ed6436333a25c1c638c8e22c8bdf1cf71e96162c43e61a9fa86d0c3bb2bab",
"options/locale/locale_lv-LV.json": "eab8fa9711921b30166649050955c9ae661a0da15261c89be92687508469d863",
"options/locale/locale_nl-NL.json": "b8e58a9832d5da637656f53c6288e35860402e458b3ffd552c0d3c980310caef",
"options/locale/locale_pl-PL.json": "b8cec5b14ac491b16b0f11254df294ac6806be684d0d28e0758f42f48d6d993f",
"options/locale/locale_pt-BR.json": "c9e7d762eb9a00733fcdd935b565372ec28a2cdc11d6d95c9ea06a870b1951a4",
"options/locale/locale_pt-PT.json": "64ab9f016103379b9b5d18d71b03190bcb4fd38e9e4f1e1768a23b14edc21840",
"options/locale/locale_ru-RU.json": "d607a77a12abe0ccb87d9392a096369005b48ac74b2d4767a19455bd608f2c42",
"options/locale/locale_si-LK.json": "ed1f7fc6fd2153d541b124dc1051666fbc8750b9c1fd3f3befd5d976590ffc0a",
"options/locale/locale_sk-SK.json": "91adc0dfe01783eb9c6b7f4a7c9941a215079a610cd1ecbeac1fe574ea67c95d",
"options/locale/locale_sv-SE.json": "c4a6448c23dbed6e383d0b417a8a872abf187f76394780da6809182ba4343749",
"options/locale/locale_tr-TR.json": "6d5774ff6293e60bee0961c072e0ddea272656a12afe8d6eb94e330bcc8dcdae",
"options/locale/locale_uk-UA.json": "fabce931b5f7bc378494fe752f3a5148d6f37c6b9ae07674c09d3afa274378d5",
"options/locale/locale_zh-CN.json": "682925d41494eab564ff1a13269def6e396676d13cfcefabb22aeff75b808a4d",
"options/locale/locale_zh-TW.json": "9d242d16492e808889d7cdcc5db2ee5e53baabfc3d3dc13ea854c8852e963ec4",
"public/assets/img/favicon.png": "cbe472b55c0b2f2e57db58e991fc457e136fbaf246f54aaf7631ff089c76344f",
"templates/mail/base/footer.tmpl": "5accf2a39dffd952fd2d55a4fa867d33d91415792f11f90bc7e2fc8f8492117b",
"templates/mail/base/head.tmpl": "239a2f805949ca7ebd27fa11a34a20bbb7a5d435a95f3c25c55a4139126d7476",
"templates/mail/org/team_invite.tmpl": "646133e75c9efdce96eb7d58d8aea0111cae890c816a718ee8098346ab089a44",
"templates/mail/repo/actions/workflow_run.tmpl": "dbd34f3c7fb4f10a7497ca2872a648104c098f425866cae50f44b98033000b45",
"templates/mail/repo/collaborator.tmpl": "dd8a858a4fcdb37c71c88b9e51f7f5bbfa3c6afec572e2c9c7801fde4ececfb5",
"templates/mail/repo/issue/assigned.tmpl": "347cbe856d748a8c36fb8c9e5db64b831b2a8faf86a266a2e926e2eb1ab9a24b",
"templates/mail/repo/issue/default.tmpl": "98c36bb240f4946d9364fa4ebd386cff599f03bc2526c107881b8ce5a39a9ab8",
"templates/mail/repo/release.tmpl": "5b6c37286eedff5502efaeeaefac52190b835af0711055f387115bb4dd7ed4e5",
"templates/mail/repo/transfer.tmpl": "f394e8c0a75a16bbd19389b2bc286111326bcbcbb4e900e43b708ae74d0546a5",
"templates/mail/user/auth/activate.tmpl": "cd360177635d49663f25d700ac1d7f15f551f8de152053a39402267f896cfdee",
"templates/mail/user/auth/activate_email.tmpl": "3bfa50ce1801fc928d6359a8a52fd9578dac40f185aea4c1d792fc5ddd4bbf0c",
"templates/mail/user/auth/register_notify.tmpl": "ee11808ccc63241cedc29c2038a8ee29583817f51743bb56adc379944de5c8af",
"templates/mail/user/auth/reset_passwd.tmpl": "9efd3651c5e82a2d411c569f0736c6c55201a062d29747b985eb6af268f81b8e"
}
}
+118 -47
View File
@@ -239,6 +239,11 @@ html,body,#preview-area,#panel-body{
<select id="sel-template" onchange="switchTemplate(this.value)"></select> <select id="sel-template" onchange="switchTemplate(this.value)"></select>
</span> </span>
<span class="sep"></span> <span class="sep"></span>
<span class="ctl-group">
<label>Language</label>
<select id="sel-locale" onchange="switchLocale(this.value)"></select>
</span>
<span class="sep"></span>
<span class="ctl-group"> <span class="ctl-group">
<label>View</label> <label>View</label>
<select id="sel-client" onchange="switchClient(this.value)"> <select id="sel-client" onchange="switchClient(this.value)">
@@ -323,10 +328,14 @@ html,body,#preview-area,#panel-body{
// //
// State — REGISTRY and PARAMS are loaded from rendered.js (single source of truth) // State — REGISTRY and PARAMS are loaded from rendered.js (single source of truth)
// //
const state = { theme:'', template:'', client:'modern', viewport:'desktop' }; const state = { theme:'', template:'', client:'modern', viewport:'desktop', locale:'en-US' };
let REGISTRY = {}; let REGISTRY = {};
let PARAMS = {}; let PARAMS = {};
let rendered = null; // window.__RENDERED__ let rendered = null;
let manifest = null;
let localeRequest = 0;
let renderRequest = 0, renderTimer;
const localeLoads = new Map();
let devMode = false; // true once SSE connects let devMode = false; // true once SSE connects
// //
@@ -338,20 +347,31 @@ function transform(html, mode) {
const e = html.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); const e = html.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
return '<!DOCTYPE html><html><head><style>body{background:#0d1117;color:#c9d1d9;font-family:Consolas,monospace;padding:16px;margin:0}pre{white-space:pre-wrap;word-break:break-all;font-size:11px;line-height:1.6;margin:0}</style></head><body><pre>'+e+'</pre></body></html>'; return '<!DOCTYPE html><html><head><style>body{background:#0d1117;color:#c9d1d9;font-family:Consolas,monospace;padding:16px;margin:0}pre{white-space:pre-wrap;word-break:break-all;font-size:11px;line-height:1.6;margin:0}</style></head><body><pre>'+e+'</pre></body></html>';
} }
return html; // Gitea sends status icons as CID attachments. Preview has no MIME message,
// so display equivalent local SVG icons without changing install templates.
const doc = new DOMParser().parseFromString(html, 'text/html');
// Static preview has no deployed instance; use the generated official asset.
// Installable mail templates keep the instance's {{AppUrl}} image URL.
doc.querySelectorAll('img.email-logo').forEach(img => { if(manifest?.logo) img.src=manifest.logo; });
doc.querySelectorAll('img[src^="cid:"]').forEach(img => {
const color = img.classList.contains('status-success') ? '#2da44e' : '#697077';
const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><circle cx="9" cy="9" r="7" fill="'+color+'"/><path d="M5 9l3 3 5-6" fill="none" stroke="white" stroke-width="2"/></svg>';
img.src = 'data:image/svg+xml,' + encodeURIComponent(svg);
});
return '<!DOCTYPE html>' + doc.documentElement.outerHTML;
} }
// //
// Render preview // Render preview
// //
function render() { function render() {
const request=++renderRequest;
clearTimeout(renderTimer);
const frame = document.getElementById('preview-frame'); const frame = document.getElementById('preview-frame');
const loading = document.getElementById('loading'); const loading = document.getElementById('loading');
if(!rendered) return; if(!rendered) return;
if (location.protocol !== 'file:') {
frame.setAttribute('sandbox', ''); frame.setAttribute('sandbox', '');
}
frame.style.display = 'none'; frame.style.display = 'none';
loading.style.display = 'flex'; loading.style.display = 'flex';
@@ -363,14 +383,15 @@ function render() {
let done = false; let done = false;
function finish() { function finish() {
if (done) return; done = true; if (done || request!==renderRequest) return; done = true;
clearTimeout(renderTimer);
loading.style.display = 'none'; loading.style.display = 'none';
frame.style.display = 'block'; frame.style.display = 'block';
frame.onload = null; frame.onload = null;
} }
frame.onload = finish; frame.onload = finish;
frame.srcdoc = content; frame.srcdoc = content;
setTimeout(finish, 5000); renderTimer=setTimeout(finish, 5000);
updatePanel(); updatePanel();
} }
@@ -391,10 +412,11 @@ function toggleSection(id) {
// Panel content updates // Panel content updates
// //
function updatePanel() { function updatePanel() {
const escape = v => String(v).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
const r = REGISTRY[state.template]; const r = REGISTRY[state.template];
document.getElementById('panel-name').textContent = r ? r.name : state.template; document.getElementById('panel-name').textContent = r ? r.name : state.template;
document.getElementById('panel-desc').textContent = r ? r.desc : ''; document.getElementById('panel-desc').textContent = r ? r.desc : '';
document.getElementById('panel-path').textContent = r ? 'themes/'+state.theme+'/'+r.path : '-'; document.getElementById('panel-path').textContent = r ? 'build/themes/'+state.theme+'/'+r.path+' (official snapshot + theme overrides)' : '-';
// Template stats // Template stats
const themes = rendered ? Object.keys(rendered).length : 0; const themes = rendered ? Object.keys(rendered).length : 0;
@@ -407,13 +429,13 @@ function updatePanel() {
} }
} }
document.getElementById('panel-stats').textContent = document.getElementById('panel-stats').textContent =
themes + ' themes loaded · ' + totalTemplates + ' templates (' + (totalTemplates - totalFailed) + ' ok, ' + totalFailed + ' failed)'; manifest.upstream + ' · ' + state.locale + ' · ' + themes + ' themes · ' + totalTemplates + ' templates (' + (totalTemplates - totalFailed) + ' ok, ' + totalFailed + ' failed)';
const p = PARAMS[state.template] || {}; const p = PARAMS[state.template] || {};
let rows = ''; let rows = '';
for(const [k,v] of Object.entries(p)){ for(const [k,v] of Object.entries(p)){
const isReq = !k.includes('RenderedNote') && !k.includes('Attachments') && !k.includes('DisableDownload') && !k.includes('IsMention') && !k.includes('IsPull') && !k.includes('IsForcePush') && !k.includes('ReviewComments') && !k.includes('Commits') && !k.includes('BaseBranch'); const isReq = !k.includes('RenderedNote') && !k.includes('Attachments') && !k.includes('DisableDownload') && !k.includes('IsMention') && !k.includes('IsPull') && !k.includes('IsForcePush') && !k.includes('ReviewComments') && !k.includes('Commits') && !k.includes('BaseBranch');
rows += '<tr><td>.'+k+'</td><td>'+String(v)+'</td><td><span class="badge '+(isReq?'badge-req':'badge-opt')+'">'+(isReq?'required':'optional')+'</span></td></tr>'; rows += '<tr><td>.'+escape(k)+'</td><td>'+escape(v)+'</td><td><span class="badge '+(isReq?'badge-req':'badge-opt')+'">'+(isReq?'required':'optional')+'</span></td></tr>';
} }
document.getElementById('panel-params').innerHTML = rows || '<tr><td colspan="3" style="color:var(--muted)">-</td></tr>'; document.getElementById('panel-params').innerHTML = rows || '<tr><td colspan="3" style="color:var(--muted)">-</td></tr>';
} }
@@ -422,7 +444,7 @@ function updatePanel() {
// Events // Events
// //
function switchTheme(v) { state.theme=v; render(); } function switchTheme(v) { state.theme=v; render(); }
function switchTemplate(v) { state.template=v; render(); } function switchTemplate(v) { state.template=v; switchLocale(state.locale); }
function switchClient(v) { state.client=v; render(); } function switchClient(v) { state.client=v; render(); }
function setViewport(v) { function setViewport(v) {
state.viewport=v; state.viewport=v;
@@ -443,7 +465,7 @@ document.addEventListener('keydown',e=>{
if(e.key==='p'&&!e.ctrlKey&&!e.metaKey&&!e.altKey){ togglePanel(); return; } if(e.key==='p'&&!e.ctrlKey&&!e.metaKey&&!e.altKey){ togglePanel(); return; }
// ← → cycle focus between Theme / Template / Client selects // ← → cycle focus between Theme / Template / Client selects
const selects=['sel-theme','sel-template','sel-client']; const selects=['sel-theme','sel-template','sel-locale','sel-client'];
const activeId=document.activeElement?.id; const activeId=document.activeElement?.id;
const cur=selects.indexOf(activeId); const cur=selects.indexOf(activeId);
@@ -460,38 +482,85 @@ document.addEventListener('keydown',e=>{
}); });
// //
// Init — load everything from rendered.js (single source of truth) // Manifest and language bundles
// function fillSelect(id, entries, selected) {
(function(){ const select = document.getElementById(id);
if(window.__REGISTRY__) REGISTRY = window.__REGISTRY__; select.replaceChildren();
if(window.__PARAMS__) PARAMS = window.__PARAMS__; for (const [value,label] of entries) {
const option = document.createElement('option');
if(window.__RENDERED__){ option.value=value; option.textContent=label; select.appendChild(option);
rendered = window.__RENDERED__;
const themeKeys = Object.keys(rendered).sort();
const selTheme = document.getElementById('sel-theme');
themeKeys.forEach(t => {
const o = document.createElement('option');
o.value = t;
o.textContent = t.charAt(0).toUpperCase() + t.slice(1);
selTheme.appendChild(o);
});
state.theme = themeKeys[0] || '';
const selTpl = document.getElementById('sel-template');
for(const [id, t] of Object.entries(REGISTRY)){
const o = document.createElement('option');
o.value = id;
o.textContent = t.name;
selTpl.appendChild(o);
} }
state.template = Object.keys(REGISTRY)[0] || ''; select.value=selected;
} else { }
document.getElementById('loading').style.display='none';
showMessage('rendered.js not found — run <code>cd tools &amp;&amp; go run . preview all</code> to generate', 'error'); function applyManifest(data) {
if (!data || data.schema !== 1 || !data.languages.length || !data.themes.length) {
throw new Error('Invalid preview manifest; regenerate with go run . preview all');
}
manifest=data; REGISTRY=data.registry; PARAMS=data.params;
if (!data.themes.includes(state.theme)) state.theme=data.themes[0];
if (!REGISTRY[state.template]) state.template=Object.keys(REGISTRY)[0];
if (!data.languages.includes(state.locale)) state.locale=data.languages.includes('en-US') ? 'en-US' : data.languages[0];
fillSelect('sel-theme',data.themes.map(t=>[t,t.charAt(0).toUpperCase()+t.slice(1)]),state.theme);
fillSelect('sel-template',Object.entries(REGISTRY).map(([id,t])=>[id,t.name]),state.template);
let names; try { names=new Intl.DisplayNames([navigator.language],{type:'language'}); } catch (_) {}
fillSelect('sel-locale',data.languages.map(l=>[l,names ? l+' — '+names.of(l) : l]),state.locale);
}
function loadLocale(lang) {
if (window.__MAIL_PREVIEW_DATA__?.[lang]) return Promise.resolve(window.__MAIL_PREVIEW_DATA__[lang]);
if (localeLoads.has(lang)) return localeLoads.get(lang);
const promise=new Promise((resolve,reject)=>{
const script=document.createElement('script');
script.src='rendered/'+encodeURIComponent(lang)+'.js'+(devMode ? '?v='+Date.now() : '');
script.onload=()=>{
script.remove();
const data=window.__MAIL_PREVIEW_DATA__?.[lang];
if (data) resolve(data); else reject(new Error('Invalid language bundle '+lang));
};
script.onerror=()=>{ script.remove(); reject(new Error('Language bundle missing: '+lang+'; regenerate preview data')); };
document.head.appendChild(script);
});
localeLoads.set(lang,promise);
promise.catch(()=>localeLoads.delete(lang));
return promise;
}
async function switchLocale(lang) {
if (!manifest?.languages.includes(lang)) return;
state.locale=lang; rendered=null;
const request=++localeRequest;
document.getElementById('preview-frame').style.display='none';
document.getElementById('loading').style.display='flex';
try {
const data=await loadLocale(lang);
if (request!==localeRequest) return;
rendered=data;
try { localStorage.setItem('mail-preview-locale',lang); } catch (_) {}
document.getElementById('locale-message')?.remove();
const warnings=manifest.warnings?.[lang]?.[state.template] || [];
const fallback=manifest.fallback?.[lang] || [];
if (fallback.length || warnings.length) {
const el=showMessage('',warnings.length ? 'warn' : 'info'); el.id='locale-message';
el.textContent=warnings.join(' ') || '[FALLBACK] '+lang+': '+fallback.length+' referenced translation keys use en-US';
} }
render(); render();
} catch (error) {
if (request!==localeRequest) return;
document.getElementById('loading').style.display='none';
showMessage('', 'error').textContent = String(error);
}
}
(function(){
try {
try { state.locale=localStorage.getItem('mail-preview-locale') || 'en-US'; } catch (_) {}
applyManifest(window.__MAIL_PREVIEW__);
switchLocale(state.locale);
} catch (error) {
document.getElementById('loading').style.display='none';
showMessage('Preview data unavailable — run <code>cd tools &amp;&amp; go run . preview all</code> to generate','error');
}
})(); })();
// Message helpers // Message helpers
@@ -512,7 +581,7 @@ function setDevMode(on) {
if (on) { if (on) {
clearMessages(); clearMessages();
showMessage( showMessage(
'Live reload active — changes to .tmpl files will auto-refresh the preview.', 'Live reload active — theme CSS, shared controls, lock/cache and fixtures auto-refresh the preview.',
'info' 'info'
).id = 'msg-dev-disclaimer'; ).id = 'msg-dev-disclaimer';
} }
@@ -520,6 +589,7 @@ function setDevMode(on) {
// Dev-server live-reload (SSE) // Dev-server live-reload (SSE)
(function(){ (function(){
if (location.protocol === 'file:') return;
let es; let es;
try { try {
es = new EventSource('/events'); es = new EventSource('/events');
@@ -533,14 +603,15 @@ function setDevMode(on) {
const script = document.createElement('script'); const script = document.createElement('script');
script.src = 'rendered.js?v=' + Date.now(); script.src = 'rendered.js?v=' + Date.now();
script.onload = function() { script.onload = function() {
if (window.__RENDERED__) rendered = window.__RENDERED__; script.remove();
if (window.__REGISTRY__) REGISTRY = window.__REGISTRY__; window.__MAIL_PREVIEW_DATA__ = {};
if (window.__PARAMS__) PARAMS = window.__PARAMS__; localeLoads.clear();
render(); applyManifest(window.__MAIL_PREVIEW__);
switchLocale(state.locale);
}; };
document.head.appendChild(script); document.head.appendChild(script);
} else if (msg.type === 'error') { } else if (msg.type === 'error') {
showMessage(msg.message, 'error'); showMessage('', 'error').textContent = msg.message;
} }
} catch(_) {} } catch(_) {}
}; };
-142
View File
@@ -1,142 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<title>
{{.locale.Tr "mail.team_invite.subject" (.Inviter.DisplayName|DotEscape) (.Organization.DisplayName|DotEscape)}}
</title>
<style type="text/css">
/* Base */
body,.ExternalClass,.ReadMsgBody { background-color:#0a0815 }
.email-wrap { max-width:600px;margin:40px auto;font-family:system-ui,-apple-system,sans-serif }
/* Header */
.email-logo { display:block;margin:0 auto 14px;border:0;outline:none;opacity:0.5 }
.email-brand { font-size:16px;font-weight:300;color:#ffffff66;letter-spacing:4px;text-transform:uppercase }
.email-header { padding:0 0 32px;text-align:center }
/* Card */
.email-card { padding:48px 40px;background:#191432cc;border:1px solid #0d948833;box-shadow:0 0 60px #0d948814,0 0 120px #6366f10a }
.email-heading { margin:0 0 20px;font-size:24px;font-weight:300;color:#ccfbf1;line-height:1.3;letter-spacing:-.5px }
.email-text { margin:0 0 24px;font-size:14px;line-height:2;color:#d2fcf5e0;font-weight:300 }
.email-subtext { margin:0;font-size:13px;line-height:1.8;color:#b4c3d7f2;font-weight:300 }
/* Divider */
.email-divider { height:1px;background:linear-gradient(90deg,transparent,#0d94884d,transparent) }
/* Button */
.email-btn-row { padding:40px 40px;text-align:center }
.email-btn { display:inline-block;padding:16px 48px;font-size:14px;font-weight:400;color:#e0f5f3!important;text-decoration:none;background:#0d94882e;border:1px solid #0d94884d;letter-spacing:1px }
.email-btn-table { margin:0 0 32px }
/* Fallback link */
.email-link-box { border:1px solid #0d94882e;background:#0d948808 }
.email-link-label { margin:0 0 8px;font-size:11px;color:#b4fceebf;font-weight:300 }
.email-link-url { margin:0;font-size:11px;font-family:monospace;color:#99f6e4;background:#00000033;padding:10px 12px;border:1px solid #0d948826;word-break:break-all }
/* Footer */
.email-footer { padding:32px 0 0;text-align:center }
.email-footer-text { margin:0;font-size:11px;color:#a0afc88c;font-weight:300 }
.email-footer-link { color:#5eead480;text-decoration:none;font-weight:400 }
/* Typography utilities */
.card-body h1,.card-body h2,.card-body h3,.card-body h4,.card-body h5,.card-body h6 { color:#ccfbf1!important }
.card-body a,.card-body a:link,.card-body a:visited { color:#5eead4!important;text-decoration:underline!important }
.card-body a:hover,.card-body a:active { color:#99f6e4!important }
.card-body li { color:#d2fcf5e0!important }
/* Responsive */
@media only screen and (max-width:480px) {
.email-card { padding:32px 20px!important }
.email-btn-row { padding:24px 20px!important }
.email-heading { font-size:20px!important }
}
</style>
</head>
<body style="margin:0;padding:0;background:#0a0815;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
<center style="width:100%;table-layout:fixed;">
<table width="100%" cellpadding="0" cellspacing="0" border="0" class="email-wrap">
<!-- Floating Title -->
<tr>
<td class="email-header">
<img src="{{AppUrl}}assets/img/favicon.png" alt="logo" width="24" height="24" class="email-logo" />
<span class="email-brand">{{AppName}}</span>
</td>
</tr>
<!-- Card -->
<tr>
<td class="email-card card-body">
<p class="email-heading">
{{.locale.Tr "mail.hi_user_x" .Invite.Email}}
</p>
<p class="email-text">
{{.locale.Tr "mail.team_invite.text_1" (DotEscape .Inviter.DisplayName) (DotEscape .Team.Name) (DotEscape .Organization.DisplayName)}}
</p>
<p class="email-subtext">
{{.locale.Tr "mail.team_invite.text_2"}}
</p>
</td>
</tr>
<!-- Divider -->
<tr>
<td class="email-divider"></td>
</tr>
<!-- Button & Link -->
<tr>
<td class="email-btn-row">
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="email-btn-table">
<tr>
<td align="center">
<a href="{{.InviteURL}}" class="email-btn">{{.locale.Tr "mail.view_it_on" AppName}}</a>
</td>
</tr>
</table>
<table width="100%" cellpadding="0" cellspacing="0" border="0" class="email-link-box">
<tr>
<td style="padding:16px 18px;">
<p class="email-link-label">{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
<p class="email-link-url">{{.InviteURL}}</p>
</td>
</tr>
</table>
<p class="email-subtext" style="margin-top:24px;font-size:12px;color:#a0afc880;">
{{.locale.Tr "mail.team_invite.text_3" .Invite.Email}}
</p>
</td>
</tr>
<!-- Footer -->
<tr>
<td class="email-footer">
<p class="email-footer-text">
&copy;
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
</p>
</td>
</tr>
</table>
</center>
</body>
</html>
@@ -1,141 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<title>
{{.Subject}}
</title>
<style type="text/css">
/* Base */
body,.ExternalClass,.ReadMsgBody { background-color:#0a0815 }
.email-wrap { max-width:600px;margin:40px auto;font-family:system-ui,-apple-system,sans-serif }
/* Header */
.email-logo { display:block;margin:0 auto 14px;border:0;outline:none;opacity:0.5 }
.email-brand { font-size:16px;font-weight:300;color:#ffffff66;letter-spacing:4px;text-transform:uppercase }
.email-header { padding:0 0 32px;text-align:center }
/* Card */
.email-card { padding:48px 40px;background:#191432cc;border:1px solid #0d948833;box-shadow:0 0 60px #0d948814,0 0 120px #6366f10a }
.email-heading { margin:0 0 20px;font-size:24px;font-weight:300;color:#ccfbf1;line-height:1.3;letter-spacing:-.5px }
.email-text { margin:0 0 24px;font-size:14px;line-height:2;color:#d2fcf5e0;font-weight:300 }
.email-subtext { margin:0;font-size:13px;line-height:1.8;color:#b4c3d7f2;font-weight:300 }
/* Divider */
.email-divider { height:1px;background:linear-gradient(90deg,transparent,#0d94884d,transparent) }
/* Button */
.email-btn-row { padding:40px 40px;text-align:center }
.email-btn { display:inline-block;padding:16px 48px;font-size:14px;font-weight:400;color:#e0f5f3!important;text-decoration:none;background:#0d94882e;border:1px solid #0d94884d;letter-spacing:1px }
.email-btn-table { margin:0 0 32px }
/* Fallback link */
.email-link-box { border:1px solid #0d94882e;background:#0d948808 }
.email-link-label { margin:0 0 8px;font-size:11px;color:#b4fceebf;font-weight:300 }
.email-link-url { margin:0;font-size:11px;font-family:monospace;color:#99f6e4;background:#00000033;padding:10px 12px;border:1px solid #0d948826;word-break:break-all }
/* Footer */
.email-footer { padding:32px 0 0;text-align:center }
.email-footer-text { margin:0;font-size:11px;color:#a0afc88c;font-weight:300 }
.email-footer-link { color:#5eead480;text-decoration:none;font-weight:400 }
/* Typography utilities */
.card-body h1,.card-body h2,.card-body h3,.card-body h4,.card-body h5,.card-body h6 { color:#ccfbf1!important }
.card-body a,.card-body a:link,.card-body a:visited { color:#5eead4!important;text-decoration:underline!important }
.card-body a:hover,.card-body a:active { color:#99f6e4!important }
.card-body li { color:#d2fcf5e0!important }
/* Responsive */
@media only screen and (max-width:480px) {
.email-card { padding:32px 20px!important }
.email-btn-row { padding:24px 20px!important }
.email-heading { font-size:20px!important }
}
</style>
</head>
<body style="margin:0;padding:0;background:#0a0815;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
<center style="width:100%;table-layout:fixed;">
<table width="100%" cellpadding="0" cellspacing="0" border="0" class="email-wrap">
<!-- Floating Title -->
<tr>
<td class="email-header">
<img src="{{AppUrl}}assets/img/favicon.png" alt="logo" width="24" height="24" class="email-logo" />
<span class="email-brand">{{AppName}}</span>
</td>
</tr>
<!-- Card -->
<tr>
<td class="email-card card-body">
<p class="email-heading"></p>
<p class="email-text">
<h2>{{.Repo.FullName}} - {{.Run.WorkflowID}}: {{.RunStatusText}}</h2>
<ul>
{{range $job := .Jobs}}
<li>
<a href="{{$job.HTMLURL}}">{{$job.Status}}: {{$job.Name}}{{if gt $job.Attempt 1}} (retry #{{$job.Attempt}}){{end}} - {{$job.Duration}}</a>
</li>
{{end}}
</ul>
</p>
<p class="email-subtext"></p>
</td>
</tr>
<!-- Divider -->
<tr>
<td class="email-divider"></td>
</tr>
<!-- Button & Link -->
<tr>
<td class="email-btn-row">
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="email-btn-table">
<tr>
<td align="center">
<a href="{{.Run.HTMLURL}}" class="email-btn">{{.locale.Tr "mail.view_it_on" AppName}}</a>
</td>
</tr>
</table>
<table width="100%" cellpadding="0" cellspacing="0" border="0" class="email-link-box">
<tr>
<td style="padding:16px 18px;">
<p class="email-link-label">{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
<p class="email-link-url">{{.Run.HTMLURL}}</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- Footer -->
<tr>
<td class="email-footer">
<p class="email-footer-text">
&copy;
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
</p>
</td>
</tr>
</table>
</center>
</body>
</html>
-134
View File
@@ -1,134 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<title>
{{.Subject}}
</title>
<style type="text/css">
/* Base */
body,.ExternalClass,.ReadMsgBody { background-color:#0a0815 }
.email-wrap { max-width:600px;margin:40px auto;font-family:system-ui,-apple-system,sans-serif }
/* Header */
.email-logo { display:block;margin:0 auto 14px;border:0;outline:none;opacity:0.5 }
.email-brand { font-size:16px;font-weight:300;color:#ffffff66;letter-spacing:4px;text-transform:uppercase }
.email-header { padding:0 0 32px;text-align:center }
/* Card */
.email-card { padding:48px 40px;background:#191432cc;border:1px solid #0d948833;box-shadow:0 0 60px #0d948814,0 0 120px #6366f10a }
.email-heading { margin:0 0 20px;font-size:24px;font-weight:300;color:#ccfbf1;line-height:1.3;letter-spacing:-.5px }
.email-text { margin:0 0 24px;font-size:14px;line-height:2;color:#d2fcf5e0;font-weight:300 }
.email-subtext { margin:0;font-size:13px;line-height:1.8;color:#b4c3d7f2;font-weight:300 }
/* Divider */
.email-divider { height:1px;background:linear-gradient(90deg,transparent,#0d94884d,transparent) }
/* Button */
.email-btn-row { padding:40px 40px;text-align:center }
.email-btn { display:inline-block;padding:16px 48px;font-size:14px;font-weight:400;color:#e0f5f3!important;text-decoration:none;background:#0d94882e;border:1px solid #0d94884d;letter-spacing:1px }
.email-btn-table { margin:0 0 32px }
/* Fallback link */
.email-link-box { border:1px solid #0d94882e;background:#0d948808 }
.email-link-label { margin:0 0 8px;font-size:11px;color:#b4fceebf;font-weight:300 }
.email-link-url { margin:0;font-size:11px;font-family:monospace;color:#99f6e4;background:#00000033;padding:10px 12px;border:1px solid #0d948826;word-break:break-all }
/* Footer */
.email-footer { padding:32px 0 0;text-align:center }
.email-footer-text { margin:0;font-size:11px;color:#a0afc88c;font-weight:300 }
.email-footer-link { color:#5eead480;text-decoration:none;font-weight:400 }
/* Typography utilities */
.card-body h1,.card-body h2,.card-body h3,.card-body h4,.card-body h5,.card-body h6 { color:#ccfbf1!important }
.card-body a,.card-body a:link,.card-body a:visited { color:#5eead4!important;text-decoration:underline!important }
.card-body a:hover,.card-body a:active { color:#99f6e4!important }
.card-body li { color:#d2fcf5e0!important }
/* Responsive */
@media only screen and (max-width:480px) {
.email-card { padding:32px 20px!important }
.email-btn-row { padding:24px 20px!important }
.email-heading { font-size:20px!important }
}
</style>
</head>
<body style="margin:0;padding:0;background:#0a0815;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
<center style="width:100%;table-layout:fixed;">
<table width="100%" cellpadding="0" cellspacing="0" border="0" class="email-wrap">
<!-- Floating Title -->
<tr>
<td class="email-header">
<img src="{{AppUrl}}assets/img/favicon.png" alt="logo" width="24" height="24" class="email-logo" />
<span class="email-brand">{{AppName}}</span>
</td>
</tr>
<!-- Card -->
<tr>
<td class="email-card card-body">
<p class="email-heading"></p>
<p class="email-text">
{{.locale.Tr "mail.repo.collaborator.added.text"}} <code>{{.RepoName}}</code>.
</p>
<p class="email-subtext"></p>
</td>
</tr>
<!-- Divider -->
<tr>
<td class="email-divider"></td>
</tr>
<!-- Button & Link -->
<tr>
<td class="email-btn-row">
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="email-btn-table">
<tr>
<td align="center">
<a href="{{.Link}}" class="email-btn">{{.locale.Tr "mail.view_it_on" AppName}}</a>
</td>
</tr>
</table>
<table width="100%" cellpadding="0" cellspacing="0" border="0" class="email-link-box">
<tr>
<td style="padding:16px 18px;">
<p class="email-link-label">{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
<p class="email-link-url">{{.Link}}</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- Footer -->
<tr>
<td class="email-footer">
<p class="email-footer-text">
&copy;
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
</p>
</td>
</tr>
</table>
</center>
</body>
</html>
-140
View File
@@ -1,140 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<title>
{{.Subject}}
</title>
<style type="text/css">
/* Base */
body,.ExternalClass,.ReadMsgBody { background-color:#0a0815 }
.email-wrap { max-width:600px;margin:40px auto;font-family:system-ui,-apple-system,sans-serif }
/* Header */
.email-logo { display:block;margin:0 auto 14px;border:0;outline:none;opacity:0.5 }
.email-brand { font-size:16px;font-weight:300;color:#ffffff66;letter-spacing:4px;text-transform:uppercase }
.email-header { padding:0 0 32px;text-align:center }
/* Card */
.email-card { padding:48px 40px;background:#191432cc;border:1px solid #0d948833;box-shadow:0 0 60px #0d948814,0 0 120px #6366f10a }
.email-heading { margin:0 0 20px;font-size:24px;font-weight:300;color:#ccfbf1;line-height:1.3;letter-spacing:-.5px }
.email-text { margin:0 0 24px;font-size:14px;line-height:2;color:#d2fcf5e0;font-weight:300 }
.email-subtext { margin:0;font-size:13px;line-height:1.8;color:#b4c3d7f2;font-weight:300 }
/* Divider */
.email-divider { height:1px;background:linear-gradient(90deg,transparent,#0d94884d,transparent) }
/* Button */
.email-btn-row { padding:40px 40px;text-align:center }
.email-btn { display:inline-block;padding:16px 48px;font-size:14px;font-weight:400;color:#e0f5f3!important;text-decoration:none;background:#0d94882e;border:1px solid #0d94884d;letter-spacing:1px }
.email-btn-table { margin:0 0 32px }
/* Fallback link */
.email-link-box { border:1px solid #0d94882e;background:#0d948808 }
.email-link-label { margin:0 0 8px;font-size:11px;color:#b4fceebf;font-weight:300 }
.email-link-url { margin:0;font-size:11px;font-family:monospace;color:#99f6e4;background:#00000033;padding:10px 12px;border:1px solid #0d948826;word-break:break-all }
/* Footer */
.email-footer { padding:32px 0 0;text-align:center }
.email-footer-text { margin:0;font-size:11px;color:#a0afc88c;font-weight:300 }
.email-footer-link { color:#5eead480;text-decoration:none;font-weight:400 }
/* Typography utilities */
.card-body h1,.card-body h2,.card-body h3,.card-body h4,.card-body h5,.card-body h6 { color:#ccfbf1!important }
.card-body a,.card-body a:link,.card-body a:visited { color:#5eead4!important;text-decoration:underline!important }
.card-body a:hover,.card-body a:active { color:#99f6e4!important }
.card-body li { color:#d2fcf5e0!important }
/* Responsive */
@media only screen and (max-width:480px) {
.email-card { padding:32px 20px!important }
.email-btn-row { padding:24px 20px!important }
.email-heading { font-size:20px!important }
}
</style>
</head>
<body style="margin:0;padding:0;background:#0a0815;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
<center style="width:100%;table-layout:fixed;">
<table width="100%" cellpadding="0" cellspacing="0" border="0" class="email-wrap">
<!-- Floating Title -->
<tr>
<td class="email-header">
<img src="{{AppUrl}}assets/img/favicon.png" alt="logo" width="24" height="24" class="email-logo" />
<span class="email-brand">{{AppName}}</span>
</td>
</tr>
<!-- Card -->
<tr>
<td class="email-card card-body">
<p class="email-heading"></p>
<p class="email-text">
{{$repo_url := HTMLFormat "<a href=\047%s\047>%s</a>" .Issue.Repo.HTMLURL .Issue.Repo.FullName}}
{{$link := HTMLFormat "<a href=\047%s\047>#%d</a>" .Link .Issue.Index}}
{{if .IsPull}}
{{.locale.Tr "mail.issue_assigned.pull" .Doer.Name $link $repo_url}}
{{else}}
{{.locale.Tr "mail.issue_assigned.issue" .Doer.Name $link $repo_url}}
{{end}}
</p>
<p class="email-subtext"></p>
</td>
</tr>
<!-- Divider -->
<tr>
<td class="email-divider"></td>
</tr>
<!-- Button & Link -->
<tr>
<td class="email-btn-row">
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="email-btn-table">
<tr>
<td align="center">
<a href="{{.Link}}" class="email-btn">{{.locale.Tr "mail.view_it_on" AppName}}</a>
</td>
</tr>
</table>
<table width="100%" cellpadding="0" cellspacing="0" border="0" class="email-link-box">
<tr>
<td style="padding:16px 18px;">
<p class="email-link-label">{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
<p class="email-link-url">{{.Link}}</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- Footer -->
<tr>
<td class="email-footer">
<p class="email-footer-text">
&copy;
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
</p>
</td>
</tr>
</table>
</center>
</body>
</html>
-209
View File
@@ -1,209 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<title>
{{.Subject}}
</title>
<style type="text/css">
/* Base */
body,.ExternalClass,.ReadMsgBody { background-color:#0a0815 }
.email-wrap { max-width:600px;margin:40px auto;font-family:system-ui,-apple-system,sans-serif }
/* Header */
.email-logo { display:block;margin:0 auto 14px;border:0;outline:none;opacity:0.5 }
.email-brand { font-size:16px;font-weight:300;color:#ffffff66;letter-spacing:4px;text-transform:uppercase }
.email-header { padding:0 0 32px;text-align:center }
/* Card */
.email-card { padding:48px 40px;background:#191432cc;border:1px solid #0d948833;box-shadow:0 0 60px #0d948814,0 0 120px #6366f10a }
.email-heading { margin:0 0 20px;font-size:24px;font-weight:300;color:#ccfbf1;line-height:1.3;letter-spacing:-.5px }
.email-text { margin:0 0 24px;font-size:14px;line-height:2;color:#d2fcf5e0;font-weight:300 }
.email-subtext { margin:0;font-size:13px;line-height:1.8;color:#b4c3d7f2;font-weight:300 }
/* Divider */
.email-divider { height:1px;background:linear-gradient(90deg,transparent,#0d94884d,transparent) }
/* Button */
.email-btn-row { padding:40px 40px;text-align:center }
.email-btn { display:inline-block;padding:16px 48px;font-size:14px;font-weight:400;color:#e0f5f3!important;text-decoration:none;background:#0d94882e;border:1px solid #0d94884d;letter-spacing:1px }
.email-btn-table { margin:0 0 32px }
/* Fallback link */
.email-link-box { border:1px solid #0d94882e;background:#0d948808 }
.email-link-label { margin:0 0 8px;font-size:11px;color:#b4fceebf;font-weight:300 }
.email-link-url { margin:0;font-size:11px;font-family:monospace;color:#99f6e4;background:#00000033;padding:10px 12px;border:1px solid #0d948826;word-break:break-all }
/* Footer */
.email-footer { padding:32px 0 0;text-align:center }
.email-footer-text { margin:0;font-size:11px;color:#a0afc88c;font-weight:300 }
.email-footer-link { color:#5eead480;text-decoration:none;font-weight:400 }
/* Typography utilities */
.card-body h1,.card-body h2,.card-body h3,.card-body h4,.card-body h5,.card-body h6 { color:#ccfbf1!important }
.card-body a,.card-body a:link,.card-body a:visited { color:#5eead4!important;text-decoration:underline!important }
.card-body a:hover,.card-body a:active { color:#99f6e4!important }
.card-body li { color:#d2fcf5e0!important }
/* Content */
.aurora-content,.aurora-content * { color:#d2fcf5e0!important }
.aurora-content a,.aurora-content a:link,.aurora-content a:visited { color:#5eead4!important;text-decoration:underline!important }
.aurora-content a:hover,.aurora-content a:active { color:#99f6e4!important }
.aurora-content code,.aurora-content pre { color:#99f6e4!important;background:#0000004d!important;border-color:#0d948833!important }
.aurora-content blockquote { color:#ffffff99!important }
.aurora-content h1,.aurora-content h2,.aurora-content h3,.aurora-content h4,.aurora-content h5,.aurora-content h6 { color:#ccfbf1!important }
.aurora-content hr { border-color:#0d94884d!important }
.aurora-content img { border:0;max-width:100%!important }
/* Responsive */
@media only screen and (max-width:480px) {
.email-card { padding:32px 20px!important }
.email-btn-row { padding:24px 20px!important }
.email-heading { font-size:20px!important }
}
</style>
</head>
<body style="margin:0;padding:0;background:#0a0815;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
<center style="width:100%;table-layout:fixed;">
<table width="100%" cellpadding="0" cellspacing="0" border="0" class="email-wrap">
<!-- Floating Title -->
<tr>
<td class="email-header">
<img src="{{AppUrl}}assets/img/favicon.png" alt="logo" width="24" height="24" class="email-logo" />
<span class="email-brand">{{AppName}}</span>
</td>
</tr>
<!-- Card -->
<tr>
<td class="email-card card-body">
<p class="email-heading"></p>
<p class="email-text">
{{if .IsMention}}
<p>{{.locale.Tr "mail.issue.x_mentioned_you" .Doer.Name}}</p>
{{end}}
{{if eq .ActionName "push"}}
<p>
{{if .Comment.IsForcePush}}
{{$oldCommitUrl := printf "%s/commit/%s" .Comment.Issue.PullRequest.BaseRepo.HTMLURL .Comment.OldCommit}}
{{$oldShortSha := ShortSha .Comment.OldCommit}}
{{$oldCommitLink := HTMLFormat "<a href=\047%[1]s\047>%[2]s</a>" $oldCommitUrl $oldShortSha}}
{{$newCommitUrl := printf "%s/commit/%s" .Comment.Issue.PullRequest.BaseRepo.HTMLURL .Comment.NewCommit}}
{{$newShortSha := ShortSha .Comment.NewCommit}}
{{$newCommitLink := HTMLFormat "<a href=\047%[1]s\047>%[2]s</a>" $newCommitUrl $newShortSha}}
{{.locale.Tr "mail.issue.action.force_push" .Doer.Name .Comment.Issue.PullRequest.HeadBranch $oldCommitLink $newCommitLink}}
{{else}}
{{.locale.TrN (len .Comment.Commits) "mail.issue.action.push_1" "mail.issue.action.push_n" .Doer.Name .Comment.Issue.PullRequest.HeadBranch (len .Comment.Commits)}}
{{end}}
</p>
{{end}}
<p>
{{if eq .ActionName "close"}}
{{.locale.Tr "mail.issue.action.close" .Doer.Name .Issue.Index}}
{{else if eq .ActionName "reopen"}}
{{.locale.Tr "mail.issue.action.reopen" .Doer.Name .Issue.Index}}
{{else if eq .ActionName "merge"}}
{{.locale.Tr "mail.issue.action.merge" .Doer.Name .Issue.Index .Issue.PullRequest.BaseBranch}}
{{else if eq .ActionName "approve"}}
{{.locale.Tr "mail.issue.action.approve" .Doer.Name}}
{{else if eq .ActionName "reject"}}
{{.locale.Tr "mail.issue.action.reject" .Doer.Name}}
{{else if eq .ActionName "review"}}
{{.locale.Tr "mail.issue.action.review" .Doer.Name}}
{{else if eq .ActionName "review_dismissed"}}
{{.locale.Tr "mail.issue.action.review_dismissed" .Doer.Name .Comment.Review.Reviewer.Name}}
{{else if eq .ActionName "ready_for_review"}}
{{.locale.Tr "mail.issue.action.ready_for_review" .Doer.Name}}
{{end}}
{{- if eq .Body ""}}
{{if eq .ActionName "new"}}
{{.locale.Tr "mail.issue.action.new" .Doer.Name .Issue.Index}}
{{end}}
{{else}}
<span class="aurora-content">{{.Body}}</span>
{{end -}}
{{if .CanReply}}
&nbsp;{{.locale.Tr "mail.reply"}}
{{end}}
</p>
{{- range .ReviewComments}}
<hr>
<p>{{$.locale.Tr "mail.issue.in_tree_path" .TreePath}}</p>
<pre>{{.Patch}}</pre>
<div class="aurora-content">{{.RenderedContent}}</div>
{{end -}}
{{if eq .ActionName "push"}}
<ul>
{{range .Comment.Commits}}
<li>
<a href="{{$.Comment.Issue.PullRequest.BaseRepo.HTMLURL}}/commit/{{.UserCommit.GitCommit.ID}}">{{ShortSha .UserCommit.GitCommit.ID.String}}</a> - {{.UserCommit.GitCommit.MessageTitle}}
</li>
{{end}}
</ul>
{{end}}
</p>
<p class="email-subtext"></p>
</td>
</tr>
<!-- Divider -->
<tr>
<td class="email-divider"></td>
</tr>
<!-- Button & Link -->
<tr>
<td class="email-btn-row">
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="email-btn-table">
<tr>
<td align="center">
<a href="{{.Link}}" class="email-btn">{{.locale.Tr "mail.view_it_on" AppName}}</a>
</td>
</tr>
</table>
<table width="100%" cellpadding="0" cellspacing="0" border="0" class="email-link-box">
<tr>
<td style="padding:16px 18px;">
<p class="email-link-label">{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
<p class="email-link-url">{{.Link}}</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- Footer -->
<tr>
<td class="email-footer">
<p class="email-footer-text">
&copy;
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
</p>
</td>
</tr>
</table>
</center>
</body>
</html>
-166
View File
@@ -1,166 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<title>
{{.Subject}}
</title>
<style type="text/css">
/* Base */
body,.ExternalClass,.ReadMsgBody { background-color:#0a0815 }
.email-wrap { max-width:600px;margin:40px auto;font-family:system-ui,-apple-system,sans-serif }
/* Header */
.email-logo { display:block;margin:0 auto 14px;border:0;outline:none;opacity:0.5 }
.email-brand { font-size:16px;font-weight:300;color:#ffffff66;letter-spacing:4px;text-transform:uppercase }
.email-header { padding:0 0 32px;text-align:center }
/* Card */
.email-card { padding:48px 40px;background:#191432cc;border:1px solid #0d948833;box-shadow:0 0 60px #0d948814,0 0 120px #6366f10a }
.email-heading { margin:0 0 20px;font-size:24px;font-weight:300;color:#ccfbf1;line-height:1.3;letter-spacing:-.5px }
.email-text { margin:0 0 24px;font-size:14px;line-height:2;color:#d2fcf5e0;font-weight:300 }
.email-subtext { margin:0;font-size:13px;line-height:1.8;color:#b4c3d7f2;font-weight:300 }
/* Divider */
.email-divider { height:1px;background:linear-gradient(90deg,transparent,#0d94884d,transparent) }
/* Button */
.email-btn-row { padding:40px 40px;text-align:center }
.email-btn { display:inline-block;padding:16px 48px;font-size:14px;font-weight:400;color:#e0f5f3!important;text-decoration:none;background:#0d94882e;border:1px solid #0d94884d;letter-spacing:1px }
.email-btn-table { margin:0 0 32px }
/* Fallback link */
.email-link-box { border:1px solid #0d94882e;background:#0d948808 }
.email-link-label { margin:0 0 8px;font-size:11px;color:#b4fceebf;font-weight:300 }
.email-link-url { margin:0;font-size:11px;font-family:monospace;color:#99f6e4;background:#00000033;padding:10px 12px;border:1px solid #0d948826;word-break:break-all }
/* Footer */
.email-footer { padding:32px 0 0;text-align:center }
.email-footer-text { margin:0;font-size:11px;color:#a0afc88c;font-weight:300 }
.email-footer-link { color:#5eead480;text-decoration:none;font-weight:400 }
/* Typography utilities */
.card-body h1,.card-body h2,.card-body h3,.card-body h4,.card-body h5,.card-body h6 { color:#ccfbf1!important }
.card-body a,.card-body a:link,.card-body a:visited { color:#5eead4!important;text-decoration:underline!important }
.card-body a:hover,.card-body a:active { color:#99f6e4!important }
.card-body li { color:#d2fcf5e0!important }
/* Content */
.aurora-content,.aurora-content * { color:#d2fcf5e0!important }
.aurora-content a,.aurora-content a:link,.aurora-content a:visited { color:#5eead4!important;text-decoration:underline!important }
.aurora-content a:hover,.aurora-content a:active { color:#99f6e4!important }
.aurora-content code,.aurora-content pre { color:#99f6e4!important;background:#0000004d!important;border-color:#0d948833!important }
.aurora-content blockquote { color:#ffffff99!important }
.aurora-content h1,.aurora-content h2,.aurora-content h3,.aurora-content h4,.aurora-content h5,.aurora-content h6 { color:#ccfbf1!important }
.aurora-content hr { border-color:#0d94884d!important }
.aurora-content img { border:0;max-width:100%!important }
/* Responsive */
@media only screen and (max-width:480px) {
.email-card { padding:32px 20px!important }
.email-btn-row { padding:24px 20px!important }
.email-heading { font-size:20px!important }
}
</style>
</head>
<body style="margin:0;padding:0;background:#0a0815;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
<center style="width:100%;table-layout:fixed;">
<table width="100%" cellpadding="0" cellspacing="0" border="0" class="email-wrap">
<!-- Floating Title -->
<tr>
<td class="email-header">
<img src="{{AppUrl}}assets/img/favicon.png" alt="logo" width="24" height="24" class="email-logo" />
<span class="email-brand">{{AppName}}</span>
</td>
</tr>
<!-- Card -->
<tr>
<td class="email-card card-body">
<p class="email-heading"></p>
<p class="email-text">
{{$release_url := HTMLFormat "<a href=\047%s\047>%s</a>" .Release.HTMLURL .Release.TagName}}
{{$repo_url := HTMLFormat "<a href=\047%s\047>%s</a>" .Release.Repo.HTMLURL .Release.Repo.FullName}}
{{.locale.Tr "mail.release.new.text" .Release.Publisher.Name $release_url $repo_url}}
<h4>{{.locale.Tr "mail.release.title" .Release.Title}}</h4>
{{if .Release.RenderedNote}}
<p class="email-text">{{.locale.Tr "mail.release.note"}}</p>
<div class="aurora-content" style="font-size:14px;line-height:2;">{{.Release.RenderedNote}}</div>
{{end}}
<p class="email-text">{{.locale.Tr "mail.release.downloads"}}</p>
<ul>
{{if not .DisableDownloadSourceArchives}}
<li><a href="{{.Release.Repo.HTMLURL}}/archive/{{.Release.TagName | PathEscapeSegments}}.zip">{{.locale.Tr "mail.release.download.zip"}}</a></li>
<li><a href="{{.Release.Repo.HTMLURL}}/archive/{{.Release.TagName | PathEscapeSegments}}.tar.gz">{{.locale.Tr "mail.release.download.targz"}}</a></li>
{{end}}
{{if .Release.Attachments}}
{{range .Release.Attachments}}
<li><a href="{{.DownloadURL}}">{{.Name}} ({{.Size | FileSize}})</a></li>
{{end}}
{{end}}
</ul>
</p>
<p class="email-subtext"></p>
</td>
</tr>
<!-- Divider -->
<tr>
<td class="email-divider"></td>
</tr>
<!-- Button & Link -->
<tr>
<td class="email-btn-row">
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="email-btn-table">
<tr>
<td align="center">
<a href="{{.Link}}" class="email-btn">{{.locale.Tr "mail.view_it_on" AppName}}</a>
</td>
</tr>
</table>
<table width="100%" cellpadding="0" cellspacing="0" border="0" class="email-link-box">
<tr>
<td style="padding:16px 18px;">
<p class="email-link-label">{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
<p class="email-link-url">{{.Link}}</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- Footer -->
<tr>
<td class="email-footer">
<p class="email-footer-text">
&copy;
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
</p>
</td>
</tr>
</table>
</center>
</body>
</html>
-135
View File
@@ -1,135 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<title>
{{.Subject}}
</title>
<style type="text/css">
/* Base */
body,.ExternalClass,.ReadMsgBody { background-color:#0a0815 }
.email-wrap { max-width:600px;margin:40px auto;font-family:system-ui,-apple-system,sans-serif }
/* Header */
.email-logo { display:block;margin:0 auto 14px;border:0;outline:none;opacity:0.5 }
.email-brand { font-size:16px;font-weight:300;color:#ffffff66;letter-spacing:4px;text-transform:uppercase }
.email-header { padding:0 0 32px;text-align:center }
/* Card */
.email-card { padding:48px 40px;background:#191432cc;border:1px solid #0d948833;box-shadow:0 0 60px #0d948814,0 0 120px #6366f10a }
.email-heading { margin:0 0 20px;font-size:24px;font-weight:300;color:#ccfbf1;line-height:1.3;letter-spacing:-.5px }
.email-text { margin:0 0 24px;font-size:14px;line-height:2;color:#d2fcf5e0;font-weight:300 }
.email-subtext { margin:0;font-size:13px;line-height:1.8;color:#b4c3d7f2;font-weight:300 }
/* Divider */
.email-divider { height:1px;background:linear-gradient(90deg,transparent,#0d94884d,transparent) }
/* Button */
.email-btn-row { padding:40px 40px;text-align:center }
.email-btn { display:inline-block;padding:16px 48px;font-size:14px;font-weight:400;color:#e0f5f3!important;text-decoration:none;background:#0d94882e;border:1px solid #0d94884d;letter-spacing:1px }
.email-btn-table { margin:0 0 32px }
/* Fallback link */
.email-link-box { border:1px solid #0d94882e;background:#0d948808 }
.email-link-label { margin:0 0 8px;font-size:11px;color:#b4fceebf;font-weight:300 }
.email-link-url { margin:0;font-size:11px;font-family:monospace;color:#99f6e4;background:#00000033;padding:10px 12px;border:1px solid #0d948826;word-break:break-all }
/* Footer */
.email-footer { padding:32px 0 0;text-align:center }
.email-footer-text { margin:0;font-size:11px;color:#a0afc88c;font-weight:300 }
.email-footer-link { color:#5eead480;text-decoration:none;font-weight:400 }
/* Typography utilities */
.card-body h1,.card-body h2,.card-body h3,.card-body h4,.card-body h5,.card-body h6 { color:#ccfbf1!important }
.card-body a,.card-body a:link,.card-body a:visited { color:#5eead4!important;text-decoration:underline!important }
.card-body a:hover,.card-body a:active { color:#99f6e4!important }
.card-body li { color:#d2fcf5e0!important }
/* Responsive */
@media only screen and (max-width:480px) {
.email-card { padding:32px 20px!important }
.email-btn-row { padding:24px 20px!important }
.email-heading { font-size:20px!important }
}
</style>
</head>
<body style="margin:0;padding:0;background:#0a0815;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
<center style="width:100%;table-layout:fixed;">
<table width="100%" cellpadding="0" cellspacing="0" border="0" class="email-wrap">
<!-- Floating Title -->
<tr>
<td class="email-header">
<img src="{{AppUrl}}assets/img/favicon.png" alt="logo" width="24" height="24" class="email-logo" />
<span class="email-brand">{{AppName}}</span>
</td>
</tr>
<!-- Card -->
<tr>
<td class="email-card card-body">
<p class="email-heading"></p>
<p class="email-text">
{{$url := HTMLFormat "<a href=\047%[1]s\047>%[2]s</a>" .Link .Repo}}
{{.Subject}}. {{.locale.Tr "mail.repo.transfer.body" $url}}
</p>
<p class="email-subtext"></p>
</td>
</tr>
<!-- Divider -->
<tr>
<td class="email-divider"></td>
</tr>
<!-- Button & Link -->
<tr>
<td class="email-btn-row">
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="email-btn-table">
<tr>
<td align="center">
<a href="{{.Link}}" class="email-btn">{{.locale.Tr "mail.view_it_on" AppName}}</a>
</td>
</tr>
</table>
<table width="100%" cellpadding="0" cellspacing="0" border="0" class="email-link-box">
<tr>
<td style="padding:16px 18px;">
<p class="email-link-label">{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
<p class="email-link-url">{{.Link}}</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- Footer -->
<tr>
<td class="email-footer">
<p class="email-footer-text">
&copy;
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
</p>
</td>
</tr>
</table>
</center>
</body>
</html>
-136
View File
@@ -1,136 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<title>
{{.locale.Tr "mail.activate_account.title" (.DisplayName|DotEscape)}}
</title>
<style type="text/css">
/* Base */
body,.ExternalClass,.ReadMsgBody { background-color:#0a0815 }
.email-wrap { max-width:600px;margin:40px auto;font-family:system-ui,-apple-system,sans-serif }
/* Header */
.email-logo { display:block;margin:0 auto 14px;border:0;outline:none;opacity:0.5 }
.email-brand { font-size:16px;font-weight:300;color:#ffffff66;letter-spacing:4px;text-transform:uppercase }
.email-header { padding:0 0 32px;text-align:center }
/* Card */
.email-card { padding:48px 40px;background:#191432cc;border:1px solid #0d948833;box-shadow:0 0 60px #0d948814,0 0 120px #6366f10a }
.email-heading { margin:0 0 20px;font-size:24px;font-weight:300;color:#ccfbf1;line-height:1.3;letter-spacing:-.5px }
.email-text { margin:0 0 24px;font-size:14px;line-height:2;color:#d2fcf5e0;font-weight:300 }
.email-subtext { margin:0;font-size:13px;line-height:1.8;color:#b4c3d7f2;font-weight:300 }
/* Divider */
.email-divider { height:1px;background:linear-gradient(90deg,transparent,#0d94884d,transparent) }
/* Button */
.email-btn-row { padding:40px 40px;text-align:center }
.email-btn { display:inline-block;padding:16px 48px;font-size:14px;font-weight:400;color:#e0f5f3!important;text-decoration:none;background:#0d94882e;border:1px solid #0d94884d;letter-spacing:1px }
.email-btn-table { margin:0 0 32px }
/* Fallback link */
.email-link-box { border:1px solid #0d94882e;background:#0d948808 }
.email-link-label { margin:0 0 8px;font-size:11px;color:#b4fceebf;font-weight:300 }
.email-link-url { margin:0;font-size:11px;font-family:monospace;color:#99f6e4;background:#00000033;padding:10px 12px;border:1px solid #0d948826;word-break:break-all }
/* Footer */
.email-footer { padding:32px 0 0;text-align:center }
.email-footer-text { margin:0;font-size:11px;color:#a0afc88c;font-weight:300 }
.email-footer-link { color:#5eead480;text-decoration:none;font-weight:400 }
/* Typography utilities */
.card-body h1,.card-body h2,.card-body h3,.card-body h4,.card-body h5,.card-body h6 { color:#ccfbf1!important }
.card-body a,.card-body a:link,.card-body a:visited { color:#5eead4!important;text-decoration:underline!important }
.card-body a:hover,.card-body a:active { color:#99f6e4!important }
.card-body li { color:#d2fcf5e0!important }
/* Responsive */
@media only screen and (max-width:480px) {
.email-card { padding:32px 20px!important }
.email-btn-row { padding:24px 20px!important }
.email-heading { font-size:20px!important }
}
</style>
</head>
<body style="margin:0;padding:0;background:#0a0815;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
<center style="width:100%;table-layout:fixed;">
<table width="100%" cellpadding="0" cellspacing="0" border="0" class="email-wrap">
<!-- Floating Title -->
<tr>
<td class="email-header">
<img src="{{AppUrl}}assets/img/favicon.png" alt="logo" width="24" height="24" class="email-logo" />
<span class="email-brand">{{AppName}}</span>
</td>
</tr>
<!-- Card -->
<tr>
<td class="email-card card-body">
<p class="email-text">
{{.locale.Tr "mail.activate_account.text_1" (.DisplayName|DotEscape) AppName}}
</p>
<p class="email-subtext">
{{.locale.Tr "mail.activate_account.text_2" .ActiveCodeLives}}
</p>
</td>
</tr>
<!-- Divider -->
<tr>
<td class="email-divider"></td>
</tr>
<!-- Button & Link -->
<tr>
<td class="email-btn-row">
{{$activate_url := printf "%suser/activate?code=%s" AppUrl (QueryEscape .Code)}}
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="email-btn-table">
<tr>
<td align="center">
<a href="{{$activate_url}}" class="email-btn">{{.locale.Tr "mail.activate_account"}}</a>
</td>
</tr>
</table>
<table width="100%" cellpadding="0" cellspacing="0" border="0" class="email-link-box">
<tr>
<td style="padding:16px 18px;">
<p class="email-link-label">{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
<p class="email-link-url">{{$activate_url}}</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- Footer -->
<tr>
<td class="email-footer">
<p class="email-footer-text">
&copy;
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
</p>
</td>
</tr>
</table>
</center>
</body>
</html>
@@ -1,138 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<title>
{{.locale.Tr "mail.activate_email.title" (.DisplayName|DotEscape)}}
</title>
<style type="text/css">
/* Base */
body,.ExternalClass,.ReadMsgBody { background-color:#0a0815 }
.email-wrap { max-width:600px;margin:40px auto;font-family:system-ui,-apple-system,sans-serif }
/* Header */
.email-logo { display:block;margin:0 auto 14px;border:0;outline:none;opacity:0.5 }
.email-brand { font-size:16px;font-weight:300;color:#ffffff66;letter-spacing:4px;text-transform:uppercase }
.email-header { padding:0 0 32px;text-align:center }
/* Card */
.email-card { padding:48px 40px;background:#191432cc;border:1px solid #0d948833;box-shadow:0 0 60px #0d948814,0 0 120px #6366f10a }
.email-heading { margin:0 0 20px;font-size:24px;font-weight:300;color:#ccfbf1;line-height:1.3;letter-spacing:-.5px }
.email-text { margin:0 0 24px;font-size:14px;line-height:2;color:#d2fcf5e0;font-weight:300 }
.email-subtext { margin:0;font-size:13px;line-height:1.8;color:#b4c3d7f2;font-weight:300 }
/* Divider */
.email-divider { height:1px;background:linear-gradient(90deg,transparent,#0d94884d,transparent) }
/* Button */
.email-btn-row { padding:40px 40px;text-align:center }
.email-btn { display:inline-block;padding:16px 48px;font-size:14px;font-weight:400;color:#e0f5f3!important;text-decoration:none;background:#0d94882e;border:1px solid #0d94884d;letter-spacing:1px }
.email-btn-table { margin:0 0 32px }
/* Fallback link */
.email-link-box { border:1px solid #0d94882e;background:#0d948808 }
.email-link-label { margin:0 0 8px;font-size:11px;color:#b4fceebf;font-weight:300 }
.email-link-url { margin:0;font-size:11px;font-family:monospace;color:#99f6e4;background:#00000033;padding:10px 12px;border:1px solid #0d948826;word-break:break-all }
/* Footer */
.email-footer { padding:32px 0 0;text-align:center }
.email-footer-text { margin:0;font-size:11px;color:#a0afc88c;font-weight:300 }
.email-footer-link { color:#5eead480;text-decoration:none;font-weight:400 }
/* Typography utilities */
.card-body h1,.card-body h2,.card-body h3,.card-body h4,.card-body h5,.card-body h6 { color:#ccfbf1!important }
.card-body a,.card-body a:link,.card-body a:visited { color:#5eead4!important;text-decoration:underline!important }
.card-body a:hover,.card-body a:active { color:#99f6e4!important }
.card-body li { color:#d2fcf5e0!important }
/* Responsive */
@media only screen and (max-width:480px) {
.email-card { padding:32px 20px!important }
.email-btn-row { padding:24px 20px!important }
.email-heading { font-size:20px!important }
}
</style>
</head>
<body style="margin:0;padding:0;background:#0a0815;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
<center style="width:100%;table-layout:fixed;">
<table width="100%" cellpadding="0" cellspacing="0" border="0" class="email-wrap">
<!-- Floating Title -->
<tr>
<td class="email-header">
<img src="{{AppUrl}}assets/img/favicon.png" alt="logo" width="24" height="24" class="email-logo" />
<span class="email-brand">{{AppName}}</span>
</td>
</tr>
<!-- Card -->
<tr>
<td class="email-card card-body">
<p class="email-heading">
{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape)}}
</p>
<p class="email-text">
{{.locale.Tr "mail.activate_email.text" .ActiveCodeLives}}
</p>
<p class="email-subtext"></p>
</td>
</tr>
<!-- Divider -->
<tr>
<td class="email-divider"></td>
</tr>
<!-- Button & Link -->
<tr>
<td class="email-btn-row">
{{$activate_url := printf "%suser/activate_email?code=%s&email=%s" AppUrl (QueryEscape .Code) (QueryEscape .Email)}}
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="email-btn-table">
<tr>
<td align="center">
<a href="{{$activate_url}}" class="email-btn">{{.locale.Tr "mail.activate_email"}}</a>
</td>
</tr>
</table>
<table width="100%" cellpadding="0" cellspacing="0" border="0" class="email-link-box">
<tr>
<td style="padding:16px 18px;">
<p class="email-link-label">{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
<p class="email-link-url">{{$activate_url}}</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- Footer -->
<tr>
<td class="email-footer">
<p class="email-footer-text">
&copy;
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
</p>
</td>
</tr>
</table>
</center>
</body>
</html>
@@ -1,140 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<title>
{{.locale.Tr "mail.register_notify.title" (.DisplayName|DotEscape) AppName}}
</title>
<style type="text/css">
/* Base */
body,.ExternalClass,.ReadMsgBody { background-color:#0a0815 }
.email-wrap { max-width:600px;margin:40px auto;font-family:system-ui,-apple-system,sans-serif }
/* Header */
.email-logo { display:block;margin:0 auto 14px;border:0;outline:none;opacity:0.5 }
.email-brand { font-size:16px;font-weight:300;color:#ffffff66;letter-spacing:4px;text-transform:uppercase }
.email-header { padding:0 0 32px;text-align:center }
/* Card */
.email-card { padding:48px 40px;background:#191432cc;border:1px solid #0d948833;box-shadow:0 0 60px #0d948814,0 0 120px #6366f10a }
.email-heading { margin:0 0 20px;font-size:24px;font-weight:300;color:#ccfbf1;line-height:1.3;letter-spacing:-.5px }
.email-text { margin:0 0 24px;font-size:14px;line-height:2;color:#d2fcf5e0;font-weight:300 }
.email-subtext { margin:0;font-size:13px;line-height:1.8;color:#b4c3d7f2;font-weight:300 }
/* Divider */
.email-divider { height:1px;background:linear-gradient(90deg,transparent,#0d94884d,transparent) }
/* Button */
.email-btn-row { padding:40px 40px;text-align:center }
.email-btn { display:inline-block;padding:16px 48px;font-size:14px;font-weight:400;color:#e0f5f3!important;text-decoration:none;background:#0d94882e;border:1px solid #0d94884d;letter-spacing:1px }
.email-btn-table { margin:0 0 32px }
/* Fallback link */
.email-link-box { border:1px solid #0d94882e;background:#0d948808 }
.email-link-label { margin:0 0 8px;font-size:11px;color:#b4fceebf;font-weight:300 }
.email-link-url { margin:0;font-size:11px;font-family:monospace;color:#99f6e4;background:#00000033;padding:10px 12px;border:1px solid #0d948826;word-break:break-all }
/* Footer */
.email-footer { padding:32px 0 0;text-align:center }
.email-footer-text { margin:0;font-size:11px;color:#a0afc88c;font-weight:300 }
.email-footer-link { color:#5eead480;text-decoration:none;font-weight:400 }
/* Typography utilities */
.card-body h1,.card-body h2,.card-body h3,.card-body h4,.card-body h5,.card-body h6 { color:#ccfbf1!important }
.card-body a,.card-body a:link,.card-body a:visited { color:#5eead4!important;text-decoration:underline!important }
.card-body a:hover,.card-body a:active { color:#99f6e4!important }
.card-body li { color:#d2fcf5e0!important }
/* Responsive */
@media only screen and (max-width:480px) {
.email-card { padding:32px 20px!important }
.email-btn-row { padding:24px 20px!important }
.email-heading { font-size:20px!important }
}
</style>
</head>
<body style="margin:0;padding:0;background:#0a0815;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
<center style="width:100%;table-layout:fixed;">
<table width="100%" cellpadding="0" cellspacing="0" border="0" class="email-wrap">
<!-- Floating Title -->
<tr>
<td class="email-header">
<img src="{{AppUrl}}assets/img/favicon.png" alt="logo" width="24" height="24" class="email-logo" />
<span class="email-brand">{{AppName}}</span>
</td>
</tr>
<!-- Card -->
<tr>
<td class="email-card card-body">
<p class="email-heading">
{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape)}}
</p>
<p class="email-text">
{{.locale.Tr "mail.register_notify.text_1" AppName}}
</p>
<p class="email-subtext">
{{.locale.Tr "mail.register_notify.text_2" .Username}}
{{$set_pwd_url := printf "%suser/forgot_password" AppUrl}}
{{.locale.Tr "mail.register_notify.text_3" $set_pwd_url}}
</p>
</td>
</tr>
<!-- Divider -->
<tr>
<td class="email-divider"></td>
</tr>
<!-- Button & Link -->
<tr>
<td class="email-btn-row">
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="email-btn-table">
<tr>
<td align="center">
<a href="{{AppUrl}}user/login" class="email-btn">{{.locale.Tr "mail.view_it_on" AppName}}</a>
</td>
</tr>
</table>
<table width="100%" cellpadding="0" cellspacing="0" border="0" class="email-link-box">
<tr>
<td style="padding:16px 18px;">
<p class="email-link-label">{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
<p class="email-link-url">{{AppUrl}}user/login</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- Footer -->
<tr>
<td class="email-footer">
<p class="email-footer-text">
&copy;
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
</p>
</td>
</tr>
</table>
</center>
</body>
</html>
@@ -1,138 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<title>
{{.locale.Tr "mail.reset_password.title" (.DisplayName|DotEscape)}}
</title>
<style type="text/css">
/* Base */
body,.ExternalClass,.ReadMsgBody { background-color:#0a0815 }
.email-wrap { max-width:600px;margin:40px auto;font-family:system-ui,-apple-system,sans-serif }
/* Header */
.email-logo { display:block;margin:0 auto 14px;border:0;outline:none;opacity:0.5 }
.email-brand { font-size:16px;font-weight:300;color:#ffffff66;letter-spacing:4px;text-transform:uppercase }
.email-header { padding:0 0 32px;text-align:center }
/* Card */
.email-card { padding:48px 40px;background:#191432cc;border:1px solid #0d948833;box-shadow:0 0 60px #0d948814,0 0 120px #6366f10a }
.email-heading { margin:0 0 20px;font-size:24px;font-weight:300;color:#ccfbf1;line-height:1.3;letter-spacing:-.5px }
.email-text { margin:0 0 24px;font-size:14px;line-height:2;color:#d2fcf5e0;font-weight:300 }
.email-subtext { margin:0;font-size:13px;line-height:1.8;color:#b4c3d7f2;font-weight:300 }
/* Divider */
.email-divider { height:1px;background:linear-gradient(90deg,transparent,#0d94884d,transparent) }
/* Button */
.email-btn-row { padding:40px 40px;text-align:center }
.email-btn { display:inline-block;padding:16px 48px;font-size:14px;font-weight:400;color:#e0f5f3!important;text-decoration:none;background:#0d94882e;border:1px solid #0d94884d;letter-spacing:1px }
.email-btn-table { margin:0 0 32px }
/* Fallback link */
.email-link-box { border:1px solid #0d94882e;background:#0d948808 }
.email-link-label { margin:0 0 8px;font-size:11px;color:#b4fceebf;font-weight:300 }
.email-link-url { margin:0;font-size:11px;font-family:monospace;color:#99f6e4;background:#00000033;padding:10px 12px;border:1px solid #0d948826;word-break:break-all }
/* Footer */
.email-footer { padding:32px 0 0;text-align:center }
.email-footer-text { margin:0;font-size:11px;color:#a0afc88c;font-weight:300 }
.email-footer-link { color:#5eead480;text-decoration:none;font-weight:400 }
/* Typography utilities */
.card-body h1,.card-body h2,.card-body h3,.card-body h4,.card-body h5,.card-body h6 { color:#ccfbf1!important }
.card-body a,.card-body a:link,.card-body a:visited { color:#5eead4!important;text-decoration:underline!important }
.card-body a:hover,.card-body a:active { color:#99f6e4!important }
.card-body li { color:#d2fcf5e0!important }
/* Responsive */
@media only screen and (max-width:480px) {
.email-card { padding:32px 20px!important }
.email-btn-row { padding:24px 20px!important }
.email-heading { font-size:20px!important }
}
</style>
</head>
<body style="margin:0;padding:0;background:#0a0815;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
<center style="width:100%;table-layout:fixed;">
<table width="100%" cellpadding="0" cellspacing="0" border="0" class="email-wrap">
<!-- Floating Title -->
<tr>
<td class="email-header">
<img src="{{AppUrl}}assets/img/favicon.png" alt="logo" width="24" height="24" class="email-logo" />
<span class="email-brand">{{AppName}}</span>
</td>
</tr>
<!-- Card -->
<tr>
<td class="email-card card-body">
<p class="email-heading">
{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape)}}
</p>
<p class="email-text">
{{.locale.Tr "mail.reset_password.text" .ResetPwdCodeLives}}
</p>
<p class="email-subtext"></p>
</td>
</tr>
<!-- Divider -->
<tr>
<td class="email-divider"></td>
</tr>
<!-- Button & Link -->
<tr>
<td class="email-btn-row">
{{$recover_url := printf "%suser/recover_account?code=%s" AppUrl (QueryEscape .Code)}}
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="email-btn-table">
<tr>
<td align="center">
<a href="{{$recover_url}}" class="email-btn">{{.locale.Tr "mail.reset_password"}}</a>
</td>
</tr>
</table>
<table width="100%" cellpadding="0" cellspacing="0" border="0" class="email-link-box">
<tr>
<td style="padding:16px 18px;">
<p class="email-link-label">{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
<p class="email-link-url">{{$recover_url}}</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- Footer -->
<tr>
<td class="email-footer">
<p class="email-footer-text">
&copy;
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
</p>
</td>
</tr>
</table>
</center>
</body>
</html>
+78
View File
@@ -0,0 +1,78 @@
/* Base */
body,.ExternalClass,.ReadMsgBody { background-color:#0a0815 }
.email-wrap { max-width:600px;margin:40px auto;font-family:system-ui,-apple-system,sans-serif }
/* Header */
.email-logo { display:block;margin:0 auto 14px;border:0;outline:none;opacity:0.5 }
.email-brand { font-size:16px;font-weight:300;color:#ffffff66;letter-spacing:4px;text-transform:uppercase }
.email-header { padding:0 0 32px;text-align:center }
/* Card */
.email-card { padding:48px 40px;background:#191432cc;border:1px solid #0d948833;box-shadow:0 0 60px #0d948814,0 0 120px #6366f10a }
.email-heading { margin:0 0 20px;font-size:24px;font-weight:300;color:#ccfbf1;line-height:1.3;letter-spacing:-.5px }
.email-text { margin:0 0 24px;font-size:14px;line-height:2;color:#d2fcf5e0;font-weight:300 }
.email-subtext { margin:0;font-size:13px;line-height:1.8;color:#b4c3d7f2;font-weight:300 }
/* Divider */
.email-divider { height:1px;background:linear-gradient(90deg,transparent,#0d94884d,transparent) }
/* Button */
.email-btn-row { padding:40px 40px;text-align:center }
.email-btn,.card-body .email-btn,.card-body .email-btn:link,.card-body .email-btn:visited { display:inline-block;padding:16px 48px;font-size:14px;font-weight:400;color:#e0f5f3!important;text-decoration:none!important;background:#0d94882e;border:1px solid #0d94884d;letter-spacing:1px }
.email-btn-table { margin:0 0 32px }
/* Fallback link */
.email-link-box { border:1px solid #0d94882e;background:#0d948808 }
.email-link-label { margin:0 0 8px;font-size:11px;color:#b4fceebf;font-weight:300 }
.email-link-url { margin:0;font-size:11px;font-family:monospace;color:#99f6e4;background:#00000033;padding:10px 12px;border:1px solid #0d948826;word-break:break-all }
/* Footer */
.email-footer { padding:32px 0 0;text-align:center }
.email-footer-text { margin:0;font-size:11px;color:#a0afc88c;font-weight:300 }
.email-footer-link { color:#5eead480;text-decoration:none;font-weight:400 }
/* Typography utilities */
.card-body h1,.card-body h2,.card-body h3,.card-body h4,.card-body h5,.card-body h6 { color:#ccfbf1!important }
.card-body a,.card-body a:link,.card-body a:visited { color:#5eead4!important;text-decoration:underline!important }
.card-body a:hover,.card-body a:active { color:#99f6e4!important }
.card-body li { color:#d2fcf5e0!important }
/* Responsive */
@media only screen and (max-width:480px) {
.email-card { padding:32px 20px!important }
.email-btn-row { padding:24px 20px!important }
.email-heading { font-size:20px!important }
}
/* Official-snapshot presentation adapter: no content or translation overrides. */
body { margin:0; padding:24px 16px; background-color:#0a0815; }
body, td { font-family:Arial,sans-serif; }
.email-wrap { width:100%; max-width:600px; border-collapse:separate; border-spacing:0; }
.card-body { font-size:14px; line-height:1.7; overflow-wrap:anywhere; }
.card-body > pre, .card-body code { font-family:Consolas,monospace; white-space:pre-wrap; overflow-wrap:anywhere; }
.card-body ul { padding-left:22px; }
.card-body blockquote { padding-left:16px; border-left:3px solid #0d9488; }
.mail-footer { margin:0; }
.mail-footer td { border-top-color:transparent!important; color:#d2fcf5!important; }
.mail-footer a { color:#0d9488!important; text-decoration:none; }
.email-body { padding:32px; }
body { background-image:radial-gradient(ellipse at center,#0d948814,transparent 65%); }
@media only screen and (max-width:480px) {
body { padding:12px 8px; }
.email-wrap { margin:0 auto; }
.email-body { padding:24px 18px!important; }
.card-body { width:auto!important; }
.resp-hide { display:none!important; }
}
/* Shared framework controls: keep URLs readable, never style them as CTAs. */
.email-link-cell { padding:16px 18px; }
.email-link-url a,.card-body .email-link-url a { color:inherit!important; text-decoration:none!important; font-size:inherit; }
.email-link-url { overflow-wrap:anywhere; word-break:break-all; }
.email-action { margin:20px 0 0; }
.email-btn-row { box-sizing:border-box; }
.card-body .email-btn { max-width:100%; box-sizing:border-box; white-space:normal; overflow-wrap:anywhere; }
@media only screen and (max-width:480px) { .card-body .email-btn { display:block; text-align:center; width:100%; } .email-btn-table { width:100%; } }
+6
View File
@@ -0,0 +1,6 @@
{
"name": "aurora",
"description": "Ethereal / Dreamlike",
"mode": "framed",
"layout": "aurora"
}
-99
View File
@@ -1,99 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<title>
{{.locale.Tr "mail.team_invite.subject" (.Inviter.DisplayName|DotEscape) (.Organization.DisplayName|DotEscape)}}
</title>
<style type="text/css">
/* Base */
body,.ExternalClass,.ReadMsgBody { background-color:#e2e8f2 }
/* Header */
.email-logo { display:inline-block;margin:0 auto 8px;border:0;outline:none }
.email-brand { margin:0;font-size:18px;font-weight:700;color:#1e293b;letter-spacing:-0.3px }
.email-header { padding:28px 32px 18px;background:linear-gradient(135deg,#dbeafe,#e0e7ff,#e8f0fe);border-bottom:1px solid #3b82f61a;text-align:center }
/* Card */
.email-card { max-width:600px;margin:24px auto;background:#ffffff;border-radius:16px;box-shadow:0 2px 4px #0000000f,0 8px 24px #0000000f,0 0 0 1px #0000000a;overflow:hidden }
.email-heading { margin:0 0 12px;font-size:17px;line-height:1.5;color:#1e293b;font-weight:600 }
.email-text { margin:0 0 8px;font-size:15px;line-height:1.6;color:#475569 }
.email-subtext { margin:0 0 20px;font-size:14px;line-height:1.6;color:#64748b }
/* Button */
.email-btn-row { margin:28px 0 24px;border-collapse:separate }
.email-btn { display:inline-block;padding:14px 40px;font-size:15px;font-weight:600;color:#ffffff!important;text-decoration:none;background-color:#3b82f6;border-radius:40px;border:1px solid #3b82f6;letter-spacing:0.2px }
/* Fallback link */
.email-link-box { background:linear-gradient(135deg,#eff4fd,#f5f7fe);border:1px solid #3b82f61a;border-radius:12px;padding:16px 18px;margin:24px 0 16px;box-shadow:inset 0 1px 0 #ffffffe6 }
.email-link-label { margin:0 0 8px;font-size:13px;color:#64748b;line-height:1.5 }
.email-link-url { margin:0;font-size:13px;color:#3b82f6;background:#ffffff;padding:10px 12px;border-radius:8px;border:1px solid #0000000f;word-break:break-all;font-family:monospace }
/* Footer */
.email-footer { padding:18px 32px 22px;background:linear-gradient(135deg,#f8f9fc,#f4f6fb);border-top:1px solid #0000000f;text-align:center }
.email-footer-text { margin:0;font-size:13px;color:#94a3b8 }
.email-footer-link { color:#3b82f6;text-decoration:none;font-weight:500 }
</style>
</head>
<body style="margin:0;padding:0;background:#e2e8f2;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
<center style="width:100%;table-layout:fixed;">
<div class="email-card">
<!-- Floating Title -->
<div class="email-header">
<img src="{{AppUrl}}assets/img/favicon.png" alt="logo" width="32" height="32" class="email-logo" />
<p class="email-brand">{{AppName}}</p>
</div>
<!-- Card -->
<div style="padding:32px 32px 24px;color:#334155;">
<p class="email-heading">
{{.locale.Tr "mail.hi_user_x" .Invite.Email}}
</p>
<p class="email-text">
{{.locale.Tr "mail.team_invite.text_1" (DotEscape .Inviter.DisplayName) (DotEscape .Team.Name) (DotEscape .Organization.DisplayName)}}
</p>
<p class="email-subtext">
{{.locale.Tr "mail.team_invite.text_2"}}
</p>
<!-- Button -->
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="email-btn-row">
<tr>
<td align="center">
<a href="{{.InviteURL}}" class="email-btn">{{.locale.Tr "mail.view_it_on" AppName}}</a>
</td>
</tr>
</table>
<!-- Fallback link -->
<div class="email-link-box">
<p class="email-link-label">{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
<p class="email-link-url">{{.InviteURL}}</p>
</div>
<p style="margin:16px 0 0;font-size:13px;color:#64748b;line-height:1.5;">
{{.locale.Tr "mail.team_invite.text_3" .Invite.Email}}
</p>
</div>
<!-- Footer -->
<div class="email-footer">
<p class="email-footer-text">
&copy;
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
</p>
</div>
</div>
</center>
</body>
</html>
@@ -1,100 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<title>
{{.Subject}}
</title>
<style type="text/css">
/* Base */
body,.ExternalClass,.ReadMsgBody { background-color:#e2e8f2 }
/* Header */
.email-logo { display:inline-block;margin:0 auto 8px;border:0;outline:none }
.email-brand { margin:0;font-size:18px;font-weight:700;color:#1e293b;letter-spacing:-0.3px }
.email-header { padding:28px 32px 18px;background:linear-gradient(135deg,#dbeafe,#e0e7ff,#e8f0fe);border-bottom:1px solid #3b82f61a;text-align:center }
/* Card */
.email-card { max-width:600px;margin:24px auto;background:#ffffff;border-radius:16px;box-shadow:0 2px 4px #0000000f,0 8px 24px #0000000f,0 0 0 1px #0000000a;overflow:hidden }
.email-heading { font-size:17px;font-weight:600;color:#1e293b;margin:0 0 16px }
/* Content */
.job-list { list-style:none;padding:0;margin:0 0 24px 0 }
.job-item { background:linear-gradient(135deg,#f8faff,#f4f7fe);border:1px solid #0000000f;border-radius:10px;padding:12px 16px;margin-bottom:8px;box-shadow:inset 0 1px 0 #ffffffb3 }
.job-link { color:#3b82f6;text-decoration:none;font-weight:500;font-size:14px;display:block }
/* Button */
.email-btn-row { margin:16px 0 24px;border-collapse:separate }
.email-btn { display:inline-block;padding:14px 40px;font-size:15px;font-weight:600;color:#ffffff!important;text-decoration:none;background-color:#3b82f6;border-radius:40px;border:1px solid #3b82f6;letter-spacing:0.2px }
/* Fallback link */
.email-link-box { background:linear-gradient(135deg,#eff4fd,#f5f7fe);border:1px solid #3b82f61a;border-radius:12px;padding:16px 18px;margin:24px 0 16px;box-shadow:inset 0 1px 0 #ffffffe6 }
.email-link-label { margin:0 0 8px;font-size:13px;color:#64748b;line-height:1.5 }
.email-link-url { margin:0;font-size:13px;color:#3b82f6;background:#ffffff;padding:10px 12px;border-radius:8px;border:1px solid #0000000f;word-break:break-all;font-family:monospace }
/* Footer */
.email-footer { padding:18px 32px 22px;background:linear-gradient(135deg,#f8f9fc,#f4f6fb);border-top:1px solid #0000000f;text-align:center }
.email-footer-text { margin:0;font-size:13px;color:#94a3b8 }
.email-footer-link { color:#3b82f6;text-decoration:none;font-weight:500 }
</style>
</head>
<body style="margin:0;padding:0;background:#e2e8f2;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
<center style="width:100%;table-layout:fixed;">
<div class="email-card">
<!-- Floating Title -->
<div class="email-header">
<img src="{{AppUrl}}assets/img/favicon.png" alt="logo" width="32" height="32" class="email-logo" />
<p class="email-brand">{{AppName}}</p>
</div>
<!-- Card -->
<div style="padding:32px 32px 24px;color:#334155;">
<h2 class="email-heading">
{{.Repo.FullName}} &mdash; {{.Run.WorkflowID}}: {{.RunStatusText}}
</h2>
<ul class="job-list">
{{range $job := .Jobs}}
<li class="job-item">
<a href="{{$job.HTMLURL}}" class="job-link">
{{$job.Status}}: {{$job.Name}}{{if gt $job.Attempt 1}} (retry #{{$job.Attempt}}){{end}} &mdash; {{$job.Duration}}
</a>
</li>
{{end}}
</ul>
<!-- Button -->
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="email-btn-row">
<tr>
<td align="center">
<a href="{{.Run.HTMLURL}}" class="email-btn">{{.locale.Tr "mail.view_it_on" AppName}}</a>
</td>
</tr>
</table>
<!-- Fallback link -->
<div class="email-link-box">
<p class="email-link-label">{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
<p class="email-link-url">{{.Run.HTMLURL}}</p>
</div>
</div>
<!-- Footer -->
<div class="email-footer">
<p class="email-footer-text">
&copy;
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
</p>
</div>
</div>
</center>
</body>
</html>
-88
View File
@@ -1,88 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<title>
{{.Subject}}
</title>
<style type="text/css">
/* Base */
body,.ExternalClass,.ReadMsgBody { background-color:#e2e8f2 }
/* Header */
.email-logo { display:inline-block;margin:0 auto 8px;border:0;outline:none }
.email-brand { margin:0;font-size:18px;font-weight:700;color:#1e293b;letter-spacing:-0.3px }
.email-header { padding:28px 32px 18px;background:linear-gradient(135deg,#dbeafe,#e0e7ff,#e8f0fe);border-bottom:1px solid #3b82f61a;text-align:center }
/* Card */
.email-card { max-width:600px;margin:24px auto;background:#ffffff;border-radius:16px;box-shadow:0 2px 4px #0000000f,0 8px 24px #0000000f,0 0 0 1px #0000000a;overflow:hidden }
.email-text { margin:0 0 20px;font-size:15px;line-height:1.6;color:#475569 }
/* Typography utilities */
code { background:#eff4fd;padding:2px 8px;border-radius:5px;font-size:14px;color:#3b82f6;font-weight:500;border:1px solid #3b82f61f }
/* Button */
.email-btn-row { margin:28px 0 24px;border-collapse:separate }
.email-btn { display:inline-block;padding:14px 40px;font-size:15px;font-weight:600;color:#ffffff!important;text-decoration:none;background-color:#3b82f6;border-radius:40px;border:1px solid #3b82f6;letter-spacing:0.2px }
/* Fallback link */
.email-link-box { background:linear-gradient(135deg,#eff4fd,#f5f7fe);border:1px solid #3b82f61a;border-radius:12px;padding:16px 18px;margin:24px 0 16px;box-shadow:inset 0 1px 0 #ffffffe6 }
.email-link-label { margin:0 0 8px;font-size:13px;color:#64748b;line-height:1.5 }
.email-link-url { margin:0;font-size:13px;color:#3b82f6;background:#ffffff;padding:10px 12px;border-radius:8px;border:1px solid #0000000f;word-break:break-all;font-family:monospace }
/* Footer */
.email-footer { padding:18px 32px 22px;background:linear-gradient(135deg,#f8f9fc,#f4f6fb);border-top:1px solid #0000000f;text-align:center }
.email-footer-text { margin:0;font-size:13px;color:#94a3b8 }
.email-footer-link { color:#3b82f6;text-decoration:none;font-weight:500 }
</style>
</head>
<body style="margin:0;padding:0;background:#e2e8f2;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
<center style="width:100%;table-layout:fixed;">
<div class="email-card">
<!-- Floating Title -->
<div class="email-header">
<img src="{{AppUrl}}assets/img/favicon.png" alt="logo" width="32" height="32" class="email-logo" />
<p class="email-brand">{{AppName}}</p>
</div>
<!-- Card -->
<div style="padding:32px 32px 24px;color:#334155;">
<p class="email-text">
{{.locale.Tr "mail.repo.collaborator.added.text"}} <code>{{.RepoName}}</code>.
</p>
<!-- Button -->
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="email-btn-row">
<tr>
<td align="center">
<a href="{{.Link}}" class="email-btn">{{.locale.Tr "mail.view_it_on" AppName}}</a>
</td>
</tr>
</table>
<!-- Fallback link -->
<div class="email-link-box">
<p class="email-link-label">{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
<p class="email-link-url">{{.Link}}</p>
</div>
</div>
<!-- Footer -->
<div class="email-footer">
<p class="email-footer-text">
&copy;
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
</p>
</div>
</div>
</center>
</body>
</html>
@@ -1,90 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<title>
{{.Subject}}
</title>
<style type="text/css">
/* Base */
body,.ExternalClass,.ReadMsgBody { background-color:#e2e8f2 }
/* Header */
.email-logo { display:inline-block;margin:0 auto 8px;border:0;outline:none }
.email-brand { margin:0;font-size:18px;font-weight:700;color:#1e293b;letter-spacing:-0.3px }
.email-header { padding:28px 32px 18px;background:linear-gradient(135deg,#dbeafe,#e0e7ff,#e8f0fe);border-bottom:1px solid #3b82f61a;text-align:center }
/* Card */
.email-card { max-width:600px;margin:24px auto;background:#ffffff;border-radius:16px;box-shadow:0 2px 4px #0000000f,0 8px 24px #0000000f,0 0 0 1px #0000000a;overflow:hidden }
.email-text { margin:0 0 20px;font-size:15px;line-height:1.6;color:#475569 }
/* Button */
.email-btn-row { margin:28px 0 24px;border-collapse:separate }
.email-btn { display:inline-block;padding:14px 40px;font-size:15px;font-weight:600;color:#ffffff!important;text-decoration:none;background-color:#3b82f6;border-radius:40px;border:1px solid #3b82f6;letter-spacing:0.2px }
/* Fallback link */
.email-link-box { background:linear-gradient(135deg,#eff4fd,#f5f7fe);border:1px solid #3b82f61a;border-radius:12px;padding:16px 18px;margin:24px 0 16px;box-shadow:inset 0 1px 0 #ffffffe6 }
.email-link-label { margin:0 0 8px;font-size:13px;color:#64748b;line-height:1.5 }
.email-link-url { margin:0;font-size:13px;color:#3b82f6;background:#ffffff;padding:10px 12px;border-radius:8px;border:1px solid #0000000f;word-break:break-all;font-family:monospace }
/* Footer */
.email-footer { padding:18px 32px 22px;background:linear-gradient(135deg,#f8f9fc,#f4f6fb);border-top:1px solid #0000000f;text-align:center }
.email-footer-text { margin:0;font-size:13px;color:#94a3b8 }
.email-footer-link { color:#3b82f6;text-decoration:none;font-weight:500 }
</style>
</head>
<body style="margin:0;padding:0;background:#e2e8f2;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
<center style="width:100%;table-layout:fixed;">
<div class="email-card">
<!-- Floating Title -->
<div class="email-header">
<img src="{{AppUrl}}assets/img/favicon.png" alt="logo" width="32" height="32" class="email-logo" />
<p class="email-brand">{{AppName}}</p>
</div>
<!-- Card -->
<div style="padding:32px 32px 24px;color:#334155;">
{{$repo_url := HTMLFormat "<a href='%s' style='color:#3b82f6;text-decoration:none;font-weight:500;'>%s</a>" .Issue.Repo.HTMLURL .Issue.Repo.FullName}}
{{$link := HTMLFormat "<a href='%s' style='color:#3b82f6;text-decoration:none;font-weight:500;'>#%d</a>" .Link .Issue.Index}}
<p class="email-text">
{{if .IsPull}}
{{.locale.Tr "mail.issue_assigned.pull" .Doer.Name $link $repo_url}}
{{else}}
{{.locale.Tr "mail.issue_assigned.issue" .Doer.Name $link $repo_url}}
{{end}}
</p>
<!-- Button -->
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="email-btn-row">
<tr>
<td align="center">
<a href="{{.Link}}" class="email-btn">{{.locale.Tr "mail.view_it_on" AppName}}</a>
</td>
</tr>
</table>
<!-- Fallback link -->
<div class="email-link-box">
<p class="email-link-label">{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
<p class="email-link-url">{{.Link}}</p>
</div>
</div>
<!-- Footer -->
<div class="email-footer">
<p class="email-footer-text">
&copy;
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
</p>
</div>
</div>
</center>
</body>
</html>
-162
View File
@@ -1,162 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<title>
{{.Subject}}
</title>
<style type="text/css">
/* Base */
body,.ExternalClass,.ReadMsgBody { background-color:#e2e8f2 }
/* Header */
.email-logo { display:inline-block;margin:0 auto 8px;border:0;outline:none }
.email-brand { margin:0;font-size:18px;font-weight:700;color:#1e293b;letter-spacing:-0.3px }
.email-header { padding:28px 32px 18px;background:linear-gradient(135deg,#dbeafe,#e0e7ff,#e8f0fe);border-bottom:1px solid #3b82f61a;text-align:center }
/* Card */
.email-card { max-width:600px;margin:24px auto;background:#ffffff;border-radius:16px;box-shadow:0 2px 4px #0000000f,0 8px 24px #0000000f,0 0 0 1px #0000000a;overflow:hidden }
.email-text { margin:0 0 16px;font-size:15px;line-height:1.6;color:#475569 }
/* Content */
blockquote { padding-left:1em;margin:1em 0;border-left:2px solid #dbeafe;color:#64748b }
pre { background:#eff4fd;padding:12px;border-radius:8px;overflow-x:auto;font-size:13px;line-height:1.5;border:1px solid #0000000f }
hr { border:none;border-top:1px solid #0000000f;margin:20px 0 }
.review-path { margin:8px 0;font-size:13px;color:#64748b }
.review-content { padding-left:1em;margin:1em 0 }
.review-body { margin-top:8px;font-size:14px;line-height:1.6 }
.commit-list { margin:16px 0;padding-left:20px }
.commit-item { margin-bottom:6px;font-size:14px }
.commit-link { color:#3b82f6;text-decoration:none;font-weight:500 }
/* Button */
.email-btn-row { margin:28px 0 24px;border-collapse:separate }
.email-btn { display:inline-block;padding:14px 40px;font-size:15px;font-weight:600;color:#ffffff!important;text-decoration:none;background-color:#3b82f6;border-radius:40px;border:1px solid #3b82f6;letter-spacing:0.2px }
/* Fallback link */
.email-link-box { background:linear-gradient(135deg,#eff4fd,#f5f7fe);border:1px solid #3b82f61a;border-radius:12px;padding:16px 18px;margin:24px 0 16px;box-shadow:inset 0 1px 0 #ffffffe6 }
.email-link-label { margin:0 0 8px;font-size:13px;color:#64748b;line-height:1.5 }
.email-link-url { margin:0;font-size:13px;color:#3b82f6;background:#ffffff;padding:10px 12px;border-radius:8px;border:1px solid #0000000f;word-break:break-all;font-family:monospace }
/* Footer */
.email-footer { padding:18px 32px 22px;background:linear-gradient(135deg,#f8f9fc,#f4f6fb);border-top:1px solid #0000000f;text-align:center }
.email-footer-text { margin:0;font-size:13px;color:#94a3b8 }
.email-footer-link { color:#3b82f6;text-decoration:none;font-weight:500 }
</style>
</head>
<body style="margin:0;padding:0;background:#e2e8f2;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
<center style="width:100%;table-layout:fixed;">
<div class="email-card">
<!-- Floating Title -->
<div class="email-header">
<img src="{{AppUrl}}assets/img/favicon.png" alt="logo" width="32" height="32" class="email-logo" />
<p class="email-brand">{{AppName}}</p>
</div>
<!-- Card -->
<div style="padding:32px 32px 24px;color:#334155;">
{{if .IsMention}}
<p class="email-text">{{.locale.Tr "mail.issue.x_mentioned_you" .Doer.Name}}</p>
{{end}}
{{if eq .ActionName "push"}}
<p class="email-text">
{{if .Comment.IsForcePush}}
{{$oldCommitUrl := printf "%s/commit/%s" .Comment.Issue.PullRequest.BaseRepo.HTMLURL .Comment.OldCommit}}
{{$oldShortSha := ShortSha .Comment.OldCommit}}
{{$oldCommitLink := HTMLFormat "<a href='%[1]s' style='color:#3b82f6;text-decoration:none;font-weight:500;'>%[2]s</a>" $oldCommitUrl $oldShortSha}}
{{$newCommitUrl := printf "%s/commit/%s" .Comment.Issue.PullRequest.BaseRepo.HTMLURL .Comment.NewCommit}}
{{$newShortSha := ShortSha .Comment.NewCommit}}
{{$newCommitLink := HTMLFormat "<a href='%[1]s' style='color:#3b82f6;text-decoration:none;font-weight:500;'>%[2]s</a>" $newCommitUrl $newShortSha}}
{{.locale.Tr "mail.issue.action.force_push" .Doer.Name .Comment.Issue.PullRequest.HeadBranch $oldCommitLink $newCommitLink}}
{{else}}
{{.locale.TrN (len .Comment.Commits) "mail.issue.action.push_1" "mail.issue.action.push_n" .Doer.Name .Comment.Issue.PullRequest.HeadBranch (len .Comment.Commits)}}
{{end}}
</p>
{{end}}
<p class="email-text">
{{if eq .ActionName "close"}}
{{.locale.Tr "mail.issue.action.close" .Doer.Name .Issue.Index}}
{{else if eq .ActionName "reopen"}}
{{.locale.Tr "mail.issue.action.reopen" .Doer.Name .Issue.Index}}
{{else if eq .ActionName "merge"}}
{{.locale.Tr "mail.issue.action.merge" .Doer.Name .Issue.Index .Issue.PullRequest.BaseBranch}}
{{else if eq .ActionName "approve"}}
{{.locale.Tr "mail.issue.action.approve" .Doer.Name}}
{{else if eq .ActionName "reject"}}
{{.locale.Tr "mail.issue.action.reject" .Doer.Name}}
{{else if eq .ActionName "review"}}
{{.locale.Tr "mail.issue.action.review" .Doer.Name}}
{{else if eq .ActionName "review_dismissed"}}
{{.locale.Tr "mail.issue.action.review_dismissed" .Doer.Name .Comment.Review.Reviewer.Name}}
{{else if eq .ActionName "ready_for_review"}}
{{.locale.Tr "mail.issue.action.ready_for_review" .Doer.Name}}
{{end}}
{{- if eq .Body ""}}
{{if eq .ActionName "new"}}
{{.locale.Tr "mail.issue.action.new" .Doer.Name .Issue.Index}}
{{end}}
{{else}}
{{.Body}}
{{end -}}{{if .CanReply}}&nbsp;{{.locale.Tr "mail.reply"}}{{end}}
</p>
{{- range .ReviewComments}}
<hr>
<p class="review-path">{{$.locale.Tr "mail.issue.in_tree_path" .TreePath}}</p>
<div class="review-content">
<pre>{{.Patch}}</pre>
<div class="review-body">{{.RenderedContent}}</div>
</div>
{{end -}}
{{if eq .ActionName "push"}}
<ul class="commit-list">
{{range .Comment.Commits}}
<li class="commit-item">
<a href="{{$.Comment.Issue.PullRequest.BaseRepo.HTMLURL}}/commit/{{.ID}}" class="commit-link">
{{ShortSha .ID.String}}
</a> &mdash; {{.UserCommit.GitCommit.MessageTitle}}
</li>
{{end}}
</ul>
{{end}}
<!-- Button -->
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="email-btn-row">
<tr>
<td align="center">
<a href="{{.Link}}" class="email-btn">{{.locale.Tr "mail.view_it_on" AppName}}</a>
</td>
</tr>
</table>
<!-- Fallback link -->
<div class="email-link-box">
<p class="email-link-label">{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
<p class="email-link-url">{{.Link}}</p>
</div>
</div>
<!-- Footer -->
<div class="email-footer">
<p class="email-footer-text">
&copy;
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
</p>
</div>
</div>
</center>
</body>
</html>
-122
View File
@@ -1,122 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<title>
{{.Subject}}
</title>
<style type="text/css">
/* Base */
body,.ExternalClass,.ReadMsgBody { background-color:#e2e8f2 }
/* Header */
.email-logo { display:inline-block;margin:0 auto 8px;border:0;outline:none }
.email-brand { margin:0;font-size:18px;font-weight:700;color:#1e293b;letter-spacing:-0.3px }
.email-header { padding:28px 32px 18px;background:linear-gradient(135deg,#dbeafe,#e0e7ff,#e8f0fe);border-bottom:1px solid #3b82f61a;text-align:center }
/* Card */
.email-card { max-width:600px;margin:24px auto;background:#ffffff;border-radius:16px;box-shadow:0 2px 4px #0000000f,0 8px 24px #0000000f,0 0 0 1px #0000000a;overflow:hidden }
.email-heading { margin:20px 0 8px;font-size:17px;font-weight:600;color:#1e293b }
.email-subheading { margin:20px 0 8px;font-weight:600;color:#1e293b }
.email-text { margin:0 0 16px;font-size:15px;line-height:1.6;color:#475569 }
/* Content */
blockquote { padding-left:1em;margin:1em 0;border-left:2px solid #dbeafe;color:#64748b }
ul { margin:12px 0 16px;padding-left:20px }
li { margin-bottom:6px;font-size:14px;line-height:1.5;color:#475569 }
.email-content { margin:12px 0 16px;font-size:14px;line-height:1.6;color:#475569 }
/* Button */
.email-btn-row { margin:16px 0 24px;border-collapse:separate }
.email-btn { display:inline-block;padding:14px 40px;font-size:15px;font-weight:600;color:#ffffff!important;text-decoration:none;background-color:#3b82f6;border-radius:40px;border:1px solid #3b82f6;letter-spacing:0.2px }
/* Fallback link */
.email-link-box { background:linear-gradient(135deg,#eff4fd,#f5f7fe);border:1px solid #3b82f61a;border-radius:12px;padding:16px 18px;margin:24px 0 16px;box-shadow:inset 0 1px 0 #ffffffe6 }
.email-link-label { margin:0 0 8px;font-size:13px;color:#64748b;line-height:1.5 }
.email-link-url { margin:0;font-size:13px;color:#3b82f6;background:#ffffff;padding:10px 12px;border-radius:8px;border:1px solid #0000000f;word-break:break-all;font-family:monospace }
/* Footer */
.email-footer { padding:18px 32px 22px;background:linear-gradient(135deg,#f8f9fc,#f4f6fb);border-top:1px solid #0000000f;text-align:center }
.email-footer-text { margin:0;font-size:13px;color:#94a3b8 }
.email-footer-link { color:#3b82f6;text-decoration:none;font-weight:500 }
</style>
</head>
<body style="margin:0;padding:0;background:#e2e8f2;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
<center style="width:100%;table-layout:fixed;">
<div class="email-card">
<!-- Floating Title -->
<div class="email-header">
<img src="{{AppUrl}}assets/img/favicon.png" alt="logo" width="32" height="32" class="email-logo" />
<p class="email-brand">{{AppName}}</p>
</div>
<!-- Card -->
<div style="padding:32px 32px 24px;color:#334155;">
{{$release_url := HTMLFormat "<a href='%s' style='color:#3b82f6;text-decoration:none;font-weight:500;'>%s</a>" .Release.HTMLURL .Release.TagName}}
{{$repo_url := HTMLFormat "<a href='%s' style='color:#3b82f6;text-decoration:none;font-weight:500;'>%s</a>" .Release.Repo.HTMLURL .Release.Repo.FullName}}
<p class="email-text">
{{.locale.Tr "mail.release.new.text" .Release.Publisher.Name $release_url $repo_url}}
</p>
<h4 class="email-heading">{{.locale.Tr "mail.release.title" .Release.Title}}</h4>
{{if .Release.RenderedNote}}
{{.locale.Tr "mail.release.note"}}<br>
<div class="email-content">
{{.Release.RenderedNote}}
</div>
{{end}}
<p class="email-subheading">{{.locale.Tr "mail.release.downloads"}}</p>
<ul>
{{if not .DisableDownloadSourceArchives}}
<li>
<a href="{{.Release.Repo.HTMLURL}}/archive/{{.Release.TagName | PathEscapeSegments}}.zip" rel="nofollow" style="color:#3b82f6;text-decoration:none;font-weight:500;">{{.locale.Tr "mail.release.download.zip"}}</a>
</li>
<li>
<a href="{{.Release.Repo.HTMLURL}}/archive/{{.Release.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow" style="color:#3b82f6;text-decoration:none;font-weight:500;">{{.locale.Tr "mail.release.download.targz"}}</a>
</li>
{{end}}
{{if .Release.Attachments}}
{{range .Release.Attachments}}
<li>
<a href="{{.DownloadURL}}" target="_blank" style="color:#3b82f6;text-decoration:none;font-weight:500;">{{.Name}} ({{.Size | FileSize}})</a>
</li>
{{end}}
{{end}}
</ul>
<!-- Button -->
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="email-btn-row">
<tr>
<td align="center">
<a href="{{.Link}}" class="email-btn">{{.locale.Tr "mail.view_it_on" AppName}}</a>
</td>
</tr>
</table>
<!-- Fallback link -->
<div class="email-link-box">
<p class="email-link-label">{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
<p class="email-link-url">{{.Link}}</p>
</div>
</div>
<!-- Footer -->
<div class="email-footer">
<p class="email-footer-text">
&copy;
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
</p>
</div>
</div>
</center>
</body>
</html>
-85
View File
@@ -1,85 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<title>
{{.Subject}}
</title>
<style type="text/css">
/* Base */
body,.ExternalClass,.ReadMsgBody { background-color:#e2e8f2 }
/* Header */
.email-logo { display:inline-block;margin:0 auto 8px;border:0;outline:none }
.email-brand { margin:0;font-size:18px;font-weight:700;color:#1e293b;letter-spacing:-0.3px }
.email-header { padding:28px 32px 18px;background:linear-gradient(135deg,#dbeafe,#e0e7ff,#e8f0fe);border-bottom:1px solid #3b82f61a;text-align:center }
/* Card */
.email-card { max-width:600px;margin:24px auto;background:#ffffff;border-radius:16px;box-shadow:0 2px 4px #0000000f,0 8px 24px #0000000f,0 0 0 1px #0000000a;overflow:hidden }
.email-text { margin:0 0 20px;font-size:15px;line-height:1.6;color:#475569 }
/* Button */
.email-btn-row { margin:28px 0 24px;border-collapse:separate }
.email-btn { display:inline-block;padding:14px 40px;font-size:15px;font-weight:600;color:#ffffff!important;text-decoration:none;background-color:#3b82f6;border-radius:40px;border:1px solid #3b82f6;letter-spacing:0.2px }
/* Fallback link */
.email-link-box { background:linear-gradient(135deg,#eff4fd,#f5f7fe);border:1px solid #3b82f61a;border-radius:12px;padding:16px 18px;margin:24px 0 16px;box-shadow:inset 0 1px 0 #ffffffe6 }
.email-link-label { margin:0 0 8px;font-size:13px;color:#64748b;line-height:1.5 }
.email-link-url { margin:0;font-size:13px;color:#3b82f6;background:#ffffff;padding:10px 12px;border-radius:8px;border:1px solid #0000000f;word-break:break-all;font-family:monospace }
/* Footer */
.email-footer { padding:18px 32px 22px;background:linear-gradient(135deg,#f8f9fc,#f4f6fb);border-top:1px solid #0000000f;text-align:center }
.email-footer-text { margin:0;font-size:13px;color:#94a3b8 }
.email-footer-link { color:#3b82f6;text-decoration:none;font-weight:500 }
</style>
</head>
<body style="margin:0;padding:0;background:#e2e8f2;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
<center style="width:100%;table-layout:fixed;">
<div class="email-card">
<!-- Floating Title -->
<div class="email-header">
<img src="{{AppUrl}}assets/img/favicon.png" alt="logo" width="32" height="32" class="email-logo" />
<p class="email-brand">{{AppName}}</p>
</div>
<!-- Card -->
<div style="padding:32px 32px 24px;color:#334155;">
{{$url := HTMLFormat "<a href='%[1]s' style='color:#3b82f6;text-decoration:none;font-weight:500;'>%[2]s</a>" .Link .Repo}}
<p class="email-text">
{{.Subject}}. {{.locale.Tr "mail.repo.transfer.body" $url}}
</p>
<!-- Button -->
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="email-btn-row">
<tr>
<td align="center">
<a href="{{.Link}}" class="email-btn">{{.locale.Tr "mail.view_it_on" AppName}}</a>
</td>
</tr>
</table>
<!-- Fallback link -->
<div class="email-link-box">
<p class="email-link-label">{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
<p class="email-link-url">{{.Link}}</p>
</div>
</div>
<!-- Footer -->
<div class="email-footer">
<p class="email-footer-text">
&copy;
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
</p>
</div>
</div>
</center>
</body>
</html>
-92
View File
@@ -1,92 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<title>
{{.locale.Tr "mail.activate_account.title" (.DisplayName|DotEscape)}}
</title>
<style type="text/css">
/* Base */
body,.ExternalClass,.ReadMsgBody { background-color:#e2e8f2 }
/* Header */
.email-logo { display:inline-block;margin:0 auto 8px;border:0;outline:none }
.email-brand { margin:0;font-size:18px;font-weight:700;color:#1e293b;letter-spacing:-0.3px }
.email-header { padding:28px 32px 18px;background:linear-gradient(135deg,#dbeafe,#e0e7ff,#e8f0fe);border-bottom:1px solid #3b82f61a;text-align:center }
/* Card */
.email-card { max-width:600px;margin:24px auto;background:#ffffff;border-radius:16px;box-shadow:0 2px 4px #0000000f,0 8px 24px #0000000f,0 0 0 1px #0000000a;overflow:hidden }
.email-heading { margin:0 0 12px;font-size:17px;line-height:1.5;color:#1e293b;font-weight:600 }
.email-text { margin:0 0 8px;font-size:15px;line-height:1.6;color:#475569 }
.email-subtext { margin:0 0 20px;font-size:14px;line-height:1.6;color:#64748b }
/* Button */
.email-btn-row { margin:28px 0 24px;border-collapse:separate }
.email-btn { display:inline-block;padding:14px 40px;font-size:15px;font-weight:600;color:#ffffff!important;text-decoration:none;background-color:#3b82f6;border-radius:40px;border:1px solid #3b82f6;letter-spacing:0.2px }
/* Fallback link */
.email-link-box { background:linear-gradient(135deg,#eff4fd,#f5f7fe);border:1px solid #3b82f61a;border-radius:12px;padding:16px 18px;margin:24px 0 16px;box-shadow:inset 0 1px 0 #ffffffe6 }
.email-link-label { margin:0 0 8px;font-size:13px;color:#64748b;line-height:1.5 }
.email-link-url { margin:0;font-size:13px;color:#3b82f6;background:#ffffff;padding:10px 12px;border-radius:8px;border:1px solid #0000000f;word-break:break-all;font-family:monospace }
/* Footer */
.email-footer { padding:18px 32px 22px;background:linear-gradient(135deg,#f8f9fc,#f4f6fb);border-top:1px solid #0000000f;text-align:center }
.email-footer-text { margin:0;font-size:13px;color:#94a3b8 }
.email-footer-link { color:#3b82f6;text-decoration:none;font-weight:500 }
</style>
</head>
<body style="margin:0;padding:0;background:#e2e8f2;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
<center style="width:100%;table-layout:fixed;">
<div class="email-card">
<!-- Floating Title -->
<div class="email-header">
<img src="{{AppUrl}}assets/img/favicon.png" alt="logo" width="32" height="32" class="email-logo" />
<p class="email-brand">{{AppName}}</p>
</div>
<!-- Card -->
<div style="padding:32px 32px 24px;color:#334155;">
<p class="email-text">
{{.locale.Tr "mail.activate_account.text_1" (.DisplayName|DotEscape) AppName}}
</p>
<p class="email-subtext">
{{.locale.Tr "mail.activate_account.text_2" .ActiveCodeLives}}
</p>
<!-- Button -->
{{$activate_url := printf "%suser/activate?code=%s" AppUrl (QueryEscape .Code)}}
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="email-btn-row">
<tr>
<td align="center">
<a href="{{$activate_url}}" class="email-btn">{{.locale.Tr "mail.activate_account"}}</a>
</td>
</tr>
</table>
<!-- Fallback link -->
<div class="email-link-box">
<p class="email-link-label">{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
<p class="email-link-url">{{$activate_url}}</p>
</div>
</div>
<!-- Footer -->
<div class="email-footer">
<p class="email-footer-text">
&copy;
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
</p>
</div>
</div>
</center>
</body>
</html>
@@ -1,91 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<title>
{{.locale.Tr "mail.activate_email.title" (.DisplayName|DotEscape)}}
</title>
<style type="text/css">
/* Base */
body,.ExternalClass,.ReadMsgBody { background-color:#e2e8f2 }
/* Header */
.email-logo { display:inline-block;margin:0 auto 8px;border:0;outline:none }
.email-brand { margin:0;font-size:18px;font-weight:700;color:#1e293b;letter-spacing:-0.3px }
.email-header { padding:28px 32px 18px;background:linear-gradient(135deg,#dbeafe,#e0e7ff,#e8f0fe);border-bottom:1px solid #3b82f61a;text-align:center }
/* Card */
.email-card { max-width:600px;margin:24px auto;background:#ffffff;border-radius:16px;box-shadow:0 2px 4px #0000000f,0 8px 24px #0000000f,0 0 0 1px #0000000a;overflow:hidden }
.email-heading { margin:0 0 12px;font-size:17px;line-height:1.5;color:#1e293b;font-weight:600 }
.email-text { margin:0 0 20px;font-size:15px;line-height:1.6;color:#475569 }
/* Button */
.email-btn-row { margin:28px 0 24px;border-collapse:separate }
.email-btn { display:inline-block;padding:14px 40px;font-size:15px;font-weight:600;color:#ffffff!important;text-decoration:none;background-color:#3b82f6;border-radius:40px;border:1px solid #3b82f6;letter-spacing:0.2px }
/* Fallback link */
.email-link-box { background:linear-gradient(135deg,#eff4fd,#f5f7fe);border:1px solid #3b82f61a;border-radius:12px;padding:16px 18px;margin:24px 0 16px;box-shadow:inset 0 1px 0 #ffffffe6 }
.email-link-label { margin:0 0 8px;font-size:13px;color:#64748b;line-height:1.5 }
.email-link-url { margin:0;font-size:13px;color:#3b82f6;background:#ffffff;padding:10px 12px;border-radius:8px;border:1px solid #0000000f;word-break:break-all;font-family:monospace }
/* Footer */
.email-footer { padding:18px 32px 22px;background:linear-gradient(135deg,#f8f9fc,#f4f6fb);border-top:1px solid #0000000f;text-align:center }
.email-footer-text { margin:0;font-size:13px;color:#94a3b8 }
.email-footer-link { color:#3b82f6;text-decoration:none;font-weight:500 }
</style>
</head>
<body style="margin:0;padding:0;background:#e2e8f2;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
<center style="width:100%;table-layout:fixed;">
<div class="email-card">
<!-- Floating Title -->
<div class="email-header">
<img src="{{AppUrl}}assets/img/favicon.png" alt="logo" width="32" height="32" class="email-logo" />
<p class="email-brand">{{AppName}}</p>
</div>
<!-- Card -->
<div style="padding:32px 32px 24px;color:#334155;">
<p class="email-heading">
{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape)}}
</p>
<p class="email-text">
{{.locale.Tr "mail.activate_email.text" .ActiveCodeLives}}
</p>
<!-- Button -->
{{$activate_url := printf "%suser/activate_email?code=%s&email=%s" AppUrl (QueryEscape .Code) (QueryEscape .Email)}}
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="email-btn-row">
<tr>
<td align="center">
<a href="{{$activate_url}}" class="email-btn">{{.locale.Tr "mail.activate_email"}}</a>
</td>
</tr>
</table>
<!-- Fallback link -->
<div class="email-link-box">
<p class="email-link-label">{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
<p class="email-link-url">{{$activate_url}}</p>
</div>
</div>
<!-- Footer -->
<div class="email-footer">
<p class="email-footer-text">
&copy;
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
</p>
</div>
</div>
</center>
</body>
</html>
@@ -1,100 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<title>
{{.locale.Tr "mail.register_notify.title" (.DisplayName|DotEscape) AppName}}
</title>
<style type="text/css">
/* Base */
body,.ExternalClass,.ReadMsgBody { background-color:#e2e8f2 }
/* Header */
.email-logo { display:inline-block;margin:0 auto 8px;border:0;outline:none }
.email-brand { margin:0;font-size:18px;font-weight:700;color:#1e293b;letter-spacing:-0.3px }
.email-header { padding:28px 32px 18px;background:linear-gradient(135deg,#dbeafe,#e0e7ff,#e8f0fe);border-bottom:1px solid #3b82f61a;text-align:center }
/* Card */
.email-card { max-width:600px;margin:24px auto;background:#ffffff;border-radius:16px;box-shadow:0 2px 4px #0000000f,0 8px 24px #0000000f,0 0 0 1px #0000000a;overflow:hidden }
.email-heading { margin:0 0 12px;font-size:17px;line-height:1.5;color:#1e293b;font-weight:600 }
.email-text { margin:0 0 8px;font-size:15px;line-height:1.6;color:#475569 }
.email-subtext { margin:0 0 20px;font-size:14px;line-height:1.6;color:#64748b }
/* Button */
.email-btn-row { margin:28px 0 24px;border-collapse:separate }
.email-btn { display:inline-block;padding:14px 40px;font-size:15px;font-weight:600;color:#ffffff!important;text-decoration:none;background-color:#3b82f6;border-radius:40px;border:1px solid #3b82f6;letter-spacing:0.2px }
/* Fallback link */
.email-link-box { background:linear-gradient(135deg,#eff4fd,#f5f7fe);border:1px solid #3b82f61a;border-radius:12px;padding:16px 18px;margin:24px 0 16px;box-shadow:inset 0 1px 0 #ffffffe6 }
.email-link-label { margin:0 0 8px;font-size:13px;color:#64748b;line-height:1.5 }
.email-link-url { margin:0;font-size:13px;color:#3b82f6;background:#ffffff;padding:10px 12px;border-radius:8px;border:1px solid #0000000f;word-break:break-all;font-family:monospace }
/* Footer */
.email-footer { padding:18px 32px 22px;background:linear-gradient(135deg,#f8f9fc,#f4f6fb);border-top:1px solid #0000000f;text-align:center }
.email-footer-text { margin:0;font-size:13px;color:#94a3b8 }
.email-footer-link { color:#3b82f6;text-decoration:none;font-weight:500 }
</style>
</head>
<body style="margin:0;padding:0;background:#e2e8f2;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
<center style="width:100%;table-layout:fixed;">
<div class="email-card">
<!-- Floating Title -->
<div class="email-header">
<img src="{{AppUrl}}assets/img/favicon.png" alt="logo" width="32" height="32" class="email-logo" />
<p class="email-brand">{{AppName}}</p>
</div>
<!-- Card -->
<div style="padding:32px 32px 24px;color:#334155;">
<p class="email-heading">
{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape)}}
</p>
<p class="email-text">
{{.locale.Tr "mail.register_notify.text_1" AppName}}
</p>
<p class="email-subtext">
{{.locale.Tr "mail.register_notify.text_2" .Username}}
</p>
<!-- Button -->
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="email-btn-row">
<tr>
<td align="center">
<a href="{{AppUrl}}user/login" class="email-btn">{{.locale.Tr "mail.view_it_on" AppName}}</a>
</td>
</tr>
</table>
{{$set_pwd_url := printf "%suser/forgot_password" AppUrl}}
<p style="margin:0 0 16px;font-size:14px;line-height:1.6;color:#64748b;">
{{.locale.Tr "mail.register_notify.text_3" $set_pwd_url}}
</p>
<!-- Fallback link -->
<div class="email-link-box">
<p class="email-link-label">{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
<p class="email-link-url">{{AppUrl}}user/login</p>
</div>
</div>
<!-- Footer -->
<div class="email-footer">
<p class="email-footer-text">
&copy;
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
</p>
</div>
</div>
</center>
</body>
</html>
@@ -1,91 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<title>
{{.locale.Tr "mail.reset_password.title" (.DisplayName|DotEscape)}}
</title>
<style type="text/css">
/* Base */
body,.ExternalClass,.ReadMsgBody { background-color:#e2e8f2 }
/* Header */
.email-logo { display:inline-block;margin:0 auto 8px;border:0;outline:none }
.email-brand { margin:0;font-size:18px;font-weight:700;color:#1e293b;letter-spacing:-0.3px }
.email-header { padding:28px 32px 18px;background:linear-gradient(135deg,#dbeafe,#e0e7ff,#e8f0fe);border-bottom:1px solid #3b82f61a;text-align:center }
/* Card */
.email-card { max-width:600px;margin:24px auto;background:#ffffff;border-radius:16px;box-shadow:0 2px 4px #0000000f,0 8px 24px #0000000f,0 0 0 1px #0000000a;overflow:hidden }
.email-heading { margin:0 0 12px;font-size:17px;line-height:1.5;color:#1e293b;font-weight:600 }
.email-text { margin:0 0 20px;font-size:15px;line-height:1.6;color:#475569 }
/* Button */
.email-btn-row { margin:28px 0 24px;border-collapse:separate }
.email-btn { display:inline-block;padding:14px 40px;font-size:15px;font-weight:600;color:#ffffff!important;text-decoration:none;background-color:#3b82f6;border-radius:40px;border:1px solid #3b82f6;letter-spacing:0.2px }
/* Fallback link */
.email-link-box { background:linear-gradient(135deg,#eff4fd,#f5f7fe);border:1px solid #3b82f61a;border-radius:12px;padding:16px 18px;margin:24px 0 16px;box-shadow:inset 0 1px 0 #ffffffe6 }
.email-link-label { margin:0 0 8px;font-size:13px;color:#64748b;line-height:1.5 }
.email-link-url { margin:0;font-size:13px;color:#3b82f6;background:#ffffff;padding:10px 12px;border-radius:8px;border:1px solid #0000000f;word-break:break-all;font-family:monospace }
/* Footer */
.email-footer { padding:18px 32px 22px;background:linear-gradient(135deg,#f8f9fc,#f4f6fb);border-top:1px solid #0000000f;text-align:center }
.email-footer-text { margin:0;font-size:13px;color:#94a3b8 }
.email-footer-link { color:#3b82f6;text-decoration:none;font-weight:500 }
</style>
</head>
<body style="margin:0;padding:0;background:#e2e8f2;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
<center style="width:100%;table-layout:fixed;">
<div class="email-card">
<!-- Floating Title -->
<div class="email-header">
<img src="{{AppUrl}}assets/img/favicon.png" alt="logo" width="32" height="32" class="email-logo" />
<p class="email-brand">{{AppName}}</p>
</div>
<!-- Card -->
<div style="padding:32px 32px 24px;color:#334155;">
<p class="email-heading">
{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape)}}
</p>
<p class="email-text">
{{.locale.Tr "mail.reset_password.text" .ResetPwdCodeLives}}
</p>
<!-- Button -->
{{$recover_url := printf "%suser/recover_account?code=%s" AppUrl (QueryEscape .Code)}}
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="email-btn-row">
<tr>
<td align="center">
<a href="{{$recover_url}}" class="email-btn">{{.locale.Tr "mail.reset_password"}}</a>
</td>
</tr>
</table>
<!-- Fallback link -->
<div class="email-link-box">
<p class="email-link-label">{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
<p class="email-link-url">{{$recover_url}}</p>
</div>
</div>
<!-- Footer -->
<div class="email-footer">
<p class="email-footer-text">
&copy;
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
</p>
</div>
</div>
</center>
</body>
</html>
+61
View File
@@ -0,0 +1,61 @@
/* Base */
body,.ExternalClass,.ReadMsgBody { background-color:#e2e8f2 }
/* Header */
.email-logo { display:inline-block;margin:0 auto 8px;border:0;outline:none }
.email-brand { margin:0;font-size:18px;font-weight:700;color:#1e293b;letter-spacing:-0.3px }
.email-header { padding:28px 32px 18px;background:linear-gradient(135deg,#dbeafe,#e0e7ff,#e8f0fe);border-bottom:1px solid #3b82f61a;text-align:center }
/* Card */
.email-card { max-width:600px;margin:24px auto;background:#ffffff;border-radius:16px;box-shadow:0 2px 4px #0000000f,0 8px 24px #0000000f,0 0 0 1px #0000000a;overflow:hidden }
.email-heading { margin:0 0 12px;font-size:17px;line-height:1.5;color:#1e293b;font-weight:600 }
.email-text { margin:0 0 8px;font-size:15px;line-height:1.6;color:#475569 }
.email-subtext { margin:0 0 20px;font-size:14px;line-height:1.6;color:#64748b }
/* Button */
.email-btn-row { margin:28px 0 24px;border-collapse:separate }
.email-btn,.card-body .email-btn,.card-body .email-btn:link,.card-body .email-btn:visited { display:inline-block;padding:14px 40px;font-size:15px;font-weight:600;color:#ffffff!important;text-decoration:none!important;background-color:#3b82f6;border-radius:40px;border:1px solid #3b82f6;letter-spacing:0.2px }
/* Fallback link */
.email-link-box { background:linear-gradient(135deg,#eff4fd,#f5f7fe);border:1px solid #3b82f61a;border-radius:12px;padding:16px 18px;margin:24px 0 16px;box-shadow:inset 0 1px 0 #ffffffe6 }
.email-link-label { margin:0 0 8px;font-size:13px;color:#64748b;line-height:1.5 }
.email-link-url { margin:0;font-size:13px;color:#3b82f6;background:#ffffff;padding:10px 12px;border-radius:8px;border:1px solid #0000000f;word-break:break-all;font-family:monospace }
/* Footer */
.email-footer { padding:18px 32px 22px;background:linear-gradient(135deg,#f8f9fc,#f4f6fb);border-top:1px solid #0000000f;text-align:center }
.email-footer-text { margin:0;font-size:13px;color:#94a3b8 }
.email-footer-link { color:#3b82f6;text-decoration:none;font-weight:500 }
/* Official-snapshot presentation adapter: no content or translation overrides. */
body { margin:0; padding:24px 16px; background-color:#e2e8f2; }
body, td { font-family:Arial,sans-serif; }
.email-wrap { width:100%; max-width:600px; border-collapse:separate; border-spacing:0; }
.card-body { font-size:14px; line-height:1.7; overflow-wrap:anywhere; }
.card-body > pre, .card-body code { font-family:Consolas,monospace; white-space:pre-wrap; overflow-wrap:anywhere; }
.card-body ul { padding-left:22px; }
.card-body blockquote { padding-left:16px; border-left:3px solid #3b82f6; }
.mail-footer { margin:0; }
.mail-footer td { border-top-color:transparent!important; color:#334155!important; }
.mail-footer a { color:#3b82f6!important; text-decoration:none; }
.email-body { padding:32px 32px 24px; color:#334155; }
.glass-halo { display:inline-block; padding:15px 17px; border-radius:18px; background:#ffffffa8; border:1px solid #ffffff; box-shadow:0 4px 18px #3b82f617; }
.email-body { background:linear-gradient(180deg,#ffffff,#fefeff); }
@media only screen and (max-width:480px) {
body { padding:12px 8px; }
.email-wrap { margin:0 auto; }
.email-body { padding:24px 18px!important; }
.card-body { width:auto!important; }
.resp-hide { display:none!important; }
}
/* Shared framework controls: keep URLs readable, never style them as CTAs. */
.email-link-cell { padding:16px 18px; }
.email-link-url a,.card-body .email-link-url a { color:inherit!important; text-decoration:none!important; font-size:inherit; }
.email-link-url { overflow-wrap:anywhere; word-break:break-all; }
.email-action { margin:20px 0 0; }
.email-btn-row { box-sizing:border-box; }
.card-body .email-btn { max-width:100%; box-sizing:border-box; white-space:normal; overflow-wrap:anywhere; }
@media only screen and (max-width:480px) { .card-body .email-btn { display:block; text-align:center; width:100%; } .email-btn-table { width:100%; } }
+6
View File
@@ -0,0 +1,6 @@
{
"name": "bloom",
"description": "Creative / Startup",
"mode": "framed",
"layout": "bloom"
}
-106
View File
@@ -1,106 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<title>
{{.locale.Tr "mail.team_invite.subject" (.Inviter.DisplayName|DotEscape) (.Organization.DisplayName|DotEscape)}}
</title>
<style type="text/css">
/* Base */
body,.ExternalClass,.ReadMsgBody { background-color:#fff7ed }
/* Header */
.email-logo { display:inline-block;vertical-align:middle;margin:0 10px;border:0;outline:none }
.email-brand { font-size:20px;font-weight:700;color:#9a3412;letter-spacing:-0.3px;vertical-align:middle;font-family:Georgia,'Times New Roman',serif }
.email-header { padding:28px 32px 16px;border-bottom:1px solid #fed7aa;background:linear-gradient(135deg,#fff7ed,#ffffff) }
/* Card */
.email-card { max-width:600px;margin:20px auto;background:#ffffff;border-radius:16px;box-shadow:0 2px 16px #ea580c14;overflow:hidden;border:1px solid #fed7aa }
.email-heading { margin:0 0 12px;font-size:17px;line-height:1.5;color:#9a3412;font-family:Georgia,'Times New Roman',serif;font-weight:600 }
.email-text { margin:0 0 8px;font-size:16px;line-height:1.6;color:#78350f }
.email-subtext { margin:0 0 20px;font-size:15px;line-height:1.6;color:#92400e }
/* Button */
.email-btn-row { margin:28px 0 24px;border-collapse:separate }
.email-btn { display:inline-block;padding:14px 40px;font-size:16px;font-weight:600;color:#ffffff!important;text-decoration:none;background:linear-gradient(135deg,#ea580c,#c2410c);border-radius:50px;font-family:system-ui,sans-serif }
/* Fallback link */
.email-link-box { background:#fff7ed;border:1px solid #fed7aa;border-radius:12px;padding:16px 18px;margin:24px 0 16px }
.email-link-label { margin:0 0 8px;font-size:14px;color:#92400e;line-height:1.5 }
.email-link-url { margin:0;font-size:13px;color:#c2410c;background:#ffffff;padding:10px 12px;border-radius:8px;border:1px solid #fed7aa;word-break:break-all;font-family:monospace }
/* Footer */
.email-footer { padding:20px 32px 24px;background:#fff7ed;border-top:1px solid #fed7aa }
.email-footer-text { margin:0;font-size:14px;color:#92400e }
.email-footer-link { color:#ea580c;text-decoration:none;font-weight:600 }
</style>
</head>
<body style="margin:0;padding:0;background-color:#fff7ed;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
<center style="width:100%;table-layout:fixed;">
<div class="email-card">
<!-- Floating Title -->
<div class="email-header">
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-collapse:collapse;">
<tr>
<td style="vertical-align:middle;">
<span style="font-size:15px;">&#x1f44b;</span>
<img src="{{AppUrl}}assets/img/favicon.png" alt="logo" width="30" height="30" class="email-logo" />
<span class="email-brand">{{AppName}}</span>
</td>
</tr>
</table>
</div>
<!-- Card -->
<div style="padding:32px 32px 24px;color:#78350f;">
<p class="email-heading">
{{.locale.Tr "mail.hi_user_x" .Invite.Email}}
</p>
<p class="email-text">
{{.locale.Tr "mail.team_invite.text_1" (DotEscape .Inviter.DisplayName) (DotEscape .Team.Name) (DotEscape .Organization.DisplayName)}}
</p>
<p class="email-subtext">
{{.locale.Tr "mail.team_invite.text_2"}}
</p>
<!-- Button -->
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="email-btn-row">
<tr>
<td align="center">
<a href="{{.InviteURL}}" class="email-btn">{{.locale.Tr "mail.view_it_on" AppName}}</a>
</td>
</tr>
</table>
<!-- Fallback link -->
<div class="email-link-box">
<p class="email-link-label">{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
<p class="email-link-url">{{.InviteURL}}</p>
</div>
<p style="margin:16px 0 0;font-size:14px;color:#92400e;line-height:1.5;">
{{.locale.Tr "mail.team_invite.text_3" .Invite.Email}}
</p>
</div>
<!-- Footer -->
<div class="email-footer">
<p class="email-footer-text">
&copy;
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
</p>
</div>
</div>
</center>
</body>
</html>
@@ -1,107 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<title>
{{.Subject}}
</title>
<style type="text/css">
/* Base */
body,.ExternalClass,.ReadMsgBody { background-color:#fff7ed }
/* Header */
.email-logo { display:inline-block;vertical-align:middle;margin:0 10px;border:0;outline:none }
.email-brand { font-size:20px;font-weight:700;color:#9a3412;letter-spacing:-0.3px;vertical-align:middle;font-family:Georgia,'Times New Roman',serif }
.email-header { padding:28px 32px 16px;border-bottom:1px solid #fed7aa;background:linear-gradient(135deg,#fff7ed,#ffffff) }
/* Card */
.email-card { max-width:600px;margin:20px auto;background:#ffffff;border-radius:16px;box-shadow:0 2px 16px #ea580c14;overflow:hidden;border:1px solid #fed7aa }
.email-heading { font-size:19px;font-weight:700;color:#9a3412;margin:0 0 16px;font-family:Georgia,'Times New Roman',serif }
/* Content */
.job-list { list-style:none;padding:0;margin:0 0 24px 0 }
.job-item { background-color:#fff7ed;border:1px solid #fed7aa;border-radius:10px;padding:12px 16px;margin-bottom:8px }
.job-link { color:#c2410c;text-decoration:none;font-weight:600;font-size:15px;display:block }
/* Button */
.email-btn-row { margin:16px 0 24px;border-collapse:separate }
.email-btn { display:inline-block;padding:14px 40px;font-size:16px;font-weight:600;color:#ffffff!important;text-decoration:none;background:linear-gradient(135deg,#ea580c,#c2410c);border-radius:50px;font-family:system-ui,sans-serif }
/* Fallback link */
.email-link-box { background:#fff7ed;border:1px solid #fed7aa;border-radius:12px;padding:16px 18px;margin:24px 0 16px }
.email-link-label { margin:0 0 8px;font-size:14px;color:#92400e;line-height:1.5 }
.email-link-url { margin:0;font-size:13px;color:#c2410c;background:#ffffff;padding:10px 12px;border-radius:8px;border:1px solid #fed7aa;word-break:break-all;font-family:monospace }
/* Footer */
.email-footer { padding:20px 32px 24px;background:#fff7ed;border-top:1px solid #fed7aa }
.email-footer-text { margin:0;font-size:14px;color:#92400e }
.email-footer-link { color:#ea580c;text-decoration:none;font-weight:600 }
</style>
</head>
<body style="margin:0;padding:0;background-color:#fff7ed;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
<center style="width:100%;table-layout:fixed;">
<div class="email-card">
<!-- Floating Title -->
<div class="email-header">
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-collapse:collapse;">
<tr>
<td style="vertical-align:middle;">
<span style="font-size:15px;">&#x1f44b;</span>
<img src="{{AppUrl}}assets/img/favicon.png" alt="logo" width="30" height="30" class="email-logo" />
<span class="email-brand">{{AppName}}</span>
</td>
</tr>
</table>
</div>
<!-- Card -->
<div style="padding:32px 32px 24px;color:#78350f;">
<h2 class="email-heading">
{{.Repo.FullName}} &mdash; {{.Run.WorkflowID}}: {{.RunStatusText}}
</h2>
<ul class="job-list">
{{range $job := .Jobs}}
<li class="job-item">
<a href="{{$job.HTMLURL}}" class="job-link">
{{$job.Status}}: {{$job.Name}}{{if gt $job.Attempt 1}} (retry #{{$job.Attempt}}){{end}} &mdash; {{$job.Duration}}
</a>
</li>
{{end}}
</ul>
<!-- Button -->
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="email-btn-row">
<tr>
<td align="center">
<a href="{{.Run.HTMLURL}}" class="email-btn">{{.locale.Tr "mail.view_it_on" AppName}}</a>
</td>
</tr>
</table>
<!-- Fallback link -->
<div class="email-link-box">
<p class="email-link-label">{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
<p class="email-link-url">{{.Run.HTMLURL}}</p>
</div>
</div>
<!-- Footer -->
<div class="email-footer">
<p class="email-footer-text">
&copy;
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
</p>
</div>
</div>
</center>
</body>
</html>
-95
View File
@@ -1,95 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<title>
{{.Subject}}
</title>
<style type="text/css">
/* Base */
body,.ExternalClass,.ReadMsgBody { background-color:#fff7ed }
/* Header */
.email-logo { display:inline-block;vertical-align:middle;margin:0 10px;border:0;outline:none }
.email-brand { font-size:20px;font-weight:700;color:#9a3412;letter-spacing:-0.3px;vertical-align:middle;font-family:Georgia,'Times New Roman',serif }
.email-header { padding:28px 32px 16px;border-bottom:1px solid #fed7aa;background:linear-gradient(135deg,#fff7ed,#ffffff) }
/* Card */
.email-card { max-width:600px;margin:20px auto;background:#ffffff;border-radius:16px;box-shadow:0 2px 16px #ea580c14;overflow:hidden;border:1px solid #fed7aa }
.email-text { margin:0 0 20px;font-size:16px;line-height:1.6;color:#78350f }
/* Typography utilities */
code { background:#fff7ed;padding:2px 8px;border-radius:6px;font-size:15px;color:#c2410c;font-weight:600 }
/* Button */
.email-btn-row { margin:28px 0 24px;border-collapse:separate }
.email-btn { display:inline-block;padding:14px 40px;font-size:16px;font-weight:600;color:#ffffff!important;text-decoration:none;background:linear-gradient(135deg,#ea580c,#c2410c);border-radius:50px;font-family:system-ui,sans-serif }
/* Fallback link */
.email-link-box { background:#fff7ed;border:1px solid #fed7aa;border-radius:12px;padding:16px 18px;margin:24px 0 16px }
.email-link-label { margin:0 0 8px;font-size:14px;color:#92400e;line-height:1.5 }
.email-link-url { margin:0;font-size:13px;color:#c2410c;background:#ffffff;padding:10px 12px;border-radius:8px;border:1px solid #fed7aa;word-break:break-all;font-family:monospace }
/* Footer */
.email-footer { padding:20px 32px 24px;background:#fff7ed;border-top:1px solid #fed7aa }
.email-footer-text { margin:0;font-size:14px;color:#92400e }
.email-footer-link { color:#ea580c;text-decoration:none;font-weight:600 }
</style>
</head>
<body style="margin:0;padding:0;background-color:#fff7ed;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
<center style="width:100%;table-layout:fixed;">
<div class="email-card">
<!-- Floating Title -->
<div class="email-header">
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-collapse:collapse;">
<tr>
<td style="vertical-align:middle;">
<span style="font-size:15px;">&#x1f44b;</span>
<img src="{{AppUrl}}assets/img/favicon.png" alt="logo" width="30" height="30" class="email-logo" />
<span class="email-brand">{{AppName}}</span>
</td>
</tr>
</table>
</div>
<!-- Card -->
<div style="padding:32px 32px 24px;color:#78350f;">
<p class="email-text">
{{.locale.Tr "mail.repo.collaborator.added.text"}} <code>{{.RepoName}}</code>.
</p>
<!-- Button -->
<table border="0" cellpadding="0" cellspacing="0" width="100%" class="email-btn-row">
<tr>
<td align="center">
<a href="{{.Link}}" class="email-btn">{{.locale.Tr "mail.view_it_on" AppName}}</a>
</td>
</tr>
</table>
<!-- Fallback link -->
<div class="email-link-box">
<p class="email-link-label">{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
<p class="email-link-url">{{.Link}}</p>
</div>
</div>
<!-- Footer -->
<div class="email-footer">
<p class="email-footer-text">
&copy;
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
</p>
</div>
</div>
</center>
</body>
</html>
Loaded 100 of 232 files, more files were not shown because too many files have changed in this diff. Show more