Skip to content

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:

  1. Check out the repository.
  2. Install the runtime and standalone tools required by your project and configured gates.
  3. Install Ward with the verified installer.
  4. Run the selected profiles and preserve the command’s exit status.
  5. 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.

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.json

GitHub’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.

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 managerSetupInstall
npmactions/setup-node@v5 with cache: npmnpm ci
pnpmpnpm/action-setup@v4, then actions/setup-node@v5 with cache: pnpmpnpm install --frozen-lockfile
Yarn 2 or lateractions/setup-node@v5 with cache: yarnyarn install --immutable
Yarn 1, or no packageManageractions/setup-node@v5 with cache: yarnyarn install --frozen-lockfile
Bunoven-sh/setup-bun@v2bun 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.

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 main changes a file under the project or the workflow itself;
  • uploads apps/api/ward.sarif under 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 on apps/api/... files.

A project whose root is the repository root keeps ward.yml exactly as before.

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: ward

Findings carry file locations for checkstyle, pmd, gitleaks, compile, and format; other failed gates appear as one alert without a location.

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.json

Configure 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.

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.

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:
- ward

CircleCI 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.

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: pipeline

Use 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.

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.json

Bitbucket 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.

  • Profiles: init-generated configurations put every selected gate in ci; use ward check --profile ci for the complete CI tier. The fast default profile is intended for local checks, while pre-push is the cumulative local tier. Existing hand-authored configurations may still select multiple profiles, such as default,ci, when needed.
  • Gate evaluation: all selected gates run and the job gets one verdict. Add --fail-fast to stop at the first failure.
  • Exit codes: 0 means all selected gates passed, 1 means a gate failed, and 2 means Ward could not run, for example because of invalid configuration. Non-zero exits fail the CI step.
  • Reports: --format json writes only the JSON report to stdout, so it can be redirected to ward-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_VERSION to a release tag such as v0.2.0 for 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.toml and 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.toml or 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 sarif writes 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.

The first ward check --profile ci in a new pipeline often fails for setup reasons rather than code findings:

SymptomCauseFix
gitleaks reports nothing on an old leak, or scans one commitThe checkout is shallowCheck out with full history (fetch-depth: 0 on GitHub Actions)
Every run downloads the Trivy or Dependency-Check database againThe cache is saved only when the job succeeds, and a gate failedRestore 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.orgTrivy downloads POMs itself and Maven Central rate-limits shared runnersRun 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 IndexNo NVD_API_KEY, or OSS Index is enabled without credentialsSet NVD_API_KEY and keep OSS Index off unless you have credentials (details), or rely on trivy, which needs no key
A gate reports UNAVAILABLEIts tool is not installed on the runnerInstall 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.

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 }}

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.

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:

Terminal window
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.