From 08f2d7c559bf08deebadf0502d3ca8f7085eb037 Mon Sep 17 00:00:00 2001 From: Colin955023 <123829404+Colin955023@users.noreply.github.com> Date: Sun, 25 Oct 2026 21:29:32 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=E7=99=BC=E8=A1=8C?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E6=B5=81=E7=A8=8B=E4=B8=AD=E7=9A=84=E6=A8=99?= =?UTF-8?q?=E7=B1=A4=E6=AA=A2=E6=9F=A5=E8=88=87=E8=B3=87=E7=94=A2=E9=A9=97?= =?UTF-8?q?=E8=AD=89=E9=82=8F=E8=BC=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-release.yml | 191 +++++++++++------------- .github/workflows/codeql.yml | 2 +- .github/workflows/oss-scorecard.yml | 9 +- .github/workflows/validate-renovate.yml | 5 - 4 files changed, 88 insertions(+), 119 deletions(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 3cfcb00..ddc8bdd 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -15,6 +15,9 @@ jobs: if: github.ref == 'refs/heads/master' runs-on: ubuntu-latest timeout-minutes: 5 + permissions: + contents: read + actions: read outputs: tag_name: ${{ steps.extract_notes.outputs.tag_name }} version: ${{ steps.extract_notes.outputs.version }} @@ -23,7 +26,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - fetch-depth: 0 + fetch-depth: 1 - name: Extract version and verify CHANGELOG id: extract_notes @@ -43,43 +46,48 @@ jobs: exit 1 } - $existingTagCommit = git rev-parse -q --verify "refs/tags/$tag^{commit}" - if ($LASTEXITCODE -eq 0) { - if ($existingTagCommit.Trim() -ne $headCommit) { - Write-Error "Preflight 失敗:Tag $tag 已存在且指向不同 commit ($($existingTagCommit.Trim())),請更新版本號!" + $existingTag = [string](git tag -l $tag) + if (-not [string]::IsNullOrWhiteSpace($existingTag)) { + $ref = "refs/tags/" + $tag + "^{commit}" + $existingTagCommit = (git rev-parse -q --verify $ref).Trim() + if ($LASTEXITCODE -ne 0 -or $existingTagCommit -ne $headCommit) { + Write-Error "Preflight 失敗:Tag $tag 已存在且指向不同 commit [$existingTagCommit],請更新版本號!" exit 1 } - Write-Host "Tag $tag 已存在且指向當前 HEAD,允許發布覆蓋/更新。" + Write-Host "Tag $tag 已存在且指向當前 HEAD;將沿用該標籤建立 Release。" } else { Write-Host "Tag $tag 尚未建立,可安全建立新標籤發布。" } - gitleaks: - name: 隱私與機密資訊檢查 (Gitleaks) - needs: preflight - if: github.ref == 'refs/heads/master' - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - - - name: 隱私與機密資訊檢查 (Gitleaks) - uses: gitleaks/gitleaks-action@e0c47f4f8be36e29cdc102c57e68cb5cbf0e8d1e # v3.0.0 + - name: 確認 master HEAD 的 CI 工作流已全數完成 + shell: pwsh env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # 品質門禁、CodeQL、機密掃描已於 push 到 master 時執行,發布流程不重複建置驗證,僅確認同一 commit 的結果 + $runs = gh run list --repo $env:GITHUB_REPOSITORY --commit $env:GITHUB_SHA ` + --event push --limit 50 --json workflowName,status,conclusion,createdAt | ConvertFrom-Json + if ($LASTEXITCODE -ne 0) { throw '無法查詢 workflow runs' } + + $latest = @($runs | Sort-Object createdAt -Descending | Group-Object workflowName | ForEach-Object { $_.Group[0] }) + $pending = @($latest | Where-Object { $_.status -ne 'completed' }) + if ($pending.Count -gt 0) { + throw "master HEAD 尚有未完成的工作流:$($pending.workflowName -join ', '),請待其完成後再發布" + } + + foreach ($name in 'CI', 'CodeQL Analysis (C#)') { + $run = $latest | Where-Object { $_.workflowName -eq $name } + if ($null -eq $run -or $run.conclusion -ne 'success') { + throw "工作流 [$name] 在 $env:GITHUB_SHA 上未成功或尚未執行,禁止發布" + } + Write-Host "[$name] 已成功" + } build: name: Publish and Attest (.NET 10) - needs: [preflight, gitleaks] - if: github.ref == 'refs/heads/master' + needs: preflight runs-on: windows-latest timeout-minutes: 30 - outputs: - tag_name: ${{ steps.release_info.outputs.tag_name }} - version: ${{ steps.release_info.outputs.version }} env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" DOTNET_CLI_FORCE_UTF8_ENCODING: "1" @@ -95,7 +103,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - fetch-depth: 0 + fetch-depth: 1 - name: Setup .NET 10 SDK uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 @@ -103,34 +111,15 @@ jobs: global-json-file: global.json cache: true cache-dependency-path: | + global.json Directory.Packages.props src/**/*.csproj tests/**/*.csproj - - name: Extract version and release notes - id: release_info - shell: pwsh - run: .\scripts\extract_release_notes.ps1 -Strict - - - name: Run C# Quality Gate - shell: pwsh - run: .\scripts\dotnet_quality_gate.ps1 - - name: Publish Dual Executables (Self-contained & Framework-dependent) shell: pwsh run: .\scripts\dotnet_publish.ps1 - - name: Verify Release Artifacts in output - shell: pwsh - run: | - $outputDir = "output" - if (-not (Test-Path $outputDir)) { - throw "輸出目錄不存在: $outputDir" - } - - Write-Host "發布目錄驗證完成,檔案列表:" - Get-ChildItem -Path $outputDir | Select-Object Name, Length | Format-Table -AutoSize - - name: Attest release executable provenance uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: @@ -183,7 +172,7 @@ jobs: - name: Generate SBOM (SPDX 2.2) shell: pwsh env: - RELEASE_VERSION: ${{ steps.release_info.outputs.version }} + RELEASE_VERSION: ${{ needs.preflight.outputs.version }} SBOM_TOOL_VERSION: v4.1.5 SBOM_TOOL_SHA256: 625767b371b7fdd58f40f618b8a86da0247a33c89e419039c86b4edba1dad4b5 run: | @@ -230,16 +219,13 @@ jobs: uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: release_payload - path: | - output/ - release_notes.md + path: output/ if-no-files-found: error retention-days: 2 publish: name: Publish Release - needs: build - if: github.ref == 'refs/heads/master' + needs: [preflight, build] runs-on: ubuntu-latest timeout-minutes: 10 permissions: @@ -247,50 +233,39 @@ jobs: actions: read attestations: read env: - RELEASE_TAG: ${{ needs.build.outputs.tag_name }} + RELEASE_TAG: ${{ needs.preflight.outputs.tag_name }} steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - fetch-depth: 0 + fetch-depth: 1 - name: Download release payload uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: release_payload - path: . + path: output + + - name: Generate release notes + shell: pwsh + run: ./scripts/extract_release_notes.ps1 -Strict - name: Create release tag env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} shell: pwsh run: | - if ($env:GITHUB_REF_NAME -ne 'master') { - Write-Error "此 workflow 只能在 master 分支上手動執行,目前是 $env:GITHUB_REF_NAME" - exit 1 - } - git fetch --tags --force $tag = $env:RELEASE_TAG - $headCommit = (git rev-parse HEAD).Trim() - if ($LASTEXITCODE -ne 0 -or -not $headCommit) { - Write-Error "無法取得目前 commit" - exit 1 - } - - $existingTagCommit = git rev-parse -q --verify "refs/tags/$tag^{commit}" - if ($LASTEXITCODE -eq 0) { - if ($existingTagCommit.Trim() -ne $headCommit) { - Write-Error "Tag $tag 已存在,但指向不同 commit;請改用新版本號" - exit 1 - } - - Write-Host "Tag already exists and matches HEAD: $tag" + if ([string](git tag -l $tag)) { + Write-Host "Tag $tag 已存在(Preflight 已驗證指向當前 HEAD),略過建立。" exit 0 } + $headCommit = (git rev-parse HEAD).Trim() + $tagRequest = @{ tag = $tag message = $tag @@ -329,17 +304,22 @@ jobs: $tag = $env:RELEASE_TAG $prereleaseArgs = @() if ($tag -match '^\d+\.\d+\.\d+-') { $prereleaseArgs += '--prerelease' } - $assets = Get-ChildItem -Path 'output' -File | Where-Object { - $_.Name -match '^MinecraftServerManager.*\.exe$' -or - $_.Name -match '^MinecraftServerManager.*\.exe\.sha256$' -or - $_.Name -match '^MinecraftServerManager.*\.exe\.intoto\.jsonl$' -or - $_.Name -match '^MinecraftServerManager.*\.spdx\.json$' -or - $_.Name -match '^MinecraftServerManager.*\.spdx\.json\.sha256$' - } | ForEach-Object { $_.FullName } - if (-not $assets -or $assets.Count -eq 0) { - Write-Error "No release executable was found in output." - exit 1 + $expectedAssets = @( + 'MinecraftServerManager-self-contained.exe', + 'MinecraftServerManager-self-contained.exe.sha256', + 'MinecraftServerManager-self-contained.exe.intoto.jsonl', + 'MinecraftServerManager-framework-dependent.exe', + 'MinecraftServerManager-framework-dependent.exe.sha256', + 'MinecraftServerManager-framework-dependent.exe.intoto.jsonl', + 'MinecraftServerManager.spdx.json', + 'MinecraftServerManager.spdx.json.sha256' + ) + $actualNames = @(Get-ChildItem -Path 'output' -File | Select-Object -ExpandProperty Name) + $missingAssets = @($expectedAssets | Where-Object { $_ -notin $actualNames }) + if ($missingAssets.Count -gt 0) { + throw "Release payload 缺少必要資產:$($missingAssets -join ', ')" } + $assets = @($expectedAssets | ForEach-Object { Join-Path 'output' $_ }) $ghArgs = @('release', 'create', $tag) + $assets + @( '--title', "Minecraft Server Manager $tag", '--notes-file', 'release_notes.md', @@ -386,14 +366,30 @@ jobs: Write-Host "成功下載 $($downloadedFiles.Count) 個發布資產:" $downloadedFiles | Select-Object Name, Length | Format-Table -AutoSize + $expectedAssets = @( + 'MinecraftServerManager-self-contained.exe', + 'MinecraftServerManager-self-contained.exe.sha256', + 'MinecraftServerManager-self-contained.exe.intoto.jsonl', + 'MinecraftServerManager-framework-dependent.exe', + 'MinecraftServerManager-framework-dependent.exe.sha256', + 'MinecraftServerManager-framework-dependent.exe.intoto.jsonl', + 'MinecraftServerManager.spdx.json', + 'MinecraftServerManager.spdx.json.sha256' + ) + $actualNames = @($downloadedFiles | Select-Object -ExpandProperty Name) + $missingAssets = @($expectedAssets | Where-Object { $_ -notin $actualNames }) + if ($missingAssets.Count -gt 0) { + throw "Release 資產不完整,缺少:$($missingAssets -join ', ')" + } + $exeFiles = @(Get-ChildItem -LiteralPath $verifyDir -Filter '*.exe' -File) - if ($exeFiles.Count -eq 0) { - throw "驗證失敗:Release 中未找到任何 .exe 執行檔" + if ($exeFiles.Count -ne 2) { + throw "驗證失敗:預期 2 個 .exe,實際找到 $($exeFiles.Count) 個" } $shaFiles = @(Get-ChildItem -LiteralPath $verifyDir -Filter '*.sha256' -File) - if ($shaFiles.Count -eq 0) { - throw "驗證失敗:Release 中未找到任何 .sha256 雜湊檔" + if ($shaFiles.Count -ne 3) { + throw "驗證失敗:預期 3 個 .sha256,實際找到 $($shaFiles.Count) 個" } Write-Host "--- 步驟 1:比對 SHA-256 雜湊值 ---" @@ -418,7 +414,7 @@ jobs: } } - Write-Host "--- 步驟 2:驗證 .exe 的 GitHub SLSA Attestation ---" + Write-Host "--- 步驟 2:驗證 .exe 的 GitHub SLSA Attestation(同時確認與建置產物摘要一致) ---" foreach ($exe in $exeFiles) { Write-Host "正在驗證 $($exe.Name) 的簽章證明..." gh attestation verify $exe.FullName --repo $env:GITHUB_REPOSITORY @@ -428,21 +424,6 @@ jobs: Write-Host " [Attestation 驗證通過] $($exe.Name)" } - Write-Host "--- 步驟 3:比對建置產物一致性(本地 output/ vs 遠端 Release) ---" - if (Test-Path -LiteralPath 'output') { - foreach ($exe in $exeFiles) { - $localExe = Join-Path 'output' $exe.Name - if (Test-Path -LiteralPath $localExe -PathType Leaf) { - $localHash = (Get-FileHash -LiteralPath $localExe -Algorithm SHA256).Hash.ToUpperInvariant() - $remoteHash = (Get-FileHash -LiteralPath $exe.FullName -Algorithm SHA256).Hash.ToUpperInvariant() - if ($localHash -ne $remoteHash) { - throw "一致性校驗失敗:$($exe.Name) 本地建置雜湊 ($localHash) 與 Release 遠端雜湊 ($remoteHash) 不符!" - } - Write-Host " [工作區一致性驗證通過] $($exe.Name)" - } - } - } - Write-Host "=== 發布後端對端資產驗證全數通過!===" } finally { diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ca9daba..52d5acb 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -57,7 +57,7 @@ jobs: env: MSBUILDDISABLENODEREUSE: "1" DOTNET_CLI_DO_NOT_USE_MSBUILD_SERVER: "1" - run: dotnet build src/MinecraftServerManager.App/MinecraftServerManager.App.csproj -c Release + run: dotnet build src/MinecraftServerManager.App/MinecraftServerManager.App.csproj -c Release -p:UseSharedCompilation=false - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 diff --git a/.github/workflows/oss-scorecard.yml b/.github/workflows/oss-scorecard.yml index 2ad47c8..0fcc78e 100644 --- a/.github/workflows/oss-scorecard.yml +++ b/.github/workflows/oss-scorecard.yml @@ -15,6 +15,7 @@ jobs: analysis: name: Scorecard analyze runs-on: ubuntu-latest + timeout-minutes: 15 permissions: security-events: write id-token: write @@ -25,7 +26,6 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - fetch-depth: 0 - name: "執行 Scorecard 分析" uses: ossf/scorecard-action@fa7117db83c52d787ae5a89f350999701505d78e # v2.5.0 (Scorecard v5.5.0) @@ -39,10 +39,3 @@ jobs: with: sarif_file: openossf_scorecard_report.sarif category: "OpenSSF Scorecard" - - - name: "上傳 Scorecard 結果檔案" - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: openossf_scorecard_report - path: openossf_scorecard_report.sarif - retention-days: 2 diff --git a/.github/workflows/validate-renovate.yml b/.github/workflows/validate-renovate.yml index 4e1f2be..ff78bc6 100644 --- a/.github/workflows/validate-renovate.yml +++ b/.github/workflows/validate-renovate.yml @@ -1,13 +1,8 @@ name: Validate Renovate config on: - push: - paths: - - "renovate.json" - - ".github/renovate.json" pull_request: paths: - - "renovate.json" - ".github/renovate.json" permissions: