Document production relups and mirror repair

9624baa33625 · AtlantisPleb · · parent c1f17ef8fb68

Document production relups and mirror repair

Deploy story

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

built
0 modules in 0 ms

Changed files

  • modified docs/2026-08-21-hot-deploy-gap-audit.md
  • modified docs/operations/forge-hot-loop.md
  • modified docs/operations/release-deployment-fallbacks.md

Diff

3 files changed, +132 -79

docs/2026-08-21-hot-deploy-gap-audit.md modified +12 -14

@@ -31,20 +31,18 @@ After this audit was measured, two claims moved:

31 31
   transaction protocol — `live` Forge target and deployment receipt recorded,
32 32
   boot convergence satisfied, process uptimes unbroken. The operator assembled
33 33
   and applied the artifact manually; no receipted automated deploy exists yet.
34
2. **"Nothing needs re-implementing" holds for the hot-load loop only.** The
35
   general relup path was, at measurement time, unconnected: `BuildArtifact`
36
   classifies only `direct_candidate` and `needs_rolling_replace`, and
37
   `RelupDeployment` accepted only the fixed `0.1.0 → 0.2.0` proof transition.
38
   Later the same day the relup **mechanism** was generalized — coordinator
39
   admission, appup generation from the two builds' compiled modules,
40
   packaging, and install proofs; see
41
   [`docs/operations/forge-hot-loop.md`](operations/forge-hot-loop.md). The
42
   **lane** is still not connected: no classifier emits a relup class,
43
   `RelupDeployment.run/2` has no caller outside tests, the release gate runs
44
   neither `ops/forge/package-relup.sh` nor
45
   `ops/relup-proof/install-proof.sh`, and no receipt binds a package to the
46
   revision it was built from. Those are engineering gaps, not operator ones.
47
   Production approval for the lane also remains an open operator decision.
34
2. **The general relup lane now carries production traffic.** The mechanism
35
   now admits arbitrary semantic version pairs, generates appup instructions
36
   from exact compiled-module diffs, and emits digest-addressed packages.
37
   `OpenAgents.Forge.RelupPackage` binds the package to the running source
38
   revision, target revision, operating system and architecture, release
39
   versions, state schemas, and target artifact digest before it calls
40
   `RelupDeployment`. On 2026-08-21, production upgraded three nodes from
41
   `0.2.0@81e4c25` to `0.2.1@9763bf7` in 48.838 seconds without restarting the
42
   BEAM. The classifier still does not select relup automatically; an operator
43
   builds and submits the package when direct loading refuses a compatible
44
   application release. See
45
   [`docs/operations/forge-hot-loop.md`](operations/forge-hot-loop.md).
48 46
49 47
---
50 48
docs/operations/forge-hot-loop.md modified +65 -37

@@ -2,13 +2,14 @@

2 2
3 3
Date: 2026-08-21
4 4
5
Status: Enablement runbook for the automated direct-load deploy loop
5
Status: Relup and mirroring are active in production. The automated direct-load
6
loop still requires fleet enablement.
6 7
7 8
This is the operator procedure for the fast deployment lane: push to the owned
8 9
forge, promote, and watch a code-only change go live across the fleet in
9
seconds without an image build. It also records why the loop reads as
10
disconnected today (`forge.loop.last_ms: null`, mirror `off`) and exactly what
11
flips it on.
10
seconds without an image build. It also describes the production relup path,
11
the independent GitHub mirror repair worker, and the remaining direct-load
12
enablement work.
12 13
13 14
## Verified state of the lanes
14 15

@@ -16,16 +17,17 @@ flips it on.

