Merge remote-tracking branch 'origin/develop' into a1q123456/split-loan-set-and-loan-accept-implementation

This commit is contained in:
JCW
2026-08-18 17:46:19 +01:00
14 changed files with 343 additions and 97 deletions

View File

@@ -250,6 +250,8 @@ words:
- Raphson
- rcflags
- replayer
- repodata
- repomd
- rerandomize
- rerandomization
- rerandomized
@@ -290,6 +292,7 @@ words:
- sles
- soci
- socidb
- Sonatype
- sponsee
- sponsees
- SRPMS

View File

@@ -1,44 +0,0 @@
name: Generate build version number
description: "Generate build version number."
outputs:
version:
description: "The generated build version number."
value: ${{ steps.version.outputs.version }}
runs:
using: composite
steps:
# When a tag is pushed, the version is used as-is.
- name: Generate version for tag event
if: ${{ startsWith(github.ref, 'refs/tags/') }}
shell: bash
env:
VERSION: ${{ github.ref_name }}
run: echo "VERSION=${VERSION}" >>"${GITHUB_ENV}"
# When a tag is not pushed, then the version (e.g. 1.2.3-b0) is extracted
# from the BuildInfo.cpp file and the shortened commit hash appended to it.
# We use a plus sign instead of a hyphen because Conan recipe versions do
# not support two hyphens.
- name: Generate version for non-tag event
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
shell: bash
run: |
echo 'Extracting version from BuildInfo.cpp.'
VERSION="$(cat src/libxrpl/protocol/BuildInfo.cpp | grep "versionString =" | awk -F '"' '{print $2}')"
if [[ -z "${VERSION}" ]]; then
echo 'Unable to extract version from BuildInfo.cpp.'
exit 1
fi
echo 'Appending shortened commit hash to version.'
SHA='${{ github.sha }}'
VERSION="${VERSION}+${SHA:0:7}"
echo "VERSION=${VERSION}" >>"${GITHUB_ENV}"
- name: Output version
id: version
shell: bash
run: echo "version=${VERSION}" >>"${GITHUB_OUTPUT}"

90
.github/actions/release-info/action.yml vendored Normal file
View File

@@ -0,0 +1,90 @@
name: Release info
description: "Derive the version, release channel and package release number for this build."
outputs:
version:
description: "The build version number."
value: ${{ steps.version.outputs.version }}
channel:
description: "The release channel this build belongs to."
value: ${{ steps.channel.outputs.channel }}
pkg_release:
description: "The package release number: 1 for a tag, the run number otherwise."
value: ${{ steps.pkg_release.outputs.pkg_release }}
runs:
using: composite
steps:
# A tag names its own version. Anything else takes it from BuildInfo.cpp and
# appends the commit hash as build metadata, joined with a plus sign because a
# Conan version cannot contain two hyphens.
- name: Determine version
id: version
shell: bash
env:
IS_TAG: ${{ startsWith(github.ref, 'refs/tags/') }}
REF_NAME: ${{ github.ref_name }}
SHA: ${{ github.sha }}
run: |
if [[ "${IS_TAG}" == "true" ]]; then
version="${REF_NAME}"
else
version="$(awk -F'"' '/versionString =/ { print $2 }' src/libxrpl/protocol/BuildInfo.cpp)"
if [[ -z "${version}" ]]; then
echo "Unable to read versionString from BuildInfo.cpp." >&2
exit 1
fi
version="${version}+${SHA:0:7}"
fi
echo "version=${version}" | tee -a "${GITHUB_OUTPUT}"
# Only a tag says how mature a build is: a push is a develop build whatever
# its version, and a non-public codebase keeps its packages to itself.
- name: Determine release channel
id: channel
shell: bash
env:
IS_TAG: ${{ startsWith(github.ref, 'refs/tags/') }}
REF_NAME: ${{ github.ref_name }}
VISIBILITY: ${{ github.event.repository.visibility }}
run: |
pre_release=""
if [[ "${REF_NAME}" == *-* ]]; then
pre_release="${REF_NAME#*-}"
fi
if [[ "${VISIBILITY}" != "public" ]]; then
channel=private
elif [[ "${IS_TAG}" != "true" ]]; then
channel=develop
elif [[ -z "${pre_release}" ]]; then
channel=stable
elif [[ "${pre_release}" =~ ^rc[0-9]+(\+.*)?$ ]]; then
channel=unstable
elif [[ "${pre_release}" =~ ^b(0|[1-9][0-9]*)(\+.*)?$ ]]; then
channel=experimental
else
echo "Unsupported pre-release in tag '${REF_NAME}'. Use bN or rcN." >&2
exit 1
fi
echo "channel=${channel}" | tee -a "${GITHUB_OUTPUT}"
# A tag is packaged once, so its release number is fixed at 1. Develop builds
# repeat the same version, so the run number is what makes each push an
# upgrade rather than a reinstall.
- name: Determine package release
id: pkg_release
shell: bash
env:
IS_TAG: ${{ startsWith(github.ref, 'refs/tags/') }}
RUN_NUMBER: ${{ github.run_number }}
run: |
if [[ "${IS_TAG}" == "true" ]]; then
pkg_release=1
else
pkg_release="${RUN_NUMBER}"
fi
echo "pkg_release=${pkg_release}" | tee -a "${GITHUB_OUTPUT}"

View File

@@ -4,7 +4,7 @@ updates:
directories:
- /
- .github/actions/build-deps/
- .github/actions/generate-version/
- .github/actions/release-info/
- .github/actions/set-compiler-env/
- .github/actions/setup-conan/
schedule:

View File

@@ -92,7 +92,7 @@
"build_type": ["Release"],
"arch": ["amd64"],
"minimal": false,
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-577d745"
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-028ccea"
}
],
@@ -102,7 +102,7 @@
"build_type": ["Release"],
"arch": ["amd64"],
"minimal": false,
"image": "ghcr.io/xrplf/xrpld/packaging-rhel:sha-577d745"
"image": "ghcr.io/xrplf/xrpld/packaging-rhel:sha-028ccea"
}
]
}

View File

@@ -77,7 +77,7 @@ jobs:
# Keep the paths below in sync with those in `on-trigger.yml`.
.github/actions/build-deps/**
.github/actions/generate-version/**
.github/actions/release-info/**
.github/actions/setup-conan/**
.github/scripts/strategy-matrix/**
.github/workflows/reusable-build-test-config.yml

View File

@@ -1,5 +1,9 @@
# This workflow uploads the libxrpl recipe to the Conan remote and builds
# release packages when a versioned tag is pushed.
# When a versioned tag is pushed, this workflow:
#
# - uploads the libxrpl recipe to the Conan remote
# - builds and tests the release binaries
# - builds the DEB and RPM packages
# - publishes those packages to the XRPLF package repositories
name: Tag
on:
@@ -24,7 +28,7 @@ jobs:
remote_password: ${{ secrets.NEXUS_REMOTE_PASSWORD }}
build-test:
if: ${{ github.repository == 'XRPLF/rippled' }}
if: ${{ github.repository_owner == 'XRPLF' }}
uses: ./.github/workflows/reusable-build-test.yml
strategy:
fail-fast: true
@@ -37,6 +41,11 @@ jobs:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
package:
if: ${{ github.repository == 'XRPLF/rippled' }}
if: ${{ github.repository_owner == 'XRPLF' }}
needs: build-test
uses: ./.github/workflows/reusable-package.yml
with:
publish: true
secrets:
remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
remote_password: ${{ secrets.NEXUS_REMOTE_PASSWORD }}

View File

@@ -15,7 +15,7 @@ on:
# Keep the paths below in sync with those in `on-pr.yml`.
- ".github/actions/build-deps/**"
- ".github/actions/generate-version/**"
- ".github/actions/release-info/**"
- ".github/actions/setup-conan/**"
- ".github/scripts/strategy-matrix/**"
- ".github/workflows/reusable-build-test-config.yml"
@@ -108,3 +108,10 @@ jobs:
package:
needs: build-test
uses: ./.github/workflows/reusable-package.yml
with:
# Packages are built on every trigger; only develop pushes in XRPLF/rippled
# publish them, matching upload-recipe above.
publish: ${{ github.repository == 'XRPLF/rippled' && github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
secrets:
remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
remote_password: ${{ secrets.NEXUS_REMOTE_PASSWORD }}

View File

@@ -111,6 +111,9 @@ jobs:
VOIDSTAR_ENABLED: ${{ contains(inputs.cmake_args, '-Dvoidstar=ON') }}
VALIDATOR_KEYS_ENABLED: ${{ contains(inputs.cmake_args, '-Dvalidator_keys=ON') }}
SANITIZERS_ENABLED: ${{ inputs.sanitizers != '' }}
# The binaries reusable-package.yml consumes. A private repository skips
# them except on a tag push, which is what produces its release packages.
PACKAGING_ARTIFACTS_ENABLED: ${{ github.event.repository.visibility == 'public' || startsWith(github.ref, 'refs/tags/') }}
steps:
- name: Cleanup workspace (macOS and Windows)
if: ${{ runner.os == 'macOS' || runner.os == 'Windows' }}
@@ -222,7 +225,7 @@ jobs:
fi
- name: Upload the binary (Linux)
if: ${{ github.event.repository.visibility == 'public' && runner.os == 'Linux' }}
if: ${{ env.PACKAGING_ARTIFACTS_ENABLED == 'true' && runner.os == 'Linux' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: xrpld-${{ inputs.config_name }}
@@ -236,7 +239,7 @@ jobs:
run: ./validator-keys --unittest
- name: Upload the validator-keys binary
if: ${{ github.event.repository.visibility == 'public' && env.VALIDATOR_KEYS_ENABLED == 'true' }}
if: ${{ env.PACKAGING_ARTIFACTS_ENABLED == 'true' && env.VALIDATOR_KEYS_ENABLED == 'true' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: validator-keys-${{ inputs.config_name }}

View File

@@ -1,17 +1,34 @@
# Build Linux packages (DEB and RPM) from pre-built binary artifacts (xrpld and
# validator-keys). Discovers which configurations to package from linux.json
# (configs in "package_configs") and fans out one job per distro. Only
# linux/amd64 is supported; the runner is hardcoded in the job below.
# Build Linux packages from the pre-built xrpld and validator-keys artifacts:
#
# - one job per distro, taken from "package_configs" in linux.json
# - each job runs in that distro's container, which is what decides DEB or RPM
# - with 'publish: true' a job also uploads what it built
# (see package/publish_pkg.sh)
#
# Only linux/amd64 is supported; the runner is hardcoded in the job below.
name: Package
on:
workflow_call:
inputs:
pkg_release:
description: "Package release number. Increment when repackaging the same executable."
publish:
description: "Whether to publish the packages after building them."
required: false
type: boolean
default: false
nexus_url:
description: "The base URL of the Nexus instance hosting the deb and rpm repositories."
required: false
type: string
default: "1"
default: https://packages.xrplf.org
secrets:
remote_username:
description: "The username of a Nexus account with write access to the repositories."
required: false
remote_password:
description: "The password or token for that Nexus account."
required: false
defaults:
run:
@@ -41,7 +58,7 @@ jobs:
package:
needs: [generate-matrix]
if: ${{ github.event.repository.visibility == 'public' }}
if: ${{ github.event.repository.visibility == 'public' || startsWith(github.ref, 'refs/tags/') }}
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
@@ -71,9 +88,14 @@ jobs:
- name: Make binaries executable
run: chmod +x "${BUILD_DIR}/xrpld" "${BUILD_DIR}/validator-keys"
- name: Determine release info
id: release_info
uses: ./.github/actions/release-info
- name: Build package
env:
PKG_RELEASE: ${{ inputs.pkg_release }}
PKG_RELEASE: ${{ steps.release_info.outputs.pkg_release }}
PKG_CHANNEL: ${{ steps.release_info.outputs.channel }}
run: ./package/build_pkg.sh
- name: Upload package artifact
@@ -85,3 +107,12 @@ jobs:
${{ env.BUILD_DIR }}/debbuild/*.ddeb
${{ env.BUILD_DIR }}/rpmbuild/RPMS/**/*.rpm
if-no-files-found: error
- name: Publish package
if: ${{ inputs.publish }}
env:
CHANNEL: ${{ steps.release_info.outputs.channel }}
NEXUS_URL: ${{ inputs.nexus_url }}
NEXUS_USERNAME: ${{ secrets.remote_username }}
NEXUS_PASSWORD: ${{ secrets.remote_password }}
run: ./package/publish_pkg.sh "${CHANNEL}" "${BUILD_DIR}"

