Author SHA1 Message Date
OpenCode Bot 7de27f79fe OpenCode (big-pickle): opencode-issue-9 2026-07-06 06:16:28 +00:00
3 changed files with 127 additions and 174 deletions
+23 -174
View File
@@ -20,10 +20,9 @@ jobs:
(github.event_name == 'issues' &&
(contains(github.event.issue.body, '/oc') || contains(github.event.issue.body, '/opencode'))) ||
(github.event_name != 'issues' &&
github.event.comment.user.login != 'opencode-bot' &&
(contains(github.event.comment.body, '/oc') || contains(github.event.comment.body, '/opencode')))
runs-on: ubuntu-latest
timeout-minutes: 40
timeout-minutes: 20
env:
GITEA_API_URL: "https://gitea.maribit.it"
@@ -40,11 +39,6 @@ jobs:
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_BODY: ${{ github.event.issue.body }}
RUN_NUMBER: ${{ github.run_number }}
# --- OpenCode ---
OPENCODE_ZEN_API_KEY: ${{ secrets.OPENCODE_ZEN_API_KEY }}
OPENCODE_API_BASE: https://opencode.ai
OPENCODE_MODEL: opencode/big-pickle
MAX_ITERATIONS: "4"
steps:
- name: Verifica autorizzazione utente
@@ -58,8 +52,8 @@ jobs:
exit 1
fi
# Feedback immediato: 👀 sul commento (o sulla issue). Le reazioni NON generano
# eventi issue_comment -> nessun doppio run.
# Feedback immediato: 👀 sul commento (o sulla issue) che ha invocato l'agente.
# Le reazioni NON generano eventi issue_comment -> nessun doppio run.
- name: Reagisci al comando (👀)
id: react
run: |
@@ -89,31 +83,6 @@ jobs:
curl -fsSL https://opencode.ai/install | bash
echo "$HOME/.opencode/bin" >> "$GITHUB_PATH"
# Agent "reviewer" read-only: legge ed esegue i test (bash), MAI edita.
# Creato solo nel runner ed escluso dai commit, così non sporca il repo.
- name: Configura agent reviewer (read-only)
run: |
mkdir -p .opencode/agents
cat > .opencode/agents/reviewer.md <<'EOF'
---
description: Revisore read-only che verifica gli acceptance criteria eseguendo i test, senza modificare il codice.
mode: primary
permission:
edit: deny
webfetch: deny
bash: allow
tools:
write: false
edit: false
bash: true
---
Sei un revisore di codice rigoroso in SOLA LETTURA.
Non modificare mai i file: puoi solo leggere il codice ed eseguire comandi
di verifica (build, lint, test). Stabilisci se ogni Acceptance Criterion
del piano è oggettivamente soddisfatto sul codice attuale.
EOF
grep -qxF '.opencode/' .git/info/exclude 2>/dev/null || echo '.opencode/' >> .git/info/exclude
- name: Determina contesto (issue / PR / manuale) e branch di lavoro
id: ctx
run: |
@@ -132,6 +101,7 @@ jobs:
PR_NUMBER="$PR_NUMBER_FROM_EVENT"
BRANCH_NAME="$PR_HEAD_REF_FROM_EVENT"
else
# issue / issue_comment: la chiave "pull_request" c'è sempre ma vale null per le issue.
ISSUE_JSON=$(curl -s -H "Authorization: token $GITEA_TOKEN" "$API/issues/$ISSUE_NUMBER")
if [ "$(echo "$ISSUE_JSON" | jq -r '.pull_request // empty')" != "" ]; then
IS_PR=true
@@ -150,13 +120,9 @@ jobs:
exit 1
fi
# Chiave univoca per il file di piano (branch con '/' -> '-').
PLAN_KEY=$(printf '%s' "$BRANCH_NAME" | tr '/' '-')
echo "is_pr=$IS_PR" >> "$GITHUB_OUTPUT"
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
echo "plan_key=$PLAN_KEY" >> "$GITHUB_OUTPUT"
if [ "$IS_PR" = "true" ]; then
git fetch origin "$BRANCH_NAME"
@@ -186,163 +152,46 @@ jobs:
CLEAN_PROMPT=$(printf '%s' "$COMMENT_BODY" | sed -e 's|/opencode||g' -e 's|/oc||g')
printf '%s' "$CLEAN_PROMPT" > /tmp/clean_prompt.txt
- name: "Fase 1 — Plan (analisi read-only)"
if: steps.ctx.outputs.is_pr != 'true'
- name: Esegui OpenCode
env:
OPENCODE_ZEN_API_KEY: ${{ secrets.OPENCODE_ZEN_API_KEY }}
OPENCODE_API_BASE: https://opencode.ai
OPENCODE_MODEL: opencode/big-pickle
run: |
CLEAN_PROMPT=$(cat /tmp/clean_prompt.txt)
CONTEXT=$(cat /tmp/context.txt 2>/dev/null || true)
mkdir -p issue_plans
PLAN_FILE="issue_plans/${{ steps.ctx.outputs.plan_key }}.md"
opencode run \
--model "$OPENCODE_MODEL" \
"Contesto della Issue/PR: $ISSUE_TITLE - $ISSUE_BODY
opencode run --agent plan --model "$OPENCODE_MODEL" \
"Sei in modalità PLAN (SOLA LETTURA): NON modificare alcun file.
Analizza il codebase e la richiesta, poi produci un piano di lavoro in Markdown
con questa struttura ESATTA:
# Piano
## Obiettivo
## Task
(elenco numerato di task atomici; per ciascuno i file coinvolti)
## Acceptance Criteria
(checklist '- [ ] ...' di criteri oggettivi e verificabili)
## Verifica
(come testare: comandi da lanciare, cosa controllare)
Richiesta ($COMMENT_AUTHOR): $CLEAN_PROMPT
Contesto: $ISSUE_TITLE - $ISSUE_BODY
$CONTEXT
Restituisci SOLO il documento Markdown del piano." | tee "$PLAN_FILE"
Richiesta specifica dell'utente ($COMMENT_AUTHOR): $CLEAN_PROMPT
cp "$PLAN_FILE" /tmp/plan.md
git add "$PLAN_FILE"
git commit -m "OpenCode: piano di lavoro (${{ steps.ctx.outputs.plan_key }})" || echo "Nessun piano da committare."
- name: "Fase 2 — Build & Review loop"
run: |
CLEAN_PROMPT=$(cat /tmp/clean_prompt.txt)
CONTEXT=$(cat /tmp/context.txt 2>/dev/null || true)
PLAN=""; [ -f /tmp/plan.md ] && PLAN="$(cat /tmp/plan.md)"
REVIEW_FEEDBACK="(prima iterazione: nessun feedback)"
for i in $(seq 1 "$MAX_ITERATIONS"); do
echo "::group::Iterazione $i — BUILD"
opencode run --agent build --auto --model "$OPENCODE_MODEL" \
"Sei in modalità BUILD. Implementa il piano completando TUTTI i task e
soddisfacendo TUTTI gli Acceptance Criteria. Applica le modifiche ai file.
=== PIANO ===
${PLAN:-Nessun piano formale. Richiesta: $CLEAN_PROMPT}
=== CONTESTO (diff/commenti PR, se presente) ===
$CONTEXT
=== FEEDBACK DELLA REVIEW PRECEDENTE (correggi SOLO questi punti) ===
$REVIEW_FEEDBACK"
echo "::endgroup::"
echo "::group::Iterazione $i — REVIEW"
opencode run --agent reviewer --auto --model "$OPENCODE_MODEL" \
"Verifica sul codice ATTUALE se OGNI Acceptance Criterion del piano è
soddisfatto. Puoi eseguire build/lint/test ma NON modificare i file.
=== PIANO ===
${PLAN:-Richiesta originale: $CLEAN_PROMPT}
Scrivi come ULTIMA riga ESATTAMENTE una di queste:
- 'VERDICT: PASS' se tutti i criteri sono soddisfatti
- 'VERDICT: FAIL' altrimenti, seguita da un elenco puntato dei criteri NON
soddisfatti e di cosa manca." | tee /tmp/review.txt
echo "::endgroup::"
if grep -q 'VERDICT: PASS' /tmp/review.txt; then
echo "✅ Acceptance criteria soddisfatti all'iterazione $i."
exit 0
fi
REVIEW_FEEDBACK="$(cat /tmp/review.txt)"
echo "⚠️ Criteri non ancora soddisfatti: procedo con l'iterazione $((i+1))."
done
echo "Raggiunto il tetto di $MAX_ITERATIONS iterazioni senza PASS completo:"
echo "le modifiche parziali verranno comunque pushate per revisione umana."
Se stai lavorando su una PR già aperta, applica solo le modifiche richieste dal feedback di review, senza riscrivere lavoro già approvato."
- name: Pusha modifiche e apri/aggiorna la PR
id: push
run: |
API="$GITEA_API_URL/api/v1/repos/$REPO_FULL_NAME"
BRANCH_NAME="${{ steps.ctx.outputs.branch_name }}"
git add .
git commit -m "OpenCode (big-pickle): $BRANCH_NAME" || echo "Niente di nuovo da committare oltre al loop."
if git diff --quiet origin/main..HEAD 2>/dev/null; then
echo "Nessuna modifica rispetto a main: niente da pushare."
echo "pr_number=" >> "$GITHUB_OUTPUT"
if [ -z "$(git status --porcelain)" ]; then
echo "Nessuna modifica rilevata da OpenCode."
exit 0
fi
git add .
git commit -m "OpenCode (big-pickle): $BRANCH_NAME"
git push origin "$BRANCH_NAME"
if [ "${{ steps.ctx.outputs.is_pr }}" = "true" ]; then
PR_NUMBER="${{ steps.ctx.outputs.pr_number }}"
else
RESP=$(curl -s -X POST "$API/pulls" \
if [ "${{ steps.ctx.outputs.is_pr }}" != "true" ]; then
curl -s -X POST "$API/pulls" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"base\":\"main\",\"head\":\"$BRANCH_NAME\",\"title\":\"PR Automatica OpenCode: $BRANCH_NAME\",\"body\":\"Generata da big-pickle su richiesta di @$COMMENT_AUTHOR. Piano: issue_plans/${{ steps.ctx.outputs.plan_key }}.md\"}")
PR_NUMBER=$(echo "$RESP" | jq -r '.number // empty')
# Se la PR per questo branch esiste già, recuperane il numero.
if [ -z "$PR_NUMBER" ]; then
PR_NUMBER=$(curl -s -H "Authorization: token $GITEA_TOKEN" "$API/pulls?state=open&limit=50" \
| jq -r --arg b "$BRANCH_NAME" '.[] | select(.head.ref==$b) | .number' | head -n1)
fi
fi
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
# Passo finale: la review dell'agente viene postata come REVIEW sulla PR
# (event COMMENT: evita i vincoli sul self-approve del proprio branch).
- name: Posta la review sulla PR
if: always()
continue-on-error: true
run: |
API="$GITEA_API_URL/api/v1/repos/$REPO_FULL_NAME"
PR_NUMBER="${{ steps.push.outputs.pr_number }}"
[ -z "$PR_NUMBER" ] && PR_NUMBER="${{ steps.ctx.outputs.pr_number }}"
if [ -z "$PR_NUMBER" ] || [ ! -s /tmp/review.txt ]; then
echo "Nessuna PR o nessuna review da postare."
exit 0
fi
# Il verdetto guida lo stato nativo della review.
if grep -q 'VERDICT: PASS' /tmp/review.txt; then
EVENT="APPROVE"
else
EVENT="REQUEST_CHANGES"
fi
BODY=$(jq -Rs . < /tmp/review.txt) # encoding JSON-safe del testo review
post_review() {
curl -s -o /tmp/review_resp.json -w '%{http_code}' \
-X POST "$API/pulls/$PR_NUMBER/reviews" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"body\":$BODY,\"event\":\"$1\"}"
}
CODE=$(post_review "$EVENT")
echo "Review '$EVENT' -> HTTP $CODE"
# Gitea rifiuta APPROVE/REQUEST_CHANGES sulla PR di cui il bot è autore:
# in quel caso ripiega su un commento di review (event=COMMENT).
if [ "$CODE" -ge 300 ]; then
echo "Fallback a event=COMMENT (probabile self-review su PR del bot). Risposta:"
cat /tmp/review_resp.json 2>/dev/null || true
echo
CODE=$(post_review "COMMENT")
echo "Review 'COMMENT' -> HTTP $CODE"
-d "{\"base\":\"main\",\"head\":\"$BRANCH_NAME\",\"title\":\"PR Automatica OpenCode: $BRANCH_NAME\",\"body\":\"Generata da big-pickle su richiesta di @$COMMENT_AUTHOR.\"}"
fi
# Stato finale via reazione (niente commenti -> niente self-trigger).
- name: Reazione finale 🚀
if: success() && steps.react.outputs.path != ''
continue-on-error: true
@@ -353,7 +202,7 @@ jobs:
-H "Content-Type: application/json" \
-d '{"content":"rocket"}' >/dev/null || true
- name: Reazione in caso di errore 😕
- name: Reazione finale 😕 (fallimento)
if: failure() && steps.react.outputs.path != ''
continue-on-error: true
run: |
+97
View File
@@ -0,0 +1,97 @@
name: Publish Docker Image
on:
workflow_dispatch:
inputs:
proxy_mode:
description: 'Proxy mode (nginx o traefik)'
required: true
default: 'nginx'
type: choice
options:
- nginx
- traefik
push:
tags:
- 'v*'
env:
IMAGE_NAME: ${{ gitea.repository }}
IMAGE_TAG: ${{ gitea.ref_name }}
# Registry (default Docker Hub, cambia con un secret se usi un registry privato)
REGISTRY: ${{ secrets.DOCKER_REGISTRY || 'docker.io' }}
# Modalità proxy
PROXY_MODE: ${{ inputs.proxy_mode || 'nginx' }}
# Container name sulla VPS
CONTAINER_NAME: test-app
# Porta host per modalità nginx
HOST_PORT: ${{ secrets.HOST_PORT || '8080' }}
jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
- name: Deploy to VPS via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
port: ${{ secrets.VPS_PORT || '22' }}
envs: IMAGE_NAME,IMAGE_TAG,REGISTRY,PROXY_MODE,CONTAINER_NAME,HOST_PORT
script: |
set -e
FULL_IMAGE="$REGISTRY/$IMAGE_NAME:$IMAGE_TAG"
echo "=== Pull immagine ==="
echo "$DOCKER_PASSWORD" | docker login "$REGISTRY" -u "$DOCKER_USERNAME" --password-stdin 2>/dev/null || true
docker pull "$FULL_IMAGE"
echo "=== Ferma e rimuovi container esistente ==="
docker stop "$CONTAINER_NAME" 2>/dev/null || true
docker rm "$CONTAINER_NAME" 2>/dev/null || true
echo "=== Avvia container (proxy: $PROXY_MODE) ==="
if [ "$PROXY_MODE" = "traefik" ]; then
docker run -d \
--name "$CONTAINER_NAME" \
--network traefik \
--label "traefik.enable=true" \
--label "traefik.http.routers.$CONTAINER_NAME.rule=Host(\`${{ secrets.DOMAIN }}\`)" \
--label "traefik.http.services.$CONTAINER_NAME.loadbalancer.server.port=80" \
"$FULL_IMAGE"
else
docker run -d \
--name "$CONTAINER_NAME" \
-p "$HOST_PORT:80" \
"$FULL_IMAGE"
fi
echo "=== Pulizia ==="
docker image prune -f
echo "=== Deploy completato ==="
echo "Immagine: $FULL_IMAGE"
echo "Proxy: $PROXY_MODE"
+7
View File
@@ -0,0 +1,7 @@
FROM nginx:stable-alpine
COPY index.html /usr/share/nginx/html/index.html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]