Cypress reporter in CI
The reporter runs inside npx cypress run: give the step that runs your tests the token, and every
result is sent when the run ends. There is no separate report step. Every example assumes the
reporter, its plugin and its support file are registered as in
Get started.
In every CI
Section titled “In every CI”- The token is a secret, the Project ID is not. Store the app token (from the Cypress card
in Workspace › Integrations) as a secret named
PROBARA_API_TOKEN. Put the Project ID incypress.config.js(projectId) or in the pipeline (PROBARA_PROJECT). - Node.js 22.12 or later for the Node.js that starts
cypress, which is where the plugin runs, and the browsers and system libraries Cypress needs on the agent. - Failed tests fail the step, reporting problems do not: the exit code of
cypress runstays the number of its failed tests. cypress-io/github-actionandnpm testwork too: the reporter comes from the config, not from the command. Give their step the same environment.- Pin the version in
package.json(or the lockfile), so a new release never changes a pipeline by surprise. The CLI is not a dependency of the reporter: the steps below that run it pin it in the command (npx --yes @probara/cli@0.3). With@probara/cliin yourdevDependencies,npx @probara/cliafternpm ciruns the locked version instead. - Builds of forks report nothing: without the secret, the reporter sends nothing and the tests
run as usual. The CLI exits 2 without a token, so the recipes turn reporting off
(
PROBARA_ENABLED=false) where a fork’s build would run it: it then exits 0. Each section below says how to keep the secret out of fork builds on that CI. - Split pipelines make one run per machine unless they share one. In GitHub Actions the jobs of one workflow run share one run automatically (parallel jobs). Elsewhere, create the run first, pass its ULID to every machine, and close it at the end (sharding).
What CI detection fills in
Section titled “What CI detection fills in”Unless an option or a PROBARA_* variable sets them, the run gets its name and source from the CI:
| CI | Detected by | Run name |
|---|---|---|
| GitHub Actions | GITHUB_ACTIONS=true | GITHUB_WORKFLOW #GITHUB_RUN_NUMBER |
| GitLab CI | GITLAB_CI=true | CI_PROJECT_NAME #CI_PIPELINE_IID |
| Jenkins | JENKINS_URL | JOB_NAME #BUILD_NUMBER |
| CircleCI | CIRCLECI=true | CIRCLE_PROJECT_REPONAME #CIRCLE_BUILD_NUM |
| Azure Pipelines | TF_BUILD=True | BUILD_DEFINITIONNAME #BUILD_BUILDNUMBER |
| Bitbucket Pipelines | BITBUCKET_BUILD_NUMBER | BITBUCKET_REPO_SLUG #BITBUCKET_BUILD_NUMBER |
| Buildkite | BUILDKITE=true | BUILDKITE_PIPELINE_SLUG #BUILDKITE_BUILD_NUMBER |
The branch, commit and build URL of each are listed in its section, and in
@probara/core’s CI detection.
Override them with PROBARA_BRANCH, PROBARA_COMMIT and PROBARA_BUILD_URL.
GitHub Actions
Section titled “GitHub Actions”1. Add the secret
Section titled “1. Add the secret”In the repository (or organization): Settings → Secrets and variables → Actions → New repository
secret, named PROBARA_API_TOKEN, holding the app token.
2. Give the test step the token
Section titled “2. Give the test step the token”name: Cypress
on: push: branches: [main] pull_request:
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: node-version: 22 - run: npm ci - name: Run Cypress tests run: npx cypress run env: PROBARA_API_TOKEN: ${{ secrets.PROBARA_API_TOKEN }} PROBARA_PROJECT: SHOPParallel jobs: one run, closed by the final job
Section titled “Parallel jobs: one run, closed by the final job”In GitHub Actions the jobs of one workflow run report into one run: the run’s source names the
workflow run by its repository, run id and attempt, and the first job to report creates the run. A
job closes its run at its end by default, so the parallel jobs set PROBARA_CLOSE_RUN: false. A
final job closes the run with probara run close, which needs no run ULID there: needs makes it
wait for every test job, and if: always() runs it when they fail. Cypress has no --shard flag,
so each job runs its own specs, here with --spec.
jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: spec: ['cypress/e2e/cart/**', 'cypress/e2e/login/**'] steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: node-version: 22 - run: npm ci - run: npx cypress run --spec '${{ matrix.spec }}' env: PROBARA_API_TOKEN: ${{ secrets.PROBARA_API_TOKEN }} PROBARA_PROJECT: SHOP # A job leaves the run open for the others. PROBARA_CLOSE_RUN: false
probara: needs: test if: always() runs-on: ubuntu-latest steps: - uses: actions/setup-node@v5 with: node-version: 22 - run: npx --yes @probara/cli@0.6 run close env: PROBARA_API_TOKEN: ${{ secrets.PROBARA_API_TOKEN }} PROBARA_PROJECT: SHOP- A job after the close is refused with
409 ci_run_closed: the reporter logs why, sends nothing more, and the tests keep their own exit code (troubleshooting). Keep every reporting job inneeds. - A re-run of the workflow is another attempt: it reports into a run of its own.
- One job only needs neither: its run is closed at the end, as anywhere else.
CI detection
Section titled “CI detection”| Field | From |
|---|---|
| Run name | GITHUB_WORKFLOW #GITHUB_RUN_NUMBER, such as Cypress #42 |
| Branch | GITHUB_HEAD_REF (the pull request’s branch), else GITHUB_REF_NAME; none for a tag |
| Commit | GITHUB_SHA |
| Build URL | GITHUB_SERVER_URL/GITHUB_REPOSITORY/actions/runs/GITHUB_RUN_ID |
| Repository | GITHUB_REPOSITORY |
| Commit author | GITHUB_ACTOR |
| Pull request | <n> of GITHUB_REF refs/pull/<n>/merge |
| Workflow | GITHUB_WORKFLOW |
| CI run | GITHUB_RUN_ID, attempt GITHUB_RUN_ATTEMPT |
| CI job | GITHUB_JOB |
On pull_request events GITHUB_SHA is the merge commit GitHub made, not the branch’s last commit.
To record the latter, add PROBARA_COMMIT: ${{ github.event.pull_request.head.sha }}. In GitHub
Actions the commit must be a 7 to 40 character hex SHA: another value is dropped with a warning.
Fork pull requests
Section titled “Fork pull requests”Workflows triggered by a pull request from a fork get empty secrets. Without a token the reporter
stays off, so fork builds run their tests and report nothing, with no extra setting. Do not use
pull_request_target to get the secret into a fork build: it runs the fork’s code with your
secrets.
GitLab CI
Section titled “GitLab CI”1. Add the variable
Section titled “1. Add the variable”Settings → CI/CD → Variables → Add variable: key PROBARA_API_TOKEN, the token as the value,
with Mask variable on. Protect variable limits it to protected branches and tags: turn it on
only if you report from those alone.
2. The test job
Section titled “2. The test job”stages: - test
cypress: stage: test image: cypress/browsers:latest variables: PROBARA_PROJECT: SHOP script: - npm ci - npx cypress runcypress/browsersholds Node.js, the browsers and the system libraries Cypress needs; pin a tag whose Node.js is 22.12 or later.- A job without the variable (a fork’s pipeline, an unprotected branch with a protected variable) runs its tests and reports nothing.
- To keep what could not be sent, set
PROBARA_RESULTS_FILEand keep the file and its attachments folder as artifacts (results file). parallel:jobs sharing one run, with a.prejob that creates it and a.postjob that closes it, are in sharding.
CI detection
Section titled “CI detection”| Field | From |
|---|---|
| Run name | CI_PROJECT_NAME #CI_PIPELINE_IID, such as shop #42 |
| Branch | CI_MERGE_REQUEST_SOURCE_BRANCH_NAME in a merge request pipeline, else CI_COMMIT_REF_NAME; none for a tag pipeline |
| Commit | CI_COMMIT_SHA |
| Build URL | CI_PIPELINE_URL |
Fork merge requests
Section titled “Fork merge requests”A merge request from a fork runs its pipeline in the fork’s project, which does not have your CI/CD variables: the reporter stays off there. If maintainers run fork pipelines in the parent project, those pipelines do get the variable, so review the change before you run it.
Jenkins
Section titled “Jenkins”1. Add the credential
Section titled “1. Add the credential”Manage Jenkins → Credentials → (a domain) → Add Credentials: kind Secret text, ID
probara-api-token, the token as the secret.
2. The pipeline
Section titled “2. The pipeline”A declarative Jenkinsfile, with Node.js 22 and the system libraries Cypress needs on the agent,
binds the token into the test stage with withCredentials:
pipeline { agent any environment { PROBARA_PROJECT = 'SHOP' } stages { stage('Test') { steps { sh 'npm ci' withCredentials([string(credentialsId: 'probara-api-token', variable: 'PROBARA_API_TOKEN')]) { sh 'npx cypress run' } } } }}Jenkins masks the bound secret in the build log.
Parallel stages: one run
Section titled “Parallel stages: one run”Parallel stages that each run part of the specs make one run each unless they share one: create it
in a first stage, pass it to each, and close it in a post { always { ... } } block:
pipeline { agent any environment { PROBARA_PROJECT = 'SHOP' } stages { stage('Create the Probara run') { steps { sh 'npm ci' withCredentials([string(credentialsId: 'probara-api-token', variable: 'PROBARA_API_TOKEN')]) { sh 'npx --yes @probara/cli@0.3 run create > probara-run-ulid' } script { env.PROBARA_RUN_ULID = readFile('probara-run-ulid').trim() } } } stage('Test') { parallel { stage('Cart') { steps { withCredentials([string(credentialsId: 'probara-api-token', variable: 'PROBARA_API_TOKEN')]) { sh "npx cypress run --spec 'cypress/e2e/cart/**' --config screenshotsFolder=cypress/screenshots-cart" } } } stage('Login') { steps { withCredentials([string(credentialsId: 'probara-api-token', variable: 'PROBARA_API_TOKEN')]) { sh "npx cypress run --spec 'cypress/e2e/login/**' --config screenshotsFolder=cypress/screenshots-login" } } } } } } post { always { withCredentials([string(credentialsId: 'probara-api-token', variable: 'PROBARA_API_TOKEN')]) { sh 'npx --yes @probara/cli@0.3 run close' } } }}Both stages share one workspace here, installed once before they start (two npm ci at the same
time in one folder would break each other). Each cypress run empties its screenshots folder when
it starts, so on one agent each stage gets its own screenshotsFolder. On separate agents, each
checks out and installs on its own.
Fork pull requests
Section titled “Fork pull requests”Jenkins does not hide credentials from builds of pull requests by itself: a multibranch job that
builds forks can hand the token to their code. Set Discover pull requests from forks → Trust so
that a pull request from anyone else builds with the Jenkinsfile of its target branch: in the
GitHub branch source, From users with Admin or Write permission (or Nobody); in the
Bitbucket branch source, Nobody, or Forks in the same account from plugin version 871 on
(the fix for CVE-2024-28152). Everyone builds a fork with its own Jenkinsfile, which can bind
the credential itself, and makes the env.CHANGE_FORK guard below useless.
In the target branch’s Jenkinsfile, bind the credential only when env.CHANGE_FORK is not set
(Jenkins sets it for a pull request from a fork), and turn reporting off for a fork build:
pipeline { agent any environment { PROBARA_PROJECT = 'SHOP' } stages { stage('Test') { steps { sh 'npm ci' script { if (env.CHANGE_FORK) { withEnv(['PROBARA_ENABLED=false']) { sh 'npx cypress run' } } else { withCredentials([string(credentialsId: 'probara-api-token', variable: 'PROBARA_API_TOKEN')]) { sh 'npx cypress run' } } } } } }}In the parallel pipeline, wrap each withCredentials the same way: with reporting off,
run create writes no ULID and run close exits 0 without a token.
CI detection
Section titled “CI detection”With JENKINS_URL set, the run is named JOB_NAME #BUILD_NUMBER, and its branch, commit and build
URL come from BRANCH_NAME (or GIT_BRANCH), GIT_COMMIT and BUILD_URL.
CircleCI
Section titled “CircleCI”1. Add the token
Section titled “1. Add the token”Organization Settings → Contexts → Create Context (such as probara-reporting), then add the
environment variable PROBARA_API_TOKEN. A project environment variable (Project Settings →
Environment Variables) works too; then drop context: below.
2. The job
Section titled “2. The job”A .circleci/config.yml whose specs are split across parallelism machines by
circleci tests split, into one run created up front. Without parallelism, keep only the test
job and run npx cypress run:
version: 2.1
jobs: create-run: docker: - image: cimg/node:22.12 environment: PROBARA_PROJECT: SHOP steps: - run: | # Without the token (a fork's build), turn reporting off: run create prints nothing. [ -n "$PROBARA_API_TOKEN" ] || export PROBARA_ENABLED=false PROBARA_RUN_ULID=$(npx --yes @probara/cli@0.3 run create) echo "PROBARA_RUN_ULID=$PROBARA_RUN_ULID" > probara.env - persist_to_workspace: root: . paths: [probara.env] test: docker: - image: cimg/node:22.12-browsers parallelism: 4 environment: PROBARA_PROJECT: SHOP steps: - checkout - attach_workspace: at: . - run: npm ci - run: | export $(cat probara.env) SPECS=$(circleci tests glob 'cypress/e2e/**/*.cy.js' | circleci tests split | paste -sd, -) npx cypress run --spec "$SPECS" close-run: docker: - image: cimg/node:22.12 environment: PROBARA_PROJECT: SHOP steps: - attach_workspace: at: . - run: when: always command: | export $(cat probara.env) [ -n "$PROBARA_API_TOKEN" ] || export PROBARA_ENABLED=false npx --yes @probara/cli@0.3 run close
workflows: cypress: jobs: - create-run: context: probara-reporting - test: context: probara-reporting requires: [create-run] - close-run: context: probara-reporting requires: - test: [success, failed]circleci tests split gives each machine its share of the spec files, and --spec takes them as
one comma-separated list. The -browsers image holds the browsers and the system libraries Cypress
needs.
Forks and CI detection
Section titled “Forks and CI detection”CircleCI does not pass contexts or project variables to builds of forked pull requests unless Pass
secrets to builds from forked pull requests is on: leave it off. A fork’s jobs that use a context
then do not run at all (CircleCI shows an authorization error). With project variables they run
without the token: the reporter sends nothing, and the PROBARA_ENABLED=false line of create-run
and close-run makes run create print nothing and both exit 0.
With CIRCLECI=true, the run is named CIRCLE_PROJECT_REPONAME #CIRCLE_BUILD_NUM, with the branch
CIRCLE_BRANCH, the commit CIRCLE_SHA1 and the build URL CIRCLE_BUILD_URL.
Azure Pipelines
Section titled “Azure Pipelines”1. Add the variable
Section titled “1. Add the variable”Pipelines → your pipeline → Edit → Variables → New variable: name PROBARA_API_TOKEN, the token
as the value, Keep this value secret on. A variable group of the library works too. Secret
variables never reach scripts on their own: map the variable into the step’s env, as below.
2. The job
Section titled “2. The job”variables: # Off in a fork's build, which gets no secrets (see forks below). PROBARA_ENABLED: $[ ne(variables['System.PullRequest.IsFork'], 'True') ]
pool: vmImage: ubuntu-latest
steps: - task: NodeTool@0 inputs: versionSpec: '22.x' - script: npm ci - script: npx cypress run env: PROBARA_API_TOKEN: $(PROBARA_API_TOKEN) PROBARA_PROJECT: SHOPubuntu-latest has the browsers and the system libraries Cypress needs.
Parallel jobs: one run
Section titled “Parallel jobs: one run”A matrix of jobs that each run part of the specs, into one run created by a first job and closed
by a last one that runs even after failed tests:
variables: PROBARA_ENABLED: $[ ne(variables['System.PullRequest.IsFork'], 'True') ]
jobs: - job: create_run pool: vmImage: ubuntu-latest steps: - task: NodeTool@0 inputs: versionSpec: '22.x' - script: | set -e ulid=$(npx --yes @probara/cli@0.3 run create) echo "##vso[task.setvariable variable=ulid;isOutput=true]$ulid" name: probara env: PROBARA_API_TOKEN: $(PROBARA_API_TOKEN) PROBARA_PROJECT: SHOP
- job: test dependsOn: create_run strategy: matrix: cart: SPEC: 'cypress/e2e/cart/**' login: SPEC: 'cypress/e2e/login/**' variables: PROBARA_RUN_ULID: $[ dependencies.create_run.outputs['probara.ulid'] ] pool: vmImage: ubuntu-latest steps: - task: NodeTool@0 inputs: versionSpec: '22.x' - script: npm ci - script: npx cypress run --spec '$(SPEC)' env: PROBARA_API_TOKEN: $(PROBARA_API_TOKEN) PROBARA_PROJECT: SHOP PROBARA_RUN_ULID: $(PROBARA_RUN_ULID)
- job: close_run dependsOn: [create_run, test] condition: always() variables: PROBARA_RUN_ULID: $[ dependencies.create_run.outputs['probara.ulid'] ] pool: vmImage: ubuntu-latest steps: - task: NodeTool@0 inputs: versionSpec: '22.x' - script: npx --yes @probara/cli@0.3 run close env: PROBARA_API_TOKEN: $(PROBARA_API_TOKEN) PROBARA_PROJECT: SHOP PROBARA_RUN_ULID: $(PROBARA_RUN_ULID)Forks and CI detection
Section titled “Forks and CI detection”Builds of pull requests from forks get no secret variables unless Make secrets available to builds
of forks is on: leave it off. Azure leaves a macro it cannot resolve as it is, so in a fork’s
build $(PROBARA_API_TOKEN) reaches the step as that literal text, a token Probara refuses with 401. The pipeline’s PROBARA_ENABLED variable is False when System.PullRequest.IsFork is
True, so a fork’s build runs its tests with reporting off, and run create and run close exit 0.
With TF_BUILD=True, the run is named BUILD_DEFINITIONNAME #BUILD_BUILDNUMBER, and its branch,
commit and build URL come from the pipeline’s variables.
Bitbucket Pipelines
Section titled “Bitbucket Pipelines”1. Add the variable
Section titled “1. Add the variable”Repository settings → Pipelines → Repository variables: name PROBARA_API_TOKEN, the token as
the value, Secured on. Bitbucket passes repository variables to every step, so the reporter finds
it in npx cypress run.
2. The pipeline
Section titled “2. The pipeline”image: cypress/browsers:latest
pipelines: default: - step: name: Cypress script: - npm ci - export PROBARA_PROJECT=SHOP - npx cypress runParallel steps: one run
Section titled “Parallel steps: one run”Parallel steps that each run part of the specs report into one run created by a first step and closed by a last one:
image: cypress/browsers:latest
pipelines: default: - step: name: Create the Probara run script: - export PROBARA_PROJECT=SHOP # Without the token (a fork's build), turn reporting off: run create prints nothing. - test -n "$PROBARA_API_TOKEN" || export PROBARA_ENABLED=false - PROBARA_RUN_ULID=$(npx --yes @probara/cli@0.3 run create) - echo "PROBARA_RUN_ULID=$PROBARA_RUN_ULID" > probara.env artifacts: - probara.env - parallel: - step: name: Cart script: - npm ci - export PROBARA_PROJECT=SHOP $(cat probara.env) - npx cypress run --spec 'cypress/e2e/cart/**' || touch cart.failed artifacts: - '*.failed' - step: name: Login script: - npm ci - export PROBARA_PROJECT=SHOP $(cat probara.env) - npx cypress run --spec 'cypress/e2e/login/**' || touch login.failed artifacts: - '*.failed' - step: name: Close the Probara run script: - export PROBARA_PROJECT=SHOP $(cat probara.env) - test -n "$PROBARA_API_TOKEN" || export PROBARA_ENABLED=false - npx --yes @probara/cli@0.3 run close - if ls *.failed >/dev/null 2>&1; then echo "Tests failed in a parallel step" >&2; exit 1; fiBitbucket runs no step after a failed one, so a parallel step that failed would leave the run open.
Each step records failed tests in a <name>.failed artifact instead, and the last step closes the
run, then fails the pipeline when a step left one: the parallel steps show as passed, and the last
step carries the failure. If the close still never ran, close the run in Probara, or
by hand.
Forks and CI detection
Section titled “Forks and CI detection”Pull requests from forks get no secured variables: the reporter sends nothing there. With
BITBUCKET_BUILD_NUMBER set, the run is named BITBUCKET_REPO_SLUG #BITBUCKET_BUILD_NUMBER, with
the branch BITBUCKET_BRANCH, the commit BITBUCKET_COMMIT and a link to the pipeline.
Buildkite
Section titled “Buildkite”1. Add the token
Section titled “1. Add the token”Expose PROBARA_API_TOKEN to the steps from a secret store: an agent environment hook that reads
it from your secrets manager, or
Buildkite secrets.
2. The pipeline
Section titled “2. The pipeline”A pipeline.yml whose parallel jobs report into one run created by a first step and closed after
them, even when they failed. Without parallelism, one step running npx cypress run is enough:
env: PROBARA_PROJECT: SHOP
steps: - label: Create the Probara run key: create-run command: | PROBARA_RUN_ULID="$(npx --yes @probara/cli@0.3 run create)" if [ -n "$$PROBARA_RUN_ULID" ]; then buildkite-agent meta-data set probara-run "$$PROBARA_RUN_ULID"; fi
- label: Cypress depends_on: create-run parallelism: 4 command: | npm ci export PROBARA_RUN_ULID="$(buildkite-agent meta-data get probara-run --default '')" SPECS=$(ls cypress/e2e/*.cy.js | awk -v i="$$BUILDKITE_PARALLEL_JOB" -v n="$$BUILDKITE_PARALLEL_JOB_COUNT" 'NR % n == i' | paste -sd, -) npx cypress run --spec "$$SPECS"
- wait: ~ continue_on_failure: true
- label: Close the Probara run command: PROBARA_RUN_ULID="$(buildkite-agent meta-data get probara-run --default '')" npx --yes @probara/cli@0.3 run closebuildkite-agent pipeline uploadfills in$NAMEand${NAME}when it uploads the pipeline, and a variable it does not know then, such asBUILDKITE_PARALLEL_JOB_COUNTthat only the parallel jobs get, becomes blank; a$$is left as one$for the job’s shell, while$(...)and$((...))are left as they are.- The run is created on a line of its own, so a failed
run createfails the first step rather than storing a blank ULID. With reporting off (a fork build on an agent that setsPROBARA_ENABLED=false, as below),run createprints no ULID and exits 0: nothing is stored,meta-data get --default ''gives the other steps an empty ULID instead of an error, and they run with reporting off too. - Each parallel job takes every
n-th spec file (BUILDKITE_PARALLEL_JOBcounts from 0), and--spectakes them as one comma-separated list. The agents need the browsers and the system libraries Cypress needs.
With BUILDKITE=true, the run is named BUILDKITE_PIPELINE_SLUG #BUILDKITE_BUILD_NUMBER, with the
branch BUILDKITE_BRANCH, the commit BUILDKITE_COMMIT and the build URL BUILDKITE_BUILD_URL.
Fork pull requests
Section titled “Fork pull requests”Buildkite builds pull requests from forks only if the pipeline allows it (off by default). Those
builds run on your agents, and pipeline upload reads the fork’s own pipeline.yml, which also
picks the agents’ queue. The fork’s code runs in the job’s shell as the agent’s user, so on an agent
that can read the token from your secret store, the fork’s code can read it too: no hook or step
keeps it out.
Keep fork builds off, or keep them off those agents: their environment hook refuses a build of a
fork (BUILDKITE_PULL_REQUEST_REPO names it), and forks build only on a separate queue or cluster
whose agents cannot read the secret store:
# The environment hook of the agents that can read the token: no build of a fork runs there.if [ -n "$BUILDKITE_PULL_REQUEST_REPO" ]; then case "${BUILDKITE_PULL_REQUEST_REPO#*://}" in github.com/acme/shop.git) ;; *) echo "Refusing a build of a fork: $BUILDKITE_PULL_REQUEST_REPO" >&2 exit 1 ;; esacfiexport PROBARA_API_TOKEN="$(your-secrets-manager get probara-api-token)"BUILDKITE_PULL_REQUEST_REPO is empty outside pull requests, and has been an https:// URL since
2022 (a git:// one before), so the hook compares it without the scheme: put your repository in
place of github.com/acme/shop.git. When it does not match, your own pull requests fail on these
agents too, which the first one shows. On the agents that build forks, set PROBARA_ENABLED=false
in their environment hook: the fork’s tests run and npx cypress run reports nothing, and
run create and run close exit 0 without a token.
Related: Sharding and projects · Results file · Troubleshooting · Source: docs/ci