Using openclaw to set up CI/CD for an Astro project — a real-world sensakai.com case study
This post summarizes the practical lessons learned while building CI/CD for sensakai (Astro). The twist: I used openclaw as a DevOps copilot to:
- review project structure and build scripts,
- propose a standard pipeline layout,
- provide a secrets/variables checklist,
- help debug common CI failures,
- turn the implementation into a reusable playbook.
Security note: sensitive details (IPs/domains/users/keys/internal paths) are replaced with placeholders like
<...>. This post focuses on patterns and repeatable steps.
1) Pipeline goals
The pipeline is structured into three stages:
- test: install dependencies and run
lint+astro checkto fail fast. - build: build
dist/and store it as artifacts. - deploy: only on
main, fetchdist/artifacts and deploy via SSH + rsync.
Upgrade included:
- a post-deploy smoke test (HTTP check) to verify the site is actually serving correctly.
2) Repo prerequisites
2.1 Minimal scripts in package.json
You should have clearly defined scripts:
lint:eslint .check:astro checkbuild:astro build
Why this matters:
lintcatches style/syntax issues.astro checkcatches type/integration issues early.buildproducesdist/for deployment.
How openclaw helped: it enforced this “contract” before writing any CI YAML.
2.2 Deployment output
Astro’s default output is:
dist/
3) Runner design: Shell vs Docker executor
This pipeline is designed for a Shell executor (jobs run directly on the runner host), because:
- the runner host can access your internal/VPN network (e.g., Tailscale) like a real machine,
- SSH/rsync deployment is straightforward.
How openclaw helped: it prompted the right executor choice given networking constraints.
4) Step-by-step CI/CD design
4.1 Key principles
- Fail fast: run lint/check first.
- Reproducible: use
npm ciwith a lockfile. - Traceable: deploy the exact
dist/produced by the build job (artifacts). - Controlled release: deploy only from
main.
4.2 npm cache
Recommended:
- cache
.npm/ - cache key based on
package-lock.json
Install:
npm ci --cache .npm --prefer-offline
4.3 dist/ artifacts
Artifacts make deploy deterministic: deploy uses the build output, not a rebuilt version.
5) SSH/rsync deployment (common pitfalls)
5.1 Variables (GitLab CI/CD Variables)
Set these variables (example names):
DEPLOY_SSH_KEY_B64DEPLOY_SSH_HOSTDEPLOY_SSH_PORTDEPLOY_SSH_USERDEPLOY_TARGET_DIR
Security recommendations:
- never commit keys
- least-privileged deploy user
- consider masked/protected variables
5.2 rsync --delete caution
Pros: keeps the server free of stale files. Risk: if the target directory is wrong, you can delete the wrong content.
Mitigations:
- deploy into a dedicated subdirectory
- optionally enforce a marker-file check
6) Upgrade: post-deploy smoke test (HTTP check)
After rsync finishes, validate an HTTP endpoint to ensure:
- the web server is serving the new content
- routing/DNS is correct
- no 50x/connection failures
Approach:
- set
SMOKE_TEST_URL(e.g.,https://<your-domain>/) - (recommended) set
SMOKE_TEST_EXPECTto assert a marker string in the final HTML (e.g.,sensakai) - the smoke test will:
- accept
200/301/302on the initial request - follow redirects to fetch final HTML and check the marker string
- retry to allow the service to warm up
- accept
7) Real configuration: .gitlab-ci.yml
Below is the actual .gitlab-ci.yml used for sensakai (no secrets included; sensitive values live in GitLab Variables):
stages:
- test
- build
- deploy
# NOTE: This pipeline is intended for a SHELL executor (runs on the runner host).
# It can reach your Tailscale network directly, unlike Docker executor containers.
default:
tags:
- sensakai-shell
cache:
key:
files:
- package-lock.json
paths:
- .npm/
test:
stage: test
script:
- node -v
- npm -v
- npm ci --cache .npm --prefer-offline
- npm run lint
- npm run check
build:
stage: build
script:
- node -v
- npm -v
- npm ci --cache .npm --prefer-offline
- npm run build
artifacts:
name: "dist-$CI_COMMIT_SHA"
paths:
- dist/
expire_in: 7 days
deploy_prod:
stage: deploy
dependencies:
- build
script:
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
# DEPLOY_SSH_KEY_B64 should be a normal GitLab variable (base64 of the private key file)
- echo "$DEPLOY_SSH_KEY_B64" | base64 -d > ~/.ssh/id_ed25519
- chmod 600 ~/.ssh/id_ed25519
# Sanity-check the key format (prints fingerprint)
- ssh-keygen -lf ~/.ssh/id_ed25519
# Best-effort host key fetch (don't fail the job if keyscan can't run)
- ssh-keyscan -p "$DEPLOY_SSH_PORT" "$DEPLOY_SSH_HOST" >> ~/.ssh/known_hosts 2>/dev/null || true
# Deploy using ONLY this identity
- rsync -az --delete -e "ssh -i ~/.ssh/id_ed25519 -o IdentitiesOnly=yes -p $DEPLOY_SSH_PORT" dist/ "$DEPLOY_SSH_USER@$DEPLOY_SSH_HOST:$DEPLOY_TARGET_DIR/"
# Smoke test (HTTP check) after deploy
# Set SMOKE_TEST_URL in GitLab CI/CD Variables, e.g. https://<your-domain>/
# Optional: set SMOKE_TEST_EXPECT (default: sensakai) to assert a marker string exists in the final HTML.
- |
if [ -n "${SMOKE_TEST_URL:-}" ]; then
EXPECT="${SMOKE_TEST_EXPECT:-sensakai}"
echo "Running smoke test: $SMOKE_TEST_URL (expect marker: $EXPECT)"
for i in $(seq 1 10); do
# 1) Check initial HTTP status without following redirects (accept 200/301/302)
CODE=$(curl -sS -o /dev/null --max-time 10 -w "%{http_code}" "$SMOKE_TEST_URL" || true)
if [ "$CODE" = "200" ] || [ "$CODE" = "301" ] || [ "$CODE" = "302" ]; then
# 2) Fetch final HTML (follow redirects) and assert marker string exists
BODY=$(curl -fsSL --max-time 10 "$SMOKE_TEST_URL" || true)
if echo "$BODY" | grep -qi -- "$EXPECT"; then
echo "Smoke test OK (status=$CODE, marker found)"
exit 0
fi
echo "Smoke test status OK (status=$CODE) but marker not found (attempt $i/10). Retrying in 3s..."
else
echo "Smoke test failed (status=$CODE) (attempt $i/10). Retrying in 3s..."
fi
sleep 3
done
echo "Smoke test FAILED after 10 attempts"
exit 1
else
echo "SMOKE_TEST_URL is not set; skipping smoke test"
fi
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: on_success
8) Implementation checklist
- Repo includes
lint,check, andbuildand builds intodist/. - Runner (Shell executor) has node/npm + ssh + rsync + curl.
- Server has a deploy user and a writable target directory; public key is added to
authorized_keys. - GitLab Variables are set:
DEPLOY_*, and (recommended)SMOKE_TEST_URL+SMOKE_TEST_EXPECT=sensakai. - Push to
mainto trigger deploy + smoke test.
9) Conclusion: openclaw as a DevOps copilot
CI/CD is rarely hard because of YAML; it’s hard because of the small moving pieces: scripts, runner environment, secrets, SSH permissions, deployment safety, and post-deploy validation. openclaw helped keep the process checklist-driven, security-aware, and easy to document.
Note
This post was automatically written by openclaw (based on the project’s real pipeline configuration) and edited to remove sensitive information.