diff --git a/.commitlintrc.json b/.commitlintrc.json new file mode 100644 index 00000000..c9c52783 --- /dev/null +++ b/.commitlintrc.json @@ -0,0 +1,6 @@ +{ + "extends": [ + "@commitlint/config-conventional" + ] +} + diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5ace4600..c1a349aa 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,3 +4,5 @@ updates: directory: "/" schedule: interval: "weekly" + commit-message: + prefix: "chore(deps)" diff --git a/.github/scripts/changelog.sh b/.github/scripts/changelog.sh deleted file mode 100755 index 196bc038..00000000 --- a/.github/scripts/changelog.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -release_has_public_changes=false - -url=$(git remote get-url origin | sed -r 's/(.*)\.git/\1/') - -previous_tag=$(git describe --tags --abbrev=0 HEAD~) - -echo "Changes since $previous_tag:" -echo - -# Loop through all commits since previous tag -for rev in $(git log $previous_tag..HEAD --format="%H" --reverse --no-merges) -do - summary=$(git log $rev~..$rev --format="%s") - # Exclude commits starting with "Meta" - if [[ $summary != Meta* ]] - then - # Print markdown list of commit headlines - echo "* [$summary]($url/commit/$rev)" - # Append commit body indented (blank lines and signoff trailer removed) - git log $rev~..$rev --format="%b" | sed '/^\s*$/d' | sed '/^Signed-off-by:/d' | \ - while read -r line - do - # Escape markdown formatting symbols _ * ` - echo " $line" | sed 's/_/\\_/g' | sed 's/`/\\`/g' | sed 's/\*/\\\*/g' - done - release_has_public_changes=true - fi -done - -if ! $release_has_public_changes -then - echo "No public changes since $previous_tag." >&2 - exit 1 -fi diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5944a931..719e9db9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -63,3 +63,18 @@ jobs: - name: Test fish run: fish conf.d/forgit.plugin.fish + + # Validate conventional commit messages of PRs + commitlint: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Install npm + run: sudo apt update && sudo apt install --no-install-recommends -y npm + - name: Install commitlint + run: npm install -g @commitlint/cli @commitlint/config-conventional + - name: Validate PR commits + run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f4ff0397..e910a85f 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -18,24 +18,8 @@ jobs: with: fetch-depth: 0 - - name: Generate Changelog - id: changelog - run: .github/scripts/changelog.sh > CHANGELOG.md - # When the script returns with an error, it indicates that we don't have - # any public changes to be released. This is expected behavior, so we - # quit the workflow successfully in this case. - continue-on-error: true - - - name: Upload Changelog - uses: actions/upload-artifact@v6 - if: steps.changelog.outcome == 'success' - with: - name: changelog - path: CHANGELOG.md - - name: Set version id: set_version - if: steps.changelog.outcome == 'success' # Set version depending on whether this is a regular monthly release or # a custom release triggered by a tag push. run: | @@ -47,8 +31,28 @@ jobs: echo "VERSION=$VERSION" echo "VERSION=$VERSION" >> $GITHUB_OUTPUT + - name: Generate Changelog + id: changelog + uses: orhun/git-cliff-action@v4 + with: + config: cliff.toml + # Consider the changes from the pushed tag (latest) when running due to a + # tag push and consider unreleased changes when this is a regular monthly + # release. This is necessary, because on monthly releases, the tag is created later. + args: --verbose $( [ "${{github.event_name }}" == "push" ] && echo --latest || echo --unreleased --tag ${{ steps.set_version.outputs.VERSION }} ) + env: + OUTPUT: CHANGELOG.md + GITHUB_REPO: ${{ github.repository }} + + - name: Check Changelog + id: check_changelog + # Test if the changelog contains any entries + run: test -n "${{ steps.changelog.outputs.content }}" + continue-on-error: true + outputs: - changeLogOutcome: ${{ steps.changelog.outcome }} + changeLogOutcome: ${{ steps.check_changelog.outcome }} + changelog: ${{ steps.changelog.outputs.content }} version: ${{ steps.set_version.outputs.VERSION }} release: @@ -61,18 +65,13 @@ jobs: with: fetch-depth: 0 - - name: Download Changelog - uses: actions/download-artifact@v7 - with: - name: changelog - - name: Create Package - run: tar -czf forgit-${{ needs.changelog.outputs.version }}.tar.gz --exclude LICENSE --exclude README.md * + run: tar -czf forgit-${{ needs.changelog.outputs.version }}.tar.gz --exclude LICENSE --exclude README.md --exclude cliff.toml * - name: Release uses: softprops/action-gh-release@v2 with: # The release action implicitly creates the tag if it does not exist. tag_name: ${{ needs.changelog.outputs.version }} - body_path: CHANGELOG.md + body: ${{ needs.changelog.outputs.changelog }} files: forgit-${{ needs.changelog.outputs.version }}.tar.gz diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 00000000..3c714737 --- /dev/null +++ b/cliff.toml @@ -0,0 +1,82 @@ +# git-cliff ~ configuration file +# https://git-cliff.org/docs/configuration + + +[changelog] +# A Tera template to be rendered for each release in the changelog. +# See https://keats.github.io/tera/docs/#introduction +body = """ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | striptags | trim | upper_first }} + {% for commit in commits %} + - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ + {% if commit.breaking %}[**breaking**] {% endif %}\ + {{ commit.message | upper_first }}\ + {% endfor %} +{% endfor %} +""" +# Remove leading and trailing whitespaces from the changelog's body. +trim = true +# Render body even when there are no releases to process. +render_always = false +# An array of regex based postprocessors to modify the changelog. +postprocessors = [ + # Replace the placeholder with a URL. + #{ pattern = '', replace = "https://github.com/orhun/git-cliff" }, +] + +[git] +# Parse commits according to the conventional commits specification. +# See https://www.conventionalcommits.org +conventional_commits = true +# Exclude commits that do not match the conventional commits specification. +filter_unconventional = true +# Require all commits to be conventional. +# Takes precedence over filter_unconventional. +require_conventional = false +# Split commits on newlines, treating each line as an individual commit. +split_commits = false +# An array of regex based parsers to modify commit messages prior to further processing. +commit_preprocessors = [ + # Replace issue numbers with link templates to be updated in `changelog.postprocessors`. + #{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"}, + # Check spelling of the commit message using https://github.com/crate-ci/typos. + # If the spelling is incorrect, it will be fixed automatically. + #{ pattern = '.*', replace_command = 'typos --write-changes -' }, +] +# Prevent commits that are breaking from being excluded by commit parsers. +protect_breaking_commits = false +# An array of regex based parsers for extracting data from the commit message. +# Assigns commits to groups. +# Optionally sets the commit's scope and can decide to exclude commits from further processing. +commit_parsers = [ + { message = "^feat", group = "🚀 Features" }, + { message = "^fix", group = "🐛 Bug Fixes" }, + { message = "^doc", group = "📚 Documentation" }, + { message = "^perf", group = "⚡ Performance" }, + { message = "^refactor", group = "🚜 Refactor" }, + { message = "^style", group = "🎨 Styling" }, + { message = "^test", skip = true }, + { message = "^\\w*(ci)", skip = true }, + { message = "^chore|^ci", skip = true }, + { body = ".*security", group = "🛡️ Security" }, + { message = "^revert", group = "◀️ Revert" }, + { message = ".*", group = "💼 Other" }, +] +# Exclude commits that are not matched by any commit parser. +filter_commits = false +# Fail on a commit that is not matched by any commit parser. +fail_on_unmatched_commit = false +# An array of link parsers for extracting external references, and turning them into URLs, using regex. +link_parsers = [] +# Include only the tags that belong to the current branch. +use_branch_tags = false +# Order releases topologically instead of chronologically. +topo_order = false +# Order commits topologically instead of chronologically. +topo_order_commits = true +# Order of commits in each group/release within the changelog. +# Allowed values: newest, oldest +sort_commits = "oldest" +# Process submodules commits +recurse_submodules = false