Skip to content

Commit 0711683

Browse files
committed
ci(workflows): add semantic-release pipeline and PR commit linting
1 parent d7ac93b commit 0711683

File tree

5 files changed

+179
-16
lines changed

5 files changed

+179
-16
lines changed

.commitlintrc.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
extends:
2+
- '@commitlint/config-conventional'
3+
4+
rules:
5+
type-enum:
6+
- 2
7+
- always
8+
- - build
9+
- chore
10+
- ci
11+
- docs
12+
- feat
13+
- fix
14+
- perf
15+
- refactor
16+
- revert
17+
- style
18+
- test
19+
scope-case:
20+
- 0
21+
subject-case:
22+
- 0
23+
header-max-length:
24+
- 1
25+
- always
26+
- 100
27+
body-max-line-length:
28+
- 0
29+
footer-max-line-length:
30+
- 0

.github/workflows/pr-lint.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: PR Lint
2+
3+
on:
4+
pull_request:
5+
types: [opened, edited, synchronize, reopened]
6+
7+
permissions:
8+
pull-requests: read
9+
10+
jobs:
11+
pr-title:
12+
name: PR title (Conventional Commits)
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Validate PR title
16+
uses: amannn/action-semantic-pull-request@v5
17+
env:
18+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
19+
with:
20+
types: |
21+
build
22+
chore
23+
ci
24+
docs
25+
feat
26+
fix
27+
perf
28+
refactor
29+
revert
30+
style
31+
test
32+
requireScope: false
33+
subjectPattern: ^[a-z].+[^.]$
34+
subjectPatternError: |
35+
Subject "{subject}" must start with a lowercase letter and must not
36+
end with a period.
37+
Example: "feat(cli): add init subcommand"
38+
39+
commits:
40+
name: Commit messages
41+
runs-on: ubuntu-latest
42+
steps:
43+
- uses: actions/checkout@v4
44+
with:
45+
fetch-depth: 0
46+
- uses: wagoid/commitlint-github-action@v6

.github/workflows/release.yml

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,77 @@
1-
name: Release to PyPI
1+
name: Release
22

33
on:
44
push:
5-
tags:
6-
- 'v*.*.*'
5+
branches: [main]
6+
workflow_dispatch:
7+
8+
# ┌─────────────────────────────────────────────────────────────────┐
9+
# │ DRAFT_RELEASE │
10+
# │ │
11+
# │ "true" → GitHub Releases are created as drafts and PyPI │
12+
# │ publishing is skipped (burn-in / review period). │
13+
# │ "false" → GitHub Releases are published immediately and the │
14+
# │ package is uploaded to PyPI. │
15+
# │ │
16+
# │ Flip to "false" once you have verified the release output. │
17+
# └─────────────────────────────────────────────────────────────────┘
18+
env:
19+
DRAFT_RELEASE: "true"
720

821
jobs:
9-
build-and-publish:
22+
release:
23+
name: Semantic Release
1024
runs-on: ubuntu-latest
25+
if: >-
26+
github.event_name == 'workflow_dispatch' ||
27+
!startsWith(github.event.head_commit.message, 'chore(release):')
28+
concurrency:
29+
group: release
30+
cancel-in-progress: false
1131
permissions:
12-
contents: read
32+
contents: write
1333
id-token: write
34+
1435
steps:
1536
- name: Checkout
1637
uses: actions/checkout@v4
38+
with:
39+
fetch-depth: 0
1740

1841
- name: Set up Python
1942
uses: actions/setup-python@v5
2043
with:
21-
python-version: '3.9'
44+
python-version: '3.12'
45+
46+
- name: Install build tools
47+
run: python -m pip install -U pip build
48+
49+
- name: Python Semantic Release
50+
id: release
51+
uses: python-semantic-release/python-semantic-release@v9
52+
with:
53+
github_token: ${{ secrets.GITHUB_TOKEN }}
54+
55+
- name: Build package
56+
if: steps.release.outputs.released == 'true'
57+
run: python -m build
2258

