Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .actionlint.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
ignore:
# NINJA_MAX_JOBS is intentionally optional defined as "" at the workflow level
# NINJA_MAX_JOBS is intentionally optional -- defined as "" at the workflow level
# so it can be overridden by uncommenting and setting a value. An empty string is
# falsy in GHA expressions, so the build-arg is omitted when unset. actionlint
# flags env.NINJA_MAX_JOBS because the key is commented out in the top-level env:
# block and therefore cannot be statically verified.
- 'Context access might be invalid: NINJA_MAX_JOBS'

2 changes: 1 addition & 1 deletion .claude/agents/dockerfile-reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@ Review `Dockerfile` and `Dockerfile.devtools` for the following:

## Context

Cross-reference with `.hadolint.yaml` for intentionally suppressed rules do not flag issues that are already documented as acceptable.
Cross-reference with `.hadolint.yaml` for intentionally suppressed rules -- do not flag issues that are already documented as acceptable.

Report findings grouped by severity: Critical, Warning, Info.
14 changes: 0 additions & 14 deletions .claude/hooks/block-pipe-to-shell.sh

This file was deleted.

40 changes: 40 additions & 0 deletions .claude/hooks/check-dockerfile-heredoc-strict.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/bin/bash
# PostToolUse hook: Ensure Dockerfile RUN heredocs contain 'set -euo pipefail'.
# Source: CLAUDE.md - "Dockerfile RUN heredocs use set -euo pipefail"
set -euo pipefail

[[ -z "$CLAUDE_FILE_PATH" ]] && exit 0

case "$(basename "$CLAUDE_FILE_PATH")" in
Dockerfile*) ;;
*) exit 0 ;;
esac

# Find RUN heredocs missing 'set -euo pipefail'.
# Tracks heredoc boundaries by matching the delimiter word after << and
# looking for it alone on a line to close the block.
violations=$(awk '
/^[^#]*RUN.*<<[[:space:]]*[A-Za-z_]+/ {
tmp = $0
sub(/.*<<[[:space:]]*/, "", tmp)
sub(/[^A-Za-z_].*/, "", tmp)
delim = tmp
in_heredoc = 1
heredoc_start = NR
found_strict = 0
next
}
in_heredoc {
if ($0 ~ /set -euo pipefail/ && $0 !~ /^[[:space:]]*#/) found_strict = 1
if ($0 == delim) {
if (!found_strict) print heredoc_start": RUN heredoc (closed at line "NR") missing set -euo pipefail"
in_heredoc = 0
}
}
' "$CLAUDE_FILE_PATH")

if [[ -n "$violations" ]]; then
printf "Convention: Dockerfile RUN heredocs must include 'set -euo pipefail'. Violations in %s:\n" "$CLAUDE_FILE_PATH" >&2
printf "%s\n" "$violations" >&2
exit 1
fi
67 changes: 67 additions & 0 deletions .claude/hooks/check-lint-registration.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/bin/bash
# PostToolUse hook: Warn when a lintable file is not registered in lint.sh.
# Also flags stale entries (files listed in lint.sh that no longer exist).
set -euo pipefail

[[ -z "$CLAUDE_FILE_PATH" ]] && exit 0

cd "$(git rev-parse --show-toplevel)" || exit 1

# Only check lintable file types
case "$(basename "$CLAUDE_FILE_PATH")" in
Dockerfile*|*.sh|*.yml|*.yaml|*.json) ;;
*) exit 0 ;;
esac

