diff --git a/.github/workflows/validate-changelog.yaml b/.github/workflows/validate-changelog.yaml new file mode 100644 index 0000000..fbe0d9a --- /dev/null +++ b/.github/workflows/validate-changelog.yaml @@ -0,0 +1,200 @@ +name: Validate changelog + +on: + workflow_call: + pull_request: + types: [opened, synchronize, reopened] + paths: + - 'CHANGELOG.md' + branches: + - 'legacy#release#v*.*.*' + - 'main#release#v*.*.*' + - 'main#release#major' + - 'main#release#minor' + - 'main#release#patch' + - 'master#release#v*.*.*' + - 'master#release#major' + - 'master#release#minor' + - 'master#release#patch' + - 'release#v*.*.*' + - 'release#major' + - 'release#minor' + - 'release#patch' + - 'release-v*.*.x#release#v*.*.*' + # "!" negates previous positive patterns so it has to be at the end. + - '!release-v*.x.x#release#v*.*.*' + +permissions: + contents: read + pull-requests: write + +jobs: + validate-changelog: + name: Validate changelog structure + runs-on: ubuntu-24.04 + + steps: + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + - name: Extract version from branch name + id: extract_version + env: + BRANCH_NAME: ${{ github.head_ref }} + run: | + branch_name="$BRANCH_NAME" + echo "Branch name: $branch_name" + + version="" + + # Use case statement for more reliable pattern matching + case "$branch_name" in + *"#release#v"*) + # Extract version from patterns like main#release#v1.2.3 + version=$(echo "$branch_name" | sed -E 's/.*#release#v([0-9]+\.[0-9]+\.[0-9]+).*/\1/') + if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Extracted explicit version: $version" + else + echo "::error::Failed to extract version from: $branch_name" + exit 1 + fi + ;; + + *"#release#major"|*"#release#minor"|*"#release#patch") + # Extract bump type and get version from changelog + bump_type=$(echo "$branch_name" | sed -E 's/.*#release#(major|minor|patch).*/\1/') + echo "Detected semantic version branch ($bump_type), extracting version from changelog..." + + if [ ! -f "CHANGELOG.md" ]; then + echo "::error::CHANGELOG.md not found" + exit 1 + fi + + # Get the first version entry (should be the newest one that architect created) + version=$(grep -E "^## \[[0-9]+\.[0-9]+\.[0-9]+\]" CHANGELOG.md | head -1 | sed -E 's/^## \[([0-9]+\.[0-9]+\.[0-9]+)\].*/\1/') + + if [ -z "$version" ]; then + echo "::error::Could not find version in CHANGELOG.md" + echo "::error::Expected to find a line like: ## [X.Y.Z] - YYYY-MM-DD" + echo "Available changelog entries:" + grep -E "^## \[" CHANGELOG.md | head -5 || echo "No version entries found" + exit 1 + fi + + echo "Extracted version from changelog for $bump_type release: $version" + ;; + + *) + echo "::error::Could not extract version from branch name: $branch_name" + echo "::error::Expected patterns:" + echo " - main#release#v1.2.3" + echo " - main#release#major" + echo " - main#release#minor" + echo " - main#release#patch" + echo " - master#release#v1.2.3" + echo " - master#release#major|minor|patch" + echo " - release#v1.2.3" + echo " - release#major|minor|patch" + exit 1 + ;; + esac + + if [ -z "$version" ]; then + echo "::error::Version extraction failed - version is empty" + exit 1 + fi + + echo "Final extracted version: $version" + echo "version=$version" >> $GITHUB_OUTPUT + + - name: Validate changelog content + run: | + version="${{ steps.extract_version.outputs.version }}" + changelog_file="CHANGELOG.md" + + echo "Validating changelog content for version $version..." + echo "Checking file: $changelog_file" + + # Check if CHANGELOG.md exists + if [ ! -f "$changelog_file" ]; then + echo "::error::No $changelog_file found in repository" + exit 1 + fi + + # Extract the content between the new version and the next version + changelog_section=$(awk " + /^## \[$version\]/ { found=1; print; next } + found && /^## \[/ && !/^## \[$version\]/ { found=0 } + found { print } + " "$changelog_file") + + if [ -z "$changelog_section" ]; then + echo "::error::No section found for version $version in changelog" + echo "::error::Expected format: ## [$version] - YYYY-MM-DD" + exit 1 + fi + + echo "Found changelog section for version $version" + + # Check if there are any ### headers (Added, Changed, Deprecated, Removed, Fixed, Security) + subsection_count=$(echo "$changelog_section" | grep -c "^### " || true) + + if [ "$subsection_count" -eq 0 ]; then + echo "::error title=Empty Changelog::No changelog content found for version $version" + echo "::error::The release appears to have an empty changelog." + echo "" + echo "Please add content under appropriate sections like:" + echo " ### Added - for new features" + echo " ### Changed - for changes in existing functionality" + echo " ### Fixed - for any bug fixes" + echo " ### Removed - for now removed features" + echo " ### Deprecated - for soon-to-be removed features" + echo " ### Security - in case of vulnerabilities" + echo "" + echo "Current changelog section:" + echo "$changelog_section" + exit 1 + else + echo "Changelog validation passed: Found $subsection_count section(s)" + echo "Sections found:" + echo "$changelog_section" | grep "^### " | sed 's/^/ /' + fi + + - name: Add PR comment if validation fails + if: failure() + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # 7.0.1 + with: + script: | + const version = '${{ steps.extract_version.outputs.version }}'; + const body = `## 📝 Changelog Validation Failed + + The changelog for version **${version}** appears to be empty. + + Please add content under appropriate sections like: + - \`### Added\` - for new features + - \`### Changed\` - for changes in existing functionality + - \`### Fixed\` - for any bug fixes + - \`### Removed\` - for now removed features + - \`### Deprecated\` - for soon-to-be removed features + - \`### Security\` - in case of vulnerabilities + + **Example:** + \`\`\`markdown + ## [${version}] - ${new Date().toISOString().split('T')[0]} + + ### Changed + + - Updated dependency X to version Y + - Improved error handling in module Z + \`\`\` + + This check will pass once you add proper changelog content.`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: body + });