-
Notifications
You must be signed in to change notification settings - Fork 0
213 lines (188 loc) · 7.57 KB
/
Copy pathdeploy.yml
File metadata and controls
213 lines (188 loc) · 7.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# Build Docker images on successful main CI, push to GHCR, deploy to DigitalOcean.
name: Build and deploy
on:
push:
branches: [main]
workflow_dispatch:
inputs:
image_tag:
description: "Image tag to deploy (default: main)"
required: false
default: main
concurrency:
group: deploy-production
cancel-in-progress: false
permissions:
contents: read
packages: write
env:
REGISTRY: ghcr.io
IMAGE_PREFIX: ghcr.io/dotnetrussell/operationchildshield
jobs:
test:
name: Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: |
backend/requirements.txt
backend/requirements-dev.txt
- name: Backend tests
working-directory: backend
run: |
pip install -r requirements.txt -r requirements-dev.txt
python -m pytest -q --tb=short
- uses: actions/setup-node@v6
with:
node-version: "24"
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Frontend tests
working-directory: frontend
run: |
npm ci
npm test
build-and-push:
name: Build and push images
needs: test
runs-on: ubuntu-latest
# Environment secrets (prod) are only available when the job declares this.
environment: prod
outputs:
image_tag: ${{ steps.meta.outputs.tag }}
steps:
- uses: actions/checkout@v7
- name: Image tag
id: meta
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ inputs.image_tag }}" ]; then
echo "tag=${{ inputs.image_tag }}" >> "$GITHUB_OUTPUT"
else
echo "tag=main" >> "$GITHUB_OUTPUT"
fi
echo "sha=sha-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
- uses: docker/setup-buildx-action@v4
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push backend
uses: docker/build-push-action@v7
with:
context: ./backend
push: true
tags: |
${{ env.IMAGE_PREFIX }}-backend:${{ steps.meta.outputs.tag }}
${{ env.IMAGE_PREFIX }}-backend:${{ steps.meta.outputs.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build and push frontend
uses: docker/build-push-action@v7
with:
context: ./frontend
push: true
build-args: |
NEXT_PUBLIC_API_URL=https://operationchildshield.org
tags: |
${{ env.IMAGE_PREFIX }}-frontend:${{ steps.meta.outputs.tag }}
${{ env.IMAGE_PREFIX }}-frontend:${{ steps.meta.outputs.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
name: Deploy to DigitalOcean
needs: build-and-push
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
# Must match the Environment name under Settings -> Environments (secrets live there).
environment: prod
steps:
- uses: actions/checkout@v7
# Prefer DEPLOY_SSH_KEY_B64 (single-line base64) - multiline PEM paste often becomes empty.
# Falls back to DEPLOY_SSH_KEY (raw PEM).
- name: Configure SSH
env:
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
DEPLOY_SSH_KEY_B64: ${{ secrets.DEPLOY_SSH_KEY_B64 }}
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
run: |
set -euo pipefail
echo "Secret presence check (values never printed):"
if [ -n "${DEPLOY_HOST:-}" ]; then echo " DEPLOY_HOST: set (${#DEPLOY_HOST} chars)"; else echo " DEPLOY_HOST: EMPTY"; fi
if [ -n "${DEPLOY_USER:-}" ]; then echo " DEPLOY_USER: set (${#DEPLOY_USER} chars)"; else echo " DEPLOY_USER: EMPTY"; fi
if [ -n "${DEPLOY_SSH_KEY_B64:-}" ]; then echo " DEPLOY_SSH_KEY_B64: set (${#DEPLOY_SSH_KEY_B64} chars)"; else echo " DEPLOY_SSH_KEY_B64: EMPTY"; fi
if [ -n "${DEPLOY_SSH_KEY:-}" ]; then echo " DEPLOY_SSH_KEY: set (${#DEPLOY_SSH_KEY} chars)"; else echo " DEPLOY_SSH_KEY: EMPTY"; fi
mkdir -p ~/.ssh
chmod 700 ~/.ssh
if [ -n "${DEPLOY_SSH_KEY_B64:-}" ]; then
echo "$DEPLOY_SSH_KEY_B64" | base64 -d > ~/.ssh/deploy_key
elif [ -n "${DEPLOY_SSH_KEY:-}" ]; then
# Normalize newlines / strip Windows CR
printf '%s\n' "$DEPLOY_SSH_KEY" | tr -d '\r' > ~/.ssh/deploy_key
else
echo "::error::Neither DEPLOY_SSH_KEY_B64 nor DEPLOY_SSH_KEY is available to this workflow."
echo "This job uses environment: prod. Secrets must be Environment secrets for prod."
echo " Settings -> Environments -> prod -> Environment secrets"
echo " DEPLOY_SSH_KEY (PEM) or DEPLOY_SSH_KEY_B64 (base64 -w0 of the private key)"
echo "Then re-run the workflow (old runs do not pick up new secrets mid-flight)."
exit 1
fi
chmod 600 ~/.ssh/deploy_key
# Validate key material
if ! grep -q "BEGIN.*PRIVATE KEY" ~/.ssh/deploy_key; then
echo "::error::Deploy key file does not look like a private key (missing BEGIN PRIVATE KEY header)."
exit 1
fi
ssh-keygen -y -f ~/.ssh/deploy_key >/dev/null
test -n "${DEPLOY_HOST:-}" || { echo "::error::Missing DEPLOY_HOST"; exit 1; }
test -n "${DEPLOY_USER:-}" || { echo "::error::Missing DEPLOY_USER"; exit 1; }
ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null || true
echo "SSH key ready."
- name: Sync deploy files
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
run: |
set -euo pipefail
PATH_ON_SERVER="${DEPLOY_PATH:-/opt/operationchildshield}"
rsync -avz -e "ssh -i ~/.ssh/deploy_key -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes" \
docker-compose.prod.ghcr.yml \
scripts/server-pull-and-up.sh \
"${DEPLOY_USER}@${DEPLOY_HOST}:${PATH_ON_SERVER}/"
- name: Pull images and restart
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
IMAGE_TAG: ${{ needs.build-and-push.outputs.image_tag }}
run: |
set -euo pipefail
PATH_ON_SERVER="${DEPLOY_PATH:-/opt/operationchildshield}"
SITE_URL="https://operationchildshield.org"
ssh -i ~/.ssh/deploy_key -o IdentitiesOnly=yes \
"${DEPLOY_USER}@${DEPLOY_HOST}" \
"cd '${PATH_ON_SERVER}' && chmod +x scripts/server-pull-and-up.sh && \
IMAGE_TAG='${IMAGE_TAG}' \
NEXT_PUBLIC_API_URL='${SITE_URL}' \
APP_DIR='${PATH_ON_SERVER}' \
./scripts/server-pull-and-up.sh"
- name: Smoke check
run: |
set -euo pipefail
BASE="https://operationchildshield.org"
for i in 1 2 3 4 5 6 7 8 9 10; do
if curl -fsS "${BASE}/api/health" | grep -q '"status"'; then
echo "Health OK at ${BASE}/api/health"
exit 0
fi
sleep 6
done
echo "Smoke check failed for ${BASE}/api/health" >&2
exit 1