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
17 changes: 16 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@ name: Publish to PyPI
on:
release:
types: [published]
# Re-publish an existing release tag with the current workflow, for example after a failed
# publish. The tagged source is built and published; the workflow file comes from the
# branch the dispatch runs on.
workflow_dispatch:
inputs:
tag:
description: "Existing release tag to publish (for example 0.2.0)"
required: true
Comment thread
greptile-apps[bot] marked this conversation as resolved.
testpypi:
description: "Also publish to TestPyPI (for a pre-release tag)"
type: boolean
default: false

permissions:
contents: read
Expand All @@ -14,6 +26,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# A dispatch input is resolved strictly as a tag: a branch name or SHA fails here.
ref: ${{ inputs.tag && format('refs/tags/{0}', inputs.tag) || github.ref }}

- uses: actions/setup-python@v5
with:
Expand Down Expand Up @@ -52,7 +67,7 @@ jobs:

publish-testpypi:
name: Publish to TestPyPI (pre-release)
if: github.event.release.prerelease
if: github.event.release.prerelease || inputs.testpypi
needs: build
runs-on: ubuntu-latest
environment:
Expand Down
21 changes: 15 additions & 6 deletions .github/workflows/semver.yml
Original file line number Diff line number Diff line change
@@ -1,24 +1,33 @@
name: Semantic Release

# Runs semantic-release when a maintainer pushes to a release/x.y branch.
# semantic-release analyses commits since the last tag, bumps the version,
# updates CHANGELOG.md + pyproject.toml, commits them, and pushes a vX.Y.Z tag.
# That tag push then triggers the publish.yml PyPI workflow.
# Runs semantic-release on every push to main that touches the package. It analyses the
# Conventional Commits since the last X.Y.Z tag, decides the bump (none for docs/chore/ci),
# updates CHANGELOG.md + pyproject.toml, commits them, pushes the tag and creates the GitHub
# release. That release triggers publish.yml, which publishes to PyPI via OIDC.
#
# Tags have no v prefix (tagFormat "${version}"). 0.2.0 was released by hand on main (the
# earlier 0.1.x tags live only on the retired release/0.1.x branch), so releases continue
# from there in 0.x.
#
# Required secrets: GH_TOKEN (PAT with contents: write)

on:
push:
branches:
- 'release/[0-9]+.x'
- 'release/[0-9]+.[0-9]+.x'
- main
paths:
- 'floci/**'
- 'tests/**'
- 'pyproject.toml'
- '.releaserc.json'
- '.github/workflows/semver.yml'

# One release at a time: semantic-release pushes a release commit and a tag, so overlapping
# runs from merges close together would race. Queue them; never cancel a release mid-way.
concurrency:
group: semantic-release
cancel-in-progress: false

permissions:
contents: write

