Publish Windows x64 release v1.8.3 #34
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: Publish Windows Release | |
| run-name: >- | |
| Publish Windows x64 release ${{ github.ref_name }} | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: Existing version tag to build and publish | |
| required: true | |
| type: string | |
| expected_source_sha: | |
| description: Expected tag commit; defaults to the selected workflow ref | |
| required: false | |
| type: string | |
| permissions: | |
| actions: write | |
| checks: read | |
| contents: write | |
| statuses: write | |
| concurrency: | |
| group: windows-release-${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }} | |
| cancel-in-progress: false | |
| jobs: | |
| release: | |
| name: Build and publish Windows x64 release | |
| runs-on: windows-latest | |
| timeout-minutes: 90 | |
| steps: | |
| - name: Resolve release tag | |
| id: version | |
| shell: pwsh | |
| env: | |
| PUSH_TAG: ${{ github.ref_name }} | |
| MANUAL_TAG: ${{ inputs.tag }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| run: | | |
| $tag = if ($env:EVENT_NAME -eq 'workflow_dispatch') { $env:MANUAL_TAG } else { $env:PUSH_TAG } | |
| if ($tag -notmatch '^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$') { | |
| throw "Release tag must match v<major>.<minor>.<patch>[-suffix]: $tag" | |
| } | |
| "tag=$tag" >> $env:GITHUB_OUTPUT | |
| "version=$($tag.Substring(1))" >> $env:GITHUB_OUTPUT | |
| - name: Checkout release tag | |
| uses: actions/checkout@v5 | |
| with: | |
| ref: ${{ steps.version.outputs.tag }} | |
| fetch-depth: 0 | |
| - name: Verify release source commit | |
| id: source | |
| shell: pwsh | |
| env: | |
| EXPECTED_SOURCE_SHA: ${{ inputs.expected_source_sha || github.sha }} | |
| RELEASE_TAG: ${{ steps.version.outputs.tag }} | |
| run: | | |
| if ($env:EXPECTED_SOURCE_SHA -notmatch '^[0-9a-fA-F]{40}$') { | |
| throw "Expected release source must be a full commit SHA." | |
| } | |
| $releaseSha = git rev-parse HEAD | |
| if ($LASTEXITCODE -ne 0 -or | |
| [string]::IsNullOrWhiteSpace($releaseSha)) { | |
| throw "Failed to resolve release tag $env:RELEASE_TAG." | |
| } | |
| $releaseSha = $releaseSha.Trim() | |
| if (-not [string]::Equals($releaseSha, $env:EXPECTED_SOURCE_SHA, | |
| [StringComparison]::OrdinalIgnoreCase)) { | |
| throw "Release tag $env:RELEASE_TAG resolves to $releaseSha, but the expected source is $env:EXPECTED_SOURCE_SHA." | |
| } | |
| "sha=$releaseSha" >> $env:GITHUB_OUTPUT | |
| - name: Set up .NET 10 SDK | |
| uses: actions/setup-dotnet@v5 | |
| with: | |
| dotnet-version: 10.0.x | |
| - name: Set up Python for USB touch bridge | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.13' | |
| - name: Set up MSYS2 UCRT64 for UxPlay | |
| uses: msys2/setup-msys2@v2 | |
| with: | |
| msystem: UCRT64 | |
| # Avoid a full MSYS2 mirror upgrade on every run. The action still | |
| # installs the pinned build dependencies below, while avoiding | |
| # unrelated mirror timeouts during database refresh. | |
| update: false | |
| install: >- | |
| mingw-w64-ucrt-x86_64-cmake | |
| mingw-w64-ucrt-x86_64-ninja | |
| mingw-w64-ucrt-x86_64-toolchain | |
| mingw-w64-ucrt-x86_64-pkgconf | |
| mingw-w64-ucrt-x86_64-gstreamer | |
| mingw-w64-ucrt-x86_64-gst-plugins-base | |
| mingw-w64-ucrt-x86_64-gst-plugins-good | |
| mingw-w64-ucrt-x86_64-gst-plugins-bad | |
| mingw-w64-ucrt-x86_64-gst-libav | |
| mingw-w64-ucrt-x86_64-libplist | |
| mingw-w64-ucrt-x86_64-openssl | |
| - name: Install Microsoft SBOM Tool | |
| shell: pwsh | |
| run: | | |
| $tool = Get-Command sbom-tool,sbom -ErrorAction SilentlyContinue | | |
| Select-Object -First 1 | |
| if ($null -eq $tool) { | |
| dotnet tool install --global Microsoft.Sbom.DotNetTool | |
| if ($LASTEXITCODE -ne 0) { throw "Failed to install Microsoft SBOM Tool." } | |
| } | |
| - name: Verify localization | |
| shell: pwsh | |
| run: ./scripts/verify_localization.ps1 | |
| - name: Build and test | |
| shell: pwsh | |
| run: ./build.ps1 -Configuration Release | |
| - name: Package release assets | |
| shell: pwsh | |
| run: >- | |
| ./scripts/package_release.ps1 | |
| -Version '${{ steps.version.outputs.version }}' | |
| -SkipBuild | |
| -GenerateSbom | |
| -UpdateReleaseManifest | |
| - name: Prepare release notes | |
| id: notes | |
| shell: pwsh | |
| env: | |
| DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} | |
| WORKFLOW_SHA: ${{ github.sha }} | |
| RELEASE_TAG: ${{ steps.version.outputs.tag }} | |
| RELEASE_VERSION: ${{ steps.version.outputs.version }} | |
| run: | | |
| $expectedNames = @( | |
| "iPhoneMirror-Setup-v$env:RELEASE_VERSION-x64.exe", | |
| "iPhoneMirror-v$env:RELEASE_VERSION-win-x64.zip", | |
| "iPhoneMirror-v$env:RELEASE_VERSION-win-x64-sbom.spdx.json", | |
| "SHA256SUMS.txt" | |
| ) | |
| $descriptions = @{ | |
| $expectedNames[0] = 'Windows x64 安装程序,支持覆盖升级与卸载。' | |
| $expectedNames[1] = '免安装便携版,完整解压后运行。' | |
| $expectedNames[2] = 'SPDX 2.2 软件物料清单。' | |
| $expectedNames[3] = '全部公开 Release 资产的 SHA-256 校验清单。' | |
| } | |
| $manifestPath = Join-Path $env:GITHUB_WORKSPACE 'updates/releases.json' | |
| $manifestJson = [IO.File]::ReadAllText( | |
| $manifestPath, [Text.Encoding]::UTF8) | ConvertFrom-Json | |
| $manifestReleases = if ($manifestJson -is [array]) { | |
| @($manifestJson) | |
| } | |
| elseif ($manifestJson.PSObject.Properties.Name -contains 'value') { | |
| @($manifestJson.value) | |
| } | |
| else { | |
| @($manifestJson) | |
| } | |
| $manifestMatches = @($manifestReleases | Where-Object { | |
| [string]::Equals([string]$_.tag_name, $env:RELEASE_TAG, | |
| [StringComparison]::OrdinalIgnoreCase) | |
| }) | |
| if ($manifestMatches.Count -ne 1) { | |
| throw "Generated manifest must contain exactly one $env:RELEASE_TAG entry." | |
| } | |
| $manifestAssets = @($manifestMatches[0].assets) | |
| if ($manifestAssets.Count -ne $expectedNames.Count) { | |
| throw "Generated manifest must contain exactly $($expectedNames.Count) release assets." | |
| } | |
| $assetRecords = @($expectedNames | ForEach-Object { | |
| $name = $_ | |
| $path = Join-Path $env:GITHUB_WORKSPACE "outputs/releases/$name" | |
| if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { | |
| throw "Expected release asset is missing: $name" | |
| } | |
| $item = Get-Item -LiteralPath $path | |
| $hash = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant() | |
| $manifestAsset = @($manifestAssets | Where-Object { | |
| [string]::Equals([string]$_.name, $name, | |
| [StringComparison]::Ordinal) | |
| }) | |
| if ($manifestAsset.Count -ne 1 -or | |
| [long]$manifestAsset[0].size -ne $item.Length -or | |
| -not [string]::Equals([string]$manifestAsset[0].digest, | |
| "sha256:$hash", [StringComparison]::OrdinalIgnoreCase)) { | |
| throw "Generated manifest does not match the local release asset: $name" | |
| } | |
| [PSCustomObject]@{ | |
| Name = $name | |
| Path = $path | |
| Size = [long]$item.Length | |
| Hash = $hash | |
| } | |
| }) | |
| $checksumPath = Join-Path $env:GITHUB_WORKSPACE ` | |
| 'outputs/releases/SHA256SUMS.txt' | |
| $checksumEntries = @(Get-Content -LiteralPath $checksumPath -Encoding utf8 | | |
| ForEach-Object { | |
| if ($_ -notmatch '^([0-9a-fA-F]{64}) (.+)$') { | |
| throw "Invalid SHA256SUMS.txt entry: $_" | |
| } | |
| [PSCustomObject]@{ | |
| Hash = $Matches[1].ToLowerInvariant() | |
| Name = $Matches[2] | |
| } | |
| }) | |
| $hashedAssets = @($assetRecords | Where-Object { | |
| $_.Name -ne 'SHA256SUMS.txt' | |
| }) | |
| if ($checksumEntries.Count -ne $hashedAssets.Count) { | |
| throw 'SHA256SUMS.txt must cover every non-checksum release asset exactly once.' | |
| } | |
| foreach ($asset in $hashedAssets) { | |
| $matches = @($checksumEntries | Where-Object { $_.Name -eq $asset.Name }) | |
| if ($matches.Count -ne 1 -or $matches[0].Hash -ne $asset.Hash) { | |
| throw "SHA256SUMS.txt does not match $($asset.Name)." | |
| } | |
| } | |
| git fetch origin ` | |
| "refs/heads/$env:DEFAULT_BRANCH`:refs/remotes/origin/$env:DEFAULT_BRANCH" | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Failed to fetch release notes from $env:DEFAULT_BRANCH." | |
| } | |
| $templateSpec = "$env:WORKFLOW_SHA`:docs/releases/$env:RELEASE_TAG.md" | |
| $templateLines = @(git show $templateSpec) | |
| if ($LASTEXITCODE -ne 0 -or $templateLines.Count -eq 0) { | |
| throw "Release notes template is missing: docs/releases/$env:RELEASE_TAG.md" | |
| } | |
| $template = ($templateLines -join "`n").Trim() | |
| if ([regex]::IsMatch($template, | |
| '(?mi)^#{1,6}[ \t]*(?:中文|英文)[ \t]*$')) { | |
| throw 'Release notes must not contain standalone Chinese or English labels.' | |
| } | |
| if ([regex]::IsMatch($template, | |
| '(?mi)^##[ \t]+发布资产[ \t]*$|<!-- release-assets:')) { | |
| throw 'The release notes template must not contain a generated asset section.' | |
| } | |
| $firstChinese = [regex]::Match($template, '[\u3400-\u9fff]') | |
| $firstEnglishHeading = [regex]::Match($template, | |
| '(?m)^### (?:Added|Changed|Fixed|Removed|Security|Notes)$') | |
| if (-not $firstChinese.Success -or -not $firstEnglishHeading.Success -or | |
| $firstChinese.Index -gt $firstEnglishHeading.Index) { | |
| throw 'Release notes must place the Chinese content above the English content.' | |
| } | |
| $assetSection = [Collections.Generic.List[string]]::new() | |
| $assetSection.Add('<!-- release-assets:start -->') | |
| $assetSection.Add('## 发布资产') | |
| $assetSection.Add('') | |
| foreach ($asset in $assetRecords) { | |
| $assetSection.Add("- ``$($asset.Name)``:$($descriptions[$asset.Name])") | |
| } | |
| $assetSection.Add('') | |
| $assetSection.Add('| 资产 | 大小 | SHA-256 |') | |
| $assetSection.Add('| --- | ---: | --- |') | |
| foreach ($asset in $assetRecords) { | |
| $size = $asset.Size.ToString('N0', [Globalization.CultureInfo]::InvariantCulture) | |
| $assetSection.Add("| ``$($asset.Name)`` | $size bytes | ``$($asset.Hash)`` |") | |
| } | |
| $assetSection.Add('<!-- release-assets:end -->') | |
| $notesPath = Join-Path $env:RUNNER_TEMP ` | |
| "release-notes-$env:GITHUB_RUN_ID-$env:GITHUB_RUN_ATTEMPT.md" | |
| $notes = $template + "`n`n" + ($assetSection -join "`n") + "`n" | |
| [IO.File]::WriteAllText($notesPath, $notes, | |
| [Text.UTF8Encoding]::new($false)) | |
| "path=$notesPath" >> $env:GITHUB_OUTPUT | |
| - name: Create draft release when needed | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_TAG: ${{ steps.version.outputs.tag }} | |
| RELEASE_NOTES: ${{ steps.notes.outputs.path }} | |
| run: | | |
| $releaseJson = gh release view $env:RELEASE_TAG ` | |
| --repo $env:GITHUB_REPOSITORY --json isDraft 2> $null | |
| if ($LASTEXITCODE -eq 0) { | |
| $release = $releaseJson | ConvertFrom-Json | |
| if (-not $release.isDraft) { | |
| throw "Release $env:RELEASE_TAG is already public; refusing to overwrite its assets." | |
| } | |
| exit 0 | |
| } | |
| gh release create $env:RELEASE_TAG ` | |
| --repo $env:GITHUB_REPOSITORY ` | |
| --draft ` | |
| --notes-file $env:RELEASE_NOTES ` | |
| --verify-tag ` | |
| --title "iPhoneMirror $env:RELEASE_TAG" | |
| if ($LASTEXITCODE -ne 0) { throw "Failed to create draft release $env:RELEASE_TAG" } | |
| - name: Upload release assets directly | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_TAG: ${{ steps.version.outputs.tag }} | |
| RELEASE_VERSION: ${{ steps.version.outputs.version }} | |
| run: | | |
| $expectedNames = @( | |
| "iPhoneMirror-Setup-v$env:RELEASE_VERSION-x64.exe", | |
| "iPhoneMirror-v$env:RELEASE_VERSION-win-x64.zip", | |
| "iPhoneMirror-v$env:RELEASE_VERSION-win-x64-sbom.spdx.json", | |
| "SHA256SUMS.txt" | |
| ) | |
| $assets = @($expectedNames | ForEach-Object { | |
| $path = Join-Path outputs/releases $_ | |
| if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { | |
| throw "Expected release asset is missing: $_" | |
| } | |
| Get-Item -LiteralPath $path | |
| }) | |
| $unexpected = @(Get-ChildItem -LiteralPath outputs/releases -File | | |
| Where-Object { $_.Name -notin $expectedNames }) | |
| if ($unexpected.Count -ne 0) { | |
| throw "Unexpected release assets: $($unexpected.Name -join ', ')" | |
| } | |
| $assetPaths = @($assets.FullName) | |
| gh release upload $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY ` | |
| @assetPaths --clobber | |
| if ($LASTEXITCODE -ne 0) { throw "Failed to upload release assets for $env:RELEASE_TAG" } | |
| - name: Verify uploaded assets and update release notes | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_TAG: ${{ steps.version.outputs.tag }} | |
| RELEASE_VERSION: ${{ steps.version.outputs.version }} | |
| RELEASE_NOTES: ${{ steps.notes.outputs.path }} | |
| run: | | |
| $expectedNames = @( | |
| "iPhoneMirror-Setup-v$env:RELEASE_VERSION-x64.exe", | |
| "iPhoneMirror-v$env:RELEASE_VERSION-win-x64.zip", | |
| "iPhoneMirror-v$env:RELEASE_VERSION-win-x64-sbom.spdx.json", | |
| "SHA256SUMS.txt" | |
| ) | |
| $localAssets = @($expectedNames | ForEach-Object { | |
| $path = Join-Path $env:GITHUB_WORKSPACE "outputs/releases/$_" | |
| $item = Get-Item -LiteralPath $path | |
| [PSCustomObject]@{ | |
| Name = $_ | |
| Size = [long]$item.Length | |
| Digest = 'sha256:' + (Get-FileHash -LiteralPath $path ` | |
| -Algorithm SHA256).Hash.ToLowerInvariant() | |
| } | |
| }) | |
| function Get-ReleaseState { | |
| $json = gh release view $env:RELEASE_TAG ` | |
| --repo $env:GITHUB_REPOSITORY --json body,isDraft,assets | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Failed to inspect release $env:RELEASE_TAG." | |
| } | |
| return $json | ConvertFrom-Json | |
| } | |
| function Assert-ReleaseAssets([object]$Release) { | |
| if (-not $Release.isDraft) { | |
| throw "Release $env:RELEASE_TAG became public before validation completed." | |
| } | |
| $remoteAssets = @($Release.assets) | |
| if ($remoteAssets.Count -ne $localAssets.Count) { | |
| throw "Release $env:RELEASE_TAG has an unexpected asset count." | |
| } | |
| foreach ($local in $localAssets) { | |
| $matches = @($remoteAssets | Where-Object { | |
| [string]::Equals([string]$_.name, $local.Name, | |
| [StringComparison]::Ordinal) | |
| }) | |
| if ($matches.Count -ne 1 -or | |
| [string]$matches[0].state -ne 'uploaded' -or | |
| [long]$matches[0].size -ne $local.Size -or | |
| -not [string]::Equals([string]$matches[0].digest, | |
| $local.Digest, [StringComparison]::OrdinalIgnoreCase)) { | |
| throw "Uploaded release asset does not match the build output: $($local.Name)" | |
| } | |
| } | |
| } | |
| $release = Get-ReleaseState | |
| Assert-ReleaseAssets $release | |
| gh release edit $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY ` | |
| --notes-file $env:RELEASE_NOTES | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Failed to update release notes for $env:RELEASE_TAG." | |
| } | |
| $updatedRelease = Get-ReleaseState | |
| Assert-ReleaseAssets $updatedRelease | |
| $expectedBody = [IO.File]::ReadAllText( | |
| $env:RELEASE_NOTES, [Text.Encoding]::UTF8) -replace "`r`n", "`n" | |
| $actualBody = ([string]$updatedRelease.body) -replace "`r`n", "`n" | |
| if (-not [string]::Equals($actualBody.TrimEnd(), | |
| $expectedBody.TrimEnd(), [StringComparison]::Ordinal)) { | |
| throw "Published release notes do not match the generated notes for $env:RELEASE_TAG." | |
| } | |
| - name: Prepare fallback release manifest commit | |
| id: manifest | |
| shell: pwsh | |
| env: | |
| DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} | |
| RELEASE_TAG: ${{ steps.version.outputs.tag }} | |
| run: | | |
| git diff --quiet -- updates/releases.json | |
| $packageDiffExit = $LASTEXITCODE | |
| if ($packageDiffExit -eq 0) { | |
| throw "Packaging did not update updates/releases.json for $env:RELEASE_TAG." | |
| } | |
| if ($packageDiffExit -ne 1) { | |
| throw "Failed to inspect the generated release manifest." | |
| } | |
| $generatedManifest = Join-Path $env:RUNNER_TEMP ` | |
| "releases-$env:GITHUB_RUN_ID-$env:GITHUB_RUN_ATTEMPT.json" | |
| Copy-Item -LiteralPath updates/releases.json ` | |
| -Destination $generatedManifest -Force | |
| git fetch origin ` | |
| "refs/heads/$env:DEFAULT_BRANCH`:refs/remotes/origin/$env:DEFAULT_BRANCH" | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Failed to fetch $env:DEFAULT_BRANCH before publishing the release manifest." | |
| } | |
| git checkout --force --detach "refs/remotes/origin/$env:DEFAULT_BRANCH" | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Failed to check out the latest $env:DEFAULT_BRANCH commit." | |
| } | |
| function Read-ReleaseManifest([string]$Path) { | |
| $parsed = [IO.File]::ReadAllText( | |
| $Path, [Text.Encoding]::UTF8) | ConvertFrom-Json | |
| if ($parsed -is [array]) { return @($parsed) } | |
| if ($parsed.PSObject.Properties.Name -contains 'value') { | |
| return @($parsed.value) | |
| } | |
| return @($parsed) | |
| } | |
| $generatedReleases = @(Read-ReleaseManifest $generatedManifest) | |
| $generatedMatches = @($generatedReleases | Where-Object { | |
| [string]::Equals([string]$_.tag_name, $env:RELEASE_TAG, | |
| [StringComparison]::OrdinalIgnoreCase) | |
| }) | |
| if ($generatedMatches.Count -ne 1) { | |
| throw "Generated manifest must contain exactly one $env:RELEASE_TAG entry." | |
| } | |
| $manifestPath = Join-Path $env:GITHUB_WORKSPACE 'updates/releases.json' | |
| $currentReleases = @(Read-ReleaseManifest $manifestPath) | |
| $currentMatches = @($currentReleases | Where-Object { | |
| [string]::Equals([string]$_.tag_name, $env:RELEASE_TAG, | |
| [StringComparison]::OrdinalIgnoreCase) | |
| }) | |
| if ($currentMatches.Count -gt 1) { | |
| throw "Current manifest contains duplicate $env:RELEASE_TAG entries." | |
| } | |
| if ($currentMatches.Count -eq 1) { | |
| $generatedMatches[0].published_at = $currentMatches[0].published_at | |
| } | |
| $mergedReleases = @($generatedMatches[0]) + @($currentReleases | | |
| Where-Object { | |
| -not [string]::Equals([string]$_.tag_name, $env:RELEASE_TAG, | |
| [StringComparison]::OrdinalIgnoreCase) | |
| }) | |
| $mergedJson = ConvertTo-Json -InputObject $mergedReleases -Depth 100 | |
| [IO.File]::WriteAllText($manifestPath, $mergedJson + "`n", | |
| [Text.UTF8Encoding]::new($false)) | |
| git diff --quiet -- updates/releases.json | |
| $mergedDiffExit = $LASTEXITCODE | |
| if ($mergedDiffExit -eq 0) { | |
| Write-Host "Release manifest already contains $env:RELEASE_TAG." | |
| "changed=false" >> $env:GITHUB_OUTPUT | |
| exit 0 | |
| } | |
| if ($mergedDiffExit -ne 1) { | |
| throw "Failed to inspect the merged release manifest." | |
| } | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add -- updates/releases.json | |
| git commit -m "Update $env:RELEASE_TAG release manifest" | |
| if ($LASTEXITCODE -ne 0) { throw "Failed to commit the release manifest." } | |
| $changedPaths = @(git diff-tree --no-commit-id --name-only -r HEAD | | |
| Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) | |
| if ($changedPaths.Count -ne 1 -or | |
| $changedPaths[0] -ne 'updates/releases.json') { | |
| throw "Release manifest commit contains unexpected paths: $($changedPaths -join ', ')" | |
| } | |
| $manifestSha = (git rev-parse HEAD).Trim() | |
| $parentSha = (git rev-parse HEAD^).Trim() | |
| $defaultBranchSha = (git rev-parse ` | |
| "refs/remotes/origin/$env:DEFAULT_BRANCH").Trim() | |
| if ($parentSha -ne $defaultBranchSha) { | |
| throw "Release manifest commit is not based on the latest $env:DEFAULT_BRANCH." | |
| } | |
| $safeTag = $env:RELEASE_TAG -replace '[^0-9A-Za-z.-]', '-' | |
| $manifestBranch = "automation/release-manifest-$safeTag-$env:GITHUB_RUN_ID-$env:GITHUB_RUN_ATTEMPT" | |
| "changed=true" >> $env:GITHUB_OUTPUT | |
| "branch=$manifestBranch" >> $env:GITHUB_OUTPUT | |
| "sha=$manifestSha" >> $env:GITHUB_OUTPUT | |
| git push origin "HEAD:refs/heads/$manifestBranch" | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Failed to push temporary manifest branch $manifestBranch." | |
| } | |
| - name: Validate fallback release manifest commit | |
| id: validation | |
| if: steps.manifest.outputs.changed == 'true' | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| MANIFEST_BRANCH: ${{ steps.manifest.outputs.branch }} | |
| MANIFEST_SHA: ${{ steps.manifest.outputs.sha }} | |
| run: | | |
| gh workflow run windows-build.yml --repo $env:GITHUB_REPOSITORY ` | |
| --ref $env:MANIFEST_BRANCH | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Failed to dispatch manifest validation for $env:MANIFEST_BRANCH." | |
| } | |
| $validationRun = $null | |
| for ($attempt = 0; $attempt -lt 60 -and $null -eq $validationRun; ++$attempt) { | |
| $runsJson = gh run list --repo $env:GITHUB_REPOSITORY ` | |
| --workflow windows-build.yml ` | |
| --branch $env:MANIFEST_BRANCH ` | |
| --event workflow_dispatch ` | |
| --limit 10 ` | |
| --json databaseId,headSha,createdAt | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Failed to query the manifest validation run." | |
| } | |
| $validationRun = @($runsJson | ConvertFrom-Json | | |
| Where-Object { $_.headSha -eq $env:MANIFEST_SHA } | | |
| Sort-Object createdAt -Descending | | |
| Select-Object -First 1)[0] | |
| if ($null -eq $validationRun) { Start-Sleep -Seconds 2 } | |
| } | |
| if ($null -eq $validationRun) { | |
| throw "Timed out waiting for manifest validation to start." | |
| } | |
| "run_id=$($validationRun.databaseId)" >> $env:GITHUB_OUTPUT | |
| "run_url=$env:GITHUB_SERVER_URL/$env:GITHUB_REPOSITORY/actions/runs/$($validationRun.databaseId)" >> ` | |
| $env:GITHUB_OUTPUT | |
| gh run watch $validationRun.databaseId --repo $env:GITHUB_REPOSITORY ` | |
| --exit-status | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Manifest validation failed: $($validationRun.databaseId)." | |
| } | |
| $checksJson = gh api ` | |
| "repos/$env:GITHUB_REPOSITORY/commits/$env:MANIFEST_SHA/check-runs" | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Failed to inspect manifest validation checks." | |
| } | |
| $matchingChecks = @(($checksJson | ConvertFrom-Json).check_runs | | |
| Where-Object { | |
| $_.name -eq 'build-test' -and | |
| $_.app.slug -eq 'github-actions' -and | |
| $_.conclusion -eq 'success' | |
| }) | |
| if ($matchingChecks.Count -eq 0) { | |
| throw "The manifest commit has no successful GitHub Actions build-test check." | |
| } | |
| $validationUrl = "$env:GITHUB_SERVER_URL/$env:GITHUB_REPOSITORY/actions/runs/$($validationRun.databaseId)" | |
| $statusJson = gh api --method POST ` | |
| "repos/$env:GITHUB_REPOSITORY/statuses/$env:MANIFEST_SHA" ` | |
| -f state=success ` | |
| -f context=build-test ` | |
| -f target_url=$validationUrl ` | |
| -f description="Validated by Windows build run $($validationRun.databaseId)" | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Failed to relay the successful manifest validation status." | |
| } | |
| $status = $statusJson | ConvertFrom-Json | |
| if ($status.state -ne 'success' -or | |
| $status.context -ne 'build-test' -or | |
| $status.target_url -ne $validationUrl -or | |
| $status.creator.login -ne 'github-actions[bot]') { | |
| throw "Relayed manifest validation status is invalid." | |
| } | |
| "status_id=$($status.id)" >> $env:GITHUB_OUTPUT | |
| - name: Verify release tag before publication | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_SOURCE_SHA: ${{ steps.source.outputs.sha }} | |
| RELEASE_TAG: ${{ steps.version.outputs.tag }} | |
| run: | | |
| $tagRefJson = gh api ` | |
| "repos/$env:GITHUB_REPOSITORY/git/ref/tags/$env:RELEASE_TAG" | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Failed to inspect release tag $env:RELEASE_TAG." | |
| } | |
| $tagObject = ($tagRefJson | ConvertFrom-Json).object | |
| while ($tagObject.type -eq 'tag') { | |
| $tagJson = gh api ` | |
| "repos/$env:GITHUB_REPOSITORY/git/tags/$($tagObject.sha)" | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Failed to peel release tag $env:RELEASE_TAG." | |
| } | |
| $tagObject = ($tagJson | ConvertFrom-Json).object | |
| } | |
| if ($tagObject.type -ne 'commit' -or | |
| -not [string]::Equals([string]$tagObject.sha, | |
| $env:RELEASE_SOURCE_SHA, | |
| [StringComparison]::OrdinalIgnoreCase)) { | |
| throw "Release tag $env:RELEASE_TAG moved during the release workflow." | |
| } | |
| - name: Preflight fallback release manifest | |
| if: steps.manifest.outputs.changed == 'true' | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} | |
| MANIFEST_BRANCH: ${{ steps.manifest.outputs.branch }} | |
| MANIFEST_SHA: ${{ steps.manifest.outputs.sha }} | |
| VALIDATION_RUN_URL: ${{ steps.validation.outputs.run_url }} | |
| VALIDATION_STATUS_ID: ${{ steps.validation.outputs.status_id }} | |
| run: | | |
| git fetch origin ` | |
| "refs/heads/$env:DEFAULT_BRANCH`:refs/remotes/origin/$env:DEFAULT_BRANCH" | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Failed to refresh $env:DEFAULT_BRANCH before publishing the manifest." | |
| } | |
| $parentSha = (git rev-parse HEAD^).Trim() | |
| $defaultBranchSha = (git rev-parse ` | |
| "refs/remotes/origin/$env:DEFAULT_BRANCH").Trim() | |
| if ($parentSha -ne $defaultBranchSha) { | |
| throw "$env:DEFAULT_BRANCH advanced during manifest validation; rerun the release workflow." | |
| } | |
| $statusesJson = gh api ` | |
| "repos/$env:GITHUB_REPOSITORY/commits/$env:MANIFEST_SHA/statuses" | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Failed to inspect the relayed manifest validation status." | |
| } | |
| $statusId = [long]$env:VALIDATION_STATUS_ID | |
| if ($statusId -le 0) { | |
| throw "The relayed manifest validation status ID is invalid." | |
| } | |
| $statuses = @($statusesJson | ConvertFrom-Json) | |
| $matchingStatuses = @($statuses | | |
| Where-Object { | |
| [long]$_.id -eq $statusId -and | |
| $_.context -eq 'build-test' -and | |
| $_.state -eq 'success' -and | |
| $_.target_url -eq $env:VALIDATION_RUN_URL -and | |
| $_.creator.login -eq 'github-actions[bot]' | |
| }) | |
| if ($matchingStatuses.Count -eq 0) { | |
| throw "The manifest commit has no valid relayed build-test status." | |
| } | |
| $headSha = (git rev-parse HEAD).Trim() | |
| if ($LASTEXITCODE -ne 0 -or $headSha -ne $env:MANIFEST_SHA) { | |
| throw 'The checked-out manifest commit does not match the validated SHA.' | |
| } | |
| $branchJson = gh api ` | |
| "repos/$env:GITHUB_REPOSITORY/git/ref/heads/$env:MANIFEST_BRANCH" | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Failed to inspect temporary branch $env:MANIFEST_BRANCH." | |
| } | |
| $branchSha = [string](($branchJson | ConvertFrom-Json).object.sha) | |
| if ($branchSha -ne $env:MANIFEST_SHA) { | |
| throw "Temporary branch $env:MANIFEST_BRANCH no longer points to the validated commit." | |
| } | |
| - name: Publish release and fallback manifest | |
| id: transaction | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} | |
| RELEASE_TAG: ${{ steps.version.outputs.tag }} | |
| RELEASE_VERSION: ${{ steps.version.outputs.version }} | |
| RELEASE_NOTES: ${{ steps.notes.outputs.path }} | |
| MANIFEST_CHANGED: ${{ steps.manifest.outputs.changed }} | |
| MANIFEST_SHA: ${{ steps.manifest.outputs.sha }} | |
| run: | | |
| $expectedNames = @( | |
| "iPhoneMirror-Setup-v$env:RELEASE_VERSION-x64.exe", | |
| "iPhoneMirror-v$env:RELEASE_VERSION-win-x64.zip", | |
| "iPhoneMirror-v$env:RELEASE_VERSION-win-x64-sbom.spdx.json", | |
| "SHA256SUMS.txt" | |
| ) | |
| $localAssets = @($expectedNames | ForEach-Object { | |
| $path = Join-Path $env:GITHUB_WORKSPACE "outputs/releases/$_" | |
| $item = Get-Item -LiteralPath $path | |
| [PSCustomObject]@{ | |
| Name = $_ | |
| Size = [long]$item.Length | |
| Digest = 'sha256:' + (Get-FileHash -LiteralPath $path ` | |
| -Algorithm SHA256).Hash.ToLowerInvariant() | |
| } | |
| }) | |
| function Read-ManifestReleases([string]$Json) { | |
| $parsed = $Json | ConvertFrom-Json | |
| if ($parsed -is [array]) { return $parsed } | |
| if ($parsed.PSObject.Properties.Name -contains 'value') { | |
| return $parsed.value | |
| } | |
| return $parsed | |
| } | |
| function ConvertTo-CanonicalRelease([object]$Release) { | |
| $assets = @($Release.assets | Sort-Object { [string]$_.name } | | |
| ForEach-Object { | |
| [ordered]@{ | |
| name = [string]$_.name | |
| size = [long]$_.size | |
| digest = [string]$_.digest | |
| browser_download_url = [string]$_.browser_download_url | |
| } | |
| }) | |
| return ([ordered]@{ | |
| tag_name = [string]$Release.tag_name | |
| name = [string]$Release.name | |
| body = [string]$Release.body | |
| published_at = [string]$Release.published_at | |
| draft = [bool]$Release.draft | |
| prerelease = [bool]$Release.prerelease | |
| assets = $assets | |
| } | ConvertTo-Json -Depth 10 -Compress) | |
| } | |
| $localManifest = [IO.File]::ReadAllText( | |
| (Join-Path $env:GITHUB_WORKSPACE 'updates/releases.json'), | |
| [Text.Encoding]::UTF8) | |
| $expectedManifestMatches = @(Read-ManifestReleases $localManifest | | |
| Where-Object { $_.tag_name -eq $env:RELEASE_TAG }) | |
| if ($expectedManifestMatches.Count -ne 1) { | |
| throw "Local manifest must contain exactly one $env:RELEASE_TAG entry." | |
| } | |
| $expectedManifestEntry = ConvertTo-CanonicalRelease ` | |
| $expectedManifestMatches[0] | |
| function Get-ReleaseState { | |
| $json = gh release view $env:RELEASE_TAG ` | |
| --repo $env:GITHUB_REPOSITORY --json body,isDraft,assets | |
| if ($LASTEXITCODE -ne 0) { return $null } | |
| return $json | ConvertFrom-Json | |
| } | |
| function Assert-ReleaseContent([object]$Release) { | |
| if ($null -eq $Release -or $Release.isDraft) { | |
| throw "Release $env:RELEASE_TAG is not public." | |
| } | |
| $remoteAssets = @($Release.assets) | |
| if ($remoteAssets.Count -ne $localAssets.Count) { | |
| throw "Public release $env:RELEASE_TAG has an unexpected asset count." | |
| } | |
| foreach ($local in $localAssets) { | |
| $matches = @($remoteAssets | Where-Object { | |
| [string]::Equals([string]$_.name, $local.Name, | |
| [StringComparison]::Ordinal) | |
| }) | |
| if ($matches.Count -ne 1 -or | |
| [string]$matches[0].state -ne 'uploaded' -or | |
| [long]$matches[0].size -ne $local.Size -or | |
| -not [string]::Equals([string]$matches[0].digest, | |
| $local.Digest, [StringComparison]::OrdinalIgnoreCase)) { | |
| throw "Public release asset does not match the build output: $($local.Name)" | |
| } | |
| } | |
| $expectedBody = [IO.File]::ReadAllText( | |
| $env:RELEASE_NOTES, [Text.Encoding]::UTF8) -replace "`r`n", "`n" | |
| $actualBody = ([string]$Release.body) -replace "`r`n", "`n" | |
| if (-not [string]::Equals($actualBody.TrimEnd(), | |
| $expectedBody.TrimEnd(), [StringComparison]::Ordinal)) { | |
| throw "Public release notes do not match the generated notes for $env:RELEASE_TAG." | |
| } | |
| } | |
| function Restore-Draft([string]$Reason) { | |
| if ($env:MANIFEST_CHANGED -ne 'true') { | |
| throw "$Reason The manifest is already public, so the Release was not returned to draft." | |
| } | |
| gh release edit $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY ` | |
| --draft=true | |
| $redraftExit = $LASTEXITCODE | |
| $release = Get-ReleaseState | |
| if ($redraftExit -eq 0 -and $null -ne $release -and $release.isDraft) { | |
| "cleanup_branch=true" >> $env:GITHUB_OUTPUT | |
| throw "$Reason Release $env:RELEASE_TAG was returned to draft state." | |
| } | |
| throw "$Reason Failed to restore draft state; manual recovery is required." | |
| } | |
| function Get-DefaultBranchSha { | |
| $refJson = gh api ` | |
| "repos/$env:GITHUB_REPOSITORY/git/ref/heads/$env:DEFAULT_BRANCH" | |
| if ($LASTEXITCODE -ne 0) { return $null } | |
| return [string](($refJson | ConvertFrom-Json).object.sha) | |
| } | |
| function Get-ManifestPublication([string]$RemoteSha) { | |
| if ([string]::IsNullOrWhiteSpace($RemoteSha)) { return 'unknown' } | |
| if ($RemoteSha -eq $env:MANIFEST_SHA) { return 'published' } | |
| $contentJson = gh api ` | |
| "repos/$env:GITHUB_REPOSITORY/contents/updates/releases.json?ref=$RemoteSha" | |
| if ($LASTEXITCODE -ne 0) { return 'unknown' } | |
| try { | |
| $contentResponse = $contentJson | ConvertFrom-Json | |
| $bytes = [Convert]::FromBase64String([string]$contentResponse.content) | |
| $remoteManifest = [Text.Encoding]::UTF8.GetString($bytes) | |
| $matches = @(Read-ManifestReleases $remoteManifest | | |
| Where-Object { $_.tag_name -eq $env:RELEASE_TAG }) | |
| if ($matches.Count -ne 1) { return 'not-published' } | |
| $actualEntry = ConvertTo-CanonicalRelease $matches[0] | |
| if ([string]::Equals($actualEntry, $expectedManifestEntry, | |
| [StringComparison]::Ordinal)) { | |
| return 'published' | |
| } | |
| return 'not-published' | |
| } | |
| catch { | |
| return 'unknown' | |
| } | |
| } | |
| gh release edit $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY ` | |
| --draft=false | |
| $releaseEditExit = $LASTEXITCODE | |
| $release = Get-ReleaseState | |
| if ($releaseEditExit -ne 0 -and ($null -eq $release -or $release.isDraft)) { | |
| Restore-Draft "Failed to publish release $env:RELEASE_TAG." | |
| } | |
| try { | |
| Assert-ReleaseContent $release | |
| $publicUri = "$env:GITHUB_API_URL/repos/$env:GITHUB_REPOSITORY/releases/tags/" + | |
| [Uri]::EscapeDataString($env:RELEASE_TAG) | |
| $publicRelease = $null | |
| $publicError = $null | |
| for ($attempt = 0; $attempt -lt 6 -and $null -eq $publicRelease; ++$attempt) { | |
| try { | |
| $candidate = Invoke-RestMethod -Uri $publicUri -Headers @{ | |
| Accept = 'application/vnd.github+json' | |
| 'X-GitHub-Api-Version' = '2022-11-28' | |
| 'User-Agent' = 'iPhoneMirror-release-workflow' | |
| } | |
| if (-not $candidate.draft -and | |
| @($candidate.assets).Count -eq $expectedNames.Count) { | |
| $publicRelease = $candidate | |
| break | |
| } | |
| $publicError = 'The public release is incomplete.' | |
| } | |
| catch { | |
| $statusCode = if ($null -ne $_.Exception.Response) { | |
| [int]$_.Exception.Response.StatusCode | |
| } | |
| else { 0 } | |
| $publicError = "Public Release API request failed with HTTP $statusCode." | |
| } | |
| if ($attempt -lt 5) { | |
| $delay = [Math]::Min([Math]::Pow(2, $attempt), 10) | |
| Start-Sleep -Seconds $delay | |
| } | |
| } | |
| if ($null -eq $publicRelease) { | |
| throw $publicError | |
| } | |
| if ($publicRelease.draft -or | |
| @($publicRelease.assets).Count -ne $expectedNames.Count) { | |
| throw 'The unauthenticated Release API does not expose the complete release.' | |
| } | |
| foreach ($name in $expectedNames) { | |
| if (@($publicRelease.assets | Where-Object { $_.name -eq $name }).Count -ne 1) { | |
| throw "The unauthenticated Release API does not expose asset $name." | |
| } | |
| } | |
| } | |
| catch { | |
| Restore-Draft "Release publication verification failed: $($_.Exception.Message)" | |
| } | |
| if ($env:MANIFEST_CHANGED -ne 'true') { | |
| "cleanup_branch=true" >> $env:GITHUB_OUTPUT | |
| exit 0 | |
| } | |
| git push origin "HEAD:refs/heads/$env:DEFAULT_BRANCH" | |
| $pushExit = $LASTEXITCODE | |
| $publication = 'unknown' | |
| for ($attempt = 0; $attempt -lt 6; ++$attempt) { | |
| $remoteSha = Get-DefaultBranchSha | |
| $publication = Get-ManifestPublication $remoteSha | |
| if ($publication -eq 'published') { break } | |
| if ($attempt -lt 5) { | |
| $delay = [Math]::Min([Math]::Pow(2, $attempt), 10) | |
| Write-Host "Release manifest is $publication; retrying in $delay seconds." | |
| Start-Sleep -Seconds $delay | |
| } | |
| } | |
| if ($publication -eq 'not-published') { | |
| Restore-Draft "Failed to publish the release manifest to $env:DEFAULT_BRANCH." | |
| } | |
| if ($publication -eq 'unknown') { | |
| throw "Unable to determine whether the release manifest reached $env:DEFAULT_BRANCH; the public Release was left unchanged." | |
| } | |
| if ($pushExit -ne 0) { | |
| Write-Warning 'git push reported an error, but the validated manifest is present on the default branch.' | |
| } | |
| "cleanup_branch=true" >> $env:GITHUB_OUTPUT | |
| - name: Reconcile interrupted release transaction | |
| id: reconcile | |
| if: >- | |
| always() && | |
| steps.manifest.outcome == 'success' && | |
| steps.transaction.outcome != 'success' && | |
| ((steps.manifest.outputs.changed == 'true' && | |
| steps.validation.outcome == 'success') || | |
| steps.manifest.outputs.changed == 'false') | |
| continue-on-error: true | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} | |
| RELEASE_TAG: ${{ steps.version.outputs.tag }} | |
| MANIFEST_CHANGED: ${{ steps.manifest.outputs.changed }} | |
| MANIFEST_SHA: ${{ steps.manifest.outputs.sha }} | |
| run: | | |
| function Read-ManifestReleases([string]$Json) { | |
| $parsed = $Json | ConvertFrom-Json | |
| if ($parsed -is [array]) { return $parsed } | |
| if ($parsed.PSObject.Properties.Name -contains 'value') { | |
| return $parsed.value | |
| } | |
| return $parsed | |
| } | |
| function ConvertTo-CanonicalRelease([object]$Release) { | |
| $assets = @($Release.assets | Sort-Object { [string]$_.name } | | |
| ForEach-Object { | |
| [ordered]@{ | |
| name = [string]$_.name | |
| size = [long]$_.size | |
| digest = [string]$_.digest | |
| browser_download_url = [string]$_.browser_download_url | |
| } | |
| }) | |
| return ([ordered]@{ | |
| tag_name = [string]$Release.tag_name | |
| name = [string]$Release.name | |
| body = [string]$Release.body | |
| published_at = [string]$Release.published_at | |
| draft = [bool]$Release.draft | |
| prerelease = [bool]$Release.prerelease | |
| assets = $assets | |
| } | ConvertTo-Json -Depth 10 -Compress) | |
| } | |
| $localManifest = [IO.File]::ReadAllText( | |
| (Join-Path $env:GITHUB_WORKSPACE 'updates/releases.json'), | |
| [Text.Encoding]::UTF8) | |
| $expectedMatches = @(Read-ManifestReleases $localManifest | | |
| Where-Object { $_.tag_name -eq $env:RELEASE_TAG }) | |
| if ($expectedMatches.Count -ne 1) { | |
| throw "Local manifest must contain exactly one $env:RELEASE_TAG entry." | |
| } | |
| $expectedEntry = ConvertTo-CanonicalRelease $expectedMatches[0] | |
| function Get-RemoteManifestState([string]$RemoteSha) { | |
| $contentJson = gh api ` | |
| "repos/$env:GITHUB_REPOSITORY/contents/updates/releases.json?ref=$RemoteSha" | |
| if ($LASTEXITCODE -ne 0) { return 'unknown' } | |
| try { | |
| $contentResponse = $contentJson | ConvertFrom-Json | |
| $bytes = [Convert]::FromBase64String([string]$contentResponse.content) | |
| $remoteManifest = [Text.Encoding]::UTF8.GetString($bytes) | |
| $matches = @(Read-ManifestReleases $remoteManifest | | |
| Where-Object { $_.tag_name -eq $env:RELEASE_TAG }) | |
| if ($matches.Count -ne 1) { return 'invalid' } | |
| $actualEntry = ConvertTo-CanonicalRelease $matches[0] | |
| if ([string]::Equals($actualEntry, $expectedEntry, | |
| [StringComparison]::Ordinal)) { | |
| return 'valid' | |
| } | |
| return 'invalid' | |
| } | |
| catch { | |
| return 'unknown' | |
| } | |
| } | |
| $releaseJson = gh release view $env:RELEASE_TAG ` | |
| --repo $env:GITHUB_REPOSITORY --json isDraft | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Unable to inspect release $env:RELEASE_TAG during reconciliation." | |
| } | |
| $release = $releaseJson | ConvertFrom-Json | |
| $refJson = gh api ` | |
| "repos/$env:GITHUB_REPOSITORY/git/ref/heads/$env:DEFAULT_BRANCH" | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Unable to inspect $env:DEFAULT_BRANCH during reconciliation." | |
| } | |
| $remoteSha = [string](($refJson | ConvertFrom-Json).object.sha) | |
| $manifestState = if ($env:MANIFEST_CHANGED -eq 'true' -and | |
| $remoteSha -eq $env:MANIFEST_SHA) { | |
| 'valid' | |
| } | |
| else { | |
| Get-RemoteManifestState $remoteSha | |
| } | |
| if ($manifestState -eq 'unknown') { | |
| throw 'Unable to validate the current remote release manifest.' | |
| } | |
| $manifestPublished = $manifestState -eq 'valid' | |
| if ($manifestPublished -and $release.isDraft) { | |
| gh release edit $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY ` | |
| --draft=false | |
| if ($LASTEXITCODE -ne 0) { | |
| throw 'The manifest is public, but the Release could not be republished.' | |
| } | |
| $releaseJson = gh release view $env:RELEASE_TAG ` | |
| --repo $env:GITHUB_REPOSITORY --json isDraft | |
| if ($LASTEXITCODE -ne 0 -or ($releaseJson | ConvertFrom-Json).isDraft) { | |
| throw 'The manifest is public, but the Release remains a draft.' | |
| } | |
| } | |
| elseif (-not $manifestPublished -and -not $release.isDraft) { | |
| gh release edit $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY ` | |
| --draft=true | |
| if ($LASTEXITCODE -ne 0) { | |
| throw 'The manifest is not public, but the Release could not be returned to draft.' | |
| } | |
| $releaseJson = gh release view $env:RELEASE_TAG ` | |
| --repo $env:GITHUB_REPOSITORY --json isDraft | |
| if ($LASTEXITCODE -ne 0 -or -not ($releaseJson | ConvertFrom-Json).isDraft) { | |
| throw 'The manifest is not public, but the Release remains public.' | |
| } | |
| } | |
| "cleanup_branch=true" >> $env:GITHUB_OUTPUT | |
| - name: Delete manifest validation branch | |
| if: >- | |
| always() && | |
| steps.manifest.outputs.changed == 'true' && | |
| (steps.validation.outcome != 'success' || | |
| steps.transaction.outputs.cleanup_branch == 'true' || | |
| steps.reconcile.outputs.cleanup_branch == 'true') | |
| continue-on-error: true | |
| shell: pwsh | |
| env: | |
| MANIFEST_BRANCH: ${{ steps.manifest.outputs.branch }} | |
| run: | | |
| git ls-remote --exit-code --heads origin ` | |
| "refs/heads/$env:MANIFEST_BRANCH" *> $null | |
| $branchExit = $LASTEXITCODE | |
| if ($branchExit -eq 2) { | |
| Write-Host "Temporary branch $env:MANIFEST_BRANCH is already deleted." | |
| exit 0 | |
| } | |
| if ($branchExit -ne 0) { | |
| Write-Warning "Failed to inspect temporary branch $env:MANIFEST_BRANCH." | |
| exit 0 | |
| } | |
| git push origin --delete $env:MANIFEST_BRANCH | |
| if ($LASTEXITCODE -ne 0) { | |
| Write-Warning "Failed to delete temporary branch $env:MANIFEST_BRANCH." | |
| } |