Browse Source

CI: Sign and package Windows builds

derrod 2 years ago
parent
commit
83e65170ad

+ 29 - 0
.github/actions/bouf/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}
+    }
+}

+ 40 - 0
.github/actions/bouf/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}."
+    }
+}

+ 149 - 0
.github/actions/bouf/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

+ 133 - 0
.github/actions/bouf/action.yaml

@@ -0,0 +1,133 @@
+name: Run bouf
+description: Generates signed OBS install files and updater files
+inputs:
+  gcpWorkloadIdentityProvider:
+    description: GCP Identity Provider Pool ID
+    required: true
+  gcpServiceAccountName:
+    description: Google service account name
+    required: true
+  gcpManifestSigningKeyName:
+    description: Name of the manifest signing key in GCP KMS
+    required: false
+  version:
+    description: Version string (e.g., 30.0.0-rc1)
+    required: true
+  channel:
+    description: Update channel
+    required: false
+    default: 'stable'
+
+runs:
+  using: composite
+  steps:
+    - name: Extract Artifact
+      shell: pwsh
+      run: |
+        Expand-Archive -Path build\*.zip -DestinationPath build
+        Remove-Item build\*.zip
+
+    - name: Setup bouf
+      shell: pwsh
+      env:
+        BOUF_TAG: 'v0.6.1'
+        BOUF_HASH: '7292e43186ecc6210079fa5702254455797c7652dc6b08b5b61ac2d721766d86'
+        BOUF_NSIS_HASH: '2f5ecff05a002913c10aafa838febc1b0ae6e779f5ca67efa545ed787ae485a0'
+        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: Download Google CNG Provider
+      shell: pwsh
+      env:
+        CNG_TAG: 'cng-v1.0'
+        GH_TOKEN: ${{ github.token }}
+      run: |
+        # Download Google CNG provider release from github
+        . ${env:GITHUB_ACTION_PATH}\Ensure-Location.ps1
+        . ${env:GITHUB_ACTION_PATH}\Invoke-External.ps1
+        Ensure-Location gcng
+        
+        Invoke-External gh release download "${env:CNG_TAG}" -R "GoogleCloudPlatform/kms-integrations" -p "*amd64.zip"
+        Expand-Archive -Path *.zip
+        $sigPath = Get-ChildItem *.sig -Recurse
+        $msiPath = Get-ChildItem *.msi -Recurse
+        # Verify digital signature against Google's public key
+        Invoke-External openssl dgst -sha384 -verify "${env:GITHUB_ACTION_PATH}/cng-release-signing-key.pem" -signature $sigPath $msiPath
+        # Finally, install the CNG provider
+        Invoke-External msiexec /i $msiPath /qn /norestart
+
+    - name: Install pandoc and rclone
+      shell: pwsh
+      run: |
+        choco install rclone --version 1.64.2 -y --no-progress
+        choco install pandoc --version 3.1.9 -y --no-progress
+
+    - 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: 'Authenticate to Google Cloud'
+      uses: 'google-github-actions/auth@35b0e87d162680511bf346c299f71c9c5c379033'
+      with:
+        workload_identity_provider: ${{ inputs.gcpWorkloadIdentityProvider }}
+        service_account: ${{ inputs.gcpServiceAccountName }}
+
+    - name: 'Set up Cloud SDK'
+      uses: 'google-github-actions/setup-gcloud@e30db14379863a8c79331b04a9969f4c1e225e0b'
+
+    - name: Download Old Builds
+      shell: pwsh
+      env:
+        RCLONE_GCS_ENV_AUTH: 'true'
+      run: |
+        rclone copy --transfers 100 :gcs:obs-builds "${{ github.workspace }}/old_builds"
+
+    - name: Run bouf
+      shell: pwsh
+      run: |
+        . ${env:GITHUB_ACTION_PATH}\Invoke-External.ps1
+        $boufArgs = @(
+           "--config",     "${env:GITHUB_ACTION_PATH}/config.toml",
+           "--version",    "${{ inputs.version }}"
+           "--branch",     "${{ inputs.channel }}"
+           "--notes-file", "${{ github.workspace }}/notes.rst"
+           "-i",           "${{ github.workspace }}/build"
+           "-p",           "${{ github.workspace }}/old_builds"
+           "-o",           "${{ github.workspace }}/output"
+        )
+        Invoke-External "${{ github.workspace }}\bouf\bin\bouf.exe" @boufArgs
+
+    - name: Sign Updater Manifest
+      shell: pwsh
+      if: inputs.gcpManifestSigningKeyName != ''
+      run: |
+        $gcloudArgs = @(
+           "--input-file",       "${{ github.workspace }}/output/manifest.json"
+           "--signature-file",   "${{ github.workspace }}/output/manifest.json.sig"
+           "--digest-algorithm", "sha512"
+           "--location",         "global"
+           "--keyring",          "production"
+           "--key",              "${{ inputs.gcpManifestSigningKeyName }}"
+           "--version",          "1"
+        )
+        gcloud kms asymmetric-sign @gcloudArgs

