pyEuropePMC

CI, branch protection and releases

This page describes how main is protected, what each GitHub Actions workflow does, how dependency updates pass CI, and how a release is made. It reflects the files in .github/workflows/ and the repository settings as of September 2026.

Branch protection on main

main is protected by a repository ruleset named Protect Main (Settings → Rules → Rulesets, or gh api repos/JonasHeinickeBio/pyEuropePMC/rulesets). It enforces:

Rule Effect
deletion main cannot be deleted.
non_fast_forward History on main cannot be rewritten.
pull_request Every change goes through a pull request; direct pushes are rejected. No approvals are required, and merge, squash and rebase merges are all allowed.
required_status_checks A pull request can merge only when these checks pass and the branch is up to date with main (strict_required_status_checks_policy: true): Lint, types & security, Tests & coverage, Unit tests (core deps only), Compatibility Summary and zizmor.

Nobody can bypass the ruleset: bypass_actors is empty. That has two consequences:

Why there is no merge queue

GitHub’s merge queue is available only to repositories owned by an organization. pyEuropePMC belongs to a personal account (JonasHeinickeBio), and the ruleset API rejects a merge_queue rule with Invalid rule 'merge_queue' (HTTP 422), even when it is the only rule in the request.

The ruleset uses the closest substitute: pull requests only, required checks against an up-to-date branch, and auto-merge. What a queue would add, re-testing against a base that changed after the branch was last updated, matters little at this repository’s pull request volume.

If the repository moves to an organization, adding a merge_queue rule to the same ruleset is enough; the workflows already handle the merge_group event.

Workflows

Pull request and push workflows

Workflow File Triggers What it does
CI cdci.yml push to main, pull request, merge group, manual Lint, types & security runs ruff, the format check, mypy, the requirements.txt sync check, poetry check --lock and bandit. Tests & coverage runs the default test suite with all extras, enforces the 75% coverage threshold and uploads reports to Codecov and Coveralls. CodeScene delta (advisory) runs cs delta when the CS_ACCESS_TOKEN secret is available and never fails the workflow.
CI — Light core install unit-tests.yml push to main, pull request, merge group, manual Installs the locked core dependencies and the dev group without any extra, checks that representative optional packages (pandas, matplotlib, flask, langchain, openai, semanticscholar) are absent, and runs pyeuropepmc --help and the default test suite.
Python Version Compatibility Matrix python-compatibility.yml push to main, pull request, merge group, Sundays at 06:00 UTC, manual Compiles and imports the package on Python 3.10 to 3.13, runs the default test suite on Python 3.10 and 3.12 across operating systems, and reports both stages in Compatibility Summary.
Workflow security audit zizmor.yml push to main that changes .github/workflows/, .github/actions/ or .github/zizmor.yml; pull request; merge group; Wednesdays at 05:00 UTC; manual Audits the workflow and action files with zizmor 1.30.1, uploads the results to code scanning and fails on findings of medium severity or higher.
CI — Functional Tests integration-tests.yml push to main, nightly at 03:00 UTC, manual Runs pytest -m functional --run-integration with all extras against the live services. On failure it opens a single issue labelled functional-test-failure, or comments on the open one, and the job fails.
Deploy Documentation to GitHub Pages deploy-docs.yml push to main or pull request that changes docs/ or the workflow, manual Builds docs/ with Jekyll and deploys it to GitHub Pages, except on pull requests.
Pull Request Labeler labeler.yml pull request opened or updated Applies labels according to .github/labeler.yml.
Greetings greetings.yml issue or pull request opened Welcomes first-time contributors; skips the repository owner and bots.
Enhance new issues enhanceIssue.yml issue opened by a person Rewrites the issue’s title and body with actions/ai-inference.
Summarize new issues summary.yml issue opened by a person Comments with a summary generated by actions/ai-inference.

Scheduled and release workflows

Workflow File Triggers What it does
Weekly Benchmarks benchmark.yml Mondays at 02:00 UTC, manual Runs the modular benchmark against the live API and opens a pull request that updates the benchmark section of README.md and .github/benchmark-history.json. A manual run with commit_results set to false opens no pull request.
Feature Suggester analyze_repo.yml Mondays at 03:00 UTC, manual Analyses the repository and files improvement suggestions as issues; see Feature Suggester workflow.
Publish to PyPI release.yml tag v*, manual Publishes a release; see Release process.
Update Changelog changelog.yml tag v*.*.*, manual When CHANGELOG.md has no section for the version, generates one from the commit subjects since the previous tag and opens a pull request with it.
Manage Stale Issues and PRs stale.yml daily at 02:30 UTC, manual Marks issues stale after 60 days without activity and closes them 7 days later; marks pull requests stale after 30 days and closes them 14 days later. Assigned items and items with exempt labels are left alone.

Two further checks have no workflow file. CodeQL runs through GitHub’s default code scanning setup for Python and GitHub Actions. Dependabot opens weekly update pull requests for the Python dependencies and for the actions in .github/workflows/ and .github/actions/setup-python-env/ (.github/dependabot.yml).

Jobs that need the project install it through the composite action .github/actions/setup-python-env. It installs Poetry 2.3.2 and poetry-plugin-export 1.10.0 with pip, exports the locked versions for the requested extras and dependency groups, and installs them into a fresh .venv with uv. poetry lock never runs in CI.

The OS matrix is reduced on pull requests

The config job in python-compatibility.yml computes the operating-system matrix up front:

