mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-06 22:53:35 +08:00
✨ feat(update): 将更新日志写入静态清单并打通 CI 生成
- 扩展 latest.json / UpdateInfo 支持 releaseNotes 字段 - latest 发版将 changelog 同步写入清单与 GitHub Release - dev 构建按上一版基线自动生成相对更新日志 - 补充清单生成与后端映射测试
This commit is contained in:
143
.github/workflows/dev-build.yml
vendored
143
.github/workflows/dev-build.yml
vendored
@@ -1103,6 +1103,9 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
# dev 更新日志需要 prev..HEAD 提交范围
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
@@ -1231,26 +1234,6 @@ jobs:
|
||||
DEV_VERSION="dev-${SHORT_SHA}"
|
||||
echo "version=${DEV_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# 静态更新清单:dev 通道读
|
||||
# https://github.com/Syngnat/GoNavi/releases/download/dev-latest/latest-dev.json
|
||||
- name: Generate static update manifest (latest-dev.json)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
DEV_VERSION="${{ steps.version.outputs.version }}"
|
||||
python3 tools/generate-update-latest-manifest.py \
|
||||
--assets-dir release-assets \
|
||||
--version "$DEV_VERSION" \
|
||||
--tag dev-latest \
|
||||
--channel dev \
|
||||
--name "Dev Build (${DEV_VERSION})" \
|
||||
--download-base-url "https://download.syngnat.top/gonavi/dev/releases/download" \
|
||||
--download-tag "$DEV_VERSION" \
|
||||
--output release-assets/latest-dev.json
|
||||
test -s release-assets/latest-dev.json
|
||||
echo "📄 latest-dev.json ready:"
|
||||
head -n 40 release-assets/latest-dev.json
|
||||
|
||||
- name: Format Build Time
|
||||
id: build_time
|
||||
shell: bash
|
||||
@@ -1265,6 +1248,115 @@ jobs:
|
||||
print(f"display={formatted}")
|
||||
PY
|
||||
|
||||
# 解析上一版 dev 基线(线上 latest-dev.json 的 version → 短 SHA),用于生成相对变更日志。
|
||||
- name: Resolve previous dev baseline
|
||||
id: prev_dev
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PREV_SHA=""
|
||||
for url in \
|
||||
"https://download.syngnat.top/gonavi/dev/releases/latest/latest-dev.json" \
|
||||
"https://github.com/${{ github.repository }}/releases/download/dev-latest/latest-dev.json"
|
||||
do
|
||||
if curl -fsSL --max-time 20 "$url" -o "$RUNNER_TEMP/prev-latest-dev.json"; then
|
||||
PREV_VERSION="$(jq -r '.version // empty' "$RUNNER_TEMP/prev-latest-dev.json" 2>/dev/null || true)"
|
||||
if [[ "$PREV_VERSION" =~ ^dev-([0-9a-fA-F]{7,40})$ ]]; then
|
||||
CANDIDATE="${BASH_REMATCH[1]}"
|
||||
if git rev-parse --verify "${CANDIDATE}^{commit}" >/dev/null 2>&1; then
|
||||
PREV_SHA="$(git rev-parse "${CANDIDATE}^{commit}")"
|
||||
echo "resolved previous dev baseline: version=${PREV_VERSION} sha=${PREV_SHA:0:12} (from ${url})"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
echo "sha=${PREV_SHA}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Generate Dev Changelog
|
||||
id: changelog
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
DEV_VERSION="${{ steps.version.outputs.version }}"
|
||||
BUILD_TIME="${{ steps.build_time.outputs.display }}"
|
||||
PREV_SHA="${{ steps.prev_dev.outputs.sha }}"
|
||||
CHANGELOG_FILE="$RUNNER_TEMP/dev-changelog.md"
|
||||
BODY_FILE="$RUNNER_TEMP/dev-release-body.md"
|
||||
NOTES_CORE="$RUNNER_TEMP/dev-changelog-core.md"
|
||||
|
||||
HEADER_FILE="$RUNNER_TEMP/dev-changelog-header.md"
|
||||
cat > "$HEADER_FILE" <<EOF
|
||||
## 🧪 测试版本 (Dev Build)
|
||||
|
||||
**版本**: \`${DEV_VERSION}\`
|
||||
**分支**: \`dev\`
|
||||
**提交**: [\`${{ github.sha }}\`](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }})
|
||||
**构建时间**: ${BUILD_TIME}
|
||||
|
||||
> ⚠️ 这是开发测试版本,仅供内部测试使用,不建议用于生产环境。
|
||||
> 每次 push 到 \`dev\` 分支会自动覆盖此 release。
|
||||
|
||||
EOF
|
||||
|
||||
if [[ -n "$PREV_SHA" ]]; then
|
||||
python3 tools/generate-release-notes.py \
|
||||
--repo "${{ github.repository }}" \
|
||||
--tag "${{ github.sha }}" \
|
||||
--previous-tag "$PREV_SHA" \
|
||||
--max-commits 100 \
|
||||
--repository-url "${{ github.server_url }}/${{ github.repository }}" \
|
||||
--output "$NOTES_CORE" || {
|
||||
echo "warning: generate-release-notes failed; falling back to empty commit list" >&2
|
||||
echo "(未能生成提交变更列表)" > "$NOTES_CORE"
|
||||
}
|
||||
else
|
||||
cat > "$NOTES_CORE" <<EOF
|
||||
## 📝 变更说明
|
||||
|
||||
- 未能解析上一测试版基线,本次仅发布当前构建。
|
||||
- 当前提交:\`${{ github.sha }}\`
|
||||
EOF
|
||||
fi
|
||||
|
||||
{
|
||||
cat "$HEADER_FILE"
|
||||
echo "## 更新日志(相对上一测试版)"
|
||||
echo
|
||||
cat "$NOTES_CORE"
|
||||
} > "$BODY_FILE"
|
||||
|
||||
# 应用内弹窗使用完整 body(含 dev 警告头)
|
||||
cp "$BODY_FILE" "$CHANGELOG_FILE"
|
||||
echo "changelog_file=$CHANGELOG_FILE" >> "$GITHUB_OUTPUT"
|
||||
echo "body_file=$BODY_FILE" >> "$GITHUB_OUTPUT"
|
||||
echo "📄 dev changelog ready ($(wc -c < "$BODY_FILE") bytes)"
|
||||
|
||||
# 静态更新清单:dev 通道读
|
||||
# https://github.com/Syngnat/GoNavi/releases/download/dev-latest/latest-dev.json
|
||||
- name: Generate static update manifest (latest-dev.json)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
DEV_VERSION="${{ steps.version.outputs.version }}"
|
||||
CHANGELOG_FILE="${{ steps.changelog.outputs.changelog_file }}"
|
||||
python3 tools/generate-update-latest-manifest.py \
|
||||
--assets-dir release-assets \
|
||||
--version "$DEV_VERSION" \
|
||||
--tag dev-latest \
|
||||
--channel dev \
|
||||
--name "Dev Build (${DEV_VERSION})" \
|
||||
--download-base-url "https://download.syngnat.top/gonavi/dev/releases/download" \
|
||||
--download-tag "$DEV_VERSION" \
|
||||
--release-notes-file "$CHANGELOG_FILE" \
|
||||
--output release-assets/latest-dev.json
|
||||
test -s release-assets/latest-dev.json
|
||||
jq -e '(.releaseNotes | type == "string" and length > 0)' release-assets/latest-dev.json >/dev/null
|
||||
echo "📄 latest-dev.json ready:"
|
||||
head -n 40 release-assets/latest-dev.json
|
||||
|
||||
# 删除旧的 dev pre-release(保持只有最新一个)
|
||||
- name: Reset Previous Dev Release
|
||||
uses: actions/github-script@v8
|
||||
@@ -1393,16 +1485,7 @@ jobs:
|
||||
files: release-assets/*
|
||||
prerelease: true
|
||||
draft: false
|
||||
body: |
|
||||
## 🧪 测试版本 (Dev Build)
|
||||
|
||||
**版本**: `${{ steps.version.outputs.version }}`
|
||||
**分支**: `dev`
|
||||
**提交**: [`${{ github.sha }}`](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }})
|
||||
**构建时间**: ${{ steps.build_time.outputs.display }}
|
||||
|
||||
> ⚠️ 这是开发测试版本,仅供内部测试使用,不建议用于生产环境。
|
||||
> 每次 push 到 `dev` 分支会自动覆盖此 release。
|
||||
body_path: ${{ steps.changelog.outputs.body_file }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
21
.github/workflows/release.yml
vendored
21
.github/workflows/release.yml
vendored
@@ -1291,6 +1291,27 @@ jobs:
|
||||
--output "$CHANGELOG_FILE"
|
||||
echo "changelog_file=$CHANGELOG_FILE" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# 将同一份 changelog 写入 latest.json,供应用内「更新日志」弹窗使用(静态清单优先路径)。
|
||||
- name: Embed changelog into latest.json
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="${{ github.ref_name }}"
|
||||
VERSION="${TAG#v}"
|
||||
CHANGELOG_FILE="${{ steps.changelog.outputs.changelog_file }}"
|
||||
test -s "$CHANGELOG_FILE"
|
||||
test -d release-assets
|
||||
python3 tools/generate-update-latest-manifest.py \
|
||||
--assets-dir release-assets \
|
||||
--version "$VERSION" \
|
||||
--tag "$TAG" \
|
||||
--channel latest \
|
||||
--download-base-url https://download.syngnat.top/gonavi/releases/download \
|
||||
--release-notes-file "$CHANGELOG_FILE" \
|
||||
--output release-assets/latest.json
|
||||
jq -e '(.releaseNotes | type == "string" and length > 0)' release-assets/latest.json >/dev/null
|
||||
echo "📄 latest.json releaseNotes embedded ($(jq -r '.releaseNotes | length' release-assets/latest.json) chars)"
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v3
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
|
||||
@@ -79,16 +79,18 @@ type UpdateInfo struct {
|
||||
ReleaseName string `json:"releaseName"`
|
||||
ReleasePublishedAt string `json:"releasePublishedAt,omitempty"`
|
||||
ReleaseNotesURL string `json:"releaseNotesUrl"`
|
||||
AssetName string `json:"assetName"`
|
||||
AssetURL string `json:"assetUrl"`
|
||||
AssetAPIURL string `json:"assetApiUrl,omitempty"`
|
||||
AssetSize int64 `json:"assetSize"`
|
||||
SHA256 string `json:"sha256"`
|
||||
Downloaded bool `json:"downloaded"`
|
||||
DownloadPath string `json:"downloadPath,omitempty"`
|
||||
InstallMode string `json:"installMode"`
|
||||
PackageType string `json:"packageType,omitempty"`
|
||||
AutoRelaunch bool `json:"autoRelaunch"`
|
||||
// ReleaseNotes 为 Markdown 更新日志正文(来自 latest.json / GitHub release body)。
|
||||
ReleaseNotes string `json:"releaseNotes,omitempty"`
|
||||
AssetName string `json:"assetName"`
|
||||
AssetURL string `json:"assetUrl"`
|
||||
AssetAPIURL string `json:"assetApiUrl,omitempty"`
|
||||
AssetSize int64 `json:"assetSize"`
|
||||
SHA256 string `json:"sha256"`
|
||||
Downloaded bool `json:"downloaded"`
|
||||
DownloadPath string `json:"downloadPath,omitempty"`
|
||||
InstallMode string `json:"installMode"`
|
||||
PackageType string `json:"packageType,omitempty"`
|
||||
AutoRelaunch bool `json:"autoRelaunch"`
|
||||
}
|
||||
|
||||
type AppInfo struct {
|
||||
@@ -167,6 +169,7 @@ type githubRelease struct {
|
||||
Name string `json:"name"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
PublishedAt string `json:"published_at"`
|
||||
Body string `json:"body"`
|
||||
Prerelease bool `json:"prerelease"`
|
||||
Assets []githubAsset `json:"assets"`
|
||||
}
|
||||
@@ -699,6 +702,7 @@ func fetchLatestUpdateInfoWithOptions(channel updateChannel, forceNetwork bool)
|
||||
ReleaseName: release.Name,
|
||||
ReleasePublishedAt: strings.TrimSpace(release.PublishedAt),
|
||||
ReleaseNotesURL: release.HTMLURL,
|
||||
ReleaseNotes: strings.TrimSpace(release.Body),
|
||||
InstallMode: string(installMode),
|
||||
PackageType: string(packageType),
|
||||
AutoRelaunch: true,
|
||||
@@ -737,6 +741,7 @@ func fetchLatestUpdateInfoWithOptions(channel updateChannel, forceNetwork bool)
|
||||
ReleaseName: release.Name,
|
||||
ReleasePublishedAt: strings.TrimSpace(release.PublishedAt),
|
||||
ReleaseNotesURL: release.HTMLURL,
|
||||
ReleaseNotes: strings.TrimSpace(release.Body),
|
||||
AssetName: asset.Name,
|
||||
AssetURL: firstNonEmptyString(asset.BrowserDownloadURL, asset.URL),
|
||||
AssetAPIURL: strings.TrimSpace(asset.URL),
|
||||
|
||||
@@ -326,6 +326,50 @@ func TestCheckForUpdatesRestoresPersistedGlobalProxyRuntime(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchLatestUpdateInfoMapsReleaseNotesFromStaticManifest(t *testing.T) {
|
||||
assetName, err := expectedAssetName(stdRuntime.GOOS, stdRuntime.GOARCH, "1.2.3")
|
||||
if err != nil {
|
||||
t.Fatalf("expectedAssetName returned error: %v", err)
|
||||
}
|
||||
|
||||
originalVersion := AppVersion
|
||||
AppVersion = "1.0.0"
|
||||
defer func() {
|
||||
AppVersion = originalVersion
|
||||
}()
|
||||
|
||||
const notes = "## ✨ 新功能\n\n- in-app release notes"
|
||||
restoreStatic := swapUpdateFetchStaticManifest(func(channel updateChannel) (*githubRelease, error) {
|
||||
return &githubRelease{
|
||||
TagName: "v1.2.3",
|
||||
Name: "v1.2.3",
|
||||
HTMLURL: "https://github.com/Syngnat/GoNavi/releases/tag/v1.2.3",
|
||||
PublishedAt: "2026-07-08T11:15:00Z",
|
||||
Body: notes + "\n",
|
||||
Assets: []githubAsset{
|
||||
{
|
||||
Name: assetName,
|
||||
BrowserDownloadURL: "https://example.com/" + assetName,
|
||||
Digest: "sha256:" + strings.Repeat("a", 64),
|
||||
Size: 4096,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
})
|
||||
defer restoreStatic()
|
||||
|
||||
info, err := fetchLatestUpdateInfo(updateChannelLatest)
|
||||
if err != nil {
|
||||
t.Fatalf("fetchLatestUpdateInfo returned error: %v", err)
|
||||
}
|
||||
if info.ReleaseNotes != notes {
|
||||
t.Fatalf("expected release notes body, got %#v", info.ReleaseNotes)
|
||||
}
|
||||
if info.ReleaseNotesURL != "https://github.com/Syngnat/GoNavi/releases/tag/v1.2.3" {
|
||||
t.Fatalf("unexpected release notes url: %#v", info.ReleaseNotesURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchLatestUpdateInfoForDevChannelUsesReleaseBuildVersion(t *testing.T) {
|
||||
assetName, err := expectedAssetName(stdRuntime.GOOS, stdRuntime.GOARCH, "dev-a1b2c3d")
|
||||
if err != nil {
|
||||
|
||||
@@ -42,9 +42,11 @@ type updateReleaseManifest struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
HTMLURL string `json:"htmlUrl,omitempty"`
|
||||
PublishedAt string `json:"publishedAt,omitempty"`
|
||||
Assets []updateManifestAsset `json:"assets"`
|
||||
FetchedAt time.Time `json:"fetchedAt,omitempty"` // 仅本地缓存写入
|
||||
Source string `json:"source,omitempty"` // static | api | disk-cache
|
||||
// ReleaseNotes 为 Markdown 更新日志(可选;旧清单无此字段时客户端走空态+外链)。
|
||||
ReleaseNotes string `json:"releaseNotes,omitempty"`
|
||||
Assets []updateManifestAsset `json:"assets"`
|
||||
FetchedAt time.Time `json:"fetchedAt,omitempty"` // 仅本地缓存写入
|
||||
Source string `json:"source,omitempty"` // static | api | disk-cache
|
||||
}
|
||||
|
||||
type updateManifestAsset struct {
|
||||
@@ -134,11 +136,12 @@ func releaseFromUpdateManifest(manifest *updateReleaseManifest) *githubRelease {
|
||||
name = tagName
|
||||
}
|
||||
return &githubRelease{
|
||||
TagName: tagName,
|
||||
Name: name,
|
||||
HTMLURL: strings.TrimSpace(manifest.HTMLURL),
|
||||
PublishedAt: strings.TrimSpace(manifest.PublishedAt),
|
||||
Assets: assets,
|
||||
TagName: tagName,
|
||||
Name: name,
|
||||
HTMLURL: strings.TrimSpace(manifest.HTMLURL),
|
||||
PublishedAt: strings.TrimSpace(manifest.PublishedAt),
|
||||
Body: strings.TrimSpace(manifest.ReleaseNotes),
|
||||
Assets: assets,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,6 +176,7 @@ func updateManifestFromGitHubRelease(channel updateChannel, release *githubRelea
|
||||
Name: strings.TrimSpace(release.Name),
|
||||
HTMLURL: strings.TrimSpace(release.HTMLURL),
|
||||
PublishedAt: strings.TrimSpace(release.PublishedAt),
|
||||
ReleaseNotes: strings.TrimSpace(release.Body),
|
||||
Assets: assets,
|
||||
FetchedAt: time.Now().UTC(),
|
||||
Source: "api",
|
||||
|
||||
@@ -154,15 +154,18 @@ def warn_if_release_history_diverged(previous_tag: str, tag: str) -> None:
|
||||
)
|
||||
|
||||
|
||||
def read_commits(tag: str, previous_tag: str) -> list[Commit]:
|
||||
def read_commits(tag: str, previous_tag: str, max_commits: int = 0) -> list[Commit]:
|
||||
revision_range = f"{previous_tag}..{tag}" if previous_tag else tag
|
||||
output = run_git(
|
||||
git_args = [
|
||||
"log",
|
||||
revision_range,
|
||||
"--no-merges",
|
||||
"-z",
|
||||
"--pretty=format:%H%x00%s%x00%an%x00%ae",
|
||||
)
|
||||
]
|
||||
if max_commits > 0:
|
||||
git_args.insert(2, f"-n{max_commits}")
|
||||
output = run_git(*git_args)
|
||||
if not output:
|
||||
return []
|
||||
fields = output.split("\x00")
|
||||
@@ -416,7 +419,13 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
|
||||
parser.add_argument(
|
||||
"--previous-tag",
|
||||
default="",
|
||||
help="Previous tag (default: preceding v* tag by creation date)",
|
||||
help="Previous git ref/tag/commit (default: preceding v* tag by creation date)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-commits",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Optional max commit count in the range (0 = unlimited; useful for dense dev builds)",
|
||||
)
|
||||
parser.add_argument("--repository-url", required=True, help="Repository web URL")
|
||||
parser.add_argument(
|
||||
@@ -428,6 +437,8 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
|
||||
args = parser.parse_args(argv)
|
||||
if not re.fullmatch(r"[^/\s]+/[^/\s]+", args.repo):
|
||||
parser.error("--repo must use owner/name format")
|
||||
if args.max_commits < 0:
|
||||
parser.error("--max-commits must be >= 0")
|
||||
return args
|
||||
|
||||
|
||||
@@ -436,7 +447,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
previous_tag = args.previous_tag.strip() or resolve_previous_tag(args.tag)
|
||||
warn_if_release_history_diverged(previous_tag, args.tag)
|
||||
revision_range = f"{previous_tag}..{args.tag}" if previous_tag else args.tag
|
||||
commits = read_commits(args.tag, previous_tag)
|
||||
commits = read_commits(args.tag, previous_tag, max_commits=args.max_commits)
|
||||
attributions = collect_attributions(
|
||||
commits=commits,
|
||||
repository=args.repo,
|
||||
|
||||
@@ -33,6 +33,11 @@ from urllib.parse import quote
|
||||
|
||||
REPO = "Syngnat/GoNavi"
|
||||
SCHEMA_VERSION = 1
|
||||
# 客户端静态清单体积保护:超过则截断并附 GitHub 完整日志提示。
|
||||
RELEASE_NOTES_MAX_BYTES = 64 * 1024
|
||||
RELEASE_NOTES_TRUNCATE_SUFFIX = (
|
||||
"\n\n---\n\n> 更新日志过长,已截断。完整内容请查看 GitHub Release 页面。\n"
|
||||
)
|
||||
SKIP_NAMES = {
|
||||
"SHA256SUMS",
|
||||
"LICENSE",
|
||||
@@ -43,6 +48,25 @@ SKIP_NAMES = {
|
||||
}
|
||||
|
||||
|
||||
def load_release_notes(path: Path | None) -> str:
|
||||
if path is None:
|
||||
return ""
|
||||
if not path.is_file():
|
||||
raise SystemExit(f"release notes file not found: {path}")
|
||||
text = path.read_text(encoding="utf-8", errors="replace").strip()
|
||||
if not text:
|
||||
return ""
|
||||
encoded = text.encode("utf-8")
|
||||
if len(encoded) <= RELEASE_NOTES_MAX_BYTES:
|
||||
return text
|
||||
# 按字节截断,避免切断多字节 UTF-8 字符
|
||||
budget = RELEASE_NOTES_MAX_BYTES - len(RELEASE_NOTES_TRUNCATE_SUFFIX.encode("utf-8"))
|
||||
if budget < 0:
|
||||
budget = 0
|
||||
truncated = encoded[:budget].decode("utf-8", errors="ignore").rstrip()
|
||||
return truncated + RELEASE_NOTES_TRUNCATE_SUFFIX
|
||||
|
||||
|
||||
def parse_sha256sums(path: Path) -> dict[str, str]:
|
||||
if not path.is_file():
|
||||
return {}
|
||||
@@ -125,6 +149,7 @@ def build_manifest(
|
||||
published_at: str | None,
|
||||
download_base_url: str = "",
|
||||
download_tag: str = "",
|
||||
release_notes: str = "",
|
||||
) -> dict:
|
||||
hashes = parse_sha256sums(assets_dir / "SHA256SUMS")
|
||||
tag = tag.strip() or f"v{normalize_version(version)}"
|
||||
@@ -133,7 +158,7 @@ def build_manifest(
|
||||
if not assets:
|
||||
raise SystemExit(f"no release assets found under {assets_dir}")
|
||||
|
||||
return {
|
||||
payload = {
|
||||
"schemaVersion": SCHEMA_VERSION,
|
||||
"channel": channel,
|
||||
"tagName": tag,
|
||||
@@ -143,6 +168,10 @@ def build_manifest(
|
||||
"publishedAt": (published_at or datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")),
|
||||
"assets": assets,
|
||||
}
|
||||
notes = (release_notes or "").strip()
|
||||
if notes:
|
||||
payload["releaseNotes"] = notes
|
||||
return payload
|
||||
|
||||
|
||||
def main() -> int:
|
||||
@@ -173,6 +202,11 @@ def main() -> int:
|
||||
default="",
|
||||
help="Output path (default: <assets-dir>/latest.json or latest-dev.json)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--release-notes-file",
|
||||
default="",
|
||||
help="Optional Markdown file embedded into releaseNotes for in-app changelog",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
assets_dir = Path(args.assets_dir).resolve()
|
||||
@@ -190,6 +224,8 @@ def main() -> int:
|
||||
|
||||
out_name = "latest-dev.json" if args.channel == "dev" else "latest.json"
|
||||
output = Path(args.output).resolve() if args.output else assets_dir / out_name
|
||||
notes_path = Path(args.release_notes_file).resolve() if args.release_notes_file.strip() else None
|
||||
release_notes = load_release_notes(notes_path)
|
||||
|
||||
manifest = build_manifest(
|
||||
channel=args.channel,
|
||||
@@ -200,9 +236,11 @@ def main() -> int:
|
||||
published_at=args.published_at or None,
|
||||
download_base_url=args.download_base_url,
|
||||
download_tag=args.download_tag,
|
||||
release_notes=release_notes,
|
||||
)
|
||||
output.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"wrote {output} ({len(manifest['assets'])} assets, version={manifest['version']})")
|
||||
notes_hint = f", notes={len(manifest.get('releaseNotes', ''))} chars" if manifest.get("releaseNotes") else ""
|
||||
print(f"wrote {output} ({len(manifest['assets'])} assets, version={manifest['version']}{notes_hint})")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -143,6 +143,40 @@ class GenerateUpdateLatestManifestTest(unittest.TestCase):
|
||||
f"https://github.com/Syngnat/GoNavi/releases/download/dev-latest/{asset_name}",
|
||||
)
|
||||
|
||||
def test_embeds_release_notes_from_file(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
assets = Path(tmp)
|
||||
(assets / "GoNavi-1.2.3-Windows-Amd64-Portable.zip").write_bytes(b"fake")
|
||||
(assets / "SHA256SUMS").write_text(
|
||||
f"{'d' * 64} GoNavi-1.2.3-Windows-Amd64-Portable.zip\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
notes = assets / "changelog.md"
|
||||
notes.write_text("## ✨ 新功能\n\n- 示例变更\n", encoding="utf-8")
|
||||
out = assets / "latest.json"
|
||||
subprocess.check_call(
|
||||
[
|
||||
sys.executable,
|
||||
str(SCRIPT),
|
||||
"--assets-dir",
|
||||
str(assets),
|
||||
"--version",
|
||||
"1.2.3",
|
||||
"--tag",
|
||||
"v1.2.3",
|
||||
"--channel",
|
||||
"latest",
|
||||
"--release-notes-file",
|
||||
str(notes),
|
||||
"--output",
|
||||
str(out),
|
||||
],
|
||||
cwd=str(ROOT),
|
||||
)
|
||||
data = json.loads(out.read_text(encoding="utf-8"))
|
||||
self.assertIn("releaseNotes", data)
|
||||
self.assertIn("示例变更", data["releaseNotes"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user