16 17
| --- | --- | --- |
17 18
| Direct BEAM transaction on production | Works | `fa4b792` loaded across three nodes via the transaction protocol; `live` target and deployment receipt recorded; uptimes unbroken |
18 19
| Automated push → promote → build → hot-load loop | Not yet operating | No receipted automated deploy exists, so `/api/status` reports `loop.last_ms: null` and `push_to_live_ms: null` |
19
| General relup lane | Manual tools only | `RelupDeployment` admits any distinct `X.Y.Z` pair, but the classifier emits only `direct_candidate` or `needs_rolling_replace`, `RelupDeployment.run/2` has no caller outside tests, and the release gate runs neither packaging script. Relups are not production-approved (`release-deployment-fallbacks.md`) |
20
| General relup lane | Active in production | `RelupPackage` binds source and target revisions, release versions, state schemas, target system, and artifact digests before `RelupDeployment` upgrades one node at a time. Production upgraded `0.2.0@81e4c25` to `0.2.1@9763bf7` in 48.838 seconds without restarting the BEAM. |
21
| Forge-to-GitHub mirror | Active in production | The production mirror uses a write-enabled deploy key. `MirrorWatch` runs independently of the deploy lane, repairs drift every five minutes, and reports freshness. Forge and GitHub exposed 10 identical refs after the production drill. |
20 22
| Rolling image replacement | Works, default for structural changes | Current release tooling path |
21 23
22 24
Two consequences worth stating plainly:
23 25
24 26
- Code-only changes do **not** require an image roll once the loop is enabled;
25 27
  the whole web layer is allowlisted.
26
- The current `main` (PostHog analytics, `73ff250`) touches
27
  `config/config.exs` and `config/runtime.exs`, which the classifier must
28
  refuse as structural. That change rides rolling replacement correctly.
28
- Changes to `config/config.exs` or `config/runtime.exs` remain structural.
29
  The classifier must refuse them for direct loading and route them to a full
30
  release path.
29 31
30 32
## Why the metrics read null
31 33

@@ -34,8 +36,8 @@ Two consequences worth stating plainly:

34 36
  application produced a receipt without timing, so the projection has no
35 37
  sample yet. The first automated loop deploy populates both.
36 38
- Mirror state `off` means no repository has a configured mirror URL
37
  (`OPENAGENTS_FORGE_MIRROR_URLS` defaults to empty). Mirroring feeds GitHub;
38
  it plays no part in the deploy loop.
39
  (`OPENAGENTS_FORGE_MIRROR_URLS_JSON` defaults to an empty map). Mirroring
40
  feeds GitHub; it plays no part in the deploy loop.
39 41
40 42
## The chain, as wired
41 43

@@ -79,9 +81,9 @@ Work top to bottom; each step gates the next.

79 81
   `forge_build_dir`, and `forge_artifact_dir` on durable state disks. Runtime
80 82
   configuration validates these when the deploy lane is enabled.
81 83
4. **Enable the deploy lane flag** (`OPENAGENTS_FEATURE_FORGE_DEPLOY=true`).
82
   This starts `Builder`, `HotLoader`, `Janitor`, and `MirrorWatch` under
83
   `OpenAgents.Forge.Supervisor`. Boot convergence is already proven on this
84
   fleet.
84
   This starts `Builder`, `HotLoader`, and `Janitor` under
85
   `OpenAgents.Forge.Supervisor`. `MirrorWatch` runs under the same supervisor
86
   regardless of this flag. Boot convergence is already proven on this fleet.
85 87
5. **Allowlist**: no change needed. Baked configuration already admits the
86 88
   whole `OpenAgentsWeb.` layer plus `OpenAgents.Changelog`,
87 89
   `OpenAgents.Forge.Browse`, `OpenAgents.BuildInfo`, and the scratch prefix,

@@ -126,9 +128,9 @@ human reaction time.

126 128
A `reverted` outcome still warrants checking fleet convergence even though
127 129
this design captures each node's prior object code for exact rollback.
128 130
129
## The relup lane generalized, but nothing drives it
131
## Use the production relup lane
130 132
131
As of 2026-08-21 (later), the mechanism handles arbitrary version pairs:
133
The mechanism handles arbitrary version pairs:
132 134
133 135
1. **Coordinator admission is general.** `RelupDeployment` admits any distinct
134 136
   `X.Y.Z` pair whose state versions stay within `[1, 2]` and never regress —

@@ -142,7 +144,7 @@ As of 2026-08-21 (later), the mechanism handles arbitrary version pairs:

142 144
   state schema. `mix openagents.relup` then checks the generated relup against
143 145
   the same diff, so packaging fails rather than shipping a relup that would
144 146
   install part of a revision.
145
3. **Packaging and install proofs exist as tools.**
147
3. **Packaging and install proofs produce deployable artifacts.**
146 148
   `ops/forge/package-relup.sh --from-version A --to-version B [--from-rev]
147 149
   [--to-rev]` builds both releases in isolated worktrees, generates the
148 150
   two-way relup, embeds it, and emits digest-addressed tarballs plus a

@@ -155,33 +157,59 @@ As of 2026-08-21 (later), the mechanism handles arbitrary version pairs:

155 157
   refuses to run unless `OPENAGENTS_RELUP_PROOF_DISPOSABLE=1`, the URL host is
156 158
   loopback, and the database name contains `proof`, `smoke`, or `test`.
157 159
158
What is not true yet, and is engineering work rather than operator work:
159
160
- **No code calls the lane.** `RelupDeployment.run/2` has no caller outside
161
  tests. Nothing classifies a candidate as a relup, builds the request, or
162
  triggers a fleet relup; an operator drives the two scripts by hand.
163
- **The release gate does not run either script.** `ops/ci/gate.sh` runs
164
  `ops/relup-proof/run.sh`, `version-chain.sh`, and `kill-during-install.sh`,
165
  which exercise the pinned `0.1.0 → 0.2.0` pair only. Neither
166
  `package-relup.sh` nor `install-proof.sh` is a gate stage, so no receipt
167
  binds a general pair to a candidate SHA.
168
- **Nothing binds a package to its gate receipt.** `package.json` records the
169
  revisions it was built from, but `RelupDeployment` verifies the receipt for
170
  `request.sha` without checking that the artifact came from that revision.
171
172
Operator work still required before the lane carries production traffic:
173
174
- Production approval recorded against
175
  [`docs/operations/release-deployment-fallbacks.md`](release-deployment-fallbacks.md),
176
  which remains the authority that relups are not production-approved.
177
- A staging rehearsal of `package-relup.sh` followed by `install-proof.sh`
178
  against a disposable database, before any fleet use.
160
`OpenAgents.Forge.RelupPackage` closes the package-to-deployment boundary. It
161
rejects a package unless its source revision matches the running revision, its
162
target system matches the node, and its target tar matches the manifest
163
digest. It then constructs the bounded fleet request and calls
164
`RelupDeployment.run/2`. The coordinator verifies the target gate receipt,
165
rechecks exact membership between nodes, verifies the installed target
166
revision, and keeps the reverse release when it makes the target permanent.
167
168
Use patch versions for routine releases, such as `0.2.0` to `0.2.1`. Change
169
the minor version only when the release introduces a deliberate compatibility
170
or feature boundary. Never reuse a release version for different bytes;
171
`RelupNode` records the artifact digest for every unpacked version and rejects
172
a conflicting reuse.
173
174
The production drill on 2026-08-21 upgraded three nodes from
175
`0.2.0@81e4c25` to `0.2.1@9763bf7` in 48.838 seconds. Every node returned
176
`permanent`, retained `0.2.0` as `old`, reported the exact target revision,
177
and preserved its uptime. The operator used the documented emergency gate
178
override because the release was an explicitly authorized recovery and
179
enablement operation. Use a normal exact-SHA gate receipt for routine relups.
179 180
180 181
Note on scope: hot-load diffs and relups remain different artifact classes.
181 182
A BEAM-diff artifact cannot drive `release_handler`; only a full release
182 183
package can. That is why the build lane's classification stays two-class and
183 184
the relup lane consumes its own packages.
184 185
186
## Operate the GitHub mirror
187
188
Set `OPENAGENTS_FORGE_MIRROR_URLS_JSON` to a repository-to-URL JSON object.
189
Use a credential-free SSH URL such as
190
`ssh://github.com/OpenAgentsInc/openagents.com.git`; provide authentication
191
through a node-side SSH key and `GIT_SSH_COMMAND`. Never put a credential in
192
the URL or instance metadata.
193
194
`Pushes.mirror_storage_key/1` resolves the logical repository name to its
195
canonical storage UUID before reading refs. This distinction matters for
196
migrated repositories whose display name and storage key differ.
197
`MirrorWatch` runs even when `OPENAGENTS_FEATURE_FORGE_DEPLOY=false`, compares
198
the canonical `main` ref every five minutes, retries a full mirror push on
199
drift, and publishes `current` or `lagging` status.
200
201
After a rollout or storage repair, force convergence before validating the
202
mirror:
203
204
```elixir
205
storage_key = OpenAgents.Forge.Pushes.mirror_storage_key("openagents.com")
206
:ok = OpenAgents.Forge.Sync.ensure_cluster_fresh(storage_key)
207
:ok = OpenAgents.Forge.Pushes.mirror_now("openagents.com")
208
```
209
210
Compare complete, sorted `git ls-remote` output from Forge and GitHub. Do not
211
accept a matching `main` branch as proof if another branch or tag differs.
212
185 213
## References
186 214
187 215
- [Hot deploy gap audit](../2026-08-21-hot-deploy-gap-audit.md)
docs/operations/release-deployment-fallbacks.md modified +55 -28

