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/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/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..bbf05ae 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: @@ -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"] 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"