Skip to content

Commit cd9c6bc

Browse files
committed
feat: add changelog management system for TestFlight releases
- Create CHANGELOG.md to maintain version history - Add changelog_helper.rb to extract version-specific changelogs - Update Fastfile to validate and use CHANGELOG.md content - Automatically format changelog for TestFlight (numbered list to bullets) - Add comprehensive documentation in CLAUDE.md - Build will fail if CHANGELOG.md missing entry for current version This ensures all TestFlight releases have proper, version-specific release notes instead of generic 'Bug fixes and improvements' text.
1 parent c3a8954 commit cd9c6bc

File tree

4 files changed

+253
-2
lines changed

4 files changed

+253
-2
lines changed

CHANGELOG.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Changelog
2+
3+
All notable changes to V2er iOS app will be documented in this file.
4+
5+
## v1.1.1 (Build 31)
6+
1. Feature: Add feed filter menu with Reddit-style dropdown for better content filtering
7+
2. Fix: Prevent crash when clicking Ignore/Report buttons without being logged in
8+
3. Fix: Improve TestFlight beta distribution configuration
9+
4. Feature: Enable automatic TestFlight beta distribution to public testers
10+
11+
## v1.1.0 (Build 30)
12+
1. Feature: Initial public beta release
13+
2. Fix: Resolve iOS build hanging at code signing step
14+
3. Fix: Improve version management system using xcconfig
15+
4. Feature: Centralized version configuration in Version.xcconfig
16+
17+
---
18+
19+
## How to Update Changelog
20+
21+
When updating the version in `V2er/Config/Version.xcconfig`:
22+
23+
1. Add a new version section at the top of this file
24+
2. List all changes since the last version:
25+
- Use "Feature:" for new features
26+
- Use "Fix:" for bug fixes
27+
- Use "Improvement:" for enhancements
28+
- Use "Breaking:" for breaking changes
29+
30+
Example format:
31+
```
32+
## vX.Y.Z (Build N)
33+
1. Feature: Description of new feature
34+
2. Fix: Description of bug fix
35+
3. Improvement: Description of enhancement
36+
```
37+
38+
The changelog will be automatically extracted and used in TestFlight release notes.

CLAUDE.md

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,63 @@ Tests are located in:
9999

100100
Currently contains only boilerplate test setup.
101101

102+
## Release Management
103+
104+
### Version and Changelog Workflow
105+
106+
Version information is centralized in `V2er/Config/Version.xcconfig`:
107+
- `MARKETING_VERSION`: User-facing version (e.g., 1.1.1)
108+
- `CURRENT_PROJECT_VERSION`: Build number (auto-incremented by CI)
109+
110+
**When updating the version for a new release:**
111+
112+
1. **Update Version.xcconfig**
113+
```bash
114+
# Edit V2er/Config/Version.xcconfig
115+
MARKETING_VERSION = 1.2.0
116+
```
117+
118+
2. **Update CHANGELOG.md**
119+
Add a new section at the top with your changes:
120+
```markdown
121+
## v1.2.0 (Build XX)
122+
1. Feature: Description of new feature
123+
2. Fix: Description of bug fix
124+
3. Improvement: Description of enhancement
125+
```
126+
127+
3. **Commit and push to trigger release**
128+
```bash
129+
git add V2er/Config/Version.xcconfig CHANGELOG.md
130+
git commit -m "chore: bump version to 1.2.0"
131+
git push origin main
132+
```
133+
134+
The CI pipeline will:
135+
- Validate that CHANGELOG.md contains an entry for the new version
136+
- Extract the changelog for TestFlight release notes
137+
- Auto-increment build number
138+
- Upload to TestFlight with your changelog
139+
140+
### Fastlane Commands
141+
142+
```bash
143+
# Build and upload to TestFlight (requires changelog)
144+
fastlane beta
145+
146+
# Distribute existing build to beta testers
147+
fastlane distribute_beta
148+
149+
# Sync certificates and provisioning profiles
150+
fastlane sync_certificates
151+
```
152+
102153
## Important Notes
103154

