Continuous integration explained
This page explains every job and step of the GitHub Actions pipeline (.github/workflows/ci.yml): what it does, which
tests it runs and why, how long it takes, how to run it on your own machine, and how to read a failure. It is written for
contributors and maintainers.
This page is bound to the pipeline. pnpm ci:docs-check (part of scripts/validate.sh, and the ci-docs job of the
pipeline itself) fails when a job or step of a workflow has no section here, when this page documents one that no longer
exists, or when ci.zh.md lists different jobs or steps. Change the pipeline, change this page (and its .zh
mirror) in the same pull request. The rules for the headings are at the end, under Keeping this page true.
Durations are typical values on the GitHub-hosted ubuntu-24.04 runner (2 cores); the run summary of each pull request
shows the real numbers. Test counts are approximate and drift as the project grows.
At a glance
Section titled “At a glance”flowchart LR PR([pull request / push to main / manual run]) --> changes PR --> ci-docs changes -- code changed --> static changes -- code changed --> unit changes -- code changed --> integration changes -- code changed --> e2e changes -- image inputs changed --> images changes -- image inputs changed --> linux-smoke changes --> ci-ok ci-docs --> ci-ok static --> ci-ok unit --> ci-ok integration --> ci-ok e2e --> ci-ok images --> ci-ok linux-smoke --> ci-ok ci-ok --> merge([the one required check])All jobs run side by side; the run takes as long as the slowest one (the end-to-end job, about 10 to 12 minutes) rather
than the sum. ci-ok is the single check a branch protection rule should require: it passes only when every job the
classifier selected succeeded.
Which jobs run depends on what the change touches (decided by the changes job):
| Change | Jobs that run |
|---|---|
Documentation only (*.md, *.html, images) |
changes (with the documentation link check), ci-docs, ci-ok |
| Anything else (code, scripts, config, fixtures, workflows, Dockerfiles) | the above plus static, unit, integration, e2e |
Also image inputs (Dockerfile, .dockerignore, package.json, lockfile, workspace file, next.config.ts, tsconfig*.json, compose.yaml, jobspy/Dockerfile, jobspy/requirements.txt, anything under .github/) |
plus images and linux-smoke |
Push to main |
like a pull request, and integration also runs the default file order |
Manual run (workflow_dispatch) |
everything |
A job the classifier did not select shows as “skipped” and ci-ok accepts exactly that; a selected job that was skipped,
cancelled or failed makes ci-ok fail.
Workflow-level settings: permissions: contents: read (no job can write to the repository; pull requests from forks run
without secrets), a concurrency group per ref (a new push cancels the older run of a pull request, never a run on
main), and every job and step has a timeout-minutes so a hung step fails instead of sitting for hours.
Reading a failed run
Section titled “Reading a failed run”- Open the pull request, look at the checks list:
ci-okis red whenever any job is red. The red job is the one to open. - In the job, the red step is expanded. Pull-request annotations (
::error::lines, Playwright’sgithubreporter) also appear on the “Files changed” tab and in the run summary. - For
e2ethe artifactplaywright-report(kept 7 days) holds the HTML report and the traces of failed tests; open it withpnpm exec playwright show-report playwright-report, or a single trace withpnpm exec playwright show-trace <trace.zip>. - A test that passed only on its retry fails the job on purpose (
--fail-on-flaky-tests): fix the flakiness, do not re-run until green. - Re-run a failed job from the run page (“Re-run failed jobs”) only when the cause is clearly the runner (network, cache service), never to get past a failing test.
The sections below give the commonly seen causes per step.
Job changes
Section titled “Job changes”Decides which of the other jobs are needed and checks the relative links of the documentation. Runs for every change. Limit 5 minutes, typically under 20 seconds.
Check out the repository
Section titled “Check out the repository”actions/checkout@v4 with fetch-depth: 0 (the whole history, because the classifier diffs two commits). Fails only on
GitHub outages.
Classify the change
Section titled “Classify the change”Computes the list of changed files and writes three outputs that the other jobs read:
code:trueunless every changed file is documentation (*.md,*.html,png,jpg,svg,gif). Tests read fixtures, YAML and Dockerfiles, so those count as code.images:truewhen an image input changed (listed above); it selectsimagesandlinux-smoke.full:trueon pushes tomainand manual runs; it adds the default-order integration run.
The range is base...head for a pull request and before..sha for a push. A new branch, a first push or a manual run
selects everything. If the diff cannot be computed the step fails (and so does ci-ok): there is no silent “nothing
changed”. Run it by hand: git diff --name-only origin/main...HEAD, then apply the two patterns above.
Documentation links
Section titled “Documentation links”node scripts/check-doc-links.mjs (also pnpm docs:check, and part of scripts/validate.sh). Checks that every relative
link in the Markdown files and every href / src of the HTML guides points to an existing file; anchors and absolute
URLs are not followed. About 5 seconds. A failure lists file -> target. Typical causes: a renamed or deleted doc, a
typo in a relative path.
Job ci-docs
Section titled “Job ci-docs”Runs pnpm ci:docs-check, which keeps this page and the pipeline in step. It is a separate job so that it also runs for
documentation-only changes (removing a heading of this page is a documentation-only change). About 1 minute, most of it
the dependency install (cached).
Check out the repository
Section titled “Check out the repository”actions/checkout@v4.
Set up pnpm
Section titled “Set up pnpm”pnpm/action-setup@v4: installs the pnpm version pinned in packageManager of package.json.
Set up Node.js
Section titled “Set up Node.js”actions/setup-node@v4, Node 22 (the version in engines), with the pnpm store cached by the hash of pnpm-lock.yaml.
Install dependencies
Section titled “Install dependencies”pnpm install --frozen-lockfile. Fails when package.json and pnpm-lock.yaml disagree: run pnpm install locally and
commit the lockfile. The same step appears in every job that runs project code and is not explained again below.
CI documentation matches the workflows
Section titled “CI documentation matches the workflows”pnpm ci:docs-check (scripts/check-ci-docs.ts). Parses every file under .github/workflows/, requires a name: on
every step, and compares jobs and steps with the headings of docs/ci.md; it also compares the job and step headings of
docs/ci.zh.md with docs/ci.md, and checks that every Dependabot ecosystem is named in the Dependabot
section. Every message says which heading to add, rename or remove. Its own tests are tests/ops/ci-docs-check.test.ts.
Run locally: pnpm ci:docs-check (a second or two).
Job static
Section titled “Job static”The checks that need no database and no browser: generated files, lint, types, configuration, shell syntax, the runner
bundle, migration drift and the Python sidecar tests. Runs only for code changes. Limit 15 minutes, typically 4 to 6
minutes (install, lint and typecheck dominate). All of it except the runner bundle, migration drift and the sidecar
tests is also in scripts/validate.sh.
Check out the repository
Section titled “Check out the repository”actions/checkout@v4.
Set up pnpm
Section titled “Set up pnpm”pnpm/action-setup@v4.
Set up Node.js
Section titled “Set up Node.js”actions/setup-node@v4, Node 22, pnpm store cache.
Install dependencies
Section titled “Install dependencies”pnpm install --frozen-lockfile (see ci-docs).
I18n catalog (messages valid, generated files current)
Section titled “I18n catalog (messages valid, generated files current)”pnpm i18n:check (tsx scripts/i18n-catalog.ts --check). Validates every messages/<id>.json (the core languages en and zh have every key, other languages have no key English lacks, every message uses the
same placeholders as the English one) and verifies that the generated catalog files are what the generator would write now. Failure
text names the key or file. Fix: edit the messages, then pnpm i18n:catalog and commit the regenerated files. A few
seconds.
pnpm lint (eslint .). Whole repository, including tests and scripts. Failures print file:line rule. Fix locally with
pnpm lint --fix where the rule allows it. About 1 minute.
Typecheck
Section titled “Typecheck”pnpm typecheck (tsc --noEmit, strict mode, all of src, tests, scripts, e2e). Catches type errors and the
broken imports behind them. About 1 minute.
Config check
Section titled “Config check”pnpm config:check (tsx scripts/config-check.ts). Validates every shipped default in config/ against its schema and
config_version, every market pack, and the cross-file references (for example that the market of preferences.yaml
resolves). A few seconds. Typical cause: a config edit that violates its zod schema. See
configuration.md.
Shell script syntax
Section titled “Shell script syntax”for f in scripts/*.sh scripts/ops/*.sh; do bash -n "$f"; done. A parse-only check of the operations scripts (deploy,
backup, restore, worker installers, validate). The scripts’ behaviour is tested in tests/ops/. Instant.
Runner bundle
Section titled “Runner bundle”pnpm build:runner: bundles the collector (src/runner/index.ts, src/runner/healthcheck.ts) with esbuild into dist/,
exactly as the Dockerfile runner target does. The e2e job uses a stand-in runner, so this is the only place where a
broken import in the real runner entry point is caught before the image build. About 20 seconds.
Migration drift
Section titled “Migration drift”Runs pnpm exec drizzle-kit generate and fails if it changes anything under drizzle/: the schema in
src/lib/db/schema.ts must match the committed migrations. The failure prints the changed files and
::error::src/lib/db/schema.ts and drizzle/ disagree. Fix: pnpm db:generate --name <slug> and commit the generated SQL
and the journal in drizzle/meta. Do not edit generated SQL by hand to silence it.
Set up Python
Section titled “Set up Python”actions/setup-python@v5, Python 3.12, with the pip cache keyed on jobspy/requirements*.txt.
Install jobspy sidecar dependencies
Section titled “Install jobspy sidecar dependencies”python -m pip install -r requirements-dev.txt in jobspy/. Installs the sidecar’s runtime and test dependencies.
Locally use a virtual environment: python3.12 -m venv .venv && .venv/bin/pip install -r jobspy/requirements-dev.txt.
Jobspy sidecar tests
Section titled “Jobspy sidecar tests”python -m pytest -q in jobspy/ (about 20 tests in jobspy/tests/, with the scraping library mocked: request
validation, result mapping and error handling of the small HTTP service in jobspy/app.py). Seconds. Typical cause: a
change to jobspy/app.py without the matching test, or a dependency bump.
Job unit
Section titled “Job unit”The Vitest unit suite: about 4300 tests in about 330 files under tests/** (everything except tests/integration/),
jsdom for components and the node environment for server code. The suite takes about 4 minutes in one piece, so it is
split with Vitest’s --shard into two jobs, unit (1/2) and unit (2/2), that run in parallel (about 2 to 3 minutes
each). fail-fast is off, so both shards always report. Runs only for code changes; limit 15 minutes.
These tests need no database, no network and no real clock: time is injected or faked, config is built per test in a
temporary directory, and the frozen fixture in tests/fixtures/config/defaults/ replaces the shipped config/ (tests never
assert values of the shipped defaults).
Check out the repository
Section titled “Check out the repository”actions/checkout@v4.
Set up pnpm
Section titled “Set up pnpm”pnpm/action-setup@v4.
Set up Node.js
Section titled “Set up Node.js”actions/setup-node@v4, Node 22, pnpm store cache.
Install dependencies
Section titled “Install dependencies”pnpm install --frozen-lockfile.
Unit tests
Section titled “Unit tests”pnpm test --shard=<n>/2, that is vitest run for half of the files. Locally run everything with pnpm test, one file
with pnpm test tests/ops/backup.test.ts, or one test with -t "<name>". The shard a file lands in is decided by Vitest from
the file list, so a failing file may move between shards when files are added. The failure output is Vitest’s: the
test name, the diff of expected and actual, and the line. Common causes: an unmocked clock, a test that depends on the
time zone or on another test’s leftovers (re-run the file alone to tell), a snapshot of text that changed with the messages.
Job integration
Section titled “Job integration”Tests that need a real PostgreSQL 17: repositories and migrations, the cycle and queues, the control requests, the
ai-worker, the dashboard queries, backups with the real pg_dump / pg_restore. About 600 tests in about 60 files under
tests/integration/, run through vitest.integration.config.ts (node environment, files one after another because they share one
database that is emptied before and after every file). Limit 20 minutes, typically 3 to 4 minutes.
The job has a postgres:17 service container and the environment DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres.
The role may create databases: every run creates a uniquely named database (tests/integration/global-setup.ts) and
drops it at the end, so nothing else on the server is touched. REQUIRE_PG_TOOLS=1 makes the backup tests fail instead
of skipping when the client tools are missing.
Check out the repository
Section titled “Check out the repository”actions/checkout@v4.
Set up pnpm
Section titled “Set up pnpm”pnpm/action-setup@v4.
Set up Node.js
Section titled “Set up Node.js”actions/setup-node@v4, Node 22, pnpm store cache.
Install dependencies
Section titled “Install dependencies”pnpm install --frozen-lockfile.
Provide PostgreSQL 17 client tools
Section titled “Provide PostgreSQL 17 client tools”Creates pg_dump and pg_restore as symlinks to scripts/ops/pg-docker.sh, which runs the tool from the postgres:17
image in a --network host container (so localhost:5432 is reachable). The server and the client must have the same
major version; taking the tools from the image already pulled for the service avoids apt.postgresql.org, which once
hung this step for an hour. It prints both versions as a smoke test. Locally, on Linux, do the same
(ln -sf "$PWD/scripts/ops/pg-docker.sh" /tmp/pgbin/pg_dump, same for pg_restore) and export PG_DUMP_PATH,
PG_RESTORE_PATH; on macOS (Docker Desktop has no host networking) install PostgreSQL 17 client tools (for example
brew install postgresql@17) and point the two variables at them.
Integration tests (default order)
Section titled “Integration tests (default order)”pnpm test:integration --reporter=verbose, only when full is true (pushes to main, manual runs). The verbose
reporter prints each file as it starts, so a hung file is named. Limit 15 minutes. Run locally with a database as
described in Running the whole pipeline locally.
Integration tests (shuffled file order)
Section titled “Integration tests (shuffled file order)”Runs the same suite with --sequence.shuffle.files --sequence.seed=<seed>; the seed is printed as a ::notice::
annotation (integration file order seed: N). Pull requests run only this order: it finds files that depend on
another file’s leftovers, which a fixed order hides. To reproduce a failure, run
pnpm test:integration --sequence.shuffle.files --sequence.seed=<seed> with the seed from the annotation. Limit 15 minutes.
Job e2e
Section titled “Job e2e”The Playwright browser suite against a production build (pnpm build then pnpm start), a throwaway PostgreSQL 17 service
and a stand-in runner (scripts/e2e-runner.ts: heartbeat, config acknowledgement and control-request polling, so
“run a cycle now” and “config saved” work without a real collector). No AI provider, no job channel and no network
beyond localhost. About 240 tests in 35 spec files under e2e/ plus 12 tests in e2e-setup/. Limit 20 minutes, typically
10 to 12 minutes in total. The job runs in the runner host, not in the Playwright container image (measured: the image
was slower).
What the environment looks like: DATABASE_URL points at the service; scripts/e2e-prepare.ts creates the database
jobradar_e2e_3100 (the name follows E2E_PORT), applies the migrations and loads the dashboard seed (about 20 fictional postings), and fills a fresh
config and profile directory from the frozen fixture in tests/fixtures/config/defaults/. E2E_PORT=3100 is the server
port; E2E_SKIP_BUILD=1 tells the web server command not to build again.
Projects of the main suite (playwright.config.ts). The specs do not all run the same way:
The projects are planned in e2e/projects.ts (unit-tested by tests/e2e-projects.test.ts, which also checks with
playwright test --list that a named spec runs alone).
chromiumruns most specs fully in parallel (2 workers on the 2-core runner), each in its own browser context. It ignores every spec that writes shared configuration and every test tagged@exclusive.chromium-exclusiveruns the tests tagged@exclusive, one at a time (workers: 1), afterchromiumhas finished. All specs share one seeded database, and the parallel ones change it as they go: opening a detail page marks the job seen, a visit to Today moves the “last visit” marker, tests ignore, close and star jobs (and put them back), the scoring spec adds a job for a while. A test that changes state every page reads (the visit marker, the seen rows of all jobs:list-today.spec.ts) or that compares counts of the whole list (the/jobsfilter counts indashboard.spec.ts, the filtered toggle infilter.spec.ts, the headline numbers ofinsights.spec.ts) gets the tag. A test that changes one job uses a job no other spec touches and puts it back, and asserts only on that job; it stays parallel.- The specs that rewrite a file of the one shared config directory (settings, language, breakdown, resume, interests,
sources, market) would corrupt each other, or break the assertions of parallel specs that read those settings (the AI
cards on the Sources page, the UI language). They therefore run as single-spec projects chained with
dependencies, one after another and only afterchromium-exclusivehas finished. First the config chain:chromium-config-settings,-settings-invalid,-language,-breakdown,-resume. Then the interests chain:chromium-interests,chromium-interests-suggestionsandchromium-multi-filters(all save views or accepted suggestions to the oneinterests.yaml). Thenchromium-sources-registryandchromium-sources-discovery(both rewritesources.yaml), thenchromium-settings-advancedandchromium-settings-workflow(the advanced editor and the workflow tab rewrite ui, notify, ai, sources and pipeline config), and lastchromium-market(switching the market changes every page andpreferences.yaml). The chain is the reason the total is notslowest spec / 2: it is serial by design. - A new spec that writes any file of the config directory must be added to
SERIAL_CHAINine2e/projects.ts(its own chained project); otherwise it will fail randomly in parallel with other specs. A new test that changes shared database state or counts whole lists gets{ tag: EXCLUSIVE_TAG }. - Spec files named on the command line (
pnpm test:e2e e2e/settings.spec.ts) run alone: only the projects with a matching spec take part, each narrowed to the named specs and still in the order above. (Playwright runs a dependency project in full, so without this a chained spec would first run the whole parallel suite.) With afile:linefilter the selected projects are not chained, so only the tests at those lines run.--project <name>still runs that project’s dependencies; add--no-depsto run it alone. - In CI
retriesis 1 and the reporters aregithub,listandhtml;forbidOnlyrejects a committedtest.only.
The setup wizard suite (playwright.setup.config.ts) is a separate run because it needs a different world: a brand
new install. Its own port (E2E_SETUP_PORT, default 3105), its own database (jobradar_e2e_setup_<port>, dropped,
recreated and migrated without seed by scripts/e2e-prepare.ts --fresh), empty config and profile directories, and no
stand-in runner (requested cycles stay queued). One worker, specs in order, because they walk one install through the wizard.
Check out the repository
Section titled “Check out the repository”actions/checkout@v4.
Set up pnpm
Section titled “Set up pnpm”pnpm/action-setup@v4.
Set up Node.js
Section titled “Set up Node.js”actions/setup-node@v4, Node 22, pnpm store cache.
Install dependencies
Section titled “Install dependencies”pnpm install --frozen-lockfile.
Cache Playwright browsers
Section titled “Cache Playwright browsers”actions/cache@v4 on ~/.cache/ms-playwright, keyed on the runner OS and pnpm-lock.yaml (the browser build follows the
locked @playwright/test version). Sets steps.playwright-cache.outputs.cache-hit.
Install Playwright browser and system libraries
Section titled “Install Playwright browser and system libraries”pnpm exec playwright install --with-deps chromium, only on a cache miss. Downloads Chromium and installs the operating
system libraries it needs (apt). Up to a minute.
Install Playwright system libraries
Section titled “Install Playwright system libraries”pnpm exec playwright install-deps chromium, only on a cache hit: the cache holds the browser, not the operating system
libraries, so those are installed on every run.
Cache the Next build cache
Section titled “Cache the Next build cache”actions/cache@v4 on .next/cache (Next’s incremental build cache only; the app is always built from the current source).
The key contains the lockfile and the hash of src/, messages/, next.config.ts, with the lockfile alone as the fallback.
pnpm build (generates the i18n catalog, then next build). Produces the production build the server starts from. Up to 8
minutes allowed. A failure here is a compile or type error in the app that the static job did not
catch (for instance a server/client boundary violation).
End-to-end tests
Section titled “End-to-end tests”pnpm test:e2e --workers=2 --fail-on-flaky-tests: all projects of the main suite as described above. The web server
command first runs scripts/e2e-prepare.ts, then pnpm start -p 3100 -H 127.0.0.1. A test that only passed on its retry
fails the job; the retry still records a trace for the report. Limit 12 minutes. Locally: start a database
(docker compose up -d db, or the postgres container of the section below), pnpm build, then
E2E_PORT=3100 E2E_SKIP_BUILD=1 pnpm test:e2e; E2E_PORT=<port> gives a run its own database and directories so several
checkouts can run side by side, but run one full suite at a time per machine: parallel full runs starve each other and
produce timeouts that are not real failures. While developing run only the affected specs
(pnpm test:e2e e2e/pipeline.spec.ts, several files, or e2e/dashboard.spec.ts:206 for one test; only those run, the web
server and the stand-in runner start as usual); the full suite belongs to CI. Common failure causes: a locator that changed
with the UI text (the specs run in English), a spec that writes shared config but is missing from SERIAL_CHAIN, a test
that counts a whole list or changes shared rows without the @exclusive tag, a race that only shows up with two workers
(the trace shows it).
Setup wizard end-to-end tests
Section titled “Setup wizard end-to-end tests”pnpm test:e2e:setup --fail-on-flaky-tests, after the main suite on the same build, in its own server, database and
directories (see above). Limit 6 minutes, typically 1 to 2. Report folder: playwright-report-setup/.
Upload the Playwright report and traces
Section titled “Upload the Playwright report and traces”Runs only when a step of the job failed (if: failure()): uploads playwright-report/ and test-results/ as the artifact
playwright-report, kept 7 days. Download it from the run page and open it with pnpm exec playwright show-report.
Job images
Section titled “Job images”Builds the three production images without pushing them: web and runner (targets of the root Dockerfile) and the
jobspy sidecar (jobspy/Dockerfile), as a matrix of three parallel jobs named image (web), image (runner) and
image (jobspy). It catches broken apt / CLI installs, wrong COPY paths and build-stage breakage before a deploy does.
Runs only when an image input changed (see the table at the top). Limit 30 minutes; a few minutes with a warm layer cache, longer cold.
Layers are cached per image in the Actions cache.
Locally: docker build --target web ., docker build --target runner ., docker build jobspy.
Check out the repository
Section titled “Check out the repository”actions/checkout@v4.
Set up Docker Buildx
Section titled “Set up Docker Buildx”docker/setup-buildx-action@v3: the builder that supports the Actions layer cache.
docker/build-push-action@v6 with push: false, cache-from / cache-to of type gha scoped per image. The log shows
the failing Dockerfile line. Common causes: a package that moved in the base image’s repositories, a path added to COPY
but excluded by .dockerignore, a changed lockfile that no longer installs with --frozen-lockfile in the build stage.
Job linux-smoke
Section titled “Job linux-smoke”The Linux first run from scratch: an empty checkout on the Ubuntu runner, docker compose up --build with the shipped
defaults, and the dashboard must answer. It catches what Docker Desktop on macOS hides: file ownership of the bind
mounts (PUID / PGID), config files written with mode 0600, folders the containers cannot write. Runs only when an
image input changed, like images. Limit 45 minutes; about 10 to 15 minutes cold (it builds all four images without the
layer cache). Locally on a Linux machine: the commands of the steps below in a fresh clone (never in a checkout whose
stack and database you want to keep: the last step removes the stack and its volume).
Check out the repository
Section titled “Check out the repository”actions/checkout@v7.
Prepare .env and the data folders as the host user
Section titled “Prepare .env and the data folders as the host user”Writes a .env with only PUID / PGID set to the runner’s own user (id -u, id -g), as
host-setup.md tells Linux users to, and creates the data/ folders and .auth/ as that user, so the
bind mounts are not created by Docker as root.
Start the stack from scratch
Section titled “Start the stack from scratch”docker compose up -d --build --wait --wait-timeout 600: builds the four images and waits until every service reports
healthy (the runner applies the migrations and starts its heartbeat). A failure here is a build error (see the images
job) or a service that never became healthy: the “Container logs on failure” step prints why.
Dashboard health check
Section titled “Dashboard health check”GET /api/health must answer 200, and GET / (a page navigation) 200 or a redirect to the first-run wizard.
The web can write its config folder
Section titled “The web can write its config folder”Creates and removes a file in data/config inside the web container (the settings page and the wizard write there) and
checks that the runner can write data/backups. A failure means the container user does not own the bind-mounted
folders: check user: in compose.yaml and the PUID / PGID lines.
Container logs on failure
Section titled “Container logs on failure”Only when an earlier step failed: docker compose ps -a and the last 200 log lines of every service.
Remove the stack and its volume
Section titled “Remove the stack and its volume”Always: docker compose down -v --remove-orphans on the throwaway runner.
Job ci-ok
Section titled “Job ci-ok”The single check to require. needs lists every other job and if: always() makes it run even when they fail or are
skipped. Limit 5 minutes, a few seconds.
All selected checks passed
Section titled “All selected checks passed”Reads the needs results (jq): changes and ci-docs must have succeeded; static, unit, integration and e2e
must have succeeded when changes selected code and be skipped otherwise; images and linux-smoke likewise by the images output. Any
other combination (including a selected job that was skipped or cancelled) prints the per-job results and
::error::a selected check did not succeed. When it is red, look at the per-job lines it prints first, then open that job.
Dependabot
Section titled “Dependabot”.github/dependabot.yml opens weekly pull requests for three ecosystems:
| Ecosystem | Directory | Notes |
|---|---|---|
npm |
/ |
at most 5 open PRs; minor and patch updates grouped into one PR; major @types/node updates ignored (the types follow the Node major in engines) |
pip |
/jobspy |
at most 3 open PRs; the sidecar’s Python dependencies |
github-actions |
/ |
at most 3 open PRs; the action versions pinned in the workflows |
Dependabot PRs run the same pipeline as any other. An action bump changes .github/, so it counts as code and as an image
input and runs everything; if it renames a step, ci-docs fails until this page is updated.
Release check
Section titled “Release check”scripts/release-check.sh is not part of the pipeline. A maintainer runs it by hand before the repository is published
or a release is cut (RELEASING.md), and whenever a commit looks doubtful: gitleaks (through Docker) over
every file git would track and over the whole commit history (pinned image version), a scan for personal strings and
machine paths, a check that .gitignore covers .env, data/ and .auth/, and a check that private planning ids
(F-, B- or ADR- and three digits, M- or P- and two digits, as used in the maintainer’s tracker) appear in no file and no file name
(ADR in any case), except drizzle/ and pnpm-lock.yaml at the repository root and the exact path or path:line entries in scripts/planning-ids.allow (an id that is data, not a reference). That one check
also runs alone as scripts/release-check.sh --planning-ids-only (no Docker, about a second): it is part of scripts/validate.sh and of
the unit test tests/ops/release-check-script.test.ts, so CI fails when an id comes back. Exit 0 is clean, 1 lists findings, 2 means it could not run. It is a separate gate because it needs
Docker for gitleaks and an untracked local patterns file that CI does not have.
Running the whole pipeline locally
Section titled “Running the whole pipeline locally”Every job above is a few commands. This is the generic recipe (Docker, Node 22 with pnpm, Python 3.12 are enough); it uses a throwaway PostgreSQL on a port that is not your real database’s.
# a throwaway database (the role may create databases, like the CI service)docker run -d --name jr-ci-pg -p 127.0.0.1:55432:5432 \ -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=postgres postgres:17export DATABASE_URL=postgres://postgres:postgres@127.0.0.1:55432/postgres
pnpm install --frozen-lockfile
# jobs: changes (links) and ci-docs, static, unit (the first block is what scripts/validate.sh does)scripts/validate.sh # shell syntax, i18n, lint, typecheck, unit tests, config, doc links, CI docspnpm build:runnerpnpm exec drizzle-kit generate < /dev/null && git status --porcelain -- drizzle # must print nothing(cd jobspy && python3.12 -m venv .venv && .venv/bin/pip install -r requirements-dev.txt && .venv/bin/python -m pytest -q)
# job: integration (needs pg_dump / pg_restore of major version 17, see the job's step)export PG_DUMP_PATH=/path/to/pg_dump PG_RESTORE_PATH=/path/to/pg_restore REQUIRE_PG_TOOLS=1pnpm test:integration --reporter=verbosepnpm test:integration --sequence.shuffle.files --sequence.seed=$RANDOM
# job: e2e (one full suite at a time on this machine)pnpm exec playwright install chromiumpnpm buildE2E_PORT=3100 E2E_SKIP_BUILD=1 pnpm test:e2e --workers=2 --fail-on-flaky-testsE2E_PORT=3100 E2E_SKIP_BUILD=1 pnpm test:e2e:setup --fail-on-flaky-tests
# job: imagesdocker build --target web . && docker build --target runner . && docker build jobspy
# job: linux-smoke (Linux, in a fresh clone only: it ends with `docker compose down -v`)printf 'PUID=%s\nPGID=%s\n' "$(id -u)" "$(id -g)" > .env && mkdir -p data/{config,profile,backups,exports,logs} .authdocker compose up -d --build --wait --wait-timeout 600 && curl -fsS http://127.0.0.1:3000/api/healthdocker compose down -v --remove-orphans
docker rm -f -v jr-ci-pgA local run differs from the hosted one in three ways: it tests your working tree rather than the merge of the pull
request into main, it uses your Docker and Node versions, and it does not decide what to skip (a documentation-only
change needs only pnpm docs:check and pnpm ci:docs-check).
Keeping this page true
Section titled “Keeping this page true”- Every job of a workflow needs a heading
### Job `<job id>`(the key underjobs:; job ids are unique across workflows) and every step of it a heading#### <step name>below it, spelled exactly like the step’sname:. Every step must have aname:; steps that only run an action (checkout, setup) have one too. - Job and step headings are the only
###/####headings used inside the “Jobs” part; put extra explanations under a step, under the job’s introduction or in bold paragraphs. - Mention every Dependabot ecosystem of
.github/dependabot.ymlin backticks in the “Dependabot” section. docs/ci.zh.mdhas the same job and step headings (kept in English because they are identifiers) with translated text.- Run
pnpm ci:docs-check; it prints which heading to add, rename or remove.
Synced from docs/ in Joevonlong/job-radar@2947f47. Job Radar is not affiliated with any job board. Code under AGPL-3.0-only.