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
34 changes: 4 additions & 30 deletions .devtools/Makefile
Original file line number Diff line number Diff line change
@@ -1,44 +1,18 @@
# Single entry point for local development. Run from the repo root with
# make -C .devtools <target>
# CI runs the same hooks `check` runs, so local and CI results match.
include base.mk

ROOT := $(abspath ..)
UV := uv --project $(CURDIR)
PRE_COMMIT := $(UV) run pre-commit
PYTHON := $(UV) run --frozen python
# python.org Python builds on macOS ship without a CA bundle until "Install
# Certificates" is run; hook installs download toolchains over HTTPS, so fall back
# to certifi's bundle unless one is already configured.
export SSL_CERT_FILE ?= $(shell $(UV) run --frozen python -c 'import certifi; print(certifi.where())' 2>/dev/null)

.DEFAULT_GOAL := help
.PHONY: help setup lint validate build check scan lock clean

help: ## List targets
@grep -hE '^[a-zA-Z_-]+:.*## ' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'

setup: ## Install pinned tools and git hooks (pre-commit, commit-msg, pre-push)
$(UV) sync --frozen
cd $(ROOT) && $(PRE_COMMIT) install --install-hooks \
--hook-type pre-commit --hook-type commit-msg --hook-type pre-push

lint: ## Run every pre-commit hook on all files (includes validate)
cd $(ROOT) && $(PRE_COMMIT) run --all-files
.PHONY: validate build check lock clean

validate: ## Check every pack's layout, namespaces, JSON and references
$(PYTHON) $(CURDIR)/scripts/check-packs.py

build: ## Package every pack into dist/ (PACK=<slug> for one)
$(PYTHON) $(CURDIR)/scripts/build.py $(PACK)

check: lint build ## Everything CI runs, plus a build

scan: ## Secret and workflow security scans
cd $(ROOT) && $(PRE_COMMIT) run gitleaks --all-files
cd $(ROOT) && $(PRE_COMMIT) run zizmor --all-files
check: lint build ## Everything CI runs (lint includes validate), plus a build

lock: ## Refresh uv.lock after changing a tool version in pyproject.toml
$(UV) lock
lock: lock-tools ## Refresh every lockfile after a version change

clean: ## Remove build outputs
rm -rf $(ROOT)/dist
36 changes: 36 additions & 0 deletions .devtools/base.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Managed by studiobimo/project-template; change it there. Project targets go in Makefile.

ROOT := $(abspath ..)
UV := uv --project $(CURDIR)
PRE_COMMIT := $(UV) run pre-commit
# python.org Python builds on macOS ship without a CA bundle until "Install
# Certificates" is run; hook installs download toolchains over HTTPS, so fall back
# to certifi's bundle unless one is already configured.
export SSL_CERT_FILE ?= $(shell $(UV) run --frozen python -c 'import certifi; print(certifi.where())' 2>/dev/null)

.DEFAULT_GOAL := help
.PHONY: help setup lint scan lock-tools sync drift

help: ## List targets
@grep -hE '^[a-zA-Z_-]+:.*## ' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'

setup: ## Install pinned tools and git hooks (pre-commit, commit-msg, pre-push)
$(UV) sync --frozen
cd $(ROOT) && $(PRE_COMMIT) install --install-hooks \
--hook-type pre-commit --hook-type commit-msg --hook-type pre-push

lint: ## Run every pre-commit hook on all files
cd $(ROOT) && $(PRE_COMMIT) run --all-files

scan: ## Secret and workflow security scans
cd $(ROOT) && $(PRE_COMMIT) run gitleaks --all-files
cd $(ROOT) && $(PRE_COMMIT) run zizmor --all-files

lock-tools: ## Refresh uv.lock after changing a tool version in pyproject.toml
$(UV) lock

sync: ## Pull the files studiobimo/project-template manages (REF=<tag> for a specific release)
@$(CURDIR)/scripts/template-sync.sh $(if $(REF),--ref $(REF))

drift: ## Show where this repo differs from the template, without changing anything
@$(CURDIR)/scripts/template-sync.sh --check $(if $(REF),--ref $(REF))
66 changes: 42 additions & 24 deletions .devtools/scripts/agent-guard.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,65 +7,83 @@
# - gh pr create / gh stack submit / git push -> PR size limit
# - git checkout -b / git switch -c / git branch <name> -> Conventional Branch
#
# CI (pr-checks) remains the authoritative gate; this is a guardrail.
# The rules themselves are the org's pre-commit hooks (studiobimo/.github), run by
# id, so this file only decides when to ask. CI (pr-checks) remains the
# authoritative gate; this is a guardrail.
#
# Managed by studiobimo/project-template; change it there.
set -euo pipefail

here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
devtools="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"

command -v jq >/dev/null 2>&1 || exit 0
command -v uv >/dev/null 2>&1 || exit 0
cmd="$(jq -r '.tool_input.command // empty' 2>/dev/null || true)"
[[ -n "${cmd}" ]] || exit 0

