CI/CD Integration

Complete guide for integrating intake into continuous integration pipelines. Includes examples for GitHub Actions, GitLab CI, Jenkins, and Azure DevOps.


General strategy

What to run in CI

ActionWhenCommand
Verify specOn each PR / pushintake verify specs/<spec>/ -p . -f junit
Export specWhen merging to mainintake export specs/<spec>/ -f <format> -o .
Detect driftScheduled (weekly)Compare hashes from spec.lock.yaml with sources
Doctor checkScheduled / manualintake doctor
FeedbackWhen verify failsintake feedback specs/<spec>/ -p .

Exit codes

CodeMeaningCI Action
0All required checks passedJob passes
1At least one required check failedJob fails
2Execution errorJob fails (investigate)

GitHub Actions

intake includes an official GitHub Action (action/action.yml) that simplifies integration. The action installs intake, runs verification, generates reports, and uploads artifacts automatically.

name: Verify Spec
on: [push, pull_request]

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Verify spec compliance
        uses: Diego303/intake-cli/action@main
        with:
          spec-dir: specs/mi-feature/

Inputs

InputDefaultDescription
spec-dir(required)Path to the spec directory
project-dir.Project directory to verify against
report-formatjunitReport format: terminal, json, junit
report-outputintake-verify-report.xmlReport file path
tags""Tags to filter checks (comma-separated)
fail-fastfalseStop on the first failure
python-version3.12Python version
intake-versionlatestintake version (e.g., >=0.4.0)

Outputs

OutputDescription
resultResult: pass, fail, or error
total-checksTotal checks executed
passed-checksChecks that passed
failed-checksChecks that failed
report-pathPath to the generated report

Advanced example with outputs

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Verify spec compliance
        id: verify
        uses: Diego303/intake-cli/action@main
        with:
          spec-dir: specs/mi-feature/
          tags: "api,security"
          fail-fast: "true"

      - name: Comment on PR
        if: failure()
        run: |
          echo "Spec verification failed: ${{ steps.verify.outputs.failed-checks }} checks failed"

Manual verification in PR

If you prefer to configure the steps manually without the official action:

name: Verify Spec Compliance
on: [push, pull_request]

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Cache pip packages
        uses: actions/cache@v4
        with:
          path: ~/.cache/pip
          key: ${{ runner.os }}-pip-intake

      - name: Install intake
        run: pip install intake-ai-cli

      - name: Run acceptance checks
        run: intake verify specs/mi-feature/ -p . -f junit > test-results.xml

      - name: Upload test results
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: spec-verification
          path: test-results.xml

      - name: Publish test results
        uses: dorny/test-reporter@v1
        if: always()
        with:
          name: Spec Compliance
          path: test-results.xml
          reporter: java-junit

Complete workflow: verify + export

name: Spec Pipeline
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install intake-ai-cli
      - run: intake verify specs/mi-feature/ -p . -f junit > test-results.xml
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: test-results
          path: test-results.xml

  export:
    needs: verify
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install intake-ai-cli
      - run: intake export specs/mi-feature/ -f claude-code -o .
      - uses: actions/upload-artifact@v4
        with:
          name: exported-spec
          path: |
            CLAUDE.md
            .intake/

Spec drift detection (scheduled)

name: Spec Drift Detection
on:
  schedule:
    - cron: "0 9 * * 1"  # Monday at 9am

jobs:
  check-drift:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install intake-ai-cli

      - name: Check for stale specs
        id: drift
        run: |
          OUTPUT=$(intake show specs/mi-feature/ 2>&1)
          if echo "$OUTPUT" | grep -qi "stale\|changed"; then
            echo "drift=true" >> $GITHUB_OUTPUT
          else
            echo "drift=false" >> $GITHUB_OUTPUT
          fi

      - name: Create issue if drift detected
        if: steps.drift.outputs.drift == 'true'
        run: |
          gh issue create \
            --title "Spec drift detected: mi-feature" \
            --body "The spec sources have changed. Regenerate with: \`intake add specs/mi-feature/ --regenerate\`" \
            --label "spec-drift"
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Multiple specs in parallel

jobs:
  verify:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        spec: [api-auth, api-payments, frontend-dashboard]
      fail-fast: false
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install intake-ai-cli
      - run: intake verify specs/${{ matrix.spec }}/ -p . -f junit > results-${{ matrix.spec }}.xml
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: results-${{ matrix.spec }}
          path: results-${{ matrix.spec }}.xml

GitLab CI

Basic pipeline

# .gitlab-ci.yml
stages:
  - verify

verify-spec:
  stage: verify
  image: python:3.12-slim
  before_script:
    - pip install intake-ai-cli
  script:
    - intake verify specs/mi-feature/ -p . -f junit > test-results.xml
  artifacts:
    when: always
    reports:
      junit: test-results.xml

GitLab automatically displays JUnit results in the Merge Request widget.

Complete pipeline with cache

stages:
  - verify
  - export

.intake-base:
  image: python:3.12-slim
  cache:
    key: intake-pip
    paths:
      - .cache/pip
  variables:
    PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
  before_script:
    - pip install intake-ai-cli

verify-spec:
  extends: .intake-base
  stage: verify
  script:
    - intake verify specs/mi-feature/ -p . -f junit > test-results.xml
  artifacts:
    when: always
    reports:
      junit: test-results.xml

export-spec:
  extends: .intake-base
  stage: export
  only:
    - main
  script:
    - intake export specs/mi-feature/ -f claude-code -o .
  artifacts:
    paths:
      - CLAUDE.md
      - .intake/

Jenkins

Declarative Jenkinsfile

