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
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
9 changes: 8 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
59 changes: 59 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
29 changes: 19 additions & 10 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,30 +1,39 @@
# 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" \
org.opencontainers.image.source="https://github.com/fullstackspectrum/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"]
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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.
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"]
Expand Down
2 changes: 1 addition & 1 deletion src/gitpulse/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Local GitHub repository analytics."""

__version__ = "0.2.0"
__version__ = "0.2.1"
Loading