feat(pr-ready): add doc-check step to the workflow #261
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| jobs: | |
| build-and-test: | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: src | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version-file: src/go.mod | |
| cache-dependency-path: src/go.sum | |
| - name: Build | |
| run: go build ./... | |
| - name: Vet | |
| run: go vet ./... | |
| - name: Test | |
| run: go test ./... -v -race -count=1 | |
| - name: Check formatting | |
| run: | | |
| unformatted=$(gofmt -l .) | |
| if [ -n "$unformatted" ]; then | |
| echo "Files not formatted:" | |
| echo "$unformatted" | |
| exit 1 | |
| fi | |
| hook-smoke-tests: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Run hook smoke tests | |
| run: bash hooks/hooks_test.sh | |
| # Catches the class of bug where the shipped marketplace tree is | |
| # missing something the plugin needs at runtime. PR #52 shipped a | |
| # plugin.json pointing at bin/devkit, but bin/ was gitignored — the | |
| # plugin installed but the MCP server silently failed to start. | |
| # This job runs in a clean dir with no local build artifacts. | |
| fresh-install-smoke: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Bootstrap wrapper must exist and be executable | |
| run: | | |
| test -f bin/devkit || { echo "FATAL: bin/devkit wrapper missing from repo"; exit 1; } | |
| test -x bin/devkit || { echo "FATAL: bin/devkit not executable"; exit 1; } | |
| - name: Wrapper must not contain devkit-engine binary (that is gitignored) | |
| run: | | |
| if [ -e bin/devkit-engine ]; then | |
| echo "FATAL: bin/devkit-engine should not be tracked in git (it is the cached binary)" | |
| exit 1 | |
| fi | |
| - name: plugin.json MCP command must match wrapper path | |
| run: | | |
| cmd=$(jq -r '.mcpServers["devkit-engine"].command' .claude-plugin/plugin.json) | |
| expected='${CLAUDE_PLUGIN_ROOT}/bin/devkit' | |
| if [ "$cmd" != "$expected" ]; then | |
| echo "FATAL: plugin.json mcpServers.devkit-engine.command is '$cmd', expected '$expected'" | |
| exit 1 | |
| fi | |
| - name: shellcheck wrapper | |
| run: | | |
| sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck | |
| shellcheck bin/devkit | |
| - name: Wrapper --help must exit with a download attempt (no local engine) | |
| run: | | |
| # With no local bin/devkit-engine and no network cache, the wrapper | |
| # should try to download and either succeed or fail loudly. It must | |
| # NOT silently pass with exit 0 and no output. | |
| set +e | |
| output=$(./bin/devkit --version 2>&1) | |
| exit_code=$? | |
| set -e | |
| echo "wrapper exit=$exit_code" | |
| echo "wrapper output: $output" | |
| # The wrapper must either succeed (downloaded + ran --version) or | |
| # emit a clear error. An empty output with exit 0 would be the silent | |
| # failure class we're guarding against. | |
| if [ $exit_code -eq 0 ] && [ -z "$output" ]; then | |
| echo "FATAL: wrapper exited 0 with no output — silent failure class bug" | |
| exit 1 | |
| fi | |
| validate-counts: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Validate README and ROADMAP counts | |
| run: | | |
| set -uo pipefail | |
| ERRORS="" | |
| CHECKS=0 | |
| # Verify directories exist | |
| for dir in commands skills workflows hooks; do | |
| if [ ! -d "$dir" ]; then | |
| echo "FATAL: $dir/ directory missing" | |
| exit 1 | |
| fi | |
| done | |
| # Count actual files | |
| CMD_COUNT=$(set -- commands/*.md; echo $#) | |
| SKILL_COUNT=$(find skills -maxdepth 2 -name 'SKILL.md' | wc -l | tr -d ' ') | |
| WORKFLOW_COUNT=$(set -- workflows/*.yml; echo $#) | |
| HOOK_COUNT=$(python3 -c " | |
| import sys, json | |
| data = json.load(open('hooks/hooks.json')) | |
| scripts = set() | |
| for event in data.get('hooks', {}).values(): | |
| for entry in event: | |
| for h in entry.get('hooks', []): | |
| cmd = h.get('command', '') | |
| scripts.add(cmd.split('/')[-1]) | |
| print(len(scripts)) | |
| ") || { echo "FATAL: failed to parse hooks/hooks.json"; exit 1; } | |
| # Validate counts are non-zero | |
| for var_name in CMD_COUNT SKILL_COUNT WORKFLOW_COUNT HOOK_COUNT; do | |
| val=$(eval echo \$$var_name) | |
| if [ -z "$val" ] || [ "$val" = "0" ]; then | |
| echo "FATAL: $var_name is empty or zero ($val)" | |
| exit 1 | |
| fi | |
| done | |
| echo "Actual counts: commands=$CMD_COUNT skills=$SKILL_COUNT workflows=$WORKFLOW_COUNT hooks=$HOOK_COUNT" | |
| # Check README and ROADMAP | |
| for file in README.md ROADMAP.md; do | |
| if [ ! -f "$file" ]; then continue; fi | |
| # Commands | |
| if grep -qE "\*\*[0-9]+ slash commands\*\*" "$file"; then | |
| DOC_CMD=$(grep -m1 -oE '\*\*[0-9]+ slash commands\*\*' "$file" | grep -oE '[0-9]+' | head -1) | |
| CHECKS=$((CHECKS + 1)) | |
| if [ "$DOC_CMD" != "$CMD_COUNT" ]; then | |
| ERRORS="$ERRORS\n$file: says $DOC_CMD commands, actual is $CMD_COUNT" | |
| fi | |
| fi | |
| # Skills | |
| if grep -qE "\*\*[0-9]+ context-activated skills\*\*" "$file"; then | |
| DOC_SKILL=$(grep -m1 -oE '\*\*[0-9]+ context-activated skills\*\*' "$file" | grep -oE '[0-9]+' | head -1) | |
| CHECKS=$((CHECKS + 1)) | |
| if [ "$DOC_SKILL" != "$SKILL_COUNT" ]; then | |
| ERRORS="$ERRORS\n$file: says $DOC_SKILL skills, actual is $SKILL_COUNT" | |
| fi | |
| fi | |
| # Workflows | |
| if grep -qE "\*\*[0-9]+ YAML workflows\*\*" "$file"; then | |
| DOC_WF=$(grep -m1 -oE '\*\*[0-9]+ YAML workflows\*\*' "$file" | grep -oE '[0-9]+' | head -1) | |
| CHECKS=$((CHECKS + 1)) | |
| if [ "$DOC_WF" != "$WORKFLOW_COUNT" ]; then | |
| ERRORS="$ERRORS\n$file: says $DOC_WF workflows, actual is $WORKFLOW_COUNT" | |
| fi | |
| fi | |
| # Hooks | |
| if grep -qE "\*\*[0-9]+ hooks\*\*" "$file"; then | |
| DOC_HOOK=$(grep -m1 -oE '\*\*[0-9]+ hooks\*\*' "$file" | grep -oE '[0-9]+' | head -1) | |
| CHECKS=$((CHECKS + 1)) | |
| if [ "$DOC_HOOK" != "$HOOK_COUNT" ]; then | |
| ERRORS="$ERRORS\n$file: says $DOC_HOOK hooks, actual is $HOOK_COUNT" | |
| fi | |
| fi | |
| done | |
| # Check README repo structure comments | |
| if grep -qE '# [0-9]+ slash commands' README.md; then | |
| TREE_CMD=$(grep -m1 -oE '# [0-9]+ slash commands' README.md | grep -oE '[0-9]+' | head -1) | |
| CHECKS=$((CHECKS + 1)) | |
| if [ "$TREE_CMD" != "$CMD_COUNT" ]; then | |
| ERRORS="$ERRORS\nREADME.md tree: says $TREE_CMD commands, actual is $CMD_COUNT" | |
| fi | |
| fi | |
| if grep -qE '# [0-9]+ context-activated skills' README.md; then | |
| TREE_SKILL=$(grep -m1 -oE '# [0-9]+ context-activated skills' README.md | grep -oE '[0-9]+' | head -1) | |
| CHECKS=$((CHECKS + 1)) | |
| if [ "$TREE_SKILL" != "$SKILL_COUNT" ]; then | |
| ERRORS="$ERRORS\nREADME.md tree: says $TREE_SKILL skills, actual is $SKILL_COUNT" | |
| fi | |
| fi | |
| if grep -qE '# [0-9]+ YAML workflow' README.md; then | |
| TREE_WF=$(grep -m1 -oE '# [0-9]+ YAML workflow' README.md | grep -oE '[0-9]+' | head -1) | |
| CHECKS=$((CHECKS + 1)) | |
| if [ "$TREE_WF" != "$WORKFLOW_COUNT" ]; then | |
| ERRORS="$ERRORS\nREADME.md tree: says $TREE_WF workflows, actual is $WORKFLOW_COUNT" | |
| fi | |
| fi | |
| # Ensure at least some counts were validated | |
| if [ "$CHECKS" -lt 1 ]; then | |
| echo "FATAL: no count patterns found in README.md or ROADMAP.md — expected at least one" | |
| exit 1 | |
| fi | |
| if [ -n "$ERRORS" ]; then | |
| echo "" | |
| echo "COUNT MISMATCHES FOUND:" | |
| printf '%b\n' "$ERRORS" | |
| echo "" | |
| echo "Fix the counts in README.md and/or ROADMAP.md to match actual file counts." | |
| exit 1 | |
| else | |
| echo "All counts match ($CHECKS checks passed)." | |
| fi |