|
1 | 1 | #!/usr/bin/env bash
|
2 | 2 |
|
3 |
| -# Exit immediately if a command exits with a non-zero status |
4 |
| -set -e |
| 3 | +# Script Name: release_package.sh |
| 4 | +# Description: Automates the process of building, checking, and releasing a Python package to PyPI or TestPyPI. |
| 5 | +# |
| 6 | +# Usage: ./release_package.sh [options] |
| 7 | +# |
| 8 | +# Options: |
| 9 | +# -h, --help Show this help message and exit. |
| 10 | +# -t, --test Upload the package to TestPyPI. |
| 11 | +# -p, --production Upload the package to PyPI. |
| 12 | +# -s, --skip-tests Skip running tests before building. |
| 13 | +# -v, --version VERSION Specify the package version to release. |
| 14 | +# -n, --name NAME Specify the package name. |
| 15 | +# -c, --config FILE Specify a configuration file with default settings. |
| 16 | +# -e, --env VENV_PATH Specify a virtual environment to use. |
| 17 | +# -i, --interpreter PATH Specify the Python interpreter to use. |
| 18 | +# --dry-run Perform a dry run without uploading. |
| 19 | +# |
| 20 | +# Examples: |
| 21 | +# ./release_package.sh --test |
| 22 | +# ./release_package.sh --production --version 1.0.0 --name my-package |
| 23 | +# ./release_package.sh --config release.conf --production |
| 24 | + |
| 25 | +set -euo pipefail |
5 | 26 |
|
6 | 27 | # Color codes for output
|
7 | 28 | GREEN='\033[0;32m'
|
| 29 | +RED='\033[0;31m' |
8 | 30 | NC='\033[0m' # No Color
|
9 | 31 |
|
10 |
| -# Function to ensure a command exists, if not, install it |
11 |
| -function ensure_installed() { |
12 |
| - if ! command -v "$1" &> /dev/null; then |
13 |
| - echo -e "${GREEN}$1 not found. Installing...${NC}" |
14 |
| - pip install "$1" |
| 32 | +# Default values |
| 33 | +UPLOAD_TO_TEST=false |
| 34 | +UPLOAD_TO_PROD=false |
| 35 | +SKIP_TESTS=false |
| 36 | +PACKAGE_VERSION="" |
| 37 | +PACKAGE_NAME="" |
| 38 | +CONFIG_FILE="" |
| 39 | +VENV_PATH="" |
| 40 | +PYTHON_INTERPRETER="python" |
| 41 | +DRY_RUN=false |
| 42 | + |
| 43 | +# Function to display help message |
| 44 | +function show_help() { |
| 45 | + grep '^#' "$0" | cut -c 4- |
| 46 | + exit 0 |
| 47 | +} |
| 48 | + |
| 49 | +# Function to ensure a Python package is installed |
| 50 | +function ensure_python_package() { |
| 51 | + local package="$1" |
| 52 | + if ! "$PYTHON_INTERPRETER" -c "import $package" &> /dev/null; then |
| 53 | + echo -e "${GREEN}Installing Python package '$package'...${NC}" |
| 54 | + "$PYTHON_INTERPRETER" -m pip install --upgrade "$package" |
| 55 | + fi |
| 56 | +} |
| 57 | + |
| 58 | +# Function to run tests |
| 59 | +function run_tests() { |
| 60 | + if [ -f "setup.py" ]; then |
| 61 | + echo -e "${GREEN}Running tests...${NC}" |
| 62 | + "$PYTHON_INTERPRETER" setup.py test |
| 63 | + elif [ -f "pytest.ini" ] || [ -d "tests" ]; then |
| 64 | + echo -e "${GREEN}Running pytest...${NC}" |
| 65 | + ensure_python_package pytest |
| 66 | + "$PYTHON_INTERPRETER" -m pytest |
| 67 | + else |
| 68 | + echo -e "${RED}No tests found.${NC}" |
15 | 69 | fi
|
16 | 70 | }
|
17 | 71 |
|
18 | 72 | # Function to build the package
|
19 | 73 | function build_package() {
|
20 | 74 | echo -e "${GREEN}Building distribution packages...${NC}"
|
21 |
| - ensure_installed build |
22 |
| - python -m build |
| 75 | + ensure_python_package build |
| 76 | + rm -rf dist/ build/ |
| 77 | + "$PYTHON_INTERPRETER" -m build |
| 78 | +} |
| 79 | + |
| 80 | +# Function to check the built package |
| 81 | +function check_package() { |
| 82 | + echo -e "${GREEN}Checking distribution packages...${NC}" |
| 83 | + ensure_python_package twine |
| 84 | + "$PYTHON_INTERPRETER" -m twine check dist/* |
23 | 85 | }
|
24 | 86 |
|
25 | 87 | # Function to upload to TestPyPI
|
26 | 88 | function upload_testpypi() {
|
27 | 89 | echo -e "${GREEN}Uploading to TestPyPI...${NC}"
|
28 |
| - twine upload --repository testpypi dist/* |
29 |
| - echo -e "${GREEN}Package uploaded to TestPyPI.${NC}" |
30 |
| - echo -e "${GREEN}You can install it using:${NC}" |
31 |
| - echo "pip install --index-url https://test.pypi.org/simple/ --no-deps your-package-name" |
| 90 | + if [ "$DRY_RUN" = true ]; then |
| 91 | + echo -e "${GREEN}[Dry Run] Skipping upload to TestPyPI.${NC}" |
| 92 | + else |
| 93 | + "$PYTHON_INTERPRETER" -m twine upload --repository testpypi dist/* |
| 94 | + echo -e "${GREEN}Package uploaded to TestPyPI.${NC}" |
| 95 | + if [ -n "$PACKAGE_NAME" ]; then |
| 96 | + echo -e "${GREEN}You can install it using:${NC}" |
| 97 | + echo "pip install --index-url https://test.pypi.org/simple/ --no-deps $PACKAGE_NAME" |
| 98 | + fi |
| 99 | + fi |
32 | 100 | }
|
33 | 101 |
|
34 | 102 | # Function to upload to PyPI
|
35 | 103 | function upload_pypi() {
|
36 | 104 | echo -e "${GREEN}Uploading to PyPI...${NC}"
|
37 |
| - twine upload dist/* |
38 |
| - echo -e "${GREEN}Package uploaded to PyPI.${NC}" |
| 105 | + if [ "$DRY_RUN" = true ]; then |
| 106 | + echo -e "${GREEN}[Dry Run] Skipping upload to PyPI.${NC}" |
| 107 | + else |
| 108 | + "$PYTHON_INTERPRETER" -m twine upload dist/* |
| 109 | + echo -e "${GREEN}Package uploaded to PyPI.${NC}" |
| 110 | + fi |
39 | 111 | }
|
40 | 112 |
|
41 | 113 | # Function to check for uncommitted changes
|
42 | 114 | function check_git_status() {
|
43 |
| - if [ -n "$(git status --porcelain)" ]; then |
44 |
| - echo -e "${GREEN}Uncommitted changes detected. Please commit or stash them before releasing.${NC}" |
45 |
| - exit 1 |
| 115 | + if git rev-parse --is-inside-work-tree > /dev/null 2>&1; then |
| 116 | + if [ -n "$(git status --porcelain)" ]; then |
| 117 | + echo -e "${RED}Uncommitted changes detected. Please commit or stash them before releasing.${NC}" |
| 118 | + exit 1 |
| 119 | + fi |
| 120 | + else |
| 121 | + echo -e "${RED}Not a git repository. Skipping git status check.${NC}" |
46 | 122 | fi
|
47 | 123 | }
|
48 | 124 |
|
49 | 125 | # Function to confirm the package version
|
50 | 126 | function confirm_version() {
|
51 |
| - PACKAGE_VERSION=$(python setup.py --version) |
52 |
| - echo -e "${GREEN}Current package version is: $PACKAGE_VERSION${NC}" |
| 127 | + local version |
| 128 | + if [ -n "$PACKAGE_VERSION" ]; then |
| 129 | + version="$PACKAGE_VERSION" |
| 130 | + else |
| 131 | + version=$("$PYTHON_INTERPRETER" setup.py --version 2>/dev/null || echo "Unknown") |
| 132 | + fi |
| 133 | + echo -e "${GREEN}Current package version is: $version${NC}" |
53 | 134 | read -p "Is this the correct version to upload? (y/n): " confirm_version
|
54 | 135 | if [ "$confirm_version" != "y" ]; then
|
55 |
| - echo -e "${GREEN}Please update your package version before proceeding.${NC}" |
| 136 | + echo -e "${RED}Please update your package version before proceeding.${NC}" |
| 137 | + exit 1 |
| 138 | + fi |
| 139 | +} |
| 140 | + |
| 141 | +# Function to parse the configuration file |
| 142 | +function parse_config() { |
| 143 | + if [ -f "$CONFIG_FILE" ]; then |
| 144 | + echo -e "${GREEN}Loading configuration from $CONFIG_FILE...${NC}" |
| 145 | + while IFS='=' read -r key value; do |
| 146 | + case "$key" in |
| 147 | + upload_to_test) UPLOAD_TO_TEST="$value" ;; |
| 148 | + upload_to_prod) UPLOAD_TO_PROD="$value" ;; |
| 149 | + skip_tests) SKIP_TESTS="$value" ;; |
| 150 | + package_version) PACKAGE_VERSION="$value" ;; |
| 151 | + package_name) PACKAGE_NAME="$value" ;; |
| 152 | + python_interpreter) PYTHON_INTERPRETER="$value" ;; |
| 153 | + venv_path) VENV_PATH="$value" ;; |
| 154 | + dry_run) DRY_RUN="$value" ;; |
| 155 | + esac |
| 156 | + done < "$CONFIG_FILE" |
| 157 | + else |
| 158 | + echo -e "${RED}Configuration file $CONFIG_FILE not found.${NC}" |
56 | 159 | exit 1
|
57 | 160 | fi
|
58 | 161 | }
|
59 | 162 |
|
| 163 | +# Parse command-line arguments |
| 164 | +function parse_args() { |
| 165 | + while [[ $# -gt 0 ]]; do |
| 166 | + case $1 in |
| 167 | + -h|--help) |
| 168 | + show_help |
| 169 | + ;; |
| 170 | + -t|--test) |
| 171 | + UPLOAD_TO_TEST=true |
| 172 | + shift |
| 173 | + ;; |
| 174 | + -p|--production) |
| 175 | + UPLOAD_TO_PROD=true |
| 176 | + shift |
| 177 | + ;; |
| 178 | + -s|--skip-tests) |
| 179 | + SKIP_TESTS=true |
| 180 | + shift |
| 181 | + ;; |
| 182 | + -v|--version) |
| 183 | + PACKAGE_VERSION="$2" |
| 184 | + shift 2 |
| 185 | + ;; |
| 186 | + -n|--name) |
| 187 | + PACKAGE_NAME="$2" |
| 188 | + shift 2 |
| 189 | + ;; |
| 190 | + -c|--config) |
| 191 | + CONFIG_FILE="$2" |
| 192 | + shift 2 |
| 193 | + ;; |
| 194 | + -e|--env) |
| 195 | + VENV_PATH="$2" |
| 196 | + shift 2 |
| 197 | + ;; |
| 198 | + -i|--interpreter) |
| 199 | + PYTHON_INTERPRETER="$2" |
| 200 | + shift 2 |
| 201 | + ;; |
| 202 | + --dry-run) |
| 203 | + DRY_RUN=true |
| 204 | + shift |
| 205 | + ;; |
| 206 | + *) |
| 207 | + echo -e "${RED}Unknown option: $1${NC}" |
| 208 | + show_help |
| 209 | + ;; |
| 210 | + esac |
| 211 | + done |
| 212 | +} |
| 213 | + |
| 214 | +# Function to activate virtual environment if specified |
| 215 | +function activate_virtualenv() { |
| 216 | + if [ -n "$VENV_PATH" ]; then |
| 217 | + if [ -d "$VENV_PATH" ]; then |
| 218 | + source "$VENV_PATH/bin/activate" |
| 219 | + PYTHON_INTERPRETER="$VENV_PATH/bin/python" |
| 220 | + echo -e "${GREEN}Using virtual environment at $VENV_PATH${NC}" |
| 221 | + else |
| 222 | + echo -e "${RED}Virtual environment at $VENV_PATH not found.${NC}" |
| 223 | + exit 1 |
| 224 | + fi |
| 225 | + fi |
| 226 | +} |
| 227 | + |
60 | 228 | # Main script execution
|
61 | 229 | function main() {
|
| 230 | + parse_args "$@" |
| 231 | + |
| 232 | + # Parse configuration file if specified |
| 233 | + if [ -n "$CONFIG_FILE" ]; then |
| 234 | + parse_config |
| 235 | + fi |
| 236 | + |
| 237 | + # Activate virtual environment if specified |
| 238 | + activate_virtualenv |
| 239 | + |
62 | 240 | # Check for uncommitted changes
|
63 | 241 | check_git_status
|
64 | 242 |
|
65 | 243 | # Ensure required tools are installed
|
66 |
| - ensure_installed twine |
| 244 | + ensure_python_package pip |
| 245 | + ensure_python_package setuptools |
| 246 | + ensure_python_package wheel |
| 247 | + ensure_python_package twine |
| 248 | + |
| 249 | + # Run tests unless skipped |
| 250 | + if [ "$SKIP_TESTS" = false ]; then |
| 251 | + run_tests |
| 252 | + fi |
67 | 253 |
|
68 | 254 | # Build the package
|
69 | 255 | build_package
|
70 | 256 |
|
71 |
| - # Optionally upload to TestPyPI |
72 |
| - read -p "Do you want to upload to TestPyPI first? (y/n): " upload_test |
73 |
| - if [ "$upload_test" = "y" ]; then |
74 |
| - upload_testpypi |
75 |
| - fi |
| 257 | + # Check the built package |
| 258 | + check_package |
76 | 259 |
|
77 |
| - # Confirm version before uploading to PyPI |
| 260 | + # Confirm version before uploading |
78 | 261 | confirm_version
|
79 | 262 |
|
80 |
| - # Upload to PyPI |
81 |
| - read -p "Ready to upload to PyPI. Proceed? (y/n): " upload_pypi |
82 |
| - if [ "$upload_pypi" = "y" ]; then |
| 263 | + # Upload to TestPyPI or PyPI |
| 264 | + if [ "$UPLOAD_TO_TEST" = true ]; then |
| 265 | + upload_testpypi |
| 266 | + fi |
| 267 | + |
| 268 | + if [ "$UPLOAD_TO_PROD" = true ]; then |
83 | 269 | upload_pypi
|
84 |
| - else |
85 |
| - echo -e "${GREEN}Upload to PyPI aborted.${NC}" |
| 270 | + fi |
| 271 | + |
| 272 | + if [ "$UPLOAD_TO_TEST" = false ] && [ "$UPLOAD_TO_PROD" = false ]; then |
| 273 | + echo -e "${GREEN}Build completed. Packages are available in the 'dist/' directory.${NC}" |
86 | 274 | fi
|
87 | 275 |
|
88 | 276 | echo -e "${GREEN}Script completed.${NC}"
|
89 | 277 | }
|
90 | 278 |
|
91 | 279 | # Run the main function
|
92 |
| -main |
| 280 | +main "$@" |
0 commit comments