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
20 changes: 20 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.
87 changes: 87 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: CI

on:
push:
branches: [ main ]
pull_request:

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: Lint • Type Check • Test (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- python-version: "3.11"
experimental: false
- python-version: "3.12"
experimental: false
- python-version: "3.13"
experimental: false
continue-on-error: ${{ matrix.experimental }}

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
id: py
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install Poetry
uses: snok/install-poetry@v1
with:
virtualenvs-create: true
virtualenvs-in-project: true
installer-parallel: true

- name: Cache virtualenv
uses: actions/cache@v4
id: cache
with:
path: .venv
key: v1-poetry-${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }}

- name: Install deps (with dev group)
if: steps.cache.outputs.cache-hit != 'true'
run: |
poetry env use "${{ steps.py.outputs.python-path }}"
poetry install --no-interaction --no-ansi --with dev

- name: Sync deps (cache hit)
if: steps.cache.outputs.cache-hit == 'true'
run: |
poetry env use "${{ steps.py.outputs.python-path }}"
poetry install --no-interaction --no-ansi --with dev

- name: Show tool versions
run: |
poetry run python -V
poetry run ruff --version
poetry run mypy --version
poetry run pytest --version

- name: Ruff (format & lint)
run: |
poetry run ruff format --check .
poetry run ruff check .

- name: Mypy (install types if missing, then type check)
run: |
# Auto-install missing type stubs in CI (non-interactive)
poetry run mypy --install-types --non-interactive || true
poetry run mypy src/ tests/ --ignore-missing-imports --disable-error-code=call-overload

- name: Pytest (coverage gate)
run: |
poetry run pytest -q --cov=change_me --cov-report=term-missing --cov-fail-under=85
119 changes: 119 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
name: Docker Build & Publish

on:
push:
branches: ["**"]
tags: ["v*"]
paths-ignore:
- "**/*.md"
- "**/*.rst"
- "docs/**"
- ".github/ISSUE_TEMPLATE/**"
workflow_dispatch: {}

permissions:
contents: read
packages: write

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }} # may include uppercase (we'll normalize)
# Flip this to "true" if you want multi-arch on all branches
MULTIARCH_ON_BRANCHES: "false"

concurrency:
group: docker-${{ github.ref }}
cancel-in-progress: true