+ 5 - 0
.github/actions/bouf/cng-release-signing-key.pem

@@ -0,0 +1,5 @@
+-----BEGIN PUBLIC KEY-----
+MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEtfLbXkHUVc9oUPTNyaEK3hIwmuGRoTtd
+6zDhwqjJuYaMwNd1aaFQLMawTwZgR0Xn27ymVWtqJHBe0FU9BPIQ+SFmKw+9jSwu
+/FuqbJnLmTnWMJ1jRCtyHNZawvv2wbiB
+-----END PUBLIC KEY-----

+ 56 - 0
.github/actions/bouf/config.toml

@@ -0,0 +1,56 @@
+[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"
+
+## Preparation steps
+[prepare]
+
+[prepare.copy]
+never_copy = [
+    "bin/32bit",
+    "obs-plugins/32bit",
+    ".keepme",
+]
+
+[prepare.codesign]
+sign_cert_file = "repo/.github/actions/bouf/test.crt"
+sign_kms_key_id = "projects/ci-signing/locations/global/keyRings/testing/cryptoKeys/signing-hsm/cryptoKeyVersions/1"
+sign_digest = "sha256"
+sign_ts_serv = "http://timestamp.digicert.com"
+sign_exts = ['exe', 'dll', 'pyd']
+
+[prepare.strip_pdbs]
+# PDBs to not strip
+exclude = [
+    "obs-frontend-api.pdb",
+    "obs64.pdb",
+    "obs.pdb",
+]
+
+## Delta patch generation
+[generate]
+patch_type = "zstd"
+compress_files = true
+
+exclude_from_parallel = [
+    "libcef.dll"
+]
+
+[package]
+[package.installer]
+nsis_script = "bouf/nsis/mp-installer.nsi"
+
+[package.zip]
+skip = true
+name = "OBS-Studio-{version}.zip"
+pdb_name = "OBS-Studio-{version}-pdbs.zip"
+
+[package.updater]
+vc_redist_path = "bouf/nsis/VC_redist.x64.exe"
+skip_sign = true

+ 11 - 0
.github/actions/bouf/test.crt

@@ -0,0 +1,11 @@
+-----BEGIN CERTIFICATE-----
+MIIBpDCCAUmgAwIBAgIUXeKu2+AXK2yR8WyuTTRg8+t6p/kwCgYIKoZIzj0EAwIw
+JzElMCMGA1UEAwwcV2l6YXJkcyBvZiBPQlMgTExDIChURVNUSU5HKTAeFw0yMzEx
+MDQwMDA3NDBaFw0zMzExMDEwMDA3NDBaMCcxJTAjBgNVBAMMHFdpemFyZHMgb2Yg
+T0JTIExMQyAoVEVTVElORykwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARVAdL6
+pkfHYuO8rtndlAjpbz9bMBIVN8elrg4cDJkReAqsjH78ulhKvkor6lfF7rbRzIKA
+u026v1aDE79Z+bz+o1MwUTAdBgNVHQ4EFgQUsFQqmZEEapghq7ZPqpejjFeu8Dsw
+HwYDVR0jBBgwFoAUsFQqmZEEapghq7ZPqpejjFeu8DswDwYDVR0TAQH/BAUwAwEB
+/zAKBggqhkjOPQQDAgNJADBGAiEAmNSMQqZu3U9eMP+PbmssCigkbFKG8mGLNLFN
+J+KfcdkCIQCVOjg6cqk4zFe7H8EKhB6CwOAIOQrgjGWLihtO2lovCw==
+-----END CERTIFICATE-----

+ 89 - 0
.github/workflows/push.yaml

@@ -194,6 +194,95 @@ jobs:
           name: macos-sparkle-update
           path: ${{ github.workspace }}/output
 
+  create-windows-update:
+    name: Create Windows Update 🥩
+    if: github.repository_owner == 'obsproject' && github.ref_type == 'tag'
+    runs-on: windows-2022
+    needs: build-project
+    permissions:
+      contents: 'read'
+      id-token: 'write'
+    defaults:
+      run:
+        shell: pwsh
+    environment:
+      name: bouf
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          path: "repo"
+          fetch-depth: 0
+          ref: ${{ github.ref }}
+
+      - name: Set Up Environment 🔧
+        id: setup
+        env:
+          BOUF_ACTION_HASH: '4b421d1fa51cbf35f9c68f80795be3468dc480d47989c0bf713c39a7d62dec9e'
+        run: |
+          $channel = if ($env:GITHUB_REF_NAME -match "(beta|rc)") { "beta" } else { "stable" }
+          $shortHash = $env:GITHUB_SHA.Substring(0,9)
+          "channel=${channel}" >> $env:GITHUB_OUTPUT
+          "commitHash=${shortHash}" >> $env:GITHUB_OUTPUT
+          
+          # Ensure files in action haven't been modified
+          $folderHash = ''
+          $files = Get-ChildItem "${{ github.workspace }}\repo\.github\actions\bouf"
+          foreach ($file in $files) {
+            $folderHash += (Get-FileHash $file -Algorithm SHA256).Hash
+          }
+          # This is stupid but so is powershell
+          $stream = [IO.MemoryStream]::new([byte[]][char[]]$folderHash)
+          if ((Get-FileHash -InputStream $stream -Algorithm SHA256).Hash -ne "$env:BOUF_ACTION_HASH") {
+            throw "bouf action folder hash does not match."
+          }
+
+      - name: Download Artifact 📥
+        uses: actions/download-artifact@v3
+        with:
+          name: obs-studio-windows-x64-${{ steps.setup.outputs.commitHash }}
+          path: ${{ github.workspace }}/build
+
+      - name: Run bouf 🥩
+        uses: ./repo/.github/actions/bouf
+        with:
+          gcpWorkloadIdentityProvider: ${{ secrets.GCP_IDENTITY_POOL }}
+          gcpServiceAccountName: ${{ secrets.GCP_SERVICE_ACCOUNT_NAME }}
+          version: ${{ github.ref_name }}
+          channel: ${{ steps.setup.outputs.channel }}
+
+      - name: Upload Signed Build
+        uses: actions/upload-artifact@v4
+        with:
+          name: obs-studio-windows-x64-${{ github.ref_name }}-signed
+          compression-level: 6
+          path: ${{ github.workspace }}/output/install
+
+      - name: Upload PDBs
+        uses: actions/upload-artifact@v4
+        with:
+          name: obs-studio-windows-x64-${{ github.ref_name }}-pdbs
+          compression-level: 9
+          path: ${{ github.workspace }}/output/pdbs
+
+      - name: Upload Installer
+        uses: actions/upload-artifact@v4
+        with:
+          name: obs-studio-windows-x64-${{ github.ref_name }}-installer
+          compression-level: 0
+          path: ${{ github.workspace }}/output/*.exe
+
+      - name: Upload Updater Files
+        uses: actions/upload-artifact@v4
+        with:
+          name: obs-studio-windows-x64-${{ github.ref_name }}-patches
+          compression-level: 0
+          path: |
+            ${{ github.workspace }}/output/updater
+            ${{ github.workspace }}/output/*.json
+            ${{ github.workspace }}/output/*.sig
+            ${{ github.workspace }}/output/*.txt
+            ${{ github.workspace }}/output/*.rst
+
   create-release:
     name: Create Release 🛫
     if: github.ref_type == 'tag'