Skip to content

Commit faa2b16

Browse files
authored
docs: codify the release confidence process with executable tooling (#1052)
Captures the process designed and executed for v1.11.0 so every future release reproduces it: - .github/RELEASE_PROCESS.md v2: changelog audit, risk-based test matrix (buckets A/B/C), the Docker image gate, fix-loop re-test policy, CI-based publishing path, communication structure with a mandatory credits section, retro, and the gotchas that cost iterations this cycle - ADR-005: why releases now pass a risk-based confidence process gated on the real image, with the v1.11.0 evidence (bugs the unit suite could not catch: SEARCH-index ORDER BY 500, credential clear no-op) - scripts/release-test/: the harness built during v1.11.0 — fresh-install + upgrade gate (release-image-test.sh) and the browsable RC stack with optional dev-data copy (rc-stack.sh), plus compose/nginx encoding the API_URL, host.docker.internal and SurrealDB import learnings - make release-test / release-stack / release-stack-down targets
1 parent d9ad391 commit faa2b16

8 files changed

Lines changed: 496 additions & 40 deletions

File tree

.github/RELEASE_PROCESS.md

Lines changed: 116 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,18 @@ Open Notebook uses a flow-driven release process. Work moves from `ready`
44
issues into pull requests, pull requests merge to `main`, and maintainers cut a
55
version when the branch has enough validated change to ship.
66

7+
This document covers both the **mechanics** (how to cut, build and publish) and
8+
the **confidence process** (how we know a release is good before users get it).
9+
It was redesigned during the v1.11.0 release ([ADR-005](../docs/7-DEVELOPMENT/decisions/ADR-005-release-confidence-process.md)).
10+
711
## Release Model
812

913
- Patch releases ship backwards-compatible fixes.
1014
- Minor releases ship backwards-compatible features and improvements.
1115
- Major releases are planned with a milestone when they include breaking
1216
changes or migrations that need user coordination.
13-
- Release candidates and community soak labels are no longer part of the
14-
process. Use the `in-dev-build` label for changes available in development
15-
images and `released` for shipped work.
17+
- Use the `in-dev-build` label for changes available in development images and
18+
`released` for shipped work.
1619

1720
## Normal Flow
1821

@@ -21,59 +24,132 @@ version when the branch has enough validated change to ship.
2124
3. Merge the pull request after review and required checks pass.
2225
4. Let the development build publish the `v1-dev` image from `main`.
2326
5. Cut a stable release when `main` has a coherent set of changes ready for
24-
users.
27+
users — following the confidence process below.
2528

26-
## Cutting A Stable Release
29+
## The Confidence Process
30+
31+
Releases keep getting bigger; ad-hoc verification does not scale. Before
32+
cutting, run this sequence:
33+
34+
### 0. Changelog audit
35+
36+
Diff `git log <last-tag>..main` against the `[Unreleased]` section of the
37+
CHANGELOG. Every merged PR must be represented (entries reference the issue
38+
number when one exists, the PR number otherwise). The changelog is the input
39+
for both the test plan and the release notes — close the gaps first, via PR.
2740

28-
1. Confirm `main` is green and review the changes since the previous release.
29-
2. Update `pyproject.toml` with the target semantic version.
30-
3. Move the relevant `CHANGELOG.md` entries from `Unreleased` into a dated
31-
version section.
32-
4. Create a GitHub release for the version tag, for example `v1.11.0`.
33-
5. Run the `Build and Release` workflow. Push `v1-latest` tags for normal
34-
stable releases.
35-
6. Verify the published images are available in GHCR and Docker Hub when
36-
Docker Hub credentials are configured.
37-
7. Mark shipped issues with `released` and close any release-tracking tasks.
41+
### 1. Risk-based test matrix
3842

39-
## Docker Image Publishing
43+
Build a matrix from the actual release diff: each change → what it can break
44+
and for whom → which bucket tests it. Pay special attention to
45+
**"does the protection break legitimate use?"** for security changes (e.g. an
46+
SSRF guard vs. self-hosted Ollama on localhost) and to anything a reverse
47+
proxy, an upgrade, or a big upload would exercise.
4048

41-
The publishing workflow, from local test to promoted release:
49+
Buckets:
50+
51+
- **A — automated, high confidence, run now**: full backend suite, frontend
52+
lint/tests/production build, the smoke-e2e agent (full API happy path + UI
53+
verification), targeted regression probes for the release's specific risks,
54+
dependency audit.
55+
- **B — automatable with investment**: decide per item whether to build the
56+
muscle now (it compounds: the image gate below started as a bucket-B item)
57+
or verify manually this once.
58+
- **C — needs the release owner**: real provider credentials, real TTS podcast
59+
generation, visual/UX judgment, and the final check of the pushed image.
60+
61+
### 2. The image gate — test the artifact, not the repo
62+
63+
A green suite on `main` is not a working image. Run:
4264

4365
```bash
44-
make docker-build-local # 1. Build for the current platform, test locally
45-
make docker-push # 2. Push version tags (does NOT update latest)
46-
# 3. Test the pushed version in staging/production
47-
make docker-push-latest # 4. Promote: push version + update v1-latest
66+
make docker-build-local # builds <version> + local tags
67+
make release-test TAG=<new> OLD_TAG=<previous>
4868
```
4969

70+
This runs two scenarios against real containers (`scripts/release-test/`):
71+
72+
- **Fresh install**: empty DB → migrations on boot → in-image worker processes
73+
a source → API/frontend/nginx-proxied checks.
74+
- **Upgrade**: boot the *published* previous image, seed data, swap to the new
75+
image on the same volume → migrations apply, data survives.
76+
77+
Caveat: `docker-build-local` tags with the current `pyproject.toml` version —
78+
`docker pull` the genuine previous tag before the upgrade test so you are not
79+
comparing the new build against itself.
80+
81+
### 3. Fix loop with a re-test policy
82+
83+
Findings become focused PRs through the normal review flow. After each merge:
84+
the cheap suite always re-runs; smoke/image gates re-run only if the fix
85+
touches what they cover; manual verification is not repeated unless the fix
86+
touches what was manually verified. Pre-existing bugs found along the way that
87+
are not release regressions become backlog issues instead of scope creep.
88+
89+
## Cutting A Stable Release
90+
91+
1. Confirm `main` is green and the confidence process above has run.
92+
2. Open the **cut PR**: bump `pyproject.toml`, date the `[Unreleased]` section
93+
as `[<version>] - <date>`.
94+
3. After merge: `make tag`.
95+
4. Build and push version images **via CI** (it holds the registry
96+
credentials): trigger the *Build and Release* workflow with
97+
`push_latest=false`. Local `make docker-push` also works but requires
98+
`docker login` on both registries.
99+
5. **Verify the pushed image** (bucket C, final gate): run it locally with
100+
`make release-stack TAG=<version> [DUMP=<dev-data-dump>]` — a browsable,
101+
isolated stack, optionally with a copy of real data — and walk the core
102+
flows in the browser.
103+
6. Publish the GitHub release. A non-prerelease publication triggers the
104+
workflow again and pushes the `v1-latest` tags automatically.
105+
7. Verify the `v1-latest` manifests on Docker Hub and GHCR (both arches, both
106+
variants), and mark shipped issues with `released`.
107+
108+
## Communication
109+
110+
Release notes follow this structure (see v1.11.0 as the reference):
111+
112+
1. One-line verdict + upgrade recommendation.
113+
2. Sections: Security, Features, Performance, Notable fixes.
114+
3. **Behavior changes for self-hosters** — anything that can require a config
115+
tweak on upgrade gets an explicit callout.
116+
4. **Thanks** — credit every contributor by handle with what they shipped
117+
(collect via `git log <last-tag>..<tag>` + `gh pr view` for handles), plus
118+
the issue reporters collectively. Never skip this section.
119+
120+
Announce on Discord after `v1-latest` is live.
121+
122+
## Retro
123+
124+
Close every release by asking: what should improve in this process? Apply the
125+
accepted improvements immediately — update this document, the scripts under
126+
`scripts/release-test/`, and the decision log while the context is fresh.
127+
128+
## Docker Image Publishing (reference)
129+
50130
| Command | What it does | Updates latest? |
51131
|---------|--------------|-----------------|
52132
| `make docker-build-local` | Build for current platform only (tags `<version>` + `local`) | No registry push |
53-
| `make docker-push` | Push version tags to registries | ❌ No |
54-
| `make docker-push-latest` | Push version + update `v1-latest` | ✅ Yes |
55-
| `make docker-release` | Full release (same as docker-push-latest) | ✅ Yes |
133+
| CI *Build and Release* (`push_latest=false`) | Push version tags via CI credentials | ❌ No |
134+
| GitHub release published (non-prerelease) | CI pushes version + `v1-latest` | ✅ Yes |
135+
| `make docker-push` / `docker-push-latest` | Local equivalents (need `docker login`) | ❌ / ✅ |
56136
| `make tag` | Create and push a git tag matching `pyproject.toml` ||
57137

58-
Publishing details:
59-
60138
- **Platforms:** `linux/amd64`, `linux/arm64`
61139
- **Registries:** Docker Hub + GitHub Container Registry
62140
- **Image variants:** regular + single-container (`-single`)
63141
- **Version source:** `pyproject.toml`
64142
- Build issues: `docker builder prune`, then `make docker-buildx-reset`
65143

66-
## Manual Verification
67-
68-
Before publishing a stable release, manually verify the areas touched by the
69-
release. At minimum, cover:
70-
71-
- installation or upgrade path changed by the release;
72-
- source ingestion and processing when content or worker behavior changed;
73-
- chat, search, notes, and notebooks when user workflows changed;
74-
- provider setup when model, credential, or API behavior changed;
75-
- Docker image startup when packaging, environment, or dependency changes
76-
landed.
77-
78-
Automated checks should catch regressions where possible, but the release owner
79-
chooses the manual matrix from the actual changes in the release.
144+
## Known Gotchas
145+
146+
- **RC stack on non-default ports needs `API_URL`** or the browser talks to
147+
`host:5055` — on a dev machine that is the development API (data crossover).
148+
`rc-stack.sh` sets it; remember this for any custom setup.
149+
- **Containerized app + host services**: credentials pointing at local
150+
services (Ollama, LM Studio) need `http://host.docker.internal:<port>`.
151+
- **SurrealDB import**: `OVERWRITE` goes after the type keyword
152+
(`DEFINE FIELD OVERWRITE …`), and the exporter can leak a log line into the
153+
dump — `rc-stack.sh` handles both.
154+
- **Multiple local SurrealDB instances**: check which one the dev `.env`
155+
actually points at (`SURREAL_URL`) before exporting data.

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Added
11+
- Release confidence process, documented and executable: `.github/RELEASE_PROCESS.md` now covers the risk-based test matrix (buckets A/B/C), the Docker image gate, the fix-loop re-test policy and the communication/credits/retro structure, backed by a new decision record (ADR-005) and versioned tooling under `scripts/release-test/``make release-test TAG= OLD_TAG=` runs fresh-install + upgrade scenarios against real images, and `make release-stack TAG= [DUMP=]` boots a browsable, isolated release-candidate stack (optionally with a copy of dev data) for manual verification
12+
813
## [1.11.0] - 2026-07-11
914

1015
### Added

Makefile

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
.PHONY: run frontend check ruff database lint api start-all stop-all status clean-cache worker worker-start worker-stop worker-restart
22
.PHONY: docker-buildx-prepare docker-buildx-clean docker-buildx-reset
33
.PHONY: docker-push docker-push-latest docker-release docker-build-local tag export-docs
4+
.PHONY: release-test release-stack release-stack-down
45

56
# Get version from pyproject.toml
67
VERSION := $(shell grep -m1 version pyproject.toml | cut -d'"' -f2)
@@ -43,6 +44,25 @@ docker-buildx-clean:
4344
docker-buildx-reset: docker-buildx-clean docker-buildx-prepare
4445
@echo "✅ Buildx reset complete!"
4546

47+
# === Release Testing (see .github/RELEASE_PROCESS.md) ===
48+
49+
# Automated image gate: fresh install + upgrade against real images.
50+
# Usage: make release-test TAG=1.12.0 OLD_TAG=1.11.0
51+
release-test:
52+
@test -n "$(TAG)" || (echo "usage: make release-test TAG=<new> [OLD_TAG=<previous>]"; exit 1)
53+
bash scripts/release-test/release-image-test.sh all \
54+
"$(DOCKERHUB_IMAGE):$(TAG)" \
55+
$(if $(OLD_TAG),"$(DOCKERHUB_IMAGE):$(OLD_TAG)")
56+
57+
# Browsable RC stack for manual verification (optionally with a data dump).
58+
# Usage: make release-stack TAG=1.12.0 [DUMP=/tmp/dev-dump.surql]
59+
release-stack:
60+
@test -n "$(TAG)" || (echo "usage: make release-stack TAG=<tag> [DUMP=<dump.surql>]"; exit 1)
61+
bash scripts/release-test/rc-stack.sh up "$(TAG)" $(DUMP)
62+
63+
release-stack-down:
64+
bash scripts/release-test/rc-stack.sh down "$(or $(TAG),unused)"
65+
4666
# === Docker Build Targets ===
4767

4868
# Build production image for local platform only (no push)
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# ADR-005: Releases pass a risk-based confidence process, gated on the real image
2+
3+
- **Status**: Accepted
4+
- **Date**: 2026-07 (established during the v1.11.0 release)
5+
- **Related**: [RELEASE_PROCESS.md](../../../.github/RELEASE_PROCESS.md), `scripts/release-test/`, [ADR-004](ADR-004-background-workers.md)
6+
7+
## Context
8+
9+
Releases grew from a handful of fixes to 50+ commits spanning security
10+
hardening, features, migrations and dependency changes. Verification was
11+
ad-hoc: a green test suite on `main` plus whatever manual checks the release
12+
owner remembered. v1.11.0 proved the gap empirically — the unit suite was
13+
fully green while `sort_by=title` returned a 500 (a SEARCH-index interaction
14+
only a real SurrealDB exhibits) and clearing credential fields silently
15+
no-oped (two mirror-image bugs, frontend and API, that only an end-to-end
16+
path reveals). Neither class of bug is catchable by mocked tests, and neither
17+
was: both were found by the process this record establishes.
18+
19+
## Decision
20+
21+
**Every stable release passes a risk-based confidence process before cutting,
22+
and the final gate runs against the built Docker image — the artifact users
23+
receive — not the repository.**
24+
25+
The process (mechanics in [RELEASE_PROCESS.md](../../../.github/RELEASE_PROCESS.md)):
26+
27+
1. **Changelog audit first** — the release diff, fully represented in the
28+
CHANGELOG, is the input for both the test plan and the communication.
29+
2. **Risk matrix over test list** — each change is classified by what it can
30+
break and for whom, then assigned to a bucket: **A** (automated now),
31+
**B** (automatable with investment — build the muscle when it compounds),
32+
**C** (release-owner judgment: real credentials, real TTS, UX, the pushed
33+
image). Security changes are probed for the inverse risk: does the
34+
protection break legitimate use?
35+
3. **The image gate** — fresh-install and upgrade-with-data scenarios run
36+
against real containers (`make release-test`), because packaging bugs
37+
(supervisord flags, uv sync modes, migration ordering) never appear in the
38+
suite.
39+
4. **Fix loop with a re-test policy** — findings become focused PRs; what
40+
re-runs after each merge is defined up front. Pre-existing bugs that are
41+
not release regressions become backlog issues, not scope creep.
42+
5. **Human gates stay human** — the pushed-image verification and the release
43+
publication require the release owner explicitly; automation prepares,
44+
people pull the trigger.
45+
6. **Retro closes the loop** — accepted improvements are applied to the
46+
process docs and scripts in the same session.
47+
48+
## Alternatives considered
49+
50+
- **Keep ad-hoc verification** — free, but v1.11.0 showed it misses exactly
51+
the bug classes that hurt users most (integration and packaging).
52+
- **Full CI-based E2E on every PR** — highest coverage, but a real
53+
SurrealDB + worker + image build pipeline on every PR is slow and expensive;
54+
the release boundary is where artifact-level confidence pays off.
55+
- **Community soak (RC tags)** — previously abandoned: slow feedback and low
56+
participation; a deliberate confidence process front-loads what soaking
57+
found late.
58+
59+
## Consequences
60+
61+
- Cutting a release costs hours, not minutes — deliberately: the cost scales
62+
with release size, which is the point of the risk matrix.
63+
- Release muscle is versioned in-repo (`scripts/release-test/`, make targets)
64+
and compounds: bucket-B investments from one release become bucket-A
65+
automation for the next.
66+
- The upgrade scenario requires published previous images to remain available
67+
on the registries.
68+
- The process assumes a release owner in the loop for buckets B/C — it is a
69+
confidence process, not full automation.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
services:
2+
surrealdb:
3+
image: surrealdb/surrealdb:v2
4+
command: start --log info --user root --pass root rocksdb:/mydata/test.db
5+
user: root
6+
volumes:
7+
- ${DATA_DIR:?set DATA_DIR}/surreal:/mydata
8+
9+
app:
10+
image: ${APP_IMAGE:?set APP_IMAGE}
11+
ports:
12+
- "127.0.0.1:${API_PORT:?set API_PORT}:5055"
13+
- "127.0.0.1:${FE_PORT:?set FE_PORT}:8502"
14+
environment:
15+
# Without an explicit API_URL the frontend's /config points the BROWSER
16+
# at host:5055 — on a dev machine that is the development API, not this
17+
# stack (silent data crossover between environments!)
18+
- API_URL=${RC_API_URL:-}
19+
- OPEN_NOTEBOOK_ENCRYPTION_KEY=${RC_ENCRYPTION_KEY:-release-test-key}
20+
- SURREAL_URL=ws://surrealdb:8000/rpc
21+
- SURREAL_USER=root
22+
- SURREAL_PASSWORD=root
23+
- SURREAL_NAMESPACE=${RC_SURREAL_NS:-open_notebook}
24+
- SURREAL_DATABASE=${RC_SURREAL_DB:-open_notebook}
25+
volumes:
26+
- ${DATA_DIR}/notebook:/app/data
27+
# Reach host services (e.g. Ollama on localhost:11434) via
28+
# host.docker.internal on Linux too; Docker Desktop resolves it natively.
29+
# Credentials pointing at local services must use
30+
# http://host.docker.internal:<port> when the app runs in a container.
31+
extra_hosts:
32+
- "host.docker.internal:host-gateway"
33+
depends_on:
34+
- surrealdb
35+
36+
proxy:
37+
image: nginx:alpine
38+
ports:
39+
- "127.0.0.1:${PROXY_PORT:?set PROXY_PORT}:80"
40+
volumes:
41+
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
42+
depends_on:
43+
- app

scripts/release-test/nginx.conf

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Reverse proxy in front of the frontend with default buffering ON — the SSE
2+
# streaming fix must work anyway because the API sends X-Accel-Buffering: no,
3+
# which nginx honors. Host is passed through verbatim to exercise the
4+
# frontend's Host validation with a real proxy in the path.
5+
server {
6+
listen 80;
7+
server_name _;
8+
9+
location / {
10+
proxy_pass http://app:8502;
11+
proxy_http_version 1.1;
12+
proxy_set_header Host $http_host;
13+
proxy_set_header X-Forwarded-Proto $scheme;
14+
proxy_set_header Upgrade $http_upgrade;
15+
proxy_set_header Connection "upgrade";
16+
proxy_read_timeout 300s;
17+
}
18+
}

0 commit comments

Comments
 (0)