Document production deployment modes

c44723542d48 · AtlantisPleb · · parent 61fccd016ed6

Document production deployment modes

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • added docs/deploymodes.md

Diff

1 file changed, +223 -0

docs/deploymodes.md added +223

@@ -0,0 +1,223 @@

1
# Deployment modes
2
3
Date: 2026-08-22
4
5
Status: Active in production. Forge tries a direct hot load first, then an
6
operator uses a compatible relup or an immutable rolling replacement when the
7
classifier refuses the direct path.
8
9
OpenAgents has three deployment classes.
10
11
| Update characteristic | Deployment path | Initiation | Examples |
12
| --- | --- | --- | --- |
13
| Allowlisted BEAM-only change | Direct hot load | Automatic after promotion | LiveViews, controllers, components, templates compiled into web modules, and status-page changes |
14
| Compatible application-level change | Relup | Operator fallback after exact-SHA qualification | Core Elixir modules, explicitly supported process-state upgrades, and coordinated application-version transitions |
15
| Runtime or infrastructure change | Full image build and rolling replacement | Operator fallback after exact-SHA qualification | Dependencies, configuration, migrations, assets, OTP, native libraries, and Docker changes |
16
17
The classifier evaluates the complete candidate. It does not hot-load the
18
eligible portion of a mixed change. If one changed path, toolchain identity,
19
or module requires a fallback, Forge refuses the entire direct transaction and
20
marks the target `needs_rolling_replace`.
21
22
## Direct hot load
23
24
This is the fastest path. It works when all of these conditions hold:
25
26
- The change produces only modified BEAM modules, or an added module that does
27
  not change the application specification.
28
- Every changed module appears in the hot-load allowlist.
29
- The change does not delete a module.
30
- The application version, application specification, Elixir version, OTP version, ERTS version, and `mix.lock` remain unchanged.
31
- The commit does not change any structurally classified path.
32
33
The current allowlist includes:
34
35
- `OpenAgentsWeb.*`
36
- `OpenAgents.Forge.Browse`
37
- `OpenAgents.Forge.MirrorWatch`
38
- `OpenAgents.Changelog`
39
- `OpenAgents.Scratch.*`
40
- `OpenAgents.BuildInfo`
41
42
Good direct-load candidates include:
43
44
- LiveView behavior and rendering
45
- Controllers and API response logic
46
- UI components
47
- HEEx templates
48
- Status-page presentation
49
- Request validation inside web modules
50
- Small fixes to the explicitly allowlisted Forge modules
51
- Published documentation under `priv/docs/**`
52
53
Published documentation is a deliberate exception to the general `priv/**`
54
rule. `OpenAgentsWeb.DocsCatalog` embeds every catalogued Markdown page and
55
declares those files as external compiler resources. A documentation edit
56
therefore changes an allowlisted BEAM module, and boot convergence can restore
57
the exact documentation snapshot after a node restart. Other `priv/**`
58
changes remain structural.
59
60
The deployment builds only the changed BEAM files, verifies the artifact and
61
manifest, prepares every node, loads the candidate on a canary, verifies the
62
canary, applies the candidate across the fleet, and commits the transaction.
63
It retains prior object code for an exact transactional rollback. Boot
64
convergence also caches the live artifact so a restarted node restores the hot
65
revision before it becomes ready.
66
67
It does not rebuild Docker images or restart the application.
68
69
Direct hot loading does not run OTP `appup` instructions or transform existing
70
process state. Keep stateful domain and supervision changes out of this path
71
unless the allowlist and production proof explicitly admit them.
72
73
## Relup
74
75
Use a relup when the update remains compatible with the installed Erlang runtime but needs a complete OTP release transition.
76
77
Potential relup candidates include:
78
79
- Core `OpenAgents.*` modules outside the direct allowlist
80
- GenServer state-shape changes that implement an explicit `code_change/3`
81
- Supervision-tree changes that the generated `appup` and the installed OTP
82
  applications can validate
