claude-code-ultimate-guide/examples/context-engineering/ci-drift-check.yml
Florian BRUNIAUX fe28f89574 feat(context): Context Engineering Configurator + consolidated guide (v3.34.0)
New: interactive configurator at cc.bruniaux.com/context/ that generates a
personalized CLAUDE.md starter kit based on team size, stack, and current setup.
Multi-step flow (profile, current state, stack, results) with maturity scoring
(Level 1-5), copy-to-clipboard artifacts, localStorage persistence.

Guide content:
- guide/core/context-engineering.md (1,188 lines, 8 sections): context budget,
  150-instruction ceiling, modular architecture, team assembly, ACE pipeline,
  quality measurement, context reduction techniques
- examples/context-engineering/ (10 templates): assembler.ts, profile-template.yaml,
  skeleton-template.md, canary-check.sh, ci-drift-check.yml, eval-questions.yaml,
  context-budget-calculator.sh, rules/knowledge-feeding.md, rules/update-loop-retro.md
- tools/context-audit-prompt.md (543 lines): 8-dimension scoring /100

Navigation: guide/README.md, machine-readable/reference.yaml (24 new entries)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 11:18:04 +01:00

216 lines
8.5 KiB
YAML

name: Context Drift Check
# Detects structural degradation in CLAUDE.md on a weekly schedule.
# Opens a GitHub issue automatically when problems are found.
#
# Setup:
# 1. Copy this file to .github/workflows/context-drift.yml
# 2. Make sure canary-check.sh is committed and executable
# 3. Adjust the cron schedule and threshold values as needed
#
# The workflow checks:
# - Structural validity (broken @imports, size, conflicts)
# - CLAUDE.md size trend (warns when exceeding threshold)
# - Broken @import references
on:
schedule:
- cron: '0 9 * * 1' # Every Monday at 9:00 AM UTC
workflow_dispatch: # Allow manual trigger from GitHub UI
inputs:
project_dir:
description: 'Project directory to check (relative to repo root)'
required: false
default: '.'
env:
# Customize these thresholds for your project
CLAUDE_MD_MAX_LINES: 500
CLAUDE_MD_WARN_LINES: 400
jobs:
drift-check:
name: Check CLAUDE.md for drift
runs-on: ubuntu-latest
permissions:
contents: read
issues: write # Required to open issues on failure
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history needed for git log checks
- name: Make canary script executable
run: chmod +x ./examples/context-engineering/canary-check.sh
- name: Run structural canary checks
id: canary
run: |
PROJECT_DIR="${{ github.event.inputs.project_dir || '.' }}"
./examples/context-engineering/canary-check.sh "$PROJECT_DIR"
continue-on-error: true # Capture exit code without stopping workflow
- name: Check CLAUDE.md size against thresholds
id: size_check
run: |
FILE="CLAUDE.md"
if [ ! -f "$FILE" ]; then
echo "::warning::CLAUDE.md not found at repo root"
exit 0
fi
CURRENT=$(wc -l < "$FILE" | tr -d ' ')
echo "current_lines=$CURRENT" >> "$GITHUB_OUTPUT"
if [ "$CURRENT" -gt "$CLAUDE_MD_MAX_LINES" ]; then
echo "::error::CLAUDE.md has $CURRENT lines (max: $CLAUDE_MD_MAX_LINES). Consider modularizing with @imports."
echo "size_status=fail" >> "$GITHUB_OUTPUT"
elif [ "$CURRENT" -gt "$CLAUDE_MD_WARN_LINES" ]; then
echo "::warning::CLAUDE.md has $CURRENT lines (warn threshold: $CLAUDE_MD_WARN_LINES). Getting large."
echo "size_status=warn" >> "$GITHUB_OUTPUT"
else
echo "CLAUDE.md size: $CURRENT lines (OK)"
echo "size_status=ok" >> "$GITHUB_OUTPUT"
fi
- name: Check for stale @imports
id: import_check
run: |
FILE="CLAUDE.md"
if [ ! -f "$FILE" ]; then
exit 0
fi
BROKEN=0
while IFS= read -r line; do
if [[ "$line" =~ ^@([^[:space:]].+)$ ]]; then
IMPORT="${BASH_REMATCH[1]}"
if [ ! -f "$IMPORT" ]; then
echo "::error::Broken @import in CLAUDE.md: @$IMPORT"
BROKEN=$((BROKEN + 1))
fi
fi
done < "$FILE"
echo "broken_imports=$BROKEN" >> "$GITHUB_OUTPUT"
if [ "$BROKEN" -gt 0 ]; then
echo "::error::Found $BROKEN broken @import(s) in CLAUDE.md"
else
echo "All @imports resolve correctly"
fi
- name: Check CLAUDE.md is git-tracked and recent
id: freshness_check
run: |
FILE="CLAUDE.md"
if [ ! -f "$FILE" ]; then
exit 0
fi
# Check if tracked
if ! git ls-files --error-unmatch "$FILE" > /dev/null 2>&1; then
echo "::warning::CLAUDE.md is not tracked in git"
exit 0
fi
# Get days since last update
LAST_COMMIT_DATE=$(git log -1 --format="%cd" --date=format:"%Y-%m-%d" -- "$FILE")
TODAY=$(date +%Y-%m-%d)
DAYS_OLD=$(( ($(date -d "$TODAY" +%s 2>/dev/null || date -j -f "%Y-%m-%d" "$TODAY" +%s) - $(date -d "$LAST_COMMIT_DATE" +%s 2>/dev/null || date -j -f "%Y-%m-%d" "$LAST_COMMIT_DATE" +%s)) / 86400 ))
echo "days_old=$DAYS_OLD" >> "$GITHUB_OUTPUT"
echo "last_updated=$LAST_COMMIT_DATE" >> "$GITHUB_OUTPUT"
if [ "$DAYS_OLD" -gt 90 ]; then
echo "::warning::CLAUDE.md was last updated $DAYS_OLD days ago ($LAST_COMMIT_DATE). Consider reviewing for stale rules."
else
echo "CLAUDE.md freshness: last updated $DAYS_OLD days ago ($LAST_COMMIT_DATE) — OK"
fi
- name: Collect check results
id: summary
if: always()
run: |
CANARY_STATUS="${{ steps.canary.outcome }}"
BROKEN_IMPORTS="${{ steps.import_check.outputs.broken_imports || 0 }}"
SIZE_STATUS="${{ steps.size_check.outputs.size_status || 'ok' }}"
CURRENT_LINES="${{ steps.size_check.outputs.current_lines || 0 }}"
DAYS_OLD="${{ steps.freshness_check.outputs.days_old || 0 }}"
LAST_UPDATED="${{ steps.freshness_check.outputs.last_updated || 'unknown' }}"
ISSUES=0
[ "$CANARY_STATUS" = "failure" ] && ISSUES=$((ISSUES + 1))
[ "$BROKEN_IMPORTS" -gt 0 ] && ISSUES=$((ISSUES + BROKEN_IMPORTS))
[ "$SIZE_STATUS" = "fail" ] && ISSUES=$((ISSUES + 1))
echo "total_issues=$ISSUES" >> "$GITHUB_OUTPUT"
echo "summary_lines=$CURRENT_LINES" >> "$GITHUB_OUTPUT"
echo "summary_days=$DAYS_OLD" >> "$GITHUB_OUTPUT"
echo "summary_updated=$LAST_UPDATED" >> "$GITHUB_OUTPUT"
- name: Open issue on failure
if: always() && steps.summary.outputs.total_issues > 0
uses: actions/github-script@v7
with:
script: |
const issues = parseInt('${{ steps.summary.outputs.total_issues }}');
const lines = '${{ steps.summary.outputs.summary_lines }}';
const daysOld = '${{ steps.summary.outputs.summary_days }}';
const lastUpdated = '${{ steps.summary.outputs.summary_updated }}';
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
// Check if there's already an open issue for drift
const openIssues = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
labels: 'ai-context,maintenance',
state: 'open',
});
if (openIssues.data.length > 0) {
// Add a comment to the existing issue instead of opening a new one
const existingIssue = openIssues.data[0];
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: existingIssue.number,
body: `Weekly check still finding issues (${issues} problem(s)).\n\nRun: ${runUrl}\nCLAUDE.md: ${lines} lines, last updated: ${lastUpdated} (${daysOld} days ago)`,
});
} else {
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `Context Drift Detected — CLAUDE.md needs review`,
body: [
`The weekly context drift check found **${issues} problem(s)** in \`CLAUDE.md\`.`,
``,
`## Details`,
`- File size: ${lines} lines`,
`- Last updated: ${lastUpdated} (${daysOld} days ago)`,
`- Workflow run: ${runUrl}`,
``,
`## Action Required`,
`1. Review the workflow logs for specific issues`,
`2. Run \`./examples/context-engineering/canary-check.sh .\` locally for details`,
`3. Fix broken @imports, reduce file size, or update stale rules`,
`4. Close this issue once CLAUDE.md is back in good shape`,
``,
`_This issue was opened automatically by the context drift check workflow._`,
].join('\n'),
labels: ['ai-context', 'maintenance'],
});
}
- name: Final status
if: always()
run: |
ISSUES="${{ steps.summary.outputs.total_issues }}"
if [ "$ISSUES" -gt 0 ]; then
echo "Context drift check: FAILED ($ISSUES issues)"
exit 1
else
echo "Context drift check: PASSED"
fi