Skip to content

Commit 6232f8e

Browse files
committed
feat: automated GitHub Actions release workflow for PyPI publishing (Teradata#314)
* updating the version number * Automating push to pypi
1 parent 039ecb4 commit 6232f8e

4 files changed

Lines changed: 205 additions & 263 deletions

File tree

.github/workflows/release.yml

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*.*.*"
7+
workflow_dispatch:
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.ref }}
11+
cancel-in-progress: false
12+
13+
jobs:
14+
build:
15+
name: Build & Verify
16+
runs-on: ubuntu-latest
17+
outputs:
18+
version: ${{ steps.version.outputs.version }}
19+
steps:
20+
- uses: actions/checkout@v4
21+
22+
- uses: astral-sh/setup-uv@v4
23+
with:
24+
enable-cache: true
25+
26+
- uses: actions/setup-python@v5
27+
with:
28+
python-version: "3.11"
29+
30+
- name: Extract version from pyproject.toml
31+
id: version
32+
run: |
33+
VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
34+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
35+
36+
- name: Build artifacts
37+
run: uv build --no-cache
38+
39+
- name: Verify metadata and README
40+
run: uvx twine check dist/*
41+
42+
- name: Test wheel locally
43+
run: |
44+
uvx ./dist/teradata_mcp_server-${{ steps.version.outputs.version }}-py3-none-any.whl \
45+
teradata_mcp_server --version
46+
47+
- uses: actions/upload-artifact@v4
48+
with:
49+
name: dist
50+
path: dist/
51+
retention-days: 7
52+
53+
create-github-release:
54+
name: Create GitHub Release
55+
needs: build
56+
runs-on: ubuntu-latest
57+
permissions:
58+
contents: write
59+
steps:
60+
- uses: actions/checkout@v4
61+
62+
- uses: actions/download-artifact@v4
63+
with:
64+
name: dist
65+
path: dist/
66+
67+
- name: Create GitHub Release
68+
env:
69+
GH_TOKEN: ${{ github.token }}
70+
run: |
71+
gh release create "${{ github.ref_name }}" \
72+
--title "v${{ needs.build.outputs.version }}" \
73+
--generate-notes \
74+
dist/*
75+
76+
publish-test-pypi:
77+
name: Publish to TestPyPI
78+
needs: [build, create-github-release]
79+
runs-on: ubuntu-latest
80+
environment: test-pypi
81+
steps:
82+
- uses: actions/download-artifact@v4
83+
with:
84+
name: dist
85+
path: dist/
86+
87+
- name: Upload to TestPyPI
88+
run: uvx twine upload --repository testpypi dist/*
89+
env:
90+
TWINE_USERNAME: __token__
91+
TWINE_PASSWORD: ${{ secrets.TEST_PYPI_TOKEN }}
92+
93+
- name: Smoke test from TestPyPI
94+
run: |
95+
uvx --no-cache \
96+
--index-url https://test.pypi.org/simple \
97+
--extra-index-url https://pypi.org/simple \
98+
--index-strategy unsafe-best-match \
99+
"teradata-mcp-server==${{ needs.build.outputs.version }}" --version
100+
101+
publish-pypi:
102+
name: Publish to PyPI
103+
needs: [build, publish-test-pypi]
104+
runs-on: ubuntu-latest
105+
environment: pypi
106+
steps:
107+
- uses: actions/download-artifact@v4
108+
with:
109+
name: dist
110+
path: dist/
111+
112+
- name: Upload to PyPI
113+
run: uvx twine upload dist/*
114+
env:
115+
TWINE_USERNAME: __token__
116+
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
117+
118+
- name: Smoke test from PyPI
119+
run: |
120+
uvx --no-cache "teradata-mcp-server==${{ needs.build.outputs.version }}" --version

docs/developer_guide/DEVELOPER_GUIDE.md

Lines changed: 83 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -460,21 +460,93 @@ npx modelcontextprotocol/inspector
460460

461461
## Build, Test, and Publish
462462

463-
We build with **uv**, test locally (wheel), then push to **TestPyPI** before PyPI.
464-
The examples below use the Twine utility.
463+
We build with **uv** and publish via a GitHub Actions release workflow. Releases are not created on every commit — a release is triggered explicitly by pushing a git tag.
465464

466465
### Versions
467466
- The CLI reads its version from package metadata (`importlib.metadata`).
468467
- **Bump only in `pyproject.toml`** (do not hardcode in code).
469468
- You **cannot overwrite** an existing version on PyPI/TestPyPI — always increment.
470469

471-
### 1) Build artifacts
470+
### Automated release workflow
471+
472+
The release workflow is defined in [`.github/workflows/release.yml`](../../.github/workflows/release.yml) and runs the following stages in order:
473+
474+
```
475+
build & verify → create GitHub Release → publish TestPyPI → (approval) → publish PyPI
476+
```
477+
478+
Each stage must succeed before the next starts. The `publish-pypi` job requires manual approval via the `pypi` GitHub Environment — this gives you time to verify the TestPyPI package before it goes to production.
479+
480+
#### One-time GitHub setup
481+
482+
> **VPN required** — you must be connected to the Teradata VPN to access the GitHub repository settings and to generate or retrieve API tokens from PyPI and TestPyPI.
483+
484+
**Secrets** — add these in *Settings → Secrets and variables → Actions*:
485+
486+
| Secret | Where to get it |
487+
|---|---|
488+
| `TEST_PYPI_TOKEN` | [test.pypi.org](https://test.pypi.org) → Account → API tokens |
489+
| `PYPI_TOKEN` | [pypi.org](https://pypi.org) → Account → API tokens |
490+
491+
If you have previously published manually using Twine, your tokens are already stored locally in `~/.pypirc` — copy the `password` values from there rather than generating new ones.
492+
493+
**Environments** — create two environments in *Settings → Environments*:
494+
- `test-pypi` — no protection rules required
495+
- `pypi` — add yourself (or your team) as a **Required reviewer** to enable the approval gate
496+
497+
#### Triggering a release
498+
499+
Releases are grouped across multiple commits. When you are ready to ship:
500+
501+
```bash
502+
# 1. Bump the version in pyproject.toml, commit, and push to main as normal
503+
git add pyproject.toml
504+
git commit -m "chore: bump version to 0.2.3"
505+
git push
506+
507+
# 2. Tag the commit — pushing the tag fires the release workflow
508+
git tag v0.2.3
509+
git push origin v0.2.3
510+
```
511+
512+
The tag must match the version in `pyproject.toml`. The workflow reads the version from `pyproject.toml` at build time and uses it throughout.
513+
514+
#### Approving a production release
515+
516+
After the TestPyPI publish and smoke test succeed, the workflow pauses before publishing to production PyPI and waits for a human to approve. Here is what happens:
517+
518+
1. GitHub sends you an **email notification** and a bell notification in the GitHub UI
519+
2. Open the Actions run — you will see a yellow banner: *"This workflow is waiting for a review before continuing"*
520+
3. Click **Review deployments**, tick the `pypi` environment checkbox, and click **Approve and deploy**
521+
4. The `publish-pypi` job starts and the package is published to production PyPI
522+
523+
If something looks wrong (e.g., the TestPyPI smoke test installed the wrong version), click **Reject** instead — nothing is published to production and the run is cancelled. Fix the issue, bump the version, and start a new release.
524+
525+
> **Note:** Only the GitHub users configured as Required reviewers on the `pypi` environment can approve. The reviewer should confirm the TestPyPI smoke test passed before approving.
526+
527+
#### Re-running a failed publish
528+
529+
If a publish step fails (e.g., a transient network error), use the **Run workflow** button on the [Actions tab](https://github.com/Teradata/teradata-mcp-server/actions/workflows/release.yml) to re-trigger without creating a new tag.
530+
531+
#### What the workflow does
532+
533+
1. **Build & Verify** — runs `uv build --no-cache`, verifies metadata with `uvx twine check`, and smoke-tests the wheel locally
534+
2. **Create GitHub Release** — creates a GitHub Release for the tag with auto-generated release notes and attaches the wheel and tarball as downloadable assets
535+
3. **Publish to TestPyPI** — uploads with `uvx twine upload --repository testpypi`, then verifies installation with `uvx`
536+
4. **Approval gate** — workflow pauses; reviewer approves in the GitHub UI after checking the TestPyPI package
537+
5. **Publish to PyPI** — uploads with `uvx twine upload`, then runs a final smoke test from production PyPI
538+
539+
### Manual publish (fallback)
540+
541+
If you need to publish outside of CI, the steps below replicate what the workflow does.
542+
543+
#### 1) Build artifacts
472544
```bash
473545
uv build --no-cache
474546
# Produces dist/teradata_mcp_server-<ver>-py3-none-any.whl and .tar.gz
475547
```
476548

477-
### 2) Test the wheel locally (no install)
549+
#### 2) Test the wheel locally (no install)
478550
```bash
479551
# Run the installed console entry point from the wheel
480552
uvx ./dist/teradata_mcp_server-<ver>-py3-none-any.whl teradata_mcp_server --version
@@ -484,12 +556,12 @@ uv tool install --reinstall ./dist/teradata_mcp_server-<ver>-py3-none-any.whl
484556
~/.local/bin/teradata-mcp-server --help
485557
```
486558

487-
### 3) Verify metadata/README
559+
#### 3) Verify metadata/README
488560
```bash
489561
uvx twine check dist/*
490562
```
491563

492-
### 4) Publish to **TestPyPI** (dress rehearsal)
564+
#### 4) Publish to **TestPyPI** (dress rehearsal)
493565
```bash
494566
# Upload
495567
uvx twine upload --repository testpypi dist/*
@@ -505,15 +577,15 @@ Notes:
505577
- `--index-strategy unsafe-best-match` lets uv take our package from TestPyPI and other deps from PyPI.
506578
- Use `--no-cache` to avoid stale wheels.
507579

508-
### 5) Publish to **PyPI**
580+
#### 5) Publish to **PyPI**
509581
```bash
510582
uvx twine upload dist/*
511583
```
512-
If you see `File already exists`, it is either:
513-
- You haven't bumped the the version in `pyproject.toml`. Do so, rebuild, and upload again.
514-
- You have prior builds in the ./dist directory. Remove prior or be specify the exact version (eg. `uvx twine upload dist/*1.4.0*`)
584+
If you see `File already exists`, either:
585+
- You haven't bumped the version in `pyproject.toml`. Do so, rebuild, and upload again.
586+
- You have prior builds in the `./dist` directory. Remove them or specify the exact version (e.g., `uvx twine upload dist/*0.2.3*`).
515587

516-
### 6) Post‑publish smoke test
588+
#### 6) Post‑publish smoke test
517589
```bash
518590
# One‑off run
519591
uvx "teradata-mcp-server==<ver>" --version

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
[project]
22
name = "teradata-mcp-server"
3-
version = "0.2.2"
3+
version = "0.2.3"
4+
45
description = "Model Context Protocol (MCP) server for Teradata, Community edition"
56
readme = {file = "README.md", content-type = "text/markdown"}
67
license = {text = "MIT"}

0 commit comments

Comments
 (0)