Compare commits

...

10 Commits

Author SHA1 Message Date
gemini-cli-robot d1c0add0a2 chore(release): v0.6.0-preview.5 2025-09-22 22:36:54 +00:00
Tommaso Sciortino 0ca8669a80 Update .github directory from main branch (#9155) 2025-09-22 15:11:27 -07:00
gemini-cli-robot 4fb8dfe258 chore(release): v0.6.0-preview.4 2025-09-19 21:38:07 +00:00
Sandy Tao 85409d730e fix(security): Pin wrap-ansi to 9.0.2 (#8934) 2025-09-19 14:23:35 -07:00
Gal Zahavi 6dde251e1e fix(cli) : fix shell colors to match new coloring (#8747)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-09-19 13:14:13 -07:00
Gal Zahavi a2356b0230 fix(shell): update shell setting from usePty to enableInteractiveShell (#8726)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-09-19 13:14:00 -07:00
Gal Zahavi e20d7f1e99 refactor(shell): Send AnsiOutput when ShowColor is false (#8647) 2025-09-19 13:13:47 -07:00
Tommaso Sciortino 1610fecc3e Rollback shrinkwrap (#8926) 2025-09-19 11:52:56 -07:00
gemini-cli[bot] 84cc6a4e2d fix(patch): cherry-pick f3abfb8 to release/v0.6.0-preview.2 (#8904)
Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
2025-09-19 04:19:38 -07:00
gemini-cli-robot 5d676414ec chore(release): v0.6.0-preview.2 2025-09-17 20:34:54 +00:00
48 changed files with 1819 additions and 1012 deletions
+2 -2
View File
@@ -4,10 +4,10 @@
# Require reviews from the release approvers for critical files.
# These patterns override the rule above.
/package.json @google-gemini/gemini-cli-askmode-approvers
/npm-shrinkwrap.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
/packages/core/package.json @google-gemini/gemini-cli-askmode-approvers
@@ -0,0 +1,55 @@
name: 'Create and Merge Pull Request'
description: 'Creates a pull request and merges it automatically.'
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'
app-id:
description: 'The ID of the GitHub App.'
required: true
private-key:
description: 'The private key of the GitHub App.'
required: true
dry-run:
description: 'Whether to run in dry-run mode.'
required: false
default: 'false'
runs:
using: 'composite'
steps:
- name: 'Generate GitHub App Token'
id: 'generate_token'
if: "inputs.dry-run == 'false'"
uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b'
with:
app-id: '${{ inputs.app-id }}'
private-key: '${{ inputs.private-key }}'
permission-pull-requests: 'write'
permission-contents: 'write'
- name: 'Create and Approve Pull Request'
if: "inputs.dry-run == 'false'"
env:
GH_TOKEN: '${{ steps.generate_token.outputs.token }}'
shell: 'bash'
run: |
set -e
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 --squash
+47 -11
View File
@@ -27,10 +27,19 @@ inputs:
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
runs:
using: 'composite'
@@ -69,14 +78,8 @@ runs:
BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
DRY_RUN: '${{ inputs.dry-run }}'
RELEASE_TAG: '${{ inputs.release-tag }}'
run: |
git add package.json packages/*/package.json
if [ -f npm-shrinkwrap.json ]; then
git add npm-shrinkwrap.json
fi
if [ -f package-lock.json ]; then
git add package-lock.json
fi
run: |-
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..."
@@ -108,7 +111,7 @@ runs:
npm publish \
--dry-run="${{ inputs.dry-run }}" \
--workspace="@google/gemini-cli-core" \
--tag="${{ inputs.npm-tag }}"
--no-tag
- name: '🔗 Install latest core package'
working-directory: '${{ inputs.working-directory }}'
@@ -128,7 +131,31 @@ runs:
npm publish \
--dry-run="${{ inputs.dry-run }}" \
--workspace="@google/gemini-cli" \
--tag="${{ inputs.npm-tag }}"
--no-tag
- name: '🔬 Verify NPM release by version'
uses: './.github/actions/verify-release'
if: "${{ inputs.dry-run == 'false' && inputs.force-skip-tests == 'false' }}"
with:
npm-package: '@google/gemini-cli@${{ inputs.release-version }}'
expected-version: '${{ inputs.release-version }}'
ref: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
- name: '🏷️ Tag release'
uses: './.github/actions/tag-npm-release'
if: "${{ inputs.dry-run == 'false' }}"
with:
channel: '${{ inputs.npm-tag }}'
version: '${{ inputs.release-version }}'
dry-run: '${{ inputs.dry-run }}'
wombat-token-core: '${{ inputs.wombat-token-core }}'
wombat-token-cli: '${{ inputs.wombat-token-cli }}'
- name: 'Install deps'
working-directory: '${{ inputs.working-directory }}'
shell: 'bash'
run: |
npm install
- name: '🎁 Bundle'
working-directory: '${{ inputs.working-directory }}'
@@ -138,7 +165,7 @@ runs:
- name: '🎉 Create GitHub Release'
working-directory: '${{ inputs.working-directory }}'
if: "${{ inputs.dry-run == 'false' }}"
if: "${{ inputs.dry-run == 'false' && inputs.skip-github-release == 'false' && inputs.npm-tag != 'dev' }}"
env:
GITHUB_TOKEN: '${{ inputs.github-token }}'
shell: 'bash'
@@ -149,3 +176,12 @@ runs:
--title "Release ${{ inputs.release-tag }}" \
--notes-start-tag "${{ inputs.previous-tag }}" \
--generate-notes
- name: '🧹 Clean up release branch'
working-directory: '${{ inputs.working-directory }}'
if: "${{ inputs.dry-run == 'false' }}"
continue-on-error: true
shell: 'bash'
run: |
echo "Cleaning up release branch ${{ steps.release_branch.outputs.BRANCH_NAME }}..."
git push origin --delete "${{ steps.release_branch.outputs.BRANCH_NAME }}"
+73
View File
@@ -0,0 +1,73 @@
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
- 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 "kind/bug,release-failure"
+85
View File
@@ -0,0 +1,85 @@
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
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@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 Docker Buildx'
uses: 'docker/setup-buildx-action@v3'
- name: 'Log in to GitHub Container Registry'
uses: 'docker/login-action@v3'
with:
registry: 'ghcr.io'
username: '${{ inputs.github-actor }}'
password: '${{ inputs.github-secret }}'
- 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
- name: 'build'
id: 'docker_build'
shell: 'bash'
env:
GEMINI_SANDBOX_IMAGE_TAG: '${{ steps.image_tag.outputs.FINAL_TAG }}'
GEMINI_SANDBOX: 'docker'
run: |-
npm run build:sandbox -- \
--image ghcr.io/${{ github.repository}}/sandbox:${{ steps.image_tag.outputs.FINAL_TAG }} \
--output-file final_image_uri.txt
echo "uri=$(cat final_image_uri.txt)" >> $GITHUB_OUTPUT
- name: 'publish'
shell: 'bash'
if: "${{ inputs.dry-run == 'false' }}"
run: |-
docker push "${{ steps.docker_build.outputs.uri }}"
- 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 "kind/bug,release-failure"
@@ -0,0 +1,53 @@
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
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'
runs:
using: 'composite'
steps:
- name: 'Setup Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
with:
node-version-file: '.nvmrc'
registry-url: 'https://wombat-dressing-room.appspot.com'
scope: '@google'
- name: 'Change tag for @google/gemini-cli-core'
if: |-
${{ inputs.dry-run == 'false' }}
env:
NODE_AUTH_TOKEN: '${{ inputs.wombat-token-core }}'
shell: 'bash'
run: |
npm dist-tag add @google/gemini-cli-core@${{ inputs.version }} ${{ inputs.channel }}
- name: 'Change tag for @google/gemini-cli'
if: |-
${{ inputs.dry-run == 'false' }}
env:
NODE_AUTH_TOKEN: '${{ inputs.wombat-token-cli }}'
shell: 'bash'
run: |
npm dist-tag add @google/gemini-cli@${{ inputs.version }} ${{ inputs.channel }}
- name: 'Log dry run'
if: |-
${{ inputs.dry-run == 'true' }}
shell: 'bash'
run: |
echo "Dry run: Would have added tag '${{ inputs.channel }}' to version '${{ inputs.version }}' for @google/gemini-cli and @google/gemini-cli-core."
+55
View File
@@ -0,0 +1,55 @@
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'
expected-version:
description: 'Expected version'
required: true
ref:
description: 'The branch, tag, or SHA to release from.'
required: false
type: 'string'
default: 'main'
runs:
using: 'composite'
steps:
- name: '📝 Print Inputs'
shell: 'bash'
run: |
echo "${{ toJSON(inputs) }}"
- name: 'Checkout'
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v4
with:
ref: '${{ github.event.inputs.ref }}'
fetch-depth: 0
- 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: |-
npm install --prefer-online --no-cache -g ${{ inputs.npm-package }}
# This provides a very basic smoke test for Gemini CLI
- name: 'Run Gemini CLI'
id: 'gemini_cli'
shell: 'bash'
run: |-
echo "gemini_version=$(gemini --version)" >> $GITHUB_OUTPUT
# Force a failure if it doesn't match
- name: 'Fail workflow if version does not match'
if: '${{ steps.gemini_cli.outputs.gemini_version != inputs.expected-version }}'
shell: 'bash'
run: |-
echo '❌ Got ${{ steps.gemini_cli.outputs.gemini_version }} from ${{ inputs.npm-package }}'
echo '❌ Expected Version ${{ inputs.expected-version }}'
exit 1
+166 -253
View File
@@ -1,4 +1,4 @@
name: 'Gemini CLI CI'
name: 'Testing: CI'
on:
push:
@@ -10,6 +10,7 @@ on:
- 'main'
- 'release/**'
merge_group:
workflow_dispatch:
concurrency:
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
@@ -25,63 +26,10 @@ defaults:
run:
shell: 'bash'
env:
ACTIONLINT_VERSION: '1.7.7'
SHELLCHECK_VERSION: '0.11.0'
YAMLLINT_VERSION: '1.35.1'
jobs:
#
# Lint: GitHub Actions
#
lint_github_actions:
name: 'Lint (GitHub Actions)'
runs-on: 'ubuntu-latest'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
fetch-depth: 1
- name: 'Install shellcheck' # Actionlint uses shellcheck
run: |-
mkdir -p "${RUNNER_TEMP}/shellcheck"
curl -sSLo "${RUNNER_TEMP}/.shellcheck.txz" "https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/shellcheck-v${SHELLCHECK_VERSION}.linux.x86_64.tar.xz"
tar -xf "${RUNNER_TEMP}/.shellcheck.txz" -C "${RUNNER_TEMP}/shellcheck" --strip-components=1
echo "${RUNNER_TEMP}/shellcheck" >> "${GITHUB_PATH}"
- name: 'Install actionlint'
run: |-
mkdir -p "${RUNNER_TEMP}/actionlint"
curl -sSLo "${RUNNER_TEMP}/.actionlint.tgz" "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"
tar -xzf "${RUNNER_TEMP}/.actionlint.tgz" -C "${RUNNER_TEMP}/actionlint"
echo "${RUNNER_TEMP}/actionlint" >> "${GITHUB_PATH}"
# For actionlint, we specifically ignore shellcheck rules that are
# annoying or unhelpful. See the shellcheck action for a description.
- name: 'Run actionlint'
run: |-
actionlint \
-color \
-format '{{range $err := .}}::error file={{$err.Filepath}},line={{$err.Line}},col={{$err.Column}}::{{$err.Filepath}}@{{$err.Line}} {{$err.Message}}%0A```%0A{{replace $err.Snippet "\\n" "%0A"}}%0A```\n{{end}}' \
-ignore 'SC2002:' \
-ignore 'SC2016:' \
-ignore 'SC2129:' \
-ignore 'label ".+" is unknown'
- name: 'Run ratchet'
uses: 'sethvargo/ratchet@8b4ca256dbed184350608a3023620f267f0a5253' # ratchet:sethvargo/ratchet@v0.11.4
with:
files: |-
.github/workflows/*.yml
.github/actions/**/*.yml
#
# Lint: Javascript
#
lint_javascript:
name: 'Lint (Javascript)'
runs-on: 'ubuntu-latest'
lint:
name: 'Lint'
runs-on: 'gemini-cli-ubuntu-16-core'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
@@ -94,161 +42,33 @@ jobs:
node-version-file: '.nvmrc'
cache: 'npm'
- name: 'Run lockfile check'
run: |-
npm run check:lockfile
- name: 'Install dependencies'
run: |-
npm ci
run: 'npm ci'
- name: 'Run formatter check'
run: |-
npm run format
git diff --exit-code
- name: 'Check lockfile'
run: 'npm run check:lockfile'
- name: 'Run linter'
run: |-
npm run lint:ci
- name: 'Install linters'
run: 'node scripts/lint.js --setup'
- name: 'Run linter on integration tests'
run: |-
npx eslint integration-tests --max-warnings 0
- name: 'Run ESLint'
run: 'node scripts/lint.js --eslint'
- name: 'Run formatter on integration tests'
run: |-
npx prettier --check integration-tests
git diff --exit-code
- name: 'Run actionlint'
run: 'node scripts/lint.js --actionlint'
- name: 'Build project'
run: |-
npm run build
- name: 'Run type check'
run: |-
npm run typecheck
#
# Lint: Shell
#
lint_shell:
name: 'Lint (Shell)'
runs-on: 'ubuntu-latest'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
fetch-depth: 1
- name: 'Install shellcheck'
run: |-
mkdir -p "${RUNNER_TEMP}/shellcheck"
curl -sSLo "${RUNNER_TEMP}/.shellcheck.txz" "https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/shellcheck-v${SHELLCHECK_VERSION}.linux.x86_64.tar.xz"
tar -xf "${RUNNER_TEMP}/.shellcheck.txz" -C "${RUNNER_TEMP}/shellcheck" --strip-components=1
echo "${RUNNER_TEMP}/shellcheck" >> "${GITHUB_PATH}"
- name: 'Install shellcheck problem matcher'
run: |-
cat > "${RUNNER_TEMP}/shellcheck/problem-matcher-lint-shell.json" <<"EOF"
{
"problemMatcher": [
{
"owner": "lint_shell",
"pattern": [
{
"regexp": "^(.*):(\\\\d+):(\\\\d+):\\\\s+(?:fatal\\\\s+)?(warning|error):\\\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"message": 5
}
]
}
]
}
EOF
echo "::add-matcher::${RUNNER_TEMP}/shellcheck/problem-matcher-lint-shell.json"
# Note that only warning and error severity show up in the github files
# page. So we replace 'style' and 'note' with 'warning' to make it show
# up.
#
# We also try and find all bash scripts even if they don't have an
# explicit extension.
#
# We explicitly ignore the following rules:
#
# - SC2002: This rule suggests using "cmd < file" instead of "cat | cmd".
# While < is more efficient, pipes are much more readable and expected.
#
# - SC2129: This rule suggests grouping multiple writes to a file in
# braces like "{ cmd1; cmd2; } >> file". This is unexpected and less
# readable.
#
# - SC2310: This is an optional warning that only appears with "set -e"
# and when a command is used as a conditional.
- name: 'Run shellcheck'
run: |-
git ls-files | grep -E '^([^.]+|.*\.(sh|zsh|bash))$' | xargs file --mime-type \
| grep "text/x-shellscript" | awk '{ print substr($1, 1, length($1)-1) }' \
| xargs shellcheck \
--check-sourced \
--enable=all \
--exclude=SC2002,SC2129,SC2310 \
--severity=style \
--format=gcc \
--color=never | sed -e 's/note:/warning:/g' -e 's/style:/warning:/g'
#
# Lint: YAML
#
lint_yaml:
name: 'Lint (YAML)'
runs-on: 'ubuntu-latest'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
fetch-depth: 1
- name: 'Setup Python'
uses: 'actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065' # ratchet:actions/setup-python@v5
with:
python-version: '3'
- name: 'Install yamllint'
run: |-
pip install --user "yamllint==${YAMLLINT_VERSION}"
run: 'node scripts/lint.js --shellcheck'
- name: 'Run yamllint'
run: |-
git ls-files | grep -E '\.(yaml|yml)' | xargs yamllint --format github
run: 'node scripts/lint.js --yamllint'
#
# Lint: All
#
# This is a virtual job that other jobs depend on to wait for all linters to
# finish. It's also used to ensure linting happens on CI via required
# workflows.
lint:
name: 'Lint'
needs:
- 'lint_github_actions'
- 'lint_javascript'
- 'lint_shell'
- 'lint_yaml'
runs-on: 'ubuntu-latest'
steps:
- run: |-
echo 'All linters finished!'
- name: 'Run Prettier'
run: 'node scripts/lint.js --prettier'
#
# Test: Node
#
test:
name: 'Test'
runs-on: '${{ matrix.os }}'
test_linux:
name: 'Test (Linux)'
runs-on: 'gemini-cli-ubuntu-16-core'
needs:
- 'lint'
permissions:
@@ -256,12 +76,7 @@ jobs:
checks: 'write'
pull-requests: 'write'
strategy:
fail-fast: false # So we can see all test failures
matrix:
os:
- 'macos-latest'
- 'ubuntu-latest'
- 'windows-latest'
node-version:
- '20.x'
- '22.x'
@@ -277,18 +92,19 @@ jobs:
cache: 'npm'
- name: 'Build project'
run: |-
npm run build
run: 'npm run build'
- name: 'Install dependencies for testing'
run: |-
npm ci
run: 'npm ci'
- name: 'Run tests and generate reports'
env:
NO_COLOR: true
run: 'npm run test:ci'
- 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) }}
@@ -304,7 +120,67 @@ jobs:
${{ 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-${{ matrix.node-version }}-${{ matrix.os }}'
name: 'test-results-fork-${{ matrix.node-version }}-${{ runner.os }}'
path: 'packages/*/junit.xml'
test_slow_platforms:
name: 'Slow Test - Mac'
runs-on: '${{ matrix.os }}'
needs:
- 'lint'
permissions:
contents: 'read'
checks: 'write'
pull-requests: 'write'
continue-on-error: true
strategy:
matrix:
os:
- 'macos-latest'
node-version:
- '20.x'
- '22.x'
- '24.x'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
- 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
run: 'npm run test:ci -- --coverage.enabled=false'
- 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 ${{ matrix.node-version }})'
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-${{ matrix.node-version }}-${{ runner.os }}'
path: 'packages/*/junit.xml'
- name: 'Upload coverage reports'
@@ -315,47 +191,9 @@ jobs:
name: 'coverage-reports-${{ matrix.node-version }}-${{ matrix.os }}'
path: 'packages/*/coverage'
post_coverage_comment:
name: 'Post Coverage Comment'
runs-on: 'ubuntu-latest'
needs: 'test'
if: |-
${{ always() && github.event_name == 'pull_request' && (github.event.pull_request.head.repo.full_name == github.repository) }}
continue-on-error: true
permissions:
contents: 'read' # For checkout
pull-requests: 'write' # For commenting
strategy:
matrix:
# Reduce noise by only posting the comment once
os:
- 'ubuntu-latest'
node-version:
- '22.x'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
- name: 'Download coverage reports artifact'
uses: 'actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0' # ratchet:actions/download-artifact@v5
with:
name: 'coverage-reports-${{ matrix.node-version }}-${{ matrix.os }}'
path: 'coverage_artifact' # Download to a specific directory
- name: 'Post Coverage Comment using Composite Action'
uses: './.github/actions/post-coverage-comment' # Path to the composite action directory
with:
cli_json_file: 'coverage_artifact/cli/coverage/coverage-summary.json'
core_json_file: 'coverage_artifact/core/coverage/coverage-summary.json'
cli_full_text_summary_file: 'coverage_artifact/cli/coverage/full-text-summary.txt'
core_full_text_summary_file: 'coverage_artifact/core/coverage/full-text-summary.txt'
node_version: '${{ matrix.node-version }}'
os: '${{ matrix.os }}'
github_token: '${{ secrets.GITHUB_TOKEN }}'
codeql:
name: 'CodeQL'
runs-on: 'ubuntu-latest'
runs-on: 'gemini-cli-ubuntu-16-core'
permissions:
actions: 'read'
contents: 'read'
@@ -375,9 +213,8 @@ jobs:
# Check for changes in bundle size.
bundle_size:
name: 'Check Bundle Size'
if: |-
${{ github.event_name != 'merge_group' }}
runs-on: 'ubuntu-latest'
if: "github.event_name == 'pull_request'"
runs-on: 'gemini-cli-ubuntu-16-core'
permissions:
contents: 'read' # For checkout
pull-requests: 'write' # For commenting
@@ -395,3 +232,79 @@ jobs:
minimum-change-threshold: '1000'
compression: 'none'
clean-script: 'clean'
test_windows:
name: 'Slow Test - Win'
runs-on: 'gemini-cli-windows-16-core'
continue-on-error: true
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
- 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
NODE_OPTIONS: '--max-old-space-size=32768 --max-semi-space-size=256'
UV_THREADPOOL_SIZE: '32'
NODE_ENV: 'test'
run: 'npm run test:ci -- --coverage.enabled=false'
shell: 'pwsh'
ci:
name: 'CI'
if: 'always()'
needs:
- 'lint'
- 'test_linux'
- '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.test_linux.result }} != 'success' && ${{ needs.test_linux.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!"
-58
View File
@@ -1,58 +0,0 @@
name: 'Create Patch PR'
on:
workflow_dispatch:
inputs:
commit:
description: 'The commit SHA to cherry-pick for the patch.'
required: true
type: 'string'
channel:
description: 'The release channel to patch.'
required: true
type: 'choice'
options:
- 'stable'
- 'preview'
dry_run:
description: 'Whether to run in dry-run mode.'
required: false
type: 'boolean'
default: false
jobs:
create-patch:
runs-on: 'ubuntu-latest'
permissions:
contents: 'write'
pull-requests: 'write'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
fetch-depth: 0
- name: 'Setup 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: 'Configure Git User'
run: |-
git config user.name "gemini-cli-robot"
git config user.email "gemini-cli-robot@google.com"
- name: 'Create Patch for Stable'
if: "github.event.inputs.channel == 'stable'"
env:
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
run: 'node scripts/create-patch-pr.js --commit=${{ github.event.inputs.commit }} --channel=stable --dry-run=${{ github.event.inputs.dry_run }}'
- name: 'Create Patch for Preview'
env:
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
run: 'node scripts/create-patch-pr.js --commit=${{ github.event.inputs.commit }} --channel=${{ github.event.inputs.channel }} --dry-run=${{ github.event.inputs.dry_run }}'
+203 -28
View File
@@ -1,4 +1,4 @@
name: 'E2E Tests'
name: 'Testing: E2E'
on:
push:
@@ -14,35 +14,63 @@ on:
types: ['labeled']
merge_group:
concurrency:
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
cancel-in-progress: |-
${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/heads/release/') }}
jobs:
e2e-test:
name: 'E2E Test (${{ matrix.os }}) - ${{ matrix.sandbox }}'
# This condition ensures the job runs for pushes to main, merge groups,
# PRs from the base repo, OR PRs from forks with the correct label.
build:
name: 'Build Project'
runs-on: 'gemini-cli-ubuntu-16-core'
if: |
github.event_name == 'push' ||
github.event_name == 'merge_group' ||
(github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event.label.name == 'maintainer:e2e:ok')
runs-on: '${{ matrix.os }}'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
- name: 'Set up Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: 'Install dependencies'
run: 'npm ci'
- name: 'Build project'
run: 'npm run build'
- name: 'Archive build artifacts'
run: 'tar -cvf build-artifacts.tar bundle/ node_modules/ packages/ package.json'
- name: 'Upload build artifacts'
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02'
with:
name: 'build-artifacts-${{ github.run_id }}'
path: 'build-artifacts.tar'
e2e_linux:
name: 'E2E Test (Linux) - ${{ matrix.sandbox }}'
needs:
- 'build'
if: |
github.event_name == 'push' ||
github.event_name == 'merge_group' ||
(github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event.label.name == 'maintainer:e2e:ok')
runs-on: 'gemini-cli-ubuntu-16-core'
strategy:
fail-fast: false
matrix:
os:
- 'ubuntu-latest'
- 'macos-latest'
- 'gemini-cli-windows-16-core'
sandbox:
- 'sandbox:none'
- 'sandbox:docker'
node-version:
- '20.x'
exclude:
# Docker tests are not supported on macOS or Windows
- os: 'macos-latest'
sandbox: 'sandbox:docker'
- os: 'gemini-cli-windows-16-core'
sandbox: 'sandbox:docker'
steps:
- name: 'Checkout (fork)'
@@ -56,22 +84,22 @@ jobs:
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
if: "github.event_name != 'pull_request_target'"
- name: 'Download build artifacts'
uses: 'actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093'
with:
name: 'build-artifacts-${{ github.run_id }}'
path: '.'
- name: 'Extract build artifacts'
run: 'tar -xvf build-artifacts.tar'
- 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.os == 'ubuntu-latest' && matrix.sandbox == 'sandbox:docker'
if: "matrix.sandbox == 'sandbox:docker'"
uses: 'docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435' # ratchet:docker/setup-buildx-action@v3
- name: 'Run E2E tests'
@@ -80,6 +108,153 @@ jobs:
KEEP_OUTPUT: 'true'
SANDBOX: '${{ matrix.sandbox }}'
VERBOSE: 'true'
GEMINI_SANDBOX: 'docker'
shell: 'bash'
run: |-
npm run "test:integration:${SANDBOX}"
run: |
if [[ "${{ matrix.sandbox }}" == "sandbox:docker" ]]; then
npm run build:sandbox
fi
npx vitest run --root ./integration-tests
e2e_slow_platforms:
name: 'Slow E2E - Mac'
needs:
- 'build'
if: |
github.event_name == 'push' ||
github.event_name == 'merge_group' ||
(github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event.label.name == 'maintainer:e2e:ok')
runs-on: '${{ matrix.os }}'
continue-on-error: true
strategy:
fail-fast: false
matrix:
os:
- 'macos-latest'
node-version:
- '20.x'
steps:
- name: 'Checkout (fork)'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
if: "github.event_name == 'pull_request_target'"
with:
ref: '${{ github.event.pull_request.head.sha }}'
repository: '${{ github.event.pull_request.head.repo.full_name }}'
- name: 'Checkout (internal)'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
if: "github.event_name != 'pull_request_target'"
- name: 'Download build artifacts'
uses: 'actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093'
with:
name: 'build-artifacts-${{ github.run_id }}'
path: '.'
- name: 'Extract build artifacts'
run: 'tar -xvf build-artifacts.tar'
- name: 'Set up Node.js ${{ matrix.node-version }}'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
with:
node-version: '${{ matrix.node-version }}'
- name: 'Fix rollup optional dependencies on macOS'
if: "runner.os == 'macOS'"
run: |
npm cache clean --force
npm install --no-save @rollup/rollup-darwin-arm64 || true
- name: 'Run E2E tests (non-Windows)'
if: "runner.os != 'Windows'"
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
KEEP_OUTPUT: 'true'
SANDBOX: 'sandbox:none'
VERBOSE: 'true'
run: 'npx vitest run --root ./integration-tests'
e2e_windows:
name: 'Slow E2E - Win'
if: |
github.event_name == 'push' ||
github.event_name == 'merge_group' ||
(github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event.label.name == 'maintainer:e2e:ok')
runs-on: 'gemini-cli-windows-16-core'
continue-on-error: true
steps:
- name: 'Checkout (fork)'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
if: "github.event_name == 'pull_request_target'"
with:
ref: '${{ github.event.pull_request.head.sha }}'
repository: '${{ github.event.pull_request.head.repo.full_name }}'
- name: 'Checkout (internal)'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
if: "github.event_name != 'pull_request_target'"
- 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: 'Run E2E tests'
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
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: 'npx vitest run --root ./integration-tests'
e2e:
name: 'E2E'
if: |
always() && (
github.event_name == 'push' ||
github.event_name == 'merge_group' ||
(github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event.label.name == 'maintainer:e2e:ok')
)
needs:
- 'e2e_linux'
runs-on: 'gemini-cli-ubuntu-16-core'
steps:
- name: 'Check E2E test results'
run: |
if [[ ${{ needs.e2e_linux.result }} != 'success' ]]; then
echo "The required E2E test job failed."
exit 1
fi
echo "All required E2E test jobs passed!"
-56
View File
@@ -1,56 +0,0 @@
name: 'Patch from Comment'
on:
issue_comment:
types: ['created']
jobs:
slash-command:
runs-on: 'ubuntu-latest'
steps:
- name: 'Slash Command Dispatch'
id: 'slash_command'
uses: 'peter-evans/slash-command-dispatch@40877f718dce0101edfc7aea2b3800cc192f9ed5'
with:
token: '${{ secrets.GITHUB_TOKEN }}'
commands: 'patch'
permission: 'write'
issue-type: 'pull-request'
static-args: |
dry_run=false
- name: 'Get PR Status'
id: 'pr_status'
if: "steps.slash_command.outputs.dispatched == 'true'"
env:
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
run: |
gh pr view "${{ github.event.issue.number }}" --json mergeCommit,state > pr_status.json
echo "MERGE_COMMIT_SHA=$(jq -r .mergeCommit.oid pr_status.json)" >> "$GITHUB_OUTPUT"
echo "STATE=$(jq -r .state pr_status.json)" >> "$GITHUB_OUTPUT"
- name: 'Dispatch if Merged'
if: "steps.pr_status.outputs.STATE == 'MERGED'"
uses: 'actions/github-script@00f12e3e20659f42342b1c0226afda7f7c042325'
with:
script: |
const args = JSON.parse('${{ steps.slash_command.outputs.command-arguments }}');
github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'create-patch-pr.yml',
ref: 'main',
inputs: {
commit: '${{ steps.pr_status.outputs.MERGE_COMMIT_SHA }}',
channel: args.channel,
dry_run: args.dry_run
}
})
- name: 'Comment on Failure'
if: "steps.pr_status.outputs.STATE != 'MERGED'"
uses: 'peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d'
with:
issue-number: '${{ github.event.issue.number }}'
body: |
:x: The `/patch` command failed. This pull request must be merged before a patch can be created.
-94
View File
@@ -1,94 +0,0 @@
name: 'Patch Release'
on:
workflow_dispatch:
inputs:
type:
description: 'The type of release to patch from.'
required: true
type: 'choice'
options:
- 'stable'
- 'preview'
ref:
description: 'The branch or ref (full git sha) to release from.'
required: true
type: 'string'
default: 'main'
dry_run:
description: 'Run a dry-run of the release process; no branches, npm packages or GitHub releases will be created.'
required: true
type: 'boolean'
default: true
force_skip_tests:
description: 'Select to skip the "Run Tests" step in testing. Prod releases should run tests'
required: false
type: 'boolean'
default: false
jobs:
release:
runs-on: 'ubuntu-latest'
environment:
name: 'production-release'
url: '${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ steps.version.outputs.RELEASE_TAG }}'
if: |-
${{ github.repository == 'google-gemini/gemini-cli' }}
permissions:
contents: 'write'
packages: 'write'
id-token: 'write'
issues: 'write' # For creating issues on failure
outputs:
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
ref: '${{ github.event.inputs.ref || github.sha }}'
fetch-depth: 0
- name: 'Setup 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: 'Get the version'
id: 'version'
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
run: |-
VERSION_JSON="$(node scripts/get-release-version.js --type=patch --patch-from=${{ github.event.inputs.type }})"
echo "${VERSION_JSON}"
echo "RELEASE_TAG=$(echo "${VERSION_JSON}" | jq -r .releaseTag)" >> "${GITHUB_OUTPUT}"
echo "RELEASE_VERSION=$(echo "${VERSION_JSON}" | jq -r .releaseVersion)" >> "${GITHUB_OUTPUT}"
echo "NPM_TAG=$(echo "${VERSION_JSON}" | jq -r .npmTag)" >> "${GITHUB_OUTPUT}"
echo "PREVIOUS_TAG=$(echo "${VERSION_JSON}" | jq -r .previousReleaseTag)" >> "${GITHUB_OUTPUT}"
- name: 'Print Calculated Version'
run: |-
echo "Calculated version: ${{ steps.version.outputs.RELEASE_VERSION }}"
- name: 'Run Tests'
uses: './.github/actions/run-tests'
with:
force_skip_tests: '${{ github.event.inputs.force_skip_tests }}'
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
- name: 'Publish Release'
uses: './.github/actions/publish-release'
with:
release-version: '${{ steps.version.outputs.RELEASE_VERSION }}'
release-tag: '${{ steps.version.outputs.RELEASE_TAG }}'
npm-tag: '${{ steps.version.outputs.NPM_TAG }}'
wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
github-token: '${{ secrets.GITHUB_TOKEN }}'
dry-run: '${{ github.event.inputs.dry_run }}'
previous-tag: '${{ steps.version.outputs.PREVIOUS_TAG }}'
+56
View File
@@ -0,0 +1,56 @@
name: 'Release: Change Tags'
on:
workflow_dispatch:
inputs:
version:
description: 'The package version to tag (e.g., 0.5.0-preview-2). This version must already exist on the npm registry.'
required: true
type: 'string'
channel:
description: 'The npm dist-tag to apply (e.g., latest, preview, nightly).'
required: true
type: 'choice'
options:
- 'latest'
- 'preview'
- 'nightly'
ref:
description: 'The branch, tag, or SHA to run from.'
required: false
type: 'string'
default: 'main'
dry-run:
description: 'Whether to run in dry-run mode.'
required: false
type: 'boolean'
default: true
jobs:
change-tags:
runs-on: 'ubuntu-latest'
permissions:
packages: 'write'
issues: 'write'
steps:
- name: 'Checkout repository'
uses: 'actions/checkout@v4'
with:
ref: '${{ github.event.inputs.ref }}'
fetch-depth: 0
- name: 'Setup Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
with:
node-version-file: '.nvmrc'
registry-url: 'https://wombat-dressing-room.appspot.com'
scope: '@google'
- name: 'Change tag'
uses: './.github/actions/tag-npm-release'
with:
channel: '${{ github.event.inputs.channel }}'
version: '${{ github.event.inputs.version }}'
dry-run: '${{ github.event.inputs.dry-run }}'
wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
+89
View File
@@ -0,0 +1,89 @@
name: 'Release: Manual'
on:
workflow_dispatch:
inputs:
version:
description: 'The version to release (e.g., v0.1.11). Must be a valid semver string with a "v" prefix.'
required: true
type: 'string'
ref:
description: 'The branch, tag, or SHA to release from.'
required: true
type: 'string'
npm_channel:
description: 'The npm channel to publish to.'
required: true
type: 'choice'
options:
- 'preview'
- 'nightly'
- 'latest'
- 'dev'
default: 'dev'
dry_run:
description: 'Run a dry-run of the release process; no branches, npm packages or GitHub releases will be created.'
required: true
type: 'boolean'
default: true
force_skip_tests:
description: 'Select to skip the "Run Tests" step in testing. Prod releases should run tests'
required: false
type: 'boolean'
default: false
skip_github_release:
description: 'Select to skip creating a GitHub release and create a npm release only.'
required: false
type: 'boolean'
default: false
jobs:
release:
runs-on: 'self-hosted'
permissions:
contents: 'write'
packages: 'write'
issues: 'write'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
with:
ref: '${{ github.event.inputs.ref }}'
fetch-depth: 0
- name: 'Setup Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: 'Install Dependencies'
run: 'npm ci'
- name: 'Prepare Release Info'
id: 'release_info'
run: |
RELEASE_VERSION="${{ github.event.inputs.version }}"
echo "RELEASE_VERSION=${RELEASE_VERSION#v}" >> "${GITHUB_OUTPUT}"
echo "PREVIOUS_TAG=$(git describe --tags --abbrev=0)" >> "${GITHUB_OUTPUT}"
- name: 'Run Tests'
if: |-
${{ github.event.inputs.force_skip_tests != true }}
uses: './.github/actions/run-tests'
with:
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
- name: 'Publish Release'
uses: './.github/actions/publish-release'
with:
force-skip-tests: '${{ github.event.inputs.force_skip_tests }}'
release-version: '${{ steps.release_info.outputs.RELEASE_VERSION }}'
release-tag: '${{ github.event.inputs.version }}'
npm-tag: '${{ github.event.inputs.npm_channel }}'
wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
github-token: '${{ secrets.GITHUB_TOKEN }}'
dry-run: '${{ github.event.inputs.dry_run }}'
previous-tag: '${{ steps.release_info.outputs.PREVIOUS_TAG }}'
skip-github-release: '${{ github.event.inputs.skip_github_release }}'
@@ -1,4 +1,4 @@
name: 'Nightly Release'
name: 'Release: Nightly'
on:
schedule:
@@ -25,7 +25,10 @@ jobs:
release:
runs-on: 'ubuntu-latest'
permissions:
contents: 'write'
packages: 'write'
issues: 'write'
pull-requests: 'write'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
@@ -71,6 +74,16 @@ jobs:
dry-run: '${{ github.event.inputs.dry_run }}'
previous-tag: '${{ steps.nightly_version.outputs.PREVIOUS_TAG }}'
- name: 'Create and Merge Pull Request'
uses: './.github/actions/create-pull-request'
with:
branch-name: 'release/${{ steps.nightly_version.outputs.RELEASE_TAG }}'
pr-title: 'chore(release): bump version to ${{ steps.nightly_version.outputs.RELEASE_VERSION }}'
pr-body: 'Automated version bump for nightly release.'
app-id: '${{ secrets.APP_ID }}'
private-key: '${{ secrets.PRIVATE_KEY }}'
dry-run: '${{ github.event.inputs.dry_run }}'
- name: 'Create Issue on Failure'
if: '${{ failure() && github.event.inputs.dry_run == false }}'
env:
@@ -0,0 +1,109 @@
name: 'Release: Patch (1) Create PR'
on:
workflow_dispatch:
inputs:
commit:
description: 'The commit SHA to cherry-pick for the patch.'
required: true
type: 'string'
channel:
description: 'The release channel to patch.'
required: true
type: 'choice'
options:
- 'stable'
- 'preview'
dry_run:
description: 'Whether to run in dry-run mode.'
required: false
type: 'boolean'
default: false
ref:
description: 'The branch, tag, or SHA to test from.'
required: false
type: 'string'
default: 'main'
original_pr:
description: 'The original PR number to comment back on.'
required: false
type: 'string'
jobs:
create-patch:
runs-on: 'ubuntu-latest'
permissions:
contents: 'write'
pull-requests: 'write'
actions: 'write'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
ref: '${{ github.event.inputs.ref }}'
fetch-depth: 0
- name: 'Setup Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: 'Install Script Dependencies'
run: 'npm install yargs'
- name: 'Generate GitHub App Token'
id: 'generate_token'
uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b'
with:
app-id: '${{ secrets.APP_ID }}'
private-key: '${{ secrets.PRIVATE_KEY }}'
permission-pull-requests: 'write'
permission-contents: 'write'
- name: 'Configure Git User'
run: |-
git config user.name "gemini-cli-robot"
git config user.email "gemini-cli-robot@google.com"
# Configure git to use GITHUB_TOKEN for remote operations (has actions:write for workflow files)
git remote set-url origin "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git"
- name: 'Create Patch'
id: 'create_patch'
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
GH_TOKEN: '${{ steps.generate_token.outputs.token }}'
continue-on-error: true
run: |
# Capture output and display it in logs using tee
{
node scripts/releasing/create-patch-pr.js --commit=${{ github.event.inputs.commit }} --channel=${{ github.event.inputs.channel }} --dry-run=${{ github.event.inputs.dry_run }}
echo "EXIT_CODE=$?" >> "$GITHUB_OUTPUT"
} 2>&1 | tee >(
echo "LOG_CONTENT<<EOF" >> "$GITHUB_ENV"
cat >> "$GITHUB_ENV"
echo "EOF" >> "$GITHUB_ENV"
)
- name: 'Comment on Original PR'
if: 'always() && inputs.original_pr'
env:
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
ORIGINAL_PR: '${{ github.event.inputs.original_pr }}'
EXIT_CODE: '${{ steps.create_patch.outputs.EXIT_CODE }}'
COMMIT: '${{ github.event.inputs.commit }}'
CHANNEL: '${{ github.event.inputs.channel }}'
REPOSITORY: '${{ github.repository }}'
GITHUB_RUN_ID: '${{ github.run_id }}'
LOG_CONTENT: '${{ env.LOG_CONTENT }}'
continue-on-error: true
run: |
git checkout '${{ github.event.inputs.ref }}'
node scripts/releasing/patch-create-comment.js
- name: 'Fail Workflow if Main Task Failed'
if: 'always() && steps.create_patch.outputs.EXIT_CODE != 0'
run: |
echo "Patch creation failed with exit code: ${{ steps.create_patch.outputs.EXIT_CODE }}"
echo "Check the logs above and the comment posted to the original PR for details."
exit 1
@@ -0,0 +1,72 @@
name: 'Release: Patch (2) Trigger'
on:
pull_request:
types:
- 'closed'
branches:
- 'release/**'
workflow_dispatch:
inputs:
ref:
description: 'The head ref of the merged hotfix PR to trigger the release for (e.g. hotfix/v1.2.3/cherry-pick-abc).'
required: true
type: 'string'
workflow_ref:
description: 'The ref to checkout the workflow code from.'
required: false
type: 'string'
default: 'main'
workflow_id:
description: 'The workflow to trigger. Defaults to patch-release.yml'
required: false
type: 'string'
default: 'release-patch-3-release.yml'
dry_run:
description: 'Whether this is a dry run.'
required: false
type: 'boolean'
default: false
force_skip_tests:
description: 'Select to skip the "Run Tests" step in testing. Prod releases should run tests'
required: false
type: 'boolean'
default: false
jobs:
trigger-patch-release:
if: "(github.event_name == 'pull_request' && github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'hotfix/')) || github.event_name == 'workflow_dispatch'"
runs-on: 'ubuntu-latest'
permissions:
actions: 'write'
contents: 'write'
pull-requests: 'write'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
with:
ref: "${{ github.event.inputs.workflow_ref || 'main' }}"
fetch-depth: 1
- name: 'Setup Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: 'Install Dependencies'
run: 'npm ci'
- name: 'Trigger Patch Release'
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
HEAD_REF: "${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.event.inputs.ref }}"
PR_BODY: "${{ github.event_name == 'pull_request' && github.event.pull_request.body || '' }}"
WORKFLOW_ID: '${{ github.event.inputs.workflow_id }}'
GITHUB_REPOSITORY_OWNER: '${{ github.repository_owner }}'
GITHUB_REPOSITORY_NAME: '${{ github.event.repository.name }}'
GITHUB_EVENT_NAME: '${{ github.event_name }}'
GITHUB_EVENT_PAYLOAD: '${{ toJSON(github.event) }}'
FORCE_SKIP_TESTS: '${{ github.event.inputs.force_skip_tests }}'
run: |
node scripts/releasing/patch-trigger.js
@@ -0,0 +1,219 @@
name: 'Release: Patch (3) Release'
on:
workflow_dispatch:
inputs:
type:
description: 'The type of release to perform.'
required: true
type: 'choice'
options:
- 'stable'
- 'preview'
dry_run:
description: 'Run a dry-run of the release process; no branches, npm packages or GitHub releases will be created.'
required: true
type: 'boolean'
default: true
force_skip_tests:
description: 'Select to skip the "Run Tests" step in testing. Prod releases should run tests'
required: false
type: 'boolean'
default: false
release_ref:
description: 'The branch, tag, or SHA to release from.'
required: true
type: 'string'
original_pr:
description: 'The original PR number to comment back on.'
required: false
type: 'string'
jobs:
release:
runs-on: 'ubuntu-latest'
permissions:
contents: 'write'
packages: 'write'
issues: 'write'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
with:
fetch-depth: 0
fetch-tags: true
- name: 'Checkout Release Code'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
with:
ref: '${{ github.event.inputs.release_ref }}'
path: 'release'
fetch-depth: 0
- name: 'Setup Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: 'Install Dependencies'
working-directory: './release'
run: |-
npm ci
- name: 'Print Inputs'
shell: 'bash'
run: |-
echo "${{ toJSON(inputs) }}"
- name: 'Get Patch Version'
id: 'patch_version'
env:
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
run: |
# Use the existing get-release-version.js script to calculate patch version
# Run from main checkout which has full git history and access to npm
PATCH_JSON=$(node scripts/get-release-version.js --type=patch --patch-from=${{ github.event.inputs.type }})
echo "Patch version calculation result: ${PATCH_JSON}"
RELEASE_VERSION=$(echo "${PATCH_JSON}" | jq -r .releaseVersion)
RELEASE_TAG=$(echo "${PATCH_JSON}" | jq -r .releaseTag)
NPM_TAG=$(echo "${PATCH_JSON}" | jq -r .npmTag)
PREVIOUS_TAG=$(echo "${PATCH_JSON}" | jq -r .previousReleaseTag)
echo "RELEASE_VERSION=${RELEASE_VERSION}" >> "${GITHUB_OUTPUT}"
echo "RELEASE_TAG=${RELEASE_TAG}" >> "${GITHUB_OUTPUT}"
echo "NPM_TAG=${NPM_TAG}" >> "${GITHUB_OUTPUT}"
echo "PREVIOUS_TAG=${PREVIOUS_TAG}" >> "${GITHUB_OUTPUT}"
- name: 'Verify Version Consistency'
env:
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
CHANNEL: '${{ github.event.inputs.type }}'
run: |
echo "🔍 Verifying no concurrent patch releases have occurred..."
# Store original calculation for comparison
ORIGINAL_RELEASE_VERSION="${{ steps.patch_version.outputs.RELEASE_VERSION }}"
ORIGINAL_RELEASE_TAG="${{ steps.patch_version.outputs.RELEASE_TAG }}"
ORIGINAL_PREVIOUS_TAG="${{ steps.patch_version.outputs.PREVIOUS_TAG }}"
echo "Original calculation:"
echo " Release version: ${ORIGINAL_RELEASE_VERSION}"
echo " Release tag: ${ORIGINAL_RELEASE_TAG}"
echo " Previous tag: ${ORIGINAL_PREVIOUS_TAG}"
# Re-run the same version calculation script
echo "Re-calculating version to check for changes..."
CURRENT_PATCH_JSON=$(node scripts/get-release-version.js --type=patch --patch-from="${CHANNEL}")
CURRENT_RELEASE_VERSION=$(echo "${CURRENT_PATCH_JSON}" | jq -r .releaseVersion)
CURRENT_RELEASE_TAG=$(echo "${CURRENT_PATCH_JSON}" | jq -r .releaseTag)
CURRENT_PREVIOUS_TAG=$(echo "${CURRENT_PATCH_JSON}" | jq -r .previousReleaseTag)
echo "Current calculation:"
echo " Release version: ${CURRENT_RELEASE_VERSION}"
echo " Release tag: ${CURRENT_RELEASE_TAG}"
echo " Previous tag: ${CURRENT_PREVIOUS_TAG}"
# Compare calculations
if [[ "${ORIGINAL_RELEASE_VERSION}" != "${CURRENT_RELEASE_VERSION}" ]] || \
[[ "${ORIGINAL_RELEASE_TAG}" != "${CURRENT_RELEASE_TAG}" ]] || \
[[ "${ORIGINAL_PREVIOUS_TAG}" != "${CURRENT_PREVIOUS_TAG}" ]]; then
echo "❌ RACE CONDITION DETECTED: Version calculations have changed!"
echo "This indicates another patch release completed while this one was in progress."
echo ""
echo "Originally planned: ${ORIGINAL_RELEASE_VERSION} (from ${ORIGINAL_PREVIOUS_TAG})"
echo "Should now build: ${CURRENT_RELEASE_VERSION} (from ${CURRENT_PREVIOUS_TAG})"
echo ""
echo "# Setting outputs for failure comment"
echo "CURRENT_RELEASE_VERSION=${CURRENT_RELEASE_VERSION}" >> "${GITHUB_ENV}"
echo "CURRENT_RELEASE_TAG=${CURRENT_RELEASE_TAG}" >> "${GITHUB_ENV}"
echo "CURRENT_PREVIOUS_TAG=${CURRENT_PREVIOUS_TAG}" >> "${GITHUB_ENV}"
echo "The patch release must be restarted to use the correct version numbers."
exit 1
fi
echo "✅ Version calculations unchanged - proceeding with release"
- name: 'Print Calculated Version'
run: |-
echo "Patch Release Summary:"
echo " Release Version: ${{ steps.patch_version.outputs.RELEASE_VERSION }}"
echo " Release Tag: ${{ steps.patch_version.outputs.RELEASE_TAG }}"
echo " NPM Tag: ${{ steps.patch_version.outputs.NPM_TAG }}"
echo " Previous Tag: ${{ steps.patch_version.outputs.PREVIOUS_TAG }}"
- name: 'Run Tests'
uses: './.github/actions/run-tests'
with:
force_skip_tests: '${{ github.event.inputs.force_skip_tests }}'
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
working-directory: './release'
- name: 'Publish Release'
uses: './.github/actions/publish-release'
with:
release-version: '${{ steps.patch_version.outputs.RELEASE_VERSION }}'
release-tag: '${{ steps.patch_version.outputs.RELEASE_TAG }}'
npm-tag: '${{ steps.patch_version.outputs.NPM_TAG }}'
wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
github-token: '${{ secrets.GITHUB_TOKEN }}'
dry-run: '${{ github.event.inputs.dry_run }}'
previous-tag: '${{ steps.patch_version.outputs.PREVIOUS_TAG }}'
working-directory: './release'
- name: 'Create Issue on Failure'
if: '${{ failure() && github.event.inputs.dry_run == false }}'
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
RELEASE_TAG: '${{ steps.patch_version.outputs.RELEASE_TAG }}'
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
run: |
gh issue create \
--title 'Patch Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')' \
--body 'The patch-release workflow failed. See the full run for details: ${DETAILS_URL}' \
--label 'kind/bug,release-failure,priority/p0'
- name: 'Comment Success on Original PR'
if: '${{ success() && github.event.inputs.original_pr }}'
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
ORIGINAL_PR: '${{ github.event.inputs.original_pr }}'
SUCCESS: 'true'
RELEASE_VERSION: '${{ steps.patch_version.outputs.RELEASE_VERSION }}'
RELEASE_TAG: '${{ steps.patch_version.outputs.RELEASE_TAG }}'
NPM_TAG: '${{ steps.patch_version.outputs.NPM_TAG }}'
CHANNEL: '${{ github.event.inputs.type }}'
DRY_RUN: '${{ github.event.inputs.dry_run }}'
GITHUB_RUN_ID: '${{ github.run_id }}'
GITHUB_REPOSITORY_OWNER: '${{ github.repository_owner }}'
GITHUB_REPOSITORY_NAME: '${{ github.event.repository.name }}'
run: |
node scripts/releasing/patch-comment.js
- name: 'Comment Failure on Original PR'
if: '${{ failure() && github.event.inputs.original_pr }}'
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
ORIGINAL_PR: '${{ github.event.inputs.original_pr }}'
SUCCESS: 'false'
RELEASE_VERSION: '${{ steps.patch_version.outputs.RELEASE_VERSION }}'
RELEASE_TAG: '${{ steps.patch_version.outputs.RELEASE_TAG }}'
NPM_TAG: '${{ steps.patch_version.outputs.NPM_TAG }}'
CHANNEL: '${{ github.event.inputs.type }}'
DRY_RUN: '${{ github.event.inputs.dry_run }}'
GITHUB_RUN_ID: '${{ github.run_id }}'
GITHUB_REPOSITORY_OWNER: '${{ github.repository_owner }}'
GITHUB_REPOSITORY_NAME: '${{ github.event.repository.name }}'
# Pass current version info for race condition failures
CURRENT_RELEASE_VERSION: '${{ env.CURRENT_RELEASE_VERSION }}'
CURRENT_RELEASE_TAG: '${{ env.CURRENT_RELEASE_TAG }}'
CURRENT_PREVIOUS_TAG: '${{ env.CURRENT_PREVIOUS_TAG }}'
run: |
# Check if this was a version consistency failure
if [[ -n "${CURRENT_RELEASE_VERSION}" ]]; then
echo "Detected version race condition failure - posting specific comment with current version info"
export RACE_CONDITION_FAILURE=true
fi
node scripts/releasing/patch-comment.js
@@ -0,0 +1,187 @@
name: 'Release: Patch from Comment'
on:
issue_comment:
types: ['created']
jobs:
slash-command:
runs-on: 'ubuntu-latest'
# Only run if the comment is from a human user (not automated)
if: "github.event.comment.user.type == 'User' && github.event.comment.user.login != 'github-actions[bot]'"
permissions:
contents: 'write'
pull-requests: 'write'
actions: 'write'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
with:
fetch-depth: 1
- name: 'Slash Command Dispatch'
id: 'slash_command'
uses: 'peter-evans/slash-command-dispatch@40877f718dce0101edfc7aea2b3800cc192f9ed5'
with:
token: '${{ secrets.GITHUB_TOKEN }}'
commands: 'patch'
permission: 'write'
issue-type: 'pull-request'
- name: 'Get PR Status'
id: 'pr_status'
if: "startsWith(github.event.comment.body, '/patch')"
env:
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
run: |
gh pr view "${{ github.event.issue.number }}" --json mergeCommit,state > pr_status.json
echo "MERGE_COMMIT_SHA=$(jq -r .mergeCommit.oid pr_status.json)" >> "$GITHUB_OUTPUT"
echo "STATE=$(jq -r .state pr_status.json)" >> "$GITHUB_OUTPUT"
- name: 'Dispatch if Merged'
if: "steps.pr_status.outputs.STATE == 'MERGED'"
id: 'dispatch_patch'
uses: 'actions/github-script@00f12e3e20659f42342b1c0226afda7f7c042325'
env:
COMMENT_BODY: '${{ github.event.comment.body }}'
with:
github-token: '${{ secrets.GITHUB_TOKEN }}'
script: |
// Parse the comment body directly to extract channel(s)
const commentBody = process.env.COMMENT_BODY;
console.log('Comment body:', commentBody);
let channels = ['stable', 'preview']; // default to both
// Parse different formats:
// /patch (defaults to both)
// /patch both
// /patch stable
// /patch preview
if (commentBody.trim() === '/patch' || commentBody.trim() === '/patch both') {
channels = ['stable', 'preview'];
} else if (commentBody.trim() === '/patch stable') {
channels = ['stable'];
} else if (commentBody.trim() === '/patch preview') {
channels = ['preview'];
} else {
// Fallback parsing for legacy formats
if (commentBody.includes('channel=preview')) {
channels = ['preview'];
} else if (commentBody.includes('--channel preview')) {
channels = ['preview'];
}
}
console.log('Detected channels:', channels);
const dispatchedRuns = [];
// Dispatch workflow for each channel
for (const channel of channels) {
console.log(`Dispatching workflow for channel: ${channel}`);
const response = await github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'release-patch-1-create-pr.yml',
ref: 'main',
inputs: {
commit: '${{ steps.pr_status.outputs.MERGE_COMMIT_SHA }}',
channel: channel,
original_pr: '${{ github.event.issue.number }}'
}
});
dispatchedRuns.push({ channel, response });
}
// Wait a moment for the workflows to be created
await new Promise(resolve => setTimeout(resolve, 3000));
const runs = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'release-patch-1-create-pr.yml',
per_page: 20 // Increased to handle multiple runs
});
// Find the recent runs that match our trigger
const recentRuns = runs.data.workflow_runs.filter(run =>
run.event === 'workflow_dispatch' &&
new Date(run.created_at) > new Date(Date.now() - 15000) // Within last 15 seconds
).slice(0, channels.length); // Limit to the number of channels we dispatched
// Set outputs
core.setOutput('dispatched_channels', channels.join(','));
core.setOutput('dispatched_run_count', channels.length.toString());
if (recentRuns.length > 0) {
core.setOutput('dispatched_run_urls', recentRuns.map(r => r.html_url).join(','));
core.setOutput('dispatched_run_ids', recentRuns.map(r => r.id).join(','));
}
- name: 'Comment on Failure'
if: "startsWith(github.event.comment.body, '/patch') && steps.pr_status.outputs.STATE != 'MERGED'"
uses: 'peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d'
with:
token: '${{ secrets.GITHUB_TOKEN }}'
issue-number: '${{ github.event.issue.number }}'
body: |
:x: The `/patch` command failed. This pull request must be merged before a patch can be created.
- name: 'Final Status Comment - Success'
if: "always() && startsWith(github.event.comment.body, '/patch') && steps.dispatch_patch.outcome == 'success' && steps.dispatch_patch.outputs.dispatched_run_urls"
uses: 'peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d'
with:
token: '${{ secrets.GITHUB_TOKEN }}'
issue-number: '${{ github.event.issue.number }}'
body: |
✅ **Patch workflow(s) dispatched successfully!**
**📋 Details:**
- **Channels**: `${{ steps.dispatch_patch.outputs.dispatched_channels }}`
- **Commit**: `${{ steps.pr_status.outputs.MERGE_COMMIT_SHA }}`
- **Workflows Created**: ${{ steps.dispatch_patch.outputs.dispatched_run_count }}
**🔗 Track Progress:**
- [View patch workflows](https://github.com/${{ github.repository }}/actions/workflows/release-patch-1-create-pr.yml)
- [This workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
- name: 'Final Status Comment - Dispatch Success (No URL)'
if: "always() && startsWith(github.event.comment.body, '/patch') && steps.dispatch_patch.outcome == 'success' && !steps.dispatch_patch.outputs.dispatched_run_urls"
uses: 'peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d'
with:
token: '${{ secrets.GITHUB_TOKEN }}'
issue-number: '${{ github.event.issue.number }}'
body: |
✅ **Patch workflow(s) dispatched successfully!**
**📋 Details:**
- **Channels**: `${{ steps.dispatch_patch.outputs.dispatched_channels }}`
- **Commit**: `${{ steps.pr_status.outputs.MERGE_COMMIT_SHA }}`
- **Workflows Created**: ${{ steps.dispatch_patch.outputs.dispatched_run_count }}
**🔗 Track Progress:**
- [View patch workflows](https://github.com/${{ github.repository }}/actions/workflows/release-patch-1-create-pr.yml)
- [This workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
- name: 'Final Status Comment - Failure'
if: "always() && startsWith(github.event.comment.body, '/patch') && (steps.dispatch_patch.outcome == 'failure' || steps.dispatch_patch.outcome == 'cancelled')"
uses: 'peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d'
with:
token: '${{ secrets.GITHUB_TOKEN }}'
issue-number: '${{ github.event.issue.number }}'
body: |
❌ **Patch workflow dispatch failed!**
There was an error dispatching the patch creation workflow.
**🔍 Troubleshooting:**
- Check that the PR is properly merged
- Verify workflow permissions
- Review error logs in the workflow run
**🔗 Debug Links:**
- [This workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
- [Patch workflow history](https://github.com/${{ github.repository }}/actions/workflows/release-patch-1-create-pr.yml)
@@ -1,4 +1,4 @@
name: 'Promote Release'
name: 'Release: Promote'
on:
workflow_dispatch:
@@ -303,9 +303,6 @@ jobs:
DRY_RUN: '${{ github.event.inputs.dry_run }}'
run: |-
git add package.json packages/*/package.json
if [ -f npm-shrinkwrap.json ]; then
git add npm-shrinkwrap.json
fi
if [ -f package-lock.json ]; then
git add package-lock.json
fi
@@ -317,20 +314,15 @@ jobs:
echo "Dry run enabled. Skipping push."
fi
- name: 'Create and Approve Pull Request'
if: |-
${{ github.event.inputs.dry_run == 'false' }}
env:
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
run: |
gh pr create \
--title "chore(release): bump version to ${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}" \
--body "Automated version bump to prepare for the next nightly release." \
--base "main" \
--head "${BRANCH_NAME}" \
--fill
gh pr merge --auto --squash
- name: 'Create and Merge Pull Request'
uses: './.github/actions/create-pull-request'
with:
branch-name: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
pr-title: 'chore(release): bump version to ${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}'
pr-body: 'Automated version bump to prepare for the next nightly release.'
app-id: '${{ secrets.APP_ID }}'
private-key: '${{ secrets.PRIVATE_KEY }}'
dry-run: '${{ github.event.inputs.dry_run }}'
- name: 'Create Issue on Failure'
if: '${{ failure() && github.event.inputs.dry_run == false }}'
+33
View File
@@ -0,0 +1,33 @@
name: 'Release Sandbox'
on:
workflow_dispatch:
inputs:
ref:
description: 'The branch, tag, or SHA to release from.'
required: false
type: 'string'
default: 'main'
dry-run:
description: 'Whether this is a dry run.'
required: false
type: 'boolean'
default: true
jobs:
build:
runs-on: 'ubuntu-latest'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
with:
ref: '${{ github.event.inputs.ref || github.sha }}'
fetch-depth: 0
- name: 'Push'
uses: './.github/actions/push-sandbox'
with:
github-actor: '${{ github.actor }}'
github-secret: '${{ secrets.GITHUB_TOKEN }}'
github-sha: '${{ github.event.inputs.ref || github.sha }}'
github-ref-name: '${{github.event.inputs.ref}}'
dry-run: '${{ github.event.inputs.dry-run }}'
-202
View File
@@ -1,202 +0,0 @@
name: 'Release'
on:
workflow_dispatch:
inputs:
version:
description: 'The version to release (e.g., v0.1.11).'
required: true
type: 'string'
ref:
description: 'The branch or ref (full git sha) to release from.'
required: true
type: 'string'
default: 'main'
dry_run:
description: 'Run a dry-run of the release process; no branches, npm packages or GitHub releases will be created.'
required: true
type: 'boolean'
default: true
force_skip_tests:
description: 'Select to skip the "Run Tests" step in testing. Prod releases should run tests'
required: false
type: 'boolean'
default: false
jobs:
release:
runs-on: 'ubuntu-latest'
environment:
name: 'production-release'
url: '${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ steps.version.outputs.RELEASE_TAG }}'
if: |-
${{ github.repository == 'google-gemini/gemini-cli' }}
permissions:
contents: 'write'
packages: 'write'
id-token: 'write'
issues: 'write' # For creating issues on failure
outputs:
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
ref: '${{ github.event.inputs.ref || github.sha }}'
fetch-depth: 0
- name: 'Set booleans for simplified logic'
env:
DRY_RUN_INPUT: '${{ github.event.inputs.dry_run }}'
id: 'vars'
run: |-
is_dry_run="false"
if [[ "${DRY_RUN_INPUT}" == "true" ]]; then
is_dry_run="true"
fi
echo "is_dry_run=${is_dry_run}" >> "${GITHUB_OUTPUT}"
- name: 'Setup 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: 'Get the version'
id: 'version'
run: |-
RELEASE_TAG="${{ inputs.version }}"
# The version for npm should not have the 'v' prefix.
RELEASE_VERSION="${RELEASE_TAG#v}"
NPM_TAG="latest"
if [[ "${RELEASE_TAG}" == *"preview"* ]]; then
NPM_TAG="preview"
fi
PREVIOUS_TAG=$(git describe --tags --abbrev=0)
echo "RELEASE_TAG=${RELEASE_TAG}" >> "${GITHUB_OUTPUT}"
echo "RELEASE_VERSION=${RELEASE_VERSION}" >> "${GITHUB_OUTPUT}"
echo "NPM_TAG=${NPM_TAG}" >> "${GITHUB_OUTPUT}"
echo "PREVIOUS_TAG=${PREVIOUS_TAG}" >> "${GITHUB_OUTPUT}"
- name: 'Run Tests'
if: |-
${{ github.event.inputs.force_skip_tests != 'true' }}
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
run: |-
npm run preflight
npm run test:integration:sandbox:none
npm run test:integration:sandbox:docker
- name: 'Configure Git User'
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'
id: 'release_branch'
env:
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
run: |-
BRANCH_NAME="release/${RELEASE_TAG}"
git switch -c "${BRANCH_NAME}"
echo "BRANCH_NAME=${BRANCH_NAME}" >> "${GITHUB_OUTPUT}"
- name: 'Update package versions'
env:
RELEASE_VERSION: '${{ steps.version.outputs.RELEASE_VERSION }}'
run: |-
npm run release:version "${RELEASE_VERSION}"
- name: 'Commit and Conditionally Push package versions'
env:
BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
IS_DRY_RUN: '${{ steps.vars.outputs.is_dry_run }}'
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
run: |-
git add package.json npm-shrinkwrap.json packages/*/package.json
git commit -m "chore(release): ${RELEASE_TAG}"
if [[ "${IS_DRY_RUN}" == "false" ]]; then
echo "Pushing release branch to remote..."
git push --set-upstream origin "${BRANCH_NAME}" --follow-tags
else
echo "Dry run enabled. Skipping push."
fi
- name: 'Build and Prepare Packages'
run: |-
npm run build:packages
npm run prepare:package
- name: 'Configure npm for publishing'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
with:
node-version-file: '.nvmrc'
registry-url: 'https://wombat-dressing-room.appspot.com'
scope: '@google'
- name: 'Publish @google/gemini-cli-core'
env:
IS_DRY_RUN: '${{ steps.vars.outputs.is_dry_run }}'
NODE_AUTH_TOKEN: '${{ secrets.WOMBAT_TOKEN_CORE }}'
NPM_TAG: '${{ steps.version.outputs.NPM_TAG }}'
run: |-
npm publish \
--dry-run="${IS_DRY_RUN}" \
--workspace="@google/gemini-cli-core" \
--tag="${NPM_TAG}"
- name: 'Install latest core package'
if: |-
${{ steps.vars.outputs.is_dry_run == 'false' }}
env:
RELEASE_VERSION: '${{ steps.version.outputs.RELEASE_VERSION }}'
run: |-
npm install "@google/gemini-cli-core@${RELEASE_VERSION}" \
--workspace="@google/gemini-cli" \
--save-exact
- name: 'Publish @google/gemini-cli'
env:
IS_DRY_RUN: '${{ steps.vars.outputs.is_dry_run }}'
NODE_AUTH_TOKEN: '${{ secrets.WOMBAT_TOKEN_CLI }}'
NPM_TAG: '${{ steps.version.outputs.NPM_TAG }}'
run: |-
npm publish \
--dry-run="${IS_DRY_RUN}" \
--workspace="@google/gemini-cli" \
--tag="${NPM_TAG}"
- name: 'Create GitHub Release and Tag'
if: |-
${{ steps.vars.outputs.is_dry_run == 'false' }}
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
RELEASE_BRANCH: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
PREVIOUS_TAG: '${{ steps.version.outputs.PREVIOUS_TAG }}'
run: |-
gh release create "${RELEASE_TAG}" \
bundle/gemini.js \
--target "$RELEASE_BRANCH" \
--title "Release ${RELEASE_TAG}" \
--notes-start-tag "$PREVIOUS_TAG" \
--generate-notes
- name: 'Create Issue on Failure'
if: |-
${{ failure() }}
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }} || "N/A"'
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
run: |-
gh issue create \
--title "Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')" \
--body "The release workflow failed. See the full run for details: ${DETAILS_URL}" \
--label "kind/bug,release-failure,priority/p0"
@@ -1,30 +0,0 @@
name: 'Trigger Patch Release'
on:
pull_request:
types:
- 'closed'
jobs:
trigger-patch-release:
if: "github.event.pull_request.merged == true && startsWith(github.head_ref, 'hotfix/')"
runs-on: 'ubuntu-latest'
steps:
- name: 'Trigger Patch Release'
uses: 'actions/github-script@00f12e3e20659f42342b1c0226afda7f7c042325'
with:
script: |
const body = context.payload.pull_request.body;
const isDryRun = body.includes('[DRY RUN]');
const ref = context.payload.pull_request.base.ref;
const channel = ref.includes('preview') ? 'preview' : 'stable';
github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'patch-release.yml',
ref: ref,
inputs: {
type: channel,
dry_run: isDryRun.toString()
}
})
+31
View File
@@ -0,0 +1,31 @@
name: 'Verify NPM release tag'
on:
workflow_dispatch:
inputs:
version:
description: 'The expected Gemini binary version that should be released (e.g., 0.5.0-preview-2).'
required: true
type: 'string'
npm-package:
description: 'NPM package to verify'
required: true
type: 'string'
default: '@google/gemini-cli@latest'
ref:
description: 'The branch, tag, or SHA to release from.'
required: false
type: 'string'
default: 'main'
jobs:
build:
runs-on: 'ubuntu-latest'
steps:
- uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
- name: 'Verify release'
uses: './.github/actions/verify-release'
with:
npm-package: '${github.event.inputs.npm-package}'
expected-version: '${github.event.inputs.version}'
ref: '${github.event.inputs.ref}'
-1
View File
@@ -17,5 +17,4 @@ eslint.config.js
**/generated
gha-creds-*.json
junit.xml
npm-shrinkwrap.json
Thumbs.db
+3 -3
View File
@@ -203,9 +203,9 @@ Settings are organized into categories. All settings should be placed within the
- **Description:** Sandbox execution environment (can be a boolean or a path string).
- **Default:** `undefined`
- **`tools.usePty`** (boolean):
- **Description:** Use node-pty for shell command execution. Fallback to child_process still applies.
- **Default:** `false`
- **`tools.shell.enableInteractiveShell`** (boolean):
Use `node-pty` for an interactive shell experience. Fallback to `child_process` still applies. Defaults to `false`.
- **`tools.core`** (array of strings):
- **Description:** This can be used to restrict the set of built-in tools [with an allowlist](./enterprise.md#restricting-tool-access). See [Built-in Tools](../core/tools-api.md#built-in-tools) for a list of core tools. The match semantics are the same as `tools.allowed`.
+7 -5
View File
@@ -4,7 +4,7 @@ This document describes the `run_shell_command` tool for the Gemini CLI.
## Description
Use `run_shell_command` to interact with the underlying system, run scripts, or perform command-line operations. `run_shell_command` executes a given shell command, including interactive commands that require user input (e.g., `vim`, `git rebase -i`) if the `tools.usePty` setting is set to `true`.
Use `run_shell_command` to interact with the underlying system, run scripts, or perform command-line operations. `run_shell_command` executes a given shell command, including interactive commands that require user input (e.g., `vim`, `git rebase -i`) if the `tools.shell.enableInteractiveShell` setting is set to `true`.
On Windows, commands are executed with `cmd.exe /c`. On other platforms, they are executed with `bash -c`.
@@ -61,21 +61,23 @@ You can configure the behavior of the `run_shell_command` tool by modifying your
### Enabling Interactive Commands
To enable interactive commands, you need to set the `tools.usePty` setting to `true`. This will use `node-pty` for shell command execution, which allows for interactive sessions. If `node-pty` is not available, it will fall back to the `child_process` implementation, which does not support interactive commands.
To enable interactive commands, you need to set the `tools.shell.enableInteractiveShell` setting to `true`. This will use `node-pty` for shell command execution, which allows for interactive sessions. If `node-pty` is not available, it will fall back to the `child_process` implementation, which does not support interactive commands.
**Example `settings.json`:**
```json
{
"tools": {
"usePty": true
"shell": {
"enableInteractiveShell": true
}
}
}
```
### Showing Color in Output
To show color in the shell output, you need to set the `tools.shell.showColor` setting to `true`. **Note: This setting only applies when `tools.usePty` is enabled.**
To show color in the shell output, you need to set the `tools.shell.showColor` setting to `true`. **Note: This setting only applies when `tools.shell.enableInteractiveShell` is enabled.**
**Example `settings.json`:**
@@ -91,7 +93,7 @@ To show color in the shell output, you need to set the `tools.shell.showColor` s
### Setting the Pager
You can set a custom pager for the shell output by setting the `tools.shell.pager` setting. The default pager is `cat`. **Note: This setting only applies when `tools.usePty` is enabled.**
You can set a custom pager for the shell output by setting the `tools.shell.pager` setting. The default pager is `cat`. **Note: This setting only applies when `tools.shell.enableInteractiveShell` is enabled.**
**Example `settings.json`:**
+38 -137
View File
@@ -1,12 +1,12 @@
{
"name": "@google/gemini-cli",
"version": "0.6.0-nightly",
"version": "0.6.0-preview.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@google/gemini-cli",
"version": "0.6.0-nightly",
"version": "0.6.0-preview.5",
"workspaces": [
"packages/*"
],
@@ -1655,51 +1655,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@inquirer/core/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/@inquirer/core/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/@inquirer/core/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@inquirer/core/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@inquirer/core/node_modules/type-fest": {
"version": "0.21.3",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
@@ -1713,21 +1668,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@inquirer/core/node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@inquirer/figures": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz",
@@ -6341,23 +6281,6 @@
"node": ">=8"
}
},
"node_modules/cliui/node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/cockatiel": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz",
@@ -9764,23 +9687,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ink/node_modules/wrap-ansi": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
"integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/internal-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@@ -15263,18 +15169,6 @@
"url": "https://github.com/yeoman/update-notifier?sponsor=1"
}
},
"node_modules/update-notifier/node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/update-notifier/node_modules/boxen": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz",
@@ -15371,23 +15265,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/update-notifier/node_modules/wrap-ansi": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
"integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
@@ -15943,17 +15820,17 @@
}
},
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=12"
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
@@ -16030,6 +15907,29 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/wrap-ansi/node_modules/emoji-regex": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz",
"integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==",
"license": "MIT"
},
"node_modules/wrap-ansi/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -16279,7 +16179,7 @@
},
"packages/a2a-server": {
"name": "@google/gemini-cli-a2a-server",
"version": "0.6.0-nightly",
"version": "0.6.0-preview.5",
"dependencies": {
"@a2a-js/sdk": "^0.3.2",
"@google-cloud/storage": "^7.16.0",
@@ -16550,7 +16450,7 @@
},
"packages/cli": {
"name": "@google/gemini-cli",
"version": "0.6.0-nightly",
"version": "0.6.0-preview.5",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
"@google/genai": "1.16.0",
@@ -16578,6 +16478,7 @@
"strip-json-comments": "^3.1.1",
"undici": "^7.10.0",
"update-notifier": "^7.3.1",
"wrap-ansi": "9.0.2",
"yargs": "^17.7.2",
"zod": "^3.23.8"
},
@@ -16742,7 +16643,7 @@
},
"packages/core": {
"name": "@google/gemini-cli-core",
"version": "0.6.0-nightly",
"version": "0.6.0-preview.5",
"dependencies": {
"@google-cloud/logging": "^11.2.1",
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0",
@@ -16887,7 +16788,7 @@
},
"packages/test-utils": {
"name": "@google/gemini-cli-test-utils",
"version": "0.6.0-nightly",
"version": "0.6.0-preview.5",
"license": "Apache-2.0",
"devDependencies": {
"typescript": "^5.3.3"
@@ -16898,7 +16799,7 @@
},
"packages/vscode-ide-companion": {
"name": "gemini-cli-vscode-ide-companion",
"version": "0.6.0-nightly",
"version": "0.6.0-preview.5",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.15.1",
+6 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.6.0-nightly",
"version": "0.6.0-preview.5",
"engines": {
"node": ">=20.0.0"
},
@@ -14,7 +14,7 @@
"url": "git+https://github.com/google-gemini/gemini-cli.git"
},
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.6.0-nightly"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.6.0-preview.5"
},
"scripts": {
"start": "node scripts/start.js",
@@ -59,8 +59,7 @@
"files": [
"bundle/",
"README.md",
"LICENSE",
"npm-shrinkwrap.json"
"LICENSE"
],
"devDependencies": {
"@types/marked": "^5.0.2",
@@ -103,5 +102,8 @@
"@lydell/node-pty-win32-arm64": "1.1.0",
"@lydell/node-pty-win32-x64": "1.1.0",
"node-pty": "^1.0.0"
},
"overrides": {
"wrap-ansi": "9.0.2"
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-a2a-server",
"version": "0.6.0-nightly",
"version": "0.6.0-preview.5",
"private": true,
"description": "Gemini CLI A2A Server",
"repository": {
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.6.0-nightly",
"version": "0.6.0-preview.5",
"description": "Gemini CLI",
"repository": {
"type": "git",
@@ -25,7 +25,7 @@
"dist"
],
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.6.0-nightly"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.6.0-preview.5"
},
"dependencies": {
"@google/gemini-cli-core": "file:../core",
@@ -54,6 +54,7 @@
"strip-json-comments": "^3.1.1",
"undici": "^7.10.0",
"update-notifier": "^7.3.1",
"wrap-ansi": "9.0.2",
"yargs": "^17.7.2",
"zod": "^3.23.8"
},
+1 -1
View File
@@ -633,7 +633,7 @@ export async function loadCliConfig(
interactive,
trustedFolder,
useRipgrep: settings.tools?.useRipgrep,
shouldUseNodePtyShell: settings.tools?.usePty,
shouldUseNodePtyShell: settings.tools?.shell?.enableInteractiveShell,
skipNextSpeakerCheck: settings.model?.skipNextSpeakerCheck,
enablePromptCompletion: settings.general?.enablePromptCompletion ?? false,
truncateToolOutputThreshold: settings.tools?.truncateToolOutputThreshold,
+1 -1
View File
@@ -107,7 +107,7 @@ const MIGRATION_MAP: Record<string, string> = {
preferredEditor: 'general.preferredEditor',
sandbox: 'tools.sandbox',
selectedAuthType: 'security.auth.selectedType',
shouldUseNodePtyShell: 'tools.usePty',
shouldUseNodePtyShell: 'tools.shell.enableInteractiveShell',
shellPager: 'tools.shell.pager',
shellShowColor: 'tools.shell.showColor',
skipNextSpeakerCheck: 'model.skipNextSpeakerCheck',
+10 -10
View File
@@ -639,16 +639,6 @@ const SETTINGS_SCHEMA = {
'Sandbox execution environment (can be a boolean or a path string).',
showInDialog: false,
},
usePty: {
type: 'boolean',
label: 'Use node-pty for Shell Execution',
category: 'Tools',
requiresRestart: true,
default: false,
description:
'Use node-pty for shell command execution. Fallback to child_process still applies.',
showInDialog: true,
},
shell: {
type: 'object',
label: 'Shell',
@@ -658,6 +648,16 @@ const SETTINGS_SCHEMA = {
description: 'Settings for shell execution.',
showInDialog: false,
properties: {
enableInteractiveShell: {
type: 'boolean',
label: 'Enable Interactive Shell',
category: 'Tools',
requiresRestart: true,
default: false,
description:
'Use node-pty for an interactive shell experience. Fallback to child_process still applies.',
showInDialog: true,
},
pager: {
type: 'string',
label: 'Pager',
@@ -1262,7 +1262,7 @@ describe('SettingsDialog', () => {
},
},
tools: {
usePty: true,
enableInteractiveShell: true,
autoAccept: true,
useRipgrep: true,
},
@@ -1375,7 +1375,7 @@ describe('SettingsDialog', () => {
},
tools: {
useRipgrep: true,
usePty: false,
enableInteractiveShell: false,
},
},
);
@@ -1449,7 +1449,7 @@ describe('SettingsDialog', () => {
it('should render with tools and security settings', () => {
const settings = createMockSettings({
tools: {
usePty: true,
enableInteractiveShell: true,
autoAccept: false,
useRipgrep: true,
truncateToolOutputThreshold: 25000,
@@ -1508,7 +1508,7 @@ describe('SettingsDialog', () => {
},
},
tools: {
usePty: false,
enableInteractiveShell: false,
autoAccept: false,
useRipgrep: false,
},
@@ -12,7 +12,7 @@ import { ToolCallStatus } from '../../types.js';
import { ToolMessage } from './ToolMessage.js';
import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
import { theme } from '../../semantic-colors.js';
import { SHELL_COMMAND_NAME } from '../../constants.js';
import { SHELL_COMMAND_NAME, SHELL_NAME } from '../../constants.js';
import { useConfig } from '../../contexts/ConfigContext.js';
interface ToolGroupMessageProps {
@@ -47,11 +47,15 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
);
const config = useConfig();
const isShellCommand = toolCalls.some((t) => t.name === SHELL_COMMAND_NAME);
const isShellCommand = toolCalls.some(
(t) => t.name === SHELL_COMMAND_NAME || t.name === SHELL_NAME,
);
const borderColor =
hasPending || isShellCommand || isShellFocused
? theme.status.warning
: theme.border.default;
isShellCommand || isShellFocused
? theme.ui.symbol
: hasPending
? theme.status.warning
: theme.border.default;
const staticHeight = /* border */ 2 + /* marginBottom */ 1;
// This is a bit of a magic number, but it accounts for the border and
@@ -14,7 +14,11 @@ import { AnsiOutputText } from '../AnsiOutput.js';
import { GeminiRespondingSpinner } from '../GeminiRespondingSpinner.js';
import { MaxSizedBox } from '../shared/MaxSizedBox.js';
import { ShellInputPrompt } from '../ShellInputPrompt.js';
import { SHELL_COMMAND_NAME, TOOL_STATUS } from '../../constants.js';
import {
SHELL_COMMAND_NAME,
SHELL_NAME,
TOOL_STATUS,
} from '../../constants.js';
import { theme } from '../../semantic-colors.js';
import type { AnsiOutput, Config } from '@google/gemini-cli-core';
@@ -58,6 +62,11 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
ptyId === activeShellPtyId &&
shellFocused;
const isThisShellFocusable =
(name === SHELL_COMMAND_NAME || name === 'Shell') &&
status === ToolCallStatus.Executing &&
config?.getShouldUseNodePtyShell();
const availableHeight = availableTerminalHeight
? Math.max(
availableTerminalHeight - STATIC_HEIGHT - RESERVED_LINE_COUNT,
@@ -83,16 +92,18 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
return (
<Box paddingX={1} paddingY={0} flexDirection="column">
<Box minHeight={1}>
<ToolStatusIndicator status={status} />
<ToolStatusIndicator status={status} name={name} />
<ToolInfo
name={name}
status={status}
description={description}
emphasis={emphasis}
/>
{isThisShellFocused && (
<Box marginLeft={1}>
<Text color={theme.text.accent}>[Focused]</Text>
{isThisShellFocusable && (
<Box marginLeft={1} flexShrink={0}>
<Text color={theme.text.accent}>
{isThisShellFocused ? '(Focused)' : '(ctrl+f to focus)'}
</Text>
</Box>
)}
{emphasis === 'high' && <TrailingIndicator />}
@@ -118,7 +129,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
</Box>
</MaxSizedBox>
) : typeof resultDisplay === 'object' &&
!Array.isArray(resultDisplay) ? (
'fileDiff' in resultDisplay ? (
<DiffRenderer
diffContent={resultDisplay.fileDiff}
filename={resultDisplay.fileName}
@@ -148,43 +159,50 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
type ToolStatusIndicatorProps = {
status: ToolCallStatus;
name: string;
};
const ToolStatusIndicator: React.FC<ToolStatusIndicatorProps> = ({
status,
}) => (
<Box minWidth={STATUS_INDICATOR_WIDTH}>
{status === ToolCallStatus.Pending && (
<Text color={theme.status.success}>{TOOL_STATUS.PENDING}</Text>
)}
{status === ToolCallStatus.Executing && (
<GeminiRespondingSpinner
spinnerType="toggle"
nonRespondingDisplay={TOOL_STATUS.EXECUTING}
/>
)}
{status === ToolCallStatus.Success && (
<Text color={theme.status.success} aria-label={'Success:'}>
{TOOL_STATUS.SUCCESS}
</Text>
)}
{status === ToolCallStatus.Confirming && (
<Text color={theme.status.warning} aria-label={'Confirming:'}>
{TOOL_STATUS.CONFIRMING}
</Text>
)}
{status === ToolCallStatus.Canceled && (
<Text color={theme.status.warning} aria-label={'Canceled:'} bold>
{TOOL_STATUS.CANCELED}
</Text>
)}
{status === ToolCallStatus.Error && (
<Text color={theme.status.error} aria-label={'Error:'} bold>
{TOOL_STATUS.ERROR}
</Text>
)}
</Box>
);
name,
}) => {
const isShell = name === SHELL_COMMAND_NAME || name === SHELL_NAME;
const statusColor = isShell ? theme.ui.symbol : theme.status.warning;
return (
<Box minWidth={STATUS_INDICATOR_WIDTH}>
{status === ToolCallStatus.Pending && (
<Text color={theme.status.success}>{TOOL_STATUS.PENDING}</Text>
)}
{status === ToolCallStatus.Executing && (
<GeminiRespondingSpinner
spinnerType="toggle"
nonRespondingDisplay={TOOL_STATUS.EXECUTING}
/>
)}
{status === ToolCallStatus.Success && (
<Text color={theme.status.success} aria-label={'Success:'}>
{TOOL_STATUS.SUCCESS}
</Text>
)}
{status === ToolCallStatus.Confirming && (
<Text color={statusColor} aria-label={'Confirming:'}>
{TOOL_STATUS.CONFIRMING}
</Text>
)}
{status === ToolCallStatus.Canceled && (
<Text color={statusColor} aria-label={'Canceled:'} bold>
{TOOL_STATUS.CANCELED}
</Text>
)}
{status === ToolCallStatus.Error && (
<Text color={theme.status.error} aria-label={'Error:'} bold>
{TOOL_STATUS.ERROR}
</Text>
)}
</Box>
);
};
type ToolInfo = {
name: string;
+2
View File
@@ -16,6 +16,8 @@ export const STREAM_DEBOUNCE_MS = 100;
export const SHELL_COMMAND_NAME = 'Shell Command';
export const SHELL_NAME = 'Shell';
// Tool status symbols used in ToolMessage component
export const TOOL_STATUS = {
SUCCESS: '✓',
@@ -238,7 +238,7 @@ describe('useShellCommandProcessor', () => {
'/test/dir',
expect.any(Function),
expect.any(Object),
false, // usePty
false, // enableInteractiveShell
expect.any(Object),
);
@@ -143,6 +143,8 @@ export const useShellCommandProcessor = (
const activeTheme = themeManager.getActiveTheme();
const shellExecutionConfig = {
...config.getShellExecutionConfig(),
terminalWidth,
terminalHeight,
defaultFg: activeTheme.colors.Foreground,
defaultBg: activeTheme.colors.Background,
};
@@ -158,14 +160,14 @@ export const useShellCommandProcessor = (
if (isBinaryStream) break;
// PTY provides the full screen state, so we just replace.
// Child process provides chunks, so we append.
if (
if (config.getShouldUseNodePtyShell()) {
cumulativeStdout = event.chunk;
shouldUpdate = true;
} else if (
typeof event.chunk === 'string' &&
typeof cumulativeStdout === 'string'
) {
cumulativeStdout += event.chunk;
} else {
cumulativeStdout = event.chunk;
shouldUpdate = true;
}
break;
case 'binary_detected':
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-core",
"version": "0.6.0-nightly",
"version": "0.6.0-preview.5",
"description": "Gemini CLI Core",
"repository": {
"type": "git",
@@ -10,6 +10,7 @@ import type { Readable } from 'node:stream';
import { type ChildProcess } from 'node:child_process';
import type { ShellOutputEvent } from './shellExecutionService.js';
import { ShellExecutionService } from './shellExecutionService.js';
import type { AnsiOutput } from '../utils/terminalSerializer.js';
// Hoisted Mocks
const mockPtySpawn = vi.hoisted(() => vi.fn());
@@ -54,6 +55,10 @@ vi.mock('../utils/terminalSerializer.js', () => ({
serializeTerminalToObject: mockSerializeTerminalToObject,
}));
const mockProcessKill = vi
.spyOn(process, 'kill')
.mockImplementation(() => true);
const shellExecutionConfig = {
terminalWidth: 80,
terminalHeight: 24,
@@ -61,9 +66,25 @@ const shellExecutionConfig = {
showColor: false,
};
const mockProcessKill = vi
.spyOn(process, 'kill')
.mockImplementation(() => true);
const createExpectedAnsiOutput = (text: string | string[]): AnsiOutput => {
const lines = Array.isArray(text) ? text : text.split('\n');
const expected: AnsiOutput = Array.from(
{ length: shellExecutionConfig.terminalHeight },
(_, i) => [
{
text: expect.stringMatching((lines[i] || '').trim()),
bold: false,
italic: false,
underline: false,
dim: false,
inverse: false,
fg: '',
bg: '',
},
],
);
return expected;
};
describe('ShellExecutionService', () => {
let mockPtyProcess: EventEmitter & {
@@ -77,6 +98,11 @@ describe('ShellExecutionService', () => {
let mockHeadlessTerminal: {
resize: Mock;
scrollLines: Mock;
buffer: {
active: {
viewportY: number;
};
};
};
let onOutputEventMock: Mock<(event: ShellOutputEvent) => void>;
@@ -110,6 +136,11 @@ describe('ShellExecutionService', () => {
mockHeadlessTerminal = {
resize: vi.fn(),
scrollLines: vi.fn(),
buffer: {
active: {
viewportY: 0,
},
},
};
mockPtySpawn.mockReturnValue(mockPtyProcess);
@@ -161,7 +192,7 @@ describe('ShellExecutionService', () => {
expect(onOutputEventMock).toHaveBeenCalledWith({
type: 'data',
chunk: 'file1.txt',
chunk: createExpectedAnsiOutput('file1.txt'),
});
});
@@ -175,7 +206,7 @@ describe('ShellExecutionService', () => {
expect(onOutputEventMock).toHaveBeenCalledWith(
expect.objectContaining({
type: 'data',
chunk: expect.stringContaining('aredword'),
chunk: createExpectedAnsiOutput('aredword'),
}),
);
});
@@ -197,7 +228,7 @@ describe('ShellExecutionService', () => {
expect(onOutputEventMock).toHaveBeenCalledWith(
expect.objectContaining({
chunk: expect.stringMatching(/^\s*$/),
chunk: createExpectedAnsiOutput(''),
}),
);
});
@@ -438,17 +469,44 @@ describe('ShellExecutionService', () => {
);
});
it('should call onOutputEvent with plain string when showColor is false', async () => {
await simulateExecution('ls --color=auto', (pty) => {
pty.onData.mock.calls[0][0]('a\u001b[31mred\u001b[0mword');
pty.onExit.mock.calls[0][0]({ exitCode: 0, signal: null });
});
it('should call onOutputEvent with AnsiOutput when showColor is false', async () => {
await simulateExecution(
'ls --color=auto',
(pty) => {
pty.onData.mock.calls[0][0]('a\u001b[31mred\u001b[0mword');
pty.onExit.mock.calls[0][0]({ exitCode: 0, signal: null });
},
{ ...shellExecutionConfig, showColor: false },
);
const expected = createExpectedAnsiOutput('aredword');
expect(mockSerializeTerminalToObject).not.toHaveBeenCalled();
expect(onOutputEventMock).toHaveBeenCalledWith(
expect.objectContaining({
type: 'data',
chunk: 'aredword',
chunk: expected,
}),
);
});
it('should handle multi-line output correctly when showColor is false', async () => {
await simulateExecution(
'ls --color=auto',
(pty) => {
pty.onData.mock.calls[0][0](
'line 1\n\u001b[32mline 2\u001b[0m\nline 3',
);
pty.onExit.mock.calls[0][0]({ exitCode: 0, signal: null });
},
{ ...shellExecutionConfig, showColor: false },
);
const expected = createExpectedAnsiOutput(['line 1', 'line 2', 'line 3']);
expect(onOutputEventMock).toHaveBeenCalledWith(
expect.objectContaining({
type: 'data',
chunk: expected,
}),
);
});
@@ -541,7 +599,7 @@ describe('ShellExecutionService child_process fallback', () => {
expect(onOutputEventMock).toHaveBeenCalledWith(
expect.objectContaining({
type: 'data',
chunk: expect.stringContaining('aredword'),
chunk: 'aredword',
}),
);
});
@@ -85,17 +85,6 @@ interface ActivePty {
headlessTerminal: pkg.Terminal;
}
const getVisibleText = (terminal: pkg.Terminal): string => {
const buffer = terminal.buffer.active;
const lines: string[] = [];
for (let i = 0; i < terminal.rows; i++) {
const line = buffer.getLine(buffer.viewportY + i);
const lineContent = line ? line.translateToString(true) : '';
lines.push(lineContent);
}
return lines.join('\n').trimEnd();
};
const getFullBufferText = (terminal: pkg.Terminal): string => {
const buffer = terminal.buffer.active;
const lines: string[] = [];
@@ -379,6 +368,7 @@ export class ShellExecutionService {
cols,
rows,
});
headlessTerminal.scrollToTop();
this.activePtys.set(ptyProcess.pid, { ptyProcess, headlessTerminal });
@@ -404,14 +394,33 @@ export class ShellExecutionService {
if (!isStreamingRawContent) {
return;
}
const newOutput = shellExecutionConfig.showColor
? serializeTerminalToObject(headlessTerminal, {
defaultFg: shellExecutionConfig.defaultFg,
defaultBg: shellExecutionConfig.defaultBg,
})
: getVisibleText(headlessTerminal);
// console.log(newOutput)
let newOutput: AnsiOutput;
if (shellExecutionConfig.showColor) {
newOutput = serializeTerminalToObject(headlessTerminal, {
defaultFg: shellExecutionConfig.defaultFg,
defaultBg: shellExecutionConfig.defaultBg,
});
} else {
const buffer = headlessTerminal.buffer.active;
const lines: AnsiOutput = [];
for (let y = 0; y < headlessTerminal.rows; y++) {
const line = buffer.getLine(buffer.viewportY + y);
const lineContent = line ? line.translateToString(true) : '';
lines.push([
{
text: lineContent,
bold: false,
italic: false,
underline: false,
dim: false,
inverse: false,
fg: '',
bg: '',
},
]);
}
newOutput = lines;
}
// Using stringify for a quick deep comparison.
if (JSON.stringify(output) !== JSON.stringify(newOutput)) {
@@ -602,6 +611,9 @@ export class ShellExecutionService {
if (activePty) {
try {
activePty.headlessTerminal.scrollLines(lines);
if (activePty.headlessTerminal.buffer.active.viewportY < 0) {
activePty.headlessTerminal.scrollToTop();
}
} catch (e) {
// Ignore errors if the pty has already exited, which can happen
// due to a race condition between the exit event and this call.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-test-utils",
"version": "0.6.0-nightly",
"version": "0.6.0-preview.5",
"private": true,
"main": "src/index.ts",
"license": "Apache-2.0",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "gemini-cli-vscode-ide-companion",
"displayName": "Gemini CLI Companion",
"description": "Enable Gemini CLI with direct access to your IDE workspace.",
"version": "0.6.0-nightly",
"version": "0.6.0-preview.5",
"publisher": "google",
"icon": "assets/icon.png",
"repository": {
@@ -94,7 +94,7 @@ function collectDependencies(packageName, packageLock, dependenciesMap) {
const packageInfo = packageLock.packages[`node_modules/${packageName}`];
if (!packageInfo) {
console.warn(
`Warning: Could not find package info for ${packageName} in npm-shrinkwrap.json.`,
`Warning: Could not find package info for ${packageName} in package-lock.json.`,
);
return;
}
@@ -114,7 +114,7 @@ async function main() {
const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8');
const packageJson = JSON.parse(packageJsonContent);
const packageLockJsonPath = path.join(projectRoot, 'npm-shrinkwrap.json');
const packageLockJsonPath = path.join(projectRoot, 'package-lock.json');
const packageLockJsonContent = await fs.readFile(
packageLockJsonPath,
'utf-8',
+2 -2
View File
@@ -10,7 +10,7 @@ import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, '..');
const lockfilePath = join(root, 'npm-shrinkwrap.json');
const lockfilePath = join(root, 'package-lock.json');
function readJsonFile(filePath) {
try {
@@ -64,7 +64,7 @@ for (const [location, details] of Object.entries(packages)) {
if (invalidPackages.length > 0) {
console.error(
'\nError: The following dependencies in npm-shrinkwrap.json are missing the "resolved" or "integrity" field:',
'\nError: The following dependencies in package-lock.json are missing the "resolved" or "integrity" field:',
);
invalidPackages.forEach((pkg) => console.error(`- ${pkg}`));
process.exitCode = 1;
+1 -1
View File
@@ -75,7 +75,7 @@ if (cliPackageJson.config?.sandboxImageUri) {
writeJson(cliPackageJsonPath, cliPackageJson);
}
// 6. Run `npm install` to update npm-shrinkwrap.json.
// 6. Run `npm install` to update package-lock.json.
run('npm install');
console.log(`Successfully bumped versions to v${newVersion}.`);