Skip to content

Setup a new repo

Complete checklist for migrating a package repo to the cava-devops CI/CD system.


1. pyproject.toml

Switch to hatch-vcs for dynamic versioning. Remove the static version = "X.Y.Z" line.

[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"

[tool.hatch.version]
source = "vcs"
raw-options = { tag_regex = '^v?(?P<version>[0-9]+\.[0-9]+\.[0-9]+)$' }

[project]
dynamic = ["version"]
# remove: version = "X.Y.Z"

[tool.uv]
cache-keys = [{ file = "pyproject.toml" }, { git = { commit = true, tags = true } }]

The v? in tag_regex accepts both v1.2.3 and bare 1.2.3 tags, preserving compatibility with repos that have existing bare tags.

uv_build backend

If the repo currently uses uv_build as its build backend, change both requires and build-backend to the hatchling values above.


2. Add workflow files

ci.yml

For repos without a Postgres service:

name: CI
on:
  pull_request:
    branches: [main]
    types: [opened, synchronize, reopened, labeled, unlabeled]
jobs:
  label-gate:
    uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/label-gate.yml@main
  build-test:
    uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/build-test.yml@main

For repos with a Postgres service:

jobs:
  label-gate:
    uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/label-gate.yml@main
  build-test:
    uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/build-test-postgres.yml@main
    with:
      postgres-db: <db-name>         # repo-specific
      # postgres-image: pgvector/pgvector:pg16   # pgvector repos only

For repos with two test suites (e.g. SQLite + Postgres):

jobs:
  label-gate:
    uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/label-gate.yml@main
  build-test-sqlite:
    uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/build-test.yml@main
  build-test-postgres:
    uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/build-test-postgres.yml@main
    with:
      postgres-db: test_db

merge.yml

name: Release Update
on:
  pull_request:
    types: [closed]
jobs:
  sync-description:
    if: github.event.pull_request.merged == true
    uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/sync-pr-description.yml@main
  draft:
    needs: [sync-description]
    if: needs.sync-description.result == 'success'
    uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/release-drafter.yml@main
    secrets: inherit

publish.yml

name: Publish
on:
  push:
    tags: ['v*']
jobs:
  build:
    uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/publish.yml@main
  publish:
    needs: build
    runs-on: ubuntu-latest
    permissions:
      id-token: write
    environment:
      name: pypi
      url: https://pypi.org/p/<package-name>
    steps:
      - uses: actions/download-artifact@v4
        with: { name: dist, path: dist/ }
      - uses: pypa/gh-action-pypi-publish@release/v1

Success

Replace <package-name> with the PyPI slug.

docs.yml

Update the trigger from workflow_run: ["Release"] or push: branches: [main] to:

on:
  push:
    tags: ['v*']

3. Add release-drafter config

Copy templates/release-drafter.yml from this repo to .github/release-drafter.yml in the package repo.


4. Delete old files

Remove these files if present in the repo:

File What it was
release.yml Monolithic release workflow (semantic-release or similar)
lint-pr.yml PR title lint check (amannn/action-semantic-pull-request)
.releaserc.json semantic-release configuration
python-publish.yml Previous PyPI publish workflow
tests.yml Previous standalone test workflow

5. GitHub configuration

Merge settings

In Settings > General > Pull Requests:

Setting Value
Allow merge commits disabled
Allow squash merging enabled
Default commit message Pull request title
Allow rebase merging disabled

"Pull request title" leaves the extended description box empty in the merge dialog, prompting the person merging to write the changelog entry from scratch. The other options pre-fill the box with commit messages or the PR opening description, which would pollute the changelog.

Branch protection ruleset on main

Setting Value
Target main
Require pull request 1 approval
Required status checks See below
Require linear history enabled
Block force pushes enabled
Allow bypass Repository admin (for initial setup only)

Required status checks by CI variant:

ci.yml variant Required status checks
Standard (build-test.yml) label-gate / check, build-test / test
Postgres (build-test-postgres.yml) label-gate / check, build-test / test
Two suites (sqlite + postgres) label-gate / check, build-test-sqlite / test, build-test-postgres / test

The check name prefix is the calling job name in ci.yml.

GitHub environment

Create an environment named pypi in the repo settings (Settings > Environments). No protection rules are required unless you want manual approval before every release.

PyPI trusted publisher

Add a pending trusted publisher on pypi.org:

Field Value
Owner AustralianCancerDataNetwork
Repository name exact GitHub repo name (e.g. my-repo)
Workflow filename publish.yml
Environment name pypi

If the repo already has a trusted publisher from an old workflow (e.g. python-publish.yml), add a new one for publish.yml rather than editing the existing entry. The old entry becomes inert when the old workflow is deleted.

Labels

# From the cava-devops repo root:
scripts/bootstrap-labels.sh AustralianCancerDataNetwork/<repo-name>

This creates five labels: breaking, feature, fix, dependencies (bump labels) and chore (excluded from changelog, no version bump).


6. First release after cutover

Merge all migration changes via a PR labelled chore. merge.yml triggers and release-drafter creates a draft. What to do next depends on whether the repo has existing tags.

The draft body will be empty (chore is excluded from the changelog) and the suggested version will be the next patch after the last existing tag (e.g. v1.2.4 if the last tag was v1.2.3).

Leave the draft unpublished. The next real feature or fix PR will update the same draft with a proper changelog entry. Publish when you are ready to ship that change.

The draft will suggest an arbitrary version since there is no base tag to work from.

Before publishing, confirm the package name is available on PyPI and the trusted publisher entry is in place.

Go to the Releases page, open the draft, and change the "Tag version" field to the correct initial version (e.g. v0.1.0). Confirm the target branch is main. Click Publish release to create the tag, which triggers publish.yml and uploads the first release to PyPI.