diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..adc04de --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,61 @@ +# Builds and publishes a release when a version tag (v0.2.0, v1.0.0-rc.1) +# is pushed: +# * a Gitea release with the CHANGELOG notes, the linux tarballs, the CLI +# for Linux, macOS and Windows, the .debs and SHA256SUMS; +# * the .debs (amd64, arm64) in the owner's Debian package registry. +# +# Needs a repository secret RELEASE_TOKEN: an access token of a user who +# can write to the repository, with the write:repository and write:package +# scopes. docs/development.md ("Releasing") has the steps. +name: release + +on: + push: + tags: ["v*"] + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Check the tag against VERSION and CHANGELOG.md + run: | + version=$(cat VERSION) + if [ "$GITHUB_REF_NAME" != "v$version" ]; then + echo "::error::tag $GITHUB_REF_NAME does not match VERSION ($version)" + exit 1 + fi + if ! grep -q "^## \[$version\]" CHANGELOG.md; then + echo "::error::CHANGELOG.md has no '## [$version]' section" + exit 1 + fi + + - name: Tools + run: | + missing= + for t in jq dpkg-deb curl; do command -v $t >/dev/null || missing="$missing $t"; done + if [ -n "$missing" ]; then + sudo=; [ "$(id -u)" = 0 ] || sudo=sudo + $sudo apt-get update -qq && $sudo apt-get install -y -qq jq dpkg curl + fi + + - name: Test + run: make test + + - name: Build + run: | + export DEB_MAINTAINER="$(git log -1 --format='%an <%ae>')" + make dist + make cli-dist + make deb DEB_ARCH=amd64 + make deb DEB_ARCH=arm64 + + - name: Publish + env: + GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }} + run: scripts/gitea-release.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 458e85b..65a7638 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,9 @@ All notable changes to Jarl are documented here. The format follows ## [Unreleased] -The first version, to be released as 0.1.0. +## [0.1.0] - 2026-10-09 + +The first version. ### Added @@ -68,12 +70,23 @@ The first version, to be released as 0.1.0. `socket`. The systemd unit creates `/run/jarl` (0750). - Jarl is released under the MIT License (`LICENSE`), which is installed with the docs and is the Debian package's `copyright` file. -- A Debian package: `make deb` builds `dist/jarl__.deb` with - only `dpkg-deb` (no root), for the host or any other architecture - (`DEB_ARCH=arm64`). Installing it creates the `jarl` user and directories - and an example `/etc/jarl/jarl.yaml`; upgrades restart a running jarld and - never touch `/etc/jarl/jarl.yaml`; removing it stops jarld, and even a - purge keeps Jarl's database and the files named uses. +- Debian packages: `make deb` builds, with only `dpkg-deb` (no root) and + for the host or any other architecture (`DEB_ARCH=arm64`), `jarl` (the + server: jarld, its units and docs) and `jarl-cli` (the `jarl` CLI, its + completion and the ACME hooks), which `jarl` depends on. `jarl-cli` has no + scripts, so it can go on any machine you manage Jarl from. Installing + `jarl` creates the `jarl` user and directories and an example + `/etc/jarl/jarl.yaml`; upgrades restart a running jarld and never touch + `/etc/jarl/jarl.yaml`; removing it stops jarld, and even a purge keeps + Jarl's database and the files named uses. +- Releases from Gitea Actions: pushing a `v` tag runs + `.gitea/workflows/release.yml`, which tests, builds and publishes a Gitea + release (the CHANGELOG section as notes; the tarballs, the CLI for Linux, + macOS and Windows, the `.deb`s and `SHA256SUMS`), and uploads the `.deb`s + for amd64 and arm64 to the Gitea Debian registry, so servers install and + upgrade Jarl with apt. `make cli-dist` builds the standalone CLI files; + `scripts/gitea-release.sh` publishes. docs/development.md has the setup + (a runner and a `RELEASE_TOKEN` secret), docs/install.md the apt source. - Bash completion for `jarl` and `jarld`, installed by `make install` and the Debian package. Besides commands and flags, it completes server and group names from the CLI configuration, zone, view, ACL, TSIG key and diff --git a/Makefile b/Makefile index 99f904e..c2a5d25 100644 --- a/Makefile +++ b/Makefile @@ -6,9 +6,12 @@ # make e2e end-to-end test against a real named (scripts/e2e.sh) # sudo make install install binaries, bash completion, systemd units, # sysusers/tmpfiles files and an example /etc/jarl/jarl.yaml -# make deb a Debian package in dist/ (DEB_ARCH=arm64 for another -# architecture); install it with 'sudo apt install ./dist/jarl_*.deb' +# make deb Debian packages in dist/ (DEB_ARCH=arm64 for another +# architecture): jarl (the server) and jarl-cli (the CLI, +# which jarl depends on); 'sudo apt install ./dist/jarl*_amd64.deb' # make dist static binaries for linux/amd64 and linux/arm64 in dist/ +# make cli-dist the CLI alone for Linux, macOS and Windows in dist/ +# (jarl-cli---[.exe]) VERSION ?= $(shell cat VERSION 2>/dev/null || echo 0.0.0-dev) LDFLAGS := -s -w -X jarl/internal/version.Version=$(VERSION) @@ -34,7 +37,7 @@ JARL_BIN ?= bin/jarl DEB_ARCH ?= $(shell dpkg --print-architecture 2>/dev/null || go env GOARCH) DEB_VERSION ?= $(subst -,~,$(VERSION)) -.PHONY: all build completion test vet e2e install install-files uninstall deb dist clean +.PHONY: all build completion test vet e2e install install-files uninstall deb dist cli-dist clean all: build @@ -114,5 +117,15 @@ dist: build tar -C dist -czf dist/jarl-$(VERSION)-linux-$$arch.tar.gz jarl-$(VERSION)-linux-$$arch || exit 1; \ done +# The platforms cli-dist builds the CLI for. +CLI_PLATFORMS ?= linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64 windows/arm64 + +cli-dist: + @mkdir -p dist + for p in $(CLI_PLATFORMS); do \ + os=$${p%/*} arch=$${p#*/} ext=; [ $$os = windows ] && ext=.exe; \ + GOOS=$$os GOARCH=$$arch go build $(GOFLAGS) -ldflags '$(LDFLAGS)' -o dist/jarl-cli-$(VERSION)-$$os-$$arch$$ext ./cmd/jarl || exit 1; \ + done + clean: rm -rf bin dist diff --git a/README.md b/README.md index 3f1056a..e7081fe 100644 --- a/README.md +++ b/README.md @@ -67,8 +67,9 @@ error: www.adbraun.lol has a CNAME, so it can't have any other records (found A) fallback. * One static Go binary for the daemon (web UI embedded, SQLite built in) and one for the CLI, run by a sandboxed systemd service as an unprivileged - user. `make deb` builds a Debian package, and both come with bash - completion. + user. Releases come as Debian packages in an apt repository (server and + CLI separately) and as standalone CLI downloads for Linux, macOS and + Windows; both binaries have bash completion. ## Quick start @@ -76,7 +77,8 @@ On a Debian/Ubuntu BIND 9.20 server ([install.md](docs/install.md) has the details): ```sh -make deb && sudo apt install ./dist/jarl_*.deb # or: make && sudo make install, see install.md +sudo apt install jarl # from the Gitea package registry (install.md shows adding it) +# or build it: make deb && sudo apt install ./dist/jarl*_amd64.deb, or make && sudo make install sudo -u jarl jarld init # database + first administrator sudo -u jarl jarld import # read the current named.conf and zones sudo jarld adopt # switch named over (keeps a backup, rolls back on failure) diff --git a/deploy/debian/control-jarl-cli.in b/deploy/debian/control-jarl-cli.in new file mode 100644 index 0000000..c530e92 --- /dev/null +++ b/deploy/debian/control-jarl-cli.in @@ -0,0 +1,18 @@ +Package: jarl-cli +Version: @VERSION@ +Architecture: @ARCH@ +Maintainer: @MAINTAINER@ +Installed-Size: @SIZE@ +Replaces: jarl +Section: net +Priority: optional +Description: command line client for Jarl, the BIND 9 manager + jarl is the command line client for Jarl servers (jarld): zones, + records, views, ACLs, TSIG keys and options, changesets, users and + tokens, and query statistics, on one server or a group of them. + It talks to jarld's REST API with an API token, or, on the server + itself, to jarld's local socket. + . + Install this package alone to manage Jarl servers from another machine; + the jarl package (the server) depends on it. It also has the ACME DNS-01 + hooks for acme.sh and lego in /usr/share/doc/jarl-cli/contrib. diff --git a/deploy/debian/control.in b/deploy/debian/control-jarl.in similarity index 60% rename from deploy/debian/control.in rename to deploy/debian/control-jarl.in index d570945..cc74d60 100644 --- a/deploy/debian/control.in +++ b/deploy/debian/control-jarl.in @@ -3,7 +3,7 @@ Version: @VERSION@ Architecture: @ARCH@ Maintainer: @MAINTAINER@ Installed-Size: @SIZE@ -Depends: systemd +Depends: systemd, jarl-cli (= @VERSION@) Recommends: bind9 (>= 1:9.20) Section: net Priority: optional @@ -14,7 +14,8 @@ Description: web UI, REST API and CLI for managing a BIND 9 server loaded into named as one changeset that can be reverted. Users and API tokens have fine-grained permissions, and every change is audited. . - The package installs jarld (the daemon, as the systemd service jarl) and - jarl (the CLI), with bash completion for both. jarld isn't started on a - first install: see /usr/share/doc/jarl/docs/install.md to create the - database and adopt the BIND configuration. + This package is the server: jarld (the daemon, as the systemd service + jarl) with its daily backup timer. The jarl CLI comes from jarl-cli, + which can also be installed on its own on other machines. jarld isn't + started on a first install: see /usr/share/doc/jarl/docs/install.md to + create the database and adopt the BIND configuration. diff --git a/docs/acme.md b/docs/acme.md index 9e36d8a..f0cd936 100644 --- a/docs/acme.md +++ b/docs/acme.md @@ -68,7 +68,7 @@ configuration, so `certbot renew` works unattended. Install the DNS API hook that comes with Jarl: ```sh -cp contrib/acme.sh/dns_jarl.sh ~/.acme.sh/dnsapi/ # or from /usr/local/share/doc/jarl/contrib/ +cp contrib/acme.sh/dns_jarl.sh ~/.acme.sh/dnsapi/ # or from /usr/share/doc/jarl-cli/contrib/ (jarl-cli package) export JARL_SERVER=https://ns1.adbraun.lol:8053 export JARL_TOKEN=$(cat /etc/jarl/certbot.token) acme.sh --issue --dns dns_jarl -d adbraun.lol -d '*.adbraun.lol' @@ -86,7 +86,7 @@ Jarl ships one, `contrib/lego/jarl-lego.sh`, which calls the `jarl` CLI (so jarl must be installed on the same host): ```sh -sudo install -m 0755 contrib/lego/jarl-lego.sh /usr/local/bin/jarl-lego # or from /usr/local/share/doc/jarl/contrib/ +sudo install -m 0755 contrib/lego/jarl-lego.sh /usr/local/bin/jarl-lego # or from /usr/share/doc/jarl-cli/contrib/ (jarl-cli package) export EXEC_PATH=/usr/local/bin/jarl-lego export JARL_SERVER=https://ns1.adbraun.lol:8053 export JARL_TOKEN=@/etc/jarl/certbot.token diff --git a/docs/cli.md b/docs/cli.md index 860a3d7..1860b20 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -25,6 +25,10 @@ Besides commands and flags, Tab completes: ## Setup +On the DNS server the CLI comes with Jarl. To use it from another machine, +install just the `jarl-cli` package there, or copy the static `jarl` binary +([install.md](install.md#just-the-cli-on-another-machine)). + ```sh jarl config set server https://ns1.adbraun.lol:8053 jarl config set token # paste the token at the prompt diff --git a/docs/development.md b/docs/development.md index 33835b4..3fca648 100644 --- a/docs/development.md +++ b/docs/development.md @@ -49,7 +49,7 @@ make # bin/jarld, bin/jarl (CGO_ENABLED=0, version from ./VERSION), # and bin/completion/{jarl,jarld} (bash completion) make test # go vet, gofmt check, go test ./... make e2e # scripts/e2e.sh -make deb # dist/jarl__.deb (DEB_ARCH=arm64 etc. cross-builds) +make deb # dist/jarl__.deb (server) and jarl-cli__.deb (DEB_ARCH=arm64 etc. cross-builds) ``` ### The Debian package @@ -70,7 +70,7 @@ To try the package without installing it on your machine: ```sh docker run --rm -it -v "$PWD/dist:/pkg:ro" debian:trixie \ - sh -c 'apt-get update && apt-get install -y /pkg/jarl_*_amd64.deb && bash' + sh -c 'apt-get update && apt-get install -y /pkg/jarl_*_amd64.deb /pkg/jarl-cli_*_amd64.deb && bash' ``` ### Shell completion @@ -154,7 +154,40 @@ files are embedded at build time, so rebuild jarld after changes. ## Releasing -1. Move the "Unreleased" section of `CHANGELOG.md` under the new version - and date. +1. Move the "Unreleased" section of `CHANGELOG.md` under + `## [] - `. 2. Put the version in `VERSION`. -3. `make test && JARL_BIND_ROOT=... make e2e && make dist`. +3. `make test && JARL_BIND_ROOT=... make e2e` (CI runs only `make test`; + it has no BIND). +4. Commit, tag and push: `git tag v && git push origin main v`. + +The tag starts `.gitea/workflows/release.yml`, which checks that the tag +matches `VERSION` and that `CHANGELOG.md` has the section, runs `make +test`, builds everything (`make dist`, `make cli-dist`, `make deb` for +amd64 and arm64) and runs `scripts/gitea-release.sh`. That creates the +Gitea release (the CHANGELOG section as notes; the tarballs, the CLI for +Linux, macOS and Windows, the `.deb`s and `SHA256SUMS` attached) and +uploads the `.deb`s to the owner's Debian registry, distribution `stable`, +component `main` ([install.md](install.md#from-the-package-registry)). A +version with a hyphen (`1.0.0-rc.1`) becomes a pre-release; its packages +get version `1.0.0~rc.1`, which apt sorts before `1.0.0`. + +Pushing the same tag again (`git tag -f`, `git push -f`) re-runs it, and +the script replaces the attachments and packages, so a failed release can +simply be retried. + +Once, on Gitea: + +* Actions on for the repository (Settings → Repository → Actions), and a + runner with the `ubuntu-latest` label: act_runner's default, which runs + jobs in `docker.gitea.com/runner-images:ubuntu-latest`. The job + downloads Go itself (`actions/setup-go`, from `go.mod`), so the runner + needs internet access, or a mirror of the actions and Go. +* A repository secret `RELEASE_TOKEN` (Settings → Actions → Secrets): an + access token (User settings → Applications) with the `write:repository` + and `write:package` scopes, from a user who can write to the repository. + The job's automatic token can't be relied on to publish packages. + +`scripts/gitea-release.sh` also runs by hand after the same builds, with +`GITEA_URL`, `GITEA_REPO` (`owner/name`) and `GITEA_TOKEN` set; it needs +`curl`, `jq` and `dpkg-deb`. diff --git a/docs/install.md b/docs/install.md index f47d923..1881caf 100644 --- a/docs/install.md +++ b/docs/install.md @@ -12,6 +12,12 @@ to load Jarl's configuration the old one is put back automatically. ## 1. Build +Each release is already built: the Debian packages are in the Gitea +package registry ([below](#from-the-package-registry)), and the release +page (https://gitea.adbraun.lol/adam/jarl/releases) has the packages, the +linux tarballs, and the CLI for Linux, macOS and Windows. Build it yourself +for an unreleased version. + Jarl is two static binaries with no runtime dependencies (SQLite is compiled in). Building needs Go 1.27 or newer, on any machine: @@ -30,23 +36,62 @@ package (below) and copy that. There are two ways: a Debian package, which is easiest to upgrade and remove, or `make install`. -### With the Debian package +### With the Debian packages + +There are two packages: + +| Package | Holds | Install it | +|---------|-------|-----------| +| `jarl` | the server: `jarld`, its systemd units, the docs | on the DNS server | +| `jarl-cli` | the `jarl` CLI, its bash completion, the ACME hooks | wherever you manage Jarl from; `jarl` depends on it | + +#### From the package registry + +Every release is published to Gitea's Debian registry (amd64 and arm64). +Add it to apt once, and apt then finds `jarl-cli` by itself and +`apt upgrade` brings new releases: ```sh -make deb # dist/jarl_0.1.0_amd64.deb -make deb DEB_ARCH=arm64 # for another architecture (also armhf, i386, ...) -sudo apt install ./dist/jarl_0.1.0_amd64.deb +sudo install -d /etc/apt/keyrings +sudo curl -fsSL https://gitea.adbraun.lol/api/packages/adam/debian/repository.key -o /etc/apt/keyrings/jarl.asc +echo "deb [signed-by=/etc/apt/keyrings/jarl.asc] https://gitea.adbraun.lol/api/packages/adam/debian stable main" \ + | sudo tee /etc/apt/sources.list.d/jarl.list +sudo apt update +sudo apt install jarl ``` +The packages are as visible as the Gitea account that owns them, not the +repository: with a public account, apt needs no login even if the +repository is private. For a private or limited account, give apt a token +with the `read:package` scope in `/etc/apt/auth.conf.d/jarl.conf` (mode +0600): + +``` +machine gitea.adbraun.lol/api/packages/adam/debian +login adam +password +``` + +#### From files + +Download the two `.deb` files from the release page, or build them: + +```sh +make deb # dist/jarl_0.1.0_amd64.deb and dist/jarl-cli_0.1.0_amd64.deb +make deb DEB_ARCH=arm64 # for another architecture (also armhf, i386, ...) +sudo apt install ./dist/jarl_0.1.0_amd64.deb ./dist/jarl-cli_0.1.0_amd64.deb +``` + +Give apt both files: `jarl` needs `jarl-cli` of the same version. `make deb` needs only `dpkg-deb` (no root, no debhelper) and cross-builds for any architecture Go supports. apt also installs `bind9` (recommended) -if it's missing. Installing the package: +if it's missing. Installing the server package: -* puts the binaries in `/usr/bin`, bash completion in +* puts `jarld` in `/usr/bin`, its bash completion in `/usr/share/bash-completion/completions`, the systemd units in `/usr/lib/systemd/system` (`jarl.service`, and `jarl-backup.service` with its daily `jarl-backup.timer`), the sysusers and tmpfiles files - in `/usr/lib`, and the docs and contrib files in `/usr/share/doc/jarl`; + in `/usr/lib`, and the docs in `/usr/share/doc/jarl`; * creates the `jarl` user (in group `bind`) and Jarl's directories, and writes `/etc/jarl/jarl.yaml` from the example on the first install. No file in `/etc` belongs to the package, so your configuration is never @@ -58,17 +103,44 @@ if it's missing. Installing the package: * doesn't start jarld: continue with [3. Configure](#3-configure), then `systemctl enable --now jarl` in step 8. -**Upgrading**: build and install the newer package the same way. A running -jarld is restarted, and upgrades its database as it starts. +`jarl-cli` installs `/usr/bin/jarl`, its completion, and +`/usr/share/doc/jarl-cli` (the CLI, ACME and permissions docs, and the +acme.sh and lego hooks in `contrib/`). + +**Upgrading**: `sudo apt update && sudo apt upgrade` with the registry, or +install the newer files the same way. A running +jarld is restarted, and upgrades its database as it starts. Up to 0.1.0 +everything was one `jarl` package; installing the two new ones over it +moves `/usr/bin/jarl` to `jarl-cli` by itself. **Removing** (`sudo apt remove jarl`) stops jarld and the backup timer and -removes the program, but named keeps running with the configuration Jarl +removes the server (the CLI stays until you remove `jarl-cli` too), but named keeps running with the configuration Jarl last wrote. Even `apt purge jarl` keeps `/etc/jarl`, `/var/lib/jarl` (the database), `/var/backups/jarl` and the files in `/etc/bind/jarl`, `/var/lib/bind/jarl` and `/var/cache/bind/jarl`, since named.conf may still include them. Remove them by hand once named no longer uses them. +### Just the CLI, on another machine + +To manage Jarl servers from your laptop or another server, install only +`jarl-cli`. It needs nothing else and changes nothing on the machine (no +users, services or directories): + +```sh +sudo apt install jarl-cli # with the registry above; or: sudo apt install ./jarl-cli_0.1.0_amd64.deb +jarl config set server https://ns1.adbraun.lol:8053 +jarl config set token +``` + +Elsewhere, download the CLI from the release page: +`jarl-cli---` for Linux, macOS (`darwin`) and Windows +(`.exe`), with their checksums in `SHA256SUMS`. It is one static file; put +it in your PATH as `jarl` (on macOS, `xattr -d com.apple.quarantine jarl` +lets an unsigned download run). `make cli-dist` builds the same files, and +`make` builds it for this machine in `bin/`. [cli.md](cli.md) +explains the setup, and several servers in one configuration. + ### With make install On the DNS server, as root: @@ -297,8 +369,8 @@ without dropping connections. More in [acme.md](acme.md). ## Upgrading ```sh -make && sudo make install -sudo systemctl restart jarl +sudo apt update && sudo apt upgrade # Debian packages from the registry +make && sudo make install && sudo systemctl restart jarl # make install ``` Database migrations run automatically at startup. Before an upgrade, take diff --git a/docs/operations.md b/docs/operations.md index 37bf484..4ff0785 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -14,7 +14,7 @@ | Restart / reload TLS | `systemctl restart jarl` / `systemctl reload jarl` | | Work on the server itself | `sudo jarl ...`: root uses jarld's local socket and needs no token ([cli.md](cli.md#on-the-server-itself)) | | Renew or replace a token | `jarl token rotate NAME --grace 1h` (same grants, new secret; [permissions.md](permissions.md#rotating-a-token)). `jarl token list --all --expiring` shows those that expire within 14 days | -| Upgrade Jarl | Debian package: build the new one and `sudo apt install ./dist/jarl_*.deb`; otherwise `make && sudo make install && sudo systemctl restart jarl`. jarld upgrades its database when it starts | +| Upgrade Jarl | Debian packages: build the new ones and `sudo apt install ./dist/jarl*_amd64.deb` (server and CLI); otherwise `make && sudo make install && sudo systemctl restart jarl`. jarld upgrades its database when it starts | Restarting jarld doesn't affect DNS: named keeps serving the files it loaded. jarld is only needed to make changes. diff --git a/internal/stats/inode_other.go b/internal/stats/inode_other.go new file mode 100644 index 0000000..1dd87ee --- /dev/null +++ b/internal/stats/inode_other.go @@ -0,0 +1,9 @@ +//go:build !unix + +package stats + +import "io/fs" + +// inode is 0 where there are no inodes (the CLI uses this package on +// Windows for its types; only jarld tails logs). +func inode(fs.FileInfo) uint64 { return 0 } diff --git a/internal/stats/inode_unix.go b/internal/stats/inode_unix.go new file mode 100644 index 0000000..ab5d6f1 --- /dev/null +++ b/internal/stats/inode_unix.go @@ -0,0 +1,15 @@ +//go:build unix + +package stats + +import ( + "io/fs" + "syscall" +) + +func inode(fi fs.FileInfo) uint64 { + if st, ok := fi.Sys().(*syscall.Stat_t); ok { + return st.Ino + } + return 0 +} diff --git a/internal/stats/tail.go b/internal/stats/tail.go index 48d512e..5519646 100644 --- a/internal/stats/tail.go +++ b/internal/stats/tail.go @@ -6,7 +6,6 @@ import ( "io" "io/fs" "os" - "syscall" ) // Tailer follows a log file that named rotates (renaming it to FILE.0, @@ -30,13 +29,6 @@ type Position struct { Offset int64 `json:"offset"` } -func inode(fi fs.FileInfo) uint64 { - if st, ok := fi.Sys().(*syscall.Stat_t); ok { - return st.Ino - } - return 0 -} - // Position returns the current position (of complete lines). func (t *Tailer) Position() Position { return Position{Inode: t.ino, Offset: t.offset - int64(len(t.partial))} diff --git a/scripts/gitea-release.sh b/scripts/gitea-release.sh new file mode 100755 index 0000000..54fd97e --- /dev/null +++ b/scripts/gitea-release.sh @@ -0,0 +1,103 @@ +#!/bin/sh +# Publishes a built release (make dist, make cli-dist, make deb for each +# architecture) to Gitea: +# +# * a release for tag v with the CHANGELOG section for VERSION as +# its notes, and every file in dist/ (tarballs, CLI binaries, .debs) +# plus a SHA256SUMS of them attached; +# * the .debs in the owner's Debian package registry, so servers can +# 'apt install jarl' and get updates with apt (docs/install.md). +# +# Run by .gitea/workflows/release.yml when a v* tag is pushed. Running it +# again for the same version replaces the attachments and packages. +# +# Variables: +# GITEA_URL e.g. https://gitea.example.com (in Actions: +# GITHUB_SERVER_URL) +# GITEA_REPO owner/name (in Actions: GITHUB_REPOSITORY) +# GITEA_TOKEN a token with write:repository and write:package +# VERSION default: the VERSION file +# DEB_DISTRIBUTION the apt distribution (default: stable) +# DEB_COMPONENT the apt component (default: main) +# +# Needs curl and jq. +set -eu + +: "${GITEA_URL:=${GITHUB_SERVER_URL:-}}" "${GITEA_REPO:=${GITHUB_REPOSITORY:-}}" +: "${GITEA_URL:?set GITEA_URL}" "${GITEA_REPO:?set GITEA_REPO}" "${GITEA_TOKEN:?set GITEA_TOKEN}" +VERSION=${VERSION:-$(cat VERSION)} +DEB_DISTRIBUTION=${DEB_DISTRIBUTION:-stable} +DEB_COMPONENT=${DEB_COMPONENT:-main} +GITEA_URL=${GITEA_URL%/} +owner=${GITEA_REPO%%/*} +tag=v$VERSION +api=$GITEA_URL/api/v1/repos/$GITEA_REPO + +# curl with the token; fails on HTTP errors except the codes in $ok. +call() { + ok=$1 + shift + out=$(mktemp) + code=$(curl -sS -o "$out" -w '%{http_code}' -H "Authorization: token $GITEA_TOKEN" "$@") + case " 200 201 204 $ok " in + *" $code "*) cat "$out"; rm -f "$out"; return 0 ;; + esac + echo "gitea-release: HTTP $code from $*" >&2 + cat "$out" >&2 + echo >&2 + rm -f "$out" + return 1 +} + +files=$(find dist -maxdepth 1 -type f ! -name SHA256SUMS | sort) +[ -n "$files" ] || { echo "gitea-release: nothing in dist/" >&2; exit 1; } +for f in $files; do + case $f in *"$VERSION"*) ;; *) echo "gitea-release: $f is not version $VERSION" >&2; exit 1 ;; esac +done +# shellcheck disable=SC2046 # file names have no spaces +(cd dist && sha256sum $(printf '%s\n' "$files" | sed 's|^dist/||')) > dist/SHA256SUMS +files="$files dist/SHA256SUMS" + +# The release notes: the CHANGELOG section for this version. +notes=$(awk -v v="$VERSION" ' + /^## \[/ { if (on) exit; on = index($0, "## [" v "]") == 1; next } + on' CHANGELOG.md) +[ -n "$notes" ] || notes="See CHANGELOG.md." +prerelease=false +case $VERSION in *-*) prerelease=true ;; esac + +# The release (created, or found when publishing again). +release=$(call 404 "$api/releases/tags/$tag") +id=$(printf '%s\n' "$release" | jq -r '.id // empty') +if [ -z "$id" ]; then + body=$(jq -n --arg tag "$tag" --arg notes "$notes" --argjson pre "$prerelease" \ + '{tag_name: $tag, name: ("Jarl " + $tag), body: $notes, prerelease: $pre}') + release=$(call "" -X POST -H 'Content-Type: application/json' -d "$body" "$api/releases") + id=$(printf '%s\n' "$release" | jq -r .id) + echo "gitea-release: created release $tag" +fi +for f in $files; do + name=${f#dist/} + old=$(printf '%s\n' "$release" | jq -r --arg n "$name" '.assets[]? | select(.name == $n) | .id') + [ -z "$old" ] || call "" -X DELETE "$api/releases/$id/assets/$old" >/dev/null + call "" -X POST -F "attachment=@$f" "$api/releases/$id/assets?name=$name" >/dev/null + echo "gitea-release: attached $name" +done + +# The Debian packages. Gitea refuses a package version it already has +# (409), so publishing again deletes it first. +pool=$GITEA_URL/api/packages/$owner/debian/pool/$DEB_DISTRIBUTION/$DEB_COMPONENT +for f in dist/*.deb; do + [ -e "$f" ] || continue + # shellcheck disable=SC2016,SC2046 # dpkg-deb's fields; split into "$@" + set -- $(dpkg-deb --showformat '${Package} ${Version} ${Architecture}' --show "$f") + if ! call 409 --upload-file "$f" "$pool/upload" >/dev/null; then + echo "gitea-release: upload of $f failed" >&2 + exit 1 + fi + if [ "$code" = 409 ]; then + call "" -X DELETE "$pool/$1/$2/$3" >/dev/null + call "" --upload-file "$f" "$pool/upload" >/dev/null + fi + echo "gitea-release: uploaded $1 $2 $3 to $DEB_DISTRIBUTION/$DEB_COMPONENT" +done diff --git a/scripts/mkdeb.sh b/scripts/mkdeb.sh index a4e108b..fe266cf 100755 --- a/scripts/mkdeb.sh +++ b/scripts/mkdeb.sh @@ -1,19 +1,29 @@ #!/bin/sh -# Builds a Debian package of Jarl: dist/jarl__.deb. +# Builds Jarl's two Debian packages: +# +# dist/jarl-cli__.deb the CLI, to install anywhere: +# /usr/bin/jarl, its bash completion, and /usr/share/doc/jarl-cli/ +# (README, CHANGELOG, the CLI, ACME and permissions docs, contrib/ with +# the acme.sh and lego hooks). No scripts: installing it changes nothing +# else on the machine. +# dist/jarl__.deb the server, which depends on +# jarl-cli of the same version: +# /usr/bin/jarld and its bash completion +# /usr/lib/systemd/system/{jarl.service,jarl-backup.service,jarl-backup.timer} +# /usr/lib/sysusers.d/jarl.conf, /usr/lib/tmpfiles.d/jarl.conf +# /usr/share/doc/jarl/ (README, CHANGELOG, docs) +# Its scripts (deploy/debian) create the jarl user and directories and +# /etc/jarl/jarl.yaml on the first install, turn on the daily backup +# timer, restart a running jarld on upgrades, and stop both on removal. +# No file in /etc belongs to the package, so an upgrade never asks +# about configuration files. +# +# Until 0.1.0 one package, jarl, held both; jarl-cli takes /usr/bin/jarl +# over from it (Replaces: jarl). +# # Run through 'make deb' (which sets the variables below); needs only # dpkg-deb, no root and no debhelper. # -# The package installs, with Debian's paths: -# /usr/bin/jarld, /usr/bin/jarl -# /usr/share/bash-completion/completions/{jarl,jarld} -# /usr/lib/systemd/system/{jarl.service,jarl-backup.service,jarl-backup.timer} -# /usr/lib/sysusers.d/jarl.conf, /usr/lib/tmpfiles.d/jarl.conf -# /usr/share/doc/jarl/ (README, CHANGELOG, docs, contrib) -# and its scripts (deploy/debian) create the jarl user and directories and -# /etc/jarl/jarl.yaml on the first install, turn on the daily backup timer, -# restart a running jarld on upgrades, and stop both on removal. No file in /etc belongs to the package, -# so an upgrade never asks about configuration files. -# # Variables: DEB_ARCH (Debian architecture, e.g. amd64, arm64, armhf), # DEB_VERSION, VERSION, LDFLAGS, GOFLAGS, and DEB_MAINTAINER (default: your # git name and email). @@ -41,26 +51,42 @@ if [ -z "${DEB_MAINTAINER:-}" ]; then DEB_MAINTAINER="${name:-Jarl packager} <${email:-root@localhost}>" fi -work=dist/deb/jarl_${DEB_VERSION}_${DEB_ARCH} -root=$work/root -out=dist/jarl_${DEB_VERSION}_${DEB_ARCH}.deb +work=dist/deb/${DEB_VERSION}_${DEB_ARCH} +stage=$work/stage rm -rf "$work" -mkdir -p "$root/DEBIAN" +mkdir -p "$stage" # Binaries for the package's architecture (bin/ holds the host's, which # also generated the completion scripts). echo "mkdeb: building for linux/$goarch" # go reads GOFLAGS (-trimpath) from the environment. export GOFLAGS -GOOS=linux GOARCH=$goarch GOARM=$goarm CGO_ENABLED=0 go build -ldflags "$LDFLAGS" -o "$work/jarld" ./cmd/jarld -GOOS=linux GOARCH=$goarch GOARM=$goarm CGO_ENABLED=0 go build -ldflags "$LDFLAGS" -o "$work/jarl" ./cmd/jarl +GOOS=linux GOARCH=$goarch GOARM=$goarm CGO_ENABLED=0 go build -ldflags "$LDFLAGS" -o "$work/bin/jarld" ./cmd/jarld +GOOS=linux GOARCH=$goarch GOARM=$goarm CGO_ENABLED=0 go build -ldflags "$LDFLAGS" -o "$work/bin/jarl" ./cmd/jarl -make --no-print-directory install-files DESTDIR="$root" PREFIX=/usr \ - UNITDIR=/usr/lib/systemd/system JARLD_BIN="$work/jarld" JARL_BIN="$work/jarl" >/dev/null +# Everything 'make install' would install, then split in two. +make --no-print-directory install-files DESTDIR="$stage" PREFIX=/usr \ + UNITDIR=/usr/lib/systemd/system JARLD_BIN="$work/bin/jarld" JARL_BIN="$work/bin/jarl" >/dev/null + +cli=$work/jarl-cli +srv=$work/jarl +mkdir -p "$cli/DEBIAN" "$cli/usr/bin" "$cli/usr/share/bash-completion/completions" "$cli/usr/share/doc/jarl-cli/docs" +mv "$stage/usr/bin/jarl" "$cli/usr/bin/" +mv "$stage/usr/share/bash-completion/completions/jarl" "$cli/usr/share/bash-completion/completions/" +mv "$stage/usr/share/doc/jarl/contrib" "$cli/usr/share/doc/jarl-cli/" +cp "$stage/usr/share/doc/jarl/README.md" "$stage/usr/share/doc/jarl/CHANGELOG.md" "$cli/usr/share/doc/jarl-cli/" +for d in cli acme permissions; do + cp "$stage/usr/share/doc/jarl/docs/$d.md" "$cli/usr/share/doc/jarl-cli/docs/" +done +mv "$stage" "$srv" +mkdir -p "$srv/DEBIAN" +for f in postinst prerm postrm; do + install -m 0755 "deploy/debian/$f" "$srv/DEBIAN/$f" +done # Debian's machine-readable copyright file, from LICENSE. indent() { sed -e 's/^$/./' -e 's/^/ /' "$1"; } -{ +copyright() { echo "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/" echo "Upstream-Name: jarl" echo @@ -68,22 +94,26 @@ indent() { sed -e 's/^$/./' -e 's/^/ /' "$1"; } echo "Copyright: $(sed -n 's/^Copyright (c) //p' LICENSE)" echo "License: Expat" indent LICENSE -} > "$root/usr/share/doc/jarl/copyright" +} -for f in postinst prerm postrm; do - install -m 0755 "deploy/debian/$f" "$root/DEBIAN/$f" -done -# Directories 0755 and no group/other write, whatever the build's umask. -find "$root" -type d -exec chmod 0755 {} + -chmod -R go-w "$root" -size=$(du -sk --exclude=DEBIAN "$root" | cut -f1) -sed -e "s/@VERSION@/$DEB_VERSION/" -e "s/@ARCH@/$DEB_ARCH/" -e "s/@SIZE@/$size/" \ - -e "s|@MAINTAINER@|$DEB_MAINTAINER|" deploy/debian/control.in > "$root/DEBIAN/control" -(cd "$root" && find . -path ./DEBIAN -prune -o -type f -print | sed 's|^\./||' | sort | xargs md5sum) > "$root/DEBIAN/md5sums" -chmod 0644 "$root/DEBIAN/control" "$root/DEBIAN/md5sums" - -# Files belong to root in the package, whoever builds it. -dpkg-deb --root-owner-group -Zxz --build "$root" "$out" >/dev/null +# build PACKAGE ROOT: writes the control file and md5sums and builds the .deb. +build() { + pkg=$1 root=$2 out=dist/${1}_${DEB_VERSION}_${DEB_ARCH}.deb + copyright > "$root/usr/share/doc/$pkg/copyright" + rm -f "$root/usr/share/doc/$pkg/LICENSE" + # Directories 0755 and no group/other write, whatever the build's umask. + find "$root" -type d -exec chmod 0755 {} + + chmod -R go-w "$root" + size=$(du -sk --exclude=DEBIAN "$root" | cut -f1) + sed -e "s/@VERSION@/$DEB_VERSION/g" -e "s/@ARCH@/$DEB_ARCH/" -e "s/@SIZE@/$size/" \ + -e "s|@MAINTAINER@|$DEB_MAINTAINER|" "deploy/debian/control-$pkg.in" > "$root/DEBIAN/control" + (cd "$root" && find . -path ./DEBIAN -prune -o -type f -print | sed 's|^\./||' | sort | xargs md5sum) > "$root/DEBIAN/md5sums" + chmod 0644 "$root/DEBIAN/control" "$root/DEBIAN/md5sums" + # Files belong to root in the package, whoever builds it. + dpkg-deb --root-owner-group -Zxz --build "$root" "$out" >/dev/null + echo "mkdeb: wrote $out" + dpkg-deb --info "$out" | sed -n 's/^ \(Package\|Version\|Architecture\|Installed-Size\|Depends\):/ \1:/p' +} +build jarl-cli "$cli" +build jarl "$srv" rm -rf dist/deb -echo "mkdeb: wrote $out" -dpkg-deb --info "$out" | sed -n 's/^ \(Package\|Version\|Architecture\|Installed-Size\):/ \1:/p'