From fb4aeb0c3f59279a2943c6810d06886ece6e9f6e Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Wed, 27 May 2026 14:24:29 -0600 Subject: [PATCH] Add GitHub workflows --- .github/workflows/ci.yml | 93 +++++++++++++++++ .github/workflows/release.yml | 190 ++++++++++++++++++++++++++++++++++ 2 files changed, 283 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6d7fa8f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,93 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +defaults: + run: + shell: bash + +jobs: + test: + name: Build and test + runs-on: ubuntu-24.04 + env: + IMPORT_NAME: src_py_lib + PYTHON_VERSION: "3.13" + UV_VERSION: "0.11.7" + + steps: + - name: Check out code + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: pip + + - name: Install uv + run: | + python -m pip install --upgrade pip + python -m pip install "uv==${UV_VERSION}" + + - name: Validate lockfile + run: uv lock --check + + - name: Lint Markdown + run: npx --yes markdownlint-cli2 + + - name: Lint Python + run: uv run --frozen ruff check . + + - name: Check Python formatting + run: uv run --frozen ruff format --check . + + - name: Type check + run: uv run --frozen pyright + + - name: Run tests + run: uv run --frozen python -m unittest discover -s tests + + - name: Smoke test source checkout import + run: | + uv run --frozen python - <<'PY' + import os + + import src_py_lib + + if src_py_lib.__name__ != os.environ["IMPORT_NAME"]: + raise SystemExit(f"unexpected import name: {src_py_lib.__name__}") + PY + + - name: Build wheel + run: uv build --wheel --out-dir dist --no-create-gitignore + + - name: Smoke test installed wheel + run: | + python -m venv build/ci-venv + . build/ci-venv/bin/activate + python -m pip install --upgrade pip + python -m pip install dist/*.whl + python - <<'PY' + import os + + import src_py_lib + + if src_py_lib.__name__ != os.environ["IMPORT_NAME"]: + raise SystemExit(f"unexpected import name: {src_py_lib.__name__}") + PY + + - name: Upload wheel artifact + uses: actions/upload-artifact@v7 + with: + name: src-py-lib-wheel + path: dist/*.whl diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..78222a7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,190 @@ +name: Build release + +on: + push: + tags: + - "v*" + workflow_dispatch: + inputs: + tag: + description: "Existing release tag to publish, for example v0.1.0" + required: true + type: string + +permissions: + contents: write + +concurrency: + group: release-${{ github.event.inputs.tag || github.ref_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + wheel: + name: Build wheel + runs-on: ubuntu-24.04 + env: + IMPORT_NAME: src_py_lib + PYTHON_VERSION: "3.13" + UV_VERSION: "0.11.7" + + steps: + - name: Check out release ref + uses: actions/checkout@v6 + with: + fetch-depth: 0 + persist-credentials: false + ref: ${{ github.event.inputs.tag || github.ref }} + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: pip + + - name: Install build tools + run: | + python -m pip install --upgrade pip + python -m pip install "uv==${UV_VERSION}" + + - name: Validate release inputs + id: release + run: | + release_tag="${{ github.event.inputs.tag || github.ref_name }}" + if [[ ! "${release_tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error title=Invalid release tag::Use a vMAJOR.MINOR.PATCH tag, got '${release_tag}'." + exit 1 + fi + if ! git rev-parse --verify --quiet "refs/tags/${release_tag}" >/dev/null; then + echo "::error title=Missing tag::Tag '${release_tag}' was not fetched. Create and push it before running this workflow." + exit 1 + fi + + project_version=$(uv run --frozen python - <<'PY' + import tomllib + + with open("pyproject.toml", "rb") as pyproject_file: + print(tomllib.load(pyproject_file)["project"]["version"]) + PY + ) + if [[ "v${project_version}" != "${release_tag}" ]]; then + echo "::error title=Version mismatch::pyproject.toml version '${project_version}' does not match tag '${release_tag}'." + exit 1 + fi + + echo "tag=${release_tag}" >> "${GITHUB_OUTPUT}" + + - name: Validate package + run: | + uv lock --check + uv run --frozen ruff check . + uv run --frozen ruff format --check . + uv run --frozen pyright + uv run --frozen python -m unittest discover -s tests + uv run --frozen python - <<'PY' + import os + + import src_py_lib + + if src_py_lib.__name__ != os.environ["IMPORT_NAME"]: + raise SystemExit(f"unexpected import name: {src_py_lib.__name__}") + PY + + - name: Build wheel + id: build + run: | + dist_dir="build/release/dist" + rm -rf build/release + mkdir -p "${dist_dir}" + + uv build --wheel --out-dir "${dist_dir}" --no-create-gitignore + project_wheels=("${dist_dir}"/*.whl) + if [[ "${#project_wheels[@]}" -ne 1 ]]; then + echo "::error title=Unexpected wheel count::Expected one project wheel, found ${#project_wheels[@]}." + exit 1 + fi + wheel_path="${project_wheels[0]}" + wheel_name="$(basename "${wheel_path}")" + checksum_path="${wheel_path}.sha256" + + ( + cd "$(dirname "${wheel_path}")" + shasum -a 256 "${wheel_name}" > "$(basename "${checksum_path}")" + ) + + echo "wheel_path=${wheel_path}" >> "${GITHUB_OUTPUT}" + echo "wheel_name=${wheel_name}" >> "${GITHUB_OUTPUT}" + echo "checksum_path=${checksum_path}" >> "${GITHUB_OUTPUT}" + + - name: Smoke test installed wheel + run: | + python -m venv build/release/install-venv + . build/release/install-venv/bin/activate + python -m pip install --upgrade pip + python -m pip install "${{ steps.build.outputs.wheel_path }}" + python - <<'PY' + import os + + import src_py_lib + + if src_py_lib.__name__ != os.environ["IMPORT_NAME"]: + raise SystemExit(f"unexpected import name: {src_py_lib.__name__}") + PY + + - name: Write release notes + id: notes + run: | + release_tag="${{ steps.release.outputs.tag }}" + wheel_name="${{ steps.build.outputs.wheel_name }}" + notes_path="build/release/release-notes.md" + cat > "${notes_path}" <> "${GITHUB_OUTPUT}" + + - name: Upload workflow artifact + uses: actions/upload-artifact@v7 + with: + name: src-py-lib-release + path: | + ${{ steps.build.outputs.wheel_path }} + ${{ steps.build.outputs.checksum_path }} + ${{ steps.notes.outputs.path }} + + - name: Publish GitHub release assets + env: + GH_TOKEN: ${{ github.token }} + run: | + release_tag="${{ steps.release.outputs.tag }}" + wheel_path="${{ steps.build.outputs.wheel_path }}" + checksum_path="${{ steps.build.outputs.checksum_path }}" + notes_path="${{ steps.notes.outputs.path }}" + + if gh release view "${release_tag}" >/dev/null 2>&1; then + gh release edit "${release_tag}" --title "${release_tag}" --notes-file "${notes_path}" + gh release upload "${release_tag}" "${wheel_path}" "${checksum_path}" --clobber + else + gh release create "${release_tag}" \ + "${wheel_path}" \ + "${checksum_path}" \ + --title "${release_tag}" \ + --notes-file "${notes_path}" \ + --verify-tag + fi