Skip to content

Merge pull request #554 from tryigit/bolt-rust-memory-optimizations-1… #1370

Merge pull request #554 from tryigit/bolt-rust-memory-optimizations-1…

Merge pull request #554 from tryigit/bolt-rust-memory-optimizations-1… #1370

Workflow file for this run

name: Build
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
workflow_dispatch:
permissions:
contents: write
jobs:
# ============================================================
# Phase 1: Safety & Quality Gate (runs first, gates everything)
# ============================================================
safety-check:
name: "🛡️ Safety & Quality Gate"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: "🔒 SELinux Policy — Check semicolons"
run: |
echo "=== Checking sepolicy.rule for missing semicolons ==="
ERRORS=0
LINE_NUM=0
while IFS= read -r line; do
LINE_NUM=$((LINE_NUM + 1))
trimmed=$(echo "$line" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
[ -z "$trimmed" ] && continue
[[ "$trimmed" == \#* ]] && continue
[[ "$trimmed" == type\ * ]] && continue
if echo "$trimmed" | grep -qE '^(allow|dontaudit|neverallow|auditallow) '; then
statement=$(echo "$trimmed" | sed 's/#.*//' | sed 's/[[:space:]]*$//')
if [[ "$statement" != *";" ]]; then
echo "::error file=module/template/sepolicy.rule,line=$LINE_NUM::Missing semicolon: $trimmed"
ERRORS=$((ERRORS + 1))
fi
fi
done < module/template/sepolicy.rule
if [ "$ERRORS" -gt 0 ]; then
echo "::error::$ERRORS sepolicy statement(s) missing semicolons! This WILL break policy compilation on device."
exit 1
fi
echo "✅ All sepolicy statements properly terminated"
- name: "🔑 Security — Check for insecure PRNG"
run: |
echo "=== Scanning for java.util.Random in production code ==="
ERRORS=0
FILES=$(grep -rl "java\.util\.Random" --include="*.kt" --include="*.java" --exclude-dir="test" --exclude-dir="androidTest" service/ module/ 2>/dev/null || true)
for f in $FILES; do
LINES=$(grep -n "java\.util\.Random" "$f" | grep -iv "test" || true)
if [ -n "$LINES" ]; then
while IFS= read -r match; do
LINE_NUM=$(echo "$match" | cut -d: -f1)
echo "::error file=$f,line=$LINE_NUM::Insecure java.util.Random in production code. Use java.security.SecureRandom."
ERRORS=$((ERRORS + 1))
done <<< "$LINES"
fi
done
if [ "$ERRORS" -gt 0 ]; then
exit 1
fi
echo "✅ No insecure PRNG usage"
- name: "🦀 Security — Check for unwrap/expect in Rust FFI"
run: |
echo "=== Scanning for unwrap()/expect() near FFI boundary ==="
for f in $(find rust/ -name "*.rs" ! -path "*/test*"); do
if grep -q 'extern "C"' "$f" 2>/dev/null; then
MATCHES=$(awk '
/^#\[cfg\(test\)\]/ { in_test=1 }
/^mod tests/ { in_test=1 }
in_test && /^}$/ { in_test=0 }
!in_test && /\.(unwrap|expect)\(/ { print NR": "$0 }
' "$f" 2>/dev/null || true)
if [ -n "$MATCHES" ]; then
while IFS= read -r match; do
LINE_NUM=$(echo "$match" | cut -d: -f1)
echo "::warning file=$f,line=$LINE_NUM::unwrap()/expect() found near FFI boundary"
done <<< "$MATCHES"
fi
fi
done
echo "✅ FFI panic safety check complete"
- name: "⚡ Performance — Check binder FD caching"
run: |
if grep -q 'if (is_binder)' module/src/main/cpp/binder_interceptor.cpp 2>/dev/null; then
if ! grep -q 'g_binder_fds\[fd\] = is_binder' module/src/main/cpp/binder_interceptor.cpp 2>/dev/null; then
echo "::warning file=module/src/main/cpp/binder_interceptor.cpp::is_binder_fd may not cache negative results"
fi
fi
echo "✅ Performance check complete"
- name: "📱 Real-World — Validate Magisk/KernelSU module structure"
run: |
ERRORS=0
for f in module.prop customize.sh service.sh post-fs-data.sh sepolicy.rule; do
if [ ! -f "module/template/$f" ]; then
echo "::error::Missing required module file: module/template/$f"
ERRORS=$((ERRORS + 1))
fi
done
if [ -f "module/template/module.prop" ]; then
for field in id name version versionCode author description; do
if ! grep -q "^${field}=" "module/template/module.prop"; then
echo "::error file=module/template/module.prop::Missing required field: $field"
ERRORS=$((ERRORS + 1))
fi
done
fi
if [ "$ERRORS" -gt 0 ]; then exit 1; fi
echo "✅ Module structure valid"
- name: "📱 Real-World — Validate shell scripts"
run: |
ERRORS=0
for script in module/template/action.sh module/template/customize.sh module/template/service.sh module/template/post-fs-data.sh; do
if [ -f "$script" ]; then
if ! bash -n "$script" 2>/dev/null; then
echo "::error file=$script::Shell syntax error"
ERRORS=$((ERRORS + 1))
fi
fi
done
if [ "$ERRORS" -gt 0 ]; then exit 1; fi
echo "✅ Shell scripts valid"
- name: "📱 Real-World — Validate Rust static libraries"
run: |
ERRORS=0
for abi in arm64-v8a armeabi-v7a x86 x86_64; do
LIB="module/src/main/cpp/external/rust_libs/$abi/libcleverestricky_cbor_cose.a"
if [ ! -f "$LIB" ]; then
echo "::warning::Missing Rust library for $abi"
else
SIZE=$(stat -c%s "$LIB" 2>/dev/null || stat -f%z "$LIB" 2>/dev/null)
if [ "$SIZE" -lt 1000 ]; then
echo "::error file=$LIB::Rust library suspiciously small ($SIZE bytes)"
ERRORS=$((ERRORS + 1))
fi
fi
done
if [ "$ERRORS" -gt 0 ]; then exit 1; fi
echo "✅ Rust libraries valid"
- name: "🔗 Real-World — Check Binder transaction code consistency"
run: |
CPP_PROP_CODE=$(grep -oP 'REGISTER_PROPERTY_SERVICE\s*=\s*\K\d+' module/src/main/cpp/binder_interceptor.cpp 2>/dev/null || echo "not_found")
KT_PROP_CODE=$(grep -oP 'REGISTER_PROPERTY_SERVICE_TRANSACTION_CODE\s*=\s*\K\d+' service/src/main/java/cleveres/tricky/cleverestech/binder/BinderInterceptor.kt 2>/dev/null || echo "not_found")
if [ "$CPP_PROP_CODE" != "not_found" ] && [ "$KT_PROP_CODE" != "not_found" ]; then
if [ "$CPP_PROP_CODE" != "$KT_PROP_CODE" ]; then
echo "::error::Binder transaction code mismatch! C++=$CPP_PROP_CODE vs Kotlin=$KT_PROP_CODE"
exit 1
fi
fi
echo "✅ Binder codes consistent"
- name: "🔄 Daemon — Check crash retry logic in service.sh"
run: |
ERRORS=0
SERVICE_SH="module/template/service.sh"
# 1. Verify retry counter exists
if ! grep -q 'FAIL_COUNT' "$SERVICE_SH"; then
echo "::error file=$SERVICE_SH::Missing FAIL_COUNT retry counter — daemon will crash permanently on first non-zero exit"
ERRORS=$((ERRORS + 1))
fi
# 2. Verify max retries limit exists
if ! grep -q 'MAX_FAILS' "$SERVICE_SH"; then
echo "::error file=$SERVICE_SH::Missing MAX_FAILS limit — daemon has no retry cap"
ERRORS=$((ERRORS + 1))
fi
# 3. Verify sleep between retries to prevent rapid crash loops
if ! grep -q 'sleep' "$SERVICE_SH"; then
echo "::error file=$SERVICE_SH::Missing sleep between retries — daemon may enter rapid crash loop"
ERRORS=$((ERRORS + 1))
fi
# 4. Verify retry counter resets on clean exit
if ! grep -q 'FAIL_COUNT=0' "$SERVICE_SH"; then
echo "::error file=$SERVICE_SH::Retry counter never resets — daemon will eventually die even after successful runs"
ERRORS=$((ERRORS + 1))
fi
# 5. service.sh must not give up with a hard non-zero exit after MAX_FAILS
if grep -q 'Max retries reached, giving up' "$SERVICE_SH"; then
echo "::error file=$SERVICE_SH::service.sh still gives up after MAX_FAILS — this can trigger repeated boot-time failure loops"
ERRORS=$((ERRORS + 1))
fi
# 6. Verify daemon script exists and is a valid shell script
DAEMON_SCRIPT="module/template/daemon"
if [ ! -f "$DAEMON_SCRIPT" ]; then
echo "::error::Missing daemon script: $DAEMON_SCRIPT"
ERRORS=$((ERRORS + 1))
else
# Verify it launches the Java service with app_process
if ! grep -q 'app_process' "$DAEMON_SCRIPT"; then
echo "::error file=$DAEMON_SCRIPT::Daemon script does not use app_process to launch Java service"
ERRORS=$((ERRORS + 1))
fi
# Verify exec is used (prevents zombie processes)
if ! grep -q '^exec ' "$DAEMON_SCRIPT"; then
echo "::error file=$DAEMON_SCRIPT::Daemon script should use exec to replace shell process"
ERRORS=$((ERRORS + 1))
fi
fi
if [ "$ERRORS" -gt 0 ]; then exit 1; fi
echo "✅ Daemon crash resilience checks passed"
- name: "💀 Daemon — Check for unguarded exitProcess calls"
run: |
ERRORS=0
# Scan for exitProcess(1) calls that could cause permanent daemon death
# exitProcess(0) is fine (clean restart), exitProcess(1) is dangerous
FILES=$(grep -rl 'exitProcess(1)' --include="*.kt" service/ 2>/dev/null || true)
EXIT1_COUNT=0
for f in $FILES; do
LINES=$(grep -c 'exitProcess(1)' "$f" 2>/dev/null || echo "0")
EXIT1_COUNT=$((EXIT1_COUNT + LINES))
done
# Threshold: more than 3 exitProcess(1) calls indicates excessive hard exits
# that increase the risk of daemon crash loops. Each call is a potential
# permanent daemon death point if retry logic is insufficient.
if [ "$EXIT1_COUNT" -gt 3 ]; then
echo "::warning::Found $EXIT1_COUNT exitProcess(1) calls in service code. Each one can crash the daemon permanently if service.sh lacks retry logic."
fi
# Verify Main.kt doesn't have unguarded exitProcess
if grep -q 'exitProcess' service/src/main/java/cleveres/tricky/cleverestech/Main.kt 2>/dev/null; then
echo "::warning file=service/src/main/java/cleveres/tricky/cleverestech/Main.kt::Main.kt contains exitProcess — ensure daemon restart handles this"
fi
echo "✅ Daemon exit analysis complete (found $EXIT1_COUNT hard exit points)"
- name: "🔄 Interceptor — Check retry counter in DRM/Telephony interceptors"
run: |
ERRORS=0
# DrmInterceptor: triedCount must be incremented when service not found
# This prevents infinite retry loops that waste CPU
DRM_FILE="service/src/main/java/cleveres/tricky/cleverestech/DrmInterceptor.kt"
if [ -f "$DRM_FILE" ]; then
# Count triedCount increments — must be at least 2 (service-not-found + injection-failed)
INC_COUNT=$(grep -c 'triedCount += 1' "$DRM_FILE" 2>/dev/null || echo "0")
if [ "$INC_COUNT" -lt 2 ]; then
echo "::error file=$DRM_FILE::DrmInterceptor has only $INC_COUNT triedCount increments — needs at least 2 (service-not-found + injection-failed paths)"
ERRORS=$((ERRORS + 1))
fi
# Must have retry limit
if ! grep -q 'triedCount >= 3' "$DRM_FILE" 2>/dev/null; then
echo "::error file=$DRM_FILE::DrmInterceptor missing retry limit (triedCount >= 3)"
ERRORS=$((ERRORS + 1))
fi
fi
TEL_FILE="service/src/main/java/cleveres/tricky/cleverestech/TelephonyInterceptor.kt"
if [ -f "$TEL_FILE" ]; then
if ! grep -q 'triedCount >= 3' "$TEL_FILE" 2>/dev/null; then
echo "::error file=$TEL_FILE::TelephonyInterceptor missing retry limit"
ERRORS=$((ERRORS + 1))
fi
fi
if [ "$ERRORS" -gt 0 ]; then exit 1; fi
echo "✅ Interceptor retry logic valid"
- name: "📝 SecureFile — Check write integrity"
run: |
ERRORS=0
SF_FILE="service/src/main/java/cleveres/tricky/cleverestech/util/SecureFile.kt"
if [ -f "$SF_FILE" ]; then
# Must detect partial writes (not silently succeed)
if ! grep -q 'Incomplete write\|Partial write' "$SF_FILE" 2>/dev/null; then
echo "::error file=$SF_FILE::SecureFile.writeBytes does not detect partial writes — silent data corruption risk"
ERRORS=$((ERRORS + 1))
fi
# Must use atomic rename
if ! grep -q 'Os.rename' "$SF_FILE" 2>/dev/null; then
echo "::error file=$SF_FILE::SecureFile missing Os.rename — non-atomic writes can corrupt config"
ERRORS=$((ERRORS + 1))
fi
# Must fsync before rename
if ! grep -q 'Os.fsync' "$SF_FILE" 2>/dev/null; then
echo "::error file=$SF_FILE::SecureFile missing Os.fsync — crash during write can lose data"
ERRORS=$((ERRORS + 1))
fi
fi
if [ "$ERRORS" -gt 0 ]; then exit 1; fi
echo "✅ SecureFile write integrity checks passed"
- name: "🔢 Injection — Check PID bounds validation"
run: |
ERRORS=0
INJECT_FILE="module/src/main/cpp/inject/main.cpp"
if [ -f "$INJECT_FILE" ]; then
# Must check for INT_MAX to prevent integer overflow
if ! grep -q 'INT_MAX' "$INJECT_FILE" 2>/dev/null; then
echo "::error file=$INJECT_FILE::PID validation missing INT_MAX check — potential injection into wrong process"
ERRORS=$((ERRORS + 1))
fi
# Must include climits for INT_MAX
if ! grep -q '<climits>' "$INJECT_FILE" 2>/dev/null; then
echo "::error file=$INJECT_FILE::Missing #include <climits> for INT_MAX"
ERRORS=$((ERRORS + 1))
fi
fi
if [ "$ERRORS" -gt 0 ]; then exit 1; fi
echo "✅ Injection PID validation checks passed"
- name: "🛡️ Security — Check for shell injection patterns"
run: |
# Scan for dangerous sh -c with string interpolation in production Kotlin code
# Pattern: exec("cmd $variable") or arrayOf("sh", "-c", "cmd $var")
# Safe pattern: Runtime.getRuntime().exec(arrayOf("cmd", arg1, arg2))
WARNINGS=0
for f in $(find service/src/main/java -name "*.kt" 2>/dev/null); do
# Find lines with exec() that contain dollar-sign interpolation
# Skip lines that are purely comments (start with //)
while IFS= read -r match; do
LINE_NUM=$(echo "$match" | cut -d: -f1)
LINE_CONTENT=$(echo "$match" | cut -d: -f2-)
# Skip pure comment lines
TRIMMED=$(echo "$LINE_CONTENT" | sed 's/^[[:space:]]*//')
if [[ "$TRIMMED" != //* ]]; then
echo "::warning file=$f,line=$LINE_NUM::Potential shell injection: exec() with string interpolation. Use array-based exec instead."
WARNINGS=$((WARNINGS + 1))
fi
done < <(grep -n 'exec(".*\$' "$f" 2>/dev/null || true)
done
echo "✅ Shell injection pattern scan complete (found $WARNINGS warnings)"
- name: "🔌 Process — Check for stream leaks in Runtime.exec"
run: |
# Files using Runtime.exec() in retry loops must drain process streams
# to prevent FD exhaustion. This check warns when exec is used without
# any stream handling in the same file.
WARNINGS=0
for f in $(find service/src/main/java -name "*.kt" 2>/dev/null); do
EXEC_COUNT=$(grep -c 'Runtime.getRuntime().exec' "$f" 2>/dev/null || echo "0")
if [ "$EXEC_COUNT" -gt 0 ]; then
STREAM_COUNT=$(grep -c 'inputStream\.\|errorStream\.\|ProcessBuilder' "$f" 2>/dev/null || echo "0")
if [ "$STREAM_COUNT" -eq 0 ]; then
echo "::warning file=$f::$EXEC_COUNT Runtime.exec() call(s) without any stream handling — potential FD leak"
WARNINGS=$((WARNINGS + 1))
fi
fi
done
echo "✅ Process stream leak scan complete (found $WARNINGS warnings)"
# ============================================================
# Phase 2: Rust Tests (after safety check)
# ============================================================
rust-test:
needs: safety-check
runs-on: ubuntu-latest
env:
RUSTFLAGS: "-D warnings"
defaults:
run:
working-directory: rust/cbor-cose
steps:
- name: Check out
uses: actions/checkout@v6
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo registry and build
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
rust/cbor-cose/target
key: ${{ runner.os }}-cargo-${{ hashFiles('rust/cbor-cose/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Run tests
run: cargo test --verbose
- name: Run tests (release mode)
run: cargo test --release --verbose
- name: Check formatting
run: cargo fmt -- --check
- name: Run clippy
run: cargo clippy -- -D warnings
# ============================================================
# Phase 3: Instrumentation Tests (after safety check)
# ============================================================
instrumentation-test:
needs: safety-check
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
api-level: [33]
name: instrumentation-test (API ${{ matrix.api-level }})
steps:
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Check out
uses: actions/checkout@v6
with:
submodules: "recursive"
fetch-depth: 0
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
gradle-home-cache-cleanup: true
- name: Setup Android SDK
uses: android-actions/setup-android@v4
with:
packages: ''
- name: Download KeyAttestation APK
run: wget -q https://github.com/vvb2060/KeyAttestation/releases/download/v1.8.4/KeyAttestation-v1.8.4.apk -O KeyAttestation.apk
- name: Build Module
run: ./gradlew :module:zipDebug
- name: Run E2E Integration Tests (Magisk)
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
target: google_apis_playstore
arch: x86_64
force-avd-creation: false
emulator-options: -no-window -gpu auto -no-snapshot -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: |
echo "Installing Magisk via rootAVD..."
git clone https://github.com/newbit1/rootAVD.git
rm -rf rootAVD/.git
(cd rootAVD && ./rootAVD.sh system-images/android-33/google_apis_playstore/x86_64/ramdisk.img)
adb wait-for-device
echo "Pushing and Installing CleveresTricky Module..."
MODULE_ZIP="$(ls module/release/*-debug.zip 2>/dev/null | head -n 1)"
if [ -z "$MODULE_ZIP" ]; then
echo "Error: debug module zip not found"
exit 1
fi
adb push "$MODULE_ZIP" /data/local/tmp/module.zip
if ! adb shell "su -c 'magisk --install-module /data/local/tmp/module.zip'"; then
echo "Installation via su failed, attempting direct magisk command..."
adb shell "magisk --install-module /data/local/tmp/module.zip"
fi
adb reboot
adb wait-for-device
echo "Downloading Test APKs..."
wget -q https://github.com/vvb2060/KeyAttestation/releases/download/v1.8.4/KeyAttestation-v1.8.4.apk -O KeyAttestation.apk
wget -q https://github.com/herzhenr/spic-android/releases/download/v1.4.0/spic-v1.4.0.apk -O spic.apk
echo "Installing Test APKs..."
adb install -r KeyAttestation.apk
adb install -r spic.apk
echo "Starting Test Apps..."
adb logcat -c
adb shell am start -n io.github.vvb2060.keyattestation/.home.HomeActivity
adb shell am start -n com.henrikherzig.playintegritychecker/.MainActivity
echo "Waiting for logs to generate..."
sleep 15
adb logcat -d > logcat.txt
echo "Verifying CleveresTricky daemon..."
if ! grep -qi "CleveresTricky daemon" logcat.txt; then
echo "Error: CleveresTricky daemon not found in logcat!"
cat logcat.txt
exit 1
fi
echo "Verifying Adaptive Binder Interceptor..."
if ! grep -qi "Adaptive Binder Interceptor" logcat.txt; then
echo "Error: Adaptive Binder Interceptor not found in logcat!"
cat logcat.txt
exit 1
fi
echo "Checking Play Integrity Results..."
if grep -qi "MEETS_DEVICE_INTEGRITY" logcat.txt; then
echo "Play Integrity: MEETS_DEVICE_INTEGRITY"
else
echo "Emulator detected, hardware keystore missing - skipping strict Integrity Check"
fi
echo "E2E Integration Test Passed!"
emulator-boot-timeout: 1200
env:
ANDROID_EMULATOR_WAIT_TIME_BEFORE_KILL: 60
# ============================================================
# Phase 4: Build & Release (after Rust tests pass)
# ============================================================
build:
needs: rust-test
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v6
with:
submodules: "recursive"
fetch-depth: 0
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
gradle-home-cache-cleanup: true
- name: Setup Android SDK
uses: android-actions/setup-android@v4
with:
packages: ''
- name: Run Module Tests
run: ./gradlew :module:testDebugUnitTest
- name: Run Unit Tests
run: ./gradlew testDebugUnitTest
- name: Build with Gradle
run: |
./gradlew zipRelease
./gradlew zipDebug
./gradlew :encryptor-app:assembleRelease
- name: Prepare artifact
if: success()
id: prepareArtifact
run: |
releasePath=$(ls module/release/*-release.zip)
releaseName=$(basename "$releasePath" .zip)
echo "releaseName=$releaseName" >> $GITHUB_OUTPUT
debugPath=$(ls module/release/*-debug.zip)
debugName=$(basename "$debugPath" .zip)
echo "debugName=$debugName" >> $GITHUB_OUTPUT
unzip "$releasePath" -d module-release
unzip "$debugPath" -d module-debug
- name: Upload release
uses: actions/upload-artifact@v7
with:
name: ${{ steps.prepareArtifact.outputs.releaseName }}
path: "./module-release/*"
- name: Upload debug
uses: actions/upload-artifact@v7
with:
name: ${{ steps.prepareArtifact.outputs.debugName }}
path: "./module-debug/*"
- name: Upload release mappings
uses: actions/upload-artifact@v7
with:
name: release-mappings
path: "./service/build/outputs/mapping/release"
- name: Upload Encryptor App
uses: actions/upload-artifact@v7
with:
name: EncryptorApp
path: "./encryptor-app/build/outputs/apk/release/*.apk"
- name: Check for Release
if: success() && github.event_name == 'push' && github.ref == 'refs/heads/master'
id: check_release
run: |
VERSION=$(grep 'val verName by extra' build.gradle.kts | cut -d'"' -f2 | sed 's/^[vV]//')
echo "VERSION=$VERSION" >> $GITHUB_ENV
if git ls-remote --exit-code --tags origin "refs/tags/V$VERSION" >/dev/null 2>&1; then
echo "Tag V$VERSION already exists."
echo "create_release=false" >> $GITHUB_OUTPUT
else
echo "Tag V$VERSION does not exist."
echo "create_release=true" >> $GITHUB_OUTPUT
fi
- name: Create Tag
if: steps.check_release.outputs.create_release == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag "V${{ env.VERSION }}"
git push origin "V${{ env.VERSION }}"
- name: Generate Changelog
if: steps.check_release.outputs.create_release == 'true'
id: changelog
run: |
PREV_TAG=$(git describe --tags --abbrev=0 "V${{ env.VERSION }}^" 2>/dev/null || echo "")
if [ -z "$PREV_TAG" ]; then
CHANGELOG=$(git log --pretty=format:"* %s (%h)" "V${{ env.VERSION }}")
else
CHANGELOG=$(git log --pretty=format:"* %s (%h)" "$PREV_TAG..V${{ env.VERSION }}")
fi
echo "changelog<<EOF" >> $GITHUB_OUTPUT
echo "$CHANGELOG" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create Release
if: steps.check_release.outputs.create_release == 'true'
uses: softprops/action-gh-release@v2
with:
tag_name: "V${{ env.VERSION }}"
name: "Release V${{ env.VERSION }}"
body: ${{ steps.changelog.outputs.changelog }}
files: |
module/release/*-release.zip
module/release/*-debug.zip
encryptor-app/build/outputs/apk/release/*.apk