mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-29 03:18:14 +08:00
ci: preserve manual release summaries and raw changelog
This commit is contained in:
128
.github/workflows/build-v3.yml
vendored
128
.github/workflows/build-v3.yml
vendored
@@ -240,106 +240,9 @@ jobs:
|
||||
type=gha,scope=moviepilot-v3t-docker-arm64,version=2
|
||||
cache-to: type=gha,scope=moviepilot-v3t-docker,mode=max,version=2
|
||||
|
||||
- name: Generate Changelog
|
||||
id: changelog
|
||||
run: |
|
||||
# 获取上一个 tag(优先 v3.*,回退到任意 v* 版本 tag)
|
||||
PREVIOUS_TAG=$(git tag -l 'v3.*' --sort=-v:refname | grep -v "^v${{ env.app_version }}$" | head -n 1)
|
||||
if [ -z "$PREVIOUS_TAG" ]; then
|
||||
PREVIOUS_TAG=$(git tag -l 'v[0-9]*' --sort=-v:refname | grep -v "^v${{ env.app_version }}$" | head -n 1)
|
||||
fi
|
||||
echo "Previous tag: $PREVIOUS_TAG"
|
||||
|
||||
# 使用 || 作为分隔符,同时获取 commit 消息和作者 GitHub 用户名
|
||||
if [ -z "$PREVIOUS_TAG" ]; then
|
||||
# 首次发布且无任何历史版本 tag,限制条数避免打印整库历史撑爆环境变量
|
||||
COMMITS=$(git log --pretty=format:"%s||%an" -n 300 "${SOURCE_COMMIT}")
|
||||
else
|
||||
COMMITS=$(git log --pretty=format:"%s||%an" "${PREVIOUS_TAG}..${SOURCE_COMMIT}")
|
||||
fi
|
||||
|
||||
# 分类收集 commit 消息(使用关联数组去重)
|
||||
declare -A SEEN
|
||||
FEATURES=""
|
||||
FIXES=""
|
||||
OTHERS=""
|
||||
|
||||
while IFS= read -r line; do
|
||||
# 跳过空行
|
||||
if [ -z "$line" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# 分离 commit 消息和作者
|
||||
msg=$(echo "$line" | sed 's/||[^|]*$//')
|
||||
author=$(echo "$line" | sed 's/.*||//')
|
||||
|
||||
# 跳过 Merge commit 和版本更新 commit
|
||||
if echo "$msg" | grep -qE "^Merge pull request|^Merge branch|^更新 version"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# 按 Conventional Commits 前缀分类
|
||||
if echo "$msg" | grep -qiE "^feat(\(.+\))?:"; then
|
||||
desc=$(echo "$msg" | sed -E 's/^feat(\([^)]*\))?:\s*//')
|
||||
category="FEATURES"
|
||||
elif echo "$msg" | grep -qiE "^fix(\(.+\))?:"; then
|
||||
desc=$(echo "$msg" | sed -E 's/^fix(\([^)]*\))?:\s*//')
|
||||
category="FIXES"
|
||||
elif echo "$msg" | grep -qiE "^(docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?:"; then
|
||||
desc=$(echo "$msg" | sed -E 's/^(docs|style|refactor|perf|test|build|ci|chore|revert)(\([^)]*\))?:\s*//')
|
||||
category="OTHERS"
|
||||
else
|
||||
desc="$msg"
|
||||
category="OTHERS"
|
||||
fi
|
||||
|
||||
# 使用 "分类+描述" 作为去重的 key,跳过重复内容
|
||||
dedup_key="${category}::${desc}"
|
||||
if [ -n "${SEEN[$dedup_key]+x}" ]; then
|
||||
continue
|
||||
fi
|
||||
SEEN[$dedup_key]=1
|
||||
|
||||
# 添加 by @author 引用
|
||||
entry="- ${desc} by @${author}"
|
||||
|
||||
case "$category" in
|
||||
FEATURES) FEATURES="${FEATURES}${entry}\n" ;;
|
||||
FIXES) FIXES="${FIXES}${entry}\n" ;;
|
||||
OTHERS) OTHERS="${OTHERS}${entry}\n" ;;
|
||||
esac
|
||||
done <<< "$COMMITS"
|
||||
|
||||
# 组装 changelog
|
||||
CHANGELOG=""
|
||||
|
||||
if [ -n "$FEATURES" ]; then
|
||||
CHANGELOG="${CHANGELOG}### ✨ 新功能\n\n${FEATURES}\n"
|
||||
fi
|
||||
|
||||
if [ -n "$FIXES" ]; then
|
||||
CHANGELOG="${CHANGELOG}### 🐛 修复\n\n${FIXES}\n"
|
||||
fi
|
||||
|
||||
if [ -n "$OTHERS" ]; then
|
||||
CHANGELOG="${CHANGELOG}### 🔧 其他\n\n${OTHERS}\n"
|
||||
fi
|
||||
|
||||
# 添加版本对比链接
|
||||
if [ -n "$PREVIOUS_TAG" ]; then
|
||||
CHANGELOG="${CHANGELOG}**完整更新记录**: https://github.com/${{ github.repository }}/compare/${PREVIOUS_TAG}...v${{ env.app_version }}"
|
||||
fi
|
||||
|
||||
# 写入环境变量
|
||||
echo "CHANGELOG<<EOF" >> $GITHUB_ENV
|
||||
echo -e "$CHANGELOG" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Get existing release body
|
||||
id: get_release_body
|
||||
env:
|
||||
CHANGELOG: ${{ env.CHANGELOG }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
tag_name="v${{ env.app_version }}"
|
||||
@@ -364,20 +267,25 @@ jobs:
|
||||
fi
|
||||
echo "exists=$release_exists" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# 如果已有手动编写的 release body,则保留;否则使用自动生成的 changelog
|
||||
if [ -n "$release_body" ] && [ "$release_body" != "null" ] && [ "$release_body" != "" ]; then
|
||||
{
|
||||
echo "RELEASE_BODY<<EOF"
|
||||
echo "$release_body"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_ENV"
|
||||
else
|
||||
{
|
||||
echo "RELEASE_BODY<<EOF"
|
||||
echo "$CHANGELOG"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_ENV"
|
||||
# 随机分隔符避免手工正文中的 EOF 等独立行截断工作流环境变量。
|
||||
delimiter="RELEASE_NOTES_$(python3 -c 'import uuid; print(uuid.uuid4().hex)')"
|
||||
if [ "$release_body" = "null" ]; then
|
||||
release_body=""
|
||||
fi
|
||||
{
|
||||
printf 'RELEASE_BODY<<%s\n' "$delimiter"
|
||||
printf '%s\n' "$release_body"
|
||||
printf '%s\n' "$delimiter"
|
||||
} >> "$GITHUB_ENV"
|
||||
|
||||
- name: Generate Release Notes
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
python3 scripts/release_notes.py \
|
||||
--repository "$GITHUB_REPOSITORY" \
|
||||
--tag "v${{ env.app_version }}" \
|
||||
--source "$SOURCE_COMMIT"
|
||||
|
||||
- name: Publish Release Tag
|
||||
env:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Commit Conventions
|
||||
|
||||
This project uses **Conventional Commits**. The release workflow parses commit messages to categorize changelog entries. This is not stylistic — it is functional.
|
||||
This project uses **Conventional Commits**. Release notes preserve commit subjects without categorization or rewriting.
|
||||
|
||||
### Format
|
||||
|
||||
@@ -74,6 +74,20 @@ ci: improve docker build cache
|
||||
|
||||
---
|
||||
|
||||
## Release Notes
|
||||
|
||||
正式后端 Release 固定包含三个章节:`## 本次更新内容`、`## 完整变更记录`、`## 版本对比`。
|
||||
首个章节留空,由维护者在 GitHub Release 中手工填写;同版本重建保留该章节内容,仅刷新后两个自动章节。
|
||||
旧版手工正文迁入首个章节,旧版自动分类记录由新格式替换。
|
||||
|
||||
工作流通过 `scripts/release_notes.py` 收集上一同大版本正式标签到固定来源 SHA 的所有分页提交,
|
||||
保留原始标题、提交链接与真实 GitHub `@作者`,不分类、翻译或按标题合并。
|
||||
仅过滤 Merge 和构建生成的插件市场发布快照提交。未关联 GitHub 账号时展示 Git 作者名,不伪造账号。
|
||||
兼容历史 `v3.x.y-N` 数字修订标签;本版本、较高版本和命名预发布标签不会成为比较起点;首次发布无起点时版本对比章节留空。
|
||||
前端 Release 仅生成相同规则的 changelog 列表,不包含摘要、其它章节或 Compare 链接。
|
||||
|
||||
---
|
||||
|
||||
## CI/CD
|
||||
|
||||
- CI runs on every push and pull request. The pipeline typically includes:
|
||||
|
||||
112
scripts/release_notes.py
Normal file
112
scripts/release_notes.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""发布工作流使用的原始提交记录生成器,不分类或改写提交标题。"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib.parse import quote
|
||||
|
||||
|
||||
def command(*args: str) -> str:
|
||||
"""以参数列表执行发布工具;查询失败时终止,避免发布不完整记录。"""
|
||||
return subprocess.run(args, check=True, capture_output=True, text=True).stdout
|
||||
|
||||
|
||||
def previous_tag(tag: str) -> str | None:
|
||||
"""选取低于当前版本的最近正式标签,兼容历史数字修订后缀,排除本版本和命名预发布标签。"""
|
||||
version = re.fullmatch(r"v(\d+)\.(\d+)\.(\d+)(?:-(\d+))?", tag)
|
||||
if version is None:
|
||||
raise ValueError(f"不支持的正式版本标签:{tag}")
|
||||
current = tuple(int(part or 0) for part in version.groups())
|
||||
candidates = []
|
||||
for candidate in command("git", "tag", "--list", "v*").splitlines():
|
||||
match = re.fullmatch(r"v(\d+)\.(\d+)\.(\d+)(?:-(\d+))?", candidate)
|
||||
if match is not None:
|
||||
number = tuple(int(part or 0) for part in match.groups())
|
||||
if number[0] == current[0] and number < current:
|
||||
candidates.append((number, candidate))
|
||||
return max(candidates)[1] if candidates else None
|
||||
|
||||
|
||||
def collect_commits(repository: str, source: str, previous: str | None) -> list[dict[str, Any]]:
|
||||
"""分页读取指定发布范围的提交及真实 GitHub 作者账号,支持直接提交。"""
|
||||
if previous:
|
||||
endpoint = f"repos/{repository}/compare/{quote(previous, safe='')}...{source}?per_page=100"
|
||||
pages = json.loads(command("gh", "api", "--paginate", "--slurp", endpoint))
|
||||
commits = [commit for page in pages for commit in page["commits"]]
|
||||
else:
|
||||
endpoint = f"repos/{repository}/commits?sha={source}&per_page=100"
|
||||
pages = json.loads(command("gh", "api", "--paginate", "--slurp", endpoint))
|
||||
commits = [commit for page in pages for commit in page]
|
||||
commits.reverse()
|
||||
return commits
|
||||
|
||||
|
||||
def changelog(commits: list[dict[str, Any]]) -> str:
|
||||
"""保留提交标题、作者与链接,仅排除合并和自动发布快照,不合并同名提交。"""
|
||||
entries = []
|
||||
seen = set()
|
||||
for commit in commits:
|
||||
sha = commit["sha"]
|
||||
if sha in seen:
|
||||
continue
|
||||
seen.add(sha)
|
||||
subject = commit["commit"]["message"].splitlines()[0]
|
||||
if len(commit.get("parents", [])) > 1 or subject.startswith("Merge "):
|
||||
continue
|
||||
if subject.startswith("build(plugin-market): sync default from MoviePilot-Wiki@"):
|
||||
continue
|
||||
author = commit.get("author") or {}
|
||||
login = author.get("login")
|
||||
# 未关联 GitHub 账号的提交保留显示名,不伪造 @账号。
|
||||
attribution = f"@{login}" if login else commit["commit"]["author"]["name"]
|
||||
entries.append(f"- {subject} by {attribution} ([{sha[:7]}]({commit['html_url']}))")
|
||||
return "\n".join(entries)
|
||||
|
||||
|
||||
def manual_summary(body: str) -> str:
|
||||
"""保留手工摘要及内部 Markdown;旧版自动记录不迁入用户摘要。"""
|
||||
match = re.search(r"^## 本次更新内容[ \t]*\r?$", body, re.MULTILINE)
|
||||
if match:
|
||||
remaining = body[match.end():]
|
||||
boundary = re.search(r"^## (?:完整变更记录|版本对比)[ \t]*\r?$", remaining, re.MULTILINE)
|
||||
return remaining[:boundary.start()].strip() if boundary else remaining.strip()
|
||||
# 兼容已有手工 Release 正文,避免首次切换模板时丢失公告。
|
||||
legacy_generated = re.search(r"^### (?:✨ 新功能|🐛 修复|🔧 其他)", body, re.MULTILINE)
|
||||
return body[:legacy_generated.start()].strip() if legacy_generated else body.strip()
|
||||
|
||||
|
||||
def release_body(summary: str, records: str, repository: str, previous: str | None, tag: str) -> str:
|
||||
"""生成后端固定三章节;首版无比较起点时仅保留空的版本对比章节。"""
|
||||
comparison = ""
|
||||
if previous:
|
||||
url = f"https://github.com/{repository}/compare/{previous}...{tag}"
|
||||
comparison = f"[完整 Compare]({url})"
|
||||
return f"## 本次更新内容\n\n{summary}\n\n## 完整变更记录\n\n{records}\n\n## 版本对比\n\n{comparison}\n"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""向发布工作流传递完整正文;前端模式仅输出原始 changelog。"""
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--repository", required=True)
|
||||
parser.add_argument("--tag", required=True)
|
||||
parser.add_argument("--source", required=True)
|
||||
parser.add_argument("--changelog-only", action="store_true")
|
||||
args = parser.parse_args()
|
||||
previous = previous_tag(args.tag)
|
||||
records = changelog(collect_commits(args.repository, args.source, previous))
|
||||
body = records if args.changelog_only else release_body(
|
||||
manual_summary(os.environ.get("RELEASE_BODY", "")),
|
||||
records, args.repository, previous, args.tag,
|
||||
)
|
||||
delimiter = f"RELEASE_NOTES_{uuid.uuid4().hex}"
|
||||
with Path(os.environ["GITHUB_ENV"]).open("a", encoding="utf-8") as output:
|
||||
output.write(f"RELEASE_BODY<<{delimiter}\n{body}\n{delimiter}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
111
tests/test_release_notes.py
Normal file
111
tests/test_release_notes.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""发布记录的范围、作者、分页与手工摘要保护契约。"""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts import release_notes
|
||||
|
||||
|
||||
def make_commit(sha: str, subject: str, login: str | None = "contributor", parents: int = 1) -> dict:
|
||||
"""构造包含 GitHub 作者及原始 Git 显示名的提交响应。"""
|
||||
return {
|
||||
"sha": sha,
|
||||
"html_url": f"https://github.com/owner/repo/commit/{sha}",
|
||||
"parents": [{}] * parents,
|
||||
"author": {"login": login} if login else None,
|
||||
"commit": {"message": subject, "author": {"name": "Display Name"}},
|
||||
}
|
||||
|
||||
|
||||
def test_changelog_preserves_subjects_and_distinct_commits() -> None:
|
||||
"""不分类、翻译或按标题去重,真实账号与未关联作者分别展示。"""
|
||||
commits = [
|
||||
make_commit("a" * 40, "fix(scope): 原始标题\n详细正文"),
|
||||
make_commit("b" * 40, "fix(scope): 原始标题", login=None),
|
||||
make_commit("c" * 40, "Merge pull request #1", parents=2),
|
||||
make_commit("d" * 40, "合并提交", parents=2),
|
||||
make_commit("e" * 40, "build(plugin-market): sync default from MoviePilot-Wiki@123"),
|
||||
make_commit("f" * 40, "ci: improve pipeline"),
|
||||
]
|
||||
body = release_notes.changelog(commits + commits[:1])
|
||||
assert body.count("fix(scope): 原始标题") == 2
|
||||
assert "by @contributor" in body
|
||||
assert "by Display Name" in body
|
||||
assert "@Display Name" not in body
|
||||
assert "Merge" not in body and "合并提交" not in body
|
||||
assert "plugin-market" not in body
|
||||
assert "ci: improve pipeline" in body
|
||||
assert "详细正文" not in body
|
||||
|
||||
|
||||
def test_previous_tag_excludes_current_future_and_other_major(monkeypatch) -> None:
|
||||
"""同版本重打包仍从上一正式版计算,不受较高版本或预发布标签影响。"""
|
||||
monkeypatch.setattr(release_notes, "command", lambda *_: "v2.9.9\nv3.1.2\nv3.1.2-1\nv3.1.3\nv3.1.4\nv3.1.3-beta.1\n")
|
||||
assert release_notes.previous_tag("v3.1.3") == "v3.1.2-1"
|
||||
assert release_notes.previous_tag("v3.1.2-2") == "v3.1.2-1"
|
||||
assert release_notes.previous_tag("v3.0.0") is None
|
||||
|
||||
|
||||
def test_collect_commits_includes_every_page(monkeypatch) -> None:
|
||||
"""跨页发布记录包含直接提交,比较终点使用固定 SHA 而非可移动标签。"""
|
||||
calls = []
|
||||
|
||||
def fake_command(*args):
|
||||
"""模拟 GitHub Compare API 的多页响应。"""
|
||||
calls.append(args)
|
||||
return json.dumps([{"commits": [make_commit("a", "first")]}, {"commits": [make_commit("b", "second")]}])
|
||||
|
||||
monkeypatch.setattr(release_notes, "command", fake_command)
|
||||
commits = release_notes.collect_commits("owner/repo", "fixed-sha", "v3.1.2")
|
||||
assert [commit["sha"] for commit in commits] == ["a", "b"]
|
||||
assert calls == [("gh", "api", "--paginate", "--slurp", "repos/owner/repo/compare/v3.1.2...fixed-sha?per_page=100")]
|
||||
|
||||
|
||||
def test_first_release_collects_entire_history(monkeypatch) -> None:
|
||||
"""首次发布不凭空选择其它大版本,提交历史按时间正序输出。"""
|
||||
monkeypatch.setattr(release_notes, "command", lambda *_: json.dumps([[make_commit("b", "new")], [make_commit("a", "old")]]))
|
||||
commits = release_notes.collect_commits("owner/repo", "fixed-sha", None)
|
||||
assert [commit["sha"] for commit in commits] == ["a", "b"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("summary", ["", "- 手工功能说明\n\n### 注意事项\n\n原样保留 `代码` 与 @author", "说明\n\n## 手工子标题\n\n细节"])
|
||||
def test_rebuild_preserves_manual_summary_and_refreshes_records(summary: str) -> None:
|
||||
"""重建保留手工摘要,刷新自动章节,正文始终仅包含三个约定章节。"""
|
||||
old = release_notes.release_body(summary, "old records", "owner/repo", "v3.1.1", "v3.1.2")
|
||||
preserved = release_notes.manual_summary(old)
|
||||
new = release_notes.release_body(preserved, "new records", "owner/repo", "v3.1.1", "v3.1.2")
|
||||
assert preserved == summary
|
||||
assert "old records" not in new
|
||||
assert "new records" in new
|
||||
assert "https://github.com/owner/repo/compare/v3.1.1...v3.1.2" in new
|
||||
assert "贡献者" not in new
|
||||
|
||||
|
||||
def test_legacy_manual_body_is_preserved_but_generated_body_is_not_summary() -> None:
|
||||
"""兼容旧手工公告,避免把原有自动分类记录当作本次用户摘要。"""
|
||||
assert release_notes.manual_summary("手工公告\n- 新功能") == "手工公告\n- 新功能"
|
||||
assert release_notes.manual_summary("### ✨ 新功能\n\n- generated by @author") == ""
|
||||
assert release_notes.manual_summary("手工公告\n\n### ✨ 新功能\n- generated") == "手工公告"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("frontend", [False, True])
|
||||
def test_main_emits_workflow_body_without_evaluating_commit_text(tmp_path, monkeypatch, frontend) -> None:
|
||||
"""前端仅输出列表,后端保留手工内容;提交中的工作流分隔符不得截断正文。"""
|
||||
environment = tmp_path / "github.env"
|
||||
monkeypatch.setenv("GITHUB_ENV", str(environment))
|
||||
monkeypatch.setenv("RELEASE_BODY", "## 本次更新内容\n\n手工公告\n\n## 完整变更记录\nold")
|
||||
monkeypatch.setattr(release_notes, "previous_tag", lambda _: "v3.1.2")
|
||||
monkeypatch.setattr(release_notes, "collect_commits", lambda *_: [make_commit("a" * 40, "fix: $() EOF")])
|
||||
args = ["release_notes.py", "--repository", "owner/repo", "--tag", "v3.1.3", "--source", "fixed-sha"]
|
||||
if frontend:
|
||||
args.append("--changelog-only")
|
||||
monkeypatch.setattr("sys.argv", args)
|
||||
release_notes.main()
|
||||
content = environment.read_text(encoding="utf-8")
|
||||
delimiter = content.splitlines()[0].split("<<")[1]
|
||||
assert content.splitlines()[-1] == delimiter
|
||||
assert "fix: $() EOF by @contributor" in content
|
||||
assert ("## 本次更新内容" in content) is not frontend
|
||||
assert ("手工公告" in content) is not frontend
|
||||
assert ("## 版本对比" in content) is not frontend
|
||||
@@ -96,7 +96,6 @@ def _run_release_script(
|
||||
"GITHUB_REPOSITORY": "jxxghp/MoviePilot",
|
||||
"GITHUB_ENV": str(tmp_path / "github.env"),
|
||||
"GITHUB_OUTPUT": str(tmp_path / "github.output"),
|
||||
"CHANGELOG": "generated changelog",
|
||||
}
|
||||
)
|
||||
env.update(extra_env or {})
|
||||
@@ -270,7 +269,8 @@ def test_release_uses_github_cli_for_tag_and_release_lifecycle() -> None:
|
||||
("response", "exit_code", "expected_exists", "expected_body"),
|
||||
[
|
||||
("HTTP/2.0 200 OK\nHeader: value\n\nmanual body\n", 0, "true", "manual body"),
|
||||
("HTTP/2.0 404 Not Found\n\n", 1, "false", "generated changelog"),
|
||||
("HTTP/2.0 200 OK\n\nmanual\nEOF\nMORE=value\n", 0, "true", "manual\nEOF\nMORE=value"),
|
||||
("HTTP/2.0 404 Not Found\n\n", 1, "false", ""),
|
||||
],
|
||||
)
|
||||
def test_release_query_preserves_existing_body_or_handles_explicit_404(
|
||||
@@ -280,7 +280,7 @@ def test_release_query_preserves_existing_body_or_handles_explicit_404(
|
||||
expected_exists: str,
|
||||
expected_body: str,
|
||||
) -> None:
|
||||
"""已有 Release 保留正文,只有明确 404 才使用自动变更记录。"""
|
||||
"""已有 Release 保留正文,只有明确 404 才将正文初始化为空。"""
|
||||
script = _steps_by_name(_load_workflow())["Get existing release body"]["run"]
|
||||
script = script.replace("v${{ env.app_version }}", "v3.0.0")
|
||||
|
||||
@@ -291,6 +291,9 @@ def test_release_query_preserves_existing_body_or_handles_explicit_404(
|
||||
environment = (tmp_path / "github.env").read_text(encoding="utf-8")
|
||||
assert f"exists={expected_exists}" in output
|
||||
assert expected_body in environment
|
||||
delimiter = environment.splitlines()[0].split("<<")[1]
|
||||
assert delimiter.startswith("RELEASE_NOTES_")
|
||||
assert environment.splitlines()[-1] == delimiter
|
||||
|
||||
|
||||
def test_release_query_fails_closed_on_non_404_error(tmp_path: Path) -> None:
|
||||
|
||||
Reference in New Issue
Block a user