DramaticShapeVoxelMod archival mirror · read-only
.github/workflows/release.yml

release.yml

.github/workflows/release.yml7.0 KB1006446b36f84f95
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
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"