Claude Code CI demo � Claude as remote developer via Forgejo Actions
Find a file
2026-06-05 01:47:48 +00:00
.forgejo/workflows fix: single-line BODY strings in YAML block scalar 2026-06-05 01:07:58 +00:00
scripts Add Claude Code CI workflow + helper scripts 2026-06-05 06:56:40 +10:00
README.md docs: add comprehensive README 2026-06-05 01:47:48 +00:00

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

  1. Issue opened → Claude posts 2-4 clarifying questions, sets awaiting-clarification
  2. Comment on issue (label = awaiting-clarification) → Claude generates implementation plan, removes awaiting-clarification, adds awaiting-approval
  3. Comment "approved" (label = awaiting-approval) → Claude creates branch, implements, opens PR, adds awaiting-review
  4. 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-runner registered and active
  • Claude CLI installed on runner (claude -p must 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 data field, 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-clarification
  • awaiting-approval
  • awaiting-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-minutes if 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

  1. Add a new label (e.g. awaiting-testing)
  2. Add a condition in the "Determine action" step
  3. Add a new workflow step with if: steps.context.outputs.action == 'your-action'
  4. Update update-label.sh call 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.