- Shell 100%
| .forgejo/workflows | ||
| scripts | ||
| README.md | ||
Claude CI — Autonomous Issue-to-PR via Forgejo Actions
A self-contained CI/CD workflow that turns GitHub/Forgejo issues into pull requests using Claude Code. Open an issue, answer questions, approve the plan, and Claude writes the code.
Architecture
Issue opened → Brainstorm (ask questions)
↓
Reply to issue → Plan (generate implementation plan)
↓
Reply "approved" → Implement (write code, tests, open PR)
↓
Review PR → Revise (address feedback, push fixes)
One workflow file, one state machine. Issue comments are the conversation bus. Labels track state transitions.
State Machine
| Label | Meaning | Trigger |
|---|---|---|
| (none) | Fresh issue | Issue opened |
awaiting-clarification |
Claude asked questions | After brainstorm |
awaiting-approval |
Plan posted, needs approval | After plan |
awaiting-review |
PR opened, needs review | After implement |
Transitions
- Issue opened → Claude posts 2-4 clarifying questions, sets
awaiting-clarification - Comment on issue (label =
awaiting-clarification) → Claude generates implementation plan, removesawaiting-clarification, addsawaiting-approval - Comment "approved" (label =
awaiting-approval) → Claude creates branch, implements, opens PR, addsawaiting-review - PR review submitted → Claude reads feedback, fixes code, pushes updates
Comments not matching current state are ignored (no duplicate actions).
Files
claude-ci-demo/
├── .forgejo/
│ └── workflows/
│ └── claude-task.yml # Main workflow (8 steps, 1 job)
├── scripts/
│ ├── post-comment.sh # Post comment via Forgejo API
│ ├── fetch-issue-context.sh # Fetch issue + comments as markdown
│ ├── fetch-pr-reviews.sh # Fetch PR reviews + inline comments
│ └── update-label.sh # Remove old label, add new label
└── README.md
Workflow Steps
| Step | Condition | Action |
|---|---|---|
| Determine action | Always | Read event context, set action output |
| Skip | action == 'skip' |
No-op for irrelevant events |
| Checkout | action != 'skip' |
Clone repo with full history |
| Configure git | action != 'skip' |
Set user, .netrc auth, python3 symlink |
| Brainstorm | action == 'brainstorm' |
Claude asks questions |
| Plan | action == 'plan' |
Claude generates plan |
| Implement | action == 'implement' |
Branch, code, tests, PR |
| Revise | action == 'revise' |
Read reviews, fix, push |
Setup
Prerequisites
- Forgejo 10+ (Actions API required — 8.x/9.x lack full runner support)
forgejo-runnerregistered and active- Claude CLI installed on runner (
claude -pmust work) - Python 3 on runner (for JSON parsing)
1. Create the repo
Create a repo on your Forgejo instance. Push the workflow and scripts to the default branch.
2. Configure secrets
In repo Settings → Secrets, add:
| Secret | Value | Purpose |
|---|---|---|
FORGEJO_TOKEN |
Forgejo API token (write scope) | Issue comments, labels, PRs |
GIT_TOKEN |
Same token (or separate with push scope) | Git push via .netrc |
Note
: Forgejo secrets use PUT with plain text
datafield, NOT base64 like GitHub. Example:curl -X PUT -H "Authorization: token $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{"data":"your-token-here"}' \ "http://YOUR_FORGEJO/api/v1/repos/OWNER/REPO/secrets/FORGEJO_TOKEN"
3. Create labels
Create these labels in the repo (Settings → Labels):
awaiting-clarificationawaiting-approvalawaiting-review
4. Register a runner
# On the runner machine
forgejo-runner register \
--name my-runner \
--instance http://YOUR_FORGEJO:3200 \
--token <REGISTRATION_SECRET> \
--labels ubuntu-latest:docker
# Start the runner
forgejo-runner daemon
Generate the registration secret:
docker exec forgejo forgejo-cli actions generate-secret
5. Install Claude CLI on runner
# npm install -g @anthropic-ai/claude-code
# Then authenticate
claude auth login
Claude must work in non-interactive mode: claude -p "test" should return a response.
How It Works
Git Authentication
The workflow uses .netrc for git push authentication. This is the most reliable method for CI/CD:
# Generated at runtime in the workflow
printf "machine %s\nlogin %s\npassword %s\n" "$SERVER" "$ACTOR" "$GIT_TOKEN" > ~/.netrc
chmod 600 ~/.netrc
git config --global credential.helper store
Why not actions/checkout@v4 with token? The Forgejo auto-provided secrets.FORGEJO_TOKEN is Actions-scoped — it works for API calls but NOT for git HTTP push. The separate GIT_TOKEN secret solves this.
Comment Body Construction
All comment bodies use BODY="..." shell variables passed to post-comment.sh. The script uses python3 -c "import json..." to safely build the JSON payload, avoiding shell escaping issues.
Critical: BODY strings MUST be single-line. If a BODY string spans multiple YAML lines, the continuation at 0-indent will terminate the YAML block scalar (run: |), causing a parse error.
# CORRECT: single line
run: |
BODY="## Title\n\nContent here"
bash scripts/post-comment.sh "$NUM" "$REPO" "$BODY"
# WRONG: split across lines (continuation at 0-indent breaks block scalar)
run: |
BODY="## Title
Content here" # ← 0-indent continuation terminates the block scalar!
Avoiding Heredocs in YAML
Heredocs (<< EOF) in YAML run: | blocks are fragile. The heredoc terminator must have 0-indent after YAML processing, but YAML block scalars strip indentation relative to the first content line. If the terminator is indented differently from expectations, you get unexpected end of file errors.
Solution: Use BODY="..." variables instead. Keep all strings on one line, using \n for newlines.
Debugging
Check workflow runs
curl -s -H "Authorization: token $TOKEN" \
"http://FORGEJO/api/v1/repos/OWNER/REPO/actions/runs" | python3 -m json.tool
Check runner logs
journalctl --user-unit forgejo-runner -f
Common issues
| Symptom | Cause | Fix |
|---|---|---|
YAML parse error mapping values are not allowed |
Split BODY string breaks block scalar | Merge to single line |
unexpected end of file (bash) |
Misaligned heredoc terminator | Replace heredoc with BODY variable |
| Git push auth failure | Actions token can't push | Use .netrc with GIT_TOKEN secret |
python3: command not found |
Runner has python not python3 |
Workflow creates symlink |
| Runner polls but no tasks | Forgejo version too old / Actions disabled | Upgrade Forgejo, check app.ini |
| No workflow triggered | Missing secrets.FORGEJO_TOKEN |
Add secret in repo settings |
Validate YAML locally
import yaml
content = open('.forgejo/workflows/claude-task.yml').read()
yaml.safe_load(content) # Raises on syntax errors
print("YAML valid")
Environment Variables
| Variable | Source | Used by |
|---|---|---|
FORGEJO_TOKEN |
secrets.FORGEJO_TOKEN |
All scripts, API calls |
GIT_TOKEN |
secrets.GIT_TOKEN |
.netrc for git push |
GITHUB_TOKEN |
Aliased to FORGEJO_TOKEN |
Checkout action compatibility |
GITHUB_API_URL |
Auto-set by Forgejo | All scripts |
GITHUB_SERVER_URL |
Auto-set by Forgejo | Git remote, .netrc |
GITHUB_ACTOR |
Auto-set by Forgejo | Git config, .netrc |
Limitations
- No merge automation: The workflow opens PRs but doesn't merge. Human review is required.
- Single issue per branch: Each implementation creates a new branch. Multiple approvals create multiple PRs.
- 30-minute timeout: Long-running Claude tasks may time out. Adjust
timeout-minutesif needed. - Claude cost: Each workflow run invokes Claude 1-2 times. Monitor your API usage.
- Runner capacity: Default runner capacity is 1. Parallel issues will queue.
Extending
Add new states
- Add a new label (e.g.
awaiting-testing) - Add a condition in the "Determine action" step
- Add a new workflow step with
if: steps.context.outputs.action == 'your-action' - Update
update-label.shcall to transition the label
Add test automation
Add a step after implement that runs tests and posts results:
- name: Test
if: steps.context.outputs.action == 'implement'
run: |
pytest --tb=short 2>&1 | tee /tmp/test-results.txt
BODY="## Test Results\n\n$(cat /tmp/test-results.txt)"
bash scripts/post-comment.sh "$ISSUE_NUM" "$REPO" "$BODY"
Multi-repo support
The workflow already uses ${{ github.repository }} throughout. To cross-reference repos, pass the target repo as a parameter or read it from the issue body.