Add a disposable production release smoke

3064e5237061 · Christopher David · · parent 2b52b624f2d7

Add a disposable production release smoke

Build colocated assets before Tailwind, package the production release, start it against an explicitly disposable database, verify health, and clean generated digests. Record the completed Gate 0 proof.

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

  • modified docs/2026-08-20-integration-hardening-and-staging-readiness-recommendations.md
  • modified mix.exs
  • added ops/ci/release-smoke.sh

Diff

3 files changed, +118 -3

docs/2026-08-20-integration-hardening-and-staging-readiness-recommendations.md modified +10 -3

@@ -104,9 +104,16 @@ Completed on 2026-08-20:

104 104
  coverage node before a test stops the peer.
105 105
- Added direct `RaBootstrap` decision tests for healthy, phantom, join, form,
106 106
  and wait outcomes instead of treating cluster execution as indirect proof.
107
108
Gate 0 still requires release startup proof against a disposable database and
109
an exact-SHA gate receipt.
107
- Added `ops/ci/release-smoke.sh`. It requires an explicitly acknowledged
108
  disposable PostgreSQL URL, generates throwaway runtime secrets, builds the
109
  production assets and release, starts the real release, waits for the bounded
110
  `/healthz` response, and terminates the release cleanly.
111
- Fixed `mix assets.deploy` to compile Phoenix's colocated assets before
112
  Tailwind resolves them. The release smoke exposed this production-only build
113
  failure and now passes against a fresh PostgreSQL 18 container.
114
115
Gate 0 still requires an exact-SHA gate receipt that runs and records these
116
completed stages from one clean commit.
110 117
111 118
Do not use the current green suite as evidence for untested code. The updated
112 119
coverage audit records strong Issues and Projects coverage and the defects it
mix.exs modified +1

@@ -105,6 +105,7 @@ defmodule OpenAgents.MixProject do

105 105
      "assets.build": ["compile", "tailwind openagents", "esbuild openagents"],
106 106
      "assets.test": ["cmd --cd assets npm test"],
107 107
      "assets.deploy": [
108
        "compile",
108 109
        "tailwind openagents --minify",
109 110
        "esbuild openagents --minify",
110 111
        "phx.digest"
ops/ci/release-smoke.sh added +107

@@ -0,0 +1,107 @@

1
#!/bin/sh
2
set -eu
3
4
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
5
repo_root=$(CDPATH= cd -- "$script_dir/../.." && pwd)
6
release_bin="$repo_root/_build/prod/rel/openagents/bin/openagents"
7
database_url=${OPENAGENTS_RELEASE_SMOKE_DATABASE_URL:-}
8
disposable=${OPENAGENTS_RELEASE_SMOKE_DISPOSABLE:-}
9
port=${OPENAGENTS_RELEASE_SMOKE_PORT:-$((40000 + ($$ % 20000)))}
10
release_pid=
11
assets_digested=0
12
smoke_root=$(mktemp -d /tmp/openagents-release-smoke.XXXXXX)
13
release_log="$smoke_root/release.log"
14
15
cleanup() {
16
  if [ -n "$release_pid" ] && kill -0 "$release_pid" 2>/dev/null; then
17
    kill -TERM "$release_pid" 2>/dev/null || true
18
    wait "$release_pid" 2>/dev/null || true
19
  fi
20
21
  if [ "$assets_digested" = "1" ]; then
22
    (cd "$repo_root" && MIX_ENV=prod mix phx.digest.clean --all >/dev/null 2>&1) || true
23
  fi
24
25
  rm -rf -- "$smoke_root"
26
}
27
28
trap cleanup EXIT INT TERM
29
30
if [ ! -d "$repo_root/.git" ]; then
31
  echo "release smoke must run from a Git worktree" >&2
32
  exit 1
33
fi
34
35
if [ "$disposable" != "1" ]; then
36
  echo "set OPENAGENTS_RELEASE_SMOKE_DISPOSABLE=1 after you provision a disposable database" >&2
37
  exit 1
38
fi
39
40
if [ -z "$database_url" ]; then
41
  echo "OPENAGENTS_RELEASE_SMOKE_DATABASE_URL is required" >&2
42
  exit 1
43
fi
44
45
for command_name in curl openssl; do
46
  if ! command -v "$command_name" >/dev/null 2>&1; then
47
    echo "$command_name is required for the release smoke" >&2
48
    exit 1
49
  fi
50
done
51
52
cd "$repo_root"
53
54
echo "Building production assets and release"
55
assets_digested=1
56
MIX_ENV=prod mix assets.deploy
57
MIX_ENV=prod mix release --overwrite
58
59
secret_key_base=$(openssl rand -base64 64 | tr -d '\n')
60
github_token_key=$(openssl rand -base64 32 | tr -d '\n')
61
62
echo "Starting release against the disposable database"
63
env \
64
  DATABASE_URL="$database_url" \
65
  GITHUB_CLIENT_ID="release-smoke-client" \
66
  GITHUB_CLIENT_SECRET="release-smoke-secret" \
67
  GITHUB_REDIRECT_URI="https://127.0.0.1/auth/github/callback" \
68
  GITHUB_TOKEN_ENCRYPTION_KEY="$github_token_key" \
69
  PHX_HOST="127.0.0.1" \
70
  PHX_SERVER="true" \
71
  POOL_SIZE="2" \
72
  PORT="$port" \
73
  SECRET_KEY_BASE="$secret_key_base" \
74
  "$release_bin" start >"$release_log" 2>&1 &
75
release_pid=$!
76
77
health_url="http://127.0.0.1:$port/healthz"
78
health_body=
79
80
for attempt in $(seq 1 120); do
81
  if ! kill -0 "$release_pid" 2>/dev/null; then
82
    echo "release exited before it became healthy" >&2
83
    tail -40 "$release_log" >&2
84
    exit 1
85
  fi
86
87
  if health_body=$(curl --fail --silent --show-error --max-time 2 "$health_url" 2>/dev/null); then
88
    break
89
  fi
90
91
  sleep 0.5
92
done
93
94
case "$health_body" in
95
  *'"status":"ok"'*) ;;
96
  *)
97
    echo "release did not return the expected bounded health response" >&2
98
    tail -40 "$release_log" >&2
99
    exit 1
100
    ;;
101
esac
102
103
kill -TERM "$release_pid"
104
wait "$release_pid"
105
release_pid=
106
107
echo "Release startup smoke passed"

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