chore: refresh docs and adapt workflows for Gitea
Release / Validate Templates (push) Successful in 3m1s
Release / Package & Release (push) Skipped
Release / Update Latest Release Documentation (push) Skipped

This commit is contained in:
KenanZhu committed 2026-10-09 22:02:33 +08:00
1 parent fec3ace600
commit f4d96de79e
14 files changed
+986 -505

No files matched your search

+10 -16
View File
@@ -1,22 +1,16 @@
> **Correction (2026-09-23):** v1.27.2 did not fix every push notification template. Bloom, Ember, and Heritage still used the old commit ID path and can fail to render push notifications on Gitea 1.27.1+. Use v1.27.3 for the fix.
# v1.27.2
FEATURES
> **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).
- THEMES (Aurora)
- Add `email-text` class to release template paragraphs for consistent typography with the rest of the theme
## Changes
BUGFIXES
- 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.
- THEMES
- Update push commit data paths in seven themes for Gitea 1.27.1 compatibility (`.ID` → `.UserCommit.GitCommit.ID`); see the correction above for the three remaining themes
- Replace `rgba()` colors with `#RRGGBBAA` hex notation across all 10 themes
## Documentation
DOCS
- Record compatibility for Gitea 1.27.0, 1.27.1 and 1.27.2.
- Maintain English and Simplified Chinese README and contributor guides.
- Track Gitea 1.27.0, 1.27.1, and 1.27.2 compatibility
- Keep English and Simplified Chinese README and CONTRIBUTING files
---
**Full Changelog:** [v1.0.1...v1.27.2](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/compare/v1.0.1...v1.27.2)
[Full changelog: v1.0.1…v1.27.2](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/compare/v1.0.1...v1.27.2)
+12 -14
View File
@@ -1,22 +1,20 @@
FEATURES
# v1.27.3
- RELEASE
- Include English and Simplified Chinese README and CONTRIBUTING files in the release archives
- Use reviewed release notes in the publishing workflow
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.
BUGFIXES
## Fixes
- THEMES (Bloom, Ember, Heritage)
- Fix pull request push notification commit links and abbreviated hashes for Gitea 1.27.1–1.27.3 by reading `.UserCommit.GitCommit.ID`
- CI
- Add a push notification regression test covering all 10 themes and run Go tests before packaging a release
- 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.
DOCS
## Packaging
- Verify compatibility with Gitea 1.27.3; its mail templates, mailer, and locale files are unchanged from 1.27.2
- Correct the compatibility matrix to distinguish the affected v1.27.2 archive from this fixed release
- 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)
[Full changelog: v1.27.2…v1.27.3](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/compare/v1.27.2...v1.27.3)
+11 -14
View File
@@ -1,21 +1,18 @@
FEATURES
# v28.0.0
- COMPATIBILITY
- Add a Gitea 28.0.0-aligned release of all 10 themes and 11 mail types per theme
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).
BUGFIXES
## Fixes
- THEMES (All 10)
- Replace the removed `FileSize` function with `FormatByteSize` in release-attachment emails, preventing Gitea 28.0.0 template rendering errors
- PREVIEW AND TESTS
- Match the preview's file-size function to Gitea 28.0.0 and render release attachments in the example data
- Add an all-theme regression test for release attachments and the removed function
- 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.
DOCS
## Documentation
- Update the English and Simplified Chinese READMEs and compatibility matrix for Gitea 28.0.0
- Clarify that v28.0.0 requires Gitea 28.0.0; use v1.27.3 for Gitea 1.25.0–1.27.3
- 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)
[Full changelog: v1.27.3…v28.0.0](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/compare/v1.27.3...v28.0.0)
+152
View File
@@ -0,0 +1,152 @@
"""Gitea API operations for the tracker and release workflows (Python stdlib only)."""
import argparse
import json
import os
from pathlib import Path
import re
import subprocess
from urllib.error import HTTPError
from urllib.parse import quote, urlencode, urlsplit
from urllib.request import HTTPRedirectHandler, Request, build_opener
class NoRedirects(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
# Never forward an instance token to a redirect destination.
return None
class GiteaAPI:
def __init__(self, server, repository, token):
parsed = urlsplit(server)
if parsed.scheme != "https" or not parsed.netloc or parsed.username or parsed.password or parsed.query or parsed.fragment:
raise ValueError("GITEA_SERVER_URL must be an HTTPS instance URL")
if not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", repository):
raise ValueError("GITEA_REPOSITORY must be owner/repository")
if not token:
raise ValueError("GITEA_TOKEN is required")
self.base = server.rstrip("/") + "/api/v1/repos/" + repository
self.token = token
self.opener = build_opener(NoRedirects())
def request(self, method, path, data=None, content_type="application/json", allow_missing=False):
if data is not None and not isinstance(data, bytes):
data = json.dumps(data).encode("utf-8")
req = Request(self.base + path, data=data, method=method, headers={
"Authorization": "token " + self.token,
"Accept": "application/json",
"Content-Type": content_type,
"User-Agent": "GiteaMailTemplates-actions",
})
try:
with self.opener.open(req, timeout=120) as response:
return json.load(response)
except HTTPError as error:
status = error.code
error.close()
if allow_missing and status == 404:
return None
raise RuntimeError(f"Gitea API {method} {path}: HTTP {status}") from None
def git(*args):
return subprocess.check_output(["git", *args], text=True).strip()
def create_pull_request(api, version):
if not re.fullmatch(r"\d+\.\d+\.\d+", version):
raise ValueError("Expected a stable X.Y.Z upstream version")
branch = "track/gitea-" + version
title = f"Track Gitea {version} compatibility"
changed = git("diff", "--name-only", "HEAD", "--").splitlines()
if any(not name.endswith(".md") for name in changed):
raise ValueError("Tracker may commit only Markdown changes")
if not changed:
print("[PASS] No tracked documentation changes")
return
# Repeated scheduled runs can find a branch from an earlier pending PR.
# Reuse identical content, but never force-push over a changed branch.
remote = git("ls-remote", "--heads", "origin", "refs/heads/" + branch)
if remote:
git("fetch", "origin", "refs/heads/" + branch)
if git("diff", "--name-only", "FETCH_HEAD", "--"):
raise ValueError(f"Existing {branch} differs; review it before updating the tracking PR")
else:
git("switch", "-c", branch)
git("add", "--", *changed)
git("-c", "user.name=release-bot", "-c", "user.email=release-bot@users.noreply.local",
"commit", "-m", f"docs: track Gitea {version} pending verification")
git("push", "origin", "HEAD:refs/heads/" + branch)
page = 1
while True:
pulls = api.request("GET", f"/pulls?state=open&base_branch=main&limit=50&page={page}")
for pull in pulls:
if (pull["head"]["ref"] == branch and pull["base"]["ref"] == "main"
and pull["head"]["repo"]["full_name"] == pull["base"]["repo"]["full_name"]):
print(f"[PASS] Tracking PR already exists: {pull['html_url']}")
return
if not pulls:
break
page += 1
body = f"""Gitea **{version}** is recorded as [PENDING]. Verified/tested versions remain unchanged.
- [ ] Review upstream mail templates, mailer data, functions and translation keys.
- [ ] Run `go test ./...` and `go run . preview all` from `tools/`.
- [ ] Record results in `COMPATIBILITY.md`; update verified status only after testing.
- [ ] Release template changes separately with reviewed notes and assets.
"""
pull = api.request("POST", "/pulls", {"title": title, "head": branch, "base": "main", "body": body})
print(f"[PASS] Created tracking PR: {pull['html_url']}")
def publish_release(api, version, root=Path(".")):
if not re.fullmatch(r"v\d+\.\d+\.\d+", version):
raise ValueError("Expected a stable vX.Y.Z release tag")
root = Path(root)
lock = json.loads((root / "gitea.lock.json").read_text(encoding="utf-8"))
if lock["tag"] != version:
raise ValueError("Release tag must match gitea.lock.json")
notes = (root / ".github" / "release-notes" / (version + ".md")).read_text(encoding="utf-8")
if not notes.strip():
raise ValueError("Release notes are empty")
assets = [root / "dist" / ("gitea-mail-templates-" + version + ext) for ext in (".zip", ".tar.gz")]
for asset in assets:
if asset.is_symlink() or not asset.is_file() or not asset.stat().st_size:
raise ValueError(f"Missing or invalid release archive: {asset}")
existing = api.request("GET", "/releases/tags/" + quote(version, safe=""), allow_missing=True)
if existing is not None:
raise ValueError("Release already exists; refusing to replace its notes or assets")
release = api.request("POST", "/releases", {
"tag_name": version, "name": version, "body": notes, "draft": True, "prerelease": False,
})
release_id = int(release["id"])
# Gitea accepts raw attachment data with the filename in the query string.
# Publish only after both uploads succeed; failures leave a draft for review.
for asset in assets:
api.request("POST", f"/releases/{release_id}/assets?" + urlencode({"name": asset.name}),
asset.read_bytes(), content_type="application/octet-stream")
release = api.request("PATCH", f"/releases/{release_id}", {"draft": False})
print(f"[PASS] Published {release['html_url']}")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("operation", choices=("create-pull-request", "publish-release"))
parser.add_argument("--version", required=True)
args = parser.parse_args()
try:
api = GiteaAPI(os.environ.get("GITEA_SERVER_URL", ""), os.environ.get("GITEA_REPOSITORY", ""), os.environ.get("GITEA_TOKEN", ""))
if args.operation == "create-pull-request":
create_pull_request(api, args.version)
else:
publish_release(api, args.version)
except (ValueError, KeyError, OSError, RuntimeError, subprocess.CalledProcessError) as error:
parser.exit(1, f"[FAIL] {error}\n")
if __name__ == "__main__":
main()
+160
View File
@@ -0,0 +1,160 @@
"""Offline Gitea API and disposable-Git workflow regression tests."""
import importlib.util
import json
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
from unittest.mock import Mock
from urllib.error import HTTPError
SPEC = importlib.util.spec_from_file_location("gitea_actions", Path(__file__).with_name("gitea_actions.py"))
ACTIONS = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(ACTIONS)
class APITests(unittest.TestCase):
def test_instance_url_token_and_raw_asset_request(self):
api = ACTIONS.GiteaAPI("https://git.example/subpath/", "owner/repo", "test-token")
response = Mock()
response.__enter__ = Mock(return_value=response)
response.__exit__ = Mock(return_value=False)
response.read.return_value = b'{"id": 1}'
api.opener = Mock()
api.opener.open.return_value = response
self.assertEqual({"id": 1}, api.request("POST", "/releases/1/assets?name=test.zip", b"archive", "application/octet-stream"))
request = api.opener.open.call_args.args[0]
self.assertEqual("https://git.example/subpath/api/v1/repos/owner/repo/releases/1/assets?name=test.zip", request.full_url)
self.assertEqual("token test-token", request.get_header("Authorization"))
self.assertEqual(b"archive", request.data)
self.assertEqual("application/octet-stream", request.get_header("Content-type"))
def test_only_explicit_404_is_missing_and_redirects_are_refused(self):
api = ACTIONS.GiteaAPI("https://git.example", "owner/repo", "test-token")
api.opener = Mock()
for status in (401, 403, 500, 302):
api.opener.open.side_effect = HTTPError(api.base, status, "error", {}, None)
with self.assertRaisesRegex(RuntimeError, f"HTTP {status}"):
api.request("GET", "/releases/tags/v28.0.0", allow_missing=True)
api.opener.open.side_effect = HTTPError(api.base, 404, "missing", {}, None)
self.assertIsNone(api.request("GET", "/releases/tags/v28.0.0", allow_missing=True))
self.assertIsNone(ACTIONS.NoRedirects().redirect_request(None, None, 302, "", {}, "https://elsewhere.example"))
def test_rejects_invalid_configuration(self):
for server, repository, token in [("http://git.example", "owner/repo", "x"),
("https://user:password@git.example", "owner/repo", "x"),
("https://git.example", "../owner/repo", "x"), ("https://git.example", "owner/repo", "")]:
with self.assertRaises(ValueError):
ACTIONS.GiteaAPI(server, repository, token)
class ReleaseTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
(self.root / "gitea.lock.json").write_text(json.dumps({"tag": "v28.0.0"}))
notes = self.root / ".github/release-notes/v28.0.0.md"
notes.parent.mkdir(parents=True)
notes.write_text("Reviewed notes", encoding="utf-8")
(self.root / "dist").mkdir()
for ext in (".zip", ".tar.gz"):
(self.root / "dist" / ("gitea-mail-templates-v28.0.0" + ext)).write_bytes(b"archive")
def test_existing_release_is_not_modified(self):
api = Mock()
api.request.return_value = {"id": 5}
with self.assertRaisesRegex(ValueError, "already exists"):
ACTIONS.publish_release(api, "v28.0.0", self.root)
self.assertEqual(["GET"], [call.args[0] for call in api.request.call_args_list])
def test_publish_only_after_both_uploads_succeed(self):
api = Mock()
api.request.side_effect = [None, {"id": 5}, {"id": 6}, {"id": 7}, {"html_url": "https://git.example/release"}]
ACTIONS.publish_release(api, "v28.0.0", self.root)
calls = api.request.call_args_list
self.assertEqual(["GET", "POST", "POST", "POST", "PATCH"], [c.args[0] for c in calls])
self.assertTrue(calls[1].args[2]["draft"])
self.assertEqual("Reviewed notes", calls[1].args[2]["body"])
self.assertEqual("/releases/5/assets?name=gitea-mail-templates-v28.0.0.zip", calls[2].args[1])
self.assertEqual("/releases/5/assets?name=gitea-mail-templates-v28.0.0.tar.gz", calls[3].args[1])
self.assertEqual({"draft": False}, calls[4].args[2])
def test_failed_upload_leaves_draft_unpublished(self):
api = Mock()
api.request.side_effect = [None, {"id": 5}, RuntimeError("upload failed")]
with self.assertRaisesRegex(RuntimeError, "upload failed"):
ACTIONS.publish_release(api, "v28.0.0", self.root)
self.assertNotIn("PATCH", [c.args[0] for c in api.request.call_args_list])
def test_bad_version_or_missing_archive_fails_before_api_call(self):
api = Mock()
for version in ("v28.0.1", "v28.0.0-rc1", "../../x"):
with self.assertRaises(ValueError):
ACTIONS.publish_release(api, version, self.root)
(self.root / "dist/gitea-mail-templates-v28.0.0.zip").unlink()
with self.assertRaisesRegex(ValueError, "archive"):
ACTIONS.publish_release(api, "v28.0.0", self.root)
api.request.assert_not_called()
class PullRequestTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.previous = Path.cwd()
os.chdir(self.root)
self.addCleanup(os.chdir, self.previous)
subprocess.run(["git", "init", "--bare", "remote.git"], check=True, capture_output=True)
subprocess.run(["git", "init", "-b", "main", "work"], check=True, capture_output=True)
os.chdir(self.root / "work")
ACTIONS.git("config", "user.name", "test")
ACTIONS.git("config", "user.email", "test@example.invalid")
ACTIONS.git("remote", "add", "origin", str(self.root / "remote.git"))
Path("README.md").write_text("Baseline\n")
Path("source.txt").write_text("Source\n")
ACTIONS.git("add", ".")
ACTIONS.git("commit", "-m", "baseline")
ACTIONS.git("push", "origin", "main")
Path("README.md").write_text("Pending 28.1.0\n")
def test_create_then_reuse_branch_and_paginated_pr(self):
api = Mock()
api.request.side_effect = [[], {"html_url": "https://git.example/pulls/1"}]
ACTIONS.create_pull_request(api, "28.1.0")
self.assertEqual("track/gitea-28.1.0", ACTIONS.git("branch", "--show-current"))
payload = api.request.call_args.args[2]
self.assertEqual("main", payload["base"])
self.assertEqual("track/gitea-28.1.0", payload["head"])
self.assertIn("[PENDING]", payload["body"])
original_head = ACTIONS.git("rev-parse", "HEAD")
ACTIONS.git("switch", "main")
Path("README.md").write_text("Pending 28.1.0\n")
api.reset_mock()
api.request.side_effect = [[{"head": {"ref": "unrelated"}}], [{
"head": {"ref": "track/gitea-28.1.0", "repo": {"full_name": "owner/repo"}},
"base": {"ref": "main", "repo": {"full_name": "owner/repo"}},
"html_url": "https://git.example/pulls/1",
}]]
ACTIONS.create_pull_request(api, "28.1.0")
self.assertEqual(["GET", "GET"], [c.args[0] for c in api.request.call_args_list])
self.assertEqual(original_head, ACTIONS.git("rev-parse", "FETCH_HEAD"))
Path("README.md").write_text("Different pending content\n")
with self.assertRaisesRegex(ValueError, "Existing.*differs"):
ACTIONS.create_pull_request(api, "28.1.0")
def test_non_documentation_change_is_not_committed(self):
Path("source.txt").write_text("Changed source\n")
api = Mock()
with self.assertRaisesRegex(ValueError, "only Markdown"):
ACTIONS.create_pull_request(api, "28.1.0")
api.request.assert_not_called()
self.assertEqual("baseline", ACTIONS.git("log", "-1", "--format=%s"))
if __name__ == "__main__":
unittest.main()
+12 -15
View File
@@ -12,12 +12,15 @@ on:
jobs:
track:
name: Track Gitea Release
runs-on: ubuntu-latest
runs-on: linux-amd64-docker-small
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
ref: main
token: ${{ secrets.GITEA_TOKEN }}
- uses: actions/setup-python@v5
with:
@@ -31,20 +34,14 @@ jobs:
run: python -B .github/scripts/track_gitea_release.py
env:
GITEA_VERSION: ${{ github.event.inputs.version }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Optional GitHub token for upstream API rate limits; never use the Gitea job token.
GITHUB_TOKEN: ${{ secrets.UPSTREAM_GITHUB_TOKEN }}
- name: Create Pull Request
if: steps.version.outputs.changed == 'true'
uses: peter-evans/create-pull-request@v7
with:
add-paths: '**/*.md'
branch: track/gitea-${{ steps.version.outputs.version }}
commit-message: "docs: track Gitea ${{ steps.version.outputs.version }} pending verification"
title: "Track Gitea ${{ steps.version.outputs.version }} compatibility"
body: |
Gitea **${{ steps.version.outputs.version }}** is recorded as [PENDING] across the marked documentation. Verified/tested ranges remain unchanged.
- [ ] Review upstream mail templates, mailer data, functions, and translation keys.
- [ ] Run `cd tools && go test ./...` and `go run . preview all`.
- [ ] Record findings in `COMPATIBILITY.md`; update tested ranges and status text only after verification.
- [ ] If template changes are needed, release them separately with reviewed notes and assets.
run: python -B .github/scripts/gitea_actions.py create-pull-request --version "$UPSTREAM_VERSION"
env:
UPSTREAM_VERSION: ${{ steps.version.outputs.version }}
GITEA_SERVER_URL: ${{ github.server_url }}
GITEA_REPOSITORY: ${{ github.repository }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
+10 -13
View File
@@ -8,7 +8,7 @@ on:
jobs:
validate:
name: Validate Templates
runs-on: ubuntu-latest
runs-on: linux-amd64-docker-small
steps:
- uses: actions/checkout@v4
@@ -41,13 +41,13 @@ jobs:
name: Package & Release
if: startsWith(github.ref, 'refs/tags/v')
needs: validate
runs-on: ubuntu-latest
runs-on: linux-amd64-docker-small
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN }}
token: ${{ secrets.GITEA_TOKEN }}
- uses: actions/setup-go@v5
with:
@@ -79,28 +79,25 @@ jobs:
TEMPLATE_RELEASE: ${{ github.ref_name }}
- name: Create Release
uses: softprops/action-gh-release@v3
with:
files: |
dist/*.zip
dist/*.tar.gz
fail_on_unmatched_files: true
body_path: .github/release-notes/${{ github.ref_name }}.md
run: python -B .github/scripts/gitea_actions.py publish-release --version "$TEMPLATE_RELEASE"
env:
GITHUB_TOKEN: ${{ secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN }}
TEMPLATE_RELEASE: ${{ github.ref_name }}
GITEA_SERVER_URL: ${{ github.server_url }}
GITEA_REPOSITORY: ${{ github.repository }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
sync-release-docs:
name: Update Latest Release Documentation
if: startsWith(github.ref, 'refs/tags/v')
needs: package
runs-on: ubuntu-latest
runs-on: linux-amd64-docker-small
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: main
token: ${{ secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN }}
token: ${{ secrets.GITEA_TOKEN }}
- uses: actions/setup-python@v5
with: