diff --git a/.github/actions/windows-patches/Ensure-Location.ps1 b/.github/actions/windows-patches/Ensure-Location.ps1 new file mode 100644 index 000000000..36fb2f7ae --- /dev/null +++ b/.github/actions/windows-patches/Ensure-Location.ps1 @@ -0,0 +1,29 @@ +function Ensure-Location { + <# + .SYNOPSIS + Ensures current location to be set to specified directory. + .DESCRIPTION + If specified directory exists, switch to it. Otherwise create it, + then switch. + .EXAMPLE + Ensure-Location "My-Directory" + Ensure-Location -Path "Path-To-My-Directory" + #> + + param( + [Parameter(Mandatory)] + [string] $Path + ) + + if ( ! ( Test-Path $Path ) ) { + $_Params = @{ + ItemType = "Directory" + Path = ${Path} + ErrorAction = "SilentlyContinue" + } + + New-Item @_Params | Set-Location + } else { + Set-Location -Path ${Path} + } +} diff --git a/.github/actions/windows-patches/Invoke-External.ps1 b/.github/actions/windows-patches/Invoke-External.ps1 new file mode 100644 index 000000000..d1bfb7585 --- /dev/null +++ b/.github/actions/windows-patches/Invoke-External.ps1 @@ -0,0 +1,40 @@ +function Invoke-External { + <# + .SYNOPSIS + Invokes a non-PowerShell command. + .DESCRIPTION + Runs a non-PowerShell command, and captures its return code. + Throws an exception if the command returns non-zero. + .EXAMPLE + Invoke-External 7z x $MyArchive + #> + + if ( $args.Count -eq 0 ) { + throw 'Invoke-External called without arguments.' + } + + if ( ! ( Test-Path function:Log-Information ) ) { + . $PSScriptRoot/Logger.ps1 + } + + $Command = $args[0] + $CommandArgs = @() + + if ( $args.Count -gt 1) { + $CommandArgs = $args[1..($args.Count - 1)] + } + + $_EAP = $ErrorActionPreference + $ErrorActionPreference = "Continue" + + Log-Debug "Invoke-External: ${Command} ${CommandArgs}" + + & $command $commandArgs + $Result = $LASTEXITCODE + + $ErrorActionPreference = $_EAP + + if ( $Result -ne 0 ) { + throw "${Command} ${CommandArgs} exited with non-zero code ${Result}." + } +} diff --git a/.github/actions/windows-patches/Logger.ps1 b/.github/actions/windows-patches/Logger.ps1 new file mode 100644 index 000000000..61a81e515 --- /dev/null +++ b/.github/actions/windows-patches/Logger.ps1 @@ -0,0 +1,149 @@ +function Log-Debug { + [CmdletBinding()] + param( + [Parameter(Mandatory,ValueFromPipeline)] + [ValidateNotNullOrEmpty()] + [string[]] $Message + ) + + Process { + foreach($m in $Message) { + Write-Debug $m + } + } +} + +function Log-Verbose { + [CmdletBinding()] + param( + [Parameter(Mandatory,ValueFromPipeline)] + [ValidateNotNullOrEmpty()] + [string[]] $Message + ) + + Process { + foreach($m in $Message) { + Write-Verbose $m + } + } +} + +function Log-Warning { + [CmdletBinding()] + param( + [Parameter(Mandatory,ValueFromPipeline)] + [ValidateNotNullOrEmpty()] + [string[]] $Message + ) + + Process { + foreach($m in $Message) { + Write-Warning $m + } + } +} + +function Log-Error { + [CmdletBinding()] + param( + [Parameter(Mandatory,ValueFromPipeline)] + [ValidateNotNullOrEmpty()] + [string[]] $Message + ) + + Process { + foreach($m in $Message) { + Write-Error $m + } + } +} + +function Log-Information { + [CmdletBinding()] + param( + [Parameter(Mandatory,ValueFromPipeline)] + [ValidateNotNullOrEmpty()] + [string[]] $Message + ) + + Process { + if ( ! ( $script:Quiet ) ) { + $StageName = $( if ( $script:StageName -ne $null ) { $script:StageName } else { '' }) + $Icon = ' =>' + + foreach($m in $Message) { + Write-Host -NoNewLine -ForegroundColor Blue " ${StageName} $($Icon.PadRight(5)) " + Write-Host "${m}" + } + } + } +} + +function Log-Group { + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline)] + [string[]] $Message + ) + + Process { + if ( $Env:CI -ne $null ) { + if ( $script:LogGroup ) { + Write-Output '::endgroup::' + $script:LogGroup = $false + } + + if ( $Message.count -ge 1 ) { + Write-Output "::group::$($Message -join ' ')" + $script:LogGroup = $true + } + } else { + if ( $Message.count -ge 1 ) { + Log-Information $Message + } + } + } +} + +function Log-Status { + [CmdletBinding()] + param( + [Parameter(Mandatory,ValueFromPipeline)] + [ValidateNotNullOrEmpty()] + [string[]] $Message + ) + + Process { + if ( ! ( $script:Quiet ) ) { + $StageName = $( if ( $StageName -ne $null ) { $StageName } else { '' }) + $Icon = ' >' + + foreach($m in $Message) { + Write-Host -NoNewLine -ForegroundColor Green " ${StageName} $($Icon.PadRight(5)) " + Write-Host "${m}" + } + } + } +} + +function Log-Output { + [CmdletBinding()] + param( + [Parameter(Mandatory,ValueFromPipeline)] + [ValidateNotNullOrEmpty()] + [string[]] $Message + ) + + Process { + if ( ! ( $script:Quiet ) ) { + $StageName = $( if ( $script:StageName -ne $null ) { $script:StageName } else { '' }) + $Icon = '' + + foreach($m in $Message) { + Write-Output " ${StageName} $($Icon.PadRight(5)) ${m}" + } + } + } +} + +$Columns = (Get-Host).UI.RawUI.WindowSize.Width - 5 diff --git a/.github/actions/windows-patches/action.yaml b/.github/actions/windows-patches/action.yaml new file mode 100644 index 000000000..e9103867e --- /dev/null +++ b/.github/actions/windows-patches/action.yaml @@ -0,0 +1,111 @@ +name: Run bouf Patch Generation +description: Generates OBS updater manifest and patches +inputs: + gcsAccessKeyId: + description: GCS S3 Access Key ID + required: true + gcsAccessKeySecret: + description: GCS S3 Access Key Secret + required: true + workflowSecret: + description: GitHub API token to use for API calls + required: true + tagName: + description: GitHub Release tag + required: true + channel: + description: Update channel + required: false + default: 'stable' + +runs: + using: composite + steps: + - uses: actions/checkout@v4 + with: + path: "repo" + fetch-depth: 0 + + - name: Download Release Artifact + shell: pwsh + env: + GH_TOKEN: ${{ inputs.workflowSecret }} + run: | + # Download OBS release + . ${env:GITHUB_ACTION_PATH}\Invoke-External.ps1 + Invoke-External gh release download "${{ inputs.tagName }}" -p "*-Windows.zip" + Expand-Archive -Path "*-Windows.zip" -DestinationPath "${{ github.workspace }}/build" + + - name: Setup bouf + shell: pwsh + env: + BOUF_TAG: 'v0.6.3' + BOUF_HASH: '7f1d266467620aa553a705391ee06128e8ee14af66129a0e64a282997fb6fd83' + BOUF_NSIS_HASH: 'a234126de89f122b6a552df3416de3eabcb4195217626c7f4eaec71b20fe36eb' + GH_TOKEN: ${{ github.token }} + run: | + # Download bouf release + . ${env:GITHUB_ACTION_PATH}\Ensure-Location.ps1 + . ${env:GITHUB_ACTION_PATH}\Invoke-External.ps1 + Ensure-Location bouf + $windows_zip = "bouf-windows-${env:BOUF_TAG}.zip" + $nsis_zip = "bouf-nsis-${env:BOUF_TAG}.zip" + Invoke-External gh release download "${env:BOUF_TAG}" -R "obsproject/bouf" -p $windows_zip -p $nsis_zip + + if ((Get-FileHash $windows_zip -Algorithm SHA256).Hash -ne "${env:BOUF_HASH}") { + throw "bouf hash does not match." + } + if ((Get-FileHash $nsis_zip -Algorithm SHA256).Hash -ne "${env:BOUF_NSIS_HASH}") { + throw "NSIS package hash does not match." + } + + Expand-Archive -Path $windows_zip -DestinationPath bin + Expand-Archive -Path $nsis_zip -DestinationPath nsis + + - name: Install rclone and pandoc + shell: pwsh + run: | + choco install rclone pandoc -y --no-progress + + - name: Download Previous Builds + shell: pwsh + env: + RCLONE_TRANSFERS: '100' + RCLONE_FAST_LIST: 'true' + RCLONE_EXCLUDE: '{pdbs/**,**/${{ inputs.tagName }}/**}' + RCLONE_S3_PROVIDER: 'GCS' + RCLONE_S3_ACCESS_KEY_ID: '${{ inputs.gcsAccessKeyId }}' + RCLONE_S3_SECRET_ACCESS_KEY: '${{ inputs.gcsAccessKeySecret }}' + RCLONE_S3_ENDPOINT: 'https://storage.googleapis.com' + run: | + rclone -q copy ":s3:obs-builds" "${{ github.workspace }}/old_builds" + + - name: Prepare Release Notes + shell: pwsh + run: | + # Release notes are just the tag body on Windows + Set-Location repo + git tag -l --format='%(contents:body)' ${{ inputs.version }} > "${{ github.workspace }}/notes.rst" + + - name: Run bouf + shell: pwsh + run: | + . ${env:GITHUB_ACTION_PATH}\Invoke-External.ps1 + $boufArgs = @( + "--config", "${env:GITHUB_ACTION_PATH}/config.toml", + "--version", "${{ inputs.tagName }}" + "--branch", "${{ inputs.channel }}" + "--notes-file", "${{ github.workspace }}/notes.rst" + "-i", "${{ github.workspace }}/build" + "-p", "${{ github.workspace }}/old_builds" + "-o", "${{ github.workspace }}/output" + "--updater-data-only" + ) + Invoke-External "${{ github.workspace }}\bouf\bin\bouf.exe" @boufArgs + + - name: Upload Outputs + uses: actions/upload-artifact@v4 + with: + name: windows-updater-files + compression-level: 0 + path: ${{ github.workspace }}/output diff --git a/.github/actions/windows-patches/config.toml b/.github/actions/windows-patches/config.toml new file mode 100644 index 000000000..93546e0f2 --- /dev/null +++ b/.github/actions/windows-patches/config.toml @@ -0,0 +1,25 @@ +[general] +log_level = "trace" + +[env] +# On CI these should be in %PATH% +sevenzip_path = "7z" +makensis_path = "makensis" +pandoc_path = "pandoc" +pdbcopy_path = "C:/Program Files (x86)/Windows Kits/10/Debuggers/x64/pdbcopy.exe" + +[prepare.codesign] +skip_sign = true + +[generate] +patch_type = "zstd" +compress_files = true + +[package] + +[package.installer] +skip = true + +[package.updater] +vc_redist_path = "bouf/nsis/VC_redist.x64.exe" +skip_sign = true diff --git a/.github/workflows/dispatch.yaml b/.github/workflows/dispatch.yaml index 496c707f9..cf6bc78d0 100644 --- a/.github/workflows/dispatch.yaml +++ b/.github/workflows/dispatch.yaml @@ -12,6 +12,7 @@ on: - services - translations - documentation + - patches ref: description: GitHub reference to use for job type: string @@ -28,6 +29,10 @@ on: description: Custom macOS Intel build for Steam Upload type: string required: false + channel: + description: Channel to use when generating Windows update files + type: string + required: false permissions: contents: write jobs: @@ -130,3 +135,17 @@ jobs: apiToken: ${{ secrets.CF_API_TOKEN }} accountId: ${{ secrets.CF_ACCOUNT_ID }} command: pages publish . --project-name=${{ vars.CF_PAGES_PROJECT }} --commit-hash='${{ steps.setup.outputs.commitHash }}' + + windows-patches: + name: Create Windows Patches 🩹 + if: github.repository_owner == 'obsproject' && inputs.job == 'patches' + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/windows-patches + with: + tagName: ${{ inputs.ref }} + workflowSecret: ${{ github.token }} + channel: ${{ inputs.channel }} + gcsAccessKeyId: ${{ secrets.GCS_ACCESS_KEY_ID }} + gcsAccessKeySecret: ${{ secrets.GCS_ACCESS_KEY_SECRET }} diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 16c371b0f..0b023e3f9 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -32,10 +32,12 @@ jobs: +([0-9]).+([0-9]).+([0-9]) ) echo 'validTag=true' >> $GITHUB_OUTPUT echo 'flatpakMatrix=["beta", "stable"]' >> $GITHUB_OUTPUT + echo 'updateChannel=stable' >> $GITHUB_OUTPUT ;; +([0-9]).+([0-9]).+([0-9])-@(beta|rc)*([0-9]) ) echo 'validTag=true' >> $GITHUB_OUTPUT echo 'flatpakMatrix=["beta"]' >> $GITHUB_OUTPUT + echo 'updateChannel=beta' >> $GITHUB_OUTPUT ;; *) echo 'validTag=false' >> $GITHUB_OUTPUT ;; esac @@ -171,3 +173,18 @@ jobs: workflowSecret: ${{ github.token }} tagName: ${{ github.ref_name }} preview: false + + windows-patches: + name: Create Windows Patches 🩹 + needs: check-tag + if: github.repository_owner == 'obsproject' && fromJSON(needs.check-tag.outputs.validTag) + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/windows-patches + with: + tagName: ${{ github.ref_name }} + workflowSecret: ${{ github.token }} + channel: ${{ needs.check-tag.outputs.updateChannel }} + gcsAccessKeyId: ${{ secrets.GCS_ACCESS_KEY_ID }} + gcsAccessKeySecret: ${{ secrets.GCS_ACCESS_KEY_SECRET }}