23-
- name: Build sdist and wheel
59+
- name: Create GitHub Release
60+
if: steps.release.outputs.released == 'true'
61+
env:
62+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2463
run: |
25-
python -m pip install -U pip build
26-
python -m build
64+
if [ "$DRAFT_RELEASE" = "true" ]; then
65+
DRAFT_FLAG="--draft"
66+
fi
67+
gh release create "${{ steps.release.outputs.tag }}" \
68+
${DRAFT_FLAG:-} \
69+
--title "${{ steps.release.outputs.tag }}" \
70+
--generate-notes \
71+
dist/*
2772
28-
- name: Publish package to PyPI
73+
- name: Publish to PyPI
74+
if: steps.release.outputs.released == 'true' && env.DRAFT_RELEASE != 'true'
2975
uses: pypa/gh-action-pypi-publish@release/v1
3076
with:
3177
skip-existing: true

CONTRIBUTING.md

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -205,19 +205,26 @@ Co-authored-by: Name <email>
205205

206206
## Pull request checklist
207207

208+
- PR title: Conventional Commits format (CI-enforced by `pr-lint.yml`).
208209
- Tests: added/updated; `pytest` passes.
209210
- Lint/format: `ruff check .`, `black` pass.
210-
- Docs: update `README.md` and any Django docs pages if behavior changes.
211+
- Docs: update `README.md` if behavior changes.
211212
- Templates: update `templates/` if generator output changes.
212213
- No generated artifacts committed.
213214

214215
## Versioning and releases
215216

216-
- The library version is tracked in `pyproject.toml` (`project.version`). Use SemVer.
217-
- Workflow:
218-
- Contributors: branch off `main` (or `dev` if used) and open PRs.
219-
- Maintainer (release): bump version, tag, and publish to PyPI.
220-
- Tag on `main`: `git tag -a vX.Y.Z -m "Release vX.Y.Z" && git push --tags`.
217+
- The version is tracked in `pyproject.toml` (`project.version`) and mirrored in `src/pythonnative/__init__.py` as `__version__`. Both files are updated automatically by [python-semantic-release](https://python-semantic-release.readthedocs.io/).
218+
- **Automated release pipeline** (on every merge to `main`):
219+
1. `python-semantic-release` scans Conventional Commit messages since the last tag.
220+
2. It determines the next SemVer bump: `feat`**minor**, `fix`/`perf`**patch**, `BREAKING CHANGE`**major** (minor while version < 1.0).
221+
3. Version files are updated, `CHANGELOG.md` is generated, and a tagged release commit (`chore(release): vX.Y.Z`) is pushed.
222+
4. A GitHub Release is created with auto-generated release notes and the built sdist/wheel attached.
223+
5. When drafts are disabled, the package is also published to PyPI via Trusted Publishing.
224+
- **Draft / published toggle**: the `DRAFT_RELEASE` variable at the top of `.github/workflows/release.yml` controls release mode. Set to `"true"` (the default) for draft GitHub Releases with PyPI publishing skipped; flip to `"false"` to publish releases and upload to PyPI immediately.
225+
- Commit types that trigger a release: `feat` (minor), `fix` and `perf` (patch), `BREAKING CHANGE` (major). All other types (`build`, `chore`, `ci`, `docs`, `refactor`, `revert`, `style`, `test`) are recorded in the changelog but do **not** trigger a release on their own.
226+
- Tag format: `v`-prefixed (e.g., `v0.4.0`).
227+
- Manual version bumps are no longer needed — just merge PRs with valid Conventional Commit titles. For ad-hoc runs, use the workflow's **Run workflow** button (`workflow_dispatch`).
221228

222229
### Branch naming (suggested)
223230

@@ -249,6 +256,13 @@ release/v0.2.0
249256
hotfix/cli-regression
250257
```
251258

259+
### CI
260+
261+
- **CI** (`ci.yml`): runs formatter, linter, type checker, and tests on every push and PR.
262+
- **PR Lint** (`pr-lint.yml`): validates the PR title against Conventional Commits format (protects squash merges) and checks individual commit messages via commitlint (protects rebase merges). Recommended: add the **PR title** job as a required status check in branch-protection settings.
263+
- **Release** (`release.yml`): runs on merge to `main`; computes version, generates changelog, tags, creates GitHub Release, and (when `DRAFT_RELEASE` is `"false"`) publishes to PyPI.
264+
- **Docs** (`docs.yml`): deploys documentation to GitHub Pages on push to `main`.
265+
252266
## Security and provenance
253267

254268
- Avoid bundling secrets or credentials in templates or code.

pyproject.toml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,30 @@ ignore = []
8888
[tool.black]
8989
line-length = 120
9090
target-version = ['py39']
91+
92+
# ── Semantic Release ────────────────────────────────────────────────
93+
94+
[tool.semantic_release]
95+
version_toml = ["pyproject.toml:project.version"]
96+
version_variables = ["src/pythonnative/__init__.py:__version__"]
97+
commit_message = "chore(release): v{version}"
98+
tag_format = "v{version}"
99+
major_on_zero = false
100+
101+
[tool.semantic_release.branches.main]
102+
match = "main"
103+
prerelease = false
104+
105+
[tool.semantic_release.changelog]
106+
changelog_file = "CHANGELOG.md"
107+
exclude_commit_patterns = [
108+
"^chore\\(release\\):",
109+
]
110+
111+
[tool.semantic_release.commit_parser_options]
112+
allowed_tags = [
113+
"build", "chore", "ci", "docs", "feat", "fix",
114+
"perf", "refactor", "revert", "style", "test",
115+
]
116+
minor_tags = ["feat"]
117+
patch_tags = ["fix", "perf"]

0 commit comments

Comments
 (0)