104155
- Minimum iOS version: iOS 15.0
105156
- Supported architectures: armv7, arm64
106157
- Orientation: Portrait only on iPhone, all orientations on iPad
107158
- UI Style: Light mode enforced
108159
- Website submodule: Located at `website/` (separate repository)
109-
- create PR should always use english
160+
- Create PR should always use English
161+
- **CHANGELOG.md is required** for all releases - the build will fail if the current version is missing from the changelog

fastlane/Fastfile

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Fastfile for V2er iOS app
22

3+
# Import changelog helper
4+
require_relative 'changelog_helper'
5+
36
default_platform(:ios)
47

58
platform :ios do
@@ -105,6 +108,11 @@ platform :ios do
105108

106109
desc "Build and upload to TestFlight"
107110
lane :beta do
111+
# Validate that changelog exists for current version
112+
unless ChangelogHelper.validate_changelog_exists
113+
UI.user_error!("Please update CHANGELOG.md with an entry for the current version before releasing!")
114+
end
115+
108116
# Ensure we have the latest certificates
109117
sync_certificates
110118

@@ -116,6 +124,7 @@ platform :ios do
116124
xcconfig_path = "../V2er/Config/Version.xcconfig"
117125
xcconfig_content = File.read(xcconfig_path)
118126
current_build_number = xcconfig_content.match(/CURRENT_PROJECT_VERSION = (\d+)/)[1].to_i
127+
current_version = xcconfig_content.match(/MARKETING_VERSION = (.+)/)[1].strip
119128

120129
latest_testflight = latest_testflight_build_number(api_key: api_key)
121130

@@ -154,6 +163,9 @@ platform :ios do
154163
# Build the app
155164
build_ipa
156165

