# 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 "" > ~/.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: In: Forgejo web UI → Settings → Applications 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/ ```