diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6a955ea..bbd2d66 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,7 +54,7 @@ When committing changes, please try to follow [Conventional Commits](https://decisions.seedcase-project.org/why-conventional-commits/) as Git messages. Using this convention allows us to be able to automatically create a release based on the commit message by using -[Commitizen](https://decisions.seedcase-project.org/why-semantic-release-with-commitizen/). +[Cocogitto](https://decisions.seedcase-project.org/why-semantic-release-with-cocogitto/). If you don't use Conventional Commits when making a commit, we will revise the pull request title to follow that format, as we use squash merges when merging pull requests, so all other commits in the pull diff --git a/index.qmd b/index.qmd index eeea73e..3df257f 100644 --- a/index.qmd +++ b/index.qmd @@ -43,12 +43,10 @@ including for developing the package. - Uses [Quarto](https://quarto.org/) Markdown for the website content, allowing for easy integration of code, text, and figures. - Uses - [Commitizen](https://decisions.seedcase-project.org/why-changelog-with-commitizen/) - to - [check](https://decisions.seedcase-project.org/why-lint-with-commitizen/) - commit messages and automatically create the changelog. -- Automated Git tagging and GitHub releases with - [commitizen](https://decisions.seedcase-project.org/why-semantic-release-with-commitizen/) + [git-cliff](https://decisions.seedcase-project.org/why-changelog-with-git-cliff/) + to automatically create the changelog. +- Automates Git tagging and GitHub releases with + [Cocogitto](https://decisions.seedcase-project.org/why-semantic-release-with-cocogitto/) that are based on messages following [Conventional Commits](https://decisions.seedcase-project.org/why-conventional-commits/). - Uses a [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/) diff --git a/template/.config/cliff.toml b/template/.config/cliff.toml new file mode 100644 index 0000000..40f947a --- /dev/null +++ b/template/.config/cliff.toml @@ -0,0 +1,134 @@ +[remote] +# Strictly don't connect to the internet to generate the changelog. +offline = false + +[remote.github] +# TODO: Fill in the owner and repo so that the links in the changelog work correctly. +owner = "" +repo = "" + +[changelog] +# A Tera template to be rendered for each release in the changelog. +header = """ +# Changelog + +Since we follow +[Conventional Commits](https://decisions.seedcase-project.org/why-conventional-commits/) +for commit messages, we can automatically create +releases of the Python package based on those messages. The +releases are also published to Zenodo for easier discovery, archiving, +and citation. + +We use +[Cocogitto](https://decisions.seedcase-project.org/why-semantic-release-with-cocogitto/) +to automate releases, which uses +[SemVar](https://semverdoc.org) as the version numbering scheme, +and [Git Cliff](https://decisions.seedcase-project.org/why-changelog-with-git-cliff/) +to generate the changelog from commit messages. + +Because releases are generated automatically, new versions are released +often---sometimes several times in a day--- +and each release usually contains only a small number of changes. Below +is a list of the releases and the changes +within each one. + +Commits from bots, like `dependabot` or `pre-commit-ci`, are not included in +the changelog. +""" + +body = """ +{%- macro remote_url() -%} + https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} +{%- endmacro -%} + +{% macro print_commit(commit) -%} + - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ + {% if commit.breaking %}**breaking** {% endif %}\ + {{ commit.message | upper_first }} \ + {% if commit.remote.username %} by \ + {% if commit.remote.username is containing("[bot]") %} + `@{{ commit.remote.username }}`\ + {% else %}\ + [`@{{ commit.remote.username }}`](https://github.com/{{ commit.remote.username }})\ + {% endif %}\ + {% endif %} \ + ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ +{% endmacro -%} + +{% if version %}\ + {% if previous.version %}\ + ## [{{ version | trim_start_matches(pat="v") }}]\ + ({{ self::remote_url() }}/compare/{{ previous.version }}..{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }} + {% else %}\ + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} + {% endif %}\ +{% else %}\ + ## [unreleased] +{% endif %}\ + +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | striptags | trim | upper_first }} + {% for commit in commits + | filter(attribute="scope") + | sort(attribute="scope") %} + {{ self::print_commit(commit=commit) }} + {%- endfor %} + {% for commit in commits %} + {%- if not commit.scope -%} + {{ self::print_commit(commit=commit) }} + {% endif -%} + {% endfor -%} +{% endfor -%} + +{%- if github -%} +{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %} + ### โค๏ธ New contributors +{% endif %}\ +{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %} + {% if contributor.username is containing("[bot]") %} + - `@{{ contributor.username }}` started making automated contributions\ + {% else %}\ + - [`@{{ contributor.username }}`](https://github.com/{{ contributor.username }}) made their first contribution + {%- if contributor.pr_number %} in \ + [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }})\ + {%- endif %} + {%- endif %}\ +{%- endfor -%} +{%- endif %} + +""" + +# Remove leading and trailing whitespaces from the changelog's body. +trim = true +output = "CHANGELOG.md" + +[git] +commit_preprocessors = [ + # TODO: Replace OWNER and REPO with actual owner and repo + # Replace pull request numbers with links to GitHub. + { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "[#${2}](https://github.com/OWNER/REPO/pull/${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 = 'uvx typos --write-changes -' }, + # Remove gitmoji, both actual UTF emoji and :emoji: + { pattern = ' *(:\w+:|[\p{Emoji_Presentation}\p{Extended_Pictographic}](?:\u{FE0F})?\u{200D}?) *', replace = "" }, +] + +commit_parsers = [ + # Don't include commits from bots. + { field = "author.name", pattern = ".*(dependabot|github-actions|pre-commit-ci).*", skip = true }, + # Don't include the version update commits. + { message = ".*update version", skip = true }, + { message = "^feat", group = "โœจ Features" }, + { message = "^fix", group = "๐Ÿ› Fixes" }, + { message = "^refactor", group = "โ™ป๏ธ Refactor" }, + { message = "^docs", group = "๐Ÿ“ Documentation" }, + { message = "^perf", group = "โšก Performance" }, + { message = "^style", group = "๐Ÿ’„ Style" }, + { message = "^test", group = "๐Ÿงช Tests" }, + { message = "^ci", group = "๐Ÿ‘ท CI/CD" }, + { message = "^build", group = "๐Ÿงฑ Build system" }, + { message = "^chore", group = "๐Ÿงน Chores" }, + { message = "^revert", group = "โช Revert" }, + { message = ".*", skip = true }, +] diff --git a/template/.config/cog.toml b/template/.config/cog.toml new file mode 100644 index 0000000..37117a8 --- /dev/null +++ b/template/.config/cog.toml @@ -0,0 +1,18 @@ +from_latest_tag = true +disable_changelog = true +disable_bump_commit = true +branch_whitelist = ["main"] +pre_bump_hooks = [ + # Quiet the log output of git-cliff, it is noisy. + "RUST_LOG='none' uvx git-cliff --tag {{version}}", + "uvx rumdl fmt CHANGELOG.md --silent", + "uv version {{version}}", + "git commit CHANGELOG.md pyproject.toml uv.lock -m 'build: ๐Ÿ”– update version to {{version}} [skip ci]'", +] +post_bump_hooks = ["git push", "git push --tags"] + +[commit_types] +refactor = { bump_patch = true } +perf = { bump_patch = true } +fix = { bump_patch = true } +feat = { bump_minor = true } diff --git a/template/.cz.toml b/template/.cz.toml deleted file mode 100644 index ff8c8fb..0000000 --- a/template/.cz.toml +++ /dev/null @@ -1,6 +0,0 @@ -[tool.commitizen] -bump_message = "build(version): :bookmark: update version from $current_version to $new_version" -update_changelog_on_bump = true -version_provider = "uv" -# Don't regenerate the changelog on every update -changelog_incremental = true diff --git a/template/.github/workflows/release-package.yml b/template/.github/workflows/release-package.yml deleted file mode 100644 index 8aef6bc..0000000 --- a/template/.github/workflows/release-package.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Release package - -on: - push: - branches: - - main - -# Limit token permissions for security -permissions: read-all - -jobs: - release: - # This job outputs env variables `previous_version` and `current_version`. - # Only give permissions for this job. - permissions: - contents: write - uses: seedcase-project/.github/.github/workflows/reusable-release-project.yml@main - with: - app-id: ${{ vars.UPDATE_VERSION_APP_ID }} - secrets: - update-version-gh-token: ${{ secrets.UPDATE_VERSION_TOKEN }} - - pypi-publish: - name: Publish to PyPI - runs-on: ubuntu-latest - # Only give permissions for this job. - permissions: - # IMPORTANT: mandatory for trusted publishing. - id-token: write - environment: - name: pypi - needs: - - release - if: ${{ needs.release.outputs.previous_version != needs.release.outputs.current_version }} - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 - with: - egress-policy: audit - - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - # Need to explicitly get the current version, otherwise it defaults to current commit - # (which is not the same as the release/version commit). - ref: ${{ needs.release.outputs.current_version }} - - # This workflow and the publish workflows are based on: - # - https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/ - # - https://www.andrlik.org/dispatches/til-use-uv-for-build-and-publish-github-actions/ - # - https://github.com/astral-sh/trusted-publishing-examples - - name: Set up uv - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 - - - name: Build distributions - # Builds dists from source and stores them in the dist/ directory. - run: uv build - - - name: Publish ๐Ÿ“ฆ to PyPI - # Only publish if the option is explicitly set in the calling workflow. - run: uv publish --trusted-publishing always diff --git a/template/.github/workflows/release.yml b/template/.github/workflows/release.yml new file mode 100644 index 0000000..df58f15 --- /dev/null +++ b/template/.github/workflows/release.yml @@ -0,0 +1,137 @@ +name: Release + +on: + push: + branches: + - main + +# Limit token permissions for security +permissions: read-all + +jobs: + github-release: + if: "!startsWith(github.event.head_commit.message, 'build: ๐Ÿ”– update version')" + runs-on: ubuntu-latest + # To generate releases, this job needs write access to the repository contents. + permissions: + contents: write + # Can only release one version at a time, so need to stop any other jobs that + # are also trying to release, to prevent conflicts. + concurrency: + group: release-group + cancel-in-progress: true + # Used by the publishing job + outputs: + has_changes: ${{ steps.check_changes.outputs.has_changes }} + current_version: ${{ steps.create_release.outputs.current_version }} + steps: + # This is a useful security step to check for unexpected outbound calls from the runner, + # which could indicate a compromised token or runner. + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 + with: + egress-policy: audit + + # Using this security pattern for GitHub Apps is recommended by GitHub and ensures that + # the token is only available for a short time and has limited permissions. Check out + # for more details. + - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + id: app-token + with: + client-id: ${{ vars.UPDATE_VERSION_APP_ID }} + private-key: ${{ secrets.UPDATE_VERSION_TOKEN }} + + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + # Only need the last commit from the repo. + fetch-depth: 0 + # Requires the token in order to push changes to the repo for the release. + token: ${{ steps.app-token.outputs.token }} + + # Set this for the bot user who will make the release commit. + - name: Set bot user + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Install Cocogitto + uses: cocogitto/cocogitto-action@9a9fe03b31c47444290c0d7f9b1ee1b44ee13f20 # v4.1.0 + with: + command: check + + # Install uv to use git-cliff and rumdl + - name: Set up uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + enable-cache: true + + - name: Check if there are releasable changes + continue-on-error: true + id: check_changes + run: | + # Determine if a bump is possible. + if [[ $(cog bump --auto --dry-run --config .config/cog.toml) != No* ]]; then + echo "has_changes=true" >> $GITHUB_OUTPUT + else + echo "has_changes=false" >> $GITHUB_OUTPUT + fi + + - name: Create tag and update changelog + if: steps.check_changes.outputs.has_changes == 'true' + run: | + cog bump --auto --config .config/cog.toml + + - name: Create GitHub release + id: create_release + if: steps.check_changes.outputs.has_changes == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + version=$(cog get-version) + # Remove logging from git-cliff. + RUST_LOG='none' uvx git-cliff --latest --output RELEASE_NOTES.md --strip all + gh release create "${version}" \ + --title "Release ${version}" \ + --notes-file RELEASE_NOTES.md + echo "current_version=${version}" >> $GITHUB_OUTPUT + + pypi-publish: + name: Publish to PyPI + runs-on: ubuntu-latest + # Only give permissions for this job. + permissions: + # IMPORTANT: Mandatory for trusted publishing. + id-token: write + environment: + name: pypi + needs: + - github-release + if: ${{ needs.github-release.outputs.has_changes }} + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + # Need to explicitly get the current version, otherwise it defaults to current commit + # (which is not the same as the release/version commit). + ref: ${{ needs.github-release.outputs.current_version }} + + # This workflow and the publish workflows are based on: + # - https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/ + # - https://www.andrlik.org/dispatches/til-use-uv-for-build-and-publish-github-actions/ + # - https://github.com/astral-sh/trusted-publishing-examples + - name: Set up uv + uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1 + + - name: Build distributions + # Builds dists from source and stores them in the dist/ directory. + run: uv build + + - name: Publish ๐Ÿ“ฆ to PyPI + # Only publish if the option is explicitly set in the calling workflow. + run: uv publish --trusted-publishing always diff --git a/template/.pre-commit-config.yaml b/template/.pre-commit-config.yaml index a863a32..9c8e660 100644 --- a/template/.pre-commit-config.yaml +++ b/template/.pre-commit-config.yaml @@ -18,11 +18,6 @@ repos: - id: check-merge-conflict args: [--assume-in-merge] - - repo: https://github.com/commitizen-tools/commitizen - rev: v4.13.7 - hooks: - - id: commitizen - # Use the mirror since the main `typos` repo has tags for different # sub-packages, which confuses pre-commit when it tries to find the latest # version diff --git a/template/CHANGELOG.md b/template/CHANGELOG.md index ea23039..86afa39 100644 --- a/template/CHANGELOG.md +++ b/template/CHANGELOG.md @@ -1,17 +1 @@ -# Changelog - -Since we follow -[Conventional Commits](https://decisions.seedcase-project.org/why-conventional-commits/) -when writing commit messages, we're able to automatically create formal -releases of the Python package based on the commit messages. The -releases are also published to Zenodo for easier discovery, archival, -and citation purposes. We use -[Commitizen](https://decisions.seedcase-project.org/why-semantic-release-with-commitizen/) -to be able to automatically create these releases, which uses -[SemVar](https://semverdoc.org) as the version numbering scheme. - -Because releases are created based on commit messages, we release quite -often, sometimes several times in a day. This also means that any -individual release will not have many changes within it. Below is a list -of the releases we've made so far, along with what was changed within -each release. + diff --git a/template/CONTRIBUTING.md.jinja b/template/CONTRIBUTING.md.jinja index ae4a2bf..2b8108f 100644 --- a/template/CONTRIBUTING.md.jinja +++ b/template/CONTRIBUTING.md.jinja @@ -51,7 +51,7 @@ When committing changes, please try to follow [Conventional Commits](https://decisions.seedcase-project.org/why-conventional-commits/) as Git messages. Using this convention allows us to be able to automatically create a release based on the commit message by using -[Commitizen](https://decisions.seedcase-project.org/why-semantic-release-with-commitizen/). +[Cocogitto](https://decisions.seedcase-project.org/why-semantic-release-with-cocogitto/). If you don't use Conventional Commits when making a commit, we will revise the pull request title to follow that format, as we use [squash merges](https://git-scm.com/docs/git-merge) when merging pull requests, diff --git a/template/README.qmd b/template/README.qmd index ee40fe8..a598fed 100644 --- a/template/README.qmd +++ b/template/README.qmd @@ -40,9 +40,9 @@ Seedcase template :tada: - `.copier-answers.yml`: Contains the answers you gave when copying the project from the template. **You should not modify this file directly.** -- `.cz.toml`: - [Commitizen](https://commitizen-tools.github.io/commitizen/) - configuration file for managing versions and changelogs. +- `.config/cog.toml`: + [Cocogitto](https://docs.cocogitto.io) configuration file for managing + versions and changelogs. - `.pre-commit-config.yaml`: [Pre-commit](https://pre-commit.com/) configuration file for managing and running checks before each commit. - `.typos.toml`: [typos](https://github.com/crate-ci/typos) spell diff --git a/template/justfile.jinja b/template/justfile.jinja index 6b9c91c..a1e0d08 100644 --- a/template/justfile.jinja +++ b/template/justfile.jinja @@ -84,19 +84,6 @@ build-website: build-quartodoc preview-website: build-quartodoc uv run quarto preview --execute -# Check the commit messages on the current branch that are not on the main branch -check-commits: - #!/usr/bin/env bash - branch_name=$(git rev-parse --abbrev-ref HEAD) - number_of_commits=$(git rev-list --count HEAD ^main) - if [[ ${branch_name} != "main" && ${number_of_commits} -gt 0 ]] - then - # If issue happens, try `uv tool update-shell` - uvx --from commitizen cz check --rev-range main..HEAD - else - echo "On 'main' or current branch doesn't have any commits." - fi - # Run basic security checks on the package check-security: uvx bandit -r src/