4.6 KiB
CI/CD Integration
Run FreeCrawl's headless CLI as a quality gate in your pipeline — crawl a deploy, fail the build on broken links, validate the sitemap, and catch SEO regressions between releases.
Two ready-to-adapt templates live next to this file:
| File | Platform |
|---|---|
github-actions-example.yml |
GitHub Actions |
gitlab-ci-example.yml |
GitLab CI/CD |
Both crawl TARGET_URL, fail when 4xx/5xx URLs exceed BROKEN_LINK_THRESHOLD,
validate the sitemap (advisory), and upload the crawl as an artifact.
How the CLI is built in CI
The runner clones the source and builds only the CLI and the packages it imports:
npm ci --ignore-scripts # skip the Playwright download — undici crawl needs no Chromium
npx tsc -b apps/cli # builds shared-types → db → core → cli via project references
node apps/cli/dist/index.js <url> [options]
npm run build:clicompiles onlyapps/cliand does not build the workspace packages the CLI imports, so the crawl would fail to resolve@freecrawl/coreat runtime. Usenpx tsc -b apps/cli, which follows the project references and builds the dependency graph in order — while skipping the heavy Electron desktop build. Drop--ignore-scriptsonly if you enable JS rendering via a--configfile (that path needs Chromium).
Exit-code contract
Every command returns a CI-friendly exit code, so most gates need no scripting:
| Command | 0 |
1 |
2 |
|---|---|---|---|
<url> (crawl) |
clean — no 4xx/5xx | a 4xx/5xx was found, the seed was unreachable, or nothing was fetched | — |
compare <before> <after> |
no differences | at least one diff category is non-zero | a project failed to open |
validate-sitemap <url> |
valid | validation warnings | fetch failure |
audit-robots <url> |
URL allowed | URL disallowed | invalid URL |
Gate variants
Zero-tolerance (no script). Let the crawl's own exit code fail the step — any 4xx/5xx fails the build:
- run: node apps/cli/dist/index.js "$TARGET_URL" --max 5000
Threshold (what the templates do). Tolerate up to N broken URLs. Run the
crawl with --json (append || true so the CLI's exit code doesn't pre-empt
the gate), then count 4xx/5xx from the summary's byStatus map:
node apps/cli/dist/index.js "$TARGET_URL" --max 5000 --json > summary.json || true
node -e "
const s = require('./summary.json');
const by = (s.summary && s.summary.byStatus) || {};
let n = 0;
for (const [code, count] of Object.entries(by)) if (Number(code) >= 400) n += count;
if (n > Number(process.env.BROKEN_LINK_THRESHOLD)) { console.error('too many broken:', n); process.exit(1); }
"
Reading byStatus (a { '200': 1234, '404': 5, … } map) is robust against the
full dump's column layout and avoids parsing a multi-MB file just to count.
Regression gating with compare
Fail a deploy when SEO metrics regress against a known-good baseline. Keep a
baseline .seoproject (from a previous green run, restored from cache or an
artifact store), crawl the new deploy into a fresh project, then diff:
node apps/cli/dist/index.js "$TARGET_URL" --max 5000 --db new.seoproject
node apps/cli/dist/index.js compare baseline.seoproject new.seoproject
# exit 1 → a page changed status / title / meta / h1 / canonical / indexability
compare runs the same engine as the desktop Compare dialog and exits 1 on
any non-zero diff category, so it drops straight into a pipeline gate. Promote
new.seoproject to the new baseline when the pipeline passes.
Reproducible config
Export your settings once from the desktop app (Settings → Export Settings) and commit the JSON, then feed it to every run so CI crawls exactly like your desktop audits:
node apps/cli/dist/index.js "$TARGET_URL" --config freecrawl.config.json --max 5000
Per-flag overrides still win over the file, so --max can differ per run while
the shared config stays fixed.