docs: add release build spec and implementation plan

Also un-ignores docs/ directory so project documentation (specs, plans)
is tracked in git. Individual research files remain gitignored.
This commit is contained in:
eddy 2026-07-30 19:54:13 +02:00
commit 70f004c45b
11 changed files with 3096 additions and 2 deletions

View file

@ -0,0 +1,552 @@
# Release Build & Forgejo Publishing — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Signed APK + AAB release builds, published as Forgejo release with firmware attachments.
**Architecture:** A `signing.properties` file (gitignored) provides Gradle with keystore credentials. A `scripts/release.sh` script builds APK and AAB, collects firmware, tags git, and uploads everything to Forgejo via HTTP API.
**Tech Stack:** Gradle 8.14, AGP 8.5.2, keytool, curl, Forgejo API v1
---
### Pre-check: BUILD_NUMBER timestamp
`buildConfigField "String", "BUILD_NUMBER", ..."` staat in `defaultConfig` in `build.gradle`, maar `BUILD_NUMBER` wordt nergens in de Kotlin code gebruikt. Geen impact op release build reproduceerbaarheid. Het veld blijft bestaan (geen actie nodig).
---
### Task 1: Genereer keystore en sla op in Obsidian vault
**Files:**
- Create: `android/keystore/sylda-release.jks` (gitignored, later)
- Create: Obsidian vault notitie met credentials
- [ ] **Step 1: Genereer keystore in workspace**
Run vanaf de project root:
```bash
keytool -genkey -v \
-keystore android/keystore/sylda-release.jks \
-alias sylda \
-keyalg RSA \
-keysize 2048 \
-validity 10000 \
-storepass "SyldaRel34se2026!" \
-keypass "SyldaRel34se2026!" \
-dname "CN=Eddy Devink, OU=SylDa, O=SylDa, L=Unknown, ST=Unknown, C=NL"
```
Verify:
```bash
keytool -list -v -keystore android/keystore/sylda-release.jks -storepass "SyldaRel34se2026!" | head -10
```
Expected: Alias `sylda`, certificate details.
- [ ] **Step 2: Bewaar in Obsidian vault**
Maak notitie `Homelab/Project_SylDa/keystore.md` via `obsidian-vault` skill of directe CouchDB API met:
```
# SylDa Release Keystore
- Path workspace: android/keystore/sylda-release.jks
- Alias: sylda
- Key password: SyldaRel34se2026!
- Store password: SyldaRel34se2026!
- Validity: 10000 days (tot ~2053)
- Keyalg: RSA 2048
## Keytool commando (hergeneratie)
keytool -genkey -v -keystore android/keystore/sylda-release.jks -alias sylda -keyalg RSA -keysize 2048 -validity 10000 -storepass "SyldaRel34se2026!" -keypass "SyldaRel34se2026!" -dname "CN=Eddy Devink, OU=SylDa, O=SylDa, L=Unknown, ST=Unknown, C=NL"
```
- [ ] **Step 3: Commit (keystore wordt later gitignored in Task 4 — nu nog niet gecommit)**
Geen git actie nog. Task 4 voegt gitignore toe.
---
### Task 2: Maak signing.properties
**Files:**
- Create: `android/signing.properties` (gitignored in Task 4)
- [ ] **Step 1: Creëer signing.properties**
Run:
```bash
cat > android/signing.properties << 'EOF'
KEYSTORE_PATH=keystore/sylda-release.jks
KEYSTORE_PASSWORD=SyldaRel34se2026!
KEY_ALIAS=sylda
KEY_PASSWORD=SyldaRel34se2026!
EOF
```
- [ ] **Step 2: Verifieer bestand**
```bash
cat android/signing.properties
```
Expected: 4 properties zoals hierboven.
---
### Task 3: Update build.gradle met signing config
**Files:**
- Modify: `android/app/build.gradle` (hele bestand overschrijven)
- [ ] **Step 1: Voeg signing block toe**
Huidig bestand (`android/app/build.gradle`):
```groovy
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
namespace 'nl.eddydevink.sylda'
compileSdk 34
defaultConfig {
applicationId "nl.eddydevink.sylda"
minSdk 26
targetSdk 34
versionCode 1
versionName "1.0"
buildConfigField "String", "BUILD_NUMBER", "\"${new java.text.SimpleDateFormat("yyyyMMddHHmmss").format(new Date())}\""
}
buildFeatures {
buildConfig true
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'io.github.dkaukov:esp32-flash-lib:1.1.17'
implementation 'com.eclipsesource.minimal-json:minimal-json:0.9.5'
implementation 'org.slf4j:slf4j-nop:2.0.16'
implementation 'androidx.security:security-crypto:1.1.0-alpha06'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
}
```
Vervangen door (signing block toegevoegd na `buildTypes {}`):
```groovy
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
def signingPropsFile = rootProject.file('signing.properties')
def hasSigningConfig = signingPropsFile.exists()
android {
namespace 'nl.eddydevink.sylda'
compileSdk 34
if (hasSigningConfig) {
def props = new Properties()
props.load(new FileInputStream(signingPropsFile))
signingConfigs {
release {
storeFile rootProject.file(props['KEYSTORE_PATH'])
storePassword props['KEYSTORE_PASSWORD']
keyAlias props['KEY_ALIAS']
keyPassword props['KEY_PASSWORD']
}
}
}
defaultConfig {
applicationId "nl.eddydevink.sylda"
minSdk 26
targetSdk 34
versionCode 1
versionName "1.0"
buildConfigField "String", "BUILD_NUMBER", "\"${new java.text.SimpleDateFormat("yyyyMMddHHmmss").format(new Date())}\""
}
buildFeatures {
buildConfig true
}
buildTypes {
release {
if (hasSigningConfig) {
signingConfig signingConfigs.release
}
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'io.github.dkaukov:esp32-flash-lib:1.1.17'
implementation 'com.eclipsesource.minimal-json:minimal-json:0.9.5'
implementation 'org.slf4j:slf4j-nop:2.0.16'
implementation 'androidx.security:security-crypto:1.1.0-alpha06'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
}
```
- [ ] **Step 2: Sync Gradle**
```bash
./gradlew --no-daemon dependencies > /dev/null 2>&1
echo $?
```
Expected: `0` (geen Gradle-fouten).
- [ ] **Step 3: Commit**
```bash
git add android/app/build.gradle
git commit -m "feat: add release signing config via signing.properties"
```
---
### Task 4: Update .gitignore
**Files:**
- Modify: `.gitignore` (toevoegen van nieuwe ignore-regels)
- [ ] **Step 1: Voeg keystore en signing.properties toe aan .gitignore**
Toevoegen aan `.gitignore`, onder de Android/Gradle sectie:
```
# Android signing (contains secrets)
android/keystore/
android/signing.properties
```
- [ ] **Step 2: Verifieer dat keystore nu gitignored is**
```bash
git status android/keystore/ android/signing.properties
```
Expected: geen output (bestanden worden genegeerd).
- [ ] **Step 3: Commit**
```bash
git add .gitignore
git commit -m "chore: gitignore keystore and signing.properties"
```
---
### Task 5: Maak release script
**Files:**
- Create: `scripts/release.sh`
- [ ] **Step 1: Creëer scripts/ director**
```bash
mkdir -p scripts
```
- [ ] **Step 2: Schrijf release.sh**
```bash
cat > scripts/release.sh << 'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
log() { echo -e "${GREEN}[release]${NC} $1"; }
err() { echo -e "${RED}[release]${NC} $1" >&2; }
# ------------------------------------------------------------------ config
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
ANDROID_DIR="$PROJECT_DIR/android"
APP_BUILD_GRADLE="$ANDROID_DIR/app/build.gradle"
SIGNING_PROPS="$ANDROID_DIR/signing.properties"
FORGEJO_URL="https://git.eddydevink.nl/api/v1/repos/eddy/Project_SylDa"
FIRMWARE_DIR="$ANDROID_DIR/app/src/main/assets/firmware"
# lees versionName uit build.gradle
VERSION=$(grep -oP 'versionName\s+"\K[^"]+' "$APP_BUILD_GRADLE")
TAG="v$VERSION"
RELEASE_DIR="$PROJECT_DIR/release/$TAG"
log "Release v$VERSION (tag $TAG)"
# ------------------------------------------------------------------ checks
if [ ! -f "$SIGNING_PROPS" ]; then
err "signing.properties niet gevonden op $SIGNING_PROPS"
exit 1
fi
# ------------------------------------------------------------------ clean
log "Clean..."
cd "$ANDROID_DIR"
./gradlew clean
# ------------------------------------------------------------------ build
log "Build APK + AAB..."
./gradlew assembleRelease bundleRelease
# ------------------------------------------------------------------ verzamel artefacten
log "Verzamel artefacten in $RELEASE_DIR"
mkdir -p "$RELEASE_DIR"
cp "$ANDROID_DIR/app/build/outputs/apk/release/app-release.apk" "$RELEASE_DIR/"
cp "$ANDROID_DIR/app/build/outputs/bundle/release/app-release.aab" "$RELEASE_DIR/"
cp "$FIRMWARE_DIR/bootloader.bin" "$RELEASE_DIR/"
cp "$FIRMWARE_DIR/partitions.bin" "$RELEASE_DIR/"
cp "$FIRMWARE_DIR/boot_app0.bin" "$RELEASE_DIR/"
cp "$FIRMWARE_DIR/firmware.bin" "$RELEASE_DIR/"
# ------------------------------------------------------------------ checksums
log "Checksums..."
cd "$RELEASE_DIR"
sha256sum * > checksums.txt
cat checksums.txt
# ------------------------------------------------------------------ git tag
cd "$PROJECT_DIR"
if git rev-parse "$TAG" >/dev/null 2>&1; then
log "Tag $TAG bestaat al, skip git tag."
else
log "Git tag $TAG..."
git tag "$TAG"
git push origin "$TAG"
fi
# ------------------------------------------------------------------ Forgejo release
log "Forgejo release aanmaken..."
TOKEN_FILE="$HOME/.config/sylda/forgejo-token"
if [ ! -f "$TOKEN_FILE" ]; then
err "Forgejo token niet gevonden op $TOKEN_FILE"
err "Genereer een token in Forgejo: Settings > Applications > Generate Token (scope: write:repository)"
err "Sla op als: echo 'je-token' > $TOKEN_FILE"
exit 1
fi
TOKEN=$(cat "$TOKEN_FILE")
# maak release aan
RELEASE_RESP=$(curl -s -X POST "$FORGEJO_URL/releases" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"$TAG\", \"name\": \"$TAG\", \"body\": \"SylDa Android app + firmware\", \"draft\": false, \"prerelease\": false}")
RELEASE_ID=$(echo "$RELEASE_RESP" | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])' 2>/dev/null || true)
if [ -z "$RELEASE_ID" ]; then
err "Release aanmaken mislukt. Response:"
echo "$RELEASE_RESP"
exit 1
fi
log "Release ID: $RELEASE_ID"
# upload artefacten
for f in *; do
log " Upload $f..."
curl -s -X POST "$FORGEJO_URL/releases/$RELEASE_ID/assets?name=$f" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary "@$f" > /dev/null
done
log "Klaar! Release $TAG: $FORGEJO_URL/releases/tag/$TAG (vervang api/v1/repos door web URL)"
SCRIPT
chmod +x scripts/release.sh
```
- [ ] **Step 3: Verifieer script syntax**
```bash
bash -n scripts/release.sh
echo $?
```
Expected: `0`.
- [ ] **Step 4: Commit**
```bash
git add scripts/release.sh
git commit -m "feat: add release build script"
```
---
### Task 6: Droogtest build
**Files:**
- Geen wijzigingen — verifieer dat release build werkt
- [ ] **Step 1: Controleer dat keystore en signing.properties aanwezig zijn**
```bash
ls -la android/keystore/sylda-release.jks android/signing.properties
```
- [ ] **Step 2: Build release APK + AAB**
```bash
./gradlew assembleRelease bundleRelease
```
Expected: `BUILD SUCCESSFUL`
- [ ] **Step 3: Verifieer ondertekening APK**
```bash
jarsigner -verify -verbose -certs android/app/build/outputs/apk/release/app-release.apk 2>&1 | grep -E "CN=|jar verified"
```
Expected melding: `CN=Eddy Devink` en `jar verified`.
- [ ] **Step 4: Verifieer ondertekening AAB**
```bash
jarsigner -verify -verbose -certs android/app/build/outputs/bundle/release/app-release.aab 2>&1 | grep -E "CN=|jar verified"
```
Expected melding: `CN=Eddy Devink` en `jar verified`.
---
### Task 7: Maak Forgejo access token aan
**Files:**
- Create: `~/.config/sylda/forgejo-token` (lokaal configuratiebestand, niet in repo)
- [ ] **Step 1: Maak token in Forgejo web UI**
Navigeer in browser naar: `https://git.eddydevink.nl/user/settings/applications`
- Klik "Generate Token"
- Token name: `syl-release`
- Scope: vink `write:repository` aan
- Klik "Generate"
Kopieer de gegenereerde token.
- [ ] **Step 2: Sla token op**
```bash
mkdir -p ~/.config/sylda
echo "<gekopieerde-token>" > ~/.config/sylda/forgejo-token
chmod 600 ~/.config/sylda/forgejo-token
```
- [ ] **Step 3: Sla token op in Obsidian vault**
Maak notitie `Homelab/Project_SylDa/forgejo-token.md` met:
```
# Forgejo API Token — syl-release
Token voor release uploads. Scope: write:repository.
Gegenereerd op: <datum>
In: Forgejo web UI → Settings → Applications
Token: <token>
Bestand: ~/.config/sylda/forgejo-token (chmod 600)
```
- [ ] **Step 4: Verifieer token werkt**
```bash
ssh aiaiaiaiagent@nextcloud 'curl -s http://localhost:3000/api/v1/user -H "Authorization: token '"$(cat ~/.config/sylda/forgejo-token)"'"' | python3 -c 'import sys,json; d=json.load(sys.stdin); print(d.get("login","FAIL"))'
```
Expected: `eddy` (de gebruikersnaam in Forgejo).
---
### Task 8: Draai volledige release
**Files:**
- Geen wijzigingen — voert het script uit
- [ ] **Step 1: Check git status (schoon)**
```bash
git status
```
Expected: `nothing to commit, working tree clean`
- [ ] **Step 2: Draai release script**
```bash
./scripts/release.sh
```
- [ ] **Step 3: Verifieer release op Forgejo**
```bash
ssh aiaiaiaiagent@nextcloud 'curl -s http://localhost:3000/api/v1/repos/eddy/Project_SylDa/releases/tags/v1.0' | python3 -c 'import sys,json; r=json.load(sys.stdin); print(f"Tag: {r[\"tag_name\"]}, Assets: {len(r[\"assets\"])}"); [print(f" - {a[\"name\"]} ({a[\"size\"]} bytes)") for a in r["assets"]]'
```
Expected: Tag `v1.0`, 6 assets (APK, AAB, 4 firmware bins, checksums.txt).
- [ ] **Step 4: Open browser ter verificatie**
Open `https://git.eddydevink.nl/eddy/Project_SylDa/releases` en controleer dat release `v1.0` zichtbaar is met alle attachments.
---
### Post-release: Cleanup
Release artefacten in `release/v1.0/` worden niet gecommit (`.gitignore` sluit `*.apk` en `*.aab` al uit). Na release draaien kan deze directory verwijderd worden:
```bash
rm -rf release/
```