From e754740c502b120749c879aeac3f871947f407e5 Mon Sep 17 00:00:00 2001 From: ShortArrow Date: Tue, 20 Oct 2026 12:34:08 +0900 Subject: [PATCH 1/2] feat(release): ship Debian packages for the APT registry pkg.shortarrow.jp indexes the .deb files each project attaches to its release with a SHA256SUMS. Building Linux on Ubuntu 22.04 keeps the arm64 binary off glibc 2.39, which bookworm lacks (ADR 0034). --- .github/workflows/ci.yml | 35 ++++++++ .github/workflows/release.yml | 61 +++++++++++++- CHANGELOG.md | 12 +++ README.md | 20 ++++- docs/CONTRIBUTING.md | 10 ++- docs/adr/0034-debian-packages.jp.md | 29 +++++++ docs/adr/0034-debian-packages.md | 69 ++++++++++++++++ docs/adr/README.jp.md | 1 + docs/adr/README.md | 1 + packaging/apt/build-deb.sh | 77 ++++++++++++++++++ packaging/apt/glibc-floor.sh | 20 +++++ packaging/apt/install-test.sh | 86 ++++++++++++++++++++ packaging/apt/test-build-deb.sh | 120 ++++++++++++++++++++++++++++ 13 files changed, 533 insertions(+), 8 deletions(-) create mode 100644 docs/adr/0034-debian-packages.jp.md create mode 100644 docs/adr/0034-debian-packages.md create mode 100644 packaging/apt/build-deb.sh create mode 100644 packaging/apt/glibc-floor.sh create mode 100644 packaging/apt/install-test.sh create mode 100644 packaging/apt/test-build-deb.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f9a2510..3028708 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,6 +81,40 @@ jobs: echo "CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=${TOOLCHAIN}/bin/aarch64-linux-android24-clang" >> "$GITHUB_ENV" - run: cargo check --target aarch64-linux-android + deb: + # The Debian package on the release's build image (ADR 0034): a debug + # build is enough to check the package's layout, its Depends and the + # oldest glibc it declares, by installing it with apt. + name: Debian package (${{ matrix.arch }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + os: ubuntu-22.04 + - arch: arm64 + os: ubuntu-22.04-arm + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install build deps + run: sudo apt-get update && sudo apt-get install -y libwayland-dev apt-utils + - name: Install Rust + run: | + rustup toolchain install stable --profile minimal + rustup default stable + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + - run: cargo build --locked + - name: Build the package and install it with apt + run: | + set -euo pipefail + version="$(sed -n 's/^version = "\(.*\)"$/\1/p' Cargo.toml | head -n 1)" + sh packaging/apt/build-deb.sh target/debug/gitreant "$version" ${{ matrix.arch }} deb + sh packaging/apt/install-test.sh deb "$version" ${{ matrix.arch }} + web: name: Frontend + Storybook runs-on: ubuntu-latest @@ -181,6 +215,7 @@ jobs: with: persist-credentials: false - run: bash .github/scripts/test-release-scripts.sh + - run: bash packaging/apt/test-build-deb.sh - name: The released version has notes in CHANGELOG.md run: > bash .github/scripts/release-notes.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b11636a..8341711 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -70,7 +70,8 @@ jobs: # Desktop targets build natively (GitHub has free arm64 runners for # public repositories, so no cross toolchains for rfd's GUI libraries); # Android (Termux) is the one cross build, linked with the NDK's clang — - # rfd/open are cfg'd out there. + # rfd/open are cfg'd out there. Linux builds on Ubuntu 22.04, whose + # glibc sets the oldest glibc the executable starts on (ADR 0034). name: Build (${{ matrix.target }}) needs: [version, test] runs-on: ${{ matrix.os }} @@ -86,10 +87,10 @@ jobs: os: windows-11-arm archive: zip - target: x86_64-unknown-linux-gnu - os: ubuntu-latest + os: ubuntu-22.04 archive: tar.gz - target: aarch64-unknown-linux-gnu - os: ubuntu-24.04-arm + os: ubuntu-22.04-arm archive: tar.gz - target: x86_64-apple-darwin os: macos-26 @@ -168,9 +169,50 @@ jobs: name: gitreant-${{ matrix.target }} path: gitreant-${{ matrix.target }}.* + deb: + # The Debian packages (ADR 0034), built from the release executables and + # installed with apt in Debian and Ubuntu containers before anything is + # published. The version is Cargo.toml's, which never carries a "-", so + # a prerelease tag rehearses this job too; the package registry indexes + # stable releases only. + name: Debian package (${{ matrix.arch }}) + needs: build + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + target: x86_64-unknown-linux-gnu + os: ubuntu-latest + - arch: arm64 + target: aarch64-unknown-linux-gnu + os: ubuntu-24.04-arm + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: gitreant-${{ matrix.target }} + path: artifacts + - name: Build the package and install it with apt + run: | + set -euo pipefail + sudo apt-get update -qq && sudo apt-get install -y -qq apt-utils + version="$(sed -n 's/^version = "\(.*\)"$/\1/p' Cargo.toml | head -n 1)" + tar xzf artifacts/gitreant-${{ matrix.target }}.tar.gz -C artifacts + sh packaging/apt/build-deb.sh artifacts/gitreant "$version" ${{ matrix.arch }} deb + sh packaging/apt/install-test.sh deb "$version" ${{ matrix.arch }} + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: gitreant-deb-${{ matrix.arch }} + path: deb/*.deb + release: name: Create Release - needs: build + needs: [build, deb] runs-on: ubuntu-latest timeout-minutes: 10 permissions: @@ -188,6 +230,17 @@ jobs: path: artifacts merge-multiple: true + - name: Write SHA256SUMS + # The package registry checks every package against it before + # indexing (ADR 0034), and anyone can check a download the same way. + # Written outside the directory so it does not list itself. + working-directory: artifacts + run: | + set -euo pipefail + find . -maxdepth 1 -type f -printf '%P\n' | LC_ALL=C sort | xargs sha256sum >../SHA256SUMS + mv ../SHA256SUMS . + cat SHA256SUMS + - name: Attest build provenance # Signed SLSA provenance for every artifact, published before the # release itself so a failure aborts instead of shipping unattested diff --git a/CHANGELOG.md b/CHANGELOG.md index 33b3bc5..bdd70e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Debian packages for amd64 and arm64, attached to each release and + installable with `apt` from `pkg.shortarrow.jp`, and a `SHA256SUMS` for + every release asset + ([ADR 0034](https://github.com/ShortArrow/gitreant/blob/main/docs/adr/0034-debian-packages.md)). + +### Fixed + +- The arm64 Linux binary starts on glibc 2.35 and later, Debian 12 and + Raspberry Pi OS bookworm included; it needed glibc 2.39. + ## [0.2.4] - 2026-10-15 ### Added diff --git a/README.md b/README.md index c761268..a1538b9 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,25 @@ Every feature in detail: [docs/FEATURES.md](docs/FEATURES.md). ## Install / Build -Requirements: Rust (stable), Node.js + pnpm. +On Debian 12 or Ubuntu 22.04 and later, amd64 or arm64, from the APT +repository at `pkg.shortarrow.jp` +([ADR 0034](docs/adr/0034-debian-packages.md)): + +```console +$ sudo curl -fsSLo /usr/share/keyrings/shortarrow-archive-keyring.gpg https://pkg.shortarrow.jp/apt/shortarrow-archive-keyring.gpg +$ echo "deb [signed-by=/usr/share/keyrings/shortarrow-archive-keyring.gpg] https://pkg.shortarrow.jp/apt stable main" | sudo tee /etc/apt/sources.list.d/shortarrow.list +$ sudo apt update +$ sudo apt install shortarrow-archive-keyring gitreant +``` + +Installing `shortarrow-archive-keyring` by name keeps it, and the key that +verifies the repository, when gitreant is removed; later keys arrive with +`apt upgrade`. `gitreant --version` checks the install. + +Every GitHub Release also carries the binaries for Windows, macOS, Linux +and Termux, with a `SHA256SUMS`. + +To build from source, you need Rust (stable), Node.js + pnpm: ```console $ make build # build the frontend and embed it into a release binary diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 4ec8df7..e8f7dbd 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -164,9 +164,13 @@ How a release is guarded is [ADR 0030](adr/0030-release-safeguards.md). 1. checks that the tag is on `main`, matches `Cargo.toml` and has a changelog section; 2. runs `cargo test --locked` on every platform it ships for; - 3. builds the native binaries with the frontend embedded, attests them - (SLSA provenance) and attaches them to a GitHub Release whose notes - are the changelog section; + 3. builds the native binaries with the frontend embedded, builds the + Debian packages from the Linux ones and installs them with apt in + containers ([ADR 0034](adr/0034-debian-packages.md)), attests them + all (SLSA provenance) and attaches them, with a `SHA256SUMS`, to a + GitHub Release whose notes are the changelog section. The APT + registry ([ShortArrow/pkg](https://github.com/ShortArrow/pkg)) picks + the packages up from there within the hour; 4. waits for approval in the `release` environment, then publishes the crate to crates.io via OIDC Trusted Publishing. The published package ships the prebuilt `frontend/dist` (see the `include` list in diff --git a/docs/adr/0034-debian-packages.jp.md b/docs/adr/0034-debian-packages.jp.md new file mode 100644 index 0000000..d543f4a --- /dev/null +++ b/docs/adr/0034-debian-packages.jp.md @@ -0,0 +1,29 @@ +# ADR 0034: 共有の APT リポジトリ向けに Debian パッケージを配る + +## Status + +Accepted (2026-10-20) + +## Context + +Linux のユーザーは gitreant をリリースの tar.gz か `cargo install` で入れ、手で入れ替えるまでその版を使い続ける。`https://pkg.shortarrow.jp/apt` は ShortArrow のツールを配る APT リポジトリである([ShortArrow/pkg ADR 0001](https://github.com/ShortArrow/pkg/blob/main/docs/adr/0001-package-registry.md))。リポジトリ自身はパッケージをビルドしない。各プロジェクトが `.deb` をビルドし、インストールできることを確かめ、`SHA256SUMS` と一緒に自分の GitHub Release に添付する。リポジトリは各パッケージをその `SHA256SUMS` で照合し、置かれた場所のまま索引に載せる。パッケージの中身、名前、`Depends` はプロジェクトが決める。 + +リポジトリは公開した版ごとに、`debian:bookworm`(glibc 2.36)と `ubuntu:24.04` にインストールして確かめる。0.2.3 までの gitreant は `ubuntu-latest`(24.04、glibc 2.39)でビルドしている。0.2.3 の実行ファイルを `readelf -V` で見ると、x86_64 版は `GLIBC_2.39` を弱い参照でだけ、`GLIBC_2.34` を必須として要求する。一方 aarch64 版は `GLIBC_2.39` を必須として要求する。動的ローダーは、必須として要求された glibc のバージョンが無い実行ファイルを起動しない。そのため arm64 の tar.gz は、Raspberry Pi OS の bookworm も含め、bookworm では起動しない。 + +## Decision + +- **パッケージ。** 名前は `gitreant`、バージョンはリリースの版に Debian リビジョン `1` を付けたもの(`0.3.0-1`)で、`amd64` と `arm64` を作る。`/usr/bin/gitreant` と、`/usr/share/doc/gitreant/` 以下のライセンスを入れる。ビルドは `packaging/apt/build-deb.sh` が行う。 + - `Depends`:実行ファイルが名指しするライブラリ(`libc6`、`libgcc-s1`、`libwayland-client0`)、`gitreant doctor` が必須とする `git`、リポジトリの `shortarrow-archive-keyring`。最後の 1 つは、gitreant が入っている間、ソースを検証する鍵を残しておくためのもの。 + - `libc6` の下限は、実行ファイルが必須として要求する glibc のバージョンのうち最も高いもの。手で書かず、ビルド時に `readelf` で読み取る(`packaging/apt/glibc-floor.sh`)。 + - `Recommends: xdg-utils`(ブラウザを開く)。`Suggests: gh, gnupg`(`gitreant doctor` が任意とするツール)。 + - `-` を含む版にはパッケージを作らない。Debian の順序では正式版より後ろに並ぶため。リリースのワークフローは版を `Cargo.toml` から取り、そこには `-` が付かない。そのため、プレリリースのタグでも `main` の版でパッケージをビルドしてテストする。 +- **Linux 版のリリース用実行ファイルは `ubuntu-22.04` と `ubuntu-22.04-arm`(glibc 2.35)でビルドする。** これでどちらも bookworm より新しい glibc を要求しない。 +- **リリースのワークフロー**は、リリース用の実行ファイルからパッケージをビルドする。それぞれを使い捨ての署名付きリポジトリから `debian:bookworm`、`ubuntu:22.04`、`ubuntu:24.04` に `apt` でインストールし、`gitreant --version` と `gitreant doctor` を実行する(`packaging/apt/install-test.sh`)。パッケージは、全アセットを載せた `SHA256SUMS` と一緒に GitHub Release に添付する。CI もプルリクエストごとに、同じイメージ上のデバッグビルドで同じことを行う。 + +## Consequences + +- リポジトリが gitreant を載せれば、安定版のリリースは `apt upgrade` で Debian と Ubuntu に届く。載るまでの間や、リポジトリの反映が遅れたときも、パッケージは GitHub Release にある。 +- Release のページからダウンロードした `.deb` は、単独ではインストールできない。`shortarrow-archive-keyring` はリポジトリにしか無いため。 +- arm64 の tar.gz は、bookworm を含む glibc 2.35 以降で起動する。 +- GitHub はいずれ Ubuntu 22.04 のイメージを廃止する。新しいイメージでビルドすると、誰も選ばないまま `libc6` の下限が上がりうる。そのときは bookworm でのインストールテストが失敗するので、ビルドを bookworm のコンテナに移す。 +- GitHub Release には毎回 `SHA256SUMS` が付く。 diff --git a/docs/adr/0034-debian-packages.md b/docs/adr/0034-debian-packages.md new file mode 100644 index 0000000..85406b9 --- /dev/null +++ b/docs/adr/0034-debian-packages.md @@ -0,0 +1,69 @@ +# ADR 0034: Ship Debian packages for the shared APT registry + +## Status + +Accepted (2026-10-20) + +## Context + +A Linux user installs gitreant from a release tarball or with +`cargo install`, and keeps that version until they replace it by hand. +`https://pkg.shortarrow.jp/apt` is the APT repository for ShortArrow's +tools ([ShortArrow/pkg ADR 0001](https://github.com/ShortArrow/pkg/blob/main/docs/adr/0001-package-registry.md)). +It builds no package itself. Each project builds its `.deb` files, tests +that they install, and attaches them to its GitHub Release with a +`SHA256SUMS` asset; the registry checks each package against that file and +indexes it where it is. What a package contains, its name and its +`Depends` are the project's to decide. + +The registry checks every published version by installing it in +`debian:bookworm` (glibc 2.36) and `ubuntu:24.04`. gitreant up to 0.2.3 is +built on `ubuntu-latest` (24.04, glibc 2.39). `readelf -V` on the 0.2.3 +executables shows that the x86_64 one needs `GLIBC_2.39` only weakly and +`GLIBC_2.34` outright, while the aarch64 one needs `GLIBC_2.39` outright. +The dynamic loader refuses to start an executable that needs a missing +glibc version outright, so the arm64 tarball does not start on bookworm, +Raspberry Pi OS bookworm included. + +## Decision + +- **The package.** `gitreant`, versioned as the release with Debian + revision `1` (`0.3.0-1`), for `amd64` and `arm64`. It installs + `/usr/bin/gitreant` and the licences under `/usr/share/doc/gitreant/`. + `packaging/apt/build-deb.sh` builds it. + - `Depends`: the libraries the executable names (`libc6`, `libgcc-s1`, + `libwayland-client0`), `git`, which `gitreant doctor` reports as + required, and the registry's `shortarrow-archive-keyring`, so the key + that verifies the source stays installed while gitreant is. + - The `libc6` floor is the highest glibc version the executable needs + outright, read with `readelf` at build time + (`packaging/apt/glibc-floor.sh`), not written by hand. + - `Recommends: xdg-utils`, which opens the browser; `Suggests: gh, + gnupg`, the tools `gitreant doctor` reports as optional. + - A version containing `-` gets no package: it would sort after the final + release in Debian's ordering. The release workflow takes the version + from `Cargo.toml`, which never has one, so a prerelease tag builds and + tests the package at the version `main` carries. +- **The Linux release executables are built on `ubuntu-22.04` and + `ubuntu-22.04-arm`** (glibc 2.35), so neither needs a glibc newer than + bookworm's. +- **The release workflow** builds the packages from the release + executables, installs each with `apt` from a throwaway signed repository + in `debian:bookworm`, `ubuntu:22.04` and `ubuntu:24.04`, and runs + `gitreant --version` and `gitreant doctor` there + (`packaging/apt/install-test.sh`). It attaches them to the GitHub Release + with a `SHA256SUMS` listing every asset. CI does the same on every pull + request with a debug build on the same images. + +## Consequences + +- Once the registry lists gitreant, `apt upgrade` delivers each stable + release to Debian and Ubuntu machines. Until then, and whenever the + registry lags, the packages are on the GitHub Release. +- A `.deb` downloaded from the Release page does not install on its own: + `shortarrow-archive-keyring` exists only in the registry. +- The arm64 tarball starts on glibc 2.35 and later, bookworm included. +- GitHub will retire the Ubuntu 22.04 images. A build on a newer image can + raise the `libc6` floor without anyone choosing it; the bookworm install + test then fails, and the build has to move into a bookworm container. +- Every GitHub Release carries a `SHA256SUMS`. diff --git a/docs/adr/README.jp.md b/docs/adr/README.jp.md index ba59546..f22c56f 100644 --- a/docs/adr/README.jp.md +++ b/docs/adr/README.jp.md @@ -37,3 +37,4 @@ gitreant の設計判断の記録。各 ADR は決定時点で凍結され、変 | [0031](0031-wording-only-changes.jp.md) | 片方の言語だけの言い回しの修正はコミットの印で日英同時更新の対象から外す | Accepted | | [0032](0032-static-crt-on-windows.jp.md) | Windows 版は C ランタイムを静的リンクする | Accepted | | [0033](0033-diff-attribute.jp.md) | バイナリかどうかは git diff と同じく diff 属性で決める | Accepted | +| [0034](0034-debian-packages.jp.md) | 共有の APT リポジトリ向けに Debian パッケージを配る | Accepted | diff --git a/docs/adr/README.md b/docs/adr/README.md index 30d17f0..76d2752 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -41,3 +41,4 @@ are canonical; the files below are their English translations. | [0031](0031-wording-only-changes.md) | Exempt a one-language rewording from the bilingual check by a commit mark | Accepted | | [0032](0032-static-crt-on-windows.md) | Link the C runtime statically on Windows | Accepted | | [0033](0033-diff-attribute.md) | Decide binary diffs by the diff attribute, as git diff does | Accepted | +| [0034](0034-debian-packages.md) | Ship Debian packages for the shared APT registry | Accepted | diff --git a/packaging/apt/build-deb.sh b/packaging/apt/build-deb.sh new file mode 100644 index 0000000..c09c037 --- /dev/null +++ b/packaging/apt/build-deb.sh @@ -0,0 +1,77 @@ +#!/bin/sh +# Builds the gitreant Debian package from a release executable (ADR 0034): +# /usr/bin/gitreant and the licences under /usr/share/doc/gitreant. +# +# Usage: build-deb.sh +# Writes /gitreant_-1_.deb and prints its path. +# Needs dpkg-deb and readelf. +set -eu + +exe="${1:?usage: build-deb.sh }" +version="${2:?version}" +arch="${3:?arch}" +out="${4:?out dir}" +repo="$(cd "$(dirname "$0")/../.." && pwd)" + +case "$version" in + *-*) + echo "build-deb: $version is a pre-release, which gets no package (ADR 0034)" >&2 + exit 1 + ;; +esac +case "$arch" in + amd64 | arm64) ;; + *) + echo "build-deb: unknown architecture $arch" >&2 + exit 1 + ;; +esac +[ -f "$exe" ] || { + echo "build-deb: $exe is not a file" >&2 + exit 1 +} +glibc="$(readelf -V -W "$exe" | sh "$repo/packaging/apt/glibc-floor.sh")" + +root="$(mktemp -d)" +trap 'rm -rf "$root"' EXIT + +chmod 0755 "$root" +mkdir -p "$root/usr/bin" "$root/usr/share/doc/gitreant" "$root/DEBIAN" +install -m 0755 "$exe" "$root/usr/bin/gitreant" +install -m 0644 "$repo/LICENSE-MIT" "$repo/LICENSE-APACHE" "$root/usr/share/doc/gitreant/" +cat >"$root/usr/share/doc/gitreant/copyright" <<'EOF' +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: gitreant +Source: https://github.com/ShortArrow/gitreant + +Files: * +Copyright: ShortArrow +License: MIT or Apache-2.0 + The full texts are in /usr/share/doc/gitreant/LICENSE-MIT and + /usr/share/doc/gitreant/LICENSE-APACHE. +EOF +chmod 0644 "$root/usr/share/doc/gitreant/copyright" + +installed_size="$(du -sk "$root/usr" | cut -f1)" +cat >"$root/DEBIAN/control" < +Installed-Size: ${installed_size} +Depends: libc6 (>= ${glibc}), libgcc-s1, libwayland-client0, git, shortarrow-archive-keyring +Recommends: xdg-utils +Suggests: gh, gnupg +Section: vcs +Priority: optional +Homepage: https://github.com/ShortArrow/gitreant +Description: commit graphs of local git repositories in the browser + gitreant serves the commit graphs of local repositories as a single-page + web app and opens it in the browser: several repositories side by side, + with their submodules, worktrees, diffs and signatures. +EOF + +mkdir -p "$out" +deb="$out/gitreant_${version}-1_${arch}.deb" +dpkg-deb --root-owner-group -Zxz --build "$root" "$deb" >/dev/null +echo "$deb" diff --git a/packaging/apt/glibc-floor.sh b/packaging/apt/glibc-floor.sh new file mode 100644 index 0000000..eb00a6b --- /dev/null +++ b/packaging/apt/glibc-floor.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# Prints the lowest glibc an executable loads on: the highest GLIBC_x.y +# version it needs without the WEAK flag (ADR 0034). The dynamic loader +# refuses to start an executable that needs a missing version outright, +# and skips one needed only weakly. +# +# Usage: readelf -V -W | glibc-floor.sh +# Exits 1 when the input names no glibc version. +set -eu + +floor="$( + sed -n 's/.*Name: GLIBC_\([0-9][0-9.]*\) *Flags: none.*/\1/p' | + sort -t. -k1,1n -k2,2n -k3,3n | + tail -n 1 +)" +[ -n "$floor" ] || { + echo "glibc-floor: no glibc version needed" >&2 + exit 1 +} +echo "$floor" diff --git a/packaging/apt/install-test.sh b/packaging/apt/install-test.sh new file mode 100644 index 0000000..be7a4cd --- /dev/null +++ b/packaging/apt/install-test.sh @@ -0,0 +1,86 @@ +#!/bin/sh +# Installs the gitreant package with apt in fresh Debian and Ubuntu +# containers, then runs `gitreant --version` and `gitreant doctor` +# (ADR 0034). The package is served from a throwaway repository signed +# with a key made for this run, beside a stand-in +# shortarrow-archive-keyring package that holds that key, so apt checks +# the signature and resolves Depends as it does against the registry. +# +# Usage: install-test.sh +# Needs docker, gpg, dpkg-deb and apt-ftparchive (apt-utils). +set -eu + +debs="${1:?usage: install-test.sh }" +version="${2:?version}" +arch="${3:?arch}" +keyring_name=shortarrow-archive-keyring + +work="$(mktemp -d)" +containers='' +cleanup() { + for c in $containers; do docker rm -f "$c" >/dev/null 2>&1 || true; done + rm -rf "$work" +} +trap cleanup EXIT + +export GNUPGHOME="$work/gnupg" +mkdir -m 0700 "$GNUPGHOME" +gpg --batch --quiet --passphrase '' --quick-gen-key 'gitreant install test ' ed25519 sign 1d + +repo="$work/repo/apt" +mkdir -p "$repo/pool/test" "$repo/dists/stable/main/binary-$arch" +gpg --batch --export >"$repo/$keyring_name.gpg" + +kr="$work/keyring" +mkdir -p "$kr/DEBIAN" "$kr/usr/share/keyrings" +cp "$repo/$keyring_name.gpg" "$kr/usr/share/keyrings/" +cat >"$kr/DEBIAN/control" < +Description: throwaway keyring for the gitreant install test +EOF +dpkg-deb --root-owner-group --build "$kr" "$repo/pool/test/${keyring_name}_0.0.0~test_all.deb" >/dev/null +cp "$debs"/gitreant_*_"$arch".deb "$repo/pool/test/" + +( + cd "$repo" + apt-ftparchive --arch "$arch" packages pool >"dists/stable/main/binary-$arch/Packages" + gzip -k9 "dists/stable/main/binary-$arch/Packages" + apt-ftparchive \ + -o APT::FTPArchive::Release::Suite=stable \ + -o APT::FTPArchive::Release::Codename=stable \ + -o APT::FTPArchive::Release::Components=main \ + -o "APT::FTPArchive::Release::Architectures=$arch" \ + release dists/stable >"$work/Release" + mv "$work/Release" dists/stable/Release + gpg --batch --yes --clearsign -o dists/stable/InRelease dists/stable/Release +) + +failed=0 +for image in debian:bookworm ubuntu:22.04 ubuntu:24.04; do + name="gitreant-install-test-$$-$(echo "$image" | tr ':.' '--')" + containers="$containers $name" + docker create --name "$name" "$image" sh -c " + set -eu + cp /repo/apt/$keyring_name.gpg /usr/share/keyrings/ + echo 'deb [signed-by=/usr/share/keyrings/$keyring_name.gpg] file:/repo/apt stable main' \ + >/etc/apt/sources.list.d/gitreant.list + apt-get update -qq + DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends $keyring_name gitreant >/dev/null + out=\$(gitreant --version) + echo \"gitreant --version -> \$out\" + [ \"\$out\" = 'gitreant $version' ] || { echo 'expected gitreant $version'; exit 1; } + gitreant doctor + " >/dev/null + docker cp "$work/repo" "$name:/repo" + echo "== $image ($arch)" + if docker start -a "$name"; then + echo "install test passed on $image" + else + echo "install test FAILED on $image" + failed=1 + fi +done +exit "$failed" diff --git a/packaging/apt/test-build-deb.sh b/packaging/apt/test-build-deb.sh new file mode 100644 index 0000000..43e82e2 --- /dev/null +++ b/packaging/apt/test-build-deb.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# Behaviour examples for build-deb.sh (ADR 0034), run against a stand-in +# executable. Needs dpkg-deb and readelf. +set -euo pipefail + +here="$(cd "$(dirname "$0")" && pwd)" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +failures=0 + +# A real ELF executable stands in for gitreant, since the libc6 floor is +# read from its symbol versions. +cp "$(type -P true)" "$work/gitreant" +chmod 0644 "$work/gitreant" + +# expect +expect() { + local description="$1" expected="$2" + shift 2 + local actual=0 + "$@" >"$work/out" 2>"$work/err" || actual=$? + if [ "$actual" -ne "$expected" ]; then + echo "FAIL: $description (exit $actual, expected $expected)" + sed 's/^/ stderr: /' "$work/err" + failures=$((failures + 1)) + else + echo "ok: $description" + fi +} + +# check : passes when the command succeeds +check() { + local description="$1" + shift + if "$@" >/dev/null 2>&1; then + echo "ok: $description" + else + echo "FAIL: $description" + failures=$((failures + 1)) + fi +} + +expect "a release version builds a package" 0 \ + bash "$here/build-deb.sh" "$work/gitreant" 0.3.0 amd64 "$work/out-amd64" +deb="$work/out-amd64/gitreant_0.3.0-1_amd64.deb" +check "the package is named gitreant_-1_.deb" test -f "$deb" +check "build-deb.sh prints the package's path" grep -qxF "$deb" "$work/out" + +field() { dpkg-deb --field "$deb" "$1"; } +check "Package is gitreant" test "$(field Package)" = gitreant +check "Version carries Debian revision 1" test "$(field Version)" = 0.3.0-1 +check "Architecture is the one asked for" test "$(field Architecture)" = amd64 +depends="$(field Depends)" +floor="$(readelf -V -W "$work/gitreant" | sh "$here/glibc-floor.sh")" +for dep in "libc6 (>= $floor)" libgcc-s1 libwayland-client0 git shortarrow-archive-keyring; do + check "Depends names $dep" grep -qF "$dep" <<<"$depends" +done +check "Recommends xdg-utils, which opens the browser" \ + grep -qF xdg-utils <<<"$(field Recommends)" +check "Installed-Size is set" test -n "$(field Installed-Size)" + +contents="$(dpkg-deb --contents "$deb")" +check "the executable is /usr/bin/gitreant with mode 0755, owned by root" \ + grep -qE '^-rwxr-xr-x root/root .* \./usr/bin/gitreant$' <<<"$contents" +for doc in copyright LICENSE-MIT LICENSE-APACHE; do + check "the package carries /usr/share/doc/gitreant/$doc" \ + grep -qE " \./usr/share/doc/gitreant/$doc\$" <<<"$contents" +done + +# Version needs as readelf -V prints them: gitreant 0.2.3's x86_64 build, +# which needs GLIBC_2.39 only weakly, and its aarch64 build, which needs it +# outright. +cat >"$work/x86_64.txt" <<'EOF' +Version needs section '.gnu.version_r' contains 4 entries: + 000000: Version: 1 File: libgcc_s.so.1 Cnt: 3 + 0x0040: Name: GCC_3.0 Flags: none Version: 4 + 0x0010: Version: 1 File: libm.so.6 Cnt: 1 + 0x0070: Name: GLIBC_2.29 Flags: none Version: 7 + 0x0020: Version: 1 File: libc.so.6 Cnt: 20 + 0x0080: Name: GLIBC_2.2.5 Flags: none Version: 3 + 0x0170: Name: GLIBC_2.30 Flags: WEAK Version: 14 + 0x0180: Name: GLIBC_2.32 Flags: none Version: 20 + 0x0190: Name: GLIBC_2.33 Flags: none Version: 13 + 0x01a0: Name: GLIBC_2.34 Flags: none Version: 2 + 0x01b0: Name: GLIBC_2.39 Flags: WEAK Version: 16 + 0x0030: Version: 1 File: ld-linux-x86-64.so.2 Cnt: 1 + 0x01c0: Name: GLIBC_2.3 Flags: none Version: 6 +EOF +cat >"$work/aarch64.txt" <<'EOF' +Version needs section '.gnu.version_r' contains 3 entries: + 0x0010: Name: GLIBC_2.29 Flags: none Version: 13 + 0x00a0: Name: GLIBC_2.34 Flags: none Version: 6 + 0x00b0: Name: GLIBC_2.17 Flags: none Version: 5 + 0x00c0: Name: GLIBC_2.39 Flags: none Version: 3 +EOF +check "the floor ignores a weakly needed glibc version" \ + test "$(sh "$here/glibc-floor.sh" <"$work/x86_64.txt")" = 2.34 +check "the floor is the highest glibc version needed outright" \ + test "$(sh "$here/glibc-floor.sh" <"$work/aarch64.txt")" = 2.39 +expect "no glibc version at all is refused" 1 \ + sh "$here/glibc-floor.sh" Date: Tue, 20 Oct 2026 12:50:16 +0900 Subject: [PATCH 2/2] feat(release): version a prerelease's package before its release An rc package with the release's version would keep apt from upgrading a machine that installed it; 0.3.0~rc.1 sorts before 0.3.0. --- .github/workflows/release.yml | 9 +++++---- docs/adr/0034-debian-packages.jp.md | 2 +- docs/adr/0034-debian-packages.md | 11 +++++++---- packaging/apt/deb-version.sh | 23 +++++++++++++++++++++++ packaging/apt/test-build-deb.sh | 12 ++++++++++++ 5 files changed, 48 insertions(+), 9 deletions(-) create mode 100644 packaging/apt/deb-version.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8341711..a4684f7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -172,9 +172,9 @@ jobs: deb: # The Debian packages (ADR 0034), built from the release executables and # installed with apt in Debian and Ubuntu containers before anything is - # published. The version is Cargo.toml's, which never carries a "-", so - # a prerelease tag rehearses this job too; the package registry indexes - # stable releases only. + # published. A prerelease tag rehearses this job too, with a package + # version that sorts before the release (0.3.0~rc.1); the package + # registry indexes stable releases only. name: Debian package (${{ matrix.arch }}) needs: build runs-on: ${{ matrix.os }} @@ -202,8 +202,9 @@ jobs: set -euo pipefail sudo apt-get update -qq && sudo apt-get install -y -qq apt-utils version="$(sed -n 's/^version = "\(.*\)"$/\1/p' Cargo.toml | head -n 1)" + deb_version="$(sh packaging/apt/deb-version.sh "$version" "$GITHUB_REF_NAME")" tar xzf artifacts/gitreant-${{ matrix.target }}.tar.gz -C artifacts - sh packaging/apt/build-deb.sh artifacts/gitreant "$version" ${{ matrix.arch }} deb + sh packaging/apt/build-deb.sh artifacts/gitreant "$deb_version" ${{ matrix.arch }} deb sh packaging/apt/install-test.sh deb "$version" ${{ matrix.arch }} - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: diff --git a/docs/adr/0034-debian-packages.jp.md b/docs/adr/0034-debian-packages.jp.md index d543f4a..247afc4 100644 --- a/docs/adr/0034-debian-packages.jp.md +++ b/docs/adr/0034-debian-packages.jp.md @@ -16,7 +16,7 @@ Linux のユーザーは gitreant をリリースの tar.gz か `cargo install` - `Depends`:実行ファイルが名指しするライブラリ(`libc6`、`libgcc-s1`、`libwayland-client0`)、`gitreant doctor` が必須とする `git`、リポジトリの `shortarrow-archive-keyring`。最後の 1 つは、gitreant が入っている間、ソースを検証する鍵を残しておくためのもの。 - `libc6` の下限は、実行ファイルが必須として要求する glibc のバージョンのうち最も高いもの。手で書かず、ビルド時に `readelf` で読み取る(`packaging/apt/glibc-floor.sh`)。 - `Recommends: xdg-utils`(ブラウザを開く)。`Suggests: gh, gnupg`(`gitreant doctor` が任意とするツール)。 - - `-` を含む版にはパッケージを作らない。Debian の順序では正式版より後ろに並ぶため。リリースのワークフローは版を `Cargo.toml` から取り、そこには `-` が付かない。そのため、プレリリースのタグでも `main` の版でパッケージをビルドしてテストする。 + - プレリリースのタグでは、接尾辞を `~` の後ろに付けた版でパッケージを作る(`v0.3.0-rc.1` なら `0.3.0~rc.1-1`。`packaging/apt/deb-version.sh` が決める)。`-` を使うと Debian の順序で正式版より後ろに並ぶ。`Cargo.toml` の版だけを使うと、rc と正式版が中身の違う同じ版になり、rc を入れたマシンを apt が正式版に上げない。`Cargo.toml` と違う版を名指すタグにはパッケージを作らない。 - **Linux 版のリリース用実行ファイルは `ubuntu-22.04` と `ubuntu-22.04-arm`(glibc 2.35)でビルドする。** これでどちらも bookworm より新しい glibc を要求しない。 - **リリースのワークフロー**は、リリース用の実行ファイルからパッケージをビルドする。それぞれを使い捨ての署名付きリポジトリから `debian:bookworm`、`ubuntu:22.04`、`ubuntu:24.04` に `apt` でインストールし、`gitreant --version` と `gitreant doctor` を実行する(`packaging/apt/install-test.sh`)。パッケージは、全アセットを載せた `SHA256SUMS` と一緒に GitHub Release に添付する。CI もプルリクエストごとに、同じイメージ上のデバッグビルドで同じことを行う。 diff --git a/docs/adr/0034-debian-packages.md b/docs/adr/0034-debian-packages.md index 85406b9..a1f3ace 100644 --- a/docs/adr/0034-debian-packages.md +++ b/docs/adr/0034-debian-packages.md @@ -40,10 +40,13 @@ Raspberry Pi OS bookworm included. (`packaging/apt/glibc-floor.sh`), not written by hand. - `Recommends: xdg-utils`, which opens the browser; `Suggests: gh, gnupg`, the tools `gitreant doctor` reports as optional. - - A version containing `-` gets no package: it would sort after the final - release in Debian's ordering. The release workflow takes the version - from `Cargo.toml`, which never has one, so a prerelease tag builds and - tests the package at the version `main` carries. + - A prerelease tag builds the package with its suffix after a `~` + (`v0.3.0-rc.1` gives `0.3.0~rc.1-1`, by `packaging/apt/deb-version.sh`). + A `-` would sort it after the release in Debian's ordering, and + `Cargo.toml`'s version alone would give an rc and its release the same + version with different contents, so apt would not upgrade a machine + that installed the rc. A tag that names a version other than + `Cargo.toml`'s gets no package. - **The Linux release executables are built on `ubuntu-22.04` and `ubuntu-22.04-arm`** (glibc 2.35), so neither needs a glibc newer than bookworm's. diff --git a/packaging/apt/deb-version.sh b/packaging/apt/deb-version.sh new file mode 100644 index 0000000..ad6bb60 --- /dev/null +++ b/packaging/apt/deb-version.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# Prints the upstream version of the Debian package a tag builds +# (ADR 0034): Cargo.toml's version for a release tag, and that version with +# the prerelease suffix after a "~" for a prerelease tag (v0.3.0-rc.1 gives +# 0.3.0~rc.1), which sorts before the release in Debian's ordering. +# +# Usage: deb-version.sh +# Exits 1 when the tag names a version other than Cargo.toml's. +set -eu + +cargo="${1:?usage: deb-version.sh }" +tag="${2:?tag}" +version="${tag#v}" +base="${version%%-*}" + +[ "$base" = "$cargo" ] || { + echo "deb-version: $tag does not name version $cargo" >&2 + exit 1 +} +case "$version" in + *-*) echo "${base}~${version#*-}" ;; + *) echo "$base" ;; +esac diff --git a/packaging/apt/test-build-deb.sh b/packaging/apt/test-build-deb.sh index 43e82e2..4e70390 100644 --- a/packaging/apt/test-build-deb.sh +++ b/packaging/apt/test-build-deb.sh @@ -105,6 +105,18 @@ expect "arm64 builds too" 0 \ check "the arm64 package says so" \ test "$(dpkg-deb --field "$work/out-arm64/gitreant_0.3.0-1_arm64.deb" Architecture)" = arm64 +expect "a pre-release spelled with ~ builds a package" 0 \ + bash "$here/build-deb.sh" "$work/gitreant" 0.3.0~rc.1 amd64 "$work/out-tilde" +check "the ~ pre-release sorts before the release" \ + dpkg --compare-versions "$(dpkg-deb --field "$work/out-tilde/gitreant_0.3.0~rc.1-1_amd64.deb" Version)" lt 0.3.0-1 + +check "a release tag gives Cargo.toml's version" \ + test "$(sh "$here/deb-version.sh" 0.3.0 v0.3.0)" = 0.3.0 +check "a prerelease tag gives a version that sorts before the release" \ + test "$(sh "$here/deb-version.sh" 0.3.0 v0.3.0-rc.1)" = '0.3.0~rc.1' +expect "a prerelease tag for another version is refused" 1 \ + sh "$here/deb-version.sh" 0.3.0 v0.4.0-rc.1 + expect "a pre-release version is refused" 1 \ bash "$here/build-deb.sh" "$work/gitreant" 0.3.0-rc.1 amd64 "$work/out-rc" check "a refused pre-release leaves no package" test ! -e "$work/out-rc"