Re: [ndctl PATCH] contrib: add Fedora release scripts

Dave Jiang <[email protected]> Tue, 14 Jul 2026 15:16:03 -0700
Newsgroups dev.linux.lists.nvdimm
Message-ID <[email protected]>

On 7/6/26 8:57 PM, Alison Schofield wrote:
> Documentation and scripts used by ndctl maintainers to build and
> release Fedora packages have traditionally been maintained outside
> the ndctl git repository. Bring them into the tree under
> contrib/fedora/ so the release process is documented and
> discoverable, and the scripts are reviewed and versioned alongside
> the project they serve.
> 
> The scripts automate the Fedora package build and release process.
> They run from the upstream repository and operate on a local clone
> of the Fedora package repository (dist-git), configured via
> contrib/fedora/config.local.
> 
> The workflow is split into discrete, individually verifiable steps:
> 
>   01-prep_build            download release, compose the spec, upload
>                            sources to the lookaside cache
>   02-do_build_local_checks sanity-check the dist-git workspace
>   03-do_build_push         commit and push the spec to dist-git main
>   04-do_build_branches     scratch-build gate, then fire Koji builds
>                            for rawhide and the stable branches
>   05-do_submit_updates     verify builds completed, submit bodhi
>                            updates for the stable branches
>   06-cleanup               remove temporary files
> 
> Notable design choices:
> 
>  - Stable branches receive the release by copying the spec/sources
>    files from dist-git main rather than merging, since stable
>    branches can permanently diverge from main (e.g. after a Fedora
>    mass rebuild lands on them independently).
> 
>  - Builds are submitted with --nowait and run in parallel in Koji;
>    bodhi submission is a separate step (05) run after the builds are
>    verified green, providing a natural checkpoint.
> 
>  - Every script supports a --check mode that verifies configuration
>    and paths without performing any real work.
> 
>  - Local mock builds are dropped in favor of the Koji scratch build,
>    which uses the real Fedora build environment with no local setup.
> 
> The contrib/ directory is excluded from release tarballs via
> .gitattributes export-ignore, and config.local is git-ignored as it
> is machine-specific.
> 
> Originally-by: Vishal Verma <[email protected]>
> Assisted-by: Claude:Opus-4-8
> Signed-off-by: Alison Schofield <[email protected]>

LGTM

Reviewed-by: Dave Jiang <[email protected]>