83
- Coordinated updates across several applications or stateful processes
84
- Changes that need an application patch-version transition
85
- Code changes whose safety requires forward, reverse, and re-upgrade instructions
86
87
A relup installs a complete release through OTP's `release_handler`. It can
88
transform running process state and preserve process identity. That makes it
89
appropriate when loading a new BEAM file alone would not correctly update
90
existing state.
91
92
Relups require more validation than direct loads:
93
94
- A new patch version, such as `0.2.0` → `0.2.1`
95
- Valid generated `appup` instructions
96
- Compatible state-schema direction
97
- Matching OTP and target system
98
- Exact artifact and revision digests
99
- A working reverse transition
100
- Staging or disposable-node installation proof
101
102
A relup does not necessarily require a new serving image, but it does require building and validating complete release packages.
103
104
The classifier does not automatically manufacture or install a relup. It
105
records the direct-path refusal, and an operator builds the exact source and
106
target release pair, runs the 13-stage exact-SHA gate, and starts the
107
one-node-at-a-time coordinator. After every node returns `permanent`,
108
`OpenAgents.Forge.Targets.finish_relup_deployment/2` validates the package,
109
revisions, versions, duration, artifact digest, and node results before it
110
atomically settles the original Forge target and writes the deployment
111
receipt.
112
113
A valid generated relup can still fail OTP's live installation checks. In the
114
2026-08-22 production drill, `0.2.0` to `0.2.1` upgraded all three nodes in
115
53.876 seconds. The following `0.2.1` to `0.2.2` attempt stopped on its first
116
node when `release_handler` could not inspect `libring`'s
117
`DynamicSupervisor`. The node stayed on the known-good release, the
118
coordinator did not touch the other two nodes, and the operator used the
119
rolling-image fallback. Treat relup eligibility as something the package and
120
live preflight prove, not something a source diff alone guarantees.
121
122
## Full image build and rolling replacement
123
124
The classifier immediately treats these paths as structural:
125
126
- `mix.exs`
127
- `mix.lock`
128
- `config/**`
129
- `assets/**`
130
- `priv/static/**`
131
- `priv/repo/migrations/**`
132
- `priv/**`, except the embedded `priv/docs/**` catalog
133
- `rel/**`
134
- `native/**`
135
- `c_src/**`
136
- `Dockerfile` and `Dockerfile.*`
137
- Files ending in `.so`, `.nif`, `.dll`, or `.dylib`
138
139
A full build is also required when any of these identities change:
140
141
- Elixir
142
- OTP
143
- ERTS
144
- Application specification
145
- Dependency lock
146
- Native runtime
147
- System packages
148
149
Concrete examples include:
150
151
- Adding or upgrading a Hex dependency
152
- Changing runtime environment-variable handling
153
- Adding a database migration
154
- Rebuilding JavaScript or CSS
155
- Changing Postgres, networking, or endpoint configuration
156
- Adding an NIF or Rustler component
157
- Changing fonts or static images
158
- Upgrading Erlang or Elixir
159
- Changing the Docker image or installed OS packages
160
161
These changes produce a new immutable application image. The rollout replaces one node at a time while the other two continue serving traffic.
162
163
Before replacing a node, the coordinator requires an exact-SHA gate receipt,
164
an immutable image digest, two remaining healthy nodes, and a matching fleet
165
snapshot. It drains the selected node, replaces it, and verifies its revision,
166
release, database access, cluster membership, and load-balancer health before
167
continuing. It then settles the original target with a rolling-replacement
168
receipt. A failed relup does not authorize skipping these checks.
169
170
## Important conservative boundaries
171
172
Some changes could theoretically be hot-loaded but currently are not allowed directly.
173
174
For example, changing `OpenAgents.Accounts` produces ordinary BEAM code, but that namespace is not on the direct allowlist. The loop therefore refuses direct loading and routes the update to the fallback path. That refusal is intentional until that module class has production hot-load evidence.
175
176
Module deletion also forces a fallback. Loading new code does not reliably purge deleted modules or prove that no running process still references them.
177
178
Database migrations always require the full-release path because application code and database schema must remain compatible while old and new nodes overlap.
179
180
Changing only comments, uncompiled internal documentation, or another file
181
that produces no runtime artifact can result in a zero-module build. Forge can
182
record that target as live without changing running code, but operators must
183
not interpret a zero-module receipt as proof that an unembedded file appeared
184
inside an existing container.
185
186
## Version policy
187
188
Keep the application version unchanged for direct BEAM transactions. Use the
189
next patch version for a compatible full relup package. Change the minor
190
version only for a planned compatibility or feature boundary. Never publish
191
different release bytes under an existing version.
192
193
The application version and source revision serve different purposes. A new
194
commit does not require a new application version when the direct path can
195
deploy it.
196
197
## Receipts and status
198
199
`/status` and `/api/status` expose the active Forge lane, current target,
200
deployment class, recent targets and deployments, push-to-live timing, boot
201
convergence, and Forge-to-GitHub mirror freshness. The displayed fallback
202
order is `direct,relup,rolling`.
203
204
Every successful path must leave the original promoted target in `live` with
205
an immutable deployment receipt. A direct refusal remains visible as
206
`needs_rolling_replace` until the relup or rolling coordinator settles it. Do
207
not mark a target live because an operator observed healthy traffic.
208
209
## Practical rule
210
211
Use this decision order:
212
213
1. **Direct:** Only allowlisted Elixir web or approved Forge modules changed.
214
2. **Relup:** The change is still pure, runtime-compatible Elixir, but it affects broader or stateful application behavior.
215
3. **Rolling image:** The change affects dependencies, configuration, data schema, assets, native code, the runtime, or the container.
216
217
The loop makes this decision from the actual Git diff, compiled module digests, and toolchain identity. It does not rely on the commit message or an operator guessing correctly.
218
219
For operating procedures and recovery details, see:
220
221
- [Forge hot loop runbook](operations/forge-hot-loop.md)
222
- [Release deployment fallbacks](operations/release-deployment-fallbacks.md)
223
- [Transactional Forge deployment](operations/forge-transactional-deployment.md)

This page updates live while a promote is in flight · changelog