Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0 # setuptools-scm needs the tags
- uses: actions/setup-python@v6
with:
python-version: "3.13"
Expand Down
44 changes: 30 additions & 14 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
name: Publish

# Trusted Publishing: PyPI and TestPyPI trust this workflow, so no API token is stored.
# Run by hand (Actions → Publish → Run workflow) → TestPyPI.
# Push a tag v<version> → PyPI.
# Deployment environments are not available in this repository. The "release tags" ruleset
# lets only repository admins create v* tags, so only they can release to PyPI.
# A GitHub release published on a v* tag → PyPI, and the files are attached to the release.
# Run by hand on a branch (Actions → Publish) → a .devN version on TestPyPI.
# The Tag workflow creates the v* tags on merges to main. The version comes from the tag
# (setuptools-scm).

on:
push:
tags: ["v*"]
release:
types: [published]
workflow_dispatch:

permissions:
Expand All @@ -19,18 +19,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0 # setuptools-scm needs the tags
- uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Tag matches the version in pyproject.toml
- run: python -m pip install --upgrade build twine
- run: python -m build
- name: Built version matches the tag
if: startsWith(github.ref, 'refs/tags/v')
run: |
version=$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')
if [ "v$version" != "$GITHUB_REF_NAME" ]; then
echo "::error::Tag $GITHUB_REF_NAME does not match version $version in pyproject.toml"; exit 1
if [ ! -f "dist/mfiles_grpc-${GITHUB_REF_NAME#v}.tar.gz" ]; then
echo "::error::Tag $GITHUB_REF_NAME, but built $(ls dist)"; exit 1
fi
- run: python -m pip install --upgrade build twine
- run: python -m build
- run: twine check --strict dist/*
# The package must never carry the .proto itself, only the stubs generated from it.
- name: No .proto in the package
Expand All @@ -47,7 +48,7 @@ jobs:
path: dist/

testpypi:
if: github.event_name == 'workflow_dispatch'
if: github.event_name == 'workflow_dispatch' && !startsWith(github.ref, 'refs/tags/')
needs: build
runs-on: ubuntu-latest
permissions:
Expand All @@ -62,7 +63,7 @@ jobs:
repository-url: https://test.pypi.org/legacy/

pypi:
if: startsWith(github.ref, 'refs/tags/v')
if: github.event_name == 'release' && startsWith(github.ref, 'refs/tags/v')
needs: build
runs-on: ubuntu-latest
permissions:
Expand All @@ -73,3 +74,18 @@ jobs:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1

attach:
needs: pypi
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v5
with:
name: dist
path: dist/
- run: gh release upload "$GITHUB_REF_NAME" dist/*
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
71 changes: 71 additions & 0 deletions .github/workflows/tag.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: Tag

# When a pull request is merged to main, tag the merge commit with the next version. The pull
# request's labels choose the step:
# none → patch 1.2.3 → 1.2.4
# minor-version → minor 1.2.3 → 1.3.0
# major-version → major 1.2.3 → 2.0.0
# The first tag is v1.0.0.
#
# pull_request_target runs this file as it is on main, with a token that can write. It never
# checks out or runs the pull request's code; it reads only the labels and the merge commit.
# The "release tags" ruleset stops v* tags from being moved or deleted, but not created:
# GITHUB_TOKEN cannot be given a ruleset bypass.
#
# Tagging publishes nothing: creating a GitHub release on the tag starts Publish.

on:
pull_request_target:
types: [closed]
branches: [main]

permissions: {}

concurrency:
group: tag
cancel-in-progress: false

jobs:
tag:
if: github.event.pull_request.merged
runs-on: ubuntu-latest
permissions:
contents: write
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
SHA: ${{ github.event.pull_request.merge_commit_sha }}
LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }}
steps:
- name: Next version
id: next
run: |
gh api --paginate "repos/$GH_REPO/git/matching-refs/tags/v" -q '.[] | .ref + " " + .object.sha' |
python3 -c '
import json, os, re, sys
labels = set(json.loads(os.environ["LABELS"]))
versions = []
for line in sys.stdin:
ref, sha = line.split()
if m := re.fullmatch(r"refs/tags/v(\d+)\.(\d+)\.(\d+)", ref):
if sha == os.environ["SHA"]:
sys.exit(f"::error::{ref} already tags {sha}")
versions.append(tuple(map(int, m.groups())))
if not versions:
major, minor, patch = 1, 0, 0
else:
major, minor, patch = max(versions)
if "major-version" in labels:
major, minor, patch = major + 1, 0, 0
elif "minor-version" in labels:
minor, patch = minor + 1, 0
else:
patch += 1
print(f"tag=v{major}.{minor}.{patch}")
' >> "$GITHUB_OUTPUT"
- name: Create the tag
run: |
gh api "repos/$GH_REPO/git/refs" -f ref="refs/tags/$TAG" -f sha="$SHA" --silent
echo "Tagged $SHA as $TAG" >> "$GITHUB_STEP_SUMMARY"
env:
TAG: ${{ steps.next.outputs.tag }}
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,28 @@ flake8 --max-line-length 120 --extend-exclude src/mfiles_grpc/_generated src tes

Offline; they need no vault. `scripts/live_object_test.py` is the live check.

## Releasing

Versions are never set by hand: every pull request merged to `main` gets the next one.

1. The Tag workflow tags the merge commit with the next version. The pull request's labels
choose the step:

| Label | Step | Example |
| --- | --- | --- |
| none | patch | 1.2.3 → 1.2.4 |
| `minor-version` | minor | 1.2.3 → 1.3.0 |
| `major-version` | major | 1.2.3 → 2.0.0 |

The first tag is `v1.0.0`.
2. To release, create a GitHub release on that tag (Releases → Draft a new release). Publishing
it starts the Publish workflow, which builds the package, publishes it to PyPI and attaches
the wheel and the sdist to the release. A tag without a release is not published.

The package version comes from the tag ([setuptools-scm](https://setuptools-scm.readthedocs.io/)),
so `pyproject.toml` holds none. Running Publish by hand on a branch publishes a `.devN` version
to TestPyPI.

## License

MIT; see [LICENSE](https://github.com/M-Files/mfiles-grpc-python/blob/main/LICENSE).
10 changes: 8 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
[build-system]
requires = ["setuptools>=77.0"]
requires = ["setuptools>=77.0", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"

[project]
name = "mfiles-grpc"
version = "0.1.0"
dynamic = ["version"]
description = "Python client for the M-Files gRPC API"
readme = "README.md"
license = "MIT"
Expand Down Expand Up @@ -55,5 +55,11 @@ mfiles-grpc = "mfiles_grpc.__main__:main"
[tool.setuptools.packages.find]
where = ["src"]

[tool.setuptools_scm]
# The version is the latest v* tag, which the Tag workflow creates on every merge to main.
# Between tags it is a .devN version; the local "+g<hash>" part is dropped because PyPI
# and TestPyPI refuse it.
local_scheme = "no-local-version"

[tool.pytest.ini_options]
testpaths = ["tests"]
Loading