Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/promote-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,20 @@ jobs:
mkdir -p .ci-output/packages
docker cp "$container:/workspace/.ci-output/packages/." .ci-output/packages/
(cd .ci-output/packages && sha256sum -c "SHA256SUMS-${STABLE}.txt")
# Stable binaries are rebuilt by the same tasks as a tag release, so they
# must carry cargo-auditable's `.dep-v0` section too. Without it the SBOMs
# generated from these files fall back to detection heuristics.
- name: Verify the audit section reached the shipped binaries
run: |
set -eu
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
for arch in x86_64 aarch64; do
tar -xzf ".ci-output/packages/rustnzb-${STABLE}-linux-${arch}.tar.gz" \
-C "$work" "rustnzb-${STABLE}/rustnzb"
objdump -h "$work/rustnzb-${STABLE}/rustnzb" | grep -q '\.dep-v0'
printf 'audit: linux-%s carries .dep-v0\n' "$arch"
done
- uses: actions/upload-artifact@v4
with:
name: rustnzb-${{ needs.resolve.outputs.stable_tag }}-release-assets
Expand Down
33 changes: 33 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,19 @@ jobs:
mkdir -p .ci-output/packages
docker cp "$container:/workspace/.ci-output/packages/." .ci-output/packages/
(cd .ci-output/packages && sha256sum -c "SHA256SUMS-${GITHUB_REF_NAME}.txt")
# The shipped binaries must carry cargo-auditable's `.dep-v0` section, or
# syft falls back to guessing and the SBOM is not dependency-accurate.
- name: Verify the audit section reached the shipped binaries
run: |
set -eu
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
for arch in x86_64 aarch64; do
tar -xzf ".ci-output/packages/rustnzb-${GITHUB_REF_NAME}-linux-${arch}.tar.gz" \
-C "$work" "rustnzb-${GITHUB_REF_NAME}/rustnzb"
objdump -h "$work/rustnzb-${GITHUB_REF_NAME}/rustnzb" | grep -q '\.dep-v0'
printf 'audit: linux-%s carries .dep-v0\n' "$arch"
done
- uses: sigstore/cosign-installer@v3.7.0
with:
cosign-release: v2.4.1
Expand All @@ -240,6 +253,26 @@ jobs:
case "$file" in *.spdx.json) continue ;; esac
syft "$file" -o "spdx-json=${file}.spdx.json"
done
# Package-manager manifests, rendered from the release's own checksum file
# that was produced above. This is a dry run: the files are uploaded as a
# workflow artefact for the operator to review and submit. Nothing is
# published to Homebrew, Scoop or winget from CI.
- name: Render Scoop and winget manifests (dry run, upload only)
run: |
set -eu
mkdir -p .ci-output/manifests
packaging/distribution/render-windows-manifests.sh \
"${GITHUB_REF_NAME}" \
--sums ".ci-output/packages/SHA256SUMS-${GITHUB_REF_NAME}.txt" \
--assets-dir .ci-output/packages \
--release-date "$(date -u +%Y-%m-%d)" \
--out .ci-output/manifests
- uses: actions/upload-artifact@v4
with:
name: rustnzb-${{ github.ref_name }}-distribution-manifests
path: .ci-output/manifests/packaging/**
if-no-files-found: error
retention-days: 30
# Build provenance (who built what, from which commit and workflow) for
# every binary package, plus the signed checksum file.
- name: Attest build provenance for the release artefacts
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ also carries generated release notes with the merged pull requests.

- **config:** Back up and restore every setting as one TOML file, with a masked
export by default and a validated import
- **packaging:** Homebrew formula and Scoop/winget manifest templates, rendered from release checksums
- Embed dependency data in release binaries with cargo-auditable
- **ledger:** Prune damage-ledger rows for removed jobs and expired holes
- **queue:** Leave held PAR2 volumes out of queue size and record volumes fetched in history
- **dispatch:** Hold PAR2 recovery volumes until the slice map shows damage
Expand Down
20 changes: 20 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
ARG CROSS_IMAGE=repo.indexarr.net/indexarr/rustnzb-ci-cross@sha256:d747454fca94ebfd9dba1f566d9f549c4c9fbedf785c6aba36a0c383d9b9a955
ARG UNRAR_VERSION=7.2.3
ARG UNRAR_SHA256=3995af0aa32b1505a566da053725551a1f0698dc42b2fdf7ba7d65db0d004e33
ARG CARGO_AUDITABLE_VERSION=v0.7.7
ARG CARGO_AUDITABLE_SHA256=4c8d3b08c3c34e9f2333367224dbbfb4aab68067698b43de59fe19aded3416dd

FROM alpine:3.23 AS unrar-builder

Expand All @@ -22,6 +24,8 @@ FROM --platform=$BUILDPLATFORM ${CROSS_IMAGE} AS builder
ARG TARGETPLATFORM
ARG RUSTNZB_BUILD_REF=local
ARG RELEASE_OPTIMIZED=false
ARG CARGO_AUDITABLE_VERSION
ARG CARGO_AUDITABLE_SHA256

WORKDIR /build

Expand All @@ -41,6 +45,19 @@ COPY Cargo.toml Cargo.lock ./
COPY apps apps
COPY crates crates

# cargo-auditable embeds the exact crate versions in a `.dep-v0` section, so
# `syft` reads the dependencies from the shipped binary instead of guessing
# from the build tree. Pinned by version and tarball checksum.
RUN set -eu; \
tarball=/tmp/cargo-auditable.tar.xz; \
curl -fsSL -o "$tarball" \
"https://github.com/rust-secure-code/cargo-auditable/releases/download/${CARGO_AUDITABLE_VERSION}/cargo-auditable-x86_64-unknown-linux-gnu.tar.xz"; \
printf '%s %s\n' "${CARGO_AUDITABLE_SHA256}" "$tarball" | sha256sum -c -; \
mkdir -p /usr/local/lib/cargo-auditable; \
tar -xJf "$tarball" -C /usr/local/lib/cargo-auditable --strip-components=1; \
rm -f "$tarball"; \
test -x /usr/local/lib/cargo-auditable/cargo-auditable

# The build uses only public crates.io dependencies for the rustnzb binary.
RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \
--mount=type=cache,target=/usr/local/cargo/git,sharing=locked \
Expand All @@ -57,9 +74,12 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \
CARGO_PROFILE_RELEASE_STRIP=symbols; \
fi; \
CARGO_TARGET_DIR=/build/target \
RUSTC_WORKSPACE_WRAPPER=/usr/local/lib/cargo-auditable/cargo-auditable \
CARGO_AUDITABLE_ORIG_ARGS='{"offline":false,"locked":true,"frozen":false,"config":[]}' \
RUSTNZB_BUILD_REF="$RUSTNZB_BUILD_REF" \
cargo zigbuild --release --locked -p rustnzb \
--features webdav,vendored-openssl --target "$rust_target"; \
objdump -h "/build/target/$rust_target/release/rustnzb" | grep -q '\.dep-v0'; \
mkdir -p /out; \
cp "/build/target/$rust_target/release/rustnzb" /out/rustnzb

Expand Down
100 changes: 100 additions & 0 deletions ci/tasks/auditable
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#!/bin/sh
# cargo-auditable install/verification, shared by the release build tasks.
#
# Every standalone release binary (Linux amd64/arm64, Windows) is built with
# rust-secure-code/cargo-auditable so its dependency list is embedded in a
# `.dep-v0` section (ELF) / `.dep-v0`-equivalent PE section. That is what lets
# `Syft`/`cargo audit bin`/`grype` read the exact crate versions from the shipped
# file instead of guessing from the build tree.
#
# The version and the Linux tarball checksum are pinned here, mirroring the
# SHA-256 style used for Zig and sccache in Dockerfile.ci. Bump both together.
#
# Usage:
# . ci/tasks/auditable
# install_cargo_auditable # puts cargo-auditable on PATH
# auditable_cargo build --release # `cargo` with audit data injected
# verify_auditable_binary PATH TRIPLE
set -eu

CARGO_AUDITABLE_VERSION=v0.7.7
CARGO_AUDITABLE_TARBALL=cargo-auditable-x86_64-unknown-linux-gnu.tar.xz
CARGO_AUDITABLE_SHA256=4c8d3b08c3c34e9f2333367224dbbfb4aab68067698b43de59fe19aded3416dd
CARGO_AUDITABLE_URL="https://github.com/rust-secure-code/cargo-auditable/releases/download/${CARGO_AUDITABLE_VERSION}/${CARGO_AUDITABLE_TARBALL}"

auditable_dir() {
printf '%s\n' "${CARGO_AUDITABLE_DIR:-$REPO_ROOT/.ci-output/cargo-auditable-${CARGO_AUDITABLE_VERSION}}"
}

install_cargo_auditable() {
dir=$(auditable_dir)
if [ ! -x "$dir/cargo-auditable" ]; then
mkdir -p "$dir"
tarball="$dir/$CARGO_AUDITABLE_TARBALL"
curl -fsSL -o "$tarball" "$CARGO_AUDITABLE_URL"
printf '%s %s\n' "$CARGO_AUDITABLE_SHA256" "$tarball" | sha256sum -c - >/dev/null
tar -xJf "$tarball" -C "$dir" --strip-components=1
rm -f "$tarball"
fi
test -x "$dir/cargo-auditable"
PATH="$dir:$PATH"
export PATH
}

# `cargo auditable <args>` for the build tasks that invoke cargo themselves and
# `RUSTC_WORKSPACE_WRAPPER=... cargo <args>` for the cross builders
# (cargo zigbuild / cargo xwin) that construct their own cargo invocation.
#
# cargo-auditable embeds the dependency list by running a rustc wrapper on
# workspace members only (RUSTC_WORKSPACE_WRAPPER), and only when
# CARGO_AUDITABLE_ORIG_ARGS is present. Exported as a function so the build
# scripts read the same way whichever cargo front end they use.
auditable_cargo() {
dir=$(auditable_dir)
test -x "$dir/cargo-auditable" || {
printf 'auditable_cargo: run install_cargo_auditable first\n' >&2
return 127
}
PATH="$dir:$PATH" "$dir/cargo-auditable" auditable "$@"
}

# The cross builders spawn cargo with a modified environment. Pass the wrapper
# explicitly and provide the flags `cargo auditable` would have recorded, so the
# wrapper can call `cargo metadata` with the same --locked/--offline options.
#
# The JSON is deliberately a plain string: cargo-auditable parses it as JSON,
# so the quotes and braces must survive the export unchanged.
auditable_env() {
dir=$(auditable_dir)
test -x "$dir/cargo-auditable" || {
printf 'auditable_env: run install_cargo_auditable first\n' >&2
return 127
}
# cargo-auditable parses this value as JSON, so its quotes and braces must
# stay literal. shellcheck's SC2089/SC2090 warnings describe exactly that
# (deliberate) quoting, hence the directive rather than a rewrite.
# shellcheck disable=SC2089,SC2090
CARGO_AUDITABLE_ORIG_ARGS='{"offline":false,"locked":true,"frozen":false,"config":[]}'
RUSTC_WORKSPACE_WRAPPER="$dir/cargo-auditable"
# shellcheck disable=SC2090
export CARGO_AUDITABLE_ORIG_ARGS
export RUSTC_WORKSPACE_WRAPPER
}

# Fail the release build when the audit data did not make it into the binary.
# ELF and PE both expose it through the section table, so objdump is enough and
# no extra dependency is required in the cross image.
verify_auditable_binary() {
binary=$1
triple=${2:-}
test -f "$binary" || {
printf 'verify_auditable_binary: %s is missing\n' "$binary" >&2
return 1
}
if objdump -h "$binary" 2>/dev/null | grep -q '\.dep-v0'; then
printf 'audit: %s carries .dep-v0 (%s)\n' "$binary" "${triple:-unknown target}"
return 0
fi
printf 'verify_auditable_binary: %s has no .dep-v0 audit section\n' "$binary" >&2
return 1
}
7 changes: 5 additions & 2 deletions ci/tasks/build-linux
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
#!/bin/sh
set -eu
. "$(dirname "$0")/lib.sh"
. "$(dirname "$0")/auditable"
trap cleanup_task_frontend EXIT
task_start build-linux cargo npm
task_start build-linux cargo npm curl xz
task_target_dir build-linux
ensure_frontend
install_cargo_auditable
if [ -n "${CI_COMMIT_TAG:-}" ]; then
export CARGO_PROFILE_RELEASE_LTO=fat CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1 CARGO_PROFILE_RELEASE_STRIP=symbols
fi
cargo build --release -p rustnzb --features webdav --locked
auditable_cargo build --release -p rustnzb --features webdav --locked
test -x "$CARGO_TARGET_DIR/release/rustnzb"
verify_auditable_binary "$CARGO_TARGET_DIR/release/rustnzb" x86_64-unknown-linux-gnu
ls -lh "$CARGO_TARGET_DIR/release/rustnzb"
show_sccache_stats
6 changes: 5 additions & 1 deletion ci/tasks/build-linux-arm64
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
#!/bin/sh
set -eu
. "$(dirname "$0")/lib.sh"
. "$(dirname "$0")/auditable"
trap cleanup_task_frontend EXIT
task_start build-linux-arm64 cargo zig
task_start build-linux-arm64 cargo zig curl xz
task_target_dir build-linux-arm64
ensure_frontend
install_cargo_auditable
auditable_env
if [ -n "${CI_COMMIT_TAG:-}" ]; then
export CARGO_PROFILE_RELEASE_LTO=fat CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1 CARGO_PROFILE_RELEASE_STRIP=symbols
fi
Expand All @@ -13,6 +16,7 @@ cargo zigbuild --release -p rustnzb \
--features webdav,vendored-openssl --locked
binary=$CARGO_TARGET_DIR/aarch64-unknown-linux-gnu/release/rustnzb
test -x "$binary"
verify_auditable_binary "$binary" aarch64-unknown-linux-gnu
file "$binary"
ls -lh "$binary"
show_sccache_stats
6 changes: 5 additions & 1 deletion ci/tasks/build-windows
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
#!/bin/sh
set -eu
. "$(dirname "$0")/lib.sh"
. "$(dirname "$0")/auditable"
trap cleanup_task_frontend EXIT
task_start build-windows cargo clang lld
task_start build-windows cargo clang lld curl xz
task_target_dir build-windows
ensure_frontend
install_cargo_auditable
auditable_env
if [ -n "${CI_COMMIT_TAG:-}" ]; then
export CARGO_PROFILE_RELEASE_LTO=fat CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1 CARGO_PROFILE_RELEASE_STRIP=symbols
fi
XWIN_ACCEPT_LICENSE=1 cargo xwin build --release --target x86_64-pc-windows-msvc --features webdav --locked
binary=$CARGO_TARGET_DIR/x86_64-pc-windows-msvc/release/rustnzb.exe
test -f "$binary"
verify_auditable_binary "$binary" x86_64-pc-windows-msvc
file "$binary"
ls -lh "$binary"
show_sccache_stats
40 changes: 40 additions & 0 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,49 @@ takes the rebuild path below.
ghcr.io/thedancingdeveloper-org/rustnzbd:latest
```

8. Refresh the package-manager manifests for the new release, then submit
them yourself. CI renders the Windows manifests but publishes nothing.

```bash
packaging/homebrew/update-formula.sh vX.Y.Z
packaging/distribution/render-windows-manifests.sh vX.Y.Z --release-date YYYY-MM-DD
```

Both scripts read the release's own `SHA256SUMS-<tag>.txt` and abort on a
mismatch. Commit the refreshed files and follow
[packaging/README.md](../packaging/README.md) for submission. Pre-release
tags are never offered to package managers.

GitHub generates release notes from the tagged history. Review them before
publishing; do not add version-specific release-note files to the repository.

## SBOMs and dependency accuracy

The standalone binaries are built with `cargo-auditable`, which embeds the
resolved crate list in a `.dep-v0` section of the executable. The release and
promotion workflows assert that section is present in both Linux tarballs
before generating SBOMs, so syft reads exact crate versions from the shipped
file rather than guessing from the build tree. The container image is built
the same way for the same reason.

`ci/tasks/auditable` pins the cargo-auditable version and its tarball SHA-256.
Bump both together. A binary built without the wrapper still works, but its
SBOM will list far fewer crates — the release workflow fails rather than
shipping one.

Every release asset gets a `.spdx.json` alongside it, generated by syft from
the asset itself (the Windows installer included). The container image carries
its SBOM as an image attestation instead.

### Cosign signatures on the mirrors

`SHA256SUMS-<tag>.txt` is signed keyless with the release workflow's OIDC
identity, and its `.sig`/`.pem` live on the GitHub release. The Forgejo
rollback registry and the old GHCR namespace mirror the image digest but not
the blob signature. Whether to copy the signature and certificate to those
mirrors is an open operator decision; until it is made, verify checksums
against the GitHub release.

## Standalone nzb-* crates

The seven `nzb-*` crates under `crates/` are published to crates.io from
Expand Down
Loading
Loading