Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 181 additions & 17 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,125 @@ on:
- 'v*'

permissions:
actions: read
contents: write

jobs:
release:
name: Build & Publish Release APK
validate:
name: Validate Release Provenance
timeout-minutes: 20
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
outputs:
release_commit: ${{ steps.provenance.outputs.release_commit }}

steps:
- name: Checkout repository
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: ${{ github.ref }}
fetch-depth: 0

- name: Validate release tag
- name: Validate release provenance
id: provenance
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail

if [[ "$GITHUB_REF_TYPE" != "tag" || ! "$GITHUB_REF_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "ERROR: release requires vX.Y.Z tag; got '$GITHUB_REF_NAME' ($GITHUB_REF_TYPE)"
exit 1
fi

event_ref=$(jq --raw-output '.ref' "$GITHUB_EVENT_PATH")
event_after=$(jq --raw-output '.after' "$GITHUB_EVENT_PATH")
tag_object=$(git rev-parse "$GITHUB_REF")
release_commit=$(git rev-parse "${GITHUB_REF}^{commit}")
if [[ "$event_ref" != "$GITHUB_REF" || ( "$event_after" != "$tag_object" && "$event_after" != "$release_commit" ) ]]; then
echo "ERROR: release event ref does not match checked-out tag object"
exit 1
fi

git fetch --no-tags --force origin +refs/heads/main:refs/remotes/origin/main
main_commit=$(git rev-parse origin/main)
if [[ "$release_commit" != "$main_commit" ]]; then
echo "ERROR: tag commit $release_commit is not current origin/main commit $main_commit"
exit 1
fi
git checkout --detach "$release_commit"
echo "release_commit=$release_commit" >> "$GITHUB_OUTPUT"

version_name=$(sed -nE 's/^[[:space:]]*versionName[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' app/build.gradle.kts)
if [[ $(printf '%s\n' "$version_name" | sed '/^$/d' | wc -l) -ne 1 || "$version_name" != "${GITHUB_REF_NAME#v}" ]]; then
echo "ERROR: app versionName '$version_name' does not match tag '$GITHUB_REF_NAME'"
exit 1
fi

ci_runs_url="https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/workflows/ci.yml/runs?head_sha=${release_commit}&event=push&status=completed&per_page=100"
curl --fail --silent --show-error \
--header 'Accept: application/vnd.github+json' \
--header "Authorization: Bearer $GH_TOKEN" \
--header 'X-GitHub-Api-Version: 2022-11-28' \
"$ci_runs_url" > "$RUNNER_TEMP/ci-runs.json"
ci_run_id=$(jq --exit-status --raw-output --arg commit "$release_commit" '
[.workflow_runs[] | select(.head_sha == $commit and .event == "push" and .head_branch == "main" and .status == "completed")]
| max_by(.run_started_at) | select(.conclusion == "success") | .id
' "$RUNNER_TEMP/ci-runs.json") || {
echo "ERROR: Android CI push run for $release_commit on main has not completed successfully"
exit 1
}
curl --fail --silent --show-error \
--header 'Accept: application/vnd.github+json' \
--header "Authorization: Bearer $GH_TOKEN" \
--header 'X-GitHub-Api-Version: 2022-11-28' \
"https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/runs/${ci_run_id}/jobs?filter=latest&per_page=100" > "$RUNNER_TEMP/ci-jobs.json"
if ! jq --exit-status '
any(.jobs[]; .name == "Build & Test" and .status == "completed" and .conclusion == "success")
' "$RUNNER_TEMP/ci-jobs.json" > /dev/null; then
echo "ERROR: Android CI Build & Test job for $release_commit has not completed successfully"
exit 1
fi

release_url="https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/tags/${GITHUB_REF_NAME}"
release_status=$(curl --silent --show-error --output "$RUNNER_TEMP/existing-release.json" --write-out '%{http_code}' \
--header 'Authorization: Bearer $GH_TOKEN' \
--header 'Accept: application/vnd.github+json' \
--header 'X-GitHub-Api-Version: 2022-11-28' \
"$release_url")
case "$release_status" in
404) ;;
200)
jq --exit-status --raw-output '.body | strings' "$RUNNER_TEMP/existing-release.json" > "$RUNNER_TEMP/existing-release-body.md"
echo "ERROR: release for $GITHUB_REF_NAME already exists; refusing to overwrite its notes or assets"
exit 1
;;
*)
echo "ERROR: unable to query existing release for $GITHUB_REF_NAME (HTTP $release_status)"
exit 1
;;
esac

