diff --git a/.github/scripts/backfill-need-triage.cjs b/.github/scripts/backfill-need-triage.cjs new file mode 100644 index 0000000000..e621396528 --- /dev/null +++ b/.github/scripts/backfill-need-triage.cjs @@ -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); +}); diff --git a/.github/scripts/pr-triage.sh b/.github/scripts/pr-triage.sh index ddbe4182ce..2052406869 100755 --- a/.github/scripts/pr-triage.sh +++ b/.github/scripts/pr-triage.sh @@ -1,4 +1,8 @@ #!/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 @@ -10,7 +14,7 @@ ISSUE_LABELS_CACHE_FLAT="" # Function to get area and priority labels from an issue (with caching) get_issue_labels() { - local ISSUE_NUM=$1 + local ISSUE_NUM="${1}" if [[ -z "${ISSUE_NUM}" || "${ISSUE_NUM}" == "null" || "${ISSUE_NUM}" == "" ]]; then return fi @@ -18,10 +22,13 @@ get_issue_labels() { # Check cache case " ${ISSUE_LABELS_CACHE_FLAT} " in *" ${ISSUE_NUM}:"*) - local suffix="${ISSUE_LABELS_CACHE_FLAT#* ${ISSUE_NUM}:}" + local suffix="${ISSUE_LABELS_CACHE_FLAT#* " ${ISSUE_NUM}:"}" echo "${suffix%% *}" return ;; + *) + # Cache miss, proceed to fetch + ;; esac echo " ๐Ÿ“ฅ Fetching area and priority labels from issue #${ISSUE_NUM}" >&2 @@ -33,19 +40,19 @@ get_issue_labels() { fi local labels - labels=$(echo "${gh_output}" | grep -E "^(area|priority)/" | tr '\n' ',' | sed 's/,$//' || echo "") + labels=$(echo "${gh_output}" | grep -E '^(area|priority)/' | tr '\n' ',' | sed 's/,$//' || echo "") # Save to flat cache ISSUE_LABELS_CACHE_FLAT="${ISSUE_LABELS_CACHE_FLAT} ${ISSUE_NUM}:${labels}" - echo "$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 + local PR_NUMBER="${1}" + local IS_DRAFT="${2}" + local ISSUE_NUMBER="${3}" + local CURRENT_LABELS="${4}" # Comma-separated labels echo "๐Ÿ”„ Processing PR #${PR_NUMBER}" @@ -84,7 +91,7 @@ process_pr_optimized() { ISSUE_LABELS=$(get_issue_labels "${ISSUE_NUMBER}") if [[ -n "${ISSUE_LABELS}" ]]; then - local IFS_OLD=$IFS + local IFS_OLD="${IFS}" IFS=',' for label in ${ISSUE_LABELS}; do if [[ -n "${label}" ]] && [[ ",${CURRENT_LABELS}," != *",${label},"* ]]; then @@ -94,8 +101,8 @@ process_pr_optimized() { LABELS_TO_ADD="${LABELS_TO_ADD},${label}" fi fi - done - IFS=$IFS_OLD +done + IFS="${IFS_OLD}" fi if [[ -z "${LABELS_TO_ADD}" && -z "${LABELS_TO_REMOVE}" ]]; then @@ -135,7 +142,7 @@ JQ_EXTRACT_FIELDS='{ labels: [.labels[].name] | join(",") }' -JQ_TSV_FORMAT='"\((.number | tostring))\t\(.isDraft)\t\((.issue // "null") | tostring)\t\(.labels)"' +JQ_TSV_FORMAT='"\((.number | tostring))\t\(.isDraft)\t\((.issue // \"null\") | tostring)\t\(.labels)"' # Corrected escaping for quotes within the string literal if [[ -n "${PR_NUMBER:-}" ]]; then echo "๐Ÿ”„ Processing single PR #${PR_NUMBER}" @@ -144,9 +151,9 @@ if [[ -n "${PR_NUMBER:-}" ]]; then 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" + 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) || { @@ -157,11 +164,15 @@ else 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 < <(echo "${PR_DATA_ALL}" | jq -r ".[] | $JQ_EXTRACT_FIELDS | $JQ_TSV_FORMAT") + [[ -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 @@ -170,4 +181,4 @@ else echo "prs_needing_comment=[${PRS_NEEDING_COMMENT}]" >> "${GITHUB_OUTPUT}" fi -echo "โœ… PR triage completed" \ No newline at end of file +echo "โœ… PR triage completed" diff --git a/.github/workflows/gemini-automated-issue-triage.yml b/.github/workflows/gemini-automated-issue-triage.yml index 47801fcb9b..08b97db0a2 100644 --- a/.github/workflows/gemini-automated-issue-triage.yml +++ b/.github/workflows/gemini-automated-issue-triage.yml @@ -95,7 +95,8 @@ jobs: id: 'generate_token' env: APP_ID: '${{ secrets.APP_ID }}' - if: "${{ env.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 }}' @@ -305,22 +306,6 @@ jobs: }); core.info(`Successfully added labels for #${issueNumber}: ${labelsToAdd.join(', ')}`); - // Remove the 'status/need-triage' label - try { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - name: 'status/need-triage' - }); - core.info(`Successfully removed 'status/need-triage' label.`); - } catch (error) { - // If the label doesn't exist, the API call will throw a 404. We can ignore this. - if (error.status !== 404) { - core.warning(`Failed to remove 'status/need-triage': ${error.message}`); - } - } - - name: 'Post Issue Analysis Failure Comment' if: |- ${{ failure() && steps.gemini_issue_analysis.outcome == 'failure' }} diff --git a/.github/workflows/gemini-scheduled-issue-triage.yml b/.github/workflows/gemini-scheduled-issue-triage.yml index 7a966d59aa..25b0cdf4ec 100644 --- a/.github/workflows/gemini-scheduled-issue-triage.yml +++ b/.github/workflows/gemini-scheduled-issue-triage.yml @@ -40,7 +40,8 @@ jobs: permission-issues: 'write' - name: 'Get issue from event' - if: "github.event_name == 'issues'" + if: |- + ${{ github.event_name == 'issues' }} id: 'get_issue_from_event' env: ISSUE_EVENT: '${{ toJSON(github.event.issue) }}' @@ -51,7 +52,8 @@ jobs: echo "โœ… Found issue #${{ github.event.issue.number }} from event to triage! ๐ŸŽฏ" - name: 'Find untriaged issues' - if: "github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'" + if: |- + ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }} id: 'find_issues' env: GITHUB_TOKEN: '${{ steps.generate_token.outputs.token }}' @@ -161,7 +163,6 @@ jobs: 9. For each issue please check if CLI version is present, this is usually in the output of the /about command and will look like 0.1.5 - Anything more than 6 versions older than the most recent should add the status/need-retesting label 10. If you see that the issue doesn't look like it has sufficient information recommend the status/need-information label and leave a comment politely requesting the relevant information, eg.. if repro steps are missing request for repro steps. if version information is missing request for version information into the explanation section below. - - After identifying appropriate labels to an issue, add "status/need-triage" label to labels_to_remove in the output. 11. If you think an issue might be a Priority/P0 do not apply the priority/p0 label. Instead apply a status/manual-triage label and include a note in your explanation. 12. 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. @@ -262,24 +263,6 @@ jobs: core.info(`Successfully added labels for #${issueNumber}: ${labelsToAdd.join(', ')}${explanation}`); } - if (entry.labels_to_remove && entry.labels_to_remove.length > 0) { - for (const label of entry.labels_to_remove) { - try { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - name: label - }); - } catch (error) { - if (error.status !== 404) { - throw error; - } - } - } - core.info(`Successfully removed labels for #${issueNumber}: ${entry.labels_to_remove.join(', ')}`); - } - if (entry.explanation) { await github.rest.issues.createComment({ owner: context.repo.owner, diff --git a/.github/workflows/issue-opened-labeler.yml b/.github/workflows/issue-opened-labeler.yml new file mode 100644 index 0000000000..69a0911954 --- /dev/null +++ b/.github/workflows/issue-opened-labeler.yml @@ -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.'); + } diff --git a/scripts/batch_triage.sh b/scripts/batch_triage.sh index c6f1982491..1f4a84b97a 100755 --- a/scripts/batch_triage.sh +++ b/scripts/batch_triage.sh @@ -4,37 +4,39 @@ # Example: ./scripts/batch_triage.sh google-gemini/maintainers-gemini-cli set -e +set -o pipefail REPO="${1:-google-gemini/gemini-cli}" WORKFLOW="gemini-automated-issue-triage.yml" -echo "๐Ÿ” Searching for open issues in '$REPO' that need triage (missing 'area/' label)..." +echo "๐Ÿ” Searching for open issues in '${REPO}' that need triage (missing 'area/' label)..." # Fetch open issues with number, title, and labels # We fetch up to 1000 issues. -ISSUES_JSON=$(gh issue list --repo "$REPO" --state open --limit 1000 --json number,title,labels) +ISSUES_JSON=$(gh issue list --repo "${REPO}" --state open --limit 1000 --json number,title,labels) # Filter issues that DO NOT have a label starting with 'area/' -TARGET_ISSUES=$(echo "$ISSUES_JSON" | jq '[.[] | select(.labels | map(.name) | any(startswith("area/")) | not)]') +TARGET_ISSUES=$(echo "${ISSUES_JSON}" | jq '[.[] | select(.labels | map(.name) | any(startswith("area/")) | not)]') -COUNT=$(echo "$TARGET_ISSUES" | jq '. | length') +# Avoid masking return value +COUNT=$(jq '. | length' <<< "${TARGET_ISSUES}") -if [ "$COUNT" -eq 0 ]; then - echo "โœ… No issues found needing triage in '$REPO'." +if [[ "${COUNT}" -eq 0 ]]; then + echo "โœ… No issues found needing triage in '${REPO}'." exit 0 fi -echo "๐Ÿš€ Found $COUNT issues to triage." +echo "๐Ÿš€ Found ${COUNT} issues to triage." # Loop through and trigger workflow -echo "$TARGET_ISSUES" | jq -r '.[] | "\(.number)|\(.title)"' | while IFS="|" read -r number title; do - echo "โ–ถ๏ธ Triggering triage for #$number: $title" +echo "${TARGET_ISSUES}" | jq -r '.[] | "\(.number)|\(.title)"' | while IFS="|" read -r number title; do + echo "โ–ถ๏ธ Triggering triage for #${number}: ${title}" # Trigger the workflow dispatch event - gh workflow run "$WORKFLOW" --repo "$REPO" -f issue_number="$number" + gh workflow run "${WORKFLOW}" --repo "${REPO}" -f issue_number="${number}" # Sleep briefly to be nice to the API sleep 1 done -echo "๐ŸŽ‰ All triage workflows triggered!" +echo "๐ŸŽ‰ All triage workflows triggered!" \ No newline at end of file diff --git a/scripts/relabel_issues.sh b/scripts/relabel_issues.sh index 82857bfa45..9e8a776440 100755 --- a/scripts/relabel_issues.sh +++ b/scripts/relabel_issues.sh @@ -3,40 +3,42 @@ # Usage: ./scripts/relabel_issues.sh [repository] set -e +set -o pipefail -OLD_LABEL="$1" -NEW_LABEL="$2" +OLD_LABEL="${1}" +NEW_LABEL="${2}" REPO="${3:-google-gemini/gemini-cli}" -if [ -z "$OLD_LABEL" ] || [ -z "$NEW_LABEL" ]; then +if [[ -z "${OLD_LABEL}" ]] || [[ -z "${NEW_LABEL}" ]]; then echo "Usage: $0 [repository]" echo "Example: $0 'area/models' 'area/agent'" exit 1 fi -echo "๐Ÿ” Searching for open issues in '$REPO' with label '$OLD_LABEL'..." +echo "๐Ÿ” Searching for open issues in '${REPO}' with label '${OLD_LABEL}'..." # Fetch issues with the old label -ISSUES=$(gh issue list --repo "$REPO" --label "$OLD_LABEL" --state open --limit 1000 --json number,title) +ISSUES=$(gh issue list --repo "${REPO}" --label "${OLD_LABEL}" --state open --limit 1000 --json number,title) -COUNT=$(echo "$ISSUES" | jq '. | length') +# Avoid masking return value +COUNT=$(jq '. | length' <<< "${ISSUES}") -if [ "$COUNT" -eq 0 ]; then - echo "โœ… No issues found with label '$OLD_LABEL'." +if [[ "${COUNT}" -eq 0 ]]; then + echo "โœ… No issues found with label '${OLD_LABEL}'." exit 0 fi -echo "found $COUNT issues to relabel." +echo "found ${COUNT} issues to relabel." # Iterate and update -echo "$ISSUES" | jq -r '.[] | "\(.number) \(.title)"' | while read -r number title; do - echo "๐Ÿ”„ Processing #$number: $title" - echo " - Removing: $OLD_LABEL" - echo " + Adding: $NEW_LABEL" +echo "${ISSUES}" | jq -r '.[] | "\(.number) \(.title)"' | while read -r number title; do + echo "๐Ÿ”„ Processing #${number}: ${title}" + echo " - Removing: ${OLD_LABEL}" + echo " + Adding: ${NEW_LABEL}" - gh issue edit "$number" --repo "$REPO" --add-label "$NEW_LABEL" --remove-label "$OLD_LABEL" + gh issue edit "${number}" --repo "${REPO}" --add-label "${NEW_LABEL}" --remove-label "${OLD_LABEL}" echo " โœ… Done." done -echo "๐ŸŽ‰ All issues relabeled!" +echo "๐ŸŽ‰ All issues relabeled!" \ No newline at end of file diff --git a/scripts/send_gemini_request.sh b/scripts/send_gemini_request.sh index 18cedfa5bf..ebccf7e89b 100755 --- a/scripts/send_gemini_request.sh +++ b/scripts/send_gemini_request.sh @@ -30,9 +30,10 @@ set -e -E # Load environment variables from .env if it exists -if [ -f ".env" ]; then +if [[ -f ".env" ]]; then echo "Loading environment variables from .env file..." set -a # Automatically export all variables + # shellcheck source=/dev/null source .env set +a fi @@ -49,32 +50,32 @@ STREAM_MODE=false # Parse command line arguments while [[ "$#" -gt 0 ]]; do case $1 in - --payload) PAYLOAD_FILE="$2"; shift ;; - --model) MODEL_ID="$2"; shift ;; + --payload) PAYLOAD_FILE="${2}"; shift ;; + --model) MODEL_ID="${2}"; shift ;; --stream) STREAM_MODE=true ;; - *) echo "Unknown parameter passed: $1"; usage ;; + *) echo "Unknown parameter passed: ${1}"; usage ;; esac shift done # Validate inputs -if [ -z "$PAYLOAD_FILE" ] || [ -z "$MODEL_ID" ]; then +if [[ -z "${PAYLOAD_FILE}" ]] || [[ -z "${MODEL_ID}" ]]; then echo "Error: Missing required arguments." usage fi -if [ -z "$GEMINI_API_KEY" ]; then +if [[ -z "${GEMINI_API_KEY}" ]]; then echo "Error: GEMINI_API_KEY environment variable is not set." exit 1 fi -if [ ! -f "$PAYLOAD_FILE" ]; then - echo "Error: Payload file '$PAYLOAD_FILE' does not exist." +if [[ ! -f "${PAYLOAD_FILE}" ]]; then + echo "Error: Payload file '${PAYLOAD_FILE}' does not exist." exit 1 fi # API Endpoint definition -if [ "$STREAM_MODE" = true ]; then +if [[ "${STREAM_MODE}" = true ]]; then GENERATE_CONTENT_API="streamGenerateContent" echo "Mode: Streaming" else @@ -82,16 +83,18 @@ else echo "Mode: Non-streaming (Default)" fi -echo "Sending request to model: $MODEL_ID" -echo "Using payload from: $PAYLOAD_FILE" +echo "Sending request to model: ${MODEL_ID}" +echo "Using payload from: ${PAYLOAD_FILE}" echo "----------------------------------------" # Make the cURL request. If non-streaming, pipe through jq for readability if available. -if [ "$STREAM_MODE" = false ] && command -v jq &> /dev/null; then - curl -s -X POST \ +if [[ "${STREAM_MODE}" = false ]] && command -v jq &> /dev/null; then + # Invoke curl separately to avoid masking its return value + output=$(curl -s -X POST \ -H "Content-Type: application/json" \ "https://generativelanguage.googleapis.com/v1beta/models/${MODEL_ID}:${GENERATE_CONTENT_API}?key=${GEMINI_API_KEY}" \ - -d "@${PAYLOAD_FILE}" | jq . + -d "@${PAYLOAD_FILE}") + echo "${output}" | jq . else curl -X POST \ -H "Content-Type: application/json" \