No description
  • PHP 98.6%
  • Dockerfile 1%
  • Shell 0.4%
Find a file
Rune Jensen 38f12fb42b
All checks were successful
ci/woodpecker/push/build Pipeline was successful
Cleanup pass for consistency and maintainability
- Move the URL-or-owner/name parsing duplicated across two commands
  into GitHubRepo::fromUrlOrFullName(), and let package:add accept
  both forms like its siblings instead of URLs only.
- Simplify HomeController's list building back to the arrow-fn idiom
  used elsewhere.
- Give both functional test classes the same tearDown so the shared
  test storage dir can't leak packages between tests.
- Drop the empty Doctrine scaffold dirs (src/Entity, src/Repository —
  no ORM here, Doctrine only backs the queue) and the stale Flex
  placeholder in src/Controller.
- Track .editorconfig (it defines the project style) and gitignore
  build.log.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 20:29:34 +00:00
.woodpecker Drop buildcache 2026-07-02 15:09:12 +00:00
bin Rebuild the registry on every app container start 2026-07-04 20:22:37 +00:00
config Log 404s/401s in prod and document Caddy access logging 2026-07-03 17:04:04 +00:00
kubernetes Cleartextpassword 2026-07-02 16:50:31 +00:00
public PAK init 2026-07-02 14:21:33 +00:00
secrets PAK init 2026-07-02 14:21:33 +00:00
src Cleanup pass for consistency and maintainability 2026-07-04 20:29:34 +00:00
tests Cleanup pass for consistency and maintainability 2026-07-04 20:29:34 +00:00
.dockerignore PAK init 2026-07-02 14:21:33 +00:00
.editorconfig Cleanup pass for consistency and maintainability 2026-07-04 20:29:34 +00:00
.env Cleartextpassword 2026-07-02 16:50:31 +00:00
.env.dev env til build 2026-07-02 14:49:20 +00:00
.env.docker.example Log 404s/401s in prod and document Caddy access logging 2026-07-03 17:04:04 +00:00
.env.example Cleartextpassword 2026-07-02 16:50:31 +00:00
.env.test Cleartextpassword 2026-07-02 16:50:31 +00:00
.gitignore Cleanup pass for consistency and maintainability 2026-07-04 20:29:34 +00:00
compose.yaml Rebuild the registry on every app container start 2026-07-04 20:22:37 +00:00
composer.json PAK init 2026-07-02 14:21:33 +00:00
composer.lock php 8.5 2026-07-02 14:27:28 +00:00
Dockerfile PAK init 2026-07-02 14:21:33 +00:00
phpunit.dist.xml PAK init 2026-07-02 14:21:33 +00:00
README.md Cleanup pass for consistency and maintainability 2026-07-04 20:29:34 +00:00
symfony.lock PAK init 2026-07-02 14:21:33 +00:00

pak

A small, webhook-driven private Composer registry for GitHub-hosted packages — the core of satis, minus the full-rebuild model. When a tag or branch is created (or deleted) on a configured repository, a GitHub webhook queues a refresh of that one package, so new releases are installable seconds after tagging.

How it works

GitHub App webhook (create/push/delete)          composer client
        │ POST /webhook/github                    │ GET /packages.json, /p2/…  (basic auth)
        ▼                                         ▼
  app container ── verify HMAC ──► queue (messenger, sqlite on /data)
                                        │
                                  worker container
                                        ▼
              index repo via GitHub API (composer/composer GitHubDriver, no cloning)
                                        ▼
              /data/repo/p2/vendor/name{,~dev}.json + packages.json  (atomic writes)
  • Metadata only — dist entries point at GitHub's zipball API. Clients fetch archives straight from GitHub with their own credentials, so the registry stores no archives and needs no purging.
  • Composer 2 format (metadata-url) — one metadata file per package, which is what makes cheap partial updates possible.
  • The repository list lives in a satis-like JSON file (/data/registry.json in Docker), managed with console commands.
  • GET / is a tiny human-only page listing indexed package names — a quick way to check what's in the registry in a browser. Composer clients never hit it; they read /packages.json directly.

Quick start

cp .env.docker.example .env.docker      # fill in the values (see below)
docker compose up -d --build

# add a repository and index it
docker compose exec app php bin/console package:add https://github.com/acme/widget

.env.docker values:

