From 6d4231edcaaf4ccebc11bdb2f90da870c6c80168 Mon Sep 17 00:00:00 2001 From: AndyMik90 Date: Fri, 2 Jan 2026 13:39:46 +0100 Subject: [PATCH] ci(release): add CHANGELOG.md validation and fix release workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The release workflow was failing with "GitHub Releases requires a tag" when triggered via workflow_dispatch because no tag existed. Changes: - prepare-release.yml: Validates CHANGELOG.md has entry for version BEFORE creating tag (fails early with clear error message) - release.yml: Uses CHANGELOG.md content instead of release-drafter for release notes; fixes workflow_dispatch to be dry-run only - bump-version.js: Warns if CHANGELOG.md missing entry for new version - RELEASE.md: Updated documentation for new changelog-first workflow This ensures releases are only created when CHANGELOG.md is properly updated, preventing incomplete releases and giving better release notes. šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/prepare-release.yml | 130 ++++++++++++++++++++++++-- .github/workflows/release.yml | 74 +++++++++++++-- RELEASE.md | 113 +++++++++++++++++----- scripts/bump-version.js | 73 +++++++++++++-- 4 files changed, 345 insertions(+), 45 deletions(-) diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index d50940c1..ac108378 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -1,8 +1,10 @@ name: Prepare Release # Triggers when code is pushed to main (e.g., merging develop → main) -# If package.json version is newer than the latest tag, creates a new tag -# which then triggers the release.yml workflow +# If package.json version is newer than the latest tag: +# 1. Validates CHANGELOG.md has an entry for this version (FAILS if missing) +# 2. Extracts release notes from CHANGELOG.md +# 3. Creates a new tag which triggers release.yml on: push: @@ -67,8 +69,122 @@ jobs: echo "ā­ļø No release needed (package version not newer than latest tag)" fi - - name: Create and push tag + # CRITICAL: Validate CHANGELOG.md has entry for this version BEFORE creating tag + - name: Validate and extract changelog if: steps.check.outputs.should_release == 'true' + id: changelog + run: | + VERSION="${{ steps.check.outputs.new_version }}" + CHANGELOG_FILE="CHANGELOG.md" + + echo "šŸ” Validating CHANGELOG.md for version $VERSION..." + + if [ ! -f "$CHANGELOG_FILE" ]; then + echo "::error::CHANGELOG.md not found! Please create CHANGELOG.md with release notes." + exit 1 + fi + + # Extract changelog section for this version + # Looks for "## X.Y.Z" header and captures until next "## " or "---" or end + CHANGELOG_CONTENT=$(awk -v ver="$VERSION" ' + BEGIN { found=0; content="" } + /^## / { + if (found) exit + # Match version at start of header (e.g., "## 2.7.3 -" or "## 2.7.3") + if ($2 == ver || $2 ~ "^"ver"[[:space:]]*-") { + found=1 + # Skip the header line itself, we will add our own + next + } + } + /^---$/ { if (found) exit } + found { content = content $0 "\n" } + END { + if (!found) { + print "NOT_FOUND" + exit 1 + } + # Trim leading/trailing whitespace + gsub(/^[[:space:]]+|[[:space:]]+$/, "", content) + print content + } + ' "$CHANGELOG_FILE") + + if [ "$CHANGELOG_CONTENT" = "NOT_FOUND" ] || [ -z "$CHANGELOG_CONTENT" ]; then + echo "" + echo "::error::═══════════════════════════════════════════════════════════════════════" + echo "::error:: CHANGELOG VALIDATION FAILED" + echo "::error::═══════════════════════════════════════════════════════════════════════" + echo "::error::" + echo "::error:: Version $VERSION not found in CHANGELOG.md!" + echo "::error::" + echo "::error:: Before releasing, please update CHANGELOG.md with an entry like:" + echo "::error::" + echo "::error:: ## $VERSION - Your Release Title" + echo "::error::" + echo "::error:: ### ✨ New Features" + echo "::error:: - Feature description" + echo "::error::" + echo "::error:: ### šŸ› Bug Fixes" + echo "::error:: - Fix description" + echo "::error::" + echo "::error::═══════════════════════════════════════════════════════════════════════" + echo "" + + # Also add to job summary for visibility + echo "## āŒ Release Blocked: Missing Changelog" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Version **$VERSION** was not found in CHANGELOG.md." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### How to fix:" >> $GITHUB_STEP_SUMMARY + echo "1. Update CHANGELOG.md with release notes for version $VERSION" >> $GITHUB_STEP_SUMMARY + echo "2. Commit and push the changes" >> $GITHUB_STEP_SUMMARY + echo "3. The release will automatically retry" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Expected format:" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`markdown" >> $GITHUB_STEP_SUMMARY + echo "## $VERSION - Release Title" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ✨ New Features" >> $GITHUB_STEP_SUMMARY + echo "- Feature description" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### šŸ› Bug Fixes" >> $GITHUB_STEP_SUMMARY + echo "- Fix description" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + + exit 1 + fi + + echo "āœ… Found changelog entry for version $VERSION" + echo "" + echo "--- Extracted Release Notes ---" + echo "$CHANGELOG_CONTENT" + echo "--- End Release Notes ---" + + # Save changelog to file for artifact upload + echo "$CHANGELOG_CONTENT" > changelog-extract.md + + # Also save to output (for short changelogs) + # Using heredoc for multiline output + { + echo "content<> $GITHUB_OUTPUT + + echo "changelog_valid=true" >> $GITHUB_OUTPUT + + # Upload changelog as artifact for release.yml to use + - name: Upload changelog artifact + if: steps.check.outputs.should_release == 'true' && steps.changelog.outputs.changelog_valid == 'true' + uses: actions/upload-artifact@v4 + with: + name: changelog-${{ steps.check.outputs.new_version }} + path: changelog-extract.md + retention-days: 1 + + - name: Create and push tag + if: steps.check.outputs.should_release == 'true' && steps.changelog.outputs.changelog_valid == 'true' run: | VERSION="${{ steps.check.outputs.new_version }}" TAG="v$VERSION" @@ -85,17 +201,19 @@ jobs: - name: Summary run: | - if [ "${{ steps.check.outputs.should_release }}" = "true" ]; then + if [ "${{ steps.check.outputs.should_release }}" = "true" ] && [ "${{ steps.changelog.outputs.changelog_valid }}" = "true" ]; then echo "## šŸš€ Release Triggered" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Version:** v${{ steps.check.outputs.new_version }}" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY + echo "āœ… Changelog validated and extracted from CHANGELOG.md" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY echo "The release workflow has been triggered and will:" >> $GITHUB_STEP_SUMMARY echo "1. Build binaries for all platforms" >> $GITHUB_STEP_SUMMARY - echo "2. Generate changelog from PRs" >> $GITHUB_STEP_SUMMARY + echo "2. Use changelog from CHANGELOG.md" >> $GITHUB_STEP_SUMMARY echo "3. Create GitHub release" >> $GITHUB_STEP_SUMMARY echo "4. Update README with new version" >> $GITHUB_STEP_SUMMARY - else + elif [ "${{ steps.check.outputs.should_release }}" = "false" ]; then echo "## ā­ļø No Release Needed" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Package version:** ${{ steps.package.outputs.version }}" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7512b627..3360c512 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -505,23 +505,78 @@ jobs: cat release-assets/checksums.sha256 >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - - name: Generate changelog - if: ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.dry_run != true) }} + - name: Extract changelog from CHANGELOG.md + if: ${{ github.event_name == 'push' }} id: changelog - uses: release-drafter/release-drafter@v6 - with: - config-name: release-drafter.yml - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Extract version from tag (v2.7.2 -> 2.7.2) + VERSION=${GITHUB_REF_NAME#v} + CHANGELOG_FILE="CHANGELOG.md" + + echo "šŸ“‹ Extracting release notes for version $VERSION from CHANGELOG.md..." + + if [ ! -f "$CHANGELOG_FILE" ]; then + echo "::warning::CHANGELOG.md not found, using minimal release notes" + echo "body=Release v$VERSION" >> $GITHUB_OUTPUT + exit 0 + fi + + # Extract changelog section for this version + # Looks for "## X.Y.Z" header and captures until next "## " or "---" + CHANGELOG_CONTENT=$(awk -v ver="$VERSION" ' + BEGIN { found=0; content="" } + /^## / { + if (found) exit + # Match version at start of header (e.g., "## 2.7.3 -" or "## 2.7.3") + if ($2 == ver || $2 ~ "^"ver"[[:space:]]*-") { + found=1 + next + } + } + /^---$/ { if (found) exit } + found { content = content $0 "\n" } + END { + if (!found) { + print "NOT_FOUND" + exit 0 + } + # Trim leading/trailing whitespace + gsub(/^[[:space:]]+|[[:space:]]+$/, "", content) + print content + } + ' "$CHANGELOG_FILE") + + if [ "$CHANGELOG_CONTENT" = "NOT_FOUND" ] || [ -z "$CHANGELOG_CONTENT" ]; then + echo "::warning::Version $VERSION not found in CHANGELOG.md, using minimal release notes" + CHANGELOG_CONTENT="Release v$VERSION + +See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md) for details." + fi + + echo "āœ… Extracted changelog content" + + # Save to file first (more reliable for multiline) + echo "$CHANGELOG_CONTENT" > changelog-body.md + + # Use file-based output for multiline content + { + echo "body<> $GITHUB_OUTPUT - name: Create Release - if: ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.dry_run != true) }} + if: ${{ github.event_name == 'push' }} uses: softprops/action-gh-release@v2 with: body: | ${{ steps.changelog.outputs.body }} + --- + ${{ steps.virustotal.outputs.vt_results }} + + **Full Changelog**: https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md files: release-assets/* draft: false prerelease: ${{ contains(github.ref, 'beta') || contains(github.ref, 'alpha') }} @@ -532,7 +587,8 @@ jobs: update-readme: needs: [create-release] runs-on: ubuntu-latest - if: ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.dry_run != true) }} + # Only update README on actual releases (tag push), not dry runs + if: ${{ github.event_name == 'push' }} permissions: contents: write steps: diff --git a/RELEASE.md b/RELEASE.md index d7f6eb10..21d0e6b5 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -69,9 +69,38 @@ This will: - Update `apps/frontend/package.json` - Update `package.json` (root) - Update `apps/backend/__init__.py` +- Check if `CHANGELOG.md` has an entry for the new version (warns if missing) - Create a commit with message `chore: bump version to X.Y.Z` -### Step 2: Push and Create PR +### Step 2: Update CHANGELOG.md (REQUIRED) + +**IMPORTANT: The release will fail if CHANGELOG.md doesn't have an entry for the new version.** + +Add release notes to `CHANGELOG.md` at the top of the file: + +```markdown +## 2.8.0 - Your Release Title + +### ✨ New Features +- Feature description + +### šŸ› ļø Improvements +- Improvement description + +### šŸ› Bug Fixes +- Fix description + +--- +``` + +Then amend the version bump commit: + +```bash +git add CHANGELOG.md +git commit --amend --no-edit +``` + +### Step 3: Push and Create PR ```bash # Push your branch @@ -81,24 +110,25 @@ git push origin your-branch gh pr create --base main --title "Release v2.8.0" ``` -### Step 3: Merge to Main +### Step 4: Merge to Main Once the PR is approved and merged to `main`, GitHub Actions will automatically: 1. **Detect the version bump** (`prepare-release.yml`) -2. **Create a git tag** (e.g., `v2.8.0`) -3. **Trigger the release workflow** (`release.yml`) -4. **Build binaries** for all platforms: +2. **Validate CHANGELOG.md** has an entry for the new version (FAILS if missing) +3. **Extract release notes** from CHANGELOG.md +4. **Create a git tag** (e.g., `v2.8.0`) +5. **Trigger the release workflow** (`release.yml`) +6. **Build binaries** for all platforms: - macOS Intel (x64) - code signed & notarized - macOS Apple Silicon (arm64) - code signed & notarized - Windows (NSIS installer) - code signed - Linux (AppImage + .deb) -5. **Generate changelog** from merged PRs (using release-drafter) -6. **Scan binaries** with VirusTotal -7. **Create GitHub release** with all artifacts -8. **Update README** with new version badge and download links +7. **Scan binaries** with VirusTotal +8. **Create GitHub release** with release notes from CHANGELOG.md +9. **Update README** with new version badge and download links -### Step 4: Verify +### Step 5: Verify After merging, check: - [GitHub Actions](https://github.com/AndyMik90/Auto-Claude/actions) - ensure all workflows pass @@ -113,28 +143,49 @@ We follow [Semantic Versioning](https://semver.org/): - **MINOR** (0.X.0): New features, backwards compatible - **PATCH** (0.0.X): Bug fixes, backwards compatible -## Changelog Generation +## Changelog Management -Changelogs are automatically generated from merged PRs using [Release Drafter](https://github.com/release-drafter/release-drafter). +Release notes are managed in `CHANGELOG.md` and used for GitHub releases. -### PR Labels for Changelog Categories +### Changelog Format -| Label | Category | -|-------|----------| -| `feature`, `enhancement` | New Features | -| `bug`, `fix` | Bug Fixes | -| `improvement`, `refactor` | Improvements | -| `documentation` | Documentation | -| (any other) | Other Changes | +Each version entry in `CHANGELOG.md` should follow this format: -**Tip:** Add appropriate labels to your PRs for better changelog organization. +```markdown +## X.Y.Z - Release Title + +### ✨ New Features +- Feature description with context + +### šŸ› ļø Improvements +- Improvement description + +### šŸ› Bug Fixes +- Fix description + +--- +``` + +### Changelog Validation + +The release workflow **validates** that `CHANGELOG.md` has an entry for the version being released: + +- If the entry is **missing**, the release is **blocked** with a clear error message +- If the entry **exists**, its content is used for the GitHub release notes + +### Writing Good Release Notes + +- **Be specific**: Instead of "Fixed bug", write "Fixed crash when opening large files" +- **Group by impact**: Features first, then improvements, then fixes +- **Credit contributors**: Mention contributors for significant changes +- **Link issues**: Reference GitHub issues where relevant (e.g., "Fixes #123") ## Workflows | Workflow | Trigger | Purpose | |----------|---------|---------| -| `prepare-release.yml` | Push to `main` | Detects version bump, creates tag | -| `release.yml` | Tag `v*` pushed | Builds binaries, creates release | +| `prepare-release.yml` | Push to `main` | Detects version bump, **validates CHANGELOG.md**, creates tag | +| `release.yml` | Tag `v*` pushed | Builds binaries, extracts changelog, creates release | | `validate-version.yml` | Tag `v*` pushed | Validates tag matches package.json | | `update-readme` (in release.yml) | After release | Updates README with new version | @@ -153,6 +204,22 @@ Changelogs are automatically generated from merged PRs using [Release Drafter](h git diff HEAD~1 --name-only | grep package.json ``` +### Release blocked: Missing changelog entry + +If you see "CHANGELOG VALIDATION FAILED" in the workflow: + +1. The `prepare-release.yml` workflow validated that `CHANGELOG.md` doesn't have an entry for the new version +2. **Fix**: Add an entry to `CHANGELOG.md` with the format `## X.Y.Z - Title` +3. Commit and push the changelog update +4. The workflow will automatically retry when the changes are pushed to `main` + +```bash +# Add changelog entry, then: +git add CHANGELOG.md +git commit -m "docs: add changelog for vX.Y.Z" +git push origin main +``` + ### Build failed after tag was created - The release won't be published if builds fail diff --git a/scripts/bump-version.js b/scripts/bump-version.js index 6297d580..5736eb07 100644 --- a/scripts/bump-version.js +++ b/scripts/bump-version.js @@ -149,6 +149,27 @@ function updateBackendInit(newVersion) { return true; } +// Check if CHANGELOG.md has an entry for the version +function checkChangelogEntry(version) { + const changelogPath = path.join(__dirname, '..', 'CHANGELOG.md'); + + if (!fs.existsSync(changelogPath)) { + warning('CHANGELOG.md not found - you will need to create it before releasing'); + return false; + } + + const content = fs.readFileSync(changelogPath, 'utf8'); + + // Look for "## X.Y.Z" or "## X.Y.Z -" header + const versionPattern = new RegExp(`^## ${version.replace(/\./g, '\\.')}(\\s|-)`, 'm'); + + if (versionPattern.test(content)) { + return true; + } + + return false; +} + // Main function function main() { const bumpType = process.argv[2]; @@ -198,7 +219,35 @@ function main() { // after the GitHub release is successfully published. This prevents version // mismatches where README shows a version that doesn't exist yet. - // 6. Create git commit + // 6. Check if CHANGELOG.md has entry for this version + info('Checking CHANGELOG.md...'); + const hasChangelogEntry = checkChangelogEntry(newVersion); + + if (hasChangelogEntry) { + success(`CHANGELOG.md already has entry for ${newVersion}`); + } else { + log(''); + warning('═══════════════════════════════════════════════════════════════════════'); + warning(' CHANGELOG.md does not have an entry for version ' + newVersion); + warning('═══════════════════════════════════════════════════════════════════════'); + warning(''); + warning(' The release workflow will FAIL if CHANGELOG.md is not updated!'); + warning(''); + warning(' Please add an entry to CHANGELOG.md before creating your PR:'); + warning(''); + log(` ## ${newVersion} - Your Release Title`, colors.cyan); + log('', colors.cyan); + log(' ### ✨ New Features', colors.cyan); + log(' - Feature description', colors.cyan); + log('', colors.cyan); + log(' ### šŸ› Bug Fixes', colors.cyan); + log(' - Fix description', colors.cyan); + warning(''); + warning('═══════════════════════════════════════════════════════════════════════'); + log(''); + } + + // 7. Create git commit info('Creating git commit...'); exec('git add apps/frontend/package.json package.json apps/backend/__init__.py'); exec(`git commit -m "chore: bump version to ${newVersion}"`); @@ -208,18 +257,28 @@ function main() { // when this commit is merged to main, ensuring releases only happen after // successful builds. - // 7. Instructions + // 8. Instructions log('\nšŸ“‹ Next steps:', colors.yellow); - log(` 1. Review the changes: git log -1`, colors.yellow); - log(` 2. Push to your branch: git push origin `, colors.yellow); - log(` 3. Create PR to main (or merge develop → main)`, colors.yellow); - log(` 4. When merged, GitHub Actions will automatically:`, colors.yellow); + if (!hasChangelogEntry) { + log(` 1. UPDATE CHANGELOG.md with release notes for ${newVersion}`, colors.red); + log(` 2. Commit the changelog: git add CHANGELOG.md && git commit --amend --no-edit`, colors.yellow); + log(` 3. Push to your branch: git push origin `, colors.yellow); + } else { + log(` 1. Review the changes: git log -1`, colors.yellow); + log(` 2. Push to your branch: git push origin `, colors.yellow); + } + log(` ${hasChangelogEntry ? '3' : '4'}. Create PR to main (or merge develop → main)`, colors.yellow); + log(` ${hasChangelogEntry ? '4' : '5'}. When merged, GitHub Actions will automatically:`, colors.yellow); + log(` - Validate CHANGELOG.md has entry for v${newVersion}`, colors.yellow); log(` - Create tag v${newVersion}`, colors.yellow); log(` - Build binaries for all platforms`, colors.yellow); - log(` - Create GitHub release with changelog`, colors.yellow); + log(` - Create GitHub release with changelog from CHANGELOG.md`, colors.yellow); log(` - Update README with new version\n`, colors.yellow); warning('Note: The commit has been created locally but NOT pushed.'); + if (!hasChangelogEntry) { + warning('IMPORTANT: Update CHANGELOG.md before pushing or the release will fail!'); + } info('Tags are created automatically by GitHub Actions when merged to main.'); log('\n✨ Version bump complete!\n', colors.green);