7 Commits
v4 ... v11

Author SHA1 Message Date
Markus Hofstetter 5cd267296e fix: update tool-call extraction logic for Pi session JSONL
Refine parsing of assistant tool calls in review.sh to handle "toolCall" and "arguments" structures, aligning with Pi session format changes.
2026-06-13 14:47:06 +02:00
Markus Hofstetter 32829ae8ba feat: add debug logging for session directory and file in review script
Includes additional diagnostic outputs for PI_DEBUG mode: contents of the session directory and the selected session file.
2026-06-13 11:42:04 +02:00
Markus Hofstetter cd77158577 feat: add server_url input to override auto-detected API server URL
Allows specifying internal routing addresses for Gitea/GitHub API calls (e.g., http://server:3000), addressing container network restrictions when external URLs are not routable.
2026-06-13 11:30:02 +02:00
Markus Hofstetter 7e6b266c34 fix: resolve PR number from event payload for Gitea Actions
Gitea Actions mirrors GitHub Actions env vars and does not expose a flat
GITEA_EVENT_PULL_REQUEST_NUMBER. Parse the PR number out of
$GITHUB_EVENT_PATH, key platform detection on $GITEA_ACTIONS, and use
the correct Accept header for GitHub's diff endpoint.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 23:58:38 +02:00
Markus Hofstetter f1a4958b34 fix: fetch diff via Gitea/GitHub API instead of git
Git operations inside the Docker container have no auth credentials
(actions/checkout@v5 stores them in $RUNNER_TEMP, not mounted).

Instead of fighting git auth, fetch the diff directly from the
Gitea API: GET /repos/{owner}/{repo}/pulls/{index}.diff

This uses the same token already passed for posting comments.
No pre-fetch workflow step needed. No git required in the container.

Also filters excluded patterns (lockfiles, etc.) from the API diff.
2026-05-21 01:08:21 +02:00
Markus Hofstetter 15df936641 fix: use token for git auth inside Docker container
actions/checkout@v5 stores credentials in $RUNNER_TEMP which is not
mounted into the Docker container. Instead of requiring a pre-fetch
step in the workflow, we now inject the token into the remote URL
so git operations work inside the container.

Workflow no longer needs the 'Fetch base branch' pre-step.
2026-05-20 00:18:03 +02:00
Markus Hofstetter cfc5ae1d5c fix: check existing refs before trying to fetch inside Docker
The Docker container has no git auth credentials. Instead of trying
to fetch (which silently fails), first check if origin/main already
exists from a workflow pre-step. Only fall back to fetching if no
refs are found, with a clear error message telling users to add a
pre-fetch step.
2026-05-20 00:13:25 +02:00
3 changed files with 158 additions and 67 deletions
+4
View File
@@ -18,6 +18,10 @@ inputs:
description: "Custom API base URL (for OpenAI-compatible providers). Ignored for built-in providers." description: "Custom API base URL (for OpenAI-compatible providers). Ignored for built-in providers."
required: false required: false
default: "" default: ""
server_url:
description: "Override the Gitea/GitHub server URL used for API calls (e.g. http://server:3000 for internal routing inside the runner). Defaults to the auto-detected server URL."
required: false
default: ""
token: token:
description: "Git platform token for posting PR comments" description: "Git platform token for posting PR comments"
required: true required: true
+1
View File
@@ -11,6 +11,7 @@ export PI_API_KEY="${INPUT_API_KEY}"
export PI_PROVIDER="${INPUT_PROVIDER:-zai}" export PI_PROVIDER="${INPUT_PROVIDER:-zai}"
export PI_MODEL="${INPUT_MODEL:-glm-5.1}" export PI_MODEL="${INPUT_MODEL:-glm-5.1}"
export PI_BASE_URL="${INPUT_BASE_URL:-}" export PI_BASE_URL="${INPUT_BASE_URL:-}"
export PI_SERVER_URL="${INPUT_SERVER_URL:-}"
export PI_TOOLS="${INPUT_TOOLS:-read,grep,find,ls}" export PI_TOOLS="${INPUT_TOOLS:-read,grep,find,ls}"
export PI_REVIEW_PROMPT="${INPUT_REVIEW_PROMPT:-}" export PI_REVIEW_PROMPT="${INPUT_REVIEW_PROMPT:-}"
export PI_EXCLUDE="${INPUT_EXCLUDE_PATTERNS:-*.lock package-lock.json yarn.lock pnpm-lock.yaml *.min.js *.min.css *.map}" export PI_EXCLUDE="${INPUT_EXCLUDE_PATTERNS:-*.lock package-lock.json yarn.lock pnpm-lock.yaml *.min.js *.min.css *.map}"
+149 -63
View File
@@ -70,57 +70,148 @@ chmod 600 "$AUTH_FILE"
echo "Configured provider: ${PI_PROVIDER}" echo "Configured provider: ${PI_PROVIDER}"
echo "::endgroup::" echo "::endgroup::"
# ─── Phase 2: Generate diff ─────────────────────────────────────────────────── # ─── Phase 2: Fetch diff via API ───────────────────────────────────────────────
echo "::group::Generate diff" echo "Generate diff"
# actions/checkout for PRs only fetches the PR ref (refs/pull/N/head). # Git operations inside the Docker container have no auth credentials
# It does NOT create remote tracking branches like origin/main. # (actions/checkout@v5 stores them in $RUNNER_TEMP, which isn't mounted).
# We must explicitly fetch the base branch. # Instead, we get the diff directly from the Gitea/GitHub API using the token
# we already have for posting comments.
# Unshallow if needed (fetch-depth: 0 already does this, but be safe) # Gitea Actions mirrors GitHub Actions env vars ($GITHUB_*) and exposes the
git fetch --unshallow origin 2>/dev/null || true # event payload at $GITHUB_EVENT_PATH. There is no flat $GITEA_EVENT_*
# variable for the PR number, so we parse it out of the event JSON.
# Fetch base branch with explicit refspec to ensure origin/main exists REPO="${GITHUB_REPOSITORY:-${GITEA_REPOSITORY:-}}"
if git fetch origin refs/heads/main:refs/remotes/origin/main 2>/dev/null; then
BASE="origin/main" # Detect Gitea vs GitHub. Gitea sets $GITHUB_SERVER_URL too, so the only
elif git fetch origin refs/heads/master:refs/remotes/origin/master 2>/dev/null; then # reliable signal is $GITEA_ACTIONS (or a non-github.com server URL).
BASE="origin/master" #
# $PI_SERVER_URL (server_url input) overrides the auto-detected server URL.
# Gitea reports its *external* hostname (e.g. git.example.com), which may not
# be routable from inside the runner's container network. Setting server_url
# to the internal address (e.g. http://server:3000) fixes API calls.
SERVER_URL="${PI_SERVER_URL:-${GITHUB_SERVER_URL:-${GITEA_SERVER_URL:-}}}"
if [ -n "${GITEA_ACTIONS:-}" ] || { [ -n "$SERVER_URL" ] && [ "${SERVER_URL#*github.com}" = "$SERVER_URL" ]; }; then
API_BASE="${SERVER_URL}/api/v1"
PLATFORM="gitea"
echo "Platform: Gitea (${SERVER_URL})"
else else
# Fallback: try Gitea/GitHub event context for the target branch API_BASE="${GITHUB_API_URL:-https://api.github.com}"
TARGET_BRANCH="${GITEA_BASE_REF:-${GITHUB_BASE_REF:-}}" PLATFORM="github"
if [ -n "${TARGET_BRANCH}" ] && git fetch origin "refs/heads/${TARGET_BRANCH}:refs/remotes/origin/${TARGET_BRANCH}" 2>/dev/null; then echo "Platform: GitHub"
BASE="origin/${TARGET_BRANCH}"
else
echo "::warning::Could not fetch base branch. Trying origin/HEAD."
git fetch origin 2>/dev/null || true
BASE="origin/HEAD"
fi
fi fi
echo "Base ref: ${BASE} -> $(git rev-parse --short "${BASE}" 2>/dev/null || echo 'NOT FOUND')" # Resolve PR number from the event payload.
echo "HEAD: $(git rev-parse --short HEAD)" PR_NUMBER=""
echo "Files changed:" if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -f "${GITHUB_EVENT_PATH}" ]; then
git diff --stat "${BASE}...HEAD" 2>/dev/null | tail -3 || echo "(could not stat diff)" PR_NUMBER=$(node -e "
try {
# Build exclude pathspecs const e = require(process.env.GITHUB_EVENT_PATH);
EXCLUDE_ARGS="" const n = (e.pull_request && e.pull_request.number) || e.number || '';
for pattern in $PI_EXCLUDE; do process.stdout.write(String(n || ''));
EXCLUDE_ARGS="$EXCLUDE_ARGS ':!$pattern'" } catch { process.stdout.write(''); }
done ")
eval "git diff ${BASE}...HEAD ${EXCLUDE_ARGS}" > /tmp/pi-diff.txt 2>/dev/null || true
# Truncate if needed
if [ "${PI_MAX_DIFF}" -gt 0 ]; then
head -c "${PI_MAX_DIFF}" /tmp/pi-diff.txt > /tmp/pi-diff-trunc.txt
mv /tmp/pi-diff-trunc.txt /tmp/pi-diff.txt
fi fi
DIFF_SIZE=$(wc -c < /tmp/pi-diff.txt || echo 0) echo "Repo: ${REPO}, PR: ${PR_NUMBER}"
echo "Diff size: ${DIFF_SIZE} bytes" echo "DEBUG: PLATFORM=${PLATFORM} API_BASE=${API_BASE} EVENT=${GITHUB_EVENT_PATH:-}"
echo "::endgroup::"
if [ "${DIFF_SIZE}" -eq 0 ]; then if [ -z "$PR_NUMBER" ]; then
echo "Not a pull request event. Skipping review."
exit 0
fi
# Fetch diff via API — works regardless of git auth inside the container.
# Gitea: GET /repos/{owner}/{repo}/pulls/{index}.diff
# GitHub: GET /repos/{owner}/{repo}/pulls/{index} (Accept: application/diff)
node -e "
const http = require('http');
const https = require('https');
const apiBase = '${API_BASE}';
const repo = '${REPO}';
const prNumber = '${PR_NUMBER}';
const token = '${PI_TOKEN}';
const platform = '${PLATFORM}';
const maxBytes = ${PI_MAX_DIFF:-80000};
function fetchDiff() {
return new Promise((resolve, reject) => {
// Gitea: GET {api}/repos/{owner}/{repo}/pulls/{n}.diff
// GitHub: GET {api}/repos/{owner}/{repo}/pulls/{n} with Accept: application/vnd.github.v3.diff
const path = platform === 'gitea'
? '/repos/' + repo + '/pulls/' + prNumber + '.diff'
: '/repos/' + repo + '/pulls/' + prNumber;
const url = new URL(apiBase + path);
const transport = url.protocol === 'http:' ? http : https;
const headers = {
'Authorization': 'token ' + token,
'User-Agent': 'pi-code-review',
'Accept': platform === 'gitea' ? 'text/plain' : 'application/vnd.github.v3.diff',
};
const options = {
hostname: url.hostname,
port: url.port || (url.protocol === 'http:' ? 80 : 443),
path: url.pathname,
method: 'GET',
headers,
};
const req = transport.request(options, (res) => {
if (res.statusCode < 200 || res.statusCode >= 300) {
let body = '';
res.on('data', (c) => { body += c; });
res.on('end', () => { reject(new Error('API ' + res.statusCode + ' for ' + url.href + ': ' + body.slice(0, 200))); });
return;
}
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => { resolve(data); });
});
req.on('error', (e) => { reject(e); });
req.end();
});
}
fetchDiff().then((diff) => {
const fs = require('fs');
// Filter out excluded patterns (lockfiles, generated code, etc.)
const excludePatterns = '${PI_EXCLUDE}'.split(' ').filter(Boolean);
if (excludePatterns.length > 0) {
const lines = diff.split('\\n');
const filtered = [];
let skipFile = false;
for (const line of lines) {
if (line.startsWith('diff --git')) {
skipFile = excludePatterns.some(p => {
const glob = p.replace(/\\./g, '\\\\.').replace(/\\*/g, '.*');
return new RegExp(glob).test(line);
});
}
if (!skipFile) filtered.push(line);
}
diff = filtered.join('\\n');
}
if (maxBytes > 0 && diff.length > maxBytes) {
diff = diff.slice(0, maxBytes) + '\\n... (truncated at ' + maxBytes + ' bytes)';
}
fs.writeFileSync('/tmp/pi-diff.txt', diff);
console.log('Diff fetched: ' + diff.length + ' bytes');
}).catch((e) => {
console.error('Failed to fetch diff: ' + e.message);
process.exit(1);
});
"
if [ ! -s /tmp/pi-diff.txt ]; then
echo "No changes to review. Skipping." echo "No changes to review. Skipping."
exit 0 exit 0
fi fi
@@ -164,14 +255,21 @@ if [ ! -s /tmp/pi-review.md ]; then
exit 1 exit 1
fi fi
echo "Debug mode: PI_DEBUG=${PI_DEBUG}"
# If debug mode, extract tool calls from session and append to review # If debug mode, extract tool calls from session and append to review
if [ "${PI_DEBUG}" = "true" ]; then if [ "${PI_DEBUG}" = "true" ]; then
echo "Session dir contents:"
find /tmp/pi-session -type f 2>/dev/null || echo " (none)"
SESSION_FILE=$(find /tmp/pi-session -name '*.jsonl' -type f 2>/dev/null | head -1) SESSION_FILE=$(find /tmp/pi-session -name '*.jsonl' -type f 2>/dev/null | head -1)
echo "Session file: ${SESSION_FILE:-<none found>}"
TOOL_LOG="" TOOL_LOG=""
if [ -n "${SESSION_FILE}" ]; then if [ -n "${SESSION_FILE}" ]; then
# Extract tool-use entries: each line is a JSON object. # Extract tool-call entries from Pi session JSONL.
# Tool calls appear as messages with tool_use/function_call content. # Pi stores assistant messages with content blocks of type "toolCall"
# (NOT the Anthropic SDK's "tool_use"), using "arguments" (NOT "input").
# Tool results are separate entries with role "toolResult".
TOOL_LOG=$(node -e " TOOL_LOG=$(node -e "
const fs = require('fs'); const fs = require('fs');
const lines = fs.readFileSync('${SESSION_FILE}', 'utf8').trim().split('\n'); const lines = fs.readFileSync('${SESSION_FILE}', 'utf8').trim().split('\n');
@@ -179,20 +277,18 @@ if [ "${PI_DEBUG}" = "true" ]; then
for (const line of lines) { for (const line of lines) {
try { try {
const entry = JSON.parse(line); const entry = JSON.parse(line);
if (entry.type === 'message') { if (entry.type !== 'message') continue;
const msg = entry.message; const msg = entry.message;
// Model requesting tool use if (!msg || !msg.content) continue;
if (msg.role === 'assistant' && msg.content) {
const parts = Array.isArray(msg.content) ? msg.content : [msg.content]; const parts = Array.isArray(msg.content) ? msg.content : [msg.content];
for (const part of parts) { for (const part of parts) {
if (typeof part === 'object' && part.type === 'tool_use') { if (typeof part !== 'object') continue;
const input = part.input || {}; if (part.type === 'toolCall') {
const args = Object.entries(input).map(([k,v]) => k + '=' + JSON.stringify(v)).join(' '); const args = Object.entries(part.arguments || {})
.map(([k,v]) => k + '=' + JSON.stringify(v)).join(' ');
entries.push('[tool:' + part.name + '] ' + args); entries.push('[tool:' + part.name + '] ' + args);
} }
} }
}
}
} catch {} } catch {}
} }
console.log(entries.join('\n')); console.log(entries.join('\n'));
@@ -225,17 +321,7 @@ echo "::endgroup::"
# ─── Phase 4: Post comment ──────────────────────────────────────────────────── # ─── Phase 4: Post comment ────────────────────────────────────────────────────
echo "::group::Post review comment" echo "::group::Post review comment"
# Detect Gitea vs GitHub # API_BASE / REPO / PR_NUMBER were resolved in Phase 2.
if [ -n "${GITEA_SERVER_URL:-}" ]; then
API_BASE="${GITEA_SERVER_URL}/api/v1"
PR_NUMBER="${GITEA_EVENT_PULL_REQUEST_NUMBER:-}"
REPO="${GITEA_REPOSITORY:-}"
else
API_BASE="${GITHUB_API_URL:-https://api.github.com}"
PR_NUMBER="${GITHUB_EVENT_PULL_REQUEST_NUMBER:-}"
REPO="${GITHUB_REPOSITORY:-}"
fi
if [ -z "$PR_NUMBER" ]; then if [ -z "$PR_NUMBER" ]; then
echo "Not a pull request event. Review written to /tmp/pi-review.md only." echo "Not a pull request event. Review written to /tmp/pi-review.md only."
echo "::endgroup::" echo "::endgroup::"