Skip to content

Instantly share code, notes, and snippets.

@kekyo
Last active May 29, 2025 07:25
Show Gist options
  • Select an option

  • Save kekyo/680809245c73f7a46266bf459cf0ef8c to your computer and use it in GitHub Desktop.

Select an option

Save kekyo/680809245c73f7a46266bf459cf0ef8c to your computer and use it in GitHub Desktop.
Simple NuGet package publisher using curl instead of `dotnet nuget push`.
#!/bin/bash
# curl-nuget-push.sh
# Alternative curl script for dotnet nuget push
# May 29, 2025
# License: CC0
# https://gist.github.com/kekyo/680809245c73f7a46266bf459cf0ef8c
set -euo pipefail
# Default settings
DEFAULT_SERVER="https://api.nuget.org"
DEFAULT_TIMEOUT=300
DEFAULT_PROTOCOL="http1.1"
DEFAULT_API_VERSION="auto"
# Show usage
show_usage() {
cat << EOF
Usage: $0 <package.nupkg> <api-key> [options]
Arguments:
package.nupkg NuGet package file
api-key API authentication key
Options:
-s, --server URL NuGet server endpoint URL (default: $DEFAULT_SERVER)
-t, --timeout SEC Timeout in seconds (default: $DEFAULT_TIMEOUT)
-p, --protocol PROTO HTTP protocol (http1.1|http2) (default: $DEFAULT_PROTOCOL)
-a, --api-version VER API version (v2|v3|auto) (default: $DEFAULT_API_VERSION)
-v, --verbose Verbose output
-h, --help Show this help
Examples:
$0 MyPackage.1.0.0.nupkg "MY-API-KEY"
$0 MyPackage.1.0.0.nupkg "MY-API-KEY" --server "https://package.example.com/api/v2/package"
$0 MyPackage.1.0.0.nupkg "MY-API-KEY" --server "https://nexus.example.com/repository/nuget" --api-version v3
$0 MyPackage.1.0.0.nupkg "MY-API-KEY" --protocol http1.1 --verbose
EOF
}
# Log functions
log_info() {
echo "INFO: $*" >&2
}
log_success() {
echo "SUCCESS: $*" >&2
}
log_warning() {
echo "WARNING: $*" >&2
}
log_error() {
echo "ERROR: $*" >&2
}
# NuGet v3 Push multipart form creation
create_multipart_form() {
local package_file="$1"
local temp_form_file="$2"
local boundary="----FormBoundary$(date +%s)$(shuf -i 1000-9999 -n 1)"
{
echo "--$boundary"
echo "Content-Disposition: form-data; name=\"package\"; filename=\"$(basename "$package_file")\""
echo "Content-Type: application/octet-stream"
echo ""
cat "$package_file"
echo ""
echo "--$boundary--"
} > "$temp_form_file"
echo "$boundary"
}
# Parameter parsing
PACKAGE_FILE=""
API_KEY=""
SERVER_URL="$DEFAULT_SERVER"
TIMEOUT="$DEFAULT_TIMEOUT"
PROTOCOL="$DEFAULT_PROTOCOL"
API_VERSION="$DEFAULT_API_VERSION"
VERBOSE=false
while [[ $# -gt 0 ]]; do
case $1 in
-s|--server)
SERVER_URL="$2"
shift 2
;;
-t|--timeout)
TIMEOUT="$2"
shift 2
;;
-p|--protocol)
PROTOCOL="$2"
shift 2
;;
-a|--api-version)
API_VERSION="$2"
shift 2
;;
-v|--verbose)
VERBOSE=true
shift
;;
-h|--help)
show_usage
exit 0
;;
-*)
log_error "Unknown option: $1"
show_usage
exit 1
;;
*)
if [[ -z "$PACKAGE_FILE" ]]; then
PACKAGE_FILE="$1"
elif [[ -z "$API_KEY" ]]; then
API_KEY="$1"
else
log_error "Too many arguments"
show_usage
exit 1
fi
shift
;;
esac
done
# Required parameter check
if [[ -z "$PACKAGE_FILE" ]] || [[ -z "$API_KEY" ]]; then
log_error "Missing required arguments"
show_usage
exit 1
fi
# File existence check
if [[ ! -f "$PACKAGE_FILE" ]]; then
log_error "Package file not found: $PACKAGE_FILE"
exit 1
fi
# Protocol configuration
case "$PROTOCOL" in
http1.1)
PROTOCOL_OPTION="--http1.1"
;;
http2)
PROTOCOL_OPTION="--http2"
;;
*)
log_error "Invalid protocol: $PROTOCOL (use http1.1 or http2)"
exit 1
;;
esac
# API version configuration
DETECTED_API_VERSION=""
case "$API_VERSION" in
v2)
DETECTED_API_VERSION="v2"
;;
v3)
DETECTED_API_VERSION="v3"
;;
auto)
# Auto-detect based on server URL patterns
if [[ "$SERVER_URL" == */repository/nuget* ]] || [[ "$SERVER_URL" == */v3/* ]] || [[ "$SERVER_URL" == */api/v3* ]]; then
DETECTED_API_VERSION="v3"
else
DETECTED_API_VERSION="v2"
fi
;;
*)
log_error "Invalid API version: $API_VERSION (use v2, v3, or auto)"
exit 1
;;
esac
# Use server URL as-is (no automatic path modification)
ENDPOINT="$SERVER_URL"
# Temporary files
RESPONSE_FILE=$(mktemp)
HEADERS_FILE=$(mktemp)
FORM_FILE=""
if [[ "$DETECTED_API_VERSION" == "v3" ]]; then
FORM_FILE=$(mktemp)
fi
# Cleanup function
cleanup() {
rm -f "$RESPONSE_FILE" "$HEADERS_FILE" "$FORM_FILE"
}
trap cleanup EXIT
# Display package information
PACKAGE_SIZE=$(stat -c%s "$PACKAGE_FILE" 2>/dev/null || stat -f%z "$PACKAGE_FILE" 2>/dev/null || echo "unknown")
log_info "Package: $PACKAGE_FILE (${PACKAGE_SIZE} bytes)"
log_info "Endpoint: $ENDPOINT"
log_info "Protocol: $PROTOCOL"
log_info "API Version: $DETECTED_API_VERSION"
log_info "Timeout: ${TIMEOUT}s"
# Execute curl
log_info "Uploading package..."
# Basic curl options
CURL_OPTS=(
"$PROTOCOL_OPTION"
-H "X-NuGet-ApiKey: $API_KEY"
-H "User-Agent: curl-nuget-client/1.0"
--max-time "$TIMEOUT"
--connect-timeout 30
-w '%{http_code}'
-o "$RESPONSE_FILE"
-D "$HEADERS_FILE"
-s
)
if [[ "$VERBOSE" == true ]]; then
CURL_OPTS+=(-v)
fi
# API version specific processing
if [[ "$DETECTED_API_VERSION" == "v3" ]]; then
# NuGet v3 API: multipart/form-data
boundary=$(create_multipart_form "$PACKAGE_FILE" "$FORM_FILE")
CURL_OPTS+=(
-X POST
-H "Content-Type: multipart/form-data; boundary=$boundary"
--data-binary "@$FORM_FILE"
)
log_info "Using NuGet v3 API (multipart form)"
else
# NuGet v2 API: application/octet-stream
CURL_OPTS+=(
-X PUT
-H "Content-Type: application/octet-stream"
--data-binary "@$PACKAGE_FILE"
)
log_info "Using NuGet v2 API (binary stream)"
fi
HTTP_CODE=$(curl "${CURL_OPTS[@]}" "$ENDPOINT")
CURL_EXIT_CODE=$?
# Check curl execution result
if [[ $CURL_EXIT_CODE -ne 0 ]]; then
log_error "curl failed with exit code $CURL_EXIT_CODE"
case $CURL_EXIT_CODE in
6)
log_error "Could not resolve host"
;;
7)
log_error "Failed to connect to host"
;;
28)
log_error "Operation timeout"
;;
*)
log_error "curl error (see man curl for details)"
;;
esac
exit 1
fi
# Get response details
RESPONSE_BODY=""
if [[ -f "$RESPONSE_FILE" ]]; then
RESPONSE_BODY=$(cat "$RESPONSE_FILE")
fi
SERVER_HEADER=""
if [[ -f "$HEADERS_FILE" ]]; then
SERVER_HEADER=$(grep -i "^server:" "$HEADERS_FILE" 2>/dev/null | cut -d' ' -f2- | tr -d '\r\n' || echo "unknown")
fi
# Result evaluation
case "$HTTP_CODE" in
200|201|202)
log_success "Package uploaded successfully (HTTP $HTTP_CODE)"
if [[ "$VERBOSE" == true ]]; then
log_info "Server: $SERVER_HEADER"
log_info "API Version: $DETECTED_API_VERSION"
fi
exit 0
;;
400)
log_error "Bad Request (HTTP 400)"
if [[ -n "$RESPONSE_BODY" ]]; then
echo "Response: $RESPONSE_BODY" >&2
fi
# Suggest using v3 API if multipart required error is detected
if [[ "$RESPONSE_BODY" == *"Multipart request required"* ]] && [[ "$DETECTED_API_VERSION" == "v2" ]]; then
log_warning "Server requires multipart format, but using v2 endpoint"
log_info "Try running with --api-version v3"
fi
exit 1
;;
401)
log_error "Unauthorized (HTTP 401)"
log_error "Check your API key"
exit 1
;;
403)
log_error "Forbidden (HTTP 403)"
log_error "Insufficient permissions"
exit 1
;;
409)
log_warning "Package already exists (HTTP 409)"
log_info "Package version already published"
exit 0
;;
413)
log_error "Package too large (HTTP 413)"
log_error "Package size: ${PACKAGE_SIZE} bytes"
exit 1
;;
500|502|503|504)
log_error "Server error (HTTP $HTTP_CODE)"
log_error "Try again later"
exit 1
;;
*)
log_error "Unexpected response (HTTP $HTTP_CODE)"
if [[ -n "$RESPONSE_BODY" ]]; then
echo "Response: $RESPONSE_BODY" >&2
fi
exit 1
;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment