Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8089e8a7e | ||
|
|
cb9382ade4 | ||
|
|
8ccb5ac443 | ||
|
|
3d7cf17fb1 | ||
|
|
bcf1b87e82 | ||
|
|
6733e81356 | ||
|
|
e51e2e9996 | ||
|
|
5f5ad058a8 |
No files matched your search
@@ -1,16 +0,0 @@
|
|||||||
# 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)
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# 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)
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# 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)
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
"""Gitea API operations for the release workflow (Python stdlib only)."""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
import re
|
|
||||||
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 create_issue(api, version):
|
|
||||||
if not re.fullmatch(r"v\d+\.\d+\.\d+", version):
|
|
||||||
raise ValueError("Expected a stable vX.Y.Z release tag")
|
|
||||||
marker = f"<!-- gitea-mail-templates:template-release:{version} -->"
|
|
||||||
title = f"Update documentation for template release {version}"
|
|
||||||
page = 1
|
|
||||||
while True:
|
|
||||||
issues = api.request("GET", f"/issues?state=all&type=issues&limit=50&page={page}")
|
|
||||||
for issue in issues:
|
|
||||||
if issue.get("pull_request") is None and (marker in (issue.get("body") or "") or issue.get("title") == title):
|
|
||||||
print(f"[PASS] Reminder issue already exists: {issue['html_url']}")
|
|
||||||
return issue
|
|
||||||
if not issues:
|
|
||||||
break
|
|
||||||
page += 1
|
|
||||||
body = f"""{marker}
|
|
||||||
Template release **{version}** has been published. Documentation is maintained manually.
|
|
||||||
|
|
||||||
- [ ] Update the release version in the English and Chinese READMEs.
|
|
||||||
- [ ] Review compatibility records and mark versions verified only according to test results.
|
|
||||||
- [ ] Check release links, release notes and remaining version references.
|
|
||||||
|
|
||||||
This reminder does not change repository files, branches or existing release assets.
|
|
||||||
"""
|
|
||||||
issue = api.request("POST", "/issues", {"title": title, "body": body})
|
|
||||||
print(f"[PASS] Created reminder issue: {issue['html_url']}")
|
|
||||||
return issue
|
|
||||||
|
|
||||||
|
|
||||||
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-issue", "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-issue":
|
|
||||||
create_issue(api, args.version)
|
|
||||||
else:
|
|
||||||
publish_release(api, args.version)
|
|
||||||
except (ValueError, KeyError, OSError, RuntimeError) as error:
|
|
||||||
parser.exit(1, f"[FAIL] {error}\n")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -4,10 +4,28 @@ import argparse
|
|||||||
import hashlib
|
import hashlib
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
from pathlib import Path, PurePosixPath
|
|
||||||
import re
|
import re
|
||||||
import tarfile
|
import tarfile
|
||||||
import zipfile
|
import zipfile
|
||||||
|
from pathlib import Path, PurePosixPath
|
||||||
|
|
||||||
|
|
||||||
|
VERSION_PATTERN = re.compile(r"v[0-9]+\.[0-9]+\.[0-9]+")
|
||||||
|
THEME_NAME_PATTERN = re.compile(r"[a-z][a-z0-9-]*")
|
||||||
|
BASE_TEMPLATES = {"mail/base/head.tmpl", "mail/base/footer.tmpl"}
|
||||||
|
REQUIRED_FILES = (
|
||||||
|
"LICENSE",
|
||||||
|
"README.md",
|
||||||
|
"CONTRIBUTING.md",
|
||||||
|
"COMPATIBILITY.md",
|
||||||
|
"docs/README.zh-CN.md",
|
||||||
|
"docs/CONTRIBUTING.zh-CN.md",
|
||||||
|
"preview/index.html",
|
||||||
|
)
|
||||||
|
OPTIONAL_ROOT_DOCUMENTS = ("AGENTS.md", "THIRD_PARTY_NOTICES.md")
|
||||||
|
DOCUMENTATION_DIRECTORIES = ("docs", ".github/release-notes")
|
||||||
|
PREVIEW_MANIFEST_PREFIX = b"window.__MAIL_PREVIEW__ = "
|
||||||
|
ARCHIVE_EXTENSIONS = (".zip", ".tar.gz")
|
||||||
|
|
||||||
|
|
||||||
def read_file(path):
|
def read_file(path):
|
||||||
@@ -31,41 +49,75 @@ def digest(data):
|
|||||||
return hashlib.sha256(data).hexdigest()
|
return hashlib.sha256(data).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def collect(root, version):
|
def read_json(path):
|
||||||
root = Path(root)
|
return json.loads(read_file(path))
|
||||||
if not re.fullmatch(r"v[0-9]+\.[0-9]+\.[0-9]+", version):
|
|
||||||
|
|
||||||
|
def load_lock(root, version):
|
||||||
|
"""Validate the requested release identity before collecting any output."""
|
||||||
|
if not VERSION_PATTERN.fullmatch(version):
|
||||||
raise ValueError("Expected a stable vX.Y.Z package version")
|
raise ValueError("Expected a stable vX.Y.Z package version")
|
||||||
lock = json.loads(read_file(root / "gitea.lock.json"))
|
lock = read_json(root / "gitea.lock.json")
|
||||||
if version != lock["tag"]:
|
if version != lock["tag"]:
|
||||||
raise ValueError("Package version must match the locked Gitea tag")
|
raise ValueError("Package version must match the locked Gitea tag")
|
||||||
files = {}
|
return lock
|
||||||
names = sorted(p.name for p in (root / "themes").iterdir() if p.is_dir() and not p.is_symlink())
|
|
||||||
|
|
||||||
|
def discover_theme_names(root):
|
||||||
|
names = sorted(
|
||||||
|
path.name
|
||||||
|
for path in (root / "themes").iterdir()
|
||||||
|
if path.is_dir() and not path.is_symlink()
|
||||||
|
)
|
||||||
if not names:
|
if not names:
|
||||||
raise ValueError("No source theme manifests")
|
raise ValueError("No source theme manifests")
|
||||||
for name in names:
|
return names
|
||||||
if not re.fullmatch(r"[a-z][a-z0-9-]*", name):
|
|
||||||
|
|
||||||
|
def expected_template_paths(root, lock, mode):
|
||||||
|
"""Shared themes override base files; framed themes also supply all bodies."""
|
||||||
|
paths = set(BASE_TEMPLATES)
|
||||||
|
if mode == "framed":
|
||||||
|
paths.update(
|
||||||
|
path.removeprefix("templates/")
|
||||||
|
for path in lock["files"]
|
||||||
|
if path.startswith("templates/mail/") and path.endswith(".tmpl")
|
||||||
|
)
|
||||||
|
paths.update(
|
||||||
|
path.relative_to(root / "framework").as_posix()
|
||||||
|
for path in (root / "framework" / "mail").rglob("*.tmpl")
|
||||||
|
)
|
||||||
|
return paths
|
||||||
|
|
||||||
|
|
||||||
|
def collect_theme(root, lock, name):
|
||||||
|
"""Accept only a complete build whose identity and checksums still match."""
|
||||||
|
if not THEME_NAME_PATTERN.fullmatch(name):
|
||||||
raise ValueError(f"Invalid theme name: {name}")
|
raise ValueError(f"Invalid theme name: {name}")
|
||||||
source = root / "themes" / name
|
source = root / "themes" / name
|
||||||
theme = json.loads(read_file(source / "theme.json"))
|
theme = read_json(source / "theme.json")
|
||||||
if theme["name"] != name or theme["mode"] not in ("shared", "framed"):
|
if theme["name"] != name or theme["mode"] not in ("shared", "framed"):
|
||||||
raise ValueError(f"Invalid theme metadata: {name}")
|
raise ValueError(f"Invalid theme metadata: {name}")
|
||||||
built = root / "build" / "themes" / name
|
built = root / "build" / "themes" / name
|
||||||
meta = json.loads(read_file(built / "build.json"))
|
metadata = read_json(built / "build.json")
|
||||||
if (meta["theme"], meta["mode"], meta["gitea_tag"], meta["gitea_commit"]) != (name, theme["mode"], version, lock["commit"]):
|
actual_identity = (
|
||||||
|
metadata["theme"], metadata["mode"],
|
||||||
|
metadata["gitea_tag"], metadata["gitea_commit"],
|
||||||
|
)
|
||||||
|
expected_identity = (name, theme["mode"], lock["tag"], lock["commit"])
|
||||||
|
if actual_identity != expected_identity:
|
||||||
raise ValueError(f"Stale build identity: {name}")
|
raise ValueError(f"Stale build identity: {name}")
|
||||||
if not {"theme.json", "theme.css"}.issubset(meta.get("sources") or {}) or not meta.get("files"):
|
has_sources = {"theme.json", "theme.css"}.issubset(metadata.get("sources") or {})
|
||||||
|
if not has_sources or not metadata.get("files"):
|
||||||
raise ValueError(f"Missing source/file provenance: {name}")
|
raise ValueError(f"Missing source/file provenance: {name}")
|
||||||
expected_files = {"mail/base/head.tmpl", "mail/base/footer.tmpl"}
|
if set(metadata["files"]) != expected_template_paths(root, lock, theme["mode"]):
|
||||||
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}")
|
raise ValueError(f"Incomplete generated install package: {name}")
|
||||||
for path, expected in meta["sources"].items():
|
for path, expected in metadata["sources"].items():
|
||||||
base = root if path.startswith(("framework/", "tools/")) else source
|
base = root if path.startswith(("framework/", "tools/")) else source
|
||||||
if digest(read_file(checked_path(base, path))) != expected:
|
if digest(read_file(checked_path(base, path))) != expected:
|
||||||
raise ValueError(f"Source changed since build: {name}/{path}")
|
raise ValueError(f"Source changed since build: {name}/{path}")
|
||||||
for path, expected in meta["files"].items():
|
files = {}
|
||||||
|
for path, expected in metadata["files"].items():
|
||||||
if not path.startswith("mail/") or not path.endswith(".tmpl"):
|
if not path.startswith("mail/") or not path.endswith(".tmpl"):
|
||||||
raise ValueError(f"Unexpected generated file: {path}")
|
raise ValueError(f"Unexpected generated file: {path}")
|
||||||
data = read_file(checked_path(built, path))
|
data = read_file(checked_path(built, path))
|
||||||
@@ -73,59 +125,111 @@ def collect(root, version):
|
|||||||
raise ValueError(f"Generated checksum mismatch: {name}/{path}")
|
raise ValueError(f"Generated checksum mismatch: {name}/{path}")
|
||||||
files[f"themes/{name}/{path}"] = data
|
files[f"themes/{name}/{path}"] = data
|
||||||
files[f"themes/{name}/build.json"] = read_file(built / "build.json")
|
files[f"themes/{name}/build.json"] = read_file(built / "build.json")
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
|
def parse_script_payload(data, prefix):
|
||||||
|
"""Read generated JSON without executing the surrounding JavaScript."""
|
||||||
|
return json.loads(data.split(prefix, 1)[1].strip().removesuffix(b";"))
|
||||||
|
|
||||||
|
|
||||||
|
def collect_preview(root, lock, theme_names):
|
||||||
manifest_data = read_file(root / "preview" / "rendered.js")
|
manifest_data = read_file(root / "preview" / "rendered.js")
|
||||||
prefix = b"window.__MAIL_PREVIEW__ = "
|
manifest = parse_script_payload(manifest_data, PREVIEW_MANIFEST_PREFIX)
|
||||||
manifest = json.loads(manifest_data.split(prefix, 1)[1].strip().removesuffix(b";"))
|
languages = sorted(
|
||||||
languages = sorted(path.removeprefix("options/locale/locale_").removesuffix(".json") for path in lock["files"] if path.startswith("options/locale/locale_") and path.endswith(".json"))
|
path.removeprefix("options/locale/locale_").removesuffix(".json")
|
||||||
if manifest["upstream"] != version or manifest.get("upstream_commit") != lock["commit"] or sorted(manifest["themes"]) != names or sorted(manifest["languages"]) != languages:
|
for path in lock["files"]
|
||||||
raise ValueError("Preview does not match lock/all source themes and languages; run preview all")
|
if path.startswith("options/locale/locale_") and path.endswith(".json")
|
||||||
files["preview/rendered.js"] = manifest_data
|
)
|
||||||
for lang in languages:
|
matches_build = (
|
||||||
data = read_file(root / "preview" / "rendered" / (lang + ".js"))
|
manifest["upstream"] == lock["tag"]
|
||||||
marker = ('window.__MAIL_PREVIEW_DATA__[' + json.dumps(lang) + '] = ').encode()
|
and manifest.get("upstream_commit") == lock["commit"]
|
||||||
payload = json.loads(data.split(marker, 1)[1].strip().removesuffix(b";"))
|
and sorted(manifest["themes"]) == theme_names
|
||||||
if sorted(payload) != names or any(set(payload[name]) != set(manifest["registry"]) for name in names):
|
and sorted(manifest["languages"]) == languages
|
||||||
raise ValueError(f"Incomplete preview language bundle: {lang}")
|
)
|
||||||
files[f"preview/rendered/{lang}.js"] = data
|
if not matches_build:
|
||||||
for name in ["LICENSE", "README.md", "CONTRIBUTING.md", "COMPATIBILITY.md", "docs/README.zh-CN.md", "docs/CONTRIBUTING.zh-CN.md", "preview/index.html"]:
|
raise ValueError(
|
||||||
files[name] = read_file(root / name)
|
"Preview does not match lock/all source themes and languages; run preview all"
|
||||||
for name in ["AGENTS.md", "THIRD_PARTY_NOTICES.md"]:
|
)
|
||||||
|
files = {"preview/rendered.js": manifest_data}
|
||||||
|
expected_templates = set(manifest["registry"])
|
||||||
|
for language in languages:
|
||||||
|
data = read_file(root / "preview" / "rendered" / (language + ".js"))
|
||||||
|
prefix = ('window.__MAIL_PREVIEW_DATA__[' + json.dumps(language) + '] = ').encode()
|
||||||
|
payload = parse_script_payload(data, prefix)
|
||||||
|
if sorted(payload) != theme_names or any(
|
||||||
|
set(payload[name]) != expected_templates for name in theme_names
|
||||||
|
):
|
||||||
|
raise ValueError(f"Incomplete preview language bundle: {language}")
|
||||||
|
files[f"preview/rendered/{language}.js"] = data
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
|
def collect_documentation(root):
|
||||||
|
files = {name: read_file(root / name) for name in REQUIRED_FILES}
|
||||||
|
for name in OPTIONAL_ROOT_DOCUMENTS:
|
||||||
if (root / name).exists():
|
if (root / name).exists():
|
||||||
files[name] = read_file(root / name)
|
files[name] = read_file(root / name)
|
||||||
for directory in [root / "docs", root / ".github" / "release-notes"]:
|
for directory in DOCUMENTATION_DIRECTORIES:
|
||||||
for path in sorted(directory.rglob("*.md")):
|
for path in sorted((root / directory).rglob("*.md")):
|
||||||
name = path.relative_to(root).as_posix()
|
name = path.relative_to(root).as_posix()
|
||||||
files[name] = read_file(checked_path(root, name))
|
files[name] = read_file(checked_path(root, name))
|
||||||
for path in sorted((root / "docs" / "images").iterdir()):
|
for path in sorted((root / "docs" / "images").iterdir()):
|
||||||
if path.is_file() and (path.suffix == ".png" or path.name == "README.md"):
|
if path.is_file() and (path.suffix == ".png" or path.name == "README.md"):
|
||||||
files["docs/images/" + path.name] = read_file(path)
|
files["docs/images/" + path.name] = read_file(path)
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
|
def collect_provenance(root, lock):
|
||||||
license_data = read_file(root / "build" / "upstream" / "LICENSE")
|
license_data = read_file(root / "build" / "upstream" / "LICENSE")
|
||||||
if digest(license_data) != lock["files"]["LICENSE"]:
|
if digest(license_data) != lock["files"]["LICENSE"]:
|
||||||
raise ValueError("Upstream license checksum mismatch")
|
raise ValueError("Upstream license checksum mismatch")
|
||||||
files["GITEA-LICENSE"] = license_data
|
return {
|
||||||
files["upstream-lock.json"] = read_file(root / "gitea.lock.json")
|
"GITEA-LICENSE": license_data,
|
||||||
|
"upstream-lock.json": read_file(root / "gitea.lock.json"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def collect(root, version):
|
||||||
|
"""Collect verified release content without writing any archives."""
|
||||||
|
root = Path(root)
|
||||||
|
lock = load_lock(root, version)
|
||||||
|
theme_names = discover_theme_names(root)
|
||||||
|
files = {}
|
||||||
|
for name in theme_names:
|
||||||
|
files.update(collect_theme(root, lock, name))
|
||||||
|
files.update(collect_preview(root, lock, theme_names))
|
||||||
|
files.update(collect_documentation(root))
|
||||||
|
files.update(collect_provenance(root, lock))
|
||||||
return files
|
return files
|
||||||
|
|
||||||
|
|
||||||
def package(root, version, output):
|
def package(root, version, output):
|
||||||
|
"""Write both archive formats from the same verified file collection."""
|
||||||
files = collect(root, version)
|
files = collect(root, version)
|
||||||
archive = f"gitea-mail-templates-{version}"
|
archive = f"gitea-mail-templates-{version}"
|
||||||
output = Path(output)
|
output = Path(output)
|
||||||
targets = [output / (archive + ext) for ext in (".zip", ".tar.gz")]
|
targets = [output / (archive + ext) for ext in ARCHIVE_EXTENSIONS]
|
||||||
if any(path.exists() for path in targets):
|
if any(path.exists() for path in targets):
|
||||||
raise ValueError("Refusing to overwrite existing release archives; use a new output directory")
|
raise ValueError(
|
||||||
|
"Refusing to overwrite existing release archives; use a new output directory"
|
||||||
|
)
|
||||||
output.mkdir(parents=True, exist_ok=True)
|
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:
|
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()):
|
for name, data in sorted(files.items()):
|
||||||
path = archive + "/" + name
|
path = archive + "/" + name
|
||||||
zipped.writestr(path, data)
|
zipped.writestr(path, data)
|
||||||
info = tarfile.TarInfo(path)
|
info = tarfile.TarInfo(path)
|
||||||
info.size, info.mode = len(data), 0o644
|
info.size = len(data)
|
||||||
|
info.mode = 0o644
|
||||||
tar.addfile(info, io.BytesIO(data))
|
tar.addfile(info, io.BytesIO(data))
|
||||||
print(f"[PASS] Packaged {len(files)} verified files: {targets[0]}, {targets[1]}")
|
print(f"[PASS] Packaged {len(files)} verified files: {targets[0]}, {targets[1]}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
def main():
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
parser.add_argument("--version", required=True)
|
parser.add_argument("--version", required=True)
|
||||||
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[2])
|
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[2])
|
||||||
@@ -135,3 +239,7 @@ if __name__ == "__main__":
|
|||||||
package(args.root, args.version, args.output)
|
package(args.root, args.version, args.output)
|
||||||
except (ValueError, KeyError, IndexError, OSError) as error:
|
except (ValueError, KeyError, IndexError, OSError) as error:
|
||||||
parser.exit(1, f"[FAIL] {error}\n")
|
parser.exit(1, f"[FAIL] {error}\n")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
"""Offline Gitea API and issue-reminder regression tests."""
|
|
||||||
|
|
||||||
import importlib.util
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
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 IssueTests(unittest.TestCase):
|
|
||||||
def test_creates_release_documentation_issue(self):
|
|
||||||
api = Mock()
|
|
||||||
api.request.side_effect = [[], {"html_url": "https://git.example/issues/1"}]
|
|
||||||
ACTIONS.create_issue(api, "v28.1.0")
|
|
||||||
calls = api.request.call_args_list
|
|
||||||
self.assertEqual(["GET", "POST"], [c.args[0] for c in calls])
|
|
||||||
self.assertEqual("/issues", calls[-1].args[1])
|
|
||||||
payload = calls[-1].args[2]
|
|
||||||
self.assertIn("<!-- gitea-mail-templates:template-release:v28.1.0 -->", payload["body"])
|
|
||||||
self.assertIn("Documentation is maintained manually", payload["body"])
|
|
||||||
|
|
||||||
def test_reuses_closed_issue_on_later_page_and_ignores_pull_requests(self):
|
|
||||||
api = Mock()
|
|
||||||
marker = "<!-- gitea-mail-templates:template-release:v28.1.0 -->"
|
|
||||||
api.request.side_effect = [[{"body": marker, "pull_request": {"url": "pr"}}],
|
|
||||||
[{"body": marker, "state": "closed", "title": "Renamed", "html_url": "https://git.example/issues/1"}]]
|
|
||||||
ACTIONS.create_issue(api, "v28.1.0")
|
|
||||||
self.assertEqual(["GET", "GET"], [c.args[0] for c in api.request.call_args_list])
|
|
||||||
self.assertIn("state=all", api.request.call_args_list[0].args[1])
|
|
||||||
self.assertIn("page=2", api.request.call_args_list[1].args[1])
|
|
||||||
|
|
||||||
def test_other_release_versions_do_not_suppress_reminder(self):
|
|
||||||
api = Mock()
|
|
||||||
api.request.side_effect = [[{"body": "<!-- gitea-mail-templates:template-release:v28.0.0 -->"}], [],
|
|
||||||
{"html_url": "https://git.example/issues/2"}]
|
|
||||||
ACTIONS.create_issue(api, "v28.1.0")
|
|
||||||
payload = api.request.call_args.args[2]
|
|
||||||
self.assertIn("template-release:v28.1.0", payload["body"])
|
|
||||||
self.assertIn("READMEs", payload["body"])
|
|
||||||
|
|
||||||
def test_invalid_versions_fail_before_api_call(self):
|
|
||||||
api = Mock()
|
|
||||||
for version in ("28.1.0", "v28.1.0-rc1", "../../x"):
|
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
ACTIONS.create_issue(api, version)
|
|
||||||
api.request.assert_not_called()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,97 +1,196 @@
|
|||||||
"""Offline artifact/provenance tests independent of generated repository output."""
|
"""Offline packaging tests using a minimal, independently generated repository."""
|
||||||
|
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
import tarfile
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
import zipfile
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
SPEC = importlib.util.spec_from_file_location("package_release", Path(__file__).with_name("package_release.py"))
|
|
||||||
|
SCRIPT_PATH = Path(__file__).with_name("package_release.py")
|
||||||
|
SPEC = importlib.util.spec_from_file_location("package_release", SCRIPT_PATH)
|
||||||
PACKER = importlib.util.module_from_spec(SPEC)
|
PACKER = importlib.util.module_from_spec(SPEC)
|
||||||
SPEC.loader.exec_module(PACKER)
|
SPEC.loader.exec_module(PACKER)
|
||||||
|
|
||||||
|
|
||||||
class PackagingTests(unittest.TestCase):
|
class PackagingTests(unittest.TestCase):
|
||||||
|
VERSION = "v28.0.0"
|
||||||
|
COMMIT = "a" * 40
|
||||||
|
THEME = "demo"
|
||||||
|
LANGUAGE = "en-US"
|
||||||
|
ROOT_DOCUMENTS = (
|
||||||
|
"LICENSE",
|
||||||
|
"AGENTS.md",
|
||||||
|
"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",
|
||||||
|
)
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.temp = tempfile.TemporaryDirectory()
|
temporary_directory = tempfile.TemporaryDirectory()
|
||||||
self.addCleanup(self.temp.cleanup)
|
self.addCleanup(temporary_directory.cleanup)
|
||||||
self.root = Path(self.temp.name)
|
self.root = Path(temporary_directory.name)
|
||||||
self.tag = "v28.0.0"
|
self.output = self.root / "artifacts"
|
||||||
self.commit = "a" * 40
|
self.create_theme()
|
||||||
theme = json.dumps({"name": "demo", "mode": "shared"}).encode()
|
self.create_snapshot()
|
||||||
css = b"a { color:blue; }"
|
self.create_preview()
|
||||||
self.write("themes/demo/theme.json", theme)
|
for name in self.ROOT_DOCUMENTS:
|
||||||
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", "AGENTS.md", "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")
|
self.write(name, b"test")
|
||||||
|
|
||||||
|
# Fixture helpers keep test bodies focused on the behavior being checked.
|
||||||
def write(self, name, data):
|
def write(self, name, data):
|
||||||
path = self.root / name
|
path = self.root / name
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
path.write_bytes(data)
|
path.write_bytes(data)
|
||||||
|
|
||||||
def write_json(self, name, data):
|
def write_json(self, name, data):
|
||||||
self.write(name, json.dumps(data).encode())
|
self.write(name, json.dumps(data).encode("utf-8"))
|
||||||
|
|
||||||
def test_packages_declared_outputs_only_and_never_overwrites(self):
|
def create_theme(self):
|
||||||
|
theme_data = json.dumps({"name": self.THEME, "mode": "shared"}).encode("utf-8")
|
||||||
|
css_data = b"a { color:blue; }"
|
||||||
|
self.write("themes/demo/theme.json", theme_data)
|
||||||
|
self.write("themes/demo/theme.css", css_data)
|
||||||
|
|
||||||
|
generated_files = {
|
||||||
|
"mail/base/head.tmpl": b"<title>test</title>",
|
||||||
|
"mail/base/footer.tmpl": b"<p>footer</p>",
|
||||||
|
}
|
||||||
|
metadata = {
|
||||||
|
"theme": self.THEME,
|
||||||
|
"mode": "shared",
|
||||||
|
"gitea_tag": self.VERSION,
|
||||||
|
"gitea_commit": self.COMMIT,
|
||||||
|
"sources": {
|
||||||
|
"theme.json": PACKER.digest(theme_data),
|
||||||
|
"theme.css": PACKER.digest(css_data),
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
path: PACKER.digest(data)
|
||||||
|
for path, data in generated_files.items()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for path, data in generated_files.items():
|
||||||
|
self.write("build/themes/demo/" + path, data)
|
||||||
|
self.write_json("build/themes/demo/build.json", metadata)
|
||||||
|
|
||||||
|
def create_snapshot(self):
|
||||||
|
self.write_json("gitea.lock.json", {
|
||||||
|
"tag": self.VERSION,
|
||||||
|
"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")
|
||||||
|
|
||||||
|
def create_preview(self):
|
||||||
|
manifest = {
|
||||||
|
"upstream": self.VERSION,
|
||||||
|
"upstream_commit": self.COMMIT,
|
||||||
|
"themes": [self.THEME],
|
||||||
|
"languages": [self.LANGUAGE],
|
||||||
|
"registry": {"activate": {}},
|
||||||
|
}
|
||||||
|
bundle = {self.THEME: {"activate": "<html></html>"}}
|
||||||
|
self.write("preview/rendered.js", (
|
||||||
|
"window.__MAIL_PREVIEW__ = " + json.dumps(manifest) + ";"
|
||||||
|
).encode("utf-8"))
|
||||||
|
self.write("preview/rendered/en-US.js", (
|
||||||
|
'window.__MAIL_PREVIEW_DATA__["en-US"] = ' + json.dumps(bundle) + ";"
|
||||||
|
).encode("utf-8"))
|
||||||
|
|
||||||
|
# Archives: both formats contain the verified bytes, never stale build files.
|
||||||
|
def test_archives_contain_only_verified_outputs(self):
|
||||||
self.write("build/themes/stale/mail/stale.tmpl", b"stale")
|
self.write("build/themes/stale/mail/stale.tmpl", b"stale")
|
||||||
out = self.root / "artifacts"
|
expected_files = PACKER.collect(self.root, self.VERSION)
|
||||||
PACKER.package(self.root, self.tag, out)
|
prefix = f"gitea-mail-templates-{self.VERSION}/"
|
||||||
with zipfile.ZipFile(next(out.glob("*.zip"))) as archive:
|
expected_members = {
|
||||||
names = archive.namelist()
|
prefix + name: data for name, data in expected_files.items()
|
||||||
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):
|
PACKER.package(self.root, self.VERSION, self.output)
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
|
with zipfile.ZipFile(self.output / f"gitea-mail-templates-{self.VERSION}.zip") as archive:
|
||||||
|
zipped = {name: archive.read(name) for name in archive.namelist()}
|
||||||
|
with tarfile.open(self.output / f"gitea-mail-templates-{self.VERSION}.tar.gz") as archive:
|
||||||
|
tarred = {
|
||||||
|
member.name: archive.extractfile(member).read()
|
||||||
|
for member in archive.getmembers()
|
||||||
|
}
|
||||||
|
self.assertTrue(all(member.mode == 0o644 for member in archive.getmembers()))
|
||||||
|
self.assertEqual(expected_members, zipped)
|
||||||
|
self.assertEqual(expected_members, tarred)
|
||||||
|
self.assertIn(prefix + "themes/demo/mail/base/head.tmpl", zipped)
|
||||||
|
self.assertFalse(any("stale" in name or "/build/" in name for name in zipped))
|
||||||
|
|
||||||
|
def test_existing_archives_are_not_overwritten(self):
|
||||||
|
PACKER.package(self.root, self.VERSION, self.output)
|
||||||
|
original_archives = {
|
||||||
|
path.name: path.read_bytes() for path in self.output.iterdir()
|
||||||
|
}
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(ValueError, "Refusing to overwrite"):
|
||||||
|
PACKER.package(self.root, self.VERSION, self.output)
|
||||||
|
|
||||||
|
self.assertEqual(original_archives, {
|
||||||
|
path.name: path.read_bytes() for path in self.output.iterdir()
|
||||||
|
})
|
||||||
|
|
||||||
|
# Validation: release identity, source checksums and generated checksums.
|
||||||
|
def test_wrong_version_is_rejected(self):
|
||||||
|
with self.assertRaisesRegex(ValueError, "must match the locked Gitea tag"):
|
||||||
PACKER.collect(self.root, "v28.1.0")
|
PACKER.collect(self.root, "v28.1.0")
|
||||||
|
|
||||||
|
def test_changed_source_is_rejected(self):
|
||||||
self.write("themes/demo/theme.css", b"changed")
|
self.write("themes/demo/theme.css", b"changed")
|
||||||
with self.assertRaisesRegex(ValueError, "Source changed"):
|
with self.assertRaisesRegex(ValueError, "Source changed"):
|
||||||
PACKER.collect(self.root, self.tag)
|
PACKER.collect(self.root, self.VERSION)
|
||||||
|
|
||||||
def test_refuses_missing_language_bundle(self):
|
def test_missing_language_bundle_is_rejected(self):
|
||||||
(self.root / "preview/rendered/en-US.js").unlink()
|
(self.root / "preview/rendered/en-US.js").unlink()
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaisesRegex(ValueError, "Expected regular file"):
|
||||||
PACKER.collect(self.root, self.tag)
|
PACKER.collect(self.root, self.VERSION)
|
||||||
|
|
||||||
def test_refuses_modified_generated_files(self):
|
def test_modified_generated_files_are_rejected(self):
|
||||||
self.write("build/themes/demo/mail/base/head.tmpl", b"modified")
|
self.write("build/themes/demo/mail/base/head.tmpl", b"modified")
|
||||||
with self.assertRaisesRegex(ValueError, "checksum mismatch"):
|
with self.assertRaisesRegex(ValueError, "checksum mismatch"):
|
||||||
PACKER.collect(self.root, self.tag)
|
PACKER.collect(self.root, self.VERSION)
|
||||||
|
|
||||||
def test_includes_nested_documentation_and_release_note_sources(self):
|
# Documentation: recursive inclusion and optional root files.
|
||||||
|
def test_nested_documentation_and_release_notes_are_included(self):
|
||||||
documents = {
|
documents = {
|
||||||
"docs/INDEX.zh-CN.md": "中文索引".encode(),
|
"docs/INDEX.zh-CN.md": "中文索引".encode("utf-8"),
|
||||||
"docs/images/README.zh-CN.md": "截图指南".encode(),
|
"docs/images/README.zh-CN.md": "截图指南".encode("utf-8"),
|
||||||
"docs/release-notes/v28.0.0.zh-CN.md": "发布说明".encode(),
|
"docs/release-notes/v28.0.0.zh-CN.md": "发布说明".encode("utf-8"),
|
||||||
".github/release-notes/v28.0.0.md": b"English release notes",
|
".github/release-notes/v28.0.0.md": b"English release notes",
|
||||||
}
|
}
|
||||||
for name, data in documents.items():
|
for name, data in documents.items():
|
||||||
self.write(name, data)
|
self.write(name, data)
|
||||||
self.write("docs/private.tmp", b"not documentation")
|
self.write("docs/private.tmp", b"not documentation")
|
||||||
files = PACKER.collect(self.root, self.tag)
|
|
||||||
|
files = PACKER.collect(self.root, self.VERSION)
|
||||||
|
|
||||||
for name, data in documents.items():
|
for name, data in documents.items():
|
||||||
self.assertEqual(files[name], data)
|
with self.subTest(document=name):
|
||||||
|
self.assertEqual(data, files[name])
|
||||||
self.assertIn("AGENTS.md", files)
|
self.assertIn("AGENTS.md", files)
|
||||||
self.assertNotIn("docs/private.tmp", files)
|
self.assertNotIn("docs/private.tmp", files)
|
||||||
|
|
||||||
def test_packages_without_removed_optional_root_documents(self):
|
def test_optional_root_documents_may_be_absent(self):
|
||||||
for name in ["AGENTS.md", "THIRD_PARTY_NOTICES.md"]:
|
for name in ("AGENTS.md", "THIRD_PARTY_NOTICES.md"):
|
||||||
(self.root / name).unlink()
|
(self.root / name).unlink()
|
||||||
files = PACKER.collect(self.root, self.tag)
|
|
||||||
|
files = PACKER.collect(self.root, self.VERSION)
|
||||||
|
|
||||||
self.assertNotIn("AGENTS.md", files)
|
self.assertNotIn("AGENTS.md", files)
|
||||||
self.assertNotIn("THIRD_PARTY_NOTICES.md", files)
|
self.assertNotIn("THIRD_PARTY_NOTICES.md", files)
|
||||||
self.assertIn("GITEA-LICENSE", files)
|
self.assertIn("GITEA-LICENSE", files)
|
||||||
|
|||||||
@@ -2,44 +2,17 @@ name: Release
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
|
||||||
tags: ['v*']
|
tags: ['v*']
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
validate:
|
validate:
|
||||||
name: Validate Templates
|
name: Validate
|
||||||
runs-on: linux-amd64-docker-small
|
uses: ./.github/workflows/validate.yml
|
||||||
steps:
|
permissions:
|
||||||
- uses: actions/checkout@v4
|
contents: read
|
||||||
|
|
||||||
- uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: '1.25.x'
|
|
||||||
|
|
||||||
- uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: '3.11'
|
|
||||||
|
|
||||||
- name: Test workflow scripts
|
|
||||||
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
|
|
||||||
|
|
||||||
package:
|
package:
|
||||||
name: Package & Release
|
name: Package
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
|
||||||
needs: validate
|
needs: validate
|
||||||
runs-on: linux-amd64-docker-small
|
runs-on: linux-amd64-docker-small
|
||||||
permissions:
|
permissions:
|
||||||
@@ -57,50 +30,44 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version: '3.11'
|
python-version: '3.11'
|
||||||
|
|
||||||
- name: Verify release tag matches the official snapshot
|
- name: Verify tag matches the snapshot
|
||||||
run: |
|
run: |
|
||||||
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"'
|
python - <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
- name: Verify release notes
|
with open("gitea.lock.json", encoding="utf-8") as lock_file:
|
||||||
run: test -s ".github/release-notes/${GITHUB_REF_NAME}.md"
|
lock = json.load(lock_file)
|
||||||
|
assert lock["tag"] == os.environ["GITHUB_REF_NAME"], (
|
||||||
|
"Release tag must match the pinned Gitea tag"
|
||||||
|
)
|
||||||
|
PY
|
||||||
|
|
||||||
- name: Generate static preview for the archive
|
- name: Generate preview
|
||||||
working-directory: tools
|
working-directory: tools
|
||||||
run: go run . preview all
|
run: go run . preview all
|
||||||
|
|
||||||
- name: Package generated installable themes and multilingual preview
|
- name: Package themes and preview
|
||||||
run: python -B .github/scripts/package_release.py --version "$TEMPLATE_RELEASE"
|
run: python -B .github/scripts/package_release.py --version "$TEMPLATE_RELEASE"
|
||||||
env:
|
env:
|
||||||
TEMPLATE_RELEASE: ${{ github.ref_name }}
|
TEMPLATE_RELEASE: ${{ github.ref_name }}
|
||||||
|
|
||||||
- name: Create Release
|
- name: Prepare release runtime
|
||||||
run: python -B .github/scripts/gitea_actions.py publish-release --version "$TEMPLATE_RELEASE"
|
uses: actions/setup-node@v4
|
||||||
env:
|
|
||||||
TEMPLATE_RELEASE: ${{ github.ref_name }}
|
|
||||||
GITEA_SERVER_URL: ${{ github.server_url }}
|
|
||||||
GITEA_REPOSITORY: ${{ github.repository }}
|
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
|
||||||
|
|
||||||
notify-release-docs:
|
|
||||||
name: Remind to Update Release Documentation
|
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
|
||||||
needs: package
|
|
||||||
concurrency:
|
|
||||||
group: release-documentation-${{ github.ref }}
|
|
||||||
cancel-in-progress: false
|
|
||||||
runs-on: linux-amd64-docker-small
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
issues: write
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/setup-python@v5
|
|
||||||
with:
|
with:
|
||||||
python-version: '3.11'
|
node-version: '22'
|
||||||
- name: Create documentation reminder Issue
|
|
||||||
run: python -B .github/scripts/gitea_actions.py create-issue --version "$TEMPLATE_RELEASE"
|
- name: Release
|
||||||
env:
|
uses: actions/gitea-release-action@v1.3.7
|
||||||
TEMPLATE_RELEASE: ${{ github.ref_name }}
|
with:
|
||||||
GITEA_SERVER_URL: ${{ github.server_url }}
|
server_url: ${{ github.server_url }}
|
||||||
GITEA_REPOSITORY: ${{ github.repository }}
|
repository: ${{ github.repository }}
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
token: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
name: ${{ github.ref_name }}
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
body: ''
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
files: |
|
||||||
|
dist/gitea-mail-templates-${{ github.ref_name }}.zip
|
||||||
|
dist/gitea-mail-templates-${{ github.ref_name }}.tar.gz
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
name: Validate
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['**']
|
||||||
|
pull_request:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate:
|
||||||
|
name: Validate
|
||||||
|
runs-on: linux-amd64-docker-small
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.25.x'
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Test workflow scripts
|
||||||
|
run: python -B -m unittest discover -s .github/scripts -p 'test_*.py'
|
||||||
|
|
||||||
|
- name: Verify snapshot
|
||||||
|
working-directory: tools
|
||||||
|
run: |
|
||||||
|
go run . upstream prepare
|
||||||
|
go run . upstream verify
|
||||||
|
|
||||||
|
- name: Tools test
|
||||||
|
working-directory: tools
|
||||||
|
run: go test ./...
|
||||||
|
|
||||||
|
- name: Build and render all themes and official languages
|
||||||
|
working-directory: tools
|
||||||
|
run: go run . preview all
|
||||||
+32
-131
@@ -1,23 +1,24 @@
|
|||||||
# Gitea Compatibility
|
# Gitea Compatibility
|
||||||
<!-- DOC-TAGS: {"TRACKER":["LATEST-VERIFIED","VERSION-MAP","HISTORY"]} -->
|
---
|
||||||
|
[简体中文](docs/COMPATIBILITY.zh-CN.md)
|
||||||
|
|
||||||
This document lists supported release combinations, known limitations and the checks used to assess compatibility.
|
This document describes the compatibility of Gitea Mail Templates with different Gitea versions.
|
||||||
|
|
||||||
[Project overview](README.md) · [Contributor guide](CONTRIBUTING.md) · [简体中文使用说明](docs/README.zh-CN.md)
|
|
||||||
|
|
||||||
## Compatibility Matrix
|
## Compatibility Matrix
|
||||||
|
|
||||||
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.
|
Select the recommended template release for the Gitea version you run. `[PASS]` identifies a combination recorded as compatible; `[PENDING]` means verification is incomplete.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Historical entries reflect the project's existing compatibility assessments. They do not mean that every patch version is retested for each release.
|
||||||
|
|
||||||
<!-- TRACKER:VERSION-MAP -->
|
|
||||||
| Gitea version | Recommended template release | Status | Notes |
|
| Gitea version | Recommended template release | Status | Notes |
|
||||||
|---------------|------------------------------|--------|-------|
|
|---|---|---|---|
|
||||||
| 28.1.0 | — | [PENDING] | Compatibility verification pending |
|
| 28.1.0 | **v28.1.0** | [PASS] | - |
|
||||||
| 28.0.0 | **v28.0.0** | [PASS] | Earlier template releases use the removed `FileSize` mail function |
|
| 28.0.0 | **v28.1.0** | [PASS] | Gitea replaced the mail function `FileSize` with `FormatByteSize` |
|
||||||
| 1.27.3 | **v1.27.3** | [PASS] | Push-to-PR commit links fixed in every theme |
|
| 1.27.3 | **v1.27.3** | [PASS] | Pull request push notification 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.2 | **v1.27.3** | [PASS] | The matching v1.27.2 template release has push notification defects 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.1 | **v1.27.3** | [PASS] | Older v1.0.x templates use obsolete push commit fields |
|
||||||
| 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.27.0 | — | [PENDING] | Gitea uses a new commit data structure; v1.27.3 may be compatible but has not been verified against this version |
|
||||||
| 1.26.4 | **v1.0.1** | [PASS] | — |
|
| 1.26.4 | **v1.0.1** | [PASS] | — |
|
||||||
| 1.26.3 | **v1.0.1** | [PASS] | — |
|
| 1.26.3 | **v1.0.1** | [PASS] | — |
|
||||||
| 1.26.2 | **v1.0.1** | [PASS] | — |
|
| 1.26.2 | **v1.0.1** | [PASS] | — |
|
||||||
@@ -25,141 +26,41 @@ Select a template release for the Gitea version you run. `[PASS]` records a comp
|
|||||||
| 1.26.0 | **v1.0.1** | [PASS] | — |
|
| 1.26.0 | **v1.0.1** | [PASS] | — |
|
||||||
| 1.25.5 | **v1.0.1** | [PASS] | — |
|
| 1.25.5 | **v1.0.1** | [PASS] | — |
|
||||||
| 1.25.0 | **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.
|
> [!NOTE]
|
||||||
|
> Gitea 1.24.x and earlier use a different mail template layout and are `[UNSUPPORTED]`; Gitea Mail Templates no longer provides separate support for them. The v1.0.0 template tag has been superseded by v1.0.1 and is no longer recommended separately.
|
||||||
<!-- 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
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# On your Gitea server:
|
|
||||||
gitea --version
|
|
||||||
# Or check the web UI footer / Site Administration → Monitoring
|
|
||||||
```
|
|
||||||
|
|
||||||
## Gitea Version History — Mail Template Impact
|
## Gitea Version History — Mail Template Impact
|
||||||
|
|
||||||
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.
|
This table describes changes in **Gitea**, not fixes in this template repository. “None” means that the Gitea version introduces no relevant mail template changes; recommended archives are listed in the compatibility matrix above.
|
||||||
|
|
||||||
<!-- TRACKER:HISTORY -->
|
| Gitea | Release date | Mail template changes | Impact |
|
||||||
| Gitea | Release Date | Mail Template Changes | Impact |
|
|---|---|---|---|
|
||||||
|-------|-------------|-----------------------|--------|
|
| **28.1.0** | 2026-10-06 | None | None |
|
||||||
| **28.1.0** | 2026-10-06 | [PENDING] Review of mail-template changes | TBD |
|
| **28.0.0** | 2026-09-29 | Mail function `FileSize` renamed to `FormatByteSize` ([PR](https://github.com/go-gitea/gitea/pull/39083)) | **Breaking change:** old release attachment templates fail |
|
||||||
| **28.0.0** | 2026-09-29 | `FileSize` replaced by `FormatByteSize`; mail template internals reorganized | **Breaking:** legacy release-attachment templates can fail |
|
| | | Introduced `templates/mail/base/` ([PR](https://github.com/go-gitea/gitea/pull/38396)) | **Breaking change:** older Gitea versions cannot directly use the new mail templates |
|
||||||
| **1.27.3** | 2026-08-29 | None | None |
|
| **1.27.3** | 2026-08-29 | None | None |
|
||||||
| **1.27.2** | 2026-08-14 | 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.1** | 2026-07-27 | Bundled push template updated to use `.UserCommit.GitCommit` ([PR](https://github.com/go-gitea/gitea/pull/38467)) | **Breaking change: an existing defect remains unresolved.** Old custom pull request push 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.27.0** | 2026-07-13 | Push commit data moved under `.UserCommit.GitCommit` ([ISSUE](https://github.com/go-gitea/gitea/issues/38469)) | **Breaking change:** old pull request push templates fail |
|
||||||
| **1.26.4** | 2026-06-21 | None | None |
|
| **1.26.4** | 2026-06-21 | None | None |
|
||||||
| **1.26.3** | 2026-06-20 | None | None |
|
| **1.26.3** | 2026-06-20 | None | None |
|
||||||
| **1.26.2** | 2026-05-20 | None | None |
|
| **1.26.2** | 2026-05-20 | None | None |
|
||||||
| **1.26.1** | 2026-04-22 | None | None |
|
| **1.26.1** | 2026-04-22 | None | None |
|
||||||
| **1.26.0** | 2026-04-19 | None | None |
|
| **1.26.0** | 2026-04-19 | None | None |
|
||||||
| **1.25.5** | 2026-03-10 | 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.25.0** | 2025 | Custom mail paths and subject/body format changed ([PR](https://github.com/go-gitea/gitea/pull/35150)) | **Breaking change:** older template layouts are unsupported |
|
||||||
| **≤ 1.24.x** | — | Legacy custom-mail layout | [UNSUPPORTED] |
|
| **≤ 1.24.x** | — | Legacy custom mail layout | [UNSUPPORTED] |
|
||||||
<!-- /TRACKER:HISTORY -->
|
|
||||||
|
|
||||||
## Template Variable Reference
|
## Template Variable Reference
|
||||||
|
|
||||||
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.
|
Official templates define the context for each mail type. For specific mail templates, functions and variables used during development, refer to the official mail template documentation [1](#notes).
|
||||||
|
|
||||||
### Relevant Template Functions
|
## Translation Keys
|
||||||
|
|
||||||
| Function | Gitea version | Notes |
|
Official templates reference `mail.*` and `actions.runs.attempt`. AST-based checks cover `.locale.Tr`, `$.locale.Tr` and both plural keys in `TrN`, including nested pipelines. Every referenced key must exist in the locked English catalog.
|
||||||
|----------|---------------|-------|
|
|
||||||
| `AppName` | ≤ 1.21 | Application name |
|
|
||||||
| `AppUrl` | ≤ 1.21 | Application base URL |
|
|
||||||
| `AppDomain` | ≤ 1.21 | Server domain |
|
|
||||||
| `DotEscape` | ≤ 1.21 | Prevents auto-linking of dotted text |
|
|
||||||
| `QueryEscape` | ≤ 1.21 | URL query encoding |
|
|
||||||
| `PathEscapeSegments` | ≤ 1.21 | Per-segment path encoding |
|
|
||||||
| `ShortSha` | ≤ 1.21 | Truncated commit hash |
|
|
||||||
| `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 |
|
|
||||||
| `Eval` | ≤ 1.21 | Evaluate template tokens |
|
|
||||||
| `StringUtils` | ≤ 1.21 | String manipulation helpers |
|
|
||||||
| `SliceUtils` | ≤ 1.21 | Slice manipulation helpers |
|
|
||||||
| `JsonUtils` | ≤ 1.21 | JSON helpers |
|
|
||||||
| `DumpVar` | ≤ 1.21 | Debug variable dump |
|
|
||||||
|
|
||||||
### Data Contexts by Template
|
---
|
||||||
|
### Notes
|
||||||
|
|
||||||
| Template | Key Variables |
|
[1](#notes): [Mail templates | Gitea Documentation](https://docs.gitea.com/administration/mail-templates/)
|
||||||
|----------|--------------|
|
|
||||||
| `user/auth/activate` | `DisplayName`, `Code`, `ActiveCodeLives` |
|
|
||||||
| `user/auth/activate_email` | `DisplayName`, `Code`, `Email`, `ActiveCodeLives` |
|
|
||||||
| `user/auth/register_notify` | `DisplayName`, `Username` |
|
|
||||||
| `user/auth/reset_passwd` | `DisplayName`, `Code`, `ResetPwdCodeLives` |
|
|
||||||
| `org/team_invite` | `Inviter`, `Team`, `Organization`, `InviteURL`, `Invite` |
|
|
||||||
| `repo/collaborator` | `Subject`, `RepoName`, `Link` |
|
|
||||||
| `repo/transfer` | `Subject`, `Repo`, `Link` |
|
|
||||||
| `repo/release` | `Release` (with `Publisher`, `TagName`, `Title`, `RenderedNote`, `Attachments`), `Link` |
|
|
||||||
| `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 available in the authentication mail contexts. Collaborator, transfer, release, workflow, assignment and issue-update templates use the context-specific fields listed above.
|
|
||||||
|
|
||||||
### Translation Keys
|
|
||||||
|
|
||||||
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. **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.
|
|
||||||
|
|
||||||
## Version Tracking
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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.
|
|
||||||
+286
-141
@@ -1,12 +1,13 @@
|
|||||||
# Contributing to Gitea Mail Templates
|
# Contributing
|
||||||
|
---
|
||||||
|
|
||||||
[简体中文](docs/CONTRIBUTING.zh-CN.md) · [Project overview](README.md) · [Compatibility](COMPATIBILITY.md)
|
[简体中文](docs/CONTRIBUTING.zh-CN.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).
|
Contributions to themes, tools, documentation and translations are welcome. This guide covers the development environment, command-line tools and contribution workflow. For template installation and release selection, see the [README](README.md#installation) and [compatibility matrix](COMPATIBILITY.md#compatibility-matrix).
|
||||||
|
|
||||||
## Local Development
|
## Local Development
|
||||||
|
|
||||||
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:
|
Use **Go 1.24 or later**. From the repository root, prepare the dependencies and locked official files, run the tests and generate the preview:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd tools
|
cd tools
|
||||||
@@ -16,35 +17,123 @@ go test ./...
|
|||||||
go run . preview all
|
go run . preview all
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
Then open [preview/index.html](preview/index.html) in a browser, or run `go run . dev` for live preview. The validation and packaging workflows use Python 3.11.
|
||||||
|
|
||||||
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.
|
The project is organized into a shared framework, theme styles and build tools. Official templates provide notification content, conditions, subjects and links. The shared framework handles presentation, while theme metadata and CSS select layouts and define styles. See the [README](README.md#project-directory-structure) for the directory layout.
|
||||||
|
|
||||||
### Common Commands
|
> [!NOTE]
|
||||||
|
> Downloaded official files, generated templates and preview data are not tracked in version control. Submit the relevant source files, tests and documentation; do not include `build/`, `dist/` or generated preview data.
|
||||||
|
|
||||||
Run these commands from `tools/`. Place flags before positional arguments.
|
## Development Tools
|
||||||
|
|
||||||
| Command | Result |
|
The CLI prepares official files, manages themes, builds templates and generates previews.
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> Run all commands below from `tools/`. Place options before positional arguments. In the examples, `my-theme` is a theme name; quote paths containing spaces.
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `go run . list` | List available themes |
|
| `upstream` | Prepare, synchronize or verify locked official build inputs |
|
||||||
| `go run . create my-theme` | Create theme metadata and CSS |
|
| `build` | Generate installable themes from official inputs and shared controls |
|
||||||
| `go run . build my-theme` | Generate one theme's installable templates |
|
| `list` | List available themes and their metadata |
|
||||||
| `go run . build all` | Generate every theme |
|
| `create` | Create theme CSS and metadata for the shared framework |
|
||||||
| `go run . preview all` | Build every theme and render all official languages |
|
| `delete` | Delete one or more theme source directories |
|
||||||
| `go run . dev` | Start the development server on port 3456 |
|
| `preview` | Pre-render mail templates to generate preview and test data |
|
||||||
| `go run . dev --port 3457` | Use a different local port |
|
| `dev` | Start a live-reload development server with SSE browser refresh |
|
||||||
| `go run . delete my-theme` | Remove the named theme's source directory |
|
| `help`, `h` | Show the command list or help for a specific command |
|
||||||
|
|
||||||
`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.
|
### upstream
|
||||||
|
|
||||||
## Adding a Theme
|
`upstream` manages the official files pinned by `gitea.lock.json`, including mail templates, locale files, the icon, license and reviewed adapter reference sources. Downloaded files are stored in `build/upstream/`.
|
||||||
|
|
||||||
1. Run `go run . create my-theme` from `tools/`.
|
| Subcommand | Behavior |
|
||||||
2. Edit `themes/my-theme/theme.json` and `theme.css`.
|
|---|---|
|
||||||
3. Run `go test ./...` and `go run . preview all` from `tools/`.
|
| `prepare` | Read the root lock file. Download and verify inputs if the cache is absent, or verify an existing cache before use. Does not modify the root lock file. |
|
||||||
4. Check desktop and mobile layouts in English, Simplified Chinese and at least one other official language. Include screenshots with the pull request.
|
| `sync --tag <TAG>` | Download the specified stable version's official files, verify them, then replace the cache and update the root lock file. |
|
||||||
|
| `verify` | Verify the existing cache against the root lock file, offline and read-only. |
|
||||||
|
|
||||||
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`.
|
To prepare inputs for the first time or check them independently, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . upstream prepare
|
||||||
|
go run . upstream verify
|
||||||
|
```
|
||||||
|
|
||||||
|
All three subcommands support `--root`, which defaults to `..` relative to the current working directory. For example:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
go run . upstream verify --root "C:\Projects\GiteaMailTemplates"
|
||||||
|
```
|
||||||
|
|
||||||
|
The root lock file records the Gitea tag, its commit and file SHA-256 hashes. `verify` checks file hashes, reviewed adapter reference sources and English translation keys referenced by official templates. Missing keys in other languages produce warnings and fall back to English in the preview. Building and rendering also check framework translation keys and action-link anchors; cache verification does not replace those checks.
|
||||||
|
|
||||||
|
#### Updating the Official Version
|
||||||
|
|
||||||
|
Use `sync` only when reviewing a new official version. For example, to explicitly synchronize to `v28.1.0`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . upstream sync --tag v28.1.0
|
||||||
|
go run . upstream verify
|
||||||
|
go test ./...
|
||||||
|
go run . preview all
|
||||||
|
```
|
||||||
|
|
||||||
|
Tags must use the `vX.Y.Z` format and identify a stable release of Gitea 28 or later. `latest`, prerelease tags and older version tags are not supported. `sync` resolves tags through `api.github.com` and downloads files from `raw.githubusercontent.com`; it does not accept a local Gitea clone or an authentication token option.
|
||||||
|
|
||||||
|
After synchronization, review the lock file diff, adjust adapter logic and preview data as needed, and update compatibility records after validation. Synchronization does not automatically update documentation, release tags or compatibility conclusions.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> When adapter reference sources change, review the affected behavior before updating their hashes. Do not change hashes simply to bypass the checks.
|
||||||
|
|
||||||
|
Network or validation errors before cache replacement leave existing files unchanged. Cache replacement and root lock file writing are separate operations; if a filesystem error interrupts the process, check that both agree before retrying. Stop `dev` before updating inputs to avoid concurrent processes writing to the cache.
|
||||||
|
|
||||||
|
### build
|
||||||
|
|
||||||
|
`build` generates installable mail templates from locked official files, the shared framework and theme sources. Specify one or more themes, or use `all` to build every theme:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . build aurora
|
||||||
|
go run . build aurora bloom
|
||||||
|
go run . build all
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `--folder` | `../themes` | Theme source directory; its parent is used as the project root |
|
||||||
|
|
||||||
|
When using another project path, that project's root must also provide the lock file and shared framework:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
go run . build --folder "C:\Projects\GiteaMailTemplates\themes" aurora
|
||||||
|
```
|
||||||
|
|
||||||
|
Output is written to `build/themes/<name>/mail/`, with build provenance and generated file hashes recorded in the adjacent `build.json`. Rebuilding updates the selected themes and removes their obsolete templates; it does not clean unselected themes. Building does not deploy templates. See the [README](README.md#install-a-theme) for installation.
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> To change templates, edit theme sources, the shared framework or build adapter logic. Direct edits to generated `.tmpl` files are overwritten by subsequent builds and cause packaging provenance checks to fail.
|
||||||
|
|
||||||
|
### list
|
||||||
|
|
||||||
|
`list` displays theme names, modes and short descriptions, sorted by name:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . list
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `--folder` to select a theme directory; the default is `../themes`. The command validates theme metadata and styles without downloading official files or generating templates. Fix any reported theme-format errors first.
|
||||||
|
|
||||||
|
### create
|
||||||
|
|
||||||
|
`create` creates `theme.json` and `theme.css` for a new theme and supports creating multiple themes at once:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . create my-theme
|
||||||
|
go run . create first-theme second-theme
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `--folder` to select the theme source directory; the default is `../themes`. The command does not overwrite existing directories. Theme names start with a lowercase letter and contain only lowercase letters, digits and hyphens. Avoid `all`, because build and preview commands treat it as the selector for every theme.
|
||||||
|
|
||||||
|
Edit the generated metadata and CSS to describe and style the theme. For example:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -55,76 +144,128 @@ Theme names start with a lowercase letter and may contain lowercase letters, dig
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
| Mode | Generated presentation |
|
`name` must match the directory name. Theme directories contain only these two source files, without copies of official mail templates or translations.
|
||||||
|
|
||||||
|
| Mode | Presentation structure |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `framed` | Shared controls and a framework layout, styled with the theme's CSS. New themes use this mode with the `standard` layout. |
|
| `framed` | Shared controls and a layout, styled with theme CSS. New themes default to this mode and the `standard` layout. |
|
||||||
| `shared` | CSS in the official head partial and the official footer partial; no additional framework controls or per-mail overrides. |
|
| `shared` | CSS is added to the official head partial, and the official footer partial is retained. No framework controls or per-mail template overrides are added. |
|
||||||
|
|
||||||
### Shared Framework
|
A `framed` theme can use `layout` to select a preset from `framework/layouts/`. Headers, action buttons, fallback links, sidebars and footers live in `framework/mail/base/`. Structural changes shared by multiple themes belong in the framework; adaptation between official content and controls is handled in `tools/builder/framework.go`.
|
||||||
|
|
||||||
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.
|
Theme CSS uses existing classes such as `email-heading`, `email-text` and `email-subtext` to distinguish headings, body text and supporting text. Preserve official notification wording, translation keys, variables and functional links. Do not generate wording or hide notification content through CSS. The style validator rejects template expressions, external resource references and unsupported hiding rules.
|
||||||
|
|
||||||
`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.
|
After completing the theme, run the tests and generate a preview:
|
||||||
|
|
||||||
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
|
```bash
|
||||||
cd tools
|
|
||||||
go run . upstream sync --tag v28.0.0
|
|
||||||
go run . upstream verify
|
|
||||||
go test ./...
|
go test ./...
|
||||||
|
go run . preview my-theme
|
||||||
|
```
|
||||||
|
|
||||||
|
### delete
|
||||||
|
|
||||||
|
`delete` removes the source directories of one or more named themes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . delete my-theme
|
||||||
|
go run . delete first-theme second-theme
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `--folder` to select the theme directory; the default is `../themes`.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> After validation, the command deletes directories directly, without a confirmation prompt or recovery through the recycle bin. Check the names and preserve any changes you need before running it.
|
||||||
|
|
||||||
|
Missing themes produce warnings; invalid themes and symlink directories are rejected. Themes are processed sequentially, so a later failure does not restore directories already deleted. `all` does not mean deleting every theme.
|
||||||
|
|
||||||
|
Deleting sources does not remove previously generated builds, preview data or deployed templates. Run `preview all` again to update the preview manifest. Remove deployed templates separately at their actual installation location.
|
||||||
|
|
||||||
|
### preview
|
||||||
|
|
||||||
|
`preview` builds the selected themes and renders all locked official languages using sample data. Specify one or more themes, or use `all`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . preview aurora
|
||||||
|
go run . preview aurora bloom
|
||||||
go run . preview all
|
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.
|
| Option | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `--folder` | `../themes` | Theme source directory; its parent is used as the project root |
|
||||||
|
| `--config` | `./data/templates_config.json` | Mail type descriptions and preview sample data |
|
||||||
|
| `--output` | `preview/rendered.js` under the project root | Preview manifest path |
|
||||||
|
|
||||||
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.
|
Default output includes `preview/rendered.js` and `preview/rendered/<locale>.js`. Open [preview/index.html](preview/index.html) in a browser; no server is required. See the [README](README.md#controls-and-keyboard-shortcuts) for controls and shortcuts.
|
||||||
|
|
||||||
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.
|
> [!IMPORTANT]
|
||||||
|
> Each run overwrites the preview manifest at the output location instead of merging with previously selected themes. For example, after `preview aurora`, the manifest contains only Aurora. Run `preview all` to restore every theme.
|
||||||
|
|
||||||
### Cache Recovery
|
To use a custom output path:
|
||||||
|
|
||||||
`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.
|
```bash
|
||||||
|
go run . preview --config ./data/templates_config.json --output ../build/preview-review/rendered.js all
|
||||||
|
```
|
||||||
|
|
||||||
For example, in PowerShell **from the repository root**, with an unused backup name:
|
> [!NOTE]
|
||||||
|
> Language data is written to a `rendered/` directory next to the output file. The command does not copy the preview interface or change its script references; the existing `preview/index.html` still reads data from the default location.
|
||||||
|
|
||||||
|
When adding mail types or rendering branches, extend the descriptions and sample contexts in `tools/data/templates_config.json`. Keep integer values, such as counts and sizes, as integers in JSON. Use controlled test content for HTML fields such as body text, release notes and review content; do not include personal information or real credentials.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> The preview checks template structure and presentation using sample contexts. It does not replace parameter compatibility review for the target Gitea version. Browsers and mail clients also differ in their CSS support.
|
||||||
|
|
||||||
|
### dev
|
||||||
|
|
||||||
|
`dev` starts a local development server, building every theme and preparing preview data on startup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://127.0.0.1:3456](http://127.0.0.1:3456). The default port is `3456`; use `--port` to change it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . dev --port 3457
|
||||||
|
```
|
||||||
|
|
||||||
|
The server listens only on `127.0.0.1`. Press `Ctrl + C` in the terminal to stop it. The command locates the project relative to the current `tools/` working directory and does not provide `--folder`, `--config` or `--root` options.
|
||||||
|
|
||||||
|
The server watches theme sources, the shared framework, the root lock file, official cache and preview data in `tools/data/`. Changes trigger a rebuild and refresh the preview through server-sent events (SSE). If a build fails, resolve the terminal error and save the file again.
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> Refresh the page manually after editing `preview/index.html`. Stop and restart the server after editing Go tool code. These files are outside the current automatic rebuild watch scope.
|
||||||
|
|
||||||
|
### help, h
|
||||||
|
|
||||||
|
View all commands or help for a specific command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . help
|
||||||
|
go run . h
|
||||||
|
go run . help build
|
||||||
|
go run . preview --help
|
||||||
|
```
|
||||||
|
|
||||||
|
For `upstream` subcommand options, place `--help` after the subcommand:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . upstream prepare --help
|
||||||
|
go run . upstream sync --help
|
||||||
|
go run . upstream verify --help
|
||||||
|
```
|
||||||
|
|
||||||
|
Tool diagnostics use the `[LEVEL] [NAME] Message` format. `[I]`, `[W]`, `[E]` and `[F]` mean information, warning, error and fatal error, respectively. Level markers use different colors in supported terminals; set `NO_COLOR` to disable colors. Failed commands return a nonzero exit code. Preserve the complete output when troubleshooting.
|
||||||
|
|
||||||
|
## Frequently Asked Questions
|
||||||
|
|
||||||
|
### What if the lock file or official cache is missing?
|
||||||
|
|
||||||
|
If an existing clone lacks `gitea.lock.json`, restore the tracked root lock file. The copy at `build/upstream/lock.json` cannot replace it. If only the cache is missing, run `upstream prepare` rather than synchronizing to another version to recover it. `build`, `preview` and `dev` prepare official inputs automatically. They can run offline once the cache, Go toolchain and dependencies are available; `upstream verify` always checks local files only. For network errors or GitHub API rate limits during the initial download, resolve the network issue and retry.
|
||||||
|
|
||||||
|
### What if the cache is corrupt or does not match the lock file?
|
||||||
|
|
||||||
|
`prepare` does not repair corrupt caches automatically, and `sync` refuses to replace a non-empty invalid snapshot. Stop the development server, preserve the root lock file and move the cache to a backup location before preparing it again. For example, in PowerShell **from the repository root**, use a backup name that does not already exist:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
Move-Item -LiteralPath .\build\upstream -Destination .\build\upstream.backup
|
Move-Item -LiteralPath .\build\upstream -Destination .\build\upstream.backup
|
||||||
@@ -133,87 +274,91 @@ go run . upstream prepare
|
|||||||
go run . upstream verify
|
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.
|
> [!WARNING]
|
||||||
|
> Do not edit official cached files directly or delete the rest of `build/`. To revert the official version, restore the previously reviewed root lock file and rebuild the cache in the same way.
|
||||||
|
|
||||||
### Known Upstream Formatting Issue
|
### Why does synchronizing a new version report changed adapter sources?
|
||||||
|
|
||||||
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.
|
The project pins reviewed mail-rendering and translation reference sources. When upstream changes these implementations, the tool requires another review to avoid using incompatible preview functions or variable structures. Examine the official changes, adjust adapter logic and relevant tests, then update the reference hashes. New mail types, translation keys, functions or variable paths may also require changes. Update [compatibility records](COMPATIBILITY.md) after validation; a successful download does not establish compatibility.
|
||||||
|
|
||||||
## Verification and Release
|
### Why do some languages use English or produce formatting warnings?
|
||||||
|
|
||||||
### Checks for a Pull Request
|
Official English must contain all referenced translation keys. Missing keys in other languages fall back to English and produce warnings. Preserve official locale files without adding translations to the cache.
|
||||||
|
|
||||||
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 example, Gitea v28.0.0 has a known placeholder defect in the Polish `mail.team_invite.text_1`. The preview preserves official output for the exact reviewed source text and reports a warning. This exception does not apply to other formatting errors. Include the version, translation key and reproduction details when reporting new issues for review and upstream feedback.
|
||||||
|
|
||||||
For documentation, tracker or packaging changes, run from the repository root:
|
### Why does the preview not show a new theme or recent changes?
|
||||||
|
|
||||||
|
The static preview does not build automatically. Run `preview all` again and refresh the page, then confirm that the manifest contains the required themes. With a custom output path, check where the preview interface actually reads its data. With `dev`, first check the terminal for rebuild errors. Go code changes require a server restart; preview interface changes require a manual refresh. If the default port is occupied, select another port with `--port`.
|
||||||
|
|
||||||
|
### Does a successful build mean all Gitea versions are supported?
|
||||||
|
|
||||||
|
Builds target the official inputs specified in the root lock file. Other versions may change template directories, functions or variable structures, so a successful build or preview alone cannot establish backward compatibility. Select release archives using the [compatibility matrix](COMPATIBILITY.md#compatibility-matrix), rather than installing new-version templates directly on older instances.
|
||||||
|
|
||||||
|
### Why does the packaging script reject the current output?
|
||||||
|
|
||||||
|
Run `preview all` before packaging to ensure that every theme and language bundle is present and generated files match the current sources. The version argument must match the tag in the root lock file. Rebuild and regenerate previews after source or generated file changes. The script does not overwrite existing release archives. Use a new output directory when repackaging and retain old artifacts needed for comparison.
|
||||||
|
|
||||||
|
## Other Contribution Information
|
||||||
|
|
||||||
|
### Themes and the Shared Framework
|
||||||
|
|
||||||
|
Use CSS suitable for mail clients. When editing an existing theme, preserve its palette, fonts, borders and layout, keep headings, body text and supporting text distinct, and account for long text, long URLs and mobile widths. Layout fragments use presentation markup and support `table`, `tbody`, `tr`, `td`, `div`, `span` and attributes allowed by the validator. Combined markup must have balanced tags. `__HEADER__` inserts the shared header, `__MAIL_TYPE__` expands to the mail ID, and `__SIDEBAR__` inserts the shared sidebar in footer fragments. Review changes to notification variables and conditions in the build adapter logic, not in theme CSS or layout presets.
|
||||||
|
|
||||||
|
For new themes, update the galleries in both English and Chinese READMEs. Use PNG images of at most 50 KiB each; 10–20 KiB is preferred. Follow the [capture guide](docs/images/README.md) for consistent themes, mail types, languages and cropping.
|
||||||
|
|
||||||
|
### Commits and Pull Requests
|
||||||
|
|
||||||
|
Keep changes focused on a specific problem and avoid unrelated formatting changes. Describe the reason, scope and validation results in the pull request. For visual changes, you can include comparison images generated with sample data. When changing themes, the shared framework or Go tools, run from `tools/`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./...
|
||||||
|
go run . preview all
|
||||||
|
```
|
||||||
|
|
||||||
|
For changes to mail content, links, variables or function adaptation, add regression tests covering affected conditional branches. Existing tests check generated output, translation keys, notification subjects, text and links across every theme and language, including push, review, reply, workflow and attachment branches.
|
||||||
|
|
||||||
|
When changing packaging scripts, run from the repository root:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -B -m unittest discover -s .github/scripts -p 'test_*.py'
|
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.
|
For documentation-only changes, check wording, command examples and links. Keep Chinese and English documentation aligned; preserve commands, paths, variables and version information without changing their meaning in translation.
|
||||||
|
|
||||||
### Browser and Gitea Checks
|
Use concise commit messages. The following prefixes are available as a reference:
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
| Prefix | Scope |
|
| Prefix | Scope |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `style(<name>):` | Theme presentation |
|
| `style(<name>):` | Theme presentation styles |
|
||||||
| `preview:` | Browser preview |
|
| `preview:` | Preview tools |
|
||||||
| `tools:` | CLI, build and snapshot tools |
|
| `tools:` | CLI, build and snapshot tools |
|
||||||
| `docs:` | Documentation and translations |
|
| `docs:` | Documentation and translations |
|
||||||
| `fix:` | Bug fixes |
|
| `fix:` | Bug fixes |
|
||||||
| `project:` | Repository configuration and project structure |
|
| `project:` | Repository configuration and project structure |
|
||||||
| `refactor:` | Code restructuring |
|
| `refactor:` | Code restructuring |
|
||||||
| `chore:` | Maintenance |
|
| `chore:` | Routine maintenance |
|
||||||
|
|
||||||
## Translations and License
|
### Version and Release Maintenance
|
||||||
|
|
||||||
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).
|
Release tags must match the locked Gitea tag. Version labels, badges and compatibility records are maintained manually; changing official inputs does not trigger automatic version maintenance. Retain historical release assets and check documentation and compatibility records before tagging. After validation and generation of all previews, you can package manually from the repository root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python .github/scripts/package_release.py --version v28.1.0 --output build/release-review
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the example version with the tag in the root lock file and use an output directory without release archives of the same name. The script checks source and artifact hashes, packages themes, previews, documentation, the official license and provenance, and excludes stale theme builds.
|
||||||
|
|
||||||
|
`.github/workflows/validate.yml` handles checks for ordinary pushes and pull requests and is also called by the Release workflow. After validation, Release packages the archives and uploads them through the release action. It uses the tag as the title and creates new releases with an empty body, without requiring a release notes file. Republishing an existing tag preserves its existing body and replaces same-name attachments. Check the release and attachment state before retrying a failed publication.
|
||||||
|
|
||||||
|
Workflows use the `linux-amd64-docker-small` runner label, with setup steps preparing Go, Python and Node.js for publication. Actions use `actions/...@version` references, resolved through the runner's action source configuration; consider the current `github_mirror` setting when maintaining them. Publication uses the built-in `GITEA_TOKEN`, and the repository must permit release writes.
|
||||||
|
|
||||||
|
### Reporting Problems
|
||||||
|
|
||||||
|
Include the Gitea version, template release or source commit, theme, mail type, language and steps to reproduce. For build or preview issues, also include the tag in the root lock file, relevant commands and complete error output. For display issues, specify the mail client and attach screenshots with personal information removed.
|
||||||
|
|
||||||
|
For improvement suggestions, explain the use case, expected result and affected themes or tools. For official template or translation defects, preserve the original inputs and provide a minimal example that reproduces the issue.
|
||||||
|
|
||||||
|
### License
|
||||||
|
|
||||||
|
This project uses the [MIT License](LICENSE). Contribute code, styles and images that can be legally redistributed. When generating or distributing derived templates, retain Gitea copyright, the official license and snapshot provenance; release archives already include these files.
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
Gitea Mail Template offers a range of email template styles for self-hosted [Gitea](https://about.gitea.com) instances.
|
Gitea Mail Template offers a range of email template styles for self-hosted [Gitea](https://about.gitea.com) instances.
|
||||||
|
|
||||||
> Latest release: [v28.0.0](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/latest)
|
> Latest release: [v28.1.0](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/latest)
|
||||||
|
|
||||||
The repository includes several themes for Gitea emails, suitable for most deployment scenarios and use cases. Before installing, consult the [compatibility matrix](COMPATIBILITY.md#compatibility-matrix) for your Gitea version and choose the corresponding release archive.
|
The repository includes several themes for Gitea emails, suitable for most deployment scenarios and use cases. Before installing, consult the [compatibility matrix](COMPATIBILITY.md#compatibility-matrix) for your Gitea version and choose the corresponding release archive.
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ The gallery shows how the email templates render in the preview tool.
|
|||||||
|  | **Terminal** | Dark background, monospace fonts and green accents |
|
|  | **Terminal** | Dark background, monospace fonts and green accents |
|
||||||
|  | **Terra** | Earth tones, terracotta buttons and serif fonts |
|
|  | **Terra** | Earth tones, terracotta buttons and serif fonts |
|
||||||
|
|
||||||
Screenshots show only the current default build. Use the [local preview](#preview) to view other email types and languages. See the [screenshot guide](docs/images/README.md) when updating screenshots.
|
Screenshots show only the current default build. Use the [preview](#preview) to view other email types and languages. See the [screenshot guide](docs/images/README.md) when updating screenshots.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ go run . build all
|
|||||||
cd ..
|
cd ..
|
||||||
```
|
```
|
||||||
|
|
||||||
Building usable templates requires resources from the official Gitea repository to ensure consistent source inputs. The command automatically downloads the official files pinned by `gitea.lock.json`. Subsequent builds use the cached resources without downloading them again. For missing files or a damaged cache, see [official snapshot updates](CONTRIBUTING.md#official-snapshot-updates).
|
Building usable templates requires resources from the official Gitea repository to ensure consistent source inputs. The command automatically downloads the official files pinned by `gitea.lock.json`. Subsequent builds use the cached resources without downloading them again.
|
||||||
|
|
||||||
### Install a Theme
|
### Install a Theme
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ go run . dev
|
|||||||
cd ..
|
cd ..
|
||||||
```
|
```
|
||||||
|
|
||||||
Open [http://127.0.0.1:3456](http://127.0.0.1:3456). The Go server watches theme files, the shared framework, the lock file and cache, and preview test data. When a file changes, it rebuilds the email templates and refreshes the page through server-sent events (SSE) to provide live reload.
|
Open [http://127.0.0.1:3456](http://127.0.0.1:3456). The server watches theme files, the shared framework, the lock file and cache, and preview test data. When a file changes, it rebuilds the email templates and refreshes the page through server-sent events (SSE) to provide live reload.
|
||||||
|
|
||||||
### Controls and Keyboard Shortcuts
|
### Controls and Keyboard Shortcuts
|
||||||
|
|
||||||
@@ -126,9 +126,9 @@ Open [http://127.0.0.1:3456](http://127.0.0.1:3456). The Go server watches theme
|
|||||||
|
|
||||||
## Compatibility
|
## Compatibility
|
||||||
|
|
||||||
- **Latest tested:** Gitea 28.0.0
|
- **Latest tested:** Gitea 28.1.0
|
||||||
- **Latest release:** [v28.0.0](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/latest)
|
- **Latest release:** [v28.1.0](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/latest)
|
||||||
- **Upstream Gitea 28.1.0:** [PENDING]
|
- **Upstream Gitea 28.1.0:** [PASS]
|
||||||
|
|
||||||
## Project Directory Structure
|
## Project Directory Structure
|
||||||
|
|
||||||
@@ -138,7 +138,6 @@ The project is organized around a shared framework, theme styles and build tools
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
.github/ # Workflows and maintenance scripts
|
.github/ # Workflows and maintenance scripts
|
||||||
release-notes/ # Release notes for each version
|
|
||||||
scripts/ # Documentation updates, packaging and Gitea release scripts
|
scripts/ # Documentation updates, packaging and Gitea release scripts
|
||||||
workflows/ # Validation and publication
|
workflows/ # Validation and publication
|
||||||
docs/ # Chinese documentation, documentation index and gallery images
|
docs/ # Chinese documentation, documentation index and gallery images
|
||||||
@@ -155,7 +154,6 @@ tools/ # Go CLI, build tools and tests
|
|||||||
preview/ # Email rendering, locale adaptation and development server
|
preview/ # Email rendering, locale adaptation and development server
|
||||||
upstream/ # Official snapshot downloads, verification and translation key discovery
|
upstream/ # Official snapshot downloads, verification and translation key discovery
|
||||||
gitea.lock.json # Official version tag, commit and file checksums
|
gitea.lock.json # Official version tag, commit and file checksums
|
||||||
README.md
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Official templates provide notification content, conditions, subject lines and links. The shared framework handles email presentation, while themes use metadata and CSS to select layouts and define styles. Theme directories do not contain official email source files or translations.
|
Official templates provide notification content, conditions, subject lines and links. The shared framework handles email presentation, while themes use metadata and CSS to select layouts and define styles. Theme directories do not contain official email source files or translations.
|
||||||
@@ -196,14 +194,13 @@ Gitea currently includes the following 11 email templates [1](#notes):
|
|||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributions to themes, tools, documentation and translations are welcome. The [contributor guide](CONTRIBUTING.md) covers local environment setup, design guidelines, required checks and the release process.
|
Contributions to themes, tools, documentation and translations are welcome. See the [contributor guide](CONTRIBUTING.md) for details on contributing.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under [MIT](LICENSE). Generated release archives retain Gitea's license and snapshot provenance.
|
This project is licensed under [MIT](LICENSE). Generated release archives retain Gitea's license and snapshot provenance.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Notes
|
### Notes
|
||||||
|
|
||||||
[1](#notes): [Mail templates | Gitea Documentation](https://docs.gitea.com/administration/mail-templates/)
|
[1](#notes): [Mail templates | Gitea Documentation](https://docs.gitea.com/administration/mail-templates/)
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Gitea 兼容性
|
||||||
|
---
|
||||||
|
[English](../COMPATIBILITY.md)
|
||||||
|
|
||||||
|
此文件描述了 Gitea Mail Templates 与 Gitea 不同版本之间的兼容性。
|
||||||
|
|
||||||
|
## 兼容性表
|
||||||
|
|
||||||
|
根据所运行的 Gitea 版本选择对应推荐的模板版本。`[PASS]` 表示已记录为兼容的组合,`[PENDING]` 表示尚未完成验证。
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> 历史条目仅反映项目已有的兼容性评估,不代表每次发布都重新测试了每一个补丁版本。
|
||||||
|
|
||||||
|
| Gitea 版本 | 推荐模板版本 | 状态 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 28.1.0 | **v28.1.0** | [PASS] | - |
|
||||||
|
| 28.0.0 | **v28.1.0** | [PASS] | Gitea 将邮件函数 `FileSize` 替换为 `FormatByteSize` |
|
||||||
|
| 1.27.3 | **v1.27.3** | [PASS] | 全部主题已修复拉取请求推送通知的提交链接 |
|
||||||
|
| 1.27.2 | **v1.27.3** | [PASS] | 同号 v1.27.2 模板的 Bloom、Ember、Heritage 存在推送通知缺陷 |
|
||||||
|
| 1.27.1 | **v1.27.3** | [PASS] | 较早 v1.0.x 模板使用过时的推送提交字段 |
|
||||||
|
| 1.27.0 | — | [PENDING] | Gitea 使用了新的提交数据结构;v1.27.3 可能适用,但未在此版本上验证 |
|
||||||
|
| 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] | — |
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Gitea 1.24.x 及更早版本使用不同的邮件模板布局,属于 `[UNSUPPORTED]`,Gitea Mail Templates 不再另外支持。v1.0.0 模板标签已由 v1.0.1 替代,不再单独推荐。
|
||||||
|
|
||||||
|
## Gitea 历版本对邮件模板影响
|
||||||
|
|
||||||
|
此表描述 **Gitea** 的变化,不是本模板仓库的修复记录。“无”表示该 Gitea 版本没有新增相关邮件模板影响;推荐归档见上方兼容性表。
|
||||||
|
|
||||||
|
| Gitea | 发布日期 | 邮件模板变化 | 影响 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **28.1.0** | 2026-10-06 | 无 | 无 |
|
||||||
|
| **28.0.0** | 2026-09-29 | 邮件函数 `FileSize` 改为 `FormatByteSize` ([PR](https://github.com/go-gitea/gitea/pull/#39083)) | **不兼容变更:** 旧的发布附件模板失效 |
|
||||||
|
|||引入 templates/mail/base/ ([PR](https://github.com/go-gitea/gitea/pull/#38396))| **不兼容变更:** 旧版本 Gitea 无法直接使用新版本邮件模板 |
|
||||||
|
| **1.27.3** | 2026-08-29 | 无 | 无 |
|
||||||
|
| **1.27.2** | 2026-08-14 | 无 | 无 |
|
||||||
|
| **1.27.1** | 2026-07-27 | 内置推送模板改用 `.UserCommit.GitCommit`([PR](https://github.com/go-gitea/gitea/pull/38467)) | **不兼容变更:存在既有未修复缺陷** 旧的自定义拉取请求推送模板可能失效 |
|
||||||
|
| **1.27.0** | 2026-07-13 | 推送提交数据移至 `.UserCommit.GitCommit`([ISSUE](https://github.com/go-gitea/gitea/issues/38469)) | **不兼容变更:** 旧的拉取请求推送模板失效 |
|
||||||
|
| **1.26.4** | 2026-06-21 | 无 | 无 |
|
||||||
|
| **1.26.3** | 2026-06-20 | 无 | 无 |
|
||||||
|
| **1.26.2** | 2026-05-20 | 无 | 无 |
|
||||||
|
| **1.26.1** | 2026-04-22 | 无 | 无 |
|
||||||
|
| **1.26.0** | 2026-04-19 | 无 | 无 |
|
||||||
|
| **1.25.5** | 2026-03-10 | 无 | 无 |
|
||||||
|
| **1.25.0** | 2025 | 自定义邮件路径及主题和正文格式变化([PR](https://github.com/go-gitea/gitea/pull/35150)) | **不兼容变更:** 不支持更早的模板布局 |
|
||||||
|
| **≤ 1.24.x** | — | 旧的自定义邮件布局 | [UNSUPPORTED] |
|
||||||
|
|
||||||
|
## 模板变量参考
|
||||||
|
|
||||||
|
各类邮件上下文由官方模板定义。开发时具体的邮件模板和函数和变量请参考官方邮件模板文档 [1](#注释)。
|
||||||
|
|
||||||
|
## 翻译键
|
||||||
|
|
||||||
|
官方模板引用 `mail.*` 和 `actions.runs.attempt`。基于 AST 的检查覆盖 `.locale.Tr`、`$.locale.Tr` 和 `TrN` 的两个复数键,包括嵌套管道。所有引用键必须存在于锁定的英文目录。
|
||||||
|
|
||||||
|
---
|
||||||
|
### 注释
|
||||||
|
|
||||||
|
[1](#注释): [Mail templates | Gitea Documentation](https://docs.gitea.com/administration/mail-templates/)
|
||||||
+281
-136
@@ -1,12 +1,13 @@
|
|||||||
# 贡献指南
|
# 贡献指南
|
||||||
|
---
|
||||||
|
|
||||||
[English](../CONTRIBUTING.md) · [项目概览](README.zh-CN.md) · [兼容性](../COMPATIBILITY.md)
|
[English](../CONTRIBUTING.md)
|
||||||
|
|
||||||
欢迎改进主题、工具、测试、文档和翻译。本指南介绍当前源码架构的开发流程;安装和版本选择请参阅 [README](README.zh-CN.md#安装)。
|
欢迎改进主题、工具、文档和翻译。本指南介绍开发环境、命令行工具和贡献流程。安装邮件模板和选择发行版本,请参阅 [README](README.zh-CN.md#安装)及[兼容性表](COMPATIBILITY.zh-CN.md#兼容性表)。
|
||||||
|
|
||||||
## 本地开发
|
## 本地开发
|
||||||
|
|
||||||
使用 **Go 1.24 或更高版本**。从仓库根目录开始,准备依赖和锁定的官方文件,再运行检查并生成预览:
|
使用 **Go 1.24 或更高版本**。在仓库根目录执行以下命令,准备依赖和锁定的官方文件,运行测试并生成预览:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd tools
|
cd tools
|
||||||
@@ -16,35 +17,123 @@ go test ./...
|
|||||||
go run . preview all
|
go run . preview all
|
||||||
```
|
```
|
||||||
|
|
||||||
在浏览器中打开 `preview/index.html`,或在 `tools/` 中运行 `go run . dev`,访问 [http://127.0.0.1:3456](http://127.0.0.1:3456) 使用实时预览。开发服务器监听主题、框架、锁文件与缓存、预览测试数据的变化。
|
随后在浏览器中打开 [preview/index.html](../preview/index.html),或运行 `go run . dev` 使用实时预览。检查与打包工作流使用 Python 3.11。
|
||||||
|
|
||||||
CLI 使用 urfave/cli 和 x/net HTML 解析器。文档与打包检查使用 Python 3.11 或更高版本;只有浏览器检查需要 Node.js 和 `tools/qa` 中的依赖。
|
项目按共享框架、主题样式和构建工具组织。官方模板提供通知内容、条件、主题行和链接;共享框架负责邮件呈现,主题通过元数据和 CSS 选择布局、定义样式。目录说明见 [README](README.zh-CN.md#项目目录结构)。
|
||||||
|
|
||||||
### 常用命令
|
> [!NOTE]
|
||||||
|
> 下载的官方文件、生成的模板和预览数据不纳入版本控制。贡献时提交相应源码、测试和文档,无需提交 `build/`、`dist/` 或生成的预览数据。
|
||||||
|
|
||||||
以下命令均在 `tools/` 中执行。带有位置参数时,将选项放在位置参数之前。
|
## 开发工具
|
||||||
|
|
||||||
|
命令行工具提供官方文件准备、主题管理、模板构建和预览功能。
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> 以下命令均在 `tools/` 中执行;带有位置参数时,将选项放在位置参数之前。示例中的 `my-theme` 表示主题名称,包含空格的路径需要加引号。
|
||||||
|
|
||||||
| 命令 | 用途 |
|
| 命令 | 用途 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `go run . list` | 列出可用主题 |
|
| `upstream` | 准备、同步或校验锁定的官方构建输入 |
|
||||||
| `go run . create my-theme` | 创建主题元数据和 CSS |
|
| `build` | 根据官方输入和共享控件生成可安装的主题 |
|
||||||
| `go run . build my-theme` | 生成指定主题的安装模板 |
|
| `list` | 列出可用主题及其元数据 |
|
||||||
| `go run . build all` | 生成全部主题 |
|
| `create` | 创建使用共享框架的主题 CSS 和元数据 |
|
||||||
| `go run . preview all` | 构建全部主题并渲染所有官方语言 |
|
| `delete` | 删除一个或多个主题源码目录 |
|
||||||
| `go run . dev` | 在端口 3456 启动开发服务器 |
|
| `preview` | 预渲染邮件模板,生成预览和测试数据 |
|
||||||
| `go run . dev --port 3457` | 指定其他本地端口 |
|
| `dev` | 启动通过 SSE 刷新页面的热重载开发服务器 |
|
||||||
| `go run . delete my-theme` | 删除指定主题的源码目录 |
|
| `help`、`h` | 显示命令列表或指定命令的帮助 |
|
||||||
|
|
||||||
`build` 将模板写入 `build/themes/<名称>/mail/`,并在 `build.json` 中记录文件和源码哈希。`preview` 同时完成构建,随后写入 `preview/rendered.js` 和 `preview/rendered/<语言>.js`。这些生成文件和下载的官方文件均被 Git 忽略,无需随贡献提交。
|
### upstream
|
||||||
|
|
||||||
## 添加主题
|
`upstream` 管理 `gitea.lock.json` 锁定的官方文件,包括邮件模板、语言文件、图标、许可证和已评审的适配参考源码。下载的文件保存到 `build/upstream/`。
|
||||||
|
|
||||||
1. 在 `tools/` 中运行 `go run . create my-theme`。
|
| 子命令 | 行为 |
|
||||||
2. 编辑 `themes/my-theme/theme.json` 和 `theme.css`。
|
|---|---|
|
||||||
3. 在 `tools/` 中运行 `go test ./...` 和 `go run . preview all`。
|
| `prepare` | 读取根锁文件。缓存缺失时下载并校验;已有缓存则校验后使用。不修改根锁文件。 |
|
||||||
4. 检查英文、简体中文及至少另一种官方语言的桌面与移动端布局,在拉取请求中附上截图。
|
| `sync --tag <TAG>` | 下载指定稳定版本的官方文件,校验后替换缓存并更新根锁文件。 |
|
||||||
|
| `verify` | 对照根锁文件,只读、离线校验已有缓存。 |
|
||||||
|
|
||||||
主题名称以小写字母开头,可包含小写字母、数字和连字符。主题目录只包含 `theme.json` 和 `theme.css`。
|
首次准备或单独检查输入时执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . upstream prepare
|
||||||
|
go run . upstream verify
|
||||||
|
```
|
||||||
|
|
||||||
|
三个子命令均支持 `--root`,默认值为相对当前工作目录的 `..`。例如:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
go run . upstream verify --root "C:\Projects\GiteaMailTemplates"
|
||||||
|
```
|
||||||
|
|
||||||
|
根锁文件记录 Gitea 标签、对应提交和文件的 SHA-256。`verify` 检查文件哈希、已评审的适配参考源码,以及官方模板引用的英文翻译键;非英文语言缺少的键会报告警告,并在预览中回退为英文。构建和渲染还会检查框架使用的翻译键及操作链接锚点,缓存校验不能代替这些检查。
|
||||||
|
|
||||||
|
#### 更新官方版本
|
||||||
|
|
||||||
|
只有需要评审新的官方版本时才使用 `sync`。例如,明确同步到 `v28.1.0`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . upstream sync --tag v28.1.0
|
||||||
|
go run . upstream verify
|
||||||
|
go test ./...
|
||||||
|
go run . preview all
|
||||||
|
```
|
||||||
|
|
||||||
|
标签必须采用 `vX.Y.Z` 格式,并且是 Gitea 28 或更高版本的稳定标签;不支持 `latest`、预发布标签或旧版本标签。`sync` 通过 `api.github.com` 解析标签,通过 `raw.githubusercontent.com` 下载文件,不接受本地 Gitea 克隆或认证令牌参数。
|
||||||
|
|
||||||
|
同步后检查锁文件差异,按需调整适配逻辑和预览数据,完成验证后更新兼容性记录。同步不会自动更新文档、发行标签或兼容性结论。
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> 适配参考源码发生变化时,须先评审相关行为,再更新对应参考哈希,不应仅修改哈希以跳过检查。
|
||||||
|
|
||||||
|
网络或校验错误发生在缓存替换前时,原有文件保持不变。缓存替换和根锁文件写入是两个操作;文件系统错误中断操作后,重试前应检查二者是否一致。更新输入前先停止 `dev`,避免多个进程同时写入缓存。
|
||||||
|
|
||||||
|
### build
|
||||||
|
|
||||||
|
`build` 根据锁定的官方文件、共享框架和主题源码生成可安装的邮件模板。必须指定一个或多个主题,或使用 `all` 构建全部主题:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . build aurora
|
||||||
|
go run . build aurora bloom
|
||||||
|
go run . build all
|
||||||
|
```
|
||||||
|
|
||||||
|
| 选项 | 默认值 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `--folder` | `../themes` | 主题源码目录;工具将其父目录作为项目根目录 |
|
||||||
|
|
||||||
|
使用其他项目路径时,该项目根目录也需要提供锁文件和共享框架:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
go run . build --folder "C:\Projects\GiteaMailTemplates\themes" aurora
|
||||||
|
```
|
||||||
|
|
||||||
|
结果写入 `build/themes/<名称>/mail/`,同级的 `build.json` 记录构建来源和生成文件的校验值。重新构建会更新所选主题的模板并清理其过时模板,不清理未选中的主题。构建不会部署邮件模板,安装步骤见 [README](README.zh-CN.md#安装主题)。
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> 需要修改模板时,应编辑主题源码、共享框架或构建适配逻辑。直接修改生成的 `.tmpl` 文件会在后续构建中被覆盖,也会导致打包时的来源校验失败。
|
||||||
|
|
||||||
|
### list
|
||||||
|
|
||||||
|
`list` 按名称排序,显示主题名称、模式和简短说明:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . list
|
||||||
|
```
|
||||||
|
|
||||||
|
可通过 `--folder` 指定主题目录,默认值为 `../themes`。命令会校验主题元数据和样式,不下载官方文件,也不生成模板。主题格式无效时,应先修复报告的问题。
|
||||||
|
|
||||||
|
### create
|
||||||
|
|
||||||
|
`create` 为新主题创建 `theme.json` 和 `theme.css`,支持一次创建多个主题:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . create my-theme
|
||||||
|
go run . create first-theme second-theme
|
||||||
|
```
|
||||||
|
|
||||||
|
可通过 `--folder` 指定主题源码目录,默认值为 `../themes`。命令不会覆盖已存在的目录。主题名称以小写字母开头,仅包含小写字母、数字和连字符;避免使用 `all`,因为构建和预览命令将其作为全部主题的选择参数。
|
||||||
|
|
||||||
|
编辑生成的元数据和 CSS,定义主题说明及样式。例如:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -55,76 +144,128 @@ CLI 使用 urfave/cli 和 x/net HTML 解析器。文档与打包检查使用 Pyt
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
| 模式 | 生成的展示结构 |
|
`name` 必须与目录名称一致。主题目录仅保存这两个源码文件,不保存官方邮件模板或翻译副本。
|
||||||
|
|
||||||
|
| 模式 | 展示结构 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `framed` | 使用共享控件和框架布局,由主题 CSS 定义样式。新主题默认使用此模式及 `standard` 布局。 |
|
| `framed` | 使用共享控件和布局,由主题 CSS 定义样式。新主题默认使用此模式及 `standard` 布局。 |
|
||||||
| `shared` | 在官方页头片段中加入 CSS,并保留官方页脚片段;不增加框架控件或各类邮件的覆盖模板。 |
|
| `shared` | 在官方页头片段中加入 CSS,并保留官方页脚片段;不增加框架控件或各类邮件的覆盖模板。 |
|
||||||
|
|
||||||
### 共享框架
|
`framed` 主题可通过 `layout` 选择 `framework/layouts/` 中的布局预设。页头、操作按钮、备用链接、侧栏和页脚位于 `framework/mail/base/`。多个主题共用的结构调整应放在共享框架中,官方内容与控件之间的适配位于 `tools/builder/framework.go`。
|
||||||
|
|
||||||
页头、操作按钮、备用链接、侧栏和页脚位于 `framework/mail/base/`。布局预设位于 `framework/layouts/`,`framed` 主题可通过可选的 `layout` 字段选择预设。配色、字体和间距由主题 CSS 定义。
|
主题 CSS 使用 `email-heading`、`email-text`、`email-subtext` 等现有类定义标题、正文和辅助说明层级。保留官方通知文字、翻译键、变量和功能链接,不通过 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
|
```bash
|
||||||
cd tools
|
|
||||||
go run . upstream sync --tag v28.0.0
|
|
||||||
go run . upstream verify
|
|
||||||
go test ./...
|
go test ./...
|
||||||
|
go run . preview my-theme
|
||||||
|
```
|
||||||
|
|
||||||
|
### delete
|
||||||
|
|
||||||
|
`delete` 删除指定主题的源码目录,支持一个或多个名称:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . delete my-theme
|
||||||
|
go run . delete first-theme second-theme
|
||||||
|
```
|
||||||
|
|
||||||
|
可通过 `--folder` 指定主题目录,默认值为 `../themes`。
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> 命令校验主题后直接删除目录,不提供确认提示或回收站恢复。执行前确认名称,并保留需要的修改。
|
||||||
|
|
||||||
|
不存在的主题会报告警告;无效主题或符号链接目录会被拒绝。多个主题依次处理,后续失败不会恢复已删除的目录。`all` 不表示删除全部主题。
|
||||||
|
|
||||||
|
删除源码不会移除之前生成的构建文件、预览数据或已部署的模板。重新执行 `preview all` 更新预览清单;需要移除部署的主题时,按实际安装路径单独处理。
|
||||||
|
|
||||||
|
### preview
|
||||||
|
|
||||||
|
`preview` 构建所选主题,并使用示例数据渲染全部锁定的官方语言。必须指定一个或多个主题,或使用 `all`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . preview aurora
|
||||||
|
go run . preview aurora bloom
|
||||||
go run . preview all
|
go run . preview all
|
||||||
```
|
```
|
||||||
|
|
||||||
评审其他版本时,明确指定对应标签。检查锁文件差异,按需更新框架适配逻辑和测试数据,完成检查及同版本实例冒烟测试后,再更新兼容性记录。`sync` 只修改缓存和锁文件,文档更新与版本发布需单独完成。仅缺少缓存时使用 `prepare`。
|
| 选项 | 默认值 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `--folder` | `../themes` | 主题源码目录;其父目录作为项目根目录 |
|
||||||
|
| `--config` | `./data/templates_config.json` | 邮件类型说明和预览示例数据 |
|
||||||
|
| `--output` | 项目根目录下的 `preview/rendered.js` | 预览清单路径 |
|
||||||
|
|
||||||
已评审的翻译或邮件渲染参考源码发生变化时,须先评审适配逻辑,再更新参考哈希。官方英文必须包含所有引用的翻译键;其他语言缺少的翻译回退为英文。新增邮件类型需要补充框架适配和 `tools/data/templates_config.json` 中的测试数据,该文件提供预览元数据和示例上下文。JSON 测试数据中的整数应保持整数形式,以便 Go 正确格式化。
|
默认输出包括 `preview/rendered.js` 和 `preview/rendered/<语言>.js`。在浏览器中打开 [preview/index.html](../preview/index.html) 即可查看,无需启动服务器。操作和快捷键见 [README](README.zh-CN.md#控制和快捷键)。
|
||||||
|
|
||||||
缓存替换前发生网络或校验错误时,原有文件保持不变。缓存替换和根锁文件写入是两个操作;文件系统错误中断操作后,重试前应检查二者是否一致。
|
> [!IMPORTANT]
|
||||||
|
> 每次生成会覆盖输出位置的预览清单,不与先前选择的主题合并。例如,执行 `preview aurora` 后,该清单只包含 Aurora;需要恢复全部主题时执行 `preview all`。
|
||||||
|
|
||||||
### 缓存恢复
|
自定义输出示例:
|
||||||
|
|
||||||
`prepare` 会报告损坏、不完整或与锁文件不匹配的缓存,不会自动修复。`sync` 同样拒绝替换非空的无效快照。检查错误后,将生成的缓存移至备份位置,再按已评审的根锁文件重新准备。
|
```bash
|
||||||
|
go run . preview --config ./data/templates_config.json --output ../build/preview-review/rendered.js all
|
||||||
|
```
|
||||||
|
|
||||||
例如,在**仓库根目录的 PowerShell** 中,使用尚不存在的备份名称:
|
> [!NOTE]
|
||||||
|
> 语言数据写入输出文件同级的 `rendered/` 目录。命令不会复制预览界面或修改其脚本引用;现有 `preview/index.html` 仍读取默认位置的数据。
|
||||||
|
|
||||||
|
新增邮件类型或渲染分支时,补充 `tools/data/templates_config.json` 中的说明和示例上下文。JSON 中用于计数、大小等数值的整数应保持整数形式。正文、发布说明和评审内容等 HTML 字段应使用可控的测试内容,避免包含个人信息或真实凭据。
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> 预览用于检查模板结构和展示效果。它使用示例上下文,不能代替目标 Gitea 版本的参数兼容性评审;浏览器与邮件客户端对 CSS 的支持也存在差异。
|
||||||
|
|
||||||
|
### dev
|
||||||
|
|
||||||
|
`dev` 启动本地开发服务器,首次启动时构建全部主题并准备预览数据:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . dev
|
||||||
|
```
|
||||||
|
|
||||||
|
打开 [http://127.0.0.1:3456](http://127.0.0.1:3456)。默认端口为 `3456`,可通过 `--port` 修改:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . dev --port 3457
|
||||||
|
```
|
||||||
|
|
||||||
|
服务器仅监听 `127.0.0.1`。在终端中按 `Ctrl + C` 停止运行。该命令按当前 `tools/` 工作目录定位项目,不提供 `--folder`、`--config` 或 `--root` 选项。
|
||||||
|
|
||||||
|
服务器监听主题源码、共享框架、根锁文件、官方缓存及 `tools/data/` 中的预览数据。文件变化后重新构建,通过服务器发送事件(SSE)刷新预览。构建失败时,根据终端输出修复问题,再保存文件。
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> 修改 `preview/index.html` 后手动刷新页面;修改 Go 工具代码后停止并重新启动服务器。这些文件不属于当前的自动重建监听范围。
|
||||||
|
|
||||||
|
### help、h
|
||||||
|
|
||||||
|
查看全部命令或指定命令的帮助:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . help
|
||||||
|
go run . h
|
||||||
|
go run . help build
|
||||||
|
go run . preview --help
|
||||||
|
```
|
||||||
|
|
||||||
|
查看 `upstream` 子命令的参数时,将 `--help` 放在子命令后:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . upstream prepare --help
|
||||||
|
go run . upstream sync --help
|
||||||
|
go run . upstream verify --help
|
||||||
|
```
|
||||||
|
|
||||||
|
工具诊断信息采用 `[LEVEL] [NAME] Message` 格式,`[I]`、`[W]`、`[E]`、`[F]` 分别表示信息、警告、错误和致命错误。在支持颜色的终端中,等级标记使用不同颜色;设置 `NO_COLOR` 可禁用颜色。命令失败时返回非零退出码,排查时保留完整输出。
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### 缺少锁文件或官方缓存怎么办?
|
||||||
|
|
||||||
|
已有克隆缺少 `gitea.lock.json` 时,应恢复受版本控制的根锁文件。缓存中的 `build/upstream/lock.json` 不能代替它。仅缺少缓存时执行 `upstream prepare`,不要为了恢复缓存而同步到其他版本。`build`、`preview` 和 `dev` 会自动准备官方输入。缓存、Go 工具链和依赖齐备后可以离线使用;`upstream verify` 始终只检查本地文件。首次下载遇到网络错误或 GitHub API 限流时,解决网络问题后重试。
|
||||||
|
|
||||||
|
### 缓存损坏或与锁文件不匹配怎么办?
|
||||||
|
|
||||||
|
`prepare` 不会自动修复损坏的缓存,`sync` 也会拒绝替换非空的无效快照。先停止开发服务器,保留根锁文件,将缓存移至备份位置后重新准备。例如,在**仓库根目录的 PowerShell** 中,使用尚不存在的备份名称:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
Move-Item -LiteralPath .\build\upstream -Destination .\build\upstream.backup
|
Move-Item -LiteralPath .\build\upstream -Destination .\build\upstream.backup
|
||||||
@@ -133,80 +274,64 @@ go run . upstream prepare
|
|||||||
go run . upstream verify
|
go run . upstream verify
|
||||||
```
|
```
|
||||||
|
|
||||||
保留 `build/` 中的其他内容,不直接编辑缓存中的官方文件。需要回退快照时,恢复此前已评审的根锁文件,再按同样方式重建缓存。替换输入前先停止 `dev`,避免多个进程同时写入同一缓存。
|
> [!WARNING]
|
||||||
|
> 不要直接修改缓存中的官方文件,也不要删除 `build/` 中的其他内容。回退官方版本时,恢复此前已评审的根锁文件,再按同样方式重建缓存。
|
||||||
|
|
||||||
### 已知上游格式问题
|
### 为什么同步新版本时提示适配源码发生变化?
|
||||||
|
|
||||||
Gitea v28.0.0 的波兰语 `mail.team_invite.text_1` 存在占位符缺陷。预览保留官方输出并报告 `[UPSTREAM-WARN]`。该例外仅匹配已评审的完整原文,其他格式错误会使渲染失败。发现上游问题时应报告问题,不修改缓存中的语言文件。
|
项目固定了已评审的邮件渲染和翻译参考源码。上游修改这些实现后,工具会要求重新评审,避免继续使用不匹配的预览函数或变量结构。检查官方改动,调整适配逻辑和相关测试,再更新参考哈希。新增邮件、翻译键、函数或变量路径都可能需要对应修改。验证通过后再更新[兼容性记录](COMPATIBILITY.zh-CN.md),不要把成功下载等同于完成兼容性确认。
|
||||||
|
|
||||||
## 验证与发布
|
### 为什么部分语言使用英文或出现格式警告?
|
||||||
|
|
||||||
### 拉取请求检查
|
官方英文必须包含所有引用的翻译键;其他语言缺少的键回退为英文,并报告警告。保留官方语言文件,不在缓存中补写译文。
|
||||||
|
|
||||||
在 `tools/` 中运行 `go test ./...` 和 `go run . preview all`。测试覆盖生成结果的确定性、操作锚点变化、翻译键,以及全部主题和语言的通知主题行、正文与链接。测试数据包含推送、评审、回复、工作流和附件分支;共享控件与品牌展示作为附加内容单独处理。
|
例如 Gitea v28.0.0 的波兰语 `mail.team_invite.text_1` 存在已知占位符缺陷。预览对已评审的完整原文保留官方输出并报告警告,该例外不适用于其他格式错误。新增问题应提供版本、翻译键和复现信息,以便评审和向上游反馈。
|
||||||
|
|
||||||
修改文档、追踪脚本或打包流程时,在仓库根目录执行:
|
### 为什么预览没有显示新主题或最新修改?
|
||||||
|
|
||||||
|
静态预览不会自动构建。重新执行 `preview all` 并刷新页面,确认预览清单包含所需主题。使用自定义输出路径时,检查预览界面实际读取的位置;使用 `dev` 时,先查看终端是否报告重建失败。Go 代码变更需要重启服务器,预览界面变更需要手动刷新。默认端口被占用时,使用 `--port` 指定其他端口。
|
||||||
|
|
||||||
|
### 构建成功是否意味着支持所有 Gitea 版本?
|
||||||
|
|
||||||
|
构建结果针对根锁文件指定的官方输入。不同版本可能修改模板目录、函数或变量结构,不能仅凭构建或预览成功确认向下兼容。部署时按[兼容性表](COMPATIBILITY.zh-CN.md#兼容性表)选择发行包,不将新版本模板直接用于旧版本实例。
|
||||||
|
|
||||||
|
### 为什么打包脚本拒绝当前产物?
|
||||||
|
|
||||||
|
打包前执行 `preview all`,确保全部主题和语言数据齐备,且生成文件与当前源码一致。版本参数必须与根锁文件中的标签一致。源码或生成文件变化后,应重新构建和预览。脚本不会覆盖已有发行压缩包。重新打包时选择新的输出目录,保留需要对照的旧产物。
|
||||||
|
|
||||||
|
## 其它事宜
|
||||||
|
|
||||||
|
### 主题与共享框架
|
||||||
|
|
||||||
|
使用适合邮件客户端的 CSS。修改已有主题时,保留其配色、字体、边框和布局特征,使标题、正文和辅助说明的层级清晰,并考虑长文本、长 URL 和移动端宽度。布局片段使用展示性标记,支持 `table`、`tbody`、`tr`、`td`、`div`、`span` 及校验器允许的属性。组合后的标签必须完整配对;`__HEADER__` 插入共享页头,`__MAIL_TYPE__` 展开为邮件 ID,页脚中的 `__SIDEBAR__` 插入共享侧栏。业务变量和通知条件的变更应在构建适配逻辑中评审,不放入主题 CSS 或布局预设。
|
||||||
|
|
||||||
|
新增主题时更新中英文 README 的画廊。图片使用 PNG,单张不超过 50 KiB,建议为 10–20 KiB;主题、模板、语言和裁切方式按[截图指南](images/README.md)保持一致。
|
||||||
|
|
||||||
|
### 提交与拉取请求
|
||||||
|
|
||||||
|
围绕一个明确问题组织改动,避免混入无关的格式调整。拉取请求说明修改原因、影响范围和验证结果;涉及视觉变化时,可附上使用示例数据生成的对照图。修改主题、共享框架或 Go 工具时,在 `tools/` 中运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./...
|
||||||
|
go run . preview all
|
||||||
|
```
|
||||||
|
|
||||||
|
涉及邮件内容、链接、变量或函数适配时,补充对应的回归测试,覆盖受影响的条件分支。现有测试检查生成结果、翻译键及全部主题和语言的通知主题行、正文与链接,包括推送、评审、回复、工作流和附件分支。
|
||||||
|
|
||||||
|
修改打包脚本时,在仓库根目录运行:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -B -m unittest discover -s .github/scripts -p 'test_*.py'
|
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(<名称>):` | 主题展示样式 |
|
| `style(<名称>):` | 主题展示样式 |
|
||||||
| `preview:` | 浏览器预览 |
|
| `preview:` | 预览工具 |
|
||||||
| `tools:` | CLI、构建与快照工具 |
|
| `tools:` | CLI、构建与快照工具 |
|
||||||
| `docs:` | 文档和翻译 |
|
| `docs:` | 文档和翻译 |
|
||||||
| `fix:` | 问题修复 |
|
| `fix:` | 问题修复 |
|
||||||
@@ -214,6 +339,26 @@ PR 创建和发行附件上传通过 `.github/scripts/gitea_actions.py` 调用
|
|||||||
| `refactor:` | 代码重构 |
|
| `refactor:` | 代码重构 |
|
||||||
| `chore:` | 日常维护 |
|
| `chore:` | 日常维护 |
|
||||||
|
|
||||||
## 翻译与许可证
|
### 版本与发布维护
|
||||||
|
|
||||||
[英文指南](../CONTRIBUTING.md)介绍相同的开发流程。贡献采用 MIT 许可证;分发派生模板时,请保留 Gitea 版权和官方许可证,详见[第三方声明](../THIRD_PARTY_NOTICES.md)。
|
发行标签必须与锁定的 Gitea 标签一致。版本标记、徽章和兼容性记录由维护者手动更新;修改官方输入不会自动触发版本维护。历史发行附件应保留,文档和兼容性记录应在打标签前检查。完成验证并生成全部预览后,可在仓库根目录手动打包:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python .github/scripts/package_release.py --version v28.1.0 --output build/release-review
|
||||||
|
```
|
||||||
|
|
||||||
|
将示例版本替换为根锁文件中的标签,并使用没有同名发行压缩包的输出目录。脚本检查源码与产物的校验值,打包主题、预览、文档、官方许可证和来源信息,排除残留的旧主题构建。
|
||||||
|
|
||||||
|
`.github/workflows/validate.yml` 负责普通推送和拉取请求检查,也供 Release 工作流调用。Release 验证通过后打包归档,并通过发布 Action 上传附件,以标签作为标题,新建 Release 的正文为空,不要求发行说明文件。重新发布已有标签时保留已有正文并替换同名附件;失败后重试前检查发行版及附件状态。
|
||||||
|
|
||||||
|
工作流使用 `linux-amd64-docker-small` Runner 标签,并通过安装步骤准备 Go、Python 和发布所需的 Node.js。Action 使用 `actions/...@版本` 引用,由 Runner 的 Action 来源配置解析;维护时应考虑当前 `github_mirror` 配置。发布使用内置 `GITEA_TOKEN`,仓库需具备发行版写入权限。
|
||||||
|
|
||||||
|
### 报告问题
|
||||||
|
|
||||||
|
问题报告应包含 Gitea 版本、模板发行版或源码提交、主题、邮件类型、语言和复现步骤。构建或预览问题还应提供根锁文件中的标签、相关命令和完整错误输出;显示问题注明邮件客户端,并附上移除个人信息后的截图。
|
||||||
|
|
||||||
|
提出改进建议时,说明使用场景、预期效果及涉及的主题或工具。发现官方模板或翻译问题时,保留原始输入,提供能够复现问题的最小示例。
|
||||||
|
|
||||||
|
### 许可证
|
||||||
|
|
||||||
|
本项目采用 [MIT 许可证](../LICENSE)。贡献应使用能够合法分发的代码、样式和图片。生成和分发派生模板时,保留 Gitea 版权、官方许可证及快照来源信息;发行包已包含相关文件。
|
||||||
+12
-13
@@ -5,9 +5,9 @@
|
|||||||
|
|
||||||
Gitea Mail Template 为自托管的 [Gitea](https://about.gitea.com) 提供多种可选、适合的邮件模板风格。
|
Gitea Mail Template 为自托管的 [Gitea](https://about.gitea.com) 提供多种可选、适合的邮件模板风格。
|
||||||
|
|
||||||
> 最新发布版:[v28.0.0](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/latest)
|
> 最新发布版:[v28.1.0](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/latest)
|
||||||
|
|
||||||
仓库包含数种主题的 Gitea 邮件模板,适合大多数的部署场景与应用范围。安装前,请根据 Gitea 版本查阅[兼容矩阵](../COMPATIBILITY.md#compatibility-matrix),选择对应的发行包。
|
仓库包含数种主题的 Gitea 邮件模板,适合大多数的部署场景与应用范围。安装前,请根据 Gitea 版本查阅[兼容性表](./COMPATIBILITY.zh-CN.md#兼容性表),选择对应的发行包。
|
||||||
|
|
||||||
## 风格画廊
|
## 风格画廊
|
||||||
|
|
||||||
@@ -26,13 +26,13 @@ Gitea Mail Template 为自托管的 [Gitea](https://about.gitea.com) 提供多
|
|||||||
|  | **Terminal** | 深色背景、等宽字体、绿色强调色 |
|
|  | **Terminal** | 深色背景、等宽字体、绿色强调色 |
|
||||||
|  | **Terra** | 大地色调、陶土色按钮、衬线字体 |
|
|  | **Terra** | 大地色调、陶土色按钮、衬线字体 |
|
||||||
|
|
||||||
截图仅展示当前默认构建效果,其他邮件类型和语言可通过[本地预览](#预览)查看;更新截图请参阅[截图指南](images/README.md)。
|
截图仅展示当前默认构建效果,其他邮件类型和语言可通过[预览](#预览)查看;更新截图请参阅[截图指南](images/README.md)。
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
### 选择安装来源
|
### 选择安装来源
|
||||||
|
|
||||||
运行 `gitea --version` 确认部署的 Gitea 实例版本,再按照[兼容矩阵](../COMPATIBILITY.md#compatibility-matrix)选择模板版本。
|
运行 `gitea --version` 确认部署的 Gitea 实例版本,再按照[兼容性表](COMPATIBILITY.zh-CN.md#兼容性表)选择模板版本。
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> 我们不建议直接从源码仓库拉取并构建,因为这些版本还未经过完全测试达到发行可用的水平。如果存在未知的模板参数过时或遗漏可能导致 Gitea 实例启动异常。
|
> 我们不建议直接从源码仓库拉取并构建,因为这些版本还未经过完全测试达到发行可用的水平。如果存在未知的模板参数过时或遗漏可能导致 Gitea 实例启动异常。
|
||||||
@@ -52,7 +52,8 @@ cd tools
|
|||||||
go run . build all
|
go run . build all
|
||||||
cd ..
|
cd ..
|
||||||
```
|
```
|
||||||
构建可用模板时需要依赖于 Gitea 官方仓库的资源文件,以确保来源的一致性。因此该命令会自动下载由`gitea.lock.json` 文件锁定的官方文件,后续构建无需再次下载,使用缓存目录资源即可运行。文件缺失或缓存损坏的处理方式见[更新官方快照](CONTRIBUTING.zh-CN.md#更新官方快照)。
|
|
||||||
|
构建可用模板时需要依赖于 Gitea 官方仓库的资源文件,以确保来源的一致性。因此该命令会自动下载由`gitea.lock.json` 文件锁定的官方文件,后续构建无需再次下载,使用缓存目录资源即可运行。
|
||||||
|
|
||||||
### 安装主题
|
### 安装主题
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ systemctl restart gitea
|
|||||||
```
|
```
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> 使用源码构建产物时,将复制来源改为 `build/themes/horizon/mail/.`。Docker 和 Windows 部署请使> 用相应的容器或服务管理方式暂停和重启。
|
> 使用源码构建产物时,将复制来源改为 `build/themes/horizon/mail/.`。Docker 和 Windows 部署请使用相应的容器或服务管理方式暂停和重启。
|
||||||
|
|
||||||
### 切换主题
|
### 切换主题
|
||||||
|
|
||||||
@@ -109,7 +110,7 @@ go run . dev
|
|||||||
cd ..
|
cd ..
|
||||||
```
|
```
|
||||||
|
|
||||||
打开 [http://127.0.0.1:3456](http://127.0.0.1:3456)。Go 服务器监听主题文件、共享框架、锁文件与缓存、预览测试数据的变化。一旦监听到文件更改,服务器会将邮件模板重新构建后通过发送事件(SSE)刷新页面,以实现实时热重载。
|
打开 [http://127.0.0.1:3456](http://127.0.0.1:3456)。服务器会监听主题文件、共享框架、锁文件与缓存、预览测试数据的变化。一旦监听到文件更改,服务器会将邮件模板重新构建后通过发送事件(SSE)刷新页面,以实现实时热重载。
|
||||||
|
|
||||||
### 控制和快捷键
|
### 控制和快捷键
|
||||||
|
|
||||||
@@ -125,8 +126,8 @@ cd ..
|
|||||||
|
|
||||||
## 兼容性
|
## 兼容性
|
||||||
|
|
||||||
- **最新测试:** Gitea 28.0.0
|
- **最新测试:** Gitea 28.1.0
|
||||||
- **最新发布版:** [v28.0.0](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/latest)
|
- **最新发布版:** [v28.1.0](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/latest)
|
||||||
- **上游 Gitea 28.1.0:** [PENDING]
|
- **上游 Gitea 28.1.0:** [PENDING]
|
||||||
|
|
||||||
## 项目目录结构
|
## 项目目录结构
|
||||||
@@ -137,7 +138,6 @@ cd ..
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
.github/ # 工作流及维护脚本
|
.github/ # 工作流及维护脚本
|
||||||
release-notes/ # 各版本的发行说明
|
|
||||||
scripts/ # 文档更新、打包和 Gitea 发布脚本
|
scripts/ # 文档更新、打包和 Gitea 发布脚本
|
||||||
workflows/ # 验证和发布
|
workflows/ # 验证和发布
|
||||||
docs/ # 中文文档、文档索引和画廊图片
|
docs/ # 中文文档、文档索引和画廊图片
|
||||||
@@ -154,7 +154,6 @@ tools/ # Go 命令行工具、构建工具和测试
|
|||||||
preview/ # 邮件渲染、语言适配和开发服务器
|
preview/ # 邮件渲染、语言适配和开发服务器
|
||||||
upstream/ # 官方快照下载、校验和翻译键发现
|
upstream/ # 官方快照下载、校验和翻译键发现
|
||||||
gitea.lock.json # 官方版本标签、提交和文件校验值
|
gitea.lock.json # 官方版本标签、提交和文件校验值
|
||||||
README.md
|
|
||||||
```
|
```
|
||||||
|
|
||||||
官方模板提供通知内容、条件、主题行和链接;共享框架负责邮件呈现,主题通过元数据和 CSS 选择布局、定义样式。主题目录中不保存官方邮件源码或翻译文件。
|
官方模板提供通知内容、条件、主题行和链接;共享框架负责邮件呈现,主题通过元数据和 CSS 选择布局、定义样式。主题目录中不保存官方邮件源码或翻译文件。
|
||||||
@@ -195,11 +194,11 @@ preview/rendered/<LOCALE>.js # 各语言的预览数据
|
|||||||
|
|
||||||
## 参与贡献
|
## 参与贡献
|
||||||
|
|
||||||
欢迎改进主题、工具、文档和翻译。[贡献指南](CONTRIBUTING.zh-CN.md)介绍了本地环境准备、设计规范、检查要求和发布流程。
|
欢迎改进主题、工具、文档和翻译。[贡献指南](CONTRIBUTING.zh-CN.md)介绍了贡献相关的事宜。
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
本项目采用 [MIT 许可证](../LICENSE)。生成的发行包保留 Gitea 许可证和快照来源信息,详见[第三方声明](../THIRD_PARTY_NOTICES.md)。
|
本项目采用 [MIT 许可证](../LICENSE)。生成的发行包保留 Gitea 许可证和快照来源信息。
|
||||||
|
|
||||||
---
|
---
|
||||||
### 注释
|
### 注释
|
||||||
|
|||||||
+3
-3
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"schema": 1,
|
"schema": 1,
|
||||||
"repository": "https://github.com/go-gitea/gitea",
|
"repository": "https://github.com/go-gitea/gitea",
|
||||||
"tag": "v28.0.0",
|
"tag": "v28.1.0",
|
||||||
"commit": "15b8a5805adf57c5189602008d38cccfd3c795e0",
|
"commit": "4ebd5e319b6a9e629bfd7a04292a6fe85b08dee9",
|
||||||
"files": {
|
"files": {
|
||||||
"LICENSE": "ed2f10a9d78b8c6c9ef33f1420d0eb266981891caf2f15d630553f02dc60d3ae",
|
"LICENSE": "ed2f10a9d78b8c6c9ef33f1420d0eb266981891caf2f15d630553f02dc60d3ae",
|
||||||
"modules/templates/mail.go": "fb8204b79f700f88c7c3f0da16e50c3d678a2216e62e98f712805ed1d42ab3a9",
|
"modules/templates/mail.go": "fb8204b79f700f88c7c3f0da16e50c3d678a2216e62e98f712805ed1d42ab3a9",
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"options/locale/locale_cs-CZ.json": "816c4a0cfc6112c8cc9a63e63b3de0bc623819a23d2f2bbb800b09c45d23da66",
|
"options/locale/locale_cs-CZ.json": "816c4a0cfc6112c8cc9a63e63b3de0bc623819a23d2f2bbb800b09c45d23da66",
|
||||||
"options/locale/locale_de-DE.json": "593ec4e3652c0faf9ecb5d5b27a649c1c0a933030ecdfbf4dc810ea4ad69bfb8",
|
"options/locale/locale_de-DE.json": "593ec4e3652c0faf9ecb5d5b27a649c1c0a933030ecdfbf4dc810ea4ad69bfb8",
|
||||||
"options/locale/locale_el-GR.json": "dd03e6e083d6286d8eb2045a37bcf5c578fc339ab87a72a7397e01733078d89e",
|
"options/locale/locale_el-GR.json": "dd03e6e083d6286d8eb2045a37bcf5c578fc339ab87a72a7397e01733078d89e",
|
||||||
"options/locale/locale_en-US.json": "bb58779f10b2cd0ee94f71297b2231a621d4d30f2a56a2b223f3c586f635f592",
|
"options/locale/locale_en-US.json": "3505b23a6285140eb4ff97f7b24846963254013403791189e468967b5c870c95",
|
||||||
"options/locale/locale_es-ES.json": "cbb1cd7bc4709a189b8b189d26a83a06fed0c32e0b5ac5602370fb0f03a39d04",
|
"options/locale/locale_es-ES.json": "cbb1cd7bc4709a189b8b189d26a83a06fed0c32e0b5ac5602370fb0f03a39d04",
|
||||||
"options/locale/locale_fa-IR.json": "ad85f5829335844883608ed5ed09dee5bc5f4800cda7ef9277200aa4a7f35f32",
|
"options/locale/locale_fa-IR.json": "ad85f5829335844883608ed5ed09dee5bc5f4800cda7ef9277200aa4a7f35f32",
|
||||||
"options/locale/locale_fi-FI.json": "3b811b0ee794548399bcb64a887711a26cc0c26e04dfd0305b0846742d065952",
|
"options/locale/locale_fi-FI.json": "3b811b0ee794548399bcb64a887711a26cc0c26e04dfd0305b0846742d065952",
|
||||||
|
|||||||
+88
-77
@@ -29,6 +29,7 @@ body{font-family:system-ui,-apple-system,sans-serif;background:var(--bg);color:v
|
|||||||
#topbar .btn-group button+button{border-left:none}
|
#topbar .btn-group button+button{border-left:none}
|
||||||
#topbar .btn-group button.on{background:var(--accent);color:#000;border-color:var(--accent);font-weight:600}
|
#topbar .btn-group button.on{background:var(--accent);color:#000;border-color:var(--accent);font-weight:600}
|
||||||
#topbar .hint{font-size:10px;color:var(--muted);opacity:.5;white-space:nowrap}
|
#topbar .hint{font-size:10px;color:var(--muted);opacity:.5;white-space:nowrap}
|
||||||
|
button:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
|
||||||
|
|
||||||
/* Topbar responsive — progressively compact as width shrinks */
|
/* Topbar responsive — progressively compact as width shrinks */
|
||||||
@media(max-width:900px){#topbar .hint{display:none}}
|
@media(max-width:900px){#topbar .hint{display:none}}
|
||||||
@@ -49,32 +50,29 @@ body{font-family:system-ui,-apple-system,sans-serif;background:var(--bg);color:v
|
|||||||
|
|
||||||
/* Main layout */
|
/* Main layout */
|
||||||
#main{flex:1;display:flex;position:relative;min-height:0;overflow:hidden}
|
#main{flex:1;display:flex;position:relative;min-height:0;overflow:hidden}
|
||||||
#preview-area{flex:1;display:flex;align-items:flex-start;justify-content:center;padding:24px;overflow:auto;background:linear-gradient(135deg,#0b1120,#131c30,#0d1525);transition:padding-right .3s cubic-bezier(.4,0,.2,1)}
|
#preview-area{flex:1;min-width:0;min-height:0;display:flex;align-items:flex-start;justify-content:center;padding:24px;overflow:auto;background:linear-gradient(135deg,#0b1120,#131c30,#0d1525)}
|
||||||
|
|
||||||
/* --- Floating panel --- */
|
/* Panel and preview share the available width; the handle occupies its own gutter. */
|
||||||
#panel{
|
#panel{
|
||||||
position:absolute;right:12px;top:12px;bottom:12px;
|
--panel-w:min(440px,40vw);
|
||||||
width:var(--panel-w);
|
position:relative;flex:0 0 var(--panel-w);width:var(--panel-w);min-width:0;
|
||||||
|
margin:12px 12px 12px 40px;
|
||||||
background:var(--surface);
|
background:var(--surface);
|
||||||
border:1px solid var(--border);
|
border:1px solid var(--border);
|
||||||
border-radius:10px;
|
border-radius:10px;
|
||||||
box-shadow:-4px 4px 32px rgba(0,0,0,.5),0 0 0 1px rgba(255,255,255,.03) inset;
|
box-shadow:-4px 4px 32px rgba(0,0,0,.5),0 0 0 1px rgba(255,255,255,.03) inset;
|
||||||
z-index:20;
|
|
||||||
display:flex;flex-direction:column;
|
display:flex;flex-direction:column;
|
||||||
overflow:hidden;
|
transition:flex-basis .3s ease,width .3s ease;
|
||||||
transition:transform .35s cubic-bezier(.4,0,.2,1),opacity .35s ease,box-shadow .35s ease;
|
|
||||||
}
|
}
|
||||||
#panel.collapsed{
|
#panel.collapsed{
|
||||||
transform:translateX(calc(var(--panel-w) - 36px));
|
flex-basis:0;width:0;border-width:0;background:transparent;box-shadow:none;
|
||||||
box-shadow:-2px 2px 16px rgba(0,0,0,.35),0 0 0 1px rgba(255,255,255,.03) inset;
|
|
||||||
opacity:.92;
|
|
||||||
}
|
}
|
||||||
#panel.collapsed:hover{opacity:1}
|
#panel.collapsed #panel-body{visibility:hidden}
|
||||||
|
|
||||||
/* Panel drag-handle / collapse tab */
|
/* Panel drag-handle / collapse tab */
|
||||||
#panel-handle{
|
#panel-handle{
|
||||||
position:absolute;left:0;top:50%;transform:translateY(-50%);
|
position:absolute;left:-40px;top:50%;transform:translateY(-50%);
|
||||||
width:28px;height:64px;
|
width:40px;height:80px;
|
||||||
background:var(--surface);
|
background:var(--surface);
|
||||||
border:1px solid var(--border);
|
border:1px solid var(--border);
|
||||||
border-right:none;
|
border-right:none;
|
||||||
@@ -82,17 +80,11 @@ body{font-family:system-ui,-apple-system,sans-serif;background:var(--bg);color:v
|
|||||||
cursor:pointer;
|
cursor:pointer;
|
||||||
display:flex;align-items:center;justify-content:center;
|
display:flex;align-items:center;justify-content:center;
|
||||||
transition:background .15s;
|
transition:background .15s;
|
||||||
z-index:21;
|
color:var(--muted);
|
||||||
/* sit just outside the panel's left edge */
|
|
||||||
margin-left:-1px;
|
|
||||||
}
|
|
||||||
#panel.collapsed #panel-handle{
|
|
||||||
/* when collapsed, handle moves with panel, peeking from right edge */
|
|
||||||
left:1px;
|
|
||||||
}
|
}
|
||||||
#panel-handle:hover{background:var(--bg)}
|
#panel-handle:hover{background:var(--bg)}
|
||||||
#panel-handle svg{
|
#panel-handle svg{
|
||||||
width:14px;height:14px;
|
width:20px;height:20px;
|
||||||
stroke:var(--muted);
|
stroke:var(--muted);
|
||||||
fill:none;stroke-width:2;
|
fill:none;stroke-width:2;
|
||||||
transition:transform .35s cubic-bezier(.4,0,.2,1),stroke .15s;
|
transition:transform .35s cubic-bezier(.4,0,.2,1),stroke .15s;
|
||||||
@@ -101,22 +93,7 @@ body{font-family:system-ui,-apple-system,sans-serif;background:var(--bg);color:v
|
|||||||
#panel.collapsed #panel-handle svg{transform:rotate(180deg)}
|
#panel.collapsed #panel-handle svg{transform:rotate(180deg)}
|
||||||
|
|
||||||
/* Panel inner scroll */
|
/* Panel inner scroll */
|
||||||
#panel-body{flex:1;overflow-y:auto;overflow-x:hidden}
|
#panel-body{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;border-radius:10px}
|
||||||
|
|
||||||
/* Panel close button (top-right) */
|
|
||||||
#panel-close{
|
|
||||||
position:absolute;right:8px;top:8px;
|
|
||||||
width:24px;height:24px;
|
|
||||||
background:none;border:none;
|
|
||||||
cursor:pointer;
|
|
||||||
display:flex;align-items:center;justify-content:center;
|
|
||||||
border-radius:4px;
|
|
||||||
color:var(--muted);
|
|
||||||
font-size:16px;line-height:1;
|
|
||||||
transition:background .15s,color .15s;
|
|
||||||
z-index:2;
|
|
||||||
}
|
|
||||||
#panel-close:hover{background:rgba(255,255,255,.06);color:var(--text)}
|
|
||||||
|
|
||||||
/* Section */
|
/* Section */
|
||||||
#panel section{padding:12px 14px;border-bottom:1px solid var(--border)}
|
#panel section{padding:12px 14px;border-bottom:1px solid var(--border)}
|
||||||
@@ -148,17 +125,15 @@ section.collapsed .section-chevron{transform:rotate(-90deg)}
|
|||||||
|
|
||||||
/* Collapsible content wrapper */
|
/* Collapsible content wrapper */
|
||||||
.section-content{
|
.section-content{
|
||||||
overflow:hidden;
|
display:grid;grid-template-rows:1fr;opacity:1;margin-top:10px;
|
||||||
transition:max-height .3s ease,opacity .25s ease,margin-top .3s ease;
|
transition:grid-template-rows .3s ease,opacity .25s ease,margin-top .3s ease;
|
||||||
max-height:2000px;
|
|
||||||
opacity:1;
|
|
||||||
margin-top:10px;
|
|
||||||
}
|
}
|
||||||
|
.section-content-inner{min-height:0;overflow:hidden}
|
||||||
section.collapsed .section-content{
|
section.collapsed .section-content{
|
||||||
max-height:0;
|
grid-template-rows:0fr;opacity:0;margin-top:0;
|
||||||
opacity:0;
|
|
||||||
margin-top:0;
|
|
||||||
}
|
}
|
||||||
|
#sec-params .section-content-inner{max-height:50vh;overflow:auto;overscroll-behavior:contain}
|
||||||
|
section.collapsed .section-content-inner{overflow:hidden}
|
||||||
|
|
||||||
#panel .card{background:var(--bg);border-radius:var(--rad);padding:10px 12px;margin-bottom:8px}
|
#panel .card{background:var(--bg);border-radius:var(--rad);padding:10px 12px;margin-bottom:8px}
|
||||||
#panel .card:last-child{margin-bottom:0}
|
#panel .card:last-child{margin-bottom:0}
|
||||||
@@ -166,19 +141,21 @@ section.collapsed .section-content{
|
|||||||
#panel .card .value{font-size:12px;color:var(--text)}
|
#panel .card .value{font-size:12px;color:var(--text)}
|
||||||
#panel table{width:100%;border-collapse:collapse}
|
#panel table{width:100%;border-collapse:collapse}
|
||||||
#panel table td{padding:4px 6px;font-size:11px;border-bottom:1px solid rgba(255,255,255,.03);vertical-align:top}
|
#panel table td{padding:4px 6px;font-size:11px;border-bottom:1px solid rgba(255,255,255,.03);vertical-align:top}
|
||||||
#panel table td:first-child{font-family:monospace;color:var(--accent);font-size:10px;width:0;white-space:nowrap}
|
#panel table td:first-child{font-family:monospace;color:var(--accent);font-size:10px;overflow-wrap:anywhere}
|
||||||
#panel table td:last-child{color:var(--muted);text-align:right;font-size:10px;word-break:break-all}
|
#panel table td:nth-child(2){color:var(--muted);font-size:10px;overflow-wrap:anywhere}
|
||||||
|
#panel table td:last-child{text-align:right;width:1%}
|
||||||
#panel .badge{display:inline-block;font-size:9px;padding:1px 6px;border-radius:3px;font-weight:600;letter-spacing:.2px;white-space:nowrap}
|
#panel .badge{display:inline-block;font-size:9px;padding:1px 6px;border-radius:3px;font-weight:600;letter-spacing:.2px;white-space:nowrap}
|
||||||
.badge-req{background:rgba(52,211,153,.12);color:var(--green)}
|
.badge-req{background:rgba(52,211,153,.12);color:var(--green)}
|
||||||
.badge-opt{background:rgba(107,125,153,.1);color:var(--muted)}
|
.badge-opt{background:rgba(107,125,153,.1);color:var(--muted)}
|
||||||
#panel .stat{font-size:10px;color:var(--muted);margin-top:6px}
|
#panel .stat{font-size:10px;color:var(--muted);margin-top:6px}
|
||||||
|
|
||||||
/* Viewport */
|
/* Viewport */
|
||||||
#viewport{border-radius:var(--rad);box-shadow:0 8px 32px rgba(0,0,0,.45);transition:all .3s;flex-shrink:0;overflow:hidden;background:#fff}
|
#viewport{border:0 solid #1e293b;border-radius:var(--rad);box-shadow:0 8px 32px rgba(0,0,0,.45);flex-shrink:0;max-width:100%;overflow:hidden;background:#fff;display:flex;flex-direction:column;transition:width .3s ease,border-radius .3s ease,border-width .3s ease,box-shadow .3s ease}
|
||||||
#viewport.desktop{width:1386px;height:780px}
|
#viewport.desktop{width:100%;max-width:1386px;height:780px}
|
||||||
#viewport.mobile{width:390px;height:780px;border-radius:28px;border:3px solid #1e293b;box-shadow:0 8px 32px rgba(0,0,0,.5),inset 0 0 0 1px #0f172a;display:flex;flex-direction:column}
|
#viewport.mobile{width:390px;height:780px;border-radius:28px;border:3px solid #1e293b;box-shadow:0 8px 32px rgba(0,0,0,.5),inset 0 0 0 1px #0f172a;display:flex;flex-direction:column}
|
||||||
#viewport.mobile::before{content:'';display:block;height:24px;background:#1e293b;flex-shrink:0}
|
#viewport::before{content:'';display:block;height:0;background:#1e293b;flex-shrink:0;transition:height .3s ease}
|
||||||
#preview-frame{width:100%;height:100%;border:none;display:block;background:#fff}
|
#viewport.mobile::before{height:24px}
|
||||||
|
#preview-frame{width:100%;height:100%;flex:1;min-height:0;border:none;display:block;background:#fff}
|
||||||
#viewport.desktop #preview-frame{border-radius:var(--rad)}
|
#viewport.desktop #preview-frame{border-radius:var(--rad)}
|
||||||
|
|
||||||
/* Loading */
|
/* Loading */
|
||||||
@@ -193,7 +170,7 @@ section.collapsed .section-content{
|
|||||||
.message code{background:rgba(0,0,0,0.12);padding:1px 6px;border-radius:3px}
|
.message code{background:rgba(0,0,0,0.12);padding:1px 6px;border-radius:3px}
|
||||||
|
|
||||||
/* Scrollbar — neutral gray, consistent across all browsers */
|
/* Scrollbar — neutral gray, consistent across all browsers */
|
||||||
html,body,#preview-area,#panel-body{
|
html,body,#preview-area,#panel-body,#sec-params .section-content-inner{
|
||||||
scrollbar-width:thin;
|
scrollbar-width:thin;
|
||||||
scrollbar-color:rgba(107,125,153,.25) transparent;
|
scrollbar-color:rgba(107,125,153,.25) transparent;
|
||||||
}
|
}
|
||||||
@@ -201,27 +178,30 @@ html,body,#preview-area,#panel-body{
|
|||||||
-ms-overflow-style:-ms-autohiding-scrollbar;
|
-ms-overflow-style:-ms-autohiding-scrollbar;
|
||||||
}
|
}
|
||||||
#preview-area::-webkit-scrollbar,
|
#preview-area::-webkit-scrollbar,
|
||||||
|
#sec-params .section-content-inner::-webkit-scrollbar,
|
||||||
#panel-body::-webkit-scrollbar { width:6px; height:6px }
|
#panel-body::-webkit-scrollbar { width:6px; height:6px }
|
||||||
#preview-area::-webkit-scrollbar-track,
|
#preview-area::-webkit-scrollbar-track,
|
||||||
|
#sec-params .section-content-inner::-webkit-scrollbar-track,
|
||||||
#panel-body::-webkit-scrollbar-track { background:transparent }
|
#panel-body::-webkit-scrollbar-track { background:transparent }
|
||||||
#preview-area::-webkit-scrollbar-thumb,
|
#preview-area::-webkit-scrollbar-thumb,
|
||||||
|
#sec-params .section-content-inner::-webkit-scrollbar-thumb,
|
||||||
#panel-body::-webkit-scrollbar-thumb {
|
#panel-body::-webkit-scrollbar-thumb {
|
||||||
background-color:rgba(107,125,153,.25);
|
background-color:rgba(107,125,153,.25);
|
||||||
border-radius:3px;
|
border-radius:3px;
|
||||||
border:1px solid rgba(255,255,255,.06);
|
border:1px solid rgba(255,255,255,.06);
|
||||||
}
|
}
|
||||||
#preview-area::-webkit-scrollbar-thumb:hover,
|
#preview-area::-webkit-scrollbar-thumb:hover,
|
||||||
|
#sec-params .section-content-inner::-webkit-scrollbar-thumb:hover,
|
||||||
#panel-body::-webkit-scrollbar-thumb:hover { background-color:rgba(107,125,153,.45) }
|
#panel-body::-webkit-scrollbar-thumb:hover { background-color:rgba(107,125,153,.45) }
|
||||||
#preview-area::-webkit-scrollbar-corner,
|
#preview-area::-webkit-scrollbar-corner,
|
||||||
|
#sec-params .section-content-inner::-webkit-scrollbar-corner,
|
||||||
#panel-body::-webkit-scrollbar-corner { background:transparent }
|
#panel-body::-webkit-scrollbar-corner { background:transparent }
|
||||||
|
|
||||||
@media(max-width:900px){
|
|
||||||
#panel{--panel-w:380px;right:6px;top:6px;bottom:6px}
|
|
||||||
#panel.collapsed{transform:translateX(calc(var(--panel-w) - 36px))}
|
|
||||||
}
|
|
||||||
@media(max-width:700px){
|
@media(max-width:700px){
|
||||||
#panel{display:none}
|
#panel{--panel-w:50vw;margin-top:6px;margin-bottom:6px;margin-right:6px}
|
||||||
|
#preview-area{padding:12px}
|
||||||
}
|
}
|
||||||
|
@media(prefers-reduced-motion:reduce){#panel,#panel-handle svg,.section-content,.section-chevron,#viewport,#viewport::before{transition:none}}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -274,12 +254,9 @@ html,body,#preview-area,#panel-body{
|
|||||||
|
|
||||||
<aside id="panel">
|
<aside id="panel">
|
||||||
<!-- Collapse handle tab on the left edge -->
|
<!-- Collapse handle tab on the left edge -->
|
||||||
<div id="panel-handle" title="Toggle panel" onclick="togglePanel()">
|
<button id="panel-handle" type="button" title="Collapse panel" aria-label="Collapse panel" aria-expanded="true" aria-controls="panel-body" onclick="togglePanel()">
|
||||||
<svg viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
|
<svg viewBox="0 0 24 24" aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg>
|
||||||
</div>
|
</button>
|
||||||
|
|
||||||
<!-- Close button (top-right) -->
|
|
||||||
<button id="panel-close" title="Close panel" onclick="togglePanel()">×</button>
|
|
||||||
|
|
||||||
<div id="panel-body">
|
<div id="panel-body">
|
||||||
<section id="sec-info">
|
<section id="sec-info">
|
||||||
@@ -288,6 +265,7 @@ html,body,#preview-area,#panel-body{
|
|||||||
<svg class="section-chevron" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg>
|
<svg class="section-chevron" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-content">
|
<div class="section-content">
|
||||||
|
<div class="section-content-inner">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="label">Type</div>
|
<div class="label">Type</div>
|
||||||
<div class="value" id="panel-name">-</div>
|
<div class="value" id="panel-name">-</div>
|
||||||
@@ -298,6 +276,7 @@ html,body,#preview-area,#panel-body{
|
|||||||
</div>
|
</div>
|
||||||
<div class="stat" id="panel-stats"></div>
|
<div class="stat" id="panel-stats"></div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="sec-params">
|
<section id="sec-params">
|
||||||
@@ -306,8 +285,10 @@ html,body,#preview-area,#panel-body{
|
|||||||
<svg class="section-chevron" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg>
|
<svg class="section-chevron" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-content">
|
<div class="section-content">
|
||||||
|
<div class="section-content-inner">
|
||||||
<table id="panel-params"><tr><td colspan="2" style="color:var(--muted)">-</td></tr></table>
|
<table id="panel-params"><tr><td colspan="2" style="color:var(--muted)">-</td></tr></table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="sec-source">
|
<section id="sec-source">
|
||||||
@@ -316,8 +297,10 @@ html,body,#preview-area,#panel-body{
|
|||||||
<svg class="section-chevron" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg>
|
<svg class="section-chevron" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-content">
|
<div class="section-content">
|
||||||
|
<div class="section-content-inner">
|
||||||
<div class="card" style="font-family:monospace;font-size:10px;color:var(--muted);word-break:break-all" id="panel-path">-</div>
|
<div class="card" style="font-family:monospace;font-size:10px;color:var(--muted);word-break:break-all" id="panel-path">-</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -400,12 +383,18 @@ function render() {
|
|||||||
//
|
//
|
||||||
function togglePanel() {
|
function togglePanel() {
|
||||||
const panel = document.getElementById('panel');
|
const panel = document.getElementById('panel');
|
||||||
panel.classList.toggle('collapsed');
|
const collapsed = panel.classList.toggle('collapsed');
|
||||||
|
const handle = document.getElementById('panel-handle');
|
||||||
|
handle.setAttribute('aria-expanded', String(!collapsed));
|
||||||
|
handle.setAttribute('aria-label', collapsed ? 'Expand panel' : 'Collapse panel');
|
||||||
|
handle.title = collapsed ? 'Expand panel' : 'Collapse panel';
|
||||||
|
document.getElementById('panel-body').inert = collapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSection(id) {
|
function toggleSection(id) {
|
||||||
const sec = document.getElementById(id);
|
const sec = document.getElementById(id);
|
||||||
sec.classList.toggle('collapsed');
|
const collapsed = sec.classList.toggle('collapsed');
|
||||||
|
sec.querySelector('.section-content').inert = collapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -447,11 +436,11 @@ function switchTheme(v) { state.theme=v; render(); }
|
|||||||
function switchTemplate(v) { state.template=v; switchLocale(state.locale); }
|
function switchTemplate(v) { state.template=v; switchLocale(state.locale); }
|
||||||
function switchClient(v) { state.client=v; render(); }
|
function switchClient(v) { state.client=v; render(); }
|
||||||
function setViewport(v) {
|
function setViewport(v) {
|
||||||
|
if (state.viewport === v) return;
|
||||||
state.viewport=v;
|
state.viewport=v;
|
||||||
document.getElementById('btn-desk').classList.toggle('on',v==='desktop');
|
document.getElementById('btn-desk').classList.toggle('on',v==='desktop');
|
||||||
document.getElementById('btn-mob').classList.toggle('on',v==='mobile');
|
document.getElementById('btn-mob').classList.toggle('on',v==='mobile');
|
||||||
document.getElementById('viewport').className=v;
|
document.getElementById('viewport').className=v;
|
||||||
render();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -461,7 +450,7 @@ document.addEventListener('keydown',e=>{
|
|||||||
// Viewport shortcuts (no modifiers)
|
// Viewport shortcuts (no modifiers)
|
||||||
if(e.key==='d'&&!e.ctrlKey&&!e.metaKey&&!e.altKey){ setViewport('desktop'); return; }
|
if(e.key==='d'&&!e.ctrlKey&&!e.metaKey&&!e.altKey){ setViewport('desktop'); return; }
|
||||||
if(e.key==='m'&&!e.ctrlKey&&!e.metaKey&&!e.altKey){ setViewport('mobile'); return; }
|
if(e.key==='m'&&!e.ctrlKey&&!e.metaKey&&!e.altKey){ setViewport('mobile'); return; }
|
||||||
// 'p' to toggle floating panel
|
// 'p' to toggle the side panel
|
||||||
if(e.key==='p'&&!e.ctrlKey&&!e.metaKey&&!e.altKey){ togglePanel(); return; }
|
if(e.key==='p'&&!e.ctrlKey&&!e.metaKey&&!e.altKey){ togglePanel(); return; }
|
||||||
|
|
||||||
// ← → cycle focus between Theme / Template / Client selects
|
// ← → cycle focus between Theme / Template / Client selects
|
||||||
@@ -493,6 +482,37 @@ function fillSelect(id, entries, selected) {
|
|||||||
select.value=selected;
|
select.value=selected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stable native names also cover browsers with incomplete ICU locale data.
|
||||||
|
const LOCALE_NAMES = {
|
||||||
|
'cs-CZ':'čeština (Česko)', 'de-DE':'Deutsch (Deutschland)',
|
||||||
|
'el-GR':'Ελληνικά (Ελλάδα)', 'en-US':'English (United States)',
|
||||||
|
'es-ES':'español (España)', 'fa-IR':'فارسی (ایران)',
|
||||||
|
'fi-FI':'suomi (Suomi)', 'fr-FR':'français (France)',
|
||||||
|
'ga-IE':'Gaeilge (Éire)', 'hu-HU':'magyar (Magyarország)',
|
||||||
|
'id-ID':'Indonesia (Indonesia)', 'is-IS':'íslenska (Ísland)',
|
||||||
|
'it-IT':'italiano (Italia)', 'ja-JP':'日本語 (日本)',
|
||||||
|
'ko-KR':'한국어 (대한민국)', 'lv-LV':'latviešu (Latvija)',
|
||||||
|
'nl-NL':'Nederlands (Nederland)', 'pl-PL':'polski (Polska)',
|
||||||
|
'pt-BR':'português (Brasil)', 'pt-PT':'português (Portugal)',
|
||||||
|
'ru-RU':'русский (Россия)', 'si-LK':'සිංහල (ශ්රී ලංකාව)',
|
||||||
|
'sk-SK':'slovenčina (Slovensko)',
|
||||||
|
'sv-SE':'svenska (Sverige)', 'tr-TR':'Türkçe (Türkiye)',
|
||||||
|
'uk-UA':'українська (Україна)', 'zh-CN':'中文 (简体,中国)',
|
||||||
|
'zh-TW':'中文 (繁體,台灣)'
|
||||||
|
};
|
||||||
|
|
||||||
|
function localeLabel(locale) {
|
||||||
|
let name = LOCALE_NAMES[locale];
|
||||||
|
if (!name) {
|
||||||
|
try {
|
||||||
|
if (Intl.DisplayNames.supportedLocalesOf([locale]).length) {
|
||||||
|
name = new Intl.DisplayNames([locale], {type:'language', fallback:'none'}).of(locale);
|
||||||
|
}
|
||||||
|
} catch (_) { /* Unknown languages remain selectable by their locale code. */ }
|
||||||
|
}
|
||||||
|
return name && name !== locale ? locale + ' — ' + name : locale;
|
||||||
|
}
|
||||||
|
|
||||||
function applyManifest(data) {
|
function applyManifest(data) {
|
||||||
if (!data || data.schema !== 1 || !data.languages.length || !data.themes.length) {
|
if (!data || data.schema !== 1 || !data.languages.length || !data.themes.length) {
|
||||||
throw new Error('Invalid preview manifest; regenerate with go run . preview all');
|
throw new Error('Invalid preview manifest; regenerate with go run . preview all');
|
||||||
@@ -503,16 +523,7 @@ function applyManifest(data) {
|
|||||||
if (!data.languages.includes(state.locale)) state.locale=data.languages.includes('en-US') ? 'en-US' : data.languages[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-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);
|
fillSelect('sel-template',Object.entries(REGISTRY).map(([id,t])=>[id,t.name]),state.template);
|
||||||
fillSelect('sel-locale',data.languages.map(locale=>{
|
fillSelect('sel-locale',data.languages.map(locale=>[locale,localeLabel(locale)]),state.locale);
|
||||||
let name;
|
|
||||||
try {
|
|
||||||
// Show each language in its own language, independent of the browser UI.
|
|
||||||
if (Intl.DisplayNames.supportedLocalesOf([locale]).length) {
|
|
||||||
name=new Intl.DisplayNames([locale],{type:'language',fallback:'none'}).of(locale);
|
|
||||||
}
|
|
||||||
} catch (_) { /* Older browsers can still select languages by locale code. */ }
|
|
||||||
return [locale,name && name!==locale ? locale+' — '+name : locale];
|
|
||||||
}),state.locale);
|
|
||||||
document.querySelectorAll('#sel-locale option').forEach(option=>{
|
document.querySelectorAll('#sel-locale option').forEach(option=>{
|
||||||
option.lang=option.value;
|
option.lang=option.value;
|
||||||
option.dir='auto';
|
option.dir='auto';
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ func TestSnapshotAndKeys(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if s.Lock.Tag != "v28.0.0" || len(s.Templates) != 13 || len(s.Locales) != 28 {
|
if s.Lock.Tag != "v28.1.0" || len(s.Templates) != 13 || len(s.Locales) != 28 {
|
||||||
t.Fatalf("unexpected baseline %s", s.Lock.Tag)
|
t.Fatalf("unexpected baseline %s", s.Lock.Tag)
|
||||||
}
|
}
|
||||||
keys, err := ReferencedKeys(s)
|
keys, err := ReferencedKeys(s)
|
||||||
|
|||||||
Reference in new issue
Block a user