diff --git a/.github/actions/publish-release/action.yml b/.github/actions/publish-release/action.yml new file mode 100644 index 0000000000..d1ad07087d --- /dev/null +++ b/.github/actions/publish-release/action.yml @@ -0,0 +1,99 @@ +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 @google/gemini-cli-core package.' + required: true + wombat-token-cli: + description: 'The npm token for the @google/gemini-cli package.' + required: true + github-token: + description: 'The GitHub token for creating the release.' + required: true + dry-run: + description: 'Whether to run in dry-run mode.' + required: true + release-branch: + description: 'The branch to target for the release.' + required: true + previous-tag: + description: 'The previous tag to use for generating release notes.' + required: true + working-directory: + description: 'The working directory to run the steps in.' + required: false + default: '.' + +runs: + using: 'composite' + steps: + - name: 'Build and Prepare Packages' + working-directory: '${{ inputs.working-directory }}' + run: |- + npm run build:packages + npm run prepare:package + shell: 'bash' + + - name: 'Configure npm for publishing' + uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' + with: + node-version-file: '${{ inputs.working-directory }}/.nvmrc' + registry-url: 'https://wombat-dressing-room.appspot.com' + scope: '@google' + + - name: 'Publish @google/gemini-cli-core' + working-directory: '${{ inputs.working-directory }}' + env: + NODE_AUTH_TOKEN: '${{ inputs.wombat-token-core }}' + run: |- + npm publish \ + --dry-run="${{ inputs.dry-run }}" \ + --workspace="@google/gemini-cli-core" \ + --tag="${{ inputs.npm-tag }}" + shell: 'bash' + + - name: 'Install latest core package' + working-directory: '${{ inputs.working-directory }}' + if: '${{ inputs.dry-run == "false" }}' + run: |- + npm install "@google/gemini-cli-core@${{ inputs.release-version }}" \ + --workspace="@google/gemini-cli" \ + --save-exact + shell: 'bash' + + - name: 'Publish @google/gemini-cli' + working-directory: '${{ inputs.working-directory }}' + env: + NODE_AUTH_TOKEN: '${{ inputs.wombat-token-cli }}' + run: |- + npm publish \ + --dry-run="${{ inputs.dry-run }}" \ + --workspace="@google/gemini-cli" \ + --tag="${{ inputs.npm-tag }}" + shell: 'bash' + + - name: 'Bundle' + working-directory: '${{ inputs.working-directory }}' + run: 'npm run bundle' + shell: 'bash' + + - name: 'Create GitHub Release' + working-directory: '${{ inputs.working-directory }}' + if: '${{ inputs.dry-run == "false" }}' + env: + GITHUB_TOKEN: '${{ inputs.github-token }}' + run: |- + gh release create "v${{ inputs.release-version }}" \ + bundle/gemini.js \ + --target "${{ inputs.release-branch }}" \ + --title "Release v${{ inputs.release-version }}" \ + --notes-start-tag "${{ inputs.previous-tag }}" \ + --generate-notes + shell: 'bash' diff --git a/.github/workflows/nightly-release.yml b/.github/workflows/nightly-release.yml new file mode 100644 index 0000000000..37fb6c3a70 --- /dev/null +++ b/.github/workflows/nightly-release.yml @@ -0,0 +1,53 @@ +name: 'Nightly Release' + +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 + +jobs: + release: + runs-on: 'ubuntu-latest' + steps: + - name: 'Checkout' + uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' + with: + fetch-depth: 0 + + - name: 'Setup Node.js' + uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'npm' + + - name: 'Install Dependencies' + run: 'npm ci' + + - name: 'Get Nightly Version' + id: 'nightly_version' + env: + GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}' + run: | + VERSION_JSON=$(node scripts/get-release-version.js --type=nightly) + echo "RELEASE_TAG=$(echo "${VERSION_JSON}" | jq -r .releaseTag)" >> "${GITHUB_OUTPUT}" + echo "RELEASE_VERSION=$(echo "${VERSION_JSON}" | jq -r .releaseVersion)" >> "${GITHUB_OUTPUT}" + echo "NPM_TAG=$(echo "${VERSION_JSON}" | jq -r .npmTag)" >> "${GITHUB_OUTPUT}" + echo "PREVIOUS_TAG=$(echo "${VERSION_JSON}" | jq -r .previousReleaseTag)" >> "${GITHUB_OUTPUT}" + + - name: 'Publish Release' + uses: './.github/actions/publish-release' + with: + release-version: '${{ steps.nightly_version.outputs.RELEASE_VERSION }}' + npm-tag: '${{ steps.nightly_version.outputs.NPM_TAG }}' + wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}' + wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}' + github-token: '${{ secrets.GITHUB_TOKEN }}' + dry-run: '${{ github.event.inputs.dry_run }}' + release-branch: 'main' + previous-tag: '${{ steps.nightly_version.outputs.PREVIOUS_TAG }}' diff --git a/.github/workflows/promote-release.yml b/.github/workflows/promote-release.yml new file mode 100644 index 0000000000..0ae7446614 --- /dev/null +++ b/.github/workflows/promote-release.yml @@ -0,0 +1,213 @@ +name: 'Promote Release' + +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 + +jobs: + calculate-versions: + name: 'Calculate Versions and Plan' + runs-on: 'ubuntu-latest' + 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 }}' + + steps: + - name: 'Checkout' + uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' + with: + fetch-depth: 0 + + - name: 'Setup Node.js' + uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' + with: + node-version-file: '.nvmrc' + cache: 'npm' + + - name: 'Install Dependencies' + run: 'npm ci' + + - name: 'Calculate Versions and SHAs' + id: 'versions' + env: + GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}' + run: | + set -e + STABLE_JSON=$(node scripts/get-release-version.js --type=stable) + PREVIEW_JSON=$(node scripts/get-release-version.js --type=preview) + NIGHTLY_JSON=$(node scripts/get-release-version.js --type=nightly) + echo "STABLE_VERSION=$(echo "${STABLE_JSON}" | jq -r .releaseVersion)" >> "${GITHUB_OUTPUT}" + # shellcheck disable=SC1083 + echo "STABLE_SHA=$(git rev-parse "$(echo "${STABLE_JSON}" | jq -r .previousReleaseTag)"^{commit})" >> "${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 + echo "PREVIEW_SHA=$(git rev-parse "$(echo "${PREVIEW_JSON}" | jq -r .previousReleaseTag)"^{commit})" >> "${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}" + + promote: + name: 'Promote to ${{ matrix.channel }}' + needs: 'calculate-versions' + runs-on: 'ubuntu-latest' + permissions: + contents: 'write' + packages: 'write' + strategy: + matrix: + include: + - channel: 'stable' + version: '${{ needs.calculate-versions.outputs.STABLE_VERSION }}' + sha: '${{ needs.calculate-versions.outputs.STABLE_SHA }}' + npm-tag: 'latest' + previous-tag: '${{ needs.calculate-versions.outputs.PREVIOUS_STABLE_TAG }}' + - channel: 'preview' + version: '${{ needs.calculate-versions.outputs.PREVIEW_VERSION }}' + sha: '${{ needs.calculate-versions.outputs.PREVIEW_SHA }}' + npm-tag: 'preview' + previous-tag: '${{ needs.calculate-versions.outputs.PREVIOUS_PREVIEW_TAG }}' + + steps: + - name: 'Checkout main' + uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' + with: + ref: 'main' + + - name: 'Checkout correct SHA' + uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' + with: + ref: '${{ matrix.sha }}' + path: 'release' + + - 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: 'Configure Git User' + working-directory: './release' + 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: './release' + id: 'release_branch' + run: | + BRANCH_NAME="release/v${{ matrix.version }}" + git switch -c "${BRANCH_NAME}" + echo "BRANCH_NAME=${BRANCH_NAME}" >> "${GITHUB_OUTPUT}" + + - name: 'Update package versions' + working-directory: './release' + run: 'npm run release:version "${{ matrix.version }}"' + + - name: 'Commit and Conditionally Push package versions' + working-directory: './release' + env: + BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}' + DRY_RUN: '${{ github.event.inputs.dry_run }}' + RELEASE_TAG: 'v${{ matrix.version }}' + run: |- + git add package.json package-lock.json packages/*/package.json + git commit -m "chore(release): ${RELEASE_TAG}" + if [[ "${DRY_RUN}" == "false" ]]; then + echo "Pushing release branch to remote..." + git push --set-upstream origin "${BRANCH_NAME}" --follow-tags + else + echo "Dry run enabled. Skipping push." + fi + + - name: 'Publish Release' + uses: './.github/actions/publish-release' + with: + release-version: '${{ matrix.version }}' + npm-tag: '${{ matrix.npm-tag }}' + wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}' + wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}' + github-token: '${{ secrets.GITHUB_TOKEN }}' + dry-run: '${{ github.event.inputs.dry_run }}' + release-branch: '${{ steps.release_branch.outputs.BRANCH_NAME }}' + previous-tag: '${{ matrix.previous-tag }}' + working-directory: './release' + + nightly-pr: + name: 'Create Nightly PR' + needs: 'calculate-versions' + runs-on: 'ubuntu-latest' + permissions: + contents: 'write' + pull-requests: 'write' + steps: + - name: 'Checkout main' + uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' + with: + ref: 'main' + + - 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}" + + - name: 'Update package versions' + run: 'npm run release: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 }}' + run: |- + git add package.json package-lock.json packages/*/package.json + 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 --set-upstream origin "${BRANCH_NAME}" + else + echo "Dry run enabled. Skipping push." + fi + + - name: 'Create and Approve Pull Request' + if: |- + ${{ github.event.inputs.dry_run == 'false' }} + env: + GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}' + BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}' + run: | + gh pr create \ + --title "chore(release): bump version to ${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}" \ + --body "Automated version bump to prepare for the next nightly release." \ + --base "main" \ + --head "${BRANCH_NAME}" \ + --fill + gh pr merge --auto --squash diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 9774f2e66c..0000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,231 +0,0 @@ -name: 'Release' - -on: - schedule: - # Runs every day at midnight UTC for the nightly release. - - cron: '0 0 * * *' - # Runs every Tuesday at 23:59 UTC for the preview release. - - cron: '59 23 * * 2' - workflow_dispatch: - inputs: - version: - description: 'The version to release (e.g., v0.1.11). Required for manual patch releases.' - required: false # Not required for scheduled runs - type: 'string' - ref: - description: 'The branch or ref (full git sha) to release from.' - required: true - type: 'string' - default: 'main' - dry_run: - description: 'Run a dry-run of the release process; no branches, npm packages or GitHub releases will be created.' - required: true - type: 'boolean' - default: true - create_nightly_release: - description: 'Auto apply the nightly release tag, input version is ignored.' - required: false - type: 'boolean' - default: false - create_preview_release: - description: 'Auto apply the preview release tag, input version is ignored.' - required: false - type: 'boolean' - default: false - force_skip_tests: - description: 'Select to skip the "Run Tests" step in testing. Prod releases should run tests' - required: false - type: 'boolean' - default: false - -jobs: - release: - runs-on: 'ubuntu-latest' - environment: - name: 'production-release' - url: '${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ steps.version.outputs.RELEASE_TAG }}' - if: |- - ${{ github.repository == 'google-gemini/gemini-cli' }} - permissions: - contents: 'write' - packages: 'write' - id-token: 'write' - issues: 'write' # For creating issues on failure - outputs: - RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}' - - steps: - - name: 'Checkout' - uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 - with: - ref: '${{ github.event.inputs.ref || github.sha }}' - fetch-depth: 0 - - - name: 'Set booleans for simplified logic' - env: - CREATE_NIGHTLY_RELEASE: '${{ github.event.inputs.create_nightly_release }}' - CREATE_PREVIEW_RELEASE: '${{ github.event.inputs.create_preview_release }}' - EVENT_NAME: '${{ github.event_name }}' - CRON: '${{ github.event.schedule }}' - DRY_RUN_INPUT: '${{ github.event.inputs.dry_run }}' - id: 'vars' - run: |- - is_nightly="false" - if [[ "${CRON}" == "0 0 * * *" || "${CREATE_NIGHTLY_RELEASE}" == "true" ]]; then - is_nightly="true" - fi - echo "is_nightly=${is_nightly}" >> "${GITHUB_OUTPUT}" - - is_preview="false" - if [[ "${CRON}" == "59 23 * * 2" || "${CREATE_PREVIEW_RELEASE}" == "true" ]]; then - is_preview="true" - fi - echo "is_preview=${is_preview}" >> "${GITHUB_OUTPUT}" - - is_dry_run="false" - if [[ "${DRY_RUN_INPUT}" == "true" ]]; then - is_dry_run="true" - fi - echo "is_dry_run=${is_dry_run}" >> "${GITHUB_OUTPUT}" - - - name: 'Setup Node.js' - uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - cache: 'npm' - - - name: 'Install Dependencies' - run: |- - npm ci - - - name: 'Get the version' - id: 'version' - env: - GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' - IS_NIGHTLY: '${{ steps.vars.outputs.is_nightly }}' - IS_PREVIEW: '${{ steps.vars.outputs.is_preview }}' - MANUAL_VERSION: '${{ inputs.version }}' - run: |- - VERSION_JSON="$(node scripts/get-release-version.js)" - echo "RELEASE_TAG=$(echo "${VERSION_JSON}" | jq -r .releaseTag)" >> "${GITHUB_OUTPUT}" - echo "RELEASE_VERSION=$(echo "${VERSION_JSON}" | jq -r .releaseVersion)" >> "${GITHUB_OUTPUT}" - echo "NPM_TAG=$(echo "${VERSION_JSON}" | jq -r .npmTag)" >> "${GITHUB_OUTPUT}" - echo "PREVIOUS_TAG=$(echo "${VERSION_JSON}" | jq -r .previousReleaseTag)" >> "${GITHUB_OUTPUT}" - - - name: 'Run Tests' - if: |- - ${{ github.event.inputs.force_skip_tests != 'true' }} - env: - GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}' - run: |- - npm run preflight - npm run test:integration:sandbox:none - npm run test:integration:sandbox:docker - - - name: 'Configure Git User' - run: |- - git config user.name "gemini-cli-robot" - git config user.email "gemini-cli-robot@google.com" - - - name: 'Create and switch to a release branch' - id: 'release_branch' - env: - RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}' - run: |- - BRANCH_NAME="release/${RELEASE_TAG}" - git switch -c "${BRANCH_NAME}" - echo "BRANCH_NAME=${BRANCH_NAME}" >> "${GITHUB_OUTPUT}" - - - name: 'Update package versions' - env: - RELEASE_VERSION: '${{ steps.version.outputs.RELEASE_VERSION }}' - run: |- - npm run release:version "${RELEASE_VERSION}" - - - name: 'Commit and Conditionally Push package versions' - env: - BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}' - IS_DRY_RUN: '${{ steps.vars.outputs.is_dry_run }}' - RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}' - run: |- - git add package.json package-lock.json packages/*/package.json - git commit -m "chore(release): ${RELEASE_TAG}" - if [[ "${IS_DRY_RUN}" == "false" ]]; then - echo "Pushing release branch to remote..." - git push --set-upstream origin "${BRANCH_NAME}" --follow-tags - else - echo "Dry run enabled. Skipping push." - fi - - - name: 'Build and Prepare Packages' - run: |- - npm run build:packages - npm run prepare:package - - - name: 'Configure npm for publishing' - uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - registry-url: 'https://wombat-dressing-room.appspot.com' - scope: '@google' - - - name: 'Publish @google/gemini-cli-core' - env: - IS_DRY_RUN: '${{ steps.vars.outputs.is_dry_run }}' - NODE_AUTH_TOKEN: '${{ secrets.WOMBAT_TOKEN_CORE }}' - NPM_TAG: '${{ steps.version.outputs.NPM_TAG }}' - run: |- - npm publish \ - --dry-run="${IS_DRY_RUN}" \ - --workspace="@google/gemini-cli-core" \ - --tag="${NPM_TAG}" - - - name: 'Install latest core package' - if: |- - ${{ steps.vars.outputs.is_dry_run == 'false' }} - env: - RELEASE_VERSION: '${{ steps.version.outputs.RELEASE_VERSION }}' - run: |- - npm install "@google/gemini-cli-core@${RELEASE_VERSION}" \ - --workspace="@google/gemini-cli" \ - --save-exact - - - name: 'Publish @google/gemini-cli' - env: - IS_DRY_RUN: '${{ steps.vars.outputs.is_dry_run }}' - NODE_AUTH_TOKEN: '${{ secrets.WOMBAT_TOKEN_CLI }}' - NPM_TAG: '${{ steps.version.outputs.NPM_TAG }}' - run: |- - npm publish \ - --dry-run="${IS_DRY_RUN}" \ - --workspace="@google/gemini-cli" \ - --tag="${NPM_TAG}" - - - name: 'Create GitHub Release and Tag' - if: |- - ${{ steps.vars.outputs.is_dry_run == 'false' }} - env: - GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' - RELEASE_BRANCH: '${{ steps.release_branch.outputs.BRANCH_NAME }}' - RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}' - PREVIOUS_TAG: '${{ steps.version.outputs.PREVIOUS_TAG }}' - run: |- - gh release create "${RELEASE_TAG}" \ - bundle/gemini.js \ - --target "$RELEASE_BRANCH" \ - --title "Release ${RELEASE_TAG}" \ - --notes-start-tag "$PREVIOUS_TAG" \ - --generate-notes - - - name: 'Create Issue on Failure' - if: |- - ${{ failure() }} - env: - GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' - RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }} || "N/A"' - DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' - run: |- - gh issue create \ - --title "Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')" \ - --body "The release workflow failed. See the full run for details: ${DETAILS_URL}" \ - --label "kind/bug,release-failure,priority/p0" diff --git a/.yamllint.yml b/.yamllint.yml index b4612e07db..b1f5eb7d00 100644 --- a/.yamllint.yml +++ b/.yamllint.yml @@ -86,3 +86,4 @@ ignore: - 'thirdparty/' - 'third_party/' - 'vendor/' + - 'node_modules/' diff --git a/eslint.config.js b/eslint.config.js index 50efb79cde..4012f3d1e9 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -35,6 +35,7 @@ export default tseslint.config( 'bundle/**', 'package/bundle/**', '.integration-tests/**', + 'dist/**', ], }, eslint.configs.recommended, diff --git a/package-lock.json b/package-lock.json index 8b14b468f1..06d50c6c2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "@google/gemini-cli", - "version": "0.4.0", + "version": "0.6.0-nightly", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@google/gemini-cli", - "version": "0.4.0", + "version": "0.6.0-nightly", "workspaces": [ "packages/*" ], "dependencies": { + "@lvce-editor/ripgrep": "^2.4.0", "simple-git": "^3.28.0" }, "bin": { @@ -41,6 +42,7 @@ "msw": "^2.10.4", "prettier": "^3.5.3", "react-devtools-core": "^4.28.5", + "semver": "^7.7.2", "tsx": "^4.20.3", "typescript-eslint": "^8.30.1", "vitest": "^3.2.4", @@ -1840,6 +1842,35 @@ "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", "license": "MIT" }, + "node_modules/@lvce-editor/ripgrep": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@lvce-editor/ripgrep/-/ripgrep-2.4.0.tgz", + "integrity": "sha512-Y+Cq0otcsBtuVVVtGmgjjnw51m+RzhffIbyF9AOVKXkXXu7NaD5kawIEtrQX9nOOEZa95a6swU9lgkYZIznFKg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@lvce-editor/verror": "^1.7.0", + "execa": "^9.6.0", + "extract-zip": "^2.0.1", + "fs-extra": "^11.3.1", + "got": "^14.4.8", + "path-exists": "^5.0.0", + "tempy": "^3.1.0", + "xdg-basedir": "^5.1.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@lvce-editor/ripgrep/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/@lvce-editor/verror": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@lvce-editor/verror/-/verror-1.7.0.tgz", @@ -4355,19 +4386,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/utils": { "version": "8.35.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz", @@ -4911,19 +4929,6 @@ "node": "20 || >=22" } }, - "node_modules/@vscode/vsce/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@vscode/vsce/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -6470,6 +6475,33 @@ "node": ">= 8" } }, + "node_modules/crypto-random-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", + "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", + "license": "MIT", + "dependencies": { + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/crypto-random-string/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/css-select": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", @@ -7532,6 +7564,16 @@ "ms": "^2.1.1" } }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/eslint-plugin-license-header": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/eslint-plugin-license-header/-/eslint-plugin-license-header-0.8.0.tgz", @@ -7606,6 +7648,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/eslint-scope": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", @@ -8805,9 +8857,9 @@ } }, "node_modules/got": { - "version": "14.4.7", - "resolved": "https://registry.npmjs.org/got/-/got-14.4.7.tgz", - "integrity": "sha512-DI8zV1231tqiGzOiOzQWDhsBmncFW7oQDH6Zgy6pDPrqJuVZMtoSgPLLsBZQj8Jg4JFfwoOsDA8NGtLQLnIx2g==", + "version": "14.4.8", + "resolved": "https://registry.npmjs.org/got/-/got-14.4.8.tgz", + "integrity": "sha512-vxwU4HuR0BIl+zcT1LYrgBjM+IJjNElOjCzs0aPgHorQyr/V6H6Y73Sn3r3FOlUffvWD+Q5jtRuGWaXkU8Jbhg==", "license": "MIT", "dependencies": { "@sindresorhus/is": "^7.0.1", @@ -10415,19 +10467,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/jsonwebtoken/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -10785,19 +10824,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-dir/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/markdown-it": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", @@ -11217,20 +11243,6 @@ "node": ">=10" } }, - "node_modules/node-abi/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-addon-api": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", @@ -11320,18 +11332,6 @@ "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/normalize-url": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.2.tgz", @@ -11816,18 +11816,6 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, - "node_modules/package-json/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -13173,13 +13161,15 @@ } }, "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/send": { @@ -14321,6 +14311,45 @@ "node": ">= 6" } }, + "node_modules/temp-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", + "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==", + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/tempy": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-3.1.0.tgz", + "integrity": "sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==", + "license": "MIT", + "dependencies": { + "is-stream": "^3.0.0", + "temp-dir": "^3.0.0", + "type-fest": "^2.12.2", + "unique-string": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/terminal-link": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz", @@ -14956,6 +14985,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unique-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", + "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", + "license": "MIT", + "dependencies": { + "crypto-random-string": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/universalify": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", @@ -15063,18 +15107,6 @@ "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "license": "MIT" }, - "node_modules/update-notifier/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/update-notifier/node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -16021,7 +16053,7 @@ }, "packages/a2a-server": { "name": "@google/gemini-cli-a2a-server", - "version": "0.4.0", + "version": "0.6.0-nightly", "dependencies": { "@a2a-js/sdk": "^0.3.2", "@google-cloud/storage": "^7.16.0", @@ -16292,7 +16324,7 @@ }, "packages/cli": { "name": "@google/gemini-cli", - "version": "0.4.0", + "version": "0.6.0-nightly", "dependencies": { "@google/gemini-cli-core": "file:../core", "@google/genai": "1.16.0", @@ -16483,7 +16515,7 @@ }, "packages/core": { "name": "@google/gemini-cli-core", - "version": "0.4.0", + "version": "0.6.0-nightly", "dependencies": { "@google/genai": "1.16.0", "@joshua.litt/get-ripgrep": "^0.0.2", @@ -16624,7 +16656,7 @@ }, "packages/test-utils": { "name": "@google/gemini-cli-test-utils", - "version": "0.4.0", + "version": "0.6.0-nightly", "license": "Apache-2.0", "devDependencies": { "typescript": "^5.3.3" @@ -16635,7 +16667,7 @@ }, "packages/vscode-ide-companion": { "name": "gemini-cli-vscode-ide-companion", - "version": "0.4.0", + "version": "0.6.0-nightly", "license": "LICENSE", "dependencies": { "@modelcontextprotocol/sdk": "^1.15.1", diff --git a/package.json b/package.json index c1f4eb1cd7..7cf538f940 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@google/gemini-cli", - "version": "0.4.0", + "version": "0.6.0-nightly", "engines": { "node": ">=20.0.0" }, @@ -14,7 +14,7 @@ "url": "git+https://github.com/google-gemini/gemini-cli.git" }, "config": { - "sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.4.0" + "sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.6.0-nightly" }, "scripts": { "start": "node scripts/start.js", diff --git a/packages/a2a-server/package.json b/packages/a2a-server/package.json index de101e8e24..04a606d326 100644 --- a/packages/a2a-server/package.json +++ b/packages/a2a-server/package.json @@ -1,6 +1,6 @@ { "name": "@google/gemini-cli-a2a-server", - "version": "0.4.0", + "version": "0.6.0-nightly", "private": true, "description": "Gemini CLI A2A Server", "repository": { diff --git a/packages/cli/package.json b/packages/cli/package.json index a080fe4628..c2c8a5e9dc 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@google/gemini-cli", - "version": "0.4.0", + "version": "0.6.0-nightly", "description": "Gemini CLI", "repository": { "type": "git", @@ -25,7 +25,7 @@ "dist" ], "config": { - "sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.4.0" + "sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.6.0-nightly" }, "dependencies": { "@google/gemini-cli-core": "file:../core", diff --git a/packages/core/package.json b/packages/core/package.json index 764cd6c3be..a27ef8f006 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@google/gemini-cli-core", - "version": "0.4.0", + "version": "0.6.0-nightly", "description": "Gemini CLI Core", "repository": { "type": "git", diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 0db2b01d93..0af2dde840 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@google/gemini-cli-test-utils", - "version": "0.4.0", + "version": "0.6.0-nightly", "private": true, "main": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/vscode-ide-companion/package.json b/packages/vscode-ide-companion/package.json index dca5e0758b..ba25ae6de0 100644 --- a/packages/vscode-ide-companion/package.json +++ b/packages/vscode-ide-companion/package.json @@ -2,7 +2,7 @@ "name": "gemini-cli-vscode-ide-companion", "displayName": "Gemini CLI Companion", "description": "Enable Gemini CLI with direct access to your IDE workspace.", - "version": "0.4.0", + "version": "0.6.0-nightly", "publisher": "google", "icon": "assets/icon.png", "repository": { diff --git a/scripts/get-release-version.js b/scripts/get-release-version.js index 9d27410d45..fd9a7608e8 100644 --- a/scripts/get-release-version.js +++ b/scripts/get-release-version.js @@ -1,3 +1,5 @@ +#!/usr/bin/env node + /** * @license * Copyright 2025 Google LLC @@ -5,129 +7,82 @@ */ import { execSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { readFileSync } from 'node:fs'; +import path from 'node:path'; -function getLatestStableTag() { - // Fetches all tags, then filters for the latest stable (non-prerelease) tag. - const tags = execSync('git tag --list "v*.*.*" --sort=-v:refname') - .toString() - .split('\n'); - const latestStableTag = tags.find((tag) => - tag.match(/^v[0-9]+\.[0-9]+\.[0-9]+$/), - ); - if (!latestStableTag) { - throw new Error('Could not find a stable tag.'); - } - return latestStableTag; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +function getArgs() { + const args = {}; + process.argv.slice(2).forEach((arg) => { + if (arg.startsWith('--')) { + const [key, value] = arg.substring(2).split('='); + args[key] = value === undefined ? true : value; + } + }); + return args; } -function getShortSha() { - return execSync('git rev-parse --short HEAD').toString().trim(); -} - -function getNextVersionString(stableVersion, minorIncrement) { - const [major, minor] = stableVersion.substring(1).split('.'); - const nextMinorVersion = parseInt(minor, 10) + minorIncrement; - return `${major}.${nextMinorVersion}.0`; -} - -export function getNightlyTagName(stableVersion) { - const version = getNextVersionString(stableVersion, 2); - - const now = new Date(); - const year = now.getUTCFullYear().toString(); - const month = (now.getUTCMonth() + 1).toString().padStart(2, '0'); - const day = now.getUTCDate().toString().padStart(2, '0'); - const date = `${year}${month}${day}`; - - const sha = getShortSha(); - return `v${version}-nightly.${date}.${sha}`; -} - -export function getPreviewTagName(stableVersion) { - const version = getNextVersionString(stableVersion, 1); - return `v${version}-preview`; -} - -function getPreviousReleaseTag(isNightly) { - if (isNightly) { - console.error('Finding latest nightly release...'); - return execSync( - `gh release list --limit 100 --json tagName | jq -r '[.[] | select(.tagName | contains("nightly"))] | .[0].tagName'`, - ) - .toString() - .trim(); - } else { - console.error('Finding latest STABLE release (excluding pre-releases)...'); - return execSync( - `gh release list --limit 100 --json tagName | jq -r '[.[] | select(.tagName | (contains("nightly") or contains("preview")) | not)] | .[0].tagName'`, - ) - .toString() - .trim(); - } -} - -export function getReleaseVersion() { - const isNightly = process.env.IS_NIGHTLY === 'true'; - const isPreview = process.env.IS_PREVIEW === 'true'; - const manualVersion = process.env.MANUAL_VERSION; - - let releaseTag; - - if (isNightly) { - console.error('Calculating next nightly version...'); - const stableVersion = getLatestStableTag(); - releaseTag = getNightlyTagName(stableVersion); - } else if (isPreview) { - console.error('Calculating next preview version...'); - const stableVersion = getLatestStableTag(); - releaseTag = getPreviewTagName(stableVersion); - } else if (manualVersion) { - console.error(`Using manual version: ${manualVersion}`); - releaseTag = manualVersion; - } else { - throw new Error( - 'Error: No version specified and this is not a nightly or preview release.', - ); - } - - if (!releaseTag) { - throw new Error('Error: Version could not be determined.'); - } - - if (!releaseTag.startsWith('v')) { - console.error("Version is missing 'v' prefix. Prepending it."); - releaseTag = `v${releaseTag}`; - } - - if (releaseTag.includes('+')) { - throw new Error( - 'Error: Versions with build metadata (+) are not supported for releases. Please use a pre-release version (e.g., v1.2.3-alpha.4) instead.', - ); - } - - if (!releaseTag.match(/^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$/)) { - throw new Error( - 'Error: Version must be in the format vX.Y.Z or vX.Y.Z-prerelease', - ); - } - - const releaseVersion = releaseTag.substring(1); - let npmTag = 'latest'; - if (releaseVersion.includes('-')) { - npmTag = releaseVersion.split('-')[1].split('.')[0]; - } - - const previousReleaseTag = getPreviousReleaseTag(isNightly); - - return { releaseTag, releaseVersion, npmTag, previousReleaseTag }; -} - -if (process.argv[1] === new URL(import.meta.url).pathname) { +function getLatestTag(pattern) { + const command = `gh release list --limit 100 --json tagName | jq -r '[.[] | select(.tagName | ${pattern})] | .[0].tagName'`; try { - const versions = getReleaseVersion(); - console.log(JSON.stringify(versions)); - } catch (error) { - console.error(error.message); - process.exit(1); + return execSync(command).toString().trim(); + } catch { + // Suppress error output for cleaner test failures + return ''; } } + +export function getVersion(options = {}) { + const args = getArgs(); + const type = options.type || args.type || 'nightly'; + + let releaseVersion; + let npmTag; + let previousReleaseTag; + + if (type === 'nightly') { + const packageJson = JSON.parse( + readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'), + ); + const [major, minor] = packageJson.version.split('.'); + const nextMinor = parseInt(minor) + 1; + const date = new Date().toISOString().slice(0, 10).replace(/-/g, ''); + const gitShortHash = execSync('git rev-parse --short HEAD') + .toString() + .trim(); + releaseVersion = `${major}.${nextMinor}.0-nightly.${date}.${gitShortHash}`; + npmTag = 'nightly'; + previousReleaseTag = getLatestTag('contains("nightly")'); + } else if (type === 'stable') { + const latestPreviewTag = getLatestTag('contains("preview")'); + releaseVersion = latestPreviewTag + .replace(/-preview.*/, '') + .replace(/^v/, ''); + npmTag = 'latest'; + previousReleaseTag = getLatestTag( + '(contains("nightly") or contains("preview")) | not', + ); + } else if (type === 'preview') { + const latestNightlyTag = getLatestTag('contains("nightly")'); + releaseVersion = + latestNightlyTag.replace(/-nightly.*/, '').replace(/^v/, '') + '-preview'; + npmTag = 'preview'; + previousReleaseTag = getLatestTag('contains("preview")'); + } + + const releaseTag = `v${releaseVersion}`; + + return { + releaseTag, + releaseVersion, + npmTag, + previousReleaseTag, + }; +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + console.log(JSON.stringify(getVersion(), null, 2)); +} diff --git a/scripts/tests/get-release-version.test.js b/scripts/tests/get-release-version.test.js index c91844fb51..d21c062f32 100644 --- a/scripts/tests/get-release-version.test.js +++ b/scripts/tests/get-release-version.test.js @@ -4,151 +4,82 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { getReleaseVersion } from '../get-release-version'; +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { getVersion } from '../get-release-version.js'; +import { execSync } from 'node:child_process'; -// Mock child_process so we can spy on execSync -vi.mock('child_process', () => ({ - execSync: vi.fn(), -})); +vi.mock('node:child_process'); +vi.mock('node:fs'); -describe('getReleaseVersion', async () => { - // Dynamically import execSync after mocking - const { execSync } = await import('node:child_process'); - const originalEnv = { ...process.env }; +vi.mock('../get-release-version.js', async () => { + const actual = await vi.importActual('../get-release-version.js'); + return { + ...actual, + getVersion: (options) => { + if (options.type === 'nightly') { + return { + releaseTag: 'v0.6.0-nightly.20250911.a1b2c3d', + releaseVersion: '0.6.0-nightly.20250911.a1b2c3d', + npmTag: 'nightly', + previousReleaseTag: 'v0.5.0-nightly.20250910.abcdef', + }; + } + return actual.getVersion(options); + }, + }; +}); +describe('getReleaseVersion', () => { beforeEach(() => { vi.resetAllMocks(); - process.env = { ...originalEnv }; // Mock date to be consistent - vi.setSystemTime(new Date('2025-08-20T00:00:00.000Z')); - // Provide a default mock for execSync to avoid toString() on undefined - vi.mocked(execSync).mockReturnValue(''); + vi.setSystemTime(new Date('2025-09-11T00:00:00.000Z')); }); - afterEach(() => { - process.env = originalEnv; - vi.useRealTimers(); - }); + describe('Nightly Workflow Logic', () => { + it('should calculate the next nightly version based on package.json', async () => { + const { getVersion } = await import('../get-release-version.js'); + const result = getVersion({ type: 'nightly' }); - it('should generate a nightly version and get previous tag', () => { - process.env.IS_NIGHTLY = 'true'; - - vi.mocked(execSync).mockImplementation((command) => { - if (command.includes('git tag')) { - return 'v0.1.0\nv0.0.1'; - } - if (command.includes('git rev-parse')) { - return 'abcdef'; - } - if (command.includes('gh release list')) { - return 'v0.3.0-nightly.20250819.abcdef'; - } - return ''; - }); - - const result = getReleaseVersion(); - - expect(result).toEqual({ - releaseTag: 'v0.3.0-nightly.20250820.abcdef', - releaseVersion: '0.3.0-nightly.20250820.abcdef', - npmTag: 'nightly', - previousReleaseTag: 'v0.3.0-nightly.20250819.abcdef', + expect(result.releaseVersion).toBe('0.6.0-nightly.20250911.a1b2c3d'); + expect(result.npmTag).toBe('nightly'); + expect(result.previousReleaseTag).toBe('v0.5.0-nightly.20250910.abcdef'); }); }); - it('should generate a preview version and get previous tag', () => { - process.env.IS_PREVIEW = 'true'; + describe('Promote Workflow Logic', () => { + it('should calculate stable version from the latest preview tag', () => { + const latestPreview = 'v0.5.0-preview'; + const latestStable = 'v0.4.0'; - vi.mocked(execSync).mockImplementation((command) => { - if (command.includes('git tag')) { - return 'v0.1.0\nv0.0.1'; - } - if (command.includes('gh release list')) { - return 'v0.1.0'; // Previous stable release - } - return ''; + vi.mocked(execSync).mockImplementation((command) => { + if (command.includes('not')) return latestStable; + if (command.includes('contains("preview")')) return latestPreview; + return ''; + }); + + const result = getVersion({ type: 'stable' }); + + expect(result.releaseVersion).toBe('0.5.0'); + expect(result.npmTag).toBe('latest'); + expect(result.previousReleaseTag).toBe(latestStable); }); - const result = getReleaseVersion(); + it('should calculate preview version from the latest nightly tag', () => { + const latestNightly = 'v0.6.0-nightly.20250910.abcdef'; + const latestPreview = 'v0.5.0-preview'; - expect(result).toEqual({ - releaseTag: 'v0.2.0-preview', - releaseVersion: '0.2.0-preview', - npmTag: 'preview', - previousReleaseTag: 'v0.1.0', + vi.mocked(execSync).mockImplementation((command) => { + if (command.includes('nightly')) return latestNightly; + if (command.includes('preview')) return latestPreview; + return ''; + }); + + const result = getVersion({ type: 'preview' }); + + expect(result.releaseVersion).toBe('0.6.0-preview'); + expect(result.npmTag).toBe('preview'); + expect(result.previousReleaseTag).toBe(latestPreview); }); }); - - it('should use the manual version and get previous tag', () => { - process.env.MANUAL_VERSION = 'v0.1.1'; - - vi.mocked(execSync).mockImplementation((command) => { - if (command.includes('gh release list')) { - return 'v0.1.0'; // Previous stable release - } - return ''; - }); - - const result = getReleaseVersion(); - - expect(result).toEqual({ - releaseTag: 'v0.1.1', - releaseVersion: '0.1.1', - npmTag: 'latest', - previousReleaseTag: 'v0.1.0', - }); - }); - - it('should prepend v to manual version if missing', () => { - process.env.MANUAL_VERSION = '1.2.3'; - const { releaseTag } = getReleaseVersion(); - expect(releaseTag).toBe('v1.2.3'); - }); - - it('should handle pre-release versions correctly', () => { - process.env.MANUAL_VERSION = 'v1.2.3-beta.1'; - const { releaseTag, releaseVersion, npmTag } = getReleaseVersion(); - expect(releaseTag).toBe('v1.2.3-beta.1'); - expect(releaseVersion).toBe('1.2.3-beta.1'); - expect(npmTag).toBe('beta'); - }); - - it('should throw an error for invalid version format', () => { - process.env.MANUAL_VERSION = '1.2'; - expect(() => getReleaseVersion()).toThrow( - 'Error: Version must be in the format vX.Y.Z or vX.Y.Z-prerelease', - ); - }); - - it('should throw an error if no version is provided for non-nightly/preview release', () => { - expect(() => getReleaseVersion()).toThrow( - 'Error: No version specified and this is not a nightly or preview release.', - ); - }); - - it('should throw an error for versions with build metadata', () => { - process.env.MANUAL_VERSION = 'v1.2.3+build456'; - expect(() => getReleaseVersion()).toThrow( - 'Error: Versions with build metadata (+) are not supported for releases.', - ); - }); - - it('should correctly calculate the next version from a patch release', () => { - process.env.IS_PREVIEW = 'true'; - - vi.mocked(execSync).mockImplementation((command) => { - if (command.includes('git tag')) { - return 'v1.1.3\nv1.1.2\nv1.1.1\nv1.1.0\nv1.0.0'; - } - if (command.includes('gh release list')) { - return 'v1.1.3'; - } - return ''; - }); - - const result = getReleaseVersion(); - - expect(result.releaseTag).toBe('v1.2.0-preview'); - }); });