From 05486d51c88958e5c55b50194b4c6c43d2c942f8 Mon Sep 17 00:00:00 2001 From: Joshua Gilman Date: Fri, 21 Aug 2026 21:30:31 -0700 Subject: [PATCH] docs(packages): document native installs --- ...l-release-cli-from-package-repositories.md | 168 ++++++++++++++++++ docs/how-to/set-up-package-repository.md | 45 ++--- .../go-release/.github/workflows/release.yml | 13 ++ examples/go-release/README.md | 26 ++- 4 files changed, 222 insertions(+), 30 deletions(-) create mode 100644 docs/how-to/install-release-cli-from-package-repositories.md diff --git a/docs/how-to/install-release-cli-from-package-repositories.md b/docs/how-to/install-release-cli-from-package-repositories.md new file mode 100644 index 0000000..7051c98 --- /dev/null +++ b/docs/how-to/install-release-cli-from-package-repositories.md @@ -0,0 +1,168 @@ +# Install release-cli from native package repositories + +Use this guide to install and update `release-cli` from the signed APT, DNF, or APK repository at `https://pkgs.meigma.dev`. + +The repository publishes amd64 and arm64 packages. Each client verifies both repository metadata and the producer package before installation. Do not disable either check when recovering a failed install. + +## Prerequisites + +You need: + +- root access on the target host; +- a working system clock and HTTPS certificate store; +- Debian or Ubuntu with APT, a DNF-based Linux distribution, or Alpine Linux. + +## Install with APT + +Install the HTTPS prerequisites from the distribution repositories: + +```sh +sudo apt-get update +sudo apt-get install -y ca-certificates curl +``` + +Install the repository key in a dedicated keyring file: + +```sh +sudo install -d -m 0755 /etc/apt/keyrings +curl --fail --silent --show-error --location \ + https://pkgs.meigma.dev/keys/apt-repository-001.asc \ + | sudo tee /etc/apt/keyrings/meigma-packages.asc >/dev/null +sudo chmod 0644 /etc/apt/keyrings/meigma-packages.asc +``` + +Add the signed repository and install the command: + +```sh +printf '%s\n' \ + 'deb [signed-by=/etc/apt/keyrings/meigma-packages.asc] https://pkgs.meigma.dev/apt stable main' \ + | sudo tee /etc/apt/sources.list.d/meigma-packages.list >/dev/null +sudo apt-get update +sudo apt-get install -y release-cli +``` + +Confirm the installed package: + +```sh +dpkg-query --show --showformat='${Package} ${Version} ${Architecture}\n' release-cli +``` + +## Install with DNF + +Create a repository definition that checks the aggregate repository signature and the producer RPM signature: + +```sh +sudo tee /etc/yum.repos.d/meigma-packages.repo >/dev/null <<'EOF' +[meigma-packages] +name=Meigma packages +baseurl=https://pkgs.meigma.dev/rpm/stable/$basearch +enabled=1 +gpgcheck=1 +repo_gpgcheck=1 +gpgkey=https://pkgs.meigma.dev/keys/rpm-repository-001.asc https://pkgs.meigma.dev/keys/release-rpm-001.asc +EOF +``` + +Install and inspect the package: + +```sh +sudo dnf install -y release-cli +rpm -q --qf '%{NAME} %{VERSION}-%{RELEASE} %{ARCH}\n' release-cli +``` + +Keep both `gpgcheck=1` and `repo_gpgcheck=1`. The first checks the producer-signed RPM; the second checks aggregate repository metadata. + +## Install with APK + +APK identifies signing keys by filename. Keep the published filenames unchanged when you install them: + +```sh +sudo wget -q \ + https://pkgs.meigma.dev/keys/apk-index-001.rsa.pub \ + -O /etc/apk/keys/apk-index-001.rsa.pub +sudo wget -q \ + https://pkgs.meigma.dev/keys/meigma-release-001.rsa.pub \ + -O /etc/apk/keys/meigma-release-001.rsa.pub +``` + +Add the repository, refresh its signed index, and install the command: + +```sh +printf '%s\n' 'https://pkgs.meigma.dev/apk/stable/main' \ + | sudo tee -a /etc/apk/repositories >/dev/null +sudo apk update +sudo apk add release-cli +apk info --installed release-cli +``` + +The aggregate key verifies `APKINDEX.tar.gz`. The producer key verifies the package. Both keys are required. + +## Update release-cli + +Use the package manager's normal update path: + +```sh +sudo apt-get update && sudo apt-get install --only-upgrade release-cli +``` + +```sh +sudo dnf upgrade release-cli +``` + +```sh +sudo apk update && sudo apk upgrade release-cli +``` + +The repository retains published versions. A normal install selects the newest version accepted by the client unless the host has a package pin or version lock. + +## Recover a failed install + +### TLS verification fails + +Check the system clock and update the host's CA certificate package from its distribution repository. Do not replace `https://pkgs.meigma.dev` with HTTP and do not disable certificate verification. + +### A repository or package signature is rejected + +Stop before installing the package. Do not use APT `trusted=yes`, DNF `gpgcheck=0`, APK `--allow-untrusted`, or a similar bypass. + +Confirm that the configured key URLs and filenames match the reviewed package policy. The current public names are: + +- `apt-repository-001.asc` for APT metadata; +- `rpm-repository-001.asc` and `release-rpm-001.asc` for DNF metadata and packages; +- `apk-index-001.rsa.pub` and `meigma-release-001.rsa.pub` for APK indexes and packages. + +A key replacement uses a new reviewed public-key object; immutable key objects are not overwritten. Replace keys manually: + +1. Obtain the new key URL, expected fingerprint or digest, and activation notice through the trusted release channel. +2. Download the new key to a temporary file over HTTPS. +3. Compare its fingerprint or digest with the independently reviewed value. +4. Install the new key alongside the old key. For APK, preserve the announced basename exactly. +5. Update the APT `signed-by` path, DNF `gpgkey` list, or APK key set to include the new key. +6. Refresh repository metadata and complete one verified installation. +7. Remove the retired key only after the refreshed metadata and package both verify. + +If no reviewed replacement exists, leave the old configuration in place and report the failure. A repeated signature error can indicate stale metadata, an incomplete key rollout, or tampering; bypassing verification hides the distinction. + +### The requested version is unavailable + +Refresh metadata, then inspect the versions visible to the client: + +```sh +apt-cache policy release-cli +``` + +```sh +dnf --showduplicates list release-cli +``` + +```sh +apk policy release-cli +``` + +If the release is visible on GitHub but absent from the package repository, wait for the central publication workflow or contact its operator. Do not install an unverified package copied from a failed publication run. + +### An update was interrupted + +Run the metadata refresh and install command again. Publication is convergent: immutable package objects are never overwritten, and repository roots become visible only after their referenced objects are uploaded. A replay either completes the same state or fails without accepting conflicting immutable content. + +For repository operation and replay procedures, see [Set up the shared package repository](set-up-package-repository.md). The [package repository contract](../reference/package-repository-contract.md) defines the public paths, trust checks, cache behavior, and publication states. diff --git a/docs/how-to/set-up-package-repository.md b/docs/how-to/set-up-package-repository.md index b47a1ce..adb12c8 100644 --- a/docs/how-to/set-up-package-repository.md +++ b/docs/how-to/set-up-package-repository.md @@ -1,6 +1,6 @@ # Set up the shared package repository -Use this guide to publish verified DEB, RPM, and APK packages from producer GitHub Releases to a static Cloudflare R2 repository. The [package repository contract](../reference/package-repository-contract.md) defines the accepted release, configuration, object layout, trust checks, and recovery behavior. +Use this guide to publish verified DEB, RPM, and APK packages from producer GitHub Releases to a static Cloudflare R2 repository. The [package repository contract](../reference/package-repository-contract.md) defines the accepted release, configuration, object layout, trust checks, and recovery behavior. After publication, give consumers the [native package installation guide](install-release-cli-from-package-repositories.md). ## Prerequisites @@ -129,31 +129,34 @@ Keep this workflow on the central repository's default branch. GitHub runs `repo ## Dispatch from a producer -After the producer publishes its GitHub Release, mint a short-lived GitHub App token whose installation includes the central repository. Send only the producer repository and exact stable tag: +Add the reusable dispatch job after the producer's public GitHub Release job. +Keep it disabled during setup: ```yaml -- name: Create package repository token - id: package-app - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 +package-repository: + name: Request package repository publication + needs: github-release + permissions: {} + uses: meigma/release/.github/workflows/request-package-repository.yml@583937edadfbae183e49f16df46b98e0b36807ba # v0.1.16 with: - client-id: ${{ vars.MEIGMA_RELEASE_APP_CLIENT_ID }} - private-key: ${{ secrets.MEIGMA_RELEASE_APP_PRIVATE_KEY }} - owner: meigma - repositories: pkgs - -- name: Request package repository publication - env: - GH_TOKEN: ${{ steps.package-app.outputs.token }} - PRODUCER_REPOSITORY: ${{ github.repository }} - PRODUCER_TAG: ${{ github.ref_name }} - run: | - gh api --method POST repos/meigma/pkgs/dispatches \ - -f event_type=package-release \ - -f "client_payload[repository]=${PRODUCER_REPOSITORY}" \ - -f "client_payload[tag]=${PRODUCER_TAG}" + package-repository-owner: meigma + package-repository-name: pkgs + release-app-client-id: ${{ vars.MEIGMA_RELEASE_APP_CLIENT_ID }} + publish-package-repository: false + secrets: + release-app-private-key: ${{ secrets.MEIGMA_RELEASE_APP_PRIVATE_KEY }} ``` -The GitHub App needs permission to send the dispatch to `meigma/pkgs`. It does not need R2 or aggregate signing credentials. +The reusable workflow mints a short-lived token scoped to the central +repository and sends only the producer repository and exact tag. The GitHub App +installation needs repository Contents write permission on `meigma/pkgs` so it +can create `repository_dispatch` events. It does not receive R2 credentials or +aggregate signing keys. + +After the central policy, keys, environment, and receiver workflow are ready, +change `publish-package-repository` to `true`. Keep the job ordered after the +public GitHub Release; the receiver rejects a missing, draft, or mismatched +release. ## Verify the first publication diff --git a/examples/go-release/.github/workflows/release.yml b/examples/go-release/.github/workflows/release.yml index 715c634..118f1f0 100644 --- a/examples/go-release/.github/workflows/release.yml +++ b/examples/go-release/.github/workflows/release.yml @@ -78,3 +78,16 @@ jobs: publish-release: false secrets: release-app-private-key: ${{ secrets.MEIGMA_RELEASE_APP_PRIVATE_KEY }} + + package-repository: + name: Request package repository publication + needs: github-release + permissions: {} + uses: meigma/release/.github/workflows/request-package-repository.yml@583937edadfbae183e49f16df46b98e0b36807ba # v0.1.16 + with: + package-repository-owner: OWNER + package-repository-name: packages + release-app-client-id: ${{ vars.MEIGMA_RELEASE_APP_CLIENT_ID }} + publish-package-repository: false + secrets: + release-app-private-key: ${{ secrets.MEIGMA_RELEASE_APP_PRIVATE_KEY }} diff --git a/examples/go-release/README.md b/examples/go-release/README.md index 421fc41..793da4c 100644 --- a/examples/go-release/README.md +++ b/examples/go-release/README.md @@ -1,8 +1,8 @@ # Go release example -This directory models a repository named `example` with module `example.com/meigma/release-consumer` and command `./cmd/example`. It contains the minimum source needed to build GitHub Release assets and a multi-architecture OCI image. It is not a complete CI policy: add the consumer repository's own build, test, review, and branch-protection controls. +This directory models a repository named `example` with module `example.com/meigma/release-consumer` and command `./cmd/example`. It contains the minimum source needed to build GitHub Release assets and a multi-architecture OCI image, plus a disabled package-repository dispatch job. It is not a complete CI policy: add the consumer repository's own build, test, review, and branch-protection controls. -See [Configure GitHub Releases](../../docs/how-to/configure-github-releases.md) for release credential setup and [Configure OCI image publication](../../docs/how-to/configure-oci-images.md) for image configuration, publication, and verification. See [Rehearse and recover GitHub Releases](../../docs/how-to/rehearse-and-recover-github-releases.md) before the first publication. Use [Upgrade GitHub Release workflows](../../docs/how-to/upgrade-github-release-workflows.md) to change the pinned revision. The reusable interfaces are defined in the [GitHub Release contract](../../docs/reference/github-release-contract.md) and [OCI image contract](../../docs/reference/oci-image-contract.md). +See [Configure GitHub Releases](../../docs/how-to/configure-github-releases.md) for release credential setup and [Configure OCI image publication](../../docs/how-to/configure-oci-images.md) for image configuration, publication, and verification. [Set up the shared package repository](../../docs/how-to/set-up-package-repository.md) defines the central receiver, producer policy, public keys, and signing prerequisites; [Install release-cli from native package repositories](../../docs/how-to/install-release-cli-from-package-repositories.md) shows the resulting client flow. See [Rehearse and recover GitHub Releases](../../docs/how-to/rehearse-and-recover-github-releases.md) before the first publication. Use [Upgrade GitHub Release workflows](../../docs/how-to/upgrade-github-release-workflows.md) to change the pinned revision. The reusable interfaces are defined in the [GitHub Release contract](../../docs/reference/github-release-contract.md), [OCI image contract](../../docs/reference/oci-image-contract.md), and [package repository contract](../../docs/reference/package-repository-contract.md). ## Files to copy @@ -37,8 +37,9 @@ The stage command runs `goreleaser release --clean --skip=publish` under mise's The GoReleaser configuration packages each Linux build as DEB, RPM, and APK and emits one SBOM for every archive and native package. These standalone -packages are GitHub Release assets; the example does not configure a package -repository or native package-manager signing. +packages are GitHub Release assets. The example does not configure native +RPM or APK signing. Its package-repository request remains disabled until the +producer is reviewed, allowlisted, and configured with both signing keys. GoReleaser has no command-line distribution-directory option in this invocation. The consumer's `.goreleaser.yaml` must write the same distribution directory that the workflow passes to `release-cli stage --dist`. This example uses GoReleaser's default `dist` directory and passes `--dist dist`. @@ -58,20 +59,27 @@ Replace these project-specific example values: - `initial-version` value `0.1.0` in `release-please-config.json` if the first intended release differs. - Manifest value `0.0.0` in `.release-please-manifest.json` if the consumer already has a release. Use its latest released version without the `v` prefix. - Linker variables `main.version` and `main.commit` in `.goreleaser.yaml` if the consumer command exposes version data through different variables. The copied sample defines both variables and prints `example ()` for `--version`. +- Package repository owner and repository name in `.github/workflows/release.yml` with the reviewed central receiver. -The example uses one full commit SHA for every reusable workflow reference and -the checksum signing identity. That SHA is the consumer pin for the complete -release unit. The current pin is the `v0.1.3` release revision, -`0fc99489d31d400bc3f69d6636d60e7d3f3d0251`. +The release-asset and OCI jobs use one full commit SHA for their reusable +workflow references and checksum signing identity. That SHA is their consumer +pin for the complete release unit. The current baseline pin is the `v0.1.3` +release revision, `0fc99489d31d400bc3f69d6636d60e7d3f3d0251`. + +The disabled package-repository job uses the `v0.1.16` revision, +`583937edadfbae183e49f16df46b98e0b36807ba`, because that revision introduced +the dispatch workflow and multi-version repository fix. Upgrade the complete +release unit to a reviewed current revision before enabling the job. Keep these contract values unchanged: - all four reusable workflow references at `0fc99489d31d400bc3f69d6636d60e7d3f3d0251`; - `checksum-signing-workflow-ref` value `meigma/release/.github/workflows/go-pre-publish.yml@0fc99489d31d400bc3f69d6636d60e7d3f3d0251`; +- disabled package-repository workflow reference at `583937edadfbae183e49f16df46b98e0b36807ba`; - organization variable `MEIGMA_RELEASE_APP_CLIENT_ID`; - organization secret `MEIGMA_RELEASE_APP_PRIVATE_KEY`; and - the locked Go 1.26.6, GoReleaser 2.17.1, Syft 1.51.0, Cosign 3.1.3, GitHub CLI 2.97.0, Melange 0.59.1, and apko 1.2.37 versions unless the shared workflow contract is deliberately updated. -The caller sets both `publish-image: false` and `publish-release: false` so the first run leaves a populated draft without writing to GHCR. After inspecting the draft and `oci-image` artifact, change both inputs to `true` and follow the recovery guide to publish through the same tag and release. +The caller sets `publish-image: false`, `publish-release: false`, and `publish-package-repository: false`. The first run therefore leaves a populated draft without writing to GHCR or dispatching to a central package repository. After inspecting the draft and `oci-image` artifact, change the image and GitHub Release inputs to `true` and follow the recovery guide to publish through the same tag and release. Enable package-repository dispatch only after the release is public, the central receiver allowlists the producer, native RPM and APK signing is configured, and the receiver's protected environment is ready. The OCI builder retains the signed APK repository, apko lock, SPDX files, and OCI layout in the `oci-image` workflow artifact. The separate publisher verifies that artifact before pushing, signing, and attesting `ghcr.io/owner/repository`.