Skip to content
Open
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
83 changes: 83 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
# ── General file hygiene ──────────────────────────────────────────────
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
args: ['--allow-multiple-documents']
exclude: ^mkdocs\.yml$
- id: check-added-large-files
args: ['--maxkb=1000']
- id: check-merge-conflict
- id: detect-private-key

# ── Python (ruff) ────────────────────────────────────────────────────
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.12
hooks:
- id: ruff-format
files: ^(components/runners/|scripts/)
types_or: [python]
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
files: ^(components/runners/|scripts/)
types_or: [python]

# ── Go ───────────────────────────────────────────────────────────────
- repo: local
hooks:
- id: gofmt-check
name: gofmt
entry: scripts/pre-commit/gofmt-check.sh
language: script
types: [go]
pass_filenames: true

- id: go-vet
name: go vet
entry: scripts/pre-commit/go-vet.sh
language: script
types: [go]
pass_filenames: true
require_serial: true

- id: golangci-lint
name: golangci-lint
entry: scripts/pre-commit/golangci-lint.sh
language: script
types: [go]
pass_filenames: true
require_serial: true

# ── Frontend (ESLint) ────────────────────────────────────────────────
- repo: local
hooks:
- id: eslint
name: eslint
entry: scripts/pre-commit/eslint.sh
language: script
files: ^components/frontend/.*\.(ts|tsx|js|jsx)$
pass_filenames: true

# ── Branch protection ────────────────────────────────────────────────
- repo: local
hooks:
- id: branch-protection
name: branch protection
entry: python3 scripts/git-hooks/pre-commit
language: system
always_run: true
pass_filenames: false
stages: [pre-commit]

- id: push-protection
name: push protection
entry: python3 scripts/git-hooks/pre-push
language: system
always_run: true
pass_filenames: false
stages: [pre-push]
73 changes: 57 additions & 16 deletions CLAUDE.md
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we cant let this type of change sneak in.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jeremyeder ++

How can we guard against this? CLAUDE.md and AGENTS.md mutations are gonna get weird 🤪

Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,55 @@ then review this PR for token handling issues."
instead of service accounts for API operations."
```

## Pre-commit Hooks

The project uses the [pre-commit](https://pre-commit.com/) framework to run linters locally before every commit. Configuration lives in `.pre-commit-config.yaml`.

### Install

```bash
make setup-hooks
```

### What Runs

**On every `git commit`:**

| Hook | Scope |
|------|-------|
| trailing-whitespace, end-of-file-fixer, check-yaml, check-added-large-files, check-merge-conflict, detect-private-key | All files |
| ruff-format, ruff (check + fix) | Python (runners, scripts) |
| gofmt, go vet, golangci-lint | Go (backend, operator, public-api — per-module) |
| eslint | Frontend TypeScript/JavaScript |
| branch-protection | Blocks commits to main/master/production |

**On every `git push`:**

| Hook | Scope |
|------|-------|
| push-protection | Blocks pushes to main/master/production |

### Run Manually

```bash
make lint # All hooks, all files
pre-commit run gofmt-check --all-files # Single hook
pre-commit run --files path/to/file.go # Single file
```

### Skip Hooks

```bash
git commit --no-verify # Skip pre-commit hooks
git push --no-verify # Skip pre-push hooks
```

### Notes

- Go and ESLint wrappers (`scripts/pre-commit/`) skip gracefully if the toolchain is not installed
- `tsc --noEmit` and `npm run build` are **not** included (slow; CI gates on them)
- Branch/push protection scripts remain in `scripts/git-hooks/` and are invoked by pre-commit

## Development Commands

### Quick Start - Local Development
Expand Down Expand Up @@ -891,31 +940,22 @@ Before committing backend or operator code, verify:
- [ ] **Status Updates**: Used `UpdateStatus` subresource, handled IsNotFound gracefully
- [ ] **Tests**: Added/updated tests for new functionality
- [ ] **Logging**: Structured logs with relevant context (namespace, resource name, etc.)
- [ ] **Code Quality**: Ran all linting checks locally (see below)
- [ ] **Code Quality**: Pre-commit hooks pass (`make lint` or commit triggers them automatically)

**Run these commands before committing:**
**Linting runs automatically** via pre-commit hooks on every commit. To run manually:

```bash
# Backend
cd components/backend
gofmt -l . # Check formatting (should output nothing)
go vet ./... # Detect suspicious constructs
golangci-lint run # Run comprehensive linting

