diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 254d03e..5b782d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: CI on: push: branches: [master] - tags: ['v*.*.*'] + tags: ['v*'] pull_request: workflow_dispatch: @@ -162,31 +162,3 @@ jobs: repository="$(printf '%s' "${GITHUB_REPOSITORY}" | tr '[:upper:]' '[:lower:]')" printf '%s' "$GITHUB_TOKEN" | helm registry login ghcr.io --username "$GITHUB_ACTOR" --password-stdin helm push "${RUNNER_TEMP}/helm-packages/alisten-${chart_version}.tgz" "oci://ghcr.io/${repository}/charts" - - release: - needs: helm-release - if: github.ref_type == 'tag' - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - uses: actions/setup-python@v6 - with: - python-version: '3.11' - - name: Generate release notes - shell: bash - run: | - set -euo pipefail - python scripts/generate_changelog.py --tag "$GITHUB_REF_NAME" --output release-notes.md - test -s release-notes.md - - name: Publish release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ github.ref_name }} - name: ${{ github.ref_name }} - body_path: release-notes.md - files: protocol/idl.json - fail_on_unmatched_files: true diff --git a/.github/workflows/release-assets.yml b/.github/workflows/release-assets.yml new file mode 100644 index 0000000..f6b8ab2 --- /dev/null +++ b/.github/workflows/release-assets.yml @@ -0,0 +1,37 @@ +name: Release assets + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: 要上传附件的 Release tag + required: true + type: string + +concurrency: + group: release-assets-${{ github.event.release.tag_name || inputs.tag }} + +permissions: + contents: read + +jobs: + idl: + runs-on: ubuntu-latest + permissions: + contents: write + env: + TAG: ${{ github.event.release.tag_name || inputs.tag }} + GH_REPO: ${{ github.repository }} + GH_TOKEN: ${{ github.token }} + steps: + - uses: actions/checkout@v7 + with: + ref: refs/tags/${{ env.TAG }} + - uses: actions/setup-go@v7 + with: + go-version-file: go.mod + - run: go run ./cmd/protocolgen -check + - run: gh release upload "$TAG" protocol/idl.json --clobber + - run: gh release download "$TAG" --pattern idl.json --output - | cmp - protocol/idl.json diff --git a/scripts/generate_changelog.py b/scripts/generate_changelog.py deleted file mode 100644 index 37c6c3e..0000000 --- a/scripts/generate_changelog.py +++ /dev/null @@ -1,222 +0,0 @@ -"""根据正式版本标签生成项目 Changelog。""" - -from __future__ import annotations - -import argparse -import json -import re -import subprocess -import sys -from collections import defaultdict -from dataclasses import dataclass -from pathlib import Path - -VERSION_TAG_PATTERN = re.compile(r"^v(\d+)\.(\d+)\.(\d+)$") -# type 与 emoji 之间的空格可有可无(\s*),同时兼容 "feat ✨:" 旧格式与 "feat✨:" 新格式 -COMMIT_PATTERN = re.compile( - r"^(?P[A-Za-z]+)(?:\s*[^\w\s:(]+)?" - r"(?:\((?P[^)]+)\))?\s*:\s*(?P.+)$" -) -GITHUB_NOREPLY_PATTERN = re.compile( - r"^(?:\d+\+)?(?P[^@]+)@users\.noreply\.github\.com$", - re.IGNORECASE, -) -DEFAULT_NICKNAME_MAP_PATH = Path(__file__).with_name("git-nickname-username.json") - -CATEGORY_BY_TYPE = { - "feat": "✨ Features", - "fix": "🐛 Bug Fixes", - "patch": "🐛 Bug Fixes", - "perf": "🚀 Performance Improvements", - "refactor": "🎨 Code Refactoring", - "docs": "📚 Documentation", - "deps": "🧩 Dependency Updates", - "build": "🧩 Dependency Updates", - "test": "🧪 Tests", - "ci": "⚙️ Continuous Integration", - "chore": "🧹 Maintenance", - "style": "🧹 Maintenance", - "format": "🧹 Maintenance", - "config": "🧹 Maintenance", -} -CATEGORY_ORDER = tuple(dict.fromkeys(CATEGORY_BY_TYPE.values())) + ("Other Changes",) - - -@dataclass(frozen=True) -class Commit: - subject: str - author_name: str - author_email: str - category: str - - -def run_git(*args: str) -> str: - result = subprocess.run( - ["git", *args], - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - encoding="utf-8", - ) - return result.stdout - - -def parse_version_tag(tag: str) -> tuple[int, int, int] | None: - match = VERSION_TAG_PATTERN.fullmatch(tag) - if not match: - return None - return int(match.group(1)), int(match.group(2)), int(match.group(3)) - - -def get_release_tags() -> list[str]: - tags = [line.strip() for line in run_git("tag", "--list").splitlines()] - versions = [ - (version, tag) - for tag in tags - if (version := parse_version_tag(tag)) is not None - ] - return [tag for _, tag in sorted(versions, reverse=True)] - - -def get_previous_tag(tag: str, tags: list[str]) -> str | None: - index = tags.index(tag) - return tags[index + 1] if index + 1 < len(tags) else None - - -def read_commit_records(tag: str, previous_tag: str | None) -> list[str]: - revision = f"{previous_tag}..{tag}" if previous_tag else tag - output = run_git( - "log", - "--no-merges", - "--format=%an%x1f%ae%x1f%s%x1e", - revision, - ) - return [record.strip() for record in output.split("\x1e") if record.strip()] - - -def load_nickname_map(path: Path = DEFAULT_NICKNAME_MAP_PATH) -> dict[str, str]: - if not path.exists(): - return {} - data = json.loads(path.read_text(encoding="utf-8")) - if not isinstance(data, dict): - raise ValueError(f"Git 昵称映射配置必须是 JSON 对象:{path}") - return { - name: username - for name, username in data.items() - if not name.startswith("$") and isinstance(username, str) - } - - -def format_author(name: str, email: str, nickname_map: dict[str, str]) -> str: - mapped_username = nickname_map.get(name.strip()) - if mapped_username: - return f"@{mapped_username}" - match = GITHUB_NOREPLY_PATTERN.fullmatch(email.strip()) - return f"@{match.group('username')}" if match else name.strip() - - -def parse_commit_record(record: str) -> Commit: - fields = record.split("\x1f", maxsplit=2) - if len(fields) != 3: - raise ValueError("Git 提交记录格式损坏") - - author_name, author_email, raw_subject = (field.strip() for field in fields) - match = COMMIT_PATTERN.fullmatch(raw_subject) - if not match: - return Commit(raw_subject, author_name, author_email, "Other Changes") - - commit_type = match.group("type").lower() - scope = match.group("scope") - message = match.group("message").strip() - subject = f"*({scope})* {message}" if scope else message - category = CATEGORY_BY_TYPE.get(commit_type, "Other Changes") - return Commit(subject, author_name, author_email, category) - - -def render_version(tag: str, tags: list[str], nickname_map: dict[str, str]) -> str: - if parse_version_tag(tag) is None: - raise ValueError(f"不是正式版本标签:{tag}") - if tag not in tags: - raise ValueError(f"正式版本标签不存在:{tag}") - - run_git("rev-parse", "--verify", f"refs/tags/{tag}") - previous_tag = get_previous_tag(tag, tags) - commits = [parse_commit_record(record) for record in read_commit_records(tag, previous_tag)] - grouped: dict[str, list[Commit]] = defaultdict(list) - for commit in commits: - grouped[commit.category].append(commit) - - date = run_git("log", "-1", "--format=%cs", tag).strip() - lines = [f"## {tag.removeprefix('v')} ({date})"] - for category in CATEGORY_ORDER: - category_commits = grouped.get(category) - if not category_commits: - continue - lines.extend(("", f"### {category}", "")) - lines.extend( - f"- {commit.subject} {format_author(commit.author_name, commit.author_email, nickname_map)}" - for commit in category_commits - ) - return "\n".join(lines) + "\n" - - -def render_unreleased(nickname_map: dict[str, str]) -> str: - """无 tag 时,将 HEAD 视为未发布版本,范围为全部提交。""" - commits = [parse_commit_record(record) for record in read_commit_records("HEAD", None)] - grouped: dict[str, list[Commit]] = defaultdict(list) - for commit in commits: - grouped[commit.category].append(commit) - - lines = ["## 未发布"] - for category in CATEGORY_ORDER: - category_commits = grouped.get(category) - if not category_commits: - continue - lines.extend(("", f"### {category}", "")) - lines.extend( - f"- {commit.subject} {format_author(commit.author_name, commit.author_email, nickname_map)}" - for commit in category_commits - ) - return "\n".join(lines) + "\n" - - -def render_changelog(tags: list[str], nickname_map: dict[str, str]) -> str: - if not tags: - return render_unreleased(nickname_map) - sections = [render_version(tag, tags, nickname_map).rstrip() for tag in tags] - return "# 更新日志\n\n" + "\n\n".join(sections) + "\n" - - -def parse_args(argv: list[str] | None) -> argparse.Namespace: - parser = argparse.ArgumentParser(description="根据正式版本标签生成项目 Changelog") - mode = parser.add_mutually_exclusive_group(required=True) - mode.add_argument("--tag", help="只生成指定正式版本的发布说明") - mode.add_argument("--all", action="store_true", help="生成全部正式版本的 Changelog") - parser.add_argument("--output", type=Path, help="将结果写入指定文件") - return parser.parse_args(argv) - - -def main(argv: list[str] | None = None) -> int: - args = parse_args(argv) - try: - tags = get_release_tags() - nickname_map = load_nickname_map() - content = ( - render_changelog(tags, nickname_map) - if args.all - else render_version(args.tag, tags, nickname_map) - ) - if args.output: - args.output.write_text(content, encoding="utf-8", newline="\n") - else: - sys.stdout.buffer.write(content.encode("utf-8")) - return 0 - except (subprocess.CalledProcessError, ValueError, OSError) as error: - detail = error.stderr.strip() if isinstance(error, subprocess.CalledProcessError) else str(error) - print(f"生成 Changelog 失败:{detail}", file=sys.stderr) - return 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/git-nickname-username.json b/scripts/git-nickname-username.json deleted file mode 100644 index 9ae4166..0000000 --- a/scripts/git-nickname-username.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$schema": "./git-nickname-username.schema.json", - "$comment": "Git 昵称到 GitHub 用户名的映射,用于 generate_changelog.py 脚本", - "azmiao": "azmiao", - "AZMIAO": "azmiao", - "DiheChen": "DiheChen", - "dependabot[bot]": "dependabot[bot]" -} diff --git a/scripts/git-nickname-username.schema.json b/scripts/git-nickname-username.schema.json deleted file mode 100644 index 8138b73..0000000 --- a/scripts/git-nickname-username.schema.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Git Nickname to GitHub Username Mapping", - "description": "Git 提交昵称到 GitHub 用户名的映射配置,用于 generate_changelog.py 正确生成 @username 链接", - "type": "object", - "properties": { - "$schema": { - "type": "string", - "description": "JSON Schema 引用" - }, - "$comment": { - "type": "string", - "description": "配置文件说明" - } - }, - "additionalProperties": { - "type": "string", - "description": "GitHub 用户名", - "pattern": "^[a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38}(\\[bot\\])?$" - }, - "examples": [ - { - "azmiao": "azmiao" - } - ] -}