release automation: gitea actions build every push and publish tagged releases (version from the tag, packages, sha256sums, changelog section as text), simple 5-step install guide in the readme

This commit is contained in:
MineTec committed 2026-11-07 16:47:42 +01:00
1 parent f5fd411592
commit a64df88c77
7 files changed
+262 -9

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
# Builds and tests every push and pull request (tests that need the game CD skip themselves).
name: build
on:
push:
branches: ['**']
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: |
25
21
- name: Build and test
run: |
./gradlew --no-daemon \
-Porg.gradle.java.installations.fromEnv=JAVA_HOME_25_X64 \
-Porg.gradle.java.installations.auto-download=false \
build
+61
View File
@@ -0,0 +1,61 @@
# Builds and publishes a release when a version tag is pushed:
# git tag v0.2.1 && git push origin v0.2.1
# The release gets the four packages (full/launcher x Windows/Linux), SHA256SUMS.txt and the
# matching CHANGELOG.md section as its text. Tests that need the game CD skip themselves here.
#
# Needs: Gitea Actions enabled and a runner with the label "ubuntu-latest" (act_runner).
# The built-in token is used; if it may not create releases on your instance, add a repository
# secret RELEASE_TOKEN (a token with write access to the repository).
name: release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: JDK 25 (toolchain) and 21 (runs Gradle)
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: |
25
21
- name: Version from the tag
run: |
V="${GITHUB_REF_NAME#v}"
echo "VERSION=$V" >> "$GITHUB_ENV"
python3 tools/release/changelog_section.py "$V" > release-notes.md
echo "Release $V"; head -5 release-notes.md
- name: Build, test and package
run: |
./gradlew --no-daemon \
-Porg.gradle.java.installations.fromEnv=JAVA_HOME_25_X64 \
-Porg.gradle.java.installations.auto-download=false \
-PreleaseVersion="$VERSION" \
build packageAll
- name: Checksums
run: |
cd build/dist
ls -la
sha256sum LlamaMod-"$VERSION"-*.zip LlamaMod-"$VERSION"-*.tar.gz > SHA256SUMS.txt
cat SHA256SUMS.txt
- name: Publish
env:
GITEA_URL: ${{ github.server_url }}
GITEA_REPO: ${{ github.repository }}
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN || github.token }}
run: |
python3 tools/release/publish_gitea.py \
--tag "$GITHUB_REF_NAME" --title "LlamaMod $VERSION" --notes release-notes.md \
build/dist/LlamaMod-"$VERSION"-*.zip build/dist/LlamaMod-"$VERSION"-*.tar.gz \
build/dist/SHA256SUMS.txt
+27 -8
View File
@@ -46,16 +46,35 @@ More pictures:
## Get it
You need **your own copy of the game** (CD or ISO image). Development used the German CD; other
language versions should work but are untested.
You need **your own copy of the game** (the CD or an ISO image of it). Nothing else – LlamaMod
brings everything it needs (also Java). Development used the German CD; other language versions
should work but are untested.
| System | Package | Start |
|---|---|---|
| Windows | `LlamaMod-<version>-full-windows-x64.zip` | unzip, start `LlamaMod.exe` (no installation, no admin rights) |
| Linux | `LlamaMod-<version>-full-linux-x64.tar.gz` | unpack, start `./LlamaMod`; install **Wine** to run the game |
### Install in 5 steps
Both bring their own Java runtime; the editor needs OpenGL 3.3. A *launcher* edition without the
editor is also built. What changed: [CHANGELOG.md](CHANGELOG.md).
1. **Download** from the **Releases** page of this repository (top of the page):
* Windows: `LlamaMod-<version>-full-windows-x64.zip`
* Linux: `LlamaMod-<version>-full-linux-x64.tar.gz`
(*full* = with the level editor. The *launcher* packages are the same without the editor.)
2. **Unpack** it into any folder you like (for example *Documents* or your home folder).
There is no installer and you need no admin rights.
3. **Start** LlamaMod:
* Windows: double-click **`LlamaMod.exe`**. If Windows says *"Windows protected your PC"*,
click **More info → Run anyway** (the program is not signed, that's all).
* Linux: install Wine once (`sudo apt install wine` on Ubuntu/Mint/Debian), then double-click
**`LlamaMod`** or run `./LlamaMod`. `./install-desktop-entry.sh` adds it to your app menu.
4. **Set up the game** – on the first start a short wizard asks for your game: put the CD in or
pick the ISO file, click **Set up** and wait about a minute. Your CD/ISO is only read, never
changed.
5. **Play** – on the **Mods** tab tick the mods you want, click **Apply**, then **Play**.
*Restore original* switches all mods off again.
New levels (Test World, Alligator Arena, Llama Coaster, your own) are in the hidden world
**TestLevel**: open the **Save games** tab once and click **Unlock TestLevel**, then pick it on the
world map. If your antivirus complains about `groove.exe`: mods that change the game program are
marked *modifies groove.exe*; LlamaMod tells you what to do. More help:
[Troubleshooting](docs/manual/20-troubleshooting.md). What changed: [CHANGELOG.md](CHANGELOG.md).
### Build from source
+2 -1
View File
@@ -4,7 +4,8 @@ plugins {
}
group = 'com.eliasmueller.llamamod'
version = '0.2.0'
// Release builds pass -PreleaseVersion=<tag without v> (see .gitea/workflows/release.yml).
version = findProperty('releaseVersion') ?: '0.2.1'
repositories {
mavenCentral()
+22
View File
@@ -84,3 +84,25 @@ Maintenance notes: [notes/editor-manual.md](../notes/editor-manual.md).
[`notes/`](../notes/README.md) is the project's lab notebook: one file per format, editor area and
mod, with addresses in `groove.exe`, confidence levels, what was verified in the game and known
limits. Process material (agent briefs, review reports) is in [`notes/dev/`](../notes/dev/).
## Releasing
Releases are built by Gitea Actions ([.gitea/workflows/release.yml](../.gitea/workflows/release.yml)):
1. Add a `## <version> – …` section at the top of [CHANGELOG.md](../CHANGELOG.md) (it becomes the
release text; the workflow fails if it is missing).
2. `git tag v<version> && git push origin v<version>`
The workflow builds and tests (tests that need the game CD skip themselves on the runner), runs
`packageAll` with `-PreleaseVersion=<version>` (the tag decides the version; without it
`build.gradle`'s default is used), writes `SHA256SUMS.txt` and creates the release with the four
packages via [tools/release/publish_gitea.py](../tools/release/publish_gitea.py) (standard library
only; re-running replaces files of the same name). [.gitea/workflows/build.yml](../.gitea/workflows/build.yml)
builds every push.
Requirements on the Gitea side: Actions enabled (`[actions] ENABLED = true`), an `act_runner` with
the label `ubuntu-latest` (an image with `python3`, `curl`/network access – the build downloads the
Temurin JDK/jmods and launch4j), and an attachment size limit above ~60 MB (`[attachment] MAX_SIZE`
in `app.ini`). If the built-in token may not create releases, add a repository secret
`RELEASE_TOKEN` with write access. Locally the same packages come from
`./gradlew -PreleaseVersion=<version> packageAll` (output in `build/dist`).
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
"""Prints the CHANGELOG.md section of one version (used as the release text).
changelog_section.py 0.2.1 [CHANGELOG.md]
A section starts at a "## <version>" heading (anything may follow the version, e.g. " – changes
since 0.1.0") and ends before the next "## " heading. Exit code 1 if the version has no section.
"""
import re
import sys
def section(text, version):
out, inside = [], False
for line in text.splitlines():
if line.startswith("## "):
if inside:
break
inside = re.match(r"## v?" + re.escape(version) + r"(\s|$)", line) is not None
continue
if inside:
out.append(line)
return "\n".join(out).strip() if inside or out else None
def main():
if len(sys.argv) < 2:
sys.exit(__doc__)
path = sys.argv[2] if len(sys.argv) > 2 else "CHANGELOG.md"
with open(path, encoding="utf-8") as f:
s = section(f.read(), sys.argv[1])
if not s:
sys.exit("no section for version %s in %s" % (sys.argv[1], path))
print(s)
if __name__ == "__main__":
main()
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""Creates (or reuses) a Gitea release for a tag and uploads files to it.
publish_gitea.py --tag v0.2.1 --title "LlamaMod 0.2.1" --notes notes.md FILE...
Environment: GITEA_URL (e.g. https://git.example.com), GITEA_REPO (owner/name), GITEA_TOKEN
(a token with write access to the repository). In Gitea Actions these come from the workflow.
Files that already exist on the release with the same name are replaced, so a re-run is safe.
Only the standard library is used.
"""
import argparse
import json
import mimetypes
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
import uuid
def api(method, url, token, data=None, headers=None):
h = {"Authorization": "token " + token, "Accept": "application/json"}
h.update(headers or {})
req = urllib.request.Request(url, data=data, method=method, headers=h)
try:
with urllib.request.urlopen(req) as r:
body = r.read()
return r.status, json.loads(body) if body else None
except urllib.error.HTTPError as e:
return e.code, e.read().decode("utf-8", "replace")
def multipart(path):
boundary = uuid.uuid4().hex
name = os.path.basename(path)
ctype = mimetypes.guess_type(name)[0] or "application/octet-stream"
with open(path, "rb") as f:
content = f.read()
head = ('--%s\r\nContent-Disposition: form-data; name="attachment"; filename="%s"\r\n'
"Content-Type: %s\r\n\r\n" % (boundary, name, ctype)).encode()
tail = ("\r\n--%s--\r\n" % boundary).encode()
return head + content + tail, "multipart/form-data; boundary=" + boundary
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--tag", required=True)
ap.add_argument("--title", required=True)
ap.add_argument("--notes", help="file with the release text (Markdown)")
ap.add_argument("--prerelease", action="store_true")
ap.add_argument("files", nargs="+")
a = ap.parse_args()
base, repo, token = (os.environ.get(k, "") for k in ("GITEA_URL", "GITEA_REPO", "GITEA_TOKEN"))
if not (base and repo and token):
sys.exit("set GITEA_URL, GITEA_REPO and GITEA_TOKEN")
root = "%s/api/v1/repos/%s" % (base.rstrip("/"), repo)
body = open(a.notes, encoding="utf-8").read() if a.notes else ""
st, rel = api("GET", "%s/releases/tags/%s" % (root, urllib.parse.quote(a.tag)), token)
if st == 404:
payload = json.dumps({"tag_name": a.tag, "name": a.title, "body": body,
"draft": False, "prerelease": a.prerelease}).encode()
st, rel = api("POST", root + "/releases", token, payload, {"Content-Type": "application/json"})
if st not in (200, 201):
sys.exit("creating the release failed (%s): %s" % (st, rel))
print("created release", rel["id"], a.tag)
elif st == 200:
print("reusing release", rel["id"], a.tag)
else:
sys.exit("reading the release failed (%s): %s" % (st, rel))
existing = {x["name"]: x["id"] for x in rel.get("assets") or []}
for path in a.files:
name = os.path.basename(path)
if name in existing:
api("DELETE", "%s/releases/%s/assets/%s" % (root, rel["id"], existing[name]), token)
data, ctype = multipart(path)
st, res = api("POST", "%s/releases/%s/assets?name=%s" % (root, rel["id"], urllib.parse.quote(name)),
token, data, {"Content-Type": ctype})
if st not in (200, 201):
sys.exit("uploading %s failed (%s): %s" % (name, st, res))
print("uploaded", name, "(%.1f MB)" % (len(data) / 1e6))
if __name__ == "__main__":
main()