@@ -2,23 +2,24 @@

2 2
3 3
Date: 2026-08-20
4 4
5
Status: Implemented. The isolated staging fleet uses the coordinated rolling
6
lane, and production uses the same one-node-at-a-time health boundary.
5
Status: Implemented. Production uses the relup lane for compatible application
6
releases and the coordinated rolling lane for structural changes.
7 7
8 8
## Purpose
9 9
10 10
Use this runbook when a candidate cannot use the direct BEAM transaction. The
11 11
classifier must choose one strategy for the complete candidate:
12 12
13
- Use a relup only for the supported `0.1.0` to `0.2.0` application transition
14
  and its tested reverse transition.
13
- Use a relup for a packaged `X.Y.Z` transition when the generated appup,
14
  target system, exact revisions, artifact digests, state schemas, and reverse
15
  path pass validation.
15 16
- Use rolling replacement for ERTS, OTP, dependency, native-code, asset,
16 17
  configuration, migration, module-deletion, or otherwise unclassified
17 18
  changes.
18 19
19
The relup lane is not production-approved. Use rolling replacement in
20
production only with explicit operator authority, an immutable image digest,
21
two remaining healthy nodes, and exact revision checks after each replacement.
20
Production relups and rolling replacements require explicit operator authority.
21
For a rolling replacement, also require an immutable image digest, two
22
remaining healthy nodes, and exact revision checks after each replacement.
22 23
23 24
## Local release gate
24 25

@@ -56,14 +57,22 @@ recovery. Do not use it for an ordinary release.

56 57
57 58
## Relup lane
58 59
59
### Supported transition
60
### Supported transitions
60 61
61
The repository owns one explicit two-way transition:
62
`RelupDeployment` accepts any distinct semantic `X.Y.Z` pair when the state
63
schema remains in the supported range and does not regress. The generated
64
appup remains the authoritative compatibility check. If it cannot describe
65
the complete module change, package generation or
66
`release_handler.check_install_release/1` refuses the transition.
62 67
63 68
| Direction | Application version | State schema |
64 69
| --- | --- | --- |
65
| Upgrade | `0.1.0` to `0.2.0` | 1 to 2 |
66
| Downgrade | `0.2.0` to `0.1.0` | 2 to 1 |
70
| Upgrade | `X.Y.Z` to a distinct semantic version | 1 or 2 to the same or a higher supported schema |
71
| Downgrade | The embedded reverse transition | The target schema back to the packaged source schema |
72
73
Use patch versions for routine releases. Reserve a minor-version change for a
74
deliberate compatibility or feature boundary. Never publish different bytes
75
under an existing version.
67 76
68 77
`OpenAgents.ReleaseState` is a supervised, long-lived process with a versioned
69 78
`OpenAgents.ReleaseState.State` struct. `code_change/3` preserves the PID and

