DEV Community

Cover image for Automate Python Package Releases: Semantic Release, AWS CodeArtifact, Poetry and Bitbucket Pipelines
Mel Cadano
Mel Cadano

Posted on

Automate Python Package Releases: Semantic Release, AWS CodeArtifact, Poetry and Bitbucket Pipelines

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]

Create an empty 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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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" }
]
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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!"]
Enter fullscreen mode Exit fullscreen mode
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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

ℹ️ 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
Enter fullscreen mode Exit fullscreen mode

Checking your git log, you should also see a Bump version commit authored by semantic-release, along with an initial CHANGELOG.md:

First version bump

CHANGELOG.md

# CHANGELOG

<!-- version list -->

## v0.0.0 (2026-06-14)

- Initial Release
Enter fullscreen mode Exit fullscreen mode

1.7 Push main and the v0.0.0 tag to origin

$ git push origin main
$ git push origin tag v0.0.0
Enter fullscreen mode Exit fullscreen mode

✅ 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]

Repository main branch

[https://bitbucket.org/[your-workspace]/phub-python-shared/commits/tag/v0.0.0]

Repository 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]

  1. Go to Repository settings → Security → Access tokens → Create access Token.
  2. Name it something like Version Crafter.
  3. Grant the following scopes: repository:write. Create access token
  4. Copy the generated token and email. Created access token configuration

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}"
Enter fullscreen mode Exit fullscreen mode

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

New variable added

2.4 Add the initial bitbucket-pipelines.yml

Create a bitbucket-pipelines.yml at the repo root with two steps:

  1. Unit Tests
  2. Bump Version - using BITBUCKET_BOT_TOKEN to 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Head over to Pipelines in your Bitbucket repository to watch the first run:

[https://bitbucket.org/[your-workspace]/phub-python-shared/pipelines]

Pipeline Overview
First pipeline

Pipeline Steps
Verify that the bump version step completes successfully and bumps the version to v0.0.1:

Pipeline steps

Finally, confirm in Bitbucket that:

  • ✅ The main branch now contains the automated bump commit.
  • ✅ The new v0.0.1 tag is present.
  • ✅ The bot account - Version Crafter is the author of the commit instead of the default semantic-release.

Repository main branch

Repository tag v0.0.1


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.

Create a CodeArtifact domain

Then create a repository inside that domain. You can leave the upstream as pypi-store so missing public dependencies are proxied transparently.

Create a CodeArtifact repository

Created an empty repository

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.

Creating the IAM user deployer

🔐 Attached AWS-managed policy is PowerUserAccess for 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": "*"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

3.3 Generate an access key

After creating the IAM user, On the user's Security credentials tab, click Create access key.

Create IAM user access key

Copy both the Access key ID and Secret access key — the secret is shown only once.

Auto-generated access keys

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

Updated repository variables with AWS configurations

3.5 Add the Publish step to pipeline configuration

Pull the latest commits and tags from your remote origin:

git pull origin main
Enter fullscreen mode Exit fullscreen mode

Add the publish step to bitbucket-pipelines.yml and update with the following workflow rules:

  • lint-and-test: Runs on all feature branches, excluding main, to validate code before merging.
  • bump-version: Runs strictly on main branch 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

Publish after version bump
Publish successful

Finally, log into your AWS Console and navigate to AWS CodeArtifact to verify that your new package version is available in the repository:

AWS CodeArtifact


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)