Release setup: tarballs, Gitea workflow, apt registry
make dist builds tarballs for linux/amd64 and linux/arm64. A pushed v* tag runs .gitea/workflows/release.yml, which checks the tag against VERSION and the CHANGELOG, tests, builds the tarballs and the .debs (amd64, arm64) and publishes them with scripts/gitea-release.sh: a Gitea release, and the .debs in the Debian registry (stable, or testing for a release candidate). As in Housecarl and Steward; no cli-dist, Guard is Linux only. docs/development.md has the steps, docs/install.md the apt source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d06bf8d531
commit
941d4fb10d
8 files changed
+302
-5
No files matched your search
@@ -0,0 +1,68 @@
|
||||
# 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
|
||||
# .debs and SHA256SUMS;
|
||||
# * the .debs (amd64, arm64) in the owner's Debian package registry:
|
||||
# distribution "stable", or "testing" for a pre-release (a version
|
||||
# with a hyphen), which Gitea also marks as a pre-release.
|
||||
#
|
||||
# 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
|
||||
# The release notes (a pre-release may use its final version's
|
||||
# section or Unreleased; see scripts/gitea-release.sh).
|
||||
if ! scripts/gitea-release.sh notes > /dev/null; then
|
||||
echo "::error::no release notes for $version in CHANGELOG.md"
|
||||
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
|
||||
env:
|
||||
# Checked against VERSION above; don't depend on the checkout
|
||||
# having the tag.
|
||||
RELEASE_TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
export DEB_MAINTAINER="$(git log -1 --format='%an <%ae>')"
|
||||
make dist
|
||||
make deb DEB_ARCH=amd64
|
||||
make deb DEB_ARCH=arm64
|
||||
|
||||
- name: Publish
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
run: scripts/gitea-release.sh
|
||||
@@ -51,7 +51,8 @@ make e2e # with ../housecarl's lab: needs Docker (DOCKER_HOST=unix:///var/r
|
||||
|
||||
As in Housecarl: commit only when asked, never push; branches
|
||||
`dev-X.Y.Z`; a release is a commit "Release X" setting `VERSION`, with
|
||||
the annotated tag `vX` ("Guard X").
|
||||
the annotated tag `vX` ("Guard X"). Pushing the tag runs
|
||||
`.gitea/workflows/release.yml` (docs/development.md, "Releasing").
|
||||
|
||||
## Shell hygiene
|
||||
|
||||
|
||||
+3
-1
@@ -25,4 +25,6 @@ All notable changes to Guard are documented here. The format follows
|
||||
`test` (a watch's patterns over a log), `status` (through a local
|
||||
socket), `bans`, `ban`, `unban`, `version`.
|
||||
- A Debian package with a sandboxed systemd unit, as the `guard` user in
|
||||
the `adm` and `systemd-journal` groups.
|
||||
the `adm` and `systemd-journal` groups, published with each release to
|
||||
the Gitea Debian registry (`stable`, or `testing` for a release
|
||||
candidate) along with tarballs for Linux amd64 and arm64.
|
||||
@@ -6,6 +6,11 @@
|
||||
# sudo make install install the binary, completion, systemd unit and sysusers file,
|
||||
# and an example /etc/guard/guard.yaml
|
||||
# make deb the Debian package in dist/ (DEB_ARCH=arm64 for another architecture)
|
||||
# make dist static binaries for linux/amd64 and linux/arm64 in dist/
|
||||
# (guard-<version>-linux-<arch>.tar.gz)
|
||||
#
|
||||
# Guard is Linux only (it reads the journal), so there is no cli-dist as in
|
||||
# Housecarl and Steward.
|
||||
|
||||
VERSION ?= $(shell cat VERSION 2>/dev/null || echo 0.0.0-dev)
|
||||
RELEASE_TAG ?= $(shell t=v$(VERSION); git tag --points-at HEAD 2>/dev/null | grep -qx "$$t" && git diff --quiet HEAD 2>/dev/null && echo $$t)
|
||||
@@ -30,7 +35,7 @@ DEB_DEV_VERSION := $(shell [ -n '$(DESCRIBE)' ] || exit 0; \
|
||||
echo '$(DESCRIBE)' | sed -E 's/^v(.*)-([0-9]+)-g([0-9a-f]{7})[0-9a-f]*$$/\1+git\2.g\3/' | tr - '~' | sed "s/$$/$$m/")
|
||||
DEB_VERSION ?= $(if $(RELEASE_TAG),$(subst -,~,$(VERSION)),$(or $(DEB_DEV_VERSION),$(subst -,~,$(VERSION))))
|
||||
|
||||
.PHONY: all build completion test e2e install install-files uninstall deb clean
|
||||
.PHONY: all build completion test e2e install install-files uninstall deb dist clean
|
||||
|
||||
all: build
|
||||
|
||||
@@ -81,5 +86,11 @@ uninstall:
|
||||
deb: build
|
||||
DEB_ARCH=$(DEB_ARCH) DEB_VERSION=$(DEB_VERSION) VERSION=$(VERSION) LDFLAGS="$(LDFLAGS)" GOFLAGS="$(GOFLAGS)" scripts/mkdeb.sh
|
||||
|
||||
dist: build
|
||||
for arch in amd64 arm64; do \
|
||||
GOOS=linux GOARCH=$$arch go build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o dist/guard-$(VERSION)-linux-$$arch/guard ./cmd/guard && \
|
||||
tar -C dist -czf dist/guard-$(VERSION)-linux-$$arch.tar.gz guard-$(VERSION)-linux-$$arch || exit 1; \
|
||||
done
|
||||
|
||||
clean:
|
||||
rm -rf bin dist
|
||||
@@ -58,6 +58,7 @@ watch:
|
||||
* [configuration.md](docs/configuration.md): `/etc/guard/guard.yaml`
|
||||
* [presets.md](docs/presets.md): the presets, and writing your own patterns
|
||||
* [cli.md](docs/cli.md): the `guard` command
|
||||
* [development.md](docs/development.md): building, testing, releasing
|
||||
* [CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
Housecarl's side: its [docs/bans.md](https://gitea.adbraun.lol/adam/housecarl/src/branch/main/docs/bans.md).
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Developing Guard
|
||||
|
||||
[AGENTS.md](../AGENTS.md) has the layout and the ground rules.
|
||||
|
||||
## Building and testing
|
||||
|
||||
```sh
|
||||
make # bin/guard and its bash completion
|
||||
make test # go vet, gofmt check, go test ./...
|
||||
make e2e # scripts/e2e.sh: needs Docker and ../housecarl
|
||||
make deb # dist/guard_<version>_<arch>.deb
|
||||
make dist # dist/guard-<version>-linux-<arch>.tar.gz (amd64, arm64)
|
||||
```
|
||||
|
||||
**Unit tests** need neither Housecarl nor a journal: the daemon tests
|
||||
ban into a fake Housecarl, the journal test runs a fake journalctl
|
||||
(`GUARD_JOURNALCTL`), and `TestPresets` runs each preset over its sample
|
||||
lines in `testdata/presets/`.
|
||||
|
||||
**The e2e test** borrows Housecarl's lab: a router container (Debian 13,
|
||||
nftables 1.1.3, the `housecarl-nft` image that Housecarl's `make e2e`
|
||||
makes) runs housecarld built from `../housecarl` (or `HOUSECARL_SRC`),
|
||||
a LAN server runs Guard over a log file, and a host on the internet
|
||||
misbehaves. It checks a ban at the router (one port, for that server
|
||||
alone), `never_ban`, bans waiting while Housecarl is down, and Guard on
|
||||
the router banning scanners from Housecarl's log of dropped packets.
|
||||
`KEEP=1` leaves the lab running; `DOCKER_HOST` points at another Docker
|
||||
daemon.
|
||||
|
||||
The systemd sandbox is only tested by installing the package in a
|
||||
container that runs systemd (`--privileged --cgroupns=host /sbin/init`).
|
||||
|
||||
## Releasing
|
||||
|
||||
As for Jarl, Steward and Housecarl:
|
||||
|
||||
1. Move "Unreleased" in `CHANGELOG.md` under `## [<version>] - <date>`.
|
||||
A release candidate may leave it there.
|
||||
2. Put the version in `VERSION`, and update the README's status line.
|
||||
3. Run `make test && make e2e`.
|
||||
4. Commit as "Release X", tag it `git tag -a vX -m "Guard X"`, and push.
|
||||
|
||||
The tag starts `.gitea/workflows/release.yml`, which checks the tag
|
||||
against `VERSION` and the CHANGELOG, runs the tests, builds the tarballs
|
||||
and the `.deb`s (amd64, arm64), and publishes them
|
||||
(`scripts/gitea-release.sh`): a Gitea release with the notes and the
|
||||
files, and the `.deb`s in the owner's Debian registry, `stable` for a
|
||||
release and `testing` for a release candidate. It needs the repository
|
||||
secret `RELEASE_TOKEN` (`write:repository`, `write:package`).
|
||||
+17
-2
@@ -28,11 +28,26 @@ A watch that reads Housecarl's own log (`housecarl-drops`) also needs
|
||||
|
||||
## 2. The package
|
||||
|
||||
Guard's releases publish its Debian package to the same Gitea registry
|
||||
as Jarl's, Steward's and Housecarl's: `stable` for releases, `testing`
|
||||
for release candidates. If apt already has it, `sudo apt install guard`
|
||||
is all it takes. Otherwise add it:
|
||||
|
||||
```sh
|
||||
sudo apt install ./guard_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/gitea-adam.asc
|
||||
echo "deb [signed-by=/etc/apt/keyrings/gitea-adam.asc] https://gitea.adbraun.lol/api/packages/adam/debian stable main" \
|
||||
| sudo tee /etc/apt/sources.list.d/gitea-adam.list
|
||||
sudo apt update
|
||||
sudo apt install guard
|
||||
```
|
||||
|
||||
It makes the `guard` user (in the `adm` and `systemd-journal` groups,
|
||||
For release candidates too, add the `testing` distribution: the same
|
||||
`deb` line with `testing` instead of `stable`. The release page also has
|
||||
the `.deb` files (`sudo apt install ./guard_0.1.0_amd64.deb`) and
|
||||
tarballs for Linux amd64 and arm64.
|
||||
|
||||
The package makes the `guard` user (in the `adm` and `systemd-journal` groups,
|
||||
to read logs), installs `guard.service` without starting it, and writes
|
||||
an example `/etc/guard/guard.yaml`.
|
||||
|
||||
|
||||
Executable
+150
@@ -0,0 +1,150 @@
|
||||
#!/bin/sh
|
||||
# Publishes a built release (make dist, make deb for each
|
||||
# architecture) to Gitea:
|
||||
#
|
||||
# * a release for tag v<VERSION> with the CHANGELOG section for VERSION as
|
||||
# its notes, and every file in dist/ (tarballs, .debs)
|
||||
# plus a SHA256SUMS of them attached;
|
||||
# * the .debs in the owner's Debian package registry, so servers can
|
||||
# 'apt install guard' 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.
|
||||
#
|
||||
# A version with a hyphen (0.2.0-rc.1) is a pre-release: Gitea marks the
|
||||
# release so and doesn't count it as the latest, and its packages go to
|
||||
# the "testing" distribution, so servers that follow "stable" never get
|
||||
# a pre-release from apt.
|
||||
#
|
||||
# Release notes: the CHANGELOG section "## [VERSION]". A pre-release
|
||||
# without one of its own uses the section of the version it leads up to
|
||||
# ("## [0.2.0]"), or else "## [Unreleased]".
|
||||
#
|
||||
# 'gitea-release.sh notes' only prints the notes, failing if there are
|
||||
# none (the workflow checks this before building).
|
||||
#
|
||||
# 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, testing for
|
||||
# a pre-release)
|
||||
# DEB_COMPONENT the apt component (default: main)
|
||||
#
|
||||
# Needs curl and jq.
|
||||
set -eu
|
||||
|
||||
VERSION=${VERSION:-$(cat VERSION)}
|
||||
prerelease=false
|
||||
case $VERSION in *-*) prerelease=true ;; esac
|
||||
|
||||
# section prints the CHANGELOG section headed "## [$1]" (without the
|
||||
# heading); nothing if there is none.
|
||||
section() {
|
||||
awk -v v="$1" '
|
||||
/^## \[/ { if (on) exit; on = index($0, "## [" v "]") == 1; next }
|
||||
on' CHANGELOG.md
|
||||
}
|
||||
|
||||
# The release notes (see above). A section with only blank lines counts
|
||||
# as missing.
|
||||
notes=$(section "$VERSION")
|
||||
from="[$VERSION]"
|
||||
if [ "$prerelease" = true ]; then
|
||||
for s in "${VERSION%%-*}" Unreleased; do
|
||||
[ -z "$(printf '%s' "$notes" | tr -d '[:space:]')" ] || break
|
||||
notes=$(section "$s")
|
||||
from="[$s]"
|
||||
done
|
||||
fi
|
||||
if [ -z "$(printf '%s' "$notes" | tr -d '[:space:]')" ]; then
|
||||
if [ "$prerelease" = true ]; then
|
||||
echo "gitea-release: CHANGELOG.md has no '## [$VERSION]', '## [${VERSION%%-*}]' or '## [Unreleased]' section with notes" >&2
|
||||
else
|
||||
echo "gitea-release: CHANGELOG.md has no '## [$VERSION]' section with notes" >&2
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
if [ "${1:-}" = notes ]; then
|
||||
echo "gitea-release: notes for $VERSION from CHANGELOG.md $from" >&2
|
||||
printf '%s\n' "$notes"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
: "${GITEA_URL:=${GITHUB_SERVER_URL:-}}" "${GITEA_REPO:=${GITHUB_REPOSITORY:-}}"
|
||||
: "${GITEA_URL:?set GITEA_URL}" "${GITEA_REPO:?set GITEA_REPO}" "${GITEA_TOKEN:?set GITEA_TOKEN}"
|
||||
if [ "$prerelease" = true ]; then
|
||||
DEB_DISTRIBUTION=${DEB_DISTRIBUTION:-testing}
|
||||
else
|
||||
DEB_DISTRIBUTION=${DEB_DISTRIBUTION:-stable}
|
||||
fi
|
||||
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; }
|
||||
# (Debian package versions have ~ for -: guard_0.2.0~rc.1_amd64.deb.)
|
||||
debversion=$(printf '%s' "$VERSION" | tr - '~')
|
||||
for f in $files; do
|
||||
case $f in *"$VERSION"* | *"$debversion"*) ;; *) 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 (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: ("Guard " + $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
|
||||
Reference in new issue
Block a user