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:
1 parent
f5fd411592
commit
a64df88c77
7 files changed
+262
-9
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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`).
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user