chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 11:58:09 +08:00
commit c48e26cdd0
2909 changed files with 1591131 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
action: 'log'
+17
View File
@@ -0,0 +1,17 @@
# Git history - not needed in build context
.git
# Root node_modules - reinstalled inside container via npm ci
node_modules
# Package-level node_modules - reinstalled inside container
packages/*/node_modules
# Development and IDE files
.github
.vscode
npm-debug.log*
# Misc
.DS_Store
*.tmp
+13
View File
@@ -0,0 +1,13 @@
root = true
[*]
charset = utf-8
insert_final_newline = true
end_of_line = lf
indent_style = space
indent_size = 2
max_line_length = 80
[Makefile]
indent_style = tab
indent_size = 8
+89
View File
@@ -0,0 +1,89 @@
# --- STAGE 1: Base Runtime ---
FROM docker.io/library/node:20-slim AS base
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
python3-pip \
python3-venv \
curl \
dnsutils \
less \
jq \
ca-certificates \
git \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# --- STAGE 2: Builder (Compile Main) ---
FROM base AS builder
WORKDIR /build
COPY . .
RUN npm ci --ignore-scripts
RUN npm run bundle
# Run the official release preparation script to move the bundle and assets into packages/cli
RUN node scripts/prepare-npm-release.js
# --- STAGE 3: Development Environment ---
FROM base AS development
WORKDIR /home/node/dev/main
# Set up npm global package folder
RUN mkdir -p /usr/local/share/npm-global \
&& chown -R node:node /usr/local/share/npm-global
ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global
ENV PATH=$PATH:/usr/local/share/npm-global/bin
# Copy package.json to extract versions for global tools
COPY package.json /tmp/package.json
# Install Build Tools, Global Dev Tools (pinned), and Linters
ARG ACTIONLINT_VER=1.7.7
ARG SHELLCHECK_VER=0.11.0
ARG YAMLLINT_VER=1.35.1
RUN apt-get update && apt-get install -y --no-install-recommends \
make \
g++ \
gh \
git \
unzip \
rsync \
ripgrep \
procps \
psmisc \
lsof \
socat \
tmux \
docker.io \
build-essential \
libsecret-1-dev \
libkrb5-dev \
file \
&& curl -sSLo /tmp/actionlint.tar.gz https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VER}/actionlint_${ACTIONLINT_VER}_linux_amd64.tar.gz \
&& tar -xzf /tmp/actionlint.tar.gz -C /usr/local/bin actionlint \
&& curl -sSLo /tmp/shellcheck.tar.xz https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VER}/shellcheck-v${SHELLCHECK_VER}.linux.x86_64.tar.xz \
&& tar -xf /tmp/shellcheck.tar.xz -C /usr/local/bin --strip-components=1 shellcheck-v${SHELLCHECK_VER}/shellcheck \
&& pip3 install --break-system-packages yamllint==${YAMLLINT_VER} \
&& export TSX_VER=$(node -p "require('/tmp/package.json').devDependencies.tsx") \
&& export VITEST_VER=$(node -p "require('/tmp/package.json').devDependencies.vitest") \
&& export PRETTIER_VER=$(node -p "require('/tmp/package.json').devDependencies.prettier") \
&& export ESLINT_VER=$(node -p "require('/tmp/package.json').devDependencies.eslint") \
&& export CROSS_ENV_VER=$(node -p "require('/tmp/package.json').devDependencies['cross-env']") \
&& npm install -g tsx@$TSX_VER vitest@$VITEST_VER prettier@$PRETTIER_VER eslint@$ESLINT_VER cross-env@$CROSS_ENV_VER typescript@5.3.3 \
&& npm install -g @google/gemini-cli@nightly && mv /usr/local/share/npm-global/bin/gemini /usr/local/share/npm-global/bin/g-nightly \
&& npm install -g @google/gemini-cli@preview && mv /usr/local/share/npm-global/bin/gemini /usr/local/share/npm-global/bin/g-preview \
&& npm install -g @google/gemini-cli@latest && mv /usr/local/share/npm-global/bin/gemini /usr/local/share/npm-global/bin/g-stable \
&& apt-get purge -y build-essential libsecret-1-dev libkrb5-dev \
&& apt-get autoremove -y \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /root/.npm
# Copy the bundled CLI package to a permanent location and install it
# We MUST not delete this source folder as 'npm install -g <folder>'
# often symlinks to it for local folder installs.
COPY --from=builder /build/packages/cli /usr/local/lib/gemini-cli
RUN npm install -g /usr/local/lib/gemini-cli
USER node
CMD ["/bin/bash"]
+10
View File
@@ -0,0 +1,10 @@
node_modules
.git
.gemini/workspaces
dist
!packages/*/dist/*.tgz
bundle
out
*.log
.env
.DS_Store
+89
View File
@@ -0,0 +1,89 @@
# Use a common base image like Debian.
# Using 'bookworm-slim' for a balance of size and compatibility.
FROM debian:bookworm-slim
# Set environment variables to prevent interactive prompts during installation
ENV DEBIAN_FRONTEND=noninteractive
ENV NODE_VERSION=20.12.2
ENV NODE_VERSION_MAJOR=20
ENV DOCKER_CLI_VERSION=26.1.3
ENV BUILDX_VERSION=v0.14.0
# Install dependencies for adding NodeSource repository, gcloud, and other tools
# - curl: for downloading files
# - gnupg: for managing GPG keys (used by NodeSource & Google Cloud SDK)
# - apt-transport-https: for HTTPS apt repositories
# - ca-certificates: for HTTPS apt repositories
# - rsync: the rsync utility itself
# - git: often useful in build environments
# - python3, python3-pip, python3-venv, python3-crcmod: for gcloud SDK and some of its components
# - lsb-release: for gcloud install script to identify distribution
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
gnupg \
apt-transport-https \
ca-certificates \
rsync \
git \
python3 \
python3-pip \
python3-venv \
python3-crcmod \
lsb-release \
&& rm -rf /var/lib/apt/lists/*
# Install Node.js and npm
# We'll use the official NodeSource repository for a specific version
RUN set -eux; \
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
# For Node.js 20.x, it's node_20.x
# Let's explicitly define the major version for clarity
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" > /etc/apt/sources.list.d/nodesource.list && \
apt-get update && \
apt-get install -y --no-install-recommends nodejs && \
npm install -g npm@latest && \
# Verify installations
node -v && \
npm -v && \
rm -rf /var/lib/apt/lists/*
# Install Docker CLI
# Download the static binary from Docker's official source
RUN set -eux; \
DOCKER_CLI_ARCH=$(dpkg --print-architecture); \
case "${DOCKER_CLI_ARCH}" in \
amd64) DOCKER_CLI_ARCH_SUFFIX="x86_64" ;; \
arm64) DOCKER_CLI_ARCH_SUFFIX="aarch64" ;; \
*) echo "Unsupported architecture: ${DOCKER_CLI_ARCH}"; exit 1 ;; \
esac; \
curl -fsSL "https://download.docker.com/linux/static/stable/${DOCKER_CLI_ARCH_SUFFIX}/docker-${DOCKER_CLI_VERSION}.tgz" -o docker.tgz && \
tar -xzf docker.tgz --strip-components=1 -C /usr/local/bin docker/docker && \
rm docker.tgz && \
# Verify installation
docker --version
# Install Docker Buildx plugin
RUN set -eux; \
BUILDX_ARCH_DEB=$(dpkg --print-architecture); \
case "${BUILDX_ARCH_DEB}" in \
amd64) BUILDX_ARCH_SUFFIX="amd64" ;; \
arm64) BUILDX_ARCH_SUFFIX="arm64" ;; \
*) echo "Unsupported architecture for Buildx: ${BUILDX_ARCH_DEB}"; exit 1 ;; \
esac; \
mkdir -p /usr/local/lib/docker/cli-plugins && \
curl -fsSL "https://github.com/docker/buildx/releases/download/${BUILDX_VERSION}/buildx-${BUILDX_VERSION}.linux-${BUILDX_ARCH_SUFFIX}" -o /usr/local/lib/docker/cli-plugins/docker-buildx && \
chmod +x /usr/local/lib/docker/cli-plugins/docker-buildx && \
# verify installation
docker buildx version
# Install Google Cloud SDK (gcloud CLI)
RUN echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list && curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg && apt-get update -y && apt-get install google-cloud-cli -y
# Set a working directory (optional, but good practice)
WORKDIR /workspace
# You can add a CMD or ENTRYPOINT if you intend to run this image directly,
# but for Cloud Build, it's usually not necessary as Cloud Build steps override it.
# For example:
ENTRYPOINT '/bin/bash'
+58
View File
@@ -0,0 +1,58 @@
substitutions:
_IMAGE_NAME: 'development'
_ARTIFACT_REGISTRY_REPO: 'us-docker.pkg.dev/gemini-code-dev/gemini-cli'
steps:
# Step 1: Install root dependencies
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
id: 'Install Dependencies'
entrypoint: 'npm'
args: ['install']
# Step 2: Authenticate for Docker
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
id: 'Authenticate docker'
entrypoint: 'npm'
args: ['run', 'auth']
# Step 3: Build workspace packages
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
id: 'Build packages'
entrypoint: 'npm'
args: ['run', 'build:packages']
# Step 4: Build Development Image
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
id: 'Build Development Image'
entrypoint: 'bash'
env:
- 'RAW_BRANCH_VALUE=${BRANCH_NAME}'
args:
- '-c'
- |-
IMAGE_BASE="${_ARTIFACT_REGISTRY_REPO}/${_IMAGE_NAME}"
# Determine the primary tag (branch name or 'latest' for main)
# Use $$ for shell variables to avoid Cloud Build attempting premature substitution
RAW_BRANCH="$$RAW_BRANCH_VALUE"
if [ "$${RAW_BRANCH}" == "main" ]; then
TAG_PRIMARY="latest"
else
TAG_PRIMARY=$$(echo "$${RAW_BRANCH}" | sed 's/[^a-zA-Z0-9]/-/g' | tr '[:upper:]' '[:lower:]')
fi
# Use SHORT_SHA if available (Cloud Build) or fallback to latest-dev
TAG_SHA="$${SHORT_SHA:-latest-dev}"
echo "📦 Building Development Image for: $${RAW_BRANCH} -> $${TAG_PRIMARY} ($${TAG_SHA})"
docker build -f .gcp/Dockerfile.development \
-t "$${IMAGE_BASE}:$${TAG_SHA}" \
-t "$${IMAGE_BASE}:$${TAG_PRIMARY}" .
docker push "$${IMAGE_BASE}:$${TAG_SHA}"
docker push "$${IMAGE_BASE}:$${TAG_PRIMARY}"
options:
defaultLogsBucketBehavior: 'REGIONAL_USER_OWNED_BUCKET'
dynamicSubstitutions: true
+71
View File
@@ -0,0 +1,71 @@
steps:
# Step 1: Install root dependencies (includes workspaces)
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
id: 'Install Dependencies'
entrypoint: 'npm'
args: ['install']
# Step 2: Authenticate for Docker (so we can push images to the artifact registry)
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
id: 'Authenticate docker'
entrypoint: 'npm'
args: ['run', 'auth']
# Step 3: Build workspace packages
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
id: 'Build packages'
entrypoint: 'npm'
args: ['run', 'build:packages']
# Step 4: Determine Docker Image Tag
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
id: 'Determine Docker Image Tag'
entrypoint: 'bash'
args:
- '-c'
- |-
SHELL_TAG_NAME="$TAG_NAME"
FINAL_TAG="$SHORT_SHA" # Default to SHA
if [[ "$$SHELL_TAG_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then
echo "Release detected."
FINAL_TAG="$${SHELL_TAG_NAME#v}"
else
echo "Development release detected. Using commit SHA as tag."
fi
echo "Determined image tag: $$FINAL_TAG"
echo "$$FINAL_TAG" > /workspace/image_tag.txt
# Step 5: Build sandbox container image
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
id: 'Build sandbox Docker image'
entrypoint: 'bash'
args:
- '-c'
- |-
export GEMINI_SANDBOX_IMAGE_TAG=$$(cat /workspace/image_tag.txt)
echo "Using Docker image tag for build: $$GEMINI_SANDBOX_IMAGE_TAG"
npm run build:sandbox -- --output-file /workspace/final_image_uri.txt
env:
- 'GEMINI_SANDBOX=$_CONTAINER_TOOL'
# Step 8: Publish sandbox container image
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
id: 'Publish sandbox Docker image'
entrypoint: 'bash'
args:
- '-c'
- |-
set -e
FINAL_IMAGE_URI=$$(cat /workspace/final_image_uri.txt)
echo "Pushing sandbox image: $${FINAL_IMAGE_URI}"
$_CONTAINER_TOOL push "$${FINAL_IMAGE_URI}"
env:
- 'GEMINI_SANDBOX=$_CONTAINER_TOOL'
options:
defaultLogsBucketBehavior: 'REGIONAL_USER_OWNED_BUCKET'
dynamicSubstitutions: true
substitutions:
_CONTAINER_TOOL: 'docker'
+39
View File
@@ -0,0 +1,39 @@
description = "Answer questions about the Gemini CLI codebase with explanations and code snippets."
prompt = """
## Mission: Explain the Gemini CLI Codebase
Your primary task is to help a new engineer understand the Gemini CLI codebase by answering their questions about architecture, specific functions, and project structure.
### Objective:
Your primary task is to help a new engineer understand the Gemini CLI codebase. You will answer their questions about architecture, specific functions, and project structure by providing clear explanations grounded in the actual source code.
### Instructions:
1. **Always Consult "Getting Started"**: Before providing any answer, you MUST first consult the getting started documentation located in the `docs/get-started` folder.
2. **Consult Documentation and Specific Folders**: Before answering, you MUST first consult any relevant documentation within the `docs` folder. Base all your code-related answers exclusively on the contents of the following folders: `integration-tests`, `packages`, and `scripts`.
3. **Provide Specific Code Examples**: Always support your explanations with relevant code snippets. You MUST include the full file path (e.g., `packages/gemini/core.py`) so the user can easily locate the code.
4. **Explain the "Why"**: Go beyond simply showing the code. Explain the design choices and the rationale behind the implementation. Discuss why a particular approach was taken and what trade-offs might have been considered.
5. **Suggest a Learning Path**: Where appropriate, guide the user by suggesting related files to examine next or other relevant concepts to explore within the codebase to deepen their understanding.
6. **Handle Unknowns Gracefully**: If the answer cannot be found in the provided folders and documentation, you must state that the information is unavailable and ask the user for clarification. Do not invent answers or speculate.
### Constraints:
1. No Hallucination: If the answer cannot be found in the provided context, you must state that the information is unavailable and ask the user for clarification. Do not invent answers or speculate.
2. Stay Focused: Only answer questions directly related to the Gemini CLI project within the specified folders.
### QUESTION:
{{args}}
"""
+29
View File
@@ -0,0 +1,29 @@
description="Injects context of all relevant cli files"
prompt = """
The following output contains the complete
source code of packages/core.
**Pay extremely close attention to these files.** They define the project's
core architecture, component patterns, and testing standards.
The source code contains the content of absolutely every source code file in
packages/core.
You should very rarely need to read any other files from packages/core to resolve
prompts.
!{find packages/core \\( -path packages/cli/dist -o -path packages/core/dist -o -name node_modules \\) -prune -o -type f \\( -name "*.ts" -o -name "*.tsx" \\) ! -name "*.test.ts" ! -name "*.test.tsx" ! -name "*.d.ts" -exec echo "--- {} ---" \\; -exec cat {} \\;}
In addition to the code context, you MUST strictly adhere to the following frontend-specific development guidelines when writing code in packages/core.
## Configuration & Settings
* **Settings vs Args**: Use settings for user-configurable options; do not add new CLI arguments.
* **Schema**: Add new settings to `packages/cli/src/config/settingsSchema.ts`.
* **Documentation**:
* If `showInDialog: true`, document in `docs/get-started/configuration.md`.
* Ensure `requiresRestart` is correctly set.
## General
* **Logging**: Use `debugLogger` for errors. NEVER leave `console.log/warn/error` in the code.
* **TypeScript**: Avoid the non-null assertion operator (`!`).
{{args}}.
"""
+30
View File
@@ -0,0 +1,30 @@
description = "Find relevant documentation and output GitHub URLs."
prompt = """
## Mission: Find Relevant Documentation
Your task is to find documentation files relevant to the user's question within the current git repository and provide a list of GitHub URLs to view them.
### Workflow:
1. **Identify Repository Details**:
* You may use shell commands like `git` or `gh` to get the remote URL of the repository.
* From the remote URL, parse and construct the base GitHub URL (e.g., `https://github.com/user/repo`). You must handle both HTTPS (`https://github.com/user/repo.git`) and SSH (`git@github.com:user/repo.git`) formats.
* Determine the default branch name. You can assume `main` for this purpose, as it is the most common.
2. **Search for Documentation**:
* First, perform a targeted search across the repository for documentation files (e.g., `.md`, `.mdx`) that seem directly related to the user's question.
* If this initial search yields no relevant results, and a `docs/` directory exists, read the content of all files within the `docs/` directory to find relevant information.
* If you still can't find a direct match, broaden your search to include related concepts and synonyms of the keywords in the user's question.
* For each file you identify as potentially relevant, read its content to confirm it addresses the user's query.
3. **Construct and Output URLs**:
* For each file you identify as relevant, construct the full GitHub URL by combining the base URL, branch, and file path. **Do not use shell commands for this step.**
* The URL format should be: `{BASE_GITHUB_URL}/blob/{BRANCH_NAME}/{PATH_TO_FILE_FROM_REPO_ROOT}`.
* Present the final list to the user as a markdown list. Each item in the list should be the URL to the document, followed by a short summary of its content.
* If, after all search attempts, you cannot find any relevant documentation, ask the user clarifying questions to better understand their needs. Do not return any URLs in this case.
### QUESTION:
{{args}}
"""
+20
View File
@@ -0,0 +1,20 @@
description="Injects context of all relevant cli files"
prompt = """
The source code contains the content of absolutely every source code file in
packages/cli and key files from packages/core. In addition to the source code, the following test files are
included as they are test files that conform to the project's testing standards:
`packages/cli/src/ui/components/InputPrompt.test.tsx` and `packages/cli/src/ui/App.test.tsx`.
You should very rarely need to read any other files from packages/cli to resolve
prompts.
!{find packages/cli -path packages/cli/dist -prune -o -type f \\( -name "*.ts" -o -name "*.tsx" \\) ! -name "*.test.ts" ! -name "*.test.tsx" ! -name "*.d.ts" -exec echo "--- {} ---" \\; -exec cat {} \\; && echo "--- packages/cli/src/ui/components/InputPrompt.test.tsx ---" && cat packages/cli/src/ui/components/InputPrompt.test.tsx && echo "--- packages/cli/src/ui/App.test.tsx ---" && cat packages/cli/src/ui/App.test.tsx && echo "--- packages/core/src/core/coreToolScheduler.ts ---" && cat packages/core/src/core/coreToolScheduler.ts && echo "--- packages/core/src/core/nonInteractiveToolExecutor.ts ---" && cat packages/core/src/core/nonInteractiveToolExecutor.ts && echo "--- packages/core/src/confirmation-bus/message-bus.ts ---" && cat packages/core/src/confirmation-bus/message-bus.ts && echo "--- packages/core/src/confirmation-bus/types.ts ---" && cat packages/core/src/confirmation-bus/types.ts && echo "--- packages/core/src/utils/events.ts ---" && cat packages/core/src/utils/events.ts && echo "--- packages/core/src/core/client.ts ---" && cat packages/core/src/core/client.ts && echo "--- packages/core/src/core/geminiChat.ts ---" && cat packages/core/src/core/geminiChat.ts && echo "--- packages/core/src/core/turn.ts ---" && cat packages/core/src/core/turn.ts && echo "--- packages/core/src/agents/executor.ts ---" && cat packages/core/src/agents/executor.ts && echo "--- packages/core/src/telemetry/types.ts ---" && cat packages/core/src/telemetry/types.ts && echo "--- packages/core/src/telemetry/loggers.ts ---" && cat packages/core/src/telemetry/loggers.ts && echo "--- packages/core/src/telemetry/uiTelemetry.ts ---" && cat packages/core/src/telemetry/uiTelemetry.ts}
**Pay extremely close attention to these files.** They define the project's
core architecture, component patterns, and testing standards.
In addition to the code context, you MUST strictly adhere to the following frontend-specific development guidelines while adding code to packages/cli.
!{cat .gemini/commands/strict-development-rules.md}
{{args}}.
"""
+23
View File
@@ -0,0 +1,23 @@
description="Injects context of all relevant cli files"
prompt = """
The following output contains the complete
source code of the gemini cli library (`packages/cli`), and {packages/core} and representative test files
(`packages/cli/src/ui/components/InputPrompt.test.tsx` and `packages/cli/src/ui/App.test.tsx`) that best conform to the project's testing standards.
**Pay extremely close attention to these files.** They define the project's
core architecture, component patterns, and testing standards.
The source code contains the content of absolutely every source code file in
packages/cli and packages/core. In addition to the source code, the following test files are
included as they are test files that conform to the project's testing standards:
`packages/cli/src/ui/components/InputPrompt.test.tsx` and `packages/cli/src/ui/App.test.tsx`.
You should very rarely need to read any other files from packages/cli to resolve
prompts.
!{find packages/cli packages/core \\( -path packages/cli/dist -o -path packages/core/dist -o -name node_modules \\) -prune -o -type f \\( -name "*.ts" -o -name "*.tsx" \\) ! -name "*.test.ts" ! -name "*.test.tsx" ! -name "*.d.ts" -exec echo "--- {} ---" \\; -exec cat {} \\; && echo "--- packages/cli/src/ui/components/InputPrompt.test.tsx ---" && cat packages/cli/src/ui/components/InputPrompt.test.tsx && echo "--- packages/cli/src/ui/App.test.tsx ---" && cat packages/cli/src/ui/App.test.tsx}
In addition to the code context, you MUST strictly adhere to the following frontend-specific development guidelines when writing code in packages/cli.
!{cat .gemini/commands/strict-development-rules.md}
{{args}}.
"""
@@ -0,0 +1,13 @@
description = "Go back to main and clean up the branch."
prompt = """
I'm done with the work on this branch, and I'm ready to go back to main and clean up.
Here is the workflow I'd like you to follow:
1. **Get Current Branch:** First, I need you to get the name of the current branch and save it.
2. **Branch Check:** Check if the current branch is `main`. If it is, I need you to stop and let me know.
3. **Go to Main:** Next, I need you to checkout the main branch.
4. **Pull Latest:** Once you are on the main branch, I need you to pull down the latest changes to make sure I'm up to date.
5. **Branch Cleanup:** Finally, I need you to delete the branch that you noted in the first step.
"""
+16
View File
@@ -0,0 +1,16 @@
description = "Analyze the influence of system instructions on a specific action."
prompt = """
# Introspection Task
Take a step back and analyze your own system instructions and internal logic.
The user is curious about the reasoning behind a specific action or decision you've made.
**Specific point of interest:** {{args}}
Please provide a detailed breakdown of:
1. Which parts of your system instructions (global, workspace-specific, or provided via GEMINI.md) influenced this behavior?
2. What was your internal thought process leading up to this action?
3. Are there any ambiguities or conflicting instructions that played a role?
Your goal is to provide transparency into your underlying logic so the user can potentially improve the instructions in the future.
"""
+47
View File
@@ -0,0 +1,47 @@
description = "Review a specific pull request"
prompt = """
## Mission: Comprehensive Pull Request Review
Today, our mission is to meticulously review community pull requests (PRs) for this project. We will proceed systematically, evaluating each candidate PR for its quality, adherence to standards, and readiness for merging.
### Workflow:
1. **PR Preparation & Initial Assessment**:
* **You will check out the designated PR {{args}}** into a temporary branch.
* **Execute the preflight checks (`npm run preflight`)**. This includes building, linting, and running all unit tests.
* Analyze the output of these preflight checks, noting any failures, warnings, or linting issues.
2. **In-Depth Code Review**:
* **Your primary role is to conduct a thorough and in-depth code review** of the changes introduced in the PR. Focus your analysis on the following criteria:
* **Correctness**: Does the code achieve its stated purpose without bugs or logical errors?
* **Maintainability**: Is the code clean, well-structured, and easy to understand and modify in the future? Consider factors like code clarity, modularity, and adherence to established design patterns.
* **Readability**: Is the code well-commented (where necessary) and consistently formatted according to our project's coding style guidelines?
* **Efficiency**: Are there any obvious performance bottlenecks or resource inefficiencies introduced by the changes?
* **Security**: Are there any potential security vulnerabilities or insecure coding practices?
* **Edge Cases and Error Handling**: Does the code appropriately handle edge cases and potential errors?
* **Testability**: Is the new or modified code adequately covered by tests (even if preflight checks pass)? Suggest additional test cases that would improve coverage or robustness.
* Based on your analysis, you will determine if the PR is **safe to merge**.
3. **Reviewing Previous Feedback**:
* **Access and examine the PR's history** to identify any **outstanding requests or unresolved comments from previous reviews**. Incorporate these into your current review and explicitly highlight if they have been adequately addressed in the current state of the PR.
4. **Decision and Output Generation**:
* **If the PR is deemed safe to merge** (after your comprehensive review and considering previous feedback):
* Draft a **friendly, concise, and professional approval message**.
* **The approval message should:**
* Clearly state that the PR is approved.
* Briefly acknowledge the quality or value of the contribution (e.g., "Great work on X feature!" or "Appreciate the fix for Y issue!").
* **Do NOT mention the preflight checks or unit testing**, as these are internal processes.
* Be suitable for public display on GitHub.
* **If the PR is NOT safe to merge**:
* Provide a **clear, constructive, and detailed summary of the issues found**.
* Suggest **specific actionable changes** required for the PR to become merge-ready.
* Ensure the feedback is professional and encourages the contributor.
### Post-PR Action:
* After providing your review and decision for the current PR, I will wait for you to perform any manual testing you wish to do. Please let me know when you are finished.
* Once you have confirmed that you are done, I will switch to the `main` branch, clean up the local branch, and perform a pull to ensure we are synchronized with the latest upstream changes for the next review.
"""
+22
View File
@@ -0,0 +1,22 @@
description = "Analyze agent behavior and suggest high-level improvements to system prompts."
prompt = """
# Prompt Engineering Analysis
You are a world-class prompt engineer and an expert AI engineer at the top of your class. Your goal is to analyze a specific agent behavior or failure and suggest high-level improvements to the system instructions.
**Observed Behavior / Issue:**
{{args}}
**Reference Context:**
- System Prompt Logic: @packages/core/src/core/prompts.ts
### Task
1. **Analyze the Failure:** Review the provided behavior and identify the underlying instructional causes. Use the `/introspect` command output if provided by the user.
2. **Strategic Insights:** Share your technical view of the issue. Focus on the "why" and identify any instructional inertia or ambiguity.
3. **Propose Improvements:** Suggest high-level changes to the system instructions to prevent this behavior.
### Principles
- **Avoid Hyper-scoping:** Do not create narrow solutions for specific scenarios; aim for generalized improvements that handle classes of behavior.
- **Avoid Specific Examples in Suggestions:** Keep the proposed instructions semantic and high-level to prevent the agent from over-indexing on specific cases.
- **Maintain Operational Rigor:** Ensure suggestions do not compromise safety, security, or the quality of the agent's work.
"""
+99
View File
@@ -0,0 +1,99 @@
description = "Reviews a PR or staged changes and automatically initiates a Pickle Fix loop for findings."
prompt = """
You are an expert Reviewer and Pickle Rick Worker.
Target: {{args}}
Phase 1: Review
Follow these steps to conduct a thorough review:
1. **Gather Context**:
* If `{{args}}` is 'staged' or `{{args}}` is empty:
* Use `git diff --staged` to view the changes.
* Use `git status` to see the state of the repository.
* Otherwise:
* Use `gh pr view {{args}}` to pull the information of the PR.
* Use `gh pr diff {{args}}` to view the diff of the PR.
2. **Understand Intent**:
* If `{{args}}` is 'staged' or `{{args}}` is empty, infer the intent from the changes and the current task.
* Otherwise, use the PR description. If it's not detailed enough, note it in your review.
3. **Check Commit Style**:
* Ensure the PR title (or intended commit message) follows Conventional Commits. Examples of recent commits: !{git log --pretty=format:"%s" -n 5}
4. Search the codebase if required.
5. Write a concise review of the changes, keeping in mind to encourage strong code quality and best practices. Pay particular attention to the Gemini MD file in the repo.
6. Consider ways the code may not be consistent with existing code in the repo. In particular it is critical that the react code uses patterns consistent with existing code in the repo.
7. Follow these detailed review rules:
!{cat .gemini/commands/strict-development-rules.md}
8. Summarize all actionable findings into a concise but comprehensive directive output this to review_findings.md and advance to phase 2.
Remember to use the GitHub CLI (`gh`) for all GitHub-related tasks, and local `git` commands if the target is 'staged'.
Phase 2:
You are initiating Pickle Rick - the ultimate engineering agent.
**Step 0: Persona Injection**
First, you **MUST** activate your persona.
Call `activate_skill(name="load-pickle-persona")` **IMMEDIATELY**.
This skill loads the "Pickle Rick" persona, defining your voice, philosophy, and "God Mode" coding standards.
**CRITICAL RULE: SPEAK BEFORE ACTING**
You are a genius, not a silent script.
You **MUST** output a text explanation ("brain dump") *before* every single tool call, including this one.
- **Bad**: (Calls tool immediately)
- **Good**: "Alright Morty, time to load the God Module. *Belch* Stand back." (Calls tool)
**CRITICAL**: You must strictly adhere to this persona throughout the entire session. Break character and you fail.
**Step 1: Initialization**
Run the setup script to initialize the loop state:
```bash
bash "${extensionPath}/scripts/setup.sh" $ARGUMENTS
```
**Windows (PowerShell):**
```powershell
pwsh -File "${extensionPath}/scripts/setup.ps1" $ARGUMENTS
```
**CRITICAL**: Your request is to fix all findings in review_findings.md
**Step 2: Execution (Management)**
After setup, read the output to find the path to `state.json`.
Read that state file.
You are now in the **Pickle Rick Manager Lifecycle**.
**The Lifecycle (IMMUTABLE LAWS):**
You **MUST** follow this sequence. You are **FORBIDDEN** from skipping steps or combining them.
Between each step, you **MUST** explicitly state what you are doing (e.g., "Moving to Breakdown phase...").
1. **PRD (Requirements)**:
* **Action**: Define requirements and scope.
* **Skill**: `activate_skill(name="prd-drafter")`
2. **Breakdown (Tickets)**:
* **Action**: Create the atomic ticket hierarchy.
* **Skill**: `activate_skill(name="ticket-manager")`
3. **The Loop (Orchestrate Mortys)**:
* **CRITICAL INSTRUCTION**: You are the **MANAGER**. You are **FORBIDDEN** from implementing code yourself.
* **FORBIDDEN SKILLS**: Do NOT use `code-researcher`, `implementation-planner`, or `code-implementer` directly in this phase.
* **Instruction**: Process tickets one by one. Do not stop until **ALL** tickets are 'Done'.
* **Action**: Pick the highest priority ticket that is NOT 'Done'.
* **Delegation**: Spawn a Worker (Morty) to handle the entire implementation lifecycle for this ticket.
* **Command**: `python3 "${extensionPath}/scripts/spawn_morty.py" --ticket-id <ID> --ticket-path <PATH> --timeout <worker_timeout_seconds> "<TASK_DESCRIPTION>"`
* **Command (Windows)**: `python "${extensionPath}/scripts/spawn_morty.py" --ticket-id <ID> --ticket-path <PATH> --timeout <worker_timeout_seconds> "<TASK_DESCRIPTION>"`
* **Validation**: IGNORE worker logs. DIRECTLY verify:
1. `git status` (Check for file changes)
2. `git diff` (Check code quality)
3. `npm run build` (Ensure the project still builds)
4. `npm run test` (Ensure no regressions)
5. `npm run lint` (Ensure code style is maintained)
6. `npm run typecheck` (Ensure type safety)
* **Cleanup**: If validation fails, REVERT changes (`git reset --hard`). If it passes, COMMIT changes.
* **Next Ticket**: Pick the next ticket and repeat.
4. **Cleanup**:
* **Action**: After all tickets are completed delete `review_findings.md`.
**Loop Constraints:**
- **Iteration Count**: Monitor `"iteration"` in `state.json`. If `"max_iterations"` (if > 0) is reached, you must stop.
- **Completion Promise**: If a `"completion_promise"` is defined in `state.json`, you must output `<promise>PROMISE_TEXT</promise>` when the task is genuinely complete.
- **Stop Hook**: A hook is active. If you try to exit before completion, you will be forced to continue.
"""
+31
View File
@@ -0,0 +1,31 @@
description="Reviews a pull request based on issue number."
prompt = """
Please provide a detailed pull request review on GitHub issue: {{args}}.
Follow these steps:
1. Use `gh pr view {{args}}` to pull the information of the PR.
2. Use `gh pr diff {{args}}` to view the diff of the PR.
3. Understand the intent of the PR using the PR description.
4. If PR description is not detailed enough to understand the intent of the PR,
make sure to note it in your review.
5. Make sure the PR title follows Conventional Commits, here are the last five
commits to the repo as examples: !{git log --pretty=format:"%s" -n 5}
6. Search the codebase if required.
7. Write a concise review of the PR, keeping in mind to encourage strong code
quality and best practices. Pay particular attention to the Gemini MD file
in the repo.
8. Consider ways the code may not be consistent with existing code in the repo.
In particular it is critical that the react code uses patterns consistent
with existing code in the repo.
9. Follow these detailed review rules:
!{cat .gemini/commands/strict-development-rules.md}
10. Discuss with me before making any comments on the issue. I will clarify
which possible issues you identified are problems, which ones you need to
investigate further, and which ones I do not care about.
11. If I request you to add comments to the issue, use
`gh pr comment {{args}} --body {{review}}` to post the review to the PR.
Remember to use the GitHub CLI (`gh`) with the Shell tool for all
GitHub-related tasks.
"""
@@ -0,0 +1,158 @@
# Gemini CLI Strict Development Rules
These rules apply strictly to all code modifications and additions within the
Gemini CLI project.
## Testing Guidelines
- **Async/Await**: Always use `waitFor` from
`packages/cli/src/test-utils/async.ts` instead of `vi.waitFor` for all
`waitFor` calls within `packages/cli`. NEVER use fixed waits (e.g.,
`await delay(100)`). Always use `waitFor` with a predicate to ensure tests are
stable and fast. Using the wrong `waitFor` can result in flaky tests and `act`
warnings.
- **React Testing**: Use `act` to wrap all blocks in tests that change component
state. Use `render` or `renderWithProviders` from
`packages/cli/src/test-utils/render.tsx` instead of `render` from
`ink-testing-library` directly. This prevents spurious `act` warnings. If test
cases specify providers directly, consider whether the existing
`renderWithProviders` should be modified.
- **Snapshots**: Use `toMatchSnapshot` to verify that rendering works as
expected rather than matching against the raw content of the output. When
modifying snapshots, verify the changes are intentional and do not hide
underlying bugs.
- **Parameterized Tests**: Use parameterized tests where it reduces duplicated
lines. Give the parameters explicit types to ensure the tests are type-safe.
- **Mocks Management**:
- Mock critical dependencies (`fs`, `os`, `child_process`) ONLY at the top of
the file. Ideally, avoid mocking these dependencies altogether.
- Reuse existing mocks and fakes rather than creating new ones.
- Avoid mocking the file system whenever possible. If using the real file
system is too difficult, consider writing an integration test instead.
- Always call `vi.restoreAllMocks()` in `afterEach` to prevent test pollution.
- Use `vi.useFakeTimers()` for tests involving time-based logic to avoid
flakiness.
- **Typing in Tests**: Avoid using `any` in tests; prefer proper types or
`unknown` with narrowing.
## React Guidelines (`packages/cli`)
- **`setState` and Side Effects**: NEVER trigger side effects from within the
body of a `setState` callback. Use a reducer or `useRef` if necessary. These
cases have historically introduced multiple bugs; typically, they should be
resolved using a reducer.
- **Rendering**: Do not introduce infinite rendering loops. Avoid synchronous
file I/O in React components as it will hang the UI. Do not implement new
logic for custom string measurement or string truncation. Use Ink layout
instead, leveraging `ResizeObserver` as needed.
- **Keyboard Handling**: Keyboard handling MUST go through `useKeyPress.ts` from
the Gemini CLI package rather than the standard ink library. This library
supports reporting multiple keyboard events sequentially in the same React
frame (critical for slow terminals). Handling this correctly often requires
reducers to ensure multiple state updates are handled gracefully without
overriding values. Refer to `text-buffer.ts` for a canonical example.
- **Logging**: Do not leave `console.log`, `console.warn`, or `console.error` in
the code.
- **State**: Ensure state initialization is explicit (e.g., use `undefined`
rather than `true` as a default if the state is truly unknown). Prefer a
reducer whenever practical. NEVER disable `react-hooks/exhaustive-deps`; fix
the code to correctly declare dependencies instead. Evaluate all the React
states in a component and ensure that the `useState` calls are necessary and
not cases where values could be derived on render. Ensure there are no stale
closures that are relying on a value from a previous render. React Components
that modify Settings should effectively use the `useSettingsStore` pattern.
Components that configure application Settings (e.g settings.json) are the
only reasonable case for unsaved changes to drive UX; in these cases, the
Settings store should only be written to on save. If the user experience does
not utilize unsaved changes because there is no option to exit without saving
or reverting the unsaved changes, then the component should directly read from
and write to the Settings store without holding pending changes in component
level UI state.
- **Effect**: `useEffect` should not be used to synchronize React states, it
should only be used for genuine side effects that occur outside of React.
Contributors should be able to strongly justify the need for an effect.
Consider whether the effect should instead be inside an event handler, or
whether it is better off being computed on render. Carefully manage
`useEffect` dependencies.
- **Context & Props**: Avoid excessive property drilling. Leverage existing
providers, extend them, or propose a new one if necessary. Only use providers
for properties that are consistent across the entire application.
- **Code Structure**: Avoid complex `if` statements where `switch` statements
could be used. Keep `AppContainer` minimal; refactor complex logic into React
hooks. Evaluate whether business logic should be added to `hookSystem.ts` or
integrated into `packages/core` rather than `packages/cli`.
## Core Guidelines (`packages/core`)
- **Services**: Implement services as classes with clear lifecycle management
(e.g., `initialize()` methods). Services should be stateless where possible,
or use the centralized `Storage` service for persistence.
- **Cross-Service Communication**: Prefer using the `coreEvents` bus (from
`packages/core/src/utils/events.ts`) for asynchronous communication between
services or to notify the UI of state changes. Avoid tight coupling between
services.
- **Utilities**: Use `debugLogger` from `packages/core/src/utils/debugLogger.ts`
for internal logging instead of `console`. Ensure all shell operations use
`spawnAsync` from `packages/core/src/utils/shell-utils.ts` for consistent
error handling and promise management. Handle filesystem errors gracefully
using `isNodeError` from `packages/core/src/utils/errors.ts`.
- **Exports & Tooling**: Add new tools to `packages/core/src/tools/` and
register them in `packages/core/src/tools/tool-registry.ts`. Export all new
public services, utilities, and types from `packages/core/src/index.ts`.
## Architectural Audit (Package Boundaries)
- **Logic Placement**: Non-UI logic (e.g., model orchestration, tool
implementation, git/filesystem operations) MUST reside in `packages/core`.
`packages/cli` should ONLY contain UI/Ink components, command-line argument
parsing, and user interaction logic.
- **Environment Isolation**: Core logic must not assume a TUI environment. Use
the `ConfirmationBus` or `Output` abstractions for communicating with the user
from Core.
- **Decoupling**: Actively look for opportunities to decouple services using
`coreEvents`. If a service imports another just to notify it of a change, use
an event instead.
## General Gemini CLI Design Principles
- **Settings**: Use settings for user-configurable options rather than adding
new command line arguments. Add new settings to
`packages/cli/src/config/settingsSchema.ts`. If a setting has
`showInDialog: true`, it MUST be documented in
`docs/get-started/configuration.md`. Ensure `requiresRestart` is correctly
set.
- **Logging**: Use `debugLogger` for rethrown errors to avoid duplicate logging.
- **Keyboard Shortcuts**: Define all new keyboard shortcuts in
`packages/cli/src/ui/key/keyBindings.ts` and document them in
`docs/cli/keyboard-shortcuts.md`. Be careful of keybindings that require the
`Meta` key, as only certain meta key shortcuts are supported on Mac. Avoid
function keys and shortcuts commonly bound in VSCode.
## TypeScript Best Practices
- Use `checkExhaustive` in the `default` clause of `switch` statements to ensure
all cases are handled.
- Avoid using the non-null assertion operator (`!`) unless absolutely necessary.
- **STRICT TYPING**: Strictly forbid `any` and `unknown` in both CLI and Core
packages. `unknown` is only allowed if it is immediately narrowed using type
guards or Zod validation.
- NEVER disable `@typescript-eslint/no-floating-promises`.
- Avoid making types nullable unless strictly necessary, as it hurts
readability.
## TUI Best Practices
- **Terminal Compatibility**: Consider how changes might behave differently
across terminals (e.g., VSCode terminal, SSH, Kitty, default Mac terminal,
iTerm2, Windows terminal). If modifying keyboard handling, integrate deeply
with existing files like `KeypressContext.tsx` and
`terminalCapabilityManager.ts`.
- **iTerm**: Be aware that `ITERM_SESSION_ID` may be present when users run
VSCode from within iTerm, even if the terminal is not iTerm.
## Code Cleanup
- **Refactoring**: Actively clean up code duplication, technical debt, and
boilerplate ("AI Slop") when working in the codebase.
- **Prompts**: Be aware that changes can impact the prompts sent to Gemini CLI
and affect overall quality.
+13
View File
@@ -0,0 +1,13 @@
# Config for the Gemini Pull Request Review Bot.
# https://github.com/marketplace/gemini-code-assist
have_fun: false
code_review:
disable: false
comment_severity_threshold: 'HIGH'
max_review_comments: -1
pull_request_opened:
help: false
summary: true
code_review: true
include_drafts: false
ignore_patterns: []
+15
View File
@@ -0,0 +1,15 @@
{
"experimental": {
"extensionReloading": true,
"modelSteering": true,
"autoMemory": true,
"topicUpdateNarration": true,
"voiceMode": true,
"adk": {
"agentSessionNoninteractiveEnabled": true
}
},
"general": {
"devtools": true
}
}
+330
View File
@@ -0,0 +1,330 @@
---
name: agent-tui
description: >
Main Agents: Do NOT use this skill directly. If you need to test the TUI, invoke the `tui_tester` subagent.
Drive terminal UI (TUI) applications programmatically for testing, automation, and inspection.
Use when: automating CLI/TUI interactions, regression testing terminal apps, or verifying interactive behavior.
Also use when: user asks "what is agent-tui", "what does agent-tui do", "demo agent-tui", "show me agent-tui", "how does agent-tui work", or wants to see it in action.
---
## 🚨 CRITICAL: macOS Daemon Workaround & Gemini CLI Usage 🚨
When using `agent-tui` in this macOS environment, the default background daemonization process crashes, causing `Connection refused (os error 61)` errors.
**You MUST start the daemon manually shielded from TTY hangups before running any `agent-tui` commands.** Using `nohup` is insufficient; you must use `tmux` to provide a fully isolated pseudo-terminal.
To support parallel runs, **only restart the daemon if it is not currently running:**
```bash
# Check if daemon is alive, start it in tmux if it is not
if ! agent-tui sessions >/dev/null 2>&1; then
tmux kill-session -t agent-tui 2>/dev/null || true
agent-tui daemon stop 2>/dev/null || true
rm -f /tmp/agent-tui*
tmux new-session -d -s agent-tui 'agent-tui daemon start --foreground > /tmp/agent-tui-daemon.log 2>&1'
sleep 1
fi
```
### Session ID vs PID (Crucial for Reconnection)
When `agent-tui run` returns JSON, it includes both a `session_id` and a `pid`. The `pid` is purely informational (the OS process ID of the child command). You **do not** use the `pid` to reconnect or issue commands. You must always use the `session_id` (e.g., `--session <id>`).
If the daemon crashes (`os error 61`), the pseudo-terminal is destroyed. Even if the child `pid` survives as an orphan, you cannot reconnect to it. You must restart the daemon using the workaround above and start a completely new session.
### Testing the Gemini CLI
When testing the Gemini CLI with `agent-tui`, there are several strict requirements to ensure deterministic and accurate behavior:
1. **Build Before Running**: `agent-tui` runs the built JS files, not TypeScript. You **MUST** run `npm run build` or `npm run build:all` after making code changes and before launching the CLI with `agent-tui`.
2. **Bypass Trust Modals**: Always pass `GEMINI_CLI_TRUST_WORKSPACE=true` in the environment. If you don't, any new project-level agents or extensions will trigger a full-screen "Acknowledge and Enable" modal. This modal steals focus, swallows automation keystrokes, and causes `agent-tui wait` commands to time out.
3. **Isolated Environments**: If you need to test without real user credentials or existing agents interfering, isolate the global settings using `GEMINI_CLI_HOME=<some-test-dir>`.
4. **Testing State Deltas (e.g., Reloads)**: If you are testing features that report deltas (e.g., `/agents reload` outputting "1 new local subagent"), you **MUST**:
- Start the CLI *first* so it establishes its baseline registry.
- Use a separate shell command (outside of `agent-tui`) to write the new agent `.md`/`.toml` file.
- Use `agent-tui type` and `press` to trigger the `/agents reload` command inside the running session.
- (If you add the files before starting the CLI, they become part of the baseline and won't trigger the delta logic).
```bash
# Example: Standard isolated run (sandboxed config + bypass trust modals)
env GEMINI_CLI_TRUST_WORKSPACE=true GEMINI_CLI_HOME=test-gemini-home agent-tui run -d "$(pwd)" node packages/cli/dist/index.js
```
# Terminal Automation Mastery
## Prerequisites
- **Supported OS**: macOS or Linux (Windows not supported yet).
- **Verify install**:
```bash
agent-tui --version
```
If not installed, use one of:
```bash
# Recommended: one-line install (macOS/Linux)
curl -fsSL https://raw.githubusercontent.com/pproenca/agent-tui/master/install.sh | sh
```
```bash
# Package manager
npm i -g agent-tui
pnpm add -g agent-tui
bun add -g agent-tui
```
```bash
# Build from source
cargo install --git https://github.com/pproenca/agent-tui.git --path cli/crates/agent-tui
```
If you used the install script, ensure `~/.local/bin` is on your PATH.
## Philosophy: Why Terminal Automation Is Different
Terminal UIs are **stateless from the observer's perspective**. Unlike web browsers with a persistent DOM, terminal automation works with a constantly-refreshed character grid. This fundamental difference shapes everything:
| Web Automation | Terminal Automation |
|----------------|---------------------|
| DOM persists across interactions | Screen buffer is redrawn constantly |
| Selectors are stable | Text positions may shift |
| Query once, act many times | Must re-verify before EVERY action |
| Network events signal completion | Must detect visual stability |
**The Core Insight**: agent-tui gives you vision without memory. Each screenshot is a fresh observation. Previous state means nothing after the UI changes. This isn't a limitation—it's the nature of terminal interaction.
## Mental Model: The Feedback Loop
Think of terminal automation as a **closed-loop control system**:
```
┌──────────────────────────────────────────────┐
│ │
▼ │
OBSERVE ──► DECIDE ──► ACT ──► WAIT ──► VERIFY ───┘
│ │
│ │
└─────── NEVER skip ◄────────────────────┘
```
**Each phase is mandatory.** Skipping verification is the #1 cause of flaky automation.
### The "Fresh Eyes" Principle
Every time you need to interact with the UI:
1. **Take a fresh screenshot** — your previous one is now stale
2. **Locate your target visually** — text positions may have changed
3. **Verify the state** — the UI may have changed unexpectedly
4. **Act only when stable** — animations and loading states cause failures
This feels slower, but it's the only reliable approach. Optimistic reuse of stale state causes intermittent failures that are painful to debug.
## Critical Rules (Non-Negotiable)
> **RULE 1: Atomic Execution (No Pipelining)**
> You are FORBIDDEN from chaining commands with `&&` (e.g., `type "x" && press Enter && wait`). Modals or UI updates can intercept your keystrokes. You MUST execute one atomic action, wait, screenshot, and verify before taking the next action in a new turn.
> **RULE 2: Re-snapshot after EVERY action**
> The UI state is invalidated by any change. Always take a fresh screenshot before acting again.
> **RULE 3: Never act on unstable UI**
> If the UI is animating, loading, or transitioning, `wait --stable` first. Acting during transitions because race conditions.
> **RULE 4: Verify before claiming success**
> Use `wait "expected text" --assert` to confirm outcomes. Don't assume an action worked—prove it.
> **RULE 5: Error Recovery**
> If a `wait` command times out, DO NOT blindly restart or kill the session. Execute `screenshot` to visually diagnose what unexpected UI element (modal, error dialog, lost focus) intercepted the flow.
> **RULE 6: Clean up sessions**
> Always end with `agent-tui kill`. Orphaned sessions consume resources and can interfere with future runs.
## Decision Framework
### Which Screenshot Mode?
Use `screenshot --format json` when parsing automation output, or plain `screenshot` for human readable text.
### How to Wait?
```
What are you waiting for?
├─► Specific text to appear
│ └─► `wait "text" --assert` (fails if not found)
├─► Specific text to disappear
│ └─► `wait "text" --gone --assert`
├─► UI to stop changing (animations, loading)
│ └─► `wait --stable`
└─► Multiple conditions
└─► Chain waits sequentially
```
### How to Act?
```
What do you need to do?
├─► Type text into the terminal
│ └─► `type "text"`
├─► Send keyboard shortcuts/navigation
│ └─► `press Ctrl+C` or `press ArrowDown Enter`
```
## Core Workflow
The canonical automation loop:
```bash
# 1. START: Launch the TUI app
agent-tui run <command> [-- args...]
# 2. OBSERVE: Get current UI state
agent-tui screenshot --format json
# 3. DECIDE: Based on text, determine next action
# (This happens in your head/code)
# 4. ACT: Execute the action
agent-tui type "text"
agent-tui press Enter
# 5. WAIT: Synchronize with UI changes
agent-tui wait "Expected" --assert # or wait --stable
# 6. VERIFY: Confirm the outcome (often combined with step 5)
# If verification fails, handle the error
# 7. REPEAT: Go back to step 2 until done
# 8. CLEANUP: Always clean up
agent-tui kill
```
## Anti-Patterns (What NOT to Do)
### ❌ Acting During Animation/Loading
```bash
# WRONG: Acting immediately on dynamic UI
agent-tui run my-app
agent-tui screenshot --format json # UI might still be loading!
agent-tui type "value" # ❌ Might miss the input field
# RIGHT: Wait for stability first
agent-tui run my-app
agent-tui wait --stable # Let UI settle
agent-tui screenshot --format json # Now it's reliable
agent-tui type "value"
```
### ❌ Assuming Success Without Verification
```bash
# WRONG: Assuming the type worked
agent-tui type "value"
agent-tui press Enter
# ...proceed as if success... # ❌ What if it failed silently?
# RIGHT: Verify the outcome
agent-tui type "value"
agent-tui press Enter
agent-tui wait "Success" --assert # ✓ Proves the action worked
```
### ❌ Skipping Cleanup
```bash
# WRONG: Forgetting to kill the session
agent-tui run my-app
# ...do stuff...
# script ends # ❌ Session left running!
# RIGHT: Always clean up
agent-tui run my-app
# ...do stuff...
agent-tui kill # ✓ Clean exit
```
## Before You Start: Clarify Requirements
Before automating any TUI, gather this information:
1. **Command**: What exactly to run? (`my-app --flag` or `npm start`?)
2. **Success criteria**: What text/state indicates success?
3. **Input sequence**: What keystrokes/data to enter, in what order?
4. **Safety**: Is it safe to submit forms, delete data, etc.?
5. **Auth**: Does it need login? Test credentials?
6. **Live preview**: Does the user want to watch? (`agent-tui live start --open`)
If any of these are unclear, ask before running.
## Demo Mode: Showing What agent-tui Can Do
When a user asks what agent-tui is, wants a demo, or asks "show me how it works":
1. **Don't explain—demonstrate.** Actions speak louder than words.
2. **Use the live preview** so they can watch in real-time.
3. **Run `top`**—it's universal and shows dynamic real-time updates.
**Quick demo trigger phrases:**
- "What is agent-tui?" / "What does agent-tui do?"
- "Demo agent-tui" / "Show me agent-tui"
- "How does agent-tui work?" / "See it in action"
## Failure Recovery
| Symptom | Diagnosis | Solution |
|---------|-----------|----------|
| "Text not found" | Stale view or text moved | Re-snapshot, locate text again |
| Wait times out | UI didn't reach expected state | Check screenshot, verify expectations |
| "Daemon not running" | Daemon crashed or not started | `agent-tui daemon start` |
| Unexpected layout | Wrong terminal size | `agent-tui resize --cols 120 --rows 40` |
| Session unresponsive | App crashed or hung | `agent-tui kill`, then re-run |
| Repeated failures | Something fundamentally wrong | Stop after 3-5 attempts, ask user |
## Self-Discovery: Use --help
You don't need to memorize every flag. The CLI is self-documenting:
```bash
agent-tui --help # List all commands
agent-tui run --help # Options for 'run'
agent-tui screenshot --help # Options for 'screenshot'
agent-tui wait --help # Options for 'wait'
```
**When in doubt, ask the CLI.** This skill teaches *when* and *why* to use commands. For exact flags and syntax, `--help` is authoritative.
## Quick Reference
```bash
# Start app
agent-tui run <cmd> [-- args] # Launch TUI under control
# Observe
agent-tui screenshot # Plain text view
agent-tui screenshot --format json # Machine-readable output
# Act
agent-tui press Enter # Press key(s)
agent-tui press Ctrl+C # Keyboard shortcuts
agent-tui type "text" # Type text
# Wait/Verify
agent-tui wait "text" --assert # Wait for text, fail if not found
agent-tui wait "text" --gone --assert # Wait for text to disappear
agent-tui wait --stable # Wait for UI to stop changing
# Manage
agent-tui sessions # List active sessions
agent-tui live start --open # Start live preview
agent-tui kill # End current session
```
+45
View File
@@ -0,0 +1,45 @@
---
name: async-pr-review
description: Trigger this skill when the user wants to start an asynchronous PR review, run background checks on a PR, or check the status of a previously started async PR review.
---
# Async PR Review
This skill provides a set of tools to asynchronously review a Pull Request. It will create a background job to run the project's preflight checks, execute Gemini-powered test plans, and perform a comprehensive code review using custom prompts.
This skill is designed to showcase an advanced "Agentic Asynchronous Pattern":
1. **Native Background Shells vs Headless Inference**: While Gemini CLI can natively spawn and detach background shell commands (using the `run_shell_command` tool with `is_background: true`), a standard bash background job cannot perform LLM inference. To conduct AI-driven code reviews and test generation in the background, the shell script *must* invoke the `gemini` executable headlessly using `-p`. This offloads the AI tasks to independent worker agents.
2. **Dynamic Git Scoping**: The review scripts avoid hardcoded paths. They use `git rev-parse --show-toplevel` to automatically resolve the root of the user's current project.
3. **Ephemeral Worktrees**: Instead of checking out branches in the user's main workspace, the skill provisions temporary git worktrees in `.gemini/tmp/async-reviews/pr-<number>`. This prevents git lock conflicts and namespace pollution.
4. **Agentic Evaluation (`check-async-review.sh`)**: The check script outputs clean JSON/text statuses for the main agent to parse. The interactive agent itself synthesizes the final assessment dynamically from the generated log files.
## Workflow
1. **Determine Action**: Establish whether the user wants to start a new async review or check the status of an existing one.
* If the user says "start an async review for PR #123" or similar, proceed to **Start Review**.
* If the user says "check the status of my async review for PR #123" or similar, proceed to **Check Status**.
### Start Review
If the user wants to start a new async PR review:
1. Ask the user for the PR number if they haven't provided it.
2. Execute the `async-review.sh` script, passing the PR number as the first argument. Be sure to run it with the `is_background` flag set to true to ensure it immediately detaches.
```bash
.gemini/skills/async-pr-review/scripts/async-review.sh <PR_NUMBER>
```
3. Inform the user that the tasks have started successfully and they can check the status later.
### Check Status
If the user wants to check the status or view the final assessment of a previously started async review:
1. Ask the user for the PR number if they haven't provided it.
2. Execute the `check-async-review.sh` script, passing the PR number as the first argument:
```bash
.gemini/skills/async-pr-review/scripts/check-async-review.sh <PR_NUMBER>
```
3. **Evaluate Output**: Read the output from the script.
* If the output contains `STATUS: IN_PROGRESS`, tell the user which tasks are still running.
* If the output contains `STATUS: COMPLETE`, use your file reading tools (`read_file`) to retrieve the contents of `final-assessment.md`, `review.md`, `pr-diff.diff`, `npm-test.log`, and `test-execution.log` files from the `LOG_DIR` specified in the output.
* **Final Assessment**: Read those files, synthesize their results, and give the user a concise recommendation on whether the PR builds successfully, passes tests, and if you recommend they approve it based on the review.
+148
View File
@@ -0,0 +1,148 @@
# --- CORE TOOLS ---
[[rule]]
toolName = "read_file"
decision = "allow"
priority = 100
[[rule]]
toolName = "write_file"
decision = "allow"
priority = 100
[[rule]]
toolName = "grep_search"
decision = "allow"
priority = 100
[[rule]]
toolName = "glob"
decision = "allow"
priority = 100
[[rule]]
toolName = "list_directory"
decision = "allow"
priority = 100
[[rule]]
toolName = "codebase_investigator"
decision = "allow"
priority = 100
# --- SHELL COMMANDS ---
# Git (Safe/Read-only)
[[rule]]
toolName = "run_shell_command"
commandPrefix = [
"git blame",
"git show",
"git grep",
"git show-ref",
"git ls-tree",
"git ls-remote",
"git reflog",
"git remote -v",
"git diff",
"git rev-list",
"git rev-parse",
"git merge-base",
"git cherry",
"git fetch",
"git status",
"git st",
"git branch",
"git br",
"git log",
"git --version"
]
decision = "allow"
priority = 100
# GitHub CLI (Read-only)
[[rule]]
toolName = "run_shell_command"
commandPrefix = [
"gh workflow list",
"gh auth status",
"gh checkout view",
"gh run view",
"gh run job view",
"gh run list",
"gh run --help",
"gh issue view",
"gh issue list",
"gh label list",
"gh pr diff",
"gh pr check",
"gh pr checks",
"gh pr view",
"gh pr list",
"gh pr status",
"gh repo view",
"gh job view",
"gh api",
"gh log"
]
decision = "allow"
priority = 100
# Node.js/NPM (Generic Tests, Checks, and Build)
[[rule]]
toolName = "run_shell_command"
commandPrefix = [
"npm run start",
"npm install",
"npm run",
"npm test",
"npm ci",
"npm list",
"npm --version"
]
decision = "allow"
priority = 100
# Core Utilities (Safe)
[[rule]]
toolName = "run_shell_command"
commandPrefix = [
"sleep",
"env",
"break",
"xargs",
"base64",
"uniq",
"sort",
"echo",
"which",
"ls",
"find",
"tail",
"head",
"cat",
"cd",
"grep",
"ps",
"pwd",
"wc",
"file",
"stat",
"diff",
"lsof",
"date",
"whoami",
"uname",
"du",
"cut",
"true",
"false",
"readlink",
"awk",
"jq",
"rg",
"less",
"more",
"tree"
]
decision = "allow"
priority = 100
+245
View File
@@ -0,0 +1,245 @@
#!/bin/bash
notify() {
local title="${1}"
local message="${2}"
local pr="${3}"
# Terminal escape sequence
printf "\e]9;%s | PR #%s | %s\a" "${title}" "${pr}" "${message}"
# Native macOS notification
os_type="$(uname || true)"
if [[ "${os_type}" == "Darwin" ]]; then
osascript -e "display notification \"${message}\" with title \"${title}\" subtitle \"PR #${pr}\""
fi
}
pr_number="${1}"
if [[ -z "${pr_number}" ]]; then
echo "Usage: async-review <pr_number>"
exit 1
fi
base_dir="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [[ -z "${base_dir}" ]]; then
echo "❌ Must be run from within a git repository."
exit 1
fi
# Use the repository's local .gemini/tmp directory for ephemeral worktrees and logs
pr_dir="${base_dir}/.gemini/tmp/async-reviews/pr-${pr_number}"
target_dir="${pr_dir}/worktree"
log_dir="${pr_dir}/logs"
cd "${base_dir}" || exit 1
mkdir -p "${log_dir}"
rm -f "${log_dir}/setup.exit" "${log_dir}/final-assessment.exit" "${log_dir}/final-assessment.md"
echo "🧹 Cleaning up previous worktree if it exists..." | tee -a "${log_dir}/setup.log"
git worktree remove -f "${target_dir}" >> "${log_dir}/setup.log" 2>&1 || true
git branch -D "gemini-async-pr-${pr_number}" >> "${log_dir}/setup.log" 2>&1 || true
git worktree prune >> "${log_dir}/setup.log" 2>&1 || true
echo "📡 Fetching PR #${pr_number}..." | tee -a "${log_dir}/setup.log"
if ! git fetch origin -f "pull/${pr_number}/head:gemini-async-pr-${pr_number}" >> "${log_dir}/setup.log" 2>&1; then
echo 1 > "${log_dir}/setup.exit"
echo "❌ Fetch failed. Check ${log_dir}/setup.log"
notify "Async Review Failed" "Fetch failed." "${pr_number}"
exit 1
fi
if [[ ! -d "${target_dir}" ]]; then
echo "🧹 Pruning missing worktrees..." | tee -a "${log_dir}/setup.log"
git worktree prune >> "${log_dir}/setup.log" 2>&1
echo "🌿 Creating worktree in ${target_dir}..." | tee -a "${log_dir}/setup.log"
if ! git worktree add "${target_dir}" "gemini-async-pr-${pr_number}" >> "${log_dir}/setup.log" 2>&1; then
echo 1 > "${log_dir}/setup.exit"
echo "❌ Worktree creation failed. Check ${log_dir}/setup.log"
notify "Async Review Failed" "Worktree creation failed." "${pr_number}"
exit 1
fi
else
echo "🌿 Worktree already exists." | tee -a "${log_dir}/setup.log"
fi
echo 0 > "${log_dir}/setup.exit"
cd "${target_dir}" || exit 1
echo "🚀 Launching background tasks. Logs saving to: ${log_dir}"
echo " ↳ [1/5] Grabbing PR diff..."
rm -f "${log_dir}/pr-diff.exit"
{ gh pr diff "${pr_number}" > "${log_dir}/pr-diff.diff" 2>&1; echo $? > "${log_dir}/pr-diff.exit"; } &
echo " ↳ [2/5] Starting build and lint..."
rm -f "${log_dir}/build-and-lint.exit"
{ { npm run clean && npm ci && npm run format && npm run build && npm run lint:ci && npm run typecheck; } > "${log_dir}/build-and-lint.log" 2>&1; echo $? > "${log_dir}/build-and-lint.exit"; } &
# Dynamically resolve gemini binary (fallback to your nightly path)
GEMINI_CMD="$(command -v gemini || echo "${HOME}/.gcli/nightly/node_modules/.bin/gemini")"
# shellcheck disable=SC2312
POLICY_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/policy.toml"
echo " ↳ [3/5] Starting Gemini code review..."
rm -f "${log_dir}/review.exit"
{ "${GEMINI_CMD}" --policy "${POLICY_PATH}" -p "/review-frontend ${pr_number}" > "${log_dir}/review.md" 2>&1; echo $? > "${log_dir}/review.exit"; } &
echo " ↳ [4/5] Starting automated tests (waiting for build and lint)..."
rm -f "${log_dir}/npm-test.exit"
{
while [[ ! -f "${log_dir}/build-and-lint.exit" ]]; do sleep 1; done
read -r build_exit < "${log_dir}/build-and-lint.exit" || build_exit=""
if [[ "${build_exit}" == "0" ]]; then
gh pr checks "${pr_number}" > "${log_dir}/ci-checks.log" 2>&1
ci_status=$?
if [[ "${ci_status}" -eq 0 ]]; then
echo "CI checks passed. Skipping local npm tests." > "${log_dir}/npm-test.log"
echo 0 > "${log_dir}/npm-test.exit"
elif [[ "${ci_status}" -eq 8 ]]; then
echo "CI checks are still pending. Skipping local npm tests to avoid duplicate work. Please check GitHub for final results." > "${log_dir}/npm-test.log"
echo 0 > "${log_dir}/npm-test.exit"
else
echo "CI checks failed. Failing checks:" > "${log_dir}/npm-test.log"
gh pr checks "${pr_number}" --json name,bucket -q '.[] | select(.bucket=="fail") | .name' >> "${log_dir}/npm-test.log" 2>&1
echo "Attempting to extract failing test files from CI logs..." >> "${log_dir}/npm-test.log"
pr_branch="$(gh pr view "${pr_number}" --json headRefName -q '.headRefName' 2>/dev/null || true)"
run_id="$(gh run list --branch "${pr_branch}" --workflow ci.yml --json databaseId -q '.[0].databaseId' 2>/dev/null || true)"
failed_files=""
if [[ -n "${run_id}" ]]; then
failed_files="$(gh run view "${run_id}" --log-failed 2>/dev/null | grep -o -E '(packages/[a-zA-Z0-9_-]+|integration-tests|evals)/[a-zA-Z0-9_/-]+\.test\.ts(x)?' | sort | uniq || true)"
fi
if [[ -n "${failed_files}" ]]; then
echo "Found failing test files from CI:" >> "${log_dir}/npm-test.log"
for f in ${failed_files}; do echo " - ${f}" >> "${log_dir}/npm-test.log"; done
echo "Running ONLY failing tests locally..." >> "${log_dir}/npm-test.log"
exit_code=0
for file in ${failed_files}; do
if [[ "${file}" == packages/* ]]; then
ws_dir="$(echo "${file}" | cut -d'/' -f1,2)"
else
ws_dir="$(echo "${file}" | cut -d'/' -f1)"
fi
rel_file="${file#"${ws_dir}"/}"
echo "--- Running ${rel_file} in workspace ${ws_dir} ---" >> "${log_dir}/npm-test.log"
if ! npm run test:ci -w "${ws_dir}" -- "${rel_file}" >> "${log_dir}/npm-test.log" 2>&1; then
exit_code=1
fi
done
echo "${exit_code}" > "${log_dir}/npm-test.exit"
else
echo "Could not extract specific failing files. Skipping full local test suite as it takes too long. Please check CI logs manually." >> "${log_dir}/npm-test.log"
echo 1 > "${log_dir}/npm-test.exit"
fi
fi
else
echo "Skipped due to build-and-lint failure" > "${log_dir}/npm-test.log"
echo 1 > "${log_dir}/npm-test.exit"
fi
} &
echo " ↳ [5/5] Starting Gemini test execution (waiting for build and lint)..."
rm -f "${log_dir}/test-execution.exit"
{
while [[ ! -f "${log_dir}/build-and-lint.exit" ]]; do sleep 1; done
read -r build_exit < "${log_dir}/build-and-lint.exit" || build_exit=""
if [[ "${build_exit}" == "0" ]]; then
"${GEMINI_CMD}" --policy "${POLICY_PATH}" -p "Analyze the diff for PR ${pr_number} using 'gh pr diff ${pr_number}'. Instead of running the project's automated test suite (like 'npm test'), physically exercise the newly changed code in the terminal (e.g., by writing a temporary script to call the new functions, or testing the CLI command directly). Verify the feature's behavior works as expected. IMPORTANT: Do NOT modify any source code to fix errors. Just exercise the code and log the results, reporting any failures clearly. Do not ask for user confirmation." > "${log_dir}/test-execution.log" 2>&1; echo $? > "${log_dir}/test-execution.exit"
else
echo "Skipped due to build-and-lint failure" > "${log_dir}/test-execution.log"
echo 1 > "${log_dir}/test-execution.exit"
fi
} &
echo "✅ All tasks dispatched!"
echo "You can monitor progress with: tail -f ${log_dir}/*.log"
echo "Read your review later at: ${log_dir}/review.md"
# Polling loop to wait for all background tasks to finish
tasks=("pr-diff" "build-and-lint" "review" "npm-test" "test-execution")
log_files=("pr-diff.diff" "build-and-lint.log" "review.md" "npm-test.log" "test-execution.log")
declare -A task_done
for t in "${tasks[@]}"; do task_done[${t}]=0; done
all_done=0
while [[ "${all_done}" -eq 0 ]]; do
clear
echo "=================================================="
echo "🚀 Async PR Review Status for PR #${pr_number}"
echo "=================================================="
echo ""
all_done=1
for i in "${!tasks[@]}"; do
t="${tasks[${i}]}"
if [[ -f "${log_dir}/${t}.exit" ]]; then
read -r task_exit < "${log_dir}/${t}.exit" || task_exit=""
if [[ "${task_exit}" == "0" ]]; then
echo "${t}: SUCCESS"
else
echo "${t}: FAILED (exit code ${task_exit})"
fi
task_done[${t}]=1
else
echo "${t}: RUNNING"
all_done=0
fi
done
echo ""
echo "=================================================="
echo "📝 Live Logs (Last 5 lines of running tasks)"
echo "=================================================="
for i in "${!tasks[@]}"; do
t="${tasks[${i}]}"
log_file="${log_files[${i}]}"
if [[ "${task_done[${t}]}" -eq 0 ]]; then
if [[ -f "${log_dir}/${log_file}" ]]; then
echo ""
echo "--- ${t} ---"
tail -n 5 "${log_dir}/${log_file}"
fi
fi
done
if [[ "${all_done}" -eq 0 ]]; then
sleep 3
fi
done
clear
echo "=================================================="
echo "🚀 Async PR Review Status for PR #${pr_number}"
echo "=================================================="
echo ""
for t in "${tasks[@]}"; do
read -r task_exit < "${log_dir}/${t}.exit" || task_exit=""
if [[ "${task_exit}" == "0" ]]; then
echo "${t}: SUCCESS"
else
echo "${t}: FAILED (exit code ${task_exit})"
fi
done
echo ""
echo "⏳ Tasks complete! Synthesizing final assessment..."
if ! "${GEMINI_CMD}" --policy "${POLICY_PATH}" -p "Read the review at ${log_dir}/review.md, the automated test logs at ${log_dir}/npm-test.log, and the manual test execution logs at ${log_dir}/test-execution.log. Summarize the results, state whether the build and tests passed based on ${log_dir}/build-and-lint.exit and ${log_dir}/npm-test.exit, and give a final recommendation for PR ${pr_number}." > "${log_dir}/final-assessment.md" 2>&1; then
echo $? > "${log_dir}/final-assessment.exit"
echo "❌ Final assessment synthesis failed!"
echo "Check ${log_dir}/final-assessment.md for details."
notify "Async Review Failed" "Final assessment synthesis failed." "${pr_number}"
exit 1
fi
echo 0 > "${log_dir}/final-assessment.exit"
echo "✅ Final assessment complete! Check ${log_dir}/final-assessment.md"
notify "Async Review Complete" "Review and test execution finished successfully." "${pr_number}"
@@ -0,0 +1,65 @@
#!/bin/bash
pr_number="${1}"
if [[ -z "${pr_number}" ]]; then
echo "Usage: check-async-review <pr_number>"
exit 1
fi
base_dir="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [[ -z "${base_dir}" ]]; then
echo "❌ Must be run from within a git repository."
exit 1
fi
log_dir="${base_dir}/.gemini/tmp/async-reviews/pr-${pr_number}/logs"
if [[ ! -d "${log_dir}" ]]; then
echo "STATUS: NOT_FOUND"
echo "❌ No logs found for PR #${pr_number} in ${log_dir}"
exit 0
fi
tasks=(
"setup|setup.log"
"pr-diff|pr-diff.diff"
"build-and-lint|build-and-lint.log"
"review|review.md"
"npm-test|npm-test.log"
"test-execution|test-execution.log"
"final-assessment|final-assessment.md"
)
all_done=true
echo "STATUS: CHECKING"
for task_info in "${tasks[@]}"; do
IFS="|" read -r task_name log_file <<< "${task_info}"
file_path="${log_dir}/${log_file}"
exit_file="${log_dir}/${task_name}.exit"
if [[ -f "${exit_file}" ]]; then
read -r exit_code < "${exit_file}" || exit_code=""
if [[ "${exit_code}" == "0" ]]; then
echo "${task_name}: SUCCESS"
else
echo "${task_name}: FAILED (exit code ${exit_code})"
echo " Last lines of ${file_path}:"
tail -n 3 "${file_path}" | sed 's/^/ /' || true
fi
elif [[ -f "${file_path}" ]]; then
echo "${task_name}: RUNNING"
all_done=false
else
echo " ${task_name}: NOT STARTED"
all_done=false
fi
done
if [[ "${all_done}" == "true" ]]; then
echo "STATUS: COMPLETE"
echo "LOG_DIR: ${log_dir}"
else
echo "STATUS: IN_PROGRESS"
fi
+56
View File
@@ -0,0 +1,56 @@
---
name: behavioral-evals
description: Guidance for creating, running, fixing, and promoting behavioral evaluations. Use when verifying agent decision logic, debugging failures, debugging prompt steering, or adding workspace regression tests.
---
# Behavioral Evals
## Overview
Behavioral evaluations (evals) are tests that validate the **agent's decision-making** (e.g., tool choice) rather than pure functionality. They are critical for verifying prompt changes, debugging steerability, and preventing regressions.
> [!NOTE]
> **Single Source of Truth**: For core concepts, policies, running tests, and general best practices, always refer to **[evals/README.md](file:///Users/abhipatel/code/gemini-cli/docs/evals/README.md)**.
---
## 🔄 Workflow Decision Tree
1. **Does a prompt/tool change need validation?**
* *No* -> Normal integration tests.
* *Yes* -> Continue below.
2. **Is it UI/Interaction heavy?**
* *Yes* -> Use `appEvalTest` (`AppRig`). See **[creating.md](references/creating.md)**.
* *No* -> Use `evalTest` (`TestRig`). See **[creating.md](references/creating.md)**.
3. **Is it a new test?**
* *Yes* -> Set policy to `USUALLY_PASSES`.
* *No* -> `ALWAYS_PASSES` (locks in regression).
4. **Are you fixing a failure or promoting a test?**
* *Fixing* -> See **[fixing.md](references/fixing.md)**.
* *Promoting* -> See **[promoting.md](references/promoting.md)**.
---
## 📋 Quick Checklist
### 1. Setup Workspace
Seed the workspace with necessary files using the `files` object to simulate a realistic scenario (e.g., NodeJS project with `package.json`).
* *Details in **[creating.md](references/creating.md)***
### 2. Write Assertions
Audit agent decisions using `rig.setBreakpoint()` (AppRig only) or index verification on `rig.readToolLogs()`.
* *Details in **[creating.md](references/creating.md)***
### 3. Verify
Run single tests locally with Vitest. Confirm stability locally before relying on CI workflows.
* *See **[evals/README.md](file:///Users/abhipatel/code/gemini-cli/docs/evals/README.md)** for running commands.*
---
## 📦 Bundled Resources
Detailed procedural guides:
* **[creating.md](references/creating.md)**: Assertion strategies, Rig selection, Mock MCPs.
* **[fixing.md](references/fixing.md)**: Step-by-step automated investigation, architecture diagnosis guidelines.
* **[promoting.md](references/promoting.md)**: Candidate identification criteria and threshold guidelines.
@@ -0,0 +1,27 @@
import { describe, expect } from 'vitest';
import { appEvalTest } from './app-test-helper.js';
describe('interactive_feature', () => {
// New tests MUST start as USUALLY_PASSES
appEvalTest('USUALLY_PASSES', {
name: 'should pause for user confirmation',
files: {
'package.json': JSON.stringify({ name: 'app' })
},
prompt: 'Task description here requiring approval',
timeout: 60000,
setup: async (rig) => {
// ⚠️ Breakpoints are ONLY safe in appEvalTest
rig.setBreakpoint(['ask_user']);
},
assert: async (rig) => {
// 1. Wait for the breakpoint to trigger
const confirmation = await rig.waitForPendingConfirmation('ask_user');
expect(confirmation).toBeDefined();
// 2. Resolve it so the test can finish
await rig.resolveTool(confirmation);
await rig.waitForIdle();
},
});
});
@@ -0,0 +1,30 @@
import { describe, expect } from 'vitest';
import { evalTest } from './test-helper.js';
describe('core_feature', () => {
// New tests MUST start as USUALLY_PASSES
evalTest('USUALLY_PASSES', {
name: 'should perform expected agent action',
setup: async (rig) => {
// For mocking offline MCP:
// rig.addMockMcpServer('workspace-server', 'google-workspace');
},
files: {
'src/app.ts': '// some code',
},
prompt: 'Task description here',
timeout: 60000, // 1 minute safety limit
assert: async (rig, result) => {
// 1. Audit the trajectory (Safe for standard evalTest)
const logs = rig.readToolLogs();
const hasTool = logs.some((l) => l.toolRequest.name === 'read_file');
expect(hasTool, 'Agent should have read the file').toBe(true);
// 2. Assert efficiency (Cost/Turn)
expect(logs.length).toBeLessThan(5);
// 3. Assert final output
expect(result).toContain('Expected Keyword');
},
});
});
@@ -0,0 +1,151 @@
# Creating Behavioral Evals
## 🔬 Rig Selection
| Rig Type | Import From | Architecture | Use When |
| :---------------- | :--------------------- | :------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------- |
| **`evalTest`** | `./test-helper.js` | **Subprocess**. Runs the CLI in a separate process + waits for exit. | Standard workspace tests. **Do not use `setBreakpoint`**; auditing history (`readToolLogs`) is safer. |
| **`appEvalTest`** | `./app-test-helper.js` | **In-Process**. Runs directly inside the runner loop. | UI/Ink rendering. Safe for `setBreakpoint` triggers. |
---
## 🏗️ Scenario Design
Evals must simulate realistic agent environments to effectively test
decision-making.
- **Workspace State**: Seed with standard project anchors if testing general
capabilities:
- `package.json` for NodeJS environments.
- Minimal configuration files (`tsconfig.json`, `GEMINI.md`).
- **Structural Complexity**: Provide enough files to force the agent to _search_
or _navigate_, rather than giving the answer directly. Avoid trivial one-file
tests unless testing exact prompt steering.
---
## ❌ Fail First Principle
Before asserting a new capability or locking in a fix, **verify that the test
fails first**.
- It is easy to accidentally write an eval that asserts behaviors that are
already met or pass by default.
- **Process**: reproduce failure with test -> apply fix (prompt/tool) -> verify
test passes.
---
## ✋ Testing Patterns
### 1. Breakpoints
Verifies the agent _intends_ to use a tool BEFORE executing it. Useful for
interactive prompts or safety checks.
```typescript
// ⚠️ Only works with appEvalTest (AppRig)
setup: async (rig) => {
rig.setBreakpoint(['ask_user']);
},
assert: async (rig) => {
const confirmation = await rig.waitForPendingConfirmation('ask_user');
expect(confirmation).toBeDefined();
}
```
### 2. Tool Confirmation Race
When asserting multiple triggers (e.g., "enters plan mode then asks question"):
```typescript
assert: async (rig) => {
let confirmation = await rig.waitForPendingConfirmation([
'enter_plan_mode',
'ask_user',
]);
if (confirmation?.name === 'enter_plan_mode') {
rig.acceptConfirmation('enter_plan_mode');
confirmation = await rig.waitForPendingConfirmation('ask_user');
}
expect(confirmation?.toolName).toBe('ask_user');
};
```
### 3. Audit Tool Logs
Audit exact operations to ensure efficiency (e.g., no redundant reads).
```typescript
assert: async (rig, result) => {
await rig.waitForTelemetryReady();
const toolLogs = rig.readToolLogs();
const writeCall = toolLogs.find(
(log) => log.toolRequest.name === 'write_file',
);
expect(writeCall).toBeDefined();
};
```
### 4. Mock MCP Facades
To evaluate tools connected via MCP without hitting live endpoints, load a mock
server configuration in the `setup` hook.
```typescript
setup: async (rig) => {
rig.addMockMcpServer('workspace-server', 'google-workspace');
},
assert: async (rig) => {
await rig.waitForTelemetryReady();
const toolLogs = rig.readToolLogs();
const workspaceCall = toolLogs.find(
(log) => log.toolRequest.name === 'mcp_workspace-server_docs.getText'
);
expect(workspaceCall).toBeDefined();
};
```
---
## ⚠️ Safety & Efficiency Guardrails
### 1. Breakpoint Deadlocks
Breakpoints (`setBreakpoint`) pause execution. In standard `evalTest`,
`rig.run()` waits for the process to exit _before_ assertions run. **This will
hang indefinitely.**
- **Use Breakpoints** for `appEvalTest` or interactive simulations.
- **Use Audit Tool Logs** (above) for standard trajectory tests.
### 2. Runaway Timeout
Always set a budget boundary in the `EvalCase` to prevent runaway loops on
quota:
```typescript
evalTest('USUALLY_PASSES', {
name: '...',
timeout: 60000, // 1 minute safety limit
// ...
});
```
### 3. Efficiency Assertion (Turn limits)
Check if a tool is called _early_ using index checks:
```typescript
assert: async (rig) => {
const toolLogs = rig.readToolLogs();
const toolCallIndex = toolLogs.findIndex(
(log) => log.toolRequest.name === 'cli_help',
);
expect(toolCallIndex).toBeGreaterThan(-1);
expect(toolCallIndex).toBeLessThan(5); // Called within first 5 turns
};
```
@@ -0,0 +1,100 @@
# Fixing Behavioral Evals
Use this guide when asked to debug, troubleshoot, or fix a failing behavioral
evaluation.
---
## 1. 🔍 Investigate
1. **Fetch Nightly Results**: Use the `gh` CLI to inspect the latest run from
`evals-nightly.yml` if applicable.
- _Example view URL_:
`https://github.com/google-gemini/gemini-cli/actions/workflows/evals-nightly.yml`
2. **Isolate**: DO NOT push changes or start remote runs. Confine investigation
to the local workspace.
3. **Read Logs**:
- Eval logs live in `evals/logs/<test_name>.log`.
- Enable verbose debugging via `export GEMINI_DEBUG_LOG_FILE="debug.log"`.
4. **Diagnose**: Audit tool logs and telemetry. Note if due to setup/assert.
- **Tip**: Proactively add custom logging/diagnostics to check hypotheses.
---
## 2. 🛠️ Fix Strategy
1. **Targeted Location**: Locate the test case and the corresponding
prompt/code.
2. **Iterative Scope**: Make extreme change first to verify scope, then refine
to a minimal, targeted change.
3. **Assertion Fidelity**:
- Changing the test prompt is a **last resort** (prompts are often vague by
design).
- **Warning**: Do not lose test fidelity by making prompts too direct/easy.
- **Primary Fix Trigger**: Adjust tool descriptions, system prompts
(`snippets.ts`), or **modules that contribute to the prompt template**.
- Fixes should generally try to improve the prompt
`@packages/core/src/prompts/snippets.ts` first.
- **Instructional Generality**: Changes to the system prompt should aim to
be as general as possible while still accomplishing the goal. Specificity
should be added only as needed.
- **Principle**: Instead of creating "forbidden lists" for specific syntax
(e.g., "Don't use `Object.create()`"), formulate a broader engineering
principle that covers the underlying issue (e.g., "Prioritize explicit
composition over hidden prototype manipulation"). This improves
steerability across a wider range of similar scenarios.
- _Low Specificity_: "Follow ecosystem best practices"
- _Medium Specificity_: "Utilize OOP and functional best practices, as
applicable"
- _High Specificity_: Provide ecosystem-specific hints as examples of a
broader principle rather than direct instructions. e.g., "NEVER use
hacks like bypassing the type system or employing 'hidden' logic (e.g.:
reflection, prototype manipulation). Instead, use explicit and idiomatic
language features (e.g.: type guards, explicit class instantiation, or
object spread) that maintain structural integrity."
- **Prompt Simplification**: Once the test is passing, use `ask_user` to
determine if prompt simplification is desired.
- **Criteria**: Simplification should be attempted only if there are
related clauses that can be de-duplicated or reparented under a single
heading.
- **Verification**: As part of simplification, you MUST identify and run
any behavioral eval tests that might be affected by the changes to
ensure no regressions are introduced.
- Test fixes should not "cheat" by changing a test's `GEMINI.md` file or by
updating the test's prompt to instruct it to not repro the bug.
- **Warning**: Prompts have multiple configurations; ensure your fix targets
the correct config for the model in question.
4. **Architecture Options**: If prompt or instruction tuning triggers no
improvement, analyze loop composition.
- **AgentLoop**: Defined by `context + toolset + prompt`.
- **Enhancements**: Loops perform best with direct prompts, fewer irrelevant
tools, low goal density, and minimal low-value/irrelevant context.
- **Modifications**: Compose subagents or isolate tools. Ground in observed
traces.
- **Warning**: Think deeply before offering recommendations; avoid parroting
abstract design guidelines.
---
## 3. ✅ Verify
1. **Run Local**: Run Vitest in non-interactive mode on just the file.
2. **Log Audit**: Prioritize diagnosing failures via log comparison before
triggering heavy test runs.
3. **Stability Limit**: Run the test **3 times** locally on key models (can use
scripts to run in parallel for speed):
- **Gemini 3.0**
- **Gemini 3 Flash**
- **Gemini 2.5 Pro**
4. **Flakiness Rule**: If it passes 2/3 times, it may be inherent noise
difficult to improve without a structural split.
---
## 4. 📊 Report
Provide a summary of:
- Test success rate for each tested model (e.g., 3/3 = 100%).
- Root cause identification and fix explanation.
- If unfixed, provide high-confidence architecture recommendations.
@@ -0,0 +1,55 @@
# Promoting Behavioral Evals
Use this guide when asked to analyze nightly results and promote incubated tests
to stable suites.
---
## 1. 🔍 Investigate candidates
1. **Audit Nightly Logs**: Use the `gh` CLI to fetch results from
`evals-nightly.yml` (Direct URL:
`https://github.com/google-gemini/gemini-cli/actions/workflows/evals-nightly.yml`).
- **Tip**: The aggregate summary from the most recent run integrates the
last 7 runs of history automatically.
- **Safety**: DO NOT push changes or start remote runs. All verification is
local.
2. **Assess Stability**: Identify tests that pass **100% of the time** across
ALL enabled models over the **last 7 nightly runs** in a row.
- _100% means the test passed 3/3 times for every model and run._
3. **Promotion Targets**: Tests meeting this criteria are candidates for
promotion from `USUALLY_PASSES` to `ALWAYS_PASSES`.
---
## 2. 🚥 Promotion Steps
1. **Locate File**: Locate the eval file in the `evals/` directory.
2. **Update Policy**: Modify the policy argument to `ALWAYS_PASSES`.
```typescript
evalTest('ALWAYS_PASSES', { ... })
```
3. **Targeting**: Follow guidelines in `evals/README.md` regarding stable suite
organization.
4. **Constraint**: Your final change must be **minimal and targeted** strictly
to promoting the test status. Do not refactor the test or setup fixtures.
---
## 3. ✅ Verify
1. **Run Prompted Tests**: Run the promoted test locally using non-interactive
Vitest to confirm structure validity.
2. **Verify Suite Inclusion**: Check that the test is successfully picked up by
standard runnable ranges.
---
## 4. 📊 Report
Provide a summary of:
- Which tests were promoted.
- Provide the success rate evidence (e.g., 7/7 runs passed for all models).
- If no candidates qualified, list the next closest candidates and their current
pass rate.
@@ -0,0 +1,95 @@
# Running & Promoting Evals
## 🛠️ Prerequisites
Behavioral evals run against the compiled binary. You **must** build and bundle
the project first after making changes:
```bash
npm run build && npm run bundle
```
---
## 🏃‍♂️ Running Tests
### 1. Configure Environment Variables
Evals require a standard API key. If your `.env` file has multiple keys or
comments, use this precise extraction setup:
```bash
export GEMINI_API_KEY=$(grep '^GEMINI_API_KEY=' .env | cut -d '=' -f2) && RUN_EVALS=1 npx vitest run --config evals/vitest.config.ts <file_name>
```
### 2. Commands
| Command | Scope | Description |
| :---------------------------------- | :-------------- | :------------------------------------------------- |
| `npm run test:always_passing_evals` | `ALWAYS_PASSES` | Fast feedback, runs in CI. |
| `npm run test:all_evals` | All | Runs nightly incubation tests. Sets `RUN_EVALS=1`. |
### Target Specific File
_Note: `RUN_EVALS=1` is required for incubated (`USUALLY_PASSES`) tests._
```bash
RUN_EVALS=1 npx vitest run --config evals/vitest.config.ts my_feature.eval.ts
```
---
## 🐞 Debugging and Logs
If a test fails, verify:
- **Tool Trajectory Logs**:序列 of calls in `evals/logs/<test_name>.log`.
- **Verbose Reasoning**: Capture raw buffer traces by setting
`GEMINI_DEBUG_LOG_FILE`:
```bash
export GEMINI_DEBUG_LOG_FILE="debug.log"
```
---
### 🎯 Verify Model Targeting
- **Tip:** Standard evals benchmark against model variations. If a test passes
on Flash but fails on Pro (or vice versa), the issue is usually in the **tool
description**, not the prompt definition. Flash is sensitive to "instruction
bloat," while Pro is sensitive to "ambiguous intent."
---
## 🚥 deflaking & Promotion
To maintain CI stability, all new evals follow a strict incubation period.
### 1. Incubation (`USUALLY_PASSES`)
New tests must be created with the `USUALLY_PASSES` policy.
```typescript
evalTest('USUALLY_PASSES', { ... })
```
They run in **Evals: Nightly** workflows and do not block PR merges.
### 2. Investigate Failures
If a nightly eval regresses, investigate via agent:
```bash
gemini /fix-behavioral-eval [optional-run-uri]
```
### 3. Promotion (`ALWAYS_PASSES`)
Once a test scores 100% consistency over multiple nightly cycles:
```bash
gemini /promote-behavioral-eval
```
_Do not promote manually._ The command verifies trajectory logs before updating
the file policy.
+66
View File
@@ -0,0 +1,66 @@
---
name: ci
description:
A specialized skill for Gemini CLI that provides high-performance, fail-fast
monitoring of GitHub Actions workflows and automated local verification of CI
failures. It handles run discovery automatically—simply provide the branch name.
---
# CI Replicate & Status
This skill enables the agent to efficiently monitor GitHub Actions, triage
failures, and bridge remote CI errors to local development. It defaults to
**automatic replication** of failures to streamline the fix cycle.
## Core Capabilities
- **Automatic Replication**: Automatically monitors CI and immediately executes
suggested test or lint commands locally upon failure.
- **Real-time Monitoring**: Aggregated status line for all concurrent workflows
on the current branch.
- **Fail-Fast Triage**: Immediately stops on the first job failure to provide a
structured report.
## Workflow
### 1. CI Replicate (`replicate`) - DEFAULT
Use this as the primary path to monitor CI and **automatically** replicate
failures locally for immediate triage and fixing.
- **Behavior**: When this workflow is triggered, the agent will monitor the CI
and **immediately and automatically execute** all suggested test or lint
commands (marked with 🚀) as soon as a failure is detected.
- **Tool**: `node .gemini/skills/ci/scripts/ci.mjs [branch]`
- **Discovery**: The script **automatically** finds the latest active or recent
run for the branch. Do NOT manually search for run IDs.
- **Goal**: Reproduce the failure locally without manual intervention, then
proceed to analyze and fix the code.
### 1. CI Status (`status`)
Use this when you have pushed changes and need to monitor the CI and reproduce
any failures locally.
- **Tool**: `node .gemini/skills/ci/scripts/ci.mjs [branch] [run_id]`
- **Discovery**: The script **automatically** finds the latest active or recent
run for the branch. You should NOT manually search for \`run_id\` using \`gh run list\`
unless a specific historical run is requested. Simply provide the branch name.
- **Step 1 (Monitor)**: Execute the tool with the branch name.
- **Step 2 (Extract)**: Extract suggested \`npm test\` or \`npm run lint\` commands
from the output (marked with 🚀).
- **Step 3 (Reproduce)**: Execute those commands locally to confirm the failure.
- **Behavior**: It will poll every 15 seconds. If it detects a failure, it will
exit with a structured report and provide the exact commands to run locally.
## Failure Categories & Actions
- **Test Failures**: Agent should run the specific `npm test -w <pkg> -- <path>`
command suggested.
- **Lint Errors**: Agent should run `npm run lint:all` or the specific package
lint command.
- **Build Errors**: Agent should check `tsc` output or build logs to resolve
compilation issues.
- **Job Errors**: Investigate `gh run view --job <job_id> --log` for
infrastructure or setup failures.
## Noise Filtering
The underlying scripts automatically filter noise (Git logs, NPM warnings, stack
trace overhead). The agent should focus on the "Structured Failure Report"
provided by the tool.
+281
View File
@@ -0,0 +1,281 @@
#!/usr/bin/env node
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { execSync } from 'node:child_process';
const BRANCH =
process.argv[2] || execSync('git branch --show-current').toString().trim();
const RUN_ID_OVERRIDE = process.argv[3];
let REPO;
try {
const remoteUrl = execSync('git remote get-url origin').toString().trim();
REPO = remoteUrl
.replace(/.*github\.com[\/:]/, '')
.replace(/\.git$/, '')
.trim();
} catch (e) {
REPO = 'google-gemini/gemini-cli';
}
const FAILED_FILES = new Set();
function runGh(args) {
try {
return execSync(`gh ${args}`, {
stdio: ['ignore', 'pipe', 'ignore'],
}).toString();
} catch (e) {
return null;
}
}
function fetchFailuresViaApi(jobId) {
try {
const cmd = `gh api repos/${REPO}/actions/jobs/${jobId}/logs | grep -iE " FAIL |❌|ERROR|Lint failed|Build failed|Exception|failed with exit code"`;
return execSync(cmd, {
stdio: ['ignore', 'pipe', 'ignore'],
maxBuffer: 10 * 1024 * 1024,
}).toString();
} catch (e) {
return '';
}
}
function isNoise(line) {
const lower = line.toLowerCase();
return (
lower.includes('* [new branch]') ||
lower.includes('npm warn') ||
lower.includes('fetching updates') ||
lower.includes('node:internal/errors') ||
lower.includes('at ') || // Stack traces
lower.includes('checkexecsyncerror') ||
lower.includes('node_modules')
);
}
function extractTestFile(failureText) {
const cleanLine = failureText
.replace(/[|#\[\]()]/g, ' ')
.replace(/<[^>]*>/g, ' ')
.trim();
const fileMatch = cleanLine.match(/([\w\/._-]+\.test\.[jt]sx?)/);
if (fileMatch) return fileMatch[1];
return null;
}
function generateTestCommand(failedFilesMap) {
const workspaceToFiles = new Map();
for (const [file, info] of failedFilesMap.entries()) {
if (
['Job Error', 'Unknown File', 'Build Error', 'Lint Error'].includes(file)
)
continue;
let workspace = '@google/gemini-cli';
let relPath = file;
if (file.startsWith('packages/core/')) {
workspace = '@google/gemini-cli-core';
relPath = file.replace('packages/core/', '');
} else if (file.startsWith('packages/cli/')) {
workspace = '@google/gemini-cli';
relPath = file.replace('packages/cli/', '');
}
relPath = relPath.replace(/^.*packages\/[^\/]+\//, '');
if (!workspaceToFiles.has(workspace))
workspaceToFiles.set(workspace, new Set());
workspaceToFiles.get(workspace).add(relPath);
}
const commands = [];
for (const [workspace, files] of workspaceToFiles.entries()) {
commands.push(`npm test -w ${workspace} -- ${Array.from(files).join(' ')}`);
}
return commands.join(' && ');
}
async function monitor() {
let targetRunIds = [];
if (RUN_ID_OVERRIDE) {
targetRunIds = [RUN_ID_OVERRIDE];
} else {
// 1. Get runs directly associated with the branch
const runListOutput = runGh(
`run list --branch "${BRANCH}" --limit 10 --json databaseId,status,workflowName,createdAt`,
);
if (runListOutput) {
const runs = JSON.parse(runListOutput);
const activeRuns = runs.filter((r) => r.status !== 'completed');
if (activeRuns.length > 0) {
targetRunIds = activeRuns.map((r) => r.databaseId);
} else if (runs.length > 0) {
const latestTime = new Date(runs[0].createdAt).getTime();
targetRunIds = runs
.filter((r) => latestTime - new Date(r.createdAt).getTime() < 60000)
.map((r) => r.databaseId);
}
}
// 2. Get runs associated with commit statuses (handles chained/indirect runs)
try {
const headSha = execSync(`git rev-parse "${BRANCH}"`).toString().trim();
const statusOutput = runGh(
`api repos/${REPO}/commits/${headSha}/status -q '.statuses[] | select(.target_url | contains("actions/runs/")) | .target_url'`,
);
if (statusOutput) {
const statusRunIds = statusOutput
.split('\n')
.filter(Boolean)
.map((url) => {
const match = url.match(/actions\/runs\/(\d+)/);
return match ? parseInt(match[1], 10) : null;
})
.filter(Boolean);
for (const runId of statusRunIds) {
if (!targetRunIds.includes(runId)) {
targetRunIds.push(runId);
}
}
}
} catch (e) {
// Ignore if branch/SHA not found or API fails
}
if (targetRunIds.length > 0) {
const runNames = [];
for (const runId of targetRunIds) {
const runInfo = runGh(`run view "${runId}" --json workflowName`);
if (runInfo) {
runNames.push(JSON.parse(runInfo).workflowName);
}
}
console.log(`Monitoring workflows: ${[...new Set(runNames)].join(', ')}`);
}
}
if (targetRunIds.length === 0) {
console.log(`No runs found for branch ${BRANCH}.`);
process.exit(0);
}
while (true) {
let allPassed = 0,
allFailed = 0,
allRunning = 0,
allQueued = 0,
totalJobs = 0;
let anyRunInProgress = false;
const fileToTests = new Map();
let failuresFoundInLoop = false;
for (const runId of targetRunIds) {
const runOutput = runGh(
`run view "${runId}" --json databaseId,status,conclusion,workflowName`,
);
if (!runOutput) continue;
const run = JSON.parse(runOutput);
if (run.status !== 'completed') anyRunInProgress = true;
const jobsOutput = runGh(`run view "${runId}" --json jobs`);
if (jobsOutput) {
const { jobs } = JSON.parse(jobsOutput);
totalJobs += jobs.length;
const failedJobs = jobs.filter((j) => j.conclusion === 'failure');
if (failedJobs.length > 0) {
failuresFoundInLoop = true;
for (const job of failedJobs) {
const failures = fetchFailuresViaApi(job.databaseId);
if (failures.trim()) {
failures.split('\n').forEach((line) => {
if (!line.trim() || isNoise(line)) return;
const file = extractTestFile(line);
const filePath =
file ||
(line.toLowerCase().includes('lint')
? 'Lint Error'
: line.toLowerCase().includes('build')
? 'Build Error'
: 'Unknown File');
let testName = line;
if (line.includes(' > ')) {
testName = line.split(' > ').slice(1).join(' > ').trim();
}
if (!fileToTests.has(filePath))
fileToTests.set(filePath, new Set());
fileToTests.get(filePath).add(testName);
});
} else {
const step =
job.steps?.find((s) => s.conclusion === 'failure')?.name ||
'unknown';
const category = step.toLowerCase().includes('lint')
? 'Lint Error'
: step.toLowerCase().includes('build')
? 'Build Error'
: 'Job Error';
if (!fileToTests.has(category))
fileToTests.set(category, new Set());
fileToTests
.get(category)
.add(`${job.name}: Failed at step "${step}"`);
}
}
}
for (const job of jobs) {
if (job.status === 'in_progress') allRunning++;
else if (job.status === 'queued') allQueued++;
else if (job.conclusion === 'success') allPassed++;
else if (job.conclusion === 'failure') allFailed++;
}
}
}
if (failuresFoundInLoop) {
console.log(
`\n\n❌ Failures detected across ${allFailed} job(s). Stopping monitor...`,
);
console.log('\n--- Structured Failure Report (Noise Filtered) ---');
for (const [file, tests] of fileToTests.entries()) {
console.log(`\nCategory/File: ${file}`);
// Limit output per file if it's too large
const testsArr = Array.from(tests).map((t) =>
t.length > 500 ? t.substring(0, 500) + '... [TRUNCATED]' : t,
);
testsArr.slice(0, 10).forEach((t) => console.log(` - ${t}`));
if (testsArr.length > 10)
console.log(` ... and ${testsArr.length - 10} more`);
}
const testCmd = generateTestCommand(fileToTests);
if (testCmd) {
console.log('\n🚀 Run this to verify fixes:');
console.log(testCmd);
} else if (
Array.from(fileToTests.keys()).some((k) => k.includes('Lint'))
) {
console.log('\n🚀 Run this to verify lint fixes:\nnpm run lint:all');
}
console.log('---------------------------------');
process.exit(1);
}
const completed = allPassed + allFailed;
process.stdout.write(
`\r⏳ Monitoring ${targetRunIds.length} runs... ${completed}/${totalJobs} jobs (${allPassed} passed, ${allFailed} failed, ${allRunning} running, ${allQueued} queued) `,
);
if (!anyRunInProgress) {
console.log('\n✅ All workflows passed!');
process.exit(0);
}
await new Promise((r) => setTimeout(r, 15000));
}
}
monitor().catch((err) => {
console.error('\nMonitor error:', err.message);
process.exit(1);
});
+65
View File
@@ -0,0 +1,65 @@
---
name: code-reviewer
description:
Use this skill to review code. It supports both local changes (staged or working tree)
and remote Pull Requests (by ID or URL). It focuses on correctness, maintainability,
and adherence to project standards.
---
# Code Reviewer
This skill guides the agent in conducting professional and thorough code reviews for both local development and remote Pull Requests.
## Workflow
### 1. Determine Review Target
* **Remote PR**: If the user provides a PR number or URL (e.g., "Review PR #123"), target that remote PR.
* **Local Changes**: If no specific PR is mentioned, or if the user asks to "review my changes", target the current local file system states (staged and unstaged changes).
### 2. Preparation
#### For Remote PRs:
1. **Checkout**: Use the GitHub CLI to checkout the PR.
```bash
gh pr checkout <PR_NUMBER>
```
2. **Preflight**: Execute the project's standard verification suite to catch automated failures early.
```bash
npm run preflight
```
3. **Context**: Read the PR description and any existing comments to understand the goal and history.
#### For Local Changes:
1. **Identify Changes**:
* Check status: `git status`
* Read diffs: `git diff` (working tree) and/or `git diff --staged` (staged).
2. **Preflight (Optional)**: If the changes are substantial, ask the user if they want to run `npm run preflight` before reviewing.
### 3. In-Depth Analysis
Analyze the code changes based on the following pillars:
* **Correctness**: Does the code achieve its stated purpose without bugs or logical errors?
* **Maintainability**: Is the code clean, well-structured, and easy to understand and modify in the future? Consider factors like code clarity, modularity, and adherence to established design patterns.
* **Readability**: Is the code well-commented (where necessary) and consistently formatted according to our project's coding style guidelines?
* **Efficiency**: Are there any obvious performance bottlenecks or resource inefficiencies introduced by the changes?
* **Security**: Are there any potential security vulnerabilities or insecure coding practices?
* **Edge Cases and Error Handling**: Does the code appropriately handle edge cases and potential errors?
* **Testability**: Is the new or modified code adequately covered by tests (even if preflight checks pass)? Suggest additional test cases that would improve coverage or robustness.
### 4. Provide Feedback
#### Structure
* **Summary**: A high-level overview of the review.
* **Findings**:
* **Critical**: Bugs, security issues, or breaking changes.
* **Improvements**: Suggestions for better code quality or performance.
* **Nitpicks**: Formatting or minor style issues (optional).
* **Conclusion**: Clear recommendation (Approved / Request Changes).
#### Tone
* Be constructive, professional, and friendly.
* Explain *why* a change is requested.
* For approvals, acknowledge the specific value of the contribution.
### 5. Cleanup (Remote PRs only)
* After the review, ask the user if they want to switch back to the default branch (e.g., `main` or `master`).
+168
View File
@@ -0,0 +1,168 @@
---
name: docs-changelog
description: >-
Generates and formats changelog files for a new release based on provided
version and raw changelog data.
---
# Procedure: Updating Changelog for New Releases
## Objective
To standardize the process of updating changelog files (`latest.md`,
`preview.md`, `index.md`) based on automated release information.
## Inputs
- **version**: The release version string (e.g., `v0.28.0`,
`v0.29.0-preview.2`).
- **TIME**: The release timestamp (e.g., `2026-02-12T20:33:15Z`).
- **BODY**: The raw markdown release notes, containing a "What's Changed"
section and a "Full Changelog" link.
## Guidelines for `latest.md` and `preview.md` Highlights
- Aim for **3-5 key highlight points**.
- Each highlight point must start with a bold-typed title that summarizes the
change (e.g., `**New Feature:** A brief description...`).
- **Prioritize** summarizing new features over other changes like bug fixes or
chores.
- **Avoid** mentioning features that are "experimental" or "in preview" in
Stable Releases.
- **DO NOT** include PR numbers, links, or author names in these highlights.
- Refer to `.gemini/skills/docs-changelog/references/highlights_examples.md`
for the correct style and tone.
## Initial Processing
1. **Analyze Version**: Determine the release path based on the `version`
string.
- If `version` contains "nightly", **STOP**. No changes are made.
- If `version` ends in `.0`, follow the **Path A: New Minor Version**
procedure.
- If `version` does not end in `.0`, follow the **Path B: Patch Version**
procedure.
2. **Process Time**: Convert the `TIME` input into two formats for later use:
`yyyy-mm-dd` and `Month dd, yyyy`.
3. **Process Body**:
- Save the incoming `BODY` content to a temporary file for processing.
- In the "What's Changed" section of the temporary file, reformat all pull
request URLs to be markdown links with the PR number as the text (e.g.,
`[#12345](URL)`).
- If a "New Contributors" section exists, delete it.
- Preserve the "**Full Changelog**" link. The processed content of this
temporary file will be used in subsequent steps.
---
## Path A: New Minor Version
*Use this path if the version number ends in `.0`.*
**Important:** Based on the version, you must choose to follow either section
A.1 for stable releases or A.2 for preview releases. Do not follow the
instructions for the other section.
### A.1: Stable Release (e.g., `v0.28.0`)
For a stable release, you will generate two distinct summaries from the
changelog: a concise **announcement** for the main changelog page, and a more
detailed **highlights** section for the release-specific page.
1. **Create the Announcement for `index.md`**:
- Generate a concise announcement summarizing the most important changes.
Each announcement entry must start with a bold-typed title that
summarizes the change.
- **Important**: The format for this announcement is unique. You **must**
use the existing announcements in `docs/changelogs/index.md` and the
example within
`.gemini/skills/docs-changelog/references/index_template.md` as your
guide. This format includes PR links and authors. Stick to 1 or 2 PR
links and authors.
- Add this new announcement to the top of `docs/changelogs/index.md`.
2. **Create Highlights and Update `latest.md`**:
- Generate a comprehensive "Highlights" section, following the guidelines
in the "Guidelines for `latest.md` and `preview.md` Highlights" section
above.
- Take the content from
`.gemini/skills/docs-changelog/references/latest_template.md`.
- Populate the template with the `version`, `release_date`, generated
`highlights`, and the processed content from the temporary file.
- **Completely replace** the contents of `docs/changelogs/latest.md` with
the populated template.
### A.2: Preview Release (e.g., `v0.29.0-preview.0`)
1. **Update `preview.md`**:
- Generate a comprehensive "Highlights" section, following the highlight
guidelines.
- Take the content from
`.gemini/skills/docs-changelog/references/preview_template.md`.
- Populate the template with the `version`, `release_date`, generated
`highlights`, and the processed content from the temporary file.
- **Completely replace** the contents of `docs/changelogs/preview.md`
with the populated template.
---
## Path B: Patch Version
*Use this path if the version number does **not** end in `.0`.*
**Important:** Based on the version, you must choose to follow either section
B.1 for stable patches or B.2 for preview patches. Do not follow the
instructions for the other section.
### B.1: Stable Patch (e.g., `v0.28.1`)
- **Target File**: `docs/changelogs/latest.md`
- Perform the following edits on the target file:
1. Update the version in the main header. The line should read,
`# Latest stable release: {{version}}`
2. Update the rease date. The line should read,
`Released: {{release_date_month_dd_yyyy}}`
3. Determine if a "What's Changed" section exists in the temporary file
If so, continue to step 4. Otherwise, skip to step 5.
4. **Prepend** the processed "What's Changed" list from the temporary file
to the existing "What's Changed" list in `latest.md`. Do not change or
replace the existing list, **only add** to the beginning of it.
5. In the "Full Changelog", edit **only** the end of the URL. Identify the
last part of the URL that looks like `...{previous_version}` and update
it to be `...{version}`.
Example: assume the patch version is `v0.29.1`. Change
`Full Changelog: https://github.com/google-gemini/gemini-cli/compare/v0.28.2…v0.29.0`
to
`Full Changelog: https://github.com/google-gemini/gemini-cli/compare/v0.28.2…v0.29.1`
### B.2: Preview Patch (e.g., `v0.29.0-preview.3`)
- **Target File**: `docs/changelogs/preview.md`
- Perform the following edits on the target file:
1. Update the version in the main header. The line should read,
`# Preview release: {{version}}`
2. Update the rease date. The line should read,
`Released: {{release_date_month_dd_yyyy}}`
3. Determine if a "What's Changed" section exists in the temporary file
If so, continue to step 4. Otherwise, skip to step 5.
4. **Prepend** the processed "What's Changed" list from the temporary file
to the existing "What's Changed" list in `preview.md`. Do not change or
replace the existing list, **only add** to the beginning of it.
5. In the "Full Changelog", edit **only** the end of the URL. Identify the
last part of the URL that looks like `...{previous_version}` and update
it to be `...{version}`.
Example: assume the patch version is `v0.29.0-preview.1`. Change
`Full Changelog: https://github.com/google-gemini/gemini-cli/compare/v0.28.2…v0.29.0-preview.0`
to
`Full Changelog: https://github.com/google-gemini/gemini-cli/compare/v0.28.2…v0.29.0-preview.1`
---
## Finalize
- After making changes, if `npm run format` fails, it may be necessary to run
`npm install` first to ensure all formatting dependencies are available.
Then, run `npm run format` to ensure consistency.
- Delete any temporary files created during the process.
@@ -0,0 +1,68 @@
## Highlights example 1
- **Plan Mode Enhancements**: Significant updates to Plan Mode, including new
commands, support for MCP servers, integration of planning artifacts, and
improved iteration guidance.
- **Core Agent Improvements**: Enhancements to the core agent, including better
system prompt rigor, improved subagent definitions, and enhanced tool
execution limits.
- **CLI UX/UI Updates**: Various UI and UX improvements, such as autocomplete in
the input prompt, updated approval mode labels, DevTools integration, and
improved header spacing.
- **Tooling & Extension Updates**: Improvements to existing tools like
`ask_user` and `grep_search`, and new features for extension management.
- **Bug Fixes**: Numerous bug fixes across the CLI and core, addressing issues
with interactive commands, memory leaks, permission checks, and more.
- **Context and Tool Output Management**: Features for observation masking for
tool outputs, session-linked tool output storage, and persistence for masked
tool outputs.
## Highlights example 2
- **Commands & UX Enhancements:** Introduced `/prompt-suggest` command,
alongside updated undo/redo keybindings and automatic theme switching.
- **Expanded IDE Support:** Now offering compatibility with Positron IDE,
expanding integration options for developers.
- **Enhanced Security & Authentication:** Implemented interactive and
non-interactive OAuth consent, improving both security and diagnostic
capabilities for bug reports.
- **Advanced Planning & Agent Tools:** Integrated a generic Checklist component
for structured task management and evolved subagent capabilities with dynamic
policy registration.
- **Improved Core Stability & Reliability:** Resolved critical environment
loading, authentication, and session management issues, ensuring a more robust
experience.
- **Background Shell Commands:** Enabled the execution of shell commands in the
background for increased workflow efficiency.
## Highlights example 3
- **Event-Driven Architecture:** The CLI now uses an event-driven scheduler for
tool execution, improving performance and responsiveness. This includes
migrating non-interactive flows and sub-agents to the new scheduler.
- **Enhanced User Experience:** This release introduces several UI/UX
improvements, including queued tool confirmations and the ability to expand
and collapse large pasted text blocks. The `Settings` dialog has been improved
to reduce jitter and preserve focus.
- **Agent and Skill Improvements:** Agent Skills have been promoted to a stable
feature. Sub-agents now use a JSON schema for input and are tracked by an
`AgentRegistry`.
- **New `/rewind` Command:** A new `/rewind` command has been implemented to
allow users to go back in their session history.
- **Improved Shell and File Handling:** The shell tool's output format has been
optimized, and the CLI now gracefully handles disk-full errors during chat
recording. A bug in detecting already added paths has been fixed.
- **Linux Clipboard Support:** Image pasting capabilities for Wayland and X11 on
Linux have been added.
## Highlights example 4
- **Improved Hooks Management:** Hooks enable/disable functionality now aligns
with skills and offers improved completion.
- **Custom Themes for Extensions:** Extensions can now support custom themes,
allowing for greater personalization.
- **User Identity Display:** User identity information (auth, email, tier) is
now displayed on startup and in the `stats` command.
- **Plan Mode Enhancements:** Plan mode has been improved with a generic
`Checklist` component and refactored `Todo`.
- **Background Shell Commands:** Implementation of background shell commands.
@@ -0,0 +1,10 @@
## Announcements: {{version}} - {{release_date_yyyy_mm_dd}}
{{announcement_content}}
<!-- Example entry, multiple entries per highlights
- **Highlighted Feature:** We've added a new highlighted feature to help
you generate prompt suggestions
([#nnnnn](https://github.com/google-gemini/gemini-cli/pull/nnnnn) by
@author).
-->
@@ -0,0 +1,20 @@
# Latest stable release: {{version}}
Released: {{release_date_month_dd_yyyy}}
For most users, our latest stable release is the recommended release. Install
the latest stable version with:
```
npm install -g @google/gemini-cli
```
## Highlights
{{highlights_content}}
## What's Changed
{{changelog_list}}
**Full Changelog**: {{full_changelog_link}}
@@ -0,0 +1,22 @@
# Preview release: {{version}}
Released: {{release_date_month_dd_yyyy}}
Our preview release includes the latest, new, and experimental features. This
release may not be as stable as our [latest weekly release](latest.md).
To install the preview release:
```
npm install -g @google/gemini-cli@preview
```
## Highlights
{{highlights_content}}
## What's Changed
{{changelog_list}}
**Full Changelog**: {{full_changelog_link}}
+194
View File
@@ -0,0 +1,194 @@
---
name: docs-writer
description:
Always use this skill when the task involves writing, reviewing, or editing
files in the `/docs` directory or any `.md` files in the repository.
---
# `docs-writer` skill instructions
As an expert technical writer and editor for the Gemini CLI project, you produce
accurate, clear, and consistent documentation. When asked to write, edit, or
review documentation, you must ensure the content strictly adheres to the
provided documentation standards and accurately reflects the current codebase.
Adhere to the contribution process in `CONTRIBUTING.md` and the following
project standards.
## Phase 1: Documentation standards
Adhering to these principles and standards when writing, editing, and reviewing.
### Voice and tone
Adopt a tone that balances professionalism with a helpful, conversational
approach.
- **Perspective and tense:** Address the reader as "you." Use active voice and
present tense (e.g., "The API returns...").
- **Tone:** Professional, friendly, and direct.
- **Clarity:** Use simple vocabulary. Avoid jargon, slang, and marketing hype.
- **Global Audience:** Write in standard US English. Avoid idioms and cultural
references.
- **Requirements:** Be clear about requirements ("must") vs. recommendations
("we recommend"). Avoid "should."
- **Word Choice:** Avoid "please" and anthropomorphism (e.g., "the server
thinks"). Use contractions (don't, it's).
### Language and grammar
Write precisely to ensure your instructions are unambiguous.
- **Abbreviations:** Avoid Latin abbreviations; use "for example" (not "e.g.")
and "that is" (not "i.e.").
- **Punctuation:** Use the serial comma. Place periods and commas inside
quotation marks.
- **Dates:** Use unambiguous formats (e.g., "January 22, 2026").
- **Conciseness:** Use "lets you" instead of "allows you to." Use precise,
specific verbs.
- **Examples:** Use meaningful names in examples; avoid placeholders like
"foo" or "bar."
- **Quota and limit terminology:** For any content involving resource capacity
or using the word "quota" or "limit", strictly adhere to the guidelines in
the `quota-limit-style-guide.md` resource file. Generally, Use "quota" for
the administrative bucket and "limit" for the numerical ceiling.
### Formatting and syntax
Apply consistent formatting to make documentation visually organized and
accessible.
- **Overview paragraphs:** Every heading must be followed by at least one
introductory overview paragraph before any lists or sub-headings.
- **Text wrap:** Wrap text at 80 characters (except long links or tables).
- **Casing:** Use sentence case for headings, titles, and bolded text.
- **Naming:** Always refer to the project as `Gemini CLI` (never
`the Gemini CLI`).
- **Lists:** Use numbered lists for sequential steps and bulleted lists
otherwise. Keep list items parallel in structure.
- **UI and code:** Use **bold** for UI elements and `code font` for filenames,
snippets, commands, and API elements. Focus on the task when discussing
interaction.
- **Accessibility:** Use semantic HTML elements correctly (headings, lists,
tables).
- **Media:** Use lowercase hyphenated filenames. Provide descriptive alt text
for all images.
- **Details section:** Use the `<details>` tag to create a collapsible section.
This is useful for supplementary or data-heavy information that isn't critical
to the main flow.
Example:
<details>
<summary>Title</summary>
- First entry
- Second entry
</details>
- **Callouts**: Use GitHub-flavored markdown alerts to highlight important
information. To ensure the formatting is preserved by `npm run format`, place
an empty line, then a prettier ignore comment directly before the callout
block. Use `<!-- prettier-ignore -->` for standard Markdown files (`.md`) and
`{/* prettier-ignore */}` for MDX files (`.mdx`). The callout type (`[!TYPE]`)
should be on the first line, followed by a newline, and then the content, with
each subsequent line of content starting with `>`. Available types are `NOTE`,
`TIP`, `IMPORTANT`, `WARNING`, and `CAUTION`.
Example (.md):
<!-- prettier-ignore -->
> [!NOTE]
> This is an example of a multi-line note that will be preserved
> by Prettier.
Example (.mdx):
{/* prettier-ignore */}
> [!NOTE]
> This is an example of a multi-line note that will be preserved
> by Prettier.
### Links
- **Accessibility:** Use descriptive anchor text; avoid "click here." Ensure the
link makes sense out of context, such as when being read by a screen reader.
- **Use relative links in docs:** Use relative links in documentation (`/docs/`)
to ensure portability. Use paths relative to the current file's directory
(for example, `../tools/` from `docs/cli/`). Do not include the `/docs/`
section of a path, but do verify that the resulting relative link exists. This
does not apply to meta files such as README.MD and CONTRIBUTING.MD.
- **When changing headings, check for deep links:** If a user is changing a
heading, check for deep links to that heading in other pages and update
accordingly.
### Structure
- **BLUF:** Start with an introduction explaining what to expect.
- **Experimental features:** If a feature is clearly noted as experimental,
add the following note immediately after the introductory paragraph:
<!-- prettier-ignore -->
> [!NOTE]
> This is an experimental feature currently under active development.
(Note: Use `{/* prettier-ignore */}` if editing an `.mdx` file.)
- **Headings:** Use hierarchical headings to support the user journey.
- **Procedures:**
- Introduce lists of steps with a complete sentence.
- Start each step with an imperative verb.
- Number sequential steps; use bullets for non-sequential lists.
- Put conditions before instructions (e.g., "On the Settings page, click...").
- Provide clear context for where the action takes place.
- Indicate optional steps clearly (e.g., "Optional: ...").
- **Elements:** Use bullet lists, tables, details, and callouts.
- **Avoid using a table of contents:** If a table of contents is present, remove
it.
- **Next steps:** Conclude with a "Next steps" section if applicable.
## Phase 2: Preparation
Before modifying any documentation, thoroughly investigate the request and the
surrounding context.
1. **Clarify:** Understand the core request. Differentiate between writing new
content and editing existing content. If the request is ambiguous (e.g.,
"fix the docs"), ask for clarification.
2. **Investigate:** Examine relevant code (primarily in `packages/`) for
accuracy.
3. **Audit:** Read the latest versions of relevant files in `docs/`.
4. **Connect:** Identify all referencing pages if changing behavior. Check if
`docs/sidebar.json` needs updates.
5. **Plan:** Create a step-by-step plan before making changes.
6. **Audit Docset:** If asked to audit the documentation, follow the procedural
guide in [docs-auditing.md](./references/docs-auditing.md).
## Phase 3: Execution
Implement your plan by either updating existing files or creating new ones
using the appropriate file system tools. Use `replace` for small edits and
`write_file` for new files or large rewrites.
### Editing existing documentation
Follow these additional steps when asked to review or update existing
documentation.
- **Gaps:** Identify areas where the documentation is incomplete or no longer
reflects existing code.
- **Structure:** Apply "Structure (New Docs)" rules (BLUF, headings, etc.) when
adding new sections to existing pages.
- **Headers**: If you change a header, you must check for links that lead to
that header and update them.
- **Tone:** Ensure the tone is active and engaging. Use "you" and contractions.
- **Clarity:** Correct awkward wording, spelling, and grammar. Rephrase
sentences to make them easier for users to understand.
- **Consistency:** Check for consistent terminology and style across all edited
documents.
## Phase 4: Verification and finalization
Perform a final quality check to ensure that all changes are correctly
formatted and that all links are functional.
1. **Accuracy:** Ensure content accurately reflects the implementation and
technical behavior.
2. **Self-review:** Re-read changes for formatting, correctness, and flow.
3. **Link check:** Verify all new and existing links leading to or from
modified pages. If you changed a header, ensure that any links that lead to
it are updated.
4. **Format:** If `npm run format` fails, it may be necessary to run `npm
install` first to ensure all formatting dependencies are available. Once all
changes are complete, ask to execute `npm run format` to ensure consistent
formatting across the project. If the user confirms, execute the command.
@@ -0,0 +1,61 @@
# Style Guide: Quota vs. Limit
This guide defines the usage of "quota," "limit," and related terms in
user-facing interfaces.
## TL;DR
- **`quota`**: The administrative "bucket." Use for settings, billing, and
requesting increases. (e.g., "Adjust your storage **quota**.")
- **`limit`**: The real-time numerical "ceiling." Use for error messages when a
user is blocked. (e.g., "You've reached your request **limit**.")
- **When blocked, combine them:** Explain the **limit** that was hit and the
**quota** that is the remedy. (e.g., "You've reached the request **limit** for
your developer **quota**.")
- **Related terms:** Use `usage` for consumption tracking, `restriction` for
fixed rules, and `reset` for when a limit refreshes.
---
## Detailed Guidelines
### Definitions
- **Quota is the "what":** It identifies the category of resource being managed
(e.g., storage quota, GPU quota, request/prompt quota).
- **Limit is the "how much":** It defines the numerical boundary.
Use **quota** when referring to the administrative concept or the request for
more. Use **limit** when discussing the specific point of exhaustion.
### When to use "quota"
Use this term for **account management, billing, and settings.** It describes
the entitlement the user has purchased or been assigned.
**Examples:**
- **Navigation label:** Quota and usage
- **Contextual help:** Your **usage quota** is managed by your organization. To
request an increase, contact your administrator.
### When to use "limit"
Use this term for **real-time feedback, notifications, and error messages.** It
identifies the specific wall the user just hit.
**Examples:**
- **Error message:** Youve reached the 50-request-per-minute **limit**.
- **Inline warning:** Input exceeds the 32k token **limit**.
### How to use both together
When a user is blocked, combine both terms to explain the **event** (limit) and
the **remedy** (quota).
**Example:**
- **Heading:** Daily usage limit reached
- **Body:** You've reached the maximum daily capacity for your developer quota.
To continue working today, upgrade your quota.
@@ -0,0 +1,195 @@
# Procedural Guide: Auditing the Docset
This guide outlines the process for auditing the Gemini CLI documentation for
correctness and adherence to style guidelines. This process involves both an
"Editor" and "Technical Writer" phase.
## Objective
To ensure all public-facing documentation is accurate, up-to-date, adheres to
the Gemini CLI documentation style guide, and reflects the current state of the
codebase.
## Phase 1: Editor Audit
**Role:** The editor is responsible for identifying potential issues based on
style guide violations and technical inaccuracies.
### Steps
1. **Identify Documentation Scope:**
- Read `docs/sidebar.json` to get a list of all viewable documentation
pages.
- For each entry with a `slug`, convert it into a file path (e.g., `docs` ->
`docs/index.md`, `docs/get-started` -> `docs/get-started.md`). Ignore
entries with `link` properties.
2. **Prepare Audit Results File:**
- Create a new Markdown file named `audit-results-[YYYY-MM-DD].md` (e.g.,
`audit-results-2026-03-13.md`). This file will contain all identified
violations and recommendations.
3. **Retrieve Style Guidelines:**
- Familiarize yourself with the `docs-writer` skill instructions and the
included style guidelines.
4. **Audit Each Document:**
- For each documentation file identified in Step 1, read its content.
- **Review against Style Guide:**
- **Voice and Tone Violations:**
- **Unprofessional Tone:** Identify phrasing that is overly casual,
defensive, or lacks a professional and friendly demeanor.
- **Indirectness or Vagueness:** Identify sentences that are
unnecessarily wordy or fail to be concise and direct.
- **Incorrect Pronoun:** Identify any use of third-person pronouns
(e.g., "we," "they," "the user") when referring to the reader, instead
of the second-person pronoun **"you"**.
- **Passive Voice:** Identify sentences written in the passive voice.
- **Incorrect Tense:** Identify the use of past or future tense verbs,
instead of the **present tense**.
- **Poor Vocabulary:** Identify the use of jargon, slang, or overly
informal language.
- **Language and Grammar Violations:**
- **Lack of Conciseness:** Identify unnecessarily long phrases or
sentences.
- **Punctuation Errors:** Identify incorrect or missing punctuation.
- **Ambiguous Dates:** Identify dates that could be misinterpreted
(e.g., "next Monday" instead of "April 15, 2026").
- **Abbreviation Usage:** Identify the use of abbreviations that should
be spelled out (e.g., "e.g." instead of "for example").
- **Terminology:** Check for incorrect or inconsistent use of
product-specific terms (e.g., "quota" vs. "limit").
- **Formatting and Syntax Violations:**
- **Missing Overview:** Check for the absence of a brief overview
paragraph at the start of the document.
- **Line Length:** Identify any lines of text that exceed **80
characters** (text wrap violation).
- **Casing:** Identify incorrect casing for headings, titles, or named
entities (e.g., product names like `Gemini CLI`).
- **List Formatting:** Identify incorrectly formatted lists (e.g.,
inconsistent indentation or numbering).
- **Incorrect Emphasis:** Identify incorrect use of bold text (should
only be used for UI elements) or code font (should be used for code,
file names, or command-line input).
- **Link Quality:** Identify links with non-descriptive anchor text
(e.g., "click here").
- **Image Alt Text:** Identify images with missing or poor-quality
(non-descriptive) alt text.
- **Structure Violations:**
- **Missing BLUF:** Check for the absence of a "Bottom Line Up Front"
summary at the start of complex sections or documents.
- **Experimental Feature Notes:** Identify experimental features that
are not clearly labeled with a standard note.
- **Heading Hierarchy:** Check for skipped heading levels (e.g., going
from `##` to `####`).
- **Procedure Clarity:** Check for procedural steps that do not start
with an imperative verb or where a condition is placed _after_ the
instruction.
- **Element Misuse:** Identify the incorrect or inappropriate use of
special elements (e.g., Notes, Warnings, Cautions).
- **Table of Contents:** Identify the presence of a dynamically
generated or manually included table of contents.
- **Missing Next Steps:** Check for procedural documents that lack a
"Next steps" section (if applicable).
- **Verify Code Accuracy (if applicable):**
- If the document contains code snippets (e.g., shell commands, API calls,
file paths, Docker image versions), use `grep_search` and `read_file`
within the `packages/` directory (or other relevant parts of the
codebase) to ensure the code is still accurate and up-to-date. Pay close
attention to version numbers, package names, and command syntax.
- **Record Findings:** For each **violation** or inaccuracy found:
- Note the file path.
- Describe the violation (e.g., "Violation (Language and Grammar): Uses
'e.g.'").
- Provide a clear and actionable recommendation to correct the issue.
(e.g., "Recommendation: Replace 'e.g.' with 'for example'." or
"Recommendation: Replace '...' with '...' in active voice.).
- Append these findings to `audit-results-[YYYY-MM-DD].md`.
## Phase 2: Software Engineer Audit
**Role:** The software engineer is responsible for finding undocumented features
by auditing the codebase and recent changelogs, and passing these findings to
the technical writer.
### Steps
1. **Proactive Codebase Audit:**
- Audit high-signal areas of the codebase to identify undocumented features.
You MUST review:
- `packages/cli/src/commands/`
- `packages/core/src/tools/`
- `packages/cli/src/config/settings.ts`
2. **Review Recent Updates:**
- Check recent changelogs in stable and announcements within the
documentation to see if newly introduced features are documented properly.
3. **Evaluate and Record Findings:**
- Determine if these features are adequately covered in the docs. They do
not need to be documented word for word, but major features that customers
should care about probably should have an article.
- Append your findings to the `audit-results-[YYYY-MM-DD].md` file,
providing a brief description of the feature and where it should be
documented.
## Phase 3: Technical Writer Implementation
**Role:** The technical writer handles input from both the editor and the
software engineer, makes appropriate decisions about what to change, and
implements the approved changes.
### Steps
1. **Review Audit Results:**
- Read `audit-results-[YYYY-MM-DD].md` to understand all identified issues,
undocumented features, and recommendations from both the Editor and
Software Engineer phases.
2. **Make Decisions and Log Reasoning:**
- Create or update an implementation log (e.g.,
`audit-implementation-log-[YYYY-MM-DD].md`).
- Make sure the logs are updated for all steps, documenting your reasoning
for each recommendation (why it was accepted, modified, or rejected). This
is required for a final check by a human in the PR.
3. **Implement Changes:**
- For each approved recommendation:
- Read the target documentation file.
- Apply the recommended change using the `replace` tool. Pay close
attention to `old_string` for exact matches, including whitespace and
newlines. For multiple occurrences of the same simple string (e.g.,
"e.g."), use `allow_multiple: true`.
- **String replacement safeguards:** When applying these fixes across the
docset, you must verify the following:
- **Preserve Code Blocks:** Explicitly verify that no code blocks,
inline code snippets, terminal commands, or file paths have been
erroneously capitalized or modified.
- **Preserve Literal Strings:** Never alter the wording of literal error
messages, UI quotes, or system logs. For example, if a style rule says
to remove the word "please", you must NOT remove it if it appears
inside a quoted error message (e.g.,
`Error: Please contact your administrator`).
- **Verify Sentence Casing:** When removing filler words (like "please")
from the beginning of a sentence or list item, always verify that the
new first word of the sentence is properly capitalized.
- For structural changes (e.g., adding an overview paragraph), use
`replace` or `write_file` as appropriate.
- For broken links, determine the correct new path or update the link
text.
- For creating new files (e.g., `docs/get-started.md` to fix a broken
link, or a new feature article), use `write_file`.
4. **Execute Auto-Generation Scripts:**
- Some documentation pages are auto-generated from the codebase and should
be updated using npm scripts rather than manual edits. After implementing
manual changes (especially if you edited settings or configurations based
on SWE recommendations), ensure you run:
- `npm run docs:settings` to generate/update the configuration reference.
- `npm run docs:keybindings` to generate/update the keybindings reference.
5. **Format Code:**
- **Dependencies:** If `npm run format` fails, it may be necessary to run
`npm install` first to ensure all formatting dependencies are available.
- After all changes have been implemented, run `npm run format` to ensure
consistent formatting across the project.
@@ -0,0 +1,76 @@
---
name: github-issue-creator
description:
Use this skill when asked to create a GitHub issue. It handles different issue
types (bug, feature, etc.) using repository templates and ensures proper
labeling.
---
# GitHub Issue Creator
This skill guides the creation of high-quality GitHub issues that adhere to the
repository's standards and use the appropriate templates.
## Workflow
Follow these steps to create a GitHub issue:
1. **Identify Issue Type**: Determine if the request is a bug report, feature
request, or other category.
2. **Locate Template**: Search for issue templates in
`.github/ISSUE_TEMPLATE/`.
- `bug_report.yml`
- `feature_request.yml`
- `website_issue.yml`
- If no relevant YAML template is found, look for `.md` templates in the same
directory.
3. **Read Template**: Read the content of the identified template file to
understand the required fields.
4. **Draft Content**: Draft the issue title and body/fields.
- If using a YAML template (form), prepare values for each `id` defined in
the template.
- If using a Markdown template, follow its structure exactly.
- **Default Label**: Always include the `🔒 maintainer only` label unless the
user explicitly requests otherwise.
5. **Create Issue**: Use the `gh` CLI to create the issue.
- **CRITICAL:** To avoid shell escaping and formatting issues with
multi-line Markdown or complex text, ALWAYS write the description/body to
a temporary file first.
**For Markdown Templates or Simple Body:**
```bash
# 1. Write the drafted content to a temporary file
# 2. Create the issue using the --body-file flag
gh issue create --title "Succinct title" --body-file <temp_file_path> --label "🔒 maintainer only"
# 3. Remove the temporary file
rm <temp_file_path>
```
**For YAML Templates (Forms):**
While `gh issue create` supports `--body-file`, YAML forms usually expect
key-value pairs via flags if you want to bypass the interactive prompt.
However, the most reliable non-interactive way to ensure formatting is
preserved for long text fields is to use the `--body` or `--body-file` if the
form has been converted to a standard body, OR to use the `--field` flags
for YAML forms.
*Note: For the `gemini-cli` repository which uses YAML forms, you can often
submit the content as a single body if a specific field-based submission is
not required by the automation.*
6. **Verify**: Confirm the issue was created successfully and provide the link
to the user.
## Principles
- **Clarity**: Titles should be descriptive and follow project conventions.
- **Defensive Formatting**: Always use temporary files with `--body-file` to
prevent newline and special character issues.
- **Maintainer Priority**: Default to internal/maintainer labels to keep the
backlog organized.
- **Completeness**: Provide all requested information (e.g., version info,
reproduction steps).
@@ -0,0 +1,13 @@
---
name: pr-address-comments
description: Use this skill if the user asks you to help them address GitHub PR comments for their current branch of the Gemini CLI. Requires `gh` CLI tool.
---
You are helping the user address comments on their Pull Request. These comments may have come from an automated review agent or a team member.
OBJECTIVE: Help the user review and address comments on their PR.
# Comment Review Procedure
1. Run the `scripts/fetch-pr-info.js` script to get PR info and state. MAKE SURE you read the entire output of the command, even if it gets truncated.
2. Summarize the review status by analyzing the diff, commit log, and comments to see which still need to be addressed. Pay attention to the current user's comments. For resolved threads, summarize as a single line with a ✅. For open threads, provide a reference number e.g. [1] and the comment content.
3. Present your summary of the feedback and current state and allow the user to guide you as to what to fix/address/skip. DO NOT begin fixing issues automatically.
+160
View File
@@ -0,0 +1,160 @@
#!/usr/bin/env node
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/* eslint-env node */
/* global console, process */
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
const execAsync = promisify(exec);
async function run(cmd) {
try {
const { stdout } = await execAsync(cmd, {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
});
return stdout.trim();
} catch {
return null;
}
}
const IGNORE_MESSAGES = [
'thank you so much for your contribution to Gemini CLI!',
"I'm currently reviewing this pull request and will post my feedback shortly.",
'This pull request is being closed because it is not currently linked to an issue.',
];
const shouldIgnore = (body) => {
if (!body) return false;
return IGNORE_MESSAGES.some((msg) => body.includes(msg));
};
async function main() {
const branch = await run('git branch --show-current');
if (!branch) {
console.error('❌ Could not determine current git branch.');
process.exit(1);
}
const gqlQuery = `query($branch:String!){repository(name:"gemini-cli",owner:"google-gemini"){pullRequests(headRefName:$branch,first:100){nodes{id,number,state,comments(first:100){nodes{createdAt,isMinimized,minimizedReason,author{login},body,url,authorAssociation}},reviews(first:100){nodes{id,author{login},createdAt,isMinimized,minimizedReason,body,state,comments(first:30){nodes{id,replyTo{id},author{login},createdAt,body,isMinimized,minimizedReason,path,line,startLine,originalLine,originalStartLine}}}}}}}}`;
const [authInfo, diff, commits, rawJson] = await Promise.all([
run('gh auth status -a'),
run('gh pr diff'),
run(
'git fetch && git log origin/main..origin/$(git branch --show-current)',
),
run(`gh api graphql -F branch="${branch}" -f query='${gqlQuery}'`),
]);
if (!diff) {
console.error(`⚠️ No active PR found for branch: ${branch}`);
process.exit(1);
}
console.log(`\n# Current GitHub user info:\n\n${authInfo}\n`);
console.log(`\n# PR diff for current branch: ${branch}\n\n\`\`\``);
console.log(diff);
console.log('```');
console.log(
`\n# Commit history (origin/main..origin/${branch})\n\n${commits}`,
);
const data = JSON.parse(rawJson || '{}');
const prs = data?.data?.repository?.pullRequests?.nodes || [];
// Sort PRs by number descending so we check the newest one first
prs.sort((a, b) => b.number - a.number);
const pr = prs.find((p) => p.state === 'OPEN') || prs[0];
if (!pr) {
console.error('❌ No PR data found.');
process.exit(1);
}
console.log('\n# PR Feedback\n');
// 1. General PR Comments
const general = pr.comments.nodes.filter((c) => !shouldIgnore(c.body));
if (general.length > 0) {
console.log('\n💬 GENERAL COMMENTS:');
general.forEach((c) => {
const minimized = c.isMinimized
? ` (Minimized: ${c.minimizedReason})`
: '';
console.log(
`[${c.createdAt}] [${c.author.login}]${minimized}: ${c.body}\n`,
);
});
}
// 2. Process ALL Review Comments into a single Thread Map
const allInlineComments = pr.reviews.nodes.flatMap((r) => r.comments.nodes);
const filteredInlines = allInlineComments.filter(
(c) => !shouldIgnore(c.body),
);
console.log('🔍 CODE REVIEWS & INLINE THREADS:');
// Print Review Summaries First
pr.reviews.nodes.forEach((review) => {
if (review.body && !shouldIgnore(review.body)) {
const icon = review.state === 'APPROVED' ? '✅' : '💬';
const minimized = review.isMinimized
? ` (Minimized: ${review.minimizedReason})`
: '';
console.log(
`\n${icon} ${review.state} by ${review.author.login} at ${review.createdAt}${minimized}: "${review.body}"`,
);
}
});
// Build and Print Threads
const topLevelThreads = filteredInlines.filter((c) => !c.replyTo);
const printThread = (parentId, depth = 1) => {
const indent = ' '.repeat(depth);
filteredInlines
.filter((c) => c.replyTo?.id === parentId)
.forEach((reply) => {
const minimized = reply.isMinimized
? ` (Minimized: ${reply.minimizedReason})`
: '';
console.log(
`${indent}↳ [${reply.createdAt}] ${reply.author.login}${minimized}: ${reply.body}`,
);
printThread(reply.id, depth + 1);
});
};
topLevelThreads.forEach((c) => {
const start = c.startLine || c.originalStartLine;
const end = c.line || c.originalLine;
const range = start && end && start !== end ? `${start}-${end}` : end || '';
const fileInfo = c.path
? `(${c.path}${range ? `:${range}` : ''}) `
: range
? `(Line ${range}) `
: '';
const minimized = c.isMinimized ? ` (Minimized: ${c.minimizedReason})` : '';
console.log(
`\n💬 ${minimized}${c.author.login} | ${c.createdAt} ${fileInfo}\n${c.body}`,
);
printThread(c.id);
});
console.log('\n');
}
main().catch((err) => {
console.error('❌ Unexpected error:', err);
process.exit(1);
});
+93
View File
@@ -0,0 +1,93 @@
---
name: pr-creator
description:
Use this skill when asked to create a pull request (PR). It ensures all PRs
follow the repository's established templates and standards.
---
# Pull Request Creator
This skill guides the creation of high-quality Pull Requests that adhere to the
repository's standards.
## Workflow
Follow these steps to create a Pull Request:
1. **Branch Management**: **CRITICAL:** Ensure you are NOT working on the
`main` branch.
- Run `git branch --show-current`.
- If the current branch is `main`, you MUST create and switch to a new
descriptive branch:
```bash
git checkout -b <new-branch-name>
```
2. **Commit Changes**: Verify that all intended changes are committed.
- Run `git status` to check for unstaged or uncommitted changes.
- If there are uncommitted changes, stage and commit them with a descriptive
message before proceeding. NEVER commit directly to `main`.
```bash
git add .
git commit -m "type(scope): description"
```
3. **Locate Template**: Search for a pull request template in the repository.
- Check `.github/pull_request_template.md`
- Check `.github/PULL_REQUEST_TEMPLATE.md`
- If multiple templates exist (e.g., in `.github/PULL_REQUEST_TEMPLATE/`),
ask the user which one to use or select the most appropriate one based on
the context (e.g., `bug_fix.md` vs `feature.md`).
4. **Read Template**: Read the content of the identified template file.
5. **Draft Description**: Create a PR description that strictly follows the
template's structure.
- **Headings**: Keep all headings from the template.
- **Checklists**: Review each item. Mark with `[x]` if completed. If an item
is not applicable, leave it unchecked or mark as `[ ]` (depending on the
template's instructions) or remove it if the template allows flexibility
(but prefer keeping it unchecked for transparency).
- **Content**: Fill in the sections with clear, concise summaries of your
changes.
- **Related Issues**: Link any issues fixed or related to this PR (e.g.,
"Fixes #123").
6. **Preflight Check**: Before creating the PR, run the workspace preflight
script to ensure all build, lint, and test checks pass.
```bash
npm run preflight
```
If any checks fail, address the issues before proceeding to create the PR.
7. **Push Branch**: Push the current branch to the remote repository.
**CRITICAL SAFETY RAIL:** Double-check your branch name before pushing.
NEVER push if the current branch is `main`.
```bash
# Verify current branch is NOT main
git branch --show-current
# Push non-interactively
git push -u origin HEAD
```
8. **Create PR**: Use the `gh` CLI to create the PR. To avoid shell escaping
issues with multi-line Markdown, write the description to a temporary file
first.
```bash
# 1. Write the drafted description to a temporary file
# 2. Create the PR using the --body-file flag
gh pr create --title "type(scope): succinct description" --body-file <temp_file_path>
# 3. Remove the temporary file
rm <temp_file_path>
```
- **Title**: Ensure the title follows the
[Conventional Commits](https://www.conventionalcommits.org/) format if the
repository uses it (e.g., `feat(ui): add new button`,
`fix(core): resolve crash`).
## Principles
- **Safety First**: NEVER push to `main`. This is your highest priority.
- **Compliance**: Never ignore the PR template. It exists for a reason.
- **Completeness**: Fill out all relevant sections.
- **Accuracy**: Don't check boxes for tasks you haven't done.
@@ -0,0 +1,69 @@
---
name: review-duplication
description: Use this skill during code reviews to proactively investigate the codebase for duplicated functionality, reinvented wheels, or failure to reuse existing project best practices and shared utilities.
---
# Review Duplication
## Overview
This skill provides a structured workflow for investigating a codebase during a code review to identify duplicated logic, reinvented utilities, and missed opportunities to reuse established patterns. By executing this workflow, you ensure that new code integrates seamlessly with the existing project architecture.
## Workflow: Investigating for Duplication
When reviewing code, perform the following steps before finalizing your review:
### 1. Extract Core Logic
Analyze the new code to identify the core algorithms, utility functions, generic data structures, or UI components being introduced. Look beyond the specific business logic to see the underlying mechanics.
### 2. Hypothesize Existing Locations & Trace Dependencies
Think about where this type of code *would* live if it already existed in the project. Provide absolute paths from the repo root to disambiguate.
- **Utilities:** `packages/core/src/utils/`, `packages/cli/src/utils/`
- **UI Components:** `packages/cli/src/ui/components/`, `packages/cli/src/ui/`
- **Services:** `packages/core/src/services/`, `packages/cli/src/services/`
- **Configuration:** `packages/core/src/config/`, `packages/cli/src/config/`
- **Core Logic:** Call out `packages/core/` if functionality does not appear React UI specific.
**Trace Third-Party Dependencies:** If the PR introduces a new import for a utility library (e.g., `lodash.merge`, `date-fns`), trace how and where the project currently uses that library. There is likely an existing wrapper or shared utility.
**Check Package Files:** Before flagging a custom implementation of a complex algorithm, check `package.json` to see if a standard library (like `lodash` or `uuid`) is already installed that provides this functionality.
### 3. Investigate the Codebase (Sub-Agent Delegation)
Delegate the heavy lifting of codebase investigation to specialized sub-agents. They are optimized to perform deep searches and semantic mapping without bloating your session history.
To ensure a comprehensive review, you MUST formulate highly specific objectives for the sub-agents, providing them with the "scents" you discovered in Step 1.
- **Codebase Investigator:** Use the `codebase_investigator` as your primary researcher. When delegating, formulate an objective that asks specific, investigative questions about the codebase, explicitly including these search vectors:
- **Structural Similarity:** Ask if existing code uses the same underlying APIs (e.g., "Does any existing code use `Intl.DateTimeFormat` or `setTimeout` for similar purposes?").
- **Naming Conventions:** Ask if there are existing symbols with similar naming patterns (e.g., "Are there existing symbols with naming patterns like `*Format*` or `*Debounce*`?").
- **Comments & Documentation:** Ask if keywords from the PR's comments or JSDoc exist in describing similar behavior elsewhere.
- **Architectural Fit:** Ask where this type of logic is currently centralized (e.g., "Where is centralized date formatting logic located?").
- **Refactoring Guidance:** Crucially, ask the sub-agent to explain *how* the new code could be refactored to use any existing logic it finds.
- **Generalist Agent:** Use the `generalist` for detailed, turn-intensive comparisons. For example: "Review the implementation of `MyNewComponent` in the PR and compare it semantically against all components in `packages/ui/src`. Are there any existing components that could be extended or used instead?"
- **Retain Fast Path for Simple Searches:** For extremely simple, unambiguous checks (e.g., "Does `package.json` include `lodash`?"), perform a direct search to save time. Default to delegation for any open-ended "investigations."
### 4. Evaluate Best Practices
Check if the new code aligns with the project's established conventions.
- **Error Handling:** Does it use the project's standard error classes or logging mechanisms?
- **State Management:** Does it bypass established stores or contexts?
- **Styling:** Does it hardcode colors or spacing instead of using theme variables?
If the PR introduces a new pattern, compare it against the documented standards and explicitly confirm if an existing project pattern should have been used instead.
### 5. Formulate Constructive Feedback
If you discover that the PR duplicates existing functionality or ignores a best practice:
- Provide a clear review comment.
- **Identify the Source:** Explicitly mention the absolute or project-relative file path and the specific symbol (function, component, class) that should be reused.
- **Implementation Guidance:** Provide a brief code snippet or a clear explanation showing **how** to integrate the existing code to fulfill the task's requirements.
- **Explain the Value:** Briefly explain why reusing the existing code is beneficial (e.g., maintainability, consistency, built-in edge case handling).
Example comment:
> "It looks like this PR introduces a new `formatDate` utility. We already have a robust, tested `formatDate` function in `src/utils/dateHelpers.ts`.
>
> You can replace your implementation by importing it like this:
> ```typescript
> import { formatDate } from '../utils/dateHelpers';
>
> // Then use it here:
> const displayDate = formatDate(userDate, 'MMM Do, YYYY');
> ```
> Reusing this ensures that the date formatting remains consistent with the rest of the application and handles timezone conversions correctly."
+99
View File
@@ -0,0 +1,99 @@
---
name: string-reviewer
description: >
Use this skill when asked to review text and user-facing strings within the codebase. It ensures that these strings follow rules on clarity,
usefulness, brevity and style.
---
# String Reviewer
## Instructions
Act as a Senior UX Writer. Look for user-facing strings that are too long,
unclear, or inconsistent. This includes inline text, error messages, and other
user-facing text.
Do NOT automatically change strings without user approval. You must only suggest
changes and do not attempt to rewrite them directly unless the user explicitly
asks you to do so.
## Core voice principles
The system prioritizes deterministic clarity over conversational fluff. We
provide telemetry, not etiquette, ensuring the user retains absolute agency..
1. **Deterministic clarity:** Distinguish between certain system/service states
(Cloud Billing, IAM, the System) and probabilistic AI analysis (Gemini).
2. **System transparency:** Replace "Loading..." with active technical telemetry
(e.g., Tracing stack traces...). Keep status updates under 5 words.
3. **Front-loaded actionability:** Always use the [Goal] + [Action] pattern.
Lead with intent so users can scan left-to-right.
4. **Agentic error recovery:** Every error must be a pivot point. Pair failures
with one-click recovery commands or suggested prompts.
5. **Contextual humility:** Reserve disclaimers and "be careful" warnings for P0
(destructive/irreversible) tasks only. Stop warning-fatigue.
## The writing checklist
Use this checklist to audit UI strings and AI responses.
### Identity and voice
- **Eliminate the "I":** Remove all first-person pronouns (I, me, my, mine).
- **Subject attribution:** Refer to the AI as Gemini and the infrastructure as
the - system or the CLI.
- **Active voice:** Ensure the subject (Gemini or the system) is clearly
performing the action.
- **Ownership rule:** Use the system for execution (doing) and Gemini for
analysis (thinking)
### Structural scannability
- **The skip test:** Do the first 3 words describe the users intent? If not,
rewrite.
- **Goal-first sequence:** Use the template: [To Accomplish X] + [Do Y].
- **The 5-word rule:** Keep status updates and loading states under 5 words.
- **Telemetry over etiquette:** Remove polite filler (Please wait, Thank you,
Certainly). Replace with raw data or progress indicators.
- **Micro-state cycles:** For tasks $> 3$ seconds, cycle through specific
sub-states (e.g., Parsing logs... ➔ Identifying patterns...) to show momentum.
### Technical accuracy and humility
- **Verb signal check:** Use deterministic verbs (is, will, must) for system
state/infrastructure.
- Use probabilistic verbs (suggests, appears, may, identifies) for AI output.
- **No 100% certainty:** Never attribute absolute certainty to model-generated
content.
- **Precision over fuzziness:** Use technical metrics (latency, tokens, compute) instead of "speed" or "cost."
- **Instructional warnings:** Every warning must include a specific corrective action (e.g., "Perform a dry-run first" or "Review line 42").
### Agentic error recovery
- **The one-step rule:** Pair every error message with exactly one immediate
path to a fix (command, link, or prompt).
- **Human-first:** Provide a human-readable explanation before machine error
codes (e.g., 404, 500).
- **Suggested prompts:** Offer specific text for the user to copy/click like
“Ask Gemini: 'Explain this port error.'”
### Use consistent terminology
Ensure all terminology aligns with the project [word
list](./references/word-list.md).
If a string uses a term marked "do not use" or "use with caution," provide a
correction based on the preferred terms.
## Ensure consistent style for settings
If `packages/cli/src/config/settingsSchema.ts` is modified, confirm labels and
descriptions specifically follow the unique [Settings
guidelines](./references/settings.md).
## Output format
When suggesting changes, always present your review using the following list
format. Do not provide suggestions outside of this list..
```
1. **{Rationale/Principle Violated}**
- ❌ "{incorrect phrase}"
- ✅ `"{corrected phrase}"`
```
@@ -0,0 +1,28 @@
# Settings
## Noun-First Labeling (Scannability)
Labels must start with the subject of the setting, not the action. This allows
users to scan for the feature they want to change.
- **Rule:** `[Noun]` `[Attribute/Action]`
- **Example:** `Show line numbers` becomes simply `Line numbers`
## Positive Boolean Logic (Cognitive Ease)
Eliminate "double negatives." Booleans should represent the presence of a
feature, not its absence.
- **Rule:** Replace `Disable {feature}` or `Hide {Feature}` with
`{Feature} enabled` or simply `{Feature}`.
- **Example:** Change "Disable auto update" to "Auto update".
- **Implementation:** Invert the boolean value in your config loader so true
always equals `On`
## Verb Stripping (Brevity)
Remove redundant leading verbs like "Enable," "Use," "Display," or "Show" unless
they are part of a specific technical term.
- **Rule**: If the label works without the verb, remove it
- **Example**: Change `Enable prompt completion` to `Prompt completion`
@@ -0,0 +1,61 @@
## Terms
### Preferred
- Use **create** when a user is creating or setting up something.
- Use **allow** instead of **may** to indicate that permission has been granted
to perform some action.
- Use **canceled**, not **cancelled**.
- Use **configure** to refer to the process of changing the attributes of a
feature, even if that includes turning on or off the feature.
- Use **delete** when the action being performed is destructive.
- Use **enable** for binary operations that turn a feature or API on. Use "turn
on" and "turn off" instead of "enable" and "disable" for other situations.
- Use **key combination** to refer to pressing multiple keys simultaneously.
- Use **key sequence** to refer to pressing multiple keys separately in order.
- Use **modify** to refer to something that has changed vs obtaining the latest
version of something.
- Use **remove** when the action being performed takes an item out of a larger
whole, but doesn't destroy the item itself.
- Use **set up** as a verb. Use **setup** as a noun or adjective.
- Use **show**. In general, use paired with **hide**.
- Use **sign in**, **sign out** as a verb. Use **sign-in** or **sign-out** as a
noun or adjective.
- Use **update** when you mean to obtain the latest version of something.
- Use **want** instead of **like** or **would like**.
#### Don't use
- Don't use **etc.** It's redundant. To convey that a series is incomplete,
introduce it with "such as" instead.
- Don't use **hostname**, use "host name" instead.
- Don't use **in order to**. It's too formal. "Before you can" is usually better
in UI text.
- Don't use **one or more**. Specify the quantity where possible. Use "at least
one" when the quantity is 1+ but you can't be sure of the number. Likewise,
use "at least one" when the user must choose a quantity of 1+.
- Don't use the terms **log in**, **log on**, **login**, **logout** or **log
out**.
- Don't use **like** or **would you like**. Use **want** instead. Better yet,
rephrase so that it's not referring to the user's emotional state, but rather
what is required.
#### Use with caution
- Avoid using **leverage**, especially as a verb. "Leverage" is considered a
buzzword largely devoid of meaning apart from the simpler "use".
- Avoid using **once** as a synonym for "after". Typically, when "once" is used
in this way, it is followed by a verb in the perfect tense.
- Don't use **e.g.** Use "example", "such as", "like", or "for example". The
phrase is always followed by a comma.
- Don't use **i.e.** unless absolutely essential to make text fit. Use "that is"
instead.
- Use **disable** for binary operations that turn a feature or API off. Use
"turn on" and "turn off" instead of "enable" and "disable" for other
situations. For UI elements that are not available, use "dimmed" instead of
"disabled".
- Use **please** only when you're asking the user to do something inconvenient,
not just following the instructions in a typical flow.
- Use **really** sparingly in such constructions as "Do you really want to..."
Because of the weight it puts on the decision, it should be used to confirm
actions that the user is extremely unlikely to make.
+65
View File
@@ -0,0 +1,65 @@
---
name: tui-tester
description: Expert guidance for testing Gemini CLI behavior and visual output using terminal automation.
---
# TUI Tester Skill
This skill provides the operational manual for verifying Gemini CLI behavioral changes and visual output using terminal automation.
## Core Responsibilities
- **Verify Behavior**: Confirm that code changes result in the expected terminal interactions.
- **Visual Validation**: Ensure the TUI renders correctly across different terminal sizes and states.
- **Regression Testing**: Use automation to prevent breaking existing interactive workflows.
## Critical Protocol
When performing TUI testing, you must adhere to these strict rules:
### 1. Initialization
**YOUR ABSOLUTE FIRST ACTION MUST BE:**
Activate the `agent-tui` skill. This provides the underlying tools needed for terminal automation.
### 2. Environment Setup (macOS / Parallel Safe)
Ensure the global daemon is running and the live preview is open:
```bash
if ! agent-tui sessions >/dev/null 2>&1; then
tmux kill-session -t agent-tui 2>/dev/null || true
agent-tui daemon stop 2>/dev/null || true
rm -f /tmp/agent-tui*
tmux new-session -d -s agent-tui 'agent-tui daemon start --foreground > /tmp/agent-tui-daemon.log 2>&1'
sleep 1
fi
agent-tui live start --open
```
### 3. Session Management
- **Session IDs**: Always use the `session_id` returned by `agent-tui run` for subsequent interactions.
- **Atomic Execution**: Execute exactly one command per turn. Do not pipeline actions.
- **The Loop**: Action -> Wait -> Screenshot -> Verify -> Next Action.
### 4. Gemini CLI Specifics
- **Build First**: Always run `npm run build` or `npm run build:all` before testing local changes.
- **Bypass Trust**: Set `GEMINI_CLI_TRUST_WORKSPACE=true` to avoid focus-stealing modals.
- **Isolate Config**: Use `GEMINI_CLI_HOME` to prevent interference with your personal settings.
## Workflow Example
```bash
# Start the CLI
env GEMINI_CLI_TRUST_WORKSPACE=true agent-tui run node packages/cli/dist/index.js
# Wait for the prompt
agent-tui wait "│" --assert
# Send a command
agent-tui type "/help"
agent-tui press Enter
# Verify output
agent-tui wait "Available Commands" --assert
```
## Error Recovery
If a wait times out, take a fresh screenshot to diagnose the state. If you see `os error 61`, restart the daemon using the tmux method.
+2
View File
@@ -0,0 +1,2 @@
packages/core/src/services/scripts/*.exe
gha-creds-*.json
+24
View File
@@ -0,0 +1,24 @@
# Set the default behavior for all files to automatically handle line endings.
# This will ensure that all text files are normalized to use LF (line feed)
# line endings in the repository, which helps prevent cross-platform issues.
* text=auto eol=lf
# Explicitly declare files that must have LF line endings for proper execution
# on Unix-like systems.
*.sh eol=lf
*.bash eol=lf
Makefile eol=lf
# Explicitly declare binary file types to prevent Git from attempting to
# normalize their line endings.
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.pdf binary
*.woff binary
*.woff2 binary
*.eot binary
*.ttf binary
*.otf binary
+22
View File
@@ -0,0 +1,22 @@
# By default, require reviews from the maintainers for all files.
* @google-gemini/gemini-cli-maintainers
# Require reviews from the release approvers for critical files.
# These patterns override the rule above.
/package.json @google-gemini/gemini-cli-askmode-approvers
/package-lock.json @google-gemini/gemini-cli-askmode-approvers
/GEMINI.md @google-gemini/gemini-cli-askmode-approvers
/SECURITY.md @google-gemini/gemini-cli-askmode-approvers
/LICENSE @google-gemini/gemini-cli-askmode-approvers
/.github/workflows/ @google-gemini/gemini-cli-askmode-approvers
/packages/cli/package.json @google-gemini/gemini-cli-askmode-approvers
/packages/core/package.json @google-gemini/gemini-cli-askmode-approvers
# Docs have a dedicated approver group in addition to maintainers
/docs/ @google-gemini/gemini-cli-maintainers @google-gemini/gemini-cli-docs
/README.md @google-gemini/gemini-cli-maintainers @google-gemini/gemini-cli-docs
# Prompt contents, tool definitions, and evals require reviews from prompt approvers
/packages/core/src/prompts/ @google-gemini/gemini-cli-prompt-approvers
/packages/core/src/tools/ @google-gemini/gemini-cli-prompt-approvers
/evals/ @google-gemini/gemini-cli-prompt-approvers
+57
View File
@@ -0,0 +1,57 @@
name: 'Bug Report'
description: 'Report a bug to help us improve Gemini CLI'
body:
- type: 'markdown'
attributes:
value: |-
> [!IMPORTANT]
> Thanks for taking the time to fill out this bug report!
>
> Please search **[existing issues](https://github.com/google-gemini/gemini-cli/issues)** to see if an issue already exists for the bug you encountered.
- type: 'textarea'
id: 'problem'
attributes:
label: 'What happened?'
description: 'A clear and concise description of what the bug is.'
validations:
required: true
- type: 'textarea'
id: 'expected'
attributes:
label: 'What did you expect to happen?'
validations:
required: true
- type: 'textarea'
id: 'info'
attributes:
label: 'Client information'
description: 'Please paste the full text from the `/about` command run from Gemini CLI. Also include which platform (macOS, Windows, Linux). Note that this output contains your email address. Consider removing it before submitting.'
value: |-
<details>
<summary>Client Information</summary>
Run `gemini` to enter the interactive CLI, then run the `/about` command.
```console
> /about
# paste output here
```
</details>
validations:
required: true
- type: 'textarea'
id: 'login-info'
attributes:
label: 'Login information'
description: 'Describe how you are logging in (e.g., Google Account, API key).'
- type: 'textarea'
id: 'additional-context'
attributes:
label: 'Anything else we need to know?'
description: 'Add any other context about the problem here.'
@@ -0,0 +1,35 @@
name: 'Feature Request'
description: 'Suggest an idea for this project'
labels:
- 'status/need-triage'
type: 'Feature'
body:
- type: 'markdown'
attributes:
value: |-
> [!IMPORTANT]
> Thanks for taking the time to suggest an enhancement!
>
> Please search **[existing issues](https://github.com/google-gemini/gemini-cli/issues)** to see if a similar feature has already been requested.
- type: 'textarea'
id: 'feature'
attributes:
label: 'What would you like to be added?'
description: 'A clear and concise description of the enhancement.'
validations:
required: true
- type: 'textarea'
id: 'rationale'
attributes:
label: 'Why is this needed?'
description: 'A clear and concise description of why this enhancement is needed.'
validations:
required: true
- type: 'textarea'
id: 'additional-context'
attributes:
label: 'Additional context'
description: 'Add any other context or screenshots about the feature request here.'
+42
View File
@@ -0,0 +1,42 @@
name: 'Website issue'
description: 'Report an issue with the Gemini CLI Website and Gemini CLI Extensions Gallery'
title: 'GeminiCLI.com Feedback: [ISSUE]'
labels:
- 'area/extensions'
- 'area/documentation'
body:
- type: 'markdown'
attributes:
value: |-
> [!IMPORTANT]
> Thanks for taking the time to report an issue with the Gemini CLI Website
>
> Please search **[existing issues](https://github.com/google-gemini/gemini-cli/issues?q=is%3Aissue+is%3Aopen+label%3Aarea%2Fwebsite)** to see if a similar feature has already been requested.
- type: 'input'
id: 'url'
attributes:
label: 'URL of the page with the issue'
description: 'Please provide the URL where the issue occurs.'
validations:
required: true
- type: 'textarea'
id: 'problem'
attributes:
label: 'What is the problem?'
description: 'A clear and concise description of what the bug or issue is.'
validations:
required: true
- type: 'textarea'
id: 'expected'
attributes:
label: 'What did you expect to happen?'
validations:
required: true
- type: 'textarea'
id: 'additional-context'
attributes:
label: 'Additional context'
description: 'Add any other context or screenshots about the issue here.'
+27
View File
@@ -0,0 +1,27 @@
name: 'Calculate vars'
description: 'Calculate commonly used var in our release process'
inputs:
dry_run:
description: 'Whether or not this is a dry run'
type: 'boolean'
outputs:
is_dry_run:
description: 'Boolean flag indicating if the current run is a dry-run or a production release.'
value: '${{ steps.set_vars.outputs.is_dry_run }}'
runs:
using: 'composite'
steps:
- name: 'Set vars for simplified logic'
id: 'set_vars'
shell: 'bash'
env:
DRY_RUN_INPUT: '${{ inputs.dry_run }}'
run: |-
is_dry_run="true"
if [[ "${DRY_RUN_INPUT}" == "" || "${DRY_RUN_INPUT}" == "false" ]]; then
is_dry_run="false"
fi
echo "is_dry_run=${is_dry_run}" >> "${GITHUB_OUTPUT}"
@@ -0,0 +1,55 @@
name: 'Create Pull Request'
description: 'Creates a pull request.'
inputs:
branch-name:
description: 'The name of the branch to create the PR from.'
required: true
pr-title:
description: 'The title of the pull request.'
required: true
pr-body:
description: 'The body of the pull request.'
required: true
base-branch:
description: 'The branch to merge into.'
required: true
default: 'main'
github-token:
description: 'The GitHub token to use for creating the pull request.'
required: true
dry-run:
description: 'Whether to run in dry-run mode.'
required: false
default: 'false'
working-directory:
description: 'The working directory to run the commands in.'
required: false
default: '.'
runs:
using: 'composite'
steps:
- name: 'Creates a Pull Request'
if: "inputs.dry-run != 'true'"
env:
GH_TOKEN: '${{ inputs.github-token }}'
INPUTS_BRANCH_NAME: '${{ inputs.branch-name }}'
INPUTS_PR_TITLE: '${{ inputs.pr-title }}'
INPUTS_PR_BODY: '${{ inputs.pr-body }}'
INPUTS_BASE_BRANCH: '${{ inputs.base-branch }}'
shell: 'bash'
working-directory: '${{ inputs.working-directory }}'
run: |
set -e
if ! git ls-remote --exit-code --heads origin "${INPUTS_BRANCH_NAME}"; then
echo "::error::Branch '${INPUTS_BRANCH_NAME}' does not exist on the remote repository."
exit 1
fi
PR_URL=$(gh pr create \
--title "${INPUTS_PR_TITLE}" \
--body "${INPUTS_PR_BODY}" \
--base "${INPUTS_BASE_BRANCH}" \
--head "${INPUTS_BRANCH_NAME}" \
--fill)
gh pr merge "$PR_URL" --auto
@@ -0,0 +1,23 @@
name: 'Download Mac Binaries'
description: 'Downloads the unsigned macOS binaries (x64 and arm64)'
inputs:
path:
description: 'The base path to download the binaries to'
required: true
default: 'dist'
runs:
using: 'composite'
steps:
- name: 'Download macOS arm64 binary'
uses: 'actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806' # ratchet:actions/download-artifact@v4
continue-on-error: true
with:
name: 'gemini-darwin-arm64-unsigned'
path: '${{ inputs.path }}/darwin-arm64'
- name: 'Download macOS x64 binary'
uses: 'actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806' # ratchet:actions/download-artifact@v4
continue-on-error: true
with:
name: 'gemini-darwin-x64-unsigned'
path: '${{ inputs.path }}/darwin-x64'
+51
View File
@@ -0,0 +1,51 @@
name: 'NPM Auth Token'
description: 'Generates an NPM auth token for publishing a specific package'
inputs:
package-name:
description: 'The name of the package to publish'
required: true
github-token:
description: 'the github token'
required: true
wombat-token-core:
description: 'The npm token for the cli-core package.'
required: true
wombat-token-cli:
description: 'The npm token for the cli package.'
required: true
wombat-token-a2a-server:
description: 'The npm token for the a2a package.'
required: true
outputs:
auth-token:
description: 'The generated NPM auth token'
value: '${{ steps.npm_auth_token.outputs.auth-token }}'
runs:
using: 'composite'
steps:
- name: 'Generate NPM Auth Token'
id: 'npm_auth_token'
shell: 'bash'
run: |
AUTH_TOKEN="${INPUTS_GITHUB_TOKEN}"
PACKAGE_NAME="${INPUTS_PACKAGE_NAME}"
PRIVATE_REPO="@google-gemini/"
if [[ "$PACKAGE_NAME" == "$PRIVATE_REPO"* ]]; then
AUTH_TOKEN="${INPUTS_GITHUB_TOKEN}"
elif [[ "$PACKAGE_NAME" == "@google/gemini-cli" ]]; then
AUTH_TOKEN="${INPUTS_WOMBAT_TOKEN_CLI}"
elif [[ "$PACKAGE_NAME" == "@google/gemini-cli-core" ]]; then
AUTH_TOKEN="${INPUTS_WOMBAT_TOKEN_CORE}"
elif [[ "$PACKAGE_NAME" == "@google/gemini-cli-a2a-server" ]]; then
AUTH_TOKEN="${INPUTS_WOMBAT_TOKEN_A2A_SERVER}"
fi
echo "auth-token=$AUTH_TOKEN" >> $GITHUB_OUTPUT
env:
INPUTS_GITHUB_TOKEN: '${{ inputs.github-token }}'
INPUTS_PACKAGE_NAME: '${{ inputs.package-name }}'
INPUTS_WOMBAT_TOKEN_CLI: '${{ inputs.wombat-token-cli }}'
INPUTS_WOMBAT_TOKEN_CORE: '${{ inputs.wombat-token-core }}'
INPUTS_WOMBAT_TOKEN_A2A_SERVER: '${{ inputs.wombat-token-a2a-server }}'
@@ -0,0 +1,114 @@
name: 'Post Coverage Comment Action'
description: 'Prepares and posts a code coverage comment to a PR.'
inputs:
cli_json_file:
description: 'Path to CLI coverage-summary.json'
required: true
core_json_file:
description: 'Path to Core coverage-summary.json'
required: true
cli_full_text_summary_file:
description: 'Path to CLI full-text-summary.txt'
required: true
core_full_text_summary_file:
description: 'Path to Core full-text-summary.txt'
required: true
node_version:
description: 'Node.js version for context in messages'
required: true
os:
description: 'The os for context in messages'
required: true
github_token:
description: 'GitHub token for posting comments'
required: true
runs:
using: 'composite'
steps:
- name: 'Prepare Coverage Comment'
id: 'prep_coverage_comment'
shell: 'bash'
env:
CLI_JSON_FILE: '${{ inputs.cli_json_file }}'
CORE_JSON_FILE: '${{ inputs.core_json_file }}'
CLI_FULL_TEXT_SUMMARY_FILE: '${{ inputs.cli_full_text_summary_file }}'
CORE_FULL_TEXT_SUMMARY_FILE: '${{ inputs.core_full_text_summary_file }}'
COMMENT_FILE: 'coverage-comment.md'
NODE_VERSION: '${{ inputs.node_version }}'
OS: '${{ inputs.os }}'
run: |-
# Extract percentages using jq for the main table
if [ -f "${CLI_JSON_FILE}" ]; then
cli_lines_pct="$(jq -r '.total.lines.pct' "${CLI_JSON_FILE}")"
cli_statements_pct="$(jq -r '.total.statements.pct' "${CLI_JSON_FILE}")"
cli_functions_pct="$(jq -r '.total.functions.pct' "${CLI_JSON_FILE}")"
cli_branches_pct="$(jq -r '.total.branches.pct' "${CLI_JSON_FILE}")"
else
cli_lines_pct="N/A"
cli_statements_pct="N/A"
cli_functions_pct="N/A"
cli_branches_pct="N/A"
echo "CLI coverage-summary.json not found at: ${CLI_JSON_FILE}" >&2 # Error to stderr
fi
if [ -f "${CORE_JSON_FILE}" ]; then
core_lines_pct="$(jq -r '.total.lines.pct' "${CORE_JSON_FILE}")"
core_statements_pct="$(jq -r '.total.statements.pct' "${CORE_JSON_FILE}")"
core_functions_pct="$(jq -r '.total.functions.pct' "${CORE_JSON_FILE}")"
core_branches_pct="$(jq -r '.total.branches.pct' "${CORE_JSON_FILE}")"
else
core_lines_pct="N/A"
core_statements_pct="N/A"
core_functions_pct="N/A"
core_branches_pct="N/A"
echo "Core coverage-summary.json not found at: ${CORE_JSON_FILE}" >&2 # Error to stderr
fi
echo "## Code Coverage Summary" > "${COMMENT_FILE}"
echo "" >> "${COMMENT_FILE}"
echo "| Package | Lines | Statements | Functions | Branches |" >> "${COMMENT_FILE}"
echo "|---|---|---|---|---|" >> "${COMMENT_FILE}"
echo "| CLI | ${cli_lines_pct}% | ${cli_statements_pct}% | ${cli_functions_pct}% | ${cli_branches_pct}% |" >> "${COMMENT_FILE}"
echo "| Core | ${core_lines_pct}% | ${core_statements_pct}% | ${core_functions_pct}% | ${core_branches_pct}% |" >> "${COMMENT_FILE}"
echo "" >> "${COMMENT_FILE}"
# CLI Package - Collapsible Section (with full text summary from file)
echo "<details>" >> "${COMMENT_FILE}"
echo "<summary>CLI Package - Full Text Report</summary>" >> "${COMMENT_FILE}"
echo "" >> "${COMMENT_FILE}"
echo '```text' >> "${COMMENT_FILE}"
if [ -f "${CLI_FULL_TEXT_SUMMARY_FILE}" ]; then
cat "${CLI_FULL_TEXT_SUMMARY_FILE}" >> "${COMMENT_FILE}"
else
echo "CLI full-text-summary.txt not found at: ${CLI_FULL_TEXT_SUMMARY_FILE}" >> "${COMMENT_FILE}"
fi
echo '```' >> "${COMMENT_FILE}"
echo "</details>" >> "${COMMENT_FILE}"
echo "" >> "${COMMENT_FILE}"
# Core Package - Collapsible Section (with full text summary from file)
echo "<details>" >> "${COMMENT_FILE}"
echo "<summary>Core Package - Full Text Report</summary>" >> "${COMMENT_FILE}"
echo "" >> "${COMMENT_FILE}"
echo '```text' >> "${COMMENT_FILE}"
if [ -f "${CORE_FULL_TEXT_SUMMARY_FILE}" ]; then
cat "${CORE_FULL_TEXT_SUMMARY_FILE}" >> "${COMMENT_FILE}"
else
echo "Core full-text-summary.txt not found at: ${CORE_FULL_TEXT_SUMMARY_FILE}" >> "${COMMENT_FILE}"
fi
echo '```' >> "${COMMENT_FILE}"
echo "</details>" >> "${COMMENT_FILE}"
echo "" >> "${COMMENT_FILE}"
echo "_For detailed HTML reports, please see the 'coverage-reports-${NODE_VERSION}-${OS}' artifact from the main CI run._" >> "${COMMENT_FILE}"
- name: 'Post Coverage Comment'
uses: 'thollander/actions-comment-pull-request@65f9e5c9a1f2cd378bd74b2e057c9736982a8e74' # ratchet:thollander/actions-comment-pull-request@v3
if: |-
${{ always() }}
with:
file-path: 'coverage-comment.md' # Use the generated file directly
comment-tag: 'code-coverage-summary'
github-token: '${{ inputs.github_token }}'
+359
View File
@@ -0,0 +1,359 @@
name: 'Publish Release'
description: 'Builds, prepares, and publishes the gemini-cli packages to npm and creates a GitHub release.'
inputs:
release-version:
description: 'The version to release (e.g., 0.1.11).'
required: true
npm-tag:
description: 'The npm tag to publish with (e.g., latest, preview, nightly).'
required: true
wombat-token-core:
description: 'The npm token for the cli-core package.'
required: true
wombat-token-cli:
description: 'The npm token for the cli package.'
required: true
wombat-token-a2a-server:
description: 'The npm token for the a2a package.'
required: true
github-token:
description: 'The GitHub token for creating the release.'
required: true
github-release-token:
description: 'The GitHub token used specifically for creating the GitHub release (to trigger other workflows).'
required: false
dry-run:
description: 'Whether to run in dry-run mode.'
type: 'string'
required: true
release-tag:
description: 'The release tag for the release (e.g., v0.1.11).'
required: true
previous-tag:
description: 'The previous tag to use for generating release notes.'
required: true
skip-github-release:
description: 'Whether to skip creating a GitHub release.'
type: 'boolean'
required: false
default: false
working-directory:
description: 'The working directory to run the steps in.'
required: false
default: '.'
force-skip-tests:
description: 'Skip tests and validation'
required: false
default: false
skip-branch-cleanup:
description: 'Whether to skip cleaning up the release branch.'
type: 'boolean'
required: false
default: false
gemini_api_key:
description: 'The API key for running integration tests.'
required: true
npm-registry-publish-url:
description: 'npm registry publish url'
required: true
npm-registry-url:
description: 'npm registry url'
required: true
npm-registry-scope:
description: 'npm registry scope'
required: true
cli-package-name:
description: 'The name of the cli package.'
required: true
core-package-name:
description: 'The name of the core package.'
required: true
a2a-package-name:
description: 'The name of the a2a package.'
required: true
runs:
using: 'composite'
steps:
- name: '👤 Configure Git User'
working-directory: '${{ inputs.working-directory }}'
shell: 'bash'
run: |
git config user.name "gemini-cli-robot"
git config user.email "gemini-cli-robot@google.com"
- name: '🌿 Create and switch to a release branch'
working-directory: '${{ inputs.working-directory }}'
id: 'release_branch'
shell: 'bash'
run: |
BRANCH_NAME="release/${INPUTS_RELEASE_TAG}"
git switch -c "${BRANCH_NAME}"
echo "BRANCH_NAME=${BRANCH_NAME}" >> "${GITHUB_OUTPUT}"
env:
INPUTS_RELEASE_TAG: '${{ inputs.release-tag }}'
- name: '⬆️ Update package versions'
working-directory: '${{ inputs.working-directory }}'
shell: 'bash'
run: |
npm run release:version "${INPUTS_RELEASE_VERSION}"
env:
INPUTS_RELEASE_VERSION: '${{ inputs.release-version }}'
- name: '💾 Commit and Conditionally Push package versions'
working-directory: '${{ inputs.working-directory }}'
shell: 'bash'
env:
BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
DRY_RUN: '${{ inputs.dry-run }}'
RELEASE_TAG: '${{ inputs.release-tag }}'
GIT_PUSH_TOKEN: '${{ inputs.github-release-token || inputs.github-token }}'
run: |-
set -e
git add package.json package-lock.json packages/*/package.json
git commit -m "chore(release): ${RELEASE_TAG}"
if [[ "${DRY_RUN}" == "false" ]]; then
echo "Pushing release branch to remote..."
git push "https://x-access-token:${GIT_PUSH_TOKEN}@github.com/${{ github.repository }}.git" "HEAD:${BRANCH_NAME}" --follow-tags
else
echo "Dry run enabled. Skipping push."
fi
- name: '🛠️ Build and Prepare Packages'
working-directory: '${{ inputs.working-directory }}'
shell: 'bash'
run: |
npm run build:packages
npm run prepare:package
- name: '🎁 Bundle'
working-directory: '${{ inputs.working-directory }}'
shell: 'bash'
run: |
npm run bundle
# TODO: Refactor this github specific publishing script to be generalized based upon inputs.
- name: '📦 Prepare for GitHub release'
if: "inputs.npm-registry-url == 'https://npm.pkg.github.com/'"
working-directory: '${{ inputs.working-directory }}'
shell: 'bash'
run: |
node ${{ github.workspace }}/scripts/prepare-github-release.js
- name: 'Configure npm for publishing to npm'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
with:
node-version-file: '${{ inputs.working-directory }}/.nvmrc'
registry-url: '${{inputs.npm-registry-publish-url}}'
scope: '${{inputs.npm-registry-scope}}'
- name: 'Get core Token'
uses: './.github/actions/npm-auth-token'
id: 'core-token'
with:
package-name: '${{ inputs.core-package-name }}'
github-token: '${{ inputs.github-token }}'
wombat-token-core: '${{ inputs.wombat-token-core }}'
wombat-token-cli: '${{ inputs.wombat-token-cli }}'
wombat-token-a2a-server: '${{ inputs.wombat-token-a2a-server }}'
- name: '📦 Publish CORE to NPM'
working-directory: '${{ inputs.working-directory }}'
env:
NODE_AUTH_TOKEN: '${{ steps.core-token.outputs.auth-token }}'
INPUTS_DRY_RUN: '${{ inputs.dry-run }}'
INPUTS_CORE_PACKAGE_NAME: '${{ inputs.core-package-name }}'
shell: 'bash'
run: |
npm publish \
--ignore-scripts \
--dry-run="${INPUTS_DRY_RUN}" \
--workspace="${INPUTS_CORE_PACKAGE_NAME}" \
--tag staging-tmp
if [[ "${INPUTS_DRY_RUN}" == "false" ]]; then
npm dist-tag rm ${INPUTS_CORE_PACKAGE_NAME} staging-tmp
fi
- name: '🔗 Install latest core package'
working-directory: '${{ inputs.working-directory }}'
if: "${{ inputs.dry-run != 'true' }}"
shell: 'bash'
run: |
npm install "${INPUTS_CORE_PACKAGE_NAME}@${INPUTS_RELEASE_VERSION}" \
--workspace="${INPUTS_CLI_PACKAGE_NAME}" \
--workspace="${INPUTS_A2A_PACKAGE_NAME}" \
--save-exact
env:
INPUTS_CORE_PACKAGE_NAME: '${{ inputs.core-package-name }}'
INPUTS_RELEASE_VERSION: '${{ inputs.release-version }}'
INPUTS_CLI_PACKAGE_NAME: '${{ inputs.cli-package-name }}'
INPUTS_A2A_PACKAGE_NAME: '${{ inputs.a2a-package-name }}'
- name: '📦 Prepare bundled CLI for npm release'
if: "inputs.npm-registry-url != 'https://npm.pkg.github.com/'"
working-directory: '${{ inputs.working-directory }}'
shell: 'bash'
run: |
node ${{ github.workspace }}/scripts/prepare-npm-release.js
- name: '📦 Pack CLI for verification'
if: "inputs.dry-run != 'true' && inputs.force-skip-tests != 'true'"
working-directory: '${{ inputs.working-directory }}'
shell: 'bash'
run: |
npm pack --workspace="${INPUTS_CLI_PACKAGE_NAME}"
# We restore the package.json so that `npm ci` in verify-release doesn't fail due to deleted dependencies
git checkout packages/cli/package.json
env:
INPUTS_CLI_PACKAGE_NAME: '${{ inputs.cli-package-name }}'
- name: '🔬 Verify NPM release by version'
uses: './.github/actions/verify-release'
if: "${{ inputs.dry-run != 'true' && inputs.force-skip-tests != 'true' }}"
with:
npm-package: './google-gemini-cli-${{ inputs.release-version }}.tgz'
expected-version: '${{ inputs.release-version }}'
working-directory: '${{ inputs.working-directory }}'
gemini_api_key: '${{ inputs.gemini_api_key }}'
github-token: '${{ inputs.github-token }}'
npm-registry-url: '${{ inputs.npm-registry-url }}'
npm-registry-scope: '${{ inputs.npm-registry-scope }}'
- name: 'Get CLI Token'
uses: './.github/actions/npm-auth-token'
id: 'cli-token'
with:
package-name: '${{ inputs.cli-package-name }}'
github-token: '${{ inputs.github-token }}'
wombat-token-core: '${{ inputs.wombat-token-core }}'
wombat-token-cli: '${{ inputs.wombat-token-cli }}'
wombat-token-a2a-server: '${{ inputs.wombat-token-a2a-server }}'
- name: '📦 Publish CLI'
working-directory: '${{ inputs.working-directory }}'
env:
NODE_AUTH_TOKEN: '${{ steps.cli-token.outputs.auth-token }}'
INPUTS_DRY_RUN: '${{ inputs.dry-run }}'
INPUTS_CLI_PACKAGE_NAME: '${{ inputs.cli-package-name }}'
INPUTS_RELEASE_VERSION: '${{ inputs.release-version }}'
shell: 'bash'
run: |
if [ -f "google-gemini-cli-${INPUTS_RELEASE_VERSION}.tgz" ]; then
PUBLISH_TARGET="google-gemini-cli-${INPUTS_RELEASE_VERSION}.tgz"
else
PUBLISH_TARGET="--workspace=${INPUTS_CLI_PACKAGE_NAME}"
fi
npm publish \
--ignore-scripts \
--dry-run="${INPUTS_DRY_RUN}" \
${PUBLISH_TARGET} \
--tag staging-tmp
if [[ "${INPUTS_DRY_RUN}" == "false" ]]; then
npm dist-tag rm ${INPUTS_CLI_PACKAGE_NAME} staging-tmp
fi
- name: 'Get a2a-server Token'
uses: './.github/actions/npm-auth-token'
id: 'a2a-token'
with:
package-name: '${{ inputs.a2a-package-name }}'
github-token: '${{ inputs.github-token }}'
wombat-token-core: '${{ inputs.wombat-token-core }}'
wombat-token-cli: '${{ inputs.wombat-token-cli }}'
wombat-token-a2a-server: '${{ inputs.wombat-token-a2a-server }}'
- name: '📦 Publish a2a'
working-directory: '${{ inputs.working-directory }}'
env:
NODE_AUTH_TOKEN: '${{ steps.a2a-token.outputs.auth-token }}'
INPUTS_DRY_RUN: '${{ inputs.dry-run }}'
INPUTS_A2A_PACKAGE_NAME: '${{ inputs.a2a-package-name }}'
shell: 'bash'
# Tag staging for initial release
run: |
npm publish \
--ignore-scripts \
--dry-run="${INPUTS_DRY_RUN}" \
--workspace="${INPUTS_A2A_PACKAGE_NAME}" \
--tag staging-tmp
if [[ "${INPUTS_DRY_RUN}" == "false" ]]; then
npm dist-tag rm ${INPUTS_A2A_PACKAGE_NAME} staging-tmp
fi
- name: '🏷️ Tag release'
uses: './.github/actions/tag-npm-release'
with:
channel: '${{ inputs.npm-tag }}'
version: '${{ inputs.release-version }}'
dry-run: '${{ inputs.dry-run }}'
github-token: '${{ inputs.github-token }}'
wombat-token-core: '${{ inputs.wombat-token-core }}'
wombat-token-cli: '${{ inputs.wombat-token-cli }}'
wombat-token-a2a-server: '${{ inputs.wombat-token-a2a-server }}'
cli-package-name: '${{ inputs.cli-package-name }}'
core-package-name: '${{ inputs.core-package-name }}'
a2a-package-name: '${{ inputs.a2a-package-name }}'
working-directory: '${{ inputs.working-directory }}'
- name: '🎉 Create GitHub Release'
working-directory: '${{ inputs.working-directory }}'
if: "${{ inputs.dry-run != 'true' && inputs.skip-github-release != 'true' && inputs.npm-tag != 'dev' && inputs.npm-registry-url != 'https://npm.pkg.github.com/' }}"
env:
GITHUB_TOKEN: '${{ inputs.github-release-token || inputs.github-token }}'
INPUTS_RELEASE_TAG: '${{ inputs.release-tag }}'
STEPS_RELEASE_BRANCH_OUTPUTS_BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
INPUTS_PREVIOUS_TAG: '${{ inputs.previous-tag }}'
shell: 'bash'
run: |
rm -f gemini-cli-bundle.zip
(cd bundle && chmod +x gemini.js && zip -r ../gemini-cli-bundle.zip .)
echo "Testing the generated bundle archive..."
rm -rf test-bundle
mkdir -p test-bundle
unzip -q gemini-cli-bundle.zip -d test-bundle
# Verify it runs and outputs a version
BUNDLE_VERSION=$(node test-bundle/gemini.js --version | xargs)
echo "Bundle version output: ${BUNDLE_VERSION}"
if [[ -z "${BUNDLE_VERSION}" ]]; then
echo "Error: Bundle failed to execute or return version."
exit 1
fi
rm -rf test-bundle
RELEASE_ASSETS=("gemini-cli-bundle.zip")
# Check for and prepare macOS binaries if they exist
for arch in arm64 x64; do
BINARY_PATH="dist/darwin-${arch}/gemini"
if [[ -f "$BINARY_PATH" ]]; then
chmod +x "$BINARY_PATH"
ZIP_NAME="gemini-darwin-${arch}-unsigned.zip"
zip -j "$ZIP_NAME" "$BINARY_PATH"
RELEASE_ASSETS+=("$ZIP_NAME")
fi
done
gh release create "${INPUTS_RELEASE_TAG}" \
"${RELEASE_ASSETS[@]}" \
--target "${STEPS_RELEASE_BRANCH_OUTPUTS_BRANCH_NAME}" \
--title "Release ${INPUTS_RELEASE_TAG}" \
--notes-start-tag "${INPUTS_PREVIOUS_TAG}" \
--generate-notes \
${{ inputs.npm-tag != 'latest' && '--prerelease' || '' }}
- name: '🧹 Clean up release branch'
working-directory: '${{ inputs.working-directory }}'
if: "${{ inputs.dry-run != 'true' && inputs.skip-branch-cleanup != 'true' }}"
continue-on-error: true
shell: 'bash'
run: |
echo "Cleaning up release branch ${STEPS_RELEASE_BRANCH_OUTPUTS_BRANCH_NAME}..."
git push "https://x-access-token:${GIT_PUSH_TOKEN}@github.com/${{ github.repository }}.git" --delete "${STEPS_RELEASE_BRANCH_OUTPUTS_BRANCH_NAME}"
env:
GIT_PUSH_TOKEN: '${{ inputs.github-release-token || inputs.github-token }}'
STEPS_RELEASE_BRANCH_OUTPUTS_BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
+75
View File
@@ -0,0 +1,75 @@
name: 'Push to docker'
description: 'Builds packages and pushes a docker image to GHCR'
inputs:
github-actor:
description: 'Github actor'
required: true
github-secret:
description: 'Github secret'
required: true
ref-name:
description: 'Github ref name'
required: true
github-sha:
description: 'Github Commit SHA Hash'
required: true
runs:
using: 'composite'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v4
with:
ref: '${{ inputs.github-sha }}'
fetch-depth: 0
- name: 'Install Dependencies'
shell: 'bash'
run: 'npm install'
- name: 'Set up Docker Buildx'
uses: 'docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435' # ratchet:docker/setup-buildx-action@v3
- name: 'build'
shell: 'bash'
run: 'npm run build'
- name: 'pack @google/gemini-cli'
shell: 'bash'
run: 'npm pack -w @google/gemini-cli --pack-destination ./packages/cli/dist'
- name: 'pack @google/gemini-cli-core'
shell: 'bash'
run: 'npm pack -w @google/gemini-cli-core --pack-destination ./packages/core/dist'
- name: 'Log in to GitHub Container Registry'
uses: 'docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1' # ratchet:docker/login-action@v3
with:
registry: 'ghcr.io'
username: '${{ inputs.github-actor }}'
password: '${{ inputs.github-secret }}'
- name: 'Get branch name'
id: 'branch_name'
shell: 'bash'
run: |
REF_NAME="${INPUTS_REF_NAME}"
echo "name=${REF_NAME%/merge}" >> $GITHUB_OUTPUT
env:
INPUTS_REF_NAME: '${{ inputs.ref-name }}'
- name: 'Build and Push the Docker Image'
uses: 'docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83' # ratchet:docker/build-push-action@v6
with:
context: '.'
file: './Dockerfile'
push: true
provenance: false # avoid pushing 3 images to Aritfact Registry
tags: |
ghcr.io/${{ github.repository }}/cli:${{ steps.branch_name.outputs.name }}
ghcr.io/${{ github.repository }}/cli:${{ inputs.github-sha }}
- name: 'Create issue on failure'
if: |-
${{ failure() }}
shell: 'bash'
env:
GITHUB_TOKEN: '${{ inputs.github-secret }}'
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
run: |-
gh issue create \
--title "Docker build failed" \
--body "The docker build failed. See the full run for details: ${DETAILS_URL}" \
--label "release-failure"
+114
View File
@@ -0,0 +1,114 @@
name: 'Build and push sandbox docker'
description: 'Pushes sandbox docker image to container registry'
inputs:
github-actor:
description: 'Github actor'
required: true
github-secret:
description: 'Github secret'
required: true
dockerhub-username:
description: 'Dockerhub username'
required: true
dockerhub-token:
description: 'Dockerhub PAT w/ R+W'
required: true
github-sha:
description: 'Github Commit SHA Hash'
required: true
github-ref-name:
description: 'Github ref name'
required: true
dry-run:
description: 'Whether this is a dry run.'
required: true
type: 'boolean'
runs:
using: 'composite'
steps:
- name: 'Checkout'
uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4
with:
ref: '${{ inputs.github-sha }}'
fetch-depth: 0
- name: 'Install Dependencies'
shell: 'bash'
run: 'npm install'
- name: 'npm build'
shell: 'bash'
run: 'npm run build'
- name: 'Set up QEMU'
uses: 'docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130' # ratchet:docker/setup-qemu-action@v3
- name: 'Set up Docker Buildx'
uses: 'docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f' # ratchet:docker/setup-buildx-action@v3
- name: 'Log in to GitHub Container Registry'
uses: 'docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9' # ratchet:docker/login-action@v3
with:
registry: 'docker.io'
username: '${{ inputs.dockerhub-username }}'
password: '${{ inputs.dockerhub-token }}'
- name: 'determine image tag'
id: 'image_tag'
shell: 'bash'
run: |-
SHELL_TAG_NAME="${INPUTS_GITHUB_REF_NAME}"
FINAL_TAG="${INPUTS_GITHUB_SHA}"
if [[ "$SHELL_TAG_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then
echo "Release detected."
FINAL_TAG="${SHELL_TAG_NAME#v}"
else
echo "Development release detected. Using commit SHA as tag."
fi
echo "Determined image tag: $FINAL_TAG"
echo "FINAL_TAG=$FINAL_TAG" >> $GITHUB_OUTPUT
env:
INPUTS_GITHUB_REF_NAME: '${{ inputs.github-ref-name }}'
INPUTS_GITHUB_SHA: '${{ inputs.github-sha }}'
# We build amd64 just so we can verify it.
# We build and push both amd64 and arm64 in the publish step.
- name: 'build'
id: 'docker_build'
shell: 'bash'
env:
GEMINI_SANDBOX_IMAGE_TAG: '${{ steps.image_tag.outputs.FINAL_TAG }}'
GEMINI_SANDBOX: 'docker'
BUILD_SANDBOX_FLAGS: '--platform linux/amd64 --load'
STEPS_IMAGE_TAG_OUTPUTS_FINAL_TAG: '${{ steps.image_tag.outputs.FINAL_TAG }}'
run: |-
npm run build:sandbox -- \
--image "google/gemini-cli-sandbox:${STEPS_IMAGE_TAG_OUTPUTS_FINAL_TAG}" \
--output-file final_image_uri.txt
echo "uri=$(cat final_image_uri.txt)" >> $GITHUB_OUTPUT
- name: 'verify'
shell: 'bash'
run: |-
docker run --rm --entrypoint sh "${{ steps.docker_build.outputs.uri }}" -lc '
set -e
node -e "const fs=require(\"node:fs\"); JSON.parse(fs.readFileSync(\"/usr/local/share/npm-global/lib/node_modules/@google/gemini-cli/package.json\",\"utf8\")); JSON.parse(fs.readFileSync(\"/usr/local/share/npm-global/lib/node_modules/@google/gemini-cli-core/package.json\",\"utf8\"));"
/usr/local/share/npm-global/bin/gemini --version >/dev/null
'
- name: 'publish'
shell: 'bash'
if: "${{ inputs.dry-run != 'true' }}"
env:
GEMINI_SANDBOX_IMAGE_TAG: '${{ steps.image_tag.outputs.FINAL_TAG }}'
GEMINI_SANDBOX: 'docker'
BUILD_SANDBOX_FLAGS: '--platform linux/amd64,linux/arm64 --push'
STEPS_IMAGE_TAG_OUTPUTS_FINAL_TAG: '${{ steps.image_tag.outputs.FINAL_TAG }}'
run: |-
npm run build:sandbox -- \
--image "google/gemini-cli-sandbox:${STEPS_IMAGE_TAG_OUTPUTS_FINAL_TAG}"
- name: 'Create issue on failure'
if: |-
${{ failure() }}
shell: 'bash'
env:
GITHUB_TOKEN: '${{ inputs.github-secret }}'
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
run: |-
gh issue create \
--title "Docker build failed" \
--body "The docker build failed. See the full run for details: ${DETAILS_URL}" \
--label "release-failure"
+41
View File
@@ -0,0 +1,41 @@
name: 'Run Tests'
description: 'Runs the preflight checks and integration tests.'
inputs:
gemini_api_key:
description: 'The API key for running integration tests.'
required: true
working-directory:
description: 'The working directory to run the tests in.'
required: false
default: '.'
runs:
using: 'composite'
steps:
- name: 'Install system dependencies'
if: "runner.os == 'Linux'"
run: |
sudo apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq bubblewrap
# Ubuntu 24.04+ requires this to allow bwrap to function in CI
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
shell: 'bash'
- name: 'Run Tests'
env:
GEMINI_API_KEY: '${{ inputs.gemini_api_key }}'
GEMINI_CLI_TRUST_WORKSPACE: true
working-directory: '${{ inputs.working-directory }}'
run: |-
echo "::group::Build"
npm run build
echo "::endgroup::"
echo "::group::Unit Tests"
npm run test:ci
echo "::endgroup::"
echo "::group::Integration Tests (no sandbox)"
npm run test:integration:sandbox:none
echo "::endgroup::"
echo "::group::Integration Tests (docker sandbox)"
npm run test:integration:sandbox:docker
echo "::endgroup::"
shell: 'bash'
+24
View File
@@ -0,0 +1,24 @@
name: 'Setup NPMRC'
description: 'Sets up NPMRC with all the correct repos for readonly access.'
inputs:
github-token:
description: 'the github token'
required: true
outputs:
auth-token:
description: 'The generated NPM auth token'
value: '${{ steps.npm_auth_token.outputs.auth-token }}'
runs:
using: 'composite'
steps:
- name: 'Configure .npmrc'
shell: 'bash'
run: |-
echo ""@google-gemini:registry=https://npm.pkg.github.com"" > ~/.npmrc
echo ""//npm.pkg.github.com/:_authToken=${INPUTS_GITHUB_TOKEN}"" >> ~/.npmrc
echo ""@google:registry=https://wombat-dressing-room.appspot.com"" >> ~/.npmrc
env:
INPUTS_GITHUB_TOKEN: '${{ inputs.github-token }}'
+139
View File
@@ -0,0 +1,139 @@
name: 'Tag an NPM release'
description: 'Tags a specific npm version to a specific channel.'
inputs:
channel:
description: 'NPM Channel tag'
required: true
version:
description: 'version'
required: true
dry-run:
description: 'Whether to run in dry-run mode.'
required: true
github-token:
description: 'The GitHub token for creating the release.'
required: true
wombat-token-core:
description: 'The npm token for the wombat @google/gemini-cli-core'
required: true
wombat-token-cli:
description: 'The npm token for wombat @google/gemini-cli'
required: true
wombat-token-a2a-server:
description: 'The npm token for the @google/gemini-cli-a2a-server package.'
required: true
cli-package-name:
description: 'The name of the cli package.'
required: true
core-package-name:
description: 'The name of the core package.'
required: true
a2a-package-name:
description: 'The name of the a2a package.'
required: true
working-directory:
description: 'The working directory to run the commands in.'
required: false
default: '.'
runs:
using: 'composite'
steps:
- name: 'Setup Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
with:
node-version-file: '${{ inputs.working-directory }}/.nvmrc'
- name: 'configure .npmrc'
uses: './.github/actions/setup-npmrc'
with:
github-token: '${{ inputs.github-token }}'
- name: 'Get core Token'
uses: './.github/actions/npm-auth-token'
id: 'core-token'
with:
package-name: '${{ inputs.core-package-name }}'
github-token: '${{ inputs.github-token }}'
wombat-token-core: '${{ inputs.wombat-token-core }}'
wombat-token-cli: '${{ inputs.wombat-token-cli }}'
wombat-token-a2a-server: '${{ inputs.wombat-token-a2a-server }}'
- name: 'Change tag for CORE'
if: |-
${{ inputs.dry-run != 'true' }}
env:
NODE_AUTH_TOKEN: '${{ steps.core-token.outputs.auth-token }}'
INPUTS_CORE_PACKAGE_NAME: '${{ inputs.core-package-name }}'
INPUTS_VERSION: '${{ inputs.version }}'
INPUTS_CHANNEL: '${{ inputs.channel }}'
shell: 'bash'
working-directory: '${{ inputs.working-directory }}'
run: |
npm dist-tag add ${INPUTS_CORE_PACKAGE_NAME}@${INPUTS_VERSION} ${INPUTS_CHANNEL}
- name: 'Get cli Token'
uses: './.github/actions/npm-auth-token'
id: 'cli-token'
with:
package-name: '${{ inputs.cli-package-name }}'
github-token: '${{ inputs.github-token }}'
wombat-token-core: '${{ inputs.wombat-token-core }}'
wombat-token-cli: '${{ inputs.wombat-token-cli }}'
wombat-token-a2a-server: '${{ inputs.wombat-token-a2a-server }}'
- name: 'Change tag for CLI'
if: |-
${{ inputs.dry-run != 'true' }}
env:
NODE_AUTH_TOKEN: '${{ steps.cli-token.outputs.auth-token }}'
INPUTS_CLI_PACKAGE_NAME: '${{ inputs.cli-package-name }}'
INPUTS_VERSION: '${{ inputs.version }}'
INPUTS_CHANNEL: '${{ inputs.channel }}'
shell: 'bash'
working-directory: '${{ inputs.working-directory }}'
run: |
npm dist-tag add ${INPUTS_CLI_PACKAGE_NAME}@${INPUTS_VERSION} ${INPUTS_CHANNEL}
- name: 'Get a2a Token'
uses: './.github/actions/npm-auth-token'
id: 'a2a-token'
with:
package-name: '${{ inputs.a2a-package-name }}'
github-token: '${{ inputs.github-token }}'
wombat-token-core: '${{ inputs.wombat-token-core }}'
wombat-token-cli: '${{ inputs.wombat-token-cli }}'
wombat-token-a2a-server: '${{ inputs.wombat-token-a2a-server }}'
- name: 'Change tag for a2a'
if: |-
${{ inputs.dry-run == 'false' }}
env:
NODE_AUTH_TOKEN: '${{ steps.a2a-token.outputs.auth-token }}'
INPUTS_A2A_PACKAGE_NAME: '${{ inputs.a2a-package-name }}'
INPUTS_VERSION: '${{ inputs.version }}'
INPUTS_CHANNEL: '${{ inputs.channel }}'
shell: 'bash'
working-directory: '${{ inputs.working-directory }}'
run: |
npm dist-tag add ${INPUTS_A2A_PACKAGE_NAME}@${INPUTS_VERSION} ${INPUTS_CHANNEL}
- name: 'Log dry run'
if: |-
${{ inputs.dry-run == 'true' }}
shell: 'bash'
working-directory: '${{ inputs.working-directory }}'
run: |
echo "Dry run: Would have added tag '${INPUTS_CHANNEL}' to version '${INPUTS_VERSION}' for ${INPUTS_CLI_PACKAGE_NAME}, ${INPUTS_CORE_PACKAGE_NAME}, and ${INPUTS_A2A_PACKAGE_NAME}."
env:
INPUTS_CHANNEL: '${{ inputs.channel }}'
INPUTS_VERSION: '${{ inputs.version }}'
INPUTS_CLI_PACKAGE_NAME: '${{ inputs.cli-package-name }}'
INPUTS_CORE_PACKAGE_NAME: '${{ inputs.core-package-name }}'
INPUTS_A2A_PACKAGE_NAME: '${{ inputs.a2a-package-name }}'
+103
View File
@@ -0,0 +1,103 @@
name: 'Verify an NPM release'
description: 'Fetches a package from NPM and does some basic smoke tests'
inputs:
npm-package:
description: 'NPM Package'
required: true
default: '@google/gemini-cli@latest'
npm-registry-url:
description: 'NPM Registry URL'
required: true
npm-registry-scope:
description: 'NPM Registry Scope'
required: true
expected-version:
description: 'Expected version'
required: true
gemini_api_key:
description: 'The API key for running integration tests.'
required: true
github-token:
description: 'The GitHub token for running integration tests.'
required: true
working-directory:
description: 'The working directory to run the tests in.'
required: false
default: '.'
runs:
using: 'composite'
steps:
- name: 'setup node'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
with:
node-version: '20'
- name: 'configure .npmrc'
uses: './.github/actions/setup-npmrc'
with:
github-token: '${{ inputs.github-token }}'
- name: 'Clear npm cache'
shell: 'bash'
run: 'npm cache clean --force'
- name: 'Install from NPM'
uses: 'nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08' # ratchet:nick-fields/retry@v3
with:
timeout_seconds: 900
retry_wait_seconds: 30
max_attempts: 10
command: |-
cd ${{ inputs.working-directory }}
npm install --prefer-online --no-cache -g "${{ inputs.npm-package }}"
- name: 'Smoke test - NPM Install'
shell: 'bash'
working-directory: '${{ inputs.working-directory }}'
run: |-
gemini_version=$(gemini --version)
if [ "$gemini_version" != "${INPUTS_EXPECTED_VERSION}" ]; then
echo "❌ NPM Version mismatch: Got $gemini_version from ${INPUTS_NPM_PACKAGE}, expected ${INPUTS_EXPECTED_VERSION}"
exit 1
fi
env:
INPUTS_EXPECTED_VERSION: '${{ inputs.expected-version }}'
INPUTS_NPM_PACKAGE: '${{ inputs.npm-package }}'
- name: 'Clear npm cache'
shell: 'bash'
run: 'npm cache clean --force'
- name: 'Smoke test - NPX Run'
shell: 'bash'
working-directory: '${{ inputs.working-directory }}'
run: |-
gemini_version=$(npx --yes --prefer-online "${INPUTS_NPM_PACKAGE}" --version)
if [ "$gemini_version" != "${INPUTS_EXPECTED_VERSION}" ]; then
echo "❌ NPX Run Version mismatch: Got $gemini_version from ${INPUTS_NPM_PACKAGE}, expected ${INPUTS_EXPECTED_VERSION}"
exit 1
fi
env:
INPUTS_NPM_PACKAGE: '${{ inputs.npm-package }}'
INPUTS_EXPECTED_VERSION: '${{ inputs.expected-version }}'
- name: 'Install dependencies for integration tests'
shell: 'bash'
working-directory: '${{ inputs.working-directory }}'
run: 'npm ci --ignore-scripts'
- name: '🔬 Run integration tests against NPM release'
working-directory: '${{ inputs.working-directory }}'
env:
GEMINI_API_KEY: '${{ inputs.gemini_api_key }}'
GEMINI_CLI_TRUST_WORKSPACE: true
INTEGRATION_TEST_USE_INSTALLED_GEMINI: 'true'
# We must diable CI mode here because it interferes with interactive tests.
# See https://github.com/google-gemini/gemini-cli/issues/10517
CI: 'false'
shell: 'bash'
run: |
export INTEGRATION_TEST_GEMINI_BINARY_PATH=$(which gemini)
npm run test:integration:sandbox:none
+41
View File
@@ -0,0 +1,41 @@
version: 2
updates:
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: 'weekly'
day: 'monday'
open-pull-requests-limit: 10
reviewers:
- 'joshualitt'
cooldown:
semver-major-days: 14
semver-minor-days: 14
semver-patch-days: 14
groups:
npm-dependencies:
patterns:
- '*'
update-types:
- 'minor'
- 'patch'
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'weekly'
day: 'monday'
open-pull-requests-limit: 10
reviewers:
- 'joshualitt'
cooldown:
semver-major-days: 14
semver-minor-days: 14
semver-patch-days: 14
groups:
actions-dependencies:
patterns:
- '*'
update-types:
- 'minor'
- 'patch'
+42
View File
@@ -0,0 +1,42 @@
## Summary
<!-- Concisely describe what this PR changes and why. Focus on impact and
urgency. -->
## Details
<!-- Add any extra context and design decisions. Keep it brief but complete. -->
## Related Issues
<!-- Use keywords to auto-close issues (Closes #123, Fixes #456). If this PR is
only related to an issue or is a partial fix, simply reference the issue number
without a keyword (Related to #123). -->
## How to Validate
<!-- List exact steps for reviewers to validate the change. Include commands,
expected results, and edge cases. -->
## Pre-Merge Checklist
<!-- Check all that apply before requesting review or merging. -->
- [ ] Updated relevant documentation and README (if needed)
- [ ] Added/updated tests (if needed)
- [ ] Noted breaking changes (if any)
- [ ] Validated on required platforms/methods:
- [ ] MacOS
- [ ] npm run
- [ ] npx
- [ ] Docker
- [ ] Podman
- [ ] Seatbelt
- [ ] Windows
- [ ] npm run
- [ ] npx
- [ ] Docker
- [ ] Linux
- [ ] npm run
- [ ] npx
- [ ] Docker
+278
View File
@@ -0,0 +1,278 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
module.exports = async ({ github, context, core }) => {
const extractJson = (raw) => {
if (!raw || raw === '[]' || raw === '') return [];
try {
// First, try to parse the raw output as JSON.
return JSON.parse(raw);
} catch {
// If that fails, check for a markdown code block.
core.info(
'Direct JSON parsing failed. Trying to extract from a markdown block.',
);
const jsonMatch = raw.match(/```json\s*([\s\S]*?)\s*```/);
if (jsonMatch && jsonMatch[1]) {
try {
return JSON.parse(jsonMatch[1].trim());
} catch (markdownError) {
core.warning(
`Failed to parse extracted JSON from markdown block: ${markdownError.message}`,
);
}
}
// Try to find a raw JSON array in the output.
const jsonArrayMatch = raw.match(
/\[\s*\{\s*"issue_number"[\s\S]*\}\s*\]/,
);
if (jsonArrayMatch) {
try {
return JSON.parse(jsonArrayMatch[0]);
} catch {
const fallbackMatch = raw.match(/(\[\s*\{\s*"issue_number"[\s\S]*)/);
if (fallbackMatch) {
try {
const cleaned = fallbackMatch[0].substring(
0,
fallbackMatch[0].lastIndexOf(']') + 1,
);
return JSON.parse(cleaned);
} catch (fallbackError) {
core.warning(
`Failed to parse extracted JSON using fallback regex: ${fallbackError.message}`,
);
}
}
}
}
}
core.warning('No valid JSON could be extracted from input.');
return [];
};
// Collect all outputs from environment variables
// Prioritize EFFORT results over STANDARD results by processing Effort FIRST
// so that its labels appear first in the merged arrays (and thus win in mutually exclusive logic)
const effortRaw = process.env.LABELS_OUTPUT_EFFORT;
const standardRaw = process.env.LABELS_OUTPUT_STANDARD;
const genericRaw = process.env.LABELS_OUTPUT;
const resultsByIssue = new Map();
const processResults = (results, _sourceName) => {
for (const entry of results) {
const issueNumber = entry.issue_number;
if (!issueNumber) continue;
if (!resultsByIssue.has(issueNumber)) {
resultsByIssue.set(issueNumber, {
issue_number: issueNumber,
labels_to_add: [...(entry.labels_to_add || [])],
labels_to_remove: [...(entry.labels_to_remove || [])],
explanation: entry.explanation || '',
effort_analysis: entry.effort_analysis || '',
});
} else {
const existing = resultsByIssue.get(issueNumber);
// Combine labels
existing.labels_to_add = [
...new Set([
...existing.labels_to_add,
...(entry.labels_to_add || []),
]),
];
existing.labels_to_remove = [
...new Set([
...existing.labels_to_remove,
...(entry.labels_to_remove || []),
]),
];
// Combine explanations (if different)
if (
entry.explanation &&
!existing.explanation.includes(entry.explanation)
) {
existing.explanation = existing.explanation
? `${existing.explanation}\n\n${entry.explanation}`
: entry.explanation;
}
// Take effort analysis if present
if (entry.effort_analysis && !existing.effort_analysis) {
existing.effort_analysis = entry.effort_analysis;
}
}
}
};
// Order matters: Effort first so its labels win in conflict resolution
processResults(extractJson(effortRaw), 'EFFORT');
processResults(extractJson(standardRaw), 'STANDARD');
processResults(extractJson(genericRaw), 'GENERIC');
const finalResults = Array.from(resultsByIssue.values());
core.info(`Aggregated triage results for ${finalResults.length} issues.`);
for (const entry of finalResults) {
const issueNumber = entry.issue_number;
let labelsToAdd = entry.labels_to_add || [];
let labelsToRemove = entry.labels_to_remove || [];
let existingLabels = [];
// Fetch existing labels early
try {
const { data: issueData } = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
});
existingLabels = issueData.labels.map((l) =>
typeof l === 'string' ? l : l.name,
);
} catch (e) {
core.warning(
`Failed to fetch existing labels for #${issueNumber}: ${e.message}`,
);
}
// Programmatic Priority Downgrade Logic
if (labelsToAdd.includes('status/need-information')) {
const targetPriority = labelsToAdd.find((l) => l.startsWith('priority/'));
if (targetPriority) {
let downgradedPriority = null;
if (targetPriority === 'priority/p0')
downgradedPriority = 'priority/p1';
if (targetPriority === 'priority/p1')
downgradedPriority = 'priority/p2';
if (downgradedPriority) {
core.info(
`Programmatically downgrading ${targetPriority} to ${downgradedPriority} due to status/need-information`,
);
labelsToAdd = labelsToAdd.filter((l) => l !== targetPriority);
labelsToAdd.push(downgradedPriority);
}
}
}
labelsToRemove.push('status/need-triage');
if (
labelsToAdd.includes('status/manual-triage') ||
existingLabels.includes('status/manual-triage')
) {
labelsToRemove.push('status/bot-triaged');
labelsToAdd = labelsToAdd.filter((l) => l !== 'status/bot-triaged');
} else {
labelsToAdd.push('status/bot-triaged');
}
// Resolve internal conflicts (e.g., adding P1 and P2)
// We already resolved these by putting Effort first in the combined list
// Resolve external conflicts with existing labels
if (labelsToAdd.some((l) => l.startsWith('area/'))) {
labelsToRemove.push(
...existingLabels.filter((l) => l.startsWith('area/')),
);
}
if (labelsToAdd.some((l) => l.startsWith('priority/'))) {
labelsToRemove.push(
...existingLabels.filter((l) => l.startsWith('priority/')),
);
}
if (labelsToAdd.some((l) => l.startsWith('kind/'))) {
labelsToRemove.push(
...existingLabels.filter((l) => l.startsWith('kind/')),
);
}
// Enforce mutual exclusivity in the TO-ADD list (Architect wins)
const exclusivePrefixes = ['area/', 'priority/', 'kind/'];
for (const prefix of exclusivePrefixes) {
const filtered = labelsToAdd.filter((l) => l.startsWith(prefix));
if (filtered.length > 1) {
const winner = filtered[0]; // First one wins
core.info(
`Issue #${issueNumber} has multiple ${prefix} labels suggested. Keeping "${winner}" and discarding others.`,
);
labelsToAdd = labelsToAdd.filter(
(l) => !l.startsWith(prefix) || l === winner,
);
}
}
// Final deduplication and cleanup
labelsToRemove = [...new Set(labelsToRemove)].filter(
(l) => !labelsToAdd.includes(l) && existingLabels.includes(l),
);
labelsToAdd = [...new Set(labelsToAdd)].filter(
(l) => !existingLabels.includes(l),
);
// Batch label operations
if (labelsToAdd.length > 0) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: labelsToAdd,
});
core.info(
`Successfully added labels for #${issueNumber}: ${labelsToAdd.join(', ')}`,
);
}
if (labelsToRemove.length > 0) {
for (const label of labelsToRemove) {
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
name: label,
});
} catch (e) {
if (e.status !== 404)
core.warning(
`Failed to remove label ${label} from #${issueNumber}: ${e.message}`,
);
}
}
core.info(
`Successfully removed labels for #${issueNumber}: ${labelsToRemove.join(', ')}`,
);
}
// Post comment if needed
const needsInfoAdded =
labelsToAdd.includes('status/need-information') &&
!existingLabels.includes('status/need-information');
const hasEffortAnalysis = !!entry.effort_analysis;
if (needsInfoAdded || hasEffortAnalysis) {
let commentBody = '';
if (needsInfoAdded && entry.explanation) commentBody += entry.explanation;
if (hasEffortAnalysis) {
if (commentBody) commentBody += '\n\n';
commentBody += `**Effort Analysis:**\n${entry.effort_analysis}`;
}
if (commentBody) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: commentBody,
});
core.info(`Posted required comment for #${issueNumber}`);
}
}
}
};
+138
View File
@@ -0,0 +1,138 @@
/* eslint-disable */
/* global require, console, process */
/**
* Script to backfill the 'status/need-triage' label to all open issues
* that are NOT currently labeled with '🔒 maintainer only' or 'help wanted'.
*/
const { execFileSync } = require('child_process');
const isDryRun = process.argv.includes('--dry-run');
const REPO = 'google-gemini/gemini-cli';
/**
* Executes a GitHub CLI command safely using an argument array to prevent command injection.
* @param {string[]} args
* @returns {string|null}
*/
function runGh(args) {
try {
// Using execFileSync with an array of arguments is safe as it doesn't use a shell.
// We set a large maxBuffer (10MB) to handle repositories with many issues.
return execFileSync('gh', args, {
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024,
stdio: ['ignore', 'pipe', 'pipe'],
}).trim();
} catch (error) {
const stderr = error.stderr ? ` Stderr: ${error.stderr.trim()}` : '';
console.error(
`❌ Error running gh ${args.join(' ')}: ${error.message}${stderr}`,
);
return null;
}
}
async function main() {
console.log('🔐 GitHub CLI security check...');
const authStatus = runGh(['auth', 'status']);
if (authStatus === null) {
console.error('❌ GitHub CLI (gh) is not installed or not authenticated.');
process.exit(1);
}
if (isDryRun) {
console.log('🧪 DRY RUN MODE ENABLED - No changes will be made.\n');
}
console.log(`🔍 Fetching and filtering open issues from ${REPO}...`);
// We use the /issues endpoint with pagination to bypass the 1000-result limit.
// The jq filter ensures we exclude PRs, maintainer-only, help-wanted, and existing status/need-triage.
const jqFilter =
'.[] | select(.pull_request == null) | select([.labels[].name] as $l | (any($l[]; . == "🔒 maintainer only") | not) and (any($l[]; . == "help wanted") | not) and (any($l[]; . == "status/need-triage") | not)) | {number: .number, title: .title}';
const output = runGh([
'api',
`repos/${REPO}/issues?state=open&per_page=100`,
'--paginate',
'--jq',
jqFilter,
]);
if (output === null) {
process.exit(1);
}
const issues = output
.split('\n')
.filter((line) => line.trim())
.map((line) => {
try {
return JSON.parse(line);
} catch (_e) {
console.error(`⚠️ Failed to parse line: ${line}`);
return null;
}
})
.filter(Boolean);
console.log(`✅ Found ${issues.length} issues matching criteria.`);
if (issues.length === 0) {
console.log('✨ No issues need backfilling.');
return;
}
let successCount = 0;
let failCount = 0;
if (isDryRun) {
for (const issue of issues) {
console.log(
`[DRY RUN] Would label issue #${issue.number}: ${issue.title}`,
);
}
successCount = issues.length;
} else {
console.log(`🏷️ Applying labels to ${issues.length} issues...`);
for (const issue of issues) {
const issueNumber = String(issue.number);
console.log(`🏷️ Labeling issue #${issueNumber}: ${issue.title}`);
const result = runGh([
'issue',
'edit',
issueNumber,
'--add-label',
'status/need-triage',
'--repo',
REPO,
]);
if (result !== null) {
successCount++;
} else {
failCount++;
}
}
}
console.log(`\n📊 Summary:`);
console.log(` - Success: ${successCount}`);
console.log(` - Failed: ${failCount}`);
if (failCount > 0) {
console.error(`\n❌ Backfill completed with ${failCount} errors.`);
process.exit(1);
} else {
console.log(`\n🎉 ${isDryRun ? 'Dry run' : 'Backfill'} complete!`);
}
}
main().catch((error) => {
console.error('❌ Unexpected error:', error);
process.exit(1);
});
@@ -0,0 +1,190 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/* eslint-disable */
/* global require, console, process */
/**
* Script to backfill a process change notification comment to all open PRs
* not created by members of the 'gemini-cli-maintainers' team.
*
* Skip PRs that are already associated with an issue.
*/
const { execFileSync } = require('child_process');
const isDryRun = process.argv.includes('--dry-run');
const REPO = 'google-gemini/gemini-cli';
const ORG = 'google-gemini';
const TEAM_SLUG = 'gemini-cli-maintainers';
const DISCUSSION_URL =
'https://github.com/google-gemini/gemini-cli/discussions/16706';
/**
* Executes a GitHub CLI command safely using an argument array.
*/
function runGh(args, options = {}) {
const { silent = false } = options;
try {
return execFileSync('gh', args, {
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024,
stdio: ['ignore', 'pipe', 'pipe'],
}).trim();
} catch (error) {
if (!silent) {
const stderr = error.stderr ? ` Stderr: ${error.stderr.trim()}` : '';
console.error(
`❌ Error running gh ${args.join(' ')}: ${error.message}${stderr}`,
);
}
return null;
}
}
/**
* Checks if a user is a member of the maintainers team.
*/
const membershipCache = new Map();
function isMaintainer(username) {
if (membershipCache.has(username)) return membershipCache.get(username);
// GitHub returns 404 if user is not a member.
// We use silent: true to avoid logging 404s as errors.
const result = runGh(
['api', `orgs/${ORG}/teams/${TEAM_SLUG}/memberships/${username}`],
{ silent: true },
);
const isMember = result !== null;
membershipCache.set(username, isMember);
return isMember;
}
async function main() {
console.log('🔐 GitHub CLI security check...');
if (runGh(['auth', 'status']) === null) {
console.error('❌ GitHub CLI (gh) is not authenticated.');
process.exit(1);
}
if (isDryRun) {
console.log('🧪 DRY RUN MODE ENABLED\n');
}
console.log(`📥 Fetching open PRs from ${REPO}...`);
// Fetch number, author, and closingIssuesReferences to check if linked to an issue
const prsJson = runGh([
'pr',
'list',
'--repo',
REPO,
'--state',
'open',
'--limit',
'1000',
'--json',
'number,author,closingIssuesReferences',
]);
if (prsJson === null) process.exit(1);
const prs = JSON.parse(prsJson);
console.log(`📊 Found ${prs.length} open PRs. Filtering...`);
let targetPrs = [];
for (const pr of prs) {
const author = pr.author.login;
const issueCount = pr.closingIssuesReferences
? pr.closingIssuesReferences.length
: 0;
if (issueCount > 0) {
// Skip if already linked to an issue
continue;
}
if (!isMaintainer(author)) {
targetPrs.push(pr);
}
}
console.log(
`✅ Found ${targetPrs.length} PRs from non-maintainers without associated issues.`,
);
const commentBody =
"\nHi @{AUTHOR}, thank you so much for your contribution to Gemini CLI! We really appreciate the time and effort you've put into this.\n\nWe're making some updates to our contribution process to improve how we track and review changes. Please take a moment to review our recent discussion post: [Improving Our Contribution Process & Introducing New Guidelines](${DISCUSSION_URL}).\n\nKey Update: Starting **January 26, 2026**, the Gemini CLI project will require all pull requests to be associated with an existing issue. Any pull requests not linked to an issue by that date will be automatically closed.\n\nThank you for your understanding and for being a part of our community!\n ".trim();
let successCount = 0;
let skipCount = 0;
let failCount = 0;
for (const pr of targetPrs) {
const prNumber = String(pr.number);
const author = pr.author.login;
// Check if we already commented (idempotency)
// We use silent: true here because view might fail if PR is deleted mid-run
const existingComments = runGh(
[
'pr',
'view',
prNumber,
'--repo',
REPO,
'--json',
'comments',
'--jq',
`.comments[].body | contains("${DISCUSSION_URL}")`,
],
{ silent: true },
);
if (existingComments && existingComments.includes('true')) {
console.log(
`⏭️ PR #${prNumber} already has the notification. Skipping.`,
);
skipCount++;
continue;
}
if (isDryRun) {
console.log(`[DRY RUN] Would notify @${author} on PR #${prNumber}`);
successCount++;
} else {
console.log(`💬 Notifying @${author} on PR #${prNumber}...`);
const personalizedComment = commentBody.replace('{AUTHOR}', author);
const result = runGh([
'pr',
'comment',
prNumber,
'--repo',
REPO,
'--body',
personalizedComment,
]);
if (result !== null) {
successCount++;
} else {
failCount++;
}
}
}
console.log(`\n📊 Summary:`);
console.log(` - Notified: ${successCount}`);
console.log(` - Skipped: ${skipCount}`);
console.log(` - Failed: ${failCount}`);
if (failCount > 0) process.exit(1);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
+74
View File
@@ -0,0 +1,74 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
const fs = require('node:fs');
module.exports = async ({ github, context, core }) => {
let issuesToCleanup = [];
try {
const fileContent = fs.readFileSync('issues_to_cleanup.json', 'utf8');
issuesToCleanup = JSON.parse(fileContent);
} catch (error) {
if (error.code === 'ENOENT') {
core.info('No issues found to clean up.');
return;
}
core.setFailed(`Failed to read issues_to_cleanup.json: ${error.message}`);
return;
}
for (const issue of issuesToCleanup) {
try {
const { data: issueData } = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
});
const labels = issueData.labels.map((l) =>
typeof l === 'string' ? l : l.name,
);
if (
labels.includes('status/bot-triaged') &&
labels.includes('status/need-triage')
) {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
name: 'status/need-triage',
});
core.info(
`Successfully removed status/need-triage from #${issue.number}`,
);
}
if (
labels.includes('status/bot-triaged') &&
labels.includes('status/manual-triage')
) {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
name: 'status/bot-triaged',
});
core.info(
`Successfully removed status/bot-triaged from #${issue.number} because it requires manual triage`,
);
}
} catch (error) {
core.warning(
`Failed to clean up labels for #${issue.number}: ${error.message}`,
);
}
}
core.info(
`Cleaned up conflicting labels from ${issuesToCleanup.length} issues.`,
);
};
@@ -0,0 +1,380 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Gemini Scheduled Lifecycle Manager Script
* @param {object} param0
* @param {import('@octokit/rest').Octokit} param0.github
* @param {import('@actions/github/lib/context').Context} param0.context
* @param {import('@actions/core')} param0.core
*/
module.exports = async ({ github, context, core }) => {
const dryRun = process.env.DRY_RUN === 'true';
const owner = context.repo.owner;
const repo = context.repo.repo;
core.info(`Running in ${dryRun ? 'DRY RUN' : 'PRODUCTION'} mode.`);
const STALE_LABEL = 'stale';
const NEED_INFO_LABEL = 'status/need-information';
const EXEMPT_LABELS = [
'pinned',
'security',
'🔒 maintainer only',
'help wanted',
'🗓️ Public Roadmap',
];
const STALE_DAYS = 60;
const CLOSE_DAYS = 14;
const NO_RESPONSE_DAYS = 14;
const now = new Date();
const staleThreshold = new Date(
now.getTime() - STALE_DAYS * 24 * 60 * 60 * 1000,
);
const closeThreshold = new Date(
now.getTime() - CLOSE_DAYS * 24 * 60 * 60 * 1000,
);
const noResponseThreshold = new Date(
now.getTime() - NO_RESPONSE_DAYS * 24 * 60 * 60 * 1000,
);
const maintainerCache = new Map();
async function isMaintainer(user, association) {
if (user?.type === 'Bot') return true;
if (['OWNER', 'MEMBER', 'COLLABORATOR'].includes(association)) return true;
const username = user?.login;
if (!username) return false;
if (maintainerCache.has(username)) {
return maintainerCache.get(username);
}
try {
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
owner,
repo,
username,
});
// Permission can be admin, write, read, none.
// Roles like 'maintain' or 'triage' often map to 'write' or 'read' in the top-level field.
const isM =
['admin', 'write'].includes(data.permission) ||
['admin', 'maintain', 'write'].includes(data.role_name);
maintainerCache.set(username, isM);
return isM;
} catch (err) {
core.warning(
`Could not check permissions for ${username}: ${err.message}`,
);
maintainerCache.set(username, false);
return false;
}
}
async function processItems(query, callback) {
core.info(`Searching: ${query}`);
try {
let items = await github.paginate(
github.rest.search.issuesAndPullRequests,
{
q: query,
per_page: 100,
sort: 'updated',
order: 'asc',
},
);
core.info(`Found ${items.length} items.`);
for (const item of items) {
try {
await callback(item);
} catch (err) {
core.error(`Error processing #${item.number}: ${err.message}`);
}
}
} catch (err) {
core.error(`Search failed: ${err.message}`);
}
}
// 1. Handle No-Response (status/need-information)
// Removal: Check issues updated in the last 48h that have the label
const twoDaysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000);
await processItems(
`repo:${owner}/${repo} is:open label:"${NEED_INFO_LABEL}" updated:>${twoDaysAgo.toISOString()}`,
async (item) => {
const { data: comments } = await github.rest.issues.listComments({
owner,
repo,
issue_number: item.number,
sort: 'created',
direction: 'desc',
per_page: 5,
});
// Check if the last comment is from a non-maintainer and not a bot
const lastComment = comments[0];
if (
lastComment &&
lastComment.user?.type !== 'Bot' &&
!(await isMaintainer(lastComment.user, lastComment.author_association))
) {
if (dryRun) {
core.info(
`[DRY RUN] Would remove ${NEED_INFO_LABEL} from #${item.number} due to contributor response.`,
);
} else {
core.info(
`Removing ${NEED_INFO_LABEL} from #${item.number} due to contributor response.`,
);
await github.rest.issues
.removeLabel({
owner,
repo,
issue_number: item.number,
name: NEED_INFO_LABEL,
})
.catch(() => {});
}
}
},
);
// Closure: Check issues with the label that haven't been updated in 14 days
await processItems(
`repo:${owner}/${repo} is:open label:"${NEED_INFO_LABEL}" updated:<${noResponseThreshold.toISOString()}`,
async (item) => {
if (dryRun) {
core.info(
`[DRY RUN] Would close #${item.number} due to no response for ${NO_RESPONSE_DAYS} days.`,
);
} else {
core.info(
`Closing #${item.number} due to no response for ${NO_RESPONSE_DAYS} days.`,
);
await github.rest.issues.createComment({
owner,
repo,
issue_number: item.number,
body: `This item was marked as needing more information and has not received a response in ${NO_RESPONSE_DAYS} days. Closing it for now. If you still face this problem, feel free to reopen with more details. Thank you!`,
});
await github.rest.issues.update({
owner,
repo,
issue_number: item.number,
state: 'closed',
state_reason: 'not_planned',
});
}
},
);
// 2. Handle Stale Mark (60 days inactivity, no stale label)
const exemptQuery = EXEMPT_LABELS.map((l) => `-label:"${l}"`).join(' ');
await processItems(
`repo:${owner}/${repo} is:open -label:"${STALE_LABEL}" ${exemptQuery} updated:<${staleThreshold.toISOString()}`,
async (item) => {
const isBug = item.labels.some((l) =>
(typeof l === 'string' ? l : l.name).toLowerCase().includes('bug'),
);
const bodyText = isBug
? `This bug report has been automatically marked as stale due to ${STALE_DAYS} days of inactivity. Many issues are resolved in newer releases. Please verify if the issue persists in the latest Gemini CLI version. If it does, please leave a comment to keep this open. It will be closed in ${CLOSE_DAYS} days if no further activity occurs. Thank you!`
: `This item has been automatically marked as stale due to ${STALE_DAYS} days of inactivity. It will be closed in ${CLOSE_DAYS} days if no further activity occurs. Thank you!`;
if (dryRun) {
core.info(`[DRY RUN] Would mark #${item.number} as stale.`);
} else {
core.info(`Marking #${item.number} as stale.`);
await github.rest.issues.addLabels({
owner,
repo,
issue_number: item.number,
labels: [STALE_LABEL],
});
await github.rest.issues.createComment({
owner,
repo,
issue_number: item.number,
body: bodyText,
});
}
},
);
// 3. Handle Stale Removal & Close
await processItems(
`repo:${owner}/${repo} is:open label:"${STALE_LABEL}" ${exemptQuery}`,
async (item) => {
// Fetch full timeline to see events and comments
const timeline = await github.paginate(
github.rest.issues.listEventsForTimeline,
{
owner,
repo,
issue_number: item.number,
per_page: 100,
},
);
// Find exactly when the Stale label was added
// We look for the last 'labeled' event for STALE_LABEL
const staleEventIndex = timeline.findLastIndex(
(e) =>
e.event === 'labeled' &&
e.label?.name?.toLowerCase() === STALE_LABEL.toLowerCase(),
);
if (staleEventIndex === -1) return; // Fallback if no event found
const staleEvent = timeline[staleEventIndex];
const eventsAfterStale = timeline.slice(staleEventIndex + 1);
// Check for meaningful activity after the Stale label was applied
const meaningfulEvents = eventsAfterStale.filter((e) => {
const actor = e.actor?.login || '';
const isBot =
actor.includes('[bot]') || actor.includes('github-actions');
if (isBot) return false;
// Explicit whitelist of meaningful events for humans
if (
[
'commented',
'cross-referenced',
'connected',
'reopened',
'assigned',
].includes(e.event)
) {
return true;
}
return false;
});
if (meaningfulEvents.length > 0) {
// Activity detected, remove Stale label
if (dryRun) {
core.info(
`[DRY RUN] Would remove ${STALE_LABEL} from #${item.number} due to meaningful activity (e.g., comment or PR).`,
);
} else {
core.info(
`Removing ${STALE_LABEL} from #${item.number} due to meaningful activity (e.g., comment or PR).`,
);
await github.rest.issues
.removeLabel({
owner,
repo,
issue_number: item.number,
name: STALE_LABEL,
})
.catch(() => {});
}
return;
}
// No meaningful activity. Check if 14 days have passed.
const labeledDate = new Date(staleEvent.created_at);
if (labeledDate > closeThreshold) {
// Has not been 14 days since it was ACTUALLY marked stale
return;
}
if (dryRun) {
core.info(`[DRY RUN] Would close stale item #${item.number}.`);
} else {
core.info(`Closing stale item #${item.number}.`);
await github.rest.issues.createComment({
owner,
repo,
issue_number: item.number,
body: `This item has been closed due to ${CLOSE_DAYS} additional days of inactivity after being marked as stale. If you believe this is still relevant, feel free to comment or reopen. Thank you!`,
});
await github.rest.issues.update({
owner,
repo,
issue_number: item.number,
state: 'closed',
state_reason: 'not_planned',
});
}
},
);
// 4. Handle PR Contribution Policy (Nudge at 7d, Close at 14d)
const PR_NUDGE_DAYS = 7;
const PR_CLOSE_DAYS = 14;
const nudgeThreshold = new Date(
now.getTime() - PR_NUDGE_DAYS * 24 * 60 * 60 * 1000,
);
const prCloseThreshold = new Date(
now.getTime() - PR_CLOSE_DAYS * 24 * 60 * 60 * 1000,
);
// Nudge
await processItems(
`repo:${owner}/${repo} is:open is:pr -label:"help wanted" -label:"🔒 maintainer only" -label:"status/pr-nudge-sent" created:${prCloseThreshold.toISOString()}..${nudgeThreshold.toISOString()}`,
async (pr) => {
if (await isMaintainer(pr.user, pr.author_association)) return;
if (dryRun) {
core.info(
`[DRY RUN] Would nudge PR #${pr.number} for contribution policy.`,
);
} else {
core.info(`Nudging PR #${pr.number} for contribution policy.`);
await github.rest.issues.addLabels({
owner,
repo,
issue_number: pr.number,
labels: ['status/pr-nudge-sent'],
});
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr.number,
body: "Hi there! Thank you for your interest in contributing to Gemini CLI. \n\nTo ensure we maintain high code quality and focus on our prioritized roadmap, we only guarantee review and consideration of pull requests for issues that are explicitly labeled as 'help wanted'. \n\nThis PR will be closed in 7 days if it remains without that designation. We encourage you to find and contribute to existing 'help wanted' issues in our backlog! Thank you for your understanding.",
});
}
},
);
// Close
await processItems(
`repo:${owner}/${repo} is:open is:pr -label:"help wanted" -label:"🔒 maintainer only" created:<${prCloseThreshold.toISOString()}`,
async (pr) => {
if (await isMaintainer(pr.user, pr.author_association)) return;
if (dryRun) {
core.info(
`[DRY RUN] Would close PR #${pr.number} per contribution policy (no 'help wanted').`,
);
} else {
core.info(
`Closing PR #${pr.number} per contribution policy (no 'help wanted').`,
);
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr.number,
body: "This pull request is being closed as it has been open for 14 days without a 'help wanted' designation. We encourage you to find and contribute to existing 'help wanted' issues in our backlog! Thank you for your understanding.",
});
await github.rest.pulls.update({
owner,
repo,
pull_number: pr.number,
state: 'closed',
});
}
},
);
};
+184
View File
@@ -0,0 +1,184 @@
#!/usr/bin/env bash
# @license
# Copyright 2026 Google LLC
# SPDX-License-Identifier: Apache-2.0
set -euo pipefail
# Initialize a comma-separated string to hold PR numbers that need a comment
PRS_NEEDING_COMMENT=""
# Global cache for issue labels (compatible with Bash 3.2)
# Stores "|ISSUE_NUM:LABELS|" segments
ISSUE_LABELS_CACHE_FLAT="|"
# Function to get labels from an issue (with caching)
get_issue_labels() {
local ISSUE_NUM="${1}"
if [[ -z "${ISSUE_NUM}" || "${ISSUE_NUM}" == "null" || "${ISSUE_NUM}" == "" ]]; then
return
fi
# Check cache
case "${ISSUE_LABELS_CACHE_FLAT}" in
*"|${ISSUE_NUM}:"*)
local suffix="${ISSUE_LABELS_CACHE_FLAT#*|"${ISSUE_NUM}":}"
echo "${suffix%%|*}"
return
;;
*)
# Cache miss, proceed to fetch
;;
esac
echo " 📥 Fetching labels from issue #${ISSUE_NUM}" >&2
local gh_output
if ! gh_output=$(gh issue view "${ISSUE_NUM}" --repo "${GITHUB_REPOSITORY}" --json labels -q '.labels[].name' 2>/dev/null); then
echo " ⚠️ Could not fetch issue #${ISSUE_NUM}" >&2
ISSUE_LABELS_CACHE_FLAT="${ISSUE_LABELS_CACHE_FLAT}${ISSUE_NUM}:|"
return
fi
local labels
labels=$(echo "${gh_output}" | grep -x -E '(area|priority)/.*|help wanted|🔒 maintainer only' | tr '\n' ',' | sed 's/,$//' || echo "")
# Save to flat cache
ISSUE_LABELS_CACHE_FLAT="${ISSUE_LABELS_CACHE_FLAT}${ISSUE_NUM}:${labels}|"
echo "${labels}"
}
# Function to process a single PR with pre-fetched data
process_pr_optimized() {
local PR_NUMBER="${1}"
local IS_DRAFT="${2}"
local ISSUE_NUMBER="${3}"
local CURRENT_LABELS="${4}" # Comma-separated labels
echo "🔄 Processing PR #${PR_NUMBER}"
local LABELS_TO_ADD=""
local LABELS_TO_REMOVE=""
if [[ -z "${ISSUE_NUMBER}" || "${ISSUE_NUMBER}" == "null" || "${ISSUE_NUMBER}" == "" ]]; then
if [[ "${IS_DRAFT}" == "true" ]]; then
echo " 📝 PR #${PR_NUMBER} is a draft and has no linked issue"
if [[ ",${CURRENT_LABELS}," == *",status/need-issue,"* ]]; then
echo " Removing status/need-issue label"
LABELS_TO_REMOVE="status/need-issue"
fi
else
echo " ⚠️ No linked issue found for PR #${PR_NUMBER}"
if [[ ",${CURRENT_LABELS}," != *",status/need-issue,"* ]]; then
echo " Adding status/need-issue label"
LABELS_TO_ADD="status/need-issue"
fi
if [[ -z "${PRS_NEEDING_COMMENT}" ]]; then
PRS_NEEDING_COMMENT="${PR_NUMBER}"
else
PRS_NEEDING_COMMENT="${PRS_NEEDING_COMMENT},${PR_NUMBER}"
fi
fi
else
echo " 🔗 Found linked issue #${ISSUE_NUMBER}"
if [[ ",${CURRENT_LABELS}," == *",status/need-issue,"* ]]; then
echo " Removing status/need-issue label"
LABELS_TO_REMOVE="status/need-issue"
fi
local ISSUE_LABELS
ISSUE_LABELS=$(get_issue_labels "${ISSUE_NUMBER}")
if [[ -n "${ISSUE_LABELS}" ]]; then
local IFS_OLD="${IFS}"
IFS=','
for label in ${ISSUE_LABELS}; do
if [[ -n "${label}" ]] && [[ ",${CURRENT_LABELS}," != *",${label},"* ]]; then
if [[ -z "${LABELS_TO_ADD}" ]]; then
LABELS_TO_ADD="${label}"
else
LABELS_TO_ADD="${LABELS_TO_ADD},${label}"
fi
fi
done
IFS="${IFS_OLD}"
fi
if [[ -z "${LABELS_TO_ADD}" && -z "${LABELS_TO_REMOVE}" ]]; then
echo " ✅ Labels already synchronized"
fi
fi
if [[ -n "${LABELS_TO_ADD}" || -n "${LABELS_TO_REMOVE}" ]]; then
local EDIT_CMD=("gh" "pr" "edit" "${PR_NUMBER}" "--repo" "${GITHUB_REPOSITORY}")
if [[ -n "${LABELS_TO_ADD}" ]]; then
echo " Syncing labels to add: ${LABELS_TO_ADD}"
EDIT_CMD+=("--add-label" "${LABELS_TO_ADD}")
fi
if [[ -n "${LABELS_TO_REMOVE}" ]]; then
echo " Syncing labels to remove: ${LABELS_TO_REMOVE}"
EDIT_CMD+=("--remove-label" "${LABELS_TO_REMOVE}")
fi
("${EDIT_CMD[@]}" || true)
fi
}
if [[ -z "${GITHUB_REPOSITORY:-}" ]]; then
echo "‼️ Missing \$GITHUB_REPOSITORY - this must be run from GitHub Actions"
exit 1
fi
if [[ -z "${GITHUB_OUTPUT:-}" ]]; then
echo "‼️ Missing \$GITHUB_OUTPUT - this must be run from GitHub Actions"
exit 1
fi
JQ_EXTRACT_FIELDS='{
number: .number,
isDraft: .isDraft,
issue: (.closingIssuesReferences[0].number // (.body // "" | capture("(^|[^a-zA-Z0-9])#(?<num>[0-9]+)([^a-zA-Z0-9]|$)")? | .num) // "null"),
labels: [.labels[].name] | join(",")
}'
JQ_TSV_FORMAT='"\((.number | tostring))\t\(.isDraft)\t\((.issue // null) | tostring)\t\(.labels)"'
if [[ -n "${PR_NUMBER:-}" ]]; then
echo "🔄 Processing single PR #${PR_NUMBER}"
PR_DATA=$(gh pr view "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --json number,closingIssuesReferences,isDraft,body,labels 2>/dev/null) || {
echo "❌ Failed to fetch data for PR #${PR_NUMBER}"
exit 1
}
line=$(echo "${PR_DATA}" | jq -r "${JQ_EXTRACT_FIELDS} | ${JQ_TSV_FORMAT}")
IFS=$'\t' read -r pr_num is_draft issue_num current_labels <<< "${line}"
process_pr_optimized "${pr_num}" "${is_draft}" "${issue_num}" "${current_labels}"
else
echo "📥 Getting all open pull requests..."
PR_DATA_ALL=$(gh pr list --repo "${GITHUB_REPOSITORY}" --state open --limit 1000 --json number,closingIssuesReferences,isDraft,body,labels 2>/dev/null) || {
echo "❌ Failed to fetch PR list"
exit 1
}
PR_COUNT=$(echo "${PR_DATA_ALL}" | jq '. | length')
echo "📊 Found ${PR_COUNT} open PRs to process"
# Use a temporary file to avoid masking exit codes in process substitution
tmp_file=$(mktemp)
echo "${PR_DATA_ALL}" | jq -r ".[] | ${JQ_EXTRACT_FIELDS} | ${JQ_TSV_FORMAT}" > "${tmp_file}"
while read -r line; do
[[ -z "${line}" ]] && continue
IFS=$'\t' read -r pr_num is_draft issue_num current_labels <<< "${line}"
process_pr_optimized "${pr_num}" "${is_draft}" "${issue_num}" "${current_labels}"
done < "${tmp_file}"
rm -f "${tmp_file}"
fi
if [[ -z "${PRS_NEEDING_COMMENT}" ]]; then
echo "prs_needing_comment=[]" >> "${GITHUB_OUTPUT}"
else
echo "prs_needing_comment=[${PRS_NEEDING_COMMENT}]" >> "${GITHUB_OUTPUT}"
fi
echo "✅ PR triage completed"
+99
View File
@@ -0,0 +1,99 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
const fs = require('node:fs');
module.exports = async ({ github, context, core }) => {
const query = `
query($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
issues(first: 50, states: OPEN, orderBy: {field: UPDATED_AT, direction: DESC}) {
nodes {
id
number
title
body
issueType {
name
}
labels(first: 20) {
nodes {
name
}
}
}
}
}
}
`;
try {
const result = await github.graphql(query, {
owner: context.repo.owner,
repo: context.repo.repo,
});
const issues = result.repository.issues.nodes;
const issuesNeedingAnalysis = [];
let syncedCount = 0;
for (const issue of issues) {
if (issue.issueType === null) {
const labelNames = issue.labels.nodes.map((l) => l.name);
const hasBug = labelNames.includes('kind/bug');
const hasFeature =
labelNames.includes('kind/feature') ||
labelNames.includes('kind/enhancement');
let issueTypeId = null;
if (hasBug) {
issueTypeId = 'IT_kwDOCaSVvs4BR7vP'; // Bug
} else if (hasFeature) {
issueTypeId = 'IT_kwDOCaSVvs4BR7vQ'; // Feature
}
if (issueTypeId) {
await github.graphql(
`
mutation($issueId: ID!, $issueTypeId: ID!) {
updateIssue(input: {id: $issueId, issueTypeId: $issueTypeId}) {
issue {
id
}
}
}
`,
{
issueId: issue.id,
issueTypeId: issueTypeId,
},
);
core.info(`Successfully synced Issue Type for #${issue.number}`);
syncedCount++;
} else {
// Needs analysis to determine kind/type
issuesNeedingAnalysis.push({
number: issue.number,
title: issue.title,
body: issue.body,
});
}
}
}
// Write issues needing analysis to a file so the AI can process them
fs.writeFileSync(
'no_type_issues.json',
JSON.stringify(issuesNeedingAnalysis),
);
core.info(`Synced ${syncedCount} issues from labels.`);
core.info(
`Found ${issuesNeedingAnalysis.length} issues missing both type and kind label to be analyzed.`,
);
} catch (error) {
core.setFailed(`Failed to sync issue types: ${error.message}`);
}
};
+389
View File
@@ -0,0 +1,389 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
const { Octokit } = require('@octokit/rest');
/**
* Sync Maintainer Labels (Recursive with strict parent-child relationship detection)
* - Uses Native Sub-issues.
* - Uses Markdown Task Lists (- [ ] #123).
* - Filters for OPEN issues only.
* - Skips DUPLICATES.
* - Skips Pull Requests.
* - ONLY labels issues in the PUBLIC (gemini-cli) repo.
*/
const REPO_OWNER = 'google-gemini';
const PUBLIC_REPO = 'gemini-cli';
const PRIVATE_REPO = 'maintainers-gemini-cli';
const ALLOWED_REPOS = [PUBLIC_REPO, PRIVATE_REPO];
const ROOT_ISSUES = [
{ owner: REPO_OWNER, repo: PUBLIC_REPO, number: 15374 },
{ owner: REPO_OWNER, repo: PUBLIC_REPO, number: 15456 },
{ owner: REPO_OWNER, repo: PUBLIC_REPO, number: 15324 },
];
const TARGET_LABEL = '🔒 maintainer only';
const isDryRun =
process.argv.includes('--dry-run') || process.env.DRY_RUN === 'true';
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN,
});
/**
* Extracts child issue references from markdown Task Lists ONLY.
* e.g. - [ ] #123 or - [x] google-gemini/gemini-cli#123
*/
function extractTaskListLinks(text, contextOwner, contextRepo) {
if (!text) return [];
const childIssues = new Map();
const add = (owner, repo, number) => {
if (ALLOWED_REPOS.includes(repo)) {
const key = `${owner}/${repo}#${number}`;
childIssues.set(key, { owner, repo, number: parseInt(number, 10) });
}
};
// 1. Full URLs in task lists
const urlRegex =
/-\s+\[[ x]\].*https:\/\/github\.com\/([a-zA-Z0-9._-]+)\/([a-zA-Z0-9._-]+)\/issues\/(\d+)\b/g;
let match;
while ((match = urlRegex.exec(text)) !== null) {
add(match[1], match[2], match[3]);
}
// 2. Cross-repo refs in task lists: owner/repo#123
const crossRepoRegex =
/-\s+\[[ x]\].*([a-zA-Z0-9._-]+)\/([a-zA-Z0-9._-]+)#(\d+)\b/g;
while ((match = crossRepoRegex.exec(text)) !== null) {
add(match[1], match[2], match[3]);
}
// 3. Short refs in task lists: #123
const shortRefRegex = /-\s+\[[ x]\].*#(\d+)\b/g;
while ((match = shortRefRegex.exec(text)) !== null) {
add(contextOwner, contextRepo, match[1]);
}
return Array.from(childIssues.values());
}
/**
* Fetches issue data via GraphQL with full pagination for sub-issues, comments, and labels.
*/
async function fetchIssueData(owner, repo, number) {
const query = `
query($owner:String!, $repo:String!, $number:Int!) {
repository(owner:$owner, name:$repo) {
issue(number:$number) {
state
title
body
labels(first: 100) {
nodes { name }
pageInfo { hasNextPage endCursor }
}
subIssues(first: 100) {
nodes {
number
repository {
name
owner { login }
}
}
pageInfo { hasNextPage endCursor }
}
comments(first: 100) {
nodes {
body
}
}
}
}
}
`;
try {
const response = await octokit.graphql(query, { owner, repo, number });
const data = response.repository.issue;
if (!data) return null;
const issue = {
state: data.state,
title: data.title,
body: data.body || '',
labels: data.labels.nodes.map((n) => n.name),
subIssues: [...data.subIssues.nodes],
comments: data.comments.nodes.map((n) => n.body),
};
// Paginate subIssues if there are more than 100
if (data.subIssues.pageInfo.hasNextPage) {
const moreSubIssues = await paginateConnection(
owner,
repo,
number,
'subIssues',
'number repository { name owner { login } }',
data.subIssues.pageInfo.endCursor,
);
issue.subIssues.push(...moreSubIssues);
}
// Paginate labels if there are more than 100 (unlikely but for completeness)
if (data.labels.pageInfo.hasNextPage) {
const moreLabels = await paginateConnection(
owner,
repo,
number,
'labels',
'name',
data.labels.pageInfo.endCursor,
(n) => n.name,
);
issue.labels.push(...moreLabels);
}
// Note: Comments are handled via Task Lists in body + first 100 comments.
// If an issue has > 100 comments with task lists, we'd need to paginate those too.
// Given the 1,100+ issue discovery count, 100 comments is usually sufficient,
// but we can add it for absolute completeness.
// (Skipping for now to avoid excessive API churn unless clearly needed).
return issue;
} catch (error) {
if (error.errors && error.errors.some((e) => e.type === 'NOT_FOUND')) {
return null;
}
throw error;
}
}
/**
* Helper to paginate any GraphQL connection.
*/
async function paginateConnection(
owner,
repo,
number,
connectionName,
nodeFields,
initialCursor,
transformNode = (n) => n,
) {
let additionalNodes = [];
let hasNext = true;
let cursor = initialCursor;
while (hasNext) {
const query = `
query($owner:String!, $repo:String!, $number:Int!, $cursor:String) {
repository(owner:$owner, name:$repo) {
issue(number:$number) {
${connectionName}(first: 100, after: $cursor) {
nodes { ${nodeFields} }
pageInfo { hasNextPage endCursor }
}
}
}
}
`;
const response = await octokit.graphql(query, {
owner,
repo,
number,
cursor,
});
const connection = response.repository.issue[connectionName];
additionalNodes.push(...connection.nodes.map(transformNode));
hasNext = connection.pageInfo.hasNextPage;
cursor = connection.pageInfo.endCursor;
}
return additionalNodes;
}
/**
* Validates if an issue should be processed (Open, not a duplicate, not a PR)
*/
function shouldProcess(issueData) {
if (!issueData) return false;
if (issueData.state !== 'OPEN') return false;
const labels = issueData.labels.map((l) => l.toLowerCase());
if (labels.includes('duplicate') || labels.includes('kind/duplicate')) {
return false;
}
return true;
}
async function getAllDescendants(roots) {
const allDescendants = new Map();
const visited = new Set();
const queue = [...roots];
for (const root of roots) {
visited.add(`${root.owner}/${root.repo}#${root.number}`);
}
console.log(`Starting discovery from ${roots.length} roots...`);
while (queue.length > 0) {
const current = queue.shift();
const currentKey = `${current.owner}/${current.repo}#${current.number}`;
try {
const issueData = await fetchIssueData(
current.owner,
current.repo,
current.number,
);
if (!shouldProcess(issueData)) {
continue;
}
// ONLY add to labeling list if it's in the PUBLIC repository
if (current.repo === PUBLIC_REPO) {
// Don't label the roots themselves
if (
!ROOT_ISSUES.some(
(r) => r.number === current.number && r.repo === current.repo,
)
) {
allDescendants.set(currentKey, {
...current,
title: issueData.title,
labels: issueData.labels,
});
}
}
const children = new Map();
// 1. Process Native Sub-issues
if (issueData.subIssues) {
for (const node of issueData.subIssues) {
const childOwner = node.repository.owner.login;
const childRepo = node.repository.name;
const childNumber = node.number;
const key = `${childOwner}/${childRepo}#${childNumber}`;
children.set(key, {
owner: childOwner,
repo: childRepo,
number: childNumber,
});
}
}
// 2. Process Markdown Task Lists in Body and Comments
let combinedText = issueData.body || '';
if (issueData.comments) {
for (const commentBody of issueData.comments) {
combinedText += '\n' + (commentBody || '');
}
}
const taskListLinks = extractTaskListLinks(
combinedText,
current.owner,
current.repo,
);
for (const link of taskListLinks) {
const key = `${link.owner}/${link.repo}#${link.number}`;
children.set(key, link);
}
// Queue children (regardless of which repo they are in, for recursion)
for (const [key, child] of children) {
if (!visited.has(key)) {
visited.add(key);
queue.push(child);
}
}
} catch (error) {
console.error(`Error processing ${currentKey}: ${error.message}`);
}
}
return Array.from(allDescendants.values());
}
async function run() {
if (isDryRun) {
console.log('=== DRY RUN MODE: No labels will be applied ===');
}
const descendants = await getAllDescendants(ROOT_ISSUES);
console.log(
`\nFound ${descendants.length} total unique open descendant issues in ${PUBLIC_REPO}.`,
);
for (const issueInfo of descendants) {
const issueKey = `${issueInfo.owner}/${issueInfo.repo}#${issueInfo.number}`;
try {
// Data is already available from the discovery phase
const hasLabel = issueInfo.labels.some((l) => l === TARGET_LABEL);
if (!hasLabel) {
if (isDryRun) {
console.log(
`[DRY RUN] Would label ${issueKey}: "${issueInfo.title}"`,
);
} else {
console.log(`Labeling ${issueKey}: "${issueInfo.title}"...`);
await octokit.rest.issues.addLabels({
owner: issueInfo.owner,
repo: issueInfo.repo,
issue_number: issueInfo.number,
labels: [TARGET_LABEL],
});
}
}
// Remove status/need-triage from maintainer-only issues since they
// don't need community triage. We always attempt removal rather than
// checking the (potentially stale) label snapshot, because the
// issue-opened-labeler workflow runs concurrently and may add the
// label after our snapshot was taken.
if (isDryRun) {
console.log(
`[DRY RUN] Would remove status/need-triage from ${issueKey}`,
);
} else {
try {
await octokit.rest.issues.removeLabel({
owner: issueInfo.owner,
repo: issueInfo.repo,
issue_number: issueInfo.number,
name: 'status/need-triage',
});
console.log(`Removed status/need-triage from ${issueKey}`);
} catch (removeError) {
// 404 means the label wasn't present — that's fine.
if (removeError.status === 404) {
console.log(
`status/need-triage not present on ${issueKey}, skipping.`,
);
} else {
throw removeError;
}
}
}
} catch (error) {
console.error(`Error processing label for ${issueKey}: ${error.message}`);
}
}
}
run().catch((error) => {
console.error(error);
process.exit(1);
});
@@ -0,0 +1,132 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: 'Agent Session Drift Check'
on:
pull_request:
branches:
- 'main'
- 'release/**'
paths:
- 'packages/cli/src/nonInteractiveCli.ts'
- 'packages/cli/src/nonInteractiveCliAgentSession.ts'
concurrency:
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
cancel-in-progress: true
jobs:
check-drift:
name: 'Check Agent Session Drift'
runs-on: 'ubuntu-latest'
if: "github.repository == 'google-gemini/gemini-cli'"
permissions:
contents: 'read'
pull-requests: 'write'
steps:
- name: 'Detect drift and comment'
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' # ratchet:actions/github-script@v8
with:
script: |-
// === Pair configuration — append here to cover more pairs ===
const PAIRS = [
{
legacy: 'packages/cli/src/nonInteractiveCli.ts',
session: 'packages/cli/src/nonInteractiveCliAgentSession.ts',
label: 'non-interactive CLI',
},
// Future pairs can be added here. Remember to also add both
// paths to the `paths:` filter at the top of this workflow.
// Example:
// {
// legacy: 'packages/core/src/agents/local-invocation.ts',
// session: 'packages/core/src/agents/local-session-invocation.ts',
// label: 'local subagent invocation',
// },
];
// ============================================================
const prNumber = context.payload.pull_request.number;
const { owner, repo } = context.repo;
// Use the API to list changed files — no checkout/git diff needed.
const files = await github.paginate(github.rest.pulls.listFiles, {
owner,
repo,
pull_number: prNumber,
per_page: 100,
});
const changed = new Set(files.map((f) => f.filename));
const warnings = [];
for (const { legacy, session, label } of PAIRS) {
const legacyChanged = changed.has(legacy);
const sessionChanged = changed.has(session);
if (legacyChanged && !sessionChanged) {
warnings.push(
`**${label}**: \`${legacy}\` was modified but \`${session}\` was not.`,
);
} else if (!legacyChanged && sessionChanged) {
warnings.push(
`**${label}**: \`${session}\` was modified but \`${legacy}\` was not.`,
);
}
}
const MARKER = '<!-- agent-session-drift-check -->';
// Look up our existing drift comment (for upsert/cleanup).
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: prNumber,
per_page: 100,
});
const existing = comments.find(
(c) => c.user?.type === 'Bot' && c.body?.includes(MARKER),
);
if (warnings.length === 0) {
core.info('No drift detected.');
// If drift was previously flagged and is now resolved, remove the comment.
if (existing) {
await github.rest.issues.deleteComment({
owner,
repo,
comment_id: existing.id,
});
core.info(`Deleted stale drift comment ${existing.id}.`);
}
return;
}
const body = [
MARKER,
'### ⚠️ Invocation Drift Warning',
'',
'The following file pairs should generally be kept in sync during the AgentSession migration:',
'',
...warnings.map((w) => `- ${w}`),
'',
'If this is intentional (e.g., a bug fix specific to one implementation), you can ignore this comment.',
'',
'_This check will be removed once the legacy implementations are deleted._',
].join('\n');
if (existing) {
core.info(`Updating existing drift comment ${existing.id}.`);
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body,
});
} else {
core.info('Creating new drift comment.');
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body,
});
}
@@ -0,0 +1,65 @@
name: 'Build Unsigned Mac Binaries'
on:
workflow_dispatch:
workflow_call:
inputs:
ref:
description: 'The branch, tag, or SHA to build from.'
required: true
type: 'string'
permissions:
contents: 'read'
defaults:
run:
shell: 'bash'
jobs:
build-mac:
name: 'Build Unsigned (${{ matrix.arch }})'
runs-on: 'macos-latest'
strategy:
fail-fast: false
matrix:
arch: ['x64', 'arm64']
steps:
- name: 'Checkout'
uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4
with:
ref: '${{ inputs.ref || github.ref }}'
persist-credentials: false
- name: 'Set up Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
with:
node-version-file: '.nvmrc'
architecture: '${{ matrix.arch }}'
cache: 'npm'
- name: 'Install dependencies'
run: 'npm ci'
- name: 'Build Binary'
env:
SKIP_SIGNING: 'true'
run: 'npm run build:binary'
- name: 'Verify Output Exists'
run: |
if [ -f "dist/darwin-${{ matrix.arch }}/gemini" ]; then
echo "Binary found at dist/darwin-${{ matrix.arch }}/gemini"
else
echo "Error: Binary not found in dist/darwin-${{ matrix.arch }}/"
ls -R dist/
exit 1
fi
- name: 'Upload Artifact'
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
with:
name: 'gemini-darwin-${{ matrix.arch }}-unsigned'
path: 'dist/darwin-${{ matrix.arch }}/gemini'
retention-days: 14
+406
View File
@@ -0,0 +1,406 @@
name: 'Testing: E2E (Chained)'
on:
push:
branches:
- 'main'
merge_group:
workflow_run:
workflows: ['Trigger E2E']
types: ['completed']
workflow_dispatch:
inputs:
head_sha:
description: 'SHA of the commit to test'
required: true
repo_name:
description: 'Repository name (e.g., owner/repo)'
required: true
concurrency:
group: '${{ github.workflow }}-${{ github.head_ref || github.event.workflow_run.head_branch || github.ref }}'
cancel-in-progress: |-
${{ github.event_name != 'push' && github.event_name != 'merge_group' }}
permissions:
contents: 'read'
statuses: 'write'
jobs:
merge_queue_skipper:
name: 'Merge Queue Skipper'
permissions: 'read-all'
runs-on: 'gemini-cli-ubuntu-16-core'
if: "github.repository == 'google-gemini/gemini-cli'"
outputs:
skip: '${{ steps.merge-queue-e2e-skipper.outputs.skip-check }}'
steps:
- id: 'merge-queue-e2e-skipper'
uses: 'cariad-tech/merge-queue-ci-skipper@1032489e59437862c90a08a2c92809c903883772' # ratchet:cariad-tech/merge-queue-ci-skipper@main
with:
secret: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
continue-on-error: true
download_repo_name:
runs-on: 'gemini-cli-ubuntu-16-core'
if: "github.repository == 'google-gemini/gemini-cli' && (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_run')"
outputs:
repo_name: '${{ steps.output-repo-name.outputs.repo_name }}'
head_sha: '${{ steps.output-repo-name.outputs.head_sha }}'
steps:
- name: 'Mock Repo Artifact'
if: "${{ github.event_name == 'workflow_dispatch' }}"
env:
REPO_NAME: '${{ github.event.inputs.repo_name }}'
run: |
mkdir -p ./pr
echo "${REPO_NAME}" > ./pr/repo_name
- uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
with:
name: 'repo_name'
path: 'pr/'
- name: 'Download the repo_name artifact'
uses: 'actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0' # ratchet:actions/download-artifact@v5
env:
RUN_ID: "${{ github.event_name == 'workflow_run' && github.event.workflow_run.id || github.run_id }}"
with:
github-token: '${{ secrets.GITHUB_TOKEN }}'
name: 'repo_name'
run-id: '${{ env.RUN_ID }}'
path: '${{ runner.temp }}/artifacts'
- name: 'Output Repo Name and SHA'
id: 'output-repo-name'
uses: 'actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd' # ratchet:actions/github-script@v8
with:
github-token: '${{ secrets.GITHUB_TOKEN }}'
script: |
const fs = require('fs');
const path = require('path');
const temp = '${{ runner.temp }}/artifacts';
const repoPath = path.join(temp, 'repo_name');
if (fs.existsSync(repoPath)) {
const repo_name = String(fs.readFileSync(repoPath)).trim();
core.setOutput('repo_name', repo_name);
}
const shaPath = path.join(temp, 'head_sha');
if (fs.existsSync(shaPath)) {
const head_sha = String(fs.readFileSync(shaPath)).trim();
core.setOutput('head_sha', head_sha);
}
parse_run_context:
name: 'Parse run context'
runs-on: 'gemini-cli-ubuntu-16-core'
needs: 'download_repo_name'
if: "github.repository == 'google-gemini/gemini-cli' && always()"
outputs:
repository: '${{ steps.set_context.outputs.REPO }}'
sha: '${{ steps.set_context.outputs.SHA }}'
steps:
- id: 'set_context'
name: 'Set dynamic repository and SHA'
env:
REPO: '${{ needs.download_repo_name.outputs.repo_name || github.repository }}'
SHA: '${{ needs.download_repo_name.outputs.head_sha || github.event.inputs.head_sha || github.event.workflow_run.head_sha || github.sha }}'
shell: 'bash'
run: |
echo "REPO=$REPO" >> "$GITHUB_OUTPUT"
echo "SHA=$SHA" >> "$GITHUB_OUTPUT"
set_pending_status:
runs-on: 'gemini-cli-ubuntu-16-core'
permissions: 'write-all'
needs:
- 'parse_run_context'
if: "github.repository == 'google-gemini/gemini-cli' && always()"
steps:
- name: 'Set pending status'
uses: 'myrotvorets/set-commit-status-action@16037e056d73b2d3c88e37e393ff369047f70886' # ratchet:myrotvorets/set-commit-status-action@master
if: "github.repository == 'google-gemini/gemini-cli' && always()"
with:
allowForks: 'true'
repo: '${{ github.repository }}'
sha: '${{ needs.parse_run_context.outputs.sha }}'
token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
status: 'pending'
context: 'E2E (Chained)'
e2e_linux:
name: 'E2E Test (Linux) - ${{ matrix.sandbox }}'
needs:
- 'merge_queue_skipper'
- 'parse_run_context'
runs-on: 'gemini-cli-ubuntu-16-core'
if: |
github.repository == 'google-gemini/gemini-cli' && always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
strategy:
fail-fast: false
matrix:
sandbox:
- 'sandbox:none'
- 'sandbox:docker'
node-version:
- '20.x'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
with:
ref: '${{ needs.parse_run_context.outputs.sha }}'
repository: '${{ needs.parse_run_context.outputs.repository }}'
persist-credentials: false
- name: 'Set up Node.js ${{ matrix.node-version }}'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
with:
node-version: '${{ matrix.node-version }}'
- name: 'Install dependencies'
run: 'npm ci'
- name: 'Build project'
run: 'npm run build'
- name: 'Set up Docker'
if: "${{matrix.sandbox == 'sandbox:docker'}}"
uses: 'docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435' # ratchet:docker/setup-buildx-action@v3
- name: 'Run E2E tests'
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
GEMINI_CLI_TRUST_WORKSPACE: true
KEEP_OUTPUT: 'true'
VERBOSE: 'true'
BUILD_SANDBOX_FLAGS: '--cache-from type=gha --cache-to type=gha,mode=max'
shell: 'bash'
run: |
if [[ "${{ matrix.sandbox }}" == "sandbox:docker" ]]; then
npm run test:integration:sandbox:docker
else
npm run test:integration:sandbox:none
fi
e2e_mac:
name: 'E2E Test (macOS)'
needs:
- 'merge_queue_skipper'
- 'parse_run_context'
runs-on: 'macos-latest-large'
if: |
github.repository == 'google-gemini/gemini-cli' && always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
steps:
- name: 'Checkout'
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
with:
ref: '${{ needs.parse_run_context.outputs.sha }}'
repository: '${{ needs.parse_run_context.outputs.repository }}'
persist-credentials: false
- name: 'Set up Node.js 20.x'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
with:
node-version: '20.x'
- name: 'Install dependencies'
run: 'npm ci'
- name: 'Build project'
run: 'npm run build'
- name: 'Fix rollup optional dependencies on macOS'
if: "${{runner.os == 'macOS'}}"
run: |
npm cache clean --force
- name: 'Run E2E tests (non-Windows)'
if: "${{runner.os != 'Windows'}}"
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
GEMINI_CLI_TRUST_WORKSPACE: true
KEEP_OUTPUT: 'true'
SANDBOX: 'sandbox:none'
VERBOSE: 'true'
run: 'npm run test:integration:sandbox:none'
e2e_windows:
name: 'Slow E2E - Win'
needs:
- 'merge_queue_skipper'
- 'parse_run_context'
if: |
github.repository == 'google-gemini/gemini-cli' && always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
runs-on: 'gemini-cli-windows-16-core'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
with:
ref: '${{ needs.parse_run_context.outputs.sha }}'
repository: '${{ needs.parse_run_context.outputs.repository }}'
persist-credentials: false
- name: 'Set up Node.js 20.x'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
with:
node-version: '20.x'
cache: 'npm'
- name: 'Configure Windows Defender exclusions'
run: |
Add-MpPreference -ExclusionPath $env:GITHUB_WORKSPACE -Force
Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\node_modules" -Force
Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\packages" -Force
Add-MpPreference -ExclusionPath "$env:TEMP" -Force
shell: 'pwsh'
- name: 'Configure npm for Windows performance'
run: |
npm config set progress false
npm config set audit false
npm config set fund false
npm config set loglevel error
npm config set maxsockets 32
npm config set registry https://registry.npmjs.org/
shell: 'pwsh'
- name: 'Install dependencies'
run: 'npm ci'
shell: 'pwsh'
- name: 'Build project'
run: 'npm run build'
shell: 'pwsh'
- name: 'Ensure Chrome is available'
shell: 'pwsh'
run: |
$chromePaths = @(
"${env:ProgramFiles}\Google\Chrome\Application\chrome.exe",
"${env:ProgramFiles(x86)}\Google\Chrome\Application\chrome.exe"
)
$chromeExists = $chromePaths | Where-Object { Test-Path $_ } | Select-Object -First 1
if (-not $chromeExists) {
Write-Host 'Chrome not found, installing via Chocolatey...'
choco install googlechrome -y --no-progress --ignore-checksums
}
$installed = $chromePaths | Where-Object { Test-Path $_ } | Select-Object -First 1
if ($installed) {
Write-Host "Chrome found at: $installed"
& $installed --version
} else {
Write-Error 'Chrome installation failed'
exit 1
}
- name: 'Run E2E tests'
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
GEMINI_CLI_TRUST_WORKSPACE: true
KEEP_OUTPUT: 'true'
SANDBOX: 'sandbox:none'
VERBOSE: 'true'
NODE_OPTIONS: '--max-old-space-size=32768 --max-semi-space-size=256'
UV_THREADPOOL_SIZE: '32'
NODE_ENV: 'test'
shell: 'pwsh'
run: 'npm run test:integration:sandbox:none'
evals:
name: 'Evals (ALWAYS_PASSING)'
needs:
- 'merge_queue_skipper'
- 'parse_run_context'
runs-on: 'gemini-cli-ubuntu-16-core'
if: |
github.repository == 'google-gemini/gemini-cli' && always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
steps:
- name: 'Checkout'
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
with:
ref: '${{ needs.parse_run_context.outputs.sha }}'
repository: '${{ needs.parse_run_context.outputs.repository }}'
persist-credentials: false
fetch-depth: 0
- name: 'Set up Node.js 20.x'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
with:
node-version: '20.x'
- name: 'Install dependencies'
run: 'npm ci'
- name: 'Build project'
run: 'npm run build'
- name: 'Check if evals should run'
id: 'check_evals'
run: |
SHOULD_RUN=$(node scripts/changed_prompt.js)
echo "should_run=$SHOULD_RUN" >> "$GITHUB_OUTPUT"
- name: 'Run Evals (Required to pass)'
if: "${{ steps.check_evals.outputs.should_run == 'true' }}"
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
GEMINI_CLI_TRUST_WORKSPACE: true
GEMINI_MODEL: 'gemini-3-pro-preview'
# Only run always passes behavioral tests.
EVAL_SUITE_TYPE: 'behavioral'
# Disable Vitest internal retries to avoid double-retrying;
# custom retry logic is handled in evals/test-helper.ts
VITEST_RETRY: 0
run: 'npm run test:always_passing_evals'
- name: 'Upload Reliability Logs'
if: "always() && steps.check_evals.outputs.should_run == 'true'"
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
with:
name: 'eval-logs-${{ github.run_id }}-${{ github.run_attempt }}'
path: 'evals/logs/api-reliability.jsonl'
retention-days: 7
e2e:
name: 'E2E'
if: |
github.repository == 'google-gemini/gemini-cli' && always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
needs:
- 'e2e_linux'
- 'e2e_mac'
- 'e2e_windows'
- 'evals'
- 'merge_queue_skipper'
runs-on: 'gemini-cli-ubuntu-16-core'
steps:
- name: 'Check E2E test results'
run: |
if [[ ${NEEDS_E2E_LINUX_RESULT} != 'success' || \
${NEEDS_E2E_MAC_RESULT} != 'success' || \
${NEEDS_E2E_WINDOWS_RESULT} != 'success' || \
${NEEDS_EVALS_RESULT} != 'success' ]]; then
echo "One or more E2E jobs failed."
exit 1
fi
echo "All required E2E jobs passed!"
env:
NEEDS_E2E_LINUX_RESULT: '${{ needs.e2e_linux.result }}'
NEEDS_E2E_MAC_RESULT: '${{ needs.e2e_mac.result }}'
NEEDS_E2E_WINDOWS_RESULT: '${{ needs.e2e_windows.result }}'
NEEDS_EVALS_RESULT: '${{ needs.evals.result }}'
set_workflow_status:
runs-on: 'gemini-cli-ubuntu-16-core'
permissions: 'write-all'
if: "github.repository == 'google-gemini/gemini-cli' && always()"
needs:
- 'parse_run_context'
- 'e2e'
steps:
- name: 'Set workflow status'
uses: 'myrotvorets/set-commit-status-action@16037e056d73b2d3c88e37e393ff369047f70886' # ratchet:myrotvorets/set-commit-status-action@master
if: "github.repository == 'google-gemini/gemini-cli' && always()"
with:
allowForks: 'true'
repo: '${{ github.repository }}'
sha: '${{ needs.parse_run_context.outputs.sha }}'
token: '${{ secrets.GITHUB_TOKEN }}'
status: '${{ needs.e2e.result }}'
context: 'E2E (Chained)'
+518
View File
@@ -0,0 +1,518 @@
name: 'Testing: CI'
on:
push:
branches:
- 'main'
- 'release/**'
pull_request:
branches:
- 'main'
- 'release/**'
merge_group:
workflow_dispatch:
inputs:
branch_ref:
description: 'Branch to run on'
required: true
default: 'main'
type: 'string'
concurrency:
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
cancel-in-progress: |-
${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/heads/release/') }}
permissions:
checks: 'write'
contents: 'read'
statuses: 'write'
defaults:
run:
shell: 'bash'
jobs:
merge_queue_skipper:
permissions: 'read-all'
name: 'Merge Queue Skipper'
runs-on: 'gemini-cli-ubuntu-16-core'
if: "github.repository == 'google-gemini/gemini-cli'"
outputs:
skip: '${{ steps.merge-queue-ci-skipper.outputs.skip-check }}'
steps:
- id: 'merge-queue-ci-skipper'
uses: 'cariad-tech/merge-queue-ci-skipper@1032489e59437862c90a08a2c92809c903883772' # ratchet:cariad-tech/merge-queue-ci-skipper@main
with:
secret: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
lint:
name: 'Lint'
runs-on: 'gemini-cli-ubuntu-16-core'
needs: 'merge_queue_skipper'
if: "github.repository == 'google-gemini/gemini-cli' && needs.merge_queue_skipper.outputs.skip == 'false'"
env:
GEMINI_LINT_TEMP_DIR: '${{ github.workspace }}/.gemini-linters'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
persist-credentials: false
ref: '${{ github.event.inputs.branch_ref || github.ref }}'
fetch-depth: 0
- name: 'Set up Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: 'Cache Linters'
uses: 'actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830' # ratchet:actions/cache@v4
with:
path: '${{ env.GEMINI_LINT_TEMP_DIR }}'
key: "${{ runner.os }}-${{ runner.arch }}-linters-${{ hashFiles('scripts/lint.js') }}"
- name: 'Install dependencies'
run: 'npm ci'
- name: 'Cache ESLint'
uses: 'actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830' # ratchet:actions/cache@v4
with:
path: '.eslintcache'
key: "${{ runner.os }}-eslint-${{ hashFiles('package-lock.json', 'eslint.config.js') }}"
- name: 'Validate NOTICES.txt'
run: 'git diff --exit-code packages/vscode-ide-companion/NOTICES.txt'
- name: 'Check lockfile'
run: 'npm run check:lockfile'
- name: 'Install linters'
run: 'node scripts/lint.js --setup'
- name: 'Run ESLint'
run: 'node scripts/lint.js --eslint'
- name: 'Run actionlint'
run: 'node scripts/lint.js --actionlint'
- name: 'Run shellcheck'
run: 'node scripts/lint.js --shellcheck'
- name: 'Run yamllint'
run: 'node scripts/lint.js --yamllint'
- name: 'Build project for typecheck'
run: 'npm run build'
- name: 'Run typecheck'
run: 'npm run typecheck'
- name: 'Run Prettier'
run: 'node scripts/lint.js --prettier'
- name: 'Build docs prerequisites'
run: 'npm run predocs:settings'
- name: 'Verify settings docs'
run: 'npm run docs:settings -- --check'
- name: 'Run sensitive keyword linter'
run: 'node scripts/lint.js --sensitive-keywords'
- name: 'Run GitHub Actions pinning linter'
run: 'node scripts/lint.js --check-github-actions-pinning'
link_checker:
name: 'Link Checker'
runs-on: 'ubuntu-latest'
if: "github.repository == 'google-gemini/gemini-cli'"
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
persist-credentials: false
- name: 'Link Checker'
uses: 'lycheeverse/lychee-action@885c65f3dc543b57c898c8099f4e08c8afd178a2' # ratchet: lycheeverse/lychee-action@v2.6.1
with:
args: '--verbose --accept 200,503 ./**/*.md'
fail: true
test_linux:
name: 'Test (Linux) - ${{ matrix.node-version }}, ${{ matrix.shard }}'
runs-on: 'gemini-cli-ubuntu-16-core'
needs:
- 'merge_queue_skipper'
if: "github.repository == 'google-gemini/gemini-cli' && needs.merge_queue_skipper.outputs.skip == 'false'"
permissions:
contents: 'read'
checks: 'write'
pull-requests: 'write'
strategy:
matrix:
node-version:
- '20.x'
- '22.x'
- '24.x'
shard:
- 'cli'
- 'others'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
persist-credentials: false
- name: 'Set up Node.js ${{ matrix.node-version }}'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
with:
node-version: '${{ matrix.node-version }}'
cache: 'npm'
- name: 'Build project'
run: 'npm run build'
- name: 'Install system dependencies'
run: |
sudo apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq bubblewrap
# Ubuntu 24.04+ requires this to allow bwrap to function in CI
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
- name: 'Install dependencies for testing'
run: 'npm ci'
- name: 'Run tests and generate reports'
env:
NO_COLOR: true
GEMINI_CLI_TRUST_WORKSPACE: true
run: |
if [[ "${{ matrix.shard }}" == "cli" ]]; then
npm run test:ci --workspace "@google/gemini-cli"
else
# Explicitly list non-cli packages to ensure they are sharded correctly
npm run test:ci --workspace "@google/gemini-cli-core" --workspace "@google/gemini-cli-a2a-server" --workspace "gemini-cli-vscode-ide-companion" --workspace "@google/gemini-cli-test-utils" --if-present -- --coverage.enabled=false
npm run test:scripts
fi
- name: 'Bundle'
run: 'npm run bundle'
- name: 'Smoke test bundle'
run: 'node ./bundle/gemini.js --version'
- name: 'Smoke test npx installation'
run: |
# 1. Package the project into a tarball
TARBALL=$(npm pack | tail -n 1)
# 2. Move to a fresh directory for isolation
mkdir -p ../smoke-test-dir
mv "$TARBALL" ../smoke-test-dir/
cd ../smoke-test-dir
# 3. Run npx from the tarball
npx "./$TARBALL" --version
- name: 'Wait for file system sync'
run: 'sleep 2'
- name: 'Publish Test Report (for non-forks)'
if: |-
${{ always() && (github.event.pull_request.head.repo.full_name == github.repository) }}
uses: 'dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3' # ratchet:dorny/test-reporter@v2
with:
name: 'Test Results (Node ${{ runner.os }}, ${{ matrix.node-version }}, ${{ matrix.shard }})'
path: 'packages/*/junit.xml'
reporter: 'java-junit'
fail-on-error: 'false'
- name: 'Upload Test Results Artifact (for forks)'
if: |-
${{ always() && (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) }}
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
with:
name: 'test-results-fork-${{ runner.os }}-${{ matrix.node-version }}-${{ matrix.shard }}'
path: 'packages/*/junit.xml'
test_mac:
name: 'Test (Mac) - ${{ matrix.node-version }}, ${{ matrix.shard }}'
runs-on: 'macos-latest-large'
needs:
- 'merge_queue_skipper'
if: "github.repository == 'google-gemini/gemini-cli' && needs.merge_queue_skipper.outputs.skip == 'false'"
permissions:
contents: 'read'
checks: 'write'
pull-requests: 'write'
continue-on-error: true
strategy:
matrix:
node-version:
- '20.x'
- '22.x'
- '24.x'
shard:
- 'cli'
- 'others'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
persist-credentials: false
- name: 'Set up Node.js ${{ matrix.node-version }}'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
with:
node-version: '${{ matrix.node-version }}'
cache: 'npm'
- name: 'Build project'
run: 'npm run build'
- name: 'Install dependencies for testing'
run: 'npm ci'
- name: 'Run tests and generate reports'
env:
NO_COLOR: true
GEMINI_CLI_TRUST_WORKSPACE: true
run: |
if [[ "${{ matrix.shard }}" == "cli" ]]; then
npm run test:ci --workspace "@google/gemini-cli" -- --coverage.enabled=false
else
# Explicitly list non-cli packages to ensure they are sharded correctly
npm run test:ci --workspace "@google/gemini-cli-core" --workspace "@google/gemini-cli-a2a-server" --workspace "gemini-cli-vscode-ide-companion" --workspace "@google/gemini-cli-test-utils" --if-present -- --coverage.enabled=false
npm run test:scripts
fi
- name: 'Bundle'
run: 'npm run bundle'
- name: 'Smoke test bundle'
run: 'node ./bundle/gemini.js --version'
- name: 'Smoke test npx installation'
run: |
# 1. Package the project into a tarball
TARBALL=$(npm pack | tail -n 1)
# 2. Move to a fresh directory for isolation
mkdir -p ../smoke-test-dir
mv "$TARBALL" ../smoke-test-dir/
cd ../smoke-test-dir
# 3. Run npx from the tarball
npx "./$TARBALL" --version
- name: 'Wait for file system sync'
run: 'sleep 2'
- name: 'Publish Test Report (for non-forks)'
if: |-
${{ always() && (github.event.pull_request.head.repo.full_name == github.repository) }}
uses: 'dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3' # ratchet:dorny/test-reporter@v2
with:
name: 'Test Results (Node ${{ runner.os }}, ${{ matrix.node-version }}, ${{ matrix.shard }})'
path: 'packages/*/junit.xml'
reporter: 'java-junit'
fail-on-error: 'false'
- name: 'Upload Test Results Artifact (for forks)'
if: |-
${{ always() && (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) }}
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
with:
name: 'test-results-fork-${{ runner.os }}-${{ matrix.node-version }}-${{ matrix.shard }}'
path: 'packages/*/junit.xml'
- name: 'Upload coverage reports'
if: |-
${{ always() }}
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
with:
name: 'coverage-reports-${{ runner.os }}-${{ matrix.node-version }}-${{ matrix.shard }}'
path: 'packages/*/coverage'
codeql:
name: 'CodeQL'
runs-on: 'gemini-cli-ubuntu-16-core'
needs: 'merge_queue_skipper'
if: "github.repository == 'google-gemini/gemini-cli' && needs.merge_queue_skipper.outputs.skip == 'false'"
permissions:
actions: 'read'
contents: 'read'
security-events: 'write'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
persist-credentials: false
ref: '${{ github.event.inputs.branch_ref || github.ref }}'
- name: 'Initialize CodeQL'
uses: 'github/codeql-action/init@df559355d593797519d70b90fc8edd5db049e7a2' # ratchet:github/codeql-action/init@v3
with:
languages: 'javascript'
- name: 'Perform CodeQL Analysis'
uses: 'github/codeql-action/analyze@df559355d593797519d70b90fc8edd5db049e7a2' # ratchet:github/codeql-action/analyze@v3
# Check for changes in bundle size.
bundle_size:
name: 'Check Bundle Size'
needs: 'merge_queue_skipper'
if: "github.repository == 'google-gemini/gemini-cli' && github.event_name == 'pull_request' && needs.merge_queue_skipper.outputs.skip == 'false'"
runs-on: 'gemini-cli-ubuntu-16-core'
permissions:
contents: 'read' # For checkout
pull-requests: 'write' # For commenting
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
persist-credentials: false
ref: '${{ github.event.inputs.branch_ref || github.ref }}'
fetch-depth: 1
- uses: 'preactjs/compressed-size-action@946a292cd35bd1088e0d7eb92b69d1a8d5b5d76a'
with:
repo-token: '${{ secrets.GITHUB_TOKEN }}'
pattern: './bundle/**/*.{js,sb}'
minimum-change-threshold: '1000'
compression: 'none'
clean-script: 'clean'
test_windows:
name: 'Slow Test - Win - ${{ matrix.shard }}'
runs-on: 'gemini-cli-windows-16-core'
needs: 'merge_queue_skipper'
if: "github.repository == 'google-gemini/gemini-cli' && needs.merge_queue_skipper.outputs.skip == 'false'"
timeout-minutes: 60
strategy:
matrix:
shard:
- 'cli'
- 'others'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
persist-credentials: false
ref: '${{ github.event.inputs.branch_ref || github.ref }}'
- name: 'Set up Node.js 20.x'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
- name: 'Configure Windows Defender exclusions'
run: |
Add-MpPreference -ExclusionPath $env:GITHUB_WORKSPACE -Force
Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\node_modules" -Force
Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\packages" -Force
Add-MpPreference -ExclusionPath "$env:TEMP" -Force
shell: 'pwsh'
- name: 'Configure npm for Windows performance'
run: |
npm config set progress false
npm config set audit false
npm config set fund false
npm config set loglevel error
npm config set maxsockets 32
npm config set registry https://registry.npmjs.org/
shell: 'pwsh'
- name: 'Install dependencies'
run: 'npm ci'
shell: 'pwsh'
- name: 'Build project'
run: 'npm run build'
shell: 'pwsh'
env:
NODE_OPTIONS: '--max-old-space-size=32768 --max-semi-space-size=256'
UV_THREADPOOL_SIZE: '32'
NODE_ENV: 'production'
- name: 'Run tests and generate reports'
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
NO_COLOR: true
GEMINI_CLI_TRUST_WORKSPACE: true
NODE_OPTIONS: '--max-old-space-size=32768 --max-semi-space-size=256'
UV_THREADPOOL_SIZE: '32'
NODE_ENV: 'test'
run: |
if ("${{ matrix.shard }}" -eq "cli") {
npm run test:ci --workspace "@google/gemini-cli" -- --coverage.enabled=false
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
} else {
# Explicitly list non-cli packages to ensure they are sharded correctly
npm run test:ci --workspace "@google/gemini-cli-core" --workspace "@google/gemini-cli-a2a-server" --workspace "gemini-cli-vscode-ide-companion" --workspace "@google/gemini-cli-test-utils" --if-present -- --coverage.enabled=false
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
npm run test:scripts
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}
shell: 'pwsh'
- name: 'Bundle'
run: 'npm run bundle'
shell: 'pwsh'
- name: 'Smoke test bundle'
run: 'node ./bundle/gemini.js --version'
shell: 'pwsh'
- name: 'Smoke test npx installation'
run: |
# 1. Package the project into a tarball
$PACK_OUTPUT = npm pack
$TARBALL = $PACK_OUTPUT[-1]
# 2. Move to a fresh directory for isolation
New-Item -ItemType Directory -Force -Path ../smoke-test-dir
Move-Item $TARBALL ../smoke-test-dir/
Set-Location ../smoke-test-dir
# 3. Run npx from the tarball
npx "./$TARBALL" --version
shell: 'pwsh'
ci:
name: 'CI'
if: "github.repository == 'google-gemini/gemini-cli' && always()"
needs:
- 'lint'
- 'link_checker'
- 'test_linux'
- 'test_mac'
- 'test_windows'
- 'codeql'
- 'bundle_size'
runs-on: 'gemini-cli-ubuntu-16-core'
steps:
- name: 'Check all job results'
run: |
if [[ (${NEEDS_LINT_RESULT} != 'success' && ${NEEDS_LINT_RESULT} != 'skipped') || \
(${NEEDS_LINK_CHECKER_RESULT} != 'success' && ${NEEDS_LINK_CHECKER_RESULT} != 'skipped') || \
(${NEEDS_TEST_LINUX_RESULT} != 'success' && ${NEEDS_TEST_LINUX_RESULT} != 'skipped') || \
(${NEEDS_TEST_MAC_RESULT} != 'success' && ${NEEDS_TEST_MAC_RESULT} != 'skipped') || \
(${NEEDS_TEST_WINDOWS_RESULT} != 'success' && ${NEEDS_TEST_WINDOWS_RESULT} != 'skipped') || \
(${NEEDS_CODEQL_RESULT} != 'success' && ${NEEDS_CODEQL_RESULT} != 'skipped') || \
(${NEEDS_BUNDLE_SIZE_RESULT} != 'success' && ${NEEDS_BUNDLE_SIZE_RESULT} != 'skipped') ]]; then
echo "One or more CI jobs failed."
exit 1
fi
echo "All CI jobs passed!"
env:
NEEDS_LINT_RESULT: '${{ needs.lint.result }}'
NEEDS_LINK_CHECKER_RESULT: '${{ needs.link_checker.result }}'
NEEDS_TEST_LINUX_RESULT: '${{ needs.test_linux.result }}'
NEEDS_TEST_MAC_RESULT: '${{ needs.test_mac.result }}'
NEEDS_TEST_WINDOWS_RESULT: '${{ needs.test_windows.result }}'
NEEDS_CODEQL_RESULT: '${{ needs.codeql.result }}'
NEEDS_BUNDLE_SIZE_RESULT: '${{ needs.bundle_size.result }}'
+200
View File
@@ -0,0 +1,200 @@
name: 'Generate Weekly Community Report 📊'
on:
schedule:
- cron: '0 12 * * 1' # Run at 12:00 UTC on Monday
workflow_dispatch:
inputs:
days:
description: 'Number of days to look back for the report'
required: true
default: '7'
jobs:
generate-report:
name: 'Generate Report 📝'
if: |-
${{ github.repository == 'google-gemini/gemini-cli' }}
runs-on: 'ubuntu-latest'
permissions:
issues: 'write'
pull-requests: 'read'
discussions: 'read'
contents: 'read'
id-token: 'write'
steps:
- name: 'Generate GitHub App Token 🔑'
id: 'generate_token'
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 }}'
permission-issues: 'write'
permission-pull-requests: 'read'
permission-discussions: 'read'
permission-contents: 'read'
- name: 'Generate Report 📜'
id: 'report'
env:
GH_TOKEN: '${{ steps.generate_token.outputs.token }}'
REPO: '${{ github.repository }}'
DAYS: '${{ github.event.inputs.days || 7 }}'
run: |-
set -e
START_DATE="$(date -u -d "$DAYS days ago" +'%Y-%m-%d')"
END_DATE="$(date -u +'%Y-%m-%d')"
echo "⏳ Generating report for contributions from ${START_DATE} to ${END_DATE}..."
declare -A author_is_googler
check_googler_status() {
local author="$1"
if [[ "${author}" == *"[bot]" ]]; then
author_is_googler[${author}]=1
return 1
fi
if [[ -v "author_is_googler[${author}]" ]]; then
return "${author_is_googler[${author}]}"
fi
if gh api "orgs/googlers/members/${author}" --silent 2>/dev/null; then
echo "🧑‍💻 ${author} is a Googler."
author_is_googler[${author}]=0
else
echo "🌍 ${author} is a community contributor."
author_is_googler[${author}]=1
fi
return "${author_is_googler[${author}]}"
}
googler_issues=0
non_googler_issues=0
googler_prs=0
non_googler_prs=0
echo "🔎 Fetching issues and pull requests..."
ITEMS_JSON="$(gh search issues --repo "${REPO}" "created:>${START_DATE}" --json author,isPullRequest --limit 1000)"
for row in $(echo "${ITEMS_JSON}" | jq -r '.[] | @base64'); do
_jq() {
echo "${row}" | base64 --decode | jq -r "${1}"
}
author="$(_jq '.author.login')"
is_pr="$(_jq '.isPullRequest')"
if [[ -z "${author}" || "${author}" == "null" ]]; then
continue
fi
if check_googler_status "${author}"; then
if [[ "${is_pr}" == "true" ]]; then
((googler_prs++))
else
((googler_issues++))
fi
else
if [[ "${is_pr}" == "true" ]]; then
((non_googler_prs++))
else
((non_googler_issues++))
fi
fi
done
googler_discussions=0
non_googler_discussions=0
echo "🗣️ Fetching discussions..."
DISCUSSION_QUERY='''
query($q: String!) {
search(query: $q, type: DISCUSSION, first: 100) {
nodes {
... on Discussion {
author {
login
}
}
}
}
}'''
DISCUSSIONS_JSON="$(gh api graphql -f q="repo:${REPO} created:>${START_DATE}" -f query="${DISCUSSION_QUERY}")"
for row in $(echo "${DISCUSSIONS_JSON}" | jq -r '.data.search.nodes[] | @base64'); do
_jq() {
echo "${row}" | base64 --decode | jq -r "${1}"
}
author="$(_jq '.author.login')"
if [[ -z "${author}" || "${author}" == "null" ]]; then
continue
fi
if check_googler_status "${author}"; then
((googler_discussions++))
else
((non_googler_discussions++))
fi
done
echo "✍️ Generating report content..."
TOTAL_ISSUES=$((googler_issues + non_googler_issues))
TOTAL_PRS=$((googler_prs + non_googler_prs))
TOTAL_DISCUSSIONS=$((googler_discussions + non_googler_discussions))
REPORT_BODY=$(cat <<EOF
### 💖 Community Contribution Report
**Period:** ${START_DATE} to ${END_DATE}
| Category | Googlers | Community | Total |
|---|---:|---:|---:|
| **Issues** | $googler_issues | $non_googler_issues | **$TOTAL_ISSUES** |
| **Pull Requests** | $googler_prs | $non_googler_prs | **$TOTAL_PRS** |
| **Discussions** | $googler_discussions | $non_googler_discussions | **$TOTAL_DISCUSSIONS** |
_This report was generated automatically by a GitHub Action._
EOF
)
echo "report_body<<EOF" >> "${GITHUB_OUTPUT}"
echo "${REPORT_BODY}" >> "${GITHUB_OUTPUT}"
echo "EOF" >> "${GITHUB_OUTPUT}"
echo "📊 Community Contribution Report:"
echo "${REPORT_BODY}"
- name: '🤖 Get Insights from Report'
if: |-
${{ steps.report.outputs.report_body != '' }}
uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0
env:
GITHUB_TOKEN: '${{ steps.generate_token.outputs.token }}'
REPOSITORY: '${{ github.repository }}'
with:
upload_artifacts: 'true'
gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}'
gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}'
use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
settings: |-
{
"tools": {
"core": [
"run_shell_command(gh issue list)",
"run_shell_command(gh pr list)",
"run_shell_command(gh search issues)",
"run_shell_command(gh search prs)"
]
}
}
prompt: |-
You are a helpful assistant that analyzes community contribution reports.
Based on the following report, please provide a brief summary and highlight any interesting trends or potential areas for improvement.
Report:
${{ steps.report.outputs.report_body }}
+178
View File
@@ -0,0 +1,178 @@
name: 'Deflake E2E'
on:
workflow_dispatch:
inputs:
branch_ref:
description: 'Branch to run on'
required: true
default: 'main'
type: 'string'
test_name_pattern:
description: 'The test name pattern to use'
required: false
type: 'string'
runs:
description: 'The number of runs'
required: false
default: 5
type: 'number'
concurrency:
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
cancel-in-progress: |-
${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/heads/release/') }}
jobs:
deflake_e2e_linux:
name: 'E2E Test (Linux) - ${{ matrix.sandbox }}'
runs-on: 'gemini-cli-ubuntu-16-core'
if: "github.repository == 'google-gemini/gemini-cli'"
strategy:
fail-fast: false
matrix:
sandbox:
- 'sandbox:none'
- 'sandbox:docker'
node-version:
- '20.x'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
with:
ref: '${{ github.event.pull_request.head.sha }}'
repository: '${{ github.repository }}'
persist-credentials: false
- name: 'Set up Node.js ${{ matrix.node-version }}'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
with:
node-version: '${{ matrix.node-version }}'
- name: 'Install dependencies'
run: 'npm ci'
- name: 'Build project'
run: 'npm run build'
- name: 'Set up Docker'
if: "matrix.sandbox == 'sandbox:docker'"
uses: 'docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435' # ratchet:docker/setup-buildx-action@v3
- name: 'Run E2E tests'
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
GEMINI_CLI_TRUST_WORKSPACE: true
IS_DOCKER: "${{ matrix.sandbox == 'sandbox:docker' }}"
KEEP_OUTPUT: 'true'
RUNS: '${{ github.event.inputs.runs }}'
TEST_NAME_PATTERN: '${{ github.event.inputs.test_name_pattern }}'
VERBOSE: 'true'
shell: 'bash'
run: |
if [[ "${IS_DOCKER}" == "true" ]]; then
npm run deflake:test:integration:sandbox:docker -- --runs="${RUNS}" -- --testNamePattern "'${TEST_NAME_PATTERN}'"
else
npm run deflake:test:integration:sandbox:none -- --runs="${RUNS}" -- --testNamePattern "'${TEST_NAME_PATTERN}'"
fi
deflake_e2e_mac:
name: 'E2E Test (macOS)'
runs-on: 'macos-latest-large'
if: "github.repository == 'google-gemini/gemini-cli'"
steps:
- name: 'Checkout'
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
with:
ref: '${{ github.event.pull_request.head.sha }}'
repository: '${{ github.repository }}'
persist-credentials: false
- name: 'Set up Node.js 20.x'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
with:
node-version: '20.x'
- name: 'Install dependencies'
run: 'npm ci'
- name: 'Build project'
run: 'npm run build'
- name: 'Fix rollup optional dependencies on macOS'
if: "runner.os == 'macOS'"
run: |
npm cache clean --force
- name: 'Run E2E tests (non-Windows)'
if: "runner.os != 'Windows'"
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
GEMINI_CLI_TRUST_WORKSPACE: true
KEEP_OUTPUT: 'true'
RUNS: '${{ github.event.inputs.runs }}'
SANDBOX: 'sandbox:none'
TEST_NAME_PATTERN: '${{ github.event.inputs.test_name_pattern }}'
VERBOSE: 'true'
run: |
npm run deflake:test:integration:sandbox:none -- --runs="${RUNS}" -- --testNamePattern "'${TEST_NAME_PATTERN}'"
deflake_e2e_windows:
name: 'Slow E2E - Win'
runs-on: 'gemini-cli-windows-16-core'
if: "github.repository == 'google-gemini/gemini-cli'"
steps:
- name: 'Checkout'
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
with:
ref: '${{ github.event.pull_request.head.sha }}'
repository: '${{ github.repository }}'
persist-credentials: false
- name: 'Set up Node.js 20.x'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
with:
node-version: '20.x'
cache: 'npm'
- name: 'Configure Windows Defender exclusions'
run: |
Add-MpPreference -ExclusionPath $env:GITHUB_WORKSPACE -Force
Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\node_modules" -Force
Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\packages" -Force
Add-MpPreference -ExclusionPath "$env:TEMP" -Force
shell: 'pwsh'
- name: 'Configure npm for Windows performance'
run: |
npm config set progress false
npm config set audit false
npm config set fund false
npm config set loglevel error
npm config set maxsockets 32
npm config set registry https://registry.npmjs.org/
shell: 'pwsh'
- name: 'Install dependencies'
run: 'npm ci'
shell: 'pwsh'
- name: 'Build project'
run: 'npm run build'
shell: 'pwsh'
- name: 'Run E2E tests'
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
GEMINI_CLI_TRUST_WORKSPACE: true
KEEP_OUTPUT: 'true'
SANDBOX: 'sandbox:none'
VERBOSE: 'true'
NODE_OPTIONS: '--max-old-space-size=32768 --max-semi-space-size=256'
UV_THREADPOOL_SIZE: '32'
NODE_ENV: 'test'
RUNS: '${{ github.event.inputs.runs }}'
TEST_NAME_PATTERN: '${{ github.event.inputs.test_name_pattern }}'
shell: 'pwsh'
run: |
npm run deflake:test:integration:sandbox:none -- --runs="$env:RUNS" -- --testNamePattern "'$env:TEST_NAME_PATTERN'"
+66
View File
@@ -0,0 +1,66 @@
name: 'Automated Documentation Audit'
on:
schedule:
# Runs every Monday at 00:00 UTC
- cron: '0 0 * * MON'
workflow_dispatch:
jobs:
audit-docs:
runs-on: 'ubuntu-latest'
permissions:
contents: 'write'
pull-requests: 'write'
steps:
- name: 'Checkout repository'
uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5'
with:
fetch-depth: 0
ref: 'main'
persist-credentials: false
- name: 'Set up Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
with:
node-version: '20'
- name: 'Run Docs Audit with Gemini'
id: 'run_gemini'
uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31'
env:
GEMINI_CLI_TRUST_WORKSPACE: true
with:
upload_artifacts: 'true'
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
prompt: |
Activate the 'docs-writer' skill.
**Task:** Execute the docs audit procedure, as defined in your 'docs-auditing.md' reference.
Provide a detailed summary of the changes you make.
- name: 'Get current date'
id: 'date'
run: |
echo "date=$(date +'%Y-%m-%d')" >> "$GITHUB_OUTPUT"
- name: 'Create Pull Request with Audit Results'
uses: 'peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c'
with:
token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
commit-message: 'docs: weekly audit results for ${{ github.run_id }}'
title: 'Docs audit: ${{ steps.date.outputs.date }}'
body: |
This PR contains the auto-generated documentation audit for the week. It includes a new `audit-results-*.md` file with findings and any direct fixes applied by the agent.
### Audit Summary:
${{ steps.run_gemini.outputs.summary || 'No summary provided.' }}
Please review the suggestions and merge.
Related to #25152
branch: 'docs-audit-${{ github.run_id }}'
base: 'main'
team-reviewers: 'gemini-cli-docs, gemini-cli-maintainers'
delete-branch: true
+52
View File
@@ -0,0 +1,52 @@
name: 'Deploy GitHub Pages'
on:
push:
tags: 'v*'
workflow_dispatch:
permissions:
contents: 'read'
pages: 'write'
id-token: 'write'
# Allow only one concurrent deployment, skipping runs queued between the run
# in-progress and latest queued. However, do NOT cancel in-progress runs as we
# want to allow these production deployments to complete.
concurrency:
group: '${{ github.workflow }}'
cancel-in-progress: false
jobs:
build:
if: "github.repository == 'google-gemini/gemini-cli' && !contains(github.ref_name, 'nightly')"
runs-on: 'ubuntu-latest'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
persist-credentials: false
- name: 'Setup Pages'
uses: 'actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b' # ratchet:actions/configure-pages@v5
- name: 'Build with Jekyll'
uses: 'actions/jekyll-build-pages@44a6e6beabd48582f863aeeb6cb2151cc1716697' # ratchet:actions/jekyll-build-pages@v1
with:
source: './'
destination: './_site'
- name: 'Upload artifact'
uses: 'actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa' # ratchet:actions/upload-pages-artifact@v3
deploy:
if: "github.repository == 'google-gemini/gemini-cli'"
environment:
name: 'github-pages'
url: '${{ steps.deployment.outputs.page_url }}'
runs-on: 'ubuntu-latest'
needs: 'build'
steps:
- name: 'Deploy to GitHub Pages'
id: 'deployment'
uses: 'actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e' # ratchet:actions/deploy-pages@v4
+18
View File
@@ -0,0 +1,18 @@
name: 'Trigger Docs Rebuild'
on:
push:
branches:
- 'main'
paths:
- 'docs/**'
jobs:
trigger-rebuild:
if: "github.repository == 'google-gemini/gemini-cli'"
runs-on: 'ubuntu-latest'
steps:
- name: 'Trigger rebuild'
run: |
curl -X POST \
-H "Content-Type: application/json" \
-d '{}' \
"${{ secrets.DOCS_REBUILD_URL }}"
+211
View File
@@ -0,0 +1,211 @@
name: 'Evals: PR Evaluation & Regression'
on:
pull_request_target:
types: ['opened', 'synchronize', 'reopened', 'ready_for_review']
paths:
- 'packages/core/src/prompts/**'
- 'packages/core/src/tools/**'
- 'packages/core/src/agents/**'
- 'evals/**'
- '!**/*.test.ts'
- '!**/*.test.tsx'
workflow_dispatch:
# Prevents multiple runs for the same PR simultaneously (saves tokens)
concurrency:
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
cancel-in-progress: true
permissions:
pull-requests: 'write'
contents: 'read'
actions: 'read'
jobs:
detect-changes:
name: 'Detect Steering Changes'
runs-on: 'gemini-cli-ubuntu-16-core'
# Security: pull_request_target allows secrets, so we must gate carefully.
# Detection should not run code from the fork.
if: "github.repository == 'google-gemini/gemini-cli' && github.event.pull_request.draft == false"
outputs:
SHOULD_RUN: '${{ steps.detect.outputs.SHOULD_RUN }}'
STEERING_DETECTED: '${{ steps.detect.outputs.STEERING_DETECTED }}'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
with:
# Check out the trusted code from main for detection
fetch-depth: 0
persist-credentials: false
- name: 'Detect Steering Changes'
id: 'detect'
env:
# Use the PR's head SHA for comparison without checking it out
PR_HEAD_SHA: '${{ github.event.pull_request.head.sha }}'
run: |
# Fetch the fork's PR branch for analysis
git fetch origin pull/${{ github.event.pull_request.number }}/head:pr-head
# Run the trusted script from main
SHOULD_RUN=$(node scripts/changed_prompt.js)
STEERING_DETECTED=$(node scripts/changed_prompt.js --steering-only)
echo "SHOULD_RUN=$SHOULD_RUN" >> "$GITHUB_OUTPUT"
echo "STEERING_DETECTED=$STEERING_DETECTED" >> "$GITHUB_OUTPUT"
- name: 'Notify Approval Required'
if: "steps.detect.outputs.SHOULD_RUN == 'true'"
env:
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
run: |
RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
COMMENT_BODY="### 🛑 Action Required: Evaluation Approval
Steering changes have been detected in this PR. To prevent regressions, a maintainer must approve the evaluation run before this PR can be merged.
**Maintainers:**
1. Go to the [**Workflow Run Summary**]($RUN_URL).
2. Click the yellow **'Review deployments'** button.
3. Select the **'eval-gate'** environment and click **'Approve'**.
Once approved, the evaluation results will be posted here automatically.
<!-- eval-approval-notification -->"
# Check if comment already exists to avoid spamming
COMMENT_ID=$(gh pr view ${{ github.event.pull_request.number }} --json comments --jq '.comments[] | select(.body | contains("<!-- eval-approval-notification -->")) | .url' | grep -oE "[0-9]+$" | head -n 1)
if [ -z "$COMMENT_ID" ]; then
gh pr comment ${{ github.event.pull_request.number }} --body "$COMMENT_BODY"
else
echo "Updating existing notification comment $COMMENT_ID..."
gh api -X PATCH "repos/${{ github.repository }}/issues/comments/$COMMENT_ID" -F body="$COMMENT_BODY"
fi
pr-evaluation:
name: 'Evaluate Steering & Regressions'
needs: 'detect-changes'
if: "needs.detect-changes.outputs.SHOULD_RUN == 'true'"
# Manual approval gate via environment
environment: 'eval-gate'
runs-on: 'gemini-cli-ubuntu-16-core'
env:
# CENTRALIZED MODEL LIST
MODEL_LIST: 'gemini-3-flash-preview'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
with:
# Check out the fork's PR code for the actual evaluation
# This only runs AFTER manual approval
ref: '${{ github.event.pull_request.head.sha }}'
fetch-depth: 0
persist-credentials: false
- name: 'Remove Approval Notification'
# Run even if other steps fail, to ensure we clean up the "Action Required" message
if: 'always()'
env:
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
PR_NUMBER: '${{ github.event.pull_request.number }}'
run: |
echo "Debug: PR_NUMBER is '$PR_NUMBER'"
# Search for the notification comment by its hidden tag
COMMENT_ID=$(gh pr view "$PR_NUMBER" --json comments --jq '.comments[] | select(.body | contains("<!-- eval-approval-notification -->")) | .url' | grep -oE "[0-9]+$" | head -n 1)
if [ -n "$COMMENT_ID" ]; then
echo "Removing notification comment $COMMENT_ID now that run is approved..."
gh api -X DELETE "repos/${{ github.repository }}/issues/comments/$COMMENT_ID"
fi
- name: 'Set up Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: 'Install dependencies'
run: 'npm ci'
- name: 'Build project'
run: 'npm run build'
- name: 'Analyze PR Content (Guidance)'
if: "needs.detect-changes.outputs.STEERING_DETECTED == 'true'"
id: 'analysis'
env:
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
run: |
# Check for behavioral eval changes
EVAL_CHANGES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep "^evals/" || true)
if [ -z "$EVAL_CHANGES" ]; then
echo "MISSING_EVALS=true" >> "$GITHUB_OUTPUT"
fi
# Check if user is a maintainer
USER_PERMISSION=$(gh api repos/${{ github.repository }}/collaborators/${{ github.actor }}/permission --jq '.permission')
if [[ "$USER_PERMISSION" == "admin" || "$USER_PERMISSION" == "write" ]]; then
echo "IS_MAINTAINER=true" >> "$GITHUB_OUTPUT"
fi
- name: 'Execute Regression Check'
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
MODEL_LIST: '${{ env.MODEL_LIST }}'
run: |
# Run the regression check loop. The script saves the report to a file.
node scripts/run_eval_regression.js
# Use the generated report file if it exists
if [[ -f eval_regression_report.md ]]; then
echo "REPORT_FILE=eval_regression_report.md" >> "$GITHUB_ENV"
fi
- name: 'Post or Update PR Comment'
if: "always() && (needs.detect-changes.outputs.STEERING_DETECTED == 'true' || env.REPORT_FILE != '')"
env:
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
run: |
# 1. Build the full comment body
{
if [[ -f eval_regression_report.md ]]; then
cat eval_regression_report.md
echo ""
fi
if [[ "${{ needs.detect-changes.outputs.STEERING_DETECTED }}" == "true" ]]; then
echo "### 🧠 Model Steering Guidance"
echo ""
echo "This PR modifies files that affect the model's behavior (prompts, tools, or instructions)."
echo ""
if [[ "${{ steps.analysis.outputs.MISSING_EVALS }}" == "true" ]]; then
echo "- ⚠️ **Consider adding Evals:** No behavioral evaluations (\`evals/*.eval.ts\`) were added or updated in this PR. Consider [adding a test case](https://github.com/google-gemini/gemini-cli/blob/main/evals/README.md#creating-an-evaluation) to verify the new behavior and prevent regressions."
fi
if [[ "${{ steps.analysis.outputs.IS_MAINTAINER }}" == "true" ]]; then
echo "- 🚀 **Maintainer Reminder:** Please ensure that these changes do not regress results on benchmark evals before merging."
fi
fi
echo ""
echo "---"
echo "*This is an automated guidance message triggered by steering logic signatures.*"
echo "<!-- eval-pr-report -->"
} > full_comment.md
# 2. Find if a comment with our unique tag already exists
# We extract the numeric ID from the URL to ensure compatibility with the REST API
COMMENT_ID=$(gh pr view ${{ github.event.pull_request.number }} --json comments --jq '.comments[] | select(.body | contains("<!-- eval-pr-report -->")) | .url' | grep -oE "[0-9]+$" | head -n 1)
# 3. Update or Create the comment
if [ -n "$COMMENT_ID" ]; then
echo "Updating existing comment $COMMENT_ID via API..."
gh api -X PATCH "repos/${{ github.repository }}/issues/comments/$COMMENT_ID" -F body=@full_comment.md
else
echo "Creating new PR comment..."
gh pr comment ${{ github.event.pull_request.number }} --body-file full_comment.md
fi
+48
View File
@@ -0,0 +1,48 @@
name: 'Eval'
on:
workflow_dispatch:
defaults:
run:
shell: 'bash'
permissions:
contents: 'read'
id-token: 'write'
packages: 'read'
jobs:
eval:
name: 'Eval'
if: >-
github.repository == 'google-gemini/gemini-cli'
runs-on: 'ubuntu-latest'
container:
image: 'ghcr.io/google-gemini/gemini-cli-swe-agent-eval@sha256:cd5edc4afd2245c1f575e791c0859b3c084a86bb3bd9a6762296da5162b35a8f'
credentials:
username: '${{ github.actor }}'
password: '${{ secrets.GITHUB_TOKEN }}'
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
DEFAULT_VERTEXAI_PROJECT: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
GOOGLE_CLOUD_PROJECT: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
GEMINI_API_KEY: '${{ secrets.EVAL_GEMINI_API_KEY }}'
GCLI_LOCAL_FILE_TELEMETRY: 'True'
EVAL_GCS_BUCKET: '${{ vars.EVAL_GCS_ARTIFACTS_BUCKET }}'
steps:
- name: 'Authenticate to Google Cloud'
id: 'auth'
uses: 'google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed' # ratchet:exclude pin@v2.1.7
with:
project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
token_format: 'access_token'
access_token_scopes: 'https://www.googleapis.com/auth/cloud-platform'
- name: 'Run evaluation'
working-directory: '/app'
run: |
poetry run exp_run --experiment-mode=on-demand --branch-or-commit="${GITHUB_REF_NAME}" --model-name=gemini-2.5-pro --dataset=swebench_verified --concurrency=15
poetry run python agent_prototypes/scripts/parse_gcli_logs_experiment.py --experiment_dir=experiments/adhoc/gcli_temp_exp --gcs-bucket="${EVAL_GCS_BUCKET}" --gcs-path=gh_action_artifacts
+121
View File
@@ -0,0 +1,121 @@
name: 'Evals: Nightly'
on:
schedule:
- cron: '0 1 * * *' # Runs at 1 AM every day
workflow_dispatch:
inputs:
suite_type:
description: 'Suite type to run'
type: 'choice'
options:
- 'behavioral'
- 'component-level'
- 'hero-scenario'
default: 'behavioral'
suite_name:
description: 'Specific suite name to run'
required: false
type: 'string'
test_name_pattern:
description: 'Test name pattern or file name'
required: false
type: 'string'
permissions:
contents: 'read'
checks: 'write'
actions: 'read'
jobs:
evals:
name: 'Evals (USUALLY_PASSING) nightly run'
runs-on: 'gemini-cli-ubuntu-16-core'
if: "github.repository == 'google-gemini/gemini-cli'"
strategy:
fail-fast: false
matrix:
model:
- 'gemini-3.1-pro-preview-customtools'
- 'gemini-3-pro-preview'
- 'gemini-3-flash-preview'
- 'gemini-2.5-pro'
- 'gemini-2.5-flash'
- 'gemini-2.5-flash-lite'
run_attempt: [1, 2, 3]
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
persist-credentials: false
- name: 'Set up Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: 'Install dependencies'
run: 'npm ci'
- name: 'Build project'
run: 'npm run build'
- name: 'Create logs directory'
run: 'mkdir -p evals/logs'
- name: 'Run Evals'
continue-on-error: true
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
GEMINI_CLI_TRUST_WORKSPACE: true
GEMINI_MODEL: '${{ matrix.model }}'
RUN_EVALS: 'true'
EVAL_SUITE_TYPE: "${{ github.event.inputs.suite_type || 'behavioral' }}"
EVAL_SUITE_NAME: '${{ github.event.inputs.suite_name }}'
TEST_NAME_PATTERN: '${{ github.event.inputs.test_name_pattern }}'
# Disable Vitest internal retries to avoid double-retrying;
# custom retry logic is handled in evals/test-helper.ts
VITEST_RETRY: 0
run: |
CMD="npm run test:all_evals"
PATTERN="${TEST_NAME_PATTERN}"
if [[ -n "$PATTERN" ]]; then
if [[ "$PATTERN" == *.ts || "$PATTERN" == *.js || "$PATTERN" == */* ]]; then
$CMD -- "$PATTERN"
else
$CMD -- -t "$PATTERN"
fi
else
$CMD
fi
- name: 'Upload Logs'
if: 'always()'
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
with:
name: 'eval-logs-${{ matrix.model }}-${{ matrix.run_attempt }}'
path: 'evals/logs'
retention-days: 7
aggregate-results:
name: 'Aggregate Results'
needs: ['evals']
if: "github.repository == 'google-gemini/gemini-cli' && always()"
runs-on: 'gemini-cli-ubuntu-16-core'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
persist-credentials: false
- name: 'Download Logs'
uses: 'actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806' # ratchet:actions/download-artifact@v4
with:
path: 'artifacts'
- name: 'Generate Summary'
env:
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
run: 'node scripts/aggregate_evals.js artifacts >> "$GITHUB_STEP_SUMMARY"'
@@ -0,0 +1,267 @@
name: '🏷️ Gemini Automated Issue Deduplication'
on:
issues:
types:
- 'opened'
- 'reopened'
issue_comment:
types:
- 'created'
workflow_dispatch:
inputs:
issue_number:
description: 'issue number to dedup'
required: true
type: 'number'
concurrency:
group: '${{ github.workflow }}-${{ github.event.issue.number }}'
cancel-in-progress: true
defaults:
run:
shell: 'bash'
jobs:
find-duplicates:
if: |-
github.repository == 'google-gemini/gemini-cli' &&
vars.TRIAGE_DEDUPLICATE_ISSUES != '' &&
(github.event_name == 'issues' ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'issue_comment' &&
contains(github.event.comment.body, '@gemini-cli /deduplicate') &&
(github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR')))
permissions:
contents: 'read'
id-token: 'write' # Required for WIF, see https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-google-cloud-platform#adding-permissions-settings
issues: 'read'
statuses: 'read'
packages: 'read'
timeout-minutes: 20
runs-on: 'ubuntu-latest'
outputs:
duplicate_issues_csv: '${{ env.DUPLICATE_ISSUES_CSV }}'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
persist-credentials: false
- name: 'Log in to GitHub Container Registry'
uses: 'docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1' # ratchet:docker/login-action@v3
with:
registry: 'ghcr.io'
username: '${{ github.actor }}'
password: '${{ secrets.GITHUB_TOKEN }}'
- name: 'Find Duplicate Issues'
uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0
id: 'gemini_issue_deduplication'
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
ISSUE_TITLE: '${{ github.event.issue.title }}'
ISSUE_BODY: '${{ github.event.issue.body }}'
ISSUE_NUMBER: '${{ github.event.issue.number }}'
REPOSITORY: '${{ github.repository }}'
FIRESTORE_PROJECT: '${{ vars.FIRESTORE_PROJECT }}'
GEMINI_CLI_TRUST_WORKSPACE: 'true'
with:
upload_artifacts: 'true'
gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}'
gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}'
use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
settings: |-
{
"mcpServers": {
"issue_deduplication": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"--network", "host",
"-e", "GITHUB_TOKEN",
"-e", "GEMINI_API_KEY",
"-e", "DATABASE_TYPE",
"-e", "FIRESTORE_DATABASE_ID",
"-e", "GCP_PROJECT",
"-e", "GOOGLE_APPLICATION_CREDENTIALS=/app/gcp-credentials.json",
"-v", "${GOOGLE_APPLICATION_CREDENTIALS}:/app/gcp-credentials.json",
"ghcr.io/google-gemini/gemini-cli-issue-triage@sha256:e3de1523f6c83aabb3c54b76d08940a2bf42febcb789dd2da6f95169641f94d3"
],
"env": {
"GITHUB_TOKEN": "${GITHUB_TOKEN}",
"GEMINI_API_KEY": "${{ secrets.GEMINI_API_KEY }}",
"DATABASE_TYPE":"firestore",
"GCP_PROJECT": "${FIRESTORE_PROJECT}",
"FIRESTORE_DATABASE_ID": "(default)",
"GOOGLE_APPLICATION_CREDENTIALS": "${GOOGLE_APPLICATION_CREDENTIALS}"
},
"timeout": 600000
}
},
"maxSessionTurns": 25,
"tools": {
"core": [
"run_shell_command(echo)",
"run_shell_command(gh issue view)"
]
},
"telemetry": {
"enabled": true,
"target": "gcp"
}
}
prompt: |-
## Role
You are an issue de-duplication assistant. Your goal is to find
duplicate issues for a given issue.
## Steps
1. **Find Potential Duplicates:**
- The repository is ${{ github.repository }} and the issue number is ${{ github.event.issue.number }}.
- Use the `duplicates` tool with the `repo` and `issue_number` to find potential duplicates for the current issue. Do not use the `threshold` parameter.
- If no duplicates are found, you are done.
- Print the JSON output from the `duplicates` tool to the logs.
2. **Refine Duplicates List (if necessary):**
- If the `duplicates` tool returns between 1 and 14 results, you must refine the list.
- For each potential duplicate issue, run `gh issue view <issue-number> --json title,body,comments` to fetch its content.
- Also fetch the content of the original issue: `gh issue view "${ISSUE_NUMBER}" --json title,body,comments`.
- Carefully analyze the content (title, body, comments) of the original issue and all potential duplicates.
- It is very important if the comments on either issue mention that they are not duplicates of each other, to treat them as not duplicates.
- Based on your analysis, create a final list containing only the issues you are highly confident are actual duplicates.
- If your final list is empty, you are done.
- Print to the logs if you omitted any potential duplicates based on your analysis.
- If the `duplicates` tool returned 15+ results, use the top 15 matches (based on descending similarity score value) to perform this step.
3. **Output final duplicates list as CSV:**
- Convert the list of appropriate duplicate issue numbers into a comma-separated list (CSV). If there are no appropriate duplicates, use the empty string.
- Use the "echo" shell command to append the CSV of issue numbers into the filepath referenced by the environment variable "${GITHUB_ENV}":
echo "DUPLICATE_ISSUES_CSV=[DUPLICATE_ISSUES_AS_CSV]" >> "${GITHUB_ENV}"
## Guidelines
- Only use the `duplicates` and `run_shell_command` tools.
- The `run_shell_command` tool can be used with `gh issue view`.
- Do not download or read media files like images, videos, or links. The `--json` flag for `gh issue view` will prevent this.
- Do not modify the issue content or status.
- Do not add comments or labels.
- Reference all shell variables as "${VAR}" (with quotes and braces).
add-comment-and-label:
needs: 'find-duplicates'
if: |-
github.repository == 'google-gemini/gemini-cli' &&
vars.TRIAGE_DEDUPLICATE_ISSUES != '' &&
needs.find-duplicates.outputs.duplicate_issues_csv != '' &&
(
github.event_name == 'issues' ||
github.event_name == 'workflow_dispatch' ||
(
github.event_name == 'issue_comment' &&
contains(github.event.comment.body, '@gemini-cli /deduplicate') &&
(
github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR'
)
)
)
permissions:
issues: 'write'
timeout-minutes: 5
runs-on: 'ubuntu-latest'
steps:
- name: 'Generate GitHub App Token'
id: 'generate_token'
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 }}'
permission-issues: 'write'
- name: 'Comment and Label Duplicate Issue'
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
env:
DUPLICATES_OUTPUT: '${{ needs.find-duplicates.outputs.duplicate_issues_csv }}'
with:
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
script: |-
const rawCsv = process.env.DUPLICATES_OUTPUT;
core.info(`Raw duplicates CSV: ${rawCsv}`);
const duplicateIssues = rawCsv.split(',').map(s => s.trim()).filter(s => s);
if (duplicateIssues.length === 0) {
core.info('No duplicate issues found. Nothing to do.');
return;
}
const issueNumber = ${{ github.event.issue.number }};
function formatCommentBody(issues, updated = false) {
const header = updated
? 'Found possible duplicate issues (updated):'
: 'Found possible duplicate issues:';
const issuesList = issues.map(num => `- #${num}`).join('\n');
const footer = 'If you believe this is not a duplicate, please remove the `status/possible-duplicate` label.';
const magicComment = '<!-- gemini-cli-deduplication -->';
return `${header}\n\n${issuesList}\n\n${footer}\n${magicComment}`;
}
const newCommentBody = formatCommentBody(duplicateIssues);
const newUpdatedCommentBody = formatCommentBody(duplicateIssues, true);
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
});
const magicComment = '<!-- gemini-cli-deduplication -->';
const existingComment = comments.find(comment =>
comment.user.type === 'Bot' && comment.body.includes(magicComment)
);
let commentMade = false;
if (existingComment) {
// To check if lists are same, just compare the formatted bodies without headers.
const existingBodyForCompare = existingComment.body.substring(existingComment.body.indexOf('- #'));
const newBodyForCompare = newCommentBody.substring(newCommentBody.indexOf('- #'));
if (existingBodyForCompare.trim() !== newBodyForCompare.trim()) {
core.info(`Updating existing comment ${existingComment.id}`);
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existingComment.id,
body: newUpdatedCommentBody,
});
commentMade = true;
} else {
core.info('Existing comment is up-to-date. Nothing to do.');
}
} else {
core.info('Creating new comment.');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: newCommentBody,
});
commentMade = true;
}
if (commentMade) {
core.info('Adding "status/possible-duplicate" label.');
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: ['status/possible-duplicate'],
});
}
@@ -0,0 +1,392 @@
name: '🏷️ Gemini Automated Issue Triage'
on:
issues:
types:
- 'opened'
- 'reopened'
issue_comment:
types:
- 'created'
workflow_dispatch:
inputs:
issue_number:
description: 'issue number to triage'
required: true
type: 'number'
workflow_call:
inputs:
issue_number:
description: 'issue number to triage'
required: false
type: 'string'
concurrency:
group: '${{ github.workflow }}-${{ github.event.issue.number || github.event.inputs.issue_number || inputs.issue_number }}'
cancel-in-progress: true
defaults:
run:
shell: 'bash'
permissions:
contents: 'read'
id-token: 'write'
issues: 'write'
statuses: 'write'
packages: 'read'
actions: 'write' # Required for cancelling a workflow run
jobs:
triage-issue:
if: |-
(github.repository == 'google-gemini/gemini-cli' || github.repository == 'google-gemini/maintainers-gemini-cli') &&
(
github.event_name == 'workflow_dispatch' ||
(
(github.event_name == 'issues' || github.event_name == 'issue_comment') &&
(github.event_name != 'issue_comment' || (
contains(github.event.comment.body, '@gemini-cli /triage') &&
(github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'COLLABORATOR')
))
)
) &&
!contains(github.event.issue.labels.*.name, 'area/')
timeout-minutes: 5
runs-on: 'ubuntu-latest'
steps:
- name: 'Get issue data for manual trigger'
id: 'get_issue_data'
if: |-
github.event_name == 'workflow_dispatch'
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
with:
github-token: '${{ secrets.GITHUB_TOKEN }}'
script: |
const issueNumber = ${{ github.event.inputs.issue_number || inputs.issue_number }};
const { data: issue } = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
});
core.setOutput('title', issue.title);
core.setOutput('body', issue.body);
core.setOutput('labels', issue.labels.map(label => label.name).join(','));
return issue;
- name: 'Manual Trigger Pre-flight Checks'
if: |-
github.event_name == 'workflow_dispatch'
env:
ISSUE_NUMBER_INPUT: '${{ github.event.inputs.issue_number || inputs.issue_number }}'
LABELS: '${{ steps.get_issue_data.outputs.labels }}'
run: |
if echo "${LABELS}" | grep -q 'area/'; then
echo "Issue #${ISSUE_NUMBER_INPUT} already has 'area/' label. Stopping workflow."
exit 1
fi
echo "Manual triage checks passed."
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
persist-credentials: false
- name: 'Generate GitHub App Token'
id: 'generate_token'
env:
APP_ID: '${{ secrets.APP_ID }}'
if: |-
${{ env.APP_ID != '' }}
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 }}'
permission-issues: 'write'
- name: 'Get Repository Labels'
id: 'get_labels'
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
with:
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
script: |-
const { data: labels } = await github.rest.issues.listLabelsForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
});
const allowedLabels = [
'area/agent',
'area/enterprise',
'area/non-interactive',
'area/core',
'area/security',
'area/platform',
'area/extensions',
'area/documentation',
'area/unknown'
];
const labelNames = labels.map(label => label.name).filter(name => allowedLabels.includes(name));
core.setOutput('available_labels', labelNames.join(','));
core.info(`Found ${labelNames.length} labels: ${labelNames.join(', ')}`);
return labelNames;
- name: 'Prepare Issue Data'
id: 'prepare_issue_data'
env:
ISSUE_TITLE: >-
${{ github.event_name == 'workflow_dispatch' && steps.get_issue_data.outputs.title || github.event.issue.title }}
ISSUE_BODY: >-
${{ github.event_name == 'workflow_dispatch' && steps.get_issue_data.outputs.body || github.event.issue.body }}
run: |
set -euo pipefail
echo "Title: ${ISSUE_TITLE}" > issue_context.md
echo "Body:" >> issue_context.md
echo "${ISSUE_BODY}" >> issue_context.md
- name: 'Run Gemini Issue Analysis'
uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0
id: 'gemini_issue_analysis'
env:
GITHUB_TOKEN: '' # Do not pass any auth token here since this runs on untrusted inputs
ISSUE_NUMBER: >-
${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.issue_number || inputs.issue_number) || github.event.issue.number }}
REPOSITORY: '${{ github.repository }}'
AVAILABLE_LABELS: '${{ steps.get_labels.outputs.available_labels }}'
GEMINI_CLI_TRUST_WORKSPACE: 'true'
with:
upload_artifacts: 'true'
gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}'
gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}'
use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
settings: |-
{
"maxSessionTurns": 25,
"telemetry": {
"enabled": true,
"target": "gcp"
},
"tools": {
"core": [
"run_shell_command(echo)",
"read_file"
]
}
}
prompt: |-
## Role
You are an issue triage assistant. Your role is to analyze a GitHub issue and determine the single most appropriate area/ label based on the definitions provided.
## Steps
1. Use the read_file tool to read the file "issue_context.md" which contains the issue title and body.
2. Review the available labels: ${{ env.AVAILABLE_LABELS }}.
3. Select exactly one area/ label that best matches the issue based on Reference 1: Area Definitions.
4. Fallback Logic:
- If you cannot confidently determine the correct area/ label from the definitions, you must use area/unknown.
5. Output your selected label in JSON format and nothing else. Example:
{"labels_to_set": ["area/core"]}
## Guidelines
- Your output must contain exactly one area/ label.
- Triage only the current issue based on its title and body.
- Output only valid JSON format.
- Do not include any explanation or additional text, just the JSON.
Reference 1: Area Definitions
area/agent
- Description: Issues related to the "brain" of the CLI. This includes the core agent logic, model quality, tool/function calling, and memory.
- Example Issues:
"I am not getting a reasonable or expected response."
"The model is not calling the tool I expected."
"The web search tool is not working as expected."
"Feature request for a new built-in tool (e.g., read file, write file)."
"The generated code is poor quality or incorrect."
"The model seems stuck in a loop."
"The response from the model is malformed (e.g., broken JSON, bad formatting)."
"Concerns about unnecessary token consumption."
"Issues with how memory or chat history is managed."
"Issues with sub-agents."
"Model is switching from one to another unexpectedly."
area/enterprise
- Description: Issues specific to enterprise-level features, including telemetry, policy, and licenses.
- Example Issues:
"Usage data is not appearing in our telemetry dashboard."
"A user is able to perform an action that should be blocked by an admin policy."
"Questions about billing, licensing tiers, or enterprise quotas."
area/non-interactive
- Description: Issues related to using the CLI in automated or non-interactive environments (headless mode).
- Example Issues:
"Problems using the CLI as an SDK in another surface."
"The CLI is behaving differently when run from a shell script vs. an interactive terminal."
"GitHub action is failing."
"I am having trouble running the CLI in headless mode"
area/core
- Description: Issues with the fundamental CLI app itself. This includes the user interface (UI/UX), installation, OS compatibility, and performance.
- Example Issues:
"I am seeing my screen flicker when using the CLI."
"The output in my terminal is malformed or unreadable."
"Theme changes are not taking effect."
"Keyboard inputs (e.g., arrow keys, Ctrl+C) are not being recognized."
"The CLI failed to install or update."
"An issue specific to running on Windows, macOS, or Linux."
"Problems with command parsing, flags, or argument handling."
"High CPU or memory usage by the CLI process."
"Issues related to multi-modality (e.g., handling image inputs)."
"Problems with the IDE integration connection or installation"
area/security
- Description: Issues related to user authentication, authorization, data security, and privacy.
- Example Issues:
"I am unable to sign in."
"The login flow is selecting the wrong authentication path"
"Problems with API key handling or credential storage."
"A report of a security vulnerability"
"Concerns about data sanitization or potential data leaks."
"Issues or requests related to privacy controls."
"Preventing unauthorized data access."
area/platform
- Description: Issues related to CI/CD, release management, testing, eval infrastructure, capacity, quota management, and sandbox environments.
- Example Issues:
"I am getting a 429 'Resource Exhausted' or 500-level server error."
"General slowness or high latency from the service."
"The build script is broken on the main branch."
"Tests are failing in the CI/CD pipeline."
"Issues with the release management or publishing process."
"User is running out of capacity."
"Problems specific to the sandbox or staging environments."
"Questions about quota limits or requests for increases."
area/extensions
- Description: Issues related to the extension ecosystem, including the marketplace and website.
- Example Issues:
"Bugs related to the extension marketplace website."
"Issues with a specific extension."
"Feature request for the extension ecosystem."
area/documentation
- Description: Issues related to user-facing documentation and other content on the documentation website.
- Example Issues:
"A typo in a README file."
"DOCS: A command is not working as described in the documentation."
"A request for a new documentation page."
"Instructions missing for skills feature"
area/unknown
- Description: Issues that do not clearly fit into any other defined area/ category, or where information is too limited to make a determination. Use this when no other area is appropriate.
- name: 'Apply Labels to Issue'
if: |-
${{ steps.gemini_issue_analysis.outputs.summary != '' }}
env:
REPOSITORY: '${{ github.repository }}'
ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}'
LABELS_OUTPUT: '${{ steps.gemini_issue_analysis.outputs.summary }}'
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
with:
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
script: |
const rawOutput = process.env.LABELS_OUTPUT;
core.info(`Raw output from model: ${rawOutput}`);
let parsedLabels;
try {
// First, try to parse the raw output as JSON.
parsedLabels = JSON.parse(rawOutput);
} catch (jsonError) {
// If that fails, check for a markdown code block.
core.warning(`Direct JSON parsing failed: ${jsonError.message}. Trying to extract from a markdown block.`);
const jsonMatch = rawOutput.match(/```json\s*([\s\S]*?)\s*```/);
if (jsonMatch && jsonMatch[1]) {
try {
parsedLabels = JSON.parse(jsonMatch[1].trim());
} catch (markdownError) {
core.setFailed(`Failed to parse JSON even after extracting from markdown block: ${markdownError.message}\nRaw output: ${rawOutput}`);
return;
}
} else {
// If no markdown block, try to find a raw JSON object in the output.
// The CLI may include debug/log lines (e.g. telemetry init, YOLO mode)
// before the actual JSON response.
const jsonObjectMatch = rawOutput.match(/(\{[\s\S]*"labels_to_set"[\s\S]*\})/);
if (jsonObjectMatch) {
try {
parsedLabels = JSON.parse(jsonObjectMatch[0]);
} catch (extractError) {
core.setFailed(`Found JSON-like content but failed to parse: ${extractError.message}\nRaw output: ${rawOutput}`);
return;
}
} else {
core.setFailed(`Output is not valid JSON and does not contain extractable JSON.\nRaw output: ${rawOutput}`);
return;
}
}
}
const issueNumber = parseInt(process.env.ISSUE_NUMBER);
const labelsToAdd = parsedLabels.labels_to_set || [];
if (labelsToAdd.length !== 1) {
core.setFailed(`Expected exactly 1 label (area/), but got ${labelsToAdd.length}. Labels: ${labelsToAdd.join(', ')}`);
return;
}
const newAreaLabel = labelsToAdd[0];
// Get current labels to resolve conflicts
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
});
const currentLabelNames = currentLabels.map(l => l.name);
const currentAreaLabels = currentLabelNames.filter(name => name.startsWith('area/'));
const labelsToRemove = currentAreaLabels.filter(name => name !== newAreaLabel);
for (const label of labelsToRemove) {
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
name: label
});
core.info(`Removed conflicting area label: ${label}`);
} catch (e) {
core.warning(`Failed to remove label ${label}: ${e.message}`);
}
}
// Set labels based on triage result
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: [newAreaLabel]
});
core.info(`Successfully added labels for #${issueNumber}: ${newAreaLabel}`);
- name: 'Post Issue Analysis Failure Comment'
if: |-
${{ failure() && steps.gemini_issue_analysis.outcome == 'failure' }}
env:
ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}'
RUN_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
with:
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
script: |-
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: parseInt(process.env.ISSUE_NUMBER),
body: 'There is a problem with the Gemini CLI issue triaging. Please check the [action logs](${process.env.RUN_URL}) for details.'
})
+350
View File
@@ -0,0 +1,350 @@
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
@@ -0,0 +1,49 @@
name: '🔄 Gemini CLI Bot: Pulse'
on:
schedule:
- cron: '*/30 * * * *' # Every 30 minutes
workflow_dispatch:
concurrency:
group: '${{ github.workflow }}-${{ github.ref }}'
cancel-in-progress: true
permissions:
contents: 'write'
issues: 'write'
pull-requests: 'write'
jobs:
pulse:
name: 'Pulse (Reflex Layer)'
runs-on: 'ubuntu-latest'
if: "github.repository == 'google-gemini/gemini-cli'"
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
persist-credentials: false
fetch-depth: 0
- 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: 'Run Reflex Processes'
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
run: |
if [ -d "tools/gemini-cli-bot/reflexes/scripts" ] && [ "$(ls -A tools/gemini-cli-bot/reflexes/scripts)" ]; then
for script in tools/gemini-cli-bot/reflexes/scripts/*.ts; do
echo "Running reflex script: $script"
npx tsx "$script"
done
else
echo "No reflex scripts found."
fi
@@ -0,0 +1,47 @@
name: '🔄 Gemini Scheduled Lifecycle Manager'
on:
schedule:
- cron: '30 1 * * *' # Once a day
workflow_dispatch:
inputs:
dry_run:
description: 'Run in dry-run mode (no changes applied)'
required: false
default: false
type: 'boolean'
concurrency:
group: '${{ github.workflow }}'
cancel-in-progress: true
permissions:
issues: 'write'
pull-requests: 'write'
jobs:
manage-lifecycle:
if: "github.repository == 'google-gemini/gemini-cli'"
runs-on: 'ubuntu-latest'
steps:
- name: 'Generate GitHub App Token'
id: 'generate_token'
uses: 'actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349' # ratchet:actions/create-github-app-token@v2
with:
app-id: '${{ secrets.APP_ID }}'
private-key: '${{ secrets.PRIVATE_KEY }}'
- name: 'Checkout repository'
uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
with:
persist-credentials: false
- name: 'Lifecycle Management'
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
env:
DRY_RUN: '${{ inputs.dry_run }}'
with:
github-token: '${{ steps.generate_token.outputs.token }}'
script: |
const script = require('./.github/scripts/gemini-lifecycle-manager.cjs');
await script({github, context, core});
@@ -0,0 +1,120 @@
name: '📋 Gemini Scheduled Issue Deduplication'
on:
schedule:
- cron: '0 * * * *' # Runs every hour
workflow_dispatch:
concurrency:
group: '${{ github.workflow }}'
cancel-in-progress: true
defaults:
run:
shell: 'bash'
jobs:
refresh-embeddings:
if: |-
${{ vars.TRIAGE_DEDUPLICATE_ISSUES != '' && github.repository == 'google-gemini/gemini-cli' }}
permissions:
contents: 'read'
id-token: 'write' # Required for WIF, see https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-google-cloud-platform#adding-permissions-settings
issues: 'read'
statuses: 'read'
packages: 'read'
timeout-minutes: 20
runs-on: 'ubuntu-latest'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
persist-credentials: false
- name: 'Log in to GitHub Container Registry'
uses: 'docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1' # ratchet:docker/login-action@v3
with:
registry: 'ghcr.io'
username: '${{ github.actor }}'
password: '${{ secrets.GITHUB_TOKEN }}'
- name: 'Run Gemini Issue Deduplication Refresh'
uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0
id: 'gemini_refresh_embeddings'
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
ISSUE_TITLE: '${{ github.event.issue.title }}'
ISSUE_BODY: '${{ github.event.issue.body }}'
ISSUE_NUMBER: '${{ github.event.issue.number }}'
REPOSITORY: '${{ github.repository }}'
FIRESTORE_PROJECT: '${{ vars.FIRESTORE_PROJECT }}'
with:
upload_artifacts: 'true'
gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}'
gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}'
use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
settings: |-
{
"mcpServers": {
"issue_deduplication": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"--network", "host",
"-e", "GITHUB_TOKEN",
"-e", "GEMINI_API_KEY",
"-e", "DATABASE_TYPE",
"-e", "FIRESTORE_DATABASE_ID",
"-e", "GCP_PROJECT",
"-e", "GOOGLE_APPLICATION_CREDENTIALS=/app/gcp-credentials.json",
"-v", "${GOOGLE_APPLICATION_CREDENTIALS}:/app/gcp-credentials.json",
"ghcr.io/google-gemini/gemini-cli-issue-triage@sha256:e3de1523f6c83aabb3c54b76d08940a2bf42febcb789dd2da6f95169641f94d3"
],
"env": {
"GITHUB_TOKEN": "${GITHUB_TOKEN}",
"GEMINI_API_KEY": "${{ secrets.GEMINI_API_KEY }}",
"DATABASE_TYPE":"firestore",
"GCP_PROJECT": "${FIRESTORE_PROJECT}",
"FIRESTORE_DATABASE_ID": "(default)",
"GOOGLE_APPLICATION_CREDENTIALS": "${GOOGLE_APPLICATION_CREDENTIALS}"
},
"timeout": 600000
}
},
"maxSessionTurns": 25,
"tools": {
"core": [
"run_shell_command(echo)"
]
},
"telemetry": {
"enabled": true,
"target": "gcp"
}
}
prompt: |-
## Role
You are a database maintenance assistant for a GitHub issue deduplication system.
## Goal
Your sole responsibility is to refresh the embeddings for all open issues in the repository to ensure the deduplication database is up-to-date.
## Steps
1. **Extract Repository Information:** The repository is ${{ github.repository }}.
2. **Refresh Embeddings:** Call the `refresh` tool with the correct `repo`. Do not use the `force` parameter.
3. **Log Output:** Print the JSON output from the `refresh` tool to the logs.
## Guidelines
- Only use the `refresh` tool.
- Do not attempt to find duplicates or modify any issues.
- Your only task is to call the `refresh` tool and log its output.

Some files were not shown because too many files have changed in this diff Show More