@@ -72,16 +81,27 @@ advanced update explicitly. Do not add a version transition until its forward,

72 81
reverse, and re-upgrade state paths have focused tests and a real packaged-node
73 82
proof.
74 83
75
Build the release pair and relup:
84
Build a digest-addressed release pair and relup:
76 85
77 86
```sh
78
ops/relup-proof/run.sh
87
ops/forge/package-relup.sh \
88
  --from-version 0.2.0 \
89
  --to-version 0.2.1 \
90
  --from-rev SOURCE_SHA \
91
  --to-rev TARGET_SHA \
92
  --out-dir /path/to/package
79 93
```
80 94
81
The script builds explicit release versions, generates both directions with
82
`mix openagents.relup`, embeds the generated `relup` in the candidate tar, and
83
caches checksummed proof artifacts under `.git/openagents/relup-proof/<sha>/`.
84
It never deletes a shared release directory.
95
The script builds each exact revision in an isolated worktree, generates both
96
directions with `mix openagents.relup`, embeds the generated `relup` in the
97
candidate tar, and writes a `package.json` manifest with source and target
98
revisions, versions, state schemas, target system, and SHA-256 digests. It
99
never deletes a shared release directory.
100
101
`OpenAgents.Forge.RelupPackage.deploy/2` validates that manifest against the
102
running revision and system architecture before it reads the bounded target
103
artifact and calls `RelupDeployment`. A stale package cannot upgrade a fleet
104
that another deployment has already replaced.
85 105
86 106
Run the live state and interruption drills against a disposable database:
87 107

@@ -99,19 +119,26 @@ ops/relup-proof/kill-during-install.sh

99 119
100 120
`OpenAgents.Forge.RelupDeployment` performs this sequence:
101 121
102
1. Verify the complete local gate receipt for the candidate SHA.
103
2. Snapshot the exact sorted member set and configured fleet size.
104
3. Stage one node's release tar in a digest-addressed cache.
105
4. Verify the cached and consumable tar digests.
106
5. Restore the consumable tar from cache, then unpack it.
107
6. Generate version-specific runtime configuration and run
122
1. Validate the package source revision, target system, and target artifact
123
   digest.
124
2. Verify the complete local gate receipt for the candidate SHA.
125
3. Snapshot the exact sorted member set and configured fleet size.
126
4. Stage one node's release tar in a digest-addressed cache.
127
5. Verify the cached and consumable tar digests.
128
6. Restore the consumable tar from cache, then unpack it.
129
7. Generate version-specific runtime configuration and run
108 130
   `release_handler.check_install_release/1`.
109
7. Install the candidate without changing the permanent release.
110
8. Verify current release status, application readiness, and the expected
111
   migrated state schema.
112
9. Make the release permanent, verify permanence, and recheck exact fleet
131
8. Install the candidate without changing the permanent release.
132
9. Verify current release status, exact target revision, application readiness,
133
   and the expected migrated state schema.
134
10. Make the release permanent, verify permanence, and recheck exact fleet
113 135
   membership.
114
10. Start the next node only after the previous node passes every check.
136
11. Start the next node only after the previous node passes every check.
137
138
The 2026-08-21 production drill upgraded three nodes from
139
`0.2.0@81e4c25` to `0.2.1@9763bf7` in 48.838 seconds. All nodes returned
140
`permanent`, retained `0.2.0` as the reverse release, and preserved their
141
uptimes.
115 142
116 143
If post-install health or permanence verification fails, the coordinator
117 144
installs the reverse relup, verifies the prior release and state schema, restores

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