From 422eb3e8629c0da57aaeb70a2d4e81698806819b Mon Sep 17 00:00:00 2001 From: colinmoynes Date: Tue, 3 Nov 2026 23:05:43 +0000 Subject: [PATCH 1/2] Build the Docker image on Docker Hardened Images (Alpine) Switches both stages from python:3.13-slim to dhi.io/python:3.13-alpine (and -alpine-dev for the build). GitPulse now installs into a pip-free virtualenv that is the only thing copied into a runtime with no shell or package manager, running as UID 65532. Trivy findings drop from 193 (50 High) to 0, and the image shrinks from 227 MB to 105 MB. CI and the release workflow log in to dhi.io with the existing Docker Hub secrets; fork PRs skip the image build. Bumps the version to 0.2.1. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 8 ++++++++ .github/workflows/release.yml | 9 ++++++++- Dockerfile | 29 +++++++++++++++++++---------- README.md | 2 +- src/gitpulse/__init__.py | 2 +- 5 files changed, 37 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0065121..8945f12 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,10 +35,18 @@ jobs: - run: twine check --strict dist/* docker: + # The base images on dhi.io need a login, and pull requests from forks don't get + # secrets, so those skip the image build. + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: dhi.io + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build image uses: docker/build-push-action@v6 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 98767ba..8ae9a46 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,10 +63,17 @@ jobs: - uses: actions/checkout@v4 - uses: docker/setup-qemu-action@v3 - uses: docker/setup-buildx-action@v3 - - uses: docker/login-action@v3 + - name: Log in to Docker Hub + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Log in to Docker Hardened Images + uses: docker/login-action@v3 + with: + registry: dhi.io + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - id: meta uses: docker/metadata-action@v5 with: diff --git a/Dockerfile b/Dockerfile index 6667d51..378eb83 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,21 @@ # syntax=docker/dockerfile:1 -FROM python:3.13-slim AS build +# Docker Hardened Images (https://dhi.io), Alpine variant: a minimal, non-root Python +# runtime with no shell or package manager. Pulling them needs `docker login dhi.io` +# with a Docker account. +ARG PYTHON_VERSION=3.13 + +FROM dhi.io/python:${PYTHON_VERSION}-alpine-dev AS build WORKDIR /src -RUN pip install --no-cache-dir build COPY pyproject.toml README.md LICENSE ./ COPY src ./src -RUN python -m build --wheel --outdir /dist +# The venv is created without pip so the runtime image ships only GitPulse and its +# dependencies; the build stage's pip installs into it from outside. +RUN python3 -m venv --without-pip /opt/venv \ + && pip --python /opt/venv/bin/python install --no-cache-dir . \ + && mkdir /data -FROM python:3.13-slim +FROM dhi.io/python:${PYTHON_VERSION}-alpine ARG VERSION=dev LABEL org.opencontainers.image.title="gitpulse" \ org.opencontainers.image.description="GitHub traffic, clones, referrers, and release downloads, with org-wide HTML dashboards" \ @@ -15,16 +23,17 @@ LABEL org.opencontainers.image.title="gitpulse" \ org.opencontainers.image.licenses="MIT" \ org.opencontainers.image.version="${VERSION}" -ENV PYTHONDONTWRITEBYTECODE=1 \ +ENV PATH=/opt/venv/bin:$PATH \ + PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ GITPULSE_DB=/data/gitpulse.sqlite3 -COPY --from=build /dist/*.whl /tmp/ -RUN pip install --no-cache-dir /tmp/*.whl && rm /tmp/*.whl \ - && useradd --create-home --uid 1000 gitpulse \ - && mkdir /data && chown gitpulse:gitpulse /data +COPY --from=build /opt/venv /opt/venv +# The runtime image has no shell, so /data is created in the build stage. +COPY --from=build --chown=65532:65532 /data /data -USER gitpulse +# The base image already runs as the non-root user 65532; this makes it explicit. +USER 65532:65532 # Dashboards and snapshot history land here; mount a volume to keep them. WORKDIR /data VOLUME ["/data"] diff --git a/README.md b/README.md index 215e6f2..acd403b 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ python -m pip install gitpulse-analytics docker run --rm -e GITHUB_TOKEN -v "$PWD:/data" fullstackspectrum/gitpulse org ORG ``` -The container runs as UID 1000. On Linux, if your user can't write the mounted directory as that UID, add `--user "$(id -u):$(id -g)"`. +The image is built on a [Docker Hardened Image](https://dhi.io) (Alpine), so it has no shell or package manager and runs as the non-root UID 65532. On Linux, if that UID can't write the mounted directory, add `--user "$(id -u):$(id -g)"`. **From source**, for development: diff --git a/src/gitpulse/__init__.py b/src/gitpulse/__init__.py index df81141..0413fa7 100644 --- a/src/gitpulse/__init__.py +++ b/src/gitpulse/__init__.py @@ -1,3 +1,3 @@ """Local GitHub repository analytics.""" -__version__ = "0.2.0" +__version__ = "0.2.1" From 1f3cacd81003460f71d961d04cd93c047eb57ff5 Mon Sep 17 00:00:00 2001 From: colinmoynes Date: Tue, 3 Nov 2026 23:09:14 +0000 Subject: [PATCH 2/2] Add a changelog for 0.2.0 and 0.2.1 Records the first public release and the hardened Docker image, including the upgrade step for volumes written by the 0.2.0 image (UID 1000 to 65532). Links the changelog from the package metadata, ships it in the sdist, and adds it to the release steps. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 5 +++-- pyproject.toml | 4 ++-- 3 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0b03010 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,59 @@ +# Changelog + +All notable changes to GitPulse are documented here. The format is based on +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project follows +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.2.1] - 2026-11-03 + +### Security + +- The Docker image is now built on the Alpine variant of + [Docker Hardened Images](https://dhi.io) for Python instead of `python:3.13-slim`. + Trivy findings drop from 193 (50 High, none fixable in Debian) to 0. +- The runtime image has no shell or package manager. GitPulse runs from its own + virtualenv containing only GitPulse and its dependencies, with no pip. +- `urllib3` is 2.8.0 in the Docker image and the Homebrew formula, fixing + CVE-2026-97687, CVE-2026-97688, and CVE-2026-97689. + +### Changed + +- The container runs as the non-root UID 65532 instead of 1000. +- The Docker image is 105 MB, down from 227 MB. + +### Upgrading + +If you kept snapshot history in a Docker volume or bind mount with 0.2.0, its files +belong to UID 1000. Saving with 0.2.1 then fails with +`attempt to write a readonly database`. Give the files to the new user once; your +history is kept: + +```sh +docker run --rm -v gitpulse-data:/data alpine chown -R 65532:65532 /data +``` + +For a bind mount, run `sudo chown -R 65532:65532 DIR` on the host, or keep the old UID +with `--user 1000:1000`. + +## [0.2.0] - 2026-11-03 + +First public release. + +### Added + +- `gitpulse org ORG` builds a dashboard for a whole organization or user account. It + prints a Rich summary in the terminal and writes a self-contained HTML dashboard + with daily traffic, the most viewed and most starred repositories, languages, + referrers, release downloads, and a sortable, filterable table of every repository. +- Per-repository commands: `repos`, `traffic`, `snapshot`, `history`, and `downloads`. +- Local SQLite snapshots to keep traffic history beyond GitHub's 14-day window. +- `gitpulse --version`. +- Releases on PyPI as `gitpulse-analytics`, in the + [`fullstackspectrum/gitpulse`](https://github.com/fullstackspectrum/homebrew-gitpulse) + Homebrew tap, and as the multi-arch `fullstackspectrum/gitpulse` Docker image. + +[Unreleased]: https://github.com/fullstackspectrum/gitpulse/compare/v0.2.1...HEAD +[0.2.1]: https://github.com/fullstackspectrum/gitpulse/compare/v0.2.0...v0.2.1 +[0.2.0]: https://github.com/fullstackspectrum/gitpulse/releases/tag/v0.2.0 diff --git a/README.md b/README.md index acd403b..bbf05ae 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,8 @@ Run tests with `pytest` and lint with `ruff check .`. ### Releasing -1. Bump `__version__` in `src/gitpulse/__init__.py` and merge to `main`. -2. Publish a GitHub release with the tag `vX.Y.Z` matching that version. +1. Bump `__version__` in `src/gitpulse/__init__.py`. +2. In [CHANGELOG.md](CHANGELOG.md), move the `Unreleased` entries under a new `X.Y.Z` heading with the date, and update the comparison links at the bottom. Merge to `main`. +3. Publish a GitHub release with the tag `vX.Y.Z` matching that version, using that version's changelog section as the release notes. The `Release` workflow checks that the tag matches the version, then publishes to PyPI. After that it pushes the multi-arch Docker image and updates the formula in [homebrew-gitpulse](https://github.com/fullstackspectrum/homebrew-gitpulse). A release marked as a pre-release goes to PyPI and to Docker Hub under its exact version tag only, without moving `latest` or the Homebrew formula. The formula template lives in `packaging/homebrew/`; edit it there, not in the tap. diff --git a/pyproject.toml b/pyproject.toml index 78354b8..e426776 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ dev = ["pytest>=8,<9", "ruff>=0.6,<1"] [project.urls] Homepage = "https://github.com/fullstackspectrum/gitpulse" Issues = "https://github.com/fullstackspectrum/gitpulse/issues" -Changelog = "https://github.com/fullstackspectrum/gitpulse/releases" +Changelog = "https://github.com/fullstackspectrum/gitpulse/blob/main/CHANGELOG.md" [project.scripts] gitpulse = "gitpulse.cli:app" @@ -50,7 +50,7 @@ path = "src/gitpulse/__init__.py" packages = ["src/gitpulse"] [tool.hatch.build.targets.sdist] -include = ["src", "tests", "README.md", "LICENSE"] +include = ["src", "tests", "README.md", "LICENSE", "CHANGELOG.md"] [tool.pytest.ini_options] pythonpath = ["src"]