View File

@@ -49,9 +49,9 @@ jobs:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Generate build version number
id: version
uses: ./.github/actions/generate-version
- name: Determine release info
id: release_info
uses: ./.github/actions/release-info
- name: Set up Conan
uses: ./.github/actions/setup-conan
@@ -64,8 +64,8 @@ jobs:
- name: Upload Conan recipe (version)
run: |
conan export . --version=${{ steps.version.outputs.version }}
conan upload --confirm --check --remote="${REMOTE_NAME}" xrpl/${{ steps.version.outputs.version }}
conan export . --version=${{ steps.release_info.outputs.version }}
conan upload --confirm --check --remote="${REMOTE_NAME}" xrpl/${{ steps.release_info.outputs.version }}
# When this workflow is triggered by a push event, it will always be when merging into the
# 'develop' branch, see on-trigger.yml.
@@ -92,4 +92,4 @@ jobs:
conan upload --confirm --check --remote="${REMOTE_NAME}" xrpl/release
outputs:
ref: xrpl/${{ steps.version.outputs.version }}
ref: xrpl/${{ steps.release_info.outputs.version }}

View File

@@ -8,7 +8,8 @@ a build configured with `-Dvalidator_keys=ON`.
```
package/
build_pkg.sh Staging and build script (called by the CMake `package` target and CI)
build_pkg.sh Staging and build script (called by the CMake `package` target and CI)
publish_pkg.sh Uploads built packages to the XRPLF Nexus repositories (called by CI)
rpm/
xrpld.spec RPM spec
debian/ Debian control files (control, rules, copyright, xrpld.docs, xrpld.links, source/format)
@@ -87,7 +88,7 @@ docker run --rm \
./package/build_pkg.sh --pkg-release "${PKG_RELEASE}"
# Output:
# build/debbuild/*.deb (DEB + dbgsym .ddeb)
# build/debbuild/*.deb (DEB + dbgsym; Debian names both .deb)
# build/rpmbuild/RPMS/x86_64/*.rpm
```
@@ -120,6 +121,50 @@ The package version is not a CMake input on this path: `build_pkg.sh` derives it
from the just-built `xrpld` binary's `xrpld --version` output. The package
release defaults to 1 and is overridable with `-Dpkg_release=N`.
## Publishing packages
Packages are published to the XRPLF repositories on Sonatype Nexus at
`https://packages.xrplf.org`. The `release-info` action decides the channel from
the event, and `publish_pkg.sh` maps that channel to a repository pair:
| Event | Version | Channel | DEB repository | RPM repository |
| ------------------------ | ----------------- | -------------- | ------------------ | ------------------ |
| tag | `X.Y.Z` | `stable` | `deb-stable` | `rpm-stable` |
| tag | `X.Y.Z-rcN` | `unstable` | `deb-unstable` | `rpm-unstable` |
| tag | `X.Y.Z-bN` | `experimental` | `deb-experimental` | `rpm-experimental` |
| push to `develop` | `xrpld --version` | `develop` | `deb-develop` | `rpm-develop` |
| tag, non-public codebase | _any_ | `private` | `deb-private` | `rpm-private` |
Only a tag names a channel — do not extend that to `develop`, where
`BuildInfo.cpp`'s `versionString` moves through `-bN`, `-rcN` and even the final
version during a release cycle, which would send develop builds into `stable`.
Versions sort in row order, so moving to a more mature channel never downgrades.
The action decides the package release number on the same split: a tag's version
is unique, so its packages are release 1, while develop repeats the same version
and takes `github.run_number` so each push supersedes the last. Both reach the
packaging scripts as arguments, so neither script derives anything itself.
Publishing is the last step of each packaging job, uploading from the container
that built the packages. It runs when the caller passes `publish: true`:
`on-trigger.yml` for develop pushes in `XRPLF/rippled`, `on-tag.yml` for tags in
any `XRPLF` repository, `on-pr.yml` never. Both authenticate with the
`NEXUS_REMOTE_USERNAME` / `NEXUS_REMOTE_PASSWORD` secrets already used for the
Conan remote.
Nexus owns the repository metadata; nothing here signs or indexes anything. Worth
knowing:
- Each apt-hosted repository needs a distribution and a PGP signing keypair
configured in Nexus, which rejects one created without a keypair.
- yum metadata is rebuilt asynchronously, so a successful publish is not
immediately installable.
- Each job uploads only what it built, and uploads are not transactional, so a
failure can leave one format published alone. Re-running is safe: both the apt
POST and the yum PUT replace an existing asset.
- The `develop` repositories gain a package per push, so they need a cleanup
policy to stay bounded; tagged channels publish each version once.
## How `build_pkg.sh` works
`build_pkg.sh` derives the `xrpld` software version from
@@ -151,10 +196,9 @@ With `PKG_RELEASE=1`, the package metadata becomes:
| `3.2.0-b1` | `3.2.0~b1-1%{?dist}` | `3.2.0~b1-1` |
| `3.2.0-rc1` | `3.2.0~rc1-1%{?dist}` | `3.2.0~rc1-1` |
The Debian changelog entry carries the repository component: final releases use
`stable`, `b0` builds, including `b0+metadata`, use `develop`, and `bN`/`rcN`
pre-releases use `unstable`.
Build metadata on a final release, such as `3.2.0+abc123`, is rejected.
The Debian changelog entry carries the channel passed as `--channel`
(`PKG_CHANNEL`), defaulting to `unstable`. An unsupported pre-release, and build
metadata on a final release such as `3.2.0+abc123`, are both rejected.
The RPM path intentionally uses `~` in `Version`, matching the Debian
pre-release ordering convention, so RPM filenames/NVRs begin with forms like
@@ -209,17 +253,20 @@ service restart.
5. Generates a minimal `debian/changelog` using `${pkg_version}-${PKG_RELEASE}`,
where `pkg_version` is derived from the binary-reported `xrpld` version.
6. Runs `dpkg-buildpackage -b --no-sign -d` (`-d` skips the build-dependency check, since the binary is already built). `debian/rules` uses manual `install` commands.
7. Output: `debbuild/*.deb` and `debbuild/*.ddeb` (dbgsym package)
7. Output: `debbuild/*.deb`, the binary package and the `-dbgsym` package.
Debian gives dbgsym packages a `.deb` extension; only Ubuntu uses `.ddeb`.
## Post-build verification
```bash
# DEB
dpkg-deb -c debbuild/*.deb | grep -E 'systemd|sysusers|tmpfiles'
lintian -I debbuild/*.deb
# RPM
rpm -qlp rpmbuild/RPMS/x86_64/*.rpm
# Optional, and not in the packaging image: apt-get install -y lintian
lintian -I debbuild/*.deb
```
## Reproducibility

