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
54 changes: 54 additions & 0 deletions standards/check-claude-ignore.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# check-claude-ignore.sh <repo-dir>: warn-only (never fails); see dev-env#178.
set -euo pipefail

repo="${1:?usage: check-claude-ignore.sh <repo-dir>}"
probe=".claude/__standards_probe__"
ref="smartwatermelon/dev-env#178"
fix="Add '.claude/' to .gitignore (or '.claude/*' plus '!.claude/<path>' for shared files),"
fix+=" then 'git rm --cached' any tracked file listed. See ${ref}."

# Blank global excludes. .git/info/exclude still applies.
_git() { git -C "${repo}" -c core.excludesFile=/dev/null "$@"; }

# Encode %, CR, LF for annotations.
_esc() {
local s="$1"
s="${s//'%'/%25}"
s="${s//$'\r'/%0D}"
s="${s//$'\n'/%0A}"
printf '%s' "${s}"
}

if ! git -C "${repo}" rev-parse --git-dir >/dev/null 2>&1; then
echo "::error::${repo} is not a git repository"
exit 2
fi

warned=0

# check-ignore: 0 ignored, 1 not, else error.
rc=0
_git check-ignore -q --no-index -- "${probe}" || rc=$?
if ((rc == 1)); then
echo "::warning title=claude-ignore::$(_esc ".claude/ is not ignored by the committed .gitignore. ${fix}")"
warned=1
elif ((rc != 0)); then
echo "::warning title=claude-ignore::git check-ignore failed (exit ${rc}); not checked"
exit 0
fi

# Tracked files matching an ignore rule.
lsrc=0
tracked="$(_git ls-files -ci --exclude-standard -- .claude/)" || lsrc=$?
if ((lsrc != 0)); then
echo "::warning title=claude-ignore::git ls-files failed (exit ${lsrc}); tracked files not checked"
exit 0
fi
if [[ -n "${tracked}" ]]; then
echo "::warning title=claude-ignore::$(_esc "Tracked file(s) under .claude/ match an ignore rule:"$'\n'"${tracked}"$'\n'"${fix}")"
warned=1
fi

if ((warned == 0)); then echo ".claude/ ignore policy: OK"; fi
exit 0
8 changes: 8 additions & 0 deletions standards/run-standards.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
# opened fails their unrelated change. Measured across the fleet, that was the
# single largest source of standards-check failures.
#
# claude-ignore: warn-only, whole-repo.
#
# Scope: the flag narrows the four linters that enumerate through _tracked
# (shellcheck, yamllint, zizmor, markdownlint). actionlint and the Node-floor
# check find their own inputs and stay whole-repo — both are cheap, and
Expand Down Expand Up @@ -293,6 +295,12 @@ if _skipped node-floor; then echo "== node-floor: skipped by input"; else
_lint node-floor bash "${config_dir}/check-node-floor.sh" "${repo}" "${node_floor}"
fi

# claude-ignore: warning-only (dev-env#178); never adds to failed[].
if _skipped claude-ignore; then echo "== claude-ignore: skipped by input"; else
_header claude-ignore
bash "${config_dir}/check-claude-ignore.sh" "${repo}"
fi

echo
if ((${#failed[@]} > 0)); then
_write_summary
Expand Down
56 changes: 56 additions & 0 deletions tests/test-check-claude-ignore.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# Validation for standards/check-claude-ignore.sh on throwaway git repos.
# It only warns, so cases assert on output and exit 0.
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
checker="${here}/../standards/check-claude-ignore.sh"
# Isolate from the developer's global git config (hooks, excludes).
export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null
tmp="$(mktemp -d)"
trap 'rm -rf "${tmp}"' EXIT
pass=0; fail=0
_ok() { echo " ok $1"; pass=$((pass + 1)); }
_bad() { echo " FAIL $1"; fail=$((fail + 1)); }

# _repo NAME GITIGNORE-CONTENT [TRACKED-FILE...]: build a fixture repo.
_repo() {
local name="$1" ign="$2" d f
shift 2
d="${tmp}/${name}"
git init -q "${d}"
if [[ -n "${ign}" ]]; then printf '%b' "${ign}" >"${d}/.gitignore"; fi
mkdir -p "${d}/.claude"
for f in "$@"; do echo x >"${d}/${f}"; done
git -C "${d}" add -f -A
}

# _run NAME: run the checker; sets out and requires exit 0.
_run() {
out="$("${checker}" "${tmp}/$1" 2>&1)" || { _bad "$1: exit status nonzero"; return 1; }
}
_warns() { grep -q '^::warning title=claude-ignore::' <<<"${out}"; }

_repo nogi ""
_run nogi && { if _warns; then _ok "no .gitignore warns"; else _bad "no .gitignore did not warn"; fi; }

_repo clean '.claude/\n'
_run clean && { if _warns; then _bad "'.claude/' ignored warned"; else _ok "'.claude/' ignored is clean"; fi; }

_repo shared '.claude/*\n!.claude/pre-launch.sh\n' .claude/pre-launch.sh
_run shared && { if _warns; then _bad "negated shared file warned"; else _ok "'.claude/*' + negation + tracked shared file is clean"; fi; }

_repo tracked '.claude/\n' .claude/README.md
_run tracked && { if _warns && grep -q '\.claude/README\.md' <<<"${out}"; then _ok "tracked file under ignored .claude/ warns and is named"; else _bad "tracked file case missing warning or name"; fi; }

_repo nonegate '.claude/*\n' .claude/pre-launch.sh
_run nonegate && { if _warns && grep -q '\.claude/pre-launch\.sh' <<<"${out}"; then _ok "'.claude/*' without negation warns and names file"; else _bad "no-negation case missing warning or name"; fi; }

# A global excludes file must not mask a missing rule.
_repo masked ""
printf '.claude/\n' >"${tmp}/global-ignore"
printf '[core]\n\texcludesFile = %s\n' "${tmp}/global-ignore" >"${tmp}/gitconfig"
out="$(GIT_CONFIG_GLOBAL="${tmp}/gitconfig" "${checker}" "${tmp}/masked" 2>&1)"
if _warns; then _ok "global excludes does not mask a missing rule"; else _bad "global excludes masked the check"; fi

echo "${pass} passed, ${fail} failed"
[[ "${fail}" -eq 0 ]]
3 changes: 3 additions & 0 deletions tests/test-run-standards.sh
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ printf 'on: push\npermissions:\n contents: read\njobs:\n a:\n runs-on: ubun
echo "lts/krypton" >"${tmp}/clean/.nvmrc"
git -C "${tmp}/clean" add -A
_expect_pass "clean repo passes every linter" clean
# claude-ignore (dev-env#178) is warning-only: the clean fixture has no
# .gitignore, so the run must warn AND still pass.
if grep -q '^::warning title=claude-ignore::' "${tmp}/clean.log"; then _ok "claude-ignore warns without failing the run"; else _bad "claude-ignore warning missing from the clean run"; fi

if bash "${runner}" --repo "${tmp}/bad-sh" --config-dir "${cfg}" --skip shellcheck >/dev/null 2>&1; then _ok "--skip shellcheck disables the linter"; else _bad "--skip shellcheck did not disable it"; fi

Expand Down
Loading