name: Release pipeline

on:
  workflow_dispatch:
    inputs:
      tag:
        description: 'Release tag to rehearse (for example v0.1.0-alpha.20)'
        required: false
        type: string
  push:
    tags:
      - 'v*'

permissions:
  contents: write
  id-token: write

jobs:
  bootstrap:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7.0.1

      - name: Setup cosign
        uses: sigstore/cosign-installer@v3.5.0

      - name: Validate bootstrap
        run: |
          bash -n deploy/ochenstarik-server-monitor-manager.sh
          bash -n deploy/smm-setup.sh
          bash -n deploy/ochenstarik-smm-policy-apply
          bash -n deploy/ochenstarik-smm-emergency
          bash -n tests/bootstrap/run-native-systemd-smoke.sh
          bash -n tests/bootstrap/run-systemd-container-smoke.sh
          shellcheck --severity=error deploy/ochenstarik-server-monitor-manager.sh
          shellcheck --severity=error deploy/smm-setup.sh
          shellcheck --severity=error deploy/ochenstarik-smm-policy-apply
          shellcheck --severity=error deploy/ochenstarik-smm-emergency
          shellcheck --severity=error tests/bootstrap/run-native-systemd-smoke.sh
          shellcheck --severity=error tests/bootstrap/run-systemd-container-smoke.sh
          bash tests/bootstrap/test-bootstrap-contract.sh
          bash tests/bootstrap/test-manifest-verification.sh
          bash tests/bootstrap/test-release-contract.sh

      - name: Package bootstrap
        shell: bash
        env:
          REHEARSAL_TAG: ${{ inputs.tag }}
        run: |
          set -Eeuo pipefail
          DIST_DIR=dist
          install -d "$DIST_DIR"
          install -m 0755 deploy/ochenstarik-server-monitor-manager.sh "$DIST_DIR/ochenstarik-server-monitor-manager.sh"
          install -m 0755 deploy/smm-setup.sh "$DIST_DIR/smm-setup.sh"
          
          # A branch rehearsal with an explicit tag must exercise the same
          # version-consistency gate as an immutable tag build.
          RELEASE_TAG=""
          if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
            RELEASE_TAG="${GITHUB_REF_NAME}"
          elif [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && -n "$REHEARSAL_TAG" ]]; then
            RELEASE_TAG="$REHEARSAL_TAG"
          fi
          if [[ -n "$RELEASE_TAG" ]]; then
            [[ "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]] \
              || { printf 'Invalid release tag: %s\n' "$RELEASE_TAG" >&2; exit 1; }
            sed -i "s/^PROGRAM_VERSION=.*$/PROGRAM_VERSION=\"${RELEASE_TAG}\"/" "$DIST_DIR/ochenstarik-server-monitor-manager.sh"
            grep -Fq "readonly DEFAULT_RELEASE_TAG=\"${RELEASE_TAG}\"" "$DIST_DIR/smm-setup.sh"
          fi
          
          (
            cd "$DIST_DIR"
            sha256sum ochenstarik-server-monitor-manager.sh > ochenstarik-server-monitor-manager.sh.sha256
            sha256sum smm-setup.sh > smm-setup.sh.sha256
          )

      - name: Upload bootstrap artifact
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a  # v7.0.1
        with:
          name: server-monitor-manager-bootstrap
          path: |
            dist/ochenstarik-server-monitor-manager.sh
            dist/ochenstarik-server-monitor-manager.sh.sha256
            dist/smm-setup.sh
            dist/smm-setup.sh.sha256

  publish-linux:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        runtime: [linux-x64, linux-arm64]
    steps:
      - name: Checkout
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7.0.1

      - name: Set up .NET 10
        uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68  # v6.0.0
        with:
          dotnet-version: 10.0.x

      - name: Set Version
        id: version
        shell: bash
        env:
          REHEARSAL_TAG: ${{ inputs.tag }}
        run: |
          if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
            VERSION="${GITHUB_REF_NAME#v}"
          elif [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && -n "$REHEARSAL_TAG" ]]; then
            VERSION="${REHEARSAL_TAG#v}"
          else
            VERSION="0.2.0-dev"
          fi
          echo "version=${VERSION}" >> $GITHUB_OUTPUT

      - name: Publish agent
        run: dotnet publish src/ServerMonitorManager.Agent/ServerMonitorManager.Agent.csproj --configuration Release --runtime ${{ matrix.runtime }} --self-contained true -p:PublishSingleFile=true -p:PublishTrimmed=true -p:RestoreLockedMode=true -p:Version=${{ steps.version.outputs.version }} -o out/agent

      - name: Publish control
        run: dotnet publish src/ServerMonitorManager.Control/ServerMonitorManager.Control.csproj --configuration Release --runtime ${{ matrix.runtime }} --self-contained true -p:PublishSingleFile=true -p:PublishTrimmed=true -p:RestoreLockedMode=true -p:Version=${{ steps.version.outputs.version }} -o out/control

      - name: Publish provisioning helper
        run: dotnet publish src/ServerMonitorManager.Provisioning.Helper/ServerMonitorManager.Provisioning.Helper.csproj --configuration Release --runtime ${{ matrix.runtime }} --self-contained true -p:PublishSingleFile=true -p:PublishTrimmed=true -p:RestoreLockedMode=true -p:Version=${{ steps.version.outputs.version }} -o out/provisioning-helper

      - name: Package
        shell: bash
        run: |
          set -Eeuo pipefail
          install -d out/deploy out/bootstrap
          install -m 0644 deploy/ochenstarik-smm-control.service out/deploy/
          install -m 0644 deploy/ochenstarik-smm-agent.service out/deploy/
          install -m 0644 deploy/ochenstarik-smm-provisioning-helper.service out/deploy/
          install -m 0644 deploy/ochenstarik-smm-firewall.service out/deploy/
          install -m 0755 deploy/ochenstarik-smm-policy-apply out/deploy/
          install -m 0755 deploy/ochenstarik-smm-emergency out/deploy/
          install -m 0755 deploy/ochenstarik-server-monitor-manager.sh out/bootstrap/
          archive="server-monitor-manager-${{ matrix.runtime }}.tar.gz"
          tar -C out -czf "$archive" agent control provisioning-helper deploy bootstrap
          sha256sum "$archive" > "$archive.sha256"

      - name: Generate SBOM
        run: |
          dotnet tool install --global CycloneDX --version 6.2.0
          dotnet CycloneDX ServerMonitorManager.slnx \
            --output . \
            --output-format Json \
            --filename "server-monitor-manager-${{ matrix.runtime }}-sbom.json"

      - name: Upload artifact
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a  # v7.0.1
        with:
          name: server-monitor-manager-${{ matrix.runtime }}
          path: |
            server-monitor-manager-${{ matrix.runtime }}.tar.gz
            server-monitor-manager-${{ matrix.runtime }}.tar.gz.sha256
            server-monitor-manager-${{ matrix.runtime }}-sbom.json

  package-windows:
    runs-on: windows-latest
    env:
      SIGNING_CERTIFICATE_BASE64: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_BASE64 }}
      SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }}
    steps:
      - name: Checkout
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7.0.1

      - name: Set up .NET 10
        uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68  # v6.0.0
        with:
          dotnet-version: 10.0.x

      - name: Prepare signing certificate
        id: signing
        shell: pwsh
        run: |
          $directory = Join-Path $env:RUNNER_TEMP 'smm-signing'
          New-Item -ItemType Directory -Path $directory -Force | Out-Null
          if ($env:SIGNING_CERTIFICATE_BASE64 -and $env:SIGNING_CERTIFICATE_PASSWORD) {
            Write-Output "::add-mask::$env:SIGNING_CERTIFICATE_PASSWORD"
            $pfx = Join-Path $directory 'trusted-signing.pfx'
            [IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:SIGNING_CERTIFICATE_BASE64))
            "certificate=$pfx" >> $env:GITHUB_OUTPUT
            "password=$env:SIGNING_CERTIFICATE_PASSWORD" >> $env:GITHUB_OUTPUT
            "test_certificate=false" >> $env:GITHUB_OUTPUT
          } else {
            $password = [Convert]::ToBase64String([Security.Cryptography.RandomNumberGenerator]::GetBytes(24))
            Write-Output "::add-mask::$password"
            ./build/windows/New-TestSigningCertificate.ps1 -OutputDirectory $directory -Password $password
            "certificate=$(Join-Path $directory 'server-monitor-manager-test-signing.pfx')" >> $env:GITHUB_OUTPUT
            "public_certificate=$(Join-Path $directory 'server-monitor-manager-test-signing.cer')" >> $env:GITHUB_OUTPUT
            "password=$password" >> $env:GITHUB_OUTPUT
            "test_certificate=true" >> $env:GITHUB_OUTPUT
          }

      - name: Set Version
        id: version
        shell: bash
        env:
          REHEARSAL_TAG: ${{ inputs.tag }}
        run: |
          if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
            VERSION="${GITHUB_REF_NAME#v}"
          elif [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && -n "$REHEARSAL_TAG" ]]; then
            VERSION="${REHEARSAL_TAG#v}"
          else
            VERSION="0.2.0-dev"
          fi
          echo "version=${VERSION}" >> $GITHUB_OUTPUT

      - name: Build signed MSIX
        shell: pwsh
        run: ./build/windows/Build-Installer.ps1 -CertificatePath '${{ steps.signing.outputs.certificate }}' -CertificatePassword '${{ steps.signing.outputs.password }}' -Version '${{ steps.version.outputs.version }}'

      - name: Include test certificate
        if: steps.signing.outputs.test_certificate == 'true'
        shell: pwsh
        run: Copy-Item -LiteralPath '${{ steps.signing.outputs.public_certificate }}' -Destination artifacts/windows-installer/ServerMonitorManager-test-signing.cer

      - name: Verify checksum
        shell: pwsh
        run: |
          $line = Get-Content artifacts/windows-installer/SHA256SUMS
          $expected = ($line -split ' ')[0]
          $actual = (Get-FileHash artifacts/windows-installer/ServerMonitorManager-win-x64.msix -Algorithm SHA256).Hash.ToLowerInvariant()
          if ($actual -ne $expected) { throw 'Windows installer checksum mismatch.' }

      - name: Generate SBOM
        shell: bash
        run: |
          dotnet tool install --global CycloneDX --version 6.2.0
          dotnet CycloneDX ServerMonitorManager.slnx \
            --output . \
            --output-format Json \
            --filename server-monitor-manager-win-x64-sbom.json

      - name: Upload installer artifact
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a  # v7.0.1
        with:
          name: server-monitor-manager-win-x64
          path: |
            artifacts/windows-installer/ServerMonitorManager-win-x64.msix
            artifacts/windows-installer/ServerMonitorManager-test-signing.cer
            artifacts/windows-installer/SHA256SUMS
            server-monitor-manager-win-x64-sbom.json
          if-no-files-found: error

  manifest:
    runs-on: ubuntu-latest
    needs: [bootstrap, publish-linux, package-windows]
    steps:
      - name: Setup cosign
        uses: sigstore/cosign-installer@v3.5.0
        
      - name: Download all artifacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          path: artifacts

      - name: Generate Manifest v2
        shell: bash
        env:
          REHEARSAL_TAG: ${{ inputs.tag }}
        run: |
          set -Eeuo pipefail

          # PowerShell emits CRLF for SHA256SUMS. Normalize the release asset and
          # prove that GNU sha256sum can consume it before publication.
          windows_dir=artifacts/server-monitor-manager-win-x64/artifacts/windows-installer
          perl -pi -e 's/\x0D$//' "$windows_dir/SHA256SUMS"
          (
            cd "$windows_dir"
            sha256sum -c SHA256SUMS
          )
          
          # Version fallback for non-tag runs
          if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
            VERSION="${GITHUB_REF_NAME}"
          elif [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && -n "$REHEARSAL_TAG" ]]; then
            VERSION="$REHEARSAL_TAG"
          else
            VERSION="0.0.0-dev"
          fi
          
          # Calculate hashes
          BOOTSTRAP_SHA=$(sha256sum artifacts/server-monitor-manager-bootstrap/ochenstarik-server-monitor-manager.sh | awk '{print $1}')
          SETUP_SHA=$(sha256sum artifacts/server-monitor-manager-bootstrap/smm-setup.sh | awk '{print $1}')
          LINUX_X64_SHA=$(sha256sum artifacts/server-monitor-manager-linux-x64/server-monitor-manager-linux-x64.tar.gz | awk '{print $1}')
          LINUX_ARM64_SHA=$(sha256sum artifacts/server-monitor-manager-linux-arm64/server-monitor-manager-linux-arm64.tar.gz | awk '{print $1}')
          MSIX_SHA=$(sha256sum artifacts/server-monitor-manager-win-x64/artifacts/windows-installer/ServerMonitorManager-win-x64.msix | awk '{print $1}')
          LINUX_X64_SBOM_SHA=$(sha256sum artifacts/server-monitor-manager-linux-x64/server-monitor-manager-linux-x64-sbom.json | awk '{print $1}')
          LINUX_ARM64_SBOM_SHA=$(sha256sum artifacts/server-monitor-manager-linux-arm64/server-monitor-manager-linux-arm64-sbom.json | awk '{print $1}')
          WIN_X64_SBOM_SHA=$(sha256sum artifacts/server-monitor-manager-win-x64/server-monitor-manager-win-x64-sbom.json | awk '{print $1}')
          
          jq -n \
            --arg schema "smm-manifest/v2" \
            --arg version "$VERSION" \
            --arg bootstrap_sha256 "$BOOTSTRAP_SHA" \
            --arg setup_sha256 "$SETUP_SHA" \
            --arg linux_x64_sha256 "$LINUX_X64_SHA" \
            --arg linux_arm64_sha256 "$LINUX_ARM64_SHA" \
            --arg msix_sha256 "$MSIX_SHA" \
            --arg linux_x64_sbom_sha256 "$LINUX_X64_SBOM_SHA" \
            --arg linux_arm64_sbom_sha256 "$LINUX_ARM64_SBOM_SHA" \
            --arg win_x64_sbom_sha256 "$WIN_X64_SBOM_SHA" \
            '{
              schema: $schema,
              version: $version,
              components: {
                control: $version,
                agent: $version,
                helper: $version,
                desktop: $version
              },
              protocols: {
                helper_protocol: "v1"
              },
              hashes: {
                "ochenstarik-server-monitor-manager.sh": $bootstrap_sha256,
                "smm-setup.sh": $setup_sha256,
                "server-monitor-manager-linux-x64.tar.gz": $linux_x64_sha256,
                "server-monitor-manager-linux-arm64.tar.gz": $linux_arm64_sha256,
                "ServerMonitorManager-win-x64.msix": $msix_sha256,
                "server-monitor-manager-linux-x64-sbom.json": $linux_x64_sbom_sha256,
                "server-monitor-manager-linux-arm64-sbom.json": $linux_arm64_sbom_sha256,
                "server-monitor-manager-win-x64-sbom.json": $win_x64_sbom_sha256
              }
            }' > server-monitor-manager-manifest.json

      - name: Sign Manifest
        shell: bash
        run: |
          cosign sign-blob --yes \
            --output-signature server-monitor-manager-manifest.sig \
            --output-certificate server-monitor-manager-manifest.pem \
            server-monitor-manager-manifest.json

      - name: Attach artifacts to GitHub Release
        if: startsWith(github.ref, 'refs/tags/')
        uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228  # v3.0.2
        with:
          prerelease: ${{ contains(github.ref_name, '-') }}
          files: |
            artifacts/server-monitor-manager-bootstrap/*
            artifacts/server-monitor-manager-linux-x64/*
            artifacts/server-monitor-manager-linux-arm64/*
            artifacts/server-monitor-manager-win-x64/server-monitor-manager-win-x64-sbom.json
            artifacts/server-monitor-manager-win-x64/artifacts/windows-installer/*
            server-monitor-manager-manifest.json
            server-monitor-manager-manifest.sig
            server-monitor-manager-manifest.pem
