centralize guides and rails under mosaic with runtime compatibility links
This commit is contained in:
289
rails/bootstrap/agent-lint.sh
Executable file
289
rails/bootstrap/agent-lint.sh
Executable file
@@ -0,0 +1,289 @@
|
||||
#!/bin/bash
|
||||
# agent-lint.sh — Audit agent configuration across all coding projects
|
||||
#
|
||||
# Usage:
|
||||
# agent-lint.sh # Scan all projects in ~/src/
|
||||
# agent-lint.sh --project <path> # Scan single project
|
||||
# agent-lint.sh --json # Output JSON for jarvis-brain
|
||||
# agent-lint.sh --verbose # Show per-check details
|
||||
# agent-lint.sh --fix-hint # Show fix commands for failures
|
||||
#
|
||||
# Checks per project:
|
||||
# 1. Has CLAUDE.md?
|
||||
# 2. Has AGENTS.md?
|
||||
# 3. CLAUDE.md references agent-guides (conditional loading)?
|
||||
# 4. CLAUDE.md has quality gates?
|
||||
# 5. For monorepos: sub-directories have AGENTS.md?
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Defaults
|
||||
SRC_DIR="$HOME/src"
|
||||
SINGLE_PROJECT=""
|
||||
JSON_OUTPUT=false
|
||||
VERBOSE=false
|
||||
FIX_HINT=false
|
||||
|
||||
# Exclusion patterns (not coding projects)
|
||||
EXCLUDE_PATTERNS=(
|
||||
"_worktrees"
|
||||
".backup"
|
||||
"_old"
|
||||
"_bak"
|
||||
"junk"
|
||||
"traefik"
|
||||
"infrastructure"
|
||||
)
|
||||
|
||||
# Parse args
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--project) SINGLE_PROJECT="$2"; shift 2 ;;
|
||||
--json) JSON_OUTPUT=true; shift ;;
|
||||
--verbose) VERBOSE=true; shift ;;
|
||||
--fix-hint) FIX_HINT=true; shift ;;
|
||||
--src-dir) SRC_DIR="$2"; shift 2 ;;
|
||||
-h|--help)
|
||||
echo "Usage: agent-lint.sh [--project <path>] [--json] [--verbose] [--fix-hint] [--src-dir <dir>]"
|
||||
exit 0
|
||||
;;
|
||||
*) echo "Unknown option: $1"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Colors (disabled for JSON mode)
|
||||
if $JSON_OUTPUT; then
|
||||
GREEN="" RED="" YELLOW="" NC="" BOLD="" DIM=""
|
||||
else
|
||||
GREEN='\033[0;32m' RED='\033[0;31m' YELLOW='\033[0;33m'
|
||||
NC='\033[0m' BOLD='\033[1m' DIM='\033[2m'
|
||||
fi
|
||||
|
||||
# Determine if a directory is a coding project
|
||||
is_coding_project() {
|
||||
local dir="$1"
|
||||
[[ -f "$dir/package.json" ]] || \
|
||||
[[ -f "$dir/pyproject.toml" ]] || \
|
||||
[[ -f "$dir/Cargo.toml" ]] || \
|
||||
[[ -f "$dir/go.mod" ]] || \
|
||||
[[ -f "$dir/Makefile" && -f "$dir/src/main.rs" ]] || \
|
||||
[[ -f "$dir/pom.xml" ]] || \
|
||||
[[ -f "$dir/build.gradle" ]]
|
||||
}
|
||||
|
||||
# Check if directory should be excluded
|
||||
is_excluded() {
|
||||
local dir_name
|
||||
dir_name=$(basename "$1")
|
||||
for pattern in "${EXCLUDE_PATTERNS[@]}"; do
|
||||
if [[ "$dir_name" == *"$pattern"* ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Detect if project is a monorepo
|
||||
is_monorepo() {
|
||||
local dir="$1"
|
||||
[[ -f "$dir/pnpm-workspace.yaml" ]] || \
|
||||
[[ -f "$dir/turbo.json" ]] || \
|
||||
[[ -f "$dir/lerna.json" ]] || \
|
||||
(grep -q '"workspaces"' "$dir/package.json" 2>/dev/null)
|
||||
}
|
||||
|
||||
# Check for CLAUDE.md
|
||||
check_claude_md() {
|
||||
[[ -f "$1/CLAUDE.md" ]]
|
||||
}
|
||||
|
||||
# Check for AGENTS.md
|
||||
check_agents_md() {
|
||||
[[ -f "$1/AGENTS.md" ]]
|
||||
}
|
||||
|
||||
# Check conditional loading (references agent-guides)
|
||||
check_conditional_loading() {
|
||||
local claude_md="$1/CLAUDE.md"
|
||||
[[ -f "$claude_md" ]] && grep -qi "agent-guides\|conditional.*loading\|conditional.*documentation" "$claude_md" 2>/dev/null
|
||||
}
|
||||
|
||||
# Check quality gates
|
||||
check_quality_gates() {
|
||||
local claude_md="$1/CLAUDE.md"
|
||||
[[ -f "$claude_md" ]] && grep -qi "quality.gates\|must pass before\|lint\|typecheck\|test" "$claude_md" 2>/dev/null
|
||||
}
|
||||
|
||||
# Check monorepo sub-AGENTS.md
|
||||
check_monorepo_sub_agents() {
|
||||
local dir="$1"
|
||||
local missing=()
|
||||
|
||||
if ! is_monorepo "$dir"; then
|
||||
echo "N/A"
|
||||
return
|
||||
fi
|
||||
|
||||
# Check apps/, packages/, services/, plugins/ directories
|
||||
for subdir_type in apps packages services plugins; do
|
||||
if [[ -d "$dir/$subdir_type" ]]; then
|
||||
for subdir in "$dir/$subdir_type"/*/; do
|
||||
[[ -d "$subdir" ]] || continue
|
||||
# Only check if it has its own manifest
|
||||
if [[ -f "$subdir/package.json" ]] || [[ -f "$subdir/pyproject.toml" ]]; then
|
||||
if [[ ! -f "$subdir/AGENTS.md" ]]; then
|
||||
missing+=("$(basename "$subdir")")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#missing[@]} -eq 0 ]]; then
|
||||
echo "OK"
|
||||
else
|
||||
echo "MISS:${missing[*]}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Lint a single project
|
||||
lint_project() {
|
||||
local dir="$1"
|
||||
local name
|
||||
name=$(basename "$dir")
|
||||
|
||||
local has_claude has_agents has_guides has_quality mono_status
|
||||
local score=0 max_score=4
|
||||
|
||||
check_claude_md "$dir" && has_claude="OK" || has_claude="MISS"
|
||||
check_agents_md "$dir" && has_agents="OK" || has_agents="MISS"
|
||||
check_conditional_loading "$dir" && has_guides="OK" || has_guides="MISS"
|
||||
check_quality_gates "$dir" && has_quality="OK" || has_quality="MISS"
|
||||
mono_status=$(check_monorepo_sub_agents "$dir")
|
||||
|
||||
[[ "$has_claude" == "OK" ]] && ((score++)) || true
|
||||
[[ "$has_agents" == "OK" ]] && ((score++)) || true
|
||||
[[ "$has_guides" == "OK" ]] && ((score++)) || true
|
||||
[[ "$has_quality" == "OK" ]] && ((score++)) || true
|
||||
|
||||
if $JSON_OUTPUT; then
|
||||
cat <<JSONEOF
|
||||
{
|
||||
"project": "$name",
|
||||
"path": "$dir",
|
||||
"claude_md": "$has_claude",
|
||||
"agents_md": "$has_agents",
|
||||
"conditional_loading": "$has_guides",
|
||||
"quality_gates": "$has_quality",
|
||||
"monorepo_sub_agents": "$mono_status",
|
||||
"score": $score,
|
||||
"max_score": $max_score
|
||||
}
|
||||
JSONEOF
|
||||
else
|
||||
# Color-code the status
|
||||
local c_claude c_agents c_guides c_quality
|
||||
[[ "$has_claude" == "OK" ]] && c_claude="${GREEN} OK ${NC}" || c_claude="${RED} MISS ${NC}"
|
||||
[[ "$has_agents" == "OK" ]] && c_agents="${GREEN} OK ${NC}" || c_agents="${RED} MISS ${NC}"
|
||||
[[ "$has_guides" == "OK" ]] && c_guides="${GREEN} OK ${NC}" || c_guides="${RED} MISS ${NC}"
|
||||
[[ "$has_quality" == "OK" ]] && c_quality="${GREEN} OK ${NC}" || c_quality="${RED} MISS ${NC}"
|
||||
|
||||
local score_color="$RED"
|
||||
[[ $score -ge 3 ]] && score_color="$YELLOW"
|
||||
[[ $score -eq 4 ]] && score_color="$GREEN"
|
||||
|
||||
printf " %-35s %b %b %b %b ${score_color}%d/%d${NC}" \
|
||||
"$name" "$c_claude" "$c_agents" "$c_guides" "$c_quality" "$score" "$max_score"
|
||||
|
||||
# Show monorepo status if applicable
|
||||
if [[ "$mono_status" != "N/A" && "$mono_status" != "OK" ]]; then
|
||||
printf " ${YELLOW}(mono: %s)${NC}" "$mono_status"
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if $VERBOSE && ! $JSON_OUTPUT; then
|
||||
[[ "$has_claude" == "MISS" ]] && echo " ${DIM} CLAUDE.md missing${NC}"
|
||||
[[ "$has_agents" == "MISS" ]] && echo " ${DIM} AGENTS.md missing${NC}"
|
||||
[[ "$has_guides" == "MISS" ]] && echo " ${DIM} No conditional loading (agent-guides not referenced)${NC}"
|
||||
[[ "$has_quality" == "MISS" ]] && echo " ${DIM} No quality gates section${NC}"
|
||||
if [[ "$mono_status" == MISS:* ]]; then
|
||||
echo " ${DIM} Monorepo sub-AGENTS.md missing: ${mono_status#MISS:}${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if $FIX_HINT && ! $JSON_OUTPUT; then
|
||||
if [[ "$has_claude" == "MISS" || "$has_agents" == "MISS" ]]; then
|
||||
echo " ${DIM}Fix: ~/.mosaic/rails/bootstrap/init-project.sh --name \"$name\" --type auto${NC}"
|
||||
elif [[ "$has_guides" == "MISS" ]]; then
|
||||
echo " ${DIM}Fix: ~/.mosaic/rails/bootstrap/agent-upgrade.sh $dir --section conditional-loading${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Return score for summary
|
||||
echo "$score" > /tmp/agent-lint-score-$$
|
||||
}
|
||||
|
||||
# Main
|
||||
main() {
|
||||
local projects=()
|
||||
local total=0 passing=0 total_score=0
|
||||
|
||||
if [[ -n "$SINGLE_PROJECT" ]]; then
|
||||
projects=("$SINGLE_PROJECT")
|
||||
else
|
||||
for dir in "$SRC_DIR"/*/; do
|
||||
[[ -d "$dir" ]] || continue
|
||||
is_excluded "$dir" && continue
|
||||
is_coding_project "$dir" && projects+=("${dir%/}")
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ ${#projects[@]} -eq 0 ]]; then
|
||||
echo "No coding projects found."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if $JSON_OUTPUT; then
|
||||
echo '{ "audit_date": "'$(date -I)'", "projects": ['
|
||||
local first=true
|
||||
for dir in "${projects[@]}"; do
|
||||
$first || echo ","
|
||||
first=false
|
||||
lint_project "$dir"
|
||||
done
|
||||
echo '] }'
|
||||
else
|
||||
echo ""
|
||||
echo -e "${BOLD}Agent Configuration Audit — $(date +%Y-%m-%d)${NC}"
|
||||
echo "========================================================"
|
||||
printf " %-35s %s %s %s %s %s\n" \
|
||||
"Project" "CLAUDE" "AGENTS" "Guides" "Quality" "Score"
|
||||
echo " -----------------------------------------------------------------------"
|
||||
|
||||
for dir in "${projects[@]}"; do
|
||||
lint_project "$dir"
|
||||
local score
|
||||
score=$(cat /tmp/agent-lint-score-$$ 2>/dev/null || echo 0)
|
||||
((total++)) || true
|
||||
((total_score += score)) || true
|
||||
[[ $score -eq 4 ]] && ((passing++)) || true
|
||||
done
|
||||
|
||||
rm -f /tmp/agent-lint-score-$$
|
||||
|
||||
echo " -----------------------------------------------------------------------"
|
||||
local need_attention=$((total - passing))
|
||||
echo ""
|
||||
echo -e " ${BOLD}Summary:${NC} $total projects | ${GREEN}$passing pass${NC} | ${RED}$need_attention need attention${NC}"
|
||||
echo ""
|
||||
|
||||
if [[ $need_attention -gt 0 ]] && ! $FIX_HINT; then
|
||||
echo -e " ${DIM}Run with --fix-hint for suggested fixes${NC}"
|
||||
echo -e " ${DIM}Run with --verbose for per-check details${NC}"
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
main
|
||||
318
rails/bootstrap/agent-upgrade.sh
Executable file
318
rails/bootstrap/agent-upgrade.sh
Executable file
@@ -0,0 +1,318 @@
|
||||
#!/bin/bash
|
||||
# agent-upgrade.sh — Non-destructively upgrade agent configuration in projects
|
||||
#
|
||||
# Usage:
|
||||
# agent-upgrade.sh <project-path> # Upgrade one project
|
||||
# agent-upgrade.sh --all # Upgrade all projects in ~/src/
|
||||
# agent-upgrade.sh --all --dry-run # Preview what would change
|
||||
# agent-upgrade.sh <path> --section conditional-loading # Inject specific section
|
||||
# agent-upgrade.sh <path> --create-agents # Create AGENTS.md if missing
|
||||
# agent-upgrade.sh <path> --monorepo-scan # Create sub-AGENTS.md for monorepo dirs
|
||||
#
|
||||
# Safety:
|
||||
# - Creates .bak backup before any modification
|
||||
# - Append-only — never modifies existing sections
|
||||
# - --dry-run shows what would change without writing
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Defaults
|
||||
SRC_DIR="$HOME/src"
|
||||
FRAGMENTS_DIR="$HOME/.mosaic/templates/agent/fragments"
|
||||
TEMPLATES_DIR="$HOME/.mosaic/templates/agent"
|
||||
DRY_RUN=false
|
||||
ALL_PROJECTS=false
|
||||
TARGET_PATH=""
|
||||
SECTION_ONLY=""
|
||||
CREATE_AGENTS=false
|
||||
MONOREPO_SCAN=false
|
||||
|
||||
# Exclusion patterns (same as agent-lint.sh)
|
||||
EXCLUDE_PATTERNS=(
|
||||
"_worktrees"
|
||||
".backup"
|
||||
"_old"
|
||||
"_bak"
|
||||
"junk"
|
||||
"traefik"
|
||||
"infrastructure"
|
||||
)
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m' RED='\033[0;31m' YELLOW='\033[0;33m'
|
||||
NC='\033[0m' BOLD='\033[1m' DIM='\033[2m'
|
||||
|
||||
# Parse args
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--all) ALL_PROJECTS=true; shift ;;
|
||||
--dry-run) DRY_RUN=true; shift ;;
|
||||
--section) SECTION_ONLY="$2"; shift 2 ;;
|
||||
--create-agents) CREATE_AGENTS=true; shift ;;
|
||||
--monorepo-scan) MONOREPO_SCAN=true; shift ;;
|
||||
--src-dir) SRC_DIR="$2"; shift 2 ;;
|
||||
-h|--help)
|
||||
echo "Usage: agent-upgrade.sh [<project-path>|--all] [--dry-run] [--section <name>] [--create-agents] [--monorepo-scan]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --all Upgrade all projects in ~/src/"
|
||||
echo " --dry-run Preview changes without writing"
|
||||
echo " --section <name> Inject only a specific fragment (conditional-loading, commit-format, secrets, multi-agent, code-review, campsite-rule)"
|
||||
echo " --create-agents Create AGENTS.md if missing"
|
||||
echo " --monorepo-scan Create sub-AGENTS.md for monorepo directories"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
if [[ -d "$1" ]]; then
|
||||
TARGET_PATH="$1"
|
||||
else
|
||||
echo "Unknown option or invalid path: $1"
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ! $ALL_PROJECTS && [[ -z "$TARGET_PATH" ]]; then
|
||||
echo "Error: Specify a project path or use --all"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Helpers
|
||||
is_coding_project() {
|
||||
local dir="$1"
|
||||
[[ -f "$dir/package.json" ]] || \
|
||||
[[ -f "$dir/pyproject.toml" ]] || \
|
||||
[[ -f "$dir/Cargo.toml" ]] || \
|
||||
[[ -f "$dir/go.mod" ]] || \
|
||||
[[ -f "$dir/pom.xml" ]] || \
|
||||
[[ -f "$dir/build.gradle" ]]
|
||||
}
|
||||
|
||||
is_excluded() {
|
||||
local dir_name
|
||||
dir_name=$(basename "$1")
|
||||
for pattern in "${EXCLUDE_PATTERNS[@]}"; do
|
||||
[[ "$dir_name" == *"$pattern"* ]] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
is_monorepo() {
|
||||
local dir="$1"
|
||||
[[ -f "$dir/pnpm-workspace.yaml" ]] || \
|
||||
[[ -f "$dir/turbo.json" ]] || \
|
||||
[[ -f "$dir/lerna.json" ]] || \
|
||||
(grep -q '"workspaces"' "$dir/package.json" 2>/dev/null)
|
||||
}
|
||||
|
||||
has_section() {
|
||||
local file="$1"
|
||||
local pattern="$2"
|
||||
[[ -f "$file" ]] && grep -qi "$pattern" "$file" 2>/dev/null
|
||||
}
|
||||
|
||||
backup_file() {
|
||||
local file="$1"
|
||||
if [[ -f "$file" ]] && ! $DRY_RUN; then
|
||||
cp "$file" "${file}.bak"
|
||||
fi
|
||||
}
|
||||
|
||||
# Inject a fragment into CLAUDE.md if the section doesn't exist
|
||||
inject_fragment() {
|
||||
local project_dir="$1"
|
||||
local fragment_name="$2"
|
||||
local claude_md="$project_dir/CLAUDE.md"
|
||||
local fragment_file="$FRAGMENTS_DIR/$fragment_name.md"
|
||||
|
||||
if [[ ! -f "$fragment_file" ]]; then
|
||||
echo -e " ${RED}Fragment not found: $fragment_file${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Determine detection pattern for this fragment
|
||||
local detect_pattern
|
||||
case "$fragment_name" in
|
||||
conditional-loading) detect_pattern="agent-guides\|Conditional.*Loading\|Conditional.*Documentation" ;;
|
||||
commit-format) detect_pattern="<type>.*#issue\|Types:.*feat.*fix" ;;
|
||||
secrets) detect_pattern="NEVER hardcode secrets\|\.env.example.*committed" ;;
|
||||
multi-agent) detect_pattern="Multi-Agent Coordination\|pull --rebase.*before" ;;
|
||||
code-review) detect_pattern="codex-code-review\|codex-security-review\|Code Review" ;;
|
||||
campsite-rule) detect_pattern="Campsite Rule\|Touching it makes it yours\|was already there.*NEVER" ;;
|
||||
*) echo "Unknown fragment: $fragment_name"; return 1 ;;
|
||||
esac
|
||||
|
||||
if [[ ! -f "$claude_md" ]]; then
|
||||
echo -e " ${YELLOW}No CLAUDE.md — skipping fragment injection${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if has_section "$claude_md" "$detect_pattern"; then
|
||||
echo -e " ${DIM}$fragment_name already present${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if $DRY_RUN; then
|
||||
echo -e " ${GREEN}Would inject: $fragment_name${NC}"
|
||||
else
|
||||
backup_file "$claude_md"
|
||||
echo "" >> "$claude_md"
|
||||
cat "$fragment_file" >> "$claude_md"
|
||||
echo "" >> "$claude_md"
|
||||
echo -e " ${GREEN}Injected: $fragment_name${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Create AGENTS.md from template
|
||||
create_agents_md() {
|
||||
local project_dir="$1"
|
||||
local agents_md="$project_dir/AGENTS.md"
|
||||
|
||||
if [[ -f "$agents_md" ]]; then
|
||||
echo -e " ${DIM}AGENTS.md already exists${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local project_name
|
||||
project_name=$(basename "$project_dir")
|
||||
|
||||
# Detect project type for quality gates
|
||||
local quality_gates="# Add quality gate commands here"
|
||||
if [[ -f "$project_dir/package.json" ]]; then
|
||||
quality_gates="npm run lint && npm run typecheck && npm test"
|
||||
if grep -q '"pnpm"' "$project_dir/package.json" 2>/dev/null || [[ -f "$project_dir/pnpm-lock.yaml" ]]; then
|
||||
quality_gates="pnpm lint && pnpm typecheck && pnpm test"
|
||||
fi
|
||||
elif [[ -f "$project_dir/pyproject.toml" ]]; then
|
||||
quality_gates="uv run ruff check src/ tests/ && uv run mypy src/ && uv run pytest --cov"
|
||||
fi
|
||||
|
||||
if $DRY_RUN; then
|
||||
echo -e " ${GREEN}Would create: AGENTS.md${NC}"
|
||||
else
|
||||
# Use generic AGENTS.md template with substitutions
|
||||
sed -e "s/\${PROJECT_NAME}/$project_name/g" \
|
||||
-e "s/\${QUALITY_GATES}/$quality_gates/g" \
|
||||
-e "s/\${TASK_PREFIX}/${project_name^^}/g" \
|
||||
-e "s|\${SOURCE_DIR}|src|g" \
|
||||
"$TEMPLATES_DIR/AGENTS.md.template" > "$agents_md"
|
||||
echo -e " ${GREEN}Created: AGENTS.md${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Create sub-AGENTS.md for monorepo directories
|
||||
create_sub_agents() {
|
||||
local project_dir="$1"
|
||||
|
||||
if ! is_monorepo "$project_dir"; then
|
||||
echo -e " ${DIM}Not a monorepo — skipping sub-AGENTS scan${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local created=0
|
||||
for subdir_type in apps packages services plugins; do
|
||||
if [[ -d "$project_dir/$subdir_type" ]]; then
|
||||
for subdir in "$project_dir/$subdir_type"/*/; do
|
||||
[[ -d "$subdir" ]] || continue
|
||||
# Only if it has its own manifest
|
||||
if [[ -f "$subdir/package.json" ]] || [[ -f "$subdir/pyproject.toml" ]]; then
|
||||
if [[ ! -f "$subdir/AGENTS.md" ]]; then
|
||||
local dir_name
|
||||
dir_name=$(basename "$subdir")
|
||||
if $DRY_RUN; then
|
||||
echo -e " ${GREEN}Would create: $subdir_type/$dir_name/AGENTS.md${NC}"
|
||||
else
|
||||
sed -e "s/\${DIRECTORY_NAME}/$dir_name/g" \
|
||||
-e "s/\${DIRECTORY_PURPOSE}/Part of the $subdir_type layer./g" \
|
||||
"$TEMPLATES_DIR/sub-agents.md.template" > "${subdir}AGENTS.md"
|
||||
echo -e " ${GREEN}Created: $subdir_type/$dir_name/AGENTS.md${NC}"
|
||||
fi
|
||||
((created++)) || true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $created -eq 0 ]]; then
|
||||
echo -e " ${DIM}All monorepo sub-AGENTS.md present${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Upgrade a single project
|
||||
upgrade_project() {
|
||||
local dir="$1"
|
||||
local name
|
||||
name=$(basename "$dir")
|
||||
|
||||
echo -e "\n${BOLD}$name${NC} ${DIM}($dir)${NC}"
|
||||
|
||||
if [[ -n "$SECTION_ONLY" ]]; then
|
||||
inject_fragment "$dir" "$SECTION_ONLY"
|
||||
return
|
||||
fi
|
||||
|
||||
# Always try conditional-loading (highest impact)
|
||||
inject_fragment "$dir" "conditional-loading"
|
||||
|
||||
# Try other fragments if CLAUDE.md exists
|
||||
if [[ -f "$dir/CLAUDE.md" ]]; then
|
||||
inject_fragment "$dir" "commit-format"
|
||||
inject_fragment "$dir" "secrets"
|
||||
inject_fragment "$dir" "multi-agent"
|
||||
inject_fragment "$dir" "code-review"
|
||||
inject_fragment "$dir" "campsite-rule"
|
||||
fi
|
||||
|
||||
# Create AGENTS.md if missing (always unless --section was used)
|
||||
if $CREATE_AGENTS || [[ -z "$SECTION_ONLY" ]]; then
|
||||
create_agents_md "$dir"
|
||||
fi
|
||||
|
||||
# Monorepo sub-AGENTS.md
|
||||
if $MONOREPO_SCAN || [[ -z "$SECTION_ONLY" ]]; then
|
||||
create_sub_agents "$dir"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main
|
||||
main() {
|
||||
local projects=()
|
||||
|
||||
if $ALL_PROJECTS; then
|
||||
for dir in "$SRC_DIR"/*/; do
|
||||
[[ -d "$dir" ]] || continue
|
||||
is_excluded "$dir" && continue
|
||||
is_coding_project "$dir" && projects+=("${dir%/}")
|
||||
done
|
||||
else
|
||||
projects=("$TARGET_PATH")
|
||||
fi
|
||||
|
||||
if [[ ${#projects[@]} -eq 0 ]]; then
|
||||
echo "No coding projects found."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
local mode="LIVE"
|
||||
$DRY_RUN && mode="DRY RUN"
|
||||
|
||||
echo -e "${BOLD}Agent Configuration Upgrade — $(date +%Y-%m-%d) [$mode]${NC}"
|
||||
echo "========================================================"
|
||||
|
||||
for dir in "${projects[@]}"; do
|
||||
upgrade_project "$dir"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD}Done.${NC}"
|
||||
if $DRY_RUN; then
|
||||
echo -e "${DIM}Run without --dry-run to apply changes.${NC}"
|
||||
else
|
||||
echo -e "${DIM}Backups saved as .bak files. Run agent-lint.sh to verify.${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
main
|
||||
463
rails/bootstrap/init-project.sh
Executable file
463
rails/bootstrap/init-project.sh
Executable file
@@ -0,0 +1,463 @@
|
||||
#!/bin/bash
|
||||
# init-project.sh - Bootstrap a project for AI-assisted development
|
||||
# Usage: init-project.sh [OPTIONS]
|
||||
#
|
||||
# Creates CLAUDE.md, AGENTS.md, and standard directories using templates.
|
||||
# Optionally initializes git labels and milestones.
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TEMPLATE_DIR="$HOME/.mosaic/templates/agent"
|
||||
GIT_SCRIPT_DIR="$HOME/.mosaic/rails/git"
|
||||
|
||||
# Defaults
|
||||
PROJECT_NAME=""
|
||||
PROJECT_TYPE=""
|
||||
REPO_URL=""
|
||||
TASK_PREFIX=""
|
||||
PROJECT_DESCRIPTION=""
|
||||
SKIP_LABELS=false
|
||||
SKIP_CI=false
|
||||
CICD_DOCKER=false
|
||||
DRY_RUN=false
|
||||
declare -a CICD_SERVICES=()
|
||||
CICD_BRANCHES="main,develop"
|
||||
|
||||
show_help() {
|
||||
cat <<'EOF'
|
||||
Usage: init-project.sh [OPTIONS]
|
||||
|
||||
Bootstrap a project for AI-assisted development.
|
||||
|
||||
Options:
|
||||
-n, --name <name> Project name (required)
|
||||
-t, --type <type> Project type: nestjs-nextjs, django, generic (default: auto-detect)
|
||||
-r, --repo <url> Git remote URL
|
||||
-p, --prefix <prefix> Orchestrator task prefix (e.g., MS, UC)
|
||||
-d, --description <desc> One-line project description
|
||||
--skip-labels Skip creating git labels and milestones
|
||||
--skip-ci Skip copying CI pipeline files
|
||||
--cicd-docker Generate Docker build/push/link pipeline steps
|
||||
--cicd-service <name:path> Service for Docker CI (repeatable, requires --cicd-docker)
|
||||
--cicd-branches <list> Branches for Docker builds (default: main,develop)
|
||||
--dry-run Show what would be created without creating anything
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
# Full bootstrap with auto-detection
|
||||
init-project.sh --name "My App" --description "A web application"
|
||||
|
||||
# Specific type
|
||||
init-project.sh --name "My API" --type django --prefix MA
|
||||
|
||||
# Dry run
|
||||
init-project.sh --name "Test" --type generic --dry-run
|
||||
|
||||
# With Docker CI/CD pipeline
|
||||
init-project.sh --name "My App" --cicd-docker \
|
||||
--cicd-service "my-api:src/api/Dockerfile" \
|
||||
--cicd-service "my-web:src/web/Dockerfile"
|
||||
|
||||
Project Types:
|
||||
nestjs-nextjs NestJS + Next.js monorepo (pnpm + TurboRepo)
|
||||
django Django project (pytest + ruff + mypy)
|
||||
typescript Standalone TypeScript/Next.js project
|
||||
python-fastapi Python FastAPI project (pytest + ruff + mypy + uv)
|
||||
python-library Python library/SDK (pytest + ruff + mypy + uv)
|
||||
generic Generic project (uses base templates)
|
||||
auto Auto-detect from project files (default)
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-n|--name)
|
||||
PROJECT_NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
-t|--type)
|
||||
PROJECT_TYPE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-r|--repo)
|
||||
REPO_URL="$2"
|
||||
shift 2
|
||||
;;
|
||||
-p|--prefix)
|
||||
TASK_PREFIX="$2"
|
||||
shift 2
|
||||
;;
|
||||
-d|--description)
|
||||
PROJECT_DESCRIPTION="$2"
|
||||
shift 2
|
||||
;;
|
||||
--skip-labels)
|
||||
SKIP_LABELS=true
|
||||
shift
|
||||
;;
|
||||
--skip-ci)
|
||||
SKIP_CI=true
|
||||
shift
|
||||
;;
|
||||
--cicd-docker)
|
||||
CICD_DOCKER=true
|
||||
shift
|
||||
;;
|
||||
--cicd-service)
|
||||
CICD_SERVICES+=("$2")
|
||||
shift 2
|
||||
;;
|
||||
--cicd-branches)
|
||||
CICD_BRANCHES="$2"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
echo "Run with --help for usage" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate required args
|
||||
if [[ -z "$PROJECT_NAME" ]]; then
|
||||
echo "Error: --name is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Auto-detect project type if not specified
|
||||
detect_project_type() {
|
||||
# Monorepo (pnpm + turbo or npm workspaces with NestJS)
|
||||
if [[ -f "pnpm-workspace.yaml" ]] || [[ -f "turbo.json" ]]; then
|
||||
echo "nestjs-nextjs"
|
||||
return
|
||||
fi
|
||||
if [[ -f "package.json" ]] && grep -q '"workspaces"' package.json 2>/dev/null; then
|
||||
echo "nestjs-nextjs"
|
||||
return
|
||||
fi
|
||||
# Django
|
||||
if [[ -f "manage.py" ]] && [[ -f "pyproject.toml" ]]; then
|
||||
echo "django"
|
||||
return
|
||||
fi
|
||||
# FastAPI
|
||||
if [[ -f "pyproject.toml" ]] && grep -q "fastapi" pyproject.toml 2>/dev/null; then
|
||||
echo "python-fastapi"
|
||||
return
|
||||
fi
|
||||
# Standalone TypeScript
|
||||
if [[ -f "tsconfig.json" ]] && [[ -f "package.json" ]]; then
|
||||
echo "typescript"
|
||||
return
|
||||
fi
|
||||
# Python library/tool
|
||||
if [[ -f "pyproject.toml" ]]; then
|
||||
echo "python-library"
|
||||
return
|
||||
fi
|
||||
echo "generic"
|
||||
}
|
||||
|
||||
if [[ -z "$PROJECT_TYPE" || "$PROJECT_TYPE" == "auto" ]]; then
|
||||
PROJECT_TYPE=$(detect_project_type)
|
||||
echo "Auto-detected project type: $PROJECT_TYPE"
|
||||
fi
|
||||
|
||||
# Derive defaults
|
||||
if [[ -z "$REPO_URL" ]]; then
|
||||
REPO_URL=$(git remote get-url origin 2>/dev/null || echo "")
|
||||
fi
|
||||
|
||||
if [[ -z "$TASK_PREFIX" ]]; then
|
||||
# Generate prefix from project name initials
|
||||
TASK_PREFIX=$(echo "$PROJECT_NAME" | sed 's/[^A-Za-z ]//g' | awk '{for(i=1;i<=NF;i++) printf toupper(substr($i,1,1))}')
|
||||
if [[ -z "$TASK_PREFIX" ]]; then
|
||||
TASK_PREFIX="PRJ"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$PROJECT_DESCRIPTION" ]]; then
|
||||
PROJECT_DESCRIPTION="$PROJECT_NAME"
|
||||
fi
|
||||
|
||||
PROJECT_DIR=$(basename "$(pwd)")
|
||||
|
||||
# Detect quality gates, source dir, and stack info based on type
|
||||
case "$PROJECT_TYPE" in
|
||||
nestjs-nextjs)
|
||||
export QUALITY_GATES="pnpm typecheck && pnpm lint && pnpm test"
|
||||
export SOURCE_DIR="apps"
|
||||
export BUILD_COMMAND="pnpm build"
|
||||
export TEST_COMMAND="pnpm test"
|
||||
export LINT_COMMAND="pnpm lint"
|
||||
export TYPECHECK_COMMAND="pnpm typecheck"
|
||||
export FRONTEND_STACK="Next.js + React + TailwindCSS + Shadcn/ui"
|
||||
export BACKEND_STACK="NestJS + Prisma ORM"
|
||||
export DATABASE_STACK="PostgreSQL"
|
||||
export TESTING_STACK="Vitest + Playwright"
|
||||
export DEPLOYMENT_STACK="Docker + docker-compose"
|
||||
export CONFIG_FILES="turbo.json, pnpm-workspace.yaml, tsconfig.json"
|
||||
;;
|
||||
django)
|
||||
export QUALITY_GATES="ruff check . && mypy . && pytest tests/"
|
||||
export SOURCE_DIR="src"
|
||||
export BUILD_COMMAND="pip install -e ."
|
||||
export TEST_COMMAND="pytest tests/"
|
||||
export LINT_COMMAND="ruff check ."
|
||||
export TYPECHECK_COMMAND="mypy ."
|
||||
export FRONTEND_STACK="N/A"
|
||||
export BACKEND_STACK="Django / Django REST Framework"
|
||||
export DATABASE_STACK="PostgreSQL"
|
||||
export TESTING_STACK="pytest + pytest-django"
|
||||
export DEPLOYMENT_STACK="Docker + docker-compose"
|
||||
export CONFIG_FILES="pyproject.toml"
|
||||
export PROJECT_SLUG=$(echo "$PROJECT_NAME" | tr '[:upper:]' '[:lower:]' | tr ' ' '_' | sed 's/[^a-z0-9_]//g')
|
||||
;;
|
||||
typescript)
|
||||
PKG_MGR="npm"
|
||||
[[ -f "pnpm-lock.yaml" ]] && PKG_MGR="pnpm"
|
||||
[[ -f "yarn.lock" ]] && PKG_MGR="yarn"
|
||||
export QUALITY_GATES="$PKG_MGR run lint && $PKG_MGR run typecheck && $PKG_MGR test"
|
||||
export SOURCE_DIR="src"
|
||||
export BUILD_COMMAND="$PKG_MGR run build"
|
||||
export TEST_COMMAND="$PKG_MGR test"
|
||||
export LINT_COMMAND="$PKG_MGR run lint"
|
||||
export TYPECHECK_COMMAND="npx tsc --noEmit"
|
||||
export FRAMEWORK="TypeScript"
|
||||
export PACKAGE_MANAGER="$PKG_MGR"
|
||||
export FRONTEND_STACK="N/A"
|
||||
export BACKEND_STACK="N/A"
|
||||
export DATABASE_STACK="N/A"
|
||||
export TESTING_STACK="Vitest or Jest"
|
||||
export DEPLOYMENT_STACK="TBD"
|
||||
export CONFIG_FILES="tsconfig.json, package.json"
|
||||
# Detect Next.js
|
||||
if grep -q '"next"' package.json 2>/dev/null; then
|
||||
export FRAMEWORK="Next.js"
|
||||
export FRONTEND_STACK="Next.js + React"
|
||||
fi
|
||||
;;
|
||||
python-fastapi)
|
||||
export PROJECT_SLUG=$(echo "$PROJECT_NAME" | tr '[:upper:]' '[:lower:]' | tr ' ' '_' | sed 's/[^a-z0-9_]//g')
|
||||
export QUALITY_GATES="uv run ruff check src/ tests/ && uv run ruff format --check src/ && uv run mypy src/ && uv run pytest --cov"
|
||||
export SOURCE_DIR="src"
|
||||
export BUILD_COMMAND="uv sync --all-extras"
|
||||
export TEST_COMMAND="uv run pytest --cov"
|
||||
export LINT_COMMAND="uv run ruff check src/ tests/"
|
||||
export TYPECHECK_COMMAND="uv run mypy src/"
|
||||
export FRONTEND_STACK="N/A"
|
||||
export BACKEND_STACK="FastAPI"
|
||||
export DATABASE_STACK="TBD"
|
||||
export TESTING_STACK="pytest + httpx"
|
||||
export DEPLOYMENT_STACK="Docker"
|
||||
export CONFIG_FILES="pyproject.toml"
|
||||
;;
|
||||
python-library)
|
||||
export PROJECT_SLUG=$(echo "$PROJECT_NAME" | tr '[:upper:]' '[:lower:]' | tr ' ' '_' | sed 's/[^a-z0-9_]//g')
|
||||
export QUALITY_GATES="uv run ruff check src/ tests/ && uv run ruff format --check src/ && uv run mypy src/ && uv run pytest --cov"
|
||||
export SOURCE_DIR="src"
|
||||
export BUILD_COMMAND="uv sync --all-extras"
|
||||
export TEST_COMMAND="uv run pytest --cov"
|
||||
export LINT_COMMAND="uv run ruff check src/ tests/"
|
||||
export TYPECHECK_COMMAND="uv run mypy src/"
|
||||
export BUILD_SYSTEM="hatchling"
|
||||
export FRONTEND_STACK="N/A"
|
||||
export BACKEND_STACK="N/A"
|
||||
export DATABASE_STACK="N/A"
|
||||
export TESTING_STACK="pytest"
|
||||
export DEPLOYMENT_STACK="PyPI / Gitea Packages"
|
||||
export CONFIG_FILES="pyproject.toml"
|
||||
;;
|
||||
*)
|
||||
export QUALITY_GATES="echo 'No quality gates configured — update CLAUDE.md'"
|
||||
export SOURCE_DIR="src"
|
||||
export BUILD_COMMAND="echo 'No build command configured'"
|
||||
export TEST_COMMAND="echo 'No test command configured'"
|
||||
export LINT_COMMAND="echo 'No lint command configured'"
|
||||
export TYPECHECK_COMMAND="echo 'No typecheck command configured'"
|
||||
export FRONTEND_STACK="TBD"
|
||||
export BACKEND_STACK="TBD"
|
||||
export DATABASE_STACK="TBD"
|
||||
export TESTING_STACK="TBD"
|
||||
export DEPLOYMENT_STACK="TBD"
|
||||
export CONFIG_FILES="TBD"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Export common variables
|
||||
export PROJECT_NAME
|
||||
export PROJECT_DESCRIPTION
|
||||
export PROJECT_DIR
|
||||
export REPO_URL
|
||||
export TASK_PREFIX
|
||||
|
||||
echo "=== Project Bootstrap ==="
|
||||
echo " Name: $PROJECT_NAME"
|
||||
echo " Type: $PROJECT_TYPE"
|
||||
echo " Prefix: $TASK_PREFIX"
|
||||
echo " Description: $PROJECT_DESCRIPTION"
|
||||
echo " Repo: ${REPO_URL:-'(not set)'}"
|
||||
echo " Directory: $(pwd)"
|
||||
echo ""
|
||||
|
||||
# Select template directory
|
||||
STACK_TEMPLATE_DIR="$TEMPLATE_DIR/projects/$PROJECT_TYPE"
|
||||
if [[ ! -d "$STACK_TEMPLATE_DIR" ]]; then
|
||||
STACK_TEMPLATE_DIR="$TEMPLATE_DIR"
|
||||
echo "No stack-specific templates found for '$PROJECT_TYPE', using generic templates."
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
echo "[DRY RUN] Would create:"
|
||||
echo " - CLAUDE.md (from $STACK_TEMPLATE_DIR/CLAUDE.md.template)"
|
||||
echo " - AGENTS.md (from $STACK_TEMPLATE_DIR/AGENTS.md.template)"
|
||||
echo " - docs/scratchpads/"
|
||||
echo " - docs/reports/"
|
||||
echo " - docs/templates/"
|
||||
if [[ "$SKIP_CI" != true ]]; then
|
||||
echo " - .woodpecker/codex-review.yml"
|
||||
echo " - .woodpecker/schemas/*.json"
|
||||
fi
|
||||
if [[ "$SKIP_LABELS" != true ]]; then
|
||||
echo " - Standard git labels (epic, feature, bug, task, documentation, security, breaking)"
|
||||
echo " - Milestone: 0.1.0 - MVP"
|
||||
fi
|
||||
if [[ "$CICD_DOCKER" == true ]]; then
|
||||
echo " - Docker build/push/link steps appended to .woodpecker.yml"
|
||||
for svc in "${CICD_SERVICES[@]}"; do
|
||||
echo " - docker-build-${svc%%:*}"
|
||||
done
|
||||
echo " - link-packages"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Create CLAUDE.md
|
||||
if [[ -f "CLAUDE.md" ]]; then
|
||||
echo "CLAUDE.md already exists — skipping (rename or delete to recreate)"
|
||||
else
|
||||
if [[ -f "$STACK_TEMPLATE_DIR/CLAUDE.md.template" ]]; then
|
||||
envsubst < "$STACK_TEMPLATE_DIR/CLAUDE.md.template" > CLAUDE.md
|
||||
echo "Created CLAUDE.md"
|
||||
else
|
||||
echo "Warning: No CLAUDE.md template found at $STACK_TEMPLATE_DIR" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create AGENTS.md
|
||||
if [[ -f "AGENTS.md" ]]; then
|
||||
echo "AGENTS.md already exists — skipping (rename or delete to recreate)"
|
||||
else
|
||||
if [[ -f "$STACK_TEMPLATE_DIR/AGENTS.md.template" ]]; then
|
||||
envsubst < "$STACK_TEMPLATE_DIR/AGENTS.md.template" > AGENTS.md
|
||||
echo "Created AGENTS.md"
|
||||
else
|
||||
echo "Warning: No AGENTS.md template found at $STACK_TEMPLATE_DIR" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create directories
|
||||
mkdir -p docs/scratchpads docs/reports docs/templates
|
||||
echo "Created docs/scratchpads/, docs/reports/, docs/templates/"
|
||||
|
||||
# Set up CI/CD pipeline
|
||||
if [[ "$SKIP_CI" != true ]]; then
|
||||
CODEX_DIR="$HOME/.mosaic/rails/codex"
|
||||
if [[ -d "$CODEX_DIR/woodpecker" ]]; then
|
||||
mkdir -p .woodpecker/schemas
|
||||
cp "$CODEX_DIR/woodpecker/codex-review.yml" .woodpecker/
|
||||
cp "$CODEX_DIR/schemas/"*.json .woodpecker/schemas/
|
||||
echo "Created .woodpecker/ with Codex review pipeline"
|
||||
else
|
||||
echo "Codex pipeline templates not found — skipping CI setup"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Generate Docker build/push/link pipeline steps
|
||||
if [[ "$CICD_DOCKER" == true ]]; then
|
||||
CICD_SCRIPT="$HOME/.mosaic/rails/cicd/generate-docker-steps.sh"
|
||||
if [[ -x "$CICD_SCRIPT" ]]; then
|
||||
# Parse org and repo from git remote
|
||||
CICD_REGISTRY=""
|
||||
CICD_ORG=""
|
||||
CICD_REPO_NAME=""
|
||||
if [[ -n "$REPO_URL" ]]; then
|
||||
# Extract host from https://host/org/repo.git or git@host:org/repo.git
|
||||
CICD_REGISTRY=$(echo "$REPO_URL" | sed -E 's|https?://([^/]+)/.*|\1|; s|git@([^:]+):.*|\1|')
|
||||
CICD_ORG=$(echo "$REPO_URL" | sed -E 's|https?://[^/]+/([^/]+)/.*|\1|; s|git@[^:]+:([^/]+)/.*|\1|')
|
||||
CICD_REPO_NAME=$(echo "$REPO_URL" | sed -E 's|.*/([^/]+?)(\.git)?$|\1|')
|
||||
fi
|
||||
|
||||
if [[ -n "$CICD_REGISTRY" && -n "$CICD_ORG" && -n "$CICD_REPO_NAME" && ${#CICD_SERVICES[@]} -gt 0 ]]; then
|
||||
# Build service args
|
||||
SVC_ARGS=""
|
||||
for svc in "${CICD_SERVICES[@]}"; do
|
||||
SVC_ARGS="$SVC_ARGS --service $svc"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Generating Docker CI/CD pipeline steps..."
|
||||
|
||||
# Add kaniko_setup anchor to variables section if .woodpecker.yml exists
|
||||
if [[ -f ".woodpecker.yml" ]]; then
|
||||
# Append Docker steps to existing pipeline
|
||||
"$CICD_SCRIPT" \
|
||||
--registry "$CICD_REGISTRY" \
|
||||
--org "$CICD_ORG" \
|
||||
--repo "$CICD_REPO_NAME" \
|
||||
$SVC_ARGS \
|
||||
--branches "$CICD_BRANCHES" >> .woodpecker.yml
|
||||
echo "Appended Docker build/push/link steps to .woodpecker.yml"
|
||||
else
|
||||
echo "Warning: No .woodpecker.yml found — generate quality gates first, then re-run with --cicd-docker" >&2
|
||||
fi
|
||||
else
|
||||
if [[ ${#CICD_SERVICES[@]} -eq 0 ]]; then
|
||||
echo "Warning: --cicd-docker requires at least one --cicd-service" >&2
|
||||
else
|
||||
echo "Warning: Could not parse registry/org/repo from git remote — specify --repo" >&2
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "Docker CI/CD generator not found at $CICD_SCRIPT — skipping" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
# Initialize labels and milestones
|
||||
if [[ "$SKIP_LABELS" != true ]]; then
|
||||
LABEL_SCRIPT="$SCRIPT_DIR/init-repo-labels.sh"
|
||||
if [[ -x "$LABEL_SCRIPT" ]]; then
|
||||
echo ""
|
||||
echo "Initializing git labels and milestones..."
|
||||
"$LABEL_SCRIPT"
|
||||
else
|
||||
echo "Label init script not found — skipping label setup"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Bootstrap Complete ==="
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Review and customize CLAUDE.md"
|
||||
echo " 2. Review and customize AGENTS.md"
|
||||
echo " 3. Update quality gate commands if needed"
|
||||
echo " 4. Commit: git add CLAUDE.md AGENTS.md docs/ .woodpecker/ && git commit -m 'feat: Bootstrap project for AI development'"
|
||||
if [[ "$SKIP_CI" != true ]]; then
|
||||
echo " 5. Add 'codex_api_key' secret to Woodpecker CI"
|
||||
fi
|
||||
if [[ "$CICD_DOCKER" == true ]]; then
|
||||
echo " 6. Add 'gitea_username' and 'gitea_token' secrets to Woodpecker CI"
|
||||
echo " (token needs package:write scope)"
|
||||
fi
|
||||
121
rails/bootstrap/init-repo-labels.sh
Executable file
121
rails/bootstrap/init-repo-labels.sh
Executable file
@@ -0,0 +1,121 @@
|
||||
#!/bin/bash
|
||||
# init-repo-labels.sh - Create standard labels and initial milestone for a repository
|
||||
# Usage: init-repo-labels.sh [--skip-milestone]
|
||||
#
|
||||
# Works with both Gitea (tea) and GitHub (gh).
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
GIT_SCRIPT_DIR="$HOME/.mosaic/rails/git"
|
||||
source "$GIT_SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
SKIP_MILESTONE=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--skip-milestone)
|
||||
SKIP_MILESTONE=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: $(basename "$0") [--skip-milestone]"
|
||||
echo ""
|
||||
echo "Create standard labels and initial milestone for the current repository."
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --skip-milestone Skip creating the 0.1.0 MVP milestone"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
PLATFORM=$(detect_platform)
|
||||
OWNER=$(get_repo_owner)
|
||||
REPO=$(get_repo_name)
|
||||
|
||||
echo "Platform: $PLATFORM"
|
||||
echo "Repository: $OWNER/$REPO"
|
||||
echo ""
|
||||
|
||||
# Standard labels with colors
|
||||
# Format: "name|color|description"
|
||||
LABELS=(
|
||||
"epic|3E4B9E|Large feature spanning multiple issues"
|
||||
"feature|0E8A16|New functionality"
|
||||
"bug|D73A4A|Defect fix"
|
||||
"task|0075CA|General work item"
|
||||
"documentation|0075CA|Documentation updates"
|
||||
"security|B60205|Security-related"
|
||||
"breaking|D93F0B|Breaking change"
|
||||
)
|
||||
|
||||
create_label_github() {
|
||||
local name="$1" color="$2" description="$3"
|
||||
|
||||
# Check if label already exists
|
||||
if gh label list --repo "$OWNER/$REPO" --json name -q ".[].name" 2>/dev/null | grep -qx "$name"; then
|
||||
echo " [skip] '$name' already exists"
|
||||
return 0
|
||||
fi
|
||||
|
||||
gh label create "$name" \
|
||||
--repo "$OWNER/$REPO" \
|
||||
--color "$color" \
|
||||
--description "$description" 2>/dev/null && \
|
||||
echo " [created] '$name'" || \
|
||||
echo " [error] Failed to create '$name'"
|
||||
}
|
||||
|
||||
create_label_gitea() {
|
||||
local name="$1" color="$2" description="$3"
|
||||
|
||||
# Check if label already exists
|
||||
if tea labels list 2>/dev/null | grep -q "$name"; then
|
||||
echo " [skip] '$name' already exists"
|
||||
return 0
|
||||
fi
|
||||
|
||||
tea labels create --name "$name" --color "#$color" --description "$description" 2>/dev/null && \
|
||||
echo " [created] '$name'" || \
|
||||
echo " [error] Failed to create '$name'"
|
||||
}
|
||||
|
||||
echo "Creating labels..."
|
||||
|
||||
for label_def in "${LABELS[@]}"; do
|
||||
IFS='|' read -r name color description <<< "$label_def"
|
||||
|
||||
case "$PLATFORM" in
|
||||
github)
|
||||
create_label_github "$name" "$color" "$description"
|
||||
;;
|
||||
gitea)
|
||||
create_label_gitea "$name" "$color" "$description"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unsupported platform '$PLATFORM'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo ""
|
||||
|
||||
# Create initial milestone
|
||||
if [[ "$SKIP_MILESTONE" != true ]]; then
|
||||
echo "Creating initial milestone..."
|
||||
|
||||
"$GIT_SCRIPT_DIR/milestone-create.sh" -t "0.1.0" -d "MVP - Minimum Viable Product" 2>/dev/null && \
|
||||
echo " [created] Milestone '0.1.0 - MVP'" || \
|
||||
echo " [skip] Milestone may already exist or creation failed"
|
||||
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "Label initialization complete."
|
||||
265
rails/codex/README.md
Normal file
265
rails/codex/README.md
Normal file
@@ -0,0 +1,265 @@
|
||||
# Codex CLI Review Scripts
|
||||
|
||||
AI-powered code review and security review scripts using OpenAI's Codex CLI.
|
||||
|
||||
These scripts provide **independent** code analysis separate from Claude sessions, giving you a second AI perspective on code changes to catch issues that might be missed.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
```bash
|
||||
# Install Codex CLI
|
||||
npm i -g @openai/codex
|
||||
|
||||
# Verify installation
|
||||
codex --version
|
||||
|
||||
# Authenticate (first run)
|
||||
codex # Will prompt for ChatGPT account or API key
|
||||
|
||||
# Verify jq is installed (for JSON processing)
|
||||
jq --version
|
||||
```
|
||||
|
||||
## Scripts
|
||||
|
||||
### `codex-code-review.sh`
|
||||
General code quality review focusing on:
|
||||
- **Correctness** — logic errors, edge cases, error handling
|
||||
- **Code Quality** — complexity, duplication, naming, dead code
|
||||
- **Testing** — coverage, test quality
|
||||
- **Performance** — N+1 queries, blocking operations, resource cleanup
|
||||
- **Dependencies** — deprecated packages
|
||||
- **Documentation** — comments, public API docs
|
||||
|
||||
**Output:** Structured JSON with findings categorized as `blocker`, `should-fix`, or `suggestion`.
|
||||
|
||||
### `codex-security-review.sh`
|
||||
Security vulnerability review focusing on:
|
||||
- **OWASP Top 10** — injection, broken auth, XSS, CSRF, SSRF, etc.
|
||||
- **Secrets Detection** — hardcoded credentials, API keys, tokens
|
||||
- **Injection Flaws** — SQL, NoSQL, OS command, LDAP
|
||||
- **Auth/Authz Gaps** — missing checks, privilege escalation, IDOR
|
||||
- **Data Exposure** — logging sensitive data, information disclosure
|
||||
- **Supply Chain** — vulnerable dependencies, typosquatting
|
||||
|
||||
**Output:** Structured JSON with findings categorized as `critical`, `high`, `medium`, or `low` with CWE IDs and OWASP categories.
|
||||
|
||||
## Usage
|
||||
|
||||
### Review Uncommitted Changes
|
||||
|
||||
```bash
|
||||
# Code review
|
||||
~/.mosaic/rails/codex/codex-code-review.sh --uncommitted
|
||||
|
||||
# Security review
|
||||
~/.mosaic/rails/codex/codex-security-review.sh --uncommitted
|
||||
```
|
||||
|
||||
### Review a Pull Request
|
||||
|
||||
```bash
|
||||
# Review and post findings as a PR comment
|
||||
~/.mosaic/rails/codex/codex-code-review.sh -n 42
|
||||
|
||||
# Security review and post to PR
|
||||
~/.mosaic/rails/codex/codex-security-review.sh -n 42
|
||||
```
|
||||
|
||||
### Review Against Base Branch
|
||||
|
||||
```bash
|
||||
# Code review changes vs main
|
||||
~/.mosaic/rails/codex/codex-code-review.sh -b main
|
||||
|
||||
# Security review changes vs develop
|
||||
~/.mosaic/rails/codex/codex-security-review.sh -b develop
|
||||
```
|
||||
|
||||
### Review a Specific Commit
|
||||
|
||||
```bash
|
||||
~/.mosaic/rails/codex/codex-code-review.sh -c abc123f
|
||||
~/.mosaic/rails/codex/codex-security-review.sh -c abc123f
|
||||
```
|
||||
|
||||
### Save Results to File
|
||||
|
||||
```bash
|
||||
# Save JSON output
|
||||
~/.mosaic/rails/codex/codex-code-review.sh --uncommitted -o review-results.json
|
||||
~/.mosaic/rails/codex/codex-security-review.sh --uncommitted -o security-results.json
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
Both scripts support the same options:
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-n, --pr <number>` | PR number (auto-enables posting to PR) |
|
||||
| `-b, --base <branch>` | Base branch to diff against (default: main) |
|
||||
| `-c, --commit <sha>` | Review a specific commit |
|
||||
| `-o, --output <path>` | Write JSON results to file |
|
||||
| `--post-to-pr` | Post findings as PR comment (requires -n) |
|
||||
| `--uncommitted` | Review uncommitted changes (staged + unstaged + untracked) |
|
||||
| `-h, --help` | Show help |
|
||||
|
||||
## Woodpecker CI Integration
|
||||
|
||||
Automated PR reviews in CI pipelines.
|
||||
|
||||
### Setup
|
||||
|
||||
1. **Copy the pipeline template to your repo:**
|
||||
```bash
|
||||
cp ~/.mosaic/rails/codex/woodpecker/codex-review.yml your-repo/.woodpecker/
|
||||
```
|
||||
|
||||
2. **Copy the schemas directory:**
|
||||
```bash
|
||||
cp -r ~/.mosaic/rails/codex/schemas your-repo/.woodpecker/
|
||||
```
|
||||
|
||||
3. **Add Codex API key to Woodpecker:**
|
||||
- Go to your repo in Woodpecker CI
|
||||
- Settings → Secrets
|
||||
- Add secret: `codex_api_key` with your OpenAI API key
|
||||
|
||||
4. **Commit and push:**
|
||||
```bash
|
||||
cd your-repo
|
||||
git add .woodpecker/
|
||||
git commit -m "feat: Add Codex AI review pipeline"
|
||||
git push
|
||||
```
|
||||
|
||||
### Pipeline Behavior
|
||||
|
||||
- **Triggers on:** Pull requests
|
||||
- **Runs:** Code review + Security review in parallel
|
||||
- **Fails if:**
|
||||
- Code review finds blockers
|
||||
- Security review finds critical or high severity issues
|
||||
- **Outputs:** Structured JSON results in CI logs
|
||||
|
||||
## Output Format
|
||||
|
||||
### Code Review JSON
|
||||
|
||||
```json
|
||||
{
|
||||
"summary": "Overall assessment...",
|
||||
"verdict": "approve|request-changes|comment",
|
||||
"confidence": 0.85,
|
||||
"findings": [
|
||||
{
|
||||
"severity": "blocker",
|
||||
"title": "SQL injection vulnerability",
|
||||
"file": "src/api/users.ts",
|
||||
"line_start": 42,
|
||||
"line_end": 45,
|
||||
"description": "User input directly interpolated into SQL query",
|
||||
"suggestion": "Use parameterized queries"
|
||||
}
|
||||
],
|
||||
"stats": {
|
||||
"files_reviewed": 5,
|
||||
"blockers": 1,
|
||||
"should_fix": 3,
|
||||
"suggestions": 8
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Security Review JSON
|
||||
|
||||
```json
|
||||
{
|
||||
"summary": "Security assessment...",
|
||||
"risk_level": "high",
|
||||
"confidence": 0.90,
|
||||
"findings": [
|
||||
{
|
||||
"severity": "high",
|
||||
"title": "Hardcoded API key",
|
||||
"file": "src/config.ts",
|
||||
"line_start": 10,
|
||||
"description": "API key hardcoded in source",
|
||||
"cwe_id": "CWE-798",
|
||||
"owasp_category": "A02:2021-Cryptographic Failures",
|
||||
"remediation": "Move to environment variables or secrets manager"
|
||||
}
|
||||
],
|
||||
"stats": {
|
||||
"files_reviewed": 5,
|
||||
"critical": 0,
|
||||
"high": 1,
|
||||
"medium": 2,
|
||||
"low": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Platform Support
|
||||
|
||||
Works with both **GitHub** and **Gitea** via the shared `~/.mosaic/rails/git/` infrastructure:
|
||||
- Auto-detects platform from git remote
|
||||
- Posts PR comments using `gh` (GitHub) or `tea` (Gitea)
|
||||
- Unified interface across both platforms
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
codex-code-review.sh
|
||||
codex-security-review.sh
|
||||
↓
|
||||
common.sh
|
||||
↓ sources
|
||||
../git/detect-platform.sh (platform detection)
|
||||
../git/pr-review.sh (post PR comments)
|
||||
↓ uses
|
||||
gh (GitHub) or tea (Gitea)
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "codex: command not found"
|
||||
```bash
|
||||
npm i -g @openai/codex
|
||||
```
|
||||
|
||||
### "jq: command not found"
|
||||
```bash
|
||||
# Arch Linux
|
||||
sudo pacman -S jq
|
||||
|
||||
# Debian/Ubuntu
|
||||
sudo apt install jq
|
||||
```
|
||||
|
||||
### "Error: Not inside a git repository"
|
||||
Run the script from inside a git repository.
|
||||
|
||||
### "No changes found to review"
|
||||
The specified mode (--uncommitted, --base, etc.) found no changes to review.
|
||||
|
||||
### "Codex produced no output"
|
||||
Check your Codex API key and authentication:
|
||||
```bash
|
||||
codex # Re-authenticate if needed
|
||||
```
|
||||
|
||||
## Model Configuration
|
||||
|
||||
By default, scripts use the model configured in `~/.codex/config.toml`:
|
||||
- **Model:** `gpt-5.3-codex` (recommended for code review)
|
||||
- **Reasoning effort:** `high`
|
||||
|
||||
For best results, use `gpt-5.2-codex` or newer for strongest review accuracy.
|
||||
|
||||
## See Also
|
||||
|
||||
- `~/.mosaic/guides/code-review.md` — Manual code review checklist
|
||||
- `~/.mosaic/rails/git/` — Git helper scripts (issue/PR management)
|
||||
- OpenAI Codex CLI docs: https://developers.openai.com/codex/cli/
|
||||
238
rails/codex/codex-code-review.sh
Executable file
238
rails/codex/codex-code-review.sh
Executable file
@@ -0,0 +1,238 @@
|
||||
#!/bin/bash
|
||||
# codex-code-review.sh - Run an AI-powered code quality review using Codex CLI
|
||||
# Usage: codex-code-review.sh [OPTIONS]
|
||||
#
|
||||
# Runs codex exec in read-only sandbox mode with a structured code review prompt.
|
||||
# Outputs findings as JSON and optionally posts them to a PR.
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
# Defaults
|
||||
PR_NUMBER=""
|
||||
BASE_BRANCH="main"
|
||||
COMMIT_SHA=""
|
||||
OUTPUT_FILE=""
|
||||
POST_TO_PR=false
|
||||
UNCOMMITTED=false
|
||||
REVIEW_MODE=""
|
||||
|
||||
show_help() {
|
||||
cat <<'EOF'
|
||||
Usage: codex-code-review.sh [OPTIONS]
|
||||
|
||||
Run an AI-powered code quality review using OpenAI Codex CLI.
|
||||
|
||||
Options:
|
||||
-n, --pr <number> PR number (auto-enables posting findings to PR)
|
||||
-b, --base <branch> Base branch to diff against (default: main)
|
||||
-c, --commit <sha> Review a specific commit
|
||||
-o, --output <path> Write JSON results to file
|
||||
--post-to-pr Post findings as PR comment (requires -n)
|
||||
--uncommitted Review uncommitted changes (staged + unstaged + untracked)
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
# Review uncommitted changes
|
||||
codex-code-review.sh --uncommitted
|
||||
|
||||
# Review a PR and post findings as a comment
|
||||
codex-code-review.sh -n 42
|
||||
|
||||
# Review changes against main, save JSON
|
||||
codex-code-review.sh -b main -o review.json
|
||||
|
||||
# Review a specific commit
|
||||
codex-code-review.sh -c abc123f
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-n|--pr)
|
||||
PR_NUMBER="$2"
|
||||
POST_TO_PR=true
|
||||
REVIEW_MODE="pr"
|
||||
shift 2
|
||||
;;
|
||||
-b|--base)
|
||||
BASE_BRANCH="$2"
|
||||
REVIEW_MODE="base"
|
||||
shift 2
|
||||
;;
|
||||
-c|--commit)
|
||||
COMMIT_SHA="$2"
|
||||
REVIEW_MODE="commit"
|
||||
shift 2
|
||||
;;
|
||||
-o|--output)
|
||||
OUTPUT_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--post-to-pr)
|
||||
POST_TO_PR=true
|
||||
shift
|
||||
;;
|
||||
--uncommitted)
|
||||
UNCOMMITTED=true
|
||||
REVIEW_MODE="uncommitted"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
echo "Run with --help for usage" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate
|
||||
if [[ -z "$REVIEW_MODE" ]]; then
|
||||
echo "Error: Specify a review mode: --uncommitted, --base <branch>, --commit <sha>, or --pr <number>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$POST_TO_PR" == true && -z "$PR_NUMBER" ]]; then
|
||||
echo "Error: --post-to-pr requires -n <pr_number>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
check_codex
|
||||
check_jq
|
||||
|
||||
# Verify we're in a git repo
|
||||
if ! git rev-parse --is-inside-work-tree &>/dev/null; then
|
||||
echo "Error: Not inside a git repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get the diff context
|
||||
echo "Gathering diff context..." >&2
|
||||
case "$REVIEW_MODE" in
|
||||
uncommitted) DIFF_CONTEXT=$(build_diff_context "uncommitted" "") ;;
|
||||
base) DIFF_CONTEXT=$(build_diff_context "base" "$BASE_BRANCH") ;;
|
||||
commit) DIFF_CONTEXT=$(build_diff_context "commit" "$COMMIT_SHA") ;;
|
||||
pr) DIFF_CONTEXT=$(build_diff_context "pr" "$PR_NUMBER") ;;
|
||||
esac
|
||||
|
||||
if [[ -z "$DIFF_CONTEXT" ]]; then
|
||||
echo "No changes found to review." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Build the review prompt
|
||||
REVIEW_PROMPT=$(cat <<'PROMPT'
|
||||
You are an expert code reviewer. Review the following code changes thoroughly.
|
||||
|
||||
Focus on issues that are ACTIONABLE and IMPORTANT. Do not flag trivial style issues.
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### Correctness
|
||||
- Code does what it claims to do
|
||||
- Edge cases are handled
|
||||
- Error conditions are managed properly
|
||||
- No obvious bugs or logic errors
|
||||
|
||||
### Code Quality
|
||||
- Functions are focused and reasonably sized
|
||||
- No unnecessary complexity
|
||||
- DRY - no significant duplication
|
||||
- Clear naming for variables and functions
|
||||
- No dead code or commented-out code
|
||||
|
||||
### Testing
|
||||
- Tests exist for new functionality
|
||||
- Tests cover happy path AND error cases
|
||||
- No flaky tests introduced
|
||||
|
||||
### Performance
|
||||
- No obvious N+1 queries
|
||||
- No blocking operations in hot paths
|
||||
- Resource cleanup (connections, file handles)
|
||||
|
||||
### Dependencies
|
||||
- No deprecated packages
|
||||
- No unnecessary new dependencies
|
||||
|
||||
### Documentation
|
||||
- Complex logic has explanatory comments
|
||||
- Public APIs are documented
|
||||
|
||||
## Severity Guide
|
||||
- **blocker**: Must fix before merge (bugs, correctness issues, missing error handling)
|
||||
- **should-fix**: Important but not blocking (code quality, minor issues)
|
||||
- **suggestion**: Optional improvements (nice-to-haves)
|
||||
|
||||
Only report findings you are confident about (confidence > 0.7).
|
||||
If the code looks good, say so — don't manufacture issues.
|
||||
|
||||
PROMPT
|
||||
)
|
||||
|
||||
# Set up temp files for output and diff
|
||||
TEMP_OUTPUT=$(mktemp /tmp/codex-review-XXXXXX.json)
|
||||
TEMP_DIFF=$(mktemp /tmp/codex-diff-XXXXXX.txt)
|
||||
trap 'rm -f "$TEMP_OUTPUT" "$TEMP_DIFF"' EXIT
|
||||
|
||||
SCHEMA_FILE="$SCRIPT_DIR/schemas/code-review-schema.json"
|
||||
|
||||
# Write diff to temp file
|
||||
echo "$DIFF_CONTEXT" > "$TEMP_DIFF"
|
||||
|
||||
echo "Running Codex code review..." >&2
|
||||
echo " Diff size: $(wc -l < "$TEMP_DIFF") lines" >&2
|
||||
|
||||
# Build full prompt with diff reference
|
||||
FULL_PROMPT="${REVIEW_PROMPT}
|
||||
|
||||
Here are the code changes to review:
|
||||
|
||||
\`\`\`diff
|
||||
$(cat "$TEMP_DIFF")
|
||||
\`\`\`"
|
||||
|
||||
# Run codex exec with prompt from stdin to avoid arg length limits
|
||||
echo "$FULL_PROMPT" | codex exec \
|
||||
--sandbox read-only \
|
||||
--output-schema "$SCHEMA_FILE" \
|
||||
-o "$TEMP_OUTPUT" \
|
||||
- 2>&1 | while IFS= read -r line; do
|
||||
echo " [codex] $line" >&2
|
||||
done
|
||||
|
||||
# Check output was produced
|
||||
if [[ ! -s "$TEMP_OUTPUT" ]]; then
|
||||
echo "Error: Codex produced no output" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate JSON
|
||||
if ! jq empty "$TEMP_OUTPUT" 2>/dev/null; then
|
||||
echo "Error: Codex output is not valid JSON" >&2
|
||||
cat "$TEMP_OUTPUT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Save output if requested
|
||||
if [[ -n "$OUTPUT_FILE" ]]; then
|
||||
cp "$TEMP_OUTPUT" "$OUTPUT_FILE"
|
||||
echo "Results saved to: $OUTPUT_FILE" >&2
|
||||
fi
|
||||
|
||||
# Post to PR if requested
|
||||
if [[ "$POST_TO_PR" == true && -n "$PR_NUMBER" ]]; then
|
||||
echo "Posting findings to PR #$PR_NUMBER..." >&2
|
||||
post_to_pr "$PR_NUMBER" "$TEMP_OUTPUT" "code"
|
||||
echo "Posted review to PR #$PR_NUMBER" >&2
|
||||
fi
|
||||
|
||||
# Always print results to stdout
|
||||
print_results "$TEMP_OUTPUT" "code"
|
||||
235
rails/codex/codex-security-review.sh
Executable file
235
rails/codex/codex-security-review.sh
Executable file
@@ -0,0 +1,235 @@
|
||||
#!/bin/bash
|
||||
# codex-security-review.sh - Run an AI-powered security vulnerability review using Codex CLI
|
||||
# Usage: codex-security-review.sh [OPTIONS]
|
||||
#
|
||||
# Runs codex exec in read-only sandbox mode with a security-focused review prompt.
|
||||
# Outputs findings as JSON and optionally posts them to a PR.
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
# Defaults
|
||||
PR_NUMBER=""
|
||||
BASE_BRANCH="main"
|
||||
COMMIT_SHA=""
|
||||
OUTPUT_FILE=""
|
||||
POST_TO_PR=false
|
||||
UNCOMMITTED=false
|
||||
REVIEW_MODE=""
|
||||
|
||||
show_help() {
|
||||
cat <<'EOF'
|
||||
Usage: codex-security-review.sh [OPTIONS]
|
||||
|
||||
Run an AI-powered security vulnerability review using OpenAI Codex CLI.
|
||||
|
||||
Options:
|
||||
-n, --pr <number> PR number (auto-enables posting findings to PR)
|
||||
-b, --base <branch> Base branch to diff against (default: main)
|
||||
-c, --commit <sha> Review a specific commit
|
||||
-o, --output <path> Write JSON results to file
|
||||
--post-to-pr Post findings as PR comment (requires -n)
|
||||
--uncommitted Review uncommitted changes (staged + unstaged + untracked)
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
# Security review uncommitted changes
|
||||
codex-security-review.sh --uncommitted
|
||||
|
||||
# Security review a PR and post findings
|
||||
codex-security-review.sh -n 42
|
||||
|
||||
# Security review against main, save JSON
|
||||
codex-security-review.sh -b main -o security.json
|
||||
|
||||
# Security review a specific commit
|
||||
codex-security-review.sh -c abc123f
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-n|--pr)
|
||||
PR_NUMBER="$2"
|
||||
POST_TO_PR=true
|
||||
REVIEW_MODE="pr"
|
||||
shift 2
|
||||
;;
|
||||
-b|--base)
|
||||
BASE_BRANCH="$2"
|
||||
REVIEW_MODE="base"
|
||||
shift 2
|
||||
;;
|
||||
-c|--commit)
|
||||
COMMIT_SHA="$2"
|
||||
REVIEW_MODE="commit"
|
||||
shift 2
|
||||
;;
|
||||
-o|--output)
|
||||
OUTPUT_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--post-to-pr)
|
||||
POST_TO_PR=true
|
||||
shift
|
||||
;;
|
||||
--uncommitted)
|
||||
UNCOMMITTED=true
|
||||
REVIEW_MODE="uncommitted"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
echo "Run with --help for usage" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate
|
||||
if [[ -z "$REVIEW_MODE" ]]; then
|
||||
echo "Error: Specify a review mode: --uncommitted, --base <branch>, --commit <sha>, or --pr <number>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$POST_TO_PR" == true && -z "$PR_NUMBER" ]]; then
|
||||
echo "Error: --post-to-pr requires -n <pr_number>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
check_codex
|
||||
check_jq
|
||||
|
||||
# Verify we're in a git repo
|
||||
if ! git rev-parse --is-inside-work-tree &>/dev/null; then
|
||||
echo "Error: Not inside a git repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get the diff context
|
||||
echo "Gathering diff context..." >&2
|
||||
case "$REVIEW_MODE" in
|
||||
uncommitted) DIFF_CONTEXT=$(build_diff_context "uncommitted" "") ;;
|
||||
base) DIFF_CONTEXT=$(build_diff_context "base" "$BASE_BRANCH") ;;
|
||||
commit) DIFF_CONTEXT=$(build_diff_context "commit" "$COMMIT_SHA") ;;
|
||||
pr) DIFF_CONTEXT=$(build_diff_context "pr" "$PR_NUMBER") ;;
|
||||
esac
|
||||
|
||||
if [[ -z "$DIFF_CONTEXT" ]]; then
|
||||
echo "No changes found to review." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Build the security review prompt
|
||||
REVIEW_PROMPT=$(cat <<'PROMPT'
|
||||
You are an expert application security engineer performing a security-focused code review.
|
||||
Your goal is to identify vulnerabilities, security anti-patterns, and data exposure risks.
|
||||
|
||||
## Security Review Scope
|
||||
|
||||
### OWASP Top 10 (2021)
|
||||
- A01: Broken Access Control — missing authorization checks, IDOR, privilege escalation
|
||||
- A02: Cryptographic Failures — weak algorithms, plaintext secrets, missing encryption
|
||||
- A03: Injection — SQL, NoSQL, OS command, LDAP, XPath injection
|
||||
- A04: Insecure Design — missing threat modeling, unsafe business logic
|
||||
- A05: Security Misconfiguration — debug mode, default credentials, unnecessary features
|
||||
- A06: Vulnerable Components — known CVEs in dependencies
|
||||
- A07: Authentication Failures — weak auth, missing MFA, session issues
|
||||
- A08: Data Integrity Failures — deserialization, unsigned updates
|
||||
- A09: Logging Failures — sensitive data in logs, missing audit trails
|
||||
- A10: SSRF — unvalidated URLs, internal service access
|
||||
|
||||
### Additional Checks
|
||||
- Hardcoded secrets, API keys, tokens, passwords
|
||||
- Insecure direct object references
|
||||
- Missing input validation at trust boundaries
|
||||
- Cross-Site Scripting (XSS) — reflected, stored, DOM-based
|
||||
- Cross-Site Request Forgery (CSRF) protection
|
||||
- Insecure file handling (path traversal, unrestricted upload)
|
||||
- Race conditions and TOCTOU vulnerabilities
|
||||
- Information disclosure (stack traces, verbose errors)
|
||||
- Supply chain risks (typosquatting, dependency confusion)
|
||||
|
||||
## Severity Guide
|
||||
- **critical**: Exploitable vulnerability with immediate impact (RCE, auth bypass, data breach)
|
||||
- **high**: Significant vulnerability requiring prompt fix (injection, XSS, secrets exposure)
|
||||
- **medium**: Vulnerability with limited exploitability or impact (missing headers, weak config)
|
||||
- **low**: Minor security concern or hardening opportunity (informational, defense-in-depth)
|
||||
|
||||
## Rules
|
||||
- Include CWE IDs when applicable
|
||||
- Include OWASP category when applicable
|
||||
- Provide specific remediation steps for every finding
|
||||
- Only report findings you are confident about
|
||||
- Do NOT flag non-security code quality issues
|
||||
- If no security issues found, say so clearly
|
||||
|
||||
PROMPT
|
||||
)
|
||||
|
||||
# Set up temp files for output and diff
|
||||
TEMP_OUTPUT=$(mktemp /tmp/codex-security-XXXXXX.json)
|
||||
TEMP_DIFF=$(mktemp /tmp/codex-diff-XXXXXX.txt)
|
||||
trap 'rm -f "$TEMP_OUTPUT" "$TEMP_DIFF"' EXIT
|
||||
|
||||
SCHEMA_FILE="$SCRIPT_DIR/schemas/security-review-schema.json"
|
||||
|
||||
# Write diff to temp file
|
||||
echo "$DIFF_CONTEXT" > "$TEMP_DIFF"
|
||||
|
||||
echo "Running Codex security review..." >&2
|
||||
echo " Diff size: $(wc -l < "$TEMP_DIFF") lines" >&2
|
||||
|
||||
# Build full prompt with diff reference
|
||||
FULL_PROMPT="${REVIEW_PROMPT}
|
||||
|
||||
Here are the code changes to security review:
|
||||
|
||||
\`\`\`diff
|
||||
$(cat "$TEMP_DIFF")
|
||||
\`\`\`"
|
||||
|
||||
# Run codex exec with prompt from stdin to avoid arg length limits
|
||||
echo "$FULL_PROMPT" | codex exec \
|
||||
--sandbox read-only \
|
||||
--output-schema "$SCHEMA_FILE" \
|
||||
-o "$TEMP_OUTPUT" \
|
||||
- 2>&1 | while IFS= read -r line; do
|
||||
echo " [codex] $line" >&2
|
||||
done
|
||||
|
||||
# Check output was produced
|
||||
if [[ ! -s "$TEMP_OUTPUT" ]]; then
|
||||
echo "Error: Codex produced no output" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate JSON
|
||||
if ! jq empty "$TEMP_OUTPUT" 2>/dev/null; then
|
||||
echo "Error: Codex output is not valid JSON" >&2
|
||||
cat "$TEMP_OUTPUT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Save output if requested
|
||||
if [[ -n "$OUTPUT_FILE" ]]; then
|
||||
cp "$TEMP_OUTPUT" "$OUTPUT_FILE"
|
||||
echo "Results saved to: $OUTPUT_FILE" >&2
|
||||
fi
|
||||
|
||||
# Post to PR if requested
|
||||
if [[ "$POST_TO_PR" == true && -n "$PR_NUMBER" ]]; then
|
||||
echo "Posting findings to PR #$PR_NUMBER..." >&2
|
||||
post_to_pr "$PR_NUMBER" "$TEMP_OUTPUT" "security"
|
||||
echo "Posted security review to PR #$PR_NUMBER" >&2
|
||||
fi
|
||||
|
||||
# Always print results to stdout
|
||||
print_results "$TEMP_OUTPUT" "security"
|
||||
191
rails/codex/common.sh
Executable file
191
rails/codex/common.sh
Executable file
@@ -0,0 +1,191 @@
|
||||
#!/bin/bash
|
||||
# common.sh - Shared utilities for Codex review scripts
|
||||
# Source this file from review scripts: source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||
|
||||
set -e
|
||||
|
||||
CODEX_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
GIT_SCRIPT_DIR="$CODEX_SCRIPT_DIR/../git"
|
||||
|
||||
# Source platform detection
|
||||
source "$GIT_SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Check codex is installed
|
||||
check_codex() {
|
||||
if ! command -v codex &>/dev/null; then
|
||||
echo "Error: codex CLI not found. Install with: npm i -g @openai/codex" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check jq is installed (needed for JSON processing)
|
||||
check_jq() {
|
||||
if ! command -v jq &>/dev/null; then
|
||||
echo "Error: jq not found. Install with your package manager." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Build the codex exec command args for the review mode
|
||||
# Arguments: $1=mode (--uncommitted|--base|--commit), $2=value (branch/sha)
|
||||
build_diff_context() {
|
||||
local mode="$1"
|
||||
local value="$2"
|
||||
local diff_text=""
|
||||
|
||||
case "$mode" in
|
||||
uncommitted)
|
||||
diff_text=$(git diff HEAD 2>/dev/null; git diff --cached 2>/dev/null; git ls-files --others --exclude-standard 2>/dev/null | while read -r f; do echo "=== NEW FILE: $f ==="; cat "$f" 2>/dev/null; done)
|
||||
;;
|
||||
base)
|
||||
diff_text=$(git diff "${value}...HEAD" 2>/dev/null)
|
||||
;;
|
||||
commit)
|
||||
diff_text=$(git show "$value" 2>/dev/null)
|
||||
;;
|
||||
pr)
|
||||
# For PRs, we need to fetch the PR diff
|
||||
detect_platform
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
diff_text=$(gh pr diff "$value" 2>/dev/null)
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
# tea doesn't have a direct pr diff command, use git
|
||||
local pr_base
|
||||
pr_base=$(tea pr list --fields index,base --output simple 2>/dev/null | grep "^${value}" | awk '{print $2}')
|
||||
if [[ -n "$pr_base" ]]; then
|
||||
diff_text=$(git diff "${pr_base}...HEAD" 2>/dev/null)
|
||||
else
|
||||
# Fallback: fetch PR info via API
|
||||
local repo_info
|
||||
repo_info=$(get_repo_info)
|
||||
local remote_url
|
||||
remote_url=$(git remote get-url origin 2>/dev/null)
|
||||
local host
|
||||
host=$(echo "$remote_url" | sed -E 's|.*://([^/]+).*|\1|; s|.*@([^:]+).*|\1|')
|
||||
diff_text=$(curl -s "https://${host}/api/v1/repos/${repo_info}/pulls/${value}" \
|
||||
-H "Authorization: token $(tea login list --output simple 2>/dev/null | head -1 | awk '{print $2}')" \
|
||||
2>/dev/null | jq -r '.diff_url // empty')
|
||||
if [[ -n "$diff_text" && "$diff_text" != "null" ]]; then
|
||||
diff_text=$(curl -s "$diff_text" 2>/dev/null)
|
||||
else
|
||||
diff_text=$(git diff "main...HEAD" 2>/dev/null)
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "$diff_text"
|
||||
}
|
||||
|
||||
# Format JSON findings as markdown for PR comments
|
||||
# Arguments: $1=json_file, $2=review_type (code|security)
|
||||
format_findings_as_markdown() {
|
||||
local json_file="$1"
|
||||
local review_type="$2"
|
||||
|
||||
if [[ ! -f "$json_file" ]]; then
|
||||
echo "Error: JSON file not found: $json_file" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local summary verdict confidence
|
||||
summary=$(jq -r '.summary' "$json_file")
|
||||
confidence=$(jq -r '.confidence' "$json_file")
|
||||
|
||||
if [[ "$review_type" == "code" ]]; then
|
||||
verdict=$(jq -r '.verdict' "$json_file")
|
||||
local blockers should_fix suggestions files_reviewed
|
||||
blockers=$(jq -r '.stats.blockers' "$json_file")
|
||||
should_fix=$(jq -r '.stats.should_fix' "$json_file")
|
||||
suggestions=$(jq -r '.stats.suggestions' "$json_file")
|
||||
files_reviewed=$(jq -r '.stats.files_reviewed' "$json_file")
|
||||
|
||||
cat <<EOF
|
||||
## Codex Code Review
|
||||
|
||||
**Verdict:** ${verdict} | **Confidence:** ${confidence} | **Files reviewed:** ${files_reviewed}
|
||||
**Findings:** ${blockers} blockers, ${should_fix} should-fix, ${suggestions} suggestions
|
||||
|
||||
### Summary
|
||||
${summary}
|
||||
|
||||
EOF
|
||||
else
|
||||
local risk_level critical high medium low files_reviewed
|
||||
risk_level=$(jq -r '.risk_level' "$json_file")
|
||||
critical=$(jq -r '.stats.critical' "$json_file")
|
||||
high=$(jq -r '.stats.high' "$json_file")
|
||||
medium=$(jq -r '.stats.medium' "$json_file")
|
||||
low=$(jq -r '.stats.low' "$json_file")
|
||||
files_reviewed=$(jq -r '.stats.files_reviewed' "$json_file")
|
||||
|
||||
cat <<EOF
|
||||
## Codex Security Review
|
||||
|
||||
**Risk Level:** ${risk_level} | **Confidence:** ${confidence} | **Files reviewed:** ${files_reviewed}
|
||||
**Findings:** ${critical} critical, ${high} high, ${medium} medium, ${low} low
|
||||
|
||||
### Summary
|
||||
${summary}
|
||||
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Output findings
|
||||
local finding_count
|
||||
finding_count=$(jq '.findings | length' "$json_file")
|
||||
|
||||
if [[ "$finding_count" -gt 0 ]]; then
|
||||
echo "### Findings"
|
||||
echo ""
|
||||
|
||||
jq -r '.findings[] | "#### [\(.severity | ascii_upcase)] \(.title)\n- **File:** `\(.file)`\(if .line_start then " (L\(.line_start)\(if .line_end and .line_end != .line_start then "-L\(.line_end)" else "" end))" else "" end)\n- \(.description)\(if .suggestion then "\n- **Suggestion:** \(.suggestion)" else "" end)\(if .cwe_id then "\n- **CWE:** \(.cwe_id)" else "" end)\(if .owasp_category then "\n- **OWASP:** \(.owasp_category)" else "" end)\(if .remediation then "\n- **Remediation:** \(.remediation)" else "" end)\n"' "$json_file"
|
||||
else
|
||||
echo "*No issues found.*"
|
||||
fi
|
||||
|
||||
echo "---"
|
||||
echo "*Reviewed by Codex ($(codex --version 2>/dev/null || echo "unknown"))*"
|
||||
}
|
||||
|
||||
# Post review findings to a PR
|
||||
# Arguments: $1=pr_number, $2=json_file, $3=review_type (code|security)
|
||||
post_to_pr() {
|
||||
local pr_number="$1"
|
||||
local json_file="$2"
|
||||
local review_type="$3"
|
||||
|
||||
local markdown
|
||||
markdown=$(format_findings_as_markdown "$json_file" "$review_type")
|
||||
|
||||
detect_platform
|
||||
|
||||
# Determine review action based on findings
|
||||
local action="comment"
|
||||
if [[ "$review_type" == "code" ]]; then
|
||||
local verdict
|
||||
verdict=$(jq -r '.verdict' "$json_file")
|
||||
action="$verdict"
|
||||
else
|
||||
local risk_level
|
||||
risk_level=$(jq -r '.risk_level' "$json_file")
|
||||
case "$risk_level" in
|
||||
critical|high) action="request-changes" ;;
|
||||
medium) action="comment" ;;
|
||||
low|none) action="comment" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Post the review
|
||||
"$GIT_SCRIPT_DIR/pr-review.sh" -n "$pr_number" -a "$action" -c "$markdown"
|
||||
}
|
||||
|
||||
# Print review results to stdout
|
||||
# Arguments: $1=json_file, $2=review_type (code|security)
|
||||
print_results() {
|
||||
local json_file="$1"
|
||||
local review_type="$2"
|
||||
|
||||
format_findings_as_markdown "$json_file" "$review_type"
|
||||
}
|
||||
84
rails/codex/schemas/code-review-schema.json
Normal file
84
rails/codex/schemas/code-review-schema.json
Normal file
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"summary": {
|
||||
"type": "string",
|
||||
"description": "Brief overall assessment of the code changes"
|
||||
},
|
||||
"verdict": {
|
||||
"type": "string",
|
||||
"enum": ["approve", "request-changes", "comment"],
|
||||
"description": "Overall review verdict"
|
||||
},
|
||||
"confidence": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"description": "Confidence score for the review (0-1)"
|
||||
},
|
||||
"findings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"severity": {
|
||||
"type": "string",
|
||||
"enum": ["blocker", "should-fix", "suggestion"],
|
||||
"description": "Finding severity: blocker (must fix), should-fix (important), suggestion (optional)"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Short title describing the issue"
|
||||
},
|
||||
"file": {
|
||||
"type": "string",
|
||||
"description": "File path where the issue was found"
|
||||
},
|
||||
"line_start": {
|
||||
"type": "integer",
|
||||
"description": "Starting line number"
|
||||
},
|
||||
"line_end": {
|
||||
"type": "integer",
|
||||
"description": "Ending line number"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Detailed explanation of the issue"
|
||||
},
|
||||
"suggestion": {
|
||||
"type": "string",
|
||||
"description": "Suggested fix or improvement"
|
||||
}
|
||||
},
|
||||
"required": ["severity", "title", "file", "line_start", "line_end", "description", "suggestion"]
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"files_reviewed": {
|
||||
"type": "integer",
|
||||
"description": "Number of files reviewed"
|
||||
},
|
||||
"blockers": {
|
||||
"type": "integer",
|
||||
"description": "Count of blocker findings"
|
||||
},
|
||||
"should_fix": {
|
||||
"type": "integer",
|
||||
"description": "Count of should-fix findings"
|
||||
},
|
||||
"suggestions": {
|
||||
"type": "integer",
|
||||
"description": "Count of suggestion findings"
|
||||
}
|
||||
},
|
||||
"required": ["files_reviewed", "blockers", "should_fix", "suggestions"]
|
||||
}
|
||||
},
|
||||
"required": ["summary", "verdict", "confidence", "findings", "stats"]
|
||||
}
|
||||
96
rails/codex/schemas/security-review-schema.json
Normal file
96
rails/codex/schemas/security-review-schema.json
Normal file
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"summary": {
|
||||
"type": "string",
|
||||
"description": "Brief overall security assessment of the code changes"
|
||||
},
|
||||
"risk_level": {
|
||||
"type": "string",
|
||||
"enum": ["critical", "high", "medium", "low", "none"],
|
||||
"description": "Overall security risk level"
|
||||
},
|
||||
"confidence": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"description": "Confidence score for the review (0-1)"
|
||||
},
|
||||
"findings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"severity": {
|
||||
"type": "string",
|
||||
"enum": ["critical", "high", "medium", "low"],
|
||||
"description": "Vulnerability severity level"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Short title describing the vulnerability"
|
||||
},
|
||||
"file": {
|
||||
"type": "string",
|
||||
"description": "File path where the vulnerability was found"
|
||||
},
|
||||
"line_start": {
|
||||
"type": "integer",
|
||||
"description": "Starting line number"
|
||||
},
|
||||
"line_end": {
|
||||
"type": "integer",
|
||||
"description": "Ending line number"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Detailed explanation of the vulnerability"
|
||||
},
|
||||
"cwe_id": {
|
||||
"type": "string",
|
||||
"description": "CWE identifier if applicable (e.g., CWE-79)"
|
||||
},
|
||||
"owasp_category": {
|
||||
"type": "string",
|
||||
"description": "OWASP Top 10 category if applicable (e.g., A03:2021-Injection)"
|
||||
},
|
||||
"remediation": {
|
||||
"type": "string",
|
||||
"description": "Specific remediation steps to fix the vulnerability"
|
||||
}
|
||||
},
|
||||
"required": ["severity", "title", "file", "line_start", "line_end", "description", "cwe_id", "owasp_category", "remediation"]
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"files_reviewed": {
|
||||
"type": "integer",
|
||||
"description": "Number of files reviewed"
|
||||
},
|
||||
"critical": {
|
||||
"type": "integer",
|
||||
"description": "Count of critical findings"
|
||||
},
|
||||
"high": {
|
||||
"type": "integer",
|
||||
"description": "Count of high findings"
|
||||
},
|
||||
"medium": {
|
||||
"type": "integer",
|
||||
"description": "Count of medium findings"
|
||||
},
|
||||
"low": {
|
||||
"type": "integer",
|
||||
"description": "Count of low findings"
|
||||
}
|
||||
},
|
||||
"required": ["files_reviewed", "critical", "high", "medium", "low"]
|
||||
}
|
||||
},
|
||||
"required": ["summary", "risk_level", "confidence", "findings", "stats"]
|
||||
}
|
||||
90
rails/codex/woodpecker/codex-review.yml
Normal file
90
rails/codex/woodpecker/codex-review.yml
Normal file
@@ -0,0 +1,90 @@
|
||||
# Codex AI Review Pipeline for Woodpecker CI
|
||||
# Drop this into your repo's .woodpecker/ directory to enable automated
|
||||
# code and security reviews on every pull request.
|
||||
#
|
||||
# Required secrets:
|
||||
# - codex_api_key: OpenAI API key or Codex-compatible key
|
||||
#
|
||||
# Optional secrets:
|
||||
# - gitea_token: Gitea API token for posting PR comments (if not using tea CLI auth)
|
||||
|
||||
when:
|
||||
event: pull_request
|
||||
|
||||
variables:
|
||||
- &node_image "node:22-slim"
|
||||
- &install_codex "npm i -g @openai/codex"
|
||||
|
||||
steps:
|
||||
# --- Code Quality Review ---
|
||||
code-review:
|
||||
image: *node_image
|
||||
environment:
|
||||
CODEX_API_KEY:
|
||||
from_secret: codex_api_key
|
||||
commands:
|
||||
- *install_codex
|
||||
- apt-get update -qq && apt-get install -y -qq jq git > /dev/null 2>&1
|
||||
|
||||
# Generate the diff
|
||||
- git fetch origin ${CI_COMMIT_TARGET_BRANCH:-main}
|
||||
- DIFF=$(git diff origin/${CI_COMMIT_TARGET_BRANCH:-main}...HEAD)
|
||||
|
||||
# Run code review with structured output
|
||||
- |
|
||||
codex exec \
|
||||
--sandbox read-only \
|
||||
--output-schema .woodpecker/schemas/code-review-schema.json \
|
||||
-o /tmp/code-review.json \
|
||||
"You are an expert code reviewer. Review the following code changes for correctness, code quality, testing, performance, and documentation issues. Only flag actionable, important issues. Categorize as blocker/should-fix/suggestion. If code looks good, say so.
|
||||
|
||||
Changes:
|
||||
$DIFF"
|
||||
|
||||
# Output summary
|
||||
- echo "=== Code Review Results ==="
|
||||
- jq '.' /tmp/code-review.json
|
||||
- |
|
||||
BLOCKERS=$(jq '.stats.blockers // 0' /tmp/code-review.json)
|
||||
if [ "$BLOCKERS" -gt 0 ]; then
|
||||
echo "FAIL: $BLOCKERS blocker(s) found"
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: No blockers found"
|
||||
|
||||
# --- Security Review ---
|
||||
security-review:
|
||||
image: *node_image
|
||||
environment:
|
||||
CODEX_API_KEY:
|
||||
from_secret: codex_api_key
|
||||
commands:
|
||||
- *install_codex
|
||||
- apt-get update -qq && apt-get install -y -qq jq git > /dev/null 2>&1
|
||||
|
||||
# Generate the diff
|
||||
- git fetch origin ${CI_COMMIT_TARGET_BRANCH:-main}
|
||||
- DIFF=$(git diff origin/${CI_COMMIT_TARGET_BRANCH:-main}...HEAD)
|
||||
|
||||
# Run security review with structured output
|
||||
- |
|
||||
codex exec \
|
||||
--sandbox read-only \
|
||||
--output-schema .woodpecker/schemas/security-review-schema.json \
|
||||
-o /tmp/security-review.json \
|
||||
"You are an expert application security engineer. Review the following code changes for security vulnerabilities including OWASP Top 10, hardcoded secrets, injection flaws, auth/authz gaps, XSS, CSRF, SSRF, path traversal, and supply chain risks. Include CWE IDs and remediation steps. Only flag real security issues, not code quality.
|
||||
|
||||
Changes:
|
||||
$DIFF"
|
||||
|
||||
# Output summary
|
||||
- echo "=== Security Review Results ==="
|
||||
- jq '.' /tmp/security-review.json
|
||||
- |
|
||||
CRITICAL=$(jq '.stats.critical // 0' /tmp/security-review.json)
|
||||
HIGH=$(jq '.stats.high // 0' /tmp/security-review.json)
|
||||
if [ "$CRITICAL" -gt 0 ] || [ "$HIGH" -gt 0 ]; then
|
||||
echo "FAIL: $CRITICAL critical, $HIGH high severity finding(s)"
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: No critical or high severity findings"
|
||||
83
rails/git/detect-platform.ps1
Normal file
83
rails/git/detect-platform.ps1
Normal file
@@ -0,0 +1,83 @@
|
||||
# detect-platform.ps1 - Detect git platform (Gitea or GitHub) for current repo
|
||||
# Usage: . .\detect-platform.ps1; Get-GitPlatform
|
||||
# or: .\detect-platform.ps1 (prints platform name)
|
||||
|
||||
function Get-GitPlatform {
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
$remoteUrl = git remote get-url origin 2>$null
|
||||
|
||||
if ([string]::IsNullOrEmpty($remoteUrl)) {
|
||||
Write-Error "Not a git repository or no origin remote"
|
||||
return $null
|
||||
}
|
||||
|
||||
# Check for GitHub
|
||||
if ($remoteUrl -match "github\.com") {
|
||||
return "github"
|
||||
}
|
||||
|
||||
# Check for common Gitea indicators
|
||||
# Gitea URLs typically don't contain github.com, gitlab.com, bitbucket.org
|
||||
if ($remoteUrl -notmatch "gitlab\.com" -and $remoteUrl -notmatch "bitbucket\.org") {
|
||||
# Assume Gitea for self-hosted repos
|
||||
return "gitea"
|
||||
}
|
||||
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
function Get-GitRepoInfo {
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
$remoteUrl = git remote get-url origin 2>$null
|
||||
|
||||
if ([string]::IsNullOrEmpty($remoteUrl)) {
|
||||
Write-Error "Not a git repository or no origin remote"
|
||||
return $null
|
||||
}
|
||||
|
||||
# Extract owner/repo from URL
|
||||
# Handles: git@host:owner/repo.git, https://host/owner/repo.git, https://host/owner/repo
|
||||
$repoPath = $remoteUrl
|
||||
if ($remoteUrl -match "^git@") {
|
||||
$repoPath = ($remoteUrl -split ":")[1]
|
||||
} else {
|
||||
# Remove protocol and host
|
||||
$repoPath = $remoteUrl -replace "^https?://[^/]+/", ""
|
||||
}
|
||||
|
||||
# Remove .git suffix if present
|
||||
$repoPath = $repoPath -replace "\.git$", ""
|
||||
|
||||
return $repoPath
|
||||
}
|
||||
|
||||
function Get-GitRepoOwner {
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
$repoInfo = Get-GitRepoInfo
|
||||
if ($repoInfo) {
|
||||
return ($repoInfo -split "/")[0]
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-GitRepoName {
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
$repoInfo = Get-GitRepoInfo
|
||||
if ($repoInfo) {
|
||||
return ($repoInfo -split "/")[-1]
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
# If script is run directly (not dot-sourced), output the platform
|
||||
if ($MyInvocation.InvocationName -ne ".") {
|
||||
Get-GitPlatform
|
||||
}
|
||||
80
rails/git/detect-platform.sh
Executable file
80
rails/git/detect-platform.sh
Executable file
@@ -0,0 +1,80 @@
|
||||
#!/bin/bash
|
||||
# detect-platform.sh - Detect git platform (Gitea or GitHub) for current repo
|
||||
# Usage: source detect-platform.sh && detect_platform
|
||||
# or: ./detect-platform.sh (prints platform name)
|
||||
|
||||
detect_platform() {
|
||||
local remote_url
|
||||
remote_url=$(git remote get-url origin 2>/dev/null)
|
||||
|
||||
if [[ -z "$remote_url" ]]; then
|
||||
echo "error: not a git repository or no origin remote" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check for GitHub
|
||||
if [[ "$remote_url" == *"github.com"* ]]; then
|
||||
PLATFORM="github"
|
||||
export PLATFORM
|
||||
echo "github"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check for common Gitea indicators
|
||||
# Gitea URLs typically don't contain github.com, gitlab.com, bitbucket.org
|
||||
if [[ "$remote_url" != *"gitlab.com"* ]] && \
|
||||
[[ "$remote_url" != *"bitbucket.org"* ]]; then
|
||||
# Assume Gitea for self-hosted repos
|
||||
PLATFORM="gitea"
|
||||
export PLATFORM
|
||||
echo "gitea"
|
||||
return 0
|
||||
fi
|
||||
|
||||
PLATFORM="unknown"
|
||||
export PLATFORM
|
||||
echo "unknown"
|
||||
return 1
|
||||
}
|
||||
|
||||
get_repo_info() {
|
||||
local remote_url
|
||||
remote_url=$(git remote get-url origin 2>/dev/null)
|
||||
|
||||
if [[ -z "$remote_url" ]]; then
|
||||
echo "error: not a git repository or no origin remote" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Extract owner/repo from URL
|
||||
# Handles: git@host:owner/repo.git, https://host/owner/repo.git, https://host/owner/repo
|
||||
local repo_path
|
||||
if [[ "$remote_url" == git@* ]]; then
|
||||
repo_path="${remote_url#*:}"
|
||||
else
|
||||
repo_path="${remote_url#*://}"
|
||||
repo_path="${repo_path#*/}"
|
||||
fi
|
||||
|
||||
# Remove .git suffix if present
|
||||
repo_path="${repo_path%.git}"
|
||||
|
||||
echo "$repo_path"
|
||||
}
|
||||
|
||||
get_repo_owner() {
|
||||
local repo_info
|
||||
repo_info=$(get_repo_info)
|
||||
echo "${repo_info%%/*}"
|
||||
}
|
||||
|
||||
get_repo_name() {
|
||||
local repo_info
|
||||
repo_info=$(get_repo_info)
|
||||
echo "${repo_info##*/}"
|
||||
}
|
||||
|
||||
# If script is run directly (not sourced), output the platform
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
detect_platform
|
||||
fi
|
||||
111
rails/git/issue-assign.ps1
Normal file
111
rails/git/issue-assign.ps1
Normal file
@@ -0,0 +1,111 @@
|
||||
# issue-assign.ps1 - Assign issues on Gitea or GitHub
|
||||
# Usage: .\issue-assign.ps1 -Issue ISSUE_NUMBER [-Assignee assignee] [-Labels labels] [-Milestone milestone]
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[Alias("i")]
|
||||
[int]$Issue,
|
||||
|
||||
[Alias("a")]
|
||||
[string]$Assignee,
|
||||
|
||||
[Alias("l")]
|
||||
[string]$Labels,
|
||||
|
||||
[Alias("m")]
|
||||
[string]$Milestone,
|
||||
|
||||
[Alias("r")]
|
||||
[switch]$RemoveAssignee,
|
||||
|
||||
[Alias("h")]
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. "$ScriptDir\detect-platform.ps1"
|
||||
|
||||
function Show-Usage {
|
||||
@"
|
||||
Usage: issue-assign.ps1 [OPTIONS]
|
||||
|
||||
Assign or update an issue on the current repository (Gitea or GitHub).
|
||||
|
||||
Options:
|
||||
-Issue, -i NUMBER Issue number (required)
|
||||
-Assignee, -a USER Assign to user (use @me for self)
|
||||
-Labels, -l LABELS Add comma-separated labels
|
||||
-Milestone, -m NAME Set milestone
|
||||
-RemoveAssignee, -r Remove current assignee
|
||||
-Help, -h Show this help message
|
||||
|
||||
Examples:
|
||||
.\issue-assign.ps1 -i 42 -a "username"
|
||||
.\issue-assign.ps1 -i 42 -l "in-progress" -m "0.2.0"
|
||||
.\issue-assign.ps1 -i 42 -a @me
|
||||
"@
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($Help) {
|
||||
Show-Usage
|
||||
}
|
||||
|
||||
$platform = Get-GitPlatform
|
||||
|
||||
switch ($platform) {
|
||||
"github" {
|
||||
if ($Assignee) {
|
||||
gh issue edit $Issue --add-assignee $Assignee
|
||||
}
|
||||
if ($RemoveAssignee) {
|
||||
$current = gh issue view $Issue --json assignees -q '.assignees[].login' 2>$null
|
||||
if ($current) {
|
||||
$assignees = ($current -split "`n") -join ","
|
||||
gh issue edit $Issue --remove-assignee $assignees
|
||||
}
|
||||
}
|
||||
if ($Labels) {
|
||||
gh issue edit $Issue --add-label $Labels
|
||||
}
|
||||
if ($Milestone) {
|
||||
gh issue edit $Issue --milestone $Milestone
|
||||
}
|
||||
Write-Host "Issue #$Issue updated successfully"
|
||||
}
|
||||
"gitea" {
|
||||
$needsEdit = $false
|
||||
$cmd = @("tea", "issue", "edit", $Issue)
|
||||
|
||||
if ($Assignee) {
|
||||
$cmd += @("--assignees", $Assignee)
|
||||
$needsEdit = $true
|
||||
}
|
||||
if ($Labels) {
|
||||
$cmd += @("--labels", $Labels)
|
||||
$needsEdit = $true
|
||||
}
|
||||
if ($Milestone) {
|
||||
$milestoneList = tea milestones list 2>$null
|
||||
$milestoneId = ($milestoneList | Select-String "^\s*(\d+).*$Milestone" | ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -First 1)
|
||||
if ($milestoneId) {
|
||||
$cmd += @("--milestone", $milestoneId)
|
||||
$needsEdit = $true
|
||||
} else {
|
||||
Write-Warning "Could not find milestone '$Milestone'"
|
||||
}
|
||||
}
|
||||
|
||||
if ($needsEdit) {
|
||||
& $cmd[0] $cmd[1..($cmd.Length-1)]
|
||||
Write-Host "Issue #$Issue updated successfully"
|
||||
} else {
|
||||
Write-Host "No changes specified"
|
||||
}
|
||||
}
|
||||
default {
|
||||
Write-Error "Could not detect git platform"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
135
rails/git/issue-assign.sh
Executable file
135
rails/git/issue-assign.sh
Executable file
@@ -0,0 +1,135 @@
|
||||
#!/bin/bash
|
||||
# issue-assign.sh - Assign issues on Gitea or GitHub
|
||||
# Usage: issue-assign.sh -i ISSUE_NUMBER [-a assignee] [-l labels] [-m milestone]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Default values
|
||||
ISSUE=""
|
||||
ASSIGNEE=""
|
||||
LABELS=""
|
||||
MILESTONE=""
|
||||
REMOVE_ASSIGNEE=false
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [OPTIONS]
|
||||
|
||||
Assign or update an issue on the current repository (Gitea or GitHub).
|
||||
|
||||
Options:
|
||||
-i, --issue NUMBER Issue number (required)
|
||||
-a, --assignee USER Assign to user (use @me for self)
|
||||
-l, --labels LABELS Add comma-separated labels
|
||||
-m, --milestone NAME Set milestone
|
||||
-r, --remove-assignee Remove current assignee
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
$(basename "$0") -i 42 -a "username"
|
||||
$(basename "$0") -i 42 -l "in-progress" -m "0.2.0"
|
||||
$(basename "$0") -i 42 -a @me
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-i|--issue)
|
||||
ISSUE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-a|--assignee)
|
||||
ASSIGNEE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-l|--labels)
|
||||
LABELS="$2"
|
||||
shift 2
|
||||
;;
|
||||
-m|--milestone)
|
||||
MILESTONE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-r|--remove-assignee)
|
||||
REMOVE_ASSIGNEE=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$ISSUE" ]]; then
|
||||
echo "Error: Issue number is required (-i)" >&2
|
||||
usage
|
||||
fi
|
||||
|
||||
PLATFORM=$(detect_platform)
|
||||
|
||||
case "$PLATFORM" in
|
||||
github)
|
||||
if [[ -n "$ASSIGNEE" ]]; then
|
||||
gh issue edit "$ISSUE" --add-assignee "$ASSIGNEE"
|
||||
fi
|
||||
if [[ "$REMOVE_ASSIGNEE" == true ]]; then
|
||||
# Get current assignees and remove them
|
||||
CURRENT=$(gh issue view "$ISSUE" --json assignees -q '.assignees[].login' 2>/dev/null | tr '\n' ',')
|
||||
if [[ -n "$CURRENT" ]]; then
|
||||
gh issue edit "$ISSUE" --remove-assignee "${CURRENT%,}"
|
||||
fi
|
||||
fi
|
||||
if [[ -n "$LABELS" ]]; then
|
||||
gh issue edit "$ISSUE" --add-label "$LABELS"
|
||||
fi
|
||||
if [[ -n "$MILESTONE" ]]; then
|
||||
gh issue edit "$ISSUE" --milestone "$MILESTONE"
|
||||
fi
|
||||
echo "Issue #$ISSUE updated successfully"
|
||||
;;
|
||||
gitea)
|
||||
# tea issue edit syntax
|
||||
CMD="tea issue edit $ISSUE"
|
||||
NEEDS_EDIT=false
|
||||
|
||||
if [[ -n "$ASSIGNEE" ]]; then
|
||||
# tea uses --assignees flag
|
||||
CMD="$CMD --assignees \"$ASSIGNEE\""
|
||||
NEEDS_EDIT=true
|
||||
fi
|
||||
if [[ -n "$LABELS" ]]; then
|
||||
# tea uses --labels flag (replaces existing)
|
||||
CMD="$CMD --labels \"$LABELS\""
|
||||
NEEDS_EDIT=true
|
||||
fi
|
||||
if [[ -n "$MILESTONE" ]]; then
|
||||
MILESTONE_ID=$(tea milestones list 2>/dev/null | grep -E "^\s*[0-9]+" | grep "$MILESTONE" | awk '{print $1}' | head -1)
|
||||
if [[ -n "$MILESTONE_ID" ]]; then
|
||||
CMD="$CMD --milestone $MILESTONE_ID"
|
||||
NEEDS_EDIT=true
|
||||
else
|
||||
echo "Warning: Could not find milestone '$MILESTONE'" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$NEEDS_EDIT" == true ]]; then
|
||||
eval "$CMD"
|
||||
echo "Issue #$ISSUE updated successfully"
|
||||
else
|
||||
echo "No changes specified"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Error: Could not detect git platform" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
64
rails/git/issue-close.sh
Executable file
64
rails/git/issue-close.sh
Executable file
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
# issue-close.sh - Close an issue on GitHub or Gitea
|
||||
# Usage: issue-close.sh -i <issue_number> [-c <comment>]
|
||||
|
||||
set -e
|
||||
|
||||
# Source platform detection
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Parse arguments
|
||||
ISSUE_NUMBER=""
|
||||
COMMENT=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-i|--issue)
|
||||
ISSUE_NUMBER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-c|--comment)
|
||||
COMMENT="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: issue-close.sh -i <issue_number> [-c <comment>]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -i, --issue Issue number (required)"
|
||||
echo " -c, --comment Comment to add before closing (optional)"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$ISSUE_NUMBER" ]]; then
|
||||
echo "Error: Issue number is required (-i)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Detect platform and close issue
|
||||
detect_platform
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
if [[ -n "$COMMENT" ]]; then
|
||||
gh issue comment "$ISSUE_NUMBER" --body "$COMMENT"
|
||||
fi
|
||||
gh issue close "$ISSUE_NUMBER"
|
||||
echo "Closed GitHub issue #$ISSUE_NUMBER"
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
if [[ -n "$COMMENT" ]]; then
|
||||
tea issue comment "$ISSUE_NUMBER" "$COMMENT"
|
||||
fi
|
||||
tea issue close "$ISSUE_NUMBER"
|
||||
echo "Closed Gitea issue #$ISSUE_NUMBER"
|
||||
else
|
||||
echo "Error: Unknown platform"
|
||||
exit 1
|
||||
fi
|
||||
61
rails/git/issue-comment.sh
Executable file
61
rails/git/issue-comment.sh
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/bin/bash
|
||||
# issue-comment.sh - Add a comment to an issue on GitHub or Gitea
|
||||
# Usage: issue-comment.sh -i <issue_number> -c <comment>
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Parse arguments
|
||||
ISSUE_NUMBER=""
|
||||
COMMENT=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-i|--issue)
|
||||
ISSUE_NUMBER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-c|--comment)
|
||||
COMMENT="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: issue-comment.sh -i <issue_number> -c <comment>"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -i, --issue Issue number (required)"
|
||||
echo " -c, --comment Comment text (required)"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$ISSUE_NUMBER" ]]; then
|
||||
echo "Error: Issue number is required (-i)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$COMMENT" ]]; then
|
||||
echo "Error: Comment is required (-c)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
detect_platform
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
gh issue comment "$ISSUE_NUMBER" --body "$COMMENT"
|
||||
echo "Added comment to GitHub issue #$ISSUE_NUMBER"
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
tea issue comment "$ISSUE_NUMBER" "$COMMENT"
|
||||
echo "Added comment to Gitea issue #$ISSUE_NUMBER"
|
||||
else
|
||||
echo "Error: Unknown platform"
|
||||
exit 1
|
||||
fi
|
||||
80
rails/git/issue-create.ps1
Normal file
80
rails/git/issue-create.ps1
Normal file
@@ -0,0 +1,80 @@
|
||||
# issue-create.ps1 - Create issues on Gitea or GitHub
|
||||
# Usage: .\issue-create.ps1 -Title "Title" [-Body "Body"] [-Labels "label1,label2"] [-Milestone "milestone"]
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[Alias("t")]
|
||||
[string]$Title,
|
||||
|
||||
[Alias("b")]
|
||||
[string]$Body,
|
||||
|
||||
[Alias("l")]
|
||||
[string]$Labels,
|
||||
|
||||
[Alias("m")]
|
||||
[string]$Milestone,
|
||||
|
||||
[Alias("h")]
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. "$ScriptDir\detect-platform.ps1"
|
||||
|
||||
function Show-Usage {
|
||||
@"
|
||||
Usage: issue-create.ps1 [OPTIONS]
|
||||
|
||||
Create an issue on the current repository (Gitea or GitHub).
|
||||
|
||||
Options:
|
||||
-Title, -t TITLE Issue title (required)
|
||||
-Body, -b BODY Issue body/description
|
||||
-Labels, -l LABELS Comma-separated labels (e.g., "bug,feature")
|
||||
-Milestone, -m NAME Milestone name to assign
|
||||
-Help, -h Show this help message
|
||||
|
||||
Examples:
|
||||
.\issue-create.ps1 -Title "Fix login bug" -Labels "bug,priority-high"
|
||||
.\issue-create.ps1 -t "Add dark mode" -b "Implement theme switching" -m "0.2.0"
|
||||
"@
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($Help) {
|
||||
Show-Usage
|
||||
}
|
||||
|
||||
$platform = Get-GitPlatform
|
||||
|
||||
switch ($platform) {
|
||||
"github" {
|
||||
$cmd = @("gh", "issue", "create", "--title", $Title)
|
||||
if ($Body) { $cmd += @("--body", $Body) }
|
||||
if ($Labels) { $cmd += @("--label", $Labels) }
|
||||
if ($Milestone) { $cmd += @("--milestone", $Milestone) }
|
||||
& $cmd[0] $cmd[1..($cmd.Length-1)]
|
||||
}
|
||||
"gitea" {
|
||||
$cmd = @("tea", "issue", "create", "--title", $Title)
|
||||
if ($Body) { $cmd += @("--description", $Body) }
|
||||
if ($Labels) { $cmd += @("--labels", $Labels) }
|
||||
if ($Milestone) {
|
||||
# Try to get milestone ID by name
|
||||
$milestoneList = tea milestones list 2>$null
|
||||
$milestoneId = ($milestoneList | Select-String "^\s*(\d+).*$Milestone" | ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -First 1)
|
||||
if ($milestoneId) {
|
||||
$cmd += @("--milestone", $milestoneId)
|
||||
} else {
|
||||
Write-Warning "Could not find milestone '$Milestone', creating without milestone"
|
||||
}
|
||||
}
|
||||
& $cmd[0] $cmd[1..($cmd.Length-1)]
|
||||
}
|
||||
default {
|
||||
Write-Error "Could not detect git platform"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
92
rails/git/issue-create.sh
Executable file
92
rails/git/issue-create.sh
Executable file
@@ -0,0 +1,92 @@
|
||||
#!/bin/bash
|
||||
# issue-create.sh - Create issues on Gitea or GitHub
|
||||
# Usage: issue-create.sh -t "Title" [-b "Body"] [-l "label1,label2"] [-m "milestone"]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Default values
|
||||
TITLE=""
|
||||
BODY=""
|
||||
LABELS=""
|
||||
MILESTONE=""
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [OPTIONS]
|
||||
|
||||
Create an issue on the current repository (Gitea or GitHub).
|
||||
|
||||
Options:
|
||||
-t, --title TITLE Issue title (required)
|
||||
-b, --body BODY Issue body/description
|
||||
-l, --labels LABELS Comma-separated labels (e.g., "bug,feature")
|
||||
-m, --milestone NAME Milestone name to assign
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
$(basename "$0") -t "Fix login bug" -l "bug,priority-high"
|
||||
$(basename "$0") -t "Add dark mode" -b "Implement theme switching" -m "0.2.0"
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-t|--title)
|
||||
TITLE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-b|--body)
|
||||
BODY="$2"
|
||||
shift 2
|
||||
;;
|
||||
-l|--labels)
|
||||
LABELS="$2"
|
||||
shift 2
|
||||
;;
|
||||
-m|--milestone)
|
||||
MILESTONE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$TITLE" ]]; then
|
||||
echo "Error: Title is required (-t)" >&2
|
||||
usage
|
||||
fi
|
||||
|
||||
PLATFORM=$(detect_platform)
|
||||
|
||||
case "$PLATFORM" in
|
||||
github)
|
||||
CMD="gh issue create --title \"$TITLE\""
|
||||
[[ -n "$BODY" ]] && CMD="$CMD --body \"$BODY\""
|
||||
[[ -n "$LABELS" ]] && CMD="$CMD --label \"$LABELS\""
|
||||
[[ -n "$MILESTONE" ]] && CMD="$CMD --milestone \"$MILESTONE\""
|
||||
eval "$CMD"
|
||||
;;
|
||||
gitea)
|
||||
CMD="tea issue create --title \"$TITLE\""
|
||||
[[ -n "$BODY" ]] && CMD="$CMD --description \"$BODY\""
|
||||
[[ -n "$LABELS" ]] && CMD="$CMD --labels \"$LABELS\""
|
||||
# tea accepts milestone by name directly (verified 2026-02-05)
|
||||
[[ -n "$MILESTONE" ]] && CMD="$CMD --milestone \"$MILESTONE\""
|
||||
eval "$CMD"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Could not detect git platform" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
84
rails/git/issue-edit.sh
Executable file
84
rails/git/issue-edit.sh
Executable file
@@ -0,0 +1,84 @@
|
||||
#!/bin/bash
|
||||
# issue-edit.sh - Edit an issue on GitHub or Gitea
|
||||
# Usage: issue-edit.sh -i <issue_number> [-t <title>] [-b <body>] [-l <labels>] [-m <milestone>]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Parse arguments
|
||||
ISSUE_NUMBER=""
|
||||
TITLE=""
|
||||
BODY=""
|
||||
LABELS=""
|
||||
MILESTONE=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-i|--issue)
|
||||
ISSUE_NUMBER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-t|--title)
|
||||
TITLE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-b|--body)
|
||||
BODY="$2"
|
||||
shift 2
|
||||
;;
|
||||
-l|--labels)
|
||||
LABELS="$2"
|
||||
shift 2
|
||||
;;
|
||||
-m|--milestone)
|
||||
MILESTONE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: issue-edit.sh -i <issue_number> [-t <title>] [-b <body>] [-l <labels>] [-m <milestone>]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -i, --issue Issue number (required)"
|
||||
echo " -t, --title New title"
|
||||
echo " -b, --body New body/description"
|
||||
echo " -l, --labels Labels (comma-separated, replaces existing)"
|
||||
echo " -m, --milestone Milestone name"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$ISSUE_NUMBER" ]]; then
|
||||
echo "Error: Issue number is required (-i)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
detect_platform
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
CMD="gh issue edit $ISSUE_NUMBER"
|
||||
[[ -n "$TITLE" ]] && CMD="$CMD --title \"$TITLE\""
|
||||
[[ -n "$BODY" ]] && CMD="$CMD --body \"$BODY\""
|
||||
[[ -n "$LABELS" ]] && CMD="$CMD --add-label \"$LABELS\""
|
||||
[[ -n "$MILESTONE" ]] && CMD="$CMD --milestone \"$MILESTONE\""
|
||||
eval $CMD
|
||||
echo "Updated GitHub issue #$ISSUE_NUMBER"
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
CMD="tea issue edit $ISSUE_NUMBER"
|
||||
[[ -n "$TITLE" ]] && CMD="$CMD --title \"$TITLE\""
|
||||
[[ -n "$BODY" ]] && CMD="$CMD --description \"$BODY\""
|
||||
[[ -n "$LABELS" ]] && CMD="$CMD --add-labels \"$LABELS\""
|
||||
[[ -n "$MILESTONE" ]] && CMD="$CMD --milestone \"$MILESTONE\""
|
||||
eval $CMD
|
||||
echo "Updated Gitea issue #$ISSUE_NUMBER"
|
||||
else
|
||||
echo "Error: Unknown platform"
|
||||
exit 1
|
||||
fi
|
||||
78
rails/git/issue-list.ps1
Normal file
78
rails/git/issue-list.ps1
Normal file
@@ -0,0 +1,78 @@
|
||||
# issue-list.ps1 - List issues on Gitea or GitHub
|
||||
# Usage: .\issue-list.ps1 [-State state] [-Label label] [-Milestone milestone] [-Assignee assignee]
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Alias("s")]
|
||||
[ValidateSet("open", "closed", "all")]
|
||||
[string]$State = "open",
|
||||
|
||||
[Alias("l")]
|
||||
[string]$Label,
|
||||
|
||||
[Alias("m")]
|
||||
[string]$Milestone,
|
||||
|
||||
[Alias("a")]
|
||||
[string]$Assignee,
|
||||
|
||||
[Alias("n")]
|
||||
[int]$Limit = 100,
|
||||
|
||||
[Alias("h")]
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. "$ScriptDir\detect-platform.ps1"
|
||||
|
||||
function Show-Usage {
|
||||
@"
|
||||
Usage: issue-list.ps1 [OPTIONS]
|
||||
|
||||
List issues from the current repository (Gitea or GitHub).
|
||||
|
||||
Options:
|
||||
-State, -s STATE Filter by state: open, closed, all (default: open)
|
||||
-Label, -l LABEL Filter by label
|
||||
-Milestone, -m NAME Filter by milestone name
|
||||
-Assignee, -a USER Filter by assignee
|
||||
-Limit, -n N Maximum issues to show (default: 100)
|
||||
-Help, -h Show this help message
|
||||
|
||||
Examples:
|
||||
.\issue-list.ps1 # List open issues
|
||||
.\issue-list.ps1 -s all -l bug # All issues with 'bug' label
|
||||
.\issue-list.ps1 -m "0.2.0" # Issues in milestone 0.2.0
|
||||
"@
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($Help) {
|
||||
Show-Usage
|
||||
}
|
||||
|
||||
$platform = Get-GitPlatform
|
||||
|
||||
switch ($platform) {
|
||||
"github" {
|
||||
$cmd = @("gh", "issue", "list", "--state", $State, "--limit", $Limit)
|
||||
if ($Label) { $cmd += @("--label", $Label) }
|
||||
if ($Milestone) { $cmd += @("--milestone", $Milestone) }
|
||||
if ($Assignee) { $cmd += @("--assignee", $Assignee) }
|
||||
& $cmd[0] $cmd[1..($cmd.Length-1)]
|
||||
}
|
||||
"gitea" {
|
||||
$cmd = @("tea", "issues", "list", "--state", $State, "--limit", $Limit)
|
||||
if ($Label) { $cmd += @("--labels", $Label) }
|
||||
if ($Milestone) { $cmd += @("--milestones", $Milestone) }
|
||||
& $cmd[0] $cmd[1..($cmd.Length-1)]
|
||||
if ($Assignee) {
|
||||
Write-Warning "Assignee filtering may require manual review for Gitea"
|
||||
}
|
||||
}
|
||||
default {
|
||||
Write-Error "Could not detect git platform"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
96
rails/git/issue-list.sh
Executable file
96
rails/git/issue-list.sh
Executable file
@@ -0,0 +1,96 @@
|
||||
#!/bin/bash
|
||||
# issue-list.sh - List issues on Gitea or GitHub
|
||||
# Usage: issue-list.sh [-s state] [-l label] [-m milestone] [-a assignee]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Default values
|
||||
STATE="open"
|
||||
LABEL=""
|
||||
MILESTONE=""
|
||||
ASSIGNEE=""
|
||||
LIMIT=100
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [OPTIONS]
|
||||
|
||||
List issues from the current repository (Gitea or GitHub).
|
||||
|
||||
Options:
|
||||
-s, --state STATE Filter by state: open, closed, all (default: open)
|
||||
-l, --label LABEL Filter by label
|
||||
-m, --milestone NAME Filter by milestone name
|
||||
-a, --assignee USER Filter by assignee
|
||||
-n, --limit N Maximum issues to show (default: 100)
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
$(basename "$0") # List open issues
|
||||
$(basename "$0") -s all -l bug # All issues with 'bug' label
|
||||
$(basename "$0") -m "0.2.0" # Issues in milestone 0.2.0
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-s|--state)
|
||||
STATE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-l|--label)
|
||||
LABEL="$2"
|
||||
shift 2
|
||||
;;
|
||||
-m|--milestone)
|
||||
MILESTONE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-a|--assignee)
|
||||
ASSIGNEE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-n|--limit)
|
||||
LIMIT="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
PLATFORM=$(detect_platform)
|
||||
|
||||
case "$PLATFORM" in
|
||||
github)
|
||||
CMD="gh issue list --state $STATE --limit $LIMIT"
|
||||
[[ -n "$LABEL" ]] && CMD="$CMD --label \"$LABEL\""
|
||||
[[ -n "$MILESTONE" ]] && CMD="$CMD --milestone \"$MILESTONE\""
|
||||
[[ -n "$ASSIGNEE" ]] && CMD="$CMD --assignee \"$ASSIGNEE\""
|
||||
eval "$CMD"
|
||||
;;
|
||||
gitea)
|
||||
CMD="tea issues list --state $STATE --limit $LIMIT"
|
||||
[[ -n "$LABEL" ]] && CMD="$CMD --labels \"$LABEL\""
|
||||
[[ -n "$MILESTONE" ]] && CMD="$CMD --milestones \"$MILESTONE\""
|
||||
# Note: tea may not support assignee filter directly
|
||||
eval "$CMD"
|
||||
if [[ -n "$ASSIGNEE" ]]; then
|
||||
echo "Note: Assignee filtering may require manual review for Gitea" >&2
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Error: Could not detect git platform" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
62
rails/git/issue-reopen.sh
Executable file
62
rails/git/issue-reopen.sh
Executable file
@@ -0,0 +1,62 @@
|
||||
#!/bin/bash
|
||||
# issue-reopen.sh - Reopen a closed issue on GitHub or Gitea
|
||||
# Usage: issue-reopen.sh -i <issue_number> [-c <comment>]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Parse arguments
|
||||
ISSUE_NUMBER=""
|
||||
COMMENT=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-i|--issue)
|
||||
ISSUE_NUMBER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-c|--comment)
|
||||
COMMENT="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: issue-reopen.sh -i <issue_number> [-c <comment>]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -i, --issue Issue number (required)"
|
||||
echo " -c, --comment Comment to add when reopening (optional)"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$ISSUE_NUMBER" ]]; then
|
||||
echo "Error: Issue number is required (-i)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
detect_platform
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
if [[ -n "$COMMENT" ]]; then
|
||||
gh issue comment "$ISSUE_NUMBER" --body "$COMMENT"
|
||||
fi
|
||||
gh issue reopen "$ISSUE_NUMBER"
|
||||
echo "Reopened GitHub issue #$ISSUE_NUMBER"
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
if [[ -n "$COMMENT" ]]; then
|
||||
tea issue comment "$ISSUE_NUMBER" "$COMMENT"
|
||||
fi
|
||||
tea issue reopen "$ISSUE_NUMBER"
|
||||
echo "Reopened Gitea issue #$ISSUE_NUMBER"
|
||||
else
|
||||
echo "Error: Unknown platform"
|
||||
exit 1
|
||||
fi
|
||||
48
rails/git/issue-view.sh
Executable file
48
rails/git/issue-view.sh
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
# issue-view.sh - View issue details on GitHub or Gitea
|
||||
# Usage: issue-view.sh -i <issue_number>
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Parse arguments
|
||||
ISSUE_NUMBER=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-i|--issue)
|
||||
ISSUE_NUMBER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: issue-view.sh -i <issue_number>"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -i, --issue Issue number (required)"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$ISSUE_NUMBER" ]]; then
|
||||
echo "Error: Issue number is required (-i)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
detect_platform
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
gh issue view "$ISSUE_NUMBER"
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
tea issue "$ISSUE_NUMBER"
|
||||
else
|
||||
echo "Error: Unknown platform"
|
||||
exit 1
|
||||
fi
|
||||
50
rails/git/milestone-close.sh
Executable file
50
rails/git/milestone-close.sh
Executable file
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
# milestone-close.sh - Close a milestone on GitHub or Gitea
|
||||
# Usage: milestone-close.sh -t <title>
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Parse arguments
|
||||
TITLE=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-t|--title)
|
||||
TITLE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: milestone-close.sh -t <title>"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -t, --title Milestone title (required)"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$TITLE" ]]; then
|
||||
echo "Error: Milestone title is required (-t)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
detect_platform
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
gh api -X PATCH "/repos/{owner}/{repo}/milestones/$(gh api "/repos/{owner}/{repo}/milestones" --jq ".[] | select(.title==\"$TITLE\") | .number")" -f state=closed
|
||||
echo "Closed GitHub milestone: $TITLE"
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
tea milestone close "$TITLE"
|
||||
echo "Closed Gitea milestone: $TITLE"
|
||||
else
|
||||
echo "Error: Unknown platform"
|
||||
exit 1
|
||||
fi
|
||||
98
rails/git/milestone-create.ps1
Normal file
98
rails/git/milestone-create.ps1
Normal file
@@ -0,0 +1,98 @@
|
||||
# milestone-create.ps1 - Create milestones on Gitea or GitHub
|
||||
# Usage: .\milestone-create.ps1 -Title "Title" [-Description "Description"] [-Due "YYYY-MM-DD"]
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Alias("t")]
|
||||
[string]$Title,
|
||||
|
||||
[Alias("d")]
|
||||
[string]$Description,
|
||||
|
||||
[string]$Due,
|
||||
|
||||
[switch]$List,
|
||||
|
||||
[Alias("h")]
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. "$ScriptDir\detect-platform.ps1"
|
||||
|
||||
function Show-Usage {
|
||||
@"
|
||||
Usage: milestone-create.ps1 [OPTIONS]
|
||||
|
||||
Create or list milestones on the current repository (Gitea or GitHub).
|
||||
|
||||
Versioning Convention:
|
||||
- Features get dedicated milestones
|
||||
- Pre-release: 0.X.0 for breaking changes, 0.X.Y for patches
|
||||
- Post-release: X.0.0 for breaking changes
|
||||
- MVP starts at 0.1.0
|
||||
|
||||
Options:
|
||||
-Title, -t TITLE Milestone title/version (e.g., "0.2.0")
|
||||
-Description, -d DESC Milestone description
|
||||
-Due DATE Due date (YYYY-MM-DD format)
|
||||
-List List existing milestones
|
||||
-Help, -h Show this help message
|
||||
|
||||
Examples:
|
||||
.\milestone-create.ps1 -List
|
||||
.\milestone-create.ps1 -t "0.1.0" -d "MVP Release"
|
||||
.\milestone-create.ps1 -t "0.2.0" -d "User Authentication Feature" -Due "2025-03-01"
|
||||
"@
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($Help) {
|
||||
Show-Usage
|
||||
}
|
||||
|
||||
$platform = Get-GitPlatform
|
||||
|
||||
if ($List) {
|
||||
switch ($platform) {
|
||||
"github" {
|
||||
gh api repos/:owner/:repo/milestones --jq '.[] | "\(.number)`t\(.title)`t\(.state)`t\(.open_issues)/\(.closed_issues) issues"'
|
||||
}
|
||||
"gitea" {
|
||||
tea milestones list
|
||||
}
|
||||
default {
|
||||
Write-Error "Could not detect git platform"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
exit 0
|
||||
}
|
||||
|
||||
if (-not $Title) {
|
||||
Write-Error "Title is required (-t) for creating milestones"
|
||||
Show-Usage
|
||||
}
|
||||
|
||||
switch ($platform) {
|
||||
"github" {
|
||||
$payload = @{ title = $Title }
|
||||
if ($Description) { $payload.description = $Description }
|
||||
if ($Due) { $payload.due_on = "${Due}T00:00:00Z" }
|
||||
|
||||
$json = $payload | ConvertTo-Json -Compress
|
||||
$json | gh api repos/:owner/:repo/milestones --method POST --input -
|
||||
Write-Host "Milestone '$Title' created successfully"
|
||||
}
|
||||
"gitea" {
|
||||
$cmd = @("tea", "milestones", "create", "--title", $Title)
|
||||
if ($Description) { $cmd += @("--description", $Description) }
|
||||
if ($Due) { $cmd += @("--deadline", $Due) }
|
||||
& $cmd[0] $cmd[1..($cmd.Length-1)]
|
||||
Write-Host "Milestone '$Title' created successfully"
|
||||
}
|
||||
default {
|
||||
Write-Error "Could not detect git platform"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
117
rails/git/milestone-create.sh
Executable file
117
rails/git/milestone-create.sh
Executable file
@@ -0,0 +1,117 @@
|
||||
#!/bin/bash
|
||||
# milestone-create.sh - Create milestones on Gitea or GitHub
|
||||
# Usage: milestone-create.sh -t "Title" [-d "Description"] [--due "YYYY-MM-DD"]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Default values
|
||||
TITLE=""
|
||||
DESCRIPTION=""
|
||||
DUE_DATE=""
|
||||
LIST_ONLY=false
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [OPTIONS]
|
||||
|
||||
Create or list milestones on the current repository (Gitea or GitHub).
|
||||
|
||||
Versioning Convention:
|
||||
- Features get dedicated milestones
|
||||
- Pre-release: 0.X.0 for breaking changes, 0.X.Y for patches
|
||||
- Post-release: X.0.0 for breaking changes
|
||||
- MVP starts at 0.1.0
|
||||
|
||||
Options:
|
||||
-t, --title TITLE Milestone title/version (e.g., "0.2.0")
|
||||
-d, --desc DESCRIPTION Milestone description
|
||||
--due DATE Due date (YYYY-MM-DD format)
|
||||
--list List existing milestones
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
$(basename "$0") --list
|
||||
$(basename "$0") -t "0.1.0" -d "MVP Release"
|
||||
$(basename "$0") -t "0.2.0" -d "User Authentication Feature" --due "2025-03-01"
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-t|--title)
|
||||
TITLE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-d|--desc)
|
||||
DESCRIPTION="$2"
|
||||
shift 2
|
||||
;;
|
||||
--due)
|
||||
DUE_DATE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--list)
|
||||
LIST_ONLY=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
PLATFORM=$(detect_platform)
|
||||
|
||||
if [[ "$LIST_ONLY" == true ]]; then
|
||||
case "$PLATFORM" in
|
||||
github)
|
||||
gh api repos/:owner/:repo/milestones --jq '.[] | "\(.number)\t\(.title)\t\(.state)\t\(.open_issues)/\(.closed_issues) issues"'
|
||||
;;
|
||||
gitea)
|
||||
tea milestones list
|
||||
;;
|
||||
*)
|
||||
echo "Error: Could not detect git platform" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -z "$TITLE" ]]; then
|
||||
echo "Error: Title is required (-t) for creating milestones" >&2
|
||||
usage
|
||||
fi
|
||||
|
||||
case "$PLATFORM" in
|
||||
github)
|
||||
# GitHub uses the API for milestone creation
|
||||
JSON_PAYLOAD="{\"title\":\"$TITLE\""
|
||||
[[ -n "$DESCRIPTION" ]] && JSON_PAYLOAD="$JSON_PAYLOAD,\"description\":\"$DESCRIPTION\""
|
||||
[[ -n "$DUE_DATE" ]] && JSON_PAYLOAD="$JSON_PAYLOAD,\"due_on\":\"${DUE_DATE}T00:00:00Z\""
|
||||
JSON_PAYLOAD="$JSON_PAYLOAD}"
|
||||
|
||||
gh api repos/:owner/:repo/milestones --method POST --input - <<< "$JSON_PAYLOAD"
|
||||
echo "Milestone '$TITLE' created successfully"
|
||||
;;
|
||||
gitea)
|
||||
CMD="tea milestones create --title \"$TITLE\""
|
||||
[[ -n "$DESCRIPTION" ]] && CMD="$CMD --description \"$DESCRIPTION\""
|
||||
[[ -n "$DUE_DATE" ]] && CMD="$CMD --deadline \"$DUE_DATE\""
|
||||
eval "$CMD"
|
||||
echo "Milestone '$TITLE' created successfully"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Could not detect git platform" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
43
rails/git/milestone-list.sh
Executable file
43
rails/git/milestone-list.sh
Executable file
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
# milestone-list.sh - List milestones on GitHub or Gitea
|
||||
# Usage: milestone-list.sh [-s <state>]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Parse arguments
|
||||
STATE="open"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-s|--state)
|
||||
STATE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: milestone-list.sh [-s <state>]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -s, --state Filter by state: open, closed, all (default: open)"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
detect_platform
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
gh api "/repos/{owner}/{repo}/milestones?state=$STATE" --jq '.[] | "\(.title) (\(.state)) - \(.open_issues) open, \(.closed_issues) closed"'
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
tea milestone list
|
||||
else
|
||||
echo "Error: Unknown platform"
|
||||
exit 1
|
||||
fi
|
||||
62
rails/git/pr-close.sh
Executable file
62
rails/git/pr-close.sh
Executable file
@@ -0,0 +1,62 @@
|
||||
#!/bin/bash
|
||||
# pr-close.sh - Close a pull request without merging on GitHub or Gitea
|
||||
# Usage: pr-close.sh -n <pr_number> [-c <comment>]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Parse arguments
|
||||
PR_NUMBER=""
|
||||
COMMENT=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-n|--number)
|
||||
PR_NUMBER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-c|--comment)
|
||||
COMMENT="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: pr-close.sh -n <pr_number> [-c <comment>]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -n, --number PR number (required)"
|
||||
echo " -c, --comment Comment before closing (optional)"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$PR_NUMBER" ]]; then
|
||||
echo "Error: PR number is required (-n)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
detect_platform
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
if [[ -n "$COMMENT" ]]; then
|
||||
gh pr comment "$PR_NUMBER" --body "$COMMENT"
|
||||
fi
|
||||
gh pr close "$PR_NUMBER"
|
||||
echo "Closed GitHub PR #$PR_NUMBER"
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
if [[ -n "$COMMENT" ]]; then
|
||||
tea pr comment "$PR_NUMBER" "$COMMENT"
|
||||
fi
|
||||
tea pr close "$PR_NUMBER"
|
||||
echo "Closed Gitea PR #$PR_NUMBER"
|
||||
else
|
||||
echo "Error: Unknown platform"
|
||||
exit 1
|
||||
fi
|
||||
130
rails/git/pr-create.ps1
Normal file
130
rails/git/pr-create.ps1
Normal file
@@ -0,0 +1,130 @@
|
||||
# pr-create.ps1 - Create pull requests on Gitea or GitHub
|
||||
# Usage: .\pr-create.ps1 -Title "Title" [-Body "Body"] [-Base base] [-Head head] [-Labels "labels"] [-Milestone "milestone"]
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Alias("t")]
|
||||
[string]$Title,
|
||||
|
||||
[Alias("b")]
|
||||
[string]$Body,
|
||||
|
||||
[Alias("B")]
|
||||
[string]$Base,
|
||||
|
||||
[Alias("H")]
|
||||
[string]$Head,
|
||||
|
||||
[Alias("l")]
|
||||
[string]$Labels,
|
||||
|
||||
[Alias("m")]
|
||||
[string]$Milestone,
|
||||
|
||||
[Alias("i")]
|
||||
[int]$Issue,
|
||||
|
||||
[Alias("d")]
|
||||
[switch]$Draft,
|
||||
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. "$ScriptDir\detect-platform.ps1"
|
||||
|
||||
function Show-Usage {
|
||||
@"
|
||||
Usage: pr-create.ps1 [OPTIONS]
|
||||
|
||||
Create a pull request on the current repository (Gitea or GitHub).
|
||||
|
||||
Options:
|
||||
-Title, -t TITLE PR title (required, or use -Issue)
|
||||
-Body, -b BODY PR description/body
|
||||
-Base, -B BRANCH Base branch to merge into (default: main/master)
|
||||
-Head, -H BRANCH Head branch with changes (default: current branch)
|
||||
-Labels, -l LABELS Comma-separated labels
|
||||
-Milestone, -m NAME Milestone name
|
||||
-Issue, -i NUMBER Link to issue (auto-generates title if not provided)
|
||||
-Draft, -d Create as draft PR
|
||||
-Help Show this help message
|
||||
|
||||
Examples:
|
||||
.\pr-create.ps1 -Title "Add login feature" -Body "Implements user authentication"
|
||||
.\pr-create.ps1 -t "Fix bug" -B main -H feature/fix-123
|
||||
.\pr-create.ps1 -i 42 -b "Implements the feature described in #42"
|
||||
.\pr-create.ps1 -t "WIP: New feature" -Draft
|
||||
"@
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($Help) {
|
||||
Show-Usage
|
||||
}
|
||||
|
||||
# If no title but issue provided, generate title
|
||||
if (-not $Title -and $Issue) {
|
||||
$Title = "Fixes #$Issue"
|
||||
}
|
||||
|
||||
if (-not $Title) {
|
||||
Write-Error "Title is required (-t) or provide an issue (-i)"
|
||||
Show-Usage
|
||||
}
|
||||
|
||||
# Default head branch to current branch
|
||||
if (-not $Head) {
|
||||
$Head = git branch --show-current
|
||||
}
|
||||
|
||||
# Add issue reference to body if provided
|
||||
if ($Issue) {
|
||||
if ($Body) {
|
||||
$Body = "$Body`n`nFixes #$Issue"
|
||||
} else {
|
||||
$Body = "Fixes #$Issue"
|
||||
}
|
||||
}
|
||||
|
||||
$platform = Get-GitPlatform
|
||||
|
||||
switch ($platform) {
|
||||
"github" {
|
||||
$cmd = @("gh", "pr", "create", "--title", $Title)
|
||||
if ($Body) { $cmd += @("--body", $Body) }
|
||||
if ($Base) { $cmd += @("--base", $Base) }
|
||||
if ($Head) { $cmd += @("--head", $Head) }
|
||||
if ($Labels) { $cmd += @("--label", $Labels) }
|
||||
if ($Milestone) { $cmd += @("--milestone", $Milestone) }
|
||||
if ($Draft) { $cmd += "--draft" }
|
||||
& $cmd[0] $cmd[1..($cmd.Length-1)]
|
||||
}
|
||||
"gitea" {
|
||||
$cmd = @("tea", "pr", "create", "--title", $Title)
|
||||
if ($Body) { $cmd += @("--description", $Body) }
|
||||
if ($Base) { $cmd += @("--base", $Base) }
|
||||
if ($Head) { $cmd += @("--head", $Head) }
|
||||
if ($Labels) { $cmd += @("--labels", $Labels) }
|
||||
|
||||
if ($Milestone) {
|
||||
$milestoneList = tea milestones list 2>$null
|
||||
$milestoneId = ($milestoneList | Select-String "^\s*(\d+).*$Milestone" | ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -First 1)
|
||||
if ($milestoneId) {
|
||||
$cmd += @("--milestone", $milestoneId)
|
||||
} else {
|
||||
Write-Warning "Could not find milestone '$Milestone', creating without milestone"
|
||||
}
|
||||
}
|
||||
|
||||
if ($Draft) {
|
||||
Write-Warning "Draft PR may not be supported by your tea version"
|
||||
}
|
||||
|
||||
& $cmd[0] $cmd[1..($cmd.Length-1)]
|
||||
}
|
||||
default {
|
||||
Write-Error "Could not detect git platform"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
164
rails/git/pr-create.sh
Executable file
164
rails/git/pr-create.sh
Executable file
@@ -0,0 +1,164 @@
|
||||
#!/bin/bash
|
||||
# pr-create.sh - Create pull requests on Gitea or GitHub
|
||||
# Usage: pr-create.sh -t "Title" [-b "Body"] [-B base] [-H head] [-l "labels"] [-m "milestone"]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Default values
|
||||
TITLE=""
|
||||
BODY=""
|
||||
BASE_BRANCH=""
|
||||
HEAD_BRANCH=""
|
||||
LABELS=""
|
||||
MILESTONE=""
|
||||
DRAFT=false
|
||||
ISSUE=""
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [OPTIONS]
|
||||
|
||||
Create a pull request on the current repository (Gitea or GitHub).
|
||||
|
||||
Options:
|
||||
-t, --title TITLE PR title (required, or use --issue)
|
||||
-b, --body BODY PR description/body
|
||||
-B, --base BRANCH Base branch to merge into (default: main/master)
|
||||
-H, --head BRANCH Head branch with changes (default: current branch)
|
||||
-l, --labels LABELS Comma-separated labels
|
||||
-m, --milestone NAME Milestone name
|
||||
-i, --issue NUMBER Link to issue (auto-generates title if not provided)
|
||||
-d, --draft Create as draft PR
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
$(basename "$0") -t "Add login feature" -b "Implements user authentication"
|
||||
$(basename "$0") -t "Fix bug" -B main -H feature/fix-123
|
||||
$(basename "$0") -i 42 -b "Implements the feature described in #42"
|
||||
$(basename "$0") -t "WIP: New feature" --draft
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-t|--title)
|
||||
TITLE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-b|--body)
|
||||
BODY="$2"
|
||||
shift 2
|
||||
;;
|
||||
-B|--base)
|
||||
BASE_BRANCH="$2"
|
||||
shift 2
|
||||
;;
|
||||
-H|--head)
|
||||
HEAD_BRANCH="$2"
|
||||
shift 2
|
||||
;;
|
||||
-l|--labels)
|
||||
LABELS="$2"
|
||||
shift 2
|
||||
;;
|
||||
-m|--milestone)
|
||||
MILESTONE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-i|--issue)
|
||||
ISSUE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-d|--draft)
|
||||
DRAFT=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# If no title but issue provided, generate title
|
||||
if [[ -z "$TITLE" ]] && [[ -n "$ISSUE" ]]; then
|
||||
TITLE="Fixes #$ISSUE"
|
||||
fi
|
||||
|
||||
if [[ -z "$TITLE" ]]; then
|
||||
echo "Error: Title is required (-t) or provide an issue (-i)" >&2
|
||||
usage
|
||||
fi
|
||||
|
||||
# Default head branch to current branch
|
||||
if [[ -z "$HEAD_BRANCH" ]]; then
|
||||
HEAD_BRANCH=$(git branch --show-current)
|
||||
fi
|
||||
|
||||
# Add issue reference to body if provided
|
||||
if [[ -n "$ISSUE" ]]; then
|
||||
if [[ -n "$BODY" ]]; then
|
||||
BODY="$BODY
|
||||
|
||||
Fixes #$ISSUE"
|
||||
else
|
||||
BODY="Fixes #$ISSUE"
|
||||
fi
|
||||
fi
|
||||
|
||||
PLATFORM=$(detect_platform)
|
||||
|
||||
case "$PLATFORM" in
|
||||
github)
|
||||
CMD="gh pr create --title \"$TITLE\""
|
||||
[[ -n "$BODY" ]] && CMD="$CMD --body \"$BODY\""
|
||||
[[ -n "$BASE_BRANCH" ]] && CMD="$CMD --base \"$BASE_BRANCH\""
|
||||
[[ -n "$HEAD_BRANCH" ]] && CMD="$CMD --head \"$HEAD_BRANCH\""
|
||||
[[ -n "$LABELS" ]] && CMD="$CMD --label \"$LABELS\""
|
||||
[[ -n "$MILESTONE" ]] && CMD="$CMD --milestone \"$MILESTONE\""
|
||||
[[ "$DRAFT" == true ]] && CMD="$CMD --draft"
|
||||
eval "$CMD"
|
||||
;;
|
||||
gitea)
|
||||
# tea pull create syntax
|
||||
CMD="tea pr create --title \"$TITLE\""
|
||||
[[ -n "$BODY" ]] && CMD="$CMD --description \"$BODY\""
|
||||
[[ -n "$BASE_BRANCH" ]] && CMD="$CMD --base \"$BASE_BRANCH\""
|
||||
[[ -n "$HEAD_BRANCH" ]] && CMD="$CMD --head \"$HEAD_BRANCH\""
|
||||
|
||||
# Handle labels for tea
|
||||
if [[ -n "$LABELS" ]]; then
|
||||
# tea may use --labels flag
|
||||
CMD="$CMD --labels \"$LABELS\""
|
||||
fi
|
||||
|
||||
# Handle milestone for tea
|
||||
if [[ -n "$MILESTONE" ]]; then
|
||||
MILESTONE_ID=$(tea milestones list 2>/dev/null | grep -E "^\s*[0-9]+" | grep "$MILESTONE" | awk '{print $1}' | head -1)
|
||||
if [[ -n "$MILESTONE_ID" ]]; then
|
||||
CMD="$CMD --milestone $MILESTONE_ID"
|
||||
else
|
||||
echo "Warning: Could not find milestone '$MILESTONE', creating without milestone" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
# Note: tea may not support --draft flag in all versions
|
||||
if [[ "$DRAFT" == true ]]; then
|
||||
echo "Note: Draft PR may not be supported by your tea version" >&2
|
||||
fi
|
||||
|
||||
eval "$CMD"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Could not detect git platform" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
88
rails/git/pr-diff.sh
Executable file
88
rails/git/pr-diff.sh
Executable file
@@ -0,0 +1,88 @@
|
||||
#!/bin/bash
|
||||
# pr-diff.sh - Get the diff for a pull request on GitHub or Gitea
|
||||
# Usage: pr-diff.sh -n <pr_number> [-o <output_file>]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Parse arguments
|
||||
PR_NUMBER=""
|
||||
OUTPUT_FILE=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-n|--number)
|
||||
PR_NUMBER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-o|--output)
|
||||
OUTPUT_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: pr-diff.sh -n <pr_number> [-o <output_file>]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -n, --number PR number (required)"
|
||||
echo " -o, --output Output file (optional, prints to stdout if omitted)"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$PR_NUMBER" ]]; then
|
||||
echo "Error: PR number is required (-n)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
detect_platform > /dev/null
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
if [[ -n "$OUTPUT_FILE" ]]; then
|
||||
gh pr diff "$PR_NUMBER" > "$OUTPUT_FILE"
|
||||
else
|
||||
gh pr diff "$PR_NUMBER"
|
||||
fi
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
# tea doesn't have a direct diff command — use the API
|
||||
OWNER=$(get_repo_owner)
|
||||
REPO=$(get_repo_name)
|
||||
REMOTE_URL=$(git remote get-url origin 2>/dev/null)
|
||||
|
||||
# Extract host from remote URL
|
||||
if [[ "$REMOTE_URL" == https://* ]]; then
|
||||
HOST=$(echo "$REMOTE_URL" | sed -E 's|https://([^/]+)/.*|\1|')
|
||||
elif [[ "$REMOTE_URL" == git@* ]]; then
|
||||
HOST=$(echo "$REMOTE_URL" | sed -E 's|git@([^:]+):.*|\1|')
|
||||
else
|
||||
echo "Error: Cannot determine host from remote URL" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DIFF_URL="https://${HOST}/api/v1/repos/${OWNER}/${REPO}/pulls/${PR_NUMBER}.diff"
|
||||
|
||||
# Use tea's auth token if available
|
||||
TEA_TOKEN=$(tea login list 2>/dev/null | grep "$HOST" | awk '{print $NF}' || true)
|
||||
|
||||
if [[ -n "$TEA_TOKEN" ]]; then
|
||||
DIFF_CONTENT=$(curl -sS -H "Authorization: token $TEA_TOKEN" "$DIFF_URL")
|
||||
else
|
||||
DIFF_CONTENT=$(curl -sS "$DIFF_URL")
|
||||
fi
|
||||
|
||||
if [[ -n "$OUTPUT_FILE" ]]; then
|
||||
echo "$DIFF_CONTENT" > "$OUTPUT_FILE"
|
||||
else
|
||||
echo "$DIFF_CONTENT"
|
||||
fi
|
||||
else
|
||||
echo "Error: Unknown platform" >&2
|
||||
exit 1
|
||||
fi
|
||||
76
rails/git/pr-list.ps1
Normal file
76
rails/git/pr-list.ps1
Normal file
@@ -0,0 +1,76 @@
|
||||
# pr-list.ps1 - List pull requests on Gitea or GitHub
|
||||
# Usage: .\pr-list.ps1 [-State state] [-Label label] [-Author author]
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Alias("s")]
|
||||
[ValidateSet("open", "closed", "merged", "all")]
|
||||
[string]$State = "open",
|
||||
|
||||
[Alias("l")]
|
||||
[string]$Label,
|
||||
|
||||
[Alias("a")]
|
||||
[string]$Author,
|
||||
|
||||
[Alias("n")]
|
||||
[int]$Limit = 100,
|
||||
|
||||
[Alias("h")]
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. "$ScriptDir\detect-platform.ps1"
|
||||
|
||||
function Show-Usage {
|
||||
@"
|
||||
Usage: pr-list.ps1 [OPTIONS]
|
||||
|
||||
List pull requests from the current repository (Gitea or GitHub).
|
||||
|
||||
Options:
|
||||
-State, -s STATE Filter by state: open, closed, merged, all (default: open)
|
||||
-Label, -l LABEL Filter by label
|
||||
-Author, -a USER Filter by author
|
||||
-Limit, -n N Maximum PRs to show (default: 100)
|
||||
-Help, -h Show this help message
|
||||
|
||||
Examples:
|
||||
.\pr-list.ps1 # List open PRs
|
||||
.\pr-list.ps1 -s all # All PRs
|
||||
.\pr-list.ps1 -s merged -a username # Merged PRs by user
|
||||
"@
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($Help) {
|
||||
Show-Usage
|
||||
}
|
||||
|
||||
$platform = Get-GitPlatform
|
||||
|
||||
switch ($platform) {
|
||||
"github" {
|
||||
$cmd = @("gh", "pr", "list", "--state", $State, "--limit", $Limit)
|
||||
if ($Label) { $cmd += @("--label", $Label) }
|
||||
if ($Author) { $cmd += @("--author", $Author) }
|
||||
& $cmd[0] $cmd[1..($cmd.Length-1)]
|
||||
}
|
||||
"gitea" {
|
||||
$cmd = @("tea", "pr", "list", "--state", $State, "--limit", $Limit)
|
||||
|
||||
if ($Label) {
|
||||
Write-Warning "Label filtering may require manual review for Gitea"
|
||||
}
|
||||
if ($Author) {
|
||||
Write-Warning "Author filtering may require manual review for Gitea"
|
||||
}
|
||||
|
||||
& $cmd[0] $cmd[1..($cmd.Length-1)]
|
||||
}
|
||||
default {
|
||||
Write-Error "Could not detect git platform"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
93
rails/git/pr-list.sh
Executable file
93
rails/git/pr-list.sh
Executable file
@@ -0,0 +1,93 @@
|
||||
#!/bin/bash
|
||||
# pr-list.sh - List pull requests on Gitea or GitHub
|
||||
# Usage: pr-list.sh [-s state] [-l label] [-a author]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Default values
|
||||
STATE="open"
|
||||
LABEL=""
|
||||
AUTHOR=""
|
||||
LIMIT=100
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [OPTIONS]
|
||||
|
||||
List pull requests from the current repository (Gitea or GitHub).
|
||||
|
||||
Options:
|
||||
-s, --state STATE Filter by state: open, closed, merged, all (default: open)
|
||||
-l, --label LABEL Filter by label
|
||||
-a, --author USER Filter by author
|
||||
-n, --limit N Maximum PRs to show (default: 100)
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
$(basename "$0") # List open PRs
|
||||
$(basename "$0") -s all # All PRs
|
||||
$(basename "$0") -s merged -a username # Merged PRs by user
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-s|--state)
|
||||
STATE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-l|--label)
|
||||
LABEL="$2"
|
||||
shift 2
|
||||
;;
|
||||
-a|--author)
|
||||
AUTHOR="$2"
|
||||
shift 2
|
||||
;;
|
||||
-n|--limit)
|
||||
LIMIT="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
PLATFORM=$(detect_platform)
|
||||
|
||||
case "$PLATFORM" in
|
||||
github)
|
||||
CMD="gh pr list --state $STATE --limit $LIMIT"
|
||||
[[ -n "$LABEL" ]] && CMD="$CMD --label \"$LABEL\""
|
||||
[[ -n "$AUTHOR" ]] && CMD="$CMD --author \"$AUTHOR\""
|
||||
eval "$CMD"
|
||||
;;
|
||||
gitea)
|
||||
# tea pr list - note: tea uses 'pulls' subcommand in some versions
|
||||
CMD="tea pr list --state $STATE --limit $LIMIT"
|
||||
|
||||
# tea filtering may be limited
|
||||
if [[ -n "$LABEL" ]]; then
|
||||
echo "Note: Label filtering may require manual review for Gitea" >&2
|
||||
fi
|
||||
if [[ -n "$AUTHOR" ]]; then
|
||||
echo "Note: Author filtering may require manual review for Gitea" >&2
|
||||
fi
|
||||
|
||||
eval "$CMD"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Could not detect git platform" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
81
rails/git/pr-merge.ps1
Normal file
81
rails/git/pr-merge.ps1
Normal file
@@ -0,0 +1,81 @@
|
||||
# pr-merge.ps1 - Merge pull requests on Gitea or GitHub
|
||||
# Usage: .\pr-merge.ps1 -Number PR_NUMBER [-Method method] [-DeleteBranch]
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[Alias("n")]
|
||||
[int]$Number,
|
||||
|
||||
[Alias("m")]
|
||||
[ValidateSet("merge", "squash", "rebase")]
|
||||
[string]$Method = "merge",
|
||||
|
||||
[Alias("d")]
|
||||
[switch]$DeleteBranch,
|
||||
|
||||
[Alias("h")]
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. "$ScriptDir\detect-platform.ps1"
|
||||
|
||||
function Show-Usage {
|
||||
@"
|
||||
Usage: pr-merge.ps1 [OPTIONS]
|
||||
|
||||
Merge a pull request on the current repository (Gitea or GitHub).
|
||||
|
||||
Options:
|
||||
-Number, -n NUMBER PR number to merge (required)
|
||||
-Method, -m METHOD Merge method: merge, squash, rebase (default: merge)
|
||||
-DeleteBranch, -d Delete the head branch after merge
|
||||
-Help, -h Show this help message
|
||||
|
||||
Examples:
|
||||
.\pr-merge.ps1 -n 42 # Merge PR #42
|
||||
.\pr-merge.ps1 -n 42 -m squash # Squash merge
|
||||
.\pr-merge.ps1 -n 42 -m rebase -d # Rebase and delete branch
|
||||
"@
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($Help) {
|
||||
Show-Usage
|
||||
}
|
||||
|
||||
$platform = Get-GitPlatform
|
||||
|
||||
switch ($platform) {
|
||||
"github" {
|
||||
$cmd = @("gh", "pr", "merge", $Number)
|
||||
switch ($Method) {
|
||||
"merge" { $cmd += "--merge" }
|
||||
"squash" { $cmd += "--squash" }
|
||||
"rebase" { $cmd += "--rebase" }
|
||||
}
|
||||
if ($DeleteBranch) { $cmd += "--delete-branch" }
|
||||
& $cmd[0] $cmd[1..($cmd.Length-1)]
|
||||
}
|
||||
"gitea" {
|
||||
$cmd = @("tea", "pr", "merge", $Number)
|
||||
switch ($Method) {
|
||||
"merge" { $cmd += @("--style", "merge") }
|
||||
"squash" { $cmd += @("--style", "squash") }
|
||||
"rebase" { $cmd += @("--style", "rebase") }
|
||||
}
|
||||
|
||||
if ($DeleteBranch) {
|
||||
Write-Warning "Branch deletion after merge may need to be done separately with tea"
|
||||
}
|
||||
|
||||
& $cmd[0] $cmd[1..($cmd.Length-1)]
|
||||
}
|
||||
default {
|
||||
Write-Error "Could not detect git platform"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "PR #$Number merged successfully"
|
||||
110
rails/git/pr-merge.sh
Executable file
110
rails/git/pr-merge.sh
Executable file
@@ -0,0 +1,110 @@
|
||||
#!/bin/bash
|
||||
# pr-merge.sh - Merge pull requests on Gitea or GitHub
|
||||
# Usage: pr-merge.sh -n PR_NUMBER [-m method] [-d]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Default values
|
||||
PR_NUMBER=""
|
||||
MERGE_METHOD="merge" # merge, squash, rebase
|
||||
DELETE_BRANCH=false
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [OPTIONS]
|
||||
|
||||
Merge a pull request on the current repository (Gitea or GitHub).
|
||||
|
||||
Options:
|
||||
-n, --number NUMBER PR number to merge (required)
|
||||
-m, --method METHOD Merge method: merge, squash, rebase (default: merge)
|
||||
-d, --delete-branch Delete the head branch after merge
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
$(basename "$0") -n 42 # Merge PR #42
|
||||
$(basename "$0") -n 42 -m squash # Squash merge
|
||||
$(basename "$0") -n 42 -m rebase -d # Rebase and delete branch
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-n|--number)
|
||||
PR_NUMBER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-m|--method)
|
||||
MERGE_METHOD="$2"
|
||||
shift 2
|
||||
;;
|
||||
-d|--delete-branch)
|
||||
DELETE_BRANCH=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$PR_NUMBER" ]]; then
|
||||
echo "Error: PR number is required (-n)" >&2
|
||||
usage
|
||||
fi
|
||||
|
||||
PLATFORM=$(detect_platform)
|
||||
|
||||
case "$PLATFORM" in
|
||||
github)
|
||||
CMD="gh pr merge $PR_NUMBER"
|
||||
case "$MERGE_METHOD" in
|
||||
merge) CMD="$CMD --merge" ;;
|
||||
squash) CMD="$CMD --squash" ;;
|
||||
rebase) CMD="$CMD --rebase" ;;
|
||||
*)
|
||||
echo "Error: Invalid merge method '$MERGE_METHOD'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
[[ "$DELETE_BRANCH" == true ]] && CMD="$CMD --delete-branch"
|
||||
eval "$CMD"
|
||||
;;
|
||||
gitea)
|
||||
# tea pr merge syntax
|
||||
CMD="tea pr merge $PR_NUMBER"
|
||||
|
||||
# tea merge style flags
|
||||
case "$MERGE_METHOD" in
|
||||
merge) CMD="$CMD --style merge" ;;
|
||||
squash) CMD="$CMD --style squash" ;;
|
||||
rebase) CMD="$CMD --style rebase" ;;
|
||||
*)
|
||||
echo "Error: Invalid merge method '$MERGE_METHOD'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Delete branch after merge if requested
|
||||
if [[ "$DELETE_BRANCH" == true ]]; then
|
||||
echo "Note: Branch deletion after merge may need to be done separately with tea" >&2
|
||||
fi
|
||||
|
||||
eval "$CMD"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Could not detect git platform" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "PR #$PR_NUMBER merged successfully"
|
||||
114
rails/git/pr-metadata.sh
Executable file
114
rails/git/pr-metadata.sh
Executable file
@@ -0,0 +1,114 @@
|
||||
#!/bin/bash
|
||||
# pr-metadata.sh - Get PR metadata as JSON on GitHub or Gitea
|
||||
# Usage: pr-metadata.sh -n <pr_number> [-o <output_file>]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Parse arguments
|
||||
PR_NUMBER=""
|
||||
OUTPUT_FILE=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-n|--number)
|
||||
PR_NUMBER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-o|--output)
|
||||
OUTPUT_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: pr-metadata.sh -n <pr_number> [-o <output_file>]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -n, --number PR number (required)"
|
||||
echo " -o, --output Output file (optional, prints to stdout if omitted)"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$PR_NUMBER" ]]; then
|
||||
echo "Error: PR number is required (-n)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
detect_platform > /dev/null
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
METADATA=$(gh pr view "$PR_NUMBER" --json number,title,body,state,author,headRefName,baseRefName,files,labels,assignees,milestone,createdAt,updatedAt,url,isDraft)
|
||||
|
||||
if [[ -n "$OUTPUT_FILE" ]]; then
|
||||
echo "$METADATA" > "$OUTPUT_FILE"
|
||||
else
|
||||
echo "$METADATA"
|
||||
fi
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
OWNER=$(get_repo_owner)
|
||||
REPO=$(get_repo_name)
|
||||
REMOTE_URL=$(git remote get-url origin 2>/dev/null)
|
||||
|
||||
# Extract host from remote URL
|
||||
if [[ "$REMOTE_URL" == https://* ]]; then
|
||||
HOST=$(echo "$REMOTE_URL" | sed -E 's|https://([^/]+)/.*|\1|')
|
||||
elif [[ "$REMOTE_URL" == git@* ]]; then
|
||||
HOST=$(echo "$REMOTE_URL" | sed -E 's|git@([^:]+):.*|\1|')
|
||||
else
|
||||
echo "Error: Cannot determine host from remote URL" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
API_URL="https://${HOST}/api/v1/repos/${OWNER}/${REPO}/pulls/${PR_NUMBER}"
|
||||
|
||||
# Use tea's auth token if available
|
||||
TEA_TOKEN=$(tea login list 2>/dev/null | grep "$HOST" | awk '{print $NF}' || true)
|
||||
|
||||
if [[ -n "$TEA_TOKEN" ]]; then
|
||||
RAW=$(curl -sS -H "Authorization: token $TEA_TOKEN" "$API_URL")
|
||||
else
|
||||
RAW=$(curl -sS "$API_URL")
|
||||
fi
|
||||
|
||||
# Normalize Gitea response to match our expected schema
|
||||
METADATA=$(echo "$RAW" | python3 -c "
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
normalized = {
|
||||
'number': data.get('number'),
|
||||
'title': data.get('title'),
|
||||
'body': data.get('body', ''),
|
||||
'state': data.get('state'),
|
||||
'author': data.get('user', {}).get('login', ''),
|
||||
'headRefName': data.get('head', {}).get('ref', ''),
|
||||
'baseRefName': data.get('base', {}).get('ref', ''),
|
||||
'labels': [l.get('name', '') for l in data.get('labels', [])],
|
||||
'assignees': [a.get('login', '') for a in data.get('assignees', [])],
|
||||
'milestone': data.get('milestone', {}).get('title', '') if data.get('milestone') else '',
|
||||
'createdAt': data.get('created_at', ''),
|
||||
'updatedAt': data.get('updated_at', ''),
|
||||
'url': data.get('html_url', ''),
|
||||
'isDraft': data.get('draft', False),
|
||||
'mergeable': data.get('mergeable'),
|
||||
'diffUrl': data.get('diff_url', ''),
|
||||
}
|
||||
json.dump(normalized, sys.stdout, indent=2)
|
||||
")
|
||||
|
||||
if [[ -n "$OUTPUT_FILE" ]]; then
|
||||
echo "$METADATA" > "$OUTPUT_FILE"
|
||||
else
|
||||
echo "$METADATA"
|
||||
fi
|
||||
else
|
||||
echo "Error: Unknown platform" >&2
|
||||
exit 1
|
||||
fi
|
||||
115
rails/git/pr-review.sh
Executable file
115
rails/git/pr-review.sh
Executable file
@@ -0,0 +1,115 @@
|
||||
#!/bin/bash
|
||||
# pr-review.sh - Review a pull request on GitHub or Gitea
|
||||
# Usage: pr-review.sh -n <pr_number> -a <action> [-c <comment>]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Parse arguments
|
||||
PR_NUMBER=""
|
||||
ACTION=""
|
||||
COMMENT=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-n|--number)
|
||||
PR_NUMBER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-a|--action)
|
||||
ACTION="$2"
|
||||
shift 2
|
||||
;;
|
||||
-c|--comment)
|
||||
COMMENT="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: pr-review.sh -n <pr_number> -a <action> [-c <comment>]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -n, --number PR number (required)"
|
||||
echo " -a, --action Review action: approve, request-changes, comment (required)"
|
||||
echo " -c, --comment Review comment (required for request-changes)"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$PR_NUMBER" ]]; then
|
||||
echo "Error: PR number is required (-n)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$ACTION" ]]; then
|
||||
echo "Error: Action is required (-a): approve, request-changes, comment"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
detect_platform
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
case $ACTION in
|
||||
approve)
|
||||
gh pr review "$PR_NUMBER" --approve ${COMMENT:+--body "$COMMENT"}
|
||||
echo "Approved GitHub PR #$PR_NUMBER"
|
||||
;;
|
||||
request-changes)
|
||||
if [[ -z "$COMMENT" ]]; then
|
||||
echo "Error: Comment required for request-changes"
|
||||
exit 1
|
||||
fi
|
||||
gh pr review "$PR_NUMBER" --request-changes --body "$COMMENT"
|
||||
echo "Requested changes on GitHub PR #$PR_NUMBER"
|
||||
;;
|
||||
comment)
|
||||
if [[ -z "$COMMENT" ]]; then
|
||||
echo "Error: Comment required"
|
||||
exit 1
|
||||
fi
|
||||
gh pr review "$PR_NUMBER" --comment --body "$COMMENT"
|
||||
echo "Added review comment to GitHub PR #$PR_NUMBER"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unknown action: $ACTION"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
case $ACTION in
|
||||
approve)
|
||||
tea pr approve "$PR_NUMBER" ${COMMENT:+--comment "$COMMENT"}
|
||||
echo "Approved Gitea PR #$PR_NUMBER"
|
||||
;;
|
||||
request-changes)
|
||||
if [[ -z "$COMMENT" ]]; then
|
||||
echo "Error: Comment required for request-changes"
|
||||
exit 1
|
||||
fi
|
||||
tea pr reject "$PR_NUMBER" --comment "$COMMENT"
|
||||
echo "Requested changes on Gitea PR #$PR_NUMBER"
|
||||
;;
|
||||
comment)
|
||||
if [[ -z "$COMMENT" ]]; then
|
||||
echo "Error: Comment required"
|
||||
exit 1
|
||||
fi
|
||||
tea pr comment "$PR_NUMBER" "$COMMENT"
|
||||
echo "Added comment to Gitea PR #$PR_NUMBER"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unknown action: $ACTION"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
else
|
||||
echo "Error: Unknown platform"
|
||||
exit 1
|
||||
fi
|
||||
48
rails/git/pr-view.sh
Executable file
48
rails/git/pr-view.sh
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
# pr-view.sh - View pull request details on GitHub or Gitea
|
||||
# Usage: pr-view.sh -n <pr_number>
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Parse arguments
|
||||
PR_NUMBER=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-n|--number)
|
||||
PR_NUMBER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: pr-view.sh -n <pr_number>"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -n, --number PR number (required)"
|
||||
echo " -h, --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$PR_NUMBER" ]]; then
|
||||
echo "Error: PR number is required (-n)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
detect_platform
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
gh pr view "$PR_NUMBER"
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
tea pr "$PR_NUMBER"
|
||||
else
|
||||
echo "Error: Unknown platform"
|
||||
exit 1
|
||||
fi
|
||||
15
rails/qa/debug-hook.sh
Executable file
15
rails/qa/debug-hook.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
# Debug hook to identify available variables
|
||||
|
||||
echo "=== Hook Debug ===" >> /tmp/hook-debug.log
|
||||
echo "Date: $(date)" >> /tmp/hook-debug.log
|
||||
echo "All args: $@" >> /tmp/hook-debug.log
|
||||
echo "Arg count: $#" >> /tmp/hook-debug.log
|
||||
echo "Arg 1: ${1:-EMPTY}" >> /tmp/hook-debug.log
|
||||
echo "Arg 2: ${2:-EMPTY}" >> /tmp/hook-debug.log
|
||||
echo "Arg 3: ${3:-EMPTY}" >> /tmp/hook-debug.log
|
||||
echo "Environment:" >> /tmp/hook-debug.log
|
||||
env | grep -i file >> /tmp/hook-debug.log 2>/dev/null || true
|
||||
env | grep -i path >> /tmp/hook-debug.log 2>/dev/null || true
|
||||
env | grep -i tool >> /tmp/hook-debug.log 2>/dev/null || true
|
||||
echo "==================" >> /tmp/hook-debug.log
|
||||
197
rails/qa/qa-hook-handler.sh
Executable file
197
rails/qa/qa-hook-handler.sh
Executable file
@@ -0,0 +1,197 @@
|
||||
#!/bin/bash
|
||||
# Universal QA hook handler with robust error handling
|
||||
# Location: ~/.mosaic/rails/qa-hook-handler.sh
|
||||
|
||||
# Don't exit on unset variables initially to handle missing params gracefully
|
||||
set -eo pipefail
|
||||
|
||||
PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
||||
TOOL_NAME="${1:-}"
|
||||
FILE_PATH="${2:-}"
|
||||
|
||||
# Debug logging
|
||||
echo "[DEBUG] Script called with args: \$1='$1' \$2='$2'" >> "$PROJECT_ROOT/logs/qa-automation.log" 2>/dev/null || true
|
||||
|
||||
# Validate inputs
|
||||
if [ -z "$FILE_PATH" ] || [ -z "$TOOL_NAME" ]; then
|
||||
echo "[ERROR] Missing required parameters: tool='$TOOL_NAME' file='$FILE_PATH'" >&2
|
||||
echo "[ERROR] Usage: $0 <tool> <file_path>" >&2
|
||||
# Log to file if possible
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] Missing parameters - tool='$TOOL_NAME' file='$FILE_PATH'" >> "$PROJECT_ROOT/logs/qa-automation.log" 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Now enable strict mode after parameter handling
|
||||
set -u
|
||||
|
||||
# Skip non-JS/TS files
|
||||
if ! [[ "$FILE_PATH" =~ \.(ts|tsx|js|jsx|mjs|cjs)$ ]]; then
|
||||
echo "[INFO] Skipping non-JS/TS file: $FILE_PATH"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Generate naming components
|
||||
TIMESTAMP=$(date '+%Y%m%d-%H%M')
|
||||
SANITIZED_NAME=$(echo "$FILE_PATH" | sed 's/\//-/g' | sed 's/^-//' | sed 's/\.\./\./g')
|
||||
ITERATION=1
|
||||
|
||||
# Log file for debugging
|
||||
LOG_FILE="$PROJECT_ROOT/logs/qa-automation.log"
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
|
||||
# Function to detect Epic with fallback
|
||||
detect_epic() {
|
||||
local file_path="$1"
|
||||
local epic=""
|
||||
|
||||
# Try to detect Epic from path patterns
|
||||
case "$file_path" in
|
||||
*/apps/frontend/src/components/*adapter*|*/apps/frontend/src/views/*adapter*)
|
||||
epic="E.3001-ADAPTER-CONFIG-SYSTEM"
|
||||
;;
|
||||
*/services/backend/src/adapters/*)
|
||||
epic="E.3001-ADAPTER-CONFIG-SYSTEM"
|
||||
;;
|
||||
*/services/backend/src/*)
|
||||
epic="E.2004-enterprise-data-synchronization-engine"
|
||||
;;
|
||||
*/services/syncagent-debezium/*|*/services/syncagent-n8n/*)
|
||||
epic="E.2004-enterprise-data-synchronization-engine"
|
||||
;;
|
||||
*)
|
||||
epic="" # General QA
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "$epic"
|
||||
}
|
||||
|
||||
# Detect Epic association
|
||||
EPIC_FOLDER=$(detect_epic "$FILE_PATH")
|
||||
|
||||
# Function to setup report directory with creation if needed
|
||||
setup_report_dir() {
|
||||
local epic="$1"
|
||||
local project_root="$2"
|
||||
local report_dir=""
|
||||
|
||||
if [ -n "$epic" ]; then
|
||||
# Check if Epic directory exists
|
||||
local epic_dir="$project_root/docs/task-management/epics/active/$epic"
|
||||
|
||||
if [ -d "$epic_dir" ]; then
|
||||
# Epic exists, use it
|
||||
report_dir="$epic_dir/reports/qa-automation/pending"
|
||||
echo "[INFO] Using existing Epic: $epic" | tee -a "$LOG_FILE"
|
||||
else
|
||||
# Epic doesn't exist, check if we should create it
|
||||
local epic_parent="$project_root/docs/task-management/epics/active"
|
||||
|
||||
if [ -d "$epic_parent" ]; then
|
||||
# Parent exists, create Epic structure
|
||||
echo "[WARN] Epic $epic not found, creating structure..." | tee -a "$LOG_FILE"
|
||||
mkdir -p "$epic_dir/reports/qa-automation/pending"
|
||||
mkdir -p "$epic_dir/reports/qa-automation/in-progress"
|
||||
mkdir -p "$epic_dir/reports/qa-automation/done"
|
||||
mkdir -p "$epic_dir/reports/qa-automation/escalated"
|
||||
|
||||
# Create Epic README
|
||||
cat > "$epic_dir/README.md" << EOF
|
||||
# Epic: $epic
|
||||
|
||||
**Status**: Active
|
||||
**Created**: $(date '+%Y-%m-%d')
|
||||
**Purpose**: Auto-created by QA automation system
|
||||
|
||||
## Description
|
||||
This Epic was automatically created to organize QA remediation reports.
|
||||
|
||||
## QA Automation
|
||||
- Reports are stored in \`reports/qa-automation/\`
|
||||
- Pending issues: \`reports/qa-automation/pending/\`
|
||||
- Escalated issues: \`reports/qa-automation/escalated/\`
|
||||
EOF
|
||||
report_dir="$epic_dir/reports/qa-automation/pending"
|
||||
echo "[INFO] Created Epic structure: $epic" | tee -a "$LOG_FILE"
|
||||
else
|
||||
# Epic structure doesn't exist, fall back to general
|
||||
echo "[WARN] Epic structure not found, using general QA" | tee -a "$LOG_FILE"
|
||||
report_dir="$project_root/docs/reports/qa-automation/pending"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# No Epic association, use general
|
||||
report_dir="$project_root/docs/reports/qa-automation/pending"
|
||||
echo "[INFO] No Epic association, using general QA" | tee -a "$LOG_FILE"
|
||||
fi
|
||||
|
||||
# Ensure directory exists
|
||||
mkdir -p "$report_dir"
|
||||
echo "$report_dir"
|
||||
}
|
||||
|
||||
# Setup report directory (capture only the last line which is the path)
|
||||
REPORT_DIR=$(setup_report_dir "$EPIC_FOLDER" "$PROJECT_ROOT" | tail -1)
|
||||
|
||||
# Check for existing reports from same timestamp
|
||||
check_existing_iteration() {
|
||||
local dir="$1"
|
||||
local name="$2"
|
||||
local timestamp="$3"
|
||||
local max_iter=0
|
||||
|
||||
for file in "$dir"/${name}_${timestamp}_*_remediation_needed.md; do
|
||||
if [ -f "$file" ]; then
|
||||
# Extract iteration number
|
||||
local iter=$(echo "$file" | sed 's/.*_\([0-9]\+\)_remediation_needed\.md$/\1/')
|
||||
if [ "$iter" -gt "$max_iter" ]; then
|
||||
max_iter=$iter
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo $((max_iter + 1))
|
||||
}
|
||||
|
||||
ITERATION=$(check_existing_iteration "$REPORT_DIR" "$SANITIZED_NAME" "$TIMESTAMP")
|
||||
|
||||
# Check if we're at max iterations
|
||||
if [ "$ITERATION" -gt 5 ]; then
|
||||
echo "[ERROR] Max iterations (5) reached for $FILE_PATH" | tee -a "$LOG_FILE"
|
||||
# Move to escalated immediately
|
||||
REPORT_DIR="${REPORT_DIR/pending/escalated}"
|
||||
mkdir -p "$REPORT_DIR"
|
||||
ITERATION=5 # Cap at 5
|
||||
fi
|
||||
|
||||
# Create report filename
|
||||
REPORT_FILE="${SANITIZED_NAME}_${TIMESTAMP}_${ITERATION}_remediation_needed.md"
|
||||
REPORT_PATH="$REPORT_DIR/$REPORT_FILE"
|
||||
|
||||
# Log the action
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] QA Hook: $TOOL_NAME on $FILE_PATH" | tee -a "$LOG_FILE"
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Creating report: $REPORT_PATH" | tee -a "$LOG_FILE"
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Epic: ${EPIC_FOLDER:-general}, Iteration: $ITERATION" | tee -a "$LOG_FILE"
|
||||
|
||||
# Create a task file for the QA agent instead of calling Claude directly
|
||||
cat > "$REPORT_PATH" << EOF
|
||||
# QA Remediation Report
|
||||
|
||||
**File:** $FILE_PATH
|
||||
**Tool Used:** $TOOL_NAME
|
||||
**Epic:** ${EPIC_FOLDER:-general}
|
||||
**Iteration:** $ITERATION
|
||||
**Generated:** $(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
## Status
|
||||
Pending QA validation
|
||||
|
||||
## Next Steps
|
||||
This report was created by the QA automation hook.
|
||||
To process this report, run:
|
||||
\`\`\`bash
|
||||
claude -p "Use Task tool to launch universal-qa-agent for report: $REPORT_PATH"
|
||||
\`\`\`
|
||||
EOF
|
||||
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Created report template at: $REPORT_PATH" | tee -a "$LOG_FILE"
|
||||
59
rails/qa/qa-hook-stdin.sh
Executable file
59
rails/qa/qa-hook-stdin.sh
Executable file
@@ -0,0 +1,59 @@
|
||||
#!/bin/bash
|
||||
# QA Hook handler that reads from stdin
|
||||
# Location: ~/.mosaic/rails/qa-hook-stdin.sh
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
||||
LOG_FILE="$PROJECT_ROOT/logs/qa-automation.log"
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
|
||||
# Read JSON from stdin
|
||||
JSON_INPUT=$(cat)
|
||||
|
||||
# Log raw input for debugging
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Hook triggered with JSON:" >> "$LOG_FILE"
|
||||
echo "$JSON_INPUT" >> "$LOG_FILE"
|
||||
|
||||
# Extract file path using jq if available, otherwise use grep/sed
|
||||
if command -v jq &> /dev/null; then
|
||||
# Try multiple paths - tool_input.file_path is the actual structure from Claude Code
|
||||
FILE_PATH=$(echo "$JSON_INPUT" | jq -r '.tool_input.file_path // .tool_response.filePath // .file_path // .path // .file // empty' 2>/dev/null || echo "")
|
||||
TOOL_NAME=$(echo "$JSON_INPUT" | jq -r '.tool_name // .tool // .matcher // "Edit"' 2>/dev/null || echo "Edit")
|
||||
else
|
||||
# Fallback parsing without jq - search in tool_input first
|
||||
FILE_PATH=$(echo "$JSON_INPUT" | grep -o '"tool_input"[^}]*}' | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' | head -1)
|
||||
if [ -z "$FILE_PATH" ]; then
|
||||
FILE_PATH=$(echo "$JSON_INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' | head -1)
|
||||
fi
|
||||
if [ -z "$FILE_PATH" ]; then
|
||||
FILE_PATH=$(echo "$JSON_INPUT" | grep -o '"filePath"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"filePath"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' | head -1)
|
||||
fi
|
||||
TOOL_NAME=$(echo "$JSON_INPUT" | grep -o '"tool_name"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"tool_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' | head -1)
|
||||
if [ -z "$TOOL_NAME" ]; then
|
||||
TOOL_NAME=$(echo "$JSON_INPUT" | grep -o '"tool"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"tool"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' | head -1)
|
||||
fi
|
||||
[ -z "$TOOL_NAME" ] && TOOL_NAME="Edit"
|
||||
fi
|
||||
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Extracted: tool=$TOOL_NAME file=$FILE_PATH" >> "$LOG_FILE"
|
||||
|
||||
# Validate we got a file path
|
||||
if [ -z "$FILE_PATH" ]; then
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] Could not extract file path from JSON" >> "$LOG_FILE"
|
||||
exit 0 # Exit successfully to not block Claude
|
||||
fi
|
||||
|
||||
# Skip non-JS/TS files
|
||||
if ! [[ "$FILE_PATH" =~ \.(ts|tsx|js|jsx|mjs|cjs)$ ]]; then
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] Skipping non-JS/TS file: $FILE_PATH" >> "$LOG_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Call the main QA handler with extracted parameters
|
||||
if [ -f ~/.mosaic/rails/qa-hook-handler.sh ]; then
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Calling QA handler for $FILE_PATH" >> "$LOG_FILE"
|
||||
~/.mosaic/rails/qa-hook-handler.sh "$TOOL_NAME" "$FILE_PATH" 2>&1 | tee -a "$LOG_FILE"
|
||||
else
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] QA handler script not found" >> "$LOG_FILE"
|
||||
fi
|
||||
19
rails/qa/qa-hook-wrapper.sh
Executable file
19
rails/qa/qa-hook-wrapper.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
# Wrapper script that handles hook invocation more robustly
|
||||
|
||||
# Get the most recently modified JS/TS file as a fallback
|
||||
RECENT_FILE=$(find . -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \) -mmin -1 2>/dev/null | head -1)
|
||||
|
||||
# Use provided file path or fallback to recent file
|
||||
FILE_PATH="${2:-$RECENT_FILE}"
|
||||
TOOL_NAME="${1:-Edit}"
|
||||
|
||||
# Log the attempt
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Hook wrapper called: tool=$TOOL_NAME file=$FILE_PATH" >> logs/qa-automation.log 2>/dev/null || true
|
||||
|
||||
# Call the actual QA handler if we have a file
|
||||
if [ -n "$FILE_PATH" ]; then
|
||||
~/.mosaic/rails/qa-hook-handler.sh "$TOOL_NAME" "$FILE_PATH"
|
||||
else
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] No file path available for QA check" >> logs/qa-automation.log 2>/dev/null || true
|
||||
fi
|
||||
91
rails/qa/qa-queue-monitor.sh
Executable file
91
rails/qa/qa-queue-monitor.sh
Executable file
@@ -0,0 +1,91 @@
|
||||
#!/bin/bash
|
||||
# Monitor QA queues with graceful handling of missing directories
|
||||
# Location: ~/.mosaic/rails/qa-queue-monitor.sh
|
||||
|
||||
PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
||||
|
||||
echo "=== QA Automation Queue Status ==="
|
||||
echo "Project: $(basename "$PROJECT_ROOT")"
|
||||
echo "Time: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo
|
||||
|
||||
# Function to count files safely
|
||||
count_files() {
|
||||
local dir="$1"
|
||||
if [ -d "$dir" ]; then
|
||||
ls "$dir" 2>/dev/null | wc -l
|
||||
else
|
||||
echo "0"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check Epic-specific queues
|
||||
EPIC_BASE="$PROJECT_ROOT/docs/task-management/epics/active"
|
||||
if [ -d "$EPIC_BASE" ]; then
|
||||
for EPIC_DIR in "$EPIC_BASE"/*/; do
|
||||
if [ -d "$EPIC_DIR" ]; then
|
||||
EPIC_NAME=$(basename "$EPIC_DIR")
|
||||
QA_DIR="$EPIC_DIR/reports/qa-automation"
|
||||
|
||||
if [ -d "$QA_DIR" ]; then
|
||||
echo "Epic: $EPIC_NAME"
|
||||
echo " Pending: $(count_files "$QA_DIR/pending")"
|
||||
echo " In Progress: $(count_files "$QA_DIR/in-progress")"
|
||||
echo " Done: $(count_files "$QA_DIR/done")"
|
||||
echo " Escalated: $(count_files "$QA_DIR/escalated")"
|
||||
|
||||
# Show escalated files if any
|
||||
if [ -d "$QA_DIR/escalated" ] && [ "$(ls "$QA_DIR/escalated" 2>/dev/null | wc -l)" -gt 0 ]; then
|
||||
echo " ⚠️ Escalated Issues:"
|
||||
for file in "$QA_DIR/escalated"/*_remediation_needed.md; do
|
||||
if [ -f "$file" ]; then
|
||||
echo " - $(basename "$file")"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
echo
|
||||
fi
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "[WARN] No Epic structure found at: $EPIC_BASE"
|
||||
echo
|
||||
fi
|
||||
|
||||
# Check general queue
|
||||
GENERAL_DIR="$PROJECT_ROOT/docs/reports/qa-automation"
|
||||
if [ -d "$GENERAL_DIR" ]; then
|
||||
echo "General (Non-Epic):"
|
||||
echo " Pending: $(count_files "$GENERAL_DIR/pending")"
|
||||
echo " In Progress: $(count_files "$GENERAL_DIR/in-progress")"
|
||||
echo " Done: $(count_files "$GENERAL_DIR/done")"
|
||||
echo " Escalated: $(count_files "$GENERAL_DIR/escalated")"
|
||||
|
||||
# Show escalated files
|
||||
if [ -d "$GENERAL_DIR/escalated" ] && [ "$(ls "$GENERAL_DIR/escalated" 2>/dev/null | wc -l)" -gt 0 ]; then
|
||||
echo " ⚠️ Escalated Issues:"
|
||||
for file in "$GENERAL_DIR/escalated"/*_remediation_needed.md; do
|
||||
if [ -f "$file" ]; then
|
||||
echo " - $(basename "$file")"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
else
|
||||
echo "[INFO] No general QA directory found (will be created on first use)"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "=== Recent Activity ==="
|
||||
# Show last 5 log entries
|
||||
if [ -f "$PROJECT_ROOT/logs/qa-automation.log" ]; then
|
||||
tail -5 "$PROJECT_ROOT/logs/qa-automation.log"
|
||||
else
|
||||
echo "No activity log found"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "=== Queue Processing Tips ==="
|
||||
echo "• View pending reports: ls -la $PROJECT_ROOT/docs/reports/qa-automation/pending/"
|
||||
echo "• Check stale reports: find $PROJECT_ROOT -path '*/in-progress/*' -mmin +60"
|
||||
echo "• Manual escalation: mv {report} {path}/escalated/"
|
||||
echo "• View full log: tail -f $PROJECT_ROOT/logs/qa-automation.log"
|
||||
66
rails/qa/remediation-hook-handler.sh
Executable file
66
rails/qa/remediation-hook-handler.sh
Executable file
@@ -0,0 +1,66 @@
|
||||
#!/bin/bash
|
||||
# Universal remediation hook handler with error recovery
|
||||
# Location: ~/.mosaic/rails/remediation-hook-handler.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
||||
REPORT_FILE="${1:-}"
|
||||
|
||||
# Validate input
|
||||
if [ -z "$REPORT_FILE" ] || [ ! -f "$REPORT_FILE" ]; then
|
||||
echo "[ERROR] Invalid or missing report file: $REPORT_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LOG_FILE="$PROJECT_ROOT/logs/qa-automation.log"
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Remediation triggered for: $REPORT_FILE" | tee -a "$LOG_FILE"
|
||||
|
||||
# Extract components from path and filename
|
||||
BASE_NAME=$(basename "$REPORT_FILE" _remediation_needed.md)
|
||||
DIR_PATH=$(dirname "$REPORT_FILE")
|
||||
|
||||
# Validate directory structure
|
||||
if [[ ! "$DIR_PATH" =~ /pending$ ]]; then
|
||||
echo "[ERROR] Report not in pending directory: $DIR_PATH" | tee -a "$LOG_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Setup in-progress directory
|
||||
IN_PROGRESS_DIR="${DIR_PATH/pending/in-progress}"
|
||||
|
||||
# Handle missing in-progress directory
|
||||
if [ ! -d "$IN_PROGRESS_DIR" ]; then
|
||||
echo "[WARN] Creating missing in-progress directory: $IN_PROGRESS_DIR" | tee -a "$LOG_FILE"
|
||||
mkdir -p "$IN_PROGRESS_DIR"
|
||||
|
||||
# Also ensure done and escalated exist
|
||||
mkdir -p "${DIR_PATH/pending/done}"
|
||||
mkdir -p "${DIR_PATH/pending/escalated}"
|
||||
fi
|
||||
|
||||
# Move from pending to in-progress (with error handling)
|
||||
if ! mv "$REPORT_FILE" "$IN_PROGRESS_DIR/" 2>/dev/null; then
|
||||
echo "[ERROR] Failed to move report to in-progress" | tee -a "$LOG_FILE"
|
||||
# Check if already in progress
|
||||
if [ -f "$IN_PROGRESS_DIR/$(basename "$REPORT_FILE")" ]; then
|
||||
echo "[WARN] Report already in progress, skipping" | tee -a "$LOG_FILE"
|
||||
exit 0
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create actions file
|
||||
ACTIONS_FILE="${BASE_NAME}_remediation_actions.md"
|
||||
ACTIONS_PATH="$IN_PROGRESS_DIR/$ACTIONS_FILE"
|
||||
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting remediation: $ACTIONS_PATH" | tee -a "$LOG_FILE"
|
||||
|
||||
# Trigger remediation agent
|
||||
claude -p "Use Task tool to launch auto-remediation-agent for:
|
||||
- Remediation Report: $IN_PROGRESS_DIR/$(basename "$REPORT_FILE")
|
||||
- Actions File: $ACTIONS_PATH
|
||||
- Max Iterations: 5
|
||||
Process the report, create action plan using Sequential Thinking, research with Context7, and execute fixes systematically." 2>&1 | tee -a "$LOG_FILE"
|
||||
Reference in New Issue
Block a user