chore: add Forgejo PR workflow
This commit is contained in:
parent
e61c4ec077
commit
e2603ca479
9 changed files with 498 additions and 0 deletions
54
.claude/hooks/log-request.sh
Executable file
54
.claude/hooks/log-request.sh
Executable file
|
|
@ -0,0 +1,54 @@
|
|||
#!/usr/bin/env bash
|
||||
# Claude Code — UserPromptSubmit hook
|
||||
#
|
||||
# Logs each new user prompt to .claude/request-log.jsonl (git-tracked).
|
||||
# Runs async so it never blocks Claude's response.
|
||||
#
|
||||
# Input (stdin): JSON with fields: session_id, cwd, prompt, is_continuation
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
|
||||
PROMPT=$(echo "$INPUT" | jq -r '.prompt // ""')
|
||||
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""')
|
||||
CWD=$(echo "$INPUT" | jq -r '.cwd // "$PWD"')
|
||||
IS_CONTINUATION=$(echo "$INPUT" | jq -r '.is_continuation // false')
|
||||
|
||||
LOG_FILE="${CWD}/.claude/request-log.jsonl"
|
||||
|
||||
# Skip: continuation turns, trivially short prompts, no log file dir
|
||||
if [ "$IS_CONTINUATION" = "true" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "${#PROMPT}" -lt 5 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -d "${CWD}/.claude" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
REQ_ID="req_$(date +%Y%m%d_%H%M%S)_$(LC_ALL=C tr -dc 'a-z0-9' < /dev/urandom | head -c 5)"
|
||||
BRANCH=$(git -C "$CWD" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
|
||||
|
||||
jq -n \
|
||||
--arg id "$REQ_ID" \
|
||||
--arg ts "$TIMESTAMP" \
|
||||
--arg prompt "$PROMPT" \
|
||||
--arg session "$SESSION_ID" \
|
||||
--arg branch "$BRANCH" \
|
||||
'{
|
||||
id: $id,
|
||||
timestamp: $ts,
|
||||
prompt: $prompt,
|
||||
session: $session,
|
||||
branch: $branch,
|
||||
pr_number: null,
|
||||
pr_url: null,
|
||||
status: "pending"
|
||||
}' >> "$LOG_FILE"
|
||||
|
||||
exit 0
|
||||
39
.claude/hooks/session-start.sh
Executable file
39
.claude/hooks/session-start.sh
Executable file
|
|
@ -0,0 +1,39 @@
|
|||
#!/usr/bin/env bash
|
||||
# Claude Code — SessionStart hook
|
||||
#
|
||||
# Injects recent request history into Claude's context at session start.
|
||||
# stdout is appended to Claude's context window.
|
||||
#
|
||||
# Input (stdin): JSON with fields: session_id, cwd, session_start_source
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
CWD=$(echo "$INPUT" | jq -r '.cwd // "$PWD"')
|
||||
|
||||
LOG_FILE="${CWD}/.claude/request-log.jsonl"
|
||||
|
||||
if [ ! -f "$LOG_FILE" ] || [ ! -s "$LOG_FILE" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
TOTAL=$(wc -l < "$LOG_FILE" | tr -d ' ')
|
||||
OPEN=$(jq -r 'select(.status == "open")' "$LOG_FILE" 2>/dev/null | jq -s 'length')
|
||||
|
||||
echo "=== Forgejo Workflow: Session Context ==="
|
||||
echo "Request log: .claude/request-log.jsonl ($TOTAL total, $OPEN open PRs)"
|
||||
echo ""
|
||||
|
||||
# Last 5 entries
|
||||
echo "Recent requests:"
|
||||
tail -5 "$LOG_FILE" | jq -r '
|
||||
" [\(.timestamp[0:16])] \(.prompt | gsub("\n"; " ") | .[0:72])" +
|
||||
"\n branch: \(.branch)" +
|
||||
(if .pr_number then " | PR #\(.pr_number) (\(.status))" else " | no PR yet" end)
|
||||
' 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
echo "Run '.claude/scripts/show-history.sh' for full history."
|
||||
echo "========================================="
|
||||
|
||||
exit 0
|
||||
0
.claude/request-log.jsonl
Normal file
0
.claude/request-log.jsonl
Normal file
100
.claude/scripts/create-pr.sh
Executable file
100
.claude/scripts/create-pr.sh
Executable file
|
|
@ -0,0 +1,100 @@
|
|||
#!/usr/bin/env bash
|
||||
# Create a PR on Forgejo via tea and record it in the request log.
|
||||
#
|
||||
# Usage:
|
||||
# .claude/scripts/create-pr.sh <title> [body] [base]
|
||||
#
|
||||
# Environment:
|
||||
# FORGEJO_BASE_BRANCH — override default base branch (default: main)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TITLE="${1:-}"
|
||||
BODY="${2:-}"
|
||||
BASE="${3:-${FORGEJO_BASE_BRANCH:-main}}"
|
||||
LOG_FILE=".claude/request-log.jsonl"
|
||||
|
||||
if [ -z "$TITLE" ]; then
|
||||
echo "Usage: create-pr.sh <title> [body] [base-branch]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v tea &>/dev/null; then
|
||||
cat >&2 <<'MSG'
|
||||
Error: 'tea' CLI not found.
|
||||
|
||||
Install options:
|
||||
nix develop path:~/.claude/workflows/forgejo # recommended
|
||||
brew install gitea/tap/tea # macOS
|
||||
go install gitea.com/gitea/tea@latest # Go
|
||||
|
||||
Then configure: tea login add
|
||||
MSG
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
if [ "$CURRENT_BRANCH" = "$BASE" ]; then
|
||||
echo "Error: currently on base branch '$BASE'. Create a feature branch first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Creating PR: '$TITLE'"
|
||||
echo " head: $CURRENT_BRANCH → base: $BASE"
|
||||
echo ""
|
||||
|
||||
# Build tea args
|
||||
TEA_ARGS=(pr create
|
||||
--title "$TITLE"
|
||||
--head "$CURRENT_BRANCH"
|
||||
--base "$BASE"
|
||||
)
|
||||
|
||||
if [ -n "$BODY" ]; then
|
||||
TEA_ARGS+=(--description "$BODY")
|
||||
fi
|
||||
|
||||
PR_OUTPUT=$(tea "${TEA_ARGS[@]}" 2>&1)
|
||||
echo "$PR_OUTPUT"
|
||||
|
||||
# Extract URL — tea prints something like: "Created pull request #42"
|
||||
# and/or a URL line
|
||||
PR_URL=$(echo "$PR_OUTPUT" | grep -oE 'https?://[^[:space:]]+/pulls/[0-9]+' | head -1 || true)
|
||||
PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$' || true)
|
||||
|
||||
if [ -z "$PR_NUMBER" ]; then
|
||||
# Fallback: parse "Created pull request #N"
|
||||
PR_NUMBER=$(echo "$PR_OUTPUT" | grep -oE '#[0-9]+' | grep -oE '[0-9]+' | head -1 || true)
|
||||
fi
|
||||
|
||||
# Update most recent pending entry in the log
|
||||
if [ -n "$PR_NUMBER" ] && [ -f "$LOG_FILE" ] && [ -s "$LOG_FILE" ]; then
|
||||
TMPFILE=$(mktemp)
|
||||
|
||||
# Find and update the last entry that matches current branch & is pending
|
||||
awk -v branch="$CURRENT_BRANCH" -v pr_num="$PR_NUMBER" -v pr_url="$PR_URL" '
|
||||
BEGIN { updated = 0 }
|
||||
{
|
||||
lines[NR] = $0
|
||||
}
|
||||
END {
|
||||
# Walk backwards to find last matching entry
|
||||
for (i = NR; i >= 1; i--) {
|
||||
if (!updated && lines[i] ~ ("\"branch\":\"" branch "\"") && lines[i] ~ "\"status\":\"pending\"") {
|
||||
# Use a simple substitution — jq would be cleaner but we avoid a subshell per line
|
||||
cmd = "echo " lines[i] " | jq --arg pn \"" pr_num "\" --arg pu \"" pr_url "\" \".pr_number = ($pn | tonumber) | .pr_url = $pu | .status = \\\"open\\\"\""
|
||||
cmd | getline updated_line
|
||||
close(cmd)
|
||||
lines[i] = updated_line
|
||||
updated = 1
|
||||
}
|
||||
}
|
||||
for (i = 1; i <= NR; i++) print lines[i]
|
||||
}
|
||||
' "$LOG_FILE" > "$TMPFILE"
|
||||
|
||||
mv "$TMPFILE" "$LOG_FILE"
|
||||
echo ""
|
||||
echo "Request log updated with PR #$PR_NUMBER"
|
||||
fi
|
||||
84
.claude/scripts/show-history.sh
Executable file
84
.claude/scripts/show-history.sh
Executable file
|
|
@ -0,0 +1,84 @@
|
|||
#!/usr/bin/env bash
|
||||
# Display the Claude request history for this project.
|
||||
#
|
||||
# Usage:
|
||||
# .claude/scripts/show-history.sh [--filter open|merged|pending] [--last N] [--json]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
LOG_FILE=".claude/request-log.jsonl"
|
||||
FILTER=""
|
||||
LAST=0
|
||||
JSON_MODE=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--filter) FILTER="$2"; shift 2 ;;
|
||||
--last) LAST="$2"; shift 2 ;;
|
||||
--json) JSON_MODE=true; shift ;;
|
||||
-*) echo "Unknown flag: $1" >&2; exit 1 ;;
|
||||
*) LOG_FILE="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ ! -f "$LOG_FILE" ]; then
|
||||
echo "No request log found at $LOG_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! command -v jq &>/dev/null; then
|
||||
echo "Error: jq not found. Run: nix develop path:~/.claude/workflows/forgejo" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TOTAL=$(wc -l < "$LOG_FILE" | tr -d ' ')
|
||||
|
||||
if [ "$TOTAL" -eq 0 ]; then
|
||||
echo "No requests logged yet."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Build jq filter
|
||||
JQ_SELECT='.'
|
||||
if [ -n "$FILTER" ]; then
|
||||
JQ_SELECT="select(.status == \"$FILTER\")"
|
||||
fi
|
||||
|
||||
CONTENT=$(jq -r "$JQ_SELECT" "$LOG_FILE" 2>/dev/null | jq -s '.')
|
||||
|
||||
if [ "$LAST" -gt 0 ]; then
|
||||
CONTENT=$(echo "$CONTENT" | jq ".[-${LAST}:]")
|
||||
fi
|
||||
|
||||
if [ "$JSON_MODE" = true ]; then
|
||||
echo "$CONTENT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
COUNT=$(echo "$CONTENT" | jq 'length')
|
||||
|
||||
echo "━━━ Request History ($LOG_FILE) ━━━"
|
||||
echo "Total: $TOTAL entries | Showing: $COUNT${FILTER:+ ($FILTER)}"
|
||||
echo ""
|
||||
|
||||
echo "$CONTENT" | jq -r '.[] |
|
||||
"[\(.timestamp[0:16])] \(.id)",
|
||||
" Prompt: \(.prompt | gsub("\n";" ") | .[0:80])\(if (.prompt | length) > 80 then "…" else "" end)",
|
||||
" Branch: \(.branch)",
|
||||
(if .pr_number then
|
||||
" PR: #\(.pr_number) [\(.status)] \(.pr_url // "")"
|
||||
else
|
||||
" PR: none yet (status: \(.status))"
|
||||
end),
|
||||
""
|
||||
'
|
||||
|
||||
# Summary stats
|
||||
OPEN=$(jq -r 'select(.status == "open")' "$LOG_FILE" | jq -s 'length')
|
||||
MERGED=$(jq -r 'select(.status == "merged")' "$LOG_FILE" | jq -s 'length')
|
||||
PENDING=$(jq -r 'select(.status == "pending")' "$LOG_FILE" | jq -s 'length')
|
||||
|
||||
echo "━━━ Summary ━━━"
|
||||
echo " Open PRs: $OPEN"
|
||||
echo " Merged: $MERGED"
|
||||
echo " No PR yet: $PENDING"
|
||||
66
.claude/scripts/update-entry.sh
Executable file
66
.claude/scripts/update-entry.sh
Executable file
|
|
@ -0,0 +1,66 @@
|
|||
#!/usr/bin/env bash
|
||||
# Update a specific log entry (e.g. mark a PR as merged).
|
||||
#
|
||||
# Usage:
|
||||
# .claude/scripts/update-entry.sh <req-id> --status merged
|
||||
# .claude/scripts/update-entry.sh <req-id> --pr-number 42 --pr-url https://...
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REQ_ID="${1:-}"
|
||||
LOG_FILE=".claude/request-log.jsonl"
|
||||
NEW_STATUS=""
|
||||
NEW_PR_NUMBER=""
|
||||
NEW_PR_URL=""
|
||||
|
||||
shift || true
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--status) NEW_STATUS="$2"; shift 2 ;;
|
||||
--pr-number) NEW_PR_NUMBER="$2"; shift 2 ;;
|
||||
--pr-url) NEW_PR_URL="$2"; shift 2 ;;
|
||||
*) echo "Unknown arg: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$REQ_ID" ]; then
|
||||
echo "Usage: update-entry.sh <req-id> [--status STATUS] [--pr-number N] [--pr-url URL]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$LOG_FILE" ]; then
|
||||
echo "Log file not found: $LOG_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TMPFILE=$(mktemp)
|
||||
|
||||
JQ_ARGS=()
|
||||
JQ_FILTER='.'
|
||||
|
||||
if [ -n "$NEW_STATUS" ]; then
|
||||
JQ_ARGS+=(--arg status "$NEW_STATUS")
|
||||
JQ_FILTER="$JQ_FILTER | .status = \$status"
|
||||
fi
|
||||
|
||||
if [ -n "$NEW_PR_NUMBER" ]; then
|
||||
JQ_ARGS+=(--argjson pr_num "$NEW_PR_NUMBER")
|
||||
JQ_FILTER="$JQ_FILTER | .pr_number = \$pr_num"
|
||||
fi
|
||||
|
||||
if [ -n "$NEW_PR_URL" ]; then
|
||||
JQ_ARGS+=(--arg pr_url "$NEW_PR_URL")
|
||||
JQ_FILTER="$JQ_FILTER | .pr_url = \$pr_url"
|
||||
fi
|
||||
|
||||
while IFS= read -r line; do
|
||||
ID=$(echo "$line" | jq -r '.id // ""')
|
||||
if [ "$ID" = "$REQ_ID" ]; then
|
||||
echo "$line" | jq "${JQ_ARGS[@]}" "$JQ_FILTER"
|
||||
else
|
||||
echo "$line"
|
||||
fi
|
||||
done < "$LOG_FILE" > "$TMPFILE"
|
||||
|
||||
mv "$TMPFILE" "$LOG_FILE"
|
||||
echo "Updated entry $REQ_ID"
|
||||
29
.claude/settings.json
Normal file
29
.claude/settings.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"hooks": {
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"matcher": "",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/log-request.sh",
|
||||
"timeout": 10,
|
||||
"async": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-start.sh",
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -9,3 +9,9 @@ ios/
|
|||
*.local
|
||||
.DS_Store
|
||||
.claude/worktrees/
|
||||
e2e/test-results/
|
||||
e2e/report/
|
||||
e2e/.features-gen/
|
||||
|
||||
# Claude Code local settings (secrets, personal config)
|
||||
.claude/settings.local.json
|
||||
|
|
|
|||
120
CLAUDE.md
120
CLAUDE.md
|
|
@ -798,3 +798,123 @@ function getItemsInContainer(containerId: string): Item[] {
|
|||
return allItems.filter(i => i.storageContainerId === containerId);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Forgejo PR Workflow
|
||||
|
||||
Every substantive session is tracked in `.claude/request-log.jsonl` and backed by a Forgejo PR.
|
||||
|
||||
### When to create a branch + PR
|
||||
|
||||
Create a branch and PR for:
|
||||
- New features (`feat/`)
|
||||
- Bug fixes (`fix/`)
|
||||
- Refactors (`refactor/`)
|
||||
- Dependency or config changes (`chore/`)
|
||||
|
||||
Skip branching/PR for:
|
||||
- Questions, explanations, code reading
|
||||
- Trivial one-liner fixes that are best committed directly to the current branch
|
||||
|
||||
### Branch naming
|
||||
|
||||
```
|
||||
feat/YYYYMMDD-short-kebab-description
|
||||
fix/YYYYMMDD-short-kebab-description
|
||||
chore/YYYYMMDD-short-kebab-description
|
||||
refactor/YYYYMMDD-short-kebab-description
|
||||
```
|
||||
|
||||
Check the current branch first:
|
||||
```bash
|
||||
git rev-parse --abbrev-ref HEAD
|
||||
```
|
||||
|
||||
If already on a feature branch from this session, continue on it. Only create a new branch for a new distinct task.
|
||||
|
||||
### Workflow for each task
|
||||
|
||||
1. **Create branch** (if needed):
|
||||
```bash
|
||||
git checkout -b feat/$(date +%Y%m%d)-short-description
|
||||
```
|
||||
|
||||
2. **Do the work** — write code, run tests, iterate.
|
||||
|
||||
3. **Commit** with conventional commit messages:
|
||||
```bash
|
||||
git add <files>
|
||||
git commit -m "feat: short description"
|
||||
```
|
||||
|
||||
4. **Push**:
|
||||
```bash
|
||||
git push -u origin HEAD
|
||||
```
|
||||
|
||||
5. **Create PR**:
|
||||
```bash
|
||||
bash .claude/scripts/create-pr.sh \
|
||||
"feat: short description" \
|
||||
"$(cat <<'EOF'
|
||||
## Summary
|
||||
|
||||
- What was done and why
|
||||
|
||||
## Test plan
|
||||
|
||||
- [ ] Manual test steps
|
||||
|
||||
🤖 Generated with Claude Code
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
This also updates `.claude/request-log.jsonl` with the PR number.
|
||||
|
||||
6. **Commit the updated request log** (as part of the PR or separately):
|
||||
```bash
|
||||
git add .claude/request-log.jsonl
|
||||
git commit -m "chore: log request #<req-id>"
|
||||
```
|
||||
|
||||
### Show history
|
||||
|
||||
When the user asks for history, recent work, or what's been done:
|
||||
|
||||
```bash
|
||||
bash .claude/scripts/show-history.sh
|
||||
bash .claude/scripts/show-history.sh --filter open
|
||||
bash .claude/scripts/show-history.sh --last 10
|
||||
```
|
||||
|
||||
### tea CLI quick reference
|
||||
|
||||
```bash
|
||||
tea pr list # list open PRs
|
||||
tea pr view <number> # view a PR
|
||||
tea pr merge <number> # merge a PR
|
||||
tea issue create # create an issue
|
||||
tea repo info # show repo details
|
||||
tea login list # show configured Forgejo instances
|
||||
```
|
||||
|
||||
### Request log format
|
||||
|
||||
`.claude/request-log.jsonl` — one JSON object per line, git-tracked:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "req_20260226_143012_a3f9b",
|
||||
"timestamp": "2026-02-26T14:30:12Z",
|
||||
"prompt": "Add dark mode support",
|
||||
"session": "abc123",
|
||||
"branch": "feat/20260226-dark-mode",
|
||||
"pr_number": 42,
|
||||
"pr_url": "https://forge.example.com/user/repo/pulls/42",
|
||||
"status": "open"
|
||||
}
|
||||
```
|
||||
|
||||
Possible statuses: `pending` (no PR yet), `open`, `merged`, `closed`.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue