name: Release

# Triggered when a vX.Y.Z tag is pushed. Builds the plugin against the
# current Dalamud staging branch, locates the latest.zip produced by
# DalamudPackager and attaches it to the matching Gitea Release.
#
# User-controlled inputs touched by this workflow:
#   - the tag name (filtered by on.tags = v*, validated again at runtime
#     against ^v\d+\.\d+\.\d+$ before being used in any string)
# All other values are either repo-controlled (paths under
# HellionChat/bin/Release derived from find / Get-ChildItem) or pinned
# URLs to goatcorp / gitea. Nothing from a webhook event payload (issue/PR
# titles, commit messages, etc.) flows into a run-step.
#
# Linux runner: gitea.com Cloud Actions only ships ubuntu-latest. The
# plugin csproj targets net10.0-windows, `dotnet build` cross-compiles on
# Linux when the Dalamud staging assemblies sit under $(HOME)/.xlcore/...

on:
  push:
    tags:
      - 'v*'
  # Manual recovery trigger. Use Gitea's "Run workflow" UI and select the
  # tag (e.g. v1.4.4) from the Ref dropdown - not main. The Validate tag
  # ref step below hard-fails if a non-tag ref is selected, because the
  # release-action reads GITHUB_REF directly and rejects anything that
  # does not start with refs/tags/.
  workflow_dispatch:

permissions:
  contents: write

jobs:
  release:
    name: Build and attach release ZIP
    runs-on: ubuntu-latest
    timeout-minutes: 20

    steps:
      # release-action@main reads GITHUB_REF directly (its action.yml
      # does not declare a tag_name input). Validate up-front so manual
      # dispatches from a branch ref fail loud here instead of burning
      # a full build before the final step errors out with "ref X is
      # not a tag".
      - name: Validate tag ref
        run: |
          if [[ "${GITHUB_REF}" != refs/tags/v* ]]; then
              echo "::error::Release workflow must run on a v*.X.Y tag ref, got ${GITHUB_REF}"
              echo "::error::Push a tag, or pick the tag (not main) in the workflow_dispatch Ref dropdown."
              exit 1
          fi

      - name: Checkout
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

      - name: Setup .NET 10
        uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5
        with:
          dotnet-version: 10.0.x

      - name: Download Dalamud staging
        run: |
          hooks="$HOME/.xlcore/dalamud/Hooks/dev"
          mkdir -p "$hooks"
          curl -fsSL https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -o dalamud.zip
          unzip -oq dalamud.zip -d "$hooks"

      - name: Build (Release)
        run: dotnet build HellionChat/HellionChat.csproj --configuration Release

      - name: Locate latest.zip
        id: locate
        run: |
          zip="$(find HellionChat/bin/Release -name latest.zip -print -quit)"
          if [ -z "$zip" ]; then
              echo "latest.zip not found under HellionChat/bin/Release" >&2
              exit 1
          fi
          echo "Found: $zip"
          echo "path=$zip" >> "$GITHUB_OUTPUT"

      # Build a release body from the matching changelog block in
      # HellionChat.yaml plus a static install / docs footer. Fails the
      # workflow if no block exists for the tagged version, which is the
      # automated counterpart to the "yaml + repo.json + release body
      # kept in sync" rule.
      #
      # GITHUB_REF_NAME is read via env: (not ${{ }} interpolation) so the
      # tag value is treated as a PowerShell variable, not as inline shell
      # text. The strict regex below rejects anything that is not a clean
      # semver tag before it is used to build a string.
      - name: Generate release body
        shell: pwsh
        env:
          # github.ref_name is the tag because Validate tag ref above
          # already enforced refs/tags/v*. Read via env: so the value
          # is a PowerShell variable, not inline shell text, and gets
          # re-validated against the semver regex below.
          TAG_NAME: ${{ github.ref_name }}
        run: |
          $tag = $env:TAG_NAME
          if ($tag -notmatch '^v\d+\.\d+\.\d+$') {
              throw "Refusing to generate release body for non-semver tag: $tag"
          }
          $version = $tag.Substring(1)

          $yamlPath = "HellionChat/HellionChat.yaml"
          $raw = Get-Content -Path $yamlPath -Raw

          $marker = "changelog: |-"
          $idx = $raw.IndexOf($marker)
          if ($idx -lt 0) { throw "changelog block not found in $yamlPath" }

          # changelog: is the last top-level key in the manifest, so
          # everything after the marker is the literal block. Strip the
          # 4-space yaml indent (prettier convention) from each line.
          $afterMarker = $raw.Substring($idx + $marker.Length)
          $changelogBody = (($afterMarker -split "`r?`n") | ForEach-Object {
              if ($_ -match '^    ') { $_.Substring(4) } else { $_ }
          }) -join "`n"

          # Subblock convention: "**vX.Y.Z — <subtitle> (<date>)**"
          # matches verify-changelog-sync.sh and slim-rule grep.
          $header = "**v$version "
          $start = $changelogBody.IndexOf($header)
          if ($start -lt 0) {
              throw "No changelog entry for version $version found in $yamlPath. Update the changelog block before tagging a release."
          }

          $rest = $changelogBody.Substring($start)
          $nextHdr = $rest.IndexOf("`n`n**v", 1)
          $trailer = $rest.IndexOf("`n`n---")

          if ($nextHdr -ge 0 -and ($trailer -lt 0 -or $nextHdr -lt $trailer)) {
              $currentBlock = $rest.Substring(0, $nextHdr).TrimEnd()
          } elseif ($trailer -ge 0) {
              $currentBlock = $rest.Substring(0, $trailer).TrimEnd()
          } else {
              $currentBlock = $rest.TrimEnd()
          }

          # Static install / docs / licence footer is maintained as a
          # separate file so the workflow YAML stays clean (no embedded
          # heredoc that would have to be indented under the run-block).
          $footerPath = ".github/release-footer.md"
          if (-not (Test-Path $footerPath)) {
              throw "Release footer template not found: $footerPath"
          }
          $footer = Get-Content -Path $footerPath -Raw

          $body = $currentBlock + "`n" + $footer
          $body | Out-File -FilePath release-body.md -Encoding utf8 -NoNewline

          Write-Host "Generated release body for $tag :"
          Write-Host "----------------------------------------"
          Write-Host $body
          Write-Host "----------------------------------------"

      # release-action@main only declares files/title/body/pre_release/
      # draft/api_key/insecure as inputs (see its action.yml). It silently
      # ignores anything else, including body_path and tag_name. The tag
      # itself comes from GITHUB_REF, the body must be passed inline via
      # body:, so we re-emit release-body.md as a step output first.
      - name: Expose release body for release-action
        id: body
        shell: bash
        run: |
          {
              echo 'content<<RELEASE_BODY_EOF'
              cat release-body.md
              echo 'RELEASE_BODY_EOF'
          } >> "$GITHUB_OUTPUT"

      # Gitea-native release action. Creates the release if the tag has no
      # release yet, or updates the existing one with latest.zip attached
      # and the generated body. The auto-injected GITHUB_TOKEN on Gitea
      # Actions has Gitea-API scope and is sufficient for release write.
      - name: Attach to Gitea release
        uses: https://gitea.com/actions/release-action@main
        with:
          files: ${{ steps.locate.outputs.path }}
          body: ${{ steps.body.outputs.content }}
          api_key: ${{ secrets.GITHUB_TOKEN }}
