diff --git a/.claude/agents/action-validator.md b/.claude/agents/action-validator.md new file mode 100644 index 0000000..c9e8105 --- /dev/null +++ b/.claude/agents/action-validator.md @@ -0,0 +1,30 @@ +You review action.yml files against the repository's critical prevention rules. + +Check each action.yml file for these violations: + +1. All external action refs are SHA-pinned (not @main/@v1) +2. All internal action refs use `ivuorinen/actions/name@SHA` format +3. Shell scripts use `set -eu` (POSIX, not bash) +4. Steps with referenced outputs have `id:` fields +5. Tool availability checked before use (`command -v`) +6. Variables properly quoted (`"$var"`) +7. `$GITHUB_OUTPUT` uses `printf`, not `echo` +8. No nested `${{ }}` in quoted YAML strings +9. Token inputs use `${{ github.token }}` default +10. Fallbacks provided for tools not on all runners + +Run `actionlint` on each file. Report violations with file path, line, and fix suggestion. + +To find all action.yml files: + +```bash +find . -name "action.yml" -not -path "./.git/*" +``` + +For each file, read it and check against all 10 rules. Then run: + +```bash +actionlint +``` + +Output a summary table of violations found, grouped by action. diff --git a/.claude/agents/test-coverage-reviewer.md b/.claude/agents/test-coverage-reviewer.md new file mode 100644 index 0000000..c8ffc22 --- /dev/null +++ b/.claude/agents/test-coverage-reviewer.md @@ -0,0 +1,33 @@ +You review test coverage for GitHub Actions in this monorepo. + +For each action: + +1. Read the action.yml to understand inputs, outputs, and steps +2. Read the corresponding test files in `_tests/unit//` +3. Check if all inputs have validation tests +4. Check if error paths are tested (missing required inputs, invalid values) +5. Check if shell scripts have edge case tests (spaces in paths, empty strings, special chars) +6. Report coverage gaps with specific test suggestions + +To find all actions and their tests: + +```bash +ls -d */action.yml | sed 's|/action.yml||' +ls -d _tests/unit/*/ +``` + +Compare the two lists to find actions without any tests. + +For each action with tests, check coverage of: + +- All required inputs validated +- All optional inputs with defaults tested +- Error conditions (missing inputs, invalid formats) +- Edge cases in shell logic (empty strings, special characters, spaces in paths) +- Output values verified + +Output a coverage report with: + +- Actions with no tests (critical) +- Actions with partial coverage (list missing test cases) +- Actions with good coverage (brief confirmation) diff --git a/.claude/hooks/block-rules-yml.sh b/.claude/hooks/block-rules-yml.sh new file mode 100755 index 0000000..2dbb0bc --- /dev/null +++ b/.claude/hooks/block-rules-yml.sh @@ -0,0 +1,16 @@ +#!/bin/sh +set -eu + +# Read JSON input from stdin to get the file path +INPUT=$(cat) +FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.filePath // empty') + +if [ -z "$FILE_PATH" ]; then + exit 0 +fi + +case "$FILE_PATH" in + */rules.yml) + echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"rules.yml files are auto-generated. Run make update-validators instead."}}' + ;; +esac diff --git a/.claude/hooks/post-edit-write.sh b/.claude/hooks/post-edit-write.sh new file mode 100755 index 0000000..11fbc87 --- /dev/null +++ b/.claude/hooks/post-edit-write.sh @@ -0,0 +1,39 @@ +#!/bin/sh +set -eu + +# Read JSON input from stdin to get the file path +INPUT=$(cat) +FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.filePath // empty') + +if [ -z "$FILE_PATH" ]; then + exit 0 +fi + +case "$FILE_PATH" in + */rules.yml) + # rules.yml should not be reached here (blocked by PreToolUse), + # but skip formatting just in case + exit 0 + ;; + *.py) + ruff format --quiet "$FILE_PATH" 2>/dev/null || true + ruff check --fix --quiet "$FILE_PATH" 2>/dev/null || true + ;; + *.sh) + shfmt -w "$FILE_PATH" 2>/dev/null || true + shellcheck "$FILE_PATH" 2>&1 || true + ;; + *.yml|*.yaml|*.json) + npx prettier --write "$FILE_PATH" 2>/dev/null || true + ;; + *.md) + npx prettier --write "$FILE_PATH" 2>/dev/null || true + ;; +esac + +# Run actionlint on action.yml files +case "$FILE_PATH" in + */action.yml) + actionlint "$FILE_PATH" 2>&1 || true + ;; +esac diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..9f967c6 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,26 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rules-yml.sh" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/post-edit-write.sh" + } + ] + } + ] + } +} diff --git a/.claude/skills/check-pins/SKILL.md b/.claude/skills/check-pins/SKILL.md new file mode 100644 index 0000000..ecd3f5f --- /dev/null +++ b/.claude/skills/check-pins/SKILL.md @@ -0,0 +1,39 @@ +--- +name: check-pins +description: Verify all action references are properly SHA-pinned +disable-model-invocation: true +--- + +# Check SHA-Pinned Action References + +## 1. Check version references + +```bash +make check-version-refs +``` + +This verifies that all `ivuorinen/actions/*` references in `action.yml` files use SHA-pinned commits. + +## 2. Check local references + +```bash +make check-local-refs +``` + +This verifies that test workflows use `./action-name` format (local references are allowed in tests). + +## 3. Interpret results + +**Violations to fix:** + +- `@main` or `@v*` references in `action.yml` files must be replaced with full SHA commits +- `./action-name` in `action.yml` (non-test) files must use `ivuorinen/actions/action-name@` +- External actions must be pinned to SHA commits, not version tags + +**How to get the current SHA:** + +```bash +git rev-parse HEAD +``` + +Use this SHA when pinning internal action references after committing changes. diff --git a/.claude/skills/new-action/SKILL.md b/.claude/skills/new-action/SKILL.md new file mode 100644 index 0000000..2404254 --- /dev/null +++ b/.claude/skills/new-action/SKILL.md @@ -0,0 +1,60 @@ +--- +name: new-action +description: Scaffold a new GitHub Action with all required files +disable-model-invocation: true +--- + +# Scaffold a New GitHub Action + +## 1. Gather information + +Ask the user for: + +- **Action name** (kebab-case, e.g. `my-new-action`) +- **Description** (one line) +- **Category** (setup, linting, testing, build, publishing, repository, utility) +- **Inputs** (name, description, required, default for each) +- **What it does** (shell commands, composite steps, etc.) + +## 2. Create directory and action.yml + +Create `/action.yml` following the existing action patterns: + +- Use `composite` runs type +- Include `set -eu` in shell scripts (POSIX sh, not bash) +- Use `${{ github.token }}` for token defaults +- Pin all external action references to SHA commits +- Pin internal action references using `ivuorinen/actions/action-name@` +- Add `id:` to steps whose outputs are referenced + +## 3. Generate validation rules + +```bash +make update-validators +``` + +This generates `/rules.yml` from the action's inputs. + +## 4. Generate test scaffolding + +```bash +make generate-tests +``` + +## 5. Generate README + +```bash +make docs +``` + +## 6. Run validation + +```bash +make all +``` + +Fix any issues before considering the action complete. + +## 7. Update repository overview + +Remind the user to update the Serena memory `repository_overview` if they use Serena. diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md new file mode 100644 index 0000000..dcd7039 --- /dev/null +++ b/.claude/skills/release/SKILL.md @@ -0,0 +1,57 @@ +--- +name: release +description: Create a new CalVer release with validation checks +disable-model-invocation: true +--- + +# Release Workflow + +Follow these steps to create a new CalVer release: + +## 1. Pre-flight checks + +Run the full validation pipeline: + +```bash +make all +``` + +If any step fails, fix the issues before proceeding. + +## 2. Check version references + +Verify all action references are properly pinned: + +```bash +make check-version-refs +make check-local-refs +``` + +## 3. Prepare the release + +Run release preparation (updates version references): + +```bash +make release-prep +``` + +Review the changes with `git diff`. + +## 4. Confirm with user + +Ask the user to confirm: + +- The version number (defaults to `vYYYY.MM.DD` based on today's date) +- That all changes look correct + +## 5. Create the release + +```bash +make release VERSION=vYYYY.MM.DD +``` + +Replace `vYYYY.MM.DD` with the confirmed version. + +## 6. Verify + +Show the user the created tag and any output from the release process. diff --git a/.claude/skills/test-action/SKILL.md b/.claude/skills/test-action/SKILL.md new file mode 100644 index 0000000..5c44b92 --- /dev/null +++ b/.claude/skills/test-action/SKILL.md @@ -0,0 +1,34 @@ +--- +name: test-action +description: Run tests for a specific GitHub Action by name +disable-model-invocation: true +--- + +# Test a Specific Action + +## 1. Identify the action + +Ask the user which action to test if not already specified. +List available actions if needed: + +```bash +ls -d */action.yml | sed 's|/action.yml||' +``` + +## 2. Run tests + +```bash +make test-action ACTION= +``` + +## 3. Display results + +Show the test output. If tests fail, read the relevant test files in `_tests/unit//` and the action's `action.yml` to help diagnose the issue. + +## 4. Coverage (optional) + +If the user wants coverage information: + +```bash +make test-coverage +``` diff --git a/.claude/skills/validate/SKILL.md b/.claude/skills/validate/SKILL.md new file mode 100644 index 0000000..26818d9 --- /dev/null +++ b/.claude/skills/validate/SKILL.md @@ -0,0 +1,49 @@ +--- +name: validate +description: Run full validation pipeline (docs, format, lint, test) +disable-model-invocation: true +--- + +# Full Validation Pipeline + +Run the complete validation pipeline: + +```bash +make all +``` + +This runs in order: `docs` -> `format` -> `lint` -> `test` + +## If validation fails + +### Formatting issues + +```bash +make format +``` + +Then re-run `make all`. + +### Linting issues + +- **actionlint**: Check action.yml syntax, step IDs, expression usage +- **shellcheck**: POSIX compliance, quoting, variable usage +- **ruff**: Python style and errors +- **markdownlint**: Markdown formatting +- **prettier**: YAML/JSON/MD formatting + +### Test failures + +```bash +make test +``` + +Read the failing test output and fix the underlying action or test. + +### Documentation drift + +```bash +make docs +``` + +Regenerates READMEs from action.yml files. diff --git a/CLAUDE.md b/CLAUDE.md index c91349f..751a7ca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,12 +25,22 @@ ### Folders - `.serena/` – Internal config (do not edit) +- `.claude/hooks/` – Claude Code hook scripts (auto-format, lint, block rules.yml edits) +- `.claude/skills/` – Claude Code skills (`/release`, `/test-action`, `/new-action`, `/validate`, `/check-pins`) +- `.claude/agents/` – Claude Code subagents (action-validator, test-coverage-reviewer) - `.github/` – Workflows/templates - `_tests/` – ShellSpec tests - `_tools/` – Helper tools - `validate-inputs/` – Python validation system + tests - `*/rules.yml` – Auto-generated validation rules +### Claude Code Hooks + +**Auto-formatting**: PostToolUse hooks auto-format files on Edit/Write (ruff for .py, shfmt for .sh, prettier for .yml/.yaml/.json/.md, actionlint for action.yml) +**Blocked edits**: PreToolUse hook blocks direct edits to `rules.yml` (auto-generated, use `make update-validators`) +**Hook schema**: `matcher` is a regex string matching tool names (e.g. `"Edit|Write"`), not an object. File filtering done in hook scripts via stdin JSON (`jq -r '.tool_input.file_path'`) +**Reference**: `$CLAUDE_PROJECT_DIR` for project-relative paths in hook commands + ### Memory System **Location**: `.serena/memories/` (9 consolidated memories for context)