Expand Down
13 changes: 7 additions & 6 deletions .releaserc.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
{
"tagFormat": "${version}",
"branches": [
{
"name": "release/+([0-9])?(.{+([0-9]),x}).x",
"channel": "${name.replace(/^release\\//,'')}"
}
"main"
],
"plugins": [
"@semantic-release/commit-analyzer",
Expand All @@ -18,13 +15,17 @@
[
"@semantic-release/exec",
{
"prepareCmd": "python3 -c \"import re, sys; f=open('pyproject.toml'); c=f.read(); f.close(); c=re.sub(r'(?m)^version = \\\".*\\\"', 'version = \\\"${nextRelease.version}\\\"', c); open('pyproject.toml','w').write(c)\""
"prepareCmd": "python3 scripts/set_version.py ${nextRelease.version}"
}
],
[
"@semantic-release/git",
{
"assets": ["pyproject.toml", "CHANGELOG.md"],
"assets": [
"pyproject.toml",
"uv.lock",
"CHANGELOG.md"
],
"message": "chore(release): ${nextRelease.version}\n\n${nextRelease.notes}"
}
],
Expand Down
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Changelog

## 0.2.0 (2026-10-07)

First release from `main`. 0.1.1 was cut from the retired `release/0.1.x` branch and predates
everything below.

### Features

* add the missing service configs `BackupConfig`, `Route53Config`, `TextractConfig` and
`TransferFamilyConfig`, plus `TlsConfig`, `StorageConfig` and `with_log_level()` ([#1](https://github.com/floci-io/testcontainers-floci-python/issues/1))
* add `with_access_key()` and `with_secret_key()` setters ([#1](https://github.com/floci-io/testcontainers-floci-python/issues/1))

### Bug Fixes

* `with_dedicated_network()` now creates the Docker network it promises ([#1](https://github.com/floci-io/testcontainers-floci-python/issues/1))
* service ports are exposed in one place, so a config's ports are exposed exactly once ([#1](https://github.com/floci-io/testcontainers-floci-python/issues/1))

### Other

* `LICENSE`, `CODE_OF_CONDUCT.md`, `SECURITY.md` and a standardized README ([#2](https://github.com/floci-io/testcontainers-floci-python/issues/2), [#5](https://github.com/floci-io/testcontainers-floci-python/issues/5))
* CI integration tests run again (`pytest-timeout`) and install from `uv.lock` ([#6](https://github.com/floci-io/testcontainers-floci-python/issues/6), [#7](https://github.com/floci-io/testcontainers-floci-python/issues/7))
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ Why Floci uses the DCO and not a CLA is explained in the

- Use [Conventional Commits](https://www.conventionalcommits.org/), e.g.
`fix(container): expose the RDS port range`.
- Releases are cut by semantic-release from these messages: `feat` and `fix` commits decide the
next version, so pick the type that matches the change.
- Releases are cut automatically from `main` by semantic-release, using these messages: `feat`
gives a minor release, `fix`/`perf` a patch, other types none, so pick the type that matches the
change. A merge to `main` with a `feat` or `fix` commit publishes to PyPI.
- **No AI attribution.** Do not add "Generated by", "Co-Authored-By: …-bot", or similar
trailers to commit messages.

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "testcontainers-floci"
version = "0.1.0"
version = "0.2.0"
description = "Testcontainers module for Floci — the open-source local AWS emulator"
readme = "README.md"
license = { text = "MIT" }
Expand Down
31 changes: 31 additions & 0 deletions scripts/set_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Set the package version in pyproject.toml and uv.lock (run by semantic-release's prepare step).

uv.lock records the project's own version, and CI installs with `uv sync --locked`, so both
files must change together or the first CI run after a release fails.
"""

import re
import sys
from pathlib import Path


def main(version: str) -> None:
pyproject, lock = Path("pyproject.toml"), Path("uv.lock")
pyproject_text, n = re.subn(
r'(?m)^version = "[^"]*"', f'version = "{version}"', pyproject.read_text(), count=1
)
if n != 1:
sys.exit("pyproject.toml: no top-level version line found")
pattern = r'(\nname = "testcontainers-floci"\nversion = ")[^"]*(")'
lock_text, n = re.subn(pattern, rf"\g<1>{version}\g<2>", lock.read_text(), count=1)
if n != 1:
sys.exit("uv.lock: testcontainers-floci package entry not found")
# Both files validated: write them together so a failure never leaves mixed versions.
pyproject.write_text(pyproject_text)
lock.write_text(lock_text)


if __name__ == "__main__":
if len(sys.argv) != 2:
sys.exit("usage: set_version.py <version>")
main(sys.argv[1])
36 changes: 36 additions & 0 deletions tests/test_set_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""scripts/set_version.py must update pyproject.toml and uv.lock together, or neither."""

import runpy
import shutil
from pathlib import Path

import pytest

ROOT = Path(__file__).resolve().parent.parent
SCRIPT = ROOT / "scripts" / "set_version.py"


def run(version: str, cwd: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.chdir(cwd)
monkeypatch.setattr("sys.argv", ["set_version.py", version])
runpy.run_path(str(SCRIPT), run_name="__main__")


def test_updates_both_files(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
shutil.copy(ROOT / "pyproject.toml", tmp_path)
shutil.copy(ROOT / "uv.lock", tmp_path)
run("9.8.7", tmp_path, monkeypatch)
assert 'version = "9.8.7"' in (tmp_path / "pyproject.toml").read_text()
assert 'name = "testcontainers-floci"\nversion = "9.8.7"' in (tmp_path / "uv.lock").read_text()


def test_rejected_lock_leaves_both_files_unchanged(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
shutil.copy(ROOT / "pyproject.toml", tmp_path)
(tmp_path / "uv.lock").write_text("version = 1\n") # no testcontainers-floci entry
before = (tmp_path / "pyproject.toml").read_text()
with pytest.raises(SystemExit):
run("9.8.7", tmp_path, monkeypatch)
assert (tmp_path / "pyproject.toml").read_text() == before
assert (tmp_path / "uv.lock").read_text() == "version = 1\n"
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading