Ziel: Jeden Dev-Deploy leichtgewichtig absichern – ohne Cypress/Playwright‑Suite. Fokus: API ok? Seiten rendern? A11y ok? Links nicht tot?
- 3–4 kleine Skripte + 1 Job in GitLab.
- Laufzeit: ~2–3 Minuten pro Deploy.
- Wartungsarm, Agentur muss keine E2E liefern.
pnpm add -D cheerio linkinator pa11y # Kern-Snippets
# optional für Mini-Browser-Smoke
pnpm add -D playwrightStruktur (Beispiel):
scripts/
smoke/
api.smoke.mjs
html.smoke.mjs
links.smoke.mjs
browser.smoke.ts # optional
Setze die Zielbasis via ENV: BASE=https://dev.example.com.
Sichert: Status 200, Shape/Keys, Cache‑Header.
// scripts/smoke/api.smoke.mjs
const BASE = process.env.BASE || 'https://dev.example.com'
const must = (cond, msg) => { if (!cond) { console.error(msg); process.exit(1) } }
const check = async (path, validate) => {
const res = await fetch(BASE + path)
must(res.status === 200, `${path}: expected 200, got ${res.status}`)
await validate(res)
console.log(`✓ ${path}`)
}
await check('/++api++/@search?portal_type=Document', async (r) => {
const js = await r.json()
must(Array.isArray(js.items), '@search: items[] missing')
const cc = r.headers.get('cache-control') || ''
must(/max-age|s-maxage/i.test(cc), '@search: Cache-Control missing max-age/s-maxage')
})
await check('/++api++/@navigation?root=/', async (r) => {
const js = await r.json()
must(Array.isArray(js.items) && js.items.length > 0, '@navigation: no items')
})Sichert: Seiten rendern (SSR). Nutzt Cheerio (kein Browser).
// scripts/smoke/html.smoke.mjs
import * as cheerio from 'cheerio'
const BASE = process.env.BASE || 'https://dev.example.com'
const must = (c,m) => { if(!c){ console.error(m); process.exit(1) } }
const probe = async (path, sel, why) => {
const res = await fetch(BASE + path)
must(res.status === 200, `${path}: HTTP ${res.status}`)
const html = await res.text()
const $ = cheerio.load(html)
must($(sel).length > 0, `${path}: selector ${sel} missing (${why})`)
console.log(`✓ ${path} has ${sel}`)
}
await probe('/', 'header nav', 'Navigation sichtbar')
await probe('/de/search', 'h1', 'H1 vorhanden')
await probe('/de/story/beispiel', 'main article', 'Story-Container sichtbar')
⚠️ Wenn SSR aus: Diesen Check überspringen oder unten Mini‑Browser‑Smoke verwenden.
Sichert: WCAG2AA – Errors auf Kernseiten (nur Errors zählen).
npx pa11y "$BASE/" --standard WCAG2AA
npx pa11y "$BASE/de/search" --standard WCAG2AA
npx pa11y "$BASE/de/kontakt" --standard WCAG2AASichert: keine 404/500 auf Home + eine Ebene Tiefe.
// scripts/smoke/links.smoke.mjs
import { LinkChecker } from 'linkinator'
const BASE = process.env.BASE || 'https://dev.example.com'
const checker = new LinkChecker()
const result = await checker.check({ path: BASE + '/', recurse: true, maxDepth: 1, retry: true })
const bad = result.links.filter(l => !l.state || l.state.status !== 'OK')
if (bad.length) {
console.error('Broken links:', bad.map(b => `${b.url} (${b.status})`).join('\n'))
process.exit(1)
}
console.log('✓ Links OK')Minimaler „lebt‑die‑App?“-Check ohne E2E‑Suite.
// scripts/smoke/browser.smoke.ts
import { chromium } from 'playwright'
const BASE = process.env.BASE || 'https://dev.example.com'
const must = (c,m) => { if(!c){ console.error(m); process.exit(1)} }
const browser = await chromium.launch()
const page = await browser.newPage()
await page.goto(BASE + '/')
must(await page.getByRole('navigation').count() > 0, 'Nav fehlt')
await page.getByRole('link', { name: /story|geschichten|news/i }).first().click().catch(()=>{})
await page.waitForLoadState('networkidle')
must(await page.getByRole('heading', { level: 1 }).count() > 0, 'H1 auf Detail fehlt')
await browser.close()
console.log('✓ Mini-Browser-Smoke OK')# .gitlab-ci.yml (Ausschnitt)
stages: [build, deploy, postdeploy]
dev-smoke:
stage: postdeploy
needs: ["deploy-dev"] # ↩︎ euren Deploy-Job eintragen
image: cr.gitlab.fhnw.ch/webteam/fhnw-webauftritt-2025/node:22.15.0
rules:
- if: '$CI_COMMIT_BRANCH =~ /^(develop|redesign|main)$/'
variables:
BASE: "https://dev.example.com" # ↩︎ eure Dev-URL
script:
- pnpm i --frozen-lockfile
- node scripts/smoke/api.smoke.mjs
- node scripts/smoke/html.smoke.mjs
- npx pa11y "$BASE/" --standard WCAG2AA
- node scripts/smoke/links.smoke.mjs
# optional:
# - pnpm tsx scripts/smoke/browser.smoke.ts
allow_failure: true # Woche 1–2 soft, dann entfernen
artifacts:
when: always
expire_in: 1 dayrules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: delayed
start_in: 7 minutes # Puffer passend zu eurer Deploy-DauerBeim Build eine kleine Metadatei ausliefern und danach darauf warten.
Build‑Snippet (FE):
node -e "require('fs').writeFileSync('build/_build.json', JSON.stringify({sha: process.env.CI_COMMIT_SHORT_SHA, built: new Date().toISOString()}))"Wait‑Script:
# scripts/wait-for-deploy.sh
#!/usr/bin/env bash
set -euo pipefail
BASE="${1:?BASE missing}"; SHA="${2:?SHA missing}"; TIMEOUT="${3:-600}"
deadline=$(( $(date +%s) + TIMEOUT ))
echo "Waiting for $SHA on $BASE/_build.json (timeout ${TIMEOUT}s)…"
while true; do
if data=$(curl -sf "$BASE/_build.json"); then
live=$(echo "$data" | sed -n 's/.*"sha" *: *"\([^"]*\)".*/\1/p')
if [[ "$live" == "$SHA" ]]; then
echo "OK: live SHA = $live"; exit 0
else
echo "still $live …"
fi
else
echo "endpoint not ready…"
fi
if (( $(date +%s) > deadline )); then echo "Timeout waiting for $SHA"; exit 1; fi
sleep 10
doneYAML:
wait-for-dev:
stage: postdeploy
image: curlimages/curl:8.8.0
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: delayed
start_in: 5 minutes
variables:
BASE: "https://dev.example.com"
script:
- apk add --no-cache bash
- ./scripts/wait-for-deploy.sh "$BASE" "$CI_COMMIT_SHORT_SHA" 900
dev-smoke:
stage: postdeploy
needs: ["wait-for-dev"]
# ... (wie oben)- GitLab Trigger-Token anlegen, CD ruft nach Deploy:
curl -X POST \
-F token=$DEV_SMOKE_TRIGGER_TOKEN \
-F ref=main \
-F "variables[PIPELINE_KIND]=DEV_SMOKE" \
https://gitlab.example.com/api/v4/projects/<PROJECT_ID>/trigger/pipeline- In
.gitlab-ci.yml:
workflow:
rules:
- if: '$PIPELINE_KIND == "DEV_SMOKE"'
- when: never- API: echte Endpoints, Header & Shape ok → Backend/Proxy/CDN korrekt.
- HTML/SSR: Seiten rendern → Templating/Routing in Ordnung.
- A11y: grobe WCAG‑Fehler früh sichtbar.
- Links: keine offensichtlichen 404/500 nach Content‑Änderungen.
Startet mit
allow_failure: true. Wenn stabil → hart schalten. Drei Skripte, großer Effekt.