|
4 | 4 | push: |
5 | 5 | branches: |
6 | 6 | - main |
| 7 | + - j4n/docker-pr |
7 | 8 | pull_request: |
8 | 9 | paths-ignore: |
9 | 10 | - 'scripts/**' |
10 | 11 | - '**/README.md' |
11 | 12 | - 'CHANGELOG.md' |
12 | 13 | - 'LICENSE' |
13 | 14 |
|
| 15 | +env: |
| 16 | + REGISTRY: ghcr.io |
| 17 | + IMAGE_NAME: ${{ github.repository }} |
| 18 | + |
14 | 19 | jobs: |
| 20 | + build-docker: |
| 21 | + name: Build Docker image |
| 22 | + runs-on: ubuntu-latest |
| 23 | + permissions: |
| 24 | + contents: read |
| 25 | + packages: write |
| 26 | + outputs: |
| 27 | + image: ${{ steps.image-ref.outputs.image }} |
| 28 | + steps: |
| 29 | + - uses: actions/checkout@v4 |
| 30 | + |
| 31 | + - name: Clone chatmail/docker |
| 32 | + run: git clone https://github.com/chatmail/docker docker |
| 33 | + |
| 34 | + - name: Set up Docker Buildx |
| 35 | + uses: docker/setup-buildx-action@v3 |
| 36 | + |
| 37 | + - name: Login to GHCR |
| 38 | + if: github.event_name == 'push' |
| 39 | + uses: docker/login-action@v3 |
| 40 | + with: |
| 41 | + registry: ${{ env.REGISTRY }} |
| 42 | + username: ${{ github.actor }} |
| 43 | + password: ${{ secrets.GITHUB_TOKEN }} |
| 44 | + |
| 45 | + - name: Extract metadata (tags, labels) |
| 46 | + id: meta |
| 47 | + uses: docker/metadata-action@v5 |
| 48 | + with: |
| 49 | + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} |
| 50 | + tags: | |
| 51 | + # Tagged releases: v1.2.3 -> :1.2.3, :1.2, :latest |
| 52 | + type=semver,pattern={{version}} |
| 53 | + type=semver,pattern={{major}}.{{minor}} |
| 54 | + # Branch pushes: foo/docker-pr -> :foo-docker-pr |
| 55 | + type=ref,event=branch |
| 56 | + # Always: :sha-<hash> |
| 57 | + type=sha |
| 58 | +
|
| 59 | + - name: Copy .dockerignore to build context |
| 60 | + run: cp docker/.dockerignore .dockerignore |
| 61 | + |
| 62 | + - name: Build and push |
| 63 | + uses: docker/build-push-action@v6 |
| 64 | + with: |
| 65 | + context: . |
| 66 | + file: docker/chatmail_relay.dockerfile |
| 67 | + push: ${{ github.event_name == 'push' }} |
| 68 | + tags: ${{ steps.meta.outputs.tags }} |
| 69 | + labels: ${{ steps.meta.outputs.labels }} |
| 70 | + cache-from: type=gha |
| 71 | + cache-to: type=gha,mode=max |
| 72 | + build-args: | |
| 73 | + GIT_HASH=${{ github.sha }} |
| 74 | +
|
| 75 | + - name: Output image reference |
| 76 | + id: image-ref |
| 77 | + run: | |
| 78 | + SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7) |
| 79 | + IMAGE="${{ env.REGISTRY }}/$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]'):sha-${SHORT_SHA}" |
| 80 | + echo "image=${IMAGE}" >> "$GITHUB_OUTPUT" |
| 81 | +
|
15 | 82 | deploy: |
16 | 83 | name: deploy on staging-ipv4.testrun.org, and run tests |
17 | 84 | runs-on: ubuntu-latest |
|
22 | 89 | concurrency: staging-ipv4.testrun.org |
23 | 90 | steps: |
24 | 91 | - uses: actions/checkout@v4 |
| 92 | + with: |
| 93 | + submodules: true |
25 | 94 |
|
26 | 95 | - name: prepare SSH |
27 | 96 | run: | |
|
55 | 124 | run: echo venv/bin >>$GITHUB_PATH |
56 | 125 |
|
57 | 126 | - name: upload TLS cert after rebuilding |
| 127 | + id: wait-for-vps |
58 | 128 | run: | |
59 | 129 | echo " --- wait until staging-ipv4.testrun.org VPS is rebuilt --- " |
60 | 130 | rm ~/.ssh/known_hosts |
|
68 | 138 | rsync -avz dkimkeys-restore/dkimkeys root@staging-ipv4.testrun.org:/etc/ || true |
69 | 139 | ssh -o StrictHostKeyChecking=accept-new -v root@staging-ipv4.testrun.org chown root:root -R /var/lib/acme || true |
70 | 140 |
|
71 | | - - name: run deploy-chatmail offline tests |
72 | | - run: pytest --pyargs cmdeploy |
| 141 | + - name: run deploy-chatmail offline tests |
| 142 | + run: pytest --pyargs cmdeploy |
73 | 143 |
|
74 | 144 | - name: setup dependencies |
75 | 145 | run: | |
@@ -102,3 +172,133 @@ jobs: |
102 | 172 | - name: cmdeploy dns |
103 | 173 | run: ssh root@staging-ipv4.testrun.org "cd relay && scripts/cmdeploy dns -v --ssh-host localhost" |
104 | 174 |
|
| 175 | + # --- Docker deploy (push only, runs even if bare failed) --- |
| 176 | + |
| 177 | + - name: stop bare services |
| 178 | + if: >- |
| 179 | + !cancelled() && github.event_name == 'push' |
| 180 | + && steps.wait-for-vps.outcome == 'success' |
| 181 | + run: | |
| 182 | + ssh root@staging-ipv4.testrun.org 'systemctl stop postfix dovecot nginx opendkim unbound filtermail doveauth chatmail-metadata iroh-relay mtail fcgiwrap acmetool 2>/dev/null || true' |
| 183 | +
|
| 184 | + - name: install Docker on VPS |
| 185 | + if: >- |
| 186 | + !cancelled() && github.event_name == 'push' |
| 187 | + && steps.wait-for-vps.outcome == 'success' |
| 188 | + run: | |
| 189 | + ssh root@staging-ipv4.testrun.org 'apt-get update && apt-get install -y ca-certificates curl' |
| 190 | + ssh root@staging-ipv4.testrun.org 'install -m 0755 -d /etc/apt/keyrings' |
| 191 | + ssh root@staging-ipv4.testrun.org 'curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc && chmod a+r /etc/apt/keyrings/docker.asc' |
| 192 | + ssh root@staging-ipv4.testrun.org 'echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $(. /etc/os-release && echo $VERSION_CODENAME) stable" > /etc/apt/sources.list.d/docker.list' |
| 193 | + ssh root@staging-ipv4.testrun.org 'apt-get update && apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin' |
| 194 | +
|
| 195 | + - name: prepare Docker bind mounts |
| 196 | + if: >- |
| 197 | + !cancelled() && github.event_name == 'push' |
| 198 | + && steps.wait-for-vps.outcome == 'success' |
| 199 | + run: | |
| 200 | + ssh root@staging-ipv4.testrun.org 'mkdir -p /srv/chatmail/certs /srv/chatmail/dkim' |
| 201 | + ssh root@staging-ipv4.testrun.org 'cp -a /var/lib/acme/. /srv/chatmail/certs/ && cp -a /etc/dkimkeys/. /srv/chatmail/dkim/' || true |
| 202 | +
|
| 203 | + - name: upload chatmail.ini for Docker |
| 204 | + if: >- |
| 205 | + !cancelled() && github.event_name == 'push' |
| 206 | + && steps.wait-for-vps.outcome == 'success' |
| 207 | + run: | |
| 208 | + # Reuse chatmail.ini already created by the bare-metal deploy steps |
| 209 | + ssh root@staging-ipv4.testrun.org "cp relay/chatmail.ini /srv/chatmail/chatmail.ini" |
| 210 | +
|
| 211 | + - name: deploy with Docker |
| 212 | + if: >- |
| 213 | + !cancelled() && github.event_name == 'push' |
| 214 | + && steps.wait-for-vps.outcome == 'success' |
| 215 | + run: | |
| 216 | + SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7) |
| 217 | + GHCR_IMAGE="${{ env.REGISTRY }}/$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]'):sha-${SHORT_SHA}" |
| 218 | + rsync -avz --exclude='.git' --exclude='venv' --exclude='__pycache__' ./ root@staging-ipv4.testrun.org:/srv/chatmail/relay/ |
| 219 | + # Login to GHCR on VPS and pull pre-built image |
| 220 | + echo "${{ secrets.GITHUB_TOKEN }}" | ssh root@staging-ipv4.testrun.org 'docker login ghcr.io -u ${{ github.actor }} --password-stdin' |
| 221 | + ssh root@staging-ipv4.testrun.org "docker pull ${GHCR_IMAGE}" |
| 222 | + ssh root@staging-ipv4.testrun.org "cd /srv/chatmail/relay && CHATMAIL_IMAGE=${GHCR_IMAGE} MAIL_DOMAIN=staging-ipv4.testrun.org docker compose -f docker/docker-compose.yaml -f docker/docker-compose.ci.yaml up -d" |
| 223 | +
|
| 224 | + - name: wait for container healthy |
| 225 | + if: >- |
| 226 | + !cancelled() && github.event_name == 'push' |
| 227 | + && steps.wait-for-vps.outcome == 'success' |
| 228 | + run: | |
| 229 | + # Stream journald inside the container |
| 230 | + ssh root@staging-ipv4.testrun.org 'docker exec chatmail journalctl -f --no-pager' & |
| 231 | + LOG_PID=$! |
| 232 | + trap "kill $LOG_PID 2>/dev/null || true" EXIT |
| 233 | + for i in $(seq 1 60); do |
| 234 | + status=$(ssh root@staging-ipv4.testrun.org 'docker inspect --format={{.State.Health.Status}} chatmail 2>/dev/null' || echo "missing") |
| 235 | + echo " [$i/60] status=$status" |
| 236 | + if [ "$status" = "healthy" ]; then |
| 237 | + echo "Container is healthy." |
| 238 | + exit 0 |
| 239 | + fi |
| 240 | + if [ "$status" = "unhealthy" ]; then |
| 241 | + echo "Container is unhealthy!" |
| 242 | + break |
| 243 | + fi |
| 244 | + sleep 5 |
| 245 | + done |
| 246 | + echo "Container did not become healthy." |
| 247 | + kill $LOG_PID 2>/dev/null || true |
| 248 | + echo "--- failed units ---" |
| 249 | + ssh root@staging-ipv4.testrun.org 'docker exec chatmail systemctl --failed --no-pager' || true |
| 250 | + echo "--- service logs ---" |
| 251 | + ssh root@staging-ipv4.testrun.org 'docker exec chatmail journalctl -u dovecot -u postfix -u nginx -u unbound --no-pager -n 50' || true |
| 252 | + echo "--- listening ports ---" |
| 253 | + ssh root@staging-ipv4.testrun.org 'docker exec chatmail ss -tlnp' || true |
| 254 | + echo "--- chatmail.ini ---" |
| 255 | + ssh root@staging-ipv4.testrun.org 'docker exec chatmail cat /etc/chatmail/chatmail.ini' || true |
| 256 | + exit 1 |
| 257 | +
|
| 258 | + - name: show container state |
| 259 | + if: >- |
| 260 | + !cancelled() && github.event_name == 'push' |
| 261 | + && steps.wait-for-vps.outcome == 'success' |
| 262 | + run: | |
| 263 | + echo "--- listening ports ---" |
| 264 | + ssh root@staging-ipv4.testrun.org 'docker exec chatmail ss -tlnp' |
| 265 | + echo "--- chatmail.ini ---" |
| 266 | + ssh root@staging-ipv4.testrun.org 'docker exec chatmail cat /etc/chatmail/chatmail.ini' |
| 267 | +
|
| 268 | + - name: Docker integration tests |
| 269 | + if: >- |
| 270 | + !cancelled() && github.event_name == 'push' |
| 271 | + && steps.wait-for-vps.outcome == 'success' |
| 272 | + run: | |
| 273 | + ssh root@staging-ipv4.testrun.org 'docker exec chatmail cmdeploy test --slow --ssh-host @local' |
| 274 | +
|
| 275 | + - name: Docker DNS |
| 276 | + if: >- |
| 277 | + !cancelled() && github.event_name == 'push' |
| 278 | + && steps.wait-for-vps.outcome == 'success' |
| 279 | + run: | |
| 280 | + # Reset zone file in case bare DNS already appended to it |
| 281 | + git checkout .github/workflows/staging-ipv4.testrun.org-default.zone |
| 282 | + ssh root@staging-ipv4.testrun.org 'docker exec chatmail chown opendkim:opendkim -R /etc/dkimkeys' |
| 283 | + ssh root@staging-ipv4.testrun.org 'docker exec chatmail cmdeploy dns --ssh-host @local --zonefile /opt/chatmail/staging.zone --verbose' |
| 284 | + ssh root@staging-ipv4.testrun.org 'docker cp chatmail:/opt/chatmail/staging.zone /tmp/staging.zone' |
| 285 | + scp root@staging-ipv4.testrun.org:/tmp/staging.zone staging-generated.zone |
| 286 | + cat staging-generated.zone >> .github/workflows/staging-ipv4.testrun.org-default.zone |
| 287 | + cat .github/workflows/staging-ipv4.testrun.org-default.zone |
| 288 | + scp .github/workflows/staging-ipv4.testrun.org-default.zone root@ns.testrun.org:/etc/nsd/staging-ipv4.testrun.org.zone |
| 289 | + ssh root@ns.testrun.org nsd-checkzone staging-ipv4.testrun.org /etc/nsd/staging-ipv4.testrun.org.zone |
| 290 | + ssh root@ns.testrun.org systemctl reload nsd |
| 291 | +
|
| 292 | + - name: Docker final DNS check |
| 293 | + if: >- |
| 294 | + !cancelled() && github.event_name == 'push' |
| 295 | + && steps.wait-for-vps.outcome == 'success' |
| 296 | + run: ssh root@staging-ipv4.testrun.org 'docker exec chatmail cmdeploy dns -v --ssh-host @local' |
| 297 | + |
| 298 | + # --- Cleanup --- |
| 299 | + |
| 300 | + - name: add SSH keys |
| 301 | + if: >- |
| 302 | + !cancelled() |
| 303 | + && steps.wait-for-vps.outcome == 'success' |
| 304 | + run: ssh root@staging-ipv4.testrun.org 'curl -s https://github.com/hpk42.keys https://github.com/j4n.keys >> .ssh/authorized_keys' |
0 commit comments