# Get path relative to repo root; skip files outside the repo
repo_root=$(pwd)
if [[ "$CLAUDE_FILE_PATH" == "$repo_root"/* ]]; then
rel_path="${CLAUDE_FILE_PATH#"$repo_root"/}"
else
exit 0
fi

# Extract all file entries from lint.sh arrays
extract_entries() {
awk '
/^[A-Z_]+=\(/ && !/\)/ { in_arr=1; next }
in_arr && /^\)/ { in_arr=0; next }
in_arr {
gsub(/^[[:space:]]+/, "")
gsub(/[[:space:]]+$/, "")
if ($0 != "" && $0 !~ /^#/) print
}
' lint.sh
}

rc=0

# Check if this file is registered as an array entry (not just mentioned anywhere)
registered=false
while IFS= read -r entry; do
if [[ "$entry" == "$rel_path" ]]; then
registered=true
break
fi
done < <(extract_entries)

if [[ "$registered" == false ]]; then
printf "Convention: '%s' is not registered in lint.sh. Add it to the appropriate file list.\n" "$rel_path" >&2
rc=1
fi

# Check for stale entries (files listed but no longer on disk)
stale=""
while IFS= read -r entry; do
if [[ -n "$entry" && ! -f "$entry" ]]; then
stale+=" $entry"$'\n'
fi
done < <(extract_entries)

if [[ -n "$stale" ]]; then
printf "Stale entries in lint.sh (files no longer exist). Remove them:\n" >&2
printf "%s" "$stale" >&2
rc=1
fi

exit "$rc"
23 changes: 23 additions & 0 deletions .claude/hooks/check-shell-strict-mode.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash
# PostToolUse hook: Ensure shell scripts contain 'set -euo pipefail'.
# Exception: lint.sh uses 'set -uo pipefail' (omits -e for non-fail-fast design).

[[ -z "$CLAUDE_FILE_PATH" ]] && exit 0

case "$(basename "$CLAUDE_FILE_PATH")" in
*.sh) ;;
*) exit 0 ;;
esac

# lint.sh intentionally omits -e so all checks run before reporting a summary.
if [[ "$(basename "$CLAUDE_FILE_PATH")" == "lint.sh" ]]; then
if ! grep -qE '^[^#]*set -uo pipefail' "$CLAUDE_FILE_PATH"; then
echo "Convention: lint.sh must include 'set -uo pipefail'. Not found in $CLAUDE_FILE_PATH" >&2
exit 1
fi
else
if ! grep -qE '^[^#]*set -euo pipefail' "$CLAUDE_FILE_PATH"; then
echo "Convention: shell scripts must include 'set -euo pipefail'. Not found in $CLAUDE_FILE_PATH" >&2
exit 1
fi
fi
35 changes: 35 additions & 0 deletions .claude/hooks/check-trailing-whitespace.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/bin/bash
# PostToolUse hook: Flag trailing whitespace in changed files.
# Markdown exception: trailing two spaces after text (line break) is allowed.
set -euo pipefail

[[ -z "$CLAUDE_FILE_PATH" ]] && exit 0

# Skip binary files
case "$(basename "$CLAUDE_FILE_PATH")" in
*.png|*.jpg|*.jpeg|*.gif|*.ico|*.pdf|*.zip|*.tar*|*.gz|*.bz2|*.xz|*.woff*|*.ttf|*.eot|*.tgz) exit 0 ;;
esac

base=$(basename "$CLAUDE_FILE_PATH")

if [[ "$base" == *.md ]]; then
# In markdown, exactly 2 trailing spaces after non-whitespace text is a line break.
# Flag all other trailing whitespace (tabs, 1 space, 3+ spaces, whitespace-only lines).
violations=$(awk '
/[[:space:]]$/ {
line = $0
n = 0
while (n < length(line) && substr(line, length(line) - n, 1) == " ") n++
if (n == 2 && line ~ /[^[:space:]]/) next
print NR": "$0
}
' "$CLAUDE_FILE_PATH")
else
violations=$(grep -nE '[[:space:]]$' "$CLAUDE_FILE_PATH" || true)
fi

if [[ -n "$violations" ]]; then
printf "Convention: remove trailing whitespace. Violations in %s:\n" "$CLAUDE_FILE_PATH" >&2
printf "%s\n" "$violations" >&2
exit 1
fi
39 changes: 39 additions & 0 deletions .claude/hooks/check-unicode-lookalikes.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/bin/bash
# PostToolUse hook: Flag Unicode lookalikes that sneak in via autocorrect/copy-paste.
# Catches em/en dashes, smart quotes, non-breaking spaces, and ellipsis.
# Intentional Unicode (e.g., box-drawing chars in terminal output) is NOT matched.
set -euo pipefail

[[ -z "$CLAUDE_FILE_PATH" ]] && exit 0

case "$(basename "$CLAUDE_FILE_PATH")" in
*.sh|*.yml|*.yaml|*.json|*.md|Dockerfile*) ;;
*) exit 0 ;;
esac

# Build grep character class from UTF-8 byte sequences (keeps this script pure ASCII).
# U+00A0 non-breaking space
# U+2013 en dash
# U+2014 em dash
# U+2018 left single smart quote
# U+2019 right single smart quote
# U+201C left double smart quote
# U+201D right double smart quote
# U+2026 ellipsis
NBSP=$(printf '\xc2\xa0')
EN_DASH=$(printf '\xe2\x80\x93')
EM_DASH=$(printf '\xe2\x80\x94')
LSQUO=$(printf '\xe2\x80\x98')
RSQUO=$(printf '\xe2\x80\x99')
LDQUO=$(printf '\xe2\x80\x9c')
RDQUO=$(printf '\xe2\x80\x9d')
ELLIPSIS=$(printf '\xe2\x80\xa6')

violations=$(grep -n "[${NBSP}${EN_DASH}${EM_DASH}${LSQUO}${RSQUO}${LDQUO}${RDQUO}${ELLIPSIS}]" "$CLAUDE_FILE_PATH" || true)

if [[ -n "$violations" ]]; then
printf "Convention: avoid Unicode lookalikes (smart quotes, em/en dashes, etc.) in source files.\n" >&2
printf "Use ASCII equivalents instead. Violations in %s:\n" "$CLAUDE_FILE_PATH" >&2
printf "%s\n" "$violations" >&2
exit 1
fi
43 changes: 43 additions & 0 deletions .claude/hooks/check-workflow-expressions.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/bin/bash
# PostToolUse hook: Flag ${{ github.* }} in workflow run: blocks.
# These should use shell env vars set via the step's env: key.
#
# Checks for ${{ github.* }} on lines inside run: blocks. These should be
# replaced with shell env vars (e.g., $GITHUB_REF) set via the step's env: key.

[[ -z "$CLAUDE_FILE_PATH" ]] && exit 0

case "$(basename "$CLAUDE_FILE_PATH")" in
*.yml|*.yaml) ;;
*) exit 0 ;;
esac

# Use awk to find ${{ github.* }} only inside run: blocks.
# Track indentation to detect when a run: block ends.
violations=$(awk '
/^[[:space:]]+run:[[:space:]]*[|>]/ {
in_run = 1
match($0, /^[[:space:]]*/)
run_indent = RLENGTH
next
}
/^[[:space:]]+run:[[:space:]]*[^|>]/ {
# Single-line run: value
if ($0 ~ /\$\{\{[[:space:]]*github\./) print NR": "$0
next
}
in_run {
if (NF == 0) next
match($0, /^[[:space:]]*/)
cur_indent = RLENGTH
if (cur_indent <= run_indent && $0 ~ /[a-zA-Z_-]+:/) { in_run = 0; next }
if ($0 ~ /\$\{\{[[:space:]]*github\./) print NR": "$0
}
' "$CLAUDE_FILE_PATH")

if [[ -n "$violations" ]]; then
printf "Convention: use shell env vars (e.g., \$GITHUB_REF) instead of \${{ github.* }} in run: blocks.\n" >&2
printf "Set values via the step's env: key. Violations in %s:\n" "$CLAUDE_FILE_PATH" >&2
printf "%s\n" "$violations" >&2
exit 1
fi
10 changes: 10 additions & 0 deletions .claude/hooks/lint-changed-file.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/bash
# PostToolUse hook: Lint only the file that was just edited/written.
# Delegates to lint.sh --file for consistent linter args.
set -euo pipefail

[[ -z "$CLAUDE_FILE_PATH" ]] && exit 0

cd "$(git rev-parse --show-toplevel)" || exit 1

./lint.sh --file "$CLAUDE_FILE_PATH" 2>&1 | tail -20
37 changes: 25 additions & 12 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,36 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/block-pipe-to-shell.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "bash -c 'cd \"$(git rev-parse --show-toplevel)\" && file=\"$CLAUDE_FILE_PATH\"; case \"$file\" in Dockerfile*|*.sh|*.yml|*.yaml|*.json) bash lint.sh 2>&1 | tail -20 ;; esac; exit 0'"
"command": "./.claude/hooks/lint-changed-file.sh"
},
{
"type": "command",
"command": "./.claude/hooks/check-shell-strict-mode.sh"
},
{
"type": "command",
"command": "./.claude/hooks/check-workflow-expressions.sh"
},
{
"type": "command",
"command": "./.claude/hooks/check-lint-registration.sh"
},
{
"type": "command",
"command": "./.claude/hooks/check-dockerfile-heredoc-strict.sh"
},
{
"type": "command",
"command": "./.claude/hooks/check-unicode-lookalikes.sh"
},
{
"type": "command",
"command": "./.claude/hooks/check-trailing-whitespace.sh"
}
]
}
Expand Down
21 changes: 15 additions & 6 deletions .claude/skills/build/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: build
description: Build Docker images locally. Use after linting passes.
description: Build salt-dev Docker container images (base and devtools) via buildx with the salt-8cpu builder. Creates the builder if missing.
disable-model-invocation: true
---

Expand All @@ -9,8 +9,17 @@ disable-model-invocation: true

Build the project Docker images:

1. First run `bash lint.sh` — abort if any check fails
2. Build base image: `docker buildx build --pull -t salt-dev --load .`
3. If $ARGUMENTS contains "devtools" or "all":
- Build devtools: `docker buildx build -f Dockerfile.devtools -t salt-dev-tools --load .`
4. Report build success/failure and image sizes via `docker images | grep salt-dev`
1. Run `./lint.sh` -- abort if any check fails
2. Ensure the `salt-8cpu` builder exists:
- Check: `docker buildx inspect salt-8cpu 2>/dev/null`
- If missing, create and constrain to 8 CPUs:
```bash
docker buildx create --name salt-8cpu --driver docker-container --driver-opt default-load=true
docker buildx inspect --bootstrap salt-8cpu
docker update --cpus 8 "$(docker ps -qf 'name=buildx_buildkit_salt-8cpu')"
```
- Check memory: `docker info --format '{{.MemTotal}}'` -- warn user if < 22 GB
3. Build base image: `docker buildx build --builder salt-8cpu --pull -t salt-dev --load .`
4. If $ARGUMENTS contains "devtools" or "all":
- Build devtools: `docker buildx build --builder salt-8cpu -f Dockerfile.devtools -t salt-dev-tools --load .`
5. Report build success/failure and image sizes via `docker images | grep salt-dev`
17 changes: 0 additions & 17 deletions .claude/skills/lint/SKILL.md

This file was deleted.

Loading