CI Integration
Zero-drift CI integration for GitHub Actions, GitLab CI, Bitbucket Pipelines, and Jenkins.
Zero-Drift Principle
The core promise: the same rules that run in your hooks run in CI. ci/craftsman-ci.sh sources the same pack validators and the same rules engine as the hooks, and resolves severity per file, so a directory’s .craft-rules.yml applies in both. The plugin’s own test suite fails when the two front-ends disagree on a severity.
Generate CI Config
On Claude Code, for GitHub Actions:
/craftsman:ci export
It copies the plugin’s template to .github/workflows/craftsman-quality-gate.yml, and asks before overwriting an existing file. For any provider, the CLI writes the template into the current repository (for Bitbucket, only when no bitbucket-pipelines.yml exists yet):
bash <plugin-root>/bin/craftsman-ci init --provider gitlab # github, gitlab, bitbucket or jenkins
/craftsman:ci status reports what is in place: the workflow file, whether ci/craftsman-ci.sh is present and executable, the .craft-config.yml it reads, and the stack it detects.
The command every template runs
bash ci/craftsman-ci.sh ci --provider <github|gitlab|bitbucket|jenkins> [--changed-only [--base REF]]
On a pull or merge request the templates pass --changed-only: only the files that differ from the target branch are validated, so a one-file change does not pay for a full scan or report every pre-existing finding. A push to the default branch still scans everything. --changed-only needs history to diff against, which is why each template fetches the full depth.
Exit codes are the same on every provider: 0 clean, 1 warnings only, 2 violations found.
GitHub Actions
Template: ci/templates/craftsman-quality-gate.yml. The essentials:
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
pull-requests: write
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
# PHP and Node setup and installs run only when composer.json or package.json exists
- name: Run craftsman-ci quality gate
env:
GH_TOKEN: ${{ github.token }}
GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }}
CRAFTSMAN_SCOPE: ${{ github.event_name == 'pull_request' && '--changed-only' || '' }}
run: |
chmod +x ci/craftsman-ci.sh
bash ci/craftsman-ci.sh ci --provider github $CRAFTSMAN_SCOPE
Actions are pinned by commit SHA, and the job holds least privilege. The adapter writes inline annotations and a pull request comment.
GitLab CI
Template: ci/templates/.gitlab-ci.craftsman.yml, included from your .gitlab-ci.yml:
craftsman-quality-gate:
stage: test
image: python:3.12-slim
variables:
GIT_DEPTH: 0
rules:
- if: $CI_MERGE_REQUEST_IID
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
script:
- bash ci/craftsman-ci.sh ci --provider gitlab $SCOPE # --changed-only on a merge request
artifacts:
reports:
codequality: gl-code-quality-report.json
when: always
Findings appear in the merge request’s code-quality report. The merge request note needs a GITLAB_TOKEN (a project access token or a PAT): CI_JOB_TOKEN is read-only on the notes API. Without a token you still get the verdict and the exit code.
Bitbucket Pipelines
Template: ci/templates/bitbucket-pipelines.craftsman.yml, to merge into your bitbucket-pipelines.yml:
clone:
depth: full
pipelines:
pull-requests:
'**':
- step:
name: Craftsman Quality Gate
image: python:3.12-slim
script:
- git fetch origin "+refs/heads/$BITBUCKET_PR_DESTINATION_BRANCH:refs/remotes/origin/$BITBUCKET_PR_DESTINATION_BRANCH"
- bash ci/craftsman-ci.sh ci --provider bitbucket --changed-only
The adapter publishes a Code Insights report on the build, which needs no credential inside Pipelines. A pull request comment is optional and needs BITBUCKET_TOKEN, a Bearer-capable token (an app password will not authenticate).
Jenkins
Template: ci/templates/Jenkinsfile.craftsman. The gate stage:
int status = sh(
script: 'bash ci/craftsman-ci.sh ci --provider jenkins ${CHANGE_TARGET:+--changed-only}',
returnStatus: true
)
if (fileExists('craftsman-checkstyle.xml')) {
recordIssues(tools: [checkStyle(pattern: 'craftsman-checkstyle.xml', name: 'Craftsman')],
enabledForFailure: true)
}
if (status == 2) { error 'Craftsman quality gate failed.' }
if (status == 1) { unstable 'Craftsman reported warnings.' }
The Jenkins adapter writes craftsman-checkstyle.xml, which the Warnings Next Generation plugin reads natively: each finding lands on its file and line, with the rule ID as its category. It also writes craftsman-comment.md and prints the findings to the console, so a build without the plugin still gets the verdict.
CI Adapter Architecture
Every provider goes through the same lifecycle, behind one interface:
craftsman-ci.sh ci --provider <name>
↓
scan: the pack validators and the rules engine the hooks use
↓
adapter: detect → run → annotate → comment → exit
├── github.sh inline annotations, pull request comment
├── gitlab.sh code-quality report, merge request note
├── bitbucket.sh Code Insights report, optional comment
├── jenkins.sh Checkstyle report for Warnings Next Generation
└── generic.sh the fallback for anything else
Rules must match
If your project .craft-config.yml overrides global rules, commit it to your repository so CI uses the same configuration. Never rely on ~/.claude/.craft-config.yml in CI: it’s a local file.