Skip to content

Commit b76af95

Browse files
committed
ci(deploy): add release and production deploy workflow
1 parent 223fdb1 commit b76af95

1 file changed

Lines changed: 296 additions & 0 deletions

File tree

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
name: Release and Deploy Backend
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths:
8+
- ".github/workflows/release-deploy.yml"
9+
- "Cargo.lock"
10+
- "Cargo.toml"
11+
- "Dockerfile"
12+
- "clickhouse/**"
13+
- "crates/**"
14+
- "docs/env/**"
15+
- "docs/templates/**"
16+
- "migrations/**"
17+
- "scripts/**"
18+
- "services/**"
19+
workflow_dispatch:
20+
inputs:
21+
release_tag:
22+
description: "Optional Docker release tag. Defaults to production-<short-sha>."
23+
required: false
24+
type: string
25+
image_platform:
26+
description: "Docker platform for production hosts."
27+
required: false
28+
default: "linux/amd64"
29+
type: string
30+
deploy_control_plane:
31+
description: "Deploy rend-api and rend-media-worker after the image release."
32+
required: true
33+
default: true
34+
type: boolean
35+
deploy_edges:
36+
description: "Deploy configured edge hosts after the control plane."
37+
required: true
38+
default: true
39+
type: boolean
40+
run_readiness:
41+
description: "Run the synthetic playback readiness gate after deploy."
42+
required: true
43+
default: true
44+
type: boolean
45+
46+
permissions:
47+
contents: read
48+
49+
concurrency:
50+
group: rend-production-deploy
51+
cancel-in-progress: false
52+
53+
jobs:
54+
release-images:
55+
name: Build and push release images
56+
runs-on: ubuntu-latest
57+
permissions:
58+
contents: read
59+
packages: write
60+
outputs:
61+
image-platform: ${{ steps.release.outputs.image_platform }}
62+
manifest-path: ${{ steps.release.outputs.manifest_path }}
63+
release-tag: ${{ steps.release.outputs.release_tag }}
64+
steps:
65+
- name: Check out repository
66+
uses: actions/checkout@v4
67+
with:
68+
fetch-depth: 0
69+
70+
- name: Log in to GHCR
71+
uses: docker/login-action@v3
72+
with:
73+
registry: ghcr.io
74+
username: ${{ github.actor }}
75+
password: ${{ secrets.GITHUB_TOKEN }}
76+
77+
- name: Build and push images
78+
id: release
79+
env:
80+
IMAGE_PLATFORM_INPUT: ${{ inputs.image_platform || 'linux/amd64' }}
81+
RELEASE_TAG_INPUT: ${{ inputs.release_tag || '' }}
82+
run: |
83+
set -euo pipefail
84+
85+
image_prefix="ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/rend"
86+
short_sha="${GITHUB_SHA::12}"
87+
release_tag="${RELEASE_TAG_INPUT:-production-$short_sha}"
88+
image_platform="${IMAGE_PLATFORM_INPUT:-linux/amd64}"
89+
manifest_path=".rend/releases/rend-images-$release_tag.json"
90+
91+
scripts/release-images.sh \
92+
--tag "$release_tag" \
93+
--registry "$image_prefix" \
94+
--platform "$image_platform" \
95+
--manifest "$manifest_path" \
96+
--artifact-dir ".rend/release-artifacts" \
97+
--push
98+
99+
{
100+
echo "image_platform=$image_platform"
101+
echo "manifest_path=$manifest_path"
102+
echo "release_tag=$release_tag"
103+
} >> "$GITHUB_OUTPUT"
104+
105+
- name: Upload release manifest
106+
uses: actions/upload-artifact@v4
107+
with:
108+
name: rend-release-manifest
109+
path: ${{ steps.release.outputs.manifest_path }}
110+
if-no-files-found: error
111+
retention-days: 30
112+
113+
deploy:
114+
name: Deploy production hosts
115+
runs-on: ubuntu-latest
116+
needs: release-images
117+
if: ${{ github.event_name == 'push' || inputs.deploy_control_plane || inputs.deploy_edges }}
118+
environment:
119+
name: Production
120+
url: ${{ vars.REND_API_BASE_URL || 'https://api.rend.so' }}
121+
permissions:
122+
contents: read
123+
packages: read
124+
env:
125+
DEPLOY_CONTROL_PLANE: ${{ github.event_name == 'push' || inputs.deploy_control_plane }}
126+
DEPLOY_EDGES: ${{ github.event_name == 'push' || inputs.deploy_edges }}
127+
RUN_READINESS: ${{ github.event_name == 'push' || inputs.run_readiness }}
128+
IMAGE_PLATFORM: ${{ needs.release-images.outputs.image-platform }}
129+
REND_API_BASE_URL: ${{ vars.REND_API_BASE_URL || 'https://api.rend.so' }}
130+
REND_READINESS_EDGES: ${{ vars.REND_READINESS_EDGES }}
131+
REND_READINESS_API_KEY: ${{ secrets.REND_READINESS_API_KEY }}
132+
REND_EDGE_INTERNAL_TOKEN: ${{ secrets.REND_EDGE_INTERNAL_TOKEN }}
133+
REND_SSH_PRIVATE_KEY: ${{ secrets.REND_SSH_PRIVATE_KEY }}
134+
REND_SSH_KNOWN_HOSTS: ${{ secrets.REND_SSH_KNOWN_HOSTS }}
135+
REND_CONTROL_PLANE_SSH_HOST: ${{ secrets.REND_CONTROL_PLANE_SSH_HOST }}
136+
REND_CONTROL_PLANE_SSH_USER: ${{ secrets.REND_CONTROL_PLANE_SSH_USER }}
137+
REND_CONTROL_PLANE_SSH_PORT: ${{ secrets.REND_CONTROL_PLANE_SSH_PORT }}
138+
REND_EDGE_ASH_SSH_HOST: ${{ secrets.REND_EDGE_ASH_SSH_HOST }}
139+
REND_EDGE_ASH_SSH_USER: ${{ secrets.REND_EDGE_ASH_SSH_USER }}
140+
REND_EDGE_ASH_SSH_PORT: ${{ secrets.REND_EDGE_ASH_SSH_PORT }}
141+
REND_EDGE_AMS_SSH_HOST: ${{ secrets.REND_EDGE_AMS_SSH_HOST }}
142+
REND_EDGE_AMS_SSH_USER: ${{ secrets.REND_EDGE_AMS_SSH_USER }}
143+
REND_EDGE_AMS_SSH_PORT: ${{ secrets.REND_EDGE_AMS_SSH_PORT }}
144+
steps:
145+
- name: Check out repository
146+
uses: actions/checkout@v4
147+
148+
- name: Download release manifest
149+
uses: actions/download-artifact@v4
150+
with:
151+
name: rend-release-manifest
152+
path: .rend/deploy
153+
154+
- name: Resolve release manifest
155+
id: manifest
156+
run: |
157+
set -euo pipefail
158+
manifest="$(find .rend/deploy -type f -name 'rend-images-*.json' | sort | head -n 1)"
159+
if [[ -z "$manifest" ]]; then
160+
echo "release manifest was not downloaded" >&2
161+
exit 1
162+
fi
163+
python3 -m json.tool "$manifest" >/dev/null
164+
echo "path=$manifest" >> "$GITHUB_OUTPUT"
165+
166+
- name: Configure SSH
167+
env:
168+
SSH_KEY_PATH: ${{ runner.temp }}/rend_deploy_key
169+
run: |
170+
set -euo pipefail
171+
if [[ -z "$REND_SSH_PRIVATE_KEY" ]]; then
172+
echo "REND_SSH_PRIVATE_KEY is required" >&2
173+
exit 1
174+
fi
175+
if [[ -z "$REND_SSH_KNOWN_HOSTS" ]]; then
176+
echo "REND_SSH_KNOWN_HOSTS is required" >&2
177+
exit 1
178+
fi
179+
180+
install -m 700 -d "$HOME/.ssh"
181+
printf '%s\n' "$REND_SSH_PRIVATE_KEY" > "$SSH_KEY_PATH"
182+
chmod 600 "$SSH_KEY_PATH"
183+
printf '%s\n' "$REND_SSH_KNOWN_HOSTS" > "$HOME/.ssh/known_hosts"
184+
chmod 600 "$HOME/.ssh/known_hosts"
185+
echo "REND_SSH_KEY_PATH=$SSH_KEY_PATH" >> "$GITHUB_ENV"
186+
187+
- name: Deploy control plane
188+
if: env.DEPLOY_CONTROL_PLANE == 'true'
189+
run: |
190+
set -euo pipefail
191+
: "${REND_CONTROL_PLANE_SSH_HOST:?REND_CONTROL_PLANE_SSH_HOST is required}"
192+
: "${REND_CONTROL_PLANE_SSH_USER:?REND_CONTROL_PLANE_SSH_USER is required}"
193+
194+
scripts/deploy-release-over-ssh.sh \
195+
--role control-plane \
196+
--host "$REND_CONTROL_PLANE_SSH_HOST" \
197+
--user "$REND_CONTROL_PLANE_SSH_USER" \
198+
--port "${REND_CONTROL_PLANE_SSH_PORT:-22}" \
199+
--expected-platform "$IMAGE_PLATFORM" \
200+
--remote-dir "/tmp/rend-deploy-${GITHUB_RUN_ID}-${GITHUB_SHA::12}-control-plane" \
201+
--manifest "${{ steps.manifest.outputs.path }}"
202+
203+
- name: Deploy edge hosts
204+
if: env.DEPLOY_EDGES == 'true'
205+
run: |
206+
set -euo pipefail
207+
208+
deploy_edge() {
209+
local label="$1"
210+
local host="$2"
211+
local user="$3"
212+
local port="${4:-22}"
213+
214+
if [[ -z "$host" && -z "$user" ]]; then
215+
echo "Skipping $label edge: SSH host/user secrets are not configured"
216+
return 0
217+
fi
218+
if [[ -z "$host" || -z "$user" ]]; then
219+
echo "$label edge requires both host and user secrets" >&2
220+
return 1
221+
fi
222+
223+
scripts/deploy-release-over-ssh.sh \
224+
--role edge \
225+
--host "$host" \
226+
--user "$user" \
227+
--port "${port:-22}" \
228+
--expected-platform "$IMAGE_PLATFORM" \
229+
--remote-dir "/tmp/rend-deploy-${GITHUB_RUN_ID}-${GITHUB_SHA::12}-$label" \
230+
--manifest "${{ steps.manifest.outputs.path }}"
231+
}
232+
233+
deployed=0
234+
if [[ -n "$REND_EDGE_ASH_SSH_HOST$REND_EDGE_ASH_SSH_USER" ]]; then
235+
deploy_edge "ash" "$REND_EDGE_ASH_SSH_HOST" "$REND_EDGE_ASH_SSH_USER" "${REND_EDGE_ASH_SSH_PORT:-22}"
236+
deployed=$((deployed + 1))
237+
fi
238+
if [[ -n "$REND_EDGE_AMS_SSH_HOST$REND_EDGE_AMS_SSH_USER" ]]; then
239+
deploy_edge "ams" "$REND_EDGE_AMS_SSH_HOST" "$REND_EDGE_AMS_SSH_USER" "${REND_EDGE_AMS_SSH_PORT:-22}"
240+
deployed=$((deployed + 1))
241+
fi
242+
243+
if [[ "$deployed" == "0" ]]; then
244+
echo "DEPLOY_EDGES=true but no edge SSH targets were configured" >&2
245+
exit 1
246+
fi
247+
248+
- name: Verify public API readiness
249+
run: |
250+
set -euo pipefail
251+
curl -fsS --retry 12 --retry-delay 5 --retry-all-errors "$REND_API_BASE_URL/readyz"
252+
253+
internal_status="$(curl -sS -o /dev/null -w '%{http_code}' "$REND_API_BASE_URL/internal/edges/heartbeat")"
254+
if [[ "$internal_status" != "404" ]]; then
255+
echo "public API must not expose /internal/*; got HTTP $internal_status" >&2
256+
exit 1
257+
fi
258+
259+
assets_status="$(curl -sS -o /dev/null -w '%{http_code}' "$REND_API_BASE_URL/v1/assets")"
260+
case "$assets_status" in
261+
401 | 403) ;;
262+
*)
263+
echo "public /v1/assets route should require auth; got HTTP $assets_status" >&2
264+
exit 1
265+
;;
266+
esac
267+
268+
- name: Install playback readiness dependencies
269+
if: env.RUN_READINESS == 'true'
270+
run: |
271+
set -euo pipefail
272+
sudo apt-get update
273+
sudo apt-get install -y ffmpeg
274+
275+
- name: Run playback readiness gate
276+
if: env.RUN_READINESS == 'true'
277+
run: |
278+
set -euo pipefail
279+
: "${REND_READINESS_API_KEY:?REND_READINESS_API_KEY is required when RUN_READINESS=true}"
280+
: "${REND_EDGE_INTERNAL_TOKEN:?REND_EDGE_INTERNAL_TOKEN is required when RUN_READINESS=true}"
281+
: "${REND_READINESS_EDGES:?REND_READINESS_EDGES repository/environment variable is required when RUN_READINESS=true}"
282+
283+
node scripts/playback-readiness-gate.mjs \
284+
--target configured \
285+
--skip-local-stack \
286+
--output ".rend/readiness/playback-readiness-${GITHUB_RUN_ID}.json" \
287+
--latest-output ".rend/readiness/playback-readiness-latest.json"
288+
289+
- name: Upload readiness artifact
290+
if: always() && env.RUN_READINESS == 'true'
291+
uses: actions/upload-artifact@v4
292+
with:
293+
name: rend-playback-readiness
294+
path: .rend/readiness/*.json
295+
if-no-files-found: ignore
296+
retention-days: 14

0 commit comments

Comments
 (0)