Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
337fc1f
chore: upgrade dependencies and fix edge-to-edge
pasichDev Aug 31, 2026
86802dd
fix: apply system insets to edge-to-edge screens
pasichDev Aug 31, 2026
2569066
feat: add Google Credential Manager authentication
pasichDev Aug 31, 2026
124335e
fix: use stable Google credential identity
pasichDev Aug 31, 2026
487c125
feat: add deterministic sync merge core
pasichDev Aug 31, 2026
27988af
feat: persist sync record metadata
pasichDev Aug 31, 2026
c5744cf
feat: add provider-independent sync service
pasichDev Aug 31, 2026
9e7f174
build: connect Firebase authentication project
pasichDev Aug 31, 2026
7b27c35
feat: complete Google Drive sync flow and profile UI
pasichDev Aug 31, 2026
68d08c5
feat: localize Google sync messages
pasichDev Aug 31, 2026
b353b52
feat: track sync metadata for local mutations
pasichDev Aug 31, 2026
6768e43
feat: complete sync hardening and rollout guardrails
pasichDev Aug 31, 2026
9a6ce51
test: stabilize Room migration verification
pasichDev Aug 31, 2026
d3ddcfe
style: format migration test
pasichDev Aug 31, 2026
0a2aeb9
fix: persist sync state atomically in Room
pasichDev Aug 31, 2026
ddbdc25
chore: keep local docs out of the repository
pasichDev Aug 31, 2026
d63f847
fix: keep settings tabs fixed
pasichDev Aug 31, 2026
43e816c
build: keep the project buildable and runnable without Firebase
pasichDev Aug 31, 2026
22bd2d5
fix(sync): use the Drive v3 version counter instead of an ETag
pasichDev Aug 31, 2026
ba11764
fix(sync): keep Room off the main thread
pasichDev Aug 31, 2026
fd6a205
fix(sync): stop a started sync from erasing the last success time
pasichDev Aug 31, 2026
8cb6e8a
fix(sync): never crash when a sync outlives its screen
pasichDev Aug 31, 2026
11865d4
feat(sync): give the account and sync controls their own tab
pasichDev Aug 31, 2026
8077f74
fix: clear every lint error
pasichDev Aug 31, 2026
1c0843e
test(sync): cover convergence between two devices
pasichDev Aug 31, 2026
452fee7
ci: restore google-services.json from a repository secret
pasichDev Aug 31, 2026
b69fad9
fix(sync): harden first sync safeguards
pasichDev Sep 1, 2026
8301d28
release: prepare 2.6.47
pasichDev Sep 1, 2026
ffca27e
fix ci
pasichDev Sep 1, 2026
8636ce2
fix ci2
pasichDev Sep 1, 2026
7fc0b78
ci: publish tagged releases to Play closed testing
pasichDev Sep 1, 2026
0cf8d84
fix: preserve concurrent Drive sync snapshots
pasichDev Sep 1, 2026
cccfb94
fix: reduce redundant and metered background sync
pasichDev Sep 1, 2026
9efa7cc
chore: measure coverage and document optional sync
pasichDev Sep 1, 2026
9dbc626
chore: prepare version 2.6.48
pasichDev Sep 1, 2026
b8a5e51
style: format sync safety changes
pasichDev Sep 1, 2026
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
147 changes: 106 additions & 41 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
name: Release — build & publish

on:
pull_request:
types: [closed]
branches: [master]
paths-ignore:
- '**/*.md'
- 'docs/**'
push:
tags:
- 'v*'

jobs:
release:
name: Build signed APK/AAB & publish
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write

steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Validate release tag
run: |
git fetch origin master --no-tags
git merge-base --is-ancestor "$GITHUB_SHA" origin/master
VERSION=$(sed -nE 's/^def appVersionName = "([^"]+)"$/\1/p' app/build.gradle)
test -n "$VERSION"
test "$GITHUB_REF_NAME" = "v$VERSION"

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
Expand Down Expand Up @@ -49,12 +54,19 @@ jobs:
- name: Make gradlew executable
run: chmod +x ./gradlew

- name: Extract version
id: version
- name: Decode google-services.json
env:
GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }}
run: |
CODE=$(grep -oP '(?<=def appVersionCode = )\d+' app/build.gradle | head -n1)
echo "code=$CODE" >> $GITHUB_OUTPUT
echo "name=2.6.$CODE" >> $GITHUB_OUTPUT
# Kept out of the repository and injected here. A fork or a pull request from a fork
# gets no secrets, and the build is deliberately able to run without the file: sign-in
# and Drive sync are simply disabled in that case.
if [ -n "$GOOGLE_SERVICES_JSON" ]; then
echo "$GOOGLE_SERVICES_JSON" | base64 -d > app/google-services.json
echo "google-services.json restored from the repository secret."
else
echo "GOOGLE_SERVICES_JSON is not available; building without Firebase."
fi

