From 0c28d572af764a932cf177dca5705f08a3937344 Mon Sep 17 00:00:00 2001 From: Florian Weber <2+f.weber@noreply.git.morlana.online> Date: Wed, 12 Aug 2026 09:47:07 +0000 Subject: [PATCH] Add Packaging --- Packaging.md | 102 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 Packaging.md diff --git a/Packaging.md b/Packaging.md new file mode 100644 index 0000000..7d6319e --- /dev/null +++ b/Packaging.md @@ -0,0 +1,102 @@ +# Packaging & declarative state + +## Build and push a package + +```bash +docker run --rm -v "$PWD:/work" -w /work \ + git.morlana.online/f.weber/aptly-deb-builder: \ + aptly-release --config nfpm.yaml -- --repo stable --distribution stable +``` + +`aptly-release` = `aptly-pack` (builds a `.deb`) + `aptly-push` (uploads it, includes +it into the repo, refreshes the publish target) in one call. Everything before `--` +goes to `aptly-pack`, everything after goes to `aptly-push`; the built `.deb` files +are appended automatically. + +**Two packaging paths**, auto-detected: +- no `debian/` directory present → **nfpm** (`nfpm.yaml`, no Debian toolchain + knowledge needed) +- `debian/` directory present → real `dpkg-buildpackage -us -uc -b` + (unsigned — signing is aptly's job at publish time, not the package's) + +Auth: `APTLY_USER`/`APTLY_PASSWORD` (or none at all, for an open repo) — works +unchanged across all four presets from [security.md](security.md). Against a signed +repo: do **not** pass `--no-sign`, and make sure the server has a signing key (see +below); against an unsigned repo: pass `--no-sign`. + +## Reusable Gitea Action + +```yaml +- uses: f.weber/aptly-containerized@image/v1.6.3-1 + with: + url: https://apt.example.com + repo: stable + distribution: stable + username: ${{ secrets.APTLY_USER }} + password: ${{ secrets.APTLY_PASSWORD }} +``` + +Lives at the repo root deliberately: `uses:` references to other Gitea repos **with +a subpath** (`owner/repo/path/action@ref`) are not reliable, only root and local +(`./.gitea/actions/x`) references are. A more robust fallback that needs no action +resolution at all: call the image directly (see above). + +## Declarative state (`state.yaml`) + +Consumed by `aptly-reconcile` — a single file in Compose (`compose/config/state.yaml`), +generated from `values.yaml`'s `aptly.{localRepos,mirrors,publish}` in the Helm +chart. Runs exclusively against the **REST API**, never against the aptly CLI: +`aptly api serve` holds the LevelDB lock, and a CLI call against the same `rootDir` +would either block or (with `-no-lock`) endanger the database. + +```yaml +localRepos: + - { name: stable, comment: "", defaultDistribution: stable, defaultComponent: main } +mirrors: + - name: debian-security + archiveURL: http://security.debian.org/debian-security + distribution: trixie-security + components: [main] + architectures: [amd64, arm64] +publish: + - name: stable-root # only internal to the reconciler, not part of the aptly API + prefix: "" # "" = repo root + distribution: stable + sourceKind: local # local | snapshot + sources: [{ name: stable, component: main }] + architectures: [amd64, arm64] + acquireByHash: true +``` + +**Known, deliberate limitation:** `mirrors[].components` cannot be changed after a +mirror has been created — aptly's API has no endpoint for that. Changing it requires +deleting and recreating the mirror. Everything else in a mirror definition (filter, +architectures, download flags, …), and all of a local repo's metadata, is +synchronized on every run. + +**Idempotency:** GET-before-write, never POST-and-swallow-40x (a swallowed 400 would +also skip real *changes*). `reconcile.failOnError` (Helm) resp. `APTLY_FAIL_ON_ERROR` +(Compose/env) defaults to `false` — an unreachable mirror must not make +`helm upgrade`/`docker compose up` fail. + +**The prefix-escaping pitfall:** aptly's API expects the root prefix in the URL as +the literal `.` (not an empty path) — and `curl` normalizes away `/./` segments +client-side unless you set `--path-as-is` (every script here already does). Coming +back the other way, `GET /api/publish` also returns the root prefix as the literal +`"."` (empirically verified, not `""`) — so when checking "does this publish target +already exist", you need to compare against `"."`, not against the empty string from +`values.yaml`/`state.yaml`. Nested prefixes (`debian/stable`), on the other hand, are +returned with real slashes, not underscore-escaped — the escaped form (`/` → `_`) is +only a quirk of URL *path* construction, not of the stored/listed values. Both are +already handled correctly in `rootfs/usr/local/bin/aptly-reconcile` (`api_prefix()` +and the `prefix_for_compare` special case) — worth knowing if you write your own +scripts against the same API. + +## GPG + +Default: the `gpg` provider (a real gnupg binary, included in the image). Key source +in production is always a Secret (`aptly.gpg.signingKey.existingSecret` in the +chart, `compose/config/gpg/private.asc` in Compose) — never inline in `values.yaml`. +The key ID is auto-detected (`gpg --list-secret-keys --with-colons`); the public key +is served under `/signing-key.asc` so clients can reference it directly via +`signed-by=` (a copy-paste snippet appears in the Helm NOTES resp. `compose/README`).