## Section 12: Intro to Continuous Delivery — Publishing to PyPI - [108. Section introduction](#108.-Section-introduction) - [109. DevOps and Waterfall vs. Agile](#109.-DevOps-and-Waterfall-vs.-Agile) - [110. Publishing to PyPI; Test vs. Prod PyPI](#110.-Publishing-to-PyPI;-Test-vs.-Prod-PyPI) - [111. Generating API tokens for PyPI](#111.-Generating-API-tokens-for-PyPI) - [112. Running twine upload](#112.-Running-twine-upload) - [113. Task runners - part 1 - CMake and Makefile](#113.-Task-runners---part-1---CMake-and-Makefile) - [114. Task runners - part 2 - Alternatives to CMake (Just, PyInvoke, Bash)](#114.-Task-runners---part-2---Alternatives-to-CMake-(Just,-PyInvoke,-Bash)) - [115. Task runners - part 3 - Bash as a task runner](#115.-Task-runners---part-3---Bash-as-a-task-runner) - [116. Publishing to TestPyPI](#116.-Publishing-to-TestPyPI) - [117. Publishing to Prod PyPI; Why we should move deployment to GitHub Actions](#117.-Publishing-to-Prod-PyPI;-Why-we-should-move-deployment-to-GitHub-Actions) ## 108. Section introduction - **DevOps** = Software Development + IT Operations - **Goal**: Build and deliver software efficiently - **Key DevOps practices**: - Trunk-based development (frequent, small commits) - Small PRs (if needed) - CI/CD pipelines - **Course Goal:** - You should be equipped as a **DevOps engineer** - You’ll understand not only software development, but also **publishing and delivery workflows** - Example implementation: publish a Python package to PyPI ## 109. DevOps and Waterfall vs. Agile - **Waterfall Project Management** - Traditional method used in early software development. - **Problem**: Doesn't handle feedback or uncertainty well. - Often results in **building the wrong product** due to inaccurate user assumptions. - **Agile Project Management** - **Modern alternative** to Waterfall. - Converts the linear waterfall steps into a **repeating cycle**. - **Validated learning**: Ship fast, get feedback, adjust direction. - **Outcome**: Avoids wasting time on unused or irrelevant features. - **Agile in Practice** - Common Agile frameworks: - **Scrum**: 4-week development cycles (sprints) with planning, daily standups, retrospectives. - **Kanban**: Visual task tracking with "To Do", "In Progress", "Testing", "Done" columns. - Tools: - **Jira** for task management (controversial). - **GitHub Actions**, **Jenkins**, **Bamboo** for automation. - **DevOps Infinity Loop** - DevOps is about **fast, reliable, and repeatable software delivery**. - Loop includes: - **Plan → Code → Build → Test → Release → Deploy → Operate → Monitor** - Loop repeats continuously. - **Goal**: Deliver small changes often, efficiently, and with quality. - **DevOps vs Agile** - **Agile** = methodology for building the right thing through iteration. - **DevOps** = practice for building and shipping software efficiently. - **CI + CD** are part of DevOps. - GitHub Actions help automate both development (left side) and delivery (right side). ## 110. Publishing to PyPI; Test vs. Prod PyPI - **What is PyPI?** - **PyPI** stands for *Python Package Index* – the central repository where Python packages are published and shared. - Developers and users can browse, download, and install packages via `pip`. - **How PyPI Versions Work** - Each release on PyPI is **immutable**: - Once a version (e.g., `1.0.0`) is published, it **cannot be replaced**. - If there's an issue, the version must be **yanked** (marked as unusable), not overwritten. - **Yanked versions**: - **A yanked version on PyPI is a released version of a Python package that is marked as not recommended for use, but not completely deleted.** - Used when a release has major issues (e.g., bugs, security vulnerabilities). - Still listed in release history, but **no longer installable via pip**. - **Your Deployment Strategy** 1. **First deploy to Test PyPI** to ensure the package uploads and installs correctly. 2. **Only after success**, publish to the real(produciton) PyPI. 3. Avoid releasing broken packages and filling your release history with yanked versions. ## 111. Generating API tokens for PyPI - **Step 1: Register Accounts** - **Register separately** on: - [https://test.pypi.org](https://test.pypi.org/) (TestPyPI) - [https://pypi.org](https://pypi.org/) (Production PyPI) - **Step 2: This Is Important** - You **cannot proceed** with package publishing steps unless you: - Have valid credentials. - Have generated **API tokens** for each environment. - These tokens are required for **automated publishing pipelines**. - **Step 3: Generate API Token on TestPyPI** - Log in to your [TestPyPI account](https://test.pypi.org/manage/account/). - Go to **Account Settings → API Tokens → Add API Token**. - Be aware: - **Broad scope tokens** are powerful – if leaked, they compromise *all* your packages. - For sensitive environments (e.g., companies), use private PyPI or SSH installs. - **Step 4: Secure Your Token** - Save the token in a **secure, private location**: - Examples: Password manager (e.g., LastPass secure note), `.env` file (excluded from Git), or secret vault. - **Do NOT store the token in your Git repository.** - **Step 5: Using the Token** - Tool used: **twine** (officially recommended for package publishing). - Format for authentication: - **Username**: `__token__` - **Password**: Your generated token ## 112. Running twine upload - **Tools Used** - [**twine**](https://twine.readthedocs.io/) – recommended CLI for uploading Python packages to PyPI/TestPyPI. - [**build**](https://pypa-build.readthedocs.io/) – used to build source distributions (`.tar.gz`) and wheels (`.whl`). - ### **Steps to Publish with twine** 1. **Install twine** ```bash pip install twine ``` 2. **Clean `dist/` folder** (optional but recommended) ```bash rm -rf dist/ ``` 3. **Build the package** ```bash pip install build python -m build ``` - This creates `.tar.gz` and `.whl` files in the `dist/` folder. 4. **Upload to TestPyPI** ```bash twine upload --repository testpypi dist/* ``` - When prompted: - **Username**: `__token__` - **Password**: Your **Test PyPI API token** - **Common Issues** - **Authentication prompt**: Manual input of token is required here, but **not suitable for automation**. - **Package name conflict**: If a package with your name already exists on PyPI/TestPyPI, you'll get an error (e.g., “not allowed to upload to project”). - **Solution**: Use a **globally unique** package name. - **Tip**: Check availability on both [PyPI](https://pypi.org/) and [TestPyPI](https://test.pypi.org/). - **Recommended Practices** - For official guidance, consult: [https://packaging.python.org](https://packaging.python.org/) - This site reflects the latest **PEPs** and canonical Python packaging practices. ## 113. Task runners - part 1 - CMake and Makefile - **Why We Need a Task Runner** - **Definition**: **Task runners are tools that automate repetitive development tasks, such as testing, building, linting, or packaging your code**. - Projects often rely on many CLI tools (e.g., `pre-commit`, `flake8`, `pytest`, `mypy`, `twine`, `build`, `coverage`, `sphinx`, `docker`, etc.). - Each tool has **many arguments and config options**, making onboarding difficult. - Task runners help: - **Document commonly used commands** - **Reduce cognitive load** for contributors - **Standardize development workflows** - Simplify **open source contributions** and **internal team adoption** - **What is a Makefile?** - A `Makefile` is a widely used **task runner** that allows developers to define CLI tasks. - Syntax: ```makefile Makefile install: echo hello ``` Run with: ```bash make install ``` - **Advantages of Makefile** - **Portable**: Preinstalled or easily installable on most Linux/macOS systems. - Can be run with a simple `make <task>` command. - Useful for simple, one-liner development tasks. - **Reproducibility**: Helps set up consistent dev environments with commands like `make install`. - **Drawbacks of Makefile** - **Tab-sensitive syntax**: Must use tabs (not spaces) to indent task commands. - Causes confusing errors like `missing separator`. - **Difficult variable handling**: - Variables can behave unpredictably. - Variables declared inside task bodies don’t persist across lines. - **Poor Bash compatibility**: - Not actually Bash—defaults to `sh` which lacks common features. - Multi-line commands, conditionals, and pipelines are fragile. - **Limited support for logic and expressions**: - Complex logic becomes hard to maintain or debug. - **Weak error messages** and steep learning curve. - Bash already has enough gotchas—Makefile adds more on top. - **Summary** - Keep Bash scripts **simple** and avoid clever tricks. - Use `Makefile` for **simple, portable, reproducible** tasks. - **For anything more complex, consider using other task runners or Bash scripts**. ## 114. Task runners - part 2 - Alternatives to CMake (Just, PyInvoke, Bash) - **Problems with Makefiles** - **Hard to learn** and full of idiosyncrasies. - **Can't accept arguments** for targets (e.g., `make test myfile.py` isn't supported). - Complex logic (variables, conditionals, multiline commands) is **fragile** and error-prone. - Still, it’s: - **Extremely portable** - Comes pre-installed on most UNIX-like systems - Supports **dependency graphs** (e.g., `build: install` runs `install` before `build`) - **Alternatives to Makefiles** - [**Just**](https://github.com/casey/just) - [**Invoke**](pyinvoke.org) - **Poetry / Tox / Earthly** - **Drawbacks of All These Alternatives** - Most require installation steps. - Varying learning curves. - None match the **universality** of `make`. - **A Minimalist Approach: The "Task File"** - Proposed by **Adrian Cooney** as a simple, Bash-based task runner. - Just a single `.sh` script (`run.sh`) with named functions like: ```bash build() { python -m build } test() { pytest } ``` - To run a task: `./run.sh build` or `source run.sh && build` - **Benefits**: - **Highly portable** - **No dependencies** - Easy to understand, modify, and extend - Works well with **GitHub Copilot** or other AI auto-completion - **Conclusion** - While tools like `make`, `just`, and `invoke` have strengths, **plain Bash may be best for simplicity and portability**. - For this course, the instructor will use a **Bash-based `run.sh` task file** to manage project commands. ## 115. Task runners - part 3 - Bash as a task runner - **Purpose of Task Runner (run.sh)** - A **task runner** helps manage common development commands in one place. - Prevents developers from memorizing or mistyping long CLI commands. - Offers a portable, familiar way to manage tasks like `install`, `build`, `lint`, and `publish`. - **Basic Setup of `run.sh`** - Make the script executable: `chmod +x run.sh`. - Use `"$@"` to **dynamically** pass command-line arguments to functions. - Default function is set to `help`, showing available tasks if no input is given. - **Script Improvements** - Added `set -e`: causes the script to exit immediately if any command fails. - Optionally add `set -x` to print every command before execution (useful for debugging). - Replaced hardcoded `./` with `${DIR}` to ensure relative paths work even if script is run from elsewhere. - **Tasks Implemented** 1. **`install`** - Upgrades `pip` - Installs project in editable mode with dev dependencies - Uses `${DIR}` for consistent project root path 2. **`build`** - Runs `python -m build --sdist --wheel` using the build CLI 3. **`publish:test`** - Publishes to TestPyPI via `twine upload --repository testpypi dist/*` - Still requires manual username/token entry 4. **`lint`** - Runs `pre-commit run --all-files` - Adds `pre-commit` to the `static-code` extras group in `pyproject.toml` - **pyproject.toml Modifications** - Introduced extra groups like `[project.optional-dependencies]` for: - `test` - `release` (includes `build`, `twine`) - `static-code` (includes `pre-commit`) - Combined all extras under `[dev]` for comprehensive local installs - **Makefile Integration** - Optional: create a minimal `Makefile` that wraps `run.sh` commands. ``` install: bash run.sh install ``` - Provides familiar interface (`make install`) while delegating logic to `run.sh`. - **Why This Works Well** - Avoids Makefile limitations (e.g., lack of argument passing, tab sensitivity). - Keeps commands organized, editable, and extensible. - Users only need Bash — no new tooling or installs. ## 116. Publishing to TestPyPI - **Objective** - Automate publishing a Python package to **TestPyPI** without human input using a `run.sh` task runner. - **twine Automation Setup** - `twine upload` supports: - `-username __token__` - `-password YOUR_TOKEN` - `-non-interactive` to disable CLI prompts. - Use `dist/*` to upload both source and wheel distributions. - Use backslashes for multi-line Bash commands (no trailing spaces!). - **Avoid Hardcoding Secrets** - Never hardcode tokens in scripts—**severe security risk**. - Use environment variables to securely manage sensitive info. - **Use `.env` File (Not Committed to Git)** - Add sensitive tokens like: ``` TEST_PYPI_TOKEN=your-token-here ``` - Ensure `.env` is ignored via `.gitignore`: ```bash *.env *env ```` - **Load `.env` in Bash Script** - Use a Bash function: ```bash function load_dot_env { while read -r line; do export "$line"; done < <(grep -v '^#' "$DIR/.env") } ``` - Call `load_dot_env` before running `twine upload`. - **Update `publish:test` Task** - Add environment variable usage: ```bash --username __token__ \ --password "$TEST_PYPI_TOKEN" ``` - Call `load_dot_env` before the command. - **Add a `clean` Task** - Removes build artifacts (`dist/`, `.egg-info`, `build/`) with: ```bash rm -rf dist/ build/ *.egg-info ``` - **Fix Version Conflicts** - If PyPI returns a "file already exists" error: - Update version in `pyproject.toml` (e.g., `0.0.0` → `0.0.1`) - PyPI is **immutable** — you can’t overwrite published versions. - **Add a `release:test` Pipeline** - New function `release:test`: ```bash function release:test { run clean run build run publish:test } ``` - Combines cleanup, build, and upload into one command. - **TestPyPI Success** - Upon successful upload, a URL is printed to view the package on [Test PyPI](https://test.pypi.org/). ## 117. Publishing to Prod PyPI; Why we should move deployment to GitHub Actions - **Add `publish:prod` Task** - Copy `publish:test` and modify: - Change repository to `pypi` - Use `PROD_PYPI_TOKEN` from `.env` - Chose not to generalize with arguments—favored **readability** over abstraction. - **Add `release:prod` Pipeline** - New `release:prod` task: - Runs `release:test` (which includes: `lint`, `clean`, `build`, `publish:test`) - Then runs `publish:prod` - Version bumped to `0.0.3` to avoid PyPI immutability error. - Successfully published to both **Test PyPI** and **Prod PyPI**. - **Why This Matters** - **Scripting releases** reduces reliance on manual, error-prone steps. - Avoids situations where only one dev knows the deployment “magic.” - Increases transparency and reliability. - **Secret Sharing Best Practices** - Do **not** commit `.env` to Git. - Use a **shared password manager** (e.g., LastPass, 1Password, Vault). - GitHub secrets are useful but **not ideal** as your only source of truth (you can’t view them once set). - **Pitfalls of Local Deployment** - Local publishing relies on: - Having correct `.env` - Being present (on vacation, sick, quit?) - Breaks **auditing**, **team sharing**, and **security controls**. - **Why Move to GitHub Actions** - Ensures: - **CI/CD consistency** - **Logs and audit trails** - **No lost secrets** when devs leave - **Controlled rollouts** (with optional approvals)