-
Notifications
You must be signed in to change notification settings - Fork 45
feat: introduce CHANGELOG.md mechanism and release pipeline integration #182
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- Create CHANGELOG.md with history from git (v2.3.10 - v2.3.14) - Add fastlane/changelog_helper.rb for changelog extraction and validation - Modify release.yml to validate changelog exists before release - Update release.yml to generate GitHub Release notes from CHANGELOG.md - Update release.yml to generate Google Play whatsnew from CHANGELOG.md - Add validate_changelog and version_info lanes to Fastfile - Add changelog validation to deploy_beta and deploy_production lanes This aligns with the iOS version's changelog workflow where: - Developers manually maintain CHANGELOG.md with version entries - Release will fail if no changelog entry exists for current version - Release notes are automatically extracted from CHANGELOG.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR introduces a CHANGELOG.md-based release notes mechanism to replace the previous git commit-based approach. The implementation adds changelog validation to the release pipeline, ensuring that every release has a corresponding changelog entry before proceeding.
Key changes:
- Created
CHANGELOG.mdwith historical version entries (v2.3.10 - v2.3.14) in a structured format - Added Ruby helper module for changelog parsing and validation with locale-specific formatting
- Modified release workflow to validate changelog presence and extract release notes from CHANGELOG.md
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 15 comments.
| File | Description |
|---|---|
CHANGELOG.md |
New changelog file documenting version history with Feature/Fix/Improvement categorization |
fastlane/changelog_helper.rb |
Ruby module for extracting and formatting changelog entries for GitHub and Google Play |
fastlane/Fastfile |
Added validate_changelog and version_info lanes, integrated validation into deploy lanes |
.github/workflows/release.yml |
Replaced git-based changelog generation with CHANGELOG.md extraction, added validation step |
| feedback_header = "Feedback: https://v2er.app/help\n\n" | ||
| feedback_header_zh = "唯一问题反馈渠道:https://v2er.app/help\n\n" | ||
|
|
||
| # Reserve space for feedback header | ||
| available_space = GOOGLE_PLAY_LIMIT - feedback_header.length |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The feedback header includes a newline at the end (\n\n), but when calculating available_space on line 184, only the length of the string literal is used. The literal string "Feedback: https://v2er.app/help\n\n" counts \n as two characters (backslash + n), not as actual newlines. This means the actual length when used will differ from the calculated length, potentially causing the content to exceed the GOOGLE_PLAY_LIMIT. Consider using actual newlines in the string or using .length on the actual string that will be concatenated.
.github/workflows/release.yml
Outdated
| current_len=${#content} | ||
| # Feedback header | ||
| FEEDBACK_HEADER_EN="Feedback: https://v2er.app/help" | ||
| FEEDBACK_HEADER_ZH="唯一问题反馈渠道:https://v2er.app/help" | ||
| # Build release notes for each version | ||
| declare -a version_notes_en | ||
| declare -a version_notes_zh | ||
| # Current version (HEAD to last tag or all commits if no tags) | ||
| declare -A curr_features curr_bugs curr_improvements curr_performance | ||
| if [ ${#TAGS[@]} -gt 0 ]; then | ||
| collect_version_commits "${TAGS[0]}..HEAD" curr_features curr_bugs curr_improvements curr_performance | ||
| else | ||
| collect_version_commits "HEAD~10..HEAD" curr_features curr_bugs curr_improvements curr_performance | ||
| fi | ||
| CURR_NOTES=$(generate_version_notes curr_features curr_bugs curr_improvements curr_performance) | ||
| if [ -n "$CURR_NOTES" ]; then | ||
| version_notes_en+=("$CURR_NOTES") | ||
| version_notes_zh+=("$CURR_NOTES") | ||
| fi | ||
| # Add changelog for each version | ||
| for i in "${!VERSIONS[@]}"; do | ||
| version="${VERSIONS[$i]}" | ||
| raw_changelog=$(extract_version_changelog "$version") | ||
| formatted=$(format_for_google_play "$raw_changelog") | ||
|
|
||
| # Previous versions (from tags) | ||
| for i in 0 1; do | ||
| if [ $i -lt ${#TAGS[@]} ] && [ $((i+1)) -lt ${#TAGS[@]} ]; then | ||
| declare -A prev_features prev_bugs prev_improvements prev_performance | ||
| collect_version_commits "${TAGS[$((i+1))]}..${TAGS[$i]}" prev_features prev_bugs prev_improvements prev_performance | ||
| PREV_NOTES=$(generate_version_notes prev_features prev_bugs prev_improvements prev_performance) | ||
| if [ -n "$PREV_NOTES" ]; then | ||
| version_notes_en+=("--- ${TAGS[$i]} ---"$'\n'"$PREV_NOTES") | ||
| version_notes_zh+=("--- ${TAGS[$i]} ---"$'\n'"$PREV_NOTES") | ||
| if [ -z "$formatted" ]; then | ||
| continue | ||
| fi | ||
| unset prev_features prev_bugs prev_improvements prev_performance | ||
| elif [ $i -lt ${#TAGS[@]} ] && [ $((i+1)) -ge ${#TAGS[@]} ]; then | ||
| # Last tag - get commits before it | ||
| declare -A prev_features prev_bugs prev_improvements prev_performance | ||
| collect_version_commits "${TAGS[$i]}~5..${TAGS[$i]}" prev_features prev_bugs prev_improvements prev_performance | ||
| PREV_NOTES=$(generate_version_notes prev_features prev_bugs prev_improvements prev_performance) | ||
| if [ -n "$PREV_NOTES" ]; then | ||
| version_notes_en+=("--- ${TAGS[$i]} ---"$'\n'"$PREV_NOTES") | ||
| version_notes_zh+=("--- ${TAGS[$i]} ---"$'\n'"$PREV_NOTES") | ||
|
|
||
| if [ "$i" -eq 0 ]; then | ||
| version_section="$formatted" | ||
| else | ||
| version_section=" | ||
| --- v${version} --- | ||
| ${formatted}" | ||
| fi | ||
| unset prev_features prev_bugs prev_improvements prev_performance | ||
| fi | ||
| done | ||
|
|
||
| # Generate English release notes with character limit | ||
| { | ||
| echo "$FEEDBACK_HEADER_EN" | ||
| echo "" | ||
| echo "V2er $CURRENT_VERSION" | ||
| echo "" | ||
| } > whatsnew/whatsnew-en-US | ||
| section_len=${#version_section} | ||
| if [ $((current_len + section_len)) -lt "$max_chars" ]; then |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The character length calculation uses ${#content} which counts characters, but in bash this counts bytes for multi-byte UTF-8 characters, not actual characters. Since Google Play's 500-character limit likely refers to Unicode characters, this could cause issues if the changelog contains non-ASCII characters (e.g., Chinese characters, emojis). The Ruby version has the same potential issue. Consider using a method that counts actual Unicode characters, or ensure that the character count matches Google Play's counting method.
.github/workflows/release.yml
Outdated
| echo "" | ||
| } > whatsnew/whatsnew-en-US | ||
| section_len=${#version_section} | ||
| if [ $((current_len + section_len)) -lt "$max_chars" ]; then |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comparison operator -lt is used for a less-than check, but should be -le (less than or equal) to properly respect the character limit. If the current length plus section length exactly equals MAX_CHARS, the content should still be included since it doesn't exceed the limit. Using -lt means content that would fit exactly at the limit would be incorrectly excluded.
| if [ $((current_len + section_len)) -lt "$max_chars" ]; then | |
| if [ $((current_len + section_len)) -le "$max_chars" ]; then |
CHANGELOG.md
Outdated
|
|
||
| Example format: | ||
| ``` | ||
| ## vX.Y.Z (Build N) |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation states to use "vX.Y.Z" format in the example (line 55), but the actual changelog entries use "v2.3.14" format with lowercase 'v' and the version header pattern in the code matches both formats. For consistency and to avoid confusion, the example should match the actual format used in the file. Update the example to use lowercase 'v' to match the actual entries above.
|
|
||
| # Check if adding this section would exceed the limit | ||
| current_length = combined_parts.join.length | ||
| if current_length + version_section.length <= available_space |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar issue as in release.yml line 604: the comparison uses -lt instead of -le. When current_length + version_section.length exactly equals available_space, the content should be included since it fits within the limit. Using -lt incorrectly excludes content that would fit exactly at the boundary.
fastlane/changelog_helper.rb
Outdated
| formatted | ||
| end | ||
|
|
||
| # Get changelog for current version and up to 2 previous versions |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function header comment says "Get changelog for current version and up to 2 previous versions" but the code on line 176 uses all_versions[current_index, 3] which gets up to 3 versions total (current + 2 previous). The comment should say "up to 3 versions (current + 2 previous)" for accuracy, or the implementation should be changed if only 2 versions total were intended.
| # Get changelog for current version and up to 2 previous versions | |
| # Get changelog for current version and up to 2 previous versions (3 versions total) |
| content="${header} | ||
| # Get last 3 tags for multi-version release notes | ||
| TAGS=($(git tag --sort=-v:refname | head -3)) | ||
| CURRENT_VERSION="${{ needs.prepare.outputs.version }}" | ||
| V2er ${CURRENT_VERSION} | ||
|
|
||
| echo "Found tags: ${TAGS[*]}" | ||
| echo "Current version: $CURRENT_VERSION" | ||
| " | ||
| current_len=${#content} |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The heredoc-style string definition starting with content="${header} and continuing over multiple lines (578-582) embeds literal newlines into the content variable. However, the way bash handles this with the closing quote on line 582 creates the string with actual newlines. This is correct, but the length calculation on line 583 using ${#content} will count the actual newlines as single bytes. Ensure this matches how Google Play counts characters in release notes.
.github/workflows/release.yml
Outdated
| fi | ||
| # Check if changelog has entry for this version | ||
| if grep -qE "^##\s+v?${VERSION_NUMBER}(\s|$)" CHANGELOG.md; then |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The grep pattern on line 158 uses (\s|$) to match either whitespace or end of line after the version number, but this doesn't account for the build number that appears in actual changelog entries like "## v2.3.14 (Build 244)". The pattern should handle the optional build number: grep -qE "^##\s+v?${VERSION_NUMBER}(\s+\(Build\s+[0-9]+\))?\s*$" to match the actual format used in CHANGELOG.md.
| if grep -qE "^##\s+v?${VERSION_NUMBER}(\s|$)" CHANGELOG.md; then | |
| if grep -qE "^##\s+v?${VERSION_NUMBER}(\s+\(Build\s+[0-9]+\))?\s*$" CHANGELOG.md; then |
.github/workflows/release.yml
Outdated
| BEGIN { found=0; printing=0 } | ||
| /^##[[:space:]]+v?[0-9]+\.[0-9]+\.[0-9]+/ { | ||
| if (found && printing) { exit } | ||
| if ($0 ~ "v?" ver) { found=1; printing=1; next } |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The awk pattern $0 ~ "v?" ver on line 397 performs a literal substring match, not a regex match. This means it will match "v?" (the literal characters 'v' and '?') concatenated with the version string, which is not the intended behavior. To make the 'v' optional as intended, the pattern needs to be a proper regex. Consider using: match($0, "^##[[:space:]]+v?" ver "([[:space:]]|\\(|$)") or building the regex pattern correctly.
| if ($0 ~ "v?" ver) { found=1; printing=1; next } | |
| if (match($0, "^##[[:space:]]+v?" ver "([[:space:]]|\\(|$)")) { found=1; printing=1; next } |
.github/workflows/release.yml
Outdated
| BEGIN { found=0; printing=0 } | ||
| /^##[[:space:]]+v?[0-9]+\.[0-9]+\.[0-9]+/ { | ||
| if (found && printing) { exit } | ||
| if ($0 ~ "v?" ver) { found=1; printing=1; next } |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same issue as line 397: the pattern $0 ~ "v?" ver performs a literal substring match for "v?" followed by the version, not a regex match with optional 'v'. This will fail to match version headers in CHANGELOG.md. Use proper regex construction like match($0, "^##[[:space:]]+v?" ver "([[:space:]]|\\(|$)") to make the 'v' optional as intended.
| if ($0 ~ "v?" ver) { found=1; printing=1; next } | |
| if (match($0, "^##[[:space:]]+v?" ver "([[:space:]]|\\(|$)")) { found=1; printing=1; next } |
- Fix awk pattern to use proper regex match for version detection - Use -le instead of -lt for character limit comparison (include at exact limit) - Fix comment wording: "add entry" instead of "update" - Update CHANGELOG.md example to use realistic version format - Add clarifying comment about RELEASE_NOTES.md vs CHANGELOG.md - Fix regex pattern in changelog validation to handle optional build number - Remove unnecessary .strip() call in Ruby version extraction - Update function comment to clarify "3 versions total" 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
Summary
CHANGELOG.mdwith history from git (v2.3.10 - v2.3.14)fastlane/changelog_helper.rbfor changelog extraction and validationrelease.ymlto validate changelog exists before releaserelease.ymlto generate GitHub Release notes fromCHANGELOG.mdrelease.ymlto generate Google Play whatsnew fromCHANGELOG.mdvalidate_changelogandversion_infolanes to Fastfiledeploy_betaanddeploy_productionlanesMotivation
This aligns with the iOS version's changelog workflow where:
CHANGELOG.mdwith version entriesCHANGELOG.mdNew Workflow
When releasing a new version:
config.gradleCHANGELOG.mdCHANGELOG.mdFiles Changed
CHANGELOG.mdfastlane/changelog_helper.rb.github/workflows/release.ymlfastlane/FastfileTest plan
fastlane validate_changelogworks correctlyfastlane version_infodisplays changelog🤖 Generated with Claude Code