Skip to content

Commit deea47b

Browse files
lwjohnst86signekbpre-commit-ci[bot]
authored
refactor: 👷 switch to using Cocogitto and git-cliff for releasing (#282)
# Description This switches to using Cocogitto and git-cliff for releasing and changelog. I haven't tested the publishing to PyPI yet, but it should (TM) work. Needs a thorough review. ## Checklist - [x] Ran `just run-all` --------- Co-authored-by: Signe Kirk Brødbæk <40836345+signekb@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent d64df38 commit deea47b

12 files changed

Lines changed: 299 additions & 113 deletions

File tree

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ When committing changes, please try to follow
5454
[Conventional Commits](https://decisions.seedcase-project.org/why-conventional-commits/)
5555
as Git messages. Using this convention allows us to be able to
5656
automatically create a release based on the commit message by using
57-
[Commitizen](https://decisions.seedcase-project.org/why-semantic-release-with-commitizen/).
57+
[Cocogitto](https://decisions.seedcase-project.org/why-semantic-release-with-cocogitto/).
5858
If you don't use Conventional Commits when making a commit, we will
5959
revise the pull request title to follow that format, as we use squash
6060
merges when merging pull requests, so all other commits in the pull

index.qmd

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,10 @@ including for developing the package.
4343
- Uses [Quarto](https://quarto.org/) Markdown for the website content,
4444
allowing for easy integration of code, text, and figures.
4545
- Uses
46-
[Commitizen](https://decisions.seedcase-project.org/why-changelog-with-commitizen/)
47-
to
48-
[check](https://decisions.seedcase-project.org/why-lint-with-commitizen/)
49-
commit messages and automatically create the changelog.
50-
- Automated Git tagging and GitHub releases with
51-
[commitizen](https://decisions.seedcase-project.org/why-semantic-release-with-commitizen/)
46+
[git-cliff](https://decisions.seedcase-project.org/why-changelog-with-git-cliff/)
47+
to automatically create the changelog.
48+
- Automates Git tagging and GitHub releases with
49+
[Cocogitto](https://decisions.seedcase-project.org/why-semantic-release-with-cocogitto/)
5250
that are based on messages following
5351
[Conventional Commits](https://decisions.seedcase-project.org/why-conventional-commits/).
5452
- Uses a [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/)

template/.config/cliff.toml

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
[remote]
2+
# Strictly don't connect to the internet to generate the changelog.
3+
offline = false
4+
5+
[remote.github]
6+
# TODO: Fill in the owner and repo so that the links in the changelog work correctly.
7+
owner = ""
8+
repo = ""
9+
10+
[changelog]
11+
# A Tera template to be rendered for each release in the changelog.
12+
header = """
13+
# Changelog
14+
15+
Since we follow
16+
[Conventional Commits](https://decisions.seedcase-project.org/why-conventional-commits/)
17+
for commit messages, we can automatically create
18+
releases of the Python package based on those messages. The
19+
releases are also published to Zenodo for easier discovery, archiving,
20+
and citation.
21+
22+
We use
23+
[Cocogitto](https://decisions.seedcase-project.org/why-semantic-release-with-cocogitto/)
24+
to automate releases, which uses
25+
[SemVar](https://semverdoc.org) as the version numbering scheme,
26+
and [Git Cliff](https://decisions.seedcase-project.org/why-changelog-with-git-cliff/)
27+
to generate the changelog from commit messages.
28+
29+
Because releases are generated automatically, new versions are released
30+
often---sometimes several times in a day---
31+
and each release usually contains only a small number of changes. Below
32+
is a list of the releases and the changes
33+
within each one.
34+
35+
Commits from bots, like `dependabot` or `pre-commit-ci`, are not included in
36+
the changelog.
37+
"""
38+
39+
body = """
40+
{%- macro remote_url() -%}
41+
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
42+
{%- endmacro -%}
43+
44+
{% macro print_commit(commit) -%}
45+
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
46+
{% if commit.breaking %}**breaking** {% endif %}\
47+
{{ commit.message | upper_first }} \
48+
{% if commit.remote.username %} by \
49+
{% if commit.remote.username is containing("[bot]") %}
50+
`@{{ commit.remote.username }}`\
51+
{% else %}\
52+
[`@{{ commit.remote.username }}`](https://github.com/{{ commit.remote.username }})\
53+
{% endif %}\
54+
{% endif %} \
55+
([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
56+
{% endmacro -%}
57+
58+
{% if version %}\
59+
{% if previous.version %}\
60+
## [{{ version | trim_start_matches(pat="v") }}]\
61+
({{ self::remote_url() }}/compare/{{ previous.version }}..{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
62+
{% else %}\
63+
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
64+
{% endif %}\
65+
{% else %}\
66+
## [unreleased]
67+
{% endif %}\
68+
69+
{% for group, commits in commits | group_by(attribute="group") %}
70+
### {{ group | striptags | trim | upper_first }}
71+
{% for commit in commits
72+
| filter(attribute="scope")
73+
| sort(attribute="scope") %}
74+
{{ self::print_commit(commit=commit) }}
75+
{%- endfor %}
76+
{% for commit in commits %}
77+
{%- if not commit.scope -%}
78+
{{ self::print_commit(commit=commit) }}
79+
{% endif -%}
80+
{% endfor -%}
81+
{% endfor -%}
82+
83+
{%- if github -%}
84+
{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
85+
### ❤️ New contributors
86+
{% endif %}\
87+
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
88+
{% if contributor.username is containing("[bot]") %}
89+
- `@{{ contributor.username }}` started making automated contributions\
90+
{% else %}\
91+
- [`@{{ contributor.username }}`](https://github.com/{{ contributor.username }}) made their first contribution
92+
{%- if contributor.pr_number %} in \
93+
[#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }})\
94+
{%- endif %}
95+
{%- endif %}\
96+
{%- endfor -%}
97+
{%- endif %}
98+
99+
"""
100+
101+
# Remove leading and trailing whitespaces from the changelog's body.
102+
trim = true
103+
output = "CHANGELOG.md"
104+
105+
[git]
106+
commit_preprocessors = [
107+
# TODO: Replace OWNER and REPO with actual owner and repo
108+
# Replace pull request numbers with links to GitHub.
109+
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "[#${2}](https://github.com/OWNER/REPO/pull/${2})" },
110+
# Check spelling of the commit message using https://github.com/crate-ci/typos.
111+
# If the spelling is incorrect, it will be fixed automatically.
112+
{ pattern = '.*', replace_command = 'uvx typos --write-changes -' },
113+
# Remove gitmoji, both actual UTF emoji and :emoji:
114+
{ pattern = ' *(:\w+:|[\p{Emoji_Presentation}\p{Extended_Pictographic}](?:\u{FE0F})?\u{200D}?) *', replace = "" },
115+
]
116+
117+
commit_parsers = [
118+
# Don't include commits from bots.
119+
{ field = "author.name", pattern = ".*(dependabot|github-actions|pre-commit-ci).*", skip = true },
120+
# Don't include the version update commits.
121+
{ message = ".*update version", skip = true },
122+
{ message = "^feat", group = "<!-- 0 -->✨ Features" },
123+
{ message = "^fix", group = "<!-- 1 -->🐛 Fixes" },
124+
{ message = "^refactor", group = "<!-- 2 -->♻️ Refactor" },
125+
{ message = "^docs", group = "<!-- 3 -->📝 Documentation" },
126+
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
127+
{ message = "^style", group = "<!-- 5 -->💄 Style" },
128+
{ message = "^test", group = "<!-- 6 -->🧪 Tests" },
129+
{ message = "^ci", group = "<!-- 7 -->👷 CI/CD" },
130+
{ message = "^build", group = "<!-- 8 -->🧱 Build system" },
131+
{ message = "^chore", group = "<!-- 9 -->🧹 Chores" },
132+
{ message = "^revert", group = "<!-- 10 -->⏪ Revert" },
133+
{ message = ".*", skip = true },
134+
]

template/.config/cog.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from_latest_tag = true
2+
disable_changelog = true
3+
disable_bump_commit = true
4+
branch_whitelist = ["main"]
5+
pre_bump_hooks = [
6+
# Quiet the log output of git-cliff, it is noisy.
7+
"RUST_LOG='none' uvx git-cliff --tag {{version}}",
8+
"uvx rumdl fmt CHANGELOG.md --silent",
9+
"uv version {{version}}",
10+
"git commit CHANGELOG.md pyproject.toml uv.lock -m 'build: 🔖 update version to {{version}} [skip ci]'",
11+
]
12+
post_bump_hooks = ["git push", "git push --tags"]
13+
14+
[commit_types]
15+
refactor = { bump_patch = true }
16+
perf = { bump_patch = true }
17+
fix = { bump_patch = true }
18+
feat = { bump_minor = true }

template/.cz.toml

Lines changed: 0 additions & 6 deletions
This file was deleted.

template/.github/workflows/release-package.yml

Lines changed: 0 additions & 61 deletions
This file was deleted.
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
# Limit token permissions for security
9+
permissions: read-all
10+
11+
jobs:
12+
github-release:
13+
if: "!startsWith(github.event.head_commit.message, 'build: 🔖 update version')"
14+
runs-on: ubuntu-latest
15+
# To generate releases, this job needs write access to the repository contents.
16+
permissions:
17+
contents: write
18+
# Can only release one version at a time, so need to stop any other jobs that
19+
# are also trying to release, to prevent conflicts.
20+
concurrency:
21+
group: release-group
22+
cancel-in-progress: true
23+
# Used by the publishing job
24+
outputs:
25+
has_changes: ${{ steps.check_changes.outputs.has_changes }}
26+
current_version: ${{ steps.create_release.outputs.current_version }}
27+
steps:
28+
# This is a useful security step to check for unexpected outbound calls from the runner,
29+
# which could indicate a compromised token or runner.
30+
- name: Harden the runner (Audit all outbound calls)
31+
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
32+
with:
33+
egress-policy: audit
34+
35+
# Using this security pattern for GitHub Apps is recommended by GitHub and ensures that
36+
# the token is only available for a short time and has limited permissions. Check out
37+
# <https://guidebook.seedcase-project.org/operations/security> for more details.
38+
- uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
39+
id: app-token
40+
with:
41+
client-id: ${{ vars.UPDATE_VERSION_APP_ID }}
42+
private-key: ${{ secrets.UPDATE_VERSION_TOKEN }}
43+
44+
- name: Checkout
45+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
46+
with:
47+
# Only need the last commit from the repo.
48+
fetch-depth: 0
49+
# Requires the token in order to push changes to the repo for the release.
50+
token: ${{ steps.app-token.outputs.token }}
51+
52+
# Set this for the bot user who will make the release commit.
53+
- name: Set bot user
54+
run: |
55+
git config user.name "github-actions[bot]"
56+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
57+
58+
- name: Install Cocogitto
59+
uses: cocogitto/cocogitto-action@9a9fe03b31c47444290c0d7f9b1ee1b44ee13f20 # v4.1.0
60+
with:
61+
command: check
62+
63+
# Install uv to use git-cliff and rumdl
64+
- name: Set up uv
65+
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
66+
with:
67+
enable-cache: true
68+
69+
- name: Check if there are releasable changes
70+
continue-on-error: true
71+
id: check_changes
72+
run: |
73+
# Determine if a bump is possible.
74+
if [[ $(cog bump --auto --dry-run --config .config/cog.toml) != No* ]]; then
75+
echo "has_changes=true" >> $GITHUB_OUTPUT
76+
else
77+
echo "has_changes=false" >> $GITHUB_OUTPUT
78+
fi
79+
80+
- name: Create tag and update changelog
81+
if: steps.check_changes.outputs.has_changes == 'true'
82+
run: |
83+
cog bump --auto --config .config/cog.toml
84+
85+
- name: Create GitHub release
86+
id: create_release
87+
if: steps.check_changes.outputs.has_changes == 'true'
88+
env:
89+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
90+
run: |
91+
version=$(cog get-version)
92+
# Remove logging from git-cliff.
93+
RUST_LOG='none' uvx git-cliff --latest --output RELEASE_NOTES.md --strip all
94+
gh release create "${version}" \
95+
--title "Release ${version}" \
96+
--notes-file RELEASE_NOTES.md
97+
echo "current_version=${version}" >> $GITHUB_OUTPUT
98+
99+
pypi-publish:
100+
name: Publish to PyPI
101+
runs-on: ubuntu-latest
102+
# Only give permissions for this job.
103+
permissions:
104+
# IMPORTANT: Mandatory for trusted publishing.
105+
id-token: write
106+
environment:
107+
name: pypi
108+
needs:
109+
- github-release
110+
if: ${{ needs.github-release.outputs.has_changes }}
111+
steps:
112+
- name: Harden the runner (Audit all outbound calls)
113+
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
114+
with:
115+
egress-policy: audit
116+
117+
- name: Checkout
118+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
119+
with:
120+
# Need to explicitly get the current version, otherwise it defaults to current commit
121+
# (which is not the same as the release/version commit).
122+
ref: ${{ needs.github-release.outputs.current_version }}
123+
124+
# This workflow and the publish workflows are based on:
125+
# - https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/
126+
# - https://www.andrlik.org/dispatches/til-use-uv-for-build-and-publish-github-actions/
127+
# - https://github.com/astral-sh/trusted-publishing-examples
128+
- name: Set up uv
129+
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
130+
131+
- name: Build distributions
132+
# Builds dists from source and stores them in the dist/ directory.
133+
run: uv build
134+
135+
- name: Publish 📦 to PyPI
136+
# Only publish if the option is explicitly set in the calling workflow.
137+
run: uv publish --trusted-publishing always

template/.pre-commit-config.yaml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,6 @@ repos:
2020
- id: check-merge-conflict
2121
args: [--assume-in-merge]
2222

23-
- repo: https://github.com/commitizen-tools/commitizen
24-
rev: v4.13.7
25-
hooks:
26-
- id: commitizen
27-
2823
# Use the mirror since the main `typos` repo has tags for different
2924
# sub-packages, which confuses pre-commit when it tries to find the latest
3025
# version

0 commit comments

Comments
 (0)