Bundle a public directory once as a versioned, checksum-verified PyPI wheel — share Docker build assets, project scaffolds, and tool config that consumers pull with just pip or curl
Project description
repak
Bundle a public directory once. Everything that re-pulls — a Docker build, a new project, a teammate's machine — gets the latest version automatically.
repak packages a local directory as a versioned, checksum-verified PyPI
wheel and uploads it. Consumers pull it back two ways:
- With pip —
pip install -U repak-{name} && unpak-{name} ./dest. No Docker, no clone. Good for scaffolds and shared tool config. - Without pip — a
curl/unzip/tarshell snippet. Good for Dockerfiles whose base image has no Python.
Either way, the consuming side needs no repak install and PyPI does the hosting, versioning, and CDN delivery for free.
Public data only. Uploading to public PyPI publishes your files to the world. Only bundle data you would publish anyway — open-source project assets, public test fixtures, public datasets, demo/tutorial configs, scaffolds and tool config you'd open-source.
repakdoes not and cannot enforce this; it is your responsibility. For private data, use a private index (see Private indexes) — or a container registry.
Not related to
trumank/repak, the Rust CLI for Unreal Engine.pakfiles. Different tool, same name.
Is repak the right tool?
repak rides on public PyPI as a free, CDN-backed file host. That only makes sense for a narrow case. Reach for it only when all of the following hold:
- The bundle is genuinely public (you would publish it anyway).
- You have no infrastructure for the job — no container registry, no internal package server, no appetite to run one.
- You want consumers to need zero specialized tooling — just
pip, or justcurl/unzip/tar.
If you already run a container registry, COPY --from=registry/... is the
better answer for build-context files. If you want parameterized project
scaffolding, cookiecutter/copier do variable substitution that repak does
not. If your shared config lives in a repo people already clone, a git
submodule or a git clone is simpler. repak wins only when there is no such
infra and the data is public.
Using PyPI as a generic file host is guest use of infrastructure funded by the Python Software Foundation and its sponsors. Keep bundles small, don't churn them needlessly, and move heavy or internal use to a private index.
What repak transports
repak does one thing: snapshot a directory's contents into a versioned, SHA-256'd artifact and put it where anyone can pull the current version with zero setup. It is not a templating engine and not a sync daemon — it ships a static snapshot that consumers extract (overwrite/merge) into a destination.
Three use cases fit that shape well.
1. Shared bundles for Docker builds
A project maintains public files that many Docker images need — example configs, seed fixtures, demo datasets. Without repak, those files get copy-pasted across Dockerfiles and drift the moment anyone edits the source.
With repak, the owning team keeps the files in one directory and runs repak
when anything changes; every Dockerfile has one RUN line that pulls the
current version. Any rebuild — by anyone, on any machine — gets the latest. No
PRs across a dozen repos. See Docker consumers for the
ready-to-paste snippets.
2. Project scaffolds / starter templates
You start new projects from the same skeleton — CI workflow, linter and
formatter config, Makefile, license, a minimal source layout. Keep that
skeleton in a directory and publish it:
cd my-python-scaffold && repak
Starting a new project anywhere, with nothing but pip:
pip install -U repak-my-python-scaffold
unpak-my-python-scaffold ./new-service
Every new project picks up the current scaffold. Fix a flaw in your CI
config once, re-run repak, and the next project you start has the fix — no
template repo to clone, no scaffolding tool to install on each machine.
Snapshot, not template. repak copies files verbatim. It does not substitute
{{project_name}}or run post-generation hooks. If you need parameterized generation, usecookiecutter/copier. repak is for "shared starting point you then edit," with versioning and checksums for free.
3. Shared, evolving tool config
A set of editor/linter/assistant config that you want identical across many
repos and machines and that changes over time: .editorconfig,
.pre-commit-config.yaml, ruff.toml, shared AI-assistant config and prompt
files (e.g. an .claude/ directory of skills and instructions), shell
dotfiles you'd publish anyway.
cd my-dev-config && repak
On any machine or in any repo:
pip install -U repak-my-dev-config
unpak-my-dev-config . # merge into the current repo
# or: unpak-my-dev-config ~ # into your home dir
Edit the config once, re-publish, and pip install -U on the next machine
pulls the new version. Extraction is overwrite/merge: your unrelated files are
left untouched. This is the closest repak gets to a "sync" story — it's still
pull-on-demand, not a daemon, but the pip install -U step is lighter than
maintaining a dotfiles repo clone or a submodule across N projects.
The alternative — a config repo people git clone — is perfectly fine and
often simpler. repak's only edge here is the versioned, checksum-verified,
clone-free pip install -U ergonomic for genuinely public config.
Common bundle contents
All examples assume the contents are public (see the warning above).
| Use case | Contents | Typical destination |
|---|---|---|
| Docker assets | demo nginx.conf, logrotate rules, seed SQL |
/etc/service, initdb.d |
| Docker CA bundles | published trust stores (.crt) |
/usr/local/share/ca-certificates |
| Scaffold | CI yaml, pyproject.toml, Makefile, src skeleton |
new project dir |
| Tool config | .editorconfig, .pre-commit-config.yaml, ruff.toml |
repo root |
| Assistant config | shared skills / instruction files | .claude/, ~ |
Install
pip install repak
Usage
export PYPI_TOKEN="pypi-..." # or TWINE_PASSWORD
repak --path /path/to/folder
# or, from inside the folder:
repak
# non-interactive (skip confirmation prompts):
repak --path /path/to/folder --yes
The PyPI package name is derived from the directory name as repak-{folder},
and the extractor entry point is unpak-{folder}. After a successful upload
repak prints all consumer commands — the pip install/unpak pair and
both Dockerfile snippets — with the correct package name and sha256 already
filled in.
pip consumers (no Docker)
This path needs no generated snippet — it is fully determined by the name:
pip install -U repak-{folder} # newest published version
# or pin: pip install repak-{folder}==0.3
unpak-{folder} /path/to/destination
unpak-{folder} verifies the SHA-256 before writing anything, then
reproduces the directory contents at the destination (overwrite/merge in
place). repak itself is not needed on the destination.
Docker consumers
For base images without Python, each consuming Dockerfile picks one mode.
repak prints both, filled in, after every upload.
# always latest — every rebuild pulls the newest upload
RUN set -eu \
&& IDX=$(curl -fsSL https://pypi.org/simple/repak-example-assets/) \
&& VER=$(printf '%s' "$IDX" \
| grep -o 'repak_example_assets-[0-9][0-9]*\.[0-9][0-9]*-py3-none-any\.whl' \
| sed 's/^repak_example_assets-//;s/-py3-none-any\.whl$//' \
| sort -t. -k1,1n -k2,2n | tail -1) \
&& WHL_URL=$(printf '%s' "$IDX" \
| grep -o 'https://[^#"]*repak_example_assets-'"$VER"'-py3-none-any\.whl[^#"]*' \
| head -1) \
&& curl -fsSL "$WHL_URL" -o /tmp/bundle.whl \
&& cd /tmp && unzip -q bundle.whl repak_example_assets/payload.tar.gz repak_example_assets/payload.sha256 \
&& echo "$(cat repak_example_assets/payload.sha256) repak_example_assets/payload.tar.gz" | sha256sum -c \
&& mkdir -p /etc/service \
&& tar -xzf repak_example_assets/payload.tar.gz -C /etc/service \
&& rm -rf /tmp/bundle.whl /tmp/repak_example_assets
# pinned at 0.3 — reproducible builds, sha256 locked at publish time
RUN set -eu \
&& WHL_URL=$(curl -fsSL https://pypi.org/simple/repak-example-assets/ \
| grep -o 'https://[^#"]*repak_example_assets-0.3-py3-none-any\.whl[^#"]*' \
| head -1) \
&& curl -fsSL "$WHL_URL" -o /tmp/bundle.whl \
&& cd /tmp && unzip -q bundle.whl repak_example_assets/payload.tar.gz \
&& echo "a3f9... repak_example_assets/payload.tar.gz" | sha256sum -c \
&& mkdir -p /etc/service \
&& tar -xzf repak_example_assets/payload.tar.gz -C /etc/service \
&& rm -rf /tmp/bundle.whl /tmp/repak_example_assets
The "always latest" snippet selects the newest version by a numeric
major.minor sort of the simple index, so it does not depend on the order in
which the index lists files. Both snippets require curl and unzip in the
base image:
# Alpine
RUN apk add -q curl unzip
# Debian/Ubuntu
RUN apt-get install -y --no-install-recommends curl unzip
Pinned vs. latest
| Pinned | Always latest | |
|---|---|---|
| Behavior | Identical every pull | Pulls current upload |
| sha256 | Baked in at publish time | Read from bundle at extract time |
| Recommended for | Production images, reproducible builds | Active development, scaffolds, shared config |
Private indexes
For data that should not be on public PyPI, point repak at a private index.
--repository-url sets the twine upload target; --index-url sets the
simple-index base baked into the generated Docker snippets so consumers fetch
from the same place:
repak --path /path/to/folder \
--repository-url https://pypi.internal.example.com/legacy/ \
--index-url https://pypi.internal.example.com/simple
Running a private index is its own hosting burden — if you have a container
registry, a COPY --from= against an image it hosts is usually the simpler
answer for private data.
How it works
Upload side
- The directory name is normalized to a PyPI name:
repak-{folder}. - PyPI is queried; a new package starts at
0.1, an existing repak package is bumped. The minor wraps into the major at 100 (0.99is followed by1.0), so there is no upload ceiling. You confirm before overwriting. - The directory contents are tarred (
.git/.hg/.svn/.bzrexcluded), gzipped, and SHA-256'd. - A self-contained wheel is built containing the tar blob, the checksum, and
an
unpak-{folder}console-script entry point. - The wheel size is checked against PyPI's 100 MiB per-file limit, then
uploaded with
twine.
Download side — with pip
pip install -U repak-myproject
unpak-myproject /target/landing/folder
The entry point verifies the SHA-256 before writing anything, then
reproduces the directory. repak itself does not need to be installed on the
destination.
Download side — without pip (Docker)
Use the generated RUN snippets above. They use the PyPI simple index to
resolve the wheel URL, then extract and verify using only shell primitives.
Behavior & constraints
- Extract is overwrite/merge in place: existing unrelated files are left untouched; re-running with the same payload is idempotent.
- Safety: tar entries with absolute paths or
..traversal are rejected; a checksum mismatch aborts before any file is written. - Versioning is
MAJOR.MINORwith minor0–99and an unbounded major (0.1, … 0.99, 1.0, … 1.99, 2.0, …) — no upload ceiling. PyPI does not allow re-uploading an existing version. - Name collisions: if
repak-{folder}already exists on PyPI but was not created by repak (no repak marker), upload is refused. - Mirror lag: propagation from public PyPI to an internal mirror is not immediate and the delay is environment-specific.
- Mirror limits: a private mirror may impose size limits below PyPI's 100 MiB.
Non-goals
No dependency resolution, no templating or variable substitution (the wheel is a transport container for a static snapshot), no sync daemon, no "correct" packaging, no git integration (clone/prepare the directory yourself), and no repak install required on the destination side.
Development
pip install -e ".[dev]"
pytest -q # full suite, incl. venv install + roundtrip e2e
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file repak-0.5.0.tar.gz.
File metadata
- Download URL: repak-0.5.0.tar.gz
- Upload date:
- Size: 24.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9d70b6cac035870294b98dcb2a02e2518e16da33972f2ce33be156aa00a4cf35
|
|
| MD5 |
754d883543f1a854603860bea870bd76
|
|
| BLAKE2b-256 |
67876431ec644df319cc4239176d13b8e895f0e3815e966d2e4ef3ca566dad07
|
Provenance
The following attestation bundles were made for repak-0.5.0.tar.gz:
Publisher:
release.yml on dlfelps/repak
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
repak-0.5.0.tar.gz -
Subject digest:
9d70b6cac035870294b98dcb2a02e2518e16da33972f2ce33be156aa00a4cf35 - Sigstore transparency entry: 1571959727
- Sigstore integration time:
-
Permalink:
dlfelps/repak@70a34e6271063ec03201690836b0b1af3ea2bf72 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/dlfelps
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@70a34e6271063ec03201690836b0b1af3ea2bf72 -
Trigger Event:
release
-
Statement type:
File details
Details for the file repak-0.5.0-py3-none-any.whl.
File metadata
- Download URL: repak-0.5.0-py3-none-any.whl
- Upload date:
- Size: 17.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c29bc2c22859d12bd639c4345e76c52e2995a927d38d5165b564091d9c41af6c
|
|
| MD5 |
d281bdd227c355acfb1a6ee317a43fb8
|
|
| BLAKE2b-256 |
e3781e50978745371ffc66fd512ec770ab768f2b4b39bd3b298152ea47c4bd07
|
Provenance
The following attestation bundles were made for repak-0.5.0-py3-none-any.whl:
Publisher:
release.yml on dlfelps/repak
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
repak-0.5.0-py3-none-any.whl -
Subject digest:
c29bc2c22859d12bd639c4345e76c52e2995a927d38d5165b564091d9c41af6c - Sigstore transparency entry: 1571959736
- Sigstore integration time:
-
Permalink:
dlfelps/repak@70a34e6271063ec03201690836b0b1af3ea2bf72 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/dlfelps
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@70a34e6271063ec03201690836b0b1af3ea2bf72 -
Trigger Event:
release
-
Statement type: