2 Commits
Author SHA1 Message Date
BiasFandClaude Opus 5 32659ff0b9 Gitea: warum die APK abgelehnt wird, und zwei Auswege
Die Instanz gefragt statt geraten. Ihre Liste erlaubter Dateitypen für
Anhänge enthält .apk nicht - daran scheitert das Hochladen. An der Grösse
liegt es nicht, die Grenze steht bei 100 MiB und die APK bei 11.

Der Quelltext als zip und tar.gz ist etwas anderes: den hängt Gitea an jedes
Release automatisch, erzeugt aus dem Etikett. Das ist nichts Hochgeladenes
und lässt sich auch nicht abschalten.

RELEASE.md nennt jetzt beides und zwei Wege. Der richtige ist eine Zeile in
der app.ini, die .apk erlaubt - für Release-Anhänge sind je nach Fassung
[attachment] oder [repository.release] zuständig, deshalb beide.

Ohne Serverzugriff hilft die Paket-Registry: die nimmt beliebige Dateien
ohne Typenliste und ist auf dieser Instanz eingeschaltet. Dafür ein Skript,
das den Zugangsschlüssel aus einer Datei liest und nirgends ausgibt, und
das die üblichen Fehlschläge benennt statt nur einen Statuscode zu zeigen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 11:55:30 +02:00
BiasFandClaude Opus 5 3f37e6f047 Android: Release-Build signieren und veröffentlichen
Der Release-Build lieferte bisher app-release-unsigned.apk - und die
installiert Android nicht, auch nicht per Sideload. Ohne Signatur kann das
System nicht feststellen, ob ein Update vom selben Absender stammt wie die
Erstinstallation; das ist unabhängig davon, woher die Datei kommt.

Der Build signiert jetzt. Liegt Android/keystore.properties vor, mit dem
eigenen Schlüssel; sonst wird aus der Umgebung gelesen, damit ein Bauknecht
sie aus seinen Geheimnissen setzen kann, ohne dass ein Passwort im Verlauf
landet. Fehlt beides, wird mit dem Debug-Schlüssel signiert - der Build
sagt das dann deutlich, denn eine so signierte Release-APK sieht sonst aus
wie eine richtige, lässt sich aber vom nächsten Rechner aus nicht mehr
aktualisieren. Schlüssel und Passwörter sind ausgeschlossen.

Version und Versionszähler lassen sich beim Bauen setzen, damit sie aus
einem Etikett kommen können.

Dazu ein Ablauf für Gitea Actions: ein Etikett hochschieben baut die APK,
legt das Release an, falls es noch nicht besteht, und hängt sie an. Beides
muss gehen - sonst scheitert es je nachdem, ob man das Release vorher von
Hand angelegt hat.

Android/RELEASE.md erklärt beide Wege und sagt auch, warum die APK nicht
ins Repository gehört: Release-Anhänge liegen in Gitea neben dem
Repository. Eine 11-MB-Binärdatei je Version im Verlauf wäre unwiderruflich
- Git vergisst nichts, ein späteres Löschen bringt den Platz nicht zurück.

Geprüft: die signierte APK installiert sich im Emulator, meldet sich mit
der übergebenen Version und trägt keine Debug-Kennzeichnung mehr.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 11:49:07 +02:00
5 changed files with 359 additions and 2 deletions
+94
View File
@@ -0,0 +1,94 @@
# Baut die Android-APK und hängt sie an ein Gitea-Release.
#
# Ausgelöst durch ein Etikett der Form v1.2.3. Voraussetzungen und die
# Einrichtung der Geheimnisse stehen in Android/RELEASE.md.
name: APK veröffentlichen
on:
push:
tags:
- "v*"
jobs:
apk:
runs-on: ubuntu-latest
steps:
- name: Quelltext holen
uses: actions/checkout@v4
with:
# Der Versionszähler kommt aus der Anzahl der Commits; dafür wird
# der ganze Verlauf gebraucht, nicht nur der letzte Stand.
fetch-depth: 0
- name: Java einrichten
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
- name: Android SDK einrichten
uses: android-actions/setup-android@v3
- name: Signaturschlüssel bereitlegen
env:
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
run: |
# Ohne hinterlegten Schlüssel wird mit dem Debug-Schlüssel signiert.
# Der Build sagt das selbst deutlich; hier nur der Hinweis im Protokoll.
if [ -z "$KEYSTORE_BASE64" ]; then
echo "Kein Schlüssel hinterlegt es wird mit dem Debug-Schlüssel signiert."
exit 0
fi
echo "$KEYSTORE_BASE64" | base64 -d > "$RUNNER_TEMP/release.jks"
echo "KEYSTORE_FILE=$RUNNER_TEMP/release.jks" >> "$GITHUB_ENV"
- name: APK bauen
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
run: |
VERSION="${GITHUB_REF_NAME#v}"
# Die Anzahl der Commits steigt verlässlich und erfüllt damit, was
# Android vom Versionszähler verlangt: er darf nie kleiner werden.
CODE="$(git rev-list --count HEAD)"
echo "Version $VERSION (Zähler $CODE)"
cd Android
chmod +x gradlew
./gradlew :app:assembleRelease \
-PversionName="$VERSION" \
-PversionCode="$CODE" --no-daemon
mv app/build/outputs/apk/release/app-release.apk \
"../CamperMonitor-$VERSION.apk"
- name: An das Release hängen
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -eu
VERSION="${GITHUB_REF_NAME#v}"
API="$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY/releases"
AUTH="Authorization: token $TOKEN"
# Wurde das Release schon von Hand angelegt, wird es genommen;
# sonst hier erzeugt. Beides muss gehen, sonst scheitert der Ablauf
# je nachdem, in welcher Reihenfolge man vorgeht.
ID="$(curl -sf -H "$AUTH" "$API/tags/$GITHUB_REF_NAME" \
| sed -n 's/.*"id":\([0-9]*\).*/\1/p' | head -1 || true)"
if [ -z "$ID" ]; then
ID="$(curl -sf -X POST -H "$AUTH" -H "Content-Type: application/json" \
-d "{\"tag_name\":\"$GITHUB_REF_NAME\",\"name\":\"$GITHUB_REF_NAME\"}" \
"$API" | sed -n 's/.*"id":\([0-9]*\).*/\1/p' | head -1)"
fi
if [ -z "$ID" ]; then
echo "Release konnte weder gefunden noch angelegt werden." >&2
exit 1
fi
curl -sf -X POST -H "$AUTH" \
-F "attachment=@CamperMonitor-$VERSION.apk" \
"$API/$ID/assets?name=CamperMonitor-$VERSION.apk" > /dev/null
echo "CamperMonitor-$VERSION.apk hängt an Release $GITHUB_REF_NAME."
+5
View File
@@ -6,3 +6,8 @@ local.properties
.idea/
captures/
.cxx/
# Signaturschlüssel und Passwörter gehören nirgends ins Repository.
keystore.properties
*.jks
*.keystore
+155
View File
@@ -0,0 +1,155 @@
# Eine APK veröffentlichen
## Vorweg: die APK gehört nicht ins Repository
Ein Release-Anhang in Gitea liegt **neben** dem Repository, nicht darin. Gitea
legt ihn in seinem Anhang-Speicher ab; im Git-Verlauf taucht er nicht auf.
Das ist auch gut so. Eine APK ist eine 11-MB-Binärdatei, die sich bei jedem
Build komplett ändert. Läge sie im Repository, wüchse der Verlauf mit jeder
Version um 11 MB — und zwar **unwiderruflich**: Git vergisst nichts, ein
späteres Löschen bringt den Platz nicht zurück. Nach zehn Versionen wäre das
Auschecken des Projekts ein 110-MB-Download, obwohl der Quelltext selbst nur
wenige hundert Kilobyte hat.
Deshalb: bauen, an ein Release hängen, fertig. `.gitignore` sorgt dafür, dass
die gebauten Dateien gar nicht erst versehentlich hineinrutschen.
## Signieren
Android installiert **keine unsignierte APK** — auch nicht per Sideload. Das
hat nichts mit Googles Sperren zu tun, sondern damit, dass das System ohne
Signatur nicht feststellen kann, ob ein Update vom selben Absender stammt wie
die Erstinstallation.
Ohne eigenen Schlüssel signiert der Build mit dem Android-Debug-Schlüssel und
sagt das beim Bauen deutlich. Das funktioniert, hat aber einen Haken: Dieser
Schlüssel wird pro Rechner erzeugt. Baust du die nächste Version auf einem
anderen Rechner, hat sie eine andere Signatur, und Android verweigert das
Update — es bliebe nur Deinstallieren und neu einrichten.
### Einen eigenen Schlüssel anlegen
Einmalig. Das Passwort tippst du dabei selbst.
```bash
keytool -genkeypair -v \
-keystore ~/camper-monitor.jks \
-alias camper -keyalg RSA -keysize 4096 -validity 10000
```
Danach `Android/keystore.properties` anlegen (die Datei ist in `.gitignore`,
sie landet nicht im Repository):
```properties
storeFile=/Users/DEINNAME/camper-monitor.jks
storePassword=DEIN_PASSWORT
keyAlias=camper
keyPassword=DEIN_PASSWORT
```
> **Diesen Schlüssel aufbewahren.** Geht er verloren, lässt sich für alle, die
> die App installiert haben, nie wieder ein Update ausliefern. Eine Kopie an
> einen zweiten Ort, getrennt vom Rechner.
## Wenn Gitea die APK nicht annimmt
Zwei Dinge, die leicht zusammenfallen:
**Der Quelltext als `.zip` und `.tar.gz`** hängt an *jedem* Gitea-Release
automatisch dran. Gitea erzeugt beides aus dem Etikett; es ist nichts, was
versehentlich hochgeladen wurde, und es lässt sich auch nicht abschalten. Es
stört nur optisch und kostet keinen Speicher — die Archive werden erst beim
Anklicken erzeugt.
**Die APK abgelehnt** wird dagegen aus einem handfesten Grund: Gitea nimmt nur
Dateitypen an, die in seiner Liste stehen, und `.apk` steht dort nicht. Die
Liste dieser Instanz lässt sich abfragen:
```bash
curl -s https://git.home.fritob.de/api/v1/settings/attachment
```
Kommt dort eine `allowed_types`-Liste ohne `.apk` zurück, ist das die Ursache.
Die Grösse ist es nicht — die Grenze liegt bei 100 MiB, die APK bei 11.
### Der richtige Weg: `.apk` erlauben
In der `app.ini` des Servers (bei Docker meist
`/data/gitea/conf/app.ini`, sonst `/etc/gitea/app.ini`):
```ini
[attachment]
ALLOWED_TYPES = .avif,.cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.webp,.xls,.xlsx,.zip,.apk
[repository.release]
ALLOWED_TYPES = .apk,.zip
```
Beide Abschnitte, weil je nach Gitea-Fassung mal der eine, mal der andere für
Release-Anhänge zuständig ist. Danach Gitea neu starten.
Wer es sich einfach machen will, schreibt in beide Schlüssel `*/*` — dann ist
alles erlaubt. Auf einer Instanz im eigenen Netz ist das vertretbar; öffentlich
wäre es das nicht, weil dann auch beliebige ausführbare Dateien hochladbar sind.
### Ohne Serverzugriff: die Paket-Registry
Die nimmt beliebige Dateien an, ganz ohne Typenliste, und ist auf dieser
Instanz eingeschaltet. Der Preis: Die Datei landet unter *Packages* statt am
Release.
```bash
Android/upload-package.sh 1.0.0
```
Das Skript liest den Zugangsschlüssel aus `~/.gitea-token` und gibt ihn
nirgends aus. Ein Token mit dem Recht `write:package` genügt; anzulegen unter
*Einstellungen → Anwendungen → Zugriffstoken*.
## Von Hand veröffentlichen
```bash
source Android/env.sh
cd Android
./gradlew :app:assembleRelease -PversionName=1.0.0 -PversionCode=1
```
Die APK liegt dann unter
`Android/app/build/outputs/apk/release/app-release.apk`.
In Gitea: **Releases → New Release**, ein Etikett wie `v1.0.0` wählen, und die
APK unter *Attachments* hochladen. Fertig — genau das ist das „Wie sage ich
Gitea, dass die APK dort hinein soll".
## Von selbst veröffentlichen
`.gitea/workflows/release.yml` nimmt das ab: Ein Etikett hochschieben genügt.
```bash
git tag v1.0.0
git push origin v1.0.0
```
Der Ablauf baut die APK, legt das Release an, falls es noch nicht besteht, und
hängt die Datei an. Die Versionsnummer kommt aus dem Etikett, der
Versionszähler aus der Anzahl der Commits — so steigt er verlässlich.
### Voraussetzungen
1. **Ein Runner.** Actions brauchen in Gitea einen registrierten Runner mit dem
Etikett `ubuntu-latest`. Unter *Site Administration → Actions → Runners*
nachsehen. Ist keiner da, bleibt der Weg von Hand — der tut es genauso.
2. **Actions eingeschaltet**, im Repository unter *Settings → Advanced*.
3. **Der Schlüssel als Geheimnis**, falls mit eigenem Schlüssel signiert werden
soll. Unter *Settings → Actions → Secrets* anlegen:
| Name | Inhalt |
| ------------------ | ------------------------------------------ |
| `KEYSTORE_BASE64` | `base64 -i ~/camper-monitor.jks \| pbcopy` |
| `KEYSTORE_PASSWORD`| das Passwort des Schlüsselspeichers |
| `KEY_ALIAS` | `camper` |
| `KEY_PASSWORD` | das Passwort des Schlüssels |
Fehlen sie, baut der Ablauf trotzdem — dann eben mit dem Debug-Schlüssel,
und die APK heisst entsprechend.
+64 -2
View File
@@ -1,9 +1,32 @@
import java.util.Properties
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
}
/**
* Angaben zum Signaturschlüssel.
*
* Sie stehen in `Android/keystore.properties` - einer Datei, die bewusst nicht
* im Repository liegt. Fehlt sie, wird stattdessen aus der Umgebung gelesen;
* so kann ein Bauknecht sie aus seinen Geheimnissen setzen, ohne dass ein
* Passwort je im Verlauf landet.
*/
val signing = Properties().apply {
val file = rootProject.file("keystore.properties")
if (file.exists()) {
file.inputStream().use { load(it) }
} else {
System.getenv("KEYSTORE_FILE")?.let { setProperty("storeFile", it) }
System.getenv("KEYSTORE_PASSWORD")?.let { setProperty("storePassword", it) }
System.getenv("KEY_ALIAS")?.let { setProperty("keyAlias", it) }
System.getenv("KEY_PASSWORD")?.let { setProperty("keyPassword", it) }
}
}
val hasSigningKey = signing.getProperty("storeFile") != null
android {
namespace = "de.fritob.campermonitor"
compileSdk = 35
@@ -13,13 +36,35 @@ android {
// Android 8: älter lohnt nicht, dort fehlt zu viel an Bluetooth LE.
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "1.0"
versionCode = (findProperty("versionCode") as String?)?.toInt() ?: 1
// Beim Bauen aus einem Etikett heraus überschreibbar:
// ./gradlew :app:assembleRelease -PversionName=1.2.0
versionName = (findProperty("versionName") as String?) ?: "1.0"
}
signingConfigs {
if (hasSigningKey) {
create("release") {
storeFile = file(signing.getProperty("storeFile"))
storePassword = signing.getProperty("storePassword")
keyAlias = signing.getProperty("keyAlias")
keyPassword = signing.getProperty("keyPassword")
}
}
}
buildTypes {
release {
isMinifyEnabled = false
// Ohne eigenen Schlüssel wird mit dem Debug-Schlüssel signiert.
// Unsigniert wäre die APK schlicht nicht installierbar - Android
// weist sie ab, auch beim Sideloading. Der Hinweis unten sagt, was
// das für Folgen hat.
signingConfig = if (hasSigningKey) {
signingConfigs.getByName("release")
} else {
signingConfigs.getByName("debug")
}
}
}
@@ -59,3 +104,20 @@ dependencies {
// Die Ablage der Geräte und Profile nutzt org.json aus Android selbst -
// eine weitere Bibliothek lohnt für eine Handvoll Felder nicht.
}
// Sichtbar machen, womit signiert wurde - eine mit dem Debug-Schlüssel
// signierte Release-APK sieht sonst aus wie eine richtige.
tasks.matching { it.name == "assembleRelease" }.configureEach {
doLast {
if (!hasSigningKey) {
logger.lifecycle(
"\n ACHTUNG: mit dem Debug-Schlüssel signiert." +
"\n Installierbar, aber an diesen Rechner gebunden: auf einem anderen" +
"\n entsteht ein anderer Debug-Schlüssel, und ein Update liesse sich" +
"\n nicht mehr über die alte Installation legen." +
"\n Für einen eigenen Schlüssel: Android/keystore.properties anlegen." +
"\n Anleitung in Android/RELEASE.md.\n"
)
}
}
}
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Lädt die gebaute APK in Gitas Paket-Registry.
#
# Der Weg über die Registry umgeht die Typenliste für Release-Anhänge, die
# `.apk` nicht enthält. Siehe RELEASE.md.
#
# Android/upload-package.sh 1.0.0
set -euo pipefail
VERSION="${1:?Aufruf: upload-package.sh <version>, z. B. 1.0.0}"
SERVER="${GITEA_SERVER:-https://git.home.fritob.de}"
OWNER="${GITEA_OWNER:-fritob}"
TOKEN_FILE="${GITEA_TOKEN_FILE:-$HOME/.gitea-token}"
APK="$(dirname "$0")/app/build/outputs/apk/release/app-release.apk"
[ -f "$APK" ] || {
echo "Keine APK gefunden. Erst bauen:" >&2
echo " source Android/env.sh && cd Android && ./gradlew :app:assembleRelease -PversionName=$VERSION" >&2
exit 1
}
[ -f "$TOKEN_FILE" ] || {
echo "Kein Zugangsschlüssel in $TOKEN_FILE." >&2
echo "In Gitea unter Einstellungen -> Anwendungen einen mit 'write:package' anlegen," >&2
echo "dann: printf %s 'DEIN_TOKEN' > $TOKEN_FILE && chmod 600 $TOKEN_FILE" >&2
exit 1
}
NAME="CamperMonitor-$VERSION.apk"
URL="$SERVER/api/packages/$OWNER/generic/camper-monitor/$VERSION/$NAME"
# Der Schlüssel geht nur an curl und steht nirgends in der Ausgabe.
CODE="$(curl -s -o /dev/null -w '%{http_code}' \
-H "Authorization: token $(cat "$TOKEN_FILE")" \
--upload-file "$APK" "$URL")"
case "$CODE" in
201) echo "Hochgeladen: $URL" ;;
409) echo "Version $VERSION liegt dort schon. Erst löschen oder Version erhöhen." >&2; exit 1 ;;
401|403) echo "Abgelehnt (HTTP $CODE) - fehlt dem Token das Recht 'write:package'?" >&2; exit 1 ;;
*) echo "Fehlgeschlagen, HTTP $CODE" >&2; exit 1 ;;
esac