doc/ci.md
This page explains how the main CI workflow is structured, how it decides which jobs to run, and the docs-only fast path.
The CI workflow is generated. The source of truth is
.github/workflows/ci.ts, a Deno script that builds the workflow object and
writes it to .github/workflows/ci.generated.yml. Never edit the .yml
directly; it carries a GENERATED BY ./ci.ts -- DO NOT DIRECTLY EDIT header.
To regenerate after changing ci.ts:
deno run -A .github/workflows/ci.ts
Other workflows in .github/workflows/ follow the same *.ts →
*.generated.yml pattern.
On a pull request the workflow runs, roughly:
pre-build — a fast gating job. It decides whether downstream jobs should
run at all and exposes the decision through job outputs.build (per platform) and build libs — compile Deno and the
deno_core libraries.test (per platform, sharded) — the spec, unit, node-compat and WPT
suites, depending on build.bench — benchmarks.deno-core-test and deno-core-miri — the deno_core / libs/*
crate tests (the latter under Miri).lint — formatting check, jsdoc check, and tools/lint.js (Rust + JS).ci-status — an aggregator that the branch protection rule keys on. It
passes as long as none of its dependencies failed or were cancelled; skipped
dependencies are fine.pre-build gates the restpre-build runs a few cheap checks and publishes their results as outputs that
downstream jobs read in their if: conditions:
skip_build — set when a draft PR has not opted into CI. When true, the
build, test, lint, bench and deno_core jobs are all skipped.skip_deno_core_test — set when no deno_core-related files changed, so the
deno_core crate tests can be skipped (tools/check_deno_core_changes.js).docs_only — set when the PR only touches the doc/ directory
(tools/check_docs_only_changes.js).Each gated job's if: combines the relevant outputs, for example:
needs.pre_build.outputs.skip_build != 'true' &&
needs.pre_build.outputs.docs_only != 'true'
A pull request that changes only files under doc/ does not need to compile
or test anything. For those PRs CI runs the lint job and skips everything
else.
How it works:
pre-build, tools/check_docs_only_changes.js diffs the PR against its
base SHA. If every changed file is under doc/ (and at least one file
changed), it writes docs_only=true to the job output. An empty or failed
diff defaults to false, so the full pipeline runs when in doubt.deno-core-test and deno-core-miri
jobs each add && docs_only != 'true' to their run condition, so they are
skipped on a docs-only PR. The test jobs depend on build, so skipping
build cascades to them automatically.lint job is intentionally not gated on docs_only. It still runs,
which means Markdown is still formatted and linted (deno fmt --check covers
doc/). This is the "lint and nothing else" behavior.ci-status still runs and passes: its skipped dependencies count as neither
failure nor cancellation, and lint succeeds, so the required status check
goes green and the PR can merge.The moment a PR also touches anything outside doc/, docs_only is false and
the full pipeline runs as normal.
When you change which jobs run or add a new gate, edit ci.ts, regenerate the
YAML, and commit both. The generated file is checked in so reviewers can see the
effective workflow in the diff.