mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 21:32:56 -07:00
4464ff23fc
- Sets `GH_PAGER: ''` in the workflow environment variables to ensure any `gh` CLI commands executed by the bot or in bash scripts do not hang waiting for user input in the headless CI environment.
395 lines
18 KiB
YAML
395 lines
18 KiB
YAML
name: '🧠 Gemini CLI Bot: Brain'
|
|
|
|
on:
|
|
schedule:
|
|
- cron: '0 0 * * *' # Nightly (Strategic Metrics)
|
|
- cron: '0 */4 * * *' # Every 4 hours (Issue Fixing)
|
|
issue_comment:
|
|
types: ['created']
|
|
workflow_dispatch:
|
|
inputs:
|
|
run_interactive:
|
|
description: 'Run interactive flow (requires issue_number)'
|
|
type: 'boolean'
|
|
default: false
|
|
issue_number:
|
|
description: 'Issue/PR number to simulate context from'
|
|
type: 'string'
|
|
required: false
|
|
comment_id:
|
|
description: 'Specific comment ID to simulate'
|
|
type: 'string'
|
|
required: false
|
|
clear_memory:
|
|
description: 'Clear memory (drops learnings from previous runs)'
|
|
type: 'boolean'
|
|
default: false
|
|
enable_prs:
|
|
description: 'Enable PRs (automatically promote changes to PRs)'
|
|
type: 'boolean'
|
|
default: true
|
|
mandate:
|
|
description: 'Mandate to execute'
|
|
type: 'choice'
|
|
options:
|
|
- 'auto'
|
|
- 'issue-fixer'
|
|
- 'metrics'
|
|
- 'interactive'
|
|
default: auto
|
|
|
|
concurrency:
|
|
group: '${{ github.workflow }}-${{ github.event.issue.number || github.event.inputs.issue_number || github.ref }}'
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
reasoning:
|
|
name: 'Brain (Reasoning Layer)'
|
|
runs-on: 'ubuntu-latest'
|
|
if: |
|
|
github.repository == 'google-gemini/gemini-cli' && (
|
|
github.event_name == 'schedule' ||
|
|
(github.event_name == 'workflow_dispatch') ||
|
|
(github.event_name == 'issue_comment' && github.event.comment.user.login != 'gemini-cli[bot]' && contains(github.event.comment.body, '@gemini-cli') && contains(fromJSON('["COLLABORATOR", "MEMBER", "OWNER"]'), github.event.comment.author_association))
|
|
)
|
|
# The reasoning phase is strictly readonly.
|
|
permissions:
|
|
contents: 'read'
|
|
issues: 'read'
|
|
actions: 'read'
|
|
env:
|
|
GEMINI_CLI_TRUST_WORKSPACE: 'true'
|
|
steps:
|
|
- name: 'Determine Checkout Ref'
|
|
id: 'determine_ref'
|
|
env:
|
|
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
|
ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}'
|
|
run: |
|
|
REF="${{ github.ref }}"
|
|
if [ -n "$ISSUE_NUMBER" ]; then
|
|
PR_HEAD=$(gh pr view "$ISSUE_NUMBER" --repo "${{ github.repository }}" --json headRefName --jq .headRefName 2>/dev/null || echo "")
|
|
if [ -n "$PR_HEAD" ]; then
|
|
REF="$PR_HEAD"
|
|
fi
|
|
fi
|
|
echo "ref=$REF" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: 'Checkout'
|
|
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
|
with:
|
|
ref: '${{ steps.determine_ref.outputs.ref }}'
|
|
fetch-depth: 0
|
|
persist-credentials: false
|
|
|
|
- name: 'Setup Node.js'
|
|
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
|
with:
|
|
node-version: '20'
|
|
cache: 'npm'
|
|
|
|
- name: 'Install dependencies'
|
|
run: 'npm ci'
|
|
|
|
- name: 'Build Gemini CLI'
|
|
run: 'npm run bundle'
|
|
|
|
- name: 'Download Previous State'
|
|
env:
|
|
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
|
run: |
|
|
if [ "${{ github.event.inputs.clear_memory }}" = "true" ]; then
|
|
echo "Memory clear requested. Skipping previous state download."
|
|
exit 0
|
|
fi
|
|
|
|
# Find the last successful run of this workflow
|
|
LAST_RUN_ID=$(gh run list --workflow "${{ github.workflow }}" --status success --limit 1 --json databaseId --jq '.[0].databaseId')
|
|
|
|
if [ -n "$LAST_RUN_ID" ]; then
|
|
echo "Found previous successful run: $LAST_RUN_ID"
|
|
|
|
# Download brain memory to a temp dir so we can selectively restore only persistent state
|
|
mkdir -p .temp_brain_data
|
|
gh run download "$LAST_RUN_ID" -n brain-data -D .temp_brain_data || echo "brain-data not found"
|
|
|
|
# Restore only persistent memory files
|
|
cp .temp_brain_data/tools/gemini-cli-bot/lessons-learned.md tools/gemini-cli-bot/lessons-learned.md 2>/dev/null || true
|
|
mkdir -p tools/gemini-cli-bot/history/
|
|
cp .temp_brain_data/tools/gemini-cli-bot/history/*.csv tools/gemini-cli-bot/history/ 2>/dev/null || true
|
|
rm -rf .temp_brain_data
|
|
else
|
|
echo "No previous successful run found."
|
|
fi
|
|
|
|
- name: 'Collect Current Metrics'
|
|
env:
|
|
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
|
run: 'npx tsx tools/gemini-cli-bot/metrics/index.ts'
|
|
|
|
- name: 'Run Brain Phases'
|
|
env:
|
|
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
|
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
|
GEMINI_MODEL: 'gemini-3-flash-preview'
|
|
GEMINI_CLI_HOME: 'tools/gemini-cli-bot'
|
|
ENABLE_PRS: "${{ github.event.inputs.enable_prs || 'true' }}"
|
|
TRIGGER_ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}'
|
|
TRIGGER_COMMENT_ID: '${{ github.event.comment.id || github.event.inputs.comment_id }}'
|
|
# Enable detailed activity logging for debugging
|
|
GEMINI_TELEMETRY_ENABLED: 'true'
|
|
GEMINI_TELEMETRY_LOG_PROMPTS: 'true'
|
|
GEMINI_TELEMETRY_OUTFILE: 'brain-telemetry.json'
|
|
GEMINI_DEBUG_LOG_FILE: 'brain-debug.log'
|
|
GH_PAGER: ''
|
|
run: |
|
|
# Determine intent and prompt
|
|
MANDATE_INPUT="${{ github.event.inputs.mandate || 'auto' }}"
|
|
|
|
# Initialize defaults
|
|
PROMPT_FILE="tools/gemini-cli-bot/brain/scheduled.md"
|
|
MANDATE="Your specific mandate for this run: Implement surgical fixes for repository issues (issue-fixer skill)."
|
|
|
|
# Resolve Mandate and Prompt File
|
|
if [ "$MANDATE_INPUT" = "issue-fixer" ]; then
|
|
echo "Trigger: Manual Override (issue-fixer)"
|
|
MANDATE="Your specific mandate for this run: Implement surgical fixes for repository issues (issue-fixer skill)."
|
|
elif [ "$MANDATE_INPUT" = "metrics" ]; then
|
|
echo "Trigger: Manual Override (metrics)"
|
|
MANDATE="Your specific mandate for this run: Analyze repository metrics to identify bottlenecks and self-evolve (metrics skill)."
|
|
elif [ "$MANDATE_INPUT" = "interactive" ]; then
|
|
echo "Trigger: Manual Override (interactive)"
|
|
PROMPT_FILE="tools/gemini-cli-bot/brain/interactive.md"
|
|
MANDATE="Your specific mandate for this run: Respond to the user request in <untrusted_context>."
|
|
elif [ "${{ github.event_name }}" = "issue_comment" ] || [ "${{ github.event.inputs.run_interactive }}" = "true" ]; then
|
|
echo "Trigger: Issue/PR Comment or Interactive Dispatch"
|
|
PROMPT_FILE="tools/gemini-cli-bot/brain/interactive.md"
|
|
MANDATE="Your specific mandate for this run: Respond to the user request in <untrusted_context>."
|
|
elif [ "${{ github.event.schedule }}" = "0 0 * * *" ]; then
|
|
echo "Trigger: Nightly Schedule (Metrics)"
|
|
MANDATE="Your specific mandate for this run: Analyze repository metrics to identify bottlenecks and self-evolve (metrics skill)."
|
|
else
|
|
echo "Trigger: Scheduled or Manual Dispatch (Default: Issue-Fixer)"
|
|
fi
|
|
|
|
echo "Selected Prompt: $PROMPT_FILE"
|
|
echo "Selected Mandate: $MANDATE"
|
|
|
|
# Prepare Context if available
|
|
touch trigger_context.md
|
|
if [ -n "$TRIGGER_ISSUE_NUMBER" ]; then
|
|
echo "<untrusted_context>" > trigger_context.md
|
|
echo "# Interactive Trigger Context" >> trigger_context.md
|
|
echo "You were invoked by a user in issue/PR #$TRIGGER_ISSUE_NUMBER." >> trigger_context.md
|
|
|
|
if [ -n "$TRIGGER_COMMENT_ID" ]; then
|
|
echo "## User Comment" >> trigger_context.md
|
|
gh api "repos/${{ github.repository }}/issues/comments/$TRIGGER_COMMENT_ID" -q '.body' >> trigger_context.md 2>/dev/null || gh api "repos/${{ github.repository }}/pulls/comments/$TRIGGER_COMMENT_ID" -q '.body' >> trigger_context.md
|
|
echo "" >> trigger_context.md
|
|
fi
|
|
|
|
echo "## Issue/PR Context" >> trigger_context.md
|
|
gh issue view "$TRIGGER_ISSUE_NUMBER" >> trigger_context.md 2>/dev/null || gh pr view "$TRIGGER_ISSUE_NUMBER" >> trigger_context.md
|
|
echo "</untrusted_context>" >> trigger_context.md
|
|
fi
|
|
|
|
# Pass PR Enablement Directive
|
|
PR_DIRECTIVE="PR creation is DISABLED. You MUST NOT stage files."
|
|
if [ "${{ github.event.inputs.enable_prs || 'true' }}" = "true" ] || [ "${{ github.event_name }}" = "issue_comment" ]; then
|
|
PR_DIRECTIVE="PR creation is ENABLED. You MUST activate the 'prs' skill to stage changes if proposing fixes."
|
|
fi
|
|
|
|
# Assemble final prompt: Context + Base Brain + Specific Mandate
|
|
echo "System: $PR_DIRECTIVE" > combined_prompt.md
|
|
cat trigger_context.md "$PROMPT_FILE" >> combined_prompt.md
|
|
echo -e "\n\n# MANDATE FOR THIS RUN\n$MANDATE" >> combined_prompt.md
|
|
|
|
node bundle/gemini.js --policy tools/gemini-cli-bot/ci-policy.toml --prompt="$(cat combined_prompt.md)"
|
|
|
|
if [ -n "$TRIGGER_ISSUE_NUMBER" ] && [ ! -s "issue-comment.md" ] && [ ! -s "pr-comment.md" ]; then
|
|
echo "Agent failed to respond. Generating fallback error message."
|
|
echo "⚠️ **Gemini CLI Bot failed to generate a response.**" > "issue-comment.md"
|
|
echo "" >> "issue-comment.md"
|
|
echo "I encountered an error or failed to generate a complete response to your request. You can check the [GitHub Actions Run Log](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) for more details on what went wrong." >> "issue-comment.md"
|
|
fi
|
|
|
|
- name: 'Run Critique Phase'
|
|
if: "${{ github.event.inputs.enable_prs == 'true' || github.event_name == 'issue_comment' || github.event.inputs.run_interactive == 'true' }}"
|
|
env:
|
|
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
|
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
|
GEMINI_MODEL: 'gemini-3-flash-preview'
|
|
GEMINI_CLI_HOME: 'tools/gemini-cli-bot'
|
|
GH_PAGER: ''
|
|
run: |
|
|
if git diff --staged --quiet; then
|
|
echo "No changes staged. Skipping critique."
|
|
echo "[APPROVED]" > critique_result.txt
|
|
else
|
|
node bundle/gemini.js --policy tools/gemini-cli-bot/ci-policy.toml --prompt="$(cat tools/gemini-cli-bot/.gemini/skills/critique/SKILL.md)" 2>&1 | tee critique_output.log
|
|
|
|
if [ "${PIPESTATUS[0]}" -eq 0 ] && grep -q "\[APPROVED\]" critique_output.log && ! grep -q "\[REJECTED\]" critique_output.log; then
|
|
echo "[APPROVED]" > critique_result.txt
|
|
else
|
|
echo "Critique failed, rejected, or did not explicitly approve changes. Skipping PR creation."
|
|
echo "[REJECTED]" > critique_result.txt
|
|
fi
|
|
fi
|
|
|
|
- name: 'Generate Patch'
|
|
if: "${{ github.event.inputs.enable_prs == 'true' || github.event_name == 'issue_comment' || github.event.inputs.run_interactive == 'true' }}"
|
|
run: |
|
|
touch bot-changes.patch
|
|
touch pr-description.md
|
|
if [ -f critique_result.txt ] && grep -q "\[APPROVED\]" critique_result.txt && ! grep -q "\[REJECTED\]" critique_result.txt; then
|
|
git diff --staged > bot-changes.patch
|
|
else
|
|
echo "Critique did not approve. Skipping patch generation."
|
|
fi
|
|
|
|
- name: 'Archive Brain Data'
|
|
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
|
|
with:
|
|
name: 'brain-data'
|
|
path: |
|
|
tools/gemini-cli-bot/lessons-learned.md
|
|
tools/gemini-cli-bot/history/*.csv
|
|
brain-telemetry.json
|
|
brain-debug.log
|
|
bot-changes.patch
|
|
pr-description.md
|
|
branch-name.txt
|
|
pr-comment.md
|
|
pr-number.txt
|
|
issue-comment.md
|
|
retention-days: 90
|
|
|
|
publish:
|
|
name: 'Publish Artifacts (Archive Layer)'
|
|
needs: 'reasoning'
|
|
runs-on: 'ubuntu-latest'
|
|
if: "github.repository == 'google-gemini/gemini-cli'"
|
|
# The publish phase is for archiving artifacts and optionally creating PRs.
|
|
permissions:
|
|
contents: 'write'
|
|
pull-requests: 'write'
|
|
actions: 'write'
|
|
steps:
|
|
- name: 'Generate GitHub App Token 🔑'
|
|
id: 'generate_token'
|
|
if: "${{ github.event.inputs.enable_prs == 'true' || github.event_name == 'issue_comment' || github.event.inputs.run_interactive == 'true' }}"
|
|
uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b' # ratchet:actions/create-github-app-token@v2
|
|
with:
|
|
app-id: '${{ secrets.APP_ID }}'
|
|
private-key: '${{ secrets.PRIVATE_KEY }}'
|
|
owner: '${{ github.repository_owner }}'
|
|
repositories: '${{ github.event.repository.name }}'
|
|
permission-contents: 'write'
|
|
permission-pull-requests: 'write'
|
|
permission-issues: 'write'
|
|
|
|
- name: 'Determine Checkout Ref'
|
|
id: 'determine_ref'
|
|
env:
|
|
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
|
ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}'
|
|
run: |
|
|
REF="main"
|
|
if [ -n "$ISSUE_NUMBER" ]; then
|
|
PR_HEAD=$(gh pr view "$ISSUE_NUMBER" --repo "${{ github.repository }}" --json headRefName --jq .headRefName 2>/dev/null || echo "")
|
|
if [ -n "$PR_HEAD" ]; then
|
|
REF="$PR_HEAD"
|
|
fi
|
|
fi
|
|
echo "ref=$REF" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: 'Checkout'
|
|
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
|
with:
|
|
ref: '${{ steps.determine_ref.outputs.ref }}'
|
|
fetch-depth: 0
|
|
persist-credentials: false
|
|
|
|
- name: 'Download Brain Data'
|
|
uses: 'actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093' # ratchet:actions/download-artifact@v4
|
|
with:
|
|
name: 'brain-data'
|
|
path: '${{ runner.temp }}/brain-data/'
|
|
|
|
- name: 'Create or Update PR'
|
|
if: "${{ github.event.inputs.enable_prs == 'true' || github.event_name == 'issue_comment' || github.event.inputs.run_interactive == 'true' }}"
|
|
env:
|
|
GH_TOKEN: '${{ steps.generate_token.outputs.token }}'
|
|
FALLBACK_PAT: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
|
|
run: |
|
|
if [ -s "${{ runner.temp }}/brain-data/bot-changes.patch" ]; then
|
|
git config user.name "gemini-cli[bot]"
|
|
git config user.email "gemini-cli[bot]@users.noreply.github.com"
|
|
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git"
|
|
|
|
BRANCH_NAME="bot/productivity-updates-$(date +'%Y%m%d%H%M%S')-${{ github.run_id }}"
|
|
if [ -f "${{ runner.temp }}/brain-data/branch-name.txt" ]; then
|
|
BRANCH_NAME=$(cat "${{ runner.temp }}/brain-data/branch-name.txt")
|
|
fi
|
|
|
|
if [[ ! "$BRANCH_NAME" =~ ^bot/ ]]; then
|
|
echo "Error: Branch name '$BRANCH_NAME' does not start with 'bot/'. Safety abort."
|
|
exit 1
|
|
fi
|
|
|
|
git checkout -B "$BRANCH_NAME"
|
|
git apply "${{ runner.temp }}/brain-data/bot-changes.patch"
|
|
git add .
|
|
|
|
PR_TITLE="🤖 Gemini Bot Maintenance Update"
|
|
if [ -s "${{ runner.temp }}/brain-data/pr-description.md" ]; then
|
|
git commit -F "${{ runner.temp }}/brain-data/pr-description.md"
|
|
PR_TITLE=$(head -n 1 "${{ runner.temp }}/brain-data/pr-description.md")
|
|
else
|
|
git commit -m "$PR_TITLE"
|
|
fi
|
|
|
|
if ! git push origin "$BRANCH_NAME" --force; then
|
|
echo "Push failed. Retrying with FALLBACK_PAT..."
|
|
export GH_TOKEN="$FALLBACK_PAT"
|
|
git remote set-url origin "https://x-access-token:${FALLBACK_PAT}@github.com/${{ github.repository }}.git"
|
|
git push origin "$BRANCH_NAME" --force
|
|
fi
|
|
|
|
if ! gh pr view "$BRANCH_NAME" > /dev/null 2>&1; then
|
|
gh pr create --draft --title "$PR_TITLE" --body-file "${{ runner.temp }}/brain-data/pr-description.md" --head "$BRANCH_NAME" --base main || \
|
|
gh pr create --draft --title "🤖 Gemini Bot Productivity Optimizations" --body "Automated changes generated by Gemini CLI Bot." --head "$BRANCH_NAME" --base main
|
|
else
|
|
PR_STATE=$(gh pr view "$BRANCH_NAME" --json state --jq .state)
|
|
if [ "$PR_STATE" = "CLOSED" ]; then
|
|
NEW_BRANCH_NAME="${BRANCH_NAME}-retry-${{ github.run_id }}"
|
|
git checkout -b "$NEW_BRANCH_NAME"
|
|
git push origin "$NEW_BRANCH_NAME" --force
|
|
gh pr create --draft --title "$PR_TITLE" --body-file "${{ runner.temp }}/brain-data/pr-description.md" --head "$NEW_BRANCH_NAME" --base main || \
|
|
gh pr create --draft --title "🤖 Gemini Bot Productivity Optimizations" --body "Automated changes generated by Gemini CLI Bot." --head "$NEW_BRANCH_NAME" --base main
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
- name: 'Post PR/Issue Comment'
|
|
env:
|
|
GH_TOKEN: '${{ steps.generate_token.outputs.token }}'
|
|
TRIGGER_ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}'
|
|
run: |
|
|
if [ -s "${{ runner.temp }}/brain-data/issue-comment.md" ] && [ -n "$TRIGGER_ISSUE_NUMBER" ]; then
|
|
echo "Posting comment to triggering issue #$TRIGGER_ISSUE_NUMBER"
|
|
# Use REST API (gh api) instead of GraphQL (gh issue comment) to ensure robot identity
|
|
# while avoiding potential GraphQL-specific authorization hurdles with PATs.
|
|
gh api "repos/${{ github.repository }}/issues/$TRIGGER_ISSUE_NUMBER/comments" -F body=@"${{ runner.temp }}/brain-data/issue-comment.md"
|
|
fi
|
|
|
|
if [ -s "${{ runner.temp }}/brain-data/pr-comment.md" ] && [ -f "${{ runner.temp }}/brain-data/pr-number.txt" ]; then
|
|
PR_NUM=$(cat "${{ runner.temp }}/brain-data/pr-number.txt")
|
|
|
|
# Using GitHub App, so author check is no longer valid against gemini-cli-robot
|
|
# Skipping author validation here to let the app post.
|
|
|
|
# Use REST API (gh api) for consistency and robot identity
|
|
gh api "repos/${{ github.repository }}/issues/$PR_NUM/comments" -F body=@"${{ runner.temp }}/brain-data/pr-comment.md"
|
|
fi
|