mirror of
git://f0xx.org/android_cast
synced 2026-07-29 05:37:52 +03:00
git flow, infra descriptions, cast flow changes (mobile side)
This commit is contained in:
384
docs/GIT_FLOW.md
Normal file
384
docs/GIT_FLOW.md
Normal file
@@ -0,0 +1,384 @@
|
||||
# Android Cast — Git flow (green `master`)
|
||||
|
||||
**Version:** 1.0 · **Last updated:** 2026-05-21
|
||||
**PDF:** regenerate with `python3 docs/_pdf_build/build_git_flow_pdf.py` → `docs/GIT_FLOW.pdf`
|
||||
|
||||
This document defines how we develop, integrate, release, and hotfix. **Cursor agents and humans follow the same rules.**
|
||||
|
||||
---
|
||||
|
||||
## 1. Goals
|
||||
|
||||
| Goal | How |
|
||||
|------|-----|
|
||||
| **Always shippable `master`** | Only merges that passed CI/tests; tagged releases point here |
|
||||
| **Safe integration** | Day-to-day work lands on `next` first |
|
||||
| **Simple mental model** | Two long-lived branches + short-lived topic branches |
|
||||
| **Recoverable hotfixes** | Production fixes branch from what users actually run |
|
||||
|
||||
---
|
||||
|
||||
## 2. Branches
|
||||
|
||||
| Branch | Role | Direct commits? |
|
||||
|--------|------|-----------------|
|
||||
| **`master`** | Production line; **green** (build + tests pass); release tags (`v0.1.2`) | **No** — merge only |
|
||||
| **`next`** | Integration / release candidate; absorbs features until a release is approved | **No** — merge only |
|
||||
| **`feature/*`**, **`fix/*`**, **`topic/*`** | Short-lived work; fork **`next`** | Yes (normal dev) |
|
||||
| **`hotfix/*`** | Urgent fix for **released** code | Yes; merge target **`master`** first |
|
||||
|
||||
**Naming:** `feature/cast-resolution-presets`, `fix/crash-upload-500`, `hotfix/v0.1.1-pin-crash`.
|
||||
|
||||
If `next` does not exist yet on the remote:
|
||||
|
||||
```bash
|
||||
git checkout master
|
||||
git pull
|
||||
git checkout -b next
|
||||
git push -u origin next
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Branch diagram (steady state)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph long["Long-lived"]
|
||||
M[master\nshippable + tagged]
|
||||
N[next\nintegration]
|
||||
end
|
||||
F[feature/*\nfix/*]
|
||||
H[hotfix/*]
|
||||
|
||||
F -->|PR / merge after CI| N
|
||||
N -->|release merge after CI| M
|
||||
H -->|merge after CI| M
|
||||
M -->|sync after release or hotfix| N
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Daily development
|
||||
|
||||
1. **Start from `next`:** `git fetch && git checkout next && git pull`
|
||||
2. **Create topic branch:** `git checkout -b feature/my-change`
|
||||
3. **Commit** in small logical chunks on the topic branch.
|
||||
4. **Before merge:** run project checks (Android: `./gradlew :app:testDebugUnitTest` or CI equivalent; backend: relevant tests/lint).
|
||||
5. **Merge to `next`:** prefer merge commit or squash per team habit; **do not** merge unfinished work.
|
||||
6. **Delete** topic branch after merge (local + remote).
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Dev as Developer / agent
|
||||
participant F as feature/*
|
||||
participant N as next
|
||||
participant CI as CI / local tests
|
||||
|
||||
Dev->>F: branch from next
|
||||
Dev->>F: commits
|
||||
Dev->>CI: test
|
||||
CI-->>Dev: green
|
||||
Dev->>N: merge feature → next
|
||||
Dev->>F: delete branch
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Release: `next` → `master`
|
||||
|
||||
**When:** `next` is feature-complete for the version, soak/QA agreed, CI green.
|
||||
|
||||
1. Ensure **`next`** is up to date and **all tests pass**.
|
||||
2. **Merge `next` → `master`** (merge commit recommended for traceability).
|
||||
3. On **`master`:** final smoke / CI.
|
||||
4. **Tag** on `master`: `git tag -a v0.1.2 -m "Release v0.1.2"` then `git push origin master --tags`.
|
||||
5. **Sync `next` with `master`** so both lines share the release commit and any last-minute release fixes:
|
||||
|
||||
```bash
|
||||
git checkout next
|
||||
git merge master # fast-forward or merge commit
|
||||
git push origin next
|
||||
```
|
||||
|
||||
6. Continue new work: branch **`feature/*` from `next`** again.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant N as next
|
||||
participant M as master
|
||||
participant T as tag v0.1.x
|
||||
|
||||
Note over N: CI green, QA ok
|
||||
N->>M: merge next → master
|
||||
M->>M: CI / smoke
|
||||
M->>T: annotated tag
|
||||
M->>N: merge master → next (sync)
|
||||
```
|
||||
|
||||
**Versioning:** tags on `master` only (`vMAJOR.MINOR.PATCH`). Bump per [SemVer](https://semver.org/) conventions you adopt for the app.
|
||||
|
||||
---
|
||||
|
||||
## 6. Hotfixes — chosen strategy
|
||||
|
||||
### Default: **hotfix from `master` (option 1)** ✓
|
||||
|
||||
Use when the bug affects **what is already released** (code on `master` / latest tag).
|
||||
|
||||
1. `git checkout master && git pull`
|
||||
2. `git checkout -b hotfix/v0.1.2-crash-on-stop`
|
||||
3. Fix + tests
|
||||
4. Merge **`hotfix/*` → `master`**, CI green
|
||||
5. **Tag** patch on `master`: e.g. `v0.1.3`
|
||||
6. **`git checkout next && git merge master`** — bring hotfix into integration (resolve conflicts if `next` diverged)
|
||||
7. Push `master`, `next`, tags; delete `hotfix/*`
|
||||
|
||||
**Why not hotfix only on `next` (option 2)?**
|
||||
`next` often contains unreleased features. Fixing there delays shipping the patch and risks dragging unrelated commits into production when you later merge `next` → `master`.
|
||||
|
||||
### Exception: fix only on `next` (option 2)
|
||||
|
||||
Use when the bug **does not exist on `master`** (introduced on `next` only).
|
||||
|
||||
1. `git checkout -b fix/...` from **`next`**
|
||||
2. Merge to **`next`** only
|
||||
3. No tag until the normal **release** merge `next` → `master`
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Q{BUG IN RELEASED\nmaster / tag?}
|
||||
Q -->|Yes| H[hotfix/* from master]
|
||||
H --> M[merge → master]
|
||||
M --> T[tag patch on master]
|
||||
T --> S[merge master → next]
|
||||
Q -->|No, next only| F[fix/* from next]
|
||||
F --> N[merge → next]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Protection rules (CI + policy)
|
||||
|
||||
| Rule | `master` | `next` | topic branches |
|
||||
|------|----------|--------|----------------|
|
||||
| Required CI (build + tests) before merge | ✓ | ✓ | ✓ (before PR/merge) |
|
||||
| No direct pushes of WIP | ✓ | ✓ | — |
|
||||
| Only maintainers merge to `master` | ✓ | optional | — |
|
||||
| Tag only on `master` | ✓ | — | — |
|
||||
|
||||
See **[§7.1 How to protect branches](#71-how-to-protect-branches-not-local-hooks-alone)** below.
|
||||
|
||||
### 7.1 How to protect branches (not local hooks alone)
|
||||
|
||||
**Short answer:** use **server-side** rules on `f0xx.org` (or whatever hosts `origin`). **Local git hooks** on your laptop are optional helpers only — they do not stop you or someone else from pushing bad history from another machine.
|
||||
|
||||
| Layer | Where it runs | Stops bad pushes? | Good for |
|
||||
|-------|----------------|-------------------|----------|
|
||||
| **Hosting UI / branch protection** | Git server (GitHub, GitLab, Gitea, …) | **Yes** | Most teams; use this if available |
|
||||
| **Server git hooks** (`pre-receive` / `update`) | Bare repo on server | **Yes** | Self-hosted `git://` / SSH without a web UI |
|
||||
| **Local hooks** (`.git/hooks/pre-push`) | Your PC only | **No** (easy to skip) | Personal reminders, format/tests before push |
|
||||
| **CI** (build on push/PR) | CI runner | Indirect | Prove tests passed; pair with “required check” on server |
|
||||
|
||||
#### What to enable on `master` and `next`
|
||||
|
||||
1. **No force-push** (no `git push --force` rewriting history).
|
||||
2. **No direct push** (optional but strong): changes only via merge from `feature/*` or from PR/MR — you merge locally or on the web, but the server rejects `git push origin master` with a normal commit on top without review (exact option name varies).
|
||||
3. **Require CI green** before merge (if you have Actions/GitLab CI/Gitea Actions).
|
||||
4. **Restrict who can push** to you (and CI deploy key if needed).
|
||||
|
||||
`feature/*` branches stay **unprotected** — push freely.
|
||||
|
||||
#### If your host has a web UI (Gitea / GitLab / GitHub)
|
||||
|
||||
Look for **Branch protection** / **Protected branches**:
|
||||
|
||||
- Add rules for `master` and `next`.
|
||||
- Enable: prevent force-push, require pull request (optional), require status checks.
|
||||
|
||||
No git internals required — the server rejects illegal pushes.
|
||||
|
||||
#### If you only have bare git on a server (`git://` / SSH)
|
||||
|
||||
Ask whoever administers `git://f0xx.org/android_cast` to install **server-side hooks** in the **bare repository** (not in your clone):
|
||||
|
||||
- `hooks/pre-receive` or `hooks/update` — script receives branch name + old/new SHA; **exit 1** to reject (e.g. block direct push to `refs/heads/master` unless user is deploy; block all force-push to `master`/`next`).
|
||||
|
||||
Example policy in words: “reject push if ref is `master` or `next` and user is not in allowlist” or “reject non-fast-forward updates to `master`/`next`”.
|
||||
|
||||
Local clone hooks live in `.git/hooks/` and are **not copied** when others clone — so they are **not** team protection.
|
||||
|
||||
#### Local hooks (optional, for you only)
|
||||
|
||||
Useful on your machine, not security:
|
||||
|
||||
```bash
|
||||
# .git/hooks/pre-push (chmod +x) — example: warn before pushing to master
|
||||
protected_regex='^(refs/heads/)?(master|next)$'
|
||||
while read local_ref local_sha remote_ref remote_sha; do
|
||||
if echo "$remote_ref" | grep -qE "$protected_regex"; then
|
||||
echo "Blocked: push to master/next — use feature branch + merge (see docs/GIT_FLOW.md)"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
Skip with `git push --no-verify` — another reason server rules matter.
|
||||
|
||||
#### CI without branch UI
|
||||
|
||||
Run tests on every push; you still **choose** not to merge until green. Branch protection + “required check” automates that on modern hosts.
|
||||
|
||||
---
|
||||
|
||||
## 8. Sync cheat sheet
|
||||
|
||||
| Event | Action |
|
||||
|-------|--------|
|
||||
| After **release** (`next` → `master` + tag) | `git checkout next && git merge master && git push` |
|
||||
| After **hotfix** to `master` + tag | Same: **merge `master` → `next`** |
|
||||
| Feature finished | merge **topic → `next`** only |
|
||||
| Started wrong base | rebase topic onto latest `next` (coordinate if already shared) |
|
||||
|
||||
---
|
||||
|
||||
## 9. Agent / Cursor checklist
|
||||
|
||||
Agents working in this repo **must**:
|
||||
|
||||
1. **Never** commit or push directly to `master` or `next` unless the user explicitly asks for that merge/release step.
|
||||
2. **Branch from `next`** for features and non-production fixes (`feature/*`, `fix/*`).
|
||||
3. **Run tests** (or state clearly if not run) before recommending merge to `next`.
|
||||
4. **Releases:** only describe merging `next` → `master` and tagging after user confirms QA.
|
||||
5. **Hotfixes on released code:** branch from `master`, merge back to `master`, tag, then **merge `master` into `next`**.
|
||||
6. **Set active branch** metadata when using a named topic branch (project convention).
|
||||
7. **Do not** force-push `master` / `next`.
|
||||
|
||||
---
|
||||
|
||||
## 10. Comparison to classic Git Flow
|
||||
|
||||
| Git Flow | This repo |
|
||||
|----------|-----------|
|
||||
| `main` | `master` (green, tagged) |
|
||||
| `develop` | `next` |
|
||||
| `feature/*` | same, base = `next` |
|
||||
| `release/*` | optional short branch; default = test on `next` then merge to `master` |
|
||||
| `hotfix/*` | from **`master`**, merge to **`master`**, sync to **`next`** |
|
||||
|
||||
We intentionally avoid a permanently divergent `develop` without a green production branch: **`master` is always releasable.**
|
||||
|
||||
---
|
||||
|
||||
## 11. First-time setup (maintainer)
|
||||
|
||||
```bash
|
||||
# Ensure next exists and tracks origin
|
||||
git checkout master && git pull
|
||||
git checkout -b next 2>/dev/null || git checkout next
|
||||
git push -u origin next
|
||||
|
||||
# Optional: protect branches on hosting (GitHub example)
|
||||
# gh api repos/{owner}/{repo}/branches/master/protection ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Private primary + GitHub publish (dual remote)
|
||||
|
||||
**Canonical repo:** your server — today `origin` → `git://f0xx.org/android_cast`.
|
||||
**Daily work:** always `git push` / `git pull` against **`origin`** first.
|
||||
**GitHub:** secondary remote for visibility, issues, and (optional) GitHub Actions — not the source of truth unless you decide otherwise.
|
||||
|
||||
### 12.1 Add GitHub without replacing origin
|
||||
|
||||
```bash
|
||||
# Create empty repo on GitHub (no README if you already have history)
|
||||
git remote add github git@github.com:YOUR_USER/android-cast.git
|
||||
# origin stays: git://f0xx.org/android_cast
|
||||
git remote -v
|
||||
```
|
||||
|
||||
Keep **`origin`** = private server. Name the public remote **`github`** (or `publish`) so commands stay obvious.
|
||||
|
||||
### 12.2 What to push where
|
||||
|
||||
| Ref | Private `origin` | GitHub `github` |
|
||||
|-----|------------------|-----------------|
|
||||
| `feature/*`, WIP | ✓ always | optional (omit for cleaner public) |
|
||||
| `next` | ✓ always | your choice: public integration or keep private |
|
||||
| `master` + tags | ✓ always | ✓ publish releases you want visible |
|
||||
| Secrets, `local.properties`, logs | never in either | never |
|
||||
|
||||
**Typical OSS-style:** push everything to **`origin`**; push only **`master`** and **tags** to **`github`** until you are ready to open `next`.
|
||||
|
||||
```bash
|
||||
# After merge to master on private server
|
||||
git push origin master --tags
|
||||
git push github master --tags
|
||||
```
|
||||
|
||||
Push `next` to GitHub only when you want public integration:
|
||||
|
||||
```bash
|
||||
git push origin next
|
||||
git push github next
|
||||
```
|
||||
|
||||
### 12.3 Daily habit
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git checkout next && git pull origin next
|
||||
# ... work on feature/foo ...
|
||||
git push -u origin feature/foo
|
||||
# merge to next on origin (PR or local merge + push)
|
||||
git push origin next
|
||||
|
||||
# When ready to publish a release line to GitHub
|
||||
git push github master --tags
|
||||
```
|
||||
|
||||
Set upstream on topic branches to **origin**, not github:
|
||||
|
||||
```bash
|
||||
git push -u origin feature/my-change
|
||||
```
|
||||
|
||||
### 12.4 Where branch protection lives
|
||||
|
||||
| Server | Protect |
|
||||
|--------|---------|
|
||||
| **f0xx.org (`origin`)** | `master`, `next` — server hooks or admin UI (primary enforcement) |
|
||||
| **GitHub (`github`)** | `master` (and `next` if public) — Settings → Branch protection |
|
||||
|
||||
GitHub rules only affect pushes **to GitHub**. They do not protect your private server unless you also configure **`origin`**.
|
||||
|
||||
### 12.5 One-way mirror (optional automation)
|
||||
|
||||
On the private server (cron or post-receive hook), after a successful push to `origin`:
|
||||
|
||||
```bash
|
||||
git push --prune github +refs/heads/master:refs/heads/master +refs/tags/*:refs/tags/*
|
||||
```
|
||||
|
||||
Or use GitHub’s “mirror repository” importers only for initial import; ongoing sync is usually `git push github …` from CI or hook.
|
||||
|
||||
Avoid `git push github --mirror` unless GitHub is a full duplicate of **private** history — it copies all branches and deletes remote branches you deleted locally.
|
||||
|
||||
### 12.6 Suggested order of operations (your situation)
|
||||
|
||||
1. On **f0xx.org:** create **`next`** from `master`, push `origin next`.
|
||||
2. On **f0xx.org:** enable server protection for `master` / `next` (admin).
|
||||
3. Create **GitHub** repo; `git remote add github …`.
|
||||
4. First publish: `git push github master` and tags you want public.
|
||||
5. Enable **GitHub branch protection** on `master` (block force-push).
|
||||
6. Continue feature flow on **`origin`** only; publish to GitHub when you merge/tag on `master`.
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [ROADMAP.md](ROADMAP.md) — product tracks
|
||||
- [.cursor/rules/git-flow.mdc](../.cursor/rules/git-flow.mdc) — agent enforcement
|
||||
- `docs/_pdf_build/build_git_flow_pdf.py` — PDF generator
|
||||
200
docs/GIT_FLOW.pdf
Normal file
200
docs/GIT_FLOW.pdf
Normal file
File diff suppressed because one or more lines are too long
76
docs/OPEN_API.md
Normal file
76
docs/OPEN_API.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Crash console — open ingest API (design)
|
||||
|
||||
Triage **real device crashes** vs **synthetic / bulk test traffic** when exposing upload or query APIs beyond the Android app. Builds on RBAC ([`examples/crash_reporter/backend/README.md`](../examples/crash_reporter/backend/README.md)).
|
||||
|
||||
## Problems
|
||||
|
||||
- Load tests and CI may POST many similar JSON blobs → drown signal in the reports list.
|
||||
- Third-party integrations need stable auth and filters without console login cookies.
|
||||
- Similarity grouping already uses payload fingerprints; need **provenance** to exclude known harness traffic.
|
||||
|
||||
## Payload extension (ingest)
|
||||
|
||||
Add optional top-level fields on upload (backward compatible):
|
||||
|
||||
| Field | Type | Meaning |
|
||||
|-------|------|---------|
|
||||
| `ingest.source` | string | `device` (default), `open_api`, `ci`, `manual` |
|
||||
| `ingest.client_id` | string | Stable id per API key or CI job |
|
||||
| `ingest.run_id` | string | Correlates a burst of reports from one test run |
|
||||
|
||||
Android app omits these (defaults to `device`). Harness sets `open_api` + `run_id`.
|
||||
|
||||
Store in DB:
|
||||
|
||||
- Column `reports.ingest_source` (indexed) **or** JSON path in `payload_json` with generated column later.
|
||||
- Prefer column for list filters and MariaDB permissions story.
|
||||
|
||||
## Upload API (`POST …/api/upload.php`)
|
||||
|
||||
Phases:
|
||||
|
||||
1. **Phase A (now):** Accept extra JSON fields; persist inside `payload_json`; console search includes `ingest.source` in blob.
|
||||
2. **Phase B:** API key header `X-Crash-Api-Key` → maps to `company_id` (RBAC); rate limit per key.
|
||||
3. **Phase C:** Reject or quarantine when `ingest.source=open_api` and fingerprint matches org blocklist.
|
||||
|
||||
## Console / query API (read)
|
||||
|
||||
Thin JSON endpoints (session or API key):
|
||||
|
||||
| Endpoint | Purpose |
|
||||
|----------|---------|
|
||||
| `GET /api/reports?ingest_source=device` | Default triage view |
|
||||
| `GET /api/reports?ingest_source_ne=open_api` | Hide harness |
|
||||
| `GET /api/reports?group=fingerprint&min_count=N` | Bulk duplicate detector |
|
||||
| `GET /api/reports/{id}` | Detail (existing shape) |
|
||||
|
||||
UI: filter chips on reports toolbar — **Devices only** / **Include API traffic** / tag `synthetic`.
|
||||
|
||||
## Classifying synthetic traffic (heuristics)
|
||||
|
||||
Mark report or tag `synthetic` when any:
|
||||
|
||||
- `ingest.source` in (`open_api`, `ci`) **and** user has not promoted it.
|
||||
- Same `ingest.run_id` + fingerprint count > threshold in 10 minutes.
|
||||
- Identical `report_id` prefix pattern from known test fixture.
|
||||
|
||||
Manual: console **Clear synthetic tag** / **Promote to real** on detail page.
|
||||
|
||||
## Security
|
||||
|
||||
- API keys scoped to `company_id`; no cross-tenant list.
|
||||
- Separate write-only keys for CI upload vs read keys for dashboards.
|
||||
- Log upload failures to existing crash event log; no stack traces in response body.
|
||||
|
||||
## Android
|
||||
|
||||
No change required for v1. Optional later: dev setting to set `ingest.source=manual` for dogfood builds.
|
||||
|
||||
## Implementation order
|
||||
|
||||
1. Document fields; parse and store in payload (Phase A).
|
||||
2. List filter `ingest_source` in `ReportRepository` + UI chip.
|
||||
3. API keys table + `X-Crash-Api-Key` on upload.
|
||||
4. Grouped bulk endpoint for duplicate review.
|
||||
|
||||
See also [CRASH_REPORTER.md](CRASH_REPORTER.md) and `examples/crash_reporter/ROADMAP.md`.
|
||||
77
docs/PRO_AAR.md
Normal file
77
docs/PRO_AAR.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Pro / OSS split (cast-core + cast-pro AAR)
|
||||
|
||||
Design notes for a commercial build without forking the cast protocol. **Not implemented yet** — complements [COMMERCIAL.md](COMMERCIAL.md) (dev-gated Play Billing placeholders).
|
||||
|
||||
## Goals
|
||||
|
||||
- **OSS (`cast-core`)** — protocol, UDP/TCP transport, receiver, sender engine, crash reporter client, settings model. Publishable repo for trust and forks.
|
||||
- **Closed (`cast-pro` AAR)** — entitlements, license check, optional premium codecs/UI, Play Billing glue. Distributed via private Maven or Play App Bundle module (not on public Maven).
|
||||
- **Single APK** on Play: `app` depends on `cast-core` + `cast-pro` at compile time; F-Droid / OSS build uses `cast-core` only with stub entitlements.
|
||||
|
||||
## Entitlements API (in core)
|
||||
|
||||
Keep a thin interface in core so free code never imports Play Billing:
|
||||
|
||||
```java
|
||||
public interface CastEntitlements {
|
||||
boolean isPro(Context ctx);
|
||||
int maxReceivers();
|
||||
boolean allowsResolution(CastSettings.Resolution r);
|
||||
boolean allowsStreamProtection();
|
||||
}
|
||||
```
|
||||
|
||||
- **Default (OSS):** `FreeCastEntitlements` — e.g. max 1 receiver, cap outgoing long edge at 480 unless dev override, FEC/NACK off or dev-only.
|
||||
- **Pro:** `ProCastEntitlements` in AAR — reads signed license / Play purchase cache, returns higher limits.
|
||||
|
||||
Call sites (minimal first pass):
|
||||
|
||||
| Area | Gate |
|
||||
|------|------|
|
||||
| `CastTuningEngine` / sender encode | `allowsResolution()` |
|
||||
| Multi-receiver connect | `maxReceivers()` |
|
||||
| Stream protection UI | `allowsStreamProtection()` |
|
||||
| Developer resolution UI modes | Always allowed when `BuildConfig.DEBUG` or `AppPreferences` dev flags |
|
||||
|
||||
`CommercialFeatures` stays **developer toggles** for ads/IAP/Play review; **pro entitlement** is runtime product state from Billing + AAR.
|
||||
|
||||
## Module layout (Gradle)
|
||||
|
||||
```
|
||||
cast-core/ # android library, published OSS
|
||||
cast-pro/ # android library, proprietary, implementation project OR maven coords
|
||||
app/ # application
|
||||
```
|
||||
|
||||
`app/build.gradle`:
|
||||
|
||||
- `implementation project(':cast-core')`
|
||||
- `implementation 'com.foxx.androidcast:cast-pro:…'` (release) or `implementation project(':cast-pro')` (internal)
|
||||
- Optional `ossRelease` flavor: `implementation project(':cast-core')` only + `FreeCastEntitlements`
|
||||
|
||||
**ServiceLoader or manifest meta-data** to bind `CastEntitlements` implementation avoids `compileOnly` reflection hacks.
|
||||
|
||||
## Play Billing flow
|
||||
|
||||
1. User buys SKU (see `InAppPurchaseCoordinator.SKU_PREMIUM`).
|
||||
2. Pro AAR verifies purchase token, caches entitlement locally (encrypted prefs or Play Library `BillingClient` + acknowledge).
|
||||
3. `CastEntitlements` refreshes; UI shows unlocked resolutions / receivers.
|
||||
4. Offline grace period (e.g. 7 days) documented in product terms.
|
||||
|
||||
## Build & release
|
||||
|
||||
- Version **cast-core** and **cast-pro** independently; app pins both in `gradle/libs.versions.toml`.
|
||||
- CI: OSS job builds `cast-core` + F-Droid APK; private job signs `cast-pro` and release APK.
|
||||
- Do not embed license secrets in OSS; pro AAR may contain obfuscated verification only.
|
||||
|
||||
## Migration steps (suggested order)
|
||||
|
||||
1. Extract `CastEntitlements` + `FreeCastEntitlements` in app (no module split yet).
|
||||
2. Wire gates at encode + multi-receiver (behind dev “simulate free tier” toggle).
|
||||
3. Create `:cast-core` library — move `network/`, `cast/`, shared settings (large refactor).
|
||||
4. Add `:cast-pro` with `ProCastEntitlements` + Billing bridge.
|
||||
5. Play Console product + privacy policy ([COMMERCIAL.md](COMMERCIAL.md)).
|
||||
|
||||
## Relation to sender resolution test
|
||||
|
||||
Developer **Sender resolution UI mode** ([`SenderResolutionUi`](../app/src/main/java/com/foxx/androidcast/sender/SenderResolutionUi.java)) is for UX experiments only. Production free tier should still enforce caps in `CastTuningEngine` via entitlements, not by hiding spinner entries alone.
|
||||
@@ -66,7 +66,17 @@ All **off by default**. Enable only in **Developer settings** for internal testi
|
||||
| AdMob / unit IDs / banner layouts in main UI | TODO |
|
||||
| Play Console products + privacy policy | TODO |
|
||||
|
||||
See [COMMERCIAL.md](COMMERCIAL.md).
|
||||
See [COMMERCIAL.md](COMMERCIAL.md). Pro/OSS module split: [PRO_AAR.md](PRO_AAR.md) (design).
|
||||
|
||||
---
|
||||
|
||||
## Crash console open API
|
||||
|
||||
| Item | Status |
|
||||
|------|--------|
|
||||
| `ingest.source` / run metadata in payload | Design — [OPEN_API.md](OPEN_API.md) |
|
||||
| List filters + synthetic tagging | TODO |
|
||||
| API key upload auth (per company) | TODO |
|
||||
|
||||
---
|
||||
|
||||
@@ -126,6 +136,8 @@ See [ndk/README.md](../ndk/README.md).
|
||||
| Multi-receiver 1:N | Done (soak or gate for alpha) |
|
||||
| Legacy `ReceiverActivity` in manifest | Orphan — remove or wire |
|
||||
| Immersive A/V presets (dev) | Experimental |
|
||||
| Sender resolution UI modes (dev: Separated / Legacy / Combined) | Done (test) |
|
||||
| True dual-stream encode (480p + 720p simultaneous) | Deferred |
|
||||
|
||||
---
|
||||
|
||||
@@ -142,8 +154,11 @@ See [ndk/README.md](../ndk/README.md).
|
||||
|
||||
## Related docs
|
||||
|
||||
- [GIT_FLOW.md](GIT_FLOW.md) — green `master` / `next` development cycle ([PDF](GIT_FLOW.pdf))
|
||||
- [README.md](../README.md) — quick start
|
||||
- [USB_HDMI_CAST.md](USB_HDMI_CAST.md) — D & E
|
||||
- [COMMERCIAL.md](COMMERCIAL.md) — ads / IAP / Play
|
||||
- [PRO_AAR.md](PRO_AAR.md) — cast-core / cast-pro entitlements
|
||||
- [OPEN_API.md](OPEN_API.md) — crash ingest triage API
|
||||
- [OTA.md](OTA.md) — backend layout v0
|
||||
- [CRASH_REPORTER.md](CRASH_REPORTER.md) — crash upload
|
||||
|
||||
298
docs/_pdf_build/build_git_flow_pdf.py
Normal file
298
docs/_pdf_build/build_git_flow_pdf.py
Normal file
@@ -0,0 +1,298 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build docs/GIT_FLOW.pdf from structured content and Mermaid diagrams.
|
||||
|
||||
Source of truth for edits: docs/GIT_FLOW.md (narrative) + this script (PDF layout).
|
||||
Regenerate: python3 docs/_pdf_build/build_git_flow_pdf.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.enums import TA_LEFT
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
||||
from reportlab.lib.units import cm
|
||||
from reportlab.platypus import (
|
||||
Image,
|
||||
PageBreak,
|
||||
Paragraph,
|
||||
SimpleDocTemplate,
|
||||
Spacer,
|
||||
Table,
|
||||
TableStyle,
|
||||
)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
BUILD = Path(__file__).resolve().parent
|
||||
OUT_PDF = ROOT / "docs" / "GIT_FLOW.pdf"
|
||||
|
||||
DIAGRAMS = [
|
||||
("git_flow_diagram_branches.mmd", "git_flow_diagram_branches.png", "Branch model (green master)"),
|
||||
("git_flow_diagram_feature.mmd", "git_flow_diagram_feature.png", "Daily development"),
|
||||
("git_flow_diagram_release.mmd", "git_flow_diagram_release.png", "Release: next → master"),
|
||||
("git_flow_diagram_hotfix.mmd", "git_flow_diagram_hotfix.png", "Hotfix decision and flow"),
|
||||
]
|
||||
|
||||
|
||||
def render_mermaid() -> None:
|
||||
for mmd, png, _ in DIAGRAMS:
|
||||
src = BUILD / mmd
|
||||
dst = BUILD / png
|
||||
if not src.exists():
|
||||
raise FileNotFoundError(src)
|
||||
subprocess.run(
|
||||
[
|
||||
"npx",
|
||||
"--yes",
|
||||
"@mermaid-js/mermaid-cli@11.4.0",
|
||||
"-i",
|
||||
str(src),
|
||||
"-o",
|
||||
str(dst),
|
||||
"-b",
|
||||
"white",
|
||||
"-w",
|
||||
"1200",
|
||||
"-H",
|
||||
"800",
|
||||
],
|
||||
cwd=str(ROOT),
|
||||
check=True,
|
||||
timeout=180,
|
||||
)
|
||||
|
||||
|
||||
def p(text: str, style) -> Paragraph:
|
||||
return Paragraph(text.replace("\n", "<br/>"), style)
|
||||
|
||||
|
||||
def add_table(story, headers: list[str], rows: list[list[str]], col_widths) -> None:
|
||||
data = [headers] + rows
|
||||
t = Table(data, colWidths=col_widths)
|
||||
t.setStyle(
|
||||
TableStyle(
|
||||
[
|
||||
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1565C0")),
|
||||
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
|
||||
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
||||
("FONTSIZE", (0, 0), (-1, -1), 9),
|
||||
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||
("GRID", (0, 0), (-1, -1), 0.5, colors.lightgrey),
|
||||
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F5F5F5")]),
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 6),
|
||||
("RIGHTPADDING", (0, 0), (-1, -1), 6),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 4),
|
||||
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
|
||||
]
|
||||
)
|
||||
)
|
||||
story.append(t)
|
||||
|
||||
|
||||
def add_diagram(story, mmd_png: str, caption: str, h2, iw: float = 16 * cm) -> None:
|
||||
path = BUILD / mmd_png
|
||||
if path.exists():
|
||||
story.append(p(caption, h2))
|
||||
story.append(Image(str(path), width=iw, height=iw * 0.55))
|
||||
story.append(Spacer(1, 0.35 * cm))
|
||||
|
||||
|
||||
def build_pdf() -> None:
|
||||
styles = getSampleStyleSheet()
|
||||
title = ParagraphStyle(
|
||||
"DocTitle",
|
||||
parent=styles["Title"],
|
||||
fontSize=18,
|
||||
spaceAfter=10,
|
||||
textColor=colors.HexColor("#1565C0"),
|
||||
)
|
||||
h1 = ParagraphStyle("H1", parent=styles["Heading1"], fontSize=14, spaceBefore=12, spaceAfter=6)
|
||||
h2 = ParagraphStyle("H2", parent=styles["Heading2"], fontSize=11, spaceBefore=8, spaceAfter=4)
|
||||
body = ParagraphStyle("Body", parent=styles["Normal"], fontSize=10, leading=14, alignment=TA_LEFT)
|
||||
small = ParagraphStyle("Small", parent=body, fontSize=9, textColor=colors.grey)
|
||||
bullet = ParagraphStyle("Bullet", parent=body, leftIndent=14, bulletIndent=6)
|
||||
|
||||
story: list = []
|
||||
story.append(p("Android Cast — Git flow (green master)", title))
|
||||
story.append(
|
||||
p(
|
||||
"Development cycles, branch roles, release and hotfix strategy. "
|
||||
"Version 1.0 · 2026-05-21. Markdown source: docs/GIT_FLOW.md",
|
||||
small,
|
||||
)
|
||||
)
|
||||
story.append(Spacer(1, 0.3 * cm))
|
||||
|
||||
# 1 Goals
|
||||
story.append(p("1. Goals", h1))
|
||||
add_table(
|
||||
story,
|
||||
["Goal", "Mechanism"],
|
||||
[
|
||||
["Always shippable master", "Merge to master only when CI/tests pass"],
|
||||
["Safe integration", "Features land on next first"],
|
||||
["Clear production line", "Release tags only on master"],
|
||||
["Fast production patches", "hotfix/* from master, then sync to next"],
|
||||
],
|
||||
[4.5 * cm, 12 * cm],
|
||||
)
|
||||
story.append(Spacer(1, 0.4 * cm))
|
||||
|
||||
# 2 Branches
|
||||
story.append(p("2. Branches", h1))
|
||||
add_table(
|
||||
story,
|
||||
["Branch", "Role", "Direct commits?"],
|
||||
[
|
||||
["master", "Production; green; annotated tags vX.Y.Z", "No — merge only"],
|
||||
["next", "Integration / release candidate", "No — merge only"],
|
||||
["feature/*, fix/*", "Short-lived work", "Yes; merge → next"],
|
||||
["hotfix/*", "Urgent fix for released code", "Yes; merge → master first"],
|
||||
],
|
||||
[3 * cm, 8.5 * cm, 4 * cm],
|
||||
)
|
||||
story.append(Spacer(1, 0.3 * cm))
|
||||
add_diagram(story, DIAGRAMS[0][1], DIAGRAMS[0][2], h2)
|
||||
story.append(PageBreak())
|
||||
|
||||
# 3 Daily dev
|
||||
story.append(p("3. Daily development", h1))
|
||||
for line in [
|
||||
"1. git fetch && git checkout next && git pull",
|
||||
"2. git checkout -b feature/short-description",
|
||||
"3. Commit on topic branch; run tests before merge",
|
||||
"4. Merge topic → next (after CI green)",
|
||||
"5. Delete topic branch",
|
||||
]:
|
||||
story.append(p(f"• {line}", bullet))
|
||||
story.append(Spacer(1, 0.2 * cm))
|
||||
story.append(
|
||||
p(
|
||||
"<b>Agents:</b> never push directly to master or next unless the user explicitly "
|
||||
"requests a release or hotfix merge.",
|
||||
body,
|
||||
)
|
||||
)
|
||||
add_diagram(story, DIAGRAMS[1][1], DIAGRAMS[1][2], h2, iw=15 * cm)
|
||||
story.append(PageBreak())
|
||||
|
||||
# 4 Release
|
||||
story.append(p("4. Release (next → master)", h1))
|
||||
for line in [
|
||||
"Confirm next is QA-ready and CI green",
|
||||
"Merge next → master",
|
||||
"Smoke / CI on master",
|
||||
"Tag on master: git tag -a v0.1.2 -m \"Release v0.1.2\"",
|
||||
"Sync: git checkout next && git merge master && git push",
|
||||
"Continue feature branches from updated next",
|
||||
]:
|
||||
story.append(p(f"• {line}", bullet))
|
||||
add_diagram(story, DIAGRAMS[2][1], DIAGRAMS[2][2], h2, iw=15 * cm)
|
||||
story.append(PageBreak())
|
||||
|
||||
# 5 Hotfix
|
||||
story.append(p("5. Hotfixes — recommended strategy", h1))
|
||||
story.append(
|
||||
p(
|
||||
"<b>Default (option 1):</b> branch hotfix/* from <b>master</b> when the bug affects "
|
||||
"released code. After fix and tests: merge → master, tag patch (e.g. v0.1.3), then "
|
||||
"<b>merge master → next</b> so integration does not miss the fix.",
|
||||
body,
|
||||
)
|
||||
)
|
||||
story.append(Spacer(1, 0.15 * cm))
|
||||
story.append(
|
||||
p(
|
||||
"<b>Exception (option 2):</b> bug exists only on next — branch fix/* from next, "
|
||||
"merge to next only; ships with the next normal release.",
|
||||
body,
|
||||
)
|
||||
)
|
||||
story.append(Spacer(1, 0.15 * cm))
|
||||
story.append(
|
||||
p(
|
||||
"<b>Why not hotfix only on next?</b> next often contains unreleased features; "
|
||||
"merging next to master for a patch would ship extra code or block the fix.",
|
||||
body,
|
||||
)
|
||||
)
|
||||
add_diagram(story, DIAGRAMS[3][1], DIAGRAMS[3][2], h2, iw=14 * cm)
|
||||
|
||||
story.append(p("5.1 Sync cheat sheet", h2))
|
||||
add_table(
|
||||
story,
|
||||
["Event", "Action"],
|
||||
[
|
||||
["After release + tag", "merge master → next"],
|
||||
["After hotfix + tag", "merge master → next"],
|
||||
["Feature done", "merge topic → next only"],
|
||||
["Wrong base branch", "rebase topic onto latest next"],
|
||||
],
|
||||
[4.5 * cm, 12 * cm],
|
||||
)
|
||||
story.append(PageBreak())
|
||||
|
||||
# 6 Protection + agents
|
||||
story.append(p("6. Branch protection and CI", h1))
|
||||
add_table(
|
||||
story,
|
||||
["Rule", "master", "next"],
|
||||
[
|
||||
["CI required before merge", "yes", "yes"],
|
||||
["No WIP direct push", "yes", "yes"],
|
||||
["Annotated release tags", "yes", "no"],
|
||||
["Force-push", "forbidden", "forbidden"],
|
||||
],
|
||||
[5.5 * cm, 5 * cm, 5 * cm],
|
||||
)
|
||||
story.append(Spacer(1, 0.35 * cm))
|
||||
story.append(p("7. Cursor / agent checklist", h1))
|
||||
for line in [
|
||||
"Branch from next for features and non-production fixes",
|
||||
"Run tests before recommending merge to next",
|
||||
"Releases and tags only with explicit user approval",
|
||||
"Hotfix released code from master; sync master into next after tag",
|
||||
"Never force-push master or next",
|
||||
]:
|
||||
story.append(p(f"• {line}", bullet))
|
||||
story.append(Spacer(1, 0.4 * cm))
|
||||
story.append(
|
||||
p(
|
||||
"Full narrative and Mermaid sources: docs/GIT_FLOW.md, "
|
||||
"docs/_pdf_build/git_flow_diagram_*.mmd · "
|
||||
"Regenerate PDF: python3 docs/_pdf_build/build_git_flow_pdf.py",
|
||||
small,
|
||||
)
|
||||
)
|
||||
|
||||
doc = SimpleDocTemplate(
|
||||
str(OUT_PDF),
|
||||
pagesize=A4,
|
||||
leftMargin=2 * cm,
|
||||
rightMargin=2 * cm,
|
||||
topMargin=2 * cm,
|
||||
bottomMargin=2 * cm,
|
||||
)
|
||||
doc.build(story)
|
||||
print(f"Wrote {OUT_PDF}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
render_mermaid()
|
||||
except subprocess.CalledProcessError as e:
|
||||
print("Mermaid render failed:", e, file=sys.stderr)
|
||||
return 1
|
||||
except FileNotFoundError as e:
|
||||
print(e, file=sys.stderr)
|
||||
return 1
|
||||
build_pdf()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
21
docs/_pdf_build/git_flow_diagram_branches.mmd
Normal file
21
docs/_pdf_build/git_flow_diagram_branches.mmd
Normal file
@@ -0,0 +1,21 @@
|
||||
flowchart TB
|
||||
subgraph legend[" "]
|
||||
direction LR
|
||||
L1[Long-lived]
|
||||
L2[Short-lived]
|
||||
end
|
||||
|
||||
M["master\n(always green, tagged releases)"]
|
||||
N["next\n(integration / release candidate)"]
|
||||
F["feature/* fix/*\n(fork next)"]
|
||||
H["hotfix/*\n(fork master)"]
|
||||
|
||||
F -->|"merge + CI green"| N
|
||||
N -->|"release: merge + CI green"| M
|
||||
H -->|"urgent patch + CI green"| M
|
||||
M -->|"sync after release or hotfix"| N
|
||||
|
||||
style M fill:#c8e6c9,stroke:#2e7d32
|
||||
style N fill:#bbdefb,stroke:#1565c0
|
||||
style F fill:#fff9c4,stroke:#f9a825
|
||||
style H fill:#ffccbc,stroke:#e64a19
|
||||
BIN
docs/_pdf_build/git_flow_diagram_branches.png
Normal file
BIN
docs/_pdf_build/git_flow_diagram_branches.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
15
docs/_pdf_build/git_flow_diagram_feature.mmd
Normal file
15
docs/_pdf_build/git_flow_diagram_feature.mmd
Normal file
@@ -0,0 +1,15 @@
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant Dev as Developer or agent
|
||||
participant Topic as feature/* or fix/*
|
||||
participant Next as next
|
||||
participant CI as Tests / CI
|
||||
|
||||
Dev->>Next: fetch, checkout, pull
|
||||
Dev->>Topic: create branch from next
|
||||
Dev->>Topic: commits
|
||||
Dev->>CI: run tests
|
||||
CI-->>Dev: pass
|
||||
Dev->>Next: merge topic branch
|
||||
Note over Next: next stays integration line
|
||||
Dev->>Topic: delete branch
|
||||
BIN
docs/_pdf_build/git_flow_diagram_feature.png
Normal file
BIN
docs/_pdf_build/git_flow_diagram_feature.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
21
docs/_pdf_build/git_flow_diagram_hotfix.mmd
Normal file
21
docs/_pdf_build/git_flow_diagram_hotfix.mmd
Normal file
@@ -0,0 +1,21 @@
|
||||
flowchart TD
|
||||
Start([Defect found])
|
||||
Q{Affects released code\non master / latest tag?}
|
||||
|
||||
Start --> Q
|
||||
Q -->|Yes| A[Branch hotfix/* from master]
|
||||
A --> B[Fix + tests]
|
||||
B --> C[Merge to master]
|
||||
C --> D[Tag patch on master e.g. v0.1.3]
|
||||
D --> E[Merge master into next]
|
||||
E --> End([Done])
|
||||
|
||||
Q -->|No, only on next| F[Branch fix/* from next]
|
||||
F --> G[Fix + tests]
|
||||
G --> H[Merge to next only]
|
||||
H --> End2([Ships with next release])
|
||||
|
||||
style C fill:#c8e6c9
|
||||
style D fill:#c8e6c9
|
||||
style E fill:#bbdefb
|
||||
style H fill:#bbdefb
|
||||
BIN
docs/_pdf_build/git_flow_diagram_hotfix.png
Normal file
BIN
docs/_pdf_build/git_flow_diagram_hotfix.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
16
docs/_pdf_build/git_flow_diagram_release.mmd
Normal file
16
docs/_pdf_build/git_flow_diagram_release.mmd
Normal file
@@ -0,0 +1,16 @@
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant Next as next
|
||||
participant Master as master
|
||||
participant CI as Tests / CI
|
||||
participant Tag as tag vX.Y.Z
|
||||
|
||||
Note over Next: feature complete, QA ok
|
||||
Next->>CI: full test suite
|
||||
CI-->>Next: green
|
||||
Next->>Master: merge next to master
|
||||
Master->>CI: smoke on master
|
||||
CI-->>Master: green
|
||||
Master->>Tag: annotated tag on master
|
||||
Master->>Next: merge master into next (sync)
|
||||
Note over Next: continue features from synced next
|
||||
BIN
docs/_pdf_build/git_flow_diagram_release.png
Normal file
BIN
docs/_pdf_build/git_flow_diagram_release.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
Reference in New Issue
Block a user