name: Release

# Triggered by pushing an annotated tag of the form v1.2.3 (or any v-prefixed
# semver). The job runs on a Windows runner because building the WiX MSI
# requires the WiX SDK which is supported on Windows; the engine + console
# can in principle be built on Linux, but for simplicity we do everything in
# one job here so the publish output paths line up for the installer.
on:
  push:
    tags:
      - 'v*.*.*'

jobs:
  build-msi:
    runs-on: windows-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0  # tags + full history needed to derive the version

      - name: Derive version from tag
        id: ver
        shell: pwsh
        run: |
          # GITHUB_REF is refs/tags/vX.Y.Z; strip the prefix.
          $tag = "${env:GITHUB_REF}".Replace('refs/tags/', '')
          $version = $tag.TrimStart('v')
          "tag=$tag" >> $env:GITHUB_OUTPUT
          "version=$version" >> $env:GITHUB_OUTPUT
          Write-Host "Building version $version (tag $tag)"

      - name: Setup .NET 8
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: 8.0.x

      # Probe for the signing cert up front and expose a step output downstream
      # steps can use in their `if:` guards. We can't reference `secrets.*`
      # directly from `if:` (Forgejo/GitHub policy), so we set a dummy env
      # variable from the secret and check whether it's non-empty here.
      - name: Detect signing configuration
        id: signcfg
        shell: pwsh
        env:
          PFX_PROBE: ${{ secrets.SIGN_CERT_PFX_BASE64 }}
        run: |
          if ($env:PFX_PROBE) {
            "enabled=true" >> $env:GITHUB_OUTPUT
            Write-Host "Code-signing: ENABLED (cert secret detected)."
          } else {
            "enabled=false" >> $env:GITHUB_OUTPUT
            Write-Host "Code-signing: DISABLED (SIGN_CERT_PFX_BASE64 not set). Build will produce unsigned binaries."
          }

      - name: Restore (Windows solution filter)
        run: dotnet restore Dragon-ISO.Windows.slnf

      - name: Build (Release, treat warnings as errors)
        run: dotnet build Dragon-ISO.Windows.slnf --configuration Release --no-restore /p:Version=${{ steps.ver.outputs.version }}

      - name: Run unit tests (excluding requires=ndi)
        run: >
          dotnet test Dragon-ISO.Windows.slnf
          --configuration Release
          --no-build
          --filter "Category!=ndi&requires!=ndi"

      - name: Publish Dragon-ISO.App (framework-dependent, win-x64)
        run: >
          dotnet publish src/Dragon-ISO.App/Dragon-ISO.App.csproj
          --configuration Release
          --runtime win-x64
          --self-contained false
          --output publish/Dragon-ISO
          /p:Version=${{ steps.ver.outputs.version }}

      - name: Publish Dragon-ISO.Console (framework-dependent, win-x64)
        run: >
          dotnet publish src/Dragon-ISO.Console/Dragon-ISO.Console.csproj
          --configuration Release
          --runtime win-x64
          --self-contained false
          --output publish/Dragon-ISO-Console
          /p:Version=${{ steps.ver.outputs.version }}

      # Code-sign the WPF .exe BEFORE the MSI is built, so the MSI's embedded
      # binaries are signed too. Skipped silently when the signing secrets
      # aren't configured â€” that's the default state and keeps unsigned builds
      # working unchanged.
      #
      # To enable signing, set both Forgejo Actions secrets:
      #   SIGN_CERT_PFX_BASE64  â€” base64 of your code-signing PFX file
      #                           ( certutil -encode in.pfx out.b64; strip BEGIN/END lines )
      #   SIGN_CERT_PASSWORD    â€” the PFX password
      # Optionally:
      #   SIGN_TIMESTAMP_URL    â€” RFC 3161 timestamp server (default: digicert)
      - name: Sign Dragon-ISO.exe (optional, skipped if no cert)
        if: ${{ steps.signcfg.outputs.enabled == 'true' }}
        env:
          SIGN_CERT_PFX_BASE64: ${{ secrets.SIGN_CERT_PFX_BASE64 }}
          SIGN_CERT_PASSWORD: ${{ secrets.SIGN_CERT_PASSWORD }}
          SIGN_TIMESTAMP_URL: ${{ secrets.SIGN_TIMESTAMP_URL }}
        shell: pwsh
        run: |
          $tsUrl = if ($env:SIGN_TIMESTAMP_URL) { $env:SIGN_TIMESTAMP_URL } else { 'http://timestamp.digicert.com' }
          $pfxPath = Join-Path $env:RUNNER_TEMP 'codesign.pfx'
          [IO.File]::WriteAllBytes($pfxPath, [Convert]::FromBase64String($env:SIGN_CERT_PFX_BASE64))
          $signtool = Get-ChildItem 'C:\Program Files (x86)\Windows Kits\10\bin' -Recurse -Filter signtool.exe `
            | Where-Object { $_.FullName -match '\\x64\\' } `
            | Select-Object -First 1
          if (-not $signtool) { throw 'signtool.exe not found on runner' }
          & $signtool.FullName sign `
            /f $pfxPath `
            /p $env:SIGN_CERT_PASSWORD `
            /fd SHA256 `
            /td SHA256 `
            /tr $tsUrl `
            'publish/Dragon-ISO/DragonISO.exe'
          if ($LASTEXITCODE -ne 0) { throw "signtool failed on Dragon-ISO.exe (exit $LASTEXITCODE)" }
          Remove-Item $pfxPath -Force

      - name: Build MSI installer
        run: >
          dotnet build installer/Dragon-ISO.Installer.wixproj
          --configuration Release
          /p:Version=${{ steps.ver.outputs.version }}

      - name: Locate MSI
        id: msi
        shell: pwsh
        run: |
          $msi = Get-ChildItem -Path installer/bin -Recurse -Filter '*.msi' | Select-Object -First 1
          if (-not $msi) { throw "No MSI produced under installer/bin." }
          "path=$($msi.FullName)" >> $env:GITHUB_OUTPUT
          "name=$($msi.Name)" >> $env:GITHUB_OUTPUT
          Write-Host "MSI: $($msi.FullName) ($($msi.Length) bytes)"

      # Sign the produced MSI itself. Same gate as exe signing â€” runs only if
      # the cert secret is set. Splitting the two stages means the inner exe
      # is signed before being embedded, AND the wrapping MSI carries its own
      # signature for SmartScreen.
      - name: Sign MSI (optional, skipped if no cert)
        if: ${{ steps.signcfg.outputs.enabled == 'true' }}
        env:
          SIGN_CERT_PFX_BASE64: ${{ secrets.SIGN_CERT_PFX_BASE64 }}
          SIGN_CERT_PASSWORD: ${{ secrets.SIGN_CERT_PASSWORD }}
          SIGN_TIMESTAMP_URL: ${{ secrets.SIGN_TIMESTAMP_URL }}
          MSI_PATH: ${{ steps.msi.outputs.path }}
        shell: pwsh
        run: |
          $tsUrl = if ($env:SIGN_TIMESTAMP_URL) { $env:SIGN_TIMESTAMP_URL } else { 'http://timestamp.digicert.com' }
          $pfxPath = Join-Path $env:RUNNER_TEMP 'codesign.pfx'
          [IO.File]::WriteAllBytes($pfxPath, [Convert]::FromBase64String($env:SIGN_CERT_PFX_BASE64))
          $signtool = Get-ChildItem 'C:\Program Files (x86)\Windows Kits\10\bin' -Recurse -Filter signtool.exe `
            | Where-Object { $_.FullName -match '\\x64\\' } `
            | Select-Object -First 1
          if (-not $signtool) { throw 'signtool.exe not found on runner' }
          & $signtool.FullName sign `
            /f $pfxPath `
            /p $env:SIGN_CERT_PASSWORD `
            /fd SHA256 `
            /td SHA256 `
            /tr $tsUrl `
            $env:MSI_PATH
          if ($LASTEXITCODE -ne 0) { throw "signtool failed on MSI (exit $LASTEXITCODE)" }
          Remove-Item $pfxPath -Force

      - name: Upload MSI as workflow artifact
        uses: actions/upload-artifact@v4
        with:
          name: ${{ steps.msi.outputs.name }}
          path: ${{ steps.msi.outputs.path }}

      # Forgejo doesn't ship a stable upload-release-asset action, so we use
      # the REST API directly. This: (1) finds the release that the tag push
      # auto-created, (2) uploads the MSI as an asset on it. Requires that
      # the repo's "Create a release on tag push" setting is on, OR that the
      # release was created beforehand. If no release exists, we create one.
      - name: Attach MSI to release
        shell: pwsh
        env:
          FORGE_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          FORGE_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
          TAG: ${{ steps.ver.outputs.tag }}
          MSI_PATH: ${{ steps.msi.outputs.path }}
          MSI_NAME: ${{ steps.msi.outputs.name }}
        run: |
          $headers = @{ Authorization = "token $env:FORGE_TOKEN" }

          # Find the release for this tag.
          try {
            $release = Invoke-RestMethod -Method Get `
              -Uri "$env:FORGE_API/releases/tags/$env:TAG" -Headers $headers
          } catch {
            Write-Host "No release found for $env:TAG; creating one."
            $body = @{
              tag_name   = $env:TAG
              name       = "Dragon-ISO $env:TAG"
              body       = "Automated build from tag $env:TAG."
              draft      = $false
              prerelease = $env:TAG -match '-(alpha|beta|rc)'
            } | ConvertTo-Json
            $release = Invoke-RestMethod -Method Post `
              -Uri "$env:FORGE_API/releases" -Headers $headers `
              -ContentType 'application/json' -Body $body
          }

          # Upload the MSI as an asset.
          $uploadUri = "$env:FORGE_API/releases/$($release.id)/assets?name=$env:MSI_NAME"
          curl.exe -fSL `
            -H "Authorization: token $env:FORGE_TOKEN" `
            -H "Content-Type: application/octet-stream" `
            --upload-file "$env:MSI_PATH" `
            "$uploadUri"
          Write-Host "Asset $env:MSI_NAME attached to release $env:TAG."
