Project_SylDa/docs/superpowers/plans/2026-07-30-release-build.md
eddy 70f004c45b 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.
2026-07-30 19:54:13 +02:00

14 KiB

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:

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:

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:

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
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):

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 {}):

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
./gradlew --no-daemon dependencies > /dev/null 2>&1
echo $?

Expected: 0 (geen Gradle-fouten).

  • Step 3: Commit
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
git status android/keystore/ android/signing.properties

Expected: geen output (bestanden worden genegeerd).

  • Step 3: Commit
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

mkdir -p scripts
  • Step 2: Schrijf release.sh
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 -n scripts/release.sh
echo $?

Expected: 0.

  • Step 4: Commit
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

ls -la android/keystore/sylda-release.jks android/signing.properties
  • Step 2: Build release APK + AAB
./gradlew assembleRelease bundleRelease

Expected: BUILD SUCCESSFUL

  • Step 3: Verifieer ondertekening APK
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
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
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
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)

git status

Expected: nothing to commit, working tree clean

  • Step 2: Draai release script
./scripts/release.sh
  • Step 3: Verifieer release op Forgejo
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:

rm -rf release/