jobs:
build-and-push:
runs-on: ubuntu-latest
env:
IS_MAIN: ${{ github.ref == 'refs/heads/main' }}
IS_TAG: ${{ startsWith(github.ref, 'refs/tags/v') }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Buildx
uses: docker/setup-buildx-action@v3

# Only set up QEMU when we actually need arm64 (main or tag builds)
- name: Set up QEMU (for arm64)
if: env.IS_MAIN == 'true' || env.IS_TAG == 'true' || env.MULTIARCH_ON_BRANCHES == 'true'
uses: docker/setup-qemu-action@v3

- name: Normalize image name to lowercase
id: repo
run: |
echo "image_name_lc=$(echo '${{ env.IMAGE_NAME }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT"

- name: Derive tags
id: meta
run: |
REF="${GITHUB_REF#refs/}"
SHA_TAG=${GITHUB_SHA::12}
echo "sha_tag=${SHA_TAG}" >> "$GITHUB_OUTPUT"
if [[ "$REF" == tags/v* ]]; then
V="${REF#tags/v}"
echo "version_tag=${V}" >> "$GITHUB_OUTPUT"
fi
if [[ "$REF" == heads/main ]]; then
echo "latest_tag=latest" >> "$GITHUB_OUTPUT"
fi

# Fast path for branches: single-arch build, cache, don't push
- name: Build (branch) — fast single-arch, no push
if: env.IS_MAIN != 'true' && env.IS_TAG != 'true' && env.MULTIARCH_ON_BRANCHES != 'true'
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: false
load: true # so we can run the smoke test
platforms: linux/amd64
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false
tags: |
${{ env.REGISTRY }}/${{ steps.repo.outputs.image_name_lc }}:${{ steps.meta.outputs.sha_tag }}

# Publish path for main/tags: multi-arch build + push (with cache)
- name: Build & push (multi-arch)
if: env.IS_MAIN == 'true' || env.IS_TAG == 'true' || env.MULTIARCH_ON_BRANCHES == 'true'
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false
tags: |
${{ env.REGISTRY }}/${{ steps.repo.outputs.image_name_lc }}:${{ steps.meta.outputs.sha_tag }}
${{ steps.meta.outputs.version_tag && format('{0}/{1}:{2}', env.REGISTRY, steps.repo.outputs.image_name_lc, steps.meta.outputs.version_tag) || '' }}
${{ steps.meta.outputs.latest_tag && format('{0}/{1}:{2}', env.REGISTRY, steps.repo.outputs.image_name_lc, steps.meta.outputs.latest_tag) || '' }}

# Smoke test: use the local image for branch builds, or pull the pushed one for main/tags
- name: Smoke test (branch image)
if: env.IS_MAIN != 'true' && env.IS_TAG != 'true' && env.MULTIARCH_ON_BRANCHES != 'true'
run: |
IMAGE="${{ env.REGISTRY }}/${{ steps.repo.outputs.image_name_lc }}:${{ steps.meta.outputs.sha_tag }}"
echo "Smoke testing local image ${IMAGE}"
docker run --rm "$IMAGE" python -c "import change_me, sys; print('✅ import ok:', getattr(change_me,'__version__','?'), 'on', sys.version)"

- name: Smoke test (pushed image)
if: env.IS_MAIN == 'true' || env.IS_TAG == 'true' || env.MULTIARCH_ON_BRANCHES == 'true'
run: |
IMAGE="${{ env.REGISTRY }}/${{ steps.repo.outputs.image_name_lc }}:${{ steps.meta.outputs.sha_tag }}"
echo "Smoke testing pushed image ${IMAGE}"
docker pull "$IMAGE"
docker run --rm "$IMAGE" python -c "import change_me, sys; print('✅ import ok:', getattr(change_me,'__version__','?'), 'on', sys.version)"
54 changes: 54 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Contributing to This Project

First of all, thank you for considering contributing! 🎉
We welcome issues, bug reports, feature requests, and pull requests.

---

## How to Contribute

### Reporting Issues
* Use the [GitHub Issues](../../issues) tab.
* Search first to see if your issue is already reported.
* Include steps to reproduce, expected behavior, and screenshots/logs if relevant.

### Making Changes
1. Fork the repository.
2. Create a new branch for your changes:
```bash
git checkout -b feature/my-new-feature
```
3. Make your changes and commit with a clear message:
```bash
git commit -m "Add feature: my-new-feature"
```
4. Push to your fork and open a Pull Request (PR).

### Code Style
* Follow the existing code style in the project.
* Use descriptive names for variables, functions, and classes.
* Add docstrings or comments where needed for clarity.

### Testing
* Ensure that your code runs without errors.
* Add or update tests if applicable.
* Run all tests before submitting:
```bash
make test
```

### Pull Requests
* Keep PRs focused — one feature or bug fix per PR.
* Describe your changes clearly in the PR description.
* Reference related issues if applicable.

---

## Community Guidelines
* Be respectful and constructive in discussions.
* Assume good intentions and help others learn.

---

## License
By contributing, you agree that your contributions will be licensed under the [MIT License](LICENSE).
33 changes: 33 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# syntax=docker/dockerfile:1.7-labs
FROM python:3.12-slim

# Speed + deterministic behavior
ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1 \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1

# Select package + version at build-time
ARG PKG=change_me
ARG VER=latest
ENV PKG=${PKG} VER=${VER}

# Install package from PyPI with a cache mount (huge speedup on rebuilds)
# NOTE: --root-user-action needs a value; use =ignore to silence root warnings.
RUN --mount=type=cache,target=/root/.cache/pip \
python -m pip install --upgrade pip && \
if [ "$VER" = "latest" ]; then \
pip install --root-user-action=ignore "$PKG"; \
else \
pip install --root-user-action=ignore "$PKG==$VER"; \
fi

# Simple smoke test script
WORKDIR /app
RUN printf '%s\n' \
"import importlib, os, sys" \
"m = importlib.import_module(os.environ.get('PKG', '${PKG}'))" \
"print('✅ import ok:', getattr(m, '__version__', 'unknown'), 'on', sys.version)" \
> smoke.py

CMD ["python", "smoke.py"]
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,8 @@ clean:
update:
poetry cache clear pypi --all
poetry update


docker:
docker build --no-cache -f Dockerfile -t change_me-smoke .
docker run --rm change_me-smoke
Loading