Repository navigation
251 lines (221 loc) · 10.4 KB
/
Copy pathci.yml
File metadata and controls
251 lines (221 loc) · 10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# CI Pipeline - Claude Workflow Engine
# Triggers on push to main, pull_request targeting main, and manual workflow_dispatch.
# Step ordering DAG:
# check-enabled -> [secrets_check || lint] -> install -> unit_tests
# -> integration_tests -> coverage -> upload-artifact
#
# CONFIGURABLE: Control via GitHub repo variables (Settings > Variables > Actions):
# ENABLE_CI = true/false (master switch, default: true)
# ENABLE_TESTS = true/false (run test job, default: true)
# ENABLE_LINT = true/false (run lint job, default: true)
# ENABLE_COVERAGE = true/false (run coverage report, default: true)
#
# To disable CI entirely: set ENABLE_CI=false in repo variables.
# To skip just tests: set ENABLE_TESTS=false (lint still runs).
# To skip just lint: set ENABLE_LINT=false (tests still run).
name: CI
on:
push:
branches: [main]
paths-ignore:
- "docs/**"
- "uml/**"
- "drawio/**"
- "**/*.md"
- "LICENSE"
- "VERSION"
pull_request:
branches: [main]
paths-ignore:
- "docs/**"
- "uml/**"
- "drawio/**"
- "**/*.md"
- "LICENSE"
- "VERSION"
workflow_dispatch:
inputs:
run_tests:
description: "Run test suite"
type: boolean
default: true
run_lint:
description: "Run linter"
type: boolean
default: true
run_coverage:
description: "Generate coverage report"
type: boolean
default: true
# Cancel superseded runs for the same ref to avoid wasted compute.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
# Minimal permissions - read-only access to repository contents.
permissions:
contents: read
jobs:
# --- Gate: Check if CI is enabled ------------------------------------------
# Reads ENABLE_CI repo variable. If false, all downstream jobs are skipped.
check-enabled:
name: Check CI flags
runs-on: ubuntu-latest
outputs:
ci_enabled: ${{ steps.flags.outputs.ci_enabled }}
tests_enabled: ${{ steps.flags.outputs.tests_enabled }}
lint_enabled: ${{ steps.flags.outputs.lint_enabled }}
coverage_enabled: ${{ steps.flags.outputs.coverage_enabled }}
steps:
- id: flags
run: |
# Master switch (default: true if not set)
CI_FLAG="${{ vars.ENABLE_CI }}"
echo "ci_enabled=${CI_FLAG:-true}" >> "$GITHUB_OUTPUT"
# Per-job flags (respect workflow_dispatch inputs if provided)
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "tests_enabled=${{ inputs.run_tests }}" >> "$GITHUB_OUTPUT"
echo "lint_enabled=${{ inputs.run_lint }}" >> "$GITHUB_OUTPUT"
echo "coverage_enabled=${{ inputs.run_coverage }}" >> "$GITHUB_OUTPUT"
else
TESTS_FLAG="${{ vars.ENABLE_TESTS }}"
LINT_FLAG="${{ vars.ENABLE_LINT }}"
COV_FLAG="${{ vars.ENABLE_COVERAGE }}"
echo "tests_enabled=${TESTS_FLAG:-true}" >> "$GITHUB_OUTPUT"
echo "lint_enabled=${LINT_FLAG:-true}" >> "$GITHUB_OUTPUT"
echo "coverage_enabled=${COV_FLAG:-true}" >> "$GITHUB_OUTPUT"
fi
# --- Job 1: Test -----------------------------------------------------------
# Runs pytest across two Python versions using a matrix strategy.
# Stages: secrets_check (hard gate) -> install -> unit_tests
# -> integration_tests -> coverage -> upload-artifact
test:
name: Test (Python ${{ matrix.python-version }})
needs: check-enabled
if: needs.check-enabled.outputs.ci_enabled == 'true' && needs.check-enabled.outputs.tests_enabled == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false # Keep running other versions even if one fails
matrix:
# mcp>=1.0.0 requires Python>=3.10; minimum version updated from 3.9 to 3.10
python-version: ["3.10", "3.11"]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
run: pip install uv
# --- Secrets scan (hard gate — NO continue-on-error) -------------------
# Blocks the entire job if secrets are found. Must pass before any test runs.
- name: Secrets scan (gate)
run: python scripts/secrets_check.py
# --- Policy audit conformance (hard gates) -----------------------------
# Both are stdlib-only, so they run before dependency install.
# Until this step existed, nothing in the repository invoked either gate:
# they passed locally and were never executed against the artifact in CI.
# A check nothing runs is the same class of defect as a check that
# cannot fail.
#
# The matrix gate asserts AC1-AC7 for V2-004 and V2-005, including that
# every Post-plugin plan cell is populated and in vocabulary. It is
# EXPECTED to fail whenever a disposition is left undecided, and that
# failure is the mechanism that surfaces the decision rather than a
# broken check to be loosened.
- name: Policy audit matrix conformance (gate)
run: python scripts/verify_policy_audit_matrix.py
# The orphan gate asserts an empty symmetric difference between the
# matrix and as-built-prd section 4.2, in both directions. It is not a
# count check: a count agrees whenever two names are simultaneously
# wrong.
- name: Policy orphan dispositions (gate)
run: python scripts/verify_policy_orphan_dispositions.py
# SRS FR-30: no text-mode open() may rely on the platform default codepage.
# cp1252 on the Windows dev machine, UTF-8 on this runner -- the same file
# read on both yields different bytes, so this is a correctness gate and
# not a style one. It is an AST pass rather than a grep because the
# baseline it replaced was a grep, and that grep undercounted by seven by
# requiring an explicit mode string: open(path) with no mode is already
# text mode. Undecidable call sites fail rather than pass quietly.
- name: Text-mode open() encoding (gate)
run: python scripts/verify_open_encoding.py
# Home-directory literals in CODE (FR-15 / SRS FR-28). Landed with V2-018
# and left unwired until V2-025's audit found it -- the same defect class
# this repository had already found once, when three policy gates existed
# and nothing executed any of them. Classification is by enclosing node,
# so a path inside a docstring is documentation and does not fail.
- name: Home-directory path literals (gate)
run: python scripts/verify_home_paths.py
# Fixed wall-clock timeouts on the long-running pipeline path (NFR-2 /
# SRS NFR-8 / ADR-016). A timeout there is a temporal proxy for "is this
# making progress", and it answers that question wrongly in both
# directions. Detection is an AST pass, not a grep, because "timeout=" is
# ordinary prose in docstrings and comments; classification records
# whether the value is unbounded, configurable, or a fixed default, and
# the one permitted socket-level exception is an enumerated entry whose
# COUNT the gate itself enforces.
- name: Fixed pipeline timeouts (gate)
run: python scripts/verify_no_fixed_timeouts.py
- name: Install production dependencies
run: uv pip install -r requirements.txt --system
- name: Install dev dependencies
run: uv pip install -r requirements-dev.txt --system
# Validate test discovery without running tests.
# Fails fast on import errors or broken test file syntax.
- name: Collect tests (validate discovery)
run: pytest tests/ --tb=short -q --collect-only
# --- Unit tests (exclude integration-marked tests) ---------------------
- name: Run unit tests
run: pytest tests/ -m "not integration" --tb=short -q
# --- Integration tests -------------------------------------------------
# Hard gate: CI fails if integration tests fail. Tests use offline mocks
# (responses library) and do not require live GitHub credentials.
- name: Run integration tests
env:
GITHUB_TOKEN: fake-token-for-testing
ANTHROPIC_API_KEY: fake-key-for-testing
run: pytest tests/integration/ -m integration --tb=short -q
# --- Coverage report ---------------------------------------------------
# Enforces the threshold configured in .coveragerc, which is fail_under = 30.
# This comment read "50% ... (fail_under=50)" and was wrong in both halves;
# measured 2026-08-05, total coverage is 44.17%, so a real 50% floor would
# have been failing the build rather than passing it.
# Generates coverage.xml for artifact upload.
- name: Coverage report + threshold enforcement
if: needs.check-enabled.outputs.coverage_enabled == 'true'
run: |
pytest tests/ -m "not integration" --tb=short -q \
--cov=langgraph_engine --cov=scripts --cov=src \
--cov-report=term-missing \
--cov-report=xml:coverage.xml \
--cov-fail-under=30
# --- Upload coverage artifact ------------------------------------------
- name: Upload coverage report
if: needs.check-enabled.outputs.coverage_enabled == 'true'
uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.python-version }}-${{ github.sha }}
path: coverage.xml
retention-days: 30
# --- Job 2: Lint -----------------------------------------------------------
# Runs ruff on scripts/ and src/ to catch syntax errors, undefined names,
# and PEP 8 violations. Line-length (E501) is ignored per project style.
lint:
name: Lint (ruff)
needs: check-enabled
if: needs.check-enabled.outputs.ci_enabled == 'true' && needs.check-enabled.outputs.lint_enabled == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install ruff
run: pip install ruff
# E = pycodestyle errors, W = pycodestyle warnings, F = pyflakes.
# E501 (line too long) is intentionally excluded.
- name: Run ruff
run: ruff check langgraph_engine/ scripts/ src/ --select E,W,F --ignore E501