# Operator
cd components/operator
gofmt -l .
go vet ./...
golangci-lint run
make lint # All hooks, all files
pre-commit run golangci-lint --all-files # Go linting only
```

**Auto-format code:**

```bash
gofmt -w components/backend components/operator
gofmt -w components/backend components/operator components/public-api
```

**Note**: GitHub Actions will automatically run these checks on your PR. Fix any issues locally before pushing.
**Note**: GitHub Actions will also run these checks on your PR.

### Common Mistakes to Avoid

Expand Down Expand Up @@ -1055,7 +1095,7 @@ make kind-down # Cleanup
- **Consolidated**: User journey tests, not isolated element checks
- **Real Agent Testing**: Verifies actual Claude responses (not hardcoded messages)

**Documentation**:
**Documentation**:
- [E2E Testing README](e2e/README.md) - Running tests
- [Kind Local Dev Guide](docs/developer/local-development/kind.md) - Using kind for development
- [E2E Testing Guide](docs/testing/e2e-guide.md) - Writing tests
Expand Down Expand Up @@ -1159,6 +1199,7 @@ Before committing frontend code:
- [ ] All routes have loading.tsx, error.tsx
- [ ] `npm run build` passes with 0 errors, 0 warnings
- [ ] All types use `type` instead of `interface`
- [ ] Pre-commit hooks pass (`make lint` or commit triggers them automatically)

### Reference Files

Expand Down
22 changes: 17 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ Before contributing, ensure you have:

### Install Git Hooks (Recommended)

To prevent accidental commits to protected branches (`main`, `master`, `production`), install our git hooks:
We use the [pre-commit](https://pre-commit.com/) framework to run linters and branch protection checks automatically on every commit. Install with:

```bash
make setup-hooks
Expand All @@ -106,12 +106,24 @@ Or run the installation script directly:
./scripts/install-git-hooks.sh
```

**What the hooks do:**
**What runs on every commit:**

- **pre-commit** - Blocks commits to `main`/`master`/`production` branches
- **pre-push** - Blocks pushes to `main`/`master`/`production` branches
- **File hygiene** - trailing whitespace, EOF fixer, YAML validation, large file check, merge conflict markers, private key detection
- **Python** - `ruff format` + `ruff check --fix` (runners and scripts)
- **Go** - `gofmt`, `go vet`, `golangci-lint` (backend, operator, public-api)
- **Frontend** - ESLint (TypeScript/JavaScript)
- **Branch protection** - blocks commits to `main`/`master`/`production`

**Hooks are automatically installed** when you run `make dev-start`.
**What runs on push:**

- **Push protection** - blocks pushes to `main`/`master`/`production`

**Run all hooks manually:**

```bash
make lint
# or: pre-commit run --all-files
```

If you need to override the hooks (e.g., for hotfixes):

Expand Down
22 changes: 16 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
.PHONY: local-dev-token
.PHONY: local-logs local-logs-backend local-logs-frontend local-logs-operator local-shell local-shell-frontend
.PHONY: local-test local-test-dev local-test-quick test-all local-url local-troubleshoot local-port-forward local-stop-port-forward
.PHONY: push-all registry-login setup-hooks remove-hooks check-minikube check-kind check-kubectl
.PHONY: push-all registry-login setup-hooks remove-hooks lint check-minikube check-kind check-kubectl
.PHONY: e2e-test e2e-setup e2e-clean deploy-langfuse-openshift
.PHONY: setup-minio minio-console minio-logs minio-status
.PHONY: validate-makefile lint-makefile check-shell makefile-health
Expand Down Expand Up @@ -153,17 +153,27 @@ build-public-api: ## Build public API gateway image
-t $(PUBLIC_API_IMAGE) .
@echo "$(COLOR_GREEN)✓$(COLOR_RESET) Public API built: $(PUBLIC_API_IMAGE)"

##@ Git Hooks
##@ Git Hooks & Linting

setup-hooks: ## Install git hooks for branch protection
setup-hooks: ## Install pre-commit hooks (linters + branch protection)
@./scripts/install-git-hooks.sh

remove-hooks: ## Remove git hooks
remove-hooks: ## Remove pre-commit hooks
@echo "$(COLOR_BLUE)▶$(COLOR_RESET) Removing git hooks..."
@rm -f .git/hooks/pre-commit
@rm -f .git/hooks/pre-push
@if command -v pre-commit >/dev/null 2>&1; then \
pre-commit uninstall && pre-commit uninstall --hook-type pre-push; \
else \
rm -f .git/hooks/pre-commit .git/hooks/pre-push; \
fi
@echo "$(COLOR_GREEN)✓$(COLOR_RESET) Git hooks removed"

lint: ## Run all pre-commit linters on the entire repo
@if ! command -v pre-commit >/dev/null 2>&1; then \
echo "$(COLOR_RED)✗$(COLOR_RESET) pre-commit not installed. Run: make setup-hooks"; \
exit 1; \
fi
pre-commit run --all-files

##@ Registry Operations

registry-login: ## Login to container registry
Expand Down
6 changes: 3 additions & 3 deletions components/runners/claude-code-runner/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ RUN dnf install -y 'dnf-command(config-manager)' && \
dnf install -y git jq && \
dnf clean all


# Install Node.js
# Use UBI AppStream to avoid conflicts with preinstalled nodejs-full-i18n
RUN dnf module reset -y nodejs && \
Expand Down Expand Up @@ -54,8 +54,8 @@ RUN dnf module reset -y nodejs && \
# "PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD": "0"
# }
# }
# Install uv (provides uvx for package execution)
RUN pip install --no-cache-dir uv
# Install uv (provides uvx for package execution) and pre-commit (for repo hooks)
RUN pip install --no-cache-dir uv pre-commit

# Create working directory
WORKDIR /app
Expand Down
31 changes: 17 additions & 14 deletions components/runners/state-sync/hydrate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ echo "S3 connection successful"
echo "Checking for existing session state in S3..."
if rclone --config /tmp/.config/rclone/rclone.conf lsf "${S3_PATH}/" 2>/dev/null | grep -q .; then
echo "Found existing session state, downloading from S3..."

# Download .claude data to /app/.claude (SubPath mount matches runner container)
if rclone --config /tmp/.config/rclone/rclone.conf lsf "${S3_PATH}/.claude/" 2>/dev/null | grep -q .; then
echo " Downloading .claude/..."
Expand All @@ -116,7 +116,7 @@ if rclone --config /tmp/.config/rclone/rclone.conf lsf "${S3_PATH}/" 2>/dev/null
else
echo " No data for .claude/"
fi

# Download other sync paths to /workspace
for path in "${SYNC_PATHS[@]}"; do
if rclone --config /tmp/.config/rclone/rclone.conf lsf "${S3_PATH}/${path}/" 2>/dev/null | grep -q .; then
Expand All @@ -130,7 +130,7 @@ if rclone --config /tmp/.config/rclone/rclone.conf lsf "${S3_PATH}/" 2>/dev/null
echo " No data for ${path}/"
fi
done

echo "State hydration complete!"
else
echo "No existing state found, starting fresh session"
Expand Down Expand Up @@ -179,20 +179,24 @@ if [ -n "$REPOS_JSON" ] && [ "$REPOS_JSON" != "null" ] && [ "$REPOS_JSON" != ""
while [ $i -lt $REPO_COUNT ]; do
REPO_URL=$(echo "$REPOS_JSON" | jq -r ".[$i].url // empty" 2>/dev/null || echo "")
REPO_BRANCH=$(echo "$REPOS_JSON" | jq -r ".[$i].branch // \"main\"" 2>/dev/null || echo "main")

# Derive repo name from URL
REPO_NAME=$(basename "$REPO_URL" .git 2>/dev/null || echo "")

if [ -n "$REPO_NAME" ] && [ -n "$REPO_URL" ] && [ "$REPO_URL" != "null" ]; then
REPO_DIR="/workspace/repos/$REPO_NAME"
echo " Cloning $REPO_NAME (branch: $REPO_BRANCH)..."

# Mark repo directory as safe
git config --global --add safe.directory "$REPO_DIR" 2>/dev/null || true

# Clone repository (for private repos, runner will handle token injection)
if git clone --branch "$REPO_BRANCH" --single-branch "$REPO_URL" "$REPO_DIR" 2>&1; then
echo " ✓ Cloned $REPO_NAME"
# Install pre-commit hooks if the repo has a config
if [ -f "$REPO_DIR/.pre-commit-config.yaml" ] && command -v pre-commit &>/dev/null; then
(cd "$REPO_DIR" && pre-commit install 2>/dev/null) || true
fi
else
echo " ⚠ Failed to clone $REPO_NAME (may require authentication)"
fi
Expand All @@ -208,31 +212,31 @@ fi
if [ -n "$ACTIVE_WORKFLOW_GIT_URL" ] && [ "$ACTIVE_WORKFLOW_GIT_URL" != "null" ]; then
WORKFLOW_BRANCH="${ACTIVE_WORKFLOW_BRANCH:-main}"
WORKFLOW_PATH="${ACTIVE_WORKFLOW_PATH:-}"

echo "Cloning workflow repository..."
echo " URL: $ACTIVE_WORKFLOW_GIT_URL"
echo " Branch: $WORKFLOW_BRANCH"
if [ -n "$WORKFLOW_PATH" ]; then
echo " Subpath: $WORKFLOW_PATH"
fi

# Derive workflow name from URL
WORKFLOW_NAME=$(basename "$ACTIVE_WORKFLOW_GIT_URL" .git)
WORKFLOW_FINAL="/workspace/workflows/${WORKFLOW_NAME}"
WORKFLOW_TEMP="/tmp/workflow-clone-$$"

git config --global --add safe.directory "$WORKFLOW_FINAL" 2>/dev/null || true

# Clone to temp location
if git clone --branch "$WORKFLOW_BRANCH" --single-branch "$ACTIVE_WORKFLOW_GIT_URL" "$WORKFLOW_TEMP" 2>&1; then
echo " Clone successful, processing..."

# Extract subpath if specified
if [ -n "$WORKFLOW_PATH" ]; then
SUBPATH_FULL="$WORKFLOW_TEMP/$WORKFLOW_PATH"
echo " Checking for subpath: $SUBPATH_FULL"
ls -la "$SUBPATH_FULL" 2>&1 || echo " Subpath does not exist"

if [ -d "$SUBPATH_FULL" ]; then
echo " Extracting subpath: $WORKFLOW_PATH"
mkdir -p "$(dirname "$WORKFLOW_FINAL")"
Expand Down Expand Up @@ -262,4 +266,3 @@ echo "========================================="
echo "Workspace initialized successfully"
echo "========================================="
exit 0

Loading
Loading