blob: 7cb4a9204c04367e90255b5c97745edb9cd7fadc [file]
#!/usr/bin/env bash
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# End-to-end verification that the four published Rust crates can actually
# be uploaded to a crates.io-like registry. Spins up cargo-http-registry as
# a local alt-registry, then publishes iggy_binary_protocol, iggy_common,
# iggy and iggy-cli in topological order. Catches missing `version = "..."`
# on path deps, wrong publish order, and any manifest-level regression that
# only surfaces at `cargo publish` time (the exact class of bug that broke
# Apache Iggy 0.8.0 rc1).
#
# This script is invoked both by developers (locally) and by CI. It must
# leave the working tree untouched on exit, even on failure.
#
# Usage:
# scripts/verify-crates-publish.sh
#
# Dependencies (install manually, or re-run with AUTO_INSTALL=1):
# cargo-http-registry (cargo install cargo-http-registry --locked)
#
# Environment variables (all optional):
# REGISTRY_PORT Port for the local registry (default: 35503)
# CA_BUNDLE Path to the system CA bundle used by libgit2.
# Auto-detected on common Linux distros if unset.
# AUTO_INSTALL If "1", install cargo-http-registry (default: 0)
#
# Exit codes:
# 0 success, all four crates published to the local registry
# 1 tooling missing or preflight failure
# 2 publish chain failed on one of the four crates
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"
REGISTRY_PORT="${REGISTRY_PORT:-35503}"
REGISTRY_DIR="$(mktemp -d -t iggy-local-reg.XXXXXX)"
REGISTRY_LOG="$(mktemp -t iggy-local-reg-log.XXXXXX)"
# Byte-exact snapshot of Cargo.toml. Restored by cleanup() instead of
# `git checkout`, so the revert is independent of git state and cannot
# eat an in-flight edit if the preflight dirty check is ever bypassed.
CARGO_TOML_BACKUP="$(mktemp -t iggy-cargo-toml.XXXXXX)"
cp Cargo.toml "$CARGO_TOML_BACKUP"
AUTO_INSTALL="${AUTO_INSTALL:-0}"
# Ordered topologically: each crate depends only on the ones before it.
# MUST be lowercase (crates-index path is derived from the name directly).
CRATES=(
iggy_binary_protocol
iggy_common
iggy
iggy-cli
)
REGISTRY_PID=""
CARGO_CONFIG_CREATED=false
CARGO_TOML_PATCHED=false
log() { printf '>>> %s\n' "$*"; }
err() { printf '!!! %s\n' "$*" >&2; }
# Invoked via `trap ... EXIT INT TERM`; shellcheck can't see indirect calls.
# shellcheck disable=SC2329
cleanup() {
local ec=$?
set +e
trap - EXIT INT TERM
if [[ "$CARGO_TOML_PATCHED" == "true" ]]; then
log "Reverting Cargo.toml patch"
cp "$CARGO_TOML_BACKUP" Cargo.toml
rm -f Cargo.toml.bak
fi
rm -f "$CARGO_TOML_BACKUP"
if [[ "$CARGO_CONFIG_CREATED" == "true" ]]; then
log "Removing scratch .cargo/config.toml"
rm -f .cargo/config.toml
rmdir .cargo 2>/dev/null || true
fi
if [[ -n "$REGISTRY_PID" ]] && kill -0 "$REGISTRY_PID" 2>/dev/null; then
log "Stopping local registry (pid $REGISTRY_PID)"
kill -TERM "$REGISTRY_PID" 2>/dev/null || true
# Give it half a second then force
for _ in 1 2 3 4 5; do
kill -0 "$REGISTRY_PID" 2>/dev/null || break
sleep 0.1
done
kill -KILL "$REGISTRY_PID" 2>/dev/null || true
wait "$REGISTRY_PID" 2>/dev/null || true
fi
rm -rf "$REGISTRY_DIR" "$REGISTRY_LOG"
exit "$ec"
}
trap cleanup EXIT INT TERM
# ---------------------------------------------------------------------------
# 0. Preconditions
# ---------------------------------------------------------------------------
if ! command -v cargo >/dev/null 2>&1; then
err "cargo is not on PATH"
exit 1
fi
if ! command -v cargo-http-registry >/dev/null 2>&1; then
if [[ "$AUTO_INSTALL" == "1" ]]; then
log "Installing cargo-http-registry"
cargo install cargo-http-registry --locked
else
err "cargo-http-registry is not installed"
err "Install it with: cargo install cargo-http-registry --locked"
err "Or re-run this script with AUTO_INSTALL=1"
exit 1
fi
fi
# libgit2 inside cargo needs an explicit CA bundle on many Linux distros.
# Auto-detect common locations when the caller hasn't overridden it.
if [[ -z "${CA_BUNDLE:-}" ]]; then
for candidate in \
/etc/ssl/certs/ca-certificates.crt \
/etc/pki/tls/certs/ca-bundle.crt \
/etc/ssl/cert.pem \
/etc/ssl/ca-bundle.pem \
/opt/homebrew/etc/ca-certificates/cert.pem \
/usr/local/etc/ca-certificates/cert.pem
do
if [[ -r "$candidate" ]]; then
CA_BUNDLE="$candidate"
break
fi
done
fi
if [[ -z "${CA_BUNDLE:-}" || ! -r "$CA_BUNDLE" ]]; then
err "Could not locate a system CA bundle. Set CA_BUNDLE=/path/to/ca.crt"
exit 1
fi
log "Using CA bundle: $CA_BUNDLE"
if [[ -e .cargo/config.toml ]]; then
err ".cargo/config.toml already exists; refusing to overwrite"
err "Move it aside before running this script"
exit 1
fi
# Refuse to run on top of in-flight Cargo.toml edits. The script patches
# this file in step 3, and we don't want to mix caller edits with the
# scratch registry patch: a successful run would happily roll both
# forward, and a failed run would force the caller to disentangle them.
if ! git diff --quiet HEAD -- Cargo.toml; then
err "Cargo.toml has uncommitted changes; refusing to patch"
err "Commit or stash your changes before running this script"
exit 1
fi
# ---------------------------------------------------------------------------
# 0b. MSRV contract: the four crates we publish must advertise an MSRV on
# crates.io. The rust-msrv CI gate compile-checks against the floor, but
# it pins the toolchain regardless of the manifest, so it cannot notice
# the `rust-version` field silently disappearing. Assert it here, at the
# publish boundary, plus that every crate agrees on the value.
# ---------------------------------------------------------------------------
if ! command -v jq >/dev/null 2>&1; then
err "jq is required for the MSRV check but is not on PATH"
exit 1
fi
metadata="$(cargo metadata --format-version 1 --no-deps)"
msrv=""
for crate in "${CRATES[@]}"; do
crate_msrv="$(jq -r --arg c "$crate" \
'.packages[] | select(.name == $c) | .rust_version // ""' <<< "$metadata")"
if [[ -z "$crate_msrv" ]]; then
err "${crate} declares no rust-version; published MSRV would be unset"
exit 1
fi
if [[ -z "$msrv" ]]; then
msrv="$crate_msrv"
elif [[ "$crate_msrv" != "$msrv" ]]; then
err "MSRV mismatch: ${crate} pins ${crate_msrv}, expected ${msrv}"
exit 1
fi
done
log "Published crates declare MSRV ${msrv}"
# ---------------------------------------------------------------------------
# 1. Start the local registry
# ---------------------------------------------------------------------------
log "Starting cargo-http-registry at 127.0.0.1:${REGISTRY_PORT}"
cargo-http-registry --addr "127.0.0.1:${REGISTRY_PORT}" "$REGISTRY_DIR" \
> "$REGISTRY_LOG" 2>&1 &
REGISTRY_PID=$!
# cargo-http-registry speaks git-http; `git ls-remote` is the closest
# thing to a health check. A GET on any URL path returns 405 because the
# git-upload-pack endpoint expects POST, so don't probe with plain curl.
for attempt in $(seq 1 20); do
if git ls-remote "http://127.0.0.1:${REGISTRY_PORT}/git" HEAD >/dev/null 2>&1; then
log "Registry is up (pid $REGISTRY_PID)"
break
fi
if ! kill -0 "$REGISTRY_PID" 2>/dev/null; then
err "cargo-http-registry died before becoming ready"
cat "$REGISTRY_LOG" >&2 || true
exit 1
fi
sleep 0.5
if (( attempt == 20 )); then
err "Registry failed to become ready within 10s"
cat "$REGISTRY_LOG" >&2 || true
exit 1
fi
done
# ---------------------------------------------------------------------------
# 2. Write a local .cargo/config.toml pointing at the registry
# ---------------------------------------------------------------------------
log "Writing .cargo/config.toml"
mkdir -p .cargo
CARGO_CONFIG_CREATED=true
cat > .cargo/config.toml <<EOF
[registries.local-dev]
index = "http://127.0.0.1:${REGISTRY_PORT}/git"
[net]
git-fetch-with-cli = true
[http]
cainfo = "${CA_BUNDLE}"
EOF
# ---------------------------------------------------------------------------
# 3. Patch root Cargo.toml so path deps on the four iggy crates resolve
# against local-dev during `cargo publish`. The edit is reverted by the
# cleanup trap, whether the script succeeds or fails.
# ---------------------------------------------------------------------------
log "Patching workspace.dependencies to target local-dev"
CARGO_TOML_PATCHED=true
# Injects `, registry = "local-dev"` before the closing brace of the line.
# Match by crate name + path + version so we do not touch unrelated entries.
sed -i.bak -E \
-e 's|^(iggy_binary_protocol = \{ path = "[^"]+", version = "[^"]+")( \})$|\1, registry = "local-dev"\2|' \
-e 's|^(iggy_common = \{ path = "[^"]+", version = "[^"]+")( \})$|\1, registry = "local-dev"\2|' \
-e 's|^(iggy = \{ path = "[^"]+", version = "[^"]+")( \})$|\1, registry = "local-dev"\2|' \
-e 's|^(iggy-cli = \{ path = "[^"]+", version = "[^"]+")( \})$|\1, registry = "local-dev"\2|' \
Cargo.toml
rm -f Cargo.toml.bak
# Sanity check: all four lines must now carry the registry marker. This
# catches formatting drift in Cargo.toml before it becomes a confusing
# publish-time error.
missing=0
for crate in "${CRATES[@]}"; do
if ! grep -Eq "^${crate} = \{.*registry = \"local-dev\".*\}$" Cargo.toml; then
err "Failed to patch workspace dep for ${crate}"
missing=1
fi
done
if (( missing )); then
err "One or more workspace deps did not match the expected format"
err "Expected: <crate> = { path = \"...\", version = \"...\" }"
err "Current state:"
grep -nE "^(iggy|iggy-cli|iggy_binary_protocol|iggy_common) = " Cargo.toml >&2 || true
exit 1
fi
# ---------------------------------------------------------------------------
# 4. Publish the four crates to local-dev in topological order
# ---------------------------------------------------------------------------
# --no-verify: the PR build pipeline already compiles the workspace; this
# job only checks the publish path. Skipping verify means we
# don't need the transitive crates.io dep graph to be locally
# resolvable.
# --allow-dirty: we just patched Cargo.toml; the working tree is dirty by
# design. The cleanup trap reverts the patch on exit.
for crate in "${CRATES[@]}"; do
log "Publishing ${crate} to local-dev"
if ! cargo publish \
-p "$crate" \
--registry local-dev \
--token ci \
--allow-dirty \
--no-verify
then
err "Publish failed for ${crate}"
exit 2
fi
done
# ---------------------------------------------------------------------------
# 5. Cross-check: every crate's tarball must actually exist on the registry
# ---------------------------------------------------------------------------
log "Verifying all four crates appear in the local registry"
# crates-index path layout for 4+ char names: "<first-2>/<next-2>/<name>".
# Every published iggy crate is >=4 chars, so the shorter-name buckets
# (1/, 2/, 3/<c>/) from the full layout are intentionally not handled.
for crate in "${CRATES[@]}"; do
lc="$crate" # relies on CRATES entries being lowercase (see definition above)
rel="${lc:0:2}/${lc:2:2}/$lc"
if [[ ! -f "$REGISTRY_DIR/$rel" ]]; then
err "Crate ${crate} missing from local registry (expected at $rel)"
ls -la "$REGISTRY_DIR" >&2 || true
exit 2
fi
done
log "All four crates published successfully to local-dev"
log "Registry directory: $REGISTRY_DIR (will be cleaned up)"
exit 0