feat: 更新版本号至 v1.11.1,新增特殊音频替换逻辑以支持特定事件的音效替换 #68
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - '*.*.*' | |
| - '*.*.*-*' | |
| jobs: | |
| build-and-release: | |
| runs-on: windows-latest | |
| permissions: | |
| contents: write | |
| packages: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: '10.x' | |
| - name: Configure NuGet authentication | |
| run: dotnet nuget update source Duckov-Custom-Model --username Duckov-Custom-Model --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text | |
| - name: Restore dependencies | |
| run: dotnet restore | |
| env: | |
| CI: true | |
| GITHUB_ACTIONS: true | |
| - name: Build | |
| run: dotnet build --configuration Release --no-restore | |
| env: | |
| CI: true | |
| GITHUB_ACTIONS: true | |
| - name: Create Release Package | |
| run: | | |
| $outputPath = "DuckovCustomModel/bin/Release/netstandard2.1" | |
| $packageDir = "DuckovCustomModel/bin/Release/PackageTemp" | |
| $zipPath = "DuckovCustomModel/bin/Release/DuckovCustomModel.zip" | |
| New-Item -ItemType Directory -Path $packageDir -Force | Out-Null | |
| Get-ChildItem -Path $outputPath -Recurse -File | ForEach-Object { | |
| $relativePath = $_.FullName.Substring((Resolve-Path $outputPath).Path.Length).TrimStart('\', '/') | |
| $destPath = Join-Path $packageDir $relativePath | |
| $destDir = Split-Path $destPath -Parent | |
| if (-not (Test-Path $destDir)) { | |
| New-Item -ItemType Directory -Path $destDir -Force | Out-Null | |
| } | |
| Copy-Item $_.FullName -Destination $destPath -Force | |
| } | |
| if (Test-Path $zipPath) { | |
| Remove-Item $zipPath -Force | |
| } | |
| Compress-Archive -Path (Join-Path $packageDir '*') -DestinationPath $zipPath -Force | |
| Remove-Item -Path $packageDir -Recurse -Force | |
| shell: pwsh | |
| - name: Upload Release Artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: release-package | |
| path: DuckovCustomModel/bin/Release/DuckovCustomModel.zip | |
| retention-days: 30 | |
| - name: Prepare Release Body | |
| id: prepare_release_body | |
| run: | | |
| $tagName = "${{ github.ref_name }}" | |
| $version = $tagName -replace '^v', '' | |
| $changelogFile = "CHANGELOG.md" | |
| $manualChangelog = "" | |
| if (Test-Path $changelogFile) { | |
| $content = Get-Content $changelogFile -Raw -Encoding UTF8 | |
| $escapedVersion = [regex]::Escape($version) | |
| # 判断是否为 fix 版本 | |
| $isFixVersion = $version -match '-fix\d+$' | |
| if ($isFixVersion) { | |
| # Fix 版本:提取从 fix 版本到下一个版本之间的所有内容 | |
| # 例如:v1.9.5-fix1 应该包含 v1.9.5-fix1 和 v1.9.5 的内容 | |
| # 提取基础版本号(去掉 -fix1 等后缀) | |
| $baseVersion = $version -replace '-fix\d+$', '' | |
| $escapedBaseVersion = [regex]::Escape($baseVersion) | |
| # 找到 fix 版本的起始位置 | |
| $fixVersionPattern = "##\s+v?$escapedVersion\s*\r?\n" | |
| $fixVersionMatch = [regex]::Match($content, $fixVersionPattern) | |
| if ($fixVersionMatch.Success) { | |
| # 从 fix 版本开始,找到下一个不同版本号的位置 | |
| # 需要跳过基础版本号(如 v1.9.5),找到下一个不同的版本号(如 v1.9.4) | |
| $startPos = $fixVersionMatch.Index + $fixVersionMatch.Length | |
| $remainingContent = $content.Substring($startPos) | |
| # 先匹配所有版本号标题 | |
| $allVersionPattern = "(?m)^##\s+v?(\d+\.\d+\.\d+(?:-fix\d+)?)" | |
| $allVersionMatches = [regex]::Matches($remainingContent, $allVersionPattern) | |
| $nextVersionIndex = -1 | |
| foreach ($match in $allVersionMatches) { | |
| $matchedVersion = $match.Groups[1].Value | |
| # 如果匹配到的版本号不是基础版本号的任何变体,则找到了下一个不同版本号 | |
| if ($matchedVersion -notmatch "^$escapedBaseVersion(?:-fix\d+)?$") { | |
| $nextVersionIndex = $match.Index | |
| break | |
| } | |
| } | |
| if ($nextVersionIndex -ge 0) { | |
| # 提取到下一个不同版本号之前的所有内容 | |
| $changelogContent = $remainingContent.Substring(0, $nextVersionIndex) | |
| } else { | |
| # 如果没有找到下一个不同版本号,提取到文件结尾 | |
| $changelogContent = $remainingContent | |
| } | |
| # 分离 fix 内容和基础版本内容 | |
| $baseVersionPattern = "##\s+v?$escapedBaseVersion\s*\r?\n" | |
| $baseVersionMatch = [regex]::Match($changelogContent, $baseVersionPattern) | |
| if ($baseVersionMatch.Success) { | |
| # 分离两部分内容 | |
| $fixContent = $changelogContent.Substring(0, $baseVersionMatch.Index).Trim() | |
| $baseContent = $changelogContent.Substring($baseVersionMatch.Index + $baseVersionMatch.Length).Trim() | |
| # 处理 fix 内容:保留 fix 版本标识,但转换为更清晰的格式 | |
| # 获取当前 fix 版本号 | |
| $currentFixNumber = "" | |
| $fixMatch = [regex]::Match($version, '-fix(\d+)$') | |
| if ($fixMatch.Success) { | |
| $currentFixNumber = $fixMatch.Groups[1].Value | |
| } | |
| # 匹配所有 fix 版本标题行,转换为只带 fix 编号的格式(但当前 fix 版本不添加标题) | |
| $fixContent = [regex]::Replace($fixContent, "(?m)^##\s+v?$escapedBaseVersion-fix(\d+)\s*\r?\n", { | |
| param($match) | |
| $matchedFixNumber = $match.Groups[1].Value | |
| if ($matchedFixNumber -eq $currentFixNumber) { | |
| # 当前 fix 版本,不添加标题,只保留换行 | |
| return "`n`n" | |
| } else { | |
| # 其他 fix 版本,添加标题 | |
| return "`n`n**fix$matchedFixNumber 修复:**`n`n" | |
| } | |
| }) | |
| # 如果当前 fix 版本内容在开头且没有标题,保持原样(不添加标题) | |
| # 清理多余的连续换行(超过2个换行的地方只保留2个) | |
| $fixContent = [regex]::Replace($fixContent, "(\r?\n){3,}", "`n`n") | |
| # 移除基础版本标题行(如果存在) | |
| $baseContent = [regex]::Replace($baseContent, "(?m)^##\s+v?$escapedBaseVersion(?:-fix\d+)?\s*\r?\n", "") | |
| # 清理基础版本内容的连续换行 | |
| $baseContent = [regex]::Replace($baseContent, "(\r?\n){3,}", "`n`n") | |
| # 组合两部分,明确区分 fix 内容和原始版本内容 | |
| if ($fixContent -and $baseContent) { | |
| $manualChangelog = "**修复内容:**`n`n$fixContent`n`n**原始更新内容:**`n`n$baseContent" | |
| } elseif ($fixContent) { | |
| $manualChangelog = "**修复内容:**`n`n$fixContent" | |
| } else { | |
| $manualChangelog = $baseContent | |
| } | |
| } else { | |
| # 如果没有找到基础版本,获取当前 fix 版本号 | |
| $currentFixNumber = "" | |
| $fixMatch = [regex]::Match($version, '-fix(\d+)$') | |
| if ($fixMatch.Success) { | |
| $currentFixNumber = $fixMatch.Groups[1].Value | |
| } | |
| # 转换 fix 版本标题行(当前 fix 版本不添加标题) | |
| $changelogContent = [regex]::Replace($changelogContent, "(?m)^##\s+v?$escapedBaseVersion-fix(\d+)\s*\r?\n", { | |
| param($match) | |
| $matchedFixNumber = $match.Groups[1].Value | |
| if ($matchedFixNumber -eq $currentFixNumber) { | |
| # 当前 fix 版本,不添加标题,只保留换行 | |
| return "`n`n" | |
| } else { | |
| # 其他 fix 版本,添加标题 | |
| return "`n`n**fix$matchedFixNumber 修复:**`n`n" | |
| } | |
| }) | |
| # 清理多余的连续换行 | |
| $changelogContent = [regex]::Replace($changelogContent, "(\r?\n){3,}", "`n`n") | |
| $manualChangelog = $changelogContent.Trim() | |
| } | |
| Write-Host "✓ 从 CHANGELOG.md 提取到 fix 版本 changelog ($($manualChangelog.Length) 字符)" | |
| if ($manualChangelog.Length -gt 0) { | |
| Write-Host "预览前100字符: $($manualChangelog.Substring(0, [Math]::Min(100, $manualChangelog.Length)))" | |
| } | |
| } else { | |
| Write-Host "⚠ 未在 CHANGELOG.md 中找到 fix 版本 $version 的内容" | |
| } | |
| } else { | |
| # 非 fix 版本:使用原有逻辑 | |
| $pattern = "(?s)##\s+v?$escapedVersion\s*\r?\n(.*?)(?=\r?\n##\s+v?\d+\.\d+\.\d+|$)" | |
| $match = [regex]::Match($content, $pattern) | |
| if ($match.Success) { | |
| $manualChangelog = $match.Groups[1].Value.Trim() | |
| Write-Host "✓ 从 CHANGELOG.md 提取到 changelog ($($manualChangelog.Length) 字符)" | |
| if ($manualChangelog.Length -gt 0) { | |
| Write-Host "预览前100字符: $($manualChangelog.Substring(0, [Math]::Min(100, $manualChangelog.Length)))" | |
| } | |
| } else { | |
| Write-Host "⚠ 未在 CHANGELOG.md 中找到版本 $version 的内容" | |
| Write-Host "尝试的正则表达式: $pattern" | |
| } | |
| } | |
| } | |
| $headers = @{ | |
| Authorization = "token ${{ secrets.GITHUB_TOKEN }}" | |
| Accept = "application/vnd.github.v3+json" | |
| } | |
| $generateNotesUrl = "https://api.github.com/repos/${{ github.repository }}/releases/generate-notes" | |
| $generateNotesBody = @{ | |
| tag_name = $tagName | |
| } | ConvertTo-Json | |
| $autoNotes = "" | |
| try { | |
| $notesResponse = Invoke-RestMethod -Uri $generateNotesUrl -Method Post -Headers $headers -Body $generateNotesBody -ContentType "application/json" | |
| $autoNotes = $notesResponse.body | |
| Write-Host "✓ 获取到自动生成的 release notes" | |
| } catch { | |
| Write-Host "⚠ 无法获取自动生成的 release notes: $_" | |
| } | |
| $releaseBody = "" | |
| if ($manualChangelog) { | |
| $releaseBody = $manualChangelog | |
| if ($autoNotes) { | |
| $releaseBody += "`n`n---`n`n" | |
| } | |
| } | |
| if ($autoNotes) { | |
| $releaseBody += $autoNotes | |
| } | |
| if (-not $releaseBody) { | |
| $releaseBody = "Release $tagName" | |
| } | |
| echo "release_body<<EOF" >> $env:GITHUB_OUTPUT | |
| echo "$releaseBody" >> $env:GITHUB_OUTPUT | |
| echo "EOF" >> $env:GITHUB_OUTPUT | |
| Write-Host "✓ Release body 准备完成 ($($releaseBody.Length) 字符)" | |
| shell: pwsh | |
| - name: Create GitHub Release | |
| id: create_release | |
| uses: softprops/action-gh-release@v1 | |
| with: | |
| files: DuckovCustomModel/bin/Release/DuckovCustomModel.zip | |
| body: ${{ steps.prepare_release_body.outputs.release_body }} | |
| generate_release_notes: false | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Check Steam Workshop Secrets | |
| id: check_steam_secrets | |
| run: | | |
| $hasUsername = "${{ secrets.STEAM_USERNAME }}" -ne "" | |
| $hasPassword = "${{ secrets.STEAM_PASSWORD }}" -ne "" | |
| $hasAppId = "${{ secrets.STEAM_APP_ID }}" -ne "" | |
| if ($hasUsername -and $hasPassword -and $hasAppId) { | |
| echo "steam_enabled=true" >> $env:GITHUB_OUTPUT | |
| Write-Host "✓ Steam Workshop secrets 已配置" | |
| } else { | |
| echo "steam_enabled=false" >> $env:GITHUB_OUTPUT | |
| Write-Host "⚠ Steam Workshop secrets 未配置,将跳过上传" | |
| } | |
| shell: pwsh | |
| - name: Setup SteamCMD | |
| if: steps.check_steam_secrets.outputs.steam_enabled == 'true' | |
| uses: buildalon/setup-steamcmd@v1 | |
| - name: Prepare Workshop Content | |
| if: steps.check_steam_secrets.outputs.steam_enabled == 'true' | |
| id: prepare_workshop | |
| run: | | |
| $outputPath = "DuckovCustomModel/bin/Release/netstandard2.1" | |
| $workshopDir = "DuckovCustomModel/bin/Release/WorkshopContent" | |
| # 检查编译输出目录是否存在 | |
| if (-not (Test-Path $outputPath)) { | |
| Write-Error "编译输出目录不存在: $outputPath,请确保 Build 步骤已成功执行" | |
| exit 1 | |
| } | |
| # 创建 Workshop 内容目录 | |
| if (Test-Path $workshopDir) { | |
| Remove-Item -Path $workshopDir -Recurse -Force | |
| } | |
| New-Item -ItemType Directory -Path $workshopDir -Force | Out-Null | |
| # 从编译输出目录复制所有文件到 Workshop 目录 | |
| Get-ChildItem -Path $outputPath -Recurse -File | ForEach-Object { | |
| $relativePath = $_.FullName.Substring((Resolve-Path $outputPath).Path.Length).TrimStart('\', '/') | |
| $destPath = Join-Path $workshopDir $relativePath | |
| $destDir = Split-Path $destPath -Parent | |
| if (-not (Test-Path $destDir)) { | |
| New-Item -ItemType Directory -Path $destDir -Force | Out-Null | |
| } | |
| Copy-Item $_.FullName -Destination $destPath -Force | |
| } | |
| Write-Host "✓ Workshop 内容已准备完成(从 $outputPath 复制)" | |
| echo "workshop_dir=$workshopDir" >> $env:GITHUB_OUTPUT | |
| shell: pwsh | |
| - name: Upload to Steam Workshop | |
| if: steps.check_steam_secrets.outputs.steam_enabled == 'true' | |
| id: upload_steam | |
| uses: buildalon/upload-steam@v1 | |
| with: | |
| username: ${{ secrets.STEAM_USERNAME }} | |
| password: ${{ secrets.STEAM_PASSWORD }} | |
| app_id: ${{ secrets.STEAM_APP_ID }} | |
| workshop_item_id: ${{ secrets.STEAM_WORKSHOP_ITEM_ID }} | |
| description: ${{ steps.prepare_release_body.outputs.release_body }} | |
| content_root: ${{ steps.prepare_workshop.outputs.workshop_dir }} | |
| - name: Get Release Info | |
| id: release_info | |
| if: success() | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GITHUB_REPOSITORY: ${{ github.repository }} | |
| TAG_NAME: ${{ github.ref_name }} | |
| run: | | |
| $scriptContent = @' | |
| # -*- coding: utf-8 -*- | |
| import json | |
| import os | |
| import sys | |
| import time | |
| import urllib.request | |
| from datetime import datetime | |
| # Fix encoding for Windows console | |
| if sys.platform == 'win32': | |
| import io | |
| sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') | |
| sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') | |
| def get_release_with_retry(api_url, headers, package_file_name="DuckovCustomModel.zip", max_retries=15, retry_delay=3): | |
| last_error = None | |
| release = None | |
| for attempt in range(1, max_retries + 1): | |
| try: | |
| print(f"尝试获取 Release 信息 ({attempt}/{max_retries})...") | |
| req = urllib.request.Request(api_url, headers=headers) | |
| with urllib.request.urlopen(req) as response: | |
| release = json.load(response) | |
| if not release.get('tag_name'): | |
| raise ValueError("Release 数据不完整:缺少 tag_name") | |
| assets = release.get('assets', []) | |
| package_asset = next((a for a in assets if a.get('name') == package_file_name), None) | |
| if package_asset: | |
| print("✓ Release 信息获取成功,assets 已上传") | |
| print(f" 找到打包文件: {package_asset.get('name')}") | |
| print(f" 下载链接: {package_asset.get('browser_download_url')}") | |
| return release | |
| if attempt < max_retries: | |
| print(f"⚠ Assets 尚未上传完成,等待 {retry_delay} 秒后重试...") | |
| time.sleep(retry_delay) | |
| else: | |
| print("⚠ 已达到最大重试次数,assets 可能尚未上传完成") | |
| return release | |
| except urllib.error.HTTPError as e: | |
| last_error = e | |
| status_code = e.code | |
| print(f"⚠ 尝试 {attempt}/{max_retries} 失败") | |
| print(f" HTTP 状态码: {status_code}") | |
| print(f" 错误信息: {str(e)}") | |
| if status_code == 404 and attempt < max_retries: | |
| print(f"等待 {retry_delay} 秒后重试...") | |
| time.sleep(retry_delay) | |
| elif status_code == 403: | |
| print("API 访问被拒绝,可能是限流或权限问题") | |
| if attempt < max_retries: | |
| time.sleep(retry_delay) | |
| elif attempt >= max_retries: | |
| raise Exception(f"获取 Release 信息失败,已重试 {max_retries} 次: {str(e)}") | |
| except Exception as e: | |
| last_error = e | |
| print(f"⚠ 尝试 {attempt}/{max_retries} 失败: {str(e)}") | |
| if attempt >= max_retries: | |
| raise Exception(f"获取 Release 信息失败,已重试 {max_retries} 次: {str(e)}") | |
| time.sleep(retry_delay) | |
| if not release: | |
| raise Exception(f"获取 Release 信息失败,已重试 {max_retries} 次: {last_error}") | |
| return release | |
| def format_datetime_to_iso8601(dt_value): | |
| if isinstance(dt_value, str): | |
| try: | |
| for fmt in ["%Y-%m-%dT%H:%M:%SZ", "%Y-%m-%dT%H:%M:%S.%fZ", "%Y-%m-%dT%H:%M:%S%z"]: | |
| try: | |
| dt = datetime.strptime(dt_value.replace('Z', '+00:00'), fmt.replace('Z', '%z')) | |
| return dt.strftime("%Y-%m-%dT%H:%M:%S.000Z") | |
| except: | |
| continue | |
| dt = datetime.fromisoformat(dt_value.replace('Z', '+00:00')) | |
| return dt.strftime("%Y-%m-%dT%H:%M:%S.000Z") | |
| except Exception as e: | |
| print(f"⚠ 无法解析日期格式: {dt_value},使用原始值") | |
| return dt_value | |
| else: | |
| return dt_value | |
| def parse_download_links(assets, package_file_name="DuckovCustomModel.zip"): | |
| download_links = [] | |
| if assets: | |
| package_asset = next((a for a in assets if a.get('name') == package_file_name), None) | |
| if package_asset and package_asset.get('browser_download_url'): | |
| download_links.append({ | |
| 'name': 'Github Release', | |
| 'url': package_asset.get('browser_download_url') | |
| }) | |
| return download_links | |
| github_token = os.environ.get('GITHUB_TOKEN') | |
| github_repository = os.environ.get('GITHUB_REPOSITORY') | |
| tag_name = os.environ.get('TAG_NAME') | |
| if not github_token or not github_repository or not tag_name: | |
| print("::error::缺少必要的环境变量", file=sys.stderr) | |
| sys.exit(1) | |
| api_url = f"https://api.github.com/repos/{github_repository}/releases/tags/{tag_name}" | |
| print("获取 Release 信息...") | |
| print(f"Tag: {tag_name}") | |
| print(f"API URL: {api_url}") | |
| headers = { | |
| 'Authorization': f'token {github_token}', | |
| 'Accept': 'application/vnd.github.v3+json' | |
| } | |
| release = get_release_with_retry(api_url, headers, max_retries=15, retry_delay=3) | |
| version = release.get('tag_name', '').lstrip('v') | |
| release_name = release.get('name') or f"Release {version}" | |
| published_at = format_datetime_to_iso8601(release.get('published_at', '')) | |
| changelog = release.get('body') or '' | |
| download_links = parse_download_links(release.get('assets', [])) | |
| print("Release 信息:") | |
| print(f" 版本: {version}") | |
| print(f" 名称: {release_name}") | |
| print(f" 发布时间: {published_at}") | |
| print(f" 更新日志: {'已设置 (' + str(len(changelog)) + ' 字符)' if changelog else '未设置'}") | |
| print(f" 下载链接: {'已找到 (' + str(len(download_links)) + ' 项)' if download_links else '未找到'}") | |
| output_file = os.environ.get('GITHUB_OUTPUT') | |
| if output_file: | |
| with open(output_file, 'a', encoding='utf-8') as f: | |
| f.write(f"version={version}\n") | |
| f.write(f"release_name={release_name}\n") | |
| f.write(f"published_at={published_at}\n") | |
| delimiter_changelog = f"CHANGELOG_EOF_{os.urandom(8).hex()}" | |
| f.write(f"changelog<<{delimiter_changelog}\n") | |
| f.write(changelog) | |
| f.write(f"\n{delimiter_changelog}\n") | |
| delimiter_download = f"DOWNLOAD_LINKS_EOF_{os.urandom(8).hex()}" | |
| f.write(f"download_links<<{delimiter_download}\n") | |
| f.write(json.dumps(download_links, ensure_ascii=False)) | |
| f.write(f"\n{delimiter_download}\n") | |
| else: | |
| print("::error::GITHUB_OUTPUT 环境变量未设置", file=sys.stderr) | |
| sys.exit(1) | |
| '@ | |
| $scriptPath = Join-Path $env:TEMP "get_release_info_$(New-Guid).py" | |
| $scriptContent | Set-Content -Path $scriptPath -Encoding UTF8 -NoNewline | |
| try { | |
| python $scriptPath | |
| if ($LASTEXITCODE -ne 0) { | |
| Write-Error "Python 脚本执行失败,退出码: $LASTEXITCODE" | |
| exit $LASTEXITCODE | |
| } | |
| } finally { | |
| if (Test-Path $scriptPath) { | |
| Remove-Item -Path $scriptPath -Force | |
| } | |
| } | |
| shell: pwsh | |
| - name: Trigger Pages Update | |
| if: success() | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.PAGES_REPO_TOKEN }} | |
| PAGES_REPO_OWNER: ${{ secrets.PAGES_REPO_OWNER }} | |
| PAGES_REPO_NAME: ${{ secrets.PAGES_REPO_NAME }} | |
| RELEASE_VERSION: ${{ steps.release_info.outputs.version }} | |
| RELEASE_NAME: ${{ steps.release_info.outputs.release_name }} | |
| RELEASE_PUBLISHED_AT: ${{ steps.release_info.outputs.published_at }} | |
| RELEASE_CHANGELOG: ${{ steps.release_info.outputs.changelog }} | |
| RELEASE_DOWNLOAD_LINKS: ${{ steps.release_info.outputs.download_links }} | |
| run: | | |
| $scriptContent = @' | |
| # -*- coding: utf-8 -*- | |
| import json | |
| import os | |
| import sys | |
| import time | |
| import urllib.request | |
| # Fix encoding for Windows console | |
| if sys.platform == 'win32': | |
| import io | |
| sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') | |
| sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') | |
| def parse_download_links_from_json(json_str): | |
| if not json_str or not json_str.strip(): | |
| return None | |
| trimmed = json_str.strip() | |
| if trimmed in ['[]', 'null', 'undefined', '']: | |
| return None | |
| try: | |
| parsed = json.loads(trimmed) | |
| if isinstance(parsed, list): | |
| valid_links = [] | |
| for item in parsed: | |
| if isinstance(item, dict) and 'name' in item and 'url' in item: | |
| name = str(item.get('name', '')) | |
| url = str(item.get('url', '')) | |
| if name and url: | |
| valid_links.append({ | |
| 'name': name, | |
| 'url': url | |
| }) | |
| return valid_links if valid_links else None | |
| elif isinstance(parsed, dict): | |
| if 'name' in parsed and 'url' in parsed: | |
| name = str(parsed.get('name', '')) | |
| url = str(parsed.get('url', '')) | |
| if name and url: | |
| return [{'name': name, 'url': url}] | |
| return None | |
| except Exception as e: | |
| print(f"⚠ download_links JSON 解析失败: {e}", file=sys.stderr) | |
| return None | |
| def invoke_repository_dispatch(api_url, headers, payload, max_retries=15, retry_delay=5): | |
| body = { | |
| 'event_type': 'update-release', | |
| 'client_payload': payload | |
| } | |
| body_json = json.dumps(body, ensure_ascii=False) | |
| body_bytes = body_json.encode('utf-8') | |
| last_error = None | |
| for attempt in range(1, max_retries + 1): | |
| try: | |
| print(f"尝试触发页面更新 ({attempt}/{max_retries})...") | |
| req = urllib.request.Request(api_url, data=body_bytes, headers=headers, method='POST') | |
| with urllib.request.urlopen(req) as response: | |
| status_code = response.getcode() | |
| # GitHub API returns 204 No Content on success for repository_dispatch | |
| if status_code == 204: | |
| print("✓ 页面更新触发成功 (204 No Content)") | |
| return True | |
| # Try to read response body if available | |
| try: | |
| response_body = response.read().decode('utf-8') | |
| if response_body: | |
| response_data = json.loads(response_body) | |
| print("✓ 页面更新触发成功") | |
| print(f"响应: {json.dumps(response_data, ensure_ascii=False)}") | |
| else: | |
| print(f"✓ 页面更新触发成功 (状态码: {status_code}, 无响应体)") | |
| return True | |
| except json.JSONDecodeError: | |
| # Response is not JSON, but status code indicates success | |
| print(f"✓ 页面更新触发成功 (状态码: {status_code}, 非 JSON 响应)") | |
| return True | |
| except urllib.error.HTTPError as e: | |
| last_error = e | |
| status_code = e.code | |
| print(f"⚠ 尝试 {attempt}/{max_retries} 失败", file=sys.stderr) | |
| print(f" HTTP 状态码: {status_code}", file=sys.stderr) | |
| print(f" 错误信息: {str(e)}", file=sys.stderr) | |
| if e.fp: | |
| try: | |
| response_body = e.fp.read().decode('utf-8') | |
| print(f" 响应内容: {response_body}", file=sys.stderr) | |
| except: | |
| print(" 无法读取响应内容", file=sys.stderr) | |
| if attempt < max_retries: | |
| print(f"等待 {retry_delay} 秒后重试...") | |
| time.sleep(retry_delay) | |
| except Exception as e: | |
| last_error = e | |
| print(f"⚠ 尝试 {attempt}/{max_retries} 失败: {str(e)}", file=sys.stderr) | |
| if attempt < max_retries: | |
| print(f"等待 {retry_delay} 秒后重试...") | |
| time.sleep(retry_delay) | |
| raise Exception(f"页面更新触发失败,已重试 {max_retries} 次: {last_error}") | |
| pages_repo_owner = os.environ.get('PAGES_REPO_OWNER') | |
| pages_repo_name = os.environ.get('PAGES_REPO_NAME') | |
| pages_repo_token = os.environ.get('GITHUB_TOKEN') | |
| required_secrets = { | |
| 'PAGES_REPO_OWNER': pages_repo_owner, | |
| 'PAGES_REPO_NAME': pages_repo_name, | |
| 'PAGES_REPO_TOKEN': pages_repo_token | |
| } | |
| for key, value in required_secrets.items(): | |
| if not value or not value.strip(): | |
| print(f"::error::{key} secret is not set", file=sys.stderr) | |
| sys.exit(1) | |
| client_payload = { | |
| 'version': os.environ.get('RELEASE_VERSION', ''), | |
| 'release_name': os.environ.get('RELEASE_NAME', ''), | |
| 'published_at': os.environ.get('RELEASE_PUBLISHED_AT', '') | |
| } | |
| changelog = os.environ.get('RELEASE_CHANGELOG', '') | |
| if changelog: | |
| client_payload['changelog'] = changelog | |
| download_links = parse_download_links_from_json(os.environ.get('RELEASE_DOWNLOAD_LINKS', '')) | |
| if download_links: | |
| client_payload['download_links'] = download_links | |
| headers = { | |
| 'Authorization': f'token {pages_repo_token}', | |
| 'Accept': 'application/vnd.github.v3+json', | |
| 'Content-Type': 'application/json' | |
| } | |
| api_url = f"https://api.github.com/repos/{pages_repo_owner}/{pages_repo_name}/dispatches" | |
| print("准备触发页面更新...") | |
| print(f"目标仓库: {pages_repo_owner}/{pages_repo_name}") | |
| print(f"版本: {client_payload.get('version')}") | |
| print(f"更新日志: {'已包含 (' + str(len(changelog)) + ' 字符)' if changelog else '未包含'}") | |
| print(f"下载链接: {'已包含 (' + str(len(download_links)) + ' 项)' if download_links else '未包含'}") | |
| print(f"API URL: {api_url}") | |
| invoke_repository_dispatch(api_url, headers, client_payload, max_retries=15, retry_delay=5) | |
| '@ | |
| $scriptPath = Join-Path $env:TEMP "trigger_pages_update_$(New-Guid).py" | |
| $scriptContent | Set-Content -Path $scriptPath -Encoding UTF8 -NoNewline | |
| try { | |
| python $scriptPath | |
| if ($LASTEXITCODE -ne 0) { | |
| Write-Error "Python 脚本执行失败,退出码: $LASTEXITCODE" | |
| exit $LASTEXITCODE | |
| } | |
| } finally { | |
| if (Test-Path $scriptPath) { | |
| Remove-Item -Path $scriptPath -Force | |
| } | |
| } | |
| shell: pwsh |