Release binaries, security policy, and a full README

scripts/build-release.sh builds reproducible linux/amd64 and linux/arm64
archives with SHA256SUMS; the release workflow runs it on a v* tag after
vet, tests and govulncheck, and publishes the release. The README now
covers what the tool is, what it does step by step, why each decision is
made, how to deploy and run a mail host, the security model, and
troubleshooting. -h exits 0.
This commit is contained in:
2026-09-13 22:21:18 -07:00
parent d19696dec3
commit 1332ade130
6 changed files with 968 additions and 183 deletions
+4
View File
@@ -0,0 +1,4 @@
# Funding platforms shown behind the repository's Sponsor button.
# https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository
github: jcoffey-dev
+69
View File
@@ -0,0 +1,69 @@
# Publish a release when a version tag is pushed: test, check for known
# vulnerabilities, build the Linux binaries, attach them with their checksums.
#
# Tags are the date of the commit, as ihasmail's are: v2026.9.13, and
# v2026.9.13.1 for a second release on the same day.
name: Release
on:
push:
tags: ["v*"]
# For a tag whose run never started. GitHub has queued and then orphaned
# runs before, and a pushed tag has no other way to trigger this again.
workflow_dispatch:
inputs:
tag:
description: "Existing tag to release, e.g. v2026.9.13"
required: true
type: string
concurrency:
group: release-${{ github.event.inputs.tag || github.ref_name }}
cancel-in-progress: false
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
env:
TAG: ${{ github.event.inputs.tag || github.ref_name }}
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.inputs.tag || github.ref }}
fetch-depth: 0
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
- name: Check the tag names a commit on main
run: |
set -euo pipefail
git fetch --quiet origin main
git merge-base --is-ancestor "$(git rev-parse "$TAG^{commit}")" origin/main \
|| { echo "::error::$TAG is not on main"; exit 1; }
- name: Vet and test
run: |
go vet ./...
go test ./...
- name: Known vulnerabilities
run: go run golang.org/x/vuln/cmd/govulncheck@latest ./...
- name: Build
run: |
SOURCE_DATE_EPOCH="$(git log -1 --format=%ct "$TAG")" scripts/build-release.sh "$TAG" dist
- name: Publish
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
if gh release view "$TAG" >/dev/null 2>&1; then
gh release upload "$TAG" dist/* --clobber
else
gh release create "$TAG" dist/* --verify-tag --title "$TAG" --generate-notes
fi
+808 -180
View File
File diff suppressed because it is too large Load Diff
+43
View File
@@ -0,0 +1,43 @@
# Security Policy
## Supported Versions
Security fixes go into the latest release. Older releases do not receive
backported fixes; a fixed release is a download away, and the tool keeps no
state of its own between runs.
| Version | Supported |
| --- | --- |
| Latest release | :white_check_mark: |
| Older releases | :x: |
## Reporting a Vulnerability
**Please do not open a public GitHub issue for security vulnerabilities.**
Report them privately by emailing **johnellisATlinuxDOTcom**, with:
- A description of the issue and its impact
- Steps to reproduce
- The ihasmail-oneshot version (`ihasmail-oneshot version`)
- Whether it is in the tool itself or in the deployment it writes
You should hear back within a few days. Once a fix is released, disclosure
timing and credit are coordinated with you.
## Scope
In scope:
- Secrets the tool generates or writes: file modes, where they end up, what
outlives the setup
- The deployment it writes: what it publishes, and what it tells Stalwart to
trust (forwarded client addresses, addresses exempt from bans)
- The Caddyfile and compose.yaml it renders
Out of scope, and best reported upstream:
- Vulnerabilities in Stalwart itself
- Vulnerabilities in ihasmail itself — see
[its security policy](https://github.com/Coffey-Labs/ihasmail/security/policy)
- Vulnerabilities in Caddy or Docker
+4 -3
View File
@@ -60,10 +60,11 @@ func main() {
fmt.Fprintf(os.Stderr, "unknown command %q\n\n%s", os.Args[1], usageText)
os.Exit(2)
}
if err != nil {
if !errors.Is(err, flag.ErrHelp) {
fmt.Fprintf(os.Stderr, "!! %v\n", err)
if errors.Is(err, flag.ErrHelp) {
return // -h printed its usage, which is what was asked for
}
if err != nil {
fmt.Fprintf(os.Stderr, "!! %v\n", err)
os.Exit(1)
}
}
+40
View File
@@ -0,0 +1,40 @@
#!/bin/bash
# SPDX-FileCopyrightText: 2026 Coffey Labs
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Build the release archives: one per architecture, plus SHA256SUMS.
#
# Usage: scripts/build-release.sh VERSION [OUTDIR]
# scripts/build-release.sh v2026.9.13 dist
#
# The release workflow runs exactly this, so a release can be reproduced -- or
# checked before tagging -- on any machine with Go. Archive names carry no
# version, so .../releases/latest/download/<name> always means the newest.
set -euo pipefail
VERSION="${1:?usage: $0 VERSION [OUTDIR]}"
OUT="${2:-dist}"
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
rm -rf "$OUT" && mkdir -p "$OUT"
OUT="$(cd "$OUT" && pwd)"
STAGE="$(mktemp -d)"
trap 'rm -rf "$STAGE"' EXIT
# Linux only: the tool drives Docker on the host it runs on, and deploys a mail
# server that publishes ports there.
for arch in amd64 arm64; do
name="ihasmail-oneshot-linux-$arch"
mkdir -p "$STAGE/$name"
echo "==> building $name ($VERSION)"
(cd "$ROOT" && CGO_ENABLED=0 GOOS=linux GOARCH="$arch" go build -trimpath \
-ldflags "-s -w -X main.version=$VERSION" -o "$STAGE/$name/ihasmail-oneshot" ./cmd/ihasmail-oneshot)
cp "$ROOT/LICENSE" "$ROOT/README.md" "$STAGE/$name/"
# Fixed owner and time, so the same commit gives the same archive.
tar --sort=name --owner=0 --group=0 --numeric-owner --mtime="@${SOURCE_DATE_EPOCH:-0}" \
-C "$STAGE/$name" -czf "$OUT/$name.tar.gz" ihasmail-oneshot LICENSE README.md
done
(cd "$OUT" && sha256sum ./*.tar.gz | sed 's| \./| |' > SHA256SUMS)
echo "==> $OUT:"
(cd "$OUT" && cat SHA256SUMS)