mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-14 05:42:54 -07:00
2334e9b1c4
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
351 lines
17 KiB
YAML
351 lines
17 KiB
YAML
name: '🧠 Gemini CLI Bot: Brain'
|
|
|
|
on:
|
|
schedule:
|
|
- cron: '0 0 * * *' # Every 24 hours
|
|
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: false
|
|
|
|
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.inputs.run_interactive != 'true') ||
|
|
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_interactive == 'true') ||
|
|
(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 || 'false' }}"
|
|
TRIGGER_ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}'
|
|
TRIGGER_COMMENT_ID: '${{ github.event.comment.id || github.event.inputs.comment_id }}'
|
|
run: |
|
|
PROMPT_PATH="tools/gemini-cli-bot/brain/scheduled.md"
|
|
if [ "${{ github.event_name }}" = "issue_comment" ] || [ "${{ github.event.inputs.run_interactive }}" = "true" ]; then
|
|
PROMPT_PATH="tools/gemini-cli-bot/brain/interactive.md"
|
|
export ENABLE_PRS="true"
|
|
fi
|
|
|
|
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
|
|
|
|
if [ "$ENABLE_PRS" = "true" ]; then
|
|
echo "**System Directive**: PR creation is ENABLED for this run. You MUST activate the **'prs' skill** to stage your changes and generate a \`pr-description.md\` file if you are proposing fixes." >> trigger_context.md
|
|
echo "**CRITICAL System Directive**: You MUST ONLY propose and implement a **SINGLE** improvement or fix per run. Bundling unrelated changes (e.g., a documentation update and a script fix, or a metrics update and a logic fix) into a single PR is STRICTLY FORBIDDEN and will result in immediate rejection during the critique phase. If you identify multiple issues, pick the most impactful one and ignore the others for now." >> trigger_context.md
|
|
else
|
|
echo "**System Directive**: PR creation is DISABLED for this run. You MUST NOT stage files or attempt to create a PR description." >> trigger_context.md
|
|
fi
|
|
echo "" >> trigger_context.md
|
|
|
|
cat trigger_context.md "$PROMPT_PATH" > 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'
|
|
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
|
|
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 .
|
|
|
|
if [ -s "${{ runner.temp }}/brain-data/pr-description.md" ]; then
|
|
git commit -F "${{ runner.temp }}/brain-data/pr-description.md"
|
|
else
|
|
git commit -m "🤖 Gemini Bot Productivity Optimizations"
|
|
fi
|
|
|
|
PR_TITLE="🤖 Gemini Bot Productivity Optimizations"
|
|
if [ -s "${{ runner.temp }}/brain-data/pr-description.md" ]; then
|
|
PR_TITLE=$(head -n 1 "${{ runner.temp }}/brain-data/pr-description.md")
|
|
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
|