Overview
Maintaining a private Python package shouldn't mean manually bumping versions, tagging commits, and pushing releases every time you ship a change. For a while, that's exactly what I was doing — editing pyproject.toml, running git tag, pushing, and hoping I didn't fat-finger the version number.
In this article, we'll wire up a fully automated release pipeline for a private Python package using:
- Poetry — dependency management and packaging
- Python Semantic Release — automatic version bumps from commit messages
- Bitbucket Pipelines — CI/CD runner
- AWS CodeArtifact — private package registry
By the end, every merge to main will automatically bump the version, generate a changelog, tag the commit, and publish to CodeArtifact — based purely on your commit messages.
Prerequisites
- A Bitbucket account (workspace + repo creation rights)
- An AWS account (CodeArtifact access)
- Python 3.11+
Reference repository
The complete sample code and starter files used throughout this article are available on this GitHub repository: [https://github.com/mel-cdn/playground-hub-python-shared].
Step 1 — Initialize the Private Python Library Repository
We'll commit an initial v0.0.0 baseline so semantic-release has a starting point.
💡 Already have an existing package with a version? Skip ahead and manually tag your current version, then push the tag to origin. See [Step 1.7].
1.1 Create an empty Bitbucket repository
[https://bitbucket.org/[your-workspace]/workspace/create/repository]
1.2 Clone it locally
$ git clone git@bitbucket.org:[your-workspace]/phub-python-shared.git
Cloning into 'phub-python-shared'...
warning: You appear to have cloned an empty repository.
1.3 Add the initial files
You can check out the initial-files branch of the Reference Repository.
phub-python-shared
├─ phub_python_shared/ # The package source
│ ├── __init__
│ ├── unique_id.py
├─ tests/ # Unit tests files
│ ├── __init__
│ ├── test_unique_id.py
├─ .gitignore
├─ poetry.lock # The snapshot of the package versions
├─ pyproject.toml # The package build configuration
├─ README.md # The manual
1.4 Configure pyproject.toml
Let's walk through the relevant sections.
Project metadata — note the initial version is 0.0.0:
# -----------------------------
# Project Metadata
# -----------------------------
[project]
name = "phub-python-shared"
version = "0.0.0"
description = "Python Shared Utilities for Playground Hub"
readme = { file = "README.md", content-type = "text/markdown" }
authors = [
{ name = "Mel Cadano", email = "mel.cdn@outlook.ph" }
]
Build system & dependencies:
# -----------------------------
# Build system
# -----------------------------
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[[tool.poetry.source]]
name = "PyPI"
priority = "primary"
# -----------------------------
# Project Dependencies
# -----------------------------
[tool.poetry.dependencies]
python = ">=3.11,<3.13"
ulid = "==1.1"
# -----------------------------
# Dev Dependencies
# -----------------------------
[tool.poetry.group.dev.dependencies]
pytest = "==9.0.3"
python-semantic-release = "==10.5.3"
Semantic Release configuration — this tells semantic-release to target Bitbucket, write a CHANGELOG.md, and keep the version in pyproject.toml in sync:
# -----------------------------
# Semantic Release Configuration
# -----------------------------
[tool.semantic_release]
# Commit Configurations
vcs = "bitbucket"
branch = "main"
allow_zero_version = true
changelog_file = "CHANGELOG.md"
commit_message = "✨ Bump to version {version} [skip ci]"
commit_parser = "conventional"
tag_format = "v{version}"
# For the CHANGELOG.MD builder
repository_author = "mello-world"
repository_name = "phub-python-shared"
version_toml = [
"pyproject.toml:project.version",
]
[tool.semantic_release.remote]
name = "origin"
type = "bitbucket"
Commit-message → version bump mapping. Semantic-release reads your commit messages to decide what the next version will be:
# For Version Tagging
[tool.semantic_release.commit_parser_options]
patch_tags = ["fix", "chore", "docs", "style", "refactor", "test"]
minor_tags = ["feat"]
major_tags = ["feat!"]
| Commit prefix | Bump type | Example |
|---|---|---|
fix:, chore:, docs:, style:, refactor:, test:
|
Patch (0.0.X) | fix: handle empty input |
feat: |
Minor (0.X.0) | feat: add ULID generator |
feat!: |
Major (X.0.0) | feat!: drop Python 3.10 support |
1.5 Now let's commit these as starter files.
git commit -a -m "Add initial files"
1.6 Install the package and run a local version bump
$ pip install poetry
$ eval $(poetry env activate)
$ poetry install --no-root
$ semantic-release version
You'll see something like this:
[01:36:15] WARNING Token value is missing! config.py:782
0.0.0
The next version is: 0.0.0! 🚀
WARNING:root:[Errno 2] No such file or directory: '.../phub-python-shared/CHANGELOG.md'
No build command specified, skipping
::ERROR:: Requested to use token but no token set.
Run semantic-release in very verbose mode (-vv) to see the full traceback.
ℹ️ The "Token value is missing" error is expected locally — pushing to the remote requires a token, which we'll configure later in Bitbucket Pipelines. The local tag and changelog are still created.
Since there's no feat/fix commit yet, the version doesn't bump — but because the v0.0.0 tag didn't exist, semantic-release creates one matching the current version:
$ git tag --list
v0.0.0
Checking your git log, you should also see a Bump version commit authored by semantic-release, along with an initial CHANGELOG.md:
CHANGELOG.md
# CHANGELOG
<!-- version list -->
## v0.0.0 (2026-06-14)
- Initial Release
1.7 Push main and the v0.0.0 tag to origin
$ git push origin main
$ git push origin tag v0.0.0
✅ Confirm in Bitbucket that the main branch and the v0.0.0 tag are both present.
[https://bitbucket.org/[your-workspace]/phub-python-shared/commits/branch/main]
[https://bitbucket.org/[your-workspace]/phub-python-shared/commits/tag/v0.0.0]
Step 2 — Automate the Bump in Bitbucket Pipelines
Now that local bumping works, let's move the tagging into Bitbucket Pipelines so every merge to main is versioned automatically.
2.1 Create a Bitbucket access token (bot)
Semantic-release needs to push commits and tags back to main, which the default $BITBUCKET_* pipeline credentials can't do. Create a dedicated bot token:
[https://bitbucket.org/[your-workspace]/phub-python-shared/admin/access-tokens]
- Go to Repository settings → Security → Access tokens → Create access Token.
- Name it something like
Version Crafter. - Grant the following scopes:
repository:write.
- Copy the generated token and email.
2.2 Update the pyproject.toml.
Wire up the bot identity so semantic-release can sign commits and push tags on your behalf. Use the access token name and email from the previous step as the commit_author.
Add the commit_author line to your pyproject.toml.
# -----------------------------
# Semantic Release Configuration
# -----------------------------
[tool.semantic_release]
# Commit Configurations
vcs = "bitbucket"
branch = "main"
allow_zero_version = true
changelog_file = "CHANGELOG.md"
commit_author = "Version Crafter <y1qvnblznuaapeboc86b0w62qhkuj0@bots.bitbucket.org>"
commit_message = "✨ Bump to version {version} [skip ci]"
commit_parser = "conventional"
tag_format = "v{version}"
2.3 Update the Repository Variables for the Pipeline
[https://bitbucket.org/[your-workspace]/phub-python-shared/admin/pipelines/repository-variables]
🛠️ You need to enable pipelines first to be able to add repository variables.
In Repository settings → Repository variables, add:
| Variable | Value | Secured |
|---|---|---|
BITBUCKET_BOT_TOKEN |
(the access token) | Yes |
2.4 Add the initial bitbucket-pipelines.yml
Create a bitbucket-pipelines.yml at the repo root with two steps:
- Unit Tests
-
Bump Version - using
BITBUCKET_BOT_TOKENto push the next tag.
The pipeline has two steps. unit tests and bump version. bump version will use BITBUCKET_BOT_TOKEN so it can push the next tag
image: python:3.12
definitions:
steps:
- step: &lint-and-tests
name: Unit Tests
script:
- pip install poetry
- poetry install --no-root
- poetry run python -m pytest
- step: &bump-version
name: Bump Version
script:
- pip install python-semantic-release==10.5.3
- export BITBUCKET_TOKEN=$BITBUCKET_BOT_TOKEN
- git remote set-url origin "https://x-token-auth:${BITBUCKET_TOKEN}@bitbucket.org/${BITBUCKET_REPO_FULL_NAME}.git"
- PYTHONPATH="${PWD}" semantic-release version
pipelines:
# Runs when code hits the main branch
branches:
main:
- step: *lint-and-tests
- step: *bump-version
2.5 Commit, push, and validate the pipeline
With pyproject.toml and bitbucket-pipelines.yml in place, commit both files and push to main.
We'll use a chore commit type — this is an intentional change that semantic-release will detect and bump to the first real version.
git add pyproject.toml bitbucket-pipelines.yml
git commit -m "chore: Configure package build and pipeline configuration"
git push origin main
Head over to Pipelines in your Bitbucket repository to watch the first run:
[https://bitbucket.org/[your-workspace]/phub-python-shared/pipelines]
Pipeline Steps
Verify that the bump version step completes successfully and bumps the version to v0.0.1:
Finally, confirm in Bitbucket that:
- ✅ The
mainbranch now contains the automated bump commit. - ✅ The new
v0.0.1tag is present. - ✅ The bot account -
Version Crafteris the author of the commit instead of the defaultsemantic-release.
Step 3 — Automate the Push to AWS CodeArtifact
With versioning automated, the last piece is publishing the built wheel to a private registry.
3.1 Create the CodeArtifact domain & repository
In the AWS Console, go to CodeArtifact → Domains → Create domain.
Then create a repository inside that domain. You can leave the upstream as pypi-store so missing public dependencies are proxied transparently.
3.2 Create a deployer IAM user
The pipeline needs AWS credentials to push to CodeArtifact. Create a dedicated IAM user for it.
🧠 For simplicity, we'll use an IAM user with access keys. If you'd rather use short-lived credentials via OIDC (recommended for production), follow this guide instead: Bitbucket → AWS OIDC setup.
🔐 Attached AWS-managed policy is
PowerUserAccessfor a quick setup. For production, you can use below in-line policy to restrict to the least privilege required (recommended).
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"codeartifact:GetAuthorizationToken",
"codeartifact:GetRepositoryEndpoint",
"codeartifact:ReadFromRepository",
"codeartifact:PublishPackageVersion",
"codeartifact:PutPackageMetadata"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "sts:GetServiceBearerToken",
"Condition": {
"StringEquals": { "sts:AWSServiceName": "codeartifact.amazonaws.com" }
},
"Resource": "*"
}
]
}
3.3 Generate an access key
After creating the IAM user, On the user's Security credentials tab, click Create access key.
Copy both the Access key ID and Secret access key — the secret is shown only once.
3.4 Update the repository variables for the pipeline
[https://bitbucket.org/[your-workspace]/phub-python-shared/admin/pipelines/repository-variables]
Add the AWS keys and values:
| Variable | Value | Secured |
|---|---|---|
AWS_ACCOUNT_ID |
(13 digit AWS account number) | Yes |
AWS_DEFAULT_REGION |
(AWS region) | No |
AWS_ACCESS_KEY_ID |
(IAM user access key) | Yes |
AWS_SECRET_ACCESS_KEY |
(IAM user secret access key) | Yes |
3.5 Add the Publish step to pipeline configuration
Pull the latest commits and tags from your remote origin:
git pull origin main
Add the publish step to bitbucket-pipelines.yml and update with the following workflow rules:
-
lint-and-test: Runs on all feature branches, excludingmain, to validate code before merging. -
bump-version: Runs strictly onmainbranch to handle automated versioning and tagging. -
publish: Triggers only when a new tag is pushed, building and uploading the package to AWS CodeArtifact.
Updated bitbucket-pipelines.yml:
image: python:3.12
definitions:
steps:
- step: &lint-and-tests
name: Unit Tests
script:
- pip install poetry
- poetry install --no-root
- poetry run python -m pytest
- step: &bump-version
name: Bump Version
script:
- pip install python-semantic-release==10.5.3
- export BITBUCKET_TOKEN=$BITBUCKET_CI_TOKEN
- git remote set-url origin "https://x-token-auth:${BITBUCKET_TOKEN}@bitbucket.org/${BITBUCKET_REPO_FULL_NAME}.git"
- PYTHONPATH="${PWD}" semantic-release version
- step: &publish
name: Publish to CodeArtifact
script:
# Install dependencies
- apt-get update -qq && apt-get install -y -qq jq
- pip install awscli==1.45.25 poetry==2.4.1
# Preparing CodeArtifact repository
- |
AUTH_TOKEN=$(
aws codeartifact get-authorization-token \
--domain "playground-hub" \
--domain-owner $AWS_ACCOUNT_ID \
--region $AWS_DEFAULT_REGION \
--query 'authorizationToken' \
--output text
)
REPO_URL=$(
aws codeartifact get-repository-endpoint \
--domain "playground-hub" \
--domain-owner $AWS_ACCOUNT_ID \
--region $AWS_DEFAULT_REGION \
--repository "phub-python-shared" \
--format pypi \
--query 'repositoryEndpoint' \
--output text
)
# Building and push package
- |
poetry build
poetry config repositories.phub-python-shared "$REPO_URL"
poetry config http-basic.phub-python-shared aws "$AUTH_TOKEN"
poetry publish -r phub-python-shared
pipelines:
# A. Runs on every branch EXCEPT main
default:
- step: *lint-and-tests
# B. Runs ONLY when code hits the main branch
branches:
main:
- step: *bump-version
# C. Runs ONLY when there's a new tag with vX.X.X format
tags:
'v*.*.*':
- step: *publish
3.6 Commit and push to Publish
We'll intentionally use feat commit type to trigger the bumping of the feature identifier
git add bitbucket-pipelines.yml
git commit -m "feat: Add publish AWS CodeArtifact publish step on tags"
git push origin main
Head over again to Pipelines in your Bitbucket repository:
[https://bitbucket.org/[your-workspace]/phub-python-shared/pipelines]
3.7 Validate the publish
Once the main branch pipeline completes, the automated version bump kicks off the tag trigger.
The pipeline will automatically run the publish step immediately after the version tag (v0.1.0) is generated:
Finally, log into your AWS Console and navigate to AWS CodeArtifact to verify that your new package version is available in the repository:
Conclusion
And that's it! You have successfully configured an automated Python package versioning and publishing pipeline using Bitbucket Pipelines and AWS CodeArtifact.
In a future article, I will cover how to easily pull down and consume this package as a standard library inside your Python applications.
Salamat po 🇵🇭



















Top comments (0)