ca8ba69acb
Replaces the v0.3.0 / v0.7.0 allowed_emails admission gate with an admin-grant flow (roadmap item #6, SPEC §6.1 / §6.2 / §14.1 / §17). Any valid email can sign in via OTC; a fresh user lands in permission_state='pending' with a captured first/last/why profile, and an admin grant flips them to 'granted' before write endpoints accept them. Grandfathered users pass through the migration with the column default 'granted' so existing contributors are unaffected. The allowed_emails table stays in the schema as a fast-path bypass pending v0.9.0's admin user-management page (item #7). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
904 lines
44 KiB
Markdown
904 lines
44 KiB
Markdown
# Changelog
|
|
|
|
The binding policy for what each kind of version bump means and what
|
|
the changelog has to carry is in [`SPEC.md` §20](./SPEC.md). The
|
|
practical recipe downstream deployments follow when reading this file
|
|
is in [`docs/DEPLOYMENTS.md`](./docs/DEPLOYMENTS.md).
|
|
|
|
The canonical version is the `VERSION` file at the repo root;
|
|
`frontend/package.json#version` mirrors it. Deployments pin to a
|
|
specific framework version in their own `.rfc-app-version` file
|
|
(per §20.5).
|
|
|
|
While the framework is pre-1.0, minor-version bumps may introduce
|
|
breaking changes; the entry below each such bump documents the
|
|
upgrade steps a deployment must apply.
|
|
|
|
Upgrade-steps blocks use the [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119)
|
|
/ [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174) normative-
|
|
language convention per `SPEC.md` §20.4. **MUST** / **SHALL** steps
|
|
are required; **SHOULD** steps are recommended (the framework's
|
|
default and tested path); **MAY** steps are optional. Upgrades that
|
|
skip versions are the composition of each intervening adjacent
|
|
release's steps in order — no A-to-B path is pre-computed beyond
|
|
that.
|
|
|
|
## 0.8.0 — 2026-05-28
|
|
|
|
**Minor — schema migration required; admission semantics shift.**
|
|
This release replaces the v0.3.0 / v0.7.0 `allowed_emails` admission
|
|
gate with an admin-grant flow (roadmap item #6, SPEC §6.1 / §6.2 /
|
|
§14.1 / §17). Anyone with a valid email can sign in via the v0.7.0
|
|
OTC flow; the OTC request endpoint no longer consults the
|
|
allowlist. A fresh user lands in `permission_state='pending'` until
|
|
an admin grants access. The first-OTC sign-in captures first name,
|
|
last name, and a free-text "why I should be included in the beta"
|
|
via a new `POST /api/auth/me/beta-request` endpoint; the captured
|
|
fields populate the same `users` row alongside the OAuth-era
|
|
columns.
|
|
|
|
A pending user has the same read access an anonymous viewer has —
|
|
the catalog, RFC bodies, the philosophy page, and every public
|
|
conversation are reachable. Every write-shaped endpoint
|
|
(`auth.require_contributor` floor) refuses pending users with 403.
|
|
The frontend renders a thin "Your beta access request is in
|
|
review" banner on every page and re-purposes the v0.3.0
|
|
`/beta-pending` page as the post-capture landing surface.
|
|
|
|
Grandfathered behavior: every `users` row at migration time
|
|
carries `permission_state='granted'` via the column default, so
|
|
existing contributors are unaffected. The OAuth fallback at
|
|
`/auth/callback` still consults the v0.3.0 allowlist (legacy
|
|
path); the OTC flow does not.
|
|
|
|
The `allowed_emails` table stays in the schema as a fast-path
|
|
bypass — the v0.3.0 admin UI continues to manage it, but the OTC
|
|
request handler no longer reads it. v0.9.0 (roadmap item #7) is
|
|
expected to ship the admin user-management page that replaces the
|
|
allowlist surface entirely; until then, admin grants are done by
|
|
direct DB `UPDATE`.
|
|
|
|
### Upgrade steps (from 0.13.0)
|
|
|
|
1. **MUST** restart the backend so migration `014_beta_access.sql`
|
|
runs. The migration adds `permission_state` (default `'granted'`,
|
|
so existing rows pass through unaffected), `first_name`,
|
|
`last_name`, `beta_request_reason`, `permission_decided_by`,
|
|
and `permission_decided_at` to the `users` table, plus an index
|
|
on `permission_state` for the pending queue. The migration is
|
|
ALTER-TABLE-based (no table rebuild) — every foreign key and
|
|
existing row passes through untouched.
|
|
2. **MUST** rebuild the frontend. The `Login.jsx` surface now
|
|
runs a conditional third step (the capture form) on fresh OTC
|
|
sign-ins; `BetaPending.jsx` carries the new "your request is
|
|
in review" copy; `App.jsx` renders a thin pending-access
|
|
banner. The build embeds the new `/api/auth/me/beta-request`
|
|
client call.
|
|
3. **SHOULD** announce the new admission flow to existing users.
|
|
Wording suggestion: "We've replaced our email-allowlist gate
|
|
with an admin-review flow. Existing users are unaffected;
|
|
new visitors sign in with their email, tell us a bit about
|
|
themselves, and an admin reviews their request before
|
|
discussion and contribution unlock." Existing sessions
|
|
remain valid.
|
|
4. **SHOULD** plan the admin grant mechanism. v0.8.0 does not
|
|
ship a UI for the grant — v0.9.0 (roadmap item #7) will. For
|
|
the v0.8.0 window, an admin grants access via direct DB
|
|
gesture:
|
|
```sql
|
|
UPDATE users
|
|
SET permission_state = 'granted',
|
|
permission_decided_by = <admin_user_id>,
|
|
permission_decided_at = datetime('now')
|
|
WHERE email = '<approved>';
|
|
```
|
|
The pending queue lives in `SELECT * FROM users WHERE
|
|
permission_state = 'pending' ORDER BY created_at`.
|
|
5. **MUST** decide whether to drain the `allowed_emails` table.
|
|
The OTC request handler no longer consults it; populated
|
|
rows are inert at the request surface. Three operator
|
|
choices, all valid:
|
|
* **Leave as-is** (the framework's default behavior — the
|
|
v0.3.0 admin UI continues to work, the rows stay as a
|
|
fast-path bypass record). Recommended if you anticipate
|
|
v0.9.0's user-management page folding the allowlist UI
|
|
into its surface.
|
|
* **Drain via the existing admin UI** (`/admin/allowlist`)
|
|
— one row at a time, no data loss elsewhere.
|
|
* **Bulk-drain via DB** — `DELETE FROM allowed_emails;`
|
|
drops every row; the table stays.
|
|
6. **MAY** announce write access individually to grandfathered
|
|
users you want to keep at `'granted'`. The default-`'granted'`
|
|
migration means no action is required for them; this step
|
|
exists only if you want to send a "you're still in" message.
|
|
|
|
### Added
|
|
|
|
- **`backend/migrations/014_beta_access.sql`** — adds
|
|
`permission_state` (CHECK in `('pending', 'granted', 'revoked')`,
|
|
default `'granted'`), `first_name`, `last_name`,
|
|
`beta_request_reason`, `permission_decided_by` (FK to users,
|
|
ON DELETE SET NULL), `permission_decided_at` to the `users`
|
|
table. Plus `idx_users_permission_state` for the pending
|
|
queue.
|
|
- **`POST /api/auth/me/beta-request`** — body
|
|
`{first_name, last_name, beta_request_reason}` (all required;
|
|
bounds 120 / 120 / 4000). Writes the fields to the signed-in
|
|
user's row and leaves `permission_state='pending'`. Refuses
|
|
HTTP 409 for already-granted / revoked users; refuses HTTP
|
|
401 for anonymous callers.
|
|
- **`needs_profile` flag** on the `/auth/otc/verify` response.
|
|
`true` iff the user is `permission_state='pending'` AND
|
|
carries no profile fields yet (a fresh OTC sign-in). The
|
|
Login.jsx surface uses the flag to gate the capture step.
|
|
- **`permission_state` field** on the `/api/auth/me` response,
|
|
plus `first_name`, `last_name`, `beta_request_reason`, and
|
|
the same `needs_profile` flag.
|
|
- **First-OTC profile capture step** in `Login.jsx`. Third
|
|
step in the sign-in surface, gated by the verify response's
|
|
`needs_profile` flag.
|
|
- **`/beta-pending` repurpose** in `BetaPending.jsx`. The
|
|
page now reads as "your request is in review" when the
|
|
viewer is pending; the v0.3.0 "private beta" framing
|
|
remains as the anonymous-viewer fallback.
|
|
- **Thin pending-access banner** at the top of every page
|
|
for `permission_state='pending'` viewers (other than
|
|
`/beta-pending` itself).
|
|
- **SPEC `§6` opening / `§6.1` / `§6.2` / `§14.1` / `§17` /
|
|
`§19.2`** corrections per §19.3 rule-2 — the admission
|
|
shift, the orthogonality of permission_state vs role / muted
|
|
/ notification-mutes, the new endpoints, and the
|
|
newly-surfaced §19.2 candidates (admin user-management page,
|
|
allowlist deprecation, admin notification on new request).
|
|
- **`backend/tests/test_beta_access_vertical.py`** — 9 new
|
|
tests covering: a fresh OTC user lands pending with empty
|
|
profile; the capture endpoint populates the fields and
|
|
keeps state pending; the capture endpoint refuses
|
|
anonymous / granted / revoked callers; a pending user is
|
|
refused write endpoints; an admin grant promotes pending →
|
|
granted; a grandfathered user is unaffected by the
|
|
migration; the OTC request endpoint accepts any email
|
|
regardless of allowlist state; the `allowed_emails` table
|
|
is still present in the schema.
|
|
|
|
### Changed
|
|
|
|
- **`backend/app/auth.py#require_contributor`** widens its
|
|
gate to refuse `permission_state != 'granted'` with HTTP
|
|
403. The §6.1 contributor capabilities (propose, branch,
|
|
PR, chat, claim) all funnel through this dependency, so
|
|
the widening covers them transitively. `SessionUser` now
|
|
carries `permission_state` (default `'granted'` for the
|
|
dataclass-default fallback path).
|
|
- **`backend/app/otc.py#request_code`** drops the allowlist
|
|
check from the OTC request flow. The `RequestOutcome`
|
|
shape loses the `'allowlist'` reason (replaced by
|
|
`'sent'` / `'cooldown'` / `'invalid'`).
|
|
- **`backend/app/otc.py#provision_or_link_user`** sets
|
|
`permission_state='pending'` explicitly on a fresh row.
|
|
Grandfathered (link-by-email) users pass through with
|
|
their existing column value.
|
|
- **`backend/app/auth.py#provision_user`** (OAuth fallback)
|
|
now sets `permission_state='granted'` explicitly on a
|
|
fresh row. The OAuth callback still consults the
|
|
`is_allowed_sign_in` allowlist check (the legacy fallback
|
|
path retains its v0.3.0 admission shape during the OAuth
|
|
migration window).
|
|
- **`backend/tests/test_otc_vertical.py`** — the
|
|
`test_otc_request_silently_drops_when_email_not_on_allowlist`
|
|
test (asserted the v0.7.0 allowlist gate) is replaced by
|
|
`test_otc_request_admits_emails_regardless_of_allowlist_population`
|
|
which asserts the v0.8.0 open-request contract. The
|
|
on-list test stays as a regression net for the
|
|
rate-limit / outbound-buffer plumbing.
|
|
- **`SPEC.md`** §6 opening, §6.1, §6.2, §14.1, §17, §19.2
|
|
per §19.3 rule-2.
|
|
- **`VERSION`** → `0.8.0`. `frontend/package.json#version` and
|
|
the lockfile mirror.
|
|
|
|
### Deferred to later releases
|
|
|
|
- **Admin user-management page** at `/admin/users` (item #7,
|
|
v0.9.0) — replaces the manual DB `UPDATE` gesture.
|
|
- **Allowlist UI deprecation** (also v0.9.0) — once the
|
|
admin user-management page lands, the `/admin/allowlist`
|
|
surface and the `allowed_emails` table both retire.
|
|
- **Admin email notification on new beta request** (item #7
|
|
again, v0.9.0).
|
|
- **Revoke gesture in the UI** — the `permission_state='revoked'`
|
|
state is wired in the schema and the auth gate; v0.9.0 ships
|
|
the admin UI that flips the column.
|
|
|
|
## 0.13.0 — 2026-05-28
|
|
|
|
**Minor — schema migration required; new optional env vars.** This
|
|
release ships the cookie / privacy consent surface (roadmap item #11,
|
|
SPEC §14.5 / §14.6). Every viewer — authenticated and anonymous alike —
|
|
now sees a non-modal bottom-of-page banner on first visit asking which
|
|
categories of cookies they allow (essential / essential + analytics /
|
|
essential + analytics + other). The choice persists in `localStorage`
|
|
for anonymous viewers and in a new `cookie_consent` table for
|
|
authenticated viewers, with server-side overriding local on sign-in.
|
|
The framework also ships default `/privacy` and `/cookies` policy pages
|
|
that deployments can layer their own policy URL on top of via two new
|
|
optional env vars. No analytics SDK ships in this release — the
|
|
consent infrastructure is wired so item #13 (v0.15.0) can read from
|
|
`frontend/src/lib/consent.js` when the SDK lands.
|
|
|
|
### Added
|
|
|
|
- **Cookie consent banner** (`frontend/src/components/CookieConsentBanner.jsx`).
|
|
Non-modal, bottom of viewport. Three single-select choices with
|
|
inline descriptions. Visible until the user makes a choice; hides
|
|
thereafter. Reachable for revision via the settings surface.
|
|
- **Consent helper** (`frontend/src/lib/consent.js`). Exports
|
|
`getConsent()`, `hasChosen()`, `onConsentChange(cb)`, `setConsent()`,
|
|
`hydrateFromServer()`, `clearLocal()`. Cross-tab sync via the
|
|
`storage` event. Item #13's analytics SDK reads consent here before
|
|
importing.
|
|
- **Privacy and cookies policy pages**
|
|
(`frontend/src/pages/Privacy.jsx`, `frontend/src/pages/Cookies.jsx`).
|
|
Default minimal policies that describe the framework's stance and
|
|
list the cookies the framework sets. Deployments override via the
|
|
two new env vars below; the framework's stub always renders above
|
|
the link so the framework-level contract stays visible.
|
|
- **"Privacy & cookies" tab** in `/settings/notifications` showing
|
|
the current consent choice, the recorded-at stamp, and a "Change"
|
|
button that re-opens the banner via a custom DOM event.
|
|
- **`§17` endpoints** —
|
|
- `GET /api/users/me/cookie-consent` — read the current consent
|
|
record.
|
|
- `PUT /api/users/me/cookie-consent` — write a new consent record.
|
|
Upserts a single row per user, stamps `recorded_at` to now,
|
|
accepts `essential` for symmetry but always persists it as true.
|
|
- **Schema migration** `013_cookie_consent.sql` — new
|
|
`cookie_consent` table keyed by `user_id`, three flags
|
|
(`essential`, `analytics`, `other_cookies`), and `recorded_at`.
|
|
(Renumbered from `012_*` during driver integration because v0.7.0
|
|
also added a `012_otc.sql` migration that landed in the integration
|
|
order before this one.)
|
|
- **SPEC `§14.5` Cookie / privacy consent** — settles the banner
|
|
shape, the three-category single-select, the storage shape (local
|
|
for anon, server row for authenticated), the precedence rule on
|
|
sign-in, and the `consent.js` helper surface for downstream
|
|
callers including item #13.
|
|
- **SPEC `§14.6` Privacy and cookies policy pages** — settles the
|
|
`/privacy` and `/cookies` routes, the framework's stub content, and
|
|
the `VITE_PRIVACY_POLICY_URL` / `VITE_COOKIES_POLICY_URL` override
|
|
shape.
|
|
- **SPEC `§5`** — names the `cookie_consent` table in the canonical
|
|
app-tables list.
|
|
- **SPEC `§17`** — lists the two new cookie-consent endpoints.
|
|
- **SPEC `§19.2`** — surfaces four candidates: policy content via
|
|
content-repo file vs env var, GPC / DNT headers, multi-language
|
|
consent text, and the item #13 analytics-SDK gating dependency.
|
|
|
|
### Changed
|
|
|
|
- **`frontend/.env.example`** — documents the two new optional env
|
|
vars `VITE_PRIVACY_POLICY_URL` and `VITE_COOKIES_POLICY_URL`. Unset
|
|
is supported; defaults render the framework's stub.
|
|
- **`backend/app/api_notifications.py`** — module docstring grew two
|
|
endpoint lines; the new endpoints sit alongside the existing
|
|
`/api/users/me/*` neighbors.
|
|
- **`frontend/src/App.jsx`** — registers `/privacy` and `/cookies`
|
|
routes (anonymous-reachable), wires `<CookieConsentBanner>` into
|
|
the global chrome, and listens for a `rfc-app:cookie-consent-reopen`
|
|
custom event to re-open the banner from the settings surface.
|
|
|
|
### Upgrade steps (from 0.7.0)
|
|
|
|
- You **MUST** rebuild the frontend and restart the backend after
|
|
upgrading. `frontend/package.json#version` and `VERSION` both move
|
|
to `0.13.0` and the build embeds the new env-var contract.
|
|
- You **MUST** apply schema migration `013_cookie_consent.sql`. The
|
|
migration creates a single new table keyed by `user_id` with three
|
|
flag columns and a `recorded_at` stamp. The framework runs
|
|
migrations automatically at process start; no manual step is
|
|
required beyond restarting the backend so the migration runner
|
|
picks the file up.
|
|
- You **MAY** set `VITE_PRIVACY_POLICY_URL` to an http(s) URL that
|
|
points at your deployment's full privacy policy. The framework's
|
|
`/privacy` page renders its built-in stub above a link to the
|
|
configured URL. Unset is supported — the stub is sufficient for a
|
|
default-config deployment.
|
|
- You **MAY** set `VITE_COOKIES_POLICY_URL` to an http(s) URL that
|
|
points at your deployment's full cookies policy. Same shape as the
|
|
privacy URL.
|
|
- You **MAY** announce the new consent banner to your users. Existing
|
|
authenticated users will see the banner on their next visit
|
|
(because their `cookie_consent` row does not yet exist); their
|
|
current sessions remain valid.
|
|
|
|
## 0.7.0 — 2026-05-28
|
|
|
|
**Minor — schema migration required; new auth path is additive.**
|
|
This release lands email + one-time-code sign-in (roadmap item #5,
|
|
SPEC §6.2) as the primary human-auth gesture. Users sign in by
|
|
typing their email, receiving a six-digit code via email, and
|
|
entering it. The Gitea OAuth callback (`/auth/callback`) remains
|
|
functional during migration — the new UI no longer points at it
|
|
primarily, but a "Sign in with Gitea (fallback)" link survives on
|
|
the new login surface so users with active OAuth sessions or older
|
|
invite paths still have a way in. A future release retires the
|
|
OAuth path entirely once every active user has signed in at least
|
|
once via OTC.
|
|
|
|
The migration path for existing users: first OTC sign-in matches by
|
|
`users.email` (case-insensitive) to the OAuth-era row and reuses
|
|
that row's `id` and `gitea_id`. New users provisioned via OTC carry
|
|
`gitea_id = NULL` and `gitea_login = NULL`. The `gitea_id` linker
|
|
remains the canonical handle for grandfathered users; `email`
|
|
becomes the identity key for everything provisioned after v0.7.0.
|
|
|
|
The Gitea **bot** user + token are still required (server-side git
|
|
operations — repo reads, PR creation — still flow through it). Only
|
|
the operator-facing sign-in surface moves.
|
|
|
|
### Upgrade steps (from 0.6.0)
|
|
|
|
1. **MUST** restart the backend so migration `012_otc.sql` runs.
|
|
The migration rebuilds the `users` table (SQLite cannot ALTER
|
|
COLUMN); existing rows pass through unchanged, but the new
|
|
schema relaxes `gitea_id` / `gitea_login` to nullable (with
|
|
partial unique indexes that ignore NULL) and adds a partial
|
|
unique index on `email`. A new `otc_codes` table is created.
|
|
2. **MUST** confirm the SMTP overlay is set (`SMTP_HOST`,
|
|
`SMTP_PORT`, `SMTP_USER`, `SMTP_PASSWORD`, `SMTP_STARTTLS`,
|
|
`EMAIL_FROM`, `EMAIL_FROM_NAME`). The OHM overlay already
|
|
carries these as of v0.5.0; deployments without them fall back
|
|
to logging the code to stdout (dev-only path — production
|
|
visitors will not receive their codes).
|
|
3. **SHOULD** announce the new email-based sign-in to existing
|
|
users. Wording suggestion: "You can now sign in by entering
|
|
your email and a one-time code we'll send you. Your old
|
|
account is linked automatically the first time you sign in."
|
|
4. **MAY** keep the existing OAuth callback as a fallback path.
|
|
The new login UI surfaces a small "Sign in with Gitea
|
|
(fallback)" link beneath the primary email/code form; a
|
|
deployment that prefers to hide it can override the Login
|
|
component in a future framework release that exposes the link
|
|
behind a feature flag. For v0.7.0, the link is hard-coded.
|
|
|
|
### New environment variables (all optional with defaults)
|
|
|
|
- `OTC_TTL_MINUTES` (default `10`) — how long a one-time code is
|
|
valid after issuance. Re-requesting invalidates the prior code
|
|
immediately regardless of TTL.
|
|
- `OTC_REQUEST_COOLDOWN_SECONDS` (default `60`) — per-email cooldown
|
|
between successive `/auth/otc/request` calls. The endpoint returns
|
|
HTTP 429 when the cooldown blocks a request (the loud-failure
|
|
shape; the abuse path is visible rather than swallowed).
|
|
|
|
No new secrets are required. The existing `SECRET_KEY` continues to
|
|
sign session cookies; OTC codes are bcrypt-hashed at rest using a
|
|
per-row salt the library generates.
|
|
|
|
### Added
|
|
|
|
- **`POST /auth/otc/request`** — body `{email}`. Generates a six-digit
|
|
code, stores its bcrypt hash with an expiry, and dispatches a plain
|
|
text email via the existing SMTP layer. Returns HTTP 200 (`{ok:true}`)
|
|
uniformly so allowlist state is not leaked. Returns HTTP 429 when
|
|
the per-email cooldown blocks the request.
|
|
- **`POST /auth/otc/verify`** — body `{email, code}`. Validates the
|
|
bcrypt hash against the most-recent unconsumed non-expired row, marks
|
|
the row consumed, provisions or links the `users` row by email, and
|
|
stores the session cookie. Returns HTTP 200 on success, HTTP 400 on
|
|
any failure (expired, consumed, wrong, unknown).
|
|
- **`backend/migrations/012_otc.sql`** — creates `otc_codes` and
|
|
rebuilds `users` with nullable `gitea_id` / `gitea_login` plus a
|
|
partial unique index on `email`.
|
|
- **`backend/app/otc.py`** — the OTC request/verify state machine and
|
|
the `provision_or_link_user` linker.
|
|
- **`backend/app/email_otc.py`** — outbound OTC mail composition. Reuses
|
|
the SMTP plumbing from `email.py` (`EmailConfig.from_env()`) and the
|
|
test buffer (`_SENT`) but writes its own envelope (no unsubscribe
|
|
footer, no quiet-hours hold — OTC mail carries a credential and
|
|
ignores notification preferences).
|
|
- **`frontend/src/components/Login.jsx`** — two-step sign-in surface
|
|
at `/login`. Step 1: enter email → request code. Step 2: enter
|
|
six-digit code → verify. Cmd/Ctrl+Enter on the code field submits.
|
|
The previous header "Sign in" link and the `Welcome` component's
|
|
inline link now route to `/login` instead of jumping straight to
|
|
the Gitea OAuth dance.
|
|
- **SPEC `§6.1` / `§6.2` / `§14.1` / `§17` / `§19.2`** corrections per
|
|
§19.3 rule-2 — see below.
|
|
- **`backend/tests/test_otc_vertical.py`** — 11 new tests covering
|
|
the happy path, expired/consumed/wrong codes, the per-email rate
|
|
limit (and its per-email isolation), the allowlist gate, the
|
|
migration link to OAuth-era users, fresh provisioning, and the
|
|
prior-code-invalidation behavior on re-request.
|
|
|
|
### Changed
|
|
|
|
- **`backend/app/auth.py#current_user`** — coerces NULL `gitea_id` and
|
|
NULL `gitea_login` to `0` / `""` so the `SessionUser` shape stays
|
|
stable for OTC-only users. The DB remains the source of truth for
|
|
"is this user OAuth-linked" (`gitea_id IS NOT NULL`).
|
|
- **`backend/requirements.txt`** — adds `bcrypt>=4.2` for OTC code
|
|
hashing. Pure-Python wheels are available on every platform the
|
|
deployment matrix targets; `pip install -r backend/requirements.txt`
|
|
picks it up.
|
|
- **`frontend/src/App.jsx`** — adds the `/login` route and replaces
|
|
the header "Sign in" `<a href="/auth/login">` with `<Link to="/login">`.
|
|
The `Welcome` component's inline sign-in link follows suit.
|
|
- **`frontend/src/components/Landing.jsx`** — the `/welcome` page's
|
|
primary action moves from "Sign in with Gitea" to "Sign in" pointing
|
|
at `/login`.
|
|
|
|
### Deferred to later releases
|
|
|
|
Per the v0.7.0 scope discipline (the foundation for items #6, #8, #9,
|
|
#10), several adjacent capabilities are intentionally not in this
|
|
release and surface as §19.2 candidates:
|
|
|
|
- **First-OTC profile capture** (first name, last name, "why") — item
|
|
#6, expected v0.8.0.
|
|
- **Open beta-access request flow** replacing the allowlist gate —
|
|
also item #6, v0.8.0.
|
|
- **Passcodes** (a long-term reauth token alternative) — item #8,
|
|
expected v0.10.0.
|
|
- **Device-trust 30-day skip** — item #9, expected v0.11.0.
|
|
- **Cloudflare Turnstile** on `/auth/otc/request` — item #10,
|
|
expected v0.12.0.
|
|
- **Removing the Gitea OAuth `/auth/callback` route entirely** — a
|
|
later release after every active user has signed in via OTC.
|
|
|
|
## 0.6.0 — 2026-05-28
|
|
|
|
**Minor — no operator action required.** A sweep-the-edges hardening
|
|
release (roadmap item #4, "anon discuss + contribute off-limits") that
|
|
audits every write-shaped backend endpoint and asserts each one
|
|
enforces an explicit `auth.require_contributor` (or stricter) gate
|
|
before doing any state-changing work. v0.3.0 hid write affordances
|
|
behind a sign-in CTA on the frontend; v0.5.0 added the PR-less
|
|
discussion surface with its own write gate; v0.6.0 sweeps the rest
|
|
and adds a regression test net so future endpoints can't quietly
|
|
ship without a gate. No schema migration, no env-var changes, no
|
|
new dependencies. The only behavioural change is one tightening: the
|
|
`GET /api/rfcs/<slug>/graduate/progress` SSE now requires
|
|
`auth.require_user` since it surfaces operator-visible step detail
|
|
(repo name, PR number, rollback steps) not part of the v0.3.0
|
|
anonymous-read contract for catalog/RFC bodies.
|
|
|
|
### Changed
|
|
|
|
- **`backend/app/api_graduation.py`** — `GET /graduate/progress`
|
|
now calls `auth.require_user(request)` as its first line.
|
|
Anonymous callers receive 401 instead of being able to subscribe
|
|
to a graduation's SSE. The floor is `require_user` (not
|
|
`require_contributor`) so a write-muted operator can still observe
|
|
the progress of a graduation they kicked off before being muted.
|
|
- **SPEC `§6.1`** (`SPEC.md`) — the Anonymous role's bullet now
|
|
documents the v0.6.0 audit: every write-shaped endpoint in §17
|
|
enforces an explicit gate; anonymous writes refuse 401. The list
|
|
of audited write families is recorded in-line.
|
|
- **SPEC `§10.10`** — the discussion-vs-contribution section now
|
|
records that the v0.5.0 write gates have a regression test net
|
|
(`test_anon_offlimits_vertical.py`) added in v0.6.0.
|
|
- **SPEC `§17`** — the `GET /api/rfcs/<slug>/graduate/progress`
|
|
bullet now documents the `require_user` gate added in v0.6.0,
|
|
with the rationale.
|
|
|
|
### Added (tests)
|
|
|
|
- **`backend/tests/test_anon_offlimits_vertical.py`** — twelve new
|
|
tests asserting that every write-shaped endpoint surveyed in the
|
|
v0.6.0 audit refuses anonymous callers with 401, and that the five
|
|
anonymous-read surfaces (health, philosophy, auth/me, catalog,
|
|
RFC view, discussion threads, proposals) stay reachable. Sixty-six
|
|
assertions in total, covering: propose; proposal merge / decline /
|
|
withdraw; branch promote-to-branch / start-edit-branch / metadata /
|
|
manual-flush / visibility / grants (POST + DELETE) / threads (POST)
|
|
/ messages (POST) / resolve / chat-seen / changes (accept / decline
|
|
/ reask) / chat-stream; super-draft start-edit-branch + metadata;
|
|
PR pr-draft / open / seen / review / merge / withdraw /
|
|
description / resolution-branch; discussion thread create + message
|
|
post + resolve; admin role / mute / allowlist (POST + DELETE) plus
|
|
the admin reads; notification preferences / quiet-hours / watch /
|
|
mark-read / user-mute (POST + DELETE); funder credentials (POST +
|
|
DELETE) + consent (POST + DELETE); graduation kickoff + claim +
|
|
progress SSE; PR review page anonymous-readable.
|
|
|
|
### Anonymous-writeable allowlist
|
|
|
|
Two endpoints are intentionally anonymous-by-design. They are not
|
|
audit findings; they are documented here so the contract is
|
|
explicit:
|
|
|
|
- `GET /auth/login` and `GET /auth/callback` — the OAuth
|
|
round-trip. Anonymous-by-design because they ARE the sign-in
|
|
entrypoint.
|
|
- `POST /api/webhooks/gitea` and `POST /api/webhooks/email-bounce`
|
|
— anonymous in the session sense but authenticated by HMAC shared
|
|
secret (`GITEA_WEBHOOK_SECRET` and `WEBHOOK_EMAIL_BOUNCE_SECRET`
|
|
respectively). The webhook receiver is the wrong place for an
|
|
authenticated session; the shared-secret shape is correct.
|
|
|
|
### §19.2 candidates surfaced
|
|
|
|
- None unique to this release. The two pre-existing candidates the
|
|
audit touched — anonymous-read polish for the discussion surface
|
|
(carried from v0.5.0) and the operator-visibility floor on
|
|
graduation progress — were settled here as `require_user` on
|
|
`/graduate/progress` rather than deferred.
|
|
|
|
### Upgrade steps (from 0.5.0)
|
|
|
|
1. The deployment **MUST** rebuild the frontend (`npm install &&
|
|
npm run build`) so the frontend bundle's reported version matches
|
|
the backend's. No new env vars; existing `frontend/.env` is
|
|
sufficient.
|
|
2. The deployment **MUST** restart the backend so the new
|
|
`require_user` gate on `/graduate/progress` is enforced. No
|
|
schema migration runs.
|
|
3. The deployment **MAY** announce the audit completion to
|
|
operators: every write-shaped backend endpoint now enforces an
|
|
explicit gate, and the `test_anon_offlimits_vertical.py` test
|
|
net asserts the contract on every CI run. The audited surfaces
|
|
are listed in the `Added (tests)` section above and in `SPEC.md`
|
|
§6.1.
|
|
4. The deployment **MUST NOT** assume any new envelope behaviour:
|
|
v0.6.0 is purely a hardening release. No schema, no env, no
|
|
dependency changes; the operator's role is reduced to rebuild +
|
|
restart.
|
|
|
|
|
|
## 0.5.0 — 2026-05-27
|
|
|
|
**Minor — no operator action required.** This release wires the
|
|
PR-less per-RFC discussion surface (roadmap item #3, SPEC §10.10).
|
|
An RFC's main view now carries a discussion panel distinct from PR
|
|
comments and branch chat; contribution — proposing edits the document
|
|
will land — still requires opening a PR via the §10.1 affordance.
|
|
The substrate is the existing `threads` / `thread_messages` tables;
|
|
rows with `threads.branch_name IS NULL` scope to the RFC's main view.
|
|
No schema migration is required.
|
|
|
|
### Added
|
|
|
|
- **PR-less discussion endpoints** (`backend/app/api_discussion.py`)
|
|
mounted at `/api/rfcs/<slug>/discussion/...`:
|
|
- `GET /api/rfcs/<slug>/discussion/threads` — list threads where
|
|
`threads.branch_name IS NULL`. Anonymous-readable per the v0.3.0
|
|
contract; the default whole-doc chat thread is materialized
|
|
lazily on first read.
|
|
- `POST /api/rfcs/<slug>/discussion/threads` — open a new
|
|
discussion thread (`thread_kind='chat'`, `anchor_kind='whole-doc'`,
|
|
`branch_name=NULL`). Body: optional `label`, optional first
|
|
`message`. Requires contributor role.
|
|
- `GET /api/rfcs/<slug>/discussion/threads/<thread_id>/messages`
|
|
— read messages on a discussion thread. Anonymous-readable.
|
|
- `POST /api/rfcs/<slug>/discussion/threads/<thread_id>/messages`
|
|
— post a message. Body: `text`, optional `quote`. Requires
|
|
contributor role.
|
|
- `POST /api/rfcs/<slug>/discussion/threads/<thread_id>/resolve`
|
|
— resolve a discussion thread. Permission per §10.10: thread
|
|
creator, RFC owner / arbiter, or app admin / owner.
|
|
- **`RFCDiscussionPanel.jsx`** — the right-column surface on the RFC
|
|
view when the viewer is on `main`. Composer requires sign-in;
|
|
Cmd/Ctrl+Enter sends. Multiple threads surface as pill-shaped
|
|
tabs above the message feed. A "New thread" affordance opens a
|
|
fresh thread on the same RFC.
|
|
- **SPEC `§10.10` PR-less discussion vs. contribution** — settles
|
|
the distinction between discussion (RFC-scoped, no PR, both
|
|
anonymous-readable and contributor-writeable) and contribution
|
|
(still requires a PR via §10.1). Also extends `§5`'s `threads`
|
|
table commentary so the null-branch interpretation is documented
|
|
as actively used rather than reserved scaffold.
|
|
- **SPEC `§17`** — lists the five new `discussion/...` endpoints
|
|
in the illustrative table.
|
|
|
|
### Changed
|
|
|
|
- **`backend/app/chat.py`** — `_fan_out_chat` now passes
|
|
`branch_name` straight through to the notify chokepoint instead of
|
|
coercing `None` to `"main"`. The notifications row carries the
|
|
null through, which preserves the §15.7 reconciler's keying on
|
|
`(rfc_slug, branch_name)` for the eventual discussion-side
|
|
chat-seen advance (deferred to a §19.2 candidate). Existing
|
|
branch-scoped chat continues to pass non-null branch names; the
|
|
change is invisible to that path.
|
|
- **`backend/app/notify.py`** — `fan_out_chat_message`'s
|
|
`branch_name` parameter is now typed `str | None` to match the
|
|
v0.5.0 PR-less shape. Routing rules are unchanged; the inbox prose
|
|
renders identically whether the chat lives on a branch or on the
|
|
RFC's discussion surface, which is the right honest signal.
|
|
- **`RFCView.jsx`** — the right-column panel is now conditional:
|
|
when `branchParam === 'main'`, render `RFCDiscussionPanel`
|
|
(the new PR-less surface); otherwise render the existing
|
|
`ChatPanel` (branch chat unchanged).
|
|
|
|
### §19.2 candidates surfaced
|
|
|
|
- PR-less discussion: range / paragraph anchors (the data model
|
|
permits them; the UI is the deferred part).
|
|
- PR-less discussion: distinct notification `event_kind`s
|
|
(`open_rfc_discussion_thread`, etc.) if usage shows contributors
|
|
want to filter discussion-vs-branch in the §15.2 inbox.
|
|
- PR-less discussion: chat-seen cursor closing the §15.7
|
|
reconciliation loop for the new surface.
|
|
- PR-less discussion: AI participant invocation (discussion-only,
|
|
no `<change>` block side-effects).
|
|
- PR-less discussion: anonymous-read polish to match the v0.6.0
|
|
write-gate hardening (item #4).
|
|
|
|
### Upgrade steps (from 0.4.0)
|
|
|
|
1. The deployment **MUST** rebuild the frontend (`npm install &&
|
|
npm run build`) so `RFCDiscussionPanel.jsx` ships in the bundle.
|
|
No new env vars; existing `frontend/.env` is sufficient.
|
|
2. The deployment **MUST** restart the backend so the new
|
|
`api_discussion` router mounts. No schema migration runs — the
|
|
`threads` table already supports `branch_name IS NULL` per §5,
|
|
and the v0.5.0 build is the first to write rows in that shape.
|
|
3. The deployment **MAY** announce the new surface to its
|
|
contributors: the RFC view's main page now carries a discussion
|
|
panel below the document. Existing branch chat and PR review
|
|
surfaces are unchanged.
|
|
4. The deployment **SHOULD NOT** expect a hardening of the
|
|
anonymous-write gate in v0.5.0 — that lands in v0.6.0 (item #4).
|
|
v0.5.0's write paths already refuse anonymous posts, so no
|
|
pre-emptive operator action is needed.
|
|
|
|
## 0.4.0 — 2026-05-27
|
|
|
|
**Minor — no operator action required beyond rebuild + restart.** The
|
|
proposer of a new RFC is now the implicit first owner of its super-
|
|
draft entry, set automatically at propose time from the session user.
|
|
The §13.1 claim flow remains available for *additional* owners. No
|
|
schema changes, no env-var changes, no API-shape changes; only newly
|
|
proposed RFCs receive the auto-owner — existing super-drafts whose
|
|
`owners:` is empty are unaffected and can still be claimed via §13.1
|
|
as before. This is a §19.3 rule-2 spec correction: `SPEC.md` §9.1,
|
|
§9.2, and §13.1 are updated to reflect the new shape.
|
|
|
|
### Changed
|
|
|
|
- **`POST /api/rfcs/propose`** (`backend/app/api.py`) now sets
|
|
`Entry.owners = [user.gitea_login]` when constructing the new
|
|
super-draft entry, instead of `owners=[]`. The session's
|
|
`gitea_login` is the canonical source; the endpoint never accepted
|
|
an owner field from the request payload and still doesn't.
|
|
- **`SPEC.md` §9.1** narrowed: the "no proposed-owner or working-
|
|
group fields" sentence becomes a proposer-owner-auto / working-
|
|
group-deferred split, with a §19.3 rule-2 note.
|
|
- **`SPEC.md` §9.2** frontmatter shape: `owners: []` → `owners:
|
|
[<proposer.gitea_login>]`, with a §19.3 rule-2 note.
|
|
- **`SPEC.md` §13.1** reframed: claim flow is now a graduation-time
|
|
broadening for additional owners, not a precondition for the
|
|
proposer's own RFC. The §13.1 / §13.2 / §13.3 graduation pipeline
|
|
itself is unchanged — the "at least one owner" precondition for
|
|
graduation still holds and is now satisfied by default.
|
|
|
|
### Upgrade steps (from 0.3.0)
|
|
|
|
1. The deployment **MUST** rebuild and restart per the routine
|
|
deploy steps. No `.env` changes, no schema/migration changes, no
|
|
API-shape changes.
|
|
2. Operators **SHOULD** note that newly proposed RFCs after the
|
|
upgrade carry the proposer in `owners:` automatically. Existing
|
|
super-drafts with empty `owners:` are not migrated; they remain
|
|
claimable via the §13.1 flow exactly as before. No deployment-
|
|
side data action is required.
|
|
3. Deployments **MAY** communicate the UX shift to active proposers
|
|
(the "Claim ownership" affordance no longer applies to your own
|
|
newly proposed RFC), but the affordance simply hides on RFCs the
|
|
viewer already owns, so no operator-side action is required.
|
|
|
|
## 0.3.0 — 2026-05-27
|
|
|
|
**Minor — operator action required if a deployment wants to enable the
|
|
private-beta gate; no action required to stay open.** This release adds
|
|
an email allowlist that, when populated, restricts OAuth sign-in to the
|
|
listed emails while keeping all read paths public. Anonymous visitors
|
|
now see the full app (catalog, RFC bodies, public branch conversations)
|
|
in read-only mode instead of the §14.1 landing-page wall.
|
|
|
|
### Added
|
|
|
|
- **`allowed_emails` table** (`backend/migrations/011_allowlist.sql`).
|
|
Empty list = gate off (any successful OAuth provisions a user, as
|
|
before). Any rows present = gate on (only listed emails, plus
|
|
users already grandfathered by `gitea_id`, may sign in).
|
|
- **Admin → Allowlist tab** at `/admin/allowlist`. Add/remove emails,
|
|
see who added each row and when. Status banner shows whether the
|
|
gate is currently active.
|
|
- **`/beta-pending` page** shown after a rejected OAuth callback. Free-
|
|
text invite-contact line is configurable via the new
|
|
`VITE_BETA_CONTACT` env var (optional; falls back to a generic line).
|
|
- **Beta chips** next to the Discuss/Contribute mode toggle, the Sign
|
|
in link, and the header Sign-in button so anonymous viewers see
|
|
immediately what is gated.
|
|
- **Anonymous read mode** in the React app: the §14.1 Landing page is
|
|
preserved at `/welcome` for deployments that want to link to it, but
|
|
the default route now renders the full app shell with write
|
|
affordances hidden behind a sign-in CTA.
|
|
|
|
### Changed
|
|
|
|
- **`/auth/callback`** now consults `auth.is_allowed_sign_in()` after
|
|
fetching the Gitea profile. Rejected sign-ins clear the OAuth state
|
|
and redirect to `/beta-pending`; the session is not populated.
|
|
- **`Catalog`** receives a `viewer` prop. Anonymous viewers see "Sign
|
|
in to propose (Beta)" instead of "+ Propose New RFC".
|
|
- **`PhilosophyWithSidebar`** now reads `authenticated` from the
|
|
current viewer instead of hardcoded `true`.
|
|
|
|
### Fixed
|
|
|
|
- **Single-finger scroll on the `/philosophy` page** (and any other
|
|
`.chrome-pane`-hosted view: `/admin/*`, `/settings/notifications`)
|
|
was broken on iOS Safari. The `.app` container used `height: 100vh`,
|
|
which on iOS measures the URL-bar-hidden ("largest") viewport — so
|
|
`.app` overflowed what's actually visible. Combined with the
|
|
`body { overflow: hidden }` in `index.css`, this meant single-finger
|
|
touches on the visible area were consumed by the (blocked) page-
|
|
level scroll attempt rather than reaching the nested `.chrome-pane`
|
|
scroll. Two-finger touches bypassed the page-level layer and
|
|
one-finger then worked once the URL bar had collapsed. Switched
|
|
`.app` to `height: 100dvh` (dynamic viewport — adjusts as the URL
|
|
bar shows/hides), with `100vh` retained as a fallback for browsers
|
|
predating iOS 15.4 / Chrome 108.
|
|
|
|
### Upgrade steps (from 0.2.3)
|
|
|
|
1. The deployment **MUST** rebuild the frontend with the new
|
|
`VITE_BETA_CONTACT` env var optionally set in `frontend/.env` (it
|
|
is OK to leave it blank — the `/beta-pending` page falls back to
|
|
a generic line).
|
|
2. The deployment **MUST** restart the backend so migration
|
|
`011_allowlist.sql` runs. No data loss; the new table starts
|
|
empty, which keeps the gate off and preserves existing behavior.
|
|
3. To **enable** the private-beta gate, the deployment operator
|
|
**SHOULD** sign in once (so their `users` row exists and they
|
|
grandfather in by `gitea_id`), then open `/admin/allowlist` and
|
|
add the first invited email. The first row added turns the gate
|
|
on for any user not yet in `users`.
|
|
4. To **stay open**, do nothing — leave `allowed_emails` empty and
|
|
the deployment behaves exactly as 0.2.3.
|
|
5. The deployment **MAY** customise its `/beta-pending` contact line
|
|
by setting `VITE_BETA_CONTACT` (an email, a URL, or a short
|
|
instruction) before the frontend build. Unset is fine.
|
|
|
|
## 0.2.3 — 2026-05-26
|
|
|
|
**Patch — no operator action required.** Rebuild and restart per the
|
|
routine deploy steps; no `.env` changes, no schema changes, no
|
|
behavior changes a deployment would notice in steady state beyond
|
|
the new endpoint below.
|
|
|
|
### Added
|
|
|
|
- **`GET /api/health`** — an unauthenticated probe returning JSON
|
|
`{version, status}` for ops tooling. `version` is the running
|
|
framework version (the contents of `VERSION` per §20.1, read at
|
|
process start and cached as a module-level constant in
|
|
`backend/app/health.py`); `status` is `"ok"` with HTTP 200 in
|
|
v1. The `"degraded"` / HTTP 503 path stays reserved in the
|
|
response shape so a later release can wire real degradation
|
|
conditions without breaking the contract. The version-match
|
|
check is the structural value — a deploy-control-panel polling
|
|
the endpoint after `systemctl restart` catches the failure mode
|
|
where a restart did not pick up the new code. See `SPEC.md` §17
|
|
and the §19.2 settlement.
|
|
|
|
### Upgrade steps (from 0.2.2)
|
|
|
|
1. The deployment **MAY** configure its monitoring (Pingdom,
|
|
Healthchecks.io, the flotilla deploy control panel, etc.) to
|
|
probe `/api/health` and compare the returned `version` against
|
|
the tag last deployed. The endpoint is unauthenticated by
|
|
design — no PII in the payload, no session required.
|
|
|
|
## 0.2.2 — 2026-05-26
|
|
|
|
**Patch — no operator action required.** Rebuild the frontend and
|
|
restart per the routine deploy steps; no `.env` changes, no schema
|
|
changes, no behavior changes a deployment would notice in steady
|
|
state beyond the fix below.
|
|
|
|
### Fixed
|
|
|
|
- **Mermaid blocks rendered as raw code on `/philosophy`.**
|
|
`frontend/src/components/Philosophy.jsx` parsed PHILOSOPHY.md with
|
|
the bare `marked` import and `dangerouslySetInnerHTML`, so
|
|
```` ```mermaid ```` fences fell through as `<pre>` blocks rather
|
|
than rendered diagrams. Swapped to `MarkdownPreview`, which already
|
|
carries the lazy mermaid loader + SVG render path used by the RFC
|
|
body view, so the philosophy surface now renders mermaid the same
|
|
way RFC bodies do. Pre-existing gap, surfaced when a deployment
|
|
authored a mermaid block in its `PHILOSOPHY_PATH` override.
|
|
|
|
## 0.2.1 — 2026-05-26
|
|
|
|
**Patch — no operator action required.** Rebuild the frontend and
|
|
restart per the routine deploy steps; no `.env` changes, no schema
|
|
changes, no behavior changes a deployment would notice in steady
|
|
state.
|
|
|
|
### Fixed
|
|
|
|
- **PR view blank page (React #310).** `frontend/src/components/PRView.jsx`
|
|
declared its `threadsByKind` `useMemo` *after* the early-return
|
|
guard for the loading state (`if (!pr) return …`). On first mount
|
|
`pr` was null so the early return fired and 14 hooks were called;
|
|
on the second render `pr` was populated and execution reached the
|
|
`useMemo`, calling 15 hooks — violating the Rules of Hooks and
|
|
unmounting the page subtree (blank screen). The `useMemo` is now
|
|
declared above the early returns with optional-chaining on `pr`,
|
|
keeping the hook count stable between the loading and loaded
|
|
renders. Pre-existing bug in the post-Contribute-rewrite PR view;
|
|
surfaced in production by 0.2.0's graduation merge race fix
|
|
enabling the workflow that opens this code path.
|
|
|
|
## 0.2.0 — 2026-05-26
|
|
|
|
**Breaking config change.** The frontend now requires
|
|
`VITE_APP_NAME` to be set at build time. `npm run build` fails with a
|
|
clear message if it is missing. There is no default; every deployment
|
|
names itself.
|
|
|
|
### Upgrade steps (from 0.1.0)
|
|
|
|
1. The deployment **MUST** create a `frontend/.env` file before
|
|
building. See `frontend/.env.example` for the contract.
|
|
2. The deployment **MUST** set `VITE_APP_NAME` in `frontend/.env` to
|
|
the user-visible name it wants to ship — the string used as the
|
|
browser tab title, the header brand, and the landing H1. The
|
|
build will fail loudly if this is unset or blank.
|
|
3. The deployment **MUST** rebuild the frontend (`npm install && npm
|
|
run build`) and redeploy the resulting bundle. The previous
|
|
bundle does not read `VITE_APP_NAME`.
|
|
4. The deployment **SHOULD** verify the name appears correctly in
|
|
the browser tab and the header after redeploy before bumping its
|
|
`.rfc-app-version` pin to `0.2.0`.
|
|
5. The deployment **MAY**, in the same upgrade, take the opportunity
|
|
to retire any local overrides it was using to brand the
|
|
pre-0.2.0 hardcoded strings — those overrides are now obsolete.
|
|
6. The deployment **MUST NOT** continue to expect graduation step 4
|
|
to fail on Gitea's "Please try again later" race; that path is
|
|
now handled by `wait_for_mergeable` + bounded retry. Any local
|
|
workaround retrying graduation at the deployment level is now
|
|
redundant and **SHOULD** be removed.
|
|
|
|
### Fixed
|
|
|
|
- **Graduation merge race.** §13.3 step 4 (`merge_pr`) used to call
|
|
Gitea's merge endpoint the instant step 3 returned, which races
|
|
Gitea's background mergeability computation and produces the
|
|
`405 "Please try again later"` response. Step 4 now waits for the
|
|
`mergeable` field to become non-null (bounded by a 30s timeout) and
|
|
retries the merge call up to three times on the transient response.
|
|
See `backend/app/gitea.py#wait_for_mergeable` and
|
|
`backend/app/bot.py#_merge_with_retry`.
|
|
|
|
### Changed
|
|
|
|
- `frontend/src/App.jsx` header brand reads `VITE_APP_NAME` instead of
|
|
a hardcoded string.
|
|
- `frontend/src/components/Landing.jsx` H1 reads `VITE_APP_NAME`
|
|
instead of a hardcoded string. The pitch / deck / attribution copy
|
|
in this file is still deployment-specific and tracked as a follow-up
|
|
for extraction into deployment config.
|
|
- `frontend/index.html` `<title>` is rewritten at build time via Vite's
|
|
HTML transform.
|
|
|
|
### Added
|
|
|
|
- `frontend/.env.example` documenting the new required variable.
|
|
- Top-level `VERSION` file and this `CHANGELOG.md` as the canonical
|
|
release log.
|
|
- `SPEC.md` §20 (versioning and downstream deployments) as the
|
|
binding policy for the framework/deployment relationship.
|
|
- `docs/DEPLOYMENTS.md` as the practical guide for building on
|
|
rfc-app and upgrading existing deployments to new versions.
|
|
- `CLAUDE.md` at the repo root capturing the separation-of-concerns
|
|
rule for working sessions.
|
|
|
|
## 0.1.0 — v1 build
|
|
|
|
Initial release. See `docs/DEV.md` for the slicing plan and build
|
|
history.
|