pipeline {
    agent {
        docker {
            image 'python:3.12-slim'
        }
    }

    environment {
        ANTHROPIC_API_KEY = credentials('anthropic-api-key')
    }

    stages {
        stage('Install') {
            steps {
                sh 'pip install intake-ai-cli'
            }
        }

        stage('Verify') {
            steps {
                sh 'intake verify specs/mi-feature/ -p . -f junit > test-results.xml'
            }
            post {
                always {
                    junit 'test-results.xml'
                }
            }
        }

        stage('Export') {
            when {
                branch 'main'
            }
            steps {
                sh 'intake export specs/mi-feature/ -f claude-code -o .'
                archiveArtifacts artifacts: 'CLAUDE.md,.intake/**'
            }
        }
    }
}

Jenkinsfile with multiple specs

pipeline {
    agent any
    stages {
        stage('Verify All Specs') {
            parallel {
                stage('API Auth') {
                    steps {
                        sh 'intake verify specs/api-auth/ -p . -f junit > results-auth.xml'
                    }
                }
                stage('API Payments') {
                    steps {
                        sh 'intake verify specs/api-payments/ -p . -f junit > results-payments.xml'
                    }
                }
            }
            post {
                always {
                    junit 'results-*.xml'
                }
            }
        }
    }
}

Azure DevOps

azure-pipelines.yml

trigger:
  - main

pool:
  vmImage: 'ubuntu-latest'

steps:
  - task: UsePythonVersion@0
    inputs:
      versionSpec: '3.12'

  - script: pip install intake-ai-cli
    displayName: 'Install intake'

  - script: intake verify specs/mi-feature/ -p . -f junit > test-results.xml
    displayName: 'Verify spec compliance'

  - task: PublishTestResults@2
    condition: always()
    inputs:
      testResultsFormat: 'JUnit'
      testResultsFiles: 'test-results.xml'
      testRunTitle: 'Spec Compliance'

Report formats for CI

JUnit XML

The most compatible format with CI. All systems support it:

intake verify specs/mi-feature/ -p . -f junit > test-results.xml
PlatformHow it’s consumed
GitHub Actionsdorny/test-reporter action or actions/upload-artifact
GitLab CIartifacts.reports.junit (appears in MR widget)
Jenkinsjunit post step (appears in dashboard)
Azure DevOpsPublishTestResults@2 task

JSON

For custom integrations:

intake verify specs/mi-feature/ -p . -f json > results.json

Extract metrics with jq:

# Total checks
jq '.total_checks' results.json

# Only names of failed checks
jq '.results[] | select(.passed == false) | .name' results.json

# Summary: passed/failed
jq '{passed: .passed, failed: .failed, all_ok: .all_required_passed}' results.json

Pre-commit hooks

See Deployment > Pre-commit hooks for detailed configuration.

Quick summary:

# .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: intake-verify
        name: Verify spec compliance
        entry: intake verify specs/mi-feature/ -p . --fail-fast -t structure
        language: system
        pass_filenames: false
        files: ^src/

Spec drift detection

What it is

“Spec drift” occurs when requirement sources change but the spec is not regenerated. The spec.lock.yaml stores SHA-256 hashes of the original sources, allowing changes to be detected.

When to check

  • Weekly (cron job): detect drift from sources that change without regeneration
  • In PR: if a PR modifies a source listed in spec.lock.yaml, alert that the spec may be outdated

Implementation

The general pattern:

# 1. intake show detects staleness automatically
intake show specs/mi-feature/

# 2. If sources changed, regenerate
intake add specs/mi-feature/ --regenerate

# 3. Review changes
intake diff specs/mi-feature-old/ specs/mi-feature/

See the GitHub Actions and GitLab CI examples above for CI implementations.


Notifications

Slack (webhook)

# GitHub Actions: notify Slack if verify fails
- name: Notify Slack on failure
  if: failure()
  run: |
    curl -X POST "${{ secrets.SLACK_WEBHOOK }}" \
      -H "Content-Type: application/json" \
      -d '{
        "text": "Spec verification failed for mi-feature",
        "blocks": [
          {
            "type": "section",
            "text": {
              "type": "mrkdwn",
              "text": "*Spec Verification Failed*\nSpec: `mi-feature`\nBranch: `${{ github.ref_name }}`\nPR: ${{ github.event.pull_request.html_url }}"
            }
          }
        ]
      }'

Microsoft Teams (webhook)

- name: Notify Teams on failure
  if: failure()
  run: |
    curl -X POST "${{ secrets.TEAMS_WEBHOOK }}" \
      -H "Content-Type: application/json" \
      -d '{
        "title": "Spec Verification Failed",
        "text": "The mi-feature spec did not pass the acceptance checks on branch ${{ github.ref_name }}"
      }'

Tips

Dependency caching

Caching pip reduces ~30 seconds per execution:

# GitHub Actions
- uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: ${{ runner.os }}-pip-intake-${{ hashFiles('**/requirements*.txt') }}

# GitLab CI
cache:
  key: intake-pip
  paths:
    - .cache/pip

verify.sh without installing intake

The generic exporter generates a standalone verify.sh. In CI where you don’t want to install intake:

# Once: generate verify.sh and commit it
intake export specs/mi-feature/ -f generic -o output/
git add output/verify.sh
git commit -m "Add standalone verify script"

# In CI: run directly (without intake)
chmod +x output/verify.sh
./output/verify.sh .

Tradeoff: less flexible than intake verify (no tags, no JUnit), but zero dependencies.

Structure-only verification

For fast checks in pre-commit, run only structure checks (no tests):

intake verify specs/mi-feature/ -p . -t structure --fail-fast