name: Release

# Packs the mod into an installable .zip and publishes it as a GitHub Release,
# once per push to main.
#
# Archive layout: every mod file at the archive root, manifest.json included.
# That is one of the two shapes the game accepts on MODS > Import mod .zip
# (src/mods/LauncherMods.lua locateRoot: manifest at the root, or inside a
# single top-level folder). Nothing else is added, so the archive stays
# installable by hand too.
#
# Versioning, first rule that applies wins:
#   1. the "version" input of a manual run,
#   2. "[release X.Y.Z]" anywhere in the commit message,
#   3. manifest.json's own version, when it is ahead of every existing tag,
#      so bumping the manifest is the normal way to cut a release,
#   4. otherwise the newest vX.Y.Z tag with its patch incremented
#      (0.2.99 rolls over to 0.3.0).
# Whichever wins is written into the manifest.json inside the archive, so a
# shipped mod never reports a different version than the release it came from.
#
# Generated by: python3 tools/modkit.py add-release-workflow <mod-id>
# MOD_ID below is stamped to this mod's id when the file is copied.

on:
  push:
    branches: [master]
    paths-ignore:
      - '.github/**'
      - '**.md'
  workflow_dispatch:
    inputs:
      version:
        description: "Exact version to release (e.g. 0.3.0). Leave blank to auto-resolve."
        required: false
        default: ""

permissions:
  contents: write

concurrency:
  group: release
  cancel-in-progress: false

jobs:
  release:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Determine version
        id: ver
        env:
          DISPATCH_VERSION: ${{ github.event.inputs.version }}
        run: |
          set -euo pipefail
          python3 - <<'PY' >> "$GITHUB_OUTPUT"
          import json, os, re, subprocess, sys

          SEMVER = re.compile(r"^(\d+)\.(\d+)\.(\d+)$")

          def sh(*args):
              return subprocess.run(args, capture_output=True, text=True).stdout.strip()

          def parse(text):
              m = SEMVER.match(text)
              return tuple(int(p) for p in m.groups()) if m else None

          def die(msg):
              print(f"::error::{msg}", file=sys.stderr)
              raise SystemExit(1)

          with open("manifest.json", encoding="utf-8") as fh:
              manifest_version = str(json.load(fh).get("version", ""))

          released = sorted(
              v for v in (parse(tag[1:]) for tag in sh("git", "tag", "-l", "v*").splitlines()) if v
          )
          latest = released[-1] if released else None

          override = os.environ.get("DISPATCH_VERSION", "").strip()
          if not override:
              found = re.search(r"\[release\s+(\d+\.\d+\.\d+)\]", sh("git", "log", "-1", "--pretty=%B"))
              override = found.group(1) if found else ""

          manifest_ver = parse(manifest_version)
          if override:
              version = parse(override) or die(f"invalid version override {override!r} (expected X.Y.Z)")
              source = "the override"
          elif manifest_ver and (latest is None or manifest_ver > latest):
              version = manifest_ver
              source = "manifest.json"
          elif latest:
              major, minor, patch = latest
              patch += 1
              if patch > 99:
                  minor, patch = minor + 1, 0
              version = (major, minor, patch)
              source = "a patch bump on v%d.%d.%d" % latest
          else:
              die(f"manifest.json version {manifest_version!r} is not X.Y.Z "
                  "and there is no vX.Y.Z tag to count from")

          text = "%d.%d.%d" % version
          print(f"Releasing {text}, from {source}.", file=sys.stderr)
          print(f"version={text}")
          print(f"tag=v{text}")
          PY

      - name: Refuse to clobber an existing release
        env:
          GH_TOKEN: ${{ github.token }}
          TAG: ${{ steps.ver.outputs.tag }}
        run: |
          set -euo pipefail
          if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
            echo "::error::Tag $TAG already exists. Pick a different version."
            exit 1
          fi
          if gh release view "$TAG" >/dev/null 2>&1; then
            echo "::error::Release $TAG already exists. Pick a different version."
            exit 1
          fi

      - name: Build the mod .zip
        env:
          VERSION: ${{ steps.ver.outputs.version }}
          MOD_ID: "DRAMATIC_SHAPE"
        run: |
          set -euo pipefail
          staging="$RUNNER_TEMP/pkg"
          out="$GITHUB_WORKSPACE/dist"
          rm -rf "$staging" "$out"
          mkdir -p "$staging" "$out"

          git archive HEAD | tar -x -C "$staging"

          rm -rf "$staging/.github" "$staging/.gitattributes" \
                 "$staging/.gitignore" "$staging/.luarc.json"

          python3 - "$staging/manifest.json" "$VERSION" <<'PY'
          import json, sys
          path, version = sys.argv[1], sys.argv[2]
          with open(path, encoding="utf-8") as fh:
              manifest = json.load(fh)
          manifest["version"] = version
          with open(path, "w", encoding="utf-8") as fh:
              json.dump(manifest, fh, indent=2, ensure_ascii=False)
              fh.write("\n")
          PY

          zip_path="$out/${MOD_ID}-${VERSION}.zip"
          (cd "$staging" && zip -qr "$zip_path" .)
          unzip -l "$zip_path"

          unzip -p "$zip_path" manifest.json > "$RUNNER_TEMP/packed-manifest.json"
          python3 - "$RUNNER_TEMP/packed-manifest.json" "$VERSION" <<'PY'
          import json, sys
          path, expected = sys.argv[1], sys.argv[2]
          with open(path, encoding="utf-8") as fh:
              version = json.load(fh)["version"]
          if version != expected:
              raise SystemExit(f"::error::packed manifest says {version}, expected {expected}")
          print(f"manifest.json is at the archive root and reports {version}")
          PY

          (cd "$out" && sha256sum "${MOD_ID}"-*.zip > sha256sums.txt)
          cat "$out/sha256sums.txt"

      - name: Publish GitHub Release
        env:
          GH_TOKEN: ${{ github.token }}
          VERSION: ${{ steps.ver.outputs.version }}
          TAG: ${{ steps.ver.outputs.tag }}
          MOD_ID: "DRAMATIC_SHAPE"
        run: |
          set -euo pipefail

          prev="$(git tag -l 'v*' --sort=-v:refname | grep -v "^${TAG}$" | head -1 || true)"
          range="${prev:+${prev}..}$GITHUB_SHA"
          changes="$(git log --no-merges --pretty='- %s' "$range" | head -50 || true)"

          notes=$'Download the .zip and install it from the game: MODS > Import mod .zip.'
          if [ -n "$changes" ]; then
            notes+=$'\n\n## Changes\n\n'"$changes"
          fi
          printf 'Release notes:\n%s\n' "$notes"

          gh release create "$TAG" \
            --target "$GITHUB_SHA" \
            --title "$VERSION" \
            --notes "$notes" \
            "dist/${MOD_ID}-${VERSION}.zip" \
            "dist/sha256sums.txt"

          echo "Published release $TAG"
