ci: prefer standalone sass install in windows release workflow #11
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 Windows | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: "Tag to release (e.g. v0.1.5)" | |
| required: true | |
| type: string | |
| permissions: | |
| contents: write | |
| id-token: write | |
| attestations: write | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: false | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | |
| jobs: | |
| release: | |
| name: Build and publish signed Windows release | |
| runs-on: windows-2022 | |
| environment: production-release | |
| timeout-minutes: 120 | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| steps: | |
| - name: Checkout tag/ref | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref }} | |
| - name: Verify tag matches package version | |
| shell: pwsh | |
| run: | | |
| $pkgVersion = (Get-Content package.json -Raw | ConvertFrom-Json).version | |
| $expectedTag = "v$pkgVersion" | |
| $actualTag = if ('${{ github.event_name }}' -eq 'workflow_dispatch') { '${{ inputs.tag }}' } else { '${{ github.ref_name }}' } | |
| if ($actualTag -ne $expectedTag) { | |
| throw "Tag/package mismatch: got '$actualTag' but package.json version expects '$expectedTag'." | |
| } | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v5 | |
| with: | |
| version: 9.15.0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24 | |
| cache: pnpm | |
| - name: Setup Hugo Extended | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = 'Stop' | |
| function Test-HugoExtended { | |
| if (-not (Get-Command hugo -ErrorAction SilentlyContinue)) { | |
| return $false | |
| } | |
| $versionText = & hugo version | |
| if ($versionText -match 'extended') { | |
| Write-Host "Hugo Extended ready: $versionText" | |
| return $true | |
| } | |
| return $false | |
| } | |
| function Try-ChocoInstall { | |
| if (-not (Get-Command choco -ErrorAction SilentlyContinue)) { | |
| return $false | |
| } | |
| Write-Host 'Attempting Hugo Extended install via Chocolatey...' | |
| choco install hugo-extended -y --no-progress | |
| return $LASTEXITCODE -eq 0 | |
| } | |
| function Try-WingetInstall { | |
| if (-not (Get-Command winget -ErrorAction SilentlyContinue)) { | |
| return $false | |
| } | |
| Write-Host 'Attempting Hugo Extended install via Winget...' | |
| winget install --id Hugo.Hugo.Extended --silent --accept-package-agreements --accept-source-agreements --disable-interactivity | |
| return $LASTEXITCODE -eq 0 | |
| } | |
| function Try-DirectZipInstall { | |
| Write-Host 'Attempting Hugo Extended direct download install...' | |
| $apiUrl = 'https://api.github.com/repos/gohugoio/hugo/releases/latest' | |
| $release = Invoke-RestMethod -Uri $apiUrl -Headers @{ 'User-Agent' = 'BlogForEveryone-CI' } | |
| $asset = $release.assets | Where-Object { $_.name -match '^hugo_extended_.*_windows-amd64\.zip$' } | Select-Object -First 1 | |
| if (-not $asset) { | |
| return $false | |
| } | |
| $zipPath = Join-Path $env:RUNNER_TEMP 'hugo-extended.zip' | |
| $extractDir = Join-Path $env:RUNNER_TEMP 'hugo-extended-bin' | |
| Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $zipPath | |
| if (Test-Path $extractDir) { | |
| Remove-Item -Recurse -Force $extractDir | |
| } | |
| Expand-Archive -Path $zipPath -DestinationPath $extractDir -Force | |
| $envPath = $env:GITHUB_PATH | |
| if (-not $envPath) { | |
| return $false | |
| } | |
| Add-Content -Path $envPath -Value $extractDir | |
| $env:PATH = "$extractDir;$env:PATH" | |
| return $true | |
| } | |
| if (Test-HugoExtended) { | |
| exit 0 | |
| } | |
| $installed = $false | |
| try { if (Try-ChocoInstall) { $installed = Test-HugoExtended } } catch { Write-Warning "Chocolatey install failed: $($_.Exception.Message)" } | |
| if (-not $installed) { | |
| try { if (Try-WingetInstall) { $installed = Test-HugoExtended } } catch { Write-Warning "Winget install failed: $($_.Exception.Message)" } | |
| } | |
| if (-not $installed) { | |
| try { if (Try-DirectZipInstall) { $installed = Test-HugoExtended } } catch { Write-Warning "Direct ZIP install failed: $($_.Exception.Message)" } | |
| } | |
| if (-not $installed) { | |
| throw 'Failed to install Hugo Extended via Chocolatey, Winget, or direct ZIP download.' | |
| } | |
| $hugoCommand = Get-Command hugo -ErrorAction Stop | |
| "HUGO_EXECUTABLE=$($hugoCommand.Source)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append | |
| Write-Host "Pinned HUGO_EXECUTABLE to $($hugoCommand.Source)" | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Setup Dart Sass | |
| shell: pwsh | |
| run: | | |
| $installed = $false | |
| try { | |
| choco install sass --yes --no-progress | |
| $installed = $true | |
| } catch { | |
| Write-Warning "Chocolatey Sass install failed: $($_.Exception.Message)" | |
| } | |
| if (-not $installed) { | |
| Write-Host "Falling back to pnpm global sass install" | |
| pnpm add -g sass | |
| } | |
| $sassCommand = Get-Command sass -ErrorAction Stop | |
| $sassExecutable = $sassCommand.Source | |
| if ($sassExecutable -match '\.ps1$') { | |
| $cmdCandidate = [IO.Path]::ChangeExtension($sassExecutable, '.cmd') | |
| if (Test-Path $cmdCandidate) { | |
| $sassExecutable = $cmdCandidate | |
| } else { | |
| $batCandidate = [IO.Path]::ChangeExtension($sassExecutable, '.bat') | |
| if (Test-Path $batCandidate) { | |
| $sassExecutable = $batCandidate | |
| } | |
| } | |
| } | |
| if (-not (Test-Path $sassExecutable)) { | |
| throw "Resolved Sass executable does not exist: $sassExecutable" | |
| } | |
| & $sassExecutable --version | |
| "SASS_EXECUTABLE=$sassExecutable" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append | |
| Write-Host "Pinned SASS_EXECUTABLE to $sassExecutable" | |
| - name: Restore signing certificate into temp file | |
| shell: pwsh | |
| run: | | |
| $pfxPath = Join-Path $env:RUNNER_TEMP "codesign.pfx" | |
| [IO.File]::WriteAllBytes($pfxPath, [Convert]::FromBase64String('${{ secrets.BFE_CODESIGN_PFX_B64 }}')) | |
| "CSC_LINK=$pfxPath" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append | |
| "CSC_KEY_PASSWORD=${{ secrets.BFE_CODESIGN_PFX_PASSWORD }}" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append | |
| - name: Verify signing env | |
| run: pnpm run verify:windows-signing-env | |
| - name: Run full release pipeline | |
| run: pnpm run release | |
| - name: Verify installer signature | |
| shell: pwsh | |
| run: | | |
| $exe = Get-ChildItem dist -Filter "*Setup-*.exe" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 | |
| if (-not $exe) { throw "No installer found in dist/." } | |
| $signTool = Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\bin" -Recurse -Filter signtool.exe | | |
| Sort-Object FullName -Descending | Select-Object -First 1 | |
| if (-not $signTool) { throw "signtool.exe not found" } | |
| & $signTool.FullName verify /pa /v $exe.FullName | |
| - name: Upload release artifacts for audit | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: windows-release-assets-${{ github.ref_name }} | |
| path: | | |
| dist/*.exe | |
| dist/*.blockmap | |
| dist/latest.yml | |
| - name: Attest build provenance | |
| uses: actions/attest-build-provenance@v2 | |
| with: | |
| subject-path: | | |
| dist/*.exe | |
| dist/*.blockmap | |
| dist/latest.yml | |
| - name: Cleanup certificate file | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| Remove-Item (Join-Path $env:RUNNER_TEMP "codesign.pfx") -ErrorAction SilentlyContinue |