From 12d43589be177c41ab015db84ccf2e85124566dc Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Wed, 20 Aug 2025 17:05:39 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20Enhance=20Release=20Notes=20Gene?= =?UTF-8?q?ration=20with=20Previous=20Tag=20Detection=20(#394)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add automated release notes generation with previous tag detection * chore: npm run format --- .github/workflows/release.yml | 24 +++- scripts/get-previous-tag.js | 213 ++++++++++++++++++++++++++++++++++ 2 files changed, 232 insertions(+), 5 deletions(-) create mode 100644 scripts/get-previous-tag.js diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9805735c..c78daf9c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -84,6 +84,11 @@ jobs: 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 + + # Get the previous tag for release notes generation + CURRENT_TAG=$(echo $VERSION_JSON | jq -r .releaseTag) + PREVIOUS_TAG=$(node scripts/get-previous-tag.js "$CURRENT_TAG" || echo "") + echo "PREVIOUS_TAG=${PREVIOUS_TAG}" >> $GITHUB_OUTPUT env: IS_NIGHTLY: ${{ steps.vars.outputs.is_nightly }} MANUAL_VERSION: ${{ inputs.version }} @@ -158,11 +163,20 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_BRANCH: ${{ steps.release_branch.outputs.BRANCH_NAME }} run: | - gh release create ${{ steps.version.outputs.RELEASE_TAG }} \ - bundle/gemini.js \ - --target "$RELEASE_BRANCH" \ - --title "Release ${{ steps.version.outputs.RELEASE_TAG }}" \ - --generate-notes + # Build the gh release create command with appropriate options + RELEASE_CMD="gh release create ${{ steps.version.outputs.RELEASE_TAG }} bundle/gemini.js --target \"$RELEASE_BRANCH\" --title \"Release ${{ steps.version.outputs.RELEASE_TAG }}\"" + + # Add previous tag for release notes if available + if [[ -n "${{ steps.version.outputs.PREVIOUS_TAG }}" ]]; then + echo "Generating release notes from previous tag: ${{ steps.version.outputs.PREVIOUS_TAG }}" + RELEASE_CMD="$RELEASE_CMD --generate-notes --notes-start-tag ${{ steps.version.outputs.PREVIOUS_TAG }}" + else + echo "No previous tag found, generating release notes from repository history" + RELEASE_CMD="$RELEASE_CMD --generate-notes" + fi + + # Execute the release command + eval $RELEASE_CMD - name: Create Issue on Failure if: failure() diff --git a/scripts/get-previous-tag.js b/scripts/get-previous-tag.js new file mode 100644 index 00000000..04e00d5a --- /dev/null +++ b/scripts/get-previous-tag.js @@ -0,0 +1,213 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { execSync } from 'child_process'; + +/** + * Determines the correct previous tag for release notes generation. + * This function handles the complexity of mixed tag types (regular releases vs nightly releases). + * + * @param {string} currentTag - The current release tag (e.g., "v0.1.23") + * @returns {string|null} - The previous tag to compare against, or null if no suitable tag found + */ +export function getPreviousTag(currentTag) { + try { + // Parse the current tag to understand its type + const currentTagInfo = parseTag(currentTag); + if (!currentTagInfo) { + console.error(`Invalid current tag format: ${currentTag}`); + return null; + } + + // Find the appropriate previous tag based on the current tag type + let previousTag = null; + + if (currentTagInfo.isNightly) { + // For nightly releases, find the last stable release + previousTag = findLastStableTag(currentTagInfo); + } else { + // For stable releases, find the previous stable release + previousTag = findPreviousStableTag(currentTagInfo); + } + + return previousTag; + } catch (error) { + console.error('Error getting previous tag:', error.message); + return null; + } +} + +/** + * Parses a tag string to extract version information and type + */ +function parseTag(tag) { + // Remove 'v' prefix if present + const cleanTag = tag.startsWith('v') ? tag.substring(1) : tag; + + // Match pattern: X.Y.Z or X.Y.Z-prerelease + const match = cleanTag.match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/); + if (!match) { + return null; + } + + const [, major, minor, patch, prerelease] = match; + + return { + original: tag, + major: parseInt(major), + minor: parseInt(minor), + patch: parseInt(patch), + prerelease: prerelease || null, + isNightly: prerelease && prerelease.startsWith('nightly'), + isPreview: prerelease && prerelease.startsWith('preview'), + version: `${major}.${minor}.${patch}`, + }; +} + +/** + * Finds the last stable tag for a nightly release + * Assumes version numbers are incremental and checks backwards from current version + */ +function findLastStableTag(currentTagInfo) { + // For nightly releases, find the stable version of the same version number first + const baseVersion = `v${currentTagInfo.version}`; + + // Check if the stable version of the current version exists + if (tagExists(baseVersion)) { + return baseVersion; + } + + // If not, look for the previous stable versions by decrementing version numbers + let { major, minor, patch } = currentTagInfo; + + // Try decrementing patch version first + while (patch > 0) { + patch--; + const candidateTag = `v${major}.${minor}.${patch}`; + if (tagExists(candidateTag)) { + return candidateTag; + } + } + + // Try decrementing minor version + while (minor > 0) { + minor--; + patch = 999; // Start from a high patch number and work backwards + while (patch >= 0) { + const candidateTag = `v${major}.${minor}.${patch}`; + if (tagExists(candidateTag)) { + return candidateTag; + } + patch--; + // Don't check too many patch versions to avoid infinite loops + if (patch < 0) break; + } + } + + // Try decrementing major version + while (major > 0) { + major--; + minor = 999; // Start from a high minor number and work backwards + while (minor >= 0) { + patch = 999; + while (patch >= 0) { + const candidateTag = `v${major}.${minor}.${patch}`; + if (tagExists(candidateTag)) { + return candidateTag; + } + patch--; + if (patch < 0) break; + } + minor--; + if (minor < 0) break; + } + } + + return null; +} + +/** + * Finds the previous stable tag for a stable release + * Assumes version numbers are incremental and checks backwards from current version + */ +function findPreviousStableTag(currentTagInfo) { + let { major, minor, patch } = currentTagInfo; + + // Try decrementing patch version first + while (patch > 0) { + patch--; + const candidateTag = `v${major}.${minor}.${patch}`; + if (tagExists(candidateTag)) { + return candidateTag; + } + } + + // Try decrementing minor version + while (minor > 0) { + minor--; + patch = 999; // Start from a high patch number and work backwards + while (patch >= 0) { + const candidateTag = `v${major}.${minor}.${patch}`; + if (tagExists(candidateTag)) { + return candidateTag; + } + patch--; + // Don't check too many patch versions to avoid infinite loops + if (patch < 0) break; + } + } + + // Try decrementing major version + while (major > 0) { + major--; + minor = 999; // Start from a high minor number and work backwards + while (minor >= 0) { + patch = 999; + while (patch >= 0) { + const candidateTag = `v${major}.${minor}.${patch}`; + if (tagExists(candidateTag)) { + return candidateTag; + } + patch--; + if (patch < 0) break; + } + minor--; + if (minor < 0) break; + } + } + + return null; +} + +/** + * Checks if a git tag exists + */ +function tagExists(tag) { + try { + execSync(`git rev-parse --verify ${tag}`, { stdio: 'ignore' }); + return true; + } catch { + return false; + } +} + +// CLI usage +if (process.argv[1] === new URL(import.meta.url).pathname) { + const currentTag = process.argv[2]; + + if (!currentTag) { + console.error('Usage: node get-previous-tag.js '); + process.exit(1); + } + + const previousTag = getPreviousTag(currentTag); + if (previousTag) { + console.log(previousTag); + } else { + console.error('No suitable previous tag found'); + process.exit(1); + } +}