- name: Decode keystore
run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > /tmp/release.jks
Expand All @@ -71,49 +83,102 @@ jobs:
run: |
mkdir -p artifacts
cp app/build/outputs/apk/release/*.apk \
"artifacts/MyNotes-v${{ steps.version.outputs.name }}.apk"
"artifacts/MyNotes-${{ github.ref_name }}.apk"
cp app/build/outputs/bundle/release/*.aab \
"artifacts/MyNotes-v${{ steps.version.outputs.name }}.aab"
"artifacts/MyNotes-${{ github.ref_name }}.aab"

- name: Generate release notes from commits
- name: Generate English release notes from changelog
id: notes
run: |
PREV=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
RANGE=${PREV:+"$PREV..HEAD"}
VERSION="${GITHUB_REF_NAME#v}"
awk -v version="$VERSION" '
$0 == "## [" version "]" || index($0, "## [" version "] -") == 1 { in_version = 1; next }
in_version && /^## \[/ { exit }
in_version { print }
' CHANGELOG.md > release-notes.md
test -s release-notes.md

mkdir -p distribution/whatsnew
python3 - <<'PY'
from pathlib import Path
import re

sections = []
section = None
bullet = None
for raw_line in Path("release-notes.md").read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line:
continue
heading = re.fullmatch(r"\*\*(.+?)\*\*", line)
if heading:
section = heading.group(1)
continue
if line.startswith("- "):
bullet = [line[2:]]
sections.append((section, bullet))
elif bullet is not None:
bullet.append(line)

def shorten(text, limit=140):
text = re.sub(r"\*\*(.*?)\*\*", r"\1", " ".join(text))
text = re.sub(r"\s+", " ", text).strip()
if len(text) <= limit:
return text
return text[: limit - 1].rsplit(" ", 1)[0] + "…"

seen_sections = set()
highlights = []
for category, lines in sections:
category = category or "Updates"
if category in seen_sections:
continue
seen_sections.add(category)
highlights.append(f"• {category} — {shorten(lines)}")

selected = []
for highlight in highlights:
candidate = "\n".join(selected + [highlight])
if len(candidate) > 500:
break
selected.append(highlight)
Path("distribution/whatsnew/whatsnew-en-US").write_text(
"\n".join(selected), encoding="utf-8"
)
PY

{
echo 'notes<<EOF'
git log ${RANGE} --no-merges --format='### %s%n%n%b' | \
awk '/^$/{if(p)print ""; p=0; next} {p=1; print}'
cat release-notes.md
echo 'EOF'
} >> "$GITHUB_OUTPUT"

- name: Authenticate to Google Cloud
id: google-auth
uses: google-github-actions/auth@v3
with:
project_id: ${{ vars.GOOGLE_CLOUD_PROJECT_ID }}
workload_identity_provider: ${{ vars.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ vars.GOOGLE_PLAY_SERVICE_ACCOUNT_EMAIL }}
create_credentials_file: true

- name: Publish AAB to Google Play closed testing
uses: r0adkll/upload-google-play@v1.1.5
with:
serviceAccountJson: ${{ steps.google-auth.outputs.credentials_file_path }}
packageName: com.pasich.mynotes
releaseFiles: artifacts/MyNotes-${{ github.ref_name }}.aab
tracks: ${{ vars.GOOGLE_PLAY_CLOSED_TRACK }}
status: completed
whatsNewDirectory: distribution/whatsnew

- name: Create GitHub Release
uses: ncipollo/release-action@v1
with:
tag: v${{ steps.version.outputs.name }}
name: "MyNotes v${{ steps.version.outputs.name }}"
tag: ${{ github.ref_name }}
name: "MyNotes ${{ github.ref_name }}"
body: ${{ steps.notes.outputs.notes }}
artifacts: "artifacts/*"
artifactErrorsFailBuild: true
makeLatest: true
token: ${{ secrets.GITHUB_TOKEN }}

# - name: Generate Play Store release notes
# run: |
# mkdir -p distribution/whatsnew
# PREV=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
# RANGE=${PREV:+"$PREV..HEAD"}
# git log ${RANGE} --no-merges --format='• %s' \
# | head -20 \
# | head -c 490 \
# > distribution/whatsnew/whatsnew-en-US

# - name: Publish to Google Play (Open Testing)
# uses: r0adkll/upload-google-play@v3
# with:
# serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
# packageName: com.pasich.mynotes
# releaseFiles: artifacts/MyNotes-v${{ steps.version.outputs.name }}.aab
# track: beta
# status: completed
# whatsNewDirectory: distribution/whatsnew
87 changes: 73 additions & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,47 +1,106 @@
name: CI — build check
name: CI — build and quality checks

on:
pull_request:
types: [opened, synchronize, reopened]

jobs:
editor:
name: Build notes editor and audit dependencies
runs-on: ubuntu-latest

defaults:
run:
working-directory: notes_editor

steps:
- name: Checkout
uses: actions/checkout@v6

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
cache-dependency-path: notes_editor/package-lock.json

- name: Install editor dependencies
run: npm ci

- name: Build editor bundle
run: npm run build

- name: Audit dependencies
run: npm audit --audit-level=high

build:
name: Build debug & run tests
name: Android build, tests, lint, and formatting
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6

- name: Set up JDK 17
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: '17'

- name: Cache Gradle
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: ${{ runner.os }}-gradle-

- name: Accept SDK licenses & install platform
- name: Set up Android SDK
uses: android-actions/setup-android@v4
with:
log-accepted-android-sdk-licenses: 'false'

- name: Install Android platform
run: |
yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null 2>&1
$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager \
sdkmanager \
--channel=3 \
"platforms;android-37" \
"build-tools;35.0.0" \
> /dev/null 2>&1 || true
"platforms;android-37.2" \
"build-tools;35.0.0"

- name: Make gradlew executable
run: chmod +x ./gradlew

- name: Decode google-services.json
env:
GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }}
run: |
# Kept out of the repository and injected here. A fork or a pull request from a fork
# gets no secrets, and the build is deliberately able to run without the file: sign-in
# and Drive sync are simply disabled in that case.
if [ -n "$GOOGLE_SERVICES_JSON" ]; then
echo "$GOOGLE_SERVICES_JSON" | base64 -d > app/google-services.json
echo "google-services.json restored from the repository secret."
else
echo "GOOGLE_SERVICES_JSON is not available; building without Firebase."
fi

- name: Build debug
run: ./gradlew assembleDebug --no-daemon
run: ./gradlew :app:assembleDebug --no-daemon --stacktrace

- name: Run unit tests and generate coverage
run: ./gradlew :app:testDebugUnitTest :app:createDebugUnitTestCoverageReport --no-daemon --stacktrace

- name: Upload debug unit-test coverage
if: always()
uses: actions/upload-artifact@v4
with:
name: debug-unit-test-coverage
path: app/build/reports/coverage/
if-no-files-found: error

- name: Run lint
run: ./gradlew :app:lintDebug --no-daemon --stacktrace

- name: Run unit tests
run: ./gradlew test --no-daemon
- name: Check Java formatting
run: ./gradlew :app:spotlessCheck --no-daemon --stacktrace
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,9 @@ local.properties
## Changelog.md
app/src/main/res/raw/*
!app/src/main/res/raw/keep.xml
# Firebase project configuration (local/variant-specific)
app/google-services.json
gha-creds-*.json

# Local design/plan notes, deliberately kept out of the repository
docs/
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,47 @@
# CHANGELOG

## [2.6.48] - 01.09.2026

**Improvements**

- **Safer Google Drive sync:** Sync snapshots are now published as immutable files and merged
deterministically, so a concurrent device update cannot overwrite another device's data.
- **Efficient background sync:** Periodic sync now runs only on unmetered networks when the battery
is not low, and unchanged data no longer creates an extra Drive snapshot.
- **Quality visibility:** Added JaCoCo unit-test coverage reports to CI for every pull request.

**Fixes**

- Sync now requires explicit first-sync confirmation in the coordinator itself, preventing any
caller from bypassing the data-upload review.
- Fixed updates to an existing Drive sync bundle on Android/JDK configurations that reject HTTP
PATCH requests.

## [2.6.47] - 01.09.2026

**New**

- **Google Drive sync:** Optionally keep notes, tasks, tags, preferences, and attachments in sync
across devices while continuing to work offline. Your data is merged safely before a sync is
published, and the first sync clearly explains what may be uploaded.
- **Your data:** Added an Account tab with Google sign-in, sync status, a manual sync action, and
an optional background-sync switch. Backup, export, and import remain available in their own
tabs.

**Improvements**

- Attachments are deduplicated and verified during sync, reducing unnecessary uploads while
protecting file integrity.
- The app now remains fully usable when Google services are unavailable or when you choose not to
sign in.
- Updated translations across all supported languages for the new sync and account experience.

**Fixes**

- Fixed several sync stability issues, including leaving the screen during an active sync and
preserving the time of the last successful sync.
- Fixed Google sign-in compatibility on Android 8.0 and 8.1.

## [2.6.46] - 18.05.2026

**New**
Expand Down
Loading
Loading