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
11 changes: 11 additions & 0 deletions .github/workflows/quality-schedule.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,14 @@ jobs:
with:
name: mutation-report
path: .ci-artifacts/mutants

crate-drift:
runs-on: [self-hosted]
steps:
- uses: actions/checkout@v7
- name: Diff the standalone nzb-* repos against the monorepo head
run: ci/tasks/crate-drift
env:
# Optional: a token that can read the standalone repos if any are
# private. Public repos clone anonymously and this stays empty.
GH_TOKEN: ${{ secrets.NZB_SYNC_TOKEN }}
14 changes: 7 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,13 @@ feed-rs = "2"
unicode-normalization = "0.1"

# Shared NZB crates
nzb-web = { version = "0.4.20", path = "crates/nzb-web", features = ["groups-db"] }
nzb-nntp = { version = "0.2.22", path = "crates/nzb-nntp" }
nzb-core = { version = "0.2.17", path = "crates/nzb-core", features = ["groups-db"] }
nzb-decode = { version = "0.1.2", path = "crates/nzb-decode" }
nzb-news = { version = "0.1.12", path = "crates/nzb-news" }
nzb-dispatch = { version = "0.2.6", path = "crates/nzb-dispatch" }
nzb-postproc = { version = "0.2.7", path = "crates/nzb-postproc" }
nzb-web = { version = "0.4.22", path = "crates/nzb-web", features = ["groups-db"] }
nzb-nntp = { version = "0.2.24", path = "crates/nzb-nntp" }
nzb-core = { version = "0.2.18", path = "crates/nzb-core", features = ["groups-db"] }
nzb-decode = { version = "0.1.4", path = "crates/nzb-decode" }
nzb-news = { version = "0.1.14", path = "crates/nzb-news" }
nzb-dispatch = { version = "0.2.8", path = "crates/nzb-dispatch" }
nzb-postproc = { version = "0.2.8", path = "crates/nzb-postproc" }
mock-nntp-server = { path = "crates/mock-nntp-server" }
rust-par2 = { version = "0.1.3" }
yenc-simd = { version = "0.1.1" }
Expand Down
11 changes: 11 additions & 0 deletions ci/nzb-crates.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Standalone nzb-* crates kept in sync from this monorepo (the head).
# One crate directory name per line; blank lines and #-comments are ignored.
# The monorepo owns version numbers; standalone repos are sync targets.
# Used by ci/tasks/sync-crate and ci/tasks/crate-drift.
nzb-core
nzb-nntp
nzb-decode
nzb-news
nzb-dispatch
nzb-postproc
nzb-web
89 changes: 89 additions & 0 deletions ci/tasks/crate-drift
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/bin/sh
set -eu

# crate-drift — verify the standalone nzb-* repos still match the monorepo.
#
# The monorepo is the head: every synced crate's `src/` tree and its package
# version must be identical in the standalone repo it publishes from. This
# task clones each standalone repo (from the crate's `repository` field),
# compares it against `crates/<name>/`, and exits non-zero when any crate has
# drifted, naming the sync command to fix it. It never writes anywhere.
#
# Public repos clone anonymously. For private mirrors, set GH_TOKEN and the
# clone authenticates the same way as ci/tasks/mirror-github.

TASK_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
REPO_ROOT=$(CDPATH= cd -- "$TASK_DIR/../.." && pwd)
cd "$REPO_ROOT"

crate_list=ci/nzb-crates.txt
[ -f "$crate_list" ] || { echo "crate-drift: missing $crate_list" >&2; exit 2; }

workdir=$(mktemp -d)
askpass=""
cleanup() { rm -rf "$workdir"; [ -z "$askpass" ] || rm -f "$askpass"; }
trap cleanup EXIT

if [ -n "${GH_TOKEN:-}" ]; then
askpass=$(mktemp)
cat >"$askpass" <<'ASK'
#!/bin/sh
case "$1" in
*Username*) printf '%s\n' x-access-token ;;
*Password*) printf '%s\n' "$GH_TOKEN" ;;
*) exit 1 ;;
esac
ASK
chmod 700 "$askpass"
export GIT_ASKPASS="$askpass" GIT_TERMINAL_PROMPT=0
fi

manifest_field() {
# manifest_field <file> <key> — first `<key> = "value"` line's value.
grep -m1 "^$2[[:space:]]*=" "$1" | sed 's/[^"]*"\([^"]*\)".*/\1/'
}

failures=0
note_drift() { echo "drift: $1" >&2; failures=$((failures + 1)); }

while IFS= read -r crate; do
case "$crate" in ''|\#*) continue ;; esac

manifest="crates/$crate/Cargo.toml"
if [ ! -f "$manifest" ]; then
note_drift "$crate: no $manifest in the monorepo"
continue
fi

