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:
parent
ff3e92b8dd
commit
70f004c45b
11 changed files with 3096 additions and 2 deletions
552
docs/superpowers/plans/2026-07-30-release-build.md
Normal file
552
docs/superpowers/plans/2026-07-30-release-build.md
Normal 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/
|
||||
```
|
||||
Loading…
Reference in a new issue