Files
sharedinbox/Taskfile.yml
T
3e2da2bdf8 feat: use icon.svg as app icon for Android and Linux (#459)
Closes #451

## What changed

Replaces the default Flutter blue logo with the project's rainbow-rings `icon.svg` on all supported platforms.

**Android** — all five mipmap densities regenerated (`mdpi` 48px through `xxxhdpi` 192px).

**Linux** — `linux/sharedinbox.png` (512×512) added, installed next to the binary via `CMakeLists.txt`, and set as the GTK window icon via `gtk_window_set_icon_from_file` in `my_application.cc`.

**Tooling** — `icon.png` (1024×1024 source raster) committed; `flutter_launcher_icons` added as dev dep with a `flutter_icons` config block; `task generate-icons` added to `Taskfile.yml` for future regeneration; `librsvg` added to `flake.nix` so `rsvg-convert` is available inside `nix develop`.

## How verified

Icons were generated with Inkscape from `icon.svg` and visually confirmed (rainbow-rings design appears correctly at all sizes). The `playstore/icon.png` was already correct and unchanged.

Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de>
Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/459
2026-06-06 05:32:29 +02:00

730 lines
28 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
version: "3"
silent: true
env:
DAGGER_NO_NAG: "1"
tasks:
default:
desc: Run all checks (analyze + unit tests + widget tests + integration, in parallel)
deps: [check]
_nix-check:
internal: true
run: once
cmds:
- cmd: |
if ! nix show-config experimental-features 2>/dev/null | grep -q "nix-command" || ! nix show-config experimental-features 2>/dev/null | grep -q "flakes"; then
echo "CRITICAL: Nix experimental features 'nix-command' and 'flakes' must be enabled."
echo "Attempting to fix by updating ~/.config/nix/nix.conf ..."
mkdir -p ~/.config/nix
CONF=~/.config/nix/nix.conf
if [ ! -f "$CONF" ]; then
echo "experimental-features = nix-command flakes" > "$CONF"
elif ! grep -q "experimental-features" "$CONF"; then
echo "experimental-features = nix-command flakes" >> "$CONF"
elif ! grep -E "experimental-features.*nix-command" "$CONF" || ! grep -E "experimental-features.*flakes" "$CONF"; then
echo "Your $CONF already has experimental-features but is missing 'nix-command' or 'flakes'."
echo "Please add them manually to the 'experimental-features' line."
exit 1
fi
echo "Local configuration updated. Please re-run your command."
exit 1
fi
_preflight:
internal: true
run: once
deps: [_nix-check]
preconditions:
- sh: '[ "$(id -u)" != "0" ]'
msg: "Do not run as root. Use the dedicated dev user (see DEVELOPMENT.md)."
- sh: test -n "${IN_NIX_SHELL}"
msg: "Not in nix dev shell. Run: nix develop"
cmds:
- scripts/silent_on_success.sh pre-commit install
_flutter-check:
internal: true
run: once
deps: [_preflight]
cmds:
- cmd: scripts/silent_on_success.sh fvm install --skip-pub-get
- cmd: scripts/silent_on_success.sh fvm use --skip-pub-get
setup:
desc: Fully set up the dev environment (FVM, pub get, codegen, hooks)
deps: [_preflight, _codegen, generate-changelog]
cmds:
- echo "Setup complete."
generate-icons:
desc: Rasterise icon.svg → icon.png and regenerate all platform launcher icons
deps: [_pub-get]
cmds:
- rsvg-convert -w 1024 -h 1024 icon.svg -o icon.png
- rsvg-convert -w 512 -h 512 icon.svg -o playstore/icon.png
- fvm flutter pub run flutter_launcher_icons
generate-changelog:
desc: Generate assets/changelog.txt from git history
cmds:
- mkdir -p assets
- 'git log -n 50 --pretty=format:"* %ad [%h](https://codeberg.org/guettli/sharedinbox/commit/%H): %s" --date=short > assets/changelog.txt'
_pub-get:
internal: true
run: once
deps: [_flutter-check]
cmds:
- scripts/silent_on_success.sh fvm flutter pub get --suppress-analytics
_linux-deps-check:
internal: true
run: once
preconditions:
- sh: command -v clang >/dev/null 2>&1
msg: "Linux desktop deps missing. Run: sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev libstdc++-12-dev"
- sh: pkg-config --exists gtk+-3.0 2>/dev/null
msg: "Linux desktop deps missing. Run: sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev libstdc++-12-dev"
install-hooks:
desc: Install pre-commit hooks (requires pre-commit; see .pre-commit-config.yaml)
cmds:
- pre-commit install
_codegen:
internal: true
run: once
deps: [_pub-get]
sources:
- lib/**/*.dart
- pubspec.yaml
generates:
- lib/**/*.g.dart
cmds:
- scripts/silent_on_success.sh fvm flutter pub run build_runner build --delete-conflicting-outputs
codegen:
desc: Generate Drift DB code via Dagger (exports generated files back to host)
cmds:
- dagger call --progress=plain -q -m ci --source=. codegen -o .
analyze:
desc: Static analysis via Dagger (dart analyze --fatal-infos)
cmds:
- dagger call --progress=plain -q -m ci --source=. analyze
format:
desc: Format all Dart source files via Dagger (writes back to host)
cmds:
- dagger call --progress=plain -q -m ci --source=. format-write -o .
check-mocks:
desc: Fail if any *.mocks.dart file is out of date (re-runs build_runner)
deps: [_preflight, _pub-get]
sources:
- lib/**/*.dart
- test/**/*.dart
- pubspec.yaml
cmds:
- scripts/check_mocks_fresh.sh
analyze-fix:
desc: Auto-fix lint issues via Dagger (dart fix --apply, writes back to host)
cmds:
- dagger call --progress=plain -q -m ci --source=. analyze-fix -o .
test:
desc: Unit tests + coverage gate (fails if any non-excluded lib/ file is missing)
deps: [_preflight, _codegen]
sources:
- lib/**/*.dart
- test/unit/**/*.dart
generates:
- coverage/lcov.info
cmds:
- scripts/run_unit_tests.sh
test-widget:
desc: Widget tests — headless, no display or network required
deps: [_preflight, _codegen]
sources:
- lib/**/*.dart
- test/widget/**/*.dart
cmds:
- scripts/run_widget_tests.sh
test-flutter:
desc: Full Flutter test suite (unit + widget + integration)
deps: [_preflight]
sources:
- lib/**/*.dart
- test/**/*.dart
- integration_test/**/*.dart
cmds:
- fvm flutter test
test-backend:
desc: Backend tests against a local Stalwart mail server (via Dagger)
cmds:
- timeout --kill-after=10 600 dagger call --progress=plain -q -m ci --source=. test-backend
integration-ui:
desc: UI E2E tests on Linux via Xvfb — headless, no emulator needed (via Dagger)
cmds:
- timeout --kill-after=10 600 dagger call --progress=plain -q -m ci --source=. test-integration
sync-reliability:
desc: Run sync reliability runner (via Dagger)
cmds:
- timeout --kill-after=10 600 dagger call --progress=plain -q -m ci --source=. test-sync-reliability
test-android-firebase:
desc: Build Android debug APKs and run instrumented tests on Firebase Test Lab (via Dagger)
preconditions:
- sh: test -n "$FIREBASE_TEST_LAB_SERVICE_ACCOUNT_KEY"
msg: "FIREBASE_TEST_LAB_SERVICE_ACCOUNT_KEY is not set"
- sh: test -n "$FIREBASE_PROJECT_ID"
msg: "FIREBASE_PROJECT_ID is not set"
cmds:
- scripts/run_firebase_test.sh
ci-graph:
desc: Print a Mermaid diagram of the CI pipeline — paste into mermaid.live or any Markdown renderer
cmds:
- timeout --kill-after=10 60 dagger call --progress=plain -q -m ci --source=. graph
stalwart:
desc: Start a Stalwart instance for local development (via Dagger)
cmds:
- echo "Starting Stalwart on default ports (JMAP=8080, IMAP=1430, SMTP=1025, SIEVE=4190)"
- dagger call --progress=plain -q -m ci --source=. stalwart up --ports 8080:8080 --ports 1430:1430 --ports 1025:1025 --ports 4190:4190
deploy-linux:
desc: Build and deploy Linux release via Dagger
preconditions:
- sh: test -n "$SSH_PRIVATE_KEY"
msg: "SSH_PRIVATE_KEY is not set"
- sh: test -n "$SSH_KNOWN_HOSTS"
msg: "SSH_KNOWN_HOSTS is not set"
cmds:
- HASH=$(git rev-parse --short HEAD) && scripts/silent_on_success.sh timeout --kill-after=10 1800 dagger call --progress=plain -q -m ci --source=. deploy-linux --ssh-key env:SSH_PRIVATE_KEY --known-hosts env:SSH_KNOWN_HOSTS --ssh-user "$SSH_USER" --ssh-host "$SSH_HOST" --commit-hash "$HASH"
build-android-bundle:
desc: Build AAB via Dagger (cached, versionCode=1 placeholder) and export locally
cmds:
- mkdir -p build/app/outputs/bundle/release
- HASH=$(git rev-parse --short HEAD) && timeout --kill-after=10 1800 dagger call --progress=plain -q -m ci --source=. build-android-release --commit-hash "$HASH" -o build/app/outputs/bundle/release/app-release.aab
upload-android-bundle:
desc: Upload AAB from build/ to Play Store via Dagger
preconditions:
- sh: test -n "$PLAY_STORE_CONFIG_JSON"
msg: "PLAY_STORE_CONFIG_JSON is not set"
- sh: test -f build/app/outputs/bundle/release/app-release.aab
msg: "AAB not found — run build-android-bundle first"
cmds:
- timeout --kill-after=10 600 dagger call --progress=plain -q -m ci --source=. upload-to-play-store --aab build/app/outputs/bundle/release/app-release.aab --play-store-config env:PLAY_STORE_CONFIG_JSON
publish-android:
desc: Build cached AAB, stamp versionCode, sign, and publish to Play Store via Dagger
deps: [generate-changelog]
preconditions:
- sh: test -n "$PLAY_STORE_CONFIG_JSON"
msg: "PLAY_STORE_CONFIG_JSON is not set"
- sh: test -n "$ANDROID_KEYSTORE_BASE64"
msg: "ANDROID_KEYSTORE_BASE64 is not set"
- sh: test -n "$ANDROID_KEYSTORE_PASSWORD"
msg: "ANDROID_KEYSTORE_PASSWORD is not set"
cmds:
- HASH=$(git rev-parse --short HEAD) && scripts/silent_on_success.sh timeout --kill-after=10 1800 dagger call --progress=plain -q -m ci --source=. publish-android --play-store-config env:PLAY_STORE_CONFIG_JSON --keystore-base64 env:ANDROID_KEYSTORE_BASE64 --keystore-password env:ANDROID_KEYSTORE_PASSWORD --commit-hash "$HASH"
deploy-apk:
desc: Build and deploy Android APK via Dagger
preconditions:
- sh: test -n "$SSH_PRIVATE_KEY"
msg: "SSH_PRIVATE_KEY is not set"
- sh: test -n "$SSH_KNOWN_HOSTS"
msg: "SSH_KNOWN_HOSTS is not set"
- sh: test -n "$ANDROID_KEYSTORE_BASE64"
msg: "ANDROID_KEYSTORE_BASE64 is not set"
- sh: test -n "$ANDROID_KEYSTORE_PASSWORD"
msg: "ANDROID_KEYSTORE_PASSWORD is not set"
cmds:
- HASH=$(git rev-parse --short HEAD) && scripts/silent_on_success.sh timeout --kill-after=10 1800 dagger call --progress=plain -q -m ci --source=. deploy-apk --ssh-key env:SSH_PRIVATE_KEY --known-hosts env:SSH_KNOWN_HOSTS --ssh-user "$SSH_USER" --ssh-host "$SSH_HOST" --commit-hash "$HASH" --keystore-base64 env:ANDROID_KEYSTORE_BASE64 --keystore-password env:ANDROID_KEYSTORE_PASSWORD --build-number "$(git log -1 --format=%ct HEAD)"
publish-website:
desc: Build and publish website via Dagger
preconditions:
- sh: test -n "$SSH_PRIVATE_KEY"
msg: "SSH_PRIVATE_KEY is not set"
- sh: test -n "$SSH_KNOWN_HOSTS"
msg: "SSH_KNOWN_HOSTS is not set"
cmds:
- HASH=$(git rev-parse --short HEAD) && timeout --kill-after=10 600 dagger call --progress=plain -q -m ci --source=. publish-website --ssh-key env:SSH_PRIVATE_KEY --known-hosts env:SSH_KNOWN_HOSTS --ssh-user "$SSH_USER" --ssh-host "$SSH_HOST" --commit-hash "$HASH"
check-dagger:
desc: Run full check suite via Dagger (with OTEL timing report if python3 is available)
cmds:
- |
DAGGER_OUT=$(mktemp)
RC_FILE=$(mktemp)
_ts() { date -u '+[%H:%M:%S]'; }
run_dagger() {
: > "$DAGGER_OUT"; : > "$RC_FILE"
{ timeout --kill-after=10 600 "$@"; echo $? > "$RC_FILE"; } 2>&1 | tee "$DAGGER_OUT"
RC=$(cat "$RC_FILE" 2>/dev/null || echo 1)
if [ "$RC" -eq 124 ] && grep -q "All tests passed" "$DAGGER_OUT"; then
echo "$(_ts) dagger: hung in teardown after success; treating as exit 0." >&2
RC=0
fi
return "$RC"
}
retry_dagger() {
for attempt in 1 2 3; do
run_dagger "$@" && return 0
RC=$?
if [ "$attempt" -lt 3 ] && { grep -qE "connection reset|context deadline exceeded|connection refused|invalid return status code" "$DAGGER_OUT" || [ "$RC" -eq 2 ]; }; then
echo "$(_ts) dagger: network error on attempt $attempt/3, retrying..." >&2
elif [ "$attempt" -lt 3 ] && grep -q "No space left on device" "$DAGGER_OUT"; then
echo "$(_ts) dagger: disk space error on attempt $attempt/3, pruning Dagger cache..." >&2
timeout 120 dagger query '{ engine { localCache { prune(targetSpace: "20gb") } } }' 2>/dev/null || true
echo "$(_ts) dagger: waiting 90s for freed space to settle..." >&2
sleep 90
else
return "$RC"
fi
done
}
if ! command -v python3 >/dev/null 2>&1; then
retry_dagger dagger call --progress=plain -q -m ci --source=. check
RC=$?
rm -f "$DAGGER_OUT" "$RC_FILE"
exit $RC
fi
PORTFILE=$(mktemp)
python3 ci/otel-receiver.py --port-file="$PORTFILE" &
RECV_PID=$!
cleanup() {
rm -f "$PORTFILE" "$DAGGER_OUT" "$RC_FILE"
}
trap cleanup EXIT
until [ -s "$PORTFILE" ]; do
sleep 0.05
if ! kill -0 "$RECV_PID" 2>/dev/null; then
echo "$(_ts) otel-receiver.py died before writing port file; falling back to plain run" >&2
retry_dagger dagger call --progress=plain -q -m ci --source=. check
RC=$?
rm -f "$PORTFILE" "$DAGGER_OUT" "$RC_FILE"
exit $RC
fi
done
PORT=$(cat "$PORTFILE")
retry_dagger env \
OTEL_EXPORTER_OTLP_ENDPOINT="http://127.0.0.1:$PORT" \
OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf" \
dagger call --progress=plain -q -m ci --source=. check
RC=$?
curl -sf "http://127.0.0.1:$PORT/shutdown" >/dev/null 2>&1 || true
wait "$RECV_PID" 2>/dev/null || true
exit $RC
dagger-prune:
desc: Prune the Dagger engine cache (keeps named volumes unless total exceeds 75 GB, then targets 50 GB)
cmds:
- |
dagger query '{ engine { localCache { prune(maxUsedSpace: "75gb", targetSpace: "50gb") } } }'
renovate:
desc: Run Renovate bot against the repository via Dagger
preconditions:
- sh: test -n "$RENOVATE_FORGEJO_TOKEN"
msg: "RENOVATE_FORGEJO_TOKEN is not set"
cmds:
- timeout --kill-after=10 1800 dagger call --progress=plain -q -m ci --source=. renovate --renovate-token env:RENOVATE_FORGEJO_TOKEN
integration-android:
desc: UI integration tests on a connected Android emulator (Stalwart on host, emulator reaches it via 10.0.2.2)
deps: [_preflight, _android-sdk-check, _android-avd-setup]
sources:
- lib/**/*.dart
- integration_test/app_e2e_test.dart
- android/**/*
generates:
- build/integration-android.done
cmds:
- stalwart-dev/integration_android_test.sh
- touch build/integration-android.done
build-linux:
desc: Build the Linux desktop app (debug)
deps: [_preflight, _linux-deps-check, _codegen, generate-changelog]
method: timestamp
sources:
- lib/**/*.dart
- linux/**/*
- pubspec.yaml
generates:
- build/linux/x64/debug/bundle/sharedinbox
cmds:
- scripts/silent_on_success.sh fvm flutter build linux --debug --no-pub
build-linux-release:
desc: Build the Linux desktop app (release)
deps: [_preflight, _linux-deps-check, _codegen, generate-changelog]
method: timestamp
sources:
- lib/**/*.dart
- linux/**/*
- pubspec.yaml
generates:
- build/linux/x64/release/bundle/sharedinbox
cmds:
- scripts/silent_on_success.sh fvm flutter build linux --release --no-pub --dart-define=GIT_HASH=$(git rev-parse --short HEAD)
deploy-linux-to-server:
desc: Package and deploy the Linux release bundle to the server, update latest.json
deps: [build-linux-release]
preconditions:
- sh: test -n "$SSH_USER"
msg: "SSH_USER is not set"
- sh: test -n "$SSH_HOST"
msg: "SSH_HOST is not set"
- sh: test -n "$SSH_KNOWN_HOSTS"
msg: "SSH_KNOWN_HOSTS is not set"
cmds:
- |
mkdir -p ~/.ssh
printf '%s\n' "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
HASH=$(git rev-parse --short HEAD)
DATE_PATH=$(date -u +%Y/%m/%d)
REMOTE_DIR="public_html/builds/$DATE_PATH"
TARBALL="sharedinbox-linux-amd64-$HASH.tar.gz"
tar -czf /tmp/$TARBALL -C build/linux/x64/release bundle
ssh "$SSH_USER@$SSH_HOST" "mkdir -p $REMOTE_DIR"
scp /tmp/$TARBALL "$SSH_USER@$SSH_HOST:$REMOTE_DIR/$TARBALL"
DOWNLOAD_URL="https://sharedinbox.de/builds/$DATE_PATH/$TARBALL"
# Merge with any existing latest.json so we don't overwrite the windows key
EXISTING=$(ssh "$SSH_USER@$SSH_HOST" "cat public_html/latest.json 2>/dev/null || echo '{}'")
WINDOWS_URL=$(echo "$EXISTING" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('windows',''))" 2>/dev/null || true)
if [ -n "$WINDOWS_URL" ]; then
echo "{\"version\":\"$HASH\",\"linux\":\"$DOWNLOAD_URL\",\"windows\":\"$WINDOWS_URL\"}" | \
ssh "$SSH_USER@$SSH_HOST" "cat > public_html/latest.json"
else
echo "{\"version\":\"$HASH\",\"linux\":\"$DOWNLOAD_URL\"}" | \
ssh "$SSH_USER@$SSH_HOST" "cat > public_html/latest.json"
fi
echo "Uploaded $TARBALL and updated latest.json"
deploy-bugreport:
desc: Deploy the Go bugreport server by restarting the systemd service (it pulls latest code from Codeberg)
preconditions:
- sh: test -n "$SSH_USER"
msg: "SSH_USER is not set"
- sh: test -n "$SSH_HOST"
msg: "SSH_HOST is not set"
- sh: test -n "$SSH_KNOWN_HOSTS"
msg: "SSH_KNOWN_HOSTS is not set"
cmds:
- |
mkdir -p ~/.ssh
printf '%s\n' "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
ssh "root@$SSH_HOST" "systemctl restart bugreport"
echo "Restarted bugreport service on $SSH_HOST to pull latest code from Codeberg"
build-windows-release:
desc: Build the Windows desktop app (release) — must run on a Windows machine with MSVC
deps: [_pub-get, generate-changelog]
method: timestamp
sources:
- lib/**/*.dart
- windows/**/*
- pubspec.yaml
generates:
- build/windows/x64/runner/Release/sharedinbox.exe
cmds:
- fvm flutter build windows --release --no-pub --dart-define=GIT_HASH=$(git rev-parse --short HEAD)
deploy-windows-to-server:
desc: Package and deploy the Windows release bundle to the server, update latest.json
deps: [build-windows-release]
preconditions:
- sh: test -n "$SSH_USER"
msg: "SSH_USER is not set"
- sh: test -n "$SSH_HOST"
msg: "SSH_HOST is not set"
- sh: test -n "$SSH_KNOWN_HOSTS"
msg: "SSH_KNOWN_HOSTS is not set"
cmds:
- |
mkdir -p ~/.ssh
printf '%s\n' "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
HASH=$(git rev-parse --short HEAD)
DATE_PATH=$(date -u +%Y/%m/%d)
REMOTE_DIR="public_html/builds/$DATE_PATH"
ZIPFILE="sharedinbox-windows-x64-$HASH.zip"
cd build/windows/x64/runner && zip -r /tmp/$ZIPFILE Release/ && cd -
ssh "$SSH_USER@$SSH_HOST" "mkdir -p $REMOTE_DIR"
scp /tmp/$ZIPFILE "$SSH_USER@$SSH_HOST:$REMOTE_DIR/$ZIPFILE"
DOWNLOAD_URL="https://sharedinbox.de/builds/$DATE_PATH/$ZIPFILE"
EXISTING=$(ssh "$SSH_USER@$SSH_HOST" "cat public_html/latest.json 2>/dev/null || echo '{}'")
LINUX_URL=$(echo "$EXISTING" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('linux',''))" 2>/dev/null || true)
if [ -n "$LINUX_URL" ]; then
echo "{\"version\":\"$HASH\",\"linux\":\"$LINUX_URL\",\"windows\":\"$DOWNLOAD_URL\"}" | \
ssh "$SSH_USER@$SSH_HOST" "cat > public_html/latest.json"
else
echo "{\"version\":\"$HASH\",\"windows\":\"$DOWNLOAD_URL\"}" | \
ssh "$SSH_USER@$SSH_HOST" "cat > public_html/latest.json"
fi
echo "Uploaded $ZIPFILE and updated latest.json"
_android-avd-setup:
internal: true
run: once
status:
- test -f "${ANDROID_HOME:-$HOME/Android/Sdk}/emulator/emulator"
- test -d "$HOME/.android/avd/sharedinbox_test.avd"
cmds:
- cmd: |
SDK="${ANDROID_HOME:-$HOME/Android/Sdk}"
SDKMANAGER="$SDK/cmdline-tools/latest/bin/sdkmanager"
AVDMANAGER="$SDK/cmdline-tools/latest/bin/avdmanager"
yes | "$SDKMANAGER" --licenses >/dev/null 2>&1 || true
"$SDKMANAGER" "emulator" "system-images;android-34;google_apis;x86_64"
echo no | "$AVDMANAGER" create avd \
--name sharedinbox_test \
--package "system-images;android-34;google_apis;x86_64" \
--device pixel_4 \
--force
_android-sdk-check:
internal: true
run: once
preconditions:
- sh: test -d "${ANDROID_HOME:-$HOME/Android/Sdk}"
msg: |
Android SDK not found. Install it with:
wget https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O /tmp/cmdtools.zip
mkdir -p ~/Android/Sdk/cmdline-tools
unzip /tmp/cmdtools.zip -d ~/Android/Sdk/cmdline-tools
mv ~/Android/Sdk/cmdline-tools/cmdline-tools ~/Android/Sdk/cmdline-tools/latest
~/Android/Sdk/cmdline-tools/latest/bin/sdkmanager "platform-tools" "build-tools;34.0.0" "platforms;android-34"
- sh: ls "${ANDROID_HOME:-$HOME/Android/Sdk}/platforms/" 2>/dev/null | grep -qE '^android-([3-9][4-9]|[4-9][0-9]|[1-9][0-9]{2,})$'
msg: |
Android platform 34 or higher not found. Install it with:
${ANDROID_HOME:-$HOME/Android/Sdk}/cmdline-tools/latest/bin/sdkmanager "build-tools;34.0.0" "platforms;android-34"
build-android:
desc: Build a release APK
deps: [_preflight, _android-sdk-check, _pub-get, generate-changelog]
method: timestamp
sources:
- lib/**/*.dart
- android/**/*
- pubspec.yaml
generates:
- build/app/outputs/flutter-apk/app-release.apk
cmds:
- ANDROID_HOME=${ANDROID_HOME:-$HOME/Android/Sdk} fvm flutter build apk --release --no-pub --dart-define=GIT_HASH=$(git rev-parse --short HEAD) | grep -Ev "was tree-shaken|Tree-shaking can be disabled"
deploy-android-bundle:
desc: Build, sign, and upload AAB to Play Store internal track via Dagger
deps: [generate-changelog]
dotenv: [".env"]
cmds:
- sops exec-env secrets.enc.yaml 'HASH=$(git rev-parse --short HEAD) && scripts/silent_on_success.sh timeout --kill-after=10 1800 dagger call --progress=plain -q -m ci --source=. publish-android --play-store-config env:PLAY_STORE_CONFIG_JSON --keystore-base64 env:ANDROID_KEYSTORE_BASE64 --keystore-password env:ANDROID_KEYSTORE_PASSWORD --commit-hash "$HASH"'
deploy-android:
desc: Build release APK and upload via scp to $ANDROID_APK_SCP_USER@$ANDROID_APK_SCP_HOST:$ANDROID_APK_SCP_PATH
deps: [check, build-android]
sources:
- build/app/outputs/flutter-apk/app-release.apk
generates:
- build/deploy-android.done
dotenv: [".env"]
preconditions:
- sh: test -n "$ANDROID_APK_SCP_HOST"
msg: "ANDROID_APK_SCP_HOST is not set in .env"
- sh: test -n "$ANDROID_APK_SCP_USER"
msg: "ANDROID_APK_SCP_USER is not set in .env"
- sh: test -n "$ANDROID_APK_SCP_PATH"
msg: "ANDROID_APK_SCP_PATH is not set in .env"
cmds:
# integration-android runs after check (not in parallel) so the two E2E
# test suites don't compete for CPU and slow the Android emulator.
- task: integration-android
- scripts/deploy_android.sh
- touch build/deploy-android.done
run:
desc: Run the app on Linux desktop
deps: [_preflight, _linux-deps-check, _pub-get, _codegen]
cmds:
- fvm flutter run -d linux --no-pub
run-android:
desc: Run the app on a connected Android emulator (boots sharedinbox_test AVD if needed)
deps: [_preflight, _android-sdk-check, _android-avd-setup, _pub-get]
cmds:
- stalwart-dev/run_android.sh
coverage:
desc: Coverage gate — run after test (and optionally integration) have written lcov.info
deps: [_preflight]
cmds:
- fvm dart run scripts/check_coverage.dart
check-coverage:
desc: Run unit+widget tests with coverage, then fail if the gate is not met
deps: [test]
cmds:
- task: coverage
website-dev:
desc: Run Hugo development server
cmds:
- fvm flutter pub global run # Just a placeholder if nix shell hook isn't active
- nix develop --command hugo server --source website -D
website-build:
desc: Build the static website (embeds current git hash via HUGO_PARAMS_GITVERSION)
env:
HUGO_PARAMS_GITVERSION:
sh: git rev-parse --short HEAD
cmds:
- hugo --source website --minify
website-verify:
desc: Check that the deployed site serves the current git version
cmds:
- scripts/website-verify.sh
deploy-apk-to-server:
desc: SCP the release APK to the server at public_html/builds/YYYY/MM/DD/
deps: [build-android]
preconditions:
- sh: test -n "$SSH_USER"
msg: "SSH_USER is not set"
- sh: test -n "$SSH_HOST"
msg: "SSH_HOST is not set"
- sh: test -n "$SSH_KNOWN_HOSTS"
msg: "SSH_KNOWN_HOSTS is not set"
cmds:
- |
mkdir -p ~/.ssh
printf '%s\n' "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
HASH=$(git rev-parse --short HEAD)
DATE_PATH=$(date -u +%Y/%m/%d)
REMOTE_DIR="public_html/builds/$DATE_PATH"
APK_NAME="sharedinbox-mua-$HASH.apk"
ssh "$SSH_USER@$SSH_HOST" "mkdir -p $REMOTE_DIR"
scp \
build/app/outputs/flutter-apk/app-release.apk \
"$SSH_USER@$SSH_HOST:$REMOTE_DIR/$APK_NAME"
echo "Uploaded $APK_NAME to $REMOTE_DIR"
generate-build-history:
desc: Generate Hugo build-history pages from Linux and Android builds on the server
preconditions:
- sh: test -n "$SSH_USER"
msg: "SSH_USER is not set"
- sh: test -n "$SSH_HOST"
msg: "SSH_HOST is not set"
cmds:
- python3 scripts/generate_build_history.py
website-publish:
desc: Generate build history, build Hugo site, and rsync to server (requires SSH_USER + SSH_HOST)
preconditions:
- sh: test -n "$SSH_USER"
msg: "SSH_USER is not set"
- sh: test -n "$SSH_HOST"
msg: "SSH_HOST is not set"
cmds:
- task: generate-build-history
- task: website-deploy
website-deploy:
desc: Deploy the website via rsync to public_html
deps: [website-build]
preconditions:
- sh: test -n "$SSH_KNOWN_HOSTS"
msg: "SSH_KNOWN_HOSTS is not set"
cmds:
- |
mkdir -p ~/.ssh
printf '%s\n' "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
rsync -avz --delete \
--exclude='*.apk' \
--exclude='*.tar.gz' \
website/public/ \
${SSH_USER}@${SSH_HOST}:public_html/
check-fast:
desc: Pre-commit checks via Dagger (format, analyze, mocks, coverage — no integration or backend)
cmds:
- dagger call --progress=plain -q -m ci --source=. check-fast
check-layers:
desc: Enforce architecture — ui/ must not import data/ (only core/ interfaces allowed)
cmds:
- |
VIOLATIONS=$(grep -rn "package:sharedinbox/data/" lib/ui/ 2>/dev/null || true)
if [ -n "$VIOLATIONS" ]; then
echo "ERROR: UI layer imports data layer (only core/ interfaces are allowed from ui/):"
echo "$VIOLATIONS"
exit 1
fi
check-hygiene:
desc: Verify that no forbidden files (like home dir config) are tracked
cmds:
- |
FORBIDDEN=".ssh .bashrc .config .local .cache .gitconfig .android Android .gradle .pub-cache .dartServer .flutter .dart-cli-completion .atuin .bash_logout .profile .zcompdump .zshrc snap .emulator_console_auth_token .lesshst .metadata .tmux.conf"
TRACKED=$(git ls-files $FORBIDDEN 2>/dev/null)
if [ -n "$TRACKED" ]; then
echo "ERROR: The following forbidden files are tracked in git:"
echo "$TRACKED"
echo "Please remove them with 'git rm --cached <file>'"
exit 1
fi
echo "Hygiene check passed."
check-ci-images:
desc: Verify that all container images referenced in ci/main.go are reachable
cmds:
- scripts/check_ci_images.sh
_integrations:
internal: true
run: once
cmds:
- task: test-backend
- task: integration-ui
ci-logs:
desc: "Fetch latest CI job logs from Codeberg. Usage: task ci-logs [RUN=8] [JOB=0]"
cmds:
- scripts/ci_logs.sh "{{.RUN}}" "{{.JOB}}"
screenshots:
desc: Generate Play Store promotional screenshots (30 golden files — 3 devices × 2 themes × 5 scenes)
deps: [_preflight, _codegen]
cmds:
- fvm flutter test test/screenshot_automation_test.dart --update-goldens
chaos-monkey-backend:
desc: Chaos monkey — random IMAP/SMTP ops against Stalwart (via Dagger, headless)
cmds:
- timeout --kill-after=10 600 dagger call --progress=plain -q -m ci --source=. chaos-monkey-backend
check:
desc: Full check suite — unit tests first, then integration (merges coverage), then gate
deps: [analyze, build-linux, test]
cmds:
- task: _integrations
- task: coverage