Skip to content

Commit 6a71e84

Browse files
committed
refactor CI/CD for automatic releases
1 parent 06ebd95 commit 6a71e84

File tree

4 files changed

+179
-54
lines changed

4 files changed

+179
-54
lines changed

.github/workflows/release.yml

Lines changed: 44 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,77 @@
1-
name: Test, Release, and Publish
1+
name: Release
22

33
on:
44
push:
5-
branches:
6-
- 'dev-create-release'
7-
# tags:
8-
# - 'v*'
9-
workflow_dispatch: # allows manual triggering of the workflow from the GitHub Actions tab
5+
tags:
6+
- 'v*' # Trigger only on version tags (e.g., v1.0.0)
7+
workflow_dispatch: # allows manual triggering of the workflow from the GitHub Actions tab
108

119
jobs:
1210
# 1. TEST JOB: Runs Pytest
1311
test:
14-
name: Run Unit Tests
12+
uses: ./.github/workflows/test.yml # Reuse the test workflow defined in test.yml
13+
14+
# 2. BUILD: Only runs if 'test' passes
15+
build:
16+
needs: test
1517
runs-on: ubuntu-latest
1618
steps:
17-
- name: Checkout code
18-
uses: actions/checkout@v4
19+
- uses: actions/checkout@v4
20+
with:
21+
fetch-depth: 0 # Required for versioning tools to see tags
1922

2023
- name: Set up Python
2124
uses: actions/setup-python@v5
2225
with:
23-
python-version: '3.8.12' # to match pyproject.tom exactly
26+
python-version: '3.8.12' # to match pyproject.toml exactly
2427

25-
- name: Install dependencies
28+
- name: Build
2629
run: |
27-
python -m pip install --upgrade pip setuptools wheel
28-
# Install the package in editable mode with all dependencies
29-
pip install -e ".[dev]"
30+
pip install build
31+
python -m build .
3032
31-
- name: Run Pytest
32-
run: pytest
33+
- name: Upload build artifacts
34+
uses: actions/upload-artifact@v4
35+
with:
36+
name: dist
37+
path: dist/
3338

34-
# 2. RELEASE JOB: Only runs if 'test' passes
35-
release:
36-
name: Create GitHub Release
37-
needs: test
39+
# 3. GITHUB RELEASE: Creates the release on GitHub and attaches the build files
40+
github-release:
41+
needs: build
3842
runs-on: ubuntu-latest
3943
permissions:
40-
contents: write
44+
contents: write # Needed to create releases and upload assets
4145
steps:
42-
- name: Checkout code
43-
uses: actions/checkout@v4
46+
- name: Download build artifacts
47+
uses: actions/download-artifact@v4
48+
with:
49+
name: dist
50+
path: dist/
4451