Variable Meaning
APP_SECRET any long random string
GITHUB_APP_ID your GitHub App's ID; leave empty to run unauthenticated (public repos only)
GITHUB_APP_PRIVATE_KEY_PATH keep the default and put the PEM at ./secrets/github-app.pem
GITHUB_WEBHOOK_SECRET the webhook secret configured on the GitHub App
REGISTRY_USER / REGISTRY_PASSWORD credentials composer clients use (cleartext)

GitHub App setup

Only needed for private repositories, or to raise GitHub's API rate limit above the ~60/hour anonymous ceiling. Skip this whole section and leave GITHUB_APP_ID empty to index public repositories only.

Create a GitHub App (org or personal account → Settings → Developer settings → GitHub Apps → New GitHub App):

  • Webhook URL: https://your-host/webhook/github, with a generated secret (openssl rand -hex 32) → GITHUB_WEBHOOK_SECRET
  • Permissions: Repository contents — Read-only, Metadata — Read-only
  • Subscribe to events: create, push, delete
  • Identifying and authorizing users (Callback URL, "Request user authorization (OAuth) during installation"): leave this whole section blank/unchecked. pak has no user login of any kind — the app talks to GitHub server-to-server only (a JWT signed with its private key, exchanged for a short-lived installation token), never through a browser, so there is nothing for a callback URL to do.

After creating the app: copy its App ID — the plain number at the top of the settings page — into GITHUB_APP_ID. Don't use the Client ID shown right next to it (looks like Iv1.xxxxxxxxxxxxxxxx); that's for the OAuth user-login flow pak doesn't use, and using it here fails auth with a flat 401 on every request. Then, further down the same page under "Private keys," click "Generate a private key" — GitHub creates the RSA keypair itself (keeping the public half; pak never sees or needs it) and downloads the private half as a .pem file, this once. Move that file to wherever GITHUB_APP_PRIVATE_KEY_PATH points (./secrets/github-app.pem by default, chmod 600 it) — no format conversion needed. Finally install the app on the repositories or the whole org you want indexed.

You don't need to install it on every repository you plan to add: if package:add/package:update hits a repository the app has no installation covering, pak automatically retries unauthenticated instead of failing — which only ever succeeds for genuinely public repos, since a private one still 404s unauthenticated. So a single instance can mix private repositories (via the app) with arbitrary public ones.

Because the webhook is app-level, every installed repository reports tag and branch activity automatically — no per-repo webhook setup.

The webhook endpoint is authenticated by GitHub's HMAC signature; everything else — including the plain package list at / — sits behind HTTP basic auth (/healthz excepted).

CI

.woodpecker/build.yaml runs the test suite and then builds and pushes the image (Woodpecker CI, buildx plugin with registry layer caching). Set the registry, registry_username and registry_password secrets and mark the repo as trusted; pushes to main publish :latest, git tags vX.Y.Z publish semver tags.

Kubernetes

Manifests (Deployment, Service, PVC, Ingress, kustomization) live in kubernetes/ together with deployment instructions.

Consuming the registry

In a project's composer.json:

{
    "repositories": [
        {"type": "composer", "url": "https://packages.example.org"}
    ]
}

and in auth.json (or composer config --global):

{
    "http-basic": {
        "packages.example.org": {"username": "composer", "password": "…"}
    }
}

For private repositories, clients also need a GitHub token (composer config --global github-oauth.github.com <token>) because dist archives are downloaded from GitHub directly.

Commands

Command Purpose
package:add <url or owner/name> add a repository, index it immediately
package:remove <url or owner/name> remove a repository and its published metadata
package:update <url or owner/name> manual reindex (same code path as a webhook)
registry:build full rebuild of everything (initial population / recovery)

Run them with docker compose exec app php bin/console ….

The app container also runs registry:build itself on every start (see bin/docker-entrypoint), before it starts serving traffic — so a fresh volume, a redeploy, or config/registry.json edited by hand while the container was stopped all end up fully reconciled without a manual step. A failed rebuild at startup doesn't block boot; it logs a warning and serves whatever was already on disk.

Development

No local PHP needed; the composer image (PHP 8.5) works for everything:

alias dphp='docker run --rm -u $(id -u) -e COMPOSER_HOME=/tmp/ch -v $PWD:/app -w /app composer:2'
dphp composer install
dphp php vendor/bin/phpunit

Locally (outside Docker) the repository list is config/registry.json and generated metadata lands in var/data/repo/ — see the pak registry block in .env.

Not implemented (by design)

Satis features intentionally dropped: dist archive mirroring/purge, version-constraint cherry-picking, require-dependencies resolution, and Packagist proxying. / is a minimal, human-only package list — not a searchable or styled Satis-style index.