4343 run : |
4444 i=1
4545 while IFS= read -r line || [ -n "$line" ]; do
46- [[ "$line" =~ ^[[:space:]]*(#|$) ]] && continue
47- echo "cmd${i}=${line}" >> "$GITHUB_OUTPUT"
46+ trimmed=$(echo "$line" | xargs)
47+ [[ -z "$trimmed" || "$trimmed" == "#"* ]] && continue
48+ echo "cmd${i}=${trimmed}" >> "$GITHUB_OUTPUT"
4849 i=$((i+1))
4950 done <<'EOF'
5051 ${{ inputs.cmlxc_commands }}
@@ -68,22 +69,25 @@ jobs:
6869 repository : chatmail/cmlxc
6970 ref : ${{ inputs.cmlxc_version }}
7071 path : cmlxc
71- fetch-depth : 0
7272
7373 - name : Install Incus (Zabbly)
7474 run : |
7575 sudo mkdir -p /etc/apt/keyrings
7676 sudo curl -fsSL https://pkgs.zabbly.com/key.asc -o /etc/apt/keyrings/zabbly.asc
7777 echo "deb [signed-by=/etc/apt/keyrings/zabbly.asc] https://pkgs.zabbly.com/incus/stable $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/zabbly-incus.list
7878 sudo apt-get update
79- sudo apt-get install -y incus
79+ sudo apt-get install -y incus-base
8080
8181 - name : Initialise Incus
8282 run : |
8383 sudo systemctl stop docker.socket docker || true
8484 sudo iptables -P FORWARD ACCEPT
8585 sudo sysctl -w fs.inotify.max_user_instances=65535
8686 sudo sysctl -w fs.inotify.max_user_watches=65535
87+ # Disable AppArmor restrictions so Docker-in-LXC containers
88+ # can run systemd (needs cgroup notification socket access).
89+ sudo systemctl stop apparmor || true
90+ sudo apparmor_parser -R /etc/apparmor.d/* 2>/dev/null || true
8791 sudo incus admin init --auto
8892 sudo chmod 666 /var/lib/incus/unix.socket
8993
@@ -97,17 +101,19 @@ jobs:
97101 python -m pip install --upgrade pip
98102 pip install ./cmlxc
99103
100- - name : Cache Incus images
104+ - name : Restore Incus image cache
101105 id : cache-images
102- uses : actions/cache@v5
106+ uses : actions/cache/restore @v5
103107 with :
104108 path : /tmp/incus-cache
105- key : incus-v4-${{ runner.os }}-${{ hashFiles('cmlxc/src/cmlxc/*.py') }}
109+ key : incus-v5-${{ runner.os }}-${{ hashFiles('cmlxc/src/cmlxc/*.py') }}
110+ restore-keys : |
111+ incus-v5-${{ runner.os }}-
106112
107113 - name : Import cached images
108114 run : |
109115 mkdir -p /tmp/incus-cache
110- for alias in localchat-base localchat-builder; do
116+ for alias in localchat-base localchat-builder localchat-cmdeploy localchat-docker ; do
111117 if [ -f /tmp/incus-cache/$alias.tar.gz ]; then
112118 echo "Importing: $alias"
113119 incus image import /tmp/incus-cache/$alias.tar.gz --alias $alias || true
@@ -170,12 +176,12 @@ jobs:
170176 set -eu
171177 i=0
172178 while IFS= read -r cmd || [ -n "$cmd" ]; do
173- [[ "$cmd" =~ ^[[:space:]]*(#|$) ]] && continue
179+ trimmed=$(echo "$cmd" | xargs)
180+ [[ -z "$trimmed" || "$trimmed" == "#"* ]] && continue
174181 i=$((i+1))
175182 if [ $i -le 12 ]; then continue; fi
176-
177- echo "::group::Run: $cmd"
178- eval "$cmd" || { echo "::endgroup::"; exit 1; }
183+ echo "::group::Run: $trimmed"
184+ eval "$trimmed" || { echo "::endgroup::"; exit 1; }
179185 echo "::endgroup::"
180186 done <<< "$CMLXC_COMMANDS"
181187
@@ -184,26 +190,73 @@ jobs:
184190 run : |
185191 for c in $(incus list -c n --format csv); do
186192 echo "::group::Logs for $c"
187- incus exec "$c" -- journalctl -p warning --no-pager -n 100 || true
193+ incus exec "$c" -- journalctl --no-pager -n 200 || true
194+ # Dump Docker container logs if present
195+ svc=chatmail
196+ if incus exec "$c" -- docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q "$svc"; then
197+ echo "--- docker logs $svc ---"
198+ incus exec "$c" -- docker logs "$svc" --tail 200 2>&1 || true
199+ echo "--- dovecot journal ---"
200+ incus exec "$c" -- docker exec "$svc" journalctl -u dovecot --no-pager -n 50 2>&1 || true
201+ echo "--- postfix journal ---"
202+ incus exec "$c" -- docker exec "$svc" journalctl -u postfix --no-pager -n 50 2>&1 || true
203+ echo "--- failed units ---"
204+ incus exec "$c" -- docker exec "$svc" systemctl --failed --no-pager 2>&1 || true
205+ echo "--- dovecot -n (effective config) ---"
206+ incus exec "$c" -- docker exec "$svc" dovecot -n 2>&1 | tail -40 || true
207+ echo "--- ssl cert check ---"
208+ incus exec "$c" -- docker exec "$svc" ls -la /etc/ssl/certs/mailserver.pem /etc/ssl/private/mailserver.key 2>&1 || true
209+ fi
188210 echo "::endgroup::"
189211 done
190212
191213 - name : Export images for cache
192214 if : always() && steps.cache-images.outputs.cache-hit != 'true'
193215 run : |
194216 mkdir -p /tmp/incus-cache
217+ # Publish the builder LXC container as a cached image (the Docker
218+ # container inside gets recreated on compose up, so the LXC is clean).
219+ # Only skip localchat-cmdeploy on failure -- it bakes deploy state
220+ # directly into the LXC and would carry broken config into the next run.
195221 if incus list -c n --format csv | grep -q builder-localchat; then
196- incus exec builder-localchat -- rm -rf /root/relays /root/minitest-venv /root/.ssh/config*
222+ echo "Cleaning up builder container before publishing ..."
223+ incus exec builder-localchat -- bash -c 'rm -rf /root/relays/* /root/.cache/* /root/.npm /root/.bun'
197224 echo "Publishing builder container as image ..."
198225 incus publish builder-localchat --alias localchat-builder --force || true
199226 fi
200- for alias in localchat-base localchat-builder; do
227+ # Publish Docker relay container with engine only (strip images to keep cache small)
228+ for ct in $(incus list -c n --format csv | grep -v builder); do
229+ if incus exec "$ct" -- docker info >/dev/null 2>&1; then
230+ echo "Stripping Docker images from $ct ..."
231+ incus exec "$ct" -- docker system prune -af --volumes 2>/dev/null || true
232+ echo "Publishing $ct as localchat-docker ..."
233+ incus publish "$ct" --alias localchat-docker --force || true
234+ break
235+ fi
236+ done
237+ exported=0
238+ if [ "${{ job.status }}" = "success" ]; then
239+ aliases="localchat-base localchat-builder localchat-cmdeploy localchat-docker"
240+ else
241+ aliases="localchat-base localchat-builder localchat-docker"
242+ fi
243+ for alias in $aliases; do
201244 if incus image list --format csv -c l | grep -q "^$alias$"; then
202245 echo "Exporting: $alias"
203246 incus image export $alias /tmp/incus-cache/$alias || true
204247 if [ -f /tmp/incus-cache/$alias ] && [ ! -f /tmp/incus-cache/$alias.tar.gz ]; then
205248 mv /tmp/incus-cache/$alias /tmp/incus-cache/$alias.tar.gz
206249 fi
250+ exported=$((exported+1))
207251 fi
208252 done
253+ echo "exported=$exported" >> "$GITHUB_OUTPUT"
254+ id : export-images
255+
256+ - name : Save Incus image cache
257+ if : always() && steps.export-images.outputs.exported > 0
258+ uses : actions/cache/save@v5
259+ with :
260+ path : /tmp/incus-cache
261+ key : incus-v5-${{ runner.os }}-${{ hashFiles('cmlxc/src/cmlxc/*.py') }}
209262
0 commit comments