45-
- name: Create Release
52+
- name: Create GitHub Release
4653
uses: softprops/action-gh-release@v2
4754
with:
48-
# tag_name: ${{ github.ref_name }}
49-
tag_name: dev-build-${{ github.run_number }} # Use a unique tag for each release
50-
# name: Release ${{ github.ref_name }}
51-
name: Dev Build ${{ github.run_number }}
52-
# body: "Automated release for version ${{ github.ref_name }} (Tests Passed)"
53-
body: "Automated dev release from branch 'dev-create-release' (Commit: ${{ github.sha }})"
54-
draft: false
55-
# prerelease: false
56-
prerelease: true # Mark as pre-release since it's from a dev branch
55+
files: dist/*
56+
generate_release_notes: true # Automatically generate release notes based on commits since the last release
57+
draft: false # Set to true if you want to create a draft release instead of publishing immediately
58+
prerelease: false # Set to true if this is a pre-release (e.g., alpha, beta)
5759

58-
# 3. PUBLISH JOB: Only runs if 'release' passes
60+
61+
# 4. PUBLISH: Only runs if 'build' passes
5962
publish:
60-
name: Build and Publish to PyPI
61-
needs: release
63+
needs: [build, github-release] # Ensure both build and GitHub release steps succeed before publishing
6264
runs-on: ubuntu-latest
65+
# Optional: Use environment protection rule for manual approval before publishing
6366
environment: pypi
6467
permissions:
65-
id-token: write
68+
id-token: write # Needed for authentication with PyPI using GitHub Actions
6669
steps:
67-
- name: Checkout code
68-
uses: actions/checkout@v4
69-
70-
- name: Set up Python
71-
uses: actions/setup-python@v5
70+
- name: Download build artifacts
71+
uses: actions/download-artifact@v4
7272
with:
73-
python-version: '3.8.12' # to match pyproject.tom exactly
74-
75-
- name: Install build tools
76-
run: pip install build
77-
78-
- name: Build binary wheel and source tarball
79-
# Note: This will still use the version in pyproject.toml
80-
# To test PyPI repeatedly, you'd need to change the version in the file
81-
run: python -m build
73+
name: dist
74+
path: dist/
8275

8376
- name: Publish to PyPI
84-
# We use 'continue-on-error' for dev branch PyPI pushes
85-
# because PyPI will reject duplicate versions.
86-
continue-on-error: true
8777
uses: pypa/gh-action-pypi-publish@release/v1

.github/workflows/test.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
branches:
6+
- '**' # Trigger on any branch push
7+
pull_request:
8+
branches:
9+
- '**' # Trigger on any pull request
10+
workflow_call: # CRITICAL: Allows this workflow to be called by others
11+
12+
jobs:
13+
test:
14+
name: Run Tests
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Checkout code
18+
uses: actions/checkout@v4
19+
20+
- name: Set up Python
21+
uses: actions/setup-python@v5
22+
with:
23+
python-version: '3.8.12'
24+
25+
- name: Install dependencies
26+
run: |
27+
python -m pip install --upgrade pip setuptools wheel
28+
pip install -e ".[dev]"
29+
30+
- name: Run Pytest
31+
run: pytest -v

CONTRIBUTING.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Contributing
2+
3+
## Repository Download
4+
5+
```sh
6+
git clone git@github.com:sandialabs/pytribeam.git
7+
```
8+
9+
## Virtual Environment
10+
11+
From within the `pytribeam` directory, create a virtual environment. A virtual environment is a self-contained directory that contains a specific Python installation, along with additional packages. It allows users to create isolated environments for different projects. This ensures that dependencies and libraries do not interfere with each other.
12+
13+
Create a virtual environment with either `pip` or `uv`. `pip` is already included with Python. `uv` must be [installed](https://docs.astral.sh/uv/getting-started/installation/). `uv` is 10 to 100 times faster than `pip`.
14+
15+
```sh
16+
cd pytribeam
17+
18+
# (a) pip method, or
19+
python -m venv .venv
20+
21+
# (b) uv method
22+
uv venv
23+
24+
# both methods
25+
source .venv/bin/activate # bash
26+
source .venv/bin/activate.fish # fish shell
27+
```
28+
29+
30+
Install the code in editable form,
31+
32+
```sh
33+
# (a) pip method, or
34+
pip install -e .[dev]
35+
36+
# (b) uv method
37+
uv pip install -e .[dev]
38+
```
39+
40+
## CI/CD
41+
42+
We separate the concerns of test, build, release, and publish throughout the `.github/workflows/` files.
43+
44+
* **Test (Verification)**
45+
* **Purpose:** To ensure that the code is functional and hasn't introduced regressions (broken existing features).
46+
* **What happens:** Automated tools like `pytest` run your unit and integration tests. It often includes "linting" (checking code style) and type-checking.
47+
* **Key Outcome:** Confidence. If this stage fails, the process stops immediately, preventing broken code from ever reaching a user.
48+
* **Build (Packaging)**
49+
* **Purpose:** To transform your "human-readable" source code into "machine-installable" artifacts.
50+
* **What happens:** Tools (like `python -m build`) bundle your code into standard formats, such as a Wheel (`.whl`) or a Source Distribution (`.tar.gz`).
51+
* **Key Outcome:** Portability. You now have a single file (an "artifact") that contains everything needed to install your library on any compatible system.
52+
* **Release (Documentation & Tagging)**
53+
* **Purpose:** To create an official "point-in-time" snapshot of the project for project management and users.
54+
* **What happens:** A permanent Git tag (like v1.0.0) is assigned to a specific commit. A GitHub Release page is generated with a Changelog (i.e., What's New?) and the build artifacts are attached to it as "Release Assets."
55+
* **Key Outcome:** Traceability. It provides a clear history of the project's evolution and a stable place for users to download specific versions.
56+
* **Publish (Distribution)**
57+
* **Purpose:** To make the software easily available to the global ecosystem.
58+
* **What happens:** The built artifacts are uploaded to a package registry, such as PyPI (the Python Package Index).
59+
* **Key Outcome:** Accessibility. Once published, anyone in the world can install your software using a simple command like `pip install pytribeam`.
60+
61+
Implementation details:
62+
63+
* The reuse of `test.yml` via a `workflow_call` ensures that test logic is not duplicated.
64+
* **Dependency Chain:** `build` waits for `test`, and publish waits for both `build` and `github-release`.
65+
* **Artifact Integrity:** By building once and downing the artifacts in subsequent jobs, we ensure the exact same files go to GitHub and PyPI.
66+
* **Security:** We use `id-token: write` for PyPI's Trusted Publishing, which is a modern and secure way to handle authentication.
67+
68+
In `release.yml` we have removed the manual `-p ${{ secrets.PYPI_TOKEN }}`. The industry standard is now [**Trusted Publishing**](https://docs.pypi.org/trusted-publishers/). You configure this in your PyPI project settings once, and GitHub Actions authenticates securely without you needing to store and rotate secrets.
69+
70+
To configure Trusted Publishing, you tell PyPI, "Trust any code from this specific GitHub repository and workflow." This removes the need to mange long-lived API tokens or passwords in your secrets.
71+
72+
Step:
73+
74+
* Log into your [PyPI](https://pypi.org) account
75+
* Go your project's **Manage** page (or your accounts **Publishing** settings if you are setting it up for the first time.)
76+
* Look for the **Publishing**tab
77+
* Click **Add new publisher**
78+
* Select **GitHub** as the source
79+
* Enter the following details:
80+
* Owner: sandialabs
81+
* Repository name: pytribeam
82+
* Workflow name: `release.yml` (This must match your filename in your `.github/workflows/` directory))
83+
* Environment name: You can leave this blank or name it `pypi` (if you use it in your YAML). We used `pypi`.
84+
* Click the **Add** button
85+
86+
To create a release:
87+
88+
* Merge the `dev` branch into the `main` branch.
89+
* On the `main` branch, `git tag` and push to `main`, e.g.,
90+
91+
```sh
92+
# Ensure you are on the main branch
93+
git checkout main
94+
git pull
95+
96+
# View existing tags, if any
97+
git tag
98+
99+
# Create the new tag, e.g.,
100+
git tag -a v1.0.0 -m "Release version 1.0.0"
101+
102+
# On the main branch, push the tag to GitHub
103+
git push origin v1.0.0
104+
```

0 commit comments

Comments
 (0)