Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
4 changes: 2 additions & 2 deletions .claude/project.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
## Versions (pin everything)

- **Frappe v16+ ONLY.** Never generate code targeting v14 or v15. Use type annotations on every controller method, `autoname: "uuid"` for opaque IDs (audit, polymorphic, votes), `frappe.utils.data_masker` for PII, virtual DocTypes for read-only views over external data, the improved scheduler with proper `cron` events.
- Python 3.11+
- Node 20 LTS, Yarn
- Python 3.14 (frappe v16 pins `requires-python = ">=3.14,<3.15"`)
- Node 24 LTS, Yarn (frappe v16 `package.json` pins `engines.node = ">=24"`)
- Vue 3 + Vite + Frappe UI (logged-in SPA)
- Jinja (public site)
- Chart.js (analytics — no Insights dependency)
Expand Down
90 changes: 72 additions & 18 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ jobs:

- uses: actions/setup-python@v5
with:
python-version: "3.11"
# Frappe v16.17.0 pins requires-python = ">=3.14,<3.15" (verified
# against the version-16 branch). Both lint and test jobs use 3.14
# to keep linting in sync with what gets executed.
python-version: "3.14"
cache: pip

- name: Install ruff
Expand Down Expand Up @@ -86,13 +89,21 @@ jobs:

- uses: actions/setup-python@v5
with:
python-version: "3.11"
# Frappe v16.17.0 pins requires-python = ">=3.14,<3.15" (verified
# against the version-16 branch). Both lint and test jobs use 3.14
# to keep linting in sync with what gets executed.
python-version: "3.14"
cache: pip

- uses: actions/setup-node@v4
with:
node-version: "20"
cache: yarn
# Frappe v16 package.json declares engines.node ">=24" since
# v16.0.0-rc.1; node 20 fails yarn install at bench init.
node-version: "24"
# cache: yarn — re-enable once a yarn.lock exists at repo root.
# Will be added by T-005 (themes scaffolding) or T-068 onwards
# (Vue inbox SPA), whichever lands first. Until then setup-node
# still runs so `node` is available for bench's asset hooks.

- name: System packages
run: |
Expand All @@ -105,37 +116,80 @@ jobs:
- name: Install bench
run: pip install frappe-bench

- name: bench init (Frappe v16)
# All five apps below are pinned to fixed refs (tags or SHAs) to defend
# against upstream HEAD instability. The 16.17.x release-storm
# (2026-04-28 → 05-01: three emergency releases in 4 days, 16.17.0 also
# raising the Python floor + tightening package.json) was the trigger
# for this pinning. Re-evaluate quarterly or when a new v16 feature
# is needed; bump deliberately and verify CI before merge.
- name: bench init (Frappe v16.16.0 — pinned)
run: |
bench init \
--skip-redis-config-generation \
--skip-assets \
--frappe-branch version-16 \
--frappe-branch v16.16.0 \
--python "$(which python)" \
frappe-bench

# -- Get peer apps (per-app, so a failure points at exactly one app) --
- name: get-app payments
- name: get-app payments (pinned to 3cebd942 — 2026-03-13)
working-directory: frappe-bench
run: bench get-app payments

- name: get-app buzz
# Pin pattern: see the comment block above the buzz step below
# for why we use git reset --hard rather than a pre-clone install.
run: |
bench get-app payments
git -C apps/payments fetch --unshallow upstream
git -C apps/payments reset --hard 3cebd942

# buzz and payments have no release tags. We let bench get-app run
# its install pipeline against the default branch (which today equals
# the pin SHA — though for far-back pins it doesn't), then
# `git fetch --unshallow upstream` to deepen the shallow `--depth 1`
# clone to full history, and `git reset --hard <sha>` to lock the
# working tree. Note: bench clones with `--origin upstream`, so
# fetch uses `upstream` rather than `origin` as the remote name.
# We use `--unshallow` rather than `--depth=N` to avoid hardcoding
# a number that silently breaks once a future pin is older than N
# commits from HEAD (the bug that caught us at 26e8c20 — plain
# `git fetch upstream` doesn't deepen a shallow clone). Both repos
# are small (<1000 commits each); full fetch is ~1-2 seconds. We
# don't try `--depth=1 <sha>` because GitHub smart-HTTP doesn't
# advertise arbitrary SHAs as fetchable refs.
# Tradeoff: if the SHAs later drift away from HEAD, dependencies
# resolved during bench get-app will be from HEAD before the reset,
# not the pinned tree. Acceptable today since both apps are small
# Frappe apps installed in editable mode pointing at the working tree.
# Switch to a pre-clone + file:/// install if a real drift-resolution
# bug surfaces (i.e., not a fetch-syntax issue like the one this
# comment was rewritten in response to).
- name: get-app buzz (pinned to 5118a99d — 2026-02-01)
working-directory: frappe-bench
run: bench get-app https://github.com/BuildWithHussain/buzz
run: |
bench get-app https://github.com/BuildWithHussain/buzz
git -C apps/buzz fetch --unshallow upstream
git -C apps/buzz reset --hard 5118a99d

- name: get-app erpnext (school_connected only)
- name: get-app erpnext v16.16.0 (school_connected only)
if: matrix.alumni_mode == 'school_connected'
working-directory: frappe-bench
run: bench get-app erpnext --branch version-16
run: bench get-app erpnext --branch v16.16.0

- name: get-app education (school_connected only)
- name: get-app education v16.0.1 (school_connected only)
if: matrix.alumni_mode == 'school_connected'
working-directory: frappe-bench
run: bench get-app education --branch version-16
run: bench get-app education --branch v16.0.1

- name: Link alumni app into bench
run: |
mv apps/alumni frappe-bench/apps/alumni
- name: install alumni app into bench
working-directory: frappe-bench
# bench get-app runs `git clone <url>` with cwd=<bench_root>/apps/,
# so a relative URL resolves from there — not from the step's
# working-directory. We learned this when ../apps/alumni from
# frappe-bench/ resolved to frappe-bench/apps/../apps/alumni
# (= frappe-bench/apps/alumni) which doesn't exist. Pass an
# absolute path via $GITHUB_WORKSPACE to sidestep the path-math
# entirely. bench then handles the pip install + apps.txt
# registration as expected.
run: bench get-app $GITHUB_WORKSPACE/apps/alumni

- name: Configure bench for CI
working-directory: frappe-bench
Expand Down
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ repos:
args: [--fix]
- id: ruff-format

# Python upgrade syntax to py3.11+
# Python upgrade syntax to py3.12+
- repo: https://github.com/asottile/pyupgrade
rev: v3.16.0
hooks:
- id: pyupgrade
args: [--py311-plus]
args: [--py312-plus]

# Markdown / YAML / JSON / CSS / JS formatting
# (eslint runs as a project-local script via yarn, not as a pre-commit hook,
Expand Down
2 changes: 1 addition & 1 deletion BUILD_TICKETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Tickets in dependency order. Each is sized for one Claude Code session (1–6 ho
**Outcome:** A Frappe v16 app installable with `bench install-app alumni` on a vanilla site.
**Steps:**
- `bench new-app alumni` with AGPL-3.0 license
- Add `pyproject.toml` deps: `frappe>=16.0.0`, `payments`, `buzz` (peer-dep noted in README)
- Leave `pyproject.toml` `dependencies = []` — frappe, payments, and buzz are peer Frappe apps installed by bench, not pip deps (re-declaring frappe trips uv's transitive URL-dep refusal on frappe's pypika/gunicorn git pins; following buzz/payments/ERPNext/Education convention). Document peer-app fetch in README.
- Drop the 11 root docs: `README.md`, `CLAUDE.md`, `DECISIONS.md`, `SPEC.md`, `INTEGRATIONS.md`, `THEMES.md`, `BUILD_TICKETS.md`, `PROJECT_STATE.md`, `CLAUDE_CODE_SETUP.md`, `CHANGELOG.md`, `license.txt`
- CI: GitHub Actions running `ruff`, `pytest`, `bench migrate` on a vanilla Frappe v16 site with `payments` and `buzz` installed
- pre-commit: ruff + eslint + prettier + pyupgrade
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ Read `DECISIONS.md` ADRs 031–049 before touching any of these areas.
## Stack

- **Frappe v16+ ONLY**. We use v16 features: UUID autoname, data masking, virtual DocTypes, type annotations, scheduler improvements. Code that targets v14/v15 is rejected in review.
- Python 3.11+
- Node 20 LTS, Yarn
- Python 3.14 (frappe v16 pins `requires-python = ">=3.14,<3.15"`)
- Node 24 LTS, Yarn (frappe v16 `package.json` pins `engines.node = ">=24"`)
- Vue 3 + Vite + Frappe UI for the logged-in portal
- Jinja for the public site
- Chart.js for analytics (no Insights dependency)
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE_CODE_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ Both this app and the skill package target Frappe v14–v16. We use **v16 only**
- `frappe.utils.data_masker` for PII fields (phone, email) when masking is required
- Virtual DocTypes for read-only views over external data
- The improved scheduler's `cron` events
- Python: 3.11+
- Node: 20 LTS
- Python: 3.14 (frappe v16 pins `requires-python = ">=3.14,<3.15"`)
- Node: 24 LTS (frappe v16 `package.json` pins `engines.node = ">=24"`)

## Skill package

Expand Down
26 changes: 26 additions & 0 deletions PROJECT_STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,31 @@
_Bullet what shipped. Use commit hashes._
- **Spec bumped to v3** (final revision Apr 2026) after a feature-parity review against ZaiAlumni, several incumbent alumni products, and InfyVCardsSaas's 14-version history. Added 24 new ADRs (031–054), ~72 new DocTypes (33 → ~105), 55 new build tickets (T-062 → T-116) across 3 new phases (14 Elections, 17 SaaS Infra, 18 vCards/WhatsApp/AI), 3 new adapters (messaging, verification, ai — bringing total to 12), 1 new role (Alumni Board Member), 14 new email templates (29 total), 3 new dashboards, 12 built-in SMS / WhatsApp provider drivers + Custom HTTP, 4th theme (Aurora dark mode), full PWA support, custom domain + per-alumni custom domain auto-provisioning with Let's Encrypt SSL, Memorial Wall, Distinguished Alumni Awards, Perks marketplace + digital member card, Job Referrals, Speaker Bureau, Networking match suggestions, profile completeness scoring, SEO + Open Graph + JSON-LD + QR codes, **per-alumni vCards (2 built-in templates + admin-uploadable registry), WhatsApp click-to-chat + Business catalog sync, AI bio drafting opt-in, Alumni Business Directory, antispam settings, bulk admin ops, recovery codes**, and a new top-level `DESIGN_SYSTEM.md` doc.
- All 11 root docs updated to v3: SPEC.md, DECISIONS.md, INTEGRATIONS.md, BUILD_TICKETS.md, README.md, CLAUDE.md, CHANGELOG.md, PROJECT_STATE.md, THEMES.md. CLAUDE_CODE_SETUP.md unchanged (still applies).
- Day 1 of T-001 ended with PR #4 still open at 23ab8b2. CI infrastructure work successful through 5 iterations:
- yarn cache lockfile (d833ae1)
- Python 3.12 attempt (83bb2ce — wrong floor, kept for breadcrumb)
- Python 3.14 floor (615d53c — correct)
- SHA pinning all 5 peer apps + Node 24 (f903e52)
- --origin upstream remote name fix (0d9935f)
- Full-history fetch for SHA reset (23ab8b2)

CI now installs Frappe v16.16.0 + ERPNext v16.16.0 + Education v16.0.1 + buzz 3d77434b + payments 3cebd942 cleanly through bench new-site, install-app payments/buzz/erpnext/education. Stops at install-app alumni with two distinct errors:

1. buzz/__init__.py:9 calls toggle_test_mode(True) at import time, which raises AttributeError: flags during bench's command discovery (frappe.local.flags not initialized at that point). Upstream incompatibility between buzz HEAD and frappe v16.16.0.

2. Alumni app is mv'd into apps/ but not pip install -e'd, so bench install-app alumni fails with "No module named 'alumni'". Workflow design issue inherited from T-001 ticket — the ticket specified the move but not the install step.

Both deferred to T-001-followup tomorrow with fresh eyes.

Day 2 (continued): both T-001 issues from yesterday's stop fixed in 26e8c20:
- buzz pin moved to 5118a99d (pre-toggle_test_mode refactor)
- pip install -e apps/alumni added after the mv step

CI on 26e8c20 surfaced one new layer-7 issue: git fetch upstream on a shallow clone doesn't deepen history far enough to reach a SHA outside the initial --depth 1 pack. Failed at the buzz reset step on both legs. The fix from yesterday (drop --depth flag entirely) was correct for current-HEAD pins like 3d77434b but insufficient for far-back pins like 5118a99d (3 months upstream of HEAD).

Day 2 (final): 5 more iterations during the evening session brought CI through buzz unshallow fix (97fc374), buzz pin to 5118a99d (26e8c20), --unshallow for shallow-clone deepening (42b4b1b), bench get-app with mv (995f934), bench get-app from local path (742bab3). Each peeled back one bench-vs-CI interaction.

Final state at 742bab3: bench get-app ../apps/alumni fails because bench resolves relative paths from frappe-bench/apps/, not from frappe-bench/. Tomorrow's resume task is the path-math fix — either absolute path, or pre-position the source where bench's relative-path resolution expects it.

## Blockers
_What's stuck and who is unstuck-ing it._
Expand Down Expand Up @@ -51,6 +76,7 @@ _From SPEC and DECISIONS, plus emergent._
- **Standalone-mode receivables**: needs to handle multi-currency cleanly. Watching.
- **Frappe Meet still alpha** — staying on Jitsi via fallback for v1.
- **bKash / HyperPay / Moyasar** may not be in `frappe/payments` yet — may need to contribute upstream.
- **T-001 not yet merged.** Upstream buzz/frappe compatibility issue at our pinned SHAs needs investigation before merge. Alumni install pattern in CI workflow needs a `pip install -e` step or equivalent. Estimated 30 minutes tomorrow morning.

## Next 3 tickets
1. T-001 — Repo scaffold
Expand Down
63 changes: 63 additions & 0 deletions docs/upstream-issues/buzz-toggle-test-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Upstream issue draft — buzz: toggle_test_mode at import time breaks bench commands when CI=true

> Draft to file against [BuildWithHussain/buzz](https://github.com/BuildWithHussain/buzz) **after** PR #4 (alumni `fix/ci-yarn-cache`) merges. Maintainer feedback during our active fix cycle creates churn — better to file with full context, a working pin, and a link to the merged workaround.

---

**Title:** `buzz/__init__.py:toggle_test_mode(True) at import time breaks bench commands when CI=true`

**Body:**

Since `16ef2563` (refactor: use frappe's toogle_test_mode for enabling testing, 2026-02-02), `buzz/__init__.py` calls `toggle_test_mode(True)` at module import time when the `CI` env var is set:

```python
__version__ = "0.0.1"

import os

if os.environ.get("CI"):
import frappe
from frappe.tests.utils import toggle_test_mode

toggle_test_mode(True)
```

GitHub Actions sets `CI=true` on every job by default, so this fires during bench's CLI command discovery (`get_app_commands` walks installed apps and imports each `<app>.commands` before any frappe context is initialized).

`toggle_test_mode` reads `frappe.local.flags`, which doesn't exist at that point, so importing buzz raises `AttributeError: flags`. Any bench CLI command run with `CI=true` crashes — including `bench install-app`, `bench migrate`, `bench build`.

**Repro** (Frappe v16.16.0 + buzz HEAD):

```bash
CI=true bench --site test install-app some-other-app
```

Traceback:

```
File ".../frappe-bench/apps/buzz/buzz/__init__.py", line 9, in <module>
toggle_test_mode(True)
File ".../frappe/tests/utils/__init__.py", line 62, in toggle_test_mode
frappe.local.flags.in_test = enable
File ".../frappe/utils/local.py", line 28, in __getattribute__
raise AttributeError(name)
AttributeError: flags
```

**Possible fixes** (any of):

- Defer the toggle — let frappe's test runner set `in_test` itself, not at module import time.
- Gate on a more specific env var than `CI` (e.g., `FRAPPE_IN_TEST` as `5118a99d` did before the refactor).
- Wrap in `try/except AttributeError` so import succeeds outside frappe context.

**Workaround for downstream consumers:** pin to `5118a99d` (the commit immediately before the refactor) until fixed. That version uses `FRAPPE_IN_TEST` instead of `CI` and a simpler `frappe.in_test = True` attribute set, with no fragile `frappe.local` access at import time.

**Reference:**

| SHA | Date | Note |
|---|---|---|
| `16ef2563` | 2026-02-02 | Introduces the bug (`refactor: use frappe's toogle_test_mode for enabling testing`) |
| `5118a99d` | 2026-02-01 | Last known-good — recommended downstream pin |
| `3c8b5ef8` | 2025-09-26 | Pre-test-mode-machinery (`refactor: rename to Buzz (#53)`) |

Workaround applied in [Erpflow-dev/alumni#4](https://github.com/Erpflow-dev/alumni/pull/4) — pin moved from `3d77434b` (2026-04-20, affected) to `5118a99d` (2026-02-01, last known-good).
17 changes: 10 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,22 @@ authors = [
{ name = "Erpflow", email = "info@erpflow.dev" },
]
description = "Standalone Frappe v16 app for alumni networks (school-connected + standalone modes)"
requires-python = ">=3.11"
requires-python = ">=3.14,<3.15"
readme = "README.md"
license = { text = "AGPL-3.0-only" }
dynamic = ["version"]
dependencies = [
"frappe>=16.0.0",
]
# Frappe is installed and managed by bench during `bench init` —
# it's not a pip dependency. Apps that re-declare it trigger uv's
# transitive URL-dep refusal (frappe v16's pypika and gunicorn
# are git+https URL deps, which uv only follows for direct deps,
# not transitive ones — see uv issue #1808). Following the
# convention used by buzz, payments, ERPNext, and Education.
dependencies = []
keywords = ["frappe", "alumni", "school", "education", "saas"]
classifiers = [
"License :: OSI Approved :: GNU Affero General Public License v3",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.14",
"Operating System :: POSIX :: Linux",
"Framework :: Frappe",
]
Expand All @@ -37,7 +40,7 @@ name = "alumni"

[tool.ruff]
line-length = 110
target-version = "py311"
target-version = "py312"
extend-exclude = [
"frappe-bench",
"node_modules",
Expand Down
Loading