Skip to content
AI CraftsmanSUPERPOWERS

CI Integration

Zero-drift CI integration for GitHub Actions, GitLab CI, Bitbucket Pipelines, and Jenkins.

Last updated: Edit on GitHub

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.