> ---
>  .gitattributes                          |   1 +
>  .gitignore                              |   1 +
>  contrib/fedora/01-prep_build            | 166 ++++++++++
>  contrib/fedora/02-do_build_local_checks | 112 +++++++
>  contrib/fedora/03-do_build_push         |  69 ++++
>  contrib/fedora/04-do_build_branches     | 232 ++++++++++++++
>  contrib/fedora/06-cleanup               |  38 +++
>  contrib/fedora/README.md                | 403 ++++++++++++++++++++++++
>  contrib/fedora/common.sh                |  82 +++++
>  contrib/fedora/config.local.example     |  12 +
>  10 files changed, 1116 insertions(+)
>  create mode 100644 .gitattributes
>  create mode 100755 contrib/fedora/01-prep_build
>  create mode 100755 contrib/fedora/02-do_build_local_checks
>  create mode 100755 contrib/fedora/03-do_build_push
>  create mode 100755 contrib/fedora/04-do_build_branches
>  create mode 100755 contrib/fedora/06-cleanup
>  create mode 100644 contrib/fedora/README.md
>  create mode 100644 contrib/fedora/common.sh
>  create mode 100644 contrib/fedora/config.local.example
> 
> diff --git a/.gitattributes b/.gitattributes
> new file mode 100644
> index 0000000..19d5126
> --- /dev/null
> +++ b/.gitattributes
> @@ -0,0 +1 @@
> +contrib/fedora export-ignore
> diff --git a/.gitignore b/.gitignore
> index eeb275f..94b77b2 100644
> --- a/.gitignore
> +++ b/.gitignore
> @@ -6,3 +6,4 @@ sles/ndctl.spec
>  tags
>  cscope.*
>  scripts/docsurgeon_parser.sh
> +contrib/fedora/config.local
> diff --git a/contrib/fedora/01-prep_build b/contrib/fedora/01-prep_build
> new file mode 100755
> index 0000000..11e97fb
> --- /dev/null
> +++ b/contrib/fedora/01-prep_build
> @@ -0,0 +1,166 @@
> +#!/bin/bash -eEx
> +#
> +# 01-prep_build <version>
> +#
> +# PURPOSE: Download upstream sources, extract the spec file from the upstream
> +# meson build, and compose a new ndctl.spec by merging the new spec body with
> +# the existing %changelog section.  Also uploads the new sources to the Fedora
> +# lookaside cache via 'fedpkg new-sources'.
> +#
> +# RUN FROM: upstream ndctl repo root (any branch; the release is downloaded
> +# by tag from GitHub, so the local upstream branch does not matter).
> +# The dist-git clone must be on 'main' -- that is checked automatically.
> +#
> +# TESTABLE WITHOUT BUILDING: run with --check to verify setup only:
> +#   ./contrib/fedora/01-prep_build --check vNN
> +#
> +# AFTER THIS SCRIPT: Review ndctl.spec in the dist-git directory before
> +# proceeding.  The spec should show the new Version, a bumped Release, and a
> +# new %changelog entry.
> +# If something looks wrong, restore with:
> +#   cp $DISTGIT/ndctl.spec.orig $DISTGIT/ndctl.spec
> +#
> +# NEXT STEP: ./contrib/fedora/02-do_build_local_checks <version>
> +#
> +
> +# Source shared functions (config loading, branch checks, preflight)
> +SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
> +# shellcheck source=common.sh
> +source "$SCRIPTS_DIR/common.sh"
> +
> +# --- Argument handling ---
> +check_only=false
> +if [[ "$1" == "--check" ]]; then
> +	check_only=true
> +	shift
> +fi
> +
> +ver="$1"
> +if [[ ! "$ver" ]]; then
> +	printf "Usage: %s [--check] <version>  e.g. vNN\n" "$0"
> +	printf "  --check: verify setup and paths without downloading or building anything\n"
> +	exit 1
> +fi
> +
> +srcfile="ndctl-${ver#v}.tar.gz"
> +srcdir="ndctl-${ver#v}"
> +
> +# --- Pre-flight checks ---
> +# These run even with --check, so you can test your setup at any time.
> +load_config
> +verify_upstream_repo
> +verify_distgit_branch "main"
> +preflight_check "$ver"
> +
> +if $check_only; then
> +	printf "Pre-flight checks passed (--check mode, stopping here).\n"
> +	exit 0
> +fi
> +
> +# --- Kerberos authentication ---
> +# Acquire a Kerberos ticket for Fedora if not already authenticated.
> +# Reads the Fedora username from ~/.fedora.upn if present.
> +if [[ -f ~/.fedora.upn ]]; then
> +	if ! klist | grep FEDORAPROJECT; then
> +		kinit "$(cat ~/.fedora.upn)@FEDORAPROJECT.ORG"
> +	fi
> +fi
> +
> +# --- Download upstream sources ---
> +# Downloads ndctl-<ver>.tar.gz from the GitHub release archive.
> +download_release()
> +{
> +	if ! wget "https://github.com/pmem/ndctl/archive/$ver.tar.gz" -O "$srcfile"; then
> +		printf "ERROR: failed to download sources for %s\n" "$ver"
> +		exit 1
> +	fi
> +	# Re-verify Kerberos before going further -- needed for fedpkg new-sources
> +	if ! klist | grep FEDORAPROJECT; then
> +		printf "ERROR: not authenticated with FEDORAPROJECT.org\n"
> +		printf "authenticate using 'kinit <user>@FEDORAPROJECT.ORG'\n"
> +		exit 1
> +	fi
> +}
> +
> +# --- Extract spec from upstream meson build ---
> +# ndctl ships its own spec under rhel/ndctl.spec, generated by meson.
> +# We extract it here and save it as ndctl.spec.src for use in compose_spec.
> +get_src_spec()
> +{
> +	tar xvf "$srcfile"
> +	pushd "$srcdir" || exit 1
> +	meson setup build
> +	meson compile -C build rhel/ndctl.spec
> +	# Save extracted spec alongside the tarball (in upstream repo dir, not dist-git)
> +	cp build/rhel/ndctl.spec "$SCRIPTS_DIR/../../ndctl.spec.src"
> +	popd || exit 1
> +	rm -rf "$srcdir"
> +}
> +
> +# --- Compose the Fedora spec ---
> +# Combines:
> +#   - spec body (everything above %changelog) from the upstream spec (ndctl.spec.src)
> +#   - %changelog section from the existing Fedora spec (ndctl.spec.orig)
> +# Bumps the Version and Release fields using rpmdev-bumpspec.
> +# All file operations happen inside DISTGIT.
> +compose_spec()
> +{
> +	local src_spec="$SCRIPTS_DIR/../../ndctl.spec.src"
> +	local orig="$DISTGIT/ndctl.spec.orig"
> +	local out="$DISTGIT/ndctl.spec"
> +
> +	# Save the current dist-git spec as .orig before touching anything
> +	if [ -f "$out" ]; then
> +		mv "$out" "$orig"
> +	else
> +		test -f "$orig"
> +	fi
> +	rm -f "$out"
> +	test -f "$src_spec"
> +
> +	# Bump version and add a %changelog entry in the .orig file
> +	rpmdev-bumpspec -c "release $ver" -n "${ver#v}" "$orig"
> +
> +	# Find the line where %changelog starts in each file
> +	local src_split
> +	src_split="$(grep -n "%changelog" "$src_spec" | cut -d':' -f1)"
> +	src_split="$((src_split - 1))"
> +	local orig_split
> +	orig_split="$(grep -n "%changelog" "$orig" | cut -d':' -f1)"
> +
> +	# Take the spec body from upstream, append our %changelog
> +	head -n +"$src_split" "$src_spec" > "$out"
> +	tail -n +"$orig_split" "$orig" >> "$out"
> +}
> +
> +# --- Main sequence ---
> +
> +download_release
> +get_src_spec
> +
> +# Make sure dist-git is up to date with origin/main before making changes
> +git -C "$DISTGIT" pull --ff-only
> +
> +compose_spec
> +
> +# Upload the new tarball to the Fedora lookaside cache.
> +# This also updates the 'sources' file and '.gitignore' in dist-git.
> +(cd "$DISTGIT" && fedpkg new-sources "$OLDPWD/$srcfile")
> +
> +cat <<- EOF
> +
> +	ndctl.spec has been composed in: $DISTGIT/ndctl.spec
> +
> +	ACTION REQUIRED: Review the spec and confirm it looks correct:
> +	  grep -E "^Version|^Release" $DISTGIT/ndctl.spec
> +	  Expected: Version: ${ver#v}  Release: 1%{?dist}
> +
> +	  Also check the top %changelog entry looks right:
> +	  head -30 $DISTGIT/ndctl.spec
> +
> +	If something looks wrong, restore the original:
> +	  cp $DISTGIT/ndctl.spec.orig $DISTGIT/ndctl.spec
> +
> +	If it looks good, proceed with:
> +	  ./contrib/fedora/02-do_build_local_checks $ver
> +EOF
> diff --git a/contrib/fedora/02-do_build_local_checks b/contrib/fedora/02-do_build_local_checks
> new file mode 100755
> index 0000000..54482fe
> --- /dev/null
> +++ b/contrib/fedora/02-do_build_local_checks
> @@ -0,0 +1,112 @@
> +#!/bin/bash -eEx
> +#
> +# 02-do_build_local_checks <version>
> +#
> +# PURPOSE: Sanity-check the dist-git workspace state before committing or
> +# pushing anything.  Verifies that ndctl.spec has been modified and that
> +# the sources and .gitignore files have been staged by 'fedpkg new-sources'.
> +#
> +# RUN FROM: upstream ndctl repo root
> +#
> +# TESTABLE WITHOUT BUILDING: run with --check to verify setup only:
> +#   ./contrib/fedora/02-do_build_local_checks --check vNN
> +#
> +# NOTE ON MOCK BUILDS: fedpkg mockbuild is intentionally skipped here.
> +# Mock requires local environment setup (mock group membership, Fedora build
> +# root configs) that is fragile and painful to maintain for a quarterly
> +# workflow.  Script 04 runs a scratch build in Koji instead, which uses
> +# Fedora's actual build infrastructure and is more reliable.  If the scratch
> +# build fails, nothing will have been pushed to any stable branch yet, so
> +# recovery is clean.  See README for details.
> +#
> +# If this script passes, the workspace is ready to commit and push.
> +# If it fails, something went wrong in script 01 -- do not proceed.
> +#
> +# NEXT STEP: ./contrib/fedora/03-do_build_push <version>
> +#
> +
> +SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
> +# shellcheck source=common.sh
> +source "$SCRIPTS_DIR/common.sh"
> +
> +# --- Argument handling ---
> +check_only=false
> +if [[ "$1" == "--check" ]]; then
> +	check_only=true
> +	shift
> +fi
> +
> +ver="$1"
> +if [[ ! "$ver" ]]; then
> +	printf "Usage: %s [--check] <version>  e.g. vNN\n" "$0"
> +	printf "  --check: verify setup and paths without checking workspace state\n"
> +	exit 1
> +fi
> +
> +# --- Pre-flight checks ---
> +load_config
> +verify_upstream_repo
> +verify_distgit_branch "main"
> +preflight_check "$ver"
> +
> +if $check_only; then
> +	printf "Pre-flight checks passed (--check mode, stopping here).\n"
> +	exit 0
> +fi
> +
> +# --- Workspace state checks ---
> +# These verify that script 01 completed successfully and left dist-git in
> +# the expected state before we commit anything.
> +
> +trap "unwind" ERR
> +unwind()
> +{
> +	# If ndctl.spec was accidentally staged during a partial failure,
> +	# unstage it so the workspace is back to a clean state
> +	if git -C "$DISTGIT" ls-files --cached ndctl.spec | grep -q ndctl.spec; then
> +		git -C "$DISTGIT" reset HEAD ndctl.spec
> +	fi
> +}
> +
> +# check_cached: verify a file is staged in the dist-git index.
> +# sources and .gitignore are staged by 'fedpkg new-sources' in script 01.
> +check_cached()
> +{
> +	local file="$1"
> +	test -f "$DISTGIT/$file"
> +	if [[ "$(git -C "$DISTGIT" ls-files --cached "$file")" == "$file" ]]; then
> +		printf "  OK (staged):    %s\n" "$file"
> +		return
> +	else
> +		printf "ERROR: Expected '%s' to be staged in dist-git but it is not.\n" "$file"
> +		printf "Did 'fedpkg new-sources' run successfully in script 01?\n"
> +		exit 1
> +	fi
> +}
> +
> +# check_modified: verify a file has unstaged modifications in dist-git.
> +# ndctl.spec should be modified but not yet staged -- it gets staged in script 03.
> +check_modified()
> +{
> +	local file="$1"
> +	test -f "$DISTGIT/$file"
> +	if [[ "$(git -C "$DISTGIT" ls-files --modified "$file")" == "$file" ]]; then
> +		printf "  OK (modified):  %s\n" "$file"
> +		return
> +	else
> +		printf "ERROR: Expected '%s' to be modified in dist-git but it is not.\n" "$file"
> +		printf "Did script 01 run successfully and update the spec?\n"
> +		exit 1
> +	fi
> +}
> +
> +printf "Checking dist-git workspace state in: %s\n" "$DISTGIT"
> +check_modified "ndctl.spec"
> +check_cached "sources"
> +check_cached ".gitignore"
> +
> +# Uncomment to re-enable local build checks (see note above about mock):
> +# (cd "$DISTGIT" && fedpkg mockbuild)   # full mock build -- slow but thorough
> +# (cd "$DISTGIT" && fedpkg lint -i)     # RPM lint checks
> +
> +printf "\nAll checks passed. Proceed with:\n  ./contrib/fedora/03-do_build_push %s\n" "$ver"
> diff --git a/contrib/fedora/03-do_build_push b/contrib/fedora/03-do_build_push
> new file mode 100755
> index 0000000..d9df424
> --- /dev/null
> +++ b/contrib/fedora/03-do_build_push
> @@ -0,0 +1,69 @@
> +#!/bin/bash -eEx
> +#
> +# 03-do_build_push <version>
> +#
> +# PURPOSE: Commit the updated spec/sources in dist-git and push to the 'main'
> +# branch on Fedora dist-git.  This is the single push that makes the new
> +# version available for all stable branches to merge from.
> +#
> +# RUN FROM: upstream ndctl repo root
> +#
> +# TESTABLE WITHOUT BUILDING: run with --check to verify setup only:
> +#   ./contrib/fedora/03-do_build_push --check vNN
> +#
> +# IMPORTANT: This script pushes to dist-git 'main' only.  Script 04 handles
> +# merging and pushing to stable branches (f42, f43, f44).  Pushing to main
> +# first is critical -- script 04 copies the release files *from main*, so if
> +# this step is accidentally done on a stable branch, the other branches would
> +# get the old version.
> +# (Ask me how I know.  See README: "Script ran on the wrong branch".)
> +#
> +# NEXT STEP: ./contrib/fedora/04-do_build_branches <version>
> +#
> +
> +SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
> +# shellcheck source=common.sh
> +source "$SCRIPTS_DIR/common.sh"
> +
> +# --- Argument handling ---
> +check_only=false
> +if [[ "$1" == "--check" ]]; then
> +	check_only=true
> +	shift
> +fi
> +
> +ver="$1"
> +if [[ ! "$ver" ]]; then
> +	printf "Usage: %s [--check] <version>  e.g. vNN\n" "$0"
> +	printf "  --check: verify setup and paths without committing or pushing anything\n"
> +	exit 1
> +fi
> +
> +# --- Pre-flight checks ---
> +load_config
> +verify_upstream_repo
> +verify_distgit_branch "main"
> +preflight_check "$ver"
> +
> +if $check_only; then
> +	printf "Pre-flight checks passed (--check mode, stopping here).\n"
> +	exit 0
> +fi
> +
> +# --- Commit and push to dist-git main ---
> +
> +# Stage the spec file. sources and .gitignore were already staged by
> +# 'fedpkg new-sources' in script 01.
> +git -C "$DISTGIT" add ndctl.spec
> +
> +# Commit using the top %changelog entry as the commit message.
> +(cd "$DISTGIT" && fedpkg commit --clog)
> +
> +# Push to origin/main only.
> +# Script 04 handles the stable branches.
> +(cd "$DISTGIT" && fedpkg push)
> +
> +printf "\nPushed to dist-git main. Proceed with:\n"
> +printf "  ./contrib/fedora/04-do_build_branches %s\n" "$ver"
> +printf "which fires the Koji builds, then (once they are green):\n"
> +printf "  ./contrib/fedora/05-do_submit_updates %s\n" "$ver"
> diff --git a/contrib/fedora/04-do_build_branches b/contrib/fedora/04-do_build_branches
> new file mode 100755
> index 0000000..46171f7
> --- /dev/null
> +++ b/contrib/fedora/04-do_build_branches
> @@ -0,0 +1,232 @@
> +#!/bin/bash -eEx
> +#
> +# 04-do_build_branches <version>
> +#
> +# PURPOSE: Run a scratch build as a gate, then fire off real Koji builds for
> +# main (rawhide) and each stable branch.  Builds run in parallel in Koji; this
> +# script does NOT wait for them.  Watch them at the printed URLs, and when all
> +# are green, run 05-do_submit_updates to submit the bodhi updates.
> +#
> +# RUN FROM: upstream ndctl repo root
> +#
> +# TESTABLE WITHOUT BUILDING: run with --check to verify setup only:
> +#   ./contrib/fedora/04-do_build_branches --check vNN
> +#
> +# USAGE:
> +#   ./contrib/fedora/04-do_build_branches vNN              # scratch gate + main + stable branches
> +#   br=f44 ./contrib/fedora/04-do_build_branches vNN       # one branch only (skips scratch + main)
> +#   num_stable=3 ./contrib/fedora/04-do_build_branches vNN # build 3 stable branches instead of 2
> +#
> +# ENVIRONMENT VARIABLES:
> +#   br          - if set, only build this one branch (skips scratch build and main)
> +#   num_stable  - number of stable branches to build, newest first (default: 2)
> +#                 Check https://endoflife.date/fedora for the current stable set;
> +#                 use 3 only during the transition window when a new Fedora is out
> +#                 but the oldest release has not yet gone EOL.
> +#
> +# HOW BRANCHES GET THE NEW VERSION:
> +#   For each stable branch, the release files (ndctl.spec, .gitignore, sources)
> +#   are copied directly from dist-git main and committed.  This deliberately
> +#   avoids 'git merge': stable branches can permanently diverge from main
> +#   (e.g. after a Fedora mass rebuild lands on them independently, or after a
> +#   past manual fix-up), which makes fast-forward merges impossible.  The file
> +#   copy works identically whether or not the branch has diverged, and is a
> +#   no-op if the branch already has the files (safe to re-run).
> +#
> +# IF THE SCRATCH BUILD FAILS:
> +#   Fix the spec in dist-git main, commit and push, then re-run from the top.
> +#   Nothing will have been pushed to any stable branch yet.
> +#
> +# IF A BRANCH BUILD FAILS:
> +#   Check the Koji task URL printed in the output for the build log.
> +#   Fix the issue, then retry just that branch: br=<branch> $0 <version>
> +#
> +# CHECK BUILD STATUS:
> +#   Koji:  https://koji.fedoraproject.org/koji/packageinfo?packageID=ndctl
> +#
> +# NEXT STEP (after all builds are green in Koji):
> +#   ./contrib/fedora/05-do_submit_updates <version>
> +#
> +
> +SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
> +# shellcheck source=common.sh
> +source "$SCRIPTS_DIR/common.sh"
> +
> +# --- Argument handling ---
> +check_only=false
> +if [[ "$1" == "--check" ]]; then
> +	check_only=true
> +	shift
> +fi
> +
> +ver="$1"
> +: "${num_stable:=2}"   # how many stable branches to build (newest first)
> +: "${br:=""}"          # if set, build only this one branch
> +
> +if [[ ! "$ver" ]]; then
> +	printf "Usage: %s [--check] <version>  e.g. vNN\n" "$0"
> +	printf "  --check: verify setup and print branch list without building anything\n"
> +	exit 1
> +fi
> +
> +# --- Pre-flight checks ---
> +load_config
> +preflight_check "$ver"
> +
> +if $check_only; then
> +	printf "Stable branches that would be built (num_stable=%s):\n" "$num_stable"
> +	git -C "$DISTGIT" branch -r --list origin/f* | cut -d'/' -f2 | sort -Vr | head -"$num_stable"
> +	printf "Pre-flight checks passed (--check mode, stopping here).\n"
> +	exit 0
> +fi
> +
> +# Collect "branch: task URL" lines for the final summary
> +build_summary=()
> +
> +# --- fedpkg_build_wait ---
> +# Submit a build and poll Koji every 30 seconds until it completes.
> +# Used ONLY for the scratch build, which acts as a gate: it must pass before
> +# anything is pushed to stable branches.
> +# On failure: prints the Koji task URL and exits 1.
> +fedpkg_build_wait()
> +{
> +	# Poll up to 20 times (20 x 30s = 10 minutes) before giving up
> +	local timeout=20
> +
> +	(cd "$DISTGIT" && fedpkg build --nowait "$@") | tee /tmp/fedpkg-build-out
> +	taskid="$(grep -E "^Created task: [0-9]*" /tmp/fedpkg-build-out | cut -d':' -f2 | tr -d ' ')"
> +
> +	printf "Waiting for Koji task %s...\n" "$taskid"
> +	printf "  Task URL: https://koji.fedoraproject.org/koji/taskinfo?taskID=%s\n" "$taskid"
> +
> +	while true; do
> +		sleep 30
> +		koji taskinfo "$taskid" > /tmp/fedpkg-build-state || true
> +		if [ -f /tmp/fedpkg-build-state ]; then
> +			state="$(grep -E "^State: .*" /tmp/fedpkg-build-state | cut -d':' -f2 | tr -d ' ')"
> +			if [ -z "$state" ]; then
> +				timeout=$((timeout - 1))
> +				if ((timeout == 0)); then
> +					printf "ERROR: timed out waiting for Koji state after 10 minutes\n"
> +					printf "Check manually: https://koji.fedoraproject.org/koji/taskinfo?taskID=%s\n" "$taskid"
> +					exit 1
> +				fi
> +				continue
> +			fi
> +			case "$state" in
> +			closed)
> +				printf "Task %s completed successfully.\n" "$taskid"
> +				break
> +				;;
> +			open)
> +				continue
> +				;;
> +			failed)
> +				printf "ERROR: Koji task %s failed.\n" "$taskid"
> +				printf "Check: https://koji.fedoraproject.org/koji/taskinfo?taskID=%s\n" "$taskid"
> +				exit 1
> +				;;
> +			esac
> +		fi
> +	done
> +	rm -f /tmp/fedpkg-build-out /tmp/fedpkg-build-state
> +}
> +
> +# --- fedpkg_build_nowait ---
> +# Submit a build to Koji and return immediately.  Records the task URL in
> +# build_summary for the final printout.  Builds run in parallel in Koji.
> +fedpkg_build_nowait()
> +{
> +	local label="$1"
> +
> +	(cd "$DISTGIT" && fedpkg build --nowait) | tee /tmp/fedpkg-build-out
> +	taskid="$(grep -E "^Created task: [0-9]*" /tmp/fedpkg-build-out | cut -d':' -f2 | tr -d ' ')"
> +	build_summary+=("$label: https://koji.fedoraproject.org/koji/taskinfo?taskID=$taskid")
> +	rm -f /tmp/fedpkg-build-out
> +}
> +
> +# --- prepare_and_build_branch ---
> +# Switch dist-git to a branch, copy the release files from main, commit,
> +# push, and fire off the build (without waiting).
> +#
> +# The file copy replaces the old 'git merge main --ff-only' approach -- see
> +# "HOW BRANCHES GET THE NEW VERSION" in the header for why.
> +prepare_and_build_branch()
> +{
> +	local branch="$1"
> +	(cd "$DISTGIT" && fedpkg switch-branch "$branch")
> +
> +	# Copy the release files directly from main.  Works whether or not the
> +	# branch has diverged from main.
> +	git -C "$DISTGIT" checkout main -- ndctl.spec .gitignore sources
> +	git -C "$DISTGIT" add ndctl.spec .gitignore sources
> +
> +	# Commit only if the copy actually changed something.  Makes this
> +	# idempotent: safe to re-run on a branch that was already prepared.
> +	if git -C "$DISTGIT" diff --cached --quiet; then
> +		printf "  %s already has the release files; nothing to commit\n" "$branch"
> +	else
> +		git -C "$DISTGIT" commit -m "release $ver"
> +	fi
> +
> +	(cd "$DISTGIT" && fedpkg push)
> +	fedpkg_build_nowait "$branch"
> +}
> +
> +# print_summary: show all fired builds and the next step
> +print_summary()
> +{
> +	printf "\n=== Builds submitted (running in parallel in Koji) ===\n"
> +	local line
> +	for line in "${build_summary[@]}"; do
> +		printf "  %s\n" "$line"
> +	done
> +	printf "\nAll builds: https://koji.fedoraproject.org/koji/packageinfo?packageID=ndctl\n"
> +	printf "\nWhen all builds show green (state 'complete'), submit the bodhi updates:\n"
> +	printf "  ./contrib/fedora/05-do_submit_updates %s\n" "$ver"
> +}
> +
> +# --- Single branch mode ---
> +# If 'br' is set, prepare and build only that branch, then exit.
> +# Skips the scratch build and main.  Useful for retrying a failed branch.
> +if [[ $br ]]; then
> +	if ! git -C "$DISTGIT" branch -l "$br" | grep -q "$br"; then
> +		printf "ERROR: branch '%s' not found in dist-git\n" "$br"
> +		printf "Available branches:\n"
> +		git -C "$DISTGIT" branch -l
> +		exit 1
> +	fi
> +	prepare_and_build_branch "$br"
> +	(cd "$DISTGIT" && fedpkg switch-branch main)
> +	print_summary
> +	exit 0
> +fi
> +
> +# --- Normal full build mode ---
> +
> +# Step 1: scratch build on main -- the gate.
> +# Runs in Koji using Fedora's actual build infrastructure and WAITS for the
> +# result.  If this fails, the spec has a problem: fix it on main, push, and
> +# re-run.  Nothing has been pushed to any stable branch yet at this point.
> +printf "Step 1: scratch build to verify spec builds cleanly (this one waits)...\n"
> +(cd "$DISTGIT" && fedpkg switch-branch main)
> +fedpkg_build_wait --scratch
> +
> +# Step 2: fire the real build on main (becomes rawhide / next Fedora).
> +# No bodhi update needed for rawhide -- bodhi creates one automatically
> +# when the build completes.
> +printf "Step 2: submitting main (rawhide) build...\n"
> +fedpkg_build_nowait "main (rawhide)"
> +
> +# Step 3: prepare and fire builds for the stable branches, newest first.
> +printf "Step 3: submitting builds for %s stable branches...\n" "$num_stable"
> +git -C "$DISTGIT" branch -r --list origin/f* | cut -d'/' -f2 | sort -Vr | head -"$num_stable" > /tmp/fedpkg-branches
> +while read -r branch; do
> +	printf "  Preparing and building %s...\n" "$branch"
> +	prepare_and_build_branch "$branch"
> +done < /tmp/fedpkg-branches
> +rm -f /tmp/fedpkg-branches
> +
> +(cd "$DISTGIT" && fedpkg switch-branch main)
> +
> +print_summary
> diff --git a/contrib/fedora/06-cleanup b/contrib/fedora/06-cleanup
> new file mode 100755
> index 0000000..45c9721
> --- /dev/null
> +++ b/contrib/fedora/06-cleanup
> @@ -0,0 +1,38 @@
> +#!/bin/bash -eEx
> +#
> +# 06-cleanup
> +#
> +# PURPOSE: Remove temporary files generated during the build process.
> +# Safe to run at any time after script 01 has completed.
> +#
> +# RUN FROM: upstream ndctl repo root
> +#
> +# What gets removed:
> +#   ndctl.spec.src        - upstream spec extracted by script 01 (meson output)
> +#   $DISTGIT/ndctl.spec.orig  - backup of previous Fedora spec made by script 01
> +#   $DISTGIT/*.src.rpm    - any local source RPMs from mock builds
> +#   $DISTGIT/results_ndctl/   - mock build output directory
> +#   $DISTGIT/copr-out/    - COPR build output directory
> +#
> +# NOTE: ndctl.spec itself is NOT removed -- it is the live Fedora spec file
> +# tracked in dist-git.
> +#
> +
> +SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
> +# shellcheck source=common.sh
> +source "$SCRIPTS_DIR/common.sh"
> +load_config
> +
> +# Upstream repo root (two levels up from contrib/fedora/)
> +UPSTREAM_ROOT="$(cd "$SCRIPTS_DIR/../.." && pwd)"
> +
> +# Remove temp spec files from upstream repo root
> +rm -f "$UPSTREAM_ROOT/ndctl.spec.src"
> +
> +# Remove temp files from dist-git
> +rm -f "$DISTGIT/ndctl.spec.orig"
> +rm -f "$DISTGIT"/*.src.rpm
> +rm -rf "$DISTGIT/results_ndctl"
> +rm -rf "$DISTGIT/copr-out"
> +
> +printf "Cleanup complete.\n"
> diff --git a/contrib/fedora/README.md b/contrib/fedora/README.md
> new file mode 100644
> index 0000000..62ea51e
> --- /dev/null
> +++ b/contrib/fedora/README.md
> @@ -0,0 +1,403 @@
> +# ndctl Fedora Package Build Scripts
> +
> +## Overview
> +
> +These scripts live in `contrib/fedora/` in the upstream ndctl repo and automate
> +the process of building and publishing a new ndctl release to Fedora.  They are
> +meant to be run in order, once per release (~quarterly), from your upstream ndctl
> +clone — not from the dist-git clone.
> +
> +The general flow is:
> +1. Download the new upstream sources and compose a new spec file
> +2. Sanity-check the dist-git workspace before pushing anything
> +3. Commit and push the spec/sources to dist-git `main`
> +4. Fire off Koji builds for main and each stable Fedora branch
> +5. After the builds are green, submit the bodhi updates
> +6. Clean up temporary files
> +
> +Scripts 04 and 05 are deliberately split: 04 submits the builds and exits
> +(Koji runs them in parallel), you watch them complete on the Koji web page,
> +and then 05 verifies they finished and submits the bodhi updates.  This gives
> +a natural human checkpoint instead of a script blocking for hours.
> +
> +---
> +
> +## One-Time Setup
> +
> +These scripts need to know where your local dist-git clone lives.  Copy the
> +example config and fill in your path:
> +
> +```bash
> +cp contrib/fedora/config.local.example contrib/fedora/config.local
> +# edit config.local and set DISTGIT to your dist-git clone path
> +```
> +
> +`config.local` is excluded from git and from release tarballs (see
> +`.gitattributes`).  It is specific to your machine and never committed.
> +
> +You can verify your setup at any time without doing any real work by running
> +any script with `--check`:
> +
> +```bash
> +./contrib/fedora/01-prep_build --check vNN
> +./contrib/fedora/04-do_build_branches --check vNN   # also prints branch list
> +./contrib/fedora/05-do_submit_updates --check vNN   # also prints the NVRs it would verify
> +```
> +
> +---
> +
> +## Two Repos, One Workflow
> +
> +These scripts operate across two separate git repos:
> +
> +| Repo | Purpose | Typical location |
> +|---|---|---|
> +| **upstream** (this repo) | ndctl source code; where scripts live | `~/src/ndctl` |
> +| **dist-git** | Fedora packaging repo (fedpkg clone) | `~/fedora-pkgs/ndctl` |
> +
> +**All scripts are run from the upstream repo root.**  They use `$DISTGIT` (from
> +`config.local`) to operate on the dist-git clone as needed.
> +
> +---
> +
> +## Branch Strategy
> +
> +Fedora dist-git uses separate branches per release:
> +
> +- `main` / `rawhide` — the next unreleased Fedora (e.g. fc45).  Builds here
> +  but no bodhi update — rawhide gets updates automatically.
> +- `f44`, `f43`, `f42` — current stable Fedora releases.  Builds here AND
> +  submit bodhi updates so the package reaches users.
> +- `f41` and older — EOL, do not build.
> +
> +**Scripts 01-03 always operate on dist-git `main`.**  Script 04 then fans out
> +to stable branches by **copying the release files (`ndctl.spec`, `.gitignore`,
> +`sources`) from `main`** and committing them on each branch.  This means the
> +new version must land on `main` first, before any stable branch builds are
> +attempted.
> +
> +The file copy is used instead of `git merge` on purpose: stable branches can
> +permanently diverge from main (a Fedora mass rebuild landing on a branch
> +independently, or a past manual fix-up), which makes fast-forward merges
> +impossible.  The file copy works identically whether or not a branch has
> +diverged, and is a no-op if the branch already has the files — safe to re-run.
> +
> +After script 03, the dist-git branch state should look like:
> +```
> +main  → vNN    ← just pushed (the new release)
> +f44   → vNN-1  ← script 04 will copy the files from main and build
> +f43   → vNN-1  ← script 04 will copy the files from main and build
> +
> +(fNN branch names and prior versions shown are examples; the set of stable
> +branches changes as Fedora releases roll forward)
> +```
> +
> +---
> +
> +## Prerequisites
> +
> +- `kinit <user>@FEDORAPROJECT.ORG` — Kerberos auth (script 01 handles this
> +  automatically if `~/.fedora.upn` contains your Fedora username)
> +- Tools installed: `fedpkg`, `koji`, `rpm-build`, `rpmdev-bumpspec`, `wget`,
> +  `meson`
> +- dist-git clone on `main` branch before starting
> +
> +---
> +
> +## Script Reference
> +
> +### 01-prep_build \<version\>
> +
> +**Purpose:** Download upstream sources, extract the spec from the upstream meson
> +build, and compose a new `ndctl.spec` in dist-git by merging the upstream spec
> +body with the existing `%changelog` section.  Also uploads the new tarball to
> +the Fedora lookaside cache.
> +
> +**Run from:** upstream repo root (any branch — the release tarball is
> +downloaded from GitHub by tag; your local upstream tree is not used).
> +The **dist-git** clone must be on `main`.
> +
> +**Test without building:**
> +```bash
> +./contrib/fedora/01-prep_build --check vNN
> +```
> +
> +**After this script:** Review the spec in dist-git before proceeding:
> +```bash
> +grep -E "^Version|^Release" $DISTGIT/ndctl.spec
> +head -30 $DISTGIT/ndctl.spec
> +```
> +If something looks wrong, restore the original:
> +```bash
> +cp $DISTGIT/ndctl.spec.orig $DISTGIT/ndctl.spec
> +```
> +
> +---
> +
> +### 02-do_build_local_checks \<version\>
> +
> +**Purpose:** Sanity-check that dist-git is in the right state before committing
> +anything — confirms `ndctl.spec` was modified and `sources`/`.gitignore` were
> +staged by `fedpkg new-sources`.
> +
> +**Run from:** upstream repo root
> +
> +**Test without building:**
> +```bash
> +./contrib/fedora/02-do_build_local_checks --check vNN
> +```
> +
> +If this script passes, dist-git is ready to commit and push.  If it fails,
> +something went wrong in script 01 — do not proceed.
> +
> +---
> +
> +### 03-do_build_push \<version\>
> +
> +**Purpose:** Commit the updated spec/sources in dist-git and push to `main`.
> +This is the single push that makes the new version available for all stable
> +branches to copy from.
> +
> +**Run from:** upstream repo root
> +
> +**Test without building:**
> +```bash
> +./contrib/fedora/03-do_build_push --check vNN
> +```
> +
> +**Important:** This pushes to dist-git `main` only.  Script 04 handles the
> +stable branches.  Pushing to `main` first is critical — script 04 copies the
> +release files *from main*, so if this push somehow lands on a stable branch
> +instead, the other branches won't get the new version.  See "Script ran on
> +wrong branch" in Common Problems.
> +
> +---
> +
> +### 04-do_build_branches \<version\>
> +
> +**Purpose:** Run a scratch build as a gate, then fire off real Koji builds for
> +main (rawhide) and each stable branch.  The builds run **in parallel** in Koji;
> +the script prints their URLs and exits without waiting.
> +
> +**Run from:** upstream repo root
> +
> +**Test without building (also prints branch list):**
> +```bash
> +./contrib/fedora/04-do_build_branches --check vNN
> +```
> +
> +**Normal usage:**
> +```bash
> +./contrib/fedora/04-do_build_branches vNN
> +```
> +
> +**Single branch (retry a failed branch; skips scratch and main):**
> +```bash
> +br=f44 ./contrib/fedora/04-do_build_branches vNN
> +```
> +
> +**Build more stable branches** (check https://endoflife.date/fedora for the
> +current stable set; 3 is only needed in the transition window when a new
> +Fedora is out but the oldest release hasn't gone EOL yet):
> +```bash
> +num_stable=3 ./contrib/fedora/04-do_build_branches vNN
> +```
> +
> +**What it does:**
> +1. Scratch build on `main` — the gate; this one WAITS for the result.
> +   Nothing is pushed to any stable branch until it passes.
> +2. Fires the real `main` (rawhide) build with `--nowait`
> +3. For each stable branch: copies `ndctl.spec`, `.gitignore`, `sources` from
> +   `main`, commits, pushes, and fires the build with `--nowait`
> +4. Prints all Koji task URLs and exits
> +
> +**Environment variables:**
> +
> +| Variable | Default | Purpose |
> +|---|---|---|
> +| `br` | (unset) | Build only this one branch |
> +| `num_stable` | `2` | How many stable branches to build |
> +
> +**After this script:** watch the builds at the printed Koji URLs.  When all
> +show green (state `complete`), run script 05.
> +
> +---
> +
> +### 05-do_submit_updates \<version\>
> +
> +**Purpose:** Verify the Koji builds completed, then submit the bodhi updates
> +for the stable branches.  Bodhi is Fedora's update gating system — submitting
> +moves the build into `updates-testing` where users can install and karma-test
> +it before it reaches the stable repo.  A Koji build alone does not reach users.
> +
> +**Run from:** upstream repo root
> +
> +**Test without submitting (shows the NVRs it would verify):**
> +```bash
> +./contrib/fedora/05-do_submit_updates --check vNN
> +```
> +
> +**Normal usage (after all builds are green):**
> +```bash
> +./contrib/fedora/05-do_submit_updates vNN
> +```
> +
> +**What it does:**
> +1. Checks every expected build (e.g. `ndctl-85-1.fc44`) is `COMPLETE` in Koji.
> +   If any is not ready, it lists them and exits without submitting anything —
> +   wait and re-run.
> +2. Submits a bodhi update for each stable branch.
> +   Rawhide/main is skipped — bodhi creates its update automatically.
> +
> +**Bodhi update type:** a version with a dot (e.g. `vNN.M`) is submitted as a
> +`bugfix`; otherwise `enhancement`.
> +
> +**Environment variables:**
> +
> +| Variable | Default | Purpose |
> +|---|---|---|
> +| `br` | (unset) | Submit for only this one branch |
> +| `num_stable` | `2` | How many stable branches (match what 04 used) |
> +| `bz` | (unset) | Bugzilla number to attach to the updates |
> +| `rel` | `1` | RPM Release number, if the build wasn't `-1` |
> +
> +---
> +
> +### 06-cleanup
> +
> +**Purpose:** Remove temporary files from both the upstream repo and dist-git.
> +
> +**What it removes:**
> +- `ndctl.spec.src` (upstream repo) — extracted upstream spec from script 01
> +- `$DISTGIT/ndctl.spec.orig` — backup of the previous Fedora spec
> +- `$DISTGIT/*.src.rpm` — local source RPMs from mock builds
> +- `$DISTGIT/results_ndctl/` — mock build output
> +- `$DISTGIT/copr-out/` — COPR build output
> +
> +`ndctl.spec` itself is NOT removed — it is the live Fedora spec tracked in
> +dist-git.
> +
> +---
> +
> +## Local vs Scratch Builds
> +
> +Script 02 has `fedpkg mockbuild` and `fedpkg lint` commented out.  Local mock
> +builds are fragile — they depend on your local environment, mock group
> +membership, and locally installed Fedora build root configs, all of which are a
> +pain to maintain for a quarterly workflow.
> +
> +Instead, script 04 runs a **scratch build** in Koji as its first step.  A
> +scratch build runs in Fedora's actual build infrastructure using the exact same
> +environment as a real build.  If it passes, the spec is good.  This is more
> +reliable than a local mock build and requires zero local setup.
> +
> +The scratch build is intentionally the first thing script 04 does.  If it
> +fails, nothing has been pushed to any stable branch yet, so recovery is clean.
> +
> +---
> +
> +## Checking Build and Update Status
> +
> +**Koji builds:**
> +```
> +https://koji.fedoraproject.org/koji/packageinfo?packageID=ndctl
> +```
> +
> +**Bodhi updates:**
> +```
> +https://bodhi.fedoraproject.org/updates/?packages=ndctl
> +```
> +
> +---
> +
> +## Common Problems
> +
> +### Scratch build failed
> +
> +The scratch build in script 04 runs before any stable branch work.  If it
> +fails, the script aborts and nothing has been pushed to any stable branch yet —
> +recovery is clean.
> +
> +Common causes that local checks won't catch:
> +- Missing or incorrect `BuildRequires` in the spec
> +- Upstream source tarball has a different directory layout than the spec expects
> +- A patch no longer applies cleanly against the new version
> +- A macro or path referenced in the spec no longer exists in the new sources
> +
> +To recover:
> +1. Check the Koji task URL printed in the output to see the build log
> +2. Fix `$DISTGIT/ndctl.spec`
> +3. Commit and push the fix:
> +   ```bash
> +   git -C $DISTGIT add ndctl.spec
> +   git -C $DISTGIT commit -m "fix spec for vNN"
> +   (cd $DISTGIT && fedpkg push)
> +   ```
> +4. Re-run script 04 from the top — it will run a new scratch build first
> +
> +---
> +
> +### "Build already exists" error in Koji
> +
> +Koji refuses to build an NVR that was already built.  The branch's spec is
> +still at the old version — usually meaning the release files never made it to
> +that branch.  Since script 04 copies the files from `main` itself, this
> +normally only happens if the push to `main` (script 03) landed somewhere else.
> +Check the branch and main:
> +```bash
> +git -C $DISTGIT log --oneline main | head -3     # should show "release vNN"
> +git -C $DISTGIT log --oneline <branch> | head -3
> +```
> +If `main` is missing the release commit, see "Script ran on the wrong branch"
> +below.  Once main is correct, retry the branch:
> +`br=<branch> ./contrib/fedora/04-do_build_branches vNN`
> +
> +---
> +
> +### A stable branch has diverged from `main`
> +
> +This happens when commits land on a branch independently of main — e.g. a
> +Fedora mass rebuild, or a past manual fix-up.  It is expected and harmless:
> +script 04 copies the release files from main rather than merging, precisely so
> +that diverged branches work the same as clean ones.  No action needed.
> +
> +---
> +
> +### Bodhi update not submitted for a branch
> +
> +Retry just that branch (script 05 verifies the build then submits):
> +```bash
> +br=<branch> ./contrib/fedora/05-do_submit_updates vNN
> +```
> +Or fully manually:
> +```bash
> +git -C $DISTGIT checkout <branch>
> +cd $DISTGIT && fedpkg update --request=testing --type=enhancement --notes=release-vNN
> +```
> +
> +---
> +
> +### An unexpected rawhide update appeared in bodhi
> +
> +Not a problem.  Bodhi automatically creates an update for rawhide when the
> +main build completes — nobody submits it, and script 05 deliberately skips
> +rawhide for this reason.
> +
> +---
> +
> +### Script ran on the wrong branch
> +
> +If scripts 01-03 ran while dist-git was on a stable branch (e.g. `f43`) instead
> +of `main`, the new version commit landed on that branch only.  `main` still has
> +the old version, and script 04 copies from `main`, so it would propagate the
> +old version.
> +
> +To fix: copy the files from the branch where the commit landed onto `main`:
> +```bash
> +git -C $DISTGIT checkout main
> +git -C $DISTGIT checkout f43 -- ndctl.spec .gitignore sources
> +git -C $DISTGIT add ndctl.spec .gitignore sources
> +git -C $DISTGIT commit -m "release vNN"
> +git -C $DISTGIT push
> +```
> +Then proceed normally with script 04.
> diff --git a/contrib/fedora/common.sh b/contrib/fedora/common.sh
> new file mode 100644
> index 0000000..6a4705e
> --- /dev/null
> +++ b/contrib/fedora/common.sh
> @@ -0,0 +1,82 @@
> +# contrib/fedora/common.sh
> +#
> +# Shared functions and setup sourced by all ndctl Fedora packaging scripts.
> +# Not meant to be run directly.
> +#
> +
> +# Locate the directory containing these scripts (contrib/fedora/ in the
> +# upstream clone), regardless of where the caller's working directory is.
> +SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
> +
> +# Load local config (DISTGIT path etc.)
> +# If config.local is missing, print a clear setup message and exit.
> +load_config()
> +{
> +	if [[ ! -f "$SCRIPTS_DIR/config.local" ]]; then
> +		printf "ERROR: missing %s/config.local\n" "$SCRIPTS_DIR"
> +		printf "One-time setup: copy the example and fill in your paths:\n"
> +		printf "  cp %s/config.local.example %s/config.local\n" "$SCRIPTS_DIR" "$SCRIPTS_DIR"
> +		exit 1
> +	fi
> +	# shellcheck source=config.local.example
> +	source "$SCRIPTS_DIR/config.local"
> +
> +	if [[ ! -d "$DISTGIT" ]]; then
> +		printf "ERROR: DISTGIT directory not found: %s\n" "$DISTGIT"
> +		printf "Check the DISTGIT path in %s/config.local\n" "$SCRIPTS_DIR"
> +		exit 1
> +	fi
> +}
> +
> +# verify_upstream_repo: confirm we are running from inside the upstream repo.
> +# All scripts run from the upstream clone (not the dist-git clone).
> +#
> +# NOTE: The upstream repo's *branch* does not matter -- script 01 downloads
> +# the release tarball by tag from GitHub and never reads the local upstream
> +# tree.  The branch that matters is dist-git being on 'main', which is
> +# checked separately by verify_distgit_branch.
> +verify_upstream_repo()
> +{
> +	local upstream_dir
> +	upstream_dir="$(git -C "$SCRIPTS_DIR" rev-parse --show-toplevel 2>/dev/null)"
> +
> +	# Make sure the caller's cwd is inside the upstream repo
> +	local cwd_toplevel
> +	cwd_toplevel="$(git rev-parse --show-toplevel 2>/dev/null)"
> +
> +	if [[ "$cwd_toplevel" != "$upstream_dir" ]]; then
> +		printf "ERROR: must run from inside the upstream ndctl repo\n"
> +		printf "  Expected: %s\n" "$upstream_dir"
> +		printf "  Got:      %s\n" "$cwd_toplevel"
> +		exit 1
> +	fi
> +}
> +
> +# verify_distgit_branch: confirm the dist-git clone is on the expected branch.
> +verify_distgit_branch()
> +{
> +	local expected_branch="$1"
> +	local current_branch
> +	current_branch="$(git -C "$DISTGIT" rev-parse --abbrev-ref HEAD)"
> +	if [[ "$current_branch" != "$expected_branch" ]]; then
> +		printf "ERROR: dist-git repo must be on '%s' branch, currently on '%s'\n" \
> +			"$expected_branch" "$current_branch"
> +		printf "Run: git -C %s checkout %s\n" "$DISTGIT" "$expected_branch"
> +		exit 1
> +	fi
> +}
> +
> +# preflight_check: run all pre-flight checks and print a summary.
> +# Call this with --check to test the setup without doing any real work.
> +# Each script calls this at the start.
> +preflight_check()
> +{
> +	local ver="$1"
> +	printf "=== Pre-flight checks ===\n"
> +	printf "  Scripts dir:  %s\n" "$SCRIPTS_DIR"
> +	printf "  Dist-git dir: %s\n" "$DISTGIT"
> +	printf "  Version:      %s\n" "$ver"
> +	printf "  Upstream branch: %s\n" "$(git rev-parse --abbrev-ref HEAD)"
> +	printf "  Dist-git branch: %s\n" "$(git -C "$DISTGIT" rev-parse --abbrev-ref HEAD)"
> +	printf "=========================\n"
> +}
> diff --git a/contrib/fedora/config.local.example b/contrib/fedora/config.local.example
> new file mode 100644
> index 0000000..0bb38da
> --- /dev/null
> +++ b/contrib/fedora/config.local.example
> @@ -0,0 +1,12 @@
> +# contrib/fedora/config.local
> +#
> +# Local machine configuration for the ndctl Fedora packaging scripts.
> +# This file is NOT committed to git (see .gitattributes).
> +#
> +# SETUP: Copy this file to config.local and fill in your paths.
> +#   cp config.local.example config.local
> +#
> +
> +# DISTGIT: path to your local clone of the Fedora dist-git repo
> +# This is the repo you cloned with 'fedpkg clone ndctl'
> +DISTGIT=~/fedora-pkgs/ndctl