Skip to content

Commit 15993d1

Browse files
authored
Merge pull request #291 from puppetlabs/add-claude-config
(maint) Add CLAUDE.md and Claude Code project hooks
2 parents 9beabfa + a62b6a4 commit 15993d1

7 files changed

Lines changed: 181 additions & 0 deletions

File tree

.claude/hooks/_parse_input.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env bash
2+
# Shared helper: reads hook JSON from stdin and sets $cmd to the Bash tool command.
3+
# Sources into each hook with: . "$(dirname "$0")/_parse_input.sh"
4+
# Exits 0 (allow) if no JSON parser is available.
5+
6+
if command -v jq >/dev/null 2>&1; then
7+
# shellcheck disable=SC2034 # cmd is used by the sourcing hook script
8+
cmd=$(jq -r '.tool_input.command // ""' 2>/dev/null || echo "")
9+
elif command -v python3 >/dev/null 2>&1; then
10+
# shellcheck disable=SC2034 # cmd is used by the sourcing hook script
11+
cmd=$(python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('command',''))" 2>/dev/null || echo "")
12+
else
13+
exit 0
14+
fi

.claude/hooks/no-main-commits.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/usr/bin/env bash
2+
# Project rule: never work directly on the main or master branch.
3+
# Blocks: git commit when current branch is main or master.
4+
5+
# shellcheck source=/dev/null
6+
. "$(dirname "$0")/_parse_input.sh"
7+
8+
if echo "$cmd" | grep -qE '(^|[;&|`(])[[:space:]]*(([[:alpha:]_][[:alnum:]_]*=[^[:space:]]*[[:space:]]+)*)([^[:space:]]*/)?git[[:space:]]+commit([[:space:]]|$|[;&|])'; then
9+
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
10+
if [ "$branch" = "main" ] || [ "$branch" = "master" ]; then
11+
echo "{\"continue\":false,\"stopReason\":\"Project rule: never work directly on the $branch branch.\"}"
12+
fi
13+
fi

.claude/hooks/no-pr-merge.sh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/usr/bin/env bash
2+
# Project rule: never merge a pull request.
3+
# Blocks: gh pr merge
4+
5+
# shellcheck source=/dev/null
6+
. "$(dirname "$0")/_parse_input.sh"
7+
8+
if echo "$cmd" | grep -qE '(^|[;&|])[[:space:]]*gh pr merge'; then
9+
echo '{"continue":false,"stopReason":"Project rule: never merge a pull request."}'
10+
fi

.claude/hooks/no-push.sh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/usr/bin/env bash
2+
# Project rule: never push a branch without explicit instruction.
3+
# Blocks: git push
4+
5+
# shellcheck source=/dev/null
6+
. "$(dirname "$0")/_parse_input.sh"
7+
8+
if echo "$cmd" | grep -qE '(^|[;&|`(])[[:space:]]*(([[:alpha:]_][[:alnum:]_]*=[^[:space:]]*[[:space:]]+)*)([^[:space:]]*/)?git[[:space:]]+push([[:space:]]|$|[;&|])'; then
9+
echo '{"continue":false,"stopReason":"Project rule: never push a branch without explicit instruction."}'
10+
fi

.claude/hooks/no-rm.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/usr/bin/env bash
2+
# Project rule: never delete a file without explicit permission.
3+
# Blocks: rm, sudo rm, xargs rm, /bin/rm, /usr/bin/rm (and similar absolute paths).
4+
# Does not cover: find -exec rm (rm as a subprocess argument, not a shell token).
5+
6+
# shellcheck source=/dev/null
7+
. "$(dirname "$0")/_parse_input.sh"
8+
9+
if echo "$cmd" | grep -qE '(^|[;&|])[[:space:]]*(sudo[[:space:]]+|xargs[[:space:]]+)?([^[:space:]]*/)?rm([[:space:]]|$)'; then
10+
echo '{"continue":false,"stopReason":"Project rule: never delete a file without explicit permission."}'
11+
fi

