-
Notifications
You must be signed in to change notification settings - Fork 1
342 lines (302 loc) · 13.4 KB
/
Copy pathci.yml
File metadata and controls
342 lines (302 loc) · 13.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
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
# Digital Service Orchestra (DSO) — Continuous Integration
# Runs shellcheck, Python linting, and plugin tests.
name: CI
on:
push:
branches:
- main
- 'feature/**'
- 'bugfix/**'
- 'epic/**'
- 'exp/**'
pull_request:
branches:
- main
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# ── Path-change detection ───────────────────────────────────
# Mirrors the local review-gate skip logic by invoking
# plugins/dso/scripts/skip-review-check.sh — the SAME script used by
# COMMIT-WORKFLOW.md Step 0.5 to decide whether a local commit can skip
# review. This eliminates the divergence that allowed PR #66 to merge
# silently: the prior `.md`-blanket exclude let CLAUDE.md and skill .md
# changes skip tests entirely, even though those files affect agent
# behavior and ARE classified as reviewable by the local gate.
#
# skip-review-check.sh classifies a file list (one file per line on stdin):
# exit 0 → SKIP_REVIEW=true (all files allowlisted, e.g. tickets, images,
# lockfiles); CI can skip downstream tests.
# exit 1 → SKIP_REVIEW=false (at least one reviewable file: CLAUDE.md,
# hooks/, skills/, docs/workflows/, or any
# file not matched by review-gate-allowlist.conf);
# CI MUST run downstream tests.
#
# DSO_FORCE_LOCAL_REVIEW=1 disables skip-review-check.sh's
# enforcement.strategy=ci short-circuit (which is intended for the local
# pipeline, NOT for CI itself).
#
# Bug f776-d7ef: PR #66 merged with green CI because the prior `.md`-only
# filter classified its 8 .md changes as code_changed=false. Hook/Script/
# Python tests then "ran" in 4-5s (no-op) and reported SUCCESS — but the
# underlying tests would have failed (and did, on the post-merge push event).
changes:
name: Detect Changed Paths
runs-on: ubuntu-latest
timeout-minutes: 2
outputs:
code_changed: ${{ steps.filter.outputs.code_changed }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Detect reviewable changes (mirrors local review-gate)
id: filter
run: |
set -euo pipefail
# Non-PR events (push, workflow_dispatch) always run the full pipeline.
# Short-circuit before computing the diff to avoid sentinel-string fragility.
if [[ "$GITHUB_EVENT_NAME" != "pull_request" ]]; then
echo "Event ${GITHUB_EVENT_NAME}: forcing code_changed=true"
echo "code_changed=true" >> "$GITHUB_OUTPUT"
exit 0
fi
BASE_REF="origin/${{ github.base_ref }}"
# Verify BASE_REF is fetched before computing the diff. fetch-depth: 0
# on actions/checkout@v4 fetches all refs; this guard fails loud rather
# than silent if the runner's checkout shape ever changes.
if ! git rev-parse --verify --quiet "$BASE_REF" >/dev/null; then
echo "ERROR: base ref ${BASE_REF} not found in local refs after checkout. Forcing code_changed=true to be safe." >&2
echo "code_changed=true" >> "$GITHUB_OUTPUT"
exit 0
fi
CHANGED=$(git diff --name-only "$BASE_REF...HEAD")
echo "Changed files:"
echo "$CHANGED"
# Verify the local review-gate skip script is present. Fail-loud if
# absent rather than silently falling back to the broken `.md`-only
# heuristic (the very gap that landed PR #66 with broken tests).
SKIP_SCRIPT="plugins/dso/scripts/skip-review-check.sh"
if [[ ! -x "$SKIP_SCRIPT" ]]; then
echo "ERROR: ${SKIP_SCRIPT} not found or not executable. Forcing code_changed=true to be safe." >&2
echo "code_changed=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# Run the same classifier the local review gate uses.
# exit 0 → all files allowlisted (tests can skip)
# exit 1 → at least one reviewable file (tests MUST run)
# DSO_FORCE_LOCAL_REVIEW=1 disables the enforcement.strategy=ci
# short-circuit (we ARE the CI; that short-circuit would no-op us).
set +e
echo "$CHANGED" | DSO_FORCE_LOCAL_REVIEW=1 bash "$SKIP_SCRIPT"
SKIP_RC=$?
set -e
if [[ "$SKIP_RC" -eq 0 ]]; then
echo "skip-review-check: all changes allowlisted — code_changed=false"
echo "code_changed=false" >> "$GITHUB_OUTPUT"
elif [[ "$SKIP_RC" -eq 1 ]]; then
echo "skip-review-check: reviewable changes detected — code_changed=true"
echo "code_changed=true" >> "$GITHUB_OUTPUT"
else
echo "ERROR: skip-review-check.sh exited ${SKIP_RC} (expected 0 or 1). Forcing code_changed=true to be safe." >&2
echo "code_changed=true" >> "$GITHUB_OUTPUT"
fi
# ── Fast gates ──────────────────────────────────────────────
actionlint:
name: Actionlint
needs: [changes]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- name: Install actionlint
if: needs.changes.outputs.code_changed == 'true'
run: |
bash <(curl -fsSL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
echo "$PWD" >> "$GITHUB_PATH"
- name: Run actionlint
if: needs.changes.outputs.code_changed == 'true'
run: actionlint -color
- name: Job timing report
if: always()
run: echo "actionlint completed at $(date -u +%Y-%m-%dT%H:%M:%SZ)"
shellcheck:
name: ShellCheck
needs: [changes]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- name: Install shellcheck
# Download from GitHub releases instead of apt to avoid azure.archive.ubuntu.com
# DNS outages on Azure-hosted runners (saw 3 consecutive cancellations 2026-05-02).
# github.com DNS is independent of the apt repo mirrors.
if: needs.changes.outputs.code_changed == 'true'
run: |
set -euo pipefail
SHELLCHECK_VERSION=v0.10.0
TARBALL="shellcheck-${SHELLCHECK_VERSION}.linux.x86_64.tar.xz"
for attempt in 1 2 3; do
if curl -fsSL --retry 3 --retry-delay 5 \
"https://github.com/koalaman/shellcheck/releases/download/${SHELLCHECK_VERSION}/${TARBALL}" \
-o "${TARBALL}"; then
break
fi
echo "Download attempt ${attempt} failed; retrying..." >&2
sleep 10
done
tar -xJf "${TARBALL}"
sudo mv "shellcheck-${SHELLCHECK_VERSION}/shellcheck" /usr/local/bin/
rm -rf "shellcheck-${SHELLCHECK_VERSION}" "${TARBALL}"
shellcheck --version
- name: Run shellcheck
if: needs.changes.outputs.code_changed == 'true'
run: shellcheck --severity=warning plugins/dso/scripts/*.sh plugins/dso/hooks/*.sh plugins/dso/hooks/**/*.sh
- name: Job timing report
if: always()
run: echo "shellcheck completed at $(date -u +%Y-%m-%dT%H:%M:%SZ)"
lint-python:
name: Lint Python (ruff)
needs: [changes]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- name: Install ruff
if: needs.changes.outputs.code_changed == 'true'
run: pip install ruff
- name: Ruff check
if: needs.changes.outputs.code_changed == 'true'
run: ruff check plugins/dso/scripts/*.py tests/**/*.py
- name: Ruff format check
if: needs.changes.outputs.code_changed == 'true'
run: ruff format --check plugins/dso/scripts/*.py tests/**/*.py
- name: Job timing report
if: always()
run: echo "lint-python completed at $(date -u +%Y-%m-%dT%H:%M:%SZ)"
# ── Full test suite ─────────────────────────────────────────
test-hooks:
name: Hook Tests
needs: [changes, shellcheck, lint-python]
runs-on: ubuntu-latest
timeout-minutes: 10
env:
SUITE_TEST_INDEX: ${{ github.workspace }}/.test-index
steps:
- uses: actions/checkout@v4
- name: Run hook tests
if: needs.changes.outputs.code_changed == 'true'
run: bash tests/hooks/run-hook-tests.sh
- name: Job timing report
if: always()
run: echo "test-hooks completed at $(date -u +%Y-%m-%dT%H:%M:%SZ)"
test-scripts:
name: Script Tests
needs: [changes, shellcheck, lint-python]
runs-on: ubuntu-latest
timeout-minutes: 15
env:
SUITE_TEST_INDEX: ${{ github.workspace }}/.test-index
steps:
- uses: actions/checkout@v4
- name: Set up Python
if: needs.changes.outputs.code_changed == 'true'
uses: actions/setup-python@v5
with:
python-version: '3.13'
- name: Install Python dependencies
if: needs.changes.outputs.code_changed == 'true'
run: pip install ruff pyyaml jsonschema
- name: Install hyperfine
if: needs.changes.outputs.code_changed == 'true'
run: sudo apt-get update && sudo apt-get install -y hyperfine
- name: Verify hyperfine installed
if: needs.changes.outputs.code_changed == 'true'
run: hyperfine --version
- name: Run script tests
if: needs.changes.outputs.code_changed == 'true'
run: bash tests/scripts/run-script-tests.sh
- name: Job timing report
if: always()
run: echo "test-scripts completed at $(date -u +%Y-%m-%dT%H:%M:%SZ)"
validate-required-checks:
name: Validate required-checks.txt
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- name: Install Python dependencies
run: pip install pyyaml
- name: Validate check-context names
run: bash plugins/dso/scripts/onboarding/validate-required-checks.sh
# ── Mirror tracker defenses to PR ───────────────────────────
mirror-defenses-to-pr:
name: Mirror Tracker Defenses to PR
needs: [changes, test-hooks, test-scripts]
if: github.event_name == 'pull_request' && github.base_ref == 'main'
runs-on: ubuntu-latest
timeout-minutes: 5
continue-on-error: true
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Mirror defenses to PR
if: needs.changes.outputs.code_changed == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_NUMBER="${GITHUB_REF#refs/pull/}"
PR_NUMBER="${PR_NUMBER%/merge}"
bash plugins/dso/scripts/mirror-defenses-to-pr.sh "$PR_NUMBER" || true
# ── LLM code review ─────────────────────────────────────────
llm-review:
needs: [changes, test-hooks, test-scripts, mirror-defenses-to-pr]
if: github.event_name == 'pull_request' && github.base_ref == 'main'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
pull-requests: write
# REVIEW-DEFENSE: id-token: write is intentionally absent. Adding it would trigger
# the claude-code-action OIDC app-token exchange, which validates the workflow file
# is identical to the main branch as a security check. Any PR that modifies ci.yml
# (including this one) fails that check with 401 Unauthorized. The action authenticates
# via ANTHROPIC_API_KEY (direct API key auth) and does not need OIDC.
steps:
- uses: actions/checkout@v4
- name: Install litellm
if: needs.changes.outputs.code_changed == 'true'
run: pip install litellm==1.83.7
- name: Smoke-test review-complexity-classifier.sh
if: needs.changes.outputs.code_changed == 'true'
env:
CLAUDE_PLUGIN_ROOT: "${{ github.workspace }}/plugins/dso"
run: |
set -euo pipefail
FIXTURE="tests/fixtures/ci-review-corpus/fixture-diff.txt"
if [[ -f "$FIXTURE" ]]; then
DIFF_FILE="$FIXTURE"
else
echo 'No fixture diff found — generating minimal synthetic diff'
DIFF_FILE=$(mktemp /tmp/smoke-test-diff.XXXXXX)
printf '+foo\n-bar\n' > "$DIFF_FILE"
fi
OUTPUT=$(bash plugins/dso/scripts/review-complexity-classifier.sh < "$DIFF_FILE")
echo "$OUTPUT" | python3 -c 'import sys,json; d=json.load(sys.stdin); assert d.get("selected_tier") in {"light","standard","deep"}, f"invalid: {d}"'
echo "Smoke test passed: $OUTPUT"
- name: Run LLM review
if: needs.changes.outputs.code_changed == 'true'
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CLAUDE_PLUGIN_ROOT: "${{ github.workspace }}/plugins/dso"
shell: bash -eo pipefail {0}
run: |
PR_NUMBER="${GITHUB_REF#refs/pull/}"
PR_NUMBER="${PR_NUMBER%/merge}"
gh pr diff "$PR_NUMBER" | bash plugins/dso/scripts/ci-llm-review-runner.sh