block() {
printf '%s\n' "$1" >&2
exit 2
# run_hook <rule> <hook-id> [VAR=value ...]: blocks the call when the hook fails.
run_hook() {
local rule="$1" id="$2" out status=0
shift 2
out="$(env "$@" uv --project "${devtools}" run --frozen \
pre-commit run "${id}" --hook-stage manual 2>&1)" || status=$?
# 1 is the hook saying no. Anything else is pre-commit itself failing (tools not
# set up yet, the hook repo unreachable), which is not a reason to stop the agent.
if [[ "${status}" -eq 1 ]]; then
printf 'Blocked by agent-guard (%s):\n%s\n' "${rule}" "${out}" >&2
exit 2
fi
}

# Only the first line matters for matching; heredoc bodies can contain anything.
first_line="${cmd%%$'\n'*}"

# Help output never changes anything.
if [[ "${first_line}" =~ [[:space:]](--help|-h)([[:space:]]|$) ]]; then
exit 0
fi

# --- PR size -------------------------------------------------------------
if [[ "${first_line}" =~ (^|[[:space:]\;\&\|])(gh[[:space:]]+pr[[:space:]]+create|gh[[:space:]]+stack[[:space:]]+submit|git[[:space:]]+push)([[:space:]]|$) ]]; then
size_args=()
size_env=()
if [[ "${first_line}" =~ gh[[:space:]]+pr[[:space:]]+create.*(--base|-B)[[:space:]=]+[\"\']?([^[:space:]\"\']+) ]]; then
size_args=(--base "${BASH_REMATCH[2]}")
fi
if ! out="$("${here}/check-pr-size.sh" ${size_args[@]+"${size_args[@]}"} 2>&1)"; then
block "Blocked by agent-guard (PR size rule):
${out}"
size_env=("PR_BASE=${BASH_REMATCH[2]}")
fi
run_hook "PR size rule" pr-size ${size_env[@]+"${size_env[@]}"}
fi

# --- Branch names --------------------------------------------------------
new_branch=""
if [[ "${first_line}" =~ git[[:space:]]+(checkout|switch)[[:space:]]+(-b|-B|-c|-C|--create)[[:space:]]+[\"\']?([^[:space:];&|\"\']+) ]]; then
new_branch="${BASH_REMATCH[3]}"
elif [[ "${first_line}" =~ git[[:space:]]+branch[[:space:]]+(-m[[:space:]]+)?[\"\']?([^-[:space:];&|\"\'][^[:space:];&|\"\']*)[\"\']?([[:space:]]|$) ]]; then
new_branch="${BASH_REMATCH[2]}"
elif [[ "${first_line}" =~ git[[:space:]]+branch[[:space:]]+(-[mMcC]|--move|--copy)[[:space:]]+(.*)$ ]]; then
# Rename/copy: `<new>` alone renames the current branch; with `<old> <new>`
# the second name is the one being created.
args="${BASH_REMATCH[2]%%[;&|<>]*}"
args="${args% [0-9]}"
names=()
for b in ${args}; do
b="${b//[\"\']/}"
[[ -z "${b}" || "${b}" == -* ]] && continue
names+=("${b}")
done
if ((${#names[@]} >= 2)); then
new_branch="${names[1]}"
elif ((${#names[@]} == 1)); then
new_branch="${names[0]}"
fi
elif [[ "${first_line}" =~ git[[:space:]]+branch[[:space:]]+[\"\']?([^-[:space:];&|\"\'][^[:space:];&|\"\']*)[\"\']?([[:space:]]|$) ]]; then
new_branch="${BASH_REMATCH[1]}"
elif [[ "${first_line}" =~ gh[[:space:]]+stack[[:space:]]+(init|add)[[:space:]]+(.*)$ ]]; then
# Stop at the next command separator or redirection.
args="${BASH_REMATCH[2]%%[;&|<>]*}"
args="${args% [0-9]}"
for b in ${args}; do
b="${b//[\"\']/}"
[[ -z "${b}" || "${b}" == -* ]] && continue
if ! out="$("${here}/check-branch-name.sh" "${b}" 2>&1)"; then
block "Blocked by agent-guard (branch naming rule):
${out}"
fi
run_hook "branch naming rule" conventional-branch "BRANCH_NAME=${b}"
done
fi

if [[ -n "${new_branch}" ]]; then
if ! out="$("${here}/check-branch-name.sh" "${new_branch}" 2>&1)"; then
block "Blocked by agent-guard (branch naming rule):
${out}"
fi
run_hook "branch naming rule" conventional-branch "BRANCH_NAME=${new_branch}"
fi

exit 0
53 changes: 0 additions & 53 deletions .devtools/scripts/check-branch-name.sh

This file was deleted.

74 changes: 0 additions & 74 deletions .devtools/scripts/check-pr-size.sh

This file was deleted.

Loading
Loading