release:
name: Build & Publish Release APK
needs: validate
timeout-minutes: 20
runs-on: ubuntu-latest
environment: release-signing
permissions:
contents: write
env:
RELEASE_COMMIT: ${{ needs.validate.outputs.release_commit }}

steps:
- name: Checkout validated release commit
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: ${{ needs.validate.outputs.release_commit }}
fetch-depth: 1

- name: Set up JDK 17
uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4
with:
Expand Down Expand Up @@ -78,27 +178,91 @@ jobs:
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}

- name: Rename APK & Generate Checksum
- name: Package release assets and notes
id: release_assets
run: |
VERSION_TAG=$GITHUB_REF_NAME
cd app/build/outputs/apk/release/
RELEASE_APK=$(ls -1 app-*-release.apk app-release.apk 2>/dev/null | head -n 1)
if [ -n "$RELEASE_APK" ]; then
FINAL_NAME="FlowPilot-${VERSION_TAG}.apk"
mv "$RELEASE_APK" "$FINAL_NAME"
sha256sum "$FINAL_NAME" > "${FINAL_NAME}.sha256"
echo "RELEASE_APK_NAME=$FINAL_NAME" >> $GITHUB_ENV
else
echo "ERROR: Release APK not found!" && exit 1
set -euo pipefail
shopt -s nullglob
release_dir=app/build/outputs/apk/release
candidates=("$release_dir"/app-*-release.apk "$release_dir"/app-release.apk)
if [[ ${#candidates[@]} -ne 1 ]]; then
echo "ERROR: expected exactly one release APK; found ${#candidates[@]}"
printf ' %s\n' "${candidates[@]}"
exit 1
fi

release_apk=${candidates[0]}
release_apk_name="FlowPilot-${GITHUB_REF_NAME}.apk"
release_apk_path="$release_dir/$release_apk_name"
mv "$release_apk" "$release_apk_path"
release_checksum_path="${release_apk_path}.sha256"
sha256sum "$release_apk_path" > "$release_checksum_path"
sha256sum --check "$release_checksum_path"
release_sha256=$(awk '{print $1}' "$release_checksum_path")

release_title="FlowPilot ${GITHUB_REF_NAME}"
release_notes_path="$RUNNER_TEMP/release-notes.md"
cat > "$release_notes_path" <<EOF
Signed Android APK release for FlowPilot.

---

## ✨ Highlights

- Signed release APK built from current \`main\` commit \`${RELEASE_COMMIT}\`.

## 🧪 Verification

- Required Android CI \`Build & Test\` completed successfully for this exact commit.
- Release workflow completed debug unit tests, debug lint, and signed APK assembly.

## 📥 Installation

1. Download \`${release_apk_name}\`.
2. Verify checksum before installation.
3. Install APK on Android 8.0 (API 26) or newer.

### SHA-256 Checksum

\`\`\`
SHA256 (${release_apk_name}) = ${release_sha256}
\`\`\`

## 📱 Compatibility

- Android 8.0+ (API 26–36).

---

## 🇹🇷 Türkçe Özet

İmzalı FlowPilot APK sürümü hazır. Kurulumdan önce SHA-256 sağlama toplamını doğrulayın.
EOF

for heading in '## ✨ Highlights' '## 🧪 Verification' '## 📥 Installation' '### SHA-256 Checksum' '## 📱 Compatibility' '## 🇹🇷 Türkçe Özet'; do
grep --fixed-strings --quiet "$heading" "$release_notes_path" || { echo "ERROR: release notes missing $heading"; exit 1; }
done
grep --fixed-strings --quiet "SHA256 (${release_apk_name}) = ${release_sha256}" "$release_notes_path" || { echo 'ERROR: release notes checksum mismatch'; exit 1; }
if grep --fixed-strings --line-regexp --quiet "$release_title" "$release_notes_path"; then
echo 'ERROR: release title must not be repeated in release notes'
exit 1
fi

{
echo "apk_path=$release_apk_path"
echo "checksum_path=$release_checksum_path"
echo "notes_path=$release_notes_path"
echo "release_title=$release_title"
} >> "$GITHUB_OUTPUT"

- name: Create GitHub Release & Upload APK
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
with:
files: |
app/build/outputs/apk/release/FlowPilot-*.apk
app/build/outputs/apk/release/FlowPilot-*.apk.sha256
generate_release_notes: true
${{ steps.release_assets.outputs.apk_path }}
${{ steps.release_assets.outputs.checksum_path }}
name: ${{ steps.release_assets.outputs.release_title }}
body_path: ${{ steps.release_assets.outputs.notes_path }}
draft: false
prerelease: false
env:
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ Changes completed after `1.0.2` and intended for the next release.

### Security

- Background NFC discovery now opens a confirmation gate before it can run matching automations; only foreground Android ReaderMode scans execute automatically.
- Webhooks pin initial TCP connections to prevalidated public IP addresses, preserve TLS hostname verification, reject unsafe rendered headers, and use bounded HTTP/1.1 parsing.
- Sensitive SMS and notification events are accepted only while the engine is enabled, bounded and freshness-limited, and reauthorized immediately before execution.
- Automatic rule runs now use durable execution leases and revision checks, preventing cooldown bypasses and revoking queued work after rule changes.
- Release workflow now requires a current `main` commit, exact successful CI, matching version tag, and protected signing environment before signing.
- Enabled Dependabot vulnerability alerts and security update pull requests.
- Enabled secret scanning, push protection, and private vulnerability reporting for the public repository.
- Protected `main`: pull requests, a current successful `Build & Test` check, and resolved review conversations are required; force-push and branch deletion are disabled.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ FlowPilot listens to a rich spectrum of hardware, radio, and system events:
- **Shake:** Firm shake detection with configurable sensitivity slider.
- **Ambient Light:** Lux drops below or rises above target threshold.
- 📍 **Hardware Geofencing:** Enter or exit defined geographical zones using Google Play Services `GeofencingClient`. Zero idle battery drain, up to 50 persistent queued events across engine restarts, and coordinate reuse for template variables.
- 🏷️ **NFC Tags:** Instant hex UID matching on physical tag scan.
- 🏷️ **NFC Tags:** Instant hex UID matching on physical scans. Foreground ReaderMode scans run matching rules automatically; background Android discovery opens FlowPilot and requires explicit confirmation before any matching NFC automation runs.
- 📞 **Phone & SMS:** Call ringing, answered, outgoing dialed, call ended; SMS received with keyword, prefix, regex, or exact sender matching.
- 🔔 **Notifications:** Incoming notifications from selected apps with keyword filtering.

Expand Down
2 changes: 1 addition & 1 deletion README.tr.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ FlowPilot zengin bir donanım, radyo ve sistem olayı yelpazesini dinler:
- **Sallama:** Hassasiyet ayarlı telefon sallama algılaması.
- **Ortam Işığı:** Lüks değerinin belirlenen sınırın altına düşmesi veya üstüne çıkması.
- 📍 **Donanım Coğrafi Çit (Geofence):** Google Play Services `GeofencingClient` ile belirlenen alana giriş/çıkış. Boşta sıfır pil tüketimi, yeniden başlatmada kaybolmayan 50 olaylık kalıcı kuyruk ve şablon değişkenlerinde doğrudan koordinat kullanımı.
- 🏷️ **NFC Etiketleri:** Fiziksel etiket okutulduğunda anında hex UID eşleşmesi.
- 🏷️ **NFC Etiketleri:** Fiziksel taramalarda anında hex UID eşleşmesi. Ön plandaki ReaderMode taramaları eşleşen kuralları otomatik çalıştırır; Android arka plan keşfi FlowPilot'ı açar ve eşleşen NFC otomasyonu çalışmadan önce açık onay ister.
- 📞 **Arama & SMS:** Gelen arama çalıyor, yanıtlandı, giden arama başladı, arama bitti; SMS gönderen numaraya ve kelime, önek veya regex kalıbına göre tetikleme.
- 🔔 **Bildirimler:** Seçili uygulamalardan gelen bildirimler ve anahtar kelime filtreleme.

Expand Down
2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
<intent-filter>
<action android:name="moe.shizuku.manager.intent.action.REQUEST_PERMISSION" />
</intent-filter>
<!-- NFC Tag Discovery: default activity launch when tag is tapped while app is not foreground -->
<!-- Background NFC dispatch only opens an explicit user-confirmation gate. -->
<intent-filter>
<action android:name="android.nfc.action.TAG_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
Expand Down
Loading
Loading