Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4d96de79e | ||
|
|
fec3ace600 | ||
|
|
5c0589f6f6 | ||
|
|
14dc37046d | ||
|
|
23f0456622 | ||
|
|
92d2ba9889 | ||
|
|
ffb674669b | ||
|
|
4ff008e564 | ||
|
|
dfb5024dfb | ||
|
|
a4ba20418c | ||
|
|
3f6a2851c7 | ||
|
|
e06e215d2a | ||
|
|
d020e35f77 | ||
|
|
ac559eb26c | ||
|
|
f33cad7a7e | ||
|
|
070858d473 | ||
|
|
0fd9afa671 | ||
|
|
349ce602f3 |
No files matched your search
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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")
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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 -->
|
||||
[](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()
|
||||
@@ -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}")
|
||||
@@ -2,130 +2,46 @@ name: Gitea Version Tracker
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Daily at 08:00 UTC — check for new Gitea releases
|
||||
- cron: '0 8 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
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
|
||||
|
||||
jobs:
|
||||
track:
|
||||
name: Track Gitea Release
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: linux-amd64-docker-small
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- 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
|
||||
run: |
|
||||
if [ -n "${{ github.event.inputs.version }}" ]; then
|
||||
NEW_VERSION="${{ github.event.inputs.version }}"
|
||||
else
|
||||
# Fetch latest release from GitHub API (go-gitea/gitea mirror)
|
||||
RELEASE_JSON=$(curl -sSf https://api.github.com/repos/go-gitea/gitea/releases/latest)
|
||||
NEW_VERSION=$(echo "$RELEASE_JSON" | jq -r '.tag_name')
|
||||
fi
|
||||
# Strip 'v' prefix if present (API returns v1.27.0, we use 1.27.0)
|
||||
NEW_VERSION="${NEW_VERSION#v}"
|
||||
echo "New Gitea version: $NEW_VERSION"
|
||||
|
||||
# Parse marker line AND embedded OFFSET to locate the data row
|
||||
MARKER_INFO=$(grep -n 'TRACKER:QUICK-REF-MAX' COMPATIBILITY.md)
|
||||
MARKER_LINE=$(echo "$MARKER_INFO" | cut -d: -f1)
|
||||
OFFSET=$(echo "$MARKER_INFO" | sed 's/.*OFFSET=\([0-9]*\).*/\1/')
|
||||
CURRENT=$(sed -n "$((MARKER_LINE + OFFSET))p" COMPATIBILITY.md | awk -F'|' '{gsub(/\*| /, "", $4); print $4}')
|
||||
echo "Current max tested: $CURRENT"
|
||||
|
||||
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "current_version=$CURRENT" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ "$NEW_VERSION" = "$CURRENT" ]; then
|
||||
echo "No new Gitea release detected. Skipping."
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "New version detected: $CURRENT → $NEW_VERSION"
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: COMPATIBILITY.md — Quick Reference (max tested version)
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
run: |
|
||||
NEW="${{ steps.version.outputs.new_version }}"
|
||||
# Parse marker line and embedded OFFSET to locate the data row
|
||||
MARKER_INFO=$(grep -n 'TRACKER:QUICK-REF-MAX' COMPATIBILITY.md)
|
||||
MARKER=$(echo "$MARKER_INFO" | cut -d: -f1)
|
||||
OFFSET=$(echo "$MARKER_INFO" | sed 's/.*OFFSET=\([0-9]*\).*/\1/')
|
||||
TARGET=$((MARKER + OFFSET))
|
||||
# Update column 4 (Max Tested) and column 5 (Status) in the pipe-separated row
|
||||
awk -i inplace -F'|' -v OFS='|' -v ver="${NEW}" '
|
||||
NR == '$TARGET' { $4 = " **" ver "** "; $5 = " ⏳ Pending Verification " }
|
||||
{ print }
|
||||
' COMPATIBILITY.md
|
||||
|
||||
- name: COMPATIBILITY.md — Version History table
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
run: |
|
||||
NEW="${{ steps.version.outputs.new_version }}"
|
||||
TODAY=$(date +%Y-%m-%d)
|
||||
# Parse marker line and embedded OFFSET for the insertion point
|
||||
MARKER_INFO=$(grep -n 'TRACKER:VERSION-INSERT' COMPATIBILITY.md)
|
||||
MARKER=$(echo "$MARKER_INFO" | cut -d: -f1)
|
||||
OFFSET=$(echo "$MARKER_INFO" | sed 's/.*OFFSET=\([0-9]*\).*/\1/')
|
||||
INSERT=$((MARKER + OFFSET))
|
||||
ROW="| **${NEW}** | ${TODAY} | ⏳ Pending Verification | TBD |"
|
||||
sed -i "${INSERT}a ${ROW}" COMPATIBILITY.md
|
||||
|
||||
- name: 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.{ja,ko,ru,zh-CN,zh-TW}.md; do
|
||||
sed -i "/TRACKER:LATEST-TESTED/s/Gitea [0-9.]*/Gitea ${NEW}/" "$f"
|
||||
done
|
||||
run: python -B .github/scripts/track_gitea_release.py
|
||||
env:
|
||||
GITEA_VERSION: ${{ github.event.inputs.version }}
|
||||
# Optional GitHub token for upstream API rate limits; never use the Gitea job token.
|
||||
GITHUB_TOKEN: ${{ secrets.UPSTREAM_GITHUB_TOKEN }}
|
||||
|
||||
- name: Create Pull Request
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
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
|
||||
labels: compatibility, automated
|
||||
if: steps.version.outputs.changed == 'true'
|
||||
run: python -B .github/scripts/gitea_actions.py create-pull-request --version "$UPSTREAM_VERSION"
|
||||
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 }}
|
||||
@@ -8,15 +8,32 @@ on:
|
||||
jobs:
|
||||
validate:
|
||||
name: Validate Templates
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: linux-amd64-docker-small
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
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
|
||||
run: go run . preview all
|
||||
|
||||
@@ -24,26 +41,81 @@ jobs:
|
||||
name: Package & Release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: validate
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: linux-amd64-docker-small
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- 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: |
|
||||
VERSION=${GITHUB_REF#refs/tags/}
|
||||
ARCHIVE="gitea-mail-templates-${VERSION}"
|
||||
mkdir -p "dist/${ARCHIVE}"
|
||||
cp -r themes LICENSE README.md COMPATIBILITY.md "dist/${ARCHIVE}/"
|
||||
cd dist
|
||||
zip -r "${ARCHIVE}.zip" "${ARCHIVE}"
|
||||
tar -czf "${ARCHIVE}.tar.gz" "${ARCHIVE}"
|
||||
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"'
|
||||
|
||||
- name: Verify release notes
|
||||
run: test -s ".github/release-notes/${GITHUB_REF_NAME}.md"
|
||||
|
||||
- name: Update release labels in the 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
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: dist/*.zip,dist/*.tar.gz
|
||||
generate_release_notes: true
|
||||
run: python -B .github/scripts/gitea_actions.py publish-release --version "$TEMPLATE_RELEASE"
|
||||
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 }}
|
||||
@@ -1,7 +1,10 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
# Go dependencies
|
||||
vendor/
|
||||
|
||||
# Python test cache
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -18,8 +21,10 @@ Desktop.ini
|
||||
*.zip
|
||||
*.tar.gz
|
||||
|
||||
# Package lock file
|
||||
package-lock.json
|
||||
|
||||
# Preview generated files
|
||||
preview/rendered.js
|
||||
preview/rendered/
|
||||
build/
|
||||
/upstream/
|
||||
dist/
|
||||
node_modules/
|
||||
@@ -1,81 +1,72 @@
|
||||
# AGENTS.md — Gitea Mail Templates
|
||||
<!-- DOC-TAGS: {"TRACKER":["UPSTREAM"],"RELEASE":["CURRENT"]} -->
|
||||
|
||||
## 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
|
||||
|
||||
```
|
||||
themes/ # Template themes (10 styles, 11 .tmpl each = 110 source files)
|
||||
aurora/ # Ethereal / Dreamlike
|
||||
bloom/ # Creative / Startup (glassmorphism)
|
||||
ember/ # Community / Open Source
|
||||
heritage/ # Education / Research
|
||||
horizon/ # Enterprise / Corporate
|
||||
ink/ # Editorial / Publishing
|
||||
mono/ # Minimal / Swiss design
|
||||
neon/ # Cyberpunk / Gaming
|
||||
terminal/ # Developers / Tech
|
||||
terra/ # Nature / Sustainability
|
||||
tools/ # Go CLI tooling (modular, zero dependencies)
|
||||
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 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/ # Multi-language documentation
|
||||
gitea.lock.json # Generated official version, commit and file SHA-256; no vendored upstream source
|
||||
framework/ # Shared header/action/fallback/footer controls and structural layout presets
|
||||
themes/<name>/ # theme.json and theme.css only; no mail data/logic/translation definitions
|
||||
tools/ # Go 1.24+ CLI; urfave/cli and x/net HTML validation
|
||||
upstream/ # Explicit sync, checksum/adapter verification and key discovery
|
||||
builder/ # One official-context alignment layer and theme package generation
|
||||
config/, data/ # Names, descriptions and mock contexts; not a template inventory
|
||||
preview/ # Official-compatible rendering, locale adapter and SSE dev server
|
||||
cli/ # list, create, delete, build, preview, dev, upstream
|
||||
build/themes/ # Generated installable overrides; ignored
|
||||
build/upstream/ # Downloaded immutable official mail/locales/assets/license; ignored
|
||||
preview/ # Vanilla JS UI, generated manifest and per-language scripts
|
||||
docs/ # English/Simplified Chinese documentation and gallery images
|
||||
```
|
||||
|
||||
## Working With Templates
|
||||
## Source and Theme Rules
|
||||
|
||||
### Template Files
|
||||
- All `.tmpl` files use Go `html/template` syntax
|
||||
- Must use only Gitea's built-in template functions: `AppUrl`, `DotEscape`, `QueryEscape`, `ShortSha`, `HTMLFormat`, `PathEscapeSegments`, `FileSize`
|
||||
- Must use only official Gitea translation keys (`mail.*` namespace)
|
||||
- Never reference `.DisplayName` in templates where the data context lacks it (collaborator, transfer, release, workflow_run, assigned, default)
|
||||
- Each style must have all 11 template types
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- Changed reviewed translation/mail-renderer references require an adapter review before updating reference hashes.
|
||||
- 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
|
||||
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
|
||||
## Development
|
||||
|
||||
### Preview System
|
||||
- `preview/index.html` loads `preview/rendered.js` (pre-rendered by Go) and displays in iframes
|
||||
- Supports theme/template switching, view mode (Modern/Source), and viewport toggle (Desktop 1386x780 / Mobile 390x780)
|
||||
- Keyboard navigation: `←→` cycles focus between Theme/Template/View selects, `↑↓` selects within the focused dropdown, `d`/`m` toggles viewport
|
||||
- `REGISTRY` and `PARAMS` are auto-generated from `templates_config.json` — no manual syncing needed
|
||||
- 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
|
||||
1. `cd tools && go run . create <name>`.
|
||||
2. Edit only theme metadata/CSS. Shared controls or layout presets are framework changes.
|
||||
3. Run `go run . upstream prepare`, `go test ./...` and `go run . preview all`.
|
||||
4. Check desktop/mobile layouts and languages; update the gallery if appropriate.
|
||||
|
||||
### Build Tool
|
||||
- `tools/tools.go` is the main entry point for the modular CLI
|
||||
- Subcommands: `list`, `create`, `delete`, `preview`, `dev`
|
||||
- Template metadata lives in `tools/data/templates_config.json` — the single source of truth
|
||||
- `tools/config/` handles config loading and data flattening
|
||||
- `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
|
||||
`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.
|
||||
|
||||
`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.
|
||||
|
||||
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.
|
||||
|
||||
## Versioning
|
||||
|
||||
<!-- RELEASE:CURRENT -->
|
||||
- Current template release: **v28.0.0**.
|
||||
<!-- /RELEASE:CURRENT -->
|
||||
- The snapshot-driven source refactor is unreleased. Its baseline is Gitea v28.0.0; do not replace the existing v28.0.0 assets.
|
||||
- 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
|
||||
- `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
|
||||
- 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)
|
||||
Use `style(name):`, `preview:`, `tools:`, `docs:`, `fix:`, `project:`, `refactor:` or `chore:` as appropriate. Preserve unrelated user changes and keep generated files out of commits.
|
||||
@@ -1,16 +1,61 @@
|
||||
# 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 -->
|
||||
| Template Release | Min Gitea | Max Tested Gitea | Status |
|
||||
|-----------------|-----------|-----------------|--------|
|
||||
| **v1.0.1** | **1.25.0** | **1.26.4** | ✅ Active |
|
||||
| **v1.0.0** | **1.25.0** | **1.26.4** | ✅ Superseded |
|
||||
## Compatibility Matrix
|
||||
|
||||
> **Latest verified:** All 11 templates pass validation against Gitea 1.26.4 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.
|
||||
|
||||
<!-- 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 -->
|
||||
|
||||
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.
|
||||
|
||||
<!-- 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.
|
||||
<!-- /TRACKER:LATEST-VERIFIED -->
|
||||
|
||||
## Snapshot-Driven Source Status
|
||||
|
||||
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
|
||||
|
||||
@@ -22,26 +67,35 @@ gitea --version
|
||||
|
||||
## Gitea Version History — Mail Template Impact
|
||||
|
||||
<!-- TRACKER:VERSION-INSERT OFFSET=2 -->
|
||||
| Gitea | Release Date | Mail Template Changes | Breaking? |
|
||||
|-------|-------------|----------------------|-----------|
|
||||
| **1.26.4** | 2026-06-21 | None — hotfix release | No |
|
||||
| **1.26.3** | 2026-06-20 | None — security release | No |
|
||||
| **1.26.2** | 2026-05-20 | None — security + bug fixes | No |
|
||||
| **1.26.1** | 2026-04-22 | None — bug fixes | No |
|
||||
| **1.26.0** | 2026-04-19 | AppURL cleanup; SanitizeHTML deprecated → use HTMLFormat | No |
|
||||
| **1.25.5** | 2026-03-10 | None — security + maintenance | No |
|
||||
| **1.25.0** | 2025 | **Directory restructure** — templates moved to `mail/<category>/<type>.tmpl` (PR #35150); subject/body split with `---` separator; template preview support added | **Yes** (structural) |
|
||||
| **≤ 1.24.x** | — | Flat directory structure under `custom/templates/mail/` | ❌ Unsupported |
|
||||
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.
|
||||
|
||||
<!-- 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 | `FileSize` replaced by `FormatByteSize`; mail template internals reorganized | **Breaking:** legacy release-attachment templates can fail |
|
||||
| **1.27.3** | 2026-08-29 | None | None |
|
||||
| **1.27.2** | 2026-08-14 | None | None |
|
||||
| **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.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.26.4** | 2026-06-21 | None | None |
|
||||
| **1.26.3** | 2026-06-20 | None | None |
|
||||
| **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
|
||||
|
||||
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 |
|
||||
| `AppUrl` | ≤ 1.21 | Application base URL |
|
||||
| `AppDomain` | ≤ 1.21 | Server domain |
|
||||
@@ -49,7 +103,8 @@ All 11 template types use only Gitea built-in variables and functions. Verified
|
||||
| `QueryEscape` | ≤ 1.21 | URL query encoding |
|
||||
| `PathEscapeSegments` | ≤ 1.21 | Per-segment path encoding |
|
||||
| `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 |
|
||||
| `Iif` | ≤ 1.21 | Inline conditional |
|
||||
| `dict` | ≤ 1.21 | Build maps from key-value pairs |
|
||||
@@ -58,7 +113,6 @@ All 11 template types use only Gitea built-in variables and functions. Verified
|
||||
| `SliceUtils` | ≤ 1.21 | Slice manipulation helpers |
|
||||
| `JsonUtils` | ≤ 1.21 | JSON helpers |
|
||||
| `DumpVar` | ≤ 1.21 | Debug variable dump |
|
||||
| `SanitizeHTML` | ≤ 1.21 | **Deprecated in 1.26** — use `HTMLFormat` |
|
||||
|
||||
### Data Contexts by Template
|
||||
|
||||
@@ -70,30 +124,42 @@ All 11 template types use only Gitea built-in variables and functions. Verified
|
||||
| `user/auth/reset_passwd` | `DisplayName`, `Code`, `ResetPwdCodeLives` |
|
||||
| `org/team_invite` | `Inviter`, `Team`, `Organization`, `InviteURL`, `Invite` |
|
||||
| `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/actions/workflow_run` | `Run` (with `WorkflowID`, `HTMLURL`), `Jobs`, `RunStatusText` |
|
||||
| `repo/issue/assigned` | `Doer`, `Issue`, `Link`, `IsPull` |
|
||||
| `repo/actions/workflow_run` | `Subject`, `Run` (with `WorkflowID`, `HTMLURL`), `Jobs` (with status class, icon CID/alt, attempt, URL and duration) |
|
||||
| `repo/issue/assigned` | `Subject`, `Doer`, `Issue`, `Link`, `IsPull`, `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
|
||||
|
||||
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
|
||||
|
||||
1. **Automated lint** — CI renders all templates via `go run . preview all` on every push
|
||||
2. **Source audit** — Template data contexts are cross-referenced against Gitea's `services/mailer/` package
|
||||
3. **Release checklist** — Each release confirms the max-tested Gitea version in this file
|
||||
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. **Framework alignment** — `go test ./...` checks deterministic shared adaptation/generation and fails on changed primary-action anchors; theme sources cannot own business templates.
|
||||
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`.
|
||||
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
|
||||
|
||||
If you find a compatibility problem with a specific Gitea version:
|
||||
1. Check the [Gitea changelog](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md) for recent mail template changes
|
||||
2. Open an issue with: your Gitea version, which template, and the error
|
||||
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,104 +1,219 @@
|
||||
# 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
|
||||
|
||||
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)
|
||||
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:
|
||||
|
||||
```bash
|
||||
cd tools && go run . dev
|
||||
# → http://localhost:3456
|
||||
cd tools
|
||||
go mod download
|
||||
go run . upstream prepare
|
||||
go test ./...
|
||||
go run . preview all
|
||||
```
|
||||
|
||||
- Watches `themes/**/*.tmpl` — auto-rebuilds on save
|
||||
- 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`
|
||||
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.
|
||||
|
||||
### 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.
|
||||
The admin test email (**Site Administration > Configuration > Mailer > Send Test Email**)
|
||||
does not use custom mail templates — it follows a built-in code path.
|
||||
### Common Commands
|
||||
|
||||
The most reliable method is to trigger a real notification. For example, the password
|
||||
reset flow:
|
||||
Run these commands from `tools/`. Place flags before positional arguments.
|
||||
|
||||
1. Log out and click **"Forgot password"** on the login page
|
||||
2. Enter your account email and submit
|
||||
3. Check the password reset email — it will render with your custom mail templates
|
||||
| Command | Result |
|
||||
|---|---|
|
||||
| `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
|
||||
|
||||
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
|
||||
- `preview(*):` — preview tooling changes
|
||||
- `tools(*):` — Go build script changes
|
||||
- `docs(*):` — documentation and translations
|
||||
- `fix(*):` — bug fixes
|
||||
- `project(*):` — README, LICENSE, AGENTS.md, meta
|
||||
## Translations and License
|
||||
|
||||
---
|
||||
|
||||
## Translations
|
||||
|
||||
- English
|
||||
- [Simplified Chinese](docs/CONTRIBUTING.zh-CN.md)
|
||||
- [Traditional Chinese](docs/CONTRIBUTING.zh-TW.md)
|
||||
- [Russian](docs/CONTRIBUTING.ru.md)
|
||||
- [Japanese](docs/CONTRIBUTING.ja.md)
|
||||
- [Korean](docs/CONTRIBUTING.ko.md)
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under the MIT License.
|
||||
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).
|
||||
@@ -1,207 +1,193 @@
|
||||
# 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.
|
||||
|
||||
[](COMPATIBILITY.md) <!-- TRACKER:BADGE -->
|
||||
[简体中文](docs/README.zh-CN.md) · [Installation](#installation) · [Preview](#preview) · [Compatibility](COMPATIBILITY.md) · [Contributing](CONTRIBUTING.md)
|
||||
|
||||
> **110 template files — 10 visual styles, 11 email types each**
|
||||
<!-- TRACKER:BADGE -->
|
||||
[](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.
|
||||
|
||||
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.**
|
||||
|
||||
---
|
||||
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.
|
||||
|
||||
## Style Gallery
|
||||
|
||||
| Preview | Style | Audience | Character |
|
||||
|---|---|---|---|
|
||||
|  | **Horizon** | Enterprise / Corporate | Blue accent, slate typography, centered cards |
|
||||
|  | **Terminal** | Developers / Tech | Dark mode, monospace, green CLI accents |
|
||||
|  | **Ember** | Community / Open Source | Warm amber, rounded, humanist, inclusive |
|
||||
|  | **Bloom** | Creative / Startup | Frosted glass, soft blue light, iridescent accents |
|
||||
|  | **Heritage** | Education / Research | Navy and gold, serif, classic, authoritative |
|
||||
|  | **Neon** | Gaming / Web3 / Creative Tech | Cyberpunk neon glow, hot pink & cyan, synthwave energy |
|
||||
|  | **Mono** | Design Studios / Editorial | Swiss brutalist, black & white, red accent, zero radius |
|
||||
|  | **Terra** | Sustainability / Wellness | Warm earth tones, organic textures, humanist serif |
|
||||
|  | **Ink** | Publishing / News / Literature | Editorial print, navy & gold, newspaper layout, drop caps |
|
||||
|  | **Aurora** | Premium SaaS / Mindfulness | Ethereal light gradients, deep purple & teal, atmospheric glow |
|
||||
| Preview | Theme | Appearance |
|
||||
|---|---|---|
|
||||
|  | **Horizon** | Blue accents, gray text and a centered white card |
|
||||
|  | **Terminal** | Dark background, monospace text and green accents |
|
||||
|  | **Ember** | Warm orange palette, serif headings and rounded buttons |
|
||||
|  | **Bloom** | Light blue gradients, rounded cards and buttons |
|
||||
|  | **Heritage** | Navy and gold accents, double borders and serif text |
|
||||
|  | **Neon** | Dark background, pink and cyan accents and glow effects |
|
||||
|  | **Mono** | Black and white, red accents and square borders |
|
||||
|  | **Terra** | Earth tones, terracotta buttons and serif text |
|
||||
|  | **Ink** | Newspaper layout, sidebar, serif text and drop caps |
|
||||
|  | **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.
|
||||
|
||||
[**Live preview gallery**](preview/index.html) — open in a browser for an interactive style switcher with desktop/mobile viewports and view mode (Modern, Source).
|
||||
|
||||
---
|
||||
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.
|
||||
|
||||
## 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
|
||||
# Locate your Gitea custom directory
|
||||
# (set by GITEA_CUSTOM; defaults shown below)
|
||||
cd tools
|
||||
go run . build all
|
||||
cd ..
|
||||
```
|
||||
|
||||
# Copy templates (example: Horizon style)
|
||||
cp -r themes/horizon/mail/* /var/lib/gitea/custom/templates/mail/
|
||||
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).
|
||||
|
||||
# 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
|
||||
```
|
||||
|
||||
### Custom Directory Location
|
||||
|
||||
| Platform | Default Path |
|
||||
|---|---|
|
||||
| Linux (binary) | `/var/lib/gitea/custom` |
|
||||
| Linux (Docker) | `/data/gitea` |
|
||||
| Windows | `C:\gitea\custom` |
|
||||
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.
|
||||
|
||||
### 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
|
||||
cp -r themes/terminal/mail/* /var/lib/gitea/custom/templates/mail/
|
||||
systemctl restart gitea
|
||||
```
|
||||
Removing old overrides is especially important when switching from `framed` to `shared` mode: files left behind can keep the previous theme's layout.
|
||||
|
||||
### Confirming It Works
|
||||
|
||||
The admin test email does not use custom mail templates. To verify your 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.
|
||||
|
||||
---
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
||||
Generate the preview data once (Go only), then open in a browser:
|
||||
From a source checkout, generate the preview data:
|
||||
|
||||
```bash
|
||||
cd tools && go run . preview all
|
||||
open preview/index.html # no server needed
|
||||
cd tools
|
||||
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)
|
||||
|
||||
Start a pure Go development server that watches for `.tmpl` changes, auto-rebuilds, and pushes live updates to the browser via SSE:
|
||||
|
||||
```bash
|
||||
cd tools && go run . dev
|
||||
open http://localhost:3456
|
||||
cd tools
|
||||
go run . dev
|
||||
```
|
||||
|
||||
| Capability | Static | Dev |
|
||||
|-----------|--------|-----|
|
||||
| Go template rendering | ✅ | ✅ |
|
||||
| Theme/template switching | ✅ | ✅ |
|
||||
| Live reload on save | — | ✅ |
|
||||
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).
|
||||
|
||||
### 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
|
||||
- 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
|
||||
The preview renders templates using example data. Check your target mail clients separately; browser rendering does not reproduce their CSS support.
|
||||
|
||||
---
|
||||
## 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
|
||||
|
||||
```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
|
||||
│ ├── ... # 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/ # Multi-language documentation
|
||||
├── AGENTS.md # AI agent guidance
|
||||
├── CONTRIBUTING.md
|
||||
├── LICENSE
|
||||
├── README.md
|
||||
└── .gitignore
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
### 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_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/org/team_invite.tmpl` | Team invitation |
|
||||
| `mail/repo/collaborator.tmpl` | Repository collaborator added |
|
||||
| `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/issue/assigned.tmpl` | Issue / Pull Request assigned |
|
||||
| `mail/repo/issue/default.tmpl` | Issue / Pull Request updates |
|
||||
|
||||
---
|
||||
|
||||
## Compatibility
|
||||
|
||||
- **Gitea 1.25+** — matches the refactored mail template directory structure (v1.25)
|
||||
- **Latest tested:** Gitea 1.26.4 <!-- TRACKER:LATEST-TESTED -->
|
||||
- 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)
|
||||
- [Simplified Chinese](docs/README.zh-CN.md)
|
||||
- [Traditional Chinese](docs/README.zh-TW.md)
|
||||
- [Russian](docs/README.ru.md)
|
||||
- [Japanese](docs/README.ja.md)
|
||||
- [Korean](docs/README.ko.md)
|
||||
|
||||
---
|
||||
| `mail/repo/issue/assigned.tmpl` | Issue or pull request assigned |
|
||||
| `mail/repo/issue/default.tmpl` | Issue or pull request activity |
|
||||
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. Multi-language versions 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
|
||||
|
||||
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).
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<sub>Not affiliated with the Gitea project. Gitea is a community-managed lightweight code hosting solution written in Go.</sub>
|
||||
</p>
|
||||
This project is not affiliated with Gitea.
|
||||
@@ -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.
|
||||
@@ -1,76 +0,0 @@
|
||||
# コントリビューション — Gitea メールテンプレート
|
||||
|
||||
## 参加方法
|
||||
|
||||
### 新しいスタイルの追加
|
||||
|
||||
1. ツールでスキャフォールド: `cd tools && go run . create <style-name>` — 全11種類のメールタイプ用のプレースホルダ `.tmpl` ファイルを含む完全なディレクトリ構造を作成します
|
||||
2. `themes/<style-name>/` 内の各 `.tmpl` ファイルを独自デザインで編集
|
||||
3. プレビューを再生成: `cd tools && go run . preview all`(ビルドスクリプトが `themes/` 以下の全テーマを自動検出し、テーマセレクターも自動生成されます)
|
||||
4. スクリーンショット付きで PR を提出(1枚あたり 50 KiB 以下、10–20 KiB 推奨)
|
||||
|
||||
### スタイルガイドライン
|
||||
|
||||
- 各スタイルに **全11種類のテンプレート** を含める
|
||||
- Gitea 組み込み関数のみ使用
|
||||
- 翻訳キーは Gitea 公式ロケール (`mail.*`) から
|
||||
- **`.DisplayName` を使用しない** (collaborator, transfer, release, workflow_run, assigned, default)
|
||||
- 600px 幅のメールクライアント向けにデザイン
|
||||
- Gmail, Outlook, Apple Mail でテスト
|
||||
|
||||
### バグレポート
|
||||
|
||||
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
|
||||
cd tools && go run . dev
|
||||
# → http://localhost:3456
|
||||
```
|
||||
|
||||
`.tmpl` ファイルを編集すると自動的に再構築されブラウザに反映されます。
|
||||
|
||||
|
||||
### 結合テスト
|
||||
|
||||
Gitea にデプロイ後、実際のトランザクションメールで検証してください。管理画面のテストメール
|
||||
(**Site Administration > Configuration > Mailer > Send Test Email**)はカスタム
|
||||
メールテンプレートを使用しません——組み込みのコードパスを通ります。
|
||||
|
||||
最も確実な方法は実際の通知をトリガーすることです。例えば、パスワードリセットフロー:
|
||||
|
||||
1. ログアウトし、ログインページで **"Forgot password"** をクリック
|
||||
2. アカウントのメールアドレスを入力して送信
|
||||
3. パスワードリセットメールを確認——カスタムメールテンプレートでレンダリングされます
|
||||
|
||||
---
|
||||
|
||||
## コミット規約
|
||||
|
||||
- `style(<name>):` — スタイルのテンプレート変更
|
||||
- `preview:` — プレビューツールの変更
|
||||
- `tools:` — Go ビルドスクリプトの変更
|
||||
- `docs:` — ドキュメントと翻訳
|
||||
- `fix:` — バグ修正
|
||||
- `project:` — README, LICENSE, メタファイル
|
||||
|
||||
## ライセンス
|
||||
|
||||
貢献により、MIT ライセンスに同意したものとみなします。
|
||||
@@ -1,49 +0,0 @@
|
||||
# 기여 가이드 — Gitea 메일 템플릿
|
||||
|
||||
## 기여 방법
|
||||
|
||||
### 새 스타일 추가하기
|
||||
|
||||
1. 도구로 스캐폴드: `cd tools && go run . create <style-name>` — 11개 이메일 유형 모두에 대한 플레이스홀더 `.tmpl` 파일이 포함된 완전한 디렉토리 구조를 생성합니다
|
||||
2. `themes/<style-name>/`에서 각 `.tmpl` 파일을 고유한 디자인으로 편집
|
||||
3. 프리뷰 재생성: `cd tools && go run . preview all` (빌드 스크립트가 `themes/` 아래의 모든 테마 디렉토리를 자동으로 찾고 테마 선택기도 자동 생성합니다)
|
||||
4. 스크린샷과 함께 PR 제출 (이미지당 50 KiB 이하, 10–20 KiB 권장)
|
||||
|
||||
### 스타일 가이드라인
|
||||
|
||||
- 각 스타일에 **11개 템플릿 유형 모두** 포함
|
||||
- Gitea 내장 함수만 사용
|
||||
- 번역 키는 Gitea 공식 로케일 (`mail.*`) 사용
|
||||
- **`.DisplayName` 사용 금지** (collaborator, transfer, release, workflow_run, assigned, default)
|
||||
- 600px 너비의 이메일 클라이언트에 맞게 디자인
|
||||
- Gmail, Outlook, Apple Mail에서 테스트
|
||||
|
||||
### 버그 리포트
|
||||
|
||||
1. Go 변수가 올바른지 확인
|
||||
2. 번역 키가 Gitea 로케일과 일치하는지 확인
|
||||
3. `.DisplayName`이 오용되지 않았는지 확인
|
||||
4. 프리뷰 재생성: `cd tools && go run . preview all`
|
||||
5. 스타일 이름과 메일 유형을 명시하여 이슈 생성
|
||||
|
||||
---
|
||||
|
||||
## 개발 설정
|
||||
|
||||
- **Go 1.21+** 템플릿 렌더링 및 CLI 도구
|
||||
|
||||
### 로컬 프리뷰 (정적)
|
||||
|
||||
1. 데이터 생성: `cd tools && go run . preview all`
|
||||
2. `preview/index.html`을 브라우저에서 열기
|
||||
|
||||
|
||||
### 개발 서버 (실시간 리로드)
|
||||
|
||||
```bash
|
||||
cd tools && go run . dev
|
||||
# → http://localhost:3456
|
||||
```
|
||||
|
||||
`.tmpl` 파일 수정 시 자동 재빌드되어 브라우저에 반영됩니다.
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
# Участие в разработке — Gitea Mail Templates
|
||||
|
||||
## Способы участия
|
||||
|
||||
### Добавление нового стиля
|
||||
|
||||
1. Создайте каркас через инструмент: `cd tools && go run . create <имя-стиля>` — создаёт полную структуру директорий с файлами-заготовками `.tmpl` для всех 11 типов писем
|
||||
2. Отредактируйте каждый `.tmpl` файл в `themes/<имя-стиля>/` с вашим уникальным дизайном
|
||||
3. Перегенерируйте превью: `cd tools && go run . preview all` (скрипт автоматически находит все темы в `themes/` и динамически генерирует селектор тем)
|
||||
4. Отправьте PR со скриншотами (≤ 50 КиБ каждый, рекомендуется 10–20 КиБ)
|
||||
|
||||
### Рекомендации по стилю
|
||||
|
||||
- Каждый стиль должен включать **все 11 типов шаблонов**
|
||||
- Используйте только встроенные функции шаблонов Gitea
|
||||
- Ключи перевода из официальных файлов локали Gitea (`mail.*`)
|
||||
- **Не используйте `.DisplayName`** где его нет в контексте (collaborator, transfer, release, workflow_run, assigned, default)
|
||||
- Дизайн для 600px email клиентов
|
||||
- Тестируйте в Gmail, Outlook, Apple Mail
|
||||
|
||||
### Баг-репорты
|
||||
|
||||
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` в браузере — сервер не нужен
|
||||
|
||||
|
||||
### Сервер разработки (live reload)
|
||||
|
||||
```bash
|
||||
cd tools && go run . dev
|
||||
# → http://localhost:3456
|
||||
```
|
||||
|
||||
При изменении `.tmpl` файлов автоматически пересобирает и обновляет браузер.
|
||||
|
||||
|
||||
### Интеграционное тестирование
|
||||
|
||||
Разверните шаблоны на Gitea и проверяйте с реальными транзакционными письмами.
|
||||
Тестовое письмо администратора (**Site Administration > Configuration > Mailer >
|
||||
Send Test Email**) не использует пользовательские шаблоны — оно идет по
|
||||
встроенному пути.
|
||||
|
||||
Самый надежный способ — вызвать реальное уведомление. Например, сброс пароля:
|
||||
|
||||
1. Выйдите из системы и нажмите **"Forgot password"** на странице входа
|
||||
2. Введите email вашей учетной записи и отправьте
|
||||
3. Проверьте письмо для сброса пароля — оно будет отрендерено с вашими шаблонами
|
||||
|
||||
---
|
||||
|
||||
## Соглашение о коммитах
|
||||
|
||||
- `style(<имя>):` — изменения шаблонов стиля
|
||||
- `preview:` — инструменты предпросмотра
|
||||
- `tools:` — Go-скрипт сборки
|
||||
- `docs:` — документация и переводы
|
||||
- `fix:` — исправления
|
||||
- `project:` — README, LICENSE, мета-файлы
|
||||
|
||||
## Лицензия
|
||||
|
||||
Участвуя, вы соглашаетесь с лицензией MIT.
|
||||
@@ -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)
|
||||
## 本地开发
|
||||
|
||||
### 风格指南
|
||||
|
||||
- 每个风格必须包含全部 **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` — 无需服务器
|
||||
|
||||
|
||||
### 开发服务器(实时重载)
|
||||
使用 **Go 1.24 或更高版本**。从仓库根目录开始,准备依赖和锁定的官方文件,再运行检查并生成预览:
|
||||
|
||||
```bash
|
||||
cd tools && go run . dev
|
||||
# → http://localhost:3456
|
||||
cd tools
|
||||
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 实例后,请通过真实的邮件通知进行验证。管理后台的测试邮件
|
||||
(**Site Administration > Configuration > Mailer > Send Test Email**)不会使用
|
||||
自定义邮件模板——它走的是内置代码路径。
|
||||
以下命令均在 `tools/` 中执行。带有位置参数时,将选项放在位置参数之前。
|
||||
|
||||
最可靠的验证方式是触发一次真实的邮件通知,推荐使用密码重置流程:
|
||||
| 命令 | 用途 |
|
||||
|---|---|
|
||||
| `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. 退出登录,点击登录页的**"忘记密码"**
|
||||
2. 输入账户邮箱并提交
|
||||
3. 查看密码重置邮件——它将使用你的自定义邮件模板渲染
|
||||
`build` 将模板写入 `build/themes/<名称>/mail/`,并在 `build.json` 中记录文件和源码哈希。`preview` 同时完成构建,随后写入 `preview/rendered.js` 和 `preview/rendered/<语言>.js`。这些生成文件和下载的官方文件均被 Git 忽略,无需随贡献提交。
|
||||
|
||||
---
|
||||
## 添加主题
|
||||
|
||||
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 构建脚本变更
|
||||
- `docs:` — 文档和翻译
|
||||
- `fix:` — Bug 修复
|
||||
- `project:` — README、LICENSE、元文件
|
||||
| 前缀 | 范围 |
|
||||
|---|---|
|
||||
| `style(<名称>):` | 主题展示样式 |
|
||||
| `preview:` | 浏览器预览 |
|
||||
| `tools:` | CLI、构建与快照工具 |
|
||||
| `docs:` | 文档和翻译 |
|
||||
| `fix:` | 问题修复 |
|
||||
| `project:` | 仓库配置和项目结构 |
|
||||
| `refactor:` | 代码重构 |
|
||||
| `chore:` | 日常维护 |
|
||||
|
||||
## 许可协议
|
||||
## 翻译与许可证
|
||||
|
||||
参与贡献即表示您同意将您的贡献以 MIT 许可证授权。
|
||||
[英文指南](../CONTRIBUTING.md)介绍相同的开发流程。贡献采用 MIT 许可证;分发派生模板时,请保留 Gitea 版权和官方许可证,详见[第三方声明](../THIRD_PARTY_NOTICES.md)。
|
||||
@@ -1,80 +0,0 @@
|
||||
# 貢獻指南 — Gitea 郵件模板
|
||||
|
||||
## 參與方式
|
||||
|
||||
### 新增風格
|
||||
|
||||
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)
|
||||
|
||||
### 風格指南
|
||||
|
||||
- 每個風格必須包含全部 **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
|
||||
cd tools && go run . dev
|
||||
# → http://localhost:3456
|
||||
```
|
||||
|
||||
修改 `.tmpl` 檔案後自動重建並推送至瀏覽器。
|
||||
|
||||
|
||||
### 整合測試
|
||||
|
||||
將模板部署至 Gitea 實例後,請透過真實的郵件通知進行驗證。管理後台的測試郵件
|
||||
(**Site Administration > Configuration > Mailer > Send Test Email**)不會使用
|
||||
自訂郵件模板——它走的是內置程式碼路徑。
|
||||
|
||||
最可靠的驗證方式是觸發一次真實的郵件通知,推薦使用密碼重設流程:
|
||||
|
||||
1. 登出帳號,點擊登入頁的**"忘記密碼"**
|
||||
2. 輸入帳戶郵箱並提交
|
||||
3. 查看密碼重設郵件——它將使用你的自訂郵件模板渲染
|
||||
|
||||
---
|
||||
|
||||
## 提交規範
|
||||
|
||||
- `style(<名稱>):` — 特定風格的模板變更
|
||||
- `preview:` — 預覽工具變更
|
||||
- `tools:` — Go 建置腳本變更
|
||||
- `docs:` — 文件與翻譯
|
||||
- `fix:` — Bug 修復
|
||||
- `project:` — README、LICENSE、中繼資料檔案
|
||||
|
||||
## 授權條款
|
||||
|
||||
參與貢獻即表示您同意將您的貢獻以 MIT 授權條款授權。
|
||||
@@ -1,66 +0,0 @@
|
||||
# Gitea メールテンプレート
|
||||
|
||||
[Gitea](https://about.gitea.com) のためのメールテンプレートコレクション。
|
||||
|
||||
> **110 ファイル — 10 スタイル、各11種類**
|
||||
|
||||
---
|
||||
|
||||
## スタイルギャラリー
|
||||
|
||||
| プレビュー | スタイル | 対象 | 特徴 |
|
||||
|---|---|---|---|
|
||||
|  | **Horizon** | エンタープライズ | Blue accent, slate, centred cards |
|
||||
|  | **Terminal** | 開発者 | Dark mode, monospace, green CLI |
|
||||
|  | **Ember** | コミュニティ | Warm amber, rounded, inclusive |
|
||||
|  | **Bloom** | クリエイティブ | Frosted glass, soft blue light |
|
||||
|  | **Heritage** | 教育/研究 | Navy and gold, serif, classic |
|
||||
|  | **Neon** | ゲーム/Web3 | Cyberpunk neon, pink and cyan |
|
||||
|  | **Mono** | デザイン/編集 | Swiss brutalist, black-white-red |
|
||||
|  | **Terra** | サステナビリティ | Warm earth tones, organic textures |
|
||||
|  | **Ink** | 出版/ニュース | Editorial print, navy and gold |
|
||||
|  | **Aurora** | プレミアムSaaS | Ethereal gradients, purple and teal |
|
||||
|
||||
> 画像は[ライブプレビュー](../preview/index.html)からの600px幅のスクリーンショットです。キャプチャ手順は [images/README.md](images/README.md) を参照してください。
|
||||
|
||||
[**ライブプレビュー**](../preview/index.html)
|
||||
|
||||
---
|
||||
|
||||
## インストール
|
||||
|
||||
```bash
|
||||
cp -r themes/horizon/mail/* /var/lib/gitea/custom/templates/mail/
|
||||
systemctl restart gitea
|
||||
```
|
||||
|
||||
### 動作確認
|
||||
|
||||
管理画面のテストメールはカスタムテンプレートを使用しません。テンプレートが
|
||||
有効であることを確認するには、実際のメール通知をトリガーしてください。最も
|
||||
簡単な方法はパスワードリセットです:ログアウトし、ログインページで
|
||||
**"Forgot password"** をクリックし、リセットメールを確認してください。
|
||||
|
||||
## プレビュー
|
||||
|
||||
**静的モード:**
|
||||
```bash
|
||||
cd tools && go run . preview all
|
||||
```
|
||||
その後 `preview/index.html` を開く。
|
||||
|
||||
**開発サーバー(ライブリロード):**
|
||||
```bash
|
||||
cd tools && go run . dev
|
||||
# → http://localhost:3456
|
||||
```
|
||||
|
||||
## 互換性
|
||||
|
||||
- **Gitea 1.25+** — v1.25で導入されたメールテンプレートディレクトリ構造
|
||||
- **最新テスト:** Gitea 1.26.4<!-- TRACKER:LATEST-TESTED -->
|
||||
- Gitea公式テンプレートと100%互換 — 詳細は [COMPATIBILITY.md](COMPATIBILITY.md)を参照
|
||||
|
||||
## ライセンス
|
||||
|
||||
MIT — [LICENSE](../LICENSE).
|
||||
@@ -1,66 +0,0 @@
|
||||
# Gitea 메일 템플릿
|
||||
|
||||
[Gitea](https://about.gitea.com) 인스턴스를 위한 메일 템플릿 컬렉션.
|
||||
|
||||
> **110 파일 — 10 스타일, 각 11종류**
|
||||
|
||||
---
|
||||
|
||||
## 스타일 갤러리
|
||||
|
||||
| 미리보기 | 스타일 | 대상 | 특징 |
|
||||
|---|---|---|---|
|
||||
|  | **Horizon** | 엔터프라이즈 | Blue accent, slate, centred cards |
|
||||
|  | **Terminal** | 개발자 | Dark mode, monospace, green CLI |
|
||||
|  | **Ember** | 커뮤니티 | Warm amber, rounded, inclusive |
|
||||
|  | **Bloom** | 크리에이티브 | Frosted glass, soft blue light |
|
||||
|  | **Heritage** | 교육/연구 | Navy and gold, serif, classic |
|
||||
|  | **Neon** | 게임/Web3 | Cyberpunk neon, pink and cyan |
|
||||
|  | **Mono** | 디자인/편집 | Swiss brutalist, black-white-red |
|
||||
|  | **Terra** | 지속가능성 | Warm earth tones, organic textures |
|
||||
|  | **Ink** | 출판/뉴스 | Editorial print, navy and gold |
|
||||
|  | **Aurora** | 프리미엄SaaS | Ethereal gradients, purple and teal |
|
||||
|
||||
> 이미지는 [라이브 프리뷰](../preview/index.html)에서 캡처한 600px 너비 스크린샷입니다. 캡처 방법은 [images/README.md](images/README.md)를 참조하세요.
|
||||
|
||||
[**라이브 프리뷰**](../preview/index.html)
|
||||
|
||||
---
|
||||
|
||||
## 설치
|
||||
|
||||
```bash
|
||||
cp -r themes/horizon/mail/* /var/lib/gitea/custom/templates/mail/
|
||||
systemctl restart gitea
|
||||
```
|
||||
|
||||
### 작동 확인
|
||||
|
||||
관리자 테스트 이메일은 커스텀 템플릿을 사용하지 않습니다. 템플릿이 활성화
|
||||
되었는지 확인하려면 실제 이메일 알림을 트리거하세요. 가장 빠른 방법은 비밀번호
|
||||
재설정입니다: 로그아웃 후 로그인 페이지에서 **"Forgot password"** 를 클릭하고
|
||||
재설정 이메일을 확인하세요.
|
||||
|
||||
## 프리뷰
|
||||
|
||||
**정적 모드:**
|
||||
```bash
|
||||
cd tools && go run . preview all
|
||||
```
|
||||
그런 다음 `preview/index.html` 열기.
|
||||
|
||||
**개발 서버 (실시간 리로드):**
|
||||
```bash
|
||||
cd tools && go run . dev
|
||||
# → http://localhost:3456
|
||||
```
|
||||
|
||||
## 호환성
|
||||
|
||||
- **Gitea 1.25+** — v1.25에서 도입된 메일 템플릿 디렉토리 구조 사용
|
||||
- **최신 테스트:** Gitea 1.26.4<!-- TRACKER:LATEST-TESTED -->
|
||||
- Gitea 공식 템플릿과 100% 호환 — 자세한 내용은 [COMPATIBILITY.md](../COMPATIBILITY.md) 참조
|
||||
|
||||
## 라이선스
|
||||
|
||||
MIT — [LICENSE](../LICENSE).
|
||||
@@ -1,66 +0,0 @@
|
||||
# Gitea Mail Templates — Шаблоны писем Gitea
|
||||
|
||||
Коллекция шаблонов писем для [Gitea](https://about.gitea.com).
|
||||
|
||||
> **110 файлов — 10 стилей, по 11 типов писем**
|
||||
|
||||
---
|
||||
|
||||
## Галерея стилей
|
||||
|
||||
| Превью | Стиль | Аудитория | Характер |
|
||||
|---|---|---|---|
|
||||
|  | **Horizon** | Корпоративный | Синий акцент, slate, centred cards |
|
||||
|  | **Terminal** | Разработчики | Тёмная тема, monospace, зелёный CLI |
|
||||
|  | **Ember** | Open Source | Тёплый янтарь, rounded, инклюзивный |
|
||||
|  | **Bloom** | Креатив / Стартапы | Матовое стекло, мягкий синий свет |
|
||||
|  | **Heritage** | Образование | Navy и золото, serif, классика |
|
||||
|  | **Neon** | Игры / Web3 / Креатив | Киберпанк-неон, розовый и циан |
|
||||
|  | **Mono** | Дизайн / Редакция | Швейцарский брутализм, чёрный-белый-красный |
|
||||
|  | **Terra** | Устойчивость / Здоровье | Тёплые земляные тона, органические текстуры |
|
||||
|  | **Ink** | Издательство / Новости | Редакционная печать, navy и золото |
|
||||
|  | **Aurora** | Премиум SaaS | Эфирные градиенты, пурпур и бирюза |
|
||||
|
||||
> Изображения шириной 600px из [предпросмотра](../preview/index.html). Инструкции по созданию скриншотов: [images/README.md](images/README.md).
|
||||
|
||||
[**Галерея предпросмотра**](../preview/index.html)
|
||||
|
||||
---
|
||||
|
||||
## Установка
|
||||
|
||||
```bash
|
||||
cp -r themes/horizon/mail/* /var/lib/gitea/custom/templates/mail/
|
||||
systemctl restart gitea
|
||||
```
|
||||
|
||||
### Проверка работы
|
||||
|
||||
Тестовое письмо администратора не использует пользовательские шаблоны. Чтобы
|
||||
убедиться, что шаблоны активны, вызовите реальное уведомление. Самый быстрый
|
||||
способ — сброс пароля: выйдите из системы, нажмите **"Forgot password"** на
|
||||
странице входа и проверьте письмо для сброса.
|
||||
|
||||
## Предпросмотр
|
||||
|
||||
**Статический режим:**
|
||||
```bash
|
||||
cd tools && go run . preview all
|
||||
```
|
||||
Затем откройте `preview/index.html`.
|
||||
|
||||
**Dev-сервер (live reload):**
|
||||
```bash
|
||||
cd tools && go run . dev
|
||||
# → http://localhost:3456
|
||||
```
|
||||
|
||||
## Совместимость
|
||||
|
||||
- **Gitea 1.25+** — структура директорий из v1.25
|
||||
- **Последняя проверка:** Gitea 1.26.4<!-- TRACKER:LATEST-TESTED -->
|
||||
- 100% совместимость с официальными шаблонами Gitea — см. [COMPATIBILITY.md](COMPATIBILITY.md)
|
||||
|
||||
## Лицензия
|
||||
|
||||
MIT — [LICENSE](../LICENSE).
|
||||
@@ -1,87 +1,189 @@
|
||||
# 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 实例使用默认的纯文本邮件模板。本项目提供了**开箱即用、视觉精美的替代方案**——每种方案都针对特定社区或受众设计,您可以选择最适合您用户的风格。
|
||||
|
||||
每个模板都是直接替换。所有 Go 模板变量、翻译键和 Gitea 数据上下文完全兼容。**无需补丁、无需插件、无需 fork。**
|
||||
|
||||
---
|
||||
`main` 分支的源码架构**尚未发布**,以 Gitea v28.0.0 为基线,通过锁定的官方文件和共享布局框架构建模板。已发布的压缩包保留原有内容和兼容范围。安装前,请根据 Gitea 版本查阅[兼容矩阵](../COMPATIBILITY.md#compatibility-matrix),选择对应的发行包。
|
||||
|
||||
## 风格画廊
|
||||
|
||||
| 预览 | 风格 | 受众 | 特点 |
|
||||
|---|---|---|---|
|
||||
|  | **Horizon** | 企业/公司 | 蓝色强调色、石板灰排版、居中卡片 |
|
||||
|  | **Terminal** | 开发者/技术 | 暗色模式、等宽字体、绿色命令行风格 |
|
||||
|  | **Ember** | 社区/开源 | 暖琥珀色、圆角、人文主义、包容 |
|
||||
|  | **Bloom** | 创意/初创 | 磨砂玻璃、柔和蓝色光线、虹彩点缀 |
|
||||
|  | **Heritage** | 教育/研究 | 海军蓝与金色、衬线字体、经典、权威 |
|
||||
|  | **Neon** | 游戏/Web3/创意科技 | 赛博朋克霓虹、粉红与青色、合成波能量 |
|
||||
|  | **Mono** | 设计工作室/编辑 | 瑞士粗野主义、黑白红强调、零圆角 |
|
||||
|  | **Terra** | 可持续/健康 | 温暖大地色调、有机质感、人文衬线 |
|
||||
|  | **Ink** | 出版/新闻/文学 | 编辑印刷、深蓝与金色、报纸排版 |
|
||||
|  | **Aurora** | 高端SaaS/正念 | 空灵光效渐变、深紫与青绿、大气光晕 |
|
||||
| 预览 | 主题 | 样式特点 |
|
||||
|---|---|---|
|
||||
|  | **Horizon** | 蓝色强调色、灰色文字、居中白色卡片 |
|
||||
|  | **Terminal** | 深色背景、等宽字体、绿色强调色 |
|
||||
|  | **Ember** | 暖橙色调、衬线标题、圆角按钮 |
|
||||
|  | **Bloom** | 浅蓝色渐变、圆角卡片与按钮 |
|
||||
|  | **Heritage** | 藏蓝与金色、双线边框、衬线字体 |
|
||||
|  | **Neon** | 深色背景、粉红与青色、发光效果 |
|
||||
|  | **Mono** | 黑白配色、红色强调色、直角边框 |
|
||||
|  | **Terra** | 大地色调、陶土色按钮、衬线字体 |
|
||||
|  | **Ink** | 报刊式布局、侧栏、衬线字体与首字下沉 |
|
||||
|  | **Aurora** | 深紫色背景、青绿色强调色、柔和光晕 |
|
||||
|
||||
> 图片为 600px 宽截图,来自[在线预览](../preview/index.html)。截图方法参见 [images/README.md](images/README.md)。
|
||||
|
||||
[**在线预览画廊**](../preview/index.html)
|
||||
|
||||
---
|
||||
截图展示当前源码的构建结果。其他邮件类型和语言可通过[本地预览](#预览)查看;更新截图请参阅[截图指南](images/README.md)。
|
||||
|
||||
## 安装
|
||||
|
||||
选择一种风格,将 `mail/` 目录复制到 Gitea 自定义模板路径:
|
||||
### 选择安装来源
|
||||
|
||||
运行 `gitea --version` 确认实例版本,再按[兼容矩阵](../COMPATIBILITY.md#compatibility-matrix)选择模板版本。
|
||||
|
||||
| 来源 | 邮件模板目录 | 准备步骤 |
|
||||
|---|---|---|
|
||||
| 发行压缩包 | `themes/<名称>/mail/` | 下载并解压推荐版本的发行包 |
|
||||
| 源码仓库 | `build/themes/<名称>/mail/` | 使用 Go 1.24 或更高版本构建 |
|
||||
|
||||
从源码构建时,在仓库根目录执行:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
切换风格只需覆盖文件,无需更改配置。
|
||||
使用源码构建产物时,将复制来源改为 `build/themes/horizon/mail/.`。Docker 和 Windows 部署请使用相应的容器或服务管理方式重启。
|
||||
|
||||
### 切换主题
|
||||
|
||||
先备份已有邮件模板,再移除上一主题安装的文件,复制新主题的完整产物。当前源码构建会在 `build.json` 中记录生成的文件;历史发行包可参照压缩包内容确认文件范围。保留其他自定义模板。
|
||||
|
||||
从 `framed` 切换到 `shared` 模式时,尤其需要清理旧主题文件,否则残留的覆盖模板可能继续使用原有布局。
|
||||
|
||||
### 确认生效
|
||||
|
||||
管理后台的测试邮件不会使用自定义模板。要验证模板是否生效,请触发一次真实的
|
||||
邮件通知。最快的方式是密码重置:退出登录,点击登录页的**"忘记密码"**,查看
|
||||
重置邮件即可——它将使用你的自定义样式渲染。
|
||||
|
||||
---
|
||||
使用测试账户触发密码重置等邮件通知,检查邮件样式和链接。管理后台的测试邮件按钮不使用自定义邮件模板。
|
||||
|
||||
## 预览
|
||||
|
||||
**静态模式:**
|
||||
```bash
|
||||
cd tools && go run . preview all
|
||||
```
|
||||
然后打开 `preview/index.html`。
|
||||
预览支持切换主题、邮件类型和语言,查看渲染结果或 HTML 源码,切换桌面与移动端视口,以及查看示例数据面板。v28.0.0 快照包含 11 种邮件类型和 28 种语言。
|
||||
|
||||
### 静态预览
|
||||
|
||||
在源码仓库中生成预览数据:
|
||||
|
||||
**开发服务器(实时重载):**
|
||||
```bash
|
||||
cd tools && go run . dev
|
||||
# → http://localhost:3456
|
||||
cd tools
|
||||
go run . preview all
|
||||
cd ..
|
||||
```
|
||||
|
||||
| 功能 | 静态 | Dev |
|
||||
|-----------|--------|-----|
|
||||
| Go 模板渲染 | ✅ | ✅ |
|
||||
| 主题/模板切换 | ✅ | ✅ |
|
||||
| 实时重载 | — | ✅ |
|
||||
在浏览器中打开 [preview/index.html](../preview/index.html)。语言数据按需加载,支持 `file://`,无需启动服务器。当前打包脚本生成的发行包包含这些数据;历史发行包保留其原有预览内容。
|
||||
|
||||
---
|
||||
### 开发服务器
|
||||
|
||||
```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 引入的邮件模板目录结构
|
||||
- **最新测试:** Gitea 1.26.4<!-- TRACKER:LATEST-TESTED -->
|
||||
- 与 Gitea 官方模板 100% 兼容 — 详见 [COMPATIBILITY.md](COMPATIBILITY.md)
|
||||
<!-- TRACKER:LATEST-TESTED -->
|
||||
- **最新测试:** Gitea 28.0.0
|
||||
<!-- /TRACKER:LATEST-TESTED -->
|
||||
<!-- 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 官方无隶属关系。
|
||||
@@ -1,82 +0,0 @@
|
||||
# Gitea 郵件模板
|
||||
|
||||
為自託管 [Gitea](https://about.gitea.com) 實例精心設計、面向不同受眾的郵件模板集合。
|
||||
|
||||
> **110 個模板檔案 — 10 種視覺風格,每種 11 種郵件類型**
|
||||
|
||||
---
|
||||
|
||||
## 設計理念
|
||||
|
||||
大多數自託管 Gitea 實例使用預設的純文字郵件模板。本專案提供了**開箱即用、視覺精美的替代方案**——每種方案都針對特定社群或受眾設計,您可以選擇最適合您使用者的風格。
|
||||
|
||||
每個模板都是直接替換。所有 Go 模板變數、翻譯鍵和 Gitea 資料上下文完全相容。**無需修補程式、無需外掛、無需 fork。**
|
||||
|
||||
---
|
||||
|
||||
## 風格展示
|
||||
|
||||
| 預覽 | 風格 | 受眾 | 特點 |
|
||||
|---|---|---|---|
|
||||
|  | **Horizon** | 企業/公司 | 藍色強調色、石板灰排版、居中卡片 |
|
||||
|  | **Terminal** | 開發者/技術 | 暗色模式、等寬字型、綠色命令列風格 |
|
||||
|  | **Ember** | 社群/開源 | 暖琥珀色、圓角、人文主義、包容 |
|
||||
|  | **Bloom** | 創意/新創 | 磨砂玻璃、柔和藍色光線、虹彩點綴 |
|
||||
|  | **Heritage** | 教育/研究 | 海軍藍與金色、襯線字型、經典、權威 |
|
||||
|  | **Neon** | 遊戲/Web3/創意科技 | 賽博龐克霓虹、粉紅與青色、合成波能量 |
|
||||
|  | **Mono** | 設計工作室/編輯 | 瑞士粗野主義、黑白紅強調、零圓角 |
|
||||
|  | **Terra** | 永續/健康 | 溫暖大地色調、有機質感、人文襯線 |
|
||||
|  | **Ink** | 出版/新聞/文學 | 編輯印刷、深藍與金色、報紙排版 |
|
||||
|  | **Aurora** | 高端SaaS/正念 | 空靈光效漸變、深紫與青綠、大氣光暈 |
|
||||
|
||||
> 圖片為 600px 寬截圖,來自[線上預覽](../preview/index.html)。截圖方法參見 [images/README.md](images/README.md)。
|
||||
|
||||
[**線上預覽展示**](../preview/index.html)
|
||||
|
||||
---
|
||||
|
||||
## 安裝
|
||||
|
||||
選擇一種風格,將 `mail/` 目錄複製到 Gitea 自訂模板路徑:
|
||||
|
||||
```bash
|
||||
cp -r themes/horizon/mail/* /var/lib/gitea/custom/templates/mail/
|
||||
systemctl restart gitea
|
||||
```
|
||||
|
||||
切換風格只需覆蓋檔案,無需更改設定。
|
||||
|
||||
### 確認生效
|
||||
|
||||
管理後台的測試郵件不會使用自訂模板。要驗證模板是否生效,請觸發一次真實的
|
||||
郵件通知。最快的方式是密碼重設:登出帳號,點擊登入頁的**"忘記密碼"**,查看
|
||||
重設郵件即可——它將使用你的自訂樣式渲染。
|
||||
|
||||
---
|
||||
|
||||
## 預覽
|
||||
|
||||
**靜態模式:**
|
||||
```bash
|
||||
cd tools && go run . preview all
|
||||
```
|
||||
然後開啟 `preview/index.html`。
|
||||
|
||||
**開發伺服器(即時重載):**
|
||||
```bash
|
||||
cd tools && go run . dev
|
||||
# → http://localhost:3456
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 相容性
|
||||
|
||||
- **Gitea 1.25+** — v1.25 引入的郵件模板目錄結構
|
||||
- **最新測試:** Gitea 1.26.4<!-- TRACKER:LATEST-TESTED -->
|
||||
- 與 Gitea 官方模板 100% 相容 — 詳見 [COMPATIBILITY.md](COMPATIBILITY.md)
|
||||
|
||||
## 授權
|
||||
|
||||
MIT — 詳見 [LICENSE](../LICENSE)。
|
||||
@@ -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
|
||||
|
||||
```
|
||||
horizon.png terminal.png ember.png bloom.png heritage.png
|
||||
neon.png mono.png terra.png ink.png aurora.png
|
||||
Use the same settings for every theme so images remain comparable:
|
||||
|
||||
| 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
|
||||
- **Recommended:** 30–40 KiB
|
||||
- **Format:** PNG, optimised — run through `pngquant` or `optipng` before committing
|
||||
## Manual Capture
|
||||
|
||||
## 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)
|
||||
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.
|
||||
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#风格画廊).
|
||||
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 10 KiB |
|
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">
|
||||
@@ -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,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 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__">
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
</td></tr>
|
||||
</table>
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
</td><td width="36%" class="email-sidebar resp-hide">__SIDEBAR__</td></tr>
|
||||
<tr><td colspan="2" 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 colspan="2" class="email-header">__HEADER__</td></tr>
|
||||
<tr><td width="64%" 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__">
|
||||
@@ -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__">
|
||||
@@ -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__">
|
||||
@@ -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__">
|
||||
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
<p class="email-footer-text"><span class="framework-extra">© </span><a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a></p>
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="framework-extra">
|
||||
{{if eq .Variant "terminal"}}<span class="email-brand">> {{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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -239,6 +239,11 @@ html,body,#preview-area,#panel-body{
|
||||
<select id="sel-template" onchange="switchTemplate(this.value)"></select>
|
||||
</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">
|
||||
<label>View</label>
|
||||
<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)
|
||||
//
|
||||
const state = { theme:'', template:'', client:'modern', viewport:'desktop' };
|
||||
const state = { theme:'', template:'', client:'modern', viewport:'desktop', locale:'en-US' };
|
||||
let REGISTRY = {};
|
||||
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
|
||||
|
||||
//
|
||||
@@ -338,20 +347,31 @@ function transform(html, mode) {
|
||||
const e = html.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
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
|
||||
//
|
||||
function render() {
|
||||
const request=++renderRequest;
|
||||
clearTimeout(renderTimer);
|
||||
const frame = document.getElementById('preview-frame');
|
||||
const loading = document.getElementById('loading');
|
||||
if(!rendered) return;
|
||||
|
||||
if (location.protocol !== 'file:') {
|
||||
frame.setAttribute('sandbox', '');
|
||||
}
|
||||
|
||||
frame.style.display = 'none';
|
||||
loading.style.display = 'flex';
|
||||
@@ -363,14 +383,15 @@ function render() {
|
||||
|
||||
let done = false;
|
||||
function finish() {
|
||||
if (done) return; done = true;
|
||||
if (done || request!==renderRequest) return; done = true;
|
||||
clearTimeout(renderTimer);
|
||||
loading.style.display = 'none';
|
||||
frame.style.display = 'block';
|
||||
frame.onload = null;
|
||||
}
|
||||
frame.onload = finish;
|
||||
frame.srcdoc = content;
|
||||
setTimeout(finish, 5000);
|
||||
renderTimer=setTimeout(finish, 5000);
|
||||
updatePanel();
|
||||
}
|
||||
|
||||
@@ -391,10 +412,11 @@ function toggleSection(id) {
|
||||
// Panel content updates
|
||||
//
|
||||
function updatePanel() {
|
||||
const escape = v => String(v).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
const r = REGISTRY[state.template];
|
||||
document.getElementById('panel-name').textContent = r ? r.name : state.template;
|
||||
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
|
||||
const themes = rendered ? Object.keys(rendered).length : 0;
|
||||
@@ -407,13 +429,13 @@ function updatePanel() {
|
||||
}
|
||||
}
|
||||
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] || {};
|
||||
let rows = '';
|
||||
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');
|
||||
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>';
|
||||
}
|
||||
@@ -422,7 +444,7 @@ function updatePanel() {
|
||||
// Events
|
||||
//
|
||||
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 setViewport(v) {
|
||||
state.viewport=v;
|
||||
@@ -443,7 +465,7 @@ document.addEventListener('keydown',e=>{
|
||||
if(e.key==='p'&&!e.ctrlKey&&!e.metaKey&&!e.altKey){ togglePanel(); return; }
|
||||
|
||||
// ← → 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 cur=selects.indexOf(activeId);
|
||||
|
||||
@@ -460,38 +482,85 @@ document.addEventListener('keydown',e=>{
|
||||
});
|
||||
|
||||
//
|
||||
// Init — load everything from rendered.js (single source of truth)
|
||||
//
|
||||
(function(){
|
||||
if(window.__REGISTRY__) REGISTRY = window.__REGISTRY__;
|
||||
if(window.__PARAMS__) PARAMS = window.__PARAMS__;
|
||||
|
||||
if(window.__RENDERED__){
|
||||
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);
|
||||
// Manifest and language bundles
|
||||
function fillSelect(id, entries, selected) {
|
||||
const select = document.getElementById(id);
|
||||
select.replaceChildren();
|
||||
for (const [value,label] of entries) {
|
||||
const option = document.createElement('option');
|
||||
option.value=value; option.textContent=label; select.appendChild(option);
|
||||
}
|
||||
state.template = Object.keys(REGISTRY)[0] || '';
|
||||
} else {
|
||||
document.getElementById('loading').style.display='none';
|
||||
showMessage('rendered.js not found — run <code>cd tools && go run . preview all</code> to generate', 'error');
|
||||
select.value=selected;
|
||||
}
|
||||
|
||||
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();
|
||||
} 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 && go run . preview all</code> to generate','error');
|
||||
}
|
||||
})();
|
||||
|
||||
// Message helpers
|
||||
@@ -512,7 +581,7 @@ function setDevMode(on) {
|
||||
if (on) {
|
||||
clearMessages();
|
||||
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'
|
||||
).id = 'msg-dev-disclaimer';
|
||||
}
|
||||
@@ -520,6 +589,7 @@ function setDevMode(on) {
|
||||
|
||||
// Dev-server live-reload (SSE)
|
||||
(function(){
|
||||
if (location.protocol === 'file:') return;
|
||||
let es;
|
||||
try {
|
||||
es = new EventSource('/events');
|
||||
@@ -533,14 +603,15 @@ function setDevMode(on) {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'rendered.js?v=' + Date.now();
|
||||
script.onload = function() {
|
||||
if (window.__RENDERED__) rendered = window.__RENDERED__;
|
||||
if (window.__REGISTRY__) REGISTRY = window.__REGISTRY__;
|
||||
if (window.__PARAMS__) PARAMS = window.__PARAMS__;
|
||||
render();
|
||||
script.remove();
|
||||
window.__MAIL_PREVIEW_DATA__ = {};
|
||||
localeLoads.clear();
|
||||
applyManifest(window.__MAIL_PREVIEW__);
|
||||
switchLocale(state.locale);
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
} else if (msg.type === 'error') {
|
||||
showMessage(msg.message, 'error');
|
||||
showMessage('', 'error').textContent = msg.message;
|
||||
}
|
||||
} catch(_) {}
|
||||
};
|
||||
|
||||
@@ -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:rgba(255,255,255,.4);letter-spacing:4px;text-transform:uppercase }
|
||||
.email-header { padding:0 0 32px;text-align:center }
|
||||
|
||||
/* Card */
|
||||
.email-card { padding:48px 40px;background:rgba(25,20,50,.8);border:1px solid rgba(13,148,136,.2);box-shadow:0 0 60px rgba(13,148,136,.08),0 0 120px rgba(99,102,241,.04) }
|
||||
.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:rgba(210,252,245,.88);font-weight:300 }
|
||||
.email-subtext { margin:0;font-size:13px;line-height:1.8;color:rgba(180,195,215,.95);font-weight:300 }
|
||||
|
||||
/* Divider */
|
||||
.email-divider { height:1px;background:linear-gradient(90deg,transparent,rgba(13,148,136,.3),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:rgba(13,148,136,.18);border:1px solid rgba(13,148,136,.3);letter-spacing:1px }
|
||||
.email-btn-table { margin:0 0 32px }
|
||||
|
||||
/* Fallback link */
|
||||
.email-link-box { border:1px solid rgba(13,148,136,.18);background:rgba(13,148,136,.03) }
|
||||
.email-link-label { margin:0 0 8px;font-size:11px;color:rgba(180,252,238,.75);font-weight:300 }
|
||||
.email-link-url { margin:0;font-size:11px;font-family:monospace;color:#99f6e4;background:rgba(0,0,0,.2);padding:10px 12px;border:1px solid rgba(13,148,136,.15);word-break:break-all }
|
||||
|
||||
/* Footer */
|
||||
.email-footer { padding:32px 0 0;text-align:center }
|
||||
.email-footer-text { margin:0;font-size:11px;color:rgba(160,175,200,.55);font-weight:300 }
|
||||
.email-footer-link { color:rgba(94,234,212,.5);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:rgba(210,252,245,.88)!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:rgba(160,175,200,.5);">
|
||||
{{.locale.Tr "mail.team_invite.text_3" .Invite.Email}}
|
||||
</p>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td class="email-footer">
|
||||
|
||||
<p class="email-footer-text">
|
||||
©
|
||||
<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:rgba(255,255,255,.4);letter-spacing:4px;text-transform:uppercase }
|
||||
.email-header { padding:0 0 32px;text-align:center }
|
||||
|
||||
/* Card */
|
||||
.email-card { padding:48px 40px;background:rgba(25,20,50,.8);border:1px solid rgba(13,148,136,.2);box-shadow:0 0 60px rgba(13,148,136,.08),0 0 120px rgba(99,102,241,.04) }
|
||||
.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:rgba(210,252,245,.88);font-weight:300 }
|
||||
.email-subtext { margin:0;font-size:13px;line-height:1.8;color:rgba(180,195,215,.95);font-weight:300 }
|
||||
|
||||
/* Divider */
|
||||
.email-divider { height:1px;background:linear-gradient(90deg,transparent,rgba(13,148,136,.3),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:rgba(13,148,136,.18);border:1px solid rgba(13,148,136,.3);letter-spacing:1px }
|
||||
.email-btn-table { margin:0 0 32px }
|
||||
|
||||
/* Fallback link */
|
||||
.email-link-box { border:1px solid rgba(13,148,136,.18);background:rgba(13,148,136,.03) }
|
||||
.email-link-label { margin:0 0 8px;font-size:11px;color:rgba(180,252,238,.75);font-weight:300 }
|
||||
.email-link-url { margin:0;font-size:11px;font-family:monospace;color:#99f6e4;background:rgba(0,0,0,.2);padding:10px 12px;border:1px solid rgba(13,148,136,.15);word-break:break-all }
|
||||
|
||||
/* Footer */
|
||||
.email-footer { padding:32px 0 0;text-align:center }
|
||||
.email-footer-text { margin:0;font-size:11px;color:rgba(160,175,200,.55);font-weight:300 }
|
||||
.email-footer-link { color:rgba(94,234,212,.5);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:rgba(210,252,245,.88)!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">
|
||||
©
|
||||
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
|
||||
</p>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</center>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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:rgba(255,255,255,.4);letter-spacing:4px;text-transform:uppercase }
|
||||
.email-header { padding:0 0 32px;text-align:center }
|
||||
|
||||
/* Card */
|
||||
.email-card { padding:48px 40px;background:rgba(25,20,50,.8);border:1px solid rgba(13,148,136,.2);box-shadow:0 0 60px rgba(13,148,136,.08),0 0 120px rgba(99,102,241,.04) }
|
||||
.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:rgba(210,252,245,.88);font-weight:300 }
|
||||
.email-subtext { margin:0;font-size:13px;line-height:1.8;color:rgba(180,195,215,.95);font-weight:300 }
|
||||
|
||||
/* Divider */
|
||||
.email-divider { height:1px;background:linear-gradient(90deg,transparent,rgba(13,148,136,.3),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:rgba(13,148,136,.18);border:1px solid rgba(13,148,136,.3);letter-spacing:1px }
|
||||
.email-btn-table { margin:0 0 32px }
|
||||
|
||||
/* Fallback link */
|
||||
.email-link-box { border:1px solid rgba(13,148,136,.18);background:rgba(13,148,136,.03) }
|
||||
.email-link-label { margin:0 0 8px;font-size:11px;color:rgba(180,252,238,.75);font-weight:300 }
|
||||
.email-link-url { margin:0;font-size:11px;font-family:monospace;color:#99f6e4;background:rgba(0,0,0,.2);padding:10px 12px;border:1px solid rgba(13,148,136,.15);word-break:break-all }
|
||||
|
||||
/* Footer */
|
||||
.email-footer { padding:32px 0 0;text-align:center }
|
||||
.email-footer-text { margin:0;font-size:11px;color:rgba(160,175,200,.55);font-weight:300 }
|
||||
.email-footer-link { color:rgba(94,234,212,.5);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:rgba(210,252,245,.88)!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">
|
||||
©
|
||||
<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>
|
||||
{{.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:rgba(255,255,255,.4);letter-spacing:4px;text-transform:uppercase }
|
||||
.email-header { padding:0 0 32px;text-align:center }
|
||||
|
||||
/* Card */
|
||||
.email-card { padding:48px 40px;background:rgba(25,20,50,.8);border:1px solid rgba(13,148,136,.2);box-shadow:0 0 60px rgba(13,148,136,.08),0 0 120px rgba(99,102,241,.04) }
|
||||
.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:rgba(210,252,245,.88);font-weight:300 }
|
||||
.email-subtext { margin:0;font-size:13px;line-height:1.8;color:rgba(180,195,215,.95);font-weight:300 }
|
||||
|
||||
/* Divider */
|
||||
.email-divider { height:1px;background:linear-gradient(90deg,transparent,rgba(13,148,136,.3),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:rgba(13,148,136,.18);border:1px solid rgba(13,148,136,.3);letter-spacing:1px }
|
||||
.email-btn-table { margin:0 0 32px }
|
||||
|
||||
/* Fallback link */
|
||||
.email-link-box { border:1px solid rgba(13,148,136,.18);background:rgba(13,148,136,.03) }
|
||||
.email-link-label { margin:0 0 8px;font-size:11px;color:rgba(180,252,238,.75);font-weight:300 }
|
||||
.email-link-url { margin:0;font-size:11px;font-family:monospace;color:#99f6e4;background:rgba(0,0,0,.2);padding:10px 12px;border:1px solid rgba(13,148,136,.15);word-break:break-all }
|
||||
|
||||
/* Footer */
|
||||
.email-footer { padding:32px 0 0;text-align:center }
|
||||
.email-footer-text { margin:0;font-size:11px;color:rgba(160,175,200,.55);font-weight:300 }
|
||||
.email-footer-link { color:rgba(94,234,212,.5);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:rgba(210,252,245,.88)!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">
|
||||
©
|
||||
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
|
||||
</p>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</center>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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:rgba(255,255,255,.4);letter-spacing:4px;text-transform:uppercase }
|
||||
.email-header { padding:0 0 32px;text-align:center }
|
||||
|
||||
/* Card */
|
||||
.email-card { padding:48px 40px;background:rgba(25,20,50,.8);border:1px solid rgba(13,148,136,.2);box-shadow:0 0 60px rgba(13,148,136,.08),0 0 120px rgba(99,102,241,.04) }
|
||||
.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:rgba(210,252,245,.88);font-weight:300 }
|
||||
.email-subtext { margin:0;font-size:13px;line-height:1.8;color:rgba(180,195,215,.95);font-weight:300 }
|
||||
|
||||
/* Divider */
|
||||
.email-divider { height:1px;background:linear-gradient(90deg,transparent,rgba(13,148,136,.3),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:rgba(13,148,136,.18);border:1px solid rgba(13,148,136,.3);letter-spacing:1px }
|
||||
.email-btn-table { margin:0 0 32px }
|
||||
|
||||
/* Fallback link */
|
||||
.email-link-box { border:1px solid rgba(13,148,136,.18);background:rgba(13,148,136,.03) }
|
||||
.email-link-label { margin:0 0 8px;font-size:11px;color:rgba(180,252,238,.75);font-weight:300 }
|
||||
.email-link-url { margin:0;font-size:11px;font-family:monospace;color:#99f6e4;background:rgba(0,0,0,.2);padding:10px 12px;border:1px solid rgba(13,148,136,.15);word-break:break-all }
|
||||
|
||||
/* Footer */
|
||||
.email-footer { padding:32px 0 0;text-align:center }
|
||||
.email-footer-text { margin:0;font-size:11px;color:rgba(160,175,200,.55);font-weight:300 }
|
||||
.email-footer-link { color:rgba(94,234,212,.5);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:rgba(210,252,245,.88)!important }
|
||||
|
||||
/* Content */
|
||||
.aurora-content,.aurora-content * { color:rgba(210,252,245,.88)!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:rgba(0,0,0,.3)!important;border-color:rgba(13,148,136,.2)!important }
|
||||
.aurora-content blockquote { color:rgba(255,255,255,.6)!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:rgba(13,148,136,.3)!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}}
|
||||
{{.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/{{.ID}}">{{ShortSha .ID.String}}</a> - {{.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">
|
||||
©
|
||||
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
|
||||
</p>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</center>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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:rgba(255,255,255,.4);letter-spacing:4px;text-transform:uppercase }
|
||||
.email-header { padding:0 0 32px;text-align:center }
|
||||
|
||||
/* Card */
|
||||
.email-card { padding:48px 40px;background:rgba(25,20,50,.8);border:1px solid rgba(13,148,136,.2);box-shadow:0 0 60px rgba(13,148,136,.08),0 0 120px rgba(99,102,241,.04) }
|
||||
.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:rgba(210,252,245,.88);font-weight:300 }
|
||||
.email-subtext { margin:0;font-size:13px;line-height:1.8;color:rgba(180,195,215,.95);font-weight:300 }
|
||||
|
||||
/* Divider */
|
||||
.email-divider { height:1px;background:linear-gradient(90deg,transparent,rgba(13,148,136,.3),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:rgba(13,148,136,.18);border:1px solid rgba(13,148,136,.3);letter-spacing:1px }
|
||||
.email-btn-table { margin:0 0 32px }
|
||||
|
||||
/* Fallback link */
|
||||
.email-link-box { border:1px solid rgba(13,148,136,.18);background:rgba(13,148,136,.03) }
|
||||
.email-link-label { margin:0 0 8px;font-size:11px;color:rgba(180,252,238,.75);font-weight:300 }
|
||||
.email-link-url { margin:0;font-size:11px;font-family:monospace;color:#99f6e4;background:rgba(0,0,0,.2);padding:10px 12px;border:1px solid rgba(13,148,136,.15);word-break:break-all }
|
||||
|
||||
/* Footer */
|
||||
.email-footer { padding:32px 0 0;text-align:center }
|
||||
.email-footer-text { margin:0;font-size:11px;color:rgba(160,175,200,.55);font-weight:300 }
|
||||
.email-footer-link { color:rgba(94,234,212,.5);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:rgba(210,252,245,.88)!important }
|
||||
|
||||
/* Content */
|
||||
.aurora-content,.aurora-content * { color:rgba(210,252,245,.88)!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:rgba(0,0,0,.3)!important;border-color:rgba(13,148,136,.2)!important }
|
||||
.aurora-content blockquote { color:rgba(255,255,255,.6)!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:rgba(13,148,136,.3)!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>{{.locale.Tr "mail.release.note"}}</p>
|
||||
<div class="aurora-content" style="font-size:14px;line-height:2;">{{.Release.RenderedNote}}</div>
|
||||
{{end}}
|
||||
|
||||
<p>{{.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">
|
||||
©
|
||||
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
|
||||
</p>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</center>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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:rgba(255,255,255,.4);letter-spacing:4px;text-transform:uppercase }
|
||||
.email-header { padding:0 0 32px;text-align:center }
|
||||
|
||||
/* Card */
|
||||
.email-card { padding:48px 40px;background:rgba(25,20,50,.8);border:1px solid rgba(13,148,136,.2);box-shadow:0 0 60px rgba(13,148,136,.08),0 0 120px rgba(99,102,241,.04) }
|
||||
.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:rgba(210,252,245,.88);font-weight:300 }
|
||||
.email-subtext { margin:0;font-size:13px;line-height:1.8;color:rgba(180,195,215,.95);font-weight:300 }
|
||||
|
||||
/* Divider */
|
||||
.email-divider { height:1px;background:linear-gradient(90deg,transparent,rgba(13,148,136,.3),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:rgba(13,148,136,.18);border:1px solid rgba(13,148,136,.3);letter-spacing:1px }
|
||||
.email-btn-table { margin:0 0 32px }
|
||||
|
||||
/* Fallback link */
|
||||
.email-link-box { border:1px solid rgba(13,148,136,.18);background:rgba(13,148,136,.03) }
|
||||
.email-link-label { margin:0 0 8px;font-size:11px;color:rgba(180,252,238,.75);font-weight:300 }
|
||||
.email-link-url { margin:0;font-size:11px;font-family:monospace;color:#99f6e4;background:rgba(0,0,0,.2);padding:10px 12px;border:1px solid rgba(13,148,136,.15);word-break:break-all }
|
||||
|
||||
/* Footer */
|
||||
.email-footer { padding:32px 0 0;text-align:center }
|
||||
.email-footer-text { margin:0;font-size:11px;color:rgba(160,175,200,.55);font-weight:300 }
|
||||
.email-footer-link { color:rgba(94,234,212,.5);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:rgba(210,252,245,.88)!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">
|
||||
©
|
||||
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
|
||||
</p>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</center>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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:rgba(255,255,255,.4);letter-spacing:4px;text-transform:uppercase }
|
||||
.email-header { padding:0 0 32px;text-align:center }
|
||||
|
||||
/* Card */
|
||||
.email-card { padding:48px 40px;background:rgba(25,20,50,.8);border:1px solid rgba(13,148,136,.2);box-shadow:0 0 60px rgba(13,148,136,.08),0 0 120px rgba(99,102,241,.04) }
|
||||
.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:rgba(210,252,245,.88);font-weight:300 }
|
||||
.email-subtext { margin:0;font-size:13px;line-height:1.8;color:rgba(180,195,215,.95);font-weight:300 }
|
||||
|
||||
/* Divider */
|
||||
.email-divider { height:1px;background:linear-gradient(90deg,transparent,rgba(13,148,136,.3),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:rgba(13,148,136,.18);border:1px solid rgba(13,148,136,.3);letter-spacing:1px }
|
||||
.email-btn-table { margin:0 0 32px }
|
||||
|
||||
/* Fallback link */
|
||||
.email-link-box { border:1px solid rgba(13,148,136,.18);background:rgba(13,148,136,.03) }
|
||||
.email-link-label { margin:0 0 8px;font-size:11px;color:rgba(180,252,238,.75);font-weight:300 }
|
||||
.email-link-url { margin:0;font-size:11px;font-family:monospace;color:#99f6e4;background:rgba(0,0,0,.2);padding:10px 12px;border:1px solid rgba(13,148,136,.15);word-break:break-all }
|
||||
|
||||
/* Footer */
|
||||
.email-footer { padding:32px 0 0;text-align:center }
|
||||
.email-footer-text { margin:0;font-size:11px;color:rgba(160,175,200,.55);font-weight:300 }
|
||||
.email-footer-link { color:rgba(94,234,212,.5);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:rgba(210,252,245,.88)!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">
|
||||
©
|
||||
<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:rgba(255,255,255,.4);letter-spacing:4px;text-transform:uppercase }
|
||||
.email-header { padding:0 0 32px;text-align:center }
|
||||
|
||||
/* Card */
|
||||
.email-card { padding:48px 40px;background:rgba(25,20,50,.8);border:1px solid rgba(13,148,136,.2);box-shadow:0 0 60px rgba(13,148,136,.08),0 0 120px rgba(99,102,241,.04) }
|
||||
.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:rgba(210,252,245,.88);font-weight:300 }
|
||||
.email-subtext { margin:0;font-size:13px;line-height:1.8;color:rgba(180,195,215,.95);font-weight:300 }
|
||||
|
||||
/* Divider */
|
||||
.email-divider { height:1px;background:linear-gradient(90deg,transparent,rgba(13,148,136,.3),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:rgba(13,148,136,.18);border:1px solid rgba(13,148,136,.3);letter-spacing:1px }
|
||||
.email-btn-table { margin:0 0 32px }
|
||||
|
||||
/* Fallback link */
|
||||
.email-link-box { border:1px solid rgba(13,148,136,.18);background:rgba(13,148,136,.03) }
|
||||
.email-link-label { margin:0 0 8px;font-size:11px;color:rgba(180,252,238,.75);font-weight:300 }
|
||||
.email-link-url { margin:0;font-size:11px;font-family:monospace;color:#99f6e4;background:rgba(0,0,0,.2);padding:10px 12px;border:1px solid rgba(13,148,136,.15);word-break:break-all }
|
||||
|
||||
/* Footer */
|
||||
.email-footer { padding:32px 0 0;text-align:center }
|
||||
.email-footer-text { margin:0;font-size:11px;color:rgba(160,175,200,.55);font-weight:300 }
|
||||
.email-footer-link { color:rgba(94,234,212,.5);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:rgba(210,252,245,.88)!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">
|
||||
©
|
||||
<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:rgba(255,255,255,.4);letter-spacing:4px;text-transform:uppercase }
|
||||
.email-header { padding:0 0 32px;text-align:center }
|
||||
|
||||
/* Card */
|
||||
.email-card { padding:48px 40px;background:rgba(25,20,50,.8);border:1px solid rgba(13,148,136,.2);box-shadow:0 0 60px rgba(13,148,136,.08),0 0 120px rgba(99,102,241,.04) }
|
||||
.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:rgba(210,252,245,.88);font-weight:300 }
|
||||
.email-subtext { margin:0;font-size:13px;line-height:1.8;color:rgba(180,195,215,.95);font-weight:300 }
|
||||
|
||||
/* Divider */
|
||||
.email-divider { height:1px;background:linear-gradient(90deg,transparent,rgba(13,148,136,.3),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:rgba(13,148,136,.18);border:1px solid rgba(13,148,136,.3);letter-spacing:1px }
|
||||
.email-btn-table { margin:0 0 32px }
|
||||
|
||||
/* Fallback link */
|
||||
.email-link-box { border:1px solid rgba(13,148,136,.18);background:rgba(13,148,136,.03) }
|
||||
.email-link-label { margin:0 0 8px;font-size:11px;color:rgba(180,252,238,.75);font-weight:300 }
|
||||
.email-link-url { margin:0;font-size:11px;font-family:monospace;color:#99f6e4;background:rgba(0,0,0,.2);padding:10px 12px;border:1px solid rgba(13,148,136,.15);word-break:break-all }
|
||||
|
||||
/* Footer */
|
||||
.email-footer { padding:32px 0 0;text-align:center }
|
||||
.email-footer-text { margin:0;font-size:11px;color:rgba(160,175,200,.55);font-weight:300 }
|
||||
.email-footer-link { color:rgba(94,234,212,.5);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:rgba(210,252,245,.88)!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">
|
||||
©
|
||||
<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:rgba(255,255,255,.4);letter-spacing:4px;text-transform:uppercase }
|
||||
.email-header { padding:0 0 32px;text-align:center }
|
||||
|
||||
/* Card */
|
||||
.email-card { padding:48px 40px;background:rgba(25,20,50,.8);border:1px solid rgba(13,148,136,.2);box-shadow:0 0 60px rgba(13,148,136,.08),0 0 120px rgba(99,102,241,.04) }
|
||||
.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:rgba(210,252,245,.88);font-weight:300 }
|
||||
.email-subtext { margin:0;font-size:13px;line-height:1.8;color:rgba(180,195,215,.95);font-weight:300 }
|
||||
|
||||
/* Divider */
|
||||
.email-divider { height:1px;background:linear-gradient(90deg,transparent,rgba(13,148,136,.3),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:rgba(13,148,136,.18);border:1px solid rgba(13,148,136,.3);letter-spacing:1px }
|
||||
.email-btn-table { margin:0 0 32px }
|
||||
|
||||
/* Fallback link */
|
||||
.email-link-box { border:1px solid rgba(13,148,136,.18);background:rgba(13,148,136,.03) }
|
||||
.email-link-label { margin:0 0 8px;font-size:11px;color:rgba(180,252,238,.75);font-weight:300 }
|
||||
.email-link-url { margin:0;font-size:11px;font-family:monospace;color:#99f6e4;background:rgba(0,0,0,.2);padding:10px 12px;border:1px solid rgba(13,148,136,.15);word-break:break-all }
|
||||
|
||||
/* Footer */
|
||||
.email-footer { padding:32px 0 0;text-align:center }
|
||||
.email-footer-text { margin:0;font-size:11px;color:rgba(160,175,200,.55);font-weight:300 }
|
||||
.email-footer-link { color:rgba(94,234,212,.5);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:rgba(210,252,245,.88)!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">
|
||||
©
|
||||
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
|
||||
</p>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</center>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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%; } }
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "aurora",
|
||||
"description": "Ethereal / Dreamlike",
|
||||
"mode": "framed",
|
||||
"layout": "aurora"
|
||||
}
|
||||
@@ -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 rgba(59,130,246,0.1);text-align:center }
|
||||
|
||||
/* Card */
|
||||
.email-card { max-width:600px;margin:24px auto;background:#ffffff;border-radius:16px;box-shadow:0 2px 4px rgba(0,0,0,0.06),0 8px 24px rgba(0,0,0,0.06),0 0 0 1px rgba(0,0,0,0.04);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 rgba(59,130,246,0.1);border-radius:12px;padding:16px 18px;margin:24px 0 16px;box-shadow:inset 0 1px 0 rgba(255,255,255,0.9) }
|
||||
.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 rgba(0,0,0,0.06);word-break:break-all;font-family:monospace }
|
||||
|
||||
/* Footer */
|
||||
.email-footer { padding:18px 32px 22px;background:linear-gradient(135deg,#f8f9fc,#f4f6fb);border-top:1px solid rgba(0,0,0,0.06);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">
|
||||
©
|
||||
<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 rgba(59,130,246,0.1);text-align:center }
|
||||
|
||||
/* Card */
|
||||
.email-card { max-width:600px;margin:24px auto;background:#ffffff;border-radius:16px;box-shadow:0 2px 4px rgba(0,0,0,0.06),0 8px 24px rgba(0,0,0,0.06),0 0 0 1px rgba(0,0,0,0.04);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 rgba(0,0,0,0.06);border-radius:10px;padding:12px 16px;margin-bottom:8px;box-shadow:inset 0 1px 0 rgba(255,255,255,0.7) }
|
||||
.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 rgba(59,130,246,0.1);border-radius:12px;padding:16px 18px;margin:24px 0 16px;box-shadow:inset 0 1px 0 rgba(255,255,255,0.9) }
|
||||
.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 rgba(0,0,0,0.06);word-break:break-all;font-family:monospace }
|
||||
|
||||
/* Footer */
|
||||
.email-footer { padding:18px 32px 22px;background:linear-gradient(135deg,#f8f9fc,#f4f6fb);border-top:1px solid rgba(0,0,0,0.06);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}} — {{.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}} — {{$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">
|
||||
©
|
||||
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</center>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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 rgba(59,130,246,0.1);text-align:center }
|
||||
|
||||
/* Card */
|
||||
.email-card { max-width:600px;margin:24px auto;background:#ffffff;border-radius:16px;box-shadow:0 2px 4px rgba(0,0,0,0.06),0 8px 24px rgba(0,0,0,0.06),0 0 0 1px rgba(0,0,0,0.04);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 rgba(59,130,246,0.12) }
|
||||
|
||||
/* 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 rgba(59,130,246,0.1);border-radius:12px;padding:16px 18px;margin:24px 0 16px;box-shadow:inset 0 1px 0 rgba(255,255,255,0.9) }
|
||||
.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 rgba(0,0,0,0.06);word-break:break-all;font-family:monospace }
|
||||
|
||||
/* Footer */
|
||||
.email-footer { padding:18px 32px 22px;background:linear-gradient(135deg,#f8f9fc,#f4f6fb);border-top:1px solid rgba(0,0,0,0.06);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">
|
||||
©
|
||||
<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 rgba(59,130,246,0.1);text-align:center }
|
||||
|
||||
/* Card */
|
||||
.email-card { max-width:600px;margin:24px auto;background:#ffffff;border-radius:16px;box-shadow:0 2px 4px rgba(0,0,0,0.06),0 8px 24px rgba(0,0,0,0.06),0 0 0 1px rgba(0,0,0,0.04);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 rgba(59,130,246,0.1);border-radius:12px;padding:16px 18px;margin:24px 0 16px;box-shadow:inset 0 1px 0 rgba(255,255,255,0.9) }
|
||||
.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 rgba(0,0,0,0.06);word-break:break-all;font-family:monospace }
|
||||
|
||||
/* Footer */
|
||||
.email-footer { padding:18px 32px 22px;background:linear-gradient(135deg,#f8f9fc,#f4f6fb);border-top:1px solid rgba(0,0,0,0.06);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">
|
||||
©
|
||||
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</center>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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 rgba(59,130,246,0.1);text-align:center }
|
||||
|
||||
/* Card */
|
||||
.email-card { max-width:600px;margin:24px auto;background:#ffffff;border-radius:16px;box-shadow:0 2px 4px rgba(0,0,0,0.06),0 8px 24px rgba(0,0,0,0.06),0 0 0 1px rgba(0,0,0,0.04);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 rgba(0,0,0,0.06) }
|
||||
hr { border:none;border-top:1px solid rgba(0,0,0,0.06);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 rgba(59,130,246,0.1);border-radius:12px;padding:16px 18px;margin:24px 0 16px;box-shadow:inset 0 1px 0 rgba(255,255,255,0.9) }
|
||||
.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 rgba(0,0,0,0.06);word-break:break-all;font-family:monospace }
|
||||
|
||||
/* Footer */
|
||||
.email-footer { padding:18px 32px 22px;background:linear-gradient(135deg,#f8f9fc,#f4f6fb);border-top:1px solid rgba(0,0,0,0.06);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}} {{.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> — {{.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">
|
||||
©
|
||||
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</center>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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 rgba(59,130,246,0.1);text-align:center }
|
||||
|
||||
/* Card */
|
||||
.email-card { max-width:600px;margin:24px auto;background:#ffffff;border-radius:16px;box-shadow:0 2px 4px rgba(0,0,0,0.06),0 8px 24px rgba(0,0,0,0.06),0 0 0 1px rgba(0,0,0,0.04);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 rgba(59,130,246,0.1);border-radius:12px;padding:16px 18px;margin:24px 0 16px;box-shadow:inset 0 1px 0 rgba(255,255,255,0.9) }
|
||||
.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 rgba(0,0,0,0.06);word-break:break-all;font-family:monospace }
|
||||
|
||||
/* Footer */
|
||||
.email-footer { padding:18px 32px 22px;background:linear-gradient(135deg,#f8f9fc,#f4f6fb);border-top:1px solid rgba(0,0,0,0.06);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">
|
||||
©
|
||||
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</center>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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 rgba(59,130,246,0.1);text-align:center }
|
||||
|
||||
/* Card */
|
||||
.email-card { max-width:600px;margin:24px auto;background:#ffffff;border-radius:16px;box-shadow:0 2px 4px rgba(0,0,0,0.06),0 8px 24px rgba(0,0,0,0.06),0 0 0 1px rgba(0,0,0,0.04);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 rgba(59,130,246,0.1);border-radius:12px;padding:16px 18px;margin:24px 0 16px;box-shadow:inset 0 1px 0 rgba(255,255,255,0.9) }
|
||||
.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 rgba(0,0,0,0.06);word-break:break-all;font-family:monospace }
|
||||
|
||||
/* Footer */
|
||||
.email-footer { padding:18px 32px 22px;background:linear-gradient(135deg,#f8f9fc,#f4f6fb);border-top:1px solid rgba(0,0,0,0.06);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">
|
||||
©
|
||||
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</center>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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 rgba(59,130,246,0.1);text-align:center }
|
||||
|
||||
/* Card */
|
||||
.email-card { max-width:600px;margin:24px auto;background:#ffffff;border-radius:16px;box-shadow:0 2px 4px rgba(0,0,0,0.06),0 8px 24px rgba(0,0,0,0.06),0 0 0 1px rgba(0,0,0,0.04);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 rgba(59,130,246,0.1);border-radius:12px;padding:16px 18px;margin:24px 0 16px;box-shadow:inset 0 1px 0 rgba(255,255,255,0.9) }
|
||||
.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 rgba(0,0,0,0.06);word-break:break-all;font-family:monospace }
|
||||
|
||||
/* Footer */
|
||||
.email-footer { padding:18px 32px 22px;background:linear-gradient(135deg,#f8f9fc,#f4f6fb);border-top:1px solid rgba(0,0,0,0.06);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">
|
||||
©
|
||||
<a href="{{AppUrl}}" class="email-footer-link">{{AppName}}</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</center>
|
||||
</body>
|
||||
</html>
|
||||