mono_version=$(manifest_field "$manifest" version)
repo=$(manifest_field "$manifest" repository)
if [ -z "$repo" ]; then
note_drift "$crate: no repository field in $manifest"
continue
fi

dest="$workdir/$crate"
if ! git clone --depth 1 --quiet "$repo" "$dest" 2>"$workdir/$crate.err"; then
note_drift "$crate: cannot clone $repo"
sed 's/^/ /' "$workdir/$crate.err" >&2 || true
continue
fi

std_version=$(manifest_field "$dest/Cargo.toml" version)
if [ "$mono_version" != "$std_version" ]; then
note_drift "$crate: version monorepo=$mono_version standalone=$std_version"
fi

if ! git -c core.fileMode=false diff --no-index --quiet \
"$dest/src" "crates/$crate/src"; then
note_drift "$crate: src/ differs from the standalone repo — run: ci/tasks/sync-crate $crate"
git -c core.fileMode=false diff --no-index --stat \
"$dest/src" "crates/$crate/src" 2>/dev/null | sed 's/^/ /' >&2 || true
fi
done <"$crate_list"

if [ "$failures" -ne 0 ]; then
echo "crate-drift: $failures crate(s) drifted from the monorepo head" >&2
exit 1
fi
echo "crate-drift: all synced crates match the monorepo head"
83 changes: 83 additions & 0 deletions ci/tasks/sync-crate
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/bin/sh
set -eu

# sync-crate <name> — carry one nzb-* crate from the monorepo head to its
# standalone repo.
#
# Exports crates/<name> at HEAD (tracked files only), replaces the standalone
# repo's tree with it, and opens a pull request against that repo's main
# branch. The monorepo owns version numbers, so the exported Cargo.toml
# carries the version across untouched. Publishing to crates.io happens from a
# tag on the standalone repo after this PR merges (see docs/RELEASING.md); this
# task never publishes and never pushes to the monorepo.
#
# Requires GH_TOKEN (a token that can push to and open PRs on the standalone
# repo) and the gh CLI.

crate=${1:?usage: sync-crate <nzb-crate-name>}

TASK_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
REPO_ROOT=$(CDPATH= cd -- "$TASK_DIR/../.." && pwd)
cd "$REPO_ROOT"

grep -qx "$crate" ci/nzb-crates.txt \
|| { echo "$crate is not a synced nzb-* crate (see ci/nzb-crates.txt)" >&2; exit 2; }

manifest="crates/$crate/Cargo.toml"
[ -f "$manifest" ] || { echo "unknown crate: no $manifest" >&2; exit 2; }

: "${GH_TOKEN:?GH_TOKEN is required}"
command -v gh >/dev/null 2>&1 || { echo "the gh CLI is required" >&2; exit 127; }

manifest_field() { grep -m1 "^$2[[:space:]]*=" "$1" | sed 's/[^"]*"\([^"]*\)".*/\1/'; }

repo=$(manifest_field "$manifest" repository)
version=$(manifest_field "$manifest" version)
[ -n "$repo" ] || { echo "$crate has no repository field" >&2; exit 2; }
slug=$(printf '%s' "$repo" | sed 's#https://github.com/##; s#\.git$##')
head_sha=$(git rev-parse --short HEAD)
branch="sync/monorepo-$head_sha"

workdir=$(mktemp -d)
askpass=$(mktemp)
cleanup() { rm -rf "$workdir"; rm -f "$askpass"; }
trap cleanup EXIT

cat >"$askpass" <<'ASK'
#!/bin/sh
case "$1" in
*Username*) printf '%s\n' x-access-token ;;
*Password*) printf '%s\n' "$GH_TOKEN" ;;
*) exit 1 ;;
esac
ASK
chmod 700 "$askpass"
export GIT_ASKPASS="$askpass" GIT_TERMINAL_PROMPT=0 GH_TOKEN

clone="$workdir/repo"
git clone --quiet "$repo" "$clone"
git -C "$clone" checkout -q -B "$branch"

# Replace the standalone tree with the monorepo crate at HEAD. Clear the old
# tracked files first so deletions in the monorepo propagate, then extract the
# crate subtree (git archive emits tracked files only, no target/).
git -C "$clone" ls-files -z | (cd "$clone" && xargs -0 rm -f --)
git archive "HEAD:crates/$crate" | tar -x -C "$clone"

git -C "$clone" add -A
if git -C "$clone" diff --cached --quiet; then
echo "sync-crate: $crate already matches the standalone repo at v$version; nothing to do"
exit 0
fi

git -C "$clone" commit -q \
-m "sync: $crate from rustnzb monorepo $head_sha (v$version)"
git -C "$clone" push -q --force-with-lease -u origin "$branch"

gh pr create --repo "$slug" --head "$branch" --base main \
--title "sync: $crate from rustnzb monorepo $head_sha" \
--body "Automated sync of \`crates/$crate\` at rustnzb monorepo commit $head_sha (version $version).

The monorepo owns nzb-* version numbers; this PR carries its current tree across verbatim. After merge, publish v$version from a \`v$version\` tag on this repo — see rustnzb docs/RELEASING.md."

echo "sync-crate: opened a PR on $slug from branch $branch (v$version)"
4 changes: 2 additions & 2 deletions crates/nzb-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "nzb-core"
version = "0.2.17"
version = "0.2.18"
edition = "2024"
description = "Shared models, config, NZB parser, and SQLite database for NZB clients"
license = "MIT"
Expand All @@ -12,7 +12,7 @@ default = []
groups-db = []

[dependencies]
nzb-nntp = { version = "0.2.22", path = "../nzb-nntp" }
nzb-nntp = { version = "0.2.24", path = "../nzb-nntp" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "1.1"
Expand Down
2 changes: 1 addition & 1 deletion crates/nzb-decode/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "nzb-decode"
version = "0.1.2"
version = "0.1.4"
edition = "2024"
description = "yEnc decoding, file assembly, and article cache for NZB download clients"
license = "MIT"
Expand Down
10 changes: 5 additions & 5 deletions crates/nzb-dispatch/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
[package]
name = "nzb-dispatch"
version = "0.2.6"
version = "0.2.8"
edition = "2024"
description = "Article-level dispatcher: per-server worker pool, priority gating, retry + hopeless tracking. Part of the nzb-* layered usenet engine."
license = "MIT"
repository = "https://github.com/TheDancingDeveloper-org/nzb-dispatch"
readme = "README.md"

[dependencies]
nzb-nntp = { version = "0.2.22", path = "../nzb-nntp" }
nzb-decode = { version = "0.1.2", path = "../nzb-decode" }
nzb-core = { version = "0.2.17", path = "../nzb-core" }
nzb-nntp = { version = "0.2.24", path = "../nzb-nntp" }
nzb-decode = { version = "0.1.4", path = "../nzb-decode" }
nzb-core = { version = "0.2.18", path = "../nzb-core" }

tokio = { version = "1", features = ["full"] }
async-trait = "0.1"
Expand All @@ -25,7 +25,7 @@ thiserror = "2"
unicode-normalization = "0.1"

[dev-dependencies]
nzb-nntp = { version = "0.2.22", path = "../nzb-nntp", features = ["test-support"] }
nzb-nntp = { version = "0.2.24", path = "../nzb-nntp", features = ["test-support"] }
tempfile = "3"
tokio = { version = "1", features = ["full", "test-util"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
Expand Down
6 changes: 3 additions & 3 deletions crates/nzb-news/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
[package]
name = "nzb-news"
version = "0.1.12"
version = "0.1.14"
edition = "2024"
description = "Layered NNTP download engine: persistent per-connection workers, priority-aware dispatch with cascade-retry, pipelined batch fetching. Pure fetch layer — no decode, no assembly."
license = "MIT"
repository = "https://github.com/TheDancingDeveloper-org/nzb-news"
readme = "README.md"

[dependencies]
nzb-nntp = { version = "0.2.22", path = "../nzb-nntp" }
nzb-nntp = { version = "0.2.24", path = "../nzb-nntp" }

tokio = { version = "1", features = ["sync", "rt", "time", "macros"] }
tracing = "0.1"

[dev-dependencies]
nzb-nntp = { version = "0.2.22", path = "../nzb-nntp", features = ["test-support"] }
nzb-nntp = { version = "0.2.24", path = "../nzb-nntp", features = ["test-support"] }
tokio = { version = "1", features = ["full", "test-util"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

Expand Down
2 changes: 1 addition & 1 deletion crates/nzb-nntp/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "nzb-nntp"
version = "0.2.22"
version = "0.2.24"
edition = "2024"
description = "Async NNTP client with TLS, pipelining, connection pooling, and multi-server support"
license = "MIT"
Expand Down
4 changes: 2 additions & 2 deletions crates/nzb-postproc/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "nzb-postproc"
version = "0.2.7"
version = "0.2.8"
edition = "2024"
description = "Post-processing pipeline: PAR2 verify/repair, archive extraction"
license = "MIT"
Expand All @@ -12,7 +12,7 @@ default = []
groups-db = ["nzb-core/groups-db"]

[dependencies]
nzb-core = { version = "0.2.17", path = "../nzb-core" }
nzb-core = { version = "0.2.18", path = "../nzb-core" }
tokio = { version = "1", features = ["rt", "sync", "process"] }
tracing = "0.1"
opentelemetry.workspace = true
Expand Down
Loading
Loading