|
| 1 | +# CI/CD Integration |
| 2 | + |
| 3 | +Pacta is designed for CI/CD pipelines. This guide shows how to integrate architecture checks into your workflow. |
| 4 | + |
| 5 | +## Exit Codes |
| 6 | + |
| 7 | +| Code | Meaning | |
| 8 | +|------|---------| |
| 9 | +| `0` | Success, no violations (or no *new* violations with baseline) | |
| 10 | +| `1` | Violations found | |
| 11 | +| `2` | Engine error (config issue, parse error, etc.) | |
| 12 | + |
| 13 | +## GitHub Actions |
| 14 | + |
| 15 | +### Basic Check |
| 16 | + |
| 17 | +Run architecture validation on every pull request: |
| 18 | + |
| 19 | +```yaml |
| 20 | +name: Architecture Check |
| 21 | + |
| 22 | +on: |
| 23 | + pull_request: |
| 24 | + branches: [main] |
| 25 | + |
| 26 | +jobs: |
| 27 | + architecture: |
| 28 | + runs-on: ubuntu-latest |
| 29 | + steps: |
| 30 | + - uses: actions/checkout@v4 |
| 31 | + |
| 32 | + - uses: actions/setup-python@v5 |
| 33 | + with: |
| 34 | + python-version: '3.11' |
| 35 | + |
| 36 | + - name: Install Pacta |
| 37 | + run: pip install pacta |
| 38 | + |
| 39 | + - name: Check Architecture |
| 40 | + run: pacta scan . --model architecture.yml --rules rules.pacta.yml |
| 41 | +``` |
| 42 | +
|
| 43 | +### With Baseline (Recommended) |
| 44 | +
|
| 45 | +For existing projects with legacy violations, use baselines to only fail on *new* violations: |
| 46 | +
|
| 47 | +```yaml |
| 48 | +name: Architecture Check |
| 49 | + |
| 50 | +on: |
| 51 | + push: |
| 52 | + branches: [main] |
| 53 | + pull_request: |
| 54 | + branches: [main] |
| 55 | + |
| 56 | +jobs: |
| 57 | + architecture: |
| 58 | + runs-on: ubuntu-latest |
| 59 | + steps: |
| 60 | + - uses: actions/checkout@v4 |
| 61 | + |
| 62 | + - uses: actions/setup-python@v5 |
| 63 | + with: |
| 64 | + python-version: '3.11' |
| 65 | + |
| 66 | + - name: Install Pacta |
| 67 | + run: pip install pacta |
| 68 | + |
| 69 | + # On main branch: update baseline |
| 70 | + - name: Update Baseline |
| 71 | + if: github.ref == 'refs/heads/main' |
| 72 | + run: | |
| 73 | + pacta scan . \ |
| 74 | + --model architecture.yml \ |
| 75 | + --rules rules.pacta.yml \ |
| 76 | + --save-ref baseline |
| 77 | +
|
| 78 | + # On PR: check against baseline |
| 79 | + - name: Check Against Baseline |
| 80 | + if: github.event_name == 'pull_request' |
| 81 | + run: | |
| 82 | + pacta scan . \ |
| 83 | + --model architecture.yml \ |
| 84 | + --rules rules.pacta.yml \ |
| 85 | + --baseline baseline |
| 86 | +``` |
| 87 | +
|
| 88 | +!!! note "Persisting baselines" |
| 89 | + The baseline is stored in `.pacta/snapshots/`. Commit this directory to your repository, or use GitHub Actions cache/artifacts to persist it between runs. |
| 90 | + |
| 91 | +### JSON Output for PR Comments |
| 92 | + |
| 93 | +Generate JSON output and post results as a PR comment: |
| 94 | + |
| 95 | +```yaml |
| 96 | +name: Architecture Check |
| 97 | +
|
| 98 | +on: |
| 99 | + pull_request: |
| 100 | + branches: [main] |
| 101 | +
|
| 102 | +jobs: |
| 103 | + architecture: |
| 104 | + runs-on: ubuntu-latest |
| 105 | + steps: |
| 106 | + - uses: actions/checkout@v4 |
| 107 | +
|
| 108 | + - uses: actions/setup-python@v5 |
| 109 | + with: |
| 110 | + python-version: '3.11' |
| 111 | +
|
| 112 | + - name: Install Pacta |
| 113 | + run: pip install pacta |
| 114 | +
|
| 115 | + - name: Run Architecture Check |
| 116 | + id: pacta |
| 117 | + continue-on-error: true |
| 118 | + run: | |
| 119 | + pacta scan . \ |
| 120 | + --model architecture.yml \ |
| 121 | + --rules rules.pacta.yml \ |
| 122 | + --baseline baseline \ |
| 123 | + --format json > results.json |
| 124 | +
|
| 125 | + # Extract summary for PR comment |
| 126 | + VIOLATIONS=$(jq '.summary.total_violations' results.json) |
| 127 | + NEW=$(jq '.summary.new_violations // 0' results.json) |
| 128 | + echo "violations=$VIOLATIONS" >> $GITHUB_OUTPUT |
| 129 | + echo "new=$NEW" >> $GITHUB_OUTPUT |
| 130 | +
|
| 131 | + - name: Comment on PR |
| 132 | + uses: actions/github-script@v7 |
| 133 | + with: |
| 134 | + script: | |
| 135 | + const violations = ${{ steps.pacta.outputs.violations }}; |
| 136 | + const newViolations = ${{ steps.pacta.outputs.new }}; |
| 137 | +
|
| 138 | + let body; |
| 139 | + if (violations === 0) { |
| 140 | + body = '### Architecture Check Passed\n\nNo architectural violations found.'; |
| 141 | + } else if (newViolations === 0) { |
| 142 | + body = `### Architecture Check Passed\n\n${violations} existing violation(s), 0 new.`; |
| 143 | + } else { |
| 144 | + body = `### Architecture Check Failed\n\n**${newViolations} new violation(s)** introduced.\n\nRun \`pacta scan\` locally to see details.`; |
| 145 | + } |
| 146 | + |
| 147 | + github.rest.issues.createComment({ |
| 148 | + issue_number: context.issue.number, |
| 149 | + owner: context.repo.owner, |
| 150 | + repo: context.repo.repo, |
| 151 | + body: body |
| 152 | + }); |
| 153 | + |
| 154 | + - name: Fail on New Violations |
| 155 | + if: steps.pacta.outputs.new > 0 |
| 156 | + run: exit 1 |
| 157 | +``` |
| 158 | +
|
| 159 | +## GitLab CI |
| 160 | +
|
| 161 | +### Basic Check |
| 162 | +
|
| 163 | +```yaml |
| 164 | +architecture: |
| 165 | + stage: test |
| 166 | + image: python:3.11 |
| 167 | + script: |
| 168 | + - pip install pacta |
| 169 | + - pacta scan . --model architecture.yml --rules rules.pacta.yml |
| 170 | +``` |
| 171 | +
|
| 172 | +### With Baseline |
| 173 | +
|
| 174 | +```yaml |
| 175 | +stages: |
| 176 | + - test |
| 177 | + |
| 178 | +variables: |
| 179 | + PACTA_BASELINE: baseline |
| 180 | + |
| 181 | +architecture: |
| 182 | + stage: test |
| 183 | + image: python:3.11 |
| 184 | + script: |
| 185 | + - pip install pacta |
| 186 | + - | |
| 187 | + if [ "$CI_COMMIT_BRANCH" = "main" ]; then |
| 188 | + # Update baseline on main |
| 189 | + pacta scan . --model architecture.yml --rules rules.pacta.yml --save-ref $PACTA_BASELINE |
| 190 | + else |
| 191 | + # Check against baseline on feature branches |
| 192 | + pacta scan . --model architecture.yml --rules rules.pacta.yml --baseline $PACTA_BASELINE |
| 193 | + fi |
| 194 | + cache: |
| 195 | + paths: |
| 196 | + - .pacta/ |
| 197 | +``` |
| 198 | +
|
| 199 | +## Pre-commit Hook |
| 200 | +
|
| 201 | +Run Pacta as a pre-commit hook to catch violations before they're committed: |
| 202 | +
|
| 203 | +```yaml |
| 204 | +# .pre-commit-config.yaml |
| 205 | +repos: |
| 206 | + - repo: local |
| 207 | + hooks: |
| 208 | + - id: pacta |
| 209 | + name: Architecture Check |
| 210 | + entry: pacta scan . --model architecture.yml --rules rules.pacta.yml -q |
| 211 | + language: system |
| 212 | + pass_filenames: false |
| 213 | + types: [python] |
| 214 | +``` |
| 215 | +
|
| 216 | +!!! tip "Performance" |
| 217 | + Use `-q` (quiet mode) for faster output in hooks. Consider running against baseline for large codebases with legacy violations. |
| 218 | + |
| 219 | +## Makefile Integration |
| 220 | + |
| 221 | +Add Pacta to your Makefile for easy local runs: |
| 222 | + |
| 223 | +```makefile |
| 224 | +.PHONY: arch arch-baseline |
| 225 | +
|
| 226 | +# Run architecture check |
| 227 | +arch: |
| 228 | + pacta scan . --model architecture.yml --rules rules.pacta.yml |
| 229 | +
|
| 230 | +# Update baseline |
| 231 | +arch-baseline: |
| 232 | + pacta scan . --model architecture.yml --rules rules.pacta.yml --save-ref baseline |
| 233 | +
|
| 234 | +# Check against baseline (CI mode) |
| 235 | +arch-ci: |
| 236 | + pacta scan . --model architecture.yml --rules rules.pacta.yml --baseline baseline |
| 237 | +``` |
| 238 | + |
| 239 | +## JSON Schema |
| 240 | + |
| 241 | +The `--format json` output follows this structure: |
| 242 | + |
| 243 | +```json |
| 244 | +{ |
| 245 | + "run_info": { |
| 246 | + "tool_version": "0.0.2", |
| 247 | + "timestamp": "2025-01-22T12:00:00+00:00", |
| 248 | + "commit": "abc1234", |
| 249 | + "branch": "main" |
| 250 | + }, |
| 251 | + "summary": { |
| 252 | + "total_violations": 5, |
| 253 | + "by_severity": { |
| 254 | + "error": 3, |
| 255 | + "warning": 2 |
| 256 | + }, |
| 257 | + "new_violations": 1, |
| 258 | + "existing_violations": 4, |
| 259 | + "fixed_violations": 0 |
| 260 | + }, |
| 261 | + "violations": [ |
| 262 | + { |
| 263 | + "rule_id": "no_domain_to_infra", |
| 264 | + "rule_name": "Domain cannot depend on Infrastructure", |
| 265 | + "severity": "error", |
| 266 | + "status": "new", |
| 267 | + "location": { |
| 268 | + "file": "src/domain/user.py", |
| 269 | + "line": 3, |
| 270 | + "column": 1 |
| 271 | + }, |
| 272 | + "message": "Domain layer must not import from Infrastructure", |
| 273 | + "suggestion": "Use dependency injection...", |
| 274 | + "explanation": "\"myapp.domain.UserService\" in domain layer imports \"myapp.infra.Database\" in infra layer" |
| 275 | + } |
| 276 | + ] |
| 277 | +} |
| 278 | +``` |
| 279 | + |
| 280 | +Use `jq` to extract specific fields: |
| 281 | + |
| 282 | +```bash |
| 283 | +# Get new violation count |
| 284 | +pacta scan . --format json | jq '.summary.new_violations' |
| 285 | +
|
| 286 | +# List files with violations |
| 287 | +pacta scan . --format json | jq -r '.violations[].location.file' | sort -u |
| 288 | +
|
| 289 | +# Filter only errors |
| 290 | +pacta scan . --format json | jq '.violations | map(select(.severity == "error"))' |
| 291 | +``` |
0 commit comments