.claude/settings.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"hooks": {
3+
"PreToolUse": [
4+
{
5+
"matcher": "Bash",
6+
"hooks": [
7+
{
8+
"type": "command",
9+
"command": ".claude/hooks/no-pr-merge.sh",
10+
"timeout": 5,
11+
"statusMessage": "Checking project rules..."
12+
},
13+
{
14+
"type": "command",
15+
"command": ".claude/hooks/no-main-commits.sh",
16+
"timeout": 5
17+
},
18+
{
19+
"type": "command",
20+
"command": ".claude/hooks/no-push.sh",
21+
"timeout": 5
22+
},
23+
{
24+
"type": "command",
25+
"command": ".claude/hooks/no-rm.sh",
26+
"timeout": 5
27+
}
28+
]
29+
}
30+
]
31+
}
32+
}

CLAUDE.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Overview
6+
7+
`puppetlabs-provision` is a Puppet module (published as `puppetlabs-provision` on the Forge) that ships Bolt **tasks** and **plans** for provisioning and tearing down test systems — Docker containers, Vagrant VMs, LXD containers, internal ABS pooler machines, and cloud VMs via the Provision Service. It is primarily consumed by [puppet_litmus](https://puppetlabs.github.io/content-and-tooling-team/docs/litmus/) for acceptance testing, but tasks can also be run directly with Bolt or plain Ruby.
8+
9+
The module's core job for every provisioner is the same: spin up (or destroy) a system, configure SSH/WinRM access on it, and add/remove a corresponding target entry in a Bolt `inventory.yaml` file.
10+
11+
## Commands
12+
13+
Development tooling is Ruby/PDK based. Always run through `bundle exec`.
14+
15+
```sh
16+
bundle install # install gems
17+
bundle exec rake spec # run all rspec unit/task tests
18+
bundle exec rspec spec/unit/inventory_helper_spec.rb # run a single spec file
19+
bundle exec rspec spec/tasks/docker_spec.rb -e "provision" # run specs matching a description
20+
bundle exec rake validate # puppet syntax + lint + metadata lint
21+
bundle exec rake lint # puppet-lint only
22+
bundle exec rubocop # Ruby style (tasks/, lib/, spec/)
23+
bundle exec rubocop -A # auto-correct Ruby style
24+
```
25+
26+
CI (`.github/workflows/ci.yml`) runs the shared `puppetlabs/cat-github-actions` module CI on Ruby 3.1 and additionally runs **shellcheck** against the `.sh` tasks.
27+
28+
### Running a task directly (development/debugging)
29+
30+
Tasks read JSON parameters from stdin. This is the fastest way to iterate on a task without Bolt:
31+
32+
```sh
33+
echo '{ "platform": "ubuntu:14.04", "action": "provision", "inventory": "/path/to/module/" }' | bundle exec ruby tasks/docker.rb
34+
```
35+
36+
### Running via Bolt
37+
38+
```sh
39+
bundle exec bolt task run provision::docker --targets localhost action=provision platform=ubuntu:14.04
40+
bundle exec bolt task run provision::docker --targets localhost action=tear_down node_name=localhost:2222
41+
```
42+
43+
`tear_down` requires `node_name`; `provision` requires `platform`. The two are mutually exclusive (enforced at the top of each task).
44+
45+
## Architecture
46+
47+
### Tasks (`tasks/`)
48+
49+
Each provisioner is a `<name>.json` (Puppet task metadata: parameter types, description, bundled `files`) paired with an implementation (`<name>.rb` for Ruby provisioners, `<name>.sh` for shell). The Ruby provisioners (`abs`, `docker`, `lxd`, `vagrant`, `provision_service`) all follow the same contract:
50+
51+
1. Parse JSON params from `$stdin` — typically `action`, `platform`, `node_name`, `inventory`, `vars`.
52+
2. Branch on `action` (`provision` vs `tear_down`).
53+
3. On success print a JSON result `{ status: 'ok', node_name: ... }`; on `StandardError` print `{ _error: { kind: ..., msg: ..., backtrace: ... } }` and `exit 1`. This `_error` envelope is the Bolt task error convention — preserve it.
54+
55+
The `.json` `files` array lists which `lib/` helpers Bolt bundles with the task; **if a task starts requiring a new helper, add it there** or it won't be shipped to the executor.
56+
57+
Shell tasks (`install_pe.sh`, `install_puppetserver.sh`, `fix_secure_path.sh`) run *on* the provisioned target rather than locally.
58+
59+
### Shared helpers (`lib/`)
60+
61+
These are plain Ruby (not Puppet functions) `require_relative`'d by the tasks:
62+
63+
- **`inventory_helper.rb`**`InventoryHelper` (a `SimpleDelegator` over the inventory Hash). The single source of truth for reading/mutating `inventory.yaml`. Use `InventoryHelper.open(location)` (memoized per-location), then `.add(node, group)`, `.lookup(...)`, `.remove(node)`, and `.save`. It resolves a directory argument to `<dir>/inventory.yaml` (with a deprecated fallback to `spec/fixtures/litmus_inventory.yaml` for old puppet_litmus). Provisioned Linux/SSH targets go in the `ssh_nodes` group; Windows/WinRM targets in `winrm_nodes`.
64+
- **`task_helper.rb`** — cross-cutting utilities: `run_local_command` (Open3 wrapper that raises on non-zero exit), `platform_is_windows?` / `platform_uses_ssh` (regex-based platform classification — note the documented edge cases in the comments), `on_windows?`, and `token_from_fogfile` (reads the ABS token from `~/.fog`).
65+
- **`docker_helper.rb`** — Docker-specific exec/inspect/teardown helpers used by `docker.rb`.
66+
67+
### Plans (`plans/*.pp`)
68+
69+
Puppet-language Bolt plans that orchestrate the tasks into higher-level workflows (e.g. `provisioner.pp` provisions a PE server plus agents, `tests_against_agents.pp` iterates inventory targets and runs `provision::run_tests` against each). Plans call tasks via `run_task('provision::<task>', ...)` and operate on inventory groups/roles set through the `vars` parameter (e.g. `vars='role: agent_linux'`).
70+
71+
### Hiera data (`data/`, `hiera.yaml`)
72+
73+
Module-level Hiera (v5) keyed by OS family/release, used by the `.sh` install tasks and plans to look up OS-specific values. `data/common.yaml` holds defaults.
74+
75+
## Conventions & gotchas
76+
77+
- **Puppet 8 only** (`>= 8.0.0 < 9.0.0`). Ruby files use `# frozen_string_literal: true`.
78+
- All Ruby files are linted by RuboCop with the puppetlabs config in `.rubocop.yml`; puppet-lint relative-classname and 140-char checks are disabled (see `Rakefile`).
79+
- The `vars` task parameter is a **YAML string**, parsed with `YAML.safe_load`, and merged into the inventory target's `vars`. Special keys are interpreted by provisioners (e.g. `docker_run_opts`, `role`, `vagrant_box_url`).
80+
- Many behaviors are configurable via environment variables as an alternative to task params (e.g. `DOCKER_HOST`, `VAGRANT_BOX_URL`, `VAGRANT_PASSWORD`, `LITMUS_ENABLE_SYNCED_FOLDER`, `LITMUS_HYPERV_VSWITCH`). Check both when changing provisioner behavior.
81+
- Generated reference docs live in `REFERENCE.md` (puppet-strings); `CHANGELOG.md` is generated by `github_changelog_generator`. Don't hand-edit either for routine changes.
82+
- `.fixtures.yml` symlinks this module and pulls `facts`, `puppet_conf`, and `puppet_agent` for spec runs.
83+
84+
## Hard Constraints
85+
86+
- Read the files relevant to a task before suggesting or making a change.
87+
- Never merge a PR.
88+
- Never work directly on `main` / `master`.
89+
- Never push without explicit instruction.
90+
- Never delete a file without permission — even after a blanket "yes to all".
91+
- Never output, log, save, or hardcode security-sensitive values: passwords, tokens, API keys, private keys, secrets, credentials. Don't write them to files, commit messages, or responses.

0 commit comments

Comments
 (0)