166+
# Extract changelog for the current version
167+
changelog_content = ChangelogHelper.extract_changelog(current_version)
168+
157169
# Upload to TestFlight
158170
upload_to_testflight(
159171
api_key: api_key,
@@ -164,7 +176,7 @@ platform :ios do
164176
distribute_external: true, # Distribute to external testers (public beta)
165177
distribute_only: false, # Upload and distribute in one action
166178
groups: ["Public Beta", "External Testers", "Beta Testers"], # Public beta groups
167-
changelog: "Bug fixes and improvements",
179+
changelog: changelog_content, # Use changelog from CHANGELOG.md
168180
notify_external_testers: true, # Send email notifications to external testers
169181
uses_non_exempt_encryption: false, # Required for automatic distribution
170182
submit_beta_review: true, # Automatically submit for Beta review

fastlane/changelog_helper.rb

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# changelog_helper.rb
2+
# Helper module to extract changelog entries from CHANGELOG.md
3+
4+
module ChangelogHelper
5+
# Extract changelog for a specific version from CHANGELOG.md
6+
# @param version [String] The version to extract (e.g., "1.1.1")
7+
# @return [String] The changelog content for the specified version
8+
def self.extract_changelog(version)
9+
changelog_path = File.expand_path("../CHANGELOG.md", __dir__)
10+
11+
unless File.exist?(changelog_path)
12+
UI.error("CHANGELOG.md not found at #{changelog_path}")
13+
return "Bug fixes and improvements"
14+
end
15+
16+
content = File.read(changelog_path)
17+
18+
# Match the version section, handling both "vX.Y.Z" and "X.Y.Z" formats
19+
# Also capture optional build number like "v1.1.1 (Build 31)"
20+
version_pattern = /^##\s+v?#{Regexp.escape(version)}(?:\s+\(Build\s+\d+\))?\s*$/
21+
22+
lines = content.lines
23+
start_index = nil
24+
end_index = nil
25+
26+
# Find the start of the version section
27+
lines.each_with_index do |line, index|
28+
if line.match?(version_pattern)
29+
start_index = index
30+
break
31+
end
32+
end
33+
34+
if start_index.nil?
35+
UI.warning("Version #{version} not found in CHANGELOG.md")
36+
UI.message("Available versions:")
37+
lines.each do |line|
38+
if line.match?(/^##\s+v?\d+\.\d+\.\d+/)
39+
UI.message(" - #{line.strip}")
40+
end
41+
end
42+
return "Bug fixes and improvements"
43+
end
44+
45+
# Find the end of the version section (next ## heading or ---)
46+
((start_index + 1)...lines.length).each do |index|
47+
line = lines[index]
48+
if line.match?(/^##\s+/) || line.match?(/^---/)
49+
end_index = index
50+
break
51+
end
52+
end
53+
54+
end_index ||= lines.length
55+
56+
# Extract the changelog content (skip the version header)
57+
changelog_lines = lines[(start_index + 1)...end_index]
58+
59+
# Remove leading/trailing empty lines and convert to string
60+
changelog = changelog_lines
61+
.join("")
62+
.strip
63+
64+
if changelog.empty?
65+
UI.warning("No changelog content found for version #{version}")
66+
return "Bug fixes and improvements"
67+
end
68+
69+
# Format for TestFlight (convert numbered list to bullet points if needed)
70+
# TestFlight supports basic formatting
71+
formatted_changelog = format_for_testflight(changelog)
72+
73+
UI.success("Extracted changelog for version #{version}:")
74+
UI.message(formatted_changelog)
75+
76+
formatted_changelog
77+
end
78+
79+
# Format changelog content for TestFlight display
80+
# @param content [String] Raw changelog content
81+
# @return [String] Formatted changelog
82+
def self.format_for_testflight(content)
83+
# TestFlight supports:
84+
# - Plain text
85+
# - Line breaks
86+
# - Basic formatting
87+
88+
# Convert numbered lists to bullet points for better readability
89+
# "1. Feature: xxx" -> "• Feature: xxx"
90+
formatted = content.gsub(/^\d+\.\s+/, "• ")
91+
92+
# Ensure we don't exceed TestFlight's changelog length limit (4000 chars)
93+
if formatted.length > 3900
94+
formatted = formatted[0...3900] + "\n\n(See full changelog at github.com/v2er-app/iOS)"
95+
end
96+
97+
formatted
98+
end
99+
100+
# Get the current version from Version.xcconfig
101+
# @return [String] The current marketing version
102+
def self.get_current_version
103+
xcconfig_path = File.expand_path("../V2er/Config/Version.xcconfig", __dir__)
104+
105+
unless File.exist?(xcconfig_path)
106+
UI.user_error!("Version.xcconfig not found at #{xcconfig_path}")
107+
end
108+
109+
content = File.read(xcconfig_path)
110+
version_match = content.match(/MARKETING_VERSION\s*=\s*(.+)/)
111+
112+
if version_match
113+
version = version_match[1].strip
114+
UI.message("Current version from Version.xcconfig: #{version}")
115+
version
116+
else
117+
UI.user_error!("Could not find MARKETING_VERSION in Version.xcconfig")
118+
end
119+
end
120+
121+
# Validate that changelog exists for the current version
122+
# @return [Boolean] True if changelog exists, false otherwise
123+
def self.validate_changelog_exists
124+
current_version = get_current_version
125+
changelog_path = File.expand_path("../CHANGELOG.md", __dir__)
126+
127+
unless File.exist?(changelog_path)
128+
UI.error("❌ CHANGELOG.md not found!")
129+
UI.message("Please create CHANGELOG.md with an entry for version #{current_version}")
130+
return false
131+
end
132+
133+
content = File.read(changelog_path)
134+
version_pattern = /^##\s+v?#{Regexp.escape(current_version)}/
135+
136+
if content.match?(version_pattern)
137+
UI.success("✅ Changelog entry found for version #{current_version}")
138+
return true
139+
else
140+
UI.error("❌ No changelog entry found for version #{current_version}")
141+
UI.message("Please add a changelog entry in CHANGELOG.md:")
142+
UI.message("")
143+
UI.message("## v#{current_version}")
144+
UI.message("1. Feature/Fix: Description of changes")
145+
UI.message("")
146+
return false
147+
end
148+
end
149+
end

0 commit comments

Comments
 (0)