chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
# By default, require reviews from the maintainers for all files.
|
||||
* @google-gemini/gemini-cli-maintainers
|
||||
|
||||
# Require reviews from the release approvers for critical files.
|
||||
# These patterns override the rule above.
|
||||
/package.json @google-gemini/gemini-cli-askmode-approvers
|
||||
/package-lock.json @google-gemini/gemini-cli-askmode-approvers
|
||||
/GEMINI.md @google-gemini/gemini-cli-askmode-approvers
|
||||
/SECURITY.md @google-gemini/gemini-cli-askmode-approvers
|
||||
/LICENSE @google-gemini/gemini-cli-askmode-approvers
|
||||
/.github/workflows/ @google-gemini/gemini-cli-askmode-approvers
|
||||
/packages/cli/package.json @google-gemini/gemini-cli-askmode-approvers
|
||||
/packages/core/package.json @google-gemini/gemini-cli-askmode-approvers
|
||||
|
||||
# Docs have a dedicated approver group in addition to maintainers
|
||||
/docs/ @google-gemini/gemini-cli-maintainers @google-gemini/gemini-cli-docs
|
||||
/README.md @google-gemini/gemini-cli-maintainers @google-gemini/gemini-cli-docs
|
||||
|
||||
# Prompt contents, tool definitions, and evals require reviews from prompt approvers
|
||||
/packages/core/src/prompts/ @google-gemini/gemini-cli-prompt-approvers
|
||||
/packages/core/src/tools/ @google-gemini/gemini-cli-prompt-approvers
|
||||
/evals/ @google-gemini/gemini-cli-prompt-approvers
|
||||
@@ -0,0 +1,57 @@
|
||||
name: 'Bug Report'
|
||||
description: 'Report a bug to help us improve Gemini CLI'
|
||||
body:
|
||||
- type: 'markdown'
|
||||
attributes:
|
||||
value: |-
|
||||
> [!IMPORTANT]
|
||||
> Thanks for taking the time to fill out this bug report!
|
||||
>
|
||||
> Please search **[existing issues](https://github.com/google-gemini/gemini-cli/issues)** to see if an issue already exists for the bug you encountered.
|
||||
|
||||
- type: 'textarea'
|
||||
id: 'problem'
|
||||
attributes:
|
||||
label: 'What happened?'
|
||||
description: 'A clear and concise description of what the bug is.'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: 'textarea'
|
||||
id: 'expected'
|
||||
attributes:
|
||||
label: 'What did you expect to happen?'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: 'textarea'
|
||||
id: 'info'
|
||||
attributes:
|
||||
label: 'Client information'
|
||||
description: 'Please paste the full text from the `/about` command run from Gemini CLI. Also include which platform (macOS, Windows, Linux). Note that this output contains your email address. Consider removing it before submitting.'
|
||||
value: |-
|
||||
<details>
|
||||
<summary>Client Information</summary>
|
||||
|
||||
Run `gemini` to enter the interactive CLI, then run the `/about` command.
|
||||
|
||||
```console
|
||||
> /about
|
||||
# paste output here
|
||||
```
|
||||
|
||||
</details>
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: 'textarea'
|
||||
id: 'login-info'
|
||||
attributes:
|
||||
label: 'Login information'
|
||||
description: 'Describe how you are logging in (e.g., Google Account, API key).'
|
||||
|
||||
- type: 'textarea'
|
||||
id: 'additional-context'
|
||||
attributes:
|
||||
label: 'Anything else we need to know?'
|
||||
description: 'Add any other context about the problem here.'
|
||||
@@ -0,0 +1,35 @@
|
||||
name: 'Feature Request'
|
||||
description: 'Suggest an idea for this project'
|
||||
labels:
|
||||
- 'status/need-triage'
|
||||
type: 'Feature'
|
||||
body:
|
||||
- type: 'markdown'
|
||||
attributes:
|
||||
value: |-
|
||||
> [!IMPORTANT]
|
||||
> Thanks for taking the time to suggest an enhancement!
|
||||
>
|
||||
> Please search **[existing issues](https://github.com/google-gemini/gemini-cli/issues)** to see if a similar feature has already been requested.
|
||||
|
||||
- type: 'textarea'
|
||||
id: 'feature'
|
||||
attributes:
|
||||
label: 'What would you like to be added?'
|
||||
description: 'A clear and concise description of the enhancement.'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: 'textarea'
|
||||
id: 'rationale'
|
||||
attributes:
|
||||
label: 'Why is this needed?'
|
||||
description: 'A clear and concise description of why this enhancement is needed.'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: 'textarea'
|
||||
id: 'additional-context'
|
||||
attributes:
|
||||
label: 'Additional context'
|
||||
description: 'Add any other context or screenshots about the feature request here.'
|
||||
@@ -0,0 +1,42 @@
|
||||
name: 'Website issue'
|
||||
description: 'Report an issue with the Gemini CLI Website and Gemini CLI Extensions Gallery'
|
||||
title: 'GeminiCLI.com Feedback: [ISSUE]'
|
||||
labels:
|
||||
- 'area/extensions'
|
||||
- 'area/documentation'
|
||||
body:
|
||||
- type: 'markdown'
|
||||
attributes:
|
||||
value: |-
|
||||
> [!IMPORTANT]
|
||||
> Thanks for taking the time to report an issue with the Gemini CLI Website
|
||||
>
|
||||
> Please search **[existing issues](https://github.com/google-gemini/gemini-cli/issues?q=is%3Aissue+is%3Aopen+label%3Aarea%2Fwebsite)** to see if a similar feature has already been requested.
|
||||
- type: 'input'
|
||||
id: 'url'
|
||||
attributes:
|
||||
label: 'URL of the page with the issue'
|
||||
description: 'Please provide the URL where the issue occurs.'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: 'textarea'
|
||||
id: 'problem'
|
||||
attributes:
|
||||
label: 'What is the problem?'
|
||||
description: 'A clear and concise description of what the bug or issue is.'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: 'textarea'
|
||||
id: 'expected'
|
||||
attributes:
|
||||
label: 'What did you expect to happen?'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: 'textarea'
|
||||
id: 'additional-context'
|
||||
attributes:
|
||||
label: 'Additional context'
|
||||
description: 'Add any other context or screenshots about the issue here.'
|
||||
@@ -0,0 +1,27 @@
|
||||
name: 'Calculate vars'
|
||||
description: 'Calculate commonly used var in our release process'
|
||||
|
||||
inputs:
|
||||
dry_run:
|
||||
description: 'Whether or not this is a dry run'
|
||||
type: 'boolean'
|
||||
|
||||
outputs:
|
||||
is_dry_run:
|
||||
description: 'Boolean flag indicating if the current run is a dry-run or a production release.'
|
||||
value: '${{ steps.set_vars.outputs.is_dry_run }}'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: 'Set vars for simplified logic'
|
||||
id: 'set_vars'
|
||||
shell: 'bash'
|
||||
env:
|
||||
DRY_RUN_INPUT: '${{ inputs.dry_run }}'
|
||||
run: |-
|
||||
is_dry_run="true"
|
||||
if [[ "${DRY_RUN_INPUT}" == "" || "${DRY_RUN_INPUT}" == "false" ]]; then
|
||||
is_dry_run="false"
|
||||
fi
|
||||
echo "is_dry_run=${is_dry_run}" >> "${GITHUB_OUTPUT}"
|
||||
@@ -0,0 +1,55 @@
|
||||
name: 'Create Pull Request'
|
||||
description: 'Creates a pull request.'
|
||||
|
||||
inputs:
|
||||
branch-name:
|
||||
description: 'The name of the branch to create the PR from.'
|
||||
required: true
|
||||
pr-title:
|
||||
description: 'The title of the pull request.'
|
||||
required: true
|
||||
pr-body:
|
||||
description: 'The body of the pull request.'
|
||||
required: true
|
||||
base-branch:
|
||||
description: 'The branch to merge into.'
|
||||
required: true
|
||||
default: 'main'
|
||||
github-token:
|
||||
description: 'The GitHub token to use for creating the pull request.'
|
||||
required: true
|
||||
dry-run:
|
||||
description: 'Whether to run in dry-run mode.'
|
||||
required: false
|
||||
default: 'false'
|
||||
working-directory:
|
||||
description: 'The working directory to run the commands in.'
|
||||
required: false
|
||||
default: '.'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: 'Creates a Pull Request'
|
||||
if: "inputs.dry-run != 'true'"
|
||||
env:
|
||||
GH_TOKEN: '${{ inputs.github-token }}'
|
||||
INPUTS_BRANCH_NAME: '${{ inputs.branch-name }}'
|
||||
INPUTS_PR_TITLE: '${{ inputs.pr-title }}'
|
||||
INPUTS_PR_BODY: '${{ inputs.pr-body }}'
|
||||
INPUTS_BASE_BRANCH: '${{ inputs.base-branch }}'
|
||||
shell: 'bash'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: |
|
||||
set -e
|
||||
if ! git ls-remote --exit-code --heads origin "${INPUTS_BRANCH_NAME}"; then
|
||||
echo "::error::Branch '${INPUTS_BRANCH_NAME}' does not exist on the remote repository."
|
||||
exit 1
|
||||
fi
|
||||
PR_URL=$(gh pr create \
|
||||
--title "${INPUTS_PR_TITLE}" \
|
||||
--body "${INPUTS_PR_BODY}" \
|
||||
--base "${INPUTS_BASE_BRANCH}" \
|
||||
--head "${INPUTS_BRANCH_NAME}" \
|
||||
--fill)
|
||||
gh pr merge "$PR_URL" --auto
|
||||
@@ -0,0 +1,23 @@
|
||||
name: 'Download Mac Binaries'
|
||||
description: 'Downloads the unsigned macOS binaries (x64 and arm64)'
|
||||
inputs:
|
||||
path:
|
||||
description: 'The base path to download the binaries to'
|
||||
required: true
|
||||
default: 'dist'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: 'Download macOS arm64 binary'
|
||||
uses: 'actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806' # ratchet:actions/download-artifact@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: 'gemini-darwin-arm64-unsigned'
|
||||
path: '${{ inputs.path }}/darwin-arm64'
|
||||
|
||||
- name: 'Download macOS x64 binary'
|
||||
uses: 'actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806' # ratchet:actions/download-artifact@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: 'gemini-darwin-x64-unsigned'
|
||||
path: '${{ inputs.path }}/darwin-x64'
|
||||
@@ -0,0 +1,51 @@
|
||||
name: 'NPM Auth Token'
|
||||
description: 'Generates an NPM auth token for publishing a specific package'
|
||||
|
||||
inputs:
|
||||
package-name:
|
||||
description: 'The name of the package to publish'
|
||||
required: true
|
||||
github-token:
|
||||
description: 'the github token'
|
||||
required: true
|
||||
wombat-token-core:
|
||||
description: 'The npm token for the cli-core package.'
|
||||
required: true
|
||||
wombat-token-cli:
|
||||
description: 'The npm token for the cli package.'
|
||||
required: true
|
||||
wombat-token-a2a-server:
|
||||
description: 'The npm token for the a2a package.'
|
||||
required: true
|
||||
|
||||
outputs:
|
||||
auth-token:
|
||||
description: 'The generated NPM auth token'
|
||||
value: '${{ steps.npm_auth_token.outputs.auth-token }}'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: 'Generate NPM Auth Token'
|
||||
id: 'npm_auth_token'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
AUTH_TOKEN="${INPUTS_GITHUB_TOKEN}"
|
||||
PACKAGE_NAME="${INPUTS_PACKAGE_NAME}"
|
||||
PRIVATE_REPO="@google-gemini/"
|
||||
if [[ "$PACKAGE_NAME" == "$PRIVATE_REPO"* ]]; then
|
||||
AUTH_TOKEN="${INPUTS_GITHUB_TOKEN}"
|
||||
elif [[ "$PACKAGE_NAME" == "@google/gemini-cli" ]]; then
|
||||
AUTH_TOKEN="${INPUTS_WOMBAT_TOKEN_CLI}"
|
||||
elif [[ "$PACKAGE_NAME" == "@google/gemini-cli-core" ]]; then
|
||||
AUTH_TOKEN="${INPUTS_WOMBAT_TOKEN_CORE}"
|
||||
elif [[ "$PACKAGE_NAME" == "@google/gemini-cli-a2a-server" ]]; then
|
||||
AUTH_TOKEN="${INPUTS_WOMBAT_TOKEN_A2A_SERVER}"
|
||||
fi
|
||||
echo "auth-token=$AUTH_TOKEN" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
INPUTS_GITHUB_TOKEN: '${{ inputs.github-token }}'
|
||||
INPUTS_PACKAGE_NAME: '${{ inputs.package-name }}'
|
||||
INPUTS_WOMBAT_TOKEN_CLI: '${{ inputs.wombat-token-cli }}'
|
||||
INPUTS_WOMBAT_TOKEN_CORE: '${{ inputs.wombat-token-core }}'
|
||||
INPUTS_WOMBAT_TOKEN_A2A_SERVER: '${{ inputs.wombat-token-a2a-server }}'
|
||||
@@ -0,0 +1,114 @@
|
||||
name: 'Post Coverage Comment Action'
|
||||
description: 'Prepares and posts a code coverage comment to a PR.'
|
||||
|
||||
inputs:
|
||||
cli_json_file:
|
||||
description: 'Path to CLI coverage-summary.json'
|
||||
required: true
|
||||
core_json_file:
|
||||
description: 'Path to Core coverage-summary.json'
|
||||
required: true
|
||||
cli_full_text_summary_file:
|
||||
description: 'Path to CLI full-text-summary.txt'
|
||||
required: true
|
||||
core_full_text_summary_file:
|
||||
description: 'Path to Core full-text-summary.txt'
|
||||
required: true
|
||||
node_version:
|
||||
description: 'Node.js version for context in messages'
|
||||
required: true
|
||||
os:
|
||||
description: 'The os for context in messages'
|
||||
required: true
|
||||
github_token:
|
||||
description: 'GitHub token for posting comments'
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: 'Prepare Coverage Comment'
|
||||
id: 'prep_coverage_comment'
|
||||
shell: 'bash'
|
||||
env:
|
||||
CLI_JSON_FILE: '${{ inputs.cli_json_file }}'
|
||||
CORE_JSON_FILE: '${{ inputs.core_json_file }}'
|
||||
CLI_FULL_TEXT_SUMMARY_FILE: '${{ inputs.cli_full_text_summary_file }}'
|
||||
CORE_FULL_TEXT_SUMMARY_FILE: '${{ inputs.core_full_text_summary_file }}'
|
||||
COMMENT_FILE: 'coverage-comment.md'
|
||||
NODE_VERSION: '${{ inputs.node_version }}'
|
||||
OS: '${{ inputs.os }}'
|
||||
run: |-
|
||||
# Extract percentages using jq for the main table
|
||||
if [ -f "${CLI_JSON_FILE}" ]; then
|
||||
cli_lines_pct="$(jq -r '.total.lines.pct' "${CLI_JSON_FILE}")"
|
||||
cli_statements_pct="$(jq -r '.total.statements.pct' "${CLI_JSON_FILE}")"
|
||||
cli_functions_pct="$(jq -r '.total.functions.pct' "${CLI_JSON_FILE}")"
|
||||
cli_branches_pct="$(jq -r '.total.branches.pct' "${CLI_JSON_FILE}")"
|
||||
else
|
||||
cli_lines_pct="N/A"
|
||||
cli_statements_pct="N/A"
|
||||
cli_functions_pct="N/A"
|
||||
cli_branches_pct="N/A"
|
||||
echo "CLI coverage-summary.json not found at: ${CLI_JSON_FILE}" >&2 # Error to stderr
|
||||
fi
|
||||
|
||||
if [ -f "${CORE_JSON_FILE}" ]; then
|
||||
core_lines_pct="$(jq -r '.total.lines.pct' "${CORE_JSON_FILE}")"
|
||||
core_statements_pct="$(jq -r '.total.statements.pct' "${CORE_JSON_FILE}")"
|
||||
core_functions_pct="$(jq -r '.total.functions.pct' "${CORE_JSON_FILE}")"
|
||||
core_branches_pct="$(jq -r '.total.branches.pct' "${CORE_JSON_FILE}")"
|
||||
else
|
||||
core_lines_pct="N/A"
|
||||
core_statements_pct="N/A"
|
||||
core_functions_pct="N/A"
|
||||
core_branches_pct="N/A"
|
||||
echo "Core coverage-summary.json not found at: ${CORE_JSON_FILE}" >&2 # Error to stderr
|
||||
fi
|
||||
|
||||
echo "## Code Coverage Summary" > "${COMMENT_FILE}"
|
||||
echo "" >> "${COMMENT_FILE}"
|
||||
echo "| Package | Lines | Statements | Functions | Branches |" >> "${COMMENT_FILE}"
|
||||
echo "|---|---|---|---|---|" >> "${COMMENT_FILE}"
|
||||
echo "| CLI | ${cli_lines_pct}% | ${cli_statements_pct}% | ${cli_functions_pct}% | ${cli_branches_pct}% |" >> "${COMMENT_FILE}"
|
||||
echo "| Core | ${core_lines_pct}% | ${core_statements_pct}% | ${core_functions_pct}% | ${core_branches_pct}% |" >> "${COMMENT_FILE}"
|
||||
echo "" >> "${COMMENT_FILE}"
|
||||
|
||||
# CLI Package - Collapsible Section (with full text summary from file)
|
||||
echo "<details>" >> "${COMMENT_FILE}"
|
||||
echo "<summary>CLI Package - Full Text Report</summary>" >> "${COMMENT_FILE}"
|
||||
echo "" >> "${COMMENT_FILE}"
|
||||
echo '```text' >> "${COMMENT_FILE}"
|
||||
if [ -f "${CLI_FULL_TEXT_SUMMARY_FILE}" ]; then
|
||||
cat "${CLI_FULL_TEXT_SUMMARY_FILE}" >> "${COMMENT_FILE}"
|
||||
else
|
||||
echo "CLI full-text-summary.txt not found at: ${CLI_FULL_TEXT_SUMMARY_FILE}" >> "${COMMENT_FILE}"
|
||||
fi
|
||||
echo '```' >> "${COMMENT_FILE}"
|
||||
echo "</details>" >> "${COMMENT_FILE}"
|
||||
echo "" >> "${COMMENT_FILE}"
|
||||
|
||||
# Core Package - Collapsible Section (with full text summary from file)
|
||||
echo "<details>" >> "${COMMENT_FILE}"
|
||||
echo "<summary>Core Package - Full Text Report</summary>" >> "${COMMENT_FILE}"
|
||||
echo "" >> "${COMMENT_FILE}"
|
||||
echo '```text' >> "${COMMENT_FILE}"
|
||||
if [ -f "${CORE_FULL_TEXT_SUMMARY_FILE}" ]; then
|
||||
cat "${CORE_FULL_TEXT_SUMMARY_FILE}" >> "${COMMENT_FILE}"
|
||||
else
|
||||
echo "Core full-text-summary.txt not found at: ${CORE_FULL_TEXT_SUMMARY_FILE}" >> "${COMMENT_FILE}"
|
||||
fi
|
||||
echo '```' >> "${COMMENT_FILE}"
|
||||
echo "</details>" >> "${COMMENT_FILE}"
|
||||
echo "" >> "${COMMENT_FILE}"
|
||||
|
||||
echo "_For detailed HTML reports, please see the 'coverage-reports-${NODE_VERSION}-${OS}' artifact from the main CI run._" >> "${COMMENT_FILE}"
|
||||
|
||||
- name: 'Post Coverage Comment'
|
||||
uses: 'thollander/actions-comment-pull-request@65f9e5c9a1f2cd378bd74b2e057c9736982a8e74' # ratchet:thollander/actions-comment-pull-request@v3
|
||||
if: |-
|
||||
${{ always() }}
|
||||
with:
|
||||
file-path: 'coverage-comment.md' # Use the generated file directly
|
||||
comment-tag: 'code-coverage-summary'
|
||||
github-token: '${{ inputs.github_token }}'
|
||||
@@ -0,0 +1,359 @@
|
||||
name: 'Publish Release'
|
||||
description: 'Builds, prepares, and publishes the gemini-cli packages to npm and creates a GitHub release.'
|
||||
|
||||
inputs:
|
||||
release-version:
|
||||
description: 'The version to release (e.g., 0.1.11).'
|
||||
required: true
|
||||
npm-tag:
|
||||
description: 'The npm tag to publish with (e.g., latest, preview, nightly).'
|
||||
required: true
|
||||
wombat-token-core:
|
||||
description: 'The npm token for the cli-core package.'
|
||||
required: true
|
||||
wombat-token-cli:
|
||||
description: 'The npm token for the cli package.'
|
||||
required: true
|
||||
wombat-token-a2a-server:
|
||||
description: 'The npm token for the a2a package.'
|
||||
required: true
|
||||
github-token:
|
||||
description: 'The GitHub token for creating the release.'
|
||||
required: true
|
||||
github-release-token:
|
||||
description: 'The GitHub token used specifically for creating the GitHub release (to trigger other workflows).'
|
||||
required: false
|
||||
dry-run:
|
||||
description: 'Whether to run in dry-run mode.'
|
||||
type: 'string'
|
||||
required: true
|
||||
release-tag:
|
||||
description: 'The release tag for the release (e.g., v0.1.11).'
|
||||
required: true
|
||||
previous-tag:
|
||||
description: 'The previous tag to use for generating release notes.'
|
||||
required: true
|
||||
skip-github-release:
|
||||
description: 'Whether to skip creating a GitHub release.'
|
||||
type: 'boolean'
|
||||
required: false
|
||||
default: false
|
||||
working-directory:
|
||||
description: 'The working directory to run the steps in.'
|
||||
required: false
|
||||
default: '.'
|
||||
force-skip-tests:
|
||||
description: 'Skip tests and validation'
|
||||
required: false
|
||||
default: false
|
||||
skip-branch-cleanup:
|
||||
description: 'Whether to skip cleaning up the release branch.'
|
||||
type: 'boolean'
|
||||
required: false
|
||||
default: false
|
||||
gemini_api_key:
|
||||
description: 'The API key for running integration tests.'
|
||||
required: true
|
||||
npm-registry-publish-url:
|
||||
description: 'npm registry publish url'
|
||||
required: true
|
||||
npm-registry-url:
|
||||
description: 'npm registry url'
|
||||
required: true
|
||||
npm-registry-scope:
|
||||
description: 'npm registry scope'
|
||||
required: true
|
||||
cli-package-name:
|
||||
description: 'The name of the cli package.'
|
||||
required: true
|
||||
core-package-name:
|
||||
description: 'The name of the core package.'
|
||||
required: true
|
||||
a2a-package-name:
|
||||
description: 'The name of the a2a package.'
|
||||
required: true
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: '👤 Configure Git User'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
git config user.name "gemini-cli-robot"
|
||||
git config user.email "gemini-cli-robot@google.com"
|
||||
|
||||
- name: '🌿 Create and switch to a release branch'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
id: 'release_branch'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
BRANCH_NAME="release/${INPUTS_RELEASE_TAG}"
|
||||
git switch -c "${BRANCH_NAME}"
|
||||
echo "BRANCH_NAME=${BRANCH_NAME}" >> "${GITHUB_OUTPUT}"
|
||||
env:
|
||||
INPUTS_RELEASE_TAG: '${{ inputs.release-tag }}'
|
||||
|
||||
- name: '⬆️ Update package versions'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm run release:version "${INPUTS_RELEASE_VERSION}"
|
||||
env:
|
||||
INPUTS_RELEASE_VERSION: '${{ inputs.release-version }}'
|
||||
|
||||
- name: '💾 Commit and Conditionally Push package versions'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
shell: 'bash'
|
||||
env:
|
||||
BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
|
||||
DRY_RUN: '${{ inputs.dry-run }}'
|
||||
RELEASE_TAG: '${{ inputs.release-tag }}'
|
||||
GIT_PUSH_TOKEN: '${{ inputs.github-release-token || inputs.github-token }}'
|
||||
run: |-
|
||||
set -e
|
||||
git add package.json package-lock.json packages/*/package.json
|
||||
git commit -m "chore(release): ${RELEASE_TAG}"
|
||||
if [[ "${DRY_RUN}" == "false" ]]; then
|
||||
echo "Pushing release branch to remote..."
|
||||
git push "https://x-access-token:${GIT_PUSH_TOKEN}@github.com/${{ github.repository }}.git" "HEAD:${BRANCH_NAME}" --follow-tags
|
||||
else
|
||||
echo "Dry run enabled. Skipping push."
|
||||
fi
|
||||
|
||||
- name: '🛠️ Build and Prepare Packages'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm run build:packages
|
||||
npm run prepare:package
|
||||
|
||||
- name: '🎁 Bundle'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm run bundle
|
||||
|
||||
# TODO: Refactor this github specific publishing script to be generalized based upon inputs.
|
||||
- name: '📦 Prepare for GitHub release'
|
||||
if: "inputs.npm-registry-url == 'https://npm.pkg.github.com/'"
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
node ${{ github.workspace }}/scripts/prepare-github-release.js
|
||||
|
||||
- name: 'Configure npm for publishing to npm'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
|
||||
with:
|
||||
node-version-file: '${{ inputs.working-directory }}/.nvmrc'
|
||||
registry-url: '${{inputs.npm-registry-publish-url}}'
|
||||
scope: '${{inputs.npm-registry-scope}}'
|
||||
|
||||
- name: 'Get core Token'
|
||||
uses: './.github/actions/npm-auth-token'
|
||||
id: 'core-token'
|
||||
with:
|
||||
package-name: '${{ inputs.core-package-name }}'
|
||||
github-token: '${{ inputs.github-token }}'
|
||||
wombat-token-core: '${{ inputs.wombat-token-core }}'
|
||||
wombat-token-cli: '${{ inputs.wombat-token-cli }}'
|
||||
wombat-token-a2a-server: '${{ inputs.wombat-token-a2a-server }}'
|
||||
|
||||
- name: '📦 Publish CORE to NPM'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ steps.core-token.outputs.auth-token }}'
|
||||
INPUTS_DRY_RUN: '${{ inputs.dry-run }}'
|
||||
INPUTS_CORE_PACKAGE_NAME: '${{ inputs.core-package-name }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm publish \
|
||||
--ignore-scripts \
|
||||
--dry-run="${INPUTS_DRY_RUN}" \
|
||||
--workspace="${INPUTS_CORE_PACKAGE_NAME}" \
|
||||
--tag staging-tmp
|
||||
if [[ "${INPUTS_DRY_RUN}" == "false" ]]; then
|
||||
npm dist-tag rm ${INPUTS_CORE_PACKAGE_NAME} staging-tmp
|
||||
fi
|
||||
|
||||
- name: '🔗 Install latest core package'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
if: "${{ inputs.dry-run != 'true' }}"
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm install "${INPUTS_CORE_PACKAGE_NAME}@${INPUTS_RELEASE_VERSION}" \
|
||||
--workspace="${INPUTS_CLI_PACKAGE_NAME}" \
|
||||
--workspace="${INPUTS_A2A_PACKAGE_NAME}" \
|
||||
--save-exact
|
||||
env:
|
||||
INPUTS_CORE_PACKAGE_NAME: '${{ inputs.core-package-name }}'
|
||||
INPUTS_RELEASE_VERSION: '${{ inputs.release-version }}'
|
||||
INPUTS_CLI_PACKAGE_NAME: '${{ inputs.cli-package-name }}'
|
||||
INPUTS_A2A_PACKAGE_NAME: '${{ inputs.a2a-package-name }}'
|
||||
|
||||
- name: '📦 Prepare bundled CLI for npm release'
|
||||
if: "inputs.npm-registry-url != 'https://npm.pkg.github.com/'"
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
node ${{ github.workspace }}/scripts/prepare-npm-release.js
|
||||
|
||||
- name: '📦 Pack CLI for verification'
|
||||
if: "inputs.dry-run != 'true' && inputs.force-skip-tests != 'true'"
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm pack --workspace="${INPUTS_CLI_PACKAGE_NAME}"
|
||||
# We restore the package.json so that `npm ci` in verify-release doesn't fail due to deleted dependencies
|
||||
git checkout packages/cli/package.json
|
||||
env:
|
||||
INPUTS_CLI_PACKAGE_NAME: '${{ inputs.cli-package-name }}'
|
||||
|
||||
- name: '🔬 Verify NPM release by version'
|
||||
uses: './.github/actions/verify-release'
|
||||
if: "${{ inputs.dry-run != 'true' && inputs.force-skip-tests != 'true' }}"
|
||||
with:
|
||||
npm-package: './google-gemini-cli-${{ inputs.release-version }}.tgz'
|
||||
expected-version: '${{ inputs.release-version }}'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
gemini_api_key: '${{ inputs.gemini_api_key }}'
|
||||
github-token: '${{ inputs.github-token }}'
|
||||
npm-registry-url: '${{ inputs.npm-registry-url }}'
|
||||
npm-registry-scope: '${{ inputs.npm-registry-scope }}'
|
||||
|
||||
- name: 'Get CLI Token'
|
||||
uses: './.github/actions/npm-auth-token'
|
||||
id: 'cli-token'
|
||||
with:
|
||||
package-name: '${{ inputs.cli-package-name }}'
|
||||
github-token: '${{ inputs.github-token }}'
|
||||
wombat-token-core: '${{ inputs.wombat-token-core }}'
|
||||
wombat-token-cli: '${{ inputs.wombat-token-cli }}'
|
||||
wombat-token-a2a-server: '${{ inputs.wombat-token-a2a-server }}'
|
||||
|
||||
- name: '📦 Publish CLI'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ steps.cli-token.outputs.auth-token }}'
|
||||
INPUTS_DRY_RUN: '${{ inputs.dry-run }}'
|
||||
INPUTS_CLI_PACKAGE_NAME: '${{ inputs.cli-package-name }}'
|
||||
INPUTS_RELEASE_VERSION: '${{ inputs.release-version }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
if [ -f "google-gemini-cli-${INPUTS_RELEASE_VERSION}.tgz" ]; then
|
||||
PUBLISH_TARGET="google-gemini-cli-${INPUTS_RELEASE_VERSION}.tgz"
|
||||
else
|
||||
PUBLISH_TARGET="--workspace=${INPUTS_CLI_PACKAGE_NAME}"
|
||||
fi
|
||||
|
||||
npm publish \
|
||||
--ignore-scripts \
|
||||
--dry-run="${INPUTS_DRY_RUN}" \
|
||||
${PUBLISH_TARGET} \
|
||||
--tag staging-tmp
|
||||
if [[ "${INPUTS_DRY_RUN}" == "false" ]]; then
|
||||
npm dist-tag rm ${INPUTS_CLI_PACKAGE_NAME} staging-tmp
|
||||
fi
|
||||
|
||||
- name: 'Get a2a-server Token'
|
||||
uses: './.github/actions/npm-auth-token'
|
||||
id: 'a2a-token'
|
||||
with:
|
||||
package-name: '${{ inputs.a2a-package-name }}'
|
||||
github-token: '${{ inputs.github-token }}'
|
||||
wombat-token-core: '${{ inputs.wombat-token-core }}'
|
||||
wombat-token-cli: '${{ inputs.wombat-token-cli }}'
|
||||
wombat-token-a2a-server: '${{ inputs.wombat-token-a2a-server }}'
|
||||
|
||||
- name: '📦 Publish a2a'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ steps.a2a-token.outputs.auth-token }}'
|
||||
INPUTS_DRY_RUN: '${{ inputs.dry-run }}'
|
||||
INPUTS_A2A_PACKAGE_NAME: '${{ inputs.a2a-package-name }}'
|
||||
shell: 'bash'
|
||||
# Tag staging for initial release
|
||||
run: |
|
||||
npm publish \
|
||||
--ignore-scripts \
|
||||
--dry-run="${INPUTS_DRY_RUN}" \
|
||||
--workspace="${INPUTS_A2A_PACKAGE_NAME}" \
|
||||
--tag staging-tmp
|
||||
if [[ "${INPUTS_DRY_RUN}" == "false" ]]; then
|
||||
npm dist-tag rm ${INPUTS_A2A_PACKAGE_NAME} staging-tmp
|
||||
fi
|
||||
|
||||
- name: '🏷️ Tag release'
|
||||
uses: './.github/actions/tag-npm-release'
|
||||
with:
|
||||
channel: '${{ inputs.npm-tag }}'
|
||||
version: '${{ inputs.release-version }}'
|
||||
dry-run: '${{ inputs.dry-run }}'
|
||||
github-token: '${{ inputs.github-token }}'
|
||||
wombat-token-core: '${{ inputs.wombat-token-core }}'
|
||||
wombat-token-cli: '${{ inputs.wombat-token-cli }}'
|
||||
wombat-token-a2a-server: '${{ inputs.wombat-token-a2a-server }}'
|
||||
cli-package-name: '${{ inputs.cli-package-name }}'
|
||||
core-package-name: '${{ inputs.core-package-name }}'
|
||||
a2a-package-name: '${{ inputs.a2a-package-name }}'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
|
||||
- name: '🎉 Create GitHub Release'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
if: "${{ inputs.dry-run != 'true' && inputs.skip-github-release != 'true' && inputs.npm-tag != 'dev' && inputs.npm-registry-url != 'https://npm.pkg.github.com/' }}"
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ inputs.github-release-token || inputs.github-token }}'
|
||||
INPUTS_RELEASE_TAG: '${{ inputs.release-tag }}'
|
||||
STEPS_RELEASE_BRANCH_OUTPUTS_BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
|
||||
INPUTS_PREVIOUS_TAG: '${{ inputs.previous-tag }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
rm -f gemini-cli-bundle.zip
|
||||
(cd bundle && chmod +x gemini.js && zip -r ../gemini-cli-bundle.zip .)
|
||||
|
||||
echo "Testing the generated bundle archive..."
|
||||
rm -rf test-bundle
|
||||
mkdir -p test-bundle
|
||||
unzip -q gemini-cli-bundle.zip -d test-bundle
|
||||
|
||||
# Verify it runs and outputs a version
|
||||
BUNDLE_VERSION=$(node test-bundle/gemini.js --version | xargs)
|
||||
echo "Bundle version output: ${BUNDLE_VERSION}"
|
||||
if [[ -z "${BUNDLE_VERSION}" ]]; then
|
||||
echo "Error: Bundle failed to execute or return version."
|
||||
exit 1
|
||||
fi
|
||||
rm -rf test-bundle
|
||||
|
||||
RELEASE_ASSETS=("gemini-cli-bundle.zip")
|
||||
|
||||
# Check for and prepare macOS binaries if they exist
|
||||
for arch in arm64 x64; do
|
||||
BINARY_PATH="dist/darwin-${arch}/gemini"
|
||||
if [[ -f "$BINARY_PATH" ]]; then
|
||||
chmod +x "$BINARY_PATH"
|
||||
ZIP_NAME="gemini-darwin-${arch}-unsigned.zip"
|
||||
zip -j "$ZIP_NAME" "$BINARY_PATH"
|
||||
RELEASE_ASSETS+=("$ZIP_NAME")
|
||||
fi
|
||||
done
|
||||
|
||||
gh release create "${INPUTS_RELEASE_TAG}" \
|
||||
"${RELEASE_ASSETS[@]}" \
|
||||
--target "${STEPS_RELEASE_BRANCH_OUTPUTS_BRANCH_NAME}" \
|
||||
--title "Release ${INPUTS_RELEASE_TAG}" \
|
||||
--notes-start-tag "${INPUTS_PREVIOUS_TAG}" \
|
||||
--generate-notes \
|
||||
${{ inputs.npm-tag != 'latest' && '--prerelease' || '' }}
|
||||
|
||||
- name: '🧹 Clean up release branch'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
if: "${{ inputs.dry-run != 'true' && inputs.skip-branch-cleanup != 'true' }}"
|
||||
continue-on-error: true
|
||||
shell: 'bash'
|
||||
run: |
|
||||
echo "Cleaning up release branch ${STEPS_RELEASE_BRANCH_OUTPUTS_BRANCH_NAME}..."
|
||||
git push "https://x-access-token:${GIT_PUSH_TOKEN}@github.com/${{ github.repository }}.git" --delete "${STEPS_RELEASE_BRANCH_OUTPUTS_BRANCH_NAME}"
|
||||
|
||||
env:
|
||||
GIT_PUSH_TOKEN: '${{ inputs.github-release-token || inputs.github-token }}'
|
||||
STEPS_RELEASE_BRANCH_OUTPUTS_BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
|
||||
@@ -0,0 +1,75 @@
|
||||
name: 'Push to docker'
|
||||
description: 'Builds packages and pushes a docker image to GHCR'
|
||||
|
||||
inputs:
|
||||
github-actor:
|
||||
description: 'Github actor'
|
||||
required: true
|
||||
github-secret:
|
||||
description: 'Github secret'
|
||||
required: true
|
||||
ref-name:
|
||||
description: 'Github ref name'
|
||||
required: true
|
||||
github-sha:
|
||||
description: 'Github Commit SHA Hash'
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v4
|
||||
with:
|
||||
ref: '${{ inputs.github-sha }}'
|
||||
fetch-depth: 0
|
||||
- name: 'Install Dependencies'
|
||||
shell: 'bash'
|
||||
run: 'npm install'
|
||||
- name: 'Set up Docker Buildx'
|
||||
uses: 'docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435' # ratchet:docker/setup-buildx-action@v3
|
||||
- name: 'build'
|
||||
shell: 'bash'
|
||||
run: 'npm run build'
|
||||
- name: 'pack @google/gemini-cli'
|
||||
shell: 'bash'
|
||||
run: 'npm pack -w @google/gemini-cli --pack-destination ./packages/cli/dist'
|
||||
- name: 'pack @google/gemini-cli-core'
|
||||
shell: 'bash'
|
||||
run: 'npm pack -w @google/gemini-cli-core --pack-destination ./packages/core/dist'
|
||||
- name: 'Log in to GitHub Container Registry'
|
||||
uses: 'docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1' # ratchet:docker/login-action@v3
|
||||
with:
|
||||
registry: 'ghcr.io'
|
||||
username: '${{ inputs.github-actor }}'
|
||||
password: '${{ inputs.github-secret }}'
|
||||
- name: 'Get branch name'
|
||||
id: 'branch_name'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
REF_NAME="${INPUTS_REF_NAME}"
|
||||
echo "name=${REF_NAME%/merge}" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
INPUTS_REF_NAME: '${{ inputs.ref-name }}'
|
||||
- name: 'Build and Push the Docker Image'
|
||||
uses: 'docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83' # ratchet:docker/build-push-action@v6
|
||||
with:
|
||||
context: '.'
|
||||
file: './Dockerfile'
|
||||
push: true
|
||||
provenance: false # avoid pushing 3 images to Aritfact Registry
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository }}/cli:${{ steps.branch_name.outputs.name }}
|
||||
ghcr.io/${{ github.repository }}/cli:${{ inputs.github-sha }}
|
||||
- name: 'Create issue on failure'
|
||||
if: |-
|
||||
${{ failure() }}
|
||||
shell: 'bash'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ inputs.github-secret }}'
|
||||
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
run: |-
|
||||
gh issue create \
|
||||
--title "Docker build failed" \
|
||||
--body "The docker build failed. See the full run for details: ${DETAILS_URL}" \
|
||||
--label "release-failure"
|
||||
@@ -0,0 +1,114 @@
|
||||
name: 'Build and push sandbox docker'
|
||||
description: 'Pushes sandbox docker image to container registry'
|
||||
|
||||
inputs:
|
||||
github-actor:
|
||||
description: 'Github actor'
|
||||
required: true
|
||||
github-secret:
|
||||
description: 'Github secret'
|
||||
required: true
|
||||
dockerhub-username:
|
||||
description: 'Dockerhub username'
|
||||
required: true
|
||||
dockerhub-token:
|
||||
description: 'Dockerhub PAT w/ R+W'
|
||||
required: true
|
||||
github-sha:
|
||||
description: 'Github Commit SHA Hash'
|
||||
required: true
|
||||
github-ref-name:
|
||||
description: 'Github ref name'
|
||||
required: true
|
||||
dry-run:
|
||||
description: 'Whether this is a dry run.'
|
||||
required: true
|
||||
type: 'boolean'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4
|
||||
with:
|
||||
ref: '${{ inputs.github-sha }}'
|
||||
fetch-depth: 0
|
||||
- name: 'Install Dependencies'
|
||||
shell: 'bash'
|
||||
run: 'npm install'
|
||||
- name: 'npm build'
|
||||
shell: 'bash'
|
||||
run: 'npm run build'
|
||||
- name: 'Set up QEMU'
|
||||
uses: 'docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130' # ratchet:docker/setup-qemu-action@v3
|
||||
- name: 'Set up Docker Buildx'
|
||||
uses: 'docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f' # ratchet:docker/setup-buildx-action@v3
|
||||
- name: 'Log in to GitHub Container Registry'
|
||||
uses: 'docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9' # ratchet:docker/login-action@v3
|
||||
with:
|
||||
registry: 'docker.io'
|
||||
username: '${{ inputs.dockerhub-username }}'
|
||||
password: '${{ inputs.dockerhub-token }}'
|
||||
- name: 'determine image tag'
|
||||
id: 'image_tag'
|
||||
shell: 'bash'
|
||||
run: |-
|
||||
SHELL_TAG_NAME="${INPUTS_GITHUB_REF_NAME}"
|
||||
FINAL_TAG="${INPUTS_GITHUB_SHA}"
|
||||
if [[ "$SHELL_TAG_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then
|
||||
echo "Release detected."
|
||||
FINAL_TAG="${SHELL_TAG_NAME#v}"
|
||||
else
|
||||
echo "Development release detected. Using commit SHA as tag."
|
||||
fi
|
||||
echo "Determined image tag: $FINAL_TAG"
|
||||
echo "FINAL_TAG=$FINAL_TAG" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
INPUTS_GITHUB_REF_NAME: '${{ inputs.github-ref-name }}'
|
||||
INPUTS_GITHUB_SHA: '${{ inputs.github-sha }}'
|
||||
# We build amd64 just so we can verify it.
|
||||
# We build and push both amd64 and arm64 in the publish step.
|
||||
- name: 'build'
|
||||
id: 'docker_build'
|
||||
shell: 'bash'
|
||||
env:
|
||||
GEMINI_SANDBOX_IMAGE_TAG: '${{ steps.image_tag.outputs.FINAL_TAG }}'
|
||||
GEMINI_SANDBOX: 'docker'
|
||||
BUILD_SANDBOX_FLAGS: '--platform linux/amd64 --load'
|
||||
STEPS_IMAGE_TAG_OUTPUTS_FINAL_TAG: '${{ steps.image_tag.outputs.FINAL_TAG }}'
|
||||
run: |-
|
||||
npm run build:sandbox -- \
|
||||
--image "google/gemini-cli-sandbox:${STEPS_IMAGE_TAG_OUTPUTS_FINAL_TAG}" \
|
||||
--output-file final_image_uri.txt
|
||||
echo "uri=$(cat final_image_uri.txt)" >> $GITHUB_OUTPUT
|
||||
- name: 'verify'
|
||||
shell: 'bash'
|
||||
run: |-
|
||||
docker run --rm --entrypoint sh "${{ steps.docker_build.outputs.uri }}" -lc '
|
||||
set -e
|
||||
node -e "const fs=require(\"node:fs\"); JSON.parse(fs.readFileSync(\"/usr/local/share/npm-global/lib/node_modules/@google/gemini-cli/package.json\",\"utf8\")); JSON.parse(fs.readFileSync(\"/usr/local/share/npm-global/lib/node_modules/@google/gemini-cli-core/package.json\",\"utf8\"));"
|
||||
/usr/local/share/npm-global/bin/gemini --version >/dev/null
|
||||
'
|
||||
- name: 'publish'
|
||||
shell: 'bash'
|
||||
if: "${{ inputs.dry-run != 'true' }}"
|
||||
env:
|
||||
GEMINI_SANDBOX_IMAGE_TAG: '${{ steps.image_tag.outputs.FINAL_TAG }}'
|
||||
GEMINI_SANDBOX: 'docker'
|
||||
BUILD_SANDBOX_FLAGS: '--platform linux/amd64,linux/arm64 --push'
|
||||
STEPS_IMAGE_TAG_OUTPUTS_FINAL_TAG: '${{ steps.image_tag.outputs.FINAL_TAG }}'
|
||||
run: |-
|
||||
npm run build:sandbox -- \
|
||||
--image "google/gemini-cli-sandbox:${STEPS_IMAGE_TAG_OUTPUTS_FINAL_TAG}"
|
||||
- name: 'Create issue on failure'
|
||||
if: |-
|
||||
${{ failure() }}
|
||||
shell: 'bash'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ inputs.github-secret }}'
|
||||
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
run: |-
|
||||
gh issue create \
|
||||
--title "Docker build failed" \
|
||||
--body "The docker build failed. See the full run for details: ${DETAILS_URL}" \
|
||||
--label "release-failure"
|
||||
@@ -0,0 +1,41 @@
|
||||
name: 'Run Tests'
|
||||
description: 'Runs the preflight checks and integration tests.'
|
||||
|
||||
inputs:
|
||||
gemini_api_key:
|
||||
description: 'The API key for running integration tests.'
|
||||
required: true
|
||||
working-directory:
|
||||
description: 'The working directory to run the tests in.'
|
||||
required: false
|
||||
default: '.'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: 'Install system dependencies'
|
||||
if: "runner.os == 'Linux'"
|
||||
run: |
|
||||
sudo apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq bubblewrap
|
||||
# Ubuntu 24.04+ requires this to allow bwrap to function in CI
|
||||
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
|
||||
shell: 'bash'
|
||||
- name: 'Run Tests'
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ inputs.gemini_api_key }}'
|
||||
GEMINI_CLI_TRUST_WORKSPACE: true
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: |-
|
||||
echo "::group::Build"
|
||||
npm run build
|
||||
echo "::endgroup::"
|
||||
echo "::group::Unit Tests"
|
||||
npm run test:ci
|
||||
echo "::endgroup::"
|
||||
echo "::group::Integration Tests (no sandbox)"
|
||||
npm run test:integration:sandbox:none
|
||||
echo "::endgroup::"
|
||||
echo "::group::Integration Tests (docker sandbox)"
|
||||
npm run test:integration:sandbox:docker
|
||||
echo "::endgroup::"
|
||||
shell: 'bash'
|
||||
@@ -0,0 +1,24 @@
|
||||
name: 'Setup NPMRC'
|
||||
description: 'Sets up NPMRC with all the correct repos for readonly access.'
|
||||
|
||||
inputs:
|
||||
github-token:
|
||||
description: 'the github token'
|
||||
required: true
|
||||
|
||||
outputs:
|
||||
auth-token:
|
||||
description: 'The generated NPM auth token'
|
||||
value: '${{ steps.npm_auth_token.outputs.auth-token }}'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: 'Configure .npmrc'
|
||||
shell: 'bash'
|
||||
run: |-
|
||||
echo ""@google-gemini:registry=https://npm.pkg.github.com"" > ~/.npmrc
|
||||
echo ""//npm.pkg.github.com/:_authToken=${INPUTS_GITHUB_TOKEN}"" >> ~/.npmrc
|
||||
echo ""@google:registry=https://wombat-dressing-room.appspot.com"" >> ~/.npmrc
|
||||
env:
|
||||
INPUTS_GITHUB_TOKEN: '${{ inputs.github-token }}'
|
||||
@@ -0,0 +1,139 @@
|
||||
name: 'Tag an NPM release'
|
||||
description: 'Tags a specific npm version to a specific channel.'
|
||||
|
||||
inputs:
|
||||
channel:
|
||||
description: 'NPM Channel tag'
|
||||
required: true
|
||||
version:
|
||||
description: 'version'
|
||||
required: true
|
||||
dry-run:
|
||||
description: 'Whether to run in dry-run mode.'
|
||||
required: true
|
||||
github-token:
|
||||
description: 'The GitHub token for creating the release.'
|
||||
required: true
|
||||
wombat-token-core:
|
||||
description: 'The npm token for the wombat @google/gemini-cli-core'
|
||||
required: true
|
||||
wombat-token-cli:
|
||||
description: 'The npm token for wombat @google/gemini-cli'
|
||||
required: true
|
||||
wombat-token-a2a-server:
|
||||
description: 'The npm token for the @google/gemini-cli-a2a-server package.'
|
||||
required: true
|
||||
cli-package-name:
|
||||
description: 'The name of the cli package.'
|
||||
required: true
|
||||
core-package-name:
|
||||
description: 'The name of the core package.'
|
||||
required: true
|
||||
a2a-package-name:
|
||||
description: 'The name of the a2a package.'
|
||||
required: true
|
||||
working-directory:
|
||||
description: 'The working directory to run the commands in.'
|
||||
required: false
|
||||
default: '.'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
|
||||
with:
|
||||
node-version-file: '${{ inputs.working-directory }}/.nvmrc'
|
||||
|
||||
- name: 'configure .npmrc'
|
||||
uses: './.github/actions/setup-npmrc'
|
||||
with:
|
||||
github-token: '${{ inputs.github-token }}'
|
||||
|
||||
- name: 'Get core Token'
|
||||
uses: './.github/actions/npm-auth-token'
|
||||
id: 'core-token'
|
||||
with:
|
||||
package-name: '${{ inputs.core-package-name }}'
|
||||
github-token: '${{ inputs.github-token }}'
|
||||
wombat-token-core: '${{ inputs.wombat-token-core }}'
|
||||
wombat-token-cli: '${{ inputs.wombat-token-cli }}'
|
||||
wombat-token-a2a-server: '${{ inputs.wombat-token-a2a-server }}'
|
||||
|
||||
- name: 'Change tag for CORE'
|
||||
if: |-
|
||||
${{ inputs.dry-run != 'true' }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ steps.core-token.outputs.auth-token }}'
|
||||
INPUTS_CORE_PACKAGE_NAME: '${{ inputs.core-package-name }}'
|
||||
INPUTS_VERSION: '${{ inputs.version }}'
|
||||
INPUTS_CHANNEL: '${{ inputs.channel }}'
|
||||
shell: 'bash'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: |
|
||||
npm dist-tag add ${INPUTS_CORE_PACKAGE_NAME}@${INPUTS_VERSION} ${INPUTS_CHANNEL}
|
||||
|
||||
- name: 'Get cli Token'
|
||||
uses: './.github/actions/npm-auth-token'
|
||||
id: 'cli-token'
|
||||
with:
|
||||
package-name: '${{ inputs.cli-package-name }}'
|
||||
github-token: '${{ inputs.github-token }}'
|
||||
wombat-token-core: '${{ inputs.wombat-token-core }}'
|
||||
wombat-token-cli: '${{ inputs.wombat-token-cli }}'
|
||||
wombat-token-a2a-server: '${{ inputs.wombat-token-a2a-server }}'
|
||||
|
||||
- name: 'Change tag for CLI'
|
||||
if: |-
|
||||
${{ inputs.dry-run != 'true' }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ steps.cli-token.outputs.auth-token }}'
|
||||
INPUTS_CLI_PACKAGE_NAME: '${{ inputs.cli-package-name }}'
|
||||
INPUTS_VERSION: '${{ inputs.version }}'
|
||||
INPUTS_CHANNEL: '${{ inputs.channel }}'
|
||||
shell: 'bash'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: |
|
||||
npm dist-tag add ${INPUTS_CLI_PACKAGE_NAME}@${INPUTS_VERSION} ${INPUTS_CHANNEL}
|
||||
|
||||
- name: 'Get a2a Token'
|
||||
uses: './.github/actions/npm-auth-token'
|
||||
id: 'a2a-token'
|
||||
with:
|
||||
package-name: '${{ inputs.a2a-package-name }}'
|
||||
github-token: '${{ inputs.github-token }}'
|
||||
wombat-token-core: '${{ inputs.wombat-token-core }}'
|
||||
wombat-token-cli: '${{ inputs.wombat-token-cli }}'
|
||||
wombat-token-a2a-server: '${{ inputs.wombat-token-a2a-server }}'
|
||||
|
||||
- name: 'Change tag for a2a'
|
||||
if: |-
|
||||
${{ inputs.dry-run == 'false' }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ steps.a2a-token.outputs.auth-token }}'
|
||||
INPUTS_A2A_PACKAGE_NAME: '${{ inputs.a2a-package-name }}'
|
||||
INPUTS_VERSION: '${{ inputs.version }}'
|
||||
INPUTS_CHANNEL: '${{ inputs.channel }}'
|
||||
shell: 'bash'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: |
|
||||
npm dist-tag add ${INPUTS_A2A_PACKAGE_NAME}@${INPUTS_VERSION} ${INPUTS_CHANNEL}
|
||||
|
||||
- name: 'Log dry run'
|
||||
if: |-
|
||||
${{ inputs.dry-run == 'true' }}
|
||||
shell: 'bash'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: |
|
||||
echo "Dry run: Would have added tag '${INPUTS_CHANNEL}' to version '${INPUTS_VERSION}' for ${INPUTS_CLI_PACKAGE_NAME}, ${INPUTS_CORE_PACKAGE_NAME}, and ${INPUTS_A2A_PACKAGE_NAME}."
|
||||
|
||||
env:
|
||||
INPUTS_CHANNEL: '${{ inputs.channel }}'
|
||||
|
||||
INPUTS_VERSION: '${{ inputs.version }}'
|
||||
|
||||
INPUTS_CLI_PACKAGE_NAME: '${{ inputs.cli-package-name }}'
|
||||
|
||||
INPUTS_CORE_PACKAGE_NAME: '${{ inputs.core-package-name }}'
|
||||
|
||||
INPUTS_A2A_PACKAGE_NAME: '${{ inputs.a2a-package-name }}'
|
||||
@@ -0,0 +1,103 @@
|
||||
name: 'Verify an NPM release'
|
||||
description: 'Fetches a package from NPM and does some basic smoke tests'
|
||||
|
||||
inputs:
|
||||
npm-package:
|
||||
description: 'NPM Package'
|
||||
required: true
|
||||
default: '@google/gemini-cli@latest'
|
||||
npm-registry-url:
|
||||
description: 'NPM Registry URL'
|
||||
required: true
|
||||
npm-registry-scope:
|
||||
description: 'NPM Registry Scope'
|
||||
required: true
|
||||
expected-version:
|
||||
description: 'Expected version'
|
||||
required: true
|
||||
gemini_api_key:
|
||||
description: 'The API key for running integration tests.'
|
||||
required: true
|
||||
github-token:
|
||||
description: 'The GitHub token for running integration tests.'
|
||||
required: true
|
||||
working-directory:
|
||||
description: 'The working directory to run the tests in.'
|
||||
required: false
|
||||
default: '.'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: 'setup node'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: 'configure .npmrc'
|
||||
uses: './.github/actions/setup-npmrc'
|
||||
with:
|
||||
github-token: '${{ inputs.github-token }}'
|
||||
|
||||
- name: 'Clear npm cache'
|
||||
shell: 'bash'
|
||||
run: 'npm cache clean --force'
|
||||
|
||||
- name: 'Install from NPM'
|
||||
uses: 'nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08' # ratchet:nick-fields/retry@v3
|
||||
with:
|
||||
timeout_seconds: 900
|
||||
retry_wait_seconds: 30
|
||||
max_attempts: 10
|
||||
command: |-
|
||||
cd ${{ inputs.working-directory }}
|
||||
npm install --prefer-online --no-cache -g "${{ inputs.npm-package }}"
|
||||
|
||||
- name: 'Smoke test - NPM Install'
|
||||
shell: 'bash'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: |-
|
||||
gemini_version=$(gemini --version)
|
||||
if [ "$gemini_version" != "${INPUTS_EXPECTED_VERSION}" ]; then
|
||||
echo "❌ NPM Version mismatch: Got $gemini_version from ${INPUTS_NPM_PACKAGE}, expected ${INPUTS_EXPECTED_VERSION}"
|
||||
exit 1
|
||||
fi
|
||||
env:
|
||||
INPUTS_EXPECTED_VERSION: '${{ inputs.expected-version }}'
|
||||
INPUTS_NPM_PACKAGE: '${{ inputs.npm-package }}'
|
||||
|
||||
- name: 'Clear npm cache'
|
||||
shell: 'bash'
|
||||
run: 'npm cache clean --force'
|
||||
|
||||
- name: 'Smoke test - NPX Run'
|
||||
shell: 'bash'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: |-
|
||||
gemini_version=$(npx --yes --prefer-online "${INPUTS_NPM_PACKAGE}" --version)
|
||||
if [ "$gemini_version" != "${INPUTS_EXPECTED_VERSION}" ]; then
|
||||
echo "❌ NPX Run Version mismatch: Got $gemini_version from ${INPUTS_NPM_PACKAGE}, expected ${INPUTS_EXPECTED_VERSION}"
|
||||
exit 1
|
||||
fi
|
||||
env:
|
||||
INPUTS_NPM_PACKAGE: '${{ inputs.npm-package }}'
|
||||
INPUTS_EXPECTED_VERSION: '${{ inputs.expected-version }}'
|
||||
|
||||
- name: 'Install dependencies for integration tests'
|
||||
shell: 'bash'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: 'npm ci --ignore-scripts'
|
||||
|
||||
- name: '🔬 Run integration tests against NPM release'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ inputs.gemini_api_key }}'
|
||||
GEMINI_CLI_TRUST_WORKSPACE: true
|
||||
INTEGRATION_TEST_USE_INSTALLED_GEMINI: 'true'
|
||||
# We must diable CI mode here because it interferes with interactive tests.
|
||||
# See https://github.com/google-gemini/gemini-cli/issues/10517
|
||||
CI: 'false'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
export INTEGRATION_TEST_GEMINI_BINARY_PATH=$(which gemini)
|
||||
npm run test:integration:sandbox:none
|
||||
@@ -0,0 +1,41 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: 'npm'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
day: 'monday'
|
||||
open-pull-requests-limit: 10
|
||||
reviewers:
|
||||
- 'joshualitt'
|
||||
cooldown:
|
||||
semver-major-days: 14
|
||||
semver-minor-days: 14
|
||||
semver-patch-days: 14
|
||||
groups:
|
||||
npm-dependencies:
|
||||
patterns:
|
||||
- '*'
|
||||
update-types:
|
||||
- 'minor'
|
||||
- 'patch'
|
||||
|
||||
- package-ecosystem: 'github-actions'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
day: 'monday'
|
||||
open-pull-requests-limit: 10
|
||||
reviewers:
|
||||
- 'joshualitt'
|
||||
cooldown:
|
||||
semver-major-days: 14
|
||||
semver-minor-days: 14
|
||||
semver-patch-days: 14
|
||||
groups:
|
||||
actions-dependencies:
|
||||
patterns:
|
||||
- '*'
|
||||
update-types:
|
||||
- 'minor'
|
||||
- 'patch'
|
||||
@@ -0,0 +1,42 @@
|
||||
## Summary
|
||||
|
||||
<!-- Concisely describe what this PR changes and why. Focus on impact and
|
||||
urgency. -->
|
||||
|
||||
## Details
|
||||
|
||||
<!-- Add any extra context and design decisions. Keep it brief but complete. -->
|
||||
|
||||
## Related Issues
|
||||
|
||||
<!-- Use keywords to auto-close issues (Closes #123, Fixes #456). If this PR is
|
||||
only related to an issue or is a partial fix, simply reference the issue number
|
||||
without a keyword (Related to #123). -->
|
||||
|
||||
## How to Validate
|
||||
|
||||
<!-- List exact steps for reviewers to validate the change. Include commands,
|
||||
expected results, and edge cases. -->
|
||||
|
||||
## Pre-Merge Checklist
|
||||
|
||||
<!-- Check all that apply before requesting review or merging. -->
|
||||
|
||||
- [ ] Updated relevant documentation and README (if needed)
|
||||
- [ ] Added/updated tests (if needed)
|
||||
- [ ] Noted breaking changes (if any)
|
||||
- [ ] Validated on required platforms/methods:
|
||||
- [ ] MacOS
|
||||
- [ ] npm run
|
||||
- [ ] npx
|
||||
- [ ] Docker
|
||||
- [ ] Podman
|
||||
- [ ] Seatbelt
|
||||
- [ ] Windows
|
||||
- [ ] npm run
|
||||
- [ ] npx
|
||||
- [ ] Docker
|
||||
- [ ] Linux
|
||||
- [ ] npm run
|
||||
- [ ] npx
|
||||
- [ ] Docker
|
||||
@@ -0,0 +1,278 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
module.exports = async ({ github, context, core }) => {
|
||||
const extractJson = (raw) => {
|
||||
if (!raw || raw === '[]' || raw === '') return [];
|
||||
try {
|
||||
// First, try to parse the raw output as JSON.
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
// If that fails, check for a markdown code block.
|
||||
core.info(
|
||||
'Direct JSON parsing failed. Trying to extract from a markdown block.',
|
||||
);
|
||||
const jsonMatch = raw.match(/```json\s*([\s\S]*?)\s*```/);
|
||||
if (jsonMatch && jsonMatch[1]) {
|
||||
try {
|
||||
return JSON.parse(jsonMatch[1].trim());
|
||||
} catch (markdownError) {
|
||||
core.warning(
|
||||
`Failed to parse extracted JSON from markdown block: ${markdownError.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find a raw JSON array in the output.
|
||||
const jsonArrayMatch = raw.match(
|
||||
/\[\s*\{\s*"issue_number"[\s\S]*\}\s*\]/,
|
||||
);
|
||||
if (jsonArrayMatch) {
|
||||
try {
|
||||
return JSON.parse(jsonArrayMatch[0]);
|
||||
} catch {
|
||||
const fallbackMatch = raw.match(/(\[\s*\{\s*"issue_number"[\s\S]*)/);
|
||||
if (fallbackMatch) {
|
||||
try {
|
||||
const cleaned = fallbackMatch[0].substring(
|
||||
0,
|
||||
fallbackMatch[0].lastIndexOf(']') + 1,
|
||||
);
|
||||
return JSON.parse(cleaned);
|
||||
} catch (fallbackError) {
|
||||
core.warning(
|
||||
`Failed to parse extracted JSON using fallback regex: ${fallbackError.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
core.warning('No valid JSON could be extracted from input.');
|
||||
return [];
|
||||
};
|
||||
|
||||
// Collect all outputs from environment variables
|
||||
// Prioritize EFFORT results over STANDARD results by processing Effort FIRST
|
||||
// so that its labels appear first in the merged arrays (and thus win in mutually exclusive logic)
|
||||
const effortRaw = process.env.LABELS_OUTPUT_EFFORT;
|
||||
const standardRaw = process.env.LABELS_OUTPUT_STANDARD;
|
||||
const genericRaw = process.env.LABELS_OUTPUT;
|
||||
|
||||
const resultsByIssue = new Map();
|
||||
|
||||
const processResults = (results, _sourceName) => {
|
||||
for (const entry of results) {
|
||||
const issueNumber = entry.issue_number;
|
||||
if (!issueNumber) continue;
|
||||
|
||||
if (!resultsByIssue.has(issueNumber)) {
|
||||
resultsByIssue.set(issueNumber, {
|
||||
issue_number: issueNumber,
|
||||
labels_to_add: [...(entry.labels_to_add || [])],
|
||||
labels_to_remove: [...(entry.labels_to_remove || [])],
|
||||
explanation: entry.explanation || '',
|
||||
effort_analysis: entry.effort_analysis || '',
|
||||
});
|
||||
} else {
|
||||
const existing = resultsByIssue.get(issueNumber);
|
||||
// Combine labels
|
||||
existing.labels_to_add = [
|
||||
...new Set([
|
||||
...existing.labels_to_add,
|
||||
...(entry.labels_to_add || []),
|
||||
]),
|
||||
];
|
||||
existing.labels_to_remove = [
|
||||
...new Set([
|
||||
...existing.labels_to_remove,
|
||||
...(entry.labels_to_remove || []),
|
||||
]),
|
||||
];
|
||||
|
||||
// Combine explanations (if different)
|
||||
if (
|
||||
entry.explanation &&
|
||||
!existing.explanation.includes(entry.explanation)
|
||||
) {
|
||||
existing.explanation = existing.explanation
|
||||
? `${existing.explanation}\n\n${entry.explanation}`
|
||||
: entry.explanation;
|
||||
}
|
||||
|
||||
// Take effort analysis if present
|
||||
if (entry.effort_analysis && !existing.effort_analysis) {
|
||||
existing.effort_analysis = entry.effort_analysis;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Order matters: Effort first so its labels win in conflict resolution
|
||||
processResults(extractJson(effortRaw), 'EFFORT');
|
||||
processResults(extractJson(standardRaw), 'STANDARD');
|
||||
processResults(extractJson(genericRaw), 'GENERIC');
|
||||
|
||||
const finalResults = Array.from(resultsByIssue.values());
|
||||
core.info(`Aggregated triage results for ${finalResults.length} issues.`);
|
||||
|
||||
for (const entry of finalResults) {
|
||||
const issueNumber = entry.issue_number;
|
||||
let labelsToAdd = entry.labels_to_add || [];
|
||||
let labelsToRemove = entry.labels_to_remove || [];
|
||||
let existingLabels = [];
|
||||
|
||||
// Fetch existing labels early
|
||||
try {
|
||||
const { data: issueData } = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
});
|
||||
existingLabels = issueData.labels.map((l) =>
|
||||
typeof l === 'string' ? l : l.name,
|
||||
);
|
||||
} catch (e) {
|
||||
core.warning(
|
||||
`Failed to fetch existing labels for #${issueNumber}: ${e.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Programmatic Priority Downgrade Logic
|
||||
if (labelsToAdd.includes('status/need-information')) {
|
||||
const targetPriority = labelsToAdd.find((l) => l.startsWith('priority/'));
|
||||
if (targetPriority) {
|
||||
let downgradedPriority = null;
|
||||
if (targetPriority === 'priority/p0')
|
||||
downgradedPriority = 'priority/p1';
|
||||
if (targetPriority === 'priority/p1')
|
||||
downgradedPriority = 'priority/p2';
|
||||
|
||||
if (downgradedPriority) {
|
||||
core.info(
|
||||
`Programmatically downgrading ${targetPriority} to ${downgradedPriority} due to status/need-information`,
|
||||
);
|
||||
labelsToAdd = labelsToAdd.filter((l) => l !== targetPriority);
|
||||
labelsToAdd.push(downgradedPriority);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
labelsToRemove.push('status/need-triage');
|
||||
|
||||
if (
|
||||
labelsToAdd.includes('status/manual-triage') ||
|
||||
existingLabels.includes('status/manual-triage')
|
||||
) {
|
||||
labelsToRemove.push('status/bot-triaged');
|
||||
labelsToAdd = labelsToAdd.filter((l) => l !== 'status/bot-triaged');
|
||||
} else {
|
||||
labelsToAdd.push('status/bot-triaged');
|
||||
}
|
||||
|
||||
// Resolve internal conflicts (e.g., adding P1 and P2)
|
||||
// We already resolved these by putting Effort first in the combined list
|
||||
|
||||
// Resolve external conflicts with existing labels
|
||||
if (labelsToAdd.some((l) => l.startsWith('area/'))) {
|
||||
labelsToRemove.push(
|
||||
...existingLabels.filter((l) => l.startsWith('area/')),
|
||||
);
|
||||
}
|
||||
if (labelsToAdd.some((l) => l.startsWith('priority/'))) {
|
||||
labelsToRemove.push(
|
||||
...existingLabels.filter((l) => l.startsWith('priority/')),
|
||||
);
|
||||
}
|
||||
if (labelsToAdd.some((l) => l.startsWith('kind/'))) {
|
||||
labelsToRemove.push(
|
||||
...existingLabels.filter((l) => l.startsWith('kind/')),
|
||||
);
|
||||
}
|
||||
|
||||
// Enforce mutual exclusivity in the TO-ADD list (Architect wins)
|
||||
const exclusivePrefixes = ['area/', 'priority/', 'kind/'];
|
||||
for (const prefix of exclusivePrefixes) {
|
||||
const filtered = labelsToAdd.filter((l) => l.startsWith(prefix));
|
||||
if (filtered.length > 1) {
|
||||
const winner = filtered[0]; // First one wins
|
||||
core.info(
|
||||
`Issue #${issueNumber} has multiple ${prefix} labels suggested. Keeping "${winner}" and discarding others.`,
|
||||
);
|
||||
labelsToAdd = labelsToAdd.filter(
|
||||
(l) => !l.startsWith(prefix) || l === winner,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Final deduplication and cleanup
|
||||
labelsToRemove = [...new Set(labelsToRemove)].filter(
|
||||
(l) => !labelsToAdd.includes(l) && existingLabels.includes(l),
|
||||
);
|
||||
labelsToAdd = [...new Set(labelsToAdd)].filter(
|
||||
(l) => !existingLabels.includes(l),
|
||||
);
|
||||
|
||||
// Batch label operations
|
||||
if (labelsToAdd.length > 0) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
labels: labelsToAdd,
|
||||
});
|
||||
core.info(
|
||||
`Successfully added labels for #${issueNumber}: ${labelsToAdd.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (labelsToRemove.length > 0) {
|
||||
for (const label of labelsToRemove) {
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
name: label,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.status !== 404)
|
||||
core.warning(
|
||||
`Failed to remove label ${label} from #${issueNumber}: ${e.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
core.info(
|
||||
`Successfully removed labels for #${issueNumber}: ${labelsToRemove.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Post comment if needed
|
||||
const needsInfoAdded =
|
||||
labelsToAdd.includes('status/need-information') &&
|
||||
!existingLabels.includes('status/need-information');
|
||||
const hasEffortAnalysis = !!entry.effort_analysis;
|
||||
|
||||
if (needsInfoAdded || hasEffortAnalysis) {
|
||||
let commentBody = '';
|
||||
if (needsInfoAdded && entry.explanation) commentBody += entry.explanation;
|
||||
if (hasEffortAnalysis) {
|
||||
if (commentBody) commentBody += '\n\n';
|
||||
commentBody += `**Effort Analysis:**\n${entry.effort_analysis}`;
|
||||
}
|
||||
|
||||
if (commentBody) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
body: commentBody,
|
||||
});
|
||||
core.info(`Posted required comment for #${issueNumber}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
/* eslint-disable */
|
||||
/* global require, console, process */
|
||||
|
||||
/**
|
||||
* Script to backfill the 'status/need-triage' label to all open issues
|
||||
* that are NOT currently labeled with '🔒 maintainer only' or 'help wanted'.
|
||||
*/
|
||||
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
const isDryRun = process.argv.includes('--dry-run');
|
||||
const REPO = 'google-gemini/gemini-cli';
|
||||
|
||||
/**
|
||||
* Executes a GitHub CLI command safely using an argument array to prevent command injection.
|
||||
* @param {string[]} args
|
||||
* @returns {string|null}
|
||||
*/
|
||||
function runGh(args) {
|
||||
try {
|
||||
// Using execFileSync with an array of arguments is safe as it doesn't use a shell.
|
||||
// We set a large maxBuffer (10MB) to handle repositories with many issues.
|
||||
return execFileSync('gh', args, {
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
}).trim();
|
||||
} catch (error) {
|
||||
const stderr = error.stderr ? ` Stderr: ${error.stderr.trim()}` : '';
|
||||
console.error(
|
||||
`❌ Error running gh ${args.join(' ')}: ${error.message}${stderr}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('🔐 GitHub CLI security check...');
|
||||
const authStatus = runGh(['auth', 'status']);
|
||||
if (authStatus === null) {
|
||||
console.error('❌ GitHub CLI (gh) is not installed or not authenticated.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (isDryRun) {
|
||||
console.log('🧪 DRY RUN MODE ENABLED - No changes will be made.\n');
|
||||
}
|
||||
|
||||
console.log(`🔍 Fetching and filtering open issues from ${REPO}...`);
|
||||
|
||||
// We use the /issues endpoint with pagination to bypass the 1000-result limit.
|
||||
// The jq filter ensures we exclude PRs, maintainer-only, help-wanted, and existing status/need-triage.
|
||||
const jqFilter =
|
||||
'.[] | select(.pull_request == null) | select([.labels[].name] as $l | (any($l[]; . == "🔒 maintainer only") | not) and (any($l[]; . == "help wanted") | not) and (any($l[]; . == "status/need-triage") | not)) | {number: .number, title: .title}';
|
||||
|
||||
const output = runGh([
|
||||
'api',
|
||||
`repos/${REPO}/issues?state=open&per_page=100`,
|
||||
'--paginate',
|
||||
'--jq',
|
||||
jqFilter,
|
||||
]);
|
||||
|
||||
if (output === null) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const issues = output
|
||||
.split('\n')
|
||||
.filter((line) => line.trim())
|
||||
.map((line) => {
|
||||
try {
|
||||
return JSON.parse(line);
|
||||
} catch (_e) {
|
||||
console.error(`⚠️ Failed to parse line: ${line}`);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
console.log(`✅ Found ${issues.length} issues matching criteria.`);
|
||||
|
||||
if (issues.length === 0) {
|
||||
console.log('✨ No issues need backfilling.');
|
||||
return;
|
||||
}
|
||||
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
if (isDryRun) {
|
||||
for (const issue of issues) {
|
||||
console.log(
|
||||
`[DRY RUN] Would label issue #${issue.number}: ${issue.title}`,
|
||||
);
|
||||
}
|
||||
successCount = issues.length;
|
||||
} else {
|
||||
console.log(`🏷️ Applying labels to ${issues.length} issues...`);
|
||||
|
||||
for (const issue of issues) {
|
||||
const issueNumber = String(issue.number);
|
||||
console.log(`🏷️ Labeling issue #${issueNumber}: ${issue.title}`);
|
||||
|
||||
const result = runGh([
|
||||
'issue',
|
||||
'edit',
|
||||
issueNumber,
|
||||
'--add-label',
|
||||
'status/need-triage',
|
||||
'--repo',
|
||||
REPO,
|
||||
]);
|
||||
|
||||
if (result !== null) {
|
||||
successCount++;
|
||||
} else {
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n📊 Summary:`);
|
||||
console.log(` - Success: ${successCount}`);
|
||||
console.log(` - Failed: ${failCount}`);
|
||||
|
||||
if (failCount > 0) {
|
||||
console.error(`\n❌ Backfill completed with ${failCount} errors.`);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log(`\n🎉 ${isDryRun ? 'Dry run' : 'Backfill'} complete!`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('❌ Unexpected error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
/* global require, console, process */
|
||||
|
||||
/**
|
||||
* Script to backfill a process change notification comment to all open PRs
|
||||
* not created by members of the 'gemini-cli-maintainers' team.
|
||||
*
|
||||
* Skip PRs that are already associated with an issue.
|
||||
*/
|
||||
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
const isDryRun = process.argv.includes('--dry-run');
|
||||
const REPO = 'google-gemini/gemini-cli';
|
||||
const ORG = 'google-gemini';
|
||||
const TEAM_SLUG = 'gemini-cli-maintainers';
|
||||
const DISCUSSION_URL =
|
||||
'https://github.com/google-gemini/gemini-cli/discussions/16706';
|
||||
|
||||
/**
|
||||
* Executes a GitHub CLI command safely using an argument array.
|
||||
*/
|
||||
function runGh(args, options = {}) {
|
||||
const { silent = false } = options;
|
||||
try {
|
||||
return execFileSync('gh', args, {
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
}).trim();
|
||||
} catch (error) {
|
||||
if (!silent) {
|
||||
const stderr = error.stderr ? ` Stderr: ${error.stderr.trim()}` : '';
|
||||
console.error(
|
||||
`❌ Error running gh ${args.join(' ')}: ${error.message}${stderr}`,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a user is a member of the maintainers team.
|
||||
*/
|
||||
const membershipCache = new Map();
|
||||
function isMaintainer(username) {
|
||||
if (membershipCache.has(username)) return membershipCache.get(username);
|
||||
|
||||
// GitHub returns 404 if user is not a member.
|
||||
// We use silent: true to avoid logging 404s as errors.
|
||||
const result = runGh(
|
||||
['api', `orgs/${ORG}/teams/${TEAM_SLUG}/memberships/${username}`],
|
||||
{ silent: true },
|
||||
);
|
||||
|
||||
const isMember = result !== null;
|
||||
membershipCache.set(username, isMember);
|
||||
return isMember;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('🔐 GitHub CLI security check...');
|
||||
if (runGh(['auth', 'status']) === null) {
|
||||
console.error('❌ GitHub CLI (gh) is not authenticated.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (isDryRun) {
|
||||
console.log('🧪 DRY RUN MODE ENABLED\n');
|
||||
}
|
||||
|
||||
console.log(`📥 Fetching open PRs from ${REPO}...`);
|
||||
// Fetch number, author, and closingIssuesReferences to check if linked to an issue
|
||||
const prsJson = runGh([
|
||||
'pr',
|
||||
'list',
|
||||
'--repo',
|
||||
REPO,
|
||||
'--state',
|
||||
'open',
|
||||
'--limit',
|
||||
'1000',
|
||||
'--json',
|
||||
'number,author,closingIssuesReferences',
|
||||
]);
|
||||
|
||||
if (prsJson === null) process.exit(1);
|
||||
const prs = JSON.parse(prsJson);
|
||||
|
||||
console.log(`📊 Found ${prs.length} open PRs. Filtering...`);
|
||||
|
||||
let targetPrs = [];
|
||||
for (const pr of prs) {
|
||||
const author = pr.author.login;
|
||||
const issueCount = pr.closingIssuesReferences
|
||||
? pr.closingIssuesReferences.length
|
||||
: 0;
|
||||
|
||||
if (issueCount > 0) {
|
||||
// Skip if already linked to an issue
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isMaintainer(author)) {
|
||||
targetPrs.push(pr);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`✅ Found ${targetPrs.length} PRs from non-maintainers without associated issues.`,
|
||||
);
|
||||
|
||||
const commentBody =
|
||||
"\nHi @{AUTHOR}, thank you so much for your contribution to Gemini CLI! We really appreciate the time and effort you've put into this.\n\nWe're making some updates to our contribution process to improve how we track and review changes. Please take a moment to review our recent discussion post: [Improving Our Contribution Process & Introducing New Guidelines](${DISCUSSION_URL}).\n\nKey Update: Starting **January 26, 2026**, the Gemini CLI project will require all pull requests to be associated with an existing issue. Any pull requests not linked to an issue by that date will be automatically closed.\n\nThank you for your understanding and for being a part of our community!\n ".trim();
|
||||
|
||||
let successCount = 0;
|
||||
let skipCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
for (const pr of targetPrs) {
|
||||
const prNumber = String(pr.number);
|
||||
const author = pr.author.login;
|
||||
|
||||
// Check if we already commented (idempotency)
|
||||
// We use silent: true here because view might fail if PR is deleted mid-run
|
||||
const existingComments = runGh(
|
||||
[
|
||||
'pr',
|
||||
'view',
|
||||
prNumber,
|
||||
'--repo',
|
||||
REPO,
|
||||
'--json',
|
||||
'comments',
|
||||
'--jq',
|
||||
`.comments[].body | contains("${DISCUSSION_URL}")`,
|
||||
],
|
||||
{ silent: true },
|
||||
);
|
||||
|
||||
if (existingComments && existingComments.includes('true')) {
|
||||
console.log(
|
||||
`⏭️ PR #${prNumber} already has the notification. Skipping.`,
|
||||
);
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isDryRun) {
|
||||
console.log(`[DRY RUN] Would notify @${author} on PR #${prNumber}`);
|
||||
successCount++;
|
||||
} else {
|
||||
console.log(`💬 Notifying @${author} on PR #${prNumber}...`);
|
||||
const personalizedComment = commentBody.replace('{AUTHOR}', author);
|
||||
const result = runGh([
|
||||
'pr',
|
||||
'comment',
|
||||
prNumber,
|
||||
'--repo',
|
||||
REPO,
|
||||
'--body',
|
||||
personalizedComment,
|
||||
]);
|
||||
|
||||
if (result !== null) {
|
||||
successCount++;
|
||||
} else {
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n📊 Summary:`);
|
||||
console.log(` - Notified: ${successCount}`);
|
||||
console.log(` - Skipped: ${skipCount}`);
|
||||
console.log(` - Failed: ${failCount}`);
|
||||
|
||||
if (failCount > 0) process.exit(1);
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
const fs = require('node:fs');
|
||||
|
||||
module.exports = async ({ github, context, core }) => {
|
||||
let issuesToCleanup = [];
|
||||
try {
|
||||
const fileContent = fs.readFileSync('issues_to_cleanup.json', 'utf8');
|
||||
issuesToCleanup = JSON.parse(fileContent);
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
core.info('No issues found to clean up.');
|
||||
return;
|
||||
}
|
||||
core.setFailed(`Failed to read issues_to_cleanup.json: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const issue of issuesToCleanup) {
|
||||
try {
|
||||
const { data: issueData } = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
});
|
||||
|
||||
const labels = issueData.labels.map((l) =>
|
||||
typeof l === 'string' ? l : l.name,
|
||||
);
|
||||
|
||||
if (
|
||||
labels.includes('status/bot-triaged') &&
|
||||
labels.includes('status/need-triage')
|
||||
) {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
name: 'status/need-triage',
|
||||
});
|
||||
core.info(
|
||||
`Successfully removed status/need-triage from #${issue.number}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
labels.includes('status/bot-triaged') &&
|
||||
labels.includes('status/manual-triage')
|
||||
) {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
name: 'status/bot-triaged',
|
||||
});
|
||||
core.info(
|
||||
`Successfully removed status/bot-triaged from #${issue.number} because it requires manual triage`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
core.warning(
|
||||
`Failed to clean up labels for #${issue.number}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
core.info(
|
||||
`Cleaned up conflicting labels from ${issuesToCleanup.length} issues.`,
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,380 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Gemini Scheduled Lifecycle Manager Script
|
||||
* @param {object} param0
|
||||
* @param {import('@octokit/rest').Octokit} param0.github
|
||||
* @param {import('@actions/github/lib/context').Context} param0.context
|
||||
* @param {import('@actions/core')} param0.core
|
||||
*/
|
||||
module.exports = async ({ github, context, core }) => {
|
||||
const dryRun = process.env.DRY_RUN === 'true';
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
core.info(`Running in ${dryRun ? 'DRY RUN' : 'PRODUCTION'} mode.`);
|
||||
|
||||
const STALE_LABEL = 'stale';
|
||||
const NEED_INFO_LABEL = 'status/need-information';
|
||||
const EXEMPT_LABELS = [
|
||||
'pinned',
|
||||
'security',
|
||||
'🔒 maintainer only',
|
||||
'help wanted',
|
||||
'🗓️ Public Roadmap',
|
||||
];
|
||||
|
||||
const STALE_DAYS = 60;
|
||||
const CLOSE_DAYS = 14;
|
||||
const NO_RESPONSE_DAYS = 14;
|
||||
|
||||
const now = new Date();
|
||||
const staleThreshold = new Date(
|
||||
now.getTime() - STALE_DAYS * 24 * 60 * 60 * 1000,
|
||||
);
|
||||
const closeThreshold = new Date(
|
||||
now.getTime() - CLOSE_DAYS * 24 * 60 * 60 * 1000,
|
||||
);
|
||||
const noResponseThreshold = new Date(
|
||||
now.getTime() - NO_RESPONSE_DAYS * 24 * 60 * 60 * 1000,
|
||||
);
|
||||
|
||||
const maintainerCache = new Map();
|
||||
async function isMaintainer(user, association) {
|
||||
if (user?.type === 'Bot') return true;
|
||||
if (['OWNER', 'MEMBER', 'COLLABORATOR'].includes(association)) return true;
|
||||
|
||||
const username = user?.login;
|
||||
if (!username) return false;
|
||||
|
||||
if (maintainerCache.has(username)) {
|
||||
return maintainerCache.get(username);
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner,
|
||||
repo,
|
||||
username,
|
||||
});
|
||||
// Permission can be admin, write, read, none.
|
||||
// Roles like 'maintain' or 'triage' often map to 'write' or 'read' in the top-level field.
|
||||
const isM =
|
||||
['admin', 'write'].includes(data.permission) ||
|
||||
['admin', 'maintain', 'write'].includes(data.role_name);
|
||||
|
||||
maintainerCache.set(username, isM);
|
||||
return isM;
|
||||
} catch (err) {
|
||||
core.warning(
|
||||
`Could not check permissions for ${username}: ${err.message}`,
|
||||
);
|
||||
maintainerCache.set(username, false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function processItems(query, callback) {
|
||||
core.info(`Searching: ${query}`);
|
||||
try {
|
||||
let items = await github.paginate(
|
||||
github.rest.search.issuesAndPullRequests,
|
||||
{
|
||||
q: query,
|
||||
per_page: 100,
|
||||
sort: 'updated',
|
||||
order: 'asc',
|
||||
},
|
||||
);
|
||||
core.info(`Found ${items.length} items.`);
|
||||
for (const item of items) {
|
||||
try {
|
||||
await callback(item);
|
||||
} catch (err) {
|
||||
core.error(`Error processing #${item.number}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
core.error(`Search failed: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Handle No-Response (status/need-information)
|
||||
// Removal: Check issues updated in the last 48h that have the label
|
||||
const twoDaysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000);
|
||||
await processItems(
|
||||
`repo:${owner}/${repo} is:open label:"${NEED_INFO_LABEL}" updated:>${twoDaysAgo.toISOString()}`,
|
||||
async (item) => {
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: item.number,
|
||||
sort: 'created',
|
||||
direction: 'desc',
|
||||
per_page: 5,
|
||||
});
|
||||
|
||||
// Check if the last comment is from a non-maintainer and not a bot
|
||||
const lastComment = comments[0];
|
||||
if (
|
||||
lastComment &&
|
||||
lastComment.user?.type !== 'Bot' &&
|
||||
!(await isMaintainer(lastComment.user, lastComment.author_association))
|
||||
) {
|
||||
if (dryRun) {
|
||||
core.info(
|
||||
`[DRY RUN] Would remove ${NEED_INFO_LABEL} from #${item.number} due to contributor response.`,
|
||||
);
|
||||
} else {
|
||||
core.info(
|
||||
`Removing ${NEED_INFO_LABEL} from #${item.number} due to contributor response.`,
|
||||
);
|
||||
await github.rest.issues
|
||||
.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: item.number,
|
||||
name: NEED_INFO_LABEL,
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Closure: Check issues with the label that haven't been updated in 14 days
|
||||
await processItems(
|
||||
`repo:${owner}/${repo} is:open label:"${NEED_INFO_LABEL}" updated:<${noResponseThreshold.toISOString()}`,
|
||||
async (item) => {
|
||||
if (dryRun) {
|
||||
core.info(
|
||||
`[DRY RUN] Would close #${item.number} due to no response for ${NO_RESPONSE_DAYS} days.`,
|
||||
);
|
||||
} else {
|
||||
core.info(
|
||||
`Closing #${item.number} due to no response for ${NO_RESPONSE_DAYS} days.`,
|
||||
);
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: item.number,
|
||||
body: `This item was marked as needing more information and has not received a response in ${NO_RESPONSE_DAYS} days. Closing it for now. If you still face this problem, feel free to reopen with more details. Thank you!`,
|
||||
});
|
||||
await github.rest.issues.update({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: item.number,
|
||||
state: 'closed',
|
||||
state_reason: 'not_planned',
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 2. Handle Stale Mark (60 days inactivity, no stale label)
|
||||
const exemptQuery = EXEMPT_LABELS.map((l) => `-label:"${l}"`).join(' ');
|
||||
|
||||
await processItems(
|
||||
`repo:${owner}/${repo} is:open -label:"${STALE_LABEL}" ${exemptQuery} updated:<${staleThreshold.toISOString()}`,
|
||||
async (item) => {
|
||||
const isBug = item.labels.some((l) =>
|
||||
(typeof l === 'string' ? l : l.name).toLowerCase().includes('bug'),
|
||||
);
|
||||
const bodyText = isBug
|
||||
? `This bug report has been automatically marked as stale due to ${STALE_DAYS} days of inactivity. Many issues are resolved in newer releases. Please verify if the issue persists in the latest Gemini CLI version. If it does, please leave a comment to keep this open. It will be closed in ${CLOSE_DAYS} days if no further activity occurs. Thank you!`
|
||||
: `This item has been automatically marked as stale due to ${STALE_DAYS} days of inactivity. It will be closed in ${CLOSE_DAYS} days if no further activity occurs. Thank you!`;
|
||||
|
||||
if (dryRun) {
|
||||
core.info(`[DRY RUN] Would mark #${item.number} as stale.`);
|
||||
} else {
|
||||
core.info(`Marking #${item.number} as stale.`);
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: item.number,
|
||||
labels: [STALE_LABEL],
|
||||
});
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: item.number,
|
||||
body: bodyText,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 3. Handle Stale Removal & Close
|
||||
await processItems(
|
||||
`repo:${owner}/${repo} is:open label:"${STALE_LABEL}" ${exemptQuery}`,
|
||||
async (item) => {
|
||||
// Fetch full timeline to see events and comments
|
||||
const timeline = await github.paginate(
|
||||
github.rest.issues.listEventsForTimeline,
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
issue_number: item.number,
|
||||
per_page: 100,
|
||||
},
|
||||
);
|
||||
|
||||
// Find exactly when the Stale label was added
|
||||
// We look for the last 'labeled' event for STALE_LABEL
|
||||
const staleEventIndex = timeline.findLastIndex(
|
||||
(e) =>
|
||||
e.event === 'labeled' &&
|
||||
e.label?.name?.toLowerCase() === STALE_LABEL.toLowerCase(),
|
||||
);
|
||||
|
||||
if (staleEventIndex === -1) return; // Fallback if no event found
|
||||
|
||||
const staleEvent = timeline[staleEventIndex];
|
||||
const eventsAfterStale = timeline.slice(staleEventIndex + 1);
|
||||
|
||||
// Check for meaningful activity after the Stale label was applied
|
||||
const meaningfulEvents = eventsAfterStale.filter((e) => {
|
||||
const actor = e.actor?.login || '';
|
||||
const isBot =
|
||||
actor.includes('[bot]') || actor.includes('github-actions');
|
||||
|
||||
if (isBot) return false;
|
||||
|
||||
// Explicit whitelist of meaningful events for humans
|
||||
if (
|
||||
[
|
||||
'commented',
|
||||
'cross-referenced',
|
||||
'connected',
|
||||
'reopened',
|
||||
'assigned',
|
||||
].includes(e.event)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (meaningfulEvents.length > 0) {
|
||||
// Activity detected, remove Stale label
|
||||
if (dryRun) {
|
||||
core.info(
|
||||
`[DRY RUN] Would remove ${STALE_LABEL} from #${item.number} due to meaningful activity (e.g., comment or PR).`,
|
||||
);
|
||||
} else {
|
||||
core.info(
|
||||
`Removing ${STALE_LABEL} from #${item.number} due to meaningful activity (e.g., comment or PR).`,
|
||||
);
|
||||
await github.rest.issues
|
||||
.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: item.number,
|
||||
name: STALE_LABEL,
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// No meaningful activity. Check if 14 days have passed.
|
||||
const labeledDate = new Date(staleEvent.created_at);
|
||||
if (labeledDate > closeThreshold) {
|
||||
// Has not been 14 days since it was ACTUALLY marked stale
|
||||
return;
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
core.info(`[DRY RUN] Would close stale item #${item.number}.`);
|
||||
} else {
|
||||
core.info(`Closing stale item #${item.number}.`);
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: item.number,
|
||||
body: `This item has been closed due to ${CLOSE_DAYS} additional days of inactivity after being marked as stale. If you believe this is still relevant, feel free to comment or reopen. Thank you!`,
|
||||
});
|
||||
await github.rest.issues.update({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: item.number,
|
||||
state: 'closed',
|
||||
state_reason: 'not_planned',
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 4. Handle PR Contribution Policy (Nudge at 7d, Close at 14d)
|
||||
const PR_NUDGE_DAYS = 7;
|
||||
const PR_CLOSE_DAYS = 14;
|
||||
const nudgeThreshold = new Date(
|
||||
now.getTime() - PR_NUDGE_DAYS * 24 * 60 * 60 * 1000,
|
||||
);
|
||||
const prCloseThreshold = new Date(
|
||||
now.getTime() - PR_CLOSE_DAYS * 24 * 60 * 60 * 1000,
|
||||
);
|
||||
|
||||
// Nudge
|
||||
await processItems(
|
||||
`repo:${owner}/${repo} is:open is:pr -label:"help wanted" -label:"🔒 maintainer only" -label:"status/pr-nudge-sent" created:${prCloseThreshold.toISOString()}..${nudgeThreshold.toISOString()}`,
|
||||
async (pr) => {
|
||||
if (await isMaintainer(pr.user, pr.author_association)) return;
|
||||
|
||||
if (dryRun) {
|
||||
core.info(
|
||||
`[DRY RUN] Would nudge PR #${pr.number} for contribution policy.`,
|
||||
);
|
||||
} else {
|
||||
core.info(`Nudging PR #${pr.number} for contribution policy.`);
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr.number,
|
||||
labels: ['status/pr-nudge-sent'],
|
||||
});
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr.number,
|
||||
body: "Hi there! Thank you for your interest in contributing to Gemini CLI. \n\nTo ensure we maintain high code quality and focus on our prioritized roadmap, we only guarantee review and consideration of pull requests for issues that are explicitly labeled as 'help wanted'. \n\nThis PR will be closed in 7 days if it remains without that designation. We encourage you to find and contribute to existing 'help wanted' issues in our backlog! Thank you for your understanding.",
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Close
|
||||
await processItems(
|
||||
`repo:${owner}/${repo} is:open is:pr -label:"help wanted" -label:"🔒 maintainer only" created:<${prCloseThreshold.toISOString()}`,
|
||||
async (pr) => {
|
||||
if (await isMaintainer(pr.user, pr.author_association)) return;
|
||||
|
||||
if (dryRun) {
|
||||
core.info(
|
||||
`[DRY RUN] Would close PR #${pr.number} per contribution policy (no 'help wanted').`,
|
||||
);
|
||||
} else {
|
||||
core.info(
|
||||
`Closing PR #${pr.number} per contribution policy (no 'help wanted').`,
|
||||
);
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr.number,
|
||||
body: "This pull request is being closed as it has been open for 14 days without a 'help wanted' designation. We encourage you to find and contribute to existing 'help wanted' issues in our backlog! Thank you for your understanding.",
|
||||
});
|
||||
await github.rest.pulls.update({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr.number,
|
||||
state: 'closed',
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
Executable
+184
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env bash
|
||||
# @license
|
||||
# Copyright 2026 Google LLC
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Initialize a comma-separated string to hold PR numbers that need a comment
|
||||
PRS_NEEDING_COMMENT=""
|
||||
|
||||
# Global cache for issue labels (compatible with Bash 3.2)
|
||||
# Stores "|ISSUE_NUM:LABELS|" segments
|
||||
ISSUE_LABELS_CACHE_FLAT="|"
|
||||
|
||||
# Function to get labels from an issue (with caching)
|
||||
get_issue_labels() {
|
||||
local ISSUE_NUM="${1}"
|
||||
if [[ -z "${ISSUE_NUM}" || "${ISSUE_NUM}" == "null" || "${ISSUE_NUM}" == "" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
# Check cache
|
||||
case "${ISSUE_LABELS_CACHE_FLAT}" in
|
||||
*"|${ISSUE_NUM}:"*)
|
||||
local suffix="${ISSUE_LABELS_CACHE_FLAT#*|"${ISSUE_NUM}":}"
|
||||
echo "${suffix%%|*}"
|
||||
return
|
||||
;;
|
||||
*)
|
||||
# Cache miss, proceed to fetch
|
||||
;;
|
||||
esac
|
||||
|
||||
echo " 📥 Fetching labels from issue #${ISSUE_NUM}" >&2
|
||||
local gh_output
|
||||
if ! gh_output=$(gh issue view "${ISSUE_NUM}" --repo "${GITHUB_REPOSITORY}" --json labels -q '.labels[].name' 2>/dev/null); then
|
||||
echo " ⚠️ Could not fetch issue #${ISSUE_NUM}" >&2
|
||||
ISSUE_LABELS_CACHE_FLAT="${ISSUE_LABELS_CACHE_FLAT}${ISSUE_NUM}:|"
|
||||
return
|
||||
fi
|
||||
|
||||
local labels
|
||||
labels=$(echo "${gh_output}" | grep -x -E '(area|priority)/.*|help wanted|🔒 maintainer only' | tr '\n' ',' | sed 's/,$//' || echo "")
|
||||
|
||||
# Save to flat cache
|
||||
ISSUE_LABELS_CACHE_FLAT="${ISSUE_LABELS_CACHE_FLAT}${ISSUE_NUM}:${labels}|"
|
||||
echo "${labels}"
|
||||
}
|
||||
|
||||
# Function to process a single PR with pre-fetched data
|
||||
process_pr_optimized() {
|
||||
local PR_NUMBER="${1}"
|
||||
local IS_DRAFT="${2}"
|
||||
local ISSUE_NUMBER="${3}"
|
||||
local CURRENT_LABELS="${4}" # Comma-separated labels
|
||||
|
||||
echo "🔄 Processing PR #${PR_NUMBER}"
|
||||
|
||||
local LABELS_TO_ADD=""
|
||||
local LABELS_TO_REMOVE=""
|
||||
|
||||
if [[ -z "${ISSUE_NUMBER}" || "${ISSUE_NUMBER}" == "null" || "${ISSUE_NUMBER}" == "" ]]; then
|
||||
if [[ "${IS_DRAFT}" == "true" ]]; then
|
||||
echo " 📝 PR #${PR_NUMBER} is a draft and has no linked issue"
|
||||
if [[ ",${CURRENT_LABELS}," == *",status/need-issue,"* ]]; then
|
||||
echo " ➖ Removing status/need-issue label"
|
||||
LABELS_TO_REMOVE="status/need-issue"
|
||||
fi
|
||||
else
|
||||
echo " ⚠️ No linked issue found for PR #${PR_NUMBER}"
|
||||
if [[ ",${CURRENT_LABELS}," != *",status/need-issue,"* ]]; then
|
||||
echo " ➕ Adding status/need-issue label"
|
||||
LABELS_TO_ADD="status/need-issue"
|
||||
fi
|
||||
|
||||
if [[ -z "${PRS_NEEDING_COMMENT}" ]]; then
|
||||
PRS_NEEDING_COMMENT="${PR_NUMBER}"
|
||||
else
|
||||
PRS_NEEDING_COMMENT="${PRS_NEEDING_COMMENT},${PR_NUMBER}"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo " 🔗 Found linked issue #${ISSUE_NUMBER}"
|
||||
|
||||
if [[ ",${CURRENT_LABELS}," == *",status/need-issue,"* ]]; then
|
||||
echo " ➖ Removing status/need-issue label"
|
||||
LABELS_TO_REMOVE="status/need-issue"
|
||||
fi
|
||||
|
||||
local ISSUE_LABELS
|
||||
ISSUE_LABELS=$(get_issue_labels "${ISSUE_NUMBER}")
|
||||
|
||||
if [[ -n "${ISSUE_LABELS}" ]]; then
|
||||
local IFS_OLD="${IFS}"
|
||||
IFS=','
|
||||
for label in ${ISSUE_LABELS}; do
|
||||
if [[ -n "${label}" ]] && [[ ",${CURRENT_LABELS}," != *",${label},"* ]]; then
|
||||
if [[ -z "${LABELS_TO_ADD}" ]]; then
|
||||
LABELS_TO_ADD="${label}"
|
||||
else
|
||||
LABELS_TO_ADD="${LABELS_TO_ADD},${label}"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
IFS="${IFS_OLD}"
|
||||
fi
|
||||
|
||||
if [[ -z "${LABELS_TO_ADD}" && -z "${LABELS_TO_REMOVE}" ]]; then
|
||||
echo " ✅ Labels already synchronized"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "${LABELS_TO_ADD}" || -n "${LABELS_TO_REMOVE}" ]]; then
|
||||
local EDIT_CMD=("gh" "pr" "edit" "${PR_NUMBER}" "--repo" "${GITHUB_REPOSITORY}")
|
||||
if [[ -n "${LABELS_TO_ADD}" ]]; then
|
||||
echo " ➕ Syncing labels to add: ${LABELS_TO_ADD}"
|
||||
EDIT_CMD+=("--add-label" "${LABELS_TO_ADD}")
|
||||
fi
|
||||
if [[ -n "${LABELS_TO_REMOVE}" ]]; then
|
||||
echo " ➖ Syncing labels to remove: ${LABELS_TO_REMOVE}"
|
||||
EDIT_CMD+=("--remove-label" "${LABELS_TO_REMOVE}")
|
||||
fi
|
||||
|
||||
("${EDIT_CMD[@]}" || true)
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ -z "${GITHUB_REPOSITORY:-}" ]]; then
|
||||
echo "‼️ Missing \$GITHUB_REPOSITORY - this must be run from GitHub Actions"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${GITHUB_OUTPUT:-}" ]]; then
|
||||
echo "‼️ Missing \$GITHUB_OUTPUT - this must be run from GitHub Actions"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JQ_EXTRACT_FIELDS='{
|
||||
number: .number,
|
||||
isDraft: .isDraft,
|
||||
issue: (.closingIssuesReferences[0].number // (.body // "" | capture("(^|[^a-zA-Z0-9])#(?<num>[0-9]+)([^a-zA-Z0-9]|$)")? | .num) // "null"),
|
||||
labels: [.labels[].name] | join(",")
|
||||
}'
|
||||
|
||||
JQ_TSV_FORMAT='"\((.number | tostring))\t\(.isDraft)\t\((.issue // null) | tostring)\t\(.labels)"'
|
||||
|
||||
if [[ -n "${PR_NUMBER:-}" ]]; then
|
||||
echo "🔄 Processing single PR #${PR_NUMBER}"
|
||||
PR_DATA=$(gh pr view "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --json number,closingIssuesReferences,isDraft,body,labels 2>/dev/null) || {
|
||||
echo "❌ Failed to fetch data for PR #${PR_NUMBER}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
line=$(echo "${PR_DATA}" | jq -r "${JQ_EXTRACT_FIELDS} | ${JQ_TSV_FORMAT}")
|
||||
IFS=$'\t' read -r pr_num is_draft issue_num current_labels <<< "${line}"
|
||||
process_pr_optimized "${pr_num}" "${is_draft}" "${issue_num}" "${current_labels}"
|
||||
else
|
||||
echo "📥 Getting all open pull requests..."
|
||||
PR_DATA_ALL=$(gh pr list --repo "${GITHUB_REPOSITORY}" --state open --limit 1000 --json number,closingIssuesReferences,isDraft,body,labels 2>/dev/null) || {
|
||||
echo "❌ Failed to fetch PR list"
|
||||
exit 1
|
||||
}
|
||||
|
||||
PR_COUNT=$(echo "${PR_DATA_ALL}" | jq '. | length')
|
||||
echo "📊 Found ${PR_COUNT} open PRs to process"
|
||||
|
||||
# Use a temporary file to avoid masking exit codes in process substitution
|
||||
tmp_file=$(mktemp)
|
||||
echo "${PR_DATA_ALL}" | jq -r ".[] | ${JQ_EXTRACT_FIELDS} | ${JQ_TSV_FORMAT}" > "${tmp_file}"
|
||||
while read -r line; do
|
||||
[[ -z "${line}" ]] && continue
|
||||
IFS=$'\t' read -r pr_num is_draft issue_num current_labels <<< "${line}"
|
||||
process_pr_optimized "${pr_num}" "${is_draft}" "${issue_num}" "${current_labels}"
|
||||
done < "${tmp_file}"
|
||||
rm -f "${tmp_file}"
|
||||
fi
|
||||
|
||||
if [[ -z "${PRS_NEEDING_COMMENT}" ]]; then
|
||||
echo "prs_needing_comment=[]" >> "${GITHUB_OUTPUT}"
|
||||
else
|
||||
echo "prs_needing_comment=[${PRS_NEEDING_COMMENT}]" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
|
||||
echo "✅ PR triage completed"
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
const fs = require('node:fs');
|
||||
|
||||
module.exports = async ({ github, context, core }) => {
|
||||
const query = `
|
||||
query($owner: String!, $repo: String!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
issues(first: 50, states: OPEN, orderBy: {field: UPDATED_AT, direction: DESC}) {
|
||||
nodes {
|
||||
id
|
||||
number
|
||||
title
|
||||
body
|
||||
issueType {
|
||||
name
|
||||
}
|
||||
labels(first: 20) {
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await github.graphql(query, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
});
|
||||
|
||||
const issues = result.repository.issues.nodes;
|
||||
const issuesNeedingAnalysis = [];
|
||||
let syncedCount = 0;
|
||||
|
||||
for (const issue of issues) {
|
||||
if (issue.issueType === null) {
|
||||
const labelNames = issue.labels.nodes.map((l) => l.name);
|
||||
const hasBug = labelNames.includes('kind/bug');
|
||||
const hasFeature =
|
||||
labelNames.includes('kind/feature') ||
|
||||
labelNames.includes('kind/enhancement');
|
||||
|
||||
let issueTypeId = null;
|
||||
if (hasBug) {
|
||||
issueTypeId = 'IT_kwDOCaSVvs4BR7vP'; // Bug
|
||||
} else if (hasFeature) {
|
||||
issueTypeId = 'IT_kwDOCaSVvs4BR7vQ'; // Feature
|
||||
}
|
||||
|
||||
if (issueTypeId) {
|
||||
await github.graphql(
|
||||
`
|
||||
mutation($issueId: ID!, $issueTypeId: ID!) {
|
||||
updateIssue(input: {id: $issueId, issueTypeId: $issueTypeId}) {
|
||||
issue {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
issueId: issue.id,
|
||||
issueTypeId: issueTypeId,
|
||||
},
|
||||
);
|
||||
core.info(`Successfully synced Issue Type for #${issue.number}`);
|
||||
syncedCount++;
|
||||
} else {
|
||||
// Needs analysis to determine kind/type
|
||||
issuesNeedingAnalysis.push({
|
||||
number: issue.number,
|
||||
title: issue.title,
|
||||
body: issue.body,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write issues needing analysis to a file so the AI can process them
|
||||
fs.writeFileSync(
|
||||
'no_type_issues.json',
|
||||
JSON.stringify(issuesNeedingAnalysis),
|
||||
);
|
||||
core.info(`Synced ${syncedCount} issues from labels.`);
|
||||
core.info(
|
||||
`Found ${issuesNeedingAnalysis.length} issues missing both type and kind label to be analyzed.`,
|
||||
);
|
||||
} catch (error) {
|
||||
core.setFailed(`Failed to sync issue types: ${error.message}`);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,389 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
const { Octokit } = require('@octokit/rest');
|
||||
|
||||
/**
|
||||
* Sync Maintainer Labels (Recursive with strict parent-child relationship detection)
|
||||
* - Uses Native Sub-issues.
|
||||
* - Uses Markdown Task Lists (- [ ] #123).
|
||||
* - Filters for OPEN issues only.
|
||||
* - Skips DUPLICATES.
|
||||
* - Skips Pull Requests.
|
||||
* - ONLY labels issues in the PUBLIC (gemini-cli) repo.
|
||||
*/
|
||||
|
||||
const REPO_OWNER = 'google-gemini';
|
||||
const PUBLIC_REPO = 'gemini-cli';
|
||||
const PRIVATE_REPO = 'maintainers-gemini-cli';
|
||||
const ALLOWED_REPOS = [PUBLIC_REPO, PRIVATE_REPO];
|
||||
|
||||
const ROOT_ISSUES = [
|
||||
{ owner: REPO_OWNER, repo: PUBLIC_REPO, number: 15374 },
|
||||
{ owner: REPO_OWNER, repo: PUBLIC_REPO, number: 15456 },
|
||||
{ owner: REPO_OWNER, repo: PUBLIC_REPO, number: 15324 },
|
||||
];
|
||||
|
||||
const TARGET_LABEL = '🔒 maintainer only';
|
||||
const isDryRun =
|
||||
process.argv.includes('--dry-run') || process.env.DRY_RUN === 'true';
|
||||
|
||||
const octokit = new Octokit({
|
||||
auth: process.env.GITHUB_TOKEN,
|
||||
});
|
||||
|
||||
/**
|
||||
* Extracts child issue references from markdown Task Lists ONLY.
|
||||
* e.g. - [ ] #123 or - [x] google-gemini/gemini-cli#123
|
||||
*/
|
||||
function extractTaskListLinks(text, contextOwner, contextRepo) {
|
||||
if (!text) return [];
|
||||
const childIssues = new Map();
|
||||
|
||||
const add = (owner, repo, number) => {
|
||||
if (ALLOWED_REPOS.includes(repo)) {
|
||||
const key = `${owner}/${repo}#${number}`;
|
||||
childIssues.set(key, { owner, repo, number: parseInt(number, 10) });
|
||||
}
|
||||
};
|
||||
|
||||
// 1. Full URLs in task lists
|
||||
const urlRegex =
|
||||
/-\s+\[[ x]\].*https:\/\/github\.com\/([a-zA-Z0-9._-]+)\/([a-zA-Z0-9._-]+)\/issues\/(\d+)\b/g;
|
||||
let match;
|
||||
while ((match = urlRegex.exec(text)) !== null) {
|
||||
add(match[1], match[2], match[3]);
|
||||
}
|
||||
|
||||
// 2. Cross-repo refs in task lists: owner/repo#123
|
||||
const crossRepoRegex =
|
||||
/-\s+\[[ x]\].*([a-zA-Z0-9._-]+)\/([a-zA-Z0-9._-]+)#(\d+)\b/g;
|
||||
while ((match = crossRepoRegex.exec(text)) !== null) {
|
||||
add(match[1], match[2], match[3]);
|
||||
}
|
||||
|
||||
// 3. Short refs in task lists: #123
|
||||
const shortRefRegex = /-\s+\[[ x]\].*#(\d+)\b/g;
|
||||
while ((match = shortRefRegex.exec(text)) !== null) {
|
||||
add(contextOwner, contextRepo, match[1]);
|
||||
}
|
||||
|
||||
return Array.from(childIssues.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches issue data via GraphQL with full pagination for sub-issues, comments, and labels.
|
||||
*/
|
||||
async function fetchIssueData(owner, repo, number) {
|
||||
const query = `
|
||||
query($owner:String!, $repo:String!, $number:Int!) {
|
||||
repository(owner:$owner, name:$repo) {
|
||||
issue(number:$number) {
|
||||
state
|
||||
title
|
||||
body
|
||||
labels(first: 100) {
|
||||
nodes { name }
|
||||
pageInfo { hasNextPage endCursor }
|
||||
}
|
||||
subIssues(first: 100) {
|
||||
nodes {
|
||||
number
|
||||
repository {
|
||||
name
|
||||
owner { login }
|
||||
}
|
||||
}
|
||||
pageInfo { hasNextPage endCursor }
|
||||
}
|
||||
comments(first: 100) {
|
||||
nodes {
|
||||
body
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
try {
|
||||
const response = await octokit.graphql(query, { owner, repo, number });
|
||||
const data = response.repository.issue;
|
||||
if (!data) return null;
|
||||
|
||||
const issue = {
|
||||
state: data.state,
|
||||
title: data.title,
|
||||
body: data.body || '',
|
||||
labels: data.labels.nodes.map((n) => n.name),
|
||||
subIssues: [...data.subIssues.nodes],
|
||||
comments: data.comments.nodes.map((n) => n.body),
|
||||
};
|
||||
|
||||
// Paginate subIssues if there are more than 100
|
||||
if (data.subIssues.pageInfo.hasNextPage) {
|
||||
const moreSubIssues = await paginateConnection(
|
||||
owner,
|
||||
repo,
|
||||
number,
|
||||
'subIssues',
|
||||
'number repository { name owner { login } }',
|
||||
data.subIssues.pageInfo.endCursor,
|
||||
);
|
||||
issue.subIssues.push(...moreSubIssues);
|
||||
}
|
||||
|
||||
// Paginate labels if there are more than 100 (unlikely but for completeness)
|
||||
if (data.labels.pageInfo.hasNextPage) {
|
||||
const moreLabels = await paginateConnection(
|
||||
owner,
|
||||
repo,
|
||||
number,
|
||||
'labels',
|
||||
'name',
|
||||
data.labels.pageInfo.endCursor,
|
||||
(n) => n.name,
|
||||
);
|
||||
issue.labels.push(...moreLabels);
|
||||
}
|
||||
|
||||
// Note: Comments are handled via Task Lists in body + first 100 comments.
|
||||
// If an issue has > 100 comments with task lists, we'd need to paginate those too.
|
||||
// Given the 1,100+ issue discovery count, 100 comments is usually sufficient,
|
||||
// but we can add it for absolute completeness.
|
||||
// (Skipping for now to avoid excessive API churn unless clearly needed).
|
||||
|
||||
return issue;
|
||||
} catch (error) {
|
||||
if (error.errors && error.errors.some((e) => e.type === 'NOT_FOUND')) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to paginate any GraphQL connection.
|
||||
*/
|
||||
async function paginateConnection(
|
||||
owner,
|
||||
repo,
|
||||
number,
|
||||
connectionName,
|
||||
nodeFields,
|
||||
initialCursor,
|
||||
transformNode = (n) => n,
|
||||
) {
|
||||
let additionalNodes = [];
|
||||
let hasNext = true;
|
||||
let cursor = initialCursor;
|
||||
|
||||
while (hasNext) {
|
||||
const query = `
|
||||
query($owner:String!, $repo:String!, $number:Int!, $cursor:String) {
|
||||
repository(owner:$owner, name:$repo) {
|
||||
issue(number:$number) {
|
||||
${connectionName}(first: 100, after: $cursor) {
|
||||
nodes { ${nodeFields} }
|
||||
pageInfo { hasNextPage endCursor }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const response = await octokit.graphql(query, {
|
||||
owner,
|
||||
repo,
|
||||
number,
|
||||
cursor,
|
||||
});
|
||||
const connection = response.repository.issue[connectionName];
|
||||
additionalNodes.push(...connection.nodes.map(transformNode));
|
||||
hasNext = connection.pageInfo.hasNextPage;
|
||||
cursor = connection.pageInfo.endCursor;
|
||||
}
|
||||
return additionalNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if an issue should be processed (Open, not a duplicate, not a PR)
|
||||
*/
|
||||
function shouldProcess(issueData) {
|
||||
if (!issueData) return false;
|
||||
|
||||
if (issueData.state !== 'OPEN') return false;
|
||||
|
||||
const labels = issueData.labels.map((l) => l.toLowerCase());
|
||||
if (labels.includes('duplicate') || labels.includes('kind/duplicate')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function getAllDescendants(roots) {
|
||||
const allDescendants = new Map();
|
||||
const visited = new Set();
|
||||
const queue = [...roots];
|
||||
|
||||
for (const root of roots) {
|
||||
visited.add(`${root.owner}/${root.repo}#${root.number}`);
|
||||
}
|
||||
|
||||
console.log(`Starting discovery from ${roots.length} roots...`);
|
||||
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift();
|
||||
const currentKey = `${current.owner}/${current.repo}#${current.number}`;
|
||||
|
||||
try {
|
||||
const issueData = await fetchIssueData(
|
||||
current.owner,
|
||||
current.repo,
|
||||
current.number,
|
||||
);
|
||||
|
||||
if (!shouldProcess(issueData)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// ONLY add to labeling list if it's in the PUBLIC repository
|
||||
if (current.repo === PUBLIC_REPO) {
|
||||
// Don't label the roots themselves
|
||||
if (
|
||||
!ROOT_ISSUES.some(
|
||||
(r) => r.number === current.number && r.repo === current.repo,
|
||||
)
|
||||
) {
|
||||
allDescendants.set(currentKey, {
|
||||
...current,
|
||||
title: issueData.title,
|
||||
labels: issueData.labels,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const children = new Map();
|
||||
|
||||
// 1. Process Native Sub-issues
|
||||
if (issueData.subIssues) {
|
||||
for (const node of issueData.subIssues) {
|
||||
const childOwner = node.repository.owner.login;
|
||||
const childRepo = node.repository.name;
|
||||
const childNumber = node.number;
|
||||
const key = `${childOwner}/${childRepo}#${childNumber}`;
|
||||
children.set(key, {
|
||||
owner: childOwner,
|
||||
repo: childRepo,
|
||||
number: childNumber,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Process Markdown Task Lists in Body and Comments
|
||||
let combinedText = issueData.body || '';
|
||||
if (issueData.comments) {
|
||||
for (const commentBody of issueData.comments) {
|
||||
combinedText += '\n' + (commentBody || '');
|
||||
}
|
||||
}
|
||||
|
||||
const taskListLinks = extractTaskListLinks(
|
||||
combinedText,
|
||||
current.owner,
|
||||
current.repo,
|
||||
);
|
||||
for (const link of taskListLinks) {
|
||||
const key = `${link.owner}/${link.repo}#${link.number}`;
|
||||
children.set(key, link);
|
||||
}
|
||||
|
||||
// Queue children (regardless of which repo they are in, for recursion)
|
||||
for (const [key, child] of children) {
|
||||
if (!visited.has(key)) {
|
||||
visited.add(key);
|
||||
queue.push(child);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing ${currentKey}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(allDescendants.values());
|
||||
}
|
||||
|
||||
async function run() {
|
||||
if (isDryRun) {
|
||||
console.log('=== DRY RUN MODE: No labels will be applied ===');
|
||||
}
|
||||
|
||||
const descendants = await getAllDescendants(ROOT_ISSUES);
|
||||
console.log(
|
||||
`\nFound ${descendants.length} total unique open descendant issues in ${PUBLIC_REPO}.`,
|
||||
);
|
||||
|
||||
for (const issueInfo of descendants) {
|
||||
const issueKey = `${issueInfo.owner}/${issueInfo.repo}#${issueInfo.number}`;
|
||||
try {
|
||||
// Data is already available from the discovery phase
|
||||
const hasLabel = issueInfo.labels.some((l) => l === TARGET_LABEL);
|
||||
|
||||
if (!hasLabel) {
|
||||
if (isDryRun) {
|
||||
console.log(
|
||||
`[DRY RUN] Would label ${issueKey}: "${issueInfo.title}"`,
|
||||
);
|
||||
} else {
|
||||
console.log(`Labeling ${issueKey}: "${issueInfo.title}"...`);
|
||||
await octokit.rest.issues.addLabels({
|
||||
owner: issueInfo.owner,
|
||||
repo: issueInfo.repo,
|
||||
issue_number: issueInfo.number,
|
||||
labels: [TARGET_LABEL],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Remove status/need-triage from maintainer-only issues since they
|
||||
// don't need community triage. We always attempt removal rather than
|
||||
// checking the (potentially stale) label snapshot, because the
|
||||
// issue-opened-labeler workflow runs concurrently and may add the
|
||||
// label after our snapshot was taken.
|
||||
if (isDryRun) {
|
||||
console.log(
|
||||
`[DRY RUN] Would remove status/need-triage from ${issueKey}`,
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
await octokit.rest.issues.removeLabel({
|
||||
owner: issueInfo.owner,
|
||||
repo: issueInfo.repo,
|
||||
issue_number: issueInfo.number,
|
||||
name: 'status/need-triage',
|
||||
});
|
||||
console.log(`Removed status/need-triage from ${issueKey}`);
|
||||
} catch (removeError) {
|
||||
// 404 means the label wasn't present — that's fine.
|
||||
if (removeError.status === 404) {
|
||||
console.log(
|
||||
`status/need-triage not present on ${issueKey}, skipping.`,
|
||||
);
|
||||
} else {
|
||||
throw removeError;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing label for ${issueKey}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||
|
||||
name: 'Agent Session Drift Check'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'release/**'
|
||||
paths:
|
||||
- 'packages/cli/src/nonInteractiveCli.ts'
|
||||
- 'packages/cli/src/nonInteractiveCliAgentSession.ts'
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check-drift:
|
||||
name: 'Check Agent Session Drift'
|
||||
runs-on: 'ubuntu-latest'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
permissions:
|
||||
contents: 'read'
|
||||
pull-requests: 'write'
|
||||
steps:
|
||||
- name: 'Detect drift and comment'
|
||||
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' # ratchet:actions/github-script@v8
|
||||
with:
|
||||
script: |-
|
||||
// === Pair configuration — append here to cover more pairs ===
|
||||
const PAIRS = [
|
||||
{
|
||||
legacy: 'packages/cli/src/nonInteractiveCli.ts',
|
||||
session: 'packages/cli/src/nonInteractiveCliAgentSession.ts',
|
||||
label: 'non-interactive CLI',
|
||||
},
|
||||
// Future pairs can be added here. Remember to also add both
|
||||
// paths to the `paths:` filter at the top of this workflow.
|
||||
// Example:
|
||||
// {
|
||||
// legacy: 'packages/core/src/agents/local-invocation.ts',
|
||||
// session: 'packages/core/src/agents/local-session-invocation.ts',
|
||||
// label: 'local subagent invocation',
|
||||
// },
|
||||
];
|
||||
// ============================================================
|
||||
|
||||
const prNumber = context.payload.pull_request.number;
|
||||
const { owner, repo } = context.repo;
|
||||
|
||||
// Use the API to list changed files — no checkout/git diff needed.
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner,
|
||||
repo,
|
||||
pull_number: prNumber,
|
||||
per_page: 100,
|
||||
});
|
||||
const changed = new Set(files.map((f) => f.filename));
|
||||
|
||||
const warnings = [];
|
||||
for (const { legacy, session, label } of PAIRS) {
|
||||
const legacyChanged = changed.has(legacy);
|
||||
const sessionChanged = changed.has(session);
|
||||
if (legacyChanged && !sessionChanged) {
|
||||
warnings.push(
|
||||
`**${label}**: \`${legacy}\` was modified but \`${session}\` was not.`,
|
||||
);
|
||||
} else if (!legacyChanged && sessionChanged) {
|
||||
warnings.push(
|
||||
`**${label}**: \`${session}\` was modified but \`${legacy}\` was not.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const MARKER = '<!-- agent-session-drift-check -->';
|
||||
|
||||
// Look up our existing drift comment (for upsert/cleanup).
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
per_page: 100,
|
||||
});
|
||||
const existing = comments.find(
|
||||
(c) => c.user?.type === 'Bot' && c.body?.includes(MARKER),
|
||||
);
|
||||
|
||||
if (warnings.length === 0) {
|
||||
core.info('No drift detected.');
|
||||
// If drift was previously flagged and is now resolved, remove the comment.
|
||||
if (existing) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: existing.id,
|
||||
});
|
||||
core.info(`Deleted stale drift comment ${existing.id}.`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const body = [
|
||||
MARKER,
|
||||
'### ⚠️ Invocation Drift Warning',
|
||||
'',
|
||||
'The following file pairs should generally be kept in sync during the AgentSession migration:',
|
||||
'',
|
||||
...warnings.map((w) => `- ${w}`),
|
||||
'',
|
||||
'If this is intentional (e.g., a bug fix specific to one implementation), you can ignore this comment.',
|
||||
'',
|
||||
'_This check will be removed once the legacy implementations are deleted._',
|
||||
].join('\n');
|
||||
|
||||
if (existing) {
|
||||
core.info(`Updating existing drift comment ${existing.id}.`);
|
||||
await github.rest.issues.updateComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
core.info('Creating new drift comment.');
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
body,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
name: 'Build Unsigned Mac Binaries'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
description: 'The branch, tag, or SHA to build from.'
|
||||
required: true
|
||||
type: 'string'
|
||||
|
||||
permissions:
|
||||
contents: 'read'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: 'bash'
|
||||
|
||||
jobs:
|
||||
build-mac:
|
||||
name: 'Build Unsigned (${{ matrix.arch }})'
|
||||
runs-on: 'macos-latest'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: ['x64', 'arm64']
|
||||
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4
|
||||
with:
|
||||
ref: '${{ inputs.ref || github.ref }}'
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Set up Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
architecture: '${{ matrix.arch }}'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Build Binary'
|
||||
env:
|
||||
SKIP_SIGNING: 'true'
|
||||
run: 'npm run build:binary'
|
||||
|
||||
- name: 'Verify Output Exists'
|
||||
run: |
|
||||
if [ -f "dist/darwin-${{ matrix.arch }}/gemini" ]; then
|
||||
echo "Binary found at dist/darwin-${{ matrix.arch }}/gemini"
|
||||
else
|
||||
echo "Error: Binary not found in dist/darwin-${{ matrix.arch }}/"
|
||||
ls -R dist/
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: 'Upload Artifact'
|
||||
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'gemini-darwin-${{ matrix.arch }}-unsigned'
|
||||
path: 'dist/darwin-${{ matrix.arch }}/gemini'
|
||||
retention-days: 14
|
||||
@@ -0,0 +1,406 @@
|
||||
name: 'Testing: E2E (Chained)'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
merge_group:
|
||||
workflow_run:
|
||||
workflows: ['Trigger E2E']
|
||||
types: ['completed']
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
head_sha:
|
||||
description: 'SHA of the commit to test'
|
||||
required: true
|
||||
repo_name:
|
||||
description: 'Repository name (e.g., owner/repo)'
|
||||
required: true
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.head_ref || github.event.workflow_run.head_branch || github.ref }}'
|
||||
cancel-in-progress: |-
|
||||
${{ github.event_name != 'push' && github.event_name != 'merge_group' }}
|
||||
|
||||
permissions:
|
||||
contents: 'read'
|
||||
statuses: 'write'
|
||||
|
||||
jobs:
|
||||
merge_queue_skipper:
|
||||
name: 'Merge Queue Skipper'
|
||||
permissions: 'read-all'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
outputs:
|
||||
skip: '${{ steps.merge-queue-e2e-skipper.outputs.skip-check }}'
|
||||
steps:
|
||||
- id: 'merge-queue-e2e-skipper'
|
||||
uses: 'cariad-tech/merge-queue-ci-skipper@1032489e59437862c90a08a2c92809c903883772' # ratchet:cariad-tech/merge-queue-ci-skipper@main
|
||||
with:
|
||||
secret: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
|
||||
continue-on-error: true
|
||||
|
||||
download_repo_name:
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_run')"
|
||||
outputs:
|
||||
repo_name: '${{ steps.output-repo-name.outputs.repo_name }}'
|
||||
head_sha: '${{ steps.output-repo-name.outputs.head_sha }}'
|
||||
steps:
|
||||
- name: 'Mock Repo Artifact'
|
||||
if: "${{ github.event_name == 'workflow_dispatch' }}"
|
||||
env:
|
||||
REPO_NAME: '${{ github.event.inputs.repo_name }}'
|
||||
run: |
|
||||
mkdir -p ./pr
|
||||
echo "${REPO_NAME}" > ./pr/repo_name
|
||||
- uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'repo_name'
|
||||
path: 'pr/'
|
||||
- name: 'Download the repo_name artifact'
|
||||
uses: 'actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0' # ratchet:actions/download-artifact@v5
|
||||
env:
|
||||
RUN_ID: "${{ github.event_name == 'workflow_run' && github.event.workflow_run.id || github.run_id }}"
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
name: 'repo_name'
|
||||
run-id: '${{ env.RUN_ID }}'
|
||||
path: '${{ runner.temp }}/artifacts'
|
||||
- name: 'Output Repo Name and SHA'
|
||||
id: 'output-repo-name'
|
||||
uses: 'actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd' # ratchet:actions/github-script@v8
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const temp = '${{ runner.temp }}/artifacts';
|
||||
const repoPath = path.join(temp, 'repo_name');
|
||||
if (fs.existsSync(repoPath)) {
|
||||
const repo_name = String(fs.readFileSync(repoPath)).trim();
|
||||
core.setOutput('repo_name', repo_name);
|
||||
}
|
||||
const shaPath = path.join(temp, 'head_sha');
|
||||
if (fs.existsSync(shaPath)) {
|
||||
const head_sha = String(fs.readFileSync(shaPath)).trim();
|
||||
core.setOutput('head_sha', head_sha);
|
||||
}
|
||||
|
||||
parse_run_context:
|
||||
name: 'Parse run context'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
needs: 'download_repo_name'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && always()"
|
||||
outputs:
|
||||
repository: '${{ steps.set_context.outputs.REPO }}'
|
||||
sha: '${{ steps.set_context.outputs.SHA }}'
|
||||
steps:
|
||||
- id: 'set_context'
|
||||
name: 'Set dynamic repository and SHA'
|
||||
env:
|
||||
REPO: '${{ needs.download_repo_name.outputs.repo_name || github.repository }}'
|
||||
SHA: '${{ needs.download_repo_name.outputs.head_sha || github.event.inputs.head_sha || github.event.workflow_run.head_sha || github.sha }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
echo "REPO=$REPO" >> "$GITHUB_OUTPUT"
|
||||
echo "SHA=$SHA" >> "$GITHUB_OUTPUT"
|
||||
|
||||
set_pending_status:
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
permissions: 'write-all'
|
||||
needs:
|
||||
- 'parse_run_context'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && always()"
|
||||
steps:
|
||||
- name: 'Set pending status'
|
||||
uses: 'myrotvorets/set-commit-status-action@16037e056d73b2d3c88e37e393ff369047f70886' # ratchet:myrotvorets/set-commit-status-action@master
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && always()"
|
||||
with:
|
||||
allowForks: 'true'
|
||||
repo: '${{ github.repository }}'
|
||||
sha: '${{ needs.parse_run_context.outputs.sha }}'
|
||||
token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
|
||||
status: 'pending'
|
||||
context: 'E2E (Chained)'
|
||||
|
||||
e2e_linux:
|
||||
name: 'E2E Test (Linux) - ${{ matrix.sandbox }}'
|
||||
needs:
|
||||
- 'merge_queue_skipper'
|
||||
- 'parse_run_context'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
if: |
|
||||
github.repository == 'google-gemini/gemini-cli' && always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
sandbox:
|
||||
- 'sandbox:none'
|
||||
- 'sandbox:docker'
|
||||
node-version:
|
||||
- '20.x'
|
||||
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
ref: '${{ needs.parse_run_context.outputs.sha }}'
|
||||
repository: '${{ needs.parse_run_context.outputs.repository }}'
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Set up Node.js ${{ matrix.node-version }}'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
|
||||
with:
|
||||
node-version: '${{ matrix.node-version }}'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Set up Docker'
|
||||
if: "${{matrix.sandbox == 'sandbox:docker'}}"
|
||||
uses: 'docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435' # ratchet:docker/setup-buildx-action@v3
|
||||
|
||||
- name: 'Run E2E tests'
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
GEMINI_CLI_TRUST_WORKSPACE: true
|
||||
KEEP_OUTPUT: 'true'
|
||||
VERBOSE: 'true'
|
||||
BUILD_SANDBOX_FLAGS: '--cache-from type=gha --cache-to type=gha,mode=max'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
if [[ "${{ matrix.sandbox }}" == "sandbox:docker" ]]; then
|
||||
npm run test:integration:sandbox:docker
|
||||
else
|
||||
npm run test:integration:sandbox:none
|
||||
fi
|
||||
|
||||
e2e_mac:
|
||||
name: 'E2E Test (macOS)'
|
||||
needs:
|
||||
- 'merge_queue_skipper'
|
||||
- 'parse_run_context'
|
||||
runs-on: 'macos-latest-large'
|
||||
if: |
|
||||
github.repository == 'google-gemini/gemini-cli' && always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
ref: '${{ needs.parse_run_context.outputs.sha }}'
|
||||
repository: '${{ needs.parse_run_context.outputs.repository }}'
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Set up Node.js 20.x'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Fix rollup optional dependencies on macOS'
|
||||
if: "${{runner.os == 'macOS'}}"
|
||||
run: |
|
||||
npm cache clean --force
|
||||
- name: 'Run E2E tests (non-Windows)'
|
||||
if: "${{runner.os != 'Windows'}}"
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
GEMINI_CLI_TRUST_WORKSPACE: true
|
||||
KEEP_OUTPUT: 'true'
|
||||
SANDBOX: 'sandbox:none'
|
||||
VERBOSE: 'true'
|
||||
run: 'npm run test:integration:sandbox:none'
|
||||
|
||||
e2e_windows:
|
||||
name: 'Slow E2E - Win'
|
||||
needs:
|
||||
- 'merge_queue_skipper'
|
||||
- 'parse_run_context'
|
||||
if: |
|
||||
github.repository == 'google-gemini/gemini-cli' && always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
|
||||
runs-on: 'gemini-cli-windows-16-core'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
ref: '${{ needs.parse_run_context.outputs.sha }}'
|
||||
repository: '${{ needs.parse_run_context.outputs.repository }}'
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Set up Node.js 20.x'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Configure Windows Defender exclusions'
|
||||
run: |
|
||||
Add-MpPreference -ExclusionPath $env:GITHUB_WORKSPACE -Force
|
||||
Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\node_modules" -Force
|
||||
Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\packages" -Force
|
||||
Add-MpPreference -ExclusionPath "$env:TEMP" -Force
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Configure npm for Windows performance'
|
||||
run: |
|
||||
npm config set progress false
|
||||
npm config set audit false
|
||||
npm config set fund false
|
||||
npm config set loglevel error
|
||||
npm config set maxsockets 32
|
||||
npm config set registry https://registry.npmjs.org/
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Ensure Chrome is available'
|
||||
shell: 'pwsh'
|
||||
run: |
|
||||
$chromePaths = @(
|
||||
"${env:ProgramFiles}\Google\Chrome\Application\chrome.exe",
|
||||
"${env:ProgramFiles(x86)}\Google\Chrome\Application\chrome.exe"
|
||||
)
|
||||
$chromeExists = $chromePaths | Where-Object { Test-Path $_ } | Select-Object -First 1
|
||||
if (-not $chromeExists) {
|
||||
Write-Host 'Chrome not found, installing via Chocolatey...'
|
||||
choco install googlechrome -y --no-progress --ignore-checksums
|
||||
}
|
||||
$installed = $chromePaths | Where-Object { Test-Path $_ } | Select-Object -First 1
|
||||
if ($installed) {
|
||||
Write-Host "Chrome found at: $installed"
|
||||
& $installed --version
|
||||
} else {
|
||||
Write-Error 'Chrome installation failed'
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: 'Run E2E tests'
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
GEMINI_CLI_TRUST_WORKSPACE: true
|
||||
KEEP_OUTPUT: 'true'
|
||||
SANDBOX: 'sandbox:none'
|
||||
VERBOSE: 'true'
|
||||
NODE_OPTIONS: '--max-old-space-size=32768 --max-semi-space-size=256'
|
||||
UV_THREADPOOL_SIZE: '32'
|
||||
NODE_ENV: 'test'
|
||||
shell: 'pwsh'
|
||||
run: 'npm run test:integration:sandbox:none'
|
||||
|
||||
evals:
|
||||
name: 'Evals (ALWAYS_PASSING)'
|
||||
needs:
|
||||
- 'merge_queue_skipper'
|
||||
- 'parse_run_context'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
if: |
|
||||
github.repository == 'google-gemini/gemini-cli' && always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
ref: '${{ needs.parse_run_context.outputs.sha }}'
|
||||
repository: '${{ needs.parse_run_context.outputs.repository }}'
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Set up Node.js 20.x'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Check if evals should run'
|
||||
id: 'check_evals'
|
||||
run: |
|
||||
SHOULD_RUN=$(node scripts/changed_prompt.js)
|
||||
echo "should_run=$SHOULD_RUN" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 'Run Evals (Required to pass)'
|
||||
if: "${{ steps.check_evals.outputs.should_run == 'true' }}"
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
GEMINI_CLI_TRUST_WORKSPACE: true
|
||||
GEMINI_MODEL: 'gemini-3-pro-preview'
|
||||
# Only run always passes behavioral tests.
|
||||
EVAL_SUITE_TYPE: 'behavioral'
|
||||
# Disable Vitest internal retries to avoid double-retrying;
|
||||
# custom retry logic is handled in evals/test-helper.ts
|
||||
VITEST_RETRY: 0
|
||||
run: 'npm run test:always_passing_evals'
|
||||
|
||||
- name: 'Upload Reliability Logs'
|
||||
if: "always() && steps.check_evals.outputs.should_run == 'true'"
|
||||
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'eval-logs-${{ github.run_id }}-${{ github.run_attempt }}'
|
||||
path: 'evals/logs/api-reliability.jsonl'
|
||||
retention-days: 7
|
||||
|
||||
e2e:
|
||||
name: 'E2E'
|
||||
if: |
|
||||
github.repository == 'google-gemini/gemini-cli' && always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
|
||||
needs:
|
||||
- 'e2e_linux'
|
||||
- 'e2e_mac'
|
||||
- 'e2e_windows'
|
||||
- 'evals'
|
||||
- 'merge_queue_skipper'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
steps:
|
||||
- name: 'Check E2E test results'
|
||||
run: |
|
||||
if [[ ${NEEDS_E2E_LINUX_RESULT} != 'success' || \
|
||||
${NEEDS_E2E_MAC_RESULT} != 'success' || \
|
||||
${NEEDS_E2E_WINDOWS_RESULT} != 'success' || \
|
||||
${NEEDS_EVALS_RESULT} != 'success' ]]; then
|
||||
echo "One or more E2E jobs failed."
|
||||
exit 1
|
||||
fi
|
||||
echo "All required E2E jobs passed!"
|
||||
env:
|
||||
NEEDS_E2E_LINUX_RESULT: '${{ needs.e2e_linux.result }}'
|
||||
NEEDS_E2E_MAC_RESULT: '${{ needs.e2e_mac.result }}'
|
||||
NEEDS_E2E_WINDOWS_RESULT: '${{ needs.e2e_windows.result }}'
|
||||
NEEDS_EVALS_RESULT: '${{ needs.evals.result }}'
|
||||
|
||||
set_workflow_status:
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
permissions: 'write-all'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && always()"
|
||||
needs:
|
||||
- 'parse_run_context'
|
||||
- 'e2e'
|
||||
steps:
|
||||
- name: 'Set workflow status'
|
||||
uses: 'myrotvorets/set-commit-status-action@16037e056d73b2d3c88e37e393ff369047f70886' # ratchet:myrotvorets/set-commit-status-action@master
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && always()"
|
||||
with:
|
||||
allowForks: 'true'
|
||||
repo: '${{ github.repository }}'
|
||||
sha: '${{ needs.parse_run_context.outputs.sha }}'
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
status: '${{ needs.e2e.result }}'
|
||||
context: 'E2E (Chained)'
|
||||
@@ -0,0 +1,518 @@
|
||||
name: 'Testing: CI'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'release/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'release/**'
|
||||
merge_group:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch_ref:
|
||||
description: 'Branch to run on'
|
||||
required: true
|
||||
default: 'main'
|
||||
type: 'string'
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
|
||||
cancel-in-progress: |-
|
||||
${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/heads/release/') }}
|
||||
|
||||
permissions:
|
||||
checks: 'write'
|
||||
contents: 'read'
|
||||
statuses: 'write'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: 'bash'
|
||||
|
||||
jobs:
|
||||
merge_queue_skipper:
|
||||
permissions: 'read-all'
|
||||
name: 'Merge Queue Skipper'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
outputs:
|
||||
skip: '${{ steps.merge-queue-ci-skipper.outputs.skip-check }}'
|
||||
steps:
|
||||
- id: 'merge-queue-ci-skipper'
|
||||
uses: 'cariad-tech/merge-queue-ci-skipper@1032489e59437862c90a08a2c92809c903883772' # ratchet:cariad-tech/merge-queue-ci-skipper@main
|
||||
with:
|
||||
secret: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
|
||||
|
||||
lint:
|
||||
name: 'Lint'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
needs: 'merge_queue_skipper'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && needs.merge_queue_skipper.outputs.skip == 'false'"
|
||||
env:
|
||||
GEMINI_LINT_TEMP_DIR: '${{ github.workspace }}/.gemini-linters'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: '${{ github.event.inputs.branch_ref || github.ref }}'
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Set up Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Cache Linters'
|
||||
uses: 'actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830' # ratchet:actions/cache@v4
|
||||
with:
|
||||
path: '${{ env.GEMINI_LINT_TEMP_DIR }}'
|
||||
key: "${{ runner.os }}-${{ runner.arch }}-linters-${{ hashFiles('scripts/lint.js') }}"
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Cache ESLint'
|
||||
uses: 'actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830' # ratchet:actions/cache@v4
|
||||
with:
|
||||
path: '.eslintcache'
|
||||
key: "${{ runner.os }}-eslint-${{ hashFiles('package-lock.json', 'eslint.config.js') }}"
|
||||
|
||||
- name: 'Validate NOTICES.txt'
|
||||
run: 'git diff --exit-code packages/vscode-ide-companion/NOTICES.txt'
|
||||
|
||||
- name: 'Check lockfile'
|
||||
run: 'npm run check:lockfile'
|
||||
|
||||
- name: 'Install linters'
|
||||
run: 'node scripts/lint.js --setup'
|
||||
|
||||
- name: 'Run ESLint'
|
||||
run: 'node scripts/lint.js --eslint'
|
||||
|
||||
- name: 'Run actionlint'
|
||||
run: 'node scripts/lint.js --actionlint'
|
||||
|
||||
- name: 'Run shellcheck'
|
||||
run: 'node scripts/lint.js --shellcheck'
|
||||
|
||||
- name: 'Run yamllint'
|
||||
run: 'node scripts/lint.js --yamllint'
|
||||
|
||||
- name: 'Build project for typecheck'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Run typecheck'
|
||||
run: 'npm run typecheck'
|
||||
|
||||
- name: 'Run Prettier'
|
||||
run: 'node scripts/lint.js --prettier'
|
||||
|
||||
- name: 'Build docs prerequisites'
|
||||
run: 'npm run predocs:settings'
|
||||
|
||||
- name: 'Verify settings docs'
|
||||
run: 'npm run docs:settings -- --check'
|
||||
|
||||
- name: 'Run sensitive keyword linter'
|
||||
run: 'node scripts/lint.js --sensitive-keywords'
|
||||
|
||||
- name: 'Run GitHub Actions pinning linter'
|
||||
run: 'node scripts/lint.js --check-github-actions-pinning'
|
||||
|
||||
link_checker:
|
||||
name: 'Link Checker'
|
||||
runs-on: 'ubuntu-latest'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: 'Link Checker'
|
||||
uses: 'lycheeverse/lychee-action@885c65f3dc543b57c898c8099f4e08c8afd178a2' # ratchet: lycheeverse/lychee-action@v2.6.1
|
||||
with:
|
||||
args: '--verbose --accept 200,503 ./**/*.md'
|
||||
fail: true
|
||||
test_linux:
|
||||
name: 'Test (Linux) - ${{ matrix.node-version }}, ${{ matrix.shard }}'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
needs:
|
||||
- 'merge_queue_skipper'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && needs.merge_queue_skipper.outputs.skip == 'false'"
|
||||
permissions:
|
||||
contents: 'read'
|
||||
checks: 'write'
|
||||
pull-requests: 'write'
|
||||
strategy:
|
||||
matrix:
|
||||
node-version:
|
||||
- '20.x'
|
||||
- '22.x'
|
||||
- '24.x'
|
||||
shard:
|
||||
- 'cli'
|
||||
- 'others'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Set up Node.js ${{ matrix.node-version }}'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version: '${{ matrix.node-version }}'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Install system dependencies'
|
||||
run: |
|
||||
sudo apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq bubblewrap
|
||||
# Ubuntu 24.04+ requires this to allow bwrap to function in CI
|
||||
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
|
||||
|
||||
- name: 'Install dependencies for testing'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Run tests and generate reports'
|
||||
env:
|
||||
NO_COLOR: true
|
||||
GEMINI_CLI_TRUST_WORKSPACE: true
|
||||
run: |
|
||||
if [[ "${{ matrix.shard }}" == "cli" ]]; then
|
||||
npm run test:ci --workspace "@google/gemini-cli"
|
||||
else
|
||||
# Explicitly list non-cli packages to ensure they are sharded correctly
|
||||
npm run test:ci --workspace "@google/gemini-cli-core" --workspace "@google/gemini-cli-a2a-server" --workspace "gemini-cli-vscode-ide-companion" --workspace "@google/gemini-cli-test-utils" --if-present -- --coverage.enabled=false
|
||||
npm run test:scripts
|
||||
fi
|
||||
|
||||
- name: 'Bundle'
|
||||
run: 'npm run bundle'
|
||||
|
||||
- name: 'Smoke test bundle'
|
||||
run: 'node ./bundle/gemini.js --version'
|
||||
|
||||
- name: 'Smoke test npx installation'
|
||||
run: |
|
||||
# 1. Package the project into a tarball
|
||||
TARBALL=$(npm pack | tail -n 1)
|
||||
|
||||
# 2. Move to a fresh directory for isolation
|
||||
mkdir -p ../smoke-test-dir
|
||||
mv "$TARBALL" ../smoke-test-dir/
|
||||
cd ../smoke-test-dir
|
||||
|
||||
# 3. Run npx from the tarball
|
||||
npx "./$TARBALL" --version
|
||||
|
||||
- name: 'Wait for file system sync'
|
||||
run: 'sleep 2'
|
||||
|
||||
- name: 'Publish Test Report (for non-forks)'
|
||||
if: |-
|
||||
${{ always() && (github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||
uses: 'dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3' # ratchet:dorny/test-reporter@v2
|
||||
with:
|
||||
name: 'Test Results (Node ${{ runner.os }}, ${{ matrix.node-version }}, ${{ matrix.shard }})'
|
||||
path: 'packages/*/junit.xml'
|
||||
reporter: 'java-junit'
|
||||
fail-on-error: 'false'
|
||||
|
||||
- name: 'Upload Test Results Artifact (for forks)'
|
||||
if: |-
|
||||
${{ always() && (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) }}
|
||||
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'test-results-fork-${{ runner.os }}-${{ matrix.node-version }}-${{ matrix.shard }}'
|
||||
path: 'packages/*/junit.xml'
|
||||
|
||||
test_mac:
|
||||
name: 'Test (Mac) - ${{ matrix.node-version }}, ${{ matrix.shard }}'
|
||||
runs-on: 'macos-latest-large'
|
||||
needs:
|
||||
- 'merge_queue_skipper'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && needs.merge_queue_skipper.outputs.skip == 'false'"
|
||||
permissions:
|
||||
contents: 'read'
|
||||
checks: 'write'
|
||||
pull-requests: 'write'
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
matrix:
|
||||
node-version:
|
||||
- '20.x'
|
||||
- '22.x'
|
||||
- '24.x'
|
||||
shard:
|
||||
- 'cli'
|
||||
- 'others'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Set up Node.js ${{ matrix.node-version }}'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version: '${{ matrix.node-version }}'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Install dependencies for testing'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Run tests and generate reports'
|
||||
env:
|
||||
NO_COLOR: true
|
||||
GEMINI_CLI_TRUST_WORKSPACE: true
|
||||
run: |
|
||||
if [[ "${{ matrix.shard }}" == "cli" ]]; then
|
||||
npm run test:ci --workspace "@google/gemini-cli" -- --coverage.enabled=false
|
||||
else
|
||||
# Explicitly list non-cli packages to ensure they are sharded correctly
|
||||
npm run test:ci --workspace "@google/gemini-cli-core" --workspace "@google/gemini-cli-a2a-server" --workspace "gemini-cli-vscode-ide-companion" --workspace "@google/gemini-cli-test-utils" --if-present -- --coverage.enabled=false
|
||||
npm run test:scripts
|
||||
fi
|
||||
|
||||
- name: 'Bundle'
|
||||
run: 'npm run bundle'
|
||||
|
||||
- name: 'Smoke test bundle'
|
||||
run: 'node ./bundle/gemini.js --version'
|
||||
|
||||
- name: 'Smoke test npx installation'
|
||||
run: |
|
||||
# 1. Package the project into a tarball
|
||||
TARBALL=$(npm pack | tail -n 1)
|
||||
|
||||
# 2. Move to a fresh directory for isolation
|
||||
mkdir -p ../smoke-test-dir
|
||||
mv "$TARBALL" ../smoke-test-dir/
|
||||
cd ../smoke-test-dir
|
||||
|
||||
# 3. Run npx from the tarball
|
||||
npx "./$TARBALL" --version
|
||||
|
||||
- name: 'Wait for file system sync'
|
||||
run: 'sleep 2'
|
||||
|
||||
- name: 'Publish Test Report (for non-forks)'
|
||||
if: |-
|
||||
${{ always() && (github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||
uses: 'dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3' # ratchet:dorny/test-reporter@v2
|
||||
with:
|
||||
name: 'Test Results (Node ${{ runner.os }}, ${{ matrix.node-version }}, ${{ matrix.shard }})'
|
||||
path: 'packages/*/junit.xml'
|
||||
reporter: 'java-junit'
|
||||
fail-on-error: 'false'
|
||||
|
||||
- name: 'Upload Test Results Artifact (for forks)'
|
||||
if: |-
|
||||
${{ always() && (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) }}
|
||||
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'test-results-fork-${{ runner.os }}-${{ matrix.node-version }}-${{ matrix.shard }}'
|
||||
path: 'packages/*/junit.xml'
|
||||
|
||||
- name: 'Upload coverage reports'
|
||||
if: |-
|
||||
${{ always() }}
|
||||
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'coverage-reports-${{ runner.os }}-${{ matrix.node-version }}-${{ matrix.shard }}'
|
||||
path: 'packages/*/coverage'
|
||||
|
||||
codeql:
|
||||
name: 'CodeQL'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
needs: 'merge_queue_skipper'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && needs.merge_queue_skipper.outputs.skip == 'false'"
|
||||
permissions:
|
||||
actions: 'read'
|
||||
contents: 'read'
|
||||
security-events: 'write'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: '${{ github.event.inputs.branch_ref || github.ref }}'
|
||||
|
||||
- name: 'Initialize CodeQL'
|
||||
uses: 'github/codeql-action/init@df559355d593797519d70b90fc8edd5db049e7a2' # ratchet:github/codeql-action/init@v3
|
||||
with:
|
||||
languages: 'javascript'
|
||||
|
||||
- name: 'Perform CodeQL Analysis'
|
||||
uses: 'github/codeql-action/analyze@df559355d593797519d70b90fc8edd5db049e7a2' # ratchet:github/codeql-action/analyze@v3
|
||||
|
||||
# Check for changes in bundle size.
|
||||
bundle_size:
|
||||
name: 'Check Bundle Size'
|
||||
needs: 'merge_queue_skipper'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && github.event_name == 'pull_request' && needs.merge_queue_skipper.outputs.skip == 'false'"
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
permissions:
|
||||
contents: 'read' # For checkout
|
||||
pull-requests: 'write' # For commenting
|
||||
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: '${{ github.event.inputs.branch_ref || github.ref }}'
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: 'preactjs/compressed-size-action@946a292cd35bd1088e0d7eb92b69d1a8d5b5d76a'
|
||||
with:
|
||||
repo-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
pattern: './bundle/**/*.{js,sb}'
|
||||
minimum-change-threshold: '1000'
|
||||
compression: 'none'
|
||||
clean-script: 'clean'
|
||||
|
||||
test_windows:
|
||||
name: 'Slow Test - Win - ${{ matrix.shard }}'
|
||||
runs-on: 'gemini-cli-windows-16-core'
|
||||
needs: 'merge_queue_skipper'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && needs.merge_queue_skipper.outputs.skip == 'false'"
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
matrix:
|
||||
shard:
|
||||
- 'cli'
|
||||
- 'others'
|
||||
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: '${{ github.event.inputs.branch_ref || github.ref }}'
|
||||
|
||||
- name: 'Set up Node.js 20.x'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Configure Windows Defender exclusions'
|
||||
run: |
|
||||
Add-MpPreference -ExclusionPath $env:GITHUB_WORKSPACE -Force
|
||||
Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\node_modules" -Force
|
||||
Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\packages" -Force
|
||||
Add-MpPreference -ExclusionPath "$env:TEMP" -Force
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Configure npm for Windows performance'
|
||||
run: |
|
||||
npm config set progress false
|
||||
npm config set audit false
|
||||
npm config set fund false
|
||||
npm config set loglevel error
|
||||
npm config set maxsockets 32
|
||||
npm config set registry https://registry.npmjs.org/
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
shell: 'pwsh'
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=32768 --max-semi-space-size=256'
|
||||
UV_THREADPOOL_SIZE: '32'
|
||||
NODE_ENV: 'production'
|
||||
|
||||
- name: 'Run tests and generate reports'
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
NO_COLOR: true
|
||||
GEMINI_CLI_TRUST_WORKSPACE: true
|
||||
NODE_OPTIONS: '--max-old-space-size=32768 --max-semi-space-size=256'
|
||||
UV_THREADPOOL_SIZE: '32'
|
||||
NODE_ENV: 'test'
|
||||
run: |
|
||||
if ("${{ matrix.shard }}" -eq "cli") {
|
||||
npm run test:ci --workspace "@google/gemini-cli" -- --coverage.enabled=false
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
} else {
|
||||
# Explicitly list non-cli packages to ensure they are sharded correctly
|
||||
npm run test:ci --workspace "@google/gemini-cli-core" --workspace "@google/gemini-cli-a2a-server" --workspace "gemini-cli-vscode-ide-companion" --workspace "@google/gemini-cli-test-utils" --if-present -- --coverage.enabled=false
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
npm run test:scripts
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
}
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Bundle'
|
||||
run: 'npm run bundle'
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Smoke test bundle'
|
||||
run: 'node ./bundle/gemini.js --version'
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Smoke test npx installation'
|
||||
run: |
|
||||
# 1. Package the project into a tarball
|
||||
$PACK_OUTPUT = npm pack
|
||||
$TARBALL = $PACK_OUTPUT[-1]
|
||||
|
||||
# 2. Move to a fresh directory for isolation
|
||||
New-Item -ItemType Directory -Force -Path ../smoke-test-dir
|
||||
Move-Item $TARBALL ../smoke-test-dir/
|
||||
Set-Location ../smoke-test-dir
|
||||
|
||||
# 3. Run npx from the tarball
|
||||
npx "./$TARBALL" --version
|
||||
shell: 'pwsh'
|
||||
|
||||
ci:
|
||||
name: 'CI'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && always()"
|
||||
needs:
|
||||
- 'lint'
|
||||
- 'link_checker'
|
||||
- 'test_linux'
|
||||
- 'test_mac'
|
||||
- 'test_windows'
|
||||
- 'codeql'
|
||||
- 'bundle_size'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
steps:
|
||||
- name: 'Check all job results'
|
||||
run: |
|
||||
if [[ (${NEEDS_LINT_RESULT} != 'success' && ${NEEDS_LINT_RESULT} != 'skipped') || \
|
||||
(${NEEDS_LINK_CHECKER_RESULT} != 'success' && ${NEEDS_LINK_CHECKER_RESULT} != 'skipped') || \
|
||||
(${NEEDS_TEST_LINUX_RESULT} != 'success' && ${NEEDS_TEST_LINUX_RESULT} != 'skipped') || \
|
||||
(${NEEDS_TEST_MAC_RESULT} != 'success' && ${NEEDS_TEST_MAC_RESULT} != 'skipped') || \
|
||||
(${NEEDS_TEST_WINDOWS_RESULT} != 'success' && ${NEEDS_TEST_WINDOWS_RESULT} != 'skipped') || \
|
||||
(${NEEDS_CODEQL_RESULT} != 'success' && ${NEEDS_CODEQL_RESULT} != 'skipped') || \
|
||||
(${NEEDS_BUNDLE_SIZE_RESULT} != 'success' && ${NEEDS_BUNDLE_SIZE_RESULT} != 'skipped') ]]; then
|
||||
echo "One or more CI jobs failed."
|
||||
exit 1
|
||||
fi
|
||||
echo "All CI jobs passed!"
|
||||
env:
|
||||
NEEDS_LINT_RESULT: '${{ needs.lint.result }}'
|
||||
NEEDS_LINK_CHECKER_RESULT: '${{ needs.link_checker.result }}'
|
||||
NEEDS_TEST_LINUX_RESULT: '${{ needs.test_linux.result }}'
|
||||
NEEDS_TEST_MAC_RESULT: '${{ needs.test_mac.result }}'
|
||||
NEEDS_TEST_WINDOWS_RESULT: '${{ needs.test_windows.result }}'
|
||||
NEEDS_CODEQL_RESULT: '${{ needs.codeql.result }}'
|
||||
NEEDS_BUNDLE_SIZE_RESULT: '${{ needs.bundle_size.result }}'
|
||||
@@ -0,0 +1,200 @@
|
||||
name: 'Generate Weekly Community Report 📊'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 12 * * 1' # Run at 12:00 UTC on Monday
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
days:
|
||||
description: 'Number of days to look back for the report'
|
||||
required: true
|
||||
default: '7'
|
||||
|
||||
jobs:
|
||||
generate-report:
|
||||
name: 'Generate Report 📝'
|
||||
if: |-
|
||||
${{ github.repository == 'google-gemini/gemini-cli' }}
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
issues: 'write'
|
||||
pull-requests: 'read'
|
||||
discussions: 'read'
|
||||
contents: 'read'
|
||||
id-token: 'write'
|
||||
|
||||
steps:
|
||||
- name: 'Generate GitHub App Token 🔑'
|
||||
id: 'generate_token'
|
||||
uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b' # ratchet:actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: '${{ secrets.APP_ID }}'
|
||||
private-key: '${{ secrets.PRIVATE_KEY }}'
|
||||
permission-issues: 'write'
|
||||
permission-pull-requests: 'read'
|
||||
permission-discussions: 'read'
|
||||
permission-contents: 'read'
|
||||
|
||||
- name: 'Generate Report 📜'
|
||||
id: 'report'
|
||||
env:
|
||||
GH_TOKEN: '${{ steps.generate_token.outputs.token }}'
|
||||
REPO: '${{ github.repository }}'
|
||||
DAYS: '${{ github.event.inputs.days || 7 }}'
|
||||
run: |-
|
||||
set -e
|
||||
|
||||
START_DATE="$(date -u -d "$DAYS days ago" +'%Y-%m-%d')"
|
||||
END_DATE="$(date -u +'%Y-%m-%d')"
|
||||
echo "⏳ Generating report for contributions from ${START_DATE} to ${END_DATE}..."
|
||||
|
||||
declare -A author_is_googler
|
||||
check_googler_status() {
|
||||
local author="$1"
|
||||
if [[ "${author}" == *"[bot]" ]]; then
|
||||
author_is_googler[${author}]=1
|
||||
return 1
|
||||
fi
|
||||
if [[ -v "author_is_googler[${author}]" ]]; then
|
||||
return "${author_is_googler[${author}]}"
|
||||
fi
|
||||
|
||||
if gh api "orgs/googlers/members/${author}" --silent 2>/dev/null; then
|
||||
echo "🧑💻 ${author} is a Googler."
|
||||
author_is_googler[${author}]=0
|
||||
else
|
||||
echo "🌍 ${author} is a community contributor."
|
||||
author_is_googler[${author}]=1
|
||||
fi
|
||||
return "${author_is_googler[${author}]}"
|
||||
}
|
||||
|
||||
googler_issues=0
|
||||
non_googler_issues=0
|
||||
googler_prs=0
|
||||
non_googler_prs=0
|
||||
|
||||
echo "🔎 Fetching issues and pull requests..."
|
||||
ITEMS_JSON="$(gh search issues --repo "${REPO}" "created:>${START_DATE}" --json author,isPullRequest --limit 1000)"
|
||||
|
||||
for row in $(echo "${ITEMS_JSON}" | jq -r '.[] | @base64'); do
|
||||
_jq() {
|
||||
echo "${row}" | base64 --decode | jq -r "${1}"
|
||||
}
|
||||
author="$(_jq '.author.login')"
|
||||
is_pr="$(_jq '.isPullRequest')"
|
||||
|
||||
if [[ -z "${author}" || "${author}" == "null" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if check_googler_status "${author}"; then
|
||||
if [[ "${is_pr}" == "true" ]]; then
|
||||
((googler_prs++))
|
||||
else
|
||||
((googler_issues++))
|
||||
fi
|
||||
else
|
||||
if [[ "${is_pr}" == "true" ]]; then
|
||||
((non_googler_prs++))
|
||||
else
|
||||
((non_googler_issues++))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
googler_discussions=0
|
||||
non_googler_discussions=0
|
||||
|
||||
echo "🗣️ Fetching discussions..."
|
||||
DISCUSSION_QUERY='''
|
||||
query($q: String!) {
|
||||
search(query: $q, type: DISCUSSION, first: 100) {
|
||||
nodes {
|
||||
... on Discussion {
|
||||
author {
|
||||
login
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}'''
|
||||
DISCUSSIONS_JSON="$(gh api graphql -f q="repo:${REPO} created:>${START_DATE}" -f query="${DISCUSSION_QUERY}")"
|
||||
|
||||
for row in $(echo "${DISCUSSIONS_JSON}" | jq -r '.data.search.nodes[] | @base64'); do
|
||||
_jq() {
|
||||
echo "${row}" | base64 --decode | jq -r "${1}"
|
||||
}
|
||||
author="$(_jq '.author.login')"
|
||||
|
||||
if [[ -z "${author}" || "${author}" == "null" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if check_googler_status "${author}"; then
|
||||
((googler_discussions++))
|
||||
else
|
||||
((non_googler_discussions++))
|
||||
fi
|
||||
done
|
||||
|
||||
echo "✍️ Generating report content..."
|
||||
TOTAL_ISSUES=$((googler_issues + non_googler_issues))
|
||||
TOTAL_PRS=$((googler_prs + non_googler_prs))
|
||||
TOTAL_DISCUSSIONS=$((googler_discussions + non_googler_discussions))
|
||||
|
||||
REPORT_BODY=$(cat <<EOF
|
||||
### 💖 Community Contribution Report
|
||||
|
||||
**Period:** ${START_DATE} to ${END_DATE}
|
||||
|
||||
| Category | Googlers | Community | Total |
|
||||
|---|---:|---:|---:|
|
||||
| **Issues** | $googler_issues | $non_googler_issues | **$TOTAL_ISSUES** |
|
||||
| **Pull Requests** | $googler_prs | $non_googler_prs | **$TOTAL_PRS** |
|
||||
| **Discussions** | $googler_discussions | $non_googler_discussions | **$TOTAL_DISCUSSIONS** |
|
||||
|
||||
_This report was generated automatically by a GitHub Action._
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "report_body<<EOF" >> "${GITHUB_OUTPUT}"
|
||||
echo "${REPORT_BODY}" >> "${GITHUB_OUTPUT}"
|
||||
echo "EOF" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
echo "📊 Community Contribution Report:"
|
||||
echo "${REPORT_BODY}"
|
||||
|
||||
- name: '🤖 Get Insights from Report'
|
||||
if: |-
|
||||
${{ steps.report.outputs.report_body != '' }}
|
||||
uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ steps.generate_token.outputs.token }}'
|
||||
REPOSITORY: '${{ github.repository }}'
|
||||
with:
|
||||
upload_artifacts: 'true'
|
||||
gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
|
||||
gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
|
||||
gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}'
|
||||
gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
|
||||
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
|
||||
use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}'
|
||||
use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
|
||||
settings: |-
|
||||
{
|
||||
"tools": {
|
||||
"core": [
|
||||
"run_shell_command(gh issue list)",
|
||||
"run_shell_command(gh pr list)",
|
||||
"run_shell_command(gh search issues)",
|
||||
"run_shell_command(gh search prs)"
|
||||
]
|
||||
}
|
||||
}
|
||||
prompt: |-
|
||||
You are a helpful assistant that analyzes community contribution reports.
|
||||
Based on the following report, please provide a brief summary and highlight any interesting trends or potential areas for improvement.
|
||||
|
||||
Report:
|
||||
${{ steps.report.outputs.report_body }}
|
||||
@@ -0,0 +1,178 @@
|
||||
name: 'Deflake E2E'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch_ref:
|
||||
description: 'Branch to run on'
|
||||
required: true
|
||||
default: 'main'
|
||||
type: 'string'
|
||||
test_name_pattern:
|
||||
description: 'The test name pattern to use'
|
||||
required: false
|
||||
type: 'string'
|
||||
runs:
|
||||
description: 'The number of runs'
|
||||
required: false
|
||||
default: 5
|
||||
type: 'number'
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
|
||||
cancel-in-progress: |-
|
||||
${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/heads/release/') }}
|
||||
|
||||
jobs:
|
||||
deflake_e2e_linux:
|
||||
name: 'E2E Test (Linux) - ${{ matrix.sandbox }}'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
sandbox:
|
||||
- 'sandbox:none'
|
||||
- 'sandbox:docker'
|
||||
node-version:
|
||||
- '20.x'
|
||||
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
ref: '${{ github.event.pull_request.head.sha }}'
|
||||
repository: '${{ github.repository }}'
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Set up Node.js ${{ matrix.node-version }}'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
|
||||
with:
|
||||
node-version: '${{ matrix.node-version }}'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Set up Docker'
|
||||
if: "matrix.sandbox == 'sandbox:docker'"
|
||||
uses: 'docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435' # ratchet:docker/setup-buildx-action@v3
|
||||
|
||||
- name: 'Run E2E tests'
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
GEMINI_CLI_TRUST_WORKSPACE: true
|
||||
IS_DOCKER: "${{ matrix.sandbox == 'sandbox:docker' }}"
|
||||
KEEP_OUTPUT: 'true'
|
||||
RUNS: '${{ github.event.inputs.runs }}'
|
||||
TEST_NAME_PATTERN: '${{ github.event.inputs.test_name_pattern }}'
|
||||
VERBOSE: 'true'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
if [[ "${IS_DOCKER}" == "true" ]]; then
|
||||
npm run deflake:test:integration:sandbox:docker -- --runs="${RUNS}" -- --testNamePattern "'${TEST_NAME_PATTERN}'"
|
||||
else
|
||||
npm run deflake:test:integration:sandbox:none -- --runs="${RUNS}" -- --testNamePattern "'${TEST_NAME_PATTERN}'"
|
||||
fi
|
||||
|
||||
deflake_e2e_mac:
|
||||
name: 'E2E Test (macOS)'
|
||||
runs-on: 'macos-latest-large'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
ref: '${{ github.event.pull_request.head.sha }}'
|
||||
repository: '${{ github.repository }}'
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Set up Node.js 20.x'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Fix rollup optional dependencies on macOS'
|
||||
if: "runner.os == 'macOS'"
|
||||
run: |
|
||||
npm cache clean --force
|
||||
- name: 'Run E2E tests (non-Windows)'
|
||||
if: "runner.os != 'Windows'"
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
GEMINI_CLI_TRUST_WORKSPACE: true
|
||||
KEEP_OUTPUT: 'true'
|
||||
RUNS: '${{ github.event.inputs.runs }}'
|
||||
SANDBOX: 'sandbox:none'
|
||||
TEST_NAME_PATTERN: '${{ github.event.inputs.test_name_pattern }}'
|
||||
VERBOSE: 'true'
|
||||
run: |
|
||||
npm run deflake:test:integration:sandbox:none -- --runs="${RUNS}" -- --testNamePattern "'${TEST_NAME_PATTERN}'"
|
||||
|
||||
deflake_e2e_windows:
|
||||
name: 'Slow E2E - Win'
|
||||
runs-on: 'gemini-cli-windows-16-core'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
ref: '${{ github.event.pull_request.head.sha }}'
|
||||
repository: '${{ github.repository }}'
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Set up Node.js 20.x'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Configure Windows Defender exclusions'
|
||||
run: |
|
||||
Add-MpPreference -ExclusionPath $env:GITHUB_WORKSPACE -Force
|
||||
Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\node_modules" -Force
|
||||
Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\packages" -Force
|
||||
Add-MpPreference -ExclusionPath "$env:TEMP" -Force
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Configure npm for Windows performance'
|
||||
run: |
|
||||
npm config set progress false
|
||||
npm config set audit false
|
||||
npm config set fund false
|
||||
npm config set loglevel error
|
||||
npm config set maxsockets 32
|
||||
npm config set registry https://registry.npmjs.org/
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Run E2E tests'
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
GEMINI_CLI_TRUST_WORKSPACE: true
|
||||
KEEP_OUTPUT: 'true'
|
||||
SANDBOX: 'sandbox:none'
|
||||
VERBOSE: 'true'
|
||||
NODE_OPTIONS: '--max-old-space-size=32768 --max-semi-space-size=256'
|
||||
UV_THREADPOOL_SIZE: '32'
|
||||
NODE_ENV: 'test'
|
||||
RUNS: '${{ github.event.inputs.runs }}'
|
||||
TEST_NAME_PATTERN: '${{ github.event.inputs.test_name_pattern }}'
|
||||
shell: 'pwsh'
|
||||
run: |
|
||||
npm run deflake:test:integration:sandbox:none -- --runs="$env:RUNS" -- --testNamePattern "'$env:TEST_NAME_PATTERN'"
|
||||
@@ -0,0 +1,66 @@
|
||||
name: 'Automated Documentation Audit'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Runs every Monday at 00:00 UTC
|
||||
- cron: '0 0 * * MON'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
audit-docs:
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
contents: 'write'
|
||||
pull-requests: 'write'
|
||||
|
||||
steps:
|
||||
- name: 'Checkout repository'
|
||||
uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5'
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: 'main'
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Set up Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: 'Run Docs Audit with Gemini'
|
||||
id: 'run_gemini'
|
||||
uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31'
|
||||
env:
|
||||
GEMINI_CLI_TRUST_WORKSPACE: true
|
||||
with:
|
||||
upload_artifacts: 'true'
|
||||
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
|
||||
prompt: |
|
||||
Activate the 'docs-writer' skill.
|
||||
|
||||
**Task:** Execute the docs audit procedure, as defined in your 'docs-auditing.md' reference.
|
||||
Provide a detailed summary of the changes you make.
|
||||
|
||||
- name: 'Get current date'
|
||||
id: 'date'
|
||||
run: |
|
||||
echo "date=$(date +'%Y-%m-%d')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 'Create Pull Request with Audit Results'
|
||||
uses: 'peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c'
|
||||
with:
|
||||
token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
|
||||
commit-message: 'docs: weekly audit results for ${{ github.run_id }}'
|
||||
title: 'Docs audit: ${{ steps.date.outputs.date }}'
|
||||
body: |
|
||||
This PR contains the auto-generated documentation audit for the week. It includes a new `audit-results-*.md` file with findings and any direct fixes applied by the agent.
|
||||
|
||||
### Audit Summary:
|
||||
${{ steps.run_gemini.outputs.summary || 'No summary provided.' }}
|
||||
|
||||
Please review the suggestions and merge.
|
||||
|
||||
Related to #25152
|
||||
branch: 'docs-audit-${{ github.run_id }}'
|
||||
base: 'main'
|
||||
team-reviewers: 'gemini-cli-docs, gemini-cli-maintainers'
|
||||
delete-branch: true
|
||||
@@ -0,0 +1,52 @@
|
||||
name: 'Deploy GitHub Pages'
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: 'read'
|
||||
pages: 'write'
|
||||
id-token: 'write'
|
||||
|
||||
# Allow only one concurrent deployment, skipping runs queued between the run
|
||||
# in-progress and latest queued. However, do NOT cancel in-progress runs as we
|
||||
# want to allow these production deployments to complete.
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}'
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && !contains(github.ref_name, 'nightly')"
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Setup Pages'
|
||||
uses: 'actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b' # ratchet:actions/configure-pages@v5
|
||||
|
||||
- name: 'Build with Jekyll'
|
||||
uses: 'actions/jekyll-build-pages@44a6e6beabd48582f863aeeb6cb2151cc1716697' # ratchet:actions/jekyll-build-pages@v1
|
||||
with:
|
||||
source: './'
|
||||
destination: './_site'
|
||||
|
||||
- name: 'Upload artifact'
|
||||
uses: 'actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa' # ratchet:actions/upload-pages-artifact@v3
|
||||
|
||||
deploy:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
environment:
|
||||
name: 'github-pages'
|
||||
url: '${{ steps.deployment.outputs.page_url }}'
|
||||
runs-on: 'ubuntu-latest'
|
||||
needs: 'build'
|
||||
steps:
|
||||
- name: 'Deploy to GitHub Pages'
|
||||
id: 'deployment'
|
||||
uses: 'actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e' # ratchet:actions/deploy-pages@v4
|
||||
@@ -0,0 +1,18 @@
|
||||
name: 'Trigger Docs Rebuild'
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'docs/**'
|
||||
jobs:
|
||||
trigger-rebuild:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- name: 'Trigger rebuild'
|
||||
run: |
|
||||
curl -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}' \
|
||||
"${{ secrets.DOCS_REBUILD_URL }}"
|
||||
@@ -0,0 +1,211 @@
|
||||
name: 'Evals: PR Evaluation & Regression'
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: ['opened', 'synchronize', 'reopened', 'ready_for_review']
|
||||
paths:
|
||||
- 'packages/core/src/prompts/**'
|
||||
- 'packages/core/src/tools/**'
|
||||
- 'packages/core/src/agents/**'
|
||||
- 'evals/**'
|
||||
- '!**/*.test.ts'
|
||||
- '!**/*.test.tsx'
|
||||
workflow_dispatch:
|
||||
|
||||
# Prevents multiple runs for the same PR simultaneously (saves tokens)
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
pull-requests: 'write'
|
||||
contents: 'read'
|
||||
actions: 'read'
|
||||
|
||||
jobs:
|
||||
detect-changes:
|
||||
name: 'Detect Steering Changes'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
# Security: pull_request_target allows secrets, so we must gate carefully.
|
||||
# Detection should not run code from the fork.
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && github.event.pull_request.draft == false"
|
||||
outputs:
|
||||
SHOULD_RUN: '${{ steps.detect.outputs.SHOULD_RUN }}'
|
||||
STEERING_DETECTED: '${{ steps.detect.outputs.STEERING_DETECTED }}'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
# Check out the trusted code from main for detection
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Detect Steering Changes'
|
||||
id: 'detect'
|
||||
env:
|
||||
# Use the PR's head SHA for comparison without checking it out
|
||||
PR_HEAD_SHA: '${{ github.event.pull_request.head.sha }}'
|
||||
run: |
|
||||
# Fetch the fork's PR branch for analysis
|
||||
git fetch origin pull/${{ github.event.pull_request.number }}/head:pr-head
|
||||
|
||||
# Run the trusted script from main
|
||||
SHOULD_RUN=$(node scripts/changed_prompt.js)
|
||||
STEERING_DETECTED=$(node scripts/changed_prompt.js --steering-only)
|
||||
echo "SHOULD_RUN=$SHOULD_RUN" >> "$GITHUB_OUTPUT"
|
||||
echo "STEERING_DETECTED=$STEERING_DETECTED" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 'Notify Approval Required'
|
||||
if: "steps.detect.outputs.SHOULD_RUN == 'true'"
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
run: |
|
||||
RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
COMMENT_BODY="### 🛑 Action Required: Evaluation Approval
|
||||
|
||||
Steering changes have been detected in this PR. To prevent regressions, a maintainer must approve the evaluation run before this PR can be merged.
|
||||
|
||||
**Maintainers:**
|
||||
1. Go to the [**Workflow Run Summary**]($RUN_URL).
|
||||
2. Click the yellow **'Review deployments'** button.
|
||||
3. Select the **'eval-gate'** environment and click **'Approve'**.
|
||||
|
||||
Once approved, the evaluation results will be posted here automatically.
|
||||
|
||||
<!-- eval-approval-notification -->"
|
||||
|
||||
# Check if comment already exists to avoid spamming
|
||||
COMMENT_ID=$(gh pr view ${{ github.event.pull_request.number }} --json comments --jq '.comments[] | select(.body | contains("<!-- eval-approval-notification -->")) | .url' | grep -oE "[0-9]+$" | head -n 1)
|
||||
|
||||
if [ -z "$COMMENT_ID" ]; then
|
||||
gh pr comment ${{ github.event.pull_request.number }} --body "$COMMENT_BODY"
|
||||
else
|
||||
echo "Updating existing notification comment $COMMENT_ID..."
|
||||
gh api -X PATCH "repos/${{ github.repository }}/issues/comments/$COMMENT_ID" -F body="$COMMENT_BODY"
|
||||
fi
|
||||
|
||||
pr-evaluation:
|
||||
name: 'Evaluate Steering & Regressions'
|
||||
needs: 'detect-changes'
|
||||
if: "needs.detect-changes.outputs.SHOULD_RUN == 'true'"
|
||||
# Manual approval gate via environment
|
||||
environment: 'eval-gate'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
env:
|
||||
# CENTRALIZED MODEL LIST
|
||||
MODEL_LIST: 'gemini-3-flash-preview'
|
||||
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
# Check out the fork's PR code for the actual evaluation
|
||||
# This only runs AFTER manual approval
|
||||
ref: '${{ github.event.pull_request.head.sha }}'
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Remove Approval Notification'
|
||||
# Run even if other steps fail, to ensure we clean up the "Action Required" message
|
||||
if: 'always()'
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
PR_NUMBER: '${{ github.event.pull_request.number }}'
|
||||
run: |
|
||||
echo "Debug: PR_NUMBER is '$PR_NUMBER'"
|
||||
# Search for the notification comment by its hidden tag
|
||||
COMMENT_ID=$(gh pr view "$PR_NUMBER" --json comments --jq '.comments[] | select(.body | contains("<!-- eval-approval-notification -->")) | .url' | grep -oE "[0-9]+$" | head -n 1)
|
||||
if [ -n "$COMMENT_ID" ]; then
|
||||
echo "Removing notification comment $COMMENT_ID now that run is approved..."
|
||||
gh api -X DELETE "repos/${{ github.repository }}/issues/comments/$COMMENT_ID"
|
||||
fi
|
||||
|
||||
- name: 'Set up Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Analyze PR Content (Guidance)'
|
||||
if: "needs.detect-changes.outputs.STEERING_DETECTED == 'true'"
|
||||
id: 'analysis'
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
run: |
|
||||
# Check for behavioral eval changes
|
||||
EVAL_CHANGES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep "^evals/" || true)
|
||||
if [ -z "$EVAL_CHANGES" ]; then
|
||||
echo "MISSING_EVALS=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# Check if user is a maintainer
|
||||
USER_PERMISSION=$(gh api repos/${{ github.repository }}/collaborators/${{ github.actor }}/permission --jq '.permission')
|
||||
if [[ "$USER_PERMISSION" == "admin" || "$USER_PERMISSION" == "write" ]]; then
|
||||
echo "IS_MAINTAINER=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: 'Execute Regression Check'
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
MODEL_LIST: '${{ env.MODEL_LIST }}'
|
||||
run: |
|
||||
# Run the regression check loop. The script saves the report to a file.
|
||||
node scripts/run_eval_regression.js
|
||||
|
||||
# Use the generated report file if it exists
|
||||
if [[ -f eval_regression_report.md ]]; then
|
||||
echo "REPORT_FILE=eval_regression_report.md" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: 'Post or Update PR Comment'
|
||||
if: "always() && (needs.detect-changes.outputs.STEERING_DETECTED == 'true' || env.REPORT_FILE != '')"
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
run: |
|
||||
# 1. Build the full comment body
|
||||
{
|
||||
if [[ -f eval_regression_report.md ]]; then
|
||||
cat eval_regression_report.md
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [[ "${{ needs.detect-changes.outputs.STEERING_DETECTED }}" == "true" ]]; then
|
||||
echo "### 🧠 Model Steering Guidance"
|
||||
echo ""
|
||||
echo "This PR modifies files that affect the model's behavior (prompts, tools, or instructions)."
|
||||
echo ""
|
||||
|
||||
if [[ "${{ steps.analysis.outputs.MISSING_EVALS }}" == "true" ]]; then
|
||||
echo "- ⚠️ **Consider adding Evals:** No behavioral evaluations (\`evals/*.eval.ts\`) were added or updated in this PR. Consider [adding a test case](https://github.com/google-gemini/gemini-cli/blob/main/evals/README.md#creating-an-evaluation) to verify the new behavior and prevent regressions."
|
||||
fi
|
||||
|
||||
if [[ "${{ steps.analysis.outputs.IS_MAINTAINER }}" == "true" ]]; then
|
||||
echo "- 🚀 **Maintainer Reminder:** Please ensure that these changes do not regress results on benchmark evals before merging."
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "---"
|
||||
echo "*This is an automated guidance message triggered by steering logic signatures.*"
|
||||
echo "<!-- eval-pr-report -->"
|
||||
} > full_comment.md
|
||||
|
||||
# 2. Find if a comment with our unique tag already exists
|
||||
# We extract the numeric ID from the URL to ensure compatibility with the REST API
|
||||
COMMENT_ID=$(gh pr view ${{ github.event.pull_request.number }} --json comments --jq '.comments[] | select(.body | contains("<!-- eval-pr-report -->")) | .url' | grep -oE "[0-9]+$" | head -n 1)
|
||||
|
||||
# 3. Update or Create the comment
|
||||
if [ -n "$COMMENT_ID" ]; then
|
||||
echo "Updating existing comment $COMMENT_ID via API..."
|
||||
gh api -X PATCH "repos/${{ github.repository }}/issues/comments/$COMMENT_ID" -F body=@full_comment.md
|
||||
else
|
||||
echo "Creating new PR comment..."
|
||||
gh pr comment ${{ github.event.pull_request.number }} --body-file full_comment.md
|
||||
fi
|
||||
@@ -0,0 +1,48 @@
|
||||
name: 'Eval'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: 'bash'
|
||||
|
||||
permissions:
|
||||
contents: 'read'
|
||||
id-token: 'write'
|
||||
packages: 'read'
|
||||
|
||||
jobs:
|
||||
eval:
|
||||
name: 'Eval'
|
||||
if: >-
|
||||
github.repository == 'google-gemini/gemini-cli'
|
||||
runs-on: 'ubuntu-latest'
|
||||
container:
|
||||
image: 'ghcr.io/google-gemini/gemini-cli-swe-agent-eval@sha256:cd5edc4afd2245c1f575e791c0859b3c084a86bb3bd9a6762296da5162b35a8f'
|
||||
credentials:
|
||||
username: '${{ github.actor }}'
|
||||
password: '${{ secrets.GITHUB_TOKEN }}'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
DEFAULT_VERTEXAI_PROJECT: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
|
||||
GOOGLE_CLOUD_PROJECT: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
|
||||
GEMINI_API_KEY: '${{ secrets.EVAL_GEMINI_API_KEY }}'
|
||||
GCLI_LOCAL_FILE_TELEMETRY: 'True'
|
||||
EVAL_GCS_BUCKET: '${{ vars.EVAL_GCS_ARTIFACTS_BUCKET }}'
|
||||
steps:
|
||||
- name: 'Authenticate to Google Cloud'
|
||||
id: 'auth'
|
||||
uses: 'google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed' # ratchet:exclude pin@v2.1.7
|
||||
with:
|
||||
project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
|
||||
workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
|
||||
service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
|
||||
token_format: 'access_token'
|
||||
access_token_scopes: 'https://www.googleapis.com/auth/cloud-platform'
|
||||
|
||||
- name: 'Run evaluation'
|
||||
working-directory: '/app'
|
||||
run: |
|
||||
poetry run exp_run --experiment-mode=on-demand --branch-or-commit="${GITHUB_REF_NAME}" --model-name=gemini-2.5-pro --dataset=swebench_verified --concurrency=15
|
||||
poetry run python agent_prototypes/scripts/parse_gcli_logs_experiment.py --experiment_dir=experiments/adhoc/gcli_temp_exp --gcs-bucket="${EVAL_GCS_BUCKET}" --gcs-path=gh_action_artifacts
|
||||
@@ -0,0 +1,121 @@
|
||||
name: 'Evals: Nightly'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 1 * * *' # Runs at 1 AM every day
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
suite_type:
|
||||
description: 'Suite type to run'
|
||||
type: 'choice'
|
||||
options:
|
||||
- 'behavioral'
|
||||
- 'component-level'
|
||||
- 'hero-scenario'
|
||||
default: 'behavioral'
|
||||
suite_name:
|
||||
description: 'Specific suite name to run'
|
||||
required: false
|
||||
type: 'string'
|
||||
test_name_pattern:
|
||||
description: 'Test name pattern or file name'
|
||||
required: false
|
||||
type: 'string'
|
||||
|
||||
permissions:
|
||||
contents: 'read'
|
||||
checks: 'write'
|
||||
actions: 'read'
|
||||
|
||||
jobs:
|
||||
evals:
|
||||
name: 'Evals (USUALLY_PASSING) nightly run'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
model:
|
||||
- 'gemini-3.1-pro-preview-customtools'
|
||||
- 'gemini-3-pro-preview'
|
||||
- 'gemini-3-flash-preview'
|
||||
- 'gemini-2.5-pro'
|
||||
- 'gemini-2.5-flash'
|
||||
- 'gemini-2.5-flash-lite'
|
||||
run_attempt: [1, 2, 3]
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Set up Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Create logs directory'
|
||||
run: 'mkdir -p evals/logs'
|
||||
|
||||
- name: 'Run Evals'
|
||||
continue-on-error: true
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
GEMINI_CLI_TRUST_WORKSPACE: true
|
||||
GEMINI_MODEL: '${{ matrix.model }}'
|
||||
RUN_EVALS: 'true'
|
||||
EVAL_SUITE_TYPE: "${{ github.event.inputs.suite_type || 'behavioral' }}"
|
||||
EVAL_SUITE_NAME: '${{ github.event.inputs.suite_name }}'
|
||||
TEST_NAME_PATTERN: '${{ github.event.inputs.test_name_pattern }}'
|
||||
# Disable Vitest internal retries to avoid double-retrying;
|
||||
# custom retry logic is handled in evals/test-helper.ts
|
||||
VITEST_RETRY: 0
|
||||
run: |
|
||||
CMD="npm run test:all_evals"
|
||||
PATTERN="${TEST_NAME_PATTERN}"
|
||||
|
||||
if [[ -n "$PATTERN" ]]; then
|
||||
if [[ "$PATTERN" == *.ts || "$PATTERN" == *.js || "$PATTERN" == */* ]]; then
|
||||
$CMD -- "$PATTERN"
|
||||
else
|
||||
$CMD -- -t "$PATTERN"
|
||||
fi
|
||||
else
|
||||
$CMD
|
||||
fi
|
||||
|
||||
- name: 'Upload Logs'
|
||||
if: 'always()'
|
||||
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'eval-logs-${{ matrix.model }}-${{ matrix.run_attempt }}'
|
||||
path: 'evals/logs'
|
||||
retention-days: 7
|
||||
|
||||
aggregate-results:
|
||||
name: 'Aggregate Results'
|
||||
needs: ['evals']
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && always()"
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Download Logs'
|
||||
uses: 'actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806' # ratchet:actions/download-artifact@v4
|
||||
with:
|
||||
path: 'artifacts'
|
||||
|
||||
- name: 'Generate Summary'
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
run: 'node scripts/aggregate_evals.js artifacts >> "$GITHUB_STEP_SUMMARY"'
|
||||
@@ -0,0 +1,267 @@
|
||||
name: '🏷️ Gemini Automated Issue Deduplication'
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- 'opened'
|
||||
- 'reopened'
|
||||
issue_comment:
|
||||
types:
|
||||
- 'created'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue_number:
|
||||
description: 'issue number to dedup'
|
||||
required: true
|
||||
type: 'number'
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.event.issue.number }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: 'bash'
|
||||
|
||||
jobs:
|
||||
find-duplicates:
|
||||
if: |-
|
||||
github.repository == 'google-gemini/gemini-cli' &&
|
||||
vars.TRIAGE_DEDUPLICATE_ISSUES != '' &&
|
||||
(github.event_name == 'issues' ||
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event_name == 'issue_comment' &&
|
||||
contains(github.event.comment.body, '@gemini-cli /deduplicate') &&
|
||||
(github.event.comment.author_association == 'OWNER' ||
|
||||
github.event.comment.author_association == 'MEMBER' ||
|
||||
github.event.comment.author_association == 'COLLABORATOR')))
|
||||
permissions:
|
||||
contents: 'read'
|
||||
id-token: 'write' # Required for WIF, see https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-google-cloud-platform#adding-permissions-settings
|
||||
issues: 'read'
|
||||
statuses: 'read'
|
||||
packages: 'read'
|
||||
timeout-minutes: 20
|
||||
runs-on: 'ubuntu-latest'
|
||||
outputs:
|
||||
duplicate_issues_csv: '${{ env.DUPLICATE_ISSUES_CSV }}'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Log in to GitHub Container Registry'
|
||||
uses: 'docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1' # ratchet:docker/login-action@v3
|
||||
with:
|
||||
registry: 'ghcr.io'
|
||||
username: '${{ github.actor }}'
|
||||
password: '${{ secrets.GITHUB_TOKEN }}'
|
||||
|
||||
- name: 'Find Duplicate Issues'
|
||||
uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0
|
||||
id: 'gemini_issue_deduplication'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
ISSUE_TITLE: '${{ github.event.issue.title }}'
|
||||
ISSUE_BODY: '${{ github.event.issue.body }}'
|
||||
ISSUE_NUMBER: '${{ github.event.issue.number }}'
|
||||
REPOSITORY: '${{ github.repository }}'
|
||||
FIRESTORE_PROJECT: '${{ vars.FIRESTORE_PROJECT }}'
|
||||
GEMINI_CLI_TRUST_WORKSPACE: 'true'
|
||||
with:
|
||||
upload_artifacts: 'true'
|
||||
gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
|
||||
gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
|
||||
gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}'
|
||||
gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
|
||||
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
|
||||
use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}'
|
||||
use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
|
||||
settings: |-
|
||||
{
|
||||
"mcpServers": {
|
||||
"issue_deduplication": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"--network", "host",
|
||||
"-e", "GITHUB_TOKEN",
|
||||
"-e", "GEMINI_API_KEY",
|
||||
"-e", "DATABASE_TYPE",
|
||||
"-e", "FIRESTORE_DATABASE_ID",
|
||||
"-e", "GCP_PROJECT",
|
||||
"-e", "GOOGLE_APPLICATION_CREDENTIALS=/app/gcp-credentials.json",
|
||||
"-v", "${GOOGLE_APPLICATION_CREDENTIALS}:/app/gcp-credentials.json",
|
||||
"ghcr.io/google-gemini/gemini-cli-issue-triage@sha256:e3de1523f6c83aabb3c54b76d08940a2bf42febcb789dd2da6f95169641f94d3"
|
||||
],
|
||||
"env": {
|
||||
"GITHUB_TOKEN": "${GITHUB_TOKEN}",
|
||||
"GEMINI_API_KEY": "${{ secrets.GEMINI_API_KEY }}",
|
||||
"DATABASE_TYPE":"firestore",
|
||||
"GCP_PROJECT": "${FIRESTORE_PROJECT}",
|
||||
"FIRESTORE_DATABASE_ID": "(default)",
|
||||
"GOOGLE_APPLICATION_CREDENTIALS": "${GOOGLE_APPLICATION_CREDENTIALS}"
|
||||
},
|
||||
"timeout": 600000
|
||||
}
|
||||
},
|
||||
"maxSessionTurns": 25,
|
||||
"tools": {
|
||||
"core": [
|
||||
"run_shell_command(echo)",
|
||||
"run_shell_command(gh issue view)"
|
||||
]
|
||||
},
|
||||
"telemetry": {
|
||||
"enabled": true,
|
||||
"target": "gcp"
|
||||
}
|
||||
}
|
||||
prompt: |-
|
||||
## Role
|
||||
You are an issue de-duplication assistant. Your goal is to find
|
||||
duplicate issues for a given issue.
|
||||
## Steps
|
||||
1. **Find Potential Duplicates:**
|
||||
- The repository is ${{ github.repository }} and the issue number is ${{ github.event.issue.number }}.
|
||||
- Use the `duplicates` tool with the `repo` and `issue_number` to find potential duplicates for the current issue. Do not use the `threshold` parameter.
|
||||
- If no duplicates are found, you are done.
|
||||
- Print the JSON output from the `duplicates` tool to the logs.
|
||||
2. **Refine Duplicates List (if necessary):**
|
||||
- If the `duplicates` tool returns between 1 and 14 results, you must refine the list.
|
||||
- For each potential duplicate issue, run `gh issue view <issue-number> --json title,body,comments` to fetch its content.
|
||||
- Also fetch the content of the original issue: `gh issue view "${ISSUE_NUMBER}" --json title,body,comments`.
|
||||
- Carefully analyze the content (title, body, comments) of the original issue and all potential duplicates.
|
||||
- It is very important if the comments on either issue mention that they are not duplicates of each other, to treat them as not duplicates.
|
||||
- Based on your analysis, create a final list containing only the issues you are highly confident are actual duplicates.
|
||||
- If your final list is empty, you are done.
|
||||
- Print to the logs if you omitted any potential duplicates based on your analysis.
|
||||
- If the `duplicates` tool returned 15+ results, use the top 15 matches (based on descending similarity score value) to perform this step.
|
||||
3. **Output final duplicates list as CSV:**
|
||||
- Convert the list of appropriate duplicate issue numbers into a comma-separated list (CSV). If there are no appropriate duplicates, use the empty string.
|
||||
- Use the "echo" shell command to append the CSV of issue numbers into the filepath referenced by the environment variable "${GITHUB_ENV}":
|
||||
echo "DUPLICATE_ISSUES_CSV=[DUPLICATE_ISSUES_AS_CSV]" >> "${GITHUB_ENV}"
|
||||
## Guidelines
|
||||
- Only use the `duplicates` and `run_shell_command` tools.
|
||||
- The `run_shell_command` tool can be used with `gh issue view`.
|
||||
- Do not download or read media files like images, videos, or links. The `--json` flag for `gh issue view` will prevent this.
|
||||
- Do not modify the issue content or status.
|
||||
- Do not add comments or labels.
|
||||
- Reference all shell variables as "${VAR}" (with quotes and braces).
|
||||
|
||||
add-comment-and-label:
|
||||
needs: 'find-duplicates'
|
||||
if: |-
|
||||
github.repository == 'google-gemini/gemini-cli' &&
|
||||
vars.TRIAGE_DEDUPLICATE_ISSUES != '' &&
|
||||
needs.find-duplicates.outputs.duplicate_issues_csv != '' &&
|
||||
(
|
||||
github.event_name == 'issues' ||
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
github.event_name == 'issue_comment' &&
|
||||
contains(github.event.comment.body, '@gemini-cli /deduplicate') &&
|
||||
(
|
||||
github.event.comment.author_association == 'OWNER' ||
|
||||
github.event.comment.author_association == 'MEMBER' ||
|
||||
github.event.comment.author_association == 'COLLABORATOR'
|
||||
)
|
||||
)
|
||||
)
|
||||
permissions:
|
||||
issues: 'write'
|
||||
timeout-minutes: 5
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- name: 'Generate GitHub App Token'
|
||||
id: 'generate_token'
|
||||
uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b' # ratchet:actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: '${{ secrets.APP_ID }}'
|
||||
private-key: '${{ secrets.PRIVATE_KEY }}'
|
||||
permission-issues: 'write'
|
||||
|
||||
- name: 'Comment and Label Duplicate Issue'
|
||||
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
|
||||
env:
|
||||
DUPLICATES_OUTPUT: '${{ needs.find-duplicates.outputs.duplicate_issues_csv }}'
|
||||
with:
|
||||
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
|
||||
script: |-
|
||||
const rawCsv = process.env.DUPLICATES_OUTPUT;
|
||||
core.info(`Raw duplicates CSV: ${rawCsv}`);
|
||||
const duplicateIssues = rawCsv.split(',').map(s => s.trim()).filter(s => s);
|
||||
|
||||
if (duplicateIssues.length === 0) {
|
||||
core.info('No duplicate issues found. Nothing to do.');
|
||||
return;
|
||||
}
|
||||
|
||||
const issueNumber = ${{ github.event.issue.number }};
|
||||
|
||||
function formatCommentBody(issues, updated = false) {
|
||||
const header = updated
|
||||
? 'Found possible duplicate issues (updated):'
|
||||
: 'Found possible duplicate issues:';
|
||||
const issuesList = issues.map(num => `- #${num}`).join('\n');
|
||||
const footer = 'If you believe this is not a duplicate, please remove the `status/possible-duplicate` label.';
|
||||
const magicComment = '<!-- gemini-cli-deduplication -->';
|
||||
return `${header}\n\n${issuesList}\n\n${footer}\n${magicComment}`;
|
||||
}
|
||||
|
||||
const newCommentBody = formatCommentBody(duplicateIssues);
|
||||
const newUpdatedCommentBody = formatCommentBody(duplicateIssues, true);
|
||||
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
});
|
||||
|
||||
const magicComment = '<!-- gemini-cli-deduplication -->';
|
||||
const existingComment = comments.find(comment =>
|
||||
comment.user.type === 'Bot' && comment.body.includes(magicComment)
|
||||
);
|
||||
|
||||
let commentMade = false;
|
||||
|
||||
if (existingComment) {
|
||||
// To check if lists are same, just compare the formatted bodies without headers.
|
||||
const existingBodyForCompare = existingComment.body.substring(existingComment.body.indexOf('- #'));
|
||||
const newBodyForCompare = newCommentBody.substring(newCommentBody.indexOf('- #'));
|
||||
|
||||
if (existingBodyForCompare.trim() !== newBodyForCompare.trim()) {
|
||||
core.info(`Updating existing comment ${existingComment.id}`);
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existingComment.id,
|
||||
body: newUpdatedCommentBody,
|
||||
});
|
||||
commentMade = true;
|
||||
} else {
|
||||
core.info('Existing comment is up-to-date. Nothing to do.');
|
||||
}
|
||||
} else {
|
||||
core.info('Creating new comment.');
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
body: newCommentBody,
|
||||
});
|
||||
commentMade = true;
|
||||
}
|
||||
|
||||
if (commentMade) {
|
||||
core.info('Adding "status/possible-duplicate" label.');
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
labels: ['status/possible-duplicate'],
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
name: '🏷️ Gemini Automated Issue Triage'
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- 'opened'
|
||||
- 'reopened'
|
||||
issue_comment:
|
||||
types:
|
||||
- 'created'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue_number:
|
||||
description: 'issue number to triage'
|
||||
required: true
|
||||
type: 'number'
|
||||
workflow_call:
|
||||
inputs:
|
||||
issue_number:
|
||||
description: 'issue number to triage'
|
||||
required: false
|
||||
type: 'string'
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.event.issue.number || github.event.inputs.issue_number || inputs.issue_number }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: 'bash'
|
||||
|
||||
permissions:
|
||||
contents: 'read'
|
||||
id-token: 'write'
|
||||
issues: 'write'
|
||||
statuses: 'write'
|
||||
packages: 'read'
|
||||
actions: 'write' # Required for cancelling a workflow run
|
||||
|
||||
jobs:
|
||||
triage-issue:
|
||||
if: |-
|
||||
(github.repository == 'google-gemini/gemini-cli' || github.repository == 'google-gemini/maintainers-gemini-cli') &&
|
||||
(
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
(github.event_name == 'issues' || github.event_name == 'issue_comment') &&
|
||||
(github.event_name != 'issue_comment' || (
|
||||
contains(github.event.comment.body, '@gemini-cli /triage') &&
|
||||
(github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'COLLABORATOR')
|
||||
))
|
||||
)
|
||||
) &&
|
||||
!contains(github.event.issue.labels.*.name, 'area/')
|
||||
timeout-minutes: 5
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- name: 'Get issue data for manual trigger'
|
||||
id: 'get_issue_data'
|
||||
if: |-
|
||||
github.event_name == 'workflow_dispatch'
|
||||
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
script: |
|
||||
const issueNumber = ${{ github.event.inputs.issue_number || inputs.issue_number }};
|
||||
const { data: issue } = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
});
|
||||
core.setOutput('title', issue.title);
|
||||
core.setOutput('body', issue.body);
|
||||
core.setOutput('labels', issue.labels.map(label => label.name).join(','));
|
||||
return issue;
|
||||
|
||||
- name: 'Manual Trigger Pre-flight Checks'
|
||||
if: |-
|
||||
github.event_name == 'workflow_dispatch'
|
||||
env:
|
||||
ISSUE_NUMBER_INPUT: '${{ github.event.inputs.issue_number || inputs.issue_number }}'
|
||||
LABELS: '${{ steps.get_issue_data.outputs.labels }}'
|
||||
run: |
|
||||
if echo "${LABELS}" | grep -q 'area/'; then
|
||||
echo "Issue #${ISSUE_NUMBER_INPUT} already has 'area/' label. Stopping workflow."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Manual triage checks passed."
|
||||
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Generate GitHub App Token'
|
||||
id: 'generate_token'
|
||||
env:
|
||||
APP_ID: '${{ secrets.APP_ID }}'
|
||||
if: |-
|
||||
${{ env.APP_ID != '' }}
|
||||
uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b' # ratchet:actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: '${{ secrets.APP_ID }}'
|
||||
private-key: '${{ secrets.PRIVATE_KEY }}'
|
||||
permission-issues: 'write'
|
||||
|
||||
- name: 'Get Repository Labels'
|
||||
id: 'get_labels'
|
||||
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
|
||||
with:
|
||||
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
|
||||
script: |-
|
||||
const { data: labels } = await github.rest.issues.listLabelsForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
});
|
||||
const allowedLabels = [
|
||||
'area/agent',
|
||||
'area/enterprise',
|
||||
'area/non-interactive',
|
||||
'area/core',
|
||||
'area/security',
|
||||
'area/platform',
|
||||
'area/extensions',
|
||||
'area/documentation',
|
||||
'area/unknown'
|
||||
];
|
||||
const labelNames = labels.map(label => label.name).filter(name => allowedLabels.includes(name));
|
||||
core.setOutput('available_labels', labelNames.join(','));
|
||||
core.info(`Found ${labelNames.length} labels: ${labelNames.join(', ')}`);
|
||||
return labelNames;
|
||||
|
||||
- name: 'Prepare Issue Data'
|
||||
id: 'prepare_issue_data'
|
||||
env:
|
||||
ISSUE_TITLE: >-
|
||||
${{ github.event_name == 'workflow_dispatch' && steps.get_issue_data.outputs.title || github.event.issue.title }}
|
||||
ISSUE_BODY: >-
|
||||
${{ github.event_name == 'workflow_dispatch' && steps.get_issue_data.outputs.body || github.event.issue.body }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "Title: ${ISSUE_TITLE}" > issue_context.md
|
||||
echo "Body:" >> issue_context.md
|
||||
echo "${ISSUE_BODY}" >> issue_context.md
|
||||
|
||||
- name: 'Run Gemini Issue Analysis'
|
||||
uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0
|
||||
id: 'gemini_issue_analysis'
|
||||
env:
|
||||
GITHUB_TOKEN: '' # Do not pass any auth token here since this runs on untrusted inputs
|
||||
ISSUE_NUMBER: >-
|
||||
${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.issue_number || inputs.issue_number) || github.event.issue.number }}
|
||||
REPOSITORY: '${{ github.repository }}'
|
||||
AVAILABLE_LABELS: '${{ steps.get_labels.outputs.available_labels }}'
|
||||
GEMINI_CLI_TRUST_WORKSPACE: 'true'
|
||||
with:
|
||||
upload_artifacts: 'true'
|
||||
gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
|
||||
gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
|
||||
gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}'
|
||||
gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
|
||||
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
|
||||
use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}'
|
||||
use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
|
||||
settings: |-
|
||||
{
|
||||
"maxSessionTurns": 25,
|
||||
"telemetry": {
|
||||
"enabled": true,
|
||||
"target": "gcp"
|
||||
},
|
||||
"tools": {
|
||||
"core": [
|
||||
"run_shell_command(echo)",
|
||||
"read_file"
|
||||
]
|
||||
}
|
||||
}
|
||||
prompt: |-
|
||||
## Role
|
||||
|
||||
You are an issue triage assistant. Your role is to analyze a GitHub issue and determine the single most appropriate area/ label based on the definitions provided.
|
||||
|
||||
## Steps
|
||||
1. Use the read_file tool to read the file "issue_context.md" which contains the issue title and body.
|
||||
2. Review the available labels: ${{ env.AVAILABLE_LABELS }}.
|
||||
3. Select exactly one area/ label that best matches the issue based on Reference 1: Area Definitions.
|
||||
4. Fallback Logic:
|
||||
- If you cannot confidently determine the correct area/ label from the definitions, you must use area/unknown.
|
||||
5. Output your selected label in JSON format and nothing else. Example:
|
||||
{"labels_to_set": ["area/core"]}
|
||||
|
||||
## Guidelines
|
||||
- Your output must contain exactly one area/ label.
|
||||
- Triage only the current issue based on its title and body.
|
||||
- Output only valid JSON format.
|
||||
- Do not include any explanation or additional text, just the JSON.
|
||||
|
||||
Reference 1: Area Definitions
|
||||
area/agent
|
||||
- Description: Issues related to the "brain" of the CLI. This includes the core agent logic, model quality, tool/function calling, and memory.
|
||||
- Example Issues:
|
||||
"I am not getting a reasonable or expected response."
|
||||
"The model is not calling the tool I expected."
|
||||
"The web search tool is not working as expected."
|
||||
"Feature request for a new built-in tool (e.g., read file, write file)."
|
||||
"The generated code is poor quality or incorrect."
|
||||
"The model seems stuck in a loop."
|
||||
"The response from the model is malformed (e.g., broken JSON, bad formatting)."
|
||||
"Concerns about unnecessary token consumption."
|
||||
"Issues with how memory or chat history is managed."
|
||||
"Issues with sub-agents."
|
||||
"Model is switching from one to another unexpectedly."
|
||||
|
||||
area/enterprise
|
||||
- Description: Issues specific to enterprise-level features, including telemetry, policy, and licenses.
|
||||
- Example Issues:
|
||||
"Usage data is not appearing in our telemetry dashboard."
|
||||
"A user is able to perform an action that should be blocked by an admin policy."
|
||||
"Questions about billing, licensing tiers, or enterprise quotas."
|
||||
|
||||
area/non-interactive
|
||||
- Description: Issues related to using the CLI in automated or non-interactive environments (headless mode).
|
||||
- Example Issues:
|
||||
"Problems using the CLI as an SDK in another surface."
|
||||
"The CLI is behaving differently when run from a shell script vs. an interactive terminal."
|
||||
"GitHub action is failing."
|
||||
"I am having trouble running the CLI in headless mode"
|
||||
|
||||
area/core
|
||||
- Description: Issues with the fundamental CLI app itself. This includes the user interface (UI/UX), installation, OS compatibility, and performance.
|
||||
- Example Issues:
|
||||
"I am seeing my screen flicker when using the CLI."
|
||||
"The output in my terminal is malformed or unreadable."
|
||||
"Theme changes are not taking effect."
|
||||
"Keyboard inputs (e.g., arrow keys, Ctrl+C) are not being recognized."
|
||||
"The CLI failed to install or update."
|
||||
"An issue specific to running on Windows, macOS, or Linux."
|
||||
"Problems with command parsing, flags, or argument handling."
|
||||
"High CPU or memory usage by the CLI process."
|
||||
"Issues related to multi-modality (e.g., handling image inputs)."
|
||||
"Problems with the IDE integration connection or installation"
|
||||
|
||||
area/security
|
||||
- Description: Issues related to user authentication, authorization, data security, and privacy.
|
||||
- Example Issues:
|
||||
"I am unable to sign in."
|
||||
"The login flow is selecting the wrong authentication path"
|
||||
"Problems with API key handling or credential storage."
|
||||
"A report of a security vulnerability"
|
||||
"Concerns about data sanitization or potential data leaks."
|
||||
"Issues or requests related to privacy controls."
|
||||
"Preventing unauthorized data access."
|
||||
|
||||
area/platform
|
||||
- Description: Issues related to CI/CD, release management, testing, eval infrastructure, capacity, quota management, and sandbox environments.
|
||||
- Example Issues:
|
||||
"I am getting a 429 'Resource Exhausted' or 500-level server error."
|
||||
"General slowness or high latency from the service."
|
||||
"The build script is broken on the main branch."
|
||||
"Tests are failing in the CI/CD pipeline."
|
||||
"Issues with the release management or publishing process."
|
||||
"User is running out of capacity."
|
||||
"Problems specific to the sandbox or staging environments."
|
||||
"Questions about quota limits or requests for increases."
|
||||
|
||||
area/extensions
|
||||
- Description: Issues related to the extension ecosystem, including the marketplace and website.
|
||||
- Example Issues:
|
||||
"Bugs related to the extension marketplace website."
|
||||
"Issues with a specific extension."
|
||||
"Feature request for the extension ecosystem."
|
||||
|
||||
area/documentation
|
||||
- Description: Issues related to user-facing documentation and other content on the documentation website.
|
||||
- Example Issues:
|
||||
"A typo in a README file."
|
||||
"DOCS: A command is not working as described in the documentation."
|
||||
"A request for a new documentation page."
|
||||
"Instructions missing for skills feature"
|
||||
|
||||
area/unknown
|
||||
- Description: Issues that do not clearly fit into any other defined area/ category, or where information is too limited to make a determination. Use this when no other area is appropriate.
|
||||
|
||||
- name: 'Apply Labels to Issue'
|
||||
if: |-
|
||||
${{ steps.gemini_issue_analysis.outputs.summary != '' }}
|
||||
env:
|
||||
REPOSITORY: '${{ github.repository }}'
|
||||
ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}'
|
||||
LABELS_OUTPUT: '${{ steps.gemini_issue_analysis.outputs.summary }}'
|
||||
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
|
||||
with:
|
||||
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
|
||||
script: |
|
||||
const rawOutput = process.env.LABELS_OUTPUT;
|
||||
core.info(`Raw output from model: ${rawOutput}`);
|
||||
let parsedLabels;
|
||||
try {
|
||||
// First, try to parse the raw output as JSON.
|
||||
parsedLabels = JSON.parse(rawOutput);
|
||||
} catch (jsonError) {
|
||||
// If that fails, check for a markdown code block.
|
||||
core.warning(`Direct JSON parsing failed: ${jsonError.message}. Trying to extract from a markdown block.`);
|
||||
const jsonMatch = rawOutput.match(/```json\s*([\s\S]*?)\s*```/);
|
||||
if (jsonMatch && jsonMatch[1]) {
|
||||
try {
|
||||
parsedLabels = JSON.parse(jsonMatch[1].trim());
|
||||
} catch (markdownError) {
|
||||
core.setFailed(`Failed to parse JSON even after extracting from markdown block: ${markdownError.message}\nRaw output: ${rawOutput}`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// If no markdown block, try to find a raw JSON object in the output.
|
||||
// The CLI may include debug/log lines (e.g. telemetry init, YOLO mode)
|
||||
// before the actual JSON response.
|
||||
const jsonObjectMatch = rawOutput.match(/(\{[\s\S]*"labels_to_set"[\s\S]*\})/);
|
||||
if (jsonObjectMatch) {
|
||||
try {
|
||||
parsedLabels = JSON.parse(jsonObjectMatch[0]);
|
||||
} catch (extractError) {
|
||||
core.setFailed(`Found JSON-like content but failed to parse: ${extractError.message}\nRaw output: ${rawOutput}`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
core.setFailed(`Output is not valid JSON and does not contain extractable JSON.\nRaw output: ${rawOutput}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const issueNumber = parseInt(process.env.ISSUE_NUMBER);
|
||||
const labelsToAdd = parsedLabels.labels_to_set || [];
|
||||
|
||||
if (labelsToAdd.length !== 1) {
|
||||
core.setFailed(`Expected exactly 1 label (area/), but got ${labelsToAdd.length}. Labels: ${labelsToAdd.join(', ')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const newAreaLabel = labelsToAdd[0];
|
||||
|
||||
// Get current labels to resolve conflicts
|
||||
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
});
|
||||
const currentLabelNames = currentLabels.map(l => l.name);
|
||||
const currentAreaLabels = currentLabelNames.filter(name => name.startsWith('area/'));
|
||||
|
||||
const labelsToRemove = currentAreaLabels.filter(name => name !== newAreaLabel);
|
||||
|
||||
for (const label of labelsToRemove) {
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
name: label
|
||||
});
|
||||
core.info(`Removed conflicting area label: ${label}`);
|
||||
} catch (e) {
|
||||
core.warning(`Failed to remove label ${label}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Set labels based on triage result
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
labels: [newAreaLabel]
|
||||
});
|
||||
core.info(`Successfully added labels for #${issueNumber}: ${newAreaLabel}`);
|
||||
|
||||
- name: 'Post Issue Analysis Failure Comment'
|
||||
if: |-
|
||||
${{ failure() && steps.gemini_issue_analysis.outcome == 'failure' }}
|
||||
env:
|
||||
ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}'
|
||||
RUN_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
|
||||
with:
|
||||
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
|
||||
script: |-
|
||||
github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: parseInt(process.env.ISSUE_NUMBER),
|
||||
body: 'There is a problem with the Gemini CLI issue triaging. Please check the [action logs](${process.env.RUN_URL}) for details.'
|
||||
})
|
||||
@@ -0,0 +1,350 @@
|
||||
name: '🧠 Gemini CLI Bot: Brain'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *' # Every 24 hours
|
||||
issue_comment:
|
||||
types: ['created']
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
run_interactive:
|
||||
description: 'Run interactive flow (requires issue_number)'
|
||||
type: 'boolean'
|
||||
default: false
|
||||
issue_number:
|
||||
description: 'Issue/PR number to simulate context from'
|
||||
type: 'string'
|
||||
required: false
|
||||
comment_id:
|
||||
description: 'Specific comment ID to simulate'
|
||||
type: 'string'
|
||||
required: false
|
||||
clear_memory:
|
||||
description: 'Clear memory (drops learnings from previous runs)'
|
||||
type: 'boolean'
|
||||
default: false
|
||||
enable_prs:
|
||||
description: 'Enable PRs (automatically promote changes to PRs)'
|
||||
type: 'boolean'
|
||||
default: false
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.event.issue.number || github.event.inputs.issue_number || github.ref }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
reasoning:
|
||||
name: 'Brain (Reasoning Layer)'
|
||||
runs-on: 'ubuntu-latest'
|
||||
if: |
|
||||
github.repository == 'google-gemini/gemini-cli' && (
|
||||
github.event_name == 'schedule' ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_interactive != 'true') ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_interactive == 'true') ||
|
||||
(github.event_name == 'issue_comment' && github.event.comment.user.login != 'gemini-cli[bot]' && contains(github.event.comment.body, '@gemini-cli') && contains(fromJSON('["COLLABORATOR", "MEMBER", "OWNER"]'), github.event.comment.author_association))
|
||||
)
|
||||
# The reasoning phase is strictly readonly.
|
||||
permissions:
|
||||
contents: 'read'
|
||||
issues: 'read'
|
||||
actions: 'read'
|
||||
env:
|
||||
GEMINI_CLI_TRUST_WORKSPACE: 'true'
|
||||
steps:
|
||||
- name: 'Determine Checkout Ref'
|
||||
id: 'determine_ref'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}'
|
||||
run: |
|
||||
REF="${{ github.ref }}"
|
||||
if [ -n "$ISSUE_NUMBER" ]; then
|
||||
PR_HEAD=$(gh pr view "$ISSUE_NUMBER" --repo "${{ github.repository }}" --json headRefName --jq .headRefName 2>/dev/null || echo "")
|
||||
if [ -n "$PR_HEAD" ]; then
|
||||
REF="$PR_HEAD"
|
||||
fi
|
||||
fi
|
||||
echo "ref=$REF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
ref: '${{ steps.determine_ref.outputs.ref }}'
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Build Gemini CLI'
|
||||
run: 'npm run bundle'
|
||||
|
||||
- name: 'Download Previous State'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
run: |
|
||||
if [ "${{ github.event.inputs.clear_memory }}" = "true" ]; then
|
||||
echo "Memory clear requested. Skipping previous state download."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Find the last successful run of this workflow
|
||||
LAST_RUN_ID=$(gh run list --workflow "${{ github.workflow }}" --status success --limit 1 --json databaseId --jq '.[0].databaseId')
|
||||
|
||||
if [ -n "$LAST_RUN_ID" ]; then
|
||||
echo "Found previous successful run: $LAST_RUN_ID"
|
||||
|
||||
# Download brain memory to a temp dir so we can selectively restore only persistent state
|
||||
mkdir -p .temp_brain_data
|
||||
gh run download "$LAST_RUN_ID" -n brain-data -D .temp_brain_data || echo "brain-data not found"
|
||||
|
||||
# Restore only persistent memory files
|
||||
cp .temp_brain_data/tools/gemini-cli-bot/lessons-learned.md tools/gemini-cli-bot/lessons-learned.md 2>/dev/null || true
|
||||
mkdir -p tools/gemini-cli-bot/history/
|
||||
cp .temp_brain_data/tools/gemini-cli-bot/history/*.csv tools/gemini-cli-bot/history/ 2>/dev/null || true
|
||||
rm -rf .temp_brain_data
|
||||
else
|
||||
echo "No previous successful run found."
|
||||
fi
|
||||
|
||||
- name: 'Collect Current Metrics'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
run: 'npx tsx tools/gemini-cli-bot/metrics/index.ts'
|
||||
|
||||
- name: 'Run Brain Phases'
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
GEMINI_MODEL: 'gemini-3-flash-preview'
|
||||
GEMINI_CLI_HOME: 'tools/gemini-cli-bot'
|
||||
ENABLE_PRS: "${{ github.event.inputs.enable_prs || 'false' }}"
|
||||
TRIGGER_ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}'
|
||||
TRIGGER_COMMENT_ID: '${{ github.event.comment.id || github.event.inputs.comment_id }}'
|
||||
run: |
|
||||
PROMPT_PATH="tools/gemini-cli-bot/brain/scheduled.md"
|
||||
if [ "${{ github.event_name }}" = "issue_comment" ] || [ "${{ github.event.inputs.run_interactive }}" = "true" ]; then
|
||||
PROMPT_PATH="tools/gemini-cli-bot/brain/interactive.md"
|
||||
export ENABLE_PRS="true"
|
||||
fi
|
||||
|
||||
touch trigger_context.md
|
||||
if [ -n "$TRIGGER_ISSUE_NUMBER" ]; then
|
||||
echo "<untrusted_context>" > trigger_context.md
|
||||
echo "# Interactive Trigger Context" >> trigger_context.md
|
||||
echo "You were invoked by a user in issue/PR #$TRIGGER_ISSUE_NUMBER." >> trigger_context.md
|
||||
|
||||
if [ -n "$TRIGGER_COMMENT_ID" ]; then
|
||||
echo "## User Comment" >> trigger_context.md
|
||||
gh api "repos/${{ github.repository }}/issues/comments/$TRIGGER_COMMENT_ID" -q '.body' >> trigger_context.md 2>/dev/null || gh api "repos/${{ github.repository }}/pulls/comments/$TRIGGER_COMMENT_ID" -q '.body' >> trigger_context.md
|
||||
echo "" >> trigger_context.md
|
||||
fi
|
||||
|
||||
echo "## Issue/PR Context" >> trigger_context.md
|
||||
gh issue view "$TRIGGER_ISSUE_NUMBER" >> trigger_context.md 2>/dev/null || gh pr view "$TRIGGER_ISSUE_NUMBER" >> trigger_context.md
|
||||
echo "</untrusted_context>" >> trigger_context.md
|
||||
fi
|
||||
|
||||
if [ "$ENABLE_PRS" = "true" ]; then
|
||||
echo "**System Directive**: PR creation is ENABLED for this run. You MUST activate the **'prs' skill** to stage your changes and generate a \`pr-description.md\` file if you are proposing fixes." >> trigger_context.md
|
||||
echo "**CRITICAL System Directive**: You MUST ONLY propose and implement a **SINGLE** improvement or fix per run. Bundling unrelated changes (e.g., a documentation update and a script fix, or a metrics update and a logic fix) into a single PR is STRICTLY FORBIDDEN and will result in immediate rejection during the critique phase. If you identify multiple issues, pick the most impactful one and ignore the others for now." >> trigger_context.md
|
||||
else
|
||||
echo "**System Directive**: PR creation is DISABLED for this run. You MUST NOT stage files or attempt to create a PR description." >> trigger_context.md
|
||||
fi
|
||||
echo "" >> trigger_context.md
|
||||
|
||||
cat trigger_context.md "$PROMPT_PATH" > combined_prompt.md
|
||||
node bundle/gemini.js --policy tools/gemini-cli-bot/ci-policy.toml --prompt="$(cat combined_prompt.md)"
|
||||
|
||||
if [ -n "$TRIGGER_ISSUE_NUMBER" ] && [ ! -s "issue-comment.md" ] && [ ! -s "pr-comment.md" ]; then
|
||||
echo "Agent failed to respond. Generating fallback error message."
|
||||
echo "⚠️ **Gemini CLI Bot failed to generate a response.**" > "issue-comment.md"
|
||||
echo "" >> "issue-comment.md"
|
||||
echo "I encountered an error or failed to generate a complete response to your request. You can check the [GitHub Actions Run Log](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) for more details on what went wrong." >> "issue-comment.md"
|
||||
fi
|
||||
|
||||
- name: 'Run Critique Phase'
|
||||
if: "${{ github.event.inputs.enable_prs == 'true' || github.event_name == 'issue_comment' || github.event.inputs.run_interactive == 'true' }}"
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
GEMINI_MODEL: 'gemini-3-flash-preview'
|
||||
GEMINI_CLI_HOME: 'tools/gemini-cli-bot'
|
||||
run: |
|
||||
if git diff --staged --quiet; then
|
||||
echo "No changes staged. Skipping critique."
|
||||
echo "[APPROVED]" > critique_result.txt
|
||||
else
|
||||
node bundle/gemini.js --policy tools/gemini-cli-bot/ci-policy.toml --prompt="$(cat tools/gemini-cli-bot/.gemini/skills/critique/SKILL.md)" 2>&1 | tee critique_output.log
|
||||
|
||||
if [ "${PIPESTATUS[0]}" -eq 0 ] && grep -q "\[APPROVED\]" critique_output.log && ! grep -q "\[REJECTED\]" critique_output.log; then
|
||||
echo "[APPROVED]" > critique_result.txt
|
||||
else
|
||||
echo "Critique failed, rejected, or did not explicitly approve changes. Skipping PR creation."
|
||||
echo "[REJECTED]" > critique_result.txt
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: 'Generate Patch'
|
||||
if: "${{ github.event.inputs.enable_prs == 'true' || github.event_name == 'issue_comment' || github.event.inputs.run_interactive == 'true' }}"
|
||||
run: |
|
||||
touch bot-changes.patch
|
||||
touch pr-description.md
|
||||
if [ -f critique_result.txt ] && grep -q "\[APPROVED\]" critique_result.txt && ! grep -q "\[REJECTED\]" critique_result.txt; then
|
||||
git diff --staged > bot-changes.patch
|
||||
else
|
||||
echo "Critique did not approve. Skipping patch generation."
|
||||
fi
|
||||
|
||||
- name: 'Archive Brain Data'
|
||||
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'brain-data'
|
||||
path: |
|
||||
tools/gemini-cli-bot/lessons-learned.md
|
||||
tools/gemini-cli-bot/history/*.csv
|
||||
bot-changes.patch
|
||||
pr-description.md
|
||||
branch-name.txt
|
||||
pr-comment.md
|
||||
pr-number.txt
|
||||
issue-comment.md
|
||||
retention-days: 90
|
||||
|
||||
publish:
|
||||
name: 'Publish Artifacts (Archive Layer)'
|
||||
needs: 'reasoning'
|
||||
runs-on: 'ubuntu-latest'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
# The publish phase is for archiving artifacts and optionally creating PRs.
|
||||
permissions:
|
||||
contents: 'write'
|
||||
pull-requests: 'write'
|
||||
actions: 'write'
|
||||
steps:
|
||||
- name: 'Generate GitHub App Token 🔑'
|
||||
id: 'generate_token'
|
||||
if: "${{ github.event.inputs.enable_prs == 'true' || github.event_name == 'issue_comment' || github.event.inputs.run_interactive == 'true' }}"
|
||||
uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b' # ratchet:actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: '${{ secrets.APP_ID }}'
|
||||
private-key: '${{ secrets.PRIVATE_KEY }}'
|
||||
owner: '${{ github.repository_owner }}'
|
||||
repositories: '${{ github.event.repository.name }}'
|
||||
permission-contents: 'write'
|
||||
permission-pull-requests: 'write'
|
||||
permission-issues: 'write'
|
||||
|
||||
- name: 'Determine Checkout Ref'
|
||||
id: 'determine_ref'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}'
|
||||
run: |
|
||||
REF="main"
|
||||
if [ -n "$ISSUE_NUMBER" ]; then
|
||||
PR_HEAD=$(gh pr view "$ISSUE_NUMBER" --repo "${{ github.repository }}" --json headRefName --jq .headRefName 2>/dev/null || echo "")
|
||||
if [ -n "$PR_HEAD" ]; then
|
||||
REF="$PR_HEAD"
|
||||
fi
|
||||
fi
|
||||
echo "ref=$REF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
ref: '${{ steps.determine_ref.outputs.ref }}'
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Download Brain Data'
|
||||
uses: 'actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093' # ratchet:actions/download-artifact@v4
|
||||
with:
|
||||
name: 'brain-data'
|
||||
path: '${{ runner.temp }}/brain-data/'
|
||||
|
||||
- name: 'Create or Update PR'
|
||||
if: "${{ github.event.inputs.enable_prs == 'true' || github.event_name == 'issue_comment' || github.event.inputs.run_interactive == 'true' }}"
|
||||
env:
|
||||
GH_TOKEN: '${{ steps.generate_token.outputs.token }}'
|
||||
FALLBACK_PAT: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
|
||||
run: |
|
||||
if [ -s "${{ runner.temp }}/brain-data/bot-changes.patch" ]; then
|
||||
git config user.name "gemini-cli[bot]"
|
||||
git config user.email "gemini-cli[bot]@users.noreply.github.com"
|
||||
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git"
|
||||
|
||||
BRANCH_NAME="bot/productivity-updates-$(date +'%Y%m%d%H%M%S')-${{ github.run_id }}"
|
||||
if [ -f "${{ runner.temp }}/brain-data/branch-name.txt" ]; then
|
||||
BRANCH_NAME=$(cat "${{ runner.temp }}/brain-data/branch-name.txt")
|
||||
fi
|
||||
|
||||
if [[ ! "$BRANCH_NAME" =~ ^bot/ ]]; then
|
||||
echo "Error: Branch name '$BRANCH_NAME' does not start with 'bot/'. Safety abort."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git checkout -B "$BRANCH_NAME"
|
||||
git apply "${{ runner.temp }}/brain-data/bot-changes.patch"
|
||||
git add .
|
||||
|
||||
if [ -s "${{ runner.temp }}/brain-data/pr-description.md" ]; then
|
||||
git commit -F "${{ runner.temp }}/brain-data/pr-description.md"
|
||||
else
|
||||
git commit -m "🤖 Gemini Bot Productivity Optimizations"
|
||||
fi
|
||||
|
||||
PR_TITLE="🤖 Gemini Bot Productivity Optimizations"
|
||||
if [ -s "${{ runner.temp }}/brain-data/pr-description.md" ]; then
|
||||
PR_TITLE=$(head -n 1 "${{ runner.temp }}/brain-data/pr-description.md")
|
||||
fi
|
||||
|
||||
if ! git push origin "$BRANCH_NAME" --force; then
|
||||
echo "Push failed. Retrying with FALLBACK_PAT..."
|
||||
export GH_TOKEN="$FALLBACK_PAT"
|
||||
git remote set-url origin "https://x-access-token:${FALLBACK_PAT}@github.com/${{ github.repository }}.git"
|
||||
git push origin "$BRANCH_NAME" --force
|
||||
fi
|
||||
|
||||
if ! gh pr view "$BRANCH_NAME" > /dev/null 2>&1; then
|
||||
gh pr create --draft --title "$PR_TITLE" --body-file "${{ runner.temp }}/brain-data/pr-description.md" --head "$BRANCH_NAME" --base main || \
|
||||
gh pr create --draft --title "🤖 Gemini Bot Productivity Optimizations" --body "Automated changes generated by Gemini CLI Bot." --head "$BRANCH_NAME" --base main
|
||||
else
|
||||
PR_STATE=$(gh pr view "$BRANCH_NAME" --json state --jq .state)
|
||||
if [ "$PR_STATE" = "CLOSED" ]; then
|
||||
NEW_BRANCH_NAME="${BRANCH_NAME}-retry-${{ github.run_id }}"
|
||||
git checkout -b "$NEW_BRANCH_NAME"
|
||||
git push origin "$NEW_BRANCH_NAME" --force
|
||||
gh pr create --draft --title "$PR_TITLE" --body-file "${{ runner.temp }}/brain-data/pr-description.md" --head "$NEW_BRANCH_NAME" --base main || \
|
||||
gh pr create --draft --title "🤖 Gemini Bot Productivity Optimizations" --body "Automated changes generated by Gemini CLI Bot." --head "$NEW_BRANCH_NAME" --base main
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: 'Post PR/Issue Comment'
|
||||
env:
|
||||
GH_TOKEN: '${{ steps.generate_token.outputs.token }}'
|
||||
TRIGGER_ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}'
|
||||
run: |
|
||||
if [ -s "${{ runner.temp }}/brain-data/issue-comment.md" ] && [ -n "$TRIGGER_ISSUE_NUMBER" ]; then
|
||||
echo "Posting comment to triggering issue #$TRIGGER_ISSUE_NUMBER"
|
||||
# Use REST API (gh api) instead of GraphQL (gh issue comment) to ensure robot identity
|
||||
# while avoiding potential GraphQL-specific authorization hurdles with PATs.
|
||||
gh api "repos/${{ github.repository }}/issues/$TRIGGER_ISSUE_NUMBER/comments" -F body=@"${{ runner.temp }}/brain-data/issue-comment.md"
|
||||
fi
|
||||
|
||||
if [ -s "${{ runner.temp }}/brain-data/pr-comment.md" ] && [ -f "${{ runner.temp }}/brain-data/pr-number.txt" ]; then
|
||||
PR_NUM=$(cat "${{ runner.temp }}/brain-data/pr-number.txt")
|
||||
|
||||
# Using GitHub App, so author check is no longer valid against gemini-cli-robot
|
||||
# Skipping author validation here to let the app post.
|
||||
|
||||
# Use REST API (gh api) for consistency and robot identity
|
||||
gh api "repos/${{ github.repository }}/issues/$PR_NUM/comments" -F body=@"${{ runner.temp }}/brain-data/pr-comment.md"
|
||||
fi
|
||||
@@ -0,0 +1,49 @@
|
||||
name: '🔄 Gemini CLI Bot: Pulse'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '*/30 * * * *' # Every 30 minutes
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.ref }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: 'write'
|
||||
issues: 'write'
|
||||
pull-requests: 'write'
|
||||
|
||||
jobs:
|
||||
pulse:
|
||||
name: 'Pulse (Reflex Layer)'
|
||||
runs-on: 'ubuntu-latest'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Run Reflex Processes'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
run: |
|
||||
if [ -d "tools/gemini-cli-bot/reflexes/scripts" ] && [ "$(ls -A tools/gemini-cli-bot/reflexes/scripts)" ]; then
|
||||
for script in tools/gemini-cli-bot/reflexes/scripts/*.ts; do
|
||||
echo "Running reflex script: $script"
|
||||
npx tsx "$script"
|
||||
done
|
||||
else
|
||||
echo "No reflex scripts found."
|
||||
fi
|
||||
@@ -0,0 +1,47 @@
|
||||
name: '🔄 Gemini Scheduled Lifecycle Manager'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *' # Once a day
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: 'Run in dry-run mode (no changes applied)'
|
||||
required: false
|
||||
default: false
|
||||
type: 'boolean'
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
issues: 'write'
|
||||
pull-requests: 'write'
|
||||
|
||||
jobs:
|
||||
manage-lifecycle:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- name: 'Generate GitHub App Token'
|
||||
id: 'generate_token'
|
||||
uses: 'actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349' # ratchet:actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: '${{ secrets.APP_ID }}'
|
||||
private-key: '${{ secrets.PRIVATE_KEY }}'
|
||||
|
||||
- name: 'Checkout repository'
|
||||
uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Lifecycle Management'
|
||||
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
|
||||
env:
|
||||
DRY_RUN: '${{ inputs.dry_run }}'
|
||||
with:
|
||||
github-token: '${{ steps.generate_token.outputs.token }}'
|
||||
script: |
|
||||
const script = require('./.github/scripts/gemini-lifecycle-manager.cjs');
|
||||
await script({github, context, core});
|
||||
@@ -0,0 +1,120 @@
|
||||
name: '📋 Gemini Scheduled Issue Deduplication'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 * * * *' # Runs every hour
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: 'bash'
|
||||
|
||||
jobs:
|
||||
refresh-embeddings:
|
||||
if: |-
|
||||
${{ vars.TRIAGE_DEDUPLICATE_ISSUES != '' && github.repository == 'google-gemini/gemini-cli' }}
|
||||
permissions:
|
||||
contents: 'read'
|
||||
id-token: 'write' # Required for WIF, see https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-google-cloud-platform#adding-permissions-settings
|
||||
issues: 'read'
|
||||
statuses: 'read'
|
||||
packages: 'read'
|
||||
timeout-minutes: 20
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Log in to GitHub Container Registry'
|
||||
uses: 'docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1' # ratchet:docker/login-action@v3
|
||||
with:
|
||||
registry: 'ghcr.io'
|
||||
username: '${{ github.actor }}'
|
||||
password: '${{ secrets.GITHUB_TOKEN }}'
|
||||
|
||||
- name: 'Run Gemini Issue Deduplication Refresh'
|
||||
uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0
|
||||
id: 'gemini_refresh_embeddings'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
ISSUE_TITLE: '${{ github.event.issue.title }}'
|
||||
ISSUE_BODY: '${{ github.event.issue.body }}'
|
||||
ISSUE_NUMBER: '${{ github.event.issue.number }}'
|
||||
REPOSITORY: '${{ github.repository }}'
|
||||
FIRESTORE_PROJECT: '${{ vars.FIRESTORE_PROJECT }}'
|
||||
with:
|
||||
upload_artifacts: 'true'
|
||||
gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
|
||||
gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
|
||||
gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}'
|
||||
gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
|
||||
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
|
||||
use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}'
|
||||
use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
|
||||
settings: |-
|
||||
{
|
||||
"mcpServers": {
|
||||
"issue_deduplication": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"--network", "host",
|
||||
"-e", "GITHUB_TOKEN",
|
||||
"-e", "GEMINI_API_KEY",
|
||||
"-e", "DATABASE_TYPE",
|
||||
"-e", "FIRESTORE_DATABASE_ID",
|
||||
"-e", "GCP_PROJECT",
|
||||
"-e", "GOOGLE_APPLICATION_CREDENTIALS=/app/gcp-credentials.json",
|
||||
"-v", "${GOOGLE_APPLICATION_CREDENTIALS}:/app/gcp-credentials.json",
|
||||
"ghcr.io/google-gemini/gemini-cli-issue-triage@sha256:e3de1523f6c83aabb3c54b76d08940a2bf42febcb789dd2da6f95169641f94d3"
|
||||
],
|
||||
"env": {
|
||||
"GITHUB_TOKEN": "${GITHUB_TOKEN}",
|
||||
"GEMINI_API_KEY": "${{ secrets.GEMINI_API_KEY }}",
|
||||
"DATABASE_TYPE":"firestore",
|
||||
"GCP_PROJECT": "${FIRESTORE_PROJECT}",
|
||||
"FIRESTORE_DATABASE_ID": "(default)",
|
||||
"GOOGLE_APPLICATION_CREDENTIALS": "${GOOGLE_APPLICATION_CREDENTIALS}"
|
||||
},
|
||||
"timeout": 600000
|
||||
}
|
||||
},
|
||||
"maxSessionTurns": 25,
|
||||
"tools": {
|
||||
"core": [
|
||||
"run_shell_command(echo)"
|
||||
]
|
||||
},
|
||||
"telemetry": {
|
||||
"enabled": true,
|
||||
"target": "gcp"
|
||||
}
|
||||
}
|
||||
prompt: |-
|
||||
## Role
|
||||
|
||||
You are a database maintenance assistant for a GitHub issue deduplication system.
|
||||
|
||||
## Goal
|
||||
|
||||
Your sole responsibility is to refresh the embeddings for all open issues in the repository to ensure the deduplication database is up-to-date.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Extract Repository Information:** The repository is ${{ github.repository }}.
|
||||
2. **Refresh Embeddings:** Call the `refresh` tool with the correct `repo`. Do not use the `force` parameter.
|
||||
3. **Log Output:** Print the JSON output from the `refresh` tool to the logs.
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Only use the `refresh` tool.
|
||||
- Do not attempt to find duplicates or modify any issues.
|
||||
- Your only task is to call the `refresh` tool and log its output.
|
||||
@@ -0,0 +1,463 @@
|
||||
name: '📋 Gemini Scheduled Issue Triage'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 * * * *' # Runs every hour
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.event.number || github.run_id }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: 'bash'
|
||||
|
||||
permissions:
|
||||
id-token: 'write'
|
||||
issues: 'write'
|
||||
|
||||
jobs:
|
||||
triage-issues:
|
||||
timeout-minutes: 60
|
||||
if: |-
|
||||
${{ github.repository == 'google-gemini/gemini-cli' }}
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Install Utilities'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ripgrep
|
||||
|
||||
- name: 'Get Current Version'
|
||||
id: 'get_version'
|
||||
run: |
|
||||
VERSION=$(jq -r .version package.json | cut -d'-' -f1)
|
||||
echo "version=${VERSION}" >> "${GITHUB_OUTPUT}"
|
||||
echo "🚀 Current CLI Version: ${VERSION}"
|
||||
|
||||
- name: 'Generate GitHub App Token'
|
||||
id: 'generate_token'
|
||||
uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b' # ratchet:actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: '${{ secrets.APP_ID }}'
|
||||
private-key: '${{ secrets.PRIVATE_KEY }}'
|
||||
permission-issues: 'write'
|
||||
|
||||
- name: 'Get issue from event'
|
||||
if: |-
|
||||
${{ github.event_name == 'issues' }}
|
||||
id: 'get_issue_from_event'
|
||||
env:
|
||||
ISSUE_EVENT: '${{ toJSON(github.event.issue) }}'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "$ISSUE_EVENT" | jq -c '[{number: .number, title: .title, body: .body}]' > issues_to_triage.json
|
||||
echo "has_issues=true" >> "${GITHUB_OUTPUT}"
|
||||
echo "✅ Found issue #${{ github.event.issue.number }} from event to triage! 🎯"
|
||||
|
||||
- name: 'Sync Issue Types'
|
||||
if: |-
|
||||
${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }}
|
||||
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
|
||||
with:
|
||||
github-token: '${{ steps.generate_token.outputs.token }}'
|
||||
script: |-
|
||||
const syncIssueTypes = require('./.github/scripts/sync-issue-types.cjs');
|
||||
await syncIssueTypes({ github, context, core });
|
||||
|
||||
- name: 'Find Issues with Conflicting Labels'
|
||||
if: |-
|
||||
${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }}
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ steps.generate_token.outputs.token }}'
|
||||
GITHUB_REPOSITORY: '${{ github.repository }}'
|
||||
run: |-
|
||||
set -euo pipefail
|
||||
echo '🔍 Fetching open issues to find conflicts...'
|
||||
# Fetch up to 2000 open issues in one quick GraphQL-backed query
|
||||
gh issue list --repo "${GITHUB_REPOSITORY}" --search "is:issue is:open" --limit 2000 --json number,title,body,labels > all_open_issues.json
|
||||
|
||||
echo '🧹 Filtering issues with multiple area/ or priority/ labels...'
|
||||
jq -c '[ .[] | select( (.labels | map(select(.name | startswith("area/"))) | length) > 1 or (.labels | map(select(.name | startswith("priority/"))) | length) > 1 ) ] | .[0:50]' all_open_issues.json > conflicting_labels_issues.json
|
||||
|
||||
CONFLICT_COUNT=$(jq 'length' conflicting_labels_issues.json)
|
||||
echo "Found ${CONFLICT_COUNT} issues with conflicting labels (capped at 50 for processing)."
|
||||
|
||||
- name: 'Find untriaged issues'
|
||||
if: |-
|
||||
${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }}
|
||||
id: 'find_issues'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ steps.generate_token.outputs.token }}'
|
||||
GITHUB_REPOSITORY: '${{ github.repository }}'
|
||||
run: |-
|
||||
set -euo pipefail
|
||||
|
||||
echo '🔍 Finding issues missing area labels...'
|
||||
gh issue list --repo "${GITHUB_REPOSITORY}" \
|
||||
--search 'is:open is:issue -label:area/core -label:area/agent -label:area/enterprise -label:area/non-interactive -label:area/security -label:area/platform -label:area/extensions -label:area/documentation -label:area/unknown' --limit 50 --json number,title,body,labels > no_area_issues.json
|
||||
|
||||
echo '🔍 Finding issues missing kind labels...'
|
||||
gh issue list --repo "${GITHUB_REPOSITORY}" \
|
||||
--search 'is:open is:issue -label:kind/bug -label:kind/enhancement -label:kind/customer-issue -label:kind/question' --limit 50 --json number,title,body,labels > no_kind_issues.json
|
||||
|
||||
echo '🏷️ Finding issues missing priority labels...'
|
||||
gh issue list --repo "${GITHUB_REPOSITORY}" \
|
||||
--search 'is:open is:issue -label:priority/p0 -label:priority/p1 -label:priority/p2 -label:priority/p3 -label:priority/unknown' --limit 50 --json number,title,body,labels > no_priority_issues.json
|
||||
|
||||
echo '📏 Finding issues missing effort labels...'
|
||||
gh issue list --repo "${GITHUB_REPOSITORY}" \
|
||||
--search 'is:open is:issue -label:effort/small -label:effort/medium -label:effort/large label:area/core,area/extensions,area/site,area/non-interactive' --limit 20 --json number,title,body,labels > no_effort_issues.json
|
||||
|
||||
echo '🔄 Merging and deduplicating standard triage issues...'
|
||||
if [ ! -f conflicting_labels_issues.json ]; then echo "[]" > conflicting_labels_issues.json; fi
|
||||
jq -c -s 'add | unique_by(.number)' no_area_issues.json no_kind_issues.json no_priority_issues.json conflicting_labels_issues.json > standard_issues_to_triage.json
|
||||
|
||||
echo '📏 Deduplicating effort issues...'
|
||||
jq -c -s 'add | unique_by(.number)' no_effort_issues.json > effort_issues_to_triage.json
|
||||
|
||||
STANDARD_COUNT="$(jq 'length' standard_issues_to_triage.json)"
|
||||
EFFORT_COUNT="$(jq 'length' effort_issues_to_triage.json)"
|
||||
if [ "$STANDARD_COUNT" -gt 0 ] || [ "$EFFORT_COUNT" -gt 0 ]; then
|
||||
echo "has_issues=true" >> "${GITHUB_OUTPUT}"
|
||||
echo "has_standard_issues=$([ "$STANDARD_COUNT" -gt 0 ] && echo 'true' || echo 'false')" >> "${GITHUB_OUTPUT}"
|
||||
echo "has_effort_issues=$([ "$EFFORT_COUNT" -gt 0 ] && echo 'true' || echo 'false')" >> "${GITHUB_OUTPUT}"
|
||||
else
|
||||
echo "has_issues=false" >> "${GITHUB_OUTPUT}"
|
||||
echo "has_standard_issues=false" >> "${GITHUB_OUTPUT}"
|
||||
echo "has_effort_issues=false" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
echo "✅ Found ${STANDARD_COUNT} standard issues and ${EFFORT_COUNT} effort issues to triage! 🎯"
|
||||
|
||||
- name: 'Create Gemini CLI Experiments Override'
|
||||
if: |-
|
||||
steps.get_issue_from_event.outputs.has_issues == 'true' || steps.find_issues.outputs.has_issues == 'true'
|
||||
run: |
|
||||
cat << 'EOF' > gemini_exp.json
|
||||
{
|
||||
"flags": [
|
||||
{
|
||||
"flagId": 45750526,
|
||||
"boolValue": false
|
||||
}
|
||||
],
|
||||
"experimentIds": []
|
||||
}
|
||||
EOF
|
||||
|
||||
- name: 'Get Repository Labels'
|
||||
id: 'get_labels'
|
||||
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
|
||||
with:
|
||||
github-token: '${{ steps.generate_token.outputs.token }}'
|
||||
script: |-
|
||||
const { data: labels } = await github.rest.issues.listLabelsForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
});
|
||||
const labelNames = labels.map(label => label.name);
|
||||
core.setOutput('available_labels', labelNames.join(','));
|
||||
core.info(`Found ${labelNames.length} labels: ${labelNames.join(', ')}`);
|
||||
return labelNames;
|
||||
|
||||
- name: 'Run Standard Triage Analysis'
|
||||
if: |-
|
||||
steps.get_issue_from_event.outputs.has_issues == 'true' || steps.find_issues.outputs.has_standard_issues == 'true'
|
||||
uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0
|
||||
id: 'gemini_standard_issue_analysis'
|
||||
env:
|
||||
GITHUB_TOKEN: '' # Do not pass any auth token here since this runs on untrusted inputs
|
||||
REPOSITORY: '${{ github.repository }}'
|
||||
AVAILABLE_LABELS: '${{ steps.get_labels.outputs.available_labels }}'
|
||||
CLI_VERSION: '${{ steps.get_version.outputs.version }}'
|
||||
GEMINI_CLI_TRUST_WORKSPACE: 'true'
|
||||
GEMINI_EXP: 'gemini_exp.json'
|
||||
GEMINI_STRICT_TELEMETRY_LIMITS: 'true'
|
||||
GEMINI_MODEL: 'gemini-3-flash-preview'
|
||||
with:
|
||||
upload_artifacts: 'true'
|
||||
gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
|
||||
gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
|
||||
gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}'
|
||||
gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
|
||||
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
|
||||
use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}'
|
||||
use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
|
||||
settings: |-
|
||||
{
|
||||
"maxSessionTurns": 25,
|
||||
"tools": {
|
||||
"core": [
|
||||
"run_shell_command(echo)",
|
||||
"read_file"
|
||||
]
|
||||
},
|
||||
"telemetry": {
|
||||
"enabled": false,
|
||||
"target": "gcp"
|
||||
}
|
||||
}
|
||||
prompt: |-
|
||||
## Role
|
||||
|
||||
You are an issue triage assistant. Analyze issues and identify
|
||||
appropriate labels. Use the available tools to gather information;
|
||||
do not ask for information to be provided.
|
||||
|
||||
## Steps
|
||||
|
||||
1. You are only able to use the echo and read_file commands. Review the available labels in the environment variable: "${AVAILABLE_LABELS}".
|
||||
2. Use the read_file tool to read the file "standard_issues_to_triage.json" which contains the JSON array of issues to triage (including their current labels).
|
||||
3. Review the issue title, body, current labels, and any comments provided in the JSON file.
|
||||
4. Identify the most relevant labels from the existing labels, specifically focusing on area/*, kind/*, and priority/*.
|
||||
5. Label Policy:
|
||||
- If the issue already has a kind/ label, do not change it.
|
||||
- If the issue has exactly ONE priority/ label, do not change it (unless you are explicitly re-evaluating an ambiguous priority).
|
||||
- If the issue is missing a priority/ label, OR if the issue currently has MULTIPLE priority/ labels, you must evaluate the issue's impact to determine exactly ONE priority level (priority/p0, priority/p1, priority/p2, priority/p3, or priority/unknown) based the guidelines. If you are fixing an issue with multiple priority/ labels, put the correct one in `labels_to_add` and put all the incorrect ones in `labels_to_remove`.
|
||||
- If the issue has exactly ONE area/ label, do not change it.
|
||||
- If the issue is missing an area/ label, OR if the issue currently has MULTIPLE area/ labels, select exactly ONE area/ label that best fits the issue. Issues MUST NOT have multiple area/ labels. If you are fixing an issue with multiple area/ labels, put the correct one in `labels_to_add` and put all the incorrect ones in `labels_to_remove`.
|
||||
- If any of these are missing, select exactly ONE appropriate label for the missing category.
|
||||
6. Identify other applicable labels based on the issue content, such as status/*, help wanted, good first issue, etc.
|
||||
7. Give me a single short explanation about why you are selecting each label in the process.
|
||||
8. Output a JSON array of objects, each containing the issue number
|
||||
and the labels to add and remove, along with an explanation. For example:
|
||||
```
|
||||
[
|
||||
{
|
||||
"issue_number": 123,
|
||||
"labels_to_add": ["area/core", "kind/bug", "priority/p2"],
|
||||
"labels_to_remove": ["status/need-triage"],
|
||||
"explanation": "This issue is a UI bug that needs to be addressed with medium priority."
|
||||
}
|
||||
]
|
||||
```
|
||||
If an issue cannot be classified, do not include it in the output array.
|
||||
9. For each issue, carefully check if the CLI version is present. It is usually found under the "### Client information" header, as a bullet point (e.g., "• CLI Version: 0.33.1", "* **CLI Version:** 0.42.0"), or in the output of the `/about` command.
|
||||
- **Only for issues classified as kind/bug:** If the version is provided but is more than 6 minor versions older than the most recent release (current version is ${{ steps.get_version.outputs.version }}), apply the status/need-information label and leave a comment politely asking the user to verify if the issue persists in the latest version.
|
||||
10. **Only for issues classified as kind/bug:** If the issue does not have sufficient information, recommend the status/need-information label and leave a comment politely requesting the missing details. For example, if repro steps are missing, ask for them; if the CLI version is completely missing, ask for the version information in the explanation section below. Do not ask for version info if it is already in the issue body. (Check both bullet points and bold text). For features and enhancements, the CLI version is NOT required.
|
||||
11. If you think an issue is a Priority/P0, you MUST apply the priority/p1 label AND the status/manual-triage label, and include a note in your explanation that it likely requires P0 escalation.
|
||||
12. If the issue is highly ambiguous, completely lacks a description, or you are torn between two lower priorities (like P2 vs P3), you MUST retain the existing priority label if one is already present. Do not toggle the priority if you do not have enough information to make a definitive change.
|
||||
13. If you are uncertain about a category, use the area/unknown, kind/question, or priority/unknown labels as appropriate. If you are extremely uncertain, apply the status/manual-triage label.
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Output only valid JSON format
|
||||
- Do not include any explanation or additional text, just the JSON
|
||||
- Only use labels that already exist in the repository.
|
||||
- Do not add comments or modify the issue content.
|
||||
- Do not remove the following labels maintainer, help wanted or good first issue.
|
||||
- Triage only the current issue.
|
||||
- Identify exactly ONE area/ label. Do NOT assign multiple area/ labels to a single issue.
|
||||
- Identify only one kind/ label (Do not apply kind/duplicate or kind/parent-issue)
|
||||
- Identify exactly ONE priority/ label. Do NOT assign multiple priority/ labels to a single issue.
|
||||
- **Do not manually downgrade the priority.** Always assign the true priority based on the guidelines. The system will handle downgrades programmatically if information is missing.
|
||||
- **NEVER mention label names, label removals, or label additions in your `explanation`.** The explanation must be purely written for the user (e.g., "Please provide your CLI version.") without exposing internal triage mechanics (e.g., do NOT say "Removing area/unknown to leave only area/core").
|
||||
|
||||
Categorization Guidelines (Priority):
|
||||
P0 - Urgent Blocking Issues:
|
||||
- DO NOT APPLY THE priority/p0 LABEL AUTOMATICALLY. Instead apply priority/p1 and status/manual-triage.
|
||||
- Definition: Critical failures breaking core functionality for a large portion of users. Examples: CLI fails to launch globally, core commands (gemini run) crash on valid input, unhandled promise rejections on boot, critical security vulnerability.
|
||||
- Note: You must apply priority/p1 and status/manual-triage instead of priority/p0.
|
||||
P1 - Critical but Workable:
|
||||
- Definition: Severe issues without a reasonable workaround, significantly degrading the developer experience but not globally blocking. Examples: Specific tools failing consistently (e.g., `web_search` returns 500s), persistent PTY streaming hangs, memory leaks leading to OOM after short use.
|
||||
P2 - Significant Issues:
|
||||
- Definition: Affect some workflows but a clear workaround exists, or non-critical bugs. Examples: Theme flickering, confusing error messages, minor UI misalignment, failing to read deeply nested config files correctly.
|
||||
P3 - Minor/Enhancements:
|
||||
- Definition: Trivial bugs, typos, documentation requests, or feature requests.
|
||||
|
||||
Categorization Guidelines (Kind):
|
||||
kind/bug: The issue is describing an unexpected behavior or failure in the application.
|
||||
kind/enhancement: The issue is describing a feature request or an improvement to an existing feature.
|
||||
kind/question: The issue is asking a question about how to use the CLI or about a specific feature.
|
||||
|
||||
Categorization Guidelines (Area):
|
||||
area/agent: The "brain" of the CLI. Core agent logic, model quality, tool/function calling, memory, web search, generated code quality, sub-agents.
|
||||
area/core: The fundamental CLI app. UI/UX, installation, OS compatibility, performance, command parsing, theming, flickering.
|
||||
area/documentation: Website docs, READMEs, inline help text.
|
||||
area/enterprise: Telemetry, Policy, Quota / Licensing
|
||||
area/extensions: Gemini CLI extensions capability
|
||||
area/non-interactive: GitHub Actions, SDK, 3P Integrations, Shell Scripting, Command line automation
|
||||
area/platform: Platform specific behavior
|
||||
area/security: Authentication, authorization, privacy, data leaks, credential storage.
|
||||
|
||||
- name: 'Stop Telemetry Collector'
|
||||
if: |-
|
||||
steps.find_issues.outputs.has_effort_issues == 'true'
|
||||
run: 'docker rm -f gemini-telemetry-collector || true'
|
||||
|
||||
- name: 'Run Effort Triage Analysis'
|
||||
if: |-
|
||||
steps.find_issues.outputs.has_effort_issues == 'true'
|
||||
uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0
|
||||
id: 'gemini_effort_issue_analysis'
|
||||
env:
|
||||
GITHUB_TOKEN: '' # Do not pass any auth token here since this runs on untrusted inputs
|
||||
REPOSITORY: '${{ github.repository }}'
|
||||
AVAILABLE_LABELS: '${{ steps.get_labels.outputs.available_labels }}'
|
||||
CLI_VERSION: '${{ steps.get_version.outputs.version }}'
|
||||
GEMINI_CLI_TRUST_WORKSPACE: 'true'
|
||||
GEMINI_EXP: 'gemini_exp.json'
|
||||
GEMINI_STRICT_TELEMETRY_LIMITS: 'true'
|
||||
GEMINI_MODEL: 'gemini-3-flash-preview'
|
||||
with:
|
||||
upload_artifacts: 'true'
|
||||
gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
|
||||
gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
|
||||
gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}'
|
||||
gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
|
||||
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
|
||||
use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}'
|
||||
use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
|
||||
settings: |-
|
||||
{
|
||||
"maxSessionTurns": 30,
|
||||
"tools": {
|
||||
"core": [
|
||||
"run_shell_command(echo)",
|
||||
"grep_search",
|
||||
"glob",
|
||||
"read_file"
|
||||
]
|
||||
},
|
||||
"telemetry": {
|
||||
"enabled": false,
|
||||
"target": "gcp"
|
||||
}
|
||||
}
|
||||
prompt: |-
|
||||
## Role
|
||||
|
||||
You are an expert software architect. Analyze the provided GitHub issues and assign the correct `effort/*` label based on the codebase complexity.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Use the read_file tool to read "effort_issues_to_triage.json".
|
||||
2. For each issue in the array:
|
||||
- You must evaluate the architectural complexity to determine the effort level. You MUST NOT guess the root cause. You MUST actively use your codebase search tools (grep_search and glob) to search for keywords from the issue and explore the codebase. You must identify the specific files and components involved before deciding the effort.
|
||||
3. Output a JSON array of objects, each containing the issue number and the effort label to add, along with an explanation and an effort_analysis field. This effort_analysis must be highly detailed, technical, and empirical. It MUST NOT contain vague guesses (e.g., avoid words like "likely points to" or "possibly"). You must explicitly cite the specific file paths and architectural mechanisms you discovered using your search tools, explain the root cause, and then explicitly state how that complexity maps to the chosen effort level guidelines. For example:
|
||||
```
|
||||
[
|
||||
{
|
||||
"issue_number": 123,
|
||||
"labels_to_add": ["effort/small"],
|
||||
"explanation": "This is a simple logic fix.",
|
||||
"effort_analysis": "The `vscode-ide-companion` extension indiscriminately tracks active text editors via `vscode.window.onDidChangeActiveTextEditor` in `open-files-manager.ts`. When a user opens `.vscode/settings.json`, its content is sent to the CLI's context. The fix is highly localized to the VS Code companion extension's event listener. It involves adding a simple conditional check to exclude specific configuration files from the active editor tracking logic, which is a trivial logic adjustment with a clear root cause."
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Output only valid JSON format
|
||||
- Do not include any explanation or additional text, just the JSON
|
||||
- Triage only the current issue.
|
||||
|
||||
Categorization Guidelines (Effort):
|
||||
effort/small (1 day or less):
|
||||
- Trivial Logic & Config: Schema updates (Zod), feature flag toggles, adding missing fields to package.json or settings.json.
|
||||
- UI/Aesthetic Adjustments: Fixing minor layout bugs in Ink components (e.g., adding flexShrink, correcting padding in a single Box), text color changes.
|
||||
- Documentation & Strings: Typos, log message updates, CLI argument descriptions.
|
||||
- Localized Bug Fixes: Single-file logic errors, straightforward promise rejections (e.g., wrapping a known failure in a try/catch), simple regex or string parsing fixes.
|
||||
effort/medium (2-3 days):
|
||||
- React/Ink State Management: Debugging useState/useEffect/useReducer bugs, component lifecycle issues (memory leaks in the UI), terminal redraw flickering, or state synchronization between the CLI's internal input buffer and the interactive React components.
|
||||
- Asynchronous Flow & Integration: Resolving complex Promise chains, ERR_STREAM_PREMATURE_CLOSE, debugging IDE companion extensions (VS Code, Android Studio) or resolving hanging HTTP requests/IPC between the CLI and external plugins, timeouts in non-interactive/ACP modes.
|
||||
- Tooling & Output Parsers: Modifying how tools parse streaming stdout/stderr buffers, adding new built-in tools that don't require native bindings.
|
||||
- Cross-Component Refactors: Changes that span across packages/cli and packages/core to pass new data models or telemetry state.
|
||||
effort/large (3+ days):
|
||||
- Platform-Specific Complexities (PTY/Signals): Any issue involving node-pty, child_process.spawn, OS-level shell behavior (Windows vs Linux vs macOS), pseudo-terminal exhaustion (ENXIO), raw mode terminal desyncs, or POSIX signal forwarding (SIGINT/SIGTERM).
|
||||
- Core Architecture & Protocols: Refactoring the Scheduler, Agent-to-Agent (A2A) protocol implementation, low-level MCP (Model Context Protocol) transport mechanisms.
|
||||
- Performance & Memory: Diagnosing massive disk/memory leaks, severe boot time regressions, high-throughput streaming optimizations (e.g., voice streaming pipelines).
|
||||
Note: Any bug that is described as intermittent, flickering, difficult to reproduce, platform-specific, or requiring cross-environment setups (e.g., involving the VS Code IDE companion, GCA plugin, or Android Studio) MUST NOT be rated as effort/small because of the increased overhead of testing and reproducing.
|
||||
|
||||
Categorization Guidelines (Priority):
|
||||
P0 - Urgent Blocking Issues:
|
||||
- DO NOT APPLY THIS LABEL AUTOMATICALLY. Use status/manual-triage instead.
|
||||
- Definition: Urgent, block a significant percentage of the user base, and prevent frequent use of the Gemini CLI.
|
||||
- This includes core stability blockers (e.g., authentication failures, broken upgrades), critical crashes, and P0 security vulnerabilities.
|
||||
- Impact: Blocks development or testing for the entire team; Major security vulnerability; Causes data loss or corruption with no workaround; Crashes the application or makes a core feature completely unusable for all or most users.
|
||||
- Qualifier: Is the main function of the software broken?
|
||||
P1 - High-Impact Issues:
|
||||
- Definition: Affect a large number of users, blocking them from using parts of the Gemini CLI, or make the CLI frequently unusable even with workarounds available.
|
||||
- Impact: A core feature is broken or behaving incorrectly for a large number of users or use cases; Severe performance degradation; No straightforward workaround exists.
|
||||
- Qualifier: Is a key feature unusable or giving very wrong results?
|
||||
P2 - Significant Issues:
|
||||
- Definition: Affect some users significantly, such as preventing the use of certain features or authentication types.
|
||||
- Can also be issues that many users complain about, causing annoyance or hindering daily use.
|
||||
- Impact: Affects a non-critical feature or a smaller, specific subset of users; An inconvenient but functional workaround is available; Noticeable UI/UX problems that look unprofessional.
|
||||
- Qualifier: Is it an annoying but non-blocking problem?
|
||||
P3 - Low-Impact Issues:
|
||||
- Definition: Typically usability issues that cause annoyance to a limited user base.
|
||||
- Includes feature requests that could be addressed in the near future and may be suitable for community contributions.
|
||||
- Impact: Minor cosmetic issues; An edge-case bug that is very difficult to reproduce and affects a tiny fraction of users.
|
||||
- Qualifier: Is it a "nice-to-fix" issue?
|
||||
|
||||
Categorization Guidelines (Area):
|
||||
area/agent: Core Agent, Tools, Memory, Sub-Agents, Hooks, Agent Quality
|
||||
area/core: User Interface, OS Support, Core Functionality
|
||||
area/documentation: End-user and contributor-facing documentation, website-related
|
||||
area/enterprise: Telemetry, Policy, Quota / Licensing
|
||||
area/extensions: Gemini CLI extensions capability
|
||||
area/non-interactive: GitHub Actions, SDK, 3P Integrations, Shell Scripting, Command line automation
|
||||
area/platform: Build infra, Release mgmt, Testing, Eval infra, Capacity, Quota mgmt
|
||||
area/security: security related issues
|
||||
|
||||
Additional Context:
|
||||
- If users are talking about issues where the model gets downgraded from pro to flash then i want you to categorize that as a performance issue.
|
||||
- This product is designed to use different models eg.. using pro, downgrading to flash etc.
|
||||
- When users report that they dont expect the model to change those would be categorized as feature requests.
|
||||
|
||||
- name: 'Apply Triaged Labels'
|
||||
if: |-
|
||||
always() &&
|
||||
( (steps.gemini_standard_issue_analysis.outcome == 'success' && steps.gemini_standard_issue_analysis.outputs.summary != '[]' && steps.gemini_standard_issue_analysis.outputs.summary != '') ||
|
||||
(steps.gemini_effort_issue_analysis.outcome == 'success' && steps.gemini_effort_issue_analysis.outputs.summary != '[]' && steps.gemini_effort_issue_analysis.outputs.summary != '') )
|
||||
env:
|
||||
LABELS_OUTPUT_STANDARD: '${{ steps.gemini_standard_issue_analysis.outputs.summary }}'
|
||||
LABELS_OUTPUT_EFFORT: '${{ steps.gemini_effort_issue_analysis.outputs.summary }}'
|
||||
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
|
||||
with:
|
||||
github-token: '${{ steps.generate_token.outputs.token }}'
|
||||
script: |-
|
||||
const applyLabels = require('./.github/scripts/apply-issue-labels.cjs');
|
||||
await applyLabels({ github, context, core });
|
||||
|
||||
- name: 'Sync Issue Types (Post-Analysis)'
|
||||
if: |-
|
||||
always() && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
|
||||
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
|
||||
with:
|
||||
github-token: '${{ steps.generate_token.outputs.token }}'
|
||||
script: |-
|
||||
const syncIssueTypes = require('./.github/scripts/sync-issue-types.cjs');
|
||||
await syncIssueTypes({ github, context, core });
|
||||
|
||||
- name: 'Find Triaged Issues to Clean Up'
|
||||
if: |-
|
||||
always() && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ steps.generate_token.outputs.token }}'
|
||||
GITHUB_REPOSITORY: '${{ github.repository }}'
|
||||
run: |-
|
||||
set -euo pipefail
|
||||
echo '🧹 Finding issues that have conflicting status labels...'
|
||||
gh issue list --repo "${GITHUB_REPOSITORY}" \
|
||||
--search 'is:open is:issue label:status/bot-triaged label:status/need-triage' --limit 50 --json number > cleanup_1.json
|
||||
gh issue list --repo "${GITHUB_REPOSITORY}" \
|
||||
--search 'is:open is:issue label:status/bot-triaged label:status/manual-triage' --limit 50 --json number > cleanup_2.json
|
||||
jq -c -s 'add | unique_by(.number)' cleanup_1.json cleanup_2.json > issues_to_cleanup.json
|
||||
|
||||
- name: 'Clean Up Triage Labels'
|
||||
if: |-
|
||||
always() && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
|
||||
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
|
||||
with:
|
||||
github-token: '${{ steps.generate_token.outputs.token }}'
|
||||
script: |-
|
||||
const cleanupLabels = require('./.github/scripts/cleanup-triage-labels.cjs');
|
||||
await cleanupLabels({ github, context, core });
|
||||
@@ -0,0 +1,47 @@
|
||||
name: 'Gemini Scheduled PR Triage 🚀'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '*/15 * * * *' # Runs every 15 minutes
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
audit-prs:
|
||||
timeout-minutes: 15
|
||||
if: |-
|
||||
${{ github.repository == 'google-gemini/gemini-cli' }}
|
||||
permissions:
|
||||
contents: 'read'
|
||||
id-token: 'write'
|
||||
issues: 'write'
|
||||
pull-requests: 'write'
|
||||
runs-on: 'ubuntu-latest'
|
||||
outputs:
|
||||
prs_needing_comment: '${{ steps.run_triage.outputs.prs_needing_comment }}'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Generate GitHub App Token'
|
||||
id: 'generate_token'
|
||||
uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b' # ratchet:actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: '${{ secrets.APP_ID }}'
|
||||
private-key: '${{ secrets.PRIVATE_KEY }}'
|
||||
permission-issues: 'write'
|
||||
permission-pull-requests: 'write'
|
||||
|
||||
- name: 'Run PR Triage Script'
|
||||
id: 'run_triage'
|
||||
shell: 'bash'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ steps.generate_token.outputs.token }}'
|
||||
GITHUB_REPOSITORY: '${{ github.repository }}'
|
||||
run: |-
|
||||
./.github/scripts/pr-triage.sh
|
||||
# If prs_needing_comment is empty, set it to [] explicitly for downstream steps
|
||||
if [[ -z "$(grep 'prs_needing_comment' "${GITHUB_OUTPUT}" | cut -d'=' -f2-)" ]]; then
|
||||
echo "prs_needing_comment=[]" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
@@ -0,0 +1,150 @@
|
||||
name: 'Assign Issue on Comment'
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types:
|
||||
- 'created'
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.event.issue.number }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: 'bash'
|
||||
|
||||
permissions:
|
||||
contents: 'read'
|
||||
id-token: 'write'
|
||||
issues: 'write'
|
||||
statuses: 'write'
|
||||
packages: 'read'
|
||||
|
||||
jobs:
|
||||
self-assign-issue:
|
||||
if: |-
|
||||
github.repository == 'google-gemini/gemini-cli' &&
|
||||
github.event_name == 'issue_comment' &&
|
||||
(contains(github.event.comment.body, '/assign') || contains(github.event.comment.body, '/unassign'))
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- 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 }}'
|
||||
# Add 'assignments' write permission
|
||||
permission-issues: 'write'
|
||||
|
||||
- name: 'Assign issue to user'
|
||||
if: "contains(github.event.comment.body, '/assign')"
|
||||
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
|
||||
with:
|
||||
github-token: '${{ steps.generate_token.outputs.token }}'
|
||||
script: |
|
||||
const issueNumber = context.issue.number;
|
||||
const commenter = context.actor;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const MAX_ISSUES_ASSIGNED = 3;
|
||||
|
||||
const issue = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
});
|
||||
|
||||
const hasHelpWantedLabel = issue.data.labels.some(label => label.name === 'help wanted');
|
||||
|
||||
if (!hasHelpWantedLabel) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
issue_number: issueNumber,
|
||||
body: `👋 @${commenter}, thanks for your interest in this issue! We're reserving self-assignment for issues that have been marked with the \`help wanted\` label. Feel free to check out our list of [issues that need attention](https://github.com/google-gemini/gemini-cli/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22).`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Search for open issues already assigned to the commenter in this repo
|
||||
const { data: assignedIssues } = await github.rest.search.issuesAndPullRequests({
|
||||
q: `is:issue repo:${owner}/${repo} assignee:${commenter} is:open`,
|
||||
advanced_search: true
|
||||
});
|
||||
|
||||
if (assignedIssues.total_count >= MAX_ISSUES_ASSIGNED) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
issue_number: issueNumber,
|
||||
body: `👋 @${commenter}! You currently have ${assignedIssues.total_count} issues assigned to you. We have a ${MAX_ISSUES_ASSIGNED} max issues assigned at once policy. Once you close out an existing issue it will open up space to take another. You can also unassign yourself from an existing issue but please work on a hand-off if someone is expecting work on that issue.`
|
||||
});
|
||||
return; // exit
|
||||
}
|
||||
|
||||
if (issue.data.assignees.length > 0) {
|
||||
// Comment that it's already assigned
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
body: `@${commenter} Thanks for taking interest but this issue is already assigned. We'd still love to have you contribute. Check out our [Help Wanted](https://github.com/google-gemini/gemini-cli/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22help%20wanted%22) list for issues where we need some extra attention.`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If not taken, assign the user who commented
|
||||
await github.rest.issues.addAssignees({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
assignees: [commenter]
|
||||
});
|
||||
|
||||
// Post a comment to confirm assignment
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
body: `👋 @${commenter}, you've been assigned to this issue! Thank you for taking the time to contribute. Make sure to check out our [contributing guidelines](https://github.com/google-gemini/gemini-cli/blob/main/CONTRIBUTING.md).`
|
||||
});
|
||||
|
||||
- name: 'Unassign issue from user'
|
||||
if: "contains(github.event.comment.body, '/unassign')"
|
||||
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
|
||||
with:
|
||||
github-token: '${{ steps.generate_token.outputs.token }}'
|
||||
script: |
|
||||
const issueNumber = context.issue.number;
|
||||
const commenter = context.actor;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const commentBody = context.payload.comment.body.trim();
|
||||
|
||||
if (commentBody !== '/unassign') {
|
||||
return;
|
||||
}
|
||||
|
||||
const issue = await github.rest.issues.get({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
issue_number: issueNumber,
|
||||
});
|
||||
|
||||
const isAssigned = issue.data.assignees.some(assignee => assignee.login === commenter);
|
||||
|
||||
if (isAssigned) {
|
||||
await github.rest.issues.removeAssignees({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
issue_number: issueNumber,
|
||||
assignees: [commenter]
|
||||
});
|
||||
await github.rest.issues.createComment({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
issue_number: issueNumber,
|
||||
body: `👋 @${commenter}, you have been unassigned from this issue.`
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
name: '🏷️ Issue Opened Labeler'
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- 'opened'
|
||||
|
||||
jobs:
|
||||
label-issue:
|
||||
runs-on: 'ubuntu-latest'
|
||||
if: |-
|
||||
${{ github.repository == 'google-gemini/gemini-cli' || github.repository == 'google-gemini/maintainers-gemini-cli' }}
|
||||
steps:
|
||||
- name: 'Generate GitHub App Token'
|
||||
id: 'generate_token'
|
||||
env:
|
||||
APP_ID: '${{ secrets.APP_ID }}'
|
||||
if: |-
|
||||
${{ env.APP_ID != '' }}
|
||||
uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b' # ratchet:actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: '${{ secrets.APP_ID }}'
|
||||
private-key: '${{ secrets.PRIVATE_KEY }}'
|
||||
|
||||
- name: 'Add need-triage label'
|
||||
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
|
||||
with:
|
||||
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
|
||||
script: |-
|
||||
const { data: issue } = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
|
||||
const hasLabel = issue.labels.some(l => l.name === 'status/need-triage');
|
||||
if (!hasLabel) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: ['status/need-triage']
|
||||
});
|
||||
} else {
|
||||
core.info('Issue already has status/need-triage label. Skipping.');
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
name: 'Label Child Issues for Project Rollup'
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: ['opened', 'edited', 'reopened']
|
||||
schedule:
|
||||
- cron: '0 * * * *' # Run every hour
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: 'write'
|
||||
contents: 'read'
|
||||
|
||||
jobs:
|
||||
# Event-based: Quick reaction to new/edited issues in THIS repo
|
||||
labeler:
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && github.event_name == 'issues'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install Dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Run Multi-Repo Sync Script'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
run: 'node .github/scripts/sync-maintainer-labels.cjs'
|
||||
|
||||
# Scheduled/Manual: Recursive sync across multiple repos
|
||||
sync-maintainer-labels:
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')"
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install Dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Run Multi-Repo Sync Script'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
run: 'node .github/scripts/sync-maintainer-labels.cjs'
|
||||
@@ -0,0 +1,175 @@
|
||||
name: 'Label Workstream Rollup'
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: ['opened', 'edited', 'reopened']
|
||||
schedule:
|
||||
- cron: '0 * * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
labeler:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
issues: 'write'
|
||||
steps:
|
||||
- name: 'Check for Parent Workstream and Apply Label'
|
||||
uses: 'actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b' # ratchet:actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const labelToAdd = 'workstream-rollup';
|
||||
|
||||
// Allow-list of parent issue URLs
|
||||
const allowedParentUrls = [
|
||||
'https://github.com/google-gemini/gemini-cli/issues/15374',
|
||||
'https://github.com/google-gemini/gemini-cli/issues/15456',
|
||||
'https://github.com/google-gemini/gemini-cli/issues/15324',
|
||||
'https://github.com/google-gemini/gemini-cli/issues/17202',
|
||||
'https://github.com/google-gemini/gemini-cli/issues/17203'
|
||||
];
|
||||
|
||||
// Single issue processing (for event triggers)
|
||||
async function processSingleIssue(owner, repo, number) {
|
||||
const query = `
|
||||
query($owner:String!, $repo:String!, $number:Int!) {
|
||||
repository(owner:$owner, name:$repo) {
|
||||
issue(number:$number) {
|
||||
number
|
||||
parent {
|
||||
url
|
||||
parent {
|
||||
url
|
||||
parent {
|
||||
url
|
||||
parent {
|
||||
url
|
||||
parent {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
try {
|
||||
const result = await github.graphql(query, { owner, repo, number });
|
||||
|
||||
if (!result || !result.repository || !result.repository.issue) {
|
||||
console.log(`Issue #${number} not found or data missing.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const issue = result.repository.issue;
|
||||
await checkAndLabel(issue, owner, repo);
|
||||
} catch (error) {
|
||||
console.error(`Failed to process issue #${number}:`, error);
|
||||
throw error; // Re-throw to be caught by main execution
|
||||
}
|
||||
}
|
||||
|
||||
// Bulk processing (for schedule/dispatch)
|
||||
async function processAllOpenIssues(owner, repo) {
|
||||
const query = `
|
||||
query($owner:String!, $repo:String!, $cursor:String) {
|
||||
repository(owner:$owner, name:$repo) {
|
||||
issues(first: 100, states: OPEN, after: $cursor) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
nodes {
|
||||
number
|
||||
parent {
|
||||
url
|
||||
parent {
|
||||
url
|
||||
parent {
|
||||
url
|
||||
parent {
|
||||
url
|
||||
parent {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
let hasNextPage = true;
|
||||
let cursor = null;
|
||||
|
||||
while (hasNextPage) {
|
||||
try {
|
||||
const result = await github.graphql(query, { owner, repo, cursor });
|
||||
|
||||
if (!result || !result.repository || !result.repository.issues) {
|
||||
console.error('Invalid response structure from GitHub API');
|
||||
break;
|
||||
}
|
||||
|
||||
const issues = result.repository.issues.nodes || [];
|
||||
|
||||
console.log(`Processing batch of ${issues.length} issues...`);
|
||||
for (const issue of issues) {
|
||||
await checkAndLabel(issue, owner, repo);
|
||||
}
|
||||
|
||||
hasNextPage = result.repository.issues.pageInfo.hasNextPage;
|
||||
cursor = result.repository.issues.pageInfo.endCursor;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch issues batch:', error);
|
||||
throw error; // Re-throw to be caught by main execution
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function checkAndLabel(issue, owner, repo) {
|
||||
if (!issue || !issue.parent) return;
|
||||
|
||||
let currentParent = issue.parent;
|
||||
let tracedParents = [];
|
||||
let matched = false;
|
||||
|
||||
while (currentParent) {
|
||||
tracedParents.push(currentParent.url);
|
||||
|
||||
if (allowedParentUrls.includes(currentParent.url)) {
|
||||
console.log(`SUCCESS: Issue #${issue.number} is a descendant of ${currentParent.url}. Trace: ${tracedParents.join(' -> ')}. Adding label.`);
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issue.number,
|
||||
labels: [labelToAdd]
|
||||
});
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
currentParent = currentParent.parent;
|
||||
}
|
||||
|
||||
if (!matched && context.eventName === 'issues') {
|
||||
console.log(`Issue #${issue.number} did not match any allowed workstreams. Trace: ${tracedParents.join(' -> ') || 'None'}.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Main execution
|
||||
try {
|
||||
if (context.eventName === 'issues') {
|
||||
console.log(`Processing single issue #${context.payload.issue.number}...`);
|
||||
await processSingleIssue(context.repo.owner, context.repo.repo, context.payload.issue.number);
|
||||
} else {
|
||||
console.log(`Running for event: ${context.eventName}. Processing all open issues...`);
|
||||
await processAllOpenIssues(context.repo.owner, context.repo.repo);
|
||||
}
|
||||
} catch (error) {
|
||||
core.setFailed(`Workflow failed: ${error.message}`);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
name: 'Links'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['main']
|
||||
pull_request:
|
||||
branches: ['main']
|
||||
repository_dispatch:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '00 18 * * *'
|
||||
|
||||
jobs:
|
||||
linkChecker:
|
||||
if: |-
|
||||
${{ github.repository == 'google-gemini/gemini-cli' }}
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Link Checker'
|
||||
id: 'lychee'
|
||||
uses: 'lycheeverse/lychee-action@885c65f3dc543b57c898c8099f4e08c8afd178a2' # ratchet: lycheeverse/lychee-action@v2.6.1
|
||||
with:
|
||||
args: '--verbose --no-progress --accept 200,503 ./**/*.md'
|
||||
@@ -0,0 +1,35 @@
|
||||
name: 'Memory Tests: Nightly'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 * * *' # Runs at 2 AM every day
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
permissions:
|
||||
contents: 'read'
|
||||
|
||||
jobs:
|
||||
memory-test:
|
||||
name: 'Run Memory Usage Tests'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Set up Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Run Memory Tests'
|
||||
run: 'npm run test:memory'
|
||||
@@ -0,0 +1,35 @@
|
||||
name: 'Performance Tests: Nightly'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 3 * * *' # Runs at 3 AM every day
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
permissions:
|
||||
contents: 'read'
|
||||
|
||||
jobs:
|
||||
perf-test:
|
||||
name: 'Run Performance Usage Tests'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Set up Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Run Performance Tests'
|
||||
run: 'npm run test:perf'
|
||||
@@ -0,0 +1,29 @@
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||
|
||||
name: 'PR rate limiter'
|
||||
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- 'opened'
|
||||
- 'reopened'
|
||||
|
||||
jobs:
|
||||
limit:
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
permissions:
|
||||
contents: 'read'
|
||||
pull-requests: 'write'
|
||||
steps:
|
||||
- name: 'Limit open pull requests per user'
|
||||
uses: 'Homebrew/actions/limit-pull-requests@9ceb7934560eb61d131dde205a6c2d77b2e1529d' # master
|
||||
with:
|
||||
except-author-associations: 'MEMBER,OWNER,COLLABORATOR'
|
||||
comment-limit: 8
|
||||
comment: >
|
||||
You already have 7 pull requests open. Please work on getting
|
||||
existing PRs merged before opening more.
|
||||
close-limit: 8
|
||||
close: true
|
||||
@@ -0,0 +1,107 @@
|
||||
name: 'PR Size Labeler (Batch)'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
process_all:
|
||||
description: 'Process all PRs (open and closed) or open only'
|
||||
required: true
|
||||
default: 'false'
|
||||
type: 'choice'
|
||||
options:
|
||||
- 'true'
|
||||
- 'false'
|
||||
limit:
|
||||
description: 'Max number of PRs to fetch and check'
|
||||
required: true
|
||||
default: '100'
|
||||
type: 'string'
|
||||
|
||||
permissions:
|
||||
pull-requests: 'write'
|
||||
|
||||
jobs:
|
||||
batch-label:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- name: 'Batch label PRs'
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
GH_REPO: '${{ github.repository }}'
|
||||
run: |
|
||||
# Determine the state filter
|
||||
STATE="open"
|
||||
if [ "${{ github.event.inputs.process_all }}" = "true" ]; then
|
||||
STATE="all"
|
||||
fi
|
||||
|
||||
LIMIT="${{ github.event.inputs.limit }}"
|
||||
echo "Batch labeling up to $LIMIT $STATE PRs..."
|
||||
|
||||
# 1. Ensure standard premium size labels exist in the repository (self-healing)
|
||||
gh label create "size/XS" --color "7ee081" --description "XS: <10 lines changed" 2>/dev/null || true
|
||||
gh label create "size/S" --color "a6d49f" --description "S: 10-49 lines changed" 2>/dev/null || true
|
||||
gh label create "size/M" --color "f7d070" --description "M: 50-249 lines changed" 2>/dev/null || true
|
||||
gh label create "size/L" --color "f48c06" --description "L: 250-999 lines changed" 2>/dev/null || true
|
||||
gh label create "size/XL" --color "dc2f02" --description "XL: >=1000 lines changed" 2>/dev/null || true
|
||||
|
||||
# 2. Query PR list with all required fields in ONE call to prevent N+1 queries
|
||||
PR_LIST=$(gh pr list --state "$STATE" --limit "$LIMIT" --json number,additions,deletions,labels)
|
||||
if [ -z "$PR_LIST" ] || [ "$PR_LIST" = "[]" ]; then
|
||||
echo "ℹ️ No PRs found matching the criteria."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Parse and iterate over PRs
|
||||
UPDATED_COUNT=0
|
||||
SKIPPED_COUNT=0
|
||||
|
||||
echo "$PR_LIST" | jq -c '.[]' | while read -r PR_JSON; do
|
||||
PR_NUMBER=$(echo "$PR_JSON" | jq '.number')
|
||||
ADDITIONS=$(echo "$PR_JSON" | jq '.additions')
|
||||
DELETIONS=$(echo "$PR_JSON" | jq '.deletions')
|
||||
TOTAL=$((ADDITIONS + DELETIONS))
|
||||
|
||||
# Calculate target size
|
||||
if [ $TOTAL -lt 10 ]; then
|
||||
SIZE="size/XS"
|
||||
elif [ $TOTAL -lt 50 ]; then
|
||||
SIZE="size/S"
|
||||
elif [ $TOTAL -lt 250 ]; then
|
||||
SIZE="size/M"
|
||||
elif [ $TOTAL -lt 1000 ]; then
|
||||
SIZE="size/L"
|
||||
else
|
||||
SIZE="size/XL"
|
||||
fi
|
||||
|
||||
# Inspect existing labels to detect discrepancies
|
||||
EXISTING_LABELS=$(echo "$PR_JSON" | jq -r '.labels[].name' 2>/dev/null || echo "")
|
||||
|
||||
LABELS_TO_REMOVE=()
|
||||
for L in size/XS size/S size/M size/L size/XL; do
|
||||
if echo "$EXISTING_LABELS" | grep -Fq "$L" && [ "$L" != "$SIZE" ]; then
|
||||
LABELS_TO_REMOVE+=("--remove-label" "$L")
|
||||
fi
|
||||
done
|
||||
|
||||
LABEL_TO_ADD=()
|
||||
if ! echo "$EXISTING_LABELS" | grep -Fq "$SIZE"; then
|
||||
LABEL_TO_ADD+=("--add-label" "$SIZE")
|
||||
fi
|
||||
|
||||
# Update labels if there's a difference
|
||||
if [ ${#LABELS_TO_REMOVE[@]} -gt 0 ] || [ ${#LABEL_TO_ADD[@]} -gt 0 ]; then
|
||||
echo "🔄 PR #$PR_NUMBER (+$ADDITIONS/-$DELETIONS = $TOTAL lines): updating size to $SIZE"
|
||||
gh pr edit "$PR_NUMBER" "${LABELS_TO_REMOVE[@]}" "${LABEL_TO_ADD[@]}" 2>/dev/null || true
|
||||
UPDATED_COUNT=$((UPDATED_COUNT + 1))
|
||||
else
|
||||
echo "✅ PR #$PR_NUMBER (+$ADDITIONS/-$DELETIONS = $TOTAL lines): already has correct label ($SIZE). Skipping."
|
||||
SKIPPED_COUNT=$((SKIPPED_COUNT + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
echo "============================================"
|
||||
echo "🎉 Batch run completed!"
|
||||
echo "Skipped (already correct): $SKIPPED_COUNT"
|
||||
echo "Updated: $UPDATED_COUNT"
|
||||
@@ -0,0 +1,120 @@
|
||||
name: 'PR Size Labeler'
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: ['opened', 'synchronize', 'reopened']
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number to label manually (for workflow_dispatch)'
|
||||
required: false
|
||||
type: 'string'
|
||||
|
||||
permissions:
|
||||
pull-requests: 'write'
|
||||
issues: 'write'
|
||||
|
||||
jobs:
|
||||
size-label:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- name: 'Run size labeler'
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
GH_REPO: '${{ github.repository }}'
|
||||
run: |
|
||||
# Determine the target PR number
|
||||
if [ -n "${{ github.event.pull_request.number }}" ]; then
|
||||
PR_NUMBER="${{ github.event.pull_request.number }}"
|
||||
elif [ -n "${{ github.event.inputs.pr_number }}" ]; then
|
||||
PR_NUMBER="${{ github.event.inputs.pr_number }}"
|
||||
else
|
||||
echo "❌ Error: No PR number provided."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Checking PR #$PR_NUMBER..."
|
||||
|
||||
# 1. Ensure standard premium size labels exist in the repository (self-healing)
|
||||
# size/XS: Light green (#7ee081)
|
||||
# size/S: Yellow-green (#a6d49f)
|
||||
# size/M: Amber/Yellow (#f7d070)
|
||||
# size/L: Orange (#f48c06)
|
||||
# size/XL: Red (#dc2f02)
|
||||
gh label create "size/XS" --color "7ee081" --description "XS: <10 lines changed" 2>/dev/null || true
|
||||
gh label create "size/S" --color "a6d49f" --description "S: 10-49 lines changed" 2>/dev/null || true
|
||||
gh label create "size/M" --color "f7d070" --description "M: 50-249 lines changed" 2>/dev/null || true
|
||||
gh label create "size/L" --color "f48c06" --description "L: 250-999 lines changed" 2>/dev/null || true
|
||||
gh label create "size/XL" --color "dc2f02" --description "XL: >=1000 lines changed" 2>/dev/null || true
|
||||
|
||||
# 2. Fetch PR details in a single efficient API call
|
||||
PR_DATA=$(gh pr view "$PR_NUMBER" --json additions,deletions,changedFiles,labels)
|
||||
if [ -z "$PR_DATA" ]; then
|
||||
echo "❌ Error: Could not fetch PR details."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ADDITIONS=$(echo "$PR_DATA" | jq '.additions')
|
||||
DELETIONS=$(echo "$PR_DATA" | jq '.deletions')
|
||||
CHANGED_FILES=$(echo "$PR_DATA" | jq '.changedFiles')
|
||||
TOTAL=$((ADDITIONS + DELETIONS))
|
||||
|
||||
echo "PR additions: $ADDITIONS, deletions: $DELETIONS, total changes: $TOTAL, files: $CHANGED_FILES"
|
||||
|
||||
# 3. Calculate new size label
|
||||
if [ $TOTAL -lt 10 ]; then
|
||||
SIZE="size/XS"
|
||||
elif [ $TOTAL -lt 50 ]; then
|
||||
SIZE="size/S"
|
||||
elif [ $TOTAL -lt 250 ]; then
|
||||
SIZE="size/M"
|
||||
elif [ $TOTAL -lt 1000 ]; then
|
||||
SIZE="size/L"
|
||||
else
|
||||
SIZE="size/XL"
|
||||
fi
|
||||
|
||||
# 4. Check existing labels and update only if necessary
|
||||
EXISTING_LABELS=$(echo "$PR_DATA" | jq -r '.labels[].name' 2>/dev/null || echo "")
|
||||
|
||||
LABELS_TO_REMOVE=()
|
||||
for L in size/XS size/S size/M size/L size/XL; do
|
||||
if echo "$EXISTING_LABELS" | grep -Fq "$L" && [ "$L" != "$SIZE" ]; then
|
||||
LABELS_TO_REMOVE+=("--remove-label" "$L")
|
||||
fi
|
||||
done
|
||||
|
||||
LABEL_TO_ADD=()
|
||||
if ! echo "$EXISTING_LABELS" | grep -Fq "$SIZE"; then
|
||||
LABEL_TO_ADD+=("--add-label" "$SIZE")
|
||||
fi
|
||||
|
||||
# Perform a single, highly atomic edit call if changes are needed
|
||||
if [ ${#LABELS_TO_REMOVE[@]} -gt 0 ] || [ ${#LABEL_TO_ADD[@]} -gt 0 ]; then
|
||||
echo "Updating labels: removing ${LABELS_TO_REMOVE[*]}, adding $SIZE"
|
||||
gh pr edit "$PR_NUMBER" "${LABELS_TO_REMOVE[@]}" "${LABEL_TO_ADD[@]}"
|
||||
else
|
||||
echo "✅ PR #$PR_NUMBER already has the correct size label ($SIZE)."
|
||||
fi
|
||||
|
||||
# 5. Premium, anti-spam comment logic (updates previous comment to keep thread clean)
|
||||
COMMENT="📊 PR Size: **$SIZE**
|
||||
- Lines changed: **$TOTAL**
|
||||
- Additions: +$ADDITIONS
|
||||
- Deletions: -$DELETIONS
|
||||
- Files changed: $CHANGED_FILES"
|
||||
|
||||
# Find any existing size labeler comment by the github-actions bot
|
||||
echo "Searching for existing size comment..."
|
||||
COMMENT_ID=$(gh api "repos/${{ github.repository }}/issues/$PR_NUMBER/comments" \
|
||||
--jq '.[] | select(.user.login == "github-actions[bot]" and (.body | startswith("📊 PR Size:"))) | .id' | head -n 1)
|
||||
|
||||
if [ -n "$COMMENT_ID" ]; then
|
||||
echo "Updating existing comment (ID: $COMMENT_ID)..."
|
||||
gh api "repos/${{ github.repository }}/issues/comments/$COMMENT_ID" -X PATCH -f body="$COMMENT" > /dev/null
|
||||
else
|
||||
echo "Creating new comment..."
|
||||
gh pr comment "$PR_NUMBER" --body "$COMMENT" > /dev/null
|
||||
fi
|
||||
|
||||
echo "🎉 PR size labeling completed successfully."
|
||||
@@ -0,0 +1,67 @@
|
||||
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:
|
||||
- 'dev'
|
||||
- 'latest'
|
||||
- 'preview'
|
||||
- 'nightly'
|
||||
dry-run:
|
||||
description: 'Whether to run in dry-run mode.'
|
||||
required: false
|
||||
type: 'boolean'
|
||||
default: true
|
||||
environment:
|
||||
description: 'Environment'
|
||||
required: false
|
||||
type: 'choice'
|
||||
options:
|
||||
- 'prod'
|
||||
- 'dev'
|
||||
default: 'prod'
|
||||
|
||||
jobs:
|
||||
change-tags:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
permissions:
|
||||
packages: 'write'
|
||||
issues: 'write'
|
||||
steps:
|
||||
- name: 'Checkout repository'
|
||||
uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4
|
||||
with:
|
||||
ref: '${{ github.ref }}'
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- 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 }}'
|
||||
wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}'
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
cli-package-name: '${{ vars.CLI_PACKAGE_NAME }}'
|
||||
core-package-name: '${{ vars.CORE_PACKAGE_NAME }}'
|
||||
a2a-package-name: '${{ vars.A2A_PACKAGE_NAME }}'
|
||||
working-directory: '.'
|
||||
@@ -0,0 +1,151 @@
|
||||
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:
|
||||
- 'dev'
|
||||
- 'preview'
|
||||
- 'nightly'
|
||||
- 'latest'
|
||||
default: 'latest'
|
||||
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 (only used when environment is PROD)'
|
||||
required: false
|
||||
type: 'boolean'
|
||||
default: false
|
||||
environment:
|
||||
description: 'Environment'
|
||||
required: false
|
||||
type: 'choice'
|
||||
options:
|
||||
- 'prod'
|
||||
- 'dev'
|
||||
default: 'prod'
|
||||
|
||||
jobs:
|
||||
build-mac:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
uses: './.github/workflows/build-unsigned-mac-binaries.yml'
|
||||
with:
|
||||
ref: '${{ github.event.inputs.ref }}'
|
||||
|
||||
release:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
needs: ['build-mac']
|
||||
runs-on: 'ubuntu-latest'
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
permissions:
|
||||
contents: 'write'
|
||||
packages: 'write'
|
||||
issues: 'write'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Checkout Release Code'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: '${{ github.event.inputs.ref }}'
|
||||
path: 'release'
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Debug Inputs'
|
||||
shell: 'bash'
|
||||
env:
|
||||
JSON_INPUTS: '${{ toJSON(inputs) }}'
|
||||
run: 'echo "$JSON_INPUTS"'
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
|
||||
with:
|
||||
node-version-file: './release/.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install Dependencies'
|
||||
working-directory: './release'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Download macOS Binaries'
|
||||
uses: './.github/actions/download-mac-binaries'
|
||||
with:
|
||||
path: 'release/dist'
|
||||
|
||||
- name: 'Prepare Release Info'
|
||||
id: 'release_info'
|
||||
working-directory: './release'
|
||||
env:
|
||||
INPUT_VERSION: '${{ github.event.inputs.version }}'
|
||||
run: |
|
||||
RELEASE_VERSION="${INPUT_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 }}'
|
||||
working-directory: './release'
|
||||
|
||||
- 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 }}'
|
||||
wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}'
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
github-release-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
|
||||
dry-run: '${{ github.event.inputs.dry_run }}'
|
||||
previous-tag: '${{ steps.release_info.outputs.PREVIOUS_TAG }}'
|
||||
skip-github-release: '${{ github.event.inputs.skip_github_release }}'
|
||||
working-directory: './release'
|
||||
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
|
||||
npm-registry-publish-url: '${{ vars.NPM_REGISTRY_PUBLISH_URL }}'
|
||||
npm-registry-url: '${{ vars.NPM_REGISTRY_URL }}'
|
||||
npm-registry-scope: '${{ vars.NPM_REGISTRY_SCOPE }}'
|
||||
cli-package-name: '${{ vars.CLI_PACKAGE_NAME }}'
|
||||
core-package-name: '${{ vars.CORE_PACKAGE_NAME }}'
|
||||
a2a-package-name: '${{ vars.A2A_PACKAGE_NAME }}'
|
||||
|
||||
- name: 'Create Issue on Failure'
|
||||
if: '${{ failure() && github.event.inputs.dry_run == false }}'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
RELEASE_TAG: '${{ github.event.inputs.version }}'
|
||||
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
run: |
|
||||
gh issue create \
|
||||
--title 'Manual Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')' \
|
||||
--body 'The manual release workflow failed. See the full run for details: ${DETAILS_URL}' \
|
||||
--label 'release-failure,priority/p0'
|
||||
@@ -0,0 +1,176 @@
|
||||
name: 'Release: Nightly'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
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: true
|
||||
ref:
|
||||
description: 'The branch, tag, or SHA to release from.'
|
||||
required: false
|
||||
type: 'string'
|
||||
default: 'main'
|
||||
environment:
|
||||
description: 'Environment'
|
||||
required: false
|
||||
type: 'choice'
|
||||
options:
|
||||
- 'prod'
|
||||
- 'dev'
|
||||
default: 'prod'
|
||||
|
||||
jobs:
|
||||
build-mac:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
uses: './.github/workflows/build-unsigned-mac-binaries.yml'
|
||||
with:
|
||||
ref: '${{ github.event.inputs.ref }}'
|
||||
|
||||
release:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
needs: ['build-mac']
|
||||
environment: "${{ github.event_name == 'schedule' && 'internal' || github.event.inputs.environment || 'prod' }}"
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
contents: 'write'
|
||||
packages: 'write'
|
||||
issues: 'write'
|
||||
pull-requests: 'write'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Checkout Release Code'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: '${{ github.event.inputs.ref }}'
|
||||
path: 'release'
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: './release/.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install Dependencies'
|
||||
working-directory: './release'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Download macOS Binaries'
|
||||
uses: './.github/actions/download-mac-binaries'
|
||||
with:
|
||||
path: 'release/dist'
|
||||
|
||||
- name: 'Print Inputs'
|
||||
shell: 'bash'
|
||||
env:
|
||||
JSON_INPUTS: '${{ toJSON(github.event.inputs) }}'
|
||||
run: 'echo "$JSON_INPUTS"'
|
||||
|
||||
- name: 'Calculate Release Variables'
|
||||
id: 'vars'
|
||||
uses: './.github/actions/calculate-vars'
|
||||
with:
|
||||
dry_run: '${{ github.event.inputs.dry_run }}'
|
||||
|
||||
- name: 'Print Calculated vars'
|
||||
shell: 'bash'
|
||||
env:
|
||||
JSON_VARS: '${{ toJSON(steps.vars.outputs) }}'
|
||||
run: 'echo "$JSON_VARS"'
|
||||
|
||||
- name: 'Run Tests'
|
||||
if: "${{ github.event_name == 'schedule' || github.event.inputs.force_skip_tests == 'false' }}"
|
||||
uses: './.github/actions/run-tests'
|
||||
with:
|
||||
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
|
||||
working-directory: './release'
|
||||
|
||||
- name: 'Get Nightly Version'
|
||||
id: 'nightly_version'
|
||||
working-directory: './release'
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
run: |
|
||||
# Calculate the version using the centralized script
|
||||
VERSION_JSON=$(node scripts/get-release-version.js --type=nightly)
|
||||
|
||||
# Extract values for logging and outputs
|
||||
RELEASE_TAG=$(echo "${VERSION_JSON}" | jq -r .releaseTag)
|
||||
RELEASE_VERSION=$(echo "${VERSION_JSON}" | jq -r .releaseVersion)
|
||||
NPM_TAG=$(echo "${VERSION_JSON}" | jq -r .npmTag)
|
||||
PREVIOUS_TAG=$(echo "${VERSION_JSON}" | jq -r .previousReleaseTag)
|
||||
|
||||
# Print calculated values for logging
|
||||
echo "Calculated Release Tag: ${RELEASE_TAG}"
|
||||
echo "Calculated Release Version: ${RELEASE_VERSION}"
|
||||
echo "Calculated Previous Tag: ${PREVIOUS_TAG}"
|
||||
|
||||
# Set outputs for subsequent steps
|
||||
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: 'Publish Release'
|
||||
if: true
|
||||
uses: './.github/actions/publish-release'
|
||||
with:
|
||||
release-version: '${{ steps.nightly_version.outputs.RELEASE_VERSION }}'
|
||||
release-tag: '${{ steps.nightly_version.outputs.RELEASE_TAG }}'
|
||||
npm-tag: '${{ steps.nightly_version.outputs.NPM_TAG }}'
|
||||
wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
|
||||
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
|
||||
wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}'
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
github-release-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
|
||||
dry-run: '${{ steps.vars.outputs.is_dry_run }}'
|
||||
previous-tag: '${{ steps.nightly_version.outputs.PREVIOUS_TAG }}'
|
||||
working-directory: './release'
|
||||
skip-branch-cleanup: true
|
||||
force-skip-tests: "${{ github.event_name != 'schedule' && github.event.inputs.force_skip_tests == 'true' }}"
|
||||
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
|
||||
npm-registry-publish-url: "${{ vars.NPM_REGISTRY_PUBLISH_URL || 'https://wombat-dressing-room.appspot.com' }}"
|
||||
npm-registry-url: "${{ vars.NPM_REGISTRY_URL || 'https://wombat-dressing-room.appspot.com' }}"
|
||||
npm-registry-scope: "${{ vars.NPM_REGISTRY_SCOPE || '@google' }}"
|
||||
cli-package-name: "${{ vars.CLI_PACKAGE_NAME || '@google/gemini-cli' }}"
|
||||
core-package-name: "${{ vars.CORE_PACKAGE_NAME || '@google/gemini-cli-core' }}"
|
||||
a2a-package-name: "${{ vars.A2A_PACKAGE_NAME || '@google/gemini-cli-a2a-server' }}"
|
||||
|
||||
- name: 'Create and Merge Pull Request'
|
||||
if: "github.event.inputs.environment != 'dev'"
|
||||
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.'
|
||||
github-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
|
||||
dry-run: '${{ steps.vars.outputs.is_dry_run }}'
|
||||
working-directory: './release'
|
||||
|
||||
- name: 'Create Issue on Failure'
|
||||
if: "${{ failure() && github.event.inputs.environment != 'dev' && (github.event_name == 'schedule' || github.event.inputs.dry_run != 'true') }}"
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
RELEASE_TAG: '${{ steps.nightly_version.outputs.RELEASE_TAG }}'
|
||||
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
run: |
|
||||
gh issue create \
|
||||
--title "Nightly Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')" \
|
||||
--body "The nightly-release workflow failed. See the full run for details: ${DETAILS_URL}" \
|
||||
--label 'release-failure,priority/p0'
|
||||
@@ -0,0 +1,107 @@
|
||||
# This workflow is triggered on every new release.
|
||||
# It uses Gemini to generate release notes and creates a PR with the changes.
|
||||
name: 'Generate Release Notes'
|
||||
|
||||
on:
|
||||
release:
|
||||
types: ['published']
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'New version (e.g., v1.2.3)'
|
||||
required: true
|
||||
type: 'string'
|
||||
body:
|
||||
description: 'Release notes body'
|
||||
required: true
|
||||
type: 'string'
|
||||
time:
|
||||
description: 'Release time'
|
||||
required: true
|
||||
type: 'string'
|
||||
|
||||
jobs:
|
||||
generate-release-notes:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
contents: 'write'
|
||||
pull-requests: 'write'
|
||||
steps:
|
||||
- name: 'Checkout repository'
|
||||
uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
# The user-level skills need to be available to the workflow
|
||||
fetch-depth: 0
|
||||
ref: 'main'
|
||||
|
||||
- name: 'Set up Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: 'Get release information'
|
||||
id: 'release_info'
|
||||
run: |
|
||||
VERSION="${{ github.event.inputs.version || github.event.release.tag_name }}"
|
||||
TIME="${{ github.event.inputs.time || github.event.release.created_at }}"
|
||||
|
||||
echo "VERSION=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "TIME=${TIME}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Use a heredoc to preserve multiline release body
|
||||
echo 'RAW_CHANGELOG<<EOF' >> "$GITHUB_OUTPUT"
|
||||
printf "%s\n" "$BODY" >> "$GITHUB_OUTPUT"
|
||||
echo 'EOF' >> "$GITHUB_OUTPUT"
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
|
||||
BODY: '${{ github.event.inputs.body || github.event.release.body }}'
|
||||
|
||||
- name: 'Validate version'
|
||||
id: 'validate_version'
|
||||
run: |
|
||||
if echo "${{ steps.release_info.outputs.VERSION }}" | grep -q "nightly"; then
|
||||
echo "Nightly release detected. Stopping workflow."
|
||||
echo "CONTINUE=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "CONTINUE=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: 'Generate Changelog with Gemini'
|
||||
if: "steps.validate_version.outputs.CONTINUE == 'true'"
|
||||
uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0
|
||||
env:
|
||||
GEMINI_CLI_TRUST_WORKSPACE: true
|
||||
with:
|
||||
upload_artifacts: 'true'
|
||||
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
|
||||
prompt: |
|
||||
Activate the 'docs-changelog' skill.
|
||||
|
||||
**Release Information:**
|
||||
- New Version: ${{ steps.release_info.outputs.VERSION }}
|
||||
- Release Date: ${{ steps.release_info.outputs.TIME }}
|
||||
- Raw Changelog Data: ${{ steps.release_info.outputs.RAW_CHANGELOG }}
|
||||
|
||||
Execute the release notes generation process using the information provided.
|
||||
|
||||
When you are done, please output your thought process and the steps you took for future debugging purposes.
|
||||
|
||||
- name: 'Create Pull Request'
|
||||
if: "steps.validate_version.outputs.CONTINUE == 'true'"
|
||||
uses: 'peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c' # ratchet:peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
|
||||
commit-message: 'docs(changelog): update for ${{ steps.release_info.outputs.VERSION }}'
|
||||
title: 'Changelog for ${{ steps.release_info.outputs.VERSION }}'
|
||||
body: |
|
||||
This PR contains the auto-generated changelog for the ${{ steps.release_info.outputs.VERSION }} release.
|
||||
|
||||
Please review and merge.
|
||||
|
||||
Related to #18505
|
||||
branch: 'changelog-${{ steps.release_info.outputs.VERSION }}'
|
||||
base: 'main'
|
||||
team-reviewers: 'gemini-cli-docs, gemini-cli-maintainers'
|
||||
delete-branch: true
|
||||
@@ -0,0 +1,197 @@
|
||||
name: 'Release: Patch (0) 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:
|
||||
persist-credentials: false
|
||||
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 }}',
|
||||
environment: 'prod'
|
||||
}
|
||||
});
|
||||
|
||||
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(','));
|
||||
|
||||
const markdownLinks = recentRuns.map(r => `- [View dispatched workflow run](${r.html_url})`).join('\n');
|
||||
core.setOutput('dispatched_run_links', markdownLinks);
|
||||
}
|
||||
|
||||
- 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: |
|
||||
🚀 **[Step 1/4] Patch workflow(s) waiting for approval!**
|
||||
|
||||
**📋 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 }}
|
||||
|
||||
**⏳ Status:** The patch creation workflow has been triggered and is waiting for deployment approval. Please visit the specific workflow links below and approve the runs.
|
||||
|
||||
**🔗 Track Progress:**
|
||||
${{ steps.dispatch_patch.outputs.dispatched_run_links }}
|
||||
- [View patch workflow history](https://github.com/${{ github.repository }}/actions/workflows/release-patch-1-create-pr.yml)
|
||||
- [This trigger 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: |
|
||||
🚀 **[Step 1/4] Patch workflow(s) waiting for approval!**
|
||||
|
||||
**📋 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 }}
|
||||
|
||||
**⏳ Status:** The patch creation workflow has been triggered and is waiting for deployment approval. Please visit the workflow history link below and approve the runs.
|
||||
|
||||
**🔗 Track Progress:**
|
||||
- [View patch workflow history](https://github.com/${{ github.repository }}/actions/workflows/release-patch-1-create-pr.yml)
|
||||
- [This trigger 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: |
|
||||
❌ **[Step 1/4] 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)
|
||||
@@ -0,0 +1,135 @@
|
||||
name: 'Release: Patch (1) Create PR'
|
||||
|
||||
run-name: >-
|
||||
Release Patch (1) Create PR | S:${{ inputs.channel }} | C:${{ inputs.commit }} ${{ inputs.original_pr && format('| PR:#{0}', inputs.original_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'
|
||||
environment:
|
||||
description: 'Environment'
|
||||
required: false
|
||||
type: 'choice'
|
||||
options:
|
||||
- 'prod'
|
||||
- 'dev'
|
||||
default: 'prod'
|
||||
|
||||
jobs:
|
||||
create-patch:
|
||||
runs-on: 'ubuntu-latest'
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
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
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'configure .npmrc'
|
||||
uses: './.github/actions/setup-npmrc'
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
|
||||
- name: 'Install Script Dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Configure Git User'
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
REPOSITORY: '${{ github.repository }}'
|
||||
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:${GH_TOKEN}@github.com/${REPOSITORY}.git"
|
||||
|
||||
- name: 'Create Patch'
|
||||
id: 'create_patch'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
GH_TOKEN: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
|
||||
CLI_PACKAGE_NAME: '${{ vars.CLI_PACKAGE_NAME }}'
|
||||
PATCH_COMMIT: '${{ github.event.inputs.commit }}'
|
||||
PATCH_CHANNEL: '${{ github.event.inputs.channel }}'
|
||||
ORIGINAL_PR: '${{ github.event.inputs.original_pr }}'
|
||||
DRY_RUN: '${{ github.event.inputs.dry_run }}'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
# Capture output and display it in logs using tee
|
||||
{
|
||||
node scripts/releasing/create-patch-pr.js \
|
||||
--cli-package-name="${CLI_PACKAGE_NAME}" \
|
||||
--commit="${PATCH_COMMIT}" \
|
||||
--channel="${PATCH_CHANNEL}" \
|
||||
--pullRequestNumber="${ORIGINAL_PR}" \
|
||||
--dry-run="${DRY_RUN}"
|
||||
} 2>&1 | tee >(
|
||||
echo "LOG_CONTENT<<EOF" >> "$GITHUB_ENV"
|
||||
cat >> "$GITHUB_ENV"
|
||||
echo "EOF" >> "$GITHUB_ENV"
|
||||
)
|
||||
echo "EXIT_CODE=${PIPESTATUS[0]}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- 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 }}'
|
||||
TARGET_REF: '${{ github.event.inputs.ref }}'
|
||||
ENVIRONMENT: '${{ github.event.inputs.environment }}'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
git checkout "${TARGET_REF}"
|
||||
node scripts/releasing/patch-create-comment.js
|
||||
|
||||
- name: 'Fail Workflow if Main Task Failed'
|
||||
if: 'always() && steps.create_patch.outputs.EXIT_CODE != 0'
|
||||
env:
|
||||
EXIT_CODE: '${{ steps.create_patch.outputs.EXIT_CODE }}'
|
||||
run: |
|
||||
echo "Patch creation failed with exit code: ${EXIT_CODE}"
|
||||
echo "Check the logs above and the comment posted to the original PR for details."
|
||||
exit 1
|
||||
@@ -0,0 +1,95 @@
|
||||
name: 'Release: Patch (2) Trigger'
|
||||
|
||||
run-name: >-
|
||||
Release Patch (2) Trigger |
|
||||
${{ github.event.pull_request.number && format('PR #{0}', github.event.pull_request.number) || 'Manual' }} |
|
||||
${{ github.event.pull_request.head.ref || github.event.inputs.ref }}
|
||||
|
||||
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 release-patch-3-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
|
||||
test_mode:
|
||||
description: 'Whether or not to run in test mode'
|
||||
required: false
|
||||
type: 'boolean'
|
||||
default: false
|
||||
environment:
|
||||
description: 'Environment'
|
||||
required: false
|
||||
type: 'choice'
|
||||
options:
|
||||
- 'prod'
|
||||
- 'dev'
|
||||
default: 'prod'
|
||||
|
||||
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'
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
permissions:
|
||||
actions: 'write'
|
||||
contents: 'write'
|
||||
pull-requests: 'write'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
persist-credentials: false
|
||||
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 }}'
|
||||
TEST_MODE: '${{ github.event.inputs.test_mode }}'
|
||||
ENVIRONMENT: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
DRY_RUN: '${{ github.event.inputs.dry_run }}'
|
||||
run: |
|
||||
node scripts/releasing/patch-trigger.js --dry-run="${DRY_RUN}"
|
||||
@@ -0,0 +1,260 @@
|
||||
name: 'Release: Patch (3) Release'
|
||||
|
||||
run-name: >-
|
||||
Release Patch (3) Release | T:${{ inputs.type }} | R:${{ inputs.release_ref }} ${{ inputs.original_pr && format('| PR:#{0}', inputs.original_pr) || '' }}
|
||||
|
||||
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'
|
||||
environment:
|
||||
description: 'Environment'
|
||||
required: false
|
||||
type: 'choice'
|
||||
options:
|
||||
- 'prod'
|
||||
- 'dev'
|
||||
default: 'prod'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: 'ubuntu-latest'
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
permissions:
|
||||
contents: 'write'
|
||||
packages: 'write'
|
||||
pull-requests: 'write'
|
||||
issues: 'write'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: 'Checkout Release Code'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
persist-credentials: false
|
||||
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: 'configure .npmrc'
|
||||
uses: './.github/actions/setup-npmrc'
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
|
||||
- name: 'Install Script Dependencies'
|
||||
run: |-
|
||||
npm ci
|
||||
|
||||
- name: 'Install Dependencies'
|
||||
working-directory: './release'
|
||||
run: |-
|
||||
npm ci
|
||||
|
||||
- name: 'Print Inputs'
|
||||
shell: 'bash'
|
||||
env:
|
||||
JSON_INPUTS: '${{ toJSON(inputs) }}'
|
||||
run: 'echo "$JSON_INPUTS"'
|
||||
|
||||
- name: 'Get Patch Version'
|
||||
id: 'patch_version'
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
PATCH_FROM: '${{ github.event.inputs.type }}'
|
||||
CLI_PACKAGE_NAME: '${{vars.CLI_PACKAGE_NAME}}'
|
||||
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 --cli-package-name="${CLI_PACKAGE_NAME}" --patch-from="${PATCH_FROM}")
|
||||
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 }}'
|
||||
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 }}'
|
||||
VARS_CLI_PACKAGE_NAME: '${{ vars.CLI_PACKAGE_NAME }}'
|
||||
run: |
|
||||
echo "🔍 Verifying no concurrent patch releases have occurred..."
|
||||
|
||||
# Store original calculation for comparison
|
||||
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 --cli-package-name="${VARS_CLI_PACKAGE_NAME}" --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}"
|
||||
env:
|
||||
STEPS_PATCH_VERSION_OUTPUTS_RELEASE_VERSION: '${{ steps.patch_version.outputs.RELEASE_VERSION }}'
|
||||
STEPS_PATCH_VERSION_OUTPUTS_RELEASE_TAG: '${{ steps.patch_version.outputs.RELEASE_TAG }}'
|
||||
STEPS_PATCH_VERSION_OUTPUTS_NPM_TAG: '${{ steps.patch_version.outputs.NPM_TAG }}'
|
||||
STEPS_PATCH_VERSION_OUTPUTS_PREVIOUS_TAG: '${{ steps.patch_version.outputs.PREVIOUS_TAG }}'
|
||||
|
||||
- name: 'Run Tests'
|
||||
if: "${{github.event.inputs.force_skip_tests != 'true'}}"
|
||||
uses: './.github/actions/run-tests'
|
||||
with:
|
||||
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 }}'
|
||||
wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}'
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
github-release-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
|
||||
dry-run: '${{ github.event.inputs.dry_run }}'
|
||||
previous-tag: '${{ steps.patch_version.outputs.PREVIOUS_TAG }}'
|
||||
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
|
||||
npm-registry-publish-url: '${{ vars.NPM_REGISTRY_PUBLISH_URL }}'
|
||||
npm-registry-url: '${{ vars.NPM_REGISTRY_URL }}'
|
||||
npm-registry-scope: '${{ vars.NPM_REGISTRY_SCOPE }}'
|
||||
cli-package-name: '${{ vars.CLI_PACKAGE_NAME }}'
|
||||
core-package-name: '${{ vars.CORE_PACKAGE_NAME }}'
|
||||
a2a-package-name: '${{ vars.A2A_PACKAGE_NAME }}'
|
||||
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 '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,441 @@
|
||||
name: 'Release: Promote'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
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
|
||||
ref:
|
||||
description: 'The branch, tag, or SHA to release from.'
|
||||
required: false
|
||||
type: 'string'
|
||||
default: 'main'
|
||||
stable_version_override:
|
||||
description: 'Manually override the stable version number.'
|
||||
required: false
|
||||
type: 'string'
|
||||
preview_version_override:
|
||||
description: 'Manually override the preview version number.'
|
||||
required: false
|
||||
type: 'string'
|
||||
environment:
|
||||
description: 'Environment'
|
||||
required: false
|
||||
type: 'choice'
|
||||
options:
|
||||
- 'prod'
|
||||
- 'dev'
|
||||
default: 'prod'
|
||||
|
||||
jobs:
|
||||
calculate-versions:
|
||||
name: 'Calculate Versions and Plan'
|
||||
runs-on: 'ubuntu-latest'
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
|
||||
outputs:
|
||||
STABLE_VERSION: '${{ steps.versions.outputs.STABLE_VERSION }}'
|
||||
STABLE_SHA: '${{ steps.versions.outputs.STABLE_SHA }}'
|
||||
PREVIOUS_STABLE_TAG: '${{ steps.versions.outputs.PREVIOUS_STABLE_TAG }}'
|
||||
PREVIEW_VERSION: '${{ steps.versions.outputs.PREVIEW_VERSION }}'
|
||||
PREVIEW_SHA: '${{ steps.versions.outputs.PREVIEW_SHA }}'
|
||||
PREVIOUS_PREVIEW_TAG: '${{ steps.versions.outputs.PREVIOUS_PREVIEW_TAG }}'
|
||||
NEXT_NIGHTLY_VERSION: '${{ steps.versions.outputs.NEXT_NIGHTLY_VERSION }}'
|
||||
PREVIOUS_NIGHTLY_TAG: '${{ steps.versions.outputs.PREVIOUS_NIGHTLY_TAG }}'
|
||||
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install Dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Print Inputs'
|
||||
shell: 'bash'
|
||||
env:
|
||||
JSON_INPUTS: '${{ toJSON(inputs) }}'
|
||||
run: 'echo "$JSON_INPUTS"'
|
||||
|
||||
- name: 'Calculate Versions and SHAs'
|
||||
id: 'versions'
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
STABLE_OVERRIDE: '${{ github.event.inputs.stable_version_override }}'
|
||||
PREVIEW_OVERRIDE: '${{ github.event.inputs.preview_version_override }}'
|
||||
REF_INPUT: '${{ github.event.inputs.ref }}'
|
||||
run: |
|
||||
set -e
|
||||
STABLE_COMMAND="node scripts/get-release-version.js --type=stable"
|
||||
if [[ -n "${STABLE_OVERRIDE}" ]]; then
|
||||
STABLE_COMMAND+=" --stable_version_override=${STABLE_OVERRIDE}"
|
||||
fi
|
||||
PREVIEW_COMMAND="node scripts/get-release-version.js --type=preview"
|
||||
if [[ -n "${PREVIEW_OVERRIDE}" ]]; then
|
||||
PREVIEW_COMMAND+=" --preview_version_override=${PREVIEW_OVERRIDE}"
|
||||
fi
|
||||
NIGHTLY_COMMAND="node scripts/get-release-version.js --type=promote-nightly"
|
||||
STABLE_JSON=$(${STABLE_COMMAND})
|
||||
STABLE_VERSION=$(echo "${STABLE_JSON}" | jq -r .releaseVersion)
|
||||
PREVIEW_COMMAND+=" --stable-base-version=${STABLE_VERSION}"
|
||||
NIGHTLY_COMMAND+=" --stable-base-version=${STABLE_VERSION}"
|
||||
PREVIEW_JSON=$(${PREVIEW_COMMAND})
|
||||
NIGHTLY_JSON=$(${NIGHTLY_COMMAND})
|
||||
echo "STABLE_JSON_COMMAND=${STABLE_COMMAND}"
|
||||
echo "PREVIEW_JSON_COMMAND=${PREVIEW_COMMAND}"
|
||||
echo "NIGHTLY_JSON_COMMAND=${NIGHTLY_COMMAND}"
|
||||
echo "STABLE_JSON: ${STABLE_JSON}"
|
||||
echo "PREVIEW_JSON: ${PREVIEW_JSON}"
|
||||
echo "NIGHTLY_JSON: ${NIGHTLY_JSON}"
|
||||
echo "STABLE_VERSION=${STABLE_VERSION}" >> "${GITHUB_OUTPUT}"
|
||||
# shellcheck disable=SC1083
|
||||
PREVIOUS_PREVIEW_TAG=$(echo "${PREVIEW_JSON}" | jq -r .previousReleaseTag)
|
||||
STABLE_SHA=$(git rev-parse "${PREVIOUS_PREVIEW_TAG}^{commit}")
|
||||
echo "STABLE_SHA=${STABLE_SHA}" >> "${GITHUB_OUTPUT}"
|
||||
echo "PREVIOUS_STABLE_TAG=$(echo "${STABLE_JSON}" | jq -r .previousReleaseTag)" >> "${GITHUB_OUTPUT}"
|
||||
echo "PREVIEW_VERSION=$(echo "${PREVIEW_JSON}" | jq -r .releaseVersion)" >> "${GITHUB_OUTPUT}"
|
||||
# shellcheck disable=SC1083
|
||||
REF="${REF_INPUT}"
|
||||
SHA=$(git ls-remote origin "$REF" | awk -v ref="$REF" '$2 == "refs/heads/"ref || $2 == "refs/tags/"ref || $2 == ref {print $1}' | head -n 1)
|
||||
if [ -z "$SHA" ]; then
|
||||
if [[ "$REF" =~ ^[0-9a-f]{7,40}$ ]]; then
|
||||
SHA="$REF"
|
||||
else
|
||||
echo "::error::Could not resolve ref '$REF' to a commit SHA."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo "PREVIEW_SHA=$SHA" >> "${GITHUB_OUTPUT}"
|
||||
echo "PREVIOUS_PREVIEW_TAG=$(echo "${PREVIEW_JSON}" | jq -r .previousReleaseTag)" >> "${GITHUB_OUTPUT}"
|
||||
echo "NEXT_NIGHTLY_VERSION=$(echo "${NIGHTLY_JSON}" | jq -r .releaseVersion)" >> "${GITHUB_OUTPUT}"
|
||||
echo "PREVIOUS_NIGHTLY_TAG=$(echo "${NIGHTLY_JSON}" | jq -r .previousReleaseTag)" >> "${GITHUB_OUTPUT}"
|
||||
CURRENT_NIGHTLY_TAG=$(git describe --tags --abbrev=0 --match="*nightly*")
|
||||
echo "CURRENT_NIGHTLY_TAG=${CURRENT_NIGHTLY_TAG}" >> "${GITHUB_OUTPUT}"
|
||||
echo "NEXT_SHA=$SHA" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: 'Display Pending Updates'
|
||||
env:
|
||||
STABLE_VERSION: '${{ steps.versions.outputs.STABLE_VERSION }}'
|
||||
STABLE_SHA: '${{ steps.versions.outputs.STABLE_SHA }}'
|
||||
PREVIOUS_STABLE_TAG: '${{ steps.versions.outputs.PREVIOUS_STABLE_TAG }}'
|
||||
PREVIEW_VERSION: '${{ steps.versions.outputs.PREVIEW_VERSION }}'
|
||||
PREVIEW_SHA: '${{ steps.versions.outputs.PREVIEW_SHA }}'
|
||||
PREVIOUS_PREVIEW_TAG: '${{ steps.versions.outputs.PREVIOUS_PREVIEW_TAG }}'
|
||||
NEXT_NIGHTLY_VERSION: '${{ steps.versions.outputs.NEXT_NIGHTLY_VERSION }}'
|
||||
PREVIOUS_NIGHTLY_TAG: '${{ steps.versions.outputs.PREVIOUS_NIGHTLY_TAG }}'
|
||||
INPUT_REF: '${{ github.event.inputs.ref }}'
|
||||
run: |
|
||||
echo "Release Plan:"
|
||||
echo "-----------"
|
||||
echo "Stable Release: ${STABLE_VERSION}"
|
||||
echo " - Commit: ${STABLE_SHA}"
|
||||
echo " - Previous Tag: ${PREVIOUS_STABLE_TAG}"
|
||||
echo ""
|
||||
echo "Preview Release: ${PREVIEW_VERSION}"
|
||||
echo " - Commit: ${PREVIEW_SHA} (${INPUT_REF})"
|
||||
echo " - Previous Tag: ${PREVIOUS_PREVIEW_TAG}"
|
||||
echo ""
|
||||
echo "Preparing Next Nightly Release: ${NEXT_NIGHTLY_VERSION}"
|
||||
echo " - Merging Version Update PR to Branch: ${INPUT_REF}"
|
||||
echo " - Previous Tag: ${PREVIOUS_NIGHTLY_TAG}"
|
||||
|
||||
test:
|
||||
name: 'Test ${{ matrix.channel }}'
|
||||
needs: 'calculate-versions'
|
||||
runs-on: 'ubuntu-latest'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- channel: 'stable'
|
||||
sha: '${{ needs.calculate-versions.outputs.STABLE_SHA }}'
|
||||
- channel: 'preview'
|
||||
sha: '${{ needs.calculate-versions.outputs.PREVIEW_SHA }}'
|
||||
- channel: 'nightly'
|
||||
sha: '${{ github.event.inputs.ref }}'
|
||||
steps:
|
||||
- name: 'Checkout Ref'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: '${{ github.event.inputs.ref }}'
|
||||
|
||||
- name: 'Checkout correct SHA'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: '${{ matrix.sha }}'
|
||||
path: 'release'
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install Dependencies'
|
||||
working-directory: './release'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Run Tests'
|
||||
if: "${{github.event.inputs.force_skip_tests != 'true'}}"
|
||||
uses: './.github/actions/run-tests'
|
||||
with:
|
||||
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
|
||||
working-directory: './release'
|
||||
|
||||
build-mac:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
uses: './.github/workflows/build-unsigned-mac-binaries.yml'
|
||||
with:
|
||||
ref: '${{ github.event.inputs.ref }}'
|
||||
|
||||
publish-preview:
|
||||
name: 'Publish preview'
|
||||
needs: ['calculate-versions', 'test', 'build-mac']
|
||||
runs-on: 'ubuntu-latest'
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
permissions:
|
||||
contents: 'write'
|
||||
packages: 'write'
|
||||
issues: 'write'
|
||||
steps:
|
||||
- name: 'Checkout Ref'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: '${{ github.event.inputs.ref }}'
|
||||
|
||||
- name: 'Checkout correct SHA'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: '${{ needs.calculate-versions.outputs.PREVIEW_SHA }}'
|
||||
path: 'release'
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install Dependencies'
|
||||
working-directory: './release'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Download macOS Binaries'
|
||||
uses: './.github/actions/download-mac-binaries'
|
||||
with:
|
||||
path: 'release/dist'
|
||||
|
||||
- name: 'Publish Release'
|
||||
uses: './.github/actions/publish-release'
|
||||
with:
|
||||
release-version: '${{ needs.calculate-versions.outputs.PREVIEW_VERSION }}'
|
||||
release-tag: 'v${{ needs.calculate-versions.outputs.PREVIEW_VERSION }}'
|
||||
npm-tag: 'preview'
|
||||
wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
|
||||
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
|
||||
wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}'
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
github-release-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
|
||||
dry-run: '${{ github.event.inputs.dry_run }}'
|
||||
previous-tag: '${{ needs.calculate-versions.outputs.PREVIOUS_PREVIEW_TAG }}'
|
||||
working-directory: './release'
|
||||
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
|
||||
force-skip-tests: '${{ github.event.inputs.force_skip_tests }}'
|
||||
npm-registry-publish-url: '${{ vars.NPM_REGISTRY_PUBLISH_URL }}'
|
||||
npm-registry-url: '${{ vars.NPM_REGISTRY_URL }}'
|
||||
npm-registry-scope: '${{ vars.NPM_REGISTRY_SCOPE }}'
|
||||
cli-package-name: '${{ vars.CLI_PACKAGE_NAME }}'
|
||||
core-package-name: '${{ vars.CORE_PACKAGE_NAME }}'
|
||||
a2a-package-name: '${{ vars.A2A_PACKAGE_NAME }}'
|
||||
|
||||
- name: 'Create Issue on Failure'
|
||||
if: '${{ failure() && github.event.inputs.dry_run == false }}'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
RELEASE_TAG: 'v${{ needs.calculate-versions.outputs.PREVIEW_VERSION }}'
|
||||
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
run: |
|
||||
gh issue create \
|
||||
--title 'Promote Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')' \
|
||||
--body 'The promote-release workflow failed during preview publish. See the full run for details: ${DETAILS_URL}' \
|
||||
--label 'release-failure,priority/p0'
|
||||
|
||||
publish-stable:
|
||||
name: 'Publish stable'
|
||||
needs: ['calculate-versions', 'test', 'publish-preview', 'build-mac']
|
||||
runs-on: 'ubuntu-latest'
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
permissions:
|
||||
contents: 'write'
|
||||
packages: 'write'
|
||||
issues: 'write'
|
||||
steps:
|
||||
- name: 'Checkout Ref'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: '${{ github.event.inputs.ref }}'
|
||||
|
||||
- name: 'Checkout correct SHA'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: '${{ needs.calculate-versions.outputs.STABLE_SHA }}'
|
||||
path: 'release'
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install Dependencies'
|
||||
working-directory: './release'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Download macOS Binaries'
|
||||
uses: './.github/actions/download-mac-binaries'
|
||||
with:
|
||||
path: 'release/dist'
|
||||
|
||||
- name: 'Publish Release'
|
||||
uses: './.github/actions/publish-release'
|
||||
with:
|
||||
release-version: '${{ needs.calculate-versions.outputs.STABLE_VERSION }}'
|
||||
release-tag: 'v${{ needs.calculate-versions.outputs.STABLE_VERSION }}'
|
||||
npm-tag: 'latest'
|
||||
wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
|
||||
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
|
||||
wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}'
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
github-release-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
|
||||
dry-run: '${{ github.event.inputs.dry_run }}'
|
||||
previous-tag: '${{ needs.calculate-versions.outputs.PREVIOUS_STABLE_TAG }}'
|
||||
working-directory: './release'
|
||||
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
|
||||
force-skip-tests: '${{ github.event.inputs.force_skip_tests }}'
|
||||
npm-registry-publish-url: '${{ vars.NPM_REGISTRY_PUBLISH_URL }}'
|
||||
npm-registry-url: '${{ vars.NPM_REGISTRY_URL }}'
|
||||
npm-registry-scope: '${{ vars.NPM_REGISTRY_SCOPE }}'
|
||||
cli-package-name: '${{ vars.CLI_PACKAGE_NAME }}'
|
||||
core-package-name: '${{ vars.CORE_PACKAGE_NAME }}'
|
||||
a2a-package-name: '${{ vars.A2A_PACKAGE_NAME }}'
|
||||
|
||||
- name: 'Create Issue on Failure'
|
||||
if: '${{ failure() && github.event.inputs.dry_run == false }}'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
RELEASE_TAG: 'v${{ needs.calculate-versions.outputs.STABLE_VERSION }}'
|
||||
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
run: |
|
||||
gh issue create \
|
||||
--title 'Promote Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')' \
|
||||
--body 'The promote-release workflow failed during stable publish. See the full run for details: ${DETAILS_URL}' \
|
||||
--label 'release-failure,priority/p0'
|
||||
|
||||
nightly-pr:
|
||||
name: 'Create Nightly PR'
|
||||
needs: ['publish-stable', 'calculate-versions']
|
||||
runs-on: 'ubuntu-latest'
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
permissions:
|
||||
contents: 'write'
|
||||
pull-requests: 'write'
|
||||
issues: 'write'
|
||||
steps:
|
||||
- name: 'Checkout Ref'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: '${{ github.event.inputs.ref }}'
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
|
||||
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 and switch to a new branch'
|
||||
id: 'release_branch'
|
||||
run: |
|
||||
BRANCH_NAME="chore/nightly-version-bump-${NEEDS_CALCULATE_VERSIONS_OUTPUTS_NEXT_NIGHTLY_VERSION}"
|
||||
git switch -c "${BRANCH_NAME}"
|
||||
echo "BRANCH_NAME=${BRANCH_NAME}" >> "${GITHUB_OUTPUT}"
|
||||
env:
|
||||
NEEDS_CALCULATE_VERSIONS_OUTPUTS_NEXT_NIGHTLY_VERSION: '${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}'
|
||||
|
||||
- name: 'Update package versions'
|
||||
run: 'npm run release:version "${NEEDS_CALCULATE_VERSIONS_OUTPUTS_NEXT_NIGHTLY_VERSION}"'
|
||||
env:
|
||||
NEEDS_CALCULATE_VERSIONS_OUTPUTS_NEXT_NIGHTLY_VERSION: '${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}'
|
||||
|
||||
- name: 'Commit and Push package versions'
|
||||
env:
|
||||
BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
|
||||
DRY_RUN: '${{ github.event.inputs.dry_run }}'
|
||||
NEEDS_CALCULATE_VERSIONS_OUTPUTS_NEXT_NIGHTLY_VERSION: '${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}'
|
||||
GIT_PUSH_TOKEN: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
|
||||
run: |-
|
||||
git add package.json packages/*/package.json
|
||||
if [ -f package-lock.json ]; then
|
||||
git add package-lock.json
|
||||
fi
|
||||
git commit -m "chore(release): bump version to ${NEEDS_CALCULATE_VERSIONS_OUTPUTS_NEXT_NIGHTLY_VERSION}"
|
||||
if [[ "${DRY_RUN}" == "false" ]]; then
|
||||
echo "Pushing release branch to remote..."
|
||||
git push "https://x-access-token:${GIT_PUSH_TOKEN}@github.com/${{ github.repository }}.git" "HEAD:${BRANCH_NAME}" --follow-tags
|
||||
else
|
||||
echo "Dry run enabled. Skipping push."
|
||||
fi
|
||||
|
||||
- name: '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.'
|
||||
github-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
|
||||
dry-run: '${{ github.event.inputs.dry_run }}'
|
||||
|
||||
- name: 'Create Issue on Failure'
|
||||
if: '${{ failure() && github.event.inputs.dry_run == false }}'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
RELEASE_TAG: 'v${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}'
|
||||
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
run: |
|
||||
gh issue create \
|
||||
--title 'Promote Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')' \
|
||||
--body 'The promote-release workflow failed during nightly PR creation. See the full run for details: ${DETAILS_URL}' \
|
||||
--label 'release-failure,priority/p0'
|
||||
@@ -0,0 +1,244 @@
|
||||
name: 'Release: Rollback change'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
rollback_origin:
|
||||
description: 'The package version to rollback FROM and delete (e.g., 0.5.0-preview-2)'
|
||||
required: true
|
||||
type: 'string'
|
||||
rollback_destination:
|
||||
description: 'The package version to rollback TO (e.g., 0.5.0-preview-2). This version must already exist on the npm registry.'
|
||||
required: false
|
||||
type: 'string'
|
||||
channel:
|
||||
description: 'The npm dist-tag to apply to rollback_destination (e.g., latest, preview, nightly). REQUIRED IF rollback_destination is set.'
|
||||
required: false
|
||||
type: 'choice'
|
||||
options:
|
||||
- 'latest'
|
||||
- 'preview'
|
||||
- 'nightly'
|
||||
- 'dev'
|
||||
default: 'dev'
|
||||
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
|
||||
environment:
|
||||
description: 'Environment'
|
||||
required: false
|
||||
type: 'choice'
|
||||
options:
|
||||
- 'prod'
|
||||
- 'dev'
|
||||
default: 'prod'
|
||||
|
||||
jobs:
|
||||
change-tags:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
packages: 'write'
|
||||
issues: 'write'
|
||||
steps:
|
||||
- name: 'Checkout repository'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: '${{ github.event.inputs.ref }}'
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: 'configure .npmrc'
|
||||
uses: './.github/actions/setup-npmrc'
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
|
||||
- name: 'Get Origin Version Tag'
|
||||
id: 'origin_tag'
|
||||
shell: 'bash'
|
||||
env:
|
||||
ROLLBACK_ORIGIN: '${{ github.event.inputs.rollback_origin }}'
|
||||
run: |
|
||||
TAG_VALUE="v${ROLLBACK_ORIGIN}"
|
||||
echo "ORIGIN_TAG=$TAG_VALUE" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 'Get Origin Commit Hash'
|
||||
id: 'origin_hash'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
ORIGIN_TAG: '${{ steps.origin_tag.outputs.ORIGIN_TAG }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
ORIGIN_HASH=$(git rev-parse "${ORIGIN_TAG}")
|
||||
echo "ORIGIN_HASH=${ORIGIN_HASH}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 'Change tag'
|
||||
if: "${{ github.event.inputs.rollback_destination != '' }}"
|
||||
uses: './.github/actions/tag-npm-release'
|
||||
with:
|
||||
channel: '${{ github.event.inputs.channel }}'
|
||||
version: '${{ github.event.inputs.rollback_destination }}'
|
||||
dry-run: '${{ github.event.inputs.dry-run }}'
|
||||
wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
|
||||
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
|
||||
wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}'
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
cli-package-name: '${{ vars.CLI_PACKAGE_NAME }}'
|
||||
core-package-name: '${{ vars.CORE_PACKAGE_NAME }}'
|
||||
a2a-package-name: '${{ vars.A2A_PACKAGE_NAME }}'
|
||||
|
||||
- name: 'Get cli Token'
|
||||
uses: './.github/actions/npm-auth-token'
|
||||
id: 'cli-token'
|
||||
with:
|
||||
package-name: '${{ vars.CLI_PACKAGE_NAME }}'
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
|
||||
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
|
||||
wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}'
|
||||
|
||||
- name: 'Deprecate Cli Npm Package'
|
||||
if: "${{ github.event.inputs.dry-run == 'false' && github.event.inputs.environment == 'prod' }}"
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ steps.cli-token.outputs.auth-token }}'
|
||||
PACKAGE_NAME: '${{ vars.CLI_PACKAGE_NAME }}'
|
||||
ROLLBACK_ORIGIN: '${{ github.event.inputs.rollback_origin }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm deprecate "${PACKAGE_NAME}@${ROLLBACK_ORIGIN}" "This version has been rolled back."
|
||||
|
||||
- name: 'Get core Token'
|
||||
uses: './.github/actions/npm-auth-token'
|
||||
id: 'core-token'
|
||||
with:
|
||||
package-name: '${{ vars.CLI_PACKAGE_NAME }}'
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
|
||||
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
|
||||
wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}'
|
||||
|
||||
- name: 'Deprecate Core Npm Package'
|
||||
if: "${{ github.event.inputs.dry-run == 'false' && github.event.inputs.environment == 'prod' }}"
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ steps.core-token.outputs.auth-token }}'
|
||||
PACKAGE_NAME: '${{ vars.CORE_PACKAGE_NAME }}'
|
||||
ROLLBACK_ORIGIN: '${{ github.event.inputs.rollback_origin }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm deprecate "${PACKAGE_NAME}@${ROLLBACK_ORIGIN}" "This version has been rolled back."
|
||||
|
||||
- name: 'Get a2a Token'
|
||||
uses: './.github/actions/npm-auth-token'
|
||||
id: 'a2a-token'
|
||||
with:
|
||||
package-name: '${{ vars.A2A_PACKAGE_NAME }}'
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
|
||||
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
|
||||
wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}'
|
||||
|
||||
- name: 'Deprecate A2A Server Npm Package'
|
||||
if: "${{ github.event.inputs.dry-run == 'false' && github.event.inputs.environment == 'prod' }}"
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ steps.a2a-token.outputs.auth-token }}'
|
||||
PACKAGE_NAME: '${{ vars.A2A_PACKAGE_NAME }}'
|
||||
ROLLBACK_ORIGIN: '${{ github.event.inputs.rollback_origin }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm deprecate "${PACKAGE_NAME}@${ROLLBACK_ORIGIN}" "This version has been rolled back."
|
||||
|
||||
- name: 'Delete Github Release'
|
||||
if: "${{ github.event.inputs.dry-run == 'false' && github.event.inputs.environment == 'prod'}}"
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
ORIGIN_TAG: '${{ steps.origin_tag.outputs.ORIGIN_TAG }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
gh release delete "${ORIGIN_TAG}" --yes
|
||||
|
||||
- name: 'Verify Origin Release Deletion'
|
||||
if: "${{ github.event.inputs.dry-run == 'false' }}"
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
TARGET_TAG: '${{ steps.origin_tag.outputs.ORIGIN_TAG }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
RELEASE_TAG=$(gh release view "$TARGET_TAG" --json tagName --jq .tagName)
|
||||
if [ "$RELEASE_TAG" = "$TARGET_TAG" ]; then
|
||||
echo "❌ Failed to delete release with tag ${TARGET_TAG}"
|
||||
echo '❌ This means the release was not deleted, and the workflow should fail.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: 'Add Rollback Tag'
|
||||
id: 'rollback_tag'
|
||||
if: "${{ github.event.inputs.dry-run == 'false' }}"
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
ROLLBACK_TAG_NAME: '${{ steps.origin_tag.outputs.ORIGIN_TAG }}-rollback'
|
||||
ORIGIN_HASH: '${{ steps.origin_hash.outputs.ORIGIN_HASH }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
echo "ROLLBACK_TAG=$ROLLBACK_TAG_NAME" >> "$GITHUB_OUTPUT"
|
||||
git tag "$ROLLBACK_TAG_NAME" "${ORIGIN_HASH}"
|
||||
git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.git" --tags
|
||||
|
||||
- name: 'Verify Rollback Tag Added'
|
||||
if: "${{ github.event.inputs.dry-run == 'false' }}"
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
TARGET_TAG: '${{ steps.rollback_tag.outputs.ROLLBACK_TAG }}'
|
||||
TARGET_HASH: '${{ steps.origin_hash.outputs.ORIGIN_HASH }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
ROLLBACK_COMMIT=$(git rev-parse -q --verify "$TARGET_TAG")
|
||||
if [ "$ROLLBACK_COMMIT" != "$TARGET_HASH" ]; then
|
||||
echo "❌ Failed to add tag ${TARGET_TAG} to commit ${TARGET_HASH}"
|
||||
echo '❌ This means the tag was not added, and the workflow should fail.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: 'Log Dry run'
|
||||
if: "${{ github.event.inputs.dry-run == 'true' }}"
|
||||
env:
|
||||
ROLLBACK_ORIGIN: '${{ github.event.inputs.rollback_origin }}'
|
||||
ROLLBACK_DESTINATION: '${{ github.event.inputs.rollback_destination }}'
|
||||
CHANNEL: '${{ github.event.inputs.channel }}'
|
||||
REF_INPUT: '${{ github.event.inputs.ref }}'
|
||||
ORIGIN_TAG: '${{ steps.origin_tag.outputs.ORIGIN_TAG }}'
|
||||
ORIGIN_HASH: '${{ steps.origin_hash.outputs.ORIGIN_HASH }}'
|
||||
ROLLBACK_TAG: '${{ steps.rollback_tag.outputs.ROLLBACK_TAG }}'
|
||||
CLI_PACKAGE_NAME: '${{ vars.CLI_PACKAGE_NAME }}'
|
||||
CORE_PACKAGE_NAME: '${{ vars.CORE_PACKAGE_NAME }}'
|
||||
A2A_PACKAGE_NAME: '${{ vars.A2A_PACKAGE_NAME }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
echo "
|
||||
Inputs:
|
||||
- rollback_origin: '${ROLLBACK_ORIGIN}'
|
||||
- rollback_destination: '${ROLLBACK_DESTINATION}'
|
||||
- channel: '${CHANNEL}'
|
||||
- ref: '${REF_INPUT}'
|
||||
|
||||
Outputs:
|
||||
- ORIGIN_TAG: '${ORIGIN_TAG}'
|
||||
- ORIGIN_HASH: '${ORIGIN_HASH}'
|
||||
- ROLLBACK_TAG: '${ROLLBACK_TAG}'
|
||||
|
||||
Would have npm deprecate ${CLI_PACKAGE_NAME}@${ROLLBACK_ORIGIN}, ${CORE_PACKAGE_NAME}@${ROLLBACK_ORIGIN}, and ${A2A_PACKAGE_NAME}@${ROLLBACK_ORIGIN}
|
||||
Would have deleted the github release with tag ${ORIGIN_TAG}
|
||||
Would have added tag ${ORIGIN_TAG}-rollback to ${ORIGIN_HASH}
|
||||
"
|
||||
@@ -0,0 +1,51 @@
|
||||
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:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
contents: 'read'
|
||||
packages: 'write'
|
||||
issues: 'write'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: '${{ github.event.inputs.ref || github.sha }}'
|
||||
fetch-depth: 0
|
||||
- name: 'Push'
|
||||
uses: './.github/actions/push-sandbox'
|
||||
with:
|
||||
dockerhub-username: '${{ secrets.DOCKER_SERVICE_ACCOUNT_NAME }}'
|
||||
dockerhub-token: '${{ secrets.DOCKER_SERVICE_ACCOUNT_KEY }}'
|
||||
github-actor: '${{ github.actor }}'
|
||||
github-secret: '${{ secrets.GITHUB_TOKEN }}'
|
||||
github-sha: '${{ github.sha }}'
|
||||
github-ref-name: '${{github.event.inputs.ref}}'
|
||||
dry-run: '${{ github.event.inputs.dry-run }}'
|
||||
- name: 'Create Issue on Failure'
|
||||
if: '${{ failure() && github.event.inputs.dry-run == false }}'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
run: |
|
||||
gh issue create \
|
||||
--title 'Sandbox Release Failed on $(date +'%Y-%m-%d')' \
|
||||
--body 'The sandbox-release workflow failed. See the full run for details: ${DETAILS_URL}' \
|
||||
--label 'release-failure,priority/p0'
|
||||
@@ -0,0 +1,52 @@
|
||||
name: 'On Merge Smoke Test'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'release/**'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: 'The branch, tag, or SHA to test on.'
|
||||
required: false
|
||||
type: 'string'
|
||||
default: 'main'
|
||||
dry-run:
|
||||
description: 'Run a dry-run of the smoke test; No bug will be created'
|
||||
required: true
|
||||
type: 'boolean'
|
||||
default: true
|
||||
|
||||
jobs:
|
||||
smoke-test:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
contents: 'write'
|
||||
packages: 'write'
|
||||
issues: 'write'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
ref: '${{ github.event.inputs.ref || github.sha }}'
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: 'Install Dependencies'
|
||||
run: 'npm ci'
|
||||
- name: 'Build bundle'
|
||||
run: 'npm run bundle'
|
||||
- name: 'Smoke test bundle'
|
||||
run: 'node ./bundle/gemini.js --version'
|
||||
- name: 'Create Issue on Failure'
|
||||
if: '${{ failure() && github.event.inputs.dry-run == false }}'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
REF: '${{ github.event.inputs.ref }}'
|
||||
run: |
|
||||
gh issue create \
|
||||
--title 'Smoke test failed on ${REF} @ $(date +'%Y-%m-%d')' \
|
||||
--body 'Smoke test build failed. See the full run for details: ${DETAILS_URL}' \
|
||||
--label 'priority/p0'
|
||||
@@ -0,0 +1,163 @@
|
||||
name: 'Test Build Binary'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: 'read'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: 'bash'
|
||||
|
||||
jobs:
|
||||
build-node-binary:
|
||||
name: 'Build Binary (${{ matrix.os }})'
|
||||
runs-on: '${{ matrix.os }}'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 'ubuntu-latest'
|
||||
platform_name: 'linux-x64'
|
||||
arch: 'x64'
|
||||
- os: 'windows-latest'
|
||||
platform_name: 'win32-x64'
|
||||
arch: 'x64'
|
||||
- os: 'macos-latest' # Apple Silicon (ARM64)
|
||||
platform_name: 'darwin-arm64'
|
||||
arch: 'arm64'
|
||||
- os: 'macos-latest' # Intel (x64) running on ARM via Rosetta
|
||||
platform_name: 'darwin-x64'
|
||||
arch: 'x64'
|
||||
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Optimize Windows Performance'
|
||||
if: "matrix.os == 'windows-latest'"
|
||||
run: |
|
||||
Set-MpPreference -DisableRealtimeMonitoring $true
|
||||
Stop-Service -Name "wsearch" -Force -ErrorAction SilentlyContinue
|
||||
Set-Service -Name "wsearch" -StartupType Disabled
|
||||
Stop-Service -Name "SysMain" -Force -ErrorAction SilentlyContinue
|
||||
Set-Service -Name "SysMain" -StartupType Disabled
|
||||
shell: 'powershell'
|
||||
|
||||
- name: 'Set up Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
architecture: '${{ matrix.arch }}'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Check Secrets'
|
||||
id: 'check_secrets'
|
||||
run: |
|
||||
echo "has_win_cert=${{ secrets.WINDOWS_PFX_BASE64 != '' }}" >> "$GITHUB_OUTPUT"
|
||||
echo "has_mac_cert=${{ secrets.MACOS_CERT_P12_BASE64 != '' }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 'Setup Windows SDK (Windows)'
|
||||
if: "matrix.os == 'windows-latest'"
|
||||
uses: 'microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce' # ratchet:microsoft/setup-msbuild@v2
|
||||
|
||||
- name: 'Add Signtool to Path (Windows)'
|
||||
if: "matrix.os == 'windows-latest'"
|
||||
run: |
|
||||
$signtoolPath = Get-ChildItem -Path "C:\Program Files (x86)\Windows Kits\10\bin" -Recurse -Filter "signtool.exe" | Sort-Object FullName -Descending | Select-Object -First 1 -ExpandProperty DirectoryName
|
||||
echo "Found signtool at: $signtoolPath"
|
||||
echo "$signtoolPath" >> $env:GITHUB_PATH
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Setup macOS Keychain'
|
||||
if: "startsWith(matrix.os, 'macos') && steps.check_secrets.outputs.has_mac_cert == 'true' && github.event_name != 'pull_request'"
|
||||
env:
|
||||
BUILD_CERTIFICATE_BASE64: '${{ secrets.MACOS_CERT_P12_BASE64 }}'
|
||||
P12_PASSWORD: '${{ secrets.MACOS_CERT_PASSWORD }}'
|
||||
KEYCHAIN_PASSWORD: 'temp-password'
|
||||
run: |
|
||||
# Create the P12 file
|
||||
echo "$BUILD_CERTIFICATE_BASE64" | base64 --decode > certificate.p12
|
||||
|
||||
# Create a temporary keychain
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security default-keychain -s build.keychain
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
|
||||
# Import the certificate
|
||||
security import certificate.p12 -k build.keychain -P "$P12_PASSWORD" -T /usr/bin/codesign
|
||||
|
||||
# Allow codesign to access it
|
||||
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" build.keychain
|
||||
|
||||
# Set Identity for build script
|
||||
echo "APPLE_IDENTITY=${{ secrets.MACOS_CERT_IDENTITY }}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: 'Setup Windows Certificate'
|
||||
if: "matrix.os == 'windows-latest' && steps.check_secrets.outputs.has_win_cert == 'true' && github.event_name != 'pull_request'"
|
||||
env:
|
||||
PFX_BASE64: '${{ secrets.WINDOWS_PFX_BASE64 }}'
|
||||
PFX_PASSWORD: '${{ secrets.WINDOWS_PFX_PASSWORD }}'
|
||||
run: |
|
||||
$pfx_cert_byte = [System.Convert]::FromBase64String("$env:PFX_BASE64")
|
||||
$certPath = Join-Path (Get-Location) "cert.pfx"
|
||||
[IO.File]::WriteAllBytes($certPath, $pfx_cert_byte)
|
||||
echo "WINDOWS_PFX_FILE=$certPath" >> $env:GITHUB_ENV
|
||||
echo "WINDOWS_PFX_PASSWORD=$env:PFX_PASSWORD" >> $env:GITHUB_ENV
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Build Binary'
|
||||
run: 'npm run build:binary'
|
||||
|
||||
- name: 'Build Core Package'
|
||||
run: 'npm run build -w @google/gemini-cli-core'
|
||||
|
||||
- name: 'Verify Output Exists'
|
||||
run: |
|
||||
if [ -f "dist/${{ matrix.platform_name }}/gemini" ]; then
|
||||
echo "Binary found at dist/${{ matrix.platform_name }}/gemini"
|
||||
elif [ -f "dist/${{ matrix.platform_name }}/gemini.exe" ]; then
|
||||
echo "Binary found at dist/${{ matrix.platform_name }}/gemini.exe"
|
||||
else
|
||||
echo "Error: Binary not found in dist/${{ matrix.platform_name }}/"
|
||||
ls -R dist/
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: 'Smoke Test Binary'
|
||||
run: |
|
||||
echo "Running binary smoke test..."
|
||||
if [ -f "dist/${{ matrix.platform_name }}/gemini.exe" ]; then
|
||||
"./dist/${{ matrix.platform_name }}/gemini.exe" --version
|
||||
else
|
||||
"./dist/${{ matrix.platform_name }}/gemini" --version
|
||||
fi
|
||||
|
||||
- name: 'Run Integration Tests'
|
||||
if: "github.event_name != 'pull_request'"
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
GEMINI_CLI_TRUST_WORKSPACE: true
|
||||
run: |
|
||||
echo "Running integration tests with binary..."
|
||||
if [[ "${{ matrix.os }}" == 'windows-latest' ]]; then
|
||||
BINARY_PATH="$(cygpath -m "$(pwd)/dist/${{ matrix.platform_name }}/gemini.exe")"
|
||||
else
|
||||
BINARY_PATH="$(pwd)/dist/${{ matrix.platform_name }}/gemini"
|
||||
fi
|
||||
echo "Using binary at $BINARY_PATH"
|
||||
export INTEGRATION_TEST_GEMINI_BINARY_PATH="$BINARY_PATH"
|
||||
npm run test:integration:sandbox:none -- --testTimeout=600000
|
||||
|
||||
- name: 'Upload Artifact'
|
||||
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'gemini-cli-${{ matrix.platform_name }}'
|
||||
path: 'dist/${{ matrix.platform_name }}/'
|
||||
retention-days: 5
|
||||
@@ -0,0 +1,45 @@
|
||||
name: 'Testing: Tools (Python)'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'release/**'
|
||||
paths:
|
||||
- 'tools/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'release/**'
|
||||
paths:
|
||||
- 'tools/**'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: 'bash'
|
||||
|
||||
jobs:
|
||||
python-tests:
|
||||
name: 'Python Tests'
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
|
||||
|
||||
- name: 'Set up Python'
|
||||
uses: 'actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55' # ratchet:actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.13'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: 'tools/caretaker-agent/cloudrun/triage-worker/requirements.txt'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
if [ -f tools/caretaker-agent/cloudrun/triage-worker/requirements.txt ]; then
|
||||
python -m pip install -r tools/caretaker-agent/cloudrun/triage-worker/requirements.txt
|
||||
fi
|
||||
|
||||
- name: 'Run unittest suite'
|
||||
run: |
|
||||
PYTHONPATH=tools/caretaker-agent/cloudrun/triage-worker python -m unittest discover -s tools/caretaker-agent/cloudrun/triage-worker/tests -t tools/caretaker-agent/cloudrun/triage-worker
|
||||
@@ -0,0 +1,40 @@
|
||||
name: 'Trigger E2E'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
repo_name:
|
||||
description: 'Repository name (e.g., owner/repo)'
|
||||
required: false
|
||||
type: 'string'
|
||||
head_sha:
|
||||
description: 'SHA of the commit to test'
|
||||
required: false
|
||||
type: 'string'
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
save_repo_name:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
steps:
|
||||
- name: 'Save Repo name'
|
||||
env:
|
||||
REPO_NAME: '${{ github.event.inputs.repo_name || github.event.pull_request.head.repo.full_name }}'
|
||||
HEAD_SHA: '${{ github.event.inputs.head_sha || github.event.pull_request.head.sha }}'
|
||||
run: |
|
||||
mkdir -p ./pr
|
||||
echo "${REPO_NAME}" > ./pr/repo_name
|
||||
echo "${HEAD_SHA}" > ./pr/head_sha
|
||||
- uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'repo_name'
|
||||
path: 'pr/'
|
||||
trigger_e2e:
|
||||
name: 'Trigger e2e'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
steps:
|
||||
- id: 'trigger-e2e'
|
||||
run: |
|
||||
echo "Trigger e2e workflow"
|
||||
@@ -0,0 +1,315 @@
|
||||
name: 'Unassign Inactive Issue Assignees'
|
||||
|
||||
# This workflow runs daily and scans every open "help wanted" issue that has
|
||||
# one or more assignees. For each assignee it checks whether they have a
|
||||
# non-draft pull request (open and ready for review, or already merged) that
|
||||
# is linked to the issue. Draft PRs are intentionally excluded so that
|
||||
# contributors cannot reset the check by opening a no-op PR. If no
|
||||
# qualifying PR is found within 7 days of assignment the assignee is
|
||||
# automatically removed and a friendly comment is posted so that other
|
||||
# contributors can pick up the work.
|
||||
# Maintainers, org members, and collaborators (anyone with write access or
|
||||
# above) are always exempted and will never be auto-unassigned.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 9 * * *' # Every day at 09:00 UTC
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: 'Run in dry-run mode (no changes will be applied)'
|
||||
required: false
|
||||
default: false
|
||||
type: 'boolean'
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: 'bash'
|
||||
|
||||
jobs:
|
||||
unassign-inactive-assignees:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
issues: 'write'
|
||||
|
||||
steps:
|
||||
- name: 'Generate GitHub App Token'
|
||||
id: 'generate_token'
|
||||
uses: 'actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349' # ratchet:actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: '${{ secrets.APP_ID }}'
|
||||
private-key: '${{ secrets.PRIVATE_KEY }}'
|
||||
|
||||
- name: 'Unassign inactive assignees'
|
||||
uses: 'actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b' # ratchet:actions/github-script@v7
|
||||
env:
|
||||
DRY_RUN: '${{ inputs.dry_run }}'
|
||||
with:
|
||||
github-token: '${{ steps.generate_token.outputs.token }}'
|
||||
script: |
|
||||
const dryRun = process.env.DRY_RUN === 'true';
|
||||
if (dryRun) {
|
||||
core.info('DRY RUN MODE ENABLED: No changes will be applied.');
|
||||
}
|
||||
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const GRACE_PERIOD_DAYS = 7;
|
||||
const now = new Date();
|
||||
|
||||
let maintainerLogins = new Set();
|
||||
const teams = ['gemini-cli-maintainers', 'gemini-cli-askmode-approvers', 'gemini-cli-docs'];
|
||||
|
||||
for (const team_slug of teams) {
|
||||
try {
|
||||
const members = await github.paginate(github.rest.teams.listMembersInOrg, {
|
||||
org: owner,
|
||||
team_slug,
|
||||
});
|
||||
for (const m of members) maintainerLogins.add(m.login.toLowerCase());
|
||||
core.info(`Fetched ${members.length} members from team ${team_slug}.`);
|
||||
} catch (e) {
|
||||
core.warning(`Could not fetch team ${team_slug}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const isGooglerCache = new Map();
|
||||
const isGoogler = async (login) => {
|
||||
if (isGooglerCache.has(login)) return isGooglerCache.get(login);
|
||||
try {
|
||||
for (const org of ['googlers', 'google']) {
|
||||
try {
|
||||
await github.rest.orgs.checkMembershipForUser({ org, username: login });
|
||||
isGooglerCache.set(login, true);
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e.status !== 404) throw e;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
core.warning(`Could not check org membership for ${login}: ${e.message}`);
|
||||
}
|
||||
isGooglerCache.set(login, false);
|
||||
return false;
|
||||
};
|
||||
|
||||
const permissionCache = new Map();
|
||||
const isPrivilegedUser = async (login) => {
|
||||
if (maintainerLogins.has(login.toLowerCase())) return true;
|
||||
|
||||
if (permissionCache.has(login)) return permissionCache.get(login);
|
||||
|
||||
try {
|
||||
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner,
|
||||
repo,
|
||||
username: login,
|
||||
});
|
||||
const privileged = ['admin', 'maintain', 'write', 'triage'].includes(data.permission);
|
||||
permissionCache.set(login, privileged);
|
||||
if (privileged) {
|
||||
core.info(` @${login} is a repo collaborator (${data.permission}) — exempt.`);
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.status !== 404) {
|
||||
core.warning(`Could not check permission for ${login}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const googler = await isGoogler(login);
|
||||
permissionCache.set(login, googler);
|
||||
return googler;
|
||||
};
|
||||
|
||||
core.info('Fetching open "help wanted" issues with assignees...');
|
||||
|
||||
const issues = await github.paginate(github.rest.issues.listForRepo, {
|
||||
owner,
|
||||
repo,
|
||||
state: 'open',
|
||||
labels: 'help wanted',
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const assignedIssues = issues.filter(
|
||||
(issue) => !issue.pull_request && issue.assignees && issue.assignees.length > 0
|
||||
);
|
||||
|
||||
core.info(`Found ${assignedIssues.length} assigned "help wanted" issues.`);
|
||||
|
||||
let totalUnassigned = 0;
|
||||
|
||||
let timelineEvents = [];
|
||||
try {
|
||||
timelineEvents = await github.paginate(github.rest.issues.listEventsForTimeline, {
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issue.number,
|
||||
per_page: 100,
|
||||
mediaType: { previews: ['mockingbird'] },
|
||||
});
|
||||
} catch (err) {
|
||||
core.warning(`Could not fetch timeline for issue #${issue.number}: ${err.message}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const assignedAtMap = new Map();
|
||||
|
||||
for (const event of timelineEvents) {
|
||||
if (event.event === 'assigned' && event.assignee) {
|
||||
const login = event.assignee.login.toLowerCase();
|
||||
const at = new Date(event.created_at);
|
||||
assignedAtMap.set(login, at);
|
||||
} else if (event.event === 'unassigned' && event.assignee) {
|
||||
assignedAtMap.delete(event.assignee.login.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
const linkedPRAuthorSet = new Set();
|
||||
const seenPRKeys = new Set();
|
||||
|
||||
for (const event of timelineEvents) {
|
||||
if (
|
||||
event.event !== 'cross-referenced' ||
|
||||
!event.source ||
|
||||
event.source.type !== 'pull_request' ||
|
||||
!event.source.issue ||
|
||||
!event.source.issue.user ||
|
||||
!event.source.issue.number ||
|
||||
!event.source.issue.repository
|
||||
) continue;
|
||||
|
||||
const prOwner = event.source.issue.repository.owner.login;
|
||||
const prRepo = event.source.issue.repository.name;
|
||||
const prNumber = event.source.issue.number;
|
||||
const prAuthor = event.source.issue.user.login.toLowerCase();
|
||||
const prKey = `${prOwner}/${prRepo}#${prNumber}`;
|
||||
|
||||
if (seenPRKeys.has(prKey)) continue;
|
||||
seenPRKeys.add(prKey);
|
||||
|
||||
try {
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner: prOwner,
|
||||
repo: prRepo,
|
||||
pull_number: prNumber,
|
||||
});
|
||||
|
||||
const isReady = (pr.state === 'open' && !pr.draft) ||
|
||||
(pr.state === 'closed' && pr.merged_at !== null);
|
||||
|
||||
core.info(
|
||||
` PR ${prKey} by @${prAuthor}: ` +
|
||||
`state=${pr.state}, draft=${pr.draft}, merged=${!!pr.merged_at} → ` +
|
||||
(isReady ? 'qualifies' : 'does NOT qualify (draft or closed without merge)')
|
||||
);
|
||||
|
||||
if (isReady) linkedPRAuthorSet.add(prAuthor);
|
||||
} catch (err) {
|
||||
core.warning(`Could not fetch PR ${prKey}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const assigneesToRemove = [];
|
||||
|
||||
for (const assignee of issue.assignees) {
|
||||
const login = assignee.login.toLowerCase();
|
||||
|
||||
if (await isPrivilegedUser(assignee.login)) {
|
||||
core.info(` @${assignee.login}: privileged user — skipping.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const assignedAt = assignedAtMap.get(login);
|
||||
|
||||
if (!assignedAt) {
|
||||
core.warning(
|
||||
`No 'assigned' event found for @${login} on issue #${issue.number}; ` +
|
||||
`falling back to issue creation date (${issue.created_at}).`
|
||||
);
|
||||
assignedAtMap.set(login, new Date(issue.created_at));
|
||||
}
|
||||
const resolvedAssignedAt = assignedAtMap.get(login);
|
||||
|
||||
const daysSinceAssignment = (now - resolvedAssignedAt) / (1000 * 60 * 60 * 24);
|
||||
|
||||
core.info(
|
||||
` @${login}: assigned ${daysSinceAssignment.toFixed(1)} day(s) ago, ` +
|
||||
`ready-for-review PR: ${linkedPRAuthorSet.has(login) ? 'yes' : 'no'}`
|
||||
);
|
||||
|
||||
if (daysSinceAssignment < GRACE_PERIOD_DAYS) {
|
||||
core.info(` → within grace period, skipping.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (linkedPRAuthorSet.has(login)) {
|
||||
core.info(` → ready-for-review PR found, keeping assignment.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
core.info(` → no ready-for-review PR after ${GRACE_PERIOD_DAYS} days, will unassign.`);
|
||||
assigneesToRemove.push(assignee.login);
|
||||
}
|
||||
|
||||
if (assigneesToRemove.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
try {
|
||||
await github.rest.issues.removeAssignees({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issue.number,
|
||||
assignees: assigneesToRemove,
|
||||
});
|
||||
} catch (err) {
|
||||
core.warning(
|
||||
`Failed to unassign ${assigneesToRemove.join(', ')} from issue #${issue.number}: ${err.message}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const mentionList = assigneesToRemove.map((l) => `@${l}`).join(', ');
|
||||
const commentBody =
|
||||
`👋 ${mentionList} — it has been more than ${GRACE_PERIOD_DAYS} days since ` +
|
||||
`you were assigned to this issue and we could not find a pull request ` +
|
||||
`ready for review.\n\n` +
|
||||
`To keep the backlog moving and ensure issues stay accessible to all ` +
|
||||
`contributors, we require a PR that is open and ready for review (not a ` +
|
||||
`draft) within ${GRACE_PERIOD_DAYS} days of assignment.\n\n` +
|
||||
`We are automatically unassigning you so that other contributors can pick ` +
|
||||
`this up. If you are still actively working on this, please:\n` +
|
||||
`1. Re-assign yourself by commenting \`/assign\`.\n` +
|
||||
`2. Open a PR (not a draft) linked to this issue (e.g. \`Fixes #${issue.number}\`) ` +
|
||||
`within ${GRACE_PERIOD_DAYS} days so the automation knows real progress is being made.\n\n` +
|
||||
`Thank you for your contribution — we hope to see a PR from you soon! 🙏`;
|
||||
|
||||
try {
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issue.number,
|
||||
body: commentBody,
|
||||
});
|
||||
} catch (err) {
|
||||
core.warning(
|
||||
`Failed to post comment on issue #${issue.number}: ${err.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
totalUnassigned += assigneesToRemove.length;
|
||||
core.info(
|
||||
` ${dryRun ? '[DRY RUN] Would have unassigned' : 'Unassigned'}: ${assigneesToRemove.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
core.info(`\nDone. Total assignees ${dryRun ? 'that would be' : ''} unassigned: ${totalUnassigned}`);
|
||||
@@ -0,0 +1,58 @@
|
||||
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-tag:
|
||||
description: 'NPM tag to verify'
|
||||
required: true
|
||||
type: 'choice'
|
||||
options:
|
||||
- 'dev'
|
||||
- 'latest'
|
||||
- 'preview'
|
||||
- 'nightly'
|
||||
default: 'latest'
|
||||
environment:
|
||||
description: 'Environment'
|
||||
required: false
|
||||
type: 'choice'
|
||||
options:
|
||||
- 'prod'
|
||||
- 'dev'
|
||||
default: 'prod'
|
||||
|
||||
jobs:
|
||||
verify-release:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ['ubuntu-latest', 'macos-latest', 'windows-latest']
|
||||
runs-on: '${{ matrix.os }}'
|
||||
permissions:
|
||||
contents: 'read'
|
||||
packages: 'write'
|
||||
issues: 'write'
|
||||
steps:
|
||||
- name: '📝 Print vars'
|
||||
shell: 'bash'
|
||||
run: 'echo "${{ toJSON(vars) }}"'
|
||||
- uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: 'Verify release'
|
||||
uses: './.github/actions/verify-release'
|
||||
with:
|
||||
npm-package: '${{vars.CLI_PACKAGE_NAME}}@${{github.event.inputs.npm-tag}}'
|
||||
expected-version: '${{github.event.inputs.version}}'
|
||||
working-directory: '.'
|
||||
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
|
||||
npm-registry-url: '${{ vars.NPM_REGISTRY_URL }}'
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
npm-registry-scope: '${{ vars.NPM_REGISTRY_SCOPE }}'
|
||||
Reference in New Issue
Block a user