View File

@@ -16,6 +16,8 @@ Options (each can also be set via the env var shown):
xrpld and validator-keys
binaries [BUILD_DIR; default: ${PWD}/build]
--pkg-release N package release iteration [PKG_RELEASE; default: 1]
--channel NAME release channel, written
to debian/changelog [PKG_CHANNEL; default: unstable]
--source-date-epoch SECS reproducibility timestamp [SOURCE_DATE_EPOCH; latest git ctime; fallback: current time]
-h, --help show this help and exit
EOF
@@ -32,6 +34,7 @@ need_arg() {
SRC_DIR="${SRC_DIR:-}"
BUILD_DIR="${BUILD_DIR:-}"
PKG_RELEASE="${PKG_RELEASE:-1}"
PKG_CHANNEL="${PKG_CHANNEL:-unstable}"
SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-}"
while [[ $# -gt 0 ]]; do
@@ -51,6 +54,11 @@ while [[ $# -gt 0 ]]; do
PKG_RELEASE="$2"
shift 2
;;
--channel)
need_arg "$@"
PKG_CHANNEL="$2"
shift 2
;;
--source-date-epoch)
need_arg "$@"
SOURCE_DATE_EPOCH="$2"
@@ -198,7 +206,6 @@ stage_common() {
build_rpm() {
local topdir="${BUILD_DIR}/rpmbuild"
rm -rf "${topdir}"
mkdir -p "${topdir}"/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
cp "${SRC_DIR}/package/rpm/xrpld.spec" "${topdir}/SPECS/xrpld.spec"
@@ -214,7 +221,6 @@ build_rpm() {
build_deb() {
local staging="${BUILD_DIR}/debbuild/source"
rm -rf "${staging}"
mkdir -p "${staging}"
stage_common "${staging}"
@@ -225,25 +231,9 @@ build_deb() {
cp "${staging}/xrpld.tmpfiles" "${staging}/debian/xrpld.tmpfiles"
cp "${staging}/xrpld.logrotate" "${staging}/debian/xrpld.logrotate"
# Choose the Debian repository component for this package.
# 3.2.0 -> stable, *-b0[+metadata] -> develop,
# bN/rcN pre-releases -> unstable.
local deb_component
if [[ -z "${pre_release}" ]]; then
deb_component="stable"
elif [[ "${pre_release}" =~ ^b0(\+.*)?$ ]]; then
deb_component="develop"
elif [[ "${pre_release}" =~ ^(b[1-9][0-9]*|rc[0-9]+)(\+.*)?$ ]]; then
deb_component="unstable"
else
echo "build_pkg.sh: unsupported xrpld pre-release '${pre_release}'." >&2
echo "Use bN or rcN, e.g. 3.2.0-b1 or 3.2.0-rc2." >&2
exit 1
fi
# Debian version is <upstream>[~<pre>]-<pkg release>.
cat >"${staging}/debian/changelog" <<EOF
xrpld (${pkg_version}-${PKG_RELEASE}) ${deb_component}; urgency=medium
xrpld (${pkg_version}-${PKG_RELEASE}) ${PKG_CHANNEL}; urgency=medium
* Release ${xrpld_version}.
-- XRPL Foundation <contact@xrplf.org> ${CHANGELOG_DATE}
@@ -255,4 +245,8 @@ EOF
(cd "${staging}" && dpkg-buildpackage -b --no-sign -d)
}
# Remove both build directories, because a package left from an earlier build
# would otherwise be picked up and published alongside this one.
rm -rf "${BUILD_DIR}/debbuild" "${BUILD_DIR}/rpmbuild"
"build_${pkg_type}"

106
package/publish_pkg.sh Executable file
View File

@@ -0,0 +1,106 @@
#!/usr/bin/env bash
set -euo pipefail
# Publish the DEB and RPM packages built by build_pkg.sh to the XRPLF package
# repositories on Sonatype Nexus.
#
# Usage: publish_pkg.sh <channel> [package-dir]
#
# channel release channel, selecting the 'deb-<channel>' and
# 'rpm-<channel>' repository pair
# package-dir searched recursively for *.deb, *.ddeb and *.rpm ('build' by
# default)
#
# NEXUS_USERNAME and NEXUS_PASSWORD are required. NEXUS_URL overrides the target
# instance, and DRY_RUN=1 lists the uploads without performing them.
channel="${1:-}"
pkg_dir="${2:-build}"
nexus_url="${NEXUS_URL:-https://packages.xrplf.org}"
if [[ -z "${channel}" ]]; then
echo "usage: publish_pkg.sh <channel> [package-dir]" >&2
exit 2
fi
deb_repo="deb-${channel}"
rpm_repo="rpm-${channel}"
if [[ -z "${DRY_RUN:-}" ]]; then
: "${NEXUS_USERNAME:?is required}" "${NEXUS_PASSWORD:?is required}"
fi
# Deliberate curl choices:
#
# - no --fail, which would hide the response body where Nexus explains what it
# rejected
# - no --location, since curl downgrades a redirected POST to GET and turns an
# upload into a no-op that still answers 200
# - credentials on stdin, to keep them out of the process list
upload() {
local url="$1"
shift
[[ -z "${DRY_RUN:-}" ]] || return 0
local body code status=0
body="$(mktemp)"
code="$(
printf 'user = %s:%s\n' "${NEXUS_USERNAME}" "${NEXUS_PASSWORD}" |
curl \
--config - \
--silent \
--show-error \
--retry 3 \
--retry-delay 5 \
--retry-all-errors \
--output "${body}" \
--write-out '%{http_code}' \
"$@" \
"${url}"
)" || status=$?
if [[ ${status} -ne 0 || ! "${code}" =~ ^2[0-9][0-9]$ ]]; then
echo "publish_pkg.sh: upload failed (curl ${status}, HTTP ${code}): ${url}" >&2
cat "${body}" >&2
echo >&2
rm -f "${body}"
exit 1
fi
rm -f "${body}"
}
echo "Publishing ${pkg_dir} to ${deb_repo} and ${rpm_repo} on ${nexus_url}:"
count=0
while IFS= read -r -d '' file; do
name="${file##*/}"
case "${name}" in
# A raw body with a multipart Content-Type, POSTed to the repository root,
# is the documented upload for a hosted apt repository:
# https://help.sonatype.com/en/apt-repositories.html#deploying-packages-to-hosted-apt-repositories
*.deb | *.ddeb)
echo " ${name} -> ${deb_repo}"
upload "${nexus_url}/repository/${deb_repo}/" \
--header 'Content-Type: multipart/form-data' \
--data-binary "@${file}"
;;
# yum repositories are addressed by path; the arch comes from the name.
*.rpm)
arch="${name%.rpm}"
arch="${arch##*.}"
echo " ${name} -> ${rpm_repo}/${arch}"
upload "${nexus_url}/repository/${rpm_repo}/${arch}/${name}" \
--upload-file "${file}"
;;
esac
count=$((count + 1))
done < <(find "${pkg_dir}" -type f \( -name '*.deb' -o -name '*.ddeb' -o -name '*.rpm' \) -print0)
# Uploading nothing would otherwise look like a successful publish.
if [[ ${count} -eq 0 ]]; then
echo "publish_pkg.sh: no packages found in ${pkg_dir}." >&2
exit 1
fi
echo "${count} package(s) ${DRY_RUN:+would be }published."