The gap is real: a Windows-only failure between asyncio and pytest-socket after the MCP server rewrite (#190) passed the pull request checks, broke only on the push to main, and needed a follow-up fix (#202). The merge_group trigger would close the gap by running the full matrix before merging, but without a merge queue nothing sends that event.

Required checks must report on every pull request

A workflow suppressed by a paths: filter never runs, so it never reports its checks, and a required check that never reports leaves a pull request waiting indefinitely for a status. A job skipped by an if: condition reports success instead, as GitHub’s troubleshooting guide for required status checks explains.

Both path-sensitive workflows therefore trigger on every pull request and decide inside a job whether there is work to do:

Before making a job a required check, confirm two things: its workflow triggers on every pull_request without a paths: filter, and the job reports a result even when the work it depends on is skipped.

merge_group triggers

cdci.yml, unit-tests.yml, python-compatibility.yml and zizmor.yml listen for the merge_group event, so their required checks would run on queued pull requests if the repository gets a merge queue. Until then the event never fires.

Updating a dependency

pyproject.toml and poetry.lock are the source of truth. requirements.txt lists the core dependencies exported from the lock, and the Lint, types & security job fails when the committed file differs from a fresh export. The output depends on the poetry-plugin-export version (1.8.0 and 1.10.0 write some environment markers differently), so regenerate the file with the versions CI pins:

uvx --from poetry==2.3.2 --with poetry-plugin-export==1.10.0 poetry export --without-hashes -f requirements.txt -o requirements.txt

The poetry-export-requirements pre-commit hook runs the same export when pyproject.toml, poetry.lock or requirements.txt is committed.

Dependabot changes only pyproject.toml and poetry.lock, so its Python pull requests fail the sync check. Check out the pull request, replacing NUMBER with its number:

gh pr checkout NUMBER

Update the branch with main if it is behind, regenerate requirements.txt with the export command above, commit the result and push it to the same branch.

Release process

release.yml runs when a v* tag is pushed, or when it is dispatched manually (see Dry run on TestPyPI). Its jobs run in this order:

  1. verify first refuses a manual run that asks for pypi from anything but a tag, then runs poetry check --lock, ruff, the format check, mypy and the default test suite on the tagged commit, with all extras on Python 3.12. On a tag it also checks that the tag is v followed by the version in pyproject.toml, and warns when CHANGELOG.md has no section for that version.
  2. build runs poetry build, installs the wheel and the sdist into separate fresh virtual environments, checks that each reports the pyproject.toml version as pyeuropepmc.__version__ and runs pyeuropepmc --help, runs twine check --strict over both (PyPI rejects a long description it cannot render, and README.md is that description), and extracts the version’s changelog section as release notes.
  3. attest creates Sigstore build provenance attestations for the distributions.
  4. publish confirms the target index one last time - pypi.org only from a tag - then uploads the distributions with trusted publishing (OIDC, no API token) and skip-existing: true.
  5. github-release runs for tags only. It creates the GitHub Release with the distributions attached, using the changelog section as the description, with generated notes added from the commit log.
  6. publish-mcp-registry runs for tags only. It sets both version fields in server.json to the tag’s version and publishes the file to the MCP Registry as io.github.JonasHeinickeBio/pyeuropepmc, authenticating with GitHub OIDC. It has continue-on-error: true, so a registry failure does not fail a release that is already on PyPI.

changelog.yml also runs on the tag. It does nothing when CHANGELOG.md already has a section for the version.

Release prerequisites

Before you tag, the commit on main must have:

Making a release

  1. Create a branch, for example chore/release-X.Y.Z, with the version and changelog edits listed above.
  2. If the release changes release.yml, server.json, the mcp-name marker or packaging metadata, run the dry run on TestPyPI from that branch.
  3. Open a pull request titled chore(release): bump to X.Y.Z and merge it when the required checks pass.
  4. Tag the merge commit on main and push the tag, replacing X.Y.Z with the version:

    git switch main
    
    git pull --ff-only
    
    git tag vX.Y.Z
    
    git push origin vX.Y.Z
    
  5. Follow the Publish to PyPI run in the Actions tab, then check the version on PyPI, the GitHub Release and the MCP Registry listing.

Dry run on TestPyPI

Try any change to release.yml, server.json or anything else on the release path before a real tag exists. Dispatch the workflow against your branch; TestPyPI is the default target:

gh workflow run release.yml --ref <branch>

On a branch, github.ref_type is branch, so everything gated on a tag is skipped: the version and changelog checks in verify, github-release and publish-mcp-registry. verify, build, attest and publish run, and publish uploads to TestPyPI. This catches problems such as a wrongly cased MCP Registry namespace, or a mypy setting that fails verify, while the version bump and changelog entry are still easy to change.

A manual run cannot reach pypi.org from a branch. The environment input defaults to testpypi, and -f environment=pypi fails in the first step of verify unless the run is on a v* tag; publish checks the same thing again immediately before the upload. Dispatching a tag with -f environment=pypi is the supported way to retry a publish for a version whose upload failed.

Note: the pypi GitHub environment has no required reviewers and no branch restriction, so those two checks in release.yml are what stands between a dispatch and a permanent version number on pypi.org. Adding main as a deployment branch rule and a required reviewer to the environment (Settings → Environments → pypi) would put that gate in GitHub itself.

Because publish uses skip-existing: true, a dry run for a version that is already on TestPyPI uploads nothing and still succeeds.

Auto-merge

Auto-merge is enabled for the repository. gh pr merge --auto --squash, or Enable auto-merge on the pull request page, merges a pull request as soon as the required checks pass and the branch is up to date.