CI/CD
Ward is a single native binary with deterministic exit codes, so any CI system can use it as a quality gate. The common integration flow is:
- Check out the repository.
- Install the runtime and standalone tools required by your project and configured gates.
- Install Ward with the verified installer.
- Run the selected profiles and preserve the command’s exit status.
- Upload the JSON report as a diagnostic artifact.
The native Ward binary does not require Java, but Java, Maven, Gradle, or other tools may be needed
by individual gates. Ward does not install those tools. A missing executable is reported as
UNAVAILABLE and makes the check unsuccessful.
GitHub Actions
Section titled “GitHub Actions”ward init --ci github writes a Ward-owned .github/workflows/ward.yml that installs
the tools your selected gates need, runs the ci profile, and uploads SARIF to code scanning. See
ward init. To write the workflow yourself, start from this one:
name: Quality gate
on: pull_request: push:
jobs: ward: runs-on: ubuntu-24.04 steps: # Full history, so gitleaks scans every commit. - uses: actions/checkout@v5 with: fetch-depth: 0
# Use the runtime required by your project. This is not required by # the Ward native binary itself. - uses: actions/setup-java@v5 with: distribution: temurin java-version: '21' cache: maven
- name: Install Ward env: WARD_VERSION: v0.2.0 run: | curl -fsSL https://raw.githubusercontent.com/RicardoRB/ward-ai/main/install.sh | sh echo "$HOME/.local/bin" >> "$GITHUB_PATH"
# Restore and save separately: actions/cache saves only when the job succeeds. - name: Restore Trivy cache uses: actions/cache/restore@v5 with: path: ~/.cache/trivy key: trivy-${{ github.run_id }} restore-keys: trivy-
- name: Run quality gates run: ward check --profile ci --format json > ward-report.json
- name: Save Trivy cache if: always() uses: actions/cache/save@v5 with: path: ~/.cache/trivy key: trivy-${{ github.run_id }}
- name: Upload Ward report if: always() uses: actions/upload-artifact@v6 with: name: ward-report path: ward-report.jsonGitHub’s required status checks can block merges until the ward job passes. Use the runtime setup
and cache action appropriate for your project; the example uses Maven only as an illustration.
Node projects
Section titled “Node projects”For a TypeScript or JavaScript project, the workflow ward init --ci github writes sets up the runtime instead of
Java, then installs the dependencies from the lock file before running Ward, so the locally
installed tools the gates call are present:
| Package manager | Setup | Install |
|---|---|---|
| npm | actions/setup-node@v5 with cache: npm | npm ci |
| pnpm | pnpm/action-setup@v4, then actions/setup-node@v5 with cache: pnpm | pnpm install --frozen-lockfile |
| Yarn 2 or later | actions/setup-node@v5 with cache: yarn | yarn install --immutable |
Yarn 1, or no packageManager | actions/setup-node@v5 with cache: yarn | yarn install --frozen-lockfile |
| Bun | oven-sh/setup-bun@v2 | bun install --frozen-lockfile |
node-version is the major version Ward detects from .nvmrc, .node-version, or
engines.node, or "24" when none sets one. The Yarn row follows the packageManager field of
package.json, such as "yarn@4.5.0". Commit the lock file: a frozen install fails without it.
Projects in a subdirectory
Section titled “Projects in a subdirectory”GitHub only runs workflows from .github/workflows/ at the repository root. When the project sits
below it, for example apps/api in a monorepo, ward init --ci github writes the workflow there
and names it after the project path, such as .github/workflows/ward-apps-api.yml, so several
Ward projects never share one file. The workflow:
- runs every shell step in the project directory through
defaults.run.working-directory; - triggers only when a pull request or push to
mainchanges a file under the project or the workflow itself; - uploads
apps/api/ward.sarifunder its own code-scanning category (ward-apps-api), so the results of different projects do not replace each other. SARIF locations are relative to the repository root, so annotations land onapps/api/...files.
A project whose root is the repository root keeps ward.yml exactly as before.
Code scanning annotations with SARIF
Section titled “Code scanning annotations with SARIF”To see findings as pull-request annotations and code-scanning alerts, write a SARIF report and upload
it. --output keeps the readable text report in the job log:
permissions: contents: read security-events: write steps: # ... checkout, runtime, and Ward installation as above ... - name: Run quality gates run: ward check --profile ci --format sarif --output ward.sarif
- name: Upload SARIF if: always() uses: github/codeql-action/upload-sarif@v4 with: sarif_file: ward.sarif category: wardFindings carry file locations for checkstyle, pmd, gitleaks, compile, and format; other
failed gates appear as one alert without a location.
GitLab CI
Section titled “GitLab CI”GitLab checks out the repository before the job script. Use a merge-request pipeline rule when Ward should run for pull requests as well as branch pushes:
stages: - quality
ward: image: ubuntu:24.04 stage: quality rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - if: '$CI_COMMIT_BRANCH' before_script: - apt-get update -qq - apt-get install -y -qq curl # Install the JDK, build tool, and standalone gate tools required by the project here. - export WARD_VERSION="v0.2.0" - curl -fsSL https://raw.githubusercontent.com/RicardoRB/ward-ai/main/install.sh | sh - export PATH="$HOME/.local/bin:$PATH" script: - ward check --profile default,ci --format json > ward-report.json artifacts: when: always paths: - ward-report.jsonConfigure the job as a required merge-request status in the project’s approval or branch protection settings. Prefer a project image that already contains the required runtime and tools rather than installing them in every job.
Jenkins
Section titled “Jenkins”This Declarative Pipeline archives the report even when the shell step fails. The sh step’s non-zero
status still fails the build:
pipeline { agent { label 'linux' }
stages { stage('Checkout') { steps { checkout scm } }
stage('Ward') { steps { sh ''' set -eu export WARD_VERSION="v0.2.0" curl -fsSL https://raw.githubusercontent.com/RicardoRB/ward-ai/main/install.sh | sh export PATH="$HOME/.local/bin:$PATH" ward check --profile default,ci --format json > ward-report.json ''' } } }
post { always { archiveArtifacts artifacts: 'ward-report.json', allowEmptyArchive: true } }}Provision the JDK, build tool, and standalone gate executables on the agent or in its container image.
Use Jenkins credentials and environment bindings for private registry or analysis-service credentials;
do not put secrets in ward.toml or the pipeline source.
CircleCI
Section titled “CircleCI”version: 2.1
jobs: ward: machine: image: ubuntu-2404:current steps: - checkout - run: name: Install Ward command: | set -eu export WARD_VERSION="v0.2.0" curl -fsSL https://raw.githubusercontent.com/RicardoRB/ward-ai/main/install.sh | sh echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$BASH_ENV" - run: name: Run quality gates command: ward check --profile default,ci --format json > ward-report.json - store_artifacts: path: ward-report.json destination: ward-report.json
workflows: quality: jobs: - wardCircleCI stores the report as a build artifact. Add a language-specific executor or setup step before the Ward command when the configured gates need a JDK, build tool, or additional executable.
Azure Pipelines
Section titled “Azure Pipelines”trigger: - main
pr: - main
pool: vmImage: ubuntu-24.04
steps: - checkout: self
# Add the runtime and standalone tools required by the project before this step. - bash: | set -eu export WARD_VERSION="v0.2.0" curl -fsSL https://raw.githubusercontent.com/RicardoRB/ward-ai/main/install.sh | sh export PATH="$HOME/.local/bin:$PATH" ward check --profile default,ci --format json > ward-report.json displayName: Run Ward quality gates
- task: PublishPipelineArtifact@1 condition: always() inputs: targetPath: ward-report.json artifact: ward-report publishLocation: pipelineUse variable groups or secret variables for credentials needed by networked gates. The publication task
uses always() so the report remains available when the preceding Bash task fails.
Bitbucket Pipelines
Section titled “Bitbucket Pipelines”image: ubuntu:24.04
pipelines: pull-requests: '**': - step: name: Ward quality gate script: - apt-get update -qq - apt-get install -y -qq curl # Install the runtime and standalone gate tools required by the project here. - export WARD_VERSION="v0.2.0" - curl -fsSL https://raw.githubusercontent.com/RicardoRB/ward-ai/main/install.sh | sh - export PATH="$HOME/.local/bin:$PATH" - ward check --profile default,ci --format json > ward-report.json artifacts: - ward-report.jsonBitbucket checks out the repository before running the step and retains declared artifacts for the
build. Add a branches pipeline for pushes when quality gates should run outside pull requests.
Shared CI behavior
Section titled “Shared CI behavior”- Profiles: init-generated configurations put every selected gate in
ci; useward check --profile cifor the complete CI tier. The fastdefaultprofile is intended for local checks, whilepre-pushis the cumulative local tier. Existing hand-authored configurations may still select multiple profiles, such asdefault,ci, when needed. - Gate evaluation: all selected gates run and the job gets one verdict. Add
--fail-fastto stop at the first failure. - Exit codes:
0means all selected gates passed,1means a gate failed, and2means Ward could not run, for example because of invalid configuration. Non-zero exits fail the CI step. - Reports:
--format jsonwrites only the JSON report to stdout, so it can be redirected toward-report.json. Diagnostics go to stderr and remain visible in logs. Add--output <path>to write the report to a file and keep the text report in the job log. - Version pinning: set
WARD_VERSIONto a release tag such asv0.2.0for reproducible builds. Leaving it unset installs the latest release. - Working directories: run Ward from the repository root, or configure the provider’s working
directory so
ward.tomland the project files resolve correctly. - Caches: cache project dependencies using the provider’s normal cache mechanism; do not cache the Ward binary as a substitute for checksum verification.
- Secrets: pass credentials through the provider’s secret store or environment variables. Never
commit them to
ward.tomlor a pipeline file. - Artifacts: retain the JSON report after failures where the provider supports an always/post-failure condition. It is diagnostic output; the Ward process exit code remains the job verdict.
- SARIF:
--format sarifwrites a SARIF 2.1.0 log for code-scanning tools on any provider that accepts SARIF, such as GitHub code scanning.
Init does not generate a provider-specific CI workflow; add the command above to the CI system of your choice.
Common first-run failures
Section titled “Common first-run failures”The first ward check --profile ci in a new pipeline often fails for setup reasons rather than
code findings:
| Symptom | Cause | Fix |
|---|---|---|
| gitleaks reports nothing on an old leak, or scans one commit | The checkout is shallow | Check out with full history (fetch-depth: 0 on GitHub Actions) |
| Every run downloads the Trivy or Dependency-Check database again | The cache is saved only when the job succeeds, and a gate failed | Restore and save the cache in separate steps and run the save step on failure (if: always()) |
trivy fails with 429 Too Many Requests from repo.maven.apache.org | Trivy downloads POMs itself and Maven Central rate-limits shared runners | Run Trivy offline after the build-tool gates; see Trivy offline scans |
dependency-check runs for over an hour, or fails with 401 Unauthorized from OSS Index | No NVD_API_KEY, or OSS Index is enabled without credentials | Set NVD_API_KEY and keep OSS Index off unless you have credentials (details), or rely on trivy, which needs no key |
A gate reports UNAVAILABLE | Its tool is not installed on the runner | Install the tool in an earlier step; Ward never installs tools |
Install Ward with the verified installer rather than building it from source in CI: the native binary needs no JDK and starts faster.
Trivy offline scans
Section titled “Trivy offline scans”When init configures trivy after a Maven or Gradle gate, it generates
trivy fs --offline-scan --exit-code 1 .. Without --offline-scan, Trivy downloads POM files from
Maven Central to resolve the dependency tree, and Maven Central answers shared CI runners with
429 Too Many Requests. The earlier build-tool gates have already filled the local Maven repository,
so Trivy resolves dependencies from it instead. A trivy gate with no earlier build gate stays online.
The limit: offline resolution sees only what the build already downloaded. If Trivy and Maven ever resolve the dependency tree differently, an offline scan misses the difference without failing.
Offline scanning covers only dependency metadata. Trivy still downloads its vulnerability database,
so cache ~/.cache/trivy separately from the build dependency cache. Save it even when a gate fails,
or a failing run never stores it:
- name: Restore Trivy cache uses: actions/cache/restore@v5 with: path: ~/.cache/trivy key: trivy-${{ github.run_id }} restore-keys: trivy-
# ... install tools and Ward, then run the gates ...
- name: Save Trivy cache if: always() uses: actions/cache/save@v5 with: path: ~/.cache/trivy key: trivy-${{ github.run_id }}Security
Section titled “Security”Do not run Ward on untrusted pull requests, such as those from forks, without isolation:
ward.toml is executable trusted code. Use a locked-down runner and do not expose write-capable
secrets to untrusted jobs. See security.
OpenSpec provenance enforcement
Section titled “OpenSpec provenance enforcement”The optional pull-request integration must use a full checkout, fetch the pull-request base ref, install
the tested @fission-ai/openspec@1.14.1, and run:
ward spec enforce --base "origin/$BASE_REF"Configure that job as a required status check in repository settings. Add a CODEOWNERS entry using
your own placeholders, for example openspec/** @REPLACE_WITH_TEAM; Ward does not create or approve
owners. Enable required code-owner review separately. Git provenance and archive association establish
deterministic provenance only; neither proves a human approved the change.
The optional local pre-commit integration writes only Ward-owned files. It preserves a user-owned
.git/hooks/pre-commit and reports a conflict with manual chaining guidance rather than overwriting it.
Refresh and removal affect only files carrying Ward ownership markers. /opsx:verify is not part of
the blocking hook or CI command.