Bump fast-uri from 3.1.5 to 3.1.7 in /landing #35
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: Build, test, release, and publish | |
| on: | |
| push: | |
| branches: | |
| - "**" | |
| workflow_dispatch: | |
| concurrency: | |
| group: material-office-release | |
| cancel-in-progress: false | |
| permissions: | |
| contents: read | |
| env: | |
| RELEASE_BUNDLE_NAME: material-office-release-${{ github.run_id }}-${{ github.run_attempt }} | |
| LIBREOFFICE_VERSION: "26.2.5" | |
| LIBREOFFICE_MSI_NAME: LibreOffice_26.2.5_Win_x86-64.msi | |
| LIBREOFFICE_MSI_URL: https://download.documentfoundation.org/libreoffice/stable/26.2.5/win/x86_64/LibreOffice_26.2.5_Win_x86-64.msi | |
| LIBREOFFICE_MSI_SHA256: f15ba07bfcb0186986cf3171063506f5d207c11f8cc051ba0d135209e9e915f9 | |
| jobs: | |
| workflow-lint: | |
| name: Workflow schema and policy lint | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Check out source | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| submodules: false | |
| - name: Run pinned official actionlint | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| archive="$RUNNER_TEMP/actionlint.tar.gz" | |
| curl --fail --location --silent --show-error \ | |
| --output "$archive" \ | |
| https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_amd64.tar.gz | |
| echo "8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 $archive" | sha256sum --check --strict | |
| tar -xzf "$archive" -C "$RUNNER_TEMP" actionlint | |
| "$RUNNER_TEMP/actionlint" -color | |
| windows-build: | |
| name: Windows build and installer verification | |
| needs: workflow-lint | |
| if: needs.workflow-lint.result == 'success' | |
| runs-on: windows-latest | |
| timeout-minutes: 75 | |
| permissions: | |
| attestations: write | |
| contents: read | |
| id-token: write | |
| outputs: | |
| version: ${{ steps.bundle.outputs.version }} | |
| installer_name: ${{ steps.bundle.outputs.installer_name }} | |
| steps: | |
| - name: Check out source | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| submodules: false | |
| - name: Measure hosted runner | |
| shell: pwsh | |
| run: | | |
| Get-CimInstance Win32_Processor | Select-Object Name, NumberOfCores, NumberOfLogicalProcessors | |
| Get-CimInstance Win32_ComputerSystem | Select-Object TotalPhysicalMemory | |
| Get-PSDrive C | Select-Object Used, Free | |
| - name: Set up Node.js | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: "26.4.0" | |
| cache: npm | |
| - name: Install locked dependencies | |
| run: npm ci --no-audit --no-fund | |
| - name: Audit application dependencies | |
| run: npm audit --audit-level=high | |
| - name: Test application | |
| run: npm test | |
| - name: Download and install pinned official LibreOffice | |
| shell: pwsh | |
| run: | | |
| Set-StrictMode -Version Latest | |
| $ErrorActionPreference = "Stop" | |
| $msi = Join-Path $env:RUNNER_TEMP $env:LIBREOFFICE_MSI_NAME | |
| if (Test-Path -LiteralPath $msi) { Remove-Item -LiteralPath $msi -Force } | |
| & curl.exe --fail --location --retry 5 --retry-all-errors --retry-delay 5 --connect-timeout 30 --max-time 300 --output $msi $env:LIBREOFFICE_MSI_URL | |
| if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $msi) -or (Get-Item -LiteralPath $msi).Length -le 0) { | |
| throw "LibreOffice MSI download failed with curl exit code $LASTEXITCODE." | |
| } | |
| $actual = (Get-FileHash -LiteralPath $msi -Algorithm SHA256).Hash.ToLowerInvariant() | |
| if ($actual -ne $env:LIBREOFFICE_MSI_SHA256) { | |
| throw "LibreOffice MSI hash mismatch: expected $env:LIBREOFFICE_MSI_SHA256, received $actual." | |
| } | |
| $signature = Get-AuthenticodeSignature -LiteralPath $msi | |
| if ($signature.Status -ne 'Valid' -or $signature.SignerCertificate.Subject -notmatch 'The Document Foundation') { | |
| throw "LibreOffice MSI Authenticode verification failed: $($signature.Status), $($signature.SignerCertificate.Subject)." | |
| } | |
| $install = Start-Process -FilePath "$env:SystemRoot\System32\msiexec.exe" -ArgumentList @('/i', $msi, '/qn', '/norestart', 'ALLUSERS=1') -Wait -PassThru -WindowStyle Hidden | |
| if ($install.ExitCode -notin @(0, 3010)) { | |
| throw "LibreOffice MSI exited with $($install.ExitCode)." | |
| } | |
| $soffice = Join-Path $env:ProgramFiles 'LibreOffice\program\soffice.com' | |
| if (-not (Test-Path -LiteralPath $soffice -PathType Leaf)) { | |
| throw "LibreOffice did not install its console executable at $soffice." | |
| } | |
| "MATERIAL_OFFICE_SOFFICE=$soffice" >> $env:GITHUB_ENV | |
| - name: Verify genuine LibreOffice availability and conversions | |
| shell: pwsh | |
| run: npm run verify:libreoffice -- "$env:MATERIAL_OFFICE_SOFFICE" | |
| - name: Prepare pinned corresponding-source assets | |
| run: npm run prepare:git-sources | |
| - name: Build Windows installer | |
| run: npm run dist:win | |
| - name: Verify packaged files, minimal Git history, and Electron fuses | |
| run: | | |
| npm run verify:package | |
| npm run verify:fuses | |
| - name: Smoke-test packaged and installed applications | |
| shell: pwsh | |
| run: | | |
| Set-StrictMode -Version Latest | |
| $ErrorActionPreference = "Stop" | |
| $packaged = Join-Path $PWD "dist/win-unpacked/Material Office.exe" | |
| if (-not (Test-Path -LiteralPath $packaged -PathType Leaf)) { | |
| throw "The unpacked application executable is missing." | |
| } | |
| $packagedOut = Join-Path $env:RUNNER_TEMP "packaged-smoke.stdout.log" | |
| $packagedErr = Join-Path $env:RUNNER_TEMP "packaged-smoke.stderr.log" | |
| $packagedSmoke = Start-Process -FilePath $packaged -ArgumentList '--smoke-test' -RedirectStandardOutput $packagedOut -RedirectStandardError $packagedErr -Wait -PassThru -WindowStyle Hidden | |
| Get-Content -LiteralPath $packagedOut -ErrorAction SilentlyContinue | |
| Get-Content -LiteralPath $packagedErr -ErrorAction SilentlyContinue | |
| if ($packagedSmoke.ExitCode -ne 0) { | |
| throw "The packaged application smoke test exited with $($packagedSmoke.ExitCode)." | |
| } | |
| $installers = @(Get-ChildItem -LiteralPath dist -Filter "*-Setup.exe" -File) | |
| if ($installers.Count -ne 1) { | |
| throw "Expected exactly one Windows installer before install smoke, found $($installers.Count)." | |
| } | |
| $installRoot = Join-Path $env:RUNNER_TEMP "material-office-installed-$env:GITHUB_RUN_ATTEMPT" | |
| if (Test-Path -LiteralPath $installRoot) { | |
| throw "The isolated installer test directory already exists." | |
| } | |
| $installer = Start-Process -FilePath $installers[0].FullName -ArgumentList @('/S', "/D=$installRoot") -Wait -PassThru -WindowStyle Hidden | |
| if ($installer.ExitCode -ne 0) { | |
| throw "Silent installer exited with $($installer.ExitCode)." | |
| } | |
| $installed = Join-Path $installRoot "Material Office.exe" | |
| if (-not (Test-Path -LiteralPath $installed -PathType Leaf)) { | |
| throw "The installed application executable is missing." | |
| } | |
| $installedOut = Join-Path $env:RUNNER_TEMP "installed-smoke.stdout.log" | |
| $installedErr = Join-Path $env:RUNNER_TEMP "installed-smoke.stderr.log" | |
| $installedSmoke = Start-Process -FilePath $installed -ArgumentList '--smoke-test' -RedirectStandardOutput $installedOut -RedirectStandardError $installedErr -Wait -PassThru -WindowStyle Hidden | |
| Get-Content -LiteralPath $installedOut -ErrorAction SilentlyContinue | |
| Get-Content -LiteralPath $installedErr -ErrorAction SilentlyContinue | |
| if ($installedSmoke.ExitCode -ne 0) { | |
| throw "The installed application smoke test exited with $($installedSmoke.ExitCode)." | |
| } | |
| $shortcutRoot = Join-Path $env:APPDATA 'Microsoft\Windows\Start Menu\Programs' | |
| $shortcuts = @(Get-ChildItem -LiteralPath $shortcutRoot -Filter 'Material Office.lnk' -Recurse -File) | |
| if ($shortcuts.Count -ne 1) { | |
| throw "Expected exactly one installed Start-menu shortcut, found $($shortcuts.Count)." | |
| } | |
| & scripts/verify-windows-icon.ps1 ` | |
| -PackagedExecutable $packaged ` | |
| -InstalledExecutable $installed ` | |
| -ShortcutPath $shortcuts[0].FullName ` | |
| -ElectronExecutable (Join-Path $PWD 'node_modules/electron/dist/electron.exe') | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Installed icon verification exited with $LASTEXITCODE." | |
| } | |
| $uninstaller = Join-Path $installRoot "Uninstall Material Office.exe" | |
| if (-not (Test-Path -LiteralPath $uninstaller -PathType Leaf)) { | |
| throw "The silent installer did not create its uninstaller." | |
| } | |
| $removed = Start-Process -FilePath $uninstaller -ArgumentList '/S' -Wait -PassThru -WindowStyle Hidden | |
| if ($removed.ExitCode -ne 0) { | |
| throw "Silent uninstaller exited with $($removed.ExitCode)." | |
| } | |
| $deadline = [DateTimeOffset]::UtcNow.AddSeconds(20) | |
| while ((Test-Path -LiteralPath $installed -PathType Leaf) -and [DateTimeOffset]::UtcNow -lt $deadline) { | |
| Start-Sleep -Milliseconds 250 | |
| } | |
| if (Test-Path -LiteralPath $installed -PathType Leaf) { | |
| throw "The silent uninstaller left the installed executable behind." | |
| } | |
| - name: Assemble verified release bundle | |
| id: bundle | |
| shell: pwsh | |
| run: | | |
| Set-StrictMode -Version Latest | |
| $ErrorActionPreference = "Stop" | |
| $manifest = Get-Content -LiteralPath package.json -Raw | ConvertFrom-Json | |
| $version = [string]$manifest.version | |
| if ($version -notmatch '^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$') { | |
| throw "package.json contains an invalid release version." | |
| } | |
| $installers = @(Get-ChildItem -LiteralPath dist -Filter "*-Setup.exe" -File) | |
| if ($installers.Count -ne 1 -or $installers[0].Length -lt 1000000) { | |
| throw "Expected exactly one nontrivial Windows installer." | |
| } | |
| $image = (Resolve-Path "src/renderer/assets/dim-sum/hk-dish-0001-classic-har-gow.png").Path | |
| $provenancePath = (Resolve-Path "docs/legal/classic-har-gow-provenance.json").Path | |
| $provenance = Get-Content -LiteralPath $provenancePath -Raw | ConvertFrom-Json | |
| if ($provenance.provenanceStatus -ne 'catalog-record-and-byte-identity-verified' -or -not $provenance.verification.catalogRecordAvailable -or -not $provenance.verification.catalogImageAvailable -or -not $provenance.verification.copiesMatchCatalogBytes) { | |
| throw "The dim-sum provenance record does not verify source record and byte identity." | |
| } | |
| $imageSha256 = (Get-FileHash -LiteralPath $image -Algorithm SHA256).Hash.ToLowerInvariant() | |
| if ($imageSha256 -ne [string]$provenance.asset.sha256 -or (Get-Item -LiteralPath $image).Length -ne [long]$provenance.asset.bytes) { | |
| throw "The bundled dim-sum image does not match its verified provenance." | |
| } | |
| Add-Type -AssemblyName PresentationCore | |
| $imageStream = [IO.File]::OpenRead($image) | |
| try { | |
| $decoder = [System.Windows.Media.Imaging.PngBitmapDecoder]::new($imageStream, [System.Windows.Media.Imaging.BitmapCreateOptions]::PreservePixelFormat, [System.Windows.Media.Imaging.BitmapCacheOption]::OnLoad) | |
| if ($decoder.Frames.Count -ne 1 -or $decoder.Frames[0].PixelWidth -ne [int]$provenance.asset.width -or $decoder.Frames[0].PixelHeight -ne [int]$provenance.asset.height) { | |
| throw "The release PNG did not decode to its recorded single-frame dimensions." | |
| } | |
| $stride = [int][Math]::Ceiling($decoder.Frames[0].PixelWidth * $decoder.Frames[0].Format.BitsPerPixel / 8.0) | |
| $decodedPixels = [byte[]]::new($stride * $decoder.Frames[0].PixelHeight) | |
| $decoder.Frames[0].CopyPixels($decodedPixels, $stride, 0) | |
| if ($decodedPixels.Length -le 0) { throw "The release PNG decoded no pixels." } | |
| } finally { | |
| $imageStream.Dispose() | |
| } | |
| $signatureStatus = (Get-AuthenticodeSignature -LiteralPath $installers[0].FullName).Status.ToString() | |
| if ($signatureStatus -notin @('Valid', 'NotSigned')) { | |
| throw "The installer has an invalid Authenticode state: $signatureStatus." | |
| } | |
| if ($signatureStatus -eq 'NotSigned' -and ($manifest.build.nsis.allowElevation -ne $false -or $manifest.build.nsis.perMachine -ne $false -or $manifest.build.win.requestedExecutionLevel -ne 'asInvoker')) { | |
| throw "An Authenticode-unsigned build must be per-user, non-elevating, and run as the invoking user." | |
| } | |
| $bundleDirectory = Join-Path $env:RUNNER_TEMP "release-bundle" | |
| New-Item -ItemType Directory -Path $bundleDirectory | Out-Null | |
| $checksumName = "$($installers[0].Name).sha256" | |
| $installerSha256 = (Get-FileHash -LiteralPath $installers[0].FullName -Algorithm SHA256).Hash.ToLowerInvariant() | |
| $checksumSource = Join-Path $env:RUNNER_TEMP $checksumName | |
| "$installerSha256 $($installers[0].Name)" | Set-Content -LiteralPath $checksumSource -Encoding utf8NoBOM | |
| $assetInputs = @( | |
| [pscustomobject]@{ Source = $installers[0].FullName; Name = $installers[0].Name; Role = 'windows-installer' }, | |
| [pscustomobject]@{ Source = $checksumSource; Name = $checksumName; Role = 'installer-checksum' }, | |
| [pscustomobject]@{ Source = $image; Name = (Split-Path -Leaf $image); Role = 'dim-sum-image' }, | |
| [pscustomobject]@{ Source = $provenancePath; Name = 'classic-har-gow-provenance.json'; Role = 'image-provenance' }, | |
| [pscustomobject]@{ Source = (Resolve-Path 'LICENSE').Path; Name = 'LICENSE.txt'; Role = 'project-license' }, | |
| [pscustomobject]@{ Source = (Resolve-Path 'THIRD_PARTY_NOTICES.md').Path; Name = 'THIRD_PARTY_NOTICES.md'; Role = 'third-party-notices' }, | |
| [pscustomobject]@{ Source = (Resolve-Path 'build-tools/git-runtime/legal/git-runtime-component-manifest.json').Path; Name = 'git-runtime-component-manifest.json'; Role = 'git-runtime-component-manifest' } | |
| ) | |
| $sourceRoles = @{ | |
| 'git-runtime-source-manifest.json' = 'git-runtime-source-manifest' | |
| 'mingw-w64-git-2.55.0.3-1.src.tar.gz' = 'git-corresponding-source' | |
| 'libiconv-1.19.tar.gz' = 'libiconv-corresponding-source' | |
| 'gettext-1.0.tar.lz' = 'gettext-corresponding-source' | |
| 'git-for-windows-MINGW-packages-998707b909fd8fc204ba34f1c2dfb2885bc381a7-runtime-recipes.tar' = 'runtime-build-recipes' | |
| } | |
| $sourceFiles = @(Get-ChildItem -LiteralPath 'build-tools/git-runtime-sources' -File) | |
| if ($sourceFiles.Count -ne $sourceRoles.Count) { | |
| throw "The corresponding-source directory does not contain exactly the gated assets." | |
| } | |
| foreach ($sourceFile in $sourceFiles) { | |
| if (-not $sourceRoles.ContainsKey($sourceFile.Name)) { | |
| throw "Unexpected corresponding-source asset: $($sourceFile.Name)." | |
| } | |
| $assetInputs += [pscustomobject]@{ Source = $sourceFile.FullName; Name = $sourceFile.Name; Role = $sourceRoles[$sourceFile.Name] } | |
| } | |
| if (($assetInputs.Name | Sort-Object -Unique).Count -ne $assetInputs.Count) { | |
| throw "Release asset names are not unique." | |
| } | |
| $assets = @() | |
| foreach ($input in $assetInputs) { | |
| $destination = Join-Path $bundleDirectory $input.Name | |
| Copy-Item -LiteralPath $input.Source -Destination $destination | |
| $item = Get-Item -LiteralPath $destination | |
| $assets += [ordered]@{ | |
| name = $input.Name | |
| role = $input.Role | |
| bytes = $item.Length | |
| sha256 = (Get-FileHash -LiteralPath $destination -Algorithm SHA256).Hash.ToLowerInvariant() | |
| } | |
| } | |
| $metadata = [ordered]@{ | |
| schemaVersion = 2 | |
| version = $version | |
| commit = $env:GITHUB_SHA | |
| codeName = 'Classic Har Gow · 蝦餃' | |
| signatureStatus = $signatureStatus | |
| libreOffice = [ordered]@{ | |
| version = $env:LIBREOFFICE_VERSION | |
| msiName = $env:LIBREOFFICE_MSI_NAME | |
| msiSha256 = $env:LIBREOFFICE_MSI_SHA256 | |
| integrationGate = 'html-fodt-pdf-and-pyuno-import-verified' | |
| } | |
| correspondingSourceGate = 'complete-corresponding-source' | |
| assets = @($assets | Sort-Object name) | |
| } | |
| $metadata | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $bundleDirectory "release-metadata.json") -Encoding utf8NoBOM | |
| "version=$version" >> $env:GITHUB_OUTPUT | |
| "installer_name=$($installers[0].Name)" >> $env:GITHUB_OUTPUT | |
| - name: Attest exact installer build provenance | |
| uses: actions/attest-build-provenance@43d14bc2b83dec42d39ecae14e916627a18bb661 # v3.1.0 | |
| with: | |
| subject-path: dist/*-Setup.exe | |
| - name: Upload verified Windows release bundle | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: ${{ env.RELEASE_BUNDLE_NAME }} | |
| path: ${{ runner.temp }}/release-bundle | |
| if-no-files-found: error | |
| retention-days: 3 | |
| compression-level: 0 | |
| overwrite: false | |
| include-hidden-files: false | |
| landing-test: | |
| name: Documentation site audit and test | |
| needs: workflow-lint | |
| if: needs.workflow-lint.result == 'success' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Check out source | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| submodules: false | |
| - name: Measure hosted runner | |
| shell: bash | |
| run: | | |
| nproc | |
| free -h | |
| df -h / | |
| - name: Set up Node.js | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: "26.4.0" | |
| cache: npm | |
| cache-dependency-path: landing/package-lock.json | |
| - name: Install locked dependencies | |
| working-directory: landing | |
| run: npm ci --no-audit --no-fund | |
| - name: Audit documentation dependencies | |
| working-directory: landing | |
| run: npm audit --audit-level=high | |
| - name: Test Sites and static Pages builds | |
| working-directory: landing | |
| run: npm test | |
| pages: | |
| name: Static documentation deployment | |
| needs: | |
| - windows-build | |
| - landing-test | |
| - release | |
| if: github.ref == 'refs/heads/main' && needs.windows-build.result == 'success' && needs.landing-test.result == 'success' && needs.release.result == 'success' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| permissions: | |
| contents: read | |
| pages: write | |
| id-token: write | |
| environment: | |
| name: github-pages | |
| url: ${{ steps.deployment.outputs.page_url }} | |
| outputs: | |
| page_url: ${{ steps.deployment.outputs.page_url }} | |
| steps: | |
| - name: Check out source | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| submodules: false | |
| - name: Set up Node.js | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: "26.4.0" | |
| cache: npm | |
| cache-dependency-path: landing/package-lock.json | |
| - name: Install locked dependencies | |
| working-directory: landing | |
| run: npm ci --no-audit --no-fund | |
| - name: Configure Pages | |
| id: pages-config | |
| uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 | |
| - name: Build and verify base-aware static Pages output | |
| working-directory: landing | |
| env: | |
| GITHUB_PAGES_BASE_PATH: ${{ steps.pages-config.outputs.base_path }} | |
| MATERIAL_OFFICE_RELEASE_STATE: published | |
| MATERIAL_OFFICE_VERSION: ${{ needs.windows-build.outputs.version }} | |
| MATERIAL_OFFICE_RELEASE_TAG: v${{ needs.windows-build.outputs.version }}-build.${{ github.run_number }}.attempt.${{ github.run_attempt }} | |
| MATERIAL_OFFICE_REPOSITORY: ${{ github.repository }} | |
| MATERIAL_OFFICE_INSTALLER_NAME: ${{ needs.windows-build.outputs.installer_name }} | |
| run: | | |
| npm run build:pages | |
| npm run test:pages | |
| - name: Upload static Pages output | |
| uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 | |
| with: | |
| path: landing/dist-pages | |
| - name: Deploy Pages | |
| id: deployment | |
| uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 | |
| release: | |
| name: Publish and verify release | |
| needs: | |
| - windows-build | |
| - landing-test | |
| if: needs.windows-build.result == 'success' && needs.landing-test.result == 'success' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| permissions: | |
| actions: read | |
| attestations: read | |
| contents: write | |
| steps: | |
| - name: Download verified Windows release bundle | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: ${{ env.RELEASE_BUNDLE_NAME }} | |
| path: ${{ runner.temp }}/release-bundle | |
| digest-mismatch: error | |
| - name: Publish exactly one verified non-draft release | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.ORG_TOKEN || secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ needs.windows-build.outputs.version }} | |
| PAGES_URL: ${{ github.ref == 'refs/heads/main' && 'https://ding-ding-projects.github.io/material-office/' || '' }} | |
| run: | | |
| Set-StrictMode -Version Latest | |
| $ErrorActionPreference = "Stop" | |
| $bundleDirectory = Join-Path $env:RUNNER_TEMP "release-bundle" | |
| $metadataPath = Join-Path $bundleDirectory "release-metadata.json" | |
| $metadata = Get-Content -LiteralPath $metadataPath -Raw | ConvertFrom-Json | |
| if ([int]$metadata.schemaVersion -ne 2 -or $metadata.version -ne $env:VERSION -or $metadata.commit -ne $env:GITHUB_SHA) { | |
| throw "Release metadata does not identify this schema, version, and commit." | |
| } | |
| if ($metadata.correspondingSourceGate -ne 'complete-corresponding-source' -or $metadata.libreOffice.integrationGate -ne 'html-fodt-pdf-and-pyuno-import-verified') { | |
| throw "A legal-source or genuine LibreOffice gate is missing." | |
| } | |
| $expectedFiles = @($metadata.assets | ForEach-Object name) + 'release-metadata.json' | Sort-Object | |
| $actualFiles = @(Get-ChildItem -LiteralPath $bundleDirectory -File | ForEach-Object Name | Sort-Object) | |
| if (Compare-Object -ReferenceObject $expectedFiles -DifferenceObject $actualFiles) { | |
| throw "The downloaded release bundle does not contain exactly the metadata-declared files." | |
| } | |
| $requiredRoles = @( | |
| 'windows-installer', 'installer-checksum', 'dim-sum-image', 'image-provenance', | |
| 'project-license', 'third-party-notices', 'git-runtime-component-manifest', | |
| 'git-runtime-source-manifest', 'git-corresponding-source', | |
| 'libiconv-corresponding-source', 'gettext-corresponding-source', 'runtime-build-recipes' | |
| ) | Sort-Object | |
| $actualRoles = @($metadata.assets | ForEach-Object role | Sort-Object) | |
| if (Compare-Object -ReferenceObject $requiredRoles -DifferenceObject $actualRoles) { | |
| throw "The release bundle is missing a required installer, provenance, legal, or source role." | |
| } | |
| foreach ($asset in $metadata.assets) { | |
| $assetPath = Join-Path $bundleDirectory $asset.name | |
| $item = Get-Item -LiteralPath $assetPath | |
| $hash = (Get-FileHash -LiteralPath $assetPath -Algorithm SHA256).Hash.ToLowerInvariant() | |
| if ($item.Length -ne [long]$asset.bytes -or $hash -ne $asset.sha256) { | |
| throw "Release asset $($asset.name) does not match its gated metadata." | |
| } | |
| } | |
| $installerAsset = @($metadata.assets | Where-Object role -eq 'windows-installer') | |
| $checksumAsset = @($metadata.assets | Where-Object role -eq 'installer-checksum') | |
| if ($installerAsset.Count -ne 1 -or $checksumAsset.Count -ne 1) { | |
| throw "Installer and checksum roles must each be unique." | |
| } | |
| $installerPath = Join-Path $bundleDirectory $installerAsset[0].name | |
| $checksumPath = Join-Path $bundleDirectory $checksumAsset[0].name | |
| $checksumLine = (Get-Content -LiteralPath $checksumPath -Raw).Trim() | |
| if ($checksumLine -ne "$($installerAsset[0].sha256) $($installerAsset[0].name)") { | |
| throw "The installer checksum text does not match the gated installer." | |
| } | |
| gh attestation verify $installerPath --repo $env:GITHUB_REPOSITORY | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "The downloaded installer failed repository/workflow build-provenance verification." | |
| } | |
| $tag = "v$($env:VERSION)-build.$env:GITHUB_RUN_NUMBER.attempt.$env:GITHUB_RUN_ATTEMPT" | |
| gh release view $tag --repo $env:GITHUB_REPOSITORY --json tagName 2>$null | Out-Null | |
| if ($LASTEXITCODE -eq 0) { | |
| throw "A release already exists for $tag; refusing to publish a duplicate." | |
| } | |
| $codeName = [string]$metadata.codeName | |
| $releaseRowsJson = gh api --paginate --slurp "repos/$env:GITHUB_REPOSITORY/releases?per_page=100" | |
| if ($LASTEXITCODE -ne 0) { throw "Could not inspect existing release code-name use." } | |
| $releaseRows = @( | |
| foreach ($page in @($releaseRowsJson | ConvertFrom-Json)) { | |
| foreach ($row in @($page)) { $row } | |
| } | |
| ) | |
| if (@($releaseRows | Where-Object { $_.name -like "*$codeName*" -or $_.body -like "*$codeName*" }).Count -gt 0) { | |
| $title = "Material Office $($env:VERSION) build $env:GITHUB_RUN_NUMBER attempt $env:GITHUB_RUN_ATTEMPT" | |
| $codeNameLine = "**Code name:** none — the only verified bundled code name has already been used, so this build ships by version and tag." | |
| } else { | |
| $title = "Material Office $($env:VERSION) build $env:GITHUB_RUN_NUMBER attempt $env:GITHUB_RUN_ATTEMPT · $codeName" | |
| $codeNameLine = "**Code name:** $codeName" | |
| } | |
| $imageAsset = @($metadata.assets | Where-Object role -eq 'dim-sum-image') | |
| if ($imageAsset.Count -ne 1) { | |
| throw "The release must contain exactly one verified dim-sum image." | |
| } | |
| $imageAssetUrl = "https://github.com/$env:GITHUB_REPOSITORY/releases/download/$tag/$([Uri]::EscapeDataString([string]$imageAsset[0].name))" | |
| $imageLine = "" | |
| $assetLines = @($metadata.assets | Sort-Object role | ForEach-Object { "- ``$($_.name)`` — $($_.role), $($_.bytes) bytes, SHA-256 ``$($_.sha256)``" }) | |
| $documentationLine = if ($env:PAGES_URL) { | |
| "Documentation: $env:PAGES_URL" | |
| } else { | |
| "GitHub Pages deployment is default-branch-only; this branch build did not deploy the site." | |
| } | |
| $notesPath = Join-Path $env:RUNNER_TEMP "release-notes.md" | |
| @" | |
| # Material Office $($env:VERSION) | |
| $codeNameLine | |
| $imageLine | |
| This Windows x64 build was produced from commit ``$env:GITHUB_SHA`` by run ``$env:GITHUB_RUN_ID`` attempt ``$env:GITHUB_RUN_ATTEMPT``. Tests include an installed-app smoke, the isolated five-file Git history runtime, and genuine LibreOffice $($metadata.libreOffice.version) HTML/FODT-to-PDF conversions plus a bundled-Python PyUNO import. $documentationLine | |
| ## Verified assets | |
| $($assetLines -join "`n") | |
| Authenticode status: **$($metadata.signatureStatus)**. When it is ``NotSigned``, the per-user installer cannot request elevation and Windows may show its normal reputation warning until a code-signing identity is configured. Verify the exact installer against this repository's workflow identity with ``gh attestation verify $($installerAsset[0].name) --repo $env:GITHUB_REPOSITORY`` and compare the attached SHA-256 file. The attached source archives, exact Git-for-Windows recipes/patches, component manifest, notices, and provenance record accompany the binaries; the dim-sum image retains the rights boundary stated in its provenance record. | |
| "@ | Set-Content -LiteralPath $notesPath -Encoding utf8NoBOM | |
| $assetPaths = @($metadata.assets | ForEach-Object { Join-Path $bundleDirectory $_.name }) | |
| $ownedDraft = $false | |
| try { | |
| gh release create $tag @assetPaths --repo $env:GITHUB_REPOSITORY --target $env:GITHUB_SHA --title $title --notes-file $notesPath --draft | |
| if ($LASTEXITCODE -ne 0) { throw "Draft release creation or asset upload failed with exit code $LASTEXITCODE." } | |
| $ownedDraft = $true | |
| $draft = gh release view $tag --repo $env:GITHUB_REPOSITORY --json 'tagName,isDraft,isPrerelease,targetCommitish,url,assets' | ConvertFrom-Json | |
| if (-not $draft.isDraft -or $draft.isPrerelease -or $draft.targetCommitish -ne $env:GITHUB_SHA) { | |
| throw "The uploaded draft metadata does not match this gated run." | |
| } | |
| $expectedAssetNames = @($metadata.assets | ForEach-Object name | Sort-Object) | |
| $draftAssetNames = @($draft.assets | ForEach-Object name | Sort-Object) | |
| if (Compare-Object -ReferenceObject $expectedAssetNames -DifferenceObject $draftAssetNames) { | |
| throw "The uploaded draft does not contain exactly the gated assets." | |
| } | |
| foreach ($asset in $metadata.assets) { | |
| $record = @($draft.assets | Where-Object name -eq $asset.name) | |
| if ($record.Count -ne 1 -or $record[0].state -ne 'uploaded' -or [long]$record[0].size -ne [long]$asset.bytes -or $record[0].digest -ne "sha256:$($asset.sha256)") { | |
| throw "Uploaded asset metadata failed for $($asset.name)." | |
| } | |
| } | |
| $readbackDirectory = Join-Path $env:RUNNER_TEMP 'release-readback' | |
| New-Item -ItemType Directory -Path $readbackDirectory | Out-Null | |
| gh release download $tag --repo $env:GITHUB_REPOSITORY --dir $readbackDirectory | |
| if ($LASTEXITCODE -ne 0) { throw "Could not download the uploaded draft assets for byte readback." } | |
| $readbackNames = @(Get-ChildItem -LiteralPath $readbackDirectory -File | ForEach-Object Name | Sort-Object) | |
| if (Compare-Object -ReferenceObject $expectedAssetNames -DifferenceObject $readbackNames) { | |
| throw "Downloaded release readback does not contain exactly the gated assets." | |
| } | |
| foreach ($asset in $metadata.assets) { | |
| $readbackPath = Join-Path $readbackDirectory $asset.name | |
| if ((Get-Item -LiteralPath $readbackPath).Length -ne [long]$asset.bytes -or (Get-FileHash -LiteralPath $readbackPath -Algorithm SHA256).Hash.ToLowerInvariant() -ne $asset.sha256) { | |
| throw "Downloaded release readback failed for $($asset.name)." | |
| } | |
| } | |
| $readbackInstaller = Join-Path $readbackDirectory $installerAsset[0].name | |
| gh attestation verify $readbackInstaller --repo $env:GITHUB_REPOSITORY | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "The release readback installer failed repository/workflow build-provenance verification." | |
| } | |
| gh release edit $tag --repo $env:GITHUB_REPOSITORY --draft=false --latest | |
| if ($LASTEXITCODE -ne 0) { throw "Release publication failed with exit code $LASTEXITCODE." } | |
| $ownedDraft = $false | |
| } catch { | |
| if ($ownedDraft) { | |
| $owned = gh release view $tag --repo $env:GITHUB_REPOSITORY --json isDraft 2>$null | ConvertFrom-Json | |
| if ($LASTEXITCODE -eq 0 -and $owned.isDraft) { | |
| gh release delete $tag --repo $env:GITHUB_REPOSITORY --cleanup-tag --yes 2>$null | Out-Null | |
| } | |
| } | |
| throw | |
| } | |
| $published = gh release view $tag --repo $env:GITHUB_REPOSITORY --json 'tagName,name,isDraft,isPrerelease,targetCommitish,url,assets' | ConvertFrom-Json | |
| if ($published.tagName -ne $tag -or $published.isDraft -or $published.isPrerelease -or $published.targetCommitish -ne $env:GITHUB_SHA) { | |
| throw "The final published release is not the unique non-draft release for this run." | |
| } | |
| $publishedAssetNames = @($published.assets | ForEach-Object name | Sort-Object) | |
| if (Compare-Object -ReferenceObject $expectedAssetNames -DifferenceObject $publishedAssetNames) { | |
| throw "The final release asset list changed after publication." | |
| } | |
| Write-Output "Verified non-draft release: $($published.url)" |